@adobe/ecs 0.2.0 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. package/README.md +2 -2
  2. package/dist/implementation/compiler/describe.d.ts +1 -1
  3. package/dist/implementation/compiler/describe.js +3 -3
  4. package/dist/implementation/compiler/describe.js.map +1 -1
  5. package/dist/implementation/compiler/signature.js +1 -1
  6. package/dist/implementation/compiler/transform.js +3 -3
  7. package/dist/implementation/compiler/transform.js.map +1 -1
  8. package/dist/implementation/world/commands.js +50 -1
  9. package/dist/implementation/world/commands.js.map +1 -1
  10. package/dist/implementation/world/create.js.map +1 -1
  11. package/dist/implementation/world/entities.d.ts +2 -0
  12. package/dist/implementation/world/entities.js +6 -0
  13. package/dist/implementation/world/entities.js.map +1 -1
  14. package/dist/implementation/world/finalizers.js.map +1 -1
  15. package/dist/implementation/world/join.js +10 -2
  16. package/dist/implementation/world/join.js.map +1 -1
  17. package/dist/implementation/world/register.js +2 -2
  18. package/dist/implementation/world/rows.d.ts +1 -1
  19. package/dist/implementation/world/rows.js +5 -0
  20. package/dist/implementation/world/rows.js.map +1 -1
  21. package/dist/implementation/world/runner.js +3 -3
  22. package/dist/implementation/world/runner.js.map +1 -1
  23. package/dist/implementation/world/sharing.js +1 -1
  24. package/dist/implementation/world/staging.d.ts +4 -0
  25. package/dist/implementation/world/staging.js +15 -3
  26. package/dist/implementation/world/staging.js.map +1 -1
  27. package/dist/implementation/world/tables-read.d.ts +7 -0
  28. package/dist/implementation/world/tables-read.js +60 -0
  29. package/dist/implementation/world/tables-read.js.map +1 -0
  30. package/dist/implementation/world/tables.d.ts +9 -0
  31. package/dist/implementation/world/tables.js +355 -0
  32. package/dist/implementation/world/tables.js.map +1 -0
  33. package/dist/implementation/world/thread-observer.js +1 -1
  34. package/dist/implementation/world/thread-observer.js.map +1 -1
  35. package/dist/implementation/world/thread-systems.js +1 -0
  36. package/dist/implementation/world/thread-systems.js.map +1 -1
  37. package/dist/index.d.ts +8 -8
  38. package/dist/index.js +4 -9
  39. package/dist/index.js.map +1 -1
  40. package/dist/interface/component/component.d.ts +7 -1
  41. package/dist/interface/component/component.js +0 -1
  42. package/dist/interface/component/component.js.map +1 -1
  43. package/dist/interface/component/declarations.d.ts +4 -0
  44. package/dist/interface/component/declarations.js +8 -0
  45. package/dist/interface/component/declarations.js.map +1 -0
  46. package/dist/interface/component/declared.d.ts +12 -0
  47. package/dist/interface/component/declared.js +2 -0
  48. package/dist/interface/component/declared.js.map +1 -0
  49. package/dist/interface/component/index.d.ts +3 -1
  50. package/dist/interface/component/index.js +1 -1
  51. package/dist/interface/component/index.js.map +1 -1
  52. package/dist/interface/component/value.d.ts +5 -5
  53. package/dist/interface/component/values.d.ts +2 -2
  54. package/dist/interface/finalizer/body.d.ts +5 -5
  55. package/dist/interface/registry/names.d.ts +2 -0
  56. package/dist/interface/registry/names.js +9 -0
  57. package/dist/interface/registry/names.js.map +1 -1
  58. package/dist/interface/registry/schedule.d.ts +3 -3
  59. package/dist/interface/registry/schedule.js +3 -3
  60. package/dist/interface/registry/schedule.js.map +1 -1
  61. package/dist/interface/resource/condition.d.ts +3 -3
  62. package/dist/interface/resource/declarations.d.ts +4 -0
  63. package/dist/interface/resource/declarations.js +8 -0
  64. package/dist/interface/resource/declarations.js.map +1 -0
  65. package/dist/interface/resource/index.d.ts +2 -1
  66. package/dist/interface/resource/index.js +1 -1
  67. package/dist/interface/resource/index.js.map +1 -1
  68. package/dist/interface/resource/resource.d.ts +4 -1
  69. package/dist/interface/resource/resource.js +0 -1
  70. package/dist/interface/resource/resource.js.map +1 -1
  71. package/dist/interface/resource/value.d.ts +5 -5
  72. package/dist/interface/resources/cadence.d.ts +1 -1
  73. package/dist/interface/system/access.d.ts +1 -1
  74. package/dist/interface/system/body.d.ts +13 -8
  75. package/dist/interface/system/check.d.ts +1 -1
  76. package/dist/interface/system/check.js +25 -9
  77. package/dist/interface/system/check.js.map +1 -1
  78. package/dist/interface/system/commands.d.ts +20 -4
  79. package/dist/interface/system/create.d.ts +21 -9
  80. package/dist/interface/system/create.js +3 -3
  81. package/dist/interface/system/create.js.map +1 -1
  82. package/dist/interface/system/declaration.d.ts +9 -2
  83. package/dist/interface/system/struct.d.ts +2 -2
  84. package/dist/interface/system/tables.d.ts +20 -0
  85. package/dist/interface/system/tables.js +2 -0
  86. package/dist/interface/system/tables.js.map +1 -0
  87. package/dist/interface/system/values-of.d.ts +4 -4
  88. package/dist/interface/system/values.d.ts +5 -5
  89. package/dist/interface/trait/types.d.ts +4 -4
  90. package/dist/interface/world/create.d.ts +3 -7
  91. package/dist/interface/world/declarations.d.ts +2 -2
  92. package/dist/interface/world/index.d.ts +1 -1
  93. package/dist/interface/world/join.d.ts +2 -10
  94. package/dist/interface/world/table/index.d.ts +26 -0
  95. package/dist/interface/world/table/index.js +3 -0
  96. package/dist/interface/world/table/index.js.map +1 -0
  97. package/dist/interface/world/table/table.d.ts +11 -0
  98. package/dist/interface/world/table/table.js +2 -0
  99. package/dist/interface/world/table/table.js.map +1 -0
  100. package/dist/interface/world/world.d.ts +10 -10
  101. package/dist/world.d.ts +27 -0
  102. package/dist/world.js +27 -0
  103. package/dist/world.js.map +1 -0
  104. package/package.json +1 -1
  105. package/src/exports.test.ts +3 -3
  106. package/src/guide/00-keys.ts +1 -1
  107. package/src/guide/01-resources.md +1 -1
  108. package/src/guide/01-resources.ts +1 -1
  109. package/src/guide/02-entities.ts +2 -2
  110. package/src/guide/03-systems.md +2 -2
  111. package/src/guide/03-systems.out +1 -1
  112. package/src/guide/03-systems.ts +3 -3
  113. package/src/guide/04-ticks.md +1 -1
  114. package/src/guide/04-ticks.ts +3 -3
  115. package/src/guide/05-scheduling.ts +1 -1
  116. package/src/guide/06-sync-points.md +9 -2
  117. package/src/guide/06-sync-points.ts +2 -2
  118. package/src/guide/07-change-filters.ts +2 -2
  119. package/src/guide/08-input.ts +1 -1
  120. package/src/guide/09-rules.ts +1 -1
  121. package/src/guide/11-traits.ts +3 -3
  122. package/src/guide/11-traits.type-test.ts +1 -1
  123. package/src/guide/12-lookups.ts +2 -2
  124. package/src/guide/13-indexes.ts +4 -4
  125. package/src/guide/14-kernels.ts +1 -1
  126. package/src/guide/15-reductions.ts +1 -1
  127. package/src/guide/16-gpu.ts +1 -1
  128. package/src/guide/17-hierarchies.ts +1 -1
  129. package/src/guide/18-identity.out +1 -1
  130. package/src/guide/19-threads.md +4 -2
  131. package/src/guide/19-threads.ts +2 -2
  132. package/src/guide/20-shared.ts +3 -3
  133. package/src/guide/21-dom.md +2 -2
  134. package/src/guide/21-dom.ts +1 -1
  135. package/src/guide/22-canvas.md +1 -1
  136. package/src/guide/22-canvas.ts +1 -1
  137. package/src/guide/CLAUDE.md +1 -1
  138. package/src/guide/guide.test.ts +7 -5
  139. package/src/implementation/compiler/describe.test-helper.ts +1 -1
  140. package/src/implementation/compiler/describe.test.ts +1 -1
  141. package/src/implementation/compiler/describe.ts +3 -3
  142. package/src/implementation/compiler/hygiene.test.ts +1 -1
  143. package/src/implementation/compiler/plugin.test.ts +2 -2
  144. package/src/implementation/compiler/signature.ts +1 -1
  145. package/src/implementation/compiler/transform.test.ts +3 -3
  146. package/src/implementation/compiler/transform.ts +3 -3
  147. package/src/implementation/world/change-blocks.test.ts +1 -1
  148. package/src/implementation/world/commands.ts +44 -1
  149. package/src/implementation/world/create.ts +3 -3
  150. package/src/implementation/world/defaults.test.ts +1 -1
  151. package/src/implementation/world/entities.ts +6 -0
  152. package/src/implementation/world/finalizers.test.ts +1 -1
  153. package/src/implementation/world/finalizers.ts +3 -3
  154. package/src/implementation/world/index-changed.test.ts +2 -2
  155. package/src/implementation/world/index-joined.test.ts +5 -5
  156. package/src/implementation/world/index-order.test.ts +1 -1
  157. package/src/implementation/world/join.test.ts +4 -4
  158. package/src/implementation/world/join.ts +10 -2
  159. package/src/implementation/world/joined-observer.test.ts +1 -1
  160. package/src/implementation/world/joining-worker.fixture.ts +1 -1
  161. package/src/implementation/world/joining.fixture.ts +1 -1
  162. package/src/implementation/world/joining.test-helper.ts +1 -1
  163. package/src/implementation/world/kernel-spawns.test.ts +1 -1
  164. package/src/implementation/world/observe-factories.test.ts +2 -2
  165. package/src/implementation/world/observe-ticks-cases.test.ts +1 -1
  166. package/src/implementation/world/observe.test-helper.ts +1 -1
  167. package/src/implementation/world/observer-contained-worker.fixture.ts +1 -1
  168. package/src/implementation/world/observer-contained.test-helper.ts +1 -1
  169. package/src/implementation/world/observer-contained.test.ts +2 -2
  170. package/src/implementation/world/observer-joined.test-helper.ts +4 -4
  171. package/src/implementation/world/observer-joined.test.ts +23 -15
  172. package/src/implementation/world/observer.browser.test.ts +2 -2
  173. package/src/implementation/world/observer.test.ts +8 -8
  174. package/src/implementation/world/optional-workers.fixture.ts +1 -1
  175. package/src/implementation/world/optional.test.ts +7 -7
  176. package/src/implementation/world/pool-size.cases.ts +3 -1
  177. package/src/implementation/world/reads.test-helper.ts +6 -6
  178. package/src/implementation/world/reclaim.test.ts +3 -3
  179. package/src/implementation/world/register.ts +2 -2
  180. package/src/implementation/world/remote-objects.test-helper.ts +1 -1
  181. package/src/implementation/world/rows.ts +5 -1
  182. package/src/implementation/world/run-results.test.ts +10 -10
  183. package/src/implementation/world/runner.ts +3 -3
  184. package/src/implementation/world/shared-gpu.browser.test.ts +2 -2
  185. package/src/implementation/world/shared-kernels.test.ts +2 -2
  186. package/src/implementation/world/shared-workers.fixture.ts +2 -2
  187. package/src/implementation/world/shared.test.ts +8 -8
  188. package/src/implementation/world/sharing.ts +1 -1
  189. package/src/implementation/world/staging.ts +16 -3
  190. package/src/implementation/world/tables-read.ts +59 -0
  191. package/src/implementation/world/tables.test.ts +267 -0
  192. package/src/implementation/world/tables.ts +330 -0
  193. package/src/implementation/world/tails.test.ts +1 -1
  194. package/src/implementation/world/target.test.ts +2 -2
  195. package/src/implementation/world/thread-observer.ts +1 -1
  196. package/src/implementation/world/thread-systems.test-helper.ts +3 -3
  197. package/src/implementation/world/thread-systems.ts +1 -0
  198. package/src/implementation/world/where.test.ts +4 -4
  199. package/src/implementation/world/workers.fixture.ts +2 -2
  200. package/src/index.ts +4 -6
  201. package/src/interface/component/component.ts +7 -1
  202. package/src/interface/component/declarations.ts +11 -0
  203. package/src/interface/component/declared.ts +9 -0
  204. package/src/interface/component/index.ts +3 -1
  205. package/src/interface/component/value.ts +5 -5
  206. package/src/interface/component/values.ts +2 -2
  207. package/src/interface/finalizer/body.ts +5 -5
  208. package/src/interface/index.type-test.ts +3 -3
  209. package/src/interface/indexes/index.type-test.ts +1 -1
  210. package/src/interface/registry/names.ts +8 -0
  211. package/src/interface/registry/schedule.ts +4 -4
  212. package/src/interface/resource/condition.ts +3 -3
  213. package/src/interface/resource/declarations.ts +11 -0
  214. package/src/interface/resource/index.ts +2 -1
  215. package/src/interface/resource/resource.ts +4 -1
  216. package/src/interface/resource/value.ts +5 -5
  217. package/src/interface/resources/cadence.ts +1 -1
  218. package/src/interface/system/access.ts +1 -1
  219. package/src/interface/system/body.ts +9 -7
  220. package/src/interface/system/check.ts +22 -8
  221. package/src/interface/system/commands.ts +18 -4
  222. package/src/interface/system/create.test.ts +13 -13
  223. package/src/interface/system/create.ts +15 -10
  224. package/src/interface/system/declaration.ts +9 -2
  225. package/src/interface/system/lists.type-test.ts +1 -1
  226. package/src/interface/system/optional.type-test.ts +1 -1
  227. package/src/interface/system/shared.type-test.ts +1 -1
  228. package/src/interface/system/struct.ts +2 -2
  229. package/src/interface/system/system.type-test.ts +26 -20
  230. package/src/interface/system/tables.ts +21 -0
  231. package/src/interface/system/up.type-test.ts +3 -3
  232. package/src/interface/system/values-of.ts +4 -4
  233. package/src/interface/system/values.ts +5 -5
  234. package/src/interface/thread/thread.type-test.ts +1 -1
  235. package/src/interface/trait/trait.type-test.ts +4 -4
  236. package/src/interface/trait/types.ts +4 -4
  237. package/src/interface/world/create.ts +3 -7
  238. package/src/interface/world/declarations.ts +2 -2
  239. package/src/interface/world/index.ts +2 -2
  240. package/src/interface/world/join.ts +2 -10
  241. package/src/interface/world/loop.type-test.ts +6 -6
  242. package/src/interface/world/run.type-test.ts +3 -3
  243. package/src/interface/world/table/index.ts +19 -0
  244. package/src/interface/world/table/table.ts +12 -0
  245. package/src/interface/world/world.ts +10 -10
  246. package/src/observe/entity.ts +2 -2
  247. package/src/observe/members.ts +3 -3
  248. package/src/observe/query.ts +1 -1
  249. package/src/observe/resource.ts +2 -2
  250. package/src/observe/select.ts +5 -5
  251. package/src/observe/values.ts +2 -2
  252. package/src/observe/world-state.ts +3 -3
  253. package/src/perftest/benchmarks.ts +1 -1
  254. package/src/perftest/indexes.ts +3 -3
  255. package/src/perftest/loop.ts +1 -1
  256. package/src/perftest/observer.ts +4 -4
  257. package/src/perftest/reads.ts +2 -2
  258. package/src/perftest/remote.ts +5 -5
  259. package/src/perftest/strings.ts +1 -1
  260. package/src/perftest/suite.ts +1 -1
  261. package/src/world.test.ts +2 -2
  262. package/src/world.ts +32 -0
