@adobe/ecs 0.2.0 → 0.2.3

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 (262) hide show
  1. package/README.md +2 -2
  2. package/dist/implementation/compiler/describe.d.ts +1 -1
  3. package/dist/implementation/compiler/describe.js +3 -3
  4. package/dist/implementation/compiler/describe.js.map +1 -1
  5. package/dist/implementation/compiler/signature.js +1 -1
  6. package/dist/implementation/compiler/transform.js +3 -3
  7. package/dist/implementation/compiler/transform.js.map +1 -1
  8. package/dist/implementation/world/commands.js +50 -1
  9. package/dist/implementation/world/commands.js.map +1 -1
  10. package/dist/implementation/world/create.js.map +1 -1
  11. package/dist/implementation/world/entities.d.ts +2 -0
  12. package/dist/implementation/world/entities.js +6 -0
  13. package/dist/implementation/world/entities.js.map +1 -1
  14. package/dist/implementation/world/finalizers.js.map +1 -1
  15. package/dist/implementation/world/join.js +10 -2
  16. package/dist/implementation/world/join.js.map +1 -1
  17. package/dist/implementation/world/register.js +2 -2
  18. package/dist/implementation/world/rows.d.ts +1 -1
  19. package/dist/implementation/world/rows.js +5 -0
  20. package/dist/implementation/world/rows.js.map +1 -1
  21. package/dist/implementation/world/runner.js +3 -3
  22. package/dist/implementation/world/runner.js.map +1 -1
  23. package/dist/implementation/world/sharing.js +1 -1
  24. package/dist/implementation/world/staging.d.ts +4 -0
  25. package/dist/implementation/world/staging.js +15 -3
  26. package/dist/implementation/world/staging.js.map +1 -1
  27. package/dist/implementation/world/tables-read.d.ts +7 -0
  28. package/dist/implementation/world/tables-read.js +60 -0
  29. package/dist/implementation/world/tables-read.js.map +1 -0
  30. package/dist/implementation/world/tables.d.ts +9 -0
  31. package/dist/implementation/world/tables.js +355 -0
  32. package/dist/implementation/world/tables.js.map +1 -0
  33. package/dist/implementation/world/thread-observer.js +1 -1
  34. package/dist/implementation/world/thread-observer.js.map +1 -1
  35. package/dist/implementation/world/thread-systems.js +1 -0
  36. package/dist/implementation/world/thread-systems.js.map +1 -1
  37. package/dist/index.d.ts +8 -8
  38. package/dist/index.js +4 -9
  39. package/dist/index.js.map +1 -1
  40. package/dist/interface/component/component.d.ts +7 -1
  41. package/dist/interface/component/component.js +0 -1
  42. package/dist/interface/component/component.js.map +1 -1
  43. package/dist/interface/component/declarations.d.ts +4 -0
  44. package/dist/interface/component/declarations.js +8 -0
  45. package/dist/interface/component/declarations.js.map +1 -0
  46. package/dist/interface/component/declared.d.ts +12 -0
  47. package/dist/interface/component/declared.js +2 -0
  48. package/dist/interface/component/declared.js.map +1 -0
  49. package/dist/interface/component/index.d.ts +3 -1
  50. package/dist/interface/component/index.js +1 -1
  51. package/dist/interface/component/index.js.map +1 -1
  52. package/dist/interface/component/value.d.ts +5 -5
  53. package/dist/interface/component/values.d.ts +2 -2
  54. package/dist/interface/finalizer/body.d.ts +5 -5
  55. package/dist/interface/registry/names.d.ts +2 -0
  56. package/dist/interface/registry/names.js +9 -0
  57. package/dist/interface/registry/names.js.map +1 -1
  58. package/dist/interface/registry/schedule.d.ts +3 -3
  59. package/dist/interface/registry/schedule.js +3 -3
  60. package/dist/interface/registry/schedule.js.map +1 -1
  61. package/dist/interface/resource/condition.d.ts +3 -3
  62. package/dist/interface/resource/declarations.d.ts +4 -0
  63. package/dist/interface/resource/declarations.js +8 -0
  64. package/dist/interface/resource/declarations.js.map +1 -0
  65. package/dist/interface/resource/index.d.ts +2 -1
  66. package/dist/interface/resource/index.js +1 -1
  67. package/dist/interface/resource/index.js.map +1 -1
  68. package/dist/interface/resource/resource.d.ts +4 -1
  69. package/dist/interface/resource/resource.js +0 -1
  70. package/dist/interface/resource/resource.js.map +1 -1
  71. package/dist/interface/resource/value.d.ts +5 -5
  72. package/dist/interface/resources/cadence.d.ts +1 -1
  73. package/dist/interface/system/access.d.ts +1 -1
  74. package/dist/interface/system/body.d.ts +13 -8
  75. package/dist/interface/system/check.d.ts +1 -1
  76. package/dist/interface/system/check.js +25 -9
  77. package/dist/interface/system/check.js.map +1 -1
  78. package/dist/interface/system/commands.d.ts +20 -4
  79. package/dist/interface/system/create.d.ts +21 -9
  80. package/dist/interface/system/create.js +3 -3
  81. package/dist/interface/system/create.js.map +1 -1
  82. package/dist/interface/system/declaration.d.ts +9 -2
  83. package/dist/interface/system/struct.d.ts +2 -2
  84. package/dist/interface/system/tables.d.ts +20 -0
  85. package/dist/interface/system/tables.js +2 -0
  86. package/dist/interface/system/tables.js.map +1 -0
  87. package/dist/interface/system/values-of.d.ts +4 -4
  88. package/dist/interface/system/values.d.ts +5 -5
  89. package/dist/interface/trait/types.d.ts +4 -4
  90. package/dist/interface/world/create.d.ts +3 -7
  91. package/dist/interface/world/declarations.d.ts +2 -2
  92. package/dist/interface/world/index.d.ts +1 -1
  93. package/dist/interface/world/join.d.ts +2 -10
  94. package/dist/interface/world/table/index.d.ts +26 -0
  95. package/dist/interface/world/table/index.js +3 -0
  96. package/dist/interface/world/table/index.js.map +1 -0
  97. package/dist/interface/world/table/table.d.ts +11 -0
  98. package/dist/interface/world/table/table.js +2 -0
  99. package/dist/interface/world/table/table.js.map +1 -0
  100. package/dist/interface/world/world.d.ts +10 -10
  101. package/dist/world.d.ts +27 -0
  102. package/dist/world.js +27 -0
  103. package/dist/world.js.map +1 -0
  104. package/package.json +1 -1
  105. package/src/exports.test.ts +3 -3
  106. package/src/guide/00-keys.ts +1 -1
  107. package/src/guide/01-resources.md +1 -1
  108. package/src/guide/01-resources.ts +1 -1
  109. package/src/guide/02-entities.ts +2 -2
  110. package/src/guide/03-systems.md +2 -2
  111. package/src/guide/03-systems.out +1 -1
  112. package/src/guide/03-systems.ts +3 -3
  113. package/src/guide/04-ticks.md +1 -1
  114. package/src/guide/04-ticks.ts +3 -3
  115. package/src/guide/05-scheduling.ts +1 -1
  116. package/src/guide/06-sync-points.md +9 -2
  117. package/src/guide/06-sync-points.ts +2 -2
  118. package/src/guide/07-change-filters.ts +2 -2
  119. package/src/guide/08-input.ts +1 -1
  120. package/src/guide/09-rules.ts +1 -1
  121. package/src/guide/11-traits.ts +3 -3
  122. package/src/guide/11-traits.type-test.ts +1 -1
  123. package/src/guide/12-lookups.ts +2 -2
  124. package/src/guide/13-indexes.ts +4 -4
  125. package/src/guide/14-kernels.ts +1 -1
  126. package/src/guide/15-reductions.ts +1 -1
  127. package/src/guide/16-gpu.ts +1 -1
  128. package/src/guide/17-hierarchies.ts +1 -1
  129. package/src/guide/18-identity.out +1 -1
  130. package/src/guide/19-threads.md +4 -2
  131. package/src/guide/19-threads.ts +2 -2
  132. package/src/guide/20-shared.ts +3 -3
  133. package/src/guide/21-dom.md +2 -2
  134. package/src/guide/21-dom.ts +1 -1
  135. package/src/guide/22-canvas.md +1 -1
  136. package/src/guide/22-canvas.ts +1 -1
  137. package/src/guide/CLAUDE.md +1 -1
  138. package/src/guide/guide.test.ts +7 -5
  139. package/src/implementation/compiler/describe.test-helper.ts +1 -1
  140. package/src/implementation/compiler/describe.test.ts +1 -1
  141. package/src/implementation/compiler/describe.ts +3 -3
  142. package/src/implementation/compiler/hygiene.test.ts +1 -1
  143. package/src/implementation/compiler/plugin.test.ts +2 -2
  144. package/src/implementation/compiler/signature.ts +1 -1
  145. package/src/implementation/compiler/transform.test.ts +3 -3
  146. package/src/implementation/compiler/transform.ts +3 -3
  147. package/src/implementation/world/change-blocks.test.ts +1 -1
  148. package/src/implementation/world/commands.ts +44 -1
  149. package/src/implementation/world/create.ts +3 -3
  150. package/src/implementation/world/defaults.test.ts +1 -1
  151. package/src/implementation/world/entities.ts +6 -0
  152. package/src/implementation/world/finalizers.test.ts +1 -1
  153. package/src/implementation/world/finalizers.ts +3 -3
  154. package/src/implementation/world/index-changed.test.ts +2 -2
  155. package/src/implementation/world/index-joined.test.ts +5 -5
  156. package/src/implementation/world/index-order.test.ts +1 -1
  157. package/src/implementation/world/join.test.ts +4 -4
  158. package/src/implementation/world/join.ts +10 -2
  159. package/src/implementation/world/joined-observer.test.ts +1 -1
  160. package/src/implementation/world/joining-worker.fixture.ts +1 -1
  161. package/src/implementation/world/joining.fixture.ts +1 -1
  162. package/src/implementation/world/joining.test-helper.ts +1 -1
  163. package/src/implementation/world/kernel-spawns.test.ts +1 -1
  164. package/src/implementation/world/observe-factories.test.ts +2 -2
  165. package/src/implementation/world/observe-ticks-cases.test.ts +1 -1
  166. package/src/implementation/world/observe.test-helper.ts +1 -1
  167. package/src/implementation/world/observer-contained-worker.fixture.ts +1 -1
  168. package/src/implementation/world/observer-contained.test-helper.ts +1 -1
  169. package/src/implementation/world/observer-contained.test.ts +2 -2
  170. package/src/implementation/world/observer-joined.test-helper.ts +4 -4
  171. package/src/implementation/world/observer-joined.test.ts +23 -15
  172. package/src/implementation/world/observer.browser.test.ts +2 -2
  173. package/src/implementation/world/observer.test.ts +8 -8
  174. package/src/implementation/world/optional-workers.fixture.ts +1 -1
  175. package/src/implementation/world/optional.test.ts +7 -7
  176. package/src/implementation/world/pool-size.cases.ts +3 -1
  177. package/src/implementation/world/reads.test-helper.ts +6 -6
  178. package/src/implementation/world/reclaim.test.ts +3 -3
  179. package/src/implementation/world/register.ts +2 -2
  180. package/src/implementation/world/remote-objects.test-helper.ts +1 -1
  181. package/src/implementation/world/rows.ts +5 -1
  182. package/src/implementation/world/run-results.test.ts +10 -10
  183. package/src/implementation/world/runner.ts +3 -3
  184. package/src/implementation/world/shared-gpu.browser.test.ts +2 -2
  185. package/src/implementation/world/shared-kernels.test.ts +2 -2
  186. package/src/implementation/world/shared-workers.fixture.ts +2 -2
  187. package/src/implementation/world/shared.test.ts +8 -8
  188. package/src/implementation/world/sharing.ts +1 -1
  189. package/src/implementation/world/staging.ts +16 -3
  190. package/src/implementation/world/tables-read.ts +59 -0
  191. package/src/implementation/world/tables.test.ts +267 -0
  192. package/src/implementation/world/tables.ts +330 -0
  193. package/src/implementation/world/tails.test.ts +1 -1
  194. package/src/implementation/world/target.test.ts +2 -2
  195. package/src/implementation/world/thread-observer.ts +1 -1
  196. package/src/implementation/world/thread-systems.test-helper.ts +3 -3
  197. package/src/implementation/world/thread-systems.ts +1 -0
  198. package/src/implementation/world/where.test.ts +4 -4
  199. package/src/implementation/world/workers.fixture.ts +2 -2
  200. package/src/index.ts +4 -6
  201. package/src/interface/component/component.ts +7 -1
  202. package/src/interface/component/declarations.ts +11 -0
  203. package/src/interface/component/declared.ts +9 -0
  204. package/src/interface/component/index.ts +3 -1
  205. package/src/interface/component/value.ts +5 -5
  206. package/src/interface/component/values.ts +2 -2
  207. package/src/interface/finalizer/body.ts +5 -5
  208. package/src/interface/index.type-test.ts +3 -3
  209. package/src/interface/indexes/index.type-test.ts +1 -1
  210. package/src/interface/registry/names.ts +8 -0
  211. package/src/interface/registry/schedule.ts +4 -4
  212. package/src/interface/resource/condition.ts +3 -3
  213. package/src/interface/resource/declarations.ts +11 -0
  214. package/src/interface/resource/index.ts +2 -1
  215. package/src/interface/resource/resource.ts +4 -1
  216. package/src/interface/resource/value.ts +5 -5
  217. package/src/interface/resources/cadence.ts +1 -1
  218. package/src/interface/system/access.ts +1 -1
  219. package/src/interface/system/body.ts +9 -7
  220. package/src/interface/system/check.ts +22 -8
  221. package/src/interface/system/commands.ts +18 -4
  222. package/src/interface/system/create.test.ts +13 -13
  223. package/src/interface/system/create.ts +15 -10
  224. package/src/interface/system/declaration.ts +9 -2
  225. package/src/interface/system/lists.type-test.ts +1 -1
  226. package/src/interface/system/optional.type-test.ts +1 -1
  227. package/src/interface/system/shared.type-test.ts +1 -1
  228. package/src/interface/system/struct.ts +2 -2
  229. package/src/interface/system/system.type-test.ts +26 -20
  230. package/src/interface/system/tables.ts +21 -0
  231. package/src/interface/system/up.type-test.ts +3 -3
  232. package/src/interface/system/values-of.ts +4 -4
  233. package/src/interface/system/values.ts +5 -5
  234. package/src/interface/thread/thread.type-test.ts +1 -1
  235. package/src/interface/trait/trait.type-test.ts +4 -4
  236. package/src/interface/trait/types.ts +4 -4
  237. package/src/interface/world/create.ts +3 -7
  238. package/src/interface/world/declarations.ts +2 -2
  239. package/src/interface/world/index.ts +2 -2
  240. package/src/interface/world/join.ts +2 -10
  241. package/src/interface/world/loop.type-test.ts +6 -6
  242. package/src/interface/world/run.type-test.ts +3 -3
  243. package/src/interface/world/table/index.ts +19 -0
  244. package/src/interface/world/table/table.ts +12 -0
  245. package/src/interface/world/world.ts +10 -10
  246. package/src/observe/entity.ts +2 -2
  247. package/src/observe/members.ts +3 -3
  248. package/src/observe/query.ts +1 -1
  249. package/src/observe/resource.ts +2 -2
  250. package/src/observe/select.ts +5 -5
  251. package/src/observe/values.ts +2 -2
  252. package/src/observe/world-state.ts +3 -3
  253. package/src/perftest/benchmarks.ts +1 -1
  254. package/src/perftest/indexes.ts +3 -3
  255. package/src/perftest/loop.ts +1 -1
  256. package/src/perftest/observer.ts +4 -4
  257. package/src/perftest/reads.ts +2 -2
  258. package/src/perftest/remote.ts +5 -5
  259. package/src/perftest/strings.ts +1 -1
  260. package/src/perftest/suite.ts +1 -1
  261. package/src/world.test.ts +2 -2
  262. package/src/world.ts +32 -0
