@adobe/ecs 0.2.1 → 0.2.4

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 (330) hide show
  1. package/README.md +2 -2
  2. package/dist/implementation/compiler/body-access.d.ts +18 -1
  3. package/dist/implementation/compiler/body-access.js +104 -15
  4. package/dist/implementation/compiler/body-access.js.map +1 -1
  5. package/dist/implementation/compiler/describe.d.ts +1 -1
  6. package/dist/implementation/compiler/describe.js +3 -3
  7. package/dist/implementation/compiler/describe.js.map +1 -1
  8. package/dist/implementation/compiler/estree.d.ts +5 -2
  9. package/dist/implementation/compiler/estree.js +8 -5
  10. package/dist/implementation/compiler/estree.js.map +1 -1
  11. package/dist/implementation/compiler/inferred.js +4 -2
  12. package/dist/implementation/compiler/inferred.js.map +1 -1
  13. package/dist/implementation/compiler/signature.js +1 -1
  14. package/dist/implementation/compiler/transform.js +6 -5
  15. package/dist/implementation/compiler/transform.js.map +1 -1
  16. package/dist/implementation/world/activate.js +14 -2
  17. package/dist/implementation/world/activate.js.map +1 -1
  18. package/dist/implementation/world/buffer.d.ts +7 -0
  19. package/dist/implementation/world/buffer.js +7 -0
  20. package/dist/implementation/world/buffer.js.map +1 -1
  21. package/dist/implementation/world/cascade.d.ts +10 -3
  22. package/dist/implementation/world/cascade.js +48 -23
  23. package/dist/implementation/world/cascade.js.map +1 -1
  24. package/dist/implementation/world/commands.js +86 -2
  25. package/dist/implementation/world/commands.js.map +1 -1
  26. package/dist/implementation/world/create.js +53 -2
  27. package/dist/implementation/world/create.js.map +1 -1
  28. package/dist/implementation/world/entities.d.ts +2 -0
  29. package/dist/implementation/world/entities.js +6 -0
  30. package/dist/implementation/world/entities.js.map +1 -1
  31. package/dist/implementation/world/execute.d.ts +4 -4
  32. package/dist/implementation/world/execute.js +132 -83
  33. package/dist/implementation/world/execute.js.map +1 -1
  34. package/dist/implementation/world/finalizers.d.ts +2 -0
  35. package/dist/implementation/world/finalizers.js +24 -3
  36. package/dist/implementation/world/finalizers.js.map +1 -1
  37. package/dist/implementation/world/join.js +22 -4
  38. package/dist/implementation/world/join.js.map +1 -1
  39. package/dist/implementation/world/register.js +2 -2
  40. package/dist/implementation/world/resource-changes.d.ts +18 -0
  41. package/dist/implementation/world/resource-changes.js +66 -0
  42. package/dist/implementation/world/resource-changes.js.map +1 -0
  43. package/dist/implementation/world/rows.d.ts +1 -1
  44. package/dist/implementation/world/rows.js +23 -13
  45. package/dist/implementation/world/rows.js.map +1 -1
  46. package/dist/implementation/world/run-changes.d.ts +38 -0
  47. package/dist/implementation/world/run-changes.js +138 -0
  48. package/dist/implementation/world/run-changes.js.map +1 -0
  49. package/dist/implementation/world/runner.js +19 -15
  50. package/dist/implementation/world/runner.js.map +1 -1
  51. package/dist/implementation/world/sharing.js +1 -1
  52. package/dist/implementation/world/staging.d.ts +4 -0
  53. package/dist/implementation/world/staging.js +15 -3
  54. package/dist/implementation/world/staging.js.map +1 -1
  55. package/dist/implementation/world/state.d.ts +13 -0
  56. package/dist/implementation/world/state.js +13 -1
  57. package/dist/implementation/world/state.js.map +1 -1
  58. package/dist/implementation/world/synchronize.js +72 -22
  59. package/dist/implementation/world/synchronize.js.map +1 -1
  60. package/dist/implementation/world/tables-read.d.ts +9 -0
  61. package/dist/implementation/world/tables-read.js +72 -0
  62. package/dist/implementation/world/tables-read.js.map +1 -0
  63. package/dist/implementation/world/tables.d.ts +10 -0
  64. package/dist/implementation/world/tables.js +401 -0
  65. package/dist/implementation/world/tables.js.map +1 -0
  66. package/dist/implementation/world/thread-observer.js +1 -1
  67. package/dist/implementation/world/thread-observer.js.map +1 -1
  68. package/dist/implementation/world/thread-systems.js +11 -6
  69. package/dist/implementation/world/thread-systems.js.map +1 -1
  70. package/dist/implementation/world/up.js +7 -1
  71. package/dist/implementation/world/up.js.map +1 -1
  72. package/dist/implementation/world/where.d.ts +8 -0
  73. package/dist/implementation/world/where.js +40 -11
  74. package/dist/implementation/world/where.js.map +1 -1
  75. package/dist/index.d.ts +8 -28
  76. package/dist/index.js +4 -9
  77. package/dist/index.js.map +1 -1
  78. package/dist/interface/component/declarations.d.ts +4 -0
  79. package/dist/interface/component/declarations.js +8 -0
  80. package/dist/interface/component/declarations.js.map +1 -0
  81. package/dist/interface/component/declared.d.ts +12 -0
  82. package/dist/interface/component/declared.js +2 -0
  83. package/dist/interface/component/declared.js.map +1 -0
  84. package/dist/interface/component/index.d.ts +2 -0
  85. package/dist/interface/component/index.js +1 -1
  86. package/dist/interface/component/index.js.map +1 -1
  87. package/dist/interface/finalizer/body.d.ts +18 -3
  88. package/dist/interface/finalizer/create.d.ts +7 -5
  89. package/dist/interface/finalizer/create.js +8 -5
  90. package/dist/interface/finalizer/create.js.map +1 -1
  91. package/dist/interface/finalizer/declaration.d.ts +4 -2
  92. package/dist/interface/indexes/declaration-of.d.ts +17 -0
  93. package/dist/interface/indexes/declaration-of.js +20 -0
  94. package/dist/interface/indexes/declaration-of.js.map +1 -0
  95. package/dist/interface/indexes/index.d.ts +1 -0
  96. package/dist/interface/indexes/index.js +1 -0
  97. package/dist/interface/indexes/index.js.map +1 -1
  98. package/dist/interface/registry/names.d.ts +2 -0
  99. package/dist/interface/registry/names.js +9 -0
  100. package/dist/interface/registry/names.js.map +1 -1
  101. package/dist/interface/registry/schedule.d.ts +31 -4
  102. package/dist/interface/registry/schedule.js +185 -24
  103. package/dist/interface/registry/schedule.js.map +1 -1
  104. package/dist/interface/resource/declarations.d.ts +4 -0
  105. package/dist/interface/resource/declarations.js +8 -0
  106. package/dist/interface/resource/declarations.js.map +1 -0
  107. package/dist/interface/resource/index.d.ts +2 -0
  108. package/dist/interface/resource/index.js +1 -1
  109. package/dist/interface/resource/index.js.map +1 -1
  110. package/dist/interface/system/access.d.ts +15 -0
  111. package/dist/interface/system/body.d.ts +11 -6
  112. package/dist/interface/system/check.d.ts +8 -1
  113. package/dist/interface/system/check.js +42 -12
  114. package/dist/interface/system/check.js.map +1 -1
  115. package/dist/interface/system/commands.d.ts +37 -0
  116. package/dist/interface/system/create-resolved.js +1 -1
  117. package/dist/interface/system/create-resolved.js.map +1 -1
  118. package/dist/interface/system/create.d.ts +31 -15
  119. package/dist/interface/system/create.js +8 -7
  120. package/dist/interface/system/create.js.map +1 -1
  121. package/dist/interface/system/declaration.d.ts +9 -2
  122. package/dist/interface/system/describe.d.ts +120 -0
  123. package/dist/interface/system/describe.js +116 -0
  124. package/dist/interface/system/describe.js.map +1 -0
  125. package/dist/interface/system/index.d.ts +1 -0
  126. package/dist/interface/system/index.js +1 -0
  127. package/dist/interface/system/index.js.map +1 -1
  128. package/dist/interface/system/leaf.d.ts +6 -0
  129. package/dist/interface/system/render.js +1 -0
  130. package/dist/interface/system/render.js.map +1 -1
  131. package/dist/interface/system/resolve.d.ts +5 -1
  132. package/dist/interface/system/resolve.js +19 -1
  133. package/dist/interface/system/resolve.js.map +1 -1
  134. package/dist/interface/system/shared-only.d.ts +7 -0
  135. package/dist/interface/system/shared-only.js +2 -0
  136. package/dist/interface/system/shared-only.js.map +1 -0
  137. package/dist/interface/system/tables.d.ts +28 -0
  138. package/dist/interface/system/tables.js +2 -0
  139. package/dist/interface/system/tables.js.map +1 -0
  140. package/dist/interface/world/index.d.ts +1 -1
  141. package/dist/interface/world/table/index.d.ts +27 -0
  142. package/dist/interface/world/table/index.js +3 -0
  143. package/dist/interface/world/table/index.js.map +1 -0
  144. package/dist/interface/world/table/table.d.ts +11 -0
  145. package/dist/interface/world/table/table.js +2 -0
  146. package/dist/interface/world/table/table.js.map +1 -0
  147. package/dist/interface/world/world.d.ts +25 -10
  148. package/dist/world.d.ts +27 -0
  149. package/dist/world.js +27 -0
  150. package/dist/world.js.map +1 -0
  151. package/package.json +11 -2
  152. package/src/exports.test.ts +1 -1
  153. package/src/guide/00-keys.ts +1 -1
  154. package/src/guide/01-resources.md +1 -1
  155. package/src/guide/01-resources.ts +1 -1
  156. package/src/guide/02-entities.ts +2 -2
  157. package/src/guide/03-systems.md +2 -2
  158. package/src/guide/03-systems.out +1 -1
  159. package/src/guide/03-systems.ts +3 -3
  160. package/src/guide/04-ticks.md +1 -1
  161. package/src/guide/04-ticks.ts +3 -3
  162. package/src/guide/05-scheduling.ts +1 -1
  163. package/src/guide/06-sync-points.md +18 -7
  164. package/src/guide/06-sync-points.out +1 -2
  165. package/src/guide/06-sync-points.ts +10 -6
  166. package/src/guide/07-change-filters.ts +2 -2
  167. package/src/guide/08-input.ts +1 -1
  168. package/src/guide/09-rules.ts +1 -1
  169. package/src/guide/11-traits.ts +3 -3
  170. package/src/guide/12-lookups.ts +2 -2
  171. package/src/guide/13-indexes.ts +4 -4
  172. package/src/guide/14-kernels.ts +1 -1
  173. package/src/guide/15-reductions.ts +1 -1
  174. package/src/guide/16-gpu.ts +1 -1
  175. package/src/guide/17-hierarchies.ts +1 -1
  176. package/src/guide/18-identity.out +1 -1
  177. package/src/guide/19-threads.md +4 -2
  178. package/src/guide/19-threads.ts +2 -2
  179. package/src/guide/20-shared.ts +3 -3
  180. package/src/guide/21-dom.md +2 -2
  181. package/src/guide/21-dom.ts +1 -1
  182. package/src/guide/22-canvas.md +1 -1
  183. package/src/guide/22-canvas.ts +1 -1
  184. package/src/guide/CLAUDE.md +1 -1
  185. package/src/guide/guide.test.ts +9 -154
  186. package/src/implementation/compiler/body-access.ts +102 -12
  187. package/src/implementation/compiler/describe.test-helper.ts +1 -1
  188. package/src/implementation/compiler/describe.test.ts +1 -1
  189. package/src/implementation/compiler/describe.ts +3 -3
  190. package/src/implementation/compiler/estree.ts +9 -6
  191. package/src/implementation/compiler/hygiene.test.ts +1 -1
  192. package/src/implementation/compiler/inferred.test.ts +1 -1
  193. package/src/implementation/compiler/inferred.ts +4 -2
  194. package/src/implementation/compiler/plugin.test.ts +2 -2
  195. package/src/implementation/compiler/signature.ts +1 -1
  196. package/src/implementation/compiler/transform.test.ts +43 -23
  197. package/src/implementation/compiler/transform.ts +6 -4
  198. package/src/implementation/world/activate.ts +11 -2
  199. package/src/implementation/world/batches.test.ts +3 -3
  200. package/src/implementation/world/buffer.ts +7 -0
  201. package/src/implementation/world/cascade.ts +46 -22
  202. package/src/implementation/world/change-blocks.test.ts +1 -1
  203. package/src/implementation/world/commands.ts +76 -2
  204. package/src/implementation/world/create.test.ts +8 -8
  205. package/src/implementation/world/create.ts +57 -2
  206. package/src/implementation/world/defaults.test.ts +1 -1
  207. package/src/implementation/world/despawn-where.test.ts +287 -0
  208. package/src/implementation/world/entities.ts +6 -0
  209. package/src/implementation/world/execute.ts +125 -78
  210. package/src/implementation/world/finalizers.test.ts +1 -1
  211. package/src/implementation/world/finalizers.ts +27 -4
  212. package/src/implementation/world/ids.test.ts +1 -1
  213. package/src/implementation/world/index-changed.test.ts +2 -2
  214. package/src/implementation/world/index-joined.test.ts +5 -5
  215. package/src/implementation/world/index-order.test.ts +1 -1
  216. package/src/implementation/world/indexes.test.ts +2 -1
  217. package/src/implementation/world/join.test.ts +23 -4
  218. package/src/implementation/world/join.ts +26 -4
  219. package/src/implementation/world/joined-observer.test.ts +1 -1
  220. package/src/implementation/world/joining-worker.fixture.ts +1 -1
  221. package/src/implementation/world/joining.fixture.ts +1 -1
  222. package/src/implementation/world/joining.test-helper.ts +1 -1
  223. package/src/implementation/world/kernel-spawns.test.ts +1 -1
  224. package/src/implementation/world/names.test.ts +1 -2
  225. package/src/implementation/world/observe-factories.test.ts +2 -2
  226. package/src/implementation/world/observe-ticks-cases.test.ts +1 -1
  227. package/src/implementation/world/observe.test-helper.ts +1 -1
  228. package/src/implementation/world/observer-contained-worker.fixture.ts +1 -1
  229. package/src/implementation/world/observer-contained.test-helper.ts +1 -1
  230. package/src/implementation/world/observer-contained.test.ts +2 -2
  231. package/src/implementation/world/observer-joined.test-helper.ts +4 -4
  232. package/src/implementation/world/observer-joined.test.ts +23 -15
  233. package/src/implementation/world/observer.browser.test.ts +2 -2
  234. package/src/implementation/world/observer.test.ts +8 -8
  235. package/src/implementation/world/optional-workers.fixture.ts +1 -1
  236. package/src/implementation/world/optional.test.ts +7 -7
  237. package/src/implementation/world/perf.md +3 -1
  238. package/src/implementation/world/reclaim.test.ts +3 -3
  239. package/src/implementation/world/register.ts +2 -2
  240. package/src/implementation/world/remote-objects.test-helper.ts +1 -1
  241. package/src/implementation/world/resource-changes.test.ts +61 -0
  242. package/src/implementation/world/resource-changes.ts +67 -0
  243. package/src/implementation/world/rows.ts +24 -13
  244. package/src/implementation/world/rules.test.ts +37 -0
  245. package/src/implementation/world/run-changes.test.ts +88 -0
  246. package/src/implementation/world/run-changes.ts +160 -0
  247. package/src/implementation/world/run-results.test.ts +10 -10
  248. package/src/implementation/world/runner.ts +20 -16
  249. package/src/implementation/world/shared-gpu.browser.test.ts +2 -2
  250. package/src/implementation/world/shared-kernels.test.ts +2 -2
  251. package/src/implementation/world/shared-workers.fixture.ts +2 -2
  252. package/src/implementation/world/shared.test.ts +10 -10
  253. package/src/implementation/world/sharing.ts +1 -1
  254. package/src/implementation/world/staging.ts +16 -3
  255. package/src/implementation/world/state.ts +19 -0
  256. package/src/implementation/world/sync-points.test.ts +135 -0
  257. package/src/implementation/world/synchronize.ts +65 -18
  258. package/src/implementation/world/tables-read.ts +70 -0
  259. package/src/implementation/world/tables.test.ts +369 -0
  260. package/src/implementation/world/tables.ts +368 -0
  261. package/src/implementation/world/tails.test.ts +1 -1
  262. package/src/implementation/world/target.test.ts +2 -2
  263. package/src/implementation/world/thread-observer.ts +1 -1
  264. package/src/implementation/world/thread-systems.test-helper.ts +3 -3
  265. package/src/implementation/world/thread-systems.ts +11 -6
  266. package/src/implementation/world/up.test.ts +13 -1
  267. package/src/implementation/world/up.ts +6 -1
  268. package/src/implementation/world/where.test.ts +4 -4
  269. package/src/implementation/world/where.ts +38 -10
  270. package/src/implementation/world/workers.fixture.ts +2 -2
  271. package/src/index.ts +4 -29
  272. package/src/interface/component/declarations.ts +11 -0
  273. package/src/interface/component/declared.ts +9 -0
  274. package/src/interface/component/index.ts +2 -0
  275. package/src/interface/finalizer/body.ts +13 -3
  276. package/src/interface/finalizer/create.test.ts +3 -1
  277. package/src/interface/finalizer/create.ts +12 -7
  278. package/src/interface/finalizer/declaration.ts +4 -2
  279. package/src/interface/index.type-test.ts +2 -2
  280. package/src/interface/indexes/create.test.ts +10 -0
  281. package/src/interface/indexes/declaration-of.ts +34 -0
  282. package/src/interface/indexes/index.ts +1 -0
  283. package/src/interface/indexes/index.type-test.ts +1 -1
  284. package/src/interface/registry/names.ts +8 -0
  285. package/src/interface/registry/schedule.ts +200 -27
  286. package/src/interface/resource/declarations.ts +11 -0
  287. package/src/interface/resource/index.ts +2 -0
  288. package/src/interface/system/access.ts +16 -0
  289. package/src/interface/system/body.ts +7 -5
  290. package/src/interface/system/check.ts +37 -10
  291. package/src/interface/system/commands.ts +33 -0
  292. package/src/interface/system/commands.type-test.ts +31 -0
  293. package/src/interface/system/create-resolved.ts +1 -1
  294. package/src/interface/system/create.test.ts +21 -13
  295. package/src/interface/system/create.ts +24 -15
  296. package/src/interface/system/declaration.ts +9 -2
  297. package/src/interface/system/describe.test.ts +79 -0
  298. package/src/interface/system/describe.ts +122 -0
  299. package/src/interface/system/index.ts +1 -0
  300. package/src/interface/system/leaf.ts +6 -0
  301. package/src/interface/system/lists.type-test.ts +1 -1
  302. package/src/interface/system/optional.type-test.ts +1 -1
  303. package/src/interface/system/render.ts +1 -0
  304. package/src/interface/system/resolve.ts +25 -2
  305. package/src/interface/system/shared-only.ts +5 -0
  306. package/src/interface/system/shared.type-test.ts +1 -1
  307. package/src/interface/system/system.type-test.ts +26 -20
  308. package/src/interface/system/tables.ts +27 -0
  309. package/src/interface/system/up.type-test.ts +3 -3
  310. package/src/interface/world/index.ts +2 -2
  311. package/src/interface/world/run.type-test.ts +14 -4
  312. package/src/interface/world/table/index.ts +20 -0
  313. package/src/interface/world/table/table.ts +12 -0
  314. package/src/interface/world/world.ts +23 -10
  315. package/src/observe/entity.ts +2 -2
  316. package/src/observe/resource.ts +2 -2
  317. package/src/observe/select.ts +4 -4
  318. package/src/observe/world-state.ts +3 -3
  319. package/src/perftest/benchmarks.ts +19 -1
  320. package/src/perftest/indexes.ts +3 -3
  321. package/src/perftest/loop.ts +1 -1
  322. package/src/perftest/observer.ts +4 -4
  323. package/src/perftest/reads.ts +2 -2
  324. package/src/perftest/remote.ts +5 -5
  325. package/src/perftest/strings.ts +1 -1
  326. package/src/perftest/structural.ts +20 -0
  327. package/src/perftest/suite.ts +1 -1
  328. package/src/perftest/tables.ts +107 -0
  329. package/src/world.test.ts +2 -2
  330. package/src/world.ts +32 -0
