@colyseus/schema 4.0.20 → 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.
Files changed (96) hide show
  1. package/README.md +2 -0
  2. package/build/Metadata.d.ts +55 -2
  3. package/build/Reflection.d.ts +24 -30
  4. package/build/Schema.d.ts +70 -9
  5. package/build/annotations.d.ts +56 -13
  6. package/build/codegen/cli.cjs +84 -67
  7. package/build/codegen/cli.cjs.map +1 -1
  8. package/build/decoder/DecodeOperation.d.ts +48 -5
  9. package/build/decoder/Decoder.d.ts +2 -2
  10. package/build/decoder/strategy/Callbacks.d.ts +1 -1
  11. package/build/encoder/ChangeRecorder.d.ts +107 -0
  12. package/build/encoder/ChangeTree.d.ts +218 -69
  13. package/build/encoder/EncodeDescriptor.d.ts +63 -0
  14. package/build/encoder/EncodeOperation.d.ts +25 -2
  15. package/build/encoder/Encoder.d.ts +59 -3
  16. package/build/encoder/MapJournal.d.ts +62 -0
  17. package/build/encoder/RefIdAllocator.d.ts +35 -0
  18. package/build/encoder/Root.d.ts +94 -13
  19. package/build/encoder/StateView.d.ts +116 -8
  20. package/build/encoder/changeTree/inheritedFlags.d.ts +34 -0
  21. package/build/encoder/changeTree/liveIteration.d.ts +3 -0
  22. package/build/encoder/changeTree/parentChain.d.ts +24 -0
  23. package/build/encoder/changeTree/treeAttachment.d.ts +13 -0
  24. package/build/encoder/streaming.d.ts +73 -0
  25. package/build/encoder/subscriptions.d.ts +25 -0
  26. package/build/index.cjs +5202 -1552
  27. package/build/index.cjs.map +1 -1
  28. package/build/index.d.ts +7 -3
  29. package/build/index.js +5202 -1552
  30. package/build/index.mjs +5193 -1552
  31. package/build/index.mjs.map +1 -1
  32. package/build/input/InputDecoder.d.ts +32 -0
  33. package/build/input/InputEncoder.d.ts +117 -0
  34. package/build/input/index.cjs +7429 -0
  35. package/build/input/index.cjs.map +1 -0
  36. package/build/input/index.d.ts +3 -0
  37. package/build/input/index.mjs +7426 -0
  38. package/build/input/index.mjs.map +1 -0
  39. package/build/types/HelperTypes.d.ts +22 -8
  40. package/build/types/TypeContext.d.ts +9 -0
  41. package/build/types/builder.d.ts +162 -0
  42. package/build/types/custom/ArraySchema.d.ts +25 -4
  43. package/build/types/custom/CollectionSchema.d.ts +30 -2
  44. package/build/types/custom/MapSchema.d.ts +52 -3
  45. package/build/types/custom/SetSchema.d.ts +32 -2
  46. package/build/types/custom/StreamSchema.d.ts +114 -0
  47. package/build/types/symbols.d.ts +48 -5
  48. package/package.json +9 -3
  49. package/src/Metadata.ts +258 -31
  50. package/src/Reflection.ts +15 -13
  51. package/src/Schema.ts +176 -134
  52. package/src/annotations.ts +308 -236
  53. package/src/bench_bloat.ts +173 -0
  54. package/src/bench_decode.ts +221 -0
  55. package/src/bench_decode_mem.ts +165 -0
  56. package/src/bench_encode.ts +108 -0
  57. package/src/bench_init.ts +150 -0
  58. package/src/bench_static.ts +109 -0
  59. package/src/bench_stream.ts +295 -0
  60. package/src/bench_view_cmp.ts +142 -0
  61. package/src/codegen/languages/csharp.ts +0 -24
  62. package/src/codegen/parser.ts +83 -61
  63. package/src/decoder/DecodeOperation.ts +168 -63
  64. package/src/decoder/Decoder.ts +20 -10
  65. package/src/decoder/ReferenceTracker.ts +4 -0
  66. package/src/decoder/strategy/Callbacks.ts +30 -26
  67. package/src/decoder/strategy/getDecoderStateCallbacks.ts +16 -13
  68. package/src/encoder/ChangeRecorder.ts +276 -0
  69. package/src/encoder/ChangeTree.ts +674 -519
  70. package/src/encoder/EncodeDescriptor.ts +213 -0
  71. package/src/encoder/EncodeOperation.ts +107 -65
  72. package/src/encoder/Encoder.ts +630 -119
  73. package/src/encoder/MapJournal.ts +124 -0
  74. package/src/encoder/RefIdAllocator.ts +68 -0
  75. package/src/encoder/Root.ts +247 -120
  76. package/src/encoder/StateView.ts +592 -121
  77. package/src/encoder/changeTree/inheritedFlags.ts +217 -0
  78. package/src/encoder/changeTree/liveIteration.ts +74 -0
  79. package/src/encoder/changeTree/parentChain.ts +131 -0
  80. package/src/encoder/changeTree/treeAttachment.ts +171 -0
  81. package/src/encoder/streaming.ts +232 -0
  82. package/src/encoder/subscriptions.ts +71 -0
  83. package/src/index.ts +15 -3
  84. package/src/input/InputDecoder.ts +57 -0
  85. package/src/input/InputEncoder.ts +303 -0
  86. package/src/input/index.ts +3 -0
  87. package/src/types/HelperTypes.ts +21 -9
  88. package/src/types/TypeContext.ts +14 -2
  89. package/src/types/builder.ts +285 -0
  90. package/src/types/custom/ArraySchema.ts +210 -197
  91. package/src/types/custom/CollectionSchema.ts +115 -35
  92. package/src/types/custom/MapSchema.ts +162 -58
  93. package/src/types/custom/SetSchema.ts +128 -39
  94. package/src/types/custom/StreamSchema.ts +310 -0
  95. package/src/types/symbols.ts +54 -6
  96. package/src/utils.ts +4 -6