@@ -57,16 +57,16 @@ const idle = System.create({}, () => {});
57
57
  const totalPlus = System.create({}, ({ resources }, input: { readonly plus: number }) => resources[total] + input.plus);
58
58
  const described = System.create(({ resources }) => ({ total: resources[total], names: resources[names] }));
59
59
  const unpostable = System.create(() => () => "a function");
60
- const readTotal = System.create({ observer: true }, ({ resources }) => resources[total]);
60
+ const readTotal = System.create({ readonly: true }, ({ resources }) => resources[total]);
61
61
  /** Writes levels, an object of joinTestAudio, so it runs on the thread claiming it. */
62
62
  const mix = System.create({}, ({ resources }, input: { readonly level: number }) => {
63
63
  resources[levels] = [...resources[levels], input.level];
64
64
  });
65
65
  /** An observer system recording each total it reads into `into`. */
66
- const totalsInto = (into: number[]) => System.create({ observer: true }, ({ resources }) => { into.push(resources[total]); });
66
+ const totalsInto = (into: number[]) => System.create({ readonly: true }, ({ resources }) => { into.push(resources[total]); });
67
67
  /** An observer system recording how many levels it reads into `into`. */
68
- const levelsInto = (into: number[]) => System.create({ observer: true }, ({ resources }) => { into.push(resources[levels].length); });
69
- const failingRead = System.create({ observer: true }, () => { throw new Error("joinTest read failure"); });
68
+ const levelsInto = (into: number[]) => System.create({ readonly: true }, ({ resources }) => { into.push(resources[levels].length); });
69
+ const failingRead = System.create({ readonly: true }, () => { throw new Error("joinTest read failure"); });
70
70
 
