@colyseus/schema 1.1.0-alpha.0 → 2.0.0

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 (103) hide show
  1. package/README.md +1 -4
  2. package/build/cjs/index.js +1194 -1001
  3. package/build/cjs/index.js.map +1 -1
  4. package/build/esm/index.mjs +350 -311
  5. package/build/esm/index.mjs.map +1 -1
  6. package/build/umd/index.js +371 -335
  7. package/lib/Reflection.d.ts +21 -0
  8. package/lib/Reflection.js +198 -0
  9. package/lib/Reflection.js.map +1 -0
  10. package/lib/Schema.d.ts +66 -0
  11. package/lib/Schema.js +853 -0
  12. package/lib/Schema.js.map +1 -0
  13. package/lib/annotations.d.ts +96 -0
  14. package/lib/annotations.js +316 -0
  15. package/lib/annotations.js.map +1 -0
  16. package/lib/changes/ChangeTree.d.ts +53 -0
  17. package/lib/changes/ChangeTree.js +209 -0
  18. package/lib/changes/ChangeTree.js.map +1 -0
  19. package/lib/changes/ReferenceTracker.d.ts +14 -0
  20. package/lib/changes/ReferenceTracker.js +77 -0
  21. package/lib/changes/ReferenceTracker.js.map +1 -0
  22. package/lib/codegen/api.d.ts +7 -0
  23. package/lib/codegen/api.js +36 -0
  24. package/lib/codegen/api.js.map +1 -0
  25. package/lib/codegen/argv.d.ts +6 -0
  26. package/lib/codegen/argv.js +41 -0
  27. package/lib/codegen/argv.js.map +1 -0
  28. package/lib/codegen/cli.d.ts +1 -0
  29. package/lib/codegen/cli.js +49 -0
  30. package/lib/codegen/cli.js.map +1 -0
  31. package/lib/codegen/languages/cpp.d.ts +3 -0
  32. package/lib/codegen/languages/cpp.js +213 -0
  33. package/lib/codegen/languages/cpp.js.map +1 -0
  34. package/lib/codegen/languages/csharp.d.ts +4 -0
  35. package/lib/codegen/languages/csharp.js +130 -0
  36. package/lib/codegen/languages/csharp.js.map +1 -0
  37. package/lib/codegen/languages/haxe.d.ts +3 -0
  38. package/lib/codegen/languages/haxe.js +93 -0
  39. package/lib/codegen/languages/haxe.js.map +1 -0
  40. package/lib/codegen/languages/java.d.ts +6 -0
  41. package/lib/codegen/languages/java.js +92 -0
  42. package/lib/codegen/languages/java.js.map +1 -0
  43. package/lib/codegen/languages/js.d.ts +3 -0
  44. package/lib/codegen/languages/js.js +89 -0
  45. package/lib/codegen/languages/js.js.map +1 -0
  46. package/lib/codegen/languages/lua.d.ts +3 -0
  47. package/lib/codegen/languages/lua.js +97 -0
  48. package/lib/codegen/languages/lua.js.map +1 -0
  49. package/lib/codegen/languages/ts.d.ts +3 -0
  50. package/lib/codegen/languages/ts.js +125 -0
  51. package/lib/codegen/languages/ts.js.map +1 -0
  52. package/lib/codegen/parser.d.ts +5 -0
  53. package/lib/codegen/parser.js +196 -0
  54. package/lib/codegen/parser.js.map +1 -0
  55. package/lib/codegen/types.d.ts +44 -0
  56. package/lib/codegen/types.js +122 -0
  57. package/lib/codegen/types.js.map +1 -0
  58. package/lib/encoding/decode.d.ts +48 -0
  59. package/lib/encoding/decode.js +267 -0
  60. package/lib/encoding/decode.js.map +1 -0
  61. package/lib/encoding/encode.d.ts +38 -0
  62. package/lib/encoding/encode.js +281 -0
  63. package/lib/encoding/encode.js.map +1 -0
  64. package/lib/events/EventEmitter.d.ts +13 -0
  65. package/lib/events/EventEmitter.js +62 -0
  66. package/lib/events/EventEmitter.js.map +1 -0
  67. package/lib/filters/index.d.ts +8 -0
  68. package/lib/filters/index.js +25 -0
  69. package/lib/filters/index.js.map +1 -0
  70. package/lib/index.d.ts +19 -0
  71. package/lib/index.js +46 -0
  72. package/lib/index.js.map +1 -0
  73. package/lib/spec.d.ts +13 -0
  74. package/lib/spec.js +42 -0
  75. package/lib/spec.js.map +1 -0
  76. package/lib/types/ArraySchema.d.ts +230 -0
  77. package/lib/types/ArraySchema.js +559 -0
  78. package/lib/types/ArraySchema.js.map +1 -0
  79. package/lib/types/CollectionSchema.d.ts +35 -0
  80. package/lib/types/CollectionSchema.js +158 -0
  81. package/lib/types/CollectionSchema.js.map +1 -0
  82. package/lib/types/HelperTypes.d.ts +28 -0
  83. package/lib/types/HelperTypes.js +3 -0
  84. package/lib/types/HelperTypes.js.map +1 -0
  85. package/lib/types/MapSchema.d.ts +37 -0
  86. package/lib/types/MapSchema.js +214 -0
  87. package/lib/types/MapSchema.js.map +1 -0
  88. package/lib/types/SetSchema.d.ts +32 -0
  89. package/lib/types/SetSchema.js +171 -0
  90. package/lib/types/SetSchema.js.map +1 -0
  91. package/lib/types/index.d.ts +6 -0
  92. package/lib/types/index.js +13 -0
  93. package/lib/types/index.js.map +1 -0
  94. package/lib/types/typeRegistry.d.ts +5 -0
  95. package/lib/types/typeRegistry.js +13 -0
  96. package/lib/types/typeRegistry.js.map +1 -0
  97. package/lib/types/utils.d.ts +9 -0
  98. package/lib/types/utils.js +50 -0
  99. package/lib/types/utils.js.map +1 -0
  100. package/lib/utils.d.ts +2 -0
  101. package/lib/utils.js +26 -0
  102. package/lib/utils.js.map +1 -0
  103. package/package.json +4 -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,18 +1054,35 @@ 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;
