@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
|
@@ -41,7 +41,7 @@ export declare class StateCallbackStrategy<TState extends IRef> {
|
|
|
41
41
|
/**
|
|
42
42
|
* Listen to any property change on an instance.
|
|
43
43
|
*/
|
|
44
|
-
onChange<TInstance extends
|
|
44
|
+
onChange<TInstance extends object>(instance: TInstance, handler: InstanceChangeCallback): () => void;
|
|
45
45
|
/**
|
|
46
46
|
* Listen to item changes in a collection on root state.
|
|
47
47
|
*/
|
|
@@ -49,7 +49,7 @@ export declare class StateCallbackStrategy<TState extends IRef> {
|
|
|
49
49
|
/**
|
|
50
50
|
* Listen to item changes in a nested collection.
|
|
51
51
|
*/
|
|
52
|
-
onChange<TInstance extends
|
|
52
|
+
onChange<TInstance extends object, K extends CollectionPropNames<TInstance>>(instance: TInstance, property: K, handler: KeyValueCallback<CollectionKeyType<TInstance, K>, CollectionValueType<TInstance, K>>): () => void;
|
|
53
53
|
/**
|
|
54
54
|
* Listen to items added to a collection on root state.
|
|
55
55
|
*/
|
|
@@ -57,7 +57,7 @@ export declare class StateCallbackStrategy<TState extends IRef> {
|
|
|
57
57
|
/**
|
|
58
58
|
* Listen to items added to a nested collection.
|
|
59
59
|
*/
|
|
60
|
-
onAdd<TInstance
|
|
60
|
+
onAdd<TInstance, K extends CollectionPropNames<TInstance>>(instance: TInstance, property: K, handler: ValueKeyCallback<CollectionValueType<TInstance, K>, CollectionKeyType<TInstance, K>>, immediate?: boolean): () => void;
|
|
61
61
|
/**
|
|
62
62
|
* Listen to items removed from a collection on root state.
|
|
63
63
|
*/
|
|
@@ -65,12 +65,12 @@ export declare class StateCallbackStrategy<TState extends IRef> {
|
|
|
65
65
|
/**
|
|
66
66
|
* Listen to items removed from a nested collection.
|
|
67
67
|
*/
|
|
68
|
-
onRemove<TInstance
|
|
68
|
+
onRemove<TInstance, K extends CollectionPropNames<TInstance>>(instance: TInstance, property: K, handler: ValueKeyCallback<CollectionValueType<TInstance, K>, CollectionKeyType<TInstance, K>>): () => void;
|
|
69
69
|
/**
|
|
70
70
|
* Bind properties from a Schema instance to a target object.
|
|
71
71
|
* Changes will be automatically reflected on the target object.
|
|
72
72
|
*/
|
|
73
|
-
bindTo<TInstance
|
|
73
|
+
bindTo<TInstance, TTarget>(from: TInstance, to: TTarget, properties?: string[], immediate?: boolean): () => void;
|
|
74
74
|
protected triggerChanges(allChanges: DataChange[]): void;
|
|
75
75
|
}
|
|
76
76
|
/**
|
|
@@ -16,6 +16,23 @@ export declare class Encoder<T extends Schema = any> {
|
|
|
16
16
|
encodeAll(it?: Iterator, buffer?: Uint8Array): Uint8Array<ArrayBufferLike>;
|
|
17
17
|
encodeAllView(view: StateView, sharedOffset: number, it: Iterator, bytes?: Uint8Array): Uint8Array<ArrayBufferLike>;
|
|
18
18
|
encodeView(view: StateView, sharedOffset: number, it: Iterator, bytes?: Uint8Array): Uint8Array<ArrayBufferLike>;
|
|
19
|
+
/**
|
|
20
|
+
* Produce a topological ordering of `view.changes` keys so each refId
|
|
21
|
+
* is preceded by any ancestor that's also in the same view's changeset.
|
|
22
|
+
*
|
|
23
|
+
* The wire stream uses SWITCH_TO_STRUCTURE pointers; if a child is
|
|
24
|
+
* encoded before any earlier op has introduced its refId on the
|
|
25
|
+
* decoder, decode fails with "refId not found". An entry's refId can
|
|
26
|
+
* only be introduced by an ADD on one of its ancestors — so any
|
|
27
|
+
* ancestor that itself appears in this view's pending changes must
|
|
28
|
+
* be encoded first.
|
|
29
|
+
*
|
|
30
|
+
* Implementation: DFS post-order over the parent chain. The `visited`
|
|
31
|
+
* Set guards against duplicates; cycles are not expected in a
|
|
32
|
+
* well-formed parent chain but the visited check is a cheap safety
|
|
33
|
+
* net. Cost is O(n × d) for n entries with parent-chain depth d.
|
|
34
|
+
*/
|
|
35
|
+
protected topoOrderViewChanges(view: StateView): number[];
|
|
19
36
|
discardChanges(): void;
|
|
20
37
|
tryEncodeTypeId(bytes: Uint8Array, baseType: typeof Schema, targetType: typeof Schema, it: Iterator): void;
|
|
21
38
|
get hasChanges(): boolean;
|
|
@@ -21,7 +21,33 @@ export declare class StateView {
|
|
|
21
21
|
* (This is used to force encoding a property, even if it was not changed)
|
|
22
22
|
*/
|
|
23
23
|
changes: Map<number, IndexedOperations>;
|
|
24
|
+
/**
|
|
25
|
+
* Set when an operation may have left `changes` out of topological
|
|
26
|
+
* order (a parent that needs to be encoded before its descendants is
|
|
27
|
+
* positioned after them in the Map). `Encoder.encodeView` consults
|
|
28
|
+
* this flag and only runs the topo-ordering pass when it's true,
|
|
29
|
+
* skipping the work in the common case where insertion order already
|
|
30
|
+
* coincides with topo order.
|
|
31
|
+
*
|
|
32
|
+
* Only `remove()` can break the invariant: it writes entries that
|
|
33
|
+
* bypass `addParentOf`'s deepest-ancestor-first ordering. Everything
|
|
34
|
+
* else (including multi-parent re-adds) preserves order by
|
|
35
|
+
* construction. Reset to false at the end of each encodeView pass
|
|
36
|
+
* (when `changes` is cleared).
|
|
37
|
+
*/
|
|
38
|
+
changesOutOfOrder: boolean;
|
|
24
39
|
constructor(iterable?: boolean);
|
|
40
|
+
/**
|
|
41
|
+
* Get the IndexedOperations entry for `refId`, creating one if missing.
|
|
42
|
+
*
|
|
43
|
+
* Map insertion order alone doesn't guarantee parent-before-child
|
|
44
|
+
* iteration in all cases (a `view.remove()` followed by `view.add()`
|
|
45
|
+
* can put a child entry into the Map before its newly-visible
|
|
46
|
+
* ancestor). The wire-order invariant (parent SWITCH_TO_STRUCTURE
|
|
47
|
+
* before any of its children's) is enforced at encode time by
|
|
48
|
+
* `Encoder.encodeView` via a topological pass over `view.changes`.
|
|
49
|
+
*/
|
|
50
|
+
protected touchChanges(refId: number): IndexedOperations;
|
|
25
51
|
add(obj: Ref, tag?: number, checkIncludeParent?: boolean): boolean;
|
|
26
52
|
protected addParentOf(childChangeTree: ChangeTree, tag: number): void;
|
|
27
53
|
remove(obj: Ref, tag?: number): this;
|
package/build/index.cjs
CHANGED
|
@@ -4389,8 +4389,25 @@ class Encoder {
|
|
|
4389
4389
|
}
|
|
4390
4390
|
encodeView(view, sharedOffset, it, bytes = this.sharedBuffer) {
|
|
4391
4391
|
const viewOffset = it.offset;
|
|
4392
|
-
//
|
|
4393
|
-
|
|
4392
|
+
//
|
|
4393
|
+
// Iterate `view.changes` in topological order so a refId is never
|
|
4394
|
+
// SWITCH_TO_STRUCTURE'd before an earlier op has introduced it on
|
|
4395
|
+
// the decoder. Map insertion order alone isn't sufficient: a
|
|
4396
|
+
// sequence like view.remove(child) → view.add(child) on a child
|
|
4397
|
+
// whose ancestor wasn't yet visible can put the child entry into
|
|
4398
|
+
// the Map before its newly-visible ancestor.
|
|
4399
|
+
//
|
|
4400
|
+
// Hot-path optimization: `view.add` preserves topo order by
|
|
4401
|
+
// construction (addParentOf walks deepest-ancestor-first before
|
|
4402
|
+
// touching the obj's own entry). Only `view.remove` can leave the
|
|
4403
|
+
// Map dirty. `StateView.changesOutOfOrder` tracks this so most
|
|
4404
|
+
// encodes can iterate `view.changes` directly, paying nothing.
|
|
4405
|
+
//
|
|
4406
|
+
const orderedRefIds = view.changesOutOfOrder
|
|
4407
|
+
? this.topoOrderViewChanges(view)
|
|
4408
|
+
: view.changes.keys();
|
|
4409
|
+
for (const refId of orderedRefIds) {
|
|
4410
|
+
const changes = view.changes.get(refId);
|
|
4394
4411
|
const changeTree = this.root.changeTrees[refId];
|
|
4395
4412
|
if (changeTree === undefined) {
|
|
4396
4413
|
// detached instance, remove from view and skip.
|
|
@@ -4426,10 +4443,53 @@ class Encoder {
|
|
|
4426
4443
|
//
|
|
4427
4444
|
// clear "view" changes after encoding
|
|
4428
4445
|
view.changes.clear();
|
|
4446
|
+
view.changesOutOfOrder = false;
|
|
4429
4447
|
// try to encode "filtered" changes
|
|
4430
4448
|
this.encode(it, view, bytes, "filteredChanges", false, viewOffset);
|
|
4431
4449
|
return concatBytes(bytes.subarray(0, sharedOffset), bytes.subarray(viewOffset, it.offset));
|
|
4432
4450
|
}
|
|
4451
|
+
/**
|
|
4452
|
+
* Produce a topological ordering of `view.changes` keys so each refId
|
|
4453
|
+
* is preceded by any ancestor that's also in the same view's changeset.
|
|
4454
|
+
*
|
|
4455
|
+
* The wire stream uses SWITCH_TO_STRUCTURE pointers; if a child is
|
|
4456
|
+
* encoded before any earlier op has introduced its refId on the
|
|
4457
|
+
* decoder, decode fails with "refId not found". An entry's refId can
|
|
4458
|
+
* only be introduced by an ADD on one of its ancestors — so any
|
|
4459
|
+
* ancestor that itself appears in this view's pending changes must
|
|
4460
|
+
* be encoded first.
|
|
4461
|
+
*
|
|
4462
|
+
* Implementation: DFS post-order over the parent chain. The `visited`
|
|
4463
|
+
* Set guards against duplicates; cycles are not expected in a
|
|
4464
|
+
* well-formed parent chain but the visited check is a cheap safety
|
|
4465
|
+
* net. Cost is O(n × d) for n entries with parent-chain depth d.
|
|
4466
|
+
*/
|
|
4467
|
+
topoOrderViewChanges(view) {
|
|
4468
|
+
const result = [];
|
|
4469
|
+
const visited = new Set();
|
|
4470
|
+
const visit = (refId) => {
|
|
4471
|
+
if (visited.has(refId)) {
|
|
4472
|
+
return;
|
|
4473
|
+
}
|
|
4474
|
+
visited.add(refId);
|
|
4475
|
+
const changeTree = this.root.changeTrees[refId];
|
|
4476
|
+
if (changeTree !== undefined) {
|
|
4477
|
+
let chain = changeTree.parentChain;
|
|
4478
|
+
while (chain) {
|
|
4479
|
+
const parentRefId = chain.ref[$refId];
|
|
4480
|
+
if (parentRefId !== undefined && view.changes.has(parentRefId)) {
|
|
4481
|
+
visit(parentRefId);
|
|
4482
|
+
}
|
|
4483
|
+
chain = chain.next;
|
|
4484
|
+
}
|
|
4485
|
+
}
|
|
4486
|
+
result.push(refId);
|
|
4487
|
+
};
|
|
4488
|
+
for (const refId of view.changes.keys()) {
|
|
4489
|
+
visit(refId);
|
|
4490
|
+
}
|
|
4491
|
+
return result;
|
|
4492
|
+
}
|
|
4433
4493
|
discardChanges() {
|
|
4434
4494
|
// discard shared changes
|
|
4435
4495
|
let current = this.root.changes.next;
|
|
@@ -5474,6 +5534,7 @@ const Callbacks = {
|
|
|
5474
5534
|
else if ('decoder' in roomOrDecoder.serializer) {
|
|
5475
5535
|
return getDecoderStateCallbacks(roomOrDecoder.serializer.decoder);
|
|
5476
5536
|
}
|
|
5537
|
+
throw new Error('Invalid room or decoder');
|
|
5477
5538
|
},
|
|
5478
5539
|
getRawChanges(decoder, callback) {
|
|
5479
5540
|
return getRawChangesCallback(decoder, callback);
|
|
@@ -5501,12 +5562,45 @@ class StateView {
|
|
|
5501
5562
|
* (This is used to force encoding a property, even if it was not changed)
|
|
5502
5563
|
*/
|
|
5503
5564
|
changes = new Map();
|
|
5565
|
+
/**
|
|
5566
|
+
* Set when an operation may have left `changes` out of topological
|
|
5567
|
+
* order (a parent that needs to be encoded before its descendants is
|
|
5568
|
+
* positioned after them in the Map). `Encoder.encodeView` consults
|
|
5569
|
+
* this flag and only runs the topo-ordering pass when it's true,
|
|
5570
|
+
* skipping the work in the common case where insertion order already
|
|
5571
|
+
* coincides with topo order.
|
|
5572
|
+
*
|
|
5573
|
+
* Only `remove()` can break the invariant: it writes entries that
|
|
5574
|
+
* bypass `addParentOf`'s deepest-ancestor-first ordering. Everything
|
|
5575
|
+
* else (including multi-parent re-adds) preserves order by
|
|
5576
|
+
* construction. Reset to false at the end of each encodeView pass
|
|
5577
|
+
* (when `changes` is cleared).
|
|
5578
|
+
*/
|
|
5579
|
+
changesOutOfOrder = false;
|
|
5504
5580
|
constructor(iterable = false) {
|
|
5505
5581
|
this.iterable = iterable;
|
|
5506
5582
|
if (iterable) {
|
|
5507
5583
|
this.items = [];
|
|
5508
5584
|
}
|
|
5509
5585
|
}
|
|
5586
|
+
/**
|
|
5587
|
+
* Get the IndexedOperations entry for `refId`, creating one if missing.
|
|
5588
|
+
*
|
|
5589
|
+
* Map insertion order alone doesn't guarantee parent-before-child
|
|
5590
|
+
* iteration in all cases (a `view.remove()` followed by `view.add()`
|
|
5591
|
+
* can put a child entry into the Map before its newly-visible
|
|
5592
|
+
* ancestor). The wire-order invariant (parent SWITCH_TO_STRUCTURE
|
|
5593
|
+
* before any of its children's) is enforced at encode time by
|
|
5594
|
+
* `Encoder.encodeView` via a topological pass over `view.changes`.
|
|
5595
|
+
*/
|
|
5596
|
+
touchChanges(refId) {
|
|
5597
|
+
let entry = this.changes.get(refId);
|
|
5598
|
+
if (entry === undefined) {
|
|
5599
|
+
entry = {};
|
|
5600
|
+
this.changes.set(refId, entry);
|
|
5601
|
+
}
|
|
5602
|
+
return entry;
|
|
5603
|
+
}
|
|
5510
5604
|
// TODO: allow to set multiple tags at once
|
|
5511
5605
|
add(obj, tag = DEFAULT_VIEW_TAG, checkIncludeParent = true) {
|
|
5512
5606
|
const changeTree = obj?.[$changes];
|
|
@@ -5540,12 +5634,8 @@ class StateView {
|
|
|
5540
5634
|
if (checkIncludeParent && parentChangeTree) {
|
|
5541
5635
|
this.addParentOf(changeTree, tag);
|
|
5542
5636
|
}
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
changes = {};
|
|
5546
|
-
// FIXME / OPTIMIZE: do not add if no changes are needed
|
|
5547
|
-
this.changes.set(obj[$refId], changes);
|
|
5548
|
-
}
|
|
5637
|
+
// FIXME / OPTIMIZE: do not add if no changes are needed
|
|
5638
|
+
const changes = this.touchChanges(obj[$refId]);
|
|
5549
5639
|
let isChildAdded = false;
|
|
5550
5640
|
//
|
|
5551
5641
|
// Add children of this ChangeTree first.
|
|
@@ -5624,11 +5714,7 @@ class StateView {
|
|
|
5624
5714
|
}
|
|
5625
5715
|
// add parent's tag properties
|
|
5626
5716
|
if (changeTree.getChange(parentIndex) !== exports.OPERATION.DELETE) {
|
|
5627
|
-
|
|
5628
|
-
if (changes === undefined) {
|
|
5629
|
-
changes = {};
|
|
5630
|
-
this.changes.set(changeTree.ref[$refId], changes);
|
|
5631
|
-
}
|
|
5717
|
+
const changes = this.touchChanges(changeTree.ref[$refId]);
|
|
5632
5718
|
if (!this.tags) {
|
|
5633
5719
|
this.tags = new WeakMap();
|
|
5634
5720
|
}
|
|
@@ -5650,6 +5736,9 @@ class StateView {
|
|
|
5650
5736
|
console.warn("StateView#remove(), invalid object:", obj);
|
|
5651
5737
|
return this;
|
|
5652
5738
|
}
|
|
5739
|
+
// remove() bypasses addParentOf's ordering guarantee — flag the
|
|
5740
|
+
// changeset as potentially out of topological order.
|
|
5741
|
+
this.changesOutOfOrder = true;
|
|
5653
5742
|
this.visible.delete(changeTree);
|
|
5654
5743
|
// remove from iterable list
|
|
5655
5744
|
if (this.iterable &&
|
|
@@ -5660,22 +5749,12 @@ class StateView {
|
|
|
5660
5749
|
const ref = changeTree.ref;
|
|
5661
5750
|
const metadata = ref.constructor[Symbol.metadata]; // ArraySchema/MapSchema do not have metadata
|
|
5662
5751
|
const refId = ref[$refId];
|
|
5663
|
-
let changes = this.changes.get(refId);
|
|
5664
|
-
if (changes === undefined) {
|
|
5665
|
-
changes = {};
|
|
5666
|
-
this.changes.set(refId, changes);
|
|
5667
|
-
}
|
|
5668
5752
|
if (tag === DEFAULT_VIEW_TAG) {
|
|
5669
5753
|
// parent is collection (Map/Array)
|
|
5670
5754
|
const parent = changeTree.parent;
|
|
5671
5755
|
if (parent && !Metadata.isValidInstance(parent) && changeTree.isFiltered) {
|
|
5672
|
-
const
|
|
5673
|
-
|
|
5674
|
-
if (changes === undefined) {
|
|
5675
|
-
changes = {};
|
|
5676
|
-
this.changes.set(parentRefId, changes);
|
|
5677
|
-
}
|
|
5678
|
-
else if (changes[changeTree.parentIndex] === exports.OPERATION.ADD) {
|
|
5756
|
+
const parentChanges = this.touchChanges(parent[$refId]);
|
|
5757
|
+
if (parentChanges[changeTree.parentIndex] === exports.OPERATION.ADD) {
|
|
5679
5758
|
//
|
|
5680
5759
|
// SAME PATCH ADD + REMOVE:
|
|
5681
5760
|
// The 'changes' of deleted structure should be ignored.
|
|
@@ -5683,12 +5762,13 @@ class StateView {
|
|
|
5683
5762
|
this.changes.delete(refId);
|
|
5684
5763
|
}
|
|
5685
5764
|
// DELETE / DELETE BY REF ID
|
|
5686
|
-
|
|
5765
|
+
parentChanges[changeTree.parentIndex] = exports.OPERATION.DELETE;
|
|
5687
5766
|
// Remove child schema from visible set
|
|
5688
5767
|
this._recursiveDeleteVisibleChangeTree(changeTree);
|
|
5689
5768
|
}
|
|
5690
5769
|
else {
|
|
5691
5770
|
// delete all "tagged" properties.
|
|
5771
|
+
const changes = this.touchChanges(refId);
|
|
5692
5772
|
metadata?.[$viewFieldIndexes]?.forEach((index) => {
|
|
5693
5773
|
changes[index] = exports.OPERATION.DELETE;
|
|
5694
5774
|
// Remove child structures of @view() fields from visible set.
|
|
@@ -5703,6 +5783,7 @@ class StateView {
|
|
|
5703
5783
|
}
|
|
5704
5784
|
else {
|
|
5705
5785
|
// delete only tagged properties
|
|
5786
|
+
const changes = this.touchChanges(refId);
|
|
5706
5787
|
metadata?.[$fieldIndexesByViewTag][tag].forEach((index) => {
|
|
5707
5788
|
changes[index] = exports.OPERATION.DELETE;
|
|
5708
5789
|
// Remove child structures from visible set
|