@oasys/oecs 0.5.2 → 0.5.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 (189) hide show
  1. package/CHANGELOG.md +208 -14
  2. package/README.md +198 -145
  3. package/dist/core/ecs/access_check.d.cts +3 -3
  4. package/dist/core/ecs/access_check.d.ts +3 -3
  5. package/dist/core/ecs/archetype.d.cts +103 -43
  6. package/dist/core/ecs/archetype.d.ts +103 -43
  7. package/dist/core/ecs/archetype.d.ts.map +1 -1
  8. package/dist/core/ecs/archetype_graph.d.cts +8 -8
  9. package/dist/core/ecs/archetype_graph.d.ts +8 -8
  10. package/dist/core/ecs/archetype_graph.d.ts.map +1 -1
  11. package/dist/core/ecs/builtin_relations.d.cts +1 -1
  12. package/dist/core/ecs/builtin_relations.d.ts +1 -1
  13. package/dist/core/ecs/command_log.d.cts +4 -4
  14. package/dist/core/ecs/command_log.d.ts +4 -4
  15. package/dist/core/ecs/command_log.d.ts.map +1 -1
  16. package/dist/core/ecs/component.d.cts +4 -4
  17. package/dist/core/ecs/component.d.ts +4 -4
  18. package/dist/core/ecs/deferred_commands.d.cts +1 -1
  19. package/dist/core/ecs/deferred_commands.d.ts +1 -1
  20. package/dist/core/ecs/deferred_commands.d.ts.map +1 -1
  21. package/dist/core/ecs/ecs.d.cts +85 -43
  22. package/dist/core/ecs/ecs.d.ts +85 -43
  23. package/dist/core/ecs/ecs.d.ts.map +1 -1
  24. package/dist/core/ecs/ecs_memory.d.cts +15 -15
  25. package/dist/core/ecs/ecs_memory.d.ts +15 -15
  26. package/dist/core/ecs/ecs_memory.d.ts.map +1 -1
  27. package/dist/core/ecs/entity_allocator.d.cts +6 -6
  28. package/dist/core/ecs/entity_allocator.d.ts +6 -6
  29. package/dist/core/ecs/entity_allocator.d.ts.map +1 -1
  30. package/dist/core/ecs/event.d.cts +2 -2
  31. package/dist/core/ecs/event.d.ts +2 -2
  32. package/dist/core/ecs/event_registry.d.cts +1 -1
  33. package/dist/core/ecs/event_registry.d.ts +1 -1
  34. package/dist/core/ecs/event_registry.d.ts.map +1 -1
  35. package/dist/core/ecs/facades.d.cts +15 -15
  36. package/dist/core/ecs/facades.d.ts +15 -15
  37. package/dist/core/ecs/facades.d.ts.map +1 -1
  38. package/dist/core/ecs/frame_trace.d.cts +2 -2
  39. package/dist/core/ecs/frame_trace.d.ts +2 -2
  40. package/dist/core/ecs/host_commands.d.cts +12 -12
  41. package/dist/core/ecs/host_commands.d.ts +12 -12
  42. package/dist/core/ecs/host_commands.d.ts.map +1 -1
  43. package/dist/core/ecs/index.d.cts +1 -1
  44. package/dist/core/ecs/index.d.ts +1 -1
  45. package/dist/core/ecs/index.d.ts.map +1 -1
  46. package/dist/core/ecs/observer.d.cts +9 -9
  47. package/dist/core/ecs/observer.d.ts +9 -9
  48. package/dist/core/ecs/observer.d.ts.map +1 -1
  49. package/dist/core/ecs/query.d.cts +91 -70
  50. package/dist/core/ecs/query.d.ts +91 -70
  51. package/dist/core/ecs/query.d.ts.map +1 -1
  52. package/dist/core/ecs/ref.d.cts +53 -1
  53. package/dist/core/ecs/ref.d.ts +53 -1
  54. package/dist/core/ecs/ref.d.ts.map +1 -1
  55. package/dist/core/ecs/relation.d.cts +15 -15
  56. package/dist/core/ecs/relation.d.ts +15 -15
  57. package/dist/core/ecs/relation.d.ts.map +1 -1
  58. package/dist/core/ecs/relation_service.d.cts +14 -14
  59. package/dist/core/ecs/relation_service.d.ts +14 -14
  60. package/dist/core/ecs/relation_service.d.ts.map +1 -1
  61. package/dist/core/ecs/resource.d.cts +1 -1
  62. package/dist/core/ecs/resource.d.ts +1 -1
  63. package/dist/core/ecs/resource_registry.d.cts +2 -2
  64. package/dist/core/ecs/resource_registry.d.ts +2 -2
  65. package/dist/core/ecs/resume.d.cts +3 -3
  66. package/dist/core/ecs/resume.d.ts +3 -3
  67. package/dist/core/ecs/resume.d.ts.map +1 -1
  68. package/dist/core/ecs/schedule.d.cts +22 -9
  69. package/dist/core/ecs/schedule.d.ts +22 -9
  70. package/dist/core/ecs/schedule.d.ts.map +1 -1
  71. package/dist/core/ecs/snapshot_service.d.cts +3 -3
  72. package/dist/core/ecs/snapshot_service.d.ts +3 -3
  73. package/dist/core/ecs/sparse_store.d.cts +6 -6
  74. package/dist/core/ecs/sparse_store.d.ts +6 -6
  75. package/dist/core/ecs/sparse_store.d.ts.map +1 -1
  76. package/dist/core/ecs/store.d.cts +189 -110
  77. package/dist/core/ecs/store.d.ts +189 -110
  78. package/dist/core/ecs/store.d.ts.map +1 -1
  79. package/dist/core/ecs/store_layout_listener.d.cts +2 -2
  80. package/dist/core/ecs/store_layout_listener.d.ts +2 -2
  81. package/dist/core/ecs/system.d.cts +8 -8
  82. package/dist/core/ecs/system.d.ts +8 -8
  83. package/dist/core/ecs/system.d.ts.map +1 -1
  84. package/dist/core/ecs/utils/error.d.cts +7 -0
  85. package/dist/core/ecs/utils/error.d.ts +7 -0
  86. package/dist/core/ecs/utils/error.d.ts.map +1 -1
  87. package/dist/core/store/action_ring.d.cts +7 -8
  88. package/dist/core/store/action_ring.d.ts +7 -8
  89. package/dist/core/store/action_ring.d.ts.map +1 -1
  90. package/dist/core/store/allocator.d.cts +58 -35
  91. package/dist/core/store/allocator.d.ts +58 -35
  92. package/dist/core/store/allocator.d.ts.map +1 -1
  93. package/dist/core/store/buffer_backed_column.d.cts +5 -6
  94. package/dist/core/store/buffer_backed_column.d.ts +5 -6
  95. package/dist/core/store/buffer_backed_column.d.ts.map +1 -1
  96. package/dist/core/store/column_store.d.cts +23 -23
  97. package/dist/core/store/column_store.d.ts +23 -23
  98. package/dist/core/store/column_store.d.ts.map +1 -1
  99. package/dist/core/store/command_dispatch.d.cts +1 -1
  100. package/dist/core/store/command_dispatch.d.ts +1 -1
  101. package/dist/core/store/command_ring.d.cts +6 -7
  102. package/dist/core/store/command_ring.d.ts +6 -7
  103. package/dist/core/store/command_ring.d.ts.map +1 -1
  104. package/dist/core/store/descriptor.d.cts +2 -2
  105. package/dist/core/store/descriptor.d.ts +2 -2
  106. package/dist/core/store/entity_index.d.cts +3 -3
  107. package/dist/core/store/entity_index.d.ts +3 -3
  108. package/dist/core/store/event_ring.d.cts +7 -7
  109. package/dist/core/store/event_ring.d.ts +7 -7
  110. package/dist/core/store/extend.d.cts +2 -2
  111. package/dist/core/store/extend.d.ts +2 -2
  112. package/dist/core/store/extend.d.ts.map +1 -1
  113. package/dist/core/store/grow.d.cts +1 -1
  114. package/dist/core/store/grow.d.ts +1 -1
  115. package/dist/core/store/grow.d.ts.map +1 -1
  116. package/dist/core/store/header.d.cts +9 -9
  117. package/dist/core/store/header.d.ts +9 -9
  118. package/dist/core/store/header.d.ts.map +1 -1
  119. package/dist/core/store/layout_ops.d.cts +25 -7
  120. package/dist/core/store/layout_ops.d.ts +25 -7
  121. package/dist/core/store/layout_ops.d.ts.map +1 -1
  122. package/dist/core/store/state_hash.d.cts +3 -3
  123. package/dist/core/store/state_hash.d.ts +3 -3
  124. package/dist/extensions/editor/editor.d.cts +4 -4
  125. package/dist/extensions/editor/editor.d.ts +4 -4
  126. package/dist/extensions/editor/editor.d.ts.map +1 -1
  127. package/dist/extensions/editor/field_handle.d.cts +1 -1
  128. package/dist/extensions/editor/field_handle.d.ts +1 -1
  129. package/dist/extensions/editor/index.d.cts +2 -2
  130. package/dist/extensions/editor/index.d.ts +2 -2
  131. package/dist/extensions/editor/index.development.js +4 -4
  132. package/dist/extensions/editor/index.js +4 -4
  133. package/dist/extensions/reactive/ecs_sync.d.cts +4 -4
  134. package/dist/extensions/reactive/ecs_sync.d.ts +4 -4
  135. package/dist/extensions/reactive/index.d.cts +4 -4
  136. package/dist/extensions/reactive/index.d.ts +4 -4
  137. package/dist/extensions/reactive/index.development.js +3 -3
  138. package/dist/extensions/reactive/index.js +3 -3
  139. package/dist/extensions/solid/index.d.cts +1 -1
  140. package/dist/extensions/solid/index.d.ts +1 -1
  141. package/dist/extensions/solid/index.development.js +1 -1
  142. package/dist/extensions/solid/index.js +1 -1
  143. package/dist/extensions/solid/kernel_solid.d.cts +1 -1
  144. package/dist/extensions/solid/kernel_solid.d.ts +1 -1
  145. package/dist/extensions/solid/kernel_solid.d.ts.map +1 -1
  146. package/dist/{host_commands-BI8pEmjH.js → host_commands-BlR3oZ3x.js} +473 -393
  147. package/dist/host_commands-C2cpU3yC.cjs +1 -0
  148. package/dist/index.cjs +1 -1
  149. package/dist/index.d.cts +2 -2
  150. package/dist/index.d.ts +2 -2
  151. package/dist/index.d.ts.map +1 -1
  152. package/dist/index.development.cjs +1 -1
  153. package/dist/index.development.js +3345 -2963
  154. package/dist/index.js +1496 -1131
  155. package/dist/{internal-BMDgWZbt.js → internal-BoxWl4dG.js} +410 -330
  156. package/dist/internal-CGBEwfeB.cjs +2 -0
  157. package/dist/internal.cjs +1 -1
  158. package/dist/internal.development.cjs +1 -1
  159. package/dist/internal.development.js +1 -1
  160. package/dist/internal.js +1 -1
  161. package/dist/primitives.development.js +1 -1
  162. package/dist/primitives.js +1 -1
  163. package/dist/reactive/index.d.cts +2 -2
  164. package/dist/reactive/index.d.ts +2 -2
  165. package/dist/reactive/index.development.js +1 -1
  166. package/dist/reactive/index.js +1 -1
  167. package/dist/reactive/kernel.d.cts +5 -5
  168. package/dist/reactive/kernel.d.ts +5 -5
  169. package/dist/reactive/shallow.d.cts +1 -1
  170. package/dist/reactive/shallow.d.ts +1 -1
  171. package/dist/{shallow-DfnAm9vG.js → shallow-Bs39EPh1.js} +1 -1
  172. package/dist/{shallow-9S1pY_Iw.js → shallow-CiFC70Dz.js} +1 -1
  173. package/dist/{shared-BU1Cd40h.js → shared-B4v0L1cw.js} +41 -35
  174. package/dist/shared-DU78k0pU.cjs +1 -0
  175. package/dist/shared.cjs +1 -1
  176. package/dist/shared.d.cts +1 -1
  177. package/dist/shared.d.ts +1 -1
  178. package/dist/shared.development.cjs +1 -1
  179. package/dist/shared.development.js +1 -1
  180. package/dist/shared.js +1 -1
  181. package/dist/type_primitives/typed_arrays/typed_arrays.d.cts +2 -2
  182. package/dist/type_primitives/typed_arrays/typed_arrays.d.ts +2 -2
  183. package/dist/{typed_arrays-BcXcnzB4.js → typed_arrays-DIVwVy5_.js} +1 -1
  184. package/dist/version.d.cts +2 -2
  185. package/dist/version.d.ts +2 -2
  186. package/package.json +1 -1
  187. package/dist/host_commands-CxhpzMx9.cjs +0 -1
  188. package/dist/internal-LiTSB_tG.cjs +0 -2
  189. package/dist/shared-BymrGTyR.cjs +0 -1
