670 lines
22 KiB
Plaintext
670 lines
22 KiB
Plaintext
-- // 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 |