@oasys/oecs 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/CHANGELOG.md +553 -0
  2. package/README.md +42 -31
  3. package/dist/core/ecs/access_check.d.cts +77 -0
  4. package/dist/core/ecs/access_check.d.ts +8 -8
  5. package/dist/core/ecs/access_check.d.ts.map +1 -1
  6. package/dist/core/ecs/archetype.d.cts +486 -0
  7. package/dist/core/ecs/archetype.d.ts +23 -11
  8. package/dist/core/ecs/archetype.d.ts.map +1 -1
  9. package/dist/core/ecs/archetype_graph.d.cts +96 -0
  10. package/dist/core/ecs/archetype_graph.d.ts +96 -0
  11. package/dist/core/ecs/archetype_graph.d.ts.map +1 -0
  12. package/dist/core/ecs/builtin_relations.d.cts +43 -0
  13. package/dist/core/ecs/builtin_relations.d.ts +8 -8
  14. package/dist/core/ecs/builtin_relations.d.ts.map +1 -1
  15. package/dist/core/ecs/command_log.d.cts +116 -0
  16. package/dist/core/ecs/command_log.d.ts +13 -10
  17. package/dist/core/ecs/command_log.d.ts.map +1 -1
  18. package/dist/core/ecs/component.d.cts +176 -0
  19. package/dist/core/ecs/component.d.ts +75 -5
  20. package/dist/core/ecs/component.d.ts.map +1 -1
  21. package/dist/core/ecs/compute_backend.d.cts +37 -0
  22. package/dist/core/ecs/compute_backend.d.ts +2 -2
  23. package/dist/core/ecs/debug_names.d.cts +7 -0
  24. package/dist/core/ecs/debug_names.d.ts +7 -0
  25. package/dist/core/ecs/debug_names.d.ts.map +1 -0
  26. package/dist/core/ecs/deferred_commands.d.cts +55 -0
  27. package/dist/core/ecs/deferred_commands.d.ts +55 -0
  28. package/dist/core/ecs/deferred_commands.d.ts.map +1 -0
  29. package/dist/core/ecs/dispatch_trace.d.cts +93 -0
  30. package/dist/core/ecs/dispatch_trace.d.ts +3 -3
  31. package/dist/core/ecs/ecs.d.cts +562 -0
  32. package/dist/core/ecs/ecs.d.ts +314 -285
  33. package/dist/core/ecs/ecs.d.ts.map +1 -1
  34. package/dist/core/ecs/ecs_memory.d.cts +179 -0
  35. package/dist/core/ecs/ecs_memory.d.ts +1 -1
  36. package/dist/core/ecs/entity.d.cts +34 -0
  37. package/dist/core/ecs/entity.d.ts +7 -1
  38. package/dist/core/ecs/entity.d.ts.map +1 -1
  39. package/dist/core/ecs/entity_allocator.d.cts +59 -0
  40. package/dist/core/ecs/entity_allocator.d.ts +59 -0
  41. package/dist/core/ecs/entity_allocator.d.ts.map +1 -0
  42. package/dist/core/ecs/event.d.cts +79 -0
  43. package/dist/core/ecs/event.d.ts +36 -11
  44. package/dist/core/ecs/event.d.ts.map +1 -1
  45. package/dist/core/ecs/event_registry.d.cts +22 -0
  46. package/dist/core/ecs/event_registry.d.ts +22 -0
  47. package/dist/core/ecs/event_registry.d.ts.map +1 -0
  48. package/dist/core/ecs/facades.d.cts +158 -0
  49. package/dist/core/ecs/facades.d.ts +158 -0
  50. package/dist/core/ecs/facades.d.ts.map +1 -0
  51. package/dist/core/ecs/frame_stepper.d.cts +58 -0
  52. package/dist/core/ecs/frame_stepper.d.ts +58 -0
  53. package/dist/core/ecs/frame_stepper.d.ts.map +1 -0
  54. package/dist/core/ecs/frame_trace.d.cts +133 -0
  55. package/dist/core/ecs/frame_trace.d.ts +5 -5
  56. package/dist/core/ecs/frame_trace.d.ts.map +1 -1
  57. package/dist/core/ecs/host_commands.d.cts +274 -0
  58. package/dist/core/ecs/host_commands.d.ts +53 -31
  59. package/dist/core/ecs/host_commands.d.ts.map +1 -1
  60. package/dist/core/ecs/index.d.cts +46 -0
  61. package/dist/core/ecs/index.d.ts +45 -40
  62. package/dist/core/ecs/index.d.ts.map +1 -1
  63. package/dist/core/ecs/observer.d.cts +173 -0
  64. package/dist/core/ecs/observer.d.ts +43 -12
  65. package/dist/core/ecs/observer.d.ts.map +1 -1
  66. package/dist/core/ecs/query.d.cts +718 -0
  67. package/dist/core/ecs/query.d.ts +251 -90
  68. package/dist/core/ecs/query.d.ts.map +1 -1
  69. package/dist/core/ecs/ref.d.cts +31 -0
  70. package/dist/core/ecs/ref.d.ts +3 -3
  71. package/dist/core/ecs/relation.d.cts +243 -0
  72. package/dist/core/ecs/relation.d.ts +26 -14
  73. package/dist/core/ecs/relation.d.ts.map +1 -1
  74. package/dist/core/ecs/relation_service.d.cts +235 -0
  75. package/dist/core/ecs/relation_service.d.ts +235 -0
  76. package/dist/core/ecs/relation_service.d.ts.map +1 -0
  77. package/dist/core/ecs/resource.d.cts +53 -0
  78. package/dist/core/ecs/resource.d.ts +25 -5
  79. package/dist/core/ecs/resource.d.ts.map +1 -1
  80. package/dist/core/ecs/resource_registry.d.cts +20 -0
  81. package/dist/core/ecs/resource_registry.d.ts +20 -0
  82. package/dist/core/ecs/resource_registry.d.ts.map +1 -0
  83. package/dist/core/ecs/resume.d.cts +85 -0
  84. package/dist/core/ecs/resume.d.ts +6 -6
  85. package/dist/core/ecs/resume.d.ts.map +1 -1
  86. package/dist/core/ecs/run_condition.d.cts +81 -0
  87. package/dist/core/ecs/run_condition.d.ts +15 -9
  88. package/dist/core/ecs/run_condition.d.ts.map +1 -1
  89. package/dist/core/ecs/schedule.d.cts +142 -0
  90. package/dist/core/ecs/schedule.d.ts +15 -6
  91. package/dist/core/ecs/schedule.d.ts.map +1 -1
  92. package/dist/core/ecs/snapshot_service.d.cts +94 -0
  93. package/dist/core/ecs/snapshot_service.d.ts +94 -0
  94. package/dist/core/ecs/snapshot_service.d.ts.map +1 -0
  95. package/dist/core/ecs/sparse_store.d.cts +110 -0
  96. package/dist/core/ecs/sparse_store.d.ts +5 -2
  97. package/dist/core/ecs/sparse_store.d.ts.map +1 -1
  98. package/dist/core/ecs/store.d.cts +950 -0
  99. package/dist/core/ecs/store.d.ts +143 -342
  100. package/dist/core/ecs/store.d.ts.map +1 -1
  101. package/dist/core/ecs/store_layout_listener.d.cts +23 -0
  102. package/dist/core/ecs/system.d.cts +250 -0
  103. package/dist/core/ecs/system.d.ts +138 -22
  104. package/dist/core/ecs/system.d.ts.map +1 -1
  105. package/dist/{utils/arrays.d.ts → core/ecs/utils/arrays.d.cts} +1 -0
  106. package/dist/core/ecs/utils/error.d.cts +61 -0
  107. package/dist/core/ecs/utils/error.d.ts +12 -2
  108. package/dist/core/ecs/utils/error.d.ts.map +1 -1
  109. package/dist/core/store/action_ring.d.cts +136 -0
  110. package/dist/core/store/allocator.d.cts +238 -0
  111. package/dist/core/store/allocator.d.ts.map +1 -1
  112. package/dist/core/store/buffer_backed_column.d.cts +69 -0
  113. package/dist/core/store/buffer_backed_column.d.ts +2 -2
  114. package/dist/core/store/column_store.d.cts +273 -0
  115. package/dist/core/store/column_store.d.ts +18 -10
  116. package/dist/core/store/column_store.d.ts.map +1 -1
  117. package/dist/core/store/command_dispatch.d.cts +52 -0
  118. package/dist/core/store/command_ring.d.cts +107 -0
  119. package/dist/core/store/descriptor.d.cts +80 -0
  120. package/dist/core/store/descriptor.d.ts +1 -1
  121. package/dist/core/store/descriptor.d.ts.map +1 -1
  122. package/dist/core/store/entity_index.d.cts +108 -0
  123. package/dist/core/store/event_ring.d.cts +95 -0
  124. package/dist/core/store/extend.d.cts +39 -0
  125. package/dist/core/store/extend.d.ts +3 -73
  126. package/dist/core/store/extend.d.ts.map +1 -1
  127. package/dist/core/store/grow.d.cts +34 -0
  128. package/dist/core/store/grow.d.ts +4 -9
  129. package/dist/core/store/grow.d.ts.map +1 -1
  130. package/dist/core/store/header.d.cts +64 -0
  131. package/dist/core/store/header.d.ts +1 -1
  132. package/dist/core/store/header.d.ts.map +1 -1
  133. package/dist/core/store/index.d.cts +17 -0
  134. package/dist/core/store/index.d.ts +16 -15
  135. package/dist/core/store/index.d.ts.map +1 -1
  136. package/dist/core/store/layout_ops.d.cts +152 -0
  137. package/dist/core/store/layout_ops.d.ts +152 -0
  138. package/dist/core/store/layout_ops.d.ts.map +1 -0
  139. package/dist/core/store/region_table.d.cts +74 -0
  140. package/dist/core/store/region_table.d.ts +1 -1
  141. package/dist/core/store/region_table.d.ts.map +1 -1
  142. package/dist/core/store/snapshot.d.cts +43 -0
  143. package/dist/core/store/snapshot.d.ts +2 -2
  144. package/dist/core/store/state_hash.d.cts +38 -0
  145. package/dist/core/store/state_hash.d.ts +1 -1
  146. package/dist/core/store/store_regions.d.cts +38 -0
  147. package/dist/core/store/store_regions.d.ts +1 -1
  148. package/dist/core/store/vendored_abi/abi.d.ts +43 -0
  149. package/dist/core/store/{__generated__ → vendored_abi}/abi.d.ts.map +1 -1
  150. package/dist/dev_flag.d.cts +17 -0
  151. package/dist/dev_flag.d.ts +17 -0
  152. package/dist/dev_flag.d.ts.map +1 -0
  153. package/dist/extensions/editor/editor.d.cts +170 -0
  154. package/dist/extensions/editor/editor.d.ts +43 -22
  155. package/dist/extensions/editor/editor.d.ts.map +1 -1
  156. package/dist/extensions/editor/field_handle.d.cts +40 -0
  157. package/dist/extensions/editor/field_handle.d.ts +8 -3
  158. package/dist/extensions/editor/field_handle.d.ts.map +1 -1
  159. package/dist/extensions/editor/index.cjs +1 -1
  160. package/dist/extensions/editor/index.d.cts +21 -0
  161. package/dist/extensions/editor/index.d.ts +2 -2
  162. package/dist/extensions/editor/index.js +116 -88
  163. package/dist/extensions/reactive/ecs_sync.d.cts +214 -0
  164. package/dist/extensions/reactive/ecs_sync.d.ts +26 -22
  165. package/dist/extensions/reactive/ecs_sync.d.ts.map +1 -1
  166. package/dist/extensions/reactive/index.cjs +1 -1
  167. package/dist/extensions/reactive/index.d.cts +23 -0
  168. package/dist/extensions/reactive/index.d.ts +1 -1
  169. package/dist/extensions/reactive/index.js +106 -116
  170. package/dist/extensions/solid/index.cjs +1 -1
  171. package/dist/extensions/solid/index.d.cts +6 -0
  172. package/dist/extensions/solid/index.d.ts +2 -2
  173. package/dist/extensions/solid/index.js +8 -4
  174. package/dist/extensions/solid/kernel_solid.d.cts +45 -0
  175. package/dist/extensions/solid/kernel_solid.d.ts +6 -3
  176. package/dist/extensions/solid/kernel_solid.d.ts.map +1 -1
  177. package/dist/host_commands-BF8QMi3c.cjs +1 -0
  178. package/dist/host_commands-i4cAeyL5.js +2318 -0
  179. package/dist/index.cjs +1 -2
  180. package/dist/index.d.cts +66 -0
  181. package/dist/index.d.ts +50 -1
  182. package/dist/index.d.ts.map +1 -1
  183. package/dist/index.js +4469 -5933
  184. package/dist/internal.cjs +2 -0
  185. package/dist/internal.d.cts +20 -0
  186. package/dist/internal.d.ts +20 -0
  187. package/dist/internal.d.ts.map +1 -0
  188. package/dist/internal.js +141 -0
  189. package/dist/primitives.cjs +1 -1
  190. package/dist/primitives.d.cts +18 -0
  191. package/dist/primitives.d.ts +6 -6
  192. package/dist/primitives.js +24 -23
  193. package/dist/reactive/array.d.ts +24 -0
  194. package/dist/reactive/array.d.ts.map +1 -0
  195. package/dist/reactive/index.cjs +1 -0
  196. package/dist/reactive/index.d.cts +11 -0
  197. package/dist/reactive/index.d.ts +11 -0
  198. package/dist/reactive/index.d.ts.map +1 -0
  199. package/dist/reactive/index.js +18 -0
  200. package/dist/reactive/interop.d.cts +19 -0
  201. package/dist/{core/reactive → reactive}/interop.d.ts +1 -1
  202. package/dist/reactive/interop.d.ts.map +1 -0
  203. package/dist/reactive/kernel.d.cts +73 -0
  204. package/dist/{core/reactive → reactive}/kernel.d.ts +5 -3
  205. package/dist/reactive/kernel.d.ts.map +1 -0
  206. package/dist/reactive/map.d.ts +16 -0
  207. package/dist/reactive/map.d.ts.map +1 -0
  208. package/dist/reactive/shallow.d.cts +11 -0
  209. package/dist/reactive/shallow.d.ts +11 -0
  210. package/dist/reactive/shallow.d.ts.map +1 -0
  211. package/dist/reactive/struct.d.cts +10 -0
  212. package/dist/{core/reactive → reactive}/struct.d.ts +1 -1
  213. package/dist/reactive/struct.d.ts.map +1 -0
  214. package/dist/shallow-DQ8aG51_.cjs +1 -0
  215. package/dist/shallow-DfnAm9vG.js +150 -0
  216. package/dist/shared-BU1Cd40h.js +103 -0
  217. package/dist/shared-BymrGTyR.cjs +1 -0
  218. package/dist/shared.cjs +1 -1
  219. package/dist/shared.d.cts +22 -0
  220. package/dist/shared.d.ts +2 -2
  221. package/dist/shared.js +1 -1
  222. package/dist/topological_sort-BDvEyb9W.cjs +1 -0
  223. package/dist/topological_sort-DK6EjpWa.js +135 -0
  224. package/dist/type_primitives/assertions.d.cts +32 -0
  225. package/dist/type_primitives/assertions.d.ts +13 -1
  226. package/dist/type_primitives/assertions.d.ts.map +1 -1
  227. package/dist/type_primitives/binary_heap/binary_heap.d.cts +37 -0
  228. package/dist/type_primitives/bitset/bitset.d.cts +54 -0
  229. package/dist/type_primitives/brand.d.cts +23 -0
  230. package/dist/type_primitives/error.d.cts +16 -0
  231. package/dist/type_primitives/error.d.ts +4 -3
  232. package/dist/type_primitives/error.d.ts.map +1 -1
  233. package/dist/type_primitives/index.d.cts +13 -0
  234. package/dist/type_primitives/index.d.ts +9 -9
  235. package/dist/type_primitives/sparse_map/sparse_map.d.cts +30 -0
  236. package/dist/type_primitives/sparse_set/sparse_set.d.cts +28 -0
  237. package/dist/type_primitives/topological_sort/topological_sort.d.cts +29 -0
  238. package/dist/type_primitives/typed_arrays/typed_arrays.d.cts +127 -0
  239. package/dist/typed_arrays-BcXcnzB4.js +258 -0
  240. package/dist/typed_arrays-D3NmwtCG.cjs +1 -0
  241. package/dist/utils/error.d.cts +6 -0
  242. package/dist/version.d.cts +10 -0
  243. package/dist/version.d.ts +10 -0
  244. package/dist/version.d.ts.map +1 -0
  245. package/package.json +102 -19
  246. package/dist/array-CxbPyiHO.cjs +0 -1
  247. package/dist/array-uFR7Q8fU.js +0 -132
  248. package/dist/core/reactive/array.d.ts.map +0 -1
  249. package/dist/core/reactive/index.cjs +0 -1
  250. package/dist/core/reactive/index.d.ts +0 -10
  251. package/dist/core/reactive/index.d.ts.map +0 -1
  252. package/dist/core/reactive/index.js +0 -17
  253. package/dist/core/reactive/interop.d.ts.map +0 -1
  254. package/dist/core/reactive/kernel.d.ts.map +0 -1
  255. package/dist/core/reactive/map.d.ts.map +0 -1
  256. package/dist/core/reactive/struct.d.ts.map +0 -1
  257. package/dist/log/console_sink.d.ts +0 -4
  258. package/dist/log/console_sink.d.ts.map +0 -1
  259. package/dist/log/index.d.ts +0 -3
  260. package/dist/log/index.d.ts.map +0 -1
  261. package/dist/log/logger.d.ts +0 -27
  262. package/dist/log/logger.d.ts.map +0 -1
  263. package/dist/shared-BXSZnxx4.cjs +0 -1
  264. package/dist/shared-C678TAPY.js +0 -99
  265. package/dist/topological_sort-DlRpSrxu.js +0 -391
  266. package/dist/topological_sort-WAT-VHb-.cjs +0 -1
  267. package/dist/utils/arrays.d.ts.map +0 -1
  268. package/dist/utils/constants.d.ts.map +0 -1
  269. /package/dist/{utils/constants.d.ts → core/ecs/utils/constants.d.cts} +0 -0
  270. /package/dist/core/store/{__generated__/abi.d.ts → vendored_abi/abi.d.cts} +0 -0
  271. /package/dist/{core/reactive/array.d.ts → reactive/array.d.cts} +0 -0
  272. /package/dist/{core/reactive/map.d.ts → reactive/map.d.cts} +0 -0
