@colyseus/schema 3.0.11 → 3.0.13

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 (38) hide show
  1. package/build/cjs/index.js +99 -108
  2. package/build/cjs/index.js.map +1 -1
  3. package/build/esm/index.mjs +99 -108
  4. package/build/esm/index.mjs.map +1 -1
  5. package/build/umd/index.js +99 -108
  6. package/lib/Metadata.js +1 -2
  7. package/lib/Metadata.js.map +1 -1
  8. package/lib/annotations.js +4 -3
  9. package/lib/annotations.js.map +1 -1
  10. package/lib/decoder/DecodeOperation.js +0 -13
  11. package/lib/decoder/DecodeOperation.js.map +1 -1
  12. package/lib/encoder/ChangeTree.d.ts +1 -1
  13. package/lib/encoder/ChangeTree.js +1 -0
  14. package/lib/encoder/ChangeTree.js.map +1 -1
  15. package/lib/encoder/Encoder.d.ts +0 -1
  16. package/lib/encoder/Encoder.js +27 -33
  17. package/lib/encoder/Encoder.js.map +1 -1
  18. package/lib/encoder/Root.d.ts +1 -1
  19. package/lib/encoder/Root.js +1 -0
  20. package/lib/encoder/Root.js.map +1 -1
  21. package/lib/encoder/StateView.d.ts +4 -4
  22. package/lib/encoder/StateView.js +33 -21
  23. package/lib/encoder/StateView.js.map +1 -1
  24. package/lib/types/custom/ArraySchema.js +5 -1
  25. package/lib/types/custom/ArraySchema.js.map +1 -1
  26. package/lib/types/custom/MapSchema.d.ts +3 -0
  27. package/lib/types/custom/MapSchema.js +28 -35
  28. package/lib/types/custom/MapSchema.js.map +1 -1
  29. package/package.json +1 -1
  30. package/src/Metadata.ts +1 -2
  31. package/src/annotations.ts +4 -4
  32. package/src/decoder/DecodeOperation.ts +0 -14
  33. package/src/encoder/ChangeTree.ts +2 -2
  34. package/src/encoder/Encoder.ts +31 -44
  35. package/src/encoder/Root.ts +1 -0
  36. package/src/encoder/StateView.ts +34 -22
  37. package/src/types/custom/ArraySchema.ts +8 -1
  38. package/src/types/custom/MapSchema.ts +29 -43
@@ -1,6 +1,6 @@
1
1
  import type { Schema } from "../Schema";
2
2
  import { TypeContext } from "../types/TypeContext";
3
- import { $changes, $encoder, $filter, $onEncodeEnd } from "../types/symbols";
3
+ import { $changes, $encoder, $filter, $getByIndex } from "../types/symbols";
4
4
 
5
5
  import { encode } from "../encoding/encode";
6
6
  import type { Iterator } from "../encoding/decode";
@@ -10,6 +10,7 @@ import { Root } from "./Root";
10
10
 
11
11
  import type { StateView } from "./StateView";
12
12
  import type { Metadata } from "../Metadata";
13
+ import type { ChangeTree } from "./ChangeTree";
13
14
 
