fix: check for nil diffs

This commit is contained in:
littensy 2025-11-20 15:05:59 -08:00
parent 9f66803f04
commit 366fc668a2
3 changed files with 67 additions and 27 deletions

View file

@ -210,6 +210,26 @@ test("should serialize state", function()
assert(patch.isNone(payloads[1].data.a["5"]), "Expected index 5 to be serialized as string key") assert(patch.isNone(payloads[1].data.a["5"]), "Expected index 5 to be serialized as string key")
end) end)
test("should not send unchanged state", function()
local client = {}
local a = Charm.atom(0)
local payloads
server.addSignalsToClient(client, { a = a })
server.connect(function(_, p)
payloads = p
end)
server.flush()
assert(payloads[1].type == "init", "Expected first payload to be init")
assert(payloads[1].data.a == 0, "Expected init payload to contain atom 'a'")
payloads = {}
a(0)
server.flush()
assert(#payloads == 0, "Expected no payloads to be sent for unchanged state")
end)
describe("historical", function() describe("historical", function()
beforeEach(function() beforeEach(function()
config.preserveHistory = true config.preserveHistory = true
@ -344,6 +364,26 @@ describe("historical", function()
assert(payloads[4].type == "patch", "Expected fourth payload to be patch") assert(payloads[4].type == "patch", "Expected fourth payload to be patch")
assert(payloads[4].data.b == 2, "Expected patch payload to contain atom 'b'") assert(payloads[4].data.b == 2, "Expected patch payload to contain atom 'b'")
end) end)
test("should not send unchanged state", function()
local client = {}
local a = Charm.atom(0)
local payloads
server.addSignalsToClient(client, { a = a })
server.connect(function(_, p)
payloads = p
end)
server.flush()
assert(payloads[1].type == "init", "Expected first payload to be init")
assert(payloads[1].data.a == 0, "Expected init payload to contain atom 'a'")
payloads = {}
a(0)
server.flush()
assert(#payloads == 0, "Expected no payloads to be sent for unchanged state")
end)
end) end)
return {} return {}

View file

@ -75,24 +75,24 @@ local function trackReactive<T>(key: string, proxy: ReactiveProxy<T>)
return return
end end
local meta = getmetatable(proxy) local api = getmetatable(proxy)
local snapshot = utils.deepCopy(meta.value) local snapshot = utils.deepCopy(api.value)
local firstRun = true local firstRun = true
local trackedState = { local trackedState = {
target = proxy, target = proxy,
currentValue = meta.value, currentValue = api.value,
lastSyncedValue = snapshot, lastSyncedValue = snapshot,
isReactive = true, isReactive = true,
} }
trackedState.dispose = Charm.effect(function() trackedState.dispose = Charm.effect(function()
meta.track() api.track()
if not config.preserveHistory or firstRun then if not config.preserveHistory or firstRun then
firstRun = false firstRun = false
else else
local oldSnapshot = snapshot local oldSnapshot = snapshot
snapshot = utils.deepCopy(meta.value) snapshot = utils.deepCopy(api.value)
table.insert(globalUpdates, { table.insert(globalUpdates, {
key = key, key = key,
newValue = snapshot, newValue = snapshot,
@ -132,8 +132,8 @@ local function diffGlobalUpdateBuffer(): { any }
local patchCount = #globalUpdates local patchCount = #globalUpdates
local patches = table.create(patchCount) local patches = table.create(patchCount)
for index, update in globalUpdates do for _, update in globalUpdates do
patches[index] = patch.diff(update.oldValue, update.newValue) table.insert(patches, patch.diff(update.oldValue, update.newValue))
end end
return patches return patches
@ -176,7 +176,7 @@ local function selectFromGlobalPatchList(client: any, globalPatches: { any }): {
-- If a pending signal was never updated, we still need to send its initial -- If a pending signal was never updated, we still need to send its initial
-- state. This can happen if a signal was registered after its last change. -- state. This can happen if a signal was registered after its last change.
local initData: { [string]: any } = {} local initData = {}
for key, status in clientKeys do for key, status in clientKeys do
local trackedState = trackedStates[key] local trackedState = trackedStates[key]
@ -194,7 +194,7 @@ local function selectFromGlobalPatchList(client: any, globalPatches: { any }): {
return payloads return payloads
end end
local function diffGlobalState(): any local function diffGlobalState(): { [string]: any }?
local currentGlobalState: { [string]: any } = {} local currentGlobalState: { [string]: any } = {}
local previousGlobalState: { [string]: any } = {} local previousGlobalState: { [string]: any } = {}
@ -211,7 +211,7 @@ local function diffGlobalState(): any
return patch.diff(previousGlobalState, currentGlobalState) return patch.diff(previousGlobalState, currentGlobalState)
end end
local function selectFromGlobalPatch(client: any, globalPatch: any): { SyncPayload }? local function selectFromGlobalPatch(client: any, globalPatch: { [string]: any }?): { SyncPayload }?
local clientKeys = clients[client] local clientKeys = clients[client]
if not clientKeys then if not clientKeys then
@ -227,7 +227,7 @@ local function selectFromGlobalPatch(client: any, globalPatch: any): { SyncPaylo
for key, status in clientKeys do for key, status in clientKeys do
local trackedState = trackedStates[key] local trackedState = trackedStates[key]
if status == LISTENING_FOR_CHANGES then if status == LISTENING_FOR_CHANGES and globalPatch then
patchData[key] = globalPatch[key] patchData[key] = globalPatch[key]
elseif status == PENDING_INITIAL_STATE then elseif status == PENDING_INITIAL_STATE then
initData[key] = trackedState.currentValue initData[key] = trackedState.currentValue
@ -323,11 +323,11 @@ local function flush()
end end
local granular = config.preserveHistory local granular = config.preserveHistory
local globalPatch = if granular then diffGlobalUpdateBuffer() else diffGlobalState() local globalPatch: { [any]: any }? = if granular then diffGlobalUpdateBuffer() else diffGlobalState()
for client in clients do for client in clients do
local payloads = if granular local payloads = if granular
then selectFromGlobalPatchList(client, globalPatch) then selectFromGlobalPatchList(client, globalPatch :: { any })
else selectFromGlobalPatch(client, globalPatch) else selectFromGlobalPatch(client, globalPatch)
if payloads and payloads[1] then if payloads and payloads[1] then

View file

@ -6,7 +6,7 @@ export type Link = system.Link
export type Atom<T = any> = (...((T) -> T) | T) -> T export type Atom<T = any> = (...((T) -> T) | T) -> T
type ReactiveProxyMeta<T = {}> = { type ReactiveProxyApi<T = {}> = {
value: T, value: T,
track: () -> (), track: () -> (),
set: (T | (T) -> ()?) -> (), set: (T | (T) -> ()?) -> (),
@ -17,7 +17,7 @@ type ReactiveProxyProps<T = {}> = {
__signals: { [unknown]: () -> unknown }, __signals: { [unknown]: () -> unknown },
} }
export type ReactiveProxy<T = {}> = T & typeof(setmetatable({} :: ReactiveProxyProps<T>, {} :: ReactiveProxyMeta<T>)) export type ReactiveProxy<T = {}> = T & typeof(setmetatable({} :: ReactiveProxyProps<T>, {} :: ReactiveProxyApi<T>))
type EffectScopeNode = ReactiveNode & { type EffectScopeNode = ReactiveNode & {
cleanups: { () -> () }, cleanups: { () -> () },
@ -855,7 +855,7 @@ end
]] ]]
local function reactive<T>(initialValue: T & {}): (T, (update: (T) -> () | T?) -> ()) local function reactive<T>(initialValue: T & {}): (T, (update: (T) -> () | T?) -> ())
local track = signal(nil) local track = signal(nil)
local proxyMeta = { value = initialValue, track = track } local proxyApi = { value = initialValue, track = track }
local proxyMap = setmetatable({} :: { [{}]: any }, { __mode = "v" }) local proxyMap = setmetatable({} :: { [{}]: any }, { __mode = "v" })
local function trackProperty(cache: { [any]: () -> any }, target: {}, key: any) local function trackProperty(cache: { [any]: () -> any }, target: {}, key: any)
@ -876,11 +876,11 @@ local function reactive<T>(initialValue: T & {}): (T, (update: (T) -> () | T?) -
proxyMap[target] = setmetatable({ proxyMap[target] = setmetatable({
__target = target, __target = target,
__signals = setmetatable({}, { __mode = "v" }), __signals = setmetatable({}, { __mode = "v" }),
}, proxyMeta) }, proxyApi)
return proxyMap[target] return proxyMap[target]
end end
function proxyMeta.__index(self: ReactiveProxy, key: any): any function proxyApi.__index(self: ReactiveProxy, key: any): any
if activeSub then if activeSub then
trackProperty(self.__signals, self.__target, key) trackProperty(self.__signals, self.__target, key)
end end
@ -888,8 +888,8 @@ local function reactive<T>(initialValue: T & {}): (T, (update: (T) -> () | T?) -
return if type(value) == "table" then createProxy(value) else value return if type(value) == "table" then createProxy(value) else value
end end
function proxyMeta.__newindex(self: ReactiveProxy, key: any, value: any) function proxyApi.__newindex(self: ReactiveProxy, key: any, value: any)
if type(value) == "table" and getmetatable(value) == proxyMeta then if type(value) == "table" and getmetatable(value) == proxyApi then
value = value.__target value = value.__target
end end
if self.__target[key] ~= value then if self.__target[key] ~= value then
@ -898,7 +898,7 @@ local function reactive<T>(initialValue: T & {}): (T, (update: (T) -> () | T?) -
end end
end end
function proxyMeta.__iter(self: ReactiveProxy) function proxyApi.__iter(self: ReactiveProxy)
track() track()
local function iterator(target: {}, index: any) local function iterator(target: {}, index: any)
local key, value = next(target, index) local key, value = next(target, index)
@ -907,12 +907,12 @@ local function reactive<T>(initialValue: T & {}): (T, (update: (T) -> () | T?) -
return iterator, self.__target return iterator, self.__target
end end
function proxyMeta.__len(self: ReactiveProxy) function proxyApi.__len(self: ReactiveProxy)
track() track()
return #self.__target return #self.__target
end end
function proxyMeta.set(update: ((T) -> ()) | T?) function proxyApi.set(update: ((T) -> ()) | T?)
if type(update) == "function" then if type(update) == "function" then
update(initialValue) update(initialValue)
elseif type(update) == "table" then elseif type(update) == "table" then
@ -924,7 +924,7 @@ local function reactive<T>(initialValue: T & {}): (T, (update: (T) -> () | T?) -
trigger(track) trigger(track)
end end
return createProxy(initialValue), proxyMeta.set return createProxy(initialValue), proxyApi.set
end end
--[[ --[[
@ -938,10 +938,10 @@ end
local function toRaw<T>(value: T): T local function toRaw<T>(value: T): T
if type(value) == "table" and getmetatable(value) ~= nil then if type(value) == "table" and getmetatable(value) ~= nil then
local proxy: ReactiveProxy = value local proxy: ReactiveProxy = value
local meta = getmetatable(proxy) local api = getmetatable(proxy)
if meta.value == proxy.__target then if api.value == proxy.__target then
-- Track the root proxy since nested proxies are already tracked -- Track the root proxy since nested proxies are already tracked
meta.track() api.track()
end end
return proxy.__target :: any return proxy.__target :: any
else else