1081
1088
  /*
@@ -1090,7 +1097,21 @@ function type(type, context = globalContext) {
1090
1097
  * skip if descriptor already exists for this field (`@deprecated()`)
1091
1098
  */
1092
1099
  if (definition.descriptors[field]) {
1093
- return;
1100
+ if (definition.deprecated[field]) {
1101
+ // do not create accessors for deprecated properties.
1102
+ return;
1103
+ }
1104
+ else {
1105
+ // trying to define same property multiple times across inheritance.
1106
+ // https://github.com/colyseus/colyseus-unity3d/issues/131#issuecomment-814308572
1107
+ try {
1108
+ throw new Error(`@colyseus/schema: Duplicate '${field}' definition on '${constructor.name}'.\nCheck @type() annotation`);
1109
+ }
1110
+ catch (e) {
1111
+ const definitionAtLine = e.stack.split("\n")[4].trim();
1112
+ throw new Error(`${e.message} ${definitionAtLine}`);
1113
+ }
1114
+ }
1094
1115
  }
1095
1116
  const isArray = ArraySchema.is(type);
1096
1117
  const isMap = !isArray && MapSchema.is(type);
@@ -1103,6 +1124,15 @@ function type(type, context = globalContext) {
1103
1124
  context.add(childType);
1104
1125
  }
1105
1126
  }
1127
+ if (options.manual) {
1128
+ // do not declare getter/setter descriptor
1129
+ definition.descriptors[field] = {
1130
+ enumerable: true,
1131
+ configurable: true,
1132
+ writable: true,
1133
+ };
1134
+ return;
1135
+ }
1106
1136
  const fieldCached = `_${field}`;
