@adobe/ecs 0.2.4 → 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.
Files changed (120) hide show
  1. package/dist/implementation/compiler/augmentations.js +8 -1
  2. package/dist/implementation/compiler/augmentations.js.map +1 -1
  3. package/dist/implementation/compiler/inferred.js +3 -2
  4. package/dist/implementation/compiler/inferred.js.map +1 -1
  5. package/dist/implementation/compiler/type-schemas.d.ts +1 -0
  6. package/dist/implementation/compiler/type-schemas.js +35 -2
  7. package/dist/implementation/compiler/type-schemas.js.map +1 -1
  8. package/dist/implementation/world/activate.d.ts +14 -3
  9. package/dist/implementation/world/activate.js +19 -13
  10. package/dist/implementation/world/activate.js.map +1 -1
  11. package/dist/implementation/world/commands.js +3 -2
  12. package/dist/implementation/world/commands.js.map +1 -1
  13. package/dist/implementation/world/create.js +4 -4
  14. package/dist/implementation/world/create.js.map +1 -1
  15. package/dist/implementation/world/execute.js +97 -11
  16. package/dist/implementation/world/execute.js.map +1 -1
  17. package/dist/implementation/world/finalizers.d.ts +10 -2
  18. package/dist/implementation/world/finalizers.js +42 -13
  19. package/dist/implementation/world/finalizers.js.map +1 -1
  20. package/dist/implementation/world/program.js +5 -1
  21. package/dist/implementation/world/program.js.map +1 -1
  22. package/dist/implementation/world/state.d.ts +2 -0
  23. package/dist/implementation/world/state.js +9 -0
  24. package/dist/implementation/world/state.js.map +1 -1
  25. package/dist/implementation/world/synchronize.js +16 -9
  26. package/dist/implementation/world/synchronize.js.map +1 -1
  27. package/dist/implementation/world/tables.d.ts +2 -1
  28. package/dist/implementation/world/tables.js +55 -2
  29. package/dist/implementation/world/tables.js.map +1 -1
  30. package/dist/interface/finalizer/body.d.ts +24 -3
  31. package/dist/interface/finalizer/create.d.ts +3 -3
  32. package/dist/interface/finalizer/create.js +43 -11
  33. package/dist/interface/finalizer/create.js.map +1 -1
  34. package/dist/interface/finalizer/declaration.d.ts +25 -5
  35. package/dist/interface/registry/index.d.ts +1 -1
  36. package/dist/interface/registry/index.js +1 -1
  37. package/dist/interface/registry/index.js.map +1 -1
  38. package/dist/interface/registry/names.d.ts +4 -0
  39. package/dist/interface/registry/names.js +20 -0
  40. package/dist/interface/registry/names.js.map +1 -1
  41. package/dist/interface/registry/schedule.d.ts +1 -0
  42. package/dist/interface/registry/schedule.js +86 -11
  43. package/dist/interface/registry/schedule.js.map +1 -1
  44. package/dist/interface/system/check.js +4 -0
  45. package/dist/interface/system/check.js.map +1 -1
  46. package/dist/interface/system/commands.d.ts +8 -3
  47. package/dist/interface/system/create-resolved.js +2 -1
  48. package/dist/interface/system/create-resolved.js.map +1 -1
  49. package/dist/interface/system/create.js +2 -2
  50. package/dist/interface/system/create.js.map +1 -1
  51. package/dist/interface/system/declaration.d.ts +11 -4
  52. package/dist/interface/system/describe.js +1 -1
  53. package/dist/interface/system/describe.js.map +1 -1
  54. package/dist/interface/system/leaf.d.ts +4 -3
  55. package/dist/interface/system/render.js +2 -2
  56. package/dist/interface/system/render.js.map +1 -1
  57. package/dist/interface/system/resolve.d.ts +12 -3
  58. package/dist/interface/system/resolve.js +35 -4
  59. package/dist/interface/system/resolve.js.map +1 -1
  60. package/package.json +1 -1
  61. package/src/guide/01-resources.md +4 -3
  62. package/src/guide/03-systems.out +1 -1
  63. package/src/guide/05-scheduling.md +12 -6
  64. package/src/guide/05-scheduling.out +1 -0
  65. package/src/guide/05-scheduling.ts +17 -0
  66. package/src/guide/06-sync-points.md +14 -6
  67. package/src/guide/06-sync-points.out +2 -0
  68. package/src/guide/06-sync-points.ts +15 -0
  69. package/src/guide/10-finalizers.md +5 -0
  70. package/src/guide/18-identity.out +1 -1
  71. package/src/guide/guide.test.ts +4 -4
  72. package/src/implementation/compiler/augmentations.test.ts +28 -1
  73. package/src/implementation/compiler/augmentations.ts +5 -1
  74. package/src/implementation/compiler/inferred.ts +3 -2
  75. package/src/implementation/compiler/transform.test.ts +4 -4
  76. package/src/implementation/compiler/type-schemas.ts +28 -2
  77. package/src/implementation/world/activate.ts +24 -11
  78. package/src/implementation/world/commands.ts +3 -2
  79. package/src/implementation/world/create.ts +4 -4
  80. package/src/implementation/world/defaults.test.ts +24 -1
  81. package/src/implementation/world/despawn-where.test.ts +31 -8
  82. package/src/implementation/world/execute.ts +98 -9
  83. package/src/implementation/world/finalizers.ts +43 -14
  84. package/src/implementation/world/lifetime.test-helper.ts +1 -1
  85. package/src/implementation/world/passes.test.ts +61 -0
  86. package/src/implementation/world/perf.md +9 -0
  87. package/src/implementation/world/program.ts +4 -1
  88. package/src/implementation/world/run-results.test.ts +17 -0
  89. package/src/implementation/world/schedule.test.ts +42 -1
  90. package/src/implementation/world/state.ts +8 -0
  91. package/src/implementation/world/sync-points.test.ts +12 -0
  92. package/src/implementation/world/synchronize.ts +15 -9
  93. package/src/implementation/world/tables.test.ts +36 -3
  94. package/src/implementation/world/tables.ts +48 -3
  95. package/src/implementation/world/watch-traits-late.test-helper.ts +14 -0
  96. package/src/implementation/world/watch-traits.test-helper.ts +10 -0
  97. package/src/implementation/world/watch-traits.test.ts +62 -0
  98. package/src/interface/finalizer/body.ts +22 -3
  99. package/src/interface/finalizer/create.test.ts +4 -1
  100. package/src/interface/finalizer/create.ts +40 -12
  101. package/src/interface/finalizer/declaration.ts +30 -5
  102. package/src/interface/registry/index.ts +1 -1
  103. package/src/interface/registry/names.ts +19 -0
  104. package/src/interface/registry/schedule.ts +89 -16
  105. package/src/interface/rule/rule.type-test.ts +23 -1
  106. package/src/interface/system/check.ts +2 -0
  107. package/src/interface/system/commands.ts +8 -3
  108. package/src/interface/system/create-resolved.ts +2 -1
  109. package/src/interface/system/create.test.ts +3 -3
  110. package/src/interface/system/create.ts +2 -2
  111. package/src/interface/system/declaration.ts +10 -4
  112. package/src/interface/system/describe.test.ts +19 -0
  113. package/src/interface/system/describe.ts +1 -1
  114. package/src/interface/system/leaf.ts +4 -3
  115. package/src/interface/system/render.ts +2 -2
  116. package/src/interface/system/resolve.test.ts +17 -1
  117. package/src/interface/system/resolve.ts +40 -5
  118. package/src/interface/system/system.type-test.ts +6 -0
  119. package/src/perftest/benchmarks.ts +2 -0
  120. package/src/perftest/chained.ts +108 -0
