@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/annotations.ts
CHANGED
|
@@ -2,15 +2,18 @@ import "./symbol.shim.js";
|
|
|
2
2
|
import { Schema } from './Schema.js';
|
|
3
3
|
import { ArraySchema } from './types/custom/ArraySchema.js';
|
|
4
4
|
import { MapSchema } from './types/custom/MapSchema.js';
|
|
5
|
-
import { getNormalizedType, Metadata } from "./Metadata.js";
|
|
6
|
-
import { $changes, $childType, $descriptors, $numFields, $track } from "./types/symbols.js";
|
|
5
|
+
import { getNormalizedType, Metadata, resolveFieldType } from "./Metadata.js";
|
|
6
|
+
import { $changes, $childType, $descriptors, $encoders, $numFields, $track, $values } from "./types/symbols.js";
|
|
7
|
+
import { encode } from "./encoding/encode.js";
|
|
7
8
|
import { TypeDefinition, getType } from "./types/registry.js";
|
|
8
9
|
import { OPERATION } from "./encoding/spec.js";
|
|
9
10
|
import { TypeContext } from "./types/TypeContext.js";
|
|
10
|
-
import { assertInstanceType, assertType } from "./encoding/assert.js";
|
|
11
|
-
import type { InferValueType, InferSchemaInstanceType, AssignableProps, IsNever } from "./types/HelperTypes.js";
|
|
11
|
+
import { assertInstanceType, assertType, EncodeSchemaError } from "./encoding/assert.js";
|
|
12
|
+
import type { InferValueType, InferSchemaInstanceType, AssignableProps, BuilderInitProps, IsNever } from "./types/HelperTypes.js";
|
|
12
13
|
import { CollectionSchema } from "./types/custom/CollectionSchema.js";
|
|
13
14
|
import { SetSchema } from "./types/custom/SetSchema.js";
|
|
15
|
+
import { StreamSchema } from "./types/custom/StreamSchema.js";
|
|
16
|
+
import { FieldBuilder, isBuilder } from "./types/builder.js";
|
|
14
17
|
|
|
15
18
|
export type RawPrimitiveType = "string" |
|
|
16
19
|
"number" |
|
|
@@ -33,11 +36,12 @@ export type PrimitiveType = RawPrimitiveType | typeof Schema | object;
|
|
|
33
36
|
// TODO: infer "default" value type correctly.
|
|
34
37
|
export type DefinitionType<T extends PrimitiveType = PrimitiveType> = T
|
|
35
38
|
| T[]
|
|
36
|
-
| { type: T, default?: InferValueType<T>, view?: boolean | number, sync?: boolean }
|
|
37
|
-
| { array: T, default?: ArraySchema<InferValueType<T>>, view?: boolean | number, sync?: boolean }
|
|
38
|
-
| { map: T, default?: MapSchema<InferValueType<T>>, view?: boolean | number, sync?: boolean }
|
|
39
|
-
| { collection: T, default?: CollectionSchema<InferValueType<T>>, view?: boolean | number, sync?: boolean }
|
|
40
|
-
| { set: T, default?: SetSchema<InferValueType<T>>, view?: boolean | number, sync?: boolean }
|
|
39
|
+
| { type: T, default?: InferValueType<T>, view?: boolean | number, sync?: boolean, owned?: boolean }
|
|
40
|
+
| { array: T, default?: ArraySchema<InferValueType<T>>, view?: boolean | number, sync?: boolean, owned?: boolean }
|
|
41
|
+
| { map: T, default?: MapSchema<InferValueType<T>>, view?: boolean | number, sync?: boolean, owned?: boolean }
|
|
42
|
+
| { collection: T, default?: CollectionSchema<InferValueType<T>>, view?: boolean | number, sync?: boolean, owned?: boolean }
|
|
43
|
+
| { set: T, default?: SetSchema<InferValueType<T>>, view?: boolean | number, sync?: boolean, owned?: boolean }
|
|
44
|
+
| { stream: T, default?: StreamSchema<InferValueType<T>>, view?: boolean | number, sync?: boolean, owned?: boolean, priority?: (view: any, element: InferValueType<T>) => number };
|
|
41
45
|
|
|
42
46
|
export type Definition = { [field: string]: DefinitionType };
|
|
43
47
|
|
|
@@ -220,57 +224,32 @@ export function entity(constructor: any): any {
|
|
|
220
224
|
|
|
221
225
|
export function view<T> (tag: number = DEFAULT_VIEW_TAG) {
|
|
222
226
|
return function(target: T, fieldName: string) {
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
const parentClass = Object.getPrototypeOf(constructor);
|
|
226
|
-
const parentMetadata = parentClass[Symbol.metadata];
|
|
227
|
-
|
|
228
|
-
// TODO: use Metadata.initialize()
|
|
229
|
-
const metadata: Metadata = (constructor[Symbol.metadata] ??= Object.assign({}, constructor[Symbol.metadata], parentMetadata ?? Object.create(null)));
|
|
230
|
-
// const fieldIndex = metadata[fieldName];
|
|
231
|
-
|
|
232
|
-
// if (!metadata[fieldIndex]) {
|
|
233
|
-
// //
|
|
234
|
-
// // detect index for this field, considering inheritance
|
|
235
|
-
// //
|
|
236
|
-
// metadata[fieldIndex] = {
|
|
237
|
-
// type: undefined,
|
|
238
|
-
// index: (metadata[$numFields] // current structure already has fields defined
|
|
239
|
-
// ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
|
|
240
|
-
// ?? -1) + 1 // no fields defined
|
|
241
|
-
// }
|
|
242
|
-
// }
|
|
243
|
-
|
|
227
|
+
const metadata = Metadata.initialize(target.constructor as typeof Schema);
|
|
244
228
|
Metadata.setTag(metadata, fieldName, tag);
|
|
245
229
|
}
|
|
246
230
|
}
|
|
247
231
|
|
|
232
|
+
export function owned<T> (target: T, field: string) {
|
|
233
|
+
const metadata = Metadata.initialize(target.constructor as typeof Schema);
|
|
234
|
+
metadata[metadata[field]].owned = true;
|
|
235
|
+
}
|
|
236
|
+
|
|
248
237
|
export function unreliable<T> (target: T, field: string) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
// metadata[field] = {
|
|
265
|
-
// type: undefined,
|
|
266
|
-
// index: (metadata[$numFields] // current structure already has fields defined
|
|
267
|
-
// ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
|
|
268
|
-
// ?? -1) + 1 // no fields defined
|
|
269
|
-
// }
|
|
270
|
-
// }
|
|
271
|
-
|
|
272
|
-
// add owned flag to the field
|
|
273
|
-
metadata[metadata[field]].unreliable = true;
|
|
238
|
+
const metadata = Metadata.initialize(target.constructor as typeof Schema);
|
|
239
|
+
Metadata.setUnreliable(metadata, field);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* @transient — mark a field as not persisted to snapshots (encodeAll /
|
|
244
|
+
* encodeAllView). Transient fields are still emitted on per-tick patches
|
|
245
|
+
* (reliable or unreliable), but late-joining clients won't see them until
|
|
246
|
+
* the next mutation.
|
|
247
|
+
*
|
|
248
|
+
* Orthogonal to @unreliable: a field can be either, both, or neither.
|
|
249
|
+
*/
|
|
250
|
+
export function transient<T> (target: T, field: string) {
|
|
251
|
+
const metadata = Metadata.initialize(target.constructor as typeof Schema);
|
|
252
|
+
Metadata.setTransient(metadata, field);
|
|
274
253
|
}
|
|
275
254
|
|
|
276
255
|
export function type (
|
|
@@ -341,97 +320,179 @@ export function type (
|
|
|
341
320
|
);
|
|
342
321
|
|
|
343
322
|
} else {
|
|
344
|
-
const complexTypeKlass
|
|
345
|
-
|
|
346
|
-
const childType = (complexTypeKlass)
|
|
347
|
-
? Object.values(type)[0]
|
|
348
|
-
: type;
|
|
323
|
+
const { complexTypeKlass, childType } = resolveFieldType(type);
|
|
349
324
|
|
|
350
325
|
Metadata.addField(
|
|
351
326
|
metadata,
|
|
352
327
|
fieldIndex,
|
|
353
328
|
field,
|
|
354
329
|
type,
|
|
355
|
-
getPropertyDescriptor(
|
|
330
|
+
getPropertyDescriptor(field, fieldIndex, childType, complexTypeKlass)
|
|
356
331
|
);
|
|
357
332
|
}
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
333
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
complexTypeKlass: TypeDefinition,
|
|
366
|
-
) {
|
|
367
|
-
return {
|
|
368
|
-
get: function (this: Schema) { return this[fieldCached as keyof Schema]; },
|
|
369
|
-
set: function (this: Schema, value: any) {
|
|
370
|
-
const previousValue = this[fieldCached as keyof Schema] ?? undefined;
|
|
334
|
+
// Install accessor descriptor on the prototype (once per class field).
|
|
335
|
+
if (metadata[$descriptors][field]) {
|
|
336
|
+
Object.defineProperty(target, field, metadata[$descriptors][field]);
|
|
337
|
+
}
|
|
371
338
|
|
|
372
|
-
|
|
373
|
-
|
|
339
|
+
// Pre-compute encoder function for primitive types.
|
|
340
|
+
if (typeof type === "string") {
|
|
341
|
+
if (!metadata[$encoders]) {
|
|
342
|
+
Object.defineProperty(metadata, $encoders, {
|
|
343
|
+
value: [],
|
|
344
|
+
enumerable: false,
|
|
345
|
+
configurable: true,
|
|
346
|
+
writable: true,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
metadata[$encoders][fieldIndex] = (encode as any)[type];
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
374
353
|
|
|
354
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
355
|
+
// Per-field-shape specialized setters.
|
|
356
|
+
//
|
|
357
|
+
// Single shared closure used to handle all three shapes (primitive /
|
|
358
|
+
// schema-ref / collection) in one body with many branches. V8's inliner
|
|
359
|
+
// gave up on it because of the size + polymorphism. Splitting into three
|
|
360
|
+
// dedicated factories yields smaller, monomorphic bodies that the JIT can
|
|
361
|
+
// inline into hot setters like `position.x = 100`.
|
|
362
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
363
|
+
|
|
364
|
+
/** typeof target per primitive type. Cached once, looked up O(1) at decoration. */
|
|
365
|
+
const PRIMITIVE_TYPEOF: Record<string, "number" | "string" | "boolean" | "bigint"> = {
|
|
366
|
+
number: "number",
|
|
367
|
+
int8: "number", uint8: "number",
|
|
368
|
+
int16: "number", uint16: "number",
|
|
369
|
+
int32: "number", uint32: "number",
|
|
370
|
+
int64: "number", uint64: "number",
|
|
371
|
+
float32: "number", float64: "number",
|
|
372
|
+
bigint64: "bigint", biguint64: "bigint",
|
|
373
|
+
string: "string",
|
|
374
|
+
boolean: "boolean",
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
function makePrimitiveSetter(fieldName: string, fieldIndex: number, type: string) {
|
|
378
|
+
const typeofTarget = PRIMITIVE_TYPEOF[type]; // undefined for custom types
|
|
379
|
+
const allowNull = type === "string";
|
|
380
|
+
const isBool = type === "boolean";
|
|
381
|
+
return function (this: Schema, value: any) {
|
|
382
|
+
const values = this[$values];
|
|
383
|
+
const previousValue = values[fieldIndex];
|
|
384
|
+
if (value === previousValue) return;
|
|
385
|
+
|
|
386
|
+
if (value !== undefined && value !== null) {
|
|
387
|
+
// Inlined assertType primitive check.
|
|
375
388
|
if (
|
|
376
|
-
|
|
377
|
-
|
|
389
|
+
!isBool &&
|
|
390
|
+
typeofTarget !== undefined &&
|
|
391
|
+
typeof value !== typeofTarget &&
|
|
392
|
+
!(allowNull && value === null)
|
|
378
393
|
) {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
394
|
+
const ctorSuffix = (value && value.constructor) ? ` (${value.constructor.name})` : '';
|
|
395
|
+
throw new EncodeSchemaError(
|
|
396
|
+
`a '${typeofTarget}' was expected, but '${JSON.stringify(value)}'${ctorSuffix} was provided in ${this.constructor.name}#${fieldName}`
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
(this.constructor as typeof Schema)[$track](this[$changes], fieldIndex, OPERATION.ADD);
|
|
400
|
+
} else if (previousValue !== undefined && previousValue !== null) {
|
|
401
|
+
this[$changes].delete(fieldIndex);
|
|
402
|
+
}
|
|
403
|
+
values[fieldIndex] = value;
|
|
404
|
+
};
|
|
405
|
+
}
|
|
384
406
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
407
|
+
function makeSchemaRefSetter(fieldName: string, fieldIndex: number, type: typeof Schema) {
|
|
408
|
+
return function (this: Schema, value: any) {
|
|
409
|
+
const values = this[$values];
|
|
410
|
+
const previousValue = values[fieldIndex];
|
|
411
|
+
if (value === previousValue) return;
|
|
389
412
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
// value = new SetSchema(value);
|
|
393
|
-
// }
|
|
413
|
+
if (value !== undefined && value !== null) {
|
|
414
|
+
assertInstanceType(value, type, this, fieldName);
|
|
394
415
|
|
|
395
|
-
|
|
416
|
+
const changeTree = this[$changes];
|
|
417
|
+
const ctor = this.constructor as typeof Schema;
|
|
396
418
|
|
|
397
|
-
|
|
398
|
-
|
|
419
|
+
if (previousValue !== undefined && previousValue !== null && previousValue[$changes]) {
|
|
420
|
+
changeTree.root?.remove(previousValue[$changes]);
|
|
421
|
+
ctor[$track](changeTree, fieldIndex, OPERATION.DELETE_AND_ADD);
|
|
422
|
+
} else {
|
|
423
|
+
ctor[$track](changeTree, fieldIndex, OPERATION.ADD);
|
|
424
|
+
}
|
|
399
425
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
}
|
|
426
|
+
// External Schema-like instances may not carry a ChangeTree.
|
|
427
|
+
value[$changes]?.setParent(this, changeTree.root, fieldIndex);
|
|
403
428
|
|
|
404
|
-
|
|
429
|
+
} else if (previousValue !== undefined && previousValue !== null) {
|
|
430
|
+
this[$changes].delete(fieldIndex);
|
|
431
|
+
}
|
|
432
|
+
values[fieldIndex] = value;
|
|
433
|
+
};
|
|
434
|
+
}
|
|
405
435
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
436
|
+
function makeCollectionSetter(
|
|
437
|
+
_fieldName: string,
|
|
438
|
+
fieldIndex: number,
|
|
439
|
+
type: DefinitionType,
|
|
440
|
+
complexTypeKlass: TypeDefinition,
|
|
441
|
+
) {
|
|
442
|
+
const isArrayKlass = complexTypeKlass.constructor === ArraySchema;
|
|
443
|
+
const isMapKlass = complexTypeKlass.constructor === MapSchema;
|
|
444
|
+
return function (this: Schema, value: any) {
|
|
445
|
+
const values = this[$values];
|
|
446
|
+
const previousValue = values[fieldIndex];
|
|
447
|
+
if (value === previousValue) return;
|
|
448
|
+
|
|
449
|
+
if (value !== undefined && value !== null) {
|
|
450
|
+
// automatic Array → ArraySchema / Map → MapSchema conversion.
|
|
451
|
+
if (isArrayKlass && !(value instanceof ArraySchema)) {
|
|
452
|
+
value = new ArraySchema(...value);
|
|
453
|
+
} else if (isMapKlass && !(value instanceof MapSchema)) {
|
|
454
|
+
value = new MapSchema(value);
|
|
455
|
+
}
|
|
456
|
+
value[$childType] = type;
|
|
412
457
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
}
|
|
458
|
+
const changeTree = this[$changes];
|
|
459
|
+
const ctor = this.constructor as typeof Schema;
|
|
416
460
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
} else if (previousValue !== undefined) {
|
|
424
|
-
//
|
|
425
|
-
// Setting a field to `null` or `undefined` will delete it.
|
|
426
|
-
//
|
|
427
|
-
this[$changes].delete(fieldIndex);
|
|
461
|
+
if (previousValue !== undefined && previousValue !== null && previousValue[$changes]) {
|
|
462
|
+
changeTree.root?.remove(previousValue[$changes]);
|
|
463
|
+
ctor[$track](changeTree, fieldIndex, OPERATION.DELETE_AND_ADD);
|
|
464
|
+
} else {
|
|
465
|
+
ctor[$track](changeTree, fieldIndex, OPERATION.ADD);
|
|
428
466
|
}
|
|
429
467
|
|
|
430
|
-
|
|
431
|
-
},
|
|
468
|
+
value[$changes]?.setParent(this, changeTree.root, fieldIndex);
|
|
432
469
|
|
|
470
|
+
} else if (previousValue !== undefined && previousValue !== null) {
|
|
471
|
+
this[$changes].delete(fieldIndex);
|
|
472
|
+
}
|
|
473
|
+
values[fieldIndex] = value;
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export function getPropertyDescriptor(
|
|
478
|
+
fieldName: string,
|
|
479
|
+
fieldIndex: number,
|
|
480
|
+
type: DefinitionType,
|
|
481
|
+
complexTypeKlass: TypeDefinition | false,
|
|
482
|
+
) {
|
|
483
|
+
let setter: (this: Schema, value: any) => void;
|
|
484
|
+
if (complexTypeKlass) {
|
|
485
|
+
setter = makeCollectionSetter(fieldName, fieldIndex, type, complexTypeKlass);
|
|
486
|
+
} else if (typeof type === "string") {
|
|
487
|
+
setter = makePrimitiveSetter(fieldName, fieldIndex, type);
|
|
488
|
+
} else {
|
|
489
|
+
setter = makeSchemaRefSetter(fieldName, fieldIndex, type as typeof Schema);
|
|
490
|
+
}
|
|
491
|
+
return {
|
|
492
|
+
get: function (this: Schema) { return this[$values][fieldIndex]; },
|
|
493
|
+
set: setter,
|
|
433
494
|
enumerable: true,
|
|
434
|
-
configurable: true
|
|
495
|
+
configurable: true,
|
|
435
496
|
};
|
|
436
497
|
}
|
|
437
498
|
|
|
@@ -442,38 +503,21 @@ export function getPropertyDescriptor(
|
|
|
442
503
|
|
|
443
504
|
export function deprecated(throws: boolean = true): PropertyDecorator {
|
|
444
505
|
return function (klass: typeof Schema, field: string) {
|
|
445
|
-
|
|
446
|
-
// FIXME: the following block of code is repeated across `@type()`, `@deprecated()` and `@unreliable()` decorators.
|
|
447
|
-
//
|
|
448
|
-
const constructor = klass.constructor as typeof Schema;
|
|
449
|
-
|
|
450
|
-
const parentClass = Object.getPrototypeOf(constructor);
|
|
451
|
-
const parentMetadata = parentClass[Symbol.metadata];
|
|
452
|
-
const metadata: Metadata = (constructor[Symbol.metadata] ??= Object.assign({}, constructor[Symbol.metadata], parentMetadata ?? Object.create(null)));
|
|
506
|
+
const metadata = Metadata.initialize(klass.constructor as typeof Schema);
|
|
453
507
|
const fieldIndex = metadata[field];
|
|
454
508
|
|
|
455
|
-
// if (!metadata[field]) {
|
|
456
|
-
// //
|
|
457
|
-
// // detect index for this field, considering inheritance
|
|
458
|
-
// //
|
|
459
|
-
// metadata[field] = {
|
|
460
|
-
// type: undefined,
|
|
461
|
-
// index: (metadata[$numFields] // current structure already has fields defined
|
|
462
|
-
// ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
|
|
463
|
-
// ?? -1) + 1 // no fields defined
|
|
464
|
-
// }
|
|
465
|
-
// }
|
|
466
|
-
|
|
467
509
|
metadata[fieldIndex].deprecated = true;
|
|
468
510
|
|
|
469
511
|
if (throws) {
|
|
470
512
|
metadata[$descriptors] ??= {};
|
|
471
513
|
metadata[$descriptors][field] = {
|
|
472
514
|
get: function () { throw new Error(`${field} is deprecated.`); },
|
|
473
|
-
set: function (this: Schema,
|
|
515
|
+
set: function (this: Schema, _value: any) { /* throw new Error(`${field} is deprecated.`); */ },
|
|
474
516
|
enumerable: false,
|
|
475
517
|
configurable: true
|
|
476
518
|
};
|
|
519
|
+
// Override accessor on the prototype so deprecated throws at access.
|
|
520
|
+
Object.defineProperty(klass, field, metadata[$descriptors][field]);
|
|
477
521
|
}
|
|
478
522
|
|
|
479
523
|
// flag metadata[field] as non-enumerable
|
|
@@ -485,20 +529,13 @@ export function deprecated(throws: boolean = true): PropertyDecorator {
|
|
|
485
529
|
}
|
|
486
530
|
}
|
|
487
531
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
}
|
|
496
|
-
return target;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// Helper type to extract InitProps from initialize method
|
|
500
|
-
// Supports both single object parameter and multiple parameters
|
|
501
|
-
// If no initialize method is specified, use AssignableProps for field initialization
|
|
532
|
+
// Helper type to extract InitProps from initialize method.
|
|
533
|
+
// - Non-empty initialize params: use them directly.
|
|
534
|
+
// - Zero-arg initialize: no args accepted (`never`) — user-supplied field
|
|
535
|
+
// values would be dropped at runtime (parent's initialize is skipped
|
|
536
|
+
// during child construction via the `new.target === klass` guard, and
|
|
537
|
+
// own-field auto-assignment happens only inside initialize).
|
|
538
|
+
// - No initialize at all: derive from fields map.
|
|
502
539
|
type ExtractInitProps<T> = T extends { initialize: (...args: infer P) => void }
|
|
503
540
|
? P extends readonly []
|
|
504
541
|
? never
|
|
@@ -507,141 +544,190 @@ type ExtractInitProps<T> = T extends { initialize: (...args: infer P) => void }
|
|
|
507
544
|
? First
|
|
508
545
|
: P
|
|
509
546
|
: P
|
|
510
|
-
: T
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
547
|
+
: BuilderInitProps<T>;
|
|
548
|
+
|
|
549
|
+
// Does the init-props shape have at least one required property?
|
|
550
|
+
type HasRequiredKeys<X> = {} extends X ? false : true;
|
|
551
|
+
|
|
552
|
+
// Whether the constructor's init-props argument must be supplied.
|
|
553
|
+
// Mirrors the cases inside ExtractInitProps: non-empty initialize params
|
|
554
|
+
// are required; zero-arg initialize accepts nothing; no initialize
|
|
555
|
+
// depends on whether the derived BuilderInitProps has any required keys.
|
|
556
|
+
type IsInitPropsRequired<T> = T extends { initialize: (...args: infer P) => void }
|
|
557
|
+
? P extends readonly []
|
|
558
|
+
? false
|
|
559
|
+
: true
|
|
560
|
+
: HasRequiredKeys<BuilderInitProps<T>>;
|
|
561
|
+
|
|
562
|
+
// Whether T declares any non-empty `initialize` method. Used to tighten
|
|
563
|
+
// the constructor signature: authors who write an explicit `initialize()`
|
|
564
|
+
// with args opt into strict required args. Without an initialize the sig
|
|
565
|
+
// also allows `[]` so the common `new X(); x.field = ...` pattern works.
|
|
566
|
+
type HasExplicitInit<T> = T extends { initialize: (...args: infer P) => void }
|
|
567
|
+
? P extends readonly [] ? false : true
|
|
568
|
+
: false;
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* A `schema()` field definition accepts a FieldBuilder, a Schema subclass
|
|
572
|
+
* (shorthand for `t.ref(Class)`), or a method (attached to the prototype).
|
|
573
|
+
*/
|
|
574
|
+
export type FieldsAndMethods = Record<string, FieldBuilder<any, boolean, boolean> | (new (...args: any[]) => Schema) | Function>;
|
|
575
|
+
|
|
576
|
+
export interface SchemaWithExtends<T, P extends typeof Schema> {
|
|
577
|
+
extend: <T2 extends FieldsAndMethods = FieldsAndMethods>(
|
|
525
578
|
fields: T2 & ThisType<InferSchemaInstanceType<T & T2>>,
|
|
526
|
-
name?: string
|
|
527
|
-
) => SchemaWithExtendsConstructor<T & T2, ExtractInitProps<T2>, P>;
|
|
579
|
+
name?: string,
|
|
580
|
+
) => SchemaWithExtendsConstructor<T & T2, ExtractInitProps<T & T2>, P>;
|
|
528
581
|
}
|
|
529
582
|
|
|
530
583
|
/**
|
|
531
|
-
* Get the type of the schema defined via `schema({...})` method.
|
|
584
|
+
* Get the type of the schema defined via `schema('Name', {...})` method.
|
|
532
585
|
*
|
|
533
586
|
* @example
|
|
534
|
-
* const Entity = schema({
|
|
535
|
-
* x:
|
|
536
|
-
* y:
|
|
587
|
+
* const Entity = schema('Entity', {
|
|
588
|
+
* x: t.number(),
|
|
589
|
+
* y: t.number(),
|
|
537
590
|
* });
|
|
538
591
|
* type Entity = SchemaType<typeof Entity>;
|
|
539
592
|
*/
|
|
540
593
|
export type SchemaType<T extends {'~type': any}> = T['~type'];
|
|
541
594
|
|
|
542
595
|
export interface SchemaWithExtendsConstructor<
|
|
543
|
-
T
|
|
596
|
+
T,
|
|
544
597
|
InitProps,
|
|
545
598
|
P extends typeof Schema
|
|
546
599
|
> extends SchemaWithExtends<T, P> {
|
|
547
600
|
'~type': InferSchemaInstanceType<T>;
|
|
548
|
-
|
|
601
|
+
// Constructor signature:
|
|
602
|
+
// - InitProps = never (zero-arg initialize): no args.
|
|
603
|
+
// - InitProps is a tuple (multi-arg initialize): spread it.
|
|
604
|
+
// - Explicit `initialize(arg)` with required args: strict [InitProps]
|
|
605
|
+
// — the author opted into requiring them.
|
|
606
|
+
// - No initialize, but required builder fields: allow `[]` or
|
|
607
|
+
// `[InitProps]`. Preserves `new X(); x.field = ...` while still
|
|
608
|
+
// flagging incomplete-object mistakes like `new X({ hp: 1 })`.
|
|
609
|
+
// - Otherwise: optional single-arg.
|
|
610
|
+
new (...args:
|
|
611
|
+
[InitProps] extends [never] ? []
|
|
612
|
+
: InitProps extends readonly any[] ? InitProps
|
|
613
|
+
: HasExplicitInit<T> extends true ? [InitProps]
|
|
614
|
+
: IsInitPropsRequired<T> extends true ? ([] | [InitProps])
|
|
615
|
+
: [InitProps?]
|
|
616
|
+
): InferSchemaInstanceType<T> & InstanceType<P>;
|
|
549
617
|
prototype: InferSchemaInstanceType<T> & InstanceType<P> & {
|
|
550
618
|
initialize(...args: [InitProps] extends [never] ? [] : InitProps extends readonly any[] ? InitProps : [InitProps]): void;
|
|
551
619
|
};
|
|
552
620
|
}
|
|
553
621
|
|
|
622
|
+
/**
|
|
623
|
+
* Define a Schema class declaratively.
|
|
624
|
+
*
|
|
625
|
+
* @example
|
|
626
|
+
* import { schema, t } from '@colyseus/schema';
|
|
627
|
+
*
|
|
628
|
+
* const Player = schema({
|
|
629
|
+
* hp: t.uint8().default(100),
|
|
630
|
+
* name: t.string().view(),
|
|
631
|
+
* takeDamage(n: number) { this.hp -= n; },
|
|
632
|
+
* }, 'Player');
|
|
633
|
+
*
|
|
634
|
+
* const Warrior = Player.extend({
|
|
635
|
+
* weapon: t.string(),
|
|
636
|
+
* }, 'Warrior');
|
|
637
|
+
*/
|
|
554
638
|
export function schema<
|
|
555
|
-
T extends
|
|
639
|
+
T extends FieldsAndMethods,
|
|
556
640
|
P extends typeof Schema = typeof Schema
|
|
557
641
|
>(
|
|
558
642
|
fieldsAndMethods: T & ThisType<InferSchemaInstanceType<T>>,
|
|
559
643
|
name?: string,
|
|
560
|
-
inherits: P = Schema as P
|
|
644
|
+
inherits: P = Schema as P,
|
|
561
645
|
): SchemaWithExtendsConstructor<T, ExtractInitProps<T>, P> {
|
|
646
|
+
if (fieldsAndMethods == null || typeof fieldsAndMethods !== "object") {
|
|
647
|
+
throw new Error(`schema(): first argument must be a fields object (got ${typeof fieldsAndMethods}).`);
|
|
648
|
+
}
|
|
649
|
+
|
|
562
650
|
const fields: any = {};
|
|
563
651
|
const methods: any = {};
|
|
564
|
-
|
|
565
652
|
const defaultValues: any = {};
|
|
566
|
-
const viewTagFields:
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
if (
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
defaultValues[fieldName] = new
|
|
653
|
+
const viewTagFields: { [field: string]: number } = {};
|
|
654
|
+
const ownedFields: string[] = [];
|
|
655
|
+
const unreliableFields: string[] = [];
|
|
656
|
+
const transientFields: string[] = [];
|
|
657
|
+
const deprecatedFields: { [field: string]: boolean } = {};
|
|
658
|
+
const staticFields: string[] = [];
|
|
659
|
+
const streamFields: string[] = [];
|
|
660
|
+
const streamPriorityFields: { [field: string]: (view: any, element: any) => number } = {};
|
|
661
|
+
const optionalFields: string[] = [];
|
|
662
|
+
|
|
663
|
+
for (const fieldName in fieldsAndMethods) {
|
|
664
|
+
const value: any = (fieldsAndMethods as any)[fieldName];
|
|
665
|
+
|
|
666
|
+
if (isBuilder(value)) {
|
|
667
|
+
const def = value.toDefinition();
|
|
668
|
+
fields[fieldName] = getNormalizedType(def.type);
|
|
669
|
+
|
|
670
|
+
if (def.view !== undefined) { viewTagFields[fieldName] = def.view; }
|
|
671
|
+
if (def.owned) { ownedFields.push(fieldName); }
|
|
672
|
+
if (def.unreliable) { unreliableFields.push(fieldName); }
|
|
673
|
+
if (def.transient) { transientFields.push(fieldName); }
|
|
674
|
+
if (def.deprecated) { deprecatedFields[fieldName] = def.deprecatedThrows; }
|
|
675
|
+
if (def.static) { staticFields.push(fieldName); }
|
|
676
|
+
if (def.stream) { streamFields.push(fieldName); }
|
|
677
|
+
if (def.streamPriority !== undefined) { streamPriorityFields[fieldName] = def.streamPriority; }
|
|
678
|
+
if (def.optional) { optionalFields.push(fieldName); }
|
|
679
|
+
|
|
680
|
+
if (def.hasDefault) {
|
|
681
|
+
defaultValues[fieldName] = def.default;
|
|
682
|
+
} else if (!def.optional) {
|
|
683
|
+
// Auto-instantiate collection/Schema defaults when none is provided.
|
|
684
|
+
// `.optional()` opts out — field starts as undefined.
|
|
685
|
+
const rawType: any = def.type;
|
|
686
|
+
if (rawType && typeof rawType === "object") {
|
|
687
|
+
if (rawType.array !== undefined) {
|
|
688
|
+
defaultValues[fieldName] = new ArraySchema();
|
|
689
|
+
} else if (rawType.map !== undefined) {
|
|
690
|
+
defaultValues[fieldName] = new MapSchema();
|
|
691
|
+
} else if (rawType.set !== undefined) {
|
|
692
|
+
defaultValues[fieldName] = new SetSchema();
|
|
693
|
+
} else if (rawType.collection !== undefined) {
|
|
694
|
+
defaultValues[fieldName] = new CollectionSchema();
|
|
695
|
+
} else if (rawType.stream !== undefined) {
|
|
696
|
+
defaultValues[fieldName] = new StreamSchema();
|
|
697
|
+
}
|
|
698
|
+
} else if (typeof rawType === "function" && Schema.is(rawType)) {
|
|
699
|
+
if (!rawType.prototype.initialize || rawType.prototype.initialize.length === 0) {
|
|
700
|
+
defaultValues[fieldName] = new rawType();
|
|
608
701
|
}
|
|
609
702
|
}
|
|
610
|
-
} else {
|
|
611
|
-
defaultValues[fieldName] = value['default'];
|
|
612
703
|
}
|
|
613
704
|
|
|
614
|
-
|
|
615
|
-
} else if (typeof (value) === "function") {
|
|
705
|
+
} else if (typeof value === "function") {
|
|
616
706
|
if (Schema.is(value)) {
|
|
617
|
-
//
|
|
707
|
+
// Convenience: allow a bare Schema subclass (equivalent to `t.ref(Class)`).
|
|
708
|
+
fields[fieldName] = getNormalizedType(value);
|
|
618
709
|
if (!value.prototype.initialize || value.prototype.initialize.length === 0) {
|
|
619
|
-
// only auto-initialize Schema instances if:
|
|
620
|
-
// - they don't have an initialize method
|
|
621
|
-
// - or initialize method doesn't accept any parameters
|
|
622
710
|
defaultValues[fieldName] = new value();
|
|
623
711
|
}
|
|
624
|
-
fields[fieldName] = getNormalizedType(value);
|
|
625
712
|
} else {
|
|
626
713
|
methods[fieldName] = value;
|
|
627
714
|
}
|
|
628
715
|
|
|
629
716
|
} else {
|
|
630
|
-
|
|
717
|
+
throw new Error(
|
|
718
|
+
`schema(${name ? `'${name}'` : ""}): field '${fieldName}' must be a t.* builder, ` +
|
|
719
|
+
`Schema subclass, or method (got ${typeof value}).`
|
|
720
|
+
);
|
|
631
721
|
}
|
|
632
722
|
}
|
|
633
723
|
|
|
634
724
|
const getDefaultValues = () => {
|
|
635
725
|
const defaults: any = {};
|
|
636
|
-
|
|
637
|
-
// use current class default values
|
|
638
726
|
for (const fieldName in defaultValues) {
|
|
639
727
|
const defaultValue = defaultValues[fieldName];
|
|
640
|
-
if (defaultValue && typeof defaultValue.clone ===
|
|
641
|
-
// complex, cloneable values, e.g. Schema, ArraySchema, MapSchema, CollectionSchema, SetSchema
|
|
728
|
+
if (defaultValue && typeof defaultValue.clone === "function") {
|
|
642
729
|
defaults[fieldName] = defaultValue.clone();
|
|
643
730
|
} else {
|
|
644
|
-
// primitives and non-cloneable values
|
|
645
731
|
defaults[fieldName] = defaultValue;
|
|
646
732
|
}
|
|
647
733
|
}
|
|
@@ -657,44 +743,71 @@ export function schema<
|
|
|
657
743
|
}
|
|
658
744
|
}
|
|
659
745
|
return parentProps;
|
|
660
|
-
}
|
|
746
|
+
};
|
|
661
747
|
|
|
662
748
|
/** @codegen-ignore */
|
|
663
749
|
const klass = Metadata.setFields<any>(class extends (inherits as any) {
|
|
664
750
|
constructor(...args: any[]) {
|
|
665
|
-
|
|
666
|
-
if (methods.initialize && typeof methods.initialize === 'function') {
|
|
751
|
+
if (methods.initialize && typeof methods.initialize === "function") {
|
|
667
752
|
super(Object.assign({}, getDefaultValues(), getParentProps(args[0] || {})));
|
|
668
|
-
|
|
669
|
-
* only call initialize() in the current class, not the parent ones.
|
|
670
|
-
* see "should not call initialize automatically when creating an instance of inherited Schema"
|
|
671
|
-
*/
|
|
753
|
+
// Only call initialize() on the exact target class, not parents.
|
|
672
754
|
if (new.target === klass) {
|
|
673
755
|
methods.initialize.apply(this, args);
|
|
674
756
|
}
|
|
675
|
-
|
|
676
757
|
} else {
|
|
677
758
|
super(Object.assign({}, getDefaultValues(), args[0] || {}));
|
|
678
759
|
}
|
|
679
760
|
}
|
|
680
|
-
}, fields) as SchemaWithExtendsConstructor<T, ExtractInitProps<T>, P>;
|
|
761
|
+
}, fields) as unknown as SchemaWithExtendsConstructor<T, ExtractInitProps<T>, P>;
|
|
681
762
|
|
|
682
|
-
// Store the getDefaultValues function on the class for inheritance
|
|
683
763
|
(klass as any)._getDefaultValues = getDefaultValues;
|
|
684
764
|
|
|
685
|
-
// Add methods to the prototype
|
|
686
765
|
Object.assign(klass.prototype, methods);
|
|
687
766
|
|
|
688
|
-
for (
|
|
767
|
+
for (const fieldName in viewTagFields) {
|
|
689
768
|
view(viewTagFields[fieldName])(klass.prototype, fieldName);
|
|
690
769
|
}
|
|
770
|
+
for (const fieldName of ownedFields) {
|
|
771
|
+
owned(klass.prototype, fieldName);
|
|
772
|
+
}
|
|
773
|
+
for (const fieldName of unreliableFields) {
|
|
774
|
+
unreliable(klass.prototype, fieldName);
|
|
775
|
+
}
|
|
776
|
+
for (const fieldName of transientFields) {
|
|
777
|
+
transient(klass.prototype, fieldName);
|
|
778
|
+
}
|
|
779
|
+
for (const fieldName in deprecatedFields) {
|
|
780
|
+
deprecated(deprecatedFields[fieldName])(klass.prototype, fieldName);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if (staticFields.length > 0 || streamFields.length > 0) {
|
|
784
|
+
const metadata = (klass as any)[Symbol.metadata] as Metadata;
|
|
785
|
+
for (const fieldName of staticFields) {
|
|
786
|
+
Metadata.setStatic(metadata, fieldName);
|
|
787
|
+
}
|
|
788
|
+
for (const fieldName of streamFields) {
|
|
789
|
+
Metadata.setStream(metadata, fieldName);
|
|
790
|
+
}
|
|
791
|
+
for (const fieldName in streamPriorityFields) {
|
|
792
|
+
Metadata.setStreamPriority(metadata, fieldName, streamPriorityFields[fieldName]);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (optionalFields.length > 0) {
|
|
797
|
+
const metadata = (klass as any)[Symbol.metadata] as Metadata;
|
|
798
|
+
for (const fieldName of optionalFields) {
|
|
799
|
+
metadata[metadata[fieldName]].optional = true;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
691
802
|
|
|
692
803
|
if (name) {
|
|
693
804
|
Object.defineProperty(klass, "name", { value: name });
|
|
694
805
|
}
|
|
695
806
|
|
|
696
|
-
klass.
|
|
697
|
-
|
|
807
|
+
(klass as any).extend = <T2 extends FieldsAndMethods = FieldsAndMethods>(
|
|
808
|
+
childFields: T2,
|
|
809
|
+
childName?: string,
|
|
810
|
+
) => schema<T2>(childFields, childName, klass as any);
|
|
698
811
|
|
|
699
812
|
return klass;
|
|
700
813
|
}
|