diff --git a/package.json b/package.json index 704c049..577205a 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,13 @@ { "name": "data-monorepo", - "version": "0.9.67", + "version": "0.9.68", "private": true, "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-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..6abc0b4 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, @@ -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-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..6936bcb 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, @@ -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" }, 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/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..5aeb9f5 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 @@ -305,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 // ============================================================================ @@ -507,6 +597,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({