@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
@@ -22,7 +22,7 @@ declare module "@adobe/ecs" {
22
22
 
23
23
  /** What the observer system read, were it run: this test stands for the thread, so it never runs. */
24
24
  const seen: number[] = [];
25
- const watch = System.create({ observer: true, components: { with: [amount] } }, row => { seen.push(row[amount]); });
25
+ const watch = System.create({ readonly: true, components: { with: [amount] } }, row => { seen.push(row[amount]); });
26
26
 
27
27
  describe("an observer system on a joined thread", () => {
28
28
  it("is waited for, not cancelled, by a sync point after a writer copied the rest of a post the thread claimed partway", async () => {
@@ -42,7 +42,7 @@ const channel = scopeChannel<FromJoiningWorker, ToJoiningWorker>();
42
42
  let hosted: ReturnType<typeof createWorld> | null = null;
43
43
  /** What the observer system `listen` adds heard. */
44
44
  const heard: number[] = [];
45
- const hear = System.create({ observer: true }, ({ resources }) => { heard.push(resources[total]); });
45
+ const hear = System.create({ readonly: true }, ({ resources }) => { heard.push(resources[total]); });
46
46
  const messageOf = (error: unknown): string => error instanceof Error ? error.message : String(error);
47
47
  /** Ends this thread at once: node's worker exits, and a browser worker closes, dropping the tasks it queued. */
48
48
  const exit = (): void => {
@@ -16,7 +16,7 @@ import { add, drive, idle, names, shelf, total, until } from "./joining.test-hel
16
16
  /** A system only this thread imports, which a world on a worker never did. */
17
17
  const unimportedByWorker = System.create({}, () => {});
18
18
  /** An observer system only this thread imports, reading an object of `main`, so it's placed on a thread claiming it. */
19
- const shelvedHere = System.create({ observer: true }, ({ resources }) => resources[shelf].length);
19
+ const shelvedHere = System.create({ readonly: true }, ({ resources }) => resources[shelf].length);
20
20
 
21
21
  type Worker = {
22
22
  readonly ask: (message: ToJoiningWorker, transfer?: readonly unknown[]) => Promise<FromJoiningWorker>;
@@ -36,7 +36,7 @@ export const append = System.create({}, ({ resources }, input: { readonly name:
36
36
  return resources[names].length;
37
37
  });
38
38
  /** Reads `total` from an observer system, which a joined thread runs itself. */
39
- export const readTotal = System.create({ observer: true }, ({ resources }) => resources[total]);
39
+ export const readTotal = System.create({ readonly: true }, ({ resources }) => resources[total]);
40
40
  export const spawn = System.create({}, ({ commands }, input: { readonly x: number }) => {
41
41
  commands.spawn([point, label, marked])({ [point]: [input.x, -input.x], [label]: `p${input.x}` });
42
42
  });
@@ -63,7 +63,7 @@ const countAdded = System.create({ components: { added: [value], with: [made] }
63
63
 
64
64
  /** Each made entity's value, in storage order. */
65
65
  const madeValues: number[] = [];
66
- const readMade = System.create({ observer: true, components: { with: [value, made] } }, row => { madeValues.push(row[value]); });
66
+ const readMade = System.create({ readonly: true, components: { with: [value, made] } }, row => { madeValues.push(row[value]); });
67
67
  const valuesOf = async (world: ReturnType<typeof createWorld>): Promise<number[]> => {
68
68
  madeValues.length = 0;
69
69
  await world.run(readMade);
@@ -43,8 +43,7 @@ const failOnce = System.create(() => { throw new Error("unnamed"); });
43
43
 
44
44
  describe("systems, rules and finalizers of one label", () => {
45
45
  it("are distinct in schedules, both run, and are told apart in reports by their ids", async () => {
46
- // Its body is written in place, calling what it's given with the row, which the build takes as writing every member.
47
- const linked = (after: readonly System[], body: (row: { [value]: number }) => void): System => System.create({ components: { with: [value] }, after }, row => body(row));
46
+ const linked = (after: readonly System[], body: (row: { [value]: number }) => void): System => System.create({ components: { with: [value], whole: true }, after }, row => body(row));
48
47
  const a = linked([], row => { ran.push("a"); row[value] += 1; });
49
48
  const b = linked([a], row => { ran.push("b"); row[value] += 1; throw new Error("b failed"); });
50
49
  const c = linked([b], row => { ran.push(`c${row[value]}`); });
@@ -37,7 +37,7 @@ const nothing = System.create({}, () => {});
37
37
 
38
38
  /** Counts passes: the observer passes after each run's sync point. */
39
39
  const passes = { count: 0 };
40
- const counter = System.create({ observer: true }, () => { passes.count++; });
40
+ const counter = System.create({ readonly: true }, () => { passes.count++; });
41
41
 
42
42
  /**
43
43
  * A world of `count` entities, each amount its index and one more, at level 1: the runs below registered, so runners
@@ -184,7 +184,7 @@ describe("observer factories", () => {
184
184
  it("rethrow only their own systems' failures from their runs, not those of observer systems the world added", async () => {
185
185
  using reported = catchOutOfBand();
186
186
  await using world = await seeded(3);
187
- const failing = System.create({ observer: true }, () => { throw new Error("added failed"); });
187
+ const failing = System.create({ readonly: true }, () => { throw new Error("added failed"); });
188
188
  world.add(failing);
189
189
  const caught: unknown[] = [];
190
190
  const original = globalThis.queueMicrotask;
@@ -29,7 +29,7 @@ const count = System.create({}, ({ resources }) => { resources[frames] += 1; });
29
29
  const watch = System.create({}, ({ resources }) => { resources[seen] = resources[clock].tick; });
30
30
  const fail = System.create({}, () => { throw new Error("tick failed"); });
31
31
  const nothing = System.create({}, () => {});
32
- const idle = System.create({ observer: true }, () => {});
32
+ const idle = System.create({ readonly: true }, () => {});
33
33
  const fast = System.create({}, ({ resources }) => {
34
34
  resources[cadence] = { kind: "fixed", step: 1 };
35
35
  resources[always] = true;
@@ -2,7 +2,7 @@
2
2
  import { System } from "../../interface/system/system.js";
3
3
  import type { World } from "../../interface/world/world.js";
4
4
 
5
- const settled = System.create({ observer: true }, () => {});
5
+ const settled = System.create({ readonly: true }, () => {});
6
6
 
7
7
  /**
8
8
  * Resolves once every observer factory subscribed before it had its first value: after the factories' flush, which the
@@ -24,7 +24,7 @@ export type FromContainedWorker =
24
24
 
25
25
  const channel = scopeChannel<FromContainedWorker, ToContainedWorker>();
26
26
  let remote: World | null = null;
27
- const placed = System.create({ observer: true }, () => {});
27
+ const placed = System.create({ readonly: true }, () => {});
28
28
 
29
29
  channel.onMessage(async message => {
30
30
  if ("protocol" in message) return;
@@ -32,7 +32,7 @@ export const setHolding = System.create({}, ({ resources }, input: { readonly ho
32
32
  });
33
33
 
34
34
  /** After each sync point that changed the tally: notes it, and, while `holding` says so, waits there to be released. */
35
- export const watchTally = System.create({ observer: true, resources: { changed: [tally] } }, ({ resources }) => {
35
+ export const watchTally = System.create({ readonly: true, resources: { changed: [tally] } }, ({ resources }) => {
36
36
  const value = resources[tally];
37
37
  tallies.push(value);
38
38
  const { words } = gate;
@@ -29,10 +29,10 @@ const pause = System.create({}, ({ resources }, input: { readonly paused: boolea
29
29
  });
30
30
 
31
31
  const seen: { wide: number[][]; steps: number[] } = { wide: [], steps: [] };
32
- const watchWide = System.create({ observer: true, resources: { changed: [wide] } }, ({ resources }) => {
32
+ const watchWide = System.create({ readonly: true, resources: { changed: [wide] } }, ({ resources }) => {
33
33
  seen.wide.push([...resources[wide]]);
34
34
  });
35
- const watchSteps = System.create({ observer: true, resources: { not: [paused], changed: [steps] } }, ({ resources }) => {
35
+ const watchSteps = System.create({ readonly: true, resources: { not: [paused], changed: [steps] } }, ({ resources }) => {
36
36
  seen.steps.push(resources[steps]);
37
37
  });
38
38
 
@@ -103,7 +103,7 @@ export type Seen = { counter: number; rows: number; wrong: number };
103
103
  export const seen: Seen[] = [];
104
104
 
105
105
  /** Reads every row, which must all be of one tick, pausing at the end of its first chunk while `stall` says so. */
106
- export const watchRows = System.create({ observer: true, components: { with: [stamp, spot, word] } }, (row, { resources }) => {
106
+ export const watchRows = System.create({ readonly: true, components: { with: [stamp, spot, word] } }, (row, { resources }) => {
107
107
  const value = resources[counter];
108
108
  let last = seen.at(-1);
109
109
  if (last === undefined || last.counter !== value || last.rows === 1000) seen.push(last = { counter: value, rows: 0, wrong: 0 });
@@ -117,7 +117,7 @@ export const watchRows = System.create({ observer: true, components: { with: [st
117
117
  export const looked: Entity[] = [];
118
118
 
119
119
  /** Looks up every row by entity, which must all be of one tick, spinning halfway while `stall` says so. */
120
- export const lookRows = System.create({ observer: true, lookup: { found: { with: [stamp, spot] } } }, ({ resources, lookup }) => {
120
+ export const lookRows = System.create({ readonly: true, lookup: { found: { with: [stamp, spot] } } }, ({ resources, lookup }) => {
121
121
  const value = resources[counter];
122
122
  const run: Seen = { counter: value, rows: 0, wrong: 0 };
123
123
  seen.push(run);
@@ -146,7 +146,7 @@ export const nextPhase = System.create({}, ({ resources }) => {
146
146
  export const hot: number[] = [];
147
147
  export const byHeat = Index.create({ key: [heat], sorted: true });
148
148
  /** Counts the rows at least 150 hot, through the sorted index on what the GPU kernel writes. */
149
- export const countHot = System.create({ observer: true, lookup: { hot: { index: byHeat } } }, ({ lookup }) => {
149
+ export const countHot = System.create({ readonly: true, lookup: { hot: { index: byHeat } } }, ({ lookup }) => {
150
150
  hot.push(lookup.hot({ ">=": 150 }).length);
151
151
  });
152
152
 
@@ -164,7 +164,7 @@ export const mottos: { readonly round: number; readonly motto: string }[] = [];
164
164
  * Reads only resources, a string among them: waits on the gate first, while `stall` says so, its round plus one, then
165
165
  * reads the motto, which must be its round's.
166
166
  */
167
- export const watchMotto = System.create({ observer: true }, ({ resources }) => {
167
+ export const watchMotto = System.create({ readonly: true }, ({ resources }) => {
168
168
  const round = resources[mottoRound];
169
169
  pauseAt(resources[stall], round + 1);
170
170
  mottos.push({ round, motto: resources[motto] });
@@ -59,43 +59,43 @@ const reset = (): void => {
59
59
  /** The entities the tests look up, by the joined thread's systems. */
60
60
  const watched: Entity[] = [];
61
61
 
62
- const watchLevel = System.create({ observer: true, resources: { changed: [level] } }, ({ resources }) => {
62
+ const watchLevel = System.create({ readonly: true, resources: { changed: [level] } }, ({ resources }) => {
63
63
  seen.calls++;
64
64
  seen.levels.push(resources[level]);
65
65
  });
66
- const watchAmounts = System.create({ observer: true, components: { with: [amount], changed: [amount] } }, row => {
66
+ const watchAmounts = System.create({ readonly: true, components: { with: [amount], changed: [amount] } }, row => {
67
67
  (seen.amounts[seen.amounts.length - 1] ??= []).push(row[amount]);
68
68
  });
69
- const startAmounts = System.create({ observer: true }, () => { seen.amounts.push([]); });
69
+ const startAmounts = System.create({ readonly: true }, () => { seen.amounts.push([]); });
70
70
  const amountsPass = System.create([startAmounts, watchAmounts]);
71
- const lookChanged = System.create({ observer: true, lookup: { found: { with: [amount], changed: [amount] } } }, ({ lookup }) => {
71
+ const lookChanged = System.create({ readonly: true, lookup: { found: { with: [amount], changed: [amount] } } }, ({ lookup }) => {
72
72
  seen.looked.push(watched.flatMap(entity => {
73
73
  const row = lookup.found(entity);
74
74
  return row === null ? [] : [row[amount], row.changed];
75
75
  }));
76
76
  });
77
- const countTagged = System.create({ observer: true, lookup: { all: { index: allTagged } } }, ({ lookup }) => {
77
+ const countTagged = System.create({ readonly: true, lookup: { all: { index: allTagged } } }, ({ lookup }) => {
78
78
  seen.tagged.push(lookup.all().length);
79
79
  });
80
- const inCell = System.create({ observer: true, lookup: { found: { index: byCell } } }, ({ lookup }) => {
80
+ const inCell = System.create({ readonly: true, lookup: { found: { index: byCell } } }, ({ lookup }) => {
81
81
  const found = lookup.found(11);
82
82
  seen.tagged.push(found.length === 0 ? 0 : found.length * 100 + (found.at(0)?.[amount] ?? -1));
83
83
  });
84
- const failing = System.create({ observer: true }, ({ resources }) => {
84
+ const failing = System.create({ readonly: true }, ({ resources }) => {
85
85
  if (resources[level] > 1) throw new Error("observer joined failure");
86
86
  });
87
- const readLabels = System.create({ observer: true, components: { with: [label, amount] } }, row => {
87
+ const readLabels = System.create({ readonly: true, components: { with: [label, amount] } }, row => {
88
88
  seen.labels.push(`${row[label].text}${row[amount]}`);
89
89
  });
90
90
 
91
91
  /** On the joined thread, as it reads labels: each watched entity's label and amount, which the world's run resolves to. */
92
- const labelsThere = System.create({ observer: true, lookup: { labelled: { with: [label, amount] } } }, ({ lookup }) =>
92
+ const labelsThere = System.create({ readonly: true, lookup: { labelled: { with: [label, amount] } } }, ({ lookup }) =>
93
93
  watched.map(entity => {
94
94
  const row = lookup.labelled(entity);
95
95
  return row === null ? "none" : `${row[label].text}${row[amount]}`;
96
96
  }));
97
97
  /** On the joined thread, as it reads labels: throws there. */
98
- const failsThere = System.create({ observer: true, lookup: { labelled: { with: [label] } } }, ({ lookup }) => {
98
+ const failsThere = System.create({ readonly: true, lookup: { labelled: { with: [label] } } }, ({ lookup }) => {
99
99
  if (lookup.labelled(Entity.none) === null) throw new Error("observerJoinedTest failure there");
100
100
  return 0;
101
101
  });
@@ -218,7 +218,7 @@ describe("observer systems on a joined thread", () => {
218
218
  Index.create({ key: [cell], sorted: true, components: { with: [amount] } }),
219
219
  ];
220
220
  const [madeTagged, madeByCell] = made();
221
- const sizes = System.create({ observer: true, lookup: { all: { index: madeTagged, changed: [cell] }, cells: { index: madeByCell } } }, ({ lookup }) => {
221
+ const sizes = System.create({ readonly: true, lookup: { all: { index: madeTagged, changed: [cell] }, cells: { index: madeByCell } } }, ({ lookup }) => {
222
222
  const all = lookup.all();
223
223
  seen.looked.push([all.changed]);
224
224
  const high = lookup.cells({ ">=": 5 });
@@ -284,10 +284,18 @@ describe("observer systems on a joined thread", () => {
284
284
  await close();
285
285
  });
286
286
 
287
- it("refuse only systems that aren't observer", async () => {
287
+ it("refuse only systems that aren't readonly, saying to declare them so", async () => {
288
288
  const { remote, close } = await joined();
289
- expect(() => remote.add(raise)).toThrow("A joined thread adds only observer systems");
290
- expect(() => remote.remove(raise)).toThrow("A joined thread removes only observer systems");
289
+ expect(() => remote.add(raise)).toThrow(`System "raise" isn't readonly, and a joined thread adds only readonly systems: declare it readonly: true to read the world from this thread, or add it on the world's thread.`);
290
+ expect(() => remote.remove(raise)).toThrow(`System "raise" isn't readonly, and a joined thread removes only readonly systems: others are removed on the world's thread.`);
291
+ await close();
292
+ });
293
+
294
+ it("refuse a readonly system reading tables, which only the world's thread gives", async () => {
295
+ const { remote, close } = await joined();
296
+ const readTables = System.create({ readonly: true, tables: { components: [label] } }, ({ tables }) => tables.archetypes.length);
297
+ expect(() => remote.add(readTables)).toThrow(`System "readTables" reads tables, which only the world's thread gives: run or add it there.`);
298
+ await expect(remote.run(readTables)).rejects.toThrow(`System "readTables" reads tables, which only the world's thread gives: run or add it there.`);
291
299
  await close();
292
300
  });
293
301
 
@@ -309,7 +317,7 @@ describe("observer systems on a joined thread", () => {
309
317
  const { world, close } = await joined(["observerJoinedTest"]);
310
318
  await world.run(spawnLabelled);
311
319
  // Made in a function, so the build gives it no id.
312
- const made = () => System.create({ observer: true, components: { with: [label, amount] } }, row => {
320
+ const made = () => System.create({ readonly: true, components: { with: [label, amount] } }, row => {
313
321
  seen.labels.push(`${row[label].text}${row[amount]}`);
314
322
  });
315
323
  const madeLabels = made();
@@ -27,11 +27,11 @@ const warm = System.create({ kernel: "gpu", components: { with: [rate, heat] } }
27
27
  });
28
28
 
29
29
  let total = 0;
30
- const sum = System.create({ observer: true, components: { with: [heat] } }, row => { total += row[heat]; });
30
+ const sum = System.create({ readonly: true, components: { with: [heat] } }, row => { total += row[heat]; });
31
31
 
32
32
  const byHeat = Index.create({ key: [heat], sorted: true });
33
33
  let hot = 0;
34
- const countHot = System.create({ observer: true, lookup: { hot: { index: byHeat } } }, ({ lookup }) => {
34
+ const countHot = System.create({ readonly: true, lookup: { hot: { index: byHeat } } }, ({ lookup }) => {
35
35
  hot = lookup.hot({ ">=": 150 }).length;
36
36
  });
37
37
 
@@ -37,33 +37,33 @@ const grow = System.create({ components: { with: [amount] } }, row => { row[amou
37
37
  const nothing = System.create({}, () => {});
38
38
 
39
39
  // It keeps its context, which the build can't follow, so it's given what it reads.
40
- const watchLevel = System.create(giveAccess({ observer: true }, { resources: { read: [level] } }), context => {
40
+ const watchLevel = System.create(giveAccess({ readonly: true }, { resources: { read: [level] } }), context => {
41
41
  seen.levels.push(context.resources[level]);
42
42
  seen.contexts.push(context);
43
43
  });
44
- const watchAmounts = System.create({ observer: true, components: { with: [amount], changed: [amount] } }, row => { seen.amounts.push(row[amount]); });
45
- const count = System.create({ observer: true, components: { with: [amount] } }, () => { seen.counted.push(1); });
46
- const inCell = System.create({ observer: true, lookup: { found: { index: byCell } } }, ({ lookup }) => {
44
+ const watchAmounts = System.create({ readonly: true, components: { with: [amount], changed: [amount] } }, row => { seen.amounts.push(row[amount]); });
45
+ const count = System.create({ readonly: true, components: { with: [amount] } }, () => { seen.counted.push(1); });
46
+ const inCell = System.create({ readonly: true, lookup: { found: { index: byCell } } }, ({ lookup }) => {
47
47
  seen.cells.push(lookup.found(7).length);
48
48
  });
49
- const failing = System.create({ observer: true }, ({ resources }) => {
49
+ const failing = System.create({ readonly: true }, ({ resources }) => {
50
50
  if (resources[level] > 0) throw new Error("observer failed");
51
51
  });
52
52
  const raiseThenWatch = System.create([raise, watchLevel]);
53
- const watchLevelChanges = System.create({ observer: true, resources: { changed: [level] } }, ({ resources }) => {
53
+ const watchLevelChanges = System.create({ readonly: true, resources: { changed: [level] } }, ({ resources }) => {
54
54
  seen.levels.push(resources[level]);
55
55
  });
56
56
 
57
57
  /** The world a removing body acts on. */
58
58
  let current: World | null = null;
59
- const removeLevel = System.create({ observer: true }, () => {
59
+ const removeLevel = System.create({ readonly: true }, () => {
60
60
  seen.removals.push(1);
61
61
  current?.remove(watchLevel);
62
62
  });
63
63
 
64
64
  /** Adds `watchLevel` to the world the test runs, during a run. */
65
65
  const addWatch = System.create({}, () => { current?.add(watchLevel); });
66
- const addLevel = System.create({ observer: true }, () => {
66
+ const addLevel = System.create({ readonly: true }, () => {
67
67
  current?.add(watchLevel);
68
68
  });
69
69
 
@@ -30,7 +30,7 @@ const assign = System.create({ kernel: true, components: { with: [position], opt
30
30
  row[velocity] = row[position];
31
31
  });
32
32
  const totals = { moved: 0, still: 0 };
33
- const sum = System.create({ observer: true, components: { with: [position], optional: [velocity] } }, row => {
33
+ const sum = System.create({ readonly: true, components: { with: [position], optional: [velocity] } }, row => {
34
34
  if (row[velocity] === undefined) totals.still += row[position];
35
35
  else totals.moved += row[position];
36
36
  });
@@ -73,7 +73,7 @@ const copySpeedKernel = System.create({
73
73
  if (found !== null) row[order] = found[velocity] === undefined ? -found[position] : found[velocity];
74
74
  });
75
75
  const speeds: number[] = [];
76
- const readSpeeds = System.create({ observer: true, components: { with: [other, order] } }, row => { speeds.push(row[order]); });
76
+ const readSpeeds = System.create({ readonly: true, components: { with: [other, order] } }, row => { speeds.push(row[order]); });
77
77
 
78
78
  /** What finalizers read of rows that stopped, on the world's thread and on the markers'. */
79
79
  const stopped: string[] = [];
@@ -87,7 +87,7 @@ export const finalizeThere = Finalizer.create({ components: { with: [position, m
87
87
  const despawnAll = System.create({ components: { with: [position] } }, (row, { commands }) => { commands.despawn(row.entity); });
88
88
 
89
89
  let seen: (readonly [number, number | undefined, string | undefined])[] = [];
90
- const read = System.create({ observer: true, components: { with: [position], optional: [velocity, label] } }, row => {
90
+ const read = System.create({ readonly: true, components: { with: [position], optional: [velocity, label] } }, row => {
91
91
  seen.push([row[position], row[velocity], row[label]]);
92
92
  });
93
93
  const sorted = () => [...seen].sort((a, b) => a[0] - b[0]);
@@ -109,7 +109,7 @@ const lookedThere = System.create({ components: { with: [position, marker] }, lo
109
109
  });
110
110
  // An observer system there reads copies of rows across archetypes, which tell which have the optional component.
111
111
  let watched: (readonly [number, number | undefined])[] = [];
112
- const watchThere = System.create({ observer: true, components: { with: [position], optional: [velocity] } }, row => {
112
+ const watchThere = System.create({ readonly: true, components: { with: [position], optional: [velocity] } }, row => {
113
113
  watched.push([row[position], row[velocity]]);
114
114
  });
115
115
  // A tail there: it writes only its thread's objects, so it runs after the sync point, reading copies of rows.
@@ -165,7 +165,7 @@ const markEither = System.create({ kernel: true, components: { with: [mark], opt
165
165
  if (row[far] !== undefined) row[mark] = 2;
166
166
  });
167
167
  const marked: number[] = [];
168
- const watchStamped = System.create({ observer: true, components: { with: [mark], changed: [mark] } }, row => { marked.push(row[mark]); });
168
+ const watchStamped = System.create({ readonly: true, components: { with: [mark], changed: [mark] } }, row => { marked.push(row[mark]); });
169
169
  const clearNear = System.create({ kernel: false, components: { with: [mark], optional: [near] } }, row => {
170
170
  if (row[near] !== undefined) row[near] = undefined;
171
171
  });
@@ -195,7 +195,7 @@ describe("optional components", () => {
195
195
 
196
196
  it("pass change filters on other components, and are stamped changed when written", async () => {
197
197
  const changed: number[] = [];
198
- const watch = System.create({ observer: true, components: { with: [velocity], changed: [velocity] } }, row => { changed.push(row[velocity]); });
198
+ const watch = System.create({ readonly: true, components: { with: [velocity], changed: [velocity] } }, row => { changed.push(row[velocity]); });
199
199
  const speedUp = System.create({ components: { with: [position], optional: [velocity] } }, row => {
200
200
  const speed = row[velocity];
201
201
  if (speed !== undefined && row[position] > 50) row[velocity] = speed + 1;
@@ -212,7 +212,7 @@ describe("optional components", () => {
212
212
  it("run in an index's order across archetypes that have them and don't", async () => {
213
213
  const byOrder = Index.create({ key: [order], sorted: true });
214
214
  const ordered: (number | undefined)[] = [];
215
- const list = System.create({ observer: true, components: { with: [position], optional: [velocity] }, order: byOrder }, row => {
215
+ const list = System.create({ readonly: true, components: { with: [position], optional: [velocity] }, order: byOrder }, row => {
216
216
  ordered.push(row[velocity]);
217
217
  });
218
218
  await using world = inline();
@@ -297,7 +297,7 @@ describe("optional components", () => {
297
297
  await world.run(spawnMarked);
298
298
  stoppedThere.length = 0;
299
299
  await world.run(despawnAll);
300
- await remote.run(System.create({ observer: true }, () => {}));
300
+ await remote.run(System.create({ readonly: true }, () => {}));
301
301
  expect(stoppedThere.sort()).toEqual(["10:5:null", "1:none:null"]);
302
302
  await remote.dispose();
303
303
  await world.dispose();
@@ -220,7 +220,7 @@ Shared components and `where`, against `12e366d`, node: the whole suite three ti
220
220
  - **A shared value read in a row costs what a column's does:** `shared read 100K` 4.04–4.10 ns a row against 4.24–4.27 for `column read 100K`, the value bound with the row once per archetype. In a kernel it's a parameter of each call: `shared read 100K kernel` 0.40–0.41 against 0.50 for `column read 100K kernel`, one column fewer to stream.
221
221
  - **Spawning with a shared constant:** `spawn bulk shared`, one spawner reused, 17.3–18.1 ns a spawn against 33.3–34.3 for `spawn bulk plain`, whose cell is a column; `spawn singles shared`, its spawner found by its constants each call, 313–323 ns against 229 for `spawn singles plain`: about 90 ns a call to find the spawner, a tuple's leaves walked through a tree of its stored leaves (`shared.ts`) and each constant in turn through the shape's tree of spawners, with no text built.
222
222
  - **`where` over 1K cells,** 3 × 3 of them in view: `where 1K cells still` 0.49 ns a cell, 0.5 µs a run with nothing evaluated; `where 1K cells moving`, every predicate evaluated again as the camera moves each run, 18.7–19.0 ns a cell, each predicate's resources read once per evaluation.
223
- - **Many small archetypes:** `small archetypes 1K`, 10K rows over 1K archetypes of one cell each, 7.26–7.43 ns a row against 3.46–3.49 for `one archetype 10K`: about 39 ns an archetype, binding its row and walking its filters, which a shared component's values multiply.
223
+ - **Many small archetypes:** `small archetypes 1K`, 10K rows over 1K archetypes of one cell each, 6.8–7.0 ns a row against 3.04 for `one archetype 10K`: about 38 ns an archetype, binding its row, which a shared component's values multiply.
224
224
 
225
225
  Review fixes to shared components and `where`, node, alternating against the commit before each, medians of four to six rounds:
226
226
  - **Arrivals precise** (`arrivals.ts`): a system with `where` and change filters reads a ring of the moves since its last run, each archetype moved into owing the point of the one its rows came from, where every arrival made the archetype count from the oldest point of all matches, found by a scan of them. New `where changed 1K cells drifting in view`, every cell in view, 1% of 10K rows moving to the next cell before each run: 123.6 → 69.2 ns a cell; `where changed 1K cells drifting`, 3 × 3 cells in view, 2.0 → 2.8 ns a cell, the ring's 102 entries a run read, about 7 ns each, and cells moved into from culled ones scanned since that one's point; `changed 1K cells drifting`, without `where`, 52.3 → 52.0. `where 1K cells still` and `moving` within noise.
@@ -241,6 +241,8 @@ Step figures marked (vitest) are from the node vitest lane: read them as relativ
241
241
  kernel, the second call stayed a call and boxed each entity, a double past 2^32. `despawn bulk kernel` 4.60 → 4.12 ms;
242
242
  `despawn bulk` compiled 4.55 ms against 4.32 as written before, 4.10 after.
243
243
  - Inline value writes instead of closures, an 8-shape spawn cache, edges indexed by component id, archetypes grown once per sync point: create 38 → 27.6 ms.
244
+ - Holders of references spawned in a sync point are linked a block at a time before the cascade, from their columns, rather than listed as triples for the cascade and for the holder index each; and `commands.spawnTables` lists owners between its rows only when one is dropped, and reserves a table's entities at once when every row spawns: `spawn tables 1M owner` 160 → 53 ms, `spawn owned 1M` 133 → 64 ms, against 10 ms for `spawn tables 1M`. Each holder is journaled once, so an `up` tree builds again past 4096 journal entries and an eighth of its rows, as indexes do: `up load` 183 → 120 ms.
245
+ - A row loop reads the body into a local once, which V8 otherwise loads again each row, since the call could change it; rows bind without allocating, and filters are found by archetype id: `with 100K` 4.24 → 3.97 ns a row, `one archetype 10K` 3.47 → 3.04, `small archetypes 1K` 7.74 → 6.90, `shared read 100K` 4.07 → 3.78.
244
246
 
245
247
  **Change filters.**
246
248
  - Chunk ticks, the newest tick per 1024 rows, so filters skip unchanged chunks: clustered sparse to ~0.09 ms.
@@ -42,7 +42,7 @@ const caught: number[] = [];
42
42
  export const catchUp = System.create({ kernel: false, components: { with: [position], changed: [position], where: { [cell]: (at, { resources }) => at[0] === resources[camera] } } }, row => { caught.push(row[position]); });
43
43
  // On a joined thread.
44
44
  const heard: number[] = [];
45
- export const hear = System.create({ observer: true, components: { with: [cell, position], where: { [cell]: at => at[0] === 50 } } }, row => { heard.push(row[cell][0] * 1000 + row[position]); });
45
+ export const hear = System.create({ readonly: true, components: { with: [cell, position], where: { [cell]: at => at[0] === 50 } } }, row => { heard.push(row[cell][0] * 1000 + row[position]); });
46
46
 
47
47
  /** Moves an entity to cell `[x, 0]`. */
48
48
  const step = System.create({}, ({ commands }, input: { readonly entity: Entity; readonly x: number }) => { commands.add(input.entity, cell, [input.x, 0]); });
@@ -59,7 +59,7 @@ const spawnAt = (world: ReturnType<typeof inline>, x: number, components: readon
59
59
  /** Every row's cell and position, by position. */
60
60
  const cellsOf = async (world: ReturnType<typeof inline>): Promise<readonly string[]> => {
61
61
  const found: [number, string][] = [];
62
- await world.run(System.create({ observer: true, components: { with: [cell, position] } }, row => { found.push([row[position], `${row[cell][0]}:${row[position]}`]); }));
62
+ await world.run(System.create({ readonly: true, components: { with: [cell, position] } }, row => { found.push([row[position], `${row[cell][0]}:${row[position]}`]); }));
63
63
  return found.sort((a, b) => a[0] - b[0]).map(([, text]) => text);
64
64
  };
65
65
 
@@ -132,7 +132,7 @@ describe("reclaiming archetypes of shared values", () => {
132
132
  found.length = 0;
133
133
  await world.run(System.create({ kernel: false, components: { with: [cell, position] } }, row => { found.push(`body ${row[cell][0]}`); }));
134
134
  await world.run(copyCell);
135
- await world.run(System.create({ observer: true, components: { with: [cell, seen] } }, row => { found.push(`kernel ${row[seen]}`); }));
135
+ await world.run(System.create({ readonly: true, components: { with: [cell, seen] } }, row => { found.push(`kernel ${row[seen]}`); }));
136
136
  await world.run(atFifty);
137
137
  await world.run(atLast);
138
138
  expect(found.sort()).toEqual(["body 50", `body ${emptyFor + 2}`, "kernel 50", `kernel ${emptyFor + 2}`, "value 0", "where 100"].sort());
@@ -132,7 +132,7 @@ const observerTargetOf = (state: State, leaf: Leaf): ObserverTarget | undefined
132
132
 
133
133
  /** A runner for a leaf where it runs now: on the joined thread filling its thread, or that added it, or here. */
134
134
  const placedRunner = (state: State, leaf: Leaf, since = 0): Runner => {
135
- if (leaf.declaration.observer === true) {
135
+ if (leaf.declaration.readonly === true) {
136
136
  const target = observerTargetOf(state, leaf);
137
137
  return target === undefined ? createRunner(state, leaf, since) : createJoinedObserverRunner(state, leaf, target, since);
138
138
  }
@@ -394,7 +394,7 @@ export const createRegistrar = (state: State): Registrar => {
394
394
 
395
395
  const place = (): void => {
396
396
  eachRunner((leaf, old, entry) => {
397
- if (leaf.declaration.observer === true) {
397
+ if (leaf.declaration.readonly === true) {
398
398
  if (observerTargetOf(state, leaf) === old.joined?.target) return;
399
399
  replace(leaf, entry, old, placedRunner(state, leaf, old.lastRun()));
400
400
  return;
@@ -72,7 +72,7 @@ export type Observers = "factory" | "observer";
72
72
  const observer: { entity: Entity | null; seen: ((odd: boolean, held: boolean, text: string | null | undefined) => void) | null } = { entity: null, seen: null };
73
73
 
74
74
  /** After each sync point, on the joined thread: the watched entity's phase and caption, and whether it's flagged. */
75
- export const observerWatch = System.create({ observer: true, lookup: { values: { with: [phase, caption] }, flagged: { with: [flag] } } }, ({ lookup }) => {
75
+ export const observerWatch = System.create({ readonly: true, lookup: { values: { with: [phase, caption] }, flagged: { with: [flag] } } }, ({ lookup }) => {
76
76
  if (observer.entity === null) return;
77
77
  const row = lookup.values(observer.entity);
78
78
  if (row === null) return;
@@ -0,0 +1,61 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import { expect, it } from "vitest";
3
+ import { Finalizer, System } from "../../index.js";
4
+ import { Observe } from "../../observe/index.js";
5
+ import { createWorld } from "../index.js";
6
+ import { sameValue } from "./resource-changes.js";
7
+
8
+ const size = Symbol("resourceChanges/size");
9
+ const corner = Symbol("resourceChanges/corner");
10
+ const unwatched = Symbol("resourceChanges/unwatched");
11
+ const log = Symbol("resourceChanges/log");
12
+ const label = Symbol("resourceChanges/label");
13
+
14
+ declare module "@adobe/ecs" {
15
+ interface Components {
16
+ [label]: { type: string };
17
+ }
18
+ interface Resources {
19
+ [size]: { type: number; default: 1 };
20
+ [corner]: { type: readonly [number, number] };
21
+ [unwatched]: { type: number };
22
+ [log]: { type: readonly string[]; default: [] };
23
+ }
24
+ }
25
+
26
+ // Watching only resources, it sees no component's changes.
27
+ Finalizer.changes({ watch: [size, corner] }, (changes, { resources }) => {
28
+ const lines = changes.resources.map(change => change.key === size ? `size ${change.previous} → ${change.next}` : `corner ${change.previous.join(",")} → ${change.next.join(",")}`);
29
+ resources[log] = [...resources[log], ...changes.groups.map(() => "group"), ...lines];
30
+ });
31
+
32
+ const resize = System.create({}, ({ resources }, input: { readonly to: number }) => {
33
+ resources[size] = input.to;
34
+ resources[size] = input.to + 1;
35
+ });
36
+ const move = System.create({}, ({ resources }, input: { readonly x: number }) => { resources[corner] = [input.x, 0]; });
37
+ const touch = System.create({}, ({ resources }) => { resources[unwatched] += 1; });
38
+ const spawn = System.create({}, ({ commands }) => { commands.spawn([label])({ [label]: "a" }); });
39
+ const read = System.create({ readonly: true }, ({ resources }) => resources[log]);
40
+
41
+ it("reports each watched resource written since its finalizers last ran, with its first and last value, once", async () => {
42
+ await using world = createWorld();
43
+ // A run writing nothing else still has a sync point, so its finalizers see the write.
44
+ await world.run(resize, { to: 4 });
45
+ expect(await world.run(read)).toEqual(["size 1 → 5"]);
46
+ await world.run(spawn);
47
+ await world.run(touch);
48
+ expect(await world.run(read)).toEqual(["size 1 → 5"]);
49
+ await world.run(move, { x: 2 });
50
+ // Written back to an equal value, leaf by leaf: not reported.
51
+ await world.run(move, { x: 2 });
52
+ await world.run(resize, { to: 4 });
53
+ expect(await world.run(read)).toEqual(["size 1 → 5", "corner 0,0 → 2,0"]);
54
+ expect(await Observe.toPromise(Observe.resource(world, size))).toBe(5);
55
+ });
56
+
57
+ it("compares data leaf by leaf and other objects by identity", () => {
58
+ const map = new Map();
59
+ expect([sameValue([1, { a: -0 }], [1, { a: -0 }]), sameValue({ a: 1 }, { a: 1, b: 2 }), sameValue(0, -0), sameValue(NaN, NaN), sameValue(map, new Map()), sameValue(map, map)])
60
+ .toEqual([true, false, false, true, false, true]);
61
+ });
@@ -0,0 +1,67 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import { readCell } from './cells.js';
3
+ import { indexOf } from './entities.js';
4
+ import type { Storage } from './archetype.js';
5
+ import type { State } from './state.js';
6
+
7
+ /** A watched resource written to a value that isn't equal to the one its finalizers last saw. */
8
+ export type ResourceChange = { readonly id: number; readonly previous: unknown; readonly next: unknown };
9
+
10
+ const noChanges: readonly ResourceChange[] = [];
11
+
12
+ /** Where resource `id` lives now, or null before the world has it. */
13
+ const cellOf = (state: State, id: number): { readonly storage: Storage; readonly row: number } | null => {
14
+ if (state.resourceEntity < 0) return null;
15
+ const index = indexOf(state.resourceEntity);
16
+ const storage = state.archetypes[state.entities.archetype[index]]?.storageById[id];
17
+ return storage === undefined ? null : { storage, row: state.entities.row[index] };
18
+ };
19
+
20
+ /** Starts following resource `id` for a changes finalizer watching it, from its value now. */
21
+ export const watchResource = (state: State, id: number): void => {
22
+ if (state.resourcesSeen.has(id)) return;
23
+ const cell = cellOf(state, id);
24
+ state.resourcesSeen.set(id, cell === null ? { value: state.starts[id], tick: 0 } : { value: readCell(cell.storage, cell.row), tick: cell.storage.changed[cell.row] });
25
+ };
26
+
27
+ /** Whether a watched resource was written since its finalizers last ran, so a sync point is due for them to see it. */
28
+ export const resourcesWritten = (state: State): boolean => {
29
+ for (const [id, seen] of state.resourcesSeen) {
30
+ const cell = cellOf(state, id);
31
+ if (cell !== null && cell.storage.changed[cell.row] > seen.tick) return true;
32
+ }
33
+ return false;
34
+ };
35
+
36
+ /**
37
+ * Each watched resource written since its finalizers last ran, to a value that isn't equal to the one they saw then,
38
+ * with that value and its value now, which they see from now on.
39
+ */
40
+ export const takeResourceChanges = (state: State): readonly ResourceChange[] => {
41
+ let changes: ResourceChange[] | null = null;
42
+ for (const [id, seen] of state.resourcesSeen) {
43
+ const cell = cellOf(state, id);
44
+ if (cell === null) continue;
45
+ const tick = cell.storage.changed[cell.row];
46
+ if (tick <= seen.tick) continue;
47
+ const next = readCell(cell.storage, cell.row);
48
+ if (!sameValue(seen.value, next)) (changes ??= []).push({ id, previous: seen.value, next });
49
+ state.resourcesSeen.set(id, { value: next, tick });
50
+ }
51
+ return changes ?? noChanges;
52
+ };
53
+
54
+ const isPlain = (value: object): boolean => {
55
+ const prototype: unknown = Object.getPrototypeOf(value);
56
+ return prototype === Object.prototype || prototype === null;
57
+ };
58
+
59
+ /** Whether two values are equal: data leaf by leaf, as `Object.is` compares leaves, and any other object by identity. */
60
+ export const sameValue = (a: unknown, b: unknown): boolean => {
61
+ if (Object.is(a, b)) return true;
62
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
63
+ if (Array.isArray(a)) return Array.isArray(b) && a.length === b.length && a.every((item, i) => sameValue(item, b[i]));
64
+ if (Array.isArray(b) || !isPlain(a) || !isPlain(b)) return false;
65
+ const keys = Object.keys(a);
66
+ return keys.length === Object.keys(b).length && keys.every(key => Object.hasOwn(b, key) && sameValue(Reflect.get(a, key), Reflect.get(b, key)));
67
+ };