@@ -0,0 +1,243 @@
1
+ import { Brand } from '../../type_primitives/index.cjs';
2
+ import { EntityID } from './entity.cjs';
3
+ import { SparseComponentDef, SparseComponentStore } from './sparse_store.cjs';
4
+ /** Relation handle id. A separate id space from `ComponentID` and
5
+ * `SparseComponentID` — it indexes the `RelationService` registry. The numeric
6
+ * value is the registration order, stable for the lifetime of the ECS. */
7
+ export type RelationID = Brand<number, "relation_id">;
8
+ declare const __relationBrand: unique symbol;
9
+ /** A relation's registration-time cardinality: `exclusive` = one target per
10
+ * source (re-add replaces), `multi` = a target set per source. */
11
+ export type RelationCardinality = "exclusive" | "multi";
12
+ declare const __relationCardinality: unique symbol;
13
+ export type RelationDef<C extends RelationCardinality = RelationCardinality> = RelationID & {
14
+ readonly [__relationBrand]: "relation";
15
+ readonly [__relationCardinality]?: [C];
16
+ };
17
+ /** Access sentinel for the `(*, T)` wildcard query iteration
18
+ * (`Query.forEachRelatedTo`, #579). A `(*, T)` term reads **every** registered
19
+ * relation's reverse index to find sources of `T`, so it can't name a specific
20
+ * relation in `relationReads` the way `withRelation(R)` (`(R, *)`) can. A
21
+ * system that iterates a `(*, T)` wildcard lists `ANY_RELATION` in `relationReads`
22
+ * instead; `accessCheck.checkRelationReadAny` honors it. The numeric value is a
23
+ * reserved sentinel far past any real registration-order relation id (relations are
24
+ * minted from 0 upward), so it can never collide with a registered relation. */
25
+ export declare const ANY_RELATION: RelationDef;
26
+ /** Cleanup policy applied to a relation's **sources** when one of its
27
+ * **targets** is destroyed (#473). Chosen per-relation at registration, run at
28
+ * destroy-flush (and the immediate-destroy path) off the reverse index:
29
+ *
30
+ * - **`delete`** — cascade: destroy every source of the dead target too,
31
+ * recursively for chains/trees (the canonical ChildOf case).
32
+ * - **`clear`** — remove the relation from every source; the sources survive.
33
+ * - **`orphan`** — leave the link intact but dangling. Reads stay safe (the
34
+ * reverse index is `EntityID`-keyed, so the dead handle never aliases a
35
+ * recycled slot); `targetOf` returns a dead handle until the source
36
+ * re-targets or is removed. This is the pre-#473 behaviour and the default.
37
+ */
38
+ export type OnDeleteTarget = "delete" | "clear" | "orphan";
39
+ /** Default on-target-delete policy: leave the link dangling (pre-#473
40
+ * behaviour, zero change for callers that don't opt in). */
41
+ export declare const DEFAULT_ON_DELETE_TARGET: OnDeleteTarget;
42
+ /** Registration options. `exclusive` (one target per source) is the default;
43
+ * pass `{ multi: true }` for a multi-target relation. The two are mutually
44
+ * exclusive — the union makes `{ exclusive: true, multi: true }` a compile
45
+ * error (it also throws at runtime, for JS callers). `onDeleteTarget` selects
46
+ * the cleanup policy applied to sources when a target is destroyed (default
47
+ * `orphan`). */
48
+ export type RelationOptions = {
49
+ readonly exclusive?: true;
50
+ readonly multi?: false;
51
+ readonly onDeleteTarget?: OnDeleteTarget;
52
+ } | {
53
+ readonly multi: true;
54
+ readonly exclusive?: false;
55
+ readonly onDeleteTarget?: OnDeleteTarget;
56
+ };
57
+ /** Field name carrying the target `EntityID` on an exclusive relation's backing
58
+ * sparse component. Field 0 of a single-field schema. */
59
+ export declare const RELATION_TARGET_FIELD = "target";
60
+ /** Receives one canonical `(source index, sorted targets)` set during a fold
61
+ * over a multi relation's forward sets. Targets are ascending by id; empty sets
62
+ * are never yielded (see `forEachCanonicalTargetSet`). */
63
+ export type CanonicalTargetSetFn = (sourceIndex: number, targets: readonly EntityID[]) => void;
64
+ /** Receives one canonical `(source, target)` pair during a fold over a
65
+ * relation's forward links. Sources ascend by entity index, each source's
66
+ * targets ascend by id — the #470 determinism order. */
67
+ export type CanonicalPairFn = (source: EntityID, target: EntityID) => void;
68
+ /** Maps a source entity **index** to its full `EntityID` (generation from the
69
+ * live slot). Supplied by `Store`, which owns entity generations. */
70
+ export type MakeSourceID = (index: number) => EntityID;
71
+ /** One relation's derived side state. **Abstract over cardinality (#498):** the
72
+ * base owns the cardinality-agnostic **reverse index** (target → sources, keyed
73
+ * by full `EntityID` so a recycled slot can't alias a dead target's sources) and
74
+ * a handle on the backing `SparseComponentStore`; the forward representation and
75
+ * its sparse interaction are owned by the concrete `ExclusiveRelationStore` /
76
+ * `MultiRelationStore` (built by `makeRelationStore`).
77
+ *
78
+ * The registry level (`Store` + `RelationService`) owns what is genuinely its
79
+ * own — entity liveness, registration, and destroy orchestration — and drives
80
+ * a relation through the virtual `link` /
81
+ * `unlink` / `purgeSource` / `forEachCanonicalPair` / … without ever
82
+ * branching on cardinality. Each cardinality keeps forward link + reverse index
83
+ * + sparse membership in lockstep inside one method on one class, so the
84
+ * lockstep can't be broken by missing a scattered `if (rs.exclusive)` site. */
85
+ export declare abstract class RelationStore {
86
+ /** `true` → one target per source (target in the sparse field); `false` →
87
+ * a set of targets per source. The cardinality discriminant survives as a
88
+ * field because it is part of the snapshot header and the traversal/`targetOf`
89
+ * mode guards — but the forward *mechanics* are virtual, not branched on it. */
90
+ readonly exclusive: boolean;
91
+ /** Backing sparse component def: `{ target: f64 }` when exclusive, a tag when
92
+ * multi. Carries membership (and the target, when exclusive). */
93
+ readonly sparse: SparseComponentDef;
94
+ /** Cleanup policy applied to this relation's sources when a target is
95
+ * destroyed (#473). `orphan` (the default) is a no-op — the link dangles
96
+ * safely. `Store` reads this at destroy-flush off `sourcesOf`. */
97
+ readonly onDeleteTarget: OnDeleteTarget;
98
+ /** The backing sparse store instance. Owned for registration/liveness by
99
+ * `Store`, but the relation drives the forward/membership rows on it directly
100
+ * — that is the cardinality interaction this class now encapsulates (#498). */
101
+ protected readonly _store: SparseComponentStore;
102
+ /** target `EntityID` → set of source `EntityID`s. Keyed by the full id, not
103
+ * the index, so a recycled target slot can't alias a dead target's sources.
104
+ * An entry is dropped the moment its set empties (`unlinkReverse`). */
105
+ private readonly _reverse;
106
+ constructor(exclusive: boolean, sparse: SparseComponentDef, store: SparseComponentStore, onDeleteTarget: OnDeleteTarget);
107
+ /** Record that `src` points at `tgt` in the reverse index. */
108
+ protected linkReverse(tgt: EntityID, src: EntityID): void;
109
+ /** Drop the `src → tgt` edge from the reverse index, removing the target's
110
+ * entry entirely once its last source is gone (so a dead-target key doesn't
111
+ * linger with an empty set). */
112
+ protected unlinkReverse(tgt: EntityID, src: EntityID): void;
113
+ /** Drop every reverse-index entry whose **target** is no longer alive,
114
+ * returning the count dropped (#491). The `orphan` policy intentionally
115
+ * leaves a destroyed target's reverse entry intact (the link dangles
116
+ * safely), so a long-lived source that orphan-points at a stream of
117
+ * short-lived targets and never re-targets/dies accumulates dead-target keys
118
+ * without bound. This is the reclaim primitive behind `Store.compactRelations`
119
+ * — a cold path the host calls at scene/snapshot boundaries. It is
120
+ * cardinality-free (reverse-index only), so it lives on the base unchanged.
121
+ *
122
+ * Pure index reclaim: the **forward** links (the exclusive sparse target
123
+ * field, the multi forward set) are left untouched, so `targetOf` /
124
+ * `targetsOf` still return the dangling dead handle exactly as `orphan`
125
+ * promises. The reverse index is derived, so dropping a dead-target entry
126
+ * changes nothing observable except `sourcesOf` on that dead handle (which
127
+ * goes from the dangling sources to `[]` — meaningless either way, the target
128
+ * is gone) and is faithfully rebuilt by snapshot/restore from the surviving
129
+ * forward links. Only the **target** role can leak here: a destroyed *source*
130
+ * is already unlinked from every reverse set by `purgeSource`, so a live
131
+ * target's set never holds a dead source. `isAlive` is supplied by `Store`,
132
+ * which owns entity liveness. Deleting during `Map` key iteration is
133
+ * well-defined — already-visited and not-yet-reached keys are unaffected. */
134
+ pruneDeadReverse(isAlive: (id: EntityID) => boolean): number;
135
+ /** Sources that point at `tgt`, ascending by id (deterministic, cold-path
136
+ * sort) — empty when none. */
137
+ sourcesOf(tgt: EntityID): EntityID[];
138
+ /** Drop every derived side index — the reverse index and (for multi) the
139
+ * forward target sets. Restore path only (`restoreRelations`): the side
140
+ * indices are rebuilt from scratch, so they're cleared first to stay
141
+ * idempotent when restoring into a dirty world. The backing sparse store is
142
+ * cleared separately by `restoreSparseStores`. */
143
+ resetIndices(): void;
144
+ /** Clear this relation's forward representation (multi: the side map;
145
+ * exclusive: nothing — its forward links live in the sparse store, cleared by
146
+ * `restoreSparseStores`). */
147
+ protected abstract _resetForward(): void;
148
+ /** Add a `(R, tgt)` link from `src`, keeping forward link + membership +
149
+ * reverse index in lockstep. Exclusive: replaces any existing target
150
+ * (idempotent if `tgt` is already the target). Multi: adds to the set
151
+ * (idempotent on a duplicate), establishing the membership tag on the first
152
+ * target. `src` must be live — `Store` checks before delegating (#495). */
153
+ abstract link(src: EntityID, tgt: EntityID): void;
154
+ /** Remove `(R, tgt)` from `src` — or, when `tgt` is omitted, every target of
155
+ * `src` — keeping forward link + membership + reverse index in lockstep. A
156
+ * no-op when the link isn't present. */
157
+ abstract unlink(src: EntityID, tgt?: EntityID): void;
158
+ /** Purge a destroyed **source**: drop its forward link(s) and unlink it from
159
+ * every target's reverse set, but leave the sparse **membership** row alone —
160
+ * `Store._purgeSparse` drops that (for every store) right after, so this
161
+ * runs first and must not double-remove. */
162
+ abstract purgeSource(src: EntityID): void;
163
+ /** The single target of source `index`, or `undefined`. Exclusive reads the
164
+ * sparse field; multi has no single target and returns `undefined` (matching
165
+ * the production `targetOf`-on-multi read, which the `DEV` guard rejects). */
166
+ abstract singleTarget(index: number): EntityID | undefined;
167
+ /** All targets of source `index`, ascending by id — one or zero for
168
+ * exclusive, the full sorted set for multi. The symmetric counterpart to
169
+ * `sourcesOf` (#498 item 4). */
170
+ abstract targetsOf(index: number): EntityID[];
171
+ /** Whether source `index` holds any target under this relation (sparse
172
+ * membership). */
173
+ abstract has(index: number): boolean;
174
+ /** Fold `cb` over this relation's **multi** forward target sets in canonical
175
+ * order (sources ascending by index, each source's targets ascending by id),
176
+ * skipping empty sets. The single source of truth for the canonical multi
177
+ * traversal shared by `stateHash`, `snapshotRelations`, and `pairsOf`
178
+ * (#498 item 1). Exclusive relations contribute via the sparse store, so this
179
+ * is a no-op for them. */
180
+ abstract forEachCanonicalTargetSet(cb: CanonicalTargetSetFn): void;
181
+ /** Fold `cb` over every `(source, target)` pair of this relation in canonical
182
+ * order — the `(R, *)` wildcard drive order (#472). `makeId` maps a source
183
+ * index to its full `EntityID`. Exclusive rides the backing store's
184
+ * `canonicalIndices`; multi rides `forEachCanonicalTargetSet`. */
185
+ abstract forEachCanonicalPair(makeId: MakeSourceID, cb: CanonicalPairFn): void;
186
+ /** Rebuild the reverse index from the forward links after a restore.
187
+ * Exclusive reads it back from the just-restored sparse target field (the
188
+ * reverse index is never serialized); multi already rebuilt its reverse edges
189
+ * in `restoreAddTarget`, so this is a no-op for it. `makeId` maps a source
190
+ * index to its full `EntityID`. */
191
+ abstract rebuildReverseFromForward(makeId: MakeSourceID): void;
192
+ /** Restore one decoded multi forward edge `(source index → tgt)` plus its
193
+ * reverse edge (the source's full `EntityID` is `src`). Multi only — the
194
+ * snapshot header guards exclusive relations out before this is reached, so
195
+ * the exclusive override throws defensively. */
196
+ abstract restoreAddTarget(index: number, tgt: EntityID, src: EntityID): void;
197
+ }
198
+ /** Build the right `RelationStore` for a cardinality. The only construction
199
+ * site is `Store.registerRelation`, which resolves the backing sparse store and
200
+ * hands it in so the relation can drive forward/membership rows directly. */
201
+ export declare function makeRelationStore(exclusive: boolean, sparse: SparseComponentDef, store: SparseComponentStore, onDeleteTarget: OnDeleteTarget): RelationStore;
202
+ /** Serialize the relation side data that does **not** live in the sparse store
203
+ * — i.e. each **multi** relation's forward target sets (`Map<source index,
204
+ * Set<target>>`). Exclusive relations store their target in the backing sparse
205
+ * field, so they ride `snapshotSparseStores` and contribute only a header
206
+ * here. The reverse index is fully derivable (from the sparse store for
207
+ * exclusive, from these sets for multi), so it is never serialized — it is
208
+ * rebuilt by `restoreRelations` + the caller's exclusive-reverse pass.
209
+ *
210
+ * Members are emitted in canonical order (sources ascending by entity index,
211
+ * each source's targets ascending by id) so the bytes are independent of
212
+ * add/remove history — the same byte-level determinism invariant the sparse
213
+ * snapshot holds (#470). Layout (integers little-endian):
214
+ *
215
+ * u32 relationCount
216
+ * repeat relationCount times (registration / id order):
217
+ * u32 isMulti
218
+ * if multi:
219
+ * u32 sourceCount
220
+ * repeat sourceCount times (ascending source index):
221
+ * u32 sourceIndex
222
+ * u32 targetCount
223
+ * f64 × targetCount (target EntityIDs, ascending) */
224
+ export declare function snapshotRelations(relations: readonly RelationStore[]): Uint8Array;
225
+ /** Rebuild every relation's side indices from a `snapshotRelations` buffer and
226
+ * the already-restored sparse stores. Each relation's indices are reset first
227
+ * (idempotent into a dirty world), then:
228
+ *
229
+ * - **multi** — forward target sets and their reverse edges are rebuilt from
230
+ * `bytes` (the sets aren't in the sparse store, so the bytes are the only
231
+ * source). `makeId` maps a source entity index to its full `EntityID`
232
+ * (generation from the live slot) so reverse edges key correctly.
233
+ * - **exclusive** — only the reset happens here; the reverse index is rebuilt
234
+ * by the caller from the backing sparse store (which already round-tripped
235
+ * through `restoreSparseStores`), since this function can't read it.
236
+ *
237
+ * Throws `SparseRestoreError` on any shape mismatch (relation count, an
238
+ * exclusive/multi flag that disagrees with the registered relation, a multi
239
+ * source index past `MAX_INDEX`, or a truncated / over-long buffer) rather than
240
+ * silently building a corrupt index. */
241
+ export declare function restoreRelations(relations: readonly RelationStore[], bytes: Uint8Array, makeId: (index: number) => EntityID): void;
242
+ export {};
243
+ //# sourceMappingURL=relation.d.ts.map
@@ -1,13 +1,18 @@
1
- import { Brand } from '../../type_primitives';
2
- import { EntityID } from './entity';
3
- import { SparseComponentDef, SparseComponentStore } from './sparse_store';
1
+ import { Brand } from '../../type_primitives/index.js';
2
+ import { EntityID } from './entity.js';
3
+ import { SparseComponentDef, SparseComponentStore } from './sparse_store.js';
4
4
  /** Relation handle id. A separate id space from `ComponentID` and
5
- * `SparseComponentID` — it indexes `Store`'s relation registry. The numeric
5
+ * `SparseComponentID` — it indexes the `RelationService` registry. The numeric
6
6
  * value is the registration order, stable for the lifetime of the ECS. */
