@colyseus/schema 4.0.20 → 5.0.1
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/README.md +2 -0
- package/build/Metadata.d.ts +56 -2
- package/build/Reflection.d.ts +28 -34
- package/build/Schema.d.ts +70 -9
- package/build/annotations.d.ts +64 -17
- package/build/codegen/cli.cjs +84 -67
- package/build/codegen/cli.cjs.map +1 -1
- package/build/decoder/DecodeOperation.d.ts +48 -5
- package/build/decoder/Decoder.d.ts +2 -2
- package/build/decoder/strategy/Callbacks.d.ts +1 -1
- package/build/encoder/ChangeRecorder.d.ts +107 -0
- package/build/encoder/ChangeTree.d.ts +218 -69
- package/build/encoder/EncodeDescriptor.d.ts +63 -0
- package/build/encoder/EncodeOperation.d.ts +25 -2
- package/build/encoder/Encoder.d.ts +59 -3
- package/build/encoder/MapJournal.d.ts +62 -0
- package/build/encoder/RefIdAllocator.d.ts +35 -0
- package/build/encoder/Root.d.ts +94 -13
- package/build/encoder/StateView.d.ts +116 -8
- package/build/encoder/changeTree/inheritedFlags.d.ts +34 -0
- package/build/encoder/changeTree/liveIteration.d.ts +3 -0
- package/build/encoder/changeTree/parentChain.d.ts +24 -0
- package/build/encoder/changeTree/treeAttachment.d.ts +13 -0
- package/build/encoder/streaming.d.ts +73 -0
- package/build/encoder/subscriptions.d.ts +25 -0
- package/build/index.cjs +5258 -1549
- package/build/index.cjs.map +1 -1
- package/build/index.d.ts +7 -3
- package/build/index.js +5258 -1549
- package/build/index.mjs +5249 -1549
- package/build/index.mjs.map +1 -1
- package/build/input/InputDecoder.d.ts +32 -0
- package/build/input/InputEncoder.d.ts +117 -0
- package/build/input/index.cjs +7453 -0
- package/build/input/index.cjs.map +1 -0
- package/build/input/index.d.ts +3 -0
- package/build/input/index.mjs +7450 -0
- package/build/input/index.mjs.map +1 -0
- package/build/types/HelperTypes.d.ts +67 -9
- package/build/types/TypeContext.d.ts +9 -0
- package/build/types/builder.d.ts +192 -0
- package/build/types/custom/ArraySchema.d.ts +25 -4
- package/build/types/custom/CollectionSchema.d.ts +30 -2
- package/build/types/custom/MapSchema.d.ts +52 -3
- package/build/types/custom/SetSchema.d.ts +32 -2
- package/build/types/custom/StreamSchema.d.ts +114 -0
- package/build/types/symbols.d.ts +48 -5
- package/package.json +9 -3
- package/src/Metadata.ts +259 -31
- package/src/Reflection.ts +15 -13
- package/src/Schema.ts +176 -134
- package/src/annotations.ts +365 -252
- package/src/bench_bloat.ts +173 -0
- package/src/bench_decode.ts +221 -0
- package/src/bench_decode_mem.ts +165 -0
- package/src/bench_encode.ts +108 -0
- package/src/bench_init.ts +150 -0
- package/src/bench_static.ts +109 -0
- package/src/bench_stream.ts +295 -0
- package/src/bench_view_cmp.ts +142 -0
- package/src/codegen/languages/csharp.ts +0 -24
- package/src/codegen/parser.ts +83 -61
- package/src/decoder/DecodeOperation.ts +168 -63
- package/src/decoder/Decoder.ts +20 -10
- package/src/decoder/ReferenceTracker.ts +4 -0
- package/src/decoder/strategy/Callbacks.ts +30 -26
- package/src/decoder/strategy/getDecoderStateCallbacks.ts +16 -13
- package/src/encoder/ChangeRecorder.ts +276 -0
- package/src/encoder/ChangeTree.ts +674 -519
- package/src/encoder/EncodeDescriptor.ts +213 -0
- package/src/encoder/EncodeOperation.ts +107 -65
- package/src/encoder/Encoder.ts +630 -119
- package/src/encoder/MapJournal.ts +124 -0
- package/src/encoder/RefIdAllocator.ts +68 -0
- package/src/encoder/Root.ts +247 -120
- package/src/encoder/StateView.ts +592 -121
- package/src/encoder/changeTree/inheritedFlags.ts +217 -0
- package/src/encoder/changeTree/liveIteration.ts +74 -0
- package/src/encoder/changeTree/parentChain.ts +131 -0
- package/src/encoder/changeTree/treeAttachment.ts +171 -0
- package/src/encoder/streaming.ts +232 -0
- package/src/encoder/subscriptions.ts +71 -0
- package/src/index.ts +15 -3
- package/src/input/InputDecoder.ts +57 -0
- package/src/input/InputEncoder.ts +303 -0
- package/src/input/index.ts +3 -0
- package/src/types/HelperTypes.ts +121 -24
- package/src/types/TypeContext.ts +14 -2
- package/src/types/builder.ts +331 -0
- package/src/types/custom/ArraySchema.ts +210 -197
- package/src/types/custom/CollectionSchema.ts +115 -35
- package/src/types/custom/MapSchema.ts +162 -58
- package/src/types/custom/SetSchema.ts +128 -39
- package/src/types/custom/StreamSchema.ts +310 -0
- package/src/types/symbols.ts +93 -6
- package/src/utils.ts +4 -6
|
@@ -3,20 +3,76 @@ import { TypeContext } from "../types/TypeContext.js";
|
|
|
3
3
|
import type { Iterator } from "../encoding/decode.js";
|
|
4
4
|
import { Root } from "./Root.js";
|
|
5
5
|
import type { StateView } from "./StateView.js";
|
|
6
|
-
import type { ChangeSetName } from "./ChangeTree.js";
|
|
7
6
|
export declare class Encoder<T extends Schema = any> {
|
|
8
7
|
static BUFFER_SIZE: number;
|
|
9
8
|
sharedBuffer: Uint8Array;
|
|
10
9
|
context: TypeContext;
|
|
11
10
|
state: T;
|
|
12
11
|
root: Root;
|
|
13
|
-
constructor(state: T);
|
|
12
|
+
constructor(state: T, root?: Root);
|
|
14
13
|
protected setState(state: T): void;
|
|
15
|
-
|
|
14
|
+
private _encodeCtx;
|
|
15
|
+
/**
|
|
16
|
+
* Monotonic counter bumped at the start of every `encodeFullSync`
|
|
17
|
+
* call. The new value is copied to `ctx.gen` and stamped into every
|
|
18
|
+
* tree the walk touches (`tree._fullSyncGen = ctx.gen`); subsequent
|
|
19
|
+
* revisits of the same tree detect the equality and return early.
|
|
20
|
+
*/
|
|
21
|
+
private _fullSyncGen;
|
|
22
|
+
encode(it?: Iterator, view?: StateView, buffer?: Uint8Array, initialOffset?: number): Uint8Array;
|
|
23
|
+
/**
|
|
24
|
+
* Per-tick encode of the UNRELIABLE channel. Walks `root.unreliableChanges`
|
|
25
|
+
* and emits each tree's `unreliableRecorder`. Safe to call at a different
|
|
26
|
+
* cadence than `encode()` (e.g. 60Hz vs 20Hz) — the two channels are
|
|
27
|
+
* fully independent.
|
|
28
|
+
*/
|
|
29
|
+
encodeUnreliable(it?: Iterator, view?: StateView, buffer?: Uint8Array, initialOffset?: number): Uint8Array;
|
|
30
|
+
private _encodeChannel;
|
|
31
|
+
/**
|
|
32
|
+
* Structural DFS walker for full-sync (encodeAll / encodeAllView).
|
|
33
|
+
* Visits each ChangeTree in DFS preorder starting from the state root,
|
|
34
|
+
* emitting ADD operations for every currently-populated index via
|
|
35
|
+
* {@link ChangeTree.forEachLive}.
|
|
36
|
+
*/
|
|
37
|
+
private encodeFullSync;
|
|
38
|
+
private _resizeBuffer;
|
|
16
39
|
encodeAll(it?: Iterator, buffer?: Uint8Array): Uint8Array<ArrayBufferLike>;
|
|
17
40
|
encodeAllView(view: StateView, sharedOffset: number, it: Iterator, bytes?: Uint8Array): Uint8Array<ArrayBufferLike>;
|
|
18
41
|
encodeView(view: StateView, sharedOffset: number, it: Iterator, bytes?: Uint8Array): Uint8Array<ArrayBufferLike>;
|
|
42
|
+
/**
|
|
43
|
+
* Per-view unreliable encode. Walks `root.unreliableChanges` and emits
|
|
44
|
+
* only filtered fields visible to this view. Unlike `encodeView`, this
|
|
45
|
+
* doesn't emit `view.changes` entries — those are used only for
|
|
46
|
+
* reliable view bootstrap (membership ADDs) and are consumed by
|
|
47
|
+
* `encodeView` on the reliable channel.
|
|
48
|
+
*/
|
|
49
|
+
encodeUnreliableView(view: StateView, sharedOffset: number, it: Iterator, bytes?: Uint8Array): Uint8Array<ArrayBufferLike>;
|
|
50
|
+
/**
|
|
51
|
+
* Broadcast-mode counterpart to `_emitStreamPriority`. Runs when NO
|
|
52
|
+
* StateViews are registered — streams fall back to broadcast mode
|
|
53
|
+
* where up to `maxPerTick` pending ADDs per stream emit to ALL clients
|
|
54
|
+
* each shared tick. DELETEs always flush (no cap).
|
|
55
|
+
*
|
|
56
|
+
* Emits directly to the shared-encode buffer: stream & element trees
|
|
57
|
+
* are `isFiltered=true` so the main loop would otherwise skip them.
|
|
58
|
+
* Runs AFTER the main loop so state / parent refs are already encoded
|
|
59
|
+
* — stream ADD ops reference element refIds, which must be decodable.
|
|
60
|
+
*/
|
|
61
|
+
private _emitStreamBroadcast;
|
|
62
|
+
/**
|
|
63
|
+
* Walk every registered stream, pick up to `maxPerTick` positions from
|
|
64
|
+
* this view's pending backlog (priority-sorted when the view supplies a
|
|
65
|
+
* `streamPriority` callback), and hand each element to `view.add()`.
|
|
66
|
+
* `view.add()` seeds `view.changes` so the subsequent drain emits both
|
|
67
|
+
* the stream-link (position → refId) and the element's field data.
|
|
68
|
+
*
|
|
69
|
+
* Designed to run at the very top of `encodeView`, BEFORE the
|
|
70
|
+
* view.changes drain loop.
|
|
71
|
+
*/
|
|
72
|
+
private _emitStreamPriority;
|
|
19
73
|
discardChanges(): void;
|
|
74
|
+
discardUnreliableChanges(): void;
|
|
20
75
|
tryEncodeTypeId(bytes: Uint8Array, baseType: typeof Schema, targetType: typeof Schema, it: Iterator): void;
|
|
21
76
|
get hasChanges(): boolean;
|
|
77
|
+
get hasUnreliableChanges(): boolean;
|
|
22
78
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MapJournal — owns the change-tracking and wire-protocol identity for a MapSchema.
|
|
3
|
+
*
|
|
4
|
+
* Replaces three parallel structures that previously lived on MapSchema:
|
|
5
|
+
* - `$indexes: Map<number, K>` → `keyByIndex`
|
|
6
|
+
* - `_collectionIndexes: { [key]: number }` (+ counter) → `indexByKey` + `nextIndex`
|
|
7
|
+
* - `deletedItems: { [index]: V }` → `snapshots`
|
|
8
|
+
*
|
|
9
|
+
* The journal is the single source of truth for:
|
|
10
|
+
* - assigning wire-protocol indexes to keys (server side)
|
|
11
|
+
* - looking up keys from wire indexes (server + client)
|
|
12
|
+
* - holding snapshots of removed values (for view-filter visibility checks)
|
|
13
|
+
*
|
|
14
|
+
* The journal does NOT track per-index operation types or maintain enqueue
|
|
15
|
+
* order — those remain on `ChangeTree` for now. A future iteration may pull
|
|
16
|
+
* them in too, but this version is intentionally scoped to the data-model
|
|
17
|
+
* cleanup so we can validate the abstraction before going deeper.
|
|
18
|
+
*/
|
|
19
|
+
export declare class MapJournal<K = any> {
|
|
20
|
+
/** index → key (was MapSchema.$indexes). Used by encoder and decoder. */
|
|
21
|
+
keyByIndex: Map<number, K>;
|
|
22
|
+
/**
|
|
23
|
+
* key → index (was MapSchema._collectionIndexes — forward direction).
|
|
24
|
+
* Server-only. Plain object so MapSchema can expose it via a getter
|
|
25
|
+
* for backwards-compatible `_collectionIndexes?.[key]` access from
|
|
26
|
+
* ChangeTree.forEachChild and similar polymorphic call sites.
|
|
27
|
+
*/
|
|
28
|
+
indexByKey: {
|
|
29
|
+
[key: string]: number;
|
|
30
|
+
};
|
|
31
|
+
/** Monotonic counter for assigning new indexes. Server-only. */
|
|
32
|
+
private nextIndex;
|
|
33
|
+
/**
|
|
34
|
+
* Snapshot of values at the moment they were deleted. Lazy — only
|
|
35
|
+
* allocated on first delete, since most maps are pure-grow and never
|
|
36
|
+
* touch this. Used by `MapSchema[$filter]` to check view visibility
|
|
37
|
+
* of a value that's already been removed from `$items` but whose
|
|
38
|
+
* DELETE op is still in the encode queue.
|
|
39
|
+
*/
|
|
40
|
+
snapshots?: Map<number, any>;
|
|
41
|
+
/** Get the index assigned to a key, or undefined if never assigned. */
|
|
42
|
+
indexOf(key: K): number | undefined;
|
|
43
|
+
/** Assign and return a new wire index for an unseen key. */
|
|
44
|
+
assign(key: K): number;
|
|
45
|
+
/** Stash a value at the moment it's deleted (for filter visibility checks). */
|
|
46
|
+
snapshot(index: number, value: any): void;
|
|
47
|
+
/** Discard a snapshot — called when a deleted slot is being re-set. */
|
|
48
|
+
forgetSnapshot(index: number): void;
|
|
49
|
+
/** Look up a snapshot. Returns undefined if no DELETE is pending for this index. */
|
|
50
|
+
snapshotAt(index: number): any;
|
|
51
|
+
/** Decoder calls this when it sees an ADD/DELETE_AND_ADD on the wire. */
|
|
52
|
+
setIndex(index: number, key: K): void;
|
|
53
|
+
/** Reverse lookup: wire index → key. */
|
|
54
|
+
keyOf(index: number): K | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* Called from MapSchema's $onEncodeEnd hook.
|
|
57
|
+
* Cleans up index/key mappings for entries that were deleted in this tick.
|
|
58
|
+
*/
|
|
59
|
+
cleanupAfterEncode(): void;
|
|
60
|
+
/** Reset everything (called on .clear()). */
|
|
61
|
+
reset(): void;
|
|
62
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Allocates monotonically-increasing refIds with a reuse pool.
|
|
3
|
+
*
|
|
4
|
+
* `acquire()` pops from the free pool when available, otherwise bumps a
|
|
5
|
+
* counter. `release()` queues a refId for reuse; the id doesn't become
|
|
6
|
+
* acquirable until `flushReleases()` runs — the one-tick defer is what
|
|
7
|
+
* lets the encoder guarantee a DELETE for the old instance reaches the
|
|
8
|
+
* wire before the refId is handed to a new one.
|
|
9
|
+
*
|
|
10
|
+
* `reclaim()` handles "resurrection": a ref that was released but whose
|
|
11
|
+
* JS instance is still alive can be re-added to the tree, in which case
|
|
12
|
+
* the encoder must pull the refId back out of the pool before it's
|
|
13
|
+
* handed to an unrelated instance.
|
|
14
|
+
*/
|
|
15
|
+
export declare class RefIdAllocator {
|
|
16
|
+
protected nextUniqueId: number;
|
|
17
|
+
private _free;
|
|
18
|
+
private _pending;
|
|
19
|
+
private _pooled;
|
|
20
|
+
constructor(startRefId?: number);
|
|
21
|
+
acquire(): number;
|
|
22
|
+
release(refId: number): void;
|
|
23
|
+
isPooled(refId: number): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Remove a refId from the pool. Called when a ref whose refId was
|
|
26
|
+
* released is being resurrected. O(n) scan of the relevant array,
|
|
27
|
+
* but resurrection is rare.
|
|
28
|
+
*/
|
|
29
|
+
reclaim(refId: number): void;
|
|
30
|
+
/**
|
|
31
|
+
* Promote this tick's releases into the acquirable set. Called from
|
|
32
|
+
* `Encoder.discardChanges()` — never mid-encode.
|
|
33
|
+
*/
|
|
34
|
+
flushReleases(): void;
|
|
35
|
+
}
|
package/build/encoder/Root.d.ts
CHANGED
|
@@ -1,28 +1,109 @@
|
|
|
1
1
|
import { TypeContext } from "../types/TypeContext.js";
|
|
2
|
-
import { ChangeTree, ChangeTreeList,
|
|
2
|
+
import { ChangeTree, ChangeTreeList, type ChangeTreeNode } from "./ChangeTree.js";
|
|
3
|
+
import { $changes, $refId } from "../types/symbols.js";
|
|
4
|
+
import { RefIdAllocator } from "./RefIdAllocator.js";
|
|
5
|
+
import type { StateView } from "./StateView.js";
|
|
6
|
+
import type { StreamableState } from "./streaming.js";
|
|
7
|
+
/**
|
|
8
|
+
* Minimal shape the encoder needs from a streamable collection. Both
|
|
9
|
+
* `StreamSchema` and `.stream()`-decorated `MapSchema`/`SetSchema` etc.
|
|
10
|
+
* satisfy this via a single lazily-allocated `_stream` slot — the
|
|
11
|
+
* per-view / broadcast bookkeeping lives on that object, not directly
|
|
12
|
+
* on the collection, so non-streaming instances pay zero Map/Set
|
|
13
|
+
* allocation cost.
|
|
14
|
+
*/
|
|
15
|
+
export interface Streamable {
|
|
16
|
+
[$refId]?: number;
|
|
17
|
+
[$changes]: ChangeTree;
|
|
18
|
+
_stream?: StreamableState;
|
|
19
|
+
_dropView(viewId: number): void;
|
|
20
|
+
_unregister(): void;
|
|
21
|
+
}
|
|
3
22
|
export declare class Root {
|
|
4
23
|
types: TypeContext;
|
|
5
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Allocates and recycles refIds. See `RefIdAllocator` for the reuse
|
|
26
|
+
* pool semantics (one-tick defer + resurrection).
|
|
27
|
+
*/
|
|
28
|
+
readonly refIds: RefIdAllocator;
|
|
6
29
|
refCount: {
|
|
7
30
|
[id: number]: number;
|
|
8
31
|
};
|
|
9
32
|
changeTrees: {
|
|
10
33
|
[refId: number]: ChangeTree;
|
|
11
34
|
};
|
|
12
|
-
|
|
13
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Queue of all ChangeTrees with reliable dirty state. Per-tick encode()
|
|
37
|
+
* walks this queue; per-view encodeView() walks it too (filtering at
|
|
38
|
+
* emission time via tree.isFiltered + per-field @view tag).
|
|
39
|
+
*/
|
|
14
40
|
changes: ChangeTreeList;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Queue of all ChangeTrees with unreliable dirty state. Walked by
|
|
43
|
+
* `Encoder.encodeUnreliable` / `encodeUnreliableView`. A tree may live
|
|
44
|
+
* in both queues when the Schema has both reliable and unreliable
|
|
45
|
+
* fields dirty at the same time.
|
|
46
|
+
*/
|
|
47
|
+
unreliableChanges: ChangeTreeList;
|
|
48
|
+
/**
|
|
49
|
+
* Free-list of ChangeTreeNode objects. Both queues share this pool —
|
|
50
|
+
* a node carries no queue affinity, only `{ changeTree, prev, next, position }`.
|
|
51
|
+
* Reusing nodes turns ~1,250 per-tick allocations (in bench) into 0.
|
|
52
|
+
*/
|
|
53
|
+
private _nodePool;
|
|
54
|
+
/**
|
|
55
|
+
* View ID allocator for StateView visibility bitmaps on ChangeTree.
|
|
56
|
+
* Each new StateView claims the lowest free ID; releaseViewId() puts
|
|
57
|
+
* the ID back. Avoids unbounded bitmap growth across long-running rooms
|
|
58
|
+
* with view churn (clients joining/leaving).
|
|
59
|
+
*/
|
|
60
|
+
private _nextViewId;
|
|
61
|
+
private _freeViewIds;
|
|
62
|
+
/** Allocate a fresh view ID (lowest available). */
|
|
63
|
+
acquireViewId(): number;
|
|
64
|
+
/** Return a view ID to the freelist for reuse. */
|
|
65
|
+
releaseViewId(id: number): void;
|
|
66
|
+
/**
|
|
67
|
+
* Currently-bound StateViews, keyed by view ID and held via `WeakRef`
|
|
68
|
+
* so the FinalizationRegistry backstop in StateView still works when
|
|
69
|
+
* the user forgets `dispose()`. Callers must iterate via
|
|
70
|
+
* `forEachActiveView`, which prunes dead entries.
|
|
71
|
+
*/
|
|
72
|
+
activeViews: Map<number, WeakRef<StateView>>;
|
|
73
|
+
/**
|
|
74
|
+
* Streamable collections attached under this Root — `StreamSchema`
|
|
75
|
+
* plus any collection opted into streaming via `.stream()` on the
|
|
76
|
+
* builder. Encoder.encodeView / broadcast pass iterates this set to
|
|
77
|
+
* dispatch per-view / per-tick budget gates.
|
|
78
|
+
*/
|
|
79
|
+
streamTrees: Set<Streamable>;
|
|
80
|
+
registerView(view: StateView): void;
|
|
81
|
+
unregisterView(view: StateView): void;
|
|
82
|
+
/**
|
|
83
|
+
* Iterate all live StateViews bound to this Root. Prunes entries
|
|
84
|
+
* whose underlying view has been garbage collected without an
|
|
85
|
+
* explicit `dispose()`.
|
|
86
|
+
*/
|
|
87
|
+
forEachActiveView(cb: (view: StateView) => void): void;
|
|
88
|
+
registerStream(stream: Streamable): void;
|
|
89
|
+
unregisterStream(stream: Streamable): void;
|
|
90
|
+
constructor(types: TypeContext, startRefId?: number);
|
|
18
91
|
add(changeTree: ChangeTree): boolean;
|
|
19
92
|
remove(changeTree: ChangeTree): number;
|
|
20
93
|
recursivelyMoveNextToParent(changeTree: ChangeTree): void;
|
|
21
94
|
moveNextToParent(changeTree: ChangeTree): void;
|
|
22
|
-
|
|
23
|
-
enqueueChangeTree(changeTree: ChangeTree,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
95
|
+
private _moveNextToParentInList;
|
|
96
|
+
enqueueChangeTree(changeTree: ChangeTree, existingNode?: ChangeTreeNode): void;
|
|
97
|
+
enqueueUnreliable(changeTree: ChangeTree, existingNode?: ChangeTreeNode): void;
|
|
98
|
+
private _appendToList;
|
|
99
|
+
/**
|
|
100
|
+
* Release a detached node back to the free-list. Caller must have
|
|
101
|
+
* already unlinked it from any list and cleared the changeTree's
|
|
102
|
+
* pointer to it. Clears `changeTree`/`prev`/`next` so the pool
|
|
103
|
+
* doesn't retain references through the GC root.
|
|
104
|
+
*/
|
|
105
|
+
releaseNode(node: ChangeTreeNode): void;
|
|
106
|
+
removeFromQueue(changeTree: ChangeTree): boolean;
|
|
107
|
+
removeFromUnreliableQueue(changeTree: ChangeTree): boolean;
|
|
108
|
+
private _removeNode;
|
|
28
109
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { ChangeTree,
|
|
1
|
+
import { ChangeTree, Ref } from "./ChangeTree.js";
|
|
2
|
+
import { OPERATION } from "../encoding/spec.js";
|
|
2
3
|
export declare function createView(iterable?: boolean): StateView;
|
|
3
4
|
export declare class StateView {
|
|
4
5
|
iterable: boolean;
|
|
@@ -8,26 +9,133 @@ export declare class StateView {
|
|
|
8
9
|
*/
|
|
9
10
|
items: Ref[];
|
|
10
11
|
/**
|
|
11
|
-
*
|
|
12
|
+
* Unique ID assigned by the Root that owns this view's encoder. Used
|
|
13
|
+
* to address per-StateView visibility bits stored on each ChangeTree.
|
|
14
|
+
* Lazily allocated on first `add()` because the StateView itself
|
|
15
|
+
* doesn't know its Root until then.
|
|
12
16
|
*/
|
|
13
|
-
|
|
17
|
+
id: number;
|
|
18
|
+
private _root?;
|
|
19
|
+
/** Cached `id >> 5` and `1 << (id & 31)` for the hot encode-loop check. */
|
|
20
|
+
private _slot;
|
|
21
|
+
private _bit;
|
|
14
22
|
/**
|
|
15
|
-
*
|
|
23
|
+
* Per-tree custom-tag membership lives on each ChangeTree's `tagViews`
|
|
24
|
+
* map (keyed by tag, value is a per-view bitmap). The StateView only
|
|
25
|
+
* needs its slot/bit pair to read/write it. Replaces the legacy
|
|
26
|
+
* `tags: WeakMap<ChangeTree, Set<number>>` allocation per (view, tree).
|
|
16
27
|
*/
|
|
17
|
-
invisible: WeakSet<ChangeTree>;
|
|
18
|
-
tags?: WeakMap<ChangeTree, Set<number>>;
|
|
19
28
|
/**
|
|
20
29
|
* Manual "ADD" operations for changes per ChangeTree, specific to this view.
|
|
21
|
-
* (
|
|
30
|
+
* (Used to force encoding a property even if it was not changed.)
|
|
31
|
+
*
|
|
32
|
+
* Inner storage is a Map so the encode loop in `encodeView` can iterate
|
|
33
|
+
* directly with numeric keys — the legacy `{[index]: OPERATION}` shape
|
|
34
|
+
* forced an `Object.keys(...)` allocation + `Number(key)` parse per ref.
|
|
22
35
|
*/
|
|
23
|
-
changes: Map<number,
|
|
36
|
+
changes: Map<number, Map<number, OPERATION>>;
|
|
24
37
|
constructor(iterable?: boolean);
|
|
38
|
+
/**
|
|
39
|
+
* Lazily bind this view to a Root and acquire a view ID. Called on
|
|
40
|
+
* the first add() because StateView is constructed before its target
|
|
41
|
+
* Root is known.
|
|
42
|
+
*/
|
|
43
|
+
private _bindRoot;
|
|
44
|
+
/**
|
|
45
|
+
* Release this view's ID back to the Root for reuse, AND clear all
|
|
46
|
+
* visibility bits this view set on any ChangeTree. The clear is
|
|
47
|
+
* essential — without it, a future view that acquires this same ID
|
|
48
|
+
* would inherit our visibility state and see things it shouldn't
|
|
49
|
+
* (privacy bug). Documented in StateViewInternals.test.ts.
|
|
50
|
+
*
|
|
51
|
+
* Optional API but strongly recommended on client-leave; otherwise
|
|
52
|
+
* the FinalizationRegistry backstop runs at GC (non-deterministic).
|
|
53
|
+
*/
|
|
54
|
+
dispose(): void;
|
|
55
|
+
/** True iff this view can see `tree`. */
|
|
56
|
+
isVisible(tree: ChangeTree): boolean;
|
|
57
|
+
/** Mark `tree` as visible to this view. */
|
|
58
|
+
markVisible(tree: ChangeTree): void;
|
|
59
|
+
/** Clear visibility bit. */
|
|
60
|
+
unmarkVisible(tree: ChangeTree): void;
|
|
61
|
+
/** True iff this view is subscribed to `tree`. */
|
|
62
|
+
isSubscribed(tree: ChangeTree): boolean;
|
|
63
|
+
/** Set the subscription bit on `tree`. */
|
|
64
|
+
private _setSubscribed;
|
|
65
|
+
/** Clear the subscription bit on `tree`. */
|
|
66
|
+
private _clearSubscribed;
|
|
67
|
+
/** True iff this view has previously marked `tree` as invisible. */
|
|
68
|
+
isInvisible(tree: ChangeTree): boolean;
|
|
69
|
+
/** Mark `tree` as invisible to this view (used by encode loop). */
|
|
70
|
+
markInvisible(tree: ChangeTree): void;
|
|
71
|
+
/** Clear invisible bit. */
|
|
72
|
+
unmarkInvisible(tree: ChangeTree): void;
|
|
73
|
+
/** True iff this view has `tag` associated with `tree`. */
|
|
74
|
+
hasTagOnTree(tree: ChangeTree, tag: number): boolean;
|
|
75
|
+
/** Mark `tree` as carrying `tag` for this view. */
|
|
76
|
+
addTag(tree: ChangeTree, tag: number): void;
|
|
77
|
+
/** Clear this view's `tag` bit on `tree`. */
|
|
78
|
+
removeTag(tree: ChangeTree, tag: number): void;
|
|
79
|
+
/** Clear ALL tag bits this view holds on `tree` (used when the per-tag isn't known). */
|
|
80
|
+
removeAllTagsOnTree(tree: ChangeTree): void;
|
|
25
81
|
add(obj: Ref, tag?: number, checkIncludeParent?: boolean): boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Internal: force-ship an object through `view.changes` without
|
|
84
|
+
* applying stream-element routing. Called by `Encoder._emitStreamPriority`
|
|
85
|
+
* when it's draining `_pendingByView` — the element is already out of
|
|
86
|
+
* pending at that point, so re-routing back into pending would be a
|
|
87
|
+
* loop. User code should always call `add()`.
|
|
88
|
+
*/
|
|
89
|
+
_addImmediate(obj: Ref, tag?: number): void;
|
|
90
|
+
private _add;
|
|
91
|
+
/**
|
|
92
|
+
* Walk an isNew subtree marking each descendant visible. Counterpart
|
|
93
|
+
* to the `_add()` fast path: skips `view.changes` allocations because
|
|
94
|
+
* the shared encode pass emits the whole fresh subtree structurally
|
|
95
|
+
* — the view pass just needs visibility bits to let those emissions
|
|
96
|
+
* through the per-tree filter.
|
|
97
|
+
*
|
|
98
|
+
* Preserves the `@view()`-tag filter from `_add`'s forEachChild: a
|
|
99
|
+
* Schema descendant behind a non-matching field tag is skipped so
|
|
100
|
+
* tagged fields don't leak into a default-tag view. Collections have
|
|
101
|
+
* no per-field tags (`encDescriptor.tags` is empty), so the filter
|
|
102
|
+
* is a no-op for collection children.
|
|
103
|
+
*
|
|
104
|
+
* If a descendant has `isNew=false` (rare: a detached sub-collection
|
|
105
|
+
* was re-attached to a fresh parent), fall back to the full `_add`
|
|
106
|
+
* path for that branch so its cumulative state is emitted correctly.
|
|
107
|
+
*/
|
|
108
|
+
private _markSubtreeVisible;
|
|
26
109
|
protected addParentOf(childChangeTree: ChangeTree, tag: number): void;
|
|
27
110
|
remove(obj: Ref, tag?: number): this;
|
|
28
111
|
remove(obj: Ref, tag?: number, _isClear?: boolean): this;
|
|
29
112
|
has(obj: Ref): boolean;
|
|
30
113
|
hasTag(ob: Ref, tag?: number): boolean;
|
|
114
|
+
/**
|
|
115
|
+
* Persistent subscription to a collection's contents. Unlike `add()`,
|
|
116
|
+
* which is a one-shot bootstrap, `subscribe()` enrolls this view in
|
|
117
|
+
* future content changes — every subsequent push / set / add to the
|
|
118
|
+
* collection automatically flows to this view, and every removal
|
|
119
|
+
* queues a DELETE op. Works on every collection type:
|
|
120
|
+
*
|
|
121
|
+
* - `ArraySchema` / `MapSchema` / `SetSchema` / `CollectionSchema`:
|
|
122
|
+
* new children are force-shipped immediately (equivalent to
|
|
123
|
+
* `view.add(child)` per item).
|
|
124
|
+
* - `StreamSchema` (or `.stream()` maps/sets): new positions are
|
|
125
|
+
* enqueued into `_pendingByView` so the priority pass drains them
|
|
126
|
+
* respecting `maxPerTick`.
|
|
127
|
+
*
|
|
128
|
+
* Idempotent on re-subscribe. Subscribing to an already-subscribed
|
|
129
|
+
* collection is a no-op.
|
|
130
|
+
*/
|
|
131
|
+
subscribe(collection: Ref): this;
|
|
132
|
+
/**
|
|
133
|
+
* End a persistent subscription. Queues DELETE for every already-sent
|
|
134
|
+
* child and clears any pending. After this call, future content
|
|
135
|
+
* changes on the collection no longer auto-flow to this view (though
|
|
136
|
+
* direct `view.add(element)` calls still work for per-entity use).
|
|
137
|
+
*/
|
|
138
|
+
unsubscribe(collection: Ref): this;
|
|
31
139
|
clear(): void;
|
|
32
140
|
isChangeTreeVisible(changeTree: ChangeTree): boolean;
|
|
33
141
|
protected _recursiveDeleteVisibleChangeTree(changeTree: ChangeTree): void;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type ChangeTree, type Ref } from "../ChangeTree.js";
|
|
2
|
+
/**
|
|
3
|
+
* Reconcile queue membership + inherited flags for a tree that just had
|
|
4
|
+
* its root/parent assigned. See `_checkInheritedFlags` for the flag
|
|
5
|
+
* inheritance logic.
|
|
6
|
+
*/
|
|
7
|
+
export declare function checkIsFiltered(tree: ChangeTree, parent: Ref, parentIndex: number, _isNewChangeTree: boolean): void;
|
|
8
|
+
/**
|
|
9
|
+
* Inherit filter / unreliable / transient / static classification from
|
|
10
|
+
* the parent field's annotation. Collections (MapSchema / ArraySchema /
|
|
11
|
+
* etc.) inherit these from the Schema field that holds them.
|
|
12
|
+
*
|
|
13
|
+
* The common case — fresh tree attached to a parent field that carries
|
|
14
|
+
* none of the inheritable annotations — produces no flag change, no
|
|
15
|
+
* queue update, and no `parentFiltered` hit. Two small structural
|
|
16
|
+
* choices keep that case cheap without any precomputed descriptor
|
|
17
|
+
* bitmask:
|
|
18
|
+
*
|
|
19
|
+
* 1) Flag inheritance is a single bitwise OR onto `tree.flags`. The
|
|
20
|
+
* three per-annotation reads pack into `fieldBits`, the parent's
|
|
21
|
+
* inherited bits come from `parentChangeTree.flags` directly; one
|
|
22
|
+
* read-modify-write replaces three getter/setter cycles, and the
|
|
23
|
+
* bit diff against `beforeFlags` gives us the "just became static /
|
|
24
|
+
* unreliable" signal for the side-effect branches.
|
|
25
|
+
*
|
|
26
|
+
* 2) The `parentFiltered` string-key lookup is gated on
|
|
27
|
+
* `types.hasParentFilteredEntries`, which is only flipped true when
|
|
28
|
+
* `registerFilteredByParent` actually records an entry — i.e. when
|
|
29
|
+
* some @view-tagged field reaches this (child, parent, index)
|
|
30
|
+
* triple through the ancestry walk. Schemas with @view tags only on
|
|
31
|
+
* sibling fields (not along any attachment chain) skip the string
|
|
32
|
+
* concat + hash lookup entirely.
|
|
33
|
+
*/
|
|
34
|
+
export declare function checkInheritedFlags(tree: ChangeTree, parent: Ref, parentIndex: number): void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ChangeTree, ParentChain, Ref } from "../ChangeTree.js";
|
|
2
|
+
/**
|
|
3
|
+
* Add a parent to the chain. If `parent` already exists anywhere in the
|
|
4
|
+
* chain, update the primary parent's index instead (matches legacy
|
|
5
|
+
* behavior).
|
|
6
|
+
*/
|
|
7
|
+
export declare function addParent(tree: ChangeTree, parent: Ref, index: number): void;
|
|
8
|
+
/**
|
|
9
|
+
* Remove a parent from the chain.
|
|
10
|
+
* @returns true if parent was found and removed (Root.remove relies on this).
|
|
11
|
+
*/
|
|
12
|
+
export declare function removeParent(tree: ChangeTree, parent: Ref): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Find the first parent in the chain matching `predicate`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function findParent(tree: ChangeTree, predicate: (parent: Ref, index: number) => boolean): ParentChain | undefined;
|
|
17
|
+
export declare function hasParent(tree: ChangeTree, predicate: (parent: Ref, index: number) => boolean): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Return all parents as an array (debug/test helper).
|
|
20
|
+
*/
|
|
21
|
+
export declare function getAllParents(tree: ChangeTree): Array<{
|
|
22
|
+
ref: Ref;
|
|
23
|
+
index: number;
|
|
24
|
+
}>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Root } from "../Root.js";
|
|
2
|
+
import type { ChangeTree, Ref } from "../ChangeTree.js";
|
|
3
|
+
export declare function setRoot(tree: ChangeTree, root: Root): void;
|
|
4
|
+
export declare function setParent(tree: ChangeTree, parent: Ref, root?: Root, parentIndex?: number): void;
|
|
5
|
+
export declare function forEachChild(tree: ChangeTree, callback: (change: ChangeTree, at: any) => void): void;
|
|
6
|
+
/**
|
|
7
|
+
* Closure-free variant of {@link forEachChild}. Hot setRoot / setParent
|
|
8
|
+
* recursion calls this once per new Schema instance attached to the
|
|
9
|
+
* tree — the per-call closure was the #1 JS hotspot in profile-baseline.
|
|
10
|
+
* Pass an explicit `ctx` so callers can hoist the callback to module
|
|
11
|
+
* scope and avoid the allocation.
|
|
12
|
+
*/
|
|
13
|
+
export declare function forEachChildWithCtx<C>(tree: ChangeTree, ctx: C, callback: (ctx: C, change: ChangeTree, at: any) => void): void;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Root, Streamable } from "./Root.js";
|
|
2
|
+
/**
|
|
3
|
+
* Thrown (from both the `FieldBuilder` chainable and the decorator's
|
|
4
|
+
* `addField` auto-flag) when a user attempts to stream an ArraySchema.
|
|
5
|
+
* Centralized so the two callsites emit the same diagnostic.
|
|
6
|
+
*/
|
|
7
|
+
export declare const ARRAY_STREAM_NOT_SUPPORTED: string;
|
|
8
|
+
/**
|
|
9
|
+
* Per-instance bookkeeping for a streamable collection. Lazily allocated
|
|
10
|
+
* by `ensureStreamState` when the collection's ChangeTree picks up the
|
|
11
|
+
* `isStreamCollection` flag (or when the user touches `maxPerTick`).
|
|
12
|
+
*/
|
|
13
|
+
export interface StreamableState {
|
|
14
|
+
/** Per-view ADD backlog: wire-indexes not yet sent to that view. */
|
|
15
|
+
pendingByView: Map<number, Set<number>>;
|
|
16
|
+
/** Per-view SENT set — decides whether `remove()` emits a DELETE. */
|
|
17
|
+
sentByView: Map<number, Set<number>>;
|
|
18
|
+
/** Broadcast-mode ADD backlog (no active views). */
|
|
19
|
+
broadcastPending: Set<number>;
|
|
20
|
+
/** Broadcast-mode SENT set. */
|
|
21
|
+
sentBroadcast: Set<number>;
|
|
22
|
+
/** Broadcast-mode DELETE queue — flushes next shared tick. */
|
|
23
|
+
broadcastDeletes: Set<number>;
|
|
24
|
+
/** Max ADD ops emitted per tick per view (or per shared tick). */
|
|
25
|
+
maxPerTick: number;
|
|
26
|
+
/**
|
|
27
|
+
* Priority callback seeded from the schema declaration. Receives the
|
|
28
|
+
* client's StateView and the candidate element; higher return values
|
|
29
|
+
* emit first. Broadcast `encode()` ignores this and drains FIFO.
|
|
30
|
+
* Instance-level override: assign to `stream.priority`.
|
|
31
|
+
*/
|
|
32
|
+
priority?: (view: any, element: any) => number;
|
|
33
|
+
}
|
|
34
|
+
export declare function createStreamableState(): StreamableState;
|
|
35
|
+
/** Allocate `_stream` on first use (idempotent). Returns the state. */
|
|
36
|
+
export declare function ensureStreamState(s: Streamable): StreamableState;
|
|
37
|
+
/**
|
|
38
|
+
* Route an ADD into the pending backlogs.
|
|
39
|
+
* - No active views: push into broadcast pending (shared encode drains up
|
|
40
|
+
* to `maxPerTick` per tick).
|
|
41
|
+
* - With views: push into per-view pending for every currently-bound view.
|
|
42
|
+
*/
|
|
43
|
+
export declare function streamRouteAdd(s: Streamable, root: Root, index: number): void;
|
|
44
|
+
/**
|
|
45
|
+
* Route a REMOVE: silent-drop if never sent, force DELETE if already sent.
|
|
46
|
+
* Returns `true` iff no wire op reached any channel (caller can skip
|
|
47
|
+
* follow-on work like snapshotting the deleted value).
|
|
48
|
+
*/
|
|
49
|
+
export declare function streamRouteRemove(s: Streamable, root: Root, refId: number, index: number): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Queue DELETE ops for every already-sent entry on all channels and
|
|
52
|
+
* reset pending. Caller is responsible for actually clearing its own
|
|
53
|
+
* storage and releasing any element refs it owns.
|
|
54
|
+
*/
|
|
55
|
+
export declare function streamRouteClear(s: Streamable, root: Root, refId: number): void;
|
|
56
|
+
/**
|
|
57
|
+
* Push a single position into `_pendingByView[viewId]` — the building
|
|
58
|
+
* block for `StateView.add(element)` when the element lives under a
|
|
59
|
+
* streamable collection. Idempotent for already-pending positions.
|
|
60
|
+
*/
|
|
61
|
+
export declare function streamEnqueueForView(s: Streamable, viewId: number, index: number): void;
|
|
62
|
+
/**
|
|
63
|
+
* Unsubscribe a single position from a view. Returns true iff the
|
|
64
|
+
* element had already been sent and a DELETE op was queued on
|
|
65
|
+
* `view.changes`; false if it was only pending (silent drop) or not
|
|
66
|
+
* present at all.
|
|
67
|
+
*/
|
|
68
|
+
export declare function streamDequeueForView(s: Streamable, viewId: number, refId: number, index: number, viewChanges: Map<number, Map<number, number>>): boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Drop all per-view state for a disposing/GC'd StateView. Keeps memory
|
|
71
|
+
* bounded in long-running rooms with client churn.
|
|
72
|
+
*/
|
|
73
|
+
export declare function streamDropView(s: Streamable, viewId: number): void;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-view collection subscriptions — `view.subscribe(collection)` opts
|
|
3
|
+
* a view into ALL future content changes of a collection, not just a
|
|
4
|
+
* one-shot snapshot. Covers every collection type:
|
|
5
|
+
*
|
|
6
|
+
* - `ArraySchema` / `MapSchema` / `SetSchema` / `CollectionSchema`: new
|
|
7
|
+
* children are force-shipped immediately via `view._addImmediate(child)`.
|
|
8
|
+
* Subsequent field mutations on those children emit via the normal
|
|
9
|
+
* view pass (the children are now visible).
|
|
10
|
+
* - `StreamSchema` (or a `.stream()` map/set): new positions are
|
|
11
|
+
* enqueued into `_pendingByView` so the encoder's priority pass
|
|
12
|
+
* drains them respecting `maxPerTick`.
|
|
13
|
+
*
|
|
14
|
+
* The propagation hook is in `changeTree/treeAttachment.ts setParent`
|
|
15
|
+
* — every new child attachment to a collection checks the parent tree's
|
|
16
|
+
* `subscribedViews` bitmap and fans out to subscribed views.
|
|
17
|
+
*/
|
|
18
|
+
import type { ChangeTree, Ref } from "./ChangeTree.js";
|
|
19
|
+
import type { Root } from "./Root.js";
|
|
20
|
+
/**
|
|
21
|
+
* Walk the `subscribedViews` bitmap of `parentTree` and propagate a new
|
|
22
|
+
* child attachment to every subscribed view. Streams route through the
|
|
23
|
+
* priority/pending queue; all other collections force-ship immediately.
|
|
24
|
+
*/
|
|
25
|
+
export declare function propagateNewChildToSubscribers(parentTree: ChangeTree, childIndex: number, childRef: Ref, root: Root): void;
|