@@ -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 { assertInstanceType, assertType, EncodeSchemaError } from "./encoding/assert.js";
11
12
  import type { InferValueType, InferSchemaInstanceType, AssignableProps, 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 constructor = target.constructor as typeof Schema;
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
- // FIXME: the following block of code is repeated across `@type()`, `@deprecated()` and `@unreliable()` decorators.
251
- //
252
- const constructor = target.constructor as typeof Schema;
253
-
254
- const parentClass = Object.getPrototypeOf(constructor);
255
- const parentMetadata = parentClass[Symbol.metadata];
256
-
257
- // TODO: use Metadata.initialize()
258
- const metadata: Metadata = (constructor[Symbol.metadata] ??= Object.assign({}, constructor[Symbol.metadata], parentMetadata ?? Object.create(null)));
259
-
260
- // if (!metadata[field]) {
261
- // //
262
- // // detect index for this field, considering inheritance
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 = typeof(Object.keys(type)[0]) === "string" && getType(Object.keys(type)[0]);
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(`_${field}`, fieldIndex, childType, complexTypeKlass)
330
+ getPropertyDescriptor(field, fieldIndex, childType, complexTypeKlass)
356
331
  );
357
332
  }
358
- }
359
- }
360
333
 
361
- export function getPropertyDescriptor(
362
- fieldCached: string,
363
- fieldIndex: number,
364
- type: DefinitionType,
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
- // skip if value is the same as cached.
373
- if (value === previousValue) { return; }
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
- value !== undefined &&
377
- value !== null
389
+ !isBool &&
390
+ typeofTarget !== undefined &&
391
+ typeof value !== typeofTarget &&
392
+ !(allowNull && value === null)
378
393
  ) {
379
- if (complexTypeKlass) {
380
- // automaticallty transform Array into ArraySchema
381
- if (complexTypeKlass.constructor === ArraySchema && !(value instanceof ArraySchema)) {
382
- value = new ArraySchema(...value);
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
- // automaticallty transform Map into MapSchema
386
- if (complexTypeKlass.constructor === MapSchema && !(value instanceof MapSchema)) {
387
- value = new MapSchema(value);
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
- // // automaticallty transform Array into SetSchema
391
- // if (complexTypeKlass.constructor === SetSchema && !(value instanceof SetSchema)) {
392
- // value = new SetSchema(value);
393
- // }
413
+ if (value !== undefined && value !== null) {
414
+ assertInstanceType(value, type, this, fieldName);
394
415
 
395
- value[$childType] = type;
416
+ const changeTree = this[$changes];
417
+ const ctor = this.constructor as typeof Schema;
396
418
 
397
- } else if (typeof (type) !== "string") {
398
- assertInstanceType(value, type as typeof Schema, this, fieldCached.substring(1));
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
- } else {
401
- assertType(value, type, this, fieldCached.substring(1));
402
- }
426
+ // External Schema-like instances may not carry a ChangeTree.
427
+ value[$changes]?.setParent(this, changeTree.root, fieldIndex);
403
428
 
404
- const changeTree = this[$changes];
429
+ } else if (previousValue !== undefined && previousValue !== null) {
430
+ this[$changes].delete(fieldIndex);
431
+ }
432
+ values[fieldIndex] = value;
433
+ };
434
+ }
405
435
 
406
- //
407
- // Replacing existing "ref", remove it from root.
408
- //
409
- if (previousValue !== undefined && previousValue[$changes]) {
410
- changeTree.root?.remove(previousValue[$changes]);
411
- (this.constructor as typeof Schema)[$track](changeTree, fieldIndex, OPERATION.DELETE_AND_ADD);
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
- } else {
414
- (this.constructor as typeof Schema)[$track](changeTree, fieldIndex, OPERATION.ADD);
415
- }
458
+ const changeTree = this[$changes];
459
+ const ctor = this.constructor as typeof Schema;
416
460
 
417
- //
418
- // call setParent() recursively for this and its child
419
- // structures.
420
- //
421
- value[$changes]?.setParent(this, changeTree.root, fieldIndex);
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
- this[fieldCached as keyof Schema] = value;
431
- },
468
+ value[$changes]?.setParent(this, changeTree.root, fieldIndex);
469
+
470
+ } else if (previousValue !== undefined && previousValue !== null) {
471
+ this[$changes].delete(fieldIndex);
472
+ }
473
+ values[fieldIndex] = value;
474
+ };
475
+ }
432
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, value: any) { /* throw new Error(`${field} is deprecated.`); */ },
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,17 +529,6 @@ export function deprecated(throws: boolean = true): PropertyDecorator {
485
529
  }
