@colyseus/schema 4.0.20 → 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.
Files changed (96) hide show
  1. package/README.md +2 -0
  2. package/build/Metadata.d.ts +55 -2
  3. package/build/Reflection.d.ts +24 -30
  4. package/build/Schema.d.ts +70 -9
  5. package/build/annotations.d.ts +56 -13
  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 +5202 -1552
  27. package/build/index.cjs.map +1 -1
  28. package/build/index.d.ts +7 -3
  29. package/build/index.js +5202 -1552
  30. package/build/index.mjs +5193 -1552
  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 +7429 -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 +7426 -0
  38. package/build/input/index.mjs.map +1 -0
  39. package/build/types/HelperTypes.d.ts +22 -8
  40. package/build/types/TypeContext.d.ts +9 -0
  41. package/build/types/builder.d.ts +162 -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 +258 -31
  50. package/src/Reflection.ts +15 -13
  51. package/src/Schema.ts +176 -134
  52. package/src/annotations.ts +308 -236
  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 +21 -9
  88. package/src/types/TypeContext.ts +14 -2
  89. package/src/types/builder.ts +285 -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 +54 -6
  96. package/src/utils.ts +4 -6
@@ -0,0 +1,303 @@
1
+ import { OPERATION } from "../encoding/spec.js";
2
+ import { $numFields, $values, $changes } from "../types/symbols.js";
3
+ import { encode } from "../encoding/encode.js";
4
+ import { Encoder } from "../encoder/Encoder.js";
5
+ import { getEncodeDescriptor, type EncodeDescriptor } from "../encoder/EncodeDescriptor.js";
6
+ import type { Schema } from "../Schema.js";
7
+
8
+ /**
9
+ * Delivery-channel hint. Controls the wire layout:
10
+ * - `"reliable"`: single input per packet, no framing — bytes are
11
+ * wire-compatible with the standard {@link Decoder}.
12
+ * - `"unreliable"`: ring buffer of the last `historySize` inputs packed
13
+ * into one packet, each prefixed with a varint length. Gives the
14
+ * receiver redundancy against dropped packets. Use
15
+ * {@link InputDecoder.decodeAll} on the receiving end.
16
+ */
17
+ export type InputMode = "reliable" | "unreliable";
18
+
19
+ export interface InputEncoderOptions {
20
+ /** Defaults to `"reliable"`. */
21
+ mode?: InputMode;
22
+
23
+ /**
24
+ * Unreliable-mode only. Number of past inputs to pack into each
25
+ * packet as redundancy against drops. Default: 3. Ignored in
26
+ * reliable mode (always exactly one input per packet).
27
+ */
28
+ historySize?: number;
29
+
30
+ /**
31
+ * When `true`, `encode()` emits only fields that changed since the
32
+ * previous call. First call (or first after `reset()`) still emits
33
+ * a full snapshot since there's no baseline to diff against.
34
+ *
35
+ * **Reliable mode:** returns an empty `Uint8Array` when nothing
36
+ * changed — caller can skip sending.
37
+ *
38
+ * **Unreliable mode:** no-change ticks don't push a new ring slot
39
+ * (avoids bloating the ring with empties); the existing ring is
40
+ * still re-emitted for redundancy. Wire ops use absolute values so
41
+ * cross-packet re-application is per-field idempotent.
42
+ *
43
+ * Decoder side is unchanged in either case: `(index|ADD)` wire ops
44
+ * apply to the bound instance; fields absent from a packet stay at
45
+ * their previously decoded value.
46
+ */
47
+ delta?: boolean;
48
+
49
+ /**
50
+ * Override the reliable-mode output buffer. Default: 256 bytes,
51
+ * auto-grown on overflow.
52
+ */
53
+ buffer?: Uint8Array;
54
+ }
55
+
56
+ const DEFAULT_SLOT_SIZE = 256;
57
+ const LENGTH_PREFIX_WORST_CASE = 5; // varint max for a uint32 length.
58
+
59
+ /**
60
+ * Bound single-struct encoder for client→server input packets. Holds a
61
+ * reference to a Schema instance and produces wire-compatible bytes.
62
+ *
63
+ * **Reliable mode** emits one snapshot per `encode()`. The bytes decode
64
+ * cleanly through the standard {@link Decoder}.
65
+ *
66
+ * **Unreliable mode** pushes each snapshot onto a ring buffer of size
67
+ * `historySize` and emits the last N snapshots in one packet, each
68
+ * framed with a varint length prefix. Use {@link InputDecoder.decodeAll}
69
+ * to walk the framed packet on the receiving end.
70
+ *
71
+ * **Delta** (`delta: true`, any mode) wraps a standard {@link Encoder}
72
+ * and emits only fields that changed since the last call, via the
73
+ * setter-populated ChangeTree dirty set.
74
+ *
75
+ * Flat primitive fields only. Nested Schema / collection fields throw
76
+ * at construction.
77
+ */
78
+ export class InputEncoder<T extends Schema = any> {
79
+ readonly instance: T;
80
+ readonly mode: InputMode;
81
+ readonly historySize: number;
82
+ readonly delta: boolean;
83
+
84
+ private readonly _desc: EncodeDescriptor;
85
+ private readonly _numFields: number;
86
+
87
+ // Reliable-mode output + its iterator. Auto-grown on overflow, so
88
+ // not `readonly` — `_encodeFull()` may swap in a larger buffer.
89
+ private _buffer: Uint8Array;
90
+ private readonly _it = { offset: 0 };
91
+
92
+ // Unreliable-mode ring state. `_outBuffer` is where the concatenated
93
+ // packet lives; `_slots` / `_slotLens` hold each snapshot's bytes.
94
+ private _slots?: Uint8Array[];
95
+ private readonly _slotLens?: number[];
96
+ private _slotHead: number = 0;
97
+ private _slotCount: number = 0;
98
+ private _outBuffer?: Uint8Array;
99
+
100
+ // Delta-mode delegate. Setter `$track` calls populate its
101
+ // ChangeTree; `encode()` drains dirty fields into its buffer.
102
+ private readonly _encoder?: Encoder<T>;
103
+
104
+ constructor(instance: T, options: InputEncoderOptions = {}) {
105
+ this.instance = instance;
106
+ this.mode = options.mode ?? "reliable";
107
+ this.delta = options.delta ?? false;
108
+ this.historySize = this.mode === "unreliable"
109
+ ? Math.max(1, options.historySize ?? 3)
110
+ : 1;
111
+
112
+ // Resolve + validate schema metadata up front.
113
+ this._desc = getEncodeDescriptor(instance);
114
+ const numFields = this._desc.metadata?.[$numFields];
115
+ if (numFields === undefined) {
116
+ throw new Error(`InputEncoder: '${instance.constructor.name}' has no fields`);
117
+ }
118
+ this._numFields = numFields;
119
+ for (let i = 0; i <= numFields; i++) {
120
+ if (this._desc.names[i] !== undefined && this._desc.encoders[i] === undefined) {
121
+ throw new Error(
122
+ `InputEncoder: non-primitive field '${this._desc.names[i]}' on '${instance.constructor.name}' is not supported. Use Encoder for state containing refs/collections.`
123
+ );
124
+ }
125
+ }
126
+
127
+ this._buffer = options.buffer ?? new Uint8Array(DEFAULT_SLOT_SIZE);
128
+
129
+ if (this.mode === "unreliable") {
130
+ this._slots = new Array(this.historySize);
131
+ this._slotLens = new Array(this.historySize).fill(0);
132
+ for (let i = 0; i < this.historySize; i++) {
133
+ this._slots[i] = new Uint8Array(DEFAULT_SLOT_SIZE);
134
+ }
135
+ this._outBuffer = new Uint8Array(
136
+ (DEFAULT_SLOT_SIZE + LENGTH_PREFIX_WORST_CASE) * this.historySize,
137
+ );
138
+ }
139
+
140
+ if (this.delta) {
141
+ // Delegate to the standard Encoder — it attaches a Root and
142
+ // drains the setter-populated dirty set on each encode().
143
+ // Tracking stays enabled so setters keep populating it.
144
+ this._encoder = new Encoder<T>(instance);
145
+ } else {
146
+ // Full mode reads `$values` directly; setter-side tracking
147
+ // is pure overhead, so pause it.
148
+ instance.pauseTracking();
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Encode the bound instance. Returns a subarray of an internal
154
+ * buffer — copy if retaining across calls.
155
+ *
156
+ * Output shape by configuration:
157
+ * - `reliable` + full: one snapshot's worth of bytes.
158
+ * - `reliable` + delta: only changed fields, or empty when nothing
159
+ * changed.
160
+ * - `unreliable` + full: ring of last `historySize` snapshots,
161
+ * length-framed per slot.
162
+ * - `unreliable` + delta: ring of last `historySize` deltas. No-
163
+ * change ticks don't push a new slot but still re-emit the ring.
164
+ * Empty only until the first change has been pushed.
165
+ *
166
+ * Buffers auto-grow on overflow; a one-time `console.warn` is
167
+ * emitted the first time it happens.
168
+ */
169
+ encode(): Uint8Array {
170
+ const blob = this.delta ? this._produceDelta() : this._produceFull();
171
+ return this.mode === "reliable" ? blob : this._pushAndEmitRing(blob);
172
+ }
173
+
174
+ /**
175
+ * Reset the encoder's internal state:
176
+ * - Unreliable mode: drops the ring buffer.
177
+ * - Delta mode: re-marks every currently populated field as dirty,
178
+ * so the next `encode()` emits a fresh full snapshot.
179
+ *
180
+ * Useful on disconnect / reconnect / scene transitions.
181
+ */
182
+ reset(): void {
183
+ this._slotHead = 0;
184
+ this._slotCount = 0;
185
+ if (this.delta) {
186
+ this._encoder!.discardChanges();
187
+ const tree = (this.instance as any)[$changes];
188
+ const values = (this.instance as any)[$values];
189
+ for (let i = 0; i <= this._numFields; i++) {
190
+ if (values[i] === undefined || values[i] === null) continue;
191
+ tree.markDirty(i);
192
+ }
193
+ }
194
+ }
195
+
196
+ // ────────────────────────────────────────────────────────────────────
197
+ // Blob producers — return a single snapshot (or delta) of the
198
+ // current instance. Caller routes by mode.
199
+ // ────────────────────────────────────────────────────────────────────
200
+
201
+ /** Write every populated primitive field into `_buffer`. */
202
+ private _produceFull(): Uint8Array {
203
+ let buf = this._buffer;
204
+ const it = this._it;
205
+ this._writeFields(buf, it);
206
+ if (it.offset > buf.byteLength) {
207
+ buf = this._buffer = InputEncoder._grow(buf, it.offset, "reliable encode");
208
+ this._writeFields(buf, it);
209
+ }
210
+ return buf.subarray(0, it.offset);
211
+ }
212
+
213
+ /** Delegate to the wrapped Encoder, then clear its dirty set. */
214
+ private _produceDelta(): Uint8Array {
215
+ const bytes = this._encoder!.encode();
216
+ this._encoder!.discardChanges();
217
+ return bytes;
218
+ }
219
+
220
+ /** Emit every populated field as `(index|ADD)` + value. */
221
+ private _writeFields(buf: Uint8Array, it: { offset: number }): void {
222
+ const values = (this.instance as any)[$values];
223
+ const encoders = this._desc.encoders;
224
+ it.offset = 0;
225
+ for (let i = 0; i <= this._numFields; i++) {
226
+ const value = values[i];
227
+ if (value === undefined || value === null) continue;
228
+ buf[it.offset++] = (i | OPERATION.ADD) & 255;
229
+ (encoders[i] as (b: Uint8Array, v: any, it: any) => void)(buf, value, it);
230
+ }
231
+ }
232
+
233
+ // ────────────────────────────────────────────────────────────────────
234
+ // Ring — push blob to the current slot, concat (oldest → newest)
235
+ // into the output buffer, return the framed packet.
236
+ // ────────────────────────────────────────────────────────────────────
237
+
238
+ private _pushAndEmitRing(blob: Uint8Array): Uint8Array {
239
+ // Empty blob = no-change delta tick. Skip the push but re-emit
240
+ // the existing ring (redundancy). Empty ring → empty output.
241
+ if (blob.length === 0) {
242
+ if (this._slotCount === 0) return this._outBuffer!.subarray(0, 0);
243
+ return this._emitRing();
244
+ }
245
+
246
+ // Copy blob into the current slot, growing it if needed.
247
+ let slot = this._slots![this._slotHead];
248
+ if (blob.length > slot.byteLength) {
249
+ slot = this._slots![this._slotHead] = InputEncoder._grow(
250
+ slot, blob.length, "unreliable ring slot",
251
+ );
252
+ }
253
+ slot.set(blob);
254
+ this._slotLens![this._slotHead] = blob.length;
255
+ this._slotHead = (this._slotHead + 1) % this.historySize;
256
+ if (this._slotCount < this.historySize) this._slotCount++;
257
+
258
+ return this._emitRing();
259
+ }
260
+
261
+ private _emitRing(): Uint8Array {
262
+ // Upper bound: sum of slot byte counts + per-slot varint length.
263
+ let needed = 0;
264
+ for (let i = 0; i < this._slotCount; i++) {
265
+ needed += this._slotLens![i] + LENGTH_PREFIX_WORST_CASE;
266
+ }
267
+
268
+ let out = this._outBuffer!;
269
+ if (needed > out.byteLength) {
270
+ out = this._outBuffer = InputEncoder._grow(out, needed, "unreliable output packet");
271
+ }
272
+
273
+ const outIt = { offset: 0 };
274
+ const oldest = (this._slotHead - this._slotCount + this.historySize) % this.historySize;
275
+ for (let i = 0; i < this._slotCount; i++) {
276
+ const idx = (oldest + i) % this.historySize;
277
+ const len = this._slotLens![idx];
278
+ encode.number(out, len, outIt);
279
+ out.set(this._slots![idx].subarray(0, len), outIt.offset);
280
+ outIt.offset += len;
281
+ }
282
+ return out.subarray(0, outIt.offset);
283
+ }
284
+
285
+ // ────────────────────────────────────────────────────────────────────
286
+ // Buffer growth. Uint8Array writes past `byteLength` silently drop
287
+ // but `it.offset` still advances — callers detect overflow via the
288
+ // `offset > byteLength` check and re-encode into the grown buffer.
289
+ // ────────────────────────────────────────────────────────────────────
290
+
291
+ private static _warned = false;
292
+ private static _grow(buf: Uint8Array, needed: number, where: string): Uint8Array {
293
+ const newSize = Math.max(needed, buf.byteLength * 2);
294
+ if (!InputEncoder._warned) {
295
+ InputEncoder._warned = true;
296
+ console.warn(
297
+ `@colyseus/schema/input: InputEncoder buffer overflow in ${where}. ` +
298
+ `Growing to ${newSize} bytes. Pass a larger { buffer } option to avoid this at runtime.`
299
+ );
300
+ }
301
+ return new Uint8Array(newSize);
302
+ }
303
+ }
@@ -0,0 +1,3 @@
1
+ export { InputEncoder } from "./InputEncoder.js";
2
+ export type { InputMode, InputEncoderOptions } from "./InputEncoder.js";
3
+ export { InputDecoder } from "./InputDecoder.js";
@@ -4,6 +4,8 @@ import type { ArraySchema } from "./custom/ArraySchema.js";
4
4
  import type { CollectionSchema } from "./custom/CollectionSchema.js";
5
5
  import type { MapSchema } from "./custom/MapSchema.js";
6
6
  import type { SetSchema } from "./custom/SetSchema.js";
7
+ import type { StreamSchema } from "./custom/StreamSchema.js";
8
+ import type { FieldBuilder } from "./builder.js";
7
9
 
8
10
  export type Constructor<T = {}> = new (...args: any[]) => T;
9
11
 
@@ -20,8 +22,11 @@ export interface Collection<K = any, V = any, IT = V> {
20
22
  entries(): IterableIterator<[K, V]>;
21
23
  }
22
24
 
23
- export type InferValueType<T extends DefinitionType> =
24
- T extends "string" ? string
25
+ export type InferValueType<T> =
26
+ // FieldBuilder<V> unwraps to V (used by the zod-style schema() API)
27
+ T extends FieldBuilder<infer V> ? V
28
+
29
+ : T extends "string" ? string
25
30
  : T extends "number" ? number
26
31
  : T extends "int8" ? number
27
32
  : T extends "uint8" ? number
@@ -42,6 +47,8 @@ export type InferValueType<T extends DefinitionType> =
42
47
  : T extends { type: { map: infer ChildType } } ? (ChildType extends Record<string | number, string | number> ? MapSchema<ChildType[keyof ChildType]> : MapSchema<ChildType>) // TS ENUM
43
48
  : T extends { type: { set: infer ChildType } } ? (ChildType extends Record<string | number, string | number> ? SetSchema<ChildType[keyof ChildType]> : SetSchema<ChildType>) // TS ENUM
44
49
  : T extends { type: { collection: infer ChildType } } ? (ChildType extends Record<string | number, string | number> ? CollectionSchema<ChildType[keyof ChildType]> : CollectionSchema<ChildType>) // TS ENUM
50
+ : T extends { type: { stream: infer ChildType extends Constructor } } ? StreamSchema<InstanceType<ChildType>>
51
+ : T extends { type: { stream: infer ChildType } } ? StreamSchema<ChildType>
45
52
  : T extends { type: infer ChildType } ? (ChildType extends Record<string | number, string | number> ? ChildType[keyof ChildType] : ChildType) // TS ENUM
46
53
 
47
54
  // Handle direct array patterns
@@ -63,6 +70,9 @@ export type InferValueType<T extends DefinitionType> =
63
70
  : T extends { collection: infer ChildType extends RawPrimitiveType } ? CollectionSchema<InferValueType<ChildType>> // primitive types
64
71
  : T extends { collection: infer ChildType } ? (ChildType extends Record<string | number, string | number> ? CollectionSchema<ChildType[keyof ChildType]> : CollectionSchema<ChildType>) // TS ENUM
65
72
 
73
+ : T extends { stream: infer ChildType extends Constructor } ? StreamSchema<InstanceType<ChildType>>
74
+ : T extends { stream: infer ChildType } ? StreamSchema<ChildType>
75
+
66
76
  // Handle direct types
67
77
  : T extends Constructor ? InstanceType<T>
68
78
  : T extends Record<string | number, string | number> ? T[keyof T] // TS ENUM
@@ -70,10 +80,12 @@ export type InferValueType<T extends DefinitionType> =
70
80
 
71
81
  : never;
72
82
 
73
- export type InferSchemaInstanceType<T extends Definition> = {
74
- [K in keyof T]: T[K] extends (...args: any[]) => any
75
- ? (T[K] extends new (...args: any[]) => any ? InferValueType<T[K]> : T[K])
76
- : InferValueType<T[K]>
83
+ export type InferSchemaInstanceType<T> = {
84
+ [K in keyof T]: T[K] extends FieldBuilder<any>
85
+ ? InferValueType<T[K]>
86
+ : T[K] extends (...args: any[]) => any
87
+ ? (T[K] extends new (...args: any[]) => any ? InferValueType<T[K]> : T[K])
88
+ : InferValueType<T[K]>
77
89
  } & Schema;
78
90
 
79
91
  export type NonFunctionProps<T> = Omit<T, {
@@ -95,8 +107,8 @@ export type NonFunctionNonPrimitivePropNames<T> = {
95
107
  // Helper to recursively convert Schema instances to their JSON representation
96
108
  type ToJSONValue<U> = U extends Schema ? ToJSON<U> : PrimitiveStringToType<U>;
97
109
 
98
- export type ToJSON<T> = {
99
- [K in keyof T as T[K] extends Function ? never : K]:
110
+ export type ToJSON<T> = NonFunctionProps<{
111
+ [K in keyof T]:
100
112
  T[K] extends MapSchema<infer U> ? Record<string, ToJSONValue<U>>
101
113
  : T[K] extends Map<string, infer U> ? Record<string, ToJSONValue<U>>
102
114
  : T[K] extends ArraySchema<infer U> ? ToJSONValue<U>[]
@@ -104,7 +116,7 @@ export type ToJSON<T> = {
104
116
  : T[K] extends CollectionSchema<infer U> ? ToJSONValue<U>[]
105
117
  : T[K] extends Schema ? ToJSON<T[K]>
106
118
  : T[K]
107
- };
119
+ }>;
108
120
 
109
121
  // Helper type to check if T is exactly 'never' (meaning no InitProps was provided)
110
122
  export type IsNever<T> = [T] extends [never] ? true : false;
@@ -1,6 +1,6 @@
1
1
  import { Metadata } from "../Metadata.js";
2
2
  import { Schema } from "../Schema.js";
3
- import { $viewFieldIndexes } from "./symbols.js";
3
+ import { $streamFieldIndexes, $viewFieldIndexes } from "./symbols.js";
4
4
 
5
5
  export class TypeContext {
6
6
  types: { [id: number]: typeof Schema; } = {};
@@ -8,6 +8,15 @@ export class TypeContext {
8
8
 
9
9
  hasFilters: boolean = false;
10
10
  parentFiltered: {[typeIdAndParentIndex: string]: boolean} = {};
11
+ /**
12
+ * True iff `parentFiltered` has at least one entry. Flipped on by
13
+ * `registerFilteredByParent` and read in `checkInheritedFlags` as a
14
+ * cheap gate to skip the string-keyed `parentFiltered[key]` lookup
15
+ * when no class has registered filter inheritance via ancestry — the
16
+ * common case when @view tags exist only on sibling fields, not
17
+ * along any attachment chain.
18
+ */
19
+ hasParentFilteredEntries: boolean = false;
11
20
 
12
21
  /**
13
22
  * For inheritance support
@@ -100,7 +109,9 @@ export class TypeContext {
100
109
  const metadata: Metadata = (klass[Symbol.metadata] ??= {} as Metadata);
101
110
 
102
111
  // if any schema/field has filters, mark "context" as having filters.
103
- if (metadata[$viewFieldIndexes]) {
112
+ // Stream fields are always view-scoped — treat like @view tags for
113
+ // filter inheritance.
114
+ if (metadata[$viewFieldIndexes] || metadata[$streamFieldIndexes]) {
104
115
  this.hasFilters = true;
105
116
  }
106
117
 
@@ -142,6 +153,7 @@ export class TypeContext {
142
153
 
143
154
  key += `-${parentIndex}`;
144
155
  this.parentFiltered[key] = true;
156
+ this.hasParentFilteredEntries = true;
145
157
  }
146
158
 
147
159
  debug() {