@@ -11,7 +11,7 @@ import type { Body } from './body.js';
11
11
  import type { ReturnsNoPromise } from './input.js';
12
12
  import { accessorsOf } from '../gpu/accessors.js';
13
13
  import { checkLeaf } from './check.js';
14
- import { accessOf, compact, resourceUses } from './resolve.js';
14
+ import { accessOf, compact, noCommands, resourceUses } from './resolve.js';
15
15
  import type { Leaf } from './leaf.js';
16
16
  import type { StructComponent } from './struct.js';
17
17
  import type { Shared } from '../component/value.js';
@@ -96,7 +96,7 @@ export const render = <const D extends RenderDeclaration, B extends Body<D>>(
96
96
  },
97
97
  input: undefined,
98
98
  body,
99
- commands: undefined,
99
+ commands: noCommands,
100
100
  };
101
101
  if (uses.write.length > 0 || uses.reduce.length > 0) throw new Error(`${what} writes or reduces into resources; a render system only reads.`);
102
102
  checkLeaf(system);
@@ -1,7 +1,7 @@
1
1
  // © 2026 Adobe. MIT License. See /LICENSE for details.
2
2
  import { describe, expect, it } from "vitest";
3
3
  import "../registry/declarations.js";
4
- import { componentLists, lookupsLists, resourceUses, type Access } from "./resolve.js";
4
+ import { commandListsOf, componentLists, lookupsLists, resourceUses, type Access } from "./resolve.js";
5
5
 
