@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
@@ -3,14 +3,15 @@ import type { Leaf } from '../../interface/system/leaf.js';
3
3
  import type { Chunk, ChunkResult, Runner, Workers } from './runner.js';
4
4
  import type { Schedule } from '../../interface/registry/schedule.js';
5
5
  import type { Archetype, Storage } from './archetype.js';
6
- import { countRan, idOf, startRan, type State } from './state.js';
6
+ import { countRan, countRanHere, idOf, startRan, type State } from './state.js';
7
7
  import type { GpuEngine } from './gpu.js';
8
8
  import { applySync, prepareSync, syncPending } from './synchronize.js';
9
- import { activate } from './activate.js';
9
+ import { resourcesWritten } from './resource-changes.js';
10
+ import { activate, activationPending } from './activate.js';
10
11
  import { markRun } from './move-log.js';
11
12
  import { postTails, settleAll, settleKeys, settleRunners, tailsOf, type TailOutcome } from './tails.js';
12
13
  import { settlePosts, settleObserverKeys, settleObserverRunners } from './joined-observer.js';
13
- import { locate } from './entities.js';
14
+ import { locate, reserved } from './entities.js';
14
15
  import { clearBuffer, createBuffer } from './buffer.js';
15
16
  import { drainSegment, type Segment } from './command-ring.js';
16
17
  import { combine, identityOf, writeReduction, type ReductionTarget } from './reductions.js';
@@ -39,10 +40,10 @@ export type ExecuteContext = {
39
40
  };
40
41
 
41
42
  /**
42
- * Runs a schedule tier by tier, then applies one sync point, and reports what went wrong. Within a tier, kernels start
43
- * first so workers run while main-thread systems do; commands merge in tier order, then chunk order, whichever thread
44
- * ran them. A failing system stops the run after its tier finishes: later tiers are skipped, and commands so far still
45
- * apply. `system` is the one the run was given, which the GPU's errors are reported under.
43
+ * Runs a schedule tier by tier, applying a sync point after each tier its schedule says and after the last, and reports
44
+ * what went wrong. Within a tier, kernels start first so workers run while main-thread systems do; commands merge in
45
+ * tier order, then chunk order, whichever thread ran them. A failing system stops the run after its tier finishes:
46
+ * later tiers are skipped, and commands so far still apply. `system` is the one the run was given, which the GPU's errors are reported under.
46
47
  */
