@oasys/oecs 0.4.0 → 0.5.1

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 (273) hide show
  1. package/CHANGELOG.md +636 -0
  2. package/README.md +47 -32
  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 +25 -13
  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 +201 -0
  19. package/dist/core/ecs/component.d.ts +100 -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 +580 -0
  32. package/dist/core/ecs/ecs.d.ts +340 -293
  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 +8 -2
  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 +284 -0
  58. package/dist/core/ecs/host_commands.d.ts +77 -45
  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 +44 -13
  65. package/dist/core/ecs/observer.d.ts.map +1 -1
  66. package/dist/core/ecs/query.d.cts +734 -0
  67. package/dist/core/ecs/query.d.ts +267 -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/ref.d.ts.map +1 -1
  72. package/dist/core/ecs/relation.d.cts +243 -0
  73. package/dist/core/ecs/relation.d.ts +26 -14
  74. package/dist/core/ecs/relation.d.ts.map +1 -1
  75. package/dist/core/ecs/relation_service.d.cts +235 -0
  76. package/dist/core/ecs/relation_service.d.ts +235 -0
  77. package/dist/core/ecs/relation_service.d.ts.map +1 -0
  78. package/dist/core/ecs/resource.d.cts +53 -0
  79. package/dist/core/ecs/resource.d.ts +25 -5
  80. package/dist/core/ecs/resource.d.ts.map +1 -1
  81. package/dist/core/ecs/resource_registry.d.cts +20 -0
  82. package/dist/core/ecs/resource_registry.d.ts +20 -0
  83. package/dist/core/ecs/resource_registry.d.ts.map +1 -0
  84. package/dist/core/ecs/resume.d.cts +85 -0
  85. package/dist/core/ecs/resume.d.ts +6 -6
  86. package/dist/core/ecs/resume.d.ts.map +1 -1
  87. package/dist/core/ecs/run_condition.d.cts +81 -0
  88. package/dist/core/ecs/run_condition.d.ts +16 -10
  89. package/dist/core/ecs/run_condition.d.ts.map +1 -1
  90. package/dist/core/ecs/schedule.d.cts +142 -0
  91. package/dist/core/ecs/schedule.d.ts +15 -6
  92. package/dist/core/ecs/schedule.d.ts.map +1 -1
  93. package/dist/core/ecs/snapshot_service.d.cts +94 -0
  94. package/dist/core/ecs/snapshot_service.d.ts +94 -0
  95. package/dist/core/ecs/snapshot_service.d.ts.map +1 -0
  96. package/dist/core/ecs/sparse_store.d.cts +110 -0
  97. package/dist/core/ecs/sparse_store.d.ts +5 -2
  98. package/dist/core/ecs/sparse_store.d.ts.map +1 -1
  99. package/dist/core/ecs/store.d.cts +937 -0
  100. package/dist/core/ecs/store.d.ts +144 -356
  101. package/dist/core/ecs/store.d.ts.map +1 -1
  102. package/dist/core/ecs/store_layout_listener.d.cts +23 -0
  103. package/dist/core/ecs/system.d.cts +250 -0
  104. package/dist/core/ecs/system.d.ts +138 -22
  105. package/dist/core/ecs/system.d.ts.map +1 -1
  106. package/dist/{utils/arrays.d.ts → core/ecs/utils/arrays.d.cts} +1 -0
  107. package/dist/core/ecs/utils/error.d.cts +61 -0
  108. package/dist/core/ecs/utils/error.d.ts +12 -2
  109. package/dist/core/ecs/utils/error.d.ts.map +1 -1
  110. package/dist/core/store/action_ring.d.cts +136 -0
  111. package/dist/core/store/allocator.d.cts +238 -0
  112. package/dist/core/store/allocator.d.ts.map +1 -1
  113. package/dist/core/store/buffer_backed_column.d.cts +69 -0
  114. package/dist/core/store/buffer_backed_column.d.ts +2 -2
  115. package/dist/core/store/column_store.d.cts +273 -0
  116. package/dist/core/store/column_store.d.ts +18 -10
  117. package/dist/core/store/column_store.d.ts.map +1 -1
  118. package/dist/core/store/command_dispatch.d.cts +52 -0
  119. package/dist/core/store/command_ring.d.cts +107 -0
  120. package/dist/core/store/descriptor.d.cts +80 -0
  121. package/dist/core/store/descriptor.d.ts +1 -1
  122. package/dist/core/store/descriptor.d.ts.map +1 -1
  123. package/dist/core/store/entity_index.d.cts +108 -0
  124. package/dist/core/store/event_ring.d.cts +95 -0
  125. package/dist/core/store/extend.d.cts +39 -0
  126. package/dist/core/store/extend.d.ts +3 -73
  127. package/dist/core/store/extend.d.ts.map +1 -1
  128. package/dist/core/store/grow.d.cts +34 -0
  129. package/dist/core/store/grow.d.ts +4 -9
  130. package/dist/core/store/grow.d.ts.map +1 -1
  131. package/dist/core/store/header.d.cts +64 -0
  132. package/dist/core/store/header.d.ts +1 -1
  133. package/dist/core/store/header.d.ts.map +1 -1
  134. package/dist/core/store/index.d.cts +17 -0
  135. package/dist/core/store/index.d.ts +16 -15
  136. package/dist/core/store/index.d.ts.map +1 -1
  137. package/dist/core/store/layout_ops.d.cts +152 -0
  138. package/dist/core/store/layout_ops.d.ts +152 -0
  139. package/dist/core/store/layout_ops.d.ts.map +1 -0
  140. package/dist/core/store/region_table.d.cts +74 -0
  141. package/dist/core/store/region_table.d.ts +1 -1
  142. package/dist/core/store/region_table.d.ts.map +1 -1
  143. package/dist/core/store/snapshot.d.cts +43 -0
  144. package/dist/core/store/snapshot.d.ts +2 -2
  145. package/dist/core/store/state_hash.d.cts +38 -0
  146. package/dist/core/store/state_hash.d.ts +1 -1
  147. package/dist/core/store/store_regions.d.cts +38 -0
  148. package/dist/core/store/store_regions.d.ts +1 -1
  149. package/dist/core/store/vendored_abi/abi.d.ts +43 -0
  150. package/dist/core/store/{__generated__ → vendored_abi}/abi.d.ts.map +1 -1
  151. package/dist/dev_flag.d.cts +2 -0
  152. package/dist/dev_flag.d.ts +2 -0
  153. package/dist/dev_flag.d.ts.map +1 -0
  154. package/dist/extensions/editor/editor.d.cts +172 -0
  155. package/dist/extensions/editor/editor.d.ts +65 -42
  156. package/dist/extensions/editor/editor.d.ts.map +1 -1
  157. package/dist/extensions/editor/field_handle.d.cts +40 -0
  158. package/dist/extensions/editor/field_handle.d.ts +10 -5
  159. package/dist/extensions/editor/field_handle.d.ts.map +1 -1
  160. package/dist/extensions/editor/index.cjs +1 -1
  161. package/dist/extensions/editor/index.d.cts +21 -0
  162. package/dist/extensions/editor/index.d.ts +2 -2
  163. package/dist/extensions/editor/index.js +130 -100
  164. package/dist/extensions/reactive/ecs_sync.d.cts +214 -0
  165. package/dist/extensions/reactive/ecs_sync.d.ts +26 -22
  166. package/dist/extensions/reactive/ecs_sync.d.ts.map +1 -1
  167. package/dist/extensions/reactive/index.cjs +1 -1
  168. package/dist/extensions/reactive/index.d.cts +23 -0
  169. package/dist/extensions/reactive/index.d.ts +1 -1
  170. package/dist/extensions/reactive/index.js +106 -116
  171. package/dist/extensions/solid/index.cjs +1 -1
  172. package/dist/extensions/solid/index.d.cts +6 -0
  173. package/dist/extensions/solid/index.d.ts +2 -2
  174. package/dist/extensions/solid/index.js +8 -4
  175. package/dist/extensions/solid/kernel_solid.d.cts +45 -0
  176. package/dist/extensions/solid/kernel_solid.d.ts +6 -3
  177. package/dist/extensions/solid/kernel_solid.d.ts.map +1 -1
  178. package/dist/host_commands-BI8pEmjH.js +2326 -0
  179. package/dist/host_commands-CxhpzMx9.cjs +1 -0
  180. package/dist/index.cjs +1 -2
  181. package/dist/index.d.cts +66 -0
  182. package/dist/index.d.ts +50 -1
  183. package/dist/index.d.ts.map +1 -1
  184. package/dist/index.js +4489 -5898
  185. package/dist/internal.cjs +2 -0
  186. package/dist/internal.d.cts +20 -0
  187. package/dist/internal.d.ts +20 -0
  188. package/dist/internal.d.ts.map +1 -0
  189. package/dist/internal.js +141 -0
  190. package/dist/primitives.cjs +1 -1
  191. package/dist/primitives.d.cts +18 -0
  192. package/dist/primitives.d.ts +6 -6
  193. package/dist/primitives.js +24 -23
  194. package/dist/reactive/array.d.ts +24 -0
  195. package/dist/reactive/array.d.ts.map +1 -0
  196. package/dist/reactive/index.cjs +1 -0
  197. package/dist/reactive/index.d.cts +11 -0
  198. package/dist/reactive/index.d.ts +11 -0
  199. package/dist/reactive/index.d.ts.map +1 -0
  200. package/dist/reactive/index.js +18 -0
  201. package/dist/reactive/interop.d.cts +19 -0
  202. package/dist/{core/reactive → reactive}/interop.d.ts +1 -1
  203. package/dist/reactive/interop.d.ts.map +1 -0
  204. package/dist/reactive/kernel.d.cts +73 -0
  205. package/dist/{core/reactive → reactive}/kernel.d.ts +5 -3
  206. package/dist/reactive/kernel.d.ts.map +1 -0
  207. package/dist/reactive/map.d.ts +16 -0
  208. package/dist/reactive/map.d.ts.map +1 -0
  209. package/dist/reactive/shallow.d.cts +11 -0
  210. package/dist/reactive/shallow.d.ts +11 -0
  211. package/dist/reactive/shallow.d.ts.map +1 -0
  212. package/dist/reactive/struct.d.cts +10 -0
  213. package/dist/{core/reactive → reactive}/struct.d.ts +1 -1
  214. package/dist/reactive/struct.d.ts.map +1 -0
  215. package/dist/shallow-DQ8aG51_.cjs +1 -0
  216. package/dist/shallow-DfnAm9vG.js +150 -0
  217. package/dist/shared-BU1Cd40h.js +103 -0
  218. package/dist/shared-BymrGTyR.cjs +1 -0
  219. package/dist/shared.cjs +1 -1
  220. package/dist/shared.d.cts +22 -0
  221. package/dist/shared.d.ts +2 -2
  222. package/dist/shared.js +1 -1
  223. package/dist/topological_sort-BDvEyb9W.cjs +1 -0
  224. package/dist/topological_sort-DK6EjpWa.js +135 -0
  225. package/dist/type_primitives/assertions.d.cts +32 -0
  226. package/dist/type_primitives/assertions.d.ts +13 -1
  227. package/dist/type_primitives/assertions.d.ts.map +1 -1
  228. package/dist/type_primitives/binary_heap/binary_heap.d.cts +37 -0
  229. package/dist/type_primitives/bitset/bitset.d.cts +54 -0
  230. package/dist/type_primitives/brand.d.cts +23 -0
  231. package/dist/type_primitives/error.d.cts +16 -0
  232. package/dist/type_primitives/error.d.ts +4 -3
  233. package/dist/type_primitives/error.d.ts.map +1 -1
  234. package/dist/type_primitives/index.d.cts +13 -0
  235. package/dist/type_primitives/index.d.ts +9 -9
  236. package/dist/type_primitives/sparse_map/sparse_map.d.cts +30 -0
  237. package/dist/type_primitives/sparse_set/sparse_set.d.cts +28 -0
  238. package/dist/type_primitives/topological_sort/topological_sort.d.cts +29 -0
  239. package/dist/type_primitives/typed_arrays/typed_arrays.d.cts +127 -0
  240. package/dist/typed_arrays-BcXcnzB4.js +258 -0
  241. package/dist/typed_arrays-D3NmwtCG.cjs +1 -0
  242. package/dist/utils/error.d.cts +6 -0
  243. package/dist/version.d.cts +10 -0
  244. package/dist/version.d.ts +10 -0
  245. package/dist/version.d.ts.map +1 -0
  246. package/package.json +102 -19
  247. package/dist/array-CxbPyiHO.cjs +0 -1
  248. package/dist/array-uFR7Q8fU.js +0 -132
  249. package/dist/core/reactive/array.d.ts.map +0 -1
  250. package/dist/core/reactive/index.cjs +0 -1
  251. package/dist/core/reactive/index.d.ts +0 -10
  252. package/dist/core/reactive/index.d.ts.map +0 -1
  253. package/dist/core/reactive/index.js +0 -17
  254. package/dist/core/reactive/interop.d.ts.map +0 -1
  255. package/dist/core/reactive/kernel.d.ts.map +0 -1
  256. package/dist/core/reactive/map.d.ts.map +0 -1
  257. package/dist/core/reactive/struct.d.ts.map +0 -1
  258. package/dist/log/console_sink.d.ts +0 -4
  259. package/dist/log/console_sink.d.ts.map +0 -1
  260. package/dist/log/index.d.ts +0 -3
  261. package/dist/log/index.d.ts.map +0 -1
  262. package/dist/log/logger.d.ts +0 -27
  263. package/dist/log/logger.d.ts.map +0 -1
  264. package/dist/shared-BXSZnxx4.cjs +0 -1
  265. package/dist/shared-C678TAPY.js +0 -99
  266. package/dist/topological_sort-DlRpSrxu.js +0 -391
  267. package/dist/topological_sort-WAT-VHb-.cjs +0 -1
  268. package/dist/utils/arrays.d.ts.map +0 -1
  269. package/dist/utils/constants.d.ts.map +0 -1
  270. /package/dist/{utils/constants.d.ts → core/ecs/utils/constants.d.cts} +0 -0
  271. /package/dist/core/store/{__generated__/abi.d.ts → vendored_abi/abi.d.cts} +0 -0
  272. /package/dist/{core/reactive/array.d.ts → reactive/array.d.cts} +0 -0
  273. /package/dist/{core/reactive/map.d.ts → reactive/map.d.cts} +0 -0
