@adobe/ecs 0.2.1 → 0.2.4
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/README.md +2 -2
- package/dist/implementation/compiler/body-access.d.ts +18 -1
- package/dist/implementation/compiler/body-access.js +104 -15
- package/dist/implementation/compiler/body-access.js.map +1 -1
- package/dist/implementation/compiler/describe.d.ts +1 -1
- package/dist/implementation/compiler/describe.js +3 -3
- package/dist/implementation/compiler/describe.js.map +1 -1
- package/dist/implementation/compiler/estree.d.ts +5 -2
- package/dist/implementation/compiler/estree.js +8 -5
- package/dist/implementation/compiler/estree.js.map +1 -1
- package/dist/implementation/compiler/inferred.js +4 -2
- package/dist/implementation/compiler/inferred.js.map +1 -1
- package/dist/implementation/compiler/signature.js +1 -1
- package/dist/implementation/compiler/transform.js +6 -5
- package/dist/implementation/compiler/transform.js.map +1 -1
- package/dist/implementation/world/activate.js +14 -2
- package/dist/implementation/world/activate.js.map +1 -1
- package/dist/implementation/world/buffer.d.ts +7 -0
- package/dist/implementation/world/buffer.js +7 -0
- package/dist/implementation/world/buffer.js.map +1 -1
- package/dist/implementation/world/cascade.d.ts +10 -3
- package/dist/implementation/world/cascade.js +48 -23
- package/dist/implementation/world/cascade.js.map +1 -1
- package/dist/implementation/world/commands.js +86 -2
- package/dist/implementation/world/commands.js.map +1 -1
- package/dist/implementation/world/create.js +53 -2
- package/dist/implementation/world/create.js.map +1 -1
- package/dist/implementation/world/entities.d.ts +2 -0
- package/dist/implementation/world/entities.js +6 -0
- package/dist/implementation/world/entities.js.map +1 -1
- package/dist/implementation/world/execute.d.ts +4 -4
- package/dist/implementation/world/execute.js +132 -83
- package/dist/implementation/world/execute.js.map +1 -1
- package/dist/implementation/world/finalizers.d.ts +2 -0
- package/dist/implementation/world/finalizers.js +24 -3
- package/dist/implementation/world/finalizers.js.map +1 -1
- package/dist/implementation/world/join.js +22 -4
- package/dist/implementation/world/join.js.map +1 -1
- package/dist/implementation/world/register.js +2 -2
- package/dist/implementation/world/resource-changes.d.ts +18 -0
- package/dist/implementation/world/resource-changes.js +66 -0
- package/dist/implementation/world/resource-changes.js.map +1 -0
- package/dist/implementation/world/rows.d.ts +1 -1
- package/dist/implementation/world/rows.js +23 -13
- package/dist/implementation/world/rows.js.map +1 -1
- package/dist/implementation/world/run-changes.d.ts +38 -0
- package/dist/implementation/world/run-changes.js +138 -0
- package/dist/implementation/world/run-changes.js.map +1 -0
- package/dist/implementation/world/runner.js +19 -15
- package/dist/implementation/world/runner.js.map +1 -1
- package/dist/implementation/world/sharing.js +1 -1
- package/dist/implementation/world/staging.d.ts +4 -0
- package/dist/implementation/world/staging.js +15 -3
- package/dist/implementation/world/staging.js.map +1 -1
- package/dist/implementation/world/state.d.ts +13 -0
- package/dist/implementation/world/state.js +13 -1
- package/dist/implementation/world/state.js.map +1 -1
- package/dist/implementation/world/synchronize.js +72 -22
- package/dist/implementation/world/synchronize.js.map +1 -1
- package/dist/implementation/world/tables-read.d.ts +9 -0
- package/dist/implementation/world/tables-read.js +72 -0
- package/dist/implementation/world/tables-read.js.map +1 -0
- package/dist/implementation/world/tables.d.ts +10 -0
- package/dist/implementation/world/tables.js +401 -0
- package/dist/implementation/world/tables.js.map +1 -0
- package/dist/implementation/world/thread-observer.js +1 -1
- package/dist/implementation/world/thread-observer.js.map +1 -1
- package/dist/implementation/world/thread-systems.js +11 -6
- package/dist/implementation/world/thread-systems.js.map +1 -1
- package/dist/implementation/world/up.js +7 -1
- package/dist/implementation/world/up.js.map +1 -1
- package/dist/implementation/world/where.d.ts +8 -0
- package/dist/implementation/world/where.js +40 -11
- package/dist/implementation/world/where.js.map +1 -1
- package/dist/index.d.ts +8 -28
- package/dist/index.js +4 -9
- package/dist/index.js.map +1 -1
- package/dist/interface/component/declarations.d.ts +4 -0
- package/dist/interface/component/declarations.js +8 -0
- package/dist/interface/component/declarations.js.map +1 -0
- package/dist/interface/component/declared.d.ts +12 -0
- package/dist/interface/component/declared.js +2 -0
- package/dist/interface/component/declared.js.map +1 -0
- package/dist/interface/component/index.d.ts +2 -0
- package/dist/interface/component/index.js +1 -1
- package/dist/interface/component/index.js.map +1 -1
- package/dist/interface/finalizer/body.d.ts +18 -3
- package/dist/interface/finalizer/create.d.ts +7 -5
- package/dist/interface/finalizer/create.js +8 -5
- package/dist/interface/finalizer/create.js.map +1 -1
- package/dist/interface/finalizer/declaration.d.ts +4 -2
- package/dist/interface/indexes/declaration-of.d.ts +17 -0
- package/dist/interface/indexes/declaration-of.js +20 -0
- package/dist/interface/indexes/declaration-of.js.map +1 -0
- package/dist/interface/indexes/index.d.ts +1 -0
- package/dist/interface/indexes/index.js +1 -0
- package/dist/interface/indexes/index.js.map +1 -1
- package/dist/interface/registry/names.d.ts +2 -0
- package/dist/interface/registry/names.js +9 -0
- package/dist/interface/registry/names.js.map +1 -1
- package/dist/interface/registry/schedule.d.ts +31 -4
- package/dist/interface/registry/schedule.js +185 -24
- package/dist/interface/registry/schedule.js.map +1 -1
- package/dist/interface/resource/declarations.d.ts +4 -0
- package/dist/interface/resource/declarations.js +8 -0
- package/dist/interface/resource/declarations.js.map +1 -0
- package/dist/interface/resource/index.d.ts +2 -0
- package/dist/interface/resource/index.js +1 -1
- package/dist/interface/resource/index.js.map +1 -1
- package/dist/interface/system/access.d.ts +15 -0
- package/dist/interface/system/body.d.ts +11 -6
- package/dist/interface/system/check.d.ts +8 -1
- package/dist/interface/system/check.js +42 -12
- package/dist/interface/system/check.js.map +1 -1
- package/dist/interface/system/commands.d.ts +37 -0
- package/dist/interface/system/create-resolved.js +1 -1
- package/dist/interface/system/create-resolved.js.map +1 -1
- package/dist/interface/system/create.d.ts +31 -15
- package/dist/interface/system/create.js +8 -7
- package/dist/interface/system/create.js.map +1 -1
- package/dist/interface/system/declaration.d.ts +9 -2
- package/dist/interface/system/describe.d.ts +120 -0
- package/dist/interface/system/describe.js +116 -0
- package/dist/interface/system/describe.js.map +1 -0
- package/dist/interface/system/index.d.ts +1 -0
- package/dist/interface/system/index.js +1 -0
- package/dist/interface/system/index.js.map +1 -1
- package/dist/interface/system/leaf.d.ts +6 -0
- package/dist/interface/system/render.js +1 -0
- package/dist/interface/system/render.js.map +1 -1
- package/dist/interface/system/resolve.d.ts +5 -1
- package/dist/interface/system/resolve.js +19 -1
- package/dist/interface/system/resolve.js.map +1 -1
- package/dist/interface/system/shared-only.d.ts +7 -0
- package/dist/interface/system/shared-only.js +2 -0
- package/dist/interface/system/shared-only.js.map +1 -0
- package/dist/interface/system/tables.d.ts +28 -0
- package/dist/interface/system/tables.js +2 -0
- package/dist/interface/system/tables.js.map +1 -0
- package/dist/interface/world/index.d.ts +1 -1
- package/dist/interface/world/table/index.d.ts +27 -0
- package/dist/interface/world/table/index.js +3 -0
- package/dist/interface/world/table/index.js.map +1 -0
- package/dist/interface/world/table/table.d.ts +11 -0
- package/dist/interface/world/table/table.js +2 -0
- package/dist/interface/world/table/table.js.map +1 -0
- package/dist/interface/world/world.d.ts +25 -10
- package/dist/world.d.ts +27 -0
- package/dist/world.js +27 -0
- package/dist/world.js.map +1 -0
- package/package.json +11 -2
- package/src/exports.test.ts +1 -1
- package/src/guide/00-keys.ts +1 -1
- package/src/guide/01-resources.md +1 -1
- package/src/guide/01-resources.ts +1 -1
- package/src/guide/02-entities.ts +2 -2
- package/src/guide/03-systems.md +2 -2
- package/src/guide/03-systems.out +1 -1
- package/src/guide/03-systems.ts +3 -3
- package/src/guide/04-ticks.md +1 -1
- package/src/guide/04-ticks.ts +3 -3
- package/src/guide/05-scheduling.ts +1 -1
- package/src/guide/06-sync-points.md +18 -7
- package/src/guide/06-sync-points.out +1 -2
- package/src/guide/06-sync-points.ts +10 -6
- package/src/guide/07-change-filters.ts +2 -2
- package/src/guide/08-input.ts +1 -1
- package/src/guide/09-rules.ts +1 -1
- package/src/guide/11-traits.ts +3 -3
- package/src/guide/12-lookups.ts +2 -2
- package/src/guide/13-indexes.ts +4 -4
- package/src/guide/14-kernels.ts +1 -1
- package/src/guide/15-reductions.ts +1 -1
- package/src/guide/16-gpu.ts +1 -1
- package/src/guide/17-hierarchies.ts +1 -1
- package/src/guide/18-identity.out +1 -1
- package/src/guide/19-threads.md +4 -2
- package/src/guide/19-threads.ts +2 -2
- package/src/guide/20-shared.ts +3 -3
- package/src/guide/21-dom.md +2 -2
- package/src/guide/21-dom.ts +1 -1
- package/src/guide/22-canvas.md +1 -1
- package/src/guide/22-canvas.ts +1 -1
- package/src/guide/CLAUDE.md +1 -1
- package/src/guide/guide.test.ts +9 -154
- package/src/implementation/compiler/body-access.ts +102 -12
- package/src/implementation/compiler/describe.test-helper.ts +1 -1
- package/src/implementation/compiler/describe.test.ts +1 -1
- package/src/implementation/compiler/describe.ts +3 -3
- package/src/implementation/compiler/estree.ts +9 -6
- package/src/implementation/compiler/hygiene.test.ts +1 -1
- package/src/implementation/compiler/inferred.test.ts +1 -1
- package/src/implementation/compiler/inferred.ts +4 -2
- package/src/implementation/compiler/plugin.test.ts +2 -2
- package/src/implementation/compiler/signature.ts +1 -1
- package/src/implementation/compiler/transform.test.ts +43 -23
- package/src/implementation/compiler/transform.ts +6 -4
- package/src/implementation/world/activate.ts +11 -2
- package/src/implementation/world/batches.test.ts +3 -3
- package/src/implementation/world/buffer.ts +7 -0
- package/src/implementation/world/cascade.ts +46 -22
- package/src/implementation/world/change-blocks.test.ts +1 -1
- package/src/implementation/world/commands.ts +76 -2
- package/src/implementation/world/create.test.ts +8 -8
- package/src/implementation/world/create.ts +57 -2
- package/src/implementation/world/defaults.test.ts +1 -1
- package/src/implementation/world/despawn-where.test.ts +287 -0
- package/src/implementation/world/entities.ts +6 -0
- package/src/implementation/world/execute.ts +125 -78
- package/src/implementation/world/finalizers.test.ts +1 -1
- package/src/implementation/world/finalizers.ts +27 -4
- package/src/implementation/world/ids.test.ts +1 -1
- package/src/implementation/world/index-changed.test.ts +2 -2
- package/src/implementation/world/index-joined.test.ts +5 -5
- package/src/implementation/world/index-order.test.ts +1 -1
- package/src/implementation/world/indexes.test.ts +2 -1
- package/src/implementation/world/join.test.ts +23 -4
- package/src/implementation/world/join.ts +26 -4
- package/src/implementation/world/joined-observer.test.ts +1 -1
- package/src/implementation/world/joining-worker.fixture.ts +1 -1
- package/src/implementation/world/joining.fixture.ts +1 -1
- package/src/implementation/world/joining.test-helper.ts +1 -1
- package/src/implementation/world/kernel-spawns.test.ts +1 -1
- package/src/implementation/world/names.test.ts +1 -2
- package/src/implementation/world/observe-factories.test.ts +2 -2
- package/src/implementation/world/observe-ticks-cases.test.ts +1 -1
- package/src/implementation/world/observe.test-helper.ts +1 -1
- package/src/implementation/world/observer-contained-worker.fixture.ts +1 -1
- package/src/implementation/world/observer-contained.test-helper.ts +1 -1
- package/src/implementation/world/observer-contained.test.ts +2 -2
- package/src/implementation/world/observer-joined.test-helper.ts +4 -4
- package/src/implementation/world/observer-joined.test.ts +23 -15
- package/src/implementation/world/observer.browser.test.ts +2 -2
- package/src/implementation/world/observer.test.ts +8 -8
- package/src/implementation/world/optional-workers.fixture.ts +1 -1
- package/src/implementation/world/optional.test.ts +7 -7
- package/src/implementation/world/perf.md +3 -1
- package/src/implementation/world/reclaim.test.ts +3 -3
- package/src/implementation/world/register.ts +2 -2
- package/src/implementation/world/remote-objects.test-helper.ts +1 -1
- package/src/implementation/world/resource-changes.test.ts +61 -0
- package/src/implementation/world/resource-changes.ts +67 -0
- package/src/implementation/world/rows.ts +24 -13
- package/src/implementation/world/rules.test.ts +37 -0
- package/src/implementation/world/run-changes.test.ts +88 -0
- package/src/implementation/world/run-changes.ts +160 -0
- package/src/implementation/world/run-results.test.ts +10 -10
- package/src/implementation/world/runner.ts +20 -16
- package/src/implementation/world/shared-gpu.browser.test.ts +2 -2
- package/src/implementation/world/shared-kernels.test.ts +2 -2
- package/src/implementation/world/shared-workers.fixture.ts +2 -2
- package/src/implementation/world/shared.test.ts +10 -10
- package/src/implementation/world/sharing.ts +1 -1
- package/src/implementation/world/staging.ts +16 -3
- package/src/implementation/world/state.ts +19 -0
- package/src/implementation/world/sync-points.test.ts +135 -0
- package/src/implementation/world/synchronize.ts +65 -18
- package/src/implementation/world/tables-read.ts +70 -0
- package/src/implementation/world/tables.test.ts +369 -0
- package/src/implementation/world/tables.ts +368 -0
- package/src/implementation/world/tails.test.ts +1 -1
- package/src/implementation/world/target.test.ts +2 -2
- package/src/implementation/world/thread-observer.ts +1 -1
- package/src/implementation/world/thread-systems.test-helper.ts +3 -3
- package/src/implementation/world/thread-systems.ts +11 -6
- package/src/implementation/world/up.test.ts +13 -1
- package/src/implementation/world/up.ts +6 -1
- package/src/implementation/world/where.test.ts +4 -4
- package/src/implementation/world/where.ts +38 -10
- package/src/implementation/world/workers.fixture.ts +2 -2
- package/src/index.ts +4 -29
- package/src/interface/component/declarations.ts +11 -0
- package/src/interface/component/declared.ts +9 -0
- package/src/interface/component/index.ts +2 -0
- package/src/interface/finalizer/body.ts +13 -3
- package/src/interface/finalizer/create.test.ts +3 -1
- package/src/interface/finalizer/create.ts +12 -7
- package/src/interface/finalizer/declaration.ts +4 -2
- package/src/interface/index.type-test.ts +2 -2
- package/src/interface/indexes/create.test.ts +10 -0
- package/src/interface/indexes/declaration-of.ts +34 -0
- package/src/interface/indexes/index.ts +1 -0
- package/src/interface/indexes/index.type-test.ts +1 -1
- package/src/interface/registry/names.ts +8 -0
- package/src/interface/registry/schedule.ts +200 -27
- package/src/interface/resource/declarations.ts +11 -0
- package/src/interface/resource/index.ts +2 -0
- package/src/interface/system/access.ts +16 -0
- package/src/interface/system/body.ts +7 -5
- package/src/interface/system/check.ts +37 -10
- package/src/interface/system/commands.ts +33 -0
- package/src/interface/system/commands.type-test.ts +31 -0
- package/src/interface/system/create-resolved.ts +1 -1
- package/src/interface/system/create.test.ts +21 -13
- package/src/interface/system/create.ts +24 -15
- package/src/interface/system/declaration.ts +9 -2
- package/src/interface/system/describe.test.ts +79 -0
- package/src/interface/system/describe.ts +122 -0
- package/src/interface/system/index.ts +1 -0
- package/src/interface/system/leaf.ts +6 -0
- package/src/interface/system/lists.type-test.ts +1 -1
- package/src/interface/system/optional.type-test.ts +1 -1
- package/src/interface/system/render.ts +1 -0
- package/src/interface/system/resolve.ts +25 -2
- package/src/interface/system/shared-only.ts +5 -0
- package/src/interface/system/shared.type-test.ts +1 -1
- package/src/interface/system/system.type-test.ts +26 -20
- package/src/interface/system/tables.ts +27 -0
- package/src/interface/system/up.type-test.ts +3 -3
- package/src/interface/world/index.ts +2 -2
- package/src/interface/world/run.type-test.ts +14 -4
- package/src/interface/world/table/index.ts +20 -0
- package/src/interface/world/table/table.ts +12 -0
- package/src/interface/world/world.ts +23 -10
- package/src/observe/entity.ts +2 -2
- package/src/observe/resource.ts +2 -2
- package/src/observe/select.ts +4 -4
- package/src/observe/world-state.ts +3 -3
- package/src/perftest/benchmarks.ts +19 -1
- package/src/perftest/indexes.ts +3 -3
- package/src/perftest/loop.ts +1 -1
- package/src/perftest/observer.ts +4 -4
- package/src/perftest/reads.ts +2 -2
- package/src/perftest/remote.ts +5 -5
- package/src/perftest/strings.ts +1 -1
- package/src/perftest/structural.ts +20 -0
- package/src/perftest/suite.ts +1 -1
- package/src/perftest/tables.ts +107 -0
- package/src/world.test.ts +2 -2
- package/src/world.ts +32 -0
|
@@ -149,8 +149,8 @@ const emitted = { seen: 0 };
|
|
|
149
149
|
const countEmitted = System.create({ components: { added: [position], with: [spawned] } }, () => { emitted.seen++; });
|
|
150
150
|
|
|
151
151
|
const gathered: Entity[] = [];
|
|
152
|
-
const gatherPositioned = System.create({
|
|
153
|
-
const gatherSpawned = System.create({
|
|
152
|
+
const gatherPositioned = System.create({ readonly: true, components: { with: [position] } }, row => { gathered.push(row.entity); });
|
|
153
|
+
const gatherSpawned = System.create({ readonly: true, components: { with: [spawned] } }, row => { gathered.push(row.entity); });
|
|
154
154
|
|
|
155
155
|
/** The entities an observer row system passes, in storage order. */
|
|
156
156
|
const storageOrder = async (world: World, gather: System<void>): Promise<Entity[]> => {
|
package/src/index.ts
CHANGED
|
@@ -3,9 +3,6 @@
|
|
|
3
3
|
* A high-performance, multithreaded Entity Component System.
|
|
4
4
|
* @module @adobe/ecs
|
|
5
5
|
*/
|
|
6
|
-
import { create, join } from './implementation/index.js';
|
|
7
|
-
import type { Create } from './interface/world/create.js';
|
|
8
|
-
import type { Join } from './interface/world/join.js';
|
|
9
6
|
import type { World as Created } from './interface/world/world.js';
|
|
10
7
|
|
|
11
8
|
export * from './interface/index.js';
|
|
@@ -15,30 +12,8 @@ export type World = Created;
|
|
|
15
12
|
|
|
16
13
|
/**
|
|
17
14
|
* How a world is made, `World.create(resources?)`, on the thread that will run it, with resources starting at the given
|
|
18
|
-
* values in place of their defaults;
|
|
19
|
-
* `World.join(port, { threads })
|
|
15
|
+
* values in place of their defaults; how another thread joins one that shared itself with it,
|
|
16
|
+
* `World.join(port, { threads })`; and `World.Table`, an archetype's rows as a readonly system's `tables` copies them
|
|
17
|
+
* and `commands.spawnTables` spawns them.
|
|
20
18
|
*/
|
|
21
|
-
export
|
|
22
|
-
/**
|
|
23
|
-
* Creates a world on this thread, synchronously, with resources starting at the given values in place of their
|
|
24
|
-
* defaults. This thread is its `"world"` thread for the world's life: it runs the sync points, the ticks and the GPU
|
|
25
|
-
* device, and fills every logical thread no joined thread claims. Keys are the resources' symbols, built-in ones
|
|
26
|
-
* included (`{ [workers]: 0 }`); string keys are never a resource's, so they're ignored. Throws for a symbol that isn't
|
|
27
|
-
* a resource, or is one only the world writes. While a declaration's trait or default doesn't hold, each call fails to
|
|
28
|
-
* compile, once, naming each such key, its members and what's wrong with them.
|
|
29
|
-
*/
|
|
30
|
-
readonly create: Create;
|
|
31
|
-
/**
|
|
32
|
-
* Joins, from another thread, a world that `share` let join through the other end of `port`, claiming the logical
|
|
33
|
-
* `threads` this thread fills: their objects live here, and the systems and finalizers reading them run here. Resolves,
|
|
34
|
-
* once the world accepted the claim, to the same world: its `run` runs systems there, and its `add` and `remove` take
|
|
35
|
-
* observer systems, which run here after the world's sync points. Add them before awaiting anything else, so the
|
|
36
|
-
* world's `share` resolves with them in place. Its `dispose` disconnects this thread. Rejects with an `Error` saying
|
|
37
|
-
* why it didn't join: the world refused the claim, was disposed, or its thread ended first, as the platform reports it:
|
|
38
|
-
* the world's `Worker` failing to start or throwing first, a port closed, as in node, or, in a browser, the world's
|
|
39
|
-
* thread ending once it was shared through a port this thread listened on. A browser world's worker that the page ended
|
|
40
|
-
* before this thread heard from it isn't reported, so the join stays pending. Rejects too where memory can't be shared,
|
|
41
|
-
* as without cross-origin isolation.
|
|
42
|
-
*/
|
|
43
|
-
readonly join: Join;
|
|
44
|
-
} = { create, join };
|
|
19
|
+
export * as World from './world.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import type { Declared } from './declared.js';
|
|
3
|
+
import { declaredOf } from '../registry/names.js';
|
|
4
|
+
import type { Key } from './key.js';
|
|
5
|
+
|
|
6
|
+
/** Every component its modules registered so far, with its declaration. */
|
|
7
|
+
export const declarations = (): ReadonlyMap<Key, Declared> => {
|
|
8
|
+
// Registration takes only keys declared in `Components`, so each symbol registered as a component is one of its keys.
|
|
9
|
+
const found = declaredOf("component") as ReadonlyMap<Key, Declared>;
|
|
10
|
+
return found;
|
|
11
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import type { Schema } from '../schema/schema.js';
|
|
3
|
+
import type { Key as TraitKey } from '../trait/key.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A component's or resource's declaration as registered: its schema as `type` where its value is data, what it
|
|
7
|
+
* starts at as `default`, and its other traits, the built-in ones by name and those of your own by their keys.
|
|
8
|
+
*/
|
|
9
|
+
export type Declared = { readonly type?: Schema; readonly default?: unknown } & { readonly [T in TraitKey]?: unknown };
|
|
@@ -7,6 +7,8 @@ import type { LookupFunction } from '../system/values.js';
|
|
|
7
7
|
import type { WriteResources } from '../system/body.js';
|
|
8
8
|
import type { ChangesDeclaration, Declaration } from './declaration.js';
|
|
9
9
|
import type { Essential, ReadonlyType } from '../component/value.js';
|
|
10
|
+
import type { Resource } from '../resource/resource.js';
|
|
11
|
+
import type { ReadonlyType as ResourceType } from '../resource/value.js';
|
|
10
12
|
import type { Elsewhere, ThreadsOf } from '../system/threads.js';
|
|
11
13
|
|
|
12
14
|
/**
|
|
@@ -29,7 +31,10 @@ export type Context<D extends Declaration | ChangesDeclaration> =
|
|
|
29
31
|
& (true extends Elsewhere<ThreadsOf<D>> ? {} : { readonly commands: Commands });
|
|
30
32
|
|
|
31
33
|
/** The components a changes finalizer sees. */
|
|
32
|
-
export type Watched<D extends ChangesDeclaration> = Property<D, "watch"> extends readonly
|
|
34
|
+
export type Watched<D extends ChangesDeclaration> = Property<D, "watch"> extends readonly unknown[] ? Listed<D, "watch"> & Essential : Essential;
|
|
35
|
+
|
|
36
|
+
/** The resources a changes finalizer sees: those it watches. */
|
|
37
|
+
export type WatchedResources<D extends ChangesDeclaration> = Listed<D, "watch"> & Resource.Key;
|
|
33
38
|
|
|
34
39
|
/** Entities whose components changed the same way: `from` and `to` are null for those spawned and despawned. */
|
|
35
40
|
export type Group<C extends Component.Key> = {
|
|
@@ -42,9 +47,14 @@ export type Group<C extends Component.Key> = {
|
|
|
42
47
|
readonly entities: readonly Entity[];
|
|
43
48
|
};
|
|
44
49
|
|
|
45
|
-
/**
|
|
46
|
-
|
|
50
|
+
/**
|
|
51
|
+
* A sync point's structural changes to the watched components, and the values adds replace, before they apply; and each
|
|
52
|
+
* watched resource written since the last sync point's finalizers ran, to a value that isn't equal to its last one,
|
|
53
|
+
* leaf by leaf for data and by identity for objects, with its value before and after.
|
|
54
|
+
*/
|
|
55
|
+
export type ChangeSet<C extends Component.Key, R extends Resource.Key = never> = {
|
|
47
56
|
readonly groups: readonly Group<C>[];
|
|
57
|
+
readonly resources: readonly { readonly [K in R]: { readonly key: K; readonly previous: ResourceType<K>; readonly next: ResourceType<K> } }[R][];
|
|
48
58
|
/** The value before the sync point, or null if the entity didn't have it. */
|
|
49
59
|
previous<K extends C>(entity: Entity, component: K): ReadonlyType<K> | null;
|
|
50
60
|
/** The value after it, or null if the entity won't have it. */
|
|
@@ -75,7 +75,7 @@ describe("Finalizer.create and Finalizer.changes", () => {
|
|
|
75
75
|
expect(() => untyped(Finalizer.create, { components: {} }, { resources: { reduce: [total] } })).toThrow("only systems contribute to a reduction");
|
|
76
76
|
expect(() => untyped(Finalizer.create, { components: { with: [node, tags] } }, { read: [node, tags] })).toThrow('uses "typesFinalizerTest/node" on "main" and "typesFinalizerTest/tags" on "world"');
|
|
77
77
|
expect(() => untyped(Finalizer.changes, { name: "derived", watch: [slot] })).toThrow("which is derived");
|
|
78
|
-
expect(() => untyped(Finalizer.changes, { name: "
|
|
78
|
+
expect(() => untyped(Finalizer.changes, { name: "unregistered", watch: [Symbol("typesFinalizerTest/unregistered")] })).toThrow("which isn't a registered component or resource");
|
|
79
79
|
expect(() => untyped(Finalizer.create, { components: { with: [time] } })).toThrow(`uses component "typesFinalizerTest/time", which isn't a registered component`);
|
|
80
80
|
expect(() => untyped(Finalizer.create, { components: { with: [label] }, lookup: { others: { not: [Symbol("typesFinalizerTest/unregistered")] } } }, { read: [label] })).toThrow("which isn't a registered component");
|
|
81
81
|
expect(programFinalizers()).toHaveLength(7);
|
|
@@ -93,6 +93,8 @@ describe("Finalizer.create and Finalizer.changes", () => {
|
|
|
93
93
|
.toThrow('uses "typesFinalizerTest/tags" on "world" and "typesFinalizerTest/page" on "main"');
|
|
94
94
|
expect(() => untyped(Finalizer.changes, {}, { resources: { read: [page] } }))
|
|
95
95
|
.toThrow(`watches or reads values on thread "main"; a changes finalizer runs on the world's thread, in the sync point.`);
|
|
96
|
+
expect(() => untyped(Finalizer.changes, { name: "savePage", watch: [page] }))
|
|
97
|
+
.toThrow(`watches or reads values on thread "main"; a changes finalizer runs on the world's thread, in the sync point.`);
|
|
96
98
|
// Reading another thread's values, without writing or looking up, it's created.
|
|
97
99
|
const detach = Finalizer.create({ components: { with: [node] } }, row => { void row[node]; });
|
|
98
100
|
expect(programFinalizers()).toHaveLength(8);
|
|
@@ -8,9 +8,11 @@ import { checkWorldWritten } from '../registry/written.js';
|
|
|
8
8
|
import { checkLookup } from '../registry/indexes.js';
|
|
9
9
|
import type { Listed, Property } from '../system/property.js';
|
|
10
10
|
import type { ThreadOfComponent } from '../component/value.js';
|
|
11
|
+
import type { ThreadOfResource } from '../resource/value.js';
|
|
12
|
+
import type { Resource } from '../resource/resource.js';
|
|
11
13
|
import type { Component } from '../component/component.js';
|
|
12
14
|
import type { Elsewhere, OneThread, ThreadedBy, ThreadsOf } from '../system/threads.js';
|
|
13
|
-
import type { ChangeSet, Context, Row, Watched } from './body.js';
|
|
15
|
+
import type { ChangeSet, Context, Row, Watched, WatchedResources } from './body.js';
|
|
14
16
|
import type { Changes, Leaf } from './created.js';
|
|
15
17
|
import type { ChangesDeclaration, Declaration, Resolved, ResolvedChanges } from './declaration.js';
|
|
16
18
|
import { accessOf, compact, lookupsLists, resourceUses, type Access } from '../system/resolve.js';
|
|
@@ -26,7 +28,7 @@ type Remote<D> = true extends Elsewhere<ThreadsOf<D>>
|
|
|
26
28
|
: {};
|
|
27
29
|
|
|
28
30
|
/** A changes finalizer runs on the world's thread, in the sync point: it watches and reads no other thread's objects. */
|
|
29
|
-
type OnWorld<D> = true extends Elsewhere<ThreadsOf<D> | ThreadOfComponent<Listed<D, "watch"> & Component.Key>>
|
|
31
|
+
type OnWorld<D> = true extends Elsewhere<ThreadsOf<D> | ThreadOfComponent<Listed<D, "watch"> & Component.Key> | ThreadOfResource<Listed<D, "watch"> & Resource.Key>>
|
|
30
32
|
? { readonly "error: a changes finalizer watches and reads only the world's thread's objects": true }
|
|
31
33
|
: {};
|
|
32
34
|
|
|
@@ -127,12 +129,12 @@ export const create = <const D extends Declaration>(
|
|
|
127
129
|
|
|
128
130
|
/**
|
|
129
131
|
* Declares a finalizer of each sync point's structural changes, which applies in every world, and returns it. It runs
|
|
130
|
-
* on the world's thread. Throws as `create` does, for a watched component that isn't registered or is derived,
|
|
131
|
-
* one watching or reading another thread's values.
|
|
132
|
+
* on the world's thread. Throws as `create` does, for a watched component that isn't registered or is derived, a
|
|
133
|
+
* watched key that's neither a component nor a resource, and for one watching or reading another thread's values.
|
|
132
134
|
*/
|
|
133
135
|
export const changes = <const D extends ChangesDeclaration>(
|
|
134
136
|
declared: D & Inferred & OnWorld<D>,
|
|
135
|
-
body: (changes: ChangeSet<Watched<D>>, context: Context<D>) => void,
|
|
137
|
+
body: (changes: ChangeSet<Watched<D>, WatchedResources<D>>, context: Context<D>) => void,
|
|
136
138
|
): Finalizer => {
|
|
137
139
|
const id = takeIdOfDeclaration(declared);
|
|
138
140
|
const name = nameOf(declared, id, "finalizer");
|
|
@@ -148,10 +150,13 @@ export const changes = <const D extends ChangesDeclaration>(
|
|
|
148
150
|
for (const keys of [lookup.read, "has" in lookup ? lookup.has : undefined, "not" in lookup ? lookup.not : undefined]) requireRegistered("Finalizer", name, "component", keys);
|
|
149
151
|
}
|
|
150
152
|
for (const key of declaration.watch ?? []) {
|
|
151
|
-
|
|
153
|
+
const kind = kindOf(key);
|
|
154
|
+
if (kind !== "component" && kind !== "resource") throw new Error(`Finalizer "${name}" watches "${labelOf(key)}", which isn't a registered component or resource: ${whyUnregistered(key)}.`);
|
|
152
155
|
if (declarationOf(key)?.derived === true) throw new Error(`Finalizer "${name}" watches "${labelOf(key)}", which is derived; it sees only components user code adds and removes.`);
|
|
153
156
|
}
|
|
154
|
-
const
|
|
157
|
+
const watched = (kind: "component" | "resource"): readonly symbol[] => (declaration.watch ?? []).filter(key => kindOf(key) === kind);
|
|
158
|
+
const read = [...declaration.resources?.read ?? [], ...watched("resource")];
|
|
159
|
+
const thread = threadOf({ components: { read: watched("component") }, resources: { ...declaration.resources, read }, lookup: declaration.lookup }, "Finalizer", name);
|
|
155
160
|
if (thread !== null && thread !== "world") throw new Error(`Finalizer "${name}" watches or reads values on thread "${thread}"; a changes finalizer runs on the world's thread, in the sync point.`);
|
|
156
161
|
checkOrder(name, "world", declaration);
|
|
157
162
|
return addFinalizer(finalizer, id);
|
|
@@ -4,6 +4,7 @@ import type { SystemLookupLists, ResourceLists } from '../system/access.js';
|
|
|
4
4
|
import type { Lookup } from '../system/access.js';
|
|
5
5
|
import type { Finalizer } from './finalizer.js';
|
|
6
6
|
import type { Essential } from '../component/value.js';
|
|
7
|
+
import type { Resource } from '../resource/resource.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* What every finalizer declares. What its body reads of its row and lookups, and reads and writes of resources, the
|
|
@@ -26,9 +27,10 @@ export type Declaration = Shared & {
|
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* A finalizer that sees each sync point's structural changes to the components it watches: by default, every one that
|
|
29
|
-
* isn't derived or an object a joined thread fills
|
|
30
|
+
* isn't derived or an object a joined thread fills; and the resources it watches, none by default, written since the
|
|
31
|
+
* last sync point's finalizers ran.
|
|
30
32
|
*/
|
|
31
|
-
export type ChangesDeclaration = Shared & { readonly watch?: readonly Essential[] };
|
|
33
|
+
export type ChangesDeclaration = Shared & { readonly watch?: readonly (Essential | Resource.Key)[] };
|
|
32
34
|
|
|
33
35
|
type Key = symbol;
|
|
34
36
|
|
|
@@ -27,8 +27,8 @@ Rule.create({ components: { with: [position] }, derive: [slot] });
|
|
|
27
27
|
declare const create: Create;
|
|
28
28
|
declare const wgsl: typeof Gpu.wgsl;
|
|
29
29
|
export const world = create({ [workers]: 4, [gpuDevice]: null });
|
|
30
|
-
export const read = System.create({
|
|
31
|
-
export const readOne = System.create({
|
|
30
|
+
export const read = System.create({ readonly: true, components: { with: [position, slot] } }, () => {});
|
|
31
|
+
export const readOne = System.create({ readonly: true, lookup: { at: { with: [position, slot] } } }, () => {});
|
|
32
32
|
export const report = world.run(all);
|
|
33
33
|
export const shader = wgsl(draw);
|
|
34
34
|
export const added = world.add(read);
|
|
@@ -144,3 +144,13 @@ describe("systems ordering their rows by an index", () => {
|
|
|
144
144
|
expect(() => untypedRender(Index.create({ key: [amount] }))).toThrow(/orders its rows by index "index #\d+", which isn't sorted/);
|
|
145
145
|
});
|
|
146
146
|
});
|
|
147
|
+
|
|
148
|
+
describe("Index.declarationOf", () => {
|
|
149
|
+
it("gives an index's declaration as registered, its with and not as declared, and nothing for a value Index.create didn't return", () => {
|
|
150
|
+
const byName = Index.create({ key: [name], unique: true, components: { with: [shown], not: [parent] } });
|
|
151
|
+
expect(Index.declarationOf(byName)).toEqual({ key: [name], components: { with: [shown], not: [parent] }, unique: true });
|
|
152
|
+
const byCell = Index.create({ key: [cell, amount], sorted: true });
|
|
153
|
+
expect(Index.declarationOf(byCell)).toEqual({ key: [cell, amount], components: {}, sorted: true });
|
|
154
|
+
expect(Index.declarationOf({ name: "not an index" })).toBeUndefined();
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import type { Component } from '../component/component.js';
|
|
3
|
+
import { indexDefinitionOf } from '../registry/indexes.js';
|
|
4
|
+
import { kindOf } from '../registry/names.js';
|
|
5
|
+
import type { Index } from './index-type.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* An index's declaration as `Index.create` registered it: its key, the components it holds by besides its key and those
|
|
9
|
+
* it excludes, and whether it's sorted or unique.
|
|
10
|
+
*/
|
|
11
|
+
export type Declared = {
|
|
12
|
+
readonly key: readonly Component.Key[];
|
|
13
|
+
readonly components: { readonly with?: readonly Component.Key[]; readonly not?: readonly Component.Key[] };
|
|
14
|
+
readonly sorted?: boolean;
|
|
15
|
+
readonly unique?: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Registration takes only components for a key part, `with` and `not`. */
|
|
19
|
+
const isComponent = (key: symbol): key is Component.Key => kindOf(key) === "component";
|
|
20
|
+
|
|
21
|
+
/** The declaration `index` was created from, as registered, or undefined for a value `Index.create` didn't return. */
|
|
22
|
+
export const declarationOf = <D extends object>(index: Index<D>): Declared | undefined => {
|
|
23
|
+
const created = indexDefinitionOf(index);
|
|
24
|
+
if (created === undefined) return undefined;
|
|
25
|
+
const { key, components, sorted, unique } = created.declaration;
|
|
26
|
+
const held = components?.has?.filter(isComponent);
|
|
27
|
+
const excluded = components?.not?.filter(isComponent);
|
|
28
|
+
return {
|
|
29
|
+
key: key.filter(isComponent),
|
|
30
|
+
components: { ...held === undefined ? {} : { with: held }, ...excluded === undefined ? {} : { not: excluded } },
|
|
31
|
+
...sorted === undefined ? {} : { sorted },
|
|
32
|
+
...unique === undefined ? {} : { unique },
|
|
33
|
+
};
|
|
34
|
+
};
|
|
@@ -219,7 +219,7 @@ System.create({
|
|
|
219
219
|
|
|
220
220
|
// An index lookup's `changed: true` runs its system only when the index's entries changed since it last ran: its
|
|
221
221
|
// lists' `changed` then reports entries alone. Observer systems and CPU kernels take it; it's not a list of components.
|
|
222
|
-
System.create({
|
|
222
|
+
System.create({ readonly: true, lookup: { living: { index: living, changed: true } } }, ({ lookup }) => {
|
|
223
223
|
const rows = lookup.living();
|
|
224
224
|
check<Equal<typeof rows.changed, boolean>>();
|
|
225
225
|
check<Equal<typeof rows.length, number>>();
|
|
@@ -61,6 +61,14 @@ const entryOf = (key: symbol): Entry | undefined => {
|
|
|
61
61
|
return entries.get(key);
|
|
62
62
|
};
|
|
63
63
|
|
|
64
|
+
/** Every key registered as `kind`, each by the symbol a world gives its values by, with its declaration. */
|
|
65
|
+
export const declaredOf = (kind: "component" | "resource"): ReadonlyMap<symbol, AnyDeclaration> => {
|
|
66
|
+
catchUp();
|
|
67
|
+
const found = new Map<symbol, AnyDeclaration>();
|
|
68
|
+
for (const entry of entries.values()) if (entry.kind === kind && entry.declaration !== undefined) found.set(entry.newest, entry.declaration);
|
|
69
|
+
return found;
|
|
70
|
+
};
|
|
71
|
+
|
|
64
72
|
/** Whether a key is a component, a resource or a trait. */
|
|
65
73
|
export const kindOf = (key: unknown): Kind | undefined => typeof key === "symbol" ? entryOf(key)?.kind : undefined;
|
|
66
74
|
|
|
@@ -4,8 +4,8 @@ import type { Definition } from '../system/definition.js';
|
|
|
4
4
|
import type { Leaf } from '../system/leaf.js';
|
|
5
5
|
import { firstItemOf } from './items.js';
|
|
6
6
|
import { requireDefinition } from './program.js';
|
|
7
|
-
import { firstKeyOf, labelOf, sameDeclaration } from './names.js';
|
|
8
|
-
import { lookupKeysOf, orderKeysOf } from './indexes.js';
|
|
7
|
+
import { declarationOf, firstKeyOf, labelOf, sameDeclaration } from './names.js';
|
|
8
|
+
import { indexDefinitionOf, lookupKeysOf, orderKeysOf } from './indexes.js';
|
|
9
9
|
|
|
10
10
|
type Key = symbol;
|
|
11
11
|
|
|
@@ -28,21 +28,34 @@ const checkInputs = (leaves: readonly Leaf[]): void => {
|
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Leaves in run order, grouped into tiers. Leaves in one tier have no ordering path between them. `reductions` are the
|
|
31
|
-
* resources its leaves reduce into, each with how many of them contribute. `observer` are its
|
|
32
|
-
* tier: they run after its sync point, ordered with nothing.
|
|
31
|
+
* resources its leaves reduce into, each with how many of them contribute. `observer` are its readonly leaves, in no
|
|
32
|
+
* tier: they run after its sync point, ordered with nothing. `syncs` holds, after each tier, the reasons for a sync
|
|
33
|
+
* point there, which applies the commands so far before the next tier, or null for none.
|
|
33
34
|
*/
|
|
34
|
-
export type Schedule = {
|
|
35
|
+
export type Schedule = {
|
|
36
|
+
readonly tiers: readonly (readonly Leaf[])[];
|
|
37
|
+
readonly reductions: ReadonlyMap<Key, number>;
|
|
38
|
+
readonly observer: readonly Leaf[];
|
|
39
|
+
readonly syncs: readonly (readonly SyncReason[] | null)[];
|
|
40
|
+
};
|
|
35
41
|
|
|
36
42
|
export const isLeaf = (system: Definition): system is Leaf => system.kind !== "composite";
|
|
37
43
|
|
|
38
|
-
/** Whether a system is
|
|
39
|
-
export const isObserver = (system: Definition): system is Leaf & { readonly declaration: { readonly
|
|
44
|
+
/** Whether a system is a readonly system leaf, which runs after a sync point rather than in a tier. */
|
|
45
|
+
export const isObserver = (system: Definition): system is Leaf & { readonly declaration: { readonly readonly: true } } => system.kind !== "composite" && system.declaration.readonly === true;
|
|
40
46
|
|
|
41
47
|
/** Each system is an interval, from its start node to its end node. */
|
|
42
48
|
const start = (unit: number): number => 2 * unit;
|
|
43
49
|
const end = (unit: number): number => 2 * unit + 1;
|
|
44
50
|
|
|
45
|
-
type Graph = {
|
|
51
|
+
type Graph = {
|
|
52
|
+
readonly units: readonly Definition[];
|
|
53
|
+
readonly successors: readonly Set<number>[];
|
|
54
|
+
/** Each unit's members, by unit: none for a leaf. */
|
|
55
|
+
readonly members: readonly (readonly number[])[];
|
|
56
|
+
/** The units a list of systems names, readonly systems left out. */
|
|
57
|
+
readonly resolve: (systems: readonly object[] | undefined) => readonly number[];
|
|
58
|
+
};
|
|
46
59
|
|
|
47
60
|
/**
|
|
48
61
|
* A leaf runs from its start to its end, and a composite spans its members. `after` and `before` order one system's end
|
|
@@ -65,7 +78,7 @@ const graphOf = (root: Definition): Graph => {
|
|
|
65
78
|
};
|
|
66
79
|
visit(root);
|
|
67
80
|
const successors = units.flatMap(() => [new Set<number>(), new Set<number>()]);
|
|
68
|
-
//
|
|
81
|
+
// A readonly system is ordered with nothing, so constraints naming one are ignored.
|
|
69
82
|
const resolve = (systems: readonly object[] | undefined): readonly number[] =>
|
|
70
83
|
(systems ?? []).flatMap(system => {
|
|
71
84
|
const unit = indexOf.get(firstItemOf(system));
|
|
@@ -86,7 +99,7 @@ const graphOf = (root: Definition): Graph => {
|
|
|
86
99
|
successors[end(unit)].add(end(other));
|
|
87
100
|
}
|
|
88
101
|
});
|
|
89
|
-
return { units, successors };
|
|
102
|
+
return { units, successors, members, resolve };
|
|
90
103
|
};
|
|
91
104
|
|
|
92
105
|
/** Kahn's algorithm over nodes. Throws on a cycle. */
|
|
@@ -165,6 +178,74 @@ const conflictOf = (a: Access, b: Access): Key | undefined => {
|
|
|
165
178
|
return undefined;
|
|
166
179
|
};
|
|
167
180
|
|
|
181
|
+
/** Every key two accesses conflict on. */
|
|
182
|
+
const conflictsOf = (a: Access, b: Access): readonly Key[] => [...new Set([
|
|
183
|
+
...[...a.writes].filter(key => b.writes.has(key) || b.reads.has(key) || b.reduces.has(key)),
|
|
184
|
+
...[...b.writes].filter(key => a.reads.has(key) || a.reduces.has(key)),
|
|
185
|
+
])];
|
|
186
|
+
|
|
187
|
+
/** A system's leaves in its tiers, with their units and accesses, and its graph with readers of folds after their contributors. */
|
|
188
|
+
const orderedOf = (system: Definition) => {
|
|
189
|
+
const graph = graphOf(system);
|
|
190
|
+
const leaves = graph.units.flatMap((unit, index) => isLeaf(unit) && !isObserver(unit) ? [{ leaf: unit, unit: index }] : []);
|
|
191
|
+
const accesses = leaves.map(({ leaf }) => accessOf(leaf));
|
|
192
|
+
// Leaves that read a resource run after every leaf that reduces into it, so they read the finished fold.
|
|
193
|
+
const reductions = new Map<Key, number>();
|
|
194
|
+
accesses.forEach((access, contributor) => {
|
|
195
|
+
for (const key of access.reduces) {
|
|
196
|
+
reductions.set(key, (reductions.get(key) ?? 0) + 1);
|
|
197
|
+
accesses.forEach((other, reader) => {
|
|
198
|
+
if (reader !== contributor && other.reads.has(key)) graph.successors[end(leaves[contributor].unit)].add(start(leaves[reader].unit));
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
return { graph, leaves, accesses, reductions };
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* What put a leaf after another: a constraint of either, or of a composite holding it, naming the other or a composite
|
|
207
|
+
* holding it (`after`, `before`), its reading a fold the other contributes to (`reduction`), or being placed during a
|
|
208
|
+
* span (`during`). `conflicts` are the keys both access, one writing, which need the order.
|
|
209
|
+
*/
|
|
210
|
+
export type Placement = { readonly leaf: Leaf; readonly because: "after" | "before" | "during" | "reduction"; readonly conflicts: readonly Key[] };
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* For each leaf in a system's tiers, the leaves it follows directly: those with an ordering path to it through no other
|
|
214
|
+
* leaf of its tiers.
|
|
215
|
+
*/
|
|
216
|
+
export const placementsOf = (system: Definition): ReadonlyMap<Leaf, readonly Placement[]> => {
|
|
217
|
+
const { graph, leaves, accesses } = orderedOf(system);
|
|
218
|
+
const reach = reachability(topologicalOrder(graph), graph.successors);
|
|
219
|
+
const ancestors = graph.units.map(() => [] as number[]);
|
|
220
|
+
graph.members.forEach((members, unit) => {
|
|
221
|
+
const visit = (member: number): void => {
|
|
222
|
+
ancestors[member].push(unit);
|
|
223
|
+
for (const inner of graph.members[member]) visit(inner);
|
|
224
|
+
};
|
|
225
|
+
for (const member of members) visit(member);
|
|
226
|
+
});
|
|
227
|
+
/** Whether `systems` names `unit` or a composite holding it. */
|
|
228
|
+
const names = (systems: readonly object[] | undefined, unit: number): boolean => {
|
|
229
|
+
const named = graph.resolve(systems);
|
|
230
|
+
return named.includes(unit) || ancestors[unit].some(holder => named.includes(holder));
|
|
231
|
+
};
|
|
232
|
+
const constrains = (unit: number, list: "after" | "before", other: number): boolean =>
|
|
233
|
+
[unit, ...ancestors[unit]].some(holder => holder !== 0 && names(graph.units[holder].declaration[list], other));
|
|
234
|
+
const placements = new Map<Leaf, readonly Placement[]>();
|
|
235
|
+
leaves.forEach(({ leaf, unit }, b) => {
|
|
236
|
+
const before = leaves.flatMap((other, a) => a !== b && reaches(reach, end(other.unit), start(unit)) ? [a] : []);
|
|
237
|
+
const direct = before.filter(a => !before.some(other => other !== a && reaches(reach, end(leaves[a].unit), start(leaves[other].unit))));
|
|
238
|
+
placements.set(leaf, direct.map(a => {
|
|
239
|
+
const other = leaves[a];
|
|
240
|
+
const because = constrains(unit, "after", other.unit) ? "after"
|
|
241
|
+
: constrains(other.unit, "before", unit) ? "before"
|
|
242
|
+
: [...accesses[a].reduces].some(key => accesses[b].reads.has(key)) ? "reduction" : "during";
|
|
243
|
+
return { leaf: other.leaf, because, conflicts: conflictsOf(accesses[a], accesses[b]) };
|
|
244
|
+
}));
|
|
245
|
+
});
|
|
246
|
+
return placements;
|
|
247
|
+
};
|
|
248
|
+
|
|
168
249
|
const schedules = new WeakMap<Definition, Schedule>();
|
|
169
250
|
|
|
170
251
|
/** A system's schedule, created once per system. Throws on a cycle, or on two conflicting leaves with no ordering path between them. */
|
|
@@ -179,33 +260,125 @@ export const scheduleOf = (system: Definition): Schedule => {
|
|
|
179
260
|
* or on inputs that don't merge.
|
|
180
261
|
*/
|
|
181
262
|
const createSchedule = (system: Definition): Schedule => {
|
|
182
|
-
const graph =
|
|
263
|
+
const { graph, leaves, accesses, reductions } = orderedOf(system);
|
|
183
264
|
const observer = graph.units.filter(isObserver);
|
|
184
|
-
const leaves = graph.units.flatMap((unit, index) => isLeaf(unit) && !isObserver(unit) ? [{ leaf: unit, unit: index }] : []);
|
|
185
265
|
checkInputs(leaves.map(({ leaf }) => leaf));
|
|
186
|
-
const accesses = leaves.map(({ leaf }) => accessOf(leaf));
|
|
187
|
-
// Leaves that read a resource run after every leaf that reduces into it, so they read the finished fold.
|
|
188
|
-
const reductions = new Map<Key, number>();
|
|
189
|
-
accesses.forEach((access, contributor) => {
|
|
190
|
-
for (const key of access.reduces) {
|
|
191
|
-
reductions.set(key, (reductions.get(key) ?? 0) + 1);
|
|
192
|
-
accesses.forEach((other, reader) => {
|
|
193
|
-
if (reader !== contributor && other.reads.has(key)) graph.successors[end(leaves[contributor].unit)].add(start(leaves[reader].unit));
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
266
|
const order = topologicalOrder(graph);
|
|
198
267
|
const reach = reachability(order, graph.successors);
|
|
199
268
|
for (let a = 0; a < leaves.length; a++) {
|
|
200
269
|
for (let b = a + 1; b < leaves.length; b++) {
|
|
201
270
|
const key = conflictOf(accesses[a], accesses[b]);
|
|
202
271
|
if (key !== undefined && !ordered(reach, leaves[a].unit, leaves[b].unit)) {
|
|
203
|
-
|
|
272
|
+
const whole = [leaves[a].leaf, leaves[b].leaf].filter(leaf => leaf.declaration.components?.whole === true)
|
|
273
|
+
.map(leaf => ` "${leaf.name}" takes its row whole (components.whole), so it writes every member.`).join("");
|
|
274
|
+
throw new Error(`Systems "${leaves[a].leaf.name}" and "${leaves[b].leaf.name}" both access "${labelOf(key)}" and one writes it; order them with before or after.${whole}`);
|
|
204
275
|
}
|
|
205
276
|
}
|
|
206
277
|
}
|
|
207
278
|
const depth = depthsOf(graph, order);
|
|
208
|
-
const
|
|
209
|
-
for (const { leaf, unit } of leaves) (
|
|
210
|
-
|
|
279
|
+
const byDepth: Leaf[][] = [];
|
|
280
|
+
for (const { leaf, unit } of leaves) (byDepth[depth[start(unit)]] ??= []).push(leaf);
|
|
281
|
+
const tiers = byDepth.filter(tier => tier !== undefined);
|
|
282
|
+
// A sync point comes between tiers, so a schedule of one tier has none.
|
|
283
|
+
if (tiers.length < 2) return { tiers, reductions, observer, syncs: tiers.map(() => null) };
|
|
284
|
+
const tierOf = new Map<Leaf, number>();
|
|
285
|
+
tiers.forEach((tier, t) => { for (const leaf of tier) tierOf.set(leaf, t); });
|
|
286
|
+
const edges: { readonly lo: number; readonly hi: number; readonly reason: SyncReason }[] = [];
|
|
287
|
+
leaves.forEach((from, a) => {
|
|
288
|
+
const changed = structuralOf(from.leaf);
|
|
289
|
+
if (changed === null) return;
|
|
290
|
+
leaves.forEach((to, b) => {
|
|
291
|
+
if (a === b || !reaches(reach, end(from.unit), start(to.unit))) return;
|
|
292
|
+
const keys = affected(changed, touchesOf(to.leaf));
|
|
293
|
+
if (keys !== null) edges.push({ lo: tierOf.get(from.leaf) ?? 0, hi: (tierOf.get(to.leaf) ?? 0) - 1, reason: { from: from.leaf, to: to.leaf, keys } });
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
return { tiers, reductions, observer, syncs: syncsOf(tiers.length, edges) };
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Why a sync point follows a tier: a system in it or before it whose commands may change, structurally, what a system
|
|
301
|
+
* after the sync point that follows it reads, matches, looks up or indexes. `keys` are the components its commands name
|
|
302
|
+
* that the later one uses, none where it despawns, or where its commands or a cascade could change anything.
|
|
303
|
+
*/
|
|
304
|
+
export type SyncReason = { readonly from: Leaf; readonly to: Leaf; readonly keys: readonly Key[] };
|
|
305
|
+
|
|
306
|
+
/** The components a leaf's commands may add or remove, by spawn, add or remove; `"any"` once it despawns or isn't followed. */
|
|
307
|
+
const structuralOf = (leaf: Leaf): ReadonlySet<Key> | "any" | null => {
|
|
308
|
+
const { commands } = leaf;
|
|
309
|
+
if (commands === undefined) return null;
|
|
310
|
+
if (commands === null || commands.despawn === true) return "any";
|
|
311
|
+
const keys = new Set([...commands.spawn ?? [], ...commands.add ?? [], ...commands.remove ?? []].map(firstKeyOf));
|
|
312
|
+
// A reference written may despawn its holder, or remove itself, at the sync point.
|
|
313
|
+
for (const key of keys) if (declarationOf(key)?.reference !== undefined) return "any";
|
|
314
|
+
return keys.size === 0 ? null : keys;
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* The components a leaf's rows, lookups, indexes and order name, which commands could change its view of; `any` when
|
|
319
|
+
* one of them matches every entity, `cascaded` when it names a component a rule derives or `within` removes.
|
|
320
|
+
*/
|
|
321
|
+
const touchesOf = (leaf: Leaf): { readonly keys: ReadonlySet<Key>; readonly any: boolean; readonly cascaded: boolean } => {
|
|
322
|
+
const { components, lookup, up, order } = leaf.declaration;
|
|
323
|
+
const keys = new Set<Key>();
|
|
324
|
+
let any = false;
|
|
325
|
+
const add = (list: readonly Key[] | undefined): void => { for (const key of list ?? []) keys.add(firstKeyOf(key)); };
|
|
326
|
+
/** Names what a query requires; one requiring nothing matches every entity. */
|
|
327
|
+
const requires = (required: readonly (readonly Key[] | undefined)[], others: readonly (readonly Key[] | undefined)[]): void => {
|
|
328
|
+
if (required.every(list => list === undefined || list.length === 0)) any = true;
|
|
329
|
+
for (const list of [...required, ...others]) add(list);
|
|
330
|
+
};
|
|
331
|
+
const indexed = (index: unknown): void => {
|
|
332
|
+
const declaration = indexDefinitionOf(index)?.declaration;
|
|
333
|
+
if (declaration !== undefined) requires([declaration.key, declaration.components?.has], [declaration.components?.not]);
|
|
334
|
+
};
|
|
335
|
+
if (components !== undefined) {
|
|
336
|
+
requires([components.read, components.write, components.has, components.added, components.changed], [components.optional, components.not]);
|
|
337
|
+
add(Object.getOwnPropertySymbols(components.where ?? {}));
|
|
338
|
+
}
|
|
339
|
+
for (const name in lookup) {
|
|
340
|
+
const used = lookup[name];
|
|
341
|
+
if ("index" in used && used.index !== undefined) {
|
|
342
|
+
indexed(used.index);
|
|
343
|
+
add(used.read);
|
|
344
|
+
}
|
|
345
|
+
else if (!("index" in used)) requires([used.read, used.has], [used.optional, used.not, Array.isArray(used.changed) ? used.changed : undefined]);
|
|
346
|
+
}
|
|
347
|
+
if (order !== undefined) indexed(order);
|
|
348
|
+
if (up !== undefined) add([up.through, ...up.read ?? []]);
|
|
349
|
+
const cascaded = [...keys].some(key => { const declared = declarationOf(key); return declared?.derived === true || declared?.within !== undefined; });
|
|
350
|
+
return { keys, any, cascaded };
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
/** The keys through which `changed` may change what `touches` names; null where it can't. */
|
|
354
|
+
const affected = (changed: ReadonlySet<Key> | "any", touches: ReturnType<typeof touchesOf>): readonly Key[] | null => {
|
|
355
|
+
if (touches.keys.size === 0 && !touches.any) return null;
|
|
356
|
+
if (changed === "any") return [];
|
|
357
|
+
if (touches.any || touches.cascaded) return [...changed];
|
|
358
|
+
const keys = [...changed].filter(key => touches.keys.has(key));
|
|
359
|
+
return keys.length === 0 ? null : keys;
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* The fewest sync points that put one between the tiers of each edge, after a tier from `lo` to `hi`: each placed as
|
|
364
|
+
* late as the first edge it must cover allows, so it covers every edge open then. An edge's reason goes to the first
|
|
365
|
+
* sync point within its tiers.
|
|
366
|
+
*/
|
|
367
|
+
const syncsOf = (count: number, edges: readonly { readonly lo: number; readonly hi: number; readonly reason: SyncReason }[]): readonly (readonly SyncReason[] | null)[] => {
|
|
368
|
+
const syncs: (SyncReason[] | null)[] = new Array<SyncReason[] | null>(count).fill(null);
|
|
369
|
+
let last = -1;
|
|
370
|
+
for (const { lo, hi } of [...edges].sort((a, b) => a.hi - b.hi)) {
|
|
371
|
+
if (last >= lo) continue;
|
|
372
|
+
last = hi;
|
|
373
|
+
syncs[hi] = [];
|
|
374
|
+
}
|
|
375
|
+
for (const { lo, hi, reason } of edges) {
|
|
376
|
+
for (let t = lo; t <= hi; t++) {
|
|
377
|
+
const reasons = syncs[t];
|
|
378
|
+
if (reasons === null) continue;
|
|
379
|
+
reasons.push(reason);
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return syncs;
|
|
211
384
|
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import type { Declared } from '../component/declared.js';
|
|
3
|
+
import { declaredOf } from '../registry/names.js';
|
|
4
|
+
import type { Key } from './key.js';
|
|
5
|
+
|
|
6
|
+
/** Every resource its modules registered so far, with its declaration. */
|
|
7
|
+
export const declarations = (): ReadonlyMap<Key, Declared> => {
|
|
8
|
+
// Registration takes only keys declared in `Resources`, so each symbol registered as a resource is one of its keys.
|
|
9
|
+
const found = declaredOf("resource") as ReadonlyMap<Key, Declared>;
|
|
10
|
+
return found;
|
|
11
|
+
};
|