@adobe/ecs 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/README.md +2 -2
  2. package/dist/implementation/compiler/describe.d.ts +1 -1
  3. package/dist/implementation/compiler/describe.js +3 -3
  4. package/dist/implementation/compiler/describe.js.map +1 -1
  5. package/dist/implementation/compiler/signature.js +1 -1
  6. package/dist/implementation/compiler/transform.js +3 -3
  7. package/dist/implementation/compiler/transform.js.map +1 -1
  8. package/dist/implementation/world/commands.js +50 -1
  9. package/dist/implementation/world/commands.js.map +1 -1
  10. package/dist/implementation/world/entities.d.ts +2 -0
  11. package/dist/implementation/world/entities.js +6 -0
  12. package/dist/implementation/world/entities.js.map +1 -1
  13. package/dist/implementation/world/join.js +10 -2
  14. package/dist/implementation/world/join.js.map +1 -1
  15. package/dist/implementation/world/register.js +2 -2
  16. package/dist/implementation/world/rows.d.ts +1 -1
  17. package/dist/implementation/world/rows.js +5 -0
  18. package/dist/implementation/world/rows.js.map +1 -1
  19. package/dist/implementation/world/runner.js +3 -3
  20. package/dist/implementation/world/runner.js.map +1 -1
  21. package/dist/implementation/world/sharing.js +1 -1
  22. package/dist/implementation/world/staging.d.ts +4 -0
  23. package/dist/implementation/world/staging.js +15 -3
  24. package/dist/implementation/world/staging.js.map +1 -1
  25. package/dist/implementation/world/tables-read.d.ts +7 -0
  26. package/dist/implementation/world/tables-read.js +60 -0
  27. package/dist/implementation/world/tables-read.js.map +1 -0
  28. package/dist/implementation/world/tables.d.ts +9 -0
  29. package/dist/implementation/world/tables.js +355 -0
  30. package/dist/implementation/world/tables.js.map +1 -0
  31. package/dist/implementation/world/thread-observer.js +1 -1
  32. package/dist/implementation/world/thread-observer.js.map +1 -1
  33. package/dist/implementation/world/thread-systems.js +1 -0
  34. package/dist/implementation/world/thread-systems.js.map +1 -1
  35. package/dist/index.d.ts +8 -28
  36. package/dist/index.js +4 -9
  37. package/dist/index.js.map +1 -1
  38. package/dist/interface/component/declarations.d.ts +4 -0
  39. package/dist/interface/component/declarations.js +8 -0
  40. package/dist/interface/component/declarations.js.map +1 -0
  41. package/dist/interface/component/declared.d.ts +12 -0
  42. package/dist/interface/component/declared.js +2 -0
  43. package/dist/interface/component/declared.js.map +1 -0
  44. package/dist/interface/component/index.d.ts +2 -0
  45. package/dist/interface/component/index.js +1 -1
  46. package/dist/interface/component/index.js.map +1 -1
  47. package/dist/interface/registry/names.d.ts +2 -0
  48. package/dist/interface/registry/names.js +9 -0
  49. package/dist/interface/registry/names.js.map +1 -1
  50. package/dist/interface/registry/schedule.d.ts +3 -3
  51. package/dist/interface/registry/schedule.js +3 -3
  52. package/dist/interface/registry/schedule.js.map +1 -1
  53. package/dist/interface/resource/declarations.d.ts +4 -0
  54. package/dist/interface/resource/declarations.js +8 -0
  55. package/dist/interface/resource/declarations.js.map +1 -0
  56. package/dist/interface/resource/index.d.ts +2 -0
  57. package/dist/interface/resource/index.js +1 -1
  58. package/dist/interface/resource/index.js.map +1 -1
  59. package/dist/interface/system/body.d.ts +11 -6
  60. package/dist/interface/system/check.d.ts +1 -1
  61. package/dist/interface/system/check.js +25 -9
  62. package/dist/interface/system/check.js.map +1 -1
  63. package/dist/interface/system/commands.d.ts +16 -0
  64. package/dist/interface/system/create.d.ts +21 -9
  65. package/dist/interface/system/create.js +3 -3
  66. package/dist/interface/system/create.js.map +1 -1
  67. package/dist/interface/system/declaration.d.ts +9 -2
  68. package/dist/interface/system/tables.d.ts +20 -0
  69. package/dist/interface/system/tables.js +2 -0
  70. package/dist/interface/system/tables.js.map +1 -0
  71. package/dist/interface/world/index.d.ts +1 -1
  72. package/dist/interface/world/table/index.d.ts +26 -0
  73. package/dist/interface/world/table/index.js +3 -0
  74. package/dist/interface/world/table/index.js.map +1 -0
  75. package/dist/interface/world/table/table.d.ts +11 -0
  76. package/dist/interface/world/table/table.js +2 -0
  77. package/dist/interface/world/table/table.js.map +1 -0
  78. package/dist/interface/world/world.d.ts +10 -10
  79. package/dist/world.d.ts +27 -0
  80. package/dist/world.js +27 -0
  81. package/dist/world.js.map +1 -0
  82. package/package.json +1 -1
  83. package/src/exports.test.ts +1 -1
  84. package/src/guide/00-keys.ts +1 -1
  85. package/src/guide/01-resources.md +1 -1
  86. package/src/guide/01-resources.ts +1 -1
  87. package/src/guide/02-entities.ts +2 -2
  88. package/src/guide/03-systems.md +2 -2
  89. package/src/guide/03-systems.out +1 -1
  90. package/src/guide/03-systems.ts +3 -3
  91. package/src/guide/04-ticks.md +1 -1
  92. package/src/guide/04-ticks.ts +3 -3
  93. package/src/guide/05-scheduling.ts +1 -1
  94. package/src/guide/06-sync-points.md +9 -2
  95. package/src/guide/06-sync-points.ts +2 -2
  96. package/src/guide/07-change-filters.ts +2 -2
  97. package/src/guide/08-input.ts +1 -1
  98. package/src/guide/09-rules.ts +1 -1
  99. package/src/guide/11-traits.ts +3 -3
  100. package/src/guide/12-lookups.ts +2 -2
  101. package/src/guide/13-indexes.ts +4 -4
  102. package/src/guide/14-kernels.ts +1 -1
  103. package/src/guide/15-reductions.ts +1 -1
  104. package/src/guide/16-gpu.ts +1 -1
  105. package/src/guide/17-hierarchies.ts +1 -1
  106. package/src/guide/18-identity.out +1 -1
  107. package/src/guide/19-threads.md +4 -2
  108. package/src/guide/19-threads.ts +2 -2
  109. package/src/guide/20-shared.ts +3 -3
  110. package/src/guide/21-dom.md +2 -2
  111. package/src/guide/21-dom.ts +1 -1
  112. package/src/guide/22-canvas.md +1 -1
  113. package/src/guide/22-canvas.ts +1 -1
  114. package/src/guide/CLAUDE.md +1 -1
  115. package/src/guide/guide.test.ts +7 -5
  116. package/src/implementation/compiler/describe.test-helper.ts +1 -1
  117. package/src/implementation/compiler/describe.test.ts +1 -1
  118. package/src/implementation/compiler/describe.ts +3 -3
  119. package/src/implementation/compiler/hygiene.test.ts +1 -1
  120. package/src/implementation/compiler/plugin.test.ts +2 -2
  121. package/src/implementation/compiler/signature.ts +1 -1
  122. package/src/implementation/compiler/transform.test.ts +3 -3
  123. package/src/implementation/compiler/transform.ts +3 -3
  124. package/src/implementation/world/change-blocks.test.ts +1 -1
  125. package/src/implementation/world/commands.ts +44 -1
  126. package/src/implementation/world/defaults.test.ts +1 -1
  127. package/src/implementation/world/entities.ts +6 -0
  128. package/src/implementation/world/finalizers.test.ts +1 -1
  129. package/src/implementation/world/index-changed.test.ts +2 -2
  130. package/src/implementation/world/index-joined.test.ts +5 -5
  131. package/src/implementation/world/index-order.test.ts +1 -1
  132. package/src/implementation/world/join.test.ts +4 -4
  133. package/src/implementation/world/join.ts +10 -2
  134. package/src/implementation/world/joined-observer.test.ts +1 -1
  135. package/src/implementation/world/joining-worker.fixture.ts +1 -1
  136. package/src/implementation/world/joining.fixture.ts +1 -1
  137. package/src/implementation/world/joining.test-helper.ts +1 -1
  138. package/src/implementation/world/kernel-spawns.test.ts +1 -1
  139. package/src/implementation/world/observe-factories.test.ts +2 -2
  140. package/src/implementation/world/observe-ticks-cases.test.ts +1 -1
  141. package/src/implementation/world/observe.test-helper.ts +1 -1
  142. package/src/implementation/world/observer-contained-worker.fixture.ts +1 -1
  143. package/src/implementation/world/observer-contained.test-helper.ts +1 -1
  144. package/src/implementation/world/observer-contained.test.ts +2 -2
  145. package/src/implementation/world/observer-joined.test-helper.ts +4 -4
  146. package/src/implementation/world/observer-joined.test.ts +23 -15
  147. package/src/implementation/world/observer.browser.test.ts +2 -2
  148. package/src/implementation/world/observer.test.ts +8 -8
  149. package/src/implementation/world/optional-workers.fixture.ts +1 -1
  150. package/src/implementation/world/optional.test.ts +7 -7
  151. package/src/implementation/world/reclaim.test.ts +3 -3
  152. package/src/implementation/world/register.ts +2 -2
  153. package/src/implementation/world/remote-objects.test-helper.ts +1 -1
  154. package/src/implementation/world/rows.ts +5 -1
  155. package/src/implementation/world/run-results.test.ts +10 -10
  156. package/src/implementation/world/runner.ts +3 -3
  157. package/src/implementation/world/shared-gpu.browser.test.ts +2 -2
  158. package/src/implementation/world/shared-kernels.test.ts +2 -2
  159. package/src/implementation/world/shared-workers.fixture.ts +2 -2
  160. package/src/implementation/world/shared.test.ts +8 -8
  161. package/src/implementation/world/sharing.ts +1 -1
  162. package/src/implementation/world/staging.ts +16 -3
  163. package/src/implementation/world/tables-read.ts +59 -0
  164. package/src/implementation/world/tables.test.ts +267 -0
  165. package/src/implementation/world/tables.ts +330 -0
  166. package/src/implementation/world/tails.test.ts +1 -1
  167. package/src/implementation/world/target.test.ts +2 -2
  168. package/src/implementation/world/thread-observer.ts +1 -1
  169. package/src/implementation/world/thread-systems.test-helper.ts +3 -3
  170. package/src/implementation/world/thread-systems.ts +1 -0
  171. package/src/implementation/world/where.test.ts +4 -4
  172. package/src/implementation/world/workers.fixture.ts +2 -2
  173. package/src/index.ts +4 -29
  174. package/src/interface/component/declarations.ts +11 -0
  175. package/src/interface/component/declared.ts +9 -0
  176. package/src/interface/component/index.ts +2 -0
  177. package/src/interface/index.type-test.ts +2 -2
  178. package/src/interface/indexes/index.type-test.ts +1 -1
  179. package/src/interface/registry/names.ts +8 -0
  180. package/src/interface/registry/schedule.ts +4 -4
  181. package/src/interface/resource/declarations.ts +11 -0
  182. package/src/interface/resource/index.ts +2 -0
  183. package/src/interface/system/body.ts +7 -5
  184. package/src/interface/system/check.ts +22 -8
  185. package/src/interface/system/commands.ts +14 -0
  186. package/src/interface/system/create.test.ts +13 -13
  187. package/src/interface/system/create.ts +15 -10
  188. package/src/interface/system/declaration.ts +9 -2
  189. package/src/interface/system/lists.type-test.ts +1 -1
  190. package/src/interface/system/optional.type-test.ts +1 -1
  191. package/src/interface/system/shared.type-test.ts +1 -1
  192. package/src/interface/system/system.type-test.ts +26 -20
  193. package/src/interface/system/tables.ts +21 -0
  194. package/src/interface/system/up.type-test.ts +3 -3
  195. package/src/interface/world/index.ts +2 -2
  196. package/src/interface/world/run.type-test.ts +3 -3
  197. package/src/interface/world/table/index.ts +19 -0
  198. package/src/interface/world/table/table.ts +12 -0
  199. package/src/interface/world/world.ts +10 -10
  200. package/src/observe/entity.ts +2 -2
  201. package/src/observe/resource.ts +2 -2
  202. package/src/observe/select.ts +4 -4
  203. package/src/observe/world-state.ts +3 -3
  204. package/src/perftest/benchmarks.ts +1 -1
  205. package/src/perftest/indexes.ts +3 -3
  206. package/src/perftest/loop.ts +1 -1
  207. package/src/perftest/observer.ts +4 -4
  208. package/src/perftest/reads.ts +2 -2
  209. package/src/perftest/remote.ts +5 -5
  210. package/src/perftest/strings.ts +1 -1
  211. package/src/perftest/suite.ts +1 -1
  212. package/src/world.test.ts +2 -2
  213. package/src/world.ts +32 -0