package/README.md CHANGED
@@ -1,22 +1,34 @@
1
1
  # oecs
2
2
 
3
- **A full-featured, archetype-based Entity Component System for TypeScript.**
4
-
5
- `@oasys/oecs` is a complete ECS not just storage-and-queries, but the whole toolkit you expect from a
6
- mature engine: observers, relations with wildcards, sparse storage, system sets and run conditions,
7
- entity enable/disable, templates, deterministic hashing with snapshot/restore, a typed host→ECS write
8
- seam, and an optional reactive UI bridge. It is **pure TypeScript and zero-dependency by default** — it
9
- runs over a plain resizable `ArrayBuffer`, so it needs no `SharedArrayBuffer` and no cross-origin
10
- isolation (COOP/COEP). An opt-in shared-memory profile swaps in a `SharedArrayBuffer` for worker offload
11
- or a WASM compute backend; both profiles share one core and agree, byte-for-byte, on `stateHash`.
12
-
13
- - **Fast** — struct-of-arrays column storage grouped by archetype; iteration is a tight loop over typed
14
- arrays with no per-entity object allocation.
15
- - **Type-safe** component handles are callable defs with stable numeric ids at runtime and fully-typed schemas at compile time;
16
- misspelled fields are compile errors.
17
- - **Deterministic** — an opt-in mode gives a backing-agnostic `stateHash` plus snapshot/restore and
18
- command-log replay.
19
- - **Complete** the feature surface below is the whole engine, not a starting point.
3
+ **A complete, archetype-based Entity Component System for TypeScript.**
4
+
5
+ `@oasys/oecs` gives you more than storage and queries. It gives you the tools that a mature engine
6
+ has:
7
+
8
+ - observers;
9
+ - relations with wildcards;
10
+ - sparse storage;
11
+ - system sets and run conditions;
12
+ - enable and disable for an entity;
13
+ - templates;
14
+ - deterministic hashing, with snapshot and restore;
15
+ - a typed write path from the host into the ECS;
16
+ - an optional reactive UI bridge.
17
+
18
+ The package is **pure TypeScript, and it has no dependencies by default**. It runs over one plain
19
+ `ArrayBuffer`. So it does not need a `SharedArrayBuffer`, and it does not need cross-origin
20
+ isolation (COOP/COEP). An optional shared-memory profile uses a `SharedArrayBuffer` instead. Use
21
+ that profile for worker offload, or for a WASM compute backend. The two profiles use one core, and
22
+ they agree byte-for-byte on `stateHash`.
23
+
24
+ - **Data-oriented** — Columns use struct-of-arrays storage, grouped by archetype. Iteration is a
25
+ small loop over typed arrays. The loop allocates no object for each entity.
26
+ - **Type-safe** — A component handle is a callable definition. It has a stable numeric id at run
27
+ time and a full schema type at compile time. A field name with a spelling error is a compile
28
+ error.
29
+ - **Deterministic** — An optional mode gives you a `stateHash` that is independent of the storage
30
+ type. It also gives you snapshot, restore, and replay of a command log.
31
+ - **Complete** — The features below are the full engine. They are not only a start.
20
32
 
