@vworlds/vecs 1.0.44 → 1.0.46

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 (136) hide show
  1. package/README.md +7 -1
  2. package/dist/component.d.ts +57 -51
  3. package/dist/component.js +25 -9
  4. package/dist/component.js.map +1 -1
  5. package/dist/component_meta.d.ts +71 -66
  6. package/dist/component_meta.js +331 -34
  7. package/dist/component_meta.js.map +1 -1
  8. package/dist/dsl.js +1 -1
  9. package/dist/dsl.js.map +1 -1
  10. package/dist/entity/entity.base.js +4 -0
  11. package/dist/entity/entity.base.js.map +1 -1
  12. package/dist/entity/entity.components.d.ts +39 -27
  13. package/dist/entity/entity.components.js +258 -74
  14. package/dist/entity/entity.components.js.map +1 -1
  15. package/dist/entity/entity.d.ts +1 -2
  16. package/dist/entity/entity.js +2 -3
  17. package/dist/entity/entity.js.map +1 -1
  18. package/dist/entity/entity.lifecycle.d.ts +7 -1
  19. package/dist/entity/entity.lifecycle.js +40 -53
  20. package/dist/entity/entity.lifecycle.js.map +1 -1
  21. package/dist/entity/entity.queries.js +30 -0
  22. package/dist/entity/entity.queries.js.map +1 -1
  23. package/dist/entity/entity.relationships.d.ts +4 -0
  24. package/dist/entity/entity.relationships.js +18 -10
  25. package/dist/entity/entity.relationships.js.map +1 -1
  26. package/dist/index.d.ts +7 -4
  27. package/dist/index.js +4 -2
  28. package/dist/index.js.map +1 -1
  29. package/dist/inject.d.ts +63 -14
  30. package/dist/inject.js +165 -89
  31. package/dist/inject.js.map +1 -1
  32. package/dist/modules/identity.js +2 -2
  33. package/dist/modules/identity.js.map +1 -1
  34. package/dist/modules/implements.js +2 -2
  35. package/dist/modules/implements.js.map +1 -1
  36. package/dist/modules/pipeline.js +5 -5
  37. package/dist/modules/pipeline.js.map +1 -1
  38. package/dist/modules/relationship_types.d.ts +70 -3
  39. package/dist/modules/relationship_types.js +70 -3
  40. package/dist/modules/relationship_types.js.map +1 -1
  41. package/dist/modules/relationships.d.ts +31 -8
  42. package/dist/modules/relationships.js +120 -30
  43. package/dist/modules/relationships.js.map +1 -1
  44. package/dist/modules/singleton.js +2 -2
  45. package/dist/modules/singleton.js.map +1 -1
  46. package/dist/modules/snapshot.d.ts +26 -0
  47. package/dist/modules/snapshot.js +45 -0
  48. package/dist/modules/snapshot.js.map +1 -0
  49. package/dist/modules/snapshot_types.d.ts +85 -0
  50. package/dist/modules/snapshot_types.js +130 -0
  51. package/dist/modules/snapshot_types.js.map +1 -0
  52. package/dist/package.json +2 -2
  53. package/dist/query/callbacks.d.ts +9 -7
  54. package/dist/query/callbacks.js +27 -41
  55. package/dist/query/callbacks.js.map +1 -1
  56. package/dist/query/grouped_query.js +7 -7
  57. package/dist/query/grouped_query.js.map +1 -1
  58. package/dist/query/grouping.d.ts +6 -2
  59. package/dist/query/grouping.js +84 -15
  60. package/dist/query/grouping.js.map +1 -1
  61. package/dist/query/query.d.ts +4 -4
  62. package/dist/query/query.js +37 -14
  63. package/dist/query/query.js.map +1 -1
  64. package/dist/snapshot/capture.d.ts +1 -0
  65. package/dist/snapshot/capture.js +212 -0
  66. package/dist/snapshot/capture.js.map +1 -0
  67. package/dist/snapshot/clone.d.ts +1 -0
  68. package/dist/snapshot/clone.js +107 -0
  69. package/dist/snapshot/clone.js.map +1 -0
  70. package/dist/snapshot/index.d.ts +5 -0
  71. package/dist/snapshot/index.js +21 -0
  72. package/dist/snapshot/index.js.map +1 -0
  73. package/dist/snapshot/preflight.d.ts +1 -0
  74. package/dist/snapshot/preflight.js +40 -0
  75. package/dist/snapshot/preflight.js.map +1 -0
  76. package/dist/snapshot/restore.d.ts +1 -0
  77. package/dist/snapshot/restore.js +111 -0
  78. package/dist/snapshot/restore.js.map +1 -0
  79. package/dist/snapshot/types.d.ts +135 -0
  80. package/dist/snapshot/types.js +26 -0
  81. package/dist/snapshot/types.js.map +1 -0
  82. package/dist/system.d.ts +3 -1
  83. package/dist/system.js +33 -23
  84. package/dist/system.js.map +1 -1
  85. package/dist/terms/all_term.js +19 -0
  86. package/dist/terms/all_term.js.map +1 -1
  87. package/dist/terms/any_term.js +19 -0
  88. package/dist/terms/any_term.js.map +1 -1
  89. package/dist/terms/not_term.js +4 -0
  90. package/dist/terms/not_term.js.map +1 -1
  91. package/dist/terms/target_term.d.ts +1 -2
  92. package/dist/terms/target_term.js +2 -4
  93. package/dist/terms/target_term.js.map +1 -1
  94. package/dist/terms/term.d.ts +2 -0
  95. package/dist/terms/term.js +59 -10
  96. package/dist/terms/term.js.map +1 -1
  97. package/dist/util/array_map.d.ts +43 -12
  98. package/dist/util/array_map.js +90 -41
  99. package/dist/util/array_map.js.map +1 -1
  100. package/dist/util/dense_set.js +26 -0
  101. package/dist/util/dense_set.js.map +1 -1
  102. package/dist/world/world.base.js +12 -0
  103. package/dist/world/world.base.js.map +1 -1
  104. package/dist/world/world.d.ts +7 -0
  105. package/dist/world/world.deferred.js +17 -0
  106. package/dist/world/world.deferred.js.map +1 -1
  107. package/dist/world/world.entities.d.ts +13 -2
  108. package/dist/world/world.entities.js +16 -2
  109. package/dist/world/world.entities.js.map +1 -1
  110. package/dist/world/world.js +15 -3
  111. package/dist/world/world.js.map +1 -1
  112. package/dist/world/world.pipeline.d.ts +2 -2
  113. package/dist/world/world.pipeline.js +4 -2
  114. package/dist/world/world.pipeline.js.map +1 -1
  115. package/dist/world/world.queries.d.ts +7 -0
  116. package/dist/world/world.queries.js +52 -0
  117. package/dist/world/world.queries.js.map +1 -1
  118. package/dist/world/world.transfers.d.ts +137 -0
  119. package/dist/world/world.transfers.js +188 -0
  120. package/dist/world/world.transfers.js.map +1 -0
  121. package/docs/README.md +18 -6
  122. package/docs/components.md +91 -48
  123. package/docs/concepts.md +21 -17
  124. package/docs/design-guide.md +125 -89
  125. package/docs/entities.md +62 -72
  126. package/docs/execution-model.md +26 -10
  127. package/docs/getting-started.md +6 -6
  128. package/docs/glossary.md +19 -10
  129. package/docs/modules.md +8 -8
  130. package/docs/queries-and-filters.md +15 -10
  131. package/docs/relationships.md +250 -115
  132. package/docs/snapshots.md +181 -0
  133. package/docs/systems.md +64 -21
  134. package/docs/utilities.md +10 -6
  135. package/package.json +2 -2
  136. package/skills/vecs/SKILL.md +5 -4