6
6
  const mass = Symbol("resolveTest/mass");
7
7
  const pool = Symbol("resolveTest/pool");
@@ -47,3 +47,19 @@ describe("resourceUses", () => {
47
47
  expect(() => resourceUses("System \"s\"", changing(pool), undefined)).not.toThrow();
48
48
  });
49
49
  });
50
+
51
+ describe("commandListsOf", () => {
52
+ const commanding = (commands: () => unknown): Access => ({ components: null, resources: null, lookup: null, up: null, commands, at: "a test" });
53
+ it("reads what the commands name at the first call, once", () => {
54
+ let reads = 0;
55
+ const lists = commandListsOf("System \"s\"", commanding(() => { reads++; return { spawn: [[mass, position]], add: [mass], remove: [], despawn: false }; }));
56
+ expect(reads).toBe(0);
57
+ expect([lists(), lists(), reads]).toEqual([{ spawn: [mass, position], add: [mass] }, { spawn: [mass, position], add: [mass] }, 1]);
58
+ });
59
+ it("names the system and the declaration its commands name before it's initialized", () => {
60
+ for (const message of ["Cannot access 'standard' before initialization", "can't access lexical declaration 'standard' before initialization"]) {
61
+ const lists = commandListsOf("System \"s\"", commanding(() => { throw new ReferenceError(message); }));
62
+ expect(lists).toThrow(`System "s" names "standard" in its commands before it's initialized: declare it above the system.`);
63
+ }
64
+ });
65
+ });
@@ -23,7 +23,10 @@ export type Access = {
23
23
  readonly up: readonly Key[] | null;
24
24
  /** The resources the predicates of its declaration's `where` read; absent without predicates. */
25
25
  readonly where?: readonly Key[] | null;
26
- /** What its commands name, keys or lists of them, by kind, and whether it despawns; absent where no build said. */
26
+ /**
27
+ * What its commands name, keys or lists of them, by kind, and whether it despawns, or a function giving that; absent
28
+ * where no build said.
29
+ */
27
30
  readonly commands?: unknown;
28
31
  readonly at: string;
29
32
  };
@@ -35,7 +38,7 @@ const isKeys = (value: unknown): value is readonly Key[] => Array.isArray(value)
35
38
 
36
39
  const isNamed = (value: unknown): value is Named => Array.isArray(value) && value.every(item => typeof item === "symbol" || isKeys(item));
37
40
 
38
- /** A binding a command named that holds neither a key nor a list of them, as one declared below its system, isn't followed. */
41
+ /** A binding a command named that holds neither a key nor a list of them isn't followed. */
39
42
  const isCommanded = (value: unknown): value is Commanded => typeof value === "object" && value !== null
40
43
  && isNamed(Reflect.get(value, "spawn")) && isNamed(Reflect.get(value, "add")) && isNamed(Reflect.get(value, "remove"))
41
44
  && typeof Reflect.get(value, "despawn") === "boolean";
@@ -173,9 +176,28 @@ export const whereReadsOf = (what: string, components: ComponentAccess | undefin
173
176
  return reads;
174
177
  };
175
178
 
