@adobe/ecs 0.2.3 → 0.3.0
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/dist/implementation/compiler/augmentations.js +8 -1
- package/dist/implementation/compiler/augmentations.js.map +1 -1
- 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/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 +5 -2
- package/dist/implementation/compiler/inferred.js.map +1 -1
- package/dist/implementation/compiler/transform.js +5 -4
- package/dist/implementation/compiler/transform.js.map +1 -1
- package/dist/implementation/compiler/type-schemas.d.ts +1 -0
- package/dist/implementation/compiler/type-schemas.js +35 -2
- package/dist/implementation/compiler/type-schemas.js.map +1 -1
- package/dist/implementation/world/activate.d.ts +14 -3
- package/dist/implementation/world/activate.js +28 -10
- 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 +41 -5
- package/dist/implementation/world/commands.js.map +1 -1
- package/dist/implementation/world/create.js +57 -6
- package/dist/implementation/world/create.js.map +1 -1
- package/dist/implementation/world/execute.d.ts +4 -4
- package/dist/implementation/world/execute.js +228 -93
- package/dist/implementation/world/execute.js.map +1 -1
- package/dist/implementation/world/finalizers.d.ts +10 -0
- package/dist/implementation/world/finalizers.js +60 -10
- package/dist/implementation/world/finalizers.js.map +1 -1
- package/dist/implementation/world/join.js +12 -2
- package/dist/implementation/world/join.js.map +1 -1
- package/dist/implementation/world/program.js +5 -1
- package/dist/implementation/world/program.js.map +1 -1
- 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.js +21 -16
- 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 +16 -12
- package/dist/implementation/world/runner.js.map +1 -1
- package/dist/implementation/world/state.d.ts +15 -0
- package/dist/implementation/world/state.js +22 -1
- package/dist/implementation/world/state.js.map +1 -1
- package/dist/implementation/world/synchronize.js +81 -24
- package/dist/implementation/world/synchronize.js.map +1 -1
- package/dist/implementation/world/tables-read.d.ts +5 -3
- package/dist/implementation/world/tables-read.js +16 -4
- package/dist/implementation/world/tables-read.js.map +1 -1
- package/dist/implementation/world/tables.d.ts +4 -2
- package/dist/implementation/world/tables.js +137 -38
- package/dist/implementation/world/tables.js.map +1 -1
- 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 +10 -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/interface/finalizer/body.d.ts +40 -4
- package/dist/interface/finalizer/create.d.ts +8 -6
- package/dist/interface/finalizer/create.js +45 -10
- package/dist/interface/finalizer/create.js.map +1 -1
- package/dist/interface/finalizer/declaration.d.ts +26 -4
- 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/index.d.ts +1 -1
- package/dist/interface/registry/index.js +1 -1
- package/dist/interface/registry/index.js.map +1 -1
- package/dist/interface/registry/names.d.ts +4 -0
- package/dist/interface/registry/names.js +20 -0
- package/dist/interface/registry/names.js.map +1 -1
- package/dist/interface/registry/schedule.d.ts +29 -1
- package/dist/interface/registry/schedule.js +261 -25
- package/dist/interface/registry/schedule.js.map +1 -1
- package/dist/interface/system/access.d.ts +15 -0
- package/dist/interface/system/check.d.ts +7 -0
- package/dist/interface/system/check.js +21 -3
- package/dist/interface/system/check.js.map +1 -1
- package/dist/interface/system/commands.d.ts +31 -5
- package/dist/interface/system/create-resolved.js +2 -1
- package/dist/interface/system/create-resolved.js.map +1 -1
- package/dist/interface/system/create.d.ts +10 -6
- package/dist/interface/system/create.js +5 -4
- package/dist/interface/system/create.js.map +1 -1
- package/dist/interface/system/declaration.d.ts +11 -4
- 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 +7 -0
- package/dist/interface/system/render.js +2 -1
- package/dist/interface/system/render.js.map +1 -1
- package/dist/interface/system/resolve.d.ts +14 -1
- package/dist/interface/system/resolve.js +50 -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 +9 -1
- package/dist/interface/world/table/index.d.ts +3 -2
- package/dist/interface/world/world.d.ts +15 -0
- package/package.json +11 -2
- package/src/guide/01-resources.md +4 -3
- package/src/guide/03-systems.out +1 -1
- package/src/guide/05-scheduling.md +12 -6
- package/src/guide/05-scheduling.out +1 -0
- package/src/guide/05-scheduling.ts +17 -0
- package/src/guide/06-sync-points.md +20 -8
- package/src/guide/06-sync-points.out +3 -2
- package/src/guide/06-sync-points.ts +24 -5
- package/src/guide/10-finalizers.md +5 -0
- package/src/guide/18-identity.out +1 -1
- package/src/guide/guide.test.ts +6 -153
- package/src/implementation/compiler/augmentations.test.ts +28 -1
- package/src/implementation/compiler/augmentations.ts +5 -1
- package/src/implementation/compiler/body-access.ts +102 -12
- package/src/implementation/compiler/estree.ts +9 -6
- package/src/implementation/compiler/inferred.test.ts +1 -1
- package/src/implementation/compiler/inferred.ts +5 -2
- package/src/implementation/compiler/transform.test.ts +41 -21
- package/src/implementation/compiler/transform.ts +5 -3
- package/src/implementation/compiler/type-schemas.ts +28 -2
- package/src/implementation/world/activate.ts +32 -10
- 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/commands.ts +37 -5
- package/src/implementation/world/create.test.ts +8 -8
- package/src/implementation/world/create.ts +61 -6
- package/src/implementation/world/defaults.test.ts +24 -1
- package/src/implementation/world/despawn-where.test.ts +310 -0
- package/src/implementation/world/execute.ts +222 -86
- package/src/implementation/world/finalizers.ts +63 -11
- package/src/implementation/world/ids.test.ts +1 -1
- package/src/implementation/world/indexes.test.ts +2 -1
- package/src/implementation/world/join.test.ts +19 -0
- package/src/implementation/world/join.ts +16 -2
- package/src/implementation/world/lifetime.test-helper.ts +1 -1
- package/src/implementation/world/names.test.ts +1 -2
- package/src/implementation/world/passes.test.ts +61 -0
- package/src/implementation/world/perf.md +12 -1
- package/src/implementation/world/program.ts +4 -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 +21 -14
- 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 +17 -0
- package/src/implementation/world/runner.ts +17 -13
- package/src/implementation/world/schedule.test.ts +42 -1
- package/src/implementation/world/shared.test.ts +2 -2
- package/src/implementation/world/state.ts +27 -0
- package/src/implementation/world/sync-points.test.ts +147 -0
- package/src/implementation/world/synchronize.ts +73 -20
- package/src/implementation/world/tables-read.ts +16 -5
- package/src/implementation/world/tables.test.ts +139 -4
- package/src/implementation/world/tables.ts +118 -35
- package/src/implementation/world/thread-observer.ts +1 -1
- package/src/implementation/world/thread-systems.ts +10 -6
- package/src/implementation/world/up.test.ts +13 -1
- package/src/implementation/world/up.ts +6 -1
- package/src/implementation/world/watch-traits-late.test-helper.ts +14 -0
- package/src/implementation/world/watch-traits.test-helper.ts +10 -0
- package/src/implementation/world/watch-traits.test.ts +62 -0
- package/src/implementation/world/where.ts +38 -10
- package/src/interface/finalizer/body.ts +33 -4
- package/src/interface/finalizer/create.test.ts +6 -1
- package/src/interface/finalizer/create.ts +45 -12
- package/src/interface/finalizer/declaration.ts +31 -4
- 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/registry/index.ts +1 -1
- package/src/interface/registry/names.ts +19 -0
- package/src/interface/registry/schedule.ts +275 -29
- package/src/interface/rule/rule.type-test.ts +23 -1
- package/src/interface/system/access.ts +16 -0
- package/src/interface/system/check.ts +17 -2
- package/src/interface/system/commands.ts +28 -4
- package/src/interface/system/commands.type-test.ts +31 -0
- package/src/interface/system/create-resolved.ts +2 -1
- package/src/interface/system/create.test.ts +10 -2
- package/src/interface/system/create.ts +9 -5
- package/src/interface/system/declaration.ts +10 -4
- package/src/interface/system/describe.test.ts +98 -0
- package/src/interface/system/describe.ts +122 -0
- package/src/interface/system/index.ts +1 -0
- package/src/interface/system/leaf.ts +7 -0
- package/src/interface/system/render.ts +2 -1
- package/src/interface/system/resolve.test.ts +17 -1
- package/src/interface/system/resolve.ts +60 -2
- package/src/interface/system/shared-only.ts +5 -0
- package/src/interface/system/system.type-test.ts +6 -0
- package/src/interface/system/tables.ts +7 -1
- package/src/interface/world/run.type-test.ts +11 -1
- package/src/interface/world/table/index.ts +3 -2
- package/src/interface/world/world.ts +13 -0
- package/src/perftest/benchmarks.ts +20 -0
- package/src/perftest/chained.ts +108 -0
- package/src/perftest/structural.ts +20 -0
- package/src/perftest/tables.ts +107 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
2
|
import type { Archetype } from './archetype.js';
|
|
3
3
|
import { readCell } from './cells.js';
|
|
4
|
-
import { indexOf, locate } from './entities.js';
|
|
4
|
+
import { generationOf, indexOf, locate } from './entities.js';
|
|
5
5
|
import { commandValue, isMissing } from './finalizers.js';
|
|
6
6
|
import { createHolders, fitHolders, forEachHolder, linkHolder, unlinkHolder } from './holders.js';
|
|
7
7
|
import { closureOf, owner } from './structure.js';
|
|
@@ -9,8 +9,8 @@ import { archetypeOf, withoutComponent, type State } from './state.js';
|
|
|
9
9
|
import type { Queued } from './synchronize.js';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* A reference value written in this sync point: by spawn or add commands, or a
|
|
13
|
-
* and `target` triples, checked once the plan's despawns are known.
|
|
12
|
+
* A reference value written in this sync point to check: by spawn or add commands, or of a row spawned that `linkBorn`
|
|
13
|
+
* leaves to the cascade. `holder`, `component` and `target` triples, checked once the plan's despawns are known.
|
|
14
14
|
*/
|
|
15
15
|
export type Written = number[];
|
|
16
16
|
|
|
@@ -41,6 +41,48 @@ const aliveAfter = (state: State, target: number): boolean => {
|
|
|
41
41
|
return locate(entities, target) >= 0;
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Links each reference the rows spawned in this sync point hold to its target, before the cascade, so a target it
|
|
46
|
+
* despawns finds them as it finds any holder. One naming an entity that won't be alive isn't linked, and goes to
|
|
47
|
+
* `written` to cascade, as does every reference of a row a command touched, which the command may point elsewhere.
|
|
48
|
+
* The rows spawned into an archetype are those from `first`, by archetype id; an entity is one f64.
|
|
49
|
+
*/
|
|
50
|
+
export const linkBorn = (state: State, born: ReadonlyMap<number, number>, written: Written, tick: number): void => {
|
|
51
|
+
const { plan, structure, archetypes, entities, referencesWritten } = state;
|
|
52
|
+
const { slots } = plan;
|
|
53
|
+
for (const [id, first] of born) {
|
|
54
|
+
const archetype = archetypes[id];
|
|
55
|
+
const { count } = archetype;
|
|
56
|
+
if (first >= count) continue;
|
|
57
|
+
const rows = archetype.entities;
|
|
58
|
+
for (const component of structure.referenceIds) {
|
|
59
|
+
const storage = archetype.storageById[component];
|
|
60
|
+
if (storage === undefined) continue;
|
|
61
|
+
referencesWritten[component] = tick;
|
|
62
|
+
const lists = structure.holders[component] ??= createHolders();
|
|
63
|
+
fitHolders(lists, entities.length);
|
|
64
|
+
const { unitStride, unitOffset } = storage;
|
|
65
|
+
const values = storage.views.f64;
|
|
66
|
+
// `aliveAfter`, through locals, since it runs for each row spawned.
|
|
67
|
+
const { archetype: archetypeOfIndex, generation, length } = entities;
|
|
68
|
+
const { entity: planned, despawned } = plan;
|
|
69
|
+
for (let row = first; row < count; row++) {
|
|
70
|
+
const target = values[row * unitStride + unitOffset];
|
|
71
|
+
if (target === 0) continue;
|
|
72
|
+
const holder = rows[row];
|
|
73
|
+
const index = indexOf(holder);
|
|
74
|
+
const commanded = index < slots.length && slots[index] >= 0;
|
|
75
|
+
const into = indexOf(target);
|
|
76
|
+
const entry = into < slots.length ? slots[into] : -1;
|
|
77
|
+
const alive = entry >= 0 && planned[entry] === target ? despawned[entry] === 0
|
|
78
|
+
: into < length && archetypeOfIndex[into] >= 0 && generation[into] === generationOf(target);
|
|
79
|
+
if (alive) linkHolder(lists, target, holder);
|
|
80
|
+
if (commanded || !alive) written.push(holder, component, target);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
44
86
|
/**
|
|
45
87
|
* Despawns the holders that own a reference to an entity that is despawned or never lives, and removes `weak`
|
|
46
88
|
* references to one, transitively, before rules close the plan. A holder acts only if its value once the plan applies
|
|
@@ -91,7 +133,7 @@ export const cascadeReferences = (state: State, queued: readonly Queued[], writt
|
|
|
91
133
|
*/
|
|
92
134
|
export type IndexChanges = { readonly remove: number[]; readonly add: number[] };
|
|
93
135
|
|
|
94
|
-
export const indexChangesOf = (state: State, queued: readonly Queued[], fromOf: (entry: number) => Archetype | null
|
|
136
|
+
export const indexChangesOf = (state: State, queued: readonly Queued[], fromOf: (entry: number) => Archetype | null): IndexChanges => {
|
|
95
137
|
const { plan, structure, entities } = state;
|
|
96
138
|
const changes: IndexChanges = { remove: [], add: [] };
|
|
97
139
|
for (let entry = 0; entry < plan.count; entry++) {
|
|
@@ -106,24 +148,6 @@ export const indexChangesOf = (state: State, queued: readonly Queued[], fromOf:
|
|
|
106
148
|
if (typeof after === "number" && after !== 0) changes.add.push(after, holder, component);
|
|
107
149
|
}
|
|
108
150
|
}
|
|
109
|
-
// Batch spawns no command touched still hold the references they were spawned with. An entity is one f64.
|
|
110
|
-
const { slots } = plan;
|
|
111
|
-
for (const [archetype, first] of bornRows) {
|
|
112
|
-
for (const component of structure.referenceIds) {
|
|
113
|
-
const storage = archetype.storageById[component];
|
|
114
|
-
if (storage === undefined) continue;
|
|
115
|
-
const { unitStride, unitOffset } = storage;
|
|
116
|
-
const values = storage.views.f64;
|
|
117
|
-
const rows = archetype.entities;
|
|
118
|
-
for (let row = first; row < archetype.count; row++) {
|
|
119
|
-
const holder = rows[row];
|
|
120
|
-
const index = indexOf(holder);
|
|
121
|
-
if (index < slots.length && slots[index] >= 0) continue;
|
|
122
|
-
const value = values[row * unitStride + unitOffset];
|
|
123
|
-
if (value !== 0) changes.add.push(value, holder, component);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
151
|
return changes;
|
|
128
152
|
};
|
|
129
153
|
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
2
|
import type { Commands } from '../../interface/system/commands.js';
|
|
3
|
-
import { addCommand, createBuffer, despawnCommand, pushCommand, pushComponent, removeCommand, spawnCommand, type CommandBuffer } from './buffer.js';
|
|
3
|
+
import { addCommand, createBuffer, despawnCommand, despawnWhereCommand, despawnWhereHolds, pushCommand, pushComponent, removeCommand, spawnCommand, type CommandBuffer } from './buffer.js';
|
|
4
|
+
import type { Archetype } from './archetype.js';
|
|
4
5
|
import { reserve } from './entities.js';
|
|
5
6
|
import { listShapeOf, presetSpawnerOf, spawnerOf, type ListShape } from './lists.js';
|
|
6
|
-
import { commandIdOf, idOf, storedShared, type Key, type State } from './state.js';
|
|
7
|
+
import { commandIdOf, idOf, queryIdOf, storedShared, type Key, type State } from './state.js';
|
|
7
8
|
import { spawnTables } from './tables.js';
|
|
9
|
+
import { holdsValuesOf } from './where.js';
|
|
10
|
+
import { checkWhereValues } from '../../interface/system/check.js';
|
|
8
11
|
import { declarationOf, isWorldWritten, kindOf, labelOf, threadOfDeclaration, whyUnregistered } from '../../interface/registry/index.js';
|
|
9
12
|
import { matchesSchema } from '../../interface/registry/schemas.js';
|
|
10
13
|
|
|
@@ -83,6 +86,32 @@ const queueResources = (state: State, resources: object): void => {
|
|
|
83
86
|
/** Values of a list add given none, which only an untyped caller does. */
|
|
84
87
|
const empty = {};
|
|
85
88
|
|
|
89
|
+
const noKeys: readonly unknown[] = [];
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Queues a `despawnWhere`, its ids resolved and its `where` checked first, so a key that isn't a component, or a `where`
|
|
93
|
+
* that isn't of shared values, queues nothing.
|
|
94
|
+
*/
|
|
95
|
+
const pushDespawnWhere = (state: State, buffer: CommandBuffer, query: Parameters<Commands["despawnWhere"]>[0]): void => {
|
|
96
|
+
const including: readonly unknown[] = query.with;
|
|
97
|
+
const excluding: readonly unknown[] = query.not ?? noKeys;
|
|
98
|
+
const { where } = query;
|
|
99
|
+
if (including.length === 0) throw new Error("commands.despawnWhere's `with` names at least one component.");
|
|
100
|
+
for (let k = 0; k < including.length; k++) queryIdOf(state, including[k]);
|
|
101
|
+
for (let k = 0; k < excluding.length; k++) queryIdOf(state, excluding[k]);
|
|
102
|
+
let holds: ((archetype: Archetype) => boolean) | null = null;
|
|
103
|
+
if (where !== undefined && Object.getOwnPropertySymbols(where).length > 0) {
|
|
104
|
+
checkWhereValues("commands.despawnWhere", "where", "a despawn", where, false);
|
|
105
|
+
const found = holdsValuesOf(state, where, false);
|
|
106
|
+
if (typeof found === "symbol") throw new Error(`commands.despawnWhere: where names "${labelOf(found)}", which isn't a shared component.`);
|
|
107
|
+
holds = found;
|
|
108
|
+
}
|
|
109
|
+
pushCommand(buffer, despawnWhereCommand, including.length);
|
|
110
|
+
for (let k = 0; k < including.length; k++) pushComponent(buffer, queryIdOf(state, including[k]), undefined);
|
|
111
|
+
for (let k = 0; k < excluding.length; k++) pushComponent(buffer, queryIdOf(state, excluding[k]), undefined);
|
|
112
|
+
if (holds !== null) pushComponent(buffer, despawnWhereHolds, holds);
|
|
113
|
+
};
|
|
114
|
+
|
|
86
115
|
/**
|
|
87
116
|
* Commands that queue for the next sync point. A key that isn't a component commands may name throws here, inside the
|
|
88
117
|
* issuing system, before anything is queued. Values are immutable by convention, so they aren't copied.
|
|
@@ -97,6 +126,7 @@ export const createCommands = (state: State, target: { readonly buffer: CommandB
|
|
|
97
126
|
despawn: entity => {
|
|
98
127
|
pushCommand(target.buffer, despawnCommand, entity);
|
|
99
128
|
},
|
|
129
|
+
despawnWhere: query => pushDespawnWhere(state, target.buffer, query),
|
|
100
130
|
add: (entity: number, components: symbol | readonly symbol[], values?: unknown) => {
|
|
101
131
|
const { buffer } = target;
|
|
102
132
|
if (typeof components === "symbol") {
|
|
@@ -119,10 +149,12 @@ export const createCommands = (state: State, target: { readonly buffer: CommandB
|
|
|
119
149
|
pushComponent(buffer, ids[k], value === undefined ? starts[k] : value);
|
|
120
150
|
}
|
|
121
151
|
},
|
|
122
|
-
spawnTables: (tables,
|
|
123
|
-
|
|
152
|
+
spawnTables: (tables, options) => {
|
|
153
|
+
const resources = options?.resources;
|
|
154
|
+
const updates = options?.update === true ? target.buffer : null;
|
|
155
|
+
if (resources === undefined) return spawnTables(state, tables, options?.existing, updates);
|
|
124
156
|
checkResources(resources);
|
|
125
|
-
const spawned = spawnTables(state, tables);
|
|
157
|
+
const spawned = spawnTables(state, tables, options?.existing, updates);
|
|
126
158
|
queueResources(state, resources);
|
|
127
159
|
return spawned;
|
|
128
160
|
},
|
|
@@ -71,28 +71,28 @@ const positions = async (world: World): Promise<Vec2[]> =>
|
|
|
71
71
|
(await readEach(world, await select(world, { required: [position], order: { [position]: true } }), position)).map(value => value ?? [NaN, NaN]);
|
|
72
72
|
|
|
73
73
|
describe("World.create", () => {
|
|
74
|
-
it("spawns once from a changed resource, then integrates", async () => {
|
|
74
|
+
it("spawns once from a changed resource, then integrates, from the run that spawns", async () => {
|
|
75
75
|
const world = createWorld();
|
|
76
76
|
await world.run(main);
|
|
77
|
-
expect(await positions(world)).toEqual([[0, 0], [1, 0], [2, 0]]);
|
|
78
|
-
await world.run(main);
|
|
79
77
|
expect(await positions(world)).toEqual([[0, 1], [1, 1], [2, 1]]);
|
|
78
|
+
await world.run(main);
|
|
79
|
+
expect(await positions(world)).toEqual([[0, 2], [1, 2], [2, 2]]);
|
|
80
80
|
expect(await readResource(world, spawnCount)).toBe(3);
|
|
81
81
|
});
|
|
82
82
|
|
|
83
83
|
it("reports each change to a system exactly once", async () => {
|
|
84
84
|
const world = createWorld();
|
|
85
85
|
await world.run(main);
|
|
86
|
-
// Spawned at the sync point,
|
|
87
|
-
expect(await readResource(world, seen)).toBe(0);
|
|
88
|
-
await world.run(main);
|
|
86
|
+
// Spawned at the sync point before move, which watch follows: seen in the same run.
|
|
89
87
|
expect(await readResource(world, seen)).toBe(3);
|
|
88
|
+
await world.run(main);
|
|
89
|
+
expect(await readResource(world, seen)).toBe(6);
|
|
90
90
|
// Moved before watch in the last run, so already reported.
|
|
91
91
|
await world.run(watch);
|
|
92
|
-
expect(await readResource(world, seen)).toBe(
|
|
92
|
+
expect(await readResource(world, seen)).toBe(6);
|
|
93
93
|
await world.run(move);
|
|
94
94
|
await world.run(watch);
|
|
95
|
-
expect(await readResource(world, seen)).toBe(
|
|
95
|
+
expect(await readResource(world, seen)).toBe(9);
|
|
96
96
|
});
|
|
97
97
|
|
|
98
98
|
it("applies the net structural change of a sync point", async () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
2
|
import type { Finalizer } from '../../interface/finalizer/created.js';
|
|
3
3
|
import type { Rule } from '../../interface/rule/created.js';
|
|
4
|
-
import { requireDefinition, type Definition } from '../../interface/registry/index.js';
|
|
4
|
+
import { definitionOf, requireDefinition, type Definition } from '../../interface/registry/index.js';
|
|
5
5
|
import type { World } from '../../interface/world/world.js';
|
|
6
6
|
import type { Input } from '../../interface/system/input.js';
|
|
7
7
|
import type { System } from '../../interface/system/system.js';
|
|
@@ -33,8 +33,8 @@ import { refreshDirectories } from './directories.js';
|
|
|
33
33
|
import { createRegistrar, type Registration } from './register.js';
|
|
34
34
|
import type { Schedule } from '../../interface/registry/schedule.js';
|
|
35
35
|
import { ensureResources, findResource, initialOf, setResource } from './resources.js';
|
|
36
|
-
import { ensureProgramResources, activate, finalizerCrossing, placeFinalizers } from './activate.js';
|
|
37
|
-
import { isObserver, isWorldWritten, kindOf,
|
|
36
|
+
import { ensureProgramResources, activate, finalizerCrossing, placeFinalizers, programResourcesChanged, type Ensured } from './activate.js';
|
|
37
|
+
import { isObserver, isWorldWritten, kindOf, scheduleOf, type Composite } from '../../interface/registry/index.js';
|
|
38
38
|
import { always } from '../../interface/resources/always.js';
|
|
39
39
|
import { cadence, type Cadence } from '../../interface/resources/cadence.js';
|
|
40
40
|
import { clock } from '../../interface/resources/clock.js';
|
|
@@ -43,6 +43,10 @@ import { stampChanged } from './archetype.js';
|
|
|
43
43
|
import { createLoop, platformFrame, platformTimer, type Clock, type LoopPlatform } from './loop.js';
|
|
44
44
|
import { presetLevelsOf } from './lists.js';
|
|
45
45
|
import { createState, defaultOf, idOf, reside, startTicking, type Key } from './state.js';
|
|
46
|
+
import { asksForChanges, changesOf, resourcesWrittenBy, startRecording, takesInput } from './run-changes.js';
|
|
47
|
+
import type { ChangeSet } from '../../interface/finalizer/body.js';
|
|
48
|
+
import type { Essential } from '../../interface/component/value.js';
|
|
49
|
+
import type { Resource } from '../../interface/resource/resource.js';
|
|
46
50
|
import { indexOf } from './entities.js';
|
|
47
51
|
import { touchedHeld } from './up.js';
|
|
48
52
|
import { isHeld } from './transfers.js';
|
|
@@ -53,6 +57,9 @@ type Queue = <T>(task: () => T | Promise<T>) => Promise<T>;
|
|
|
53
57
|
|
|
54
58
|
/** A task's failure as the queue's own chain carries it, which never rejects: made only when a task fails. */
|
|
55
59
|
type Carried = { readonly thrown: unknown };
|
|
60
|
+
/** A schedule of nothing, whose run is its sync point alone. */
|
|
61
|
+
const noTiers: Schedule = { tiers: [], reductions: new Map(), observer: [], syncs: [] };
|
|
62
|
+
|
|
56
63
|
const thrownValues = new WeakSet<object>();
|
|
57
64
|
const thrownOf = (error: unknown): Carried => {
|
|
58
65
|
const wrapped = { thrown: error };
|
|
@@ -277,7 +284,7 @@ export const createWorld = (resources: InitialResources = {}, options: WorldOpti
|
|
|
277
284
|
if (state.observer.posts.length !== 0) await settlePosts(state, true);
|
|
278
285
|
};
|
|
279
286
|
let disposed = false;
|
|
280
|
-
const ensured = { version: -1 };
|
|
287
|
+
const ensured: Ensured = { version: -1, keys: -1, traits: false };
|
|
281
288
|
|
|
282
289
|
const minimumChunk = options.minimumChunk ?? defaultMinimumChunk;
|
|
283
290
|
|
|
@@ -538,7 +545,7 @@ export const createWorld = (resources: InitialResources = {}, options: WorldOpti
|
|
|
538
545
|
/** Follows what changed since the last run: program resources, change watches, workers and the device; null when nothing did. */
|
|
539
546
|
const prepareRun = (): Promise<void> | null => {
|
|
540
547
|
// Resources added between runs move the resource entity, whose structs a tail may still read.
|
|
541
|
-
if (ensured
|
|
548
|
+
if (programResourcesChanged(ensured) && (state.channels.length > 0 || state.observer.posts.length > 0)) return drainThenPrepare();
|
|
542
549
|
ensureProgramResources(state, ensured);
|
|
543
550
|
if (startWatches(state)) return followChanges(true, false);
|
|
544
551
|
if (workersChanged()) return followChanges(false, true);
|
|
@@ -956,9 +963,57 @@ export const createWorld = (resources: InitialResources = {}, options: WorldOpti
|
|
|
956
963
|
},
|
|
957
964
|
});
|
|
958
965
|
|
|
966
|
+
/**
|
|
967
|
+
* Runs a system or composite, recording its own changes, which it resolves to: what earlier finalizers queued
|
|
968
|
+
* applies first, at a sync point of its own, and the recording covers the run's sync points alone.
|
|
969
|
+
*/
|
|
970
|
+
const runChanging = (definition: Definition, value: unknown): Promise<unknown> => {
|
|
971
|
+
const started = enqueue(() => startChanges(definition));
|
|
972
|
+
const ran = runWith(definition, value);
|
|
973
|
+
const taken = enqueue(takeChanges);
|
|
974
|
+
return Promise.all([started, ran, taken]).then(([, , changes]) => changes);
|
|
975
|
+
};
|
|
976
|
+
const startChanges = (definition: Definition): unknown => {
|
|
977
|
+
checkLive();
|
|
978
|
+
const flushed = execution(definition, undefined, noTiers);
|
|
979
|
+
if (flushed instanceof Promise) return flushed.then(report => startRecordingOf(definition, report));
|
|
980
|
+
startRecordingOf(definition, flushed);
|
|
981
|
+
return undefined;
|
|
982
|
+
};
|
|
983
|
+
const startRecordingOf = (definition: Definition, flushed: RunReport): void => {
|
|
984
|
+
outOfBand.reportAll(flushed.failures);
|
|
985
|
+
state.recording = startRecording(state, resourcesWrittenBy(definition));
|
|
986
|
+
};
|
|
987
|
+
const takeChanges = (): unknown => {
|
|
988
|
+
const { recording } = state;
|
|
989
|
+
state.recording = null;
|
|
990
|
+
if (recording === null) throw new Error("The run's changes weren't recorded.");
|
|
991
|
+
return changesOf(state, recording);
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
function run<I, R>(system: System<I, R>, ...input: Input<NoInfer<I>>): Promise<R>;
|
|
995
|
+
function run<I, R>(system: System<I, R>, ...input: [...Input<NoInfer<I>>, options: { readonly changes: true }]): Promise<ChangeSet<Essential, Resource.Key>>;
|
|
996
|
+
function run(system: System<unknown, unknown>, ...args: readonly unknown[]): Promise<unknown> {
|
|
997
|
+
// Options come last, so a run not asking for changes reads one value more.
|
|
998
|
+
const { length } = args;
|
|
999
|
+
if (length === 0 || !asksForChanges(args[length - 1])) return runWith(system, args[0]);
|
|
1000
|
+
const definition = definitionOf(system);
|
|
1001
|
+
let input = false;
|
|
1002
|
+
try {
|
|
1003
|
+
input = definition !== undefined && takesInput(definition);
|
|
1004
|
+
}
|
|
1005
|
+
catch {
|
|
1006
|
+
// A composite whose schedule can't be made fails its run, as `runWith` finds.
|
|
1007
|
+
return runWith(system, args[0]);
|
|
1008
|
+
}
|
|
1009
|
+
// A system taking input whose input is all it was given runs with it.
|
|
1010
|
+
if (definition === undefined || (input && args.length < 2)) return runWith(system, args[0]);
|
|
1011
|
+
return runChanging(definition, input ? args[0] : undefined);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
959
1014
|
const world: CreatedWorld = {
|
|
960
1015
|
// A run gives what its system's body returned, `R` by the system's type.
|
|
961
|
-
run
|
|
1016
|
+
run,
|
|
962
1017
|
share: port => {
|
|
963
1018
|
if (disposed) return Promise.reject(new Error("The world is disposed."));
|
|
964
1019
|
if (!state.memory.shared) return Promise.reject(new Error("Without cross-origin isolation memory can't be shared, so no thread can join."));
|
|
@@ -4,7 +4,7 @@ import { Rule } from "../../interface/rule/rule.js";
|
|
|
4
4
|
import { System } from "../../interface/system/system.js";
|
|
5
5
|
import { createWorld } from "../index.js";
|
|
6
6
|
import { readResource } from "./reads.test-helper.js";
|
|
7
|
-
import { Entity, F32, I32, Mat4x4, Quat, U32, Vec2, Vec3, Vec4 } from "../../index.js";
|
|
7
|
+
import { Entity, F32, I32, Mat4x4, Quat, U32, Vec2, Vec3, Vec4, type Resource } from "../../index.js";
|
|
8
8
|
|
|
9
9
|
const pool = Symbol();
|
|
10
10
|
const names = Symbol();
|
|
@@ -14,6 +14,8 @@ const index = Symbol();
|
|
|
14
14
|
const turn = Symbol();
|
|
15
15
|
const least = Symbol();
|
|
16
16
|
const all = Symbol();
|
|
17
|
+
const written = Symbol();
|
|
18
|
+
const keys = Symbol();
|
|
17
19
|
|
|
18
20
|
/** A pool declared through an alias of ReadonlyMap. */
|
|
19
21
|
type Pool = ReadonlyMap<string, number>;
|
|
@@ -30,6 +32,8 @@ declare module "@adobe/ecs" {
|
|
|
30
32
|
[turn]: { type: Quat };
|
|
31
33
|
[least]: { type: F32; reduce: "min" };
|
|
32
34
|
[all]: { type: boolean; reduce: "&" };
|
|
35
|
+
[written]: { type: ReadonlyMap<Resource.Key, unknown> };
|
|
36
|
+
[keys]: { type: ReadonlySet<symbol> };
|
|
33
37
|
}
|
|
34
38
|
}
|
|
35
39
|
|
|
@@ -49,6 +53,25 @@ describe("a resource declared without a default", () => {
|
|
|
49
53
|
});
|
|
50
54
|
});
|
|
51
55
|
|
|
56
|
+
describe("a ReadonlyMap or ReadonlySet of what isn't data", () => {
|
|
57
|
+
it("starts at an empty one of each world's own, which a system replaces whole, keys and values kept as they are", async () => {
|
|
58
|
+
await using first = createWorld();
|
|
59
|
+
await using second = createWorld();
|
|
60
|
+
const [firstWritten, secondWritten] = [await readResource(first, written), await readResource(second, written)];
|
|
61
|
+
expect([firstWritten, secondWritten, await readResource(first, keys)]).toEqual([new Map(), new Map(), new Set()]);
|
|
62
|
+
expect(firstWritten).not.toBe(secondWritten);
|
|
63
|
+
const value = { at: 1 };
|
|
64
|
+
await first.run(System.create(({ resources }) => {
|
|
65
|
+
resources[written] = new Map([[count, value]]);
|
|
66
|
+
resources[keys] = new Set([count]);
|
|
67
|
+
}));
|
|
68
|
+
const read = await readResource(first, written);
|
|
69
|
+
expect([read.get(count), await readResource(first, keys)]).toEqual([value, new Set([count])]);
|
|
70
|
+
expect(read.get(count)).toBe(value);
|
|
71
|
+
expect(await readResource(second, written)).toBe(secondWritten);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
52
75
|
describe("a derived component declared without a default", () => {
|
|
53
76
|
it("starts every row of a world at one empty Map of its own, since values are replaced whole", async () => {
|
|
54
77
|
const spawn = System.create(({ commands }) => {
|