@colyseus/schema 4.0.19 → 5.0.0
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 +55 -2
- package/build/Reflection.d.ts +24 -30
- package/build/Schema.d.ts +70 -9
- package/build/annotations.d.ts +56 -13
- package/build/codegen/cli.cjs +84 -44
- 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 +5202 -1552
- package/build/index.cjs.map +1 -1
- package/build/index.d.ts +7 -3
- package/build/index.js +5202 -1552
- package/build/index.mjs +5193 -1552
- 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 +7429 -0
- package/build/input/index.cjs.map +1 -0
- package/build/input/index.d.ts +3 -0
- package/build/input/index.mjs +7426 -0
- package/build/input/index.mjs.map +1 -0
- package/build/types/HelperTypes.d.ts +22 -8
- package/build/types/TypeContext.d.ts +9 -0
- package/build/types/builder.d.ts +162 -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 +8 -2
- package/src/Metadata.ts +258 -31
- package/src/Reflection.ts +15 -13
- package/src/Schema.ts +176 -134
- package/src/annotations.ts +308 -236
- 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/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 +21 -9
- package/src/types/TypeContext.ts +14 -2
- package/src/types/builder.ts +285 -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 +54 -6
- package/src/utils.ts +4 -6
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared routing helpers for streamable collections (`StreamSchema`,
|
|
3
|
+
* `MapSchema.stream()`, etc.).
|
|
4
|
+
*
|
|
5
|
+
* Each streamable class carries exactly one lazy slot (`_stream`) that
|
|
6
|
+
* holds the 6 per-view / broadcast bookkeeping structures. Keeping the
|
|
7
|
+
* slot undefined until streaming actually activates means non-streaming
|
|
8
|
+
* `MapSchema` / `SetSchema` instances pay zero Map/Set allocations. One
|
|
9
|
+
* declared slot → hidden-class shape stays stable across streaming and
|
|
10
|
+
* non-streaming instances, so V8's ICs on `$items` / `journal` / etc.
|
|
11
|
+
* stay monomorphic.
|
|
12
|
+
*
|
|
13
|
+
* Lives alongside `changeTree/*.ts` — another directory of module-level
|
|
14
|
+
* free functions that operate on ChangeTree instances.
|
|
15
|
+
*/
|
|
16
|
+
import { OPERATION } from "../encoding/spec.js";
|
|
17
|
+
import type { Root, Streamable } from "./Root.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Thrown (from both the `FieldBuilder` chainable and the decorator's
|
|
21
|
+
* `addField` auto-flag) when a user attempts to stream an ArraySchema.
|
|
22
|
+
* Centralized so the two callsites emit the same diagnostic.
|
|
23
|
+
*/
|
|
24
|
+
export const ARRAY_STREAM_NOT_SUPPORTED =
|
|
25
|
+
"ArraySchema does not support streaming — positional ops " +
|
|
26
|
+
"(splice / unshift / reverse) shift subsequent indexes, so holding " +
|
|
27
|
+
"ADDs back for a later tick under `maxPerTick` would desync the " +
|
|
28
|
+
"decoder. Use `t.stream(X)` (stable monotonic positions) or " +
|
|
29
|
+
"`t.map(X).stream()` (stable keys) instead.";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Per-instance bookkeeping for a streamable collection. Lazily allocated
|
|
33
|
+
* by `ensureStreamState` when the collection's ChangeTree picks up the
|
|
34
|
+
* `isStreamCollection` flag (or when the user touches `maxPerTick`).
|
|
35
|
+
*/
|
|
36
|
+
export interface StreamableState {
|
|
37
|
+
/** Per-view ADD backlog: wire-indexes not yet sent to that view. */
|
|
38
|
+
pendingByView: Map<number, Set<number>>;
|
|
39
|
+
/** Per-view SENT set — decides whether `remove()` emits a DELETE. */
|
|
40
|
+
sentByView: Map<number, Set<number>>;
|
|
41
|
+
/** Broadcast-mode ADD backlog (no active views). */
|
|
42
|
+
broadcastPending: Set<number>;
|
|
43
|
+
/** Broadcast-mode SENT set. */
|
|
44
|
+
sentBroadcast: Set<number>;
|
|
45
|
+
/** Broadcast-mode DELETE queue — flushes next shared tick. */
|
|
46
|
+
broadcastDeletes: Set<number>;
|
|
47
|
+
/** Max ADD ops emitted per tick per view (or per shared tick). */
|
|
48
|
+
maxPerTick: number;
|
|
49
|
+
/**
|
|
50
|
+
* Priority callback seeded from the schema declaration. Receives the
|
|
51
|
+
* client's StateView and the candidate element; higher return values
|
|
52
|
+
* emit first. Broadcast `encode()` ignores this and drains FIFO.
|
|
53
|
+
* Instance-level override: assign to `stream.priority`.
|
|
54
|
+
*/
|
|
55
|
+
priority?: (view: any, element: any) => number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function createStreamableState(): StreamableState {
|
|
59
|
+
return {
|
|
60
|
+
pendingByView: new Map(),
|
|
61
|
+
sentByView: new Map(),
|
|
62
|
+
broadcastPending: new Set(),
|
|
63
|
+
sentBroadcast: new Set(),
|
|
64
|
+
broadcastDeletes: new Set(),
|
|
65
|
+
maxPerTick: 32,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Allocate `_stream` on first use (idempotent). Returns the state. */
|
|
70
|
+
export function ensureStreamState(s: Streamable): StreamableState {
|
|
71
|
+
return (s._stream ??= createStreamableState());
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Route an ADD into the pending backlogs.
|
|
76
|
+
* - No active views: push into broadcast pending (shared encode drains up
|
|
77
|
+
* to `maxPerTick` per tick).
|
|
78
|
+
* - With views: push into per-view pending for every currently-bound view.
|
|
79
|
+
*/
|
|
80
|
+
export function streamRouteAdd(s: Streamable, root: Root, index: number): void {
|
|
81
|
+
// Broadcast mode (no views registered): seed broadcast pending so
|
|
82
|
+
// the shared `encode()` pass drains it up to `maxPerTick` per tick.
|
|
83
|
+
// View mode: do nothing — users must call `view.add(element)` per
|
|
84
|
+
// entity to subscribe it for that view. This matches the StateView
|
|
85
|
+
// design philosophy: per-client visibility is imperative, not
|
|
86
|
+
// declarative. An encode-time predicate would be O(views × entities)
|
|
87
|
+
// each tick — the whole reason StateView exists is to push that
|
|
88
|
+
// bookkeeping to game-loop cadence.
|
|
89
|
+
if (root.activeViews.size === 0) {
|
|
90
|
+
ensureStreamState(s).broadcastPending.add(index);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Route a REMOVE: silent-drop if never sent, force DELETE if already sent.
|
|
96
|
+
* Returns `true` iff no wire op reached any channel (caller can skip
|
|
97
|
+
* follow-on work like snapshotting the deleted value).
|
|
98
|
+
*/
|
|
99
|
+
export function streamRouteRemove(
|
|
100
|
+
s: Streamable,
|
|
101
|
+
root: Root,
|
|
102
|
+
refId: number,
|
|
103
|
+
index: number,
|
|
104
|
+
): boolean {
|
|
105
|
+
// If `_stream` is still undefined, streaming never saw any add/remove —
|
|
106
|
+
// nothing to unwind, and nothing was ever emitted.
|
|
107
|
+
const st = s._stream;
|
|
108
|
+
if (st === undefined) return true;
|
|
109
|
+
|
|
110
|
+
let neverSent = false;
|
|
111
|
+
|
|
112
|
+
// Broadcast side.
|
|
113
|
+
if (st.broadcastPending.delete(index)) {
|
|
114
|
+
neverSent = true;
|
|
115
|
+
} else if (st.sentBroadcast.delete(index)) {
|
|
116
|
+
st.broadcastDeletes.add(index);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Per-view side.
|
|
120
|
+
root.forEachActiveView((view) => {
|
|
121
|
+
const pending = st.pendingByView.get(view.id);
|
|
122
|
+
if (pending?.has(index)) {
|
|
123
|
+
pending.delete(index);
|
|
124
|
+
neverSent = true;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const sent = st.sentByView.get(view.id);
|
|
128
|
+
if (sent?.has(index)) {
|
|
129
|
+
sent.delete(index);
|
|
130
|
+
let changes = view.changes.get(refId);
|
|
131
|
+
if (changes === undefined) {
|
|
132
|
+
changes = new Map();
|
|
133
|
+
view.changes.set(refId, changes);
|
|
134
|
+
}
|
|
135
|
+
changes.set(index, OPERATION.DELETE);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return neverSent;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Queue DELETE ops for every already-sent entry on all channels and
|
|
144
|
+
* reset pending. Caller is responsible for actually clearing its own
|
|
145
|
+
* storage and releasing any element refs it owns.
|
|
146
|
+
*/
|
|
147
|
+
export function streamRouteClear(s: Streamable, root: Root, refId: number): void {
|
|
148
|
+
const st = s._stream;
|
|
149
|
+
if (st === undefined) return;
|
|
150
|
+
|
|
151
|
+
// Broadcast: drop never-sent pending; force DELETE for sent entries.
|
|
152
|
+
st.broadcastPending.clear();
|
|
153
|
+
for (const index of st.sentBroadcast) st.broadcastDeletes.add(index);
|
|
154
|
+
st.sentBroadcast.clear();
|
|
155
|
+
|
|
156
|
+
// Per-view: clear pending; force DELETE for sent entries via
|
|
157
|
+
// `view.changes` (drained first in encodeView).
|
|
158
|
+
root.forEachActiveView((view) => {
|
|
159
|
+
st.pendingByView.get(view.id)?.clear();
|
|
160
|
+
|
|
161
|
+
const sent = st.sentByView.get(view.id);
|
|
162
|
+
if (sent !== undefined && sent.size > 0) {
|
|
163
|
+
let changes = view.changes.get(refId);
|
|
164
|
+
if (changes === undefined) {
|
|
165
|
+
changes = new Map();
|
|
166
|
+
view.changes.set(refId, changes);
|
|
167
|
+
}
|
|
168
|
+
for (const index of sent) changes.set(index, OPERATION.DELETE);
|
|
169
|
+
sent.clear();
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Push a single position into `_pendingByView[viewId]` — the building
|
|
176
|
+
* block for `StateView.add(element)` when the element lives under a
|
|
177
|
+
* streamable collection. Idempotent for already-pending positions.
|
|
178
|
+
*/
|
|
179
|
+
export function streamEnqueueForView(s: Streamable, viewId: number, index: number): void {
|
|
180
|
+
const st = ensureStreamState(s);
|
|
181
|
+
let pending = st.pendingByView.get(viewId);
|
|
182
|
+
if (pending === undefined) {
|
|
183
|
+
pending = new Set();
|
|
184
|
+
st.pendingByView.set(viewId, pending);
|
|
185
|
+
}
|
|
186
|
+
pending.add(index);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Unsubscribe a single position from a view. Returns true iff the
|
|
191
|
+
* element had already been sent and a DELETE op was queued on
|
|
192
|
+
* `view.changes`; false if it was only pending (silent drop) or not
|
|
193
|
+
* present at all.
|
|
194
|
+
*/
|
|
195
|
+
export function streamDequeueForView(
|
|
196
|
+
s: Streamable,
|
|
197
|
+
viewId: number,
|
|
198
|
+
refId: number,
|
|
199
|
+
index: number,
|
|
200
|
+
viewChanges: Map<number, Map<number, number>>,
|
|
201
|
+
): boolean {
|
|
202
|
+
const st = s._stream;
|
|
203
|
+
if (st === undefined) return false;
|
|
204
|
+
const pending = st.pendingByView.get(viewId);
|
|
205
|
+
if (pending?.has(index)) {
|
|
206
|
+
pending.delete(index);
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
const sent = st.sentByView.get(viewId);
|
|
210
|
+
if (sent?.has(index)) {
|
|
211
|
+
sent.delete(index);
|
|
212
|
+
let changes = viewChanges.get(refId);
|
|
213
|
+
if (changes === undefined) {
|
|
214
|
+
changes = new Map();
|
|
215
|
+
viewChanges.set(refId, changes);
|
|
216
|
+
}
|
|
217
|
+
changes.set(index, OPERATION.DELETE);
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Drop all per-view state for a disposing/GC'd StateView. Keeps memory
|
|
225
|
+
* bounded in long-running rooms with client churn.
|
|
226
|
+
*/
|
|
227
|
+
export function streamDropView(s: Streamable, viewId: number): void {
|
|
228
|
+
const st = s._stream;
|
|
229
|
+
if (st === undefined) return;
|
|
230
|
+
st.pendingByView.delete(viewId);
|
|
231
|
+
st.sentByView.delete(viewId);
|
|
232
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
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, Streamable } from "./Root.js";
|
|
20
|
+
import { streamEnqueueForView } from "./streaming.js";
|
|
21
|
+
import { $changes } from "../types/symbols.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Walk the `subscribedViews` bitmap of `parentTree` and propagate a new
|
|
25
|
+
* child attachment to every subscribed view. Streams route through the
|
|
26
|
+
* priority/pending queue; all other collections force-ship immediately.
|
|
27
|
+
*/
|
|
28
|
+
export function propagateNewChildToSubscribers(
|
|
29
|
+
parentTree: ChangeTree,
|
|
30
|
+
childIndex: number,
|
|
31
|
+
childRef: Ref,
|
|
32
|
+
root: Root,
|
|
33
|
+
): void {
|
|
34
|
+
const subs = parentTree.subscribedViews;
|
|
35
|
+
if (subs === undefined) return;
|
|
36
|
+
|
|
37
|
+
const isStream = parentTree.isStreamCollection;
|
|
38
|
+
const streamable = isStream ? (parentTree.ref as unknown as Streamable) : undefined;
|
|
39
|
+
const childTree = isStream ? undefined : childRef[$changes];
|
|
40
|
+
|
|
41
|
+
// Walk set bits via clz32 — same pattern as the inline recorder
|
|
42
|
+
// iteration elsewhere in the encoder.
|
|
43
|
+
for (let slot = 0, n = subs.length; slot < n; slot++) {
|
|
44
|
+
let bits = subs[slot];
|
|
45
|
+
while (bits !== 0) {
|
|
46
|
+
const bit = bits & -bits;
|
|
47
|
+
bits ^= bit;
|
|
48
|
+
const viewId = slot * 32 + (31 - Math.clz32(bit));
|
|
49
|
+
const weakRef = root.activeViews.get(viewId);
|
|
50
|
+
const view = weakRef?.deref();
|
|
51
|
+
if (view === undefined) {
|
|
52
|
+
// View was disposed / GC'd; clear the stale subscription bit.
|
|
53
|
+
subs[slot] &= ~bit;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (isStream) {
|
|
57
|
+
// Streams bypass the recorder — enqueue for the priority
|
|
58
|
+
// pass to drain under `maxPerTick`.
|
|
59
|
+
streamEnqueueForView(streamable!, viewId, childIndex);
|
|
60
|
+
} else if (childTree !== undefined) {
|
|
61
|
+
// Non-stream collections: just markVisible. The parent's
|
|
62
|
+
// recorder already carries the ADD op (triggered by the
|
|
63
|
+
// push/set/add that led to this setParent), and the
|
|
64
|
+
// child's tree carries its construction-time dirty state
|
|
65
|
+
// — the encoder's normal view pass picks both up on the
|
|
66
|
+
// next encode, no view.changes seeding needed.
|
|
67
|
+
view.markVisible(childTree);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,9 @@ export { CollectionSchema };
|
|
|
14
14
|
import { SetSchema } from "./types/custom/SetSchema.js";
|
|
15
15
|
export { SetSchema };
|
|
16
16
|
|
|
17
|
+
import { StreamSchema } from "./types/custom/StreamSchema.js";
|
|
18
|
+
export { StreamSchema };
|
|
19
|
+
|
|
17
20
|
import { registerType, defineCustomTypes } from "./types/registry.js";
|
|
18
21
|
export { registerType, defineCustomTypes };
|
|
19
22
|
|
|
@@ -21,6 +24,8 @@ registerType("map", { constructor: MapSchema });
|
|
|
21
24
|
registerType("array", { constructor: ArraySchema });
|
|
22
25
|
registerType("set", { constructor: SetSchema });
|
|
23
26
|
registerType("collection", { constructor: CollectionSchema, });
|
|
27
|
+
// "stream" is registered inside StreamSchema.ts (same pattern as others
|
|
28
|
+
// that co-locate registerType with the class for side-effect safety).
|
|
24
29
|
|
|
25
30
|
// Utils
|
|
26
31
|
export { dumpChanges } from "./utils.js";
|
|
@@ -44,19 +49,25 @@ export { Metadata } from "./Metadata.js";
|
|
|
44
49
|
export {
|
|
45
50
|
type,
|
|
46
51
|
deprecated,
|
|
47
|
-
|
|
52
|
+
owned,
|
|
53
|
+
unreliable,
|
|
54
|
+
transient,
|
|
48
55
|
view,
|
|
49
56
|
schema,
|
|
50
57
|
entity,
|
|
51
58
|
type DefinitionType,
|
|
52
59
|
type PrimitiveType,
|
|
53
60
|
type Definition,
|
|
61
|
+
type FieldsAndMethods,
|
|
54
62
|
// Raw schema() return types
|
|
55
63
|
type SchemaWithExtendsConstructor,
|
|
56
64
|
type SchemaWithExtends,
|
|
57
65
|
type SchemaType,
|
|
58
66
|
} from "./annotations.js";
|
|
59
67
|
|
|
68
|
+
// zod-style chainable builders
|
|
69
|
+
export { t, FieldBuilder, isBuilder, type BuilderDefinition, type ChildType } from "./types/builder.js";
|
|
70
|
+
|
|
60
71
|
export { TypeContext } from "./types/TypeContext.js";
|
|
61
72
|
|
|
62
73
|
// Helper types for type inference
|
|
@@ -67,8 +78,9 @@ export { Callbacks, StateCallbackStrategy } from "./decoder/strategy/Callbacks.j
|
|
|
67
78
|
export { getRawChangesCallback } from "./decoder/strategy/RawChanges.js";
|
|
68
79
|
|
|
69
80
|
export { Encoder } from "./encoder/Encoder.js";
|
|
70
|
-
export {
|
|
71
|
-
export {
|
|
81
|
+
export { Root } from "./encoder/Root.js";
|
|
82
|
+
export { encodeSchemaOperation, encodeArray, encodeKeyValueOperation, encodeMapEntry, encodeIndexedEntry } from "./encoder/EncodeOperation.js";
|
|
83
|
+
export { ChangeTree, type Ref, type IRef } from "./encoder/ChangeTree.js";
|
|
72
84
|
export { StateView } from "./encoder/StateView.js";
|
|
73
85
|
|
|
74
86
|
export { Decoder } from "./decoder/Decoder.js";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Decoder } from "../decoder/Decoder.js";
|
|
2
|
+
import { decode, type Iterator } from "../encoding/decode.js";
|
|
3
|
+
import type { Schema } from "../Schema.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Bound single-struct decoder for input packets. Wraps the standard
|
|
7
|
+
* `Decoder` so bytes emitted by `InputEncoder` land on the bound instance.
|
|
8
|
+
*
|
|
9
|
+
* - `decode(bytes)`: single-input packet (reliable mode).
|
|
10
|
+
* - `decodeAll(bytes, cb)`: multi-input length-framed packet (unreliable
|
|
11
|
+
* mode). Invokes `cb` with the mutated instance once per framed input,
|
|
12
|
+
* oldest → newest. The instance is re-used across callbacks — consume
|
|
13
|
+
* synchronously (apply to game state) rather than holding the reference.
|
|
14
|
+
*/
|
|
15
|
+
export class InputDecoder<T extends Schema = any> {
|
|
16
|
+
readonly instance: T;
|
|
17
|
+
private readonly _decoder: Decoder<T>;
|
|
18
|
+
private readonly _it: Iterator = { offset: 0 };
|
|
19
|
+
|
|
20
|
+
constructor(instance: T) {
|
|
21
|
+
this.instance = instance;
|
|
22
|
+
this._decoder = new Decoder(instance);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Decode a single-input (reliable) packet into the bound instance.
|
|
27
|
+
* Returns the instance for chaining.
|
|
28
|
+
*/
|
|
29
|
+
decode(bytes: Uint8Array): T {
|
|
30
|
+
this._decoder.decode(bytes);
|
|
31
|
+
return this.instance;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Walk a multi-input (unreliable) packet, decoding each length-framed
|
|
36
|
+
* input into the bound instance in order and invoking `onInput` after
|
|
37
|
+
* each decode. `onInput` receives the bound instance itself — reads
|
|
38
|
+
* must be synchronous; downstream code should apply the input to game
|
|
39
|
+
* state, not retain the reference.
|
|
40
|
+
*
|
|
41
|
+
* Returns the number of inputs decoded.
|
|
42
|
+
*/
|
|
43
|
+
decodeAll(bytes: Uint8Array, onInput: (instance: T, index: number) => void): number {
|
|
44
|
+
const it = this._it;
|
|
45
|
+
it.offset = 0;
|
|
46
|
+
let count = 0;
|
|
47
|
+
while (it.offset < bytes.length) {
|
|
48
|
+
const len = decode.number(bytes, it);
|
|
49
|
+
const end = it.offset + len;
|
|
50
|
+
this._decoder.decode(bytes.subarray(it.offset, end));
|
|
51
|
+
onInput(this.instance, count);
|
|
52
|
+
it.offset = end;
|
|
53
|
+
count++;
|
|
54
|
+
}
|
|
55
|
+
return count;
|
|
56
|
+
}
|
|
57
|
+
}
|