@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.
Files changed (96) hide show
  1. package/README.md +2 -0
  2. package/build/Metadata.d.ts +56 -2
  3. package/build/Reflection.d.ts +28 -34
  4. package/build/Schema.d.ts +70 -9
  5. package/build/annotations.d.ts +64 -17
  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 +5258 -1549
  27. package/build/index.cjs.map +1 -1
  28. package/build/index.d.ts +7 -3
  29. package/build/index.js +5258 -1549
  30. package/build/index.mjs +5249 -1549
  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 +7453 -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 +7450 -0
  38. package/build/input/index.mjs.map +1 -0
  39. package/build/types/HelperTypes.d.ts +67 -9
  40. package/build/types/TypeContext.d.ts +9 -0
  41. package/build/types/builder.d.ts +192 -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 +259 -31
  50. package/src/Reflection.ts +15 -13
  51. package/src/Schema.ts +176 -134
  52. package/src/annotations.ts +365 -252
  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 +121 -24
  88. package/src/types/TypeContext.ts +14 -2
  89. package/src/types/builder.ts +331 -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 +93 -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 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 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);
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, 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,20 +529,13 @@ 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
- // 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 extends Definition
511
- ? AssignableProps<InferSchemaInstanceType<T>>
512
- : never;
513
-
514
- // Helper type to determine if InitProps should be required
515
- type IsInitPropsRequired<T> = T extends { initialize: (props: any) => void }
516
- ? true
517
- : T extends { initialize: (...args: infer P) => void }
518
- ? P extends readonly []
519
- ? false
520
- : true
521
- : false;
522
-
523
- export interface SchemaWithExtends<T extends Definition, P extends typeof Schema, > {
524
- extends: <T2 extends Definition = Definition>(
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: "number",
536
- * y: "number",
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 extends Definition,
596
+ T,
544
597
  InitProps,
545
598
  P extends typeof Schema
546
599
  > extends SchemaWithExtends<T, P> {
547
600
  '~type': InferSchemaInstanceType<T>;
548
- new (...args: [InitProps] extends [never] ? [] : InitProps extends readonly any[] ? InitProps : IsInitPropsRequired<T> extends true ? [InitProps] : [InitProps?]): InferSchemaInstanceType<T> & InstanceType<P>;
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 Record<string, DefinitionType>,
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: 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']();
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
- // Direct Schema type: Type new Type()
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
- fields[fieldName] = getNormalizedType(value);
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 === 'function') {
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
- // call initialize method
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 (let fieldName in viewTagFields) {
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.extends = <T2 extends Definition = Definition>(fields: T2, name?: string) =>
697
- schema<T2>(fields, name, klass as any) as SchemaWithExtendsConstructor<T & T2, ExtractInitProps<T2>, P>;
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
  }