@colyseus/schema 4.0.19 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/build/Metadata.d.ts +55 -2
- package/build/Reflection.d.ts +24 -30
- package/build/Schema.d.ts +70 -9
- package/build/annotations.d.ts +56 -13
- package/build/codegen/cli.cjs +84 -44
- package/build/codegen/cli.cjs.map +1 -1
- package/build/decoder/DecodeOperation.d.ts +48 -5
- package/build/decoder/Decoder.d.ts +2 -2
- package/build/decoder/strategy/Callbacks.d.ts +1 -1
- package/build/encoder/ChangeRecorder.d.ts +107 -0
- package/build/encoder/ChangeTree.d.ts +218 -69
- package/build/encoder/EncodeDescriptor.d.ts +63 -0
- package/build/encoder/EncodeOperation.d.ts +25 -2
- package/build/encoder/Encoder.d.ts +59 -3
- package/build/encoder/MapJournal.d.ts +62 -0
- package/build/encoder/RefIdAllocator.d.ts +35 -0
- package/build/encoder/Root.d.ts +94 -13
- package/build/encoder/StateView.d.ts +116 -8
- package/build/encoder/changeTree/inheritedFlags.d.ts +34 -0
- package/build/encoder/changeTree/liveIteration.d.ts +3 -0
- package/build/encoder/changeTree/parentChain.d.ts +24 -0
- package/build/encoder/changeTree/treeAttachment.d.ts +13 -0
- package/build/encoder/streaming.d.ts +73 -0
- package/build/encoder/subscriptions.d.ts +25 -0
- package/build/index.cjs +5202 -1552
- package/build/index.cjs.map +1 -1
- package/build/index.d.ts +7 -3
- package/build/index.js +5202 -1552
- package/build/index.mjs +5193 -1552
- package/build/index.mjs.map +1 -1
- package/build/input/InputDecoder.d.ts +32 -0
- package/build/input/InputEncoder.d.ts +117 -0
- package/build/input/index.cjs +7429 -0
- package/build/input/index.cjs.map +1 -0
- package/build/input/index.d.ts +3 -0
- package/build/input/index.mjs +7426 -0
- package/build/input/index.mjs.map +1 -0
- package/build/types/HelperTypes.d.ts +22 -8
- package/build/types/TypeContext.d.ts +9 -0
- package/build/types/builder.d.ts +162 -0
- package/build/types/custom/ArraySchema.d.ts +25 -4
- package/build/types/custom/CollectionSchema.d.ts +30 -2
- package/build/types/custom/MapSchema.d.ts +52 -3
- package/build/types/custom/SetSchema.d.ts +32 -2
- package/build/types/custom/StreamSchema.d.ts +114 -0
- package/build/types/symbols.d.ts +48 -5
- package/package.json +8 -2
- package/src/Metadata.ts +258 -31
- package/src/Reflection.ts +15 -13
- package/src/Schema.ts +176 -134
- package/src/annotations.ts +308 -236
- package/src/bench_bloat.ts +173 -0
- package/src/bench_decode.ts +221 -0
- package/src/bench_decode_mem.ts +165 -0
- package/src/bench_encode.ts +108 -0
- package/src/bench_init.ts +150 -0
- package/src/bench_static.ts +109 -0
- package/src/bench_stream.ts +295 -0
- package/src/bench_view_cmp.ts +142 -0
- package/src/codegen/parser.ts +83 -61
- package/src/decoder/DecodeOperation.ts +168 -63
- package/src/decoder/Decoder.ts +20 -10
- package/src/decoder/ReferenceTracker.ts +4 -0
- package/src/decoder/strategy/Callbacks.ts +30 -26
- package/src/decoder/strategy/getDecoderStateCallbacks.ts +16 -13
- package/src/encoder/ChangeRecorder.ts +276 -0
- package/src/encoder/ChangeTree.ts +674 -519
- package/src/encoder/EncodeDescriptor.ts +213 -0
- package/src/encoder/EncodeOperation.ts +107 -65
- package/src/encoder/Encoder.ts +630 -119
- package/src/encoder/MapJournal.ts +124 -0
- package/src/encoder/RefIdAllocator.ts +68 -0
- package/src/encoder/Root.ts +247 -120
- package/src/encoder/StateView.ts +592 -121
- package/src/encoder/changeTree/inheritedFlags.ts +217 -0
- package/src/encoder/changeTree/liveIteration.ts +74 -0
- package/src/encoder/changeTree/parentChain.ts +131 -0
- package/src/encoder/changeTree/treeAttachment.ts +171 -0
- package/src/encoder/streaming.ts +232 -0
- package/src/encoder/subscriptions.ts +71 -0
- package/src/index.ts +15 -3
- package/src/input/InputDecoder.ts +57 -0
- package/src/input/InputEncoder.ts +303 -0
- package/src/input/index.ts +3 -0
- package/src/types/HelperTypes.ts +21 -9
- package/src/types/TypeContext.ts +14 -2
- package/src/types/builder.ts +285 -0
- package/src/types/custom/ArraySchema.ts +210 -197
- package/src/types/custom/CollectionSchema.ts +115 -35
- package/src/types/custom/MapSchema.ts +162 -58
- package/src/types/custom/SetSchema.ts +128 -39
- package/src/types/custom/StreamSchema.ts +310 -0
- package/src/types/symbols.ts +54 -6
- package/src/utils.ts +4 -6
|
@@ -1,20 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChangeTree — the per-`Ref` mutation tracker attached via `$changes`.
|
|
3
|
+
*
|
|
4
|
+
* This file owns: class shape (fields, flags, ctor), inline
|
|
5
|
+
* ChangeRecorder implementation (record / forEach / …), mutation API
|
|
6
|
+
* (change / delete / operation / …), and encode lifecycle (endEncode /
|
|
7
|
+
* discard / …). Helpers split out into ./changeTree/:
|
|
8
|
+
*
|
|
9
|
+
* - parentChain.ts addParent / removeParent / find / has / getAll
|
|
10
|
+
* - liveIteration.ts forEachLive
|
|
11
|
+
* - inheritedFlags.ts filter / unreliable / transient / static inheritance
|
|
12
|
+
* - treeAttachment.ts setRoot / setParent / forEachChild(+WithCtx)
|
|
13
|
+
*
|
|
14
|
+
* Public surface on ChangeTree is unchanged — methods are thin pass-throughs
|
|
15
|
+
* into the helpers. V8 inlines the pass-throughs; the runtime shape stays
|
|
16
|
+
* a single class to preserve hidden-class + IC behavior.
|
|
17
|
+
*/
|
|
1
18
|
import { OPERATION } from "../encoding/spec.js";
|
|
2
19
|
import { Schema } from "../Schema.js";
|
|
3
|
-
import { $changes, $childType, $decoder, $onEncodeEnd, $encoder, $getByIndex, $refId, $refTypeFieldIndexes, $
|
|
20
|
+
import { $changes, $childType, $decoder, $onEncodeEnd, $encoder, $getByIndex, $proxyTarget, $refId, $refTypeFieldIndexes, $numFields, type $deleteByIndex } from "../types/symbols.js";
|
|
4
21
|
|
|
5
22
|
import type { MapSchema } from "../types/custom/MapSchema.js";
|
|
6
23
|
import type { ArraySchema } from "../types/custom/ArraySchema.js";
|
|
7
24
|
import type { CollectionSchema } from "../types/custom/CollectionSchema.js";
|
|
8
25
|
import type { SetSchema } from "../types/custom/SetSchema.js";
|
|
26
|
+
import type { StreamSchema } from "../types/custom/StreamSchema.js";
|
|
9
27
|
|
|
10
28
|
import { Root } from "./Root.js";
|
|
11
29
|
import { Metadata } from "../Metadata.js";
|
|
30
|
+
import { type ChangeRecorder, type ICollectionChangeRecorder, SchemaChangeRecorder, CollectionChangeRecorder, popcount32 } from "./ChangeRecorder.js";
|
|
12
31
|
import type { EncodeOperation } from "./EncodeOperation.js";
|
|
32
|
+
import { type EncodeDescriptor, getEncodeDescriptor } from "./EncodeDescriptor.js";
|
|
13
33
|
import type { DecodeOperation } from "../decoder/DecodeOperation.js";
|
|
14
34
|
|
|
35
|
+
import {
|
|
36
|
+
addParent as _addParent, removeParent as _removeParent,
|
|
37
|
+
findParent as _findParent, hasParent as _hasParent,
|
|
38
|
+
getAllParents as _getAllParents,
|
|
39
|
+
} from "./changeTree/parentChain.js";
|
|
40
|
+
import { forEachLive as _forEachLive, forEachLiveWithCtx as _forEachLiveWithCtx } from "./changeTree/liveIteration.js";
|
|
41
|
+
import {
|
|
42
|
+
setRoot as _setRoot, setParent as _setParent,
|
|
43
|
+
forEachChild as _forEachChild, forEachChildWithCtx as _forEachChildWithCtx,
|
|
44
|
+
} from "./changeTree/treeAttachment.js";
|
|
45
|
+
|
|
46
|
+
// Augmenting the global `Object` interface is a deliberate trade-off:
|
|
47
|
+
// any Schema / collection instance — regardless of which bundled
|
|
48
|
+
// `@colyseus/schema` version created it — can be duck-typed against
|
|
49
|
+
// these Symbol-keyed slots. Narrower shapes (e.g. on `IRef`) would break
|
|
50
|
+
// cross-version interop where server-bundled types coexist with client-
|
|
51
|
+
// bundled ones in the same process.
|
|
15
52
|
declare global {
|
|
16
53
|
interface Object {
|
|
17
|
-
// FIXME: not a good practice to extend globals here
|
|
18
54
|
[$changes]?: ChangeTree;
|
|
19
55
|
// [$refId]?: number;
|
|
20
56
|
[$encoder]?: EncodeOperation,
|
|
@@ -22,25 +58,40 @@ declare global {
|
|
|
22
58
|
}
|
|
23
59
|
}
|
|
24
60
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
61
|
+
// Pure arithmetic, no `this` — V8 inlines into encode-loop forEach.
|
|
62
|
+
// Mirror of `ChangeTree._opAt` for the inline-ops-only branch.
|
|
63
|
+
function readInlineOpByte(low: number, high: number, index: number): number {
|
|
64
|
+
const shift = (index & 3) << 3;
|
|
65
|
+
return (index < 4)
|
|
66
|
+
? (low >>> shift) & 0xFF
|
|
67
|
+
: (high >>> shift) & 0xFF;
|
|
31
68
|
}
|
|
32
69
|
|
|
33
|
-
|
|
70
|
+
// Adapter that lets `forEach(cb)` delegate to `forEachWithCtx(cb, _invokeNoCtx)` —
|
|
71
|
+
// no per-call closure allocation. See ChangeRecorder.ts for the same pattern.
|
|
72
|
+
const _invokeNoCtx = (
|
|
73
|
+
cb: (index: number, op: OPERATION) => void,
|
|
74
|
+
index: number,
|
|
75
|
+
op: OPERATION,
|
|
76
|
+
) => cb(index, op);
|
|
34
77
|
|
|
35
|
-
export
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
[
|
|
78
|
+
export interface IRef {
|
|
79
|
+
// `[$changes]?: ChangeTree;` is intentionally omitted here — see the
|
|
80
|
+
// `declare global` augmentation above. Narrowing to IRef would break
|
|
81
|
+
// cross-version interop (Cocos Creator bundles server- and client-
|
|
82
|
+
// side schema types together; a strict declaration here would reject
|
|
83
|
+
// one side's instances at compile time).
|
|
84
|
+
[$refId]?: number;
|
|
85
|
+
// `$getByIndex` / `$deleteByIndex` are required on every actual ref
|
|
86
|
+
// the decoder / encoder ever touches (Schema + every collection
|
|
87
|
+
// implements both). Keeping them non-optional lets hot-path call
|
|
88
|
+
// sites skip the `(ref as any)` cast.
|
|
89
|
+
[$getByIndex](index: number, isEncodeAll?: boolean): any;
|
|
90
|
+
[$deleteByIndex](index: number): void;
|
|
42
91
|
}
|
|
43
92
|
|
|
93
|
+
export type Ref = Schema | ArraySchema | MapSchema | CollectionSchema | SetSchema | StreamSchema;
|
|
94
|
+
|
|
44
95
|
// Linked list node for change trees
|
|
45
96
|
export interface ChangeTreeNode {
|
|
46
97
|
changeTree: ChangeTree;
|
|
@@ -55,345 +106,548 @@ export interface ChangeTreeList {
|
|
|
55
106
|
tail?: ChangeTreeNode;
|
|
56
107
|
}
|
|
57
108
|
|
|
58
|
-
export interface ChangeSet {
|
|
59
|
-
// field index -> operation index
|
|
60
|
-
indexes: { [index: number]: number };
|
|
61
|
-
operations: number[];
|
|
62
|
-
queueRootNode?: ChangeTreeNode; // direct reference to ChangeTreeNode in the linked list
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function createChangeSet(queueRootNode?: ChangeTreeNode): ChangeSet {
|
|
66
|
-
return { indexes: {}, operations: [], queueRootNode };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
109
|
// Linked list helper functions
|
|
70
110
|
export function createChangeTreeList(): ChangeTreeList {
|
|
71
111
|
return { next: undefined, tail: undefined };
|
|
72
112
|
}
|
|
73
113
|
|
|
74
|
-
export function setOperationAtIndex(changeSet: ChangeSet, index: number) {
|
|
75
|
-
const operationsIndex = changeSet.indexes[index];
|
|
76
|
-
if (operationsIndex === undefined) {
|
|
77
|
-
changeSet.indexes[index] = changeSet.operations.push(index) - 1;
|
|
78
|
-
} else {
|
|
79
|
-
changeSet.operations[operationsIndex] = index;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function deleteOperationAtIndex(changeSet: ChangeSet, index: number | string) {
|
|
84
|
-
let operationsIndex = changeSet.indexes[index as any as number];
|
|
85
|
-
if (operationsIndex === undefined) {
|
|
86
|
-
//
|
|
87
|
-
// if index is not found, we need to find the last operation
|
|
88
|
-
// FIXME: this is not very efficient
|
|
89
|
-
//
|
|
90
|
-
// > See "should allow consecutive splices (same place)" tests
|
|
91
|
-
//
|
|
92
|
-
operationsIndex = Object.values(changeSet.indexes).at(-1);
|
|
93
|
-
index = Object.entries(changeSet.indexes).find(([_, value]) => value === operationsIndex)?.[0];
|
|
94
|
-
}
|
|
95
|
-
changeSet.operations[operationsIndex] = undefined;
|
|
96
|
-
delete changeSet.indexes[index as any as number];
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function debugChangeSet(label: string, changeSet: ChangeSet) {
|
|
100
|
-
let indexes: string[] = [];
|
|
101
|
-
let operations: string[] = [];
|
|
102
|
-
|
|
103
|
-
for (const index in changeSet.indexes) {
|
|
104
|
-
indexes.push(`\t${index} => [${changeSet.indexes[index]}]`);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
for (let i = 0; i < changeSet.operations.length; i++) {
|
|
108
|
-
const index = changeSet.operations[i];
|
|
109
|
-
if (index !== undefined) {
|
|
110
|
-
operations.push(`\t[${i}] => ${index}`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
console.log(`${label} =>\nindexes (${Object.keys(changeSet.indexes).length}) {`);
|
|
115
|
-
console.log(indexes.join("\n"), "\n}");
|
|
116
|
-
console.log(`operations (${changeSet.operations.filter(op => op !== undefined).length}) {`);
|
|
117
|
-
console.log(operations.join("\n"), "\n}");
|
|
118
|
-
}
|
|
119
|
-
|
|
120
114
|
export interface ParentChain {
|
|
121
115
|
ref: Ref;
|
|
122
116
|
index: number;
|
|
123
117
|
next?: ParentChain;
|
|
124
118
|
}
|
|
125
119
|
|
|
126
|
-
|
|
120
|
+
// Flags bitfield. *_UNRELIABLE / _TRANSIENT / _STATIC mirror the parent
|
|
121
|
+
// field's annotation — inherited at setParent/setRoot time.
|
|
122
|
+
export const IS_FILTERED = 1, IS_VISIBILITY_SHARED = 2, IS_NEW = 4;
|
|
123
|
+
export const IS_UNRELIABLE = 8, IS_TRANSIENT = 16, IS_STATIC = 32;
|
|
124
|
+
// Collection tree attached to a parent field annotated `.stream()` —
|
|
125
|
+
// drives the encoder's priority/broadcast pass. Set in inheritedFlags
|
|
126
|
+
// so both `t.stream(X)` (via StreamSchema's `$isStream` brand) and
|
|
127
|
+
// `t.map(X).stream()` / `t.set(X).stream()` route through the same
|
|
128
|
+
// emission machinery.
|
|
129
|
+
export const IS_STREAM_COLLECTION = 64;
|
|
130
|
+
/**
|
|
131
|
+
* Flags a child inherits from its parent's own transitive state via
|
|
132
|
+
* `checkInheritedFlags`. Read as a bitwise mask so the inheritance step
|
|
133
|
+
* is a single OR instead of three getter/setter pairs.
|
|
134
|
+
*
|
|
135
|
+
* `IS_UNRELIABLE` is intentionally excluded: `@unreliable` is rejected
|
|
136
|
+
* at decoration time for ref-type fields (see `Metadata.setUnreliable`)
|
|
137
|
+
* because an unreliable ADD/DELETE could leave the decoder unable to
|
|
138
|
+
* interpret later packets referencing an orphan refId. Tree-level
|
|
139
|
+
* unreliable is therefore dead on every Schema/Collection tree today;
|
|
140
|
+
* the bit and its machinery are kept in place so this can be
|
|
141
|
+
* reconsidered if a safe semantics (e.g. reliable ADD + unreliable
|
|
142
|
+
* field mutations only) is designed later.
|
|
143
|
+
*/
|
|
144
|
+
export const INHERITABLE_FLAGS = IS_TRANSIENT | IS_STATIC;
|
|
145
|
+
|
|
146
|
+
export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
127
147
|
ref: T;
|
|
128
|
-
metadata: Metadata;
|
|
129
148
|
|
|
130
|
-
|
|
131
|
-
|
|
149
|
+
/**
|
|
150
|
+
* Non-Proxy target of `ref` for encoder hot-path reads. For
|
|
151
|
+
* `ArraySchema`, `ref` is the Proxy users interact with; every property
|
|
152
|
+
* access on it runs through the `get` trap (even for symbol keys, which
|
|
153
|
+
* fall through to `Reflect.get` — one extra hop per lookup). The encoder
|
|
154
|
+
* loop reads `[$getByIndex]`, `[$childType]`, `.items`, `.tmpItems` at
|
|
155
|
+
* high frequency during `encode()` / `encodeAll()`; going through
|
|
156
|
+
* `refTarget` skips all of those traps.
|
|
157
|
+
*
|
|
158
|
+
* For non-proxied types (Schema, MapSchema, SetSchema, CollectionSchema,
|
|
159
|
+
* StreamSchema), `refTarget === ref`. Consumers that need the user-
|
|
160
|
+
* facing identity (debug output, callback parents) keep using `ref`.
|
|
161
|
+
*/
|
|
162
|
+
refTarget: T;
|
|
163
|
+
|
|
164
|
+
metadata: Metadata;
|
|
132
165
|
|
|
133
166
|
/**
|
|
134
|
-
*
|
|
167
|
+
* Per-class cache of encoder fn / filter fn / isSchema / filterBitmask /
|
|
168
|
+
* metadata, looked up once at construction. The encode loop reads
|
|
169
|
+
* `tree.encDescriptor` and never touches `ref.constructor` again. See
|
|
170
|
+
* EncodeDescriptor.ts.
|
|
135
171
|
*/
|
|
136
|
-
|
|
137
|
-
isVisibilitySharedWithParent?: boolean; // See test case: 'should not be required to manually call view.add() items to child arrays without @view() tag'
|
|
172
|
+
encDescriptor: EncodeDescriptor;
|
|
138
173
|
|
|
139
|
-
|
|
174
|
+
root?: Root;
|
|
140
175
|
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
// => https://chatgpt.com/share/67107d0c-bc20-8004-8583-83b17dd7c196
|
|
147
|
-
//
|
|
148
|
-
changes: ChangeSet = { indexes: {}, operations: [] };
|
|
149
|
-
allChanges: ChangeSet = { indexes: {}, operations: [] };
|
|
150
|
-
filteredChanges: ChangeSet;
|
|
151
|
-
allFilteredChanges: ChangeSet;
|
|
176
|
+
// Inline single parent (the common case)
|
|
177
|
+
parentRef?: Ref;
|
|
178
|
+
_parentIndex?: number;
|
|
179
|
+
extraParents?: ParentChain; // linked list for 2nd+ parents (rare: instance sharing)
|
|
152
180
|
|
|
153
|
-
|
|
181
|
+
// Packed boolean flags. See IS_* constants above for bit layout.
|
|
182
|
+
flags: number = IS_NEW;
|
|
154
183
|
|
|
155
184
|
/**
|
|
156
|
-
*
|
|
185
|
+
* Per-walk visit stamp written by `Encoder.encodeFullSync`'s DFS. A
|
|
186
|
+
* tree is considered "already visited by the current walk" iff
|
|
187
|
+
* `tree._fullSyncGen === ctx.gen` — the encoder bumps its generation
|
|
188
|
+
* counter once per walk, then stamps each tree with that value on
|
|
189
|
+
* first visit; any later encounter of the same tree (shared refs
|
|
190
|
+
* reachable through multiple parents) short-circuits on the equality
|
|
191
|
+
* check instead of recursing again.
|
|
157
192
|
*/
|
|
158
|
-
|
|
193
|
+
_fullSyncGen: number = 0;
|
|
159
194
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
195
|
+
// Schema vs Collection discriminator. Set once in ctor, never changes —
|
|
196
|
+
// per-tree-stable branch for inline ChangeRecorder dispatch.
|
|
197
|
+
_isSchema: boolean = false;
|
|
163
198
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if (this.metadata?.[$viewFieldIndexes]) {
|
|
168
|
-
this.allFilteredChanges = { indexes: {}, operations: [] };
|
|
169
|
-
this.filteredChanges = { indexes: {}, operations: [] };
|
|
170
|
-
}
|
|
171
|
-
}
|
|
199
|
+
// Inline reliable SchemaChangeRecorder state (valid only if _isSchema).
|
|
200
|
+
dirtyLow: number = 0;
|
|
201
|
+
dirtyHigh: number = 0;
|
|
172
202
|
|
|
173
|
-
|
|
174
|
-
|
|
203
|
+
// Inline ops for Schemas with ≤8 fields (4 op-bytes per number).
|
|
204
|
+
// When `ops` is set (>8 fields), reads/writes go through the Uint8Array.
|
|
205
|
+
opsLow: number = 0;
|
|
206
|
+
opsHigh: number = 0;
|
|
207
|
+
ops?: Uint8Array;
|
|
175
208
|
|
|
176
|
-
|
|
209
|
+
// Inline reliable CollectionChangeRecorder state (valid only if !_isSchema).
|
|
210
|
+
// `collDirty` is allocated in the ctor. `collPureOps` stays undefined
|
|
211
|
+
// until the first CLEAR/REVERSE (most workloads never hit this).
|
|
212
|
+
collDirty?: Map<number, OPERATION>;
|
|
213
|
+
collPureOps?: Array<[number, OPERATION]>;
|
|
177
214
|
|
|
178
|
-
|
|
215
|
+
// Lazy-allocated unreliable-channel recorder (rare — opt-in via @unreliable).
|
|
216
|
+
unreliableRecorder?: ChangeRecorder;
|
|
179
217
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
this.forEachChild((child, _) => {
|
|
183
|
-
if (child.root !== root) {
|
|
184
|
-
child.setRoot(root);
|
|
185
|
-
} else {
|
|
186
|
-
root.add(child); // increment refCount
|
|
187
|
-
}
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
}
|
|
218
|
+
// When true, mutations on the ref are NOT tracked. See pause/resume/untracked.
|
|
219
|
+
paused: boolean = false;
|
|
191
220
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
root?: Root,
|
|
195
|
-
parentIndex?: number,
|
|
196
|
-
) {
|
|
197
|
-
this.addParent(parent, parentIndex);
|
|
221
|
+
changesNode?: ChangeTreeNode; // Root.changes linked-list node
|
|
222
|
+
unreliableChangesNode?: ChangeTreeNode; // Root.unreliableChanges linked-list node
|
|
198
223
|
|
|
199
|
-
|
|
200
|
-
|
|
224
|
+
// Per-StateView visibility bitmaps. Bit `(viewId & 31)` in slot
|
|
225
|
+
// `(viewId >> 5)` is set iff the view can see this tree. Replaces
|
|
226
|
+
// per-view WeakSet lookups with direct bitwise ops.
|
|
227
|
+
// Lazy: undefined until the tree participates in any view.
|
|
228
|
+
visibleViews?: number[];
|
|
229
|
+
invisibleViews?: number[];
|
|
201
230
|
|
|
202
|
-
|
|
231
|
+
// Per-(view, tag) bitmap, indexed by tag. Custom tags only —
|
|
232
|
+
// DEFAULT_VIEW_TAG visibility lives in `visibleViews`.
|
|
233
|
+
tagViews?: Map<number, number[]>;
|
|
203
234
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
235
|
+
/**
|
|
236
|
+
* Per-view subscription bitmap — same layout as `visibleViews`. Set by
|
|
237
|
+
* `StateView.subscribe(collection)` to mark the view as persistently
|
|
238
|
+
* interested in this collection's contents. When a new child is
|
|
239
|
+
* attached to a subscribed collection (setParent hook), it's
|
|
240
|
+
* auto-propagated to every subscribed view (force-shipped for
|
|
241
|
+
* Array/Map/Set/Collection; enqueued into per-view pending for
|
|
242
|
+
* streams). Undefined until the first subscribe.
|
|
243
|
+
*/
|
|
244
|
+
subscribedViews?: number[];
|
|
245
|
+
|
|
246
|
+
// Accessor properties for flags
|
|
247
|
+
get isFiltered() { return (this.flags & IS_FILTERED) !== 0; }
|
|
248
|
+
set isFiltered(v: boolean) { this.flags = v ? (this.flags | IS_FILTERED) : (this.flags & ~IS_FILTERED); }
|
|
249
|
+
get isVisibilitySharedWithParent() { return (this.flags & IS_VISIBILITY_SHARED) !== 0; }
|
|
250
|
+
set isVisibilitySharedWithParent(v: boolean) { this.flags = v ? (this.flags | IS_VISIBILITY_SHARED) : (this.flags & ~IS_VISIBILITY_SHARED); }
|
|
251
|
+
get isNew() { return (this.flags & IS_NEW) !== 0; }
|
|
252
|
+
set isNew(v: boolean) { this.flags = v ? (this.flags | IS_NEW) : (this.flags & ~IS_NEW); }
|
|
253
|
+
get isUnreliable() { return (this.flags & IS_UNRELIABLE) !== 0; }
|
|
254
|
+
set isUnreliable(v: boolean) { this.flags = v ? (this.flags | IS_UNRELIABLE) : (this.flags & ~IS_UNRELIABLE); }
|
|
255
|
+
get isTransient() { return (this.flags & IS_TRANSIENT) !== 0; }
|
|
256
|
+
set isTransient(v: boolean) { this.flags = v ? (this.flags | IS_TRANSIENT) : (this.flags & ~IS_TRANSIENT); }
|
|
257
|
+
get isStatic() { return (this.flags & IS_STATIC) !== 0; }
|
|
258
|
+
set isStatic(v: boolean) { this.flags = v ? (this.flags | IS_STATIC) : (this.flags & ~IS_STATIC); }
|
|
259
|
+
get isStreamCollection() { return (this.flags & IS_STREAM_COLLECTION) !== 0; }
|
|
260
|
+
set isStreamCollection(v: boolean) { this.flags = v ? (this.flags | IS_STREAM_COLLECTION) : (this.flags & ~IS_STREAM_COLLECTION); }
|
|
261
|
+
|
|
262
|
+
// True iff tree inherits `isFiltered` OR its Schema class declares any
|
|
263
|
+
// @view-tagged fields. StateView.addParentOf uses this to decide whether
|
|
264
|
+
// a parent must be included in a view's bootstrap. Reads the class-level
|
|
265
|
+
// "any viewed field" flag that `EncodeDescriptor` precomputes — same
|
|
266
|
+
// pattern as `hasAnyStatic` / `hasAnyUnreliable` / `hasAnyStream`.
|
|
267
|
+
get hasFilteredFields(): boolean {
|
|
268
|
+
return this.isFiltered || this.encDescriptor.hasAnyView;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
ensureUnreliableRecorder(): ChangeRecorder {
|
|
272
|
+
if (this.unreliableRecorder === undefined) {
|
|
273
|
+
const isSchema = Metadata.isValidInstance(this.ref);
|
|
274
|
+
this.unreliableRecorder = isSchema
|
|
275
|
+
? new SchemaChangeRecorder((this.metadata?.[$numFields] ?? 0) as number)
|
|
276
|
+
: new CollectionChangeRecorder();
|
|
208
277
|
}
|
|
278
|
+
return this.unreliableRecorder;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
isFieldUnreliable(index: number): boolean {
|
|
282
|
+
// Tree-level `isUnreliable` is disabled — @unreliable is rejected
|
|
283
|
+
// on ref-type fields at decoration time, so no tree ever carries
|
|
284
|
+
// the flag. Kept as a comment in case a safe semantics is added
|
|
285
|
+
// later (see INHERITABLE_FLAGS rationale).
|
|
286
|
+
// if (this.isUnreliable) return true;
|
|
287
|
+
// Class-level fast path: most schemas have zero unreliable fields,
|
|
288
|
+
// so the per-mutation check resolves without the symbol-keyed
|
|
289
|
+
// metadata lookup. For schemas that DO have unreliable fields, the
|
|
290
|
+
// bitmask answers fields 0-31 in one bitwise op (no Array.includes
|
|
291
|
+
// linear scan). Fields ≥32 always fall back to the metadata lookup
|
|
292
|
+
// (same limitation as filterBitmask — bitmask only covers low 32).
|
|
293
|
+
const desc = this.encDescriptor;
|
|
294
|
+
if (!desc.hasAnyUnreliable) return false;
|
|
295
|
+
if (index < 32) return (desc.unreliableBitmask & (1 << index)) !== 0;
|
|
296
|
+
return Metadata.hasUnreliableAtIndex(this.metadata, index);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// @static fields sync once via full-sync; post-init mutations are ignored
|
|
300
|
+
// by the tracker (the value still lives on the instance).
|
|
301
|
+
isFieldStatic(index: number): boolean {
|
|
302
|
+
if (this.isStatic) return true;
|
|
303
|
+
const desc = this.encDescriptor;
|
|
304
|
+
if (!desc.hasAnyStatic) return false;
|
|
305
|
+
if (index < 32) return (desc.staticBitmask & (1 << index)) !== 0;
|
|
306
|
+
return Metadata.hasStaticAtIndex(this.metadata, index);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// `t.stream(...)` collection fields — encoded via per-view priority/budget
|
|
310
|
+
// gate instead of emitting all dirty ADDs in one tick. Class-level short
|
|
311
|
+
// circuit avoids the metadata chase on schemas that carry no stream fields.
|
|
312
|
+
isFieldStream(index: number): boolean {
|
|
313
|
+
const desc = this.encDescriptor;
|
|
314
|
+
if (!desc.hasAnyStream) return false;
|
|
315
|
+
if (index < 32) return (desc.streamBitmask & (1 << index)) !== 0;
|
|
316
|
+
return Metadata.hasStreamAtIndex(this.metadata, index);
|
|
317
|
+
}
|
|
209
318
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
319
|
+
constructor(ref: T) {
|
|
320
|
+
this.ref = ref;
|
|
321
|
+
// `$proxyTarget` is a self-reference set by ArraySchema on the raw
|
|
322
|
+
// target; for non-proxied refs it's undefined and we fall back to
|
|
323
|
+
// `ref`. Cached here so hot-path reads skip the Proxy `get` trap.
|
|
324
|
+
this.refTarget = ((ref as any)[$proxyTarget] ?? ref) as T;
|
|
325
|
+
|
|
326
|
+
// Single per-class lookup that subsumes Symbol.metadata,
|
|
327
|
+
// isValidInstance, $encoder, $filter, and the filter bitmask.
|
|
328
|
+
// After this, the encode loop never touches `ref.constructor`.
|
|
329
|
+
const desc = getEncodeDescriptor(ref);
|
|
330
|
+
this.encDescriptor = desc;
|
|
331
|
+
this.metadata = desc.metadata;
|
|
332
|
+
|
|
333
|
+
const isSchema = desc.isSchema;
|
|
334
|
+
this._isSchema = isSchema;
|
|
335
|
+
|
|
336
|
+
// Assign every optional slot so Schema and Collection trees share
|
|
337
|
+
// one hidden-class transition path (tsconfig useDefineForClassFields=false
|
|
338
|
+
// otherwise leaves uninitialized class fields absent from the shape).
|
|
339
|
+
this.ops = undefined;
|
|
340
|
+
this.collDirty = undefined;
|
|
341
|
+
this.collPureOps = undefined;
|
|
342
|
+
|
|
343
|
+
if (isSchema) {
|
|
344
|
+
const numFields = (this.metadata?.[$numFields] ?? 0) as number;
|
|
345
|
+
if (numFields > 7) this.ops = new Uint8Array(numFields + 1);
|
|
346
|
+
} else {
|
|
347
|
+
this.collDirty = new Map();
|
|
227
348
|
}
|
|
228
349
|
}
|
|
229
350
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (typeof ((this.ref as any)[$childType]) !== "string") {
|
|
236
|
-
// MapSchema / ArraySchema, etc.
|
|
237
|
-
for (const [key, value] of (this.ref as MapSchema).entries()) {
|
|
238
|
-
if (!value) { continue; } // sparse arrays can have undefined values
|
|
239
|
-
callback(value[$changes], this.indexes?.[key] ?? key);
|
|
240
|
-
};
|
|
241
|
-
}
|
|
351
|
+
// ────────────────────────────────────────────────────────────────────
|
|
352
|
+
// Inline ChangeRecorder implementation. Each method branches once on
|
|
353
|
+
// `_isSchema` (per-tree-stable → predictable branch). Kills one
|
|
354
|
+
// CollectionChangeRecorder+Map allocation per Collection tree.
|
|
355
|
+
// ────────────────────────────────────────────────────────────────────
|
|
242
356
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
357
|
+
// Schema-only helpers that own all inline-vs-array dispatch.
|
|
358
|
+
private _opAt(index: number): number {
|
|
359
|
+
const ops = this.ops;
|
|
360
|
+
if (ops !== undefined) return ops[index];
|
|
361
|
+
const shift = (index & 3) << 3;
|
|
362
|
+
return (index < 4)
|
|
363
|
+
? (this.opsLow >>> shift) & 0xFF
|
|
364
|
+
: (this.opsHigh >>> shift) & 0xFF;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private _opPut(index: number, op: OPERATION): void {
|
|
368
|
+
const ops = this.ops;
|
|
369
|
+
if (ops !== undefined) {
|
|
370
|
+
ops[index] = op;
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const shift = (index & 3) << 3;
|
|
374
|
+
const mask = ~(0xFF << shift);
|
|
375
|
+
if (index < 4) this.opsLow = (this.opsLow & mask) | (op << shift);
|
|
376
|
+
else this.opsHigh = (this.opsHigh & mask) | (op << shift);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private _markDirty(index: number): void {
|
|
380
|
+
if (index < 32) this.dirtyLow |= (1 << index);
|
|
381
|
+
else this.dirtyHigh |= (1 << (index - 32));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
record(index: number, op: OPERATION): void {
|
|
385
|
+
if (this._isSchema) {
|
|
386
|
+
const prev = this._opAt(index);
|
|
387
|
+
if (prev === 0) this._opPut(index, op);
|
|
388
|
+
else if (prev === OPERATION.DELETE) this._opPut(index, OPERATION.DELETE_AND_ADD);
|
|
389
|
+
// Promote ADD → DELETE_AND_ADD when a ref is replaced in the
|
|
390
|
+
// same tick. Otherwise the on-wire op collapses to plain ADD
|
|
391
|
+
// and the decoder's `refs` map leaks the displaced refId —
|
|
392
|
+
// harmless on its own, but refId pooling turns that leak into
|
|
393
|
+
// a catastrophic rebinding when the refId is later reused.
|
|
394
|
+
else if (prev === OPERATION.ADD && op === OPERATION.DELETE_AND_ADD) {
|
|
395
|
+
this._opPut(index, OPERATION.DELETE_AND_ADD);
|
|
249
396
|
}
|
|
397
|
+
// else: existing ADD / DELETE_AND_ADD — preserve op-byte.
|
|
398
|
+
this._markDirty(index);
|
|
399
|
+
} else {
|
|
400
|
+
const dirty = this.collDirty!;
|
|
401
|
+
const prev = dirty.get(index);
|
|
402
|
+
let finalOp: OPERATION;
|
|
403
|
+
if (prev === undefined) finalOp = op;
|
|
404
|
+
else if (prev === OPERATION.DELETE) finalOp = OPERATION.DELETE_AND_ADD;
|
|
405
|
+
else if (prev === OPERATION.ADD && op === OPERATION.DELETE_AND_ADD) finalOp = OPERATION.DELETE_AND_ADD;
|
|
406
|
+
else finalOp = prev;
|
|
407
|
+
dirty.set(index, finalOp);
|
|
250
408
|
}
|
|
251
409
|
}
|
|
252
410
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
this.
|
|
411
|
+
recordDelete(index: number, op: OPERATION): void {
|
|
412
|
+
if (this._isSchema) {
|
|
413
|
+
this._opPut(index, op);
|
|
414
|
+
this._markDirty(index);
|
|
415
|
+
} else {
|
|
416
|
+
this.collDirty!.set(index, op);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
259
419
|
|
|
420
|
+
recordRaw(index: number, op: OPERATION): void {
|
|
421
|
+
if (this._isSchema) {
|
|
422
|
+
this._opPut(index, op);
|
|
423
|
+
this._markDirty(index);
|
|
260
424
|
} else {
|
|
261
|
-
this.
|
|
262
|
-
this.root?.enqueueChangeTree(this, 'changes');
|
|
425
|
+
this.collDirty!.set(index, op);
|
|
263
426
|
}
|
|
264
427
|
}
|
|
265
428
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
? this.filteredChanges
|
|
270
|
-
: this.changes;
|
|
271
|
-
|
|
272
|
-
const previousOperation = this.indexedOperations[index];
|
|
273
|
-
if (!previousOperation || previousOperation === OPERATION.DELETE) {
|
|
274
|
-
const op = (!previousOperation)
|
|
275
|
-
? operation
|
|
276
|
-
: (previousOperation === OPERATION.DELETE)
|
|
277
|
-
? OPERATION.DELETE_AND_ADD
|
|
278
|
-
: operation
|
|
279
|
-
//
|
|
280
|
-
// TODO: are DELETE operations being encoded as ADD here ??
|
|
281
|
-
//
|
|
282
|
-
this.indexedOperations[index] = op;
|
|
429
|
+
recordPure(op: OPERATION): void {
|
|
430
|
+
if (this._isSchema) {
|
|
431
|
+
throw new Error("ChangeTree (Schema): pure operations are not supported");
|
|
283
432
|
}
|
|
433
|
+
(this.collPureOps ??= []).push([this.collDirty!.size, op]);
|
|
434
|
+
}
|
|
284
435
|
|
|
285
|
-
|
|
436
|
+
operationAt(index: number): OPERATION | undefined {
|
|
437
|
+
if (this._isSchema) {
|
|
438
|
+
const op = this._opAt(index);
|
|
439
|
+
return op === 0 ? undefined : op;
|
|
440
|
+
}
|
|
441
|
+
return this.collDirty!.get(index);
|
|
442
|
+
}
|
|
286
443
|
|
|
287
|
-
|
|
288
|
-
|
|
444
|
+
setOperationAt(index: number, op: OPERATION): void {
|
|
445
|
+
// Schema: overwrite only (no dirty-mark). Collection: overwrite iff key exists (legacy).
|
|
446
|
+
if (this._isSchema) {
|
|
447
|
+
this._opPut(index, op);
|
|
448
|
+
} else {
|
|
449
|
+
const dirty = this.collDirty!;
|
|
450
|
+
if (dirty.has(index)) dirty.set(index, op);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
289
453
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
454
|
+
// Cold-path delegate: all `forEach` callers are debug/dump utilities
|
|
455
|
+
// (Schema.ts debug output, utils.ts change dump, discardAll in tests).
|
|
456
|
+
// The hot encode loop uses `forEachWithCtx` directly. See ChangeRecorder.ts
|
|
457
|
+
// for the same adapter pattern.
|
|
458
|
+
forEach(cb: (index: number, op: OPERATION) => void): void {
|
|
459
|
+
this.forEachWithCtx(cb, _invokeNoCtx);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
forEachWithCtx<C>(ctx: C, cb: (ctx: C, index: number, op: OPERATION) => void): void {
|
|
463
|
+
if (this._isSchema) {
|
|
464
|
+
let low = this.dirtyLow;
|
|
465
|
+
let high = this.dirtyHigh;
|
|
466
|
+
const ops = this.ops;
|
|
467
|
+
if (ops !== undefined) {
|
|
468
|
+
while (low !== 0) {
|
|
469
|
+
const bit = low & -low;
|
|
470
|
+
const fieldIndex = 31 - Math.clz32(bit);
|
|
471
|
+
low ^= bit;
|
|
472
|
+
cb(ctx, fieldIndex, ops[fieldIndex]);
|
|
473
|
+
}
|
|
474
|
+
while (high !== 0) {
|
|
475
|
+
const bit = high & -high;
|
|
476
|
+
const fieldIndex = 31 - Math.clz32(bit) + 32;
|
|
477
|
+
high ^= bit;
|
|
478
|
+
cb(ctx, fieldIndex, ops[fieldIndex]);
|
|
479
|
+
}
|
|
480
|
+
} else {
|
|
481
|
+
const ol = this.opsLow;
|
|
482
|
+
const oh = this.opsHigh;
|
|
483
|
+
while (low !== 0) {
|
|
484
|
+
const bit = low & -low;
|
|
485
|
+
const fieldIndex = 31 - Math.clz32(bit);
|
|
486
|
+
low ^= bit;
|
|
487
|
+
cb(ctx, fieldIndex, readInlineOpByte(ol, oh, fieldIndex));
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
const dirty = this.collDirty!;
|
|
493
|
+
const pure = this.collPureOps;
|
|
494
|
+
if (pure !== undefined && pure.length > 0) {
|
|
495
|
+
let pureIdx = 0, i = 0;
|
|
496
|
+
for (const [index, op] of dirty) {
|
|
497
|
+
while (pureIdx < pure.length && pure[pureIdx][0] <= i) {
|
|
498
|
+
const pureOp = pure[pureIdx++][1];
|
|
499
|
+
cb(ctx, -pureOp, pureOp);
|
|
500
|
+
}
|
|
501
|
+
cb(ctx, index, op);
|
|
502
|
+
i++;
|
|
503
|
+
}
|
|
504
|
+
while (pureIdx < pure.length) {
|
|
505
|
+
const pureOp = pure[pureIdx++][1];
|
|
506
|
+
cb(ctx, -pureOp, pureOp);
|
|
293
507
|
}
|
|
294
|
-
|
|
295
508
|
} else {
|
|
296
|
-
|
|
297
|
-
this.root?.enqueueChangeTree(this, 'changes');
|
|
509
|
+
for (const [index, op] of dirty) cb(ctx, index, op);
|
|
298
510
|
}
|
|
299
511
|
}
|
|
300
512
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
513
|
+
size(): number {
|
|
514
|
+
if (this._isSchema) return popcount32(this.dirtyLow) + popcount32(this.dirtyHigh);
|
|
515
|
+
return this.collDirty!.size + (this.collPureOps?.length ?? 0);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
has(): boolean {
|
|
519
|
+
if (this._isSchema) return (this.dirtyLow | this.dirtyHigh) !== 0;
|
|
520
|
+
return this.collDirty!.size > 0 || (this.collPureOps !== undefined && this.collPureOps.length > 0);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
reset(): void {
|
|
524
|
+
if (this._isSchema) {
|
|
525
|
+
this.dirtyLow = 0;
|
|
526
|
+
this.dirtyHigh = 0;
|
|
527
|
+
if (this.ops !== undefined) this.ops.fill(0);
|
|
528
|
+
else { this.opsLow = 0; this.opsHigh = 0; }
|
|
529
|
+
return;
|
|
316
530
|
}
|
|
317
|
-
this.
|
|
318
|
-
|
|
531
|
+
this.collDirty!.clear();
|
|
532
|
+
if (this.collPureOps !== undefined) this.collPureOps.length = 0;
|
|
533
|
+
}
|
|
319
534
|
|
|
320
|
-
|
|
535
|
+
shift(shiftIndex: number): void {
|
|
536
|
+
if (this._isSchema) throw new Error("ChangeTree (Schema): shift is not supported");
|
|
537
|
+
const src = this.collDirty!;
|
|
538
|
+
const dst = new Map<number, OPERATION>();
|
|
539
|
+
for (const [idx, val] of src) dst.set(idx + shiftIndex, val);
|
|
540
|
+
this.collDirty = dst;
|
|
321
541
|
}
|
|
322
542
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
543
|
+
// Tree attachment + child iteration — see ./changeTree/treeAttachment.ts.
|
|
544
|
+
setRoot(root: Root): void { _setRoot(this, root); }
|
|
545
|
+
setParent(parent: Ref, root?: Root, parentIndex?: number): void { _setParent(this, parent, root, parentIndex); }
|
|
546
|
+
forEachChild(cb: (change: ChangeTree, at: any) => void): void { _forEachChild(this, cb); }
|
|
547
|
+
forEachChildWithCtx<C>(ctx: C, cb: (ctx: C, change: ChangeTree, at: any) => void): void {
|
|
548
|
+
_forEachChildWithCtx(this, ctx, cb);
|
|
549
|
+
}
|
|
550
|
+
forEachLive(cb: (index: number) => void): void { _forEachLive(this, cb); }
|
|
551
|
+
forEachLiveWithCtx<C>(ctx: C, cb: (ctx: C, index: number) => void): void {
|
|
552
|
+
_forEachLiveWithCtx(this, ctx, cb);
|
|
553
|
+
}
|
|
332
554
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
555
|
+
operation(op: OPERATION) {
|
|
556
|
+
if (this.paused || this.isStatic) return;
|
|
557
|
+
// Pure ops (CLEAR/REVERSE) only emit from collection trees — the
|
|
558
|
+
// recorder here is always a CollectionChangeRecorder by construction.
|
|
559
|
+
//
|
|
560
|
+
// Tree-level `isUnreliable` is disabled (see INHERITABLE_FLAGS):
|
|
561
|
+
// no collection tree can be marked unreliable as a whole under the
|
|
562
|
+
// ref-field rejection rule in `Metadata.setUnreliable`. The branch
|
|
563
|
+
// is kept as a comment for re-enablement.
|
|
564
|
+
// if (this.isUnreliable) {
|
|
565
|
+
// (this.ensureUnreliableRecorder() as ICollectionChangeRecorder).recordPure(op);
|
|
566
|
+
// this.root?.enqueueUnreliable(this);
|
|
567
|
+
// } else {
|
|
568
|
+
this.recordPure(op);
|
|
569
|
+
this.root?.enqueueChangeTree(this);
|
|
570
|
+
// }
|
|
336
571
|
}
|
|
337
572
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
573
|
+
/**
|
|
574
|
+
* Route a field-level mutation to the reliable or unreliable channel
|
|
575
|
+
* and enqueue into the matching queue. Shared by `change` and
|
|
576
|
+
* `indexedOperation`; `raw=true` bypasses DELETE→ADD merge
|
|
577
|
+
* (ArraySchema positional writes), `raw=false` merges inside `record`.
|
|
578
|
+
*
|
|
579
|
+
* Note: record() on both channels handles DELETE→ADD merge internally,
|
|
580
|
+
* so callers do not need to pre-compute the merged op.
|
|
581
|
+
*
|
|
582
|
+
* `@unreliable` is decoration-time-validated to apply only to primitive
|
|
583
|
+
* fields (see annotations.ts), so the per-field unreliable flag here
|
|
584
|
+
* always means "primitive value updates" — the structural-ADD-routes-
|
|
585
|
+
* reliable footgun for ref-type fields can't reach this code path.
|
|
586
|
+
*/
|
|
587
|
+
private _routeAndRecord(index: number, op: OPERATION, raw: boolean): void {
|
|
588
|
+
if (this.paused || this.isFieldStatic(index)) return;
|
|
589
|
+
if (this.isFieldUnreliable(index)) {
|
|
590
|
+
const r = this.ensureUnreliableRecorder();
|
|
591
|
+
if (raw) r.recordRaw(index, op);
|
|
592
|
+
else r.record(index, op);
|
|
593
|
+
this.root?.enqueueUnreliable(this);
|
|
594
|
+
return;
|
|
343
595
|
}
|
|
344
|
-
|
|
596
|
+
if (raw) this.recordRaw(index, op);
|
|
597
|
+
else this.record(index, op);
|
|
598
|
+
this.root?.enqueueChangeTree(this);
|
|
599
|
+
}
|
|
345
600
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
if (index > startIndex) {
|
|
349
|
-
changeSet.operations[i] = index + shiftIndex;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
601
|
+
change(index: number, operation: OPERATION = OPERATION.ADD) {
|
|
602
|
+
this._routeAndRecord(index, operation, false);
|
|
352
603
|
}
|
|
353
604
|
|
|
354
|
-
indexedOperation(index: number, operation: OPERATION
|
|
355
|
-
this.
|
|
605
|
+
indexedOperation(index: number, operation: OPERATION) {
|
|
606
|
+
this._routeAndRecord(index, operation, true);
|
|
607
|
+
}
|
|
356
608
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
609
|
+
// ArraySchema#unshift(): apply shift to both channels.
|
|
610
|
+
// Unreliable recorder on an array is always a CollectionChangeRecorder.
|
|
611
|
+
shiftChangeIndexes(shiftIndex: number) {
|
|
612
|
+
this.shift(shiftIndex);
|
|
613
|
+
(this.unreliableRecorder as ICollectionChangeRecorder | undefined)?.shift(shiftIndex);
|
|
614
|
+
}
|
|
361
615
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
setOperationAtIndex(this.changes, index);
|
|
365
|
-
this.root?.enqueueChangeTree(this, 'changes');
|
|
366
|
-
}
|
|
616
|
+
getChange(index: number) {
|
|
617
|
+
return this.operationAt(index);
|
|
367
618
|
}
|
|
368
619
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
);
|
|
620
|
+
// ────────────────────────────────────────────────────────────────────
|
|
621
|
+
// Change-tracking control API
|
|
622
|
+
// ────────────────────────────────────────────────────────────────────
|
|
623
|
+
|
|
624
|
+
pause(): void { this.paused = true; }
|
|
625
|
+
resume(): void { this.paused = false; }
|
|
626
|
+
|
|
627
|
+
untracked<T>(fn: () => T): T {
|
|
628
|
+
const wasPaused = this.paused;
|
|
629
|
+
this.paused = true;
|
|
630
|
+
try { return fn(); }
|
|
631
|
+
finally { this.paused = wasPaused; }
|
|
380
632
|
}
|
|
381
633
|
|
|
382
|
-
|
|
383
|
-
|
|
634
|
+
// Manually mark a field dirty for the next encode(). Useful after a
|
|
635
|
+
// paused mutation or a nested mutation that bypassed the setter.
|
|
636
|
+
markDirty(index: number, operation: OPERATION = OPERATION.ADD): void {
|
|
637
|
+
const wasPaused = this.paused;
|
|
638
|
+
this.paused = false;
|
|
639
|
+
try { this.change(index, operation); }
|
|
640
|
+
finally { this.paused = wasPaused; }
|
|
384
641
|
}
|
|
385
642
|
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
//
|
|
643
|
+
// used during `.encode()` — `isEncodeAll` is only consumed by ArraySchema.
|
|
644
|
+
// Reads via `refTarget` so ArraySchema's Proxy trap is bypassed on the
|
|
645
|
+
// hot per-field encode path.
|
|
389
646
|
getValue(index: number, isEncodeAll: boolean = false) {
|
|
390
|
-
|
|
391
|
-
// `isEncodeAll` param is only used by ArraySchema
|
|
392
|
-
//
|
|
393
|
-
return (this.ref as any)[$getByIndex](index, isEncodeAll);
|
|
647
|
+
return this.refTarget[$getByIndex](index, isEncodeAll);
|
|
394
648
|
}
|
|
395
649
|
|
|
396
|
-
delete(index: number, operation?: OPERATION
|
|
650
|
+
delete(index: number, operation?: OPERATION) {
|
|
397
651
|
if (index === undefined) {
|
|
398
652
|
try {
|
|
399
653
|
throw new Error(`@colyseus/schema ${this.ref.constructor.name}: trying to delete non-existing index '${index}'`);
|
|
@@ -403,292 +657,193 @@ export class ChangeTree<T extends Ref = any> {
|
|
|
403
657
|
return;
|
|
404
658
|
}
|
|
405
659
|
|
|
406
|
-
|
|
407
|
-
? this.filteredChanges
|
|
408
|
-
: this.changes;
|
|
660
|
+
if (this.paused || this.isFieldStatic(index)) return this.getValue(index);
|
|
409
661
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
662
|
+
const unreliable = this.isFieldUnreliable(index);
|
|
663
|
+
if (unreliable) this.ensureUnreliableRecorder().recordDelete(index, operation ?? OPERATION.DELETE);
|
|
664
|
+
else this.recordDelete(index, operation ?? OPERATION.DELETE);
|
|
413
665
|
|
|
414
666
|
const previousValue = this.getValue(index);
|
|
415
667
|
|
|
416
|
-
//
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
// This method is being called at decoding time when a DELETE operation is found.
|
|
422
|
-
//
|
|
423
|
-
// - This is due to using the concrete Schema class at decoding time.
|
|
424
|
-
// - "Reflected" structures do not have this problem.
|
|
425
|
-
//
|
|
426
|
-
// (The property descriptors should NOT be used at decoding time. only at encoding time.)
|
|
427
|
-
//
|
|
428
|
-
this.root?.remove(previousValue[$changes]);
|
|
429
|
-
}
|
|
668
|
+
// `this.root` is always undefined on decoder-side instances
|
|
669
|
+
// (they're built via `initializeForDecoder`, which skips Root
|
|
670
|
+
// attachment). The optional chain handles both sides; this is
|
|
671
|
+
// an intentional invariant, not a bug.
|
|
672
|
+
if (previousValue && previousValue[$changes]) this.root?.remove(previousValue[$changes]);
|
|
430
673
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
//
|
|
434
|
-
if (this.filteredChanges !== undefined) {
|
|
435
|
-
deleteOperationAtIndex(this.allFilteredChanges, allChangesIndex);
|
|
436
|
-
this.root?.enqueueChangeTree(this, 'filteredChanges');
|
|
437
|
-
|
|
438
|
-
} else {
|
|
439
|
-
this.root?.enqueueChangeTree(this, 'changes');
|
|
440
|
-
}
|
|
674
|
+
if (unreliable) this.root?.enqueueUnreliable(this);
|
|
675
|
+
else this.root?.enqueueChangeTree(this);
|
|
441
676
|
|
|
442
677
|
return previousValue;
|
|
443
678
|
}
|
|
444
679
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
this[changeSetName] = createChangeSet();
|
|
450
|
-
|
|
451
|
-
// ArraySchema and MapSchema have a custom "encode end" method
|
|
680
|
+
// Clear the reliable dirty bucket after a reliable encode pass.
|
|
681
|
+
endEncode() {
|
|
682
|
+
this.reset();
|
|
683
|
+
this.changesNode = undefined;
|
|
452
684
|
(this.ref as any)[$onEncodeEnd]?.();
|
|
453
|
-
|
|
454
|
-
// Not a new instance anymore
|
|
455
685
|
this.isNew = false;
|
|
456
686
|
}
|
|
457
687
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
// REPLACE in case same key is used on next patches.
|
|
463
|
-
//
|
|
688
|
+
// Clear the unreliable dirty bucket after an unreliable encode pass.
|
|
689
|
+
endEncodeUnreliable() {
|
|
690
|
+
this.unreliableRecorder?.reset();
|
|
691
|
+
this.unreliableChangesNode = undefined;
|
|
464
692
|
(this.ref as any)[$onEncodeEnd]?.();
|
|
693
|
+
}
|
|
465
694
|
|
|
466
|
-
|
|
467
|
-
this.
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
this.filteredChanges = createChangeSet(this.filteredChanges.queueRootNode);
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
if (discardAll) {
|
|
474
|
-
// preserve queueRootNode references
|
|
475
|
-
this.allChanges = createChangeSet(this.allChanges.queueRootNode);
|
|
476
|
-
|
|
477
|
-
if (this.allFilteredChanges !== undefined) {
|
|
478
|
-
this.allFilteredChanges = createChangeSet(this.allFilteredChanges.queueRootNode);
|
|
479
|
-
}
|
|
480
|
-
}
|
|
695
|
+
discard() {
|
|
696
|
+
(this.ref as any)[$onEncodeEnd]?.();
|
|
697
|
+
this.reset();
|
|
698
|
+
this.unreliableRecorder?.reset();
|
|
481
699
|
}
|
|
482
700
|
|
|
483
|
-
|
|
484
|
-
* Recursively discard all changes from this, and child structures.
|
|
485
|
-
* (Used in tests only)
|
|
486
|
-
*/
|
|
701
|
+
// Recursively discard all changes on this + child structures. Tests only.
|
|
487
702
|
discardAll() {
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
const value = this.getValue(
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
}
|
|
496
|
-
|
|
703
|
+
const discardChild = (index: number) => {
|
|
704
|
+
if (index < 0) return;
|
|
705
|
+
const value = this.getValue(index);
|
|
706
|
+
if (value && value[$changes]) value[$changes].discardAll();
|
|
707
|
+
};
|
|
708
|
+
this.forEach(discardChild);
|
|
709
|
+
this.unreliableRecorder?.forEach(discardChild);
|
|
497
710
|
this.discard();
|
|
498
711
|
}
|
|
499
712
|
|
|
500
713
|
get changed() {
|
|
501
|
-
return
|
|
714
|
+
return this.has() || (this.unreliableRecorder?.has() ?? false);
|
|
502
715
|
}
|
|
503
716
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
// At Schema initialization, the "root" structure might not be available
|
|
508
|
-
// yet, as it only does once the "Encoder" has been set up.
|
|
509
|
-
//
|
|
510
|
-
// So the "parent" may be already set without a "root".
|
|
511
|
-
//
|
|
512
|
-
this._checkFilteredByParent(parent, parentIndex);
|
|
717
|
+
// ────────────────────────────────────────────────────────────────────
|
|
718
|
+
// Parent chain — implementations in ./changeTree/parentChain.ts.
|
|
719
|
+
// ────────────────────────────────────────────────────────────────────
|
|
513
720
|
|
|
514
|
-
|
|
515
|
-
|
|
721
|
+
/** Immediate parent (primary). See `extraParents` for the 2nd+ chain. */
|
|
722
|
+
get parent(): Ref | undefined { return this.parentRef; }
|
|
723
|
+
get parentIndex(): number | undefined { return this._parentIndex; }
|
|
516
724
|
|
|
517
|
-
|
|
518
|
-
this.root?.enqueueChangeTree(this, 'allFilteredChanges');
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
}
|
|
725
|
+
addParent(parent: Ref, index: number): void { _addParent(this, parent, index); }
|
|
522
726
|
|
|
523
|
-
|
|
524
|
-
|
|
727
|
+
/** @returns true if parent was found and removed */
|
|
728
|
+
removeParent(parent: Ref = this.parent): boolean { return _removeParent(this, parent); }
|
|
525
729
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
}
|
|
529
|
-
}
|
|
730
|
+
findParent(predicate: (parent: Ref, index: number) => boolean): ParentChain | undefined {
|
|
731
|
+
return _findParent(this, predicate);
|
|
530
732
|
}
|
|
531
733
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
if (!parent) { return; }
|
|
535
|
-
|
|
536
|
-
//
|
|
537
|
-
// ArraySchema | MapSchema - get the child type
|
|
538
|
-
// (if refType is typeof string, the parentFiltered[key] below will always be invalid)
|
|
539
|
-
//
|
|
540
|
-
const refType = Metadata.isValidInstance(this.ref)
|
|
541
|
-
? this.ref.constructor
|
|
542
|
-
: (this.ref as any)[$childType];
|
|
543
|
-
|
|
544
|
-
let parentChangeTree: ChangeTree;
|
|
545
|
-
|
|
546
|
-
let parentIsCollection = !Metadata.isValidInstance(parent);
|
|
547
|
-
if (parentIsCollection) {
|
|
548
|
-
parentChangeTree = parent[$changes];
|
|
549
|
-
parent = parentChangeTree.parent;
|
|
550
|
-
parentIndex = parentChangeTree.parentIndex;
|
|
551
|
-
|
|
552
|
-
} else {
|
|
553
|
-
parentChangeTree = parent[$changes]
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
const parentConstructor = parent.constructor as typeof Schema;
|
|
557
|
-
|
|
558
|
-
let key = `${this.root.types.getTypeId(refType as typeof Schema)}`;
|
|
559
|
-
if (parentConstructor) {
|
|
560
|
-
key += `-${this.root.types.schemas.get(parentConstructor)}`;
|
|
561
|
-
}
|
|
562
|
-
key += `-${parentIndex}`;
|
|
563
|
-
|
|
564
|
-
const fieldHasViewTag = Metadata.hasViewTagAtIndex(parentConstructor?.[Symbol.metadata], parentIndex);
|
|
565
|
-
|
|
566
|
-
this.isFiltered = parent[$changes].isFiltered // in case parent is already filtered
|
|
567
|
-
|| this.root.types.parentFiltered[key]
|
|
568
|
-
|| fieldHasViewTag;
|
|
569
|
-
|
|
570
|
-
//
|
|
571
|
-
// "isFiltered" may not be imedialely available during `change()` due to the instance not being attached to the root yet.
|
|
572
|
-
// when it's available, we need to enqueue the "changes" changeset into the "filteredChanges" changeset.
|
|
573
|
-
//
|
|
574
|
-
if (this.isFiltered) {
|
|
575
|
-
|
|
576
|
-
this.isVisibilitySharedWithParent = (
|
|
577
|
-
parentChangeTree.isFiltered &&
|
|
578
|
-
typeof (refType) !== "string" &&
|
|
579
|
-
!fieldHasViewTag &&
|
|
580
|
-
parentIsCollection
|
|
581
|
-
);
|
|
582
|
-
|
|
583
|
-
if (!this.filteredChanges) {
|
|
584
|
-
this.filteredChanges = createChangeSet();
|
|
585
|
-
this.allFilteredChanges = createChangeSet();
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
if (this.changes.operations.length > 0) {
|
|
589
|
-
this.changes.operations.forEach((index) =>
|
|
590
|
-
setOperationAtIndex(this.filteredChanges, index));
|
|
591
|
-
|
|
592
|
-
this.allChanges.operations.forEach((index) =>
|
|
593
|
-
setOperationAtIndex(this.allFilteredChanges, index));
|
|
594
|
-
|
|
595
|
-
this.changes = createChangeSet();
|
|
596
|
-
this.allChanges = createChangeSet();
|
|
597
|
-
}
|
|
598
|
-
}
|
|
734
|
+
hasParent(predicate: (parent: Ref, index: number) => boolean): boolean {
|
|
735
|
+
return _hasParent(this, predicate);
|
|
599
736
|
}
|
|
600
737
|
|
|
601
|
-
|
|
602
|
-
* Get the immediate parent
|
|
603
|
-
*/
|
|
604
|
-
get parent(): Ref | undefined {
|
|
605
|
-
return this.parentChain?.ref;
|
|
606
|
-
}
|
|
738
|
+
getAllParents(): Array<{ ref: Ref, index: number }> { return _getAllParents(this); }
|
|
607
739
|
|
|
608
|
-
|
|
609
|
-
* Get the immediate parent index
|
|
610
|
-
*/
|
|
611
|
-
get parentIndex(): number | undefined {
|
|
612
|
-
return this.parentChain?.index;
|
|
613
|
-
}
|
|
740
|
+
}
|
|
614
741
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
742
|
+
/**
|
|
743
|
+
* Lightweight per-instance no-op ChangeTree used for instances the decoder
|
|
744
|
+
* builds. Those instances never feed back into an Encoder, so the full
|
|
745
|
+
* `ChangeTree` machinery (EncodeDescriptor lookup, recorder state, Maps /
|
|
746
|
+
* Uint8Arrays for change slots) is pure overhead — this stub carries only a
|
|
747
|
+
* `ref` back-pointer and no-op methods, so tree walkers and debug tooling
|
|
748
|
+
* continue to work.
|
|
749
|
+
*
|
|
750
|
+
* Plug-in contract: each collection class and the `Decoder` pick between
|
|
751
|
+
* `new ChangeTree(ref)` and `createUntrackedChangeTree(ref)` explicitly via
|
|
752
|
+
* dedicated factories (`initializeForDecoder` on collections,
|
|
753
|
+
* `createInstanceOfType` on the `Decoder`). There is no global state — every
|
|
754
|
+
* decision is local to the call site.
|
|
755
|
+
*/
|
|
756
|
+
export class UntrackedChangeTree {
|
|
757
|
+
ref: Ref;
|
|
625
758
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
759
|
+
// Mirror the subset of ChangeTree state that decoder-path readers touch.
|
|
760
|
+
// Everything else is deliberately undefined (matches the shape of a
|
|
761
|
+
// freshly-constructed tree that never participated in a Root).
|
|
762
|
+
root: undefined = undefined;
|
|
763
|
+
parentRef: undefined = undefined;
|
|
764
|
+
paused: boolean = false;
|
|
765
|
+
isNew: boolean = false;
|
|
766
|
+
flags: number = 0;
|
|
767
|
+
|
|
768
|
+
constructor(ref: Ref) {
|
|
769
|
+
this.ref = ref;
|
|
631
770
|
}
|
|
632
771
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
772
|
+
// Mutation surface — all no-ops.
|
|
773
|
+
change(): void {}
|
|
774
|
+
delete(): void {}
|
|
775
|
+
indexedOperation(): void {}
|
|
776
|
+
operation(): void {}
|
|
777
|
+
setParent(): void {}
|
|
778
|
+
addParent(): void {}
|
|
779
|
+
removeParent(): boolean { return false; }
|
|
780
|
+
getChange(): number { return 0; }
|
|
781
|
+
discard(): void {}
|
|
782
|
+
discardAll(): void {}
|
|
783
|
+
pause(): void {}
|
|
784
|
+
resume(): void {}
|
|
785
|
+
untracked<T>(fn: () => T): T { return fn(); }
|
|
786
|
+
markDirty(): void {}
|
|
787
|
+
|
|
788
|
+
// Tree-walk surface. Mirrors `treeAttachment.forEachChild` so debug tools
|
|
789
|
+
// and `ArraySchema.clear()` can still descend from a tracked root into
|
|
790
|
+
// decoder-built subtrees and read each child's `$changes` (which is
|
|
791
|
+
// itself an UntrackedChangeTree carrying the right `ref`).
|
|
792
|
+
forEachChild(callback: (change: any, at: any) => void): void {
|
|
793
|
+
const ref = this.ref as any;
|
|
794
|
+
if (ref[$childType]) {
|
|
795
|
+
if (typeof ref[$childType] !== "string") {
|
|
796
|
+
for (const [key, value] of ref.entries()) {
|
|
797
|
+
if (!value) continue;
|
|
798
|
+
callback(value[$changes], ref._collectionIndexes?.[key] ?? key);
|
|
651
799
|
}
|
|
652
|
-
return true;
|
|
653
800
|
}
|
|
654
|
-
|
|
655
|
-
current = current.next;
|
|
801
|
+
return;
|
|
656
802
|
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
if (predicate(current.ref, current.index)) {
|
|
667
|
-
return current;
|
|
668
|
-
}
|
|
669
|
-
current = current.next;
|
|
803
|
+
const ctor = ref.constructor as any;
|
|
804
|
+
const metadata = ctor?.[Symbol.metadata];
|
|
805
|
+
if (!metadata) return;
|
|
806
|
+
const refFieldIndexes: number[] = metadata[$refTypeFieldIndexes] ?? [];
|
|
807
|
+
for (let i = 0; i < refFieldIndexes.length; i++) {
|
|
808
|
+
const index = refFieldIndexes[i];
|
|
809
|
+
const value = ref[metadata[index].name];
|
|
810
|
+
if (!value) continue;
|
|
811
|
+
callback(value[$changes], index);
|
|
670
812
|
}
|
|
671
|
-
return undefined;
|
|
672
813
|
}
|
|
673
814
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
*/
|
|
677
|
-
hasParent(predicate: (parent: Ref, index: number) => boolean): boolean {
|
|
678
|
-
return this.findParent(predicate) !== undefined;
|
|
815
|
+
forEachChildWithCtx<C>(ctx: C, callback: (ctx: C, change: any, at: any) => void): void {
|
|
816
|
+
this.forEachChild((change, at) => callback(ctx, change, at));
|
|
679
817
|
}
|
|
680
818
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
}
|
|
819
|
+
forEachLive(): void {}
|
|
820
|
+
forEachLiveWithCtx(): void {}
|
|
821
|
+
forEach(): void {}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Factory, cast to ChangeTree so call sites that type `$changes` as
|
|
825
|
+
// `ChangeTree` accept it. The surface overlap above covers every read/write
|
|
826
|
+
// the decoder path reaches.
|
|
827
|
+
export function createUntrackedChangeTree(ref: Ref): ChangeTree {
|
|
828
|
+
return new UntrackedChangeTree(ref) as unknown as ChangeTree;
|
|
829
|
+
}
|
|
693
830
|
|
|
831
|
+
/**
|
|
832
|
+
* Install a non-enumerable `$changes: UntrackedChangeTree` on `target`.
|
|
833
|
+
* Shared by `Schema.initializeForDecoder` and every collection's
|
|
834
|
+
* `initializeForDecoder`. `publicRef` defaults to `target` — pass a Proxy
|
|
835
|
+
* instead (ArraySchema) so children attached to this tree see the Proxy
|
|
836
|
+
* as their parent, not the raw target.
|
|
837
|
+
*
|
|
838
|
+
* `enumerable: false` is load-bearing — tests use `deepStrictEqual` on
|
|
839
|
+
* decoded instances and walking into `$changes` would recurse through
|
|
840
|
+
* circular refs. Same descriptor shape as the tracked `Schema.initialize`
|
|
841
|
+
* + collection ctors.
|
|
842
|
+
*/
|
|
843
|
+
export function installUntrackedChangeTree(target: object, publicRef: object = target): void {
|
|
844
|
+
Object.defineProperty(target, $changes, {
|
|
845
|
+
value: createUntrackedChangeTree(publicRef as Ref),
|
|
846
|
+
enumerable: false,
|
|
847
|
+
writable: true,
|
|
848
|
+
});
|
|
694
849
|
}
|