@colyseus/schema 3.0.20 → 3.0.22

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.
@@ -600,6 +600,7 @@ class TypeContext {
600
600
  * Keeps track of which classes extends which. (parent -> children)
601
601
  */
602
602
  static { this.inheritedTypes = new Map(); }
603
+ static { this.cachedContexts = new Map(); }
603
604
  static register(target) {
604
605
  const parent = Object.getPrototypeOf(target);
605
606
  if (parent !== Schema) {
@@ -611,17 +612,20 @@ class TypeContext {
611
612
  inherits.add(target);
612
613
  }
613
614
  }
615
+ static cache(rootClass) {
616
+ let context = TypeContext.cachedContexts.get(rootClass);
617
+ if (!context) {
618
+ context = new TypeContext(rootClass);
619
+ TypeContext.cachedContexts.set(rootClass, context);
620
+ }
621
+ return context;
622
+ }
614
623
  constructor(rootClass) {
615
624
  this.types = {};
616
625
  this.schemas = new Map();
617
626
  this.hasFilters = false;
618
627
  this.parentFiltered = {};
619
628
  if (rootClass) {
620
- //
621
- // TODO:
622
- // cache "discoverTypes" results for each rootClass
623
- // to avoid re-discovering types for each new context/room
624
- //
625
629
  this.discoverTypes(rootClass);
626
630
  }
627
631
  }
@@ -3767,10 +3771,12 @@ class Encoder {
3767
3771
  constructor(state) {
3768
3772
  this.sharedBuffer = Buffer.allocUnsafe(Encoder.BUFFER_SIZE);
3769
3773
  //
3770
- // TODO: cache and restore "Context" based on root schema
3771
- // (to avoid creating a new context for every new room)
3774
+ // Use .cache() here to avoid re-creating a new context for every new room instance.
3775
+ //
3776
+ // We may need to make this optional in case of dynamically created
3777
+ // schemas - which would lead to memory leaks
3772
3778
  //
3773
- this.context = new TypeContext(state.constructor);
3779
+ this.context = TypeContext.cache(state.constructor);
3774
3780
  this.root = new Root(this.context);
3775
3781
  this.setState(state);
3776
3782
  // console.log(">>>>>>>>>>>>>>>> Encoder types");
@@ -3917,13 +3923,14 @@ class Encoder {
3917
3923
  const changeTree = this.root.changeTrees[refId];
3918
3924
  if (changeTree === undefined) {
3919
3925
  // detached instance, remove from view and skip.
3926
+ // console.log("detached instance, remove from view and skip.", refId);
3920
3927
  view.changes.delete(refId);
3921
3928
  continue;
3922
3929
  }
3923
3930
  const keys = Object.keys(changes);
3924
3931
  if (keys.length === 0) {
3925
3932
  // FIXME: avoid having empty changes if no changes were made
3926
- // console.log("changes.size === 0, skip", changeTree.ref.constructor.name);
3933
+ // console.log("changes.size === 0, skip", refId, changeTree.ref.constructor.name);
3927
3934
  continue;
3928
3935
  }
3929
3936
  const ref = changeTree.ref;
@@ -4069,7 +4076,7 @@ class ReferenceTracker {
4069
4076
  //
4070
4077
  // Ensure child schema instances have their references removed as well.
4071
4078
  //
4072
- if (Metadata.isValidInstance(ref)) {
4079
+ if (ref.constructor[Symbol.metadata] !== undefined) {
4073
4080
  const metadata = ref.constructor[Symbol.metadata];
4074
4081
  for (const index in metadata) {
4075
4082
  const field = metadata[index].name;
@@ -4080,7 +4087,7 @@ class ReferenceTracker {
4080
4087
  }
4081
4088
  }
4082
4089
  else {
4083
- if (typeof (Object.values(ref[$childType])[0]) === "function") {
4090
+ if (typeof (ref[$childType]) === "function") {
4084
4091
  Array.from(ref.values())
4085
4092
  .forEach((child) => {
4086
4093
  const childRefId = this.refIds.get(child);
@@ -4275,50 +4282,28 @@ class Reflection extends Schema {
4275
4282
  if (rootType > 0) {
4276
4283
  reflection.rootType = rootType;
4277
4284
  }
4278
- const buildType = (currentType, metadata) => {
4279
- for (const fieldIndex in metadata) {
4280
- const index = Number(fieldIndex);
4281
- const fieldName = metadata[index].name;
4282
- // skip fields from parent classes
4283
- if (!Object.prototype.hasOwnProperty.call(metadata, fieldName)) {
4284
- continue;
4285
- }
4286
- const field = new ReflectionField();
4287
- field.name = fieldName;
4288
- let fieldType;
4289
- const type = metadata[index].type;
4290
- if (typeof (type) === "string") {
4291
- fieldType = type;
4285
+ const includedTypeIds = new Set();
4286
+ const pendingReflectionTypes = {};
4287
+ // add type to reflection in a way that respects inheritance
4288
+ // (parent types should be added before their children)
4289
+ const addType = (type) => {
4290
+ if (type.extendsId === undefined || includedTypeIds.has(type.extendsId)) {
4291
+ includedTypeIds.add(type.id);
4292
+ reflection.types.push(type);
4293
+ const deps = pendingReflectionTypes[type.id];
4294
+ if (deps !== undefined) {
4295
+ delete pendingReflectionTypes[type.id];
4296
+ deps.forEach((childType) => addType(childType));
4292
4297
  }
4293
- else {
4294
- let childTypeSchema;
4295
- //
4296
- // TODO: refactor below.
4297
- //
4298
- if (Schema.is(type)) {
4299
- fieldType = "ref";
4300
- childTypeSchema = type;
4301
- }
4302
- else {
4303
- fieldType = Object.keys(type)[0];
4304
- if (typeof (type[fieldType]) === "string") {
4305
- fieldType += ":" + type[fieldType]; // array:string
4306
- }
4307
- else {
4308
- childTypeSchema = type[fieldType];
4309
- }
4310
- }
4311
- field.referencedType = (childTypeSchema)
4312
- ? context.getTypeId(childTypeSchema)
4313
- : -1;
4298
+ }
4299
+ else {
4300
+ if (pendingReflectionTypes[type.extendsId] === undefined) {
4301
+ pendingReflectionTypes[type.extendsId] = [];
4314
4302
  }
4315
- field.type = fieldType;
4316
- currentType.fields.push(field);
4303
+ pendingReflectionTypes[type.extendsId].push(type);
4317
4304
  }
4318
- reflection.types.push(currentType);
4319
4305
  };
4320
- for (let typeid in context.types) {
4321
- const klass = context.types[typeid];
4306
+ context.schemas.forEach((typeid, klass) => {
4322
4307
  const type = new ReflectionType();
4323
4308
  type.id = Number(typeid);
4324
4309
  // support inheritance
@@ -4326,7 +4311,57 @@ class Reflection extends Schema {
4326
4311
  if (inheritFrom !== Schema) {
4327
4312
  type.extendsId = context.schemas.get(inheritFrom);
4328
4313
  }
4329
- buildType(type, klass[Symbol.metadata]);
4314
+ const metadata = klass[Symbol.metadata];
4315
+ //
4316
+ // FIXME: this is a workaround for inherited types without additional fields
4317
+ // if metadata is the same reference as the parent class - it means the class has no own metadata
4318
+ //
4319
+ if (metadata !== inheritFrom[Symbol.metadata]) {
4320
+ for (const fieldIndex in metadata) {
4321
+ const index = Number(fieldIndex);
4322
+ const fieldName = metadata[index].name;
4323
+ // skip fields from parent classes
4324
+ if (!Object.prototype.hasOwnProperty.call(metadata, fieldName)) {
4325
+ continue;
4326
+ }
4327
+ const reflectionField = new ReflectionField();
4328
+ reflectionField.name = fieldName;
4329
+ let fieldType;
4330
+ const field = metadata[index];
4331
+ if (typeof (field.type) === "string") {
4332
+ fieldType = field.type;
4333
+ }
4334
+ else {
4335
+ let childTypeSchema;
4336
+ //
4337
+ // TODO: refactor below.
4338
+ //
4339
+ if (Schema.is(field.type)) {
4340
+ fieldType = "ref";
4341
+ childTypeSchema = field.type;
4342
+ }
4343
+ else {
4344
+ fieldType = Object.keys(field.type)[0];
4345
+ if (typeof (field.type[fieldType]) === "string") {
4346
+ fieldType += ":" + field.type[fieldType]; // array:string
4347
+ }
4348
+ else {
4349
+ childTypeSchema = field.type[fieldType];
4350
+ }
4351
+ }
4352
+ reflectionField.referencedType = (childTypeSchema)
4353
+ ? context.getTypeId(childTypeSchema)
4354
+ : -1;
4355
+ }
4356
+ reflectionField.type = fieldType;
4357
+ type.fields.push(reflectionField);
4358
+ }
4359
+ }
4360
+ addType(type);
4361
+ });
4362
+ // in case there are types that were not added due to inheritance
4363
+ for (const typeid in pendingReflectionTypes) {
4364
+ pendingReflectionTypes[typeid].forEach((type) => reflection.types.push(type));
4330
4365
  }
4331
4366
  const buf = reflectionEncoder.encodeAll(it);
4332
4367
  return Buffer.from(buf, 0, it.offset);
@@ -4695,7 +4730,7 @@ class StateView {
4695
4730
  }
4696
4731
  // TODO: allow to set multiple tags at once
4697
4732
  add(obj, tag = DEFAULT_VIEW_TAG, checkIncludeParent = true) {
4698
- if (!obj[$changes]) {
4733
+ if (!obj?.[$changes]) {
4699
4734
  console.warn("StateView#add(), invalid object:", obj);
4700
4735
  return this;
4701
4736
  }
@@ -4817,7 +4852,7 @@ class StateView {
4817
4852
  }
4818
4853
  this.items.delete(changeTree);
4819
4854
  const ref = changeTree.ref;
4820
- const metadata = ref.constructor[Symbol.metadata];
4855
+ const metadata = ref.constructor[Symbol.metadata]; // ArraySchema/MapSchema do not have metadata
4821
4856
  let changes = this.changes.get(changeTree.refId);
4822
4857
  if (changes === undefined) {
4823
4858
  changes = {};
@@ -4838,12 +4873,12 @@ class StateView {
4838
4873
  }
4839
4874
  else {
4840
4875
  // delete all "tagged" properties.
4841
- metadata[$viewFieldIndexes].forEach((index) => changes[index] = exports.OPERATION.DELETE);
4876
+ metadata?.[$viewFieldIndexes].forEach((index) => changes[index] = exports.OPERATION.DELETE);
4842
4877
  }
4843
4878
  }
4844
4879
  else {
4845
4880
  // delete only tagged properties
4846
- metadata[$fieldIndexesByViewTag][tag].forEach((index) => changes[index] = exports.OPERATION.DELETE);
4881
+ metadata?.[$fieldIndexesByViewTag][tag].forEach((index) => changes[index] = exports.OPERATION.DELETE);
4847
4882
  }
4848
4883
  // remove tag
4849
4884
  if (this.tags && this.tags.has(changeTree)) {