@adobe/data-ai 0.10.20 → 0.10.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/README.md +26 -31
- package/.claude/rules/app.md +46 -0
- package/.claude/rules/archetypes.md +5 -3
- package/.claude/rules/data-modelling.md +8 -7
- package/.claude/rules/ecs.md +11 -7
- package/.claude/rules/element.md +6 -5
- package/.claude/rules/features/data/components.md +51 -0
- package/.claude/rules/features/data/entities.md +37 -0
- package/.claude/rules/features/data/index.md +18 -70
- package/.claude/rules/features/data/resources.md +34 -0
- package/.claude/rules/features/data/values.md +36 -0
- package/.claude/rules/features/ecs/actions.md +31 -0
- package/.claude/rules/features/ecs/computed.md +31 -0
- package/.claude/rules/features/ecs/conformance.md +85 -0
- package/.claude/rules/features/ecs/core.md +43 -0
- package/.claude/rules/features/ecs/index.md +72 -0
- package/.claude/rules/features/{services/main-service → ecs}/indexes.md +6 -7
- package/.claude/rules/features/ecs/services.md +36 -0
- package/.claude/rules/features/{services/main-service → ecs}/systems.md +61 -47
- package/.claude/rules/features/ecs/transactions.md +32 -0
- package/.claude/rules/features/index.md +104 -150
- package/.claude/rules/features/services/index.md +52 -84
- package/.claude/rules/features/spec/index.md +161 -0
- package/.claude/rules/features/ui/binding-element.md +5 -5
- package/.claude/rules/features/ui/index.md +8 -3
- package/.claude/rules/features/ui/lazy-wrapper.md +2 -2
- package/.claude/rules/features/ui/presentation-test.md +1 -0
- package/.claude/rules/features/ui/presentation.md +1 -0
- package/.claude/rules/global/function-references.md +2 -2
- package/.claude/rules/global/namespace.md +3 -3
- package/.claude/rules/global/react.md +4 -4
- package/.claude/rules/lit.md +2 -2
- package/.claude/rules/plugin-modelling.md +2 -0
- package/.claude/rules/service.md +2 -0
- package/.claude/rules/versioning.md +8 -6
- package/.claude-plugin/plugin.json +1 -1
- package/package.json +3 -3
- package/skills/build-actions/SKILL.md +7 -8
- package/skills/build-app-entry/SKILL.md +12 -8
- package/skills/build-computed/SKILL.md +8 -7
- package/skills/build-core-database/SKILL.md +18 -15
- package/skills/build-data/SKILL.md +14 -13
- package/skills/build-feature/SKILL.md +2 -1
- package/skills/build-game/SKILL.md +6 -4
- package/skills/build-indexes/SKILL.md +6 -7
- package/skills/build-service-database/SKILL.md +8 -8
- package/skills/build-services/SKILL.md +9 -7
- package/skills/build-spec/SKILL.md +23 -0
- package/skills/build-systems/SKILL.md +12 -18
- package/skills/build-transactions/SKILL.md +8 -8
- package/skills/build-ui/SKILL.md +5 -4
- package/skills/review/SKILL.md +3 -3
- package/.claude/rules/features/data/state.md +0 -209
- package/.claude/rules/features/services/main-service/actions.md +0 -70
- package/.claude/rules/features/services/main-service/archetypes.md +0 -49
- package/.claude/rules/features/services/main-service/components.md +0 -41
- package/.claude/rules/features/services/main-service/computed.md +0 -82
- package/.claude/rules/features/services/main-service/conformance.md +0 -95
- package/.claude/rules/features/services/main-service/core-database.md +0 -48
- package/.claude/rules/features/services/main-service/index.md +0 -187
- package/.claude/rules/features/services/main-service/resources.md +0 -28
- package/.claude/rules/features/services/main-service/services.md +0 -25
- package/.claude/rules/features/services/main-service/transactions.md +0 -59
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/features/*/ecs/conformance/**/*.ts'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ecs/conformance/ — proving the ECS matches the spec
|
|
7
|
+
|
|
8
|
+
Test tier. The spec's cases are replayed against the ECS: every spec action against
|
|
9
|
+
the `MainService` action of the same name, and every derivation against its
|
|
10
|
+
computed. The folder holds:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
conformance/
|
|
14
|
+
projection.ts the store ⇄ State mapping
|
|
15
|
+
implementation.ts the spec paired with its ECS build
|
|
16
|
+
frame.cases.ts whole-frame cases (real-time features)
|
|
17
|
+
conformance.test.ts Conformance.checkFeature(implementation)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## `projection.ts`
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
export const projection = { fromState, toState, toData };
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- `fromState(store, state)` clears the store, inserts each entity (supplying tags and
|
|
27
|
+
implementation-only columns), sets the resources, and returns the
|
|
28
|
+
`spec id → entity` map it built. The runner resolves case ids through it.
|
|
29
|
+
- `toState(store)` reads the whole store back as a `State`.
|
|
30
|
+
- `toData(store, entity)` reads one entity as its value. It is needed only when a
|
|
31
|
+
computed emits entity ids (`hydrate`).
|
|
32
|
+
|
|
33
|
+
## `implementation.ts`
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
export const implementation = Conformance.implementation(spec, {
|
|
37
|
+
plugin: MainService.plugin,
|
|
38
|
+
computedPlugin: ComputedDatabase.plugin, // required when the spec has derivations
|
|
39
|
+
projection,
|
|
40
|
+
hydrate: ["visibleTodos"], // exactly the computeds that emit entity ids
|
|
41
|
+
services: { game: GameService.createFake }, // base fakes for app-injected services
|
|
42
|
+
frame: { args: { dt: (db, dt) => { db.store.resources.frameDelta = dt; } },
|
|
43
|
+
unmodelled: { physics: "wasm rigid-body step" },
|
|
44
|
+
cases: frameCases }, // real-time features
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- **`computedPlugin`** is the `ComputedDatabase` layer, built on its own, so a
|
|
49
|
+
`withCache` value from a layer above can't go stale across the seed.
|
|
50
|
+
- **`services`** are fakes injected into every database the runner builds. They are
|
|
51
|
+
needed for a service whose `create` throws. Each case's recording doubles override
|
|
52
|
+
them.
|
|
53
|
+
- **`frame`** is required when the spec has `systems`. `args` writes each frame data
|
|
54
|
+
arg into the store; `unmodelled` names, with a reason, each system the spec doesn't
|
|
55
|
+
model; `cases` are whole-frame cases (`frame.cases.ts` here, typed
|
|
56
|
+
`Conformance.FrameCases<State, typeof systems>`). Each spec system conforms by
|
|
57
|
+
running just that system; each frame case runs every modelled system once in
|
|
58
|
+
`db.system.order` (`systems.md`). Systems keep their in-place column writes.
|
|
59
|
+
- `hydrate` exactness and `computedPlugin` requiredness are checked at compile time.
|
|
60
|
+
|
|
61
|
+
## `conformance.test.ts`
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
Conformance.checkFeature(implementation);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
This single call conforms every spec action and derivation and round-trips every
|
|
68
|
+
`State.samples` entry through the projection. A spec op with no same-named action or
|
|
69
|
+
computed fails by name, as does an ECS system that is neither modelled nor unmodelled. So when the UI's real op is richer (`dragTodo`),
|
|
70
|
+
add a thin same-named action (`reorderTodo`) for the spec op to pair with. The UI
|
|
71
|
+
calls actions, so conforming actions covers the path the app runs.
|
|
72
|
+
|
|
73
|
+
- **Comparison.** `ReadonlyArray` is positional; `ReadonlySet` and `ReadonlyMap` are
|
|
74
|
+
order-independent. Ids compare up to a bijection. Floats use
|
|
75
|
+
`match: { tolerance }`, set on the spec (default `0.01`).
|
|
76
|
+
- **Extra ECS tests** (collision selection, a refill after the field clears) live here
|
|
77
|
+
too, since they need the spec or the projection.
|
|
78
|
+
|
|
79
|
+
## Escape hatch: per-case ambient context
|
|
80
|
+
|
|
81
|
+
A feature whose action reads ambient per-peer context (a `userId` seeded under a
|
|
82
|
+
custom concurrency, as in p2p presence) can't use `checkFeature`. It drives
|
|
83
|
+
`Conformance.runActions` directly, feeding it
|
|
84
|
+
`Conformance.adaptCases(spec.fns, spec.cases, spec.services, true)` with its own
|
|
85
|
+
`makeDb` and `seedContext`. This is the only sanctioned use of the lower-level runners.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/features/*/ecs/core/**/*.ts'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ecs/core/ — the schema
|
|
7
|
+
|
|
8
|
+
The core database builds the feature's `data/` components and resources into a plugin
|
|
9
|
+
and chooses how entities are packed into archetypes.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
// core/archetypes.ts — the spec entity plus implementation-only columns
|
|
13
|
+
export const archetypes = Database.archetypes(components, {
|
|
14
|
+
Todo: ["todo", ...Todo],
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// core/core-database.ts
|
|
18
|
+
const coreDatabasePlugin = Database.Plugin.create({ components, resources, archetypes });
|
|
19
|
+
|
|
20
|
+
export type CoreDatabase = Database.Plugin.ToDatabase<typeof coreDatabasePlugin>;
|
|
21
|
+
type CoreComponents = Store.Components<Database.Plugin.ToStore<typeof coreDatabasePlugin>>;
|
|
22
|
+
|
|
23
|
+
export namespace CoreDatabase {
|
|
24
|
+
export const plugin = coreDatabasePlugin;
|
|
25
|
+
export type Store = Database.Plugin.ToStore<typeof coreDatabasePlugin>;
|
|
26
|
+
export type Index = Database.Index<CoreComponents>;
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- **Import `components` and `resources` from `data/`.** Never author schemas here.
|
|
31
|
+
- **Spread the entity tuple** from `data/entities/` and add implementation-only
|
|
32
|
+
columns (a tag). `UpperCase` archetype names.
|
|
33
|
+
- **Never pack a `nonPersistent` column into a persistent entity's archetype** unless
|
|
34
|
+
it has a real, non-null default. On load such a column is stripped, and every
|
|
35
|
+
reloaded entity then drops out of that archetype and out of every query on it. Add
|
|
36
|
+
the column on demand with `t.update` instead. A persisting feature keeps a save/load
|
|
37
|
+
round-trip test (`data-lit-todo`'s `app/persistence.test.ts`).
|
|
38
|
+
- **A feature built on another** `extends` the lower feature's core plugin and adds its
|
|
39
|
+
own `components` barrel (which already spreads the lower one) and archetypes.
|
|
40
|
+
- **`Component` / `Archetype` key types**, when needed, derive from the `components`
|
|
41
|
+
barrel (`Extract<keyof typeof components, string>`), never from the plugin: the
|
|
42
|
+
plugin is built from `archetypes`, so a plugin-derived key type is circular when used
|
|
43
|
+
to check an archetype.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/features/*/ecs/main-service.ts'
|
|
4
|
+
- '**/features/*/ecs/*/*-database.ts'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# ecs/ — the implementation
|
|
8
|
+
|
|
9
|
+
The feature implemented as a reactive Entity-Component-System, conformed to
|
|
10
|
+
`spec/`. It is a chain of `Database.Plugin` layers, each extending the one before and
|
|
11
|
+
adding one facet:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
ecs/
|
|
15
|
+
core/ core-database.ts, archetypes.ts the schema (core.md)
|
|
16
|
+
indexes/ index-database.ts + one file per index + index.ts
|
|
17
|
+
transactions/ transaction-database.ts + one file per transaction + index.ts
|
|
18
|
+
services/ service-database.ts + database-bound service factories
|
|
19
|
+
computed/ computed-database.ts + one file per computed + index.ts
|
|
20
|
+
actions/ action-database.ts + one file per action + index.ts
|
|
21
|
+
systems/ system-database.ts (systems declared inline)
|
|
22
|
+
main-service.ts
|
|
23
|
+
conformance/ test tier (conformance.md)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The order is fixed: `core → indexes → transactions → services → computed → actions
|
|
27
|
+
→ systems`. Services sit below computed because computeds may derive from service
|
|
28
|
+
observables; a service factory therefore never reads a computed. A feature creates
|
|
29
|
+
only the layers it uses. Each layer's file exports a namespace mirroring its plugin:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
const indexDatabasePlugin = Database.Plugin.create({ extends: CoreDatabase.plugin, indexes });
|
|
33
|
+
export type IndexDatabase = Database.Plugin.ToDatabase<typeof indexDatabasePlugin>;
|
|
34
|
+
export namespace IndexDatabase {
|
|
35
|
+
export const plugin = indexDatabasePlugin;
|
|
36
|
+
export type Store = Database.Plugin.ToStore<typeof indexDatabasePlugin>;
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Runtime code here imports `data/` and `services/`, never `spec/`.
|
|
41
|
+
|
|
42
|
+
## `MainService` — the assembled database
|
|
43
|
+
|
|
44
|
+
`main-service.ts` aliases the top layer, so consumers never name it:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
export { ActionDatabase as MainService } from "./actions/action-database.js";
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`ui/`, `app/` and `conformance/` reference `MainService.plugin` / `MainService.Store`.
|
|
51
|
+
Adding or dropping a layer changes this one line. When another feature or package
|
|
52
|
+
imports a feature's databases, it aliases them feature-qualified
|
|
53
|
+
(`import { CoreDatabase as MainCoreDatabase }`).
|
|
54
|
+
|
|
55
|
+
## What each op takes
|
|
56
|
+
|
|
57
|
+
- **Transactions** take the store: `CoreDatabase.Store`, or `IndexDatabase.Store` once
|
|
58
|
+
they read an index. A store is the transaction context (it carries index handles and
|
|
59
|
+
`t.userId`).
|
|
60
|
+
- **Computed, service factories and actions** take the whole database, typed on the
|
|
61
|
+
lowest layer that exposes what they read or call — never their own layer.
|
|
62
|
+
|
|
63
|
+
## Folders are flat until they aren't
|
|
64
|
+
|
|
65
|
+
A behavioural folder holds one file per op plus an `index.ts` barrel that feeds the
|
|
66
|
+
facet. When a cohesive cluster grows past about half a dozen files, give it a
|
|
67
|
+
subfolder with its own `index.ts` (e.g. `transactions/order/`). A helper several ops
|
|
68
|
+
share lives beside them but stays **out of the barrel**, or it would be registered
|
|
69
|
+
as an op.
|
|
70
|
+
|
|
71
|
+
The per-layer rules are in this folder. How a plugin's authored and derived surfaces
|
|
72
|
+
are modelled is in the rules-root `plugin-modelling.md`.
|
|
@@ -1,24 +1,23 @@
|
|
|
1
1
|
---
|
|
2
2
|
paths:
|
|
3
|
-
- '**/features/*/
|
|
3
|
+
- '**/features/*/ecs/indexes/**/*.ts'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# ecs/indexes/ — ECS indexes
|
|
7
7
|
|
|
8
8
|
One index descriptor per file: an object `satisfies CoreDatabase.Index` that
|
|
9
9
|
tells the store to maintain a lookup keyed on a component, so queries and
|
|
10
10
|
computed values can find entities without scanning.
|
|
11
11
|
|
|
12
12
|
```ts
|
|
13
|
-
import type { CoreDatabase } from "
|
|
13
|
+
import type { CoreDatabase } from "../core/core-database.js";
|
|
14
14
|
|
|
15
15
|
export const byComplete = {
|
|
16
16
|
key: "complete",
|
|
17
17
|
} as const satisfies CoreDatabase.Index;
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
`CoreDatabase.Index` is bound to every component
|
|
21
|
-
`core-database`). Archetype-scoped indexes carry an extra `archetype` field the
|
|
20
|
+
`CoreDatabase.Index` is bound to every component in the core database. Archetype-scoped indexes carry an extra `archetype` field the
|
|
22
21
|
`Index` helper can't express, so declare those with a bare `as const` (no
|
|
23
22
|
`satisfies`) and let `index-database.ts`'s `create()` validate them.
|
|
24
23
|
|
|
@@ -37,7 +36,7 @@ Derive a discrete **bucket key** — but prefer a **computed-key extractor**
|
|
|
37
36
|
recomputes the key from its source columns, so the index stays correct with no
|
|
38
37
|
maintenance; a stored bucket column must instead be recomputed at every
|
|
39
38
|
insertion/update site (or by a system) and silently drifts if one is missed. The
|
|
40
|
-
derivation itself is a pure `data/` helper.
|
|
39
|
+
derivation itself is a pure `data/values/` helper.
|
|
41
40
|
|
|
42
41
|
Bucketed broad-phase only pays off at scale (hundreds+ candidates per query);
|
|
43
42
|
below that a linear scan over an archetype wins. Index when the scan is a
|
|
@@ -58,6 +57,6 @@ below that a linear scan over an archetype wins. Index when the scan is a
|
|
|
58
57
|
multi-value index — one bucket entry per element — and `find({ col: element })`
|
|
59
58
|
takes the element, not the array.
|
|
60
59
|
- **Model a many-to-many as a denormalized array + two indexes.** A
|
|
61
|
-
`todo.assignees: string[]` (owned
|
|
60
|
+
`todo.assignees: string[]` (owned by one feature) plus a unique
|
|
62
61
|
`usersByName` (name → user) and a multi-value `todosByAssignee` (assignee →
|
|
63
62
|
todos) gives efficient navigation in both directions from one join key.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/features/*/ecs/services/**/*.ts'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ecs/services/ — registering services on the database
|
|
7
|
+
|
|
8
|
+
`service-database.ts` registers the feature's services under the `services` facet,
|
|
9
|
+
keyed as the spec's `Services` map, and pins the two together:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
const serviceDatabasePlugin = Database.Plugin.create({
|
|
13
|
+
extends: TransactionDatabase.plugin,
|
|
14
|
+
services: {
|
|
15
|
+
nameGenerator: NameGeneratorService.create,
|
|
16
|
+
analytics: AnalyticsService.create,
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
export type ServiceDatabase = Database.Plugin.ToDatabase<typeof serviceDatabasePlugin>;
|
|
20
|
+
type _ServicesPin = Assert<Equal<ServiceDatabase["services"], Services>>;
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- **A `services/` service** registers its `create`. If `create` throws (the service
|
|
24
|
+
needs runtime input), the app injects the real one (`../../app.md`), and conformance
|
|
25
|
+
injects a fake (`conformance.md`).
|
|
26
|
+
- **A database-bound service** (it reads observables or calls transactions, such as
|
|
27
|
+
an agent) is a factory here, one per file: `createAgentService(db, …)`, with `db`
|
|
28
|
+
typed on the lowest layer it needs. It sits below computed, so it derives what it
|
|
29
|
+
observes from the store (`db.derive`) and `data/values/` helpers, sharing a read
|
|
30
|
+
helper with the transactions where useful (tictactoe's `readBoard`). The `index.ts`
|
|
31
|
+
barrel re-exports the factories.
|
|
32
|
+
- **A service built from another service** needs that service registered first, so
|
|
33
|
+
register it in a second `Plugin.create` extending the first: a factory only sees
|
|
34
|
+
the services of the plugins it extends.
|
|
35
|
+
- Register each service once, in the feature that owns it, under one key. Combined
|
|
36
|
+
plugins require the same factory per key.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
paths:
|
|
3
|
-
- '**/features/*/
|
|
3
|
+
- '**/features/*/ecs/systems/**/*.ts'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# ecs/systems/ — the real-time tick loop
|
|
7
7
|
|
|
8
8
|
Systems are the framework's real-time backbone: functions the scheduler runs
|
|
9
9
|
**every frame**, in dependency order, to advance a simulation. A feature only
|
|
@@ -21,19 +21,19 @@ arrow must be written inline in `Plugin.create` for two reasons: only there is `
|
|
|
21
21
|
strongly typed (to the assembled plugin's database, with a **writable `store`**), and
|
|
22
22
|
a pre-typed standalone `create` reference breaks the scheduler's system-name inference
|
|
23
23
|
(the name union collapses to `never`, which then rejects every `schedule` entry). Keep
|
|
24
|
-
each `create` thin by delegating
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
each `create` thin by delegating math to pure `data/values/` helpers (never `spec/`);
|
|
25
|
+
if an ecs-specific body is substantial, extract it to a `systems/<name>.ts` helper
|
|
26
|
+
`(db) => () => void` (annotate its `db`) and call it from the inline `create`.
|
|
27
27
|
|
|
28
28
|
```ts
|
|
29
29
|
// in system-database.ts
|
|
30
30
|
import { Database, scheduler } from "@adobe/data/ecs";
|
|
31
|
-
import { Motion } from "../../data/motion/motion.js";
|
|
31
|
+
import { Motion } from "../../data/values/motion/motion.js";
|
|
32
32
|
|
|
33
33
|
const plugin = Database.Plugin.create({
|
|
34
|
-
// combine scheduler with the
|
|
35
|
-
//
|
|
36
|
-
extends: Database.Plugin.combine(
|
|
34
|
+
// combine scheduler with the current top layer — ActionDatabase, since every
|
|
35
|
+
// spec action has an action (systems come last).
|
|
36
|
+
extends: Database.Plugin.combine(ActionDatabase.plugin, scheduler),
|
|
37
37
|
systems: {
|
|
38
38
|
control: { create: (db) => () => { /* input → ship; db.transactions.fireBullet() */ } },
|
|
39
39
|
movement: {
|
|
@@ -59,18 +59,15 @@ const plugin = Database.Plugin.create({
|
|
|
59
59
|
`db.store.update / insert / delete` for lifecycle (spawning, dying) that changes
|
|
60
60
|
archetype membership. Resources: `db.store.resources.x = …`.
|
|
61
61
|
- **Dispatch a transaction for a discrete atomic event** — when the effect is a
|
|
62
|
-
self-contained event
|
|
63
|
-
|
|
64
|
-
re-implementing it: you reuse the `data/`-verified logic and observers (a reactive
|
|
65
|
-
HUD) are notified. Reserve direct `db.store` writes for the hot per-row paths
|
|
62
|
+
self-contained event (score a hit, lose a life, spawn a wave, fire a bullet), call
|
|
63
|
+
`db.transactions.*`: observers (a reactive HUD) are notified. Reserve direct `db.store` writes for the hot per-row paths
|
|
66
64
|
(movement, aging) that run on every entity every frame and shouldn't pay
|
|
67
65
|
per-entity transaction overhead.
|
|
68
66
|
- **Optimize hot paths for speed.** With many entities, query with efficient
|
|
69
67
|
`queryArchetypes` and read/write minimal columns directly; avoid intermediate
|
|
70
|
-
allocations.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
direct store writes fire no observers.)
|
|
68
|
+
allocations. Never turn per-row work into per-entity transactions to make it
|
|
69
|
+
conformable: `frame` conformance checks in-place writes as they are. (Only
|
|
70
|
+
transactions are observable — direct store writes fire no observers.)
|
|
74
71
|
- **Iterate archetypes per `archetypes.md`.** Express selection with
|
|
75
72
|
`queryArchetypes(include, { exclude })`; when a system destroys/migrates rows
|
|
76
73
|
(bullets expiring, entities dying) iterate **tail → head** so hole-fills don't
|
|
@@ -79,25 +76,22 @@ const plugin = Database.Plugin.create({
|
|
|
79
76
|
a transaction that inserts/deletes invalidates any live archetype cursor, so
|
|
80
77
|
snapshot the ids you'll act on (or reverse-iterate) *before* dispatching
|
|
81
78
|
transactions inside a per-frame loop.
|
|
82
|
-
- **
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
*every* system honors the guard.
|
|
79
|
+
- **Every system guards itself** (e.g. early-return once the game is over), as its
|
|
80
|
+
spec function does — a frozen frame is only a true no-op if *every* system honors
|
|
81
|
+
the guard.
|
|
86
82
|
- **Index reads go through `db.indexes`** (plural) — the writable `db.store` a
|
|
87
83
|
system receives is typed *without* its index handles, so a broad-phase query is
|
|
88
84
|
`db.indexes.byCell.find({ cell })`, not `db.store.indexes.…`.
|
|
89
85
|
- **Ordering is declared under `schedule`, not implied.** `schedule.before` /
|
|
90
86
|
`schedule.after` are hard constraints (name sibling systems); `schedule.during`
|
|
91
87
|
is a soft same-tier hint. Systems with no ordering relation share a tier
|
|
92
|
-
(conceptually parallel) — never rely on declaration order
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
before the spawn — split that entity's advance out so it runs *after* the spawn,
|
|
100
|
-
alongside the step that owns it.
|
|
88
|
+
(conceptually parallel) — never rely on declaration order; systems sharing a tier
|
|
89
|
+
must commute (conformance checks it). The schedule is the only statement of order:
|
|
90
|
+
the spec never re-sequences systems, and frame conformance composes them in the
|
|
91
|
+
order the schedule derives. Watch the subtle case: if a spawn (fire) is followed by
|
|
92
|
+
a step that also processes the just-spawned entity (the new bullet is aged and
|
|
93
|
+
advanced this same tick), you can't advance *all* bodies in one system before the
|
|
94
|
+
spawn — split that entity's advance out so it runs *after* the spawn.
|
|
101
95
|
- Immediate-mode rendering: store the canvas in a session resource and render to
|
|
102
96
|
it inside a system (see @adobe/data-gpu for patterns).
|
|
103
97
|
|
|
@@ -109,32 +103,52 @@ Systems don't run themselves. Combine the built-in scheduler so they run on
|
|
|
109
103
|
```ts
|
|
110
104
|
import { Database, scheduler } from "@adobe/data/ecs";
|
|
111
105
|
// in system-database.ts:
|
|
112
|
-
Database.Plugin.create({ extends: Database.Plugin.combine(
|
|
106
|
+
Database.Plugin.create({ extends: Database.Plugin.combine(ActionDatabase.plugin, scheduler), systems });
|
|
113
107
|
```
|
|
114
108
|
|
|
115
109
|
The scheduler adds a `schedulerState` resource (`"running" | "paused" |
|
|
116
110
|
"disposed"`) — gate start/pause/resume through it (start `"paused"` if the game
|
|
117
|
-
begins on a user action). A headless host (tests, server sim)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
with no rAF and no rendering attached.
|
|
111
|
+
begins on a user action). A headless host (tests, server sim) drives frames itself: for each tier in
|
|
112
|
+
`db.system.order` (an array of tiers), call `db.system.functions[name]()` for each
|
|
113
|
+
name, skipping `schedulerSystem` and init-only systems (no function). So a simulation
|
|
114
|
+
is fully runnable and testable with no rAF and no rendering attached.
|
|
121
115
|
|
|
122
116
|
## Layer
|
|
123
117
|
|
|
124
|
-
`system-database.ts` extends the feature's
|
|
125
|
-
|
|
126
|
-
they must sit atop any service/action layers, not a hardcoded `ComputedDatabase`), combined with
|
|
118
|
+
`system-database.ts` extends the feature's top ecs layer (`ActionDatabase`, since every spec
|
|
119
|
+
action has an action; systems come last in the pipeline), combined with
|
|
127
120
|
`scheduler`, and declares the `systems` map **inline** (see above —
|
|
128
121
|
this is the one facet not split one-per-file, because `create`'s `db` is only typed
|
|
129
|
-
inline and standalone declarations break name inference).
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
122
|
+
inline and standalone declarations break name inference). Extracted per-frame body
|
|
123
|
+
helpers (`(db) => () => void`) sit beside it only when an inline body grows too large.
|
|
124
|
+
|
|
125
|
+
## Conformance
|
|
126
|
+
|
|
127
|
+
Each system has a same-named pure function in the spec's `systems` group
|
|
128
|
+
(`../spec/index.md`), and the implementation declares how frame args reach the store
|
|
129
|
+
and which systems the spec does not model (`conformance.md`):
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
frame: {
|
|
133
|
+
args: { dt: (db, dt) => { db.store.resources.frameDelta = dt; },
|
|
134
|
+
input: (db, input) => { db.store.resources.input = input; } },
|
|
135
|
+
unmodelled: { physics: "wasm rigid-body step", seedLevel: "init-only" },
|
|
136
|
+
cases: frameCases, // ecs/conformance/frame.cases.ts
|
|
137
|
+
},
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
- **Per system:** each case seeds the store, writes the case's args, runs **just that
|
|
141
|
+
system**, and compares with its spec function. Selection and detection logic (which
|
|
142
|
+
entities collide) is tested here, with seeded edge-case geometries.
|
|
143
|
+
- **Per frame:** each frame case (`ecs/conformance/frame.cases.ts`) folds the spec
|
|
144
|
+
system functions in `db.system.order` and also runs one real frame; both must give
|
|
145
|
+
`after`. The order comes only from the `schedule` declarations.
|
|
146
|
+
- **Shared guards:** the spec's `systems.noOp` states (game over) are checked against
|
|
147
|
+
every system, on both sides.
|
|
148
|
+
- **Unmodelled systems** (wasm, init-only) are skipped on both sides and reported as
|
|
149
|
+
skipped tests. Every system is modelled or unmodelled — a compile error otherwise.
|
|
150
|
+
To test an unmodelled system's glue, put the wasm call behind a service and model it
|
|
151
|
+
with a fake.
|
|
138
152
|
|
|
139
153
|
## Iterating archetype rows
|
|
140
154
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/features/*/ecs/transactions/**/*.ts'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ecs/transactions/ — atomic mutations
|
|
7
|
+
|
|
8
|
+
One mutation per file: a function taking the transaction store and Data args,
|
|
9
|
+
returning `void`. All its writes commit or roll back together.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
export const toggleComplete = (t: CoreDatabase.Store, { id }: { readonly id: Entity }) => {
|
|
13
|
+
const row = t.read(id, t.archetypes.Todo);
|
|
14
|
+
if (row === null) return; // not a todo: no-op, as the spec says
|
|
15
|
+
t.update(id, { complete: !row.complete });
|
|
16
|
+
};
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- **Type the store** `CoreDatabase.Store`, or `IndexDatabase.Store` once it reads an
|
|
20
|
+
index.
|
|
21
|
+
- **Validate and silently return** on illegal input rather than throwing, so replays
|
|
22
|
+
under sync stay idempotent.
|
|
23
|
+
- **Guard by entity kind.** `t.read(id)` returns any entity. Check the entity is the
|
|
24
|
+
kind the op expects (`t.read(id, t.archetypes.Todo)`, or its tag) before writing, or
|
|
25
|
+
an id naming another kind gets written to.
|
|
26
|
+
- **Decisions come from `data/values/` helpers**; the transaction reads the touched
|
|
27
|
+
slice, applies the helper, and writes the result.
|
|
28
|
+
- **Transactions are an implementation detail.** Conformance checks the action that
|
|
29
|
+
calls them (`actions.md`), so a transaction needs no spec op of its own. Give one a
|
|
30
|
+
unit test only when it has logic no action covers.
|
|
31
|
+
- **The barrel re-exports only mutations.** A read helper shared by several
|
|
32
|
+
transactions (`readBoard`) lives beside them, outside the barrel.
|