Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "data-monorepo",
"version": "0.9.66",
"version": "0.9.67",
"private": true,
"scripts": {
"build": "pnpm -r run build",
Expand Down
2 changes: 1 addition & 1 deletion packages/data-lit-tictactoe/package.json
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-lit-todo/package.json
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-lit/package.json
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-p2p-tictactoe/package.json
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-persistence/package.json
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-react-hello/package.json
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-react-pixie/package.json
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-react/package.json
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-solid-dashboard/package.json
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-solid/package.json
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-sync/package.json
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/data/package.json
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
21 changes: 21 additions & 0 deletions packages/data/src/ecs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,27 @@ const worldPlugin = Database.Plugin.create({
});
```

### Naming an archetype's row type

Each `db.archetypes.<Name>` is a `ReadonlyArchetype<Row>` 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<typeof worldPlugin>;
type PlayerRow = Database.Archetype.RowOf<WorldService, "Player">;

const a: ReadonlyArchetype<PlayerRow> = 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.<name>` and `t.indexes.<name>` (inside transactions).
Expand Down
63 changes: 63 additions & 0 deletions packages/data/src/ecs/database/archetype-row.type-test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof trackPlugin>;

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<MainService, "Track">;
type CheckRow = Assert<Equal<TrackRow, {
readonly id: Entity;
readonly trackKind: string;
readonly editingMode: string;
readonly muted: boolean;
}>>;

// 3. A named row round-trips: `ReadonlyArchetype<RowOf<...>>` accepts the handle
// with no cast, because it is structurally the same archetype.
const track: ReadonlyArchetype<TrackRow> = 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<infer V> ? V : never;
type CheckObservedCarriesRow = Assert<Extends<NonNullable<Emitted>, TrackRow>>;
void observed;
23 changes: 13 additions & 10 deletions packages/data/src/ecs/database/combine-plugins.ts
Original file line number Diff line number Diff line change
@@ -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<T extends readonly unknown[]> = Simplify<
T extends readonly [infer H, ...infer R] ? H & IntersectAll<R> : unknown
>;
type UnionAll<T extends readonly unknown[]> = Simplify<
T extends readonly [infer H, ...infer R] ? H | UnionAll<R> : 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 extends readonly unknown[]> =
[T[number]] extends [never] ? unknown : UnionToIntersection<T[number]>;

// Array-based combination type - combines plugins from an array into a flat Database.Plugin.
// Uses direct property access (P['components']) instead of conditional inference
Expand All @@ -21,7 +24,7 @@ export type CombinePlugins<Plugins extends readonly Database.Plugin[]> = Databas
{} & IntersectAll<{ [K in keyof Plugins]: Plugins[K]['archetypes'] }>,
{} & IntersectAll<{ [K in keyof Plugins]: Plugins[K]['transactions'] }>,
Extract<
UnionAll<{ [K in keyof Plugins]: StringKeyof<Plugins[K]['systems']> }>,
{ [K in keyof Plugins]: StringKeyof<Plugins[K]['systems']> }[number],
string
>,
{} & IntersectAll<{ [K in keyof Plugins]: Plugins[K]['actions'] }>,
Expand All @@ -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<UnionAll<{ [K in keyof Plugins]: StringKeyof<Plugins[K]['systems']> }>, string>,
Extract<{ [K in keyof Plugins]: StringKeyof<Plugins[K]['systems']> }[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'] }>,
Expand Down
59 changes: 59 additions & 0 deletions packages/data/src/ecs/database/combine-plugins.type-test.ts
Original file line number Diff line number Diff line change
@@ -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<H & ...>`)
* 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<Extends<WideComponents, { c00: { type: "number" } }>>;
type CheckLastComponent = Assert<Extends<WideComponents, { c15: { type: "number" } }>>;
type CheckFirstArchetype = Assert<Extends<WideArchetypes, { A00: readonly ["c00"] }>>;
type CheckLastArchetype = Assert<Extends<WideArchetypes, { A15: readonly ["c15"] }>>;

// 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<Extends<
"A00" | "A15",
keyof (typeof wideDb)["archetypes"]
>>;
void wideDb;
21 changes: 20 additions & 1 deletion packages/data/src/ecs/database/database.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -272,6 +272,25 @@ export namespace Database {
StoreIndex.Handle<C, I>;
}

// 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<typeof mainPlugin>;
* type Track = Database.Archetype.RowOf<MainService, "Track">;
* const t: ReadonlyArchetype<Track> = db.archetypes.Track; // no cast
* ```
*/
export type RowOf<
S extends { readonly archetypes: Record<string, unknown> },
K extends StringKeyof<S["archetypes"]>,
> = FromArchetype<S["archetypes"][K]>;
}

export type Plugin<
CS extends ComponentSchemas = any,
RS extends ResourceSchemas = any,
Expand Down