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