@@ -5,7 +5,7 @@ import { Component } from "../index.js";
5
5
  export const instanced = Symbol();
6
6
  declare module "@adobe/ecs" {
7
7
  interface Traits<N> {
8
- readonly [instanced]: N extends Component.Key ? ([Component.Value<N>] extends [readonly number[]] ? true : never) : never;
8
+ readonly [instanced]: N extends Component.Key ? ([Component<N>] extends [readonly number[]] ? true : never) : never;
9
9
  }
10
10
  }
11
11
 
@@ -25,7 +25,7 @@ const track = System.create({
25
25
 
26
26
  const refreshed: boolean[] = [];
27
27
  const watch = System.create({
28
- observer: true,
28
+ readonly: true,
29
29
  components: { with: [target] },
30
30
  lookup: { enemy: { with: [health], changed: [health] } },
31
31
  }, (row, { lookup }) => {
@@ -34,7 +34,7 @@ const watch = System.create({
34
34
  });
35
35
 
36
36
  const aims: number[] = [];
37
- const readAims = System.create({ observer: true, components: { with: [aim] } }, row => {
37
+ const readAims = System.create({ readonly: true, components: { with: [aim] } }, row => {
38
38
  aims.push(row[aim]);
39
39
  });
40
40
 
@@ -26,14 +26,14 @@ const count = System.create({ components: { with: [cell, crowded] }, lookup: { a
26
26
 
27
27
  const byScore = Index.create({ key: [score], sorted: true, components: { with: [player] } });
28
28
 
29
- const leaders = System.create({ observer: true, lookup: { scores: { index: byScore } } }, ({ lookup }) => {
29
+ const leaders = System.create({ readonly: true, lookup: { scores: { index: byScore } } }, ({ lookup }) => {
30
30
  const winners: string[] = [];
31
31
  const high = lookup.scores({ ">=": 90 });
32
32
  for (let i = 0; i < high.length; i++) winners.push(high.at(i)[player]);
33
33
  return winners;
34
34
  });
35
35
 
36
- const list = System.create({ observer: true, components: { with: [player, score] }, order: byScore }, row => {
36
+ const list = System.create({ readonly: true, components: { with: [player, score] }, order: byScore }, row => {
37
37
  console.log(row[player], row[score]);
38
38
  });
39
39
 
@@ -42,12 +42,12 @@ const find = System.create({ lookup: { byId: { index: byServerId } } }, ({ looku
42
42
  lookup.byId(input.id)?.[player] ?? null);
43
43
 
44
44
  const players = Index.create({ key: [], components: { with: [player] } });
45
- const countPlayers = System.create({ observer: true, lookup: { players: { index: players, changed: true } } }, ({ lookup }) => {
45
+ const countPlayers = System.create({ readonly: true, lookup: { players: { index: players, changed: true } } }, ({ lookup }) => {
46
46
  console.log("players", lookup.players().length);
47
47
  });
48
48
 
49
49
  const crowds: number[] = [];
50
- const readCrowds = System.create({ observer: true, components: { with: [crowded] } }, row => {
50
+ const readCrowds = System.create({ readonly: true, components: { with: [crowded] } }, row => {
51
51
  crowds.push(row[crowded]);
52
52
  });
53
53
 
@@ -29,7 +29,7 @@ const slow = System.create({ kernel: false, components: { with: [velocity] } },
29
29
  });
30
30
 
31
31
  const moved: string[] = [];
32
- const read = System.create({ observer: true, components: { with: [position, velocity] } }, row => {
32
+ const read = System.create({ readonly: true, components: { with: [position, velocity] } }, row => {
33
33
  moved.push(`at ${row[position][0]}, moving at ${row[velocity][0]}`);
34
34
  });
35
35
 
@@ -23,7 +23,7 @@ const weigh = System.create({ components: { with: [mass] } }, (row, { reduce })
23
23
  reduce[count](1);
24
24
  });
25
25
 
26
- const report = System.create({ observer: true }, ({ resources }) =>
26
+ const report = System.create({ readonly: true }, ({ resources }) =>
27
27
  `${resources[count]} weigh ${resources[total]}, at most ${resources[heaviest]}`);
28
28
 
29
29
  const spawn = System.create(({ commands }) => {
@@ -32,7 +32,7 @@ const draw = System.render({ shader: { accessors: { dotPosition: position, dotSi
32
32
  });
33
33
  const shaderCode = Gpu.wgsl(draw);
34
34
 
35
- const read = System.create({ observer: true, components: { with: [position] } }, row => {
35
+ const read = System.create({ readonly: true, components: { with: [position] } }, row => {
36
36
  console.log("at", row[position]);
37
37
  });
38
38
 
@@ -21,7 +21,7 @@ const place = System.create({ components: { with: [offset, placed] }, up: { thro
21
21
  });
22
22
 
23
23
  const where: string[] = [];
24
- const read = System.create({ observer: true, components: { with: [label, placed] } }, row => {
24
+ const read = System.create({ readonly: true, components: { with: [label, placed] } }, row => {
25
25
  where.push(`${row[label]} at ${row[placed]}`);
26
26
  });
27
27
 
@@ -1 +1 @@
1
- Error: System "explode" (@adobe/ecs@0.2.0/src/guide/18-identity.ts#explode) failed: boom
1
+ Error: System "explode" (@adobe/ecs@0.2.3/src/guide/18-identity.ts#explode) failed: boom
@@ -11,6 +11,8 @@ A *logical thread* is where objects live and the code touching them runs: the pa
11
11
  `World.join(port, { threads: ["audio"] })`, claiming the threads it fills. Usually the other end of the channel goes
12
12
  to a worker; [here both ends](19-threads.ts#L44-L54) are on one thread.
13
13
 
14
- - `share` resolves once the thread joined, with the observer systems it added as `join` resolved, before awaiting
15
- anything else. Those observers run there after the world's sync points: `listen` hears what `mix` wrote.
14
+ - `share` resolves once the thread joined, with the readonly systems it added as `join` resolved, before awaiting
15
+ anything else. Those readonly systems run there after the world's sync points: `listen` hears what `mix` wrote.
16
+ - A joined thread adds only [readonly systems](03-systems.md), which run on it; a system that writes is added on the
17
+ world's thread.
16
18
  - A joined world runs systems as the world does: [`audio.run`](19-threads.ts#L60-L61) spawns and mixes too.
@@ -22,7 +22,7 @@ const mix = System.create({ components: { with: [volume] } }, (row, { resources
22
22
  });
23
23
 
24
24
  let heard = 0;
25
- const listen = System.create({ observer: true }, ({ resources }) => {
25
+ const listen = System.create({ readonly: true }, ({ resources }) => {
26
26
  heard = resources[levels].size;
27
27
  });
28
28
 
@@ -31,7 +31,7 @@ const spawn = System.create(({ commands }) => {
31
31
  commands.spawn([volume])({ [volume]: 0.3 });
32
32
  });
33
33
 
34
- const countVoices = System.create({ observer: true }, ({ resources }) => resources[levels].size);
34
+ const countVoices = System.create({ readonly: true }, ({ resources }) => resources[levels].size);
35
35
 
36
36
  /** Waits a turn of the event loop at a time until `done`, for two seconds at most. */
37
37
  const until = async (done: () => boolean): Promise<void> => {
@@ -56,7 +56,7 @@ let drawn = 0;
56
56
  let left = Infinity;
57
57
  let right = -Infinity;
58
58
  const draw = System.create({
59
- observer: true,
59
+ readonly: true,
60
60
  components: { with: [position], where: { [cell]: (at, { resources }) => resources[camera]?.sees(at) === true } },
61
61
  }, row => {
62
62
  drawn += 1;
@@ -66,7 +66,7 @@ const draw = System.create({
66
66
 
67
67
  let refreshed = 0;
68
68
  const refresh = System.create({
69
- observer: true,
69
+ readonly: true,
70
70
  components: { with: [position], changed: [position], where: { [cell]: (at, { resources }) => resources[camera]?.sees(at) === true } },
71
71
  }, () => {
72
72
  refreshed += 1;
@@ -74,7 +74,7 @@ const refresh = System.create({
74
74
 
75
75
  // Only chunk [4, 0], which no unit is in until one crosses into it.
76
76
  let east = 0;
77
- const countEast = System.create({ observer: true, components: { with: [position], where: { [cell]: [4, 0] } } }, () => {
77
+ const countEast = System.create({ readonly: true, components: { with: [position], where: { [cell]: [4, 0] } } }, () => {
78
78
  east += 1;
79
79
  });
80
80
 
@@ -13,9 +13,9 @@ same.
13
13
  Each tick runs one composite:
14
14
 
15
15
  1. [`move`](21-dom.ts#L38-L44) moves every particle and turns it back from the walls.
16
- 2. [`create`](21-dom.ts#L46-L51) gives each particle without a dot an element, with `commands.add`. Observers can't
16
+ 2. [`create`](21-dom.ts#L46-L51) gives each particle without a dot an element, with `commands.add`. Readonly systems can't
17
17
  issue commands, so it ticks, and its condition `is: [stage]` waits for a stage.
18
- 3. [`draw`](21-dom.ts#L52-L60), an observer, sets each moved dot's `style.translate`, a transform, which the page
18
+ 3. [`draw`](21-dom.ts#L52-L60), a readonly system, sets each moved dot's `style.translate`, a transform, which the page
19
19
  doesn't lay out again. `changed: [position]` skips the dots that didn't move.
20
20
 
21
21
  [A finalizer](21-dom.ts#L61-L63) removes a particle's element when the particle goes, and every element when the
@@ -51,7 +51,7 @@ const create = System.create({ components: { with: [position], not: [dot] }, res
51
51
  });
52
52
  // Moves each dot whose particle moved, by a transform, which the page doesn't lay out again.
53
53
  const draw = System.create({
54
- observer: true,
54
+ readonly: true,
55
55
  components: { with: [position, dot], optional: [traced], changed: [position] },
56
56
  }, (row, { resources }) => {
57
57
  const [x, y] = row[position];
@@ -2,7 +2,7 @@
2
2
 
3
3
  The same particles, drawn in immediate mode: each frame, the canvas is cleared and drawn again whole.
4
4
 
5
- [`draw`](22-canvas.ts#L52-L80) is one observer without components, so it runs once per tick, not per particle.
5
+ [`draw`](22-canvas.ts#L52-L80) is one readonly system without components, so it runs once per tick, not per particle.
6
6
 
7
7
  - `changed: [clock]` runs it after every tick, and `is: [pen]` waits for a 2D context.
8
8
  - It reads every particle through [an index without a key](22-canvas.ts#L50-L51), `key: []`, and the traced one
@@ -51,7 +51,7 @@ const particles = Index.create({ key: [], components: { with: [position] } });
51
51
  const tracedOnes = Index.create({ key: [], components: { with: [position, traced] } });
52
52
  // Clears the canvas and draws every particle, after each tick.
53
53
  const draw = System.create({
54
- observer: true,
54
+ readonly: true,
55
55
  resources: { is: [pen], changed: [clock] },
56
56
  lookup: { particles: { index: particles }, traced: { index: tracedOnes } },
57
57
  }, ({ resources, lookup }) => {
@@ -45,5 +45,5 @@ it in `NN-name.type-test.ts`, under a `// @ts-expect-error` that the package's b
45
45
  before they're printed.
46
46
  - Keys are `Symbol()` with no description: the build's id names each by its binding.
47
47
  - Archetypes are types, so their names are PascalCase: `const Particle = [position, velocity] as const`.
48
- - Use the public API only, as a consumer imports it, not internal modules. Read a world through observer systems.
48
+ - Use the public API only, as a consumer imports it, not internal modules. Read a world through readonly systems.
49
49
  - Lines change as code does: the test of links fails on any that no longer covers whole code, so move them with it.
@@ -69,12 +69,12 @@ const listed = [...read("README.md").matchAll(/^(?:- |(\d+)\. )\[([^\]]+)\]\(([^
69
69
  /** What each chapter teaches, which its prose names as code and its program does; 0 is the unnumbered keys chapter's. */
70
70
  const teaches: { readonly [chapter: number]: readonly string[] } = {
71
71
  0: ["Symbol()", "declare module \"@adobe/ecs\"", "Components", "Resources", "row[score]", "resources[bonus]", "default"],
72
- 1: ["Resources", "default", "World.create", "observer: true", "run", "ReadonlyMap", "Entity"],
72
+ 1: ["Resources", "default", "World.create", "readonly: true", "run", "ReadonlyMap", "Entity"],
73
73
  2: ["Components", "type: true", "as const", "commands.spawn", "commands.remove", "with: [...Particle]"],
74
- 3: ["System.create", "with", "not", "observer: true", "optional: [velocity]"],
74
+ 3: ["System.create", "with", "not", "readonly: true", "optional: [velocity]"],
75
75
  4: ["clock", "delta", "cadence", "always", "resources: { changed: [elapsed] }", "wake", "reduce[wake]", "world.add", "world.remove"],
76
76
  5: ["after", "during", "System.create([heal, hit])", "during: [update]"],
77
- 6: ["commands.spawn", "commands.add", "commands.despawn", "after: [spawn]"],
77
+ 6: ["commands.spawn", "commands.add", "commands.despawn", "after: [spawn]", "readonly: true"],
78
78
  7: ["resources: { changed: [level] }", "changed: [score]", "resources: { is: [map], not: [paused] }", "resources[map]"],
79
79
  8: ["world.run(hit, { amount: 3 })", "target: \"entity\""],
80
80
  9: ["Rule.create", "derived: true"],
@@ -106,10 +106,12 @@ const mutations: { readonly [chapter: number]: readonly (readonly [string, strin
106
106
  ],
107
107
  1: [["World.create({ [gravity]: -1.6 })", "World.create()"], ["default: -9.8 ", ""], ["{ default: -Infinity }", "{}"]],
108
108
  2: [["commands.remove(row.entity, enemy);", ""], ["{ [position]: [0, 10, 0] }", "{}"]],
109
- 3: [["not: [frozen]", "not: []"], ["System.create([count, fail])", "System.create([fail])"]],
109
+ 3: [
110
+ ["not: [frozen]", "not: []"], ["System.create([count, fail])", "System.create([fail])"],
111
+ ],
110
112
  4: [["world.remove(advance);", ""]],
111
113
  5: [["after: [hit] ", ""], ["during: [update] ", ""]],
112
- 6: [["commands.add(hero, health, 5);", ""], ["commands.despawn(commands.spawn([health])({ [health]: 1 }));", "commands.spawn([health])({ [health]: 1 });"], ["watched.add(watch);", ""]],
114
+ 6: [["commands.add(hero, health, 5);", ""], ["commands.despawn(commands.spawn([health])({ [health]: 1 }));", "commands.spawn([health])({ [health]: 1 });"], ["watched.add(watch);", ""], ["const watch = System.create({ readonly: true, ", "const watch = System.create({ "]],
113
115
  7: [["resources: { changed: [level] } ", ""], [", changed: [score]", ""], [", not: [paused]", ""], ["is: [map], ", ""]],
114
116
  8: [["target: \"entity\",", ""]],
115
117
  9: [["with: [mesh], not: [hidden]", "with: [mesh]"], ["Rule.create({ components: { with: [sprite], not: [hidden] }, derive: [drawn] });", ""], ["Rule.create({ components: { with: [drawn] }, derive: [layer] });", ""]],
@@ -51,7 +51,7 @@ export const spawn = System.create(({ commands }) => {
51
51
  commands.spawn([position, velocity])({ [position]: [0, 0, 0], [velocity]: [1, 0, 0] });
52
52
  });
53
53
 
54
- export const observe = System.create({ observer: true, components: { with: [position] } }, row => {
54
+ export const observe = System.create({ readonly: true, components: { with: [position] } }, row => {
55
55
  console.log(row[position]);
56
56
  });
57
57
 
@@ -109,7 +109,7 @@ describe("a module's systems described", () => {
109
109
  it("says why systems that run as written have no code: per-run systems, observers, render systems, kernel: false and one declared in a function", () => {
110
110
  expect(described("spawn")).toMatchObject({ kind: "per-run", runs: `Its body as written, on the "world" thread` });
111
111
  for (const backend of ["js", "wasm", "wgsl"] as const) expect(reasonOf(described("spawn")[backend])).toBe("per-run systems run as written; only row systems compile");
112
- expect(described("observe")).toMatchObject({ kind: "observer", js: { kind: "none", reason: expect.stringMatching(/^an observer runs as written/) } });
112
+ expect(described("observe")).toMatchObject({ kind: "readonly", js: { kind: "none", reason: expect.stringMatching(/^a readonly system runs as written/) } });
113
113
  expect(described("draw")).toMatchObject({ kind: "render", runs: expect.stringContaining("while the world has a GPU device"), wgsl: { kind: "none", reason: expect.stringMatching(/^a render system runs as written/) } });
114
114
  const unflagged = described("unflagged");
115
115
  expect(unflagged).toMatchObject({ kernel: false, js: { kind: "none", reason: "as written, because `kernel: false`" }, wasm: { kind: "none", reason: "as written, because `kernel: false`" }, wgsl: { kind: "eligible" } });
@@ -30,7 +30,7 @@ export type Backend =
30
30
  * What runs a system: over every matched row, once per run, as an observer, on one entity its input names (`target`),
31
31
  * or as a render system.
32
32
  */
33
- export type SystemKind = "row" | "per-run" | "observer" | "target" | "render";
33
+ export type SystemKind = "row" | "per-run" | "readonly" | "target" | "render";
34
34
 
35
35
  /**
36
36
  * A system of a module as the build compiles it: its call's offsets in the code transformed, the binding it's
@@ -427,7 +427,7 @@ export const describeSystems = async (sites: TransformedSites, format: (code: st
427
427
  const kernel: boolean | "gpu" | null = literal === true || literal === false ? literal : literal === "gpu" ? "gpu" : null;
428
428
  const kind: SystemKind = created === "render" ? "render"
429
429
  : declaration === undefined ? "per-run"
430
- : propertyOf(declaration, "observer") !== null ? "observer"
430
+ : propertyOf(declaration, "readonly") !== null ? "readonly"
431
431
  : propertyOf(declaration, "target") !== null ? "target"
432
432
  : propertyOf(declaration, "components") !== null ? "row" : "per-run";
433
433
  const base = { start: node.start, end: node.end, binding: bindings.get(node) ?? null, kind, kernel };
@@ -442,7 +442,7 @@ export const describeSystems = async (sites: TransformedSites, format: (code: st
442
442
  ? threadOf(declaration, body, kind === "render" ? positionsOf("render", declaration) : positionsOf("system", declaration)) : "world";
443
443
  if (kind === "render") return asWritten("a render system runs as written, recording GPU work with shaders of your own; `Gpu.wgsl` gives them their accessors", "world");
444
444
  if (kind === "per-run") return asWritten("per-run systems run as written; only row systems compile", written());
445
- if (kind === "observer") return asWritten("an observer runs as written, reading the settled state after the run's last sync point; only row systems that aren't observers compile", written());
445
+ if (kind === "readonly") return asWritten("a readonly system runs as written, reading the settled state after the run's last sync point; only row systems that aren't readonly compile", written());
446
446
  if (kind === "target") return asWritten("a system with `target` runs as written, once, on the entity its input names; only systems over every matched row compile", written());
447
447
  if (declaration === undefined || body === undefined || body.type === "SpreadElement" || !isFunction(body)) {
448
448
  return asWritten(declaration === undefined ? "its declaration isn't written in its call, so the build can't read whether it's a kernel" : "its body isn't a function written in place or bound at module scope, so the build can't read it", "world");
@@ -34,7 +34,7 @@ const spawn = System.create(({ commands }) => {
34
34
  });
35
35
 
36
36
  const read: number[][] = [];
37
- const readAll = System.create({ observer: true, components: { with: [p, c, l, s, e, v0, v1] } }, row => {
37
+ const readAll = System.create({ readonly: true, components: { with: [p, c, l, s, e, v0, v1] } }, row => {
38
38
  read.push([row[p], row[c], row[l], row[s], row[e], row[v0], row[v1]]);
39
39
  });
40
40
 
@@ -645,7 +645,7 @@ export { World as OtherWorld };
645
645
  export const runOther = async (key) => {
646
646
  const spawn = System.create(({ commands }) => { commands.spawn([key].slice())({ [key]: 10 }); });
647
647
  let total = 0;
648
- const count = System.create({ observer: true, components: { with: [key] } }, (row) => { total += row[key]; });
648
+ const count = System.create({ readonly: true, components: { with: [key] } }, (row) => { total += row[key]; });
649
649
  const world = World.create();
650
650
  await world.run(spawn);
651
651
  await world.run(spawn);
@@ -662,7 +662,7 @@ declare$(score, "component", { type: { type: "number" } });
662
662
  ${recorder}
663
663
  const spawn = System.create(({ commands }) => { commands.spawn([score].slice())({ [score]: 1 }); });
664
664
  let counted = 0;
665
- const count = System.create({ observer: true, components: { with: [score] } }, (row) => { counted += row[score]; });
665
+ const count = System.create({ readonly: true, components: { with: [score] } }, (row) => { counted += row[score]; });
666
666
  export const copies = World === OtherWorld ? 1 : 2;
667
667
  export const run = async () => {
668
668
  const world = World.create();
@@ -120,7 +120,7 @@ export const kernelOf = (declaration: ObjectExpression): KernelRequest | null =>
120
120
  }
121
121
  }
122
122
  if (declaration.properties.some(property => property.type === "SpreadElement")) return null;
123
- if (!declares(declaration, "components") || declares(declaration, "observer") || declares(declaration, "target")) return null;
123
+ if (!declares(declaration, "components") || declares(declaration, "readonly") || declares(declaration, "target")) return null;
124
124
  return { target: "cpu", required: false };
125
125
  };
126
126
 
@@ -335,7 +335,7 @@ describe("kernels by default", () => {
335
335
  test("runs as written, without failing, a body that leaves the subset or a declaration a kernel can't have", async () => {
336
336
  expect(await attached(`const log = []; const kernel = System.create({ components: { with: [mass] } }, row => { log.push(row[mass]); });`)).toBe(false);
337
337
  expect(await attached(`const kernel = System.create({ components: { with: [mass] } }, (row, { resources }) => { resources[delta] = row[mass]; });`)).toBe(false);
338
- expect(await attached(`const kernel = System.create({ observer: true, components: { with: [mass] } }, row => { void row[mass]; });`)).toBe(false);
338
+ expect(await attached(`const kernel = System.create({ readonly: true, components: { with: [mass] } }, row => { void row[mass]; });`)).toBe(false);
339
339
  expect(await attached(`const kernel = System.create({ target: "entity", components: { with: [score] } }, row => { row[score] = 1; });`)).toBe(false);
340
340
  expect(await attached(`const make = () => System.create({ components: { with: [score] } }, row => { row[score] = 1; });`)).toBe(false);
341
341
  });
@@ -1418,11 +1418,11 @@ describe("inferred access", () => {
1418
1418
  });
1419
1419
 
1420
1420
  test("an observer's row it can't follow as unknown without a warning, since the world takes an observer's as read", async () => {
1421
- const result = await transform(system("observer: true, components: { with: [position] }", "(row) => { helper(row); }"));
1421
+ const result = await transform(system("readonly: true, components: { with: [position] }", "(row) => { helper(row); }"));
1422
1422
  expect(result !== null && "code" in result ? [result.code.includes("components: null"), result.warnings] : null).toEqual([true, []]);
1423
1423
  });
1424
1424
 
1425
- test("a row it can't follow with a warning unless the declaration says observer: true, as the world reads it", async () => {
1425
+ test("a row it can't follow with a warning unless the declaration says readonly: true, as the world reads it", async () => {
1426
1426
  for (const observer of ["observer: false, ", "observer: watching, "]) {
1427
1427
  const result = await transform(`const watching = true;\n${system(`${observer}components: { with: [position] }`, "(row) => { helper(row); }")}`);
1428
1428
  expect(result !== null && "code" in result ? result.warnings.length : null).toBe(1);
@@ -141,7 +141,7 @@ export const functionOf = (binding: ModuleBinding | undefined): Node | undefined
141
141
  const empty: ObjectExpression = { type: "ObjectExpression", properties: [], start: 0, end: 0 };
142
142
 
143
143
  /** Whether a declaration has property `name`, written out. */
144
- /** Whether a declaration gives `name` the literal `true`, as the world takes `observer`. */
144
+ /** Whether a declaration gives `name` the literal `true`, as the world takes `readonly`. */
145
145
  const declaresTrue = (declaration: ObjectExpression, name: string): boolean => declaration.properties.some(property =>
146
146
  property.type === "Property" && !property.computed && property.key.type === "Identifier" && property.key.name === name
147
147
  && property.value.type === "Literal" && property.value.value === true);
@@ -313,9 +313,9 @@ export const transformKernels = async (
313
313
  const used = kernel?.used ?? inOrder(usedBy(declared, body, module.bindings, positions), declared);
314
314
  requireRequired(used, declared, body);
315
315
  const where = at(body);
316
- // An observer's row, and a lookup's rows, it can't follow are taken as read whole, which is what their
316
+ // A readonly system's row, and a lookup's rows, it can't follow are taken as read whole, which is what their
317
317
  // declarations already say; only a row taken as written whole costs what the author should know of.
318
- if (used.query === null && !declaresTrue(declared, "observer")) warnings.push(`${where}: the build can't follow how this body uses its row, so it takes every component in with, added and changed as written: index the row by key directly, or destructure it by key, to let it tell what the body reads and writes.`);
318
+ if (used.query === null && !declaresTrue(declared, "readonly")) warnings.push(`${where}: the build can't follow how this body uses its row, so it takes every component in with, added and changed as written: index the row by key directly, or destructure it by key, to let it tell what the body reads and writes.`);
319
319
  accessing = true;
320
320
  const access = accessText(used, where, whereReads.has(node) ? whereReads.get(node) ?? null : undefined);
321
321
  if (kernel !== undefined) {
@@ -89,7 +89,7 @@ const setGroup = System.create({ target: "entity", components: { with: [group] }
89
89
  // Readers.
90
90
  let found: number[] = [];
91
91
  const onMain = System.create({ components: { with: [at], changed: [value] } }, row => { found.push(row[at]); });
92
- const observed = System.create({ observer: true, components: { with: [at], changed: [value] } }, row => { found.push(row[at]); });
92
+ const observed = System.create({ readonly: true, components: { with: [at], changed: [value] } }, row => { found.push(row[at]); });
93
93
  const ofTwo = System.create({ components: { with: [at], changed: [group, value] } }, row => { found.push(row[at]); });
94
94
  const inKernel = System.create({ kernel: true, components: { with: [seen], changed: [value] } }, row => { row[seen] += 1; });
95
95
  const inUp = System.create({ components: { with: [at], changed: [value] }, up: { through: parent } }, row => { found.push(row[at]); });
@@ -1,8 +1,12 @@
1
1
  // © 2026 Adobe. MIT License. See /LICENSE for details.
2
2
  import type { Commands } from '../../interface/system/commands.js';
3
- import { addCommand, despawnCommand, pushCommand, pushComponent, removeCommand, type CommandBuffer } from './buffer.js';
3
+ import { addCommand, createBuffer, despawnCommand, pushCommand, pushComponent, removeCommand, spawnCommand, type CommandBuffer } from './buffer.js';
4
+ import { reserve } from './entities.js';
4
5
  import { listShapeOf, presetSpawnerOf, spawnerOf, type ListShape } from './lists.js';
5
6
  import { commandIdOf, idOf, storedShared, type Key, type State } from './state.js';
7
+ import { spawnTables } from './tables.js';
8
+ import { declarationOf, isWorldWritten, kindOf, labelOf, threadOfDeclaration, whyUnregistered } from '../../interface/registry/index.js';
9
+ import { matchesSchema } from '../../interface/registry/schemas.js';
6
10
 
7
11
  const collect = (state: State, buffer: CommandBuffer, key: Key, value: unknown): void => {
8
12
  if (value !== undefined) pushComponent(buffer, idOf(state, key), value);
@@ -44,6 +48,38 @@ const addShared = (state: State, buffer: CommandBuffer, entity: number, shape: L
44
48
  for (let k = 0; k < keys.length; k++) pushComponent(buffer, ids[k], values[k]);
45
49
  };
46
50
 
51
+ /**
52
+ * Throws for a key of `resources` that isn't a resource, is one only the world writes or one living on another thread,
53
+ * or is given a value its schema doesn't take.
54
+ */
55
+ const checkResources = (resources: object): void => {
56
+ for (const key of Object.getOwnPropertySymbols(resources)) {
57
+ const fail = (message: string): never => { throw new Error(`commands.spawnTables: "${labelOf(key)}" ${message}.`); };
58
+ if (kindOf(key) !== "resource") fail(`isn't a registered resource: ${whyUnregistered(key)}`);
59
+ if (isWorldWritten(key)) fail("is written only by the world");
60
+ const declaration = declarationOf(key);
61
+ if (declaration === undefined) continue;
62
+ const thread = threadOfDeclaration(declaration);
63
+ if (thread !== null && thread !== "world") fail(`lives on thread "${thread}", not the world's`);
64
+ if (declaration.type !== undefined && !matchesSchema(declaration.type, Reflect.get(resources, key))) fail("is given a value its type doesn't take");
65
+ }
66
+ };
67
+
68
+ /**
69
+ * Queues `resources`' values for the sync point as the world's own change to its resource entity, which it reserves
70
+ * when the world has none yet: a buffer of their own, since the sync point skips systems' commands to that entity.
71
+ */
72
+ const queueResources = (state: State, resources: object): void => {
73
+ if (Object.getOwnPropertySymbols(resources).length === 0) return;
74
+ const buffer = createBuffer();
75
+ if (state.resourceEntity < 0) {
76
+ state.resourceEntity = reserve(state.entities);
77
+ pushValues(state, buffer, spawnCommand, state.resourceEntity, resources);
78
+ }
79
+ else pushValues(state, buffer, addCommand, state.resourceEntity, resources);
80
+ state.queued.push({ buffer, guarded: false });
81
+ };
82
+
47
83
  /** Values of a list add given none, which only an untyped caller does. */
48
84
  const empty = {};
49
85
 
@@ -83,6 +119,13 @@ export const createCommands = (state: State, target: { readonly buffer: CommandB
83
119
  pushComponent(buffer, ids[k], value === undefined ? starts[k] : value);
84
120
  }
85
121
  },
122
+ spawnTables: (tables, resources) => {
123
+ if (resources === undefined) return spawnTables(state, tables);
124
+ checkResources(resources);
125
+ const spawned = spawnTables(state, tables);
126
+ queueResources(state, resources);
127
+ return spawned;
128
+ },
86
129
  remove: (entity: number, components: symbol | readonly symbol[]) => {
87
130
  const { buffer } = target;
88
131
  if (typeof components === "symbol") {
@@ -46,7 +46,7 @@ import { createState, defaultOf, idOf, reside, startTicking, type Key } from './
46
46
  import { indexOf } from './entities.js';
47
47
  import { touchedHeld } from './up.js';
48
48
  import { isHeld } from './transfers.js';
49
- import type { ReadonlyValue as ReadonlyResourceValue } from '../../interface/resource/value.js';
49
+ import type { ReadonlyType as ReadonlyResourceType } from '../../interface/resource/value.js';
50
50
  import { labelOf, whyUnregistered } from '../../interface/registry/index.js';
51
51
 
52
52
  type Queue = <T>(task: () => T | Promise<T>) => Promise<T>;
@@ -425,12 +425,12 @@ export const createWorld = (resources: InitialResources = {}, options: WorldOpti
425
425
  const deviceId = idOf(state, gpuDevice);
426
426
  const deviceDefault = defaultOf(gpuDevice);
427
427
  /** The `gpuDevice` resource's value, read each run, so without allocating. */
428
- const readDevice = (): ReadonlyResourceValue<typeof gpuDevice> => {
428
+ const readDevice = (): ReadonlyResourceType<typeof gpuDevice> => {
429
429
  const index = state.resourceEntity < 0 ? -1 : indexOf(state.resourceEntity);
430
430
  const storage = index < 0 ? undefined : state.archetypes[state.entities.archetype[index]]?.storageById[deviceId];
431
431
  const value = storage !== undefined ? readCell(storage, state.entities.row[index]) : state.initial.has(gpuDevice) ? state.initial.get(gpuDevice) : deviceDefault;
432
432
  // The resource's value matches its registered schema, `GPUDevice | null`.
433
- return value as ReadonlyResourceValue<typeof gpuDevice>;
433
+ return value as ReadonlyResourceType<typeof gpuDevice>;
434
434
  };
435
435
 
436
436
  /**
@@ -56,7 +56,7 @@ describe("a derived component declared without a default", () => {
56
56
  commands.spawn([tagged])({});
57
57
  });
58
58
  const seen: unknown[] = [];
59
- const read = System.create({ observer: true, components: { with: [index] } }, row => { seen.push(row[index]); });
59
+ const read = System.create({ readonly: true, components: { with: [index] } }, row => { seen.push(row[index]); });
60
60
  await using first = createWorld();
61
61
  await using second = createWorld();
62
62
  for (const world of [first, second]) {
@@ -70,6 +70,12 @@ export const createEntities = (memory: Memory): Entities => {
70
70
  /** Reserves an entity. Safe to call from any thread between sync points. */
71
71
  export const reserve = ({ cursor, free, generation }: Reservations): Entity => entityAt(cursor, free, generation, Atomics.add(cursor, taken, 1));
72
72
 
73
+ /** Reserves an entity for each of `into`'s elements, with one atomic add. Safe to call from any thread between sync points. */
74
+ export const reserveRun = ({ cursor, free, generation }: Reservations, into: Float64Array): void => {
75
+ const first = Atomics.add(cursor, taken, into.length);
76
+ for (let i = 0; i < into.length; i++) into[i] = entityAt(cursor, free, generation, first + i);
77
+ };
78
+
73
79
  /**
74
80
  * Reservations a thread takes in blocks of positions, so threads spawning together don't contend for the cursor.
75
81
  * Positions a block leaves unused are reserved entities that never spawn, which the sync point releases.
@@ -137,7 +137,7 @@ describe("finalizers", () => {
137
137
  const reads = System.create({}, ({ resources }) => { void resources[level]; });
138
138
  // Read after every run, as a read through a run of its own would apply the commands left for it.
139
139
  let seen: string | null = null;
140
- const watch = System.create({ observer: true, lookup: { labels: { with: [label] } } }, ({ lookup }) => {
140
+ const watch = System.create({ readonly: true, lookup: { labels: { with: [label] } } }, ({ lookup }) => {
141
141
  seen = lookup.labels(other)?.[label] ?? null;
142
142
  });
143
143
  const world = createWorld({ [workers]: 0 });
@@ -12,7 +12,7 @@ import { asEntity, indexOf, locate } from './entities.js';
12
12
  import { createContext, rowBinder, type Context } from './rows.js';
13
13
  import { idOf, type Key, type State } from './state.js';
14
14
  import type { Queued } from './synchronize.js';
15
- import type { Essential, ReadonlyValue } from '../../interface/component/value.js';
15
+ import type { Essential, ReadonlyType } from '../../interface/component/value.js';
16
16
  import type { Leaf } from '../../interface/finalizer/created.js';
17
17
  import { newestKeyOf, threadOf } from '../../interface/registry/index.js';
18
18
  import type { Layout } from '../../interface/registry/layout.js';
@@ -369,8 +369,8 @@ const changesOf = (state: State, runner: FinalizerRunner, queued: readonly Queue
369
369
  return {
370
370
  groups: [...groups.values()],
371
371
  // Stored and commanded values match each component's registered schema, so they have its type.
372
- previous: <K extends Essential>(entity: Entity, key: K) => (bornHere(entity) ? null : live(entity, idOf(state, key))) as ReadonlyValue<K> | null,
373
- next: <K extends Essential>(entity: Entity, key: K) => next(entity, idOf(state, key)) as ReadonlyValue<K> | null,
372
+ previous: <K extends Essential>(entity: Entity, key: K) => (bornHere(entity) ? null : live(entity, idOf(state, key))) as ReadonlyType<K> | null,
373
+ next: <K extends Essential>(entity: Entity, key: K) => next(entity, idOf(state, key)) as ReadonlyType<K> | null,
374
374
  };
375
375
  };
376
376
 
@@ -109,7 +109,7 @@ describe("an index lookup's changed: true", () => {
109
109
  await using world = inline();
110
110
  spawned = [];
111
111
  const orders: string[] = [];
112
- const ordering = System.create({ observer: true, lookup: { inOrder: { index: inOrder, changed: true } } }, ({ lookup }) => {
112
+ const ordering = System.create({ readonly: true, lookup: { inOrder: { index: inOrder, changed: true } } }, ({ lookup }) => {
113
113
  const all = lookup.inOrder();
114
114
  const groups: number[] = [];
115
115
  for (let i = 0; i < all.length; i++) groups.push(all.at(i).entity === spawned[0] ? -1 : 0);
@@ -205,7 +205,7 @@ describe("an index lookup's changed: true", () => {
205
205
  await using world = inline();
206
206
  spawned = [];
207
207
  const totals: number[] = [];
208
- const readTotals = System.create({ observer: true, components: { with: [total] } }, row => {
208
+ const readTotals = System.create({ readonly: true, components: { with: [total] } }, row => {
209
209
  totals.push(row[total]);
210
210
  });
211
211
  const totalsOf = async (): Promise<number[]> => {
@@ -87,7 +87,7 @@ const stirBands = System.create({ components: { with: [serial, band] } }, (row,
87
87
  /** An observer system of the joined thread's labels: the serials of each reader's band, in entity order, by reader. */
88
88
  let observedBands: string[] = [];
89
89
  const observeBands = System.create({
90
- observer: true,
90
+ readonly: true,
91
91
  components: { with: [target, bandReader, label] },
92
92
  lookup: { inBand: { index: bands } },
93
93
  }, (row, { lookup }) => {
@@ -219,14 +219,14 @@ const inOrder = System.create({ components: { with: [rank, label] }, order: rank
219
219
  const countWhenMoved = System.create({ components: { with: [cell, label] }, lookup: { near: { index: grid, changed: true } } }, (row, { lookup }) => {
220
220
  row[label] = { text: String(lookup.near(row[cell]).length) };
221
221
  });
222
- const readWhenMoved = System.create({ observer: true, components: { with: [label] }, lookup: { near: { index: grid, changed: true } } }, row => {
222
+ const readWhenMoved = System.create({ readonly: true, components: { with: [label] }, lookup: { near: { index: grid, changed: true } } }, row => {
223
223
  void row[label];
224
224
  });
225
- const ownWhenMoved = System.create({ observer: true, lookup: { near: { index: grid, changed: true } } }, () => {});
225
+ const ownWhenMoved = System.create({ readonly: true, lookup: { near: { index: grid, changed: true } } }, () => {});
226
226
 
227
227
  /** The entities `readLabels` reads, and their labels. */
228
228
  const reading: { entities: readonly Entity[]; labels: readonly string[] } = { entities: [], labels: [] };
229
- const readLabels = System.create({ observer: true, lookup: { labeled: { with: [label] } } }, ({ lookup }) => {
229
+ const readLabels = System.create({ readonly: true, lookup: { labeled: { with: [label] } } }, ({ lookup }) => {
230
230
  reading.labels = reading.entities.map(entity => lookup.labeled(entity)?.[label].text ?? "missing");
231
231
  });
232
232
 
@@ -279,7 +279,7 @@ const heldSerials = Index.create({ key: [serial], unique: true, components: { wi
279
279
  let observedHeld = "";
280
280
  /** An observer system reading every held row through the keyless index, and serial 3's through the unique one. */
281
281
  const observeHeld = System.create({
282
- observer: true,
282
+ readonly: true,
283
283
  lookup: { all: { index: held }, bySerial: { index: heldSerials } },
284
284
  }, ({ lookup }) => {
285
285
  const all = lookup.all();
@@ -72,7 +72,7 @@ const expected = async (world: CreatedWorld, only?: (weighed: number) => boolean
72
72
  };
73
73
 
74
74
  const markerValues: number[] = [];
75
- const readMarkers = System.create({ observer: true, components: { with: [marker] } }, row => { markerValues.push(row[marker]); });
75
+ const readMarkers = System.create({ readonly: true, components: { with: [marker] } }, row => { markerValues.push(row[marker]); });
76
76
 
77
77
  /** Markers' values in the order they were spawned: their archetype's rows', in storage order. */
78
78
  const markers = async (world: CreatedWorld): Promise<number[]> => {