@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
|
@@ -12,6 +12,7 @@ import { ownTag } from './move-log.js';
|
|
|
12
12
|
import { resourceCell } from './resources.js';
|
|
13
13
|
import { labelOf } from '../../interface/registry/names.js';
|
|
14
14
|
import { idOf, type Key, type State } from './state.js';
|
|
15
|
+
import { tablesReader } from './tables-read.js';
|
|
15
16
|
import { combine, contributed, identityOf, type ReductionTarget } from './reductions.js';
|
|
16
17
|
import { createIndexLookup, type Since } from './index-lookups.js';
|
|
17
18
|
import type { WorldIndex } from './indexes.js';
|
|
@@ -130,13 +131,26 @@ const viewOf = (storage: RowStorage): TypedArray => {
|
|
|
130
131
|
return layout.kind === "linear" && storage.write !== writeGeneral ? storage.views[layout.leaves[0].view] : storage.views.f32;
|
|
131
132
|
};
|
|
132
133
|
|
|
133
|
-
const
|
|
134
|
+
const storageOf = (archetype: RowArchetype, id: number): RowStorage => {
|
|
134
135
|
const storage = archetype.storageById[id];
|
|
135
136
|
// A system matches only archetypes with every component it reads or writes.
|
|
136
137
|
if (storage === undefined) throw new Error(`Archetype ${archetype.id} lacks component ${id}.`);
|
|
138
|
+
return storage;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const slotOf = (archetype: RowArchetype, id: number): Slot => {
|
|
142
|
+
const storage = storageOf(archetype, id);
|
|
137
143
|
return { storage, view: viewOf(storage), value: constantOf(storage) };
|
|
138
144
|
};
|
|
139
145
|
|
|
146
|
+
/** Points `slot` at `archetype`'s storage of component `id`, allocating nothing, since rows bind once per archetype a run. */
|
|
147
|
+
const bindSlot = (slot: Slot, archetype: RowArchetype, id: number): void => {
|
|
148
|
+
const storage = storageOf(archetype, id);
|
|
149
|
+
slot.storage = storage;
|
|
150
|
+
slot.view = viewOf(storage);
|
|
151
|
+
slot.value = constantOf(storage);
|
|
152
|
+
};
|
|
153
|
+
|
|
140
154
|
/** An optional component's getter where the archetype lacks it, and its setter, which fails its system. */
|
|
141
155
|
const absentGetter = (): undefined => undefined;
|
|
142
156
|
const absentSetterOf = (key: Key): (value: unknown) => void => () => {
|
|
@@ -167,11 +181,7 @@ export const createRow = (host: RowHost, archetype: RowArchetype, keys: readonly
|
|
|
167
181
|
row.entities = next.entities;
|
|
168
182
|
for (let i = 0; i < ids.length; i++) {
|
|
169
183
|
const slot = slots[i];
|
|
170
|
-
if (slot
|
|
171
|
-
const { storage, view, value } = slotOf(next, ids[i]);
|
|
172
|
-
slot.storage = storage;
|
|
173
|
-
slot.view = view;
|
|
174
|
-
slot.value = value;
|
|
184
|
+
if (slot !== undefined) bindSlot(slot, next, ids[i]);
|
|
175
185
|
}
|
|
176
186
|
},
|
|
177
187
|
};
|
|
@@ -270,12 +280,7 @@ export const sharedRows = (host: RowHost, keys: readonly Key[], ids: readonly nu
|
|
|
270
280
|
slots: ids.map(id => slotOf(archetype, id)),
|
|
271
281
|
bind: next => {
|
|
272
282
|
row.entities = next.entities;
|
|
273
|
-
for (let i = 0; i < ids.length; i++)
|
|
274
|
-
const { storage, view, value } = slotOf(next, ids[i]);
|
|
275
|
-
row.slots[i].storage = storage;
|
|
276
|
-
row.slots[i].view = view;
|
|
277
|
-
row.slots[i].value = value;
|
|
278
|
-
}
|
|
283
|
+
for (let i = 0; i < ids.length; i++) bindSlot(row.slots[i], next, ids[i]);
|
|
279
284
|
},
|
|
280
285
|
};
|
|
281
286
|
Object.defineProperty(value, rowOfValue, { value: row });
|
|
@@ -506,8 +511,14 @@ const never: Since = { value: -1 };
|
|
|
506
511
|
const own = (): object => Object.create({});
|
|
507
512
|
|
|
508
513
|
/** Resource cells are resolved once per run: they move only at sync points. An unresolved resource throws on access. */
|
|
509
|
-
export const createContext = (state: State, declaration: Pick<Resolved, "lookup" | "resources">, { targets = [], commands, indexes, since = never }: Extras = {}): Context => {
|
|
514
|
+
export const createContext = (state: State, declaration: Pick<Resolved, "lookup" | "resources" | "tables">, { targets = [], commands, indexes, since = never }: Extras = {}): Context => {
|
|
510
515
|
const value: { up?: unknown } = own();
|
|
516
|
+
const { tables } = declaration;
|
|
517
|
+
// Each read copies the tables as they are then.
|
|
518
|
+
if (tables !== undefined) {
|
|
519
|
+
const read = tablesReader(state, tables);
|
|
520
|
+
Object.defineProperty(value, "tables", { get: read, enumerable: true });
|
|
521
|
+
}
|
|
511
522
|
const { lookup, resources } = declaration;
|
|
512
523
|
const names = Object.keys(lookup ?? {});
|
|
513
524
|
if (names.length > 0) {
|
|
@@ -226,6 +226,43 @@ describe("references", () => {
|
|
|
226
226
|
await world.dispose();
|
|
227
227
|
});
|
|
228
228
|
|
|
229
|
+
it("cascade through entities spawned together, whichever archetype each is spawned into first", async () => {
|
|
230
|
+
const world = createWorld({ [workers]: 0 });
|
|
231
|
+
const step = at(world);
|
|
232
|
+
let gone = Entity.none, owner = Entity.none, child = Entity.none, watcher = Entity.none;
|
|
233
|
+
await step(commands => { gone = commands.spawn([mark])({ [mark]: 0 }); });
|
|
234
|
+
await step(commands => commands.despawn(gone));
|
|
235
|
+
await step(commands => {
|
|
236
|
+
// The archetypes of the child and the watcher take their first spawns before the owner's does.
|
|
237
|
+
commands.spawn([mark, owned])({ [mark]: 1, [owned]: Entity.none });
|
|
238
|
+
commands.spawn([mark, follows])({ [mark]: 2, [follows]: Entity.none });
|
|
239
|
+
owner = commands.spawn([mark, owned, sprite])({ [mark]: 3, [owned]: gone });
|
|
240
|
+
child = commands.spawn([mark, owned])({ [mark]: 4, [owned]: owner });
|
|
241
|
+
watcher = commands.spawn([mark, follows])({ [mark]: 5, [follows]: owner });
|
|
242
|
+
});
|
|
243
|
+
expect(await read(world, owner, mark)).toBeNull();
|
|
244
|
+
expect(await read(world, child, mark)).toBeNull();
|
|
245
|
+
expect(await components(world, watcher)).toEqual([mark]);
|
|
246
|
+
await world.dispose();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("cascade to entities spawned in the sync point their owner despawns in", async () => {
|
|
250
|
+
const world = createWorld({ [workers]: 0 });
|
|
251
|
+
const step = at(world);
|
|
252
|
+
let owner = Entity.none, child = Entity.none, grandchild = Entity.none;
|
|
253
|
+
await step(commands => { owner = commands.spawn([mark])({ [mark]: 0 }); });
|
|
254
|
+
await step(commands => {
|
|
255
|
+
// The grandchild's archetype takes its first spawn before the child's does.
|
|
256
|
+
commands.spawn([mark, owned, sprite])({ [mark]: 1, [owned]: Entity.none });
|
|
257
|
+
child = commands.spawn([mark, owned])({ [mark]: 2, [owned]: owner });
|
|
258
|
+
grandchild = commands.spawn([mark, owned, sprite])({ [mark]: 3, [owned]: child });
|
|
259
|
+
commands.despawn(owner);
|
|
260
|
+
});
|
|
261
|
+
expect(await read(world, child, mark)).toBeNull();
|
|
262
|
+
expect(await read(world, grandchild, mark)).toBeNull();
|
|
263
|
+
await world.dispose();
|
|
264
|
+
});
|
|
265
|
+
|
|
229
266
|
it("move a holder whose weak reference is dropped to an archetype no entity had, closed by the rules", async () => {
|
|
230
267
|
const world = createWorld({ [workers]: 0 });
|
|
231
268
|
const step = at(world);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import { expect, it } from "vitest";
|
|
3
|
+
import { Entity, Finalizer, System } from "../../index.js";
|
|
4
|
+
import { createWorld } from "../index.js";
|
|
5
|
+
import type { Tables } from "../../interface/system/tables.js";
|
|
6
|
+
import type { Table } from "../../interface/world/table/table.js";
|
|
7
|
+
|
|
8
|
+
const name = Symbol("runChanges/name");
|
|
9
|
+
const score = Symbol("runChanges/score");
|
|
10
|
+
const spent = Symbol("runChanges/spent");
|
|
11
|
+
const echo = Symbol("runChanges/echo");
|
|
12
|
+
const budget = Symbol("runChanges/budget");
|
|
13
|
+
|
|
14
|
+
declare module "@adobe/ecs" {
|
|
15
|
+
interface Components {
|
|
16
|
+
[name]: { type: string };
|
|
17
|
+
[score]: { type: number };
|
|
18
|
+
[spent]: { type: true };
|
|
19
|
+
[echo]: { type: string };
|
|
20
|
+
}
|
|
21
|
+
interface Resources {
|
|
22
|
+
[budget]: { type: number; default: 10 };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// An entity spent leaves an echo, which its finalizer spawns at the next sync point.
|
|
27
|
+
Finalizer.create({ components: { with: [name, spent] } }, (row, { commands }) => { commands.spawn([echo])({ [echo]: row[name] }); });
|
|
28
|
+
|
|
29
|
+
const spawn = System.create({}, ({ commands }, input: { readonly who: string }) => { commands.spawn([name])({ [name]: input.who }); });
|
|
30
|
+
const score10 = System.create({ components: { with: [name], not: [score] }, after: [spawn] }, (row, { commands }) => { commands.add(row.entity, score, 10); });
|
|
31
|
+
const enter = System.create([spawn, score10]);
|
|
32
|
+
const rescore = System.create({ components: { with: [score] } }, (row, { commands }) => { commands.add(row.entity, score, row[score] + 1); });
|
|
33
|
+
const spend = System.create({ components: { with: [name] } }, (row, { commands, resources }) => {
|
|
34
|
+
commands.add(row.entity, spent);
|
|
35
|
+
commands.despawn(row.entity);
|
|
36
|
+
resources[budget] -= 1;
|
|
37
|
+
});
|
|
38
|
+
const raise = System.create({}, ({ resources }) => { resources[budget] = resources[budget]; });
|
|
39
|
+
const readNamed = System.create({ readonly: true, tables: { components: [name] } }, ({ tables }): Tables => tables);
|
|
40
|
+
const copied: { tables: readonly Table[] } = { tables: [] };
|
|
41
|
+
const copy = System.create(({ commands }) => { commands.spawnTables(copied.tables); });
|
|
42
|
+
const clear = System.create(({ commands }) => { commands.despawnWhere({ with: [name] }); });
|
|
43
|
+
|
|
44
|
+
const describe = (changes: Awaited<ReturnType<typeof run>>) => changes.groups.map(group => ({
|
|
45
|
+
from: group.from?.map(key => key.description) ?? null, to: group.to?.map(key => key.description) ?? null,
|
|
46
|
+
replaced: group.replaced.map(key => key.description),
|
|
47
|
+
values: group.entities.map(entity => [changes.previous(entity, score), changes.next(entity, score), changes.previous(entity, name) ?? changes.next(entity, name)]),
|
|
48
|
+
}));
|
|
49
|
+
const run = (world: ReturnType<typeof createWorld>) => world.run(rescore, { changes: true });
|
|
50
|
+
|
|
51
|
+
it("resolves to the run's own changes, merged across its sync points, with values before and after it", async () => {
|
|
52
|
+
await using world = createWorld();
|
|
53
|
+
const entered = await world.run(enter, { who: "ada" }, { changes: true });
|
|
54
|
+
expect(describe(entered)).toEqual([{ from: null, to: ["runChanges/name", "runChanges/score"], replaced: [], values: [[null, 10, "ada"]] }]);
|
|
55
|
+
expect(entered.resources).toEqual([]);
|
|
56
|
+
expect(describe(await run(world))).toEqual([{ from: ["runChanges/name", "runChanges/score"], to: ["runChanges/name", "runChanges/score"], replaced: ["runChanges/score"], values: [[10, 11, "ada"]] }]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("leaves out what finalizers of an earlier sync point queued, and gives what its systems wrote of resources", async () => {
|
|
60
|
+
await using world = createWorld();
|
|
61
|
+
await world.run(enter, { who: "bo" });
|
|
62
|
+
// Its finalizer queues an echo for the next sync point, which applies before the next run's own changes.
|
|
63
|
+
const spent = await world.run(spend, { changes: true });
|
|
64
|
+
expect(describe(spent)).toEqual([{ from: ["runChanges/name", "runChanges/score"], to: null, replaced: [], values: [[10, null, "bo"]] }]);
|
|
65
|
+
expect(spent.resources).toEqual([{ key: budget, previous: 10, next: 9 }]);
|
|
66
|
+
const raised = await world.run(raise, { changes: true });
|
|
67
|
+
expect([raised.groups, raised.resources]).toEqual([[], []]);
|
|
68
|
+
expect(raised.previous(Entity.none, name)).toBe(null);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("runs as usual without the option, and with input that only looks like it", async () => {
|
|
72
|
+
await using world = createWorld();
|
|
73
|
+
const echoing = System.create({}, (_context, input: { readonly changes: boolean }) => input.changes);
|
|
74
|
+
expect(await world.run(echoing, { changes: true })).toBe(true);
|
|
75
|
+
expect(await world.run(raise)).toBe(undefined);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("gives the rows spawnTables spawns and the entities despawnWhere despawns as a run's changes", async () => {
|
|
79
|
+
await using world = createWorld();
|
|
80
|
+
await world.run(enter, { who: "cy" });
|
|
81
|
+
copied.tables = (await world.run(readNamed)).archetypes;
|
|
82
|
+
// The tables copy names alone.
|
|
83
|
+
expect(describe(await world.run(copy, { changes: true }))).toEqual([{ from: null, to: ["runChanges/name"], replaced: [], values: [[null, null, "cy"]] }]);
|
|
84
|
+
expect(describe(await world.run(clear, { changes: true }))).toEqual([
|
|
85
|
+
{ from: ["runChanges/name"], to: null, replaced: [], values: [[null, null, "cy"]] },
|
|
86
|
+
{ from: ["runChanges/name", "runChanges/score"], to: null, replaced: [], values: [[10, null, "cy"]] },
|
|
87
|
+
]);
|
|
88
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import type { Entity } from '../../interface/entity/entity.js';
|
|
3
|
+
import type { Essential, ReadonlyType } from '../../interface/component/value.js';
|
|
4
|
+
import type { ChangeSet } from '../../interface/finalizer/body.js';
|
|
5
|
+
import type { Resource } from '../../interface/resource/resource.js';
|
|
6
|
+
import { isLeaf, newestKeyOf, scheduleOf, type Definition, type Leaf } from '../../interface/registry/index.js';
|
|
7
|
+
import type { Archetype } from './archetype.js';
|
|
8
|
+
import { readCell } from './cells.js';
|
|
9
|
+
import { asEntity, indexOf, locate } from './entities.js';
|
|
10
|
+
import { sameValue } from './resource-changes.js';
|
|
11
|
+
import { findResource, initialOf } from './resources.js';
|
|
12
|
+
import { metIdOf, type Key, type State } from './state.js';
|
|
13
|
+
import type { Queued } from './synchronize.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* What a run asked for its changes holds while it runs: by entity its sync points planned, the watched components it
|
|
17
|
+
* had before the run, none for one born in it, with their values then, and those its adds gave a value; and the
|
|
18
|
+
* resources its systems write or reduce into, with their values before it.
|
|
19
|
+
*/
|
|
20
|
+
export type Recording = {
|
|
21
|
+
readonly before: Map<number, { readonly ids: readonly number[] | null; readonly values: ReadonlyMap<number, unknown> }>;
|
|
22
|
+
readonly given: Map<number, Set<number>>;
|
|
23
|
+
readonly resources: ReadonlyMap<Key, unknown>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Every component but derived ones and the objects a joined thread fills, as a changes finalizer watches by default. */
|
|
27
|
+
const watched = (state: State, id: number): boolean => !state.derived[id] && !state.resources.has(id) && state.remote[id] === undefined;
|
|
28
|
+
|
|
29
|
+
const noValues: ReadonlyMap<number, unknown> = new Map();
|
|
30
|
+
|
|
31
|
+
/** The values of components `ids` of an archetype's row. */
|
|
32
|
+
const valuesAt = (archetype: Archetype, ids: readonly number[], row: number): ReadonlyMap<number, unknown> => {
|
|
33
|
+
const values = new Map<number, unknown>();
|
|
34
|
+
for (const id of ids) {
|
|
35
|
+
const storage = archetype.storageById[id];
|
|
36
|
+
if (storage !== undefined) values.set(id, readCell(storage, row));
|
|
37
|
+
}
|
|
38
|
+
return values;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** Starts recording a run whose systems write or reduce into `resources`, from their values now. */
|
|
42
|
+
export const startRecording = (state: State, resources: Iterable<Key>): Recording => {
|
|
43
|
+
const values = new Map<Key, unknown>();
|
|
44
|
+
for (const key of resources) {
|
|
45
|
+
const cell = findResource(state, key);
|
|
46
|
+
values.set(key, cell === undefined ? initialOf(state, key) : readCell(cell.storage, cell.row));
|
|
47
|
+
}
|
|
48
|
+
return { before: new Map(), given: new Map(), resources: values };
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* At a sync point of the run, before it applies: each entity its plan names that no earlier one did, with what it has
|
|
53
|
+
* now, and the components its adds give a value. `born` holds the rows committed from batches here, by archetype.
|
|
54
|
+
*/
|
|
55
|
+
export const recordPlan = (state: State, recording: Recording, queued: readonly Queued[], born: ReadonlyMap<number, number>): void => {
|
|
56
|
+
const { plan, archetypes, entities } = state;
|
|
57
|
+
const note = (entity: number, from: Archetype | null): void => {
|
|
58
|
+
if (recording.before.has(entity) || from?.resource === true) return;
|
|
59
|
+
if (from === null) {
|
|
60
|
+
recording.before.set(entity, { ids: null, values: noValues });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const row = entities.row[indexOf(entity)];
|
|
64
|
+
const ids = from.components.filter(id => watched(state, id));
|
|
65
|
+
recording.before.set(entity, { ids, values: valuesAt(from, ids, row) });
|
|
66
|
+
};
|
|
67
|
+
const give = (entity: number, buffer: number, start: number, end: number): void => {
|
|
68
|
+
const { ids } = queued[buffer].buffer;
|
|
69
|
+
for (let k = start; k < end; k++) {
|
|
70
|
+
if (!watched(state, ids[k])) continue;
|
|
71
|
+
let given = recording.given.get(entity);
|
|
72
|
+
if (given === undefined) recording.given.set(entity, given = new Set());
|
|
73
|
+
given.add(ids[k]);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
for (let entry = 0; entry < plan.count; entry++) {
|
|
77
|
+
const entity = plan.entity[entry];
|
|
78
|
+
const fromId = plan.from[entry];
|
|
79
|
+
const first = fromId < 0 ? undefined : born.get(fromId);
|
|
80
|
+
note(entity, fromId < 0 || (first !== undefined && entities.row[indexOf(entity)] >= first) ? null : archetypes[fromId]);
|
|
81
|
+
if (plan.start[entry] < 0) continue;
|
|
82
|
+
give(entity, plan.buffer[entry], plan.start[entry], plan.end[entry]);
|
|
83
|
+
const more = plan.more.size === 0 ? undefined : plan.more.get(entry);
|
|
84
|
+
if (more !== undefined) for (let r = 0; r < more.length; r += 3) give(entity, more[r], more[r + 1], more[r + 2]);
|
|
85
|
+
}
|
|
86
|
+
// Rows committed from batches no command touched have no entry.
|
|
87
|
+
for (const [id, first] of born) {
|
|
88
|
+
const archetype = archetypes[id];
|
|
89
|
+
for (let row = first; row < archetype.count; row++) note(archetype.entities[row], null);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** A watched component's newest key: watched components are registered and not derived, so they're essential. */
|
|
94
|
+
const keyOf = (state: State, id: number): Essential => newestKeyOf(state.keys[id]) as Essential;
|
|
95
|
+
|
|
96
|
+
/** A group of a change set, as it's built. */
|
|
97
|
+
type Grouped = { from: Essential[] | null; to: Essential[] | null; added: Essential[]; removed: Essential[]; replaced: Essential[]; entities: Entity[] };
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The run's change set once its last sync point applied: each entity grouped by the watched components it had before
|
|
101
|
+
* the run and has after, those it gained and lost, and those adds gave a value it had before and after, with their
|
|
102
|
+
* values then and now; and each resource its systems write or reduce into whose value changed.
|
|
103
|
+
*/
|
|
104
|
+
export const changesOf = (state: State, recording: Recording): ChangeSet<Essential, Resource.Key> => {
|
|
105
|
+
const { archetypes, entities } = state;
|
|
106
|
+
const groups = new Map<string, Grouped>();
|
|
107
|
+
const after = new Map<number, ReadonlyMap<number, unknown>>();
|
|
108
|
+
for (const [entity, { ids: before }] of recording.before) {
|
|
109
|
+
const index = locate(entities, entity);
|
|
110
|
+
const to = index < 0 ? null : archetypes[entities.archetype[index]];
|
|
111
|
+
const ids = to === null ? null : to.components.filter(id => watched(state, id));
|
|
112
|
+
if (before === null && ids === null) continue;
|
|
113
|
+
const added = (ids ?? []).filter(id => !(before ?? []).includes(id));
|
|
114
|
+
const removed = (before ?? []).filter(id => !(ids ?? []).includes(id));
|
|
115
|
+
const replaced = [...recording.given.get(entity) ?? []].filter(id => before?.includes(id) === true && ids?.includes(id) === true).sort((a, b) => a - b);
|
|
116
|
+
if (added.length === 0 && removed.length === 0 && replaced.length === 0) continue;
|
|
117
|
+
if (to !== null && ids !== null) {
|
|
118
|
+
after.set(entity, valuesAt(to, ids, entities.row[index]));
|
|
119
|
+
}
|
|
120
|
+
const name = `${before?.join(",") ?? "-"}|${ids?.join(",") ?? "-"}|${replaced.join(",")}`;
|
|
121
|
+
let group = groups.get(name);
|
|
122
|
+
const keys = (list: readonly number[] | null): Essential[] | null => list === null ? null : list.map(id => keyOf(state, id));
|
|
123
|
+
if (group === undefined) {
|
|
124
|
+
groups.set(name, group = { from: keys(before), to: keys(ids), added: added.map(id => keyOf(state, id)), removed: removed.map(id => keyOf(state, id)), replaced: replaced.map(id => keyOf(state, id)), entities: [] });
|
|
125
|
+
}
|
|
126
|
+
group.entities.push(asEntity(entity));
|
|
127
|
+
}
|
|
128
|
+
const resources: { key: Resource.Key; previous: unknown; next: unknown }[] = [];
|
|
129
|
+
for (const [key, previous] of recording.resources) {
|
|
130
|
+
const cell = findResource(state, key);
|
|
131
|
+
const next = cell === undefined ? previous : readCell(cell.storage, cell.row);
|
|
132
|
+
// A key the run's systems name is a resource's.
|
|
133
|
+
if (!sameValue(previous, next)) resources.push({ key: newestKeyOf(key) as Resource.Key, previous, next });
|
|
134
|
+
}
|
|
135
|
+
const valueOf = (values: ReadonlyMap<number, unknown> | undefined, key: Essential): unknown => {
|
|
136
|
+
const id = metIdOf(state, key);
|
|
137
|
+
return id !== undefined && values?.has(id) === true ? values.get(id) : null;
|
|
138
|
+
};
|
|
139
|
+
return {
|
|
140
|
+
groups: [...groups.values()],
|
|
141
|
+
// Values read from storage match each key's registered schema, so they have its type.
|
|
142
|
+
resources: resources as ChangeSet<Essential, Resource.Key>["resources"],
|
|
143
|
+
previous: <K extends Essential>(entity: Entity, key: K) => valueOf(recording.before.get(entity)?.values, key) as ReadonlyType<K> | null,
|
|
144
|
+
next: <K extends Essential>(entity: Entity, key: K) => valueOf(after.get(entity), key) as ReadonlyType<K> | null,
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/** A system's leaves that run in tiers, or the system itself. */
|
|
149
|
+
const leavesOf = (definition: Definition): readonly Leaf[] => isLeaf(definition) ? [definition] : scheduleOf(definition).tiers.flat();
|
|
150
|
+
|
|
151
|
+
/** The resources a system's leaves write or reduce into. */
|
|
152
|
+
export const resourcesWrittenBy = (definition: Definition): ReadonlySet<Key> =>
|
|
153
|
+
new Set(leavesOf(definition).flatMap(leaf => [...leaf.declaration.resources?.write ?? [], ...leaf.declaration.resources?.reduce ?? []]));
|
|
154
|
+
|
|
155
|
+
/** Whether a system or composite takes input, so a run's options follow it. */
|
|
156
|
+
export const takesInput = (definition: Definition): boolean =>
|
|
157
|
+
isLeaf(definition) ? definition.input !== undefined : [...scheduleOf(definition).tiers.flat(), ...scheduleOf(definition).observer].some(leaf => leaf.input !== undefined);
|
|
158
|
+
|
|
159
|
+
/** Whether a run's options ask for its changes. */
|
|
160
|
+
export const asksForChanges = (options: unknown): boolean => typeof options === "object" && options !== null && Reflect.get(options, "changes") === true;
|
|
@@ -30,11 +30,11 @@ const spawn = System.create(({ commands }) => {
|
|
|
30
30
|
const raise = System.create(({ resources }) => {
|
|
31
31
|
resources[level] += 1;
|
|
32
32
|
});
|
|
33
|
-
const count = System.create({
|
|
33
|
+
const count = System.create({ readonly: true, components: { with: [amount] } }, () => {
|
|
34
34
|
counted++;
|
|
35
35
|
});
|
|
36
36
|
let counted = 0;
|
|
37
|
-
const readLevel = System.create({
|
|
37
|
+
const readLevel = System.create({ readonly: true }, ({ resources }) => resources[level]);
|
|
38
38
|
const broken = System.create(() => {
|
|
39
39
|
throw new Error("broken");
|
|
40
40
|
});
|
|
@@ -57,7 +57,7 @@ describe("a run", () => {
|
|
|
57
57
|
expect(await world.run(System.create({ components: { with: [amount] } }, row => row[amount]))).toBeUndefined();
|
|
58
58
|
expect(await world.run(System.create([readLevel, raise]))).toBeUndefined();
|
|
59
59
|
expect(await world.run(raise)).toBeUndefined();
|
|
60
|
-
const whenChanged = System.create({
|
|
60
|
+
const whenChanged = System.create({ readonly: true, resources: { changed: [level] } }, ({ resources }) => resources[level]);
|
|
61
61
|
expect(await world.run(whenChanged)).toBe(2);
|
|
62
62
|
// Unchanged since it last ran: skipped.
|
|
63
63
|
expect(await world.run(whenChanged)).toBeUndefined();
|
|
@@ -94,7 +94,7 @@ describe("a run", () => {
|
|
|
94
94
|
it("rejects when its own observer system fails, and reports one added out of band", async () => {
|
|
95
95
|
using reported = catchOutOfBand();
|
|
96
96
|
await using world = inline();
|
|
97
|
-
const failingObserver = System.create({
|
|
97
|
+
const failingObserver = System.create({ readonly: true }, () => {
|
|
98
98
|
throw new Error("observer failed");
|
|
99
99
|
});
|
|
100
100
|
await expect(world.run(failingObserver)).rejects.toThrow(/observer failed/);
|
|
@@ -144,7 +144,7 @@ const spawnTwins = System.create(({ commands }) => {
|
|
|
144
144
|
commands.spawn([serial])({ [serial]: 1 });
|
|
145
145
|
commands.spawn([serial])({ [serial]: 1 });
|
|
146
146
|
});
|
|
147
|
-
const findTwin = System.create({
|
|
147
|
+
const findTwin = System.create({ readonly: true, lookup: { twins: { index: bySerial } } }, ({ lookup }) => lookup.twins(1)?.entity ?? null);
|
|
148
148
|
const setGate = System.create({}, ({ resources }, input: { readonly open: boolean }) => {
|
|
149
149
|
resources[gate] = input.open;
|
|
150
150
|
});
|
|
@@ -207,10 +207,10 @@ describe("a run's observer pass", () => {
|
|
|
207
207
|
it("fails the run for its own observer system, and reports one added out of band, in one run", async () => {
|
|
208
208
|
using reported = catchOutOfBand();
|
|
209
209
|
await using world = inline();
|
|
210
|
-
const ownFails = System.create({
|
|
210
|
+
const ownFails = System.create({ readonly: true }, () => {
|
|
211
211
|
throw new Error("own failed");
|
|
212
212
|
});
|
|
213
|
-
const addedFails = System.create({
|
|
213
|
+
const addedFails = System.create({ readonly: true }, () => {
|
|
214
214
|
throw new Error("added failed");
|
|
215
215
|
});
|
|
216
216
|
world.add(addedFails);
|
|
@@ -255,7 +255,7 @@ describe("out-of-band reports", () => {
|
|
|
255
255
|
it("aren't made again for a system skipped between its failures", async () => {
|
|
256
256
|
using reported = catchOutOfBand();
|
|
257
257
|
await using world = inline();
|
|
258
|
-
const failsWhenChanged = System.create({
|
|
258
|
+
const failsWhenChanged = System.create({ readonly: true, resources: { changed: [level] } }, () => {
|
|
259
259
|
throw new Error("changed failed");
|
|
260
260
|
});
|
|
261
261
|
world.add(failsWhenChanged);
|
|
@@ -294,7 +294,7 @@ describe("out-of-band reports", () => {
|
|
|
294
294
|
using reported = catchOutOfBand();
|
|
295
295
|
const world = inline();
|
|
296
296
|
let failing = false;
|
|
297
|
-
const failsAdded = System.create({
|
|
297
|
+
const failsAdded = System.create({ readonly: true }, () => {
|
|
298
298
|
if (failing) throw new Error("added failed");
|
|
299
299
|
});
|
|
300
300
|
world.add(failsAdded);
|
|
@@ -331,7 +331,7 @@ describe("out-of-band reports", () => {
|
|
|
331
331
|
it("aren't made again for a row system whose change filter passed no row between its failures", async () => {
|
|
332
332
|
using reported = catchOutOfBand();
|
|
333
333
|
await using world = inline();
|
|
334
|
-
const failsOnChange = System.create({
|
|
334
|
+
const failsOnChange = System.create({ readonly: true, components: { with: [amount], changed: [amount] } }, () => {
|
|
335
335
|
throw new Error("row failed");
|
|
336
336
|
});
|
|
337
337
|
const bump = System.create({ components: { with: [amount] } }, row => {
|
|
@@ -63,7 +63,7 @@ const noIndexParameters: readonly number[] = [];
|
|
|
63
63
|
const noAddresses: readonly number[] = [];
|
|
64
64
|
|
|
65
65
|
/** An archetype's change filters for a runner, and the WASM addresses of its columns and filters (`filterCache`). */
|
|
66
|
-
type Filtered = { readonly version: number; readonly filters: readonly Filter[]; addresses: readonly number[]; addressed: WasmKernel | null };
|
|
66
|
+
type Filtered = { readonly archetype: Archetype; readonly version: number; readonly filters: readonly Filter[]; addresses: readonly number[]; addressed: WasmKernel | null };
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
69
|
* Runs one leaf system. Each run takes a new tick, which becomes the system's `lastRun`. `start` does main-thread work
|
|
@@ -260,7 +260,7 @@ export const createRunner = (state: State, leaf: Leaf, since = 0): Runner => {
|
|
|
260
260
|
: createOrderedRender(indexLookups.ordered, ordered, accessors.map(accessor => idOf(state, accessor.component)), declaration.shader?.group ?? 0, `${leafNumber}:`);
|
|
261
261
|
const touches = [...new Set([...query?.read ?? [], ...query?.write ?? []])].map(key => idOf(state, key));
|
|
262
262
|
const writes = (query?.write ?? []).map(key => idOf(state, key));
|
|
263
|
-
const looks = [...new Set([...Object.values(declaration.lookup ?? {}).flatMap(lookup => lookup.read ?? []), ...declaration.up?.read ?? []])].map(key => idOf(state, key));
|
|
263
|
+
const looks = [...new Set([...Object.values(declaration.lookup ?? {}).flatMap(lookup => lookup.read ?? []), ...declaration.up?.read ?? [], ...declaration.tables?.components ?? []])].map(key => idOf(state, key));
|
|
264
264
|
/** Record keys by archetype. */
|
|
265
265
|
const keys = new Map<Archetype, string>();
|
|
266
266
|
const keyOf = (archetype: Archetype): string => {
|
|
@@ -339,7 +339,7 @@ export const createRunner = (state: State, leaf: Leaf, since = 0): Runner => {
|
|
|
339
339
|
let runInput: unknown;
|
|
340
340
|
const context = createContext(state, declaration, {
|
|
341
341
|
targets: kernel === null ? targets : [],
|
|
342
|
-
commands: leaf.kind === "render" || declaration.kernel === "gpu" || declaration.
|
|
342
|
+
commands: leaf.kind === "render" || declaration.kernel === "gpu" || declaration.readonly === true ? undefined : commands,
|
|
343
343
|
indexes: indexLookups.byName,
|
|
344
344
|
since: lookupSince,
|
|
345
345
|
});
|
|
@@ -470,7 +470,7 @@ export const createRunner = (state: State, leaf: Leaf, since = 0): Runner => {
|
|
|
470
470
|
|
|
471
471
|
/** Drops match `m`, its last taking its place; with `where`, one evaluated stays among those evaluated. */
|
|
472
472
|
const removeMatch = (m: number): void => {
|
|
473
|
-
filterCache
|
|
473
|
+
filterCache[matches[m].id] = undefined;
|
|
474
474
|
matchOf[matches[m].id] = -1;
|
|
475
475
|
let hole = m;
|
|
476
476
|
if (where !== null && m < where.evaluated) {
|
|
@@ -487,7 +487,7 @@ export const createRunner = (state: State, leaf: Leaf, since = 0): Runner => {
|
|
|
487
487
|
/** Matches `m` anew, reused for other values: its point is its last run's, and `where` evaluates it again. */
|
|
488
488
|
const renewMatch = (m: number): void => {
|
|
489
489
|
const archetype = matches[m];
|
|
490
|
-
filterCache.
|
|
490
|
+
filterCache[archetype.id] = undefined;
|
|
491
491
|
points[m] = lastRun;
|
|
492
492
|
generations[m] = archetype.generation;
|
|
493
493
|
if (where !== null && m < where.evaluated) {
|
|
@@ -563,12 +563,12 @@ export const createRunner = (state: State, leaf: Leaf, since = 0): Runner => {
|
|
|
563
563
|
* and the WASM addresses of its columns and filters, for the WASM kernel `addressed` they were taken for, since a
|
|
564
564
|
* kernel loaded again may stamp a tick column the last one muted.
|
|
565
565
|
*/
|
|
566
|
-
const filterCache
|
|
566
|
+
const filterCache: (Filtered | undefined)[] = [];
|
|
567
567
|
const noFilters: readonly Filter[] = [];
|
|
568
568
|
|
|
569
569
|
const filteredOf = (archetype: Archetype) => {
|
|
570
|
-
const cached = filterCache.
|
|
571
|
-
if (cached !== undefined && cached.version === archetype.version) return cached;
|
|
570
|
+
const cached = filterCache[archetype.id];
|
|
571
|
+
if (cached !== undefined && cached.archetype === archetype && cached.version === archetype.version) return cached;
|
|
572
572
|
const filters = addedIds.length + changedIds.length === 0 ? noFilters : [
|
|
573
573
|
...addedIds.flatMap(id => {
|
|
574
574
|
const storage = archetype.storageById[id];
|
|
@@ -579,8 +579,8 @@ export const createRunner = (state: State, leaf: Leaf, since = 0): Runner => {
|
|
|
579
579
|
return storage === undefined ? [] : [{ ticks: storage.changed, chunks: storage.chunkChanged, blocks: storage.blockChanged }];
|
|
580
580
|
}),
|
|
581
581
|
];
|
|
582
|
-
const entry: Filtered = { version: archetype.version, filters, addresses: noAddresses, addressed: null };
|
|
583
|
-
filterCache.
|
|
582
|
+
const entry: Filtered = { archetype, version: archetype.version, filters, addresses: noAddresses, addressed: null };
|
|
583
|
+
filterCache[archetype.id] = entry;
|
|
584
584
|
return entry;
|
|
585
585
|
};
|
|
586
586
|
|
|
@@ -614,13 +614,14 @@ export const createRunner = (state: State, leaf: Leaf, since = 0): Runner => {
|
|
|
614
614
|
const { value } = row;
|
|
615
615
|
const values = context.value;
|
|
616
616
|
const input = runInput;
|
|
617
|
+
const { body } = system;
|
|
617
618
|
const filters = filtersOf(archetype);
|
|
618
619
|
// Every row passes, so nothing is filtered.
|
|
619
620
|
// A body may remove its own system, after which no row of it runs.
|
|
620
621
|
if (since < 0 || filters.length === 0) {
|
|
621
622
|
for (let index = 0, count = archetype.count; index < count && !holdings.removed; index++) {
|
|
622
623
|
row.index = index;
|
|
623
|
-
const returned =
|
|
624
|
+
const returned = body(value, values, input);
|
|
624
625
|
if (returned !== undefined) noPromise(returned);
|
|
625
626
|
}
|
|
626
627
|
return;
|
|
@@ -628,7 +629,7 @@ export const createRunner = (state: State, leaf: Leaf, since = 0): Runner => {
|
|
|
628
629
|
forEachPassingRange(filters, since, 0, archetype.count, (start, end) => {
|
|
629
630
|
for (let index = start; index < end && !holdings.removed; index++) {
|
|
630
631
|
row.index = index;
|
|
631
|
-
const returned =
|
|
632
|
+
const returned = body(value, values, input);
|
|
632
633
|
if (returned !== undefined) noPromise(returned);
|
|
633
634
|
}
|
|
634
635
|
});
|
|
@@ -714,10 +715,11 @@ export const createRunner = (state: State, leaf: Leaf, since = 0): Runner => {
|
|
|
714
715
|
const upContext = context.value;
|
|
715
716
|
const input = runInput;
|
|
716
717
|
upContext.up = parentRow === null ? null : parentRow.value;
|
|
718
|
+
const { body } = system;
|
|
717
719
|
for (let n = start; n < end && !holdings.removed; n++) {
|
|
718
720
|
row.index = rows[n];
|
|
719
721
|
if (parentRow !== null) parentRow.index = parents[n];
|
|
720
|
-
const returned =
|
|
722
|
+
const returned = body(row.value, upContext, input);
|
|
721
723
|
if (returned !== undefined) noPromise(returned);
|
|
722
724
|
}
|
|
723
725
|
};
|
|
@@ -748,9 +750,10 @@ export const createRunner = (state: State, leaf: Leaf, since = 0): Runner => {
|
|
|
748
750
|
}
|
|
749
751
|
const row = bindRow(archetype);
|
|
750
752
|
const { value } = row;
|
|
753
|
+
const { body } = system;
|
|
751
754
|
for (let n = run.start; n < run.end && !holdings.removed; n++) {
|
|
752
755
|
row.index = run.rows[n];
|
|
753
|
-
const returned =
|
|
756
|
+
const returned = body(value, contextValue, input);
|
|
754
757
|
if (returned !== undefined) noPromise(returned);
|
|
755
758
|
}
|
|
756
759
|
}
|
|
@@ -762,6 +765,7 @@ export const createRunner = (state: State, leaf: Leaf, since = 0): Runner => {
|
|
|
762
765
|
const { entries, stride, words, lengths, blocks } = sorted;
|
|
763
766
|
const contextValue = context.value;
|
|
764
767
|
const input = runInput;
|
|
768
|
+
const { body } = system;
|
|
765
769
|
let current = -1;
|
|
766
770
|
let row: ReturnType<typeof bindRow> | null = null;
|
|
767
771
|
let value: object | null = null;
|
|
@@ -782,7 +786,7 @@ export const createRunner = (state: State, leaf: Leaf, since = 0): Runner => {
|
|
|
782
786
|
current = owner;
|
|
783
787
|
}
|
|
784
788
|
row.index = entries[at + 2];
|
|
785
|
-
const returned =
|
|
789
|
+
const returned = body(value, contextValue, input);
|
|
786
790
|
if (returned !== undefined) noPromise(returned);
|
|
787
791
|
}
|
|
788
792
|
}
|
|
@@ -1343,7 +1347,7 @@ export const createRunner = (state: State, leaf: Leaf, since = 0): Runner => {
|
|
|
1343
1347
|
return taken;
|
|
1344
1348
|
},
|
|
1345
1349
|
didRun: () => ran && (query === undefined || bodyRanOnRow()),
|
|
1346
|
-
restart: declaration.
|
|
1350
|
+
restart: declaration.readonly !== true || !holdsNothing(holdings) ? null : since => {
|
|
1347
1351
|
lastRun = since;
|
|
1348
1352
|
holdings.removed = false;
|
|
1349
1353
|
holdings.released = false;
|
|
@@ -59,7 +59,7 @@ const fractions = System.create({ kernel: "gpu", components: { with: [scale, val
|
|
|
59
59
|
});
|
|
60
60
|
const fractionsOf = async (world: ReturnType<typeof createWorld>): Promise<readonly (readonly number[])[]> => {
|
|
61
61
|
const read: [number, number[]][] = [];
|
|
62
|
-
await world.run(System.create({
|
|
62
|
+
await world.run(System.create({ readonly: true, components: { with: [value, copied, product, sum, compound] } }, row => { read.push([row[value], [row[copied], row[product], row[sum], row[compound], row[value]]]); }));
|
|
63
63
|
return read.sort((a, b) => a[0] - b[0]).map(([, values]) => values);
|
|
64
64
|
};
|
|
65
65
|
|
|
@@ -71,7 +71,7 @@ const device = async (): Promise<GPUDevice> => {
|
|
|
71
71
|
|
|
72
72
|
const results = async (world: ReturnType<typeof createWorld>): Promise<readonly string[]> => {
|
|
73
73
|
const read: [number, string][] = [];
|
|
74
|
-
await world.run(System.create({
|
|
74
|
+
await world.run(System.create({ readonly: true, components: { with: [value, out, steps] } }, row => { read.push([row[value], `${row[out]} ${row[steps]}`]); }));
|
|
75
75
|
return read.sort((a, b) => a[0] - b[0]).map(([, text]) => text);
|
|
76
76
|
};
|
|
77
77
|
|
|
@@ -62,7 +62,7 @@ const wideOnGpu = System.create({ kernel: "gpu", components: { with: [wide, out]
|
|
|
62
62
|
/** Every row's results, by its value. */
|
|
63
63
|
const results = async (world: ReturnType<typeof createWorld>): Promise<readonly string[]> => {
|
|
64
64
|
const read: [number, string][] = [];
|
|
65
|
-
await world.run(System.create({
|
|
65
|
+
await world.run(System.create({ readonly: true, components: { with: [value, out, steps] } }, row => { read.push([row[value], `${row[out]} ${row[steps]}`]); }));
|
|
66
66
|
return read.sort((a, b) => a[0] - b[0]).map(([, text]) => text);
|
|
67
67
|
};
|
|
68
68
|
|
|
@@ -98,7 +98,7 @@ describe("a kernel reading a shared component", () => {
|
|
|
98
98
|
await world.run(spawn);
|
|
99
99
|
expect((await runReported(world, moving)).failures).toEqual([]);
|
|
100
100
|
const scales: number[] = [];
|
|
101
|
-
await world.run(System.create({
|
|
101
|
+
await world.run(System.create({ readonly: true, components: { with: [scale] } }, row => { scales.push(row[scale]); }));
|
|
102
102
|
expect(new Set(scales)).toEqual(new Set([2]));
|
|
103
103
|
// A kernel built without knowing a component is shared, naming it in a command, fails where it registers.
|
|
104
104
|
const description = kernelOf(compute);
|