@colyseus/schema 1.1.0-alpha.2 → 2.0.3

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.
Files changed (52) hide show
  1. package/README.md +1 -4
  2. package/build/cjs/index.js +1197 -1001
  3. package/build/cjs/index.js.map +1 -1
  4. package/build/esm/index.mjs +353 -311
  5. package/build/esm/index.mjs.map +1 -1
  6. package/build/umd/index.js +374 -335
  7. package/lib/Reflection.js +23 -13
  8. package/lib/Reflection.js.map +1 -1
  9. package/lib/Schema.d.ts +26 -18
  10. package/lib/Schema.js +121 -165
  11. package/lib/Schema.js.map +1 -1
  12. package/lib/annotations.d.ts +26 -8
  13. package/lib/annotations.js +62 -14
  14. package/lib/annotations.js.map +1 -1
  15. package/lib/changes/ChangeTree.d.ts +4 -16
  16. package/lib/changes/ChangeTree.js +1 -72
  17. package/lib/changes/ChangeTree.js.map +1 -1
  18. package/lib/changes/ReferenceTracker.d.ts +14 -0
  19. package/lib/changes/ReferenceTracker.js +77 -0
  20. package/lib/changes/ReferenceTracker.js.map +1 -0
  21. package/lib/codegen/languages/csharp.js +39 -17
  22. package/lib/codegen/languages/csharp.js.map +1 -1
  23. package/lib/codegen/languages/ts.js +11 -2
  24. package/lib/codegen/languages/ts.js.map +1 -1
  25. package/lib/codegen/parser.js +3 -1
  26. package/lib/codegen/parser.js.map +1 -1
  27. package/lib/codegen/types.js +14 -1
  28. package/lib/codegen/types.js.map +1 -1
  29. package/lib/filters/index.d.ts +2 -2
  30. package/lib/filters/index.js.map +1 -1
  31. package/lib/index.d.ts +1 -1
  32. package/lib/index.js +6 -6
  33. package/lib/index.js.map +1 -1
  34. package/lib/types/ArraySchema.d.ts +8 -5
  35. package/lib/types/ArraySchema.js +22 -19
  36. package/lib/types/ArraySchema.js.map +1 -1
  37. package/lib/types/CollectionSchema.d.ts +8 -5
  38. package/lib/types/CollectionSchema.js +17 -11
  39. package/lib/types/CollectionSchema.js.map +1 -1
  40. package/lib/types/MapSchema.d.ts +8 -5
  41. package/lib/types/MapSchema.js +20 -11
  42. package/lib/types/MapSchema.js.map +1 -1
  43. package/lib/types/SetSchema.d.ts +8 -5
  44. package/lib/types/SetSchema.js +22 -14
  45. package/lib/types/SetSchema.js.map +1 -1
  46. package/lib/types/typeRegistry.d.ts +5 -0
  47. package/lib/types/typeRegistry.js +13 -0
  48. package/lib/types/typeRegistry.js.map +1 -0
  49. package/lib/types/utils.d.ts +9 -0
  50. package/lib/types/utils.js +50 -0
  51. package/lib/types/utils.js.map +1 -0
  52. package/package.json +3 -8
@@ -37,73 +37,6 @@ var OPERATION;
37
37
  // CLEAR = 10,
38
38
  // }
39
39
 
