@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.
Files changed (96) hide show
  1. package/README.md +2 -0
  2. package/build/Metadata.d.ts +56 -2
  3. package/build/Reflection.d.ts +28 -34
  4. package/build/Schema.d.ts +70 -9
  5. package/build/annotations.d.ts +64 -17
  6. package/build/codegen/cli.cjs +84 -67
  7. package/build/codegen/cli.cjs.map +1 -1
  8. package/build/decoder/DecodeOperation.d.ts +48 -5
  9. package/build/decoder/Decoder.d.ts +2 -2
  10. package/build/decoder/strategy/Callbacks.d.ts +1 -1
  11. package/build/encoder/ChangeRecorder.d.ts +107 -0
  12. package/build/encoder/ChangeTree.d.ts +218 -69
  13. package/build/encoder/EncodeDescriptor.d.ts +63 -0
  14. package/build/encoder/EncodeOperation.d.ts +25 -2
  15. package/build/encoder/Encoder.d.ts +59 -3
  16. package/build/encoder/MapJournal.d.ts +62 -0
  17. package/build/encoder/RefIdAllocator.d.ts +35 -0
  18. package/build/encoder/Root.d.ts +94 -13
  19. package/build/encoder/StateView.d.ts +116 -8
  20. package/build/encoder/changeTree/inheritedFlags.d.ts +34 -0
  21. package/build/encoder/changeTree/liveIteration.d.ts +3 -0
  22. package/build/encoder/changeTree/parentChain.d.ts +24 -0
  23. package/build/encoder/changeTree/treeAttachment.d.ts +13 -0
  24. package/build/encoder/streaming.d.ts +73 -0
  25. package/build/encoder/subscriptions.d.ts +25 -0
  26. package/build/index.cjs +5258 -1549
  27. package/build/index.cjs.map +1 -1
  28. package/build/index.d.ts +7 -3
  29. package/build/index.js +5258 -1549
  30. package/build/index.mjs +5249 -1549
  31. package/build/index.mjs.map +1 -1
  32. package/build/input/InputDecoder.d.ts +32 -0
  33. package/build/input/InputEncoder.d.ts +117 -0
  34. package/build/input/index.cjs +7453 -0
  35. package/build/input/index.cjs.map +1 -0
  36. package/build/input/index.d.ts +3 -0
  37. package/build/input/index.mjs +7450 -0
  38. package/build/input/index.mjs.map +1 -0
  39. package/build/types/HelperTypes.d.ts +67 -9
  40. package/build/types/TypeContext.d.ts +9 -0
  41. package/build/types/builder.d.ts +192 -0
  42. package/build/types/custom/ArraySchema.d.ts +25 -4
  43. package/build/types/custom/CollectionSchema.d.ts +30 -2
  44. package/build/types/custom/MapSchema.d.ts +52 -3
  45. package/build/types/custom/SetSchema.d.ts +32 -2
  46. package/build/types/custom/StreamSchema.d.ts +114 -0
  47. package/build/types/symbols.d.ts +48 -5
  48. package/package.json +9 -3
  49. package/src/Metadata.ts +259 -31
  50. package/src/Reflection.ts +15 -13
  51. package/src/Schema.ts +176 -134
  52. package/src/annotations.ts +365 -252
  53. package/src/bench_bloat.ts +173 -0
  54. package/src/bench_decode.ts +221 -0
  55. package/src/bench_decode_mem.ts +165 -0
  56. package/src/bench_encode.ts +108 -0
  57. package/src/bench_init.ts +150 -0
  58. package/src/bench_static.ts +109 -0
  59. package/src/bench_stream.ts +295 -0
  60. package/src/bench_view_cmp.ts +142 -0
  61. package/src/codegen/languages/csharp.ts +0 -24
  62. package/src/codegen/parser.ts +83 -61
  63. package/src/decoder/DecodeOperation.ts +168 -63
  64. package/src/decoder/Decoder.ts +20 -10
  65. package/src/decoder/ReferenceTracker.ts +4 -0
  66. package/src/decoder/strategy/Callbacks.ts +30 -26
  67. package/src/decoder/strategy/getDecoderStateCallbacks.ts +16 -13
  68. package/src/encoder/ChangeRecorder.ts +276 -0
  69. package/src/encoder/ChangeTree.ts +674 -519
  70. package/src/encoder/EncodeDescriptor.ts +213 -0
  71. package/src/encoder/EncodeOperation.ts +107 -65
  72. package/src/encoder/Encoder.ts +630 -119
  73. package/src/encoder/MapJournal.ts +124 -0
  74. package/src/encoder/RefIdAllocator.ts +68 -0
  75. package/src/encoder/Root.ts +247 -120
  76. package/src/encoder/StateView.ts +592 -121
  77. package/src/encoder/changeTree/inheritedFlags.ts +217 -0
  78. package/src/encoder/changeTree/liveIteration.ts +74 -0
  79. package/src/encoder/changeTree/parentChain.ts +131 -0
  80. package/src/encoder/changeTree/treeAttachment.ts +171 -0
  81. package/src/encoder/streaming.ts +232 -0
  82. package/src/encoder/subscriptions.ts +71 -0
  83. package/src/index.ts +15 -3
  84. package/src/input/InputDecoder.ts +57 -0
  85. package/src/input/InputEncoder.ts +303 -0
  86. package/src/input/index.ts +3 -0
  87. package/src/types/HelperTypes.ts +121 -24
  88. package/src/types/TypeContext.ts +14 -2
  89. package/src/types/builder.ts +331 -0
  90. package/src/types/custom/ArraySchema.ts +210 -197
  91. package/src/types/custom/CollectionSchema.ts +115 -35
  92. package/src/types/custom/MapSchema.ts +162 -58
  93. package/src/types/custom/SetSchema.ts +128 -39
  94. package/src/types/custom/StreamSchema.ts +310 -0
  95. package/src/types/symbols.ts +93 -6
  96. 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
- defineTypes,
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 { encodeSchemaOperation, encodeArray, encodeKeyValueOperation } from "./encoder/EncodeOperation.js";
71
- export { ChangeTree, type Ref, type IRef, type ChangeSetName, type ChangeSet} from "./encoder/ChangeTree.js";
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
+ }