Skip to content

Observable-query delta matching silently degrades for read models without an 'Id' property #2428

Description

@einari

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions