@colyseus/schema 4.0.20 → 4.0.22
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/encoder/Encoder.d.ts +17 -0
- package/build/encoder/StateView.d.ts +26 -0
- package/build/index.cjs +108 -29
- package/build/index.cjs.map +1 -1
- package/build/index.js +108 -29
- package/build/index.mjs +108 -29
- package/build/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/encoder/ChangeTree.ts +2 -3
- package/src/encoder/Encoder.ts +67 -2
- package/src/encoder/StateView.ts +47 -24
|
@@ -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
|
@@ -1366,14 +1366,13 @@ class ChangeTree {
|
|
|
1366
1366
|
|| this.root.types.parentFiltered[key]
|
|
1367
1367
|
|| fieldHasViewTag;
|
|
1368
1368
|
//
|
|
1369
|
-
// "isFiltered" may not be
|
|
1369
|
+
// "isFiltered" may not be immediately available during `change()` due to the instance not being attached to the root yet.
|
|
1370
1370
|
// when it's available, we need to enqueue the "changes" changeset into the "filteredChanges" changeset.
|
|
1371
1371
|
//
|
|
1372
1372
|
if (this.isFiltered) {
|
|
1373
1373
|
this.isVisibilitySharedWithParent = (parentChangeTree.isFiltered &&
|
|
1374
1374
|
typeof (refType) !== "string" &&
|
|
1375
|
-
!fieldHasViewTag
|
|
1376
|
-
parentIsCollection);
|
|
1375
|
+
!fieldHasViewTag);
|
|
1377
1376
|
if (!this.filteredChanges) {
|
|
1378
1377
|
this.filteredChanges = createChangeSet();
|
|
1379
1378
|
this.allFilteredChanges = createChangeSet();
|
|
@@ -4390,8 +4389,25 @@ class Encoder {
|
|
|
4390
4389
|
}
|
|
4391
4390
|
encodeView(view, sharedOffset, it, bytes = this.sharedBuffer) {
|
|
4392
4391
|
const viewOffset = it.offset;
|
|
4393
|
-
//
|
|
4394
|
-
|
|
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);
|
|
4395
4411
|
const changeTree = this.root.changeTrees[refId];
|
|
4396
4412
|
if (changeTree === undefined) {
|
|
4397
4413
|
// detached instance, remove from view and skip.
|
|
@@ -4427,10 +4443,53 @@ class Encoder {
|
|
|
4427
4443
|
//
|
|
4428
4444
|
// clear "view" changes after encoding
|
|
4429
4445
|
view.changes.clear();
|
|
4446
|
+
view.changesOutOfOrder = false;
|
|
4430
4447
|
// try to encode "filtered" changes
|
|
4431
4448
|
this.encode(it, view, bytes, "filteredChanges", false, viewOffset);
|
|
4432
4449
|
return concatBytes(bytes.subarray(0, sharedOffset), bytes.subarray(viewOffset, it.offset));
|
|
4433
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
|
+
}
|
|
4434
4493
|
discardChanges() {
|
|
4435
4494
|
// discard shared changes
|
|
4436
4495
|
let current = this.root.changes.next;
|
|
@@ -5502,12 +5561,45 @@ class StateView {
|
|
|
5502
5561
|
* (This is used to force encoding a property, even if it was not changed)
|
|
5503
5562
|
*/
|
|
5504
5563
|
changes = new Map();
|
|
5564
|
+
/**
|
|
5565
|
+
* Set when an operation may have left `changes` out of topological
|
|
5566
|
+
* order (a parent that needs to be encoded before its descendants is
|
|
5567
|
+
* positioned after them in the Map). `Encoder.encodeView` consults
|
|
5568
|
+
* this flag and only runs the topo-ordering pass when it's true,
|
|
5569
|
+
* skipping the work in the common case where insertion order already
|
|
5570
|
+
* coincides with topo order.
|
|
5571
|
+
*
|
|
5572
|
+
* Only `remove()` can break the invariant: it writes entries that
|
|
5573
|
+
* bypass `addParentOf`'s deepest-ancestor-first ordering. Everything
|
|
5574
|
+
* else (including multi-parent re-adds) preserves order by
|
|
5575
|
+
* construction. Reset to false at the end of each encodeView pass
|
|
5576
|
+
* (when `changes` is cleared).
|
|
5577
|
+
*/
|
|
5578
|
+
changesOutOfOrder = false;
|
|
5505
5579
|
constructor(iterable = false) {
|
|
5506
5580
|
this.iterable = iterable;
|
|
5507
5581
|
if (iterable) {
|
|
5508
5582
|
this.items = [];
|
|
5509
5583
|
}
|
|
5510
5584
|
}
|
|
5585
|
+
/**
|
|
5586
|
+
* Get the IndexedOperations entry for `refId`, creating one if missing.
|
|
5587
|
+
*
|
|
5588
|
+
* Map insertion order alone doesn't guarantee parent-before-child
|
|
5589
|
+
* iteration in all cases (a `view.remove()` followed by `view.add()`
|
|
5590
|
+
* can put a child entry into the Map before its newly-visible
|
|
5591
|
+
* ancestor). The wire-order invariant (parent SWITCH_TO_STRUCTURE
|
|
5592
|
+
* before any of its children's) is enforced at encode time by
|
|
5593
|
+
* `Encoder.encodeView` via a topological pass over `view.changes`.
|
|
5594
|
+
*/
|
|
5595
|
+
touchChanges(refId) {
|
|
5596
|
+
let entry = this.changes.get(refId);
|
|
5597
|
+
if (entry === undefined) {
|
|
5598
|
+
entry = {};
|
|
5599
|
+
this.changes.set(refId, entry);
|
|
5600
|
+
}
|
|
5601
|
+
return entry;
|
|
5602
|
+
}
|
|
5511
5603
|
// TODO: allow to set multiple tags at once
|
|
5512
5604
|
add(obj, tag = DEFAULT_VIEW_TAG, checkIncludeParent = true) {
|
|
5513
5605
|
const changeTree = obj?.[$changes];
|
|
@@ -5541,12 +5633,8 @@ class StateView {
|
|
|
5541
5633
|
if (checkIncludeParent && parentChangeTree) {
|
|
5542
5634
|
this.addParentOf(changeTree, tag);
|
|
5543
5635
|
}
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
changes = {};
|
|
5547
|
-
// FIXME / OPTIMIZE: do not add if no changes are needed
|
|
5548
|
-
this.changes.set(obj[$refId], changes);
|
|
5549
|
-
}
|
|
5636
|
+
// FIXME / OPTIMIZE: do not add if no changes are needed
|
|
5637
|
+
const changes = this.touchChanges(obj[$refId]);
|
|
5550
5638
|
let isChildAdded = false;
|
|
5551
5639
|
//
|
|
5552
5640
|
// Add children of this ChangeTree first.
|
|
@@ -5625,11 +5713,7 @@ class StateView {
|
|
|
5625
5713
|
}
|
|
5626
5714
|
// add parent's tag properties
|
|
5627
5715
|
if (changeTree.getChange(parentIndex) !== exports.OPERATION.DELETE) {
|
|
5628
|
-
|
|
5629
|
-
if (changes === undefined) {
|
|
5630
|
-
changes = {};
|
|
5631
|
-
this.changes.set(changeTree.ref[$refId], changes);
|
|
5632
|
-
}
|
|
5716
|
+
const changes = this.touchChanges(changeTree.ref[$refId]);
|
|
5633
5717
|
if (!this.tags) {
|
|
5634
5718
|
this.tags = new WeakMap();
|
|
5635
5719
|
}
|
|
@@ -5651,6 +5735,9 @@ class StateView {
|
|
|
5651
5735
|
console.warn("StateView#remove(), invalid object:", obj);
|
|
5652
5736
|
return this;
|
|
5653
5737
|
}
|
|
5738
|
+
// remove() bypasses addParentOf's ordering guarantee — flag the
|
|
5739
|
+
// changeset as potentially out of topological order.
|
|
5740
|
+
this.changesOutOfOrder = true;
|
|
5654
5741
|
this.visible.delete(changeTree);
|
|
5655
5742
|
// remove from iterable list
|
|
5656
5743
|
if (this.iterable &&
|
|
@@ -5661,22 +5748,12 @@ class StateView {
|
|
|
5661
5748
|
const ref = changeTree.ref;
|
|
5662
5749
|
const metadata = ref.constructor[Symbol.metadata]; // ArraySchema/MapSchema do not have metadata
|
|
5663
5750
|
const refId = ref[$refId];
|
|
5664
|
-
let changes = this.changes.get(refId);
|
|
5665
|
-
if (changes === undefined) {
|
|
5666
|
-
changes = {};
|
|
5667
|
-
this.changes.set(refId, changes);
|
|
5668
|
-
}
|
|
5669
5751
|
if (tag === DEFAULT_VIEW_TAG) {
|
|
5670
5752
|
// parent is collection (Map/Array)
|
|
5671
5753
|
const parent = changeTree.parent;
|
|
5672
5754
|
if (parent && !Metadata.isValidInstance(parent) && changeTree.isFiltered) {
|
|
5673
|
-
const
|
|
5674
|
-
|
|
5675
|
-
if (changes === undefined) {
|
|
5676
|
-
changes = {};
|
|
5677
|
-
this.changes.set(parentRefId, changes);
|
|
5678
|
-
}
|
|
5679
|
-
else if (changes[changeTree.parentIndex] === exports.OPERATION.ADD) {
|
|
5755
|
+
const parentChanges = this.touchChanges(parent[$refId]);
|
|
5756
|
+
if (parentChanges[changeTree.parentIndex] === exports.OPERATION.ADD) {
|
|
5680
5757
|
//
|
|
5681
5758
|
// SAME PATCH ADD + REMOVE:
|
|
5682
5759
|
// The 'changes' of deleted structure should be ignored.
|
|
@@ -5684,12 +5761,13 @@ class StateView {
|
|
|
5684
5761
|
this.changes.delete(refId);
|
|
5685
5762
|
}
|
|
5686
5763
|
// DELETE / DELETE BY REF ID
|
|
5687
|
-
|
|
5764
|
+
parentChanges[changeTree.parentIndex] = exports.OPERATION.DELETE;
|
|
5688
5765
|
// Remove child schema from visible set
|
|
5689
5766
|
this._recursiveDeleteVisibleChangeTree(changeTree);
|
|
5690
5767
|
}
|
|
5691
5768
|
else {
|
|
5692
5769
|
// delete all "tagged" properties.
|
|
5770
|
+
const changes = this.touchChanges(refId);
|
|
5693
5771
|
metadata?.[$viewFieldIndexes]?.forEach((index) => {
|
|
5694
5772
|
changes[index] = exports.OPERATION.DELETE;
|
|
5695
5773
|
// Remove child structures of @view() fields from visible set.
|
|
@@ -5704,6 +5782,7 @@ class StateView {
|
|
|
5704
5782
|
}
|
|
5705
5783
|
else {
|
|
5706
5784
|
// delete only tagged properties
|
|
5785
|
+
const changes = this.touchChanges(refId);
|
|
5707
5786
|
metadata?.[$fieldIndexesByViewTag][tag].forEach((index) => {
|
|
5708
5787
|
changes[index] = exports.OPERATION.DELETE;
|
|
5709
5788
|
// Remove child structures from visible set
|