@oasys/oecs 0.3.0 → 0.4.0

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 (204) hide show
  1. package/README.md +196 -267
  2. package/dist/array-CxbPyiHO.cjs +1 -0
  3. package/dist/array-uFR7Q8fU.js +132 -0
  4. package/dist/core/ecs/access_check.d.ts +77 -0
  5. package/dist/core/ecs/access_check.d.ts.map +1 -0
  6. package/dist/core/ecs/archetype.d.ts +474 -0
  7. package/dist/core/ecs/archetype.d.ts.map +1 -0
  8. package/dist/core/ecs/builtin_relations.d.ts +43 -0
  9. package/dist/core/ecs/builtin_relations.d.ts.map +1 -0
  10. package/dist/core/ecs/command_log.d.ts +113 -0
  11. package/dist/core/ecs/command_log.d.ts.map +1 -0
  12. package/dist/core/ecs/component.d.ts +106 -0
  13. package/dist/core/ecs/component.d.ts.map +1 -0
  14. package/dist/core/ecs/compute_backend.d.ts +37 -0
  15. package/dist/core/ecs/compute_backend.d.ts.map +1 -0
  16. package/dist/core/ecs/dispatch_trace.d.ts +93 -0
  17. package/dist/core/ecs/dispatch_trace.d.ts.map +1 -0
  18. package/dist/core/ecs/ecs.d.ts +533 -0
  19. package/dist/core/ecs/ecs.d.ts.map +1 -0
  20. package/dist/core/ecs/ecs_memory.d.ts +179 -0
  21. package/dist/core/ecs/ecs_memory.d.ts.map +1 -0
  22. package/dist/core/ecs/entity.d.ts +28 -0
  23. package/dist/core/ecs/entity.d.ts.map +1 -0
  24. package/dist/core/ecs/event.d.ts +54 -0
  25. package/dist/core/ecs/event.d.ts.map +1 -0
  26. package/dist/core/ecs/frame_trace.d.ts +133 -0
  27. package/dist/core/ecs/frame_trace.d.ts.map +1 -0
  28. package/dist/core/ecs/host_commands.d.ts +252 -0
  29. package/dist/core/ecs/host_commands.d.ts.map +1 -0
  30. package/dist/core/ecs/index.d.ts +41 -0
  31. package/dist/core/ecs/index.d.ts.map +1 -0
  32. package/dist/core/ecs/observer.d.ts +142 -0
  33. package/dist/core/ecs/observer.d.ts.map +1 -0
  34. package/dist/core/ecs/query.d.ts +557 -0
  35. package/dist/core/ecs/query.d.ts.map +1 -0
  36. package/dist/core/ecs/ref.d.ts +31 -0
  37. package/dist/core/ecs/ref.d.ts.map +1 -0
  38. package/dist/core/ecs/relation.d.ts +231 -0
  39. package/dist/core/ecs/relation.d.ts.map +1 -0
  40. package/dist/core/ecs/resource.d.ts +33 -0
  41. package/dist/core/ecs/resource.d.ts.map +1 -0
  42. package/dist/core/ecs/resume.d.ts +85 -0
  43. package/dist/core/ecs/resume.d.ts.map +1 -0
  44. package/dist/core/ecs/run_condition.d.ts +75 -0
  45. package/dist/core/ecs/run_condition.d.ts.map +1 -0
  46. package/dist/core/ecs/schedule.d.ts +133 -0
  47. package/dist/core/ecs/schedule.d.ts.map +1 -0
  48. package/dist/core/ecs/sparse_store.d.ts +107 -0
  49. package/dist/core/ecs/sparse_store.d.ts.map +1 -0
  50. package/dist/core/ecs/store.d.ts +1149 -0
  51. package/dist/core/ecs/store.d.ts.map +1 -0
  52. package/dist/core/ecs/store_layout_listener.d.ts +23 -0
  53. package/dist/core/ecs/store_layout_listener.d.ts.map +1 -0
  54. package/dist/core/ecs/system.d.ts +134 -0
  55. package/dist/core/ecs/system.d.ts.map +1 -0
  56. package/dist/core/ecs/utils/arrays.d.ts +7 -0
  57. package/dist/core/ecs/utils/arrays.d.ts.map +1 -0
  58. package/dist/core/ecs/utils/constants.d.ts +12 -0
  59. package/dist/core/ecs/utils/constants.d.ts.map +1 -0
  60. package/dist/core/ecs/utils/error.d.ts +51 -0
  61. package/dist/core/ecs/utils/error.d.ts.map +1 -0
  62. package/dist/core/reactive/array.d.ts +24 -0
  63. package/dist/core/reactive/array.d.ts.map +1 -0
  64. package/dist/core/reactive/index.cjs +1 -0
  65. package/dist/core/reactive/index.d.ts +10 -0
  66. package/dist/core/reactive/index.d.ts.map +1 -0
  67. package/dist/core/reactive/index.js +17 -0
  68. package/dist/core/reactive/interop.d.ts +19 -0
  69. package/dist/core/reactive/interop.d.ts.map +1 -0
  70. package/dist/core/reactive/kernel.d.ts +71 -0
  71. package/dist/core/reactive/kernel.d.ts.map +1 -0
  72. package/dist/core/reactive/map.d.ts +16 -0
  73. package/dist/core/reactive/map.d.ts.map +1 -0
  74. package/dist/core/reactive/struct.d.ts +10 -0
  75. package/dist/core/reactive/struct.d.ts.map +1 -0
  76. package/dist/core/store/__generated__/abi.d.ts +43 -0
  77. package/dist/core/store/__generated__/abi.d.ts.map +1 -0
  78. package/dist/core/store/action_ring.d.ts +136 -0
  79. package/dist/core/store/action_ring.d.ts.map +1 -0
  80. package/dist/core/store/allocator.d.ts +238 -0
  81. package/dist/core/store/allocator.d.ts.map +1 -0
  82. package/dist/core/store/buffer_backed_column.d.ts +69 -0
  83. package/dist/core/store/buffer_backed_column.d.ts.map +1 -0
  84. package/dist/core/store/column_store.d.ts +265 -0
  85. package/dist/core/store/column_store.d.ts.map +1 -0
  86. package/dist/core/store/command_dispatch.d.ts +52 -0
  87. package/dist/core/store/command_dispatch.d.ts.map +1 -0
  88. package/dist/core/store/command_ring.d.ts +107 -0
  89. package/dist/core/store/command_ring.d.ts.map +1 -0
  90. package/dist/core/store/descriptor.d.ts +80 -0
  91. package/dist/core/store/descriptor.d.ts.map +1 -0
  92. package/dist/core/store/entity_index.d.ts +108 -0
  93. package/dist/core/store/entity_index.d.ts.map +1 -0
  94. package/dist/core/store/event_ring.d.ts +95 -0
  95. package/dist/core/store/event_ring.d.ts.map +1 -0
  96. package/dist/core/store/extend.d.ts +109 -0
  97. package/dist/core/store/extend.d.ts.map +1 -0
  98. package/dist/core/store/grow.d.ts +39 -0
  99. package/dist/core/store/grow.d.ts.map +1 -0
  100. package/dist/core/store/header.d.ts +64 -0
  101. package/dist/core/store/header.d.ts.map +1 -0
  102. package/dist/core/store/index.d.ts +16 -0
  103. package/dist/core/store/index.d.ts.map +1 -0
  104. package/dist/core/store/region_table.d.ts +74 -0
  105. package/dist/core/store/region_table.d.ts.map +1 -0
  106. package/dist/core/store/snapshot.d.ts +43 -0
  107. package/dist/core/store/snapshot.d.ts.map +1 -0
  108. package/dist/core/store/state_hash.d.ts +38 -0
  109. package/dist/core/store/state_hash.d.ts.map +1 -0
  110. package/dist/core/store/store_regions.d.ts +38 -0
  111. package/dist/core/store/store_regions.d.ts.map +1 -0
  112. package/dist/extensions/editor/editor.d.ts +149 -0
  113. package/dist/extensions/editor/editor.d.ts.map +1 -0
  114. package/dist/extensions/editor/field_handle.d.ts +35 -0
  115. package/dist/extensions/editor/field_handle.d.ts.map +1 -0
  116. package/dist/extensions/editor/index.cjs +1 -0
  117. package/dist/extensions/editor/index.d.ts +21 -0
  118. package/dist/extensions/editor/index.d.ts.map +1 -0
  119. package/dist/extensions/editor/index.js +209 -0
  120. package/dist/extensions/reactive/ecs_sync.d.ts +210 -0
  121. package/dist/extensions/reactive/ecs_sync.d.ts.map +1 -0
  122. package/dist/extensions/reactive/index.cjs +1 -0
  123. package/dist/extensions/reactive/index.d.ts +23 -0
  124. package/dist/extensions/reactive/index.d.ts.map +1 -0
  125. package/dist/extensions/reactive/index.js +225 -0
  126. package/dist/extensions/solid/index.cjs +1 -0
  127. package/dist/extensions/solid/index.d.ts +6 -0
  128. package/dist/extensions/solid/index.d.ts.map +1 -0
  129. package/dist/extensions/solid/index.js +32 -0
  130. package/dist/extensions/solid/kernel_solid.d.ts +42 -0
  131. package/dist/extensions/solid/kernel_solid.d.ts.map +1 -0
  132. package/dist/index.cjs +2 -1
  133. package/dist/index.d.ts +16 -12
  134. package/dist/index.d.ts.map +1 -1
  135. package/dist/index.js +8563 -1233
  136. package/dist/interop-CT-REx0W.cjs +1 -0
  137. package/dist/interop-CcY6ASQc.js +18 -0
  138. package/dist/kernel-DgyrLFjW.js +227 -0
  139. package/dist/kernel-yWV3XnAb.cjs +1 -0
  140. package/dist/log/console_sink.d.ts +4 -0
  141. package/dist/log/console_sink.d.ts.map +1 -0
  142. package/dist/log/index.d.ts +3 -0
  143. package/dist/log/index.d.ts.map +1 -0
  144. package/dist/log/logger.d.ts +27 -0
  145. package/dist/log/logger.d.ts.map +1 -0
  146. package/dist/primitives.cjs +1 -0
  147. package/dist/primitives.d.ts +18 -0
  148. package/dist/primitives.d.ts.map +1 -0
  149. package/dist/primitives.js +44 -0
  150. package/dist/shared-BXSZnxx4.cjs +1 -0
  151. package/dist/shared-C678TAPY.js +99 -0
  152. package/dist/shared.cjs +1 -0
  153. package/dist/shared.d.ts +22 -0
  154. package/dist/shared.d.ts.map +1 -0
  155. package/dist/shared.js +7 -0
  156. package/dist/topological_sort-DlRpSrxu.js +391 -0
  157. package/dist/topological_sort-WAT-VHb-.cjs +1 -0
  158. package/dist/type_primitives/assertions.d.ts +12 -8
  159. package/dist/type_primitives/assertions.d.ts.map +1 -1
  160. package/dist/type_primitives/binary_heap/binary_heap.d.ts +6 -2
  161. package/dist/type_primitives/binary_heap/binary_heap.d.ts.map +1 -1
  162. package/dist/type_primitives/bitset/bitset.d.ts +16 -4
  163. package/dist/type_primitives/bitset/bitset.d.ts.map +1 -1
  164. package/dist/type_primitives/brand.d.ts +6 -1
  165. package/dist/type_primitives/brand.d.ts.map +1 -1
  166. package/dist/type_primitives/error.d.ts +4 -0
  167. package/dist/type_primitives/error.d.ts.map +1 -1
  168. package/dist/type_primitives/index.d.ts +3 -0
  169. package/dist/type_primitives/index.d.ts.map +1 -1
  170. package/dist/type_primitives/sparse_map/sparse_map.d.ts +7 -3
  171. package/dist/type_primitives/sparse_map/sparse_map.d.ts.map +1 -1
  172. package/dist/type_primitives/sparse_set/sparse_set.d.ts +4 -0
  173. package/dist/type_primitives/sparse_set/sparse_set.d.ts.map +1 -1
  174. package/dist/type_primitives/topological_sort/topological_sort.d.ts +7 -3
  175. package/dist/type_primitives/topological_sort/topological_sort.d.ts.map +1 -1
  176. package/dist/type_primitives/typed_arrays/typed_arrays.d.ts +53 -16
  177. package/dist/type_primitives/typed_arrays/typed_arrays.d.ts.map +1 -1
  178. package/dist/utils/arrays.d.ts +1 -1
  179. package/dist/utils/arrays.d.ts.map +1 -1
  180. package/dist/utils/error.d.ts +2 -20
  181. package/dist/utils/error.d.ts.map +1 -1
  182. package/package.json +36 -9
  183. package/dist/archetype.d.ts +0 -101
  184. package/dist/archetype.d.ts.map +0 -1
  185. package/dist/component.d.ts +0 -45
  186. package/dist/component.d.ts.map +0 -1
  187. package/dist/ecs.d.ts +0 -104
  188. package/dist/ecs.d.ts.map +0 -1
  189. package/dist/entity.d.ts +0 -11
  190. package/dist/entity.d.ts.map +0 -1
  191. package/dist/event.d.ts +0 -30
  192. package/dist/event.d.ts.map +0 -1
  193. package/dist/query.d.ts +0 -90
  194. package/dist/query.d.ts.map +0 -1
  195. package/dist/ref.d.ts +0 -23
  196. package/dist/ref.d.ts.map +0 -1
  197. package/dist/resource.d.ts +0 -23
  198. package/dist/resource.d.ts.map +0 -1
  199. package/dist/schedule.d.ts +0 -45
  200. package/dist/schedule.d.ts.map +0 -1
  201. package/dist/store.d.ts +0 -118
  202. package/dist/store.d.ts.map +0 -1
  203. package/dist/system.d.ts +0 -16
  204. package/dist/system.d.ts.map +0 -1