@@ -0,0 +1,122 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import { indexDefinitionOf } from '../registry/indexes.js';
3
+ import { labelOf } from '../registry/names.js';
4
+ import { requireDefinition } from '../registry/program.js';
5
+ import { isLeaf, placementsOf, scheduleOf, type Placement } from '../registry/schedule.js';
6
+ import type { Leaf } from './leaf.js';
7
+ import type { System } from './system.js';
8
+
9
+ type Key = symbol;
10
+
11
+ const none: readonly never[] = [];
12
+
13
+ /** A leaf as the system value it is. */
14
+ const asSystem = (leaf: Leaf): System<unknown, unknown> => leaf;
15
+
16
+ /** What the build inferred one system uses, and what placed it, as plain data. */
17
+ const describeLeaf = (leaf: Leaf, placements: readonly Placement[]) => {
18
+ const { components, resources, lookup, up, order } = leaf.declaration;
19
+ const { commands } = leaf;
20
+ return {
21
+ /** The system, the value its constructor returned. */
22
+ system: asSystem(leaf),
23
+ name: leaf.name,
24
+ components: {
25
+ read: [...components?.read ?? none, ...up === undefined ? none : [up.through]],
26
+ write: components?.write ?? none,
27
+ /** What it requires without reading: `with` members it doesn't use, `added` and `changed` filters' among them. */
28
+ has: components?.has ?? none,
29
+ optional: components?.optional ?? none,
30
+ not: components?.not ?? none,
31
+ added: components?.added ?? none,
32
+ changed: components?.changed ?? none,
33
+ /** What it reads of each row's parent, through `up`. */
34
+ parent: up?.read ?? none,
35
+ },
36
+ resources: {
37
+ read: resources?.read ?? none,
38
+ write: resources?.write ?? none,
39
+ reduce: resources?.reduce ?? none,
40
+ changed: resources?.changed ?? none,
41
+ is: resources?.is ?? none,
42
+ not: resources?.not ?? none,
43
+ },
44
+ lookups: Object.entries(lookup ?? {}).map(([name, used]) => ({
45
+ name,
46
+ /** The label of the index it looks up through, or null for a lookup by entity. */
47
+ index: "index" in used && used.index !== undefined ? indexDefinitionOf(used.index)?.name ?? null : null,
48
+ read: used.read ?? none,
49
+ })),
50
+ /** The label of the sorted index ordering its rows, or null. */
51
+ order: indexDefinitionOf(order)?.name ?? null,
52
+ /** What its commands name by kind, and whether it despawns; null where the build couldn't follow them. */
53
+ commands: commands === null ? null : {
54
+ spawn: commands?.spawn ?? none,
55
+ add: commands?.add ?? none,
56
+ remove: commands?.remove ?? none,
57
+ despawn: commands?.despawn === true,
58
+ },
59
+ /** Whether it takes its row whole (`components: { whole: true }`), every member written. */
60
+ whole: components?.whole === true,
61
+ /** The systems it follows directly, with what ordered it after each and the keys that need that order. */
62
+ after: placements.map(placement => ({ system: asSystem(placement.leaf), name: placement.leaf.name, because: placement.because, conflicts: placement.conflicts })),
63
+ };
64
+ };
65
+
66
+ type Described = ReturnType<typeof describeLeaf>;
67
+
68
+ const keys = (list: readonly Key[]): string => list.map(labelOf).join(" ");
69
+
70
+ /** A system's lines of text, after its name in a column of `width`. */
71
+ const linesOf = (system: Described, width: number): readonly string[] => {
72
+ const lines: string[] = [];
73
+ const add = (label: string, text: string): void => { if (text !== "") lines.push(`${label.padEnd(10)}${text}`); };
74
+ const { components, resources, commands } = system;
75
+ add("reads", keys(components.read));
76
+ add("writes", `${keys(components.write)}${system.whole ? " (its whole row)" : ""}`);
77
+ add("requires", [keys(components.has), components.not.length === 0 ? "" : `not ${keys(components.not)}`, components.optional.length === 0 ? "" : `optional ${keys(components.optional)}`].filter(text => text !== "").join(", "));
78
+ add("filters", [...components.added.map(key => `added ${labelOf(key)}`), ...components.changed.map(key => `changed ${labelOf(key)}`), ...resources.changed.map(key => `changed ${labelOf(key)}`)].join(", "));
79
+ add("parent", keys(components.parent));
80
+ add("resources", [resources.read.length === 0 ? "" : `reads ${keys(resources.read)}`, resources.write.length === 0 ? "" : `writes ${keys(resources.write)}`,
81
+ resources.reduce.length === 0 ? "" : `reduces ${keys(resources.reduce)}`, resources.is.length === 0 ? "" : `while ${keys(resources.is)}`,
82
+ resources.not.length === 0 ? "" : `unless ${keys(resources.not)}`].filter(text => text !== "").join(", "));
83
+ for (const lookup of system.lookups) add("looks up", `${lookup.name}${lookup.index === null ? "" : ` through ${lookup.index}`}${lookup.read.length === 0 ? "" : `: ${keys(lookup.read)}`}`);
84
+ if (system.order !== null) add("ordered", `by ${system.order}`);
85
+ if (commands === null) add("commands", "unknown: the build couldn't follow them");
86
+ else add("commands", [commands.spawn.length === 0 ? "" : `spawn ${keys(commands.spawn)}`, commands.add.length === 0 ? "" : `add ${keys(commands.add)}`,
87
+ commands.remove.length === 0 ? "" : `remove ${keys(commands.remove)}`, commands.despawn ? "despawn" : ""].filter(text => text !== "").join(", "));
88
+ add("after", system.after.map(placement => `${placement.name} (${placement.because}${placement.conflicts.length === 0 ? "" : `: ${keys(placement.conflicts)}`})`).join(", "));
89
+ const indent = " ".repeat(width);
90
+ return lines.length === 0 ? [system.name] : lines.map((line, i) => `${i === 0 ? system.name.padEnd(width) : indent}${line}`);
91
+ };
92
+
93
+ /**
94
+ * Describes how a system or composite runs: its tiers in run order, systems in one tier having no ordering path between
95
+ * them, each with the sync point after it, if any, and its readonly systems apart, which run after its last sync
96
+ * point. For each system, what the build inferred it reads, writes, looks up and commands, whether it takes its row
97
+ * whole, and the systems it follows directly, with what ordered it after each. `text` gives it all to read; the rest
98
+ * is data to assert on.
99
+ */
100
+ export const describe = <I, R>(system: System<I, R>) => {
101
+ const definition = requireDefinition(system);
102
+ const schedule = scheduleOf(definition);
103
+ const placements = placementsOf(definition);
104
+ const tiers = schedule.tiers.map((tier, t) => ({
105
+ systems: tier.map(leaf => describeLeaf(leaf, placements.get(leaf) ?? none)),
106
+ /**
107
+ * Why a sync point follows the tier, applying the commands so far: each system whose commands may change what a
108
+ * later one that follows it uses, through `keys`, none listed where it despawns or a cascade could; null for none.
109
+ */
110
+ sync: schedule.syncs[t]?.map(reason => ({ from: asSystem(reason.from), to: asSystem(reason.to), keys: reason.keys })) ?? null,
111
+ }));
112
+ const readonly = schedule.observer.flatMap(member => isLeaf(member) ? [describeLeaf(member, none)] : []);
113
+ const width = Math.max(0, ...[...tiers.flatMap(tier => tier.systems), ...readonly].map(described => described.name.length)) + 2;
114
+ const text = [
115
+ ...tiers.flatMap((tier, t) => [
116
+ ...tier.systems.flatMap((described, i) => linesOf(described, width).map((line, l) => `${(i === 0 && l === 0 ? `tier ${t}` : "").padEnd(8)}${line}`)),
117
+ ...(tier.sync ?? none).map((reason, r) => `${(r === 0 ? "sync" : "").padEnd(8)}${reason.from.name}'s commands${reason.keys.length === 0 ? "" : ` of ${keys(reason.keys)}`}, for ${reason.to.name}`),
118
+ ]),
119
+ ...readonly.length === 0 ? none : ["readonly", ...readonly.flatMap(described => linesOf(described, width).map(line => `${"".padEnd(8)}${line}`))],
120
+ ].map(line => line.trimEnd()).join("\n");
121
+ return { tiers, readonly, text };
122
+ };
@@ -1,4 +1,5 @@
1
1
  // © 2026 Adobe. MIT License. See /LICENSE for details.
