@adobe/ecs 0.2.1 → 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 (213) 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/entities.d.ts +2 -0
  11. package/dist/implementation/world/entities.js +6 -0
  12. package/dist/implementation/world/entities.js.map +1 -1
  13. package/dist/implementation/world/join.js +10 -2
  14. package/dist/implementation/world/join.js.map +1 -1
  15. package/dist/implementation/world/register.js +2 -2
  16. package/dist/implementation/world/rows.d.ts +1 -1
  17. package/dist/implementation/world/rows.js +5 -0
  18. package/dist/implementation/world/rows.js.map +1 -1
  19. package/dist/implementation/world/runner.js +3 -3
  20. package/dist/implementation/world/runner.js.map +1 -1
  21. package/dist/implementation/world/sharing.js +1 -1
  22. package/dist/implementation/world/staging.d.ts +4 -0
  23. package/dist/implementation/world/staging.js +15 -3
  24. package/dist/implementation/world/staging.js.map +1 -1
  25. package/dist/implementation/world/tables-read.d.ts +7 -0
  26. package/dist/implementation/world/tables-read.js +60 -0
  27. package/dist/implementation/world/tables-read.js.map +1 -0
  28. package/dist/implementation/world/tables.d.ts +9 -0
  29. package/dist/implementation/world/tables.js +355 -0
  30. package/dist/implementation/world/tables.js.map +1 -0
  31. package/dist/implementation/world/thread-observer.js +1 -1
  32. package/dist/implementation/world/thread-observer.js.map +1 -1
  33. package/dist/implementation/world/thread-systems.js +1 -0
  34. package/dist/implementation/world/thread-systems.js.map +1 -1
  35. package/dist/index.d.ts +8 -28
  36. package/dist/index.js +4 -9
  37. package/dist/index.js.map +1 -1
  38. package/dist/interface/component/declarations.d.ts +4 -0
  39. package/dist/interface/component/declarations.js +8 -0
  40. package/dist/interface/component/declarations.js.map +1 -0
  41. package/dist/interface/component/declared.d.ts +12 -0
  42. package/dist/interface/component/declared.js +2 -0
  43. package/dist/interface/component/declared.js.map +1 -0
  44. package/dist/interface/component/index.d.ts +2 -0
  45. package/dist/interface/component/index.js +1 -1
  46. package/dist/interface/component/index.js.map +1 -1
  47. package/dist/interface/registry/names.d.ts +2 -0
  48. package/dist/interface/registry/names.js +9 -0
  49. package/dist/interface/registry/names.js.map +1 -1
  50. package/dist/interface/registry/schedule.d.ts +3 -3
  51. package/dist/interface/registry/schedule.js +3 -3
  52. package/dist/interface/registry/schedule.js.map +1 -1
  53. package/dist/interface/resource/declarations.d.ts +4 -0
  54. package/dist/interface/resource/declarations.js +8 -0
  55. package/dist/interface/resource/declarations.js.map +1 -0
  56. package/dist/interface/resource/index.d.ts +2 -0
  57. package/dist/interface/resource/index.js +1 -1
  58. package/dist/interface/resource/index.js.map +1 -1
  59. package/dist/interface/system/body.d.ts +11 -6
  60. package/dist/interface/system/check.d.ts +1 -1
  61. package/dist/interface/system/check.js +25 -9
  62. package/dist/interface/system/check.js.map +1 -1
  63. package/dist/interface/system/commands.d.ts +16 -0
  64. package/dist/interface/system/create.d.ts +21 -9
  65. package/dist/interface/system/create.js +3 -3
  66. package/dist/interface/system/create.js.map +1 -1
  67. package/dist/interface/system/declaration.d.ts +9 -2
  68. package/dist/interface/system/tables.d.ts +20 -0
  69. package/dist/interface/system/tables.js +2 -0
  70. package/dist/interface/system/tables.js.map +1 -0
  71. package/dist/interface/world/index.d.ts +1 -1
  72. package/dist/interface/world/table/index.d.ts +26 -0
  73. package/dist/interface/world/table/index.js +3 -0
  74. package/dist/interface/world/table/index.js.map +1 -0
  75. package/dist/interface/world/table/table.d.ts +11 -0
  76. package/dist/interface/world/table/table.js +2 -0
  77. package/dist/interface/world/table/table.js.map +1 -0
  78. package/dist/interface/world/world.d.ts +10 -10
  79. package/dist/world.d.ts +27 -0
  80. package/dist/world.js +27 -0
  81. package/dist/world.js.map +1 -0
  82. package/package.json +1 -1
  83. package/src/exports.test.ts +1 -1
  84. package/src/guide/00-keys.ts +1 -1
  85. package/src/guide/01-resources.md +1 -1
  86. package/src/guide/01-resources.ts +1 -1
  87. package/src/guide/02-entities.ts +2 -2
  88. package/src/guide/03-systems.md +2 -2
  89. package/src/guide/03-systems.out +1 -1
  90. package/src/guide/03-systems.ts +3 -3
  91. package/src/guide/04-ticks.md +1 -1
  92. package/src/guide/04-ticks.ts +3 -3
  93. package/src/guide/05-scheduling.ts +1 -1
  94. package/src/guide/06-sync-points.md +9 -2
  95. package/src/guide/06-sync-points.ts +2 -2
  96. package/src/guide/07-change-filters.ts +2 -2
  97. package/src/guide/08-input.ts +1 -1
  98. package/src/guide/09-rules.ts +1 -1
  99. package/src/guide/11-traits.ts +3 -3
  100. package/src/guide/12-lookups.ts +2 -2
  101. package/src/guide/13-indexes.ts +4 -4
  102. package/src/guide/14-kernels.ts +1 -1
  103. package/src/guide/15-reductions.ts +1 -1
  104. package/src/guide/16-gpu.ts +1 -1
  105. package/src/guide/17-hierarchies.ts +1 -1
  106. package/src/guide/18-identity.out +1 -1
  107. package/src/guide/19-threads.md +4 -2
  108. package/src/guide/19-threads.ts +2 -2
  109. package/src/guide/20-shared.ts +3 -3
  110. package/src/guide/21-dom.md +2 -2
  111. package/src/guide/21-dom.ts +1 -1
  112. package/src/guide/22-canvas.md +1 -1
  113. package/src/guide/22-canvas.ts +1 -1
  114. package/src/guide/CLAUDE.md +1 -1
  115. package/src/guide/guide.test.ts +7 -5
  116. package/src/implementation/compiler/describe.test-helper.ts +1 -1
  117. package/src/implementation/compiler/describe.test.ts +1 -1
  118. package/src/implementation/compiler/describe.ts +3 -3
  119. package/src/implementation/compiler/hygiene.test.ts +1 -1
  120. package/src/implementation/compiler/plugin.test.ts +2 -2
  121. package/src/implementation/compiler/signature.ts +1 -1
  122. package/src/implementation/compiler/transform.test.ts +3 -3
  123. package/src/implementation/compiler/transform.ts +3 -3
  124. package/src/implementation/world/change-blocks.test.ts +1 -1
  125. package/src/implementation/world/commands.ts +44 -1
  126. package/src/implementation/world/defaults.test.ts +1 -1
  127. package/src/implementation/world/entities.ts +6 -0
  128. package/src/implementation/world/finalizers.test.ts +1 -1
  129. package/src/implementation/world/index-changed.test.ts +2 -2
  130. package/src/implementation/world/index-joined.test.ts +5 -5
  131. package/src/implementation/world/index-order.test.ts +1 -1
  132. package/src/implementation/world/join.test.ts +4 -4
  133. package/src/implementation/world/join.ts +10 -2
  134. package/src/implementation/world/joined-observer.test.ts +1 -1
  135. package/src/implementation/world/joining-worker.fixture.ts +1 -1
  136. package/src/implementation/world/joining.fixture.ts +1 -1
  137. package/src/implementation/world/joining.test-helper.ts +1 -1
  138. package/src/implementation/world/kernel-spawns.test.ts +1 -1
  139. package/src/implementation/world/observe-factories.test.ts +2 -2
  140. package/src/implementation/world/observe-ticks-cases.test.ts +1 -1
  141. package/src/implementation/world/observe.test-helper.ts +1 -1
  142. package/src/implementation/world/observer-contained-worker.fixture.ts +1 -1
  143. package/src/implementation/world/observer-contained.test-helper.ts +1 -1
  144. package/src/implementation/world/observer-contained.test.ts +2 -2
  145. package/src/implementation/world/observer-joined.test-helper.ts +4 -4
  146. package/src/implementation/world/observer-joined.test.ts +23 -15
  147. package/src/implementation/world/observer.browser.test.ts +2 -2
  148. package/src/implementation/world/observer.test.ts +8 -8
  149. package/src/implementation/world/optional-workers.fixture.ts +1 -1
  150. package/src/implementation/world/optional.test.ts +7 -7
  151. package/src/implementation/world/reclaim.test.ts +3 -3
  152. package/src/implementation/world/register.ts +2 -2
  153. package/src/implementation/world/remote-objects.test-helper.ts +1 -1
  154. package/src/implementation/world/rows.ts +5 -1
  155. package/src/implementation/world/run-results.test.ts +10 -10
  156. package/src/implementation/world/runner.ts +3 -3
  157. package/src/implementation/world/shared-gpu.browser.test.ts +2 -2
  158. package/src/implementation/world/shared-kernels.test.ts +2 -2
  159. package/src/implementation/world/shared-workers.fixture.ts +2 -2
  160. package/src/implementation/world/shared.test.ts +8 -8
  161. package/src/implementation/world/sharing.ts +1 -1
  162. package/src/implementation/world/staging.ts +16 -3
  163. package/src/implementation/world/tables-read.ts +59 -0
  164. package/src/implementation/world/tables.test.ts +267 -0
  165. package/src/implementation/world/tables.ts +330 -0
  166. package/src/implementation/world/tails.test.ts +1 -1
  167. package/src/implementation/world/target.test.ts +2 -2
  168. package/src/implementation/world/thread-observer.ts +1 -1
  169. package/src/implementation/world/thread-systems.test-helper.ts +3 -3
  170. package/src/implementation/world/thread-systems.ts +1 -0
  171. package/src/implementation/world/where.test.ts +4 -4
  172. package/src/implementation/world/workers.fixture.ts +2 -2
  173. package/src/index.ts +4 -29
  174. package/src/interface/component/declarations.ts +11 -0
  175. package/src/interface/component/declared.ts +9 -0
  176. package/src/interface/component/index.ts +2 -0
  177. package/src/interface/index.type-test.ts +2 -2
  178. package/src/interface/indexes/index.type-test.ts +1 -1
  179. package/src/interface/registry/names.ts +8 -0
  180. package/src/interface/registry/schedule.ts +4 -4
  181. package/src/interface/resource/declarations.ts +11 -0
  182. package/src/interface/resource/index.ts +2 -0
  183. package/src/interface/system/body.ts +7 -5
  184. package/src/interface/system/check.ts +22 -8
  185. package/src/interface/system/commands.ts +14 -0
  186. package/src/interface/system/create.test.ts +13 -13
  187. package/src/interface/system/create.ts +15 -10
  188. package/src/interface/system/declaration.ts +9 -2
  189. package/src/interface/system/lists.type-test.ts +1 -1
  190. package/src/interface/system/optional.type-test.ts +1 -1
  191. package/src/interface/system/shared.type-test.ts +1 -1
  192. package/src/interface/system/system.type-test.ts +26 -20
  193. package/src/interface/system/tables.ts +21 -0
  194. package/src/interface/system/up.type-test.ts +3 -3
  195. package/src/interface/world/index.ts +2 -2
  196. package/src/interface/world/run.type-test.ts +3 -3
  197. package/src/interface/world/table/index.ts +19 -0
  198. package/src/interface/world/table/table.ts +12 -0
  199. package/src/interface/world/world.ts +10 -10
  200. package/src/observe/entity.ts +2 -2
  201. package/src/observe/resource.ts +2 -2
  202. package/src/observe/select.ts +4 -4
  203. package/src/observe/world-state.ts +3 -3
  204. package/src/perftest/benchmarks.ts +1 -1
  205. package/src/perftest/indexes.ts +3 -3
  206. package/src/perftest/loop.ts +1 -1
  207. package/src/perftest/observer.ts +4 -4
  208. package/src/perftest/reads.ts +2 -2
  209. package/src/perftest/remote.ts +5 -5
  210. package/src/perftest/strings.ts +1 -1
  211. package/src/perftest/suite.ts +1 -1
  212. package/src/world.test.ts +2 -2
  213. package/src/world.ts +32 -0
