@colyseus/schema 3.0.0-alpha.4 → 3.0.0-alpha.40

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 (141) hide show
  1. package/README.md +131 -61
  2. package/bin/schema-debug +94 -0
  3. package/build/cjs/index.js +1521 -809
  4. package/build/cjs/index.js.map +1 -1
  5. package/build/esm/index.mjs +1519 -808
  6. package/build/esm/index.mjs.map +1 -1
  7. package/build/umd/index.js +1528 -816
  8. package/lib/Metadata.d.ts +21 -9
  9. package/lib/Metadata.js +169 -32
  10. package/lib/Metadata.js.map +1 -1
  11. package/lib/Reflection.d.ts +19 -4
  12. package/lib/Reflection.js +66 -32
  13. package/lib/Reflection.js.map +1 -1
  14. package/lib/Schema.d.ts +4 -4
  15. package/lib/Schema.js +44 -50
  16. package/lib/Schema.js.map +1 -1
  17. package/lib/annotations.d.ts +31 -34
  18. package/lib/annotations.js +110 -160
  19. package/lib/annotations.js.map +1 -1
  20. package/lib/bench_encode.d.ts +1 -0
  21. package/lib/bench_encode.js +130 -0
  22. package/lib/bench_encode.js.map +1 -0
  23. package/lib/codegen/api.js +1 -2
  24. package/lib/codegen/api.js.map +1 -1
  25. package/lib/codegen/languages/cpp.js +1 -2
  26. package/lib/codegen/languages/cpp.js.map +1 -1
  27. package/lib/codegen/languages/csharp.js +1 -2
  28. package/lib/codegen/languages/csharp.js.map +1 -1
  29. package/lib/codegen/languages/haxe.js +1 -2
  30. package/lib/codegen/languages/haxe.js.map +1 -1
  31. package/lib/codegen/languages/java.js +1 -2
  32. package/lib/codegen/languages/java.js.map +1 -1
  33. package/lib/codegen/languages/js.js +1 -2
  34. package/lib/codegen/languages/js.js.map +1 -1
  35. package/lib/codegen/languages/lua.js +1 -2
  36. package/lib/codegen/languages/lua.js.map +1 -1
  37. package/lib/codegen/languages/ts.js +1 -2
  38. package/lib/codegen/languages/ts.js.map +1 -1
  39. package/lib/codegen/parser.js +85 -3
  40. package/lib/codegen/parser.js.map +1 -1
  41. package/lib/codegen/types.js +6 -3
  42. package/lib/codegen/types.js.map +1 -1
  43. package/lib/debug.d.ts +1 -0
  44. package/lib/debug.js +51 -0
  45. package/lib/debug.js.map +1 -0
  46. package/lib/decoder/DecodeOperation.d.ts +0 -1
  47. package/lib/decoder/DecodeOperation.js +30 -12
  48. package/lib/decoder/DecodeOperation.js.map +1 -1
  49. package/lib/decoder/Decoder.d.ts +6 -7
  50. package/lib/decoder/Decoder.js +9 -9
  51. package/lib/decoder/Decoder.js.map +1 -1
  52. package/lib/decoder/ReferenceTracker.js +3 -2
  53. package/lib/decoder/ReferenceTracker.js.map +1 -1
  54. package/lib/decoder/strategy/RawChanges.js +1 -2
  55. package/lib/decoder/strategy/RawChanges.js.map +1 -1
  56. package/lib/decoder/strategy/StateCallbacks.d.ts +44 -11
  57. package/lib/decoder/strategy/StateCallbacks.js +75 -65
  58. package/lib/decoder/strategy/StateCallbacks.js.map +1 -1
  59. package/lib/encoder/ChangeTree.d.ts +27 -21
  60. package/lib/encoder/ChangeTree.js +246 -186
  61. package/lib/encoder/ChangeTree.js.map +1 -1
  62. package/lib/encoder/EncodeOperation.d.ts +3 -6
  63. package/lib/encoder/EncodeOperation.js +47 -61
  64. package/lib/encoder/EncodeOperation.js.map +1 -1
  65. package/lib/encoder/Encoder.d.ts +9 -8
  66. package/lib/encoder/Encoder.js +165 -88
  67. package/lib/encoder/Encoder.js.map +1 -1
  68. package/lib/encoder/Root.d.ts +22 -0
  69. package/lib/encoder/Root.js +81 -0
  70. package/lib/encoder/Root.js.map +1 -0
  71. package/lib/encoder/StateView.d.ts +7 -7
  72. package/lib/encoder/StateView.js +70 -74
  73. package/lib/encoder/StateView.js.map +1 -1
  74. package/lib/encoding/assert.d.ts +2 -1
  75. package/lib/encoding/assert.js +5 -5
  76. package/lib/encoding/assert.js.map +1 -1
  77. package/lib/encoding/decode.js +20 -21
  78. package/lib/encoding/decode.js.map +1 -1
  79. package/lib/encoding/encode.d.ts +2 -2
  80. package/lib/encoding/encode.js +52 -48
  81. package/lib/encoding/encode.js.map +1 -1
  82. package/lib/encoding/spec.d.ts +2 -1
  83. package/lib/encoding/spec.js +1 -0
  84. package/lib/encoding/spec.js.map +1 -1
  85. package/lib/index.d.ts +6 -3
  86. package/lib/index.js +19 -13
  87. package/lib/index.js.map +1 -1
  88. package/lib/types/HelperTypes.d.ts +34 -2
  89. package/lib/types/HelperTypes.js.map +1 -1
  90. package/lib/types/TypeContext.d.ts +23 -0
  91. package/lib/types/TypeContext.js +111 -0
  92. package/lib/types/TypeContext.js.map +1 -0
  93. package/lib/types/custom/ArraySchema.d.ts +2 -2
  94. package/lib/types/custom/ArraySchema.js +33 -22
  95. package/lib/types/custom/ArraySchema.js.map +1 -1
  96. package/lib/types/custom/CollectionSchema.js +1 -0
  97. package/lib/types/custom/CollectionSchema.js.map +1 -1
  98. package/lib/types/custom/MapSchema.d.ts +3 -1
  99. package/lib/types/custom/MapSchema.js +12 -4
  100. package/lib/types/custom/MapSchema.js.map +1 -1
  101. package/lib/types/custom/SetSchema.js +1 -0
  102. package/lib/types/custom/SetSchema.js.map +1 -1
  103. package/lib/types/registry.js +3 -4
  104. package/lib/types/registry.js.map +1 -1
  105. package/lib/types/symbols.d.ts +8 -5
  106. package/lib/types/symbols.js +9 -6
  107. package/lib/types/symbols.js.map +1 -1
  108. package/lib/types/utils.js +1 -2
  109. package/lib/types/utils.js.map +1 -1
  110. package/lib/utils.js +9 -7
  111. package/lib/utils.js.map +1 -1
  112. package/package.json +7 -6
  113. package/src/Metadata.ts +190 -42
  114. package/src/Reflection.ts +77 -39
  115. package/src/Schema.ts +59 -64
  116. package/src/annotations.ts +155 -203
  117. package/src/bench_encode.ts +108 -0
  118. package/src/codegen/parser.ts +107 -0
  119. package/src/codegen/types.ts +1 -0
  120. package/src/debug.ts +55 -0
  121. package/src/decoder/DecodeOperation.ts +40 -12
  122. package/src/decoder/Decoder.ts +14 -12
  123. package/src/decoder/ReferenceTracker.ts +3 -2
  124. package/src/decoder/strategy/StateCallbacks.ts +153 -82
  125. package/src/encoder/ChangeTree.ts +286 -202
  126. package/src/encoder/EncodeOperation.ts +77 -77
  127. package/src/encoder/Encoder.ts +201 -96
  128. package/src/encoder/Root.ts +93 -0
  129. package/src/encoder/StateView.ts +76 -88
  130. package/src/encoding/assert.ts +4 -3
  131. package/src/encoding/encode.ts +37 -31
  132. package/src/encoding/spec.ts +1 -0
  133. package/src/index.ts +8 -14
  134. package/src/types/HelperTypes.ts +54 -2
  135. package/src/types/TypeContext.ts +133 -0
  136. package/src/types/custom/ArraySchema.ts +49 -19
  137. package/src/types/custom/CollectionSchema.ts +1 -0
  138. package/src/types/custom/MapSchema.ts +18 -5
  139. package/src/types/custom/SetSchema.ts +1 -0
  140. package/src/types/symbols.ts +10 -7
  141. package/src/utils.ts +7 -3
@@ -29,6 +29,7 @@
29
29
  OPERATION[OPERATION["REVERSE"] = 15] = "REVERSE";
30
30
  OPERATION[OPERATION["MOVE"] = 32] = "MOVE";
31
31
  OPERATION[OPERATION["DELETE_BY_REFID"] = 33] = "DELETE_BY_REFID";
32
+ OPERATION[OPERATION["ADD_BY_REFID"] = 129] = "ADD_BY_REFID";
32
33
  })(exports.OPERATION || (exports.OPERATION = {}));
33
34
 
34
35
  Symbol.metadata ??= Symbol.for("Symbol.metadata");
@@ -48,11 +49,6 @@
48
49
  * (MapSchema, ArraySchema, etc.)
49
50
  */
50
51
  const $childType = Symbol('$childType');
51
- /**
52
- * Special ChangeTree property to identify new instances
53
- * (Once they're encoded, they're not new anymore)
54
- */
55
- const $isNew = Symbol("$isNew");
56
52
  /**
57
53
  * Optional "discard" method for custom types (ArraySchema)
58
54
  * (Discards changes for next serialization)
@@ -62,6 +58,14 @@
62
58
  * When decoding, this method is called after the instance is fully decoded
63
59
  */
64
60
  const $onDecodeEnd = Symbol("$onDecodeEnd");
61
+ /**
62
+ * Metadata
63
+ */
64
+ const $descriptors = Symbol("$descriptors");
65
+ const $numFields = "$__numFields";
66
+ const $refTypeFieldIndexes = "$__refTypeFieldIndexes";
67
+ const $viewFieldIndexes = "$__viewFieldIndexes";
68
+ const $fieldIndexesByViewTag = "$__fieldIndexesByViewTag";
65
69
 
66
70
  const registeredTypes = {};
67
71
  const identifiers = new Map();
@@ -73,177 +77,416 @@
73
77
  return registeredTypes[identifier];
74
78
  }
75
79
 