47
48
  export const execute = (context: ExecuteContext, schedule: Schedule, args: unknown, system: { readonly name: string }): RunReport | Promise<RunReport> => {
48
49
  const { gpu } = context.state;
@@ -101,6 +102,18 @@ type Prepared = {
101
102
  readonly remotes: number[];
102
103
  /** The indexes each tier's runners look up through, refreshed before it. */
103
104
  readonly uses: (readonly IndexUse[])[];
105
+ /**
106
+ * By tier, the last tier of the pass starting there: tiers whose systems all run on the world's thread as written,
107
+ * which look up through no index from the second on, run in one pass while their sync points apply nothing. Its own
108
+ * tier for any other.
109
+ */
110
+ readonly passes: number[];
111
+ /**
112
+ * By tier within a pass of several, how many runners it and the pass's later tiers hold, and whether any runs on the
113
+ * GPU: what a pass entered there counts once it ran to its end.
114
+ */
115
+ readonly passSystems: number[];
116
+ readonly passGpu: boolean[];
104
117
  /** The world's `runnersVersion` its runners were read at. */
105
118
  version: number;
106
119
  readonly started: (readonly Chunk[])[][];
@@ -128,6 +141,8 @@ type Prepared = {
128
141
  chunk: number;
129
142
  error: unknown;
130
143
  tierFailed: boolean;
144
+ /** How many commands the run's sync points dropped so far. */
145
+ dropped: number;
131
146
  };
132
147
 
133
148
  /** A schedule's tails: the schedule without them, and their runners, leaves and tiers in schedule order. */
@@ -139,6 +154,7 @@ const tailingOf = (context: ExecuteContext, schedule: Schedule): Tailing | null
139
154
  const leaves: Leaf[] = [];
140
155
  const tiers: number[] = [];
141
156
  const rest: Leaf[][] = [];
157
+ const syncs: (Schedule["syncs"][number])[] = [];
142
158
  schedule.tiers.forEach((tier, t) => {
143
159
  const kept = tier.filter((leaf, i) => {
144
160
  if (!flags[t][i]) return true;
@@ -147,9 +163,13 @@ const tailingOf = (context: ExecuteContext, schedule: Schedule): Tailing | null
147
163
  return false;
148
164
  });
149
165
  if (kept.length > 0) rest.push(kept);
166
+ // A sync point after a tier left empty follows the tier kept before it.
167
+ const reasons = schedule.syncs[t];
168
+ if (reasons !== null && rest.length > 0) syncs[rest.length - 1] = [...syncs[rest.length - 1] ?? [], ...reasons];
150
169
  });
151
170
  if (leaves.length === 0) return null;
152
- return { schedule: { tiers: rest, reductions: schedule.reductions, observer: schedule.observer }, runners: leaves.map(context.runnerOf), leaves, tiers };
171
+ const kept = rest.map((_, t) => t < rest.length - 1 ? syncs[t] ?? null : null);
172
+ return { schedule: { tiers: rest, reductions: schedule.reductions, observer: schedule.observer, syncs: kept }, runners: leaves.map(context.runnerOf), leaves, tiers };
153
173
  };
154
174
 
155
175
  type Outcome = { failures: Failure[] | null; skipped: Leaf[] | null };
@@ -161,10 +181,11 @@ const preparedFor = (context: ExecuteContext, schedule: Schedule): Prepared => {
161
181
  let prepared = bySchedule.get(schedule);
162
182
  if (prepared === undefined) {
163
183
  prepared = {
164
- runners: schedule.tiers.map(tier => tier.map(context.runnerOf)), remotes: schedule.tiers.map(() => 0), uses: schedule.tiers.map(() => []), version: -1,
184
+ runners: schedule.tiers.map(tier => tier.map(context.runnerOf)), remotes: schedule.tiers.map(() => 0), uses: schedule.tiers.map(() => []),
185
+ passes: schedule.tiers.map((_, t) => t), passSystems: schedule.tiers.map(() => 0), passGpu: schedule.tiers.map(() => false), version: -1,
165
186
  started: schedule.tiers.map(tier => new Array<readonly Chunk[]>(tier.length)),
166
187
  systems: [], tiers: [], tasks: new Map(), warnings: [], warningCount: 0, warned: null, outcome: { failures: null, skipped: null },
167
- placement: -1, tailing: null, settling: 0, chunk: 0, error: undefined, tierFailed: false,
188
+ placement: -1, tailing: null, settling: 0, chunk: 0, error: undefined, tierFailed: false, dropped: 0,
168
189
  };
169
190
  bySchedule.set(schedule, prepared);
170
191
  }
@@ -182,9 +203,38 @@ const refresh = (context: ExecuteContext, schedule: Schedule, prepared: Prepared
182
203
  prepared.remotes[t] = remote;
183
204
  prepared.uses[t] = mergeUses(runners);
184
205
  }
206
+ passesOf(schedule, prepared);
185
207
  prepared.version = context.state.runnersVersion;
186
208
  };
187
209
 
210
+ /** Whether every runner of a tier runs on the world's thread as written: none a kernel or on a joined thread. */
211
+ const asWritten = (runners: readonly Runner[]): boolean => {
212
+ for (let i = 0; i < runners.length; i++) if (runners[i].kernel !== null || runners[i].remote !== null) return false;
213
+ return true;
214
+ };
215
+
216
+ /** Finds a schedule's passes (`Prepared["passes"]`) from its runners as read now. */
217
+ const passesOf = (schedule: Schedule, prepared: Prepared): void => {
218
+ const { runners, uses, passes, passSystems, passGpu } = prepared;
219
+ let last = -1;
220
+ for (let t = 0; t < schedule.tiers.length; t++) {
221
+ if (!asWritten(runners[t])) last = t;
222
+ else if (last < t) {
223
+ last = t;
224
+ while (last + 1 < schedule.tiers.length && uses[last + 1].length === 0 && asWritten(runners[last + 1])) last++;
225
+ }
226
+ passes[t] = last;
227
+ }
228
+ for (let t = schedule.tiers.length - 1; t >= 0; t--) {
229
+ const tier = runners[t];
230
+ const continues = passes[t] > t;
231
+ let gpu = continues && passGpu[t + 1];
232
+ for (let i = 0; i < tier.length; i++) if (tier[i].placement !== "cpu") gpu = true;
233
+ passSystems[t] = tier.length + (continues ? passSystems[t + 1] : 0);
234
+ passGpu[t] = gpu;
235
+ }
236
+ };
237
+
188
238
  /**
189
239
  * Drains a worker's segment into a buffer of its own, queued in its place: commands, spawn blocks staged to commit in
190
240
  * its place, and the reservations it left unused. Fails if it doesn't start with its task's merge tag.
@@ -315,8 +365,8 @@ const runFused = async (context: ExecuteContext, schedule: Schedule, prepared: P
315
365
  const { gpu } = state;
316
366
  let last = first;
317
367
  if (prepared.version !== state.runnersVersion) refresh(context, schedule, prepared);
318
- // A tier using an index starts a task of its own, after the index is refreshed for it.
319
- while (last + 1 < schedule.tiers.length && allOn(prepared.runners[last + 1], channel) && prepared.uses[last + 1].length === 0) last++;
368
+ // A tier using an index starts a task of its own, after the index is refreshed for it, as does one after a sync point.
369
+ while (last + 1 < schedule.tiers.length && schedule.syncs[last] === null && allOn(prepared.runners[last + 1], channel) && prepared.uses[last + 1].length === 0) last++;
320
370
  if (channel.busy()) await channel.idle();
321
371
  for (let v = first; v <= last; v++) {
322
372
  if (state.tails.length !== 0) {
@@ -431,7 +481,7 @@ const endTier = (state: State, gpu: GpuEngine | null, prepared: Prepared, schedu
431
481
  };
432
482
 
433
483
  /**
434
- * Runs a schedule tier by tier, then applies one sync point, and reports what went wrong (`execute`). Runs with no GPU,
484
+ * Runs a schedule tier by tier with its sync points, and reports what went wrong (`execute`). Runs with no GPU,
435
485
  * tails or observer posts awaiting nothing, so a run that waits for nothing allocates no async frame: at the first
436
486
  * tier on a joined thread, index build across the pool or chunk on workers, `executeFrom` goes on from there.
437
487
  */
@@ -454,11 +504,11 @@ const executeTiers = (context: ExecuteContext, whole: Schedule, args: unknown):
454
504
  if (schedule.reductions.size !== 0) for (const [key, contributors] of schedule.reductions) (folds ??= new Map()).set(key, { target: null, values: [], remaining: contributors, ran: false, failed: false });
455
505
  const { outcome } = prepared;
456
506
  prepared.warningCount = 0;
507
+ prepared.dropped = 0;
457
508
  outcome.failures = null;
458
509
  outcome.skipped = null;
459
510
  if (state.gpu !== null || tailing !== null || state.tails.length !== 0 || state.observer.posts.length !== 0) return executeFrom(context, schedule, prepared, folds, args, tailing, 0, null, null);
460
511
  let handed: Promise<RunReport> | null = null;
461
- let dropped = 0;
462
512
  try {
463
513
  for (let t = 0; t < schedule.tiers.length; t++) {
464
514
  // A tick in progress takes up runners replaced since it started from its next tier on.
@@ -470,41 +520,85 @@ const executeTiers = (context: ExecuteContext, whole: Schedule, args: unknown):
470
520
  if (building !== null) return handed = executeFrom(context, schedule, prepared, folds, args, null, t, building, null);
471
521
  if (state.indexFailures.length !== 0) (outcome.failures ??= []).push(...state.indexFailures.splice(0));
472
522
  }
473
- startTier(context, prepared, t, args);
474
- const waiting = settleTier(state, folds, prepared, schedule.tiers[t], t, nothing);
475
- if (waiting !== null) return handed = executeFrom(context, schedule, prepared, folds, args, null, t, null, waiting);
476
- if (endTier(state, null, prepared, schedule, t)) break;
523
+ const last = prepared.passes[t];
524
+ if (last > t) {
525
+ const reached = runPass(context, prepared, schedule, folds, args, t, last);
526
+ if (typeof reached !== "number") return handed = reached;
527
+ if (reached < 0) break;
528
+ t = reached;
529
+ }
530
+ else {
531
+ startTier(context, prepared, t, args);
532
+ const waiting = settleTier(state, folds, prepared, schedule.tiers[t], t, nothing);
533
+ if (waiting !== null) return handed = executeFrom(context, schedule, prepared, folds, args, null, t, null, waiting);
534
+ if (endTier(state, null, prepared, schedule, t)) break;
535
+ }
536
+ if (schedule.syncs[t] !== null && !idleSync(state)) syncNow(context, prepared, false);
477
537
  }
478
538
  }
