@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.
Files changed (218) 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/body-access.d.ts +18 -1
  4. package/dist/implementation/compiler/body-access.js +104 -15
  5. package/dist/implementation/compiler/body-access.js.map +1 -1
  6. package/dist/implementation/compiler/estree.d.ts +5 -2
  7. package/dist/implementation/compiler/estree.js +8 -5
  8. package/dist/implementation/compiler/estree.js.map +1 -1
  9. package/dist/implementation/compiler/inferred.js +5 -2
  10. package/dist/implementation/compiler/inferred.js.map +1 -1
  11. package/dist/implementation/compiler/transform.js +5 -4
  12. package/dist/implementation/compiler/transform.js.map +1 -1
  13. package/dist/implementation/compiler/type-schemas.d.ts +1 -0
  14. package/dist/implementation/compiler/type-schemas.js +35 -2
  15. package/dist/implementation/compiler/type-schemas.js.map +1 -1
  16. package/dist/implementation/world/activate.d.ts +14 -3
  17. package/dist/implementation/world/activate.js +28 -10
  18. package/dist/implementation/world/activate.js.map +1 -1
  19. package/dist/implementation/world/buffer.d.ts +7 -0
  20. package/dist/implementation/world/buffer.js +7 -0
  21. package/dist/implementation/world/buffer.js.map +1 -1
  22. package/dist/implementation/world/cascade.d.ts +10 -3
  23. package/dist/implementation/world/cascade.js +48 -23
  24. package/dist/implementation/world/cascade.js.map +1 -1
  25. package/dist/implementation/world/commands.js +41 -5
  26. package/dist/implementation/world/commands.js.map +1 -1
  27. package/dist/implementation/world/create.js +57 -6
  28. package/dist/implementation/world/create.js.map +1 -1
  29. package/dist/implementation/world/execute.d.ts +4 -4
  30. package/dist/implementation/world/execute.js +228 -93
  31. package/dist/implementation/world/execute.js.map +1 -1
  32. package/dist/implementation/world/finalizers.d.ts +10 -0
  33. package/dist/implementation/world/finalizers.js +60 -10
  34. package/dist/implementation/world/finalizers.js.map +1 -1
  35. package/dist/implementation/world/join.js +12 -2
  36. package/dist/implementation/world/join.js.map +1 -1
  37. package/dist/implementation/world/program.js +5 -1
  38. package/dist/implementation/world/program.js.map +1 -1
  39. package/dist/implementation/world/resource-changes.d.ts +18 -0
  40. package/dist/implementation/world/resource-changes.js +66 -0
  41. package/dist/implementation/world/resource-changes.js.map +1 -0
  42. package/dist/implementation/world/rows.js +21 -16
  43. package/dist/implementation/world/rows.js.map +1 -1
  44. package/dist/implementation/world/run-changes.d.ts +38 -0
  45. package/dist/implementation/world/run-changes.js +138 -0
  46. package/dist/implementation/world/run-changes.js.map +1 -0
  47. package/dist/implementation/world/runner.js +16 -12
  48. package/dist/implementation/world/runner.js.map +1 -1
  49. package/dist/implementation/world/state.d.ts +15 -0
  50. package/dist/implementation/world/state.js +22 -1
  51. package/dist/implementation/world/state.js.map +1 -1
  52. package/dist/implementation/world/synchronize.js +81 -24
  53. package/dist/implementation/world/synchronize.js.map +1 -1
  54. package/dist/implementation/world/tables-read.d.ts +5 -3
  55. package/dist/implementation/world/tables-read.js +16 -4
  56. package/dist/implementation/world/tables-read.js.map +1 -1
  57. package/dist/implementation/world/tables.d.ts +4 -2
  58. package/dist/implementation/world/tables.js +137 -38
  59. package/dist/implementation/world/tables.js.map +1 -1
  60. package/dist/implementation/world/thread-observer.js +1 -1
  61. package/dist/implementation/world/thread-observer.js.map +1 -1
  62. package/dist/implementation/world/thread-systems.js +10 -6
  63. package/dist/implementation/world/thread-systems.js.map +1 -1
  64. package/dist/implementation/world/up.js +7 -1
  65. package/dist/implementation/world/up.js.map +1 -1
  66. package/dist/implementation/world/where.d.ts +8 -0
  67. package/dist/implementation/world/where.js +40 -11
  68. package/dist/implementation/world/where.js.map +1 -1
  69. package/dist/interface/finalizer/body.d.ts +40 -4
  70. package/dist/interface/finalizer/create.d.ts +8 -6
  71. package/dist/interface/finalizer/create.js +45 -10
  72. package/dist/interface/finalizer/create.js.map +1 -1
  73. package/dist/interface/finalizer/declaration.d.ts +26 -4
  74. package/dist/interface/indexes/declaration-of.d.ts +17 -0
  75. package/dist/interface/indexes/declaration-of.js +20 -0
  76. package/dist/interface/indexes/declaration-of.js.map +1 -0
  77. package/dist/interface/indexes/index.d.ts +1 -0
  78. package/dist/interface/indexes/index.js +1 -0
  79. package/dist/interface/indexes/index.js.map +1 -1
  80. package/dist/interface/registry/index.d.ts +1 -1
  81. package/dist/interface/registry/index.js +1 -1
  82. package/dist/interface/registry/index.js.map +1 -1
  83. package/dist/interface/registry/names.d.ts +4 -0
  84. package/dist/interface/registry/names.js +20 -0
  85. package/dist/interface/registry/names.js.map +1 -1
  86. package/dist/interface/registry/schedule.d.ts +29 -1
  87. package/dist/interface/registry/schedule.js +261 -25
  88. package/dist/interface/registry/schedule.js.map +1 -1
  89. package/dist/interface/system/access.d.ts +15 -0
  90. package/dist/interface/system/check.d.ts +7 -0
  91. package/dist/interface/system/check.js +21 -3
  92. package/dist/interface/system/check.js.map +1 -1
  93. package/dist/interface/system/commands.d.ts +31 -5
  94. package/dist/interface/system/create-resolved.js +2 -1
  95. package/dist/interface/system/create-resolved.js.map +1 -1
  96. package/dist/interface/system/create.d.ts +10 -6
  97. package/dist/interface/system/create.js +5 -4
  98. package/dist/interface/system/create.js.map +1 -1
  99. package/dist/interface/system/declaration.d.ts +11 -4
  100. package/dist/interface/system/describe.d.ts +120 -0
  101. package/dist/interface/system/describe.js +116 -0
  102. package/dist/interface/system/describe.js.map +1 -0
  103. package/dist/interface/system/index.d.ts +1 -0
  104. package/dist/interface/system/index.js +1 -0
  105. package/dist/interface/system/index.js.map +1 -1
  106. package/dist/interface/system/leaf.d.ts +7 -0
  107. package/dist/interface/system/render.js +2 -1
  108. package/dist/interface/system/render.js.map +1 -1
  109. package/dist/interface/system/resolve.d.ts +14 -1
  110. package/dist/interface/system/resolve.js +50 -1
  111. package/dist/interface/system/resolve.js.map +1 -1
  112. package/dist/interface/system/shared-only.d.ts +7 -0
  113. package/dist/interface/system/shared-only.js +2 -0
  114. package/dist/interface/system/shared-only.js.map +1 -0
  115. package/dist/interface/system/tables.d.ts +9 -1
  116. package/dist/interface/world/table/index.d.ts +3 -2
  117. package/dist/interface/world/world.d.ts +15 -0
  118. package/package.json +11 -2
  119. package/src/guide/01-resources.md +4 -3
  120. package/src/guide/03-systems.out +1 -1
  121. package/src/guide/05-scheduling.md +12 -6
  122. package/src/guide/05-scheduling.out +1 -0
  123. package/src/guide/05-scheduling.ts +17 -0
  124. package/src/guide/06-sync-points.md +20 -8
  125. package/src/guide/06-sync-points.out +3 -2
  126. package/src/guide/06-sync-points.ts +24 -5
  127. package/src/guide/10-finalizers.md +5 -0
  128. package/src/guide/18-identity.out +1 -1
  129. package/src/guide/guide.test.ts +6 -153
  130. package/src/implementation/compiler/augmentations.test.ts +28 -1
  131. package/src/implementation/compiler/augmentations.ts +5 -1
  132. package/src/implementation/compiler/body-access.ts +102 -12
  133. package/src/implementation/compiler/estree.ts +9 -6
  134. package/src/implementation/compiler/inferred.test.ts +1 -1
  135. package/src/implementation/compiler/inferred.ts +5 -2
  136. package/src/implementation/compiler/transform.test.ts +41 -21
  137. package/src/implementation/compiler/transform.ts +5 -3
  138. package/src/implementation/compiler/type-schemas.ts +28 -2
  139. package/src/implementation/world/activate.ts +32 -10
  140. package/src/implementation/world/batches.test.ts +3 -3
  141. package/src/implementation/world/buffer.ts +7 -0
  142. package/src/implementation/world/cascade.ts +46 -22
  143. package/src/implementation/world/commands.ts +37 -5
  144. package/src/implementation/world/create.test.ts +8 -8
  145. package/src/implementation/world/create.ts +61 -6
  146. package/src/implementation/world/defaults.test.ts +24 -1
  147. package/src/implementation/world/despawn-where.test.ts +310 -0
  148. package/src/implementation/world/execute.ts +222 -86
  149. package/src/implementation/world/finalizers.ts +63 -11
  150. package/src/implementation/world/ids.test.ts +1 -1
  151. package/src/implementation/world/indexes.test.ts +2 -1
  152. package/src/implementation/world/join.test.ts +19 -0
  153. package/src/implementation/world/join.ts +16 -2
  154. package/src/implementation/world/lifetime.test-helper.ts +1 -1
  155. package/src/implementation/world/names.test.ts +1 -2
  156. package/src/implementation/world/passes.test.ts +61 -0
  157. package/src/implementation/world/perf.md +12 -1
  158. package/src/implementation/world/program.ts +4 -1
  159. package/src/implementation/world/resource-changes.test.ts +61 -0
  160. package/src/implementation/world/resource-changes.ts +67 -0
  161. package/src/implementation/world/rows.ts +21 -14
  162. package/src/implementation/world/rules.test.ts +37 -0
  163. package/src/implementation/world/run-changes.test.ts +88 -0
  164. package/src/implementation/world/run-changes.ts +160 -0
  165. package/src/implementation/world/run-results.test.ts +17 -0
  166. package/src/implementation/world/runner.ts +17 -13
  167. package/src/implementation/world/schedule.test.ts +42 -1
  168. package/src/implementation/world/shared.test.ts +2 -2
  169. package/src/implementation/world/state.ts +27 -0
  170. package/src/implementation/world/sync-points.test.ts +147 -0
  171. package/src/implementation/world/synchronize.ts +73 -20
  172. package/src/implementation/world/tables-read.ts +16 -5
  173. package/src/implementation/world/tables.test.ts +139 -4
  174. package/src/implementation/world/tables.ts +118 -35
  175. package/src/implementation/world/thread-observer.ts +1 -1
  176. package/src/implementation/world/thread-systems.ts +10 -6
  177. package/src/implementation/world/up.test.ts +13 -1
  178. package/src/implementation/world/up.ts +6 -1
  179. package/src/implementation/world/watch-traits-late.test-helper.ts +14 -0
  180. package/src/implementation/world/watch-traits.test-helper.ts +10 -0
  181. package/src/implementation/world/watch-traits.test.ts +62 -0
  182. package/src/implementation/world/where.ts +38 -10
  183. package/src/interface/finalizer/body.ts +33 -4
  184. package/src/interface/finalizer/create.test.ts +6 -1
  185. package/src/interface/finalizer/create.ts +45 -12
  186. package/src/interface/finalizer/declaration.ts +31 -4
  187. package/src/interface/indexes/create.test.ts +10 -0
  188. package/src/interface/indexes/declaration-of.ts +34 -0
  189. package/src/interface/indexes/index.ts +1 -0
  190. package/src/interface/registry/index.ts +1 -1
  191. package/src/interface/registry/names.ts +19 -0
  192. package/src/interface/registry/schedule.ts +275 -29
  193. package/src/interface/rule/rule.type-test.ts +23 -1
  194. package/src/interface/system/access.ts +16 -0
  195. package/src/interface/system/check.ts +17 -2
  196. package/src/interface/system/commands.ts +28 -4
  197. package/src/interface/system/commands.type-test.ts +31 -0
  198. package/src/interface/system/create-resolved.ts +2 -1
  199. package/src/interface/system/create.test.ts +10 -2
  200. package/src/interface/system/create.ts +9 -5
  201. package/src/interface/system/declaration.ts +10 -4
  202. package/src/interface/system/describe.test.ts +98 -0
  203. package/src/interface/system/describe.ts +122 -0
  204. package/src/interface/system/index.ts +1 -0
  205. package/src/interface/system/leaf.ts +7 -0
  206. package/src/interface/system/render.ts +2 -1
  207. package/src/interface/system/resolve.test.ts +17 -1
  208. package/src/interface/system/resolve.ts +60 -2
  209. package/src/interface/system/shared-only.ts +5 -0
  210. package/src/interface/system/system.type-test.ts +6 -0
  211. package/src/interface/system/tables.ts +7 -1
  212. package/src/interface/world/run.type-test.ts +11 -1
  213. package/src/interface/world/table/index.ts +3 -2
  214. package/src/interface/world/world.ts +13 -0
  215. package/src/perftest/benchmarks.ts +20 -0
  216. package/src/perftest/chained.ts +108 -0
  217. package/src/perftest/structural.ts +20 -0
  218. package/src/perftest/tables.ts +107 -0