71
71
  /** A world shared with a thread joined through a channel, and the joined world, once both say it joined. */
72
72
  const joined = async (threads: readonly ("main")[] = []): Promise<{ readonly world: CreatedWorld; readonly remote: World; readonly channel: MessageChannel }> => {
@@ -33,6 +33,12 @@ const observerLeavesOf = (definition: Definition): readonly Leaf[] | null => {
33
33
  return schedule.tiers.length === 0 && schedule.observer.length > 0 ? schedule.observer : null;
34
34
  };
35
35
 
36
+ /** Throws for a readonly system reading tables, which only the world's thread gives. */
37
+ const refuseTables = (leaves: readonly Leaf[]): void => {
38
+ const reading = leaves.find(leaf => leaf.declaration.tables !== undefined);
39
+ if (reading !== undefined) throw new Error(`System "${reading.name}" reads tables, which only the world's thread gives: run or add it there.`);
40
+ };
41
+
36
42
  /** A run not yet settled: how to settle it, and its rejection once posted. */
37
43
  type Run = { readonly resolve: (value: unknown) => void; readonly reject: (error: unknown) => void; message: ToJoined | null; error: unknown; readonly local: Local | null };
38
44
 
@@ -659,6 +665,7 @@ export const joinWorld = (port: Parameters<World["share"]>[0], options: { readon
659
665
  // This thread's own observer systems run here, after the sync point of an empty run of the world's.
660
666
  const leaves = observerLeavesOf(definition);
661
667
  if (leaves !== null) {
668
+ refuseTables(leaves);
662
669
  if (failure !== null) return Promise.reject(failure);
663
670
  const locals = leaves.map(localOf);
664
671
  if (locals.length === 1 && definition.kind !== "composite") return runNumbered(locals[0].number, locals[0].leaf, undefined, locals[0]);
@@ -680,7 +687,8 @@ export const joinWorld = (port: Parameters<World["share"]>[0], options: { readon
680
687
  add: system => {
681
688
  const definition = requireDefinition(system);
682
689
  const leaves = observerLeavesOf(definition);
683
- if (leaves === null) throw new Error("A joined thread adds only observer systems; others are added to ticks on the world's thread.");
690
+ if (leaves === null) throw new Error(`System "${definition.name}" 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.`);
691
+ refuseTables(leaves);
684
692
  if (added.has(definition) || failure !== null) return;
685
693
  const locals = leaves.map(localOf);
686
694
  added.set(definition, locals);
@@ -692,7 +700,7 @@ export const joinWorld = (port: Parameters<World["share"]>[0], options: { readon
692
700
  },
693
701
  remove: system => {
694
702
  const definition = requireDefinition(system);
695
- if (observerLeavesOf(definition) === null) throw new Error("A joined thread removes only observer systems; others are removed from ticks on the world's thread.");
703
+ if (observerLeavesOf(definition) === null) throw new Error(`System "${definition.name}" isn't readonly, and a joined thread removes only readonly systems: others are removed on the world's thread.`);
696
704
  const locals = added.get(definition);
697
705
  if (locals === undefined) return;
698
706
  added.delete(definition);
@@ -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);
@@ -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();
@@ -12,7 +12,9 @@ export const poolSizeCases = (load: () => Promise<typeof Fixture>) => describe("
12
12
  });
13
13
  it("honors a number, and null again gives the platform's choice", async () => {
14
14
  const fixture = await load();
15
- expect((await fixture.resized(1, [null])).started).toEqual([1, 1 + fixture.platformCount()]);
15
+ // The platform's choice starts a pool of its own unless it's the size the pool already is.
16
+ const platform = fixture.platformCount();
17
+ expect((await fixture.resized(1, [null])).started).toEqual([1, platform === 1 ? 1 : 1 + platform]);
16
18
  });
17
19
  it("starts the pool only once a kernel has rows enough for workers", async () => {
18
20
  const fixture = await load();
@@ -1,17 +1,17 @@
1
1
  // © 2026 Adobe. MIT License. See /LICENSE for details.
2
2
  import type { Component } from '../../interface/component/component.js';
3
- import type { ReadonlyValue } from '../../interface/component/value.js';
3
+ import type { ReadonlyType } from '../../interface/component/value.js';
4
4
  import type { Entity } from '../../interface/entity/entity.js';
5
5
  import type { Resource } from '../../interface/resource/resource.js';
6
- import type { ReadonlyValue as ReadonlyResourceValue } from '../../interface/resource/value.js';
6
+ import type { ReadonlyType as ReadonlyResourceType } from '../../interface/resource/value.js';
7
7
  import type { World } from '../../interface/world/world.js';
8
8
  import { Observe } from '../../observe/index.js';
9
9
 
10
10
  // Tests' reads: each is the first value of an observer factory of `src/observe`.
11
11
 
12
12
  /** One component of an entity, or `null` if it's dead or lacks it. */
13
- export const read = async <C extends Component.Key>(world: World, entity: Entity, component: C): Promise<ReadonlyValue<C> | null> => {
14
- const values: { readonly [K in C]: ReadonlyValue<K> } | null = await Observe.toPromise(Observe.entity(world, entity, { required: [component] }));
13
+ export const read = async <C extends Component.Key>(world: World, entity: Entity, component: C): Promise<ReadonlyType<C> | null> => {
14
+ const values: { readonly [K in C]: ReadonlyType<K> } | null = await Observe.toPromise(Observe.entity(world, entity, { required: [component] }));
15
15
  return values === null ? null : values[component];
16
16
  };
17
17
 
@@ -19,12 +19,12 @@ export const read = async <C extends Component.Key>(world: World, entity: Entity
19
19
  export const readEntity = <const C extends readonly Component.Key[]>(world: World, entity: Entity, components: C) =>
20
20
  Observe.toPromise(Observe.entity(world, entity, { required: components }));
21
21
 
22
- export const readResource = <R extends Resource.Key>(world: World, name: R): Promise<ReadonlyResourceValue<R>> => Observe.toPromise(Observe.resource(world, name));
22
+ export const readResource = <R extends Resource.Key>(world: World, name: R): Promise<ReadonlyResourceType<R>> => Observe.toPromise(Observe.resource(world, name));
23
23
 
24
24
  export const select = <const R extends readonly Component.Key[] = []>(world: World, query: Observe.Query<R>): Promise<readonly Entity[]> => Observe.toPromise(Observe.select(world, query));
25
25
 
26
26
  export const count = <const R extends readonly Component.Key[] = []>(world: World, query: Omit<Observe.Query<R>, "order">): Promise<number> => Observe.toPromise(Observe.count(world, query));
27
27
 
28
28
  /** One component of each of these entities, `null` for one that's dead or lacks it. */
29
- export const readEach = <C extends Component.Key>(world: World, entities: readonly Entity[], component: C): Promise<(ReadonlyValue<C> | null)[]> =>
29
+ export const readEach = <C extends Component.Key>(world: World, entities: readonly Entity[], component: C): Promise<(ReadonlyType<C> | null)[]> =>
30
30
  Promise.all(entities.map(entity => read(world, entity, component)));
@@ -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;
@@ -12,6 +12,7 @@ import { ownTag } from './move-log.js';
12
12
  import { resourceCell } from './resources.js';
13
13
  import { labelOf } from '../../interface/registry/names.js';
14
14
  import { idOf, type Key, type State } from './state.js';
15
+ import { readTables } from './tables-read.js';
15
16
  import { combine, contributed, identityOf, type ReductionTarget } from './reductions.js';
16
17
  import { createIndexLookup, type Since } from './index-lookups.js';
17
18
  import type { WorldIndex } from './indexes.js';
@@ -506,8 +507,11 @@ const never: Since = { value: -1 };
506
507
  const own = (): object => Object.create({});
507
508
 
508
509
  /** Resource cells are resolved once per run: they move only at sync points. An unresolved resource throws on access. */
509
- export const createContext = (state: State, declaration: Pick<Resolved, "lookup" | "resources">, { targets = [], commands, indexes, since = never }: Extras = {}): Context => {
510
+ export const createContext = (state: State, declaration: Pick<Resolved, "lookup" | "resources" | "tables">, { targets = [], commands, indexes, since = never }: Extras = {}): Context => {
510
511
  const value: { up?: unknown } = own();
512
+ const { tables } = declaration;
513
+ // Each read copies the tables as they are then.
514
+ if (tables !== undefined) Object.defineProperty(value, "tables", { get: () => readTables(state, tables), enumerable: true });
511
515
  const { lookup, resources } = declaration;
512
516
  const names = Object.keys(lookup ?? {});
513
517
  if (names.length > 0) {