7
7
  export type RelationID = Brand<number, "relation_id">;
8
8
  declare const __relationBrand: unique symbol;
9
- export type RelationDef = RelationID & {
9
+ /** A relation's registration-time cardinality: `exclusive` = one target per
10
+ * source (re-add replaces), `multi` = a target set per source. */
11
+ export type RelationCardinality = "exclusive" | "multi";
12
+ declare const __relationCardinality: unique symbol;
13
+ export type RelationDef<C extends RelationCardinality = RelationCardinality> = RelationID & {
10
14
  readonly [__relationBrand]: "relation";
15
+ readonly [__relationCardinality]?: [C];
11
16
  };
12
17
  /** Access sentinel for the `(*, T)` wildcard query iteration
13
18
  * (`Query.forEachRelatedTo`, #579). A `(*, T)` term reads **every** registered
@@ -36,13 +41,19 @@ export type OnDeleteTarget = "delete" | "clear" | "orphan";
36
41
  export declare const DEFAULT_ON_DELETE_TARGET: OnDeleteTarget;
37
42
  /** Registration options. `exclusive` (one target per source) is the default;
38
43
  * pass `{ multi: true }` for a multi-target relation. The two are mutually
39
- * exclusive — passing both throws. `onDeleteTarget` selects the cleanup
40
- * policy applied to sources when a target is destroyed (default `orphan`). */
41
- export interface RelationOptions {
42
- readonly exclusive?: boolean;
43
- readonly multi?: boolean;
44
+ * exclusive — the union makes `{ exclusive: true, multi: true }` a compile
45
+ * error (it also throws at runtime, for JS callers). `onDeleteTarget` selects
46
+ * the cleanup policy applied to sources when a target is destroyed (default
47
+ * `orphan`). */
48
+ export type RelationOptions = {
49
+ readonly exclusive?: true;
50
+ readonly multi?: false;
44
51
  readonly onDeleteTarget?: OnDeleteTarget;
45
- }
52
+ } | {
53
+ readonly multi: true;
54
+ readonly exclusive?: false;
55
+ readonly onDeleteTarget?: OnDeleteTarget;
56
+ };
46
57
  /** Field name carrying the target `EntityID` on an exclusive relation's backing
47
58
  * sparse component. Field 0 of a single-field schema. */
48
59
  export declare const RELATION_TARGET_FIELD = "target";
@@ -64,8 +75,9 @@ export type MakeSourceID = (index: number) => EntityID;
64
75
  * its sparse interaction are owned by the concrete `ExclusiveRelationStore` /
65
76
  * `MultiRelationStore` (built by `makeRelationStore`).
66
77
  *
67
- * `Store` owns what is genuinely its own — entity liveness, registration, and
68
- * destroy orchestration and drives a relation through the virtual `link` /
78
+ * The registry level (`Store` + `RelationService`) owns what is genuinely its
79
+ * ownentity liveness, registration, and destroy orchestration and drives
80
+ * a relation through the virtual `link` /
69
81
  * `unlink` / `purgeSource` / `forEachCanonicalPair` / … without ever
70
82
  * branching on cardinality. Each cardinality keeps forward link + reverse index
71
83
  * + sparse membership in lockstep inside one method on one class, so the
@@ -150,7 +162,7 @@ export declare abstract class RelationStore {
150
162
  abstract purgeSource(src: EntityID): void;
151
163
  /** The single target of source `index`, or `undefined`. Exclusive reads the
152
164
  * sparse field; multi has no single target and returns `undefined` (matching
153
- * the production `targetOf`-on-multi read, which the `__DEV__` guard rejects). */
165
+ * the production `targetOf`-on-multi read, which the `DEV` guard rejects). */
154
166
  abstract singleTarget(index: number): EntityID | undefined;
155
167
  /** All targets of source `index`, ascending by id — one or zero for
156
168
  * exclusive, the full sorted set for multi. The symmetric counterpart to
@@ -1 +1 @@
1
- {"version":3,"file":"relation.d.ts","sourceRoot":"","sources":["../../../src/core/ecs/relation.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"relation.d.ts","sourceRoot":"","sources":["../../../src/core/ecs/relation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,235 @@
1
+ import { BitSet, TypedArrayTag } from '../../type_primitives/index.cjs';
2
+ import { Archetype } from './archetype.cjs';
3
+ import { EntityID } from './entity.cjs';
4
+ import { RelationDef, RelationOptions, RelationStore } from './relation.cjs';
5
+ import { SparseComponentDef, SparseComponentID, SparseComponentStore } from './sparse_store.cjs';
6
+ /** What the relation service needs from `Store` — nothing more. The accessor
7
+ * members re-read the live field on every call, so capacity growth that
8
+ * reallocates `entityGenerations` / `entityArchetype` / `entityRow` is always
9
+ * observed; never cache their return values across mutations. */
10
+ export interface RelationServiceHost {
11
+ isAlive(id: EntityID): boolean;
12
+ hasSparse(entityId: EntityID, def: SparseComponentDef): boolean;
13
+ pushSparseStore(fieldNames: string[], fieldTypes: TypedArrayTag[]): SparseComponentDef;
14
+ sparseStoreOf(def: SparseComponentDef): SparseComponentStore;
15
+ sparseStores(): readonly SparseComponentStore[];
16
+ entityGenerations(): Int32Array;
17
+ entityArchetype(): Int32Array;
18
+ entityRow(): Int32Array;
19
+ archetypes(): readonly Archetype[];
20
+ 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;
21
+ }
22
+ export declare class RelationService {
23
+ private readonly host;
24
+ private readonly relations;
25
+ private _hasTargetCleanup;
26
+ private readonly _hierarchyRadixOut;
27
+ private readonly _hierarchyRadixC0;
28
+ private readonly _hierarchyRadixC1;
29
+ constructor(host: RelationServiceHost);
30
+ /** The registry, indexed by RelationID — the read-only view the Store's
31
+ * `stateHash` / snapshot / restore paths iterate. */
32
+ get stores(): readonly RelationStore[];
33
+ /** Number of registered relations. */
34
+ get count(): number;
35
+ /** Whether any relation registered a non-`orphan` `onDeleteTarget` policy —
36
+ * the destroy paths' gate for `cleanupTarget` (#473). */
37
+ get hasTargetCleanup(): boolean;
38
+ /** Register a relation kind. `exclusive` (the default) → one target per
39
+ * source, stored in a backing `{ target: f64 }` sparse component, so the
40
+ * forward index rides the sparse store and inherits query membership (#469),
41
+ * `stateHash` + snapshot/restore (#470) for free. `multi` → a set of
42
+ * targets per source, backed by a sparse tag for membership plus a side
43
+ * forward index the relation owns. The two cardinalities are mutually
44
+ * exclusive. `onDeleteTarget` selects the cleanup policy run when a target
45
+ * is destroyed — `delete` (cascade-destroy sources), `clear` (drop the link,
46
+ * sources survive), or `orphan` (default: leave it dangling, #473). See
47
+ * ADR-0011 and `relation.ts`.
48
+ *
49
+ * The backing sparse store is resolved and handed to the relation so it can
50
+ * drive forward/membership rows directly — the cardinality-specific
51
+ * interaction is `ExclusiveRelationStore` / `MultiRelationStore`'s, not a
52
+ * branch here (#498). */
53
+ registerRelation(opts?: RelationOptions): RelationDef;
54
+ private relationOf;
55
+ /** Add a `(R, tgt)` pair to `src`. No archetype transition. Exclusive:
56
+ * replaces any existing target (engine-enforced one-per-source), a no-op if
57
+ * `tgt` is already the target. Multi: adds `tgt` to the set, a no-op if
58
+ * already present. A dead `src` *or* `tgt` is caller error: it throws in
59
+ * `DEV` and is a no-op in production — symmetric, so a production build
60
+ * never links a reverse-index entry keyed by a destroyed handle (#495). */
61
+ addRelation(src: EntityID, def: RelationDef, tgt: EntityID): void;
62
+ /** Remove a `(R, tgt)` pair from `src`. No archetype transition. Exclusive:
63
+ * `tgt` is optional and the removal is a no-op when it names a target other
64
+ * than the current one. Multi: omitting `tgt` removes *all* of `src`'s
65
+ * targets; passing one removes just that pair (dropping membership when the
66
+ * set empties). */
67
+ removeRelation(src: EntityID, def: RelationDef, tgt?: EntityID): void;
68
+ /** The single target of `src` under an exclusive relation, or `undefined`.
69
+ * Throws in `DEV` on a multi-target relation (use `targetsOf`). */
70
+ targetOf(src: EntityID, def: RelationDef): EntityID | undefined;
71
+ /** All targets of `src` under `R` — one or zero for exclusive, the full set
72
+ * for multi — ascending by id. */
73
+ targetsOf(src: EntityID, def: RelationDef): EntityID[];
74
+ /** Sources that point at `tgt` under `R` (the reverse index), ascending by
75
+ * id; empty when none. */
76
+ sourcesOf(tgt: EntityID, def: RelationDef): EntityID[];
77
+ /** Whether `src` holds any pair under `R`. */
78
+ hasRelation(src: EntityID, def: RelationDef): boolean;
79
+ /** All `(source, target)` pairs of relation `R` — the `(R, *)` wildcard
80
+ * (#472). Sources are emitted in **canonical entity-index order** (the #470
81
+ * determinism convention): exclusive relations ride the backing sparse
82
+ * store's `canonicalIndices`, multi relations ride the same
83
+ * `forEachCanonicalTargetSet` traversal `stateHash` / `snapshotRelations`
84
+ * use; a multi source's targets follow ascending by id. Empty when the
85
+ * relation holds no pairs. Cold path — allocates the result array (and, for
86
+ * multi, sorts each source's target set); not for per-tick use. The
87
+ * point-query forms are `targetOf` / `targetsOf`. */
88
+ pairsOf(def: RelationDef): readonly (readonly [EntityID, EntityID])[];
89
+ /** Every `(relation, source)` pointing at `tgt`, across **all** registered
90
+ * relation kinds — the `(*, T)` wildcard (#472). Walks the relation registry
91
+ * in id order (each relation's reverse index already returns sources
92
+ * ascending by id), so the result is ordered by relation id then source id.
93
+ * Empty when nothing targets `tgt`. The single-relation form is
94
+ * `sourcesOf(tgt, def)`. */
95
+ sourcesOfAny(tgt: EntityID): readonly (readonly [RelationDef, EntityID])[];
96
+ /** The backing sparse component id of relation `R` — the membership store a
97
+ * `(R, *)` wildcard term (`Query.withRelation`) drives through the shared
98
+ * sparse-match path. Exclusive relations back a `{ target: f64 }` sparse
99
+ * component, multi a tag; both carry per-source membership, so "has any
100
+ * `(R, *)` pair" is exactly membership in this store (including an
101
+ * orphan-dangling source, whose membership row persists — consistent with
102
+ * `pairsOf`). The def is engine-owned and never handed to callers; this hands
103
+ * the query builder only its erased id for the `sparseInclude` list. */
104
+ relationBackingSparseId(def: RelationDef): SparseComponentID;
105
+ /** Drive a `(*, T)` wildcard query (`Query.forEachRelatedTo`): every source
106
+ * related to `target` under **any** relation, intersected with the query's
107
+ * dense mask + sparse require/exclude terms + the default enabled-row filter,
108
+ * each source yielded once. Unions `sourcesOf(target, R)` across every
109
+ * relation into a `Set` (dedup by full `EntityID` — a source related to `T`
110
+ * via two relations is yielded once), then sorts ascending: the cross-relation
111
+ * union has no inherent order, so one cold sort gives a deterministic,
112
+ * canonical `(*, T)` order matching `sourcesOf` / `sourcesOfAny` (which sort
113
+ * the same way). Cold/structural — not a per-tick hot loop over many targets.
114
+ * `sparseInclude`/`sparseExclude` carry both raw sparse terms and the backing
115
+ * stores of any composed `(R, *)` terms, so it intersects with them uniformly. */
116
+ forEachRelationTargetMatch(target: EntityID, include: BitSet, exclude: BitSet | null, anyOf: BitSet | null, sparseInclude: readonly SparseComponentID[], sparseExclude: readonly SparseComponentID[], includeDisabled: boolean, cb: (entityId: EntityID) => void): void;
117
+ /** Reclaim reverse-index memory: drop every relation's reverse entries whose
118
+ * **target** has been destroyed, returning the total dropped across all
119
+ * relations (#491). Under the default `orphan` policy a destroyed target
120
+ * leaves its reverse entry intact until each source re-targets or dies, so a
121
+ * long-lived source that orphan-points at a churn of short-lived targets and
122
+ * never re-targets accumulates dead-target keys without bound. This cold-path
123
+ * hook drops them on demand — call it at scene or snapshot boundaries.
124
+ *
125
+ * Purely a memory reclaim with no observable state change: forward links stay
126
+ * dangling (so `orphan`'s `targetOf`-returns-the-dead-handle contract is
127
+ * unchanged), `stateHash` is unaffected (the reverse index is derived, never
128
+ * folded), and the dropped entries are faithfully rebuilt by snapshot/restore
129
+ * from the surviving forward links (`Store._rebuildRelationIndices`). The only
130
+ * difference a caller can see is `sourcesOf(deadHandle, R)` going from the
131
+ * dangling sources to `[]` — both meaningless once the target is gone.
132
+ * No-op (returns 0) when no relations are registered. */
133
+ compactRelations(): number;
134
+ /** Walk exclusive relation `R` from `src` toward the root, returning the
135
+ * **up**-chain `[src, parent, grandparent, …, root]` (nearest-ancestor-first,
136
+ * inclusive of both endpoints). A source with no target returns `[src]`. The
137
+ * root is the first entity in the chain with no `R`-target, **or** a dangling
138
+ * dead target handle (see below). Throws `RELATION_MODE_MISMATCH` on a multi
139
+ * relation and `RELATION_CYCLE` on a cycle (both `DEV`-only; in production
140
+ * a cycle stops at the repeated node).
141
+ *
142
+ * **Dangling links terminate the chain.** Under the `orphan` policy a source
143
+ * keeps pointing at a destroyed target (a dead handle). The walk must *not*
144
+ * advance through such a handle: the backing store is keyed by entity
145
+ * **index**, so reading the dead handle's index would return whatever now
146
+ * occupies that recycled slot — splicing the chain onto an unrelated entity
147
+ * (the ABA the `EntityID`-keyed reverse index avoids for `cascadeOf`). So a
148
+ * dead next-hop is appended as the chain's (dangling) terminus and the walk
149
+ * stops; the caller can detect it with `isAlive`, exactly as `targetOf`
150
+ * returns a dead handle. */
151
+ ancestorsOf(src: EntityID, def: RelationDef): EntityID[];
152
+ /** The root of `src`'s exclusive-relation chain — the last entity of
153
+ * `ancestorsOf` (the one with no `R`-target). `src` itself when it has no
154
+ * target. If the chain ends in a dangling dead target handle (orphan policy),
155
+ * that handle is the root — `isAlive`-check the result if dangling links are
156
+ * possible. Same `DEV` guards as `ancestorsOf`. */
157
+ rootOf(src: EntityID, def: RelationDef): EntityID;
158
+ /** Walk exclusive relation `R` **down** from `root` over the reverse index,
159
+ * returning the subtree (including `root`) breadth-first — **parents before
160
+ * children** (the `cascade` order). Children of each node come from
161
+ * `sourcesOf` (ascending by id), so the traversal is deterministic. Throws
162
+ * `RELATION_MODE_MISMATCH` on a multi relation and `RELATION_CYCLE` on a cycle
163
+ * (both `DEV`-only; in production an already-visited node is skipped, so
164
+ * it never hangs). */
165
+ cascadeOf(root: EntityID, def: RelationDef): EntityID[];
166
+ /** Purge a destroyed entity from the relation indices — its **source** role
167
+ * only: drop its forward target(s) and unlink it from every target's reverse
168
+ * set. The backing sparse membership row is dropped separately by the Store's
169
+ * `_purgeSparse`, so this must run *before* it (it reads the sparse target
170
+ * field for exclusive relations). The entity's **target** role is left
171
+ * intact: the reverse index is keyed by full `EntityID`, so a recycled slot
172
+ * never aliases the dead target's sources. The destroyed entity's **target**
173
+ * role is handled separately by `cleanupTarget` per each relation's
174
+ * `OnDeleteTarget` policy (#473). Gated by the caller on `count > 0`. */
175
+ purgeSource(entityId: EntityID): void;
176
+ /** Apply each relation's `OnDeleteTarget` policy for a destroyed **target**
177
+ * `targetId` (#473). Walks the registry; for every relation whose reverse
178
+ * index has sources pointing at the dead target:
179
+ *
180
+ * - **`delete`** — append each source to `cascade`; the caller destroys them
181
+ * through the same path, so chains/trees cascade recursively. The sources'
182
+ * own source-role purge (`purgeSource`) drops their forward link and
183
+ * unlinks them from `targetId`'s reverse set, so no reverse entry leaks.
184
+ * - **`clear`** — drop each source's link to the dead target in place via
185
+ * `rs.unlink(src, targetId)` (exclusive drops the row; multi removes the
186
+ * target from the set, dropping membership when it empties). Sources
187
+ * survive — the cardinality bookkeeping is the relation's, not branched here.
188
+ * - **`orphan`** — skipped (the link is left dangling; safe because the
189
+ * reverse key carries the generation).
190
+ *
191
+ * `sourcesOf` returns a fresh snapshot, so mutating the reverse index while
192
+ * iterating is safe. Gated by the caller on `hasTargetCleanup`, so the
193
+ * whole walk is skipped when no relation opts into a non-`orphan` policy. */
194
+ cleanupTarget(targetId: EntityID, cascade: EntityID[]): void;
195
+ /** Fourth query-match path (#581): yield the matched entities — the exact
196
+ * sparse-match intersection (dense mask + sparse require/exclude + the
197
+ * default enabled-row filter) — in canonical **hierarchy depth order** over
198
+ * exclusive relation `R`: depth ascending (parents before children), **entity
199
+ * index ascending within each depth band**. Entities deeper than `maxDepth`
200
+ * are skipped (`Infinity` = unbounded). Only reached via `Query.forEachEntity`
201
+ * on a query carrying a `.hierarchy(R)` term.
202
+ *
203
+ * `.hierarchy(R)` does not narrow the matched set — it reorders it — so an
204
+ * entity with no `R`-parent is a root (depth 0) and is yielded first. Depth is a
205
+ * structural property of the *full* tree (an ancestor outside the matched set
206
+ * still counts toward depth), computed by a memoised upward walk shared across
207
+ * the whole batch, so a shared/deep chain costs O(nodes), not O(nodes²).
208
+ *
209
+ * The canonical order is produced without a comparator sort (which the observer
210
+ * bench measured at 2–4× — `observer.ts`): (1) collect the matched ids via
211
+ * the host's `forEachSparseMatch`; (2) `radixSortByIndex` → entity-index
212
+ * ascending; (3) stable-bucket by depth — since the input is index-ascending
213
+ * and the bucket append is stable, each depth band stays index-ascending.
214
+ * Tuned for the motivating per-tick case (transform propagation): the radix
215
+ * scratch is reused instance state (`_hierarchyRadix*`), so a per-tick pass
216
+ * churns no histograms; the working set (`matched` / `buckets` / `depthMemo` /
217
+ * `visiting`) is still allocated per call, as it must stay call-local for
218
+ * re-entrancy.
219
+ *
220
+ * Exclusive-only — a multi relation throws `RELATION_MODE_MISMATCH` in `DEV`
221
+ * (mirrors `cascadeOf` / `ancestorsOf`); a cycle is a loud `RELATION_CYCLE` in
222
+ * `DEV` and a safe break in production. */
223
+ 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;
224
+ /** Depth of entity index `idx` in the exclusive-relation tree backing `store`
225
+ * (root = 0), for `forEachHierarchyMatch`. Memoised across the batch (`memo`),
226
+ * so a shared ancestor chain is walked once. Walks upward via the backing store's
227
+ * target field 0 (mirroring `ancestorsOf`), stopping at: a root (no target), an
228
+ * already-memoised node, or a dangling/dead parent (the child is then treated as
229
+ * a root — never advance through a recycled slot, the ABA `ancestorsOf` guards).
230
+ * `visiting` flags the nodes on the current upward path to catch a cycle —
231
+ * `RELATION_CYCLE` in `DEV`, treated as a root in production — and is emptied
232
+ * on the way back down so it can be reused for the next entity. */
233
+ private _hierarchyDepthOf;
234
+ }
235
+ //# sourceMappingURL=relation_service.d.ts.map