2
2
  export type { Commands } from './commands.js';
3
3
  export * from './create.js';
4
+ export { describe } from './describe.js';
4
5
  export { render, type RenderContext, type RenderTable } from './render.js';
@@ -1,5 +1,6 @@
1
1
  // © 2026 Adobe. MIT License. See /LICENSE for details.
2
2
  import type { Schema } from '../schema/schema.js';
3
+ import type { CommandLists } from './access.js';
3
4
  import type { Resolved } from './declaration.js';
4
5
  import type { Name } from './name.js';
5
6
 
@@ -17,4 +18,9 @@ export type Leaf = {
17
18
  readonly declaration: Resolved;
18
19
  readonly input: InputSchema | undefined;
19
20
  readonly body: (...parameters: never[]) => void;
21
+ /**
22
+ * What its body's commands name, as the build inferred them: null where it couldn't follow them, undefined for
23
+ * none. Apart from its declaration, so a module evaluated again with other commands declares the same system.
24
+ */
25
+ readonly commands?: CommandLists | null;
20
26
  };
@@ -23,7 +23,7 @@ export const Particle = [position, velocity] as const;
23
23
  expectType<Equal<typeof Particle, readonly [typeof position, typeof velocity]>>();
24
24
  export const one: readonly Component.Key[] = Particle;