486
530
  }
487
531
 
488
- export function defineTypes(
489
- target: typeof Schema,
490
- fields: Definition,
491
- options?: TypeOptions
492
- ) {
493
- for (let field in fields) {
494
- type(fields[field], options)(target.prototype, field);
495
- }
496
- return target;
497
- }
498
-
499
532
  // Helper type to extract InitProps from initialize method
500
533
  // Supports both single object parameter and multiple parameters
501
534
  // If no initialize method is specified, use AssignableProps for field initialization
@@ -507,9 +540,7 @@ type ExtractInitProps<T> = T extends { initialize: (...args: infer P) => void }
507
540
  ? First
508
541
  : P
509
542
  : P
510
- : T extends Definition
511
- ? AssignableProps<InferSchemaInstanceType<T>>
512
- : never;
543
+ : AssignableProps<InferSchemaInstanceType<T>>;
513
544
 
514
545
  // Helper type to determine if InitProps should be required
515
546
  type IsInitPropsRequired<T> = T extends { initialize: (props: any) => void }
@@ -520,27 +551,33 @@ type IsInitPropsRequired<T> = T extends { initialize: (props: any) => void }
520
551
  : true
521
552
  : false;
522
553
 
523
- export interface SchemaWithExtends<T extends Definition, P extends typeof Schema, > {
524
- extends: <T2 extends Definition = Definition>(
554
+ /**
555
+ * A `schema()` field definition accepts a FieldBuilder, a Schema subclass
556
+ * (shorthand for `t.ref(Class)`), or a method (attached to the prototype).
557
+ */
558
+ export type FieldsAndMethods = Record<string, FieldBuilder<any> | (new (...args: any[]) => Schema) | Function>;
559
+
560
+ export interface SchemaWithExtends<T, P extends typeof Schema> {
561
+ extend: <T2 extends FieldsAndMethods = FieldsAndMethods>(
525
562
  fields: T2 & ThisType<InferSchemaInstanceType<T & T2>>,
526
- name?: string
563
+ name?: string,
527
564
  ) => SchemaWithExtendsConstructor<T & T2, ExtractInitProps<T2>, P>;
528
565
  }
529
566
 
530
567
  /**
531
- * Get the type of the schema defined via `schema({...})` method.
568
+ * Get the type of the schema defined via `schema('Name', {...})` method.
532
569
  *
533
570
  * @example
534
- * const Entity = schema({
535
- * x: "number",
536
- * y: "number",
571
+ * const Entity = schema('Entity', {
572
+ * x: t.number(),
573
+ * y: t.number(),
537
574
  * });
538
575
  * type Entity = SchemaType<typeof Entity>;
539
576
  */
540
577
  export type SchemaType<T extends {'~type': any}> = T['~type'];
541
578
 
542
579
  export interface SchemaWithExtendsConstructor<
543
- T extends Definition,
580
+ T,
544
581
  InitProps,
545
582
  P extends typeof Schema
546
583
  > extends SchemaWithExtends<T, P> {
@@ -551,97 +588,112 @@ export interface SchemaWithExtendsConstructor<
551
588
  };
552
589
  }
553
590
 
591
+ /**
592
+ * Define a Schema class declaratively.
593
+ *
594
+ * @example
595
+ * import { schema, t } from '@colyseus/schema';
596
+ *
597
+ * const Player = schema({
598
+ * hp: t.uint8().default(100),
599
+ * name: t.string().view(),
600
+ * takeDamage(n: number) { this.hp -= n; },
601
+ * }, 'Player');
602
+ *
603
+ * const Warrior = Player.extend({
604
+ * weapon: t.string(),
605
+ * }, 'Warrior');
606
+ */
554
607
  export function schema<
555
- T extends Record<string, DefinitionType>,
608
+ T extends FieldsAndMethods,
556
609
  P extends typeof Schema = typeof Schema
557
610
  >(
558
611
  fieldsAndMethods: T & ThisType<InferSchemaInstanceType<T>>,
559
612
  name?: string,
560
- inherits: P = Schema as P
613
+ inherits: P = Schema as P,
561
614
  ): SchemaWithExtendsConstructor<T, ExtractInitProps<T>, P> {
615
+ if (fieldsAndMethods == null || typeof fieldsAndMethods !== "object") {
616
+ throw new Error(`schema(): first argument must be a fields object (got ${typeof fieldsAndMethods}).`);
617
+ }
618
+
562
619
  const fields: any = {};
563
620
  const methods: any = {};
564
-
565
621
  const defaultValues: any = {};
566
- const viewTagFields: any = {};
567
-
568
- for (let fieldName in fieldsAndMethods) {
569
- const value: any = fieldsAndMethods[fieldName] as DefinitionType;
570
- if (typeof (value) === "object") {
571
- if (value['view'] !== undefined) {
572
- viewTagFields[fieldName] = (typeof (value['view']) === "boolean")
573
- ? DEFAULT_VIEW_TAG
574
- : value['view'];
575
- }
576
-
577
- // allow to define a field as not synced
578
- if (value['sync'] !== false) {
579
- fields[fieldName] = getNormalizedType(value);
580
- }
581
-
582
- // If no explicit default provided, handle automatic instantiation for collection types
583
- if (!Object.prototype.hasOwnProperty.call(value, 'default')) {
584
- // TODO: remove Array.isArray() check. Use ['array'] !== undefined only.
585
- if (Array.isArray(value) || value['array'] !== undefined) {
586
- // Collection: Array new ArraySchema()
587
- defaultValues[fieldName] = new ArraySchema();
588
-
589
- } else if (value['map'] !== undefined) {
590
- // Collection: Map → new MapSchema()
591
- defaultValues[fieldName] = new MapSchema();
592
-
593
- } else if (value['collection'] !== undefined) {
594
- // Collection: Collection new CollectionSchema()
595
- defaultValues[fieldName] = new CollectionSchema();
596
-
597
- } else if (value['set'] !== undefined) {
598
- // Collection: Set → new SetSchema()
599
- defaultValues[fieldName] = new SetSchema();
600
-
601
- } else if (value['type'] !== undefined && Schema.is(value['type'])) {
602
- // Direct Schema type: Type → new Type()
603
- if (!value['type'].prototype.initialize || value['type'].prototype.initialize.length === 0) {
604
- // only auto-initialize Schema instances if:
605
- // - they don't have an initialize method
606
- // - or initialize method doesn't accept any parameters
607
- defaultValues[fieldName] = new value['type']();
622
+ const viewTagFields: { [field: string]: number } = {};
623
+ const ownedFields: string[] = [];
624
+ const unreliableFields: string[] = [];
625
+ const transientFields: string[] = [];
626
+ const deprecatedFields: { [field: string]: boolean } = {};
627
+ const staticFields: string[] = [];
628
+ const streamFields: string[] = [];
629
+ const streamPriorityFields: { [field: string]: (view: any, element: any) => number } = {};
630
+
631
+ for (const fieldName in fieldsAndMethods) {
632
+ const value: any = (fieldsAndMethods as any)[fieldName];
633
+
634
+ if (isBuilder(value)) {
635
+ const def = value.toDefinition();
636
+ fields[fieldName] = getNormalizedType(def.type);
637
+
638
+ if (def.view !== undefined) { viewTagFields[fieldName] = def.view; }
639
+ if (def.owned) { ownedFields.push(fieldName); }
640
+ if (def.unreliable) { unreliableFields.push(fieldName); }
641
+ if (def.transient) { transientFields.push(fieldName); }
642
+ if (def.deprecated) { deprecatedFields[fieldName] = def.deprecatedThrows; }
643
+ if (def.static) { staticFields.push(fieldName); }
644
+ if (def.stream) { streamFields.push(fieldName); }
645
+ if (def.streamPriority !== undefined) { streamPriorityFields[fieldName] = def.streamPriority; }
646
+
647
+ if (def.hasDefault) {
648
+ defaultValues[fieldName] = def.default;
649
+ } else {
650
+ // Auto-instantiate collection/Schema defaults when none is provided.
651
+ const rawType: any = def.type;
652
+ if (rawType && typeof rawType === "object") {
653
+ if (rawType.array !== undefined) {
654
+ defaultValues[fieldName] = new ArraySchema();
655
+ } else if (rawType.map !== undefined) {
656
+ defaultValues[fieldName] = new MapSchema();
657
+ } else if (rawType.set !== undefined) {
658
+ defaultValues[fieldName] = new SetSchema();
659
+ } else if (rawType.collection !== undefined) {
660
+ defaultValues[fieldName] = new CollectionSchema();
661
+ } else if (rawType.stream !== undefined) {
662
+ defaultValues[fieldName] = new StreamSchema();
663
+ }
664
+ } else if (typeof rawType === "function" && Schema.is(rawType)) {
665
+ if (!rawType.prototype.initialize || rawType.prototype.initialize.length === 0) {
666
+ defaultValues[fieldName] = new rawType();
608
667
  }
609
668
  }
610
- } else {
611
- defaultValues[fieldName] = value['default'];
612
669
  }
613
670
 
614
-
615
- } else if (typeof (value) === "function") {
671
+ } else if (typeof value === "function") {
616
672
  if (Schema.is(value)) {
617
- // Direct Schema type: Type new Type()
673
+ // Convenience: allow a bare Schema subclass (equivalent to `t.ref(Class)`).
674
+ fields[fieldName] = getNormalizedType(value);
618
675
  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
676
  defaultValues[fieldName] = new value();
623
677
  }
624
- fields[fieldName] = getNormalizedType(value);
625
678
  } else {
626
679
  methods[fieldName] = value;
627
680
  }
628
681
 
629
682
  } else {
630
- fields[fieldName] = getNormalizedType(value);
683
+ throw new Error(
684
+ `schema(${name ? `'${name}'` : ""}): field '${fieldName}' must be a t.* builder, ` +
685
+ `Schema subclass, or method (got ${typeof value}).`
686
+ );
631
687
  }
632
688
  }
633
689
 
634
690
  const getDefaultValues = () => {
635
691
  const defaults: any = {};
636
-
637
- // use current class default values
638
692
  for (const fieldName in defaultValues) {
639
693
  const defaultValue = defaultValues[fieldName];
640
- if (defaultValue && typeof defaultValue.clone === 'function') {
641
- // complex, cloneable values, e.g. Schema, ArraySchema, MapSchema, CollectionSchema, SetSchema
694
+ if (defaultValue && typeof defaultValue.clone === "function") {
642
695
  defaults[fieldName] = defaultValue.clone();
643
696
  } else {
644
- // primitives and non-cloneable values
645
697
  defaults[fieldName] = defaultValue;
646
698
  }
647
699
  }
@@ -657,44 +709,64 @@ export function schema<
657
709
  }
658
710
  }
659
711
  return parentProps;
660
- }
712
+ };
661
713
 
662
714
  /** @codegen-ignore */
663
715
  const klass = Metadata.setFields<any>(class extends (inherits as any) {
664
716
  constructor(...args: any[]) {
665
- // call initialize method
666
- if (methods.initialize && typeof methods.initialize === 'function') {
717
+ if (methods.initialize && typeof methods.initialize === "function") {
667
718
  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
- */
719
+ // Only call initialize() on the exact target class, not parents.
672
720
  if (new.target === klass) {
673
721
  methods.initialize.apply(this, args);
674
722
  }
675
-
676
723
  } else {
677
724
  super(Object.assign({}, getDefaultValues(), args[0] || {}));
678
725
  }
679
726
  }
680
- }, fields) as SchemaWithExtendsConstructor<T, ExtractInitProps<T>, P>;
727
+ }, fields) as unknown as SchemaWithExtendsConstructor<T, ExtractInitProps<T>, P>;
681
728
 
682
- // Store the getDefaultValues function on the class for inheritance
683
729
  (klass as any)._getDefaultValues = getDefaultValues;
684
730
 
685
- // Add methods to the prototype
686
731
  Object.assign(klass.prototype, methods);
687
732
 
688
- for (let fieldName in viewTagFields) {
733
+ for (const fieldName in viewTagFields) {
689
734
  view(viewTagFields[fieldName])(klass.prototype, fieldName);
690
735
  }
736
+ for (const fieldName of ownedFields) {
737
+ owned(klass.prototype, fieldName);
738
+ }
739
+ for (const fieldName of unreliableFields) {
740
+ unreliable(klass.prototype, fieldName);
741
+ }
742
+ for (const fieldName of transientFields) {
743
+ transient(klass.prototype, fieldName);
744
+ }
745
+ for (const fieldName in deprecatedFields) {
746
+ deprecated(deprecatedFields[fieldName])(klass.prototype, fieldName);
747
+ }
748
+
749
+ if (staticFields.length > 0 || streamFields.length > 0) {
750
+ const metadata = (klass as any)[Symbol.metadata] as Metadata;
751
+ for (const fieldName of staticFields) {
752
+ Metadata.setStatic(metadata, fieldName);
753
+ }
754
+ for (const fieldName of streamFields) {
755
+ Metadata.setStream(metadata, fieldName);
756
+ }
757
+ for (const fieldName in streamPriorityFields) {
758
+ Metadata.setStreamPriority(metadata, fieldName, streamPriorityFields[fieldName]);
759
+ }
760
+ }
691
761
 
692
762
  if (name) {
693
763
  Object.defineProperty(klass, "name", { value: name });
694
764
  }
695
765
 
696
- klass.extends = <T2 extends Definition = Definition>(fields: T2, name?: string) =>
697
- schema<T2>(fields, name, klass as any) as SchemaWithExtendsConstructor<T & T2, ExtractInitProps<T2>, P>;
766
+ (klass as any).extend = <T2 extends FieldsAndMethods = FieldsAndMethods>(
767
+ childFields: T2,
768
+ childName?: string,
769
+ ) => schema<T2>(childFields, childName, klass as any);
698
770
 
699
771
  return klass;
700
772
  }