@colyseus/schema 4.0.21 → 4.0.23
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.
- package/build/decoder/strategy/Callbacks.d.ts +5 -5
- package/build/encoder/Encoder.d.ts +17 -0
- package/build/encoder/StateView.d.ts +26 -0
- package/build/index.cjs +107 -26
- package/build/index.cjs.map +1 -1
- package/build/index.js +107 -26
- package/build/index.mjs +107 -26
- package/build/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/decoder/strategy/Callbacks.ts +15 -13
- package/src/encoder/Encoder.ts +67 -2
- package/src/encoder/StateView.ts +47 -24
package/package.json
CHANGED
|
@@ -88,13 +88,13 @@ export class StateCallbackStrategy<TState extends IRef> {
|
|
|
88
88
|
if (!collection || collection[$refId] === undefined) {
|
|
89
89
|
let removePropertyCallback: () => void;
|
|
90
90
|
removePropertyCallback = this.addCallback(
|
|
91
|
-
instance[$refId]
|
|
91
|
+
instance[$refId]!,
|
|
92
92
|
propertyName,
|
|
93
93
|
(value: TReturn, _: TReturn) => {
|
|
94
94
|
if (value !== null && value !== undefined) {
|
|
95
95
|
// Remove the property listener now that collection is available
|
|
96
96
|
removePropertyCallback();
|
|
97
|
-
removeHandler = this.addCallback(value[$refId]
|
|
97
|
+
removeHandler = this.addCallback(value[$refId]!, operation, handler);
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
);
|
|
@@ -113,7 +113,7 @@ export class StateCallbackStrategy<TState extends IRef> {
|
|
|
113
113
|
});
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
return this.addCallback(collection[$refId]
|
|
116
|
+
return this.addCallback(collection[$refId]!, operation, handler);
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
|
|
@@ -162,13 +162,13 @@ export class StateCallbackStrategy<TState extends IRef> {
|
|
|
162
162
|
handler(currentValue, undefined as any);
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
return this.addCallback(instance[$refId]
|
|
165
|
+
return this.addCallback(instance[$refId]!, propertyName, handler);
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
/**
|
|
169
169
|
* Listen to any property change on an instance.
|
|
170
170
|
*/
|
|
171
|
-
onChange<TInstance extends
|
|
171
|
+
onChange<TInstance extends object>(
|
|
172
172
|
instance: TInstance,
|
|
173
173
|
handler: InstanceChangeCallback
|
|
174
174
|
): () => void;
|
|
@@ -184,7 +184,7 @@ export class StateCallbackStrategy<TState extends IRef> {
|
|
|
184
184
|
/**
|
|
185
185
|
* Listen to item changes in a nested collection.
|
|
186
186
|
*/
|
|
187
|
-
onChange<TInstance extends
|
|
187
|
+
onChange<TInstance extends object, K extends CollectionPropNames<TInstance>>(
|
|
188
188
|
instance: TInstance,
|
|
189
189
|
property: K,
|
|
190
190
|
handler: KeyValueCallback<CollectionKeyType<TInstance, K>, CollectionValueType<TInstance, K>>
|
|
@@ -195,7 +195,7 @@ export class StateCallbackStrategy<TState extends IRef> {
|
|
|
195
195
|
// onChange(instance, handler) - instance change
|
|
196
196
|
const instance = args[0] as Schema;
|
|
197
197
|
const handler = args[1] as InstanceChangeCallback;
|
|
198
|
-
return this.addCallback(instance[$refId]
|
|
198
|
+
return this.addCallback(instance[$refId]!, OPERATION.REPLACE, handler);
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
if (typeof args[0] === 'string') {
|
|
@@ -229,7 +229,7 @@ export class StateCallbackStrategy<TState extends IRef> {
|
|
|
229
229
|
/**
|
|
230
230
|
* Listen to items added to a nested collection.
|
|
231
231
|
*/
|
|
232
|
-
onAdd<TInstance
|
|
232
|
+
onAdd<TInstance, K extends CollectionPropNames<TInstance>>(
|
|
233
233
|
instance: TInstance,
|
|
234
234
|
property: K,
|
|
235
235
|
handler: ValueKeyCallback<CollectionValueType<TInstance, K>, CollectionKeyType<TInstance, K>>,
|
|
@@ -269,7 +269,7 @@ export class StateCallbackStrategy<TState extends IRef> {
|
|
|
269
269
|
/**
|
|
270
270
|
* Listen to items removed from a nested collection.
|
|
271
271
|
*/
|
|
272
|
-
onRemove<TInstance
|
|
272
|
+
onRemove<TInstance, K extends CollectionPropNames<TInstance>>(
|
|
273
273
|
instance: TInstance,
|
|
274
274
|
property: K,
|
|
275
275
|
handler: ValueKeyCallback<CollectionValueType<TInstance, K>, CollectionKeyType<TInstance, K>>
|
|
@@ -299,7 +299,7 @@ export class StateCallbackStrategy<TState extends IRef> {
|
|
|
299
299
|
* Bind properties from a Schema instance to a target object.
|
|
300
300
|
* Changes will be automatically reflected on the target object.
|
|
301
301
|
*/
|
|
302
|
-
bindTo<TInstance
|
|
302
|
+
bindTo<TInstance, TTarget>(
|
|
303
303
|
from: TInstance,
|
|
304
304
|
to: TTarget,
|
|
305
305
|
properties?: string[],
|
|
@@ -327,7 +327,7 @@ export class StateCallbackStrategy<TState extends IRef> {
|
|
|
327
327
|
action();
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
-
return this.addCallback(from[$refId]
|
|
330
|
+
return this.addCallback((from as IRef)[$refId]!, OPERATION.REPLACE, action);
|
|
331
331
|
}
|
|
332
332
|
|
|
333
333
|
protected triggerChanges(allChanges: DataChange[]): void {
|
|
@@ -350,7 +350,7 @@ export class StateCallbackStrategy<TState extends IRef> {
|
|
|
350
350
|
(change.op & OPERATION.DELETE) === OPERATION.DELETE &&
|
|
351
351
|
Schema.isSchema(change.previousValue)
|
|
352
352
|
) {
|
|
353
|
-
const childRefId = (change.previousValue as Ref)[$refId]
|
|
353
|
+
const childRefId = (change.previousValue as Ref)[$refId]!;
|
|
354
354
|
const deleteCallbacks = this.callbacks[childRefId]?.[OPERATION.DELETE];
|
|
355
355
|
if (deleteCallbacks) {
|
|
356
356
|
for (let j = deleteCallbacks.length - 1; j >= 0; j--) {
|
|
@@ -518,8 +518,10 @@ export const Callbacks = {
|
|
|
518
518
|
return getDecoderStateCallbacks(roomOrDecoder);
|
|
519
519
|
|
|
520
520
|
} else if ('decoder' in roomOrDecoder.serializer) {
|
|
521
|
-
return getDecoderStateCallbacks(roomOrDecoder.serializer.decoder);
|
|
521
|
+
return getDecoderStateCallbacks((roomOrDecoder.serializer as { decoder: Decoder<T> }).decoder);
|
|
522
522
|
}
|
|
523
|
+
|
|
524
|
+
throw new Error('Invalid room or decoder');
|
|
523
525
|
},
|
|
524
526
|
|
|
525
527
|
getRawChanges(decoder: Decoder, callback: (changes: DataChange[]) => void) {
|
package/src/encoder/Encoder.ts
CHANGED
|
@@ -190,8 +190,26 @@ export class Encoder<T extends Schema = any> {
|
|
|
190
190
|
) {
|
|
191
191
|
const viewOffset = it.offset;
|
|
192
192
|
|
|
193
|
-
//
|
|
194
|
-
|
|
193
|
+
//
|
|
194
|
+
// Iterate `view.changes` in topological order so a refId is never
|
|
195
|
+
// SWITCH_TO_STRUCTURE'd before an earlier op has introduced it on
|
|
196
|
+
// the decoder. Map insertion order alone isn't sufficient: a
|
|
197
|
+
// sequence like view.remove(child) → view.add(child) on a child
|
|
198
|
+
// whose ancestor wasn't yet visible can put the child entry into
|
|
199
|
+
// the Map before its newly-visible ancestor.
|
|
200
|
+
//
|
|
201
|
+
// Hot-path optimization: `view.add` preserves topo order by
|
|
202
|
+
// construction (addParentOf walks deepest-ancestor-first before
|
|
203
|
+
// touching the obj's own entry). Only `view.remove` can leave the
|
|
204
|
+
// Map dirty. `StateView.changesOutOfOrder` tracks this so most
|
|
205
|
+
// encodes can iterate `view.changes` directly, paying nothing.
|
|
206
|
+
//
|
|
207
|
+
const orderedRefIds: Iterable<number> = view.changesOutOfOrder
|
|
208
|
+
? this.topoOrderViewChanges(view)
|
|
209
|
+
: view.changes.keys();
|
|
210
|
+
|
|
211
|
+
for (const refId of orderedRefIds) {
|
|
212
|
+
const changes = view.changes.get(refId);
|
|
195
213
|
const changeTree: ChangeTree = this.root.changeTrees[refId];
|
|
196
214
|
|
|
197
215
|
if (changeTree === undefined) {
|
|
@@ -235,6 +253,7 @@ export class Encoder<T extends Schema = any> {
|
|
|
235
253
|
//
|
|
236
254
|
// clear "view" changes after encoding
|
|
237
255
|
view.changes.clear();
|
|
256
|
+
view.changesOutOfOrder = false;
|
|
238
257
|
|
|
239
258
|
// try to encode "filtered" changes
|
|
240
259
|
this.encode(it, view, bytes, "filteredChanges", false, viewOffset);
|
|
@@ -245,6 +264,52 @@ export class Encoder<T extends Schema = any> {
|
|
|
245
264
|
);
|
|
246
265
|
}
|
|
247
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Produce a topological ordering of `view.changes` keys so each refId
|
|
269
|
+
* is preceded by any ancestor that's also in the same view's changeset.
|
|
270
|
+
*
|
|
271
|
+
* The wire stream uses SWITCH_TO_STRUCTURE pointers; if a child is
|
|
272
|
+
* encoded before any earlier op has introduced its refId on the
|
|
273
|
+
* decoder, decode fails with "refId not found". An entry's refId can
|
|
274
|
+
* only be introduced by an ADD on one of its ancestors — so any
|
|
275
|
+
* ancestor that itself appears in this view's pending changes must
|
|
276
|
+
* be encoded first.
|
|
277
|
+
*
|
|
278
|
+
* Implementation: DFS post-order over the parent chain. The `visited`
|
|
279
|
+
* Set guards against duplicates; cycles are not expected in a
|
|
280
|
+
* well-formed parent chain but the visited check is a cheap safety
|
|
281
|
+
* net. Cost is O(n × d) for n entries with parent-chain depth d.
|
|
282
|
+
*/
|
|
283
|
+
protected topoOrderViewChanges(view: StateView): number[] {
|
|
284
|
+
const result: number[] = [];
|
|
285
|
+
const visited = new Set<number>();
|
|
286
|
+
|
|
287
|
+
const visit = (refId: number) => {
|
|
288
|
+
if (visited.has(refId)) { return; }
|
|
289
|
+
visited.add(refId);
|
|
290
|
+
|
|
291
|
+
const changeTree = this.root.changeTrees[refId];
|
|
292
|
+
if (changeTree !== undefined) {
|
|
293
|
+
let chain = changeTree.parentChain;
|
|
294
|
+
while (chain) {
|
|
295
|
+
const parentRefId = chain.ref[$refId];
|
|
296
|
+
if (parentRefId !== undefined && view.changes.has(parentRefId)) {
|
|
297
|
+
visit(parentRefId);
|
|
298
|
+
}
|
|
299
|
+
chain = chain.next;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
result.push(refId);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
for (const refId of view.changes.keys()) {
|
|
307
|
+
visit(refId);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
|
|
248
313
|
discardChanges() {
|
|
249
314
|
// discard shared changes
|
|
250
315
|
let current = this.root.changes.next;
|
package/src/encoder/StateView.ts
CHANGED
|
@@ -35,12 +35,47 @@ export class StateView {
|
|
|
35
35
|
*/
|
|
36
36
|
changes = new Map<number, IndexedOperations>();
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Set when an operation may have left `changes` out of topological
|
|
40
|
+
* order (a parent that needs to be encoded before its descendants is
|
|
41
|
+
* positioned after them in the Map). `Encoder.encodeView` consults
|
|
42
|
+
* this flag and only runs the topo-ordering pass when it's true,
|
|
43
|
+
* skipping the work in the common case where insertion order already
|
|
44
|
+
* coincides with topo order.
|
|
45
|
+
*
|
|
46
|
+
* Only `remove()` can break the invariant: it writes entries that
|
|
47
|
+
* bypass `addParentOf`'s deepest-ancestor-first ordering. Everything
|
|
48
|
+
* else (including multi-parent re-adds) preserves order by
|
|
49
|
+
* construction. Reset to false at the end of each encodeView pass
|
|
50
|
+
* (when `changes` is cleared).
|
|
51
|
+
*/
|
|
52
|
+
changesOutOfOrder: boolean = false;
|
|
53
|
+
|
|
38
54
|
constructor(public iterable: boolean = false) {
|
|
39
55
|
if (iterable) {
|
|
40
56
|
this.items = [];
|
|
41
57
|
}
|
|
42
58
|
}
|
|
43
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Get the IndexedOperations entry for `refId`, creating one if missing.
|
|
62
|
+
*
|
|
63
|
+
* Map insertion order alone doesn't guarantee parent-before-child
|
|
64
|
+
* iteration in all cases (a `view.remove()` followed by `view.add()`
|
|
65
|
+
* can put a child entry into the Map before its newly-visible
|
|
66
|
+
* ancestor). The wire-order invariant (parent SWITCH_TO_STRUCTURE
|
|
67
|
+
* before any of its children's) is enforced at encode time by
|
|
68
|
+
* `Encoder.encodeView` via a topological pass over `view.changes`.
|
|
69
|
+
*/
|
|
70
|
+
protected touchChanges(refId: number): IndexedOperations {
|
|
71
|
+
let entry = this.changes.get(refId);
|
|
72
|
+
if (entry === undefined) {
|
|
73
|
+
entry = {};
|
|
74
|
+
this.changes.set(refId, entry);
|
|
75
|
+
}
|
|
76
|
+
return entry;
|
|
77
|
+
}
|
|
78
|
+
|
|
44
79
|
// TODO: allow to set multiple tags at once
|
|
45
80
|
add(obj: Ref, tag: number = DEFAULT_VIEW_TAG, checkIncludeParent: boolean = true) {
|
|
46
81
|
const changeTree: ChangeTree = obj?.[$changes];
|
|
@@ -82,12 +117,8 @@ export class StateView {
|
|
|
82
117
|
this.addParentOf(changeTree, tag);
|
|
83
118
|
}
|
|
84
119
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
changes = {};
|
|
88
|
-
// FIXME / OPTIMIZE: do not add if no changes are needed
|
|
89
|
-
this.changes.set(obj[$refId], changes);
|
|
90
|
-
}
|
|
120
|
+
// FIXME / OPTIMIZE: do not add if no changes are needed
|
|
121
|
+
const changes = this.touchChanges(obj[$refId]);
|
|
91
122
|
|
|
92
123
|
let isChildAdded = false;
|
|
93
124
|
|
|
@@ -182,11 +213,7 @@ export class StateView {
|
|
|
182
213
|
|
|
183
214
|
// add parent's tag properties
|
|
184
215
|
if (changeTree.getChange(parentIndex) !== OPERATION.DELETE) {
|
|
185
|
-
|
|
186
|
-
if (changes === undefined) {
|
|
187
|
-
changes = {};
|
|
188
|
-
this.changes.set(changeTree.ref[$refId], changes);
|
|
189
|
-
}
|
|
216
|
+
const changes = this.touchChanges(changeTree.ref[$refId]);
|
|
190
217
|
|
|
191
218
|
if (!this.tags) {
|
|
192
219
|
this.tags = new WeakMap<ChangeTree, Set<number>>();
|
|
@@ -214,6 +241,10 @@ export class StateView {
|
|
|
214
241
|
return this;
|
|
215
242
|
}
|
|
216
243
|
|
|
244
|
+
// remove() bypasses addParentOf's ordering guarantee — flag the
|
|
245
|
+
// changeset as potentially out of topological order.
|
|
246
|
+
this.changesOutOfOrder = true;
|
|
247
|
+
|
|
217
248
|
this.visible.delete(changeTree);
|
|
218
249
|
|
|
219
250
|
// remove from iterable list
|
|
@@ -229,23 +260,13 @@ export class StateView {
|
|
|
229
260
|
|
|
230
261
|
const refId = ref[$refId];
|
|
231
262
|
|
|
232
|
-
let changes = this.changes.get(refId);
|
|
233
|
-
if (changes === undefined) {
|
|
234
|
-
changes = {};
|
|
235
|
-
this.changes.set(refId, changes);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
263
|
if (tag === DEFAULT_VIEW_TAG) {
|
|
239
264
|
// parent is collection (Map/Array)
|
|
240
265
|
const parent = changeTree.parent;
|
|
241
266
|
if (parent && !Metadata.isValidInstance(parent) && changeTree.isFiltered) {
|
|
242
|
-
const
|
|
243
|
-
let changes = this.changes.get(parentRefId);
|
|
244
|
-
if (changes === undefined) {
|
|
245
|
-
changes = {};
|
|
246
|
-
this.changes.set(parentRefId, changes);
|
|
267
|
+
const parentChanges = this.touchChanges(parent[$refId]);
|
|
247
268
|
|
|
248
|
-
|
|
269
|
+
if (parentChanges[changeTree.parentIndex] === OPERATION.ADD) {
|
|
249
270
|
//
|
|
250
271
|
// SAME PATCH ADD + REMOVE:
|
|
251
272
|
// The 'changes' of deleted structure should be ignored.
|
|
@@ -254,13 +275,14 @@ export class StateView {
|
|
|
254
275
|
}
|
|
255
276
|
|
|
256
277
|
// DELETE / DELETE BY REF ID
|
|
257
|
-
|
|
278
|
+
parentChanges[changeTree.parentIndex] = OPERATION.DELETE;
|
|
258
279
|
|
|
259
280
|
// Remove child schema from visible set
|
|
260
281
|
this._recursiveDeleteVisibleChangeTree(changeTree);
|
|
261
282
|
|
|
262
283
|
} else {
|
|
263
284
|
// delete all "tagged" properties.
|
|
285
|
+
const changes = this.touchChanges(refId);
|
|
264
286
|
metadata?.[$viewFieldIndexes]?.forEach((index) => {
|
|
265
287
|
changes[index] = OPERATION.DELETE;
|
|
266
288
|
|
|
@@ -276,6 +298,7 @@ export class StateView {
|
|
|
276
298
|
|
|
277
299
|
} else {
|
|
278
300
|
// delete only tagged properties
|
|
301
|
+
const changes = this.touchChanges(refId);
|
|
279
302
|
metadata?.[$fieldIndexesByViewTag][tag].forEach((index) => {
|
|
280
303
|
changes[index] = OPERATION.DELETE;
|
|
281
304
|
|