25
25
  export const move = System.create({ components: { with: [...Particle] } }, row => { row[position] += row[velocity]; });
26
- export const read = System.create({ observer: true, components: { with: [...Particle] } }, row => { void row[velocity]; });
26
+ export const read = System.create({ readonly: true, components: { with: [...Particle] } }, row => { void row[velocity]; });
27
27
 
28
28
  // Declared without `as const`, a list's keys widen to `symbol`, which no list of components takes.
29
29
  const loose = [position, velocity];
@@ -36,7 +36,7 @@ System.create({
36
36
  }, row => { void row[velocity]; });
37
37
 
38
38
  // An observer system reads its optional components and writes nothing.
39
- System.create({ observer: true, components: { with: [position], optional: [velocity] } }, row => {
39
+ System.create({ readonly: true, components: { with: [position], optional: [velocity] } }, row => {
40
40
  void row[velocity];
41
41
  // @ts-expect-error an observer system's row is read-only
42
42
  row[velocity] = 1;
@@ -96,6 +96,7 @@ export const render = <const D extends RenderDeclaration, B extends Body<D>>(
96
96
  },
97
97
  input: undefined,
98
98
  body,
99
+ commands: undefined,
99
100
  };
100
101
  if (uses.write.length > 0 || uses.reduce.length > 0) throw new Error(`${what} writes or reduces into resources; a render system only reads.`);
101
102
  checkLeaf(system);
@@ -3,7 +3,7 @@ import { takeAccessOf } from '../registry/ids.js';
3
3
  import { indexDefinitionOf } from '../registry/indexes.js';
4
4
  import { declarationOf, labelOf } from '../registry/names.js';
5
5
  import { isString, isStruct } from '../registry/schemas.js';
6
- import type { ComponentAccess, ComponentLists, Lookup, SystemLookup, SystemLookupLists } from './access.js';
6
+ import type { CommandLists, ComponentAccess, ComponentLists, Lookup, SystemLookup, SystemLookupLists } from './access.js';
7
7
 
8
8
  type Key = symbol;
9
9
 
@@ -23,11 +23,23 @@ export type Access = {
23
23
  readonly up: readonly Key[] | null;
24
24
  /** The resources the predicates of its declaration's `where` read; absent without predicates. */
25
25
  readonly where?: readonly Key[] | null;
26
+ /** What its commands name, keys or lists of them, by kind, and whether it despawns; absent where no build said. */
27
+ readonly commands?: unknown;
26
28
  readonly at: string;
27
29
  };
28
30
 
31
+ type Named = readonly (Key | readonly Key[])[];
32
+ type Commanded = { readonly spawn: Named; readonly add: Named; readonly remove: Named; readonly despawn: boolean };
33
+
29
34
  const isKeys = (value: unknown): value is readonly Key[] => Array.isArray(value) && value.every(key => typeof key === "symbol");
30
35
 
36
+ const isNamed = (value: unknown): value is Named => Array.isArray(value) && value.every(item => typeof item === "symbol" || isKeys(item));
37
+
38
+ /** A binding a command named that holds neither a key nor a list of them, as one declared below its system, isn't followed. */
39
+ const isCommanded = (value: unknown): value is Commanded => typeof value === "object" && value !== null
40
+ && isNamed(Reflect.get(value, "spawn")) && isNamed(Reflect.get(value, "add")) && isNamed(Reflect.get(value, "remove"))
41
+ && typeof Reflect.get(value, "despawn") === "boolean";
42
+
31
43
  const isUsed = (value: unknown): value is Used => typeof value === "object" && value !== null
32
44
  && isKeys(Reflect.get(value, "read")) && isKeys(Reflect.get(value, "write"))
33
45
  && (Reflect.get(value, "mutate") === undefined || isKeys(Reflect.get(value, "mutate")));
@@ -106,7 +118,7 @@ export const componentLists = (components: ComponentAccess, used: Used | null, r
106
118
  : used.write;
107
119
  const read = used === null ? without(held, write) : without(used.read, write);
108
120
  const has = [...without(members, read, write), ...without(filtered, read, write)];
109
- return compact({ read, write, has, optional, not: components.not, added: components.added, changed: components.changed, where, whereReads });
121
+ return compact({ read, write, has, optional, not: components.not, added: components.added, changed: components.changed, where, whereReads, whole: components.whole });
110
122
  };
111
123
 
112
124
  /**
@@ -161,5 +173,16 @@ export const whereReadsOf = (what: string, components: ComponentAccess | undefin
161
173
  return reads;
162
174
  };
163
175
 
176
+ /** What a body's commands name, as the world takes it: null where the build couldn't follow them, nothing for none. */
177
+ export const commandLists = (access: Access): CommandLists | null | undefined => {
178
+ const { commands } = access;
179
+ if (commands === undefined) return undefined;
180
+ if (!isCommanded(commands)) return null;
181
+ if (!commands.despawn && commands.spawn.length + commands.add.length + commands.remove.length === 0) return undefined;
182
+ const flat = (named: Named): Key[] => [...new Set(named.flat())];
183
+ const lists = compact({ spawn: flat(commands.spawn), add: flat(commands.add), remove: flat(commands.remove), despawn: commands.despawn ? true as const : undefined });
184
+ return Object.keys(lists).length === 0 ? undefined : lists;
185
+ };
186
+
164
187
  /** What a body reads of its parent's values. */
165
188
  export const upReads = (what: string, access: Access): readonly Key[] => followed(what, access.up, "its parent's values", access);
@@ -0,0 +1,5 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import type { Shared } from '../component/value.js';
3
+
4
+ /** A `where`'s keys that aren't shared components, each an error: `where` names only shared components. */
5
+ export type SharedOnly<W> = { readonly [K in Exclude<keyof W, Shared>]: { readonly "error: where names only shared components, which each archetype holds one value of": K } };
@@ -126,7 +126,7 @@ System.create({ components: { with: [health], optional: [team] } }, row => {
126
126
  // @ts-expect-error nor an optional one
127
127
  row[team] = "red";
128
128
  });
129
- System.create({ observer: true, components: { with: [team] } }, row => {
129
+ System.create({ readonly: true, components: { with: [team] } }, row => {
130
130
  check<Equal<typeof row[typeof team], "red" | "blue">>();
131
131
  });
132
132
 
@@ -367,7 +367,7 @@ System.render({ shader: { accessors: { place: position } } }, gpu => { void gpu.
367
367
 
368
368
  // Observer systems read what a sync point left, after it: rows, resources, lookups and change filters, and no commands.
369
369
  export const watchPositions = System.create({
370
- observer: true,
370
+ readonly: true,
371
371
  components: { with: [position], changed: [position] },
372
372
  lookup: { targets: { with: [velocity] } },
373
373
  }, (row, context) => {
@@ -379,33 +379,39 @@ export const watchPositions = System.create({
379
379
  void context.commands;
380
380
  });
381
381
  check<Equal<typeof watchPositions, System>>();
382
- export const watchTime = System.create({ observer: true, resources: { changed: [time] } }, ({ resources }) => { void resources[time]; });
382
+ export const watchTime = System.create({ readonly: true, resources: { changed: [time] } }, ({ resources }) => { void resources[time]; });
383
383
  // A composite may hold observer members, which run after its sync point.
384
384
  export const withWatch = System.create([integrate, watchTime]);
385
385
  check<Equal<typeof withWatch, System>>();
386
- // @ts-expect-error observer is true or left out
387
- System.create({ observer: false }, () => {});
388
- System.create({ observer: true, components: { with: [position] } }, (row, { resources }) => {
389
- // @ts-expect-error an observer system writes no components
386
+ // @ts-expect-error observer isn't a declaration's: readonly is
387
+ System.create({ observer: true }, () => {});
388
+ // @ts-expect-error readonly is true or left out
389
+ System.create({ readonly: false }, () => {});
390
+ System.create({ readonly: true, components: { with: [position] } }, (row, { resources }) => {
391
+ // @ts-expect-error a readonly system writes no components
390
392
  row[position] = 0;
391
- // @ts-expect-error an observer system writes no resources
393
+ // @ts-expect-error a readonly system writes no resources
392
394
  resources[score] = 0;
393
395
  });
394
- System.create({ observer: true }, context => {
395
- // @ts-expect-error an observer system reduces into nothing
396
+ System.create({ readonly: true }, context => {
397
+ // @ts-expect-error a readonly system reduces into nothing
396
398
  void context.reduce;
399
+ // @ts-expect-error a readonly system has no commands
400
+ void context.commands;
397
401
  });
398
- // @ts-expect-error an observer system takes no input
399
- System.create({ observer: true }, (_context, input: { readonly count: number }) => { void input; });
400
- // @ts-expect-error an observer system isn't a kernel
401
- System.create({ observer: true, kernel: true, components: { with: [position] } }, () => {});
402
- System.create({ observer: true, kernel: false, components: { with: [position] } }, () => {});
403
- // @ts-expect-error an observer system is ordered with nothing
404
- System.create({ observer: true, after: [integrate] }, () => {});
405
- // @ts-expect-error an observer system is ordered with nothing
406
- System.create({ observer: true, before: [integrate] }, () => {});
407
- // @ts-expect-error an observer system is ordered with nothing
408
- System.create({ observer: true, during: [integrate] }, () => {});
402
+ // @ts-expect-error a readonly system takes no input
403
+ System.create({ readonly: true }, (_context, input: { readonly count: number }) => { void input; });
404
+ // @ts-expect-error a readonly system isn't a kernel
405
+ System.create({ readonly: true, kernel: true, components: { with: [position] } }, () => {});
406
+ // @ts-expect-error a readonly system isn't a GPU kernel
407
+ System.create({ readonly: true, kernel: "gpu", components: { with: [position] } }, () => {});
408
+ System.create({ readonly: true, kernel: false, components: { with: [position] } }, () => {});
409
+ // @ts-expect-error a readonly system is ordered with nothing
410
+ System.create({ readonly: true, after: [integrate] }, () => {});
411
+ // @ts-expect-error a readonly system is ordered with nothing
412
+ System.create({ readonly: true, before: [integrate] }, () => {});
413
+ // @ts-expect-error a readonly system is ordered with nothing
414
+ System.create({ readonly: true, during: [integrate] }, () => {});
409
415
 
410
416
  // A system on one entity: its input names the entity, and its body runs on that entity's row.
411
417
  type OnOne = { readonly entity: Entity; readonly by: number };
@@ -0,0 +1,27 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import type { Component } from '../component/component.js';
3
+ import type { Shared } from '../component/value.js';
4
+ import type { Resource } from '../resource/resource.js';
5
+ import type { Table } from '../world/table/table.js';
6
+
7
+ /**
8
+ * What a readonly system's `tables` reads: of every archetype matching `with`, `not` and `where` that holds any of
9
+ * `components`, a table of those it holds; and, with `resources`, each resource's value.
10
+ */
11
+ export type TablesDeclaration = {
12
+ readonly components: readonly Component.Key[];
13
+ readonly with?: readonly Component.Key[];
14
+ readonly not?: readonly Component.Key[];
15
+ /**
16
+ * Keeps the tables to the archetypes holding these values of shared components, each one required as `with`
17
+ * requires it, as a system's `where` of values keeps its system to them.
18
+ */
19
+ readonly where?: { readonly [K in Shared]?: Component<K> };
20
+ readonly resources?: readonly Resource.Key[];
21
+ };
22
+
23
+ /** A readonly system's `tables`: a table of each archetype its declaration reads, and the values of the resources it reads. */
24
+ export type Tables = {
25
+ readonly archetypes: readonly Table[];
26
+ readonly resources: { readonly [K in Resource.Key]?: Resource<K> };
27
+ };
@@ -62,6 +62,6 @@ System.create({
62
62
  up: { through: parent },
63
63
  }, () => {});
64
64
 
65
- // An observer system reads what a sync point left, in no order.
66
- // @ts-expect-error an observer system has no up
67
- System.create({ observer: true, components: { with: [local] }, up: { through: parent } }, () => {});
65
+ // A readonly system reads what a sync point left, in no order.
66
+ // @ts-expect-error a readonly system has no up
67
+ System.create({ readonly: true, components: { with: [local] }, up: { through: parent } }, () => {});
@@ -1,3 +1,3 @@
1
1
  // © 2026 Adobe. MIT License. See /LICENSE for details.
2
- // The `World` namespace's members: none, since a consumer declares nothing more with it.
3
- export {};
2
+ // The `World` namespace's types.
3
+ export type { Table } from './table/table.js';
@@ -19,7 +19,7 @@ declare module '@adobe/ecs' {
19
19
  }
20
20
 
21
21
  // A run resolves to its per-run body's value: a one-off read is an observer system returning it.
22
- const count = System.create({ observer: true }, ({ resources }) => resources[frames]);
22
+ const count = System.create({ readonly: true }, ({ resources }) => resources[frames]);
23
23
  check<Equal<typeof count, System<undefined, number>>>();
24
24
  const counted: Promise<number> = world.run(count);
25
25
  // @ts-expect-error a run of it resolves to a number
@@ -32,7 +32,7 @@ const scaled = System.create({}, ({ resources }, input: { readonly by: number })
32
32
  const scaledValue: Promise<{ scaled: number }> = world.run(scaled, { by: 2 });
33
33
 
34
34
  // A body that may be skipped, as one whose conditions or resource change filters can fail, may give undefined.
35
- const changedFrames = System.create({ observer: true, resources: { changed: [frames] } }, ({ resources }) => resources[frames]);
35
+ const changedFrames = System.create({ readonly: true, resources: { changed: [frames] } }, ({ resources }) => resources[frames]);
36
36
  check<Equal<typeof changedFrames, System<undefined, number | undefined>>>();
37
37
  const whileDevice = System.create({ resources: { is: [gpuDevice] } }, () => "drawn");
38
38
  const drawn: Promise<string | undefined> = world.run(whileDevice);
@@ -62,13 +62,23 @@ void ecs.Report;
62
62
  // @ts-expect-error no run numbers
63
63
  const numbered: Promise<number> = world.run(main);
64
64
 
65
- void [counted, notText, nextValue, scaledValue, drawn, rows, composite, input, nothing, numbered, time];
65
+ // Asked for its changes, after its input, a run resolves to them, in the shape changes finalizers take.
66
+ const changes = world.run(move, { distance: 1 }, { changes: true });
67
+ const alone = world.run(main, { changes: true });
68
+ check<Equal<typeof changes, typeof alone>>();
69
+ check<Equal<Awaited<typeof alone>["resources"][number]["key"], Ecs.Resource.Key>>();
70
+ // @ts-expect-error its input comes first
71
+ void world.run(move, { changes: true }, { distance: 1 });
72
+ // @ts-expect-error a run asks only for its changes
73
+ void world.run(main, { changes: false });
74
+
75
+ void [counted, notText, nextValue, scaledValue, drawn, rows, composite, input, nothing, numbered, time, changes];
66
76
 
67
77
  // A body returns no promise: it starts the work, and a later run finishes it.
68
78
  // @ts-expect-error a body returns no promise
69
79
  System.create(() => Promise.resolve(1));
70
80
  // @ts-expect-error a body returns no promise
71
- System.create({ observer: true }, () => Promise.resolve(1));
81
+ System.create({ readonly: true }, () => Promise.resolve(1));
72
82
  // @ts-expect-error a body returns no promise, a row system's neither
73
83
  System.create({ components: { with: [position] } }, () => Promise.resolve());
74
84
  // @ts-expect-error a body returns no promise, a render system's neither
@@ -0,0 +1,20 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+
3
+ /**
4
+ * One component's values in a table, copies taken as a readonly system reads `tables`:
5
+ * - `bytes`: a struct's or number's, `stride` bytes per row in the world's layout for it, which is a function of the
6
+ * component's schema alone;
7
+ * - `values`: a string's or an object's, one per row;
8
+ * - `value`: one value every row holds, for a shared component or one whose schema is a constant.
9
+ */
10
+ export type Column =
11
+ | { readonly kind: "bytes"; readonly bytes: Uint8Array; readonly stride: number }
12
+ | { readonly kind: "values"; readonly values: readonly unknown[] }
13
+ | { readonly kind: "value"; readonly value: unknown };
14
+
15
+ /**
16
+ * What `commands.spawnTables` spawned: each table's entities in its row order, the one spawned for each row, the one
17
+ * existing for a row its `existing` maps, and `Entity.none` for a row it didn't spawn; and how many rows it didn't,
18
+ * those owned by an entity outside the tables.
19
+ */
20
+ export type Spawned = { readonly entities: readonly Float64Array[]; readonly dropped: number };
@@ -0,0 +1,12 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import type { Component } from '../../component/component.js';
3
+ import type { Column } from './index.js';
4
+
5
+ /** The rows of one archetype: their entities in row order, and a column for each component asked for that it holds. */
6
+ export type Table = {
7
+ readonly count: number;
8
+ readonly entities: Float64Array;
9
+ readonly columns: { readonly [K in Component.Key]?: Column };
10
+ };
11
+
12
+ export type * as Table from './index.js';
@@ -1,35 +1,48 @@
1
1
  // © 2026 Adobe. MIT License. See /LICENSE for details.
2
2
  import type { Input } from '../system/input.js';
3
3
  import type { System } from '../system/system.js';
4
+ import type { ChangeSet } from '../finalizer/body.js';
5
+ import type { Essential } from '../component/value.js';
6
+ import type { Resource } from '../resource/resource.js';
4
7
 
5
8
  /**
6
- * A world. Systems read and change it; observer systems read it after each run's and tick's sync point. Every member's
9
+ * A world. Systems read and change it; readonly systems read it after each run's and tick's sync point. Every member's
7
10
  * type is named from the package root, so declarations a consumer emits for its results name no internal module.
8
11
  */
9
12
  export type World = {
10
13
  /**
11
14
  * The one way to change state. Runs are sequential in call order; physically, any that don't conflict overlap. Each
12
- * runs a system or composite, ending in a sync point, then observer systems, the run's and those added, and
15
+ * runs a system or composite, ending in a sync point, then readonly systems, the run's and those added, and
13
16
  * resolves to what a per-run body returns: a system without components resolves to its body's value, or
14
17
  * `undefined` when its body was skipped, and any other system or composite to nothing. So a one-off read is a run
15
- * of an observer system returning what it read; one that runs on a joined thread, as it reads objects there, runs
16
- * there before the run resolves, to its value as structured clone. A run of observer systems alone runs them once
18
+ * of a readonly system returning what it read; one that runs on a joined thread, as it reads objects there, runs
19
+ * there before the run resolves, to its value as structured clone. A run of readonly systems alone runs them once
17
20
  * after an empty sync point, and doesn't wake the loop.
18
21
  *
19
22
  * When a system or finalizer of the run fails, later tiers are skipped, the sync point still applies what was done,
20
- * and once its observer systems ran the run rejects with an `Error` naming each failure, its system's label and id,
23
+ * and once its readonly systems ran the run rejects with an `Error` naming each failure, its system's label and id,
21
24
  * message and cause. It rejects too when the run can't happen or finish: registration fails, the world is disposed,
22
- * its GPU device is lost, or its sync point can't apply a command's value. What fails after it, an observer system
25
+ * its GPU device is lost, or its sync point can't apply a command's value. What fails after it, a readonly system
23
26
  * added or GPU work, is reported out of band, as ticks' failures are (see `add`).
24
27
  */
25
28
  run<I, R>(system: System<I, R>, ...input: Input<NoInfer<I>>): Promise<R>;
29
+ /**
30
+ * Runs as `run` does, and resolves to this run's own changes, in the shape `Finalizer.changes` gives, rather than its
31
+ * value: the structural changes its commands made, rules and cascades included, to every component that isn't
32
+ * derived or an object a joined thread fills, the values its adds gave components entities had before and after, and
33
+ * each resource its systems write or reduce into whose value changed, compared as `Finalizer.changes` compares them;
34
+ * `previous` and `next` give values before and after the run. Commands the finalizers of an earlier sync point
35
+ * queued apply first, at a sync point of their own. Values written to rows aren't structural changes, so they're in
36
+ * none. The run pays for this only when asked. Only on the world's thread: a joined thread's rejects.
37
+ */
38
+ run<I, R>(system: System<I, R>, ...input: [...Input<NoInfer<I>>, options: { readonly changes: true }]): Promise<ChangeSet<Essential, Resource.Key>>;
26
39
  /**
27
40
  * Adds a system to every tick, from the next one on, ordered with those already added by its constraints, as a
28
41
  * composite's members are. A tick runs on a beat of the `cadence` resource where `always` is set, `wake` is due or
29
42
  * a run arrived since the last tick, and only while some system is added. Throws for a system with no ordering path
30
- * to one already added that conflicts with it. An observer system runs after every run's and tick's sync point
43
+ * to one already added that conflicts with it. A readonly system runs after every run's and tick's sync point
31
44
  * instead. It never conflicts and doesn't count as an added system for ticking. On a joined thread `add` takes only
32
- * observer systems, whose keys need ids: each runs on that thread after the world's sync points, once for the
45
+ * readonly systems, whose keys need ids: each runs on that thread after the world's sync points, once for the
33
46
  * newest when the thread fell behind, reading structs and the objects of the threads it fills as of one sync point.
34
47
  *
35
48
  * What added systems throw, and what fails in ticks, is reported out of band on the world's thread, as an `Error`
@@ -43,14 +56,14 @@ export type World = {
43
56
  * released. A system added both alone and within an added composite stays until both are removed; constraints
44
57
  * naming a removed system are ignored. Removing a value that isn't added does nothing. Throws, removing nothing,
45
58
  * when the systems left would conflict with no ordering path between them, as `add` does. On a joined thread it
46
- * takes only observer systems.
59
+ * takes only readonly systems.
47
60
  */
48
61
  remove(system: System<void>): void;
49
62
  /**
50
63
  * Lets another thread join this world through `port`, one end of a channel to it, of the browser or node: a
51
64
  * `MessagePort`, a `Worker`, or a worker's own scope, `self` or node's `parentPort`. That thread joins through the
52
65
  * other end, claiming the logical threads it fills. Resolves once it has joined: the threads it claims are its own,
53
- * and the observer systems it added as its join resolved, before awaiting anything else, are added, so a run
66
+ * and the readonly systems it added as its join resolved, before awaiting anything else, are added, so a run
54
67
  * started afterwards runs on it and reaches them; one it adds later reaches the runs after its add arrived.
55
68
  * Rejects with an `Error` saying why it didn't join: the world refused its claim, as of a thread claimed twice, the
56
69
  * world was disposed, the port is shared already, or that thread ended first, as the platform reports it: a
@@ -129,14 +129,14 @@ const createShape = (state: WorldState, byShape: Map<string, EntityShape>, id: s
129
129
  let finders: Finders | null = null;
130
130
  let source: object | null = null;
131
131
  const system = optional.length === 0
132
- ? System.create(labelled({ observer: true, lookup: { presence: { with: required, changed: required }, values: { with: required }, members: { index: membersOf(required), changed: required } } }, name, { lookup: { values: required } }), ({ lookup }) => {
132
+ ? System.create(labelled({ readonly: true, lookup: { presence: { with: required, changed: required }, values: { with: required }, members: { index: membersOf(required), changed: required } } }, name, { lookup: { values: required } }), ({ lookup }) => {
133
133
  if (source !== lookup || finders === null) {
134
134
  source = lookup;
135
135
  finders = { presence: lookup.presence, values: lookup.values, optional: noOptional };
136
136
  }
137
137
  pass(lookup.members().changed, finders);
138
138
  })
139
- : System.create(labelled({ observer: true, lookup: optionalLookups(required, optional) }, name, { lookup: optionalReads(required, optional) }), ({ lookup }) => {
139
+ : System.create(labelled({ readonly: true, lookup: optionalLookups(required, optional) }, name, { lookup: optionalReads(required, optional) }), ({ lookup }) => {
140
140
  if (source !== lookup || finders === null) {
141
141
  source = lookup;
142
142
  finders = { presence: lookup.presence, values: lookup.values, optional: (index, entity) => lookup[index](entity) };
@@ -31,14 +31,14 @@ const createShape = (state: WorldState, byKey: Map<Resource.Key, ResourceShape>,
31
31
  const label = `Observe.resource(${key.description ?? "?"})`;
32
32
  // On a joined thread, first values come once a run of this system resolved, never in its pass: none to skip.
33
33
  const { joined } = state;
34
- const notifier = System.create(labelled({ observer: true, resources: { changed: [key] } }, label, { resources: [key] }), ({ resources }) => {
34
+ const notifier = System.create(labelled({ readonly: true, resources: { changed: [key] } }, label, { resources: [key] }), ({ resources }) => {
35
35
  passed(state);
36
36
  entry.value = resources[key];
37
37
  entry.valid = true;
38
38
  if (entry.stopped > 0) sweepSubscribers(entry);
39
39
  throwFailed(give(entry, 0, true, state.pass, joined ? -1 : state.pass, none));
40
40
  });
41
- const reader = System.create(labelled({ observer: true }, label, { resources: [key] }), ({ resources }) => {
41
+ const reader = System.create(labelled({ readonly: true }, label, { resources: [key] }), ({ resources }) => {
42
42
  const through = shape.called.shift() ?? 0;
43
43
  entry.value = resources[key];
44
44
  entry.valid = true;
@@ -403,16 +403,16 @@ const createShape = (state: WorldState, byShape: Map<string, QueryShape>, id: st
403
403
  };
404
404
 
405
405
  const tracker = ordered !== null
406
- ? System.create(labelled({ observer: true, lookup: { list: { index: ordered, changed: reads } } }, name, { lookup: { list: reads } }), ({ lookup }) => {
406
+ ? System.create(labelled({ readonly: true, lookup: { list: { index: ordered, changed: reads } } }, name, { lookup: { list: reads } }), ({ lookup }) => {
407
407
  track(lookup.list(), null, fromSorted);
408
408
  })
409
409
  : byRange !== null
410
- ? System.create(labelled({ observer: true, lookup: { list: { index: byRange, changed: reads } } }, name, { lookup: { list: reads } }), ({ lookup }) => {
410
+ ? System.create(labelled({ readonly: true, lookup: { list: { index: byRange, changed: reads } } }, name, { lookup: { list: reads } }), ({ lookup }) => {
411
411
  // A range is of a `where` condition on the index's key, which the query's type checked against its
412
412
  // values.
413
413
  track(null, entry => ownRange(entry, range => lookup.list(range as Parameters<typeof lookup.list>[0]), () => lookup.list()), fromSorted);
414
414
  })
415
- : System.create(labelled({ observer: true, lookup: { list: { index: keylessOf(has, not), changed: reads } } }, name, { lookup: { list: reads } }), ({ lookup }) => {
415
+ : System.create(labelled({ readonly: true, lookup: { list: { index: keylessOf(has, not), changed: reads } } }, name, { lookup: { list: reads } }), ({ lookup }) => {
416
416
  track(lookup.list(), null, fromKeyless);
417
417
  });
418
418
 
@@ -466,7 +466,7 @@ const createShape = (state: WorldState, byShape: Map<string, QueryShape>, id: st
466
466
  for (const entry of scan.entries) entry.gather.start();
467
467
  accept = acceptOf(scan.entries);
468
468
  };
469
- const scanner = System.create(labelled({ observer: true, components: { with: [...reads, ...required.filter(key => !reads.includes(key))], not } }, name, { components: reads }), row => {
469
+ const scanner = System.create(labelled({ readonly: true, components: { with: [...reads, ...required.filter(key => !reads.includes(key))], not } }, name, { components: reads }), row => {
470
470
  if (accept === null) start();
471
471
  if (direct === 0) {
472
472
  accept?.(row);
@@ -116,7 +116,7 @@ const ignore = (): void => {
116
116
  };
117
117
 
118
118
  /** Runs nothing: a run of it resolves while its world lives. */
119
- const alive = System.create(labelled({ observer: true }, "Observe/alive"), () => {});
119
+ const alive = System.create(labelled({ readonly: true }, "Observe/alive"), () => {});
120
120
 
121
121
  /**
122
122
  * What a rejected run of the factories' own does: nothing for a disposed world, whose first values never come, and for
@@ -232,7 +232,7 @@ export const stateOf = (world: World): WorldState => {
232
232
  deliveries: [],
233
233
  idle: [],
234
234
  rejected: rejectedOf(world),
235
- deliver: System.create(labelled({ observer: true }, "Observe/deliver"), () => {
235
+ deliver: System.create(labelled({ readonly: true }, "Observe/deliver"), () => {
236
236
  const delivery = created.deliveries.shift();
237
237
  if (delivery === undefined) return;
238
238
  created.delivered = delivery.batch;
@@ -243,7 +243,7 @@ export const stateOf = (world: World): WorldState => {
243
243
  states.set(world, created);
244
244
  if (joined) return created;
245
245
  // Added before any shape's system, so in each pass it counts the pass before they run.
246
- world.add(System.create(labelled({ observer: true }, "Observe/maintain"), () => {
246
+ world.add(System.create(labelled({ readonly: true }, "Observe/maintain"), () => {
247
247
  created.pass++;
248
248
  if (created.idle.length > 0) sweepShapes(created);
249
249
  }));