@adobe/ecs 0.2.1 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (330) hide show
  1. package/README.md +2 -2
  2. package/dist/implementation/compiler/body-access.d.ts +18 -1
  3. package/dist/implementation/compiler/body-access.js +104 -15
  4. package/dist/implementation/compiler/body-access.js.map +1 -1
  5. package/dist/implementation/compiler/describe.d.ts +1 -1
  6. package/dist/implementation/compiler/describe.js +3 -3
  7. package/dist/implementation/compiler/describe.js.map +1 -1
  8. package/dist/implementation/compiler/estree.d.ts +5 -2
  9. package/dist/implementation/compiler/estree.js +8 -5
  10. package/dist/implementation/compiler/estree.js.map +1 -1
  11. package/dist/implementation/compiler/inferred.js +4 -2
  12. package/dist/implementation/compiler/inferred.js.map +1 -1
  13. package/dist/implementation/compiler/signature.js +1 -1
  14. package/dist/implementation/compiler/transform.js +6 -5
  15. package/dist/implementation/compiler/transform.js.map +1 -1
  16. package/dist/implementation/world/activate.js +14 -2
  17. package/dist/implementation/world/activate.js.map +1 -1
  18. package/dist/implementation/world/buffer.d.ts +7 -0
  19. package/dist/implementation/world/buffer.js +7 -0
  20. package/dist/implementation/world/buffer.js.map +1 -1
  21. package/dist/implementation/world/cascade.d.ts +10 -3
  22. package/dist/implementation/world/cascade.js +48 -23
  23. package/dist/implementation/world/cascade.js.map +1 -1
  24. package/dist/implementation/world/commands.js +86 -2
  25. package/dist/implementation/world/commands.js.map +1 -1
  26. package/dist/implementation/world/create.js +53 -2
  27. package/dist/implementation/world/create.js.map +1 -1
  28. package/dist/implementation/world/entities.d.ts +2 -0
  29. package/dist/implementation/world/entities.js +6 -0
  30. package/dist/implementation/world/entities.js.map +1 -1
  31. package/dist/implementation/world/execute.d.ts +4 -4
  32. package/dist/implementation/world/execute.js +132 -83
  33. package/dist/implementation/world/execute.js.map +1 -1
  34. package/dist/implementation/world/finalizers.d.ts +2 -0
  35. package/dist/implementation/world/finalizers.js +24 -3
  36. package/dist/implementation/world/finalizers.js.map +1 -1
  37. package/dist/implementation/world/join.js +22 -4
  38. package/dist/implementation/world/join.js.map +1 -1
  39. package/dist/implementation/world/register.js +2 -2
  40. package/dist/implementation/world/resource-changes.d.ts +18 -0
  41. package/dist/implementation/world/resource-changes.js +66 -0
  42. package/dist/implementation/world/resource-changes.js.map +1 -0
  43. package/dist/implementation/world/rows.d.ts +1 -1
  44. package/dist/implementation/world/rows.js +23 -13
  45. package/dist/implementation/world/rows.js.map +1 -1
  46. package/dist/implementation/world/run-changes.d.ts +38 -0
  47. package/dist/implementation/world/run-changes.js +138 -0
  48. package/dist/implementation/world/run-changes.js.map +1 -0
  49. package/dist/implementation/world/runner.js +19 -15
  50. package/dist/implementation/world/runner.js.map +1 -1
  51. package/dist/implementation/world/sharing.js +1 -1
  52. package/dist/implementation/world/staging.d.ts +4 -0
  53. package/dist/implementation/world/staging.js +15 -3
  54. package/dist/implementation/world/staging.js.map +1 -1
  55. package/dist/implementation/world/state.d.ts +13 -0
  56. package/dist/implementation/world/state.js +13 -1
  57. package/dist/implementation/world/state.js.map +1 -1
  58. package/dist/implementation/world/synchronize.js +72 -22
  59. package/dist/implementation/world/synchronize.js.map +1 -1
  60. package/dist/implementation/world/tables-read.d.ts +9 -0
  61. package/dist/implementation/world/tables-read.js +72 -0
  62. package/dist/implementation/world/tables-read.js.map +1 -0
  63. package/dist/implementation/world/tables.d.ts +10 -0
  64. package/dist/implementation/world/tables.js +401 -0
  65. package/dist/implementation/world/tables.js.map +1 -0
  66. package/dist/implementation/world/thread-observer.js +1 -1
  67. package/dist/implementation/world/thread-observer.js.map +1 -1
  68. package/dist/implementation/world/thread-systems.js +11 -6
  69. package/dist/implementation/world/thread-systems.js.map +1 -1
  70. package/dist/implementation/world/up.js +7 -1
  71. package/dist/implementation/world/up.js.map +1 -1
  72. package/dist/implementation/world/where.d.ts +8 -0
  73. package/dist/implementation/world/where.js +40 -11
  74. package/dist/implementation/world/where.js.map +1 -1
  75. package/dist/index.d.ts +8 -28
  76. package/dist/index.js +4 -9
  77. package/dist/index.js.map +1 -1
  78. package/dist/interface/component/declarations.d.ts +4 -0
  79. package/dist/interface/component/declarations.js +8 -0
  80. package/dist/interface/component/declarations.js.map +1 -0
  81. package/dist/interface/component/declared.d.ts +12 -0
  82. package/dist/interface/component/declared.js +2 -0
  83. package/dist/interface/component/declared.js.map +1 -0
  84. package/dist/interface/component/index.d.ts +2 -0
  85. package/dist/interface/component/index.js +1 -1
  86. package/dist/interface/component/index.js.map +1 -1
  87. package/dist/interface/finalizer/body.d.ts +18 -3
  88. package/dist/interface/finalizer/create.d.ts +7 -5
  89. package/dist/interface/finalizer/create.js +8 -5
  90. package/dist/interface/finalizer/create.js.map +1 -1
  91. package/dist/interface/finalizer/declaration.d.ts +4 -2
  92. package/dist/interface/indexes/declaration-of.d.ts +17 -0
  93. package/dist/interface/indexes/declaration-of.js +20 -0
  94. package/dist/interface/indexes/declaration-of.js.map +1 -0
  95. package/dist/interface/indexes/index.d.ts +1 -0
  96. package/dist/interface/indexes/index.js +1 -0
  97. package/dist/interface/indexes/index.js.map +1 -1
  98. package/dist/interface/registry/names.d.ts +2 -0
  99. package/dist/interface/registry/names.js +9 -0
  100. package/dist/interface/registry/names.js.map +1 -1
  101. package/dist/interface/registry/schedule.d.ts +31 -4
  102. package/dist/interface/registry/schedule.js +185 -24
  103. package/dist/interface/registry/schedule.js.map +1 -1
  104. package/dist/interface/resource/declarations.d.ts +4 -0
  105. package/dist/interface/resource/declarations.js +8 -0
  106. package/dist/interface/resource/declarations.js.map +1 -0
  107. package/dist/interface/resource/index.d.ts +2 -0
  108. package/dist/interface/resource/index.js +1 -1
  109. package/dist/interface/resource/index.js.map +1 -1
  110. package/dist/interface/system/access.d.ts +15 -0
  111. package/dist/interface/system/body.d.ts +11 -6
  112. package/dist/interface/system/check.d.ts +8 -1
  113. package/dist/interface/system/check.js +42 -12
  114. package/dist/interface/system/check.js.map +1 -1
  115. package/dist/interface/system/commands.d.ts +37 -0
  116. package/dist/interface/system/create-resolved.js +1 -1
  117. package/dist/interface/system/create-resolved.js.map +1 -1
  118. package/dist/interface/system/create.d.ts +31 -15
  119. package/dist/interface/system/create.js +8 -7
  120. package/dist/interface/system/create.js.map +1 -1
  121. package/dist/interface/system/declaration.d.ts +9 -2
  122. package/dist/interface/system/describe.d.ts +120 -0
  123. package/dist/interface/system/describe.js +116 -0
  124. package/dist/interface/system/describe.js.map +1 -0
  125. package/dist/interface/system/index.d.ts +1 -0
  126. package/dist/interface/system/index.js +1 -0
  127. package/dist/interface/system/index.js.map +1 -1
  128. package/dist/interface/system/leaf.d.ts +6 -0
  129. package/dist/interface/system/render.js +1 -0
  130. package/dist/interface/system/render.js.map +1 -1
  131. package/dist/interface/system/resolve.d.ts +5 -1
  132. package/dist/interface/system/resolve.js +19 -1
  133. package/dist/interface/system/resolve.js.map +1 -1
  134. package/dist/interface/system/shared-only.d.ts +7 -0
  135. package/dist/interface/system/shared-only.js +2 -0
  136. package/dist/interface/system/shared-only.js.map +1 -0
  137. package/dist/interface/system/tables.d.ts +28 -0
  138. package/dist/interface/system/tables.js +2 -0
  139. package/dist/interface/system/tables.js.map +1 -0
  140. package/dist/interface/world/index.d.ts +1 -1
  141. package/dist/interface/world/table/index.d.ts +27 -0
  142. package/dist/interface/world/table/index.js +3 -0
  143. package/dist/interface/world/table/index.js.map +1 -0
  144. package/dist/interface/world/table/table.d.ts +11 -0
  145. package/dist/interface/world/table/table.js +2 -0
  146. package/dist/interface/world/table/table.js.map +1 -0
  147. package/dist/interface/world/world.d.ts +25 -10
  148. package/dist/world.d.ts +27 -0
  149. package/dist/world.js +27 -0
  150. package/dist/world.js.map +1 -0
  151. package/package.json +11 -2
  152. package/src/exports.test.ts +1 -1
  153. package/src/guide/00-keys.ts +1 -1
  154. package/src/guide/01-resources.md +1 -1
  155. package/src/guide/01-resources.ts +1 -1
  156. package/src/guide/02-entities.ts +2 -2
  157. package/src/guide/03-systems.md +2 -2
  158. package/src/guide/03-systems.out +1 -1
  159. package/src/guide/03-systems.ts +3 -3
  160. package/src/guide/04-ticks.md +1 -1
  161. package/src/guide/04-ticks.ts +3 -3
  162. package/src/guide/05-scheduling.ts +1 -1
  163. package/src/guide/06-sync-points.md +18 -7
  164. package/src/guide/06-sync-points.out +1 -2
  165. package/src/guide/06-sync-points.ts +10 -6
  166. package/src/guide/07-change-filters.ts +2 -2
  167. package/src/guide/08-input.ts +1 -1
  168. package/src/guide/09-rules.ts +1 -1
  169. package/src/guide/11-traits.ts +3 -3
  170. package/src/guide/12-lookups.ts +2 -2
  171. package/src/guide/13-indexes.ts +4 -4
  172. package/src/guide/14-kernels.ts +1 -1
  173. package/src/guide/15-reductions.ts +1 -1
  174. package/src/guide/16-gpu.ts +1 -1
  175. package/src/guide/17-hierarchies.ts +1 -1
  176. package/src/guide/18-identity.out +1 -1
  177. package/src/guide/19-threads.md +4 -2
  178. package/src/guide/19-threads.ts +2 -2
  179. package/src/guide/20-shared.ts +3 -3
  180. package/src/guide/21-dom.md +2 -2
  181. package/src/guide/21-dom.ts +1 -1
  182. package/src/guide/22-canvas.md +1 -1
  183. package/src/guide/22-canvas.ts +1 -1
  184. package/src/guide/CLAUDE.md +1 -1
  185. package/src/guide/guide.test.ts +9 -154
  186. package/src/implementation/compiler/body-access.ts +102 -12
  187. package/src/implementation/compiler/describe.test-helper.ts +1 -1
  188. package/src/implementation/compiler/describe.test.ts +1 -1
  189. package/src/implementation/compiler/describe.ts +3 -3
  190. package/src/implementation/compiler/estree.ts +9 -6
  191. package/src/implementation/compiler/hygiene.test.ts +1 -1
  192. package/src/implementation/compiler/inferred.test.ts +1 -1
  193. package/src/implementation/compiler/inferred.ts +4 -2
  194. package/src/implementation/compiler/plugin.test.ts +2 -2
  195. package/src/implementation/compiler/signature.ts +1 -1
  196. package/src/implementation/compiler/transform.test.ts +43 -23
  197. package/src/implementation/compiler/transform.ts +6 -4
  198. package/src/implementation/world/activate.ts +11 -2
  199. package/src/implementation/world/batches.test.ts +3 -3
  200. package/src/implementation/world/buffer.ts +7 -0
  201. package/src/implementation/world/cascade.ts +46 -22
  202. package/src/implementation/world/change-blocks.test.ts +1 -1
  203. package/src/implementation/world/commands.ts +76 -2
  204. package/src/implementation/world/create.test.ts +8 -8
  205. package/src/implementation/world/create.ts +57 -2
  206. package/src/implementation/world/defaults.test.ts +1 -1
  207. package/src/implementation/world/despawn-where.test.ts +287 -0
  208. package/src/implementation/world/entities.ts +6 -0
  209. package/src/implementation/world/execute.ts +125 -78
  210. package/src/implementation/world/finalizers.test.ts +1 -1
  211. package/src/implementation/world/finalizers.ts +27 -4
  212. package/src/implementation/world/ids.test.ts +1 -1
  213. package/src/implementation/world/index-changed.test.ts +2 -2
  214. package/src/implementation/world/index-joined.test.ts +5 -5
  215. package/src/implementation/world/index-order.test.ts +1 -1
  216. package/src/implementation/world/indexes.test.ts +2 -1
  217. package/src/implementation/world/join.test.ts +23 -4
  218. package/src/implementation/world/join.ts +26 -4
  219. package/src/implementation/world/joined-observer.test.ts +1 -1
  220. package/src/implementation/world/joining-worker.fixture.ts +1 -1
  221. package/src/implementation/world/joining.fixture.ts +1 -1
  222. package/src/implementation/world/joining.test-helper.ts +1 -1
  223. package/src/implementation/world/kernel-spawns.test.ts +1 -1
  224. package/src/implementation/world/names.test.ts +1 -2
  225. package/src/implementation/world/observe-factories.test.ts +2 -2
  226. package/src/implementation/world/observe-ticks-cases.test.ts +1 -1
  227. package/src/implementation/world/observe.test-helper.ts +1 -1
  228. package/src/implementation/world/observer-contained-worker.fixture.ts +1 -1
  229. package/src/implementation/world/observer-contained.test-helper.ts +1 -1
  230. package/src/implementation/world/observer-contained.test.ts +2 -2
  231. package/src/implementation/world/observer-joined.test-helper.ts +4 -4
  232. package/src/implementation/world/observer-joined.test.ts +23 -15
  233. package/src/implementation/world/observer.browser.test.ts +2 -2
  234. package/src/implementation/world/observer.test.ts +8 -8
  235. package/src/implementation/world/optional-workers.fixture.ts +1 -1
  236. package/src/implementation/world/optional.test.ts +7 -7
  237. package/src/implementation/world/perf.md +3 -1
  238. package/src/implementation/world/reclaim.test.ts +3 -3
  239. package/src/implementation/world/register.ts +2 -2
  240. package/src/implementation/world/remote-objects.test-helper.ts +1 -1
  241. package/src/implementation/world/resource-changes.test.ts +61 -0
  242. package/src/implementation/world/resource-changes.ts +67 -0
  243. package/src/implementation/world/rows.ts +24 -13
  244. package/src/implementation/world/rules.test.ts +37 -0
  245. package/src/implementation/world/run-changes.test.ts +88 -0
  246. package/src/implementation/world/run-changes.ts +160 -0
  247. package/src/implementation/world/run-results.test.ts +10 -10
  248. package/src/implementation/world/runner.ts +20 -16
  249. package/src/implementation/world/shared-gpu.browser.test.ts +2 -2
  250. package/src/implementation/world/shared-kernels.test.ts +2 -2
  251. package/src/implementation/world/shared-workers.fixture.ts +2 -2
  252. package/src/implementation/world/shared.test.ts +10 -10
  253. package/src/implementation/world/sharing.ts +1 -1
  254. package/src/implementation/world/staging.ts +16 -3
  255. package/src/implementation/world/state.ts +19 -0
  256. package/src/implementation/world/sync-points.test.ts +135 -0
  257. package/src/implementation/world/synchronize.ts +65 -18
  258. package/src/implementation/world/tables-read.ts +70 -0
  259. package/src/implementation/world/tables.test.ts +369 -0
  260. package/src/implementation/world/tables.ts +368 -0
  261. package/src/implementation/world/tails.test.ts +1 -1
  262. package/src/implementation/world/target.test.ts +2 -2
  263. package/src/implementation/world/thread-observer.ts +1 -1
  264. package/src/implementation/world/thread-systems.test-helper.ts +3 -3
  265. package/src/implementation/world/thread-systems.ts +11 -6
  266. package/src/implementation/world/up.test.ts +13 -1
  267. package/src/implementation/world/up.ts +6 -1
  268. package/src/implementation/world/where.test.ts +4 -4
  269. package/src/implementation/world/where.ts +38 -10
  270. package/src/implementation/world/workers.fixture.ts +2 -2
  271. package/src/index.ts +4 -29
  272. package/src/interface/component/declarations.ts +11 -0
  273. package/src/interface/component/declared.ts +9 -0
  274. package/src/interface/component/index.ts +2 -0
  275. package/src/interface/finalizer/body.ts +13 -3
  276. package/src/interface/finalizer/create.test.ts +3 -1
  277. package/src/interface/finalizer/create.ts +12 -7
  278. package/src/interface/finalizer/declaration.ts +4 -2
  279. package/src/interface/index.type-test.ts +2 -2
  280. package/src/interface/indexes/create.test.ts +10 -0
  281. package/src/interface/indexes/declaration-of.ts +34 -0
  282. package/src/interface/indexes/index.ts +1 -0
  283. package/src/interface/indexes/index.type-test.ts +1 -1
  284. package/src/interface/registry/names.ts +8 -0
  285. package/src/interface/registry/schedule.ts +200 -27
  286. package/src/interface/resource/declarations.ts +11 -0
  287. package/src/interface/resource/index.ts +2 -0
  288. package/src/interface/system/access.ts +16 -0
  289. package/src/interface/system/body.ts +7 -5
  290. package/src/interface/system/check.ts +37 -10
  291. package/src/interface/system/commands.ts +33 -0
  292. package/src/interface/system/commands.type-test.ts +31 -0
  293. package/src/interface/system/create-resolved.ts +1 -1
  294. package/src/interface/system/create.test.ts +21 -13
  295. package/src/interface/system/create.ts +24 -15
  296. package/src/interface/system/declaration.ts +9 -2
  297. package/src/interface/system/describe.test.ts +79 -0
  298. package/src/interface/system/describe.ts +122 -0
  299. package/src/interface/system/index.ts +1 -0
  300. package/src/interface/system/leaf.ts +6 -0
  301. package/src/interface/system/lists.type-test.ts +1 -1
  302. package/src/interface/system/optional.type-test.ts +1 -1
  303. package/src/interface/system/render.ts +1 -0
  304. package/src/interface/system/resolve.ts +25 -2
  305. package/src/interface/system/shared-only.ts +5 -0
  306. package/src/interface/system/shared.type-test.ts +1 -1
  307. package/src/interface/system/system.type-test.ts +26 -20
  308. package/src/interface/system/tables.ts +27 -0
  309. package/src/interface/system/up.type-test.ts +3 -3
  310. package/src/interface/world/index.ts +2 -2
  311. package/src/interface/world/run.type-test.ts +14 -4
  312. package/src/interface/world/table/index.ts +20 -0
  313. package/src/interface/world/table/table.ts +12 -0
  314. package/src/interface/world/world.ts +23 -10
  315. package/src/observe/entity.ts +2 -2
  316. package/src/observe/resource.ts +2 -2
  317. package/src/observe/select.ts +4 -4
  318. package/src/observe/world-state.ts +3 -3
  319. package/src/perftest/benchmarks.ts +19 -1
  320. package/src/perftest/indexes.ts +3 -3
  321. package/src/perftest/loop.ts +1 -1
  322. package/src/perftest/observer.ts +4 -4
  323. package/src/perftest/reads.ts +2 -2
  324. package/src/perftest/remote.ts +5 -5
  325. package/src/perftest/strings.ts +1 -1
  326. package/src/perftest/structural.ts +20 -0
  327. package/src/perftest/suite.ts +1 -1
  328. package/src/perftest/tables.ts +107 -0
  329. package/src/world.test.ts +2 -2
  330. package/src/world.ts +32 -0