80
+ class TypeContext {
81
+ /**
82
+ * For inheritance support
83
+ * Keeps track of which classes extends which. (parent -> children)
84
+ */
85
+ static { this.inheritedTypes = new Map(); }
86
+ static register(target) {
87
+ const parent = Object.getPrototypeOf(target);
88
+ if (parent !== Schema) {
89
+ let inherits = TypeContext.inheritedTypes.get(parent);
90
+ if (!inherits) {
91
+ inherits = new Set();
92
+ TypeContext.inheritedTypes.set(parent, inherits);
93
+ }
94
+ inherits.add(target);
95
+ }
96
+ }
97
+ constructor(rootClass) {
98
+ this.types = {};
99
+ this.schemas = new Map();
100
+ this.hasFilters = false;
101
+ this.parentFiltered = {};
102
+ if (rootClass) {
103
+ this.discoverTypes(rootClass);
104
+ }
105
+ }
106
+ has(schema) {
107
+ return this.schemas.has(schema);
108
+ }
109
+ get(typeid) {
110
+ return this.types[typeid];
111
+ }
112
+ add(schema, typeid = this.schemas.size) {
113
+ // skip if already registered
114
+ if (this.schemas.has(schema)) {
115
+ return false;
116
+ }
117
+ this.types[typeid] = schema;
118
+ //
119
+ // Workaround to allow using an empty Schema (with no `@type()` fields)
120
+ //
121
+ if (schema[Symbol.metadata] === undefined) {
122
+ Metadata.initialize(schema);
123
+ }
124
+ this.schemas.set(schema, typeid);
125
+ return true;
126
+ }
127
+ getTypeId(klass) {
128
+ return this.schemas.get(klass);
129
+ }
130
+ discoverTypes(klass, parentIndex, parentFieldViewTag) {
131
+ if (!this.add(klass)) {
132
+ return;
133
+ }
134
+ // add classes inherited from this base class
135
+ TypeContext.inheritedTypes.get(klass)?.forEach((child) => {
136
+ this.discoverTypes(child, parentIndex, parentFieldViewTag);
137
+ });
138
+ // add parent classes
139
+ let parent = klass;
140
+ while ((parent = Object.getPrototypeOf(parent)) &&
141
+ parent !== Schema && // stop at root (Schema)
142
+ parent !== Function.prototype // stop at root (non-Schema)
143
+ ) {
144
+ this.discoverTypes(parent);
145
+ }
146
+ const metadata = (klass[Symbol.metadata] ??= {});
147
+ // if any schema/field has filters, mark "context" as having filters.
148
+ if (metadata[$viewFieldIndexes]) {
149
+ this.hasFilters = true;
150
+ }
151
+ if (parentFieldViewTag !== undefined) {
152
+ this.parentFiltered[`${this.schemas.get(klass)}-${parentIndex}`] = true;
153
+ }
154
+ for (const fieldIndex in metadata) {
155
+ const index = fieldIndex;
156
+ const fieldType = metadata[index].type;
157
+ const viewTag = metadata[index].tag;
158
+ if (typeof (fieldType) === "string") {
159
+ continue;
160
+ }
161
+ if (Array.isArray(fieldType)) {
162
+ const type = fieldType[0];
163
+ // skip primitive types
164
+ if (type === "string") {
165
+ continue;
166
+ }
167
+ this.discoverTypes(type, index, viewTag);
168
+ }
169
+ else if (typeof (fieldType) === "function") {
170
+ this.discoverTypes(fieldType, viewTag);
171
+ }
172
+ else {
173
+ const type = Object.values(fieldType)[0];
174
+ // skip primitive types
175
+ if (typeof (type) === "string") {
176
+ continue;
177
+ }
178
+ this.discoverTypes(type, index, viewTag);
179
+ }
180
+ }
181
+ }
182
+ }
183
+
184
+ function getNormalizedType(type) {
185
+ return (Array.isArray(type))
186
+ ? { array: type[0] }
187
+ : (typeof (type['type']) !== "undefined")
188
+ ? type['type']
189
+ : type;
190
+ }
76
191
  const Metadata = {
77
- addField(metadata, index, field, type, descriptor) {
192
+ addField(metadata, index, name, type, descriptor) {
78
193
  if (index > 64) {
79
- throw new Error(`Can't define field '${field}'.\nSchema instances may only have up to 64 fields.`);
194
+ throw new Error(`Can't define field '${name}'.\nSchema instances may only have up to 64 fields.`);
80
195
  }
81
- metadata[field] = Object.assign(metadata[field] || {}, // avoid overwriting previous field metadata (@owned / @deprecated)
196
+ metadata[index] = Object.assign(metadata[index] || {}, // avoid overwriting previous field metadata (@owned / @deprecated)
82
197
  {
83
- type: (Array.isArray(type))
84
- ? { array: type[0] }
85
- : type,
198
+ type: getNormalizedType(type),
86
199
  index,
87
- descriptor,
200
+ name,
88
201
  });
202
+ // create "descriptors" map
203
+ Object.defineProperty(metadata, $descriptors, {
204
+ value: metadata[$descriptors] || {},
205
+ enumerable: false,
206
+ configurable: true,
207
+ });
208
+ if (descriptor) {
209
+ // for encoder
210
+ metadata[$descriptors][name] = descriptor;
211
+ metadata[$descriptors][`_${name}`] = {
212
+ value: undefined,
213
+ writable: true,
214
+ enumerable: false,
215
+ configurable: true,
216
+ };
217
+ }
218
+ else {
219
+ // for decoder
220
+ metadata[$descriptors][name] = {
221
+ value: undefined,
222
+ writable: true,
223
+ enumerable: true,
224
+ configurable: true,
225
+ };
226
+ }
89
227
  // map -1 as last field index
90
- Object.defineProperty(metadata, -1, {
228
+ Object.defineProperty(metadata, $numFields, {
91
229
  value: index,
92
230
  enumerable: false,
93
231
  configurable: true
94
232
  });
95
- // map index => field name (non enumerable)
96
- Object.defineProperty(metadata, index, {
97
- value: field,
233
+ // map field name => index (non enumerable)
234
+ Object.defineProperty(metadata, name, {
235
+ value: index,
98
236
  enumerable: false,
99
237
  configurable: true,
100
238
  });
239
+ // if child Ref/complex type, add to -4
240
+ if (typeof (metadata[index].type) !== "string") {
241
+ if (metadata[$refTypeFieldIndexes] === undefined) {
242
+ Object.defineProperty(metadata, $refTypeFieldIndexes, {
243
+ value: [],
244
+ enumerable: false,
245
+ configurable: true,
246
+ });
247
+ }
248
+ metadata[$refTypeFieldIndexes].push(index);
249
+ }
101
250
  },
102
251
  setTag(metadata, fieldName, tag) {
252
+ const index = metadata[fieldName];
253
+ const field = metadata[index];
103
254
  // add 'tag' to the field
104
- const field = metadata[fieldName];
105
255
  field.tag = tag;
106
- if (!metadata[-2]) {
256
+ if (!metadata[$viewFieldIndexes]) {
107
257
  // -2: all field indexes with "view" tag
108
- Object.defineProperty(metadata, -2, {
258
+ Object.defineProperty(metadata, $viewFieldIndexes, {
109
259
  value: [],
110
260
  enumerable: false,
111
261
  configurable: true
112
262
  });
113
263
  // -3: field indexes by "view" tag
114
- Object.defineProperty(metadata, -3, {
264
+ Object.defineProperty(metadata, $fieldIndexesByViewTag, {
115
265
  value: {},
116
266
  enumerable: false,
117
267
  configurable: true
118
268
  });
119
269
  }
120
- metadata[-2].push(field.index);
121
- if (!metadata[-3][tag]) {
122
- metadata[-3][tag] = [];
270
+ metadata[$viewFieldIndexes].push(index);
271
+ if (!metadata[$fieldIndexesByViewTag][tag]) {
272
+ metadata[$fieldIndexesByViewTag][tag] = [];
123
273
  }
124
- metadata[-3][tag].push(field.index);
274
+ metadata[$fieldIndexesByViewTag][tag].push(index);
125
275
  },
126
276
  setFields(target, fields) {
127
- const metadata = (target.prototype.constructor[Symbol.metadata] ??= {});
128
- // target[$track] = function (changeTree, index: number, operation: OPERATION = OPERATION.ADD) {
129
- // changeTree.change(index, operation, encodeSchemaOperation);
130
- // };
131
- // target[$encoder] = encodeSchemaOperation;
132
- // target[$decoder] = decodeSchemaOperation;
133
- // if (!target.prototype.toJSON) { target.prototype.toJSON = Schema.prototype.toJSON; }
134
- let index = 0;
277
+ // for inheritance support
278
+ const constructor = target.prototype.constructor;
279
+ TypeContext.register(constructor);
280
+ const parentClass = Object.getPrototypeOf(constructor);
281
+ const parentMetadata = parentClass && parentClass[Symbol.metadata];
282
+ const metadata = Metadata.initialize(constructor);
283
+ // Use Schema's methods if not defined in the class
284
+ if (!constructor[$track]) {
285
+ constructor[$track] = Schema[$track];
286
+ }
287
+ if (!constructor[$encoder]) {
288
+ constructor[$encoder] = Schema[$encoder];
289
+ }
290
+ if (!constructor[$decoder]) {
291
+ constructor[$decoder] = Schema[$decoder];
292
+ }
293
+ if (!constructor.prototype.toJSON) {
294
+ constructor.prototype.toJSON = Schema.prototype.toJSON;
295
+ }
296
+ //
297
+ // detect index for this field, considering inheritance
298
+ //
299
+ let fieldIndex = metadata[$numFields] // current structure already has fields defined
300
+ ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
301
+ ?? -1; // no fields defined
302
+ fieldIndex++;
135
303
  for (const field in fields) {
136
304
  const type = fields[field];
305
+ const normalizedType = getNormalizedType(type);
137
306
  // FIXME: this code is duplicated from @type() annotation
138
307
  const complexTypeKlass = (Array.isArray(type))
139
308
  ? getType("array")
140
309
  : (typeof (Object.keys(type)[0]) === "string") && getType(Object.keys(type)[0]);
141
- Metadata.addField(metadata, index, field, type, getPropertyDescriptor(`_${field}`, index, type, complexTypeKlass, metadata, field));
142
- index++;
310
+ const childType = (complexTypeKlass)
311
+ ? Object.values(type)[0]
312
+ : normalizedType;
313
+ Metadata.addField(metadata, fieldIndex, field, type, getPropertyDescriptor(`_${field}`, fieldIndex, childType, complexTypeKlass));
314
+ fieldIndex++;
143
315
  }
316
+ return target;
144
317
  },
145
318
  isDeprecated(metadata, field) {
146
319
  return metadata[field].deprecated === true;
147
320
  },
321
+ init(klass) {
322
+ //
323
+ // Used only to initialize an empty Schema (Encoder#constructor)
324
+ // TODO: remove/refactor this...
325
+ //
326
+ const metadata = {};
327
+ klass[Symbol.metadata] = metadata;
328
+ Object.defineProperty(metadata, $numFields, {
329
+ value: 0,
330
+ enumerable: false,
331
+ configurable: true,
332
+ });
333
+ },
334
+ initialize(constructor) {
335
+ const parentClass = Object.getPrototypeOf(constructor);
336
+ const parentMetadata = parentClass[Symbol.metadata];
337
+ let metadata = constructor[Symbol.metadata] ?? Object.create(null);
338
+ // make sure inherited classes have their own metadata object.
339
+ if (parentClass !== Schema && metadata === parentMetadata) {
340
+ metadata = Object.create(null);
341
+ if (parentMetadata) {
342
+ //
343
+ // assign parent metadata to current
344
+ //
345
+ Object.setPrototypeOf(metadata, parentMetadata);
346
+ // $numFields
347
+ Object.defineProperty(metadata, $numFields, {
348
+ value: parentMetadata[$numFields],
349
+ enumerable: false,
350
+ configurable: true,
351
+ writable: true,
352
+ });
353
+ // $viewFieldIndexes / $fieldIndexesByViewTag
354
+ if (parentMetadata[$viewFieldIndexes] !== undefined) {
355
+ Object.defineProperty(metadata, $viewFieldIndexes, {
356
+ value: [...parentMetadata[$viewFieldIndexes]],
357
+ enumerable: false,
358
+ configurable: true,
359
+ writable: true,
360
+ });
361
+ Object.defineProperty(metadata, $fieldIndexesByViewTag, {
362
+ value: { ...parentMetadata[$fieldIndexesByViewTag] },
363
+ enumerable: false,
364
+ configurable: true,
365
+ writable: true,
366
+ });
367
+ }
368
+ // $refTypeFieldIndexes
369
+ if (parentMetadata[$refTypeFieldIndexes] !== undefined) {
370
+ Object.defineProperty(metadata, $refTypeFieldIndexes, {
371
+ value: [...parentMetadata[$refTypeFieldIndexes]],
372
+ enumerable: false,
373
+ configurable: true,
374
+ writable: true,
375
+ });
376
+ }
377
+ // $descriptors
378
+ Object.defineProperty(metadata, $descriptors, {
379
+ value: { ...parentMetadata[$descriptors] },
380
+ enumerable: false,
381
+ configurable: true,
382
+ writable: true,
383
+ });
384
+ }
385
+ }
386
+ constructor[Symbol.metadata] = metadata;
387
+ return metadata;
388
+ },
148
389
  isValidInstance(klass) {
149
390
  return (klass.constructor[Symbol.metadata] &&
150
- Object.prototype.hasOwnProperty.call(klass.constructor[Symbol.metadata], -1));
391
+ Object.prototype.hasOwnProperty.call(klass.constructor[Symbol.metadata], $numFields));
151
392
  },
152
393
  getFields(klass) {
153
394
  const metadata = klass[Symbol.metadata];
154
395
  const fields = {};
155
- for (let i = 0; i <= metadata[-1]; i++) {
156
- fields[metadata[i]] = metadata[metadata[i]].type;
396
+ for (let i = 0; i <= metadata[$numFields]; i++) {
397
+ fields[metadata[i].name] = metadata[i].type;
157
398
  }
158
399
  return fields;
159
400
  }
160
401
  };
161
402
 
162
- var _a$5;
163
- class Root {
164
- constructor() {
165
- this.nextUniqueId = 0;
166
- this.refCount = new WeakMap();
167
- // all changes
168
- this.allChanges = new Map();
169
- this.allFilteredChanges = new Map();
170
- // pending changes to be encoded
171
- this.changes = new Map();
172
- this.filteredChanges = new Map();
173
- }
174
- getNextUniqueId() {
175
- return this.nextUniqueId++;
403
+ function setOperationAtIndex(changeSet, index) {
404
+ const operationsIndex = changeSet.indexes[index];
405
+ if (operationsIndex === undefined) {
406
+ changeSet.indexes[index] = changeSet.operations.push(index) - 1;
176
407
  }
177
- add(changeTree) {
178
- const refCount = this.refCount.get(changeTree) || 0;
179
- this.refCount.set(changeTree, refCount + 1);
408
+ else {
409
+ changeSet.operations[operationsIndex] = index;
180
410
  }
181
- remove(changeTree) {
182
- const refCount = this.refCount.get(changeTree);
183
- if (refCount <= 1) {
184
- this.allChanges.delete(changeTree);
185
- this.changes.delete(changeTree);
186
- if (changeTree.isFiltered || changeTree.isPartiallyFiltered) {
187
- this.allFilteredChanges.delete(changeTree);
188
- this.filteredChanges.delete(changeTree);
189
- }
190
- this.refCount.delete(changeTree);
191
- }
192
- else {
193
- this.refCount.set(changeTree, refCount - 1);
194
- }
195
- changeTree.forEachChild((child, _) => this.remove(child));
411
+ }
412
+ function deleteOperationAtIndex(changeSet, index) {
413
+ const operationsIndex = changeSet.indexes[index];
414
+ if (operationsIndex !== undefined) {
415
+ changeSet.operations[operationsIndex] = undefined;
196
416
  }
197
- clear() {
198
- this.changes.clear();
417
+ delete changeSet.indexes[index];
418
+ }
419
+ function enqueueChangeTree(root, changeTree, changeSet, queueRootIndex = changeTree[changeSet].queueRootIndex) {
420
+ if (root && root[changeSet][queueRootIndex] !== changeTree) {
421
+ changeTree[changeSet].queueRootIndex = root[changeSet].push(changeTree) - 1;
199
422
  }
200
423
  }
201
424
  class ChangeTree {
202
- static { _a$5 = $isNew; }
203
- ;
204
425
  constructor(ref) {
205
- this.indexes = {}; // TODO: remove this, only used by MapSchema/SetSchema/CollectionSchema (`encodeKeyValueOperation`)
206
- this.currentOperationIndex = 0;
207
- this.allChanges = new Map();
208
- this.allFilteredChanges = new Map();
209
- this.changes = new Map();
210
- this.filteredChanges = new Map();
211
- this[_a$5] = true;
426
+ this.isFiltered = false;
427
+ this.isPartiallyFiltered = false;
428
+ this.indexedOperations = {};
429
+ //
430
+ // TODO:
431
+ // try storing the index + operation per item.
432
+ // example: 1024 & 1025 => ADD, 1026 => DELETE
433
+ //
434
+ // => https://chatgpt.com/share/67107d0c-bc20-8004-8583-83b17dd7c196
435
+ //
436
+ this.changes = { indexes: {}, operations: [] };
437
+ this.allChanges = { indexes: {}, operations: [] };
438
+ /**
439
+ * Is this a new instance? Used on ArraySchema to determine OPERATION.MOVE_AND_ADD operation.
440
+ */
441
+ this.isNew = true;
212
442
  this.ref = ref;
443
+ //
444
+ // Does this structure have "filters" declared?
445
+ //
446
+ if (ref.constructor[Symbol.metadata]?.[$viewFieldIndexes]) {
447
+ this.allFilteredChanges = { indexes: {}, operations: [] };
448
+ this.filteredChanges = { indexes: {}, operations: [] };
449
+ }
213
450
  }
214
451
  setRoot(root) {
215
452
  this.root = root;
216
- this.root.add(this);
217
- //
218
- // At Schema initialization, the "root" structure might not be available
219
- // yet, as it only does once the "Encoder" has been set up.
220
- //
221
- // So the "parent" may be already set without a "root".
222
- //
223
- this.checkIsFiltered(this.parent, this.parentIndex);
224
- // unique refId for the ChangeTree.
225
- this.ensureRefId();
453
+ const isNewChangeTree = this.root.add(this);
454
+ const metadata = this.ref.constructor[Symbol.metadata];
455
+ if (this.root.types.hasFilters) {
456
+ //
457
+ // At Schema initialization, the "root" structure might not be available
458
+ // yet, as it only does once the "Encoder" has been set up.
459
+ //
460
+ // So the "parent" may be already set without a "root".
461
+ //
462
+ this.checkIsFiltered(metadata, this.parent, this.parentIndex);
463
+ if (this.isFiltered || this.isPartiallyFiltered) {
464
+ enqueueChangeTree(root, this, 'filteredChanges');
465
+ if (isNewChangeTree) {
466
+ this.root.allFilteredChanges.push(this);
467
+ }
468
+ }
469
+ }
226
470
  if (!this.isFiltered) {
227
- this.root.changes.set(this, this.changes);
471
+ enqueueChangeTree(root, this, 'changes');
472
+ if (isNewChangeTree) {
473
+ this.root.allChanges.push(this);
474
+ }
228
475
  }
229
- if (this.isFiltered || this.isPartiallyFiltered) {
230
- this.root.allFilteredChanges.set(this, this.allFilteredChanges);
231
- this.root.filteredChanges.set(this, this.filteredChanges);
232
- // } else {
233
- // this.root.allChanges.set(this, this.allChanges);
476
+ // Recursively set root on child structures
477
+ if (metadata) {
478
+ metadata[$refTypeFieldIndexes]?.forEach((index) => {
479
+ const field = metadata[index];
480
+ const value = this.ref[field.name];
481
+ value?.[$changes].setRoot(root);
482
+ });
234
483
  }
235
- if (!this.isFiltered) {
236
- this.root.allChanges.set(this, this.allChanges);
484
+ else if (this.ref[$childType] && typeof (this.ref[$childType]) !== "string") {
485
+ // MapSchema / ArraySchema, etc.
486
+ this.ref.forEach((value, key) => {
487
+ value[$changes].setRoot(root);
488
+ });
237
489
  }
238
- this.forEachChild((changeTree, _) => {
239
- changeTree.setRoot(root);
240
- });
241
- // this.allChanges.forEach((_, index) => {
242
- // const childRef = this.ref[$getByIndex](index);
243
- // if (childRef && childRef[$changes]) {
244
- // childRef[$changes].setRoot(root);
245
- // }
246
- // });
247
490
  }
248
491
  setParent(parent, root, parentIndex) {
249
492
  this.parent = parent;
@@ -252,83 +495,104 @@
252
495
  if (!root) {
253
496
  return;
254
497
  }
255
- root.add(this);
498
+ const metadata = this.ref.constructor[Symbol.metadata];
256
499
  // skip if parent is already set
257
- if (root === this.root) {
258
- this.forEachChild((changeTree, atIndex) => {
259
- changeTree.setParent(this.ref, root, atIndex);
260
- });
261
- return;
500
+ if (root !== this.root) {
501
+ this.root = root;
502
+ const isNewChangeTree = root.add(this);
503
+ if (root.types.hasFilters) {
504
+ this.checkIsFiltered(metadata, parent, parentIndex);
505
+ if (this.isFiltered || this.isPartiallyFiltered) {
506
+ enqueueChangeTree(root, this, 'filteredChanges');
507
+ if (isNewChangeTree) {
508
+ this.root.allFilteredChanges.push(this);
509
+ }
510
+ }
511
+ }
512
+ if (!this.isFiltered) {
513
+ enqueueChangeTree(root, this, 'changes');
514
+ if (isNewChangeTree) {
515
+ this.root.allChanges.push(this);
516
+ }
517
+ }
262
518
  }
263
- this.root = root;
264
- this.checkIsFiltered(parent, parentIndex);
265
- if (!this.isFiltered) {
266
- this.root.changes.set(this, this.changes);
519
+ else {
520
+ root.add(this);
267
521
  }
268
- if (this.isFiltered || this.isPartiallyFiltered) {
269
- this.root.filteredChanges.set(this, this.filteredChanges);
270
- this.root.allFilteredChanges.set(this, this.filteredChanges);
522
+ // assign same parent on child structures
523
+ if (metadata) {
524
+ metadata[$refTypeFieldIndexes]?.forEach((index) => {
525
+ const field = metadata[index];
526
+ const value = this.ref[field.name];
527
+ value?.[$changes].setParent(this.ref, root, index);
528
+ // try { throw new Error(); } catch (e) {
529
+ // console.log(e.stack);
530
+ // }
531
+ });
271
532
  }
272
- else {
273
- this.root.allChanges.set(this, this.allChanges);
533
+ else if (this.ref[$childType] && typeof (this.ref[$childType]) !== "string") {
534
+ // MapSchema / ArraySchema, etc.
535
+ this.ref.forEach((value, key) => {
536
+ value[$changes].setParent(this.ref, root, this.indexes[key] ?? key);
537
+ });
274
538
  }
275
- this.ensureRefId();
276
- this.forEachChild((changeTree, atIndex) => {
277
- changeTree.setParent(this.ref, root, atIndex);
278
- });
279
539
  }
280
540
  forEachChild(callback) {
281
541
  //
282
542
  // assign same parent on child structures
283
543
  //
284
- if (Metadata.isValidInstance(this.ref)) {
285
- const metadata = this.ref['constructor'][Symbol.metadata];
286
- // FIXME: need to iterate over parent metadata instead.
287
- for (const field in metadata) {
288
- const value = this.ref[field];
289
- if (value && value[$changes]) {
290
- callback(value[$changes], metadata[field].index);
544
+ const metadata = this.ref.constructor[Symbol.metadata];
545
+ if (metadata) {
546
+ metadata[$refTypeFieldIndexes]?.forEach((index) => {
547
+ const field = metadata[index];
548
+ const value = this.ref[field.name];
549
+ if (value) {
550
+ callback(value[$changes], index);
291
551
  }
292
- }
552
+ });
293
553
  }
294
- else if (typeof (this.ref) === "object") {
554
+ else if (this.ref[$childType] && typeof (this.ref[$childType]) !== "string") {
295
555
  // MapSchema / ArraySchema, etc.
296
556
  this.ref.forEach((value, key) => {
297
- if (Metadata.isValidInstance(value)) {
298
- callback(value[$changes], this.ref[$changes].indexes[key]);
299
- }
557
+ callback(value[$changes], this.indexes[key] ?? key);
300
558
  });
301
559
  }
302
560
  }
303
561
  operation(op) {
304
- this.changes.set(--this.currentOperationIndex, op);
305
- this.root?.changes.set(this, this.changes);
562
+ // operations without index use negative values to represent them
563
+ // this is checked during .encode() time.
564
+ this.changes.operations.push(-op);
565
+ enqueueChangeTree(this.root, this, 'changes');
306
566
  }
307
567
  change(index, operation = exports.OPERATION.ADD) {
308
- const metadata = this.ref['constructor'][Symbol.metadata];
309
- const isFiltered = this.isFiltered || (metadata && metadata[metadata[index]].tag !== undefined);
568
+ const metadata = this.ref.constructor[Symbol.metadata];
569
+ const isFiltered = this.isFiltered || (metadata?.[index]?.tag !== undefined);
310
570
  const changeSet = (isFiltered)
311
571
  ? this.filteredChanges
312
572
  : this.changes;
313
- const previousOperation = changeSet.get(index);
573
+ const previousOperation = this.indexedOperations[index];
314
574
  if (!previousOperation || previousOperation === exports.OPERATION.DELETE) {
315
575
  const op = (!previousOperation)
316
576
  ? operation
317
577
  : (previousOperation === exports.OPERATION.DELETE)
318
578
  ? exports.OPERATION.DELETE_AND_ADD
319
579
  : operation;
320
- changeSet.set(index, op);
580
+ //
581
+ // TODO: are DELETE operations being encoded as ADD here ??
582
+ //
583
+ this.indexedOperations[index] = op;
321
584
  }
322
- //
323
- // TODO: are DELETE operations being encoded as ADD here ??
324
- //
585
+ setOperationAtIndex(changeSet, index);
325
586
  if (isFiltered) {
326
- this.allFilteredChanges.set(index, exports.OPERATION.ADD);
327
- this.root?.filteredChanges.set(this, this.filteredChanges);
587
+ setOperationAtIndex(this.allFilteredChanges, index);
588
+ if (this.root) {
589
+ enqueueChangeTree(this.root, this, 'filteredChanges');
590
+ enqueueChangeTree(this.root, this, 'allFilteredChanges');
591
+ }
328
592
  }
329
593
  else {
330
- this.allChanges.set(index, exports.OPERATION.ADD);
331
- this.root?.changes.set(this, this.changes);
594
+ setOperationAtIndex(this.allChanges, index);
595
+ enqueueChangeTree(this.root, this, 'changes');
332
596
  }
333
597
  }
334
598
  shiftChangeIndexes(shiftIndex) {
@@ -340,12 +604,15 @@
340
604
  const changeSet = (this.isFiltered)
341
605
  ? this.filteredChanges
342
606
  : this.changes;
343
- const changeSetEntries = Array.from(changeSet.entries());
344
- changeSet.clear();
345
- // Re-insert each entry with the shifted index
346
- for (const [index, op] of changeSetEntries) {
347
- changeSet.set(index + shiftIndex, op);
607
+ const newIndexedOperations = {};
608
+ const newIndexes = {};
609
+ for (const index in this.indexedOperations) {
610
+ newIndexedOperations[Number(index) + shiftIndex] = this.indexedOperations[index];
611
+ newIndexes[Number(index) + shiftIndex] = changeSet[index];
348
612
  }
613
+ this.indexedOperations = newIndexedOperations;
614
+ changeSet.indexes = newIndexes;
615
+ changeSet.operations = changeSet.operations.map((index) => index + shiftIndex);
349
616
  }
350
617
  shiftAllChangeIndexes(shiftIndex, startIndex = 0) {
351
618
  //
@@ -361,33 +628,42 @@
361
628
  this._shiftAllChangeIndexes(shiftIndex, startIndex, this.allChanges);
362
629
  }
363
630
  }
364
- _shiftAllChangeIndexes(shiftIndex, startIndex = 0, allChangeSet) {
365
- Array.from(allChangeSet.entries()).forEach(([index, op]) => {
366
- // console.log('shiftAllChangeIndexes', index >= startIndex, { index, op, shiftIndex, startIndex })
367
- if (index >= startIndex) {
368
- allChangeSet.delete(index);
369
- allChangeSet.set(index + shiftIndex, op);
631
+ _shiftAllChangeIndexes(shiftIndex, startIndex = 0, changeSet) {
632
+ const newIndexes = {};
633
+ for (const key in changeSet.indexes) {
634
+ const index = changeSet.indexes[key];
635
+ if (index > startIndex) {
636
+ newIndexes[Number(key) + shiftIndex] = index;
370
637
  }
371
- });
638
+ else {
639
+ newIndexes[key] = index;
640
+ }
641
+ }
642
+ changeSet.indexes = newIndexes;
643
+ for (let i = 0; i < changeSet.operations.length; i++) {
644
+ const index = changeSet.operations[i];
645
+ if (index > startIndex) {
646
+ changeSet.operations[i] = index + shiftIndex;
647
+ }
648
+ }
372
649
  }
373
650
  indexedOperation(index, operation, allChangesIndex = index) {
374
- const metadata = this.ref['constructor'][Symbol.metadata];
375
- const isFiltered = this.isFiltered || (metadata && metadata[metadata[index]].tag !== undefined);
376
- if (isFiltered) {
377
- this.allFilteredChanges.set(allChangesIndex, exports.OPERATION.ADD);
378
- this.filteredChanges.set(index, operation);
379
- this.root?.filteredChanges.set(this, this.filteredChanges);
651
+ this.indexedOperations[index] = operation;
652
+ if (this.filteredChanges) {
653
+ setOperationAtIndex(this.allFilteredChanges, allChangesIndex);
654
+ setOperationAtIndex(this.filteredChanges, index);
655
+ enqueueChangeTree(this.root, this, 'filteredChanges');
380
656
  }
381
657
  else {
382
- this.allChanges.set(allChangesIndex, exports.OPERATION.ADD);
383
- this.changes.set(index, operation);
384
- this.root?.changes.set(this, this.changes);
658
+ setOperationAtIndex(this.allChanges, allChangesIndex);
659
+ setOperationAtIndex(this.changes, index);
660
+ enqueueChangeTree(this.root, this, 'changes');
385
661
  }
386
662
  }
387
663
  getType(index) {
388
664
  if (Metadata.isValidInstance(this.ref)) {
389
- const metadata = this.ref['constructor'][Symbol.metadata];
390
- return metadata[metadata[index]].type;
665
+ const metadata = this.ref.constructor[Symbol.metadata];
666
+ return metadata[index].type;
391
667
  }
392
668
  else {
393
669
  //
@@ -400,8 +676,7 @@
400
676
  }
401
677
  }
402
678
  getChange(index) {
403
- // TODO: optimize this. avoid checking against multiple instances
404
- return this.changes.get(index) ?? this.filteredChanges.get(index);
679
+ return this.indexedOperations[index];
405
680
  }
406
681
  //
407
682
  // used during `.encode()`
@@ -422,16 +697,14 @@
422
697
  }
423
698
  return;
424
699
  }
425
- const metadata = this.ref['constructor'][Symbol.metadata];
426
- const isFiltered = this.isFiltered || (metadata && metadata[metadata[index]].tag !== undefined);
427
- const changeSet = (isFiltered)
700
+ const changeSet = (this.filteredChanges)
428
701
  ? this.filteredChanges
429
702
  : this.changes;
703
+ this.indexedOperations[index] = operation ?? exports.OPERATION.DELETE;
704
+ setOperationAtIndex(changeSet, index);
430
705
  const previousValue = this.getValue(index);
431
- changeSet.set(index, operation ?? exports.OPERATION.DELETE);
432
706
  // remove `root` reference
433
707
  if (previousValue && previousValue[$changes]) {
434
- previousValue[$changes].root = undefined;
435
708
  //
436
709
  // FIXME: this.root is "undefined"
437
710
  //
@@ -445,22 +718,26 @@
445
718
  this.root?.remove(previousValue[$changes]);
446
719
  }
447
720
  //
448
- // FIXME: this is looking a bit ugly (and repeated from `.change()`)
721
+ // FIXME: this is looking a ugly and repeated
449
722
  //
450
- if (isFiltered) {
451
- this.root?.filteredChanges.set(this, this.filteredChanges);
452
- this.allFilteredChanges.delete(allChangesIndex);
723
+ if (this.filteredChanges) {
724
+ deleteOperationAtIndex(this.allFilteredChanges, allChangesIndex);
725
+ enqueueChangeTree(this.root, this, 'filteredChanges');
453
726
  }
454
727
  else {
455
- this.root?.changes.set(this, this.changes);
456
- this.allChanges.delete(allChangesIndex);
728
+ deleteOperationAtIndex(this.allChanges, allChangesIndex);
729
+ enqueueChangeTree(this.root, this, 'changes');
457
730
  }
458
731
  }
459
732
  endEncode() {
460
- this.changes.clear();
733
+ this.indexedOperations = {};
734
+ // // clear changes
735
+ // this.changes.indexes = {};
736
+ // this.changes.operations.length = 0;
737
+ // ArraySchema and MapSchema have a custom "encode end" method
461
738
  this.ref[$onEncodeEnd]?.();
462
739
  // Not a new instance anymore
463
- delete this[$isNew];
740
+ this.isNew = false;
464
741
  }
465
742
  discard(discardAll = false) {
466
743
  //
@@ -469,13 +746,22 @@
469
746
  // REPLACE in case same key is used on next patches.
470
747
  //
471
748
  this.ref[$onEncodeEnd]?.();
472
- this.changes.clear();
473
- this.filteredChanges.clear();
474
- // reset operation index
475
- this.currentOperationIndex = 0;
749
+ this.indexedOperations = {};
750
+ this.changes.indexes = {};
751
+ this.changes.operations.length = 0;
752
+ this.changes.queueRootIndex = undefined;
753
+ if (this.filteredChanges !== undefined) {
754
+ this.filteredChanges.indexes = {};
755
+ this.filteredChanges.operations.length = 0;
756
+ this.filteredChanges.queueRootIndex = undefined;
757
+ }
476
758
  if (discardAll) {
477
- this.allChanges.clear();
478
- this.allFilteredChanges.clear();
759
+ this.allChanges.indexes = {};
760
+ this.allChanges.operations.length = 0;
761
+ if (this.allFilteredChanges !== undefined) {
762
+ this.allFilteredChanges.indexes = {};
763
+ this.allFilteredChanges.operations.length = 0;
764
+ }
479
765
  // remove children references
480
766
  this.forEachChild((changeTree, _) => this.root?.remove(changeTree));
481
767
  }
@@ -484,12 +770,13 @@
484
770
  * Recursively discard all changes from this, and child structures.
485
771
  */
486
772
  discardAll() {
487
- this.changes.forEach((_, fieldIndex) => {
488
- const value = this.getValue(fieldIndex);
773
+ const keys = Object.keys(this.indexedOperations);
774
+ for (let i = 0, len = keys.length; i < len; i++) {
775
+ const value = this.getValue(Number(keys[i]));
489
776
  if (value && value[$changes]) {
490
777
  value[$changes].discardAll();
491
778
  }
492
- });
779
+ }
493
780
  this.discard();
494
781
  }
495
782
  ensureRefId() {
@@ -500,35 +787,49 @@
500
787
  this.refId = this.root.getNextUniqueId();
501
788
  }
502
789
  get changed() {
503
- return this.changes.size > 0;
790
+ return (Object.entries(this.indexedOperations).length > 0);
504
791
  }
505
- checkIsFiltered(parent, parentIndex) {
792
+ checkIsFiltered(metadata, parent, parentIndex) {
506
793
  // Detect if current structure has "filters" declared
507
- this.isPartiallyFiltered = this.ref['constructor']?.[Symbol.metadata]?.[-2];
508
- // TODO: support "partially filtered", where the instance is visible, but only a field is not.
509
- // Detect if parent has "filters" declared
510
- while (parent && !this.isFiltered) {
511
- const metadata = parent['constructor'][Symbol.metadata];
512
- const fieldName = metadata?.[parentIndex];
513
- const isParentOwned = metadata?.[fieldName]?.tag !== undefined;
514
- this.isFiltered = isParentOwned || parent[$changes].isFiltered; // metadata?.[-2]
515
- parent = parent[$changes].parent;
794
+ this.isPartiallyFiltered = metadata?.[$viewFieldIndexes] !== undefined;
795
+ if (this.isPartiallyFiltered) {
796
+ this.filteredChanges = this.filteredChanges || { indexes: {}, operations: [] };
797
+ this.allFilteredChanges = this.allFilteredChanges || { indexes: {}, operations: [] };
516
798
  }
799
+ // skip if parent is not set
800
+ if (!parent) {
801
+ return;
802
+ }
803
+ if (!Metadata.isValidInstance(parent)) {
804
+ const parentChangeTree = parent[$changes];
805
+ parent = parentChangeTree.parent;
806
+ parentIndex = parentChangeTree.parentIndex;
807
+ }
808
+ const parentMetadata = parent.constructor?.[Symbol.metadata];
809
+ this.isFiltered = parentMetadata?.[$viewFieldIndexes]?.includes(parentIndex);
517
810
  //
518
811
  // TODO: refactor this!
519
812
  //
520
813
  // swapping `changes` and `filteredChanges` is required here
521
814
  // because "isFiltered" may not be imedialely available on `change()`
522
815
  //
523
- if (this.isFiltered && this.changes.size > 0) {
524
- // swap changes reference
525
- const changes = this.changes;
526
- this.changes = this.filteredChanges;
527
- this.filteredChanges = changes;
528
- // swap "all changes" reference
529
- const allFilteredChanges = this.allFilteredChanges;
530
- this.allFilteredChanges = this.allChanges;
531
- this.allChanges = allFilteredChanges;
816
+ if (this.isFiltered) {
817
+ this.filteredChanges = { indexes: {}, operations: [] };
818
+ this.allFilteredChanges = { indexes: {}, operations: [] };
819
+ if (this.changes.operations.length > 0) {
820
+ // swap changes reference
821
+ const changes = this.changes;
822
+ this.changes = this.filteredChanges;
823
+ this.filteredChanges = changes;
824
+ // swap "all changes" reference
825
+ const allFilteredChanges = this.allFilteredChanges;
826
+ this.allFilteredChanges = this.allChanges;
827
+ this.allChanges = allFilteredChanges;
828
+ // console.log("SWAP =>", {
829
+ // "this.allFilteredChanges": this.allFilteredChanges,
830
+ // "this.allChanges": this.allChanges
831
+ // })
832
+ }
532
833
  }
533
834
  }
534
835
  }
@@ -565,26 +866,29 @@
565
866
  textEncoder = new TextEncoder();
566
867
  }
567
868
  catch (e) { }
568
- function utf8Length(str) {
569
- var c = 0, length = 0;
570
- for (var i = 0, l = str.length; i < l; i++) {
571
- c = str.charCodeAt(i);
572
- if (c < 0x80) {
573
- length += 1;
574
- }
575
- else if (c < 0x800) {
576
- length += 2;
577
- }
578
- else if (c < 0xd800 || c >= 0xe000) {
579
- length += 3;
580
- }
581
- else {
582
- i++;
583
- length += 4;
869
+ const hasBufferByteLength = (typeof Buffer !== 'undefined' && Buffer.byteLength);
870
+ const utf8Length = (hasBufferByteLength)
871
+ ? Buffer.byteLength // node
872
+ : function (str, _) {
873
+ var c = 0, length = 0;
874
+ for (var i = 0, l = str.length; i < l; i++) {
875
+ c = str.charCodeAt(i);
876
+ if (c < 0x80) {
877
+ length += 1;
878
+ }
879
+ else if (c < 0x800) {
880
+ length += 2;
881
+ }
882
+ else if (c < 0xd800 || c >= 0xe000) {
883
+ length += 3;
884
+ }
885
+ else {
886
+ i++;
887
+ length += 4;
888
+ }
584
889
  }
585
- }
586
- return length;
587
- }
890
+ return length;
891
+ };
588
892
  function utf8Write(view, str, it) {
589
893
  var c = 0;
590
894
  for (var i = 0, l = str.length; i < l; i++) {
@@ -593,21 +897,24 @@
593
897
  view[it.offset++] = c;
594
898
  }
595
899
  else if (c < 0x800) {
596
- view[it.offset++] = 0xc0 | (c >> 6);
597
- view[it.offset++] = 0x80 | (c & 0x3f);
900
+ view[it.offset] = 0xc0 | (c >> 6);
901
+ view[it.offset + 1] = 0x80 | (c & 0x3f);
902
+ it.offset += 2;
598
903
  }
599
904
  else if (c < 0xd800 || c >= 0xe000) {
600
- view[it.offset++] = 0xe0 | (c >> 12);
601
- view[it.offset++] = 0x80 | (c >> 6 & 0x3f);
602
- view[it.offset++] = 0x80 | (c & 0x3f);
905
+ view[it.offset] = 0xe0 | (c >> 12);
906
+ view[it.offset + 1] = 0x80 | (c >> 6 & 0x3f);
907
+ view[it.offset + 2] = 0x80 | (c & 0x3f);
908
+ it.offset += 3;
603
909
  }
604
910
  else {
605
911
  i++;
606
912
  c = 0x10000 + (((c & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff));
607
- view[it.offset++] = 0xf0 | (c >> 18);
608
- view[it.offset++] = 0x80 | (c >> 12 & 0x3f);
609
- view[it.offset++] = 0x80 | (c >> 6 & 0x3f);
610
- view[it.offset++] = 0x80 | (c & 0x3f);
913
+ view[it.offset] = 0xf0 | (c >> 18);
914
+ view[it.offset + 1] = 0x80 | (c >> 12 & 0x3f);
915
+ view[it.offset + 2] = 0x80 | (c >> 6 & 0x3f);
916
+ view[it.offset + 3] = 0x80 | (c & 0x3f);
917
+ it.offset += 4;
611
918
  }
612
919
  }
613
920
  }
@@ -679,8 +986,7 @@
679
986
  if (!value) {
680
987
  value = "";
681
988
  }
682
- // let length = utf8Length(value);
683
- let length = Buffer.byteLength(value, "utf8");
989
+ let length = utf8Length(value, "utf8");
684
990
  let size = 0;
685
991
  // fixstr
686
992
  if (length < 0x20) {
@@ -791,81 +1097,30 @@
791
1097
 
792
1098
  var encode = /*#__PURE__*/Object.freeze({
793
1099
  __proto__: null,
794
- utf8Length: utf8Length,
795
- utf8Write: utf8Write,
796
- int8: int8$1,
797
- uint8: uint8$1,
1100
+ boolean: boolean$1,
1101
+ float32: float32$1,
1102
+ float64: float64$1,
798
1103
  int16: int16$1,
799
- uint16: uint16$1,
800
1104
  int32: int32$1,
801
- uint32: uint32$1,
802
1105
  int64: int64$1,
1106
+ int8: int8$1,
1107
+ number: number$1,
1108
+ string: string$1,
1109
+ uint16: uint16$1,
1110
+ uint32: uint32$1,
803
1111
  uint64: uint64$1,
804
- float32: float32$1,
805
- float64: float64$1,
1112
+ uint8: uint8$1,
1113
+ utf8Length: utf8Length,
1114
+ utf8Write: utf8Write,
806
1115
  writeFloat32: writeFloat32,
807
- writeFloat64: writeFloat64,
808
- boolean: boolean$1,
809
- string: string$1,
810
- number: number$1
1116
+ writeFloat64: writeFloat64
811
1117
  });
812
1118
 
813
- class EncodeSchemaError extends Error {
814
- }
815
- function assertType(value, type, klass, field) {
816
- let typeofTarget;
817
- let allowNull = false;
818
- switch (type) {
819
- case "number":
820
- case "int8":
821
- case "uint8":
822
- case "int16":
823
- case "uint16":
824
- case "int32":
825
- case "uint32":
826
- case "int64":
827
- case "uint64":
828
- case "float32":
829
- case "float64":
830
- typeofTarget = "number";
831
- if (isNaN(value)) {
832
- console.log(`trying to encode "NaN" in ${klass.constructor.name}#${field}`);
833
- }
834
- break;
835
- case "string":
836
- typeofTarget = "string";
837
- allowNull = true;
838
- break;
839
- case "boolean":
840
- // boolean is always encoded as true/false based on truthiness
841
- return;
842
- }
843
- if (typeof (value) !== typeofTarget && (!allowNull || (allowNull && value !== null))) {
844
- let foundValue = `'${JSON.stringify(value)}'${(value && value.constructor && ` (${value.constructor.name})`) || ''}`;
845
- throw new EncodeSchemaError(`a '${typeofTarget}' was expected, but ${foundValue} was provided in ${klass.constructor.name}#${field}`);
846
- }
847
- }
848
- function assertInstanceType(value, type, klass, field) {
849
- if (!(value instanceof type)) {
850
- throw new EncodeSchemaError(`a '${type.name}' was expected, but '${value && value.constructor.name}' was provided in ${klass.constructor.name}#${field}`);
851
- }
852
- }
853
-
854
- function encodePrimitiveType(type, bytes, value, klass, field, it) {
855
- assertType(value, type, klass, field);
856
- const encodeFunc = encode[type];
857
- if (encodeFunc) {
858
- encodeFunc(bytes, value, it);
859
- // encodeFunc(bytes, value);
860
- }
861
- else {
862
- throw new EncodeSchemaError(`a '${type}' was expected, but ${value} was provided in ${klass.constructor.name}#${field}`);
1119
+ function encodeValue(encoder, bytes, type, value, operation, it) {
1120
+ if (typeof (type) === "string") {
1121
+ encode[type]?.(bytes, value, it);
863
1122
  }
864
- }
865
- function encodeValue(encoder, bytes, ref, type, value, field, operation, it) {
866
- if (type[Symbol.metadata] !== undefined) {
867
- // TODO: move this to the `@type()` annotation
868
- assertInstanceType(value, type, ref, field);
1123
+ else if (type[Symbol.metadata] !== undefined) {
869
1124
  //
870
1125
  // Encode refId for this instance.
871
1126
  // The actual instance is going to be encoded on next `changeTree` iteration.
@@ -876,21 +1131,7 @@
876
1131
  encoder.tryEncodeTypeId(bytes, type, value.constructor, it);
877
1132
  }
878
1133
  }
879
- else if (typeof (type) === "string") {
880
- //
881
- // Primitive values
882
- //
883
- encodePrimitiveType(type, bytes, value, ref, field, it);
884
- }
885
1134
  else {
886
- //
887
- // Custom type (MapSchema, ArraySchema, etc)
888
- //
889
- const definition = getType(Object.keys(type)[0]);
890
- //
891
- // ensure a ArraySchema has been provided
892
- //
893
- assertInstanceType(ref[field], definition.constructor, ref, field);
894
1135
  //
895
1136
  // Encode refId for this instance.
896
1137
  // The actual instance is going to be encoded on next `changeTree` iteration.
@@ -902,27 +1143,23 @@
902
1143
  * Used for Schema instances.
903
1144
  * @private
904
1145
  */
905
- const encodeSchemaOperation = function (encoder, bytes, changeTree, index, operation, it) {
906
- const ref = changeTree.ref;
907
- const metadata = ref['constructor'][Symbol.metadata];
908
- const field = metadata[index];
909
- const type = metadata[field].type;
910
- const value = ref[field];
1146
+ const encodeSchemaOperation = function (encoder, bytes, changeTree, index, operation, it, _, __, metadata) {
911
1147
  // "compress" field index + operation
912
1148
  bytes[it.offset++] = (index | operation) & 255;
913
1149
  // Do not encode value for DELETE operations
914
1150
  if (operation === exports.OPERATION.DELETE) {
915
1151
  return;
916
1152
  }
1153
+ const ref = changeTree.ref;
1154
+ const field = metadata[index];
917
1155
  // TODO: inline this function call small performance gain
918
- encodeValue(encoder, bytes, ref, type, value, field, operation, it);
1156
+ encodeValue(encoder, bytes, metadata[index].type, ref[field.name], operation, it);
919
1157
  };
920
1158
  /**
921
1159
  * Used for collections (MapSchema, CollectionSchema, SetSchema)
922
1160
  * @private
923
1161
  */
924
- const encodeKeyValueOperation = function (encoder, bytes, changeTree, field, operation, it) {
925
- const ref = changeTree.ref;
1162
+ const encodeKeyValueOperation = function (encoder, bytes, changeTree, index, operation, it) {
926
1163
  // encode operation
927
1164
  bytes[it.offset++] = operation & 255;
928
1165
  // custom operations
@@ -930,27 +1167,40 @@
930
1167
  return;
931
1168
  }
932
1169
  // encode index
933
- number$1(bytes, field, it);
1170
+ number$1(bytes, index, it);
934
1171
  // Do not encode value for DELETE operations
935
1172
  if (operation === exports.OPERATION.DELETE) {
936
1173
  return;
937
1174
  }
1175
+ const ref = changeTree.ref;
938
1176
  //
939
1177
  // encode "alias" for dynamic fields (maps)
940
1178
  //
941
- if ((operation & exports.OPERATION.ADD) == exports.OPERATION.ADD) { // ADD or DELETE_AND_ADD
1179
+ if ((operation & exports.OPERATION.ADD) === exports.OPERATION.ADD) { // ADD or DELETE_AND_ADD
942
1180
  if (typeof (ref['set']) === "function") {
943
1181
  //
944
1182
  // MapSchema dynamic key
945
1183
  //
946
- const dynamicIndex = changeTree.ref['$indexes'].get(field);
1184
+ const dynamicIndex = changeTree.ref['$indexes'].get(index);
947
1185
  string$1(bytes, dynamicIndex, it);
948
1186
  }
949
1187
  }
950
- const type = changeTree.getType(field);
951
- const value = changeTree.getValue(field);
1188
+ const type = ref[$childType];
1189
+ const value = ref[$getByIndex](index);
1190
+ // try { throw new Error(); } catch (e) {
1191
+ // // only print if not coming from Reflection.ts
1192
+ // if (!e.stack.includes("src/Reflection.ts")) {
1193
+ // console.log("encodeKeyValueOperation -> ", {
1194
+ // ref: changeTree.ref.constructor.name,
1195
+ // field,
1196
+ // operation: OPERATION[operation],
1197
+ // value: value?.toJSON(),
1198
+ // items: ref.toJSON(),
1199
+ // });
1200
+ // }
1201
+ // }
952
1202
  // TODO: inline this function call small performance gain
953
- encodeValue(encoder, bytes, ref, type, value, field, operation, it);
1203
+ encodeValue(encoder, bytes, type, value, operation, it);
954
1204
  };
955
1205
  /**
956
1206
  * Used for collections (MapSchema, ArraySchema, etc.)
@@ -958,24 +1208,29 @@
958
1208
  */
959
1209
  const encodeArray = function (encoder, bytes, changeTree, field, operation, it, isEncodeAll, hasView) {
960
1210
  const ref = changeTree.ref;
961
- if (hasView &&
962
- operation === exports.OPERATION.DELETE &&
963
- typeof (changeTree.getType(field)) !== "string") {
964
- // encode delete by refId (array of schemas)
965
- bytes[it.offset++] = exports.OPERATION.DELETE_BY_REFID;
966
- const value = ref['tmpItems'][field];
967
- const refId = value[$changes].refId;
968
- number$1(bytes, refId, it);
969
- return;
1211
+ const useOperationByRefId = hasView && changeTree.isFiltered && (typeof (changeTree.getType(field)) !== "string");
1212
+ let refOrIndex;
1213
+ if (useOperationByRefId) {
1214
+ refOrIndex = ref['tmpItems'][field][$changes].refId;
1215
+ if (operation === exports.OPERATION.DELETE) {
1216
+ operation = exports.OPERATION.DELETE_BY_REFID;
1217
+ }
1218
+ else if (operation === exports.OPERATION.ADD) {
1219
+ operation = exports.OPERATION.ADD_BY_REFID;
1220
+ }
1221
+ }
1222
+ else {
1223
+ refOrIndex = field;
970
1224
  }
971
1225
  // encode operation
972
1226
  bytes[it.offset++] = operation & 255;
973
1227
  // custom operations
974
- if (operation === exports.OPERATION.CLEAR) {
1228
+ if (operation === exports.OPERATION.CLEAR ||
1229
+ operation === exports.OPERATION.REVERSE) {
975
1230
  return;
976
1231
  }
977
1232
  // encode index
978
- number$1(bytes, field, it);
1233
+ number$1(bytes, refOrIndex, it);
979
1234
  // Do not encode value for DELETE operations
980
1235
  if (operation === exports.OPERATION.DELETE) {
981
1236
  return;
@@ -990,7 +1245,7 @@
990
1245
  // items: ref.toJSON(),
991
1246
  // });
992
1247
  // TODO: inline this function call small performance gain
993
- encodeValue(encoder, bytes, ref, type, value, field, operation, it);
1248
+ encodeValue(encoder, bytes, type, value, operation, it);
994
1249
  };
995
1250
 
996
1251
  /**
@@ -1224,31 +1479,31 @@
1224
1479
 
1225
1480
  var decode = /*#__PURE__*/Object.freeze({
1226
1481
  __proto__: null,
1227
- utf8Read: utf8Read,
1228
- int8: int8,
1229
- uint8: uint8,
1230
- int16: int16,
1231
- uint16: uint16,
1232
- int32: int32,
1233
- uint32: uint32,
1482
+ arrayCheck: arrayCheck,
1483
+ boolean: boolean,
1234
1484
  float32: float32,
1235
1485
  float64: float64,
1486
+ int16: int16,
1487
+ int32: int32,
1236
1488
  int64: int64,
1237
- uint64: uint64,
1489
+ int8: int8,
1490
+ number: number,
1491
+ numberCheck: numberCheck,
1238
1492
  readFloat32: readFloat32,
1239
1493
  readFloat64: readFloat64,
1240
- boolean: boolean,
1241
1494
  string: string,
1242
1495
  stringCheck: stringCheck,
1243
- number: number,
1244
- numberCheck: numberCheck,
1245
- arrayCheck: arrayCheck,
1246
- switchStructureCheck: switchStructureCheck
1496
+ switchStructureCheck: switchStructureCheck,
1497
+ uint16: uint16,
1498
+ uint32: uint32,
1499
+ uint64: uint64,
1500
+ uint8: uint8,
1501
+ utf8Read: utf8Read
1247
1502
  });
1248
1503
 
1249
1504
  const DEFINITION_MISMATCH = -1;
1250
1505
  function decodeValue(decoder, operation, ref, index, type, bytes, it, allChanges) {
1251
- const $root = decoder.$root;
1506
+ const $root = decoder.root;
1252
1507
  const previousValue = ref[$getByIndex](index);
1253
1508
  let value;
1254
1509
  if ((operation & exports.OPERATION.DELETE) === exports.OPERATION.DELETE) {
@@ -1296,7 +1551,9 @@
1296
1551
  if (!value) {
1297
1552
  value = decoder.createInstanceOfType(childType);
1298
1553
  }
1299
- $root.addRef(refId, value, (value !== previousValue));
1554
+ $root.addRef(refId, value, (value !== previousValue || // increment ref count if value has changed
1555
+ (operation === exports.OPERATION.DELETE_AND_ADD && value === previousValue) // increment ref count if the same instance is being added again
1556
+ ));
1300
1557
  }
1301
1558
  }
1302
1559
  else if (typeof (type) === "string") {
@@ -1347,18 +1604,19 @@
1347
1604
  }
1348
1605
  const decodeSchemaOperation = function (decoder, bytes, it, ref, allChanges) {
1349
1606
  const first_byte = bytes[it.offset++];
1350
- const metadata = ref['constructor'][Symbol.metadata];
1607
+ const metadata = ref.constructor[Symbol.metadata];
1351
1608
  // "compressed" index + operation
1352
1609
  const operation = (first_byte >> 6) << 6;
1353
1610
  const index = first_byte % (operation || 255);
1354
1611
  // skip early if field is not defined
1355
1612
  const field = metadata[index];
1356
1613
  if (field === undefined) {
1614
+ console.warn("@colyseus/schema: field not defined at", { index, ref: ref.constructor.name, metadata });
1357
1615
  return DEFINITION_MISMATCH;
1358
1616
  }
1359
- const { value, previousValue } = decodeValue(decoder, operation, ref, index, metadata[field].type, bytes, it, allChanges);
1617
+ const { value, previousValue } = decodeValue(decoder, operation, ref, index, field.type, bytes, it, allChanges);
1360
1618
  if (value !== null && value !== undefined) {
1361
- ref[field] = value;
1619
+ ref[field.name] = value;
1362
1620
  }
1363
1621
  // add change
1364
1622
  if (previousValue !== value) {
@@ -1366,7 +1624,7 @@
1366
1624
  ref,
1367
1625
  refId: decoder.currentRefId,
1368
1626
  op: operation,
1369
- field: field,
1627
+ field: field.name,
1370
1628
  value,
1371
1629
  previousValue,
1372
1630
  });
@@ -1434,7 +1692,8 @@
1434
1692
  };
1435
1693
  const decodeArray = function (decoder, bytes, it, ref, allChanges) {
1436
1694
  // "uncompressed" index + operation (array/map items)
1437
- const operation = bytes[it.offset++];
1695
+ let operation = bytes[it.offset++];
1696
+ let index;
1438
1697
  if (operation === exports.OPERATION.CLEAR) {
1439
1698
  //
1440
1699
  // When decoding:
@@ -1445,11 +1704,15 @@
1445
1704
  ref.clear();
1446
1705
  return;
1447
1706
  }
1707
+ else if (operation === exports.OPERATION.REVERSE) {
1708
+ ref.reverse();
1709
+ return;
1710
+ }
1448
1711
  else if (operation === exports.OPERATION.DELETE_BY_REFID) {
1449
1712
  // TODO: refactor here, try to follow same flow as below
1450
1713
  const refId = number(bytes, it);
1451
- const previousValue = decoder.$root.refs.get(refId);
1452
- const index = ref.findIndex((value) => value === previousValue);
1714
+ const previousValue = decoder.root.refs.get(refId);
1715
+ index = ref.findIndex((value) => value === previousValue);
1453
1716
  ref[$deleteByIndex](index);
1454
1717
  allChanges.push({
1455
1718
  ref,
@@ -1462,7 +1725,17 @@
1462
1725
  });
1463
1726
  return;
1464
1727
  }
1465
- const index = number(bytes, it);
1728
+ else if (operation === exports.OPERATION.ADD_BY_REFID) {
1729
+ const refId = number(bytes, it);
1730
+ const itemByRefId = decoder.root.refs.get(refId);
1731
+ // use existing index, or push new value
1732
+ index = (itemByRefId)
1733
+ ? ref.findIndex((value) => value === itemByRefId)
1734
+ : ref.length;
1735
+ }
1736
+ else {
1737
+ index = number(bytes, it);
1738
+ }
1466
1739
  const type = ref[$childType];
1467
1740
  let dynamicIndex = index;
1468
1741
  const { value, previousValue } = decodeValue(decoder, operation, ref, index, type, bytes, it, allChanges);
@@ -1486,6 +1759,47 @@
1486
1759
  }
1487
1760
  };
1488
1761
 
1762
+ class EncodeSchemaError extends Error {
1763
+ }
1764
+ function assertType(value, type, klass, field) {
1765
+ let typeofTarget;
1766
+ let allowNull = false;
1767
+ switch (type) {
1768
+ case "number":
1769
+ case "int8":
1770
+ case "uint8":
1771
+ case "int16":
1772
+ case "uint16":
1773
+ case "int32":
1774
+ case "uint32":
1775
+ case "int64":
1776
+ case "uint64":
1777
+ case "float32":
1778
+ case "float64":
1779
+ typeofTarget = "number";
1780
+ if (isNaN(value)) {
1781
+ console.log(`trying to encode "NaN" in ${klass.constructor.name}#${field}`);
1782
+ }
1783
+ break;
1784
+ case "string":
1785
+ typeofTarget = "string";
1786
+ allowNull = true;
1787
+ break;
1788
+ case "boolean":
1789
+ // boolean is always encoded as true/false based on truthiness
1790
+ return;
1791
+ }
1792
+ if (typeof (value) !== typeofTarget && (!allowNull || (allowNull && value !== null))) {
1793
+ let foundValue = `'${JSON.stringify(value)}'${(value && value.constructor && ` (${value.constructor.name})`) || ''}`;
1794
+ throw new EncodeSchemaError(`a '${typeofTarget}' was expected, but ${foundValue} was provided in ${klass.constructor.name}#${field}`);
1795
+ }
1796
+ }
1797
+ function assertInstanceType(value, type, instance, field) {
1798
+ if (!(value instanceof type)) {
1799
+ throw new EncodeSchemaError(`a '${type.name}' was expected, but '${value && value.constructor.name}' was provided in ${instance.constructor.name}#${field}`);
1800
+ }
1801
+ }
1802
+
1489
1803
  var _a$4, _b$4;
1490
1804
  const DEFAULT_SORT = (a, b) => {
1491
1805
  const A = a.toString();
@@ -1536,6 +1850,7 @@
1536
1850
  const proxy = new Proxy(this, {
1537
1851
  get: (obj, prop) => {
1538
1852
  if (typeof (prop) !== "symbol" &&
1853
+ // FIXME: d8 accuses this as low performance
1539
1854
  !isNaN(prop) // https://stackoverflow.com/a/175787/892698
1540
1855
  ) {
1541
1856
  return this.items[prop];
@@ -1551,8 +1866,9 @@
1551
1866
  }
1552
1867
  else {
1553
1868
  if (setValue[$changes]) {
1869
+ assertInstanceType(setValue, obj[$childType], obj, key);
1554
1870
  if (obj.items[key] !== undefined) {
1555
- if (setValue[$changes][$isNew]) {
1871
+ if (setValue[$changes].isNew) {
1556
1872
  this[$changes].indexedOperation(Number(key), exports.OPERATION.MOVE_AND_ADD);
1557
1873
  }
1558
1874
  else {
@@ -1564,7 +1880,7 @@
1564
1880
  }
1565
1881
  }
1566
1882
  }
1567
- else if (setValue[$changes][$isNew]) {
1883
+ else if (setValue[$changes].isNew) {
1568
1884
  this[$changes].indexedOperation(Number(key), exports.OPERATION.ADD);
1569
1885
  }
1570
1886
  }
@@ -1597,7 +1913,10 @@
1597
1913
  }
1598
1914
  });
1599
1915
  this[$changes] = new ChangeTree(proxy);
1600
- this.push.apply(this, items);
1916
+ this[$changes].indexes = {};
1917
+ if (items.length > 0) {
1918
+ this.push(...items);
1919
+ }
1601
1920
  return proxy;
1602
1921
  }
1603
1922
  set length(newLength) {
@@ -1616,14 +1935,19 @@
1616
1935
  }
1617
1936
  push(...values) {
1618
1937
  let length = this.tmpItems.length;
1619
- values.forEach((value, i) => {
1620
- // skip null values
1938
+ const changeTree = this[$changes];
1939
+ // values.forEach((value, i) => {
1940
+ for (let i = 0, l = values.length; i < values.length; i++, length++) {
1941
+ const value = values[i];
1621
1942
  if (value === undefined || value === null) {
1943
+ // skip null values
1622
1944
  return;
1623
1945
  }
1624
- const changeTree = this[$changes];
1946
+ else if (typeof (value) === "object" && this[$childType]) {
1947
+ assertInstanceType(value, this[$childType], this, i);
1948
+ // TODO: move value[$changes]?.setParent() to this block.
1949
+ }
1625
1950
  changeTree.indexedOperation(length, exports.OPERATION.ADD, this.items.length);
1626
- // changeTree.indexes[length] = length;
1627
1951
  this.items.push(value);
1628
1952
  this.tmpItems.push(value);
1629
1953
  //
@@ -1631,8 +1955,9 @@
1631
1955
  // (to avoid encoding "refId" operations before parent's "ADD" operation)
1632
1956
  //
1633
1957
  value[$changes]?.setParent(this, changeTree.root, length);
1634
- length++;
1635
- });
1958
+ }
1959
+ // length++;
1960
+ // });
1636
1961
  return length;
1637
1962
  }
1638
1963
  /**
@@ -1653,6 +1978,7 @@
1653
1978
  }
1654
1979
  this[$changes].delete(index, undefined, this.items.length - 1);
1655
1980
  // this.tmpItems[index] = undefined;
1981
+ // this.tmpItems.pop();
1656
1982
  this.deletedIndexes[index] = true;
1657
1983
  return this.items.pop();
1658
1984
  }
@@ -1717,9 +2043,12 @@
1717
2043
  //
1718
2044
  // TODO: do not use [$changes] at decoding time.
1719
2045
  //
1720
- changeTree.root?.changes.delete(changeTree);
1721
- changeTree.root?.allChanges.delete(changeTree);
1722
- changeTree.root?.allFilteredChanges.delete(changeTree);
2046
+ const root = changeTree.root;
2047
+ if (root !== undefined) {
2048
+ root.removeChangeFromChangeSet("changes", changeTree);
2049
+ root.removeChangeFromChangeSet("allChanges", changeTree);
2050
+ root.removeChangeFromChangeSet("allFilteredChanges", changeTree);
2051
+ }
1723
2052
  });
1724
2053
  changeTree.discard(true);
1725
2054
  changeTree.operation(exports.OPERATION.CLEAR);
@@ -1763,6 +2092,7 @@
1763
2092
  const changeTree = this[$changes];
1764
2093
  changeTree.delete(index);
1765
2094
  changeTree.shiftAllChangeIndexes(-1, index);
2095
+ // this.deletedIndexes[index] = true;
1766
2096
  return this.items.shift();
1767
2097
  }
1768
2098
  /**
@@ -1843,10 +2173,12 @@
1843
2173
  changeTree.shiftChangeIndexes(items.length);
1844
2174
  // new index
1845
2175
  if (changeTree.isFiltered) {
1846
- changeTree.filteredChanges.set(this.items.length, exports.OPERATION.ADD);
2176
+ setOperationAtIndex(changeTree.filteredChanges, this.items.length);
2177
+ // changeTree.filteredChanges[this.items.length] = OPERATION.ADD;
1847
2178
  }
1848
2179
  else {
1849
- changeTree.allChanges.set(this.items.length, exports.OPERATION.ADD);
2180
+ setOperationAtIndex(changeTree.allChanges, this.items.length);
2181
+ // changeTree.allChanges[this.items.length] = OPERATION.ADD;
1850
2182
  }
1851
2183
  // FIXME: should we use OPERATION.MOVE here instead?
1852
2184
  items.forEach((_, index) => {
@@ -1871,14 +2203,6 @@
1871
2203
  lastIndexOf(searchElement, fromIndex = this.length - 1) {
1872
2204
  return this.items.lastIndexOf(searchElement, fromIndex);
1873
2205
  }
1874
- /**
1875
- * Determines whether all the members of an array satisfy the specified test.
1876
- * @param callbackfn A function that accepts up to three arguments. The every method calls
1877
- * the callbackfn function for each element in the array until the callbackfn returns a value
1878
- * which is coercible to the Boolean value false, or until the end of the array.
1879
- * @param thisArg An object to which the this keyword can refer in the callbackfn function.
1880
- * If thisArg is omitted, undefined is used as the this value.
1881
- */
1882
2206
  every(callbackfn, thisArg) {
1883
2207
  return this.items.every(callbackfn, thisArg);
1884
2208
  }
@@ -2157,6 +2481,7 @@
2157
2481
  this.$items = new Map();
2158
2482
  this.$indexes = new Map();
2159
2483
  this[$changes] = new ChangeTree(this);
2484
+ this[$changes].indexes = {};
2160
2485
  if (initialValues) {
2161
2486
  if (initialValues instanceof Map ||
2162
2487
  initialValues instanceof MapSchema) {
@@ -2183,6 +2508,9 @@
2183
2508
  if (value === undefined || value === null) {
2184
2509
  throw new Error(`MapSchema#set('${key}', ${value}): trying to set ${value} value on '${key}'.`);
2185
2510
  }
2511
+ else if (typeof (value) === "object" && this[$childType]) {
2512
+ assertInstanceType(value, this[$childType], this, key);
2513
+ }
2186
2514
  // Force "key" as string
2187
2515
  // See: https://github.com/colyseus/colyseus/issues/561#issuecomment-1646733468
2188
2516
  key = key.toString();
@@ -2191,7 +2519,7 @@
2191
2519
  const isReplace = typeof (changeTree.indexes[key]) !== "undefined";
2192
2520
  const index = (isReplace)
2193
2521
  ? changeTree.indexes[key]
2194
- : changeTree.indexes[-1] ?? 0;
2522
+ : changeTree.indexes[$numFields] ?? 0;
2195
2523
  let operation = (isReplace)
2196
2524
  ? exports.OPERATION.REPLACE
2197
2525
  : exports.OPERATION.ADD;
@@ -2203,7 +2531,7 @@
2203
2531
  if (!isReplace) {
2204
2532
  this.$indexes.set(index, key);
2205
2533
  changeTree.indexes[key] = index;
2206
- changeTree.indexes[-1] = index + 1;
2534
+ changeTree.indexes[$numFields] = index + 1;
2207
2535
  }
2208
2536
  else if (!isRef &&
2209
2537
  this.$items.get(key) === value) {
@@ -2278,8 +2606,11 @@
2278
2606
  }
2279
2607
  [$onEncodeEnd]() {
2280
2608
  const changeTree = this[$changes];
2281
- const changes = changeTree.changes.entries();
2282
- for (const [fieldIndex, operation] of changes) {
2609
+ const keys = Object.keys(changeTree.indexedOperations);
2610
+ for (let i = 0, len = keys.length; i < len; i++) {
2611
+ const key = keys[i];
2612
+ const fieldIndex = Number(key);
2613
+ const operation = changeTree.indexedOperations[key];
2283
2614
  if (operation === exports.OPERATION.DELETE) {
2284
2615
  const index = this[$getByIndex](fieldIndex);
2285
2616
  delete changeTree.indexes[index];
@@ -2305,111 +2636,24 @@
2305
2636
  // client-side
2306
2637
  cloned = Object.assign(new MapSchema(), this);
2307
2638
  }
2308
- else {
2309
- // server-side
2310
- cloned = new MapSchema();
2311
- this.forEach((value, key) => {
2312
- if (value[$changes]) {
2313
- cloned.set(key, value['clone']());
2314
- }
2315
- else {
2316
- cloned.set(key, value);
2317
- }
2318
- });
2319
- }
2320
- return cloned;
2321
- }
2322
- }
2323
- registerType("map", { constructor: MapSchema });
2324
-
2325
- const DEFAULT_VIEW_TAG = -1;
2326
- class TypeContext {
2327
- /**
2328
- * For inheritance support
2329
- * Keeps track of which classes extends which. (parent -> children)
2330
- */
2331
- static { this.inheritedTypes = new Map(); }
2332
- static register(target) {
2333
- const parent = Object.getPrototypeOf(target);
2334
- if (parent !== Schema) {
2335
- let inherits = TypeContext.inheritedTypes.get(parent);
2336
- if (!inherits) {
2337
- inherits = new Set();
2338
- TypeContext.inheritedTypes.set(parent, inherits);
2339
- }
2340
- inherits.add(target);
2341
- }
2342
- }
2343
- constructor(rootClass) {
2344
- this.types = {};
2345
- this.schemas = new Map();
2346
- this.hasFilters = false;
2347
- if (rootClass) {
2348
- this.discoverTypes(rootClass);
2349
- }
2350
- }
2351
- has(schema) {
2352
- return this.schemas.has(schema);
2353
- }
2354
- get(typeid) {
2355
- return this.types[typeid];
2356
- }
2357
- add(schema, typeid = this.schemas.size) {
2358
- // skip if already registered
2359
- if (this.schemas.has(schema)) {
2360
- return false;
2361
- }
2362
- this.types[typeid] = schema;
2363
- this.schemas.set(schema, typeid);
2364
- return true;
2365
- }
2366
- getTypeId(klass) {
2367
- return this.schemas.get(klass);
2368
- }
2369
- discoverTypes(klass) {
2370
- if (!this.add(klass)) {
2371
- return;
2372
- }
2373
- // add classes inherited from this base class
2374
- TypeContext.inheritedTypes.get(klass)?.forEach((child) => {
2375
- this.discoverTypes(child);
2376
- });
2377
- // skip if no fields are defined for this class.
2378
- if (klass[Symbol.metadata] === undefined) {
2379
- klass[Symbol.metadata] = {};
2380
- }
2381
- // const metadata = Metadata.getFor(klass);
2382
- const metadata = klass[Symbol.metadata];
2383
- // if any schema/field has filters, mark "context" as having filters.
2384
- if (metadata[-2]) {
2385
- this.hasFilters = true;
2386
- }
2387
- for (const field in metadata) {
2388
- const fieldType = metadata[field].type;
2389
- if (typeof (fieldType) === "string") {
2390
- continue;
2391
- }
2392
- if (Array.isArray(fieldType)) {
2393
- const type = fieldType[0];
2394
- if (type === "string") {
2395
- continue;
2396
- }
2397
- this.discoverTypes(type);
2398
- }
2399
- else if (typeof (fieldType) === "function") {
2400
- this.discoverTypes(fieldType);
2401
- }
2402
- else {
2403
- const type = Object.values(fieldType)[0];
2404
- // skip primitive types
2405
- if (typeof (type) === "string") {
2406
- continue;
2639
+ else {
2640
+ // server-side
2641
+ cloned = new MapSchema();
2642
+ this.forEach((value, key) => {
2643
+ if (value[$changes]) {
2644
+ cloned.set(key, value['clone']());
2407
2645
  }
2408
- this.discoverTypes(type);
2409
- }
2646
+ else {
2647
+ cloned.set(key, value);
2648
+ }
2649
+ });
2410
2650
  }
2651
+ return cloned;
2411
2652
  }
2412
2653
  }
2654
+ registerType("map", { constructor: MapSchema });
2655
+
2656
+ const DEFAULT_VIEW_TAG = -1;
2413
2657
  /**
2414
2658
  * [See documentation](https://docs.colyseus.io/state/schema/)
2415
2659
  *
@@ -2436,8 +2680,8 @@
2436
2680
  // // detect index for this field, considering inheritance
2437
2681
  // //
2438
2682
  // const parent = Object.getPrototypeOf(context.metadata);
2439
- // let fieldIndex: number = context.metadata[-1] // current structure already has fields defined
2440
- // ?? (parent && parent[-1]) // parent structure has fields defined
2683
+ // let fieldIndex: number = context.metadata[$numFields] // current structure already has fields defined
2684
+ // ?? (parent && parent[$numFields]) // parent structure has fields defined
2441
2685
  // ?? -1; // no fields defined
2442
2686
  // fieldIndex++;
2443
2687
  // if (
@@ -2557,18 +2801,20 @@
2557
2801
  const constructor = target.constructor;
2558
2802
  const parentClass = Object.getPrototypeOf(constructor);
2559
2803
  const parentMetadata = parentClass[Symbol.metadata];
2804
+ // TODO: use Metadata.initialize()
2560
2805
  const metadata = (constructor[Symbol.metadata] ??= Object.assign({}, constructor[Symbol.metadata], parentMetadata ?? Object.create(null)));
2561
- if (!metadata[fieldName]) {
2562
- //
2563
- // detect index for this field, considering inheritance
2564
- //
2565
- metadata[fieldName] = {
2566
- type: undefined,
2567
- index: (metadata[-1] // current structure already has fields defined
2568
- ?? (parentMetadata && parentMetadata[-1]) // parent structure has fields defined
2569
- ?? -1) + 1 // no fields defined
2570
- };
2571
- }
2806
+ // const fieldIndex = metadata[fieldName];
2807
+ // if (!metadata[fieldIndex]) {
2808
+ // //
2809
+ // // detect index for this field, considering inheritance
2810
+ // //
2811
+ // metadata[fieldIndex] = {
2812
+ // type: undefined,
2813
+ // index: (metadata[$numFields] // current structure already has fields defined
2814
+ // ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
2815
+ // ?? -1) + 1 // no fields defined
2816
+ // }
2817
+ // }
2572
2818
  Metadata.setTag(metadata, fieldName, tag);
2573
2819
  };
2574
2820
  }
@@ -2582,17 +2828,17 @@
2582
2828
  TypeContext.register(constructor);
2583
2829
  const parentClass = Object.getPrototypeOf(constructor);
2584
2830
  const parentMetadata = parentClass[Symbol.metadata];
2585
- const metadata = (constructor[Symbol.metadata] ??= Object.assign({}, constructor[Symbol.metadata], parentMetadata ?? Object.create(null)));
2586
- let fieldIndex;
2831
+ const metadata = Metadata.initialize(constructor);
2832
+ let fieldIndex = metadata[field];
2587
2833
  /**
2588
2834
  * skip if descriptor already exists for this field (`@deprecated()`)
2589
2835
  */
2590
- if (metadata[field]) {
2591
- if (metadata[field].deprecated) {
2836
+ if (metadata[fieldIndex] !== undefined) {
2837
+ if (metadata[fieldIndex].deprecated) {
2592
2838
  // do not create accessors for deprecated properties.
2593
2839
  return;
2594
2840
  }
2595
- else if (metadata[field].descriptor !== undefined) {
2841
+ else if (metadata[fieldIndex].type !== undefined) {
2596
2842
  // trying to define same property multiple times across inheritance.
2597
2843
  // https://github.com/colyseus/colyseus-unity3d/issues/131#issuecomment-814308572
2598
2844
  try {
@@ -2603,16 +2849,13 @@
2603
2849
  throw new Error(`${e.message} ${definitionAtLine}`);
2604
2850
  }
2605
2851
  }
2606
- else {
2607
- fieldIndex = metadata[field].index;
2608
- }
2609
2852
  }
2610
2853
  else {
2611
2854
  //
2612
2855
  // detect index for this field, considering inheritance
2613
2856
  //
2614
- fieldIndex = metadata[-1] // current structure already has fields defined
2615
- ?? (parentMetadata && parentMetadata[-1]) // parent structure has fields defined
2857
+ fieldIndex = metadata[$numFields] // current structure already has fields defined
2858
+ ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
2616
2859
  ?? -1; // no fields defined
2617
2860
  fieldIndex++;
2618
2861
  }
@@ -2631,15 +2874,15 @@
2631
2874
  const childType = (complexTypeKlass)
2632
2875
  ? Object.values(type)[0]
2633
2876
  : type;
2634
- Metadata.addField(metadata, fieldIndex, field, type, getPropertyDescriptor(`_${field}`, fieldIndex, childType, complexTypeKlass, metadata, field));
2877
+ Metadata.addField(metadata, fieldIndex, field, type, getPropertyDescriptor(`_${field}`, fieldIndex, childType, complexTypeKlass));
2635
2878
  }
2636
2879
  };
2637
2880
  }
2638
- function getPropertyDescriptor(fieldCached, fieldIndex, type, complexTypeKlass, metadata, field) {
2881
+ function getPropertyDescriptor(fieldCached, fieldIndex, type, complexTypeKlass) {
2639
2882
  return {
2640
2883
  get: function () { return this[fieldCached]; },
2641
2884
  set: function (value) {
2642
- const previousValue = this[fieldCached] || undefined;
2885
+ const previousValue = this[fieldCached] ?? undefined;
2643
2886
  // skip if value is the same as cached.
2644
2887
  if (value === previousValue) {
2645
2888
  return;
@@ -2657,22 +2900,27 @@
2657
2900
  }
2658
2901
  value[$childType] = type;
2659
2902
  }
2903
+ else if (typeof (type) !== "string") {
2904
+ assertInstanceType(value, type, this, fieldCached.substring(1));
2905
+ }
2906
+ else {
2907
+ assertType(value, type, this, fieldCached.substring(1));
2908
+ }
2909
+ const changeTree = this[$changes];
2660
2910
  //
2661
2911
  // Replacing existing "ref", remove it from root.
2662
2912
  // TODO: if there are other references to this instance, we should not remove it from root.
2663
2913
  //
2664
2914
  if (previousValue !== undefined && previousValue[$changes]) {
2665
- this[$changes].root?.remove(previousValue[$changes]);
2915
+ changeTree.root?.remove(previousValue[$changes]);
2666
2916
  }
2667
2917
  // flag the change for encoding.
2668
- this.constructor[$track](this[$changes], fieldIndex, exports.OPERATION.ADD);
2918
+ this.constructor[$track](changeTree, fieldIndex, exports.OPERATION.ADD);
2669
2919
  //
2670
2920
  // call setParent() recursively for this and its child
2671
2921
  // structures.
2672
2922
  //
2673
- if (value[$changes]) {
2674
- value[$changes].setParent(this, this[$changes].root, metadata[field].index);
2675
- }
2923
+ value[$changes]?.setParent(this, changeTree.root, fieldIndex);
2676
2924
  }
2677
2925
  else if (previousValue !== undefined) {
2678
2926
  //
@@ -2699,20 +2947,22 @@
2699
2947
  const parentClass = Object.getPrototypeOf(constructor);
2700
2948
  const parentMetadata = parentClass[Symbol.metadata];
2701
2949
  const metadata = (constructor[Symbol.metadata] ??= Object.assign({}, constructor[Symbol.metadata], parentMetadata ?? Object.create(null)));
2702
- if (!metadata[field]) {
2703
- //
2704
- // detect index for this field, considering inheritance
2705
- //
2706
- metadata[field] = {
2707
- type: undefined,
2708
- index: (metadata[-1] // current structure already has fields defined
2709
- ?? (parentMetadata && parentMetadata[-1]) // parent structure has fields defined
2710
- ?? -1) + 1 // no fields defined
2711
- };
2712
- }
2713
- metadata[field].deprecated = true;
2950
+ const fieldIndex = metadata[field];
2951
+ // if (!metadata[field]) {
2952
+ // //
2953
+ // // detect index for this field, considering inheritance
2954
+ // //
2955
+ // metadata[field] = {
2956
+ // type: undefined,
2957
+ // index: (metadata[$numFields] // current structure already has fields defined
2958
+ // ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
2959
+ // ?? -1) + 1 // no fields defined
2960
+ // }
2961
+ // }
2962
+ metadata[fieldIndex].deprecated = true;
2714
2963
  if (throws) {
2715
- metadata[field].descriptor = {
2964
+ metadata[$descriptors] ??= {};
2965
+ metadata[$descriptors][field] = {
2716
2966
  get: function () { throw new Error(`${field} is deprecated.`); },
2717
2967
  set: function (value) { },
2718
2968
  enumerable: false,
@@ -2720,8 +2970,8 @@
2720
2970
  };
2721
2971
  }
2722
2972
  // flag metadata[field] as non-enumerable
2723
- Object.defineProperty(metadata, field, {
2724
- value: metadata[field],
2973
+ Object.defineProperty(metadata, fieldIndex, {
2974
+ value: metadata[fieldIndex],
2725
2975
  enumerable: false,
2726
2976
  configurable: true
2727
2977
  });
@@ -2733,6 +2983,37 @@
2733
2983
  }
2734
2984
  return target;
2735
2985
  }
2986
+ function schema(fields, name, inherits = Schema) {
2987
+ const defaultValues = {};
2988
+ const viewTagFields = {};
2989
+ for (let fieldName in fields) {
2990
+ const field = fields[fieldName];
2991
+ if (typeof (field) === "object") {
2992
+ if (field['default'] !== undefined) {
2993
+ defaultValues[fieldName] = field['default'];
2994
+ }
2995
+ if (field['view'] !== undefined) {
2996
+ viewTagFields[fieldName] = (typeof (field['view']) === "boolean")
2997
+ ? DEFAULT_VIEW_TAG
2998
+ : field['view'];
2999
+ }
3000
+ }
3001
+ }
3002
+ const klass = Metadata.setFields(class extends inherits {
3003
+ constructor(...args) {
3004
+ args[0] = Object.assign({}, defaultValues, args[0]);
3005
+ super(...args);
3006
+ }
3007
+ }, fields);
3008
+ for (let fieldName in viewTagFields) {
3009
+ view(viewTagFields[fieldName])(klass.prototype, fieldName);
3010
+ }
3011
+ if (name) {
3012
+ Object.defineProperty(klass, "name", { value: name });
3013
+ }
3014
+ klass.extends = (fields, name) => schema(fields, name, klass);
3015
+ return klass;
3016
+ }
2736
3017
 
2737
3018
  function getIndent(level) {
2738
3019
  return (new Array(level).fill(0)).map((_, i) => (i === level - 1) ? `└─ ` : ` `).join("");
@@ -2743,15 +3024,18 @@
2743
3024
  ops: {},
2744
3025
  refs: []
2745
3026
  };
2746
- $root.changes.forEach((operations, changeTree) => {
3027
+ // for (const refId in $root.changes) {
3028
+ $root.changes.forEach(changeTree => {
3029
+ const changes = changeTree.indexedOperations;
2747
3030
  dump.refs.push(`refId#${changeTree.refId}`);
2748
- operations.forEach((op, index) => {
3031
+ for (const index in changes) {
3032
+ const op = changes[index];
2749
3033
  const opName = exports.OPERATION[op];
2750
3034
  if (!dump.ops[opName]) {
2751
3035
  dump.ops[opName] = 0;
2752
3036
  }
2753
3037
  dump.ops[exports.OPERATION[op]]++;
2754
- });
3038
+ }
2755
3039
  });
2756
3040
  return dump;
2757
3041
  }
@@ -2777,6 +3061,7 @@
2777
3061
  class Schema {
2778
3062
  static { this[_a$2] = encodeSchemaOperation; }
2779
3063
  static { this[_b$2] = decodeSchemaOperation; }
3064
+ // public [$changes]: ChangeTree;
2780
3065
  /**
2781
3066
  * Assign the property descriptors required to track changes on this instance.
2782
3067
  * @param instance
@@ -2787,35 +3072,7 @@
2787
3072
  enumerable: false,
2788
3073
  writable: true
2789
3074
  });
2790
- const metadata = instance.constructor[Symbol.metadata];
2791
- // Define property descriptors
2792
- for (const field in metadata) {
2793
- if (metadata[field].descriptor) {
2794
- // for encoder
2795
- Object.defineProperty(instance, `_${field}`, {
2796
- value: undefined,
2797
- writable: true,
2798
- enumerable: false,
2799
- configurable: true,
2800
- });
2801
- Object.defineProperty(instance, field, metadata[field].descriptor);
2802
- }
2803
- else {
2804
- // for decoder
2805
- Object.defineProperty(instance, field, {
2806
- value: undefined,
2807
- writable: true,
2808
- enumerable: true,
2809
- configurable: true,
2810
- });
2811
- }
2812
- // Object.defineProperty(instance, field, {
2813
- // ...instance.constructor[Symbol.metadata][field].descriptor
2814
- // });
2815
- // if (args[0]?.hasOwnProperty(field)) {
2816
- // instance[field] = args[0][field];
2817
- // }
2818
- }
3075
+ Object.defineProperties(instance, instance.constructor[Symbol.metadata]?.[$descriptors] || {});
2819
3076
  }
2820
3077
  static is(type) {
2821
3078
  return typeof (type[Symbol.metadata]) === "object";
@@ -2839,7 +3096,7 @@
2839
3096
  */
2840
3097
  static [$filter](ref, index, view) {
2841
3098
  const metadata = ref.constructor[Symbol.metadata];
2842
- const tag = metadata[metadata[index]].tag;
3099
+ const tag = metadata[index]?.tag;
2843
3100
  if (view === undefined) {
2844
3101
  // shared pass/encode: encode if doesn't have a tag
2845
3102
  return tag === undefined;
@@ -2860,12 +3117,16 @@
2860
3117
  }
2861
3118
  // allow inherited classes to have a constructor
2862
3119
  constructor(...args) {
3120
+ //
3121
+ // inline
3122
+ // Schema.initialize(this);
3123
+ //
2863
3124
  Schema.initialize(this);
2864
3125
  //
2865
3126
  // Assign initial values
2866
3127
  //
2867
3128
  if (args[0]) {
2868
- this.assign(args[0]);
3129
+ Object.assign(this, args[0]);
2869
3130
  }
2870
3131
  }
2871
3132
  assign(props) {
@@ -2879,7 +3140,8 @@
2879
3140
  * @param operation OPERATION to perform (detected automatically)
2880
3141
  */
2881
3142
  setDirty(property, operation) {
2882
- this[$changes].change(this.constructor[Symbol.metadata][property].index, operation);
3143
+ const metadata = this.constructor[Symbol.metadata];
3144
+ this[$changes].change(metadata[metadata[property]].index, operation);
2883
3145
  }
2884
3146
  clone() {
2885
3147
  const cloned = new (this.constructor);
@@ -2888,7 +3150,9 @@
2888
3150
  // TODO: clone all properties, not only annotated ones
2889
3151
  //
2890
3152
  // for (const field in this) {
2891
- for (const field in metadata) {
3153
+ for (const fieldIndex in metadata) {
3154
+ // const field = metadata[metadata[fieldIndex]].name;
3155
+ const field = metadata[fieldIndex].name;
2892
3156
  if (typeof (this[field]) === "object" &&
2893
3157
  typeof (this[field]?.clone) === "function") {
2894
3158
  // deep clone
@@ -2902,10 +3166,11 @@
2902
3166
  return cloned;
2903
3167
  }
2904
3168
  toJSON() {
2905
- const metadata = this.constructor[Symbol.metadata];
2906
3169
  const obj = {};
2907
- for (const fieldName in metadata) {
2908
- const field = metadata[fieldName];
3170
+ const metadata = this.constructor[Symbol.metadata];
3171
+ for (const index in metadata) {
3172
+ const field = metadata[index];
3173
+ const fieldName = field.name;
2909
3174
  if (!field.deprecated && this[fieldName] !== null && typeof (this[fieldName]) !== "undefined") {
2910
3175
  obj[fieldName] = (typeof (this[fieldName]['toJSON']) === "function")
2911
3176
  ? this[fieldName]['toJSON']()
@@ -2918,17 +3183,19 @@
2918
3183
  this[$changes].discardAll();
2919
3184
  }
2920
3185
  [$getByIndex](index) {
2921
- return this[this.constructor[Symbol.metadata][index]];
3186
+ const metadata = this.constructor[Symbol.metadata];
3187
+ return this[metadata[index].name];
2922
3188
  }
2923
3189
  [$deleteByIndex](index) {
2924
- this[this.constructor[Symbol.metadata][index]] = undefined;
3190
+ const metadata = this.constructor[Symbol.metadata];
3191
+ this[metadata[index].name] = undefined;
2925
3192
  }
2926
3193
  static debugRefIds(instance, jsonContents = true, level = 0) {
2927
3194
  const ref = instance;
2928
3195
  const changeTree = ref[$changes];
2929
3196
  const contents = (jsonContents) ? ` - ${JSON.stringify(ref.toJSON())}` : "";
2930
3197
  let output = "";
2931
- output += `${getIndent(level)}${ref.constructor.name} (${ref[$changes].refId})${contents}\n`;
3198
+ output += `${getIndent(level)}${ref.constructor.name} (refId: ${ref[$changes].refId})${contents}\n`;
2932
3199
  changeTree.forEachChild((childChangeTree) => output += this.debugRefIds(childChangeTree.ref, jsonContents, level + 1));
2933
3200
  return output;
2934
3201
  }
@@ -2946,30 +3213,40 @@
2946
3213
  const changeSetName = (isEncodeAll) ? "allChanges" : "changes";
2947
3214
  let output = `${instance.constructor.name} (${changeTree.refId}) -> .${changeSetName}:\n`;
2948
3215
  function dumpChangeSet(changeSet) {
2949
- Array.from(changeSet)
2950
- .sort((a, b) => a[0] - b[0])
2951
- .forEach(([index, operation]) => output += `- [${index}]: ${exports.OPERATION[operation]} (${JSON.stringify(changeTree.getValue(index, isEncodeAll))})\n`);
3216
+ changeSet.operations
3217
+ .filter(op => op)
3218
+ .forEach((index) => {
3219
+ const operation = changeTree.indexedOperations[index];
3220
+ console.log({ index, operation });
3221
+ output += `- [${index}]: ${exports.OPERATION[operation]} (${JSON.stringify(changeTree.getValue(Number(index), isEncodeAll))})\n`;
3222
+ });
2952
3223
  }
2953
3224
  dumpChangeSet(changeSet);
2954
3225
  // display filtered changes
2955
- if (!isEncodeAll && changeTree.filteredChanges?.size > 0) {
3226
+ if (!isEncodeAll &&
3227
+ changeTree.filteredChanges &&
3228
+ (changeTree.filteredChanges.operations).filter(op => op).length > 0) {
2956
3229
  output += `${instance.constructor.name} (${changeTree.refId}) -> .filteredChanges:\n`;
2957
3230
  dumpChangeSet(changeTree.filteredChanges);
2958
3231
  }
2959
3232
  // display filtered changes
2960
- if (isEncodeAll && changeTree.allFilteredChanges?.size > 0) {
3233
+ if (isEncodeAll &&
3234
+ changeTree.allFilteredChanges &&
3235
+ (changeTree.allFilteredChanges.operations).filter(op => op).length > 0) {
2961
3236
  output += `${instance.constructor.name} (${changeTree.refId}) -> .allFilteredChanges:\n`;
2962
3237
  dumpChangeSet(changeTree.allFilteredChanges);
2963
3238
  }
2964
3239
  return output;
2965
3240
  }
2966
- static debugChangesDeep(ref) {
3241
+ static debugChangesDeep(ref, changeSetName = "changes") {
2967
3242
  let output = "";
2968
3243
  const rootChangeTree = ref[$changes];
3244
+ const root = rootChangeTree.root;
2969
3245
  const changeTrees = new Map();
2970
3246
  let totalInstances = 0;
2971
3247
  let totalOperations = 0;
2972
- for (const [changeTree, changes] of (rootChangeTree.root.changes.entries())) {
3248
+ for (const [refId, changes] of Object.entries(root[changeSetName])) {
3249
+ const changeTree = root.changeTrees[refId];
2973
3250
  let includeChangeTree = false;
2974
3251
  let parentChangeTrees = [];
2975
3252
  let parentChangeTree = changeTree.parent?.[$changes];
@@ -2988,7 +3265,7 @@
2988
3265
  }
2989
3266
  if (includeChangeTree) {
2990
3267
  totalInstances += 1;
2991
- totalOperations += changes.size;
3268
+ totalOperations += Object.keys(changes).length;
2992
3269
  changeTrees.set(changeTree, parentChangeTrees.reverse());
2993
3270
  }
2994
3271
  }
@@ -3006,12 +3283,13 @@
3006
3283
  visitedParents.add(parentChangeTree);
3007
3284
  }
3008
3285
  });
3009
- const changes = changeTree.changes;
3286
+ const changes = changeTree.indexedOperations;
3010
3287
  const level = parentChangeTrees.length;
3011
3288
  const indent = getIndent(level);
3012
3289
  const parentIndex = (level > 0) ? `(${changeTree.parentIndex}) ` : "";
3013
- output += `${indent}${parentIndex}${changeTree.ref.constructor.name} (refId: ${changeTree.refId}) - changes: ${changes.size}\n`;
3014
- for (const [index, operation] of changes) {
3290
+ output += `${indent}${parentIndex}${changeTree.ref.constructor.name} (refId: ${changeTree.refId}) - changes: ${Object.keys(changes).length}\n`;
3291
+ for (const index in changes) {
3292
+ const operation = changes[index];
3015
3293
  output += `${getIndent(level + 1)}${exports.OPERATION[operation]}: ${index}\n`;
3016
3294
  }
3017
3295
  }
@@ -3045,6 +3323,7 @@
3045
3323
  this.$indexes = new Map();
3046
3324
  this.$refId = 0;
3047
3325
  this[$changes] = new ChangeTree(this);
3326
+ this[$changes].indexes = {};
3048
3327
  if (initialValues) {
3049
3328
  initialValues.forEach((v) => this.add(v));
3050
3329
  }
@@ -3200,6 +3479,7 @@
3200
3479
  this.$indexes = new Map();
3201
3480
  this.$refId = 0;
3202
3481
  this[$changes] = new ChangeTree(this);
3482
+ this[$changes].indexes = {};
3203
3483
  if (initialValues) {
3204
3484
  initialValues.forEach((v) => this.add(v));
3205
3485
  }
@@ -3355,6 +3635,8 @@
3355
3635
  OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
3356
3636
  PERFORMANCE OF THIS SOFTWARE.
3357
3637
  ***************************************************************************** */
3638
+ /* global Reflect, Promise, SuppressedError, Symbol */
3639
+
3358
3640
 
3359
3641
  function __decorate(decorators, target, key, desc) {
3360
3642
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -3368,37 +3650,135 @@
3368
3650
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
3369
3651
  };
3370
3652
 
3653
+ function spliceOne(arr, index) {
3654
+ // manually splice an array
3655
+ if (index === -1 || index >= arr.length) {
3656
+ return false;
3657
+ }
3658
+ const len = arr.length - 1;
3659
+ for (let i = index; i < len; i++) {
3660
+ arr[i] = arr[i + 1];
3661
+ }
3662
+ arr.length = len;
3663
+ return true;
3664
+ }
3665
+
3666
+ class Root {
3667
+ constructor(types) {
3668
+ this.types = types;
3669
+ this.nextUniqueId = 0;
3670
+ this.refCount = {};
3671
+ this.changeTrees = {};
3672
+ // all changes
3673
+ this.allChanges = [];
3674
+ this.allFilteredChanges = []; // TODO: do not initialize it if filters are not used
3675
+ // pending changes to be encoded
3676
+ this.changes = [];
3677
+ this.filteredChanges = []; // TODO: do not initialize it if filters are not used
3678
+ }
3679
+ getNextUniqueId() {
3680
+ return this.nextUniqueId++;
3681
+ }
3682
+ add(changeTree) {
3683
+ // FIXME: move implementation of `ensureRefId` to `Root` class
3684
+ changeTree.ensureRefId();
3685
+ const isNewChangeTree = (this.changeTrees[changeTree.refId] === undefined);
3686
+ if (isNewChangeTree) {
3687
+ this.changeTrees[changeTree.refId] = changeTree;
3688
+ }
3689
+ const previousRefCount = this.refCount[changeTree.refId];
3690
+ if (previousRefCount === 0) {
3691
+ //
3692
+ // When a ChangeTree is re-added, it means that it was previously removed.
3693
+ // We need to re-add all changes to the `changes` map.
3694
+ //
3695
+ const ops = changeTree.allChanges.operations;
3696
+ let len = ops.length;
3697
+ while (len--) {
3698
+ changeTree.indexedOperations[ops[len]] = exports.OPERATION.ADD;
3699
+ setOperationAtIndex(changeTree.changes, len);
3700
+ }
3701
+ }
3702
+ this.refCount[changeTree.refId] = (previousRefCount || 0) + 1;
3703
+ return isNewChangeTree;
3704
+ }
3705
+ remove(changeTree) {
3706
+ const refCount = (this.refCount[changeTree.refId]) - 1;
3707
+ if (refCount <= 0) {
3708
+ //
3709
+ // Only remove "root" reference if it's the last reference
3710
+ //
3711
+ changeTree.root = undefined;
3712
+ delete this.changeTrees[changeTree.refId];
3713
+ this.removeChangeFromChangeSet("allChanges", changeTree);
3714
+ this.removeChangeFromChangeSet("changes", changeTree);
3715
+ if (changeTree.isFiltered || changeTree.isPartiallyFiltered) {
3716
+ this.removeChangeFromChangeSet("allFilteredChanges", changeTree);
3717
+ this.removeChangeFromChangeSet("filteredChanges", changeTree);
3718
+ }
3719
+ this.refCount[changeTree.refId] = 0;
3720
+ }
3721
+ else {
3722
+ this.refCount[changeTree.refId] = refCount;
3723
+ }
3724
+ changeTree.forEachChild((child, _) => this.remove(child));
3725
+ return refCount;
3726
+ }
3727
+ removeChangeFromChangeSet(changeSetName, changeTree) {
3728
+ const changeSet = this[changeSetName];
3729
+ const index = changeSet.indexOf(changeTree);
3730
+ if (index !== -1) {
3731
+ spliceOne(changeSet, index);
3732
+ // changeSet[index] = undefined;
3733
+ }
3734
+ }
3735
+ clear() {
3736
+ this.changes.length = 0;
3737
+ }
3738
+ }
3739
+
3371
3740
  class Encoder {
3372
3741
  static { this.BUFFER_SIZE = 8 * 1024; } // 8KB
3373
- constructor(root) {
3742
+ constructor(state) {
3374
3743
  this.sharedBuffer = Buffer.allocUnsafeSlow(Encoder.BUFFER_SIZE);
3375
- this.setRoot(root);
3376
3744
  //
3377
3745
  // TODO: cache and restore "Context" based on root schema
3378
3746
  // (to avoid creating a new context for every new room)
3379
3747
  //
3380
- this.context = new TypeContext(root.constructor);
3748
+ this.context = new TypeContext(state.constructor);
3749
+ this.root = new Root(this.context);
3750
+ this.setState(state);
3381
3751
  // console.log(">>>>>>>>>>>>>>>> Encoder types");
3382
3752
  // this.context.schemas.forEach((id, schema) => {
3383
3753
  // console.log("type:", id, schema.name, Object.keys(schema[Symbol.metadata]));
3384
3754
  // });
3385
3755
  }
3386
- setRoot(state) {
3387
- this.root = new Root();
3756
+ setState(state) {
3388
3757
  this.state = state;
3389
- state[$changes].setRoot(this.root);
3758
+ this.state[$changes].setRoot(this.root);
3390
3759
  }
3391
- encode(it = { offset: 0 }, view, bytes = this.sharedBuffer, changeTrees = this.root.changes) {
3392
- const initialOffset = it.offset; // cache current offset in case we need to resize the buffer
3393
- const isEncodeAll = this.root.allChanges === changeTrees;
3760
+ encode(it = { offset: 0 }, view, buffer = this.sharedBuffer, changeSetName = "changes", isEncodeAll = changeSetName === "allChanges", initialOffset = it.offset // cache current offset in case we need to resize the buffer
3761
+ ) {
3394
3762
  const hasView = (view !== undefined);
3395
3763
  const rootChangeTree = this.state[$changes];
3396
- const changeTreesIterator = changeTrees.entries();
3397
- for (const [changeTree, changes] of changeTreesIterator) {
3764
+ const shouldDiscardChanges = !isEncodeAll && !hasView;
3765
+ const changeTrees = this.root[changeSetName];
3766
+ for (let i = 0, numChangeTrees = changeTrees.length; i < numChangeTrees; i++) {
3767
+ const changeTree = changeTrees[i];
3768
+ // // Root#removeChangeFromChangeSet() is now removing instead of setting to "undefined"
3769
+ // if (changeTree === undefined) { continue; }
3770
+ const operations = changeTree[changeSetName];
3398
3771
  const ref = changeTree.ref;
3399
- const ctor = ref['constructor'];
3772
+ const ctor = ref.constructor;
3400
3773
  const encoder = ctor[$encoder];
3401
3774
  const filter = ctor[$filter];
3775
+ const metadata = ctor[Symbol.metadata];
3776
+ // try { throw new Error(); } catch (e) {
3777
+ // // only print if not coming from Reflection.ts
3778
+ // if (!e.stack.includes("src/Reflection.ts")) {
3779
+ // console.log("ChangeTree:", { refId: changeTree.refId, ref: ref.constructor.name });
3780
+ // }
3781
+ // }
3402
3782
  if (hasView) {
3403
3783
  if (!view.items.has(changeTree)) {
3404
3784
  view.invisible.add(changeTree);
@@ -3409,12 +3789,18 @@
3409
3789
  }
3410
3790
  }
3411
3791
  // skip root `refId` if it's the first change tree
3412
- if (it.offset !== initialOffset || changeTree !== rootChangeTree) {
3413
- bytes[it.offset++] = SWITCH_TO_STRUCTURE & 255;
3414
- number$1(bytes, changeTree.refId, it);
3792
+ // (unless it "hasView", which will need to revisit the root)
3793
+ if (hasView || it.offset > initialOffset || changeTree !== rootChangeTree) {
3794
+ buffer[it.offset++] = SWITCH_TO_STRUCTURE & 255;
3795
+ number$1(buffer, changeTree.refId, it);
3415
3796
  }
3416
- const changesIterator = changes.entries();
3417
- for (const [fieldIndex, operation] of changesIterator) {
3797
+ for (let j = 0, numChanges = operations.operations.length; j < numChanges; j++) {
3798
+ const fieldIndex = operations.operations[j];
3799
+ const operation = (fieldIndex < 0)
3800
+ ? Math.abs(fieldIndex) // "pure" operation without fieldIndex (e.g. CLEAR, REVERSE, etc.)
3801
+ : (isEncodeAll)
3802
+ ? exports.OPERATION.ADD
3803
+ : changeTree.indexedOperations[fieldIndex];
3418
3804
  //
3419
3805
  // first pass (encodeAll), identify "filtered" operations without encoding them
3420
3806
  // they will be encoded per client, based on their view.
@@ -3422,93 +3808,127 @@
3422
3808
  // TODO: how can we optimize filtering out "encode all" operations?
3423
3809
  // TODO: avoid checking if no view tags were defined
3424
3810
  //
3425
- if (filter && !filter(ref, fieldIndex, view)) {
3426
- // console.log("SKIP FIELD:", { ref: changeTree.ref.constructor.name, fieldIndex, })
3811
+ if (fieldIndex === undefined || operation === undefined || (filter && !filter(ref, fieldIndex, view))) {
3427
3812
  // console.log("ADD AS INVISIBLE:", fieldIndex, changeTree.ref.constructor.name)
3428
3813
  // view?.invisible.add(changeTree);
3429
3814
  continue;
3430
3815
  }
3431
- // console.log("WILL ENCODE", {
3432
- // ref: changeTree.ref.constructor.name,
3433
- // fieldIndex,
3434
- // operation: OPERATION[operation],
3435
- // });
3436
- encoder(this, bytes, changeTree, fieldIndex, operation, it, isEncodeAll, hasView);
3816
+ // try { throw new Error(); } catch (e) {
3817
+ // // only print if not coming from Reflection.ts
3818
+ // if (!e.stack.includes("src/Reflection.ts")) {
3819
+ // console.log("WILL ENCODE", {
3820
+ // ref: changeTree.ref.constructor.name,
3821
+ // fieldIndex,
3822
+ // operation: OPERATION[operation],
3823
+ // });
3824
+ // }
3825
+ // }
3826
+ // console.log("encode...", { ref: changeTree.ref.constructor.name, refId: changeTree.refId, fieldIndex, operation });
3827
+ encoder(this, buffer, changeTree, fieldIndex, operation, it, isEncodeAll, hasView, metadata);
3828
+ }
3829
+ if (shouldDiscardChanges) {
3830
+ changeTree.discard();
3831
+ // Not a new instance anymore
3832
+ changeTree.isNew = false;
3437
3833
  }
3438
3834
  }
3439
- if (it.offset > bytes.byteLength) {
3440
- const newSize = getNextPowerOf2(this.sharedBuffer.byteLength * 2);
3441
- console.warn("@colyseus/schema encode buffer overflow. Current buffer size: " + bytes.byteLength + ", encoding offset: " + it.offset + ", new size: " + newSize);
3835
+ if (it.offset > buffer.byteLength) {
3836
+ const newSize = getNextPowerOf2(buffer.byteLength * 2);
3837
+ console.warn(`@colyseus/schema buffer overflow. Encoded state is higher than default BUFFER_SIZE. Use the following to increase default BUFFER_SIZE:
3838
+
3839
+ import { Encoder } from "@colyseus/schema";
3840
+ Encoder.BUFFER_SIZE = ${Math.round(newSize / 1024)} * 1024; // ${Math.round(newSize / 1024)} KB
3841
+ `);
3442
3842
  //
3443
3843
  // resize buffer and re-encode (TODO: can we avoid re-encoding here?)
3444
3844
  //
3445
- this.sharedBuffer = Buffer.allocUnsafeSlow(newSize);
3446
- return this.encode({ offset: initialOffset }, view);
3845
+ buffer = Buffer.allocUnsafeSlow(newSize);
3846
+ // assign resized buffer to local sharedBuffer
3847
+ if (buffer === this.sharedBuffer) {
3848
+ this.sharedBuffer = buffer;
3849
+ }
3850
+ return this.encode({ offset: initialOffset }, view, buffer, changeSetName, isEncodeAll);
3447
3851
  }
3448
3852
  else {
3449
- //
3450
- // only clear changes after making sure buffer resize is not required.
3451
- //
3452
- if (!isEncodeAll && !hasView) {
3453
- //
3454
- // FIXME: avoid iterating over change trees twice.
3455
- //
3456
- this.onEndEncode(changeTrees);
3457
- }
3458
- // return bytes;
3459
- return bytes.slice(0, it.offset);
3853
+ // //
3854
+ // // only clear changes after making sure buffer resize is not required.
3855
+ // //
3856
+ // if (shouldClearChanges) {
3857
+ // //
3858
+ // // FIXME: avoid iterating over change trees twice.
3859
+ // //
3860
+ // this.onEndEncode(changeTrees);
3861
+ // }
3862
+ return buffer.subarray(0, it.offset);
3460
3863
  }
3461
3864
  }
3462
- encodeAll(it = { offset: 0 }) {
3463
- // console.log(`encodeAll(), this.$root.allChanges (${this.$root.allChanges.size})`);
3464
- // Array.from(this.$root.allChanges.entries()).map((item) => {
3465
- // console.log("->", item[0].refId, item[0].ref.toJSON());
3466
- // });
3467
- return this.encode(it, undefined, this.sharedBuffer, this.root.allChanges);
3865
+ encodeAll(it = { offset: 0 }, buffer = this.sharedBuffer) {
3866
+ // console.log(`\nencodeAll(), this.root.allChanges (${(Object.keys(this.root.allChanges).length)})`);
3867
+ // this.debugChanges("allChanges");
3868
+ return this.encode(it, undefined, buffer, "allChanges", true);
3468
3869
  }
3469
3870
  encodeAllView(view, sharedOffset, it, bytes = this.sharedBuffer) {
3470
3871
  const viewOffset = it.offset;
3471
- // console.log(`encodeAllView(), this.$root.allFilteredChanges (${this.$root.allFilteredChanges.size})`);
3472
- // this.debugAllFilteredChanges();
3872
+ // console.log(`\nencodeAllView(), this.root.allFilteredChanges (${(Object.keys(this.root.allFilteredChanges).length)})`);
3873
+ // this.debugChanges("allFilteredChanges");
3874
+ // console.log("\n\nENCODE ALL FOR VIEW...\n\n")
3473
3875
  // try to encode "filtered" changes
3474
- this.encode(it, view, bytes, this.root.allFilteredChanges);
3876
+ this.encode(it, view, bytes, "allFilteredChanges", true, viewOffset);
3475
3877
  return Buffer.concat([
3476
- bytes.slice(0, sharedOffset),
3477
- bytes.slice(viewOffset, it.offset)
3878
+ bytes.subarray(0, sharedOffset),
3879
+ bytes.subarray(viewOffset, it.offset)
3478
3880
  ]);
3479
3881
  }
3480
- // debugAllFilteredChanges() {
3481
- // Array.from(this.$root.allFilteredChanges.entries()).map((item) => {
3482
- // console.log("->", { refId: item[0].refId }, item[0].ref.toJSON());
3483
- // if (Array.isArray(item[0].ref.toJSON())) {
3484
- // item[1].forEach((op, key) => {
3485
- // console.log(" ->", { key, op: OPERATION[op] });
3486
- // })
3487
- // }
3488
- // });
3489
- // }
3882
+ debugChanges(field) {
3883
+ const rootChangeSet = (typeof (field) === "string")
3884
+ ? this.root[field]
3885
+ : field;
3886
+ rootChangeSet.forEach((changeTree) => {
3887
+ const changeSet = changeTree[field];
3888
+ const metadata = changeTree.ref.constructor[Symbol.metadata];
3889
+ console.log("->", { ref: changeTree.ref.constructor.name, refId: changeTree.refId, changes: Object.keys(changeSet).length });
3890
+ for (const index in changeSet) {
3891
+ const op = changeSet[index];
3892
+ console.log(" ->", {
3893
+ index,
3894
+ field: metadata?.[index],
3895
+ op: exports.OPERATION[op],
3896
+ });
3897
+ }
3898
+ });
3899
+ }
3490
3900
  encodeView(view, sharedOffset, it, bytes = this.sharedBuffer) {
3491
3901
  const viewOffset = it.offset;
3492
- // try to encode "filtered" changes
3493
- this.encode(it, view, bytes, this.root.filteredChanges);
3902
+ // console.log(`\nencodeView(), view.changes (${view.changes.size})`);
3903
+ // this.debugChanges(view.changes);
3904
+ // console.log(`\nencodeView(), this.root.filteredChanges (${this.root.filteredChanges.size})`);
3905
+ // this.debugChanges("filteredChanges");
3494
3906
  // encode visibility changes (add/remove for this view)
3495
- const viewChangesIterator = view.changes.entries();
3496
- for (const [changeTree, changes] of viewChangesIterator) {
3497
- if (changes.size === 0) {
3498
- // FIXME: avoid having empty changes if no changes were made
3499
- // console.log("changes.size === 0", changeTree.ref.constructor.name);
3907
+ const refIds = Object.keys(view.changes);
3908
+ // console.log("ENCODE VIEW:", refIds);
3909
+ for (let i = 0, numRefIds = refIds.length; i < numRefIds; i++) {
3910
+ const refId = refIds[i];
3911
+ const changes = view.changes[refId];
3912
+ const changeTree = this.root.changeTrees[refId];
3913
+ if (changeTree === undefined ||
3914
+ Object.keys(changes).length === 0 // FIXME: avoid having empty changes if no changes were made
3915
+ ) {
3916
+ // console.log("changes.size === 0, skip", changeTree.ref.constructor.name);
3500
3917
  continue;
3501
3918
  }
3502
3919
  const ref = changeTree.ref;
3503
- const ctor = ref['constructor'];
3920
+ const ctor = ref.constructor;
3504
3921
  const encoder = ctor[$encoder];
3922
+ const metadata = ctor[Symbol.metadata];
3505
3923
  bytes[it.offset++] = SWITCH_TO_STRUCTURE & 255;
3506
3924
  number$1(bytes, changeTree.refId, it);
3507
- const changesIterator = changes.entries();
3508
- for (const [fieldIndex, operation] of changesIterator) {
3925
+ const keys = Object.keys(changes);
3926
+ for (let i = 0, numChanges = keys.length; i < numChanges; i++) {
3927
+ const key = keys[i];
3928
+ const operation = changes[key];
3509
3929
  // isEncodeAll = false
3510
3930
  // hasView = true
3511
- encoder(this, bytes, changeTree, fieldIndex, operation, it, false, true);
3931
+ encoder(this, bytes, changeTree, Number(key), operation, it, false, true, metadata);
3512
3932
  }
3513
3933
  }
3514
3934
  //
@@ -3516,51 +3936,64 @@
3516
3936
  // (to allow re-using StateView's for multiple clients)
3517
3937
  //
3518
3938
  // clear "view" changes after encoding
3519
- view.changes.clear();
3939
+ view.changes = {};
3940
+ // console.log("FILTERED CHANGES:", this.root.filteredChanges);
3941
+ // try to encode "filtered" changes
3942
+ this.encode(it, view, bytes, "filteredChanges", false, viewOffset);
3520
3943
  return Buffer.concat([
3521
- bytes.slice(0, sharedOffset),
3522
- bytes.slice(viewOffset, it.offset)
3944
+ bytes.subarray(0, sharedOffset),
3945
+ bytes.subarray(viewOffset, it.offset)
3523
3946
  ]);
3524
3947
  }
3525
3948
  onEndEncode(changeTrees = this.root.changes) {
3526
- const changeTreesIterator = changeTrees.entries();
3527
- for (const [changeTree, _] of changeTreesIterator) {
3528
- changeTree.endEncode();
3529
- }
3949
+ // changeTrees.forEach(function(changeTree) {
3950
+ // changeTree.endEncode();
3951
+ // });
3952
+ // for (const refId in changeTrees) {
3953
+ // const changeTree = this.root.changeTrees[refId];
3954
+ // changeTree.endEncode();
3955
+ // // changeTree.changes.clear();
3956
+ // // // ArraySchema and MapSchema have a custom "encode end" method
3957
+ // // changeTree.ref[$onEncodeEnd]?.();
3958
+ // // // Not a new instance anymore
3959
+ // // delete changeTree[$isNew];
3960
+ // }
3530
3961
  }
3531
3962
  discardChanges() {
3963
+ // console.log("DISCARD CHANGES!");
3532
3964
  // discard shared changes
3533
- if (this.root.changes.size > 0) {
3534
- this.onEndEncode(this.root.changes);
3535
- this.root.changes.clear();
3965
+ let length = this.root.changes.length;
3966
+ if (length > 0) {
3967
+ while (length--) {
3968
+ this.root.changes[length]?.endEncode();
3969
+ }
3970
+ this.root.changes.length = 0;
3536
3971
  }
3537
3972
  // discard filtered changes
3538
- if (this.root.filteredChanges.size > 0) {
3539
- this.onEndEncode(this.root.filteredChanges);
3540
- this.root.filteredChanges.clear();
3973
+ length = this.root.filteredChanges.length;
3974
+ if (length > 0) {
3975
+ while (length--) {
3976
+ this.root.filteredChanges[length]?.endEncode();
3977
+ }
3978
+ this.root.filteredChanges.length = 0;
3541
3979
  }
3542
3980
  }
3543
3981
  tryEncodeTypeId(bytes, baseType, targetType, it) {
3544
3982
  const baseTypeId = this.context.getTypeId(baseType);
3545
3983
  const targetTypeId = this.context.getTypeId(targetType);
3984
+ if (targetTypeId === undefined) {
3985
+ console.warn(`@colyseus/schema WARNING: Class "${targetType.name}" is not registered on TypeRegistry - Please either tag the class with @entity or define a @type() field.`);
3986
+ return;
3987
+ }
3546
3988
  if (baseTypeId !== targetTypeId) {
3547
3989
  bytes[it.offset++] = TYPE_ID & 255;
3548
3990
  number$1(bytes, targetTypeId, it);
3549
3991
  }
3550
3992
  }
3551
- }
3552
-
3553
- function spliceOne(arr, index) {
3554
- // manually splice an array
3555
- if (index === -1 || index >= arr.length) {
3556
- return false;
3557
- }
3558
- const len = arr.length - 1;
3559
- for (let i = index; i < len; i++) {
3560
- arr[i] = arr[i + 1];
3993
+ get hasChanges() {
3994
+ return (this.root.changes.length > 0 ||
3995
+ this.root.filteredChanges.length > 0);
3561
3996
  }
3562
- arr.length = len;
3563
- return true;
3564
3997
  }
3565
3998
 
3566
3999
  class DecodingWarning extends Error {
@@ -3641,8 +4074,9 @@
3641
4074
  // Ensure child schema instances have their references removed as well.
3642
4075
  //
3643
4076
  if (Metadata.isValidInstance(ref)) {
3644
- const metadata = ref['constructor'][Symbol.metadata];
3645
- for (const field in metadata) {
4077
+ const metadata = ref.constructor[Symbol.metadata];
4078
+ for (const index in metadata) {
4079
+ const field = metadata[index].name;
3646
4080
  const childRefId = typeof (ref[field]) === "object" && this.refIds.get(ref[field]);
3647
4081
  if (childRefId) {
3648
4082
  this.removeRef(childRefId);
@@ -3689,21 +4123,21 @@
3689
4123
  class Decoder {
3690
4124
  constructor(root, context) {
3691
4125
  this.currentRefId = 0;
3692
- this.setRoot(root);
4126
+ this.setState(root);
3693
4127
  this.context = context || new TypeContext(root.constructor);
3694
4128
  // console.log(">>>>>>>>>>>>>>>> Decoder types");
3695
4129
  // this.context.schemas.forEach((id, schema) => {
3696
4130
  // console.log("type:", id, schema.name, Object.keys(schema[Symbol.metadata]));
3697
4131
  // });
3698
4132
  }
3699
- setRoot(root) {
4133
+ setState(root) {
3700
4134
  this.state = root;
3701
- this.$root = new ReferenceTracker();
3702
- this.$root.addRef(0, root);
4135
+ this.root = new ReferenceTracker();
4136
+ this.root.addRef(0, root);
3703
4137
  }
3704
4138
  decode(bytes, it = { offset: 0 }, ref = this.state) {
3705
4139
  const allChanges = [];
3706
- const $root = this.$root;
4140
+ const $root = this.root;
3707
4141
  const totalBytes = bytes.byteLength;
3708
4142
  let decoder = ref['constructor'][$decoder];
3709
4143
  this.currentRefId = 0;
@@ -3723,7 +4157,7 @@
3723
4157
  }
3724
4158
  ref[$onDecodeEnd]?.();
3725
4159
  ref = nextRef;
3726
- decoder = ref['constructor'][$decoder];
4160
+ decoder = ref.constructor[$decoder];
3727
4161
  continue;
3728
4162
  }
3729
4163
  const result = decoder(this, bytes, it, ref, allChanges);
@@ -3784,7 +4218,7 @@
3784
4218
  previousValue: value
3785
4219
  });
3786
4220
  if (needRemoveRef) {
3787
- this.$root.removeRef(this.$root.refIds.get(value));
4221
+ this.root.removeRef(this.root.refIds.get(value));
3788
4222
  }
3789
4223
  });
3790
4224
  }
@@ -3824,14 +4258,27 @@
3824
4258
  super(...arguments);
3825
4259
  this.types = new ArraySchema();
3826
4260
  }
3827
- static encode(instance, context) {
3828
- if (!context) {
3829
- context = new TypeContext(instance.constructor);
3830
- }
4261
+ /**
4262
+ * Encodes the TypeContext of an Encoder into a buffer.
4263
+ *
4264
+ * @param encoder Encoder instance
4265
+ * @param it
4266
+ * @returns
4267
+ */
4268
+ static encode(encoder, it = { offset: 0 }) {
4269
+ const context = encoder.context;
3831
4270
  const reflection = new Reflection();
3832
- const encoder = new Encoder(reflection);
4271
+ const reflectionEncoder = new Encoder(reflection);
4272
+ // rootType is usually the first schema passed to the Encoder
4273
+ // (unless it inherits from another schema)
4274
+ const rootType = context.schemas.get(encoder.state.constructor);
4275
+ if (rootType > 0) {
4276
+ reflection.rootType = rootType;
4277
+ }
3833
4278
  const buildType = (currentType, metadata) => {
3834
- for (const fieldName in metadata) {
4279
+ for (const fieldIndex in metadata) {
4280
+ const index = Number(fieldIndex);
4281
+ const fieldName = metadata[index].name;
3835
4282
  // skip fields from parent classes
3836
4283
  if (!Object.prototype.hasOwnProperty.call(metadata, fieldName)) {
3837
4284
  continue;
@@ -3839,7 +4286,7 @@
3839
4286
  const field = new ReflectionField();
3840
4287
  field.name = fieldName;
3841
4288
  let fieldType;
3842
- const type = metadata[fieldName].type;
4289
+ const type = metadata[index].type;
3843
4290
  if (typeof (type) === "string") {
3844
4291
  fieldType = type;
3845
4292
  }
@@ -3881,66 +4328,335 @@
3881
4328
  }
3882
4329
  buildType(type, klass[Symbol.metadata]);
3883
4330
  }
3884
- const it = { offset: 0 };
3885
- const buf = encoder.encodeAll(it);
4331
+ const buf = reflectionEncoder.encodeAll(it);
3886
4332
  return Buffer.from(buf, 0, it.offset);
3887
4333
  }
4334
+ /**
4335
+ * Decodes the TypeContext from a buffer into a Decoder instance.
4336
+ *
4337
+ * @param bytes Reflection.encode() output
4338
+ * @param it
4339
+ * @returns Decoder instance
4340
+ */
3888
4341
  static decode(bytes, it) {
3889
4342
  const reflection = new Reflection();
3890
4343
  const reflectionDecoder = new Decoder(reflection);
3891
4344
  reflectionDecoder.decode(bytes, it);
3892
- const context = new TypeContext();
3893
- const schemaTypes = reflection.types.reduce((types, reflectionType) => {
3894
- const parentKlass = types[reflectionType.extendsId] || Schema;
3895
- const schema = class _ extends parentKlass {
4345
+ const typeContext = new TypeContext();
4346
+ // 1st pass, initialize metadata + inheritance
4347
+ reflection.types.forEach((reflectionType) => {
4348
+ const parentClass = typeContext.get(reflectionType.extendsId) ?? Schema;
4349
+ const schema = class _ extends parentClass {
3896
4350
  };
3897
- // const _metadata = Object.create(_classSuper[Symbol.metadata] ?? null);
3898
- const _metadata = parentKlass && parentKlass[Symbol.metadata] || Object.create(null);
3899
- Object.defineProperty(schema, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
3900
4351
  // register for inheritance support
3901
4352
  TypeContext.register(schema);
3902
- const typeid = reflectionType.id;
3903
- types[typeid] = schema;
3904
- context.add(schema, typeid);
3905
- return types;
4353
+ // // for inheritance support
4354
+ // Metadata.initialize(schema);
4355
+ typeContext.add(schema, reflectionType.id);
3906
4356
  }, {});
3907
- reflection.types.forEach((reflectionType) => {
3908
- const schemaType = schemaTypes[reflectionType.id];
3909
- const metadata = schemaType[Symbol.metadata];
3910
- const parentKlass = reflection.types[reflectionType.extendsId];
3911
- const parentFieldIndex = parentKlass && parentKlass.fields.length || 0;
4357
+ // define fields
4358
+ const addFields = (metadata, reflectionType, parentFieldIndex) => {
3912
4359
  reflectionType.fields.forEach((field, i) => {
3913
4360
  const fieldIndex = parentFieldIndex + i;
3914
4361
  if (field.referencedType !== undefined) {
3915
4362
  let fieldType = field.type;
3916
- let refType = schemaTypes[field.referencedType];
4363
+ let refType = typeContext.get(field.referencedType);
3917
4364
  // map or array of primitive type (-1)
3918
4365
  if (!refType) {
3919
4366
  const typeInfo = field.type.split(":");
3920
4367
  fieldType = typeInfo[0];
3921
- refType = typeInfo[1];
4368
+ refType = typeInfo[1]; // string
3922
4369
  }
3923
4370
  if (fieldType === "ref") {
3924
- // type(refType)(schemaType.prototype, field.name);
3925
4371
  Metadata.addField(metadata, fieldIndex, field.name, refType);
3926
4372
  }
3927
4373
  else {
3928
- // type({ [fieldType]: refType } as DefinitionType)(schemaType.prototype, field.name);
3929
4374
  Metadata.addField(metadata, fieldIndex, field.name, { [fieldType]: refType });
3930
4375
  }
3931
4376
  }
3932
4377
  else {
3933
- // type(field.type as PrimitiveType)(schemaType.prototype, field.name);
3934
4378
  Metadata.addField(metadata, fieldIndex, field.name, field.type);
3935
4379
  }
3936
4380
  });
4381
+ };
4382
+ // 2nd pass, set fields
4383
+ reflection.types.forEach((reflectionType) => {
4384
+ const schema = typeContext.get(reflectionType.id);
4385
+ // for inheritance support
4386
+ const metadata = Metadata.initialize(schema);
4387
+ const inheritedTypes = [];
4388
+ let parentType = reflectionType;
4389
+ do {
4390
+ inheritedTypes.push(parentType);
4391
+ parentType = reflection.types.find((t) => t.id === parentType.extendsId);
4392
+ } while (parentType);
4393
+ let parentFieldIndex = 0;
4394
+ inheritedTypes.reverse().forEach((reflectionType) => {
4395
+ // add fields from all inherited classes
4396
+ // TODO: refactor this to avoid adding fields from parent classes
4397
+ addFields(metadata, reflectionType, parentFieldIndex);
4398
+ parentFieldIndex += reflectionType.fields.length;
4399
+ });
3937
4400
  });
3938
- return new (schemaTypes[0])();
4401
+ const state = new (typeContext.get(reflection.rootType || 0))();
4402
+ return new Decoder(state, typeContext);
3939
4403
  }
3940
4404
  }
3941
4405
  __decorate([
3942
4406
  type([ReflectionType])
3943
4407
  ], Reflection.prototype, "types", void 0);
4408
+ __decorate([
4409
+ type("number")
4410
+ ], Reflection.prototype, "rootType", void 0);
4411
+
4412
+ function getDecoderStateCallbacks(decoder) {
4413
+ const $root = decoder.root;
4414
+ const callbacks = $root.callbacks;
4415
+ const onAddCalls = new WeakMap();
4416
+ let currentOnAddCallback;
4417
+ decoder.triggerChanges = function (allChanges) {
4418
+ const uniqueRefIds = new Set();
4419
+ for (let i = 0, l = allChanges.length; i < l; i++) {
4420
+ const change = allChanges[i];
4421
+ const refId = change.refId;
4422
+ const ref = change.ref;
4423
+ const $callbacks = callbacks[refId];
4424
+ if (!$callbacks) {
4425
+ continue;
4426
+ }
4427
+ //
4428
+ // trigger onRemove on child structure.
4429
+ //
4430
+ if ((change.op & exports.OPERATION.DELETE) === exports.OPERATION.DELETE &&
4431
+ change.previousValue instanceof Schema) {
4432
+ const deleteCallbacks = callbacks[$root.refIds.get(change.previousValue)]?.[exports.OPERATION.DELETE];
4433
+ for (let i = deleteCallbacks?.length - 1; i >= 0; i--) {
4434
+ deleteCallbacks[i]();
4435
+ }
4436
+ }
4437
+ if (ref instanceof Schema) {
4438
+ //
4439
+ // Handle schema instance
4440
+ //
4441
+ if (!uniqueRefIds.has(refId)) {
4442
+ // trigger onChange
4443
+ const replaceCallbacks = $callbacks?.[exports.OPERATION.REPLACE];
4444
+ for (let i = replaceCallbacks?.length - 1; i >= 0; i--) {
4445
+ replaceCallbacks[i]();
4446
+ // try {
4447
+ // } catch (e) {
4448
+ // console.error(e);
4449
+ // }
4450
+ }
4451
+ }
4452
+ if ($callbacks.hasOwnProperty(change.field)) {
4453
+ const fieldCallbacks = $callbacks[change.field];
4454
+ for (let i = fieldCallbacks?.length - 1; i >= 0; i--) {
4455
+ fieldCallbacks[i](change.value, change.previousValue);
4456
+ // try {
4457
+ // } catch (e) {
4458
+ // console.error(e);
4459
+ // }
4460
+ }
4461
+ }
4462
+ }
4463
+ else {
4464
+ //
4465
+ // Handle collection of items
4466
+ //
4467
+ if ((change.op & exports.OPERATION.DELETE) === exports.OPERATION.DELETE) {
4468
+ //
4469
+ // FIXME: `previousValue` should always be available.
4470
+ //
4471
+ if (change.previousValue !== undefined) {
4472
+ // triger onRemove
4473
+ const deleteCallbacks = $callbacks[exports.OPERATION.DELETE];
4474
+ for (let i = deleteCallbacks?.length - 1; i >= 0; i--) {
4475
+ deleteCallbacks[i](change.previousValue, change.dynamicIndex ?? change.field);
4476
+ }
4477
+ }
4478
+ // Handle DELETE_AND_ADD operations
4479
+ if ((change.op & exports.OPERATION.ADD) === exports.OPERATION.ADD) {
4480
+ const addCallbacks = $callbacks[exports.OPERATION.ADD];
4481
+ for (let i = addCallbacks?.length - 1; i >= 0; i--) {
4482
+ addCallbacks[i](change.value, change.dynamicIndex ?? change.field);
4483
+ }
4484
+ }
4485
+ }
4486
+ else if ((change.op & exports.OPERATION.ADD) === exports.OPERATION.ADD && change.previousValue === undefined) {
4487
+ // triger onAdd
4488
+ const addCallbacks = $callbacks[exports.OPERATION.ADD];
4489
+ for (let i = addCallbacks?.length - 1; i >= 0; i--) {
4490
+ addCallbacks[i](change.value, change.dynamicIndex ?? change.field);
4491
+ }
4492
+ }
4493
+ // trigger onChange
4494
+ if (change.value !== change.previousValue) {
4495
+ const replaceCallbacks = $callbacks[exports.OPERATION.REPLACE];
4496
+ for (let i = replaceCallbacks?.length - 1; i >= 0; i--) {
4497
+ replaceCallbacks[i](change.value, change.dynamicIndex ?? change.field);
4498
+ }
4499
+ }
4500
+ }
4501
+ uniqueRefIds.add(refId);
4502
+ }
4503
+ };
4504
+ function getProxy(metadataOrType, context) {
4505
+ let metadata = context.instance?.constructor[Symbol.metadata] || metadataOrType;
4506
+ let isCollection = ((context.instance && typeof (context.instance['forEach']) === "function") ||
4507
+ (metadataOrType && typeof (metadataOrType[Symbol.metadata]) === "undefined"));
4508
+ if (metadata && !isCollection) {
4509
+ const onAddListen = function (ref, prop, callback, immediate) {
4510
+ // immediate trigger
4511
+ if (immediate &&
4512
+ context.instance[prop] !== undefined &&
4513
+ !onAddCalls.has(currentOnAddCallback) // Workaround for https://github.com/colyseus/schema/issues/147
4514
+ ) {
4515
+ callback(context.instance[prop], undefined);
4516
+ }
4517
+ return $root.addCallback($root.refIds.get(ref), prop, callback);
4518
+ };
4519
+ /**
4520
+ * Schema instances
4521
+ */
4522
+ return new Proxy({
4523
+ listen: function listen(prop, callback, immediate = true) {
4524
+ if (context.instance) {
4525
+ return onAddListen(context.instance, prop, callback, immediate);
4526
+ }
4527
+ else {
4528
+ // collection instance not received yet
4529
+ let detachCallback = () => { };
4530
+ context.onInstanceAvailable((ref, existing) => {
4531
+ detachCallback = onAddListen(ref, prop, callback, immediate && existing && !onAddCalls.has(currentOnAddCallback));
4532
+ });
4533
+ return () => detachCallback();
4534
+ }
4535
+ },
4536
+ onChange: function onChange(callback) {
4537
+ return $root.addCallback($root.refIds.get(context.instance), exports.OPERATION.REPLACE, callback);
4538
+ },
4539
+ //
4540
+ // TODO: refactor `bindTo()` implementation.
4541
+ // There is room for improvement.
4542
+ //
4543
+ bindTo: function bindTo(targetObject, properties) {
4544
+ if (!properties) {
4545
+ properties = Object.keys(metadata).map((index) => metadata[index].name);
4546
+ }
4547
+ return $root.addCallback($root.refIds.get(context.instance), exports.OPERATION.REPLACE, () => {
4548
+ properties.forEach((prop) => targetObject[prop] = context.instance[prop]);
4549
+ });
4550
+ }
4551
+ }, {
4552
+ get(target, prop) {
4553
+ const metadataField = metadata[metadata[prop]];
4554
+ if (metadataField) {
4555
+ const instance = context.instance?.[prop];
4556
+ const onInstanceAvailable = ((callback) => {
4557
+ const unbind = $(context.instance).listen(prop, (value, _) => {
4558
+ callback(value, false);
4559
+ // FIXME: by "unbinding" the callback here,
4560
+ // it will not support when the server
4561
+ // re-instantiates the instance.
4562
+ //
4563
+ unbind?.();
4564
+ }, false);
4565
+ // has existing value
4566
+ if ($root.refIds.get(instance) !== undefined) {
4567
+ callback(instance, true);
4568
+ }
4569
+ });
4570
+ return getProxy(metadataField.type, {
4571
+ // make sure refId is available, otherwise need to wait for the instance to be available.
4572
+ instance: ($root.refIds.get(instance) && instance),
4573
+ parentInstance: context.instance,
4574
+ onInstanceAvailable,
4575
+ });
4576
+ }
4577
+ else {
4578
+ // accessing the function
4579
+ return target[prop];
4580
+ }
4581
+ },
4582
+ has(target, prop) { return metadata[prop] !== undefined; },
4583
+ set(_, _1, _2) { throw new Error("not allowed"); },
4584
+ deleteProperty(_, _1) { throw new Error("not allowed"); },
4585
+ });
4586
+ }
4587
+ else {
4588
+ /**
4589
+ * Collection instances
4590
+ */
4591
+ const onAdd = function (ref, callback, immediate) {
4592
+ // Trigger callback on existing items
4593
+ if (immediate) {
4594
+ ref.forEach((v, k) => callback(v, k));
4595
+ }
4596
+ return $root.addCallback($root.refIds.get(ref), exports.OPERATION.ADD, (value, key) => {
4597
+ onAddCalls.set(callback, true);
4598
+ currentOnAddCallback = callback;
4599
+ callback(value, key);
4600
+ onAddCalls.delete(callback);
4601
+ currentOnAddCallback = undefined;
4602
+ });
4603
+ };
4604
+ const onRemove = function (ref, callback) {
4605
+ return $root.addCallback($root.refIds.get(ref), exports.OPERATION.DELETE, callback);
4606
+ };
4607
+ return new Proxy({
4608
+ onAdd: function (callback, immediate = true) {
4609
+ //
4610
+ // https://github.com/colyseus/schema/issues/147
4611
+ // If parent instance has "onAdd" registered, avoid triggering immediate callback.
4612
+ //
4613
+ if (context.instance) {
4614
+ return onAdd(context.instance, callback, immediate && !onAddCalls.has(currentOnAddCallback));
4615
+ }
4616
+ else if (context.onInstanceAvailable) {
4617
+ // collection instance not received yet
4618
+ let detachCallback = () => { };
4619
+ context.onInstanceAvailable((ref, existing) => {
4620
+ detachCallback = onAdd(ref, callback, immediate && existing && !onAddCalls.has(currentOnAddCallback));
4621
+ });
4622
+ return () => detachCallback();
4623
+ }
4624
+ },
4625
+ onRemove: function (callback) {
4626
+ if (context.onInstanceAvailable) {
4627
+ // collection instance not received yet
4628
+ let detachCallback = () => { };
4629
+ context.onInstanceAvailable((ref) => {
4630
+ detachCallback = onRemove(ref, callback);
4631
+ });
4632
+ return () => detachCallback();
4633
+ }
4634
+ else if (context.instance) {
4635
+ return onRemove(context.instance, callback);
4636
+ }
4637
+ },
4638
+ }, {
4639
+ get(target, prop) {
4640
+ if (!target[prop]) {
4641
+ throw new Error(`Can't access '${prop}' through callback proxy. access the instance directly.`);
4642
+ }
4643
+ return target[prop];
4644
+ },
4645
+ has(target, prop) { return target[prop] !== undefined; },
4646
+ set(_, _1, _2) { throw new Error("not allowed"); },
4647
+ deleteProperty(_, _1) { throw new Error("not allowed"); },
4648
+ });
4649
+ }
4650
+ }
4651
+ function $(instance) {
4652
+ return getProxy(undefined, { instance });
4653
+ }
4654
+ return $;
4655
+ }
4656
+
4657
+ function getRawChangesCallback(decoder, callback) {
4658
+ decoder.triggerChanges = callback;
4659
+ }
3944
4660
 
3945
4661
  class StateView {
3946
4662
  constructor() {
@@ -3956,31 +4672,32 @@
3956
4672
  * Manual "ADD" operations for changes per ChangeTree, specific to this view.
3957
4673
  * (This is used to force encoding a property, even if it was not changed)
3958
4674
  */
3959
- this.changes = new Map();
4675
+ this.changes = {};
3960
4676
  }
3961
4677
  // TODO: allow to set multiple tags at once
3962
- add(obj, tag = DEFAULT_VIEW_TAG) {
4678
+ add(obj, tag = DEFAULT_VIEW_TAG, checkIncludeParent = true) {
3963
4679
  if (!obj[$changes]) {
3964
4680
  console.warn("StateView#add(), invalid object:", obj);
3965
4681
  return this;
3966
4682
  }
3967
- let changeTree = obj[$changes];
3968
- this.items.add(changeTree);
3969
- // Add children of this ChangeTree to this view
3970
- changeTree.forEachChild((change, _) => this.add(change.ref, tag));
3971
- // FIXME: ArraySchema/MapSchema does not have metadata
4683
+ // FIXME: ArraySchema/MapSchema do not have metadata
3972
4684
  const metadata = obj.constructor[Symbol.metadata];
3973
- // add parent ChangeTree's, if they are invisible to this view
3974
- // TODO: REFACTOR addParent()
3975
- this.addParent(changeTree, tag);
4685
+ const changeTree = obj[$changes];
4686
+ this.items.add(changeTree);
4687
+ // add parent ChangeTree's
4688
+ // - if it was invisible to this view
4689
+ // - if it were previously filtered out
4690
+ if (checkIncludeParent && changeTree.parent) {
4691
+ this.addParent(changeTree.parent[$changes], changeTree.parentIndex, tag);
4692
+ }
3976
4693
  //
3977
4694
  // TODO: when adding an item of a MapSchema, the changes may not
3978
4695
  // be set (only the parent's changes are set)
3979
4696
  //
3980
- let changes = this.changes.get(changeTree);
4697
+ let changes = this.changes[changeTree.refId];
3981
4698
  if (changes === undefined) {
3982
- changes = new Map();
3983
- this.changes.set(changeTree, changes);
4699
+ changes = {};
4700
+ this.changes[changeTree.refId] = changes;
3984
4701
  }
3985
4702
  // set tag
3986
4703
  if (tag !== DEFAULT_VIEW_TAG) {
@@ -3996,82 +4713,76 @@
3996
4713
  tags = this.tags.get(changeTree);
3997
4714
  }
3998
4715
  tags.add(tag);
3999
- // console.log("BY TAG:", tag);
4000
4716
  // Ref: add tagged properties
4001
- metadata?.[-3]?.[tag]?.forEach((index) => {
4717
+ metadata?.[$fieldIndexesByViewTag]?.[tag]?.forEach((index) => {
4002
4718
  if (changeTree.getChange(index) !== exports.OPERATION.DELETE) {
4003
- changes.set(index, exports.OPERATION.ADD);
4719
+ changes[index] = exports.OPERATION.ADD;
4004
4720
  }
4005
4721
  });
4006
4722
  }
4007
4723
  else {
4008
- // console.log("DEFAULT TAG", changeTree.allChanges);
4009
- // // add default tag properties
4010
- // metadata?.[-3]?.[DEFAULT_VIEW_TAG]?.forEach((index) => {
4011
- // if (changeTree.getChange(index) !== OPERATION.DELETE) {
4012
- // changes.set(index, OPERATION.ADD);
4013
- // }
4014
- // });
4015
- const allChangesSet = (changeTree.isFiltered || changeTree.isPartiallyFiltered)
4724
+ const isInvisible = this.invisible.has(changeTree);
4725
+ const changeSet = (changeTree.isFiltered || changeTree.isPartiallyFiltered)
4016
4726
  ? changeTree.allFilteredChanges
4017
4727
  : changeTree.allChanges;
4018
- const it = allChangesSet.keys();
4019
- const isInvisible = this.invisible.has(changeTree);
4020
- for (const index of it) {
4021
- if ((isInvisible || metadata?.[metadata?.[index]].tag === tag) &&
4022
- changeTree.getChange(index) !== exports.OPERATION.DELETE) {
4023
- changes.set(index, exports.OPERATION.ADD);
4728
+ for (let i = 0, len = changeSet.operations.length; i < len; i++) {
4729
+ const index = changeSet.operations[i];
4730
+ if (index === undefined) {
4731
+ continue;
4732
+ } // skip "undefined" indexes
4733
+ const op = changeTree.indexedOperations[index];
4734
+ const tagAtIndex = metadata?.[index].tag;
4735
+ if ((isInvisible || // if "invisible", include all
4736
+ tagAtIndex === undefined || // "all change" with no tag
4737
+ tagAtIndex === tag // tagged property
4738
+ ) &&
4739
+ op !== exports.OPERATION.DELETE) {
4740
+ changes[index] = op;
4024
4741
  }
4025
4742
  }
4026
4743
  }
4027
- // TODO: avoid unnecessary iteration here
4028
- while (changeTree.parent &&
4029
- (changeTree = changeTree.parent[$changes]) &&
4030
- (changeTree.isFiltered || changeTree.isPartiallyFiltered)) {
4031
- this.items.add(changeTree);
4032
- }
4744
+ // Add children of this ChangeTree to this view
4745
+ changeTree.forEachChild((change, index) => {
4746
+ // Do not ADD children that don't have the same tag
4747
+ if (metadata && metadata[index].tag !== tag) {
4748
+ return;
4749
+ }
4750
+ this.add(change.ref, tag, false);
4751
+ });
4033
4752
  return this;
4034
4753
  }
4035
- addParent(changeTree, tag) {
4036
- const parentRef = changeTree.parent;
4037
- if (!parentRef) {
4038
- return;
4754
+ addParent(changeTree, parentIndex, tag) {
4755
+ // view must have all "changeTree" parent tree
4756
+ this.items.add(changeTree);
4757
+ // add parent's parent
4758
+ const parentChangeTree = changeTree.parent?.[$changes];
4759
+ if (parentChangeTree && (parentChangeTree.isFiltered || parentChangeTree.isPartiallyFiltered)) {
4760
+ this.addParent(parentChangeTree, changeTree.parentIndex, tag);
4039
4761
  }
4040
- const parentChangeTree = parentRef[$changes];
4041
- const parentIndex = changeTree.parentIndex;
4042
- if (!this.invisible.has(parentChangeTree)) {
4043
- // parent is already available, no need to add it!
4762
+ // parent is already available, no need to add it!
4763
+ if (!this.invisible.has(changeTree)) {
4044
4764
  return;
4045
4765
  }
4046
- this.addParent(parentChangeTree, tag);
4047
4766
  // add parent's tag properties
4048
- if (parentChangeTree.getChange(parentIndex) !== exports.OPERATION.DELETE) {
4049
- let parentChanges = this.changes.get(parentChangeTree);
4050
- if (parentChanges === undefined) {
4051
- parentChanges = new Map();
4052
- this.changes.set(parentChangeTree, parentChanges);
4767
+ if (changeTree.getChange(parentIndex) !== exports.OPERATION.DELETE) {
4768
+ let changes = this.changes[changeTree.refId];
4769
+ if (changes === undefined) {
4770
+ changes = {};
4771
+ this.changes[changeTree.refId] = changes;
4053
4772
  }
4054
- // console.log("add parent change", {
4055
- // parentIndex,
4056
- // parentChanges,
4057
- // parentChange: (
4058
- // parentChangeTree.getChange(parentIndex) &&
4059
- // OPERATION[parentChangeTree.getChange(parentIndex)]
4060
- // ),
4061
- // })
4062
4773
  if (!this.tags) {
4063
4774
  this.tags = new WeakMap();
4064
4775
  }
4065
4776
  let tags;
4066
- if (!this.tags.has(parentChangeTree)) {
4777
+ if (!this.tags.has(changeTree)) {
4067
4778
  tags = new Set();
4068
- this.tags.set(parentChangeTree, tags);
4779
+ this.tags.set(changeTree, tags);
4069
4780
  }
4070
4781
  else {
4071
- tags = this.tags.get(parentChangeTree);
4782
+ tags = this.tags.get(changeTree);
4072
4783
  }
4073
4784
  tags.add(tag);
4074
- parentChanges.set(parentIndex, exports.OPERATION.ADD);
4785
+ changes[parentIndex] = exports.OPERATION.ADD;
4075
4786
  }
4076
4787
  }
4077
4788
  remove(obj, tag = DEFAULT_VIEW_TAG) {
@@ -4083,32 +4794,32 @@
4083
4794
  this.items.delete(changeTree);
4084
4795
  const ref = changeTree.ref;
4085
4796
  const metadata = ref.constructor[Symbol.metadata];
4086
- let changes = this.changes.get(changeTree);
4797
+ let changes = this.changes[changeTree.refId];
4087
4798
  if (changes === undefined) {
4088
- changes = new Map();
4089
- this.changes.set(changeTree, changes);
4799
+ changes = {};
4800
+ this.changes[changeTree.refId] = changes;
4090
4801
  }
4091
4802
  if (tag === DEFAULT_VIEW_TAG) {
4092
4803
  // parent is collection (Map/Array)
4093
4804
  const parent = changeTree.parent;
4094
4805
  if (!Metadata.isValidInstance(parent)) {
4095
4806
  const parentChangeTree = parent[$changes];
4096
- let changes = this.changes.get(parentChangeTree);
4807
+ let changes = this.changes[parentChangeTree.refId];
4097
4808
  if (changes === undefined) {
4098
- changes = new Map();
4099
- this.changes.set(parentChangeTree, changes);
4809
+ changes = {};
4810
+ this.changes[parentChangeTree.refId] = changes;
4100
4811
  }
4101
4812
  // DELETE / DELETE BY REF ID
4102
- changes.set(changeTree.parentIndex, exports.OPERATION.DELETE);
4813
+ changes[changeTree.parentIndex] = exports.OPERATION.DELETE;
4103
4814
  }
4104
4815
  else {
4105
4816
  // delete all "tagged" properties.
4106
- metadata[-2].forEach((index) => changes.set(index, exports.OPERATION.DELETE));
4817
+ metadata[$viewFieldIndexes].forEach((index) => changes[index] = exports.OPERATION.DELETE);
4107
4818
  }
4108
4819
  }
4109
4820
  else {
4110
4821
  // delete only tagged properties
4111
- metadata[-3][tag].forEach((index) => changes.set(index, exports.OPERATION.DELETE));
4822
+ metadata[$fieldIndexesByViewTag][tag].forEach((index) => changes[index] = exports.OPERATION.DELETE);
4112
4823
  }
4113
4824
  // remove tag
4114
4825
  if (this.tags && this.tags.has(changeTree)) {
@@ -4166,10 +4877,11 @@
4166
4877
  exports.encode = encode;
4167
4878
  exports.encodeKeyValueOperation = encodeArray;
4168
4879
  exports.encodeSchemaOperation = encodeSchemaOperation;
4880
+ exports.getDecoderStateCallbacks = getDecoderStateCallbacks;
4881
+ exports.getRawChangesCallback = getRawChangesCallback;
4169
4882
  exports.registerType = registerType;
4883
+ exports.schema = schema;
4170
4884
  exports.type = type;
4171
4885
  exports.view = view;
4172
4886
 
4173
- Object.defineProperty(exports, '__esModule', { value: true });
4174
-
4175
4887
  }));