@@ -1,14 +1,36 @@
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';
1
+ import { EntityID } from './entity.js';
2
+ import { FrameTraceSink } from './frame_trace.js';
3
+ import { ComponentDef, ComponentHandle, ComponentSchema, FieldValues } from './component.js';
4
+ import { SparseComponentDef, SparseComponentID } from './sparse_store.js';
5
+ import { RelationDef, RelationOptions } from './relation.js';
6
+ import { EmptyEventSchema, EventDef, EventReader, EventShape } from './event.js';
7
+ import { BitSet, TypedArrayTag } from '../../type_primitives/index.js';
8
+ import { Archetype, ArchetypeID } from './archetype.js';
9
+ import { Query, QueryHost } from './query.js';
10
+ import { ObserverHost } from './observer.js';
11
+ import { InPlaceBufferAllocator, ColumnStoreRegionHandle, StoreRegionSpec, ColumnStore } from '../store/index.js';
12
+ import { ECSMemoryCapContext } from './ecs_memory.js';
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
+ }
12
34
  /**
13
35
  * Effective `(component, entity)` structural events for one fixed-point round,
14
36
  * collected during `_flushAdds` / `_flushRemoves` and handed to the observer
@@ -34,28 +56,21 @@ export interface StructuralObserverEvents {
34
56
  enaEid: number[];
35
57
  enaLen: number;
36
58
  }
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>>>;
59
+ /** Runtime shape of one template / `addComponents` entry: a def plus optional
60
+ * field values (omitted fields zero-fill). The public authoring surface is
61
+ * callable-bundle varargs, schema-checked per item by `StrictBundles`
62
+ * (component.ts); the `ECS` facade normalizes those bundles into this erased
63
+ * array before calling the store, so the store stays schema-agnostic. */
64
+ type TemplateEntryData = {
65
+ readonly def: ComponentDef;
66
+ readonly values?: Readonly<Record<string, number>>;
45
67
  };
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
68
  /** Union of every field name owned by a component in `Defs` (distributes
54
69
  * over the def list). */
