@vworlds/vecs 1.0.48 → 1.0.50

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.
@@ -52,12 +52,14 @@ export declare class OnSnapshot extends Relationship {
52
52
  }
53
53
  /**
54
54
  * Default snapshot policy: construct a fresh instance of the component class
55
- * and clone the source instance's own enumerable fields onto it.
55
+ * and deep-copy the source instance's own enumerable fields onto it.
56
56
  *
57
- * Shallow, and therefore exactly as aliasing-sensitive as `set`: a component
58
- * holding a nested object shares it with the source until something copies it.
59
- * A type that must not carry runtime handles across worlds needs its own
60
- * policy — see {@link CustomSnapshot}.
57
+ * The copy covers primitives, arrays, plain objects, `Map`, `Set`, `Date`,
58
+ * `ArrayBuffer` and typed arrays, so the destination never aliases a nested
59
+ * value of the source. Anything else (a class instance, a function, a symbol)
60
+ * throws at capture with the component and field path. A type that holds such
61
+ * a value, or that must not carry runtime handles across worlds, needs its own
62
+ * policy — see {@link CustomSnapshot} and {@link DontSnapshot}.
61
63
  */
62
64
  export declare class ClonePublic {
63
65
  }
@@ -48,12 +48,14 @@ export class OnSnapshot extends Relationship {
48
48
  }
49
49
  /**
50
50
  * Default snapshot policy: construct a fresh instance of the component class
51
- * and clone the source instance's own enumerable fields onto it.
51
+ * and deep-copy the source instance's own enumerable fields onto it.
52
52
  *
53
- * Shallow, and therefore exactly as aliasing-sensitive as `set`: a component
54
- * holding a nested object shares it with the source until something copies it.
55
- * A type that must not carry runtime handles across worlds needs its own
56
- * policy — see {@link CustomSnapshot}.
53
+ * The copy covers primitives, arrays, plain objects, `Map`, `Set`, `Date`,
54
+ * `ArrayBuffer` and typed arrays, so the destination never aliases a nested
55
+ * value of the source. Anything else (a class instance, a function, a symbol)
56
+ * throws at capture with the component and field path. A type that holds such
57
+ * a value, or that must not carry runtime handles across worlds, needs its own
58
+ * policy — see {@link CustomSnapshot} and {@link DontSnapshot}.
57
59
  */
58
60
  export class ClonePublic {
59
61
  }
