@colyseus/schema 5.0.20 → 5.0.21

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colyseus/schema",
3
- "version": "5.0.20",
3
+ "version": "5.0.21",
4
4
  "description": "Automatic state replication for multiplayer games. Mutate plain objects, and every client gets a live, typed mirror through delta encoding.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/Metadata.ts CHANGED
@@ -488,6 +488,10 @@ export const Metadata = {
488
488
  }
489
489
 
490
490
  for (const field in fields) {
491
+ // metadata inherits the parent's, so this also catches a redeclared parent field
492
+ if (metadata[field] !== undefined) {
493
+ throw new Error(`@colyseus/schema: Duplicate '${field}' definition on '${constructor.name || "(anonymous)"}'.`);
494
+ }
491
495
  Metadata.defineField(constructor, metadata, fieldIndex, field, fields[field] as DefinitionType);
492
496
  fieldIndex++;
493
497
  }
@@ -626,11 +626,11 @@ export function defineTypes(
626
626
 
627
627
  // Helper type to extract InitProps from initialize method.
628
628
  // - Non-empty initialize params: use them directly.
629
- // - Zero-arg initialize: no args accepted (`never`) — user-supplied field
630
- // values would be dropped at runtime (parent's initialize is skipped
631
- // during child construction via the `new.target === klass` guard, and
632
- // own-field auto-assignment happens only inside initialize).
629
+ // - Zero-arg initialize: no args accepted (`never`) — an initialize() owns the
630
+ // fields, so user-supplied values would be dropped at runtime.
633
631
  // - No initialize at all: derive from fields map.
632
+ // `T & T2` from `.extend()` carries the parent's initialize unless the child
633
+ // redefines it, matching the runtime (the most-derived one runs).
634
634
  type ExtractInitProps<T> = T extends { initialize: (...args: infer P) => void }
635
635
  ? P extends readonly []
636
636
  ? never
@@ -713,9 +713,7 @@ export interface SchemaWithExtendsConstructor<
713
713
  : IsInitPropsRequired<T> extends true ? ([] | [InitProps])
714
714
  : [InitProps?]
715
715
  ): SchemaInstance<T, P>;
716
- prototype: SchemaInstance<T, P> & {
717
- initialize(...args: [InitProps] extends [never] ? [] : InitProps extends readonly any[] ? InitProps : [InitProps]): void;
718
- };
716
+ prototype: SchemaInstance<T, P>;
719
717
  }
720
718
 
721
719
  /**
@@ -742,6 +740,9 @@ function makeAutoDefaultFactory(rawType: any): (() => any) | undefined {
742
740
  /**
743
741
  * Define a Schema class declaratively.
744
742
  *
743
+ * `initialize()` acts as the constructor: a class created with `.extend()`
744
+ * inherits the parent's unless it defines its own.
745
+ *
745
746
  * @example
746
747
  * import { schema, t } from '@colyseus/schema';
747
748
  *
@@ -910,9 +911,12 @@ export function schema<
910
911
  };
911
912
 
912
913
  const hasInitialize = typeof methods.initialize === "function";
914
+ // Like a constructor: the most-derived initialize() runs once, own or
915
+ // inherited from a schema() parent. A custom base's initialize() is not
916
+ // picked up — the constructor type only sees the fields map.
917
+ const initialize = methods.initialize ?? (inherits as any)._initialize;
913
918
 
914
- /** @codegen-ignore */
915
- const klass = Metadata.setFields<any>(class extends (inherits as any) {
919
+ const klass = class extends (inherits as any) {
916
920
  constructor(...args: any[]) {
917
921
  const props = args[0];
918
922
  if (props === undefined) {
@@ -926,14 +930,22 @@ export function schema<
926
930
  // `initialize()` owns the schema fields, so only parent props flow up.
927
931
  super(Object.assign(getDefaultValues(), hasInitialize ? getParentProps(props) : props));
928
932
  }
929
- // Only call initialize() on the exact target class, not parents.
930
- if (hasInitialize && new.target === klass) {
931
- methods.initialize.apply(this, args);
933
+ // Only on the exact target class parents' constructors skip it.
934
+ if (initialize && new.target === klass) {
935
+ initialize.apply(this, args);
932
936
  }
933
937
  }
934
- }, fields) as unknown as SchemaWithExtendsConstructor<T, ExtractInitProps<T>, P>;
938
+ } as unknown as SchemaWithExtendsConstructor<T, ExtractInitProps<T>, P>;
939
+
940
+ // named before setFields so a duplicate-field error can say which class
941
+ if (name) {
942
+ Object.defineProperty(klass, "name", { value: name });
943
+ }
944
+ /** @codegen-ignore */
945
+ Metadata.setFields<any>(klass, fields);
935
946
 
936
947
  (klass as any)._getDefaultValues = getDefaultValues;
948
+ (klass as any)._initialize = initialize;
937
949
 
938
950
  Object.assign(klass.prototype, methods);
939
951
 
@@ -970,10 +982,6 @@ export function schema<
970
982
  }
971
983
  }
972
984
 
973
- if (name) {
974
- Object.defineProperty(klass, "name", { value: name });
975
- }
976
-
977
985
  (klass as any).extend = <T2 extends FieldsAndMethods = FieldsAndMethods>(
978
986
  childFields: T2,
979
987
  childName?: string,