@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
package/src/codegen/parser.ts
CHANGED
|
@@ -8,7 +8,54 @@ let currentProperty: Property;
|
|
|
8
8
|
|
|
9
9
|
let globalContext: Context;
|
|
10
10
|
|
|
11
|
+
const BUILDER_COLLECTION_KINDS = new Set(["array", "map", "set", "collection"]);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* For a t.*().chain().calls() expression, walk down to the base `t.X(...)`
|
|
15
|
+
* call and return its method name and first argument. Returns null if the
|
|
16
|
+
* node does not look like a builder chain.
|
|
17
|
+
*/
|
|
18
|
+
function extractBuilderBase(node: ts.CallExpression): { methodName: string, firstArg?: ts.Expression } | null {
|
|
19
|
+
let current: ts.CallExpression = node;
|
|
20
|
+
while (true) {
|
|
21
|
+
const expr = current.expression;
|
|
22
|
+
if (!ts.isPropertyAccessExpression(expr)) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
if (ts.isCallExpression(expr.expression)) {
|
|
26
|
+
// Chained modifier, e.g. .default() / .view() — walk deeper.
|
|
27
|
+
current = expr.expression;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
methodName: expr.name.text,
|
|
32
|
+
firstArg: current.arguments[0],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
11
37
|
function defineProperty(property: Property, initializer: any) {
|
|
38
|
+
// Builder-style: t.number(), t.array(Item), t.map(Item).view(), etc.
|
|
39
|
+
if (ts.isCallExpression(initializer)) {
|
|
40
|
+
const base = extractBuilderBase(initializer);
|
|
41
|
+
if (base) {
|
|
42
|
+
if (BUILDER_COLLECTION_KINDS.has(base.methodName)) {
|
|
43
|
+
property.type = base.methodName;
|
|
44
|
+
if (base.firstArg) {
|
|
45
|
+
property.childType = (base.firstArg as any).text ?? base.firstArg.getText();
|
|
46
|
+
}
|
|
47
|
+
} else if (base.methodName === "ref") {
|
|
48
|
+
property.type = "ref";
|
|
49
|
+
if (base.firstArg) {
|
|
50
|
+
property.childType = (base.firstArg as any).text ?? base.firstArg.getText();
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
property.type = base.methodName;
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
12
59
|
if (ts.isIdentifier(initializer)) {
|
|
13
60
|
property.type = "ref";
|
|
14
61
|
property.childType = initializer.text;
|
|
@@ -207,39 +254,6 @@ function inspectNode(node: ts.Node, context: Context, decoratorName: string) {
|
|
|
207
254
|
defineProperty(property, prop.initializer);
|
|
208
255
|
}
|
|
209
256
|
|
|
210
|
-
} else if (
|
|
211
|
-
node.getText() === "defineTypes" &&
|
|
212
|
-
(
|
|
213
|
-
node.parent.kind === ts.SyntaxKind.CallExpression ||
|
|
214
|
-
node.parent.kind === ts.SyntaxKind.PropertyAccessExpression
|
|
215
|
-
)
|
|
216
|
-
) {
|
|
217
|
-
/**
|
|
218
|
-
* JavaScript source file (`.js`)
|
|
219
|
-
* Using `defineTypes()`
|
|
220
|
-
*/
|
|
221
|
-
const callExpression = (node.parent.kind === ts.SyntaxKind.PropertyAccessExpression)
|
|
222
|
-
? node.parent.parent as ts.CallExpression
|
|
223
|
-
: node.parent as ts.CallExpression;
|
|
224
|
-
|
|
225
|
-
if (callExpression.kind !== ts.SyntaxKind.CallExpression) {
|
|
226
|
-
break;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const className = callExpression.arguments[0].getText()
|
|
230
|
-
currentStructure.name = className;
|
|
231
|
-
|
|
232
|
-
const types = callExpression.arguments[1] as any;
|
|
233
|
-
for (let i = 0; i < types.properties.length; i++) {
|
|
234
|
-
const prop = types.properties[i];
|
|
235
|
-
|
|
236
|
-
const property = currentProperty || new Property();
|
|
237
|
-
property.name = prop.name.escapedText;
|
|
238
|
-
currentStructure.addProperty(property);
|
|
239
|
-
|
|
240
|
-
defineProperty(property, prop.initializer);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
257
|
}
|
|
244
258
|
|
|
245
259
|
if (node.parent.kind === ts.SyntaxKind.ClassDeclaration) {
|
|
@@ -252,58 +266,66 @@ function inspectNode(node: ts.Node, context: Context, decoratorName: string) {
|
|
|
252
266
|
|
|
253
267
|
case ts.SyntaxKind.CallExpression:
|
|
254
268
|
/**
|
|
255
|
-
* Defining schema via
|
|
256
|
-
* - schema
|
|
257
|
-
* - schema({})
|
|
258
|
-
* -
|
|
269
|
+
* Defining schema via:
|
|
270
|
+
* - schema({ ... })
|
|
271
|
+
* - schema({ ... }, 'Name')
|
|
272
|
+
* - schema.schema({ ... }, 'Name')
|
|
273
|
+
* - ParentClass.extend({ ... }, 'Name')
|
|
259
274
|
*/
|
|
260
|
-
|
|
261
|
-
(
|
|
262
|
-
(
|
|
263
|
-
(node as ts.CallExpression).expression?.getText() === "schema.schema" ||
|
|
264
|
-
(node as ts.CallExpression).expression?.getText() === "schema"
|
|
265
|
-
) ||
|
|
266
|
-
(
|
|
267
|
-
(node as ts.CallExpression).expression?.getText().indexOf(".extends") !== -1
|
|
268
|
-
)
|
|
269
|
-
) &&
|
|
270
|
-
(node as ts.CallExpression).arguments[0].kind === ts.SyntaxKind.ObjectLiteralExpression
|
|
271
|
-
) {
|
|
275
|
+
{
|
|
272
276
|
const callExpression = node as ts.CallExpression;
|
|
277
|
+
const callee = callExpression.expression?.getText?.();
|
|
278
|
+
if (!callee) break;
|
|
279
|
+
|
|
280
|
+
const isSchemaCall = callee === "schema" || callee === "schema.schema";
|
|
281
|
+
const isExtendCall = callee.indexOf(".extend") !== -1 && !callee.endsWith(".extends");
|
|
282
|
+
if (!isSchemaCall && !isExtendCall) break;
|
|
283
|
+
|
|
284
|
+
// Signature: (fields, name?)
|
|
285
|
+
const fieldsArg = callExpression.arguments[0];
|
|
286
|
+
const nameArg = callExpression.arguments[1];
|
|
287
|
+
if (!fieldsArg || fieldsArg.kind !== ts.SyntaxKind.ObjectLiteralExpression) {
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
273
290
|
|
|
274
|
-
let className
|
|
291
|
+
let className: string | undefined;
|
|
292
|
+
if (nameArg) {
|
|
293
|
+
if (nameArg.kind === ts.SyntaxKind.StringLiteral) {
|
|
294
|
+
className = (nameArg as ts.StringLiteral).text;
|
|
295
|
+
} else {
|
|
296
|
+
className = nameArg.getText();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
275
299
|
|
|
276
300
|
if (!className && callExpression.parent.kind === ts.SyntaxKind.VariableDeclaration) {
|
|
277
301
|
className = (callExpression.parent as ts.VariableDeclaration).name?.getText();
|
|
278
302
|
}
|
|
279
303
|
|
|
280
|
-
|
|
281
|
-
if (!className) { break; }
|
|
304
|
+
if (!className) break;
|
|
282
305
|
|
|
283
306
|
if (currentStructure?.name !== className) {
|
|
284
307
|
currentStructure = new Class();
|
|
285
308
|
context.addStructure(currentStructure);
|
|
286
309
|
}
|
|
287
310
|
|
|
288
|
-
if (
|
|
289
|
-
// if it's using `.extends({})`
|
|
311
|
+
if (isExtendCall) {
|
|
290
312
|
const extendsClass = (node as any).expression?.expression?.escapedText;
|
|
291
|
-
|
|
292
|
-
// skip if no extendsClass is provided
|
|
293
|
-
if (!extendsClass) { break; }
|
|
313
|
+
if (!extendsClass) break;
|
|
294
314
|
(currentStructure as Class).extends = extendsClass;
|
|
295
|
-
|
|
296
315
|
} else {
|
|
297
|
-
|
|
298
|
-
(currentStructure as Class).extends = "Schema"; // force extends to Schema
|
|
316
|
+
(currentStructure as Class).extends = "Schema";
|
|
299
317
|
}
|
|
300
318
|
|
|
301
319
|
currentStructure.name = className;
|
|
302
320
|
|
|
303
|
-
const types =
|
|
321
|
+
const types = fieldsArg as any;
|
|
304
322
|
for (let i = 0; i < types.properties.length; i++) {
|
|
305
323
|
const prop = types.properties[i];
|
|
306
324
|
|
|
325
|
+
// Skip methods declared inside the fields object.
|
|
326
|
+
if (prop.kind === ts.SyntaxKind.MethodDeclaration) continue;
|
|
327
|
+
if (!prop.initializer) continue;
|
|
328
|
+
|
|
307
329
|
const property = currentProperty || new Property();
|
|
308
330
|
property.name = prop.name.escapedText;
|
|
309
331
|
|
|
@@ -4,7 +4,7 @@ import { Schema } from "../Schema.js";
|
|
|
4
4
|
import type { IRef, Ref } from "../encoder/ChangeTree.js";
|
|
5
5
|
import type { Decoder } from "./Decoder.js";
|
|
6
6
|
import { Iterator, decode } from "../encoding/decode.js";
|
|
7
|
-
import { $childType, $deleteByIndex, $getByIndex, $refId } from "../types/symbols.js";
|
|
7
|
+
import { $childType, $deleteByIndex, $getByIndex, $proxyTarget, $refId } from "../types/symbols.js";
|
|
8
8
|
|
|
9
9
|
import type { ArraySchema } from "../types/custom/ArraySchema.js";
|
|
10
10
|
|
|
@@ -15,7 +15,8 @@ export interface DataChange<T = any, F = string> {
|
|
|
15
15
|
ref: IRef,
|
|
16
16
|
refId: number,
|
|
17
17
|
op: OPERATION,
|
|
18
|
-
field
|
|
18
|
+
/** Set for Schema field changes; omitted for collection item changes (which carry a `dynamicIndex` instead). */
|
|
19
|
+
field?: F;
|
|
19
20
|
dynamicIndex?: number | string;
|
|
20
21
|
value: T;
|
|
21
22
|
previousValue: T;
|
|
@@ -23,26 +24,73 @@ export interface DataChange<T = any, F = string> {
|
|
|
23
24
|
|
|
24
25
|
export const DEFINITION_MISMATCH = -1;
|
|
25
26
|
|
|
27
|
+
/**
|
|
28
|
+
* When no `triggerChanges` subscriber is attached, `Decoder.decode` passes
|
|
29
|
+
* `null` so the per-field change objects are never allocated. Every push
|
|
30
|
+
* site uses `allChanges?.push(...)` — optional chaining also short-circuits
|
|
31
|
+
* the object literal, so there's nothing to collect and nothing to throw
|
|
32
|
+
* away.
|
|
33
|
+
*/
|
|
26
34
|
export type DecodeOperation<T extends Schema = any> = (
|
|
27
35
|
decoder: Decoder<T>,
|
|
28
36
|
bytes: Uint8Array,
|
|
29
37
|
it: Iterator,
|
|
30
38
|
ref: IRef,
|
|
31
|
-
allChanges: DataChange[],
|
|
39
|
+
allChanges: DataChange[] | null,
|
|
32
40
|
) => number | void;
|
|
33
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Collection-kind discriminator declared on each collection class as
|
|
44
|
+
* `static COLLECTION_KIND = CollectionKind.X`. The decoder's key/value
|
|
45
|
+
* dispatch used to make three back-to-back `typeof(ref.method) ===
|
|
46
|
+
* "function"` checks per entry; those collapse into one switch on the
|
|
47
|
+
* target's class tag. Missing / `undefined` on a ref hits the switch's
|
|
48
|
+
* `default` branch and logs a warning — a guard for future collection
|
|
49
|
+
* types that land without a tag.
|
|
50
|
+
*
|
|
51
|
+
* Declared as a `const` object (not a TS `enum`) so the codegen parser —
|
|
52
|
+
* which picks up every `EnumDeclaration` in the lib source via transitive
|
|
53
|
+
* imports — doesn't emit a generated .cs file for it.
|
|
54
|
+
*/
|
|
55
|
+
export const CollectionKind = {
|
|
56
|
+
Map: 1,
|
|
57
|
+
Array: 2,
|
|
58
|
+
Set: 3,
|
|
59
|
+
Collection: 4,
|
|
60
|
+
Stream: 5,
|
|
61
|
+
} as const;
|
|
62
|
+
export type CollectionKind = typeof CollectionKind[keyof typeof CollectionKind];
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Structural type for any class that participates in the `decodeKeyValue-
|
|
66
|
+
* Operation` dispatch. Lets the hot-path read `tgt.constructor.COLLECTION_KIND`
|
|
67
|
+
* without an `any` cast.
|
|
68
|
+
*/
|
|
69
|
+
export interface CollectionCtor {
|
|
70
|
+
readonly COLLECTION_KIND: CollectionKind;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Decode the next wire value for `ref[index]`. Returns the decoded value.
|
|
75
|
+
*
|
|
76
|
+
* Callers pass `previousValue` explicitly — it's the current value at the
|
|
77
|
+
* slot before decoding and is needed for ref-count bookkeeping (on DELETE)
|
|
78
|
+
* and for the DELETE_AND_ADD self-reassign case. Keeping it as a parameter
|
|
79
|
+
* lets this function return a single primitive instead of a pair, so the
|
|
80
|
+
* hot call path allocates nothing.
|
|
81
|
+
*/
|
|
34
82
|
export function decodeValue<T extends Ref>(
|
|
35
83
|
decoder: Decoder,
|
|
36
84
|
operation: OPERATION,
|
|
37
85
|
ref: T,
|
|
38
86
|
index: number,
|
|
87
|
+
previousValue: any,
|
|
39
88
|
type: any,
|
|
40
89
|
bytes: Uint8Array,
|
|
41
90
|
it: Iterator,
|
|
42
|
-
allChanges: DataChange[],
|
|
43
|
-
) {
|
|
91
|
+
allChanges: DataChange[] | null,
|
|
92
|
+
): any {
|
|
44
93
|
const $root = decoder.root;
|
|
45
|
-
const previousValue = (ref as any)[$getByIndex](index) as T;
|
|
46
94
|
|
|
47
95
|
let value: any;
|
|
48
96
|
|
|
@@ -56,7 +104,7 @@ export function decodeValue<T extends Ref>(
|
|
|
56
104
|
// Delete operations
|
|
57
105
|
//
|
|
58
106
|
if (operation !== OPERATION.DELETE_AND_ADD) {
|
|
59
|
-
|
|
107
|
+
ref[$deleteByIndex](index);
|
|
60
108
|
}
|
|
61
109
|
|
|
62
110
|
value = undefined;
|
|
@@ -67,6 +115,15 @@ export function decodeValue<T extends Ref>(
|
|
|
67
115
|
// Don't do anything
|
|
68
116
|
//
|
|
69
117
|
|
|
118
|
+
} else if (typeof (type) === "string") {
|
|
119
|
+
//
|
|
120
|
+
// Primitive value (number, string, boolean, …). Hot-path first
|
|
121
|
+
// because steady-state ticks are dominated by primitive field
|
|
122
|
+
// updates — moves us past a cheap typeof check instead of a
|
|
123
|
+
// Symbol-metadata lookup via `Schema.is`.
|
|
124
|
+
//
|
|
125
|
+
value = (decode as any)[type](bytes, it);
|
|
126
|
+
|
|
70
127
|
} else if (Schema.is(type)) {
|
|
71
128
|
const refId = decode.number(bytes, it);
|
|
72
129
|
value = $root.refs.get(refId);
|
|
@@ -87,19 +144,17 @@ export function decodeValue<T extends Ref>(
|
|
|
87
144
|
);
|
|
88
145
|
}
|
|
89
146
|
|
|
90
|
-
} else if (typeof(type) === "string") {
|
|
91
|
-
//
|
|
92
|
-
// primitive value (number, string, boolean, etc)
|
|
93
|
-
//
|
|
94
|
-
value = (decode as any)[type](bytes, it);
|
|
95
|
-
|
|
96
147
|
} else {
|
|
97
148
|
const typeDef = getType(Object.keys(type)[0]);
|
|
98
149
|
const refId = decode.number(bytes, it);
|
|
99
150
|
|
|
151
|
+
// `initializeForDecoder` is a static on every registered collection
|
|
152
|
+
// class — it does `Object.create(Class.prototype)` + the class-
|
|
153
|
+
// field init + assigns an untracked `$changes` directly. Keeps
|
|
154
|
+
// the decoder free of collection-type internals.
|
|
100
155
|
const valueRef: Ref = ($root.refs.has(refId))
|
|
101
156
|
? previousValue || $root.refs.get(refId)
|
|
102
|
-
:
|
|
157
|
+
: (typeDef.constructor as any).initializeForDecoder();
|
|
103
158
|
|
|
104
159
|
value = valueRef.clone(true);
|
|
105
160
|
value[$childType] = Object.values(type)[0]; // cache childType for ArraySchema and MapSchema
|
|
@@ -122,7 +177,7 @@ export function decodeValue<T extends Ref>(
|
|
|
122
177
|
$root.removeRef(previousRefId);
|
|
123
178
|
}
|
|
124
179
|
|
|
125
|
-
allChanges
|
|
180
|
+
allChanges?.push({
|
|
126
181
|
ref: previousValue,
|
|
127
182
|
refId: previousRefId,
|
|
128
183
|
op: OPERATION.DELETE,
|
|
@@ -141,7 +196,7 @@ export function decodeValue<T extends Ref>(
|
|
|
141
196
|
));
|
|
142
197
|
}
|
|
143
198
|
|
|
144
|
-
return
|
|
199
|
+
return value;
|
|
145
200
|
}
|
|
146
201
|
|
|
147
202
|
export const decodeSchemaOperation: DecodeOperation = function <T extends Schema>(
|
|
@@ -149,7 +204,7 @@ export const decodeSchemaOperation: DecodeOperation = function <T extends Schema
|
|
|
149
204
|
bytes: Uint8Array,
|
|
150
205
|
it: Iterator,
|
|
151
206
|
ref: T,
|
|
152
|
-
allChanges: DataChange[],
|
|
207
|
+
allChanges: DataChange[] | null,
|
|
153
208
|
) {
|
|
154
209
|
const first_byte = bytes[it.offset++];
|
|
155
210
|
const metadata: Metadata = (ref.constructor as typeof Schema)[Symbol.metadata];
|
|
@@ -165,11 +220,13 @@ export const decodeSchemaOperation: DecodeOperation = function <T extends Schema
|
|
|
165
220
|
return DEFINITION_MISMATCH;
|
|
166
221
|
}
|
|
167
222
|
|
|
168
|
-
const
|
|
223
|
+
const previousValue = ref[$getByIndex](index);
|
|
224
|
+
const value = decodeValue(
|
|
169
225
|
decoder,
|
|
170
226
|
operation,
|
|
171
227
|
ref,
|
|
172
228
|
index,
|
|
229
|
+
previousValue,
|
|
173
230
|
field.type,
|
|
174
231
|
bytes,
|
|
175
232
|
it,
|
|
@@ -177,12 +234,19 @@ export const decodeSchemaOperation: DecodeOperation = function <T extends Schema
|
|
|
177
234
|
);
|
|
178
235
|
|
|
179
236
|
if (value !== null && value !== undefined) {
|
|
237
|
+
// Write via the generated setter. Bypass to `(ref as any)[$values][index]`
|
|
238
|
+
// was attempted but only works for @type-decorated classes (which
|
|
239
|
+
// install accessor descriptors reading from `$values`). Reflection-
|
|
240
|
+
// decoded classes install a plain data-property descriptor instead,
|
|
241
|
+
// so their value lives as an own property on the instance — direct
|
|
242
|
+
// `$values[index]` writes are invisible to the getter on that path.
|
|
243
|
+
// Two-mode dispatch would cost more than the ~3% it'd save.
|
|
180
244
|
ref[field.name as keyof T] = value;
|
|
181
245
|
}
|
|
182
246
|
|
|
183
247
|
// add change
|
|
184
248
|
if (previousValue !== value) {
|
|
185
|
-
allChanges
|
|
249
|
+
allChanges?.push({
|
|
186
250
|
ref,
|
|
187
251
|
refId: decoder.currentRefId,
|
|
188
252
|
op: operation,
|
|
@@ -198,8 +262,14 @@ export const decodeKeyValueOperation: DecodeOperation = function (
|
|
|
198
262
|
bytes: Uint8Array,
|
|
199
263
|
it: Iterator,
|
|
200
264
|
ref: Ref,
|
|
201
|
-
allChanges: DataChange[]
|
|
265
|
+
allChanges: DataChange[] | null,
|
|
202
266
|
) {
|
|
267
|
+
// Unwrap ArraySchema Proxy once so subsequent property reads skip the
|
|
268
|
+
// `get` trap. `$proxyTarget` is a self-reference on the target; on
|
|
269
|
+
// non-proxied collections (Map/Set/Collection/Stream) the lookup is
|
|
270
|
+
// undefined and we fall back to `ref`.
|
|
271
|
+
const tgt: any = (ref as any)[$proxyTarget] ?? ref;
|
|
272
|
+
|
|
203
273
|
// "uncompressed" index + operation (array/map items)
|
|
204
274
|
const operation = bytes[it.offset++];
|
|
205
275
|
|
|
@@ -209,34 +279,38 @@ export const decodeKeyValueOperation: DecodeOperation = function (
|
|
|
209
279
|
// - enqueue items for DELETE callback.
|
|
210
280
|
// - flag child items for garbage collection.
|
|
211
281
|
//
|
|
212
|
-
decoder.removeChildRefs(
|
|
282
|
+
decoder.removeChildRefs(tgt as Collection, allChanges);
|
|
213
283
|
|
|
214
|
-
|
|
284
|
+
tgt.clear();
|
|
215
285
|
return;
|
|
216
286
|
}
|
|
217
287
|
|
|
218
288
|
const index = decode.number(bytes, it);
|
|
219
|
-
const type =
|
|
289
|
+
const type = tgt[$childType];
|
|
290
|
+
// One constructor lookup, one integer read → switch. Replaces three
|
|
291
|
+
// `typeof(ref.method) === "function"` dispatches per entry.
|
|
292
|
+
const kind: CollectionKind = (tgt.constructor as CollectionCtor).COLLECTION_KIND;
|
|
220
293
|
|
|
221
294
|
let dynamicIndex: number | string;
|
|
222
295
|
|
|
223
296
|
if ((operation & OPERATION.ADD) === OPERATION.ADD) { // ADD or DELETE_AND_ADD
|
|
224
|
-
if (
|
|
225
|
-
dynamicIndex = decode.string(bytes, it); // MapSchema
|
|
226
|
-
|
|
297
|
+
if (kind === CollectionKind.Map) {
|
|
298
|
+
dynamicIndex = decode.string(bytes, it); // MapSchema uses a wire-delivered string key
|
|
299
|
+
tgt.setIndex(index, dynamicIndex);
|
|
227
300
|
} else {
|
|
228
|
-
dynamicIndex = index;
|
|
301
|
+
dynamicIndex = index;
|
|
229
302
|
}
|
|
230
303
|
} else {
|
|
231
|
-
|
|
232
|
-
dynamicIndex = (ref as any)['getIndex'](index);
|
|
304
|
+
dynamicIndex = tgt.getIndex(index);
|
|
233
305
|
}
|
|
234
306
|
|
|
235
|
-
const
|
|
307
|
+
const previousValue = tgt[$getByIndex](index);
|
|
308
|
+
const value = decodeValue(
|
|
236
309
|
decoder,
|
|
237
310
|
operation,
|
|
238
311
|
ref,
|
|
239
312
|
index,
|
|
313
|
+
previousValue,
|
|
240
314
|
type,
|
|
241
315
|
bytes,
|
|
242
316
|
it,
|
|
@@ -244,31 +318,57 @@ export const decodeKeyValueOperation: DecodeOperation = function (
|
|
|
244
318
|
);
|
|
245
319
|
|
|
246
320
|
if (value !== null && value !== undefined) {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
// CollectionSchema
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
321
|
+
switch (kind) {
|
|
322
|
+
case CollectionKind.Map:
|
|
323
|
+
tgt.$items.set(dynamicIndex as string, value);
|
|
324
|
+
break;
|
|
325
|
+
|
|
326
|
+
case CollectionKind.Array:
|
|
327
|
+
tgt.$setAt(index, value, operation);
|
|
328
|
+
break;
|
|
329
|
+
|
|
330
|
+
// SetSchema / CollectionSchema / StreamSchema — use the wire-
|
|
331
|
+
// index we decoded above so server/client `$items` stay in sync
|
|
332
|
+
// regardless of duplicate emission (e.g. a bootstrap that walks
|
|
333
|
+
// both `encodeAll` and the shared recorder emits the same ADD
|
|
334
|
+
// op twice). Previous implementation called `ref.add(value)`
|
|
335
|
+
// and let the decoder-side `$refId++` allocate a new index per
|
|
336
|
+
// call — which for CollectionSchema (no value-dedup) turned
|
|
337
|
+
// duplicate wire ADDs into duplicate client-side entries.
|
|
338
|
+
case CollectionKind.Set:
|
|
339
|
+
case CollectionKind.Collection:
|
|
340
|
+
case CollectionKind.Stream:
|
|
341
|
+
if (!tgt.$items.has(index)) {
|
|
342
|
+
tgt.$items.set(index, value);
|
|
343
|
+
// Keep the decoder's monotonic counter ahead of any
|
|
344
|
+
// wire-index we've seen so future server-side `.add()`
|
|
345
|
+
// allocations don't collide with ones already decoded.
|
|
346
|
+
// (StreamSchema has no `$refId` counter — `typeof`
|
|
347
|
+
// guards the Set/Collection path.)
|
|
348
|
+
if (typeof tgt.$refId === "number" && index >= tgt.$refId) {
|
|
349
|
+
tgt.$refId = index + 1;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
break;
|
|
353
|
+
|
|
354
|
+
default:
|
|
355
|
+
// A future collection type landed without a COLLECTION_KIND
|
|
356
|
+
// tag. Surface it loudly instead of silently dropping the
|
|
357
|
+
// value — the missing entry here is the only place the new
|
|
358
|
+
// type's item-storage semantics need to be wired up.
|
|
359
|
+
console.warn(
|
|
360
|
+
`@colyseus/schema: missing COLLECTION_KIND on ${tgt.constructor?.name} — item at index ${index} was not stored.`
|
|
361
|
+
);
|
|
362
|
+
break;
|
|
262
363
|
}
|
|
263
364
|
}
|
|
264
365
|
|
|
265
366
|
// add change
|
|
266
367
|
if (previousValue !== value) {
|
|
267
|
-
allChanges
|
|
368
|
+
allChanges?.push({
|
|
268
369
|
ref,
|
|
269
370
|
refId: decoder.currentRefId,
|
|
270
371
|
op: operation,
|
|
271
|
-
field: "", // FIXME: remove this
|
|
272
372
|
dynamicIndex,
|
|
273
373
|
value,
|
|
274
374
|
previousValue,
|
|
@@ -281,8 +381,11 @@ export const decodeArray: DecodeOperation = function (
|
|
|
281
381
|
bytes: Uint8Array,
|
|
282
382
|
it: Iterator,
|
|
283
383
|
ref: ArraySchema,
|
|
284
|
-
allChanges: DataChange[]
|
|
384
|
+
allChanges: DataChange[] | null,
|
|
285
385
|
) {
|
|
386
|
+
// Unwrap the Proxy once — ref is always an ArraySchema here.
|
|
387
|
+
const tgt: any = (ref as any)[$proxyTarget] ?? ref;
|
|
388
|
+
|
|
286
389
|
// "uncompressed" index + operation (array/map items)
|
|
287
390
|
let operation = bytes[it.offset++];
|
|
288
391
|
let index: number;
|
|
@@ -293,25 +396,24 @@ export const decodeArray: DecodeOperation = function (
|
|
|
293
396
|
// - enqueue items for DELETE callback.
|
|
294
397
|
// - flag child items for garbage collection.
|
|
295
398
|
//
|
|
296
|
-
decoder.removeChildRefs(
|
|
297
|
-
|
|
399
|
+
decoder.removeChildRefs(tgt as Collection, allChanges);
|
|
400
|
+
tgt.clear();
|
|
298
401
|
return;
|
|
299
402
|
|
|
300
403
|
} else if (operation === OPERATION.REVERSE) {
|
|
301
|
-
|
|
404
|
+
tgt.reverse();
|
|
302
405
|
return;
|
|
303
406
|
|
|
304
407
|
} else if (operation === OPERATION.DELETE_BY_REFID) {
|
|
305
408
|
// TODO: refactor here, try to follow same flow as below
|
|
306
409
|
const refId = decode.number(bytes, it);
|
|
307
410
|
const previousValue = decoder.root.refs.get(refId);
|
|
308
|
-
index =
|
|
309
|
-
|
|
310
|
-
allChanges
|
|
411
|
+
index = tgt.findIndex((value: any) => value === previousValue);
|
|
412
|
+
tgt[$deleteByIndex](index);
|
|
413
|
+
allChanges?.push({
|
|
311
414
|
ref,
|
|
312
415
|
refId: decoder.currentRefId,
|
|
313
416
|
op: OPERATION.DELETE,
|
|
314
|
-
field: "", // FIXME: remove this
|
|
315
417
|
dynamicIndex: index,
|
|
316
418
|
value: undefined,
|
|
317
419
|
previousValue,
|
|
@@ -325,27 +427,32 @@ export const decodeArray: DecodeOperation = function (
|
|
|
325
427
|
|
|
326
428
|
// if item already exists, use existing index
|
|
327
429
|
if (itemByRefId) {
|
|
328
|
-
index =
|
|
430
|
+
index = tgt.findIndex((value: any) => value === itemByRefId);
|
|
329
431
|
}
|
|
330
432
|
|
|
331
433
|
// fallback to use last index
|
|
332
434
|
if (index === -1 || index === undefined) {
|
|
333
|
-
index =
|
|
435
|
+
index = tgt.length;
|
|
334
436
|
}
|
|
335
437
|
|
|
336
438
|
} else {
|
|
337
439
|
index = decode.number(bytes, it);
|
|
338
440
|
}
|
|
339
441
|
|
|
340
|
-
const type =
|
|
442
|
+
const type = tgt[$childType];
|
|
341
443
|
|
|
342
444
|
let dynamicIndex: number | string = index;
|
|
343
445
|
|
|
344
|
-
|
|
446
|
+
// Direct `items[index]` read — ArraySchema's `$getByIndex` is encoder-only
|
|
447
|
+
// (it consults `tmpItems`/`deletedIndexes`, which the decoder doesn't
|
|
448
|
+
// maintain). The decoder's authoritative state is `items`.
|
|
449
|
+
const previousValue = tgt.items[index];
|
|
450
|
+
const value = decodeValue(
|
|
345
451
|
decoder,
|
|
346
452
|
operation,
|
|
347
453
|
ref,
|
|
348
454
|
index,
|
|
455
|
+
previousValue,
|
|
349
456
|
type,
|
|
350
457
|
bytes,
|
|
351
458
|
it,
|
|
@@ -356,17 +463,15 @@ export const decodeArray: DecodeOperation = function (
|
|
|
356
463
|
value !== null && value !== undefined &&
|
|
357
464
|
value !== previousValue // avoid setting same value twice (if index === 0 it will result in a "unshift" for ArraySchema)
|
|
358
465
|
) {
|
|
359
|
-
|
|
360
|
-
(ref as ArraySchema)['$setAt'](index, value, operation);
|
|
466
|
+
tgt.$setAt(index, value, operation);
|
|
361
467
|
}
|
|
362
468
|
|
|
363
469
|
// add change
|
|
364
470
|
if (previousValue !== value) {
|
|
365
|
-
allChanges
|
|
471
|
+
allChanges?.push({
|
|
366
472
|
ref,
|
|
367
473
|
refId: decoder.currentRefId,
|
|
368
474
|
op: operation,
|
|
369
|
-
field: "", // FIXME: remove this
|
|
370
475
|
dynamicIndex,
|
|
371
476
|
value,
|
|
372
477
|
previousValue,
|