@@ -1 +1 @@
1
- {"version":3,"file":"snapshot_types.js","sourceRoot":"","sources":["../../src/modules/snapshot_types.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,uBAAuB;AACvB,EAAE;AACF,iFAAiF;AACjF,4EAA4E;AAC5E,iFAAiF;AACjF,0EAA0E;AAC1E,4BAA4B;AAG5B,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAoBpD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,OAAO,UAAW,SAAQ,YAAY;IAA5C;;QACE;;;WAGG;QACI,YAAO,GAAgC,SAAS,CAAC;QAExD;;;WAGG;QACI,gBAAW,GAAoC,SAAS,CAAC;IAClE,CAAC;CAAA;AAED;;;;;;;;GAQG;AACH,MAAM,OAAO,WAAW;CAAG;AAE3B;;;;;;;;GAQG;AACH,MAAM,OAAO,YAAY;CAAG;AAE5B;;;;;;GAMG;AACH,MAAM,OAAO,cAAc;CAAG;AAkB9B,+EAA+E;AAC/E,MAAM,CAAC,MAAM,YAAY,GAA2B,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AAEtE,6EAA6E;AAC7E,MAAM,CAAC,MAAM,WAAW,GAA2B,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAEpE;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CACpC,SAAiB,EACjB,MAA0B,EAC1B,OAAyB,EACzB,WAAiC;IAEjC,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC;IAC9B,MAAM,MAAM,GAAG,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IACvC,SAAS,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IAClC,SAAS,CAAC,GAAG,CAAC,UAAU,EAAE;QACxB,OAAO,EAAE,MAAM,KAAK,cAAc,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS;QACxD,WAAW,EAAE,MAAM,KAAK,cAAc,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS;KACjE,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,sBAAsB,CAAC,KAAY,EAAE,IAAmB;IACtE,MAAM,UAAU,GAAG,KAAK,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;IAC/C,MAAM,WAAW,GAAG,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,UAAU,CAAC,GAAG,CAA2B,CAAC;IAC7F,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO,YAAY,CAAC;IACtB,CAAC;IACD,MAAM,MAAM,GAAG,WAAW,CAAC,OAAO,EAAE,CAAC;IACrC,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,CAAC,GAAG,KAAK,KAAK,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC,GAAG,EAAE,CAAC;QAC5E,OAAO,YAAY,CAAC;IACtB,CAAC;IACD,IAAI,MAAM,CAAC,GAAG,KAAK,KAAK,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC,GAAG,EAAE,CAAC;QACvD,OAAO,WAAW,CAAC;IACrB,CAAC;IACD,IAAI,WAAW,CAAC,OAAO,KAAK,SAAS,IAAI,WAAW,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;QAC/E,MAAM,IAAI,KAAK,CACb,yBAAyB,IAAI,CAAC,KAAK,CAAC,IAAI,mEAAmE,CAC5G,CAAC;IACJ,CAAC;IACD,OAAO;QACL,IAAI,EAAE,QAAQ;QACd,OAAO,EAAE,WAAW,CAAC,OAAO;QAC5B,WAAW,EAAE,WAAW,CAAC,WAAW;KACrC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"snapshot_types.js","sourceRoot":"","sources":["../../src/modules/snapshot_types.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,uBAAuB;AACvB,EAAE;AACF,iFAAiF;AACjF,4EAA4E;AAC5E,iFAAiF;AACjF,0EAA0E;AAC1E,4BAA4B;AAG5B,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAoBpD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,OAAO,UAAW,SAAQ,YAAY;IAA5C;;QACE;;;WAGG;QACI,YAAO,GAAgC,SAAS,CAAC;QAExD;;;WAGG;QACI,gBAAW,GAAoC,SAAS,CAAC;IAClE,CAAC;CAAA;AAED;;;;;;;;;;GAUG;AACH,MAAM,OAAO,WAAW;CAAG;AAE3B;;;;;;;;GAQG;AACH,MAAM,OAAO,YAAY;CAAG;AAE5B;;;;;;GAMG;AACH,MAAM,OAAO,cAAc;CAAG;AAkB9B,+EAA+E;AAC/E,MAAM,CAAC,MAAM,YAAY,GAA2B,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AAEtE,6EAA6E;AAC7E,MAAM,CAAC,MAAM,WAAW,GAA2B,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAEpE;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CACpC,SAAiB,EACjB,MAA0B,EAC1B,OAAyB,EACzB,WAAiC;IAEjC,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC;IAC9B,MAAM,MAAM,GAAG,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IACvC,SAAS,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IAClC,SAAS,CAAC,GAAG,CAAC,UAAU,EAAE;QACxB,OAAO,EAAE,MAAM,KAAK,cAAc,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS;QACxD,WAAW,EAAE,MAAM,KAAK,cAAc,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS;KACjE,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,sBAAsB,CAAC,KAAY,EAAE,IAAmB;IACtE,MAAM,UAAU,GAAG,KAAK,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;IAC/C,MAAM,WAAW,GAAG,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,UAAU,CAAC,GAAG,CAA2B,CAAC;IAC7F,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO,YAAY,CAAC;IACtB,CAAC;IACD,MAAM,MAAM,GAAG,WAAW,CAAC,OAAO,EAAE,CAAC;IACrC,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,CAAC,GAAG,KAAK,KAAK,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC,GAAG,EAAE,CAAC;QAC5E,OAAO,YAAY,CAAC;IACtB,CAAC;IACD,IAAI,MAAM,CAAC,GAAG,KAAK,KAAK,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC,GAAG,EAAE,CAAC;QACvD,OAAO,WAAW,CAAC;IACrB,CAAC;IACD,IAAI,WAAW,CAAC,OAAO,KAAK,SAAS,IAAI,WAAW,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;QAC/E,MAAM,IAAI,KAAK,CACb,yBAAyB,IAAI,CAAC,KAAK,CAAC,IAAI,mEAAmE,CAC5G,CAAC;IACJ,CAAC;IACD,OAAO;QACL,IAAI,EAAE,QAAQ;QACd,OAAO,EAAE,WAAW,CAAC,OAAO;QAC5B,WAAW,EAAE,WAAW,CAAC,WAAW;KACrC,CAAC;AACJ,CAAC"}
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vworlds/vecs",
3
- "version": "1.0.48",
3
+ "version": "1.0.50",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -11,6 +11,8 @@ import { type WorldOptions } from "./world.base.js";
11
11
  * 2. **Build the pipeline** — attach systems to phase entities with
12
12
  * `DependsOn`, or call {@link setPipeline} with a custom query spec.
13
13
  * 3. **Run loop** — call {@link progress} once per frame.
14
+ * 4. **Dispose** — call {@link dispose} when the world is finished, so the
15
+ * `remove` hooks of the components its entities carry release what they own.
14
16
  *
15
17
  * ## Deferred mode
16
18
  *
@@ -0,0 +1,69 @@
1
+ import { Transfers } from "./world.transfers.js";
2
+ import type { World } from "./world.js";
3
+ /**
4
+ * World teardown. It is a layer of its own rather than a method of `World`, because the layers
5
+ * below pass `this` to parameters typed `World`, which only holds while `World` adds no members
6
+ * to `Pipeline`.
7
+ */
8
+ export declare class Disposal extends Transfers {
9
+ /** `true` once {@link dispose} has started: what makes it idempotent and safe to re-enter. */
10
+ private _disposeStarted;
11
+ /**
12
+ * End the world's life: destroy every entity the application and its modules created, so the
13
+ * `remove` hooks of the components they carry release what those components own (render
14
+ * objects, handles, timers). Call it when the world is finished — a scene or route change, a
15
+ * hot reload — and drop the reference afterwards.
16
+ *
17
+ * Each entity goes through the ordinary destroy path, as if it had received
18
+ * {@link Entity.destroy}: every component's `remove` hook fires exactly once, cleanup policies
19
+ * and relationship cascades (`ChildOf`, `OwnedTarget`, `CleanupPolicy.Delete`) run as they
20
+ * always do, and an entity a cascade or a hook has already destroyed is skipped, never
21
+ * destroyed twice.
22
+ *
23
+ * ## What stays
24
+ *
25
+ * The infrastructure the world needs to stay coherent is not destroyed:
26
+ *
27
+ * - component and module entities (every {@link Component}),
28
+ * - the entities behind queries and systems (they carry the {@link Query} component), and
29
+ * - phase entities (they carry the {@link Phase} component).
30
+ *
31
+ * What those entities hold stays too: data stored on a component entity (a singleton set with
32
+ * {@link set}) and any entity they still carry as a component key, which cannot be destroyed
33
+ * while it is in use. Release such data yourself. {@link clearAllEntities} is the full wipe,
34
+ * infrastructure included.
35
+ *
36
+ * ## Order
37
+ *
38
+ * No order is specified among unrelated entities, so do not make a hook depend on one. The one
39
+ * guarantee is that an entity used as a component key is destroyed after every entity that
40
+ * carries it, because destroying it earlier would throw under the default cleanup policy.
41
+ *
42
+ * ## Pending commands
43
+ *
44
+ * Commands queued by an enclosing deferred scope are neither flushed early nor discarded: they
45
+ * run first, in order, and the destruction follows them in the same queue, so a hook still fires
46
+ * once per component instance however it was removed. An entity whose creation is still queued
47
+ * is created, then destroyed. At top level nothing is queued and `dispose()` returns with the
48
+ * application's entities gone. Called from a system, a hook or a {@link defer} block, it queues
49
+ * the destruction like any `destroy()` in that situation, and the destruction completes when
50
+ * the queue next drains: right after the running system, or at the end of the `defer` block.
51
+ *
52
+ * ## After disposal
53
+ *
54
+ * A disposed world must not be used again. Nothing enforces it — no other method checks, so
55
+ * `entity()` and `progress()` stay as cheap as ever — but the world has no application state
56
+ * left to run on. Calling `dispose()` again, from a hook included, does nothing. A hook that
57
+ * creates entities while the world is being disposed leaves them behind. A system's `exit`
58
+ * callback only queues its event, and a disposed world never runs the system again, so release
59
+ * resources in `remove` hooks, which fire during the call. A hook that throws propagates out of
60
+ * the call and leaves the world partial, as it does from any destroy: treat it as a programming
61
+ * error.
62
+ */
63
+ dispose(this: World): void;
64
+ /**
65
+ * The entities {@link dispose} destroys, in an order that lets every destroy succeed: those the
66
+ * application created, with each one used as a component key after its carriers.
67
+ */
68
+ private _destroyOrder;
69
+ }
@@ -0,0 +1,146 @@
1
+ import { Component } from "../component.js";
2
+ import { Phase } from "../modules/pipeline.js";
3
+ import { Query } from "../query/index.js";
4
+ import { Transfers } from "./world.transfers.js";
5
+ /**
6
+ * World teardown. It is a layer of its own rather than a method of `World`, because the layers
7
+ * below pass `this` to parameters typed `World`, which only holds while `World` adds no members
8
+ * to `Pipeline`.
9
+ */
10
+ export class Disposal extends Transfers {
11
+ constructor() {
12
+ super(...arguments);
13
+ /** `true` once {@link dispose} has started: what makes it idempotent and safe to re-enter. */
14
+ this._disposeStarted = false;
15
+ }
16
+ /**
17
+ * End the world's life: destroy every entity the application and its modules created, so the
18
+ * `remove` hooks of the components they carry release what those components own (render
19
+ * objects, handles, timers). Call it when the world is finished — a scene or route change, a
20
+ * hot reload — and drop the reference afterwards.
21
+ *
22
+ * Each entity goes through the ordinary destroy path, as if it had received
23
+ * {@link Entity.destroy}: every component's `remove` hook fires exactly once, cleanup policies
24
+ * and relationship cascades (`ChildOf`, `OwnedTarget`, `CleanupPolicy.Delete`) run as they
25
+ * always do, and an entity a cascade or a hook has already destroyed is skipped, never
26
+ * destroyed twice.
27
+ *
28
+ * ## What stays
29
+ *
30
+ * The infrastructure the world needs to stay coherent is not destroyed:
31
+ *
32
+ * - component and module entities (every {@link Component}),
33
+ * - the entities behind queries and systems (they carry the {@link Query} component), and
34
+ * - phase entities (they carry the {@link Phase} component).
35
+ *
36
+ * What those entities hold stays too: data stored on a component entity (a singleton set with
37
+ * {@link set}) and any entity they still carry as a component key, which cannot be destroyed
38
+ * while it is in use. Release such data yourself. {@link clearAllEntities} is the full wipe,
39
+ * infrastructure included.
40
+ *
41
+ * ## Order
42
+ *
43
+ * No order is specified among unrelated entities, so do not make a hook depend on one. The one
44
+ * guarantee is that an entity used as a component key is destroyed after every entity that
45
+ * carries it, because destroying it earlier would throw under the default cleanup policy.
46
+ *
47
+ * ## Pending commands
48
+ *
49
+ * Commands queued by an enclosing deferred scope are neither flushed early nor discarded: they
50
+ * run first, in order, and the destruction follows them in the same queue, so a hook still fires
51
+ * once per component instance however it was removed. An entity whose creation is still queued
52
+ * is created, then destroyed. At top level nothing is queued and `dispose()` returns with the
53
+ * application's entities gone. Called from a system, a hook or a {@link defer} block, it queues
54
+ * the destruction like any `destroy()` in that situation, and the destruction completes when
55
+ * the queue next drains: right after the running system, or at the end of the `defer` block.
56
+ *
57
+ * ## After disposal
58
+ *
59
+ * A disposed world must not be used again. Nothing enforces it — no other method checks, so
60
+ * `entity()` and `progress()` stay as cheap as ever — but the world has no application state
61
+ * left to run on. Calling `dispose()` again, from a hook included, does nothing. A hook that
62
+ * creates entities while the world is being disposed leaves them behind. A system's `exit`
63
+ * callback only queues its event, and a disposed world never runs the system again, so release
64
+ * resources in `remove` hooks, which fire during the call. A hook that throws propagates out of
65
+ * the call and leaves the world partial, as it does from any destroy: treat it as a programming
66
+ * error.
67
+ */
68
+ dispose() {
69
+ if (this._disposeStarted) {
70
+ return;
71
+ }
72
+ this._disposeStarted = true;
73
+ // The destroys are queued commands, as `Entity.destroy()` makes them in a deferred scope, so a
74
+ // queue that is already filling runs ahead of them. At top level this scope drains them now.
75
+ this.defer(() => {
76
+ for (const entity of this._destroyOrder()) {
77
+ this._enqueue({ kind: 4 /* CommandKind.Destroy */, entity });
78
+ }
79
+ });
80
+ }
81
+ /**
82
+ * The entities {@link dispose} destroys, in an order that lets every destroy succeed: those the
83
+ * application created, with each one used as a component key after its carriers.
84
+ */
85
+ _destroyOrder() {
86
+ // `getEntity` rather than `entity`: `clearAllEntities()` unregisters these components.
87
+ const queryEid = this.getEntity(Query)?.eid;
88
+ const phaseEid = this.getEntity(Phase)?.eid;
89
+ const isInfrastructure = (entity) => entity instanceof Component ||
90
+ (queryEid !== undefined && entity.has(queryEid)) ||
91
+ (phaseEid !== undefined && entity.has(phaseEid));
92
+ const entities = this._entities.values();
93
+ // In a deferred scope an entity created earlier is only queued, not registered yet. Its
94
+ // creation runs ahead of the destroy that is queued after it.
95
+ for (const command of this._commandQueue) {
96
+ if (command.kind === 0 /* CommandKind.CreateEntity */ &&
97
+ !command.entity.destroyed &&
98
+ this._entities.get(command.entity.eid) !== command.entity) {
99
+ entities.push(command.entity);
100
+ }
101
+ }
102
+ return carriersFirst(entities.filter((entity) => !isInfrastructure(entity)), isInfrastructure);
103
+ }
104
+ }
105
+ /**
106
+ * Order `entities` so that an entity used as a component key follows every entity that carries it.
107
+ *
108
+ * A key destroyed while still carried throws under the default cleanup policy, so carriers go
109
+ * first. A key that something outside `entities` carries, which is infrastructure in practice, or
110
+ * that is caught in a cycle, cannot be ordered and is left out: destroying it would throw or strip
111
+ * the infrastructure.
112
+ */
113
+ function carriersFirst(entities, isInfrastructure) {
114
+ const ordered = [];
115
+ const keys = new Set();
116
+ for (const entity of entities) {
117
+ if (entity.instanceCount() === 0) {
118
+ ordered.push(entity);
119
+ }
120
+ else {
121
+ keys.add(entity);
122
+ }
123
+ }
124
+ let progressed = keys.size > 0;
125
+ while (progressed) {
126
+ progressed = false;
127
+ for (const key of keys) {
128
+ if (!isCarried(key, keys, isInfrastructure)) {
129
+ ordered.push(key);
130
+ keys.delete(key);
131
+ progressed = true;
132
+ }
133
+ }
134
+ }
135
+ return ordered;
136
+ }
137
+ /** Whether `key` is still carried by another key that waits to be ordered, or by infrastructure. */
138
+ function isCarried(key, waiting, isInfrastructure) {
139
+ for (const carrier of key.meta._carriers ?? []) {
140
+ if (carrier !== key && (waiting.has(carrier) || isInfrastructure(carrier))) {
141
+ return true;
142
+ }
143
+ }
144
+ return false;
145
+ }
146
+ //# sourceMappingURL=world.disposal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"world.disposal.js","sourceRoot":"","sources":["../../src/world/world.disposal.ts"],"names":[],"mappings":"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"}
@@ -19,6 +19,8 @@ import { Pipeline } from "./world.pipeline.js";
19
19
  * 2. **Build the pipeline** — attach systems to phase entities with
20
20
  * `DependsOn`, or call {@link setPipeline} with a custom query spec.
21
21
  * 3. **Run loop** — call {@link progress} once per frame.
22
+ * 4. **Dispose** — call {@link dispose} when the world is finished, so the
23
+ * `remove` hooks of the components its entities carry release what they own.
22
24
  *
23
25
  * ## Deferred mode
24
26
  *
@@ -1 +1 @@
1
- {"version":3,"file":"world.js","sourceRoot":"","sources":["../../src/world/world.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAG/C;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,OAAO,KAAM,SAAQ,QAAQ;IACjC,YAAmB,UAAwB,EAAE;QAC3C,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,aAAa,EAAE,CAAC,CAAC;QAC3F,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QACxE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;QAC3E;YACE,cAAc;YACd,gBAAgB;YAChB,eAAe;YACf,mBAAmB;YACnB,2EAA2E;YAC3E,sEAAsE;YACtE,qDAAqD;YACrD,cAAc;YACd,cAAc;SACf,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IACvC,CAAC;CACF"}
1
+ {"version":3,"file":"world.js","sourceRoot":"","sources":["../../src/world/world.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAG/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,OAAO,KAAM,SAAQ,QAAQ;IACjC,YAAmB,UAAwB,EAAE;QAC3C,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,aAAa,EAAE,CAAC,CAAC;QAC3F,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QACxE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;QAC3E;YACE,cAAc;YACd,gBAAgB;YAChB,eAAe;YACf,mBAAmB;YACnB,2EAA2E;YAC3E,sEAAsE;YACtE,qDAAqD;YACrD,cAAc;YACd,cAAc;SACf,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IACvC,CAAC;CACF"}
@@ -2,8 +2,8 @@ import { type QuerySpec } from "../dsl.js";
2
2
  import { Query } from "../query/index.js";
3
3
  import { System } from "../system.js";
4
4
  import type { WorldOptions } from "./world.base.js";
5
- import { Transfers } from "./world.transfers.js";
6
- export declare class Pipeline extends Transfers {
5
+ import { Disposal } from "./world.disposal.js";
6
+ export declare class Pipeline extends Disposal {
7
7
  constructor(options?: WorldOptions);
8
8
  /** Open a new frame and evaluate every registered tick source once. */
9
9
  beginFrame(delta: number): void;
@@ -1,7 +1,7 @@
1
1
  import { System } from "../system.js";
2
2
  import { ALWAYS_TICK_SOURCE } from "../timer.js";
3
- import { Transfers } from "./world.transfers.js";
4
- export class Pipeline extends Transfers {
3
+ import { Disposal } from "./world.disposal.js";
4
+ export class Pipeline extends Disposal {
5
5
  constructor(options = {}) {
6
6
  super(options);
7
7
  /** @internal Flat system execution pipeline, rebuilt from `_pipelineQuery`. */
@@ -1 +1 @@
1
- {"version":3,"file":"world.pipeline.js","sourceRoot":"","sources":["../../src/world/world.pipeline.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"world.pipeline.js","sourceRoot":"","sources":["../../src/world/world.pipeline.ts"],"names":[],"mappings":"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"}
package/docs/README.md CHANGED
@@ -16,7 +16,8 @@ Read in this order the first time through:
16
16
  4. Subsystem guides, in any order:
17
17
  - [Components](./components.md) — registration, hooks, traits, exclusivity, cleanup policies,
18
18
  interfaces, singletons.
19
- - [Entities](./entities.md) — creation, lookup, the component API, relationship targets, lifecycle.
19
+ - [Entities](./entities.md) — creation, lookup, the component API, relationship targets, lifecycle,
20
+ disposing a world.
20
21
  - [Systems](./systems.md) — membership, reactive callbacks, callback options, `each`/`run`, cadence,
21
22
  component and relationship injection.
22
23
  - [Queries and filters](./queries-and-filters.md) — the query DSL, live queries, one-shot
@@ -41,7 +42,7 @@ Keep the [Glossary](./glossary.md) at hand for terminology.
41
42
  | [concepts.md](./concepts.md) | The core vocabulary and how the pieces relate. |
42
43
  | [execution-model.md](./execution-model.md) | `world.progress`, the 8 built-in phases, deferred commands, flush boundaries, event routing. |
43
44
  | [components.md](./components.md) | Component registration, hooks, traits, exclusivity, cleanup, aliases, and singletons. |
44
- | [entities.md](./entities.md) | Entity creation, component mutation, relationship targets, lifecycle, deferred reads. |
45
+ | [entities.md](./entities.md) | Entity creation, mutation, relationship targets, lifecycle, disposal, deferred reads. |
45
46
  | [systems.md](./systems.md) | `world.system`, callbacks, injection, ordering, cadence, and tick sources. |
46
47
  | [queries-and-filters.md](./queries-and-filters.md) | `QueryDSL`, `Query`, `Filter`, `groupBy`, `ignoreSource`. |
47
48
  | [relationships.md](./relationships.md) | Relationships, target creation, projection and ownership, hierarchy, traversal, and cleanup. |
@@ -58,5 +59,5 @@ sessions, and the client world) lives in the `@vworlds/vecs-wire`, `@vworlds/vec
58
59
  - [Remote procedure calls](../../vecs-protocol/docs/rpc.md) — the call channel and typed errors.
59
60
  - [Sessions and caller context](../../vecs-server/docs/sessions.md) — per-connection RPC context,
60
61
  `closeSession`, and exactly-once teardown.
61
- - [Client world](../../vecs-client/docs/client-world.md) — replication, `invoke`/`listen`, and
62
- disposal.
62
+ - [Client world](../../vecs-client/docs/client-world.md) — replication, render time,
63
+ `invoke`/`listen`, and disposal.
@@ -69,8 +69,14 @@ Entity ids are drawn from configurable pools. The defaults
69
69
  Pass `idPools` to the `World` constructor to change ranges (each entry is an `IdPoolConfig` with
70
70
  `name`, `min`, optional `max`, and optional `reuseWaitFrames`). Freed ids wait
71
71
  `reuseWaitFrames` frames (default `DEFAULT_ID_REUSE_WAIT_FRAMES = 16`) before reuse, so an id
72
- released this frame is not immediately recycled. `world.peekNextEid(poolName)` returns the next id
73
- a pool would allocate without reserving it. Id 0 is reserved.
72
+ released this frame is not immediately recycled. A frame is one `beginFrame()`, so one
73
+ `progress()`; a wait of `0` frees the id at once. Once the wait is over, allocation hands out free
74
+ ids before never-used ones, lowest first, so a released id is recycled by one of the next entities
75
+ created. A `ClientWorld` sets the wait to `0` for the ids of replicated entities and keeps the
76
+ default for its local ones. A recycled id is why a stored id is not a safe link between entities
77
+ (see [Link with edges, not stored ids](./relationships.md#link-with-edges-not-stored-ids)).
78
+ `world.peekNextEid(poolName)` returns the next id a pool would allocate without reserving it. Id 0
79
+ is reserved.
74
80
 
75
81
  ## Hooks: component and relationship edges
76
82
 
@@ -103,6 +109,26 @@ command is applied (see [Execution model](./execution-model.md#reactive-routing)
103
109
  resource management and cross-cutting reactions that aren't worth a system; use systems when the
104
110
  reaction belongs to a phase.
105
111
 
112
+ `remove` also fires when the owning entity is destroyed, and `entity.destroyed` is already `true`
113
+ inside the hook. A dying entity takes no writes: `add`, `set`, `remove`, `modified` and `attach`
114
+ are dropped without an error, and `ensure` throws. Releasing a resource needs no check. A hook that
115
+ also writes to its own entity must skip the write when `entity.destroyed`, and that guard is the
116
+ right one here (relationship hooks follow the same rule, see
117
+ [Relationships — edge hooks](./relationships.md#edge-hooks)):
118
+
119
+ ```ts
120
+ class Shield {}
121
+ class Stats {
122
+ armor = 0;
123
+ }
124
+
125
+ world.component(Shield).on("remove", (entity) => {
126
+ if (!entity.destroyed) {
127
+ entity.ensure(Stats, (stats) => (stats.armor -= 10)); // throws on a dying entity
128
+ }
129
+ });
130
+ ```
131
+
106
132
  Multiple handlers may be registered for the same event: they fire in registration
107
133
  order. Call `on(event, handler)` to append, `on(event, handler, true)` to
108
134
  prepend, and `off(event, handler)` to remove a handler by reference. All three
@@ -133,11 +133,12 @@ entity.attach(obj); // the GameObject is now a component, keyed by its class
133
133
  world.component(Phaser.GameObjects.Arc).on("remove", (_entity, obj) => obj.destroy());
134
134
  ```
135
135
 
136
- `onRemove` (below) releases the resource whenever it leaves — whether removed explicitly or
137
- because the entity was destroyed. When several resource types should share one cleanup hook or be
138
- queried together, give them a common **interface** with `Implements` (see §7 and the §10 case
139
- study). A plain wrapper component (`class Handle { obj }`) is still fine when you need extra fields
140
- beside the resource.
136
+ `onRemove` (below) releases the resource whenever it leaves — whether removed explicitly,
137
+ because the entity was destroyed, or because the world was disposed (`world.dispose()` destroys
138
+ every entity the application created; see [Disposing a world](./entities.md#disposing-a-world)).
139
+ When several resource types should share one cleanup hook or be queried together, give them a
140
+ common **interface** with `Implements` (see §7 and the §10 case study). A plain wrapper component
141
+ (`class Handle { obj }`) is still fine when you need extra fields beside the resource.
141
142
 
142
143
  ### Component lifecycle hooks: `onAdd` / `onSet` / `onRemove`
143
144
 
@@ -305,7 +306,8 @@ Choose `onEnter: true` by asking who owns the initial value:
305
306
 
306
307
  ### Injection
307
308
 
308
- Pull components into the callback instead of `entity.get()`-ing them by hand:
309
+ **Inject every component a system callback always uses.** Pull it into the callback instead of
310
+ `entity.get()`-ing it by hand:
309
311
 
310
312
  ```ts
311
313
  .enter([Arc], (entity, [arc]) => { /* … */ })
@@ -314,6 +316,23 @@ Pull components into the callback instead of `entity.get()`-ing them by hand:
314
316
  .each([Position, Velocity], (entity, [pos, vel]) => { /* … */ })
315
317
  ```
316
318
 
319
+ Use `entity.get(C)` only for a component that is conditionally needed and rarely required: a read on
320
+ a branch most calls never take, where resolving the component for every call would be waste.
321
+
322
+ Why:
323
+
324
+ - **The signature states the inputs.** What a callback reads sits next to the `.with(...)` that
325
+ guarantees it, not in the body.
326
+ - **No dead guards.** A component guaranteed by `.with(...)` is injected as non-nullable, so there
327
+ is no `undefined` to check. `get` returns `Readonly<C> | undefined`, which invites an
328
+ `if (c !== undefined)` that tells the next reader the component might be missing when the query
329
+ says it cannot be. An injected instance is also the live, mutable one: a callback that writes in
330
+ place and calls `entity.modified(C)` (see §7) needs no cast.
331
+ - **Resolved once.** Injection resolves a component's id when the callback is registered;
332
+ `entity.get(C)` resolves the class on every call. In an `exit` callback injection matters more
333
+ still: it reads the snapshot captured when the entity left, while a `get` at replay time can find
334
+ the component already removed.
335
+
317
336
  Components listed in the inject tuple but **not** guaranteed by `.with(...)` resolve as
318
337
  `T | undefined` (the types reflect this) — guard them; the ones in `.with` are always present.
319
338
 
@@ -328,7 +347,7 @@ Components listed in the inject tuple but **not** guaranteed by `.with(...)` res
328
347
  // ❌ scans every body every tick, even if it didn't move
329
348
  .with(PhysicsPosition, RenderPosition).each([...], (e, [...]) => { if (changed) e.set(...) });
330
349
 
331
- // ✅ fires only when physics actually moved the body
350
+ // ✅ fires only when physics actually moved the body (compare before writing a networked B: see §7)
332
351
  .with(PhysicsPosition, RenderPosition).update(PhysicsPosition, (e, p) => e.set(RenderPosition, { x: p.x, y: p.y }));
333
352
  ```
334
353
 
@@ -396,15 +415,28 @@ export class PhaserServerModule extends Module<undefined> {
396
415
  .system("phaser.sync.position")
397
416
  .with(PhysicsPosition, RenderPosition)
398
417
  .phase(POST_UPDATE)
399
- .update({ watch: PhysicsPosition, onEnter: true }, (e, p) =>
400
- e.set(RenderPosition, { x: p.x, y: p.y })
418
+ .update(
419
+ { watch: PhysicsPosition, onEnter: true, inject: [RenderPosition] },
420
+ (e, p, [render]) => {
421
+ if (render.x !== p.x || render.y !== p.y) {
422
+ render.x = p.x;
423
+ render.y = p.y;
424
+ e.modified(RenderPosition);
425
+ }
426
+ }
401
427
  );
402
428
  world
403
429
  .system("phaser.sync.rotation")
404
430
  .with(PhysicsRotation, RenderRotation)
405
431
  .phase(POST_UPDATE)
406
- .update({ watch: PhysicsRotation, onEnter: true }, (e, r) =>
407
- e.set(RenderRotation, { angle: r.angle })
432
+ .update(
433
+ { watch: PhysicsRotation, onEnter: true, inject: [RenderRotation] },
434
+ (e, r, [render]) => {
435
+ if (render.angle !== r.angle) {
436
+ render.angle = r.angle;
437
+ e.modified(RenderRotation);
438
+ }
439
+ }
408
440
  );
409
441
  }
410
442
  }
@@ -583,14 +615,31 @@ rebinds. No teardown system, no special-casing anywhere.
583
615
 
584
616
  Copy `A → B`, gated on both existing (so you never _create_ `B`, you only keep an existing `B` in
585
617
  sync — pure-ECS: the owner decides whether `B` exists). Use `{ onEnter: true }` when `B` must reflect
586
- the current `A` as soon as the entity enters the sync system:
618
+ the current `A` as soon as the entity enters the sync system, inject `B` (the `.with(...)` guarantees
619
+ it, so it is non-nullable, see §4), and write only when `B` differs from `A`:
587
620
 
588
621
  ```ts
589
- .with(PhysicsPosition, RenderPosition).update({ watch: PhysicsPosition, onEnter: true }, (e, p) =>
590
- e.set(RenderPosition, { x: p.x, y: p.y })
622
+ .with(PhysicsPosition, RenderPosition).update(
623
+ { watch: PhysicsPosition, onEnter: true, inject: [RenderPosition] },
624
+ (e, p, [render]) => {
625
+ if (render.x !== p.x || render.y !== p.y) {
626
+ render.x = p.x;
627
+ render.y = p.y;
628
+ e.modified(RenderPosition);
629
+ }
630
+ }
591
631
  );
592
632
  ```
593
633
 
634
+ Every write is a modification, even with an equal value: both `set` and `modified` fire `onSet`,
635
+ route `update` events to every watching system and replicate a networked component. So a sync into a
636
+ networked or watched `B` compares before it writes. The injected `B` is the live instance, so assign
637
+ into it and call `entity.modified(B)`: that has the same effects as `set` without the props literal
638
+ and the deferred copy, and a second event for the same entity in one drain finds the first write
639
+ applied and writes nothing. Always pair an in-place write with `modified`: assignment alone notifies
640
+ nobody, so hooks, watchers and replication never see it. Use `set` when you do not hold the instance,
641
+ or when the write should create the component.
642
+
594
643
  Leave `onEnter: true` off (or set `onEnter: false`) only when a separate `.enter(...)` already initializes `B`,
595
644
  or when the callback is intentionally "future changes only."
596
645
 
@@ -654,6 +703,11 @@ The lifecycle follows from the relationship and trait:
654
703
  | `player.remove(Inventory)` | Destroys every current target |
655
704
  | Destroy `player` | `ChildOf` destroys the inventory |
656
705
 
706
+ Prefer this to a component field that holds the child entity, with an `onRemove` hook that destroys
707
+ it. Every operation that drops the edge destroys the child, and destroying the child directly
708
+ removes the edge, so the owner needs no hook and holds nothing that can go stale (see
709
+ [Link with edges, not stored ids](./relationships.md#link-with-edges-not-stored-ids)).
710
+
657
711
  Inside deferred execution, keep and mutate the target you created; the queued edge is not visible
658
712
  through `target` until flush:
659
713
 
@@ -701,12 +755,16 @@ system after whatever produces the entity/companion.
701
755
  | A god component with many optional fields | Split into small single-purpose components; absence encodes "not applicable." |
702
756
  | A large nested `inventory`, `brain`, or progression object | Use an owned target entity when the subobject needs its own components, systems, and lifecycle. |
703
757
  | A side `Map<ownerEid, SubsystemState>` | Create a target entity and add an explicit edge; use `Exclusive` + `OwnedTarget` only for destructive ownership. |
758
+ | An `eid` or `Entity` stored in a component field as a link | A relationship edge: vecs cleans it up; a stored eid can name another entity once reused ([why](./relationships.md#link-with-edges-not-stored-ids)). |
759
+ | A component field holding a child entity, destroyed in `onRemove` | `Exclusive` + `OwnedTarget`: dropping the edge destroys the child; no hook, no stale field ([§7](#owned-target-entities-entity-valued-properties)). |
760
+ | `entity.destroyed` guards on an entity looked up from a stored link | A missing edge: replace the link. Keep guards for event buffers and dying-entity hooks ([why](./relationships.md#link-with-edges-not-stored-ids)). |
704
761
  | `OwnedTarget` on `CurrentEnemy`, `InCell`, `Team`, or shared state | Use a normal relationship; replacing or removing an owned edge destroys its target. |
705
762
  | One owned target used as a collection | Use multiple child/item entities; owned targets model zero-or-one aggregates. |
706
763
  | Duplicated `enter` + `update` doing the same thing | One `update({ watch: C, onEnter: true })` when update owns initialization. |
707
764
  | One system trying to act in two phases | Split into two systems, one per phase. |
708
765
  | Custom phases for app/render logic | Use the standard taxonomy; only engine modules insert phases. |
709
766
  | `.each` scanning every entity to detect change | Reactive `.update(C)` — fires only on change. |
767
+ | `entity.get(C)` for a component the callback always uses | Inject it (`inject: [C]`); keep `get` for a component that is conditionally needed and rarely required. |
710
768
  | Treating target storage as component data | Use `add(Rel, target)` / `remove(Rel, target)` and read with `target` / `targets` / `hasTarget`; target storage is private. |
711
769
  | A `registerFooSystem(world)` wrapper per system | Declare the system inline in the module's `init`. |
712
770
 
@@ -720,9 +778,33 @@ system after whatever produces the entity/companion.
720
778
  - **`update(C)` is change-only by default.** Add `{ onEnter: true }` for projection/sync callbacks
721
779
  that must apply the current watched value on entry; leave it off for mutation-only callbacks or
722
780
  when `.enter(...)` already owns initialization.
781
+ - **A write marks the component modified, even with an equal value.** `set`, and an in-place
782
+ assignment followed by `modified`, fire `onSet`, route `update` events to every watching system and
783
+ replicate a networked component. A writer that runs every tick and copies a value into a networked
784
+ or watched component (simulation output, a derived pose) should compare first and write only when
785
+ the value changed (see §7). An in-place assignment without `modified` notifies nobody.
786
+ - **`getMut` marks the component modified even without a callback and even if you write nothing.**
787
+ Calling it every tick fires `onSet`, `update` watchers and replication every tick. Read with `get`;
788
+ call `getMut` only when a change is intended (see
789
+ [`getMut` and `ensure`](./entities.md#getmut-and-ensure-mutating-in-place)).
790
+ - **A callback that always uses a component should inject it, not `get` it.** `entity.get(C)` is for
791
+ a component that is conditionally needed and rarely required (see §4).
723
792
  - **Membership is fixed by `.with(...)` / `.without(...)` / `.update(C)`;** `update(C)` watches `C`
724
793
  and requires it.
794
+ - **A system's `enter` reaches only entities still in its query when the system drains.** One that
795
+ left or died first gets neither `enter` nor the paired `exit`, and one that leaves and re-enters
796
+ within a drain gets `enter`, `exit`, `enter`, so keep `enter` setup and `exit` teardown symmetric
797
+ (see [`.enter`](./systems.md#entercallback--enterinject-callback--enteroptions-callback)).
725
798
  - **`exit` retains injected references captured at exit** (components are not deep-cloned).
799
+ - **A `remove` hook also fires while the entity is being destroyed,** with `entity.destroyed`
800
+ already `true`. A dying entity takes no writes (`add`, `set`, `remove`, `modified` and `attach` are
801
+ dropped, `ensure` throws), so a hook that writes to its own entity guards on `entity.destroyed`.
802
+ That guard is legitimate; elsewhere a `destroyed` check usually signals a missing edge (see §8 and
803
+ [Hooks](./components.md#hooks-component-and-relationship-edges)).
804
+ - **Release resources in `remove` hooks, not in a system's `exit` callback.** `world.dispose()`
805
+ runs the hooks during the call, but a system's `exit` callback only queues its event, and a
806
+ disposed world never runs the system again (see
807
+ [Disposing a world](./entities.md#disposing-a-world)).
726
808
  - **Within a phase, order = registration order;** across phases, the spine guarantees order. Don't
727
809
  use registration order for cross-module correctness — use phases.
728
810
  - **Networked component order = wire protocol.** Reordering breaks the protocol.
@@ -733,6 +815,9 @@ system after whatever produces the entity/companion.
733
815
  ordering; `hasTarget(R, target)` tests one edge.
734
816
  - **Relationship edges are explicit.** Add with `add(R, target)`, remove one with
735
817
  `remove(R, target)`, and remove all with `remove(R)`. Do not use `set` for target storage.
818
+ - **Relationship removal hooks differ in scope.** `on("targetRemove")` fires once per removed edge;
819
+ `on("remove")` fires once, when the relationship component itself leaves, with no targets left on
820
+ the instance (see [Edge hooks](./relationships.md#edge-hooks)).
736
821
  - **Relationship cardinality and component exclusivity are different.** Add the `Exclusive` trait
737
822
  to a relationship for at-most-one target; use `setExclusiveComponents` for mutually exclusive
738
823
  component types.
@@ -754,6 +839,33 @@ system after whatever produces the entity/companion.
754
839
  explicitly, and during deferred execution keep that entity instead of re-reading `target(R)`
755
840
  before flush.
756
841
  - **`world.module(M)` runs `init` once.** Re-registering with a different config throws.
842
+ - **vecs-phaser: a `DrawBy` child follows only its parent's `Position`.** It turns with the parent
843
+ only with `InheritRotation`, which makes the resolver derive its `Rotation` every frame, so
844
+ writing `Rotation` on a tagged child has no effect (see
845
+ [Turning with the parent](../../vecs-phaser-client/README.md#turning-with-the-parent)).
846
+ - **vecs-phaser: `DrawBy` and `DrawIn` use `CleanupPolicy.Remove`,** so destroying a display parent
847
+ detaches its children instead of destroying them, unlike `ChildOf`, which deletes them. A display
848
+ child that must die with its parent also needs `ChildOf` to it, or must be its owned target (see
849
+ [Destroying the display parent](../../vecs-phaser-client/README.md#destroying-the-display-parent)).
850
+ - **vecs-physics, vecs-phaser-server: pose writes are change-only.** `set` hooks and `update` systems
851
+ on the physics and render pose fire when the pose changed, not on every step; to visit every body
852
+ every step, iterate with `each` (see
853
+ [Pose Updates Fire Only When The Pose Changes](../../vecs-physics/docs/systems-and-phases.md#pose-updates-fire-only-when-the-pose-changes)).
854
+ - **vecs-phaser-server: game code owns the physics pose; the render pose is protocol-private.** On
855
+ the server, read and write the physics `Position` and `Rotation` of every entity, including one
856
+ without a `Body`. The render `Position` and `Rotation` and the dead-reckoning `Motion` are wire
857
+ encodings of that pose that only the sync systems write: a `DeadReckoned` body has no render pose
858
+ at all, and adding one throws (see
859
+ [Dead reckoning](../../vecs-phaser-server/README.md#dead-reckoning)).
860
+ - **vecs-client: attach a `ClientWorld` socket only after `socket.connect()` resolves,** so no ack
861
+ precedes the data channel (`connectDgram` does this). Pending `invoke` calls reject with
862
+ `RPC_CLOSED_CODE` on disconnect or `dispose` (see
863
+ [Connecting](../../vecs-client/docs/client-world.md#connecting) and
864
+ [Disconnection and disposal](../../vecs-client/docs/client-world.md#disconnection-and-disposal)).
865
+ - **vecs-physics: component fields hold plain data,** never Box2D wrappers or other class
866
+ instances. `set` accepts them, but snapshot capture (`snapshotSubtree`, `moveSubtree`) rejects
867
+ non-plain values (see
868
+ [Keep Component Fields Plain Data](../../vecs-physics/docs/components.md#keep-component-fields-plain-data)).
757
869
 
758
870
  ---
759
871
 
package/docs/entities.md CHANGED
@@ -39,7 +39,8 @@ setting `entity.name` writes the `Identity` component, and duplicate names are l
39
39
  `world.entities` is a read-only view of every live entity keyed by id, and `world.clearAllEntities()`
40
40
  destroys **every** entity in the world — ordinary entities first, then component entities once
41
41
  their carriers are gone. It is a full wipe (systems and phases are backed by entities too); use it
42
- for end-of-session teardown, not selective resets.
42
+ for end-of-session teardown, not selective resets. To destroy only what the application created
43
+ and keep the world's own entities, see [Disposing a world](#disposing-a-world).
43
44
 
44
45
  ## The component API
45
46
 
@@ -95,6 +96,14 @@ e.getMut(Velocity, (vel) => {
95
96
  });
96
97
  ```
97
98
 
99
+ `getMut` marks the component modified whenever it is present, with or without a callback and whether
100
+ or not you assign anything: the callback form marks it after the callback returns, and the
101
+ callback-free form marks it before it returns the instance, so even a read-only use notifies. The
102
+ mark fires `onSet`, routes an `update` event to every system watching the component and replicates a
103
+ networked component (repeated marks before the queue drains coalesce into one). A `getMut` that runs
104
+ every tick therefore does all of that every tick. Read with `get`, and call `getMut` only when a
105
+ change is intended. When the component is absent, `getMut` returns `undefined` and marks nothing.
106
+
98
107
  Use `ensure` when absence should mean "create with defaults, then mutate":
99
108
 
100
109
  ```ts
@@ -174,6 +183,66 @@ and policy surface as a registered class, and `keyEntity.on("add" / "set" / "rem
174
183
  "targetRemove", handler)` registers its lifecycle handlers directly. This is how tag-like,
175
184
  runtime-created component types work without declaring a class.
176
185
 
186
+ ## Disposing a world
187
+
188
+ When you are finished with a world — a scene or route change, a hot reload — call
189
+ `world.dispose()` and drop the reference. It destroys every entity the application and its modules
190
+ created, so the `remove` hook of each component they carry runs and releases what the component
191
+ owns: a render object, a handle, a timer.
192
+
193
+ ```ts
194
+ class Handle {
195
+ public released = false;
196
+ }
197
+
198
+ world.component(Handle).on("remove", (_entity, handle) => {
199
+ handle.released = true; // release the resource here
200
+ });
201
+
202
+ const handle = new Handle();
203
+ world.entity().attach(handle);
204
+
205
+ world.dispose();
206
+ handle.released; // true
207
+ ```
208
+
209
+ Each entity goes through the ordinary destroy path. Every `remove` hook fires once,
210
+ [cleanup policies and cascades](./relationships.md#cleanup-when-a-target-dies) run as for any
211
+ `destroy()`, and an entity that a `ChildOf` or `OwnedTarget` cascade, or a hook, has already
212
+ destroyed is skipped instead of destroyed twice.
213
+
214
+ `dispose()` leaves the world's infrastructure in place, because the world needs it to stay
215
+ coherent. It recognizes it as follows (`lib/vecs/src/world/world.disposal.ts`):
216
+
217
+ | Infrastructure | Recognized by |
218
+ | ----------------------------- | ------------------------------------------------------------- |
219
+ | Component and module entities | They are `Component` instances; `Module` extends `Component`. |
220
+ | Query and system entities | They carry the `Query` component. |
221
+ | Phase entities | They carry the `Phase` component. |
222
+
223
+ What those entities hold stays with them: data stored on a component entity (a singleton set with
224
+ `world.set`) and any entity they still carry as a [component key](#entities-as-component-keys),
225
+ which cannot be destroyed while it is in use. Release those yourself; their `remove` hooks do not
226
+ run. `clearAllEntities()` is the full wipe that takes the infrastructure too.
227
+
228
+ - **Order.** Among unrelated entities the order is unspecified, so do not make a hook depend on it.
229
+ An entity used as a component key is destroyed after every entity that carries it, because
230
+ destroying it first would throw under the default cleanup policy.
231
+ - **Pending commands.** `dispose()` neither flushes the command queue early nor discards it. Commands
232
+ queued ahead of it run first, in order, and the destruction follows in the same queue, so each
233
+ hook fires once however the component left. At top level nothing is queued, and `dispose()`
234
+ returns with the entities gone. Inside a system, a hook or a `defer` block the world is
235
+ [deferred](./execution-model.md#deferred-mutation-and-flush-boundaries), so `dispose()` queues the
236
+ destruction as `destroy()` does, and it completes when the queue next drains. An entity whose
237
+ creation is still queued is created, then destroyed.
238
+ - **After disposal.** A disposed world must not be used again. Nothing enforces it — no other method
239
+ checks, so `entity()` and `progress()` keep their cost. Calling `dispose()` again, or from a hook
240
+ during the teardown, does nothing. An entity that a `remove` hook creates during the teardown is
241
+ left behind, so do not spawn from teardown hooks. A system's `exit` callback only queues its event,
242
+ and a disposed world never runs the system again; release resources in `remove` hooks, as
243
+ [Resources as components](./design-guide.md#resources-as-components-hold-non-ecs-objects)
244
+ recommends.
245
+
177
246
  ## Relationships, in brief
178
247
 
179
248
  `e.target(Rel)` returns one current target, `e.targets(Rel)` a readonly snapshot of every target,
@@ -162,7 +162,7 @@ component:
162
162
  their next run (`lib/vecs/src/system.ts`). So systems react to change without scanning, and they
163
163
  react _in their own phase slot_, in event arrival order.
164
164
 
165
- Three routing rules worth memorizing:
165
+ Four routing rules worth memorizing:
166
166
 
167
167
  - **`update(C)` is change-only unless you opt into entry.** When an entity enters a system,
168
168
  `{ onEnter: true }` update watchers push an `update` event for already-present watched components.
@@ -173,8 +173,9 @@ Three routing rules worth memorizing:
173
173
  objects are not deep-cloned.
174
174
  - **`enter` is drain-time truthful.** A system only receives `enter` when the entity is still a
175
175
  member when that system runs. If it left before the inbox drains, vecs drops both that queued
176
- `enter` and its paired `exit`; an entity that re-enters before draining receives the ordered
177
- lifecycle events for its final membership.
176
+ `enter` and its paired `exit`; an entity that re-enters before draining is a member again, so the
177
+ queued events replay in order as `enter`, `exit`, `enter`. The details are in
178
+ [Systems — `.enter`](./systems.md#entercallback--enterinject-callback--enteroptions-callback).
178
179
  - **In-place mutation needs `modified`.** Assigning to component fields directly notifies no one.
179
180
  Call `entity.modified(C)` (or use `set` / `getMut` / `ensure`) to route the change.
180
181
 
package/docs/glossary.md CHANGED
@@ -38,6 +38,10 @@ behavior. [Components](./components.md).
38
38
  queued as commands; `flush` drains the queue at the scope boundary (after each system in the
39
39
  pipeline). [Execution model](./execution-model.md#deferred-mutation-and-flush-boundaries).
40
40
 
41
+ **Dispose** — `world.dispose()`: end a world's life by destroying every entity the application and
42
+ its modules created, so their components' `remove` hooks run. The infrastructure (component,
43
+ module, query, system, and phase entities) stays. [Entities](./entities.md#disposing-a-world).
44
+
41
45
  **Entity** — a unique numeric id (`eid`) owned by a world, with a set of components attached.
42
46
  [Entities](./entities.md).
43
47
 
@@ -33,6 +33,35 @@ again is idempotent. Target ordering is not a public contract.
33
33
  storage. For a non-exclusive relationship, do not use `target(ref)` as a stable selection policy.
34
34
  Use `hasTarget(ref, target)` to test one edge without allocating a snapshot.
35
35
 
36
+ ## Link with edges, not stored ids
37
+
38
+ Link one entity to another with a relationship edge, not with an `eid` or an `Entity` kept in a
39
+ component field (`ownerEid = 0`, `target: Entity | undefined`). The world maintains an edge. It
40
+ cannot maintain a field:
41
+
42
+ - **A stored eid can come to name another entity.** A destroyed entity's id is reused once its
43
+ pool's `reuseWaitFrames` have passed: 16 frames by default, and `0` for replicated entities on a
44
+ `ClientWorld` (see [Id pools](./components.md#id-pools)). Allocation hands out free ids before
45
+ never-used ones, so one of the next entities created takes it. Until an entity does,
46
+ `world.getEntity(id)` returns `undefined`; from then on it returns that live, unrelated entity and
47
+ the stale id looks valid.
48
+ - **A stored `Entity` dangles.** Reuse never makes it name another entity, because a destroyed
49
+ `Entity` stays `destroyed`, but nothing clears the field when the entity dies. Writes through it
50
+ are dropped and `ensure` throws.
51
+ - **An edge is cleaned up for you.** When the target is destroyed, `onDeleteTarget` removes the edge
52
+ or destroys the source (see [Cleanup when a target dies](#cleanup-when-a-target-dies)). The
53
+ reverse index (`children`), `target` and `parent` queries, `groupBy` and network replication work
54
+ on edges only.
55
+ - **A field does not cross worlds.** `snapshotSubtree` and `moveSubtree` remap relationship targets
56
+ onto the destination's new ids. An eid field is copied as is and names the wrong entity there; an
57
+ `Entity` field makes capture throw (see [Snapshot policies](./snapshots.md#snapshot-policies)).
58
+
59
+ A `destroyed` check on an entity you looked up from a stored link is therefore a symptom of a
60
+ missing edge: replace the link, not only the check. The guard stays right where a reference is
61
+ deliberately short-lived: payloads in per-step event buffers (the `other` entity in `vecs-physics`
62
+ contact events), and hooks that run while an entity or an edge target is being destroyed (see
63
+ [Edge hooks](#edge-hooks) and [component hooks](./components.md#hooks-component-and-relationship-edges)).
64
+
36
65
  ## Built-in relationships
37
66
 
38
67
  | Class | Purpose |
@@ -192,6 +221,26 @@ calls `onRemove`. An exclusive replacement calls `onTargetRemove` for the old ta
192
221
  relationship-dependent query updates, but do not call `onSet`; `onSet` is reserved for ordinary
193
222
  relationship data changed through `set`.
194
223
 
224
+ Pick the removal hook by scope:
225
+
226
+ - `on("targetRemove", handler)` runs once for **each** removed edge: `remove(R, target)`, the
227
+ displaced edge of an `Exclusive` replacement, and every edge when the whole relationship goes away
228
+ (the final edge, `remove(R)`, or destroying the source).
229
+ - `on("remove", handler)` runs **once**, when the relationship component itself leaves the entity.
230
+ It does not run when one of several edges is removed or an `Exclusive` edge is replaced. The
231
+ relationship instance it receives holds no targets any more, so use `targetRemove` to learn which
232
+ targets went.
233
+
234
+ When the whole relationship goes away, `source.has(R)` is already `false` inside both hooks. When
235
+ only one of several edges goes, `source.has(R)` stays `true` and `source.hasTarget(R, removed)` is
236
+ `false`.
237
+
238
+ Destroying either end of an edge runs these hooks too, so `source.destroyed` or `target.destroyed`
239
+ can already be `true` inside them; both are under `Delete` cleanup. The target of an `OwnedTarget`
240
+ edge is destroyed before the public hook runs on every path. A dying end takes no writes (see
241
+ [Hooks](./components.md#hooks-component-and-relationship-edges)), so a handler that writes to
242
+ `source` or `target` guards on `destroyed`.
243
+
195
244
  Register handlers with `on("targetAdd", handler)` and `on("targetRemove", handler)`; remove them
196
245
  with `off(event, handler)`. Multiple handlers may be registered for the same event and fire in
197
246
  registration order. Framework traits install separate internal `targetPolicyAdd` /
@@ -227,6 +276,11 @@ attached: removing it throws, and only world teardown (`clearAllEntities`) bypas
227
276
  the same contract as `Traversable` and `OneOf`. Do not share an owned target between
228
277
  sources because either owner can destroy it when its edge is removed.
229
278
 
279
+ An owned target replaces the field-plus-hook pattern, where a component field holds the child
280
+ entity and an `onRemove` hook destroys it. The edge is the field: every path that drops it destroys
281
+ the child, and destroying the child directly removes the edge from its owner. There is no hook to
282
+ write and no stale reference to guard.
283
+
230
284
  ## Cleanup when a target dies
231
285
 
232
286
  What happens to each incoming edge when its target entity is destroyed is governed by the
package/docs/snapshots.md CHANGED
@@ -83,8 +83,11 @@ class Position {
83
83
  ```
84
84
 
85
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.
86
+ own enumerable fields copied over, deeply. The copy covers primitives, arrays, plain objects, `Map`,
87
+ `Set`, `Date`, `ArrayBuffer` and typed arrays, so the destination never aliases a nested value of
88
+ the source. Anything else, such as a class instance, a function or a symbol, is not plain data:
89
+ capture throws with the component and field path instead of sharing it across worlds. A component
90
+ that holds one declares `CustomSnapshot` or `DontSnapshot` (below).
88
91
 
89
92
  ### `DontSnapshot`
90
93
 
package/docs/systems.md CHANGED
@@ -89,10 +89,15 @@ 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.
92
+ The callback runs only if the entity is still a member when this system drains its inbox, at the top
93
+ of its next run. The inbox replays in arrival order, so:
94
+
95
+ - An entity that left the query or was destroyed first gets no `enter`, and in a system that
96
+ registers both callbacks no paired `exit` either. Do not add a `destroyed` guard to normal `enter`
97
+ setup code. A system with only `exit` has no `enter` to pair, so it still gets that `exit`.
98
+ - An entity that leaves and re-enters before the drain is a member again, so the system gets
99
+ `enter`, `exit`, `enter` (just `exit`, `enter` when it had already entered in an earlier drain).
100
+ Keep `enter` setup and `exit` teardown symmetric so the second `enter` starts from a clean state.
96
101
 
97
102
  For systems, injected values and `Iter.src` are captured when entry is routed and replayed from that
98
103
  snapshot when the system next drains its inbox.
@@ -189,6 +194,9 @@ an options object with `inject`, resolved per entity and passed as a typed tuple
189
194
  .each({ inject: [Position, Sprite] }, (e, [pos, sprite]) => { ... })
190
195
  ```
191
196
 
197
+ Inject every component a callback always uses, and keep `entity.get` for one that is only
198
+ conditionally needed (see [Design guide](./design-guide.md#injection)).
199
+
192
200
  Nullability follows membership: components guaranteed by `with` (or `hint`) are non-nullable;
193
201
  anything else resolves as `T | undefined` — guard it. (`enter`/`exit` injection requires the
194
202
  direct components to be present and throws otherwise; list only components your predicate
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vworlds/vecs",
3
- "version": "1.0.48",
3
+ "version": "1.0.50",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -65,6 +65,60 @@ Check the design guide before introducing:
65
65
  If the requested design conflicts with a documented vecs invariant, explain the conflict and use
66
66
  the documented pattern unless the user explicitly chooses otherwise.
67
67
 
68
+ ## Known Gotchas
69
+
70
+ Agents trip on these vecs behaviors repeatedly. Design guide sections 8 and 9 list them with the
71
+ other anti-patterns and pitfalls; each rule below links to the detail.
72
+
73
+ - Do not call `getMut` per tick unless a change is intended. It marks the component modified even
74
+ without a callback and even if nothing is written, which fires `update` watchers and replication.
75
+ Read with `get`. ([`getMut`](../../docs/entities.md#getmut-and-ensure-mutating-in-place))
76
+ - A system's `enter` reaches only entities still in its query when the system drains. An entity
77
+ that left the query or died first gets neither `enter` nor the paired `exit`, and a re-entry
78
+ within one drain gives `enter`, `exit`, `enter`, so keep setup and teardown symmetric.
79
+ ([`.enter`](../../docs/systems.md#entercallback--enterinject-callback--enteroptions-callback))
80
+ - Create relationship edges with `add(Rel, target)` and read them with `target()` / `targets()`.
81
+ React to the relationship component being removed with `on("remove")` and to each removed edge
82
+ with `on("targetRemove")`. `OwnedTarget` needs `Exclusive` and an explicitly created target, and
83
+ destroys that target when its edge is removed.
84
+ ([Edge hooks](../../docs/relationships.md#edge-hooks),
85
+ [Owned targets](../../docs/relationships.md#owned-targets))
86
+ - Link entities with relationship edges, never with an `eid` or `Entity` kept in a component field.
87
+ A destroyed entity's eid is reused after `reuseWaitFrames` (16 frames by default, 0 for replicated
88
+ entities on a `ClientWorld`), so a stale eid can name an unrelated entity. A stored `Entity`
89
+ dangles, and snapshots copy an eid unchanged and reject an `Entity`.
90
+ ([Link with edges, not stored ids](../../docs/relationships.md#link-with-edges-not-stored-ids))
91
+ - Prefer `Exclusive` + `OwnedTarget` to a component field that holds a child entity. Removing or
92
+ replacing the edge destroys the child and destroying the child removes the edge, so no remove hook
93
+ is needed. ([Owned targets](../../docs/relationships.md#owned-targets))
94
+ - A `remove` hook also fires while its entity is being destroyed, with `entity.destroyed` already
95
+ `true`. A dying entity drops `add`, `set`, `remove`, `modified` and `attach` writes and `ensure`
96
+ throws, so guard a hook's writes to its own entity with `entity.destroyed`; that guard is
97
+ legitimate. ([Hooks](../../docs/components.md#hooks-component-and-relationship-edges))
98
+ - A `destroyed` check outside per-step event buffers and hooks on a dying entity or edge target
99
+ usually means a link that should be a relationship edge. Replace the link, not only add the check.
100
+ ([Link with edges, not stored ids](../../docs/relationships.md#link-with-edges-not-stored-ids))
101
+ - `@vworlds/vecs-phaser`: a `DrawBy` child turns with its parent only with `InheritRotation`. The
102
+ resolver derives a tagged child's `Rotation` every frame, so writing `Rotation` on it has no
103
+ effect; set `InheritRotation.angle` instead.
104
+ ([Turning with the parent](../../../vecs-phaser-client/README.md#turning-with-the-parent))
105
+ - `@vworlds/vecs-phaser`: `DrawBy` and `DrawIn` use `CleanupPolicy.Remove`, so destroying a display
106
+ parent detaches its children and leaves them alive, unlike `ChildOf`. Add `ChildOf` to the parent,
107
+ or make the child an owned target, when it must die with the parent.
108
+ ([Destroying the display parent](../../../vecs-phaser-client/README.md#destroying-the-display-parent))
109
+ - `@vworlds/vecs-physics` and `@vworlds/vecs-phaser-server`: pose `set` hooks and `update` systems
110
+ on the physics and render pose fire only when the pose changes. To visit every body every step,
111
+ iterate with `each`.
112
+ ([Pose updates](../../../vecs-physics/docs/systems-and-phases.md#pose-updates-fire-only-when-the-pose-changes))
113
+ - `@vworlds/vecs-client`: attach a `ClientWorld` socket only after `socket.connect()`, so no ack
114
+ precedes the data channel (`connectDgram` does this). Pending `invoke` calls reject with
115
+ `RPC_CLOSED_CODE` on disconnect or dispose.
116
+ ([Connecting](../../../vecs-client/docs/client-world.md#connecting),
117
+ [Disconnection and disposal](../../../vecs-client/docs/client-world.md#disconnection-and-disposal))
118
+ - `@vworlds/vecs-physics` components hold plain data only, never Box2D wrappers or other class
119
+ instances: snapshot capture rejects non-plain values.
120
+ ([Plain data](../../../vecs-physics/docs/components.md#keep-component-fields-plain-data))
121
+
68
122
  ## Implement And Verify
69
123
 
70
124
  1. Reuse existing components and modules where their semantics match; do not reuse them merely