21
33
  ## Installation
22
34
 
@@ -28,8 +40,10 @@ deno add jsr:@oasys/oecs # JSR (Deno)
28
40
  npx jsr add @oasys/oecs # JSR (npm-compatible)
29
41
  ```
30
42
 
31
- Requires a runtime with resizable `ArrayBuffer` (the default heap profile grows in place):
32
- Node 20, Deno 1.38, Chrome 111+, Firefox 128+, Safari 16.4+.
43
+ Supported runtimes: Node 20 or later, Deno 1.38 or later, Chrome 111 or later, Firefox 128 or
44
+ later, and Safari 16.4 or later. The default heap profile uses a plain, fixed `ArrayBuffer`. The
45
+ optional shared and WASM profiles need a growable `SharedArrayBuffer` or `WebAssembly.Memory`, and
46
+ those requirements set the version limits.
33
47
 
34
48
  ## Quick start
35
49
 
@@ -42,17 +56,17 @@ const ecs = new ECS(); // pure-TS heap profile — no SharedArrayBuffer needed
42
56
  const Pos = ecs.registerComponent({ x: "f64", y: "f64" });
43
57
  const Vel = ecs.registerComponent(["vx", "vy"] as const);
44
58
 
45
- // A query is a live, cached view over matching archetypes build it once, reuse it.
59
+ // A query is a live, cached view of the matching archetypes. Build it once, then use it again.
46
60
  const movers = ecs.query(Pos, Vel);
47
61
 
48
- // Systems declare the components they read/write (checked in dev builds).
62
+ // Systems declare the components that they read and write (checked in development builds).
49
63
  const move = ecs.registerSystem({
50
64
  reads: [Vel],
51
- writes: [Pos], // a declared write implies read of the same component
65
+ writes: [Pos], // a declared write also gives read access to the same component
52
66
  fn: (ctx, dt) => {
53
67
  movers.eachChunk((cols, count) => {
54
- const { x, y } = cols.mut(Pos); // whole group; stamps Pos's change tick once
55
- const { vx, vy } = cols.read(Vel); // read-only group
68
+ const { x, y } = cols.mut(Pos); // the full group; sets the change tick of Pos one time
69
+ const { vx, vy } = cols.read(Vel); // a read-only group
56
70
  for (let i = 0; i < count; i++) {
57
71
  x[i] += vx[i] * dt;
58
72
  y[i] += vy[i] * dt;
@@ -69,141 +83,180 @@ ecs.addComponent(e, Pos, { x: 0, y: 0 });
69
83
  ecs.addComponent(e, Vel, { vx: 100, vy: 50 });
70
84
 
71
85
  ecs.update(1 / 60);
72
- ecs.getField(e, Pos, "x"); // 1.667
86
+ ecs.getField(e, Pos, "x"); // about 1.667
73
87
  ```
