@vworlds/vecs 1.0.45 → 1.0.47

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 -60
  3. package/dist/component.js +24 -17
  4. package/dist/component.js.map +1 -1
  5. package/dist/component_meta.d.ts +67 -79
  6. package/dist/component_meta.js +331 -37
  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 -41
  13. package/dist/entity/entity.components.js +249 -82
  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 -50
  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 -6
  39. package/dist/modules/relationship_types.js +70 -6
  40. package/dist/modules/relationship_types.js.map +1 -1
  41. package/dist/modules/relationships.d.ts +31 -9
  42. package/dist/modules/relationships.js +120 -44
  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 +90 -53
  123. package/docs/concepts.md +21 -17
  124. package/docs/design-guide.md +125 -94
  125. package/docs/entities.md +62 -76
  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 +245 -124
  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
@@ -0,0 +1,181 @@
1
+ # Subtree snapshots
2
+
3
+ A subtree snapshot moves an entity forest between two vecs worlds: capture it in one world,
4
+ recreate it in another, or do both. It is the primitive behind a multi-sector host, where a ship
5
+ crosses a world boundary and its ECS state has to appear in the next sector's world.
6
+
7
+ Three operations, one matched set:
8
+
9
+ | Operation | Source world | Destination world |
10
+ | ----------------- | --------------- | ------------------- |
11
+ | `snapshotSubtree` | read-only | unchanged |
12
+ | `restoreSubtree` | unchanged | gets fresh entities |
13
+ | `moveSubtree` | roots destroyed | gets fresh entities |
14
+
15
+ ```ts
16
+ const result = world.moveSubtree(ship, nextSectorWorld, {
17
+ components: [Position, LinearVelocity, Hull],
18
+ relationships: [ChildOf, Carries],
19
+ externalTargets: "error",
20
+ });
21
+
22
+ result.roots; // destination root entities
23
+ result.entities.get(sourceEid); // destination entity for one snapshot key
24
+ result.sourceToDestination.get(sourceEntity); // the same, keyed by source entity
25
+ ```
26
+
27
+ ## What a snapshot holds
28
+
29
+ Nothing live. Entity ids, component ids, and relationship edges are all world-local, so a snapshot
30
+ records:
31
+
32
+ - the component **classes** the forest carried, plus each instance's captured **data**;
33
+ - each relationship's class, its non-edge **data**, and its **internal targets** as snapshot keys;
34
+ - how many targets were **left behind** (see external targets below).
35
+
36
+ The source's instances are never attached to the destination, and never mutated. Restore builds
37
+ every destination component through the same materializer `Entity.set` uses, so aliases,
38
+ exclusivity, `add` hooks, and query entry fire exactly once per component, the same as for an
39
+ ordinary `set`.
40
+
41
+ ## Selecting what crosses
42
+
43
+ ```ts
44
+ world.snapshotSubtree(root, {
45
+ traversal: ChildOf, // default; any registered relationship
46
+ components: [Position, Hull],
47
+ relationships: [ChildOf, Carries],
48
+ externalTargets: "omit", // default
49
+ });
50
+ ```
51
+
52
+ - **`traversal`** — the relationship that defines "subtree". `ChildOf` by default; a convoy might
53
+ use its own ordering relationship instead. The forest is every entity reachable through that
54
+ relationship, transitively.
55
+ - **`components` / `relationships`** — allowlists. Omit one to select every type in that category;
56
+ supply one to select exactly those. `ChildOf` is captured as the traversal relationship whatever
57
+ the `relationships` list says, since without it the result is not a subtree.
58
+ - **`externalTargets`** — what to do with a selected edge that points outside the forest.
59
+
60
+ A cycle in the traversal relationship, or an entity reachable from two roots in one call, is an
61
+ error: an ambiguous forest has no single correct capture.
62
+
63
+ ## External targets
64
+
65
+ An entity outside the forest is not captured, so an edge to it has nowhere to land.
66
+
67
+ - `"omit"` (default) — drop the edge, record how many targets were dropped.
68
+ - `"error"` — refuse to capture at all.
69
+
70
+ Use `"error"` when leaving data behind is a bug you want to hear about; use `"omit"` when the edge
71
+ is genuinely world-bound, like a tether to something the next sector does not own.
72
+
73
+ ## Snapshot policies
74
+
75
+ Per component type, capture follows the type's `OnSnapshot` declaration. Ordinary plain-data
76
+ components need no declaration at all:
77
+
78
+ ```ts
79
+ class Position {
80
+ x = 0;
81
+ y = 0;
82
+ }
83
+ ```
84
+
85
+ `Position` has no policy, so it uses **`ClonePublic`**: a fresh instance with the source instance's
86
+ own enumerable fields copied over. That is the same shallow copy `set` performs, and it is exactly
87
+ as aliasing-sensitive — a component holding a nested object shares it until something copies it.
88
+
89
+ ### `DontSnapshot`
90
+
91
+ For state that cannot exist in another world: engine handles, sockets, live queries and systems.
92
+
93
+ ```ts
94
+ world.component(PhysicsEngine).snapshotPolicy(DontSnapshot);
95
+ ```
96
+
97
+ A `DontSnapshot` type is skipped during capture. **Selecting** one explicitly is an error, so a
98
+ caller cannot silently lose state it asked for by name.
99
+
100
+ ### `CustomSnapshot`
101
+
102
+ For a component that mixes declarative state with runtime state. The owner supplies both halves.
103
+
104
+ ```ts
105
+ world.component(Body).snapshotPolicy(CustomSnapshot, {
106
+ // Keep the declarative fields, drop the engine handle.
107
+ capture: (body) => ({ type: body.type }),
108
+ // Build a destination-owned instance.
109
+ materialize: (data) => Object.assign(new Body(), data),
110
+ });
111
+ ```
112
+
113
+ `capture` receives the live source instance and returns detached data. `materialize` receives that
114
+ data and returns a freshly constructed instance belonging to no other entity. The captured value
115
+ never has to be JSON — whatever `capture` returns is handed to `materialize` verbatim — but it must
116
+ not reference the source world, its entities, or anything the source still holds.
117
+
118
+ A policy is declared by the module that owns the component type. It may be declared, changed, or
119
+ revoked at any time — including after entities already carry the component — because **capture binds
120
+ the materializer it resolved**. Each captured record stores the materialize function that was in
121
+ effect at capture time, so a policy declared afterwards cannot retroactively change how an existing
122
+ snapshot restores. The same binding makes a snapshot safe to restore much later, after the owning
123
+ module has moved on.
124
+
125
+ `@vworlds/vecs-physics` installs policies for its own components, which is why a physics subtree
126
+ moves without any physics-specific code in the caller: `PhysicsEngine` is `DontSnapshot`, while
127
+ `Body`, shapes, and joint types capture their declarative fields and let the destination lifecycle
128
+ systems allocate fresh Box2D handles. Their custom policies deep-copy only primitives, arrays, and
129
+ plain `Object`/null-prototype records; a Box2D `b2Vec2` wrapper or other class instance fails at
130
+ capture with its component and field path. `JointFrame`, `JointSettings`, and joint feature
131
+ components use the default `ClonePublic` policy.
132
+
133
+ ## Restore order
134
+
135
+ Restore is ordered, and the order is observable:
136
+
137
+ 1. **Preflight** every captured class against the destination. A class that is not registered there,
138
+ or that the destination declares `DontSnapshot`, fails here — before any entity exists.
139
+ 2. **Allocate** one destination entity per record, so every edge target exists before any edge is
140
+ added.
141
+ 3. **Materialize** plain-data components.
142
+ 4. **Rebuild edges**, traversal relationship first, then everything else. Hierarchy-sensitive
143
+ policies such as `OneOf` and `Traversable` therefore see a complete tree.
144
+ 5. **Assign** each relationship's non-edge data, in capture order.
145
+
146
+ If any step throws, everything the attempt created is destroyed and the error propagates, so a
147
+ failed restore leaves the destination as it was found.
148
+
149
+ ## Move and rollback
150
+
151
+ `moveSubtree` captures and preflights **before** it destroys anything, so a snapshot that cannot be
152
+ restored leaves the source world untouched.
153
+
154
+ Once the source is destroyed and the destination restore fails, everything the restore created is
155
+ destroyed, the source snapshot is restored back into the source world, and the original error is
156
+ rethrown. The rollback rebuilds equivalent state with freshly allocated ids, so `Entity` references
157
+ held before a failed move are stale afterwards.
158
+
159
+ Destroying a root applies its component `remove` hooks and its relationship cleanup policies, which
160
+ can reach outside the captured forest. That is what "the source is gone" means; select transferable
161
+ data accordingly.
162
+
163
+ ## Deferred worlds
164
+
165
+ All three operations throw while either world is deferred. Cross-world scheduling is the
166
+ application's job — a transfer that interleaved with a half-applied frame of mutations would have no
167
+ well-defined outcome. Call them between frames.
168
+
169
+ ## Inheritance
170
+
171
+ `IsA` edges are never captured. An inheritance edge and the values it materialized would both be
172
+ captured, and the destination would end up holding two disagreeing answers. Move an inheriting
173
+ entity by selecting its effective values instead. (The inheritance work that introduces `IsA` is
174
+ not in vecs yet; when it lands, it must keep declaring `IsA` as `DontSnapshot`.)
175
+
176
+ ## What this is not
177
+
178
+ - **Not serialization.** A snapshot is an in-memory value, not a wire format, and not stable across
179
+ vecs versions. `@vworlds/vecs-wire` and `@vworlds/vecs-protocol` cover the network case.
180
+ - **Not a cross-world transaction.** Two live worlds are not locked together. Capture-then-restore
181
+ cannot make two worlds atomically consistent; it makes the _transfer_ recoverable.
package/docs/systems.md CHANGED
@@ -40,7 +40,7 @@ the full query DSL (operator semantics in
40
40
  .with({ all: [Position, { any: [Sprite, Container] }] }) // compound DSL
41
41
  .with({ not: Invisible })
42
42
  .without(Invisible) // shorthand for .with({ not: Invisible })
43
- .with({ target: [FriendOf, Position] }) // relationship target has Position
43
+ .with({ target: [FriendOf, Position] }) // any relationship target has Position
44
44
  .with({ parent: Body }) // ChildOf target has Body
45
45
  ```
46
46
 
@@ -89,6 +89,14 @@ Fires once when an entity starts matching the system:
89
89
  .enter({ inject: [Position, Sprite] }, (e, [pos, sprite]) => sprite.setPosition(pos.x, pos.y))
90
90
  ```
91
91
 
92
+ The callback runs only if the entity is still a member when this system drains its inbox. If it
93
+ left or was destroyed first, vecs drops the queued `enter` and its paired `exit`; do not add a
94
+ `destroyed` guard to normal `enter` setup code. If it leaves and re-enters before draining, the
95
+ callback receives the ordered lifecycle transitions.
96
+
97
+ For systems, injected values and `Iter.src` are captured when entry is routed and replayed from that
98
+ snapshot when the system next drains its inbox.
99
+
92
100
  ### `.update(ComponentClass, callback)` / `.update(options, callback)`
93
101
 
94
102
  Fires when the watched component is set or marked modified on a tracked entity.
@@ -121,9 +129,10 @@ registration throws instead of replacing the existing callback.
121
129
  ### `.exit(callback)` / `.exit(inject, callback)` / `.exit(options, callback)`
122
130
 
123
131
  Fires when an entity stops matching (component removed, or entity destroyed). Directly injected
124
- components are read from a **snapshot captured at exit time**, so they are still resolvable even
125
- though they are being removed; `target`-injected components resolve live at callback time and may
126
- be `undefined`:
132
+ components and relationship projections are read from a **snapshot captured at exit time**, so
133
+ the same component and entity references remain available even if storage or edges change before
134
+ the system drains its inbox. Component objects are not cloned; later in-place changes to the same
135
+ object remain visible:
127
136
 
128
137
  ```ts
129
138
  .exit([Sprite], (e, [sprite]) => sprite.destroy());
@@ -185,23 +194,55 @@ anything else resolves as `T | undefined` — guard it. (`enter`/`exit` injectio
185
194
  direct components to be present and throws otherwise; list only components your predicate
186
195
  guarantees.)
187
196
 
188
- ### Relationship injection: `target` and `down`
197
+ ### Relationship injection
189
198
 
190
- `{ target: [Rel, [C1, C2]] }` follows the entity's relationship to its target and flattens the
191
- target's components into the tuple. Target-side slots are `undefined` when the relationship or the
192
- component is absent:
199
+ `{ target: [Rel, projection] }` follows the entity's **first** relationship target and flattens the
200
+ projection into the callback tuple. Use a component class to inject one target-side component, use
201
+ `Entity` to inject the target itself, or use a tuple to inject several values in order:
193
202
 
194
203
  ```ts
195
- .forEach([Body, { target: [FriendOf, [Position, Velocity]] }], (e, [body, pos, vel]) => {
196
- // body is from e; pos and vel are from e.target(FriendOf).
204
+ .enter([{ target: [FriendOf, Entity] }], (source, [friend]) => {
205
+ // friend is source.target(FriendOf), one current target, or undefined.
206
+ });
207
+
208
+ .forEach([Body, { target: [FriendOf, Position] }], (source, [body, pos]) => {
209
+ // body is from source; pos is from source.target(FriendOf).
197
210
  });
211
+
212
+ .forEach([{ target: [FriendOf, [Entity, Position, Velocity]] }], (source, [friend, pos, vel]) => {
213
+ // friend, pos, and vel all project from the same FriendOf target.
214
+ });
215
+ ```
216
+
217
+ Every target projection is optional. If the source lacks `Rel`, every projected slot is
218
+ `undefined`. If the relationship exists, an `Entity` slot is the target while each component slot
219
+ is `undefined` when that target lacks the component.
220
+
221
+ Use `{ targets: [Rel, projection] }` to project every target. Unlike singular injection, the whole
222
+ term occupies **one callback tuple slot** containing a readonly array of rows. Each target produces
223
+ one row; row ordering follows the current target snapshot but is not a public ordering contract. The
224
+ row contains one value for a scalar projection or several values for a tuple projection:
225
+
226
+ ```ts
227
+ .forEach(
228
+ [{ targets: [FriendOf, [Entity, Position, Velocity]] }],
229
+ (source, [friends]) => {
230
+ // friends: ReadonlyArray<readonly [Entity, Position | undefined, Velocity | undefined]>
231
+ for (const [friend, position, velocity] of friends) { ... }
232
+ }
233
+ );
198
234
  ```
199
235
 
200
- With two or more components in the inner tuple, TypeScript widens the slot types to a union;
201
- mark the inner tuple `as const` (`{ target: [FriendOf, [Position, Velocity] as const] }`) to get
202
- precise per-slot types. Single-component tuples infer precisely as-is.
236
+ An absent relationship injects an empty array. The `Entity` value in each plural row is always
237
+ present; projected components remain optional. `target` and `targets` resolve live for `each`,
238
+ `forEach`, `update`, and `orderBy`. For deferred system `enter` and `exit` callbacks, vecs captures
239
+ their projected component and entity references plus sources when the event is routed. Inbox replay
240
+ retains those references even if storage or edges change; component objects are not cloned.
241
+
242
+ Inline projection tuples infer precise slot types. When reusing a projection tuple stored in a
243
+ variable, declare it `as const` to preserve each position's type.
203
244
 
204
- `{ down: [Rel, [C1]] }` fans out: the callback fires once per child targeting the visited entity
245
+ `{ down: [Rel, [C1]] }` fans out: the callback fires once per source targeting the visited entity
205
246
  through `Rel`, with that child's components filling the slots (nullable). `down` is allowed only
206
247
  in `each` and `forEach`, at most once per tuple, and entities with zero children produce no calls.
207
248
  `down` iterates **all** children through the relationship, independent of any membership filter:
@@ -231,12 +272,14 @@ system.each(
231
272
  );
232
273
  ```
233
274
 
234
- `it.src` holds the source entity per tuple slot: the visited entity for direct components, the
235
- relationship target for `target` slots, the specific child for `down` slots. When `Iter` is not
236
- requested, the non-cursor path runs with zero per-entity overhead — dispatch happens once at setup
237
- time. `each`/`forEach`/`update`/`orderBy` reuse cursor and tuple instances across the pass, so
238
- snapshot what you need before triggering further mutations from inside a callback. Passing
239
- `{ cursor: Entity, ... }` is also accepted and means the default entity-first signature.
275
+ `it.src` holds the source per tuple slot: the visited entity for direct components, the first
276
+ target for every flattened `target` slot, the readonly target-array snapshot for the single
277
+ `targets` slot, and the specific child for `down` slots. Event-time `enter` / `exit` snapshots also
278
+ capture `Iter.src`. When `Iter` is not requested, the non-cursor path has no cursor allocation;
279
+ dispatch happens once at setup time. `each`/`forEach`/`update`/`orderBy` reuse cursor and
280
+ tuple instances across the pass, so snapshot what you need before triggering further mutations
281
+ from inside a callback. Passing `{ cursor: Entity, ... }` is also accepted and means the default
282
+ entity-first signature.
240
283
 
241
284
  ## Ordering and tracking
242
285
 
@@ -321,7 +364,7 @@ While disabled, query membership is still maintained, so the tracked set is corr
321
364
  | `phase(anchor)` | Place after a phase anchor (name or entity). Default `ON_UPDATE`. |
322
365
  | `enter(cb)` / `enter(inject, cb)` | Fires once when an entity starts matching. |
323
366
  | `update(C, cb)` / `update(options, cb)` | Fires on `set`/`modified` of `C` on a member; `options.onEnter: true` also fires on entry for present watched components. |
324
- | `exit(cb)` / `exit(inject, cb)` | Fires when an entity stops matching; direct injection reads an exit-time snapshot. |
367
+ | `exit(cb)` / `exit(inject, cb)` | Fires when an entity stops matching; injection retains references captured at exit time. |
325
368
  | `each([C...], cb)` | Fires per tracked entity every tick. Implies `track`. One per system. |
326
369
  | `run(cb)` | Fires once per tick, `(now, delta)`. |
327
370
  | `interval(seconds)` / `rate(n[, source])` / `tickSource(source)` | Cadence control. |
package/docs/utilities.md CHANGED
@@ -70,12 +70,16 @@ map.size; // 0
70
70
 
71
71
  `IterableArrayMap` (`lib/vecs/src/util/array_map.ts`) is an iterable `Map<number, T>` substitute
72
72
  backed by dense entries plus sparse indexes. Use it when callers need fast `forEach` or `values`
73
- over present entries. Random access resolves `key → dense index` through an internal `ArrayMap`,
74
- then indexes into a dense items array; iteration walks the dense items array directly.
75
-
76
- Deletes keep the items array dense by moving the last value into the removed slot, so iteration
77
- is always a tight loop without sparse holes. Compared with `ArrayMap`, this costs extra memory
78
- for tuple storage and an index map.
73
+ over present entries. Random access reads the entry's dense position from a slot array indexed by
74
+ key, which holds only small integers so V8 keeps it on its fast path, then reads the value from a
75
+ dense value array. Iteration walks the dense key and value arrays directly.
76
+
77
+ Deletes keep the dense arrays dense by moving the last entry into the removed position, so
78
+ iteration is always a tight loop without sparse holes. Compared with `ArrayMap`, this costs extra
79
+ memory for the slot array and the dense key and value arrays. Because the dense arrays grow by
80
+ `push`, V8 cannot pretenure the entries, so a fresh process that creates tens of thousands of
81
+ long-lived entities in bulk and then destroys them can run slower than with per-entry
82
+ `[key, value]` tuples. The repository's `bench/README.md` has the measurements.
79
83
 
80
84
  ```ts
81
85
  import { IterableArrayMap } from "@vworlds/vecs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vworlds/vecs",
3
- "version": "1.0.45",
3
+ "version": "1.0.47",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -15,7 +15,7 @@
15
15
  "prepack": "npm run build",
16
16
  "build": "npm run clean && npm run compile",
17
17
  "compile": "tsc -p tsconfig.build.json && cp \"./package.json\" ./dist/",
18
- "bench:ecs": "NODE_OPTIONS=--max-old-space-size=32768 RUN_VECS_ECS_BENCH=1 vitest run tests/ecs.bench.test.ts --pool=forks --poolOptions.forks.singleFork",
18
+ "bench:ecs": "NODE_OPTIONS=--max-old-space-size=32768 RUN_VECS_ECS_BENCH=1 vitest run tests/ecs.bench.test.ts --pool=forks --maxWorkers=1 --reporter=default",
19
19
  "test": "vitest run",
20
20
  "test:watch": "vitest",
21
21
  "lint": "eslint \"src/**/*.ts\" \"tests/**/*.ts\" --no-error-on-unmatched-pattern",
@@ -41,8 +41,9 @@ Before implementing, identify:
41
41
  - Which component transition creates, updates, or removes related state or resources.
42
42
  - Whether the work should react through `enter`, `update`, or `exit` rather than scan every frame.
43
43
  - Which phase owns the work and what earlier or later phase it must order against.
44
- - Whether an owned target, relationship, trait, grouped query, or attached resource expresses the
45
- lifecycle better than imperative bookkeeping.
44
+ - Whether a multi-target relationship, an `Exclusive` relationship, an explicitly created owned
45
+ target, a grouped query, or an attached resource expresses the lifecycle better than imperative
46
+ bookkeeping.
46
47
  - Whether the feature belongs in a `Module` that owns registration, systems, phases, and policy.
47
48
 
48
49
  Prefer the idioms in the installed design guide over generic ECS patterns. vecs execution details,
@@ -72,8 +73,8 @@ the documented pattern unless the user explicitly chooses otherwise.
72
73
  hooks as directed by the guide.
73
74
  3. Express cross-feature ordering with phases. Treat same-phase registration order as a local
74
75
  implementation detail unless the installed docs explicitly make it part of the design.
75
- 4. Test lifecycle transitions, not only steady-state values: entry, mutation, removal, retargeting,
76
- destruction, and same-tick visibility where relevant.
76
+ 4. Test lifecycle transitions, not only steady-state values: entry, mutation, edge addition and
77
+ removal, exclusive edge replacement, destruction, and same-tick visibility where relevant.
77
78
  5. Run the consuming project's typecheck and focused tests. Resolve API uncertainty from the
78
79
  installed declarations and docs rather than guessing.
79
80