1107
1137
  definition.descriptors[fieldCached] = {
1108
1138
  enumerable: false,
@@ -1188,7 +1218,7 @@ function filterChildren(cb) {
1188
1218
  * `@deprecated()` flag a field as deprecated.
1189
1219
  * The previous `@type()` annotation should remain along with this one.
1190
1220
  */
1191
- function deprecated(throws = true, context = globalContext) {
1221
+ function deprecated(throws = true) {
1192
1222
  return function (target, field) {
1193
1223
  const constructor = target.constructor;
1194
1224
  const definition = constructor._definition;
@@ -1203,9 +1233,12 @@ function deprecated(throws = true, context = globalContext) {
1203
1233
  }
1204
1234
  };
1205
1235
  }
1206
- function defineTypes(target, fields, context = target._context || globalContext) {
1236
+ function defineTypes(target, fields, options = {}) {
1237
+ if (!options.context) {
1238
+ options.context = target._context || options.context || globalContext;
1239
+ }
1207
1240
  for (let field in fields) {
1208
- type(fields[field], context)(target.prototype, field);
1241
+ type(fields[field], options)(target.prototype, field);
1209
1242
  }
1210
1243
  return target;
1211
1244
  }
@@ -1742,6 +1775,13 @@ class CollectionSchema {
1742
1775
  initialValues.forEach((v) => this.add(v));
1743
1776
  }
1744
1777
  }
1778
+ onAdd(callback, triggerAll = true) {
1779
+ return addCallback((this.$callbacks || (this.$callbacks = [])), OPERATION.ADD, callback, (triggerAll)
1780
+ ? this.$items
1781
+ : undefined);
1782
+ }
1783
+ onRemove(callback) { return addCallback(this.$callbacks || (this.$callbacks = []), OPERATION.DELETE, callback); }
1784
+ onChange(callback) { return addCallback(this.$callbacks || (this.$callbacks = []), OPERATION.REPLACE, callback); }
1745
1785
  static is(type) {
1746
1786
  return type['collection'] !== undefined;
1747
1787
  }
@@ -1785,17 +1825,19 @@ class CollectionSchema {
1785
1825
  this.$indexes.delete(index);
1786
1826
  return this.$items.delete(index);
1787
1827
  }
1788
- clear(isDecoding) {
1828
+ clear(changes) {
1789
1829
  // discard previous operations.
1790
1830
  this.$changes.discard(true, true);
1791
1831
  this.$changes.indexes = {};
1792
1832
  // clear previous indexes
1793
1833
  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
- });
1834
+ //
1835
+ // When decoding:
1836
+ // - enqueue items for DELETE callback.
1837
+ // - flag child items for garbage collection.
1838
+ //
1839
+ if (changes) {
1840
+ removeChildRefs.call(this, changes);
1799
1841
  }
1800
1842
  // clear items
1801
1843
  this.$items.clear();
@@ -1864,9 +1906,6 @@ class CollectionSchema {
1864
1906
  }
1865
1907
  return cloned;
1866
1908
  }
1867
- triggerAll() {
1868
- Schema.prototype.triggerAll.apply(this);
1869
- }
1870
1909
  }
1871
1910
 
1872
1911
  class SetSchema {
@@ -1879,23 +1918,31 @@ class SetSchema {
1879
1918
  initialValues.forEach((v) => this.add(v));
1880
1919
  }
1881
1920
  }
1921
+ onAdd(callback, triggerAll = true) {
1922
+ return addCallback((this.$callbacks || (this.$callbacks = [])), OPERATION.ADD, callback, (triggerAll)
1923
+ ? this.$items
1924
+ : undefined);
1925
+ }
1926
+ onRemove(callback) { return addCallback(this.$callbacks || (this.$callbacks = []), OPERATION.DELETE, callback); }
1927
+ onChange(callback) { return addCallback(this.$callbacks || (this.$callbacks = []), OPERATION.REPLACE, callback); }
1882
1928
  static is(type) {
1883
1929
  return type['set'] !== undefined;
1884
1930
  }
1885
1931
  add(value) {
1932
+ // immediatelly return false if value already added.
1886
1933
  if (this.has(value)) {
1887
1934
  return false;
1888
1935
  }
1889
1936
  // set "index" for reference.
1890
1937
  const index = this.$refId++;
1891
- const isRef = (value['$changes']) !== undefined;
1892
- if (isRef) {
1938
+ if ((value['$changes']) !== undefined) {
1893
1939
  value['$changes'].setParent(this, this.$changes.root, index);
1894
1940
  }
1941
+ const operation = this.$changes.indexes[index]?.op ?? OPERATION.ADD;
1895
1942
  this.$changes.indexes[index] = index;
1896
1943
  this.$indexes.set(index, index);
1897
1944
  this.$items.set(index, value);
1898
- this.$changes.change(index);
1945
+ this.$changes.change(index, operation);
1899
1946
  return index;
1900
1947
  }
1901
1948
  entries() {
@@ -1921,17 +1968,19 @@ class SetSchema {
1921
1968
  this.$indexes.delete(index);
1922
1969
  return this.$items.delete(index);
1923
1970
  }
1924
- clear(isDecoding) {
1971
+ clear(changes) {
1925
1972
  // discard previous operations.
1926
1973
  this.$changes.discard(true, true);
1927
1974
  this.$changes.indexes = {};
1928
1975
  // clear previous indexes
1929
1976
  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
- });
1977
+ //
1978
+ // When decoding:
1979
+ // - enqueue items for DELETE callback.
1980
+ // - flag child items for garbage collection.
1981
+ //
1982
+ if (changes) {
1983
+ removeChildRefs.call(this, changes);
1935
1984
  }
1936
1985
  // clear items
1937
1986
  this.$items.clear();
@@ -2012,36 +2061,6 @@ class SetSchema {
2012
2061
  }
2013
2062
  return cloned;
2014
2063
  }
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
2064
  }
2046
2065
 
2047
2066
  class ClientState {
@@ -2064,6 +2083,75 @@ class ClientState {
2064
2083
  }
2065
2084
  }
2066
2085
 
2086
+ class ReferenceTracker {
2087
+ constructor() {
2088
+ //
2089
+ // Relation of refId => Schema structure
2090
+ // For direct access of structures during decoding time.
2091
+ //
2092
+ this.refs = new Map();
2093
+ this.refCounts = {};
2094
+ this.deletedRefs = new Set();
2095
+ this.nextUniqueId = 0;
2096
+ }
2097
+ getNextUniqueId() {
2098
+ return this.nextUniqueId++;
2099
+ }
2100
+ // for decoding
2101
+ addRef(refId, ref, incrementCount = true) {
2102
+ this.refs.set(refId, ref);
2103
+ if (incrementCount) {
2104
+ this.refCounts[refId] = (this.refCounts[refId] || 0) + 1;
2105
+ }
2106
+ }
2107
+ // for decoding
2108
+ removeRef(refId) {
2109
+ this.refCounts[refId] = this.refCounts[refId] - 1;
2110
+ this.deletedRefs.add(refId);
2111
+ }
2112
+ clearRefs() {
2113
+ this.refs.clear();
2114
+ this.deletedRefs.clear();
2115
+ this.refCounts = {};
2116
+ }
2117
+ // for decoding
2118
+ garbageCollectDeletedRefs() {
2119
+ this.deletedRefs.forEach((refId) => {
2120
+ //
2121
+ // Skip active references.
2122
+ //
2123
+ if (this.refCounts[refId] > 0) {
2124
+ return;
2125
+ }
2126
+ const ref = this.refs.get(refId);
2127
+ //
2128
+ // Ensure child schema instances have their references removed as well.
2129
+ //
2130
+ if (ref instanceof Schema) {
2131
+ for (const fieldName in ref['_definition'].schema) {
2132
+ if (typeof (ref['_definition'].schema[fieldName]) !== "string" &&
2133
+ ref[fieldName] &&
2134
+ ref[fieldName]['$changes']) {
2135
+ this.removeRef(ref[fieldName]['$changes'].refId);
2136
+ }
2137
+ }
2138
+ }
2139
+ else {
2140
+ const definition = ref['$changes'].parent._definition;
2141
+ const type = definition.schema[definition.fieldsByIndex[ref['$changes'].parentIndex]];
2142
+ if (typeof (Object.values(type)[0]) === "function") {
2143
+ Array.from(ref.values())
2144
+ .forEach((child) => this.removeRef(child['$changes'].refId));
2145
+ }
2146
+ }
2147
+ this.refs.delete(refId);
2148
+ delete this.refCounts[refId];
2149
+ });
2150
+ // clear deleted refs.
2151
+ this.deletedRefs.clear();
2152
+ }
2153
+ }
2154
+
2067
2155
  class EncodeSchemaError extends Error {
2068
2156
  }
2069
2157
  function assertType(value, type, klass, field) {
@@ -2126,12 +2214,17 @@ class Schema {
2126
2214
  // fix enumerability of fields for end-user
2127
2215
  Object.defineProperties(this, {
2128
2216
  $changes: {
2129
- value: new ChangeTree(this, undefined, new Root()),
2217
+ value: new ChangeTree(this, undefined, new ReferenceTracker()),
2130
2218
  enumerable: false,
2131
2219
  writable: true
2132
2220
  },
2133
- $listeners: {
2134
- value: {},
2221
+ // $listeners: {
2222
+ // value: undefined,
2223
+ // enumerable: false,
2224
+ // writable: true
2225
+ // },
2226
+ $callbacks: {
2227
+ value: undefined,
2135
2228
  enumerable: false,
2136
2229
  writable: true
2137
2230
  },
@@ -2154,26 +2247,43 @@ class Schema {
2154
2247
  return (type['_definition'] &&
2155
2248
  type['_definition'].schema !== undefined);
2156
2249
  }
2250
+ onChange(callback) {
2251
+ return addCallback((this.$callbacks || (this.$callbacks = [])), OPERATION.REPLACE, callback);
2252
+ }
2253
+ onRemove(callback) {
2254
+ return addCallback((this.$callbacks || (this.$callbacks = [])), OPERATION.DELETE, callback);
2255
+ }
2157
2256
  assign(props) {
2158
2257
  Object.assign(this, props);
2159
2258
  return this;
2160
2259
  }
2161
2260
  get _definition() { return this.constructor._definition; }
2261
+ /**
2262
+ * (Server-side): Flag a property to be encoded for the next patch.
2263
+ * @param instance Schema instance
2264
+ * @param property string representing the property name, or number representing the index of the property.
2265
+ * @param operation OPERATION to perform (detected automatically)
2266
+ */
2267
+ setDirty(property, operation) {
2268
+ this.$changes.change(property, operation);
2269
+ }
2162
2270
  listen(attr, callback) {
2163
- if (!this.$listeners[attr]) {
2164
- this.$listeners[attr] = new EventEmitter();
2271
+ if (!this.$callbacks) {
2272
+ this.$callbacks = {};
2273
+ }
2274
+ if (!this.$callbacks[attr]) {
2275
+ this.$callbacks[attr] = [];
2165
2276
  }
2166
- this.$listeners[attr].register(callback);
2277
+ this.$callbacks[attr].push(callback);
2167
2278
  // return un-register callback.
2168
- return () => this.$listeners[attr].remove(callback);
2279
+ return () => spliceOne(this.$callbacks[attr], this.$callbacks[attr].indexOf(callback));
2169
2280
  }
2170
- decode(bytes, it = { offset: 0 }, ref = this, allChanges = new Map()) {
2281
+ decode(bytes, it = { offset: 0 }, ref = this) {
2282
+ const allChanges = [];
2171
2283
  const $root = this.$changes.root;
2172
2284
  const totalBytes = bytes.length;
2173
2285
  let refId = 0;
2174
- let changes = [];
2175
2286
  $root.refs.set(refId, this);
2176
- allChanges.set(refId, changes);
2177
2287
  while (it.offset < totalBytes) {
2178
2288
  let byte = bytes[it.offset++];
2179
2289
  if (byte == SWITCH_TO_STRUCTURE) {
@@ -2186,9 +2296,6 @@ class Schema {
2186
2296
  throw new Error(`"refId" not found: ${refId}`);
2187
2297
  }
2188
2298
  ref = nextRef;
2189
- // create empty list of changes for this refId.
2190
- changes = [];
2191
- allChanges.set(refId, changes);
2192
2299
  continue;
2193
2300
  }
2194
2301
  const changeTree = ref['$changes'];
@@ -2202,7 +2309,7 @@ class Schema {
2202
2309
  // The `.clear()` method is calling `$root.removeRef(refId)` for
2203
2310
  // each item inside this collection
2204
2311
  //
2205
- ref.clear(true);
2312
+ ref.clear(allChanges);
2206
2313
  continue;
2207
2314
  }
2208
2315
  const fieldIndex = (isSchema)
@@ -2272,9 +2379,8 @@ class Schema {
2272
2379
  value = this.createTypeInstance(childType);
2273
2380
  value.$changes.refId = refId;
2274
2381
  if (previousValue) {
2275
- value.onChange = previousValue.onChange;
2276
- value.onRemove = previousValue.onRemove;
2277
- value.$listeners = previousValue.$listeners;
2382
+ value.$callbacks = previousValue.$callbacks;
2383
+ // value.$listeners = previousValue.$listeners;
2278
2384
  if (previousValue['$changes'].refId &&
2279
2385
  refId !== previousValue['$changes'].refId) {
2280
2386
  $root.removeRef(previousValue['$changes'].refId);
@@ -2300,40 +2406,29 @@ class Schema {
2300
2406
  value.$changes.refId = refId;
2301
2407
  // preserve schema callbacks
2302
2408
  if (previousValue) {
2303
- value.onAdd = previousValue.onAdd;
2304
- value.onRemove = previousValue.onRemove;
2305
- value.onChange = previousValue.onChange;
2409
+ value['$callbacks'] = previousValue['$callbacks'];
2306
2410
  if (previousValue['$changes'].refId &&
2307
2411
  refId !== previousValue['$changes'].refId) {
2308
2412
  $root.removeRef(previousValue['$changes'].refId);
2309
2413
  //
2310
2414
  // Trigger onRemove if structure has been replaced.
2311
2415
  //
2312
- const deletes = [];
2313
2416
  const entries = previousValue.entries();
2314
2417
  let iter;
2315
2418
  while ((iter = entries.next()) && !iter.done) {
2316
2419
  const [key, value] = iter.value;
2317
- deletes.push({
2420
+ allChanges.push({
2421
+ refId,
2318
2422
  op: OPERATION.DELETE,
2319
2423
  field: key,
2320
2424
  value: undefined,
2321
2425
  previousValue: value,
2322
2426
  });
2323
2427
  }
2324
- allChanges.set(previousValue['$changes'].refId, deletes);
2325
2428
  }
2326
2429
  }
2327
2430
  $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
2431
  }
2336
- let hasChange = (previousValue !== value);
2337
2432
  if (value !== null &&
2338
2433
  value !== undefined) {
2339
2434
  if (value['$changes']) {
@@ -2341,14 +2436,7 @@ class Schema {
2341
2436
  }
2342
2437
  if (ref instanceof Schema) {
2343
2438
  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;
2439
+ // ref[`_${fieldName}`] = value;
2352
2440
  }
2353
2441
  else if (ref instanceof MapSchema) {
2354
2442
  // const key = ref['$indexes'].get(field);
@@ -2362,19 +2450,20 @@ class Schema {
2362
2450
  // ref[key] = value;
2363
2451
  ref.setAt(fieldIndex, value);
2364
2452
  }
2365
- else if (ref instanceof CollectionSchema ||
2366
- ref instanceof SetSchema) {
2453
+ else if (ref instanceof CollectionSchema) {
2367
2454
  const index = ref.add(value);
2368
2455
  ref['setIndex'](fieldIndex, index);
2369
2456
  }
2457
+ else if (ref instanceof SetSchema) {
2458
+ const index = ref.add(value);
2459
+ if (index !== false) {
2460
+ ref['setIndex'](fieldIndex, index);
2461
+ }
2462
+ }
2370
2463
  }
2371
- if (hasChange
2372
- // &&
2373
- // (
2374
- // this.onChange || ref.$listeners[field]
2375
- // )
2376
- ) {
2377
- changes.push({
2464
+ if (previousValue !== value) {
2465
+ allChanges.push({
2466
+ refId,
2378
2467
  op: operation,
2379
2468
  field: fieldName,
2380
2469
  dynamicIndex,
@@ -2643,7 +2732,7 @@ class Schema {
2643
2732
  //
2644
2733
  // use cached bytes directly if is from Schema type.
2645
2734
  //
2646
- filteredBytes = filteredBytes.concat(changeTree.caches[fieldIndex]);
2735
+ filteredBytes.push.apply(filteredBytes, changeTree.caches[fieldIndex] ?? []);
2647
2736
  containerIndexes.add(fieldIndex);
2648
2737
  }
2649
2738
  else {
@@ -2651,7 +2740,7 @@ class Schema {
2651
2740
  //
2652
2741
  // use cached bytes if already has the field
2653
2742
  //
2654
- filteredBytes = filteredBytes.concat(changeTree.caches[fieldIndex]);
2743
+ filteredBytes.push.apply(filteredBytes, changeTree.caches[fieldIndex] ?? []);
2655
2744
  }
2656
2745
  else {
2657
2746
  //
@@ -2716,20 +2805,6 @@ class Schema {
2716
2805
  }
2717
2806
  return cloned;
2718
2807
  }
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
2808
  toJSON() {
2734
2809
  const schema = this._definition.schema;
2735
2810
  const deprecated = this._definition.deprecated;
@@ -2772,109 +2847,73 @@ class Schema {
2772
2847
  instance.$changes.root = this.$changes.root;
2773
2848
  return instance;
2774
2849
  }
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);
2850
+ _triggerChanges(changes) {
2851
+ const uniqueRefIds = new Set();
2852
+ const $refs = this.$changes.root.refs;
2853
+ for (let i = 0; i < changes.length; i++) {
2854
+ const change = changes[i];
2855
+ const refId = change.refId;
2856
+ const ref = $refs.get(refId);
2857
+ const $callbacks = ref['$callbacks'];
2858
+ //
2859
+ // trigger onRemove on child structure.
2860
+ //
2861
+ if ((change.op & OPERATION.DELETE) === OPERATION.DELETE &&
2862
+ change.previousValue instanceof Schema) {
2863
+ change.previousValue['$callbacks']?.[OPERATION.DELETE]?.forEach(callback => callback());
2864
+ }
2865
+ // no callbacks defined, skip this structure!
2866
+ if (!$callbacks) {
2867
+ continue;
2868
+ }
2869
+ if (ref instanceof Schema) {
2870
+ if (!uniqueRefIds.has(refId)) {
2871
+ try {
2872
+ // trigger onChange
2873
+ $callbacks?.[OPERATION.REPLACE]?.forEach(callback => callback(changes));
2874
+ }
2875
+ catch (e) {
2876
+ Schema.onError(e);
2795
2877
  }
2796
2878
  }
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);
2879
+ try {
2880
+ $callbacks[change.field]?.forEach(callback => callback(change.value, change.previousValue));
2881
+ }
2882
+ catch (e) {
2883
+ Schema.onError(e);
2813
2884
  }
2814
2885
  }
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
- }
2886
+ else {
2887
+ // is a collection of items
2888
+ if (change.op === OPERATION.ADD && change.previousValue === undefined) {
2889
+ // triger onAdd
2890
+ $callbacks[OPERATION.ADD]?.forEach(callback => callback(change.value, change.dynamicIndex ?? change.field));
2891
+ }
2892
+ else if (change.op === OPERATION.DELETE) {
2849
2893
  //
2850
- // trigger onRemove on child structure.
2894
+ // FIXME: `previousValue` should always be available.
2895
+ // ADD + DELETE operations are still encoding DELETE operation.
2851
2896
  //
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
- }
2897
+ if (change.previousValue !== undefined) {
2898
+ // triger onRemove
2899
+ $callbacks[OPERATION.DELETE]?.forEach(callback => callback(change.previousValue, change.dynamicIndex ?? change.field));
2864
2900
  }
2865
2901
  }
2866
- if (isSchema) {
2867
- if (ref.onChange) {
2868
- try {
2869
- ref.onChange(changes);
2870
- }
2871
- catch (e) {
2872
- Schema.onError(e);
2873
- }
2902
+ else if (change.op === OPERATION.DELETE_AND_ADD) {
2903
+ // triger onRemove
2904
+ if (change.previousValue !== undefined) {
2905
+ $callbacks[OPERATION.DELETE]?.forEach(callback => callback(change.previousValue, change.dynamicIndex ?? change.field));
2874
2906
  }
2907
+ // triger onAdd
2908
+ $callbacks[OPERATION.ADD]?.forEach(callback => callback(change.value, change.dynamicIndex ?? change.field));
2909
+ }
2910
+ // trigger onChange
2911
+ if (change.value !== change.previousValue) {
2912
+ $callbacks[OPERATION.REPLACE]?.forEach(callback => callback(change.value, change.dynamicIndex ?? change.field));
2875
2913
  }
2876
2914
  }
2877
- });
2915
+ uniqueRefIds.add(refId);
2916
+ }
2878
2917
  }
2879
2918
  }
2880
2919
  Schema._definition = SchemaDefinition.create();
@@ -2920,7 +2959,7 @@ function __decorate(decorators, target, key, desc) {
2920
2959
  return c > 3 && r && Object.defineProperty(target, key, r), r;
2921
2960
  }
2922
2961
 
2923
- const reflectionContext = new Context();
2962
+ const reflectionContext = { context: new Context() };
2924
2963
  /**
2925
2964
  * Reflection
2926
2965
  */
@@ -3025,14 +3064,14 @@ class Reflection extends Schema {
3025
3064
  refType = typeInfo[1];
3026
3065
  }
3027
3066
  if (fieldType === "ref") {
3028
- type(refType, context)(schemaType.prototype, field.name);
3067
+ type(refType, { context })(schemaType.prototype, field.name);
3029
3068
  }
3030
3069
  else {
3031
- type({ [fieldType]: refType }, context)(schemaType.prototype, field.name);
3070
+ type({ [fieldType]: refType }, { context })(schemaType.prototype, field.name);
3032
3071
  }
3033
3072
  }
3034
3073
  else {
3035
- type(field.type, context)(schemaType.prototype, field.name);
3074
+ type(field.type, { context })(schemaType.prototype, field.name);
3036
3075
  }
3037
3076
  });
3038
3077
  });
@@ -3060,8 +3099,8 @@ __decorate([
3060
3099
  type("number", reflectionContext)
3061
3100
  ], Reflection.prototype, "rootType", void 0);
3062
3101
 
3063
- registerType("map", { constructor: MapSchema, getProxy: getMapProxy });
3064
- registerType("array", { constructor: ArraySchema, getProxy: getArrayProxy });
3102
+ registerType("map", { constructor: MapSchema });
3103
+ registerType("array", { constructor: ArraySchema });
3065
3104
  registerType("set", { constructor: SetSchema });
3066
3105
  registerType("collection", { constructor: CollectionSchema, });
3067
3106