@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
|
@@ -62,6 +62,11 @@ const readTotal = System.create({ readonly: true }, ({ resources }) => resources
|
|
|
62
62
|
const mix = System.create({}, ({ resources }, input: { readonly level: number }) => {
|
|
63
63
|
resources[levels] = [...resources[levels], input.level];
|
|
64
64
|
});
|
|
65
|
+
/** Runs on the thread claiming joinTestAudio, as `mix` does, and despawns by a query, which only the world's thread does. */
|
|
66
|
+
const mixAndClear = System.create({}, ({ resources, commands }) => {
|
|
67
|
+
resources[levels] = [...resources[levels], 0];
|
|
68
|
+
commands.despawnWhere({ with: [marked] });
|
|
69
|
+
});
|
|
65
70
|
/** An observer system recording each total it reads into `into`. */
|
|
66
71
|
const totalsInto = (into: number[]) => System.create({ readonly: true }, ({ resources }) => { into.push(resources[total]); });
|
|
67
72
|
/** An observer system recording how many levels it reads into `into`. */
|
|
@@ -155,6 +160,12 @@ describe("joining", () => {
|
|
|
155
160
|
await remote.dispose();
|
|
156
161
|
await world.dispose();
|
|
157
162
|
});
|
|
163
|
+
it("rejects a run asking for its changes, which only the world's thread records", async () => {
|
|
164
|
+
const { world, remote } = await joined();
|
|
165
|
+
await expect(remote.run(idle, { changes: true })).rejects.toThrow("a run's changes are recorded only on the world's thread");
|
|
166
|
+
await remote.dispose();
|
|
167
|
+
await world.dispose();
|
|
168
|
+
});
|
|
158
169
|
it("rejects a run that failed, naming its system, and a run of a system without an id", async () => {
|
|
159
170
|
const { world, remote } = await joined();
|
|
160
171
|
await expect(remote.run(fail)).rejects.toThrow(/^System "fail" \(.*join\.test\.ts#fail\) failed: joinTest failure$/);
|
|
@@ -216,6 +227,14 @@ describe("the join signal", () => {
|
|
|
216
227
|
await world.run(mix, { level: 1 });
|
|
217
228
|
expect(await eventually(() => heard.includes(1))).toBe(true);
|
|
218
229
|
});
|
|
230
|
+
it("fails a system on a joined thread that despawns by a query, which runs on the world's thread", async () => {
|
|
231
|
+
await using world = createWorld({ [workers]: 0 });
|
|
232
|
+
const channel = new MessageChannel();
|
|
233
|
+
const shared = world.share(channel.port1);
|
|
234
|
+
await using _remote = await join(channel.port2, { threads: ["joinTestAudio"] });
|
|
235
|
+
await shared;
|
|
236
|
+
await expect(world.run(mixAndClear)).rejects.toThrow("commands.despawnWhere runs on the world's thread");
|
|
237
|
+
});
|
|
219
238
|
it("resolves several joins at once, each with its own claims, and each of their observer systems sees the world's next run", async () => {
|
|
220
239
|
await using world = createWorld({ [workers]: 0 });
|
|
221
240
|
const claims: readonly (readonly ("main" | "joinTestAudio")[])[] = [[], ["main"], ["joinTestAudio"]];
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import { asksForChanges, takesInput } from './run-changes.js';
|
|
3
|
+
import type { ChangeSet } from '../../interface/finalizer/body.js';
|
|
4
|
+
import type { Essential } from '../../interface/component/value.js';
|
|
5
|
+
import type { Resource } from '../../interface/resource/resource.js';
|
|
2
6
|
import type { Thread } from '../../interface/thread/thread.js';
|
|
3
7
|
import type { World } from '../../interface/world/world.js';
|
|
4
8
|
import type { Input } from '../../interface/system/input.js';
|
|
5
9
|
import type { System } from '../../interface/system/system.js';
|
|
6
|
-
import { declarationOf, identifiedItems, idOfItem, idOfKey, indexOfLookup, keyOfId, labelOf, requireDefinition, threadOf, threadOfDeclaration, watchedBy, type Definition } from '../../interface/registry/index.js';
|
|
10
|
+
import { declarationOf, definitionOf, identifiedItems, idOfItem, idOfKey, indexOfLookup, keyOfId, labelOf, requireDefinition, threadOf, threadOfDeclaration, watchedBy, type Definition } from '../../interface/registry/index.js';
|
|
7
11
|
import { unsharedKeyOf } from './index-keys.js';
|
|
8
12
|
import { layoutOf, type Views } from '../../interface/registry/layout.js';
|
|
9
13
|
import {
|
|
@@ -681,9 +685,19 @@ export const joinWorld = (port: Parameters<World["share"]>[0], options: { readon
|
|
|
681
685
|
return runNumbered(numberOf(id, true), definition, input, null);
|
|
682
686
|
};
|
|
683
687
|
|
|
688
|
+
function run<I, R>(system: System<I, R>, ...input: Input<NoInfer<I>>): Promise<R>;
|
|
689
|
+
function run<I, R>(system: System<I, R>, ...input: [...Input<NoInfer<I>>, options: { readonly changes: true }]): Promise<ChangeSet<Essential, Resource.Key>>;
|
|
690
|
+
function run(system: System<unknown, unknown>, ...args: readonly unknown[]): Promise<unknown> {
|
|
691
|
+
const definition = definitionOf(system);
|
|
692
|
+
// A system taking input whose input is all it was given runs with it.
|
|
693
|
+
const asked = args.length > 0 && asksForChanges(args[args.length - 1]) && definition !== undefined && !(args.length < 2 && takesInput(definition));
|
|
694
|
+
if (asked) return Promise.reject(new Error(`System "${system.name}": a run's changes are recorded only on the world's thread.`));
|
|
695
|
+
return runOn(system, args);
|
|
696
|
+
}
|
|
697
|
+
|
|
684
698
|
const world: World = {
|
|
685
699
|
// A run gives what its system's body returned, `R` by the system's type.
|
|
686
|
-
run
|
|
700
|
+
run,
|
|
687
701
|
add: system => {
|
|
688
702
|
const definition = requireDefinition(system);
|
|
689
703
|
const leaves = observerLeavesOf(definition);
|
|
@@ -73,7 +73,7 @@ export const freshKernel = (system: System): System => {
|
|
|
73
73
|
const description = kernelOf(system);
|
|
74
74
|
if (leaf === undefined || leaf.kind === "composite" || description === undefined) throw new Error("Not a kernel system.");
|
|
75
75
|
// What `System.create` makes of the same declaration and body, without the id a top-level binding takes.
|
|
76
|
-
const copy: Leaf & System = { kind: leaf.kind, name: leaf.name, declaration: leaf.declaration, input: leaf.input, body: leaf.body };
|
|
76
|
+
const copy: Leaf & System = { kind: leaf.kind, name: leaf.name, declaration: leaf.declaration, input: leaf.input, body: leaf.body, commands: leaf.commands };
|
|
77
77
|
claimSystem(copy, undefined);
|
|
78
78
|
return attachKernel(copy, description);
|
|
79
79
|
};
|
|
@@ -43,8 +43,7 @@ const failOnce = System.create(() => { throw new Error("unnamed"); });
|
|
|
43
43
|
|
|
44
44
|
describe("systems, rules and finalizers of one label", () => {
|
|
45
45
|
it("are distinct in schedules, both run, and are told apart in reports by their ids", async () => {
|
|
46
|
-
|
|
47
|
-
const linked = (after: readonly System[], body: (row: { [value]: number }) => void): System => System.create({ components: { with: [value] }, after }, row => body(row));
|
|
46
|
+
const linked = (after: readonly System[], body: (row: { [value]: number }) => void): System => System.create({ components: { with: [value], whole: true }, after }, row => body(row));
|
|
48
47
|
const a = linked([], row => { ran.push("a"); row[value] += 1; });
|
|
49
48
|
const b = linked([a], row => { ran.push("b"); row[value] += 1; throw new Error("b failed"); });
|
|
50
49
|
const c = linked([b], row => { ran.push(`c${row[value]}`); });
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { System } from "../../interface/system/system.js";
|
|
4
|
+
import { workers } from "../../interface/resources/workers.js";
|
|
5
|
+
import { createWorld } from "./create.js";
|
|
6
|
+
import { runRan, runReported } from "./outcomes.test-helper.js";
|
|
7
|
+
|
|
8
|
+
const seed = Symbol("passesTest/seed");
|
|
9
|
+
const event = Symbol("passesTest/event");
|
|
10
|
+
|
|
11
|
+
declare module "@adobe/ecs" {
|
|
12
|
+
export interface Components {
|
|
13
|
+
[seed]: { type: number };
|
|
14
|
+
[event]: { type: number };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** What the systems below ran, in order. */
|
|
19
|
+
const order: string[] = [];
|
|
20
|
+
let firing = false;
|
|
21
|
+
|
|
22
|
+
const plant = System.create(({ commands }) => { commands.spawn([seed])({ [seed]: 1 }); });
|
|
23
|
+
const emit = System.create({ components: { with: [seed] } }, (row, { commands }) => {
|
|
24
|
+
order.push("emit");
|
|
25
|
+
if (firing) commands.spawn([event])({ [event]: row[seed] });
|
|
26
|
+
});
|
|
27
|
+
const handle = System.create({ components: { with: [event] }, after: [emit] }, row => { order.push(`handle ${row[event]}`); });
|
|
28
|
+
const finish = System.create({ after: [handle] }, () => { order.push("finish"); });
|
|
29
|
+
const chain = System.create([emit, handle, finish]);
|
|
30
|
+
|
|
31
|
+
const fail = System.create({ components: { with: [seed] }, after: [emit] }, () => {
|
|
32
|
+
order.push("fail");
|
|
33
|
+
throw new Error("passesTest: fail");
|
|
34
|
+
});
|
|
35
|
+
const afterFail = System.create({ after: [fail] }, () => { order.push("after fail"); });
|
|
36
|
+
const failing = System.create([emit, fail, afterFail]);
|
|
37
|
+
|
|
38
|
+
describe("tiers on the world's thread", () => {
|
|
39
|
+
it("run one after another past sync points that apply nothing, and apply one with commands before the tier after it", async () => {
|
|
40
|
+
expect(System.describe(chain).tiers.map(tier => tier.sync === null)).toEqual([false, true, true]);
|
|
41
|
+
await using world = createWorld({ [workers]: 0 });
|
|
42
|
+
await world.run(plant);
|
|
43
|
+
order.length = 0;
|
|
44
|
+
firing = false;
|
|
45
|
+
expect((await runRan(world, chain)).systems).toBe(3);
|
|
46
|
+
firing = true;
|
|
47
|
+
expect((await runRan(world, chain)).systems).toBe(3);
|
|
48
|
+
expect(order).toEqual(["emit", "finish", "emit", "handle 1", "finish"]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("stop at a tier whose system fails, skipping those after it", async () => {
|
|
52
|
+
await using world = createWorld({ [workers]: 0 });
|
|
53
|
+
await world.run(plant);
|
|
54
|
+
order.length = 0;
|
|
55
|
+
firing = false;
|
|
56
|
+
const reported = await runReported(world, failing);
|
|
57
|
+
expect([reported.failures.map(failure => failure.message), reported.skipped]).toEqual([["passesTest: fail"], [afterFail.name]]);
|
|
58
|
+
expect(order).toEqual(["emit", "fail"]);
|
|
59
|
+
expect((await runRan(world, failing)).systems).toBe(2);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -220,7 +220,7 @@ Shared components and `where`, against `12e366d`, node: the whole suite three ti
|
|
|
220
220
|
- **A shared value read in a row costs what a column's does:** `shared read 100K` 4.04–4.10 ns a row against 4.24–4.27 for `column read 100K`, the value bound with the row once per archetype. In a kernel it's a parameter of each call: `shared read 100K kernel` 0.40–0.41 against 0.50 for `column read 100K kernel`, one column fewer to stream.
|
|
221
221
|
- **Spawning with a shared constant:** `spawn bulk shared`, one spawner reused, 17.3–18.1 ns a spawn against 33.3–34.3 for `spawn bulk plain`, whose cell is a column; `spawn singles shared`, its spawner found by its constants each call, 313–323 ns against 229 for `spawn singles plain`: about 90 ns a call to find the spawner, a tuple's leaves walked through a tree of its stored leaves (`shared.ts`) and each constant in turn through the shape's tree of spawners, with no text built.
|
|
222
222
|
- **`where` over 1K cells,** 3 × 3 of them in view: `where 1K cells still` 0.49 ns a cell, 0.5 µs a run with nothing evaluated; `where 1K cells moving`, every predicate evaluated again as the camera moves each run, 18.7–19.0 ns a cell, each predicate's resources read once per evaluation.
|
|
223
|
-
- **Many small archetypes:** `small archetypes 1K`, 10K rows over 1K archetypes of one cell each,
|
|
223
|
+
- **Many small archetypes:** `small archetypes 1K`, 10K rows over 1K archetypes of one cell each, 6.8–7.0 ns a row against 3.04 for `one archetype 10K`: about 38 ns an archetype, binding its row, which a shared component's values multiply.
|
|
224
224
|
|
|
225
225
|
Review fixes to shared components and `where`, node, alternating against the commit before each, medians of four to six rounds:
|
|
226
226
|
- **Arrivals precise** (`arrivals.ts`): a system with `where` and change filters reads a ring of the moves since its last run, each archetype moved into owing the point of the one its rows came from, where every arrival made the archetype count from the oldest point of all matches, found by a scan of them. New `where changed 1K cells drifting in view`, every cell in view, 1% of 10K rows moving to the next cell before each run: 123.6 → 69.2 ns a cell; `where changed 1K cells drifting`, 3 × 3 cells in view, 2.0 → 2.8 ns a cell, the ring's 102 entries a run read, about 7 ns each, and cells moved into from culled ones scanned since that one's point; `changed 1K cells drifting`, without `where`, 52.3 → 52.0. `where 1K cells still` and `moving` within noise.
|
|
@@ -229,6 +229,15 @@ Review fixes to shared components and `where`, node, alternating against the com
|
|
|
229
229
|
- **A spawner's guard** against a component given in both calls, one check of each constant's key a spawn: `spawn singles shared` 323 → 323 ns, `spawn bulk shared` 18.0 → 18.1 ns.
|
|
230
230
|
- **Reclaiming** (`reclaim.ts`): an entity walking one cell a run made an archetype, an interned value, a batch and spawner levels per cell, for good: after 20K cells 20,002 archetypes, 185 MB of heap and 9.1 MB of arena, and `one row after 100K cells walked`, a system over one row, 2.76 ms a run against 250 ns before the walk. Reclaimed and reused, after 100K cells: 11 archetypes, 9 values, 9 batches, 18 spawner levels, heap 29.7–29.9 MB from 1K cells on, arena 71,616 B throughout; `one row after 100K cells walked` 250 ns; `walk to a new cell` 10.3 → 2.8 µs a run. Waiting out empty sync points (`emptyFor`), `circle 8 cells`, round 8 cells, each emptied 7 sync points: 2.67–2.69 µs a run at 1, 2 and 4, reclaiming and reusing one archetype a run, 1.00 at 8 and 16, reclaiming nothing; `walk to a new cell` 2.69–2.73 µs at 1 to 4, 2.81 at 8, 3.06 at 16, scanning more candidates; `flicker between 2 cells` 958 ns at all, against 916 before reclaiming, the sync point's scan of its emptied archetypes. 8, so a cell left for up to 7 sync points keeps its archetype. Edges cached to an archetype of shared values are kept on it to clear, but a batch's archetype is checked by its generation instead: an edge object per spawned archetype, allocated between archetypes and their storages, made `where 1K cells moving` 10% slower, its 1K predicates walking archetypes spread over more memory; checked, +1.8%. Worlds without shared components, five rounds against the commit before: within noise (`run idle` 208 → 208 ns, `run observer` 125 → 125, `loop tick idle`, `move 1K`, `main thread 1K`, `sync point one spawn` 708 → 708, `create` +0.5%, `create flat`, `spawn bulk`, `despawn one`, `churn`, `add value`, `remote run`, `joined observer resource`, `joined observer 10K`, `handoff`, `tail prompt` +0.5% over eight rounds, `log replay`, `read run`); `run 50 idle` was +5% a system until the run's check for reclaimed archetypes kept the scan for new ones apart, out of what its runner inlines: 71.2 → 71.2 ns.
|
|
231
231
|
|
|
232
|
+
Sync points and tiers that apply nothing (adobe/data#245), node, alternating eight rounds against `244f952` with the
|
|
233
|
+
new benchmarks added there: eight systems over rows no entity has, each with a command it never issues. `chained
|
|
234
|
+
unordered`, one tier, 538 → 538 ns a run; `chained quiet`, eight tiers by `after` with no commands, 717 → 636;
|
|
235
|
+
`chained noting`, eight tiers whose commands no later system depends on, 723 → 636; `chained commanding`, eight tiers
|
|
236
|
+
and seven sync points, 859 → 713. A tier boundary on the world's thread costs about 14 ns, from 26, the tiers running
|
|
237
|
+
as one pass; a sync point between them with nothing to apply about 11 ns, from 20, its check of what's pending standing
|
|
238
|
+
for its activation, rewinds and publishing. The other node benchmarks are within noise of `244f952` over three
|
|
239
|
+
rounds, and `read run` (+3.7%) and `loop tick add remove again` (+3.9%), which make a schedule each run, over ten.
|
|
240
|
+
|
|
232
241
|
## Where the gains came from
|
|
233
242
|
|
|
234
243
|
Step figures marked (vitest) are from the node vitest lane: read them as relative.
|
|
@@ -241,6 +250,8 @@ Step figures marked (vitest) are from the node vitest lane: read them as relativ
|
|
|
241
250
|
kernel, the second call stayed a call and boxed each entity, a double past 2^32. `despawn bulk kernel` 4.60 → 4.12 ms;
|
|
242
251
|
`despawn bulk` compiled 4.55 ms against 4.32 as written before, 4.10 after.
|
|
243
252
|
- Inline value writes instead of closures, an 8-shape spawn cache, edges indexed by component id, archetypes grown once per sync point: create 38 → 27.6 ms.
|
|
253
|
+
- Holders of references spawned in a sync point are linked a block at a time before the cascade, from their columns, rather than listed as triples for the cascade and for the holder index each; and `commands.spawnTables` lists owners between its rows only when one is dropped, and reserves a table's entities at once when every row spawns: `spawn tables 1M owner` 160 → 53 ms, `spawn owned 1M` 133 → 64 ms, against 10 ms for `spawn tables 1M`. Each holder is journaled once, so an `up` tree builds again past 4096 journal entries and an eighth of its rows, as indexes do: `up load` 183 → 120 ms.
|
|
254
|
+
- A row loop reads the body into a local once, which V8 otherwise loads again each row, since the call could change it; rows bind without allocating, and filters are found by archetype id: `with 100K` 4.24 → 3.97 ns a row, `one archetype 10K` 3.47 → 3.04, `small archetypes 1K` 7.74 → 6.90, `shared read 100K` 4.07 → 3.78.
|
|
244
255
|
|
|
245
256
|
**Change filters.**
|
|
246
257
|
- Chunk ticks, the newest tick per 1024 rows, so filters skip unchanged chunks: clustered sparse to ~0.09 ms.
|
|
@@ -10,5 +10,8 @@ import type { Key } from './state.js';
|
|
|
10
10
|
export const triggersOf = (item: Rule | Finalizer): { readonly keys: readonly Key[]; readonly any: boolean } => {
|
|
11
11
|
if (item.kind === "rule") return { keys: item.declaration.components.has ?? [], any: false };
|
|
12
12
|
if (item.kind === "finalizer") return { keys: [...item.declaration.components.read ?? [], ...item.declaration.components.has ?? []], any: false };
|
|
13
|
-
|
|
13
|
+
const { watch } = item.declaration;
|
|
14
|
+
// Every component, or those declaring a trait, may be met at any sync point.
|
|
15
|
+
if (watch.components === true || watch.componentTraits.length + watch.resourceTraits.length > 0) return { keys: [], any: true };
|
|
16
|
+
return { keys: [...watch.components, ...watch.resources], any: true };
|
|
14
17
|
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import { expect, it } from "vitest";
|
|
3
|
+
import { Finalizer, System } from "../../index.js";
|
|
4
|
+
import { Observe } from "../../observe/index.js";
|
|
5
|
+
import { createWorld } from "../index.js";
|
|
6
|
+
import { sameValue } from "./resource-changes.js";
|
|
7
|
+
|
|
8
|
+
const size = Symbol("resourceChanges/size");
|
|
9
|
+
const corner = Symbol("resourceChanges/corner");
|
|
10
|
+
const unwatched = Symbol("resourceChanges/unwatched");
|
|
11
|
+
const log = Symbol("resourceChanges/log");
|
|
12
|
+
const label = Symbol("resourceChanges/label");
|
|
13
|
+
|
|
14
|
+
declare module "@adobe/ecs" {
|
|
15
|
+
interface Components {
|
|
16
|
+
[label]: { type: string };
|
|
17
|
+
}
|
|
18
|
+
interface Resources {
|
|
19
|
+
[size]: { type: number; default: 1 };
|
|
20
|
+
[corner]: { type: readonly [number, number] };
|
|
21
|
+
[unwatched]: { type: number };
|
|
22
|
+
[log]: { type: readonly string[]; default: [] };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Watching only resources, it sees no component's changes.
|
|
27
|
+
Finalizer.changes({ watch: [size, corner] }, (changes, { resources }) => {
|
|
28
|
+
const lines = changes.resources.map(change => change.key === size ? `size ${change.previous} → ${change.next}` : `corner ${change.previous.join(",")} → ${change.next.join(",")}`);
|
|
29
|
+
resources[log] = [...resources[log], ...changes.groups.map(() => "group"), ...lines];
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const resize = System.create({}, ({ resources }, input: { readonly to: number }) => {
|
|
33
|
+
resources[size] = input.to;
|
|
34
|
+
resources[size] = input.to + 1;
|
|
35
|
+
});
|
|
36
|
+
const move = System.create({}, ({ resources }, input: { readonly x: number }) => { resources[corner] = [input.x, 0]; });
|
|
37
|
+
const touch = System.create({}, ({ resources }) => { resources[unwatched] += 1; });
|
|
38
|
+
const spawn = System.create({}, ({ commands }) => { commands.spawn([label])({ [label]: "a" }); });
|
|
39
|
+
const read = System.create({ readonly: true }, ({ resources }) => resources[log]);
|
|
40
|
+
|
|
41
|
+
it("reports each watched resource written since its finalizers last ran, with its first and last value, once", async () => {
|
|
42
|
+
await using world = createWorld();
|
|
43
|
+
// A run writing nothing else still has a sync point, so its finalizers see the write.
|
|
44
|
+
await world.run(resize, { to: 4 });
|
|
45
|
+
expect(await world.run(read)).toEqual(["size 1 → 5"]);
|
|
46
|
+
await world.run(spawn);
|
|
47
|
+
await world.run(touch);
|
|
48
|
+
expect(await world.run(read)).toEqual(["size 1 → 5"]);
|
|
49
|
+
await world.run(move, { x: 2 });
|
|
50
|
+
// Written back to an equal value, leaf by leaf: not reported.
|
|
51
|
+
await world.run(move, { x: 2 });
|
|
52
|
+
await world.run(resize, { to: 4 });
|
|
53
|
+
expect(await world.run(read)).toEqual(["size 1 → 5", "corner 0,0 → 2,0"]);
|
|
54
|
+
expect(await Observe.toPromise(Observe.resource(world, size))).toBe(5);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("compares data leaf by leaf and other objects by identity", () => {
|
|
58
|
+
const map = new Map();
|
|
59
|
+
expect([sameValue([1, { a: -0 }], [1, { a: -0 }]), sameValue({ a: 1 }, { a: 1, b: 2 }), sameValue(0, -0), sameValue(NaN, NaN), sameValue(map, new Map()), sameValue(map, map)])
|
|
60
|
+
.toEqual([true, false, false, true, false, true]);
|
|
61
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import { readCell } from './cells.js';
|
|
3
|
+
import { indexOf } from './entities.js';
|
|
4
|
+
import type { Storage } from './archetype.js';
|
|
5
|
+
import type { State } from './state.js';
|
|
6
|
+
|
|
7
|
+
/** A watched resource written to a value that isn't equal to the one its finalizers last saw. */
|
|
8
|
+
export type ResourceChange = { readonly id: number; readonly previous: unknown; readonly next: unknown };
|
|
9
|
+
|
|
10
|
+
const noChanges: readonly ResourceChange[] = [];
|
|
11
|
+
|
|
12
|
+
/** Where resource `id` lives now, or null before the world has it. */
|
|
13
|
+
const cellOf = (state: State, id: number): { readonly storage: Storage; readonly row: number } | null => {
|
|
14
|
+
if (state.resourceEntity < 0) return null;
|
|
15
|
+
const index = indexOf(state.resourceEntity);
|
|
16
|
+
const storage = state.archetypes[state.entities.archetype[index]]?.storageById[id];
|
|
17
|
+
return storage === undefined ? null : { storage, row: state.entities.row[index] };
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Starts following resource `id` for a changes finalizer watching it, from its value now. */
|
|
21
|
+
export const watchResource = (state: State, id: number): void => {
|
|
22
|
+
if (state.resourcesSeen.has(id)) return;
|
|
23
|
+
const cell = cellOf(state, id);
|
|
24
|
+
state.resourcesSeen.set(id, cell === null ? { value: state.starts[id], tick: 0 } : { value: readCell(cell.storage, cell.row), tick: cell.storage.changed[cell.row] });
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Whether a watched resource was written since its finalizers last ran, so a sync point is due for them to see it. */
|
|
28
|
+
export const resourcesWritten = (state: State): boolean => {
|
|
29
|
+
for (const [id, seen] of state.resourcesSeen) {
|
|
30
|
+
const cell = cellOf(state, id);
|
|
31
|
+
if (cell !== null && cell.storage.changed[cell.row] > seen.tick) return true;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Each watched resource written since its finalizers last ran, to a value that isn't equal to the one they saw then,
|
|
38
|
+
* with that value and its value now, which they see from now on.
|
|
39
|
+
*/
|
|
40
|
+
export const takeResourceChanges = (state: State): readonly ResourceChange[] => {
|
|
41
|
+
let changes: ResourceChange[] | null = null;
|
|
42
|
+
for (const [id, seen] of state.resourcesSeen) {
|
|
43
|
+
const cell = cellOf(state, id);
|
|
44
|
+
if (cell === null) continue;
|
|
45
|
+
const tick = cell.storage.changed[cell.row];
|
|
46
|
+
if (tick <= seen.tick) continue;
|
|
47
|
+
const next = readCell(cell.storage, cell.row);
|
|
48
|
+
if (!sameValue(seen.value, next)) (changes ??= []).push({ id, previous: seen.value, next });
|
|
49
|
+
state.resourcesSeen.set(id, { value: next, tick });
|
|
50
|
+
}
|
|
51
|
+
return changes ?? noChanges;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const isPlain = (value: object): boolean => {
|
|
55
|
+
const prototype: unknown = Object.getPrototypeOf(value);
|
|
56
|
+
return prototype === Object.prototype || prototype === null;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** Whether two values are equal: data leaf by leaf, as `Object.is` compares leaves, and any other object by identity. */
|
|
60
|
+
export const sameValue = (a: unknown, b: unknown): boolean => {
|
|
61
|
+
if (Object.is(a, b)) return true;
|
|
62
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
63
|
+
if (Array.isArray(a)) return Array.isArray(b) && a.length === b.length && a.every((item, i) => sameValue(item, b[i]));
|
|
64
|
+
if (Array.isArray(b) || !isPlain(a) || !isPlain(b)) return false;
|
|
65
|
+
const keys = Object.keys(a);
|
|
66
|
+
return keys.length === Object.keys(b).length && keys.every(key => Object.hasOwn(b, key) && sameValue(Reflect.get(a, key), Reflect.get(b, key)));
|
|
67
|
+
};
|
|
@@ -12,7 +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 {
|
|
15
|
+
import { tablesReader } from './tables-read.js';
|
|
16
16
|
import { combine, contributed, identityOf, type ReductionTarget } from './reductions.js';
|
|
17
17
|
import { createIndexLookup, type Since } from './index-lookups.js';
|
|
18
18
|
import type { WorldIndex } from './indexes.js';
|
|
@@ -131,13 +131,26 @@ const viewOf = (storage: RowStorage): TypedArray => {
|
|
|
131
131
|
return layout.kind === "linear" && storage.write !== writeGeneral ? storage.views[layout.leaves[0].view] : storage.views.f32;
|
|
132
132
|
};
|
|
133
133
|
|
|
134
|
-
const
|
|
134
|
+
const storageOf = (archetype: RowArchetype, id: number): RowStorage => {
|
|
135
135
|
const storage = archetype.storageById[id];
|
|
136
136
|
// A system matches only archetypes with every component it reads or writes.
|
|
137
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);
|
|
138
143
|
return { storage, view: viewOf(storage), value: constantOf(storage) };
|
|
139
144
|
};
|
|
140
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
|
+
|
|
141
154
|
/** An optional component's getter where the archetype lacks it, and its setter, which fails its system. */
|
|
142
155
|
const absentGetter = (): undefined => undefined;
|
|
143
156
|
const absentSetterOf = (key: Key): (value: unknown) => void => () => {
|
|
@@ -168,11 +181,7 @@ export const createRow = (host: RowHost, archetype: RowArchetype, keys: readonly
|
|
|
168
181
|
row.entities = next.entities;
|
|
169
182
|
for (let i = 0; i < ids.length; i++) {
|
|
170
183
|
const slot = slots[i];
|
|
171
|
-
if (slot
|
|
172
|
-
const { storage, view, value } = slotOf(next, ids[i]);
|
|
173
|
-
slot.storage = storage;
|
|
174
|
-
slot.view = view;
|
|
175
|
-
slot.value = value;
|
|
184
|
+
if (slot !== undefined) bindSlot(slot, next, ids[i]);
|
|
176
185
|
}
|
|
177
186
|
},
|
|
178
187
|
};
|
|
@@ -271,12 +280,7 @@ export const sharedRows = (host: RowHost, keys: readonly Key[], ids: readonly nu
|
|
|
271
280
|
slots: ids.map(id => slotOf(archetype, id)),
|
|
272
281
|
bind: next => {
|
|
273
282
|
row.entities = next.entities;
|
|
274
|
-
for (let i = 0; i < ids.length; i++)
|
|
275
|
-
const { storage, view, value } = slotOf(next, ids[i]);
|
|
276
|
-
row.slots[i].storage = storage;
|
|
277
|
-
row.slots[i].view = view;
|
|
278
|
-
row.slots[i].value = value;
|
|
279
|
-
}
|
|
283
|
+
for (let i = 0; i < ids.length; i++) bindSlot(row.slots[i], next, ids[i]);
|
|
280
284
|
},
|
|
281
285
|
};
|
|
282
286
|
Object.defineProperty(value, rowOfValue, { value: row });
|
|
@@ -511,7 +515,10 @@ export const createContext = (state: State, declaration: Pick<Resolved, "lookup"
|
|
|
511
515
|
const value: { up?: unknown } = own();
|
|
512
516
|
const { tables } = declaration;
|
|
513
517
|
// Each read copies the tables as they are then.
|
|
514
|
-
if (tables !== undefined)
|
|
518
|
+
if (tables !== undefined) {
|
|
519
|
+
const read = tablesReader(state, tables);
|
|
520
|
+
Object.defineProperty(value, "tables", { get: read, enumerable: true });
|
|
521
|
+
}
|
|
515
522
|
const { lookup, resources } = declaration;
|
|
516
523
|
const names = Object.keys(lookup ?? {});
|
|
517
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
|
+
});
|