@vworlds/vecs 1.0.49 → 1.0.51

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.
package/README.md CHANGED
@@ -59,6 +59,8 @@ world.progress(performance.now(), 16); // call once per frame
59
59
 
60
60
  ## Documentation
61
61
 
62
+ For Node clients that need replicated Phaser poses, see [Headless clients](../vecs-phaser-client/README.md#headless-clients).
63
+
62
64
  The full documentation lives in [`docs/`](./docs/README.md). Recommended reading order:
63
65
 
64
66
  1. [Getting started](./docs/getting-started.md) — install to first ticking world.
@@ -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.49",
3
+ "version": "1.0.51",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -27,12 +27,5 @@
27
27
  "type": "git",
28
28
  "url": "https://github.com/vworlds/vecs",
29
29
  "directory": "lib/vecs"
30
- },
31
- "lint-staged": {
32
- "*.ts": [
33
- "prettier --write",
34
- "eslint --fix"
35
- ],
36
- "*.md": "prettier --write"
37
30
  }
38
31
  }
@@ -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
@@ -35,13 +36,15 @@ Keep the [Glossary](./glossary.md) at hand for terminology.
35
36
 
36
37
  ## Map
37
38
 
39
+ For Node clients that need replicated Phaser poses, see [Headless clients](../../vecs-phaser-client/README.md#headless-clients).
40
+
38
41
  | Guide | Covers |
39
42
  | -------------------------------------------------- | -------------------------------------------------------------------------------------------- |
40
43
  | [getting-started.md](./getting-started.md) | A runnable tutorial from `npm install` to a ticking world. |
41
44
  | [concepts.md](./concepts.md) | The core vocabulary and how the pieces relate. |
42
45
  | [execution-model.md](./execution-model.md) | `world.progress`, the 8 built-in phases, deferred commands, flush boundaries, event routing. |
43
46
  | [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. |
47
+ | [entities.md](./entities.md) | Entity creation, mutation, relationship targets, lifecycle, disposal, deferred reads. |
45
48
  | [systems.md](./systems.md) | `world.system`, callbacks, injection, ordering, cadence, and tick sources. |
46
49
  | [queries-and-filters.md](./queries-and-filters.md) | `QueryDSL`, `Query`, `Filter`, `groupBy`, `ignoreSource`. |
47
50
  | [relationships.md](./relationships.md) | Relationships, target creation, projection and ownership, hierarchy, traversal, and cleanup. |
@@ -56,7 +59,9 @@ sessions, and the client world) lives in the `@vworlds/vecs-wire`, `@vworlds/vec
56
59
  `@vworlds/vecs-server`, and `@vworlds/vecs-client` packages:
57
60
 
58
61
  - [Remote procedure calls](../../vecs-protocol/docs/rpc.md) — the call channel and typed errors.
62
+ - [State diffs](../../vecs-protocol/docs/state-diffs.md) — what a diff carries, the order a client
63
+ applies it in, and how a reused entity id stays unambiguous.
59
64
  - [Sessions and caller context](../../vecs-server/docs/sessions.md) — per-connection RPC context,
60
65
  `closeSession`, and exactly-once teardown.
61
- - [Client world](../../vecs-client/docs/client-world.md) — replication, `invoke`/`listen`, and
62
- disposal.
66
+ - [Client world](../../vecs-client/docs/client-world.md) — replication, render time,
67
+ `invoke`/`listen`, and disposal.
@@ -69,8 +69,22 @@ 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 `ServerWorld` allocates its replicated entities from the `entity` pool,
77
+ and holds a freed id for at least `historyFrames + 1` frames (65 by default) whatever the wait
78
+ asks for, so the frame that removed an entity has left every session's history before its id comes
79
+ back (see [Reusing entity ids](../../vecs-server/docs/sessions.md#reusing-entity-ids)). The
80
+ `networkEntityReuseWaitFrames` option of the networked worlds sets the wait of the pool that holds
81
+ the replicated entities: `entity` for a server, `network_entity` for a client. A diff that removes
82
+ an entity and snapshots a new one under the same id is read in a fixed order, which is how a client
83
+ tells the two apart (see [State diffs](../../vecs-protocol/docs/state-diffs.md)). A recycled id is
84
+ why a stored id is not a safe link between entities
85
+ (see [Link with edges, not stored ids](./relationships.md#link-with-edges-not-stored-ids)).
86
+ `world.peekNextEid(poolName)` returns the next id a pool would allocate without reserving it. Id 0
87
+ is reserved.
74
88
 
75
89
  ## Hooks: component and relationship edges
76
90
 
@@ -103,6 +117,26 @@ command is applied (see [Execution model](./execution-model.md#reactive-routing)
103
117
  resource management and cross-cutting reactions that aren't worth a system; use systems when the
104
118
  reaction belongs to a phase.
105
119
 
120
+ `remove` also fires when the owning entity is destroyed, and `entity.destroyed` is already `true`
121
+ inside the hook. A dying entity takes no writes: `add`, `set`, `remove`, `modified` and `attach`
122
+ are dropped without an error, and `ensure` throws. Releasing a resource needs no check. A hook that
123
+ also writes to its own entity must skip the write when `entity.destroyed`, and that guard is the
124
+ right one here (relationship hooks follow the same rule, see
125
+ [Relationships — edge hooks](./relationships.md#edge-hooks)):
126
+
127
+ ```ts
128
+ class Shield {}
129
+ class Stats {
130
+ armor = 0;
131
+ }
132
+
133
+ world.component(Shield).on("remove", (entity) => {
134
+ if (!entity.destroyed) {
135
+ entity.ensure(Stats, (stats) => (stats.armor -= 10)); // throws on a dying entity
136
+ }
137
+ });
138
+ ```
139
+
106
140
  Multiple handlers may be registered for the same event: they fire in registration
107
141
  order. Call `on(event, handler)` to append, `on(event, handler, true)` to
108
142
  prepend, and `off(event, handler)` to remove a handler by reference. All three
@@ -129,6 +163,7 @@ world.component(Orange).set(Implements, { interfaces: [Fruit] });
129
163
  | `Traversable` | Exclusive relationships | Maintains hierarchy depth for cascade ordering |
130
164
  | `OwnedTarget` | Exclusive relationships other than `ChildOf` | Owns each explicit target through `ChildOf` and destructive edge replacement |
131
165
  | `OneOf` | Relationships | Constrains each target to be a direct `ChildOf` child of a declared parent |
166
+ | `With` | A component and a target of the same kind | Adding the component also adds the target (the same targets, for edges) |
132
167
 
133
168
  Traits declare policy; their built-in modules install the hooks and maintain the metadata that
134
169
  enforce it. Configure traits in feature-module setup rather than opportunistically inside systems.
@@ -142,6 +177,11 @@ Do not assume all traits have the same lifecycle:
142
177
  `ChildOf` child of the declared parent.
143
178
  - `Traversable` backfills existing relationship instances.
144
179
  - Setting or removing `Implements` recomputes interface aliases.
180
+ - Add `With` before the component is in use, as `world.component(X).add(With, world.component(Y))`.
181
+ It is irrevocable, both must be plain components or both relationships, and it only brings `Y`
182
+ along: an existing `Y` is left as it is, and removing `X` does not remove `Y`. On a `ClientWorld`,
183
+ a network removal of `Y` is skipped while `X` is on the entity (see
184
+ [Applying a frame](../../vecs-client/docs/client-world.md#applying-a-frame)).
145
185
 
146
186
  Treat traits as setup policy unless their documentation explicitly supports runtime changes. Before
147
187
  writing custom component-type bookkeeping, check whether an existing trait already expresses the
@@ -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
 
@@ -176,6 +177,7 @@ world.component(Orange).set(Implements, { interfaces: [Fruit] });
176
177
  | `Traversable` | An exclusive relationship hierarchy needs depth or cascade ordering |
177
178
  | `OwnedTarget` | An exclusive relationship owns its explicitly added target |
178
179
  | `OneOf` | A relationship's targets must be direct children of a declared parent (enum-style) |
180
+ | `With` | Adding one component should bring another along, as a companion it does not own |
179
181
 
180
182
  Traits declare policy; modules implement it through hooks and metadata. Configure them in module
181
183
  setup, not inside systems. Lifecycle differs by trait: `OwnedTarget` and `Singleton` are setup-time
@@ -183,6 +185,18 @@ policy, `Traversable` backfills, and `Implements` recomputes declarations. Do no
183
185
  dynamically removable. See the [Components guide](./components.md#component-traits-policy-on-component-entities)
184
186
  for the full model and custom-trait guidance.
185
187
 
188
+ `With` is a pair-form trait: `world.component(Motion).add(With, world.component(Position))` adds a
189
+ `Position` whenever a `Motion` is added. It only brings the companion along: removing `Motion` does
190
+ not remove `Position`, and a `Position` the entity already holds is left as it is. A networked
191
+ client has one more rule, because the server decides what a client replicates but the client decides
192
+ what it derives. **`ClientWorld.apply` skips a network removal of a component that another
193
+ component present on the entity brings through `With`**, so a server can swap the source of a
194
+ client-local component in one diff, say a streamed `Position` for a dead-reckoned `Motion`, and the
195
+ entity never goes without it: no `remove` hook fires, and the queries and `DrawBy` children that
196
+ match on it never see it leave. A bringer that the same diff sets counts as present. The entity-destroy
197
+ path is unaffected, and a relationship is removed as sent (see
198
+ [Applying a frame](../../vecs-client/docs/client-world.md#applying-a-frame)).
199
+
186
200
  ### Exclusive component groups
187
201
 
188
202
  `world.setExclusiveComponents(A, B, C)` declares a mutually exclusive set: setting one
@@ -268,6 +282,23 @@ classes or DSL objects (`{ all: [...] }`, `{ any: [...] }`, `{ parent: X }`, `{
268
282
  > `.with(...)`, `.without(...)`, and `.update(C)` own membership. `.update(C)` watches change events
269
283
  > and also requires `C`, so `.with(A).update(C)` matches entities carrying both `A` and `C`.
270
284
 
285
+ **`.without` is not free.** `.without(X)` is `.with({ not: X })`: a `not` term holds every live
286
+ entity that lacks `X`, a world-sized set, and the query intersects it with its positive terms. The
287
+ exclusion does not run over the positive terms' candidates. Vecs seeds that set with one scan of
288
+ the world when the term is first built, then adds each new entity to it, removes each destroyed
289
+ entity from it, and flips an entity in or out when `X` is added to it or removed from it. Changes
290
+ to `X`'s data cost nothing. The term is shared by every query that excludes the same `X`, so the
291
+ upkeep is paid once per distinct exclusion, plus one membership check per query that uses it.
292
+
293
+ - Add `.without(X)` only when the exclusion is needed, and put positive terms next to it: they keep
294
+ the query's members and callbacks small. A query made only of `.without` terms tracks every
295
+ entity in the world.
296
+ - The creation pattern `.with(NewShape).without(Phaser.GameObjects.NewKind).enter(...)` relies on
297
+ that narrowing. The positive term limits the members to the few entities that just got
298
+ `NewShape`, while the exclusion keeps those that already have a display object from entering.
299
+
300
+ Details: [Queries and filters](./queries-and-filters.md#the-cost-of-not-and-without).
301
+
271
302
  ### Reactive callbacks: `enter` / `update` / `exit`
272
303
 
273
304
  - **`enter`** — fires once when an entity _becomes_ a member. Use it for one-time setup
@@ -281,6 +312,14 @@ classes or DSL objects (`{ all: [...] }`, `{ any: [...] }`, `{ parent: X }`, `{
281
312
  Its injected components are read from a **snapshot captured at exit time**, so they're still
282
313
  resolvable even though they're being removed.
283
314
 
315
+ **Repeated writes in one tick.** Vecs never merges `update` events, so a callback fires once per
316
+ routed write. `entity.modified(C)` and `entity.getMut(C)` coalesce through the entity's dirty bit:
317
+ repeated calls before the queued command drains give one `update`. Each `entity.set(C, …)` queues
318
+ its own command and routes its own `update`, so three `set` calls give three callbacks. Write in
319
+ place plus `modified` (or `getMut`) to get one callback per tick, or compare before `set`. When
320
+ producers already use `modified`/`getMut`, do not add a defensive per-tick dedupe or batching layer
321
+ on the consumer side. See [Systems](./systems.md#repeated-writes-in-one-tick).
322
+
284
323
  Choose `onEnter: true` by asking who owns the initial value:
285
324
 
286
325
  - Use **`onEnter: true`** for projection / sync callbacks: copying ECS data into a runtime object,
@@ -338,7 +377,8 @@ Components listed in the inject tuple but **not** guaranteed by `.with(...)` res
338
377
  ### `each` vs reactive — don't scan when you can react
339
378
 
340
379
  - **`.each([...], cb)`** iterates **every matching entity every tick**. Use it only when you
341
- genuinely need a per-frame sweep over all members.
380
+ genuinely need a per-frame sweep over all members. An `.each` must justify running every tick: a
381
+ body that returns early for most members is a reactive system or a narrower query in disguise.
342
382
  - **Reactive `enter`/`update`/`exit`** fire only on change. For "copy A→B when A changes,"
343
383
  reactive `.update(A)` touches _nothing_ on idle entities, where `.each` would scan them all.
344
384
 
@@ -350,6 +390,28 @@ Components listed in the inject tuple but **not** guaranteed by `.with(...)` res
350
390
  .with(PhysicsPosition, RenderPosition).update(PhysicsPosition, (e, p) => e.set(RenderPosition, { x: p.x, y: p.y }));
351
391
  ```
352
392
 
393
+ When the work really is per-tick (a held input, a cooldown, a charge), narrow the query with a tag
394
+ or component that exists **only while the work is live**, instead of sweeping every candidate and
395
+ returning early. A reactive `update` on the intent adds and removes the marker:
396
+
397
+ ```ts
398
+ // ❌ visits every shooter every tick; most return at once
399
+ .with(Weapon).each([Weapon], (e, [w]) => { if (!e.has(TriggerHeld)) return; /* fire */ });
400
+
401
+ // ✅ the cadence system matches only entities that are firing right now
402
+ world.system("TriggerIntent").with(Weapon).update(FireIntent, (e, intent) =>
403
+ intent.held ? e.add(Firing) : e.remove(Firing));
404
+ world.system("Fire").with(Weapon, Firing).interval(0.1).each([Weapon], (e, [w]) => { /* fire */ });
405
+ ```
406
+
407
+ For a countdown, do not write a bespoke "tick every timer" system over all entities. Vecs has no
408
+ per-entity timer component: `interval`, `rate` and `tickSource` (see below) throttle _systems_.
409
+ Model the window as an owned helper entity, an `Exclusive` + `OwnedTarget` relationship target that
410
+ carries the deadline (see [Owned target entities](#owned-target-entities-entity-valued-properties)).
411
+ Its presence is the window: an `.each` over the helper entities visits only open windows, and
412
+ destroying a helper ends its window and removes the edge through the relationship's cleanup policy,
413
+ so no remove hook or flag is needed.
414
+
353
415
  ### `run` and `interval`
354
416
 
355
417
  - **`.run(cb)`** — a system with no per-entity query; the body runs once per tick. For
@@ -702,6 +764,11 @@ The lifecycle follows from the relationship and trait:
702
764
  | `player.remove(Inventory)` | Destroys every current target |
703
765
  | Destroy `player` | `ChildOf` destroys the inventory |
704
766
 
767
+ Prefer this to a component field that holds the child entity, with an `onRemove` hook that destroys
768
+ it. Every operation that drops the edge destroys the child, and destroying the child directly
769
+ removes the edge, so the owner needs no hook and holds nothing that can go stale (see
770
+ [Link with edges, not stored ids](./relationships.md#link-with-edges-not-stored-ids)).
771
+
705
772
  Inside deferred execution, keep and mutate the target you created; the queued edge is not visible
706
773
  through `target` until flush:
707
774
 
@@ -749,6 +816,9 @@ system after whatever produces the entity/companion.
749
816
  | A god component with many optional fields | Split into small single-purpose components; absence encodes "not applicable." |
750
817
  | A large nested `inventory`, `brain`, or progression object | Use an owned target entity when the subobject needs its own components, systems, and lifecycle. |
751
818
  | A side `Map<ownerEid, SubsystemState>` | Create a target entity and add an explicit edge; use `Exclusive` + `OwnedTarget` only for destructive ownership. |
819
+ | 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)). |
820
+ | 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)). |
821
+ | `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)). |
752
822
  | `OwnedTarget` on `CurrentEnemy`, `InCell`, `Team`, or shared state | Use a normal relationship; replacing or removing an owned edge destroys its target. |
753
823
  | One owned target used as a collection | Use multiple child/item entities; owned targets model zero-or-one aggregates. |
754
824
  | Duplicated `enter` + `update` doing the same thing | One `update({ watch: C, onEnter: true })` when update owns initialization. |
@@ -774,11 +844,28 @@ system after whatever produces the entity/companion.
774
844
  replicate a networked component. A writer that runs every tick and copies a value into a networked
775
845
  or watched component (simulation output, a derived pose) should compare first and write only when
776
846
  the value changed (see §7). An in-place assignment without `modified` notifies nobody.
847
+ - **`getMut` marks the component modified even without a callback and even if you write nothing.**
848
+ Calling it every tick fires `onSet`, `update` watchers and replication every tick. Read with `get`;
849
+ call `getMut` only when a change is intended (see
850
+ [`getMut` and `ensure`](./entities.md#getmut-and-ensure-mutating-in-place)).
777
851
  - **A callback that always uses a component should inject it, not `get` it.** `entity.get(C)` is for
778
852
  a component that is conditionally needed and rarely required (see §4).
779
853
  - **Membership is fixed by `.with(...)` / `.without(...)` / `.update(C)`;** `update(C)` watches `C`
780
854
  and requires it.
855
+ - **A system's `enter` reaches only entities still in its query when the system drains.** One that
856
+ left or died first gets neither `enter` nor the paired `exit`, and one that leaves and re-enters
857
+ within a drain gets `enter`, `exit`, `enter`, so keep `enter` setup and `exit` teardown symmetric
858
+ (see [`.enter`](./systems.md#entercallback--enterinject-callback--enteroptions-callback)).
781
859
  - **`exit` retains injected references captured at exit** (components are not deep-cloned).
860
+ - **A `remove` hook also fires while the entity is being destroyed,** with `entity.destroyed`
861
+ already `true`. A dying entity takes no writes (`add`, `set`, `remove`, `modified` and `attach` are
862
+ dropped, `ensure` throws), so a hook that writes to its own entity guards on `entity.destroyed`.
863
+ That guard is legitimate; elsewhere a `destroyed` check usually signals a missing edge (see §8 and
864
+ [Hooks](./components.md#hooks-component-and-relationship-edges)).
865
+ - **Release resources in `remove` hooks, not in a system's `exit` callback.** `world.dispose()`
866
+ runs the hooks during the call, but a system's `exit` callback only queues its event, and a
867
+ disposed world never runs the system again (see
868
+ [Disposing a world](./entities.md#disposing-a-world)).
782
869
  - **Within a phase, order = registration order;** across phases, the spine guarantees order. Don't
783
870
  use registration order for cross-module correctness — use phases.
784
871
  - **Networked component order = wire protocol.** Reordering breaks the protocol.
@@ -789,6 +876,9 @@ system after whatever produces the entity/companion.
789
876
  ordering; `hasTarget(R, target)` tests one edge.
790
877
  - **Relationship edges are explicit.** Add with `add(R, target)`, remove one with
791
878
  `remove(R, target)`, and remove all with `remove(R)`. Do not use `set` for target storage.
879
+ - **Relationship removal hooks differ in scope.** `on("targetRemove")` fires once per removed edge;
880
+ `on("remove")` fires once, when the relationship component itself leaves, with no targets left on
881
+ the instance (see [Edge hooks](./relationships.md#edge-hooks)).
792
882
  - **Relationship cardinality and component exclusivity are different.** Add the `Exclusive` trait
793
883
  to a relationship for at-most-one target; use `setExclusiveComponents` for mutually exclusive
794
884
  component types.
@@ -810,6 +900,42 @@ system after whatever produces the entity/companion.
810
900
  explicitly, and during deferred execution keep that entity instead of re-reading `target(R)`
811
901
  before flush.
812
902
  - **`world.module(M)` runs `init` once.** Re-registering with a different config throws.
903
+ - **vecs-phaser: a `DrawBy` child follows only its parent's `Position`.** It turns with the parent
904
+ only with `InheritRotation`, which makes the resolver derive its `Rotation` every frame, so
905
+ writing `Rotation` on a tagged child has no effect (see
906
+ [Turning with the parent](../../vecs-phaser-client/README.md#turning-with-the-parent)).
907
+ - **vecs-phaser: `DrawBy` and `DrawIn` use `CleanupPolicy.Remove`,** so destroying a display parent
908
+ detaches its children instead of destroying them, unlike `ChildOf`, which deletes them. A display
909
+ child that must die with its parent also needs `ChildOf` to it, or must be its owned target (see
910
+ [Destroying the display parent](../../vecs-phaser-client/README.md#destroying-the-display-parent)).
911
+ - **vecs-physics, vecs-phaser-server: pose writes are change-only.** `set` hooks and `update` systems
912
+ on the physics and render pose fire when the pose changed, not on every step; to visit every body
913
+ every step, iterate with `each` (see
914
+ [Pose Updates Fire Only When The Pose Changes](../../vecs-physics/docs/systems-and-phases.md#pose-updates-fire-only-when-the-pose-changes)).
915
+ - **vecs-phaser-server: game code owns the physics pose; the render pose is protocol-private.** On
916
+ the server, read and write the physics `Position` and `Rotation` of every entity, including one
917
+ without a `Body`. The render `Position` and `Rotation` and the dead-reckoning `Motion` and
918
+ `MotionParams` are wire encodings of that pose that only the sync systems write: a `DeadReckoned`
919
+ body has no render pose at all, and adding one throws (see
920
+ [Dead reckoning](../../vecs-phaser-server/README.md#dead-reckoning)).
921
+ - **vecs-phaser-server: switch `DeadReckoned` on a change of state, and keep a constantly driven body
922
+ streamed.** The tag may be added or removed at any time, so a body can be dead reckoned while it
923
+ drifts and streamed while joints, thrust or a tow drive it (a driven body diverges from its
924
+ prediction every step and would cost a baseline of about 27 bytes per step against about 22 for a
925
+ streamed pose update). Toggle it when the state changes, not every frame: each switch costs a
926
+ baseline one way and a pose update the other, and clients blend the hand-off over `blendMs`. A
927
+ render pose or `Motion` created or switched late in a frame, in `PRE_STORE`, `ON_STORE` or an RPC
928
+ handler, is still synced from the physics pose before the frame is recorded (see
929
+ [Switching at runtime](../../vecs-phaser-server/README.md#switching-at-runtime)).
930
+ - **vecs-client: attach a `ClientWorld` socket only after `socket.connect()` resolves,** so no ack
931
+ precedes the data channel (`connectDgram` does this). Pending `invoke` calls reject with
932
+ `RPC_CLOSED_CODE` on disconnect or `dispose` (see
933
+ [Connecting](../../vecs-client/docs/client-world.md#connecting) and
934
+ [Disconnection and disposal](../../vecs-client/docs/client-world.md#disconnection-and-disposal)).
935
+ - **vecs-physics: component fields hold plain data,** never Box2D wrappers or other class
936
+ instances. `set` accepts them, but snapshot capture (`snapshotSubtree`, `moveSubtree`) rejects
937
+ non-plain values (see
938
+ [Keep Component Fields Plain Data](../../vecs-physics/docs/components.md#keep-component-fields-plain-data)).
813
939
 
814
940
  ---
815
941
 
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
 
@@ -44,6 +44,31 @@ evaluates it against each entity pointing at the candidate and also matches when
44
44
  queries and systems. `target` / `parent` are fully reactive to edge changes and target-side changes
45
45
  — see [Relationships](./relationships.md#querying-through-relationships).
46
46
 
47
+ ### The cost of `not` and `.without`
48
+
49
+ `.without(X)` is `.with({ not: X })` (`lib/vecs/src/query/query.ts`, `without`), and `.with` merges
50
+ into `{ all: [...] }`. A `not` term is built from the world term plus `X`'s term and holds every live
51
+ entity that lacks `X` (`lib/vecs/src/terms/not_term.ts`). It is a world-sized set, independent of
52
+ the query's positive terms, and the `all` term intersects it with them.
53
+
54
+ | Event | Effect on a `not X` term |
55
+ | ----------------------------------- | ----------------------------------------------------------------------------- |
56
+ | Term built (first use) | One scan of the whole world seeds the set (`term.ts`, `_initializeFromWorld`) |
57
+ | Entity created or destroyed | Added to or removed from the set (`world_term.ts`, `entityCreated`) |
58
+ | `X` added to or removed from entity | The entity leaves or re-enters the set |
59
+ | `X` data modified | Nothing (`not_term.ts`, `onTermRefresh` is a no-op) |
60
+
61
+ Terms are cached by canonical key, so the upkeep is paid once per distinct exclusion, not once per
62
+ query. Each query that uses it adds one membership check per change of the set, and the positive
63
+ term comes first, so most checks stop there.
64
+
65
+ - Use `.without` only when the exclusion is needed, and pair it with positive terms that keep the
66
+ query's members small. A query with only `.without` terms tracks every entity in the world, so
67
+ its `enter` fires for each created entity.
68
+ - The creation pattern `.with(NewShape).without(Phaser.GameObjects.NewKind).enter(...)` depends on
69
+ this: the positive term limits the members, and the exclusion drops the few that already hold the
70
+ object.
71
+
47
72
  ### `QuerySpec`: adding `cascade` and `hint`
48
73
 
49
74
  Anywhere a DSL is accepted, you can pass the object form
@@ -33,6 +33,36 @@ 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, `0` for replicated entities on a
44
+ `ClientWorld`, and at least `historyFrames + 1` on a `ServerWorld` (see
45
+ [Id pools](./components.md#id-pools)). Allocation hands out free ids before
46
+ never-used ones, so one of the next entities created takes it. Until an entity does,
47
+ `world.getEntity(id)` returns `undefined`; from then on it returns that live, unrelated entity and
48
+ the stale id looks valid.
49
+ - **A stored `Entity` dangles.** Reuse never makes it name another entity, because a destroyed
50
+ `Entity` stays `destroyed`, but nothing clears the field when the entity dies. Writes through it
51
+ are dropped and `ensure` throws.
52
+ - **An edge is cleaned up for you.** When the target is destroyed, `onDeleteTarget` removes the edge
53
+ or destroys the source (see [Cleanup when a target dies](#cleanup-when-a-target-dies)). The
54
+ reverse index (`children`), `target` and `parent` queries, `groupBy` and network replication work
55
+ on edges only.
56
+ - **A field does not cross worlds.** `snapshotSubtree` and `moveSubtree` remap relationship targets
57
+ onto the destination's new ids. An eid field is copied as is and names the wrong entity there; an
58
+ `Entity` field makes capture throw (see [Snapshot policies](./snapshots.md#snapshot-policies)).
59
+
60
+ A `destroyed` check on an entity you looked up from a stored link is therefore a symptom of a
61
+ missing edge: replace the link, not only the check. The guard stays right where a reference is
62
+ deliberately short-lived: payloads in per-step event buffers (the `other` entity in `vecs-physics`
63
+ contact events), and hooks that run while an entity or an edge target is being destroyed (see
64
+ [Edge hooks](#edge-hooks) and [component hooks](./components.md#hooks-component-and-relationship-edges)).
65
+
36
66
  ## Built-in relationships
37
67
 
38
68
  | Class | Purpose |
@@ -192,6 +222,26 @@ calls `onRemove`. An exclusive replacement calls `onTargetRemove` for the old ta
192
222
  relationship-dependent query updates, but do not call `onSet`; `onSet` is reserved for ordinary
193
223
  relationship data changed through `set`.
194
224
 
225
+ Pick the removal hook by scope:
226
+
227
+ - `on("targetRemove", handler)` runs once for **each** removed edge: `remove(R, target)`, the
228
+ displaced edge of an `Exclusive` replacement, and every edge when the whole relationship goes away
229
+ (the final edge, `remove(R)`, or destroying the source).
230
+ - `on("remove", handler)` runs **once**, when the relationship component itself leaves the entity.
231
+ It does not run when one of several edges is removed or an `Exclusive` edge is replaced. The
232
+ relationship instance it receives holds no targets any more, so use `targetRemove` to learn which
233
+ targets went.
234
+
235
+ When the whole relationship goes away, `source.has(R)` is already `false` inside both hooks. When
236
+ only one of several edges goes, `source.has(R)` stays `true` and `source.hasTarget(R, removed)` is
237
+ `false`.
238
+
239
+ Destroying either end of an edge runs these hooks too, so `source.destroyed` or `target.destroyed`
240
+ can already be `true` inside them; both are under `Delete` cleanup. The target of an `OwnedTarget`
241
+ edge is destroyed before the public hook runs on every path. A dying end takes no writes (see
242
+ [Hooks](./components.md#hooks-component-and-relationship-edges)), so a handler that writes to
243
+ `source` or `target` guards on `destroyed`.
244
+
195
245
  Register handlers with `on("targetAdd", handler)` and `on("targetRemove", handler)`; remove them
196
246
  with `off(event, handler)`. Multiple handlers may be registered for the same event and fire in
197
247
  registration order. Framework traits install separate internal `targetPolicyAdd` /
@@ -227,6 +277,11 @@ attached: removing it throws, and only world teardown (`clearAllEntities`) bypas
227
277
  the same contract as `Traversable` and `OneOf`. Do not share an owned target between
228
278
  sources because either owner can destroy it when its edge is removed.
229
279
 
280
+ An owned target replaces the field-plus-hook pattern, where a component field holds the child
281
+ entity and an `onRemove` hook destroys it. The edge is the field: every path that drops it destroys
282
+ the child, and destroying the child directly removes the edge from its owner. There is no hook to
283
+ write and no stale reference to guard.
284
+
230
285
  ## Cleanup when a target dies
231
286
 
232
287
  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
@@ -44,6 +44,7 @@ the full query DSL (operator semantics in
44
44
  .with({ parent: Body }) // ChildOf target has Body
45
45
  ```
46
46
 
47
+ `.without` is not free: see [its cost](./queries-and-filters.md#the-cost-of-not-and-without).
47
48
  Multiple `with` calls merge with `all`. The `source` / `children` operators are non-reactive and
48
49
  rejected by systems — they are filter-only (see
49
50
  [Queries and filters](./queries-and-filters.md#filters-one-shot-scans)).
@@ -89,10 +90,15 @@ Fires once when an entity starts matching the system:
89
90
  .enter({ inject: [Position, Sprite] }, (e, [pos, sprite]) => sprite.setPosition(pos.x, pos.y))
90
91
  ```
91
92
 
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.
93
+ The callback runs only if the entity is still a member when this system drains its inbox, at the top
94
+ of its next run. The inbox replays in arrival order, so:
95
+
96
+ - An entity that left the query or was destroyed first gets no `enter`, and in a system that
97
+ registers both callbacks no paired `exit` either. Do not add a `destroyed` guard to normal `enter`
98
+ setup code. A system with only `exit` has no `enter` to pair, so it still gets that `exit`.
99
+ - An entity that leaves and re-enters before the drain is a member again, so the system gets
100
+ `enter`, `exit`, `enter` (just `exit`, `enter` when it had already entered in an earlier drain).
101
+ Keep `enter` setup and `exit` teardown symmetric so the second `enter` starts from a clean state.
96
102
 
97
103
  For systems, injected values and `Iter.src` are captured when entry is routed and replayed from that
98
104
  snapshot when the system next drains its inbox.
@@ -126,6 +132,25 @@ Use `.update(C, ...)` by itself when `C` is the membership shape and watched val
126
132
  A query or system can register only one `update(C, ...)` callback for each component. A duplicate
127
133
  registration throws instead of replacing the existing callback.
128
134
 
135
+ #### Repeated writes in one tick
136
+
137
+ Each routed write is one `update` event, and a system's inbox replays its events in arrival order
138
+ without merging any (`lib/vecs/src/system.ts`, `_pushUpdate` and the drain loop in
139
+ `_runDeferred`). What differs is how many events a write routes:
140
+
141
+ | Write | Events routed for `C` in one drain window |
142
+ | -------------------------- | ------------------------------------------------------------------------------------------------ |
143
+ | `entity.set(C, …)` × n | n: every call queues its own command and fires its own `onSet` and `update` |
144
+ | `entity.modified(C)` × n | 1: the first call sets the dirty bit, the command clears it when it runs, later calls are no-ops |
145
+ | `entity.getMut(C, cb)` × n | 1: it marks modified through the same dirty bit |
146
+
147
+ Coalescing needs a queued command, so it applies to calls made while commands are deferred (inside
148
+ system callbacks); outside them `modified` applies immediately. To get one callback per tick, write
149
+ in place and call `modified` (or use `getMut`), or compare before `set` and skip equal values. Do
150
+ not add a per-tick dedupe or batching step in the consumer when producers already use
151
+ `modified`/`getMut`. Source: `lib/vecs/src/entity/entity.components.ts` (`_markModified`, `set`,
152
+ `_notifyRaw`).
153
+
129
154
  ### `.exit(callback)` / `.exit(inject, callback)` / `.exit(options, callback)`
130
155
 
131
156
  Fires when an entity stops matching (component removed, or entity destroyed). Directly injected
@@ -144,7 +169,9 @@ object remain visible:
144
169
  ### `.each(components, callback)` / `.each(options, callback)`
145
170
 
146
171
  Fires every tick the system runs, once per tracked entity, regardless of change. Use it only for
147
- genuinely per-frame sweeps — when reacting to change suffices, prefer `update` (see
172
+ genuinely per-frame sweeps — when reacting to change suffices, prefer `update`. An `.each` must
173
+ justify running every tick, and when it does, narrow `.with(...)` to a tag that exists only while
174
+ the work is live (see
148
175
  [Design guide](./design-guide.md#each-vs-reactive--dont-scan-when-you-can-react)):
149
176
 
150
177
  ```ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vworlds/vecs",
3
- "version": "1.0.49",
3
+ "version": "1.0.51",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -27,12 +27,5 @@
27
27
  "type": "git",
28
28
  "url": "https://github.com/vworlds/vecs",
29
29
  "directory": "lib/vecs"
30
- },
31
- "lint-staged": {
32
- "*.ts": [
33
- "prettier --write",
34
- "eslint --fix"
35
- ],
36
- "*.md": "prettier --write"
37
30
  }
38
31
  }
@@ -40,6 +40,10 @@ Before implementing, identify:
40
40
  - The state represented by each component and whether absence has meaning.
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
+ An `.each` must justify running every tick; when it does, narrow its query with a tag that exists
44
+ only while the work is live (for example a `Firing` tag that a reactive `update` on the intent
45
+ adds and removes) instead of looping over every candidate and returning early.
46
+ ([`each` vs reactive](../../docs/design-guide.md#each-vs-reactive--dont-scan-when-you-can-react))
43
47
  - Which phase owns the work and what earlier or later phase it must order against.
44
48
  - Whether a multi-target relationship, an `Exclusive` relationship, an explicitly created owned
45
49
  target, a grouped query, or an attached resource expresses the lifecycle better than imperative
@@ -65,6 +69,69 @@ Check the design guide before introducing:
65
69
  If the requested design conflicts with a documented vecs invariant, explain the conflict and use
66
70
  the documented pattern unless the user explicitly chooses otherwise.
67
71
 
72
+ ## Known Gotchas
73
+
74
+ Agents trip on these vecs behaviors repeatedly. Design guide sections 8 and 9 list them with the
75
+ other anti-patterns and pitfalls; each rule below links to the detail.
76
+
77
+ - Do not call `getMut` per tick unless a change is intended. It marks the component modified even
78
+ without a callback and even if nothing is written, which fires `update` watchers and replication.
79
+ Read with `get`. ([`getMut`](../../docs/entities.md#getmut-and-ensure-mutating-in-place))
80
+ - `.without(X)` does not filter the positive terms' candidates: it keeps a world-sized "not X" set,
81
+ updated on every entity create and destroy and every `X` add or remove, and intersects it with
82
+ them. Use it only when needed, after positive terms that keep the members small; a query with only
83
+ `.without` terms tracks the whole world.
84
+ ([Cost of `.without`](../../docs/queries-and-filters.md#the-cost-of-not-and-without))
85
+ - `entity.modified(C)` and `getMut(C)` coalesce into one `update` per drain; every `entity.set(C, …)`
86
+ routes its own, and the system inbox never merges events. Write in place plus `modified`, or
87
+ compare before `set`, and do not add per-tick dedupe batches when producers use `modified`/`getMut`.
88
+ ([Repeated writes](../../docs/systems.md#repeated-writes-in-one-tick))
89
+ - A system's `enter` reaches only entities still in its query when the system drains. An entity
90
+ that left the query or died first gets neither `enter` nor the paired `exit`, and a re-entry
91
+ within one drain gives `enter`, `exit`, `enter`, so keep setup and teardown symmetric.
92
+ ([`.enter`](../../docs/systems.md#entercallback--enterinject-callback--enteroptions-callback))
93
+ - Create relationship edges with `add(Rel, target)` and read them with `target()` / `targets()`.
94
+ React to the relationship component being removed with `on("remove")` and to each removed edge
95
+ with `on("targetRemove")`. `OwnedTarget` needs `Exclusive` and an explicitly created target, and
96
+ destroys that target when its edge is removed.
97
+ ([Edge hooks](../../docs/relationships.md#edge-hooks),
98
+ [Owned targets](../../docs/relationships.md#owned-targets))
99
+ - Link entities with relationship edges, never with an `eid` or `Entity` kept in a component field.
100
+ A destroyed entity's eid is reused after `reuseWaitFrames` (16 frames by default, 0 for replicated
101
+ entities on a `ClientWorld`), so a stale eid can name an unrelated entity. A stored `Entity`
102
+ dangles, and snapshots copy an eid unchanged and reject an `Entity`.
103
+ ([Link with edges, not stored ids](../../docs/relationships.md#link-with-edges-not-stored-ids))
104
+ - Prefer `Exclusive` + `OwnedTarget` to a component field that holds a child entity. Removing or
105
+ replacing the edge destroys the child and destroying the child removes the edge, so no remove hook
106
+ is needed. ([Owned targets](../../docs/relationships.md#owned-targets))
107
+ - A `remove` hook also fires while its entity is being destroyed, with `entity.destroyed` already
108
+ `true`. A dying entity drops `add`, `set`, `remove`, `modified` and `attach` writes and `ensure`
109
+ throws, so guard a hook's writes to its own entity with `entity.destroyed`; that guard is
110
+ legitimate. ([Hooks](../../docs/components.md#hooks-component-and-relationship-edges))
111
+ - A `destroyed` check outside per-step event buffers and hooks on a dying entity or edge target
112
+ usually means a link that should be a relationship edge. Replace the link, not only add the check.
113
+ ([Link with edges, not stored ids](../../docs/relationships.md#link-with-edges-not-stored-ids))
114
+ - `@vworlds/vecs-phaser`: a `DrawBy` child turns with its parent only with `InheritRotation`. The
115
+ resolver derives a tagged child's `Rotation` every frame, so writing `Rotation` on it has no
116
+ effect; set `InheritRotation.angle` instead.
117
+ ([Turning with the parent](../../../vecs-phaser-client/README.md#turning-with-the-parent))
118
+ - `@vworlds/vecs-phaser`: `DrawBy` and `DrawIn` use `CleanupPolicy.Remove`, so destroying a display
119
+ parent detaches its children and leaves them alive, unlike `ChildOf`. Add `ChildOf` to the parent,
120
+ or make the child an owned target, when it must die with the parent.
121
+ ([Destroying the display parent](../../../vecs-phaser-client/README.md#destroying-the-display-parent))
122
+ - `@vworlds/vecs-physics` and `@vworlds/vecs-phaser-server`: pose `set` hooks and `update` systems
123
+ on the physics and render pose fire only when the pose changes. To visit every body every step,
124
+ iterate with `each`.
125
+ ([Pose updates](../../../vecs-physics/docs/systems-and-phases.md#pose-updates-fire-only-when-the-pose-changes))
126
+ - `@vworlds/vecs-client`: attach a `ClientWorld` socket only after `socket.connect()`, so no ack
127
+ precedes the data channel (`connectDgram` does this). Pending `invoke` calls reject with
128
+ `RPC_CLOSED_CODE` on disconnect or dispose.
129
+ ([Connecting](../../../vecs-client/docs/client-world.md#connecting),
130
+ [Disconnection and disposal](../../../vecs-client/docs/client-world.md#disconnection-and-disposal))
131
+ - `@vworlds/vecs-physics` components hold plain data only, never Box2D wrappers or other class
132
+ instances: snapshot capture rejects non-plain values.
133
+ ([Plain data](../../../vecs-physics/docs/components.md#keep-component-fields-plain-data))
134
+
68
135
  ## Implement And Verify
69
136
 
70
137
  1. Reuse existing components and modules where their semantics match; do not reuse them merely