@@ -0,0 +1,21 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import type { Component } from '../component/component.js';
3
+ import type { Resource } from '../resource/resource.js';
4
+ import type { Table } from '../world/table/table.js';
5
+
6
+ /**
7
+ * What a readonly system's `tables` reads: of every archetype matching `with` and `not` that holds any of
8
+ * `components`, a table of those it holds; and, with `resources`, each resource's value.
9
+ */
10
+ export type TablesDeclaration = {
11
+ readonly components: readonly Component.Key[];
12
+ readonly with?: readonly Component.Key[];
13
+ readonly not?: readonly Component.Key[];
14
+ readonly resources?: readonly Resource.Key[];
15
+ };
16
+
17
+ /** A readonly system's `tables`: a table of each archetype its declaration reads, and the values of the resources it reads. */
18
+ export type Tables = {
19
+ readonly archetypes: readonly Table[];
20
+ readonly resources: { readonly [K in Resource.Key]?: Resource<K> };
21
+ };
@@ -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);
@@ -68,7 +68,7 @@ void [counted, notText, nextValue, scaledValue, drawn, rows, composite, input, n
68
68
  // @ts-expect-error a body returns no promise
69
69
  System.create(() => Promise.resolve(1));
70
70
  // @ts-expect-error a body returns no promise
71
- System.create({ observer: true }, () => Promise.resolve(1));
71
+ System.create({ readonly: true }, () => Promise.resolve(1));
72
72
  // @ts-expect-error a body returns no promise, a row system's neither
73
73
  System.create({ components: { with: [position] } }, () => Promise.resolve());
74
74
  // @ts-expect-error a body returns no promise, a render system's neither
@@ -0,0 +1,19 @@
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 new entities in its row order, `Entity.none` for a row it didn't
17
+ * spawn, and how many rows it didn't, those owned by an entity outside the tables.
18
+ */
19
+ 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';
@@ -3,23 +3,23 @@ import type { Input } from '../system/input.js';
3
3
  import type { System } from '../system/system.js';
4
4
 
5
5
  /**
6
- * A world. Systems read and change it; observer systems read it after each run's and tick's sync point. Every member's
6
+ * A world. Systems read and change it; readonly systems read it after each run's and tick's sync point. Every member's
7
7
  * type is named from the package root, so declarations a consumer emits for its results name no internal module.
8
8
  */
9
9
  export type World = {
10
10
  /**
11
11
  * 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
12
+ * runs a system or composite, ending in a sync point, then readonly systems, the run's and those added, and
13
13
  * resolves to what a per-run body returns: a system without components resolves to its body's value, or
14
14
  * `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
15
+ * of a readonly 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 readonly systems alone runs them once
17
17
  * after an empty sync point, and doesn't wake the loop.
18
18
  *
19
19
  * 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,
20
+ * and once its readonly systems ran the run rejects with an `Error` naming each failure, its system's label and id,
21
21
  * 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
22
+ * its GPU device is lost, or its sync point can't apply a command's value. What fails after it, a readonly system
23
23
  * added or GPU work, is reported out of band, as ticks' failures are (see `add`).
24
24
  */
25
25
  run<I, R>(system: System<I, R>, ...input: Input<NoInfer<I>>): Promise<R>;
@@ -27,9 +27,9 @@ export type World = {
27
27
  * Adds a system to every tick, from the next one on, ordered with those already added by its constraints, as a
28
28
  * composite's members are. A tick runs on a beat of the `cadence` resource where `always` is set, `wake` is due or
29
29
  * 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
30
+ * to one already added that conflicts with it. A readonly system runs after every run's and tick's sync point
31
31
  * 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
32
+ * readonly systems, whose keys need ids: each runs on that thread after the world's sync points, once for the
33
33
  * newest when the thread fell behind, reading structs and the objects of the threads it fills as of one sync point.
34
34
  *
35
35
  * What added systems throw, and what fails in ticks, is reported out of band on the world's thread, as an `Error`
@@ -43,14 +43,14 @@ export type World = {
43
43
  * released. A system added both alone and within an added composite stays until both are removed; constraints
44
44
  * naming a removed system are ignored. Removing a value that isn't added does nothing. Throws, removing nothing,
45
45
  * 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.
46
+ * takes only readonly systems.
47
47
  */
48
48
  remove(system: System<void>): void;
49
49
  /**
50
50
  * Lets another thread join this world through `port`, one end of a channel to it, of the browser or node: a
51
51
  * `MessagePort`, a `Worker`, or a worker's own scope, `self` or node's `parentPort`. That thread joins through the
52
52
  * 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
53
+ * and the readonly systems it added as its join resolved, before awaiting anything else, are added, so a run
54
54
  * started afterwards runs on it and reaches them; one it adds later reaches the runs after its add arrived.
55
55
  * Rejects with an `Error` saying why it didn't join: the world refused its claim, as of a thread claimed twice, the
56
56
  * 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
  }));
@@ -505,7 +505,7 @@ export const benchmarks: readonly Benchmark[] = [
505
505
  run: async () => {
506
506
  const { world, roots } = await filteredForest();
507
507
  const found: { child: Entity | null } = { child: null };
508
- await world.run(System.create({ observer: true, components: { with: [hierarchyParent] } }, row => {
508
+ await world.run(System.create({ readonly: true, components: { with: [hierarchyParent] } }, row => {
509
509
  if (found.child === null && row[hierarchyParent] === roots[0]) found.child = row.entity;
510
510
  }));
511
511
  const { child } = found;
@@ -307,13 +307,13 @@ const seedFlags = System.create({}, ({ commands }) => {
307
307
  for (let i = 0; i < flagged; i++) commands.spawn([flag])({ [flag]: i % 2 === 0 });
308
308
  });
309
309
  /** A run of observer systems alone, which passes every one added. */
310
- const pass = System.create({ observer: true }, () => {});
310
+ const pass = System.create({ readonly: true }, () => {});
311
311
  /** A count of each bucket, written only when the index's entries changed: by `changed: true`, or by its lists' `changed`. */
312
312
  const countsWhenChanged = (filtered: boolean): System => filtered
313
- ? System.create({ observer: true, lookup: { byFlag: { index: byFlag, changed: true } } }, ({ lookup }) => {
313
+ ? System.create({ readonly: true, lookup: { byFlag: { index: byFlag, changed: true } } }, ({ lookup }) => {
314
314
  found += lookup.byFlag(false).length + lookup.byFlag(true).length;
315
315
  })
316
- : System.create({ observer: true, lookup: { byFlag: { index: byFlag } } }, ({ lookup }) => {
316
+ : System.create({ readonly: true, lookup: { byFlag: { index: byFlag } } }, ({ lookup }) => {
317
317
  const off = lookup.byFlag(false);
318
318
  const on = lookup.byFlag(true);
319
319
  if (!off.changed && !on.changed) return;
@@ -12,7 +12,7 @@ export const loopTicks = 10_000;
12
12
  export const observerIdles = 100;
13
13
 
14
14
  /** Observer systems no entity matches, each run after every tick. */
15
- const idleObserver = Array.from({ length: observerIdles }, (_, i) => System.create({ observer: true, components: { with: [unused] } }, () => {}));
15
+ const idleObserver = Array.from({ length: observerIdles }, (_, i) => System.create({ readonly: true, components: { with: [unused] } }, () => {}));
16
16
 
17
17
  /**
18
18
  * The fastest of `stretches` stretches of `loopTicks` ticks with nothing changed, after one to warm up: a world with
@@ -14,14 +14,14 @@ import type { World } from "../interface/world/world.js";
14
14
  * first call only starts the stretch that warms up.
15
15
  */
16
16
  export const eachTick = (world: World, notify: () => void): (() => void) => {
17
- const counter = System.create({ observer: true, resources: { changed: [clock] } }, () => { notify(); });
17
+ const counter = System.create({ readonly: true, resources: { changed: [clock] } }, () => { notify(); });
18
18
  world.add(counter);
19
19
  return () => world.remove(counter);
20
20
  };
21
21
 
22
22
  /** The clock as an observer run last read it. */
23
23
  const read = { tick: 0, now: 0 };
24
- const readClock = System.create({ observer: true }, ({ resources }) => {
24
+ const readClock = System.create({ readonly: true }, ({ resources }) => {
25
25
  read.tick = resources[clock].tick;
26
26
  read.now = resources[clock].now;
27
27
  });
@@ -36,7 +36,7 @@ export const ticksAfter = async (world: World, count: number): Promise<void> =>
36
36
  let { tick, now } = read;
37
37
  let seen = 0;
38
38
  await new Promise<void>(resolve => {
39
- const counter = System.create({ observer: true }, ({ resources }) => {
39
+ const counter = System.create({ readonly: true }, ({ resources }) => {
40
40
  const current = resources[clock];
41
41
  if (current.tick === tick && current.now === now) return;
42
42
  tick = current.tick;
@@ -52,7 +52,7 @@ export const ticksAfter = async (world: World, count: number): Promise<void> =>
52
52
  /** The entities with every component of `has` and none of `not`, in storage order, read by an observer run. */
53
53
  export const entitiesWith = async (world: World, has: readonly Component.Key[], not: readonly Component.Key[] = []): Promise<Entity[]> => {
54
54
  const found: Entity[] = [];
55
- await world.run(System.create({ observer: true, components: { with: [...has], not } }, row => { found.push(row.entity); }));
55
+ await world.run(System.create({ readonly: true, components: { with: [...has], not } }, row => { found.push(row.entity); }));
56
56
  return found;
57
57
  };
58
58
 
@@ -78,7 +78,7 @@ const lookups = async (system: System): Promise<{ readonly best: number; readonl
78
78
  /** Every entity a seeded world holds. */
79
79
  const entitiesOf = async (world: World): Promise<readonly Entity[]> => {
80
80
  const found: Entity[] = [];
81
- await world.run(System.create({ observer: true, components: { with: [value] } }, row => { found.push(row.entity); }));
81
+ await world.run(System.create({ readonly: true, components: { with: [value] } }, row => { found.push(row.entity); }));
82
82
  return found;
83
83
  };
84
84
 
@@ -124,7 +124,7 @@ export const readBenchmarks: readonly Benchmark[] = [
124
124
  run: async () => {
125
125
  const world = await seeded(10_000);
126
126
  copied = 0;
127
- const best = await fastest(() => world.run(System.create({ observer: true }, ({ resources }) => { copied = resources[level]; })), 300);
127
+ const best = await fastest(() => world.run(System.create({ readonly: true }, ({ resources }) => { copied = resources[level]; })), 300);
128
128
  check(copied === 3, "read run observer: wrong value");
129
129
  await world.dispose();
130
130
  return best;
@@ -49,7 +49,7 @@ export const reorder = System.create([flip, reverse]);
49
49
  export const observerSeen = { last: 0, notified: 0 };
50
50
 
51
51
  /** On a joined thread, after each sync point that changed it: the observed resource. */
52
- export const observerCounter = System.create({ observer: true, resources: { changed: [counter] } }, ({ resources }) => {
52
+ export const observerCounter = System.create({ readonly: true, resources: { changed: [counter] } }, ({ resources }) => {
53
53
  observerSeen.last = resources[counter];
54
54
  observerSeen.notified++;
55
55
  });
@@ -61,7 +61,7 @@ const sortedKeys = { keys: new Float64Array(0), entities: new Float64Array(0), p
61
61
  * On a joined thread, after each sync point: the entities in `order`, sorted from a scan of the index of every ranked
62
62
  * entity into reused arrays.
63
63
  */
64
- export const observerSelect = System.create({ observer: true, lookup: { all: { index: ranked } } }, ({ lookup }) => {
64
+ export const observerSelect = System.create({ readonly: true, lookup: { all: { index: ranked } } }, ({ lookup }) => {
65
65
  const list = lookup.all();
66
66
  const count = list.length;
67
67
  if (sortedKeys.keys.length < count) {
@@ -99,12 +99,12 @@ export const retext = System.create([count, recaption]);
99
99
  export const reamounted = System.create([count, reamount]);
100
100
 
101
101
  /** On a joined thread, after each sync point: every row's caption, out of its record. */
102
- export const observerCaptions = System.create({ observer: true, components: { with: [caption] } }, row => {
102
+ export const observerCaptions = System.create({ readonly: true, components: { with: [caption] } }, row => {
103
103
  observerSeen.last += row[caption].length;
104
104
  observerSeen.notified++;
105
105
  });
106
106
  /** On a joined thread, after each sync point: every row's amount. */
107
- export const observerAmounts = System.create({ observer: true, components: { with: [amount] } }, row => {
107
+ export const observerAmounts = System.create({ readonly: true, components: { with: [amount] } }, row => {
108
108
  observerSeen.last += row[amount];
109
109
  observerSeen.notified++;
110
110
  });
@@ -129,7 +129,7 @@ const joinedFile = "./joined.js";
129
129
  /** The observed resource, read by an observer run. */
130
130
  const counterOf = async (world: World): Promise<number> => {
131
131
  let value = 0;
132
- await world.run(System.create({ observer: true }, ({ resources }) => { value = resources[counter]; }));
132
+ await world.run(System.create({ readonly: true }, ({ resources }) => { value = resources[counter]; }));
133
133
  return value;
134
134
  };
135
135
 
@@ -80,7 +80,7 @@ const read = (system: System, name: string) => measure(system, async world => {
80
80
  /** Each row's text, as the last write left it. */
81
81
  const written = (name: string) => async (world: World): Promise<void> => {
82
82
  let wrong = 0;
83
- await world.run(System.create({ observer: true, components: { with: [text, boxed] } }, row => {
83
+ await world.run(System.create({ readonly: true, components: { with: [text, boxed] } }, row => {
84
84
  if (row[text] !== row[boxed].text && row[text] !== labelOf(row.entity, round) && row[boxed].text !== labelOf(row.entity, round)) wrong++;
85
85
  }));
86
86
  check(wrong === 0, `${name}: wrong text`);
@@ -196,7 +196,7 @@ export const idle = System.create({ components: { with: [unused] } }, () => {});
196
196
  /** A system that throws, so each run of it rejects. */
197
197
  export const failing = System.create({}, () => { throw new Error("suite: failing"); });
198
198
  /** An observer system no entity matches: a run of it alone has no tiers. */
199
- export const idleObserver = System.create({ observer: true, components: { with: [unused] } }, () => {});
199
+ export const idleObserver = System.create({ readonly: true, components: { with: [unused] } }, () => {});
200
200
 
201
201
  /** Fifty systems no entity matches, in one tier. */
202
202
  export const idles = System.create({
package/src/world.test.ts CHANGED
@@ -23,7 +23,7 @@ describe("World.create", () => {
23
23
  await world.run(spawn);
24
24
  await world.run(add);
25
25
  let seen = 0;
26
- await world.run(System.create({ observer: true, components: { with: [count] } }, row => { seen = row[count]; }));
26
+ await world.run(System.create({ readonly: true, components: { with: [count] } }, row => { seen = row[count]; }));
27
27
  expect(seen).toBe(2);
28
28
  });
29
29
  it("rejects a key that isn't a resource, as worlds the implementation creates do", () => {
@@ -40,7 +40,7 @@ describe("World.join", () => {
40
40
  await shared;
41
41
  await joined.run(raise);
42
42
  let seen = 0;
43
- await world.run(System.create({ observer: true }, ({ resources }) => { seen = resources[total]; }));
43
+ await world.run(System.create({ readonly: true }, ({ resources }) => { seen = resources[total]; }));
44
44
  expect(seen).toBe(7);
45
45
  });
46
46
  });
package/src/world.ts ADDED
@@ -0,0 +1,32 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import { create as createWorld, join as joinWorld } from './implementation/index.js';
3
+ import type { Create } from './interface/world/create.js';
4
+ import type { Join } from './interface/world/join.js';
5
+
6
+ export type { Table } from './interface/world/table/table.js';
7
+
8
+ /**
9
+ * Creates a world on this thread, synchronously, with resources starting at the given values in place of their
10
+ * defaults. This thread is its `"world"` thread for the world's life: it runs the sync points, the ticks and the GPU
11
+ * device, and fills every logical thread no joined thread claims. Keys are the resources' symbols, built-in ones
12
+ * included (`{ [workers]: 0 }`); string keys are never a resource's, so they're ignored. Throws for a symbol that isn't
13
+ * a resource, or is one only the world writes. While a declaration's trait or default doesn't hold, each call fails to
14
+ * compile, once, naming each such key, its members and what's wrong with them.
15
+ * @function
16
+ */
17
+ export const create: Create = createWorld;
18
+
19
+ /**
20
+ * Joins, from another thread, a world that `share` let join through the other end of `port`, claiming the logical
21
+ * `threads` this thread fills: their objects live here, and the systems and finalizers reading them run here. Resolves,
22
+ * once the world accepted the claim, to the same world: its `run` runs systems there, and its `add` and `remove` take
23
+ * readonly systems, which run here after the world's sync points. Add them before awaiting anything else, so the
24
+ * world's `share` resolves with them in place. Its `dispose` disconnects this thread. Rejects with an `Error` saying
25
+ * why it didn't join: the world refused the claim, was disposed, or its thread ended first, as the platform reports it:
26
+ * the world's `Worker` failing to start or throwing first, a port closed, as in node, or, in a browser, the world's
27
+ * thread ending once it was shared through a port this thread listened on. A browser world's worker that the page ended
28
+ * before this thread heard from it isn't reported, so the join stays pending. Rejects too where memory can't be shared,
29
+ * as without cross-origin isolation.
30
+ * @function
31
+ */
32
+ export const join: Join = joinWorld;