From e4235027ef0ee525269494e5d8bce614f0b2132c Mon Sep 17 00:00:00 2001 From: Kris Nye Date: Wed, 15 Jul 2026 16:32:21 -0700 Subject: [PATCH 1/2] =?UTF-8?q?test(data):=20document=20the=20composition?= =?UTF-8?q?=E2=86=92derive=20pattern=20for=20resource=20reads?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a db.derive unit test that derives a value from two resources (`db.derive(db => db.resources.x + db.resources.y)`) and asserts it recomputes when either resource changes — the auto-tracked replacement for `Observe.withMap(Observe.fromProperties({ x, y }), ({ x, y }) => x + y)`. The callback param is intentionally named `db` to shadow the outer db so a read cannot bind the outer (full) db and escape dependency tracking. Co-Authored-By: Claude Opus 4.8 --- .../src/ecs/database/observe-derive.test.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/packages/data/src/ecs/database/observe-derive.test.ts b/packages/data/src/ecs/database/observe-derive.test.ts index f8f32c76..ca5c5875 100644 --- a/packages/data/src/ecs/database/observe-derive.test.ts +++ b/packages/data/src/ecs/database/observe-derive.test.ts @@ -125,6 +125,43 @@ describe("db.derive", () => { unsubscribe(); }); + // Documents the composition → derive migration for resource reads: + // before: Observe.withMap( + // Observe.fromProperties({ x: db.observe.resources.x, y: db.observe.resources.y }), + // ({ x, y }) => x + y) + // after: db.derive(db => db.resources.x + db.resources.y) + // The derive records BOTH resource reads and recomputes when either changes, + // with no hand-listed inputs. The callback param is deliberately named `db` + // to shadow the outer db, so a read cannot bind the outer (full) db and + // escape dependency tracking. + it("derives a value from multiple resources, recomputing when either changes", () => { + const sumDb = Database.create( + Database.Plugin.create({ + resources: { + x: { type: "number", default: 1 }, + y: { type: "number", default: 2 }, + }, + transactions: { + setX(store, args: { x: number }) { + store.resources.x = args.x; + }, + setY(store, args: { y: number }) { + store.resources.y = args.y; + }, + }, + }), + ); + const observer = vi.fn(); + const unsubscribe = sumDb.derive((db) => db.resources.x + db.resources.y)(observer); + expect(observer).toHaveBeenLastCalledWith(3); // initial: 1 + 2 + sumDb.transactions.setX({ x: 10 }); + expect(observer).toHaveBeenLastCalledWith(12); // 10 + 2 + sumDb.transactions.setY({ y: 5 }); + expect(observer).toHaveBeenLastCalledWith(15); // 10 + 5 + expect(observer).toHaveBeenCalledTimes(3); + unsubscribe(); + }); + it("re-emits on a change to an index bucket it read", async () => { const observer = vi.fn(); const unsubscribe = db.derive((d) => d.indexes.byA.find({ a: 1 }).length)(observer); From 33b21981bbc3b75307a1564a819d0aefb8d41e2a Mon Sep 17 00:00:00 2001 From: Kris Nye Date: Wed, 15 Jul 2026 17:00:43 -0700 Subject: [PATCH 2/2] test(data): pin that derive re-execution rebuilds the dependency set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A derive that conditionally reads `b` (only while `a > 0`) must stop re-running on `b` changes once `a` flips and the next run records deps without `b` — verifying each run replaces the observed set rather than accumulating it. Co-Authored-By: Claude Opus 4.8 --- .../data/src/ecs/database/observe-derive.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/data/src/ecs/database/observe-derive.test.ts b/packages/data/src/ecs/database/observe-derive.test.ts index ca5c5875..d0865e1b 100644 --- a/packages/data/src/ecs/database/observe-derive.test.ts +++ b/packages/data/src/ecs/database/observe-derive.test.ts @@ -162,6 +162,21 @@ describe("db.derive", () => { unsubscribe(); }); + it("re-execution rebuilds the dependency set: a read dropped last run no longer triggers", () => { + // Reads `b` only while `a > 0`. Once `a` flips non-positive, the next run + // records deps WITHOUT `b`, so a later `b` change must not re-run the body. + const compute = vi.fn((d: any) => (d.get(foo, "a") > 0 ? d.get(foo, "b") : -1)); + const unsubscribe = db.derive(compute)(() => {}); + expect(compute).toHaveBeenCalledTimes(1); // a=1>0 → reads a AND b + db.transactions.setB({ e: foo, b: 20 }); + expect(compute).toHaveBeenCalledTimes(2); // b is a dep → re-runs + db.transactions.setA({ e: foo, a: -1 }); + expect(compute).toHaveBeenCalledTimes(3); // a is a dep → re-runs; now reads a only + db.transactions.setB({ e: foo, b: 30 }); + expect(compute).toHaveBeenCalledTimes(3); // b dropped from deps → NO re-run + unsubscribe(); + }); + it("re-emits on a change to an index bucket it read", async () => { const observer = vi.fn(); const unsubscribe = db.derive((d) => d.indexes.byA.find({ a: 1 }).length)(observer);