@@ -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();
@@ -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) {
@@ -30,11 +30,11 @@ const spawn = System.create(({ commands }) => {
30
30
  const raise = System.create(({ resources }) => {
31
31
  resources[level] += 1;
32
32
  });
33
- const count = System.create({ observer: true, components: { with: [amount] } }, () => {
33
+ const count = System.create({ readonly: true, components: { with: [amount] } }, () => {
34
34
  counted++;
35
35
  });
36
36
  let counted = 0;
37
- const readLevel = System.create({ observer: true }, ({ resources }) => resources[level]);
37
+ const readLevel = System.create({ readonly: true }, ({ resources }) => resources[level]);
38
38
  const broken = System.create(() => {
39
39
  throw new Error("broken");
40
40
  });
@@ -57,7 +57,7 @@ describe("a run", () => {
57
57
  expect(await world.run(System.create({ components: { with: [amount] } }, row => row[amount]))).toBeUndefined();
58
58
  expect(await world.run(System.create([readLevel, raise]))).toBeUndefined();
59
59
  expect(await world.run(raise)).toBeUndefined();
60
- const whenChanged = System.create({ observer: true, resources: { changed: [level] } }, ({ resources }) => resources[level]);
60
+ const whenChanged = System.create({ readonly: true, resources: { changed: [level] } }, ({ resources }) => resources[level]);
61
61
  expect(await world.run(whenChanged)).toBe(2);
62
62
  // Unchanged since it last ran: skipped.
63
63
  expect(await world.run(whenChanged)).toBeUndefined();
@@ -94,7 +94,7 @@ describe("a run", () => {
94
94
  it("rejects when its own observer system fails, and reports one added out of band", async () => {
95
95
  using reported = catchOutOfBand();
96
96
  await using world = inline();
97
- const failingObserver = System.create({ observer: true }, () => {
97
+ const failingObserver = System.create({ readonly: true }, () => {
98
98
  throw new Error("observer failed");
99
99
  });
100
100
  await expect(world.run(failingObserver)).rejects.toThrow(/observer failed/);
@@ -144,7 +144,7 @@ const spawnTwins = System.create(({ commands }) => {
144
144
  commands.spawn([serial])({ [serial]: 1 });
145
145
  commands.spawn([serial])({ [serial]: 1 });
146
146
  });
147
- const findTwin = System.create({ observer: true, lookup: { twins: { index: bySerial } } }, ({ lookup }) => lookup.twins(1)?.entity ?? null);
147
+ const findTwin = System.create({ readonly: true, lookup: { twins: { index: bySerial } } }, ({ lookup }) => lookup.twins(1)?.entity ?? null);
148
148
  const setGate = System.create({}, ({ resources }, input: { readonly open: boolean }) => {
149
149
  resources[gate] = input.open;
150
150
  });
@@ -207,10 +207,10 @@ describe("a run's observer pass", () => {
207
207
  it("fails the run for its own observer system, and reports one added out of band, in one run", async () => {
208
208
  using reported = catchOutOfBand();
209
209
  await using world = inline();
210
- const ownFails = System.create({ observer: true }, () => {
210
+ const ownFails = System.create({ readonly: true }, () => {
211
211
  throw new Error("own failed");
212
212
  });
213
- const addedFails = System.create({ observer: true }, () => {
213
+ const addedFails = System.create({ readonly: true }, () => {
214
214
  throw new Error("added failed");
215
215
  });
216
216
  world.add(addedFails);
@@ -255,7 +255,7 @@ describe("out-of-band reports", () => {
255
255
  it("aren't made again for a system skipped between its failures", async () => {
256
256
  using reported = catchOutOfBand();
257
257
  await using world = inline();
258
- const failsWhenChanged = System.create({ observer: true, resources: { changed: [level] } }, () => {
258
+ const failsWhenChanged = System.create({ readonly: true, resources: { changed: [level] } }, () => {
259
259
  throw new Error("changed failed");
260
260
  });
261
261
  world.add(failsWhenChanged);
@@ -294,7 +294,7 @@ describe("out-of-band reports", () => {
294
294
  using reported = catchOutOfBand();
295
295
  const world = inline();
296
296
  let failing = false;
297
- const failsAdded = System.create({ observer: true }, () => {
297
+ const failsAdded = System.create({ readonly: true }, () => {
298
298
  if (failing) throw new Error("added failed");
299
299
  });
300
300
  world.add(failsAdded);
@@ -331,7 +331,7 @@ describe("out-of-band reports", () => {
331
331
  it("aren't made again for a row system whose change filter passed no row between its failures", async () => {
332
332
  using reported = catchOutOfBand();
333
333
  await using world = inline();
334
- const failsOnChange = System.create({ observer: true, components: { with: [amount], changed: [amount] } }, () => {
334
+ const failsOnChange = System.create({ readonly: true, components: { with: [amount], changed: [amount] } }, () => {
335
335
  throw new Error("row failed");
336
336
  });
337
337
  const bump = System.create({ components: { with: [amount] } }, row => {
@@ -260,7 +260,7 @@ export const createRunner = (state: State, leaf: Leaf, since = 0): Runner => {
260
260
  : createOrderedRender(indexLookups.ordered, ordered, accessors.map(accessor => idOf(state, accessor.component)), declaration.shader?.group ?? 0, `${leafNumber}:`);
261
261
  const touches = [...new Set([...query?.read ?? [], ...query?.write ?? []])].map(key => idOf(state, key));
262
262
  const writes = (query?.write ?? []).map(key => idOf(state, key));
263
- const looks = [...new Set([...Object.values(declaration.lookup ?? {}).flatMap(lookup => lookup.read ?? []), ...declaration.up?.read ?? []])].map(key => idOf(state, key));
263
+ const looks = [...new Set([...Object.values(declaration.lookup ?? {}).flatMap(lookup => lookup.read ?? []), ...declaration.up?.read ?? [], ...declaration.tables?.components ?? []])].map(key => idOf(state, key));
264
264
  /** Record keys by archetype. */
265
265
  const keys = new Map<Archetype, string>();
266
266
  const keyOf = (archetype: Archetype): string => {
@@ -339,7 +339,7 @@ export const createRunner = (state: State, leaf: Leaf, since = 0): Runner => {
339
339
  let runInput: unknown;
340
340
  const context = createContext(state, declaration, {
341
341
  targets: kernel === null ? targets : [],
342
- commands: leaf.kind === "render" || declaration.kernel === "gpu" || declaration.observer === true ? undefined : commands,
342
+ commands: leaf.kind === "render" || declaration.kernel === "gpu" || declaration.readonly === true ? undefined : commands,
343
343
  indexes: indexLookups.byName,
344
344
  since: lookupSince,
345
345
  });
@@ -1343,7 +1343,7 @@ export const createRunner = (state: State, leaf: Leaf, since = 0): Runner => {
1343
1343
  return taken;
1344
1344
  },
1345
1345
  didRun: () => ran && (query === undefined || bodyRanOnRow()),
1346
- restart: declaration.observer !== true || !holdsNothing(holdings) ? null : since => {
1346
+ restart: declaration.readonly !== true || !holdsNothing(holdings) ? null : since => {
1347
1347
  lastRun = since;
1348
1348
  holdings.removed = false;
1349
1349
  holdings.released = false;
@@ -59,7 +59,7 @@ const fractions = System.create({ kernel: "gpu", components: { with: [scale, val
59
59
  });
60
60
  const fractionsOf = async (world: ReturnType<typeof createWorld>): Promise<readonly (readonly number[])[]> => {
61
61
  const read: [number, number[]][] = [];
62
- await world.run(System.create({ observer: true, components: { with: [value, copied, product, sum, compound] } }, row => { read.push([row[value], [row[copied], row[product], row[sum], row[compound], row[value]]]); }));
62
+ await world.run(System.create({ readonly: true, components: { with: [value, copied, product, sum, compound] } }, row => { read.push([row[value], [row[copied], row[product], row[sum], row[compound], row[value]]]); }));
63
63
  return read.sort((a, b) => a[0] - b[0]).map(([, values]) => values);
64
64
  };
65
65
 
@@ -71,7 +71,7 @@ const device = async (): Promise<GPUDevice> => {
71
71
 
72
72
  const results = async (world: ReturnType<typeof createWorld>): Promise<readonly string[]> => {
73
73
  const read: [number, string][] = [];
74
- await world.run(System.create({ observer: true, components: { with: [value, out, steps] } }, row => { read.push([row[value], `${row[out]} ${row[steps]}`]); }));
74
+ await world.run(System.create({ readonly: true, components: { with: [value, out, steps] } }, row => { read.push([row[value], `${row[out]} ${row[steps]}`]); }));
75
75
  return read.sort((a, b) => a[0] - b[0]).map(([, text]) => text);
76
76
  };
77
77
 
@@ -62,7 +62,7 @@ const wideOnGpu = System.create({ kernel: "gpu", components: { with: [wide, out]
62
62
  /** Every row's results, by its value. */
63
63
  const results = async (world: ReturnType<typeof createWorld>): Promise<readonly string[]> => {
64
64
  const read: [number, string][] = [];
65
- await world.run(System.create({ observer: true, components: { with: [value, out, steps] } }, row => { read.push([row[value], `${row[out]} ${row[steps]}`]); }));
65
+ await world.run(System.create({ readonly: true, components: { with: [value, out, steps] } }, row => { read.push([row[value], `${row[out]} ${row[steps]}`]); }));
66
66
  return read.sort((a, b) => a[0] - b[0]).map(([, text]) => text);
67
67
  };
68
68
 
@@ -98,7 +98,7 @@ describe("a kernel reading a shared component", () => {
98
98
  await world.run(spawn);
99
99
  expect((await runReported(world, moving)).failures).toEqual([]);
100
100
  const scales: number[] = [];
101
- await world.run(System.create({ observer: true, components: { with: [scale] } }, row => { scales.push(row[scale]); }));
101
+ await world.run(System.create({ readonly: true, components: { with: [scale] } }, row => { scales.push(row[scale]); }));
102
102
  expect(new Set(scales)).toEqual(new Set([2]));
103
103
  // A kernel built without knowing a component is shared, naming it in a command, fails where it registers.
104
104
  const description = kernelOf(compute);
@@ -39,7 +39,7 @@ const written = System.create({ kernel: false, components: { with: [scale, cell,
39
39
  row[steps] = x * 1000 + y;
40
40
  });
41
41
  const read: [number, string][] = [];
42
- const results = System.create({ observer: true, components: { with: [value, out, steps] } }, row => { read.push([row[value], `${row[out]} ${row[steps]}`]); });
42
+ const results = System.create({ readonly: true, components: { with: [value, out, steps] } }, row => { read.push([row[value], `${row[out]} ${row[steps]}`]); });
43
43
 
44
44
  const outcome = async (system: typeof compute, wasm: boolean): Promise<readonly string[]> => {
45
45
  await using world = createWorld({ [workers]: 2 }, { minimumChunk: 2, wasm });
@@ -66,7 +66,7 @@ export const whereOnWorkers = async (wasm: boolean): Promise<{ readonly written:
66
66
  await world.run(negative);
67
67
  let written = 0;
68
68
  let left = 0;
69
- await world.run(System.create({ observer: true, components: { with: [value, out] } }, row => {
69
+ await world.run(System.create({ readonly: true, components: { with: [value, out] } }, row => {
70
70
  if (row[out] === 0 && row[value] !== 0) left++;
71
71
  else written++;
72
72
  }));
@@ -39,7 +39,7 @@ const inline = () => createWorld({ [workers]: 0 });
39
39
 
40
40
  // Systems a joined thread runs, which it names by the ids the build gives them at the top level.
41
41
  const heard: string[] = [];
42
- export const listen = System.create({ observer: true, components: { with: [team, health] } }, row => { heard.push(`${row[team]} ${row[health]}`); });
42
+ export const listen = System.create({ readonly: true, components: { with: [team, health] } }, row => { heard.push(`${row[team]} ${row[health]}`); });
43
43
  export const spawnBanners = System.create(({ commands }) => {
44
44
  commands.spawn([team, health, banner], { [team]: "red" })({ [health]: 1 });
45
45
  commands.spawn([team, health, banner], { [team]: "blue" })({ [health]: 2 });
@@ -60,11 +60,11 @@ export const spawnThere = System.create({ components: { with: [banner] } }, (row
60
60
  commands.spawn([health, marker], base)({ [marker]: 1 });
61
61
  commands.spawn([health], base)({});
62
62
  });
63
- export const readBanners = System.create({ observer: true, components: { with: [banner] } }, row => { banners.push(row[banner]?.text ?? "none"); });
63
+ export const readBanners = System.create({ readonly: true, components: { with: [banner] } }, row => { banners.push(row[banner]?.text ?? "none"); });
64
64
 
65
65
  /** Every entity's team and health, and whether it's armed, sorted by health. */
66
66
  const rowsOf = async (world: ReturnType<typeof inline>): Promise<readonly string[]> => {
67
- const read = System.create({ observer: true, components: { with: [team, health], optional: [armed] } }, (row) => {
67
+ const read = System.create({ readonly: true, components: { with: [team, health], optional: [armed] } }, (row) => {
68
68
  found.push(`${row[team]} ${row[health]}${row[armed] === true ? " armed" : ""}`);
69
69
  });
70
70
  const found: string[] = [];
@@ -116,7 +116,7 @@ describe("a shared component", () => {
116
116
  // Three cells' worth of [1, 2] and [2, 1], three weights and one style.
117
117
  expect((await world.tables()).archetypes - before).toBe(2 + 3 + 1);
118
118
  const read: string[] = [];
119
- await world.run(System.create({ observer: true, components: { with: [health], optional: [cell, weight, style] } }, row => {
119
+ await world.run(System.create({ readonly: true, components: { with: [health], optional: [cell, weight, style] } }, row => {
120
120
  if (row[health] === 0) return;
121
121
  read.push(`${row[health]}: ${JSON.stringify(row[cell] ?? row[style] ?? null)} ${String(row[weight])}`);
122
122
  }));
@@ -216,8 +216,8 @@ describe("a shared component", () => {
216
216
  commands.spawn([team, health], { [team]: "red" })({ [health]: 2 });
217
217
  }));
218
218
  const changes: string[] = [];
219
- const watch = System.create({ observer: true, components: { with: [health], changed: [team] } }, row => { changes.push(`changed ${row[health]}`); });
220
- const adds = System.create({ observer: true, components: { with: [health], added: [team] } }, row => { changes.push(`added ${row[health]}`); });
219
+ const watch = System.create({ readonly: true, components: { with: [health], changed: [team] } }, row => { changes.push(`changed ${row[health]}`); });
220
+ const adds = System.create({ readonly: true, components: { with: [health], added: [team] } }, row => { changes.push(`added ${row[health]}`); });
221
221
  world.add(watch);
222
222
  world.add(adds);
223
223
  await world.run(System.create(() => {}));
@@ -294,7 +294,7 @@ describe("a shared component", () => {
294
294
  }));
295
295
  await world.run(System.create({ components: { with: [weight, health] } }, row => { row[health] *= row[weight]; }));
296
296
  const read: number[] = [];
297
- await world.run(System.create({ observer: true, components: { with: [health] } }, row => { read.push(row[health]); }));
297
+ await world.run(System.create({ readonly: true, components: { with: [health] } }, row => { read.push(row[health]); }));
298
298
  expect(read.sort()).toEqual([2, 3]);
299
299
  });
300
300
 
@@ -338,7 +338,7 @@ describe("a shared component", () => {
338
338
  await world.run(writeBanners);
339
339
  await world.run(spawnThere);
340
340
  const found: string[] = [];
341
- await world.run(System.create({ observer: true, components: { with: [health], optional: [marker, team] } }, row => {
341
+ await world.run(System.create({ readonly: true, components: { with: [health], optional: [marker, team] } }, row => {
342
342
  if (row[team] === undefined) found.push(`${row[health]} ${row[marker] ?? "none"}`);
343
343
  }));
344
344
  expect(found.sort()).toEqual(["7 1", "7 1", "7 none", "7 none"]);
@@ -613,7 +613,7 @@ export const createSharing = (host: SharingHost) => {
613
613
  const { components, resources } = declared;
614
614
  // Unnamed, so other systems' labels stay as they are; its own is the thread's.
615
615
  const declaration: Resolved = {
616
- observer: true,
616
+ readonly: true,
617
617
  // Its `where` runs on its thread; here, an empty one says it has one, which every archetype passes.
618
618
  ...components === null ? {} : { components: { ...listed([["read", components.read], ["has", components.has], ["optional", components.optional], ["not", components.not], ["added", components.added], ["changed", components.changed]]), ...components.where ? { where: {} } : {} } },
619
619
  ...resources === null ? {} : { resources: listed([["read", resources.read], ["changed", resources.changed], ["is", resources.is], ["not", resources.not]]) },
@@ -69,14 +69,15 @@ export const createBatch = (layouts: readonly Layout[], ids: readonly number[],
69
69
  return { ids: sorted, shared, archetype: null, columns: sorted.map(id => columnOf(layouts[id], id, initialCapacity)), entities: new Float64Array(initialCapacity), count: 0, capacity: initialCapacity, revive: null, generation: 0 };
70
70
  };
71
71
 
72
- const grow = (batch: SpawnBatch): void => {
72
+ /** Grows a batch to hold at least `needed` rows: twice its capacity, or `needed` where that's more. */
73
+ const grow = (batch: SpawnBatch, needed = batch.count + 1): void => {
73
74
  const { revive } = batch;
74
75
  if (revive !== null) {
75
76
  batch.revive = null;
76
77
  revive();
77
- if (batch.count < batch.capacity) return;
78
+ if (needed <= batch.capacity) return;
78
79
  }
79
- const capacity = batch.capacity * 2;
80
+ const capacity = Math.max(batch.capacity * 2, needed);
80
81
  for (const column of batch.columns) {
81
82
  const views = viewsOf(new ArrayBuffer(capacity * column.stride), 0, capacity * column.stride);
82
83
  views.u8.set(column.views.u8.subarray(0, batch.count * column.stride));
@@ -146,6 +147,18 @@ export const openRow = (batch: SpawnBatch): number => {
146
147
  return batch.count;
147
148
  };
148
149
 
150
+ /** The first of the `n` rows the batch's next spawns write, growing it first to hold them. */
151
+ export const openRows = (batch: SpawnBatch, n: number): number => {
152
+ if (batch.count + n > batch.capacity) grow(batch, batch.count + n);
153
+ return batch.count;
154
+ };
155
+
156
+ /** Claims the open rows for `entities`, once all their values are written. */
157
+ export const claimRows = (batch: SpawnBatch, entities: Float64Array): void => {
158
+ batch.entities.set(entities, batch.count);
159
+ batch.count += entities.length;
160
+ };
161
+
149
162
  /** Claims the open row for `entity`, once all its values are written. */
150
163
  export const claim = (batch: SpawnBatch, entity: number): void => {
151
164
  batch.entities[batch.count] = entity;
@@ -0,0 +1,59 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import { labelOf } from '../../interface/registry/names.js';
3
+ import type { Tables, TablesDeclaration } from '../../interface/system/tables.js';
4
+ import type { Table } from '../../interface/world/table/table.js';
5
+ import type { Storage } from './archetype.js';
6
+ import { readCell } from './cells.js';
7
+ import { resourceCell } from './resources.js';
8
+ import { idOf, type Key, type State } from './state.js';
9
+
10
+ // Reading tables, apart from spawning them: rows' contexts import it, and what spawning imports would make a cycle.
11
+
12
+ /** What an archetype's storage of a component gives a table: a copy of its rows `0` to `count`. */
13
+ const columnOf = (key: Key, storage: Storage, count: number): Table.Column => {
14
+ const { layout } = storage;
15
+ switch (layout.kind) {
16
+ case "linear": return { kind: "bytes", bytes: storage.views.u8.slice(0, count * layout.stride), stride: layout.stride };
17
+ case "constant": return { kind: "value", value: layout.value };
18
+ case "object":
19
+ if (storage.remote !== null) throw new Error(`Tables can't read "${labelOf(key)}": a joined thread fills its objects.`);
20
+ return { kind: "values", values: storage.values.slice(0, count) };
21
+ case "string": {
22
+ const values: unknown[] = new Array(count);
23
+ for (let row = 0; row < count; row++) values[row] = readCell(storage, row);
24
+ return { kind: "values", values };
25
+ }
26
+ }
27
+ };
28
+
29
+ /**
30
+ * A readonly system's `tables`: of each archetype matching `with` and `not` and holding any of `components`, its
31
+ * entities and a copy of the column of each of them it holds; and the value of each resource of `resources`.
32
+ */
33
+ export const readTables = (state: State, declaration: TablesDeclaration): Tables => {
34
+ const asked = declaration.components.map(key => ({ key, id: idOf(state, key) }));
35
+ const required = (declaration.with ?? []).map(key => idOf(state, key));
36
+ const excluded = (declaration.not ?? []).map(key => idOf(state, key));
37
+ const archetypes: Table[] = [];
38
+ for (const archetype of state.archetypes) {
39
+ const { count } = archetype;
40
+ if (count === 0 || archetype.reclaimed) continue;
41
+ const { storageById } = archetype;
42
+ if (required.some(id => storageById[id] === undefined) || excluded.some(id => storageById[id] !== undefined)) continue;
43
+ const columns: { [key: symbol]: Table.Column } = {};
44
+ let held = false;
45
+ for (const { key, id } of asked) {
46
+ const storage = storageById[id];
47
+ if (storage === undefined) continue;
48
+ columns[key] = columnOf(key, storage, count);
49
+ held = true;
50
+ }
51
+ if (held) archetypes.push({ count, entities: archetype.entities.slice(0, count), columns });
52
+ }
53
+ const resources: { [key: symbol]: unknown } = {};
54
+ for (const key of declaration.resources ?? []) {
55
+ const cell = resourceCell(state, key);
56
+ resources[key] = readCell(cell.storage, cell.row);
57
+ }
58
+ return { archetypes, resources };
59
+ };