@@ -1,13 +1,13 @@
1
1
  # Relationships
2
2
 
3
- Link entities to entities with relationship components: hierarchy, reverse lookup, retargeting,
4
- cleanup cascades, and depth-ordered traversal.
3
+ Link entities to entities with relationship components: many-target edges, reverse lookup,
4
+ one-target constraints, ownership, cleanup cascades, and depth-ordered traversal.
5
5
 
6
6
  ## What a relationship is
7
7
 
8
- A relationship is a component whose `target` field points at another entity. Define one by
9
- extending the abstract `Relationship` base (`lib/vecs/src/component_meta.ts`) and registering it
10
- like any component:
8
+ A relationship is a component with private target storage managed by vecs. Define one by extending
9
+ the abstract `Relationship` base (`lib/vecs/src/component_meta.ts`) and register it like any
10
+ component:
11
11
 
12
12
  ```ts
13
13
  import { Relationship } from "@vworlds/vecs";
@@ -16,149 +16,262 @@ class EquippedBy extends Relationship {}
16
16
 
17
17
  world.component(EquippedBy);
18
18
 
19
- item.set(EquippedBy, { target: player });
19
+ item.add(EquippedBy, player);
20
20
  item.target(EquippedBy); // player
21
+ item.targets(EquippedBy); // readonly snapshot: [player]
22
+ item.hasTarget(EquippedBy, player); // true
21
23
  player.children(EquippedBy).has(item); // true
22
24
  ```
23
25
 