74
88
 
75
89
  ## Features
76
90
 
77
- **Storage & data model**
78
-
79
- - **Archetype SoA storage** over a backing-neutral `ColumnStore` — entities with the same component set
80
- share contiguous typed-array columns; cache-friendly loops, no per-entity object allocation.
81
- - **Phantom-typed components** `registerComponent({ x: "f64", y: "f64" })` returns a callable
82
- `ComponentDef` with a stable numeric `.id` at runtime and a fully-typed schema at compile time.
83
- Record syntax for per-field types, array shorthand for uniform `f64`, and `registerTag()` for
84
- data-free markers. Field types: `f32 f64 i8 i16 i32 u8 u16 u32`.
85
- - **Two storage profiles, one core** pure-TS heap (`ArrayBuffer`) by default; opt-in
86
- `SharedArrayBuffer` for workers / WASM. Same code path, same `stateHash`, sized through a single
87
- `memory` surface (entity budget, byte cap, or pinned capacity).
91
+ **Storage and the data model**
92
+
93
+ - **Archetype storage in struct-of-arrays form**, above a storage-neutral `ColumnStore`. Entities
94
+ with the same set of components share adjacent typed-array columns. Loops use the cache well, and
95
+ they allocate no object for each entity.
96
+ - **Components with phantom types** `registerComponent({ x: "f64", y: "f64" })` gives you a
97
+ callable `ComponentDef`. The definition has a stable numeric `.id` at run time and a full schema
98
+ type at compile time. Use the record syntax for different types in each field. Use the array
99
+ shorthand when all fields are `f64`. Use `registerTag()` for markers that hold no data. The field
100
+ types are `f32 f64 i8 i16 i32 u8 u16 u32`.
101
+ - **Two storage profiles, one core** The default is a pure-TS heap (`ArrayBuffer`). A
102
+ `SharedArrayBuffer` for workers or WASM is optional. The code path is the same, the `stateHash`
103
+ is the same, and one `memory` option sets the size (an entity budget, a byte limit, or a fixed
104
+ capacity).
88
105
 
89
106
  **Queries**
90
107
 