40
- //
41
- // Root holds all schema references by unique id
42
- //
43
- class Root {
44
- constructor() {
45
- //
46
- // Relation of refId => Schema structure
47
- // For direct access of structures during decoding time.
48
- //
49
- this.refs = new Map();
50
- this.refCounts = {};
51
- this.deletedRefs = new Set();
52
- this.nextUniqueId = 0;
53
- }
54
- getNextUniqueId() {
55
- return this.nextUniqueId++;
56
- }
57
- // for decoding
58
- addRef(refId, ref, incrementCount = true) {
59
- this.refs.set(refId, ref);
60
- if (incrementCount) {
61
- this.refCounts[refId] = (this.refCounts[refId] || 0) + 1;
62
- }
63
- }
64
- // for decoding
65
- removeRef(refId) {
66
- this.refCounts[refId] = this.refCounts[refId] - 1;
67
- this.deletedRefs.add(refId);
68
- }
69
- clearRefs() {
70
- this.refs.clear();
71
- this.deletedRefs.clear();
72
- this.refCounts = {};
73
- }
74
- // for decoding
75
- garbageCollectDeletedRefs() {
76
- this.deletedRefs.forEach((refId) => {
77
- if (this.refCounts[refId] <= 0) {
78
- const ref = this.refs.get(refId);
79
- //
80
- // Ensure child schema instances have their references removed as well.
81
- //
82
- if (ref instanceof Schema) {
83
- for (const fieldName in ref['_definition'].schema) {
84
- if (typeof (ref['_definition'].schema[fieldName]) !== "string" &&
85
- ref[fieldName] &&
86
- ref[fieldName]['$changes']) {
87
- this.removeRef(ref[fieldName]['$changes'].refId);
88
- }
89
- }
90
- }
91
- else {
92
- const definition = ref['$changes'].parent._definition;
93
- const type = definition.schema[definition.fieldsByIndex[ref['$changes'].parentIndex]];
94
- if (typeof (Object.values(type)[0]) === "function") {
95
- Array.from(ref.values())
96
- .forEach((child) => this.removeRef(child['$changes'].refId));
97
- }
98
- }
99
- this.refs.delete(refId);
100
- delete this.refCounts[refId];
101
- }
102
- });
103
- // clear deleted refs.
104
- this.deletedRefs.clear();
105
- }
106
- }
107
40
  class ChangeTree {
108
41
  constructor(ref, parent, root) {
109
42
  this.changed = false;
@@ -300,13 +233,48 @@ class ChangeTree {
300
233
  }
301
234
  }
302
235
 
303
- //
304
- // Notes:
305
- // -----
306
- //
307
- // - The tsconfig.json of @colyseus/schema uses ES2018.
308
- // - ES2019 introduces `flatMap` / `flat`, which is not currently relevant, and caused other issues.
309
- //
236
+ function addCallback($callbacks, op, callback, existing) {
237
+ // initialize list of callbacks
238
+ if (!$callbacks[op]) {
239
+ $callbacks[op] = [];
240
+ }
241
+ $callbacks[op].push(callback);
242
+ //
243
+ // Trigger callback for existing elements
244
+ // - OPERATION.ADD
245
+ // - OPERATION.REPLACE
246
+ //
247
+ existing?.forEach((item, key) => callback(item, key));
248
+ return () => spliceOne($callbacks[op], $callbacks[op].indexOf(callback));
249
+ }
250
+ function removeChildRefs(changes) {
251
+ const needRemoveRef = (typeof (this.$changes.getType()) !== "string");
252
+ this.$items.forEach((item, key) => {
253
+ changes.push({
254
+ refId: this.$changes.refId,
255
+ op: OPERATION.DELETE,
256
+ field: key,
257
+ value: undefined,
258
+ previousValue: item
259
+ });
260
+ if (needRemoveRef) {
261
+ this.$changes.root.removeRef(item['$changes'].refId);
262
+ }
263
+ });
264
+ }
265
+ function spliceOne(arr, index) {
266
+ // manually splice an array
267
+ if (index === -1 || index >= arr.length) {
268
+ return false;
269
+ }
270
+ const len = arr.length - 1;
271
+ for (let i = index; i < len; i++) {
272
+ arr[i] = arr[i + 1];
273
+ }
274
+ arr.length = len;
275
+ return true;
276
+ }
277
+
310
278
  const DEFAULT_SORT = (a, b) => {
311
279
  const A = a.toString();
312
280
  const B = b.toString();
@@ -373,8 +341,19 @@ class ArraySchema {
373
341
  this.$refId = 0;
374
342
  this.push.apply(this, items);
375
343
  }
344
+ onAdd(callback, triggerAll = true) {
345
+ return addCallback((this.$callbacks || (this.$callbacks = [])), OPERATION.ADD, callback, (triggerAll)
346
+ ? this.$items
347
+ : undefined);
348
+ }
349
+ onRemove(callback) { return addCallback(this.$callbacks || (this.$callbacks = []), OPERATION.DELETE, callback); }
350
+ onChange(callback) { return addCallback(this.$callbacks || (this.$callbacks = []), OPERATION.REPLACE, callback); }
376
351
  static is(type) {
377
- return Array.isArray(type);
352
+ return (
353
+ // type format: ["string"]
354
+ Array.isArray(type) ||
355
+ // type format: { array: "string" }
356
+ (type['array'] !== undefined));
378
357
  }
379
358
  set length(value) {
380
359
  if (value === 0) {
@@ -440,17 +419,19 @@ class ArraySchema {
440
419
  this.$indexes.delete(index);
441
420
  return this.$items.delete(index);
442
421
  }
443
- clear(isDecoding) {
422
+ clear(changes) {
444
423
  // discard previous operations.
445
424
  this.$changes.discard(true, true);
446
425
  this.$changes.indexes = {};
447
426
  // clear previous indexes
448
427
  this.$indexes.clear();
449
- // flag child items for garbage collection.
450
- if (isDecoding && typeof (this.$changes.getType()) !== "string") {
451
- this.$items.forEach((item) => {
452
- this.$changes.root.removeRef(item['$changes'].refId);
453
- });
428
+ //
429
+ // When decoding:
430
+ // - enqueue items for DELETE callback.
431
+ // - flag child items for garbage collection.
432
+ //
433
+ if (changes) {
434
+ removeChildRefs.call(this, changes);
454
435
  }
455
436
  // clear items
456
437
  this.$items.clear();
@@ -784,9 +765,6 @@ class ArraySchema {
784
765
  return cloned;
785
766
  }
786
767
  ;
787
- triggerAll() {
788
- Schema.prototype.triggerAll.apply(this);
789
- }
790
768
  }
791
769
 
792
770
  function getMapProxy(value) {
@@ -838,6 +816,13 @@ class MapSchema {
838
816
  }
839
817
  }
840
818
  }
819
+ onAdd(callback, triggerAll = true) {
820
+ return addCallback((this.$callbacks || (this.$callbacks = [])), OPERATION.ADD, callback, (triggerAll)
821
+ ? this.$items
822
+ : undefined);
823
+ }
824
+ onRemove(callback) { return addCallback(this.$callbacks || (this.$callbacks = []), OPERATION.DELETE, callback); }
825
+ onChange(callback) { return addCallback(this.$callbacks || (this.$callbacks = []), OPERATION.REPLACE, callback); }
841
826
  static is(type) {
842
827
  return type['map'] !== undefined;
843
828
  }
@@ -845,6 +830,9 @@ class MapSchema {
845
830
  [Symbol.iterator]() { return this.$items[Symbol.iterator](); }
846
831
  get [Symbol.toStringTag]() { return this.$items[Symbol.toStringTag]; }
847
832
  set(key, value) {
833
+ if (value === undefined || value === null) {
834
+ throw new Error(`MapSchema#set('${key}', ${value}): trying to set ${value} value on '${key}'.`);
835
+ }
848
836
  // get "index" for this value.
849
837
  const hasIndex = typeof (this.$changes.indexes[key]) !== "undefined";
850
838
  const index = (hasIndex)
@@ -888,17 +876,19 @@ class MapSchema {
888
876
  this.$changes.delete(key);
889
877
  return this.$items.delete(key);
890
878
  }
891
- clear(isDecoding) {
879
+ clear(changes) {
892
880
  // discard previous operations.
893
881
  this.$changes.discard(true, true);
894
882
  this.$changes.indexes = {};
895
883
  // clear previous indexes
896
884
  this.$indexes.clear();
897
- // flag child items for garbage collection.
898
- if (isDecoding && typeof (this.$changes.getType()) !== "string") {
899
- this.$items.forEach((item) => {
900
- this.$changes.root.removeRef(item['$changes'].refId);
901
- });
885
+ //
886
+ // When decoding:
887
+ // - enqueue items for DELETE callback.
888
+ // - flag child items for garbage collection.
889
+ //
890
+ if (changes) {
891
+ removeChildRefs.call(this, changes);
902
892
  }
903
893
  // clear items
904
894
  this.$items.clear();
@@ -970,9 +960,6 @@ class MapSchema {
970
960
  }
971
961
  return cloned;
972
962
  }
973
- triggerAll() {
974
- Schema.prototype.triggerAll.apply(this);
975
- }
976
963
  }
977
964
 
978
965
  const registeredTypes = {};
@@ -1011,6 +998,9 @@ class SchemaDefinition {
1011
998
  ? { array: type[0] }
1012
999
  : type;
1013
1000
  }
1001
+ hasField(field) {
1002
+ return this.indexes[field] !== undefined;
1003
+ }
1014
1004
  addFilter(field, cb) {
1015
1005
  if (!this.filters) {
1016
1006
  this.filters = {};
@@ -1064,20 +1054,40 @@ class Context {
1064
1054
  this.types[typeid] = schema;
1065
1055
  this.schemas.set(schema, typeid);
1066
1056
  }
1067
- static create(context = new Context) {
1057
+ static create(options = {}) {
1068
1058
  return function (definition) {
1069
- return type(definition, context);
1059
+ if (!options.context) {
1060
+ options.context = new Context();
1061
+ }
1062
+ return type(definition, options);
1070
1063
  };
1071
1064
  }
1072
1065
  }
1073
1066
  const globalContext = new Context();
1074
1067
  /**
1075
- * `@type()` decorator for proxies
1068
+ * [See documentation](https://docs.colyseus.io/state/schema/)
1069
+ *
1070
+ * Annotate a Schema property to be serializeable.
1071
+ * \@type()'d fields are automatically flagged as "dirty" for the next patch.
1072
+ *
1073
+ * @example Standard usage, with automatic change tracking.
1074
+ * ```
1075
+ * \@type("string") propertyName: string;
1076
+ * ```
1077
+ *
1078
+ * @example You can provide the "manual" option if you'd like to manually control your patches via .setDirty().
1079
+ * ```
1080
+ * \@type("string", { manual: true })
1081
+ * ```
1076
1082
  */
1077
- function type(type, context = globalContext) {
1083
+ function type(type, options = {}) {
1078
1084
  return function (target, field) {
1085
+ const context = options.context || globalContext;
1079
1086
  const constructor = target.constructor;
1080
1087
  constructor._context = context;
1088
+ if (!type) {
1089
+ throw new Error(`${constructor.name}: @type() reference provided for "${field}" is undefined. Make sure you don't have any circular dependencies.`);
1090
+ }
1081
1091
  /*
1082
1092
  * static schema
1083
1093
  */
@@ -1090,7 +1100,21 @@ function type(type, context = globalContext) {
1090
1100
  * skip if descriptor already exists for this field (`@deprecated()`)
1091
1101
  */
1092
1102
  if (definition.descriptors[field]) {
1093
- return;
1103
+ if (definition.deprecated[field]) {
1104
+ // do not create accessors for deprecated properties.
1105
+ return;
1106
+ }
1107
+ else {
1108
+ // trying to define same property multiple times across inheritance.
1109
+ // https://github.com/colyseus/colyseus-unity3d/issues/131#issuecomment-814308572
1110
+ try {
1111
+ throw new Error(`@colyseus/schema: Duplicate '${field}' definition on '${constructor.name}'.\nCheck @type() annotation`);
1112
+ }
1113
+ catch (e) {
1114
+ const definitionAtLine = e.stack.split("\n")[4].trim();
1115
+ throw new Error(`${e.message} ${definitionAtLine}`);
1116
+ }
1117
+ }
1094
1118
  }
1095
1119
  const isArray = ArraySchema.is(type);
1096
1120
  const isMap = !isArray && MapSchema.is(type);
@@ -1103,6 +1127,15 @@ function type(type, context = globalContext) {
1103
1127
  context.add(childType);
1104
1128
  }
1105
1129
  }
1130
+ if (options.manual) {
1131
+ // do not declare getter/setter descriptor
1132
+ definition.descriptors[field] = {
1133
+ enumerable: true,
1134
+ configurable: true,
1135
+ writable: true,
1136
+ };
1137
+ return;
1138
+ }
1106
1139
  const fieldCached = `_${field}`;
1107
1140
  definition.descriptors[fieldCached] = {
1108
1141
  enumerable: false,
@@ -1188,7 +1221,7 @@ function filterChildren(cb) {
1188
1221
  * `@deprecated()` flag a field as deprecated.
1189
1222
  * The previous `@type()` annotation should remain along with this one.
1190
1223
  */
1191
- function deprecated(throws = true, context = globalContext) {
1224
+ function deprecated(throws = true) {
1192
1225
  return function (target, field) {
1193
1226
  const constructor = target.constructor;
1194
1227
  const definition = constructor._definition;
@@ -1203,9 +1236,12 @@ function deprecated(throws = true, context = globalContext) {
1203
1236
  }
1204
1237
  };
1205
1238
  }
1206
- function defineTypes(target, fields, context = target._context || globalContext) {
1239
+ function defineTypes(target, fields, options = {}) {
1240
+ if (!options.context) {
1241
+ options.context = target._context || options.context || globalContext;
1242
+ }
1207
1243
  for (let field in fields) {
1208
- type(fields[field], context)(target.prototype, field);
1244
+ type(fields[field], options)(target.prototype, field);
1209
1245
  }
1210
1246
  return target;
1211
1247
  }
@@ -1742,6 +1778,13 @@ class CollectionSchema {
1742
1778
  initialValues.forEach((v) => this.add(v));
1743
1779
  }
1744
1780
  }
1781
+ onAdd(callback, triggerAll = true) {
1782
+ return addCallback((this.$callbacks || (this.$callbacks = [])), OPERATION.ADD, callback, (triggerAll)
1783
+ ? this.$items
1784
+ : undefined);
1785
+ }
1786
+ onRemove(callback) { return addCallback(this.$callbacks || (this.$callbacks = []), OPERATION.DELETE, callback); }
1787
+ onChange(callback) { return addCallback(this.$callbacks || (this.$callbacks = []), OPERATION.REPLACE, callback); }
1745
1788
  static is(type) {
1746
1789
  return type['collection'] !== undefined;
1747
1790
  }
@@ -1785,17 +1828,19 @@ class CollectionSchema {
1785
1828
  this.$indexes.delete(index);
1786
1829
  return this.$items.delete(index);
1787
1830
  }
1788
- clear(isDecoding) {
1831
+ clear(changes) {
1789
1832
  // discard previous operations.
1790
1833
  this.$changes.discard(true, true);
1791
1834
  this.$changes.indexes = {};
1792
1835
  // clear previous indexes
1793
1836
  this.$indexes.clear();
1794
- // flag child items for garbage collection.
1795
- if (isDecoding && typeof (this.$changes.getType()) !== "string") {
1796
- this.$items.forEach((item) => {
1797
- this.$changes.root.removeRef(item['$changes'].refId);
1798
- });
1837
+ //
1838
+ // When decoding:
1839
+ // - enqueue items for DELETE callback.
1840
+ // - flag child items for garbage collection.
1841
+ //
1842
+ if (changes) {
1843
+ removeChildRefs.call(this, changes);
1799
1844
  }
1800
1845
  // clear items
1801
1846
  this.$items.clear();
@@ -1864,9 +1909,6 @@ class CollectionSchema {
1864
1909
  }
1865
1910
  return cloned;
1866
1911
  }
1867
- triggerAll() {
1868
- Schema.prototype.triggerAll.apply(this);
1869
- }
1870
1912
  }
1871
1913
 
1872
1914
  class SetSchema {
@@ -1879,23 +1921,31 @@ class SetSchema {
1879
1921
  initialValues.forEach((v) => this.add(v));
1880
1922
  }
1881
1923
  }
1924
+ onAdd(callback, triggerAll = true) {
1925
+ return addCallback((this.$callbacks || (this.$callbacks = [])), OPERATION.ADD, callback, (triggerAll)
1926
+ ? this.$items
1927
+ : undefined);
1928
+ }
1929
+ onRemove(callback) { return addCallback(this.$callbacks || (this.$callbacks = []), OPERATION.DELETE, callback); }
1930
+ onChange(callback) { return addCallback(this.$callbacks || (this.$callbacks = []), OPERATION.REPLACE, callback); }
1882
1931
  static is(type) {
1883
1932
  return type['set'] !== undefined;
1884
1933
  }
1885
1934
  add(value) {
1935
+ // immediatelly return false if value already added.
1886
1936
  if (this.has(value)) {
1887
1937
  return false;
1888
1938
  }
1889
1939
  // set "index" for reference.
1890
1940
  const index = this.$refId++;
1891
- const isRef = (value['$changes']) !== undefined;
1892
- if (isRef) {
1941
+ if ((value['$changes']) !== undefined) {
1893
1942
  value['$changes'].setParent(this, this.$changes.root, index);
1894
1943
  }
1944
+ const operation = this.$changes.indexes[index]?.op ?? OPERATION.ADD;
1895
1945
  this.$changes.indexes[index] = index;
1896
1946
  this.$indexes.set(index, index);
1897
1947
  this.$items.set(index, value);
1898
- this.$changes.change(index);
1948
+ this.$changes.change(index, operation);
1899
1949
  return index;
1900
1950
  }
1901
1951
  entries() {
@@ -1921,17 +1971,19 @@ class SetSchema {
1921
1971
  this.$indexes.delete(index);
1922
1972
  return this.$items.delete(index);
1923
1973
  }
1924
- clear(isDecoding) {
1974
+ clear(changes) {
1925
1975
  // discard previous operations.
1926
1976
  this.$changes.discard(true, true);
1927
1977
  this.$changes.indexes = {};
1928
1978
  // clear previous indexes
1929
1979
  this.$indexes.clear();
1930
- // flag child items for garbage collection.
1931
- if (isDecoding && typeof (this.$changes.getType()) !== "string") {
1932
- this.$items.forEach((item) => {
1933
- this.$changes.root.removeRef(item['$changes'].refId);
1934
- });
1980
+ //
1981
+ // When decoding:
1982
+ // - enqueue items for DELETE callback.
1983
+ // - flag child items for garbage collection.
1984
+ //
1985
+ if (changes) {
1986
+ removeChildRefs.call(this, changes);
1935
1987
  }
1936
1988
  // clear items
1937
1989
  this.$items.clear();
@@ -2012,36 +2064,6 @@ class SetSchema {
2012
2064
  }
2013
2065
  return cloned;
2014
2066
  }
2015
- triggerAll() {
2016
- Schema.prototype.triggerAll.apply(this);
2017
- }
2018
- }
2019
-
2020
- /**
2021
- * Extracted from https://www.npmjs.com/package/strong-events
2022
- */
2023
- class EventEmitter {
2024
- constructor() {
2025
- this.handlers = [];
2026
- }
2027
- register(cb, once = false) {
2028
- this.handlers.push(cb);
2029
- return this;
2030
- }
2031
- invoke(...args) {
2032
- this.handlers.forEach((handler) => handler(...args));
2033
- }
2034
- invokeAsync(...args) {
2035
- return Promise.all(this.handlers.map((handler) => handler(...args)));
2036
- }
2037
- remove(cb) {
2038
- const index = this.handlers.indexOf(cb);
2039
- this.handlers[index] = this.handlers[this.handlers.length - 1];
2040
- this.handlers.pop();
2041
- }
2042
- clear() {
2043
- this.handlers = [];
2044
- }
2045
2067
  }
2046
2068
 
2047
2069
  class ClientState {
@@ -2064,6 +2086,75 @@ class ClientState {
2064
2086
  }
2065
2087
  }
2066
2088
 
2089
+ class ReferenceTracker {
2090
+ constructor() {
2091
+ //
2092
+ // Relation of refId => Schema structure
2093
+ // For direct access of structures during decoding time.
2094
+ //
2095
+ this.refs = new Map();
2096
+ this.refCounts = {};
2097
+ this.deletedRefs = new Set();
2098
+ this.nextUniqueId = 0;
2099
+ }
2100
+ getNextUniqueId() {
2101
+ return this.nextUniqueId++;
2102
+ }
2103
+ // for decoding
2104
+ addRef(refId, ref, incrementCount = true) {
2105
+ this.refs.set(refId, ref);
2106
+ if (incrementCount) {
2107
+ this.refCounts[refId] = (this.refCounts[refId] || 0) + 1;
2108
+ }
2109
+ }
2110
+ // for decoding
2111
+ removeRef(refId) {
2112
+ this.refCounts[refId] = this.refCounts[refId] - 1;
2113
+ this.deletedRefs.add(refId);
2114
+ }
2115
+ clearRefs() {
2116
+ this.refs.clear();
2117
+ this.deletedRefs.clear();
2118
+ this.refCounts = {};
2119
+ }
2120
+ // for decoding
2121
+ garbageCollectDeletedRefs() {
2122
+ this.deletedRefs.forEach((refId) => {
2123
+ //
2124
+ // Skip active references.
2125
+ //
2126
+ if (this.refCounts[refId] > 0) {
2127
+ return;
2128
+ }
2129
+ const ref = this.refs.get(refId);
2130
+ //
2131
+ // Ensure child schema instances have their references removed as well.
2132
+ //
2133
+ if (ref instanceof Schema) {
2134
+ for (const fieldName in ref['_definition'].schema) {
2135
+ if (typeof (ref['_definition'].schema[fieldName]) !== "string" &&
2136
+ ref[fieldName] &&
2137
+ ref[fieldName]['$changes']) {
2138
+ this.removeRef(ref[fieldName]['$changes'].refId);
2139
+ }
2140
+ }
2141
+ }
2142
+ else {
2143
+ const definition = ref['$changes'].parent._definition;
2144
+ const type = definition.schema[definition.fieldsByIndex[ref['$changes'].parentIndex]];
2145
+ if (typeof (Object.values(type)[0]) === "function") {
2146
+ Array.from(ref.values())
2147
+ .forEach((child) => this.removeRef(child['$changes'].refId));
2148
+ }
2149
+ }
2150
+ this.refs.delete(refId);
2151
+ delete this.refCounts[refId];
2152
+ });
2153
+ // clear deleted refs.
2154
+ this.deletedRefs.clear();
2155
+ }
2156
+ }
2157
+
2067
2158
  class EncodeSchemaError extends Error {
2068
2159
  }
2069
2160
  function assertType(value, type, klass, field) {
@@ -2126,12 +2217,17 @@ class Schema {
2126
2217
  // fix enumerability of fields for end-user
2127
2218
  Object.defineProperties(this, {
2128
2219
  $changes: {
2129
- value: new ChangeTree(this, undefined, new Root()),
2220
+ value: new ChangeTree(this, undefined, new ReferenceTracker()),
2130
2221
  enumerable: false,
2131
2222
  writable: true
2132
2223
  },
2133
- $listeners: {
2134
- value: {},
2224
+ // $listeners: {
2225
+ // value: undefined,
2226
+ // enumerable: false,
2227
+ // writable: true
2228
+ // },
2229
+ $callbacks: {
2230
+ value: undefined,
2135
2231
  enumerable: false,
2136
2232
  writable: true
2137
2233
  },
@@ -2154,26 +2250,43 @@ class Schema {
2154
2250
  return (type['_definition'] &&
2155
2251
  type['_definition'].schema !== undefined);
2156
2252
  }
2253
+ onChange(callback) {
2254
+ return addCallback((this.$callbacks || (this.$callbacks = [])), OPERATION.REPLACE, callback);
2255
+ }
2256
+ onRemove(callback) {
2257
+ return addCallback((this.$callbacks || (this.$callbacks = [])), OPERATION.DELETE, callback);
2258
+ }
2157
2259
  assign(props) {
2158
2260
  Object.assign(this, props);
2159
2261
  return this;
2160
2262
  }
2161
2263
  get _definition() { return this.constructor._definition; }
2264
+ /**
2265
+ * (Server-side): Flag a property to be encoded for the next patch.
2266
+ * @param instance Schema instance
2267
+ * @param property string representing the property name, or number representing the index of the property.
2268
+ * @param operation OPERATION to perform (detected automatically)
2269
+ */
2270
+ setDirty(property, operation) {
2271
+ this.$changes.change(property, operation);
2272
+ }
2162
2273
  listen(attr, callback) {
2163
- if (!this.$listeners[attr]) {
2164
- this.$listeners[attr] = new EventEmitter();
2274
+ if (!this.$callbacks) {
2275
+ this.$callbacks = {};
2276
+ }
2277
+ if (!this.$callbacks[attr]) {
2278
+ this.$callbacks[attr] = [];
2165
2279
  }
2166
- this.$listeners[attr].register(callback);
2280
+ this.$callbacks[attr].push(callback);
2167
2281
  // return un-register callback.
2168
- return () => this.$listeners[attr].remove(callback);
2282
+ return () => spliceOne(this.$callbacks[attr], this.$callbacks[attr].indexOf(callback));
2169
2283
  }
2170
- decode(bytes, it = { offset: 0 }, ref = this, allChanges = new Map()) {
2284
+ decode(bytes, it = { offset: 0 }, ref = this) {
2285
+ const allChanges = [];
2171
2286
  const $root = this.$changes.root;
2172
2287
  const totalBytes = bytes.length;
2173
2288
  let refId = 0;
2174
- let changes = [];
2175
2289
  $root.refs.set(refId, this);
2176
- allChanges.set(refId, changes);
2177
2290
  while (it.offset < totalBytes) {
2178
2291
  let byte = bytes[it.offset++];
2179
2292
  if (byte == SWITCH_TO_STRUCTURE) {
@@ -2186,9 +2299,6 @@ class Schema {
2186
2299
  throw new Error(`"refId" not found: ${refId}`);
2187
2300
  }
2188
2301
  ref = nextRef;
2189
- // create empty list of changes for this refId.
2190
- changes = [];
2191
- allChanges.set(refId, changes);
2192
2302
  continue;
2193
2303
  }
2194
2304
  const changeTree = ref['$changes'];
@@ -2202,7 +2312,7 @@ class Schema {
2202
2312
  // The `.clear()` method is calling `$root.removeRef(refId)` for
2203
2313
  // each item inside this collection
2204
2314
  //
2205
- ref.clear(true);
2315
+ ref.clear(allChanges);
2206
2316
  continue;
2207
2317
  }
2208
2318
  const fieldIndex = (isSchema)
@@ -2272,9 +2382,8 @@ class Schema {
2272
2382
  value = this.createTypeInstance(childType);
2273
2383
  value.$changes.refId = refId;
2274
2384
  if (previousValue) {
2275
- value.onChange = previousValue.onChange;
2276
- value.onRemove = previousValue.onRemove;
2277
- value.$listeners = previousValue.$listeners;
2385
+ value.$callbacks = previousValue.$callbacks;
2386
+ // value.$listeners = previousValue.$listeners;
2278
2387
  if (previousValue['$changes'].refId &&
2279
2388
  refId !== previousValue['$changes'].refId) {
2280
2389
  $root.removeRef(previousValue['$changes'].refId);
@@ -2300,40 +2409,29 @@ class Schema {
2300
2409
  value.$changes.refId = refId;
2301
2410
  // preserve schema callbacks
2302
2411
  if (previousValue) {
2303
- value.onAdd = previousValue.onAdd;
2304
- value.onRemove = previousValue.onRemove;
2305
- value.onChange = previousValue.onChange;
2412
+ value['$callbacks'] = previousValue['$callbacks'];
2306
2413
  if (previousValue['$changes'].refId &&
2307
2414
  refId !== previousValue['$changes'].refId) {
2308
2415
  $root.removeRef(previousValue['$changes'].refId);
2309
2416
  //
2310
2417
  // Trigger onRemove if structure has been replaced.
2311
2418
  //
2312
- const deletes = [];
2313
2419
  const entries = previousValue.entries();
2314
2420
  let iter;
2315
2421
  while ((iter = entries.next()) && !iter.done) {
2316
2422
  const [key, value] = iter.value;
2317
- deletes.push({
2423
+ allChanges.push({
2424
+ refId,
2318
2425
  op: OPERATION.DELETE,
2319
2426
  field: key,
2320
2427
  value: undefined,
2321
2428
  previousValue: value,
2322
2429
  });
2323
2430
  }
2324
- allChanges.set(previousValue['$changes'].refId, deletes);
2325
2431
  }
2326
2432
  }
2327
2433
  $root.addRef(refId, value, (valueRef !== previousValue));
2328
- //
2329
- // TODO: deprecate proxies on next version.
2330
- // get proxy to target value.
2331
- //
2332
- if (typeDef.getProxy) {
2333
- value = typeDef.getProxy(value);
2334
- }
2335
2434
  }
2336
- let hasChange = (previousValue !== value);
2337
2435
  if (value !== null &&
2338
2436
  value !== undefined) {
2339
2437
  if (value['$changes']) {
@@ -2341,14 +2439,7 @@ class Schema {
2341
2439
  }
2342
2440
  if (ref instanceof Schema) {
2343
2441
  ref[fieldName] = value;
2344
- //
2345
- // FIXME: use `_field` instead of `field`.
2346
- //
2347
- // `field` is going to use the setter of the PropertyDescriptor
2348
- // and create a proxy for array/map. This is only useful for
2349
- // backwards-compatibility with @colyseus/schema@0.5.x
2350
- //
2351
- // // ref[_field] = value;
2442
+ // ref[`_${fieldName}`] = value;
2352
2443
  }
2353
2444
  else if (ref instanceof MapSchema) {
2354
2445
  // const key = ref['$indexes'].get(field);
@@ -2362,19 +2453,20 @@ class Schema {
2362
2453
  // ref[key] = value;
2363
2454
  ref.setAt(fieldIndex, value);
2364
2455
  }
2365
- else if (ref instanceof CollectionSchema ||
2366
- ref instanceof SetSchema) {
2456
+ else if (ref instanceof CollectionSchema) {
2367
2457
  const index = ref.add(value);
2368
2458
  ref['setIndex'](fieldIndex, index);
2369
2459
  }
2460
+ else if (ref instanceof SetSchema) {
2461
+ const index = ref.add(value);
2462
+ if (index !== false) {
2463
+ ref['setIndex'](fieldIndex, index);
2464
+ }
2465
+ }
2370
2466
  }
2371
- if (hasChange
2372
- // &&
2373
- // (
2374
- // this.onChange || ref.$listeners[field]
2375
- // )
2376
- ) {
2377
- changes.push({
2467
+ if (previousValue !== value) {
2468
+ allChanges.push({
2469
+ refId,
2378
2470
  op: operation,
2379
2471
  field: fieldName,
2380
2472
  dynamicIndex,
@@ -2643,7 +2735,7 @@ class Schema {
2643
2735
  //
2644
2736
  // use cached bytes directly if is from Schema type.
2645
2737
  //
2646
- filteredBytes = filteredBytes.concat(changeTree.caches[fieldIndex]);
2738
+ filteredBytes.push.apply(filteredBytes, changeTree.caches[fieldIndex] ?? []);
2647
2739
  containerIndexes.add(fieldIndex);
2648
2740
  }
2649
2741
  else {
@@ -2651,7 +2743,7 @@ class Schema {
2651
2743
  //
2652
2744
  // use cached bytes if already has the field
2653
2745
  //
2654
- filteredBytes = filteredBytes.concat(changeTree.caches[fieldIndex]);
2746
+ filteredBytes.push.apply(filteredBytes, changeTree.caches[fieldIndex] ?? []);
2655
2747
  }
2656
2748
  else {
2657
2749
  //
@@ -2716,20 +2808,6 @@ class Schema {
2716
2808
  }
2717
2809
  return cloned;
2718
2810
  }
2719
- triggerAll() {
2720
- // skip if haven't received any remote refs yet.
2721
- if (this.$changes.root.refs.size === 0) {
2722
- return;
2723
- }
2724
- const allChanges = new Map();
2725
- Schema.prototype._triggerAllFillChanges.call(this, this, allChanges);
2726
- try {
2727
- Schema.prototype._triggerChanges.call(this, allChanges);
2728
- }
2729
- catch (e) {
2730
- Schema.onError(e);
2731
- }
2732
- }
2733
2811
  toJSON() {
2734
2812
  const schema = this._definition.schema;
2735
2813
  const deprecated = this._definition.deprecated;
@@ -2772,109 +2850,73 @@ class Schema {
2772
2850
  instance.$changes.root = this.$changes.root;
2773
2851
  return instance;
2774
2852
  }
2775
- _triggerAllFillChanges(ref, allChanges) {
2776
- if (allChanges.has(ref['$changes'].refId)) {
2777
- return;
2778
- }
2779
- const changes = [];
2780
- allChanges.set(ref['$changes'].refId || 0, changes);
2781
- if (ref instanceof Schema) {
2782
- const schema = ref._definition.schema;
2783
- for (let fieldName in schema) {
2784
- const _field = `_${fieldName}`;
2785
- const value = ref[_field];
2786
- if (value !== undefined) {
2787
- changes.push({
2788
- op: OPERATION.ADD,
2789
- field: fieldName,
2790
- value,
2791
- previousValue: undefined
2792
- });
2793
- if (value['$changes'] !== undefined) {
2794
- Schema.prototype._triggerAllFillChanges.call(this, value, allChanges);
2853
+ _triggerChanges(changes) {
2854
+ const uniqueRefIds = new Set();
2855
+ const $refs = this.$changes.root.refs;
2856
+ for (let i = 0; i < changes.length; i++) {
2857
+ const change = changes[i];
2858
+ const refId = change.refId;
2859
+ const ref = $refs.get(refId);
2860
+ const $callbacks = ref['$callbacks'];
2861
+ //
2862
+ // trigger onRemove on child structure.
2863
+ //
2864
+ if ((change.op & OPERATION.DELETE) === OPERATION.DELETE &&
2865
+ change.previousValue instanceof Schema) {
2866
+ change.previousValue['$callbacks']?.[OPERATION.DELETE]?.forEach(callback => callback());
2867
+ }
2868
+ // no callbacks defined, skip this structure!
2869
+ if (!$callbacks) {
2870
+ continue;
2871
+ }
2872
+ if (ref instanceof Schema) {
2873
+ if (!uniqueRefIds.has(refId)) {
2874
+ try {
2875
+ // trigger onChange
2876
+ $callbacks?.[OPERATION.REPLACE]?.forEach(callback => callback(changes));
2877
+ }
2878
+ catch (e) {
2879
+ Schema.onError(e);
2795
2880
  }
2796
2881
  }
2797
- }
2798
- }
2799
- else {
2800
- const entries = ref.entries();
2801
- let iter;
2802
- while ((iter = entries.next()) && !iter.done) {
2803
- const [key, value] = iter.value;
2804
- changes.push({
2805
- op: OPERATION.ADD,
2806
- field: key,
2807
- dynamicIndex: key,
2808
- value: value,
2809
- previousValue: undefined,
2810
- });
2811
- if (value['$changes'] !== undefined) {
2812
- Schema.prototype._triggerAllFillChanges.call(this, value, allChanges);
2882
+ try {
2883
+ $callbacks[change.op]?.forEach(callback => callback(change.value, change.previousValue));
2884
+ }
2885
+ catch (e) {
2886
+ Schema.onError(e);
2813
2887
  }
2814
2888
  }
2815
- }
2816
- }
2817
- _triggerChanges(allChanges) {
2818
- allChanges.forEach((changes, refId) => {
2819
- if (changes.length > 0) {
2820
- const ref = this.$changes.root.refs.get(refId);
2821
- const isSchema = ref instanceof Schema;
2822
- for (let i = 0; i < changes.length; i++) {
2823
- const change = changes[i];
2824
- const listener = ref['$listeners'] && ref['$listeners'][change.field];
2825
- if (!isSchema) {
2826
- if (change.op === OPERATION.ADD && change.previousValue === undefined) {
2827
- ref.onAdd?.(change.value, change.dynamicIndex ?? change.field);
2828
- }
2829
- else if (change.op === OPERATION.DELETE) {
2830
- //
2831
- // FIXME: `previousValue` should always be avaiiable.
2832
- // ADD + DELETE operations are still encoding DELETE operation.
2833
- //
2834
- if (change.previousValue !== undefined) {
2835
- ref.onRemove?.(change.previousValue, change.dynamicIndex ?? change.field);
2836
- }
2837
- }
2838
- else if (change.op === OPERATION.DELETE_AND_ADD) {
2839
- if (change.previousValue !== undefined) {
2840
- ref.onRemove?.(change.previousValue, change.dynamicIndex);
2841
- }
2842
- ref.onAdd?.(change.value, change.dynamicIndex);
2843
- }
2844
- else if (change.op === OPERATION.REPLACE ||
2845
- change.value !== change.previousValue) {
2846
- ref.onChange?.(change.value, change.dynamicIndex);
2847
- }
2848
- }
2889
+ else {
2890
+ // is a collection of items
2891
+ if (change.op === OPERATION.ADD && change.previousValue === undefined) {
2892
+ // triger onAdd
2893
+ $callbacks[OPERATION.ADD]?.forEach(callback => callback(change.value, change.dynamicIndex ?? change.field));
2894
+ }
2895
+ else if (change.op === OPERATION.DELETE) {
2849
2896
  //
2850
- // trigger onRemove on child structure.
2897
+ // FIXME: `previousValue` should always be available.
2898
+ // ADD + DELETE operations are still encoding DELETE operation.
2851
2899
  //
2852
- if ((change.op & OPERATION.DELETE) === OPERATION.DELETE &&
2853
- change.previousValue instanceof Schema &&
2854
- change.previousValue.onRemove) {
2855
- change.previousValue.onRemove();
2856
- }
2857
- if (listener) {
2858
- try {
2859
- listener.invoke(change.value, change.previousValue);
2860
- }
2861
- catch (e) {
2862
- Schema.onError(e);
2863
- }
2900
+ if (change.previousValue !== undefined) {
2901
+ // triger onRemove
2902
+ $callbacks[OPERATION.DELETE]?.forEach(callback => callback(change.previousValue, change.dynamicIndex ?? change.field));
2864
2903
  }
2865
2904
  }
2866
- if (isSchema) {
2867
- if (ref.onChange) {
2868
- try {
2869
- ref.onChange(changes);
2870
- }
2871
- catch (e) {
2872
- Schema.onError(e);
2873
- }
2905
+ else if (change.op === OPERATION.DELETE_AND_ADD) {
2906
+ // triger onRemove
2907
+ if (change.previousValue !== undefined) {
2908
+ $callbacks[OPERATION.DELETE]?.forEach(callback => callback(change.previousValue, change.dynamicIndex ?? change.field));
2874
2909
  }
2910
+ // triger onAdd
2911
+ $callbacks[OPERATION.ADD]?.forEach(callback => callback(change.value, change.dynamicIndex ?? change.field));
2912
+ }
2913
+ // trigger onChange
2914
+ if (change.value !== change.previousValue) {
2915
+ $callbacks[OPERATION.REPLACE]?.forEach(callback => callback(change.value, change.dynamicIndex ?? change.field));
2875
2916
  }
2876
2917
  }
2877
- });
2918
+ uniqueRefIds.add(refId);
2919
+ }
2878
2920
  }
2879
2921
  }
2880
2922
  Schema._definition = SchemaDefinition.create();
@@ -2920,7 +2962,7 @@ function __decorate(decorators, target, key, desc) {
2920
2962
  return c > 3 && r && Object.defineProperty(target, key, r), r;
2921
2963
  }
2922
2964
 
2923
- const reflectionContext = new Context();
2965
+ const reflectionContext = { context: new Context() };
2924
2966
  /**
2925
2967
  * Reflection
2926
2968
  */
@@ -3025,14 +3067,14 @@ class Reflection extends Schema {
3025
3067
  refType = typeInfo[1];
3026
3068
  }
3027
3069
  if (fieldType === "ref") {
3028
- type(refType, context)(schemaType.prototype, field.name);
3070
+ type(refType, { context })(schemaType.prototype, field.name);
3029
3071
  }
3030
3072
  else {
3031
- type({ [fieldType]: refType }, context)(schemaType.prototype, field.name);
3073
+ type({ [fieldType]: refType }, { context })(schemaType.prototype, field.name);
3032
3074
  }
3033
3075
  }
3034
3076
  else {
3035
- type(field.type, context)(schemaType.prototype, field.name);
3077
+ type(field.type, { context })(schemaType.prototype, field.name);
3036
3078
  }
3037
3079
  });
3038
3080
  });
@@ -3060,8 +3102,8 @@ __decorate([
3060
3102
  type("number", reflectionContext)
3061
3103
  ], Reflection.prototype, "rootType", void 0);
3062
3104
 
3063
- registerType("map", { constructor: MapSchema, getProxy: getMapProxy });
3064
- registerType("array", { constructor: ArraySchema, getProxy: getArrayProxy });
3105
+ registerType("map", { constructor: MapSchema });
3106
+ registerType("array", { constructor: ArraySchema });
3065
3107
  registerType("set", { constructor: SetSchema });
3066
3108
  registerType("collection", { constructor: CollectionSchema, });
3067
3109