diff --git a/package.json b/package.json index e97fed44..a69e166e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "data-monorepo", - "version": "0.9.94", + "version": "0.9.95", "private": true, "engines": { "node": ">=24" diff --git a/packages/data-ai/.claude-plugin/plugin.json b/packages/data-ai/.claude-plugin/plugin.json index c697bb5a..06d58074 100644 --- a/packages/data-ai/.claude-plugin/plugin.json +++ b/packages/data-ai/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "adobe-data-ai", - "version": "0.9.94", + "version": "0.9.95", "description": "Architecture skills for @adobe/data — data-oriented modelling, archetype iteration, hot-path performance, and related conventions.", "author": { "name": "Adobe" diff --git a/packages/data-ai/package.json b/packages/data-ai/package.json index e1c8179b..b51f39cb 100644 --- a/packages/data-ai/package.json +++ b/packages/data-ai/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-ai", - "version": "0.9.94", + "version": "0.9.95", "description": "Cross-agent architecture skills for @adobe/data — installable as a Claude Code plugin or copied into any Agent-Skills-compatible agent (Cursor, Codex).", "type": "module", "private": false, diff --git a/packages/data-gpu-hopper/package.json b/packages/data-gpu-hopper/package.json index a1234a05..ebde4a90 100644 --- a/packages/data-gpu-hopper/package.json +++ b/packages/data-gpu-hopper/package.json @@ -1,6 +1,6 @@ { "name": "data-gpu-hopper", - "version": "0.9.94", + "version": "0.9.95", "description": "Hopper sample - real-time ECS game rendered as colored cubes via @adobe/data-gpu", "type": "module", "private": true, diff --git a/packages/data-gpu-samples/package.json b/packages/data-gpu-samples/package.json index f5b67813..8cc4052d 100644 --- a/packages/data-gpu-samples/package.json +++ b/packages/data-gpu-samples/package.json @@ -1,6 +1,6 @@ { "name": "data-gpu-samples", - "version": "0.9.94", + "version": "0.9.95", "description": "WebGPU samples built on @adobe/data-gpu", "type": "module", "private": true, diff --git a/packages/data-gpu/package.json b/packages/data-gpu/package.json index c31512eb..6dfb5f21 100644 --- a/packages/data-gpu/package.json +++ b/packages/data-gpu/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-gpu", - "version": "0.9.94", + "version": "0.9.95", "description": "Adobe data WebGPU plugins and types for graphics and compute", "type": "module", "private": false, diff --git a/packages/data-lit-space-rock-game/package.json b/packages/data-lit-space-rock-game/package.json index 23d72670..390dc411 100644 --- a/packages/data-lit-space-rock-game/package.json +++ b/packages/data-lit-space-rock-game/package.json @@ -1,6 +1,6 @@ { "name": "data-lit-space-rock-game", - "version": "0.9.94", + "version": "0.9.95", "description": "Space Rock Game sample - real-time ECS game with Lit and @adobe/data", "type": "module", "private": true, diff --git a/packages/data-lit-tictactoe/package.json b/packages/data-lit-tictactoe/package.json index f5b8806b..2697ecc2 100644 --- a/packages/data-lit-tictactoe/package.json +++ b/packages/data-lit-tictactoe/package.json @@ -1,6 +1,6 @@ { "name": "data-lit-tictactoe", - "version": "0.9.94", + "version": "0.9.95", "description": "Tic-Tac-Toe sample - Lit web components with @adobe/data-lit and AgenticService", "type": "module", "private": true, diff --git a/packages/data-lit-todo/package.json b/packages/data-lit-todo/package.json index 92034056..3ac91d40 100644 --- a/packages/data-lit-todo/package.json +++ b/packages/data-lit-todo/package.json @@ -1,6 +1,6 @@ { "name": "data-lit-todo", - "version": "0.9.94", + "version": "0.9.95", "description": "Todo application - Lit web components with @adobe/data ECS", "type": "module", "private": true, diff --git a/packages/data-lit/package.json b/packages/data-lit/package.json index ed872a09..8f6b0e0f 100644 --- a/packages/data-lit/package.json +++ b/packages/data-lit/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-lit", - "version": "0.9.94", + "version": "0.9.95", "description": "Adobe data Lit bindings - hooks, elements, decorators", "type": "module", "private": false, diff --git a/packages/data-p2p-tictactoe/package.json b/packages/data-p2p-tictactoe/package.json index 164f6c65..b0c5d83f 100644 --- a/packages/data-p2p-tictactoe/package.json +++ b/packages/data-p2p-tictactoe/package.json @@ -1,6 +1,6 @@ { "name": "data-p2p-tictactoe", - "version": "0.9.94", + "version": "0.9.95", "description": "Serverless P2P tic-tac-toe — WebRTC DataChannel + @adobe/data-sync", "type": "module", "private": true, diff --git a/packages/data-persistence/package.json b/packages/data-persistence/package.json index b1c99f7c..6c1d222b 100644 --- a/packages/data-persistence/package.json +++ b/packages/data-persistence/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-persistence", - "version": "0.9.94", + "version": "0.9.95", "description": "Worker-based incremental persistence layer for @adobe/data ECS over OPFS (browser) and node:fs (server).", "type": "module", "sideEffects": false, diff --git a/packages/data-react-hello/package.json b/packages/data-react-hello/package.json index 5609d23c..551448b0 100644 --- a/packages/data-react-hello/package.json +++ b/packages/data-react-hello/package.json @@ -1,6 +1,6 @@ { "name": "data-react-hello", - "version": "0.9.94", + "version": "0.9.95", "description": "Hello World sample - click counter using @adobe/data-react", "type": "module", "private": true, diff --git a/packages/data-react-pixie/package.json b/packages/data-react-pixie/package.json index 9c85c23d..71dde449 100644 --- a/packages/data-react-pixie/package.json +++ b/packages/data-react-pixie/package.json @@ -1,6 +1,6 @@ { "name": "data-react-pixie", - "version": "0.9.94", + "version": "0.9.95", "description": "PixiJS React sample - ECS sprites (bunny, fox) with @adobe/data-react", "type": "module", "private": true, diff --git a/packages/data-react/package.json b/packages/data-react/package.json index 1ce6dbb9..26dd67fa 100644 --- a/packages/data-react/package.json +++ b/packages/data-react/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-react", - "version": "0.9.94", + "version": "0.9.95", "description": "Adobe data React bindings — hooks and context for ECS database", "type": "module", "private": false, diff --git a/packages/data-solid-dashboard/package.json b/packages/data-solid-dashboard/package.json index ebd903ac..c9939e59 100644 --- a/packages/data-solid-dashboard/package.json +++ b/packages/data-solid-dashboard/package.json @@ -1,6 +1,6 @@ { "name": "data-solid-dashboard", - "version": "0.9.94", + "version": "0.9.95", "description": "Mini dashboard sample — multiple components sharing one @adobe/data ECS database with SolidJS", "type": "module", "private": true, diff --git a/packages/data-solid/package.json b/packages/data-solid/package.json index 213187b2..f7812d68 100644 --- a/packages/data-solid/package.json +++ b/packages/data-solid/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-solid", - "version": "0.9.94", + "version": "0.9.95", "description": "Adobe data SolidJS bindings — context and provider for ECS database", "type": "module", "private": false, diff --git a/packages/data-sync/package.json b/packages/data-sync/package.json index 3aa72e75..93e979af 100644 --- a/packages/data-sync/package.json +++ b/packages/data-sync/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-sync", - "version": "0.9.94", + "version": "0.9.95", "description": "Multi-user real-time synchronisation for @adobe/data ECS — server, client, and in-process loopback.", "type": "module", "sideEffects": false, diff --git a/packages/data/package.json b/packages/data/package.json index b82a6da5..cea715dd 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data", - "version": "0.9.94", + "version": "0.9.95", "description": "Adobe data oriented programming library", "type": "module", "sideEffects": false, diff --git a/packages/data/src/ecs/database/observe-select-entities.test.ts b/packages/data/src/ecs/database/observe-select-entities.test.ts index 2454f347..ec97e89b 100644 --- a/packages/data/src/ecs/database/observe-select-entities.test.ts +++ b/packages/data/src/ecs/database/observe-select-entities.test.ts @@ -562,6 +562,64 @@ describe("observeSelectEntities", () => { }); }); + describe("Set-based include (archetype.components)", () => { + // `observe.select` accepts `ReadonlySet` — exactly the shape + // `archetype.components` has. Every other test in this file passes an + // array, so the Set path is unexercised. The select-observe cache keys + // on `JSON.stringify({ include, options })`, and `JSON.stringify(aSet)` + // is always `"{}"`, so two selects made with *different* component Sets + // collide on one cache key and the second is served the first's observe + // function. With a single Set-based subscription (a minimal chain) the + // collision is invisible; with two or more (a composed graph) it bites. + + it("should return distinct results for selects made with different component Sets", () => { + const positionObserver = vi.fn(); + const healthObserver = vi.fn(); + + database.observe.select(new Set(["position"]))(positionObserver); + database.observe.select(new Set(["health"]))(healthObserver); + + const positionResult = positionObserver.mock.calls[0][0]; + const healthResult = healthObserver.mock.calls[0][0]; + + expect(positionResult).toContain(entities.pos1); + expect(positionResult).not.toContain(entities.health1); + + expect(healthResult).toContain(entities.health1); + expect(healthResult).not.toContain(entities.pos1); + }); + + it("should re-emit on membership contraction for a Set-based select after another Set-based select was made", async () => { + // A prior subscription keyed on a different component Set poisons the + // shared cache key, so the select below is wrongly served it. + database.observe.select(new Set(["position"]))(vi.fn()); + + const observer = vi.fn(); + const unsubscribe = database.observe.select(new Set(["position", "health"]))(observer); + + // Initial emission looks correct: posHealth1 has position (the + // poisoned observe's include), so it is present. + expect(observer).toHaveBeenCalledTimes(1); + expect(observer.mock.calls[0][0]).toContain(entities.posHealth1); + + // Remove health: posHealth1 migrates out of the position+health set. + database.transactions.updateEntity({ + entity: entities.posHealth1, + values: { health: undefined } + }); + + await Promise.resolve(); + + // Must re-emit without posHealth1. With the collapsed cache key the + // subscription is actually observing {position}, for which removing + // health is a no-op, so no notification fires. + expect(observer).toHaveBeenCalledTimes(2); + expect(observer.mock.calls[1][0]).not.toContain(entities.posHealth1); + + unsubscribe(); + }); + }); + describe("Filtering and ordering", () => { it("should support where clause filtering", () => { const observer = vi.fn(); diff --git a/packages/data/src/ecs/database/observe-select-entities.ts b/packages/data/src/ecs/database/observe-select-entities.ts index e2f178ce..42c62d19 100644 --- a/packages/data/src/ecs/database/observe-select-entities.ts +++ b/packages/data/src/ecs/database/observe-select-entities.ts @@ -150,7 +150,7 @@ export const observeSelectEntities = (store: ReadonlyStore, options?: EntitySelectOptions> ) => { - const key = JSON.stringify({ include, options }); + const key = canonicalSelectKey(include, options); let observeFunction = cachedSelectObserveFunctions.get(key); if (!observeFunction) { observeFunction = Observe.withCache(createSelectObserveFunction(include, options)); @@ -158,4 +158,46 @@ export const observeSelectEntities = (store: ReadonlyStore( + include: readonly string[] | ReadonlySet, + options?: EntitySelectOptions, +): string => { + let key = [...include].sort().join(ELEMENT); + if (!options) return key; + if (options.exclude && options.exclude.length > 0) { + key += SECTION + "x" + [...options.exclude].sort().join(ELEMENT); + } + if (options.where) { + const parts = Object.entries(options.where) + .map(([k, v]) => k + "=" + JSON.stringify(v)) + .sort(); + if (parts.length > 0) key += SECTION + "w" + parts.join(ELEMENT); + } + if (options.order) { + const parts = Object.entries(options.order) + .map(([k, v]) => k + "=" + (v ? "1" : "0")); + if (parts.length > 0) key += SECTION + "o" + parts.join(ELEMENT); + } + return key; +}; \ No newline at end of file