@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
@@ -39,7 +39,7 @@ const written = System.create({ kernel: false, components: { with: [scale, cell,
39
39
  row[steps] = x * 1000 + y;
40
40
  });
41
41
  const read: [number, string][] = [];
42
- const results = System.create({ observer: true, components: { with: [value, out, steps] } }, row => { read.push([row[value], `${row[out]} ${row[steps]}`]); });
42
+ const results = System.create({ readonly: true, components: { with: [value, out, steps] } }, row => { read.push([row[value], `${row[out]} ${row[steps]}`]); });
43
43
 
44
44
  const outcome = async (system: typeof compute, wasm: boolean): Promise<readonly string[]> => {
45
45
  await using world = createWorld({ [workers]: 2 }, { minimumChunk: 2, wasm });
@@ -66,7 +66,7 @@ export const whereOnWorkers = async (wasm: boolean): Promise<{ readonly written:
66
66
  await world.run(negative);
67
67
  let written = 0;
68
68
  let left = 0;
69
- await world.run(System.create({ observer: true, components: { with: [value, out] } }, row => {
69
+ await world.run(System.create({ readonly: true, components: { with: [value, out] } }, row => {
70
70
  if (row[out] === 0 && row[value] !== 0) left++;
71
71
  else written++;
72
72
  }));
@@ -39,7 +39,7 @@ const inline = () => createWorld({ [workers]: 0 });
39
39
 
40
40
  // Systems a joined thread runs, which it names by the ids the build gives them at the top level.
41
41
  const heard: string[] = [];
42
- export const listen = System.create({ observer: true, components: { with: [team, health] } }, row => { heard.push(`${row[team]} ${row[health]}`); });
42
+ export const listen = System.create({ readonly: true, components: { with: [team, health] } }, row => { heard.push(`${row[team]} ${row[health]}`); });
43
43
  export const spawnBanners = System.create(({ commands }) => {
44
44
  commands.spawn([team, health, banner], { [team]: "red" })({ [health]: 1 });
45
45
  commands.spawn([team, health, banner], { [team]: "blue" })({ [health]: 2 });
@@ -60,11 +60,11 @@ export const spawnThere = System.create({ components: { with: [banner] } }, (row
60
60
  commands.spawn([health, marker], base)({ [marker]: 1 });
61
61
  commands.spawn([health], base)({});
62
62
  });
63
- export const readBanners = System.create({ observer: true, components: { with: [banner] } }, row => { banners.push(row[banner]?.text ?? "none"); });
63
+ export const readBanners = System.create({ readonly: true, components: { with: [banner] } }, row => { banners.push(row[banner]?.text ?? "none"); });
64
64
 
65
65
  /** Every entity's team and health, and whether it's armed, sorted by health. */
66
66
  const rowsOf = async (world: ReturnType<typeof inline>): Promise<readonly string[]> => {
67
- const read = System.create({ observer: true, components: { with: [team, health], optional: [armed] } }, (row) => {
67
+ const read = System.create({ readonly: true, components: { with: [team, health], optional: [armed] } }, (row) => {
68
68
  found.push(`${row[team]} ${row[health]}${row[armed] === true ? " armed" : ""}`);
69
69
  });
70
70
  const found: string[] = [];
@@ -116,7 +116,7 @@ describe("a shared component", () => {
116
116
  // Three cells' worth of [1, 2] and [2, 1], three weights and one style.
117
117
  expect((await world.tables()).archetypes - before).toBe(2 + 3 + 1);
118
118
  const read: string[] = [];
119
- await world.run(System.create({ observer: true, components: { with: [health], optional: [cell, weight, style] } }, row => {
119
+ await world.run(System.create({ readonly: true, components: { with: [health], optional: [cell, weight, style] } }, row => {
120
120
  if (row[health] === 0) return;
121
121
  read.push(`${row[health]}: ${JSON.stringify(row[cell] ?? row[style] ?? null)} ${String(row[weight])}`);
122
122
  }));
@@ -195,10 +195,10 @@ describe("a shared component", () => {
195
195
  }));
196
196
  expect(looked.sort()).toEqual(["1 sees red", "2 sees red"]);
197
197
  const assign: (row: object) => void = row => { Reflect.set(row, team, "blue"); };
198
- const assigning = System.create({ kernel: false, components: { with: [team, health] } }, row => {
198
+ // Taken whole, the row is written: every member but shared ones and references.
199
+ const assigning = System.create({ kernel: false, components: { with: [team, health], whole: true } }, row => {
199
200
  if (row[health] === 1) assign(row);
200
201
  });
201
- // The build takes the row as written whole, since it can't follow it: every member but shared ones and references.
202
202
  expect((await runReported(world, assigning)).failures.map(failure => failure.message)).toEqual([
203
203
  expect.stringContaining(`"sharedTest/team" is shared: its value is its archetype's, so give it another with commands.add rather than assigning it`),
204
204
  ]);
@@ -216,8 +216,8 @@ describe("a shared component", () => {
216
216
  commands.spawn([team, health], { [team]: "red" })({ [health]: 2 });
217
217
  }));
218
218
  const changes: string[] = [];
219
- const watch = System.create({ observer: true, components: { with: [health], changed: [team] } }, row => { changes.push(`changed ${row[health]}`); });
220
- const adds = System.create({ observer: true, components: { with: [health], added: [team] } }, row => { changes.push(`added ${row[health]}`); });
219
+ const watch = System.create({ readonly: true, components: { with: [health], changed: [team] } }, row => { changes.push(`changed ${row[health]}`); });
220
+ const adds = System.create({ readonly: true, components: { with: [health], added: [team] } }, row => { changes.push(`added ${row[health]}`); });
221
221
  world.add(watch);
222
222
  world.add(adds);
223
223
  await world.run(System.create(() => {}));
@@ -294,7 +294,7 @@ describe("a shared component", () => {
294
294
  }));
295
295
  await world.run(System.create({ components: { with: [weight, health] } }, row => { row[health] *= row[weight]; }));
296
296
  const read: number[] = [];
297
- await world.run(System.create({ observer: true, components: { with: [health] } }, row => { read.push(row[health]); }));
297
+ await world.run(System.create({ readonly: true, components: { with: [health] } }, row => { read.push(row[health]); }));
298
298
  expect(read.sort()).toEqual([2, 3]);
299
299
  });
300
300
 
@@ -338,7 +338,7 @@ describe("a shared component", () => {
338
338
  await world.run(writeBanners);
339
339
  await world.run(spawnThere);
340
340
  const found: string[] = [];
341
- await world.run(System.create({ observer: true, components: { with: [health], optional: [marker, team] } }, row => {
341
+ await world.run(System.create({ readonly: true, components: { with: [health], optional: [marker, team] } }, row => {
342
342
  if (row[team] === undefined) found.push(`${row[health]} ${row[marker] ?? "none"}`);
343
343
  }));
344
344
  expect(found.sort()).toEqual(["7 1", "7 1", "7 none", "7 none"]);
@@ -613,7 +613,7 @@ export const createSharing = (host: SharingHost) => {
613
613
  const { components, resources } = declared;
614
614
  // Unnamed, so other systems' labels stay as they are; its own is the thread's.
615
615
  const declaration: Resolved = {
616
- observer: true,
616
+ readonly: true,
617
617
  // Its `where` runs on its thread; here, an empty one says it has one, which every archetype passes.
618
618
  ...components === null ? {} : { components: { ...listed([["read", components.read], ["has", components.has], ["optional", components.optional], ["not", components.not], ["added", components.added], ["changed", components.changed]]), ...components.where ? { where: {} } : {} } },
619
619
  ...resources === null ? {} : { resources: listed([["read", resources.read], ["changed", resources.changed], ["is", resources.is], ["not", resources.not]]) },
@@ -69,14 +69,15 @@ export const createBatch = (layouts: readonly Layout[], ids: readonly number[],
69
69
  return { ids: sorted, shared, archetype: null, columns: sorted.map(id => columnOf(layouts[id], id, initialCapacity)), entities: new Float64Array(initialCapacity), count: 0, capacity: initialCapacity, revive: null, generation: 0 };
70
70
  };
71
71
 
72
- const grow = (batch: SpawnBatch): void => {
72
+ /** Grows a batch to hold at least `needed` rows: twice its capacity, or `needed` where that's more. */
73
+ const grow = (batch: SpawnBatch, needed = batch.count + 1): void => {
73
74
  const { revive } = batch;
74
75
  if (revive !== null) {
75
76
  batch.revive = null;
76
77
  revive();
77
- if (batch.count < batch.capacity) return;
78
+ if (needed <= batch.capacity) return;
78
79
  }
79
- const capacity = batch.capacity * 2;
80
+ const capacity = Math.max(batch.capacity * 2, needed);
80
81
  for (const column of batch.columns) {
81
82
  const views = viewsOf(new ArrayBuffer(capacity * column.stride), 0, capacity * column.stride);
82
83
  views.u8.set(column.views.u8.subarray(0, batch.count * column.stride));
@@ -146,6 +147,18 @@ export const openRow = (batch: SpawnBatch): number => {
146
147
  return batch.count;
147
148
  };
148
149
 
150
+ /** The first of the `n` rows the batch's next spawns write, growing it first to hold them. */
151
+ export const openRows = (batch: SpawnBatch, n: number): number => {
152
+ if (batch.count + n > batch.capacity) grow(batch, batch.count + n);
153
+ return batch.count;
154
+ };
155
+
156
+ /** Claims the open rows for `entities`, once all their values are written. */
157
+ export const claimRows = (batch: SpawnBatch, entities: Float64Array): void => {
158
+ batch.entities.set(entities, batch.count);
159
+ batch.count += entities.length;
160
+ };
161
+
149
162
  /** Claims the open row for `entity`, once all its values are written. */
150
163
  export const claim = (batch: SpawnBatch, entity: number): void => {
151
164
  batch.entities[batch.count] = entity;
@@ -1,4 +1,5 @@
1
1
  // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import type { Recording } from './run-changes.js';
2
3
  import { createHolds, type Holds } from './stop-lists.js';
3
4
  import type { Entity } from '../../interface/entity/entity.js';
4
5
  import type { Leaf } from '../../interface/system/leaf.js';
@@ -126,6 +127,13 @@ export type State = {
126
127
  reshapes: number;
127
128
  /** Sync points applied, which reclaiming counts. */
128
129
  syncs: number;
130
+ /**
131
+ * By id, each resource a changes finalizer watches: its value and change tick when its finalizers last ran, which a
132
+ * later write's report gives as its previous value. Empty while none watches one.
133
+ */
134
+ readonly resourcesSeen: Map<number, { value: unknown; tick: number }>;
135
+ /** What the run in progress records of its changes, when it was asked for them; null otherwise. */
136
+ recording: Recording | null;
129
137
  /** The records strings' columns name, in shared memory; null without it, where strings are held as objects. */
130
138
  readonly strings: StringStore | null;
131
139
  /** Whether each component is derived, so only rules add and remove it. */
@@ -317,6 +325,8 @@ export const createState = (shared?: boolean, wasm = true, simd = true, gpu = fa
317
325
  membership: { log: [], base: 0 },
318
326
  reshapes: 0,
319
327
  syncs: 0,
328
+ resourcesSeen: new Map(),
329
+ recording: null,
320
330
  strings: memory.shared ? createStrings(memory) : null,
321
331
  derived: [],
322
332
  transfers: [],
@@ -504,6 +514,15 @@ export const commandIdOf = (state: State, key: unknown): number => {
504
514
  return id;
505
515
  };
506
516
 
517
+ /** The id of a component a query names, derived ones included. Throws for a key that isn't a registered component. */
518
+ export const queryIdOf = (state: State, key: unknown): number => {
519
+ const known = typeof key === "symbol" ? state.ids.get(key) : undefined;
520
+ if (known !== undefined && !state.resources.has(known)) return known;
521
+ if (typeof key !== "symbol") throw new Error(`"${labelOf(key)}" isn't a component: a component's key is a symbol.`);
522
+ if (kindOf(key) === "resource") throw new Error(`"${labelOf(key)}" is a resource, not a component.`);
523
+ return idOf(state, key);
524
+ };
525
+
507
526
  /**
508
527
  * An archetype's key: its sorted component ids, each shared one with the number of its value, so archetypes of one
509
528
  * component set but other values are others.
@@ -0,0 +1,135 @@
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 { Index } from "../../interface/indexes/index-type.js";
5
+ import { System } from "../../interface/system/system.js";
6
+ import { workers } from "../../interface/resources/workers.js";
7
+ import { createWorld } from "./create.js";
8
+ import { count as countOf, readEach, select } from "./reads.test-helper.js";
9
+
10
+ const position = Symbol("syncPointsTest/position");
11
+ const cell = Symbol("syncPointsTest/cell");
12
+ const found = Symbol("syncPointsTest/found");
13
+ const team = Symbol("syncPointsTest/team");
14
+ const member = Symbol("syncPointsTest/member");
15
+ const local = Symbol("syncPointsTest/local");
16
+ const total = Symbol("syncPointsTest/total");
17
+ const parent = Symbol("syncPointsTest/parent");
18
+ const seed = Symbol("syncPointsTest/seed");
19
+ const grown = Symbol("syncPointsTest/grown");
20
+ const children = Symbol("syncPointsTest/children");
21
+
22
+ declare module "@adobe/ecs" {
23
+ export interface Components {
24
+ [position]: { type: number };
25
+ [cell]: { type: number };
26
+ [team]: { type: true };
27
+ [member]: { type: Entity; reference: "owner" };
28
+ [local]: { type: number };
29
+ [total]: { type: number };
30
+ [parent]: { type: Entity; reference: "weak" };
31
+ [seed]: { type: number };
32
+ [grown]: { type: number };
33
+ }
34
+ export interface Resources {
35
+ [found]: { type: number };
36
+ [children]: { type: number; default: 0 };
37
+ }
38
+ }
39
+
40
+ const byCell = Index.create({ key: [cell] });
41
+
42
+ const spawn = System.create(({ commands }) => { for (let k = 0; k < 3; k++) commands.spawn([position])({ [position]: k }); });
43
+ const counted: number[] = [];
44
+ const count = System.create({ components: { with: [position] }, after: [spawn] }, row => { counted.push(row[position]); });
45
+ const changes: number[] = [];
46
+ const watchChanges = System.create({ components: { with: [position], changed: [position] }, after: [count] }, row => { changes.push(row[position]); });
47
+ const deriveCells = System.create({ components: { with: [position], not: [cell] }, after: [watchChanges] }, (row, { commands }) => { commands.add(row.entity, cell, row[position] % 2); });
48
+ const pick = System.create({ lookup: { cells: { index: byCell } }, after: [deriveCells] }, ({ lookup, resources }) => { resources[found] = lookup.cells(0).length; });
49
+ const readFound = System.create({ readonly: true }, ({ resources }) => resources[found]);
50
+ const frame = System.create([spawn, count, watchChanges, deriveCells, pick]);
51
+
52
+ const spawnTeams = System.create(({ commands }) => {
53
+ for (let k = 0; k < 3; k++) {
54
+ const owner = commands.spawn([team])();
55
+ for (let m = 0; m < 2; m++) commands.spawn([member])({ [member]: owner });
56
+ }
57
+ });
58
+ const disband = System.create({ components: { with: [team] }, after: [spawnTeams] }, (row, { commands }) => { commands.despawn(row.entity); });
59
+ const round = System.create([spawnTeams, disband]);
60
+
61
+ /** Spawns a root, `count` children of it and a child of each. */
62
+ const spawnTree = System.create({}, ({ commands }, input: { readonly count: number }) => {
63
+ const root = commands.spawn([local, total])({ [local]: 1, [total]: 0 });
64
+ for (let k = 0; k < input.count; k++) {
65
+ const child = commands.spawn([local, total, parent])({ [local]: 1, [total]: 0, [parent]: root });
66
+ commands.spawn([local, total, parent])({ [local]: 1, [total]: 0, [parent]: child });
67
+ }
68
+ });
69
+ const propagate = System.create({ components: { with: [local, total], changed: [local] }, up: { through: parent }, after: [spawnTree] }, (row, { up }) => {
70
+ row[total] = (up === null ? 0 : up[total]) + row[local];
71
+ });
72
+ const grow = System.create([spawnTree, propagate]);
73
+
74
+ const plant = System.create(({ commands }) => { for (let k = 0; k < 64; k++) commands.spawn([seed])({ [seed]: k }); });
75
+ const sprout = System.create({ kernel: true, components: { with: [seed] }, after: [plant] }, (row, { commands }) => {
76
+ commands.spawn([grown])({ [grown]: row[seed] * 2 });
77
+ });
78
+ const tally = System.create({ components: { with: [grown] }, after: [sprout] }, (row, { resources }) => { resources[children] += row[grown]; });
79
+ const garden = System.create([plant, sprout, tally]);
80
+ const readChildren = System.create({ readonly: true }, ({ resources }) => resources[children]);
81
+
82
+ describe("sync points within a run", () => {
83
+ it("apply commands before a later system that depends on what they change, which then sees them in the same run", async () => {
84
+ const world = createWorld({ [workers]: 0 });
85
+ await world.run(frame);
86
+ expect(counted).toEqual([0, 1, 2]);
87
+ expect(await world.run(readFound)).toBe(2);
88
+ await world.dispose();
89
+ });
90
+ it("keep each change seen once by change filters across runs", async () => {
91
+ const world = createWorld({ [workers]: 0 });
92
+ changes.length = 0;
93
+ await world.run(frame);
94
+ expect(changes).toEqual([0, 1, 2]);
95
+ changes.length = 0;
96
+ counted.length = 0;
97
+ await world.run(frame);
98
+ // The second run's spawns are new; the first run's three are seen again by count alone.
99
+ expect([counted.length, changes]).toEqual([6, [0, 1, 2]]);
100
+ expect(await world.run(readFound)).toBe(4);
101
+ await world.dispose();
102
+ });
103
+
104
+ it("despawn what rows spawned at an earlier sync point of the run own, once a later tier despawns their owners", async () => {
105
+ expect(System.describe(round).tiers.map(tier => tier.sync === null)).toEqual([false, true]);
106
+ await using world = createWorld({ [workers]: 0 });
107
+ await world.run(round);
108
+ expect([await countOf(world, { required: [team] }), await countOf(world, { required: [member] })]).toEqual([0, 0]);
109
+ });
110
+
111
+ it("bring an `up` tree up to date at each, built again or patched, for a tier after it in the same run", async () => {
112
+ expect(System.describe(grow).tiers.map(tier => tier.sync === null)).toEqual([false, true]);
113
+ await using world = createWorld({ [workers]: 0 });
114
+ let spawned = 0;
115
+ // Past the journal's worth a tree patches, then within it.
116
+ for (const count of [5_000, 3_000, 10]) {
117
+ await world.run(grow, { count });
118
+ spawned += count;
119
+ const totals = await readEach(world, await select(world, { required: [total] }), total);
120
+ const byDepth = [1, 2, 3].map(depth => totals.filter(value => value === depth).length);
121
+ expect([byDepth, totals.length]).toEqual([[totals.length - 2 * spawned, spawned, spawned], totals.length]);
122
+ }
123
+ });
124
+
125
+ it("commit a kernel's staged spawns at a sync point within the run, for a later tier", async () => {
126
+ expect(System.describe(garden).tiers.map(tier => tier.sync === null)).toEqual([false, false, true]);
127
+ await using world = createWorld({ [workers]: 0 });
128
+ const sum = 63 * 64;
129
+ await world.run(garden);
130
+ expect(await world.run(readChildren)).toBe(sum);
131
+ // The second run's seeds are both runs' plantings, and its tally reads the first run's grown rows too.
132
+ await world.run(garden);
133
+ expect(await world.run(readChildren)).toBe(sum + sum + 2 * sum);
134
+ });
135
+ });
@@ -2,10 +2,10 @@
2
2
  import type { Views } from '../../interface/registry/layout.js';
3
3
  import { copyValue, ensureCapacity, swapRemove, writeF32, writeF32Lanes, writeF64, writeI32, writeU32, type Archetype, type Storage } from './archetype.js';
4
4
  import { blockShift, chunkShift } from './kernel.js';
5
- import { addCommand, clearBuffer, despawnCommand, endOf, referenceAt, removeCommand, spawnCommand, type CommandBuffer } from './buffer.js';
5
+ import { addCommand, clearBuffer, despawnCommand, despawnWhereCommand, despawnWhereHolds, endOf, referenceAt, removeCommand, spawnCommand, type CommandBuffer } from './buffer.js';
6
6
  import { readCell, writeCell } from './cells.js';
7
7
  import { commitBatches, commitStaged } from './batches.js';
8
- import { applyIndexChanges, cascadeReferences, indexChangesOf, type IndexChanges, type Written } from './cascade.js';
8
+ import { applyIndexChanges, cascadeReferences, indexChangesOf, linkBorn, type IndexChanges, type Written } from './cascade.js';
9
9
  import { refreshDirectories } from './directories.js';
10
10
  import { createArrivals, logArrival } from './arrivals.js';
11
11
  import { noteEdge, noteEmptied, reclaimEmptied } from './reclaim.js';
@@ -21,6 +21,8 @@ import type { Plan } from './plan.js';
21
21
  import type { MoveLog } from './move-log.js';
22
22
  import { archetypeOf, movesOf, sharedNumberOf, withComponent, withoutComponent, withShared, type State } from './state.js';
23
23
  import { releaseValue } from './transfers.js';
24
+ import { resourcesWritten } from './resource-changes.js';
25
+ import { recordPlan } from './run-changes.js';
24
26
 
25
27
  /** Buffers queued for the next sync point, in merge order. Commands in guarded buffers can't touch the resource entity. */
26
28
  export type Queued = { readonly buffer: CommandBuffer; readonly guarded: boolean };
@@ -127,6 +129,56 @@ const releaseDropped = (state: State, buffer: CommandBuffer, command: number): v
127
129
  }
128
130
  };
129
131
 
132
+ /**
133
+ * Whether `archetype` has each component of `ids` from `start` to `middle` and none from there to `end`, and holds the
134
+ * values `holds` tells it holds, when it isn't null.
135
+ */
136
+ const matches = (archetype: Archetype, ids: Int32Array, start: number, middle: number, end: number, holds: Function | null): boolean => {
137
+ const { storageById } = archetype;
138
+ for (let k = start; k < middle; k++) if (storageById[ids[k]] === undefined) return false;
139
+ for (let k = middle; k < end; k++) if (storageById[ids[k]] !== undefined) return false;
140
+ return holds === null || Reflect.apply(holds, undefined, [archetype]) === true;
141
+ };
142
+
143
+ /** The archetype an entity planned into `archetype` ends up in once derived. */
144
+ const closedOf = (state: State, archetype: Archetype): Archetype => {
145
+ const cached = state.structure.closures[archetype.id] ?? -1;
146
+ return cached >= 0 ? state.archetypes[cached] : closureOf(state.structure, state.derived, archetype, (components, shared) => archetypeOf(state, components, shared), state.archetypes);
147
+ };
148
+
149
+ /**
150
+ * Despawns each entity a `despawnWhere` command selects given the plan so far: those planned, by where they're going,
151
+ * then the rest, by the archetype they're in. Never the resource entity.
152
+ */
153
+ const despawnWhere = (state: State, buffer: CommandBuffer, command: number, within: boolean): void => {
154
+ const { plan, archetypes } = state;
155
+ const { ids } = buffer;
156
+ const start = buffer.starts[command];
157
+ const middle = start + buffer.entities[command];
158
+ const last = endOf(buffer, command);
159
+ const where = ids[last - 1] === despawnWhereHolds ? buffer.values[last - 1] : null;
160
+ const holds = typeof where === "function" ? where : null;
161
+ const end = holds === null ? last : last - 1;
162
+ for (let entry = 0; entry < plan.count; entry++) {
163
+ if (plan.despawned[entry] === 1 || plan.entity[entry] === state.resourceEntity) continue;
164
+ const to = archetypes[plan.to[entry]];
165
+ if (matches(within && plan.to[entry] !== plan.from[entry] ? closedOf(state, to) : to, ids, start, middle, end, holds)) plan.despawned[entry] = 1;
166
+ }
167
+ const { slots } = plan;
168
+ const count = archetypes.length;
169
+ for (let id = 0; id < count; id++) {
170
+ const archetype = archetypes[id];
171
+ if (archetype.resource || archetype.count === 0 || !matches(archetype, ids, start, middle, end, holds)) continue;
172
+ ensurePlanCapacity(state, plan.count + archetype.count);
173
+ const { entities } = archetype;
174
+ for (let row = 0; row < archetype.count; row++) {
175
+ const entity = entities[row];
176
+ const index = indexOf(entity);
177
+ if (slots[index] < 0) plan.despawned[addPlanned(state, entity, index, id, id)] = 1;
178
+ }
179
+ }
180
+ };
181
+
130
182
  /**
131
183
  * Plan: the net change for each entity, in issue order. Commands against dead entities do nothing. Reference values
132
184
  * written by commands go to `written`, to check once despawns are known.
@@ -146,6 +198,11 @@ const planOf = (state: State, queued: readonly Queued[], written: Written): void
146
198
  ensurePlanCapacity(state, plan.count + buffer.count);
147
199
  for (let command = 0; command < buffer.count; command++) {
148
200
  const kind = kinds[command];
201
+ if (kind === despawnWhereCommand) {
202
+ despawnWhere(state, buffer, command, within);
203
+ ensurePlanCapacity(state, plan.count + buffer.count - command);
204
+ continue;
205
+ }
149
206
  const entity = entities[command];
150
207
  if (guarded && entity === state.resourceEntity) continue;
151
208
  const index = indexOf(entity);
@@ -631,7 +688,9 @@ const noneQueued: readonly Queued[] = [];
631
688
  export const syncPending = (state: State, queued: readonly Queued[] = state.queued): boolean =>
632
689
  anyCommands(queued) || reserved(state.entities) || state.carried !== null || state.structure.rederive
633
690
  // Failures for its run, as a gone thread's stops it never finalized are.
634
- || state.unplaced.length > 0;
691
+ || state.unplaced.length > 0
692
+ // A watched resource written, which its changes finalizers see at a sync point.
693
+ || (state.resourcesSeen.size > 0 && resourcesWritten(state));
635
694
 
636
695
  /**
637
696
  * The first half of a sync point: commits batch spawns, plans every command, cascades references and closes the plan
@@ -688,22 +747,9 @@ const preparePending = (state: State, queued: readonly Queued[], isolated: boole
688
747
  if (deriving) for (let entry = 0; entry < plan.count; entry++) if (plan.despawned[entry] === 0) plan.to[entry] = close(state.archetypes[plan.to[entry]]).id;
689
748
  let index: IndexChanges | null = null;
690
749
  if (structure.referenceIds.length > 0) {
691
- for (const [id, first] of born) {
692
- const archetype = state.archetypes[id];
693
- for (const component of structure.referenceIds) {
694
- const storage = archetype.storageById[component];
695
- if (storage === undefined) continue;
696
- for (let row = first; row < archetype.count; row++) written.push(archetype.entities[row], component, Number(readCell(storage, row)));
697
- }
698
- }
750
+ linkBorn(state, born, written, tick);
699
751
  cascadeReferences(state, queued, written, entity => entryOf(state, entity));
700
- const fromOf = (entry: number): Archetype | null => {
701
- const from = plan.from[entry];
702
- if (from < 0) return null;
703
- const first = born.get(from);
704
- return first !== undefined && state.entities.row[indexOf(plan.entity[entry])] >= first ? null : state.archetypes[from];
705
- };
706
- index = indexChangesOf(state, queued, fromOf, [...born].map(([id, first]) => [state.archetypes[id], first] as const));
752
+ index = indexChangesOf(state, queued, entry => plan.from[entry] < 0 ? null : state.archetypes[plan.from[entry]]);
707
753
  }
708
754
  // References change only here: each component written to is stamped, and each holder journaled, as are spawns.
709
755
  for (let k = 1; k < written.length; k += 3) state.referencesWritten[written[k]] = tick;
@@ -741,6 +787,7 @@ export const applySync = (state: State): readonly Thrown[] => {
741
787
  let errors = nothingThrown;
742
788
  try {
743
789
  if (state.unplaced.length > 0 && !isolated) errors = state.unplaced.splice(0);
790
+ if (state.recording !== null && !isolated) recordPlan(state, state.recording, queued, born);
744
791
  if (state.finalizers.length > 0 && !isolated) {
745
792
  const thrown = runFinalizers(state, queued, born, tick);
746
793
  errors = errors.length === 0 ? thrown : [...errors, ...thrown];
@@ -0,0 +1,70 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import { labelOf } from '../../interface/registry/names.js';
3
+ import type { Tables, TablesDeclaration } from '../../interface/system/tables.js';
4
+ import type { Table } from '../../interface/world/table/table.js';
5
+ import type { Archetype, Storage } from './archetype.js';
6
+ import { readCell } from './cells.js';
7
+ import { resourceCell } from './resources.js';
8
+ import { idOf, type Key, type State } from './state.js';
9
+ import { holdsValuesOf } from './where.js';
10
+
11
+ // Reading tables, apart from spawning them: rows' contexts import it, and what spawning imports would make a cycle.
12
+
13
+ /** What an archetype's storage of a component gives a table: a copy of its rows `0` to `count`. */
14
+ const columnOf = (key: Key, storage: Storage, count: number): Table.Column => {
15
+ const { layout } = storage;
16
+ switch (layout.kind) {
17
+ case "linear": return { kind: "bytes", bytes: storage.views.u8.slice(0, count * layout.stride), stride: layout.stride };
18
+ case "constant": return { kind: "value", value: layout.value };
19
+ case "object":
20
+ if (storage.remote !== null) throw new Error(`Tables can't read "${labelOf(key)}": a joined thread fills its objects.`);
21
+ return { kind: "values", values: storage.values.slice(0, count) };
22
+ case "string": {
23
+ const values: unknown[] = new Array(count);
24
+ for (let row = 0; row < count; row++) values[row] = readCell(storage, row);
25
+ return { kind: "values", values };
26
+ }
27
+ }
28
+ };
29
+
30
+ /**
31
+ * A readonly system's `tables`, read by the function this returns, once per read: of each archetype matching `with`,
32
+ * `not` and `where` and holding any of `components`, its entities and a copy of the column of each of them it holds;
33
+ * and the value of each resource of `resources`. `where` holds its values as a system's does, and an archetype
34
+ * matches it as one matches a system's `where`, required as `with` requires them.
35
+ */
36
+ export const tablesReader = (state: State, declaration: TablesDeclaration): (() => Tables) => {
37
+ const holds = declaration.where === undefined ? null : holdsValuesOf(state, declaration.where, true);
38
+ if (typeof holds === "symbol") throw new Error(`Tables' where names "${labelOf(holds)}", which isn't a shared component.`);
39
+ const where = declaration.where === undefined ? [] : Object.getOwnPropertySymbols(declaration.where);
40
+ return () => readTables(state, declaration, [...where, ...declaration.with ?? []], holds);
41
+ };
42
+
43
+ const readTables = (state: State, declaration: TablesDeclaration, requiring: readonly Key[], holds: ((archetype: Archetype) => boolean) | null): Tables => {
44
+ const asked = declaration.components.map(key => ({ key, id: idOf(state, key) }));
45
+ const required = requiring.map(key => idOf(state, key));
46
+ const excluded = (declaration.not ?? []).map(key => idOf(state, key));
47
+ const archetypes: Table[] = [];
48
+ for (const archetype of state.archetypes) {
49
+ const { count } = archetype;
50
+ if (count === 0 || archetype.reclaimed) continue;
51
+ const { storageById } = archetype;
52
+ if (required.some(id => storageById[id] === undefined) || excluded.some(id => storageById[id] !== undefined)) continue;
53
+ if (holds !== null && !holds(archetype)) continue;
54
+ const columns: { [key: symbol]: Table.Column } = {};
55
+ let held = false;
56
+ for (const { key, id } of asked) {
57
+ const storage = storageById[id];
58
+ if (storage === undefined) continue;
59
+ columns[key] = columnOf(key, storage, count);
60
+ held = true;
61
+ }
62
+ if (held) archetypes.push({ count, entities: archetype.entities.slice(0, count), columns });
63
+ }
64
+ const resources: { [key: symbol]: unknown } = {};
65
+ for (const key of declaration.resources ?? []) {
66
+ const cell = resourceCell(state, key);
67
+ resources[key] = readCell(cell.storage, cell.row);
68
+ }
69
+ return { archetypes, resources };
70
+ };