@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
@@ -279,6 +279,7 @@ export const createThreadSystems = (host: ThreadSystemsHost) => {
279
279
  return typeof preset === "object" && preset !== null ? presetSpawnerOf(components, preset) : spawnerOf(components);
280
280
  },
281
281
  despawn: entity => beginCommand(writer, despawnCommand, entity),
282
+ spawnTables: () => { throw new Error("commands.spawnTables runs on the world's thread: run the system there."); },
282
283
  add: (entity: number, components: symbol | readonly symbol[], values?: unknown) => {
283
284
  if (typeof components === "symbol") {
284
285
  const key = givenKeyOf(components);
@@ -62,10 +62,10 @@ export const noteInView = System.create({ components: { with: [position, note],
62
62
  row[note] = { text: `${row[position]}` };
63
63
  });
64
64
  const heard: number[] = [];
65
- export const hearInView = System.create({ observer: true, components: { with: [position], changed: [position], where: { [cell]: (at, { resources }) => visible(at, resources[camera].x, resources[camera].radius) } } }, row => { heard.push(row[position]); });
65
+ export const hearInView = System.create({ readonly: true, components: { with: [position], changed: [position], where: { [cell]: (at, { resources }) => visible(at, resources[camera].x, resources[camera].radius) } } }, row => { heard.push(row[position]); });
66
66
  // Filtered by a resource the predicate doesn't read: a culled archetype coming into view catches up on its change.
67
67
  const signalled: number[] = [];
68
- export const signalInView = System.create({ observer: true, components: { with: [position], where: { [cell]: (at, { resources }) => visible(at, resources[camera].x, resources[camera].radius) } }, resources: { changed: [other] } }, row => { signalled.push(row[position]); });
68
+ export const signalInView = System.create({ readonly: true, components: { with: [position], where: { [cell]: (at, { resources }) => visible(at, resources[camera].x, resources[camera].radius) } }, resources: { changed: [other] } }, row => { signalled.push(row[position]); });
69
69
  // A system moving the camera its own predicate reads: its next run sees the new view.
70
70
  const followed: number[] = [];
71
71
  export const follow = System.create({ components: { with: [position], where: { [cell]: (at, { resources }) => visible(at, resources[camera].x, resources[camera].radius) } } }, (row, { resources }) => {
@@ -79,7 +79,7 @@ export const noteChanged = System.create({ components: { with: [position, note],
79
79
  row[note] = { text: `${row[position]}` };
80
80
  });
81
81
  const notes: string[] = [];
82
- export const readNotes = System.create({ observer: true, components: { with: [note] } }, row => { if (row[note] !== null) notes.push(row[note].text); });
82
+ export const readNotes = System.create({ readonly: true, components: { with: [note] } }, row => { if (row[note] !== null) notes.push(row[note].text); });
83
83
  export const nudgeNoted = System.create({ components: { with: [position, note] } }, row => { row[position] += 0.5; });
84
84
 
85
85
  /** Waits a turn at a time until `done`, for two seconds at most. */
@@ -91,7 +91,7 @@ const until = async (done: () => boolean): Promise<void> => {
91
91
  /** Every row's position and how often it was seen, by position. */
92
92
  const seenOf = async (world: ReturnType<typeof createWorld>): Promise<readonly string[]> => {
93
93
  const read: [number, string][] = [];
94
- await world.run(System.create({ observer: true, components: { with: [position, seen] } }, row => { read.push([row[position], `${row[position]}:${row[seen]}`]); }));
94
+ await world.run(System.create({ readonly: true, components: { with: [position, seen] } }, row => { read.push([row[position], `${row[position]}:${row[seen]}`]); }));
95
95
  return read.sort((a, b) => a[0] - b[0]).map(([, text]) => text).filter(text => !text.endsWith(":0"));
96
96
  };
97
97
 
@@ -149,8 +149,8 @@ const emitted = { seen: 0 };
149
149
  const countEmitted = System.create({ components: { added: [position], with: [spawned] } }, () => { emitted.seen++; });
150
150
 
151
151
  const gathered: Entity[] = [];
152
- const gatherPositioned = System.create({ observer: true, components: { with: [position] } }, row => { gathered.push(row.entity); });
153
- const gatherSpawned = System.create({ observer: true, components: { with: [spawned] } }, row => { gathered.push(row.entity); });
152
+ const gatherPositioned = System.create({ readonly: true, components: { with: [position] } }, row => { gathered.push(row.entity); });
153
+ const gatherSpawned = System.create({ readonly: true, components: { with: [spawned] } }, row => { gathered.push(row.entity); });
154
154
 
155
155
  /** The entities an observer row system passes, in storage order. */
156
156
  const storageOrder = async (world: World, gather: System<void>): Promise<Entity[]> => {
package/src/index.ts CHANGED
@@ -3,9 +3,6 @@
3
3
  * A high-performance, multithreaded Entity Component System.
4
4
  * @module @adobe/ecs
5
5
  */
6
- import { create, join } from './implementation/index.js';
7
- import type { Create } from './interface/world/create.js';
8
- import type { Join } from './interface/world/join.js';
9
6
  import type { World as Created } from './interface/world/world.js';
10
7
 
11
8
  export * from './interface/index.js';
@@ -15,30 +12,8 @@ export type World = Created;
15
12
 
16
13
  /**
17
14
  * How a world is made, `World.create(resources?)`, on the thread that will run it, with resources starting at the given
18
- * values in place of their defaults; and how another thread joins one that shared itself with it,
19
- * `World.join(port, { threads })`.
15
+ * values in place of their defaults; how another thread joins one that shared itself with it,
16
+ * `World.join(port, { threads })`; and `World.Table`, an archetype's rows as a readonly system's `tables` copies them
17
+ * and `commands.spawnTables` spawns them.
20
18
  */
21
- export const World: {
22
- /**
23
- * Creates a world on this thread, synchronously, with resources starting at the given values in place of their
24
- * defaults. This thread is its `"world"` thread for the world's life: it runs the sync points, the ticks and the GPU
25
- * device, and fills every logical thread no joined thread claims. Keys are the resources' symbols, built-in ones
26
- * included (`{ [workers]: 0 }`); string keys are never a resource's, so they're ignored. Throws for a symbol that isn't
27
- * a resource, or is one only the world writes. While a declaration's trait or default doesn't hold, each call fails to
28
- * compile, once, naming each such key, its members and what's wrong with them.
29
- */
30
- readonly create: Create;
31
- /**
32
- * Joins, from another thread, a world that `share` let join through the other end of `port`, claiming the logical
33
- * `threads` this thread fills: their objects live here, and the systems and finalizers reading them run here. Resolves,
34
- * once the world accepted the claim, to the same world: its `run` runs systems there, and its `add` and `remove` take
35
- * observer systems, which run here after the world's sync points. Add them before awaiting anything else, so the
36
- * world's `share` resolves with them in place. Its `dispose` disconnects this thread. Rejects with an `Error` saying
37
- * why it didn't join: the world refused the claim, was disposed, or its thread ended first, as the platform reports it:
38
- * the world's `Worker` failing to start or throwing first, a port closed, as in node, or, in a browser, the world's
39
- * thread ending once it was shared through a port this thread listened on. A browser world's worker that the page ended
40
- * before this thread heard from it isn't reported, so the join stays pending. Rejects too where memory can't be shared,
41
- * as without cross-origin isolation.
42
- */
43
- readonly join: Join;
44
- } = { create, join };
19
+ export * as World from './world.js';
@@ -0,0 +1,11 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import type { Declared } from './declared.js';
3
+ import { declaredOf } from '../registry/names.js';
4
+ import type { Key } from './key.js';
5
+
6
+ /** Every component its modules registered so far, with its declaration. */
7
+ export const declarations = (): ReadonlyMap<Key, Declared> => {
8
+ // Registration takes only keys declared in `Components`, so each symbol registered as a component is one of its keys.
9
+ const found = declaredOf("component") as ReadonlyMap<Key, Declared>;
10
+ return found;
11
+ };
@@ -0,0 +1,9 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import type { Schema } from '../schema/schema.js';
3
+ import type { Key as TraitKey } from '../trait/key.js';
4
+
5
+ /**
6
+ * A component's or resource's declaration as registered: its schema as `type` where its value is data, what it
7
+ * starts at as `default`, and its other traits, the built-in ones by name and those of your own by their keys.
8
+ */
9
+ export type Declared = { readonly type?: Schema; readonly default?: unknown } & { readonly [T in TraitKey]?: unknown };
@@ -1,3 +1,5 @@
1
1
  // © 2026 Adobe. MIT License. See /LICENSE for details.
2
2
  export type { Key } from './key.js';
3
3
  export type { With } from './value.js';
4
+ export { declarations } from './declarations.js';
5
+ export type { Declared } from './declared.js';
@@ -27,8 +27,8 @@ Rule.create({ components: { with: [position] }, derive: [slot] });
27
27
  declare const create: Create;
28
28
  declare const wgsl: typeof Gpu.wgsl;
29
29
  export const world = create({ [workers]: 4, [gpuDevice]: null });
30
- export const read = System.create({ observer: true, components: { with: [position, slot] } }, () => {});
31
- export const readOne = System.create({ observer: true, lookup: { at: { with: [position, slot] } } }, () => {});
30
+ export const read = System.create({ readonly: true, components: { with: [position, slot] } }, () => {});
31
+ export const readOne = System.create({ readonly: true, lookup: { at: { with: [position, slot] } } }, () => {});
32
32
  export const report = world.run(all);
33
33
  export const shader = wgsl(draw);
34
34
  export const added = world.add(read);
@@ -219,7 +219,7 @@ System.create({
219
219
 
220
220
  // An index lookup's `changed: true` runs its system only when the index's entries changed since it last ran: its
221
221
  // lists' `changed` then reports entries alone. Observer systems and CPU kernels take it; it's not a list of components.
222
- System.create({ observer: true, lookup: { living: { index: living, changed: true } } }, ({ lookup }) => {
222
+ System.create({ readonly: true, lookup: { living: { index: living, changed: true } } }, ({ lookup }) => {
223
223
  const rows = lookup.living();
224
224
  check<Equal<typeof rows.changed, boolean>>();
225
225
  check<Equal<typeof rows.length, number>>();
@@ -61,6 +61,14 @@ const entryOf = (key: symbol): Entry | undefined => {
61
61
  return entries.get(key);
62
62
  };
63
63
 
64
+ /** Every key registered as `kind`, each by the symbol a world gives its values by, with its declaration. */
65
+ export const declaredOf = (kind: "component" | "resource"): ReadonlyMap<symbol, AnyDeclaration> => {
66
+ catchUp();
67
+ const found = new Map<symbol, AnyDeclaration>();
68
+ for (const entry of entries.values()) if (entry.kind === kind && entry.declaration !== undefined) found.set(entry.newest, entry.declaration);
69
+ return found;
70
+ };
71
+
64
72
  /** Whether a key is a component, a resource or a trait. */
65
73
  export const kindOf = (key: unknown): Kind | undefined => typeof key === "symbol" ? entryOf(key)?.kind : undefined;
66
74
 
@@ -28,15 +28,15 @@ const checkInputs = (leaves: readonly Leaf[]): void => {
28
28
 
29
29
  /**
30
30
  * Leaves in run order, grouped into tiers. Leaves in one tier have no ordering path between them. `reductions` are the
31
- * resources its leaves reduce into, each with how many of them contribute. `observer` are its observer leaves, in no
31
+ * resources its leaves reduce into, each with how many of them contribute. `observer` are its readonly leaves, in no
32
32
  * tier: they run after its sync point, ordered with nothing.
33
33
  */
34
34
  export type Schedule = { readonly tiers: readonly (readonly Leaf[])[]; readonly reductions: ReadonlyMap<Key, number>; readonly observer: readonly Leaf[] };
35
35
 
36
36
  export const isLeaf = (system: Definition): system is Leaf => system.kind !== "composite";
37
37
 
38
- /** Whether a system is an observer leaf, which runs after a sync point rather than in a tier. */
39
- export const isObserver = (system: Definition): system is Leaf & { readonly declaration: { readonly observer: true } } => system.kind !== "composite" && system.declaration.observer === true;
38
+ /** Whether a system is a readonly system leaf, which runs after a sync point rather than in a tier. */
39
+ export const isObserver = (system: Definition): system is Leaf & { readonly declaration: { readonly readonly: true } } => system.kind !== "composite" && system.declaration.readonly === true;
40
40
 
41
41
  /** Each system is an interval, from its start node to its end node. */
42
42
  const start = (unit: number): number => 2 * unit;
@@ -65,7 +65,7 @@ const graphOf = (root: Definition): Graph => {
65
65
  };
66
66
  visit(root);
67
67
  const successors = units.flatMap(() => [new Set<number>(), new Set<number>()]);
68
- // An observer system is ordered with nothing, so constraints naming one are ignored.
68
+ // A readonly system is ordered with nothing, so constraints naming one are ignored.
69
69
  const resolve = (systems: readonly object[] | undefined): readonly number[] =>
70
70
  (systems ?? []).flatMap(system => {
71
71
  const unit = indexOf.get(firstItemOf(system));
@@ -0,0 +1,11 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import type { Declared } from '../component/declared.js';
3
+ import { declaredOf } from '../registry/names.js';
4
+ import type { Key } from './key.js';
5
+
6
+ /** Every resource its modules registered so far, with its declaration. */
7
+ export const declarations = (): ReadonlyMap<Key, Declared> => {
8
+ // Registration takes only keys declared in `Resources`, so each symbol registered as a resource is one of its keys.
9
+ const found = declaredOf("resource") as ReadonlyMap<Key, Declared>;
10
+ return found;
11
+ };
@@ -1,2 +1,4 @@
1
1
  // © 2026 Adobe. MIT License. See /LICENSE for details.
2
2
  export type { Key } from './key.js';
3
+ export { declarations } from './declarations.js';
4
+ export type { Declared } from '../component/declared.js';
@@ -10,14 +10,15 @@ import type { Commands } from './commands.js';
10
10
  import type { Declaration } from './declaration.js';
11
11
  import type { Listed, Property } from './property.js';
12
12
  import type { RenderContext } from './render.js';
13
+ import type { Tables } from './tables.js';
13
14
  import type { ComponentValues, LookupFunction, ResourceValues, Values } from './values.js';
14
15
 
15
16
  /** The components every entity a declaration matches has: those in `with`, `added` and `changed`. */
16
17
  export type Members<D> =
17
18
  | Listed<Property<D, "components">, "with"> | Listed<Property<D, "components">, "added"> | Listed<Property<D, "components">, "changed">;
18
19
 
19
- /** Whether a declaration only reads: an observer or a render system. */
20
- type Reading<D> = D extends { readonly observer: true } | { readonly shader: object } ? true : false;
20
+ /** Whether a declaration only reads: one declared `readonly`, or a render system. */
21
+ type Reading<D> = D extends { readonly readonly: true } | { readonly shader: object } ? true : false;
21
22
 
22
23
  /** The components a declaration's rows hold only where an entity has them. */
23
24
  export type Optional<D> = Exclude<Listed<Property<D, "components">, "optional">, Members<D>>;
@@ -51,8 +52,8 @@ type Is<D> = Listed<Property<D, "resources">, "is">;
51
52
 
52
53
  /**
53
54
  * What a body uses besides its row: every resource, its lookups by name, the resources it may reduce into, its
54
- * parent's values with `up`, and its commands, there for every system but a GPU kernel, a render system and an
55
- * observer system. The build infers which of them the body reads and writes.
55
+ * parent's values with `up`, and its commands, there for every system but a GPU kernel, a render system and one
56
+ * declared `readonly`. The build infers which of them the body reads and writes.
56
57
  */
57
58
  export type Context<D extends Declaration> =
58
59
  {
@@ -66,7 +67,8 @@ export type Context<D extends Declaration> =
66
67
  /** The parent's values the body reads, or null for a row without a parent or whose parent lacks one of them. */
67
68
  readonly up: ComponentValues | null;
68
69
  } : {})
69
- & (D extends { readonly kernel: "gpu" } | { readonly shader: object } | { readonly observer: true } ? {} : { readonly commands: Commands });
70
+ & (D extends { readonly kernel: "gpu" } | { readonly shader: object } | { readonly readonly: true } ? {} : { readonly commands: Commands })
71
+ & (D extends { readonly tables: object } ? { readonly tables: Tables } : {});
70
72
 
71
73
  /**
72
74
  * `(row, context, input)` for a system with `components`, `(context, input)` for one without, and `(gpu, context)` for
@@ -147,15 +147,15 @@ const checkWhere = (leaf: Leaf): void => {
147
147
  };
148
148
 
149
149
  /**
150
- * Throws for what an observer system can't declare: it only reads, after a sync point, so it writes and reduces into
150
+ * Throws for what a readonly system can't declare: it only reads, after a sync point, so it writes and reduces into
151
151
  * nothing, takes no input, isn't a kernel or a render system, has no `up`, and is ordered with nothing.
152
152
  */
153
- const checkObserver = (leaf: Leaf): void => {
153
+ const checkReadonly = (leaf: Leaf): void => {
154
154
  const { name, declaration } = leaf;
155
- const { observer } = declaration;
156
- if (observer === undefined) return;
157
- const fail = (message: string): never => { throw new Error(`System "${name}" is an observer, so it ${message}.`); };
158
- if (observer !== true) throw new Error(`System "${name}": observer is true or left out.`);
155
+ const { readonly } = declaration;
156
+ if (readonly === undefined) return;
157
+ const fail = (message: string): never => { throw new Error(`System "${name}" is readonly, so it ${message}.`); };
158
+ if (readonly !== true) throw new Error(`System "${name}": readonly is true or left out.`);
159
159
  if (leaf.kind === "render") fail("records no GPU work");
160
160
  if (declaration.kernel === true || declaration.kernel === "gpu") fail("isn't a kernel");
161
161
  if ((declaration.components?.write?.length ?? 0) + (declaration.resources?.write?.length ?? 0) > 0) fail("writes nothing");
@@ -165,9 +165,22 @@ const checkObserver = (leaf: Leaf): void => {
165
165
  if ((declaration.before?.length ?? 0) + (declaration.after?.length ?? 0) + (declaration.during?.length ?? 0) > 0) fail("is ordered with nothing");
166
166
  };
167
167
 
168
+ /** Throws for `tables` on a system that isn't readonly or has components, or naming keys that aren't registered as their kind. */
169
+ const checkTables = (leaf: Leaf): void => {
170
+ const { name, declaration } = leaf;
171
+ const { tables } = declaration;
172
+ if (tables === undefined) return;
173
+ if (declaration.readonly !== true) throw new Error(`System "${name}" reads tables, which only a readonly system reads.`);
174
+ if (declaration.components !== undefined) throw new Error(`System "${name}" reads tables, so it has no components.`);
175
+ requireRegistered("System", name, "component", tables.components);
176
+ requireRegistered("System", name, "component", tables.with);
177
+ requireRegistered("System", name, "component", tables.not);
178
+ requireRegistered("System", name, "resource", tables.resources);
179
+ };
180
+
168
181
  /**
169
182
  * Throws for a leaf that names a component or resource that isn't registered as one, lists a key in both `read` and
170
- * `write`, writes a reference, has a lookup reading a component it writes, uses object components and resources of different threads, declares what an observer system can't, or whose
183
+ * `write`, writes a reference, has a lookup reading a component it writes, uses object components and resources of different threads, declares what a readonly system can't, or whose
171
184
  * reductions, conditions, `up` or placement can't hold.
172
185
  */
173
186
  export const checkLeaf = (leaf: Leaf): void => {
@@ -190,6 +203,7 @@ export const checkLeaf = (leaf: Leaf): void => {
190
203
  if (writes.has(key)) throw new Error(`System "${name}": its rows are ordered by an index keyed by "${labelOf(key)}", which the system writes.`);
191
204
  }
192
205
  checkTarget(leaf);
206
+ checkTables(leaf);
193
207
  threadOf(declaration, "System", name);
194
208
  requireRegistered("System", name, "component", components?.has);
195
209
  requireRegistered("System", name, "component", components?.not);
@@ -202,7 +216,7 @@ export const checkLeaf = (leaf: Leaf): void => {
202
216
  if (declarationOf(key)?.reference !== undefined) throw new Error(`System "${name}" writes "${labelOf(key)}", a reference, which changes only through commands.`);
203
217
  if (declarationOf(key)?.shared === true) throw new Error(`System "${name}" writes "${labelOf(key)}", a shared component, whose value is its archetype's: give it another with commands.add.`);
204
218
  }
205
- checkObserver(leaf);
219
+ checkReadonly(leaf);
206
220
  checkWhere(leaf);
207
221
  checkReductions(leaf);
208
222
  checkConditions(leaf);
@@ -2,6 +2,7 @@
2
2
  import type { Component } from '../component/component.js';
3
3
  import type { Entity } from '../entity/entity.js';
4
4
  import type { Essential, Shared } from '../component/value.js';
5
+ import type { Table } from '../world/table/table.js';
5
6
  import type { ValuesOf } from './values-of.js';
6
7
 
7
8
  /** The value an `add` of one component takes: none for a tag. */
@@ -68,4 +69,17 @@ export type Commands = {
68
69
  add<const C extends readonly Essential[]>(entity: Entity, components: C, values: ValuesOf<C>): void;
69
70
  remove(entity: Entity, component: Essential): void;
70
71
  remove(entity: Entity, components: readonly Essential[]): void;
72
+ /**
73
+ * Spawns an entity for each row of `tables`, as a readonly system's `tables` gives them, of each table's components
74
+ * with its columns' values: a block copy per column. Entity values are rewritten as the world's references would
75
+ * leave them had the entities outside the tables despawned: each naming an entity of the tables, a reference, a
76
+ * struct's field or an array's item, names the one spawned for it; a row whose owner reference names an entity
77
+ * outside them isn't spawned, nor anything it owns, through owner references, as despawning its owner would; a
78
+ * weak reference to one is left out of its row, which is spawned without it; any other value naming one is
79
+ * `Entity.none`. Returns, at once, each table's new entities, alive at the sync point. A column's stride must be its
80
+ * component's in this world, and its length its table's count. `resources` sets each resource it names, by key, at
81
+ * the sync point, as commands apply, each value checked against its resource's schema; not one only the world
82
+ * writes, nor one living on another thread. On the world's thread only.
83
+ */
84
+ spawnTables(tables: readonly Table[], resources?: { readonly [key: symbol]: unknown }): Table.Spawned;
71
85
  };
@@ -105,17 +105,17 @@ describe("System.create", () => {
105
105
  [{ kernel: "gpu", components: { write: [position], changed: [position] } }, "can't run on the GPU: GPU kernels don't support change filters yet"],
106
106
  [{ kernel: "gpu", components: { read: [position] }, lookup: { others: { read: [position] } } }, "can't run on the GPU: GPU kernels don't support lookups yet"],
107
107
  [{ kernel: "gpu", components: { has: [position] }, resources: { reduce: [total] } }, "can't run on the GPU: GPU kernels don't support reductions yet"],
108
- [{ observer: false, resources: { read: [time] } }, "observer is true or left out"],
109
- [{ observer: true, components: { write: [position] } }, "is an observer, so it writes nothing"],
110
- [{ observer: true, resources: { write: [time] } }, "is an observer, so it writes nothing"],
111
- [{ observer: true, components: { has: [position] }, resources: { reduce: [total] } }, "is an observer, so it reduces into nothing"],
112
- [{ observer: true, kernel: true, components: { read: [position] } }, "is an observer, so it isn't a kernel"],
113
- [{ observer: true, components: { read: [position] }, up: { through: parent } }, "is an observer, so it has no up"],
114
- [{ observer: true, resources: { read: [time] }, after: [System.create(() => {})] }, "is an observer, so it is ordered with nothing"],
115
- [{ observer: true, resources: { read: [time] }, before: [System.create(() => {})] }, "is an observer, so it is ordered with nothing"],
116
- [{ observer: true, resources: { read: [time] }, during: [System.create(() => {})] }, "is an observer, so it is ordered with nothing"],
108
+ [{ readonly: false, resources: { read: [time] } }, "readonly is true or left out"],
109
+ [{ readonly: true, components: { write: [position] } }, "is readonly, so it writes nothing"],
110
+ [{ readonly: true, resources: { write: [time] } }, "is readonly, so it writes nothing"],
111
+ [{ readonly: true, components: { has: [position] }, resources: { reduce: [total] } }, "is readonly, so it reduces into nothing"],
112
+ [{ readonly: true, kernel: true, components: { read: [position] } }, "is readonly, so it isn't a kernel"],
113
+ [{ readonly: true, components: { read: [position] }, up: { through: parent } }, "is readonly, so it has no up"],
114
+ [{ readonly: true, resources: { read: [time] }, after: [System.create(() => {})] }, "is readonly, so it is ordered with nothing"],
115
+ [{ readonly: true, resources: { read: [time] }, before: [System.create(() => {})] }, "is readonly, so it is ordered with nothing"],
116
+ [{ readonly: true, resources: { read: [time] }, during: [System.create(() => {})] }, "is readonly, so it is ordered with nothing"],
117
117
  ];
118
- expect(() => createTaking({ observer: true }, { type: "object", properties: {} })).toThrow("is an observer, so it takes no input");
118
+ expect(() => createTaking({ readonly: true }, { type: "object", properties: {} })).toThrow("is readonly, so it takes no input");
119
119
  rejected.forEach(([declaration, message], index) => {
120
120
  expect(() => create({ ...declaration })).toThrow(message);
121
121
  });
@@ -171,9 +171,9 @@ describe("System.create of a composite", () => {
171
171
  const looped = System.create({ before: [first], after: [second] }, () => {});
172
172
  expect(() => System.create({ systems: [first, second, looped] })).toThrow("Ordering cycle");
173
173
  });
174
- it("runs observer members after its sync point, in no tier, never conflicting and ignoring constraints naming them", () => {
174
+ it("runs readonly members after its sync point, in no tier, never conflicting and ignoring constraints naming them", () => {
175
175
  const write = System.create({ components: { with: [position] } }, row => { row[position] += 1; });
176
- const watch = System.create({ observer: true, components: { with: [position] } }, row => { void row[position]; });
176
+ const watch = System.create({ readonly: true, components: { with: [position] } }, row => { void row[position]; });
177
177
  const later = System.create({ components: { with: [position] }, after: [watch, write] }, row => { void row[position]; });
178
178
  const free = System.create({ after: [watch] }, ({ resources }) => { void resources[time]; });
179
179
  const schedule = scheduleOf(requireDefinition(System.create([write, watch, later, free])));
@@ -205,6 +205,6 @@ describe("System.render", () => {
205
205
  const render = (declaration: object, uses: Parameters<typeof giveAccess>[1] = {}): unknown => Reflect.apply(System.render, undefined, [giveAccess(declaration, uses), () => {}]);
206
206
  expect(() => System.render({ shader: { accessors: { loop: height } } }, () => {})).toThrow(`can't render: "loop" isn't usable as a WGSL function name.`);
207
207
  expect(() => render({ shader: { accessors: {} } }, { resources: { reduce: [total] } })).toThrow("writes or reduces into resources; a render system only reads");
208
- expect(() => render({ shader: { accessors: {} }, observer: true })).toThrow("is an observer, so it records no GPU work");
208
+ expect(() => render({ shader: { accessors: {} }, readonly: true })).toThrow("is readonly, so it records no GPU work");
209
209
  });
210
210
  });
@@ -50,10 +50,10 @@ type GpuKernel = {
50
50
  };
51
51
 
52
52
  /**
53
- * An observer system only reads, after a sync point: its row and resources are read-only, and it never compiles to a
53
+ * A readonly system only reads, after a sync point: its row and resources are read-only, and it never compiles to a
54
54
  * kernel, has no `up`, and is ordered with nothing. That it takes no input is checked at its body.
55
55
  */
56
- type Observer = {
56
+ type Readonly = {
57
57
  readonly kernel?: false;
58
58
  readonly up?: never;
59
59
  readonly before?: never;
@@ -97,7 +97,12 @@ type Validate<D extends Declaration> =
97
97
  // GPU kernels don't read components an archetype may lack yet.
98
98
  & (D extends { readonly components: { readonly optional: readonly [unknown, ...unknown[]] } }
99
99
  ? { readonly kernel?: boolean | { readonly "error: a GPU kernel can't have optional components yet": true } } : {})
100
- & (D extends { readonly observer: true } ? Observer : {})
100
+ & (D extends { readonly readonly: true } ? Readonly : {})
101
+ // Tables are copies a readonly system without components reads.
102
+ & (D extends { readonly tables: object }
103
+ ? (D extends { readonly readonly: true } ? {} : { readonly tables?: { readonly "error: tables are read by a readonly system": true } })
104
+ & (Property<D, "components"> extends undefined ? {} : { readonly tables?: { readonly "error: a system reading tables has no components": true } })
105
+ : {})
101
106
  & (D extends { readonly target: string } ? Targeted : {})
102
107
  & (D extends { readonly target: string } ? Property<D, "components"> extends undefined ? { readonly target?: { readonly "error: a system on one entity runs on its row, so it needs components": true } } : {} : {})
103
108
  // A body's input is the type of its parameter after its context, not part of its declaration.
@@ -109,7 +114,7 @@ type EntityFields<I> = { [K in keyof I]-?: [Exclude<I[K], undefined>] extends [E
109
114
  /**
110
115
  * What a declaration requires of the input its body takes, checked at the body, since the input is known only once the
111
116
  * body is: a target names an entity field of it, and a system with input doesn't filter on added or changed, as its
112
- * runs pass different input, nor is it an observer. Every check passes for `never`, the input before the body is
117
+ * runs pass different input, nor is it a readonly system. Every check passes for `never`, the input before the body is
113
118
  * inferred.
114
119
  */
115
120
  type Checks<D, I> = [I] extends [never] ? unknown :
@@ -119,7 +124,7 @@ type Checks<D, I> = [I] extends [never] ? unknown :
119
124
  : unknown)
120
125
  & ([I] extends [undefined] ? unknown
121
126
  : (D extends { readonly components: { readonly added: object } | { readonly changed: object } } ? { readonly "error: a system with input can't filter on added or changed, as its runs pass different input; pass it through a resource": true } : unknown)
122
- & (D extends { readonly observer: true } ? { readonly "error: an observer system takes no input": true } : unknown));
127
+ & (D extends { readonly readonly: true } ? { readonly "error: a readonly system takes no input": true } : unknown));
123
128
 
124
129
  /** The body of a system that declares nothing, which runs once per run with its resources and commands. */
125
130
  type Alone = (context: Context<{}>) => void;
@@ -144,10 +149,10 @@ type Checked<C> = C extends Alone ? ReturnsNoPromise<C> : C extends CompositeDec
144
149
  * input, with every field of theirs. Throws if a system names a key that isn't registered as its kind, its body writes
145
150
  * a reference, a lookup's rows are read where it writes them or it goes through an index keyed by what it writes, its
146
151
  * rows are ordered by an index it isn't sorted or keyed by what it writes, its object components and resources live on
147
- * different threads, or its reductions, conditions, `up`, `order` or placement can't hold, or it's an observer and
152
+ * different threads, or its reductions, conditions, `up`, `order` or placement can't hold, or it's a readonly system and
148
153
  * writes, reduces, takes input, is a kernel, has `up` or is ordered; if the build couldn't follow how its body uses its
149
154
  * resources, lookups or parent; or if a composite's member isn't a system a constructor returned, or two members
150
- * declare an input field with different schemas. A composite's observer members run after its sync point, ordered
155
+ * declare an input field with different schemas. A composite's readonly members run after its sync point, ordered
151
156
  * with nothing.
152
157
  *
153
158
  * A run of a system without components resolves to what its body returns, or `undefined` when its conditions, resource
@@ -180,13 +185,13 @@ export function create(first: Declaration | Leaf["body"] | readonly System<unkno
180
185
 
181
186
  /** A declaration as the world takes it, with what the build inferred its body uses. */
182
187
  const resolve = (what: string, declared: Declaration, access: ReturnType<typeof accessOf>): Resolved => {
183
- const reading = declared.observer === true;
188
+ const reading = declared.readonly === true;
184
189
  const { components, lookup, up, resources, ...rest } = declared;
185
190
  const uses = resourceUses(what, access, resources?.is);
186
- // What `where`'s predicates read counts as the system's reads, for its order, conflicts and placement.
187
191
  const whereReads = whereReadsOf(what, components, access);
188
192
  const conditions: readonly symbol[] = resources?.is ?? [];
189
- for (const key of whereReads) if (!uses.read.includes(key) && !uses.write.includes(key) && !conditions.includes(key)) uses.read.push(key);
193
+ // What `where`'s predicates and `tables` read counts as the system's reads, for its order, conflicts and placement.
194
+ for (const key of [...whereReads, ...declared.tables?.resources ?? []]) if (!uses.read.includes(key) && !uses.write.includes(key) && !conditions.includes(key)) uses.read.push(key);
190
195
  const resolved: Resolved = {
191
196
  ...rest,
192
197
  ...components === undefined ? {} : { components: componentLists(components, access.components, reading, whereReads) },
@@ -4,6 +4,7 @@ import type { Resource } from '../resource/resource.js';
4
4
  import type { ComponentAccess, ComponentLists, ResourceAccess, ResourceLists, SystemLookup, SystemLookupLists, Up, UpLists } from './access.js';
5
5
  import type { Shader } from './render.js';
6
6
  import type { System } from './system.js';
7
+ import type { TablesDeclaration } from './tables.js';
7
8
 
8
9
  /**
9
10
  * A system never declares its thread: it runs on the one its object components and resources live on, those it
@@ -59,9 +60,15 @@ export type Declaration = {
59
60
  readonly during?: readonly System<unknown>[];
60
61
  /**
61
62
  * Runs after the last sync point of each run and tick (a tick is a run of the added composite), reading the settled
62
- * state; changes nothing; never ordered; doesn't keep the loop ticking.
63
+ * state, on the thread that added or ran it; changes nothing, its row and resources typed read-only and its context
64
+ * without `commands`; takes no input; never ordered; doesn't keep the loop ticking. A joined thread adds only these.
63
65
  */
64
- readonly observer?: true;
66
+ readonly readonly?: true;
67
+ /**
68
+ * For a readonly system without components: gives its context `tables`, copies of whole archetypes' columns, as
69
+ * `TablesDeclaration` says, read on the world's thread. Its components and resources count as what it reads.
70
+ */
71
+ readonly tables?: TablesDeclaration;
65
72
  };
66
73
 
67
74
  /** A declaration as the world takes it: the build's inferred reads, writes and reductions joined with what it declares. */
@@ -23,7 +23,7 @@ export const Particle = [position, velocity] as const;
23
23
  expectType<Equal<typeof Particle, readonly [typeof position, typeof velocity]>>();
24
24
  export const one: readonly Component.Key[] = Particle;
25
25
  export const move = System.create({ components: { with: [...Particle] } }, row => { row[position] += row[velocity]; });
26
- export const read = System.create({ observer: true, components: { with: [...Particle] } }, row => { void row[velocity]; });
26
+ export const read = System.create({ readonly: true, components: { with: [...Particle] } }, row => { void row[velocity]; });
27
27
 
28
28
  // Declared without `as const`, a list's keys widen to `symbol`, which no list of components takes.
29
29
  const loose = [position, velocity];
@@ -36,7 +36,7 @@ System.create({
36
36
  }, row => { void row[velocity]; });
37
37
 
38
38
  // An observer system reads its optional components and writes nothing.
39
- System.create({ observer: true, components: { with: [position], optional: [velocity] } }, row => {
39
+ System.create({ readonly: true, components: { with: [position], optional: [velocity] } }, row => {
40
40
  void row[velocity];
41
41
  // @ts-expect-error an observer system's row is read-only
42
42
  row[velocity] = 1;
@@ -126,7 +126,7 @@ System.create({ components: { with: [health], optional: [team] } }, row => {
126
126
  // @ts-expect-error nor an optional one
127
127
  row[team] = "red";
128
128
  });
129
- System.create({ observer: true, components: { with: [team] } }, row => {
129
+ System.create({ readonly: true, components: { with: [team] } }, row => {
130
130
  check<Equal<typeof row[typeof team], "red" | "blue">>();
131
131
  });
132
132
 
@@ -367,7 +367,7 @@ System.render({ shader: { accessors: { place: position } } }, gpu => { void gpu.
367
367
 
368
368
  // Observer systems read what a sync point left, after it: rows, resources, lookups and change filters, and no commands.
369
369
  export const watchPositions = System.create({
370
- observer: true,
370
+ readonly: true,
371
371
  components: { with: [position], changed: [position] },
372
372
  lookup: { targets: { with: [velocity] } },
373
373
  }, (row, context) => {
@@ -379,33 +379,39 @@ export const watchPositions = System.create({
379
379
  void context.commands;
380
380
  });
381
381
  check<Equal<typeof watchPositions, System>>();
382
- export const watchTime = System.create({ observer: true, resources: { changed: [time] } }, ({ resources }) => { void resources[time]; });
382
+ export const watchTime = System.create({ readonly: true, resources: { changed: [time] } }, ({ resources }) => { void resources[time]; });
383
383
  // A composite may hold observer members, which run after its sync point.
384
384
  export const withWatch = System.create([integrate, watchTime]);
385
385
  check<Equal<typeof withWatch, System>>();
386
- // @ts-expect-error observer is true or left out
387
- System.create({ observer: false }, () => {});
388
- System.create({ observer: true, components: { with: [position] } }, (row, { resources }) => {
389
- // @ts-expect-error an observer system writes no components
386
+ // @ts-expect-error observer isn't a declaration's: readonly is
387
+ System.create({ observer: true }, () => {});
388
+ // @ts-expect-error readonly is true or left out
389
+ System.create({ readonly: false }, () => {});
390
+ System.create({ readonly: true, components: { with: [position] } }, (row, { resources }) => {
391
+ // @ts-expect-error a readonly system writes no components
390
392
  row[position] = 0;
391
- // @ts-expect-error an observer system writes no resources
393
+ // @ts-expect-error a readonly system writes no resources
392
394
  resources[score] = 0;
393
395
  });
394
- System.create({ observer: true }, context => {
395
- // @ts-expect-error an observer system reduces into nothing
396
+ System.create({ readonly: true }, context => {
397
+ // @ts-expect-error a readonly system reduces into nothing
396
398
  void context.reduce;
399
+ // @ts-expect-error a readonly system has no commands
400
+ void context.commands;
397
401
  });
398
- // @ts-expect-error an observer system takes no input
399
- System.create({ observer: true }, (_context, input: { readonly count: number }) => { void input; });
400
- // @ts-expect-error an observer system isn't a kernel
401
- System.create({ observer: true, kernel: true, components: { with: [position] } }, () => {});
402
- System.create({ observer: true, kernel: false, components: { with: [position] } }, () => {});
403
- // @ts-expect-error an observer system is ordered with nothing
404
- System.create({ observer: true, after: [integrate] }, () => {});
405
- // @ts-expect-error an observer system is ordered with nothing
406
- System.create({ observer: true, before: [integrate] }, () => {});
407
- // @ts-expect-error an observer system is ordered with nothing
408
- System.create({ observer: true, during: [integrate] }, () => {});
402
+ // @ts-expect-error a readonly system takes no input
403
+ System.create({ readonly: true }, (_context, input: { readonly count: number }) => { void input; });
404
+ // @ts-expect-error a readonly system isn't a kernel
405
+ System.create({ readonly: true, kernel: true, components: { with: [position] } }, () => {});
406
+ // @ts-expect-error a readonly system isn't a GPU kernel
407
+ System.create({ readonly: true, kernel: "gpu", components: { with: [position] } }, () => {});
408
+ System.create({ readonly: true, kernel: false, components: { with: [position] } }, () => {});
409
+ // @ts-expect-error a readonly system is ordered with nothing
410
+ System.create({ readonly: true, after: [integrate] }, () => {});
411
+ // @ts-expect-error a readonly system is ordered with nothing
412
+ System.create({ readonly: true, before: [integrate] }, () => {});
413
+ // @ts-expect-error a readonly system is ordered with nothing
414
+ System.create({ readonly: true, during: [integrate] }, () => {});
409
415
 
410
416
  // A system on one entity: its input names the entity, and its body runs on that entity's row.
411
417
  type OnOne = { readonly entity: Entity; readonly by: number };