@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
@@ -149,8 +149,8 @@ const emitted = { seen: 0 };
149
149
  const countEmitted = System.create({ components: { added: [position], with: [spawned] } }, () => { emitted.seen++; });
150
150
 
151
151
  const gathered: Entity[] = [];
152
- const gatherPositioned = System.create({ observer: true, components: { with: [position] } }, row => { gathered.push(row.entity); });
153
- const gatherSpawned = System.create({ observer: true, components: { with: [spawned] } }, row => { gathered.push(row.entity); });
152
+ const gatherPositioned = System.create({ readonly: true, components: { with: [position] } }, row => { gathered.push(row.entity); });
153
+ const gatherSpawned = System.create({ readonly: true, components: { with: [spawned] } }, row => { gathered.push(row.entity); });
154
154
 
155
155
  /** The entities an observer row system passes, in storage order. */
156
156
  const storageOrder = async (world: World, gather: System<void>): Promise<Entity[]> => {
package/src/index.ts CHANGED
@@ -3,9 +3,6 @@
3
3
  * A high-performance, multithreaded Entity Component System.
4
4
  * @module @adobe/ecs
5
5
  */
6
- import { create, join } from './implementation/index.js';
7
- import type { Create } from './interface/world/create.js';
8
- import type { Join } from './interface/world/join.js';
9
6
  import type { World as Created } from './interface/world/world.js';
10
7
 
11
8
  export * from './interface/index.js';
@@ -15,30 +12,8 @@ export type World = Created;
15
12
 
16
13
  /**
17
14
  * How a world is made, `World.create(resources?)`, on the thread that will run it, with resources starting at the given
18
- * values in place of their defaults; and how another thread joins one that shared itself with it,
19
- * `World.join(port, { threads })`.
15
+ * values in place of their defaults; how another thread joins one that shared itself with it,
16
+ * `World.join(port, { threads })`; and `World.Table`, an archetype's rows as a readonly system's `tables` copies them
17
+ * and `commands.spawnTables` spawns them.
20
18
  */
21
- export const World: {
22
- /**
23
- * Creates a world on this thread, synchronously, with resources starting at the given values in place of their
24
- * defaults. This thread is its `"world"` thread for the world's life: it runs the sync points, the ticks and the GPU
25
- * device, and fills every logical thread no joined thread claims. Keys are the resources' symbols, built-in ones
26
- * included (`{ [workers]: 0 }`); string keys are never a resource's, so they're ignored. Throws for a symbol that isn't
27
- * a resource, or is one only the world writes. While a declaration's trait or default doesn't hold, each call fails to
28
- * compile, once, naming each such key, its members and what's wrong with them.
29
- */
30
- readonly create: Create;
31
- /**
32
- * Joins, from another thread, a world that `share` let join through the other end of `port`, claiming the logical
33
- * `threads` this thread fills: their objects live here, and the systems and finalizers reading them run here. Resolves,
34
- * once the world accepted the claim, to the same world: its `run` runs systems there, and its `add` and `remove` take
35
- * observer systems, which run here after the world's sync points. Add them before awaiting anything else, so the
36
- * world's `share` resolves with them in place. Its `dispose` disconnects this thread. Rejects with an `Error` saying
37
- * why it didn't join: the world refused the claim, was disposed, or its thread ended first, as the platform reports it:
38
- * the world's `Worker` failing to start or throwing first, a port closed, as in node, or, in a browser, the world's
39
- * thread ending once it was shared through a port this thread listened on. A browser world's worker that the page ended
40
- * before this thread heard from it isn't reported, so the join stays pending. Rejects too where memory can't be shared,
41
- * as without cross-origin isolation.
42
- */
43
- readonly join: Join;
44
- } = { create, join };
19
+ export * as World from './world.js';
@@ -0,0 +1,11 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import type { Declared } from './declared.js';
3
+ import { declaredOf } from '../registry/names.js';
4
+ import type { Key } from './key.js';
5
+
6
+ /** Every component its modules registered so far, with its declaration. */
7
+ export const declarations = (): ReadonlyMap<Key, Declared> => {
8
+ // Registration takes only keys declared in `Components`, so each symbol registered as a component is one of its keys.
9
+ const found = declaredOf("component") as ReadonlyMap<Key, Declared>;
10
+ return found;
11
+ };
@@ -0,0 +1,9 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import type { Schema } from '../schema/schema.js';
3
+ import type { Key as TraitKey } from '../trait/key.js';
4
+
5
+ /**
6
+ * A component's or resource's declaration as registered: its schema as `type` where its value is data, what it
7
+ * starts at as `default`, and its other traits, the built-in ones by name and those of your own by their keys.
8
+ */
9
+ export type Declared = { readonly type?: Schema; readonly default?: unknown } & { readonly [T in TraitKey]?: unknown };
@@ -1,3 +1,5 @@
1
1
  // © 2026 Adobe. MIT License. See /LICENSE for details.
2
2
  export type { Key } from './key.js';
3
3
  export type { With } from './value.js';
4
+ export { declarations } from './declarations.js';
5
+ export type { Declared } from './declared.js';
@@ -7,6 +7,8 @@ import type { LookupFunction } from '../system/values.js';
7
7
  import type { WriteResources } from '../system/body.js';
8
8
  import type { ChangesDeclaration, Declaration } from './declaration.js';
9
9
  import type { Essential, ReadonlyType } from '../component/value.js';
10
+ import type { Resource } from '../resource/resource.js';
11
+ import type { ReadonlyType as ResourceType } from '../resource/value.js';
10
12
  import type { Elsewhere, ThreadsOf } from '../system/threads.js';
11
13
 
12
14
  /**
@@ -29,7 +31,10 @@ export type Context<D extends Declaration | ChangesDeclaration> =
29
31
  & (true extends Elsewhere<ThreadsOf<D>> ? {} : { readonly commands: Commands });
30
32
 
31
33
  /** The components a changes finalizer sees. */
32
- export type Watched<D extends ChangesDeclaration> = Property<D, "watch"> extends readonly Essential[] ? Listed<D, "watch"> & Essential : Essential;
34
+ export type Watched<D extends ChangesDeclaration> = Property<D, "watch"> extends readonly unknown[] ? Listed<D, "watch"> & Essential : Essential;
35
+
36
+ /** The resources a changes finalizer sees: those it watches. */
37
+ export type WatchedResources<D extends ChangesDeclaration> = Listed<D, "watch"> & Resource.Key;
33
38
 
34
39
  /** Entities whose components changed the same way: `from` and `to` are null for those spawned and despawned. */
35
40
  export type Group<C extends Component.Key> = {
@@ -42,9 +47,14 @@ export type Group<C extends Component.Key> = {
42
47
  readonly entities: readonly Entity[];
43
48
  };
44
49
 
45
- /** A sync point's structural changes to the watched components, and the values adds replace, before they apply. */
46
- export type ChangeSet<C extends Component.Key> = {
50
+ /**
51
+ * A sync point's structural changes to the watched components, and the values adds replace, before they apply; and each
52
+ * watched resource written since the last sync point's finalizers ran, to a value that isn't equal to its last one,
53
+ * leaf by leaf for data and by identity for objects, with its value before and after.
54
+ */
55
+ export type ChangeSet<C extends Component.Key, R extends Resource.Key = never> = {
47
56
  readonly groups: readonly Group<C>[];
57
+ readonly resources: readonly { readonly [K in R]: { readonly key: K; readonly previous: ResourceType<K>; readonly next: ResourceType<K> } }[R][];
48
58
  /** The value before the sync point, or null if the entity didn't have it. */
49
59
  previous<K extends C>(entity: Entity, component: K): ReadonlyType<K> | null;
50
60
  /** The value after it, or null if the entity won't have it. */
@@ -75,7 +75,7 @@ describe("Finalizer.create and Finalizer.changes", () => {
75
75
  expect(() => untyped(Finalizer.create, { components: {} }, { resources: { reduce: [total] } })).toThrow("only systems contribute to a reduction");
76
76
  expect(() => untyped(Finalizer.create, { components: { with: [node, tags] } }, { read: [node, tags] })).toThrow('uses "typesFinalizerTest/node" on "main" and "typesFinalizerTest/tags" on "world"');
77
77
  expect(() => untyped(Finalizer.changes, { name: "derived", watch: [slot] })).toThrow("which is derived");
78
- expect(() => untyped(Finalizer.changes, { name: "resource", watch: [time] })).toThrow("which isn't a registered component");
78
+ expect(() => untyped(Finalizer.changes, { name: "unregistered", watch: [Symbol("typesFinalizerTest/unregistered")] })).toThrow("which isn't a registered component or resource");
79
79
  expect(() => untyped(Finalizer.create, { components: { with: [time] } })).toThrow(`uses component "typesFinalizerTest/time", which isn't a registered component`);
80
80
  expect(() => untyped(Finalizer.create, { components: { with: [label] }, lookup: { others: { not: [Symbol("typesFinalizerTest/unregistered")] } } }, { read: [label] })).toThrow("which isn't a registered component");
81
81
  expect(programFinalizers()).toHaveLength(7);
@@ -93,6 +93,8 @@ describe("Finalizer.create and Finalizer.changes", () => {
93
93
  .toThrow('uses "typesFinalizerTest/tags" on "world" and "typesFinalizerTest/page" on "main"');
94
94
  expect(() => untyped(Finalizer.changes, {}, { resources: { read: [page] } }))
95
95
  .toThrow(`watches or reads values on thread "main"; a changes finalizer runs on the world's thread, in the sync point.`);
96
+ expect(() => untyped(Finalizer.changes, { name: "savePage", watch: [page] }))
97
+ .toThrow(`watches or reads values on thread "main"; a changes finalizer runs on the world's thread, in the sync point.`);
96
98
  // Reading another thread's values, without writing or looking up, it's created.
97
99
  const detach = Finalizer.create({ components: { with: [node] } }, row => { void row[node]; });
98
100
  expect(programFinalizers()).toHaveLength(8);
@@ -8,9 +8,11 @@ import { checkWorldWritten } from '../registry/written.js';
8
8
  import { checkLookup } from '../registry/indexes.js';
9
9
  import type { Listed, Property } from '../system/property.js';
10
10
  import type { ThreadOfComponent } from '../component/value.js';
11
+ import type { ThreadOfResource } from '../resource/value.js';
12
+ import type { Resource } from '../resource/resource.js';
11
13
  import type { Component } from '../component/component.js';
12
14
  import type { Elsewhere, OneThread, ThreadedBy, ThreadsOf } from '../system/threads.js';
13
- import type { ChangeSet, Context, Row, Watched } from './body.js';
15
+ import type { ChangeSet, Context, Row, Watched, WatchedResources } from './body.js';
14
16
  import type { Changes, Leaf } from './created.js';
15
17
  import type { ChangesDeclaration, Declaration, Resolved, ResolvedChanges } from './declaration.js';
16
18
  import { accessOf, compact, lookupsLists, resourceUses, type Access } from '../system/resolve.js';
@@ -26,7 +28,7 @@ type Remote<D> = true extends Elsewhere<ThreadsOf<D>>
26
28
  : {};
27
29
 
28
30
  /** A changes finalizer runs on the world's thread, in the sync point: it watches and reads no other thread's objects. */
29
- type OnWorld<D> = true extends Elsewhere<ThreadsOf<D> | ThreadOfComponent<Listed<D, "watch"> & Component.Key>>
31
+ type OnWorld<D> = true extends Elsewhere<ThreadsOf<D> | ThreadOfComponent<Listed<D, "watch"> & Component.Key> | ThreadOfResource<Listed<D, "watch"> & Resource.Key>>
30
32
  ? { readonly "error: a changes finalizer watches and reads only the world's thread's objects": true }
31
33
  : {};
32
34
 
@@ -127,12 +129,12 @@ export const create = <const D extends Declaration>(
127
129
 
128
130
  /**
129
131
  * Declares a finalizer of each sync point's structural changes, which applies in every world, and returns it. It runs
130
- * on the world's thread. Throws as `create` does, for a watched component that isn't registered or is derived, and for
131
- * one watching or reading another thread's values.
132
+ * on the world's thread. Throws as `create` does, for a watched component that isn't registered or is derived, a
133
+ * watched key that's neither a component nor a resource, and for one watching or reading another thread's values.
132
134
  */
133
135
  export const changes = <const D extends ChangesDeclaration>(
134
136
  declared: D & Inferred & OnWorld<D>,
135
- body: (changes: ChangeSet<Watched<D>>, context: Context<D>) => void,
137
+ body: (changes: ChangeSet<Watched<D>, WatchedResources<D>>, context: Context<D>) => void,
136
138
  ): Finalizer => {
137
139
  const id = takeIdOfDeclaration(declared);
138
140
  const name = nameOf(declared, id, "finalizer");
@@ -148,10 +150,13 @@ export const changes = <const D extends ChangesDeclaration>(
148
150
  for (const keys of [lookup.read, "has" in lookup ? lookup.has : undefined, "not" in lookup ? lookup.not : undefined]) requireRegistered("Finalizer", name, "component", keys);
149
151
  }
150
152
  for (const key of declaration.watch ?? []) {
151
- if (kindOf(key) !== "component") throw new Error(`Finalizer "${name}" watches "${labelOf(key)}", which isn't a registered component: ${whyUnregistered(key)}.`);
153
+ const kind = kindOf(key);
154
+ if (kind !== "component" && kind !== "resource") throw new Error(`Finalizer "${name}" watches "${labelOf(key)}", which isn't a registered component or resource: ${whyUnregistered(key)}.`);
152
155
  if (declarationOf(key)?.derived === true) throw new Error(`Finalizer "${name}" watches "${labelOf(key)}", which is derived; it sees only components user code adds and removes.`);
153
156
  }
154
- const thread = threadOf({ components: { read: declaration.watch }, resources: declaration.resources, lookup: declaration.lookup }, "Finalizer", name);
157
+ const watched = (kind: "component" | "resource"): readonly symbol[] => (declaration.watch ?? []).filter(key => kindOf(key) === kind);
158
+ const read = [...declaration.resources?.read ?? [], ...watched("resource")];
159
+ const thread = threadOf({ components: { read: watched("component") }, resources: { ...declaration.resources, read }, lookup: declaration.lookup }, "Finalizer", name);
155
160
  if (thread !== null && thread !== "world") throw new Error(`Finalizer "${name}" watches or reads values on thread "${thread}"; a changes finalizer runs on the world's thread, in the sync point.`);
156
161
  checkOrder(name, "world", declaration);
157
162
  return addFinalizer(finalizer, id);
@@ -4,6 +4,7 @@ import type { SystemLookupLists, ResourceLists } from '../system/access.js';
4
4
  import type { Lookup } from '../system/access.js';
5
5
  import type { Finalizer } from './finalizer.js';
6
6
  import type { Essential } from '../component/value.js';
7
+ import type { Resource } from '../resource/resource.js';
7
8
 
8
9
  /**
9
10
  * What every finalizer declares. What its body reads of its row and lookups, and reads and writes of resources, the
@@ -26,9 +27,10 @@ export type Declaration = Shared & {
26
27
 
27
28
  /**
28
29
  * A finalizer that sees each sync point's structural changes to the components it watches: by default, every one that
29
- * isn't derived or an object a joined thread fills.
30
+ * isn't derived or an object a joined thread fills; and the resources it watches, none by default, written since the
31
+ * last sync point's finalizers ran.
30
32
  */
31
- export type ChangesDeclaration = Shared & { readonly watch?: readonly Essential[] };
33
+ export type ChangesDeclaration = Shared & { readonly watch?: readonly (Essential | Resource.Key)[] };
32
34
 
33
35
  type Key = symbol;
34
36
 
@@ -27,8 +27,8 @@ Rule.create({ components: { with: [position] }, derive: [slot] });
27
27
  declare const create: Create;
28
28
  declare const wgsl: typeof Gpu.wgsl;
29
29
  export const world = create({ [workers]: 4, [gpuDevice]: null });
30
- export const read = System.create({ observer: true, components: { with: [position, slot] } }, () => {});
31
- export const readOne = System.create({ observer: true, lookup: { at: { with: [position, slot] } } }, () => {});
30
+ export const read = System.create({ readonly: true, components: { with: [position, slot] } }, () => {});
31
+ export const readOne = System.create({ readonly: true, lookup: { at: { with: [position, slot] } } }, () => {});
32
32
  export const report = world.run(all);
33
33
  export const shader = wgsl(draw);
34
34
  export const added = world.add(read);
@@ -144,3 +144,13 @@ describe("systems ordering their rows by an index", () => {
144
144
  expect(() => untypedRender(Index.create({ key: [amount] }))).toThrow(/orders its rows by index "index #\d+", which isn't sorted/);
145
145
  });
146
146
  });
147
+
148
+ describe("Index.declarationOf", () => {
149
+ it("gives an index's declaration as registered, its with and not as declared, and nothing for a value Index.create didn't return", () => {
150
+ const byName = Index.create({ key: [name], unique: true, components: { with: [shown], not: [parent] } });
151
+ expect(Index.declarationOf(byName)).toEqual({ key: [name], components: { with: [shown], not: [parent] }, unique: true });
152
+ const byCell = Index.create({ key: [cell, amount], sorted: true });
153
+ expect(Index.declarationOf(byCell)).toEqual({ key: [cell, amount], components: {}, sorted: true });
154
+ expect(Index.declarationOf({ name: "not an index" })).toBeUndefined();
155
+ });
156
+ });
@@ -0,0 +1,34 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import type { Component } from '../component/component.js';
3
+ import { indexDefinitionOf } from '../registry/indexes.js';
4
+ import { kindOf } from '../registry/names.js';
5
+ import type { Index } from './index-type.js';
6
+
7
+ /**
8
+ * An index's declaration as `Index.create` registered it: its key, the components it holds by besides its key and those
9
+ * it excludes, and whether it's sorted or unique.
10
+ */
11
+ export type Declared = {
12
+ readonly key: readonly Component.Key[];
13
+ readonly components: { readonly with?: readonly Component.Key[]; readonly not?: readonly Component.Key[] };
14
+ readonly sorted?: boolean;
15
+ readonly unique?: boolean;
16
+ };
17
+
18
+ /** Registration takes only components for a key part, `with` and `not`. */
19
+ const isComponent = (key: symbol): key is Component.Key => kindOf(key) === "component";
20
+
21
+ /** The declaration `index` was created from, as registered, or undefined for a value `Index.create` didn't return. */
22
+ export const declarationOf = <D extends object>(index: Index<D>): Declared | undefined => {
23
+ const created = indexDefinitionOf(index);
24
+ if (created === undefined) return undefined;
25
+ const { key, components, sorted, unique } = created.declaration;
26
+ const held = components?.has?.filter(isComponent);
27
+ const excluded = components?.not?.filter(isComponent);
28
+ return {
29
+ key: key.filter(isComponent),
30
+ components: { ...held === undefined ? {} : { with: held }, ...excluded === undefined ? {} : { not: excluded } },
31
+ ...sorted === undefined ? {} : { sorted },
32
+ ...unique === undefined ? {} : { unique },
33
+ };
34
+ };
@@ -1,2 +1,3 @@
1
1
  // © 2026 Adobe. MIT License. See /LICENSE for details.
2
2
  export * from './create.js';
3
+ export { declarationOf, type Declared } from './declaration-of.js';
@@ -219,7 +219,7 @@ System.create({
219
219
 
220
220
  // An index lookup's `changed: true` runs its system only when the index's entries changed since it last ran: its
221
221
  // lists' `changed` then reports entries alone. Observer systems and CPU kernels take it; it's not a list of components.
222
- System.create({ observer: true, lookup: { living: { index: living, changed: true } } }, ({ lookup }) => {
222
+ System.create({ readonly: true, lookup: { living: { index: living, changed: true } } }, ({ lookup }) => {
223
223
  const rows = lookup.living();
224
224
  check<Equal<typeof rows.changed, boolean>>();
225
225
  check<Equal<typeof rows.length, number>>();
@@ -61,6 +61,14 @@ const entryOf = (key: symbol): Entry | undefined => {
61
61
  return entries.get(key);
62
62
  };
63
63
 
64
+ /** Every key registered as `kind`, each by the symbol a world gives its values by, with its declaration. */
65
+ export const declaredOf = (kind: "component" | "resource"): ReadonlyMap<symbol, AnyDeclaration> => {
66
+ catchUp();
67
+ const found = new Map<symbol, AnyDeclaration>();
68
+ for (const entry of entries.values()) if (entry.kind === kind && entry.declaration !== undefined) found.set(entry.newest, entry.declaration);
69
+ return found;
70
+ };
71
+
64
72
  /** Whether a key is a component, a resource or a trait. */
65
73
  export const kindOf = (key: unknown): Kind | undefined => typeof key === "symbol" ? entryOf(key)?.kind : undefined;
66
74
 
@@ -4,8 +4,8 @@ import type { Definition } from '../system/definition.js';
4
4
  import type { Leaf } from '../system/leaf.js';
5
5
  import { firstItemOf } from './items.js';
6
6
  import { requireDefinition } from './program.js';
7
- import { firstKeyOf, labelOf, sameDeclaration } from './names.js';
8
- import { lookupKeysOf, orderKeysOf } from './indexes.js';
7
+ import { declarationOf, firstKeyOf, labelOf, sameDeclaration } from './names.js';
8
+ import { indexDefinitionOf, lookupKeysOf, orderKeysOf } from './indexes.js';
9
9
 
10
10
  type Key = symbol;
11
11
 
@@ -28,21 +28,34 @@ const checkInputs = (leaves: readonly Leaf[]): void => {
28
28
 
29
29
  /**
30
30
  * Leaves in run order, grouped into tiers. Leaves in one tier have no ordering path between them. `reductions` are the
31
- * resources its leaves reduce into, each with how many of them contribute. `observer` are its observer leaves, in no
32
- * tier: they run after its sync point, ordered with nothing.
31
+ * resources its leaves reduce into, each with how many of them contribute. `observer` are its readonly leaves, in no
32
+ * tier: they run after its sync point, ordered with nothing. `syncs` holds, after each tier, the reasons for a sync
33
+ * point there, which applies the commands so far before the next tier, or null for none.
33
34
  */
34
- export type Schedule = { readonly tiers: readonly (readonly Leaf[])[]; readonly reductions: ReadonlyMap<Key, number>; readonly observer: readonly Leaf[] };
35
+ export type Schedule = {
36
+ readonly tiers: readonly (readonly Leaf[])[];
37
+ readonly reductions: ReadonlyMap<Key, number>;
38
+ readonly observer: readonly Leaf[];
39
+ readonly syncs: readonly (readonly SyncReason[] | null)[];
40
+ };
35
41
 
36
42
  export const isLeaf = (system: Definition): system is Leaf => system.kind !== "composite";
37
43
 
38
- /** Whether a system is an observer leaf, which runs after a sync point rather than in a tier. */
39
- export const isObserver = (system: Definition): system is Leaf & { readonly declaration: { readonly observer: true } } => system.kind !== "composite" && system.declaration.observer === true;
44
+ /** Whether a system is a readonly system leaf, which runs after a sync point rather than in a tier. */
45
+ export const isObserver = (system: Definition): system is Leaf & { readonly declaration: { readonly readonly: true } } => system.kind !== "composite" && system.declaration.readonly === true;
40
46
 
41
47
  /** Each system is an interval, from its start node to its end node. */
42
48
  const start = (unit: number): number => 2 * unit;
43
49
  const end = (unit: number): number => 2 * unit + 1;
44
50
 
45
- type Graph = { readonly units: readonly Definition[]; readonly successors: readonly Set<number>[] };
51
+ type Graph = {
52
+ readonly units: readonly Definition[];
53
+ readonly successors: readonly Set<number>[];
54
+ /** Each unit's members, by unit: none for a leaf. */
55
+ readonly members: readonly (readonly number[])[];
56
+ /** The units a list of systems names, readonly systems left out. */
57
+ readonly resolve: (systems: readonly object[] | undefined) => readonly number[];
58
+ };
46
59
 
47
60
  /**
48
61
  * A leaf runs from its start to its end, and a composite spans its members. `after` and `before` order one system's end
@@ -65,7 +78,7 @@ const graphOf = (root: Definition): Graph => {
65
78
  };
66
79
  visit(root);
67
80
  const successors = units.flatMap(() => [new Set<number>(), new Set<number>()]);
68
- // An observer system is ordered with nothing, so constraints naming one are ignored.
81
+ // A readonly system is ordered with nothing, so constraints naming one are ignored.
69
82
  const resolve = (systems: readonly object[] | undefined): readonly number[] =>
70
83
  (systems ?? []).flatMap(system => {
71
84
  const unit = indexOf.get(firstItemOf(system));
@@ -86,7 +99,7 @@ const graphOf = (root: Definition): Graph => {
86
99
  successors[end(unit)].add(end(other));
87
100
  }
88
101
  });
89
- return { units, successors };
102
+ return { units, successors, members, resolve };
90
103
  };
91
104
 
92
105
  /** Kahn's algorithm over nodes. Throws on a cycle. */
@@ -165,6 +178,74 @@ const conflictOf = (a: Access, b: Access): Key | undefined => {
165
178
  return undefined;
166
179
  };
167
180
 
181
+ /** Every key two accesses conflict on. */
182
+ const conflictsOf = (a: Access, b: Access): readonly Key[] => [...new Set([
183
+ ...[...a.writes].filter(key => b.writes.has(key) || b.reads.has(key) || b.reduces.has(key)),
184
+ ...[...b.writes].filter(key => a.reads.has(key) || a.reduces.has(key)),
185
+ ])];
186
+
187
+ /** A system's leaves in its tiers, with their units and accesses, and its graph with readers of folds after their contributors. */
188
+ const orderedOf = (system: Definition) => {
189
+ const graph = graphOf(system);
190
+ const leaves = graph.units.flatMap((unit, index) => isLeaf(unit) && !isObserver(unit) ? [{ leaf: unit, unit: index }] : []);
191
+ const accesses = leaves.map(({ leaf }) => accessOf(leaf));
192
+ // Leaves that read a resource run after every leaf that reduces into it, so they read the finished fold.
193
+ const reductions = new Map<Key, number>();
194
+ accesses.forEach((access, contributor) => {
195
+ for (const key of access.reduces) {
196
+ reductions.set(key, (reductions.get(key) ?? 0) + 1);
197
+ accesses.forEach((other, reader) => {
198
+ if (reader !== contributor && other.reads.has(key)) graph.successors[end(leaves[contributor].unit)].add(start(leaves[reader].unit));
199
+ });
200
+ }
201
+ });
202
+ return { graph, leaves, accesses, reductions };
203
+ };
204
+
205
+ /**
206
+ * What put a leaf after another: a constraint of either, or of a composite holding it, naming the other or a composite
207
+ * holding it (`after`, `before`), its reading a fold the other contributes to (`reduction`), or being placed during a
208
+ * span (`during`). `conflicts` are the keys both access, one writing, which need the order.
209
+ */
210
+ export type Placement = { readonly leaf: Leaf; readonly because: "after" | "before" | "during" | "reduction"; readonly conflicts: readonly Key[] };
211
+
212
+ /**
213
+ * For each leaf in a system's tiers, the leaves it follows directly: those with an ordering path to it through no other
214
+ * leaf of its tiers.
215
+ */
216
+ export const placementsOf = (system: Definition): ReadonlyMap<Leaf, readonly Placement[]> => {
217
+ const { graph, leaves, accesses } = orderedOf(system);
218
+ const reach = reachability(topologicalOrder(graph), graph.successors);
219
+ const ancestors = graph.units.map(() => [] as number[]);
220
+ graph.members.forEach((members, unit) => {
221
+ const visit = (member: number): void => {
222
+ ancestors[member].push(unit);
223
+ for (const inner of graph.members[member]) visit(inner);
224
+ };
225
+ for (const member of members) visit(member);
226
+ });
227
+ /** Whether `systems` names `unit` or a composite holding it. */
228
+ const names = (systems: readonly object[] | undefined, unit: number): boolean => {
229
+ const named = graph.resolve(systems);
230
+ return named.includes(unit) || ancestors[unit].some(holder => named.includes(holder));
231
+ };
232
+ const constrains = (unit: number, list: "after" | "before", other: number): boolean =>
233
+ [unit, ...ancestors[unit]].some(holder => holder !== 0 && names(graph.units[holder].declaration[list], other));
234
+ const placements = new Map<Leaf, readonly Placement[]>();
235
+ leaves.forEach(({ leaf, unit }, b) => {
236
+ const before = leaves.flatMap((other, a) => a !== b && reaches(reach, end(other.unit), start(unit)) ? [a] : []);
237
+ const direct = before.filter(a => !before.some(other => other !== a && reaches(reach, end(leaves[a].unit), start(leaves[other].unit))));
238
+ placements.set(leaf, direct.map(a => {
239
+ const other = leaves[a];
240
+ const because = constrains(unit, "after", other.unit) ? "after"
241
+ : constrains(other.unit, "before", unit) ? "before"
242
+ : [...accesses[a].reduces].some(key => accesses[b].reads.has(key)) ? "reduction" : "during";
243
+ return { leaf: other.leaf, because, conflicts: conflictsOf(accesses[a], accesses[b]) };
244
+ }));
245
+ });
246
+ return placements;
247
+ };
248
+
168
249
  const schedules = new WeakMap<Definition, Schedule>();
169
250
 
170
251
  /** A system's schedule, created once per system. Throws on a cycle, or on two conflicting leaves with no ordering path between them. */
@@ -179,33 +260,125 @@ export const scheduleOf = (system: Definition): Schedule => {
179
260
  * or on inputs that don't merge.
180
261
  */
181
262
  const createSchedule = (system: Definition): Schedule => {
182
- const graph = graphOf(system);
263
+ const { graph, leaves, accesses, reductions } = orderedOf(system);
183
264
  const observer = graph.units.filter(isObserver);
184
- const leaves = graph.units.flatMap((unit, index) => isLeaf(unit) && !isObserver(unit) ? [{ leaf: unit, unit: index }] : []);
185
265
  checkInputs(leaves.map(({ leaf }) => leaf));
186
- const accesses = leaves.map(({ leaf }) => accessOf(leaf));
187
- // Leaves that read a resource run after every leaf that reduces into it, so they read the finished fold.
188
- const reductions = new Map<Key, number>();
189
- accesses.forEach((access, contributor) => {
190
- for (const key of access.reduces) {
191
- reductions.set(key, (reductions.get(key) ?? 0) + 1);
192
- accesses.forEach((other, reader) => {
193
- if (reader !== contributor && other.reads.has(key)) graph.successors[end(leaves[contributor].unit)].add(start(leaves[reader].unit));
194
- });
195
- }
196
- });
197
266
  const order = topologicalOrder(graph);
198
267
  const reach = reachability(order, graph.successors);
199
268
  for (let a = 0; a < leaves.length; a++) {
200
269
  for (let b = a + 1; b < leaves.length; b++) {
201
270
  const key = conflictOf(accesses[a], accesses[b]);
202
271
  if (key !== undefined && !ordered(reach, leaves[a].unit, leaves[b].unit)) {
203
- throw new Error(`Systems "${leaves[a].leaf.name}" and "${leaves[b].leaf.name}" both access "${labelOf(key)}" and one writes it; order them with before or after.`);
272
+ const whole = [leaves[a].leaf, leaves[b].leaf].filter(leaf => leaf.declaration.components?.whole === true)
273
+ .map(leaf => ` "${leaf.name}" takes its row whole (components.whole), so it writes every member.`).join("");
274
+ throw new Error(`Systems "${leaves[a].leaf.name}" and "${leaves[b].leaf.name}" both access "${labelOf(key)}" and one writes it; order them with before or after.${whole}`);
204
275
  }
205
276
  }
206
277
  }
207
278
  const depth = depthsOf(graph, order);
208
- const tiers: Leaf[][] = [];
209
- for (const { leaf, unit } of leaves) (tiers[depth[start(unit)]] ??= []).push(leaf);
210
- return { tiers: tiers.filter(tier => tier !== undefined), reductions, observer };
279
+ const byDepth: Leaf[][] = [];
280
+ for (const { leaf, unit } of leaves) (byDepth[depth[start(unit)]] ??= []).push(leaf);
281
+ const tiers = byDepth.filter(tier => tier !== undefined);
282
+ // A sync point comes between tiers, so a schedule of one tier has none.
283
+ if (tiers.length < 2) return { tiers, reductions, observer, syncs: tiers.map(() => null) };
284
+ const tierOf = new Map<Leaf, number>();
285
+ tiers.forEach((tier, t) => { for (const leaf of tier) tierOf.set(leaf, t); });
286
+ const edges: { readonly lo: number; readonly hi: number; readonly reason: SyncReason }[] = [];
287
+ leaves.forEach((from, a) => {
288
+ const changed = structuralOf(from.leaf);
289
+ if (changed === null) return;
290
+ leaves.forEach((to, b) => {
291
+ if (a === b || !reaches(reach, end(from.unit), start(to.unit))) return;
292
+ const keys = affected(changed, touchesOf(to.leaf));
293
+ if (keys !== null) edges.push({ lo: tierOf.get(from.leaf) ?? 0, hi: (tierOf.get(to.leaf) ?? 0) - 1, reason: { from: from.leaf, to: to.leaf, keys } });
294
+ });
295
+ });
296
+ return { tiers, reductions, observer, syncs: syncsOf(tiers.length, edges) };
297
+ };
298
+
299
+ /**
300
+ * Why a sync point follows a tier: a system in it or before it whose commands may change, structurally, what a system
301
+ * after the sync point that follows it reads, matches, looks up or indexes. `keys` are the components its commands name
302
+ * that the later one uses, none where it despawns, or where its commands or a cascade could change anything.
303
+ */
304
+ export type SyncReason = { readonly from: Leaf; readonly to: Leaf; readonly keys: readonly Key[] };
305
+
306
+ /** The components a leaf's commands may add or remove, by spawn, add or remove; `"any"` once it despawns or isn't followed. */
307
+ const structuralOf = (leaf: Leaf): ReadonlySet<Key> | "any" | null => {
308
+ const { commands } = leaf;
309
+ if (commands === undefined) return null;
310
+ if (commands === null || commands.despawn === true) return "any";
311
+ const keys = new Set([...commands.spawn ?? [], ...commands.add ?? [], ...commands.remove ?? []].map(firstKeyOf));
312
+ // A reference written may despawn its holder, or remove itself, at the sync point.
313
+ for (const key of keys) if (declarationOf(key)?.reference !== undefined) return "any";
314
+ return keys.size === 0 ? null : keys;
315
+ };
316
+
317
+ /**
318
+ * The components a leaf's rows, lookups, indexes and order name, which commands could change its view of; `any` when
319
+ * one of them matches every entity, `cascaded` when it names a component a rule derives or `within` removes.
320
+ */
321
+ const touchesOf = (leaf: Leaf): { readonly keys: ReadonlySet<Key>; readonly any: boolean; readonly cascaded: boolean } => {
322
+ const { components, lookup, up, order } = leaf.declaration;
323
+ const keys = new Set<Key>();
324
+ let any = false;
325
+ const add = (list: readonly Key[] | undefined): void => { for (const key of list ?? []) keys.add(firstKeyOf(key)); };
326
+ /** Names what a query requires; one requiring nothing matches every entity. */
327
+ const requires = (required: readonly (readonly Key[] | undefined)[], others: readonly (readonly Key[] | undefined)[]): void => {
328
+ if (required.every(list => list === undefined || list.length === 0)) any = true;
329
+ for (const list of [...required, ...others]) add(list);
330
+ };
331
+ const indexed = (index: unknown): void => {
332
+ const declaration = indexDefinitionOf(index)?.declaration;
333
+ if (declaration !== undefined) requires([declaration.key, declaration.components?.has], [declaration.components?.not]);
334
+ };
335
+ if (components !== undefined) {
336
+ requires([components.read, components.write, components.has, components.added, components.changed], [components.optional, components.not]);
337
+ add(Object.getOwnPropertySymbols(components.where ?? {}));
338
+ }
339
+ for (const name in lookup) {
340
+ const used = lookup[name];
341
+ if ("index" in used && used.index !== undefined) {
342
+ indexed(used.index);
343
+ add(used.read);
344
+ }
345
+ else if (!("index" in used)) requires([used.read, used.has], [used.optional, used.not, Array.isArray(used.changed) ? used.changed : undefined]);
346
+ }
347
+ if (order !== undefined) indexed(order);
348
+ if (up !== undefined) add([up.through, ...up.read ?? []]);
349
+ const cascaded = [...keys].some(key => { const declared = declarationOf(key); return declared?.derived === true || declared?.within !== undefined; });
350
+ return { keys, any, cascaded };
351
+ };
352
+
353
+ /** The keys through which `changed` may change what `touches` names; null where it can't. */
354
+ const affected = (changed: ReadonlySet<Key> | "any", touches: ReturnType<typeof touchesOf>): readonly Key[] | null => {
355
+ if (touches.keys.size === 0 && !touches.any) return null;
356
+ if (changed === "any") return [];
357
+ if (touches.any || touches.cascaded) return [...changed];
358
+ const keys = [...changed].filter(key => touches.keys.has(key));
359
+ return keys.length === 0 ? null : keys;
360
+ };
361
+
362
+ /**
363
+ * The fewest sync points that put one between the tiers of each edge, after a tier from `lo` to `hi`: each placed as
364
+ * late as the first edge it must cover allows, so it covers every edge open then. An edge's reason goes to the first
365
+ * sync point within its tiers.
366
+ */
367
+ const syncsOf = (count: number, edges: readonly { readonly lo: number; readonly hi: number; readonly reason: SyncReason }[]): readonly (readonly SyncReason[] | null)[] => {
368
+ const syncs: (SyncReason[] | null)[] = new Array<SyncReason[] | null>(count).fill(null);
369
+ let last = -1;
370
+ for (const { lo, hi } of [...edges].sort((a, b) => a.hi - b.hi)) {
371
+ if (last >= lo) continue;
372
+ last = hi;
373
+ syncs[hi] = [];
374
+ }
375
+ for (const { lo, hi, reason } of edges) {
376
+ for (let t = lo; t <= hi; t++) {
377
+ const reasons = syncs[t];
378
+ if (reasons === null) continue;
379
+ reasons.push(reason);
380
+ break;
381
+ }
382
+ }
383
+ return syncs;
211
384
  };
@@ -0,0 +1,11 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import type { Declared } from '../component/declared.js';
3
+ import { declaredOf } from '../registry/names.js';
4
+ import type { Key } from './key.js';
5
+
6
+ /** Every resource its modules registered so far, with its declaration. */
7
+ export const declarations = (): ReadonlyMap<Key, Declared> => {
8
+ // Registration takes only keys declared in `Resources`, so each symbol registered as a resource is one of its keys.
9
+ const found = declaredOf("resource") as ReadonlyMap<Key, Declared>;
10
+ return found;
11
+ };
@@ -1,2 +1,4 @@
1
1
  // © 2026 Adobe. MIT License. See /LICENSE for details.
2
2
  export type { Key } from './key.js';
3
+ export { declarations } from './declarations.js';
4
+ export type { Declared } from '../component/declared.js';