@@ -4,8 +4,8 @@ import type { Definition } from '../system/definition.js';
4
4
  import type { Leaf } from '../system/leaf.js';
5
5
  import { firstItemOf } from './items.js';
6
6
  import { requireDefinition } from './program.js';
7
- import { firstKeyOf, labelOf, sameDeclaration } from './names.js';
8
- import { lookupKeysOf, orderKeysOf } from './indexes.js';
7
+ import { declarationOf, firstKeyOf, labelOf, sameDeclaration } from './names.js';
8
+ import { indexDefinitionOf, lookupKeysOf, orderKeysOf } from './indexes.js';
9
9
 
10
10
  type Key = symbol;
11
11
 
@@ -29,9 +29,15 @@ const checkInputs = (leaves: readonly Leaf[]): void => {
29
29
  /**
30
30
  * Leaves in run order, grouped into tiers. Leaves in one tier have no ordering path between them. `reductions` are the
31
31
  * resources its leaves reduce into, each with how many of them contribute. `observer` are its readonly leaves, in no
32
- * tier: they run after its sync point, ordered with nothing.
32
+ * tier: they run after its sync point, ordered with nothing. `syncs` holds, after each tier, the reasons for a sync
33
+ * point there, which applies the commands so far before the next tier, or null for none.
33
34
  */
34
- export type Schedule = { readonly tiers: readonly (readonly Leaf[])[]; readonly reductions: ReadonlyMap<Key, number>; readonly observer: readonly Leaf[] };
35
+ export type Schedule = {
36
+ readonly tiers: readonly (readonly Leaf[])[];
37
+ readonly reductions: ReadonlyMap<Key, number>;
38
+ readonly observer: readonly Leaf[];
39
+ readonly syncs: readonly (readonly SyncReason[] | null)[];
40
+ };
35
41
 
36
42
  export const isLeaf = (system: Definition): system is Leaf => system.kind !== "composite";
37
43
 
@@ -42,7 +48,14 @@ export const isObserver = (system: Definition): system is Leaf & { readonly decl
42
48
  const start = (unit: number): number => 2 * unit;
43
49
  const end = (unit: number): number => 2 * unit + 1;
44
50
 
45
- type Graph = { readonly units: readonly Definition[]; readonly successors: readonly Set<number>[] };
51
+ type Graph = {
52
+ readonly units: readonly Definition[];
53
+ readonly successors: readonly Set<number>[];
54
+ /** Each unit's members, by unit: none for a leaf. */
55
+ readonly members: readonly (readonly number[])[];
56
+ /** The units a list of systems names, readonly systems and resources left out. */
57
+ readonly resolve: (systems: readonly unknown[] | undefined) => readonly number[];
58
+ };
46
59
 
47
60
  /**
48
61
  * A leaf runs from its start to its end, and a composite spans its members. `after` and `before` order one system's end
@@ -66,8 +79,9 @@ const graphOf = (root: Definition): Graph => {
66
79
  visit(root);
67
80
  const successors = units.flatMap(() => [new Set<number>(), new Set<number>()]);
68
81
  // A readonly system is ordered with nothing, so constraints naming one are ignored.
69
- const resolve = (systems: readonly object[] | undefined): readonly number[] =>
82
+ const resolve = (systems: readonly unknown[] | undefined): readonly number[] =>
70
83
  (systems ?? []).flatMap(system => {
84
+ if (typeof system !== "object" || system === null) return [];
71
85
  const unit = indexOf.get(firstItemOf(system));
72
86
  return unit === undefined || isObserver(units[unit]) ? [] : [unit];
73
87
  });
@@ -86,7 +100,7 @@ const graphOf = (root: Definition): Graph => {
86
100
  successors[end(unit)].add(end(other));
87
101
  }
88
102
  });
89
- return { units, successors };
103
+ return { units, successors, members, resolve };
90
104
  };
91
105
 
92
106
  /** Kahn's algorithm over nodes. Throws on a cycle. */
@@ -136,7 +150,7 @@ const depthsOf = ({ units, successors }: Graph, order: readonly number[]): numbe
136
150
  return depth;
137
151
  };
138
152
 
139
- type Access = { readonly reads: ReadonlySet<Key>; readonly writes: ReadonlySet<Key>; readonly reduces: ReadonlySet<Key> };
153
+ type Access = { readonly reads: ReadonlySet<Key>; readonly writes: ReadonlySet<Key>; readonly reduces: ReadonlySet<Key>; readonly leaf: Leaf };
140
154
 
141
155
  /** The resources a leaf reduces into. */
142
156
  export const reducedBy = (leaf: Leaf): readonly Key[] => {
@@ -155,16 +169,155 @@ export const accessOf = (leaf: Leaf): Access => {
155
169
  for (const key of orderKeysOf(leaf.declaration)) reads.add(firstKeyOf(key));
156
170
  if (up !== undefined) for (const key of [up.through, ...up.read ?? []]) reads.add(firstKeyOf(key));
157
171
  const writes = new Set<Key>([...components?.write ?? [], ...resources?.write ?? []].map(firstKeyOf));
158
- return { reads, writes, reduces: new Set(reducedBy(leaf).map(firstKeyOf)) };
172
+ return { reads, writes, reduces: new Set(reducedBy(leaf).map(firstKeyOf)), leaf };
173
+ };
174
+
175
+ const names = (list: readonly Key[] | undefined, key: Key): boolean => list !== undefined && list.some(item => firstKeyOf(item) === key);
176
+
177
+ /** Whether a leaf reaches a key only through its rows: neither looked up, read through `up`, indexed nor ordered by. */
178
+ const onlyThroughRows = (leaf: Leaf, key: Key): boolean => {
179
+ const { components, lookup, up } = leaf.declaration;
180
+ if (!names(components?.read, key) && !names(components?.write, key) && !names(components?.added, key) && !names(components?.changed, key)) return false;
181
+ for (const name in lookup) if (names(lookup[name].read, key) || names(lookupKeysOf(lookup[name]), key)) return false;
182
+ if (names(orderKeysOf(leaf.declaration), key)) return false;
183
+ return up === undefined || (firstKeyOf(up.through) !== key && !names(up.read, key));
159
184
  };
160
185
 
161
- /** Contributors to one resource don't conflict with each other: their fold doesn't depend on which runs first. */
186
+ /** Whether no entity can match both leaves' rows: one requires a component the other's `not` lists. */
187
+ const exclusive = (a: Leaf, b: Leaf): boolean => {
188
+ const requiresExcluded = (one: Leaf, other: Leaf): boolean => {
189
+ const { components } = one.declaration;
190
+ const not = other.declaration.components?.not;
191
+ if (components === undefined || not === undefined || not.length === 0) return false;
192
+ for (const list of [components.read, components.write, components.has, components.added, components.changed]) {
193
+ for (const key of list ?? []) if (!names(components.optional, firstKeyOf(key)) && names(not, firstKeyOf(key))) return true;
194
+ }
195
+ return false;
196
+ };
197
+ return requiresExcluded(a, b) || requiresExcluded(b, a);
198
+ };
199
+
200
+ /** Whether a conflict on `key` is waived: both reach it only through rows that never match one entity. */
201
+ const waived = (a: Access, b: Access, key: Key): boolean => onlyThroughRows(a.leaf, key) && onlyThroughRows(b.leaf, key) && exclusive(a.leaf, b.leaf);
202
+
203
+ /**
204
+ * The first key two accesses conflict on, one writing it, or undefined. Contributors to one resource don't conflict
205
+ * with each other: their fold doesn't depend on which runs first. Nor do two whose rows never match one entity on a key
206
+ * both reach only through their rows.
207
+ */
162
208
  const conflictOf = (a: Access, b: Access): Key | undefined => {
163
- for (const key of a.writes) if (b.writes.has(key) || b.reads.has(key) || b.reduces.has(key)) return key;
164
- for (const key of b.writes) if (a.reads.has(key) || a.reduces.has(key)) return key;
209
+ for (const key of a.writes) if ((b.writes.has(key) || b.reads.has(key) || b.reduces.has(key)) && !waived(a, b, key)) return key;
210
+ for (const key of b.writes) if ((a.reads.has(key) || a.reduces.has(key)) && !waived(a, b, key)) return key;
165
211
  return undefined;
166
212
  };
167
213
 
214
+ /** Every key two accesses conflict on, as `conflictOf` finds the first. */
215
+ const conflictsOf = (a: Access, b: Access): readonly Key[] => [...new Set([
216
+ ...[...a.writes].filter(key => b.writes.has(key) || b.reads.has(key) || b.reduces.has(key)),
217
+ ...[...b.writes].filter(key => a.reads.has(key) || a.reduces.has(key)),
218
+ ])].filter(key => !waived(a, b, key));
219
+
220
+ /** A system's leaves in its tiers, with their units and accesses, and its graph with readers of folds after their contributors. */
221
+ const orderedOf = (system: Definition) => {
222
+ const graph = graphOf(system);
223
+ const leaves = graph.units.flatMap((unit, index) => isLeaf(unit) && !isObserver(unit) ? [{ leaf: unit, unit: index }] : []);
224
+ const accesses = leaves.map(({ leaf }) => accessOf(leaf));
225
+ // Leaves that read a resource run after every leaf that reduces into it, so they read the finished fold.
226
+ const reductions = new Map<Key, number>();
227
+ accesses.forEach((access, contributor) => {
228
+ for (const key of access.reduces) {
229
+ reductions.set(key, (reductions.get(key) ?? 0) + 1);
230
+ accesses.forEach((other, reader) => {
231
+ if (reader !== contributor && other.reads.has(key)) graph.successors[end(leaves[contributor].unit)].add(start(leaves[reader].unit));
232
+ });
233
+ }
234
+ });
235
+ return { graph, leaves, accesses, reductions, keyed: orderByResources(graph, leaves, accesses) };
236
+ };
237
+
238
+ const noneKeyed: ReadonlyMap<string, "before" | "after"> = new Map();
239
+
240
+ const listsResource = (list: readonly unknown[] | undefined): boolean => list !== undefined && list.some(item => typeof item === "symbol");
241
+
242
+ /** The resources a list of `before` or `after` names, each by its first symbol. */
243
+ const resourcesIn = (list: readonly unknown[] | undefined): readonly Key[] =>
244
+ (list ?? []).flatMap(item => typeof item === "symbol" ? [firstKeyOf(item)] : []);
245
+
246
+ /**
247
+ * Orders each leaf naming a resource in `before` or `after` before or after every other leaf using it, adding the edges
248
+ * to `graph`: but one its constraints already order the other way, and one naming the resource in the same list. By
249
+ * unit, as `"<earlier> <later>"`, the list that ordered each pair.
250
+ */
251
+ const orderByResources = (graph: Graph, leaves: readonly { readonly leaf: Leaf; readonly unit: number }[], accesses: readonly Access[]): ReadonlyMap<string, "before" | "after"> => {
252
+ if (!leaves.some(({ leaf }) => listsResource(leaf.declaration.before) || listsResource(leaf.declaration.after))) return noneKeyed;
253
+ const keyed = new Map<string, "before" | "after">();
254
+ const lists = leaves.map(({ leaf }) => ({ before: resourcesIn(leaf.declaration.before), after: resourcesIn(leaf.declaration.after) }));
255
+ const uses = leaves.map(({ leaf }, index) => new Set([...accesses[index].reads, ...accesses[index].writes, ...accesses[index].reduces,
256
+ ...[...leaf.declaration.resources?.is ?? [], ...leaf.declaration.resources?.not ?? []].map(firstKeyOf)]));
257
+ const reach = reachability(topologicalOrder(graph), graph.successors);
258
+ const edges: (readonly [number, number, "before" | "after"])[] = [];
259
+ leaves.forEach(({ unit }, index) => {
260
+ for (const list of ["before", "after"] as const) {
261
+ for (const key of lists[index][list]) {
262
+ leaves.forEach((other, o) => {
263
+ if (o === index || !uses[o].has(key) || lists[o][list].includes(key)) return;
264
+ const [earlier, later] = list === "before" ? [unit, other.unit] : [other.unit, unit];
265
+ if (!reaches(reach, end(later), start(earlier))) edges.push([earlier, later, list]);
266
+ });
267
+ }
268
+ }
269
+ });
270
+ for (const [earlier, later, list] of edges) {
271
+ graph.successors[end(earlier)].add(start(later));
272
+ keyed.set(`${earlier} ${later}`, list);
273
+ }
274
+ return keyed;
275
+ };
276
+
277
+ /**
278
+ * What put a leaf after another: a constraint of either, or of a composite holding it, naming the other or a composite
279
+ * holding it (`after`, `before`), its reading a fold the other contributes to (`reduction`), or being placed during a
280
+ * span (`during`). `conflicts` are the keys both access, one writing, which need the order.
281
+ */
282
+ export type Placement = { readonly leaf: Leaf; readonly because: "after" | "before" | "during" | "reduction"; readonly conflicts: readonly Key[] };
283
+
284
+ /**
285
+ * For each leaf in a system's tiers, the leaves it follows directly: those with an ordering path to it through no other
286
+ * leaf of its tiers.
287
+ */
288
+ export const placementsOf = (system: Definition): ReadonlyMap<Leaf, readonly Placement[]> => {
289
+ const { graph, leaves, accesses, keyed } = orderedOf(system);
290
+ const reach = reachability(topologicalOrder(graph), graph.successors);
291
+ const ancestors = graph.units.map(() => [] as number[]);
292
+ graph.members.forEach((members, unit) => {
293
+ const visit = (member: number): void => {
294
+ ancestors[member].push(unit);
295
+ for (const inner of graph.members[member]) visit(inner);
296
+ };
297
+ for (const member of members) visit(member);
298
+ });
299
+ /** Whether `systems` names `unit` or a composite holding it. */
300
+ const names = (systems: readonly unknown[] | undefined, unit: number): boolean => {
301
+ const named = graph.resolve(systems);
302
+ return named.includes(unit) || ancestors[unit].some(holder => named.includes(holder));
303
+ };
304
+ const constrains = (unit: number, list: "after" | "before", other: number): boolean =>
305
+ [unit, ...ancestors[unit]].some(holder => holder !== 0 && names(graph.units[holder].declaration[list], other));
306
+ const placements = new Map<Leaf, readonly Placement[]>();
307
+ leaves.forEach(({ leaf, unit }, b) => {
308
+ const before = leaves.flatMap((other, a) => a !== b && reaches(reach, end(other.unit), start(unit)) ? [a] : []);
309
+ const direct = before.filter(a => !before.some(other => other !== a && reaches(reach, end(leaves[a].unit), start(leaves[other].unit))));
310
+ placements.set(leaf, direct.map(a => {
311
+ const other = leaves[a];
312
+ const because = constrains(unit, "after", other.unit) ? "after"
313
+ : constrains(other.unit, "before", unit) ? "before"
314
+ : keyed.get(`${other.unit} ${unit}`) ?? ([...accesses[a].reduces].some(key => accesses[b].reads.has(key)) ? "reduction" : "during");
315
+ return { leaf: other.leaf, because, conflicts: conflictsOf(accesses[a], accesses[b]) };
316
+ }));
317
+ });
318
+ return placements;
319
+ };
320
+
168
321
  const schedules = new WeakMap<Definition, Schedule>();
169
322
 
170
323
  /** A system's schedule, created once per system. Throws on a cycle, or on two conflicting leaves with no ordering path between them. */
@@ -179,33 +332,126 @@ export const scheduleOf = (system: Definition): Schedule => {
179
332
  * or on inputs that don't merge.
180
333
  */
181
334
  const createSchedule = (system: Definition): Schedule => {
182
- const graph = graphOf(system);
335
+ const { graph, leaves, accesses, reductions } = orderedOf(system);
183
336
  const observer = graph.units.filter(isObserver);
184
- const leaves = graph.units.flatMap((unit, index) => isLeaf(unit) && !isObserver(unit) ? [{ leaf: unit, unit: index }] : []);
185
337
  checkInputs(leaves.map(({ leaf }) => leaf));
186
- const accesses = leaves.map(({ leaf }) => accessOf(leaf));
187
- // Leaves that read a resource run after every leaf that reduces into it, so they read the finished fold.
188
- const reductions = new Map<Key, number>();
189
- accesses.forEach((access, contributor) => {
190
- for (const key of access.reduces) {
191
- reductions.set(key, (reductions.get(key) ?? 0) + 1);
192
- accesses.forEach((other, reader) => {
193
- if (reader !== contributor && other.reads.has(key)) graph.successors[end(leaves[contributor].unit)].add(start(leaves[reader].unit));
194
- });
195
- }
196
- });
197
338
  const order = topologicalOrder(graph);
198
339
  const reach = reachability(order, graph.successors);
199
340
  for (let a = 0; a < leaves.length; a++) {
200
341
  for (let b = a + 1; b < leaves.length; b++) {
201
342
  const key = conflictOf(accesses[a], accesses[b]);
202
343
  if (key !== undefined && !ordered(reach, leaves[a].unit, leaves[b].unit)) {
203
- throw new Error(`Systems "${leaves[a].leaf.name}" and "${leaves[b].leaf.name}" both access "${labelOf(key)}" and one writes it; order them with before or after.`);
344
+ const whole = [leaves[a].leaf, leaves[b].leaf].filter(leaf => leaf.declaration.components?.whole === true)
345
+ .map(leaf => ` "${leaf.name}" takes its row whole (components.whole), so it writes every member.`).join("");
346
+ const rows = onlyThroughRows(leaves[a].leaf, key) && onlyThroughRows(leaves[b].leaf, key) ? ", or, if they never match one entity, add a component of one to the other's not" : "";
347
+ throw new Error(`Systems "${leaves[a].leaf.name}" and "${leaves[b].leaf.name}" both access "${labelOf(key)}" and one writes it; order them with before or after${rows}.${whole}`);
204
348
  }
205
349
  }
206
350
  }
207
351
  const depth = depthsOf(graph, order);
208
- const tiers: Leaf[][] = [];
209
- for (const { leaf, unit } of leaves) (tiers[depth[start(unit)]] ??= []).push(leaf);
210
- return { tiers: tiers.filter(tier => tier !== undefined), reductions, observer };
352
+ const byDepth: Leaf[][] = [];
353
+ for (const { leaf, unit } of leaves) (byDepth[depth[start(unit)]] ??= []).push(leaf);
354
+ const tiers = byDepth.filter(tier => tier !== undefined);
355
+ // A sync point comes between tiers, so a schedule of one tier has none.
356
+ if (tiers.length < 2) return { tiers, reductions, observer, syncs: tiers.map(() => null) };
357
+ const tierOf = new Map<Leaf, number>();
358
+ tiers.forEach((tier, t) => { for (const leaf of tier) tierOf.set(leaf, t); });
359
+ const edges: { readonly lo: number; readonly hi: number; readonly reason: SyncReason }[] = [];
360
+ leaves.forEach((from, a) => {
361
+ const changed = structuralOf(from.leaf);
362
+ if (changed === null) return;
363
+ leaves.forEach((to, b) => {
364
+ if (a === b || !reaches(reach, end(from.unit), start(to.unit))) return;
365
+ const keys = affected(changed, touchesOf(to.leaf));
366
+ if (keys !== null) edges.push({ lo: tierOf.get(from.leaf) ?? 0, hi: (tierOf.get(to.leaf) ?? 0) - 1, reason: { from: from.leaf, to: to.leaf, keys } });
367
+ });
368
+ });
369
+ return { tiers, reductions, observer, syncs: syncsOf(tiers.length, edges) };
370
+ };
371
+
372
+ /**
373
+ * Why a sync point follows a tier: a system in it or before it whose commands may change, structurally, what a system
374
+ * after the sync point that follows it reads, matches, looks up or indexes. `keys` are the components its commands name
375
+ * that the later one uses, none where it despawns, or where its commands or a cascade could change anything.
376
+ */
377
+ export type SyncReason = { readonly from: Leaf; readonly to: Leaf; readonly keys: readonly Key[] };
378
+
379
+ /** The components a leaf's commands may add or remove, by spawn, add or remove; `"any"` once it despawns or isn't followed. */
380
+ const structuralOf = (leaf: Leaf): ReadonlySet<Key> | "any" | null => {
381
+ const commands = leaf.commands();
382
+ if (commands === undefined) return null;
383
+ if (commands === null || commands.despawn === true) return "any";
384
+ const keys = new Set([...commands.spawn ?? [], ...commands.add ?? [], ...commands.remove ?? []].map(firstKeyOf));
385
+ // A reference written may despawn its holder, or remove itself, at the sync point.
386
+ for (const key of keys) if (declarationOf(key)?.reference !== undefined) return "any";
387
+ return keys.size === 0 ? null : keys;
388
+ };
389
+
390
+ /**
391
+ * The components a leaf's rows, lookups, indexes and order name, which commands could change its view of; `any` when
392
+ * one of them matches every entity, `cascaded` when it names a component a rule derives or `within` removes.
393
+ */
394
+ const touchesOf = (leaf: Leaf): { readonly keys: ReadonlySet<Key>; readonly any: boolean; readonly cascaded: boolean } => {
395
+ const { components, lookup, up, order } = leaf.declaration;
396
+ const keys = new Set<Key>();
397
+ let any = false;
398
+ const add = (list: readonly Key[] | undefined): void => { for (const key of list ?? []) keys.add(firstKeyOf(key)); };
399
+ /** Names what a query requires; one requiring nothing matches every entity. */
400
+ const requires = (required: readonly (readonly Key[] | undefined)[], others: readonly (readonly Key[] | undefined)[]): void => {
401
+ if (required.every(list => list === undefined || list.length === 0)) any = true;
402
+ for (const list of [...required, ...others]) add(list);
403
+ };
404
+ const indexed = (index: unknown): void => {
405
+ const declaration = indexDefinitionOf(index)?.declaration;
406
+ if (declaration !== undefined) requires([declaration.key, declaration.components?.has], [declaration.components?.not]);
407
+ };
408
+ if (components !== undefined) {
409
+ requires([components.read, components.write, components.has, components.added, components.changed], [components.optional, components.not]);
410
+ add(Object.getOwnPropertySymbols(components.where ?? {}));
411
+ }
412
+ for (const name in lookup) {
413
+ const used = lookup[name];
414
+ if ("index" in used && used.index !== undefined) {
415
+ indexed(used.index);
416
+ add(used.read);
417
+ }
418
+ else if (!("index" in used)) requires([used.read, used.has], [used.optional, used.not, Array.isArray(used.changed) ? used.changed : undefined]);
419
+ }
420
+ if (order !== undefined) indexed(order);
421
+ if (up !== undefined) add([up.through, ...up.read ?? []]);
422
+ const cascaded = [...keys].some(key => { const declared = declarationOf(key); return declared?.derived === true || declared?.within !== undefined; });
423
+ return { keys, any, cascaded };
424
+ };
425
+
426
+ /** The keys through which `changed` may change what `touches` names; null where it can't. */
427
+ const affected = (changed: ReadonlySet<Key> | "any", touches: ReturnType<typeof touchesOf>): readonly Key[] | null => {
428
+ if (touches.keys.size === 0 && !touches.any) return null;
429
+ if (changed === "any") return [];
430
+ if (touches.any || touches.cascaded) return [...changed];
431
+ const keys = [...changed].filter(key => touches.keys.has(key));
432
+ return keys.length === 0 ? null : keys;
433
+ };
434
+
435
+ /**
436
+ * The fewest sync points that put one between the tiers of each edge, after a tier from `lo` to `hi`: each placed as
437
+ * late as the first edge it must cover allows, so it covers every edge open then. An edge's reason goes to the first
438
+ * sync point within its tiers.
439
+ */
440
+ const syncsOf = (count: number, edges: readonly { readonly lo: number; readonly hi: number; readonly reason: SyncReason }[]): readonly (readonly SyncReason[] | null)[] => {
441
+ const syncs: (SyncReason[] | null)[] = new Array<SyncReason[] | null>(count).fill(null);
442
+ let last = -1;
443
+ for (const { lo, hi } of [...edges].sort((a, b) => a.hi - b.hi)) {
444
+ if (last >= lo) continue;
445
+ last = hi;
446
+ syncs[hi] = [];
447
+ }
448
+ for (const { lo, hi, reason } of edges) {
449
+ for (let t = lo; t <= hi; t++) {
450
+ const reasons = syncs[t];
451
+ if (reasons === null) continue;
452
+ reasons.push(reason);
453
+ break;
454
+ }
455
+ }
456
+ return syncs;
211
457
  };
@@ -7,7 +7,7 @@ import { System } from '../system/system.js';
7
7
  import type { Create } from '../world/create.js';
8
8
  import type { World } from '../world/world.js';
9
9
  import { Rule } from './rule.js';
10
- import type { Writable } from '../component/value.js';
10
+ import type { Essential, Writable } from '../component/value.js';
11
11
  import type { ChangeSet } from '../finalizer/body.js';
12
12
 
13
13
  type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
@@ -18,6 +18,9 @@ const hidden = Symbol("ruleTypeTest/hidden");
18
18
  const slot = Symbol("ruleTypeTest/slot");
19
19
  const parent = Symbol("ruleTypeTest/parent");
20
20
  const time = Symbol("ruleTypeTest/time");
21
+ const inDocument = Symbol("ruleTypeTest/inDocument");
22
+ const kept = Symbol("ruleTypeTest/kept");
23
+ const saved = Symbol("ruleTypeTest/saved");
21
24
 
22
25
  declare module '@adobe/ecs' {
23
26
  export interface Components {
@@ -25,9 +28,14 @@ declare module '@adobe/ecs' {
25
28
  [hidden]: { type: true };
26
29
  [slot]: { type: number; derived: true };
27
30
  [parent]: { type: Entity; reference: "owner" };
31
+ [kept]: { type: number; [inDocument]: true };
28
32
  }
29
33
  export interface Resources {
30
34
  [time]: { type: number };
35
+ [saved]: { type: number; [inDocument]: true };
36
+ }
37
+ export interface Traits<N> {
38
+ readonly [inDocument]: N extends unknown ? true : never;
31
39
  }
32
40
  }
33
41
 
@@ -81,6 +89,20 @@ Finalizer.changes({ watch: [source, parent] }, (changes, { commands }) => {
81
89
  // @ts-expect-error changes finalizers don't watch derived components
82
90
  Finalizer.changes({ watch: [slot] }, () => {});
83
91
 
92
+ // A trait of your own watches every component and resource declaring it.
93
+ Finalizer.changes({ watch: [inDocument, source] }, changes => {
94
+ check<Equal<typeof changes, ChangeSet<typeof kept | typeof source, typeof saved>>>();
95
+ });
96
+ // Every component, and the resources chosen, by key or trait.
97
+ Finalizer.changes({ watch: { components: true, resources: [time, inDocument] } }, changes => {
98
+ check<Equal<typeof changes, ChangeSet<Essential, typeof time | typeof saved>>>();
99
+ });
100
+ Finalizer.changes({ watch: { components: [inDocument], resources: [] } }, changes => {
101
+ check<Equal<typeof changes, ChangeSet<typeof kept>>>();
102
+ });
103
+ // @ts-expect-error components lists components and traits, resources resources and traits
104
+ Finalizer.changes({ watch: { components: [time], resources: [source] } }, () => {});
105
+
84
106
  // References change only through commands, so systems can't write them.
85
107
  System.create({ components: { with: [parent, source] } }, row => { row[source] = row[parent]; });
86
108
  System.create({ components: { with: [parent] } }, row => {
@@ -27,6 +27,12 @@ export type ComponentAccess = {
27
27
  * change filters, an archetype it skipped keeps its rows unseen until it passes again.
28
28
  */
29
29
  readonly where?: { readonly [K in Shared]?: Component<K> | ((value: Component<K>, context: { readonly resources: ReadResources }) => boolean) };
30
+ /**
31
+ * Takes its row whole, as a body passing it on does, where the build can't follow how it's used: every member counts
32
+ * as written, for order, conflicts and placement, but references and shared components, which change only through
33
+ * commands, as read. Without it, a body whose row the build can't follow fails the build.
34
+ */
35
+ readonly whole?: true;
30
36
  };
31
37
 
32
38
  /**
@@ -85,6 +91,8 @@ export type ComponentLists = {
85
91
  readonly where?: { readonly [key: symbol]: unknown };
86
92
  /** The resources its `where`'s predicates read, which the build infers: when one changed, they're evaluated again. */
87
93
  readonly whereReads?: readonly Key[];
94
+ /** Whether it takes its row whole, as its declaration said. */
95
+ readonly whole?: true;
88
96
  };
89
97
 
90
98
  /** A lookup as the world takes it, with what its body reads of its rows in `read`. */
@@ -105,3 +113,11 @@ export type ResourceLists = {
105
113
  readonly is?: readonly Key[];
106
114
  readonly not?: readonly Key[];
107
115
  };
116
+
117
+ /** The components a system's commands name, by kind, and whether it despawns, as the build inferred them. */
118
+ export type CommandLists = {
119
+ readonly spawn?: readonly Key[];
120
+ readonly add?: readonly Key[];
121
+ readonly remove?: readonly Key[];
122
+ readonly despawn?: true;
123
+ };
@@ -138,11 +138,23 @@ const checkWhere = (leaf: Leaf): void => {
138
138
  const where = declaration.components?.where;
139
139
  if (where === undefined) return;
140
140
  if (declaration.up !== undefined || declaration.order !== undefined || declaration.target !== undefined) throw new Error(`System "${name}" uses where with up, order or a target, which run rows across archetypes.`);
141
+ checkWhereValues(`System "${name}"`, "where", "a system", where, true);
142
+ };
143
+
144
+ /**
145
+ * Throws for a `where`, the one `named` says of `subject`, which keeps what `keeps` says to archetypes, naming what
146
+ * isn't a shared component or giving one a value not of its type, or a predicate unless `predicates`.
147
+ */
148
+ export const checkWhereValues = (subject: string, named: string, keeps: string, where: { readonly [key: symbol]: unknown }, predicates: boolean): void => {
141
149
  for (const key of Object.getOwnPropertySymbols(where)) {
142
150
  const found = declarationOf(key);
143
- if (kindOf(key) !== "component" || found?.shared !== true) throw new Error(`System "${name}": where names "${labelOf(key)}", which isn't a shared component: where keeps a system to archetypes by their shared values.`);
151
+ if (kindOf(key) !== "component" || found?.shared !== true) throw new Error(`${subject}: ${named} names "${labelOf(key)}", which isn't a shared component: where keeps ${keeps} to archetypes by their shared values.`);
144
152
  const value: unknown = Reflect.get(where, key);
145
- if (typeof value !== "function" && found.type !== undefined && !matchesSchema(found.type, value)) throw new Error(`System "${name}": where gives "${labelOf(key)}" ${labelOf(value)}, which isn't one of its type's values.`);
153
+ if (typeof value === "function") {
154
+ if (!predicates) throw new Error(`${subject}: ${named} gives "${labelOf(key)}" a predicate, and takes only values.`);
155
+ continue;
156
+ }
157
+ if (found.type !== undefined && !matchesSchema(found.type, value)) throw new Error(`${subject}: ${named} gives "${labelOf(key)}" ${labelOf(value)}, which isn't one of its type's values.`);
146
158
  }
147
159
  };
148
160
 
@@ -176,6 +188,7 @@ const checkTables = (leaf: Leaf): void => {
176
188
  requireRegistered("System", name, "component", tables.with);
177
189
  requireRegistered("System", name, "component", tables.not);
178
190
  requireRegistered("System", name, "resource", tables.resources);
191
+ if (tables.where !== undefined) checkWhereValues(`System "${name}"`, "tables' where", "tables", tables.where, false);
179
192
  };
180
193
 
181
194
  /**
@@ -207,6 +220,8 @@ export const checkLeaf = (leaf: Leaf): void => {
207
220
  threadOf(declaration, "System", name);
208
221
  requireRegistered("System", name, "component", components?.has);
209
222
  requireRegistered("System", name, "component", components?.not);
223
+ if (declaration.before !== undefined) requireRegistered("System", name, "resource", declaration.before.filter(item => typeof item === "symbol"));
224
+ if (declaration.after !== undefined) requireRegistered("System", name, "resource", declaration.after.filter(item => typeof item === "symbol"));
210
225
  for (const lookupName in lookup) {
211
226
  const found = lookup[lookupName];
212
227
  requireRegistered("System", name, "component", "has" in found ? found.has : undefined);
@@ -3,6 +3,7 @@ import type { Component } from '../component/component.js';
3
3
  import type { Entity } from '../entity/entity.js';
4
4
  import type { Essential, Shared } from '../component/value.js';
5
5
  import type { Table } from '../world/table/table.js';
6
+ import type { SharedOnly } from './shared-only.js';
6
7
  import type { ValuesOf } from './values-of.js';
7
8
 
8
9
  /** The value an `add` of one component takes: none for a tag. */
@@ -63,6 +64,18 @@ export type Commands = {
63
64
  ...constant: RequiresPreset<C> extends true ? [constant?: P & SharedPreset<C> & Named<C, P>] : [constant?: P & Named<C, P>]
64
65
  ): (...values: SpawnValues<C, P>) => Entity;
65
66
  despawn(entity: Entity): void;
67
+ /**
68
+ * Despawns, as a `despawn` of each would, every entity alive before its sync point that has each component of
69
+ * `with` and none of `not` once the commands issued before it apply, as a row system over the same query despawning
70
+ * each of its rows would; never one the sync point spawns, whenever spawned. Derived components may be named. `where` keeps it to the archetypes holding these values of shared components,
71
+ * each one required as `with` requires it, as a system's `where` of values does. Never the resource entity. Only on
72
+ * the world's thread: a joined thread's throws.
73
+ */
74
+ despawnWhere<const W extends object = {}>(query: {
75
+ readonly with: readonly [Component.Key, ...Component.Key[]];
76
+ readonly not?: readonly Component.Key[];
77
+ readonly where?: W & SharedOnly<NoInfer<W>> & { readonly [K in Shared]?: Component<K> };
78
+ }): void;
66
79
  /** Adds or replaces one component's value, marking it changed. */
67
80
  add<K extends Essential>(entity: Entity, component: K, ...value: ValueArgument<K>): void;
68
81
  /** Adds or replaces the values of `components`, marking them changed. */
@@ -77,9 +90,20 @@ export type Commands = {
77
90
  * outside them isn't spawned, nor anything it owns, through owner references, as despawning its owner would; a
78
91
  * weak reference to one is left out of its row, which is spawned without it; any other value naming one is
79
92
  * `Entity.none`. Returns, at once, each table's new entities, alive at the sync point. A column's stride must be its
80
- * component's in this world, and its length its table's count. `resources` sets each resource it names, by key, at
81
- * the sync point, as commands apply, each value checked against its resource's schema; not one only the world
82
- * writes, nor one living on another thread. On the world's thread only.
93
+ * component's in this world, and its length its table's count. On the world's thread only.
94
+ * - `existing` maps entities to ones already alive in this world, which stand for them: a row whose entity it maps
95
+ * isn't spawned, its entry in the result is the entity it maps to, and every value naming one it maps names that
96
+ * entity, whether or not it's in the tables. Fails for one that isn't alive.
97
+ * - `update` writes each row `existing` maps onto the entity standing for it, as `commands.add` of its table's
98
+ * components would: adding those the entity lacks, replacing and marking changed those it has, and leaving its
99
+ * other components as they are. Its entity values are rewritten as a spawned row's are, but a reference naming an
100
+ * entity that isn't there, weak or owner, is left out, so the entity's own stands.
101
+ * - `resources` sets each resource it names, by key, at the sync point, as commands apply, each value checked
102
+ * against its resource's schema; not one only the world writes, nor one living on another thread.
83
103
  */
84
- spawnTables(tables: readonly Table[], resources?: { readonly [key: symbol]: unknown }): Table.Spawned;
104
+ spawnTables(tables: readonly Table[], options?: {
105
+ readonly resources?: { readonly [key: symbol]: unknown };
106
+ readonly existing?: ReadonlyMap<Entity, Entity>;
107
+ readonly update?: boolean;
108
+ }): Table.Spawned;
85
109
  };
@@ -13,6 +13,7 @@ const label = Symbol("commandsTypeTest/label");
13
13
  const player = Symbol("commandsTypeTest/player");
14
14
  const slot = Symbol("commandsTypeTest/slot");
15
15
  const time = Symbol("commandsTypeTest/time");
16
+ const chunk = Symbol("commandsTypeTest/chunk");
16
17
  declare module '@adobe/ecs' {
17
18
  export interface Components {
18
19
  [position]: { type: number };
@@ -20,6 +21,7 @@ declare module '@adobe/ecs' {
20
21
  [label]: { type: string | null };
21
22
  [player]: { type: true };
22
23
  [slot]: { type: number; derived: true };
24
+ [chunk]: { type: number; shared: true };
23
25
  }
24
26
  export interface Resources {
25
27
  [time]: { type: number };
@@ -84,4 +86,33 @@ export const issue = (commands: Commands, entity: Entity): void => {
84
86
  // @ts-expect-error commands never remove a derived component
85
87
  commands.remove(entity, slot);
86
88
  commands.despawn(entity);
89
+
90
+ // `despawnWhere` takes a query of at least one component, derived ones included.
91
+ commands.despawnWhere({ with: [player] });
92
+ commands.despawnWhere({ with: [position, slot], not: [player] });
93
+ commands.despawnWhere({ with: particle });
94
+ // @ts-expect-error `with` names at least one component
95
+ commands.despawnWhere({ with: [] });
96
+ // @ts-expect-error a query names components, not resources
97
+ commands.despawnWhere({ with: [time] });
98
+ // @ts-expect-error in `not` too
99
+ commands.despawnWhere({ with: [player], not: [time] });
100
+ // @ts-expect-error it takes a query, not an entity
101
+ commands.despawnWhere(entity);
102
+ // Its `where` gives values of shared components, as a system's `where` of values does.
103
+ commands.despawnWhere({ with: [position], where: { [chunk]: 3 } });
104
+ const unloaded = { [chunk]: 4 } as const;
105
+ commands.despawnWhere({ with: [position], where: unloaded });
106
+ // @ts-expect-error a value of the component's type
107
+ commands.despawnWhere({ with: [position], where: { [chunk]: "3" } });
108
+ // @ts-expect-error where names only shared components
109
+ commands.despawnWhere({ with: [position], where: { [position]: 3 } });
110
+ // @ts-expect-error where takes values, not predicates
111
+ commands.despawnWhere({ with: [position], where: { [chunk]: (value: number) => value > 3 } });
112
+ const placed = { [position]: 3 } as const;
113
+ // @ts-expect-error where names only shared components, in a constant too
114
+ commands.despawnWhere({ with: [position], where: placed });
115
+ const mixed = { [chunk]: 3, [position]: 3 } as const;
116
+ // @ts-expect-error and in a constant naming a shared one beside it
117
+ commands.despawnWhere({ with: [position], where: mixed });
87
118
  };
@@ -3,6 +3,7 @@ import { nameOf } from '../registry/items.js';
3
3
  import { claimSystem } from '../registry/program.js';
4
4
  import { checkLeaf, inputSchemaOf } from './check.js';
5
5
  import type { Resolved } from './declaration.js';
6
+ import { noCommands } from './resolve.js';
6
7
  import type { Leaf } from './leaf.js';
7
8
  import type { System } from './system.js';
8
9
 
@@ -14,7 +15,7 @@ import type { System } from './system.js';
14
15
  export const createResolved = (declaration: Resolved, body: Leaf["body"], input?: unknown): System<unknown> => {
15
16
  const name = nameOf(declaration, undefined, "system");
16
17
  const schema = inputSchemaOf(name, body, declaration.components === undefined ? 1 : 2, input);
17
- const system: Leaf & System<unknown> = { kind: "system", name, declaration, input: schema, body };
18
+ const system: Leaf & System<unknown> = { kind: "system", name, declaration, input: schema, body, commands: noCommands };
18
19
  checkLeaf(system);
19
20
  claimSystem(system, undefined);
20
21
  return system;