24
- The **source** entity stores the relationship component; the **target** can enumerate its sources
25
- with `children(ref)` through a reverse index the world maintains automatically. An entity holds at
26
- most one instance of a given relationship class (like any component), so multi-target links are
27
- modeled with one relationship class per meaning, or child entities.
26
+ The **source** entity stores the relationship component; each **target** can enumerate its sources
27
+ with `children(ref)` through a reverse index the world maintains automatically. Relationships allow
28
+ many targets by default. Each source/relationship/target edge is unique, so adding the same target
29
+ again is idempotent. Target ordering is not a public contract.
28
30
 
29
- Because `target` is an `Entity` reference, relationships are not wire-serializable by default.
30
- However, `vecs-protocol` registers `Relationship.prototype.target` as an `"entity"` wire field
31
- (u32 eid) at module load, so **relationship subclasses listed in `networkComponents` are
32
- networkable**: the target serialises as a raw entity id, and the client resolves it to a live
33
- entity (creating a ghost placeholder if the parent hasn't arrived yet). List `ChildOf` (or custom
34
- subclasses) explicitly in your `networkComponents` array to replicate parent-child links.
31
+ `target(ref)` is a singular convenience that returns one current target, or `undefined`. Use
32
+ `targets(ref)` for all targets; it returns a safe readonly array snapshot, not vecs's internal
33
+ storage. For a non-exclusive relationship, do not use `target(ref)` as a stable selection policy.
34
+ Use `hasTarget(ref, target)` to test one edge without allocating a snapshot.
35
35
 
36
36
  ## Built-in relationships
37
37
 
38
38
  | Class | Purpose |
39
39
  | --------------- | -------------------------------------------------------------------------------------------------------------------------- |
