670 lines
22 KiB
Plaintext
Raw Normal View History

2025-07-16 00:45:20 +08:00
-- // VARIABLES // --
-- | Services | --
local CollectionService = game:GetService("CollectionService")
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
-- | Forward Declarations & Module Table | --
local Core = {}
local Utilities: {DELETED_MARKER: {}}
-- | Internal Configuration Constants | --
local REMOTE_FOLDER_NAME = "DataReplicator_InternalRemotes"
local UPDATE_EVENT_NAME = "DR_Internal_Update"
local REQUEST_FUNC_NAME = "DR_Internal_Request"
local PENDING_BATCH_TAG = "DataReplicator_PendingBatch"
-- | Remote Instances | --
local internalUpdateEvent: RemoteEvent
local internalRequestFunc: RemoteFunction
-- // SERVER-SIDE IMPLEMENTATION // --
if RunService:IsServer() then
-- | Security Configuration Constants | --
local REQUIRE_AUTHORIZATION_CALLBACK = false
local RATE_LIMIT_MAX_REQUESTS_IN_WINDOW = 100
local RATE_LIMIT_WINDOW_SECONDS = 60
local RATE_LIMIT_SUB_WINDOWS_COUNT = 6
-- | Primary Data Storage on Server | --
local replicatedDataStore: { [string]: any } = {}
-- | Mapping for Client Listeners & Obfuscation | --
local clientKeyMaps: { [Player]: { [string]: string } } = {}
local clientRealToObfuscated: { [Player]: { [string]: string } } = {}
-- | Callback for Client Access Authorization | --
local authorizationCallback: ((player: Player, realKey: string) -> boolean)? = nil
-- | Rate Limiting Tracker | --
local playerRequestTracker: { [Player]: { [number]: number } } = {}
local SUB_WINDOW_DURATION = RATE_LIMIT_WINDOW_SECONDS / RATE_LIMIT_SUB_WINDOWS_COUNT
-- | Batching System Variables | --
local playerUpdateBatchQueue: { [Player]: { [string]: any } } = {}
local isBatchProcessingScheduled = false
-- // FUNCTIONS // --
local function getOrCreateObfuscatedKey(player: Player, realKey: string): string
if not clientRealToObfuscated[player] then
clientRealToObfuscated[player] = {}
clientKeyMaps[player] = {}
end
local existingObfuscatedKey = clientRealToObfuscated[player][realKey]
if existingObfuscatedKey then
return existingObfuscatedKey
end
local newObfuscatedKey = Utilities.GenerateObfuscatedKey()
while clientKeyMaps[player][newObfuscatedKey] do
newObfuscatedKey = Utilities.GenerateObfuscatedKey()
end
clientKeyMaps[player][newObfuscatedKey] = realKey
clientRealToObfuscated[player][realKey] = newObfuscatedKey
return newObfuscatedKey
end
local function checkAndUpdateRateLimit(player: Player): boolean
local currentTime = tick()
if not playerRequestTracker[player] then
playerRequestTracker[player] = {}
end
local playerLogs = playerRequestTracker[player]
local oldestValidSubWindowTime = currentTime - RATE_LIMIT_WINDOW_SECONDS
for subWindowTimestamp, _ in pairs(playerLogs) do
if subWindowTimestamp < oldestValidSubWindowTime then
playerLogs[subWindowTimestamp] = nil
end
end
local currentRequestsInWindow = 0
for _, count in pairs(playerLogs) do
currentRequestsInWindow += count
end
if currentRequestsInWindow >= RATE_LIMIT_MAX_REQUESTS_IN_WINDOW then
return false
end
local currentSubWindowKey = math.floor(currentTime / SUB_WINDOW_DURATION) * SUB_WINDOW_DURATION
playerLogs[currentSubWindowKey] = (playerLogs[currentSubWindowKey] or 0) + 1
return true
end
local function addToPlayerBatch(player: Player, obfuscatedKey: string, data: any)
if not playerUpdateBatchQueue[player] then
playerUpdateBatchQueue[player] = {}
end
playerUpdateBatchQueue[player][obfuscatedKey] = data
if not CollectionService:HasTag(player, PENDING_BATCH_TAG) then
CollectionService:AddTag(player, PENDING_BATCH_TAG)
end
if not isBatchProcessingScheduled then
isBatchProcessingScheduled = true
task.defer(function()
isBatchProcessingScheduled = false
local playersWithPendingBatches = CollectionService:GetTagged(PENDING_BATCH_TAG)
for _, p in ipairs(playersWithPendingBatches) do
if typeof(p) == "Instance" and p:IsA("Player") and playerUpdateBatchQueue[p] then
local batchToSend = playerUpdateBatchQueue[p]
playerUpdateBatchQueue[p] = nil
CollectionService:RemoveTag(p, PENDING_BATCH_TAG)
if next(batchToSend) then
internalUpdateEvent:FireClient(p, batchToSend, nil, "batch_update")
end
end
end
end)
end
end
function Core.SetAuthorizationCallback(callback: ((player: Player, realKey: string) -> boolean)?)
if callback == nil then
authorizationCallback = nil
elseif typeof(callback) == "function" then
authorizationCallback = callback
else
warn("[DataReplicator | Core | SetAuthorizationCallback] Failed - Provided value is not a function or nil.")
end
end
function Core.Create(realKey: string, data: any): boolean
if typeof(realKey) ~= "string" or realKey == "" or replicatedDataStore[realKey] ~= nil then
warn(`[DataReplicator | Core | Create] Failed - Invalid or duplicate key: '{realKey}'`)
return false
end
replicatedDataStore[realKey] = data
return true
end
function Core.Update(realKey: string, data: any, target: Player | {Player} | "All" | "AllExcept" | nil, exceptTarget: Player | {Player} | nil): boolean
if typeof(realKey) ~= "string" or realKey == "" or replicatedDataStore[realKey] == nil then
warn(`[DataReplicator | Core | Update] Failed - Key not found or invalid: '{realKey}'`)
return false
end
replicatedDataStore[realKey] = data
local targetPlayers: { Player } = {}
local targetType = typeof(target)
if target == nil then
for player, realToObfuscatedMap in pairs(clientRealToObfuscated) do
if realToObfuscatedMap[realKey] and player.Parent then
table.insert(targetPlayers, player)
end
end
elseif targetType == "Instance" and target:IsA("Player") then
if target.Parent and clientRealToObfuscated[target :: Player] and clientRealToObfuscated[target :: Player][realKey] then
table.insert(targetPlayers, target :: Player)
end
elseif targetType == "string" and target == "All" then
for _, player in ipairs(Players:GetPlayers()) do
if clientRealToObfuscated[player] and clientRealToObfuscated[player][realKey] then
table.insert(targetPlayers, player)
end
end
elseif targetType == "string" and target == "AllExcept" then
local exclusions = {}
local exceptType = typeof(exceptTarget)
if exceptType == "Instance" and exceptTarget:IsA("Player") then
exclusions[exceptTarget :: Player] = true
elseif exceptType == "table" then
for _, p in ipairs(exceptTarget :: { Player }) do
if typeof(p) == "Instance" and p:IsA("Player") then exclusions[p] = true end
end
end
for _, player in ipairs(Players:GetPlayers()) do
if not exclusions[player] and clientRealToObfuscated[player] and clientRealToObfuscated[player][realKey] then
table.insert(targetPlayers, player)
end
end
elseif targetType == "table" then
for _, player in ipairs(target :: { Player }) do
if typeof(player) == "Instance" and player:IsA("Player") and player.Parent then
if clientRealToObfuscated[player] and clientRealToObfuscated[player][realKey] then
table.insert(targetPlayers, player)
end
end
end
else
warn(`[DataReplicator | Core | Update] Invalid target type for key '{realKey}': {targetType}`)
return true
end
if #targetPlayers > 0 then
for _, player in ipairs(targetPlayers) do
if player.Parent and clientRealToObfuscated[player] and clientRealToObfuscated[player][realKey] then
local obfuscatedKey = clientRealToObfuscated[player][realKey]
addToPlayerBatch(player, obfuscatedKey, data)
end
end
end
return true
end
function Core.Delete(realKey: string): boolean
if typeof(realKey) ~= "string" or realKey == "" or replicatedDataStore[realKey] == nil then
warn(`[DataReplicator | Core | Delete] Failed - Key not found or invalid: '{realKey}'`)
return false
end
replicatedDataStore[realKey] = nil
for player, realToObfuscatedMap in pairs(clientRealToObfuscated) do
local obfuscatedKey = realToObfuscatedMap[realKey]
if obfuscatedKey then
if player.Parent then
addToPlayerBatch(player, obfuscatedKey, Utilities.DELETED_MARKER)
end
local obfuscatedToRealMap = clientKeyMaps[player]
if obfuscatedToRealMap then obfuscatedToRealMap[obfuscatedKey] = nil end
realToObfuscatedMap[realKey] = nil
end
end
return true
end
function Core.GetServerData(realKey: string): any | nil
if typeof(realKey) ~= "string" then
warn(`[DataReplicator | Core | GetServerData] Failed - Invalid key type: {typeof(realKey)}`)
return nil
end
return replicatedDataStore[realKey]
end
local function handleServerInvoke(player: Player, action: string, realKey: string)
-- / Rate Limiting / --
if not checkAndUpdateRateLimit(player) then
warn(`[DataReplicator | Core | RateLimit] Player {player.Name} exceeded rate limit ({RATE_LIMIT_MAX_REQUESTS_IN_WINDOW} reqs in {RATE_LIMIT_WINDOW_SECONDS}s). Action: {action}, Key: {realKey}`)
return nil
end
if typeof(realKey) ~= "string" or realKey == "" then
warn(`[DataReplicator | Core | OnServerInvoke] Received invalid realKey '{realKey}' from {player.Name} (Action: {action})`)
return nil
end
-- / Authorization and Action Handling / --
if action == "request_listen" or action == "request_data" then
local isAuthorized = false
if authorizationCallback then
local successAuth, resultAuth = pcall(authorizationCallback, player, realKey)
if successAuth then
isAuthorized = resultAuth
else
warn(`[DataReplicator | Core | OnServerInvoke] Authorization callback errored for player {player.Name}, key '{realKey}': {resultAuth}. Denying access.`)
isAuthorized = false
end
else
if REQUIRE_AUTHORIZATION_CALLBACK then
warn(`[DataReplicator | Core | OnServerInvoke] DENIED: Access requires an authorization callback for key '{realKey}', but none is set. Player: {player.Name}.`)
isAuthorized = false
else
isAuthorized = true
end
end
if not isAuthorized then
return nil
end
local data = replicatedDataStore[realKey]
if data == nil and action == "request_data" then
return nil
end
local obfuscatedKey = getOrCreateObfuscatedKey(player, realKey)
if action == "request_listen" then
if data ~= nil then
task.spawn(internalUpdateEvent.FireClient, internalUpdateEvent, player, obfuscatedKey, data, "update")
else
task.spawn(internalUpdateEvent.FireClient, internalUpdateEvent, player, obfuscatedKey, nil, "delete")
end
return obfuscatedKey
elseif action == "request_data" then
return { ObfuscatedKey = obfuscatedKey, Data = data }
end
return nil
elseif action == "stop_listening" then
if clientRealToObfuscated[player] then
local obfuscatedKey = clientRealToObfuscated[player][realKey]
if obfuscatedKey then
local obfuscatedToRealMap = clientKeyMaps[player]
if obfuscatedToRealMap then obfuscatedToRealMap[obfuscatedKey] = nil end
clientRealToObfuscated[player][realKey] = nil
end
end
return true
else
warn(`[DataReplicator | Core | OnServerInvoke] Received unknown action '{action}' for key '{realKey}' from {player.Name}`)
return nil
end
end
local function onPlayerRemoving(player: Player)
if clientKeyMaps[player] then
clientKeyMaps[player] = nil
end
if clientRealToObfuscated[player] then
clientRealToObfuscated[player] = nil
end
if playerRequestTracker[player] then
playerRequestTracker[player] = nil
end
if playerUpdateBatchQueue[player] then
playerUpdateBatchQueue[player] = nil
end
if CollectionService:HasTag(player, PENDING_BATCH_TAG) then
CollectionService:RemoveTag(player, PENDING_BATCH_TAG)
end
end
Core.ServerInit = function()
internalRequestFunc.OnServerInvoke = handleServerInvoke
Players.PlayerRemoving:Connect(onPlayerRemoving)
end
-- // CLIENT-SIDE IMPLEMENTATION // --
elseif RunService:IsClient() then
-- | Data Cache Storage on Client | --
local localDataCache: { [string]: any } = {}
-- | Signal for Local Notifications | --
local updateSignals: { [string]: BindableEvent } = {}
-- | Active and Pending Mappings in Client | --
local activeListeners: { [string]: string } = {}
local pendingSignals: { [string]: BindableEvent } = {}
-- // FUNCTIONS // --
function Core.Request(realKey: string): any | nil
if typeof(realKey) ~= "string" or realKey == "" then
warn(`[DataReplicator | Core | Request] Invalid realKey provided: '{realKey}'`)
return nil
end
local responseData: { ObfuscatedKey: string, Data: any } | nil
local success, result = pcall(function()
responseData = internalRequestFunc:InvokeServer("request_data", realKey)
end)
if not success then
warn(`[DataReplicator | Core | Request] InvokeServer failed for '{realKey}': {result}`)
return nil
end
if responseData and typeof(responseData) == "table" and typeof(responseData.ObfuscatedKey) == "string" and responseData.Data ~= nil then
local obsKey = responseData.ObfuscatedKey
local data = responseData.Data
localDataCache[obsKey] = data
activeListeners[realKey] = obsKey
return data
else
return nil
end
end
function Core.GetCached(realKey: string): any | nil
if typeof(realKey) ~= "string" then return nil end
local obfuscatedKey = activeListeners[realKey]
return if obfuscatedKey then localDataCache[obfuscatedKey] else nil
end
function Core.WaitForData(realKey: string, timeout: number?): (any?, boolean)
if typeof(realKey) ~= "string" or realKey == "" then
warn(`[DataReplicator | Core | WaitForData] Invalid realKey provided: '{realKey}'`)
return nil, false
end
local cachedData = Core.GetCached(realKey)
if cachedData ~= nil then
return cachedData, true
end
local timeoutDuration = if typeof(timeout) == "number" and timeout > 0 then timeout else 5
local receivedData: any = nil
local didSucceed = false
local didTimeout = false
local eventConnection: RBXScriptConnection = nil
local signalToWaitOn = Core.Listen(realKey)
local function onDataReceivedFromSignal(newData: any)
if not didSucceed and not didTimeout then
receivedData = newData
didSucceed = true
end
end
if signalToWaitOn and typeof(signalToWaitOn.Event) == "RBXScriptSignal" then
eventConnection = signalToWaitOn.Event:Connect(onDataReceivedFromSignal)
end
local startTime = os.clock()
while not didSucceed and not didTimeout do
if (os.clock() - startTime) >= timeoutDuration then
didTimeout = true
end
if not didSucceed then
cachedData = Core.GetCached(realKey)
if cachedData ~= nil then
receivedData = cachedData
didSucceed = true
end
end
if not didSucceed and not didTimeout then
RunService.Heartbeat:Wait()
end
end
if eventConnection then
eventConnection:Disconnect()
eventConnection = nil
end
if didSucceed then
return receivedData, true
else
return nil, false
end
end
function Core.Listen(realKey: string): BindableEvent
if typeof(realKey) ~= "string" or realKey == "" then
warn(`[DataReplicator | Core | Listen] Invalid realKey provided: '{realKey}'`)
local dummySignal = Instance.new("BindableEvent")
task.defer(dummySignal.Destroy, dummySignal)
return dummySignal
end
local existingObfuscatedKey = activeListeners[realKey]
if existingObfuscatedKey and updateSignals[existingObfuscatedKey] then
local signal = updateSignals[existingObfuscatedKey]
local cachedData = localDataCache[existingObfuscatedKey]
if cachedData ~= nil then
task.spawn(signal.Fire, signal, cachedData)
end
return signal
end
if pendingSignals[realKey] then
return pendingSignals[realKey]
end
local newSignal = Instance.new("BindableEvent")
pendingSignals[realKey] = newSignal
task.spawn(function()
local returnedObfuscatedKey: string?
local success, result = pcall(function()
returnedObfuscatedKey = internalRequestFunc:InvokeServer("request_listen", realKey)
end)
local currentSignalForRealKey = pendingSignals[realKey]
or (activeListeners[realKey] and updateSignals[activeListeners[realKey]])
if success and typeof(returnedObfuscatedKey) == "string" and returnedObfuscatedKey ~= "" then
activeListeners[realKey] = returnedObfuscatedKey
if pendingSignals[realKey] == newSignal then
if updateSignals[returnedObfuscatedKey] and updateSignals[returnedObfuscatedKey] ~= newSignal then
updateSignals[returnedObfuscatedKey]:Destroy()
end
updateSignals[returnedObfuscatedKey] = newSignal
pendingSignals[realKey] = nil
elseif currentSignalForRealKey and currentSignalForRealKey ~= newSignal then
newSignal:Destroy()
end
else
warn(`[DataReplicator | Core | Listen] Failed to establish listener for '{realKey}': {result or returnedObfuscatedKey or "Unknown error"}`)
if pendingSignals[realKey] == newSignal then
pendingSignals[realKey] = nil
end
end
end)
return newSignal
end
function Core.StopListening(realKey: string)
if typeof(realKey) ~= "string" or realKey == "" then return end
local obfuscatedKey = activeListeners[realKey]
activeListeners[realKey] = nil
if pendingSignals[realKey] then
pendingSignals[realKey]:Destroy()
pendingSignals[realKey] = nil
end
if obfuscatedKey then
if updateSignals[obfuscatedKey] then
updateSignals[obfuscatedKey]:Destroy()
updateSignals[obfuscatedKey] = nil
end
localDataCache[obfuscatedKey] = nil
pcall(internalRequestFunc.InvokeServer, internalRequestFunc, "stop_listening", realKey)
end
end
local function handleClientUpdate(batchOrObfuscatedKey: string | {[string]: any}, newDataOrNil: any, actionType: string)
if actionType == "batch_update" then
local batchData = batchOrObfuscatedKey :: {[string]: any}
if typeof(batchData) ~= "table" then
warn(`[DataReplicator | Core | OnClientEvent] Received invalid batch data type: {typeof(batchData)}`)
return
end
for obfuscatedKeyInBatch, dataInBatch in pairs(batchData) do
if typeof(obfuscatedKeyInBatch) ~= "string" then
warn(`[DataReplicator | Core | OnClientEvent] Invalid obfuscatedKey in batch: {obfuscatedKeyInBatch}`)
continue
end
local effectiveAction = if dataInBatch == Utilities.DELETED_MARKER then "delete" else "update"
local effectiveData = if effectiveAction == "delete" then nil else dataInBatch
local targetSignal = updateSignals[obfuscatedKeyInBatch]
if not targetSignal then
for rKey, sig in pairs(pendingSignals) do
if activeListeners[rKey] == obfuscatedKeyInBatch then
targetSignal = sig
updateSignals[obfuscatedKeyInBatch] = targetSignal
pendingSignals[rKey] = nil; break
end
end
end
if not targetSignal then
if effectiveAction == "update" then
localDataCache[obfuscatedKeyInBatch] = effectiveData
elseif effectiveAction == "delete" then
localDataCache[obfuscatedKeyInBatch] = nil
end
continue
end
if effectiveAction == "update" then
localDataCache[obfuscatedKeyInBatch] = effectiveData
task.spawn(targetSignal.Fire, targetSignal, effectiveData)
elseif effectiveAction == "delete" then
localDataCache[obfuscatedKeyInBatch] = nil
task.spawn(targetSignal.Fire, targetSignal, nil)
for rk, obk in pairs(activeListeners) do
if obk == obfuscatedKeyInBatch then activeListeners[rk] = nil end
end
if updateSignals[obfuscatedKeyInBatch] == targetSignal then
updateSignals[obfuscatedKeyInBatch]:Destroy()
updateSignals[obfuscatedKeyInBatch] = nil
end
end
end
elseif actionType == "update" or actionType == "delete" then
local obfuscatedKey = batchOrObfuscatedKey :: string
local newData = newDataOrNil
if typeof(obfuscatedKey) ~= "string" then return end
local targetSignal = updateSignals[obfuscatedKey]
if not targetSignal then
for rk, pendingSignal in pairs(pendingSignals) do
if activeListeners[rk] == obfuscatedKey then
targetSignal = pendingSignal
updateSignals[obfuscatedKey] = targetSignal
pendingSignals[rk] = nil; break
end
end
end
if not targetSignal then
if actionType == "update" then
localDataCache[obfuscatedKey] = newData
elseif actionType == "delete" then
localDataCache[obfuscatedKey] = nil
end
return
end
if actionType == "update" then
localDataCache[obfuscatedKey] = newData
task.spawn(targetSignal.Fire, targetSignal, newData)
elseif actionType == "delete" then
localDataCache[obfuscatedKey] = nil
task.spawn(targetSignal.Fire, targetSignal, nil)
for rk, obk in pairs(activeListeners) do
if obk == obfuscatedKey then activeListeners[rk] = nil end
end
if updateSignals[obfuscatedKey] == targetSignal then
updateSignals[obfuscatedKey]:Destroy()
updateSignals[obfuscatedKey] = nil
end
end
else
warn(`[DataReplicator | Core | OnClientEvent] Received unknown action type '{actionType}' for ObsKey '{batchOrObfuscatedKey :: string}'`)
end
end
Core.ClientInit = function()
internalUpdateEvent.OnClientEvent:Connect(handleClientUpdate)
end
end
-- // INITIALIZATION // --
function Core.Init(utils: {}, mainScriptInstance: Instance)
Utilities = utils
if RunService:IsServer() then
local remoteFolder = Utilities.GetOrCreateRemote(mainScriptInstance, "Folder", REMOTE_FOLDER_NAME)
internalUpdateEvent = Utilities.GetOrCreateRemote(remoteFolder, "RemoteEvent", UPDATE_EVENT_NAME) :: RemoteEvent
internalRequestFunc = Utilities.GetOrCreateRemote(remoteFolder, "RemoteFunction", REQUEST_FUNC_NAME) :: RemoteFunction
Core.ServerInit()
elseif RunService:IsClient() then
local remoteFolder = mainScriptInstance:WaitForChild(REMOTE_FOLDER_NAME, 20)
if not remoteFolder then
error("[DataReplicator | Core | Init] Failed to find Remote Folder under Init.lua after 20 seconds.")
end
local eventChild = remoteFolder:WaitForChild(UPDATE_EVENT_NAME, 15)
if not eventChild then
error(`[DataReplicator | Core | Init] Failed to find RemoteEvent '{UPDATE_EVENT_NAME}' after 15 seconds.`)
end
internalUpdateEvent = eventChild :: RemoteEvent
local funcChild = remoteFolder:WaitForChild(REQUEST_FUNC_NAME, 15)
if not funcChild then
error(`[DataReplicator | Core | Init] Failed to find RemoteFunction '{REQUEST_FUNC_NAME}' after 15 seconds.`)
end
internalRequestFunc = funcChild :: RemoteFunction
Core.ClientInit()
end
end
return Core