@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
@@ -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
@@ -38,8 +60,14 @@ export interface StructuralObserverEvents {
38
60
  type SchemaOf<D extends ComponentDef> = D extends ComponentDef<infer S> ? S : ComponentSchema;
39
61
  /** One typed template entry: `values` is checked against the def's schema —
40
62
  * 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> = {
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
+ } : {
43
71
  readonly def: D;
44
72
  readonly values?: Partial<FieldValues<SchemaOf<D>>>;
45
73
  };
@@ -55,7 +83,7 @@ export type TemplateEntries<Defs extends readonly ComponentDef[]> = readonly [
55
83
  type TemplateFieldNames<Defs extends readonly ComponentDef[]> = Defs[number] extends ComponentDef<infer S> ? keyof S & string : never;
56
84
  /** Flat per-instance override map for `ECS.spawn`: any field of any
57
85
  * 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). */
86
+ * error (and still a `DEV` throw at runtime for untyped call sites). */
59
87
  export type TemplateOverrides<Defs extends readonly ComponentDef[]> = {
60
88
  readonly [K in TemplateFieldNames<Defs>]?: number;
61
89
  };
@@ -132,39 +160,26 @@ export interface StoreOptions {
132
160
  * called per tick. */
133
161
  deterministic?: boolean;
134
162
  }
135
- export declare class Store {
136
- private entityGenerations;
137
- private entityHighWater;
138
- private readonly entityFreeIndices;
139
- private entityAliveCount;
163
+ export declare class Store implements ObserverHost, QueryHost {
164
+ private readonly entityAllocator;
140
165
  private readonly componentMetas;
141
166
  private componentCount;
142
167
  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;
168
+ /** Debug names parallel to `sparseStores` — diagnostics only. */
169
+ private readonly sparseNames;
170
+ private readonly relationService;
171
+ private readonly events;
172
+ private readonly archGraph;
152
173
  private readonly registeredQueries;
153
174
  private emptyArchetypeId;
154
175
  private entityArchetype;
155
176
  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;
177
+ private readonly _deferred;
178
+ private readonly _snapshots;
164
179
  _tick: number;
165
180
  /** Per-world frame-trace sink (ADR-0030), installed via `ECS.setTrace`.
166
181
  * `null` unless a consumer attaches a recorder. Every call site is
167
- * `if (__DEV__) store._trace?.…`, so production builds dead-code-eliminate
182
+ * `if (DEV) store._trace?.…`, so production builds dead-code-eliminate
168
183
  * the seam and pay only this one nullable field. The sink observes; it never
169
184
  * folds into `stateHash` (a scheduling artifact, like `_changedTick` / the
170
185
  * observer state below). */
@@ -176,15 +191,14 @@ export declare class Store {
176
191
  private _toggleObserverCount;
177
192
  /** Reused effective-event scratch for the current flush round. */
178
193
  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;
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;
188
202
  private _collectDestroyEid;
189
203
  private readonly _collectDestroyRemoveBit;
190
204
  private _collectToggleEid;
@@ -210,7 +224,7 @@ export declare class Store {
210
224
  /** Monotonic counter bumped by every membership-changing path (immediate
211
225
  * `addComponent(s)`, `removeComponent(s)`, `destroyEntity`,
212
226
  * `batchAddComponent`, `batchRemoveComponent`, `flushStructural`,
213
- * `flushDestroyed`, and new-archetype installs in `archInstall`).
227
+ * `flushDestroyed`, and new-archetype installs in `ArchetypeGraph.install`).
214
228
  * Read by `Query._nonEmpty()` via `QueryResolver._getQueryDirtyEpoch`
215
229
  * — a query whose stored `_lastSeenEpoch` matches the current epoch
216
230
  * reuses its cached non-empty list (#327). Replaces the previous walk
@@ -221,17 +235,7 @@ export declare class Store {
221
235
  _queryDirtyEpoch: number;
222
236
  private readonly initialCapacity;
223
237
  private readonly _scratchTargetMask;
224
- private readonly _hierarchyRadixOut;
225
- private readonly _hierarchyRadixC0;
226
- private readonly _hierarchyRadixC1;
227
238
  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
239
  /** Installed on every SAB-backed Archetype so the Archetype can
236
240
  * request a SAB grow when an insertion would exceed its column
237
241
  * capacity. Doubles the offending archetype's row capacity (or jumps
@@ -282,7 +286,7 @@ export declare class Store {
282
286
  /** Guard the canonical-ordering determinism surface. Throws
283
287
  * `DETERMINISM_DISABLED` when determinism wasn't opted into, naming the
284
288
  * 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
289
+ * (not `DEV`-gated): the surface is cold (never per-tick) so one boolean
286
290
  * check is free, and a silent non-canonical digest is the failure mode we're
287
291
  * preventing. */
288
292
  private _requireDeterministic;
@@ -338,7 +342,7 @@ export declare class Store {
338
342
  *
339
343
  * Lockstep walk: SAB descriptors are written by `extendColumnStore` in
340
344
  * the order non-SAB archetypes are promoted, which is the same id-order
341
- * those archetypes occupy in `this.archetypes`. Iterating that array
345
+ * those archetypes occupy in `this.archGraph.archetypes`. Iterating that array
342
346
  * once, skipping non-SAB entries, and advancing an `archAddr` cursor
343
347
  * by the descriptor's `column_count` lets us write `row_count` without
344
348
  * the throwaway `Map<archId, length>` the previous version allocated
@@ -409,72 +413,42 @@ export declare class Store {
409
413
  * happened mid-tick), both of which are bugs the parity test would
410
414
  * surface. */
411
415
  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
- */
416
+ /** Find or create an archetype for the given component mask — see
417
+ * `ArchetypeGraph.getOrCreateFromMask`. */
420
418
  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
- */
419
+ /** Bulk variant of `archGetOrCreateFromMask` — one `extendColumnStore`
420
+ * call for the whole batch (Phase C prewarm, #213). See
421
+ * `ArchetypeGraph.createManyFromMasks`. */
437
422
  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
423
  /** Snapshot every existing archetype's SAB rows, call `extendColumnStore`
444
424
  * once with `newSpecs`, then refresh every pre-existing SAB-backed
445
425
  * Archetype's TypedArray views. The single `existing` snapshot is the
446
426
  * key win in the bulk variant — single-mask creation rebuilds it per
447
427
  * 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. */
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). */
460
439
  private archResolveAdd;
461
- /** Resolve "remove component_id from archetype_id" → target ArchetypeID. Caches the edge. */
440
+ /** Resolve "remove component_id from archetype_id" → target ArchetypeID (edge-cached). */
462
441
  private archResolveRemove;
463
- /** Cache a bidirectional add/remove edge between two archetypes. */
464
- private archCacheEdge;
465
442
  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
443
  /** 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). */
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). */
478
452
  private _allocEntity;
479
453
  /** Pre-check that `count` fresh entity slots can be allocated without
480
454
  * exhausting the entity-index space, so `spawnMany` commits all-or-nothing
@@ -490,9 +464,16 @@ export declare class Store {
490
464
  * column index; `TEMPLATE_OVERRIDE_AMBIGUOUS` for a name shared by more than
491
465
  * one component, which a flat override cannot target). */
492
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;
493
471
  /** Apply per-instance overrides to the freshly-spawned row. Each key is a
494
472
  * field name resolved through the template's override index. */
495
473
  private _applyOverrides;
474
+ /** Bulk variant of `_applyOverrides`: one `fill` per overridden column
475
+ * across the contiguous rows `[start, start + count)`. */
476
+ private _applyOverridesRange;
496
477
  /** Spawn one entity directly into the template's archetype (zero archetype
497
478
  * transitions). Writes the template defaults in a single append pass, then
498
479
  * applies any per-instance overrides. */
@@ -501,7 +482,7 @@ export declare class Store {
501
482
  * field writes are O(columns) — one `TypedArray.fill` per column via
502
483
  * `addEntitiesWithValues` — not O(count×columns). Returns the new ids in
503
484
  * spawn order. */
504
- spawnMany(p: Template, count: number): EntityID[];
485
+ spawnMany(p: Template, count: number, overrides?: Record<string, number | undefined>): EntityID[];
505
486
  /** Immediately destroy an entity, removing it from its archetype.
506
487
  *
507
488
  * With no `delete`/`clear` target-cleanup policy registered (the common
@@ -603,7 +584,7 @@ export declare class Store {
603
584
  private _onArchEnabledChange;
604
585
  /** Immediately disable an entity (idempotent). The entity must hold at least
605
586
  * one component — a component-less entity occupies no archetype row, so it
606
- * cannot be partitioned (a `__DEV__` error; prod no-op). */
587
+ * cannot be partitioned (a `DEV` error; prod no-op). */
607
588
  disableEntity(id: EntityID): void;
608
589
  /** Immediately enable an entity (idempotent). */
609
590
  enableEntity(id: EntityID): void;
@@ -665,17 +646,22 @@ export declare class Store {
665
646
  * the identity of what was destroyed, not a live handle to read. The
666
647
  * no-observer path is byte-for-byte unchanged (`collecting` gate). #531.
667
648
  *
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). */
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. */
673
654
  flushDestroyed(): void;
674
655
  private _drainDestroyed;
675
656
  get pendingDestroyCount(): number;
676
657
  addComponentDeferred(entityId: EntityID, def: ComponentDef<Record<string, never>>): void;
677
658
  addComponentDeferred<S extends ComponentSchema>(entityId: EntityID, def: ComponentDef<S>, values: FieldValues<S>): void;
678
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. */
679
665
  flushStructural(): void;
680
666
  /** Batch-apply all deferred component additions. */
681
667
  private _flushAdds;
@@ -714,7 +700,10 @@ export declare class Store {
714
700
  * absent at seed (the "delete on disable" semantics). Unordered here — the
715
701
  * registry radix-sorts. */
716
702
  _collectEntitiesWithComponent(cid: number): EntityID[];
717
- registerComponent<S extends Record<string, TypedArrayTag>>(schema: S): ComponentDef<S>;
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;
718
707
  /** Return the field index assigned to `(def, fieldName)` at component
719
708
  * registration. Indexes are insertion-order, zero-based, and stable for
720
709
  * the lifetime of the ECS. Used by systems that pass `(component_id,
@@ -724,7 +713,9 @@ export declare class Store {
724
713
  * allocates from a separate id space and never touches the archetype mask,
725
714
  * so it does **not** count against `STORE_DESCRIPTOR_COMPONENT_LIMIT`. See
726
715
  * ADR-0011 and `sparse_store.ts`. */
727
- registerSparseComponent<S extends Record<string, TypedArrayTag>>(schema: S): SparseComponentDef<S>;
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;
728
719
  /** Allocate the backing sparse store WITHOUT the #777 float guard, for
729
720
  * engine-internal backings whose `f64` holds an EXACT integer rather than a
730
721
  * user quantity: the exclusive-relation `{ target }` slot stores an `EntityID`
@@ -738,6 +729,7 @@ export declare class Store {
738
729
  addSparse(entityId: EntityID, def: SparseComponentDef, values?: Record<string, number>): void;
739
730
  /** Remove a sparse component from an entity. No-op if absent. */
740
731
  removeSparse(entityId: EntityID, def: SparseComponentDef): void;
732
+ /** Total, like `hasComponent` — `false` for a dead entity, never a throw. */
741
733
  hasSparse(entityId: EntityID, def: SparseComponentDef): boolean;
742
734
  getSparseField(entityId: EntityID, def: SparseComponentDef, field: string): number;
743
735
  setSparseField(entityId: EntityID, def: SparseComponentDef, field: string, value: number): void;
@@ -774,13 +766,6 @@ export declare class Store {
774
766
  * Store was constructed with `{ deterministic: true }`; paired with
775
767
  * `snapshotSparse`, which produces the canonical bytes restore consumes. */
776
768
  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
769
  /**
785
770
  * Capture the full live world to one self-contained byte buffer that
786
771
  * `restoreInto` can mount back onto a live, ticking world ("rewind a running
@@ -802,9 +787,6 @@ export declare class Store {
802
787
  * are scheduling artifacts, never folded into `stateHash`. Take the snapshot
803
788
  * at a tick boundary (between `update()`s). See the ADR. */
804
789
  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
790
  /**
809
791
  * Mount a `snapshot()` buffer onto this live world and leave it ready to keep
810
792
  * ticking. Fails closed on a malformed frame or a registration mismatch
@@ -818,15 +800,12 @@ export declare class Store {
818
800
  * entity-index capacity. **Opt-in (ADR-0020):** throws `DETERMINISM_DISABLED`
819
801
  * unless `{ deterministic: true }`. See `snapshot()` for the v1 scope. */
820
802
  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;
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;
830
809
  /** Rebuild each SAB-backed archetype's host-side `length` / `enabledCount` /
831
810
  * `_entityIds` after the dense backing was swapped in `restoreInto`. `length`
832
811
  * + the per-row entity-id back-reference come from a scan of the restored
@@ -834,167 +813,26 @@ export declare class Store {
834
813
  * from the captured host-state (the #577 partition boundary is positional only
835
814
  * — it has no per-entity byte source). */
836
815
  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
816
  registerRelation(opts?: RelationOptions): RelationDef;
853
- private relationOf;
854
817
  /** Number of registered relations. Visible to tests asserting the
855
818
  * no-transition invariant alongside `archetype_count`. */
856
819
  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
820
  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
821
  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
822
  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
823
  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`. */
824
+ sourcesOf(tgt: EntityID, def: RelationDef): EntityID[];
880
825
  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. */
826
+ pairsOf(def: RelationDef): readonly (readonly [EntityID, EntityID])[];
827
+ sourcesOfAny(tgt: EntityID): readonly (readonly [RelationDef, EntityID])[];
906
828
  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. */
829
+ /** Drive a `(*, T)` wildcard query (`Query.forEachRelatedTo`) see
830
+ * `RelationService.forEachRelationTargetMatch`. */
918
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;
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
832
  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
833
  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
834
  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
835
  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
836
  /** Second query-match path (#469 / ADR-0011): iterate entities matching a
999
837
  * dense mask **and** sparse-membership terms, invoking `cb` per entity.
1000
838
  * Yields `EntityID`s, not archetype spans — sparse members are scattered
@@ -1013,44 +851,9 @@ export declare class Store {
1013
851
  * Only reached via `Query.forEachEntity`; dense `forEach` never consults
1014
852
  * the sparse stores, so dense-only queries are unaffected (#469 AC). */
1015
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;
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. */
854
+ /** Fourth query-match path (#581): the matched set in hierarchy depth order
855
+ * (parents before children) see `RelationService.forEachHierarchyMatch`. */
1043
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;
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
857
  addComponent(entityId: EntityID, def: ComponentDef<Record<string, never>>): void;
1055
858
  addComponent<S extends ComponentSchema>(entityId: EntityID, def: ComponentDef<S>, values: FieldValues<S>): void;
1056
859
  /** Add multiple components in one transition (resolves final archetype, then moves once).
@@ -1066,7 +869,7 @@ export declare class Store {
1066
869
  * archetypes the entity never visits.
1067
870
  *
1068
871
  * #659 — composite-add edge cache. The final-mask resolve, unlike the
1069
- * single-add `edges[]` walk, re-pays a per-call `mask.hash()`, `archLookup`
872
+ * single-add `edges[]` walk, re-pays a per-call `mask.hash()`, `ArchetypeGraph.lookup`
1070
873
  * (the Map-of-buckets + `equals` scan), and `getBatchTransitionMap` on
1071
874
  * every call — a ~2× gap vs a cached edge walk that the decomposition probe
1072
875
  * pinned on the two `Map.get`s, not the hash. So a repeated (source, added-
@@ -1075,10 +878,7 @@ export declare class Store {
1075
878
  * skip the union-mask build entirely. First call per key still resolves via
1076
879
  * the final-mask path below (no intermediate planting) and plants the edge.
1077
880
  * 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;
881
+ addComponents<Defs extends readonly ComponentDef[]>(entityId: EntityID, entries: TemplateEntries<Defs>): void;
1082
882
  /** Shared move+write tail of `addComponents` (#659): place the entity into
1083
883
  * the already-resolved `targetArch` — a `moveEntityFrom` along the cached
1084
884
  * `map` when it has a row, else a fresh append (the rowless empty-archetype
@@ -1095,7 +895,10 @@ export declare class Store {
1095
895
  * removal path. Computing the difference mask up front and resolving
1096
896
  * once avoids planting N-1 intermediates the entity never lives in. */
1097
897
  removeComponents(entityId: EntityID, defs: ComponentDef[]): void;
1098
- hasComponent(entityId: EntityID, def: ComponentDef): boolean;
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;
1099
902
  /**
1100
903
  * Bulk add a component to ALL entities in the given archetype.
1101
904
  * Uses TypedArray.set() for O(columns) instead of O(N×columns).
@@ -1123,25 +926,23 @@ export declare class Store {
1123
926
  registerQuery(include: BitSet, exclude?: BitSet, anyOf?: BitSet): Archetype[];
1124
927
  updateQueryRef(result: Archetype[], query: Query<any>): void;
1125
928
  get archetypeCount(): number;
1126
- registerEvent<S extends EventSchema>(fields: readonly (keyof S & string)[]): EventDef<S>;
1127
- emitEvent(def: EventDef, values: Record<string, number>): void;
929
+ registerEvent<S extends EventShape<S>>(fields: readonly (keyof S & string)[]): EventDef<S>;
930
+ emitEvent(def: EventDef<any>, values: Record<string, number>): void;
1128
931
  emitSignal(def: EventDef<EmptyEventSchema>): void;
1129
- getEventReader<S extends EventSchema>(def: EventDef<S>): EventReader<S>;
932
+ getEventReader<S extends EventShape<S>>(def: EventDef<S>): EventReader<S>;
1130
933
  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. */
934
+ /** `DEV`-only mid-update emit detection see
935
+ * `EventRegistry.devBufferedEventCount`. */
1136
936
  _devBufferedEventCount(): number;
1137
- private readonly eventKeyMap;
1138
- registerEventByKey<S extends EventSchema>(key: symbol, fields: readonly (keyof S & string)[]): EventDef<S>;
937
+ registerEventByKey<S extends EventShape<S>>(key: symbol, fields: readonly (keyof S & string)[]): EventDef<S>;
1139
938
  getEventDefByKey(key: symbol): EventDef<any>;
1140
939
  hasEventKey(key: symbol): boolean;
1141
- private readonly resourceKeyMap;
940
+ private readonly resources;
1142
941
  registerResource(key: symbol, value: unknown): void;
1143
942
  getResource(key: symbol): unknown;
1144
943
  setResource(key: symbol, value: unknown): void;
944
+ /** Fails closed on a missing key; the present → absent → present
945
+ * lifecycle (#798) — see `ResourceRegistry.remove`. */
1145
946
  removeResource(key: symbol): void;
1146
947
  hasResource(key: symbol): boolean;
1147
948
  }