40
- | `ChildOf` | General parent/child hierarchy. `onDeleteTarget = Delete`: destroying a parent destroys its `ChildOf` children. |
41
- | `DependsOn` | Pipeline ordering: phases chain to each other and systems attach to phases through it. |
42
- | `SilenceSource` | Marks a query's silence domain; configured via [`ignoreSource`](./queries-and-filters.md#silencing-feedback-ignoresource). |
40
+ | `ChildOf` | Exclusive parent/child hierarchy. `onDeleteTarget = Delete`: destroying a parent destroys its `ChildOf` children. |
41
+ | `DependsOn` | Exclusive pipeline ordering: phases chain to each other and systems attach to phases through it. |
42
+ | `SilenceSource` | Exclusive query silence domain; configured via [`ignoreSource`](./queries-and-filters.md#silencing-feedback-ignoresource). |
43
43
 
44
- `entity.childOf(parent)` is shorthand for `entity.set(ChildOf, { target: parent })`.
44
+ `ChildOf` and `DependsOn` are also `Traversable`; all three built-ins above are `Exclusive`.
45
45
 
46
- ## Ensuring a target
46
+ ## Adding, removing, and setting
47
47
 
48
- Use `ensureTarget` when a registered relationship should always have some target and a new,
49
- independent entity is the correct default:
48
+ Create edges explicitly with `add(Relationship, target)`:
50
49
 
51
50
  ```ts
52
- world.component(FriendOf);
51
+ unit.add(VisibleTo, playerA).add(VisibleTo, playerB);
53
52
 
54
- const friend = entity.ensureTarget(FriendOf);
55
- entity.ensureTarget(FriendOf) === friend; // true
56
- entity.target(FriendOf) === friend; // true
53
+ unit.remove(VisibleTo, playerA); // remove one edge
54
+ unit.remove(VisibleTo); // remove the component and every remaining edge
57
55
  ```
58
56
 
59
- The method returns the existing target when present. Otherwise it creates an entity, initializes
60
- the relationship with that target, and attaches the relationship. During deferred execution,
61
- repeated calls share one pending relationship and return the same target; `target(FriendOf)` still
62
- returns `undefined` until flush.
57
+ `add(Relationship)` without a target is invalid. Targets must be live entities in the same world as
58
+ the source. The `target` argument accepts any `ComponentRef` (registered class, numeric component
59
+ id, or entity) and is resolved eagerly via `world.entity(target)`, so the target entity must
60
+ already exist when the call is made. Removing a missing edge is a no-op; removing the final edge
61
+ also removes the relationship component.
63
62
 
64
- The generated target is independent: `ensureTarget` does not add `ChildOf`, and destroying the
65
- source does not destroy the target. Add ownership explicitly when your model requires it. The
66
- relationship class must be registered before calling `ensureTarget`.
63
+ A relationship subclass can still declare ordinary data fields:
67
64
 
68
- ## Owned targets
65
+ ```ts
66
+ class WeightedLink extends Relationship {
67
+ weight = 0;
68
+ }
69
+
70
+ source.add(WeightedLink, target);
71
+ source.set(WeightedLink, { weight: 10 });
72
+ ```
73
+
74
+ For relationships, `set` is **existing-only and data-only**. It updates ordinary fields on an
75
+ attached relationship, but does nothing when the relationship is absent and ignores target-storage
76
+ spellings. Edge changes must use `add` and `remove`; they never fire `onSet`.
77
+
78
+ `attach`, `ensure`, and `getMut` reject relationship components because replacing or mutating an
79
+ instance could bypass edge uniqueness, reverse indexes, and cleanup policies. Target storage is
80
+ private: there is no public `target` or `targets` field to assign. The former `ensureTarget` and
81
+ `onRetarget` APIs have been removed.
69
82
 
70
- Mark a relationship component with `OwnedTarget` during world setup when every target assigned
71
- through that relationship should become a `ChildOf` child of its source:
83
+ `entity.childOf(parent)` is shorthand for `entity.add(ChildOf, parent)`. The `parent` property reads
84
+ the first `ChildOf` target; assigning a parent adds or replaces that target because `ChildOf` is
85
+ exclusive, while assigning `undefined` removes `ChildOf`.
86
+
87
+ ## Exclusive one-target relationships
88
+
89
+ Add the `Exclusive` trait to restrict a relationship to one target:
72
90
 
73
91
  ```ts
74
- import { OwnedTarget, Relationship } from "@vworlds/vecs";
92
+ import { Exclusive, Relationship } from "@vworlds/vecs";
75
93
 
76
- class Inventory extends Relationship {}
94
+ class InCell extends Relationship {}
77
95
 
78
- world.component(Inventory).add(OwnedTarget);
96
+ world.component(InCell).add(Exclusive);
79
97
 
80
- const inventory = player.ensureTarget(Inventory);
81
- inventory.parent === player; // true
98
+ unit.add(InCell, oldCell);
99
+ unit.add(InCell, newCell); // atomically replaces oldCell with newCell
100
+ ```
101
+
102
+ Adding a different target atomically replaces the existing edge. Observers never see two targets:
103
+ the reverse indexes already reflect the replacement when the old edge's remove hook runs, followed
104
+ by the new edge's add hook. Re-adding the current target is a no-op.
105
+
106
+ `ChildOf`, `DependsOn`, and `SilenceSource` are `Exclusive` out of the box. Configure `Exclusive`
107
+ before relationship instances exist; vecs rejects first-time configuration or removal while the
108
+ relationship is in use (re-adding an already attached trait is an idempotent no-op), and rejects
109
+ removing it while `OwnedTarget` or `Traversable` depends on it. The checks run when the trait
110
+ component is actually attached or detached, so in deferred mode the rejection surfaces from
111
+ `flush()` / `progress()` rather than from the `add` / `remove` call site. A thrown trait operation
112
+ leaves the world partially mutated; treat it as a programming error and discard the world.
113
+
114
+ Do not confuse this trait with `world.setExclusiveComponents(A, B)`. `Exclusive` limits **one
115
+ relationship class** to one target. `setExclusiveComponents` declares that **different component
116
+ classes** cannot coexist on one entity (see
117
+ [Components](./components.md#exclusive-component-groups)).
118
+
119
+ ## OneOf target constraint
120
+
121
+ Add the `OneOf` pair-form trait to constrain every target of a relationship to be a direct
122
+ `ChildOf` child of a declared parent entity. Use the pair form `R.add(OneOf, parent)`:
123
+
124
+ ```ts
125
+ import { OneOf, Relationship } from "@vworlds/vecs";
126
+
127
+ class ResidesIn extends Relationship {}
128
+ class Region extends Relationship {}
129
+
130
+ world.component(Region);
131
+ world.component(ResidesIn).add(OneOf, world.component(Region));
132
+
133
+ const forest = world.entity().childOf(world.component(Region));
134
+ const weather = world.entity();
82
135
 
83
- player.destroy();
84
- inventory.destroyed === true; // ChildOf cascades deletion
136
+ world.entity().add(ResidesIn, forest); // ok: forest is a child of Region
137
+ world.entity().add(ResidesIn, weather); // throws: weather is not a child of Region
85
138
  ```
86
139
 
87
- `OwnedTarget` installs `onSet`, `onRetarget`, and `onRemove` hooks on `Inventory`. New targets become
88
- `ChildOf` children, displaced targets are destroyed on retarget, and the current target is destroyed
89
- when the relationship is removed (including source destruction). During deferred execution, these
90
- changes apply while the command queue drains.
140
+ For the enum-style form where the relationship's own entity is the parent, point `OneOf` at the
141
+ relationship itself. A faction diplomacy alignment is a natural fit — every alignment target must
142
+ be a declared value:
91
143
 
92
- Configure the trait before creating relationship instances; it does not backfill existing targets.
93
- Do not share one owned target between several sources: retargeting or removing any owning
94
- relationship can destroy it. `OwnedTarget` rejects ordinary components and `ChildOf` itself.
144
+ ```ts
145
+ class Alignment extends Relationship {}
146
+
147
+ const alignment = world.component(Alignment).add(OneOf, world.component(Alignment));
148
+ const good = world.entity().childOf(alignment);
149
+ const evil = world.entity().childOf(alignment);
150
+
151
+ world.entity().add(Alignment, good); // ok: good is a child of Alignment
152
+ world.entity().add(Alignment, weather); // throws: weather is not a child of Alignment
153
+ ```
154
+
155
+ `OneOf` checks `target.parent === parentEntity`, so only a **direct** `ChildOf` edge satisfies
156
+ the constraint — transitive descendants are rejected. The check runs in the relationship's
157
+ `targetPolicyAdd` hook, so in deferred mode an invalid target surfaces from `flush()` /
158
+ `progress()` rather than from the `add` call site. A thrown edge leaves the world partially
159
+ mutated; treat it as a programming error and discard the world (same contract as other
160
+ trait-invariant violations).
161
+
162
+ Configure `OneOf` before relationship instances exist. Attaching it while the relationship is
163
+ already in use throws, and removing it throws once attached — `OneOf` is irrevocable. Re-adding
164
+ `OneOf` to the same parent is an idempotent no-op. World teardown bypasses these guards, so
165
+ `clearAllEntities` can still tear down worlds with `OneOf` configured.
95
166
 
96
- ## Retargeting
167
+ `OneOf` is independent of `Exclusive`: a non-exclusive relationship may carry it, in which case
168
+ every one of its many targets must be a child of the declared parent. It composes freely with
169
+ `OwnedTarget` and `Traversable`.
97
170
 
98
- Point a relationship somewhere else with a plain `set`:
171
+ ## Edge hooks
172
+
173
+ Relationship component entities expose one public hook slot for edge addition and one for edge
174
+ removal:
99
175
 
100
176
  ```ts
101
- unit.set(InCell, { target: newCell }); // reverse index updates automatically
177
+ world
178
+ .component(InCell)
179
+ .on("targetAdd", (source, relationship, target) => {
180
+ // The edge and reverse index already exist.
181
+ })
182
+ .on("targetRemove", (source, relationship, target) => {
183
+ // The edge and reverse index are already gone.
184
+ });
102
185
  ```
103
186
 
104
- Retargeting is a **data change, not an archetype change** — no component is added or removed, so
105
- no `enter`/`exit` churn. Prefer retargeting a relationship over swapping marker components to
106
- express "which bucket this entity is in", and pair it with
107
- [`groupBy(Relationship)`](./queries-and-filters.md#grouped-queries-groupby) for O(1) bucket
108
- lookups.
187
+ Both handlers receive `(source, relationshipInstance, target)`. For a new relationship,
188
+ `onTargetAdd` runs before `onAdd`; query entry follows `onAdd`. Removing the whole relationship
189
+ routes query exit and detaches all edges, calls `onTargetRemove` once per edge, then
190
+ calls `onRemove`. An exclusive replacement calls `onTargetRemove` for the old target before
191
+ `onTargetAdd` for the new target without removing and re-adding the component. Edge changes route
192
+ relationship-dependent query updates, but do not call `onSet`; `onSet` is reserved for ordinary
193
+ relationship data changed through `set`.
194
+
195
+ Register handlers with `on("targetAdd", handler)` and `on("targetRemove", handler)`; remove them
196
+ with `off(event, handler)`. Multiple handlers may be registered for the same event and fire in
197
+ registration order. Framework traits install separate internal `targetPolicyAdd` /
198
+ `targetPolicyRemove` policies, so adding or removing a trait does not replace these public hooks.
109
199
 
110
- `getMut` throws for relationship components: in-place target mutation would bypass the reverse
111
- index used by `children()` and the cleanup cascades. Always retarget with `set`.
200
+ ## Owned targets
112
201
 
113
- Register `onRetarget` when behavior needs both targets:
202
+ Use `OwnedTarget` when an exclusive relationship explicitly owns its target:
114
203
 
115
204
  ```ts
116
- world.component(InCell).onRetarget((entity, oldCell, newCell) => {
117
- // entity.target(InCell) is newCell here.
118
- // oldCell.children(InCell) no longer contains entity.
119
- // newCell.children(InCell) already contains entity.
120
- });
205
+ import { Exclusive, OwnedTarget, Relationship } from "@vworlds/vecs";
206
+
207
+ class Inventory extends Relationship {}
208
+
209
+ world.component(Inventory).add(Exclusive).add(OwnedTarget);
210
+
211
+ const inventory = world.entity();
212
+ player.add(Inventory, inventory);
213
+ inventory.parent === player; // true
214
+
215
+ player.remove(Inventory); // destroys inventory
121
216
  ```
122
217
 
123
- It fires only when an existing relationship moves to a different existing target, after reverse
124
- indexes update and before `onSet`. Initial assignment, same-target writes, and removal do not fire
125
- `onRetarget`; use `onSet` and `onRemove` for those lifecycle points.
218
+ `OwnedTarget` requires `Exclusive`, rejects ordinary components and `ChildOf`, and never creates a
219
+ target implicitly. You must create the target and pass it to `add`. When an edge is added, the target
220
+ becomes a `ChildOf` child of its source before the public `onTargetAdd` hook runs. When that edge is
221
+ removed, its target is destroyed before the public `onTargetRemove` hook runs. Exclusive replacement
222
+ therefore destroys the displaced target and adopts the replacement atomically; removing the
223
+ relationship or destroying its source destroys the current target.
224
+
225
+ Configure the traits before creating relationship instances. `OwnedTarget` is irrevocable once
226
+ attached: removing it throws, and only world teardown (`clearAllEntities`) bypasses the guard —
227
+ the same contract as `Traversable` and `OneOf`. Do not share an owned target between
228
+ sources because either owner can destroy it when its edge is removed.
126
229
 
127
230
  ## Cleanup when a target dies
128
231
 
129
- What happens to sources when their target entity is destroyed is governed by the relationship's
130
- `meta.onDeleteTarget`:
232
+ What happens to each incoming edge when its target entity is destroyed is governed by the
233
+ relationship's `meta.onDeleteTarget`:
131
234
 
132
- | Policy | Meaning |
133
- | ---------------------- | ----------------------------------------------------------------------------------------- |
134
- | `CleanupPolicy.Remove` | Default. The relationship component is removed from each source. |
135
- | `CleanupPolicy.Delete` | Each source entity targeting the destroyed entity through this relationship is destroyed. |
235
+ | Policy | Meaning |
236
+ | ---------------------- | ------------------------------------------------------------------------------------------ |
237
+ | `CleanupPolicy.Remove` | Default. Remove only the edge to the destroyed target; other targets remain attached. |
238
+ | `CleanupPolicy.Delete` | Destroy each source entity that has an edge to the destroyed target through this relation. |
136
239
 
137
240
  ```ts
138
241
  import { CleanupPolicy } from "@vworlds/vecs";
139
242
 
140
- world.component(MyRel).meta.onDeleteTarget = CleanupPolicy.Delete; // cascade like ChildOf
243
+ world.component(MyRel).meta.onDeleteTarget = CleanupPolicy.Delete;
141
244
  ```
142
245
 
143
246
  `ChildOf` ships with `Delete`, so destroying a parent recursively destroys its subtree:
144
247
 
145
248
  ```ts
146
- child.set(ChildOf, { target: parent });
147
- parent.destroy(); // child (and its own ChildOf children) are destroyed too
249
+ child.add(ChildOf, parent);
250
+ parent.destroy(); // child and its own ChildOf children are destroyed too
148
251
  ```
149
252
 
150
- `onDeleteTarget` is independent from `onDelete`: `onDeleteTarget` runs when a relationship
151
- **target** is destroyed; `onDelete` runs when the relationship **component entity itself** is
253
+ With the default `Remove`, cleanup follows normal per-edge semantics: `onTargetRemove` receives the
254
+ destroyed target, and the relationship component remains if another edge survives. Removing the
255
+ last edge also removes the component.
256
+
257
+ `onDeleteTarget` is independent from `onDelete`: `onDeleteTarget` applies when a relationship
258
+ **target** is destroyed; `onDelete` applies when the relationship **component entity itself** is
152
259
  destroyed (see
153
260
  [Components — cleanup policies](./components.md#cleanup-policies-destroying-a-component-entity)).
154
261
 
262
+ If a cleanup cascade hits an impossible combination — for example a `Delete` source that is itself an
263
+ in-use `Throw`-policy component entity — vecs throws and the world is unusable from that point. The
264
+ exception propagates out of the `destroy()` / `progress()` / `flush()` call that triggered the
265
+ cascade. Configure policies so cascades are consistent; this is a programming error, not a
266
+ recoverable runtime condition.
267
+
155
268
  ## Querying through relationships
156
269
 
157
270
  The DSL follows relationships in both directions
158
271
  ([operator table](./queries-and-filters.md#the-query-dsl)):
159
272
 
160
273
  ```ts
161
- // Reactive: candidates whose FriendOf target has Position and Velocity.
274
+ // Reactive: candidates with any FriendOf target that has Position and Velocity.
162
275
  world.query("body-friends").with({ all: [Body, { target: [FriendOf, [Position, Velocity]] }] });
163
276
 
164
277
  // Reactive: candidates whose ChildOf parent has Body.
@@ -168,53 +281,75 @@ world.query("positioned-children").with({ all: [Position, { parent: Body }] });
168
281
  world.filter({ all: [Body, { children: Position }] }).forEach((parent) => { ... });
169
282
  ```
170
283
 
171
- `target` / `parent` queries are **live on both sides**: adding or removing the relationship on the
172
- candidate re-routes it normally, and changing the _target's_ components refreshes every candidate
173
- pointing at it — across multiple nested `target` hops. `source` / `children` look down the reverse
174
- index and are **non-reactive**: exact for each `Filter` scan but rejected by tracked queries and
175
- systems.
284
+ `target` is existential: a source matches when **any** target through the relationship matches the
285
+ inner DSL. `target` / `parent` queries are live on both sides: edge changes re-evaluate the source,
286
+ and changes to any target's components refresh every source pointing at it, across nested `target`
287
+ hops. `source` / `children` look down the reverse index and are non-reactive: exact for each
288
+ `Filter` scan but rejected by tracked queries and systems.
176
289
 
177
- To pull a target's or children's components into callbacks, use `target` / `down` injection — see
178
- [Systems — relationship injection](./systems.md#relationship-injection-target-and-down).
290
+ To pull one current target or all targets into callbacks, use singular `target` or plural `targets`
291
+ injection; see [Systems — relationship injection](./systems.md#relationship-injection).
179
292
 
180
293
  ## Traversal order: `Traversable` and `cascade`
181
294
 
182
- Tag a relationship component entity with `Traversable` to enable automatic depth tracking: every
183
- entity carrying the relationship gets a depth (hops to its root), maintained reactively by
184
- `RelationshipsModule` (`lib/vecs/src/modules/relationships.ts`):
295
+ Tag an exclusive relationship component entity with `Traversable` to enable automatic depth
296
+ tracking: every entity carrying the relationship gets a depth (hops to its root), maintained
297
+ reactively by `RelationshipsModule` (`lib/vecs/src/modules/relationships.ts`):
185
298
 
186
299
  ```ts
187
- import { Traversable } from "@vworlds/vecs";
300
+ import { Exclusive, Traversable } from "@vworlds/vecs";
188
301
 
189
- world.component(InCell).add(Traversable);
302
+ world.component(InCell).add(Exclusive).add(Traversable);
190
303
  ```
191
304
 
192
- A query can then order its members depth-first along the relationship with `cascade`:
305
+ `Traversable` requires `Exclusive` because a node needs one unambiguous parent. It backfills existing
306
+ exclusive edges when attached. A query can then order members depth-first with `cascade`:
193
307
 
194
308
  ```ts
195
309
  world.query("tree").with({ with: Node, cascade: ChildOf });
196
310
  ```
197
311
 
198
- Parents sort before children. Retargeting updates relationship depths, and cascade-ordered queries
199
- refresh their order before the next iteration. `ChildOf` and `DependsOn` are `Traversable` out of
200
- the box — the world's default pipeline is itself a `cascade: DependsOn` query (see
201
- [Execution model](./execution-model.md#the-pipeline-is-a-query)).
312
+ Parents sort before children. Replacing an exclusive edge updates relationship depths, and
313
+ cascade-ordered queries refresh before the next iteration. `ChildOf` and `DependsOn` are
314
+ `Exclusive` and `Traversable` out of the box; the world's default pipeline is itself a
315
+ `cascade: DependsOn` query (see [Execution model](./execution-model.md#the-pipeline-is-a-query)).
316
+
317
+ ## Network snapshots
318
+
319
+ Relationship subclasses listed in a server/client world's `networkComponents` are networkable.
320
+ Their payload writes a u32 target count, every target eid, then the relationship's ordinary wire
321
+ fields. Target ids are sorted by the server for deterministic snapshots; duplicate, zero, or empty
322
+ target lists are rejected, and an `Exclusive` relationship rejects a payload with more than one
323
+ target.
324
+
325
+ The client reconciles every received edge and applies ordinary fields. If a target entity has not
326
+ arrived, it creates a networked ghost for that eid immediately; each missing target in a many-target
327
+ payload gets its own ghost. List `ChildOf` or a custom relationship explicitly in
328
+ `networkComponents` when those edges must replicate.
202
329
 
203
330
  ## Reference
204
331
 
205
- | API | Description |
206
- | --------------------------------------------------- | ---------------------------------------------------------------------------------------- |
207
- | `class R extends Relationship {}` | Declare a relationship component with a `target: Entity` field. |
208
- | `e.set(R, { target })` | Create or retarget the relationship. |
209
- | `e.target(R)` | The target entity through relationship `R`, or `undefined`. |
210
- | `e.children(R)` | `ReadonlySet<Entity>` of sources targeting `e` through `R`. |
211
- | `e.childOf(parent)` | Shorthand for `e.set(ChildOf, { target: parent })`. |
212
- | `e.ensureTarget(R)` | Return `R`'s target, creating an independent entity when absent. |
213
- | `world.component(R).add(OwnedTarget)` | Make targets assigned through `R` into `ChildOf` children of their sources. |
214
- | `world.component(R).onRetarget(handler)` | React after `R` moves between existing targets; receives source, old target, new target. |
215
- | `e.parent` / `e.parent = p` | Getter/setter for the built-in `ChildOf` target; setting `undefined` removes `ChildOf`. |
216
- | `meta.onDeleteTarget` | `Remove` (default) or `Delete` cascade when a target is destroyed. |
217
- | `world.component(R).add(Traversable)` | Enable depth tracking for `cascade` ordering. |
218
- | `{ target: [R, q] }` / `{ parent: q }` | Reactive DSL operators through the relationship. |
219
- | `{ source: [R, q] }` / `{ children: q }` | Filter-only DSL operators down the reverse index. |
220
- | `{ target: [R, [C...]] }` / `{ down: [R, [C...]] }` | Injection markers; see [Systems](./systems.md#relationship-injection-target-and-down). |
332
+ | API | Description |
333
+ | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
334
+ | `class R extends Relationship {}` | Declare a many-target relationship with private target storage. |
335
+ | `e.add(R, target)` | Add one unique edge; atomically replace the edge when `R` is `Exclusive`. `target` accepts any `ComponentRef` resolved eagerly via `world.entity(target)`. |
336
+ | `e.remove(R, target)` / `e.remove(R)` | Remove one edge, or remove the relationship and all edges. `target` accepts any `ComponentRef` resolved eagerly via `world.entity(target)`. |
337
+ | `e.set(R, ordinaryProps)` | Update ordinary data on an existing relationship; never creates or changes edges. |
338
+ | `e.target(R)` | First target through `R`, or `undefined`. |
339
+ | `e.targets(R)` | Readonly snapshot of every target through `R`; ordering is unspecified. |
340
+ | `e.hasTarget(R, target)` | Test whether one edge exists. |
341
+ | `e.children(R)` | `ReadonlySet<Entity>` of sources targeting `e` through `R`. |
342
+ | `e.childOf(parent)` | Shorthand for `e.add(ChildOf, parent)`. |
343
+ | `e.parent` / `e.parent = p` | First built-in `ChildOf` target; assignment replaces it or removes `ChildOf` for `undefined`. |
344
+ | `world.component(R).add(Exclusive)` | Restrict `R` to one target. |
345
+ | `world.component(R).add(OneOf, parent)` | Reject targets of `R` that are not direct `ChildOf` children of `parent`. |
346
+ | `world.component(R).on("targetAdd", handler)` | Register a `(source, relationship, target)` edge-add handler. |
347
+ | `world.component(R).on("targetRemove", handler)` | Register a `(source, relationship, target)` edge-remove handler. |
348
+ | `world.component(R).add(OwnedTarget)` | Adopt and destroy explicit targets; requires `Exclusive`. |
349
+ | `meta.onDeleteTarget` | `Remove` (default) per edge or `Delete` the source when a target dies. |
350
+ | `world.component(R).add(Traversable)` | Enable depth tracking for cascade ordering; requires `Exclusive`. |
351
+ | `{ target: [R, q] }` / `{ parent: q }` | Reactive existential DSL operators through relationship targets. |
352
+ | `{ source: [R, q] }` / `{ children: q }` | Filter-only DSL operators down the reverse index. |
353
+ | `{ target: [R, projection] }` | Inject projections from one current target; selection is unspecified for non-exclusive relations. |
354
+ | `{ targets: [R, projection] }` | Inject one row-array slot covering all targets; see [Systems](./systems.md#relationship-injection). |
355
+ | `{ down: [R, [C...]] }` | Fan out over sources; see [Systems](./systems.md#relationship-injection). |