14
15
  export class Encoder<T extends Schema = any> {
15
16
  static BUFFER_SIZE = (typeof(Buffer) !== "undefined") && Buffer.poolSize || 8 * 1024; // 8KB
@@ -58,14 +59,6 @@ export class Encoder<T extends Schema = any> {
58
59
  for (let i = 0, numChangeTrees = changeTrees.length; i < numChangeTrees; i++) {
59
60
  const changeTree = changeTrees[i];
60
61
 
61
- const operations = changeTree[changeSetName];
62
- const ref = changeTree.ref;
63
-
64
- const ctor = ref.constructor;
65
- const encoder = ctor[$encoder];
66
- const filter = ctor[$filter];
67
- const metadata = ctor[Symbol.metadata];
68
-
69
62
  if (hasView) {
70
63
  if (!view.items.has(changeTree)) {
71
64
  view.invisible.add(changeTree);
@@ -76,6 +69,18 @@ export class Encoder<T extends Schema = any> {
76
69
  }
77
70
  }
78
71
 
72
+ const operations = changeTree[changeSetName];
73
+ const ref = changeTree.ref;
74
+
75
+ // TODO: avoid iterating over change tree if no changes were made
76
+ const numChanges = operations.operations.length;
77
+ if (numChanges === 0) { continue; }
78
+
79
+ const ctor = ref.constructor;
80
+ const encoder = ctor[$encoder];
81
+ const filter = ctor[$filter];
82
+ const metadata = ctor[Symbol.metadata];
83
+
79
84
  // skip root `refId` if it's the first change tree
80
85
  // (unless it "hasView", which will need to revisit the root)
81
86
  if (hasView || it.offset > initialOffset || changeTree !== rootChangeTree) {
@@ -83,7 +88,7 @@ export class Encoder<T extends Schema = any> {
83
88
  encode.number(buffer, changeTree.refId, it);
84
89
  }
85
90
 
86
- for (let j = 0, numChanges = operations.operations.length; j < numChanges; j++) {
91
+ for (let j = 0; j < numChanges; j++) {
87
92
  const fieldIndex = operations.operations[j];
88
93
 
89
94
  const operation = (fieldIndex < 0)
@@ -199,17 +204,18 @@ export class Encoder<T extends Schema = any> {
199
204
  const viewOffset = it.offset;
200
205
 
201
206
  // encode visibility changes (add/remove for this view)
202
- const refIds = Object.keys(view.changes);
207
+ for (const [refId, changes] of view.changes) {
208
+ const changeTree: ChangeTree = this.root.changeTrees[refId];
203
209
 
204
- for (let i = 0, numRefIds = refIds.length; i < numRefIds; i++) {
205
- const refId = refIds[i];
206
- const changes = view.changes[refId];
207
- const changeTree = this.root.changeTrees[refId];
210
+ if (changeTree === undefined) {
211
+ // detached instance, remove from view and skip.
212
+ view.changes.delete(refId);
213
+ continue;
214
+ }
208
215
 
209
- if (
210
- changeTree === undefined ||
211
- Object.keys(changes).length === 0 // FIXME: avoid having empty changes if no changes were made
212
- ) {
216
+ const keys = Object.keys(changes);
217
+ if (keys.length === 0) {
218
+ // FIXME: avoid having empty changes if no changes were made
213
219
  // console.log("changes.size === 0, skip", changeTree.ref.constructor.name);
214
220
  continue;
215
221
  }
@@ -223,14 +229,15 @@ export class Encoder<T extends Schema = any> {
223
229
  bytes[it.offset++] = SWITCH_TO_STRUCTURE & 255;
224
230
  encode.number(bytes, changeTree.refId, it);
225
231
 
226
- const keys = Object.keys(changes);
227
232
  for (let i = 0, numChanges = keys.length; i < numChanges; i++) {
228
- const key = keys[i];
229
- const operation = changes[key];
233
+ const index = Number(keys[i]);
234
+ // workaround when using view.add() on item that has been deleted from state (see test "adding to view item that has been removed from state")
235
+ const value = changeTree.ref[$getByIndex](index);
236
+ const operation = (value !== undefined && changes[index]) || OPERATION.DELETE;
230
237
 
231
238
  // isEncodeAll = false
232
239
  // hasView = true
233
- encoder(this, bytes, changeTree, Number(key), operation, it, false, true, metadata);
240
+ encoder(this, bytes, changeTree, index, operation, it, false, true, metadata);
234
241
  }
235
242
  }
236
243
 
@@ -239,7 +246,7 @@ export class Encoder<T extends Schema = any> {
239
246
  // (to allow re-using StateView's for multiple clients)
240
247
  //
241
248
  // clear "view" changes after encoding
242
- view.changes = {};
249
+ view.changes.clear();
243
250
 
244
251
  // try to encode "filtered" changes
245
252
  this.encode(it, view, bytes, "filteredChanges", false, viewOffset);
@@ -250,26 +257,6 @@ export class Encoder<T extends Schema = any> {
250
257
  ]);
251
258
  }
252
259
 
253
- onEndEncode(changeTrees = this.root.changes) {
254
- // changeTrees.forEach(function(changeTree) {
255
- // changeTree.endEncode();
256
- // });
257
-
258
-
259
- // for (const refId in changeTrees) {
260
- // const changeTree = this.root.changeTrees[refId];
261
- // changeTree.endEncode();
262
-
263
- // // changeTree.changes.clear();
264
-
265
- // // // ArraySchema and MapSchema have a custom "encode end" method
266
- // // changeTree.ref[$onEncodeEnd]?.();
267
-
268
- // // // Not a new instance anymore
269
- // // delete changeTree[$isNew];
270
- // }
271
- }
272
-
273
260
  discardChanges() {
274
261
  // discard shared changes
275
262
  let length = this.root.changes.length;
@@ -100,6 +100,7 @@ export class Root {
100
100
  if (spliceOne(changeSet, changeSet.indexOf(changeTree))) {
101
101
  changeTree[changeSetName].queueRootIndex = -1;
102
102
  // changeSet[index] = undefined;
103
+ return true;
103
104
  }
104
105
  }
105
106
 
@@ -25,7 +25,8 @@ export class StateView {
25
25
  * Manual "ADD" operations for changes per ChangeTree, specific to this view.
26
26
  * (This is used to force encoding a property, even if it was not changed)
27
27
  */
28
- changes: { [refId: number]: IndexedOperations } = {};
28
+ // TODO: use map here!? may fix encode ordering issue
29
+ changes = new Map<number, IndexedOperations>();
29
30
 
30
31
  // TODO: allow to set multiple tags at once
31
32
  add(obj: Ref, tag: number = DEFAULT_VIEW_TAG, checkIncludeParent: boolean = true) {
@@ -43,17 +44,17 @@ export class StateView {
43
44
  // - if it was invisible to this view
44
45
  // - if it were previously filtered out
45
46
  if (checkIncludeParent && changeTree.parent) {
46
- this.addParent(changeTree.parent[$changes], changeTree.parentIndex, tag);
47
+ this.addParentOf(changeTree, tag);
47
48
  }
48
49
 
49
50
  //
50
51
  // TODO: when adding an item of a MapSchema, the changes may not
51
52
  // be set (only the parent's changes are set)
52
53
  //
53
- let changes = this.changes[changeTree.refId];
54
+ let changes = this.changes.get(changeTree.refId);
54
55
  if (changes === undefined) {
55
56
  changes = {};
56
- this.changes[changeTree.refId] = changes;
57
+ this.changes.set(changeTree.refId, changes);
57
58
  }
58
59
 
59
60
  // set tag
@@ -118,28 +119,30 @@ export class StateView {
118
119
  return this;
119
120
  }
120
121
 
121
- protected addParent(changeTree: ChangeTree, parentIndex: number, tag: number) {
122
- // view must have all "changeTree" parent tree
123
- this.items.add(changeTree);
122
+ protected addParentOf(childChangeTree: ChangeTree, tag: number) {
123
+ const changeTree = childChangeTree.parent[$changes];
124
+ const parentIndex = childChangeTree.parentIndex;
124
125
 
125
- // add parent's parent
126
- const parentChangeTree: ChangeTree = changeTree.parent?.[$changes];
127
- if (parentChangeTree && (parentChangeTree.filteredChanges !== undefined)) {
128
- this.addParent(parentChangeTree, changeTree.parentIndex, tag);
129
- }
126
+ if (!this.items.has(changeTree)) {
127
+ // view must have all "changeTree" parent tree
128
+ this.items.add(changeTree);
130
129
 
131
- // parent is already available, no need to add it!
132
- if (!this.invisible.has(changeTree)) {
133
- return;
130
+ // add parent's parent
131
+ const parentChangeTree: ChangeTree = changeTree.parent?.[$changes];
132
+ if (parentChangeTree && (parentChangeTree.filteredChanges !== undefined)) {
133
+ this.addParentOf(changeTree, tag);
134
+ }
135
+
136
+ // parent is already available, no need to add it!
137
+ if (!this.invisible.has(changeTree)) { return; }
134
138
  }
135
139
 
136
140
  // add parent's tag properties
137
141
  if (changeTree.getChange(parentIndex) !== OPERATION.DELETE) {
138
-
139
- let changes = this.changes[changeTree.refId];
142
+ let changes = this.changes.get(changeTree.refId);
140
143
  if (changes === undefined) {
141
144
  changes = {};
142
- this.changes[changeTree.refId] = changes;
145
+ this.changes.set(changeTree.refId, changes);
143
146
  }
144
147
 
145
148
  if (!this.tags) {
@@ -171,10 +174,10 @@ export class StateView {
171
174
  const ref = changeTree.ref;
172
175
  const metadata: Metadata = ref.constructor[Symbol.metadata];
173
176
 
174
- let changes = this.changes[changeTree.refId];
177
+ let changes = this.changes.get(changeTree.refId);
175
178
  if (changes === undefined) {
176
179
  changes = {};
177
- this.changes[changeTree.refId] = changes;
180
+ this.changes.set(changeTree.refId, changes);
178
181
  }
179
182
 
180
183
  if (tag === DEFAULT_VIEW_TAG) {
@@ -182,10 +185,10 @@ export class StateView {
182
185
  const parent = changeTree.parent;
183
186
  if (!Metadata.isValidInstance(parent)) {
184
187
  const parentChangeTree = parent[$changes];
185
- let changes = this.changes[parentChangeTree.refId];
188
+ let changes = this.changes.get(parentChangeTree.refId);
186
189
  if (changes === undefined) {
187
190
  changes = {};
188
- this.changes[parentChangeTree.refId] = changes;
191
+ this.changes.set(parentChangeTree.refId, changes);
189
192
  }
190
193
  // DELETE / DELETE BY REF ID
191
194
  changes[changeTree.parentIndex] = OPERATION.DELETE;
@@ -222,4 +225,13 @@ export class StateView {
222
225
 
223
226
  return this;
224
227
  }
228
+
229
+ has(obj: Ref) {
230
+ return this.items.has(obj[$changes]);
231
+ }
232
+
233
+ hasTag(ob: Ref, tag: number = DEFAULT_VIEW_TAG) {
234
+ const tags = this.tags?.get(ob[$changes]);
235
+ return tags?.has(tag) ?? false;
236
+ }
225
237
  }
@@ -92,7 +92,8 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V> {
92
92
  if (setValue[$changes]) {
93
93
  assertInstanceType(setValue, obj[$childType] as typeof Schema, obj, key);
94
94
 
95
- if (obj.items[key as unknown as number] !== undefined) {
95
+ const previousValue = obj.items[key as unknown as number];
96
+ if (previousValue !== undefined) {
96
97
  if (setValue[$changes].isNew) {
97
98
  this[$changes].indexedOperation(Number(key), OPERATION.MOVE_AND_ADD);
98
99
 
@@ -103,10 +104,16 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V> {
103
104
  this[$changes].indexedOperation(Number(key), OPERATION.MOVE);
104
105
  }
105
106
  }
107
+
108
+ // remove root reference from previous value
109
+ previousValue[$changes].root?.remove(previousValue[$changes]);
110
+
106
111
  } else if (setValue[$changes].isNew) {
107
112
  this[$changes].indexedOperation(Number(key), OPERATION.ADD);
108
113
  }
109
114
 
115
+ setValue[$changes].setParent(this, obj[$changes].root, key);
116
+
110
117
  } else {
111
118
  obj.$changeAt(Number(key), setValue);
112
119
  }
@@ -14,6 +14,7 @@ export class MapSchema<V=any, K extends string = string> implements Map<K, V>, C
14
14
 
15
15
  protected $items: Map<K, V> = new Map<K, V>();
16
16
  protected $indexes: Map<number, K> = new Map<number, K>();
17
+ protected deletedItems: { [field: string]: V } = {};
17
18
 
18
19
  protected [$changes]: ChangeTree;
19
20
 
@@ -31,9 +32,9 @@ export class MapSchema<V=any, K extends string = string> implements Map<K, V>, C
31
32
  */
32
33
  static [$filter] (ref: MapSchema, index: number, view: StateView) {
33
34
  return (
34
- !view ||
35
+ !view ||
35
36
  typeof (ref[$childType]) === "string" ||
36
- view.items.has(ref[$getByIndex](index)[$changes])
37
+ view.items.has((ref[$getByIndex](index) ?? ref.deletedItems[index])[$changes])
37
38
  );
38
39
  }
39
40
 
@@ -86,41 +87,38 @@ export class MapSchema<V=any, K extends string = string> implements Map<K, V>, C
86
87
  key = key.toString() as K;
87
88
 
88
89
  const changeTree = this[$changes];
90
+ const isRef = (value[$changes]) !== undefined;
89
91
 
90
- // get "index" for this value.
91
- const isReplace = typeof(changeTree.indexes[key]) !== "undefined";
92
+ let index: number;
93
+ let operation: OPERATION;
92
94
 
93
- const index = (isReplace)
94
- ? changeTree.indexes[key]
95
- : changeTree.indexes[$numFields] ?? 0;
95
+ // IS REPLACE?
96
+ if (typeof(changeTree.indexes[key]) !== "undefined") {
97
+ index = changeTree.indexes[key];
98
+ operation = OPERATION.REPLACE;
96
99
 
97
- let operation: OPERATION = (isReplace)
98
- ? OPERATION.REPLACE
99
- : OPERATION.ADD;
100
+ const previousValue = this.$items.get(key);
101
+ if (previousValue === value) {
102
+ // if value is the same, avoid re-encoding it.
103
+ return;
100
104
 
101
- const isRef = (value[$changes]) !== undefined;
105
+ } else if (isRef) {
106
+ // if is schema, force ADD operation if value differ from previous one.
107
+ operation = OPERATION.DELETE_AND_ADD;
108
+
109
+ // remove reference from previous value
110
+ if (previousValue !== undefined) {
111
+ previousValue[$changes].root?.remove(previousValue[$changes]);
112
+ }
113
+ }
114
+
115
+ } else {
116
+ index = changeTree.indexes[$numFields] ?? 0;
117
+ operation = OPERATION.ADD;
102
118
 
103
- //
104
- // (encoding)
105
- // set a unique id to relate directly with this key/value.
106
- //
107
- if (!isReplace) {
108
119
  this.$indexes.set(index, key);
109
120
  changeTree.indexes[key] = index;
110
121
  changeTree.indexes[$numFields] = index + 1;
111
-
112
- } else if (
113
- !isRef &&
114
- this.$items.get(key) === value
115
- ) {
116
- // if value is the same, avoid re-encoding it.
117
- return;
118
-
119
- } else if (
120
- isRef && // if is schema, force ADD operation if value differ from previous one.
121
- this.$items.get(key) !== value
122
- ) {
123
- operation = OPERATION.ADD;
124
122
  }
125
123
 
126
124
  this.$items.set(key, value);
@@ -145,7 +143,7 @@ export class MapSchema<V=any, K extends string = string> implements Map<K, V>, C
145
143
  delete(key: K) {
146
144
  const index = this[$changes].indexes[key];
147
145
 
148
- this[$changes].delete(index);
146
+ this.deletedItems[index] = this[$changes].delete(index);;
149
147
 
150
148
  return this.$items.delete(key);
151
149
  }
@@ -209,19 +207,7 @@ export class MapSchema<V=any, K extends string = string> implements Map<K, V>, C
209
207
  }
210
208
 
211
209
  protected [$onEncodeEnd]() {
212
- const changeTree = this[$changes];
213
- const keys = Object.keys(changeTree.indexedOperations);
214
-
215
- for (let i = 0, len = keys.length; i < len; i++) {
216
- const key = keys[i];
217
- const fieldIndex = Number(key);
218
- const operation = changeTree.indexedOperations[key];
219
-
220
- if (operation === OPERATION.DELETE) {
221
- const index = this[$getByIndex](fieldIndex) as string;
222
- delete changeTree.indexes[index];
223
- }
224
- }
210
+ this.deletedItems = {};
225
211
  }
226
212
 
227
213
  toJSON() {