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