55
70
  type TemplateFieldNames<Defs extends readonly ComponentDef[]> = Defs[number] extends ComponentDef<infer S> ? keyof S & string : never;
56
71
  /** Flat per-instance override map for `ECS.spawn`: any field of any
57
72
  * 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). */
73
+ * error (and still a `DEV` throw at runtime for untyped call sites). */
59
74
  export type TemplateOverrides<Defs extends readonly ComponentDef[]> = {
60
75
  readonly [K in TemplateFieldNames<Defs>]?: number;
61
76
  };
@@ -132,39 +147,26 @@ export interface StoreOptions {
132
147
  * called per tick. */
133
148
  deterministic?: boolean;
134
149
  }
135
- export declare class Store {
136
- private entityGenerations;
137
- private entityHighWater;
138
- private readonly entityFreeIndices;
139
- private entityAliveCount;
150
+ export declare class Store implements ObserverHost, QueryHost {
151
+ private readonly entityAllocator;
140
152
  private readonly componentMetas;
141
153
  private componentCount;
142
154
  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;
155
+ /** Debug names parallel to `sparseStores` — diagnostics only. */
156
+ private readonly sparseNames;
157
+ private readonly relationService;
158
+ private readonly events;
159
+ private readonly archGraph;
152
160
  private readonly registeredQueries;
153
161
  private emptyArchetypeId;
154
162
  private entityArchetype;
155
163
  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
+ private readonly _deferred;
165
+ private readonly _snapshots;
164
166
  _tick: number;
165
167
  /** Per-world frame-trace sink (ADR-0030), installed via `ECS.setTrace`.
166
168
  * `null` unless a consumer attaches a recorder. Every call site is
167
- * `if (__DEV__) store._trace?.…`, so production builds dead-code-eliminate
169
+ * `if (DEV) store._trace?.…`, so production builds dead-code-eliminate
168
170
  * the seam and pay only this one nullable field. The sink observes; it never
169
171
  * folds into `stateHash` (a scheduling artifact, like `_changedTick` / the
170
172
  * observer state below). */
@@ -176,15 +178,14 @@ export declare class Store {
176
178
  private _toggleObserverCount;
177
179
  /** Reused effective-event scratch for the current flush round. */
178
180
  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;
181
+ /** Installed via `setStructuralObserverHook` — dispatches a round's collected
182
+ * events to the observer registry (ordering + callbacks), which may enqueue
183
+ * further structural ops. */
184
+ private _structuralObserverHook;
185
+ /** Install the structural-observer dispatch hook (called once by `ECS`
186
+ * during construction) the named seam replacing direct writes to the
187
+ * previously-public field (M1). */
188
+ setStructuralObserverHook(fn: (ev: StructuralObserverEvents) => void): void;
188
189
  private _collectDestroyEid;
189
190
  private readonly _collectDestroyRemoveBit;
190
191
  private _collectToggleEid;
@@ -210,7 +211,7 @@ export declare class Store {
210
211
  /** Monotonic counter bumped by every membership-changing path (immediate
211
212
  * `addComponent(s)`, `removeComponent(s)`, `destroyEntity`,
212
213
  * `batchAddComponent`, `batchRemoveComponent`, `flushStructural`,
213
- * `flushDestroyed`, and new-archetype installs in `archInstall`).
214
+ * `flushDestroyed`, and new-archetype installs in `ArchetypeGraph.install`).
214
215
  * Read by `Query._nonEmpty()` via `QueryResolver._getQueryDirtyEpoch`
215
216
  * — a query whose stored `_lastSeenEpoch` matches the current epoch
216
217
  * reuses its cached non-empty list (#327). Replaces the previous walk
@@ -221,17 +222,7 @@ export declare class Store {
221
222
  _queryDirtyEpoch: number;
222
223
  private readonly initialCapacity;
223
224
  private readonly _scratchTargetMask;
224
- private readonly _hierarchyRadixOut;
225
- private readonly _hierarchyRadixC0;
226
- private readonly _hierarchyRadixC1;
227
225
  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
226
  /** Installed on every SAB-backed Archetype so the Archetype can
236
227
  * request a SAB grow when an insertion would exceed its column
237
228
  * capacity. Doubles the offending archetype's row capacity (or jumps
@@ -282,7 +273,7 @@ export declare class Store {
282
273
  /** Guard the canonical-ordering determinism surface. Throws
283
274
  * `DETERMINISM_DISABLED` when determinism wasn't opted into, naming the
284
275
  * 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
276
+ * (not `DEV`-gated): the surface is cold (never per-tick) so one boolean
286
277
  * check is free, and a silent non-canonical digest is the failure mode we're
287
278
  * preventing. */
288
279
  private _requireDeterministic;
@@ -338,7 +329,7 @@ export declare class Store {
338
329
  *
339
330
  * Lockstep walk: SAB descriptors are written by `extendColumnStore` in
340
331
  * the order non-SAB archetypes are promoted, which is the same id-order
341
- * those archetypes occupy in `this.archetypes`. Iterating that array
332
+ * those archetypes occupy in `this.archGraph.archetypes`. Iterating that array
342
333
  * once, skipping non-SAB entries, and advancing an `archAddr` cursor
343
334
  * by the descriptor's `column_count` lets us write `row_count` without
344
335
  * the throwaway `Map<archId, length>` the previous version allocated
@@ -409,72 +400,42 @@ export declare class Store {
409
400
  * happened mid-tick), both of which are bugs the parity test would
410
401
  * surface. */
411
402
  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
- */
403
+ /** Find or create an archetype for the given component mask — see
404
+ * `ArchetypeGraph.getOrCreateFromMask`. */
420
405
  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
- */
406
+ /** Bulk variant of `archGetOrCreateFromMask` — one `extendColumnStore`
407
+ * call for the whole batch (Phase C prewarm, #213). See
408
+ * `ArchetypeGraph.createManyFromMasks`. */
437
409
  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
410
  /** Snapshot every existing archetype's SAB rows, call `extendColumnStore`
444
411
  * once with `newSpecs`, then refresh every pre-existing SAB-backed
445
412
  * Archetype's TypedArray views. The single `existing` snapshot is the
446
413
  * key win in the bulk variant — single-mask creation rebuilds it per
447
414
  * 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
- * correctnessand 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. */
415
+ private _archExtendStoreWithNewSpecs;
416
+ /** Materialise the `Archetype` object for a freshly-minted graph node
417
+ * binds the graph's topology to THIS store's column backing and grow
418
+ * handler (`ArchetypeGraphHost.materialize`). Store-owned so the graph
419
+ * never touches `_columnStore`. */
420
+ private _materializeArchetype;
421
+ /** Push a newly-installed archetype into every registered query whose masks
422
+ * it satisfies (`ArchetypeGraphHost.fanIntoQueries`; the query registry
423
+ * stays on Store). No epoch bump see the note in `ArchetypeGraph.install`. */
424
+ private _fanIntoQueries;
425
+ /** Resolve "add component_id to archetype_id" → target ArchetypeID (edge-cached). */
460
426
  private archResolveAdd;
461
- /** Resolve "remove component_id from archetype_id" → target ArchetypeID. Caches the edge. */
427
+ /** Resolve "remove component_id from archetype_id" → target ArchetypeID (edge-cached). */
462
428
  private archResolveRemove;
463
- /** Cache a bidirectional add/remove edge between two archetypes. */
464
- private archCacheEdge;
465
429
  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
430
  /** 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). */
431
+ * the template spawn paths. Returns the packed `EntityID`; the slot index
432
+ * is left in `entityAllocator.lastIndex`. Skips the empty-archetype
433
+ * membership write `createEntity` performs (the caller installs the real
434
+ * archetype + row). This *commits* the slot (bumps counts, stamps the
435
+ * generation so `isAlive` is already true), so the caller MUST have
436
+ * reserved the column capacity for the row first
437
+ * (`Archetype.ensureRowCapacity`) — otherwise a cap throw from the
438
+ * subsequent append leaves the slot phantom-alive (#775). */
478
439
  private _allocEntity;
479
440
  /** Pre-check that `count` fresh entity slots can be allocated without
480
441
  * exhausting the entity-index space, so `spawnMany` commits all-or-nothing
@@ -489,10 +450,17 @@ export declare class Store {
489
450
  * `_flatColumns` order, and build the override index (field name → flat
490
451
  * column index; `TEMPLATE_OVERRIDE_AMBIGUOUS` for a name shared by more than
491
452
  * one component, which a flat override cannot target). */
492
- resolveTemplate<Defs extends readonly ComponentDef[]>(entries: TemplateEntries<Defs>): Template<Defs>;
453
+ resolveTemplate(entries: readonly TemplateEntryData[]): Template;
454
+ /** Resolve an override key to its flat column index, with the DEV guards
455
+ * for unknown and ambiguous field names; `-1` means skip (the production
456
+ * fallback where DEV would have thrown). */
457
+ private _resolveOverrideColumn;
493
458
  /** Apply per-instance overrides to the freshly-spawned row. Each key is a
494
459
  * field name resolved through the template's override index. */
495
460
  private _applyOverrides;
461
+ /** Bulk variant of `_applyOverrides`: one `fill` per overridden column
462
+ * across the contiguous rows `[start, start + count)`. */
463
+ private _applyOverridesRange;
496
464
  /** Spawn one entity directly into the template's archetype (zero archetype
497
465
  * transitions). Writes the template defaults in a single append pass, then
498
466
  * applies any per-instance overrides. */
@@ -501,7 +469,7 @@ export declare class Store {
501
469
  * field writes are O(columns) — one `TypedArray.fill` per column via
502
470
  * `addEntitiesWithValues` — not O(count×columns). Returns the new ids in
503
471
  * spawn order. */
504
- spawnMany(p: Template, count: number): EntityID[];
472
+ spawnMany(p: Template, count: number, overrides?: Record<string, number | undefined>): EntityID[];
505
473
  /** Immediately destroy an entity, removing it from its archetype.
506
474
  *
507
475
  * With no `delete`/`clear` target-cleanup policy registered (the common
@@ -603,7 +571,7 @@ export declare class Store {
603
571
  private _onArchEnabledChange;
604
572
  /** Immediately disable an entity (idempotent). The entity must hold at least
605
573
  * one component — a component-less entity occupies no archetype row, so it
606
- * cannot be partitioned (a `__DEV__` error; prod no-op). */
574
+ * cannot be partitioned (a `DEV` error; prod no-op). */
607
575
  disableEntity(id: EntityID): void;
608
576
  /** Immediately enable an entity (idempotent). */
609
577
  enableEntity(id: EntityID): void;
@@ -665,17 +633,22 @@ export declare class Store {
665
633
  * the identity of what was destroyed, not a live handle to read. The
666
634
  * no-observer path is byte-for-byte unchanged (`collecting` gate). #531.
667
635
  *
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). */
636
+ * Re-entrancy: while the observed fixed point owns the flush, the loop
637
+ * drains destroys itself via `_drainDestroyed`, so a re-entrant
638
+ * `ctx.flush()` from a callback no-ops (the guard lives in
639
+ * `DeferredCommandBuffer.flushDestroyed`) — otherwise it would collect
640
+ * into the shared `_obsEvents` scratch mid-dispatch and corrupt it. */
673
641
  flushDestroyed(): void;
674
642
  private _drainDestroyed;
675
643
  get pendingDestroyCount(): number;
676
644
  addComponentDeferred(entityId: EntityID, def: ComponentDef<Record<string, never>>): void;
677
645
  addComponentDeferred<S extends ComponentSchema>(entityId: EntityID, def: ComponentDef<S>, values: FieldValues<S>): void;
678
646
  removeComponentDeferred(entityId: EntityID, def: ComponentDef): void;
647
+ /** Phase-boundary structural flush. The drain policy — no-observer fast
648
+ * path, observed fixed point (adds/removes → destroys → toggles),
649
+ * convergence guard, re-entrancy — lives in `DeferredCommandBuffer`
650
+ * (H1 step 4); the batch appliers it drives are the `_flush*` /
651
+ * `_drainDestroyed` methods below. */
679
652
  flushStructural(): void;
680
653
  /** Batch-apply all deferred component additions. */
681
654
  private _flushAdds;
@@ -714,7 +687,10 @@ export declare class Store {
714
687
  * absent at seed (the "delete on disable" semantics). Unordered here — the
715
688
  * registry radix-sorts. */
716
689
  _collectEntitiesWithComponent(cid: number): EntityID[];
717
- registerComponent<S extends Record<string, TypedArrayTag>>(schema: S): ComponentDef<S>;
690
+ registerComponent<S extends Record<string, TypedArrayTag>>(schema: S, name?: string): ComponentDef<S>;
691
+ /** `'Pos' (component 5)` when the component was registered with a debug
692
+ * name, else `component 5` — the label diagnostics interpolate. */
693
+ componentLabel(cid: number): string;
718
694
  /** Return the field index assigned to `(def, fieldName)` at component
719
695
  * registration. Indexes are insertion-order, zero-based, and stable for
720
696
  * the lifetime of the ECS. Used by systems that pass `(component_id,
@@ -724,7 +700,9 @@ export declare class Store {
724
700
  * allocates from a separate id space and never touches the archetype mask,
725
701
  * so it does **not** count against `STORE_DESCRIPTOR_COMPONENT_LIMIT`. See
726
702
  * ADR-0011 and `sparse_store.ts`. */
727
- registerSparseComponent<S extends Record<string, TypedArrayTag>>(schema: S): SparseComponentDef<S>;
703
+ registerSparseComponent<S extends Record<string, TypedArrayTag>>(schema: S, name?: string): SparseComponentDef<S>;
704
+ /** Sparse sibling of `componentLabel` — sparse ids are a separate id space. */
705
+ sparseLabel(sid: number): string;
728
706
  /** Allocate the backing sparse store WITHOUT the #777 float guard, for
729
707
  * engine-internal backings whose `f64` holds an EXACT integer rather than a
730
708
  * user quantity: the exclusive-relation `{ target }` slot stores an `EntityID`
@@ -738,6 +716,7 @@ export declare class Store {
738
716
  addSparse(entityId: EntityID, def: SparseComponentDef, values?: Record<string, number>): void;
739
717
  /** Remove a sparse component from an entity. No-op if absent. */
740
718
  removeSparse(entityId: EntityID, def: SparseComponentDef): void;
719
+ /** Total, like `hasComponent` — `false` for a dead entity, never a throw. */
741
720
  hasSparse(entityId: EntityID, def: SparseComponentDef): boolean;
742
721
  getSparseField(entityId: EntityID, def: SparseComponentDef, field: string): number;
743
722
  setSparseField(entityId: EntityID, def: SparseComponentDef, field: string, value: number): void;
@@ -774,13 +753,6 @@ export declare class Store {
774
753
  * Store was constructed with `{ deterministic: true }`; paired with
775
754
  * `snapshotSparse`, which produces the canonical bytes restore consumes. */
776
755
  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
756
  /**
785
757
  * Capture the full live world to one self-contained byte buffer that
786
758
  * `restoreInto` can mount back onto a live, ticking world ("rewind a running
@@ -802,9 +774,6 @@ export declare class Store {
802
774
  * are scheduling artifacts, never folded into `stateHash`. Take the snapshot
803
775
  * at a tick boundary (between `update()`s). See the ADR. */
804
776
  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
777
  /**
809
778
  * Mount a `snapshot()` buffer onto this live world and leave it ready to keep
810
779
  * ticking. Fails closed on a malformed frame or a registration mismatch
@@ -818,15 +787,12 @@ export declare class Store {
818
787
  * entity-index capacity. **Opt-in (ADR-0020):** throws `DETERMINISM_DISABLED`
819
788
  * unless `{ deterministic: true }`. See `snapshot()` for the v1 scope. */
820
789
  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;
790
+ /** Adopt a restored dense store (`SnapshotService.restoreInto`'s mount
791
+ * step): swap the live backing, refresh every buffer-backed archetype's
792
+ * views, recover the allocator high-water from the restored region, and
793
+ * republish (the grow tail). Store-owned because it assigns
794
+ * `_columnStore` the service never writes Store fields. */
795
+ private _mountRestoredDense;
830
796
  /** Rebuild each SAB-backed archetype's host-side `length` / `enabledCount` /
831
797
  * `_entityIds` after the dense backing was swapped in `restoreInto`. `length`
832
798
  * + the per-row entity-id back-reference come from a scan of the restored
@@ -834,167 +800,26 @@ export declare class Store {
834
800
  * from the captured host-state (the #577 partition boundary is positional only
835
801
  * — it has no per-entity byte source). */
836
802
  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
803
  registerRelation(opts?: RelationOptions): RelationDef;
853
- private relationOf;
854
804
  /** Number of registered relations. Visible to tests asserting the
855
805
  * no-transition invariant alongside `archetype_count`. */
856
806
  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
807
  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
808
  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
809
  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
810
  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`. */
811
+ sourcesOf(tgt: EntityID, def: RelationDef): EntityID[];
880
812
  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. */
813
+ pairsOf(def: RelationDef): readonly (readonly [EntityID, EntityID])[];
814
+ sourcesOfAny(tgt: EntityID): readonly (readonly [RelationDef, EntityID])[];
906
815
  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. */
816
+ /** Drive a `(*, T)` wildcard query (`Query.forEachRelatedTo`) see
817
+ * `RelationService.forEachRelationTargetMatch`. */
918
818
  _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
819
  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
820
  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
821
  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
822
  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
823
  /** Second query-match path (#469 / ADR-0011): iterate entities matching a
999
824
  * dense mask **and** sparse-membership terms, invoking `cb` per entity.
1000
825
  * Yields `EntityID`s, not archetype spans — sparse members are scattered
@@ -1013,44 +838,9 @@ export declare class Store {
1013
838
  * Only reached via `Query.forEachEntity`; dense `forEach` never consults
1014
839
  * the sparse stores, so dense-only queries are unaffected (#469 AC). */
1015
840
  _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. */
841
+ /** Fourth query-match path (#581): the matched set in hierarchy depth order
842
+ * (parents before children) see `RelationService.forEachHierarchyMatch`. */
1043
843
  _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
844
  addComponent(entityId: EntityID, def: ComponentDef<Record<string, never>>): void;
1055
845
  addComponent<S extends ComponentSchema>(entityId: EntityID, def: ComponentDef<S>, values: FieldValues<S>): void;
1056
846
  /** Add multiple components in one transition (resolves final archetype, then moves once).
@@ -1066,7 +856,7 @@ export declare class Store {
1066
856
  * archetypes the entity never visits.
1067
857
  *
1068
858
  * #659 — composite-add edge cache. The final-mask resolve, unlike the
1069
- * single-add `edges[]` walk, re-pays a per-call `mask.hash()`, `archLookup`
859
+ * single-add `edges[]` walk, re-pays a per-call `mask.hash()`, `ArchetypeGraph.lookup`
1070
860
  * (the Map-of-buckets + `equals` scan), and `getBatchTransitionMap` on
1071
861
  * every call — a ~2× gap vs a cached edge walk that the decomposition probe
1072
862
  * pinned on the two `Map.get`s, not the hash. So a repeated (source, added-
@@ -1075,10 +865,7 @@ export declare class Store {
1075
865
  * skip the union-mask build entirely. First call per key still resolves via
1076
866
  * the final-mask path below (no intermediate planting) and plants the edge.
1077
867
  * 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;
868
+ addComponents(entityId: EntityID, entries: readonly TemplateEntryData[]): void;
1082
869
  /** Shared move+write tail of `addComponents` (#659): place the entity into
1083
870
  * the already-resolved `targetArch` — a `moveEntityFrom` along the cached
1084
871
  * `map` when it has a row, else a fresh append (the rowless empty-archetype
@@ -1095,7 +882,10 @@ export declare class Store {
1095
882
  * removal path. Computing the difference mask up front and resolving
1096
883
  * once avoids planting N-1 intermediates the entity never lives in. */
1097
884
  removeComponents(entityId: EntityID, defs: ComponentDef[]): void;
1098
- hasComponent(entityId: EntityID, def: ComponentDef): boolean;
885
+ /** Total (POLISH_AUDIT #9): a dead/stale `entityId` returns `false` rather
886
+ * than throwing — a "has" probe is exactly what callers reach for to avoid
887
+ * touching dead entities, so it must be safe to ask. */
888
+ hasComponent(entityId: EntityID, def: ComponentHandle): boolean;
1099
889
  /**
1100
890
  * Bulk add a component to ALL entities in the given archetype.
1101
891
  * Uses TypedArray.set() for O(columns) instead of O(N×columns).
@@ -1123,25 +913,23 @@ export declare class Store {
1123
913
  registerQuery(include: BitSet, exclude?: BitSet, anyOf?: BitSet): Archetype[];
1124
914
  updateQueryRef(result: Archetype[], query: Query<any>): void;
1125
915
  get archetypeCount(): number;
1126
- registerEvent<S extends EventSchema>(fields: readonly (keyof S & string)[]): EventDef<S>;
1127
- emitEvent(def: EventDef, values: Record<string, number>): void;
916
+ registerEvent<S extends EventShape<S>>(fields: readonly (keyof S & string)[]): EventDef<S>;
917
+ emitEvent(def: EventDef<any>, values: Record<string, number>): void;
1128
918
  emitSignal(def: EventDef<EmptyEventSchema>): void;
1129
- getEventReader<S extends EventSchema>(def: EventDef<S>): EventReader<S>;
919
+ getEventReader<S extends EventShape<S>>(def: EventDef<S>): EventReader<S>;
1130
920
  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. */
921
+ /** `DEV`-only mid-update emit detection see
922
+ * `EventRegistry.devBufferedEventCount`. */
1136
923
  _devBufferedEventCount(): number;
1137
- private readonly eventKeyMap;
1138
- registerEventByKey<S extends EventSchema>(key: symbol, fields: readonly (keyof S & string)[]): EventDef<S>;
924
+ registerEventByKey<S extends EventShape<S>>(key: symbol, fields: readonly (keyof S & string)[]): EventDef<S>;
1139
925
  getEventDefByKey(key: symbol): EventDef<any>;
1140
926
  hasEventKey(key: symbol): boolean;
1141
- private readonly resourceKeyMap;
927
+ private readonly resources;
1142
928
  registerResource(key: symbol, value: unknown): void;
1143
929
  getResource(key: symbol): unknown;
1144
930
  setResource(key: symbol, value: unknown): void;
931
+ /** Fails closed on a missing key; the present → absent → present
932
+ * lifecycle (#798) — see `ResourceRegistry.remove`. */
1145
933
  removeResource(key: symbol): void;
1146
934
  hasResource(key: symbol): boolean;
1147
935
  }