@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
|
@@ -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
|
+
}
|
package/src/types/HelperTypes.ts
CHANGED
|
@@ -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
|
|
24
|
-
|
|
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,25 @@ export type InferValueType<T extends DefinitionType> =
|
|
|
70
80
|
|
|
71
81
|
: never;
|
|
72
82
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
:
|
|
83
|
+
// Keys whose FieldBuilder generic admits `undefined` (i.e. `.optional()` was chained).
|
|
84
|
+
type OptionalBuilderKeys<T> = {
|
|
85
|
+
[K in keyof T]: T[K] extends FieldBuilder<infer V>
|
|
86
|
+
? (undefined extends V ? K : never)
|
|
87
|
+
: never
|
|
88
|
+
}[keyof T];
|
|
89
|
+
|
|
90
|
+
type RequiredBuilderKeys<T> = Exclude<keyof T, OptionalBuilderKeys<T>>;
|
|
91
|
+
|
|
92
|
+
export type InferSchemaInstanceType<T> = {
|
|
93
|
+
[K in RequiredBuilderKeys<T>]: T[K] extends FieldBuilder<any>
|
|
94
|
+
? InferValueType<T[K]>
|
|
95
|
+
: T[K] extends (...args: any[]) => any
|
|
96
|
+
? (T[K] extends new (...args: any[]) => any ? InferValueType<T[K]> : T[K])
|
|
97
|
+
: InferValueType<T[K]>
|
|
98
|
+
} & {
|
|
99
|
+
[K in OptionalBuilderKeys<T>]?: T[K] extends FieldBuilder<infer V>
|
|
100
|
+
? V
|
|
101
|
+
: never
|
|
77
102
|
} & Schema;
|
|
78
103
|
|
|
79
104
|
export type NonFunctionProps<T> = Omit<T, {
|
|
@@ -95,16 +120,28 @@ export type NonFunctionNonPrimitivePropNames<T> = {
|
|
|
95
120
|
// Helper to recursively convert Schema instances to their JSON representation
|
|
96
121
|
type ToJSONValue<U> = U extends Schema ? ToJSON<U> : PrimitiveStringToType<U>;
|
|
97
122
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
123
|
+
type ToJSONField<X> =
|
|
124
|
+
X extends MapSchema<infer U> ? Record<string, ToJSONValue<U>>
|
|
125
|
+
: X extends Map<string, infer U> ? Record<string, ToJSONValue<U>>
|
|
126
|
+
: X extends ArraySchema<infer U> ? ToJSONValue<U>[]
|
|
127
|
+
: X extends SetSchema<infer U> ? ToJSONValue<U>[]
|
|
128
|
+
: X extends CollectionSchema<infer U> ? ToJSONValue<U>[]
|
|
129
|
+
: X extends Schema ? ToJSON<X>
|
|
130
|
+
: X;
|
|
131
|
+
|
|
132
|
+
// Keys whose value type admits `undefined` — runtime `toJSON()` omits those,
|
|
133
|
+
// so they surface as `?:` on the JSON shape.
|
|
134
|
+
type ToJSONRequiredKeys<T> = {
|
|
135
|
+
[K in keyof T]-?: undefined extends T[K] ? never : K
|
|
136
|
+
}[keyof T];
|
|
137
|
+
type ToJSONOptionalKeys<T> = {
|
|
138
|
+
[K in keyof T]-?: undefined extends T[K] ? K : never
|
|
139
|
+
}[keyof T];
|
|
140
|
+
|
|
141
|
+
export type ToJSON<T> = NonFunctionProps<
|
|
142
|
+
& { [K in ToJSONRequiredKeys<T>]: ToJSONField<T[K]> }
|
|
143
|
+
& { [K in ToJSONOptionalKeys<T>]?: ToJSONField<Exclude<T[K], undefined>> }
|
|
144
|
+
>;
|
|
108
145
|
|
|
109
146
|
// Helper type to check if T is exactly 'never' (meaning no InitProps was provided)
|
|
110
147
|
export type IsNever<T> = [T] extends [never] ? true : false;
|
|
@@ -116,15 +153,75 @@ export type IsNever<T> = [T] extends [never] ? true : false;
|
|
|
116
153
|
* - Collections can be assigned from their JSON representations
|
|
117
154
|
*/
|
|
118
155
|
export type AssignableProps<T> = {
|
|
119
|
-
[K in NonFunctionPropNames<T>]?: T[K]
|
|
156
|
+
[K in NonFunctionPropNames<T>]?: AssignableValue<T[K]>
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Value-level assignment shape shared by `AssignableProps` and
|
|
161
|
+
* `BuilderInitProps`. Captures the "you can pass the real instance, or the
|
|
162
|
+
* plain-object / array shape" pattern.
|
|
163
|
+
*/
|
|
164
|
+
export type AssignableValue<V> =
|
|
165
|
+
V extends MapSchema<infer U>
|
|
120
166
|
? MapSchema<U> | Record<string, U extends Schema ? (U | AssignableProps<U>) : U>
|
|
121
|
-
:
|
|
167
|
+
: V extends ArraySchema<infer U>
|
|
122
168
|
? ArraySchema<U> | (U extends Schema ? (U | AssignableProps<U>)[] : U[])
|
|
123
|
-
:
|
|
169
|
+
: V extends SetSchema<infer U>
|
|
124
170
|
? SetSchema<U> | Set<U> | (U extends Schema ? (U | AssignableProps<U>)[] : U[])
|
|
125
|
-
:
|
|
171
|
+
: V extends CollectionSchema<infer U>
|
|
126
172
|
? CollectionSchema<U> | (U extends Schema ? (U | AssignableProps<U>)[] : U[])
|
|
127
|
-
:
|
|
128
|
-
?
|
|
129
|
-
:
|
|
130
|
-
|
|
173
|
+
: V extends Schema
|
|
174
|
+
? V | AssignableProps<V>
|
|
175
|
+
: V;
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// BuilderInitProps<T> — init-props shape derived from a schema() fields map.
|
|
179
|
+
// Unlike AssignableProps (fully partial, for `.assign()` updates), this type
|
|
180
|
+
// enforces required vs optional based on per-field `HasDefault` + `undefined`.
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
// Compile-time analogue of schema()'s Schema-ref auto-default rule:
|
|
184
|
+
// if the ref has no `initialize`, or a zero-arg `initialize`, schema()
|
|
185
|
+
// auto-instantiates it — so the field is omittable at construction.
|
|
186
|
+
export type RefHasDefault<C> =
|
|
187
|
+
C extends { prototype: { initialize(...args: infer P): any } }
|
|
188
|
+
? (P extends readonly [] ? true : false)
|
|
189
|
+
: true;
|
|
190
|
+
|
|
191
|
+
// Resolve a fields-map entry to its runtime value type.
|
|
192
|
+
type FieldValue<F> =
|
|
193
|
+
F extends FieldBuilder<infer V, boolean, boolean> ? V
|
|
194
|
+
: F extends new (...args: any[]) => infer I ? (I extends Schema ? I : never)
|
|
195
|
+
: never;
|
|
196
|
+
|
|
197
|
+
// Classify each key of a fields map as "required" / "optional" / "none"
|
|
198
|
+
// (methods). Both `HasDefault = true` and the explicit `.optional()` brand
|
|
199
|
+
// `IsOptional = true` mark the field omittable at construction. The brand
|
|
200
|
+
// sidesteps a TypeScript quirk where `undefined extends V` returned `true`
|
|
201
|
+
// for non-undefined V when V was inferred from a class with T in
|
|
202
|
+
// contravariant + covariant positions.
|
|
203
|
+
type KeyClass<T, K extends keyof T> =
|
|
204
|
+
T[K] extends FieldBuilder<unknown, infer D extends boolean, infer O extends boolean>
|
|
205
|
+
? (D extends true
|
|
206
|
+
? "optional"
|
|
207
|
+
: O extends true ? "optional" : "required")
|
|
208
|
+
: T[K] extends new (...args: any[]) => Schema
|
|
209
|
+
? (RefHasDefault<T[K]> extends true ? "optional" : "required")
|
|
210
|
+
: "none";
|
|
211
|
+
|
|
212
|
+
export type BuilderRequiredKeys<T> = {
|
|
213
|
+
[K in keyof T]-?: KeyClass<T, K> extends "required" ? K : never
|
|
214
|
+
}[keyof T];
|
|
215
|
+
|
|
216
|
+
export type BuilderOptionalKeys<T> = {
|
|
217
|
+
[K in keyof T]-?: KeyClass<T, K> extends "optional" ? K : never
|
|
218
|
+
}[keyof T];
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Constructor/init-props type for a schema() fields map. Required fields
|
|
222
|
+
* (primitives without `.default()` or `.optional()`, and Schema refs with
|
|
223
|
+
* non-zero-arg `initialize()`) are `:`; everything else is `?:`.
|
|
224
|
+
*/
|
|
225
|
+
export type BuilderInitProps<T> =
|
|
226
|
+
& { [K in BuilderRequiredKeys<T>]: AssignableValue<FieldValue<T[K]>> }
|
|
227
|
+
& { [K in BuilderOptionalKeys<T>]?: AssignableValue<Exclude<FieldValue<T[K]>, undefined>> };
|
package/src/types/TypeContext.ts
CHANGED
|
@@ -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
|
-
|
|
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() {
|