@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
@@ -0,0 +1,267 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import { describe, expect, it } from "vitest";
3
+ import { clock, Component, Entity, Resource, System, workers } from "../../index.js";
4
+ import type { Vec3 } from "../../interface/math/index.js";
5
+ import type { Tables } from "../../interface/system/tables.js";
6
+ import type { Table } from "../../interface/world/table/table.js";
7
+ import { createWorld } from "./create.js";
8
+
9
+ const position = Symbol("tablesTest/position");
10
+ const label = Symbol("tablesTest/label");
11
+ const tags = Symbol("tablesTest/tags");
12
+ const team = Symbol("tablesTest/team");
13
+ const pinned = Symbol("tablesTest/pinned");
14
+ const parent = Symbol("tablesTest/parent");
15
+ const target = Symbol("tablesTest/target");
16
+ const link = Symbol("tablesTest/link");
17
+ const friends = Symbol("tablesTest/friends");
18
+ const hidden = Symbol("tablesTest/hidden");
19
+ const buddy = Symbol("tablesTest/buddy");
20
+ const score = Symbol("tablesTest/score");
21
+ const title = Symbol("tablesTest/title");
22
+ declare module "@adobe/ecs" {
23
+ interface Components {
24
+ [position]: { type: Vec3 };
25
+ [label]: { type: string };
26
+ [tags]: { type: readonly string[] };
27
+ [team]: { type: "red" | "blue"; shared: true };
28
+ [pinned]: { type: true };
29
+ [parent]: { type: Entity; reference: "owner" };
30
+ [target]: { type: Entity; reference: "weak" };
31
+ [link]: { type: { readonly to: Entity; readonly weight: number } };
32
+ [friends]: { type: readonly Entity[] };
33
+ [hidden]: { type: true };
34
+ [buddy]: { type: Entity };
35
+ }
36
+ interface Resources {
37
+ [score]: { type: number; default: 0 };
38
+ [title]: { type: string; default: "" };
39
+ }
40
+ }
41
+
42
+ const inline = () => createWorld({ [workers]: 0 });
43
+
44
+ /** A readonly system's tables of these components. */
45
+ const tablesOf = (components: readonly Component.Key[], resources: readonly Resource.Key[] = [], not: readonly Component.Key[] = []) =>
46
+ System.create({ readonly: true, tables: { components, resources, not } }, ({ tables }): Tables => tables);
47
+
48
+ const spawnedBy: { spawned: Table.Spawned } = { spawned: { entities: [], dropped: 0 } };
49
+ const given: { tables: readonly Table[]; resources?: { readonly [key: symbol]: unknown } } = { tables: [] };
50
+ const spawnGiven = System.create(({ commands }) => {
51
+ spawnedBy.spawned = given.resources === undefined ? commands.spawnTables(given.tables) : commands.spawnTables(given.tables, given.resources);
52
+ });
53
+ const spawnTables = async (world: ReturnType<typeof inline>, tables: readonly Table[]): Promise<Table.Spawned> => {
54
+ given.tables = tables;
55
+ await world.run(spawnGiven);
56
+ return spawnedBy.spawned;
57
+ };
58
+
59
+ const values = System.create({ readonly: true, components: { with: [label], optional: [position, parent, target, link, friends, tags, buddy] } }, row => {
60
+ found.push({ entity: row.entity, label: row[label], position: row[position], parent: row[parent], target: row[target], link: row[link], friends: row[friends], tags: row[tags], buddy: row[buddy] });
61
+ });
62
+ type Found = { entity: Entity; label: string; position?: Vec3; parent?: Entity; target?: Entity; link?: { readonly to: Entity; readonly weight: number }; friends?: readonly Entity[]; tags?: readonly string[]; buddy?: Entity };
63
+ const found: Found[] = [];
64
+ const readAll = async (world: ReturnType<typeof inline>): Promise<Found[]> => {
65
+ found.length = 0;
66
+ await world.run(values);
67
+ return [...found].sort((a, b) => a.label.localeCompare(b.label));
68
+ };
69
+
70
+ describe("tables", () => {
71
+ it("give each archetype holding a component asked for, its entities, and a copy of each column in the world's layout", async () => {
72
+ await using world = inline();
73
+ await world.run(System.create(({ commands }) => {
74
+ commands.spawn([position, label])({ [position]: [1, 2, 3], [label]: "a" });
75
+ commands.spawn([position, label, tags])({ [position]: [4, 5, 6], [label]: "b", [tags]: ["x", "y"] });
76
+ commands.spawn([team, label], { [team]: "red" })({ [label]: "c" });
77
+ commands.spawn([pinned])();
78
+ }));
79
+ const { archetypes } = await world.run(tablesOf([position, label, tags, team, pinned]));
80
+ expect(archetypes.map(table => table.count)).toEqual([1, 1, 1, 1]);
81
+ const plain = archetypes.find(table => table.columns[tags] === undefined && table.columns[position] !== undefined);
82
+ const bytes = plain?.columns[position];
83
+ expect(bytes?.kind === "bytes" && [...new Float32Array(bytes.bytes.buffer, bytes.bytes.byteOffset, 3)]).toEqual([1, 2, 3]);
84
+ expect(bytes?.kind === "bytes" && bytes.stride).toBe(12);
85
+ const withTags = archetypes.find(table => table.columns[tags] !== undefined);
86
+ expect(withTags?.columns[tags]).toEqual({ kind: "values", values: [["x", "y"]] });
87
+ expect(withTags?.columns[label]).toEqual({ kind: "values", values: ["b"] });
88
+ const shared = archetypes.find(table => table.columns[team] !== undefined);
89
+ expect(shared?.columns[team]).toEqual({ kind: "value", value: "red" });
90
+ expect(archetypes.find(table => table.columns[pinned] !== undefined)?.columns[pinned]).toEqual({ kind: "value", value: true });
91
+ });
92
+
93
+ it("leave out archetypes not matching with and not, and give the values of resources asked for", async () => {
94
+ await using world = createWorld({ [workers]: 0, [score]: 7, [title]: "won" });
95
+ await world.run(System.create(({ commands }) => {
96
+ commands.spawn([label])({ [label]: "shown" });
97
+ commands.spawn([label, hidden])({ [label]: "hidden" });
98
+ }));
99
+ const { archetypes, resources } = await world.run(tablesOf([label], [score, title], [hidden]));
100
+ expect(archetypes.map(table => table.columns[label])).toEqual([{ kind: "values", values: ["shown"] }]);
101
+ expect(resources[score]).toBe(7);
102
+ expect(resources[title]).toBe("won");
103
+ });
104
+
105
+ it("spawn back as they were read, shared values and strings included", async () => {
106
+ await using from = inline();
107
+ await from.run(System.create(({ commands }) => {
108
+ commands.spawn([position, label, tags])({ [position]: [1, 2, 3], [label]: "a", [tags]: ["t"] });
109
+ commands.spawn([team, label], { [team]: "blue" })({ [label]: "b" });
110
+ }));
111
+ const { archetypes } = await from.run(tablesOf([position, label, tags, team]));
112
+ await using into = inline();
113
+ const { entities, dropped } = await spawnTables(into, archetypes);
114
+ expect([entities.map(list => list.length), dropped]).toEqual([[1, 1], 0]);
115
+ const again = await into.run(tablesOf([position, label, tags, team]));
116
+ expect(again.archetypes.map(table => ({ ...table, entities: [] }))).toEqual(archetypes.map(table => ({ ...table, entities: [] })));
117
+ expect(await readAll(into)).toMatchObject([{ label: "a", position: [1, 2, 3], tags: ["t"] }, { label: "b" }]);
118
+ });
119
+
120
+ it("rewrite entities in references, struct fields and arrays to those spawned for them, self references and cycles included", async () => {
121
+ await using from = inline();
122
+ await from.run(System.create(({ commands }) => {
123
+ const root = commands.spawn([label])({ [label]: "root" });
124
+ const child = commands.spawn([label, parent])({ [label]: "child", [parent]: root });
125
+ commands.spawn([label, target, link, friends])({ [label]: "other", [target]: child, [link]: { to: root, weight: 2 }, [friends]: [root, child] });
126
+ const a = commands.spawn([label, link])({ [label]: "self", [link]: { to: Entity.none, weight: 1 } });
127
+ commands.add(a, link, { to: a, weight: 1 });
128
+ }));
129
+ const before = await readAll(from);
130
+ const { archetypes } = await from.run(tablesOf([label, parent, target, link, friends]));
131
+ await using into = inline();
132
+ // Entities already in the world, so the new ones differ from the old.
133
+ await into.run(System.create(({ commands }) => { for (let i = 0; i < 5; i++) commands.spawn([pinned])(); }));
134
+ await spawnTables(into, archetypes);
135
+ const after = await readAll(into);
136
+ const old = new Map(before.map(entry => [entry.entity, entry.label]));
137
+ const now = new Map(after.map(entry => [entry.label, entry.entity]));
138
+ const translated = (entity: Entity | undefined): Entity | undefined => entity === undefined ? undefined : now.get(old.get(entity) ?? "");
139
+ expect(after.map(entry => entry.label)).toEqual(before.map(entry => entry.label));
140
+ for (const entry of before) {
141
+ const moved = after.find(other => other.label === entry.label);
142
+ expect(moved?.entity).not.toBe(entry.entity);
143
+ expect(moved?.parent).toBe(translated(entry.parent));
144
+ expect(moved?.target).toBe(translated(entry.target));
145
+ expect(moved?.link?.to).toBe(translated(entry.link?.to));
146
+ expect(moved?.friends).toEqual(entry.friends?.map(translated));
147
+ }
148
+ expect(after.find(entry => entry.label === "self")?.link?.to).toBe(now.get("self"));
149
+ });
150
+
151
+ it("leave out a weak reference to an entity outside the tables, its holder spawned without it", async () => {
152
+ await using world = inline();
153
+ await world.run(System.create(({ commands }) => {
154
+ const outside = commands.spawn([pinned])();
155
+ const inside = commands.spawn([label])({ [label]: "inside" });
156
+ commands.spawn([label, target])({ [label]: "away", [target]: outside });
157
+ commands.spawn([label, target])({ [label]: "near", [target]: inside });
158
+ }));
159
+ const { archetypes } = await world.run(tablesOf([label, target]));
160
+ await using other = inline();
161
+ expect((await spawnTables(other, archetypes)).dropped).toBe(0);
162
+ const read = await readAll(other);
163
+ expect(read.map(entry => [entry.label, entry.target === undefined ? "none" : read.find(found => found.entity === entry.target)?.label])).toEqual([["away", "none"], ["inside", "none"], ["near", "inside"]]);
164
+ });
165
+
166
+ it("spawn no row owned, directly or through others, by an entity outside the tables, saying how many", async () => {
167
+ await using world = inline();
168
+ await world.run(System.create(({ commands }) => {
169
+ const outside = commands.spawn([pinned])();
170
+ const top = commands.spawn([label, parent])({ [label]: "top", [parent]: outside });
171
+ const middle = commands.spawn([label, parent])({ [label]: "middle", [parent]: top });
172
+ commands.spawn([label, parent])({ [label]: "bottom", [parent]: middle });
173
+ const kept = commands.spawn([label])({ [label]: "kept" });
174
+ commands.spawn([label, parent, target])({ [label]: "kept child", [parent]: kept, [target]: middle });
175
+ }));
176
+ const { archetypes } = await world.run(tablesOf([label, parent, target]));
177
+ await using other = inline();
178
+ const { entities, dropped } = await spawnTables(other, archetypes);
179
+ expect(dropped).toBe(3);
180
+ expect(entities.flatMap(list => [...list]).filter(entity => entity === Entity.none).length).toBe(3);
181
+ const read = await readAll(other);
182
+ // The kept child's weak target was dropped, so it's spawned without it.
183
+ expect(read.map(entry => [entry.label, entry.parent === undefined ? null : read.find(found => found.entity === entry.parent)?.label, entry.target ?? null])).toEqual([["kept", null, null], ["kept child", "kept", null]]);
184
+ });
185
+
186
+ it("make any other entity value naming an entity outside the tables none: a whole component, a struct's field and an array's item", async () => {
187
+ await using world = inline();
188
+ await world.run(System.create(({ commands }) => {
189
+ const outside = commands.spawn([pinned])();
190
+ const inside = commands.spawn([label])({ [label]: "inside" });
191
+ commands.spawn([label, buddy, link, friends])({ [label]: "pointing", [buddy]: outside, [link]: { to: outside, weight: 1 }, [friends]: [outside, inside] });
192
+ }));
193
+ const { archetypes } = await world.run(tablesOf([label, buddy, link, friends]));
194
+ await using other = inline();
195
+ await spawnTables(other, archetypes);
196
+ const read = await readAll(other);
197
+ const inside = read.find(entry => entry.label === "inside")?.entity;
198
+ expect(read.find(entry => entry.label === "pointing")).toMatchObject({ buddy: Entity.none, link: { to: Entity.none, weight: 1 }, friends: [Entity.none, inside] });
199
+ });
200
+
201
+ it("set the resources they're given at the sync point, beside the entities", async () => {
202
+ await using world = createWorld({ [workers]: 0, [score]: 7 });
203
+ given.tables = [];
204
+ given.resources = { [score]: 12, [title]: "loaded" };
205
+ try {
206
+ await world.run(spawnGiven);
207
+ }
208
+ finally {
209
+ given.resources = undefined;
210
+ }
211
+ const { resources } = await world.run(tablesOf([label], [score, title]));
212
+ expect(resources[score]).toBe(12);
213
+ expect(resources[title]).toBe("loaded");
214
+ });
215
+
216
+ it("refuse a resource only the world writes or a value its type doesn't take, spawning nothing", async () => {
217
+ await using world = inline();
218
+ const table: Table = { count: 1, entities: new Float64Array([1]), columns: { [label]: { kind: "values", values: ["kept out"] } } };
219
+ given.tables = [table];
220
+ try {
221
+ given.resources = { [clock]: { now: 0, delta: 0, tick: 0 } };
222
+ await expect(world.run(spawnGiven)).rejects.toThrow(/clock" is written only by the world/);
223
+ given.resources = { [score]: "twelve" };
224
+ await expect(world.run(spawnGiven)).rejects.toThrow(/score" is given a value its type doesn't take/);
225
+ given.resources = { [position]: [0, 0, 0] };
226
+ await expect(world.run(spawnGiven)).rejects.toThrow(/position" isn't a registered resource/);
227
+ }
228
+ finally {
229
+ given.resources = undefined;
230
+ }
231
+ expect(await readAll(world)).toEqual([]);
232
+ expect((await world.run(tablesOf([label], [score]))).resources[score]).toBe(0);
233
+ });
234
+
235
+ it("fail a column that doesn't fit its component's layout or its table's count", async () => {
236
+ await using world = inline();
237
+ const table = (columns: Table["columns"], count = 1): Table => ({ count, entities: new Float64Array(count).fill(1), columns });
238
+ given.tables = [table({ [position]: { kind: "bytes", bytes: new Uint8Array(8), stride: 8 } })];
239
+ await expect(world.run(spawnGiven)).rejects.toThrow(/stride of 12 bytes in this world, not 8/);
240
+ given.tables = [table({ [position]: { kind: "bytes", bytes: new Uint8Array(24), stride: 12 } })];
241
+ await expect(world.run(spawnGiven)).rejects.toThrow(/gives 24 bytes for 1 rows of 12/);
242
+ given.tables = [table({ [label]: { kind: "values", values: ["a", "b"] } })];
243
+ await expect(world.run(spawnGiven)).rejects.toThrow(/gives 2 values for 1 rows/);
244
+ given.tables = [table({ [team]: { kind: "values", values: ["red"] } })];
245
+ await expect(world.run(spawnGiven)).rejects.toThrow(/shared component, so its column is one value/);
246
+ // Two rows of one entity can't both be what a reference to it names.
247
+ given.tables = [table({ [friends]: { kind: "values", values: [[]] } }, 1), table({ [friends]: { kind: "values", values: [[]] } }, 1)];
248
+ await expect(world.run(spawnGiven)).rejects.toThrow(/entity 1 is in two rows/);
249
+ expect(await readAll(world)).toEqual([]);
250
+ });
251
+
252
+ it("are read only by a readonly system without components", () => {
253
+ // @ts-expect-error: tables are read only by a readonly system, which the declaration's type says too.
254
+ expect(() => System.create({ tables: { components: [label] } }, () => {})).toThrow(/reads tables, which only a readonly system reads/);
255
+ });
256
+ });
257
+
258
+ describe("declarations", () => {
259
+ it("give every component and resource registered, with its schema and traits", () => {
260
+ const declared = Component.declarations().get(parent);
261
+ expect(declared?.type).toEqual({ type: "integer", format: "entity" });
262
+ expect(declared?.reference).toBe("owner");
263
+ expect(Resource.declarations().get(score)?.default).toBe(0);
264
+ const components: readonly symbol[] = [...Component.declarations().keys()];
265
+ expect(components.includes(score)).toBe(false);
266
+ });
267
+ });
@@ -0,0 +1,330 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import type { Layout, Path } from '../../interface/registry/layout.js';
3
+ import { declarationOf, labelOf } from '../../interface/registry/names.js';
4
+ import { isEntity } from '../../interface/registry/schemas.js';
5
+ import type { Schema } from '../../interface/schema/schema.js';
6
+ import type { Table } from '../../interface/world/table/table.js';
7
+ import { batchOf } from './batches.js';
8
+ import { indexOf, reserveRun } from './entities.js';
9
+ import { internShared } from './shared.js';
10
+ import { claimRows, openRows, writerOf, type Column as BatchColumn } from './staging.js';
11
+ import { commandIdOf, type Key, type State } from './state.js';
12
+
13
+ /** Whether a value of `schema` may hold an entity anywhere in it. */
14
+ const holdsEntity = (schema: Schema): boolean => {
15
+ if (isEntity(schema)) return true;
16
+ if ("anyOf" in schema) return schema.anyOf.some(holdsEntity);
17
+ if ("allOf" in schema) return schema.allOf.some(holdsEntity);
18
+ if (!("type" in schema)) return false;
19
+ switch (schema.type) {
20
+ case "array": case "set": return holdsEntity(schema.items);
21
+ case "map": return holdsEntity(schema.keys) || holdsEntity(schema.values);
22
+ case "object": return Object.values(schema.properties).some(holdsEntity);
23
+ default: return false;
24
+ }
25
+ };
26
+
27
+ /** The schema at `path` inside `schema`, through struct fields and tuple items. */
28
+ const schemaAt = (schema: Schema, path: Path): Schema | undefined => {
29
+ let at: Schema | undefined = schema;
30
+ for (const step of path) {
31
+ if (at === undefined || !("type" in at)) return undefined;
32
+ if (at.type === "object" && typeof step === "string") at = at.properties[step];
33
+ else if (at.type === "array" && typeof step === "number") at = at.items;
34
+ else return undefined;
35
+ }
36
+ return at;
37
+ };
38
+
39
+ /** The f64 leaves of a linear layout holding entities: where, in f64 elements, each row's is. */
40
+ const entityLeavesOf = (layout: Layout, schema: Schema | undefined): readonly { readonly unitStride: number; readonly unitOffset: number }[] => {
41
+ if (layout.kind !== "linear" || schema === undefined) return [];
42
+ return layout.leaves.filter(leaf => {
43
+ const leafSchema = schemaAt(schema, leaf.path);
44
+ return leaf.view === "f64" && leafSchema !== undefined && isEntity(leafSchema);
45
+ });
46
+ };
47
+
48
+ /** The entity each entity of the tables given is spawned as, and what a value naming none of them becomes. */
49
+ type Remap = (entity: number) => number;
50
+
51
+ /** `value` of `schema` with each entity in it replaced by `remap`'s. */
52
+ const remapValue = (schema: Schema, value: unknown, remap: Remap): unknown => {
53
+ if (isEntity(schema)) return typeof value === "number" ? remap(value) : value;
54
+ if (!holdsEntity(schema)) return value;
55
+ if ("anyOf" in schema) {
56
+ for (const member of schema.anyOf) {
57
+ if (isEntity(member) && typeof value === "number") return remap(value);
58
+ if (!("type" in member) || !holdsEntity(member)) continue;
59
+ if ((member.type === "array" && Array.isArray(value)) || (member.type === "set" && value instanceof Set) || (member.type === "map" && value instanceof Map)
60
+ || (member.type === "object" && typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Set) && !(value instanceof Map))) return remapValue(member, value, remap);
61
+ }
62
+ return value;
63
+ }
64
+ if ("allOf" in schema) return schema.allOf.reduce<unknown>((next, member) => remapValue(member, next, remap), value);
65
+ if (!("type" in schema)) return value;
66
+ switch (schema.type) {
67
+ case "array": return Array.isArray(value) ? value.map(item => remapValue(schema.items, item, remap)) : value;
68
+ case "set": return value instanceof Set ? new Set(Array.from(value, item => remapValue(schema.items, item, remap))) : value;
69
+ case "map": return value instanceof Map ? new Map(Array.from(value, ([key, item]) => [remapValue(schema.keys, key, remap), remapValue(schema.values, item, remap)])) : value;
70
+ case "object": {
71
+ if (typeof value !== "object" || value === null) return value;
72
+ const copy: { [name: string]: unknown } = {};
73
+ for (const name of Object.keys(value)) {
74
+ const field = schema.properties[name];
75
+ const item: unknown = Reflect.get(value, name);
76
+ copy[name] = field === undefined ? item : remapValue(field, item, remap);
77
+ }
78
+ return copy;
79
+ }
80
+ default: return value;
81
+ }
82
+ };
83
+
84
+ /** A table's components with their ids, the columns given for them, and their layouts. */
85
+ type Planned = {
86
+ readonly table: Table;
87
+ readonly keys: readonly Key[];
88
+ readonly ids: readonly number[];
89
+ readonly shared: (number | undefined)[];
90
+ };
91
+
92
+ /** Throws, naming the component, for a column that doesn't fit its component's layout or its table's count. */
93
+ const checkColumn = (state: State, key: Key, id: number, column: Table.Column, count: number): void => {
94
+ const layout = state.layouts[id];
95
+ const fail = (message: string): never => { throw new Error(`commands.spawnTables: "${labelOf(key)}" ${message}.`); };
96
+ if (state.shared[id] !== undefined) {
97
+ if (column.kind !== "value") fail("is a shared component, so its column is one value");
98
+ return;
99
+ }
100
+ if (layout.kind === "constant" || column.kind === "value") return;
101
+ if (layout.kind === "linear") {
102
+ if (column.kind !== "bytes") fail("is stored as bytes, so its column gives bytes");
103
+ else if (column.stride !== layout.stride) fail(`has a stride of ${layout.stride} bytes in this world, not ${column.stride}`);
104
+ else if (column.bytes.length !== count * layout.stride) fail(`gives ${column.bytes.length} bytes for ${count} rows of ${layout.stride}`);
105
+ return;
106
+ }
107
+ if (column.kind !== "values") fail("is stored as values, so its column gives values");
108
+ else if (column.values.length !== count) fail(`gives ${column.values.length} values for ${count} rows`);
109
+ };
110
+
111
+ /** The plan of each table: its keys, their command ids, and the numbers of its shared values. Throws for one that won't spawn. */
112
+ const planOf = (state: State, table: Table): Planned => {
113
+ const { count, entities, columns } = table;
114
+ if (!Number.isInteger(count) || count < 0 || entities.length !== count) throw new Error(`commands.spawnTables: a table of ${count} rows gives ${entities.length} entities.`);
115
+ const keys = Object.getOwnPropertySymbols(columns).filter(key => Reflect.get(columns, key) !== undefined);
116
+ const ids = keys.map(key => commandIdOf(state, key));
117
+ const shared: (number | undefined)[] = [];
118
+ keys.forEach((key, k) => {
119
+ const column: Table.Column = Reflect.get(columns, key);
120
+ checkColumn(state, key, ids[k], column, count);
121
+ const values = state.shared[ids[k]];
122
+ if (values !== undefined && column.kind === "value") {
123
+ const number = internShared(values, column.value);
124
+ if (number < 0) throw new Error(`commands.spawnTables: "${labelOf(key)}", a shared component, isn't given one of its type's values: ${labelOf(column.value)}.`);
125
+ shared[ids[k]] = number;
126
+ }
127
+ });
128
+ return { table, keys, ids, shared };
129
+ };
130
+
131
+ /**
132
+ * Where each entity of the tables is, by its index: the entity itself, to tell it from another of that index, and its
133
+ * position among all the tables' rows. Throws for an entity two rows name.
134
+ */
135
+ const placesOf = (plans: readonly Planned[]): { readonly entities: Float64Array; readonly positions: Int32Array } => {
136
+ let largest = 0;
137
+ for (const { table } of plans) for (let row = 0; row < table.count; row++) largest = Math.max(largest, indexOf(table.entities[row]));
138
+ const entities = new Float64Array(largest + 1).fill(-1);
139
+ const positions = new Int32Array(largest + 1);
140
+ let position = 0;
141
+ for (const { table } of plans) {
142
+ for (let row = 0; row < table.count; row++) {
143
+ const entity = table.entities[row];
144
+ const index = indexOf(entity);
145
+ if (entities[index] === entity) throw new Error(`commands.spawnTables: entity ${entity} is in two rows.`);
146
+ entities[index] = entity;
147
+ positions[index] = position++;
148
+ }
149
+ }
150
+ return { entities, positions };
151
+ };
152
+
153
+ /** A reference column's entity value in each row: the f64 at the row's start, a reference's whole value. */
154
+ const referencesOf = (column: Table.Column): ((row: number) => number) => {
155
+ if (column.kind !== "bytes") return () => 0;
156
+ const view = new DataView(column.bytes.buffer, column.bytes.byteOffset, column.bytes.byteLength);
157
+ const { stride } = column;
158
+ return row => view.getFloat64(row * stride, true);
159
+ };
160
+
161
+ /** Copies a table's `rows`, or all of them, of one column into rows `start` on of a batch's column, remapping entities. */
162
+ const copyColumn = (target: BatchColumn, column: Table.Column, schema: Schema | undefined, rows: readonly number[] | null, count: number, start: number, remap: Remap | null): void => {
163
+ const rowAt = (i: number): number => rows === null ? i : rows[i];
164
+ const remapped = remap !== null && schema !== undefined && holdsEntity(schema);
165
+ if (column.kind === "value") {
166
+ const write = writerOf(target, column.value);
167
+ const value = remapped ? remapValue(schema, column.value, remap) : column.value;
168
+ for (let i = 0; i < count; i++) write(start + i, value);
169
+ return;
170
+ }
171
+ if (column.kind === "bytes") {
172
+ const { stride } = target;
173
+ if (rows === null) target.views.u8.set(column.bytes, start * stride);
174
+ else for (let i = 0; i < count; i++) target.views.u8.set(column.bytes.subarray(rows[i] * stride, (rows[i] + 1) * stride), (start + i) * stride);
175
+ if (!remapped) return;
176
+ const { f64 } = target.views;
177
+ for (const leaf of entityLeavesOf(target.layout, schema)) {
178
+ for (let row = start, end = start + count; row < end; row++) {
179
+ const index = row * leaf.unitStride + leaf.unitOffset;
180
+ f64[index] = remap(f64[index]);
181
+ }
182
+ }
183
+ return;
184
+ }
185
+ if (target.remote !== null && column.values.some(value => value !== undefined && value !== null)) throw new Error(target.remote);
186
+ for (let i = 0; i < count; i++) {
187
+ const value = column.values[rowAt(i)];
188
+ target.values[start + i] = remapped ? remapValue(schema, value, remap) : value;
189
+ }
190
+ };
191
+
192
+ /** Spawns `rows` of a plan's table, or all of them, with the components it holds but `removed`, as `entities`. */
193
+ const spawnRows = (state: State, { table, keys, ids, shared }: Planned, rows: readonly number[] | null, removed: ReadonlySet<Key>, entities: Float64Array, remap: Remap | null): void => {
194
+ const kept = keys.flatMap((key, k) => removed.has(key) ? [] : [k]);
195
+ const batch = batchOf(state, kept.map(k => ids[k]), shared.length === 0 ? undefined : shared);
196
+ const start = openRows(batch, entities.length);
197
+ for (const k of kept) {
198
+ const target = batch.columns.find(column => column.id === ids[k]);
199
+ if (target === undefined || target.layout.kind === "constant") continue;
200
+ const column: Table.Column = Reflect.get(table.columns, keys[k]);
201
+ copyColumn(target, column, declarationOf(keys[k])?.type, rows, entities.length, start, remap);
202
+ }
203
+ claimRows(batch, entities);
204
+ };
205
+
206
+ /**
207
+ * Spawns an entity for each row of `tables` into the batches of their components, as references would leave them had
208
+ * the entities outside the tables despawned: a row owned, directly or through others, by an entity outside them isn't
209
+ * spawned; a weak reference to one is removed; and any other entity value naming one is `Entity.none`. Every entity
210
+ * value naming an entity spawned names the one spawned for it.
211
+ */
212
+ export const spawnTables = (state: State, tables: readonly Table[]): Table.Spawned => {
213
+ const plans = tables.map(table => planOf(state, table));
214
+ // Entities are mapped only where a value of the tables may hold one.
215
+ const remapping = plans.some(({ keys }) => keys.some(key => {
216
+ const schema = declarationOf(key)?.type;
217
+ return schema !== undefined && holdsEntity(schema);
218
+ }));
219
+ const none: ReadonlySet<Key> = new Set();
220
+ if (!remapping) {
221
+ const entities = plans.map(({ table }) => {
222
+ const spawned = new Float64Array(table.count);
223
+ reserveRun(state.entities, spawned);
224
+ return spawned;
225
+ });
226
+ plans.forEach((plan, t) => spawnRows(state, plan, null, none, entities[t], null));
227
+ return { entities, dropped: 0 };
228
+ }
229
+ const places = placesOf(plans);
230
+ const positionOf = (entity: number): number => {
231
+ const index = indexOf(entity);
232
+ return entity !== 0 && index < places.entities.length && places.entities[index] === entity ? places.positions[index] : -1;
233
+ };
234
+ const firsts: number[] = [];
235
+ let total = 0;
236
+ for (const { table } of plans) {
237
+ firsts.push(total);
238
+ total += table.count;
239
+ }
240
+ // A row owned by an entity outside the tables isn't spawned, nor anything it owns, as despawning its owner would.
241
+ const dropped = new Uint8Array(total);
242
+ /** Each owner reference between rows of the tables: the owned row's position, and its owner's. */
243
+ const ownedRows: number[] = [];
244
+ const ownerRows: number[] = [];
245
+ const waiting: number[] = [];
246
+ plans.forEach(({ table, keys }, t) => {
247
+ for (const key of keys) {
248
+ if (declarationOf(key)?.reference !== "owner") continue;
249
+ const ownerAt = referencesOf(Reflect.get(table.columns, key));
250
+ for (let row = 0; row < table.count; row++) {
251
+ const owner = ownerAt(row);
252
+ if (owner === 0) continue;
253
+ const at = positionOf(owner);
254
+ const position = firsts[t] + row;
255
+ if (at >= 0) {
256
+ ownedRows.push(position);
257
+ ownerRows.push(at);
258
+ }
259
+ else if (dropped[position] === 0) {
260
+ dropped[position] = 1;
261
+ waiting.push(position);
262
+ }
263
+ }
264
+ }
265
+ });
266
+ if (waiting.length > 0) {
267
+ const owned = new Map<number, number[]>();
268
+ for (let i = 0; i < ownedRows.length; i++) {
269
+ const list = owned.get(ownerRows[i]);
270
+ if (list === undefined) owned.set(ownerRows[i], [ownedRows[i]]);
271
+ else list.push(ownedRows[i]);
272
+ }
273
+ for (let next = waiting.pop(); next !== undefined; next = waiting.pop()) {
274
+ for (const child of owned.get(next) ?? []) {
275
+ if (dropped[child] !== 0) continue;
276
+ dropped[child] = 1;
277
+ waiting.push(child);
278
+ }
279
+ }
280
+ }
281
+ const spawnedAt = new Float64Array(total);
282
+ const entities = plans.map(({ table }, t) => {
283
+ const alive = [];
284
+ for (let row = 0; row < table.count; row++) if (dropped[firsts[t] + row] === 0) alive.push(row);
285
+ const reserved = new Float64Array(alive.length);
286
+ reserveRun(state.entities, reserved);
287
+ const spawned = new Float64Array(table.count);
288
+ alive.forEach((row, i) => {
289
+ spawned[row] = reserved[i];
290
+ spawnedAt[firsts[t] + row] = reserved[i];
291
+ });
292
+ return spawned;
293
+ });
294
+ const isSpawned = (entity: number): boolean => {
295
+ const at = positionOf(entity);
296
+ return at >= 0 && dropped[at] === 0;
297
+ };
298
+ const remap: Remap = entity => isSpawned(entity) ? spawnedAt[positionOf(entity)] : 0;
299
+ let droppedCount = 0;
300
+ plans.forEach((plan, t) => {
301
+ const { table, keys } = plan;
302
+ const first = firsts[t];
303
+ const weak = keys.filter(key => declarationOf(key)?.reference === "weak").map(key => ({ key, targetAt: referencesOf(Reflect.get(table.columns, key)) }));
304
+ const loses = (row: number): boolean => weak.some(({ targetAt }) => {
305
+ const target = targetAt(row);
306
+ return target !== 0 && !isSpawned(target);
307
+ });
308
+ let whole = true;
309
+ for (let row = 0; row < table.count && whole; row++) if (dropped[first + row] !== 0 || loses(row)) whole = false;
310
+ if (whole) return spawnRows(state, plan, null, none, entities[t], remap);
311
+ // Rows by the weak references they lose, whose targets aren't spawned: each set of components a batch of its own.
312
+ const groups = new Map<string, { readonly removed: Set<Key>; readonly rows: number[] }>();
313
+ for (let row = 0; row < table.count; row++) {
314
+ if (dropped[first + row] !== 0) {
315
+ droppedCount++;
316
+ continue;
317
+ }
318
+ const removed = weak.filter(({ targetAt }) => {
319
+ const target = targetAt(row);
320
+ return target !== 0 && !isSpawned(target);
321
+ }).map(({ key }) => key);
322
+ const signature = removed.map(key => weak.findIndex(entry => entry.key === key)).join(",");
323
+ let group = groups.get(signature);
324
+ if (group === undefined) groups.set(signature, group = { removed: new Set(removed), rows: [] });
325
+ group.rows.push(row);
326
+ }
327
+ for (const { removed, rows } of groups.values()) spawnRows(state, plan, rows, removed, Float64Array.from(rows, row => entities[t][row]), remap);
328
+ });
329
+ return { entities, dropped: droppedCount };
330
+ };
@@ -75,7 +75,7 @@ describe("tails", () => {
75
75
  const { world, remote } = await joined({ loop: driver }, { [always]: true });
76
76
  await world.run(seed, { count: 10 });
77
77
  const counters: number[] = [];
78
- const values = System.create({ observer: true }, ({ resources }) => { counters.push(resources[counter]); });
78
+ const values = System.create({ readonly: true }, ({ resources }) => { counters.push(resources[counter]); });
79
79
  world.add(ticked);
80
80
  world.add(values);
81
81
  await until(Observe.resource(world, counter), value => value >= 5);
@@ -24,8 +24,8 @@ const remove = System.create({ target: "entity", components: { with: [tagged] }
24
24
  });
25
25
  let amounts: number[] = [];
26
26
  let changed: number[] = [];
27
- const readAmounts = System.create({ observer: true, components: { with: [amount] } }, row => { amounts.push(row[amount]); });
28
- const watchChanged = System.create({ observer: true, components: { with: [amount], changed: [amount] } }, row => { changed.push(row[amount]); });
27
+ const readAmounts = System.create({ readonly: true, components: { with: [amount] } }, row => { amounts.push(row[amount]); });
28
+ const watchChanged = System.create({ readonly: true, components: { with: [amount], changed: [amount] } }, row => { changed.push(row[amount]); });
29
29
 
30
30
  const inline = () => createWorld({ [workers]: 0 });
31
31
  const amountsOf = async (world: ReturnType<typeof inline>): Promise<number[]> => {
@@ -69,7 +69,7 @@ const errorOf = (error: unknown): WorkerError =>
69
69
 
70
70
  const refuse = (): never => { throw new Error("An observer system issues no commands."); };
71
71
  /** An observer system's context has no commands, so these are never called. */
72
- const noCommands: Commands = { spawn: refuse, despawn: refuse, add: refuse, remove: refuse };
72
+ const noCommands: Commands = { spawn: refuse, despawn: refuse, add: refuse, remove: refuse, spawnTables: refuse };
73
73
  const noStamps: number[] = [];
74
74
 
75
75
  export const createThreadObserver = (host: ThreadObserverHost) => {
@@ -172,7 +172,7 @@ const reading: { entities: readonly Entity[]; objects: readonly { readonly [name
172
172
 
173
173
  /** Each entity's objects of the thread, as data, a note by its text: none for an entity that's dead or has no uid. */
174
174
  const readObjects = System.create({
175
- observer: true,
175
+ readonly: true,
176
176
  lookup: { ids: { with: [uid] }, captions: { with: [caption] }, notes: { with: [note] } },
177
177
  }, ({ lookup }) => {
178
178
  reading.objects = reading.entities.map(entity => {
@@ -322,9 +322,9 @@ export const paintReader = System.create(({ resources }) => () => resources[pain
322
322
  /** A composite of systems on the thread returning values, which no run resolves to. */
323
323
  export const paintsTogether = System.create([paintReader, paintLengthThere]);
324
324
  /** An observer system on the thread, as it reads the thread's paint: what a run of it resolves to. */
325
- export const paintObserved = System.create({ observer: true }, ({ resources }) => resources[paint].text);
325
+ export const paintObserved = System.create({ readonly: true }, ({ resources }) => resources[paint].text);
326
326
  /** An observer system on the thread, throwing there. */
327
- export const paintObservedFails = System.create({ observer: true }, ({ resources }) => {
327
+ export const paintObservedFails = System.create({ readonly: true }, ({ resources }) => {
328
328
  if (resources[paint].text !== "never") throw new Error("threadSystemsTest observer failure there");
329
329
  return 0;
330
330
  });