@colyseus/schema 4.0.20 → 5.0.1
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 +56 -2
- package/build/Reflection.d.ts +28 -34
- package/build/Schema.d.ts +70 -9
- package/build/annotations.d.ts +64 -17
- package/build/codegen/cli.cjs +84 -67
- 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 +5258 -1549
- package/build/index.cjs.map +1 -1
- package/build/index.d.ts +7 -3
- package/build/index.js +5258 -1549
- package/build/index.mjs +5249 -1549
- 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 +7453 -0
- package/build/input/index.cjs.map +1 -0
- package/build/input/index.d.ts +3 -0
- package/build/input/index.mjs +7450 -0
- package/build/input/index.mjs.map +1 -0
- package/build/types/HelperTypes.d.ts +67 -9
- package/build/types/TypeContext.d.ts +9 -0
- package/build/types/builder.d.ts +192 -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 +9 -3
- package/src/Metadata.ts +259 -31
- package/src/Reflection.ts +15 -13
- package/src/Schema.ts +176 -134
- package/src/annotations.ts +365 -252
- 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/languages/csharp.ts +0 -24
- 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 +121 -24
- package/src/types/TypeContext.ts +14 -2
- package/src/types/builder.ts +331 -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 +93 -6
- package/src/utils.ts +4 -6
package/src/encoder/Encoder.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Schema } from "../Schema.js";
|
|
2
2
|
import { TypeContext } from "../types/TypeContext.js";
|
|
3
|
-
import { $changes, $
|
|
3
|
+
import { $changes, $getByIndex, $refId } from "../types/symbols.js";
|
|
4
|
+
import { Metadata } from "../Metadata.js";
|
|
4
5
|
|
|
5
6
|
import { encode } from "../encoding/encode.js";
|
|
6
7
|
import type { Iterator } from "../encoding/decode.js";
|
|
@@ -9,8 +10,187 @@ import { OPERATION, SWITCH_TO_STRUCTURE, TYPE_ID } from '../encoding/spec.js';
|
|
|
9
10
|
import { Root } from "./Root.js";
|
|
10
11
|
|
|
11
12
|
import type { StateView } from "./StateView.js";
|
|
12
|
-
import type {
|
|
13
|
-
import {
|
|
13
|
+
import type { ChangeTree, ChangeTreeList, ChangeTreeNode } from "./ChangeTree.js";
|
|
14
|
+
import type { EncodeOperation } from "./EncodeOperation.js";
|
|
15
|
+
import { forEachLiveWithCtx as _forEachLiveWithCtx } from "./changeTree/liveIteration.js";
|
|
16
|
+
import { forEachChildWithCtx as _forEachChildWithCtx } from "./changeTree/treeAttachment.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Reusable context passed to the recorder's forEachWithCtx to iterate changes
|
|
20
|
+
* without allocating a closure per ChangeTree. All fields are (re)assigned
|
|
21
|
+
* inside the main encode loop before each `forEachWithCtx` call.
|
|
22
|
+
*/
|
|
23
|
+
interface EncodeCtx {
|
|
24
|
+
self: Encoder;
|
|
25
|
+
buffer: Uint8Array;
|
|
26
|
+
it: Iterator;
|
|
27
|
+
changeTree: ChangeTree;
|
|
28
|
+
ref: any;
|
|
29
|
+
encoder: EncodeOperation;
|
|
30
|
+
filter: ((ref: any, index: number, view?: StateView) => boolean) | undefined;
|
|
31
|
+
metadata: any;
|
|
32
|
+
view: StateView | undefined;
|
|
33
|
+
isEncodeAll: boolean;
|
|
34
|
+
hasView: boolean;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Per-tree flags, reset before each `forEachWithCtx` call. The per-field
|
|
38
|
+
* filter decision (`emitFiltered` == `treeIsFiltered || metadata[i].tag`)
|
|
39
|
+
* matches `ChangeTree.change()`'s routing rule exactly.
|
|
40
|
+
*/
|
|
41
|
+
treeIsFiltered: boolean;
|
|
42
|
+
isSchema: boolean;
|
|
43
|
+
emitFiltered: boolean;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Bitmask: bit i set iff field i has a @view tag. Lets the per-field
|
|
47
|
+
* filter check be a single bitwise op instead of a metadata[i]?.tag chase.
|
|
48
|
+
* Always 0 for collection trees.
|
|
49
|
+
*/
|
|
50
|
+
filterBitmask: number;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Current walk's visit stamp. `_fullSyncWalk` compares it against each
|
|
54
|
+
* tree's `_fullSyncGen` on entry: match means "already visited by
|
|
55
|
+
* this walk, skip"; mismatch means "first visit, stamp and recurse".
|
|
56
|
+
* Rewritten per walk by `encodeFullSync` before kicking off the DFS.
|
|
57
|
+
*/
|
|
58
|
+
gen: number;
|
|
59
|
+
/** Initial buffer offset at encodeFullSync entry — used by the walker to
|
|
60
|
+
* decide whether to emit SWITCH_TO_STRUCTURE for the root tree. */
|
|
61
|
+
initialOffset: number;
|
|
62
|
+
rootChangeTree: ChangeTree;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Lazy structure-switch state. The switch header is emitted right before
|
|
66
|
+
* the first field of a tree actually passes the filter, so trees that
|
|
67
|
+
* contribute zero bytes in a given pass don't leave orphaned headers.
|
|
68
|
+
*/
|
|
69
|
+
structSwitchEmitted: boolean;
|
|
70
|
+
isRootTree: boolean;
|
|
71
|
+
shouldEmitSwitch: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Emit the lazy structure-switch header (SWITCH_TO_STRUCTURE + refId) for
|
|
77
|
+
* the current tree if it hasn't been emitted yet in this pass.
|
|
78
|
+
*/
|
|
79
|
+
function ensureStructSwitch(ctx: EncodeCtx): void {
|
|
80
|
+
if (ctx.structSwitchEmitted) return;
|
|
81
|
+
if (ctx.shouldEmitSwitch) {
|
|
82
|
+
ctx.buffer[ctx.it.offset++] = SWITCH_TO_STRUCTURE & 255;
|
|
83
|
+
encode.number(ctx.buffer, ctx.ref[$refId], ctx.it);
|
|
84
|
+
}
|
|
85
|
+
ctx.structSwitchEmitted = true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Module-level adapter for `forEachLiveWithCtx`. Full-sync emits every live
|
|
90
|
+
* field as ADD, so we re-enter `encodeChangeCb` with that fixed op — keeps
|
|
91
|
+
* the callback closure-free across the entire DFS walk.
|
|
92
|
+
*/
|
|
93
|
+
function encodeFullSyncCb(ctx: EncodeCtx, fieldIndex: number): void {
|
|
94
|
+
encodeChangeCb(ctx, fieldIndex, OPERATION.ADD);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Structural DFS walker for `encodeFullSync`. Hoisted to module scope so
|
|
99
|
+
* the recursion allocates no per-tree closures — `_fullSyncWalkChildCb`
|
|
100
|
+
* captures nothing and is handed to `forEachChildWithCtx` once.
|
|
101
|
+
*
|
|
102
|
+
* The stamp check at the top (`tree._fullSyncGen === ctx.gen`) is how we
|
|
103
|
+
* skip shared refs that are reachable through more than one parent. On
|
|
104
|
+
* first visit the tree's stamp differs from the walk's current `ctx.gen`;
|
|
105
|
+
* we write `ctx.gen` onto the tree and recurse. Any later reach of the
|
|
106
|
+
* same tree during the SAME walk will find matching stamps and bail.
|
|
107
|
+
* Next walk bumps `ctx.gen`, so every tree starts out stale again.
|
|
108
|
+
*/
|
|
109
|
+
function _fullSyncWalk(ctx: EncodeCtx, changeTree: ChangeTree): void {
|
|
110
|
+
if (changeTree._fullSyncGen === ctx.gen) return;
|
|
111
|
+
changeTree._fullSyncGen = ctx.gen;
|
|
112
|
+
|
|
113
|
+
// Visibility gate: when a view is active, a non-visible tree contributes
|
|
114
|
+
// nothing itself but we still recurse so descendants (possibly added to
|
|
115
|
+
// the view explicitly) are reachable.
|
|
116
|
+
let visibleHere = true;
|
|
117
|
+
if (ctx.hasView) {
|
|
118
|
+
const view = ctx.view!;
|
|
119
|
+
if (!view.isChangeTreeVisible(changeTree)) {
|
|
120
|
+
view.markInvisible(changeTree);
|
|
121
|
+
visibleHere = false;
|
|
122
|
+
} else {
|
|
123
|
+
view.unmarkInvisible(changeTree);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (visibleHere) {
|
|
128
|
+
const desc = changeTree.encDescriptor;
|
|
129
|
+
ctx.changeTree = changeTree;
|
|
130
|
+
ctx.ref = changeTree.ref;
|
|
131
|
+
ctx.encoder = desc.encoder;
|
|
132
|
+
ctx.filter = desc.filter;
|
|
133
|
+
ctx.metadata = desc.metadata;
|
|
134
|
+
ctx.treeIsFiltered = changeTree.isFiltered;
|
|
135
|
+
ctx.isSchema = desc.isSchema;
|
|
136
|
+
ctx.filterBitmask = desc.filterBitmask;
|
|
137
|
+
ctx.structSwitchEmitted = false;
|
|
138
|
+
ctx.shouldEmitSwitch = (ctx.hasView || ctx.it.offset > ctx.initialOffset || changeTree !== ctx.rootChangeTree);
|
|
139
|
+
|
|
140
|
+
// Call the module function directly — the `forEachLiveWithCtx`
|
|
141
|
+
// method on ChangeTree is a pass-through that V8 doesn't inline
|
|
142
|
+
// under the polymorphism the encoder sees (Schema + every
|
|
143
|
+
// collection class share the method slot). Direct call saves the
|
|
144
|
+
// dispatched frame.
|
|
145
|
+
_forEachLiveWithCtx(changeTree, ctx, encodeFullSyncCb);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
_forEachChildWithCtx(changeTree, ctx, _fullSyncWalkChildCb);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Child-iteration callback for `_fullSyncWalk`. Module-level + closure-free:
|
|
153
|
+
* just re-enters `_fullSyncWalk` on each child. Replaces a per-tree
|
|
154
|
+
* `(child, _) => walk(child)` closure that used to allocate 2.5M times in
|
|
155
|
+
* `encodeAll(5000 entities) x 500 iterations`.
|
|
156
|
+
*/
|
|
157
|
+
function _fullSyncWalkChildCb(ctx: EncodeCtx, child: ChangeTree, _index: any): void {
|
|
158
|
+
_fullSyncWalk(ctx, child);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Pure (non-capturing) callback for recorder.forEachWithCtx. Module-level so
|
|
163
|
+
* V8 never needs to allocate a fresh function per tree. Decides per-field
|
|
164
|
+
* whether to emit based on the unified filter rule, then defers to the
|
|
165
|
+
* per-type encode function.
|
|
166
|
+
*/
|
|
167
|
+
function encodeChangeCb(ctx: EncodeCtx, fieldIndex: number, op: OPERATION): void {
|
|
168
|
+
if (fieldIndex < 0) {
|
|
169
|
+
// Pure op (CLEAR/REVERSE): encoded as a single byte. Always emitted
|
|
170
|
+
// for the pass that matches the tree's filter classification —
|
|
171
|
+
// collections route pure ops to their single dirty bucket.
|
|
172
|
+
if (ctx.treeIsFiltered !== ctx.emitFiltered) return;
|
|
173
|
+
ensureStructSwitch(ctx);
|
|
174
|
+
ctx.buffer[ctx.it.offset++] = Math.abs(fieldIndex) & 255;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Per-field filter decision (same rule as ChangeTree.change()):
|
|
179
|
+
// a field is filtered iff the tree inherits isFiltered OR the field
|
|
180
|
+
// itself carries a @view tag. Schema trees check via the precomputed
|
|
181
|
+
// bitmask; collection trees inherit tree-level (bitmask is 0).
|
|
182
|
+
const fieldFiltered = ctx.isSchema
|
|
183
|
+
? (ctx.treeIsFiltered || (ctx.filterBitmask & (1 << fieldIndex)) !== 0)
|
|
184
|
+
: ctx.treeIsFiltered;
|
|
185
|
+
if (fieldFiltered !== ctx.emitFiltered) return;
|
|
186
|
+
|
|
187
|
+
const operation = ctx.isEncodeAll ? OPERATION.ADD : op;
|
|
188
|
+
if (operation === undefined) return;
|
|
189
|
+
if (ctx.filter !== undefined && !ctx.filter(ctx.ref, fieldIndex, ctx.view)) return;
|
|
190
|
+
|
|
191
|
+
ensureStructSwitch(ctx);
|
|
192
|
+
ctx.encoder(ctx.self, ctx.buffer, ctx.changeTree, fieldIndex, operation, ctx.it, ctx.isEncodeAll, ctx.hasView, ctx.metadata);
|
|
193
|
+
}
|
|
14
194
|
|
|
15
195
|
function concatBytes(a: Uint8Array, b: Uint8Array): Uint8Array {
|
|
16
196
|
const result = new Uint8Array(a.length + b.length);
|
|
@@ -28,7 +208,7 @@ export class Encoder<T extends Schema = any> {
|
|
|
28
208
|
|
|
29
209
|
root: Root;
|
|
30
210
|
|
|
31
|
-
constructor(state: T) {
|
|
211
|
+
constructor(state: T, root?: Root) {
|
|
32
212
|
//
|
|
33
213
|
// Use .cache() here to avoid re-creating a new context for every new room instance.
|
|
34
214
|
//
|
|
@@ -36,14 +216,9 @@ export class Encoder<T extends Schema = any> {
|
|
|
36
216
|
// schemas - which would lead to memory leaks
|
|
37
217
|
//
|
|
38
218
|
this.context = TypeContext.cache(state.constructor as typeof Schema);
|
|
39
|
-
this.root = new Root(this.context);
|
|
219
|
+
this.root = root ?? new Root(this.context);
|
|
40
220
|
|
|
41
221
|
this.setState(state);
|
|
42
|
-
|
|
43
|
-
// console.log(">>>>>>>>>>>>>>>> Encoder types");
|
|
44
|
-
// this.context.schemas.forEach((id, schema) => {
|
|
45
|
-
// console.log("type:", id, schema.name, Object.keys(schema[Symbol.metadata]));
|
|
46
|
-
// });
|
|
47
222
|
}
|
|
48
223
|
|
|
49
224
|
protected setState(state: T) {
|
|
@@ -51,118 +226,190 @@ export class Encoder<T extends Schema = any> {
|
|
|
51
226
|
this.state[$changes].setRoot(this.root);
|
|
52
227
|
}
|
|
53
228
|
|
|
229
|
+
private _encodeCtx: EncodeCtx = {
|
|
230
|
+
self: undefined!, buffer: undefined!, it: undefined!, changeTree: undefined!,
|
|
231
|
+
ref: undefined, encoder: undefined!, filter: undefined, metadata: undefined,
|
|
232
|
+
view: undefined, isEncodeAll: false, hasView: false,
|
|
233
|
+
treeIsFiltered: false, isSchema: false, emitFiltered: false,
|
|
234
|
+
filterBitmask: 0,
|
|
235
|
+
structSwitchEmitted: false, isRootTree: false, shouldEmitSwitch: false,
|
|
236
|
+
gen: 0, initialOffset: 0, rootChangeTree: undefined!,
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Monotonic counter bumped at the start of every `encodeFullSync`
|
|
241
|
+
* call. The new value is copied to `ctx.gen` and stamped into every
|
|
242
|
+
* tree the walk touches (`tree._fullSyncGen = ctx.gen`); subsequent
|
|
243
|
+
* revisits of the same tree detect the equality and return early.
|
|
244
|
+
*/
|
|
245
|
+
private _fullSyncGen: number = 0;
|
|
246
|
+
|
|
54
247
|
encode(
|
|
55
248
|
it: Iterator = { offset: 0 },
|
|
56
249
|
view?: StateView,
|
|
57
250
|
buffer: Uint8Array = this.sharedBuffer,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
251
|
+
initialOffset = it.offset
|
|
252
|
+
): Uint8Array {
|
|
253
|
+
return this._encodeChannel(it, view, buffer, initialOffset, /* unreliable */ false);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Per-tick encode of the UNRELIABLE channel. Walks `root.unreliableChanges`
|
|
258
|
+
* and emits each tree's `unreliableRecorder`. Safe to call at a different
|
|
259
|
+
* cadence than `encode()` (e.g. 60Hz vs 20Hz) — the two channels are
|
|
260
|
+
* fully independent.
|
|
261
|
+
*/
|
|
262
|
+
encodeUnreliable(
|
|
263
|
+
it: Iterator = { offset: 0 },
|
|
264
|
+
view?: StateView,
|
|
265
|
+
buffer: Uint8Array = this.sharedBuffer,
|
|
266
|
+
initialOffset = it.offset
|
|
267
|
+
): Uint8Array {
|
|
268
|
+
return this._encodeChannel(it, view, buffer, initialOffset, /* unreliable */ true);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private _encodeChannel(
|
|
272
|
+
it: Iterator,
|
|
273
|
+
view: StateView | undefined,
|
|
274
|
+
buffer: Uint8Array,
|
|
275
|
+
initialOffset: number,
|
|
276
|
+
unreliable: boolean,
|
|
61
277
|
): Uint8Array {
|
|
62
278
|
const hasView = (view !== undefined);
|
|
63
279
|
const rootChangeTree = this.state[$changes];
|
|
64
280
|
|
|
65
|
-
|
|
281
|
+
const ctx = this._encodeCtx;
|
|
282
|
+
ctx.self = this;
|
|
283
|
+
ctx.buffer = buffer;
|
|
284
|
+
ctx.it = it;
|
|
285
|
+
ctx.view = view;
|
|
286
|
+
ctx.isEncodeAll = false;
|
|
287
|
+
ctx.hasView = hasView;
|
|
288
|
+
// Shared pass (no view): emit unfiltered fields. View pass: emit
|
|
289
|
+
// filtered fields only. Fields on the other side of the split are
|
|
290
|
+
// skipped inside encodeChangeCb.
|
|
291
|
+
ctx.emitFiltered = hasView;
|
|
292
|
+
|
|
293
|
+
const queue: ChangeTreeList = unreliable ? this.root.unreliableChanges : this.root.changes;
|
|
294
|
+
let current: ChangeTreeList | ChangeTreeNode = queue;
|
|
66
295
|
|
|
67
296
|
while (current = current.next) {
|
|
68
297
|
const changeTree = (current as ChangeTreeNode).changeTree;
|
|
69
298
|
|
|
70
299
|
if (hasView) {
|
|
71
300
|
if (!view.isChangeTreeVisible(changeTree)) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
continue; // skip this change tree
|
|
301
|
+
view.markInvisible(changeTree);
|
|
302
|
+
continue;
|
|
75
303
|
}
|
|
76
|
-
view.
|
|
304
|
+
view.unmarkInvisible(changeTree);
|
|
77
305
|
}
|
|
78
306
|
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
307
|
+
const recorder = unreliable ? changeTree.unreliableRecorder : changeTree;
|
|
308
|
+
if (!recorder || !recorder.has()) { continue; }
|
|
309
|
+
|
|
310
|
+
const desc = changeTree.encDescriptor;
|
|
311
|
+
ctx.changeTree = changeTree;
|
|
312
|
+
ctx.ref = changeTree.ref;
|
|
313
|
+
ctx.encoder = desc.encoder;
|
|
314
|
+
ctx.filter = desc.filter;
|
|
315
|
+
ctx.metadata = desc.metadata;
|
|
316
|
+
ctx.treeIsFiltered = changeTree.isFiltered;
|
|
317
|
+
ctx.isSchema = desc.isSchema;
|
|
318
|
+
ctx.filterBitmask = desc.filterBitmask;
|
|
319
|
+
ctx.structSwitchEmitted = false;
|
|
320
|
+
ctx.isRootTree = (changeTree === rootChangeTree);
|
|
321
|
+
// Root's struct switch is skipped at the very start of the shared
|
|
322
|
+
// pass (matches the legacy wire protocol). In view pass or after
|
|
323
|
+
// the first emission, always emit the switch.
|
|
324
|
+
ctx.shouldEmitSwitch = (hasView || it.offset > initialOffset || !ctx.isRootTree);
|
|
325
|
+
|
|
326
|
+
recorder.forEachWithCtx(ctx, encodeChangeCb);
|
|
327
|
+
}
|
|
85
328
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
329
|
+
// Broadcast-mode stream emission runs after the main loop (state /
|
|
330
|
+
// parent refs are already on the wire, so stream ADD ops can
|
|
331
|
+
// reference element refIds safely). Reliable shared pass only;
|
|
332
|
+
// skipped when any StateView is registered (priority pass in
|
|
333
|
+
// `encodeView` owns emission in that mode).
|
|
334
|
+
if (!unreliable && !hasView && this.root.activeViews.size === 0 && this.root.streamTrees.size > 0) {
|
|
335
|
+
this._emitStreamBroadcast(buffer, it);
|
|
336
|
+
}
|
|
90
337
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
encode.number(buffer, ref[$refId], it);
|
|
96
|
-
}
|
|
338
|
+
if (it.offset > buffer.byteLength) {
|
|
339
|
+
buffer = this._resizeBuffer(buffer, it.offset);
|
|
340
|
+
return this._encodeChannel({ offset: initialOffset }, view, buffer, initialOffset, unreliable);
|
|
341
|
+
}
|
|
97
342
|
|
|
98
|
-
|
|
99
|
-
|
|
343
|
+
return buffer.subarray(0, it.offset);
|
|
344
|
+
}
|
|
100
345
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
346
|
+
/**
|
|
347
|
+
* Structural DFS walker for full-sync (encodeAll / encodeAllView).
|
|
348
|
+
* Visits each ChangeTree in DFS preorder starting from the state root,
|
|
349
|
+
* emitting ADD operations for every currently-populated index via
|
|
350
|
+
* {@link ChangeTree.forEachLive}.
|
|
351
|
+
*/
|
|
352
|
+
private encodeFullSync(
|
|
353
|
+
it: Iterator,
|
|
354
|
+
buffer: Uint8Array,
|
|
355
|
+
emitFiltered: boolean,
|
|
356
|
+
view?: StateView,
|
|
357
|
+
initialOffset: number = it.offset
|
|
358
|
+
): Uint8Array {
|
|
359
|
+
const hasView = (view !== undefined);
|
|
360
|
+
const rootChangeTree = this.state[$changes];
|
|
107
361
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
362
|
+
const ctx = this._encodeCtx;
|
|
363
|
+
ctx.self = this;
|
|
364
|
+
ctx.buffer = buffer;
|
|
365
|
+
ctx.it = it;
|
|
366
|
+
ctx.view = view;
|
|
367
|
+
ctx.isEncodeAll = true;
|
|
368
|
+
ctx.hasView = hasView;
|
|
369
|
+
ctx.emitFiltered = emitFiltered;
|
|
370
|
+
|
|
371
|
+
// Bump the generation counter and carry the new value on `ctx` so
|
|
372
|
+
// the recursive walker can stamp every tree it visits. Any tree
|
|
373
|
+
// still holding the previous walk's stamp is treated as unvisited
|
|
374
|
+
// on first reach, and stamped; a second reach (shared ref via
|
|
375
|
+
// multiple parents) sees the match and bails.
|
|
376
|
+
ctx.gen = ++this._fullSyncGen;
|
|
377
|
+
ctx.initialOffset = initialOffset;
|
|
378
|
+
ctx.rootChangeTree = rootChangeTree;
|
|
379
|
+
_fullSyncWalk(ctx, rootChangeTree);
|
|
124
380
|
|
|
125
|
-
|
|
126
|
-
|
|
381
|
+
if (it.offset > buffer.byteLength) {
|
|
382
|
+
buffer = this._resizeBuffer(buffer, it.offset);
|
|
383
|
+
return this.encodeFullSync({ offset: initialOffset }, buffer, emitFiltered, view);
|
|
127
384
|
}
|
|
128
385
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
386
|
+
return buffer.subarray(0, it.offset);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
private _resizeBuffer(buffer: Uint8Array, usedOffset: number): Uint8Array {
|
|
390
|
+
const newSize = Math.ceil(usedOffset / Encoder.BUFFER_SIZE) * Encoder.BUFFER_SIZE;
|
|
133
391
|
|
|
134
|
-
|
|
392
|
+
console.warn(`@colyseus/schema buffer overflow. Encoded state is higher than default BUFFER_SIZE. Use the following to increase default BUFFER_SIZE:
|
|
135
393
|
|
|
136
394
|
import { Encoder } from "@colyseus/schema";
|
|
137
395
|
Encoder.BUFFER_SIZE = ${Math.round(newSize / 1024)} * 1024; // ${Math.round(newSize / 1024)} KB
|
|
138
396
|
`);
|
|
139
397
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
// -> No we probably can't unless we catch the need for resize before encoding which is likely more computationally expensive than resizing on demand
|
|
143
|
-
//
|
|
144
|
-
const newBuffer = new Uint8Array(newSize);
|
|
145
|
-
newBuffer.set(buffer); // copy previous encoding steps beyond the initialOffset
|
|
146
|
-
buffer = newBuffer;
|
|
147
|
-
|
|
148
|
-
// assign resized buffer to local sharedBuffer
|
|
149
|
-
if (buffer === this.sharedBuffer) {
|
|
150
|
-
this.sharedBuffer = buffer;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return this.encode({ offset: initialOffset }, view, buffer, changeSetName, isEncodeAll);
|
|
398
|
+
const newBuffer = new Uint8Array(newSize);
|
|
399
|
+
newBuffer.set(buffer);
|
|
154
400
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
return buffer.subarray(0, it.offset);
|
|
401
|
+
if (buffer === this.sharedBuffer) {
|
|
402
|
+
this.sharedBuffer = newBuffer;
|
|
158
403
|
}
|
|
404
|
+
|
|
405
|
+
return newBuffer;
|
|
159
406
|
}
|
|
160
407
|
|
|
161
408
|
encodeAll(
|
|
162
409
|
it: Iterator = { offset: 0 },
|
|
163
410
|
buffer: Uint8Array = this.sharedBuffer
|
|
164
411
|
) {
|
|
165
|
-
return this.
|
|
412
|
+
return this.encodeFullSync(it, buffer, /* emitFiltered */ false);
|
|
166
413
|
}
|
|
167
414
|
|
|
168
415
|
encodeAllView(
|
|
@@ -173,8 +420,7 @@ export class Encoder<T extends Schema = any> {
|
|
|
173
420
|
) {
|
|
174
421
|
const viewOffset = it.offset;
|
|
175
422
|
|
|
176
|
-
|
|
177
|
-
this.encode(it, view, bytes, "allFilteredChanges", true, viewOffset);
|
|
423
|
+
this.encodeFullSync(it, bytes, /* emitFiltered */ true, view, viewOffset);
|
|
178
424
|
|
|
179
425
|
return concatBytes(
|
|
180
426
|
bytes.subarray(0, sharedOffset),
|
|
@@ -190,41 +436,47 @@ export class Encoder<T extends Schema = any> {
|
|
|
190
436
|
) {
|
|
191
437
|
const viewOffset = it.offset;
|
|
192
438
|
|
|
193
|
-
//
|
|
439
|
+
// Stream priority pass: drain up to `maxPerTick` per-view entries
|
|
440
|
+
// from every registered stream before draining view.changes. Each
|
|
441
|
+
// selected element is passed to `view.add()` which populates
|
|
442
|
+
// view.changes with the stream-link ADD + element-field ADDs.
|
|
443
|
+
this._emitStreamPriority(view);
|
|
444
|
+
|
|
445
|
+
// encode visibility-triggered changes collected by view.add()
|
|
194
446
|
for (const [refId, changes] of view.changes) {
|
|
195
447
|
const changeTree: ChangeTree = this.root.changeTrees[refId];
|
|
196
448
|
|
|
197
449
|
if (changeTree === undefined) {
|
|
198
450
|
// detached instance, remove from view and skip.
|
|
199
|
-
// console.log("detached instance, remove from view and skip.", refId);
|
|
200
451
|
view.changes.delete(refId);
|
|
201
452
|
continue;
|
|
202
453
|
}
|
|
203
454
|
|
|
204
|
-
|
|
205
|
-
if (keys.length === 0) {
|
|
206
|
-
// FIXME: avoid having empty changes if no changes were made
|
|
207
|
-
// console.log("changes.size === 0, skip", refId, changeTree.ref.constructor.name);
|
|
455
|
+
if (changes.size === 0) {
|
|
208
456
|
continue;
|
|
209
457
|
}
|
|
210
458
|
|
|
459
|
+
const desc = changeTree.encDescriptor;
|
|
460
|
+
const encoder = desc.encoder;
|
|
461
|
+
const metadata = desc.metadata;
|
|
462
|
+
// `ref` → user-facing identity (Proxy on ArraySchema), used for
|
|
463
|
+
// `[$refId]`. `refTarget` → raw instance, used for the hot
|
|
464
|
+
// `[$getByIndex]` lookup that runs once per change.
|
|
211
465
|
const ref = changeTree.ref;
|
|
212
|
-
|
|
213
|
-
const ctor = ref.constructor;
|
|
214
|
-
const encoder = ctor[$encoder];
|
|
215
|
-
const metadata = ctor[Symbol.metadata];
|
|
466
|
+
const refTarget = changeTree.refTarget;
|
|
216
467
|
|
|
217
468
|
bytes[it.offset++] = SWITCH_TO_STRUCTURE & 255;
|
|
218
469
|
encode.number(bytes, ref[$refId], it);
|
|
219
470
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
471
|
+
// Iterate entries directly — the inner Map gives us the (index, op)
|
|
472
|
+
// pair without an intermediate keys array or Number() parse.
|
|
473
|
+
for (const [index, op] of changes) {
|
|
474
|
+
// workaround when using view.add() on item that has been deleted from state
|
|
475
|
+
// (see test "adding to view item that has been removed from state")
|
|
476
|
+
const value = refTarget[$getByIndex](index);
|
|
477
|
+
const operation = (value !== undefined && op) || OPERATION.DELETE;
|
|
225
478
|
|
|
226
|
-
// isEncodeAll = false
|
|
227
|
-
// hasView = true
|
|
479
|
+
// isEncodeAll = false, hasView = true
|
|
228
480
|
encoder(this, bytes, changeTree, index, operation, it, false, true, metadata);
|
|
229
481
|
}
|
|
230
482
|
}
|
|
@@ -233,11 +485,11 @@ export class Encoder<T extends Schema = any> {
|
|
|
233
485
|
// TODO: only clear view changes after all views are encoded
|
|
234
486
|
// (to allow re-using StateView's for multiple clients)
|
|
235
487
|
//
|
|
236
|
-
// clear "view" changes after encoding
|
|
237
488
|
view.changes.clear();
|
|
238
489
|
|
|
239
|
-
//
|
|
240
|
-
|
|
490
|
+
// per-tick view-scoped pass: walks the same `changes` queue as the
|
|
491
|
+
// shared pass, but `encodeChangeCb` emits only filtered fields.
|
|
492
|
+
this.encode(it, view, bytes, viewOffset);
|
|
241
493
|
|
|
242
494
|
return concatBytes(
|
|
243
495
|
bytes.subarray(0, sharedOffset),
|
|
@@ -245,22 +497,280 @@ export class Encoder<T extends Schema = any> {
|
|
|
245
497
|
);
|
|
246
498
|
}
|
|
247
499
|
|
|
500
|
+
/**
|
|
501
|
+
* Per-view unreliable encode. Walks `root.unreliableChanges` and emits
|
|
502
|
+
* only filtered fields visible to this view. Unlike `encodeView`, this
|
|
503
|
+
* doesn't emit `view.changes` entries — those are used only for
|
|
504
|
+
* reliable view bootstrap (membership ADDs) and are consumed by
|
|
505
|
+
* `encodeView` on the reliable channel.
|
|
506
|
+
*/
|
|
507
|
+
encodeUnreliableView(
|
|
508
|
+
view: StateView,
|
|
509
|
+
sharedOffset: number,
|
|
510
|
+
it: Iterator,
|
|
511
|
+
bytes: Uint8Array = this.sharedBuffer
|
|
512
|
+
) {
|
|
513
|
+
const viewOffset = it.offset;
|
|
514
|
+
|
|
515
|
+
this.encodeUnreliable(it, view, bytes, viewOffset);
|
|
516
|
+
|
|
517
|
+
return concatBytes(
|
|
518
|
+
bytes.subarray(0, sharedOffset),
|
|
519
|
+
bytes.subarray(viewOffset, it.offset)
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Broadcast-mode counterpart to `_emitStreamPriority`. Runs when NO
|
|
525
|
+
* StateViews are registered — streams fall back to broadcast mode
|
|
526
|
+
* where up to `maxPerTick` pending ADDs per stream emit to ALL clients
|
|
527
|
+
* each shared tick. DELETEs always flush (no cap).
|
|
528
|
+
*
|
|
529
|
+
* Emits directly to the shared-encode buffer: stream & element trees
|
|
530
|
+
* are `isFiltered=true` so the main loop would otherwise skip them.
|
|
531
|
+
* Runs AFTER the main loop so state / parent refs are already encoded
|
|
532
|
+
* — stream ADD ops reference element refIds, which must be decodable.
|
|
533
|
+
*/
|
|
534
|
+
private _emitStreamBroadcast(buffer: Uint8Array, it: Iterator): void {
|
|
535
|
+
const streams = this.root.streamTrees;
|
|
536
|
+
for (const stream of streams) {
|
|
537
|
+
const s: any = stream;
|
|
538
|
+
const tree: ChangeTree = s[$changes];
|
|
539
|
+
// Stream is registered with Root but not yet assigned a refId
|
|
540
|
+
// (e.g. created but never attached to state). Skip.
|
|
541
|
+
const streamRefId = s[$refId];
|
|
542
|
+
if (streamRefId === undefined) continue;
|
|
543
|
+
|
|
544
|
+
// `inheritedFlags.ensureStreamState` allocates `_stream` the
|
|
545
|
+
// moment the tree picks up `isStreamCollection` — Root only
|
|
546
|
+
// tracks trees that reached that point, so `_stream` is
|
|
547
|
+
// guaranteed defined here.
|
|
548
|
+
const st = s._stream!;
|
|
549
|
+
const deletes: Set<number> = st.broadcastDeletes;
|
|
550
|
+
const pending: Set<number> = st.broadcastPending;
|
|
551
|
+
const sent: Set<number> = st.sentBroadcast;
|
|
552
|
+
const hasDeletes = deletes.size > 0;
|
|
553
|
+
const hasAdds = pending.size > 0;
|
|
554
|
+
|
|
555
|
+
const desc = tree.encDescriptor;
|
|
556
|
+
const streamEncoder = desc.encoder;
|
|
557
|
+
const streamMetadata = desc.metadata;
|
|
558
|
+
|
|
559
|
+
// Emit stream ADD/DELETE ops for this tick, if any.
|
|
560
|
+
if (hasDeletes || hasAdds) {
|
|
561
|
+
buffer[it.offset++] = SWITCH_TO_STRUCTURE & 255;
|
|
562
|
+
encode.number(buffer, streamRefId, it);
|
|
563
|
+
|
|
564
|
+
// DELETEs first (flush all).
|
|
565
|
+
if (hasDeletes) {
|
|
566
|
+
for (const pos of deletes) {
|
|
567
|
+
streamEncoder(this, buffer, tree, pos, OPERATION.DELETE, it, false, false, streamMetadata);
|
|
568
|
+
}
|
|
569
|
+
deletes.clear();
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// ADDs up to maxPerTick.
|
|
573
|
+
const max: number = st.maxPerTick;
|
|
574
|
+
const emittedElements: any[] = [];
|
|
575
|
+
let count = 0;
|
|
576
|
+
const toDelete: number[] = [];
|
|
577
|
+
for (const pos of pending) {
|
|
578
|
+
if (count >= max) break;
|
|
579
|
+
// `$getByIndex` works for any streamable collection:
|
|
580
|
+
// StreamSchema (Map<number, V>) and MapSchema (string-keyed
|
|
581
|
+
// via journal index) both route through the same symbol.
|
|
582
|
+
const element = s[$getByIndex](pos);
|
|
583
|
+
if (element === undefined) {
|
|
584
|
+
toDelete.push(pos);
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
streamEncoder(this, buffer, tree, pos, OPERATION.ADD, it, false, false, streamMetadata);
|
|
588
|
+
sent.add(pos);
|
|
589
|
+
emittedElements.push(element);
|
|
590
|
+
toDelete.push(pos);
|
|
591
|
+
count++;
|
|
592
|
+
}
|
|
593
|
+
for (const pos of toDelete) pending.delete(pos);
|
|
594
|
+
|
|
595
|
+
// Emit each element's full state — forEachLive walks populated
|
|
596
|
+
// fields structurally, mirroring encodeAllView's bootstrap.
|
|
597
|
+
// Covers both static elements (dirty state was reset by
|
|
598
|
+
// inheritedFlags' becameStatic branch) and non-static (still
|
|
599
|
+
// has dirty state but the main loop skipped them because
|
|
600
|
+
// they're filtered).
|
|
601
|
+
for (const element of emittedElements) {
|
|
602
|
+
const elTree: ChangeTree | undefined = element[$changes];
|
|
603
|
+
if (elTree === undefined) continue;
|
|
604
|
+
const elRefId = element[$refId];
|
|
605
|
+
if (elRefId === undefined) continue;
|
|
606
|
+
|
|
607
|
+
buffer[it.offset++] = SWITCH_TO_STRUCTURE & 255;
|
|
608
|
+
encode.number(buffer, elRefId, it);
|
|
609
|
+
|
|
610
|
+
const elDesc = elTree.encDescriptor;
|
|
611
|
+
const elEncoder = elDesc.encoder;
|
|
612
|
+
const elMetadata = elDesc.metadata;
|
|
613
|
+
elTree.forEachLive((idx: number) => {
|
|
614
|
+
// @unreliable fields ship on the unreliable channel only.
|
|
615
|
+
if (Metadata.hasUnreliableAtIndex(elMetadata, idx)) return;
|
|
616
|
+
elEncoder(this, buffer, elTree, idx, OPERATION.ADD, it, false, false, elMetadata);
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Emit mutation updates for already-sent elements. Element
|
|
622
|
+
// trees are `isFiltered=true` (inherited from stream field),
|
|
623
|
+
// so the main loop skips them. We pick up their dirty state
|
|
624
|
+
// here so broadcast mode sees post-send field mutations.
|
|
625
|
+
for (const pos of sent) {
|
|
626
|
+
const element = s[$getByIndex](pos);
|
|
627
|
+
if (element === undefined) continue;
|
|
628
|
+
const elTree: ChangeTree | undefined = element[$changes];
|
|
629
|
+
if (elTree === undefined || !elTree.has()) continue;
|
|
630
|
+
|
|
631
|
+
const elRefId = element[$refId];
|
|
632
|
+
if (elRefId === undefined) continue;
|
|
633
|
+
|
|
634
|
+
buffer[it.offset++] = SWITCH_TO_STRUCTURE & 255;
|
|
635
|
+
encode.number(buffer, elRefId, it);
|
|
636
|
+
|
|
637
|
+
const elDesc = elTree.encDescriptor;
|
|
638
|
+
const elEncoder = elDesc.encoder;
|
|
639
|
+
const elMetadata = elDesc.metadata;
|
|
640
|
+
elTree.forEach((idx: number, op: OPERATION) => {
|
|
641
|
+
if (idx < 0) return; // pure ops (collection only)
|
|
642
|
+
if (Metadata.hasUnreliableAtIndex(elMetadata, idx)) return;
|
|
643
|
+
elEncoder(this, buffer, elTree, idx, op, it, false, false, elMetadata);
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Walk every registered stream, pick up to `maxPerTick` positions from
|
|
651
|
+
* this view's pending backlog (priority-sorted when the view supplies a
|
|
652
|
+
* `streamPriority` callback), and hand each element to `view.add()`.
|
|
653
|
+
* `view.add()` seeds `view.changes` so the subsequent drain emits both
|
|
654
|
+
* the stream-link (position → refId) and the element's field data.
|
|
655
|
+
*
|
|
656
|
+
* Designed to run at the very top of `encodeView`, BEFORE the
|
|
657
|
+
* view.changes drain loop.
|
|
658
|
+
*/
|
|
659
|
+
private _emitStreamPriority(view: StateView): void {
|
|
660
|
+
const streams = this.root.streamTrees;
|
|
661
|
+
if (streams.size === 0) return;
|
|
662
|
+
|
|
663
|
+
const viewId = view.id;
|
|
664
|
+
|
|
665
|
+
for (const stream of streams) {
|
|
666
|
+
const s: any = stream;
|
|
667
|
+
// Guaranteed non-undefined: `inheritedFlags.ensureStreamState`
|
|
668
|
+
// runs before Root.registerStream.
|
|
669
|
+
const st = s._stream!;
|
|
670
|
+
const pending: Set<number> | undefined = st.pendingByView.get(viewId);
|
|
671
|
+
if (pending === undefined || pending.size === 0) continue;
|
|
672
|
+
|
|
673
|
+
// Per-stream priority callback: declared at schema time (via
|
|
674
|
+
// `t.stream(X).priority(fn)` or the decorator form) and seeded
|
|
675
|
+
// into `_stream.priority` when the stream was attached. Users
|
|
676
|
+
// can also override per-instance by assigning to the setter.
|
|
677
|
+
const priority = st.priority;
|
|
678
|
+
|
|
679
|
+
// Materialize pending into an array so we can sort + slice.
|
|
680
|
+
// Small sets (typical: tens to low hundreds) — allocation is
|
|
681
|
+
// negligible compared to the priority sort and element walk.
|
|
682
|
+
const positions: number[] = [];
|
|
683
|
+
for (const p of pending) positions.push(p);
|
|
684
|
+
|
|
685
|
+
if (priority !== undefined) {
|
|
686
|
+
// Use the symbol-keyed accessor so Map/Set/Stream all route
|
|
687
|
+
// through the same lookup regardless of $items layout.
|
|
688
|
+
positions.sort(
|
|
689
|
+
(a: number, b: number) => priority(view, s[$getByIndex](b)) - priority(view, s[$getByIndex](a)),
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const max = st.maxPerTick;
|
|
694
|
+
const count = Math.min(positions.length, max);
|
|
695
|
+
|
|
696
|
+
let sent: Set<number> | undefined = st.sentByView.get(viewId);
|
|
697
|
+
if (sent === undefined) {
|
|
698
|
+
sent = new Set();
|
|
699
|
+
st.sentByView.set(viewId, sent);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
for (let i = 0; i < count; i++) {
|
|
703
|
+
const pos = positions[i];
|
|
704
|
+
const element = s[$getByIndex](pos);
|
|
705
|
+
if (element === undefined) {
|
|
706
|
+
// Element was removed after being queued but before emit.
|
|
707
|
+
pending.delete(pos);
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
// `_addImmediate` force-ships the element through view.changes
|
|
711
|
+
// (markVisible + addParentOf + forEachChild recursion) WITHOUT
|
|
712
|
+
// routing stream elements back into pending — we're already
|
|
713
|
+
// draining pending here, so the normal `add()` path would
|
|
714
|
+
// infinite-loop. addParentOf seeds
|
|
715
|
+
// `view.changes[stream.refId][pos] = ADD` (stream-link emit).
|
|
716
|
+
view._addImmediate(element);
|
|
717
|
+
// Force-seed element fields even when view.add skipped
|
|
718
|
+
// forEachLive (isNew && !isChildAdded). Matches the
|
|
719
|
+
// bootstrap emission encodeAllView does for filtered
|
|
720
|
+
// refs. `@unreliable` fields are excluded — they ship
|
|
721
|
+
// on the unreliable channel and force-seeding here
|
|
722
|
+
// would leak onto the reliable view pass.
|
|
723
|
+
const elTree = element[$changes];
|
|
724
|
+
if (elTree !== undefined) {
|
|
725
|
+
const elRefId = element[$refId];
|
|
726
|
+
let elChanges = view.changes.get(elRefId);
|
|
727
|
+
if (elChanges === undefined) {
|
|
728
|
+
elChanges = new Map();
|
|
729
|
+
view.changes.set(elRefId, elChanges);
|
|
730
|
+
}
|
|
731
|
+
const elMetadata = elTree.metadata;
|
|
732
|
+
elTree.forEachLive((index: number) => {
|
|
733
|
+
if (Metadata.hasUnreliableAtIndex(elMetadata, index)) return;
|
|
734
|
+
elChanges!.set(index, OPERATION.ADD);
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
pending.delete(pos);
|
|
738
|
+
sent.add(pos);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
248
743
|
discardChanges() {
|
|
249
|
-
|
|
250
|
-
let current =
|
|
744
|
+
const list = this.root.changes;
|
|
745
|
+
let current = list.next;
|
|
746
|
+
const root = this.root;
|
|
251
747
|
while (current) {
|
|
252
|
-
current.
|
|
253
|
-
current
|
|
748
|
+
const next = current.next;
|
|
749
|
+
current.changeTree.endEncode(); // clears changesNode internally
|
|
750
|
+
root.releaseNode(current);
|
|
751
|
+
current = next;
|
|
254
752
|
}
|
|
255
|
-
|
|
753
|
+
list.next = undefined;
|
|
754
|
+
list.tail = undefined;
|
|
755
|
+
|
|
756
|
+
// End-of-tick: refIds released during this tick have now had their
|
|
757
|
+
// DELETEs emitted through `encode()` / `encodeView()`, so they are
|
|
758
|
+
// safe to recycle on the next tick.
|
|
759
|
+
root.refIds.flushReleases();
|
|
760
|
+
}
|
|
256
761
|
|
|
257
|
-
|
|
258
|
-
|
|
762
|
+
discardUnreliableChanges() {
|
|
763
|
+
const list = this.root.unreliableChanges;
|
|
764
|
+
let current = list.next;
|
|
765
|
+
const root = this.root;
|
|
259
766
|
while (current) {
|
|
260
|
-
current.
|
|
261
|
-
current
|
|
767
|
+
const next = current.next;
|
|
768
|
+
current.changeTree.endEncodeUnreliable(); // clears unreliableChangesNode internally
|
|
769
|
+
root.releaseNode(current);
|
|
770
|
+
current = next;
|
|
262
771
|
}
|
|
263
|
-
|
|
772
|
+
list.next = undefined;
|
|
773
|
+
list.tail = undefined;
|
|
264
774
|
}
|
|
265
775
|
|
|
266
776
|
tryEncodeTypeId(
|
|
@@ -284,9 +794,10 @@ export class Encoder<T extends Schema = any> {
|
|
|
284
794
|
}
|
|
285
795
|
|
|
286
796
|
get hasChanges() {
|
|
287
|
-
return
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
797
|
+
return this.root.changes.next !== undefined;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
get hasUnreliableChanges() {
|
|
801
|
+
return this.root.unreliableChanges.next !== undefined;
|
|
291
802
|
}
|
|
292
803
|
}
|