Repository navigation
Extend x-key: '$self' to non-primitive items via structural identity #18
Description
Background
x-key: '$self' declares set semantics on an array — the item itself is its identity. It currently only works on primitive items (strings, numbers, booleans, null), because JS's native Set/Map use value equality for primitives and reference equality for everything else.
new Set([1, 1]).size // 1
new Set([{a:1}, {a:1}]).size // 2 — different referencesSo $self on an array of objects would produce nonsense diffs today, and the diff path throws (or should throw) when it encounters non-primitive items.
What this issue tracks
Lifting that restriction so $self works on any JSON value — primitives, objects, nested arrays. The natural use case is "a set of structurally-identical small records where no field is a natural key" (e.g. [lat, lng] pairs treated as a set of locations).
Options considered
Two approaches both require walking each item's structure once, because JS doesn't expose structural equality natively:
1. Canonical-JSON normalization as identity. Stringify items with sorted keys, recursively. Use the string as a Set/Map key. No collisions by construction; memory cost is O(size) per item.
2. Deep-equality linear scan. No intermediate representation. For each item in a, check if any item in b deep-equals it. O(N·M·size) but short-circuits on first mismatch and allocates nothing extra.
For the array sizes where $self realistically applies (tags, permissions, small sets), either is fine. Deep-equality is probably simpler; canonical-JSON wins for larger N+M.
Why we're not doing it now
- The overwhelming majority of "set of objects" use cases have a natural ID field, in which case
x-key: '<field>'is the right tool and already works. - Extending
$selfto objects adds structural-equality machinery (canonicalizer or deep-equal) that has to be maintained, with edge cases around number representation, key ordering, and any future widening ofJsonValue(e.g.NaN,undefined). - The clean two-tool design —
x-key: '<field>'for stable identity,$selffor primitive sets — covers ~all real schemas without overlap.
When to revisit
- A real schema lands where
$selfon objects would be meaningfully cleaner than adding an id field to the data. - Or: TC39 Records & Tuples ships, at which point
new Set(records)Just Works with value equality and the whole extension becomes free.
Acceptance criteria (if/when implemented)
x-key: '$self'no longer throws on object items.- Diffing
[{a:1}, {a:2}]→[{a:1}, {a:3}]emits one remove + one add (not nested replace). - Duplicate-collapse and reorder-invisibility behaviors of
$selfare preserved (set semantics). - Implementation choice (canonicalize vs deep-equal) documented in code with a comment explaining the trade-off.
Activity