@colyseus/schema 3.0.0-alpha.25 → 3.0.0-alpha.27

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.
@@ -329,7 +329,7 @@ class ChangeTree {
329
329
  // MapSchema / ArraySchema, etc.
330
330
  this.ref.forEach((value, key) => {
331
331
  if (Metadata.isValidInstance(value)) {
332
- callback(value[$changes], this.ref[$changes].indexes[key]);
332
+ callback(value[$changes], this.ref[$changes].indexes[key] ?? key);
333
333
  }
334
334
  });
335
335
  }
@@ -357,8 +357,9 @@ class ChangeTree {
357
357
  // TODO: are DELETE operations being encoded as ADD here ??
358
358
  //
359
359
  if (isFiltered) {
360
- this.allFilteredChanges.set(index, exports.OPERATION.ADD);
361
360
  this.root?.filteredChanges.set(this, this.filteredChanges);
361
+ this.allFilteredChanges.set(index, exports.OPERATION.ADD);
362
+ this.root?.allFilteredChanges.set(this, this.allFilteredChanges);
362
363
  }
363
364
  else {
364
365
  this.allChanges.set(index, exports.OPERATION.ADD);
@@ -1405,6 +1406,7 @@ const decodeSchemaOperation = function (decoder, bytes, it, ref, allChanges) {
1405
1406
  // skip early if field is not defined
1406
1407
  const field = metadata[index];
1407
1408
  if (field === undefined) {
1409
+ console.warn("@colyseus/schema: field not defined at", { index, ref: ref.constructor.name, metadata });
1408
1410
  return DEFINITION_MISMATCH;
1409
1411
  }
1410
1412
  const { value, previousValue } = decodeValue(decoder, operation, ref, index, metadata[field].type, bytes, it, allChanges);
@@ -2427,13 +2429,13 @@ class TypeContext {
2427
2429
  getTypeId(klass) {
2428
2430
  return this.schemas.get(klass);
2429
2431
  }
2430
- discoverTypes(klass) {
2432
+ discoverTypes(klass, parentFieldViewTag) {
2431
2433
  if (!this.add(klass)) {
2432
2434
  return;
2433
2435
  }
2434
2436
  // add classes inherited from this base class
2435
2437
  TypeContext.inheritedTypes.get(klass)?.forEach((child) => {
2436
- this.discoverTypes(child);
2438
+ this.discoverTypes(child, parentFieldViewTag);
2437
2439
  });
2438
2440
  // skip if no fields are defined for this class.
2439
2441
  if (klass[Symbol.metadata] === undefined) {
@@ -2446,7 +2448,15 @@ class TypeContext {
2446
2448
  this.hasFilters = true;
2447
2449
  }
2448
2450
  for (const field in metadata) {
2451
+ //
2452
+ // Modify the field's metadata to include the parent field's view tag
2453
+ //
2454
+ if (parentFieldViewTag !== undefined &&
2455
+ metadata[field].tag === undefined) {
2456
+ metadata[field].tag = parentFieldViewTag;
2457
+ }
2449
2458
  const fieldType = metadata[field].type;
2459
+ const viewTag = metadata[field].tag;
2450
2460
  if (typeof (fieldType) === "string") {
2451
2461
  continue;
2452
2462
  }
@@ -2455,10 +2465,10 @@ class TypeContext {
2455
2465
  if (type === "string") {
2456
2466
  continue;
2457
2467
  }
2458
- this.discoverTypes(type);
2468
+ this.discoverTypes(type, viewTag);
2459
2469
  }
2460
2470
  else if (typeof (fieldType) === "function") {
2461
- this.discoverTypes(fieldType);
2471
+ this.discoverTypes(fieldType, viewTag);
2462
2472
  }
2463
2473
  else {
2464
2474
  const type = Object.values(fieldType)[0];
@@ -2466,7 +2476,7 @@ class TypeContext {
2466
2476
  if (typeof (type) === "string") {
2467
2477
  continue;
2468
2478
  }
2469
- this.discoverTypes(type);
2479
+ this.discoverTypes(type, viewTag);
2470
2480
  }
2471
2481
  }
2472
2482
  }
@@ -3025,13 +3035,13 @@ class Schema {
3025
3035
  }
3026
3036
  return output;
3027
3037
  }
3028
- static debugChangesDeep(ref) {
3038
+ static debugChangesDeep(ref, changeSetName = "changes") {
3029
3039
  let output = "";
3030
3040
  const rootChangeTree = ref[$changes];
3031
3041
  const changeTrees = new Map();
3032
3042
  let totalInstances = 0;
3033
3043
  let totalOperations = 0;
3034
- for (const [changeTree, changes] of (rootChangeTree.root.changes.entries())) {
3044
+ for (const [changeTree, changes] of (rootChangeTree.root[changeSetName].entries())) {
3035
3045
  let includeChangeTree = false;
3036
3046
  let parentChangeTrees = [];
3037
3047
  let parentChangeTree = changeTree.parent?.[$changes];
@@ -3434,26 +3444,26 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
3434
3444
 
3435
3445
  class Encoder {
3436
3446
  static { this.BUFFER_SIZE = 8 * 1024; } // 8KB
3437
- constructor(root) {
3447
+ constructor(state) {
3438
3448
  this.sharedBuffer = Buffer.allocUnsafeSlow(Encoder.BUFFER_SIZE);
3449
+ this.root = new Root();
3439
3450
  //
3440
3451
  // TODO: cache and restore "Context" based on root schema
3441
3452
  // (to avoid creating a new context for every new room)
3442
3453
  //
3443
- this.context = new TypeContext(root.constructor);
3444
- this.setRoot(root);
3454
+ this.context = new TypeContext(state.constructor);
3455
+ this.setState(state);
3445
3456
  // console.log(">>>>>>>>>>>>>>>> Encoder types");
3446
3457
  // this.context.schemas.forEach((id, schema) => {
3447
3458
  // console.log("type:", id, schema.name, Object.keys(schema[Symbol.metadata]));
3448
3459
  // });
3449
3460
  }
3450
- setRoot(state) {
3451
- this.root = new Root();
3461
+ setState(state) {
3452
3462
  this.state = state;
3453
- state[$changes].setRoot(this.root);
3463
+ this.state[$changes].setRoot(this.root);
3454
3464
  }
3455
- encode(it = { offset: 0 }, view, buffer = this.sharedBuffer, changeTrees = this.root.changes, isEncodeAll = this.root.allChanges === changeTrees) {
3456
- const initialOffset = it.offset; // cache current offset in case we need to resize the buffer
3465
+ encode(it = { offset: 0 }, view, buffer = this.sharedBuffer, changeTrees = this.root.changes, isEncodeAll = this.root.allChanges === changeTrees, initialOffset = it.offset // cache current offset in case we need to resize the buffer
3466
+ ) {
3457
3467
  const hasView = (view !== undefined);
3458
3468
  const rootChangeTree = this.state[$changes];
3459
3469
  const changeTreesIterator = changeTrees.entries();
@@ -3492,7 +3502,6 @@ class Encoder {
3492
3502
  // TODO: avoid checking if no view tags were defined
3493
3503
  //
3494
3504
  if (filter && !filter(ref, fieldIndex, view)) {
3495
- // console.log("SKIP FIELD:", { ref: changeTree.ref.constructor.name, fieldIndex, })
3496
3505
  // console.log("ADD AS INVISIBLE:", fieldIndex, changeTree.ref.constructor.name)
3497
3506
  // view?.invisible.add(changeTree);
3498
3507
  continue;
@@ -3570,8 +3579,6 @@ class Encoder {
3570
3579
  }
3571
3580
  encodeView(view, sharedOffset, it, bytes = this.sharedBuffer) {
3572
3581
  const viewOffset = it.offset;
3573
- // try to encode "filtered" changes
3574
- this.encode(it, view, bytes, this.root.filteredChanges);
3575
3582
  // encode visibility changes (add/remove for this view)
3576
3583
  const viewChangesIterator = view.changes.entries();
3577
3584
  for (const [changeTree, changes] of viewChangesIterator) {
@@ -3598,6 +3605,8 @@ class Encoder {
3598
3605
  //
3599
3606
  // clear "view" changes after encoding
3600
3607
  view.changes.clear();
3608
+ // try to encode "filtered" changes
3609
+ this.encode(it, view, bytes, this.root.filteredChanges, false, viewOffset);
3601
3610
  return Buffer.concat([
3602
3611
  bytes.subarray(0, sharedOffset),
3603
3612
  bytes.subarray(viewOffset, it.offset)
@@ -3770,14 +3779,14 @@ class ReferenceTracker {
3770
3779
  class Decoder {
3771
3780
  constructor(root, context) {
3772
3781
  this.currentRefId = 0;
3773
- this.setRoot(root);
3782
+ this.setState(root);
3774
3783
  this.context = context || new TypeContext(root.constructor);
3775
3784
  // console.log(">>>>>>>>>>>>>>>> Decoder types");
3776
3785
  // this.context.schemas.forEach((id, schema) => {
3777
3786
  // console.log("type:", id, schema.name, Object.keys(schema[Symbol.metadata]));
3778
3787
  // });
3779
3788
  }
3780
- setRoot(root) {
3789
+ setState(root) {
3781
3790
  this.state = root;
3782
3791
  this.root = new ReferenceTracker();
3783
3792
  this.root.addRef(0, root);
@@ -3906,9 +3915,7 @@ class Reflection extends Schema {
3906
3915
  this.types = new ArraySchema();
3907
3916
  }
3908
3917
  static encode(instance, context, it = { offset: 0 }) {
3909
- if (!context) {
3910
- context = new TypeContext(instance.constructor);
3911
- }
3918
+ context ??= new TypeContext(instance.constructor);
3912
3919
  const reflection = new Reflection();
3913
3920
  const encoder = new Encoder(reflection);
3914
3921
  const buildType = (currentType, metadata) => {
@@ -4032,7 +4039,8 @@ __decorate([
4032
4039
  function getDecoderStateCallbacks(decoder) {
4033
4040
  const $root = decoder.root;
4034
4041
  const callbacks = $root.callbacks;
4035
- let isTriggeringOnAdd = false;
4042
+ const onAddCalls = new WeakMap();
4043
+ let currentOnAddCallback;
4036
4044
  decoder.triggerChanges = function (allChanges) {
4037
4045
  const uniqueRefIds = new Set();
4038
4046
  for (let i = 0, l = allChanges.length; i < l; i++) {
@@ -4095,7 +4103,6 @@ function getDecoderStateCallbacks(decoder) {
4095
4103
  }
4096
4104
  }
4097
4105
  // Handle DELETE_AND_ADD operations
4098
- // FIXME: should we set "isTriggeringOnAdd" here?
4099
4106
  if ((change.op & exports.OPERATION.ADD) === exports.OPERATION.ADD) {
4100
4107
  const addCallbacks = $callbacks[exports.OPERATION.ADD];
4101
4108
  for (let i = addCallbacks?.length - 1; i >= 0; i--) {
@@ -4105,12 +4112,10 @@ function getDecoderStateCallbacks(decoder) {
4105
4112
  }
4106
4113
  else if ((change.op & exports.OPERATION.ADD) === exports.OPERATION.ADD && change.previousValue === undefined) {
4107
4114
  // triger onAdd
4108
- isTriggeringOnAdd = true;
4109
4115
  const addCallbacks = $callbacks[exports.OPERATION.ADD];
4110
4116
  for (let i = addCallbacks?.length - 1; i >= 0; i--) {
4111
4117
  addCallbacks[i](change.value, change.dynamicIndex ?? change.field);
4112
4118
  }
4113
- isTriggeringOnAdd = false;
4114
4119
  }
4115
4120
  // trigger onChange
4116
4121
  if (change.value !== change.previousValue) {
@@ -4132,7 +4137,7 @@ function getDecoderStateCallbacks(decoder) {
4132
4137
  // immediate trigger
4133
4138
  if (immediate &&
4134
4139
  context.instance[prop] !== undefined &&
4135
- !isTriggeringOnAdd // FIXME: This is a workaround (https://github.com/colyseus/schema/issues/147)
4140
+ !onAddCalls.has(callback) // Workaround for https://github.com/colyseus/schema/issues/147
4136
4141
  ) {
4137
4142
  callback(context.instance[prop], undefined);
4138
4143
  }
@@ -4158,10 +4163,11 @@ function getDecoderStateCallbacks(decoder) {
4158
4163
  onChange: function onChange(callback) {
4159
4164
  return $root.addCallback($root.refIds.get(context.instance), exports.OPERATION.REPLACE, callback);
4160
4165
  },
4166
+ //
4167
+ // TODO: refactor `bindTo()` implementation.
4168
+ // There is room for improvement.
4169
+ //
4161
4170
  bindTo: function bindTo(targetObject, properties) {
4162
- //
4163
- // TODO: refactor this implementation. There is room for improvement here.
4164
- //
4165
4171
  if (!properties) {
4166
4172
  properties = Object.keys(metadata);
4167
4173
  }
@@ -4188,7 +4194,8 @@ function getDecoderStateCallbacks(decoder) {
4188
4194
  }
4189
4195
  });
4190
4196
  return getProxy(metadata[prop].type, {
4191
- instance,
4197
+ // make sure refId is available, otherwise need to wait for the instance to be available.
4198
+ instance: ($root.refIds.get(instance) && instance),
4192
4199
  parentInstance: context.instance,
4193
4200
  onInstanceAvailable,
4194
4201
  });
@@ -4212,7 +4219,12 @@ function getDecoderStateCallbacks(decoder) {
4212
4219
  if (immediate) {
4213
4220
  ref.forEach((v, k) => callback(v, k));
4214
4221
  }
4215
- return $root.addCallback($root.refIds.get(ref), exports.OPERATION.ADD, callback);
4222
+ return $root.addCallback($root.refIds.get(ref), exports.OPERATION.ADD, (value, key) => {
4223
+ onAddCalls.set(callback, true);
4224
+ currentOnAddCallback = callback;
4225
+ callback(value, key);
4226
+ onAddCalls.delete(callback);
4227
+ });
4216
4228
  };
4217
4229
  const onRemove = function (ref, callback) {
4218
4230
  return $root.addCallback($root.refIds.get(ref), exports.OPERATION.DELETE, callback);
@@ -4223,19 +4235,17 @@ function getDecoderStateCallbacks(decoder) {
4223
4235
  // https://github.com/colyseus/schema/issues/147
4224
4236
  // If parent instance has "onAdd" registered, avoid triggering immediate callback.
4225
4237
  //
4226
- // FIXME: "isTriggeringOnAdd" is a workaround. We should find a better way to handle this.
4227
- //
4228
- if (context.onInstanceAvailable) {
4238
+ if (context.instance) {
4239
+ return onAdd(context.instance, callback, immediate && !onAddCalls.has(currentOnAddCallback));
4240
+ }
4241
+ else if (context.onInstanceAvailable) {
4229
4242
  // collection instance not received yet
4230
4243
  let detachCallback = () => { };
4231
4244
  context.onInstanceAvailable((ref, existing) => {
4232
- detachCallback = onAdd(ref, callback, immediate && existing && !isTriggeringOnAdd);
4245
+ detachCallback = onAdd(ref, callback, immediate && existing && !onAddCalls.has(currentOnAddCallback));
4233
4246
  });
4234
4247
  return () => detachCallback();
4235
4248
  }
4236
- else if (context.instance) {
4237
- return onAdd(context.instance, callback, immediate && !isTriggeringOnAdd);
4238
- }
4239
4249
  },
4240
4250
  onRemove: function (callback) {
4241
4251
  if (context.onInstanceAvailable) {
@@ -4290,26 +4300,21 @@ class StateView {
4290
4300
  this.changes = new Map();
4291
4301
  }
4292
4302
  // TODO: allow to set multiple tags at once
4293
- add(obj, tag = DEFAULT_VIEW_TAG) {
4303
+ add(obj, tag = DEFAULT_VIEW_TAG, checkIncludeParent = true) {
4294
4304
  if (!obj[$changes]) {
4295
4305
  console.warn("StateView#add(), invalid object:", obj);
4296
4306
  return this;
4297
4307
  }
4298
4308
  // FIXME: ArraySchema/MapSchema does not have metadata
4299
4309
  const metadata = obj.constructor[Symbol.metadata];
4300
- let changeTree = obj[$changes];
4310
+ const changeTree = obj[$changes];
4301
4311
  this.items.add(changeTree);
4302
- // Add children of this ChangeTree to this view
4303
- changeTree.forEachChild((change, index) => {
4304
- // Do not ADD children that don't have the same tag
4305
- if (metadata && metadata[metadata[index]].tag !== tag) {
4306
- return;
4307
- }
4308
- this.add(change.ref, tag);
4309
- });
4310
- // add parent ChangeTree's, if they are invisible to this view
4311
- // TODO: REFACTOR addParent()
4312
- this.addParent(changeTree, tag);
4312
+ // add parent ChangeTree's
4313
+ // - if it was invisible to this view
4314
+ // - if it were previously filtered out
4315
+ if (checkIncludeParent && changeTree.parent) {
4316
+ this.addParent(changeTree.parent[$changes], changeTree.parentIndex, tag);
4317
+ }
4313
4318
  //
4314
4319
  // TODO: when adding an item of a MapSchema, the changes may not
4315
4320
  // be set (only the parent's changes are set)
@@ -4341,73 +4346,63 @@ class StateView {
4341
4346
  });
4342
4347
  }
4343
4348
  else {
4344
- // console.log("DEFAULT TAG", changeTree.allChanges);
4345
- // // add default tag properties
4346
- // metadata?.[-3]?.[DEFAULT_VIEW_TAG]?.forEach((index) => {
4347
- // if (changeTree.getChange(index) !== OPERATION.DELETE) {
4348
- // changes.set(index, OPERATION.ADD);
4349
- // }
4350
- // });
4351
- const allChangesSet = (changeTree.isFiltered || changeTree.isPartiallyFiltered)
4349
+ const isInvisible = this.invisible.has(changeTree);
4350
+ const changeSet = (changeTree.isFiltered || changeTree.isPartiallyFiltered)
4352
4351
  ? changeTree.allFilteredChanges
4353
4352
  : changeTree.allChanges;
4354
- const it = allChangesSet.keys();
4355
- const isInvisible = this.invisible.has(changeTree);
4356
- for (const index of it) {
4357
- if ((isInvisible || metadata?.[metadata?.[index]].tag === tag) &&
4358
- changeTree.getChange(index) !== exports.OPERATION.DELETE) {
4359
- changes.set(index, exports.OPERATION.ADD);
4353
+ changeSet.forEach((op, index) => {
4354
+ const tagAtIndex = metadata?.[metadata?.[index]].tag;
4355
+ if ((isInvisible || // if "invisible", include all
4356
+ tagAtIndex === undefined || // "all change" with no tag
4357
+ tagAtIndex === tag // tagged property
4358
+ ) &&
4359
+ op !== exports.OPERATION.DELETE) {
4360
+ changes.set(index, op);
4360
4361
  }
4361
- }
4362
- }
4363
- // TODO: avoid unnecessary iteration here
4364
- while (changeTree.parent &&
4365
- (changeTree = changeTree.parent[$changes]) &&
4366
- (changeTree.isFiltered || changeTree.isPartiallyFiltered)) {
4367
- this.items.add(changeTree);
4362
+ });
4368
4363
  }
4364
+ // Add children of this ChangeTree to this view
4365
+ changeTree.forEachChild((change, index) => {
4366
+ // Do not ADD children that don't have the same tag
4367
+ if (metadata && metadata[metadata[index]].tag !== tag) {
4368
+ return;
4369
+ }
4370
+ this.add(change.ref, tag, false);
4371
+ });
4369
4372
  return this;
4370
4373
  }
4371
- addParent(changeTree, tag) {
4372
- const parentRef = changeTree.parent;
4373
- if (!parentRef) {
4374
- return;
4374
+ addParent(changeTree, parentIndex, tag) {
4375
+ // view must have all "changeTree" parent tree
4376
+ this.items.add(changeTree);
4377
+ // add parent's parent
4378
+ const parentChangeTree = changeTree.parent?.[$changes];
4379
+ if (parentChangeTree && (parentChangeTree.isFiltered || parentChangeTree.isPartiallyFiltered)) {
4380
+ this.addParent(parentChangeTree, changeTree.parentIndex, tag);
4375
4381
  }
4376
- const parentChangeTree = parentRef[$changes];
4377
- const parentIndex = changeTree.parentIndex;
4378
- if (!this.invisible.has(parentChangeTree)) {
4379
- // parent is already available, no need to add it!
4382
+ // parent is already available, no need to add it!
4383
+ if (!this.invisible.has(changeTree)) {
4380
4384
  return;
4381
4385
  }
4382
- this.addParent(parentChangeTree, tag);
4383
4386
  // add parent's tag properties
4384
- if (parentChangeTree.getChange(parentIndex) !== exports.OPERATION.DELETE) {
4385
- let parentChanges = this.changes.get(parentChangeTree);
4386
- if (parentChanges === undefined) {
4387
- parentChanges = new Map();
4388
- this.changes.set(parentChangeTree, parentChanges);
4387
+ if (changeTree.getChange(parentIndex) !== exports.OPERATION.DELETE) {
4388
+ let changes = this.changes.get(changeTree);
4389
+ if (changes === undefined) {
4390
+ changes = new Map();
4391
+ this.changes.set(changeTree, changes);
4389
4392
  }
4390
- // console.log("add parent change", {
4391
- // parentIndex,
4392
- // parentChanges,
4393
- // parentChange: (
4394
- // parentChangeTree.getChange(parentIndex) &&
4395
- // OPERATION[parentChangeTree.getChange(parentIndex)]
4396
- // ),
4397
- // })
4398
4393
  if (!this.tags) {
4399
4394
  this.tags = new WeakMap();
4400
4395
  }
4401
4396
  let tags;
4402
- if (!this.tags.has(parentChangeTree)) {
4397
+ if (!this.tags.has(changeTree)) {
4403
4398
  tags = new Set();
4404
- this.tags.set(parentChangeTree, tags);
4399
+ this.tags.set(changeTree, tags);
4405
4400
  }
4406
4401
  else {
4407
- tags = this.tags.get(parentChangeTree);
4402
+ tags = this.tags.get(changeTree);
4408
4403
  }
4409
4404
  tags.add(tag);
4410
- parentChanges.set(parentIndex, exports.OPERATION.ADD);
4405
+ changes.set(parentIndex, exports.OPERATION.ADD);
4411
4406
  }
4412
4407
  }
4413
4408
  remove(obj, tag = DEFAULT_VIEW_TAG) {