feat: sync signals that return reactive proxy

This commit is contained in:
littensy 2025-11-25 00:29:08 -08:00
parent aebe3d27ee
commit b3a76face9
5 changed files with 69 additions and 71 deletions

View file

@ -5,11 +5,10 @@ local types = require("./types")
type ReactiveProxy<T = {}> = Charm.ReactiveProxy<T>
type SyncPayload = types.SyncPayload
type SignalSetter<T = any> = (T | (T) -> T) -> ()
type ReactiveSetter<T = any> = (T | (T) -> ()) -> ()
type Setter<T = any> = (T | (T) -> any) -> ()
local trackedSignals: { [string]: SignalSetter } = {}
local trackedReactives: { [string]: ReactiveSetter } = {}
local trackedSignals: { [string]: Setter } = {}
local trackedReactives: { [string]: Setter } = {}
--[[
Registers a map of setters to the state with the corresponding keys on the
@ -22,7 +21,7 @@ local trackedReactives: { [string]: ReactiveSetter } = {}
@param signals A map of setter functions to sync with the server.
]]
local function addSignals(signals: { [string]: SignalSetter | {} })
local function addSignals(signals: { [string]: Setter | {} })
for key, signal in signals do
if type(signal) == "function" then
trackedSignals[key] = signal
@ -55,7 +54,7 @@ end
local function applyInitPayload(data: { [string]: any })
for key, value in data do
local trackedSignal: SignalSetter? = trackedSignals[key] or trackedReactives[key]
local trackedSignal: Setter? = trackedSignals[key] or trackedReactives[key]
if trackedSignal then
trackedSignal(value)
end
@ -72,7 +71,7 @@ local function applyPatchPayload(data: { [string]: any })
return patch.apply(value, diffs)
end)
elseif trackedReactive then
trackedReactive(function(value)
trackedReactive(function(value): ()
patch.applyMutable(value, diffs)
end)
end

View file

@ -111,16 +111,16 @@ local function apply(oldState: any, diffs: any): any
return newState
end
local function applyMutable(targetState: any, sourceState: any): any
if isNone(sourceState) then
local function applyMutable(targetState: any, diffs: any): any
if isNone(diffs) then
return nil
elseif type(targetState) ~= "table" or type(sourceState) ~= "table" then
return sourceState
elseif type(targetState) ~= "table" or type(diffs) ~= "table" then
return diffs
end
local targetIsArray = targetState[1] ~= nil
for key, value in sourceState do
for key, value in diffs do
if targetIsArray and type(key) == "string" then
key = tonumber(key) or key
end

View file

@ -12,10 +12,10 @@ type SyncStatus = number
type ClientKeys = { [string]: SyncStatus }
type TrackedState<T = any> = {
target: any,
identifier: unknown,
currentValue: T,
lastSyncedValue: T,
isReactive: boolean?,
isReactive: boolean,
dispose: () -> (),
}
@ -35,75 +35,54 @@ local clients: { [any]: ClientKeys } = {}
local connection: RBXScriptConnection?
local syncCallback: (client: any, payloads: { SyncPayload }) -> ()?
local function trackSignal<T>(key: string, getter: () -> T)
local function trackSignal<T>(key: string, getter: () -> T, identifier: unknown)
if trackedStates[key] then
-- Ignore duplicates, but warn against name collisions
if trackedStates[key].target ~= getter then
if trackedStates[key].identifier ~= identifier then
warn("[Charm] Attempted to sync different states with the same key:", key)
end
return
end
local value = getter()
local trackedState = {
target = getter,
currentValue = value,
lastSyncedValue = value,
}
local trackedState: TrackedState<T>
local initialValue = getter()
local rawInitialValue = Charm.toRaw(initialValue)
local initialIsReactive = initialValue ~= rawInitialValue
local snapshot = if initialIsReactive then utils.deepCopy(rawInitialValue) else initialValue
local dispose = Charm.effect(function()
local newValue = getter()
local rawValue = Charm.toRaw(newValue)
local isReactive = newValue ~= rawValue
if not trackedState then
return
end
trackedState.dispose = Charm.subscribe(getter, function(newValue, oldValue)
-- When preserving history, we need to keep track of previous values
-- so we can create granular patches for each change.
if config.preserveHistory then
table.insert(globalUpdates, {
key = key,
newValue = newValue,
oldValue = oldValue,
})
end
trackedState.currentValue = newValue
end)
trackedStates[key] = trackedState
end
local function trackReactive<T>(key: string, proxy: ReactiveProxy<T>)
if trackedStates[key] then
if trackedStates[key].target ~= proxy then
warn("[Charm] Attempted to sync different states with the same key:", key)
end
return
end
local api = getmetatable(proxy)
local snapshot = utils.deepCopy(api.value)
local firstRun = true
local trackedState = {
target = proxy,
currentValue = api.value,
lastSyncedValue = snapshot,
isReactive = true,
}
trackedState.dispose = Charm.effect(function()
api.track()
if not config.preserveHistory then
return
end
if firstRun then
firstRun = false
else
local oldSnapshot = snapshot
snapshot = utils.deepCopy(api.value)
snapshot = if isReactive then utils.deepCopy(rawValue) else newValue
table.insert(globalUpdates, {
key = key,
newValue = snapshot,
oldValue = oldSnapshot,
})
end
trackedState.currentValue = rawValue
trackedState.isReactive = isReactive
end)
trackedState = {
identifier = identifier,
currentValue = rawInitialValue,
lastSyncedValue = snapshot,
isReactive = initialIsReactive,
dispose = dispose,
}
trackedStates[key] = trackedState
end
@ -269,9 +248,11 @@ local function addSignalsToClient(client: any, signals: { [string]: {} | () -> a
for key, signal in signals do
if type(signal) == "function" then
trackSignal(key, signal)
trackSignal(key, signal, signal)
else
trackReactive(key, signal :: any)
trackSignal(key, function()
return signal
end, signal)
end
if not clientKeys[key] then