176
- /** What a body's commands name, as the world takes it: null where the build couldn't follow them, nothing for none. */
177
- export const commandLists = (access: Access): CommandLists | null | undefined => {
178
- const { commands } = access;
179
+ /**
180
+ * What a body's commands name, as the world takes it, read at the first call and kept: null where the build couldn't
181
+ * follow them, nothing for none. The build gives them as a function, so a list declared below the system is read once
182
+ * its module has loaded; one still uninitialized then, as an import cycle leaves it, throws naming it.
183
+ */
184
+ export const commandListsOf = (what: string, access: Access): (() => CommandLists | null | undefined) => {
185
+ let read = false;
186
+ let lists: CommandLists | null | undefined;
187
+ return () => {
188
+ if (!read) {
189
+ lists = listsOf(what, access.commands);
190
+ read = true;
191
+ }
192
+ return lists;
193
+ };
194
+ };
195
+
196
+ /** The command lists of a system whose body issues none. */
197
+ export const noCommands = (): undefined => undefined;
198
+
199
+ const listsOf = (what: string, given: unknown): CommandLists | null | undefined => {
200
+ const commands = typeof given === "function" ? evaluated(what, () => Reflect.apply(given, undefined, [])) : given;
179
201
  if (commands === undefined) return undefined;
180
202
  if (!isCommanded(commands)) return null;
181
203
  if (!commands.despawn && commands.spawn.length + commands.add.length + commands.remove.length === 0) return undefined;
@@ -184,5 +206,18 @@ export const commandLists = (access: Access): CommandLists | null | undefined =>
184
206
  return Object.keys(lists).length === 0 ? undefined : lists;
185
207
  };
186
208
 
209
+ /** What the build's function of a body's commands gives. */
210
+ const evaluated = (what: string, commands: () => unknown): unknown => {
211
+ try {
212
+ return commands();
213
+ } catch (error) {
214
+ if (!(error instanceof ReferenceError)) throw error;
215
+ // Engines quote the binding in the message: "Cannot access 'standard' before initialization", or "can't access
216
+ // lexical declaration 'standard' before initialization".
217
+ const binding = /'([A-Za-z_$][\w$]*)'/.exec(error.message)?.[1];
218
+ throw new Error(`${what} names ${binding === undefined ? "a declaration" : `"${binding}"`} in its commands before it's initialized: declare it above the system.`, { cause: error });
219
+ }
220
+ };
221
+
187
222
  /** What a body reads of its parent's values. */
188
223
  export const upReads = (what: string, access: Access): readonly Key[] => followed(what, access.up, "its parent's values", access);
@@ -154,6 +154,12 @@ System.create({
154
154
  // @ts-expect-error ordering takes systems
155
155
  after: ["test/unregistered"],
156
156
  }, () => {});
157
+ // Ordering by a resource: before or after every other system of the composite that uses it.
158
+ System.create({ before: [time], after: [integrate, score] }, ({ resources }) => { resources[time] = resources[score]; });
159
+ System.create({
160
+ // @ts-expect-error ordering takes systems and resources, not components
161
+ after: [position],
162
+ }, () => {});
157
163
 
158
164
  // @ts-expect-error a system with input can't use change filters
159
165
  System.create({ components: { changed: [position] } }, (_row, _context, input: { readonly distance: number }) => { void input; });
@@ -36,6 +36,7 @@ import { entitiesWith } from "./observer.js";
36
36
  import { invariantBenchmarks } from "./invariants.js";
37
37
  import { journalBenchmarks } from "./journal.js";
38
38
  import { tableBenchmarks } from "./tables.js";
39
+ import { chainedBenchmarks } from "./chained.js";
39
40
 
40
41
  /** A world whose kernels run on this thread, which the benchmarks without a placement measure; `workers` ones use the pool. */
41
42
  const inline = (): World => createWorld({ [workers]: 0 });
@@ -723,4 +724,5 @@ export const benchmarks: readonly Benchmark[] = [
723
724
  ...invariantBenchmarks,
724
725
  ...journalBenchmarks,
725
726
  ...tableBenchmarks,
727
+ ...chainedBenchmarks,
726
728
  ];
@@ -0,0 +1,108 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import { workers } from "../interface/resources/workers.js";
3
+ import { System } from "../interface/system/system.js";
4
+ import { createWorld } from "../implementation/index.js";
5
+ import type { Benchmark } from "./benchmark.js";
6
+ import { fastest } from "./suite.js";
7
+
8
+ // Eight systems over rows no entity has, each with a command its body never issues, run alone or ordered one after
9
+ // another: what orderings and the sync points they need cost a frame whose commands don't fire.
10
+
11
+ const event = Symbol("chained/event");
12
+ const note = Symbol("chained/note");
13
+ declare module "@adobe/ecs" {
14
+ export interface Components {
15
+ [event]: { type: true };
16
+ [note]: { type: true };
17
+ }
18
+ }
19
+
20
+ /** Never true, so no body issues its command. */
21
+ let firing = false;
22
+ /** Systems in each composite. */
23
+ const length = 8;
24
+ /** Runs in each timed sample. */
25
+ const runs = 100;
26
+
27
+ /** A system spawning, when firing, what each of the composite's systems matches. */
28
+ const commanding = (after: readonly System[]): System => System.create({ components: { with: [event] }, after }, (_row, { commands }) => {
29
+ if (firing) commands.spawn([event])();
30
+ });
31
+ /** A system spawning, when firing, what none of them matches. */
32
+ const noting = (after: readonly System[]): System => System.create({ components: { with: [event] }, after }, (_row, { commands }) => {
33
+ if (firing) commands.spawn([note])();
34
+ });
35
+ const quiet = (after: readonly System[]): System => System.create({ components: { with: [event] }, after }, () => {});
36
+
37
+ /** `length` systems made by `make`, each after the one before when `ordered`. */
38
+ const composite = (make: (after: readonly System[]) => System, ordered: boolean): System => {
39
+ const systems: System[] = [];
40
+ for (let k = 0; k < length; k++) {
41
+ const previous = systems[k - 1];
42
+ systems.push(make(ordered && previous !== undefined ? [previous] : []));
43
+ }
44
+ return System.create(systems);
45
+ };
46
+
47
+ const unordered = composite(commanding, false);
48
+ const chainedQuiet = composite(quiet, true);
49
+ const chainedNoting = composite(noting, true);
50
+ const chainedCommanding = composite(commanding, true);
51
+
52
+ const check = (condition: boolean, message: string): void => {
53
+ if (!condition) throw new Error(message);
54
+ };
55
+
56
+ const timed = (system: System, tiers: number, syncs: number) => async (): Promise<number> => {
57
+ const { tiers: scheduled } = System.describe(system);
58
+ check(scheduled.length === tiers && scheduled.filter(tier => tier.sync !== null).length === syncs, `chained: ${scheduled.length} tiers and ${scheduled.filter(tier => tier.sync !== null).length} sync points`);
59
+ firing = false;
60
+ const world = createWorld({ [workers]: 0 });
61
+ try {
62
+ return await fastest(async () => { for (let k = 0; k < runs; k++) await world.run(system); });
63
+ }
64
+ finally {
65
+ await world.dispose();
66
+ }
67
+ };
68
+
69
+ const runBest = { bytes: 64, basis: "A run reads at least one cache line of its system's state." };
70
+
71
+ export const chainedBenchmarks: readonly Benchmark[] = [
72
+ {
73
+ name: "chained unordered",
74
+ operation: "Running eight systems no entity matches, each with a command it never issues, unordered: one tier, and a sync point at the run's end that applies nothing.",
75
+ per: "runs",
76
+ n: runs,
77
+ typical: [1, 1],
78
+ best: runBest,
79
+ run: timed(unordered, 1, 0),
80
+ },
81
+ {
82
+ name: "chained quiet",
83
+ operation: "Running eight systems no entity matches, without commands, each after the one before: eight tiers and no sync point between them, as orderings that only settle conflicts make.",
84
+ per: "runs",
85
+ n: runs,
86
+ typical: [1, 1],
87
+ best: runBest,
88
+ run: timed(chainedQuiet, length, 0),
89
+ },
90
+ {
91
+ name: "chained noting",
92
+ operation: "Running eight systems no entity matches, each after the one before, each with a command it never issues of what none of them matches: eight tiers, no sync point between them.",
93
+ per: "runs",
94
+ n: runs,
95
+ typical: [1, 1],
96
+ best: runBest,
97
+ run: timed(chainedNoting, length, 0),
98
+ },
99
+ {
100
+ name: "chained commanding",
101
+ operation: "Running eight systems no entity matches, each after the one before, each with a command it never issues of what the next matches: eight tiers and seven sync points that apply nothing.",
102
+ per: "runs",
103
+ n: runs,
104
+ typical: [1, 1],
105
+ best: runBest,
106
+ run: timed(chainedCommanding, length, length - 1),
107
+ },
108
+ ];