From 4a309b4b95ec957629cfd2bab40773eab4a41472 Mon Sep 17 00:00:00 2001 From: Kris Nye Date: Tue, 9 Jun 2026 08:34:37 -0700 Subject: [PATCH 1/5] =?UTF-8?q?feat(ecs):=20export=20Database.Plugin.ToCom?= =?UTF-8?q?putedDb=20=E2=80=94=20computed-db=20alias=20without=20Omit=20ha?= =?UTF-8?q?ck?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bump: 0.9.68 Co-Authored-By: Claude Sonnet 4.6 --- package.json | 2 +- packages/data-lit-tictactoe/package.json | 2 +- packages/data-lit-todo/package.json | 2 +- packages/data-lit/package.json | 2 +- packages/data-p2p-tictactoe/package.json | 2 +- packages/data-persistence/package.json | 2 +- packages/data-react-hello/package.json | 2 +- packages/data-react-pixie/package.json | 2 +- packages/data-react/package.json | 2 +- packages/data-solid-dashboard/package.json | 2 +- packages/data-solid/package.json | 2 +- packages/data-sync/package.json | 2 +- packages/data/package.json | 2 +- packages/data/src/ecs/database/database.ts | 27 ++++++++++++++++++++++ 14 files changed, 40 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 704c049..4ed6201 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "data-monorepo", - "version": "0.9.67", + "version": "0.9.68", "private": true, "scripts": { "build": "pnpm -r run build", diff --git a/packages/data-lit-tictactoe/package.json b/packages/data-lit-tictactoe/package.json index 94610b1..24159d7 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.67", + "version": "0.9.68", "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 9a7c273..bd45bbd 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.67", + "version": "0.9.68", "description": "Todo sample app demonstrating @adobe/data with Lit", "type": "module", "private": true, diff --git a/packages/data-lit/package.json b/packages/data-lit/package.json index 91a6b1c..b8cbd5e 100644 --- a/packages/data-lit/package.json +++ b/packages/data-lit/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-lit", - "version": "0.9.67", + "version": "0.9.68", "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 55560a4..9c2bb85 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.67", + "version": "0.9.68", "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 c8b9012..2a902e4 100644 --- a/packages/data-persistence/package.json +++ b/packages/data-persistence/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-persistence", - "version": "0.9.67", + "version": "0.9.68", "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 8ebf1ac..ca078f8 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.67", + "version": "0.9.68", "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 d1b6558..1ad70eb 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.67", + "version": "0.9.68", "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 9c7bfbf..f171f51 100644 --- a/packages/data-react/package.json +++ b/packages/data-react/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-react", - "version": "0.9.67", + "version": "0.9.68", "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 7680976..1201ea3 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.67", + "version": "0.9.68", "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 f245ce6..a15cc62 100644 --- a/packages/data-solid/package.json +++ b/packages/data-solid/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-solid", - "version": "0.9.67", + "version": "0.9.68", "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 a541661..624b070 100644 --- a/packages/data-sync/package.json +++ b/packages/data-sync/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-sync", - "version": "0.9.67", + "version": "0.9.68", "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 f42375f..01a2038 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data", - "version": "0.9.67", + "version": "0.9.68", "description": "Adobe data oriented programming library", "type": "module", "sideEffects": false, diff --git a/packages/data/src/ecs/database/database.ts b/packages/data/src/ecs/database/database.ts index b1b93da..ed0f93c 100644 --- a/packages/data/src/ecs/database/database.ts +++ b/packages/data/src/ecs/database/database.ts @@ -318,6 +318,33 @@ export namespace Database { export const combine = combinePlugins; export type ToDatabase

= Database.FromPlugin

; export type ToStore

= Store>, FromSchemas>, RemoveIndex>; + /** + * The database type seen *inside a computed factory* — same shape as + * `ToDatabase

` but with `computed: unknown`. + * + * Use this when aliasing the database for a module that doesn't consume + * `db.computed`, or anywhere `ToDatabase

` would force a dependency on + * the fully-resolved computed-values type and break inference: + * + * ```ts + * // ❌ hand-reconstructs the framework's contract + * type CoreStateDatabase = Omit, 'computed'>; + * + * // ✅ first-class contract + * type CoreStateDatabase = Database.Plugin.ToComputedDb; + * ``` + */ + export type ToComputedDb

= Database< + FromSchemas>, + FromSchemas>, + RemoveIndex, + ToTransactionFunctions>, + StringKeyof, + ToActionFunctions>, + FromServiceFactories>, + unknown, + RemoveIndex + >; /** * The plugin's store as seen *inside a transaction body* — i.e. `ToStore

` * plus the `userId` field added by the transaction dispatcher. Use this From 780781ce8791660b0a964085fb40cb260fe3cbb2 Mon Sep 17 00:00:00 2001 From: Kris Nye Date: Tue, 9 Jun 2026 08:52:56 -0700 Subject: [PATCH 2/5] feat(ecs): surface base-plugin computeds inside derived computed factories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FullDBForPlugin hardcoded the computed-factory db's CV slot to `unknown`, which erased the extends/imports base's already-resolved computeds — not just the current plugin's in-progress ones. A derived plugin's computed factory therefore could not compose on a base plugin's computed. Set CV to FromComputedFactories. XP is AmbientPlugin at the call sites, so this surfaces the base + imports computeds (all fully constructed — no circularity). The current plugin's own CVF is never a parameter to this type, so in-progress siblings stay hidden, matching the rule actions/systems already follow. Adds type-tests for both directions (base composition works; sibling stays hidden) and corrects the ToComputedDb doc now that factories see base computeds rather than `unknown`. Co-Authored-By: Claude Sonnet 4.6 --- .../data/src/ecs/database/create-plugin.ts | 15 ++++--- .../ecs/database/create-plugin.type-test.ts | 41 +++++++++++++++++++ packages/data/src/ecs/database/database.ts | 15 ++++--- 3 files changed, 61 insertions(+), 10 deletions(-) diff --git a/packages/data/src/ecs/database/create-plugin.ts b/packages/data/src/ecs/database/create-plugin.ts index 1b2afd2..8661b88 100644 --- a/packages/data/src/ecs/database/create-plugin.ts +++ b/packages/data/src/ecs/database/create-plugin.ts @@ -136,11 +136,16 @@ type FullDBForPlugin< S | StringKeyof, ToActionFunctions, FromServiceFactories & XP['services']>, - // 8: CV — placeholder. Kept `unknown` (Database's own default for this slot) - // so a concrete computed-values type never constrains computed-factory - // inference / breaks contravariance. Must be supplied explicitly because - // slot 9 (IX) sits after it. - unknown, + // 8: CV — the base plugin's already-resolved computeds. XP is + // AmbientPlugin at the call sites, so XP['computed'] carries the + // `extends` base + `imports` deps' computeds — all fully constructed, so + // surfacing them here is sound (nothing in-progress, no circularity). + // The current plugin's OWN computeds (CVF) are deliberately NOT a + // parameter to this type, so they never flow into their own factory db: + // a computed cannot reference an in-progress sibling, but it CAN compose + // on a base plugin's computed with full types — same rule actions/systems + // already follow. Resolves to `{}` for the common no-base-computeds case. + FromComputedFactories, // 9: IX — thread the index declarations so `db.indexes` is populated inside // computed factories, same as the actions/systems `db` already does. // XP is AmbientPlugin at the call sites, so XP['indexes'] carries diff --git a/packages/data/src/ecs/database/create-plugin.type-test.ts b/packages/data/src/ecs/database/create-plugin.type-test.ts index b65ee7b..74e306c 100644 --- a/packages/data/src/ecs/database/create-plugin.type-test.ts +++ b/packages/data/src/ecs/database/create-plugin.type-test.ts @@ -134,6 +134,30 @@ function validTypeInferenceTests() { }, }); + // Test: a derived plugin's computed factory can read a BASE plugin's + // already-resolved computed (composition across `extends`). This is the + // payoff of FullDBForPlugin surfacing FromComputedFactories + // rather than `unknown` in the computed-factory db. + const baseComputedPlugin = createPlugin({ + resources: { + n: { default: 10 as number }, + }, + computed: { + doubled: (db) => Observe.withMap(db.observe.resources.n, (v) => v * 2), + }, + }); + + const derivedComputedPlugin = createPlugin({ + extends: baseComputedPlugin, + computed: { + quadrupled: (db) => { + // Valid - the base plugin's computed is fully typed here. + const base: Observe = db.computed.doubled; + return Observe.withMap(base, (v) => v * 2); + }, + }, + }); + // Test: Computed + transactions co-inference // When both computed and transactions are defined in the same plugin, // TypeScript must infer TD from the transactions property independently @@ -507,6 +531,23 @@ function invalidComputedReturnsObject() { }); } +// Test: Invalid sibling computed access — an in-progress computed in the SAME +// plugin is not visible to a sibling factory (would be circular). Only a +// base plugin's already-resolved computeds are surfaced. +function invalidSiblingComputedAccess() { + createPlugin({ + resources: { n: { default: 1 as number } }, + computed: { + first: (db) => Observe.fromConstant(db.resources.n), + second: (db) => { + // @ts-expect-error - sibling computed 'first' is in-progress, not visible + const _f = db.computed.first; + return Observe.fromConstant(0); + }, + }, + }); +} + // Test: Invalid transaction call in action function invalidTransactionCallInAction() { createPlugin({ diff --git a/packages/data/src/ecs/database/database.ts b/packages/data/src/ecs/database/database.ts index ed0f93c..3c7819d 100644 --- a/packages/data/src/ecs/database/database.ts +++ b/packages/data/src/ecs/database/database.ts @@ -319,12 +319,12 @@ export namespace Database { export type ToDatabase

= Database.FromPlugin

; export type ToStore

= Store>, FromSchemas>, RemoveIndex>; /** - * The database type seen *inside a computed factory* — same shape as - * `ToDatabase

` but with `computed: unknown`. + * `ToDatabase

` with the computed surface erased (`computed: unknown`). * - * Use this when aliasing the database for a module that doesn't consume - * `db.computed`, or anywhere `ToDatabase

` would force a dependency on - * the fully-resolved computed-values type and break inference: + * Use this to alias a plugin's database in a context that must not depend + * on the fully-resolved computed-values type — e.g. to break a type cycle, + * or in a module that never reads `db.computed`. It replaces the hand-rolled + * `Omit` that reconstructs the same shape: * * ```ts * // ❌ hand-reconstructs the framework's contract @@ -333,6 +333,11 @@ export namespace Database { * // ✅ first-class contract * type CoreStateDatabase = Database.Plugin.ToComputedDb; * ``` + * + * Note: this is *not* the db a computed factory receives. A factory of a + * plugin that `extends` a base sees that base's already-resolved computeds + * (so it can compose on them); the factory's own in-progress siblings stay + * hidden. When you want that fully-resolved surface, use `ToDatabase

`. */ export type ToComputedDb

= Database< FromSchemas>, From 77436e84497f9904b338267bf253bc6fe02677b3 Mon Sep 17 00:00:00 2001 From: Kris Nye Date: Tue, 9 Jun 2026 08:56:16 -0700 Subject: [PATCH 3/5] refactor(ecs): drop Database.Plugin.ToComputedDb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With computed factories now seeing the base plugin's resolved computeds, the computed-erased alias no longer mirrors the factory context — the db a factory receives is no longer `computed: unknown`. Its only residual use (strip computed to break a type cycle) is speculative, has no consumer, and is a near-duplicate of ToDatabase; the rare call site can write `Omit, 'computed'>` inline. Co-Authored-By: Claude Sonnet 4.6 --- packages/data/src/ecs/database/database.ts | 32 ---------------------- 1 file changed, 32 deletions(-) diff --git a/packages/data/src/ecs/database/database.ts b/packages/data/src/ecs/database/database.ts index 3c7819d..b1b93da 100644 --- a/packages/data/src/ecs/database/database.ts +++ b/packages/data/src/ecs/database/database.ts @@ -318,38 +318,6 @@ export namespace Database { export const combine = combinePlugins; export type ToDatabase

= Database.FromPlugin

; export type ToStore

= Store>, FromSchemas>, RemoveIndex>; - /** - * `ToDatabase

` with the computed surface erased (`computed: unknown`). - * - * Use this to alias a plugin's database in a context that must not depend - * on the fully-resolved computed-values type — e.g. to break a type cycle, - * or in a module that never reads `db.computed`. It replaces the hand-rolled - * `Omit` that reconstructs the same shape: - * - * ```ts - * // ❌ hand-reconstructs the framework's contract - * type CoreStateDatabase = Omit, 'computed'>; - * - * // ✅ first-class contract - * type CoreStateDatabase = Database.Plugin.ToComputedDb; - * ``` - * - * Note: this is *not* the db a computed factory receives. A factory of a - * plugin that `extends` a base sees that base's already-resolved computeds - * (so it can compose on them); the factory's own in-progress siblings stay - * hidden. When you want that fully-resolved surface, use `ToDatabase

`. - */ - export type ToComputedDb

= Database< - FromSchemas>, - FromSchemas>, - RemoveIndex, - ToTransactionFunctions>, - StringKeyof, - ToActionFunctions>, - FromServiceFactories>, - unknown, - RemoveIndex - >; /** * The plugin's store as seen *inside a transaction body* — i.e. `ToStore

` * plus the `userId` field added by the transaction dispatcher. Use this From 750daf5c62939b9128b49356bd07ed6071a334a0 Mon Sep 17 00:00:00 2001 From: Kris Nye Date: Tue, 9 Jun 2026 09:08:03 -0700 Subject: [PATCH 4/5] chore: add root lint/typecheck scripts; drop dead lint scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `pnpm run lint` / `pnpm run typecheck` now work from the repo root (recursive, matching the existing `build`/`test` pattern) — no need to know which package to cd into. Removed the phantom `lint` scripts from data-sync and data-persistence: they had no eslint config or dependency and always errored, which would otherwise break the recursive root lint. CI already lints only @adobe/data, so nothing relied on them. Co-Authored-By: Claude Sonnet 4.6 --- package.json | 3 +++ packages/data-persistence/package.json | 1 - packages/data-sync/package.json | 1 - 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 4ed6201..577205a 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,9 @@ "scripts": { "build": "pnpm -r run build", "test": "pnpm -r run test", + "lint": "pnpm -r run lint", + "lint-fix": "pnpm -r run lint-fix", + "typecheck": "pnpm -r run typecheck", "dev": "pnpm -r --parallel run dev", "dev:data": "pnpm --filter @adobe/data run dev", "link": "pnpm -r --filter @adobe/data* run link", diff --git a/packages/data-persistence/package.json b/packages/data-persistence/package.json index 2a902e4..6abc0b4 100644 --- a/packages/data-persistence/package.json +++ b/packages/data-persistence/package.json @@ -18,7 +18,6 @@ "build": "cp ../../LICENSE . 2>/dev/null || true; tsc -b", "clean": "rm -rf dist node_modules", "dev": "tsc -b -w --preserveWatchOutput", - "lint": "pnpm eslint .", "test": "pnpm exec playwright install chromium && vitest --run", "test:node": "vitest --run --project=node", "test:browser": "vitest --run --project=browser", diff --git a/packages/data-sync/package.json b/packages/data-sync/package.json index 624b070..6936bcb 100644 --- a/packages/data-sync/package.json +++ b/packages/data-sync/package.json @@ -18,7 +18,6 @@ "build": "cp ../../LICENSE . 2>/dev/null || true; tsc -b", "clean": "rm -rf dist node_modules", "dev": "tsc -b -w --preserveWatchOutput", - "lint": "pnpm eslint .", "test": "vitest --run --project=node", "test:node": "vitest --run --project=node" }, From 8734bbff58c338a591d3357c2802e3e30ee63a1f Mon Sep 17 00:00:00 2001 From: Kris Nye Date: Tue, 9 Jun 2026 09:15:49 -0700 Subject: [PATCH 5/5] test(ecs): prove standalone-computed factory aggregation type-checks cleanly Adds a compile-time test for a common app pattern: computed factories authored as standalone functions (annotated with the exported `Database.Plugin.ToDatabase`) aggregated into one `computed: {}` block in a createPlugin call. Covers a plain resource read, a factory composing on a base computed, and a parameterized computed; asserts the aggregated result types exactly, with no `Omit` and no cast. Verified load-bearing: reverting the FullDBForPlugin CV slot to `unknown` breaks this exact assignment with TS2322 (`unknown` not assignable to the factory's `computed`), which is the contravariance failure that forced the old `Omit, 'computed'>` workaround. Co-Authored-By: Claude Sonnet 4.6 --- .../ecs/database/create-plugin.type-test.ts | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/packages/data/src/ecs/database/create-plugin.type-test.ts b/packages/data/src/ecs/database/create-plugin.type-test.ts index 74e306c..5aeb9f5 100644 --- a/packages/data/src/ecs/database/create-plugin.type-test.ts +++ b/packages/data/src/ecs/database/create-plugin.type-test.ts @@ -329,6 +329,72 @@ function validTypeInferenceTests() { }); } +// ============================================================================ +// Standalone computed factories aggregated in a create call +// ============================================================================ +// +// A common app pattern: computed factories are authored as standalone +// functions in their own files, then aggregated into one `computed: {}` block +// in a single createPlugin call. Each standalone factory must annotate its +// `db` parameter with a NAMED, exported type (it lives in a different module +// from the plugin). +// +// Before the FullDBForPlugin fix the factory db's `computed` slot was +// `unknown`, so the annotation had to strip it: +// type CoreStateDatabase = Omit, 'computed'>; +// Now the factory sees the base plugin's already-resolved computeds, so the +// plain exported `Database.Plugin.ToDatabase` is the correct, +// cast-free annotation — AND it lets a standalone factory compose on a base +// computed. This test proves the round trip type-checks cleanly: no `as`, no +// `Omit`, no `@ts-expect-error`. +function standaloneComputedFactoryAggregation() { + // The base "state" plugin owns the resources and one base computed. + const coreStatePlugin = createPlugin({ + resources: { + count: { default: 0 as number }, + }, + computed: { + count: (db) => db.observe.resources.count, + }, + }); + + // The single exported alias every standalone factory annotates against. + type CoreStateDatabase = Database.Plugin.ToDatabase; + + // Standalone factory (own file): reads a base resource. + const doubled = (db: CoreStateDatabase) => + Observe.withMap(db.observe.resources.count, (v) => v * 2); + + // Standalone factory (own file): composes on the base plugin's computed. + // This is the case the fix unlocks — `db.computed.count` is fully typed. + const isPositive = (db: CoreStateDatabase) => + Observe.withMap(db.computed.count, (v) => v > 0); + + // Standalone factory (own file): a parameterized computed that also reads + // a base computed. + const atLeast = (db: CoreStateDatabase) => (min: number) => + Observe.withMap(db.computed.count, (v) => v >= min); + + // Aggregate the independently-authored factories into the derived plugin's + // computed block — clean assignment, no cast. + const derived = createPlugin({ + extends: coreStatePlugin, + computed: { + doubled, + isPositive, + atLeast, + }, + }); + + // The resulting plugin exposes the aggregated computeds with exact types, + // alongside the inherited base computed. + type DerivedComputed = Database.FromPlugin['computed']; + type _CheckDoubled = Assert>>; + type _CheckIsPositive = Assert>>; + type _CheckAtLeast = Assert Observe>>; + type _CheckInheritedCount = Assert>>; +} + // ============================================================================ // INVALID TYPE INFERENCE TESTS // ============================================================================