Skip to content

fix(data): canonical observe.select cache key so Set includes don't collide - #166

Merged
krisnye merged 1 commit into
mainfrom
krisnye/fix-observe-select
Aug 11, 2026
Merged

fix(data): canonical observe.select cache key so Set includes don't collide#166
krisnye merged 1 commit into
mainfrom
krisnye/fix-observe-select

Conversation

@krisnye

@krisnye krisnye commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Description

observe.select cached its per-query Observe functions under JSON.stringify({ include, options }). Because include may be a ReadonlySet — exactly the shape of archetype.components — and JSON.stringify(aSet) is always "{}", every set-based select collapsed onto a single cache key. A second set-based select was then served the first one's observe function.

This explains the reported minimal-vs-composed divergence: with a single set-based subscription (a minimal chain) the collision is invisible; in a composed graph an earlier set-based select poisons the shared key, so a later observe.select(TrackItem.components) silently watches the wrong include set and membership contraction (update(entity, { orderedChildOf: undefined })) never re-emits (before:1, after:1). Every existing test passed an array, so the Set path was unexercised.

Fix

Replace the key builder with a hand-built canonical key:

  • include / exclude — sorted (order- and array-vs-Set-agnostic), killing the collision.
  • where — keys sorted, values JSON-serialized (so equivalent filters dedup too).
  • order — sequence preserved (it's a sort priority).

Chosen over a normalized JSON.stringify (the quick fix) and over a WeakMap identity cache: the hand-built key is ~2.3x faster than stringify in both stable-ref and fresh-literal microbenchmarks and pessimizes neither, on a subscription-setup path where absolute cost is already sub-microsecond. A WeakMap identity cache hits 127 M/s on stable refs but drops to stringify-level on fresh literals — the wrong trade for a cold path.

Tests

New Set-based include block in observe-select-entities.test.ts (red before, green after):

  • distinct component Sets return distinct results (direct proof of the collision);
  • membership contraction re-emits for a Set-based select even after another set-based select was made (the reported repro).

Full @adobe/data suite (3001 tests) and monorepo typecheck pass.

Related PRs

Follows up #144 (membership-contraction re-emit).

…ollide

observe.select cached observe functions under JSON.stringify({include, options}),
but a Set (the shape of archetype.components) serializes to "{}", collapsing every
set-based select onto one key. In a composed graph a second set-based select was
served the first's observe, so membership contraction never re-emitted. Replace
with a hand-built canonical key (sorted include/exclude, sorted where, ordered
order) — correct across array/Set and ~2.3x faster than the stringify path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@krisnye
krisnye merged commit db86a6e into main Aug 11, 2026
3 checks passed
@krisnye
krisnye deleted the krisnye/fix-observe-select branch August 11, 2026 15:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant