diff --git a/package.json b/package.json index c42b2df..704c049 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "data-monorepo", - "version": "0.9.66", + "version": "0.9.67", "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 0cc9a89..94610b1 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.66", + "version": "0.9.67", "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 5c07efa..9a7c273 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.66", + "version": "0.9.67", "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 74a49d6..91a6b1c 100644 --- a/packages/data-lit/package.json +++ b/packages/data-lit/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-lit", - "version": "0.9.66", + "version": "0.9.67", "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 d24e23c..55560a4 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.66", + "version": "0.9.67", "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 c19d47b..c8b9012 100644 --- a/packages/data-persistence/package.json +++ b/packages/data-persistence/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-persistence", - "version": "0.9.66", + "version": "0.9.67", "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 f8e9b28..8ebf1ac 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.66", + "version": "0.9.67", "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 7da9c97..d1b6558 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.66", + "version": "0.9.67", "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 87f1301..9c7bfbf 100644 --- a/packages/data-react/package.json +++ b/packages/data-react/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-react", - "version": "0.9.66", + "version": "0.9.67", "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 81e48f5..7680976 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.66", + "version": "0.9.67", "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 d93be7a..f245ce6 100644 --- a/packages/data-solid/package.json +++ b/packages/data-solid/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-solid", - "version": "0.9.66", + "version": "0.9.67", "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 98dc950..a541661 100644 --- a/packages/data-sync/package.json +++ b/packages/data-sync/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-sync", - "version": "0.9.66", + "version": "0.9.67", "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 a2c4fd4..f42375f 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data", - "version": "0.9.66", + "version": "0.9.67", "description": "Adobe data oriented programming library", "type": "module", "sideEffects": false, diff --git a/packages/data/src/ecs/README.md b/packages/data/src/ecs/README.md index 3772c79..91e284b 100644 --- a/packages/data/src/ecs/README.md +++ b/packages/data/src/ecs/README.md @@ -135,6 +135,27 @@ const worldPlugin = Database.Plugin.create({ }); ``` +### Naming an archetype's row type + +Each `db.archetypes.` is a `ReadonlyArchetype` whose `Row` is +derived from the declared columns. To name that row elsewhere — a function +parameter, a public field — **derive it; don't re-declare it.** Derive the +service type from the plugin, then pull the row out with +`Database.Archetype.RowOf`: + +```ts +type WorldService = Database.Plugin.ToDatabase; +type PlayerRow = Database.Archetype.RowOf; + +const a: ReadonlyArchetype = db.archetypes.Player; // no cast +``` + +Because the service type is derived from the same plugin, `db.archetypes` +assigns to `WorldService["archetypes"]` directly. Do **not** hand-author a +narrower archetype row and `as`-cast `db.archetypes` to fit it — a hand-written +type drifts from the real columns, and the cast hides that drift. Let the row +follow from the declaration. + ## Indexes Indexes give O(1) lookup by some derived or column-valued key. Declare them on the plugin alongside components and archetypes; the runtime maintains them automatically on every insert/update/delete and exposes typed lookup handles at `db.indexes.` and `t.indexes.` (inside transactions). diff --git a/packages/data/src/ecs/database/archetype-row.type-test.ts b/packages/data/src/ecs/database/archetype-row.type-test.ts new file mode 100644 index 0000000..620d85a --- /dev/null +++ b/packages/data/src/ecs/database/archetype-row.type-test.ts @@ -0,0 +1,63 @@ +// © 2026 Adobe. MIT License. See /LICENSE for details. + +import { createPlugin } from "./create-plugin.js"; +import { Database } from "./database.js"; +import { Assert } from "../../types/assert.js"; +import { Equal } from "../../types/equal.js"; +import { Extends } from "../../types/types.js"; +import { ReadonlyArchetype } from "../archetype/index.js"; +import { Entity } from "../entity/entity.js"; +import { Observe } from "../../observe/index.js"; + +/** + * Type-only tests for naming archetype rows without a cast. + * + * The regression these guard against: a consumer who exposes + * `db.archetypes.Track` under a hand-authored service interface used to need + * `as unknown as` at every boundary. The fix is to *derive* the service type + * from the plugin and *name* rows with `Database.Archetype.RowOf` rather than + * re-spelling their columns. These are compile-time checks only. + */ + +const trackPlugin = createPlugin({ + components: { + trackKind: { type: "string" }, + editingMode: { type: "string" }, + muted: { type: "boolean" }, + }, + archetypes: { + Track: ["trackKind", "editingMode", "muted"], + }, +}); + +// A service type derived from the plugin — the recommended pattern. +type MainService = Database.Plugin.ToDatabase; + +declare const db: MainService; + +// 1. `db.archetypes` assigns to the derived service's archetype map with NO cast. +const archetypes: MainService["archetypes"] = db.archetypes; +void archetypes; + +// 2. `RowOf` extracts the full structural row, including the implicit `id`. +type TrackRow = Database.Archetype.RowOf; +type CheckRow = Assert>; + +// 3. A named row round-trips: `ReadonlyArchetype>` accepts the handle +// with no cast, because it is structurally the same archetype. +const track: ReadonlyArchetype = db.archetypes.Track; +void track; + +// 4. The row flows through `observe.entity(id, archetype)` with no per-entity +// cast — the other place the consumer reported having to launder the type. +// The non-null emitted value carries the Track row fields. +declare const someEntity: Entity; +const observed = db.observe.entity(someEntity, db.archetypes.Track); +type Emitted = typeof observed extends Observe ? V : never; +type CheckObservedCarriesRow = Assert, TrackRow>>; +void observed; diff --git a/packages/data/src/ecs/database/combine-plugins.ts b/packages/data/src/ecs/database/combine-plugins.ts index 1d60260..518a652 100644 --- a/packages/data/src/ecs/database/combine-plugins.ts +++ b/packages/data/src/ecs/database/combine-plugins.ts @@ -1,15 +1,18 @@ // © 2026 Adobe. MIT License. See /LICENSE for details. -import { Simplify, StringKeyof } from "../../types/types.js"; +import { StringKeyof, UnionToIntersection } from "../../types/types.js"; import type { Database } from "./database.js"; -// Helper to intersect all elements (works with mapped types over tuples) -type IntersectAll = Simplify< - T extends readonly [infer H, ...infer R] ? H & IntersectAll : unknown ->; -type UnionAll = Simplify< - T extends readonly [infer H, ...infer R] ? H | UnionAll : never ->; +// Intersect every element of a tuple. Non-recursive (`UnionToIntersection` over +// the element union) so it neither trips the TS2589 recursion-depth ceiling — +// which previously capped how many plugins could be combined in one call and +// forced nested grouping — nor pays a per-level `Simplify` re-materialization +// (the bulk of the old instantiation cost). The empty-tuple guard preserves the +// old `unknown` so `{} & IntersectAll<[]>` stays `{}` rather than collapsing to +// `never`. The per-field unions here are the plugins' object-map buckets, which +// intersect soundly; the only union kept as a union is `systems` (below). +type IntersectAll = + [T[number]] extends [never] ? unknown : UnionToIntersection; // Array-based combination type - combines plugins from an array into a flat Database.Plugin. // Uses direct property access (P['components']) instead of conditional inference @@ -21,7 +24,7 @@ export type CombinePlugins = Databas {} & IntersectAll<{ [K in keyof Plugins]: Plugins[K]['archetypes'] }>, {} & IntersectAll<{ [K in keyof Plugins]: Plugins[K]['transactions'] }>, Extract< - UnionAll<{ [K in keyof Plugins]: StringKeyof }>, + { [K in keyof Plugins]: StringKeyof }[number], string >, {} & IntersectAll<{ [K in keyof Plugins]: Plugins[K]['actions'] }>, @@ -46,7 +49,7 @@ export function combinePlugins< {} & IntersectAll<{ [K in keyof Plugins]: Plugins[K]['resources'] }>, {} & IntersectAll<{ [K in keyof Plugins]: Plugins[K]['archetypes'] }>, {} & IntersectAll<{ [K in keyof Plugins]: Plugins[K]['transactions'] }>, - Extract }>, string>, + Extract<{ [K in keyof Plugins]: StringKeyof }[number], string>, {} & IntersectAll<{ [K in keyof Plugins]: Plugins[K]['actions'] }>, {} & IntersectAll<{ [K in keyof Plugins]: Plugins[K]['services'] }>, {} & IntersectAll<{ [K in keyof Plugins]: Plugins[K]['computed'] }>, diff --git a/packages/data/src/ecs/database/combine-plugins.type-test.ts b/packages/data/src/ecs/database/combine-plugins.type-test.ts new file mode 100644 index 0000000..8fce7f2 --- /dev/null +++ b/packages/data/src/ecs/database/combine-plugins.type-test.ts @@ -0,0 +1,59 @@ +// © 2026 Adobe. MIT License. See /LICENSE for details. + +import { createPlugin } from "./create-plugin.js"; +import { Database } from "./database.js"; +import { Assert } from "../../types/assert.js"; +import { Extends } from "../../types/types.js"; + +/** + * Type-only test: `Database.Plugin.combine` resolves correctly over a wide + * single call. + * + * `IntersectAll` was reformulated from linear recursion (`Simplify`) + * to `UnionToIntersection` over the element union, so instantiation depth is + * now constant regardless of plugin count and the per-level `Simplify` + * re-materialization is gone (a ~29% instantiation drop on a 24-plugin combine + * measured locally). This guards that a wide single combine still produces the + * correct merged plugin type — the buckets from the first and last plugins are + * both present, and the result builds into a usable database. + */ + +const p00 = createPlugin({ components: { c00: { type: "number" } }, archetypes: { A00: ["c00"] } }); +const p01 = createPlugin({ components: { c01: { type: "number" } }, archetypes: { A01: ["c01"] } }); +const p02 = createPlugin({ components: { c02: { type: "number" } }, archetypes: { A02: ["c02"] } }); +const p03 = createPlugin({ components: { c03: { type: "number" } }, archetypes: { A03: ["c03"] } }); +const p04 = createPlugin({ components: { c04: { type: "number" } }, archetypes: { A04: ["c04"] } }); +const p05 = createPlugin({ components: { c05: { type: "number" } }, archetypes: { A05: ["c05"] } }); +const p06 = createPlugin({ components: { c06: { type: "number" } }, archetypes: { A06: ["c06"] } }); +const p07 = createPlugin({ components: { c07: { type: "number" } }, archetypes: { A07: ["c07"] } }); +const p08 = createPlugin({ components: { c08: { type: "number" } }, archetypes: { A08: ["c08"] } }); +const p09 = createPlugin({ components: { c09: { type: "number" } }, archetypes: { A09: ["c09"] } }); +const p10 = createPlugin({ components: { c10: { type: "number" } }, archetypes: { A10: ["c10"] } }); +const p11 = createPlugin({ components: { c11: { type: "number" } }, archetypes: { A11: ["c11"] } }); +const p12 = createPlugin({ components: { c12: { type: "number" } }, archetypes: { A12: ["c12"] } }); +const p13 = createPlugin({ components: { c13: { type: "number" } }, archetypes: { A13: ["c13"] } }); +const p14 = createPlugin({ components: { c14: { type: "number" } }, archetypes: { A14: ["c14"] } }); +const p15 = createPlugin({ components: { c15: { type: "number" } }, archetypes: { A15: ["c15"] } }); + +const wide = Database.Plugin.combine( + p00, p01, p02, p03, p04, p05, p06, p07, + p08, p09, p10, p11, p12, p13, p14, p15, +); + +// The intersection resolves: components and archetypes from the first and last +// plugins are both present in the combined plugin type (no cast, no TS2589). +type WideComponents = (typeof wide)["components"]; +type WideArchetypes = (typeof wide)["archetypes"]; +type CheckFirstComponent = Assert>; +type CheckLastComponent = Assert>; +type CheckFirstArchetype = Assert>; +type CheckLastArchetype = Assert>; + +// And it is still a usable database: building it and reading the merged +// archetype map type-checks. +const wideDb = Database.create(wide); +type CheckDbArchetypes = Assert>; +void wideDb; diff --git a/packages/data/src/ecs/database/database.ts b/packages/data/src/ecs/database/database.ts index f4b7449..b1b93da 100644 --- a/packages/data/src/ecs/database/database.ts +++ b/packages/data/src/ecs/database/database.ts @@ -1,6 +1,6 @@ // © 2026 Adobe. MIT License. See /LICENSE for details. -import { Archetype, ArchetypeId, ReadonlyArchetype } from "../archetype/index.js"; +import { Archetype, ArchetypeId, FromArchetype, ReadonlyArchetype } from "../archetype/index.js"; import { ResourceComponents } from "../store/resource-components.js"; import { ReadonlyStore, Store } from "../store/index.js"; import { Entity } from "../entity/entity.js"; @@ -272,6 +272,25 @@ export namespace Database { StoreIndex.Handle; } + // eslint-disable-next-line @typescript-eslint/no-namespace + export namespace Archetype { + /** + * The row (component) type of the archetype named `K` on a store / + * database / service type `S`. Lets callers *name* an archetype row + * without re-spelling its columns — derive instead of re-declare. + * + * ```ts + * type MainService = Database.Plugin.ToDatabase; + * type Track = Database.Archetype.RowOf; + * const t: ReadonlyArchetype = db.archetypes.Track; // no cast + * ``` + */ + export type RowOf< + S extends { readonly archetypes: Record }, + K extends StringKeyof, + > = FromArchetype; + } + export type Plugin< CS extends ComponentSchemas = any, RS extends ResourceSchemas = any,