Problem
Delta transfer for observable queries matches items by a property conventionally named Id (case-insensitive), hardcoded on both sides:
- Server:
ChangeSetComputor.FindIdentityProperty (Source/DotNET/Arc.Core/Queries/ChangeSetComputor.cs) reflects for a property named Id. When absent it falls back to ComputeByJson, which only ever emits Added/Removed — never Replaced.
- Client:
applyChangeSet in useObservableQuery (Source/JavaScript/Arc.React/queries/useObservableQuery.ts) reads item?.id. When absent it falls back to JSON-string set matching for removals, then unconditionally appends added.
For a read model without an Id whose payload changes on every push (e.g. a lastSeen heartbeat timestamp), this fallback is inherently leaky: reconciliation only works if the client's accumulated array contains a byte-identical copy of the server's previous snapshot. Any desync — re-subscribe, coalesced frame, second subscriber sharing the cache — misses the removal, and since the fallback only appends, the stale row is never reclaimed.
Observed
Chronicle Workbench's Connected Clients view (read model keyed by ConnectionId, no Id): one connected client rendered as 100+ rows growing by one on every 2s heartbeat, plus ~800 React "two children with the same key" console errors. Instrumenting the removal set in the browser over 5s: 791 membership tests, 2 hits, 789 misses — each miss a permanently leaked row.
Renaming/adding Id on the model fixes it immediately (every tick becomes a clean Replaced), which is also the trap: nothing tells you the model has opted out of identity matching.
Suggestion
Let a read model declare its identity property instead of relying on the hardcoded Id name — e.g. an attribute on the model ([Key]-style) that flows through the generated proxy so ChangeSetComputor and the client getId use the declared property. Failing that, at minimum log/warn when an observable enumerable query with delta mode falls back to JSON matching, since the failure mode is silent unbounded growth in consumers.
The consuming UI often already knows the right key (PrimeReact dataKey='connectionId') — the delta layer just has no way to be told.
Problem
Delta transfer for observable queries matches items by a property conventionally named
Id(case-insensitive), hardcoded on both sides:ChangeSetComputor.FindIdentityProperty(Source/DotNET/Arc.Core/Queries/ChangeSetComputor.cs) reflects for a property namedId. When absent it falls back toComputeByJson, which only ever emitsAdded/Removed— neverReplaced.applyChangeSetinuseObservableQuery(Source/JavaScript/Arc.React/queries/useObservableQuery.ts) readsitem?.id. When absent it falls back to JSON-string set matching for removals, then unconditionally appendsadded.For a read model without an
Idwhose payload changes on every push (e.g. alastSeenheartbeat timestamp), this fallback is inherently leaky: reconciliation only works if the client's accumulated array contains a byte-identical copy of the server's previous snapshot. Any desync — re-subscribe, coalesced frame, second subscriber sharing the cache — misses the removal, and since the fallback only appends, the stale row is never reclaimed.Observed
Chronicle Workbench's Connected Clients view (read model keyed by
ConnectionId, noId): one connected client rendered as 100+ rows growing by one on every 2s heartbeat, plus ~800 React "two children with the same key" console errors. Instrumenting the removal set in the browser over 5s: 791 membership tests, 2 hits, 789 misses — each miss a permanently leaked row.Renaming/adding
Idon the model fixes it immediately (every tick becomes a cleanReplaced), which is also the trap: nothing tells you the model has opted out of identity matching.Suggestion
Let a read model declare its identity property instead of relying on the hardcoded
Idname — e.g. an attribute on the model ([Key]-style) that flows through the generated proxy soChangeSetComputorand the clientgetIduse the declared property. Failing that, at minimum log/warn when an observable enumerable query with delta mode falls back to JSON matching, since the failure mode is silent unbounded growth in consumers.The consuming UI often already knows the right key (PrimeReact
dataKey='connectionId') — the delta layer just has no way to be told.