@@ -0,0 +1,1149 @@
1
+ import { EntityID } from './entity';
2
+ import { FrameTraceSink } from './frame_trace';
3
+ import { ComponentDef, ComponentHandle, ComponentSchema, FieldValues } from './component';
4
+ import { SparseComponentDef, SparseComponentID } from './sparse_store';
5
+ import { RelationDef, RelationOptions } from './relation';
6
+ import { EmptyEventSchema, EventDef, EventReader, EventSchema } from './event';
7
+ import { BitSet, TypedArrayTag } from '../../type_primitives';
8
+ import { Archetype, ArchetypeID } from './archetype';
9
+ import { Query } from './query';
10
+ import { InPlaceBufferAllocator, ColumnStoreRegionHandle, StoreRegionSpec, ColumnStore } from '../store';
11
+ import { ECSMemoryCapContext } from './ecs_memory';
12
+ /**
13
+ * Effective `(component, entity)` structural events for one fixed-point round,
14
+ * collected during `_flushAdds` / `_flushRemoves` and handed to the observer
15
+ * dispatch hook. Flat parallel arrays, count-bounded (`*_len`), reused across
16
+ * rounds — never reallocated in the flush. This is a scheduling artifact: it is
17
+ * NOT part of `stateHash` or snapshot. See `observer.ts`.
18
+ */
19
+ export interface StructuralObserverEvents {
20
+ addComp: number[];
21
+ addEid: number[];
22
+ addLen: number;
23
+ remComp: number[];
24
+ remEid: number[];
25
+ remLen: number;
26
+ /** Effective disable events (#677) — collected during the toggle drain
27
+ * (`_flushToggles`), one per `(component, entity)` of each net-disabled
28
+ * entity's mask. Empty on a structural (add/remove/destroy) round. */
29
+ disComp: number[];
30
+ disEid: number[];
31
+ disLen: number;
32
+ /** Effective enable events (#677), symmetric with the disable arrays. */
33
+ enaComp: number[];
34
+ enaEid: number[];
35
+ enaLen: number;
36
+ }
37
+ /** Extracts the schema out of a `ComponentDef` handle. */
38
+ type SchemaOf<D extends ComponentDef> = D extends ComponentDef<infer S> ? S : ComponentSchema;
39
+ /** One typed template entry: `values` is checked against the def's schema —
40
+ * a misspelled field is a compile error. Fields may be omitted (they default
41
+ * to 0), so the map is `Partial`. */
42
+ export type TemplateEntry<D extends ComponentDef = ComponentDef> = {
43
+ readonly def: D;
44
+ readonly values?: Partial<FieldValues<SchemaOf<D>>>;
45
+ };
46
+ /** The entries tuple for `ECS.template` — each element's `values` is checked
47
+ * against its own `def`'s schema. */
48
+ export type TemplateEntries<Defs extends readonly ComponentDef[]> = readonly [
49
+ ...{
50
+ [K in keyof Defs]: TemplateEntry<Defs[K]>;
51
+ }
52
+ ];
53
+ /** Union of every field name owned by a component in `Defs` (distributes
54
+ * over the def list). */
55
+ type TemplateFieldNames<Defs extends readonly ComponentDef[]> = Defs[number] extends ComponentDef<infer S> ? keyof S & string : never;
56
+ /** Flat per-instance override map for `ECS.spawn`: any field of any
57
+ * component in the template, each optional. A misspelled field is a compile
58
+ * error (and still a `__DEV__` throw at runtime for untyped call sites). */
59
+ export type TemplateOverrides<Defs extends readonly ComponentDef[]> = {
60
+ readonly [K in TemplateFieldNames<Defs>]?: number;
61
+ };
62
+ declare const __templateDefs: unique symbol;
63
+ /** A resolved template (#462) — an archetype template produced by
64
+ * `ECS.template(...)`. **Opaque** apart from `defs`: callers hold it and pass
65
+ * it to `ECS.spawn` / `ECS.spawnMany` (and may reference it in a system's
66
+ * `spawns` / `despawns` access declaration — the scheduler expands it to
67
+ * `defs`); the remaining fields are engine-internal and may change. `spawn`
68
+ * lands an entity directly in `archetype_id` with zero archetype transitions,
69
+ * writing `flatValues` (defaults in `_flatColumns` order) in one append
70
+ * pass. See ADR-0010. */
71
+ export interface Template<Defs extends readonly ComponentDef[] = readonly ComponentDef[]> {
72
+ readonly archetypeId: ArchetypeID;
73
+ readonly flatValues: number[];
74
+ readonly overrideIndex: Map<string, number>;
75
+ /** The component set this template spawns into, in entry order. */
76
+ readonly defs: readonly ComponentDef[];
77
+ readonly [__templateDefs]?: Defs;
78
+ }
79
+ export interface StoreOptions {
80
+ initialCapacity?: number;
81
+ /** Pluggable SAB buffer source. When provided, `createColumnStore`,
82
+ * `extendColumnStore`, and `growColumnStore` route through it. Default is
83
+ * `growableSabAllocator()`. Typed `InPlaceBufferAllocator` (#682): a live
84
+ * Store's flush loops hoist entity-index views across grows, so only
85
+ * in-place allocators may back one (ADR-0008) — the constructor also
86
+ * runtime-asserts the marker for untyped JS callers. Consumers normally
87
+ * don't touch this directly; `ECSOptions.memory` resolves to it. */
88
+ bufferAllocator?: InPlaceBufferAllocator;
89
+ /** Sizing intent the world was constructed with (#682), used to phrase
90
+ * allocator-cap and entity-index-overflow errors in the caller's own
91
+ * terms ("3.2× the declared budget") instead of raw bytes. Wired by
92
+ * `ECS` from `resolveECSMemory`; absent for bare test Stores. */
93
+ capContext?: ECSMemoryCapContext;
94
+ /** Fired after every SAB resize (extend or grow). The new SAB has
95
+ * already been built and archetypes have already refreshed their
96
+ * views by the time this fires. Used by ECS to call
97
+ * `sim.setLayout(0)` so WASM-side cached pointers re-walk. */
98
+ onBufferResized?: () => void;
99
+ /** Max live entities the SAB entity-index region holds (#245 / PR 4B).
100
+ * Default `ENTITY_INDEX_DEFAULT_CAPACITY` (`1 << 20` — the full EntityID
101
+ * index space). Exceeding this at runtime throws `EID_MAX_INDEX_OVERFLOW`.
102
+ * Tests with small entity counts may set lower to bench the SAB region size
103
+ * or to make index exhaustion reachable; a 1000-entity workload fits
104
+ * comfortably in the default. */
105
+ entityIndexCapacity?: number;
106
+ /** Consumer-declared SAB regions (#623), forwarded verbatim to
107
+ * `createColumnStore`. Each `StoreRegionSpec` carries an opaque `region_id`,
108
+ * a precomputed byte size, and an `init` closure; the engine lays them out
109
+ * generically and exposes them via `regionHandle(id)` / `regionOffset(id)`.
110
+ * A game (e.g. `@internal/sim`'s region specs) supplies these — the engine
111
+ * ships no game regions of its own. Omitted ⇒ none. */
112
+ regions?: readonly StoreRegionSpec[];
113
+ /** Byte size of the opt-in sim-bindings region (#625), forwarded verbatim to
114
+ * `createColumnStore`. A consumer that attaches a WASM backend passes its own
115
+ * size (`@internal/sim`'s `SIM_BINDINGS_BYTES`, computed from the binding
116
+ * manifest); the host then writes the `(component_id, field_id)` IDs into the
117
+ * region. Omitted / 0 ⇒ no region (a pure-TS game pays nothing for the WASM
118
+ * seam). De-welded from the engine ABI in #625 so a manifest edit doesn't
119
+ * drift an engine golden. */
120
+ bindingsRegionBytes?: number;
121
+ /** Opt into the **determinism surface** (#626 / ADR-0020). Default `false`.
122
+ * Gates the three methods that fold/serialize state in canonical (sorted)
123
+ * order: `stateHash`, `snapshotSparse`, `restoreSparse`. When `false`
124
+ * those throw `DETERMINISM_DISABLED` — the canonical-ordering tax (sparse
125
+ * `canonicalIndices` sort + relation target-set sort) is never paid, and a
126
+ * consumer can't accidentally read a non-canonical digest. When `true`,
127
+ * today's behavior is reproduced bit-for-bit. This is the ONLY effect of the
128
+ * flag: it does not touch the per-tick path, the in-place-allocator invariant
129
+ * (ADR-0008, a memory-safety requirement that holds regardless), or the
130
+ * always-on `enabled_count` partition maintenance. The flag's value is a
131
+ * capability gate, not a hot-path switch — `stateHash`/snapshot are never
132
+ * called per tick. */
133
+ deterministic?: boolean;
134
+ }
135
+ export declare class Store {
136
+ private entityGenerations;
137
+ private entityHighWater;
138
+ private readonly entityFreeIndices;
139
+ private entityAliveCount;
140
+ private readonly componentMetas;
141
+ private componentCount;
142
+ private readonly sparseStores;
143
+ private readonly relations;
144
+ private _hasTargetCleanup;
145
+ private readonly eventChannels;
146
+ private readonly dirtyEventChannels;
147
+ private eventCount;
148
+ private readonly archetypes;
149
+ private readonly archetypeMap;
150
+ private nextArchetypeId;
151
+ private readonly componentIndex;
152
+ private readonly registeredQueries;
153
+ private emptyArchetypeId;
154
+ private entityArchetype;
155
+ private entityRow;
156
+ private readonly pendingDestroy;
157
+ private readonly pendingAddIds;
158
+ private readonly pendingAddDefs;
159
+ private readonly pendingAddValues;
160
+ private readonly pendingRemoveIds;
161
+ private readonly pendingRemoveDefs;
162
+ private readonly pendingToggleIds;
163
+ private readonly pendingToggleDisable;
164
+ _tick: number;
165
+ /** Per-world frame-trace sink (ADR-0030), installed via `ECS.setTrace`.
166
+ * `null` unless a consumer attaches a recorder. Every call site is
167
+ * `if (__DEV__) store._trace?.…`, so production builds dead-code-eliminate
168
+ * the seam and pay only this one nullable field. The sink observes; it never
169
+ * folds into `stateHash` (a scheduling artifact, like `_changedTick` / the
170
+ * observer state below). */
171
+ _trace: FrameTraceSink | null;
172
+ private _structuralObserverCount;
173
+ /** Count of components with any onDisable/onEnable observer (#677). While 0
174
+ * (with `_structuralObserverCount` also 0), `flushStructural` takes the
175
+ * byte-for-byte fast path and the toggle drain skips event collection. */
176
+ private _toggleObserverCount;
177
+ /** Reused effective-event scratch for the current flush round. */
178
+ private readonly _obsEvents;
179
+ /** Installed by `ECS` — dispatches a round's collected events to the observer
180
+ * registry (ordering + callbacks), which may enqueue further structural ops. */
181
+ _structuralObserverHook: ((ev: StructuralObserverEvents) => void) | null;
182
+ /** Re-entrancy guard for the observed fixed-point loop. An observer callback
183
+ * must enqueue (add/remove) and let the loop settle it — calling `ctx.flush()`
184
+ * from inside a callback would re-enter `flushStructural` and corrupt the
185
+ * shared event scratch. The guard turns that nested call into a no-op; the
186
+ * outer loop drains whatever the callback queued. */
187
+ private _flushingStructural;
188
+ private _collectDestroyEid;
189
+ private readonly _collectDestroyRemoveBit;
190
+ private _collectToggleEid;
191
+ private readonly _collectDisableBit;
192
+ private readonly _collectEnableBit;
193
+ /** Net-transition snapshot for the toggle drain (#677): entity → its disabled
194
+ * state at the START of the drain. Reused, cleared each drain. Lets
195
+ * `_flushToggles` emit one event per *net* transition (disable→enable→disable
196
+ * within a tick = a single onDisable) instead of one per buffered op — required
197
+ * because the radix canonical-order pass would otherwise reorder duplicate eids
198
+ * and mis-sequence a consumer's delete/republish. */
199
+ private readonly _toggleInitial;
200
+ _anyDirtyTracked: boolean;
201
+ private readonly _dirtyTrackedCids;
202
+ private readonly _dirtyLists;
203
+ private readonly _dirtyMarks;
204
+ /** Set by any path that changes a SAB-backed archetype's live row count
205
+ * (`flushStructural`/`flushDestroyed` when they did work; immediate
206
+ * `destroyEntity`, `addComponent(s)`, `removeComponent(s)` on the
207
+ * Store). Cleared by `publishRowCountsToDescriptor`. Lets read-only
208
+ * phases' `ctx.flush()` skip the descriptor walk entirely (#324). */
209
+ private _rowCountsDirty;
210
+ /** Monotonic counter bumped by every membership-changing path (immediate
211
+ * `addComponent(s)`, `removeComponent(s)`, `destroyEntity`,
212
+ * `batchAddComponent`, `batchRemoveComponent`, `flushStructural`,
213
+ * `flushDestroyed`, and new-archetype installs in `archInstall`).
214
+ * Read by `Query._nonEmpty()` via `QueryResolver._getQueryDirtyEpoch`
215
+ * — a query whose stored `_lastSeenEpoch` matches the current epoch
216
+ * reuses its cached non-empty list (#327). Replaces the previous walk
217
+ * over `registeredQueries` that wrote one dirty bit per query per
218
+ * mutation; 5000 startup adds × Q queries used to be 5000×Q writes,
219
+ * now it's 5000 integer increments. Public so ECS can forward through
220
+ * its `QueryResolver` impl; not part of the user-facing API. */
221
+ _queryDirtyEpoch: number;
222
+ private readonly initialCapacity;
223
+ private readonly _scratchTargetMask;
224
+ private readonly _hierarchyRadixOut;
225
+ private readonly _hierarchyRadixC0;
226
+ private readonly _hierarchyRadixC1;
227
+ private _columnStore;
228
+ /** Single-slot Uint32Array view over the entity-index region's `length`
229
+ * header field. `createEntity`'s hot path writes `entityHighWater`
230
+ * here on every high-water bump; using a pre-built typed-array view
231
+ * skips the DataView lookup + setUint32 dispatch that #368 traced to
232
+ * ~25 ns/op on `frame_loop_focused: createEntity` (closes the
233
+ * post-#361 gap to oecs entirely). Rebuilt on SAB resize. */
234
+ private _entityIndexLengthView;
235
+ /** Installed on every SAB-backed Archetype so the Archetype can
236
+ * request a SAB grow when an insertion would exceed its column
237
+ * capacity. Doubles the offending archetype's row capacity (or jumps
238
+ * to whatever fits `arch.length + additional`, whichever is larger),
239
+ * reallocs the SAB via `growColumnStore` (live rows of every archetype
240
+ * are carried forward), and republishes column views to every
241
+ * SAB-backed archetype. Plan §8.1 / §8.3 / §8.4. (#171 §6.1.9 Phase 3) */
242
+ private readonly _growHandler;
243
+ /** Build the intent-aware fatal for an allocator cap hit (#682). The
244
+ * allocator can only name raw bytes; the Store knows what the caller
245
+ * declared (`capContext`) and how many entities are live, so the error
246
+ * says "3.2× the declared budget — runaway creation upstream?" instead
247
+ * of leaving the caller to reverse-engineer byte counts. Fatality is
248
+ * unchanged (#380: no grow-beyond-cap fallback). */
249
+ private _capExceededError;
250
+ private readonly _bufferAllocator;
251
+ private readonly _capContext;
252
+ private readonly _onBufferResized;
253
+ /** Construct with an `initialCapacity` number (legacy form) or an
254
+ * options object (new in PR 3D — adds `bufferAllocator` and
255
+ * `onBufferResized` callback). Both signatures coexist so test fixtures
256
+ * that pass `new Store(4)` keep working. */
257
+ constructor(arg?: number | StoreOptions);
258
+ /** Capacity of the entity-index SAB region (max slots ≈ max live
259
+ * entities). Fixed at construction in #245's PR 4B scope; a future
260
+ * follow-up will grow it via `growColumnStore` when `entityHighWater`
261
+ * hits the cap. */
262
+ private readonly _entityIndexCapacity;
263
+ /** Consumer-declared SAB regions (#623), captured so the realloc path
264
+ * re-lays them out. `undefined` when no consumer regions were declared.
265
+ * The region contents survive a grow via the self-describing region table
266
+ * (`extend.ts` snapshot/restore), so this is only the layout recipe. */
267
+ private readonly _regions;
268
+ /** Byte size of the opt-in sim-bindings region (#625). 0 ⇒ no region (the
269
+ * pure-TS default). Captured so the initial `createColumnStore` reserves it;
270
+ * across a realloc the size is re-derived from the old header by
271
+ * `optionsFromOld`, so it is not threaded through the grow/extend path. */
272
+ private readonly _bindingsRegionBytes;
273
+ /** Determinism opt-in (#626 / ADR-0020). When `false` (the default), the
274
+ * canonical-ordering determinism surface (`stateHash` / `snapshotSparse` /
275
+ * `restoreSparse`) throws `DETERMINISM_DISABLED` rather than running its
276
+ * sort. Memory-safety invariants (the in-place allocator, ADR-0008) and the
277
+ * `enabled_count` partition are unaffected — they hold regardless. */
278
+ private readonly _deterministic;
279
+ /** Whether the determinism surface is enabled (#626). `false` ⇒ `stateHash`
280
+ * / `snapshotSparse` / `restoreSparse` throw `DETERMINISM_DISABLED`. */
281
+ get deterministic(): boolean;
282
+ /** Guard the canonical-ordering determinism surface. Throws
283
+ * `DETERMINISM_DISABLED` when determinism wasn't opted into, naming the
284
+ * method so the caller knows to pass `{ deterministic: true }`. Always on
285
+ * (not `__DEV__`-gated): the surface is cold (never per-tick) so one boolean
286
+ * check is free, and a silent non-canonical digest is the failure mode we're
287
+ * preventing. */
288
+ private _requireDeterministic;
289
+ /** Reject `f32`/`f64` fields on a `deterministic: true` world at registration
290
+ * (#777). IEEE-754 rounds differently across V8 / Bun / Zig at the 1-ULP
291
+ * level, so a float column in a fixed-update path is a silent per-tick
292
+ * `stateHash` divergence between client and server — the one thing the
293
+ * determinism opt-in (ADR-0020) exists to prevent. Non-deterministic worlds
294
+ * skip this entirely (floats stay allowed), so it costs the default path
295
+ * nothing. `kind` names the storage class in the error ("component" /
296
+ * "sparse component"); the array shorthand's `f64` default lands here too, so
297
+ * a deterministic world must pass an explicit integer type. */
298
+ private _rejectNonDeterministicFields;
299
+ /** Rebuild the Int32Array views over the SAB entity-index region
300
+ * after a host-side SAB realloc (extend / grow). Called from
301
+ * `_handleBufferResized` BEFORE the user-supplied `onBufferResized`
302
+ * callback fires so any downstream reader sees coherent views. */
303
+ private _refreshEntityIndexViews;
304
+ /** Centralised "SAB was just reallocated" handler. Refreshes the
305
+ * Int32Array views FIRST (so user callbacks observe valid views),
306
+ * then mirrors `entityHighWater` into the region's length header,
307
+ * then fires the user-supplied callback. */
308
+ private _handleBufferResized;
309
+ /** SAB backing every archetype's column views. Read-only handle; the
310
+ * live mutation happens through `archGetOrCreateFromMask`.
311
+ * Exposed for tests, snapshot/restore, and the upcoming
312
+ * `columnStoreStateHash` wire-up. Production reads of column data should
313
+ * still go through `Archetype.getColumnRead` (which sources from this
314
+ * SAB under the hood). */
315
+ get columnStore(): ColumnStore;
316
+ /** Resolve a consumer-declared SAB region's byte offset by `region_id`, or
317
+ * 0 when the region is absent (no region was declared with that id). The
318
+ * generic, de-gamed replacement (#623) for the removed game-named accessors
319
+ * (`terrain_view` / `spatial_grid_view` / … ); a consumer pairs this with
320
+ * its own region module (e.g. `@internal/sim`'s region helpers) to
321
+ * materialise a typed view. TS twin of Zig `abi.find_region`. */
322
+ regionOffset(regionId: number): number;
323
+ /** A handle to a consumer-declared SAB region resolved by `region_id`, or
324
+ * `null` when absent. Carries the live `buffer`/`view` plus the region's byte
325
+ * `offset` and `bytes`, so a consumer's region module can build a TypedArray
326
+ * view over exactly the region's span without re-reading the directory.
327
+ * Re-fetch after a SAB grow (the offset/view may have moved). (#623) */
328
+ regionHandle(regionId: number): ColumnStoreRegionHandle | null;
329
+ /**
330
+ * Stamp every SAB-backed archetype's live `length` into its descriptor's
331
+ * `row_count` field (#252 / Phase 4 PR 4E). `extendColumnStore` /
332
+ * `growColumnStore` are the only other writers of `row_count`, and they
333
+ * record the count at the moment of the resize — `Archetype.addEntity`
334
+ * does not update it, so any insertion after the most recent resize
335
+ * leaves the descriptor stale. Zig systems that drive their per-row loop
336
+ * off `arch_hdr.row_count` (every `tick_*` export added since PR 3B)
337
+ * read those stale bytes and silently skip the just-spawned rows.
338
+ *
339
+ * Lockstep walk: SAB descriptors are written by `extendColumnStore` in
340
+ * the order non-SAB archetypes are promoted, which is the same id-order
341
+ * those archetypes occupy in `this.archetypes`. Iterating that array
342
+ * once, skipping non-SAB entries, and advancing an `archAddr` cursor
343
+ * by the descriptor's `column_count` lets us write `row_count` without
344
+ * the throwaway `Map<archId, length>` the previous version allocated
345
+ * on every call (#323). Cheap: descriptor-region seeks only, no column
346
+ * I/O.
347
+ *
348
+ * Gated by `_rowCountsDirty` (#324) — mutation paths
349
+ * (`flushStructural`, `flushDestroyed`, immediate `destroyEntity`,
350
+ * `addComponent(s)`, `removeComponent(s)`) set the flag; this method
351
+ * clears it. Read-only phases that flush only to drain empty buffers
352
+ * pay nothing. */
353
+ publishRowCountsToDescriptor(): void;
354
+ /** FNV-1a-style 32-bit digest over (archetype_id, live_row_count, live
355
+ * column bytes) for each archetype in id order, followed by the sparse
356
+ * stores (out-of-identity components, ADR-0011) in registration order.
357
+ * Replaces the per-networked-component fold that compute_state_hash used
358
+ * pre-#171 §6.1.9 Phase 5 — this is the canonical "live ECS state digest"
359
+ * for cross-replay determinism.
360
+ *
361
+ * **Sparse coverage (#470).** Sparse data lives outside the archetype
362
+ * graph, so it is folded separately after the archetype loop — per store:
363
+ * the sparse-component id, the member count, then each member's source
364
+ * entity index + f64 field words, walked in CANONICAL ascending-index order
365
+ * (`SparseComponentStore.canonicalIndices`). Canonical order is what makes
366
+ * the digest insertion-order-independent: two worlds with identical sparse
367
+ * contents built by different add/remove sequences agree. Keyed by entity
368
+ * index, and destruction purges the slot, so a recycled index never carries
369
+ * a stale occupant's data into the hash.
370
+ *
371
+ * It is strictly broader than the prior per-networked-component fold
372
+ * (covers every column, not just a hand-picked subset of networked
373
+ * components), and strictly tighter than `columnStoreStateHash(...)`
374
+ * which scans the full SAB including trailing unused capacity.
375
+ *
376
+ * **Per-word fold (#326).** The inner column loop folds one 32-bit
377
+ * word at a time using FNV-1a's `xor + imul(PRIME)` step. This is NOT
378
+ * byte-for-byte FNV-1a-32 of the column bytes — it's a deterministic
379
+ * digest with the same equality semantics, ~4× faster than the per-
380
+ * byte loop it replaces. Trailing 0–3 tail bytes (only possible for
381
+ * u8/u16 columns at odd row counts) are folded together as a single
382
+ * little-endian word so the algorithm stays branch-free in the inner
383
+ * loop. The 4-byte `id` and `len` headers are folded as words for the
384
+ * same reason. Byte order is little-endian to match the platform's
385
+ * native TypedArray layout; the digest is opaque (no consumer compares
386
+ * against a literal value), so endianness is an implementation detail
387
+ * rather than wire contract.
388
+ *
389
+ * Determinism: same store ⇒ same digest within a process, and across
390
+ * processes on the same architecture (which is all `replay_match`
391
+ * needs — both replays run the same algorithm on the same words).
392
+ *
393
+ * **Opt-in (#626 / ADR-0020).** Throws `DETERMINISM_DISABLED` unless the
394
+ * Store was constructed with `{ deterministic: true }`. The canonical
395
+ * ordering this fold relies on (sparse `canonicalIndices`, sorted relation
396
+ * target sets) is the determinism tax the flag gates. */
397
+ stateHash(): number;
398
+ private archGet;
399
+ /** Look up the `EntityID` at `row` in archetype `archetype_id`. Used
400
+ * by a WASM system to resolve an
401
+ * `EntityID` from an event-ring payload — Zig writes
402
+ * `(archId, row, …)` to the event ring,
403
+ * and TS bridges it back through `ctx.emit(...)` via this method. (#250 /
404
+ * Phase 4 PR 4D)
405
+ *
406
+ * Throws `ECSError` if `archetype_id` is out of range or `row` is
407
+ * past the archetype's live row count — these would indicate a
408
+ * ring-payload corruption or a stale row index (extend / grow
409
+ * happened mid-tick), both of which are bugs the parity test would
410
+ * surface. */
411
+ entityIdAtRow(archetypeId: number, row: number): EntityID;
412
+ /**
413
+ * Find or create an archetype for the given component mask.
414
+ * Also updates the componentIndex and pushes into matching registered queries.
415
+ *
416
+ * Hot single-mask path. The bulk batched variant — used by Phase C
417
+ * pre-warming at `world.startup()` — is `archCreateManyFromMasks`;
418
+ * see #213 / `ECS.startup()` for how it gets called and why.
419
+ */
420
+ private archGetOrCreateFromMask;
421
+ /**
422
+ * Bulk variant of `archGetOrCreateFromMask` — Phase C of issue #213.
423
+ *
424
+ * Given a set of masks, creates Archetypes for the ones not already
425
+ * planted, in a SINGLE `extendColumnStore` call (instead of one per
426
+ * archetype). Single-mask creation is O(N) in archetypes-so-far because
427
+ * the extend has to copy every existing archetype's live rows forward;
428
+ * N such calls compound to O(N²). Batching collapses the per-archetype
429
+ * setup-and-copy down to one pass — the per-startup cost the design doc
430
+ * §5.2 calls out as "O(N²) → O(N)" for in-tree systems whose archetype
431
+ * set is known at registration time via `spawns` + `transitions`.
432
+ *
433
+ * Masks already in the map are skipped. Returns the resolved
434
+ * `ArchetypeID` per input mask in input order — callers that don't need
435
+ * the ids can ignore the return value.
436
+ */
437
+ archCreateManyFromMasks(masks: readonly BitSet[]): ArchetypeID[];
438
+ /** Hash-bucketed mask → ArchetypeID lookup; null when the mask isn't planted. */
439
+ private archLookup;
440
+ /** Walk the mask's set bits and resolve each non-tag component to its
441
+ * layout. Used by both single and bulk archetype-create paths. */
442
+ private archBuildLayouts;
443
+ /** Snapshot every existing archetype's SAB rows, call `extendColumnStore`
444
+ * once with `newSpecs`, then refresh every pre-existing SAB-backed
445
+ * Archetype's TypedArray views. The single `existing` snapshot is the
446
+ * key win in the bulk variant — single-mask creation rebuilds it per
447
+ * call (i.e. N times for N new archetypes). */
448
+ private archExtendStoreWithNewSpecs;
449
+ /** Materialise the `Archetype` for `id`, register in the archetype map +
450
+ * component index, and fan into any matching registered queries.
451
+ *
452
+ * Clones `mask` before storing — callers may pass scratch BitSets that
453
+ * they intend to reuse (e.g. `addComponents` / `removeComponents`'s
454
+ * `_scratchTargetMask`). The Archetype and the archetypeMap bucket
455
+ * both keep references long-term, so the clone is required for
456
+ * correctness — and is the previously-implicit guarantee made by the
457
+ * mask-allocating callers (`copyWithSet`, `copyWithClear`, etc.). */
458
+ private archInstall;
459
+ /** Resolve "add component_id to archetype_id" → target ArchetypeID. Caches the edge. */
460
+ private archResolveAdd;
461
+ /** Resolve "remove component_id from archetype_id" → target ArchetypeID. Caches the edge. */
462
+ private archResolveRemove;
463
+ /** Cache a bidirectional add/remove edge between two archetypes. */
464
+ private archCacheEdge;
465
+ createEntity(): EntityID;
466
+ /** Slot index of the entity allocated by the most recent `_allocEntity`
467
+ * call. Read immediately after the call (alloc-free out-param). */
468
+ private _spawnIndex;
469
+ /** Allocate an entity slot WITHOUT placing it in the empty archetype, for
470
+ * the template spawn paths. Returns the packed `EntityID`; the slot index is
471
+ * left in `_spawnIndex`. Mirrors `createEntity`'s allocator but skips the
472
+ * empty-archetype membership write (the caller installs the real archetype
473
+ * + row). Kept separate from `createEntity` so that hot path stays inline
474
+ * (#368). This *commits* the slot (bumps counts, stamps the generation so
475
+ * `isAlive` is already true), so the caller MUST have reserved the column
476
+ * capacity for the row first (`Archetype.ensureRowCapacity`) — otherwise a
477
+ * cap throw from the subsequent append leaves the slot phantom-alive (#775). */
478
+ private _allocEntity;
479
+ /** Pre-check that `count` fresh entity slots can be allocated without
480
+ * exhausting the entity-index space, so `spawnMany` commits all-or-nothing
481
+ * (#775). `_allocEntity`'s own per-call high-water guard would otherwise
482
+ * throw `EID_MAX_INDEX_OVERFLOW` partway through the alloc loop, leaving the
483
+ * slots it already committed phantom-alive. Free-list reuse covers the first
484
+ * `entityFreeIndices.length` slots; only the remainder draws down the
485
+ * high-water headroom. */
486
+ private _ensureEntityIndexCapacity;
487
+ /** Resolve a template: compute the target archetype (creating it if absent —
488
+ * fits the prewarm model), pre-flatten default field values into
489
+ * `_flatColumns` order, and build the override index (field name → flat
490
+ * column index; `TEMPLATE_OVERRIDE_AMBIGUOUS` for a name shared by more than
491
+ * one component, which a flat override cannot target). */
492
+ resolveTemplate<Defs extends readonly ComponentDef[]>(entries: TemplateEntries<Defs>): Template<Defs>;
493
+ /** Apply per-instance overrides to the freshly-spawned row. Each key is a
494
+ * field name resolved through the template's override index. */
495
+ private _applyOverrides;
496
+ /** Spawn one entity directly into the template's archetype (zero archetype
497
+ * transitions). Writes the template defaults in a single append pass, then
498
+ * applies any per-instance overrides. */
499
+ spawn(p: Template, overrides?: Record<string, number | undefined>): EntityID;
500
+ /** Bulk-spawn `count` identical entities into the template's archetype. The
501
+ * field writes are O(columns) — one `TypedArray.fill` per column via
502
+ * `addEntitiesWithValues` — not O(count×columns). Returns the new ids in
503
+ * spawn order. */
504
+ spawnMany(p: Template, count: number): EntityID[];
505
+ /** Immediately destroy an entity, removing it from its archetype.
506
+ *
507
+ * With no `delete`/`clear` target-cleanup policy registered (the common
508
+ * case) this tears the one entity down and returns — no allocation. When a
509
+ * policy is in play, a `delete`-target's sources are appended to a local
510
+ * work-list this method then drains in the same iterative pass (#473, #492):
511
+ * the `work.length` re-read drives chains and trees out without recursion, so
512
+ * depth is bounded by entity count, not tree depth. This mirrors the deferred
513
+ * `flushDestroyed` buffer mechanism — both paths are iterative and reach the
514
+ * identical end state; the only difference is the shared `pendingDestroy`
515
+ * buffer there vs. a local work-list here. `isAlive` dedups a source reached
516
+ * twice (diamonds) and terminates cycles, exactly as the generation guard does
517
+ * in the deferred loop. */
518
+ destroyEntity(id: EntityID): void;
519
+ /** Tear a single entity out of its archetype, relation, and sparse stores,
520
+ * then recycle (or retire) its slot. Shared by both immediate-destroy entry
521
+ * points (the fast no-cascade path and the work-list driver in
522
+ * `destroyEntity`). When `cascade` is non-null, a `delete`-policy target's
523
+ * surviving sources are appended to it for the driver to drain (#473, #492);
524
+ * `null` skips that collection for callers that cannot cascade. The caller
525
+ * must have already confirmed `id` is alive. */
526
+ private _destroyOne;
527
+ /**
528
+ * Liveness check, **fail-closed** against forged / retired / out-of-bounds
529
+ * handles (#778). For a general-purpose engine that may receive a handle from
530
+ * serialization, IPC, or any untrusted caller, three malformed inputs must read
531
+ * dead rather than alias a slot:
532
+ * - **Out of range** — an `id` outside the 31-bit packed space (`< 0` or
533
+ * `> MAX_ENTITY_ID`). Without this, the 20-bit index mask below silently
534
+ * folds garbage high bits onto a valid slot. (Same bound the snapshot /
535
+ * postMessage decode applies, #723.)
536
+ * - **Tombstone generation** — a handle carrying `RETIRED_GENERATION`, which
537
+ * the allocator stamps into a retired slot and never issues to a live
538
+ * entity, would otherwise match a retired slot's parked generation and read
539
+ * alive (the ABA tombstone, previously documented as a known gap).
540
+ * Both guards are comparisons predicted not-taken on the live path, so a
541
+ * well-formed handle pays two branches and nothing else (#778 measured).
542
+ */
543
+ isAlive(id: EntityID): boolean;
544
+ get entityCount(): number;
545
+ /** An archetype's row count moved from `preLen` to its current
546
+ * `arch.length` on a **shrink** (rows removed: the source of a transition, a
547
+ * destroy, a batch-source drain). Always marks SAB row counts dirty (#324 —
548
+ * the descriptor walk just needs "something moved"); bumps the query-dirty
549
+ * epoch (#327) only on a `length` 0/non-zero crossing, the only case where
550
+ * `Query._nonEmptyArchetypes` can change on a shrink (#328). Mutations that
551
+ * move row counts within the same side (6→5) leave the non-empty set unchanged
552
+ * and skip the bump.
553
+ *
554
+ * A shrink does **not** need the `enabledCount` crossing test (#812): the
555
+ * only enabled-count move it can make is 1→0 (the last enabled row leaves an
556
+ * archetype that keeps disabled rows), which leaves the archetype in a default
557
+ * query's non-empty list as a harmless stale *inclusion* — `count`/`forEach`
558
+ * bound on `enabledCount` (now 0) iterate it zero times. Only a **grow** into
559
+ * an all-disabled archetype can stale-*exclude* a live row, so the enabled
560
+ * crossing lives in `_onArchGrow`, off this path.
561
+ *
562
+ * **Inlining-sensitive — keep the body tiny.** This function is called
563
+ * once or twice per immediate-mode `addComponent` / `removeComponent` and the
564
+ * mutation hot path depends on it being inlined at every call site.
565
+ * #351 / PR #353 added an `if (registeredQueries.length === 0) return;`
566
+ * gate to skip the bump for no-query workloads — bench showed it
567
+ * regressed `mutation: churn_loop` 16-18% because the extra statement
568
+ * pushed the function past V8's per-call inlining budget. Reverted in
569
+ * PR #355. Any future change here needs a `bench-vs-commit` run before
570
+ * merging, not just code review. */
571
+ private _onArchLenChange;
572
+ /** An archetype **grew** — rows were appended (the target of a transition, a
573
+ * spawn, a batch-target fill). Like `_onArchLenChange` it marks row counts
574
+ * dirty and bumps the query-dirty epoch on a `length` 0/non-zero crossing
575
+ * (`includeDisabled` membership), but it *also* bumps on an `enabledCount`
576
+ * 0→1 crossing (#812). The non-empty filter is field-split (#577): a default
577
+ * query keeps archetypes with `enabledCount > 0`. An enabled row appended to
578
+ * an archetype that is non-empty but all-disabled (`length > 0,
579
+ * enabledCount == 0`) crosses `enabledCount` 0→1 without touching `length`,
580
+ * so the `preLen` test alone (the valid-while-`enabledCount === length`
581
+ * pre-#577 proxy) misses it and a cached default query keeps a stale
582
+ * `_nonEmpty` list. Only grows can do this, so only grow sites carry the test.
583
+ *
584
+ * **Precondition: ≥1 row was appended** (every caller adds at least one row),
585
+ * so `arch.length > 0` afterward — which is why the crossings simplify and the
586
+ * body stays inlinable (the inlining caveat on `_onArchLenChange` applies
587
+ * here too; verified with `bench-vs-commit mutation`). The general
588
+ * `(pre === 0) !== (post === 0)` boundary test collapses given the post side:
589
+ * - `length`: post > 0 always ⇒ a crossing iff `preLen === 0`.
590
+ * - `enabledCount`: non-decreasing on a grow ⇒ a 0-crossing iff it was 0
591
+ * before and is non-zero now (`preEnabled === 0 && enabledCount !== 0`);
592
+ * a disabled-row append leaves it 0 and correctly skips. The `enabledCount`
593
+ * read is short-circuited away on the hot path (`preLen` or `preEnabled`
594
+ * non-zero), so a no-disabled workload pays only two scalar compares. */
595
+ private _onArchGrow;
596
+ /** Dirty bookkeeping for an enable/disable toggle (#577). `length` is
597
+ * unchanged (no row added/removed) but `enabled_count` moved, so: republish
598
+ * row counts (the descriptor's `enabled_count` changed, so the WASM sim and
599
+ * snapshot see the new partition), and bump the query epoch only when the
600
+ * *enabled* count crossed 0 — the boundary at which an archetype enters/leaves
601
+ * a query's non-empty set (`Query._nonEmpty` filters on `entityCount`, which
602
+ * is now `enabled_count`). */
603
+ private _onArchEnabledChange;
604
+ /** Immediately disable an entity (idempotent). The entity must hold at least
605
+ * one component — a component-less entity occupies no archetype row, so it
606
+ * cannot be partitioned (a `__DEV__` error; prod no-op). */
607
+ disableEntity(id: EntityID): void;
608
+ /** Immediately enable an entity (idempotent). */
609
+ enableEntity(id: EntityID): void;
610
+ /** Whether `id` is currently disabled. A component-less entity is never
611
+ * disabled (it has no row to partition). */
612
+ isDisabled(id: EntityID): boolean;
613
+ /** 0-crossing detection for the per-entity flush paths (`_flushAdds`,
614
+ * `_flushRemoves`) without per-entity Map traffic — the same cost the
615
+ * destroy drain shed in #457. Each touched archetype is stamped with the
616
+ * current flush epoch (`Archetype._flushSeenEpoch`), its pre-length and
617
+ * pre-enabled-count recorded on first sight (`_flushPreLen` /
618
+ * `_flushPreEnabled`), and pushed onto this scratch list;
619
+ * `_settleFlushDirty` walks the list once after the loop. The field
620
+ * accesses per entity replace a `Map.has` + `Map.set` hash probe pair. The
621
+ * epoch is bumped at settle so the next flush re-records. */
622
+ private _flushEpoch;
623
+ private readonly _flushTouched;
624
+ /** Resolve dirty flags for a per-entity batch flush from the captured
625
+ * pre-counts. Marks row counts dirty if any archetype was touched; bumps
626
+ * the query epoch once if any touched archetype crossed the 0 boundary on
627
+ * *either* `length` (includeDisabled membership) or `enabledCount`
628
+ * (default-query membership) — the deferred analog of the immediate
629
+ * `_onArchLenChange` two-field check (#812). A single bump is sufficient
630
+ * (queries only need to know "something changed"). Clears the touched list
631
+ * and advances the flush epoch on exit. */
632
+ private _settleFlushDirty;
633
+ destroyEntityDeferred(id: EntityID): void;
634
+ /** Buffer an enable/disable toggle for the phase flush (#577). The row swap a
635
+ * toggle performs would corrupt a `forEach` over that archetype if applied
636
+ * mid-system, so it is deferred like add/remove. */
637
+ disableEntityDeferred(id: EntityID): void;
638
+ enableEntityDeferred(id: EntityID): void;
639
+ /** Drain buffered enable/disable toggles, applying each in operation order via
640
+ * the immediate path (which is idempotent and updates dirty flags). Called at
641
+ * the flush boundary after structural adds/removes settle, so a toggle sees the
642
+ * entity's final archetype placement for the tick.
643
+ *
644
+ * When an onDisable/onEnable observer is registered (`_toggleObserverCount >
645
+ * 0`, #677) this also collects effective toggle events into `_obsEvents` for
646
+ * the dispatch hook, collapsed to one event per *net* transition across the
647
+ * drain (see `_toggleInitial`). The no-observer path is byte-for-byte the
648
+ * pre-#677 drain. */
649
+ private _flushToggles;
650
+ /** Fan one entity's net toggle transition out to an onDisable / onEnable event
651
+ * per carried component (#677). Walks the entity's archetype mask through the
652
+ * matching pre-bound bit visitor; a component-less entity (no row) carries
653
+ * nothing and is skipped. */
654
+ private _collectToggle;
655
+ get pendingToggleCount(): number;
656
+ /** Flush all buffered entity destructions in batch.
657
+ *
658
+ * When onRemove observers are registered (`_structuralObserverCount > 0`),
659
+ * a destroy fires onRemove for every component the entity carried — a destroy
660
+ * *is* a remove of the whole mask — collected here and dispatched by the
661
+ * `flushStructural` fixed-point loop, the only caller in that mode (it drains
662
+ * `pendingDestroy` each round so the trailing `ctx.flush()` call is a no-op).
663
+ * Same commit-then-observe discipline as `_flushRemoves`: the entity is fully
664
+ * freed before the callback runs, so onRemove receives the (now dead) eid as
665
+ * the identity of what was destroyed, not a live handle to read. The
666
+ * no-observer path is byte-for-byte unchanged (`collecting` gate). #531.
667
+ *
668
+ * Re-entrancy: while the observed fixed point owns the flush
669
+ * (`_flushingStructural`), the loop drains destroys itself via
670
+ * `_drainDestroyed`, so a re-entrant `ctx.flush()` from a callback no-ops
671
+ * here — otherwise it would collect into the shared `_obsEvents` scratch
672
+ * mid-dispatch and corrupt it (mirrors the `flushStructural` guard). */
673
+ flushDestroyed(): void;
674
+ private _drainDestroyed;
675
+ get pendingDestroyCount(): number;
676
+ addComponentDeferred(entityId: EntityID, def: ComponentDef<Record<string, never>>): void;
677
+ addComponentDeferred<S extends ComponentSchema>(entityId: EntityID, def: ComponentDef<S>, values: FieldValues<S>): void;
678
+ removeComponentDeferred(entityId: EntityID, def: ComponentDef): void;
679
+ flushStructural(): void;
680
+ /** Batch-apply all deferred component additions. */
681
+ private _flushAdds;
682
+ /** Batch-apply all deferred component removals. */
683
+ private _flushRemoves;
684
+ get pendingStructuralCount(): number;
685
+ /** Set the per-component observation flags from the registry's aggregate of
686
+ * live observers for `cid`. Maintains `_structuralObserverCount` and
687
+ * `_toggleObserverCount` (#677) (the fast-path gates) and lazily allocates the
688
+ * dirty list when per-entity onSet tracking turns on. */
689
+ _configureComponentObservation(cid: number, hasAdd: boolean, hasRem: boolean, hasDisable: boolean, hasEnable: boolean, trackDirty: boolean): void;
690
+ /** Record a per-entity onSet "changed" event for the entity. Called from the
691
+ * field-write path (`SystemContext.setField` / `markChanged`) and gated by
692
+ * the caller on `_anyDirtyTracked`. Appends to the dirty list only if the
693
+ * dedup bit was clear (the ADR-0012 list + dedup-bit mechanism). */
694
+ _noteSet(def: ComponentHandle, eid: EntityID): void;
695
+ private _growDirtyMarks;
696
+ /** Detach and return the dirty-row list for `cid`, clearing its dedup bits and
697
+ * leaving the store with a fresh empty list (so re-dirties during the drain
698
+ * accumulate for the NEXT tick, not this one). Returns a shared empty array
699
+ * when nothing is dirty. Caller owns the returned array. */
700
+ _takeDirty(cid: number): EntityID[];
701
+ /** Clear any dirty dedup bits for a freed entity index across every tracked
702
+ * component, so a recycled slot at the same index can be marked afresh. Gated
703
+ * by `_anyDirtyTracked` at the destroy call sites. */
704
+ private _clearDirtyForIndex;
705
+ /** Visit every non-empty archetype containing `cid` whose component-column
706
+ * changed at or after `baseline`, in canonical (ascending archetype-id) order
707
+ * — the archetype-granular onSet detection point. Reuses the existing
708
+ * per-archetype change tick (free; no write-path cost). */
709
+ _forEachChangedArchetype(cid: number, baseline: number, cb: (arch: Archetype) => void): void;
710
+ /** Enabled live entities currently carrying `cid`, used by `yieldExisting` to
711
+ * replay onAdd on registration. Bounded by `enabled_count` (#677): a disabled
712
+ * entity is excluded from default queries, so seeding it via onAdd would
713
+ * publish a row that an immediate onDisable should have removed — it is simply
714
+ * absent at seed (the "delete on disable" semantics). Unordered here — the
715
+ * registry radix-sorts. */
716
+ _collectEntitiesWithComponent(cid: number): EntityID[];
717
+ registerComponent<S extends Record<string, TypedArrayTag>>(schema: S): ComponentDef<S>;
718
+ /** Return the field index assigned to `(def, fieldName)` at component
719
+ * registration. Indexes are insertion-order, zero-based, and stable for
720
+ * the lifetime of the ECS. Used by systems that pass `(component_id,
721
+ * field_id)` pairs across the WASM FFI (PR 3C / #231). */
722
+ fieldIdOf(def: ComponentHandle, fieldName: string): number;
723
+ /** Register a sparse component or tag. Unlike `registerComponent`, this
724
+ * allocates from a separate id space and never touches the archetype mask,
725
+ * so it does **not** count against `STORE_DESCRIPTOR_COMPONENT_LIMIT`. See
726
+ * ADR-0011 and `sparse_store.ts`. */
727
+ registerSparseComponent<S extends Record<string, TypedArrayTag>>(schema: S): SparseComponentDef<S>;
728
+ /** Allocate the backing sparse store WITHOUT the #777 float guard, for
729
+ * engine-internal backings whose `f64` holds an EXACT integer rather than a
730
+ * user quantity: the exclusive-relation `{ target }` slot stores an `EntityID`
731
+ * (≤ 2^53, so f64 is bit-exact and cross-host identical — the ban targets float
732
+ * *arithmetic* rounding, which a target slot never undergoes). User schemas go
733
+ * through `registerSparseComponent`, which guards first. */
734
+ private _pushSparseStore;
735
+ private sparseStoreOf;
736
+ /** Add (or overwrite) a sparse component on an entity. No archetype
737
+ * transition, no row copy — the entity's `archetype_id` is unchanged. */
738
+ addSparse(entityId: EntityID, def: SparseComponentDef, values?: Record<string, number>): void;
739
+ /** Remove a sparse component from an entity. No-op if absent. */
740
+ removeSparse(entityId: EntityID, def: SparseComponentDef): void;
741
+ hasSparse(entityId: EntityID, def: SparseComponentDef): boolean;
742
+ getSparseField(entityId: EntityID, def: SparseComponentDef, field: string): number;
743
+ setSparseField(entityId: EntityID, def: SparseComponentDef, field: string, value: number): void;
744
+ /** Drop all sparse data for a destroyed entity index so a recycled slot
745
+ * can't inherit it. Gated by the caller on `sparseStores.length > 0` to
746
+ * keep the destroy hot path free when sparse storage is unused. */
747
+ private _purgeSparse;
748
+ /** Serialize the sparse stores **and** relation side data to a self-contained
749
+ * byte buffer — the sparse half of a world snapshot (the dense half is the
750
+ * SAB snapshot). Two framed sections: the sparse stores (`snapshot_sparse_-
751
+ * stores` — exclusive relation targets + multi membership ride here) followed
752
+ * by the relation side data (`snapshotRelations` — multi forward target
753
+ * sets, which live outside the sparse store). Both are written in canonical
754
+ * entity-index order, so two worlds with identical contents inserted in
755
+ * different orders snapshot byte-for-byte the same (#470). The reverse index
756
+ * is derived and never serialized — `restoreSparse` rebuilds it. Pairs with
757
+ * `restoreSparse`.
758
+ *
759
+ * **Opt-in (#626 / ADR-0020).** Throws `DETERMINISM_DISABLED` unless the
760
+ * Store was constructed with `{ deterministic: true }` — the canonical
761
+ * entity-index ordering is the determinism tax the flag gates. */
762
+ snapshotSparse(): Uint8Array;
763
+ /** Repopulate the sparse stores from `snapshotSparse` bytes, replacing all
764
+ * current sparse data (full-equality round-trip of membership + data), then
765
+ * rebuild every relation's derived side indices: multi forward sets from the
766
+ * relation section, and the reverse index for both cardinalities (exclusive
767
+ * from the just-restored sparse target field, multi from the rebuilt forward
768
+ * sets). The sparse components and relations must already be registered in
769
+ * the same order — restore carries data, not the registration (which is
770
+ * code). Throws `SparseRestoreError` if the snapshot's shape, field identity,
771
+ * entity-index bounds, or frame length don't validate.
772
+ *
773
+ * **Opt-in (#626 / ADR-0020).** Throws `DETERMINISM_DISABLED` unless the
774
+ * Store was constructed with `{ deterministic: true }`; paired with
775
+ * `snapshotSparse`, which produces the canonical bytes restore consumes. */
776
+ restoreSparse(bytes: Uint8Array): void;
777
+ /** Rebuild every relation's derived side indices after the sparse stores have
778
+ * been restored. `restoreRelations` resets all relations, rebuilds the multi
779
+ * forward sets + their reverse edges from `relBytes`, and validates shape.
780
+ * The exclusive reverse index can't be carried in the bytes (it's derivable),
781
+ * so it's rebuilt here from the backing sparse store: every member row holds
782
+ * `(source index → target EntityID)`, which is exactly one reverse edge. */
783
+ private _rebuildRelationIndices;
784
+ /**
785
+ * Capture the full live world to one self-contained byte buffer that
786
+ * `restoreInto` can mount back onto a live, ticking world ("rewind a running
787
+ * world and keep ticking"). Three sections (see `resume.ts`): the dense SAB
788
+ * column bytes (`snapshotColumnStore`), the sparse + relation bytes
789
+ * (`snapshotSparse`), and the host-side bookkeeping the SAB omits — the world
790
+ * tick, the entity recycle free-list (in live order; no byte source, and its
791
+ * order is load-bearing for byte-identical resume), the alive count, and each
792
+ * archetype's `length` / `enabledCount`.
793
+ *
794
+ * **Opt-in (ADR-0020).** Throws `DETERMINISM_DISABLED` unless constructed with
795
+ * `{ deterministic: true }` — the sparse section rides the canonical-ordering
796
+ * surface and byte-identical resume is a determinism property. Pairs with
797
+ * `restoreInto`.
798
+ *
799
+ * **v1 scope.** Resources + events are NOT captured (resume requires
800
+ * resource-free per-tick state; events are tick-cleared). Change-detection /
801
+ * scheduler baselines (`changed()` queries) are likewise not captured — they
802
+ * are scheduling artifacts, never folded into `stateHash`. Take the snapshot
803
+ * at a tick boundary (between `update()`s). See the ADR. */
804
+ snapshot(): Uint8Array;
805
+ /** Gather the host-side state a snapshot carries alongside the dense + sparse
806
+ * bytes — see `snapshot()`. The free-list is copied (it's a live mutable). */
807
+ private _collectHostState;
808
+ /**
809
+ * Mount a `snapshot()` buffer onto this live world and leave it ready to keep
810
+ * ticking. Fails closed on a malformed frame or a registration mismatch
811
+ * BEFORE any live state is touched (the archetype/component graph is rebuilt
812
+ * from code, not the snapshot — same contract as `restoreSparse`). On
813
+ * success the world's dense + sparse state, entity allocator, and tick are
814
+ * exactly the captured world's.
815
+ *
816
+ * Requires a world whose SAB-backed archetype set + column layout match the
817
+ * snapshot's exactly (prewarm so the archetype set is stable) and the same
818
+ * entity-index capacity. **Opt-in (ADR-0020):** throws `DETERMINISM_DISABLED`
819
+ * unless `{ deterministic: true }`. See `snapshot()` for the v1 scope. */
820
+ restoreInto(bytes: Uint8Array): void;
821
+ /** Read-only validation of a `snapshotSparse` SECTION (the framed
822
+ * `[sparseLen][relLen][sparse][rel]` buffer) against the live registry, used
823
+ * by `restoreInto` to fail closed on a sparse-registration mismatch BEFORE the
824
+ * dense mount commits. Mirrors `restoreSparse`'s frame check, then validates the
825
+ * sparse-stores sub-section without mutating. A relation-registration difference
826
+ * surfaces here too: every `registerRelation` adds a backing sparse store, so a
827
+ * differing relation set changes the sparse store count / schema. Throws
828
+ * `SparseRestoreError` on a mismatch. */
829
+ private _assertSparseSectionMatches;
830
+ /** Rebuild each SAB-backed archetype's host-side `length` / `enabledCount` /
831
+ * `_entityIds` after the dense backing was swapped in `restoreInto`. `length`
832
+ * + the per-row entity-id back-reference come from a scan of the restored
833
+ * entity-index region (which entity occupies which row); `enabledCount` comes
834
+ * from the captured host-state (the #577 partition boundary is positional only
835
+ * — it has no per-entity byte source). */
836
+ private _reconstructHostRows;
837
+ /** Register a relation kind. `exclusive` (the default) → one target per
838
+ * source, stored in a backing `{ target: f64 }` sparse component, so the
839
+ * forward index rides the sparse store and inherits query membership (#469),
840
+ * `stateHash` + snapshot/restore (#470) for free. `multi` → a set of
841
+ * targets per source, backed by a sparse tag for membership plus a side
842
+ * forward index the relation owns. The two cardinalities are mutually
843
+ * exclusive. `onDeleteTarget` selects the cleanup policy run when a target
844
+ * is destroyed — `delete` (cascade-destroy sources), `clear` (drop the link,
845
+ * sources survive), or `orphan` (default: leave it dangling, #473). See
846
+ * ADR-0011 and `relation.ts`.
847
+ *
848
+ * The backing sparse store is resolved and handed to the relation so it can
849
+ * drive forward/membership rows directly — the cardinality-specific
850
+ * interaction is `ExclusiveRelationStore` / `MultiRelationStore`'s, not a
851
+ * branch here (#498). */
852
+ registerRelation(opts?: RelationOptions): RelationDef;
853
+ private relationOf;
854
+ /** Number of registered relations. Visible to tests asserting the
855
+ * no-transition invariant alongside `archetype_count`. */
856
+ get relationCount(): number;
857
+ /** Add a `(R, tgt)` pair to `src`. No archetype transition. Exclusive:
858
+ * replaces any existing target (engine-enforced one-per-source), a no-op if
859
+ * `tgt` is already the target. Multi: adds `tgt` to the set, a no-op if
860
+ * already present. A dead `src` *or* `tgt` is caller error: it throws in
861
+ * `__DEV__` and is a no-op in production — symmetric, so a production build
862
+ * never links a reverse-index entry keyed by a destroyed handle (#495). */
863
+ addRelation(src: EntityID, def: RelationDef, tgt: EntityID): void;
864
+ /** Remove a `(R, tgt)` pair from `src`. No archetype transition. Exclusive:
865
+ * `tgt` is optional and the removal is a no-op when it names a target other
866
+ * than the current one. Multi: omitting `tgt` removes *all* of `src`'s
867
+ * targets; passing one removes just that pair (dropping membership when the
868
+ * set empties). */
869
+ removeRelation(src: EntityID, def: RelationDef, tgt?: EntityID): void;
870
+ /** The single target of `src` under an exclusive relation, or `undefined`.
871
+ * Throws in `__DEV__` on a multi-target relation (use `targetsOf`). */
872
+ targetOf(src: EntityID, def: RelationDef): EntityID | undefined;
873
+ /** All targets of `src` under `R` — one or zero for exclusive, the full set
874
+ * for multi — ascending by id. */
875
+ targetsOf(src: EntityID, def: RelationDef): EntityID[];
876
+ /** Sources that point at `tgt` under `R` (the reverse index), ascending by
877
+ * id; empty when none. */
878
+ sourcesOf(def: RelationDef, tgt: EntityID): EntityID[];
879
+ /** Whether `src` holds any pair under `R`. */
880
+ hasRelation(src: EntityID, def: RelationDef): boolean;
881
+ /** All `(source, target)` pairs of relation `R` — the `(R, *)` wildcard
882
+ * (#472). Sources are emitted in **canonical entity-index order** (the #470
883
+ * determinism convention): exclusive relations ride the backing sparse
884
+ * store's `canonicalIndices`, multi relations ride the same
885
+ * `forEachCanonicalTargetSet` traversal `stateHash` / `snapshotRelations`
886
+ * use; a multi source's targets follow ascending by id. Empty when the
887
+ * relation holds no pairs. Cold path — allocates the result array (and, for
888
+ * multi, sorts each source's target set); not for per-tick use. The
889
+ * point-query forms are `targetOf` / `targetsOf`. */
890
+ pairsOf(def: RelationDef): [EntityID, EntityID][];
891
+ /** Every `(relation, source)` pointing at `tgt`, across **all** registered
892
+ * relation kinds — the `(*, T)` wildcard (#472). Walks the relation registry
893
+ * in id order (each relation's reverse index already returns sources
894
+ * ascending by id), so the result is ordered by relation id then source id.
895
+ * Empty when nothing targets `tgt`. The single-relation form is
896
+ * `sourcesOf(def, tgt)`. */
897
+ sourcesOfAny(tgt: EntityID): [RelationDef, EntityID][];
898
+ /** The backing sparse component id of relation `R` — the membership store a
899
+ * `(R, *)` wildcard term (`Query.withRelation`) drives through the shared
900
+ * sparse-match path. Exclusive relations back a `{ target: f64 }` sparse
901
+ * component, multi a tag; both carry per-source membership, so "has any
902
+ * `(R, *)` pair" is exactly membership in this store (including an
903
+ * orphan-dangling source, whose membership row persists — consistent with
904
+ * `pairsOf`). The def is engine-owned and never handed to callers; this hands
905
+ * the query builder only its erased id for the `sparseInclude` list. */
906
+ relationBackingSparseId(def: RelationDef): SparseComponentID;
907
+ /** Drive a `(*, T)` wildcard query (`Query.forEachRelatedTo`): every source
908
+ * related to `target` under **any** relation, intersected with the query's
909
+ * dense mask + sparse require/exclude terms + the default enabled-row filter,
910
+ * each source yielded once. Unions `sourcesOf(R, target)` across every
911
+ * relation into a `Set` (dedup by full `EntityID` — a source related to `T`
912
+ * via two relations is yielded once), then sorts ascending: the cross-relation
913
+ * union has no inherent order, so one cold sort gives a deterministic,
914
+ * canonical `(*, T)` order matching `sourcesOf` / `sourcesOfAny` (which sort
915
+ * the same way). Cold/structural — not a per-tick hot loop over many targets.
916
+ * `sparseInclude`/`sparseExclude` carry both raw sparse terms and the backing
917
+ * stores of any composed `(R, *)` terms, so it intersects with them uniformly. */
918
+ _forEachRelationTargetMatch(target: EntityID, include: BitSet, exclude: BitSet | null, anyOf: BitSet | null, sparseInclude: readonly SparseComponentID[], sparseExclude: readonly SparseComponentID[], includeDisabled: boolean, cb: (entityId: EntityID) => void): void;
919
+ /** Reclaim reverse-index memory: drop every relation's reverse entries whose
920
+ * **target** has been destroyed, returning the total dropped across all
921
+ * relations (#491). Under the default `orphan` policy a destroyed target
922
+ * leaves its reverse entry intact until each source re-targets or dies, so a
923
+ * long-lived source that orphan-points at a churn of short-lived targets and
924
+ * never re-targets accumulates dead-target keys without bound. This cold-path
925
+ * hook drops them on demand — call it at scene or snapshot boundaries.
926
+ *
927
+ * Purely a memory reclaim with no observable state change: forward links stay
928
+ * dangling (so `orphan`'s `targetOf`-returns-the-dead-handle contract is
929
+ * unchanged), `stateHash` is unaffected (the reverse index is derived, never
930
+ * folded), and the dropped entries are faithfully rebuilt by snapshot/restore
931
+ * from the surviving forward links (`_rebuildRelationIndices`). The only
932
+ * difference a caller can see is `sourcesOf(R, deadHandle)` going from the
933
+ * dangling sources to `[]` — both meaningless once the target is gone.
934
+ * No-op (returns 0) when no relations are registered. */
935
+ compactRelations(): number;
936
+ /** Walk exclusive relation `R` from `src` toward the root, returning the
937
+ * **up**-chain `[src, parent, grandparent, …, root]` (nearest-ancestor-first,
938
+ * inclusive of both endpoints). A source with no target returns `[src]`. The
939
+ * root is the first entity in the chain with no `R`-target, **or** a dangling
940
+ * dead target handle (see below). Throws `RELATION_MODE_MISMATCH` on a multi
941
+ * relation and `RELATION_CYCLE` on a cycle (both `__DEV__`-only; in production
942
+ * a cycle stops at the repeated node).
943
+ *
944
+ * **Dangling links terminate the chain.** Under the `orphan` policy a source
945
+ * keeps pointing at a destroyed target (a dead handle). The walk must *not*
946
+ * advance through such a handle: the backing store is keyed by entity
947
+ * **index**, so reading the dead handle's index would return whatever now
948
+ * occupies that recycled slot — splicing the chain onto an unrelated entity
949
+ * (the ABA the `EntityID`-keyed reverse index avoids for `cascadeOf`). So a
950
+ * dead next-hop is appended as the chain's (dangling) terminus and the walk
951
+ * stops; the caller can detect it with `isAlive`, exactly as `targetOf`
952
+ * returns a dead handle. */
953
+ ancestorsOf(src: EntityID, def: RelationDef): EntityID[];
954
+ /** The root of `src`'s exclusive-relation chain — the last entity of
955
+ * `ancestorsOf` (the one with no `R`-target). `src` itself when it has no
956
+ * target. If the chain ends in a dangling dead target handle (orphan policy),
957
+ * that handle is the root — `isAlive`-check the result if dangling links are
958
+ * possible. Same `__DEV__` guards as `ancestorsOf`. */
959
+ rootOf(src: EntityID, def: RelationDef): EntityID;
960
+ /** Walk exclusive relation `R` **down** from `root` over the reverse index,
961
+ * returning the subtree (including `root`) breadth-first — **parents before
962
+ * children** (the `cascade` order). Children of each node come from
963
+ * `sourcesOf` (ascending by id), so the traversal is deterministic. Throws
964
+ * `RELATION_MODE_MISMATCH` on a multi relation and `RELATION_CYCLE` on a cycle
965
+ * (both `__DEV__`-only; in production an already-visited node is skipped, so
966
+ * it never hangs). */
967
+ cascadeOf(root: EntityID, def: RelationDef): EntityID[];
968
+ /** Purge a destroyed entity from the relation indices — its **source** role
969
+ * only: drop its forward target(s) and unlink it from every target's reverse
970
+ * set. The backing sparse membership row is dropped separately by
971
+ * `_purgeSparse`, so this must run *before* it (it reads the sparse target
972
+ * field for exclusive relations). The entity's **target** role is left
973
+ * intact: the reverse index is keyed by full `EntityID`, so a recycled slot
974
+ * never aliases the dead target's sources. The destroyed entity's **target**
975
+ * role is handled separately by `_cleanupRelationTargets` per each
976
+ * relation's `OnDeleteTarget` policy (#473). Gated by the caller on
977
+ * `relations.length > 0`. */
978
+ private _purgeRelations;
979
+ /** Apply each relation's `OnDeleteTarget` policy for a destroyed **target**
980
+ * `targetId` (#473). Walks the registry; for every relation whose reverse
981
+ * index has sources pointing at the dead target:
982
+ *
983
+ * - **`delete`** — append each source to `cascade`; the caller destroys them
984
+ * through the same path, so chains/trees cascade recursively. The sources'
985
+ * own source-role purge (`_purgeRelations`) drops their forward link and
986
+ * unlinks them from `targetId`'s reverse set, so no reverse entry leaks.
987
+ * - **`clear`** — drop each source's link to the dead target in place via
988
+ * `rs.unlink(src, targetId)` (exclusive drops the row; multi removes the
989
+ * target from the set, dropping membership when it empties). Sources
990
+ * survive — the cardinality bookkeeping is the relation's, not branched here.
991
+ * - **`orphan`** — skipped (the link is left dangling; safe because the
992
+ * reverse key carries the generation).
993
+ *
994
+ * `sourcesOf` returns a fresh snapshot, so mutating the reverse index while
995
+ * iterating is safe. Gated by the caller on `_hasTargetCleanup`, so the
996
+ * whole walk is skipped when no relation opts into a non-`orphan` policy. */
997
+ private _cleanupRelationTargets;
998
+ /** Second query-match path (#469 / ADR-0011): iterate entities matching a
999
+ * dense mask **and** sparse-membership terms, invoking `cb` per entity.
1000
+ * Yields `EntityID`s, not archetype spans — sparse members are scattered
1001
+ * across archetypes, so there is no SoA column to hand back. Driven by the
1002
+ * cheapest candidate set:
1003
+ *
1004
+ * - **sparse require present** → walk the *smallest* required store's
1005
+ * `indices` (an upper bound on the result), filtering each by the other
1006
+ * required stores, the excluded stores, and the dense mask resolved from
1007
+ * the entity's own archetype. Independent of archetype count.
1008
+ * - **sparse exclude only** → walk `denseArchetypes` (already dense-mask-
1009
+ * matched and non-empty by the caller), skipping rows in any excluded
1010
+ * store.
1011
+ * - **neither** → walk `denseArchetypes`' entity ids (dense-only fallback).
1012
+ *
1013
+ * Only reached via `Query.forEachEntity`; dense `forEach` never consults
1014
+ * the sparse stores, so dense-only queries are unaffected (#469 AC). */
1015
+ _forEachSparseMatch(include: BitSet, exclude: BitSet | null, anyOf: BitSet | null, sparseInclude: readonly SparseComponentID[], sparseExclude: readonly SparseComponentID[], denseArchetypes: readonly Archetype[], cb: (entityId: EntityID) => void, includeDisabled: boolean): void;
1016
+ /** Fourth query-match path (#581): yield the matched entities — the exact
1017
+ * `_forEachSparseMatch` intersection (dense mask + sparse require/exclude +
1018
+ * the default enabled-row filter) — in canonical **hierarchy depth order** over
1019
+ * exclusive relation `R`: depth ascending (parents before children), **entity
1020
+ * index ascending within each depth band**. Entities deeper than `maxDepth`
1021
+ * are skipped (`Infinity` = unbounded). Only reached via `Query.forEachEntity`
1022
+ * on a query carrying a `.hierarchy(R)` term.
1023
+ *
1024
+ * `.hierarchy(R)` does not narrow the matched set — it reorders it — so an
1025
+ * entity with no `R`-parent is a root (depth 0) and is yielded first. Depth is a
1026
+ * structural property of the *full* tree (an ancestor outside the matched set
1027
+ * still counts toward depth), computed by a memoised upward walk shared across
1028
+ * the whole batch, so a shared/deep chain costs O(nodes), not O(nodes²).
1029
+ *
1030
+ * The canonical order is produced without a comparator sort (which the observer
1031
+ * bench measured at 2–4× — `observer.ts`): (1) collect the matched ids via
1032
+ * `_forEachSparseMatch`; (2) `radixSortByIndex` → entity-index ascending;
1033
+ * (3) stable-bucket by depth — since the input is index-ascending and the bucket
1034
+ * append is stable, each depth band stays index-ascending. Tuned for the
1035
+ * motivating per-tick case (transform propagation): the radix scratch is reused
1036
+ * instance state (`_hierarchy_radix_*`), so a per-tick pass churns no histograms;
1037
+ * the working set (`matched` / `buckets` / `depthMemo` / `visiting`) is still
1038
+ * allocated per call, as it must stay call-local for re-entrancy.
1039
+ *
1040
+ * Exclusive-only — a multi relation throws `RELATION_MODE_MISMATCH` in `__DEV__`
1041
+ * (mirrors `cascadeOf` / `ancestorsOf`); a cycle is a loud `RELATION_CYCLE` in
1042
+ * `__DEV__` and a safe break in production. */
1043
+ _forEachHierarchyMatch(include: BitSet, exclude: BitSet | null, anyOf: BitSet | null, sparseInclude: readonly SparseComponentID[], sparseExclude: readonly SparseComponentID[], denseArchetypes: readonly Archetype[], relation: RelationDef, maxDepth: number, includeDisabled: boolean, cb: (entityId: EntityID) => void): void;
1044
+ /** Depth of entity index `idx` in the exclusive-relation tree backing `store`
1045
+ * (root = 0), for `_forEachHierarchyMatch`. Memoised across the batch (`memo`),
1046
+ * so a shared ancestor chain is walked once. Walks upward via the backing store's
1047
+ * target field 0 (mirroring `ancestorsOf`), stopping at: a root (no target), an
1048
+ * already-memoised node, or a dangling/dead parent (the child is then treated as
1049
+ * a root — never advance through a recycled slot, the ABA `ancestorsOf` guards).
1050
+ * `visiting` flags the nodes on the current upward path to catch a cycle —
1051
+ * `RELATION_CYCLE` in `__DEV__`, treated as a root in production — and is emptied
1052
+ * on the way back down so it can be reused for the next entity. */
1053
+ private _hierarchyDepthOf;
1054
+ addComponent(entityId: EntityID, def: ComponentDef<Record<string, never>>): void;
1055
+ addComponent<S extends ComponentSchema>(entityId: EntityID, def: ComponentDef<S>, values: FieldValues<S>): void;
1056
+ /** Add multiple components in one transition (resolves final archetype, then moves once).
1057
+ *
1058
+ * Final-mask resolve, not graph walk. The previous implementation called
1059
+ * `archResolveAdd` once per entry, which threaded through every
1060
+ * intermediate archetype on the path — and each unseen intermediate
1061
+ * triggered a fresh `extendColumnStore` even though no entity ever lived
1062
+ * there. Computing the union mask up front and resolving once via
1063
+ * `archGetOrCreateFromMask` collapses N-1 intermediate-archetype
1064
+ * creations into zero for the batched case (#211 follow-up). The lazy
1065
+ * single-mask path remains the same; this just avoids feeding it
1066
+ * archetypes the entity never visits.
1067
+ *
1068
+ * #659 — composite-add edge cache. The final-mask resolve, unlike the
1069
+ * single-add `edges[]` walk, re-pays a per-call `mask.hash()`, `archLookup`
1070
+ * (the Map-of-buckets + `equals` scan), and `getBatchTransitionMap` on
1071
+ * every call — a ~2× gap vs a cached edge walk that the decomposition probe
1072
+ * pinned on the two `Map.get`s, not the hash. So a repeated (source, added-
1073
+ * set) add now resolves through `currentArch`'s composite-add cache: one
1074
+ * `Map.get` on an exact packed key yields the target + transition map, and we
1075
+ * skip the union-mask build entirely. First call per key still resolves via
1076
+ * the final-mask path below (no intermediate planting) and plants the edge.
1077
+ * See docs/reports/bench/regressions/add-components-composite-edge.md. */
1078
+ addComponents(entityId: EntityID, entries: {
1079
+ def: ComponentDef;
1080
+ values?: Record<string, number>;
1081
+ }[]): void;
1082
+ /** Shared move+write tail of `addComponents` (#659): place the entity into
1083
+ * the already-resolved `targetArch` — a `moveEntityFrom` along the cached
1084
+ * `map` when it has a row, else a fresh append (the rowless empty-archetype
1085
+ * source ignores `map`) — then write every entry's fields. Both the
1086
+ * composite-edge-cache hit and the final-mask cold path funnel through here so
1087
+ * the placement logic lives once. */
1088
+ private _addComponentsInto;
1089
+ removeComponent(entityId: EntityID, def: ComponentDef): void;
1090
+ /** Remove multiple components in one transition (resolves final archetype, then moves once).
1091
+ *
1092
+ * Final-mask resolve, not graph walk. Same rationale as `addComponents`
1093
+ * above — the previous per-step path threaded `archResolveRemove`
1094
+ * once per def, which materialised every intermediate archetype on the
1095
+ * removal path. Computing the difference mask up front and resolving
1096
+ * once avoids planting N-1 intermediates the entity never lives in. */
1097
+ removeComponents(entityId: EntityID, defs: ComponentDef[]): void;
1098
+ hasComponent(entityId: EntityID, def: ComponentDef): boolean;
1099
+ /**
1100
+ * Bulk add a component to ALL entities in the given archetype.
1101
+ * Uses TypedArray.set() for O(columns) instead of O(N×columns).
1102
+ * The archetype must not already contain this component.
1103
+ */
1104
+ batchAddComponent(src: ArchetypeID, def: ComponentDef, values?: Record<string, number>): void;
1105
+ /**
1106
+ * Bulk remove a component from ALL entities in the given archetype.
1107
+ * Uses TypedArray.set() for O(columns) instead of O(N×columns).
1108
+ * The archetype must contain this component.
1109
+ */
1110
+ batchRemoveComponent(src: ArchetypeID, def: ComponentDef): void;
1111
+ getEntityArchetype(entityId: EntityID): Archetype;
1112
+ getEntityRow(entityId: EntityID): number;
1113
+ /**
1114
+ * Find all archetypes matching the given masks.
1115
+ * Uses the inverted componentIndex to start from the component with the
1116
+ * fewest archetypes, minimizing the number of superset checks.
1117
+ */
1118
+ getMatchingArchetypes(required: BitSet, excluded?: BitSet, anyOf?: BitSet): readonly Archetype[];
1119
+ /**
1120
+ * Register a live query. Returns a mutable Archetype[] that this Store will
1121
+ * push newly-created matching archetypes into, keeping the query always up-to-date.
1122
+ */
1123
+ registerQuery(include: BitSet, exclude?: BitSet, anyOf?: BitSet): Archetype[];
1124
+ updateQueryRef(result: Archetype[], query: Query<any>): void;
1125
+ get archetypeCount(): number;
1126
+ registerEvent<S extends EventSchema>(fields: readonly (keyof S & string)[]): EventDef<S>;
1127
+ emitEvent(def: EventDef, values: Record<string, number>): void;
1128
+ emitSignal(def: EventDef<EmptyEventSchema>): void;
1129
+ getEventReader<S extends EventSchema>(def: EventDef<S>): EventReader<S>;
1130
+ clearEvents(): void;
1131
+ /** `__DEV__`-only: total events currently buffered across the dirty channels.
1132
+ * `ECS.update` samples this either side of `dispatchSet` to assert an onSet
1133
+ * observer emitted nothing — its emissions would be wiped by the tick-tail
1134
+ * `clearEvents` and break the empty-channel-at-boundary invariant snapshot /
1135
+ * restore relies on (#586). Walks only the dirty list, never the hot emit path. */
1136
+ _devBufferedEventCount(): number;
1137
+ private readonly eventKeyMap;
1138
+ registerEventByKey<S extends EventSchema>(key: symbol, fields: readonly (keyof S & string)[]): EventDef<S>;
1139
+ getEventDefByKey(key: symbol): EventDef<any>;
1140
+ hasEventKey(key: symbol): boolean;
1141
+ private readonly resourceKeyMap;
1142
+ registerResource(key: symbol, value: unknown): void;
1143
+ getResource(key: symbol): unknown;
1144
+ setResource(key: symbol, value: unknown): void;
1145
+ removeResource(key: symbol): void;
1146
+ hasResource(key: symbol): boolean;
1147
+ }
1148
+ export {};
1149
+ //# sourceMappingURL=store.d.ts.map