From 0dbc0587a00e74c81998e528314b01f4a8acbc53 Mon Sep 17 00:00:00 2001 From: littensy <56808540+littensy@users.noreply.github.com> Date: Mon, 29 Sep 2025 18:30:35 -0700 Subject: [PATCH] fix: send init payload even if atom did not update --- packages/charm-sync/src/client.luau | 4 ---- packages/charm-sync/src/server.luau | 37 ++++++++++++++++++++++------- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/packages/charm-sync/src/client.luau b/packages/charm-sync/src/client.luau index 40edc76..3beb209 100644 --- a/packages/charm-sync/src/client.luau +++ b/packages/charm-sync/src/client.luau @@ -6,7 +6,6 @@ type SyncPayload = types.SyncPayload local atomRegister: { [string]: Charm.Atom } = {} ---- @client local function registerAtoms(atoms: { [string]: Charm.Atom }) for key, atom in atoms do if atomRegister[key] and atomRegister[key] ~= atom then @@ -18,12 +17,10 @@ local function registerAtoms(atoms: { [string]: Charm.Atom }) end end ---- @client local function clearAtoms() table.clear(atomRegister) end ---- @client local function unregisterAtoms(...: string) for index = 1, select("#", ...) do local key = select(index, ...) @@ -54,7 +51,6 @@ local function applyPatchPayload(data: { [string | number]: any }) end end ---- @client local function applyPayloads(payloads: { SyncPayload }) for _, payload in payloads do if payload.type == "init" then diff --git a/packages/charm-sync/src/server.luau b/packages/charm-sync/src/server.luau index 355ea11..b645e8e 100644 --- a/packages/charm-sync/src/server.luau +++ b/packages/charm-sync/src/server.luau @@ -40,6 +40,8 @@ local function registerAtom(key: string, atom: () -> T) registeredAtom.stop = Charm.effect(function() local value = atom() + -- When preserving history, we need to keep track of previous values + -- so we can create granular patches for each change. if config.preserveHistory and registeredAtom.value ~= value then table.insert(globalUpdates, { key = key, @@ -81,7 +83,7 @@ end local function createHistoricalPatches(): { any } local patchCount = #globalUpdates - local patches: { any } = table.create(patchCount) + local patches = table.create(patchCount) for index, update in globalUpdates do patches[index] = patch.diff(update.previousValue, update.value) @@ -99,6 +101,8 @@ local function createHistoricalPayloadsForClient(client: any, patches: { any }): local payloads: { SyncPayload } = {} + -- Iterate over all global updates and see if the client is interested in + -- any of them. If so, we create a patch payload for the update. for index, update in globalUpdates do local patchData = patches[index] @@ -124,6 +128,23 @@ local function createHistoricalPayloadsForClient(client: any, patches: { any }): end end + -- If a pending atom was never updated, we still need to send its initial + -- state. This can happen if the atom was registered after its last change. + local initPayload: { [string]: any } = {} + + for key, status in syncedAtoms do + local registeredAtom = atomRegister[key] + + if status == PENDING_INITIAL_STATE and registeredAtom then + initPayload[key] = registeredAtom.value + syncedAtoms[key] = LISTENING_FOR_CHANGES + end + end + + if next(initPayload) ~= nil then + table.insert(payloads, 1, { type = "init" :: "init", data = initPayload }) + end + return payloads end @@ -148,10 +169,11 @@ local function createPayloadsForClient(client: any, patchData: any): { SyncPaylo return payloads end - local shouldSendInitialState = false local initPayload: { [string]: any } = {} local patchPayload: { [string]: any } = {} + -- Create patches for initialized atoms that changed, and add atoms that + -- are still pending initial state to the init payload. for key, status in syncedAtoms do local registeredAtom = atomRegister[key] @@ -160,11 +182,10 @@ local function createPayloadsForClient(client: any, patchData: any): { SyncPaylo elseif status == PENDING_INITIAL_STATE then initPayload[key] = registeredAtom.value syncedAtoms[key] = LISTENING_FOR_CHANGES - shouldSendInitialState = true end end - if shouldSendInitialState then + if next(initPayload) ~= nil then table.insert(payloads, { type = "init", data = initPayload }) end @@ -175,7 +196,6 @@ local function createPayloadsForClient(client: any, patchData: any): { SyncPaylo return payloads end ---- @server local function syncAtomsToClient(client: any, atoms: { [string]: () -> any }) if not clientRegister[client] then clientRegister[client] = {} @@ -187,17 +207,17 @@ local function syncAtomsToClient(client: any, atoms: { [string]: () -> any }) registerAtom(key, atom) if not syncedAtoms[key] then + -- Mark the atom as pending initial state so we send its full value + -- the next time we sync. syncedAtoms[key] = PENDING_INITIAL_STATE end end end ---- @server local function unsyncClient(client: any) clientRegister[client] = nil end ---- @server local function unsyncAtomsFromClient(client: any, ...: string) local syncedAtoms = clientRegister[client] @@ -215,7 +235,6 @@ local function unsyncAtomsFromClient(client: any, ...: string) end end ---- @server local function startSync(onSync: (client: any, payloads: { SyncPayload }) -> ()) if connection then connection:Disconnect() @@ -226,6 +245,8 @@ local function startSync(onSync: (client: any, payloads: { SyncPayload }) -> ()) local function sync() local patches = if config.preserveHistory then createHistoricalPatches() else createPatch() + -- While payload objects are technically mutable as they are unique to + -- each client, the data inside them is not safe to mutate. if Charm.flags.frozen then Charm.deepFreeze(patches) end