local O={}local g=require(script.Parent.MockDataStoreUtils)local Y=require(script.Parent.MockDataStoreConstants)local M=game:GetService("\072\116\116\112\083\101\114\118\105\099\101")local D=game:GetService("\080\108\097\121\101\114\115")local k=game:GetService("\082\117\110\083\101\114\118\105\099\101")local h={[Enum.DataStoreRequestType.GetAsync]=Y.BUDGET_GETASYNC,[Enum.DataStoreRequestType.GetSortedAsync]=Y.BUDGET_GETSORTEDASYNC,[Enum.DataStoreRequestType.OnUpdate]=Y.BUDGET_ONUPDATE;[Enum.DataStoreRequestType.SetIncrementAsync]=Y.BUDGET_SETINCREMENTASYNC,[Enum.DataStoreRequestType.SetIncrementSortedAsync]=Y.BUDGET_SETINCREMENTSORTEDASYNC}local X={GlobalDataStore={},DataStore={},OrderedDataStore={}}local z={}local l={}local G={[Enum.DataStoreRequestType.GetAsync]={},[Enum.DataStoreRequestType.GetSortedAsync]={};[Enum.DataStoreRequestType.OnUpdate]={};[Enum.DataStoreRequestType.SetIncrementAsync]={},[Enum.DataStoreRequestType.SetIncrementSortedAsync]={}}local function A()for O,g in pairs(h)do l[O]=g.START end l[Enum.DataStoreRequestType.UpdateAsync]=math.min(l[Enum.DataStoreRequestType.GetAsync],l[Enum.DataStoreRequestType.SetIncrementAsync])end local function J(O,g,M,D)if not Y.BUDGETING_ENABLED then return end local k=g.RATE+D*g.RATE_PLR l[O]=math.min(l[O]+M*k,g.MAX_FACTOR*k)end local function s(O)if not Y.BUDGETING_ENABLED then return end for O,g in pairs(O)do if l[g]then l[g]=math.max(0,l[g]-1)end end l[Enum.DataStoreRequestType.UpdateAsync]=math.min(l[Enum.DataStoreRequestType.GetAsync],l[Enum.DataStoreRequestType.SetIncrementAsync])end local function d(O)if not Y.BUDGETING_ENABLED then return true end for O,g in pairs(O)do if l[g]and l[g]<1 then return false end end return true end local u=false if k:IsServer()then A()task.spawn(function()local O=tick()while task.wait(Y.BUDGET_UPDATE_INTERVAL)do local g=tick()local M=(g-O)/60 O=g local k=#D:GetPlayers()if not u then for O,g in pairs(h)do J(O,g,M,k)end l[Enum.DataStoreRequestType.UpdateAsync]=math.min(l[Enum.DataStoreRequestType.GetAsync],l[Enum.DataStoreRequestType.SetIncrementAsync])end for O,g in pairs(G)do for O=#g,1,-1 do local M=g[O]local D=M.Thread local k=M.Budget local h=M.Key local X=M.Lock local z=M.Cache if not(X and(X[h]or tick()-(z[h]or 0)<Y.WRITE_COOLDOWN))and d(k)then table.remove(g,O)s(k)coroutine.resume(D)end end end end end)game:BindToClose(function()for O,g in pairs(h)do l[O]=math.max(l[O],Y.BUDGET_ONCLOSE_BASE*(g.RATE/Y.BUDGET_BASE))end l[Enum.DataStoreRequestType.UpdateAsync]=math.min(l[Enum.DataStoreRequestType.GetAsync],l[Enum.DataStoreRequestType.SetIncrementAsync])end)end function O.GetGlobalData()return X.GlobalDataStore end function O.GetData(O,g)assert(type(O)=="\115\116\114\105\110\103")assert(type(g)=="\115\116\114\105\110\103")if not X.DataStore[O]then X.DataStore[O]={}end if not X.DataStore[O][g]then X.DataStore[O][g]={}end return X.DataStore[O][g]end function O.GetOrderedData(O,g)assert(type(O)=="\115\116\114\105\110\103")assert(type(g)=="\115\116\114\105\110\103")if not X.OrderedDataStore[O]then X.OrderedDataStore[O]={}end if not X.OrderedDataStore[O][g]then X.OrderedDataStore[O][g]={}end return X.OrderedDataStore[O][g]end function O.GetDataInterface(O)return z[O]end function O.SetDataInterface(O,g)assert(type(O)=="\116\097\098\108\101")assert(type(g)=="\116\097\098\108\101")z[O]=g end function O.GetBudget(O)if Y.BUDGETING_ENABLED then return math.floor(l[O]or 0)else return math.huge end end function O.SetBudget(O,g)assert(type(g)=="\110\117\109\098\101\114")g=math.max(g,0)if O==Enum.DataStoreRequestType.UpdateAsync then l[Enum.DataStoreRequestType.SetIncrementAsync]=g l[Enum.DataStoreRequestType.GetAsync]=g end if l[O]then l[O]=g end end function O.ResetBudget()A()end function O.FreezeBudgetUpdates()u=true end function O.ThawBudgetUpdates()u=false end function O.YieldForWriteLockAndBudget(O,g,M,D,k)assert(type(O)=="\102\117\110\099\116\105\111\110")assert(type(g)=="\115\116\114\105\110\103")assert(type(M)=="\116\097\098\108\101")assert(type(D)=="\116\097\098\108\101")assert(#k>0)local h=k[1]if#G[h]>=Y.THROTTLE_QUEUE_SIZE then return false end O()table.insert(G[h],1,{Key=g,Lock=M;Cache=D;Thread=coroutine.running();Budget=k})coroutine.yield()return true end function O.YieldForBudget(O,g)assert(type(O)=="\102\117\110\099\116\105\111\110")assert(#g>0)local M=g[1]if d(g)then s(g)elseif#G[M]>=Y.THROTTLE_QUEUE_SIZE then return false else O()table.insert(G[M],1,{After=0;Thread=coroutine.running();Budget=g})coroutine.yield()end return true end function O.ExportToJSON()local O={}if next(X.GlobalDataStore)~=nil then O.GlobalDataStore=X.GlobalDataStore end O.DataStore=g.prepareDataStoresForExport(X.DataStore)O.OrderedDataStore=g.prepareDataStoresForExport(X.OrderedDataStore)return M:JSONEncode(O)end local function Q(O,M,D,k,h,X)for O,l in pairs(O)do if type(O)~="\115\116\114\105\110\103"then D(("\037\115\058 \105\103\110\111\114\101\100 \037\115 \062 \037\113 \040\110\097\109\101 \105\115 \110\111\116 \097 \115\116\114\105\110\103\044 \098\117\116 \097 \037\115\041"):format(k,h,tostring(O),typeof(O)))elseif type(l)~="\116\097\098\108\101"then D(("\037\115\058 \105\103\110\111\114\101\100 \037\115 \062 \037\113 \040\115\099\111\112\101 \108\105\115\116 \105\115 \110\111\116 \097 \116\097\098\108\101\044 \098\117\116 \097 \037\115\041"):format(k,h,O,typeof(l)))elseif#O==0 then D(("\037\115\058 \105\103\110\111\114\101\100 \037\115 \062 \037\113 \040\110\097\109\101 \105\115 \097\110 \101\109\112\116\121 \115\116\114\105\110\103\041"):format(k,h,O))elseif#O>Y.MAX_LENGTH_NAME then D(("\037\115\058 \105\103\110\111\114\101\100 \037\115 \062 \037\113 \040\110\097\109\101 \101\120\099\101\101\100\115 \037\100 \099\104\097\114\097\099\116\101\114 \108\105\109\105\116\041"):format(k,h,O,Y.MAX_LENGTH_NAME))else for l,G in pairs(l)do if type(l)~="\115\116\114\105\110\103"then D(("\037\115\058 \105\103\110\111\114\101\100 \037\115 \062 \037\113 \062 \037\113 \040\115\099\111\112\101 \105\115 \110\111\116 \097 \115\116\114\105\110\103\044 \098\117\116 \097 \037\115\041"):format(k,h,O,tostring(l),typeof(l)))elseif type(G)~="\116\097\098\108\101"then D(("\037\115\058 \105\103\110\111\114\101\100 \037\115 \062 \037\113 \062 \037\113 \040\100\097\116\097 \108\105\115\116 \105\115 \110\111\116 \097 \116\097\098\108\101\044 \098\117\116 \097 \037\115\041"):format(k,h,O,l,typeof(G)))elseif#l==0 then D(("\037\115\058 \105\103\110\111\114\101\100 \037\115 \062 \037\113 \062 \037\113 \040\115\099\111\112\101 \105\115 \097\110 \101\109\112\116\121 \115\116\114\105\110\103\041"):format(k,h,O,l))elseif#l>Y.MAX_LENGTH_SCOPE then D(("\037\115\058 \105\103\110\111\114\101\100 \037\115 \062 \037\113 \062 \037\113 \040\115\099\111\112\101 \101\120\099\101\101\100\115 \037\100 \099\104\097\114\097\099\116\101\114 \108\105\109\105\116\041"):format(k,h,O,l,Y.MAX_LENGTH_SCOPE))else if not M[O]then M[O]={}end if not M[O][l]then M[O][l]={}end g.importPairsFromTable(G,M[O][l],z[M[O][l]],D,k,("\037\115 \062 \037\113 \062 \037\113"):format(h,O,l),X)end end end end end function O.ImportFromJSON(O,Y)assert(type(O)=="\116\097\098\108\101")assert(Y==nil or type(Y)=="\098\111\111\108\101\097\110")local M=warn if Y==false then M=function() end end if type(O.GlobalDataStore)=="\116\097\098\108\101"then g.importPairsFromTable(O.GlobalDataStore,X.GlobalDataStore,z[X.GlobalDataStore],M,"\073\109\112\111\114\116\070\114\111\109\074\083\079\078","\071\108\111\098\097\108\068\097\116\097\083\116\111\114\101",false)end if type(O.DataStore)=="\116\097\098\108\101"then Q(O.DataStore,X.DataStore,M,"\073\109\112\111\114\116\070\114\111\109\074\083\079\078","\068\097\116\097\083\116\111\114\101",false)end if type(O.OrderedDataStore)=="\116\097\098\108\101"then Q(O.OrderedDataStore,X.OrderedDataStore,M,"\073\109\112\111\114\116\070\114\111\109\074\083\079\078","\079\114\100\101\114\101\100\068\097\116\097\083\116\111\114\101",true)end end local function Z(O)for g,Y in pairs(O)do O[g]=nil end end function O.ResetData()for O,g in pairs(z)do for O,Y in pairs(g.__data)do g.__data[O]=nil g.__event:Fire(O,nil)end g.__getCache={}g.__writeCache={}g.__writeLock={}if g.__sorted then g.__sorted={}g.__ref={}g.__changed=false end end Z(X.GlobalDataStore)for O,g in pairs(X.DataStore)do for O,g in pairs(g)do Z(g)end end for O,g in pairs(X.OrderedDataStore)do for O,g in pairs(g)do Z(g)end end end return O
--# selene: allow(incorrect_standard_library_use)
--[[
	MockDataStoreManager.lua
	This module does bookkeeping of data, interfaces and request limits used by MockDataStoreService and its sub-classes.

	This module is licensed under APLv2, refer to the LICENSE file or:
	buildthomas/MockDataStoreService/blob/master/LICENSE
]]

local MockDataStoreManager = {}

local Utils = require(script.Parent.MockDataStoreUtils)
local Constants = require(script.Parent.MockDataStoreConstants)
local HttpService = game:GetService("HttpService") -- for json encode/decode
local Players = game:GetService("Players") -- for restoring budgets
local RunService = game:GetService("RunService") -- for checking if running context is on server

local ConstantsMapping = {
	[Enum.DataStoreRequestType.GetAsync] = Constants.BUDGET_GETASYNC;
	[Enum.DataStoreRequestType.GetSortedAsync] = Constants.BUDGET_GETSORTEDASYNC;
	[Enum.DataStoreRequestType.OnUpdate] = Constants.BUDGET_ONUPDATE;
	[Enum.DataStoreRequestType.SetIncrementAsync] = Constants.BUDGET_SETINCREMENTASYNC;
	[Enum.DataStoreRequestType.SetIncrementSortedAsync] = Constants.BUDGET_SETINCREMENTSORTEDASYNC;
}

-- Bookkeeping of all data:
local Data = {
	GlobalDataStore = {};
	DataStore = {};
	OrderedDataStore = {};
}

-- Bookkeeping of all active GlobalDataStore/OrderedDataStore interfaces indexed by data table:
local Interfaces = {}

-- Request limit bookkeeping:
local Budgets = {}

local budgetRequestQueues = {
	[Enum.DataStoreRequestType.GetAsync] = {};
	[Enum.DataStoreRequestType.GetSortedAsync] = {};
	[Enum.DataStoreRequestType.OnUpdate] = {};
	[Enum.DataStoreRequestType.SetIncrementAsync] = {};
	[Enum.DataStoreRequestType.SetIncrementSortedAsync] = {};
}

local function initBudget()
	for requestType, const in pairs(ConstantsMapping) do
		Budgets[requestType] = const.START
	end
	Budgets[Enum.DataStoreRequestType.UpdateAsync] = math.min(
		Budgets[Enum.DataStoreRequestType.GetAsync],
		Budgets[Enum.DataStoreRequestType.SetIncrementAsync]
	)
end

local function updateBudget(req, const, dt, n)
	if not Constants.BUDGETING_ENABLED then
		return
	end
	local rate = const.RATE + n * const.RATE_PLR
	Budgets[req] = math.min(
		Budgets[req] + dt * rate,
		const.MAX_FACTOR * rate
	)
end

local function stealBudget(budget)
	if not Constants.BUDGETING_ENABLED then
		return
	end
	for _, requestType in pairs(budget) do
		if Budgets[requestType] then
			Budgets[requestType] = math.max(0, Budgets[requestType] - 1)
		end
	end
	Budgets[Enum.DataStoreRequestType.UpdateAsync] = math.min(
		Budgets[Enum.DataStoreRequestType.GetAsync],
		Budgets[Enum.DataStoreRequestType.SetIncrementAsync]
	)
end

local function checkBudget(budget)
	if not Constants.BUDGETING_ENABLED then
		return true
	end
	for _, requestType in pairs(budget) do
		if Budgets[requestType] and Budgets[requestType] < 1 then
			return false
		end
	end
	return true
end

local isFrozen = false

if RunService:IsServer() then
	-- Only do budget/throttle updating on server (in case package required on client)

	initBudget()

	task.spawn(function() -- Thread that increases budgets and de-throttles requests periodically
		local lastCheck = tick()
		while task.wait(Constants.BUDGET_UPDATE_INTERVAL) do
			local now = tick()
			local dt = (now - lastCheck) / 60
			lastCheck = now
			local n = #Players:GetPlayers()

			if not isFrozen then
				for requestType, const in pairs(ConstantsMapping) do
					updateBudget(requestType, const, dt, n)
				end
				Budgets[Enum.DataStoreRequestType.UpdateAsync] = math.min(
					Budgets[Enum.DataStoreRequestType.GetAsync],
					Budgets[Enum.DataStoreRequestType.SetIncrementAsync]
				)
			end

			for _, budgetRequestQueue in pairs(budgetRequestQueues) do
				for i = #budgetRequestQueue, 1, -1 do
					local request = budgetRequestQueue[i]

					local thread = request.Thread
					local budget = request.Budget
					local key = request.Key
					local lock = request.Lock
					local cache = request.Cache

					if not (lock and (lock[key] or tick() - (cache[key] or 0) < Constants.WRITE_COOLDOWN)) and checkBudget(budget) then
						table.remove(budgetRequestQueue, i)
						stealBudget(budget)
						coroutine.resume(thread)
					end
				end
			end
		end
	end)

	game:BindToClose(function()
		for requestType, const in pairs(ConstantsMapping) do
			Budgets[requestType] = math.max(
				Budgets[requestType],
				Constants.BUDGET_ONCLOSE_BASE * (const.RATE / Constants.BUDGET_BASE)
			)
		end
		Budgets[Enum.DataStoreRequestType.UpdateAsync] = math.min(
			Budgets[Enum.DataStoreRequestType.GetAsync],
			Budgets[Enum.DataStoreRequestType.SetIncrementAsync]
		)
	end)

end

function MockDataStoreManager.GetGlobalData()
	return Data.GlobalDataStore
end

function MockDataStoreManager.GetData(name, scope)
	assert(type(name) == "string")
	assert(type(scope) == "string")

	if not Data.DataStore[name] then
		Data.DataStore[name] = {}
	end
	if not Data.DataStore[name][scope] then
		Data.DataStore[name][scope] = {}
	end

	return Data.DataStore[name][scope]
end

function MockDataStoreManager.GetOrderedData(name, scope)
	assert(type(name) == "string")
	assert(type(scope) == "string")

	if not Data.OrderedDataStore[name] then
		Data.OrderedDataStore[name] = {}
	end
	if not Data.OrderedDataStore[name][scope] then
		Data.OrderedDataStore[name][scope] = {}
	end

	return Data.OrderedDataStore[name][scope]
end

function MockDataStoreManager.GetDataInterface(data)
	return Interfaces[data]
end

function MockDataStoreManager.SetDataInterface(data, interface)
	assert(type(data) == "table")
	assert(type(interface) == "table")

	Interfaces[data] = interface
end

function MockDataStoreManager.GetBudget(requestType)
	if Constants.BUDGETING_ENABLED then
		return math.floor(Budgets[requestType] or 0)
	else
		return math.huge
	end
end

function MockDataStoreManager.SetBudget(requestType, budget)
	assert(type(budget) == "number")
	budget = math.max(budget, 0)

	if requestType == Enum.DataStoreRequestType.UpdateAsync then
		Budgets[Enum.DataStoreRequestType.SetIncrementAsync] = budget
		Budgets[Enum.DataStoreRequestType.GetAsync] = budget
	end

	if Budgets[requestType] then
		Budgets[requestType] = budget
	end
end

function MockDataStoreManager.ResetBudget()
	initBudget()
end

function MockDataStoreManager.FreezeBudgetUpdates()
	isFrozen = true
end

function MockDataStoreManager.ThawBudgetUpdates()
	isFrozen = false
end

function MockDataStoreManager.YieldForWriteLockAndBudget(callback, key, writeLock, writeCache, budget)
	assert(type(callback) == "function")
	assert(type(key) == "string")
	assert(type(writeLock) == "table")
	assert(type(writeCache) == "table")
	assert(#budget > 0)

	local mainRequestType = budget[1]

	if #budgetRequestQueues[mainRequestType] >= Constants.THROTTLE_QUEUE_SIZE then
		return false -- no room in throttle queue
	end

	callback() -- would i.e. trigger a warning in output

	table.insert(budgetRequestQueues[mainRequestType], 1, {
		Key = key;
		Lock = writeLock;
		Cache = writeCache;
		Thread = coroutine.running();
		Budget = budget;
	})
	coroutine.yield()

	return true
end

function MockDataStoreManager.YieldForBudget(callback, budget)
	assert(type(callback) == "function")
	assert(#budget > 0)

	local mainRequestType = budget[1]

	if checkBudget(budget) then
		stealBudget(budget)
	elseif #budgetRequestQueues[mainRequestType] >= Constants.THROTTLE_QUEUE_SIZE then
		return false -- no room in throttle queue
	else
		callback() -- would i.e. trigger a warning in output

		table.insert(budgetRequestQueues[mainRequestType], 1, {
			After = 0; -- no write lock
			Thread = coroutine.running();
			Budget = budget;
		})
		coroutine.yield()
	end

	return true
end

function MockDataStoreManager.ExportToJSON()
	local export = {}

	if next(Data.GlobalDataStore) ~= nil then -- GlobalDataStore not empty
		export.GlobalDataStore = Data.GlobalDataStore
	end
	export.DataStore = Utils.prepareDataStoresForExport(Data.DataStore) -- can be nil
	export.OrderedDataStore = Utils.prepareDataStoresForExport(Data.OrderedDataStore) -- can be nil

	return HttpService:JSONEncode(export)
end

-- Import into an entire datastore type:
local function importDataStoresFromTable(origin, destination, warnFunc, methodName, prefix, isOrdered)
	for name, scopes in pairs(origin) do
		if type(name) ~= "string" then
			warnFunc(("%s: ignored %s > %q (name is not a string, but a %s)")
				:format(methodName, prefix, tostring(name), typeof(name)))
		elseif type(scopes) ~= "table" then
			warnFunc(("%s: ignored %s > %q (scope list is not a table, but a %s)")
				:format(methodName, prefix, name, typeof(scopes)))
		elseif #name == 0 then
			warnFunc(("%s: ignored %s > %q (name is an empty string)")
				:format(methodName, prefix, name))
		elseif #name > Constants.MAX_LENGTH_NAME then
			warnFunc(("%s: ignored %s > %q (name exceeds %d character limit)")
				:format(methodName, prefix, name, Constants.MAX_LENGTH_NAME))
		else
			for scope, data in pairs(scopes) do
				if type(scope) ~= "string" then
					warnFunc(("%s: ignored %s > %q > %q (scope is not a string, but a %s)")
						:format(methodName, prefix, name, tostring(scope), typeof(scope)))
				elseif type(data) ~= "table" then
					warnFunc(("%s: ignored %s > %q > %q (data list is not a table, but a %s)")
						:format(methodName, prefix, name, scope, typeof(data)))
				elseif #scope == 0 then
					warnFunc(("%s: ignored %s > %q > %q (scope is an empty string)")
						:format(methodName, prefix, name, scope))
				elseif #scope > Constants.MAX_LENGTH_SCOPE then
					warnFunc(("%s: ignored %s > %q > %q (scope exceeds %d character limit)")
						:format(methodName, prefix, name, scope, Constants.MAX_LENGTH_SCOPE))
				else
					if not destination[name] then
						destination[name] = {}
					end
					if not destination[name][scope] then
						destination[name][scope] = {}
					end
					Utils.importPairsFromTable(
						data,
						destination[name][scope],
						Interfaces[destination[name][scope]],
						warnFunc,
						methodName,
						("%s > %q > %q"):format(prefix, name, scope),
						isOrdered
					)
				end
			end
		end
	end
end

function MockDataStoreManager.ImportFromJSON(content, verbose)
	assert(type(content) == "table")
	assert(verbose == nil or type(verbose) == "boolean")

	local warnFunc = warn -- assume verbose as default
	if verbose == false then -- intentional formatting
		warnFunc = function() end
	end

	if type(content.GlobalDataStore) == "table" then
		Utils.importPairsFromTable(
			content.GlobalDataStore,
			Data.GlobalDataStore,
			Interfaces[Data.GlobalDataStore],
			warnFunc,
			"ImportFromJSON",
			"GlobalDataStore",
			false
		)
	end
	if type(content.DataStore) == "table" then
		importDataStoresFromTable(
			content.DataStore,
			Data.DataStore,
			warnFunc,
			"ImportFromJSON",
			"DataStore",
			false
		)
	end
	if type(content.OrderedDataStore) == "table" then
		importDataStoresFromTable(
			content.OrderedDataStore,
			Data.OrderedDataStore,
			warnFunc,
			"ImportFromJSON",
			"OrderedDataStore",
			true
		)
	end
end

local function clearTable(t)
	for i,_ in pairs(t) do
		t[i] = nil
	end
end

function MockDataStoreManager.ResetData()
	for _, interface in pairs(Interfaces) do
		for key, _ in pairs(interface.__data) do
			interface.__data[key] = nil
			interface.__event:Fire(key, nil)
		end
		interface.__getCache = {}
		interface.__writeCache = {}
		interface.__writeLock = {}
		if interface.__sorted then
			interface.__sorted = {};
            interface.__ref = {};
            interface.__changed = false;
		end
	end

	clearTable(Data.GlobalDataStore)

	for _, scopes in pairs(Data.DataStore) do
		for _, data in pairs(scopes) do
			clearTable(data)
		end
	end

	for _, scopes in pairs(Data.OrderedDataStore) do
		for _, data in pairs(scopes) do
			clearTable(data)
		end
	end
end

return MockDataStoreManager