@@ -1,7 +1,7 @@
1
1
  // © 2026 Adobe. MIT License. See /LICENSE for details.
2
2
  import type { Archetype } from './archetype.js';
3
3
  import { readCell } from './cells.js';
4
- import { indexOf, locate } from './entities.js';
4
+ import { generationOf, indexOf, locate } from './entities.js';
5
5
  import { commandValue, isMissing } from './finalizers.js';
6
6
  import { createHolders, fitHolders, forEachHolder, linkHolder, unlinkHolder } from './holders.js';
7
7
  import { closureOf, owner } from './structure.js';
@@ -9,8 +9,8 @@ import { archetypeOf, withoutComponent, type State } from './state.js';
9
9
  import type { Queued } from './synchronize.js';
10
10
 
11
11
  /**
12
- * A reference value written in this sync point: by spawn or add commands, or a batch spawn. `holder`, `component`
13
- * and `target` triples, checked once the plan's despawns are known.
12
+ * A reference value written in this sync point to check: by spawn or add commands, or of a row spawned that `linkBorn`
13
+ * leaves to the cascade. `holder`, `component` and `target` triples, checked once the plan's despawns are known.
14
14
  */
15
15
  export type Written = number[];
16
16
 
@@ -41,6 +41,48 @@ const aliveAfter = (state: State, target: number): boolean => {
41
41
  return locate(entities, target) >= 0;
42
42
  };