479
539
  finally {
480
- if (handed === null) dropped = finishSync(context, prepared);
540
+ if (handed === null) syncNow(context, prepared, true);
481
541
  }
482
- return reportOf(state, prepared, dropped);
542
+ return reportOf(state, prepared);
483
543
  };
484
544
 
485
545
  /**
486
- * A run's sync point with nothing to wait for: no GPU, tails or observer posts, which `executeTiers` checked before
487
- * each tier and bodies can't add. Returns how many commands it dropped.
546
+ * Runs tiers `first` to `last`, a pass, one after another with none of the checks between tiers that only a tier on
547
+ * another thread, using an index or after a sync point that applies something needs. Returns the last tier it ran,
548
+ * after which the caller applies its sync point: `last`, or one before it whose sync point applies something or whose
549
+ * bodies replaced runners; -1 when one failed, the tiers after it skipped; or what `executeFrom` returns once one waits
550
+ * for something.
488
551
  */
489
- const finishSync = (context: ExecuteContext, prepared: Prepared): number => {
552
+ const runPass = (context: ExecuteContext, prepared: Prepared, schedule: Schedule, folds: Map<unknown, Fold> | null, args: unknown, first: number, last: number): number | Promise<RunReport> => {
553
+ const { state } = context;
554
+ for (let v = first; v <= last; v++) {
555
+ startTier(context, prepared, v, args);
556
+ const waiting = settleTier(state, folds, prepared, schedule.tiers[v], v, nothing);
557
+ if (waiting !== null) {
558
+ for (let ran = first; ran < v; ran++) countRan(state, prepared.runners[ran]);
559
+ return executeFrom(context, schedule, prepared, folds, args, null, v, null, waiting);
560
+ }
561
+ if (prepared.tierFailed || (v < last && (prepared.version !== state.runnersVersion || (schedule.syncs[v] !== null && !idleSync(state))))) {
562
+ for (let ran = first; ran <= v; ran++) countRan(state, prepared.runners[ran]);
563
+ if (!prepared.tierFailed) return v;
564
+ const { outcome } = prepared;
565
+ for (let later = v + 1; later < schedule.tiers.length; later++) for (const leaf of schedule.tiers[later]) (outcome.skipped ??= []).push(leaf);
566
+ return -1;
567
+ }
568
+ }
569
+ countRanHere(state, prepared.passSystems[first], prepared.passGpu[first]);
570
+ return last;
571
+ };
572
+
573
+ /**
574
+ * Whether a sync point within a run, with no joined thread, would apply nothing and leave nothing to do after:
575
+ * nothing queued, drained, unreserved or pending (`syncPending`), no rule or finalizer to activate, and no layout for
576
+ * workers to see.
577
+ */
578
+ const idleSync = (state: State): boolean => state.queued.length === 0 && state.drained === 0 && state.unused.length === 0
579
+ && state.channels.length === 0 && state.logs.length === 0 && state.bumped.length === 0 && state.counted.length === 0 && state.carried === null && !state.structure.rederive && state.unplaced.length === 0
580
+ && (state.resourcesSeen.size === 0 || !resourcesWritten(state)) && !activationPending(state) && !reserved(state.entities);
581
+
582
+ /**
583
+ * A sync point with nothing to wait for: no GPU, tails or observer posts, which `executeTiers` checked before each
584
+ * tier and bodies can't add. The run's `last` lets go of strings no reader holds.
585
+ */
586
+ const syncNow = (context: ExecuteContext, prepared: Prepared, last: boolean): void => {
490
587
  const { state, workers, publish } = context;
491
- const { outcome } = prepared;
492
- let dropped = 0;
493
- // This run's tasks on joined threads are done: they return to their event loops while its sync point applies.
588
+ // The run's tasks on joined threads so far are done: they return to their event loops while its sync point applies.
494
589
  for (let k = 0; k < state.logs.length; k++) markRun(state.logs[k]);
495
590
  try {
496
591
  if (prepareSync(state)) {
497
- dropped = state.plan.dropped;
498
- for (const { finalizer, error, kind } of applySync(state)) (outcome.failures ??= []).push(failureOf(kind ?? "finalizer", finalizer, error));
592
+ prepared.dropped += state.plan.dropped;
593
+ for (const { finalizer, error, kind } of applySync(state)) (prepared.outcome.failures ??= []).push(failureOf(kind ?? "finalizer", finalizer, error));
499
594
  }
500
595
  }
501
596
  finally {
502
597
  // Strings let go before the last run's sync point have no reader left: this run's waited for every one.
503
- state.strings?.settle();
598
+ if (last) state.strings?.settle();
504
599
  rewind(state, workers);
505
600
  publish();
506
601
  }
507
- return dropped;
508
602
  };
509
603
 
510
604
  /**
@@ -512,12 +606,11 @@ const finishSync = (context: ExecuteContext, prepared: Prepared): number => {
512
606
  * indexes' build, is done, or once `settling`, what settling it waits for, settles.
513
607
  */
514
608
  const executeFrom = async (context: ExecuteContext, schedule: Schedule, prepared: Prepared, folds: Map<unknown, Fold> | null, args: unknown, tailing: Tailing | null, first: number, building: Promise<void> | null, settling: Promise<Settled> | null): Promise<RunReport> => {
515
- const { state, workers, publish } = context;
609
+ const { state, workers } = context;
516
610
  const { gpu } = state;
517
611
  const { outcome } = prepared;
518
612
  let built = building;
519
613
  let waited = settling;
520
- let dropped = 0;
521
614
  try {
522
615
  for (let t = first; t < schedule.tiers.length; t++) {
523
616
  const tier = schedule.tiers[t];
@@ -553,6 +646,10 @@ const executeFrom = async (context: ExecuteContext, schedule: Schedule, prepared
553
646
  const last = await runFused(context, schedule, prepared, folds, args, t, only);
554
647
  if (last < 0) break;
555
648
  t = last;
649
+ if (schedule.syncs[t] !== null) {
650
+ const pending = syncPoint(context, prepared, false);
651
+ if (pending !== null) await pending;
652
+ }
556
653
  continue;
557
654
  }
558
655
  if (gpu !== null) await readAhead(state, gpu, runners);
@@ -578,60 +675,15 @@ const executeFrom = async (context: ExecuteContext, schedule: Schedule, prepared
578
675
  let waiting = settleTier(state, folds, prepared, tier, t, value);
579
676
  while (waiting !== null) waiting = settleTier(state, folds, prepared, tier, t, await waiting);
580
677
  if (endTier(state, gpu, prepared, schedule, t)) break;
678
+ if (schedule.syncs[t] !== null) {
679
+ const pending = syncPoint(context, prepared, false);
680
+ if (pending !== null) await pending;
681
+ }
581
682
  }
582
683
  }
583
684
  finally {
584
- // This run's tasks on joined threads are done: they return to their event loops while its sync point applies.
585
- for (let k = 0; k < state.logs.length; k++) markRun(state.logs[k]);
586
- try {
587
- if (gpu !== null) {
588
- gpu.submit();
589
- gpu.beforeSync();
590
- }
591
- try {
592
- // A sync point counts as a write to everything a tail still reads.
593
- if (state.tails.length !== 0) {
594
- activate(state);
595
- const waiting = syncPending(state) ? settleAll(state) : null;
596
- if (waiting !== null) await waiting;
597
- }
598
- // Each sync point waits for observer systems on joined threads in progress, and cancels the rest.
599
- if (state.observer.posts.length !== 0) {
600
- const waiting = settlePosts(state);
601
- if (waiting !== null) await waiting;
602
- }
603
- if (prepareSync(state)) {
604
- try {
605
- if (gpu !== null) {
606
- // The sync point moves and writes existing rows of these archetypes on the CPU, and
607
- // finalizers read them, so it needs the GPU's writes there first.
608
- const disturbed = disturbedBy(state);
609
- gpu.disturb(disturbed.archetypes);
610
- if (gpu.anyAhead()) await gpu.readback(disturbed.storages);
611
- }
612
- }
613
- finally {
614
- // Applied even when the readback fails, which only a lost device makes it do: the CPU state
615
- // stays whole.
616
- dropped = state.plan.dropped;
617
- for (const { finalizer, error, kind } of applySync(state)) (outcome.failures ??= []).push(failureOf(kind ?? "finalizer", finalizer, error));
618
- }
619
- }
620
- }
621
- finally {
622
- // Even a sync point that failed partway moved rows.
623
- if (gpu !== null && state.touched !== null) {
624
- gpu.touched(state.touched);
625
- state.touched.clear();
626
- }
627
- }
628
- }
629
- finally {
630
- // Strings let go before the last run's sync point have no reader left: this run's waited for every one.
631
- state.strings?.settle();
632
- rewind(state, workers);
633
- publish();
634
- }
685
+ const pending = syncPoint(context, prepared, true);
686
+ if (pending !== null) await pending;
635
687
  }
636
688
  if (tailing !== null) {
637
689
  // Placed again since, as when a joined thread is gone: they're found again next run.
@@ -644,12 +696,96 @@ const executeFrom = async (context: ExecuteContext, schedule: Schedule, prepared
644
696
  countRan(state, tailing.runners);
645
697
  }
646
698
  }
647
- return reportOf(state, prepared, dropped);
699
+ return reportOf(state, prepared);
700
+ };
701
+
702
+ /**
703
+ * Applies the commands so far as a sync point, once what it changes is read: by tails and observer systems on joined
704
+ * threads still reading it, and where it moves rows, by the GPU. One within a run lets the tiers after it see them;
705
+ * the run's `last` lets go of strings no reader holds. Returns what to wait for, or null once it applied, so a sync
706
+ * point with nothing to wait for takes no turn of the event loop before what follows it.
707
+ */
708
+ const syncPoint = (context: ExecuteContext, prepared: Prepared, last: boolean): Promise<void> | null => {
709
+ const { state } = context;
710
+ const { gpu } = state;
711
+ // The run's tasks on joined threads so far are done: they return to their event loops while its sync point applies.
712
+ for (let k = 0; k < state.logs.length; k++) markRun(state.logs[k]);
713
+ let pending: Promise<void> | null = null;
714
+ try {
715
+ if (gpu !== null) {
716
+ gpu.submit();
717
+ gpu.beforeSync();
718
+ }
719
+ const reading = readersDone(state);
720
+ pending = reading === null ? applyOnDevice(state, prepared) : reading.then(() => applyOnDevice(state, prepared) ?? undefined);
721
+ }
722
+ finally {
723
+ if (pending === null) afterSync(context, last);
724
+ }
725
+ return pending === null ? null : pending.finally(() => afterSync(context, last));
726
+ };
727
+
728
+ /**
729
+ * Waits for the readers a sync point changes what they read for: tails, as it counts as a write to everything they
730
+ * read, then observer systems on joined threads in progress, cancelling the rest; null when none needs waiting for.
731
+ */
732
+ const readersDone = (state: State): Promise<void> | null => {
733
+ if (state.tails.length !== 0) {
734
+ activate(state);
735
+ const waiting = syncPending(state) ? settleAll(state) : null;
736
+ if (waiting !== null) return waiting.then(() => (state.observer.posts.length === 0 ? null : settlePosts(state)) ?? undefined);
737
+ }
738
+ return state.observer.posts.length === 0 ? null : settlePosts(state);
739
+ };
740
+
741
+ /**
742
+ * Prepares and applies a sync point, reading back first what the GPU wrote where it moves and writes existing rows,
743
+ * which finalizers read too; applied even when the readback fails, which only a lost device makes it do, so the CPU
744
+ * state stays whole.
745
+ */
746
+ const applyOnDevice = (state: State, prepared: Prepared): Promise<void> | null => {
747
+ if (!prepareSync(state)) return null;
748
+ const { gpu } = state;
749
+ let reading: Promise<void> | null = null;
750
+ try {
751
+ if (gpu !== null) {
752
+ const disturbed = disturbedBy(state);
753
+ gpu.disturb(disturbed.archetypes);
754
+ if (gpu.anyAhead()) reading = gpu.readback(disturbed.storages);
755
+ }
756
+ }
757
+ finally {
758
+ if (reading === null) apply(state, prepared);
759
+ }
760
+ return reading === null ? null : reading.finally(() => apply(state, prepared));
761
+ };
762
+
763
+ const apply = (state: State, prepared: Prepared): void => {
764
+ prepared.dropped += state.plan.dropped;
765
+ for (const { finalizer, error, kind } of applySync(state)) (prepared.outcome.failures ??= []).push(failureOf(kind ?? "finalizer", finalizer, error));
766
+ };
767
+
768
+ /** Once a sync point applied or failed: the rows it moved, even partway, the strings it let go, and its buffers. */
769
+ const afterSync = (context: ExecuteContext, last: boolean): void => {
770
+ const { state, workers, publish } = context;
771
+ const { gpu } = state;
772
+ try {
773
+ if (gpu !== null && state.touched !== null) {
774
+ gpu.touched(state.touched);
775
+ state.touched.clear();
776
+ }
777
+ }
778
+ finally {
779
+ // Strings let go before the last run's sync point have no reader left: this run's waited for every one.
780
+ if (last) state.strings?.settle();
781
+ rewind(state, workers);
782
+ publish();
783
+ }
648
784
  };
649
785
 
650
786
  /** A run's report once its sync point applied, its index conflicts included: a shared one when nothing went wrong. */
651
- const reportOf = (state: State, prepared: Prepared, dropped: number): RunReport => {
652
- const { outcome } = prepared;
787
+ const reportOf = (state: State, prepared: Prepared): RunReport => {
788
+ const { outcome, dropped } = prepared;
653
789
  // Conflicts unique indexes found while refreshing for finalizers.
654
790
  if (state.indexFailures.length !== 0) (outcome.failures ??= []).push(...state.indexFailures.splice(0));
655
791
  const { failures, skipped } = outcome;
@@ -14,12 +14,15 @@ import { idOf, type Key, type State } from './state.js';
14
14
  import type { Queued } from './synchronize.js';
15
15
  import type { Essential, ReadonlyType } from '../../interface/component/value.js';
16
16
  import type { Leaf } from '../../interface/finalizer/created.js';
17
- import { newestKeyOf, threadOf } from '../../interface/registry/index.js';
17
+ import { declaredOf, declaresTrait, newestKeyOf, threadOf, threadOfDeclaration } from '../../interface/registry/index.js';
18
+ import type { ResolvedChanges } from '../../interface/finalizer/declaration.js';
18
19
  import type { Layout } from '../../interface/registry/layout.js';
19
20
  import type { TaskChannel } from './remote-systems.js';
20
21
  import { resourceOffsetsOf, writeStops, type StopsOf } from './stop-lists.js';
21
22
  import { acquireLookups, refreshUses, type IndexLookups } from './indexes.js';
22
23
  import { indexWatchesOf } from './lookup-keys.js';
24
+ import { takeResourceChanges, watchResource, type ResourceChange } from './resource-changes.js';
25
+ import type { Resource } from '../../interface/resource/resource.js';
23
26
 
24
27
  /** A registered finalizer, resolved for a world: what it matches or watches, its context, and the buffer its commands go to. */
25
28
  export type FinalizerRunner = {
@@ -28,6 +31,8 @@ export type FinalizerRunner = {
28
31
  readonly excluded: readonly number[];
29
32
  /** For a changes finalizer: whether it watches a component, by id. */
30
33
  readonly watches: (id: number) => boolean;
34
+ /** For a changes finalizer: the resources it watches, by id, which grow as resources declaring a trait it watches register. */
35
+ readonly resourceWatches: number[];
31
36
  /** By archetype id: 1 if it matches, 2 if not, 0 until known. */
32
37
  readonly matches: number[];
33
38
  readonly context: Context;
@@ -59,12 +64,23 @@ export const createFinalizerRunner = (state: State, finalizer: Finalizer): Final
59
64
  const reads = query?.read ?? [];
60
65
  // By default every component that isn't derived, including ones a world first meets later; never resources, nor
61
66
  // the objects a joined thread fills, whose storage here holds only tags.
62
- let watches = (id: number): boolean => !state.derived[id] && !state.resources.has(id) && state.remote[id] === undefined;
63
- const watch = finalizer.kind === "changes" ? finalizer.declaration.watch : undefined;
64
- if (watch !== undefined) {
65
- // `Finalizer.changes` rejected derived components.
66
- const watched = new Set(watch.map(key => idOf(state, key)));
67
- watches = id => watched.has(id);
67
+ const everyComponent = (id: number): boolean => !state.derived[id] && !state.resources.has(id) && state.remote[id] === undefined;
68
+ let watches = everyComponent;
69
+ const resourceWatches: number[] = [];
70
+ if (finalizer.kind === "changes") {
71
+ const { watch } = finalizer.declaration;
72
+ followResources(state, watch, resourceWatches);
73
+ if (watch.components !== true) {
74
+ // `Finalizer.changes` rejected derived components.
75
+ const listed = new Set(watch.components.map(key => idOf(state, key)));
76
+ const traits = watch.componentTraits;
77
+ if (traits.length === 0) watches = id => listed.has(id);
78
+ else {
79
+ /** By id: 1 where its key declares one of the traits, 2 where it doesn't. */
80
+ const declaring: number[] = [];
81
+ watches = id => listed.has(id) || (everyComponent(id) && (declaring[id] ??= declaresTrait(state.keys[id], traits) ? 1 : 2) === 1);
82
+ }
83
+ }
68
84
  }
69
85
  const target = { buffer: createBuffer() };
70
86
  const commands = createCommands(state, target);
@@ -79,6 +95,7 @@ export const createFinalizerRunner = (state: State, finalizer: Finalizer): Final
79
95
  required: [...new Set([...reads, ...query?.has ?? []])].filter(key => !(query?.optional ?? []).includes(key)).map(key => idOf(state, key)),
80
96
  excluded: (query?.not ?? []).map(key => idOf(state, key)),
81
97
  watches,
98
+ resourceWatches,
82
99
  matches: [],
83
100
  context: createContext(state, declaration, commanding ? { commands, indexes: indexes.byName } : { indexes: indexes.byName }),
84
101
  target,
@@ -90,6 +107,30 @@ export const createFinalizerRunner = (state: State, finalizer: Finalizer): Final
90
107
  };
91
108
  };
92
109
 
110
+ /**
111
+ * The resources a changes finalizer watches: those it lists, and those registered so far that declare a trait it
112
+ * watches, but those on another thread than the world's.
113
+ */
114
+ export const watchedResourcesOf = (watch: ResolvedChanges["watch"]): readonly Key[] => {
115
+ if (watch.resourceTraits.length === 0) return watch.resources;
116
+ const found: Key[] = [...watch.resources];
117
+ for (const [key, declared] of declaredOf("resource")) {
118
+ const thread = threadOfDeclaration(declared);
119
+ if ((thread === null || thread === "world") && declaresTrait(key, watch.resourceTraits)) found.push(key);
120
+ }
121
+ return found;
122
+ };
123
+
124
+ /** Adds to `ids` each resource a changes finalizer watches that it doesn't hold, following its changes from now. */
125
+ export const followResources = (state: State, watch: ResolvedChanges["watch"], ids: number[]): void => {
126
+ for (const key of watchedResourcesOf(watch)) {
127
+ const id = idOf(state, key);
128
+ if (ids.includes(id)) continue;
129
+ ids.push(id);
130
+ watchResource(state, id);
131
+ }
132
+ };
133
+
93
134
  /**
94
135
  * Places a finalizer on a joined thread's channel, posting it there, or back on the world's thread with null. Its reads
95
136
  * are listed as the thread reads them: objects by their tags, linear structs by their bytes, strings by handles of
@@ -248,9 +289,14 @@ type Grouped = { from: Essential[] | null; to: Essential[] | null; added: Essent
248
289
 
249
290
  /** No components, shared by every group that replaces none. */
250
291
  const none: readonly number[] = [];
292
+ const noResources: readonly never[] = [];
293
+ const noResourceChanges: readonly ResourceChange[] = [];
251
294
 
252
- /** The sync point's structural changes to a changes finalizer's components, grouped by how they changed. */
253
- const changesOf = (state: State, runner: FinalizerRunner, queued: readonly Queued[]): ChangeSet<Essential> => {
295
+ /**
296
+ * The sync point's structural changes to a changes finalizer's components, grouped by how they changed, and of
297
+ * `written`, the resources it watches.
298
+ */
299
+ const changesOf = (state: State, runner: FinalizerRunner, queued: readonly Queued[], written: readonly ResourceChange[]): ChangeSet<Essential, Resource.Key> => {
254
300
  const { plan, archetypes, entities } = state;
255
301
  const { watches } = runner;
256
302
  const names = (archetype: Archetype | null): Essential[] | null =>
@@ -366,8 +412,12 @@ const changesOf = (state: State, runner: FinalizerRunner, queued: readonly Queue
366
412
  const value = commandValue(state, queued, entry, id);
367
413
  return isMissing(value) ? live(entity, id) : value;
368
414
  };
415
+ const resources = written.length === 0 || runner.resourceWatches.length === 0 ? noResources
416
+ : written.filter(change => runner.resourceWatches.includes(change.id)).map(({ id, previous, next }) => ({ key: newestKeyOf(state.keys[id]), previous, next }));
369
417
  return {
370
418
  groups: [...groups.values()],
419
+ // Stored values match each resource's registered schema, so they have its type.
420
+ resources: resources as ChangeSet<Essential, Resource.Key>["resources"],
371
421
  // Stored and commanded values match each component's registered schema, so they have its type.
372
422
  previous: <K extends Essential>(entity: Entity, key: K) => (bornHere(entity) ? null : live(entity, idOf(state, key))) as ReadonlyType<K> | null,
373
423
  next: <K extends Essential>(entity: Entity, key: K) => next(entity, idOf(state, key)) as ReadonlyType<K> | null,
@@ -388,6 +438,8 @@ const staged = (state: State): boolean => {
388
438
  export const runFinalizers = (state: State, queued: readonly Queued[], born: Map<number, number>, tick: number): Thrown[] => {
389
439
  const errors: Thrown[] = [];
390
440
  state.born = born;
441
+ // Taken before any finalizer runs, so what finalizers write is seen at the next sync point.
442
+ const written = state.resourcesSeen.size === 0 ? noResourceChanges : takeResourceChanges(state);
391
443
  try {
392
444
  for (const runner of state.finalizers) {
393
445
  const { finalizer, remote } = runner;
@@ -399,8 +451,8 @@ export const runFinalizers = (state: State, queued: readonly Queued[], born: Map
399
451
  if (finalizer.kind === "finalizer") finalizeStops(state, runner, finalizer, errors);
400
452
  else {
401
453
  // The context is built to the finalizer's declaration, so its body accepts it.
402
- const changes: { body(changes: ChangeSet<Essential>, context: object): void } = finalizer;
403
- guarded(state, errors, finalizer, () => changes.body(changesOf(state, runner, queued), runner.context.value));
454
+ const changes: { body(changes: ChangeSet<Essential, Resource.Key>, context: object): void } = finalizer;
455
+ guarded(state, errors, finalizer, () => changes.body(changesOf(state, runner, queued, written), runner.context.value));
404
456
  }
405
457
  if (runner.target.buffer.count > 0) {
406
458
  state.queued.push({ buffer: runner.target.buffer, guarded: true });
@@ -19,7 +19,7 @@ describe("a world given keys with ids", () => {
19
19
  const again: typeof import("./ids-keys.test-helper.js") = await import(/* @vite-ignore */ `./ids-keys.test-helper.js?again=${Date.now()}`);
20
20
  expect(again.score).not.toBe(score);
21
21
  const spawnAgain = System.create({}, ({ commands }) => { spawned.push(commands.spawn([again.score])({ [again.score]: 2 })); });
22
- const add = System.create({ components: { with: [again.score] } }, row => { row[again.score] += 10; });
22
+ const add = System.create({ components: { with: [again.score], whole: true } }, row => { row[again.score] += 10; });
23
23
  await world.run(spawnAgain);
24
24
  await world.run(add);
25
25
  const [first, second] = spawned;
@@ -253,7 +253,8 @@ const keepTicking = System.create({}, () => {});
253
253
  const watchGrid = System.create({ lookup: { near: { index: grid } } }, ({ lookup }) => { void lookup.near([0, 0]).length; });
254
254
 
255
255
  describe("index lifetime", () => {
256
- it("builds an index the first time a system uses it and releases it, arena blocks included, once none does", async () => {
256
+ // Flaky on CI's runners, which runs no flaky test: run locally until it's hardened (`followups.md`, Correctness).
257
+ it.skipIf(process.env["CI"] !== undefined)("builds an index the first time a system uses it and releases it, arena blocks included, once none does", async () => {
257
258
  const driver = backToBack();
258
259
  await using world = createWorld({ [workers]: 0, [always]: true }, { loop: driver });
259
260
  await world.run(spawnCells, { cells: [[0, 0], [1, 1]] });