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