43
43
 
44
+ /**
45
+ * Links each reference the rows spawned in this sync point hold to its target, before the cascade, so a target it
46
+ * despawns finds them as it finds any holder. One naming an entity that won't be alive isn't linked, and goes to
47
+ * `written` to cascade, as does every reference of a row a command touched, which the command may point elsewhere.
48
+ * The rows spawned into an archetype are those from `first`, by archetype id; an entity is one f64.
49
+ */
50
+ export const linkBorn = (state: State, born: ReadonlyMap<number, number>, written: Written, tick: number): void => {
51
+ const { plan, structure, archetypes, entities, referencesWritten } = state;
52
+ const { slots } = plan;
53
+ for (const [id, first] of born) {
54
+ const archetype = archetypes[id];
55
+ const { count } = archetype;
56
+ if (first >= count) continue;
57
+ const rows = archetype.entities;
58
+ for (const component of structure.referenceIds) {
59
+ const storage = archetype.storageById[component];
60
+ if (storage === undefined) continue;
61
+ referencesWritten[component] = tick;
62
+ const lists = structure.holders[component] ??= createHolders();
63
+ fitHolders(lists, entities.length);
64
+ const { unitStride, unitOffset } = storage;
65
+ const values = storage.views.f64;
66
+ // `aliveAfter`, through locals, since it runs for each row spawned.
67
+ const { archetype: archetypeOfIndex, generation, length } = entities;
68
+ const { entity: planned, despawned } = plan;
69
+ for (let row = first; row < count; row++) {
70
+ const target = values[row * unitStride + unitOffset];
71
+ if (target === 0) continue;
72
+ const holder = rows[row];
73
+ const index = indexOf(holder);
74
+ const commanded = index < slots.length && slots[index] >= 0;
75
+ const into = indexOf(target);
76
+ const entry = into < slots.length ? slots[into] : -1;
77
+ const alive = entry >= 0 && planned[entry] === target ? despawned[entry] === 0
78
+ : into < length && archetypeOfIndex[into] >= 0 && generation[into] === generationOf(target);
79
+ if (alive) linkHolder(lists, target, holder);
80
+ if (commanded || !alive) written.push(holder, component, target);
81
+ }
82
+ }
83
+ }
84
+ };
85
+
44
86
  /**
45
87
  * Despawns the holders that own a reference to an entity that is despawned or never lives, and removes `weak`
46
88
  * references to one, transitively, before rules close the plan. A holder acts only if its value once the plan applies
@@ -91,7 +133,7 @@ export const cascadeReferences = (state: State, queued: readonly Queued[], writt
91
133
  */
