local T={Client={}}local q="\080\108\097\121\101\114\068\097\116\097"local H=60 local r="\110\097\109\101\061\037\115\059\115\099\111\112\101\061\037\115"local n="\080\108\097\121\101\114\070\097\105\108\101\100"local s="\071\108\111\098\097\108\070\097\105\108\101\100"local W="\067\117\115\116\111\109\070\097\105\108\101\100"local a="\070\097\105\108\101\100"local F local b={}local U={}local h local z={}function T.GetPlayerCache(H,T)local r=b[T]if not r then if T.UserId>0 then r=F.new(tostring(T.UserId),q)else r=F.new()end b[T]=r r.Failed:Connect(function(q,r,s)H:FireEvent(n,T,q,r,s)H:FireClientEvent(a,T,q,r,s)end)end return r end function T.GetCustomCache(H,T,q)local n=r:format(T,q)local s=U[n]if not s then s=F.new(T,q)U[n]=s s.Failed:Connect(function(r,n,s)H:FireEvent(W,T,q,r,n,s)end)end return s end function T.Set(r,T,q,H)(r:GetPlayerCache(T)):Set(q,H)end function T.Get(H,T,q)return(H:GetPlayerCache(T)):Get(q)end function T.Remove(H,T,q)(H:GetPlayerCache(T)):Remove(q)end function T.OnUpdate(r,T,q,H)return(r:GetPlayerCache(T)):OnUpdate(q,H)end function T.SetGlobal(H,T,q)h:Set(T,q)end function T.GetGlobal(q,T)return h:Get(T)end function T.RemoveGlobal(q,T)h:Remove(T)end function T.OnUpdateGlobal(H,T,q)return h:OnUpdate(T,q)end function T.SetCustom(n,T,q,H,r)(n:GetCustomCache(T,q)):Set(H,r)end function T.GetCustom(r,T,q,H)return(r:GetCustomCache(T,q)):Get(H)end function T.RemoveCustom(r,T,q,H)(r:GetCustomCache(T,q)):Remove(H)end function T.OnUpdateCustom(n,T,q,H,r)return(n:GetCustomCache(T,q)):OnUpdate(H,r)end function T.Flush(q,T)(q:GetPlayerCache(T)):FlushAll()end function T.FlushKey(H,T,q)(H:GetPlayerCache(T)):Flush(q)end function T.FlushGlobal(q,T)h:Flush(T)end function T.FlushAllGlobal(T)h:FlushAll()end function T.FlushCustom(r,T,q,H)(r:GetCustomCache(T,q)):Flush(H)end function T.FlushAllCustom(r,T,q,H)(r:GetCustomCache(T,q)):FlushAll()end function T.FlushAll(T)local q={}for T,H in pairs(b)do q[#q+1]=H H.Lock=true end for T,H in pairs(U)do q[#q+1]=H H.Lock=true end for T,q in pairs(q)do q.Lock=false q:FlushAll()end h:FlushAll()end function T.FlushAllConcurrent(T)local q=coroutine.running()local H=0 local r=0 for T in pairs(b)do H=H+1 end for T in pairs(U)do H=H+1 end if H==0 then return end local function n()r=r+1 if r==H then assert(coroutine.resume(q))end end for T,q in pairs(b)do spawn(function()q:FlushAllConcurrent()n()end)end for T,q in pairs(U)do spawn(function()q:FlushAll()n()end)end h:FlushAll()coroutine.yield()end function T.BindToClose(q,T)assert(type(T)=="\102\117\110\099\116\105\111\110","\065\114\103\117\109\101\110\116 \035\049 \109\117\115\116 \098\101 \097 \102\117\110\099\116\105\111\110")z[#z+1]=T end function T.AutoSaveLoop(T)while not T.GameClosing do T:FlushAll()wait(H)end end function T.Client.Get(H,T,q)return H.Server:Get(T,q)end function T.Start(T)T.GameClosing=false local function q()local T=#z if T==0 then return end local q=Instance.new("\066\105\110\100\097\098\108\101\069\118\101\110\116")local H=0 for r,n in pairs(z)do spawn(function()local r,s=pcall(n)if not r then warn("\066\105\110\100\084\111\067\108\111\115\101 \102\117\110\099\116\105\111\110 \101\114\114\111\114\101\100\058 "..tostring(s))end H=H+1 if H==T then q:Fire()end end)end q.Event:Wait()q:Destroy()end local function r(q)if T.GameClosing then return end T:Flush(q)wait(5)if b[q]then b[q]:Destroy()b[q]=nil end end local function n()T.GameClosing=true q()T:FlushAll()end;(game:GetService("\080\108\097\121\101\114\115")).PlayerRemoving:Connect(r)game:BindToClose(n)delay(H,function()T:AutoSaveLoop()end)end function T.Init(T)T:RegisterEvent(n)T:RegisterEvent(s)T:RegisterEvent(W)T:RegisterClientEvent(a)F=T.Modules.DataStoreCache h=F.new("\103\108\111\098\097\108","\103\108\111\098\097\108")h.Failed:Connect(function(q,H,r)T:FireEvent(s,q,H,r)end)end return T
-- Data Service
-- Crazyman32
-- February 3, 2017

--[[
	
	Server:

		PLAYER DATA METHODS:
	
			DataService:Set(player, key, value)
			DataService:Get(player, key)
			DataService:Remove(player, key)
			DataService:OnUpdate(player, key, callback)
			DataService:Flush(player)
			DataService:FlushKey(player, key)
			DataService:FlushAll()
			DataService:FlushAllConcurrent()

		GLOBAL DATA METHODS:
				
			DataService:SetGlobal(key, value)
			DataService:GetGlobal(key)
			DataService:RemoveGlobal(key)
			DataService:OnUpdateGlobal(key, callback)
			DataService:FlushGlobal(key)
			DataService:FlushAllGlobal()

		CUSTOM DATA METHODS:
		
			DataService:SetCustom(name, scope, key, value)
			DataService:GetCustom(name, scope, key)
			DataService:RemoveCustom(name, scope, key)
			DataService:OnUpdateCustom(name, scope, key, callback)
			DataService:FlushCustom(name, scope, key)
			DataService:FlushAllCustom(name, scope, key)
		
		GAME CLOSING CALLBACK:

			DataService:BindToClose(callbackFunction)

		EVENTS:

			DataService.PlayerFailed(player, method, key, errorMessage)
			DataService.GlobalFailed(method, key, errorMessage)
			DataService.CustomFailed(name, scope, method, key, errorMessage)
		
	
	Client:
	
		DataService:Get(key)
		DataService.Failed(method, key, errorMessage)
	
--]]



local DataService = {
	Client = {};
}

local SCOPE = "PlayerData"
local AUTOSAVE_INTERVAL = 60

local NAME_SCOPE_KEY_FORMAT = "name=%s;scope=%s"

local PLAYER_FAILED_EVENT = "PlayerFailed"
local GLOBAL_FAILED_EVENT = "GlobalFailed"
local CUSTOM_FAILED_EVENT = "CustomFailed"
local CLIENT_FAILED_EVENT = "Failed"

local Cache

local playerCaches = {}
local customCaches = {}
local globalCache

local boundToCloseFuncs = {}


function DataService:GetPlayerCache(player)
	local cache = playerCaches[player]
	if (not cache) then
		if (player.UserId > 0) then
			cache = Cache.new(tostring(player.UserId), SCOPE)
		else
			-- Guest/offline cache (not linked to DataStore):
			cache = Cache.new()
		end
		playerCaches[player] = cache
		cache.Failed:Connect(function(method, key, errMsg)
			self:FireEvent(PLAYER_FAILED_EVENT, player, method, key, errMsg)
			self:FireClientEvent(CLIENT_FAILED_EVENT, player, method, key, errMsg)
		end)
	end
	return cache
end


function DataService:GetCustomCache(name, scope)
	local nameScopeKey = NAME_SCOPE_KEY_FORMAT:format(name, scope)
	local cache = customCaches[nameScopeKey]
	if (not cache) then
		cache = Cache.new(name, scope)
		customCaches[nameScopeKey] = cache
		cache.Failed:Connect(function(method, key, errMsg)
			self:FireEvent(CUSTOM_FAILED_EVENT, name, scope, method, key, errMsg)
		end)
	end
	return cache
end


function DataService:Set(player, key, value)
	self:GetPlayerCache(player):Set(key, value)
end


function DataService:Get(player, key)
	return self:GetPlayerCache(player):Get(key)
end


function DataService:Remove(player, key)
	self:GetPlayerCache(player):Remove(key)
end


function DataService:OnUpdate(player, key, callback)
	return self:GetPlayerCache(player):OnUpdate(key, callback)
end


function DataService:SetGlobal(key, value)
	globalCache:Set(key, value)
end


function DataService:GetGlobal(key)
	return globalCache:Get(key)
end


function DataService:RemoveGlobal(key)
	globalCache:Remove(key)
end


function DataService:OnUpdateGlobal(key, callback)
	return globalCache:OnUpdate(key, callback)
end


function DataService:SetCustom(name, scope, key, value)
	self:GetCustomCache(name, scope):Set(key, value)
end


function DataService:GetCustom(name, scope, key)
	return self:GetCustomCache(name, scope):Get(key)
end


function DataService:RemoveCustom(name, scope, key)
	self:GetCustomCache(name, scope):Remove(key)
end


function DataService:OnUpdateCustom(name, scope, key, callback)
	return self:GetCustomCache(name, scope):OnUpdate(key, callback)
end


function DataService:Flush(player)
	self:GetPlayerCache(player):FlushAll()
end


function DataService:FlushKey(player, key)
	self:GetPlayerCache(player):Flush(key)
end


function DataService:FlushGlobal(key)
	globalCache:Flush(key)
end


function DataService:FlushAllGlobal()
	globalCache:FlushAll()
end


function DataService:FlushCustom(name, scope, key)
	self:GetCustomCache(name, scope):Flush(key)
end


function DataService:FlushAllCustom(name, scope, key)
	self:GetCustomCache(name, scope):FlushAll()
end


function DataService:FlushAll()
	-- Collect caches so they can be safely accessed. Because this is a synchronous process,
	-- a cache might be removed from the cache tables during the process, thus causing errors
	-- if simply looping through the original tables.
	local caches = {}
	for _,cache in pairs(playerCaches) do
		caches[#caches + 1] = cache
		cache.Lock = true
	end
	for _,cache in pairs(customCaches) do
		caches[#caches + 1] = cache
		cache.Lock = true
	end
	-- Flush each cache individually:
	for _,cache in pairs(caches) do
		cache.Lock = false
		cache:FlushAll()
	end
	globalCache:FlushAll()
end


function DataService:FlushAllConcurrent()
	local thread = coroutine.running()
	local numCaches = 0
	local numFlushed = 0
	for _ in pairs(playerCaches) do
		numCaches = (numCaches + 1)
	end
	for _ in pairs(customCaches) do
		numCaches = (numCaches + 1)
	end
	if (numCaches == 0) then return end
	local function IncFlushed()
		numFlushed = (numFlushed + 1)
		if (numFlushed == numCaches) then
			assert(coroutine.resume(thread))
		end
	end
	for _,cache in pairs(playerCaches) do
		spawn(function()
			cache:FlushAllConcurrent()
			IncFlushed()
		end)
	end
	for _,cache in pairs(customCaches) do
		spawn(function()
			cache:FlushAll()
			IncFlushed()
		end)
	end
	globalCache:FlushAll()
	coroutine.yield()
end


function DataService:BindToClose(func)
	assert(type(func) == "function", "Argument #1 must be a function")
	boundToCloseFuncs[#boundToCloseFuncs + 1] = func
end


function DataService:AutoSaveLoop()
	while (not self.GameClosing) do
		self:FlushAll()
		wait(AUTOSAVE_INTERVAL)
	end
end


function DataService.Client:Get(player, key)
	return self.Server:Get(player, key)
end


function DataService:Start()
	
	self.GameClosing = false
	
	local function FireBoundToCloseCallbacks()

		--[[ CONCURRENCY DISABLED DUE TO BINDTOCLOSE YIELDING BUG
		local thread = coroutine.running()
		local numBinded = #boundToCloseFuncs
		if (numBinded == 0) then return end
		local numCompleted = 0
		local maxWait = 20
		local start = tick()
		for _,func in pairs(boundToCloseFuncs) do
			coroutine.wrap(function()
				pcall(func)
				numCompleted = (numCompleted + 1)
				if (numCompleted == numBinded) then
					assert(coroutine.resume(thread))
				end
			end)()
		end
		coroutine.yield()
		]]

		-- Temporary patch using BindableEvent instead of coroutines:
		local numBinded = #boundToCloseFuncs
		if (numBinded == 0) then return end
		local bindable = Instance.new("BindableEvent")
		local numCompleted = 0
		for _,func in pairs(boundToCloseFuncs) do
			spawn(function()
				local success, err = pcall(func)
				if (not success) then
					warn("BindToClose function errored: " .. tostring(err))
				end
				numCompleted = (numCompleted + 1)
				if (numCompleted == numBinded) then
					bindable:Fire()
				end
			end)
		end
		bindable.Event:Wait()
		bindable:Destroy()

	end
	
	-- Flush cache:
	local function PlayerRemoving(player)
		if (self.GameClosing) then return end
		self:Flush(player)
		wait(5)
		if (playerCaches[player]) then
			playerCaches[player]:Destroy()
			playerCaches[player] = nil
		end
	end
	
	local function GameClosing()
		self.GameClosing = true
		FireBoundToCloseCallbacks()
		--self:FlushAllConcurrent()
		self:FlushAll()
	end
	
	game:GetService("Players").PlayerRemoving:Connect(PlayerRemoving)
	
	game:BindToClose(GameClosing)
	
	delay(AUTOSAVE_INTERVAL, function()
		self:AutoSaveLoop()
	end)
	
end


function DataService:Init()

	self:RegisterEvent(PLAYER_FAILED_EVENT)
	self:RegisterEvent(GLOBAL_FAILED_EVENT)
	self:RegisterEvent(CUSTOM_FAILED_EVENT)
	self:RegisterClientEvent(CLIENT_FAILED_EVENT)

	Cache = self.Modules.DataStoreCache

	globalCache = Cache.new("global", "global")
	globalCache.Failed:Connect(function(method, key, errMsg)
		self:FireEvent(GLOBAL_FAILED_EVENT, method, key, errMsg)
	end)

end


return DataService