91
- - **Live, cached queries** — `ecs.query(Pos, Vel)` refined with `.and()` / `.without()` / `.anyOf()`;
92
- new matching archetypes are pushed in automatically.
93
- - **Two iteration verbs** — `forEach(arch => …)` for read-only archetype iteration, `eachChunk((cols, count) => …)`
94
- for the mutable hot path (`cols.mut` / `cols.read` resolve a whole component's columns at once).
95
- - **Change detection** per-`(archetype, component)` change ticks; `query.changed(Pos)` visits only
96
- archetypes written since the system's threshold tick.
97
- - **Relation & hierarchy queries** `(R, *)` / `(*, T)` wildcards, `forEachRelatedTo`, and
98
- `query.hierarchy(rel, depth)`. **Sparse queries** via `query.withSparse(...)`; disabled entities are
99
- skipped unless you opt in with `query.includeDisabled()`.
100
-
101
- **Systems & scheduling**
102
-
103
- - **Declarative systems** — plain functions in a `SystemConfig` declaring `reads` / `writes`, enforced by
104
- a dev-mode access checker (tree-shaken in production). Bare `(ctx, dt)` and `(q, ctx, dt)` +
105
- query-builder overloads exist for no-access glue; lifecycle hooks `onAdded` / `onRemoved` / `dispose`;
106
- `exclusive: true` for full-world setup/teardown.
107
- - **Topological scheduler** seven phases (`PRE_STARTUP` `STARTUP` `POST_STARTUP`, `FIXED_UPDATE`,
108
- `PRE_UPDATE` `UPDATE` `POST_UPDATE`); per-phase Kahn sort by `before` / `after`, with insertion
109
- order as a deterministic tiebreaker. Always-on cycle detection.
110
- - **Fixed timestep** accumulator loop with configurable `fixedTimestep` and spiral-of-death protection.
111
- - **System sets & run conditions** `systemSet(...)` + `configureSet(...)`; `runIfResourceEq`,
112
- `runEveryNTicks`, `runIfAnyMatch`, and custom `RunCondition`s.
108
+ - **Live, cached queries** — Write `ecs.query(Pos, Vel)`, then make it more exact with `.and()`,
109
+ `.without()`, or `.anyOf()`. The store adds new matching archetypes to the query automatically.
110
+ - **Two iteration verbs** — Use `forEach(arch => …)` to read archetypes. Use
111
+ `eachChunk((cols, count) => …)` for the high-frequency loop that writes. In that loop, `cols.mut`
112
+ and `cols.read` give you all the columns of one component at the same time.
113
+ - **Change detection** Each `(archetype, component)` pair has a change tick.
114
+ `query.changed(Pos)` visits only the archetypes that changed at or after the threshold tick of
115
+ the system.
116
+ - **Queries for relations and hierarchies** — Use the wildcards `(R, *)` and `(*, T)`, plus
117
+ `forEachRelatedTo` and `query.hierarchy(rel, depth)`. For **sparse queries**, use
118
+ `query.withSparse(...)`. Queries skip disabled entities. To include them, use
119
+ `query.includeDisabled()`.
120
+
121
+ **Systems and the schedule**
122
+
123
+ - **Systems that declare their access** — A system is a plain function in a `SystemConfig` that
124
+ declares `reads` and `writes`. A development-mode access checker holds you to that declaration,
125
+ and the build tool removes that checker from a production build. There are also `(ctx, dt)` and
126
+ `(q, ctx, dt)`
127
+ forms with a query builder, for connection code that touches no data. The lifecycle hooks are
128
+ `onAdded`, `onRemoved`, and `dispose`. Set `exclusive: true` for full-world setup or teardown.
129
+ - **A topological scheduler** — There are seven phases: `PRE_STARTUP`, `STARTUP`, `POST_STARTUP`,
130
+ `FIXED_UPDATE`, `PRE_UPDATE`, `UPDATE`, and `POST_UPDATE`. Each phase does a Kahn sort on the
131
+ `before` and `after` constraints. Insertion order breaks a tie, which keeps the result
132
+ deterministic. Cycle detection is always active.
133
+ - **A fixed timestep** — An accumulator loop uses the `fixedTimestep` value that you set. A limit
134
+ protects against the spiral of death.
135
+ - **System sets and run conditions** — Use `systemSet(...)` with `configureSet(...)`. The supplied
136
+ conditions are `runIfResourceEq`, `runEveryNTicks`, and `runIfAnyMatch`. You can also write your
137
+ own `RunCondition`.
113
138
 
114
139
  **Structural changes**
115
140
 
116
- - **Deferred inside systems, immediate on the host** — `ctx.commands` (a Bevy-`Commands`-style
117
- facade) buffers add / remove / despawn / enable / disable until the phase flush, so iterators
118
- stay valid (`commands.spawn` returns the id immediately; its component attaches are deferred). Every host-side mutation (`ecs.addComponent` / `removeComponent` /
119
- `despawn` / `disable` / `enable`) applies immediately.
120
- - **Entity enable/disable** `disable` / `enable` / `isDisabled`; disabled rows sit in a partitioned
121
- tail and are skipped by default queries.
122
- - **Templates & bundles** `ecs.template(Pos({ x, y }), …)` blueprints consumed by `spawn` /
123
- `spawnMany` for zero-transition spawns; the same callable-bundle varargs drive `spawnBundle(...)`
124
- and `addComponents(...)`.
125
-
126
- **Reactivity & relationships**
127
-
128
- - **Observers** `ecs.observe(...)` for `onAdd` / `onRemove` / `onSet` / `onEnable` / `onDisable`,
129
- structural or per-entity.
130
- - **Relations** — `(relation, target)` pairs with `ChildOf` / `IsA` presets, exclusive / multi arities,
131
- bidirectional queries (`targetOf` / `sourcesOf` / `ancestorsOf` / `rootOf` / `cascadeOf`), and
132
- configurable on-delete cleanup (`delete` / `clear` / `orphan`). Stored sparsely no archetype
133
- transition, no identity bit.
134
- - **Sparse storage** `registerSparseComponent` / `registerSparseTag`, `addSparse` / `removeSparse` for
135
- churny or rare data that shouldn't cause archetype transitions.
136
- - **Resources** typed global singletons via `resourceKey<T>`. **Events** — fire-and-forget SoA channels
137
- via `eventKey<F>` / `signalKey`, cleared at the end of each `update`.
138
- - **Cached refs** `ctx.ref(def, e)` (mutable, bumps the change tick) / `ctx.refRead(def, e)`
139
- (read-only): resolve archetype + row + column once, then `pos.x += vel.vx * dt`.
140
-
141
- **Determinism, persistence & integration**
142
-
143
- - **Determinism** (opt-in) `new ECS({ deterministic: true })`, then `ecs.snapshots.stateHash()` (an FNV-1a-style
144
- 32-bit digest over live dense bytes, sparse stores, and multi-relation target sets), `ecs.snapshots.capture()` /
145
- `ecs.snapshots.restore(...)`, plus sparse variants. Backing-agnostic: a heap world and a shared world with identical history produce
146
- identical hashes.
147
- - **Host ECS write seam** `installHostCommandSeam(ecs)` applies typed `HostCommand`s off-schedule
148
- via a blessed `exclusive` system, with record/replay (`HostCommandRecorder`, `replayCommandLog`) and a
149
- cross-thread ring transport.
150
- - **Reactive UI seam** (optional) a zero-dep signals kernel (`@oasys/oecs/reactive`), an ECS→reactive
151
- bridge that publishes only dirty entities/columns (`@oasys/oecs/reactive-sync`), and a SolidJS adapter
152
- (`@oasys/oecs/solid`).
153
- - **Editor layer** — undo/redo + field handles over the write seam (`@oasys/oecs/editor`).
154
- - **Frame tracing** `ecs.setTrace(sink)` + `FrameTraceRecorder` for a structured per-frame event
155
- stream (dev-gated).
156
- - **Compute backend seam** — `ecs.attachBackend(...)` to run a system body on a compiled backend (WASM,
157
- ) instead of its TS closure.
141
+ - **Deferred in a system, immediate on the host** — `ctx.commands` is a facade in the style of the
142
+ Bevy `Commands` type. It holds add, remove, despawn, enable, and disable operations until the
143
+ flush at the end of the phase, so that iterators stay correct. `commands.spawn` gives you the id
144
+ immediately, but it attaches the components later. Each mutation on the host
145
+ (`ecs.addComponent`, `ecs.removeComponent`, `ecs.despawn`, `ecs.disable`, and `ecs.enable`)
146
+ applies immediately.
147
+ - **Enable and disable for an entity** Use `disable`, `enable`, and `isDisabled`. Disabled rows
148
+ stay in a partition at the end of the archetype, and queries skip them by default.
149
+ - **Templates and bundles** — `ecs.template(Pos({ x, y }), …)` makes a blueprint. `spawn` and
150
+ `spawnMany` use that blueprint to create entities with no archetype transition. The same callable
151
+ bundles are the arguments to `spawnBundle(...)` and `addComponents(...)`.
152
+
153
+ **Reactions and relationships**
154
+
155
+ - **Observers** — `ecs.observe(...)` registers `onAdd`, `onRemove`, `onSet`, `onEnable`, and
156
+ `onDisable` callbacks, for a structure or for one entity.
157
+ - **Relations** A relation is a `(relation, target)` pair. The presets are `ChildOf` and `IsA`.
158
+ A relation is exclusive or multi. Queries go in both directions (`targetOf`, `sourcesOf`,
159
+ `ancestorsOf`, `rootOf`, and `cascadeOf`). The cleanup policy for a deleted target is
160
+ `delete`, `clear`, or `orphan`. Relations use sparse storage. So they cause no archetype
161
+ transition, and they use no identity bit.
162
+ - **Sparse storage** Use `registerSparseComponent` and `registerSparseTag`, then `addSparse` and
163
+ `removeSparse`. Sparse storage is correct for data that changes frequently or is rare, because it
164
+ causes no archetype transition.
165
+ - **Resources** — A resource is a typed global value, keyed with `resourceKey<T>`. **Events** are
166
+ send-and-forget channels in struct-of-arrays form, keyed with `eventKey<F>` or `signalKey`. The
167
+ ECS clears the events at the end of each `update`.
168
+ - **Cached refs** — `ctx.ref(def, e)` gives you a writable ref, and it sets the change tick.
169
+ `ctx.refRead(def, e)` gives you a read-only ref. A ref finds the archetype, the row, and the
170
+ columns one time. Then you can write `pos.x += vel.vx * dt`.
171
+ - **Cursors** — `ctx.cursor(def)` gives you an accessor that you can use again for a different
172
+ entity. `ctx.cursorRead(def)` gives you the read-only form. The same two functions are on `ecs`.
173
+ You make a cursor one time. Then each `at(entity)` call moves it to a different entity. Use a
174
+ cursor when you read or write many entities from a list of ids. The loop then makes no ref for
175
+ each entity, and the cursor finds the position of each field one time. Each `at` call finds the
176
+ archetype and the row again. Thus a structural change between two `at` calls cannot make a
177
+ cursor read a different entity.
178
+
179
+ **Determinism, storage of state, and integration**
180
+
181
+ - **Determinism** (optional) Construct the ECS with `new ECS({ deterministic: true })`. Then use
182
+ `ecs.snapshots.stateHash()`, which gives a 32-bit digest in FNV-1a style over the live dense
183
+ bytes, the sparse stores, and the target sets of multi relations. Also use
184
+ `ecs.snapshots.capture()`, `ecs.snapshots.restore(...)`, and the equivalent functions for sparse
185
+ data. The hash is independent of the storage type: a heap ECS and a shared ECS with the same
186
+ history give the same hash.
187
+ - **A write path from the host into the ECS** — `installHostCommandSeam(ecs)` applies typed
188
+ `HostCommand` values from outside the schedule, through one approved `exclusive` system. It
189
+ supports record and replay (`HostCommandRecorder` and `replayCommandLog`), and a ring transport
190
+ between threads.
191
+ - **A reactive UI connection** (optional) — There are three parts. `@oasys/oecs/reactive` is a
192
+ signals kernel with no dependencies. `@oasys/oecs/reactive-sync` is a bridge from the ECS to that
193
+ kernel, and it publishes only the changed entities and columns. `@oasys/oecs/solid` is a SolidJS
194
+ adapter.
195
+ - **An editor layer** — It adds undo, redo, and field handles above the host write path
196
+ (`@oasys/oecs/editor`).
197
+ - **Frame traces** — `ecs.setTrace(sink)` with `FrameTraceRecorder` gives you a structured stream
198
+ of the events in each frame. It is available in development builds only.
199
+ - **A compute backend connection** — `ecs.attachBackend(...)` runs the body of a system on a
200
+ compiled backend, such as WASM, instead of its TypeScript closure.
158
201
 
159
202
  **Reference**
160
203
 
161
- - **Typed errors** — an `ECSError` taxonomy with a `category` enum and an `isEcsError` guard, all exported.
162
- - **Reusable primitives** (`@oasys/oecs/primitives`) `BitSet`, `SparseSet`, `SparseMap`,
163
- `GrowableTypedArray`, `BinaryHeap`, and `topologicalSort`, usable standalone.
204
+ - **Typed errors** — There is an `ECSError` taxonomy with a `category` enum and an `isEcsError`
205
+ guard. The package exports all of them.
206
+ - **Primitives that you can use again** (`@oasys/oecs/primitives`) `BitSet`, `SparseSet`,
207
+ `SparseMap`, `GrowableTypedArray`, `BinaryHeap`, and `topologicalSort` also operate alone.
164
208
 
165
209
  ## Entry points
166
210
 
167
- The core is `@oasys/oecs`; everything else is opt-in and costs nothing until imported.
211
+ The core is `@oasys/oecs`. Each other entry point is optional, and it costs nothing until you
212
+ import it.
168
213
 
169
214
  | Import | What it is |
170
215
  | --- | --- |
171
- | `@oasys/oecs` | the ECS — pure-TS heap profile by default (production build; dev guards stripped) |
172
- | `@oasys/oecs/dev` | the same ECS with the dev guards **on** — for a direct guards-on import; see [Dev vs prod](#dev-vs-prod) |
173
- | `@oasys/oecs/shared` | opt-in `SharedArrayBuffer` allocators for worker offload / a WASM backend (needs COOP/COEP) |
174
- | `@oasys/oecs/reactive` | zero-dependency reactive kernel (`signal`/`computed`/`effect`, reactive collections) |
175
- | `@oasys/oecs/reactive-sync` | ECS→reactive bridge — publishes only dirty entities/columns |
176
- | `@oasys/oecs/editor` | undo/redo + field-handle layer over the host-write seam |
177
- | `@oasys/oecs/solid` | SolidJS adapter (`solid-js` is an **optional** peer dependency) |
178
- | `@oasys/oecs/primitives` | the standalone data structures oecs is built on |
179
- | `@oasys/oecs/internal` | unstable internals (codecs, ABI constants, access checker) — **no semver guarantees** |
180
-
181
- ## Dev vs prod
182
-
183
- A compile-time `__DEV__` flag gates every runtime check — bounds and liveness checks, duplicate-system
184
- detection, registration validation, and the system access checker (`reads`/`writes`). These are
185
- **tree-shaken out of production builds**, so treat "throws in dev" as a development tripwire, not a
186
- production guarantee. The scheduler's cycle detection and constructor-option validation (timestep,
187
- memory options, relation cardinality) are always active.
188
-
189
- **Production is the default on both channels; you opt *into* the guards.** On **npm**, `@oasys/oecs`
190
- is the stripped production build dev-mode bundlers (`vite dev`, `webpack --mode development`) pick
191
- the guards-on build automatically via the `development` export condition, or import `@oasys/oecs/dev`
192
- for it directly. On **JSR/Deno** (raw source, no bundler) the default is also production
193
- (`__DEV__ = false`); set `globalThis.__DEV__ = true` before the first import to turn the guards on
194
- while developing. Full details including the browser/CDN and manual-override paths are in the
195
- [**Development guards & production builds**](docs/PRODUCTION.md) guide.
216
+ | `@oasys/oecs` | the ECS — the pure-TS heap profile by default (a production build, with the development guards removed) |
217
+ | `@oasys/oecs/dev` | the same ECS with the development guards **on** — import this to get the guards directly; see [Development and production](#dev-vs-prod) |
218
+ | `@oasys/oecs/shared` | the optional `SharedArrayBuffer` allocators, for worker offload or a WASM backend (this needs COOP/COEP) |
219
+ | `@oasys/oecs/reactive` | the reactive kernel, which has no dependencies (`signal`, `computed`, `effect`, and reactive collections) |
220
+ | `@oasys/oecs/reactive-sync` | the bridge from the ECS to the kernel it publishes only the changed entities and columns |
221
+ | `@oasys/oecs/editor` | undo, redo, and field handles above the host write path |
222
+ | `@oasys/oecs/solid` | the SolidJS adapter (`solid-js` is an **optional** peer dependency) |
223
+ | `@oasys/oecs/primitives` | the data structures that oecs is built from, which also operate alone |
224
+ | `@oasys/oecs/internal` | unstable internal parts (codecs, ABI constants, the access checker) — **there are no semver guarantees** |
225
+
226
+ <a id="dev-vs-prod"></a>
227
+
228
+ ## Development and production
229
+
230
+ A compile-time flag, `__DEV__`, controls each run-time check. The checks include bounds and
231
+ liveness checks, detection of a system that you added two times, validation at registration, and
232
+ the system access checker for `reads` and `writes`. The build tool **removes these checks from a
233
+ production build**. So, when the documentation says that an operation "throws in development",
234
+ that behavior is a development aid. It is not a production guarantee. Two checks stay active in
235
+ each build: cycle detection in the scheduler, and validation of the constructor options (the
236
+ timestep, the memory options, and the cardinality of a relation).
237
+
238
+ **Production is the default on both channels. You must turn the guards on.** On **npm**,
239
+ `@oasys/oecs` is the production build, with the guards removed. A bundler in development mode
240
+ (`vite dev` or `webpack --mode development`) selects the build with the guards automatically,
241
+ through the `development` export condition. As an alternative, import `@oasys/oecs/dev` directly.
242
+ On **JSR and Deno** there is no bundler, because the package is raw source. The default is also
243
+ production (`__DEV__ = false`). To turn the guards on while you develop, set
244
+ `globalThis.__DEV__ = true` before the first import. For the full details, which include the
245
+ browser, CDN, and manual paths, read the
246
+ [**Development guards and production builds**](docs/PRODUCTION.md) guide.
196
247
 
197
248
  ## Documentation
198
249
 
199
- - **New to oecs?** Start with the [Getting Started](docs/GETTING_STARTED.md) tutorial, then
200
- [Best Practices](docs/BEST_PRACTICES.md) and the [Architecture](docs/ARCHITECTURE.md) overview.
201
- - **Using optional extensions?** See the [Extensions guide](docs/EXTENSIONS.md) for reactive UI,
202
- editor, Solid, shared-memory, and primitives usage.
203
- - **Upgrading from 0.4?** See the [Migration guide (0.4 0.5)](docs/MIGRATION-0.4-to-0.5.md) and the
204
- [CHANGELOG](CHANGELOG.md).
205
- - **Upgrading from 0.3?** See the [Migration guide (0.3 0.4)](docs/MIGRATION-0.3-to-0.4.md).
206
- - **Full API reference** start at the [reference index](docs/api/index.md):
250
+ - **If oecs is new to you**, start with the [Getting started](docs/GETTING_STARTED.md) tutorial.
251
+ Then read [Best practices](docs/BEST_PRACTICES.md) and the
252
+ [Architecture](docs/ARCHITECTURE.md) overview.
253
+ - **If you use the optional extensions**, read the [Extensions guide](docs/EXTENSIONS.md) for the
254
+ reactive UI, the editor, Solid, shared memory, and the primitives.
255
+ - **If you upgrade from 0.4**, read the
256
+ [Migration guide (0.4 to 0.5)](docs/MIGRATION-0.4-to-0.5.md) and the [CHANGELOG](CHANGELOG.md).
257
+ - **If you upgrade from 0.3**, read the
258
+ [Migration guide (0.3 to 0.4)](docs/MIGRATION-0.3-to-0.4.md).
259
+ - **The full API reference** — Start at the [reference index](docs/api/index.md):
207
260
  [components](docs/api/components.md) ·
208
261
  [entities](docs/api/entities.md) ·
209
262
  [queries](docs/api/queries.md) ·
@@ -219,11 +272,11 @@ while developing. Full details — including the browser/CDN and manual-override
219
272
  [determinism](docs/api/determinism.md) ·
220
273
  [memory](docs/api/memory.md) ·
221
274
  [WASM backends](docs/api/wasm.md) ·
222
- [parallelism](docs/api/parallel.md) ·
223
- [host-write seam](docs/api/host-write-seam.md) ·
275
+ [parallel execution](docs/api/parallel.md) ·
276
+ [the host write path](docs/api/host-write-seam.md) ·
224
277
  [reactive](docs/api/reactive.md) ·
225
278
  [editor](docs/api/editor.md) ·
226
- [tracing](docs/api/tracing.md) ·
279
+ [traces](docs/api/tracing.md) ·
227
280
  [primitives](docs/api/primitives.md) ·
228
281
  [errors](docs/api/errors.md)
229
282
 
@@ -239,13 +292,13 @@ pnpm exec tsc --noEmit # type check
239
292
 
240
293
  ## Acknowledgements
241
294
 
242
- oecs stands on the shoulders of the ECS community. Special thanks to:
295
+ oecs is built on the work of the ECS community. We thank:
243
296
 
244
297
  - **[Bevy](https://bevyengine.org)**, **[Flecs](https://github.com/SanderMertens/flecs)**, and
245
- **[bitECS](https://github.com/NateTheGreatt/bitECS)** — a constant source of inspiration; their
246
- designs shaped how oecs approaches archetypes, relations, scheduling, and change detection.
247
- - **[@clinuxrulz](https://github.com/clinuxrulz)** — for his amazing showcase and invaluable input on
248
- the ECS.
298
+ **[bitECS](https://github.com/NateTheGreatt/bitECS)** — a continuous source of ideas. Their
299
+ designs gave shape to the archetypes, relations, schedule, and change detection in oecs.
300
+ - **[@clinuxrulz](https://github.com/clinuxrulz)** — for an excellent demonstration and for very
301
+ valuable comments on the ECS.
249
302
 
250
303
  ## License
251
304
 
@@ -17,7 +17,7 @@ interface AccessSets {
17
17
  relationWrites: Set<number>;
18
18
  }
19
19
  declare function setsFor(desc: SystemDescriptor): AccessSets;
20
- /** The reads-only access surface a run condition declares (#576). A condition
20
+ /** The reads-only access surface a run condition declares. A condition
21
21
  * can only `reads` components (via a captured query) and `resourceReads`; every
22
22
  * mutation set is empty by construction, so the same `check_*` machinery rejects
23
23
  * any write/structural/resource-write a misbehaving predicate attempts. */
@@ -32,7 +32,7 @@ declare class AccessCheck {
32
32
  private activeName;
33
33
  private exclusive;
34
34
  enter(desc: SystemDescriptor): void;
35
- /** Open a reads-only span for a run condition (#576). No descriptor — a
35
+ /** Open a reads-only span for a run condition. No descriptor — a
36
36
  * condition can gate a whole SystemSet, so it isn't attributable to one
37
37
  * system — just its declared reads/resource_reads and a name for diagnostics.
38
38
  * Paired with `leave()`. */
@@ -52,7 +52,7 @@ declare class AccessCheck {
52
52
  checkSparseWrite(def: SparseComponentDef): void;
53
53
  checkRelationRead(def: RelationDef): void;
54
54
  checkRelationWrite(def: RelationDef): void;
55
- /** A `(*, T)` wildcard (`Query.forEachRelatedTo`, #579) reads every
55
+ /** A `(*, T)` wildcard (`Query.forEachRelatedTo`) reads every
56
56
  * relation's reverse index, so it can't name a specific relation — it is
57
57
  * authorised by the `ANY_RELATION` sentinel in `relationReads`. Honoured here
58
58
  * exactly like a per-relation read, just keyed on the reserved sentinel id
@@ -17,7 +17,7 @@ interface AccessSets {
17
17
  relationWrites: Set<number>;
18
18
  }
19
19
  declare function setsFor(desc: SystemDescriptor): AccessSets;
20
- /** The reads-only access surface a run condition declares (#576). A condition
20
+ /** The reads-only access surface a run condition declares. A condition
21
21
  * can only `reads` components (via a captured query) and `resourceReads`; every
22
22
  * mutation set is empty by construction, so the same `check_*` machinery rejects
23
23
  * any write/structural/resource-write a misbehaving predicate attempts. */
@@ -32,7 +32,7 @@ declare class AccessCheck {
32
32
  private activeName;
33
33
  private exclusive;
34
34
  enter(desc: SystemDescriptor): void;
35
- /** Open a reads-only span for a run condition (#576). No descriptor — a
35
+ /** Open a reads-only span for a run condition. No descriptor — a
36
36
  * condition can gate a whole SystemSet, so it isn't attributable to one
37
37
  * system — just its declared reads/resource_reads and a name for diagnostics.
38
38
  * Paired with `leave()`. */
@@ -52,7 +52,7 @@ declare class AccessCheck {
52
52
  checkSparseWrite(def: SparseComponentDef): void;
53
53
  checkRelationRead(def: RelationDef): void;
54
54
  checkRelationWrite(def: RelationDef): void;
55
- /** A `(*, T)` wildcard (`Query.forEachRelatedTo`, #579) reads every
55
+ /** A `(*, T)` wildcard (`Query.forEachRelatedTo`) reads every
56
56
  * relation's reverse index, so it can't name a specific relation — it is
57
57
  * authorised by the `ANY_RELATION` sentinel in `relationReads`. Honoured here
58
58
  * exactly like a per-relation read, just keyed on the reserved sentinel id