92
134
  export type IndexChanges = { readonly remove: number[]; readonly add: number[] };
93
135
 
94
- export const indexChangesOf = (state: State, queued: readonly Queued[], fromOf: (entry: number) => Archetype | null, bornRows: readonly (readonly [Archetype, number])[]): IndexChanges => {
136
+ export const indexChangesOf = (state: State, queued: readonly Queued[], fromOf: (entry: number) => Archetype | null): IndexChanges => {
95
137
  const { plan, structure, entities } = state;
96
138
  const changes: IndexChanges = { remove: [], add: [] };
97
139
  for (let entry = 0; entry < plan.count; entry++) {
@@ -106,24 +148,6 @@ export const indexChangesOf = (state: State, queued: readonly Queued[], fromOf:
106
148
  if (typeof after === "number" && after !== 0) changes.add.push(after, holder, component);
107
149
  }
108
150
  }
109
- // Batch spawns no command touched still hold the references they were spawned with. An entity is one f64.
110
- const { slots } = plan;
111
- for (const [archetype, first] of bornRows) {
112
- for (const component of structure.referenceIds) {
113
- const storage = archetype.storageById[component];
114
- if (storage === undefined) continue;
115
- const { unitStride, unitOffset } = storage;
116
- const values = storage.views.f64;
117
- const rows = archetype.entities;
118
- for (let row = first; row < archetype.count; row++) {
119
- const holder = rows[row];
120
- const index = indexOf(holder);
121
- if (index < slots.length && slots[index] >= 0) continue;
122
- const value = values[row * unitStride + unitOffset];
123
- if (value !== 0) changes.add.push(value, holder, component);
124
- }
125
- }
126
- }
127
151
  return changes;
128
152
  };
129
153
 
@@ -89,7 +89,7 @@ const setGroup = System.create({ target: "entity", components: { with: [group] }
89
89
  // Readers.
90
90
  let found: number[] = [];
91
91
  const onMain = System.create({ components: { with: [at], changed: [value] } }, row => { found.push(row[at]); });
92
- const observed = System.create({ observer: true, components: { with: [at], changed: [value] } }, row => { found.push(row[at]); });
92
+ const observed = System.create({ readonly: true, components: { with: [at], changed: [value] } }, row => { found.push(row[at]); });
93
93
  const ofTwo = System.create({ components: { with: [at], changed: [group, value] } }, row => { found.push(row[at]); });
94
94
  const inKernel = System.create({ kernel: true, components: { with: [seen], changed: [value] } }, row => { row[seen] += 1; });
95
95
  const inUp = System.create({ components: { with: [at], changed: [value] }, up: { through: parent } }, row => { found.push(row[at]); });
@@ -1,8 +1,15 @@
1
1
  // © 2026 Adobe. MIT License. See /LICENSE for details.
2
2
  import type { Commands } from '../../interface/system/commands.js';
3
- import { addCommand, despawnCommand, pushCommand, pushComponent, removeCommand, type CommandBuffer } from './buffer.js';
3
+ import { addCommand, createBuffer, despawnCommand, despawnWhereCommand, despawnWhereHolds, pushCommand, pushComponent, removeCommand, spawnCommand, type CommandBuffer } from './buffer.js';
4
+ import type { Archetype } from './archetype.js';
5
+ import { reserve } from './entities.js';
4
6
  import { listShapeOf, presetSpawnerOf, spawnerOf, type ListShape } from './lists.js';
5
- import { commandIdOf, idOf, storedShared, type Key, type State } from './state.js';
7
+ import { commandIdOf, idOf, queryIdOf, storedShared, type Key, type State } from './state.js';
8
+ import { spawnTables } from './tables.js';
9
+ import { holdsValuesOf } from './where.js';
10
+ import { checkWhereValues } from '../../interface/system/check.js';
11
+ import { declarationOf, isWorldWritten, kindOf, labelOf, threadOfDeclaration, whyUnregistered } from '../../interface/registry/index.js';
12
+ import { matchesSchema } from '../../interface/registry/schemas.js';
6
13
 
7
14
  const collect = (state: State, buffer: CommandBuffer, key: Key, value: unknown): void => {
8
15
  if (value !== undefined) pushComponent(buffer, idOf(state, key), value);
@@ -44,9 +51,67 @@ const addShared = (state: State, buffer: CommandBuffer, entity: number, shape: L
44
51
  for (let k = 0; k < keys.length; k++) pushComponent(buffer, ids[k], values[k]);
45
52
  };
46
53
 
54
+ /**
55
+ * Throws for a key of `resources` that isn't a resource, is one only the world writes or one living on another thread,
56
+ * or is given a value its schema doesn't take.
57
+ */
58
+ const checkResources = (resources: object): void => {
59
+ for (const key of Object.getOwnPropertySymbols(resources)) {
60
+ const fail = (message: string): never => { throw new Error(`commands.spawnTables: "${labelOf(key)}" ${message}.`); };
61
+ if (kindOf(key) !== "resource") fail(`isn't a registered resource: ${whyUnregistered(key)}`);
62
+ if (isWorldWritten(key)) fail("is written only by the world");
63
+ const declaration = declarationOf(key);
64
+ if (declaration === undefined) continue;
65
+ const thread = threadOfDeclaration(declaration);
66
+ if (thread !== null && thread !== "world") fail(`lives on thread "${thread}", not the world's`);
67
+ if (declaration.type !== undefined && !matchesSchema(declaration.type, Reflect.get(resources, key))) fail("is given a value its type doesn't take");
68
+ }
69
+ };
70
+
71
+ /**
72
+ * Queues `resources`' values for the sync point as the world's own change to its resource entity, which it reserves
73
+ * when the world has none yet: a buffer of their own, since the sync point skips systems' commands to that entity.
74
+ */
75
+ const queueResources = (state: State, resources: object): void => {
76
+ if (Object.getOwnPropertySymbols(resources).length === 0) return;
77
+ const buffer = createBuffer();
78
+ if (state.resourceEntity < 0) {
79
+ state.resourceEntity = reserve(state.entities);
80
+ pushValues(state, buffer, spawnCommand, state.resourceEntity, resources);
81
+ }
82
+ else pushValues(state, buffer, addCommand, state.resourceEntity, resources);
83
+ state.queued.push({ buffer, guarded: false });
84
+ };
85
+
47
86
  /** Values of a list add given none, which only an untyped caller does. */
48
87
  const empty = {};
49
88
 
89
+ const noKeys: readonly unknown[] = [];
90
+
91
+ /**
92
+ * Queues a `despawnWhere`, its ids resolved and its `where` checked first, so a key that isn't a component, or a `where`
93
+ * that isn't of shared values, queues nothing.
94
+ */
95
+ const pushDespawnWhere = (state: State, buffer: CommandBuffer, query: Parameters<Commands["despawnWhere"]>[0]): void => {
96
+ const including: readonly unknown[] = query.with;
97
+ const excluding: readonly unknown[] = query.not ?? noKeys;
98
+ const { where } = query;
99
+ if (including.length === 0) throw new Error("commands.despawnWhere's `with` names at least one component.");
100
+ for (let k = 0; k < including.length; k++) queryIdOf(state, including[k]);
101
+ for (let k = 0; k < excluding.length; k++) queryIdOf(state, excluding[k]);
102
+ let holds: ((archetype: Archetype) => boolean) | null = null;
103
+ if (where !== undefined && Object.getOwnPropertySymbols(where).length > 0) {
104
+ checkWhereValues("commands.despawnWhere", "where", "a despawn", where, false);
105
+ const found = holdsValuesOf(state, where, false);
106
+ if (typeof found === "symbol") throw new Error(`commands.despawnWhere: where names "${labelOf(found)}", which isn't a shared component.`);
107
+ holds = found;
108
+ }
109
+ pushCommand(buffer, despawnWhereCommand, including.length);
110
+ for (let k = 0; k < including.length; k++) pushComponent(buffer, queryIdOf(state, including[k]), undefined);
111
+ for (let k = 0; k < excluding.length; k++) pushComponent(buffer, queryIdOf(state, excluding[k]), undefined);
112
+ if (holds !== null) pushComponent(buffer, despawnWhereHolds, holds);
113
+ };
114
+
50
115
  /**
51
116
  * Commands that queue for the next sync point. A key that isn't a component commands may name throws here, inside the
52
117
  * issuing system, before anything is queued. Values are immutable by convention, so they aren't copied.
@@ -61,6 +126,7 @@ export const createCommands = (state: State, target: { readonly buffer: CommandB
61
126
  despawn: entity => {
62
127
  pushCommand(target.buffer, despawnCommand, entity);
63
128
  },
129
+ despawnWhere: query => pushDespawnWhere(state, target.buffer, query),
64
130
  add: (entity: number, components: symbol | readonly symbol[], values?: unknown) => {
65
131
  const { buffer } = target;
66
132
  if (typeof components === "symbol") {
@@ -83,6 +149,14 @@ export const createCommands = (state: State, target: { readonly buffer: CommandB
83
149
  pushComponent(buffer, ids[k], value === undefined ? starts[k] : value);
84
150
  }
85
151
  },
152
+ spawnTables: (tables, options) => {
153
+ const resources = options?.resources;
154
+ if (resources === undefined) return spawnTables(state, tables, options?.existing);
155
+ checkResources(resources);
156
+ const spawned = spawnTables(state, tables, options?.existing);
157
+ queueResources(state, resources);
158
+ return spawned;
159
+ },
86
160
  remove: (entity: number, components: symbol | readonly symbol[]) => {
87
161
  const { buffer } = target;
88
162
  if (typeof components === "symbol") {
@@ -71,28 +71,28 @@ const positions = async (world: World): Promise<Vec2[]> =>
71
71
  (await readEach(world, await select(world, { required: [position], order: { [position]: true } }), position)).map(value => value ?? [NaN, NaN]);
72
72
 
73
73
  describe("World.create", () => {
74
- it("spawns once from a changed resource, then integrates", async () => {
74
+ it("spawns once from a changed resource, then integrates, from the run that spawns", async () => {
75
75
  const world = createWorld();
76
76
  await world.run(main);
77
- expect(await positions(world)).toEqual([[0, 0], [1, 0], [2, 0]]);
78
- await world.run(main);
79
77
  expect(await positions(world)).toEqual([[0, 1], [1, 1], [2, 1]]);
78
+ await world.run(main);
79
+ expect(await positions(world)).toEqual([[0, 2], [1, 2], [2, 2]]);
80
80
  expect(await readResource(world, spawnCount)).toBe(3);
81
81
  });
82
82
 
83
83
  it("reports each change to a system exactly once", async () => {
84
84
  const world = createWorld();
85
85
  await world.run(main);
86
- // Spawned at the sync point, after watch ran: seen on the next run.
87
- expect(await readResource(world, seen)).toBe(0);
88
- await world.run(main);
86
+ // Spawned at the sync point before move, which watch follows: seen in the same run.
89
87
  expect(await readResource(world, seen)).toBe(3);
88
+ await world.run(main);
89
+ expect(await readResource(world, seen)).toBe(6);
90
90
  // Moved before watch in the last run, so already reported.
91
91
  await world.run(watch);
92
- expect(await readResource(world, seen)).toBe(3);
92
+ expect(await readResource(world, seen)).toBe(6);
93
93
  await world.run(move);
94
94
  await world.run(watch);
95
- expect(await readResource(world, seen)).toBe(6);
95
+ expect(await readResource(world, seen)).toBe(9);
96
96
  });
97
97
 
98
98
  it("applies the net structural change of a sync point", async () => {
@@ -1,7 +1,7 @@
1
1
  // © 2026 Adobe. MIT License. See /LICENSE for details.
2
2
  import type { Finalizer } from '../../interface/finalizer/created.js';
3
3
  import type { Rule } from '../../interface/rule/created.js';
4
- import { requireDefinition, type Definition } from '../../interface/registry/index.js';
4
+ import { definitionOf, requireDefinition, type Definition } from '../../interface/registry/index.js';
5
5
  import type { World } from '../../interface/world/world.js';
6
6
  import type { Input } from '../../interface/system/input.js';
7
7
  import type { System } from '../../interface/system/system.js';
@@ -43,6 +43,10 @@ import { stampChanged } from './archetype.js';
43
43
  import { createLoop, platformFrame, platformTimer, type Clock, type LoopPlatform } from './loop.js';
44
44
  import { presetLevelsOf } from './lists.js';
45
45
  import { createState, defaultOf, idOf, reside, startTicking, type Key } from './state.js';
46
+ import { asksForChanges, changesOf, resourcesWrittenBy, startRecording, takesInput } from './run-changes.js';
47
+ import type { ChangeSet } from '../../interface/finalizer/body.js';
48
+ import type { Essential } from '../../interface/component/value.js';
49
+ import type { Resource } from '../../interface/resource/resource.js';
46
50
  import { indexOf } from './entities.js';
47
51
  import { touchedHeld } from './up.js';
48
52
  import { isHeld } from './transfers.js';
@@ -53,6 +57,9 @@ type Queue = <T>(task: () => T | Promise<T>) => Promise<T>;
53
57
 
54
58
  /** A task's failure as the queue's own chain carries it, which never rejects: made only when a task fails. */
55
59
  type Carried = { readonly thrown: unknown };
60
+ /** A schedule of nothing, whose run is its sync point alone. */
61
+ const noTiers: Schedule = { tiers: [], reductions: new Map(), observer: [], syncs: [] };
62
+
56
63
  const thrownValues = new WeakSet<object>();
57
64
  const thrownOf = (error: unknown): Carried => {
58
65
  const wrapped = { thrown: error };
@@ -956,9 +963,57 @@ export const createWorld = (resources: InitialResources = {}, options: WorldOpti
956
963
  },
957
964
  });
958
965
 
966
+ /**
967
+ * Runs a system or composite, recording its own changes, which it resolves to: what earlier finalizers queued
968
+ * applies first, at a sync point of its own, and the recording covers the run's sync points alone.
969
+ */
970
+ const runChanging = (definition: Definition, value: unknown): Promise<unknown> => {
971
+ const started = enqueue(() => startChanges(definition));
972
+ const ran = runWith(definition, value);
973
+ const taken = enqueue(takeChanges);
974
+ return Promise.all([started, ran, taken]).then(([, , changes]) => changes);
975
+ };
976
+ const startChanges = (definition: Definition): unknown => {
977
+ checkLive();
978
+ const flushed = execution(definition, undefined, noTiers);
979
+ if (flushed instanceof Promise) return flushed.then(report => startRecordingOf(definition, report));
980
+ startRecordingOf(definition, flushed);
981
+ return undefined;
982
+ };
983
+ const startRecordingOf = (definition: Definition, flushed: RunReport): void => {
984
+ outOfBand.reportAll(flushed.failures);
985
+ state.recording = startRecording(state, resourcesWrittenBy(definition));
986
+ };
987
+ const takeChanges = (): unknown => {
988
+ const { recording } = state;
989
+ state.recording = null;
990
+ if (recording === null) throw new Error("The run's changes weren't recorded.");
991
+ return changesOf(state, recording);
992
+ };
993
+
994
+ function run<I, R>(system: System<I, R>, ...input: Input<NoInfer<I>>): Promise<R>;
995
+ function run<I, R>(system: System<I, R>, ...input: [...Input<NoInfer<I>>, options: { readonly changes: true }]): Promise<ChangeSet<Essential, Resource.Key>>;
996
+ function run(system: System<unknown, unknown>, ...args: readonly unknown[]): Promise<unknown> {
997
+ // Options come last, so a run not asking for changes reads one value more.
998
+ const { length } = args;
999
+ if (length === 0 || !asksForChanges(args[length - 1])) return runWith(system, args[0]);
1000
+ const definition = definitionOf(system);
1001
+ let input = false;
1002
+ try {
1003
+ input = definition !== undefined && takesInput(definition);
1004
+ }
1005
+ catch {
1006
+ // A composite whose schedule can't be made fails its run, as `runWith` finds.
1007
+ return runWith(system, args[0]);
1008
+ }
1009
+ // A system taking input whose input is all it was given runs with it.
1010
+ if (definition === undefined || (input && args.length < 2)) return runWith(system, args[0]);
1011
+ return runChanging(definition, input ? args[0] : undefined);
1012
+ }
1013
+
959
1014
  const world: CreatedWorld = {
960
1015
  // A run gives what its system's body returned, `R` by the system's type.
961
- run: <I, R>(system: System<I, R>, ...input: Input<NoInfer<I>>) => runWith(system, input[0]) as Promise<R>,
1016
+ run,
962
1017
  share: port => {
963
1018
  if (disposed) return Promise.reject(new Error("The world is disposed."));
964
1019
  if (!state.memory.shared) return Promise.reject(new Error("Without cross-origin isolation memory can't be shared, so no thread can join."));
@@ -56,7 +56,7 @@ describe("a derived component declared without a default", () => {
56
56
  commands.spawn([tagged])({});
57
57
  });
58
58
  const seen: unknown[] = [];
59
- const read = System.create({ observer: true, components: { with: [index] } }, row => { seen.push(row[index]); });
59
+ const read = System.create({ readonly: true, components: { with: [index] } }, row => { seen.push(row[index]); });
60
60
  await using first = createWorld();
61
61
  await using second = createWorld();
62
62
  for (const world of [first, second]) {
@@ -0,0 +1,287 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import { describe, expect, it } from "vitest";
3
+ import { Entity } from "../../interface/entity/entity.js";
4
+ import { Finalizer } from "../../interface/finalizer/finalizer.js";
5
+ import { Rule } from "../../interface/rule/rule.js";
6
+ import type { Commands } from "../../interface/system/commands.js";
7
+ import { System } from "../../interface/system/system.js";
8
+ import type { World } from "../../interface/world/world.js";
9
+ import { workers } from "../../interface/resources/workers.js";
10
+ import { Observe } from "../../observe/index.js";
11
+ import { createWorld } from "./create.js";
12
+ import { runReported } from "./outcomes.test-helper.js";
13
+ import { count, read, readResource } from "./reads.test-helper.js";
14
+
15
+ const mark = Symbol("despawnWhereTest/mark");
16
+ const kept = Symbol("despawnWhereTest/kept");
17
+ const value = Symbol("despawnWhereTest/value");
18
+ const extra = Symbol("despawnWhereTest/extra");
19
+ const host = Symbol("despawnWhereTest/host");
20
+ const derivedFrom = Symbol("despawnWhereTest/derivedFrom");
21
+ const slot = Symbol("despawnWhereTest/slot");
22
+ const parent = Symbol("despawnWhereTest/parent");
23
+ const finalized = Symbol("despawnWhereTest/finalized");
24
+ const watched = Symbol("despawnWhereTest/watched");
25
+ const trigger = Symbol("despawnWhereTest/trigger");
26
+ const level = Symbol("despawnWhereTest/level");
27
+ const journal = Symbol("despawnWhereTest/journal");
28
+ const chunk = Symbol("despawnWhereTest/chunk");
29
+ // Never declared, so never registered.
30
+ const unregistered = Symbol("despawnWhereTest/unregistered");
31
+
32
+ declare module "@adobe/ecs" {
33
+ export interface Components {
34
+ [mark]: { type: true };
35
+ [kept]: { type: true };
36
+ [value]: { type: number };
37
+ [extra]: { type: true };
38
+ [host]: { type: true };
39
+ [derivedFrom]: { type: true };
40
+ [slot]: { type: number; derived: true };
41
+ [parent]: { type: Entity; reference: "owner" };
42
+ [finalized]: { type: number };
43
+ [watched]: { type: number };
44
+ [trigger]: { type: true };
45
+ [chunk]: { type: number; shared: true };
46
+ }
47
+ export interface Resources {
48
+ [level]: { type: number };
49
+ [journal]: { type: readonly string[]; default: [] };
50
+ }
51
+ }
52
+
53
+ Rule.create({ components: { with: [host, derivedFrom] }, derive: [slot] });
54
+
55
+ const seen: number[] = [];
56
+ Finalizer.create({ components: { with: [finalized] } }, row => { seen.push(row[finalized]); });
57
+
58
+ Finalizer.changes({ watch: [watched, mark] }, (changes, { resources }) => {
59
+ const lines = changes.groups.map(group => `${group.entities.map(entity => changes.previous(entity, watched)).join(",")}: +${group.added.length} -${group.removed.length}`);
60
+ resources[journal] = [...resources[journal], ...lines];
61
+ });
62
+
63
+ /** A world, and a function running `body` once in a system with the world's commands, rejecting with what it threw. */
64
+ const withCommands = () => {
65
+ const world = createWorld({ [workers]: 0 });
66
+ let body: (commands: Commands) => void = () => {};
67
+ const issue = System.create({}, ({ commands }) => body(commands));
68
+ const run = async (next: (commands: Commands) => void): Promise<void> => {
69
+ body = next;
70
+ const [failure] = (await runReported(world, issue)).failures;
71
+ if (failure !== undefined) throw new Error(failure.message);
72
+ };
73
+ return { world, run };
74
+ };
75
+
76
+ const alive = async (world: World, entity: Entity): Promise<boolean> =>
77
+ (await Observe.toPromise(Observe.entity(world, entity, { optional: [mark] }))) !== null;
78
+
79
+ const aliveEach = (world: World, entities: readonly Entity[]): Promise<boolean[]> => Promise.all(entities.map(entity => alive(world, entity)));
80
+
81
+ describe("despawnWhere", () => {
82
+ it("despawns every entity with each `with` component and no `not` one, across archetypes", async () => {
83
+ const { world, run } = withCommands();
84
+ const spawned: Entity[] = [];
85
+ await run(commands => {
86
+ for (let i = 0; i < 3; i++) {
87
+ spawned.push(commands.spawn([mark])());
88
+ spawned.push(commands.spawn([mark, value])({ [value]: i }));
89
+ spawned.push(commands.spawn([mark, value, extra])({ [value]: i }));
90
+ spawned.push(commands.spawn([value])({ [value]: i }));
91
+ spawned.push(commands.spawn([mark, kept])());
92
+ }
93
+ });
94
+ await run(commands => commands.despawnWhere({ with: [mark], not: [kept] }));
95
+ expect(await aliveEach(world, spawned)).toEqual([0, 1, 2].flatMap(() => [false, false, false, true, true]));
96
+ await run(commands => commands.despawnWhere({ with: [value, mark] }));
97
+ expect(await count(world, { required: [value] })).toBe(3);
98
+ await world.dispose();
99
+ });
100
+
101
+ it("selects by the commands issued before it at the sync point; later ones to a despawned entity do nothing", async () => {
102
+ const { world, run } = withCommands();
103
+ let gains = Entity.none, loses = Entity.none, gainsAfter = Entity.none, changedAfter = Entity.none;
104
+ await run(commands => {
105
+ gains = commands.spawn([value])({ [value]: 1 });
106
+ loses = commands.spawn([mark, value])({ [value]: 2 });
107
+ gainsAfter = commands.spawn([value])({ [value]: 3 });
108
+ changedAfter = commands.spawn([mark, value])({ [value]: 4 });
109
+ });
110
+ await run(commands => {
111
+ commands.add(gains, mark);
112
+ commands.remove(loses, mark);
113
+ commands.despawnWhere({ with: [mark] });
114
+ commands.add(gainsAfter, mark);
115
+ commands.add(changedAfter, extra);
116
+ });
117
+ expect(await aliveEach(world, [gains, loses, gainsAfter, changedAfter])).toEqual([false, true, true, false]);
118
+ expect(await read(world, gainsAfter, mark)).toBe(true);
119
+ await world.dispose();
120
+ });
121
+
122
+ it("despawns matching entities spawned at the same sync point, whenever spawned, since spawns commit first", async () => {
123
+ const { world, run } = withCommands();
124
+ const spawned: Entity[] = [];
125
+ await run(commands => {
126
+ spawned.push(commands.spawn([mark, value])({ [value]: 1 }));
127
+ commands.despawnWhere({ with: [mark] });
128
+ spawned.push(commands.spawn([mark, value])({ [value]: 2 }));
129
+ spawned.push(commands.spawn([value])({ [value]: 3 }));
130
+ });
131
+ expect(await aliveEach(world, spawned)).toEqual([false, false, true]);
132
+ await world.dispose();
133
+ });
134
+
135
+ it("matches derived components, by where earlier commands leave an entity once derived", async () => {
136
+ const { world, run } = withCommands();
137
+ let derived = Entity.none, deriving = Entity.none, underived = Entity.none;
138
+ await run(commands => {
139
+ derived = commands.spawn([host, derivedFrom])();
140
+ deriving = commands.spawn([host])();
141
+ underived = commands.spawn([host, derivedFrom])();
142
+ });
143
+ expect(await read(world, derived, slot)).not.toBeNull();
144
+ await run(commands => {
145
+ commands.add(deriving, derivedFrom);
146
+ commands.remove(underived, derivedFrom);
147
+ commands.despawnWhere({ with: [slot] });
148
+ });
149
+ expect(await aliveEach(world, [derived, deriving, underived])).toEqual([false, false, true]);
150
+ await world.dispose();
151
+ });
152
+
153
+ it("runs finalizers for each entity it despawns, reading its values", async () => {
154
+ const { world, run } = withCommands();
155
+ seen.length = 0;
156
+ await run(commands => {
157
+ for (let i = 0; i < 4; i++) commands.spawn(i % 2 === 0 ? [finalized, mark] : [finalized])({ [finalized]: i });
158
+ });
159
+ await run(commands => commands.despawnWhere({ with: [mark, finalized] }));
160
+ expect(seen.sort()).toEqual([0, 2]);
161
+ await world.dispose();
162
+ seen.length = 0;
163
+ });
164
+
165
+ it("gives a changes finalizer the same changes as a despawn of each", async () => {
166
+ const journals: (readonly string[])[] = [];
167
+ for (const despawn of [
168
+ (commands: Commands, entities: readonly Entity[]) => { for (const entity of entities) commands.despawn(entity); },
169
+ (commands: Commands) => commands.despawnWhere({ with: [mark] }),
170
+ ]) {
171
+ const { world, run } = withCommands();
172
+ const entities: Entity[] = [];
173
+ await run(commands => {
174
+ for (let i = 0; i < 3; i++) entities.push(commands.spawn([watched, mark])({ [watched]: i }));
175
+ entities.push(commands.spawn([watched, mark, extra])({ [watched]: 9 }));
176
+ commands.spawn([watched])({ [watched]: 5 });
177
+ });
178
+ await run(commands => despawn(commands, entities));
179
+ journals.push(await readResource(world, journal));
180
+ await world.dispose();
181
+ }
182
+ expect(journals[1]).toEqual(journals[0]);
183
+ expect(journals[1]).toContain("0,1,2: +0 -2");
184
+ });
185
+
186
+ it("cascades owner references as a despawn does", async () => {
187
+ const { world, run } = withCommands();
188
+ let root = Entity.none, child = Entity.none, other = Entity.none;
189
+ await run(commands => {
190
+ root = commands.spawn([mark])();
191
+ child = commands.spawn([parent, value])({ [parent]: root, [value]: 1 });
192
+ other = commands.spawn([value])({ [value]: 2 });
193
+ });
194
+ await run(commands => commands.despawnWhere({ with: [mark] }));
195
+ expect(await aliveEach(world, [root, child, other])).toEqual([false, false, true]);
196
+ await world.dispose();
197
+ });
198
+
199
+ it("never despawns the resource entity", async () => {
200
+ const { world, run } = withCommands();
201
+ const set = System.create({}, ({ resources, commands }) => {
202
+ resources[level] = 3;
203
+ commands.spawn([mark])();
204
+ commands.despawnWhere({ with: [mark] });
205
+ });
206
+ await world.run(set);
207
+ await run(commands => commands.despawnWhere({ with: [mark] }));
208
+ expect(await readResource(world, level)).toBe(3);
209
+ expect(await count(world, { required: [mark] })).toBe(0);
210
+ await world.dispose();
211
+ });
212
+
213
+ it("throws in the issuing system, queuing nothing, for an empty `with` or a key that isn't a component", async () => {
214
+ const { world, run } = withCommands();
215
+ let entity = Entity.none;
216
+ await run(commands => { entity = commands.spawn([mark])(); });
217
+ // @ts-expect-error `with` names at least one component
218
+ await expect(run(commands => commands.despawnWhere({ with: [] }))).rejects.toThrow("names at least one component");
219
+ // @ts-expect-error never registered
220
+ await expect(run(commands => commands.despawnWhere({ with: [mark], not: [unregistered] }))).rejects.toThrow("no registered schema");
221
+ // @ts-expect-error a resource
222
+ await expect(run(commands => commands.despawnWhere({ with: [level] }))).rejects.toThrow("is a resource");
223
+ expect(await alive(world, entity)).toBe(true);
224
+ await world.dispose();
225
+ });
226
+
227
+ it("runs in a system over rows, which isn't a kernel", async () => {
228
+ const { world, run } = withCommands();
229
+ const clear = System.create({ components: { with: [trigger] } }, (_row, { commands }) => {
230
+ commands.despawnWhere({ with: [mark] });
231
+ });
232
+ await run(commands => {
233
+ for (let i = 0; i < 5; i++) commands.spawn([mark, value])({ [value]: i });
234
+ commands.spawn([trigger])();
235
+ commands.spawn([trigger])();
236
+ });
237
+ const { failures } = await runReported(world, clear);
238
+ expect(failures).toEqual([]);
239
+ expect(await count(world, { required: [mark] })).toBe(0);
240
+ expect(await count(world, { required: [trigger] })).toBe(2);
241
+ await world.dispose();
242
+ });
243
+
244
+ it("keeps to the archetypes holding where's shared values, each required as with requires it", async () => {
245
+ const { world, run } = withCommands();
246
+ const spawned: Entity[] = [];
247
+ await run(commands => {
248
+ spawned.push(commands.spawn([mark, chunk], { [chunk]: 1 })());
249
+ spawned.push(commands.spawn([mark, value, chunk], { [chunk]: 1 })({ [value]: 1 }));
250
+ spawned.push(commands.spawn([mark, chunk], { [chunk]: 2 })());
251
+ spawned.push(commands.spawn([mark])());
252
+ spawned.push(commands.spawn([value, chunk], { [chunk]: 1 })({ [value]: 2 }));
253
+ });
254
+ await run(commands => commands.despawnWhere({ with: [mark], where: { [chunk]: 1 } }));
255
+ expect(await aliveEach(world, spawned)).toEqual([false, false, true, true, true]);
256
+ await world.dispose();
257
+ });
258
+
259
+ it("despawns by where the entities spawned at the same sync point, and none for a value no archetype holds", async () => {
260
+ const { world, run } = withCommands();
261
+ const spawned: Entity[] = [];
262
+ await run(commands => {
263
+ spawned.push(commands.spawn([mark, chunk], { [chunk]: 1 })());
264
+ commands.despawnWhere({ with: [mark], where: { [chunk]: 1 } });
265
+ spawned.push(commands.spawn([mark, chunk], { [chunk]: 2 })());
266
+ commands.despawnWhere({ with: [mark], where: { [chunk]: 3 } });
267
+ });
268
+ expect(await aliveEach(world, spawned)).toEqual([false, true]);
269
+ await run(commands => commands.despawnWhere({ with: [mark], where: {} }));
270
+ expect(await aliveEach(world, spawned)).toEqual([false, false]);
271
+ await world.dispose();
272
+ });
273
+
274
+ it("throws for a where that isn't values of shared components, queuing nothing", async () => {
275
+ const { world, run } = withCommands();
276
+ let entity = Entity.none;
277
+ await run(commands => { entity = commands.spawn([mark, chunk], { [chunk]: 1 })(); });
278
+ // @ts-expect-error where names only shared components
279
+ await expect(run(commands => commands.despawnWhere({ with: [mark], where: { [value]: 1 } }))).rejects.toThrow(/where names "despawnWhereTest\/value", which isn't a shared component/);
280
+ // @ts-expect-error where takes values, not predicates
281
+ await expect(run(commands => commands.despawnWhere({ with: [mark], where: { [chunk]: (given: number) => given === 1 } }))).rejects.toThrow(/gives "despawnWhereTest\/chunk" a predicate/);
282
+ // @ts-expect-error a value of the component's type
283
+ await expect(run(commands => commands.despawnWhere({ with: [mark], where: { [chunk]: "1" } }))).rejects.toThrow(/isn't one of its type's values/);
284
+ expect(await alive(world, entity)).toBe(true);
285
+ await world.dispose();
286
+ });
287
+ });
@@ -70,6 +70,12 @@ export const createEntities = (memory: Memory): Entities => {
70
70
  /** Reserves an entity. Safe to call from any thread between sync points. */
71
71
  export const reserve = ({ cursor, free, generation }: Reservations): Entity => entityAt(cursor, free, generation, Atomics.add(cursor, taken, 1));
72
72
 
73
+ /** Reserves an entity for each of `into`'s elements, with one atomic add. Safe to call from any thread between sync points. */
74
+ export const reserveRun = ({ cursor, free, generation }: Reservations, into: Float64Array): void => {
75
+ const first = Atomics.add(cursor, taken, into.length);
76
+ for (let i = 0; i < into.length; i++) into[i] = entityAt(cursor, free, generation, first + i);
77
+ };
78
+
73
79
  /**
74
80
  * Reservations a thread takes in blocks of positions, so threads spawning together don't contend for the cursor.
75
81
  * Positions a block leaves unused are reserved entities that never spawn, which the sync point releases.