@php-skir/skir-client 1.0.19

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.
@@ -0,0 +1,4141 @@
1
+ import { decode as decodeCbor, encode as encodeCbor } from "cbor-x";
2
+ import type {
3
+ Express as ExpressApp,
4
+ json as ExpressJson,
5
+ raw as ExpressRaw,
6
+ Request as ExpressRequest,
7
+ Response as ExpressResponse,
8
+ text as ExpressText,
9
+ } from "express";
10
+
11
+ /**
12
+ * A single moment in time represented in a platform-independent format, with a
13
+ * precision of one millisecond.
14
+ *
15
+ * A `Timestamp` object can represent a maximum of ±8,640,000,000,000,000
16
+ * milliseconds, or ±100,000,000 (one hundred million) days, relative to the
17
+ * Unix epoch. This is the range from April 20, 271821 BC to September 13,
18
+ * 275760 AD.
19
+ *
20
+ * Unlike the Javascript built-in `Date` type, a `Timestamp` is immutable.
21
+ * Like a `Date`, a `Timestamp` object does not contain a timezone.
22
+ */
23
+ export class Timestamp {
24
+ /**
25
+ * Returns a `Timestamp` representing the same moment in time as the given
26
+ * Javascript `Date` object.
27
+ *
28
+ * @throws if the given `Date` object has a timestamp value of NaN
29
+ */
30
+ static from(date: Date): Timestamp {
31
+ return this.fromUnixMillis(date.getTime());
32
+ }
33
+
34
+ /**
35
+ * Creates a `Timestamp` object from a number of milliseconds from the Unix
36
+ * epoch.
37
+ *
38
+ * If the given number if outside the valid range (±8,640,000,000,000,000),
39
+ * this function will return `Timestamp.MAX` or `Timestamp.MIN` depending on
40
+ * the sign of the number.
41
+ *
42
+ * @throws if the given number is NaN
43
+ */
44
+ static fromUnixMillis(unixMillis: number): Timestamp {
45
+ if (unixMillis <= this.MIN.unixMillis) {
46
+ return Timestamp.MIN;
47
+ } else if (unixMillis < Timestamp.MAX.unixMillis) {
48
+ return new Timestamp(Math.round(unixMillis));
49
+ } else if (Number.isNaN(unixMillis)) {
50
+ throw new Error("Cannot construct Timestamp from NaN");
51
+ } else {
52
+ return Timestamp.MAX;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Creates a `Timestamp` object from a number of seconds from the Unix epoch.
58
+ *
59
+ * If the given number if outside the valid range (±8,640,000,000,000), this
60
+ * function will return `Timestamp.MAX` or `Timestamp.MIN` depending on the
61
+ * sign of the number.
62
+ *
63
+ * @throws if the given number is NaN
64
+ */
65
+ static fromUnixSeconds(unixSeconds: number): Timestamp {
66
+ return this.fromUnixMillis(unixSeconds * 1000);
67
+ }
68
+
69
+ /**
70
+ * Parses a date in the date time string format.
71
+ *
72
+ * @throws if the given string is not a date in the date time string format
73
+ * @see https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-date-time-string-format
74
+ */
75
+ static parse(date: string): Timestamp {
76
+ return this.fromUnixMillis(Date.parse(date));
77
+ }
78
+
79
+ /** Returns a `Timestamp` representing the current moment in time. */
80
+ static now(): Timestamp {
81
+ return this.fromUnixMillis(Date.now());
82
+ }
83
+
84
+ /** Thursday, 1 January 1970. */
85
+ static readonly UNIX_EPOCH = new Timestamp(0);
86
+
87
+ /**
88
+ * Earliest moment in time representable as a `Timestamp`, namely April 20,
89
+ * 271821 BC.
90
+ *
91
+ * @see https://262.ecma-international.org/5.1/#sec-15.9.1.1
92
+ */
93
+ static readonly MIN = new Timestamp(-8640000000000000);
94
+
95
+ /**
96
+ * Latest moment in time representable as a `Timestamp`, namely September 13,
97
+ * 275760 AD.
98
+ *
99
+ * @see https://262.ecma-international.org/5.1/#sec-15.9.1.1
100
+ */
101
+ static readonly MAX = new Timestamp(8640000000000000);
102
+
103
+ private constructor(
104
+ /** Number of milliseconds ellapsed since the Unix epoch. */
105
+ readonly unixMillis: number,
106
+ ) {
107
+ Object.freeze(this);
108
+ }
109
+
110
+ /** Number of seconds ellapsed since the Unix epoch. */
111
+ get unixSeconds(): number {
112
+ return this.unixMillis / 1000.0;
113
+ }
114
+
115
+ /**
116
+ * Returns a `Date` object representing the same moment in time as this
117
+ * `Timestamp`.
118
+ */
119
+ toDate(): Date {
120
+ return new Date(this.unixMillis);
121
+ }
122
+
123
+ toString(): string {
124
+ return this.toDate().toISOString();
125
+ }
126
+ }
127
+
128
+ /** An immutable array of bytes. */
129
+ export class ByteString {
130
+ /**
131
+ * Returns an immutable byte string containing all the bytes of `input` from
132
+ * `start`, inclusive, up to `end`, exclusive.
133
+ *
134
+ * If `input` is an `ArrayBuffer`, this function copies the bytes. Otherwise
135
+ * this function returns a sliced view of the input `ByteString`.
136
+ *
137
+ * @example <caption>Copy an array buffer into a byte string</caption>
138
+ * const byteString = ByteString.sliceOf(arrayBuffer);
139
+ */
140
+ static sliceOf(
141
+ input: ArrayBuffer | SharedArrayBuffer | ByteString,
142
+ start = 0,
143
+ end?: number,
144
+ ): ByteString {
145
+ const { byteLength } = input;
146
+ if (start < 0) {
147
+ start = 0;
148
+ }
149
+ if (end === undefined || end > byteLength) {
150
+ end = byteLength;
151
+ }
152
+ if (end <= start) {
153
+ return ByteString.EMPTY;
154
+ }
155
+ if (input instanceof ByteString) {
156
+ if (start <= 0 && byteLength <= end) {
157
+ // Don't copy the ByteString itself.
158
+ return input;
159
+ } else {
160
+ // Don't copy the ArrayBuffer.
161
+ const newByteOffset = input.byteOffset + start;
162
+ const newByteLength = end - start;
163
+ return new ByteString(input.arrayBuffer, newByteOffset, newByteLength);
164
+ }
165
+ } else if (input instanceof ArrayBuffer) {
166
+ return new ByteString(input.slice(start, end));
167
+ } else if (input instanceof SharedArrayBuffer) {
168
+ const slice = input.slice(start, end);
169
+ const newBuffer = new ArrayBuffer(slice.byteLength);
170
+ new Uint8Array(newBuffer).set(new Uint8Array(slice));
171
+ return new ByteString(newBuffer);
172
+ } else {
173
+ const _: never = input;
174
+ throw new TypeError(_);
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Decodes a Base64 string, which can be obtained by calling `toBase64()`.
180
+ *
181
+ * @throws if the given string is not a valid Base64 string.
182
+ * @see https://en.wikipedia.org/wiki/Base64
183
+ */
184
+ static fromBase64(base64: string): ByteString {
185
+ // See https://developer.mozilla.org/en-US/docs/Glossary/Base64
186
+ const binaryString: string = atob(base64);
187
+ const array = Uint8Array.from(binaryString, (m) => m.codePointAt(0)!);
188
+ return new this(array.buffer);
189
+ }
190
+
191
+ /**
192
+ * Decodes a hexadecimal string, which can be obtained by calling
193
+ * `toBase16()`.
194
+ *
195
+ * @throws if the given string is not a valid Base64 string.
196
+ */
197
+ static fromBase16(base16: string): ByteString {
198
+ const bytes = new Uint8Array(base16.length / 2);
199
+ for (let i = 0; i < bytes.length; ++i) {
200
+ const byte = parseInt(base16.substring(i * 2, i * 2 + 2), 16);
201
+ if (Number.isNaN(byte)) {
202
+ throw new Error("Not a valid Base64 string");
203
+ }
204
+ bytes[i] = byte;
205
+ }
206
+ return new ByteString(bytes.buffer);
207
+ }
208
+
209
+ /** An empty byte string. */
210
+ static readonly EMPTY = new ByteString(new ArrayBuffer(0));
211
+
212
+ /** Copies the contents of this byte string into the given array buffer. */
213
+ copyTo(target: ArrayBuffer, targetOffset = 0): void {
214
+ new Uint8Array(target).set(this.uint8Array, targetOffset);
215
+ }
216
+
217
+ /** Copies the contents of this byte string into a new array buffer. */
218
+ toBuffer(): ArrayBuffer {
219
+ return this.arrayBuffer.slice(
220
+ this.byteOffset,
221
+ this.byteOffset + this.byteLength,
222
+ );
223
+ }
224
+
225
+ /**
226
+ * Encodes this byte string into a Base64 string.
227
+ *
228
+ * @see https://en.wikipedia.org/wiki/Base64
229
+ */
230
+ toBase64(): string {
231
+ // See https://developer.mozilla.org/en-US/docs/Glossary/Base64
232
+ const binaryString = Array.from(this.uint8Array, (x) =>
233
+ String.fromCodePoint(x),
234
+ ).join("");
235
+ return btoa(binaryString);
236
+ }
237
+
238
+ /** Encodes this byte string into a hexadecimal string. */
239
+ toBase16(): string {
240
+ return [...this.uint8Array]
241
+ .map((x) => x.toString(16).padStart(2, "0"))
242
+ .join("");
243
+ }
244
+
245
+ at(index: number): number | undefined {
246
+ return this.uint8Array[index < 0 ? index + this.byteLength : index];
247
+ }
248
+
249
+ toString(): string {
250
+ return `ByteString(${this.byteLength})`;
251
+ }
252
+
253
+ private constructor(
254
+ private readonly arrayBuffer: ArrayBuffer,
255
+ private readonly byteOffset = 0,
256
+ /** The length of this byte string. */
257
+ readonly byteLength = arrayBuffer.byteLength,
258
+ ) {
259
+ this.uint8Array = new Uint8Array(arrayBuffer, byteOffset, byteLength);
260
+ Object.freeze(this);
261
+ }
262
+
263
+ private readonly uint8Array: Uint8Array;
264
+ }
265
+
266
+ /** A read-only JSON value. */
267
+ export type Json =
268
+ | null
269
+ | boolean
270
+ | number
271
+ | string
272
+ | readonly Json[]
273
+ | Readonly<{ [name: string]: Json }>;
274
+
275
+ /**
276
+ * Resolves to the generated mutable class for a struct.
277
+ * The type parameter is the generated frozen class.
278
+ */
279
+ export type MutableForm<Frozen> = //
280
+ Frozen extends _FrozenBase
281
+ ? ReturnType<Frozen["toMutable"]> & Freezable<Frozen>
282
+ : Freezable<Frozen>;
283
+
284
+ /** Result of encoding a struct using binary encoding format. */
285
+ export interface BinaryForm {
286
+ /** Length (in bytes) of the binary form. */
287
+ readonly byteLength: number;
288
+ /** Copies the contents of the binary form into the given array buffer. */
289
+ copyTo(target: ArrayBuffer, offset?: number): void;
290
+ /** Copies the contents of this byte string into a new array buffer. */
291
+ toBuffer(): ArrayBuffer;
292
+ }
293
+
294
+ /**
295
+ * When using the JSON serialization format, you can chose between two flavors.
296
+ *
297
+ * The dense flavor is the default flavor and is preferred in most cases.
298
+ * Structs are converted to JSON arrays, where the number of each field
299
+ * corresponds to the index of the value in the array. This results in a more
300
+ * compact representation than when using JSON objects, and this makes
301
+ * serialization and deserialization a bit faster. Because field names are left
302
+ * out of the JSON, it is a representation which allows persistence: you can
303
+ * safely rename a field in a '.skir' file without breaking backwards
304
+ * compatibility.
305
+ * One con of this representation is that it is harder to tell, just by looking
306
+ * at the JSON, what field of the struct each value in the array corresponds to.
307
+ *
308
+ * When using the readable flavor, structs are converted to JSON objects. The
309
+ * name of each field in the '.skir' file is used as-is in the JSON. This
310
+ * results in a representation which is much more readable by humans, but also
311
+ * not suited for persistence: when you rename a field in a '.skir' file, you
312
+ * will no lonnger be able to deserialize old JSONs.
313
+ *
314
+ * @example
315
+ * const jane = Person.create({firstName: "Jane", lastName: "Doe"});
316
+ *
317
+ * console.log(Person.serializer.toJson(jane, "dense"));
318
+ * // Output: ["Jane","Doe"]
319
+ *
320
+ * console.log(Person.serializer.toJson(jane));
321
+ * // Output: ["Jane","Doe"]
322
+ *
323
+ * console.log(Person.serializer.toJson(jane, "readable"));
324
+ * // Output: {
325
+ * // "firstName": "Jane",
326
+ * // "lastName": "Doe"
327
+ * // }
328
+ */
329
+ export type JsonFlavor = "dense" | "readable";
330
+
331
+ /**
332
+ * Serializes and deserializes instances of `T`. Supports two serialization
333
+ * formats: JSON and binary.
334
+ *
335
+ * All deserialization methods return a deeply-immutable `T`. If `T` is the
336
+ * generated frozen class for a struct, all serialization methods accept either
337
+ * a `T` or a `T.Mutable`.
338
+ *
339
+ * Do NOT create your own `Serializer` implementation. Only use implementations
340
+ * provided by Skir.
341
+ *
342
+ * @example
343
+ * let jane = Person.create({firstName: "Jane", lastName: "Doe"});
344
+ * const json = Person.serializer.toJson(jane);
345
+ * jane = Person.serializer.fromJson(json);
346
+ * expect(jane.firstName).toBe("Jane");
347
+ */
348
+ export interface Serializer<T> {
349
+ /**
350
+ * Converts back the given stringified JSON to `T`.
351
+ * Works with both [flavors]{@link JsonFlavor} of JSON.
352
+ *
353
+ * Pass in "keep-unrecognized-values" if and only if the input JSON comes
354
+ * from a trusted program which might have been built from more recent
355
+ * source files.
356
+ */
357
+ fromJsonCode(code: string, keep?: "keep-unrecognized-values"): T;
358
+ /**
359
+ * Converts back the given JSON to `T`.
360
+ * Works with both [flavors]{@link JsonFlavor} of JSON.
361
+ *
362
+ * Pass in "keep-unrecognized-values" if and only if the input JSON comes
363
+ * from a trusted program which might have been built from more recent
364
+ * source files.
365
+ */
366
+ fromJson(json: Json, keep?: "keep-unrecognized-values"): T;
367
+ /**
368
+ * Converts back the given binary form to `T`.
369
+ *
370
+ * Pass in "keep-unrecognized-values" if and only if the input JSON comes
371
+ * from a trusted program which might have been built from more recent
372
+ * source files.
373
+ */
374
+ fromBytes(bytes: ArrayBuffer, keep?: "keep-unrecognized-values"): T;
375
+ /**
376
+ * Converts the given `T` to JSON and returns the stringified JSON. Same as
377
+ * calling `JSON.stringify()` on the result of `toJson()`.
378
+ *
379
+ * @param flavor dense or readable, defaults to dense
380
+ * @see JsonFlavor
381
+ */
382
+ toJsonCode(input: T | MutableForm<T>, flavor?: JsonFlavor): string;
383
+ /**
384
+ * Converts the given `T` to JSON. If you only need the stringified JSON, call
385
+ * `toJsonCode()` instead.
386
+ *
387
+ * @param flavor dense or readable, defaults to dense
388
+ * @see JsonFlavor
389
+ */
390
+ toJson(input: T | MutableForm<T>, flavor?: JsonFlavor): Json;
391
+ /** Converts the given `T` to binary format. */
392
+ toBytes(input: T | MutableForm<T>): BinaryForm;
393
+ /** An object describing the type `T`. Enables reflective programming. */
394
+ typeDescriptor: TypeDescriptorSpecialization<T>;
395
+ }
396
+
397
+ /**
398
+ * Returns a serializer of instances of the given Skir primitive type.
399
+ *
400
+ * @example
401
+ * expect(
402
+ * primitiveSerializer("string").toJsonCode("foo")
403
+ * ).toBe(
404
+ * '"foo"'
405
+ * );
406
+ */
407
+ export function primitiveSerializer<P extends keyof PrimitiveTypes>(
408
+ primitiveType: P,
409
+ ): Serializer<PrimitiveTypes[P]> {
410
+ return primitiveSerializers[primitiveType];
411
+ }
412
+
413
+ /**
414
+ * Returns a serializer of arrays of `Item`s.
415
+ *
416
+ * @example
417
+ * expect(
418
+ * arraySerializer(User.serializer).toJsonCode([JANE, JOE])
419
+ * ).toBe(
420
+ * '[["jane"],["joe"]]'
421
+ * );
422
+ */
423
+ export function arraySerializer<Item>(
424
+ item: Serializer<Item>,
425
+ keyChain?: string,
426
+ ): Serializer<ReadonlyArray<Item>> {
427
+ if (
428
+ keyChain !== undefined &&
429
+ !/^[a-z_][a-z0-9_]*(\.[a-z_][a-z0-9_]*)*$/.test(keyChain)
430
+ ) {
431
+ throw new Error(`Invalid keyChain "${keyChain}"`);
432
+ }
433
+ return new ArraySerializerImpl(item as InternalSerializer<Item>, keyChain);
434
+ }
435
+
436
+ /** Returns a serializer of nullable `T`s. */
437
+ export function optionalSerializer<T>(
438
+ other: Serializer<T>,
439
+ ): Serializer<T | null> {
440
+ return other instanceof OptionalSerializerImpl
441
+ ? other
442
+ : new OptionalSerializerImpl(other as InternalSerializer<T>);
443
+ }
444
+
445
+ /**
446
+ * Describes the type `T`, where `T` is the TypeScript equivalent of a Skir
447
+ * type. Enables reflective programming.
448
+ *
449
+ * Every `TypeDescriptor` instance has a `kind` field which can take one of
450
+ * these 5 values: `"primitive"`, `"optional"`, `"array"`, `"struct"`, `"enum"`.
451
+ */
452
+ export type TypeDescriptor<T = unknown> =
453
+ | OptionalDescriptor<T>
454
+ | ArrayDescriptor<T>
455
+ | StructDescriptor<T>
456
+ | EnumDescriptor<T>
457
+ | PrimitiveDescriptor;
458
+
459
+ /** Specialization of `TypeDescriptor<T>` when `T` is known. */
460
+ export type TypeDescriptorSpecialization<T> = //
461
+ [T] extends [_FrozenBase]
462
+ ? StructDescriptor<T>
463
+ : [T] extends [_EnumBase]
464
+ ? EnumDescriptor<T>
465
+ : TypeDescriptor<T>;
466
+
467
+ interface TypeDescriptorBase {
468
+ /** Returns the JSON representation of this `TypeDescriptor`. */
469
+ asJson(): Json;
470
+
471
+ /**
472
+ * Returns the JSON code representation of this `TypeDescriptor`.
473
+ * Same as calling `JSON.stringify()` on the result of `asJson()`.
474
+ */
475
+ asJsonCode(): string;
476
+
477
+ /**
478
+ * Converts from one serialized form to another.
479
+ *
480
+ * @example
481
+ * const denseJson = User.serializer.toJson(user, "dense");
482
+ * expect(
483
+ * User.serializer.typeDescriptor.transform(denseJson, "readable")
484
+ * ).toMatch(
485
+ * User.serializer.toJson(user, "readable")
486
+ * );
487
+ */
488
+ transform(json_or_bytes: Json | ArrayBuffer, out: JsonFlavor): Json;
489
+ transform(json: Json, out: "bytes"): ArrayBuffer;
490
+ transform(
491
+ json_or_bytes: Json | ArrayBuffer,
492
+ out: JsonFlavor | "bytes",
493
+ ): Json | ArrayBuffer;
494
+ }
495
+
496
+ /** Describes a primitive Skir type. */
497
+ export interface PrimitiveDescriptor extends TypeDescriptorBase {
498
+ kind: "primitive";
499
+ primitive: keyof PrimitiveTypes;
500
+ }
501
+
502
+ /**
503
+ * An interface mapping a primitive Skir type to the corresponding TypeScript
504
+ * type.
505
+ */
506
+ export interface PrimitiveTypes {
507
+ bool: boolean;
508
+ int32: number;
509
+ int64: bigint;
510
+ hash64: bigint;
511
+ float32: number;
512
+ float64: number;
513
+ timestamp: Timestamp;
514
+ string: string;
515
+ bytes: ByteString;
516
+ }
517
+
518
+ /**
519
+ * Describes an optional type. In a '.skir' file, an optional type is
520
+ * represented with a question mark at the end of another type.
521
+ */
522
+ export interface OptionalDescriptor<T> extends TypeDescriptorBase {
523
+ readonly kind: "optional";
524
+ /** Describes the other (non-optional) type. */
525
+ readonly otherType: TypeDescriptor<NonNullable<T>>;
526
+ }
527
+
528
+ /** Describes an array type. */
529
+ export interface ArrayDescriptor<T> extends TypeDescriptorBase {
530
+ readonly kind: "array";
531
+ /** Describes the type of the array items. */
532
+ readonly itemType: TypeDescriptor<
533
+ T extends ReadonlyArray<infer Item> ? Item : unknown
534
+ >;
535
+ readonly keyChain?: string;
536
+ }
537
+
538
+ /**
539
+ * Describes a Skir struct.
540
+ * The type parameter `T` refers to the generated frozen class for the struct.
541
+ */
542
+ export interface StructDescriptor<T = unknown> extends TypeDescriptorBase {
543
+ readonly kind: "struct";
544
+ /** Name of the struct as specified in the '.skir' file. */
545
+ readonly name: string;
546
+ /**
547
+ * A string containing all the names in the hierarchic sequence above and
548
+ * including the struct. For example: "Foo.Bar" if "Bar" is nested within a
549
+ * type called "Foo", or simply "Bar" if "Bar" is defined at the top-level of
550
+ * the module.
551
+ */
552
+ readonly qualifiedName: string;
553
+ /**
554
+ * Path to the module where the struct is defined, relative to the root of the
555
+ * project.
556
+ */
557
+ readonly modulePath: string;
558
+ /**
559
+ * If the struct is nested within another type, the descriptor for that type.
560
+ * Undefined if the struct is defined at the top-level of the module.
561
+ */
562
+ readonly parentType: StructDescriptor | EnumDescriptor | undefined;
563
+ /** The fields of the struct in the order they appear in the '.skir' file. */
564
+ readonly fields: ReadonlyArray<StructField<T>>;
565
+ /** The field numbers marked as removed. */
566
+ readonly removedNumbers: ReadonlySet<number>;
567
+
568
+ /**
569
+ * Looks up a field. The key can be one of: the field name (e.g. "user_id");
570
+ * the name of the property in the generated class (e.g. "userId"), the field
571
+ * number.
572
+ *
573
+ * The return type is `StructField<T> | undefined` unless the key is known at
574
+ * compile-time to be the name of the property in the generated class, in
575
+ * which case it is `StructField<T>`.
576
+ */
577
+ getField<K extends string | number>(key: K): StructFieldResult<T, K>;
578
+
579
+ /**
580
+ * Returns a new instance of the generated mutable class for a struct.
581
+ * Performs a shallow copy of `initializer` if `initializer` is specified.
582
+ */
583
+ newMutable(initializer?: T | MutableForm<T>): MutableForm<T>;
584
+ }
585
+
586
+ /** Field of a Skir struct. */
587
+ export interface StructField<Struct = unknown, Value = unknown> {
588
+ /** Field name as specified in the '.skir' file, e.g. "user_id". */
589
+ readonly name: string;
590
+ /** Name of the property in the generated class, e.g. "userId". */
591
+ readonly property: string;
592
+ /** Field number. */
593
+ readonly number: number;
594
+ /** Describes the field type. */
595
+ readonly type: TypeDescriptor<Value>;
596
+ /**
597
+ * Documentation for this struct field, specified as doc comments in the
598
+ * '.skir' file. May be empty.
599
+ */
600
+ doc: string;
601
+
602
+ /** Extracts the value of the field from the given struct. */
603
+ get(struct: Struct | MutableForm<Struct>): Value;
604
+ /** Assigns the given value to the field of the given struct. */
605
+ set(struct: MutableForm<Struct>, value: Value): void;
606
+ }
607
+
608
+ /**
609
+ * Return type of the `StructDescriptor.getField` method. If the argument is
610
+ * known at compile-time to be the name of a property of the generated class,
611
+ * resolves to `StructField<Struct>`. Otherwise, resolves to
612
+ * `StructField<Struct> | undefined`.
613
+ *
614
+ * @example <caption>The field is kown at compile-time</caption>
615
+ * const fieldNumber: number =
616
+ * User.serializer.typeDescriptor.getField("userId").number;
617
+ *
618
+ * @example <caption>The field is not kown at compile-time</caption>
619
+ * const fieldNumber: number | undefined =
620
+ * User.serializer.typeDescriptor.getField(variable)?.number;
621
+ */
622
+ export type StructFieldResult<Struct, Key extends string | number> =
623
+ | StructField<Struct>
624
+ | (Struct extends _FrozenBase
625
+ ? Key extends keyof NonNullable<Struct[typeof _INITIALIZER]>
626
+ ? never
627
+ : undefined
628
+ : undefined);
629
+
630
+ /** Describes a Skir enum. */
631
+ export interface EnumDescriptor<T = unknown> extends TypeDescriptorBase {
632
+ readonly kind: "enum";
633
+ /** Name of the enum as specified in the '.skir' file. */
634
+ readonly name: string;
635
+ /**
636
+ * A string containing all the names in the hierarchic sequence above and
637
+ * including the enum. For example: "Foo.Bar" if "Bar" is nested within a type
638
+ * called "Foo", or simply "Bar" if "Bar" is defined at the top-level of the
639
+ * module.
640
+ */
641
+ readonly qualifiedName: string;
642
+ /**
643
+ * Path to the module where the enum is defined, relative to the root of the
644
+ * project.
645
+ */
646
+ readonly modulePath: string;
647
+ /**
648
+ * If the enum is nested within another type, the descriptor for that type.
649
+ * Undefined if the struct is defined at the top-level of the module.
650
+ */
651
+ readonly parentType: StructDescriptor | EnumDescriptor | undefined;
652
+ /**
653
+ * Includes the UNKNOWN variant, followed by the other variants in the order
654
+ * they appear in the '.skir' file.
655
+ */
656
+ readonly variants: ReadonlyArray<EnumVariant<T>>;
657
+ /** The variant numbers marked as removed. */
658
+ readonly removedNumbers: ReadonlySet<number>;
659
+
660
+ /**
661
+ * Looks up a variant. The key can be one of the variant name or the variant
662
+ * number.
663
+ *
664
+ * The return type is `EnumVariant<T> | undefined` unless the key is known at
665
+ * compile-time to be a variant name of the enum, in which case it is
666
+ * `EnumVariant<T>`.
667
+ */
668
+ getVariant<K extends string | number>(key: K): EnumVariantResult<T, K>;
669
+ }
670
+
671
+ /**
672
+ * Variant of a Skir enum. Variants which don't hold any value are called
673
+ * constant variants. Their name is always in UPPER_CASE. Variants which hold
674
+ * value of a given type are called wrapper variants, and their name is always
675
+ * in lower_case.
676
+ */
677
+ export type EnumVariant<Enum = unknown> =
678
+ | EnumConstantVariant<Enum>
679
+ | EnumWrapperVariant<Enum, unknown>;
680
+
681
+ /** Constant variant of a Skir enum. */
682
+ export interface EnumConstantVariant<Enum = unknown> {
683
+ /**
684
+ * Variant name as specified in the '.skir' file, e.g. "MONDAY".
685
+ * Always in UPPER_CASE format.
686
+ */
687
+ readonly name: string;
688
+ /** Variant number. */
689
+ readonly number: number;
690
+ /** The instance of the generated class which corresponds to this variant. */
691
+ readonly constant: Enum;
692
+ /** Always undefined, unlike the `type` field of `EnumWrapperVariant`. */
693
+ readonly type?: undefined;
694
+ /**
695
+ * Documentation for this enum variant, specified as doc comments in the
696
+ * '.skir' file. May be empty.
697
+ */
698
+ readonly doc: string;
699
+ }
700
+
701
+ /** Wrapper variant of a Skir enum. */
702
+ export interface EnumWrapperVariant<Enum = unknown, Value = unknown> {
703
+ /**
704
+ * Variant name as specified in the '.skir' file, e.g. "v4".
705
+ * Always in lower_case format.
706
+ */
707
+ readonly name: string;
708
+ /** Variant number. */
709
+ readonly number: number;
710
+ /** Describes the type of the value held by the variant. */
711
+ readonly type: TypeDescriptor<Value>;
712
+ /** Always undefined, unlike the `type` field of `EnumConstantVariant`. */
713
+ readonly constant?: undefined;
714
+ /**
715
+ * Documentation for this enum variant, specified as doc comments in the
716
+ * '.skir' file. May be empty.
717
+ */
718
+ readonly doc: string;
719
+
720
+ /**
721
+ * Extracts the value held by the given enum instance if it matches this
722
+ * enum variant. Returns undefined otherwise.
723
+ */
724
+ get(e: Enum): Value | unknown;
725
+ /**
726
+ * Returns a new enum instance matching this enum variant and holding the
727
+ * given value.
728
+ */
729
+ wrap(value: Value): Enum;
730
+ }
731
+
732
+ /**
733
+ * Return type of the `EnumDescriptor.getVariant` method. If the argument is
734
+ * known at compile-time to be the name of variant, resolves to
735
+ * `EnumVariant<Enum>`. Otherwise, resolves to
736
+ * `EnumVariant<Struct> | undefined`.
737
+ *
738
+ * @example <caption>The variant is known at compile-time</caption>
739
+ * const variantNumber: number =
740
+ * Weekday.serializer.typeDescriptor.getVariant("MONDAY").number;
741
+ *
742
+ * @example <caption>The variant is not known at compile-time</caption>
743
+ * const variantNumber: number | undefined =
744
+ * Weekday.serializer.typeDescriptor.getVariant(variable)?.number;
745
+ */
746
+ export type EnumVariantResult<Enum, Key extends string | number> =
747
+ | EnumVariant<Enum>
748
+ | (Enum extends _EnumBase
749
+ ? Key extends Enum["union"]["kind"]
750
+ ? never
751
+ : undefined
752
+ : undefined);
753
+
754
+ /**
755
+ * Identifies a procedure (the "P" in "RPC") on both the client side and the
756
+ * server side.
757
+ */
758
+ export interface Method<Request, Response> {
759
+ /** Name of the procedure as specified in the '.skir' file. */
760
+ name: string;
761
+ /**
762
+ * A number which uniquely identifies this procedure.
763
+ * When it is not specified in the '.skir' file, it is obtained by hashing the
764
+ * procedure name.
765
+ */
766
+ number: number;
767
+ /** Serializer of request objects. */
768
+ requestSerializer: Serializer<Request>;
769
+ /** Serializer of response objects. */
770
+ responseSerializer: Serializer<Response>;
771
+ /**
772
+ * Documentation for this procedure, specified as doc comments in the '.skir'
773
+ * file. May be empty.
774
+ */
775
+ doc: string;
776
+ }
777
+
778
+ /**
779
+ * Interface implemented by both the frozen and mutable classes generated for a
780
+ * struct. `T` is always the generated frozen class.
781
+ */
782
+ export interface Freezable<T> {
783
+ /**
784
+ * Returns a deeply-immutable object, either by making a copy of `this` if
785
+ * `this` is mutable, or by returning `this` as-is if `this` is already
786
+ * immutable.
787
+ */
788
+ toFrozen(): T;
789
+ }
790
+
791
+ // =============================================================================
792
+ // Implementation of serializers and type descriptors
793
+ // =============================================================================
794
+
795
+ /** JSON representation of a `TypeDescriptor`. */
796
+ type TypeDefinition = {
797
+ type: TypeSignature;
798
+ records: readonly RecordDefinition[];
799
+ };
800
+
801
+ /** A type in the JSON representation of a `TypeDescriptor`. */
802
+ type TypeSignature =
803
+ | {
804
+ kind: "optional";
805
+ value: TypeSignature;
806
+ }
807
+ | {
808
+ kind: "array";
809
+ value: {
810
+ item: TypeSignature;
811
+ key_extractor?: string;
812
+ };
813
+ }
814
+ | {
815
+ kind: "record";
816
+ value: string;
817
+ }
818
+ | {
819
+ kind: "primitive";
820
+ value: keyof PrimitiveTypes;
821
+ };
822
+
823
+ /**
824
+ * Definition of a struct field in the JSON representation of a
825
+ * `TypeDescriptor`.
826
+ */
827
+ type FieldDefinition = {
828
+ name: string;
829
+ type: TypeSignature;
830
+ number: number;
831
+ doc?: string;
832
+ };
833
+
834
+ /**
835
+ * Definition of an enum variant in the JSON representation of a
836
+ * `TypeDescriptor`.
837
+ */
838
+ type VariantDefinition = {
839
+ name: string;
840
+ type?: TypeSignature;
841
+ number: number;
842
+ doc?: string;
843
+ };
844
+
845
+ /** Definition of a struct in the JSON representation of a `TypeDescriptor`. */
846
+ type StructDefinition = {
847
+ kind: "struct";
848
+ id: string;
849
+ doc?: string;
850
+ fields: readonly FieldDefinition[];
851
+ removed_numbers?: readonly number[];
852
+ };
853
+
854
+ /** Definition of an enum in the JSON representation of a `TypeDescriptor`. */
855
+ type EnumDefinition = {
856
+ kind: "enum";
857
+ id: string;
858
+ doc?: string;
859
+ variants: readonly VariantDefinition[];
860
+ removed_numbers?: readonly number[];
861
+ };
862
+
863
+ type RecordDefinition = StructDefinition | EnumDefinition;
864
+
865
+ interface InternalSerializer<T = unknown> extends Serializer<T> {
866
+ readonly defaultValue: T;
867
+ isDefault(input: T): boolean;
868
+ decode(stream: InputStream): T;
869
+ encode(input: T, stream: OutputStream): void;
870
+ readonly typeSignature: TypeSignature;
871
+ addRecordDefinitionsTo(out: { [k: string]: RecordDefinition }): void;
872
+ }
873
+
874
+ /** Parameter of the {@link InternalSerializer.decode} method. */
875
+ class InputStream {
876
+ constructor(
877
+ readonly buffer: ArrayBuffer,
878
+ keep?: "keep-unrecognized-values",
879
+ ) {
880
+ this.dataView = new DataView(buffer);
881
+ this.keepUnrecognizedValues = !!keep;
882
+ }
883
+
884
+ readonly dataView: DataView;
885
+ readonly keepUnrecognizedValues: boolean;
886
+ offset = 0;
887
+
888
+ readUint8(): number {
889
+ return this.dataView.getUint8(this.offset++);
890
+ }
891
+ }
892
+
893
+ type DecodeNumberFn = (stream: InputStream) => number | bigint;
894
+
895
+ // For wires [232, 241]
896
+ const DECODE_NUMBER_FNS: readonly DecodeNumberFn[] = [
897
+ (s: InputStream): number => s.dataView.getUint16((s.offset += 2) - 2, true),
898
+ (s: InputStream): number => s.dataView.getUint32((s.offset += 4) - 4, true),
899
+ (s: InputStream): bigint =>
900
+ s.dataView.getBigUint64((s.offset += 8) - 8, true),
901
+ (stream: InputStream): number => stream.readUint8() - 256,
902
+ (s: InputStream): number =>
903
+ s.dataView.getUint16((s.offset += 2) - 2, true) - 65536,
904
+ (s: InputStream): number => s.dataView.getInt32((s.offset += 4) - 4, true),
905
+ (s: InputStream): bigint => s.dataView.getBigInt64((s.offset += 8) - 8, true),
906
+ (s: InputStream): bigint => s.dataView.getBigInt64((s.offset += 8) - 8, true),
907
+ (s: InputStream): number => s.dataView.getFloat32((s.offset += 4) - 4, true),
908
+ (s: InputStream): number => s.dataView.getFloat64((s.offset += 8) - 8, true),
909
+ ];
910
+
911
+ function decodeNumber(stream: InputStream): number | bigint {
912
+ const wire = stream.readUint8();
913
+ return wire < 232 ? wire : DECODE_NUMBER_FNS[wire - 232]!(stream);
914
+ }
915
+
916
+ function decodeBigInt(stream: InputStream): bigint {
917
+ const number = decodeNumber(stream);
918
+ return typeof number === "bigint" ? number : BigInt(Math.round(number));
919
+ }
920
+
921
+ /** Parameter of the {@link InternalSerializer.encode} method. */
922
+ class OutputStream implements BinaryForm {
923
+ writeUint8(value: number): void {
924
+ const dataView = this.reserve(1);
925
+ dataView.setUint8(++this.offset - 1, value);
926
+ }
927
+
928
+ writeUint16(value: number): void {
929
+ const dataView = this.reserve(2);
930
+ dataView.setUint16((this.offset += 2) - 2, value, true);
931
+ }
932
+
933
+ writeUint32(value: number): void {
934
+ const dataView = this.reserve(4);
935
+ dataView.setUint32((this.offset += 4) - 4, value, true);
936
+ }
937
+
938
+ writeInt32(value: number): void {
939
+ const dataView = this.reserve(4);
940
+ dataView.setInt32((this.offset += 4) - 4, value, true);
941
+ }
942
+
943
+ writeHash64(value: bigint): void {
944
+ const dataView = this.reserve(8);
945
+ dataView.setBigUint64((this.offset += 8) - 8, value, true);
946
+ }
947
+
948
+ writeInt64(value: bigint): void {
949
+ const dataView = this.reserve(8);
950
+ dataView.setBigInt64((this.offset += 8) - 8, value, true);
951
+ }
952
+
953
+ writeFloat32(value: number): void {
954
+ const dataView = this.reserve(4);
955
+ dataView.setFloat32((this.offset += 4) - 4, value, true);
956
+ }
957
+
958
+ writeFloat64(value: number): void {
959
+ const dataView = this.reserve(8);
960
+ dataView.setFloat64((this.offset += 8) - 8, value, true);
961
+ }
962
+
963
+ /**
964
+ * Encodes the given string to UTF-8 and writes the bytes to this stream.
965
+ * Returns the number of bytes written.
966
+ */
967
+ putUtf8String(string: string): number {
968
+ // We do at most 3 writes:
969
+ // - First, fill the current buffer as much as possible
970
+ // - If there is not enough room, allocate a new buffer of N bytes, where
971
+ // N is twice the number of remaining UTF-16 characters in the string,
972
+ // and write to it. This new buffer is very likely to have enough
973
+ // room.
974
+ // - If there was not enough room, try again one last time.
975
+ //
976
+ // See https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder
977
+ let dataView: DataView = this.dataView;
978
+ let result = 0;
979
+ while (string) {
980
+ const encodeResult = textEncoder.encodeInto(
981
+ string,
982
+ new Uint8Array(dataView.buffer, this.offset),
983
+ );
984
+ this.offset += encodeResult.written;
985
+ result += encodeResult.written;
986
+ string = string.substring(encodeResult.read);
987
+ if (string) {
988
+ dataView = this.reserve(string.length * 2);
989
+ }
990
+ }
991
+ return result;
992
+ }
993
+
994
+ putBytes(bytes: ByteString): void {
995
+ // We do at most 2 writes:
996
+ // - First, fill the current buffer as much as possible
997
+ // - If there is not enough room, allocate a new buffer of N bytes, where
998
+ // N is at least the number of bytes left in the byte string.
999
+ const { buffer } = this;
1000
+ const bytesLeftInCurrentBuffer = buffer.byteLength - this.offset;
1001
+ const head = ByteString.sliceOf(bytes, 0, bytesLeftInCurrentBuffer);
1002
+ head.copyTo(buffer, this.offset);
1003
+ this.offset += head.byteLength;
1004
+ const remainingBytes = bytes.byteLength - head.byteLength;
1005
+ if (remainingBytes <= 0) {
1006
+ // Everything was written.
1007
+ return;
1008
+ }
1009
+ const tail = ByteString.sliceOf(bytes, remainingBytes);
1010
+ this.reserve(remainingBytes);
1011
+ tail.copyTo(buffer, this.offset);
1012
+ this.offset += remainingBytes;
1013
+ }
1014
+
1015
+ finalize(): BinaryForm {
1016
+ this.flush();
1017
+ Object.freeze(this.pieces);
1018
+ Object.freeze(this);
1019
+ return this;
1020
+ }
1021
+
1022
+ copyTo(target: ArrayBuffer, offset = 0): void {
1023
+ const targetArea = new Uint8Array(target);
1024
+ for (const piece of this.pieces) {
1025
+ targetArea.set(piece, offset);
1026
+ offset += piece.length;
1027
+ }
1028
+ }
1029
+
1030
+ toBuffer(): ArrayBuffer {
1031
+ const result = new ArrayBuffer(this.byteLength);
1032
+ this.copyTo(result);
1033
+ return result;
1034
+ }
1035
+
1036
+ /** Returns a data view with enough capacity for `bytes` more bytes. */
1037
+ private reserve(bytes: number): DataView {
1038
+ if (this.offset < this.buffer.byteLength - bytes) {
1039
+ // Enough room in the current data view.
1040
+ return this.dataView;
1041
+ }
1042
+ this.flush();
1043
+ const lengthInBytes = Math.max(this.byteLength, bytes);
1044
+ this.offset = 0;
1045
+ this.buffer = new ArrayBuffer(lengthInBytes);
1046
+ return (this.dataView = new DataView(this.buffer));
1047
+ }
1048
+
1049
+ /** Adds the current buffer to `pieces`. Updates `byteLength` accordingly. */
1050
+ private flush(): void {
1051
+ const { offset } = this;
1052
+ this.pieces.push(new Uint8Array(this.dataView.buffer, 0, offset));
1053
+ this.byteLength += offset;
1054
+ }
1055
+
1056
+ buffer = new ArrayBuffer(128);
1057
+ dataView = new DataView(this.buffer);
1058
+ offset = 0;
1059
+ // The final binary form is the result of concatenating these arrays.
1060
+ // The length of each array is approximately twice the length of the previous
1061
+ // array.
1062
+ private readonly pieces: Uint8Array[] = [];
1063
+ // Updated each time `flush()` is called.
1064
+ byteLength = 0;
1065
+ }
1066
+
1067
+ function encodeUint32(length: number, stream: OutputStream): void {
1068
+ if (length < 232) {
1069
+ stream.writeUint8(length);
1070
+ } else if (length < 65536) {
1071
+ stream.writeUint8(232);
1072
+ stream.writeUint16(length);
1073
+ } else if (length < 4294967296) {
1074
+ stream.writeUint8(233);
1075
+ stream.writeUint32(length);
1076
+ } else {
1077
+ throw new Error(`max length exceeded: ${length}`);
1078
+ }
1079
+ }
1080
+
1081
+ abstract class AbstractSerializer<T> implements InternalSerializer<T> {
1082
+ fromJsonCode(code: string, keep?: "keep-unrecognized-values"): T {
1083
+ return this.fromJson(JSON.parse(code), keep);
1084
+ }
1085
+
1086
+ fromBytes(bytes: ArrayBuffer, keep?: "keep-unrecognized-values"): T {
1087
+ const inputStream = new InputStream(bytes, keep);
1088
+ inputStream.offset = 4; // Skip the "skir" header.
1089
+ return this.decode(inputStream);
1090
+ }
1091
+
1092
+ toJsonCode(input: T, flavor?: JsonFlavor): string {
1093
+ const indent = flavor === "readable" ? " " : undefined;
1094
+ return JSON.stringify(this.toJson(input, flavor), undefined, indent);
1095
+ }
1096
+
1097
+ toBytes(input: T): BinaryForm {
1098
+ const stream = new OutputStream();
1099
+ stream.putUtf8String("skir");
1100
+ this.encode(input, stream);
1101
+ return stream.finalize();
1102
+ }
1103
+
1104
+ // Default implementation; this behavior is not correct for all subclasses.
1105
+ isDefault(input: T): boolean {
1106
+ return !input;
1107
+ }
1108
+
1109
+ get typeDescriptor(): TypeDescriptorSpecialization<T> {
1110
+ return this as unknown as TypeDescriptorSpecialization<T>;
1111
+ }
1112
+
1113
+ abstract readonly defaultValue: T;
1114
+ abstract fromJson(json: Json, keep?: "keep-unrecognized-values"): T;
1115
+ abstract toJson(input: T, flavor?: JsonFlavor): Json;
1116
+ abstract decode(stream: InputStream): T;
1117
+ abstract encode(input: T, stream: OutputStream): void;
1118
+
1119
+ abstract readonly typeSignature: TypeSignature;
1120
+ abstract addRecordDefinitionsTo(out: { [k: string]: RecordDefinition }): void;
1121
+
1122
+ asJson(): Json {
1123
+ const recordDefinitions: { [k: string]: RecordDefinition } = {};
1124
+ this.addRecordDefinitionsTo(recordDefinitions);
1125
+ const result: TypeDefinition = {
1126
+ type: this.typeSignature,
1127
+ records: Object.values(recordDefinitions),
1128
+ };
1129
+ return result;
1130
+ }
1131
+
1132
+ asJsonCode(): string {
1133
+ return JSON.stringify(this.asJson(), undefined, " ");
1134
+ }
1135
+
1136
+ transform(json_or_bytes: Json | ArrayBuffer, out: JsonFlavor): Json;
1137
+ transform(json: Json, out: "bytes"): ArrayBuffer;
1138
+ transform(
1139
+ json_or_bytes: Json | ArrayBuffer,
1140
+ out: JsonFlavor | "bytes",
1141
+ ): Json | ArrayBuffer {
1142
+ const decoded: T =
1143
+ json_or_bytes instanceof ArrayBuffer
1144
+ ? this.fromBytes(json_or_bytes)
1145
+ : this.fromJson(json_or_bytes);
1146
+ return out === "bytes"
1147
+ ? this.toBytes(decoded).toBuffer()
1148
+ : this.toJson(decoded, out);
1149
+ }
1150
+ }
1151
+
1152
+ // The UNKNOWN variant is common to all enums.
1153
+ const UNKNOWN_VARIANT_DEFINITION: VariantDefinition = {
1154
+ name: "UNKNOWN",
1155
+ number: 0,
1156
+ };
1157
+
1158
+ /**
1159
+ * Returns a `TypeDescriptor` from its JSON representation as returned by
1160
+ * `asJson()`.
1161
+ */
1162
+ export function parseTypeDescriptorFromJson(json: Json): TypeDescriptor {
1163
+ const typeDefinition = json as TypeDefinition;
1164
+
1165
+ type RecordBundle = {
1166
+ readonly definition: RecordDefinition;
1167
+ readonly serializer: StructSerializerImpl<Json> | EnumSerializerImpl<Json>;
1168
+ };
1169
+ const recordBundles: { [k: string]: RecordBundle } = {};
1170
+
1171
+ // First loop: create the serializer for each record.
1172
+ // It's not yet initialized.
1173
+ for (const record of typeDefinition.records) {
1174
+ let serializer: StructSerializerImpl<Json> | EnumSerializerImpl<Json>;
1175
+ switch (record.kind) {
1176
+ case "struct":
1177
+ serializer = new StructSerializerImpl<Json>(
1178
+ {},
1179
+ (initializer: AnyRecord) => Object.freeze({ ...initializer }) as Json,
1180
+ (() => ({})) as NewMutableFn<Json>,
1181
+ );
1182
+ break;
1183
+ case "enum":
1184
+ serializer = new EnumSerializerImpl<Json>((o: unknown) => {
1185
+ if (o instanceof UnrecognizedEnum) {
1186
+ return Object.freeze({ kind: "UNKNOWN" });
1187
+ }
1188
+ if (typeof o === "string") {
1189
+ return Object.freeze({ kind: o }) as Json;
1190
+ } else {
1191
+ const kind = (o as AnyRecord).kind;
1192
+ const value = (o as AnyRecord).value;
1193
+ const ret = { kind, value } as Json;
1194
+ return Object.freeze(ret);
1195
+ }
1196
+ });
1197
+ break;
1198
+ }
1199
+ const recordBundle: RecordBundle = {
1200
+ definition: record,
1201
+ serializer: serializer,
1202
+ };
1203
+ recordBundles[record.id] = recordBundle;
1204
+ }
1205
+
1206
+ function parse(ts: TypeSignature): InternalSerializer {
1207
+ switch (ts.kind) {
1208
+ case "array": {
1209
+ const { item, key_extractor } = ts.value;
1210
+ return new ArraySerializerImpl(parse(item), key_extractor);
1211
+ }
1212
+ case "optional":
1213
+ return new OptionalSerializerImpl(parse(ts.value));
1214
+ case "primitive":
1215
+ return primitiveSerializer(ts.value) as InternalSerializer;
1216
+ case "record": {
1217
+ const recordId = ts.value;
1218
+ return recordBundles[recordId]!.serializer;
1219
+ }
1220
+ }
1221
+ }
1222
+
1223
+ // Second loop: initialize each serializer.
1224
+ const initOps: Array<() => void> = [];
1225
+ for (const recordBundle of Object.values(recordBundles)) {
1226
+ const { definition, serializer } = recordBundle;
1227
+ const { defaultValue } = serializer;
1228
+ const { id, removed_numbers } = definition;
1229
+ const idParts = id.split(":");
1230
+ const module = idParts[0]!;
1231
+ const qualifiedName = idParts[1]!;
1232
+ const nameParts = qualifiedName.split(".");
1233
+ const name = nameParts[nameParts.length - 1]!;
1234
+ const parentId = module + ":" + nameParts.slice(0, -1).join(".");
1235
+ const parentType = recordBundles[parentId]?.serializer;
1236
+ const doc = definition.doc ?? "";
1237
+ switch (definition.kind) {
1238
+ case "struct": {
1239
+ const fields: StructFieldImpl[] = [];
1240
+ for (const f of definition.fields) {
1241
+ const fieldSerializer = parse(f.type!);
1242
+ fields.push(
1243
+ new StructFieldImpl(
1244
+ f.name,
1245
+ f.name,
1246
+ f.number,
1247
+ fieldSerializer,
1248
+ f.doc ?? "",
1249
+ ),
1250
+ );
1251
+ (defaultValue as AnyRecord)[f.name] = fieldSerializer.defaultValue;
1252
+ }
1253
+ const s = serializer as StructSerializerImpl<Json>;
1254
+ initOps.push(() =>
1255
+ s.init(name, module, parentType, doc, fields, removed_numbers ?? []),
1256
+ );
1257
+ break;
1258
+ }
1259
+ case "enum": {
1260
+ const s = serializer as EnumSerializerImpl<Json>;
1261
+ const variants = [UNKNOWN_VARIANT_DEFINITION]
1262
+ .concat(definition.variants)
1263
+ .map((f) =>
1264
+ f.type
1265
+ ? new EnumWrapperVariantImpl<Json>(
1266
+ f.name,
1267
+ f.number,
1268
+ parse(f.type),
1269
+ f.doc ?? "",
1270
+ serializer.createFn,
1271
+ )
1272
+ : ({
1273
+ name: f.name,
1274
+ number: f.number,
1275
+ constant: Object.freeze({ kind: f.name }),
1276
+ doc: f.doc ?? "",
1277
+ } as EnumConstantVariant<Json>),
1278
+ );
1279
+ initOps.push(() =>
1280
+ s.init(
1281
+ name,
1282
+ module,
1283
+ parentType,
1284
+ doc,
1285
+ variants,
1286
+ removed_numbers ?? [],
1287
+ ),
1288
+ );
1289
+ break;
1290
+ }
1291
+ }
1292
+ }
1293
+ // We need to actually initialize the serializers *after* the default values
1294
+ // were constructed, because `init` calls `freezeDeeply` and this might result
1295
+ // in freezing the default of another serializer.
1296
+ initOps.forEach((op) => op());
1297
+
1298
+ return parse(typeDefinition.type).typeDescriptor;
1299
+ }
1300
+
1301
+ /**
1302
+ * Returns a `TypeDescriptor` from its JSON code representation as returned by
1303
+ * `asJsonCode()`.
1304
+ */
1305
+ export function parseTypeDescriptorFromJsonCode(code: string): TypeDescriptor {
1306
+ return parseTypeDescriptorFromJson(JSON.parse(code));
1307
+ }
1308
+
1309
+ abstract class AbstractPrimitiveSerializer<P extends keyof PrimitiveTypes>
1310
+ extends AbstractSerializer<PrimitiveTypes[P]>
1311
+ implements PrimitiveDescriptor
1312
+ {
1313
+ readonly kind = "primitive";
1314
+
1315
+ get typeSignature(): TypeSignature {
1316
+ return {
1317
+ kind: "primitive",
1318
+ value: this.primitive,
1319
+ };
1320
+ }
1321
+
1322
+ addRecordDefinitionsTo(_out: { [k: string]: RecordDefinition }): void {}
1323
+
1324
+ abstract readonly primitive: P;
1325
+ }
1326
+
1327
+ class BoolSerializer extends AbstractPrimitiveSerializer<"bool"> {
1328
+ readonly primitive = "bool";
1329
+ readonly defaultValue = false;
1330
+
1331
+ toJson(input: boolean, flavor?: JsonFlavor): boolean | number {
1332
+ return flavor === "readable" ? !!input : input ? 1 : 0;
1333
+ }
1334
+
1335
+ fromJson(json: Json): boolean {
1336
+ return !!json && json !== "0";
1337
+ }
1338
+
1339
+ encode(input: boolean, stream: OutputStream): void {
1340
+ stream.writeUint8(input ? 1 : 0);
1341
+ }
1342
+
1343
+ decode(stream: InputStream): boolean {
1344
+ return !!decodeNumber(stream);
1345
+ }
1346
+ }
1347
+
1348
+ class Int32Serializer extends AbstractPrimitiveSerializer<"int32"> {
1349
+ readonly primitive = "int32";
1350
+ readonly defaultValue = 0;
1351
+
1352
+ toJson(input: number): number {
1353
+ return input | 0;
1354
+ }
1355
+
1356
+ fromJson(json: Json): number {
1357
+ // `+value` will work if the input JSON value is a string, which is
1358
+ // what the int64 serializer produces.
1359
+ return +(json as number | string) | 0;
1360
+ }
1361
+
1362
+ encode(input: number, stream: OutputStream): void {
1363
+ if (input < 0) {
1364
+ if (input >= -256) {
1365
+ stream.writeUint8(235);
1366
+ stream.writeUint8(input + 256);
1367
+ } else if (input >= -65536) {
1368
+ stream.writeUint8(236);
1369
+ stream.writeUint16(input + 65536);
1370
+ } else {
1371
+ stream.writeUint8(237);
1372
+ stream.writeInt32(input >= -2147483648 ? input : -2147483648);
1373
+ }
1374
+ } else if (input < 232) {
1375
+ stream.writeUint8(input);
1376
+ } else if (input < 65536) {
1377
+ stream.writeUint8(232);
1378
+ stream.writeUint16(input);
1379
+ } else {
1380
+ stream.writeUint8(233);
1381
+ stream.writeUint32(input <= 2147483647 ? input : 2147483647);
1382
+ }
1383
+ }
1384
+
1385
+ decode(stream: InputStream): number {
1386
+ return Number(decodeNumber(stream)) | 0;
1387
+ }
1388
+ }
1389
+
1390
+ const int32_Serializer = new Int32Serializer();
1391
+
1392
+ abstract class FloatSerializer<
1393
+ P extends "float32" | "float64",
1394
+ > extends AbstractPrimitiveSerializer<P> {
1395
+ readonly defaultValue = 0;
1396
+
1397
+ toJson(input: number): number | string {
1398
+ if (Number.isFinite(input)) {
1399
+ return input;
1400
+ } else if (typeof input === "number") {
1401
+ // If the number is NaN or +/- Infinity, return a JSON string.
1402
+ return input.toString();
1403
+ }
1404
+ throw new TypeError();
1405
+ }
1406
+
1407
+ fromJson(json: Json): number {
1408
+ return +(json as number | string);
1409
+ }
1410
+
1411
+ decode(stream: InputStream): number {
1412
+ return Number(decodeNumber(stream));
1413
+ }
1414
+
1415
+ override isDefault(input: number): boolean {
1416
+ // Needs to work for NaN.
1417
+ return input === 0;
1418
+ }
1419
+ }
1420
+
1421
+ class Float32Serializer extends FloatSerializer<"float32"> {
1422
+ readonly primitive = "float32";
1423
+
1424
+ encode(input: number, stream: OutputStream): void {
1425
+ if (input === 0) {
1426
+ stream.writeUint8(0);
1427
+ } else {
1428
+ stream.writeUint8(240);
1429
+ stream.writeFloat32(input);
1430
+ }
1431
+ }
1432
+ }
1433
+
1434
+ class Float64Serializer extends FloatSerializer<"float64"> {
1435
+ readonly primitive = "float64";
1436
+
1437
+ encode(input: number, stream: OutputStream): void {
1438
+ if (input === 0) {
1439
+ stream.writeUint8(0);
1440
+ } else {
1441
+ stream.writeUint8(241);
1442
+ stream.writeFloat64(input);
1443
+ }
1444
+ }
1445
+ }
1446
+
1447
+ abstract class AbstractBigIntSerializer<
1448
+ P extends "int64" | "hash64",
1449
+ > extends AbstractPrimitiveSerializer<P> {
1450
+ readonly defaultValue = BigInt(0);
1451
+
1452
+ fromJson(json: Json): bigint {
1453
+ try {
1454
+ return BigInt(json as number | string);
1455
+ } catch (e) {
1456
+ if (typeof json === "number") {
1457
+ return BigInt(Math.round(json));
1458
+ } else {
1459
+ throw e;
1460
+ }
1461
+ }
1462
+ }
1463
+ }
1464
+
1465
+ const MIN_INT64 = BigInt("-9223372036854775808");
1466
+ const MAX_INT64 = BigInt("9223372036854775807");
1467
+
1468
+ class Int64Serializer extends AbstractBigIntSerializer<"int64"> {
1469
+ readonly primitive = "int64";
1470
+
1471
+ toJson(input: bigint): number | string {
1472
+ // 9007199254740991 == Number.MAX_SAFE_INTEGER
1473
+ if (-9007199254740991 <= input && input <= 9007199254740991) {
1474
+ return Number(input);
1475
+ }
1476
+ const s = BigInt(input).toString();
1477
+ // Clamp the number if it's out of bounds.
1478
+ return s.length <= 18
1479
+ ? // Small optimization for "small" numbers. The max int64 has 19 digits.
1480
+ s
1481
+ : input < MIN_INT64
1482
+ ? MIN_INT64.toString()
1483
+ : input < MAX_INT64
1484
+ ? s
1485
+ : MAX_INT64.toString();
1486
+ }
1487
+
1488
+ encode(input: bigint, stream: OutputStream): void {
1489
+ if (input) {
1490
+ if (-2147483648 <= input && input <= 2147483647) {
1491
+ int32_Serializer.encode(Number(input), stream);
1492
+ } else {
1493
+ stream.writeUint8(238);
1494
+ // Clamp the number if it's out of bounds.
1495
+ stream.writeInt64(
1496
+ input < MIN_INT64 ? MIN_INT64 : input < MAX_INT64 ? input : MAX_INT64,
1497
+ );
1498
+ }
1499
+ } else {
1500
+ stream.writeUint8(0);
1501
+ }
1502
+ }
1503
+
1504
+ decode(stream: InputStream): bigint {
1505
+ return decodeBigInt(stream);
1506
+ }
1507
+ }
1508
+
1509
+ const MAX_HASH64 = BigInt("18446744073709551615");
1510
+
1511
+ class Hash64Serializer extends AbstractBigIntSerializer<"hash64"> {
1512
+ readonly primitive = "hash64";
1513
+
1514
+ toJson(input: bigint): number | string {
1515
+ if (input <= 9007199254740991) {
1516
+ return input <= 0 ? 0 : Number(input);
1517
+ }
1518
+ input = BigInt(input);
1519
+ return MAX_HASH64 < input ? MAX_HASH64.toString() : input.toString();
1520
+ }
1521
+
1522
+ encode(input: bigint, stream: OutputStream): void {
1523
+ if (input < 232) {
1524
+ stream.writeUint8(input <= 0 ? 0 : Number(input));
1525
+ } else if (input < 4294967296) {
1526
+ if (input < 65536) {
1527
+ stream.writeUint8(232);
1528
+ stream.writeUint16(Number(input));
1529
+ } else {
1530
+ stream.writeUint8(233);
1531
+ stream.writeUint32(Number(input));
1532
+ }
1533
+ } else {
1534
+ stream.writeUint8(234);
1535
+ stream.writeHash64(input <= MAX_HASH64 ? input : MAX_HASH64);
1536
+ }
1537
+ }
1538
+
1539
+ decode(stream: InputStream): bigint {
1540
+ return decodeBigInt(stream);
1541
+ }
1542
+ }
1543
+
1544
+ type TimestampReadableJson = {
1545
+ unix_millis: number;
1546
+ formatted: string;
1547
+ };
1548
+
1549
+ class TimestampSerializer extends AbstractPrimitiveSerializer<"timestamp"> {
1550
+ readonly primitive = "timestamp";
1551
+ readonly defaultValue = Timestamp.UNIX_EPOCH;
1552
+
1553
+ toJson(
1554
+ input: Timestamp,
1555
+ flavor?: JsonFlavor,
1556
+ ): number | TimestampReadableJson {
1557
+ return flavor === "readable"
1558
+ ? {
1559
+ unix_millis: input.unixMillis,
1560
+ formatted: input.toDate().toISOString(),
1561
+ }
1562
+ : input.unixMillis;
1563
+ }
1564
+
1565
+ fromJson(json: Json): Timestamp {
1566
+ return Timestamp.fromUnixMillis(
1567
+ typeof json === "number"
1568
+ ? json
1569
+ : typeof json === "string"
1570
+ ? +json
1571
+ : (json as TimestampReadableJson)["unix_millis"],
1572
+ );
1573
+ }
1574
+
1575
+ encode(input: Timestamp, stream: OutputStream): void {
1576
+ const { unixMillis } = input;
1577
+ if (unixMillis) {
1578
+ stream.writeUint8(239);
1579
+ stream.writeInt64(BigInt(unixMillis));
1580
+ } else {
1581
+ stream.writeUint8(0);
1582
+ }
1583
+ }
1584
+
1585
+ decode(stream: InputStream): Timestamp {
1586
+ const unixMillis = decodeNumber(stream);
1587
+ return Timestamp.fromUnixMillis(Number(unixMillis));
1588
+ }
1589
+
1590
+ override isDefault(input: Timestamp): boolean {
1591
+ return !input.unixMillis;
1592
+ }
1593
+ }
1594
+
1595
+ class StringSerializer extends AbstractPrimitiveSerializer<"string"> {
1596
+ readonly primitive = "string";
1597
+ readonly defaultValue = "";
1598
+
1599
+ toJson(input: string): string {
1600
+ if (typeof input === "string") {
1601
+ return input;
1602
+ }
1603
+ throw this.newTypeError(input);
1604
+ }
1605
+
1606
+ fromJson(json: Json): string {
1607
+ if (typeof json === "string") {
1608
+ return json;
1609
+ }
1610
+ if (json === 0) {
1611
+ return "";
1612
+ }
1613
+ throw this.newTypeError(json);
1614
+ }
1615
+
1616
+ encode(input: string, stream: OutputStream): void {
1617
+ if (!input) {
1618
+ stream.writeUint8(242);
1619
+ return;
1620
+ }
1621
+ stream.writeUint8(243);
1622
+ // We don't know the length of the UTF-8 string until we actually encode the
1623
+ // string. We just know that it's at most 3 times the length of the input
1624
+ // string.
1625
+ const maxEncodedLength = input.length * 3;
1626
+ // Write zero in place of the UTF-8 sequence length. We will override this
1627
+ // number later.
1628
+ if (maxEncodedLength < 232) {
1629
+ stream.writeUint8(0);
1630
+ } else if (maxEncodedLength < 65536) {
1631
+ stream.writeUint8(232);
1632
+ stream.writeUint16(0);
1633
+ } else {
1634
+ stream.writeUint8(233);
1635
+ stream.writeUint32(0);
1636
+ }
1637
+ const { dataView, offset } = stream;
1638
+ // Write the UTF-8 string and record the number of bytes written.
1639
+ const encodedLength = stream.putUtf8String(input);
1640
+ // Write the length of the UTF-8 string where we wrote 0.
1641
+ if (maxEncodedLength < 232) {
1642
+ dataView.setUint8(offset - 1, encodedLength);
1643
+ } else if (maxEncodedLength < 65536) {
1644
+ dataView.setUint16(offset - 2, encodedLength, true);
1645
+ } else {
1646
+ dataView.setUint32(offset - 4, encodedLength, true);
1647
+ }
1648
+ }
1649
+
1650
+ decode(stream: InputStream): string {
1651
+ const wire = stream.readUint8();
1652
+ if (wire === 0 || wire === 242) {
1653
+ return "";
1654
+ }
1655
+ const encodedLength = decodeNumber(stream) as number;
1656
+ return textDecoder.decode(
1657
+ new Uint8Array(
1658
+ stream.buffer,
1659
+ (stream.offset += encodedLength) - encodedLength,
1660
+ encodedLength,
1661
+ ),
1662
+ );
1663
+ }
1664
+
1665
+ private newTypeError(actual: unknown): TypeError {
1666
+ return new TypeError(`expected: string; actual: ${typeof actual}`);
1667
+ }
1668
+ }
1669
+
1670
+ class ByteStringSerializer extends AbstractPrimitiveSerializer<"bytes"> {
1671
+ readonly primitive = "bytes";
1672
+ readonly defaultValue = ByteString.EMPTY;
1673
+
1674
+ toJson(input: ByteString, flavor?: JsonFlavor): string {
1675
+ return flavor === "readable" ? "hex:" + input.toBase16() : input.toBase64();
1676
+ }
1677
+
1678
+ fromJson(json: Json): ByteString {
1679
+ if (json === 0) {
1680
+ return ByteString.EMPTY;
1681
+ }
1682
+ const string = json as string;
1683
+ return string.startsWith("hex:")
1684
+ ? ByteString.fromBase16(string.substring(4))
1685
+ : ByteString.fromBase64(string);
1686
+ }
1687
+
1688
+ encode(input: ByteString, stream: OutputStream): void {
1689
+ const { byteLength } = input;
1690
+ if (byteLength) {
1691
+ stream.writeUint8(245);
1692
+ encodeUint32(byteLength, stream);
1693
+ stream.putBytes(input);
1694
+ } else {
1695
+ stream.writeUint8(244);
1696
+ }
1697
+ }
1698
+
1699
+ decode(stream: InputStream): ByteString {
1700
+ const wire = stream.readUint8();
1701
+ if (wire === 0 || wire === 244) {
1702
+ return ByteString.EMPTY;
1703
+ }
1704
+ const lengthInBytes = decodeNumber(stream) as number;
1705
+ return ByteString.sliceOf(
1706
+ stream.buffer,
1707
+ stream.offset,
1708
+ (stream.offset += lengthInBytes),
1709
+ );
1710
+ }
1711
+
1712
+ override isDefault(input: ByteString): boolean {
1713
+ return !input.byteLength;
1714
+ }
1715
+ }
1716
+
1717
+ type AnyRecord = Record<string, unknown>;
1718
+
1719
+ class StructFieldImpl<Struct = unknown, Value = unknown>
1720
+ implements StructField<Struct, Value>
1721
+ {
1722
+ constructor(
1723
+ readonly name: string,
1724
+ readonly property: string,
1725
+ readonly number: number,
1726
+ readonly serializer: InternalSerializer<Value>,
1727
+ readonly doc: string,
1728
+ ) {}
1729
+
1730
+ get type(): TypeDescriptor<Value> {
1731
+ return this.serializer.typeDescriptor;
1732
+ }
1733
+
1734
+ get(struct: Struct | MutableForm<Struct>): Value {
1735
+ return Reflect.get(struct as AnyRecord, this.property) as Value;
1736
+ }
1737
+
1738
+ set(struct: MutableForm<Struct>, value: Value): void {
1739
+ Reflect.set(struct, this.property, value);
1740
+ }
1741
+ }
1742
+
1743
+ type EnumVariantImpl<Enum = unknown> =
1744
+ | EnumConstantVariantImpl<Enum>
1745
+ | EnumWrapperVariantImpl<Enum, unknown>;
1746
+
1747
+ const textEncoder = new TextEncoder();
1748
+ const textDecoder = new TextDecoder();
1749
+
1750
+ class ArraySerializerImpl<Item>
1751
+ extends AbstractSerializer<readonly Item[]>
1752
+ implements ArrayDescriptor<readonly Item[]>
1753
+ {
1754
+ constructor(
1755
+ readonly itemSerializer: InternalSerializer<Item>,
1756
+ readonly keyExtractor?: string,
1757
+ ) {
1758
+ super();
1759
+ }
1760
+
1761
+ readonly kind = "array";
1762
+ readonly defaultValue = _EMPTY_ARRAY;
1763
+
1764
+ toJson(input: ReadonlyArray<Item>, flavor?: JsonFlavor): Json[] {
1765
+ return input.map((e) => this.itemSerializer.toJson(e, flavor));
1766
+ }
1767
+
1768
+ fromJson(json: Json, keep?: "keep-unrecognized-values"): ReadonlyArray<Item> {
1769
+ if (json === 0) {
1770
+ return _EMPTY_ARRAY;
1771
+ }
1772
+ return freezeArray(
1773
+ (json as readonly Json[]).map((e) =>
1774
+ this.itemSerializer.fromJson(e, keep),
1775
+ ),
1776
+ );
1777
+ }
1778
+
1779
+ encode(input: ReadonlyArray<Item>, stream: OutputStream): void {
1780
+ const { length } = input;
1781
+ if (length <= 3) {
1782
+ stream.writeUint8(246 + length);
1783
+ } else {
1784
+ stream.writeUint8(250);
1785
+ encodeUint32(length, stream);
1786
+ }
1787
+ const { itemSerializer } = this;
1788
+ for (let i = 0; i < input.length; ++i) {
1789
+ itemSerializer.encode(input[i]!, stream);
1790
+ }
1791
+ }
1792
+
1793
+ decode(stream: InputStream): readonly Item[] {
1794
+ const wire = stream.readUint8();
1795
+ if (wire === 0 || wire === 246) {
1796
+ return _EMPTY_ARRAY;
1797
+ }
1798
+ const length = wire === 250 ? (decodeNumber(stream) as number) : wire - 246;
1799
+ const { itemSerializer } = this;
1800
+ const result = new Array<Item>(length);
1801
+ for (let i = 0; i < length; ++i) {
1802
+ result[i] = itemSerializer.decode(stream);
1803
+ }
1804
+ return freezeArray(result);
1805
+ }
1806
+
1807
+ override isDefault(input: ReadonlyArray<Item>): boolean {
1808
+ return !input.length;
1809
+ }
1810
+
1811
+ get itemType(): TypeDescriptor<Item> {
1812
+ return this.itemSerializer.typeDescriptor;
1813
+ }
1814
+
1815
+ get typeSignature(): TypeSignature {
1816
+ return {
1817
+ kind: "array",
1818
+ value: {
1819
+ item: this.itemSerializer.typeSignature,
1820
+ key_extractor: this.keyExtractor,
1821
+ },
1822
+ };
1823
+ }
1824
+
1825
+ addRecordDefinitionsTo(out: { [k: string]: RecordDefinition }): void {
1826
+ this.itemSerializer.addRecordDefinitionsTo(out);
1827
+ }
1828
+ }
1829
+
1830
+ class OptionalSerializerImpl<Other>
1831
+ extends AbstractSerializer<Other | null>
1832
+ implements OptionalDescriptor<Other | null>
1833
+ {
1834
+ constructor(readonly otherSerializer: InternalSerializer<Other>) {
1835
+ super();
1836
+ }
1837
+
1838
+ readonly kind = "optional";
1839
+ readonly defaultValue = null;
1840
+
1841
+ toJson(input: Other, flavor?: JsonFlavor): Json {
1842
+ return input !== null ? this.otherSerializer.toJson(input, flavor) : null;
1843
+ }
1844
+
1845
+ fromJson(json: Json, keep?: "keep-unrecognized-values"): Other | null {
1846
+ return json !== null ? this.otherSerializer.fromJson(json, keep) : null;
1847
+ }
1848
+
1849
+ encode(input: Other | null, stream: OutputStream): void {
1850
+ if (input === null) {
1851
+ stream.writeUint8(255);
1852
+ } else {
1853
+ this.otherSerializer.encode(input, stream);
1854
+ }
1855
+ }
1856
+
1857
+ decode(stream: InputStream): Other | null {
1858
+ const wire = stream.dataView.getUint8(stream.offset);
1859
+ if (wire === 255) {
1860
+ ++stream.offset;
1861
+ return null;
1862
+ }
1863
+ return this.otherSerializer.decode(stream);
1864
+ }
1865
+
1866
+ override isDefault(input: Other | null): boolean {
1867
+ return input === null;
1868
+ }
1869
+
1870
+ get otherType(): TypeDescriptor<NonNullable<Other>> {
1871
+ return this.otherSerializer.typeDescriptor as TypeDescriptor<
1872
+ NonNullable<Other>
1873
+ >;
1874
+ }
1875
+
1876
+ get typeSignature(): TypeSignature {
1877
+ return {
1878
+ kind: "optional",
1879
+ value: this.otherSerializer.typeSignature,
1880
+ };
1881
+ }
1882
+
1883
+ addRecordDefinitionsTo(out: { [k: string]: RecordDefinition }): void {
1884
+ this.otherSerializer.addRecordDefinitionsTo(out);
1885
+ }
1886
+ }
1887
+
1888
+ const primitiveSerializers: {
1889
+ [P in keyof PrimitiveTypes]: Serializer<PrimitiveTypes[P]>;
1890
+ } = {
1891
+ bool: new BoolSerializer(),
1892
+ int32: int32_Serializer,
1893
+ int64: new Int64Serializer(),
1894
+ hash64: new Hash64Serializer(),
1895
+ float32: new Float32Serializer(),
1896
+ float64: new Float64Serializer(),
1897
+ timestamp: new TimestampSerializer(),
1898
+ string: new StringSerializer(),
1899
+ bytes: new ByteStringSerializer(),
1900
+ };
1901
+
1902
+ type NewMutableFn<Frozen> = (
1903
+ initializer?: Frozen | MutableForm<Frozen>,
1904
+ ) => MutableForm<Frozen>;
1905
+
1906
+ function decodeUnused(stream: InputStream): void {
1907
+ const wire = stream.readUint8();
1908
+ if (wire < 232) {
1909
+ return;
1910
+ }
1911
+ switch (wire - 232) {
1912
+ case 0: // uint16
1913
+ case 4: // uint16 - 65536
1914
+ stream.offset += 2;
1915
+ break;
1916
+ case 1: // uint32
1917
+ case 5: // int32
1918
+ case 8: // float32
1919
+ stream.offset += 4;
1920
+ break;
1921
+ case 2: // hash64
1922
+ case 6: // int64
1923
+ case 7: // hash64 timestamp
1924
+ case 9: // float64
1925
+ stream.offset += 8;
1926
+ break;
1927
+ case 3: // uint8 - 256
1928
+ ++stream.offset;
1929
+ break;
1930
+ case 11: // string
1931
+ case 13: {
1932
+ // bytes
1933
+ const length = decodeNumber(stream) as number;
1934
+ stream.offset += length;
1935
+ break;
1936
+ }
1937
+ case 15: // array length==1
1938
+ case 19: // enum value kind==1
1939
+ case 20: // enum value kind==2
1940
+ case 21: // enum value kind==3
1941
+ case 22: // enum value kind==4
1942
+ decodeUnused(stream);
1943
+ break;
1944
+ case 16: // array length==2
1945
+ decodeUnused(stream);
1946
+ decodeUnused(stream);
1947
+ break;
1948
+ case 17: // array length==3
1949
+ decodeUnused(stream);
1950
+ decodeUnused(stream);
1951
+ decodeUnused(stream);
1952
+ break;
1953
+ case 18: {
1954
+ // array length==N
1955
+ const length = decodeNumber(stream);
1956
+ for (let i = 0; i < length; ++i) {
1957
+ decodeUnused(stream);
1958
+ }
1959
+ break;
1960
+ }
1961
+ }
1962
+ }
1963
+
1964
+ abstract class AbstractRecordSerializer<T, F> extends AbstractSerializer<T> {
1965
+ /** Uniquely identifies this record serializer. */
1966
+ readonly token: symbol = Symbol();
1967
+ abstract kind: "struct" | "enum";
1968
+ name = "";
1969
+ modulePath = "";
1970
+ parentType: StructDescriptor | EnumDescriptor | undefined;
1971
+ doc = "";
1972
+ removedNumbers = new Set<number>();
1973
+ initialized?: true;
1974
+
1975
+ init(
1976
+ name: string,
1977
+ modulePath: string,
1978
+ parentType: StructDescriptor | EnumDescriptor | undefined,
1979
+ doc: string,
1980
+ fieldsOrVariants: readonly F[],
1981
+ removedNumbers: readonly number[],
1982
+ ): void {
1983
+ this.name = name;
1984
+ this.modulePath = modulePath;
1985
+ this.parentType = parentType;
1986
+ this.doc = doc;
1987
+ this.removedNumbers = new Set(removedNumbers);
1988
+ this.registerFieldsOrVariants(fieldsOrVariants);
1989
+ this.initialized = true;
1990
+ freezeDeeply(this);
1991
+ }
1992
+
1993
+ get qualifiedName(): string {
1994
+ const { name, parentType } = this;
1995
+ return parentType ? `${parentType.name}.${name}` : name;
1996
+ }
1997
+
1998
+ abstract registerFieldsOrVariants(fieldsOrVariants: readonly F[]): void;
1999
+
2000
+ addRecordDefinitionsTo(out: { [k: string]: RecordDefinition }): void {
2001
+ const recordId = `${this.modulePath}:${this.qualifiedName}`;
2002
+ if (out[recordId]) {
2003
+ return;
2004
+ }
2005
+ const recordDefinition = this.makeRecordDefinition(recordId);
2006
+ if (this.doc) {
2007
+ recordDefinition.doc = this.doc;
2008
+ }
2009
+ if (this.removedNumbers.size) {
2010
+ recordDefinition.removed_numbers = [...this.removedNumbers];
2011
+ }
2012
+ out[recordId] = recordDefinition;
2013
+ for (const dependency of this.dependencies()) {
2014
+ dependency.addRecordDefinitionsTo(out);
2015
+ }
2016
+ }
2017
+
2018
+ abstract makeRecordDefinition(recordId: string): RecordDefinition;
2019
+ abstract dependencies(): InternalSerializer[];
2020
+ }
2021
+
2022
+ /** Unrecognized fields found when deserializing a struct. */
2023
+ class UnrecognizedFields {
2024
+ constructor(
2025
+ /** Uniquely identifies the struct. */
2026
+ readonly token: symbol,
2027
+ /** Total number of fields in the struct. */
2028
+ readonly totalSlots: number,
2029
+ readonly json?: ReadonlyArray<Json>,
2030
+ readonly bytes?: ByteString,
2031
+ ) {
2032
+ Object.freeze(this);
2033
+ }
2034
+ }
2035
+
2036
+ class StructSerializerImpl<T = unknown>
2037
+ extends AbstractRecordSerializer<T, StructFieldImpl<T>>
2038
+ implements StructDescriptor<T>
2039
+ {
2040
+ constructor(
2041
+ readonly defaultValue: T,
2042
+ readonly createFn: (initializer: AnyRecord) => T,
2043
+ readonly newMutableFn: NewMutableFn<T>,
2044
+ ) {
2045
+ super();
2046
+ }
2047
+
2048
+ readonly kind = "struct";
2049
+ // Fields in the order they appear in the '.skir' file.
2050
+ readonly fields: Array<StructFieldImpl<T>> = [];
2051
+ readonly fieldMapping: { [key: string | number]: StructFieldImpl<T> } = {};
2052
+ // Fields sorted by number in descending order.
2053
+ private reversedFields: Array<StructFieldImpl<T>> = [];
2054
+ // This is *not* a dense array, missing slots correspond to removed fields.
2055
+ private readonly slots: Array<StructFieldImpl<T>> = [];
2056
+ private recognizedSlots = 0;
2057
+ // Contains one zero for every field number.
2058
+ private readonly zeros: Json[] = [];
2059
+ private readonly initializerTemplate: Record<string, unknown> = {};
2060
+
2061
+ toJson(input: T, flavor?: JsonFlavor): Json {
2062
+ if (input === this.defaultValue) {
2063
+ return flavor === "readable" ? {} : [];
2064
+ }
2065
+ if (flavor === "readable") {
2066
+ const { fields } = this;
2067
+ const result: { [name: string]: Json } = {};
2068
+ for (const field of fields) {
2069
+ const { serializer } = field;
2070
+ const value = (input as AnyRecord)[field.property];
2071
+ if (field.serializer.isDefault(value)) {
2072
+ continue;
2073
+ }
2074
+ result[field.name] = serializer.toJson(value, flavor);
2075
+ }
2076
+ return result;
2077
+ } else {
2078
+ // Dense flavor.
2079
+ const { slots } = this;
2080
+ let result: Json[];
2081
+ const unrecognizedFields = //
2082
+ (input as AnyRecord)["^"] as UnrecognizedFields;
2083
+ if (
2084
+ unrecognizedFields &&
2085
+ unrecognizedFields.json &&
2086
+ unrecognizedFields.token === this.token
2087
+ ) {
2088
+ // We'll need to copy the unrecognized fields to the JSON.
2089
+ result = this.zeros.concat(unrecognizedFields.json);
2090
+ for (const field of this.fields) {
2091
+ result[field.number] = field.serializer.toJson(
2092
+ (input as AnyRecord)[field.property],
2093
+ flavor,
2094
+ );
2095
+ }
2096
+ } else {
2097
+ result = [];
2098
+ const arrayLength = this.getArrayLength(input);
2099
+ for (let i = 0; i < arrayLength; ++i) {
2100
+ const field = slots[i];
2101
+ result[i] = field
2102
+ ? field.serializer.toJson(
2103
+ (input as AnyRecord)[field.property],
2104
+ flavor,
2105
+ )
2106
+ : 0;
2107
+ }
2108
+ }
2109
+ return result;
2110
+ }
2111
+ }
2112
+
2113
+ fromJson(json: Json, keep?: "keep-unrecognized-values"): T {
2114
+ if (!json) {
2115
+ return this.defaultValue;
2116
+ }
2117
+ const initializer = { ...this.initializerTemplate };
2118
+ if (json instanceof Array) {
2119
+ const { slots, recognizedSlots } = this;
2120
+ // Dense flavor.
2121
+ if (json.length > recognizedSlots) {
2122
+ // We have some unrecognized fields.
2123
+ if (keep) {
2124
+ const unrecognizedFields = new UnrecognizedFields(
2125
+ this.token,
2126
+ json.length,
2127
+ copyJson(json.slice(recognizedSlots)),
2128
+ );
2129
+ initializer["^"] = unrecognizedFields;
2130
+ }
2131
+ // Now that we have stored the unrecognized fields in `initializer`, we
2132
+ // can remove them from `json`.
2133
+ json = json.slice(0, recognizedSlots);
2134
+ }
2135
+ for (let i = 0; i < json.length && i < slots.length; ++i) {
2136
+ const field = slots[i];
2137
+ if (field) {
2138
+ initializer[field.property] = field.serializer.fromJson(
2139
+ json[i]!,
2140
+ keep,
2141
+ );
2142
+ }
2143
+ // Else the field was removed.
2144
+ }
2145
+ return this.createFn(initializer);
2146
+ } else if (json instanceof Object) {
2147
+ // Readable flavor.
2148
+ const { fieldMapping } = this;
2149
+ for (const name in json) {
2150
+ const field = fieldMapping[name];
2151
+ if (field) {
2152
+ initializer[field.property] = field.serializer.fromJson(
2153
+ json[name]!,
2154
+ keep,
2155
+ );
2156
+ }
2157
+ }
2158
+ return this.createFn(initializer);
2159
+ }
2160
+ throw TypeError();
2161
+ }
2162
+
2163
+ encode(input: T, stream: OutputStream): void {
2164
+ // Total number of slots to write. Includes removed and unrecognized fields.
2165
+ let totalSlots: number;
2166
+ let recognizedSlots: number;
2167
+ let unrecognizedBytes: ByteString | undefined;
2168
+ const unrecognizedFields = (input as AnyRecord)["^"] as UnrecognizedFields;
2169
+ if (
2170
+ unrecognizedFields &&
2171
+ unrecognizedFields.bytes &&
2172
+ unrecognizedFields.token === this.token
2173
+ ) {
2174
+ totalSlots = unrecognizedFields.totalSlots;
2175
+ recognizedSlots = this.recognizedSlots;
2176
+ unrecognizedBytes = unrecognizedFields.bytes;
2177
+ } else {
2178
+ // No unrecognized fields.
2179
+ totalSlots = recognizedSlots = this.getArrayLength(input);
2180
+ }
2181
+
2182
+ if (totalSlots <= 3) {
2183
+ stream.writeUint8(246 + totalSlots);
2184
+ } else {
2185
+ stream.writeUint8(250);
2186
+ encodeUint32(totalSlots, stream);
2187
+ }
2188
+ const { slots } = this;
2189
+ for (let i = 0; i < recognizedSlots; ++i) {
2190
+ const field = slots[i];
2191
+ if (field) {
2192
+ field.serializer.encode((input as AnyRecord)[field.property], stream);
2193
+ } else {
2194
+ // Append '0' if the field was removed.
2195
+ stream.writeUint8(0);
2196
+ }
2197
+ }
2198
+ if (unrecognizedBytes) {
2199
+ // Copy the unrecognized fields.
2200
+ stream.putBytes(unrecognizedBytes);
2201
+ }
2202
+ }
2203
+
2204
+ decode(stream: InputStream): T {
2205
+ const wire = stream.readUint8();
2206
+ if (wire === 0 || wire === 246) {
2207
+ return this.defaultValue;
2208
+ }
2209
+ const initializer = { ...this.initializerTemplate };
2210
+ const encodedSlots =
2211
+ wire === 250 ? (decodeNumber(stream) as number) : wire - 246;
2212
+ const { slots, recognizedSlots } = this;
2213
+ // Do not read more slots than the number of recognized slots.
2214
+ for (let i = 0; i < encodedSlots && i < recognizedSlots; ++i) {
2215
+ const field = slots[i];
2216
+ if (field) {
2217
+ initializer[field.property] = field.serializer.decode(stream);
2218
+ } else {
2219
+ // The field was removed.
2220
+ decodeUnused(stream);
2221
+ }
2222
+ }
2223
+ if (encodedSlots > recognizedSlots) {
2224
+ // We have some unrecognized fields.
2225
+ const start = stream.offset;
2226
+ for (let i = recognizedSlots; i < encodedSlots; ++i) {
2227
+ decodeUnused(stream);
2228
+ }
2229
+ if (stream.keepUnrecognizedValues) {
2230
+ const end = stream.offset;
2231
+ const unrecognizedBytes = ByteString.sliceOf(stream.buffer, start, end);
2232
+ const unrecognizedFields = new UnrecognizedFields(
2233
+ this.token,
2234
+ encodedSlots,
2235
+ undefined,
2236
+ unrecognizedBytes,
2237
+ );
2238
+ initializer["^"] = unrecognizedFields;
2239
+ }
2240
+ }
2241
+ return this.createFn(initializer);
2242
+ }
2243
+
2244
+ /**
2245
+ * Returns the length of the JSON array for the given input, which is also the
2246
+ * number of slots and includes removed fields.
2247
+ * Assumes that `input` does not contain unrecognized fields.
2248
+ */
2249
+ private getArrayLength(input: T): number {
2250
+ const { reversedFields } = this;
2251
+ for (let i = 0; i < reversedFields.length; ++i) {
2252
+ const field = reversedFields[i]!;
2253
+ const isDefault = //
2254
+ field.serializer.isDefault((input as AnyRecord)[field.property]);
2255
+ if (!isDefault) {
2256
+ return field.number + 1;
2257
+ }
2258
+ }
2259
+ return 0;
2260
+ }
2261
+
2262
+ override isDefault(input: T): boolean {
2263
+ if (input === this.defaultValue) {
2264
+ return true;
2265
+ }
2266
+ // It's possible for a value of type T to be equal to T.DEFAULT but to not
2267
+ // be the reference to T.DEFAULT.
2268
+ if ((input as AnyRecord)["^"] as UnrecognizedFields) {
2269
+ return false;
2270
+ }
2271
+ return this.fields.every((f) =>
2272
+ f.serializer.isDefault((input as AnyRecord)[f.property]),
2273
+ );
2274
+ }
2275
+
2276
+ get typeSignature(): TypeSignature {
2277
+ return {
2278
+ kind: "record",
2279
+ value: `${this.modulePath}:${this.qualifiedName}`,
2280
+ };
2281
+ }
2282
+
2283
+ getField<K extends string | number>(key: K): StructFieldResult<T, K> {
2284
+ return this.fieldMapping[key]!;
2285
+ }
2286
+
2287
+ newMutable(initializer?: T | MutableForm<T>): MutableForm<T> {
2288
+ return this.newMutableFn(initializer);
2289
+ }
2290
+
2291
+ registerFieldsOrVariants(fields: ReadonlyArray<StructFieldImpl<T>>): void {
2292
+ for (const field of fields) {
2293
+ const { name, number, property } = field;
2294
+ this.fields.push(field);
2295
+ this.slots[number] = field;
2296
+ this.fieldMapping[name] = field;
2297
+ this.fieldMapping[property] = field;
2298
+ this.fieldMapping[number] = field;
2299
+ this.initializerTemplate[property] = (this.defaultValue as AnyRecord)[
2300
+ field.property
2301
+ ];
2302
+ }
2303
+ // Removed numbers count as recognized slots.
2304
+ this.recognizedSlots =
2305
+ Math.max(this.slots.length - 1, ...this.removedNumbers) + 1;
2306
+ this.zeros.push(...Array<Json>(this.recognizedSlots).fill(0));
2307
+ this.reversedFields = [...this.fields].sort((a, b) => b.number - a.number);
2308
+ }
2309
+
2310
+ makeRecordDefinition(recordId: string): StructDefinition {
2311
+ return {
2312
+ kind: "struct",
2313
+ id: recordId,
2314
+ doc: undefined,
2315
+ fields: this.fields.map((f) => ({
2316
+ name: f.name,
2317
+ number: f.number,
2318
+ type: f.serializer.typeSignature,
2319
+ doc: emptyToUndefined(f.doc),
2320
+ })),
2321
+ };
2322
+ }
2323
+
2324
+ dependencies(): InternalSerializer[] {
2325
+ return this.fields.map((f) => f.serializer);
2326
+ }
2327
+ }
2328
+
2329
+ class UnrecognizedEnum {
2330
+ constructor(
2331
+ readonly token: symbol,
2332
+ readonly json?: Json,
2333
+ readonly bytes?: ByteString,
2334
+ ) {
2335
+ Object.freeze(this);
2336
+ }
2337
+ }
2338
+
2339
+ interface EnumConstantVariantImpl<Enum> extends EnumConstantVariant<Enum> {
2340
+ readonly serializer?: undefined;
2341
+ }
2342
+
2343
+ class EnumWrapperVariantImpl<Enum, Value = unknown> {
2344
+ constructor(
2345
+ readonly name: string,
2346
+ readonly number: number,
2347
+ readonly serializer: InternalSerializer<Value>,
2348
+ readonly doc: string,
2349
+ private createFn: (initializer: { kind: string; value: unknown }) => Enum,
2350
+ ) {}
2351
+
2352
+ get type(): TypeDescriptor<Value> {
2353
+ return this.serializer.typeDescriptor;
2354
+ }
2355
+
2356
+ readonly constant?: undefined;
2357
+
2358
+ get(e: Enum): Value | undefined {
2359
+ return (e as AnyRecord).kind === this.name
2360
+ ? ((e as AnyRecord).value as Value)
2361
+ : undefined;
2362
+ }
2363
+
2364
+ wrap(value: Value): Enum {
2365
+ return this.createFn({ kind: this.name, value: value });
2366
+ }
2367
+ }
2368
+
2369
+ class EnumSerializerImpl<T = unknown>
2370
+ extends AbstractRecordSerializer<T, EnumVariantImpl<T>>
2371
+ implements EnumDescriptor<T>
2372
+ {
2373
+ constructor(readonly createFn: (initializer: unknown) => T) {
2374
+ super();
2375
+ this.defaultValue = createFn("UNKNOWN");
2376
+ }
2377
+
2378
+ readonly kind = "enum";
2379
+ readonly defaultValue: T;
2380
+ readonly variants: EnumVariantImpl<T>[] = [];
2381
+ private readonly variantMapping: {
2382
+ [key: string | number]: EnumVariantImpl<T>;
2383
+ } = {};
2384
+
2385
+ toJson(input: T, flavor?: JsonFlavor): Json {
2386
+ const unrecognized = (input as AnyRecord)["^"] as
2387
+ | UnrecognizedEnum
2388
+ | undefined;
2389
+ const kind = (input as AnyRecord).kind as string;
2390
+ if (
2391
+ kind === "UNKNOWN" &&
2392
+ unrecognized &&
2393
+ unrecognized.json &&
2394
+ unrecognized.token === this.token
2395
+ ) {
2396
+ // Unrecognized variant.
2397
+ return unrecognized.json;
2398
+ }
2399
+ if (kind === "UNKNOWN") {
2400
+ return flavor === "readable" ? "unknown" : 0;
2401
+ }
2402
+ const variant = this.variantMapping[kind]!;
2403
+ const { serializer } = variant;
2404
+ if (serializer) {
2405
+ const value = (input as AnyRecord).value;
2406
+ if (flavor === "readable") {
2407
+ return {
2408
+ kind: variant.name,
2409
+ value: serializer.toJson(value, flavor),
2410
+ };
2411
+ } else {
2412
+ // Dense flavor.
2413
+ return [variant.number, serializer.toJson(value, flavor)];
2414
+ }
2415
+ } else {
2416
+ // A constant variant.
2417
+ return flavor === "readable"
2418
+ ? variant.name.toLowerCase()
2419
+ : variant.number;
2420
+ }
2421
+ }
2422
+
2423
+ fromJson(json: Json, keep?: "keep-unrecognized-values"): T {
2424
+ const isNumber = typeof json === "number";
2425
+ if (isNumber || typeof json === "string") {
2426
+ const variant = this.variantMapping[isNumber ? json : String(json)];
2427
+ if (!variant) {
2428
+ // Check if the variant was removed, in which case we want to return
2429
+ // UNKNOWN, or is unrecognized.
2430
+ return !keep || (isNumber && this.removedNumbers.has(json))
2431
+ ? this.defaultValue
2432
+ : this.createFn(new UnrecognizedEnum(this.token, copyJson(json)));
2433
+ }
2434
+ if (variant.serializer) {
2435
+ // A constant variant became a wrapper variant.
2436
+ return variant.wrap(variant.serializer.defaultValue);
2437
+ }
2438
+ return variant.constant;
2439
+ }
2440
+ let variantKey: number | string;
2441
+ let valueAsJson: Json;
2442
+ if (json instanceof Array) {
2443
+ variantKey = json[0] as number;
2444
+ valueAsJson = json[1]!;
2445
+ } else if (json instanceof Object) {
2446
+ variantKey = json["kind"] as string;
2447
+ valueAsJson = json["value"]!;
2448
+ } else {
2449
+ throw TypeError();
2450
+ }
2451
+ const variant = this.variantMapping[variantKey];
2452
+ if (!variant) {
2453
+ // Check if the variant was removed, in which case we want to return
2454
+ // UNKNOWN, or is unrecognized.
2455
+ return !keep ||
2456
+ (typeof variantKey === "number" && this.removedNumbers.has(variantKey))
2457
+ ? this.defaultValue
2458
+ : this.createFn(
2459
+ new UnrecognizedEnum(this.token, copyJson(json), undefined),
2460
+ );
2461
+ }
2462
+ const { serializer } = variant;
2463
+ if (!serializer) {
2464
+ // A wrapper variant became a constant variant.
2465
+ return variant.constant;
2466
+ }
2467
+ return variant.wrap(serializer.fromJson(valueAsJson, keep));
2468
+ }
2469
+
2470
+ encode(input: T, stream: OutputStream): void {
2471
+ const unrecognized = //
2472
+ (input as AnyRecord)["^"] as UnrecognizedEnum | undefined;
2473
+ const kind = (input as AnyRecord).kind as string;
2474
+ if (
2475
+ kind === "UNKNOWN" &&
2476
+ unrecognized &&
2477
+ unrecognized.bytes &&
2478
+ unrecognized.token === this.token
2479
+ ) {
2480
+ // Unrecognized variant.
2481
+ stream.putBytes(unrecognized.bytes);
2482
+ return;
2483
+ }
2484
+ if (kind === "UNKNOWN") {
2485
+ stream.writeUint8(0);
2486
+ return;
2487
+ }
2488
+ const variant = this.variantMapping[kind]!;
2489
+ const { number, serializer } = variant;
2490
+ if (serializer) {
2491
+ // A wrapper variant.
2492
+ const value = (input as AnyRecord).value;
2493
+ if (number < 5) {
2494
+ // The number can't be 0 or else kind == "UNKNOWN".
2495
+ stream.writeUint8(250 + number);
2496
+ } else {
2497
+ stream.writeUint8(248);
2498
+ encodeUint32(number, stream);
2499
+ }
2500
+ serializer.encode(value, stream);
2501
+ } else {
2502
+ // A constant variant.
2503
+ encodeUint32(number, stream);
2504
+ }
2505
+ }
2506
+
2507
+ decode(stream: InputStream): T {
2508
+ const startOffset = stream.offset;
2509
+ const wire = stream.dataView.getUint8(startOffset);
2510
+ if (wire < 242) {
2511
+ // A number
2512
+ const number = decodeNumber(stream) as number;
2513
+ const variant = this.variantMapping[number];
2514
+ if (!variant) {
2515
+ // Check if the variant was removed, in which case we want to return
2516
+ // UNKNOWN, or is unrecognized.
2517
+ if (!stream.keepUnrecognizedValues || this.removedNumbers.has(number)) {
2518
+ return this.defaultValue;
2519
+ } else {
2520
+ const { offset } = stream;
2521
+ const bytes = ByteString.sliceOf(stream.buffer, startOffset, offset);
2522
+ return this.createFn(
2523
+ new UnrecognizedEnum(this.token, undefined, bytes),
2524
+ );
2525
+ }
2526
+ }
2527
+ if (variant.serializer) {
2528
+ // A constant variant became a wrapper variant.
2529
+ return variant.wrap(variant.serializer.defaultValue);
2530
+ }
2531
+ return variant.constant;
2532
+ } else {
2533
+ ++stream.offset;
2534
+ const number =
2535
+ wire === 248 ? (decodeNumber(stream) as number) : wire - 250;
2536
+ const variant = this.variantMapping[number];
2537
+ if (!variant) {
2538
+ decodeUnused(stream);
2539
+ // Check if the variant was removed, in which case we want to return
2540
+ // UNKNOWN, or is unrecognized.
2541
+ if (!stream.keepUnrecognizedValues || this.removedNumbers.has(number)) {
2542
+ return this.defaultValue;
2543
+ } else {
2544
+ const { offset } = stream;
2545
+ const bytes = ByteString.sliceOf(stream.buffer, startOffset, offset);
2546
+ return this.createFn(
2547
+ new UnrecognizedEnum(this.token, undefined, bytes),
2548
+ );
2549
+ }
2550
+ }
2551
+ const { serializer } = variant;
2552
+ if (!serializer) {
2553
+ decodeUnused(stream);
2554
+ // A wrapper variant became a constant variant.
2555
+ return variant.constant;
2556
+ }
2557
+ return variant.wrap(serializer.decode(stream));
2558
+ }
2559
+ }
2560
+
2561
+ get typeSignature(): TypeSignature {
2562
+ return {
2563
+ kind: "record",
2564
+ value: `${this.modulePath}:${this.qualifiedName}`,
2565
+ };
2566
+ }
2567
+
2568
+ override isDefault(input: T): boolean {
2569
+ type Kinded = { kind: string };
2570
+ return (input as Kinded).kind === "UNKNOWN" && !(input as AnyRecord)["^"];
2571
+ }
2572
+
2573
+ getVariant<K extends string | number>(key: K): EnumVariantResult<T, K> {
2574
+ return this.variantMapping[key]!;
2575
+ }
2576
+
2577
+ registerFieldsOrVariants(variants: ReadonlyArray<EnumVariantImpl<T>>): void {
2578
+ for (const variant of variants) {
2579
+ this.variants.push(variant);
2580
+ this.variantMapping[variant.name] = variant;
2581
+ // Register case aliases for constant variants so that both UPPER_CASE
2582
+ // and lower_case names are accepted when parsing readable JSON.
2583
+ if (variant.serializer === undefined) {
2584
+ const nameUpper = variant.name.toUpperCase();
2585
+ if (nameUpper !== variant.name) {
2586
+ this.variantMapping[nameUpper] = variant;
2587
+ }
2588
+ const nameLower = variant.name.toLowerCase();
2589
+ if (nameLower !== variant.name) {
2590
+ this.variantMapping[nameLower] = variant;
2591
+ }
2592
+ }
2593
+ this.variantMapping[variant.number] = variant;
2594
+ }
2595
+ }
2596
+
2597
+ makeRecordDefinition(recordId: string): EnumDefinition {
2598
+ return {
2599
+ kind: "enum",
2600
+ id: recordId,
2601
+ doc: undefined,
2602
+ variants: this.variants
2603
+ // Skip the UNKNOWN variant.
2604
+ .filter((f) => f.number)
2605
+ .map((f) => {
2606
+ const result = {
2607
+ name: f.name.toLowerCase(),
2608
+ number: f.number,
2609
+ doc: emptyToUndefined(f.doc),
2610
+ };
2611
+ const type = f?.serializer?.typeSignature;
2612
+ return type ? { ...result, type: type } : result;
2613
+ }),
2614
+ };
2615
+ }
2616
+
2617
+ dependencies(): InternalSerializer[] {
2618
+ const result: InternalSerializer[] = [];
2619
+ for (const f of this.variants) {
2620
+ if (f.serializer) {
2621
+ result.push(f.serializer);
2622
+ }
2623
+ }
2624
+ return result;
2625
+ }
2626
+ }
2627
+
2628
+ function copyJson(input: readonly Json[]): Json[];
2629
+ function copyJson(input: Json): Json;
2630
+ function copyJson(input: Json): Json {
2631
+ if (input instanceof Array) {
2632
+ return Object.freeze(input.map(copyJson));
2633
+ } else if (input instanceof Object) {
2634
+ return Object.freeze(
2635
+ Object.fromEntries(Object.entries(input).map((k, v) => [k, copyJson(v)])),
2636
+ );
2637
+ }
2638
+ // A boolean, a number, a string or null.
2639
+ return input;
2640
+ }
2641
+
2642
+ function freezeDeeply(o: unknown): void {
2643
+ if (!(o instanceof Object)) {
2644
+ return;
2645
+ }
2646
+ if (o instanceof _FrozenBase || o instanceof _EnumBase) {
2647
+ return;
2648
+ }
2649
+ if (o instanceof AbstractRecordSerializer && !o.initialized) {
2650
+ return;
2651
+ }
2652
+ if (Object.isFrozen(o)) {
2653
+ return;
2654
+ }
2655
+ Object.freeze(o);
2656
+ for (const v of Object.values(o)) {
2657
+ freezeDeeply(v);
2658
+ }
2659
+ }
2660
+
2661
+ // =============================================================================
2662
+ // Frozen arrays
2663
+ // =============================================================================
2664
+
2665
+ interface FrozenArrayInfo {
2666
+ keyFnToIndexing?: Map<unknown, Map<unknown, unknown>>;
2667
+ }
2668
+
2669
+ const frozenArrayRegistry = new WeakMap<
2670
+ ReadonlyArray<unknown>,
2671
+ FrozenArrayInfo
2672
+ >();
2673
+
2674
+ function freezeArray<T>(array: readonly T[]): readonly T[] {
2675
+ if (!frozenArrayRegistry.has(array)) {
2676
+ frozenArrayRegistry.set(Object.freeze(array), {});
2677
+ }
2678
+ return array;
2679
+ }
2680
+
2681
+ export const _EMPTY_ARRAY = freezeArray([]);
2682
+
2683
+ export function _toFrozenArray<T, Initializer>(
2684
+ initializers: readonly Initializer[],
2685
+ itemToFrozenFn?: (item: Initializer) => T,
2686
+ ): readonly T[] {
2687
+ if (!initializers.length) {
2688
+ return _EMPTY_ARRAY;
2689
+ }
2690
+ if (frozenArrayRegistry.has(initializers)) {
2691
+ // No need to make a copy: the given array is already deeply-frozen.
2692
+ return initializers as unknown as readonly T[];
2693
+ }
2694
+ const ret = Object.freeze(
2695
+ itemToFrozenFn
2696
+ ? initializers.map(itemToFrozenFn)
2697
+ : (initializers.slice() as unknown as readonly T[]),
2698
+ );
2699
+ frozenArrayRegistry.set(ret, {});
2700
+ return ret;
2701
+ }
2702
+
2703
+ // =============================================================================
2704
+ // Shared implementation of generated classes
2705
+ // =============================================================================
2706
+
2707
+ export declare const _INITIALIZER: unique symbol;
2708
+
2709
+ const PRIVATE_KEY: unique symbol = Symbol();
2710
+
2711
+ function forPrivateUseError(t: unknown): Error {
2712
+ const clazz = Object.getPrototypeOf(t).constructor as AnyRecord;
2713
+ const { qualifiedName } = clazz.serializer as StructDescriptor;
2714
+ return Error(
2715
+ [
2716
+ "Do not call the constructor directly; ",
2717
+ `instead, call ${qualifiedName}.create(...)`,
2718
+ ].join(""),
2719
+ );
2720
+ }
2721
+
2722
+ export abstract class _FrozenBase {
2723
+ protected constructor(privateKey: symbol) {
2724
+ if (privateKey !== PRIVATE_KEY) {
2725
+ throw forPrivateUseError(this);
2726
+ }
2727
+ }
2728
+
2729
+ toMutable(): unknown {
2730
+ return new (Object.getPrototypeOf(this).constructor.Mutable)(this);
2731
+ }
2732
+
2733
+ toFrozen(): this {
2734
+ return this;
2735
+ }
2736
+
2737
+ toString(): string {
2738
+ return toStringImpl(this);
2739
+ }
2740
+
2741
+ declare [_INITIALIZER]: unknown;
2742
+ }
2743
+
2744
+ export abstract class _EnumBase {
2745
+ protected constructor(
2746
+ privateKey: symbol,
2747
+ kind: string,
2748
+ value: unknown,
2749
+ unrecognized?: UnrecognizedEnum,
2750
+ ) {
2751
+ if (privateKey !== PRIVATE_KEY) {
2752
+ throw forPrivateUseError(this);
2753
+ }
2754
+ (this as AnyRecord).kind = kind;
2755
+ (this as AnyRecord).value = value;
2756
+ if (unrecognized) {
2757
+ if (!(unrecognized instanceof UnrecognizedEnum)) {
2758
+ throw new TypeError();
2759
+ }
2760
+ (this as AnyRecord)["^"] = unrecognized;
2761
+ }
2762
+ Object.freeze(this);
2763
+ }
2764
+
2765
+ declare readonly union: {
2766
+ kind: unknown;
2767
+ value: unknown;
2768
+ };
2769
+
2770
+ toString(): string {
2771
+ return toStringImpl(this);
2772
+ }
2773
+ }
2774
+
2775
+ // The TypeScript compiler complains if we define the property within the class.
2776
+ Object.defineProperty(_EnumBase.prototype, "union", {
2777
+ get: function () {
2778
+ return this;
2779
+ },
2780
+ });
2781
+
2782
+ function toStringImpl<T>(value: T): string {
2783
+ const serializer = Object.getPrototypeOf(value).constructor
2784
+ .serializer as InternalSerializer<T>;
2785
+ return serializer.toJsonCode(value, "readable");
2786
+ }
2787
+
2788
+ // =============================================================================
2789
+ // SkirRPC services
2790
+ // =============================================================================
2791
+
2792
+ /** Metadata of an HTTP request sent by a service client. */
2793
+ export type RequestMeta = Omit<RequestInit, "body" | "method">;
2794
+
2795
+ /** Wire format used for SkirRPC HTTP request and response bodies. */
2796
+ export type TransportCodec = "legacy" | "cbor";
2797
+
2798
+ /** Configuration options for a SkirRPC service client. */
2799
+ export interface ServiceClientOptions {
2800
+ /**
2801
+ * Codec used for RPC request and response bodies.
2802
+ *
2803
+ * Defaults to `legacy`, which preserves the original Skir colon-separated
2804
+ * request body and JSON response body.
2805
+ */
2806
+ transportCodec?: TransportCodec;
2807
+ }
2808
+
2809
+ export type RawRequestBody = string | Uint8Array | ArrayBuffer;
2810
+
2811
+ function encodeCborJson(value: Json): Uint8Array {
2812
+ const encoded = encodeCbor(value);
2813
+ return new Uint8Array(encoded.buffer, encoded.byteOffset, encoded.byteLength);
2814
+ }
2815
+
2816
+ function decodeCborJson(value: Uint8Array | ArrayBuffer): Json {
2817
+ const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
2818
+ return decodeCbor(bytes) as Json;
2819
+ }
2820
+
2821
+ function normalizeHeaders(headers: RequestInit["headers"]): Headers {
2822
+ return new Headers(headers);
2823
+ }
2824
+
2825
+ /** Sends RPCs to a SkirRPC service. */
2826
+ export class ServiceClient {
2827
+ constructor(
2828
+ private readonly serviceUrl: string,
2829
+ private readonly getRequestMetadata: (
2830
+ m: Method<unknown, unknown>,
2831
+ ) => Promise<RequestMeta> | RequestMeta = (): RequestMeta => ({}),
2832
+ private readonly options: ServiceClientOptions = {},
2833
+ ) {
2834
+ const url = new URL(serviceUrl);
2835
+ if (url.search) {
2836
+ throw new Error("Service URL must not contain a query string");
2837
+ }
2838
+ }
2839
+
2840
+ /** Invokes the given method on the remote server through an RPC. */
2841
+ async invokeRemote<Request, Response>(
2842
+ method: Method<Request, Response>,
2843
+ request: Request,
2844
+ httpMethod: "GET" | "POST" = "POST",
2845
+ ): Promise<Response> {
2846
+ const requestInit: RequestInit = {
2847
+ ...(await Promise.resolve(this.getRequestMetadata(method))),
2848
+ };
2849
+ const url = new URL(this.serviceUrl);
2850
+ requestInit.method = httpMethod;
2851
+
2852
+ if (this.options.transportCodec === "cbor") {
2853
+ if (httpMethod !== "POST") {
2854
+ throw new Error("CBOR transport only supports POST requests");
2855
+ }
2856
+
2857
+ const headers = normalizeHeaders(requestInit.headers);
2858
+ headers.set("Content-Type", "application/cbor");
2859
+ headers.set("Accept", "application/cbor");
2860
+ requestInit.headers = headers;
2861
+ requestInit.body = encodeCborJson({
2862
+ method: method.name,
2863
+ request: method.requestSerializer.toJson(request, "dense"),
2864
+ });
2865
+ } else {
2866
+ const requestJson = method.requestSerializer.toJsonCode(request);
2867
+ const requestBody = [method.name, method.number, "", requestJson].join(
2868
+ ":",
2869
+ );
2870
+ if (httpMethod === "POST") {
2871
+ requestInit.body = requestBody;
2872
+ } else {
2873
+ url.search = requestBody.replace(/%/g, "%25");
2874
+ }
2875
+ }
2876
+
2877
+ const httpResponse = await fetch(url, requestInit);
2878
+ const responseData = await httpResponse.blob();
2879
+ if (httpResponse.ok) {
2880
+ if (this.options.transportCodec === "cbor") {
2881
+ return method.responseSerializer.fromJson(
2882
+ decodeCborJson(await responseData.arrayBuffer()),
2883
+ "keep-unrecognized-values",
2884
+ );
2885
+ } else {
2886
+ const jsonCode = await responseData.text();
2887
+ return method.responseSerializer.fromJsonCode(
2888
+ jsonCode,
2889
+ "keep-unrecognized-values",
2890
+ );
2891
+ }
2892
+ } else {
2893
+ let message = "";
2894
+ if (/text\/plain\b/.test(responseData.type)) {
2895
+ message = `: ${await responseData.text()}`;
2896
+ }
2897
+ throw new Error(`HTTP status ${httpResponse.status}${message}`);
2898
+ }
2899
+ }
2900
+ }
2901
+
2902
+ /** Raw response returned by the server. */
2903
+ export interface RawResponse {
2904
+ readonly data: string | Uint8Array;
2905
+ readonly statusCode: number;
2906
+ readonly contentType: string;
2907
+ }
2908
+
2909
+ function makeOkJsonResponse(data: string): RawResponse {
2910
+ return {
2911
+ data: data,
2912
+ statusCode: 200,
2913
+ contentType: "application/json",
2914
+ };
2915
+ }
2916
+
2917
+ function makeOkCborResponse(data: Json): RawResponse {
2918
+ return {
2919
+ data: encodeCborJson(data),
2920
+ statusCode: 200,
2921
+ contentType: "application/cbor",
2922
+ };
2923
+ }
2924
+
2925
+ function makeOkHtmlResponse(data: string): RawResponse {
2926
+ return {
2927
+ data: data,
2928
+ statusCode: 200,
2929
+ contentType: "text/html; charset=utf-8",
2930
+ };
2931
+ }
2932
+
2933
+ function makeBadRequestResponse(data: string): RawResponse {
2934
+ return {
2935
+ data: data,
2936
+ statusCode: 400,
2937
+ contentType: "text/plain; charset=utf-8",
2938
+ };
2939
+ }
2940
+
2941
+ function makeServerErrorResponse(data: string, statusCode = 500): RawResponse {
2942
+ return {
2943
+ data: data,
2944
+ statusCode: statusCode,
2945
+ contentType: "text/plain; charset=utf-8",
2946
+ };
2947
+ }
2948
+
2949
+ function getStudioHtml(studioAppJsUrl: string): string {
2950
+ // Copied from
2951
+ // https://github.com/gepheum/skir-studio/blob/main/index.jsdeliver.html
2952
+
2953
+ // 'studioAppJsUrl' is produced by new URL(...).toString() so it can't contain
2954
+ // any special characters that need escaping in HTML.
2955
+ return `<!DOCTYPE html>
2956
+
2957
+ <html>
2958
+ <head>
2959
+ <meta charset="utf-8" />
2960
+ <title>RPC Studio</title>
2961
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🐙</text></svg>">
2962
+ <script src="${studioAppJsUrl}"></script>
2963
+ </head>
2964
+ <body style="margin: 0; padding: 0;">
2965
+ <skir-studio-app></skir-studio-app>
2966
+ </body>
2967
+ </html>
2968
+ `;
2969
+ }
2970
+
2971
+ /**
2972
+ * If this error is thrown from a method implementation, the specified status
2973
+ * code and message will be returned in the HTTP response.
2974
+ *
2975
+ * If any other type of exception is thrown, the response status code will be
2976
+ * 500 (Internal Server Error).
2977
+ */
2978
+ export class ServiceError extends Error {
2979
+ constructor(
2980
+ private readonly spec:
2981
+ | {
2982
+ statusCode: 400;
2983
+ desc: "Bad Request";
2984
+ message?: string;
2985
+ }
2986
+ | {
2987
+ statusCode: 401;
2988
+ desc: "Unauthorized";
2989
+ message?: string;
2990
+ }
2991
+ | {
2992
+ statusCode: 402;
2993
+ desc: "Payment Required";
2994
+ message?: string;
2995
+ }
2996
+ | {
2997
+ statusCode: 403;
2998
+ desc: "Forbidden";
2999
+ message?: string;
3000
+ }
3001
+ | {
3002
+ statusCode: 404;
3003
+ desc: "Not Found";
3004
+ message?: string;
3005
+ }
3006
+ | {
3007
+ statusCode: 405;
3008
+ desc: "Method Not Allowed";
3009
+ message?: string;
3010
+ }
3011
+ | {
3012
+ statusCode: 406;
3013
+ desc: "Not Acceptable";
3014
+ message?: string;
3015
+ }
3016
+ | {
3017
+ statusCode: 407;
3018
+ desc: "Proxy Authentication Required";
3019
+ message?: string;
3020
+ }
3021
+ | {
3022
+ statusCode: 408;
3023
+ desc: "Request Timeout";
3024
+ message?: string;
3025
+ }
3026
+ | {
3027
+ statusCode: 409;
3028
+ desc: "Conflict";
3029
+ message?: string;
3030
+ }
3031
+ | {
3032
+ statusCode: 410;
3033
+ desc: "Gone";
3034
+ message?: string;
3035
+ }
3036
+ | {
3037
+ statusCode: 411;
3038
+ desc: "Length Required";
3039
+ message?: string;
3040
+ }
3041
+ | {
3042
+ statusCode: 412;
3043
+ desc: "Precondition Failed";
3044
+ message?: string;
3045
+ }
3046
+ | {
3047
+ statusCode: 413;
3048
+ desc: "Content Too Large";
3049
+ message?: string;
3050
+ }
3051
+ | {
3052
+ statusCode: 414;
3053
+ desc: "URI Too Long";
3054
+ message?: string;
3055
+ }
3056
+ | {
3057
+ statusCode: 415;
3058
+ desc: "Unsupported Media Type";
3059
+ message?: string;
3060
+ }
3061
+ | {
3062
+ statusCode: 416;
3063
+ desc: "Range Not Satisfiable";
3064
+ message?: string;
3065
+ }
3066
+ | {
3067
+ statusCode: 417;
3068
+ desc: "Expectation Failed";
3069
+ message?: string;
3070
+ }
3071
+ | {
3072
+ statusCode: 418;
3073
+ desc: "I'm a teapot";
3074
+ message?: string;
3075
+ }
3076
+ | {
3077
+ statusCode: 421;
3078
+ desc: "Misdirected Request";
3079
+ message?: string;
3080
+ }
3081
+ | {
3082
+ statusCode: 422;
3083
+ desc: "Unprocessable Content";
3084
+ message?: string;
3085
+ }
3086
+ | {
3087
+ statusCode: 423;
3088
+ desc: "Locked";
3089
+ message?: string;
3090
+ }
3091
+ | {
3092
+ statusCode: 424;
3093
+ desc: "Failed Dependency";
3094
+ message?: string;
3095
+ }
3096
+ | {
3097
+ statusCode: 425;
3098
+ desc: "Too Early";
3099
+ message?: string;
3100
+ }
3101
+ | {
3102
+ statusCode: 426;
3103
+ desc: "Upgrade Required";
3104
+ message?: string;
3105
+ }
3106
+ | {
3107
+ statusCode: 428;
3108
+ desc: "Precondition Required";
3109
+ message?: string;
3110
+ }
3111
+ | {
3112
+ statusCode: 429;
3113
+ desc: "Too Many Requests";
3114
+ message?: string;
3115
+ }
3116
+ | {
3117
+ statusCode: 431;
3118
+ desc: "Request Header Fields Too Large";
3119
+ message?: string;
3120
+ }
3121
+ | {
3122
+ statusCode: 451;
3123
+ desc: "Unavailable For Legal Reasons";
3124
+ message?: string;
3125
+ }
3126
+ | {
3127
+ statusCode: 500;
3128
+ desc: "Internal Server Error";
3129
+ message?: string;
3130
+ }
3131
+ | {
3132
+ statusCode: 501;
3133
+ desc: "Not Implemented";
3134
+ message?: string;
3135
+ }
3136
+ | {
3137
+ statusCode: 502;
3138
+ desc: "Bad Gateway";
3139
+ message?: string;
3140
+ }
3141
+ | {
3142
+ statusCode: 503;
3143
+ desc: "Service Unavailable";
3144
+ message?: string;
3145
+ }
3146
+ | {
3147
+ statusCode: 504;
3148
+ desc: "Gateway Timeout";
3149
+ message?: string;
3150
+ }
3151
+ | {
3152
+ statusCode: 505;
3153
+ desc: "HTTP Version Not Supported";
3154
+ message?: string;
3155
+ }
3156
+ | {
3157
+ statusCode: 506;
3158
+ desc: "Variant Also Negotiates";
3159
+ message?: string;
3160
+ }
3161
+ | {
3162
+ statusCode: 507;
3163
+ desc: "Insufficient Storage";
3164
+ message?: string;
3165
+ }
3166
+ | {
3167
+ statusCode: 508;
3168
+ desc: "Loop Detected";
3169
+ message?: string;
3170
+ }
3171
+ | {
3172
+ statusCode: 510;
3173
+ desc: "Not Extended";
3174
+ message?: string;
3175
+ }
3176
+ | {
3177
+ statusCode: 511;
3178
+ desc: "Network Authentication Required";
3179
+ message?: string;
3180
+ },
3181
+ ) {
3182
+ super(spec.message ?? spec.desc);
3183
+ }
3184
+
3185
+ toRawResponse(): RawResponse {
3186
+ return makeServerErrorResponse(
3187
+ this.spec.message ?? this.spec.desc,
3188
+ this.spec.statusCode,
3189
+ );
3190
+ }
3191
+ }
3192
+
3193
+ export interface RequestHandler<RequestMeta = ExpressRequest> {
3194
+ /**
3195
+ * Parses the content of a user request and invokes the appropriate method.
3196
+ * If you are using ExpressJS as your web application framework, you don't
3197
+ * need to call this method, you can simply call the
3198
+ * `installServiceOnExpressApp()` top-level function.
3199
+ *
3200
+ * If the request is a GET request, pass in the decoded query string as the
3201
+ * request's body. The query string is the part of the URL after '?', and it
3202
+ * can be decoded with DecodeURIComponent.
3203
+ */
3204
+ handleRequest(
3205
+ reqBody: RawRequestBody,
3206
+ reqMeta: RequestMeta,
3207
+ ): Promise<RawResponse>;
3208
+ }
3209
+
3210
+ /** Configuration options for a SkirRPC service. */
3211
+ export interface ServiceOptions<RequestMeta = ExpressRequest> {
3212
+ /**
3213
+ * Codec used for RPC request and response bodies.
3214
+ *
3215
+ * Defaults to `legacy`, which accepts the original Skir colon-separated
3216
+ * string and JSON-object request formats and returns JSON responses.
3217
+ */
3218
+ transportCodec: TransportCodec;
3219
+
3220
+ /**
3221
+ * Whether to keep unrecognized values when deserializing requests.
3222
+ *
3223
+ * **WARNING:** Only enable this for data from trusted sources. Malicious
3224
+ * actors could inject fields with IDs not yet defined in your schema. If you
3225
+ * preserve this data and later define those IDs in a future schema version,
3226
+ * the injected data could be deserialized as valid fields, leading to
3227
+ * security vulnerabilities or data corruption.
3228
+ *
3229
+ * Defaults to `false`.
3230
+ */
3231
+ keepUnrecognizedValues: boolean;
3232
+
3233
+ /**
3234
+ * Whether the message of an unknown error (i.e. not a `ServiceError`) can be
3235
+ * sent to the client in the response body, which can help with debugging.
3236
+ *
3237
+ * By default, unknown errors are masked and the client receives a generic
3238
+ * 'server error' message with status 500. This is to prevent leaking
3239
+ * sensitive information to the client.
3240
+ *
3241
+ * You can enable this if your server is internal or if you are sure that your
3242
+ * error messages are safe to expose. By passing a predicate instead of true
3243
+ * or false, you can control on a per-error basis whether to expose the error
3244
+ * message; for example, you can send error messages only if the user is an
3245
+ * admin.
3246
+ *
3247
+ * Defaults to `false`.
3248
+ */
3249
+ canSendUnknownErrorMessage:
3250
+ | boolean
3251
+ | ((errorInfo: MethodErrorInfo<RequestMeta, unknown>) => boolean);
3252
+
3253
+ /**
3254
+ * Callback invoked whenever an error is thrown during method execution.
3255
+ *
3256
+ * Use this to log errors for monitoring, debugging, or alerting purposes.
3257
+ *
3258
+ * Defaults to function which logs the method name and error message via
3259
+ * `console.error()`.
3260
+ */
3261
+ errorLogger: (errorInfo: MethodErrorInfo<RequestMeta, unknown>) => void;
3262
+
3263
+ /**
3264
+ * URL to the JavaScript file for the Skir Studio app.
3265
+ *
3266
+ * Skir Studio is a web interface for exploring and testing your SkirRPC service.
3267
+ * It is served when the service receives a request at '${serviceUrl}?studio'.
3268
+ */
3269
+ studioAppJsUrl: string;
3270
+ }
3271
+
3272
+ /**
3273
+ * Information about an error thrown during the execution of a method on the
3274
+ * server side.
3275
+ */
3276
+ interface MethodErrorInfo<RequestMeta, Request> {
3277
+ /** The error thrown during the execution of the method. */
3278
+ readonly error: any;
3279
+ readonly method: Method<Request, unknown>;
3280
+ readonly request: Request;
3281
+ /**
3282
+ * Metadata coming from the HTTP headers of the request.
3283
+ * Undefined if the error was thrown in the execution of the function passed
3284
+ * to `withMetaTransformer()`.
3285
+ */
3286
+ readonly reqMeta: RequestMeta | undefined;
3287
+ }
3288
+
3289
+ /**
3290
+ * Implementation of a SkirRPC service.
3291
+ *
3292
+ * Usage: call `.addMethod()` to register methods, then install the service on
3293
+ * an HTTP server either by:
3294
+ * - calling the `installServiceOnExpressApp()` top-level function if you are
3295
+ * using ExpressJS
3296
+ * - writing your own implementation of `installServiceOn*()` which calls
3297
+ * `.handleRequest()` if you are using another web application framework
3298
+ *
3299
+ * ## Handling Request Metadata
3300
+ *
3301
+ * The `RequestMeta` type parameter specifies what metadata (authentication,
3302
+ * headers, etc.) your method implementations receive. There are two approaches:
3303
+ *
3304
+ * ### Approach 1: Use the framework's request type directly
3305
+ *
3306
+ * Set `RequestMeta` to your framework's request type (e.g., `ExpressRequest`).
3307
+ * All method implementations will receive the full framework request object.
3308
+ *
3309
+ * ```typescript
3310
+ * const service = new Service<ExpressRequest>();
3311
+ * service.addMethod(myMethod, async (req, expressReq) => {
3312
+ * const isAdmin = expressReq.user?.role === 'admin';
3313
+ * // ...
3314
+ * });
3315
+ * installServiceOnExpressApp(app, '/api', service, text, json);
3316
+ * ```
3317
+ *
3318
+ * ### Approach 2: Use a simplified custom type
3319
+ *
3320
+ * Set `RequestMeta` to a minimal type containing only what your service needs.
3321
+ * Use `withMetaTransformer()` to extract this data from the framework request
3322
+ * when installing the service.
3323
+ *
3324
+ * ```typescript
3325
+ * const service = new Service<{ isAdmin: boolean }>();
3326
+ * service.addMethod(myMethod, async (req, { isAdmin }) => {
3327
+ * // Implementation is framework-agnostic and easy to unit test
3328
+ * if (!isAdmin) throw new ServiceError({ statusCode: 403, desc: "Forbidden" });
3329
+ * // ...
3330
+ * });
3331
+ *
3332
+ * // Adapt to Express when installing
3333
+ * const handler = service.withMetaTransformer((req: ExpressRequest) => ({
3334
+ * isAdmin: req.user?.role === 'admin'
3335
+ * }));
3336
+ * installServiceOnExpressApp(app, '/api', handler, text, json);
3337
+ * ```
3338
+ *
3339
+ * This approach decouples your service from the HTTP framework, making it easier
3340
+ * to test and clearer about what request data it actually uses.
3341
+ */
3342
+ export class Service<RequestMeta = ExpressRequest>
3343
+ implements RequestHandler<RequestMeta>
3344
+ {
3345
+ constructor(options?: Partial<ServiceOptions<RequestMeta>>) {
3346
+ this.options = {
3347
+ transportCodec:
3348
+ options?.transportCodec ?? DEFAULT_SERVICE_OPTIONS.transportCodec,
3349
+ keepUnrecognizedValues:
3350
+ options?.keepUnrecognizedValues ??
3351
+ DEFAULT_SERVICE_OPTIONS.keepUnrecognizedValues,
3352
+ canSendUnknownErrorMessage:
3353
+ options?.canSendUnknownErrorMessage ??
3354
+ DEFAULT_SERVICE_OPTIONS.canSendUnknownErrorMessage,
3355
+ errorLogger: options?.errorLogger ?? DEFAULT_SERVICE_OPTIONS.errorLogger,
3356
+ studioAppJsUrl: new URL(
3357
+ options?.studioAppJsUrl ?? DEFAULT_SERVICE_OPTIONS.studioAppJsUrl,
3358
+ ).toString(),
3359
+ };
3360
+ }
3361
+
3362
+ addMethod<Request, Response>(
3363
+ method: Method<Request, Response>,
3364
+ impl: (req: Request, reqMeta: RequestMeta) => Promise<Response>,
3365
+ ): Service<RequestMeta> {
3366
+ const { number } = method;
3367
+ if (this.methodImpls[number]) {
3368
+ throw new Error(
3369
+ `Method with the same number already registered (${number})`,
3370
+ );
3371
+ }
3372
+ this.methodImpls[number] = {
3373
+ method: method,
3374
+ impl: impl,
3375
+ } as MethodImpl<unknown, unknown, RequestMeta>;
3376
+ return this;
3377
+ }
3378
+
3379
+ async handleRequest(
3380
+ reqBody: RawRequestBody,
3381
+ reqMeta: RequestMeta,
3382
+ ): Promise<RawResponse> {
3383
+ return this.doHandleRequest(reqBody, reqMeta, (m) => m);
3384
+ }
3385
+
3386
+ private async doHandleRequest<OriginalRequestMeta>(
3387
+ reqBody: RawRequestBody,
3388
+ originalReqMeta: OriginalRequestMeta,
3389
+ transformReqMeta: (
3390
+ m: OriginalRequestMeta,
3391
+ ) => RequestMeta | Promise<RequestMeta>,
3392
+ ): Promise<RawResponse> {
3393
+ if (typeof reqBody === "string" && reqBody === "list") {
3394
+ const json = {
3395
+ methods: Object.values(this.methodImpls).map((methodImpl) => ({
3396
+ method: methodImpl.method.name,
3397
+ number: methodImpl.method.name,
3398
+ request: methodImpl.method.requestSerializer.typeDescriptor.asJson(),
3399
+ response:
3400
+ methodImpl.method.responseSerializer.typeDescriptor.asJson(),
3401
+ doc: emptyToUndefined(methodImpl.method.doc),
3402
+ })),
3403
+ };
3404
+ const jsonCode = JSON.stringify(json, undefined, " ");
3405
+ return makeOkJsonResponse(jsonCode);
3406
+ } else if (
3407
+ typeof reqBody === "string" &&
3408
+ (reqBody === "" || reqBody === "studio")
3409
+ ) {
3410
+ const studioHtml = getStudioHtml(this.options.studioAppJsUrl);
3411
+ return makeOkHtmlResponse(studioHtml);
3412
+ }
3413
+
3414
+ // Parse request
3415
+ let methodName: string;
3416
+ let methodNumber: number | undefined;
3417
+ let format: string;
3418
+ let requestData: ["json", Json] | ["json-code", string];
3419
+
3420
+ if (this.options.transportCodec === "cbor") {
3421
+ if (typeof reqBody === "string") {
3422
+ return makeBadRequestResponse("bad request: invalid CBOR");
3423
+ }
3424
+
3425
+ let reqBodyJson: Json;
3426
+ try {
3427
+ reqBodyJson = decodeCborJson(reqBody);
3428
+ } catch (_e) {
3429
+ return makeBadRequestResponse("bad request: invalid CBOR");
3430
+ }
3431
+
3432
+ if (
3433
+ typeof reqBodyJson !== "object" ||
3434
+ reqBodyJson === null ||
3435
+ Array.isArray(reqBodyJson)
3436
+ ) {
3437
+ return makeBadRequestResponse("bad request: CBOR body must be a map");
3438
+ }
3439
+
3440
+ const methodField = (reqBodyJson as AnyRecord)["method"];
3441
+ if (methodField === undefined) {
3442
+ return makeBadRequestResponse(
3443
+ "bad request: missing 'method' field in CBOR",
3444
+ );
3445
+ }
3446
+ if (typeof methodField === "string") {
3447
+ methodName = methodField;
3448
+ methodNumber = undefined;
3449
+ } else if (typeof methodField === "number") {
3450
+ methodName = "?";
3451
+ methodNumber = methodField;
3452
+ } else {
3453
+ return makeBadRequestResponse(
3454
+ "bad request: 'method' field must be a string or a number",
3455
+ );
3456
+ }
3457
+ format = "cbor";
3458
+ const requestField = (reqBodyJson as AnyRecord)["request"];
3459
+ if (requestField === undefined) {
3460
+ return makeBadRequestResponse(
3461
+ "bad request: missing 'request' field in CBOR",
3462
+ );
3463
+ }
3464
+ requestData = ["json", requestField as Json];
3465
+ } else if (typeof reqBody !== "string") {
3466
+ return makeBadRequestResponse("bad request: invalid request format");
3467
+ } else {
3468
+ const firstChar = reqBody.charAt(0);
3469
+ if (/\s/.test(firstChar) || firstChar === "{") {
3470
+ // A JSON object
3471
+ let reqBodyJson: Json;
3472
+ try {
3473
+ reqBodyJson = JSON.parse(reqBody);
3474
+ } catch (_e) {
3475
+ return makeBadRequestResponse("bad request: invalid JSON");
3476
+ }
3477
+ const methodField = (reqBodyJson as AnyRecord)["method"];
3478
+ if (methodField === undefined) {
3479
+ return makeBadRequestResponse(
3480
+ "bad request: missing 'method' field in JSON",
3481
+ );
3482
+ }
3483
+ if (typeof methodField === "string") {
3484
+ methodName = methodField;
3485
+ methodNumber = undefined;
3486
+ } else if (typeof methodField === "number") {
3487
+ methodName = "?";
3488
+ methodNumber = methodField;
3489
+ } else {
3490
+ return makeBadRequestResponse(
3491
+ "bad request: 'method' field must be a string or a number",
3492
+ );
3493
+ }
3494
+ format = "readable";
3495
+ const requestField = (reqBodyJson as AnyRecord)["request"];
3496
+ if (requestField === undefined) {
3497
+ return makeBadRequestResponse(
3498
+ "bad request: missing 'request' field in JSON",
3499
+ );
3500
+ }
3501
+ requestData = ["json", requestField as Json];
3502
+ } else {
3503
+ // A colon-separated string
3504
+ const match = reqBody.match(/^([^:]*):([^:]*):([^:]*):([\S\s]*)$/);
3505
+ if (!match) {
3506
+ return makeBadRequestResponse("bad request: invalid request format");
3507
+ }
3508
+ methodName = match[1]!;
3509
+ const methodNumberStr = match[2]!;
3510
+ format = match[3]!;
3511
+ requestData = ["json-code", match[4]!];
3512
+
3513
+ if (methodNumberStr) {
3514
+ if (!/^-?[0-9]+$/.test(methodNumberStr)) {
3515
+ return makeBadRequestResponse(
3516
+ "bad request: can't parse method number",
3517
+ );
3518
+ }
3519
+ methodNumber = parseInt(methodNumberStr);
3520
+ } else {
3521
+ methodNumber = undefined;
3522
+ }
3523
+ }
3524
+ }
3525
+
3526
+ // Look up method by number or name
3527
+ if (methodNumber === undefined) {
3528
+ // Try to get the method number by name
3529
+ const allMethods = Object.values(this.methodImpls);
3530
+ const nameMatches = allMethods.filter(
3531
+ (m) => m.method.name === methodName,
3532
+ );
3533
+ if (nameMatches.length === 0) {
3534
+ return makeBadRequestResponse(
3535
+ `bad request: method not found: ${methodName}`,
3536
+ );
3537
+ } else if (nameMatches.length > 1) {
3538
+ return makeBadRequestResponse(
3539
+ `bad request: method name '${methodName}' is ambiguous; use method number instead`,
3540
+ );
3541
+ }
3542
+ methodNumber = nameMatches[0]!.method.number;
3543
+ }
3544
+
3545
+ const methodImpl = this.methodImpls[methodNumber];
3546
+ if (!methodImpl) {
3547
+ return makeBadRequestResponse(
3548
+ `bad request: method not found: ${methodName}; number: ${methodNumber}`,
3549
+ );
3550
+ }
3551
+
3552
+ let req: unknown;
3553
+ try {
3554
+ if (requestData[0] == "json") {
3555
+ req = methodImpl.method.requestSerializer.fromJson(
3556
+ requestData[1],
3557
+ this.options.keepUnrecognizedValues
3558
+ ? "keep-unrecognized-values"
3559
+ : undefined,
3560
+ );
3561
+ } else {
3562
+ req = methodImpl.method.requestSerializer.fromJsonCode(
3563
+ requestData[1],
3564
+ this.options.keepUnrecognizedValues
3565
+ ? "keep-unrecognized-values"
3566
+ : undefined,
3567
+ );
3568
+ }
3569
+ } catch (e) {
3570
+ return makeBadRequestResponse(`bad request: can't parse JSON: ${e}`);
3571
+ }
3572
+
3573
+ let res: unknown;
3574
+ let reqMeta: RequestMeta | undefined;
3575
+ try {
3576
+ reqMeta = await Promise.resolve(transformReqMeta(originalReqMeta));
3577
+ res = await methodImpl.impl(req, reqMeta);
3578
+ } catch (e) {
3579
+ const errorInfo: MethodErrorInfo<RequestMeta, unknown> = {
3580
+ error: e,
3581
+ method: methodImpl.method,
3582
+ request: req,
3583
+ reqMeta: reqMeta,
3584
+ };
3585
+ this.options.errorLogger(errorInfo);
3586
+ if (e instanceof ServiceError) {
3587
+ return e.toRawResponse();
3588
+ } else {
3589
+ let { canSendUnknownErrorMessage } = this.options;
3590
+ if (typeof canSendUnknownErrorMessage !== "boolean") {
3591
+ canSendUnknownErrorMessage = canSendUnknownErrorMessage(errorInfo);
3592
+ }
3593
+ const message = canSendUnknownErrorMessage
3594
+ ? `server error: ${e}`
3595
+ : "server error";
3596
+ return makeServerErrorResponse(message);
3597
+ }
3598
+ }
3599
+
3600
+ let resJson: Json | string;
3601
+ try {
3602
+ const flavor = format === "readable" ? "readable" : "dense";
3603
+ resJson =
3604
+ format === "cbor"
3605
+ ? methodImpl.method.responseSerializer.toJson(res, "dense")
3606
+ : methodImpl.method.responseSerializer.toJsonCode(res, flavor);
3607
+ } catch (e) {
3608
+ return makeServerErrorResponse(
3609
+ `server error: can't serialize response to JSON: ${e}`,
3610
+ );
3611
+ }
3612
+
3613
+ return format === "cbor"
3614
+ ? makeOkCborResponse(resJson as Json)
3615
+ : makeOkJsonResponse(resJson as string);
3616
+ }
3617
+
3618
+ /**
3619
+ * Creates a request handler that extracts simplified request metadata from
3620
+ * framework-specific request objects before passing it to this service.
3621
+ *
3622
+ * This decouples your service implementation from the HTTP framework, making
3623
+ * it easier to unit test (tests don't need to mock framework objects) and
3624
+ * making the service implementation clearer by explicitly declaring exactly
3625
+ * what request data it needs.
3626
+ *
3627
+ * @param transformFn Function that extracts the necessary data from the
3628
+ * framework-specific request object. Can be async or sync.
3629
+ * @returns A request handler that accepts the framework-specific request type.
3630
+ *
3631
+ * @example
3632
+ * ```typescript
3633
+ * // Define a service that only needs to know if the user is an admin
3634
+ *
3635
+ * const service = new Service<{ isAdmin: boolean }>();
3636
+ *
3637
+ * service.addMethod(myMethod, async (req, { isAdmin }) => {
3638
+ * // Implementation is framework-agnostic and easy to test
3639
+ * if (!isAdmin) throw new ServiceError({ statusCode: 403, desc: "Forbidden" });
3640
+ * // ...
3641
+ * });
3642
+ *
3643
+ * // Adapt it to work with Express
3644
+ * const expressHandler = service.withMetaTransformer(
3645
+ * (req: ExpressRequest) => ({
3646
+ * isAdmin: req.user?.role === 'admin'
3647
+ * })
3648
+ * );
3649
+ * installServiceOnExpressApp(app, '/api', expressHandler, text, json);
3650
+ * ```
3651
+ */
3652
+ withMetaTransformer<OriginalRequestMeta>(
3653
+ transformFn: (
3654
+ reqMeta: OriginalRequestMeta,
3655
+ ) => RequestMeta | Promise<RequestMeta>,
3656
+ ): RequestHandler<OriginalRequestMeta> {
3657
+ return {
3658
+ handleRequest: async (
3659
+ reqBody: RawRequestBody,
3660
+ reqMeta: OriginalRequestMeta,
3661
+ ): Promise<RawResponse> => {
3662
+ return this.doHandleRequest(reqBody, reqMeta, transformFn);
3663
+ },
3664
+ };
3665
+ }
3666
+
3667
+ private readonly options: ServiceOptions<RequestMeta>;
3668
+ private readonly methodImpls: {
3669
+ [number: number]: MethodImpl<unknown, unknown, RequestMeta>;
3670
+ } = {};
3671
+ }
3672
+
3673
+ const DEFAULT_SERVICE_OPTIONS: ServiceOptions<unknown> = {
3674
+ transportCodec: "legacy",
3675
+ keepUnrecognizedValues: false,
3676
+ canSendUnknownErrorMessage: false,
3677
+ errorLogger: (errorInfo: MethodErrorInfo<unknown, unknown>) => {
3678
+ console.error(`Error in method ${errorInfo.method.name}:`, errorInfo.error);
3679
+ },
3680
+ studioAppJsUrl:
3681
+ "https://cdn.jsdelivr.net/npm/skir-studio/dist/skir-studio-standalone.js",
3682
+ };
3683
+
3684
+ interface MethodImpl<Request, Response, RequestMeta> {
3685
+ method: Method<Request, Response>;
3686
+ impl: (req: Request, reqMeta: RequestMeta) => Promise<Response>;
3687
+ }
3688
+
3689
+ export function installServiceOnExpressApp(
3690
+ app: ExpressApp,
3691
+ queryPath: string,
3692
+ service: RequestHandler<ExpressRequest>,
3693
+ text: typeof ExpressText,
3694
+ json: typeof ExpressJson,
3695
+ raw?: typeof ExpressRaw,
3696
+ ): void {
3697
+ const callback = async (
3698
+ req: ExpressRequest,
3699
+ res: ExpressResponse,
3700
+ ): Promise<void> => {
3701
+ let body: RawRequestBody;
3702
+ const indexOfQuestionMark = req.originalUrl.indexOf("?");
3703
+ if (indexOfQuestionMark >= 0) {
3704
+ const queryString = req.originalUrl.substring(indexOfQuestionMark + 1);
3705
+ body = decodeURIComponent(queryString);
3706
+ } else {
3707
+ body =
3708
+ req.body instanceof Uint8Array
3709
+ ? req.body
3710
+ : typeof req.body === "string"
3711
+ ? req.body
3712
+ : typeof req.body === "object"
3713
+ ? JSON.stringify(req.body)
3714
+ : "";
3715
+ }
3716
+ const rawResponse = await service.handleRequest(body, req);
3717
+ res
3718
+ .status(rawResponse.statusCode)
3719
+ .contentType(rawResponse.contentType)
3720
+ .send(rawResponse.data);
3721
+ };
3722
+ app.get(queryPath, callback);
3723
+ const middleware = raw
3724
+ ? [text(), json(), raw({ type: "application/cbor" }), callback]
3725
+ : [text(), json(), callback];
3726
+ app.post(queryPath, ...middleware);
3727
+ }
3728
+
3729
+ // =============================================================================
3730
+ // Module classes initialization
3731
+ // =============================================================================
3732
+
3733
+ interface StructSpec {
3734
+ kind: "struct";
3735
+ ctor: { new (privateKey: symbol): unknown };
3736
+ initFn: (target: unknown, initializer: unknown) => void;
3737
+ name: string;
3738
+ parentCtor?: { new (): unknown };
3739
+ doc?: string;
3740
+ fields: readonly StructFieldSpec[];
3741
+ removedNumbers?: readonly number[];
3742
+ }
3743
+
3744
+ interface StructFieldSpec {
3745
+ name: string;
3746
+ property: string;
3747
+ number: number;
3748
+ type: TypeSpec;
3749
+ doc?: string;
3750
+ mutableGetter?: string;
3751
+ indexable?: IndexableSpec;
3752
+ }
3753
+
3754
+ interface IndexableSpec {
3755
+ searchMethod: string;
3756
+ keyFn: (v: unknown) => unknown;
3757
+ keyToHashable?: (v: unknown) => unknown;
3758
+ }
3759
+
3760
+ interface EnumSpec<Enum = unknown> {
3761
+ kind: "enum";
3762
+ ctor: {
3763
+ new (
3764
+ privateKey: symbol,
3765
+ kind: string,
3766
+ value?: unknown,
3767
+ unrecognized?: UnrecognizedEnum,
3768
+ ): Enum;
3769
+ };
3770
+ createValueFn?: (initializer: unknown) => unknown;
3771
+ name: string;
3772
+ parentCtor?: { new (): unknown };
3773
+ doc?: string;
3774
+ variants: EnumVariantSpec[];
3775
+ removedNumbers?: readonly number[];
3776
+ }
3777
+
3778
+ interface EnumVariantSpec {
3779
+ name: string;
3780
+ number: number;
3781
+ type?: TypeSpec;
3782
+ doc?: string;
3783
+ }
3784
+
3785
+ type TypeSpec =
3786
+ | {
3787
+ kind: "optional";
3788
+ other: TypeSpec;
3789
+ }
3790
+ | {
3791
+ kind: "array";
3792
+ item: TypeSpec;
3793
+ keyChain?: string;
3794
+ }
3795
+ | {
3796
+ kind: "record";
3797
+ ctor: { new (): unknown };
3798
+ }
3799
+ | {
3800
+ kind: "primitive";
3801
+ primitive: keyof PrimitiveTypes;
3802
+ };
3803
+
3804
+ // The UNKNOWN variant is common to all enums.
3805
+ const UNKNOWN_VARIANT_SPEC: EnumVariantSpec = {
3806
+ name: "UNKNOWN",
3807
+ number: 0,
3808
+ };
3809
+
3810
+ export function _initModuleClasses(
3811
+ modulePath: string,
3812
+ records: ReadonlyArray<StructSpec | EnumSpec>,
3813
+ ): void {
3814
+ const privateKey = PRIVATE_KEY;
3815
+
3816
+ // First loop: add a serializer property to every record class.
3817
+ for (const record of records) {
3818
+ const clazz = record.ctor as unknown as AnyRecord;
3819
+ switch (record.kind) {
3820
+ case "struct": {
3821
+ const { ctor, initFn } = record;
3822
+ // Create the DEFAULT value. It will be initialized in a second loop.
3823
+ // To see why we can't initialize it in the first loop, consider this
3824
+ // example:
3825
+ // struct Foo { bar: Bar; }
3826
+ // struct Bar { foo: Foo; }
3827
+ // The default value for Foo must contain a reference to the default
3828
+ // value for Bar, and the default value for Bar also needs to contain
3829
+ // a reference to the default value for Foo.
3830
+ clazz.DEFAULT = new ctor(privateKey);
3831
+ // Expose the mutable class as a static property of the frozen class.
3832
+ const mutableCtor = makeMutableClassForRecord(record, clazz.DEFAULT);
3833
+ clazz.Mutable = mutableCtor;
3834
+ // Define the 'create' static factory function.
3835
+ const createFn = (initializer: unknown): unknown => {
3836
+ if (initializer instanceof ctor) {
3837
+ return initializer;
3838
+ }
3839
+ const ret = new ctor(privateKey);
3840
+ initFn(ret, initializer);
3841
+ if ((initializer as AnyRecord)["^"]) {
3842
+ (ret as AnyRecord)["^"] = (initializer as AnyRecord)["^"];
3843
+ }
3844
+ return Object.freeze(ret);
3845
+ };
3846
+ clazz.create = createFn;
3847
+ // Create the serializer. It will be initialized in a second loop.
3848
+ clazz.serializer = new StructSerializerImpl(
3849
+ clazz.DEFAULT,
3850
+ createFn as (initializer: AnyRecord) => unknown,
3851
+ () => new mutableCtor() as Freezable<unknown>,
3852
+ );
3853
+ break;
3854
+ }
3855
+ case "enum": {
3856
+ // Create the constants.
3857
+ // Prepend the UNKNOWN variant to the array of variants specified from the
3858
+ // generated code.
3859
+ record.variants = [UNKNOWN_VARIANT_SPEC].concat(record.variants);
3860
+ for (const variant of record.variants) {
3861
+ if (variant.type) {
3862
+ continue;
3863
+ }
3864
+ const property = variant.name;
3865
+ clazz[property] = new record.ctor(PRIVATE_KEY, variant.name);
3866
+ }
3867
+ // Define the 'create' static factory function.
3868
+ const createFn = makeCreateEnumFunction(record);
3869
+ clazz.create = createFn;
3870
+ // Create the serializer. It will be initialized in a second loop.
3871
+ clazz.serializer = new EnumSerializerImpl(createFn);
3872
+ break;
3873
+ }
3874
+ }
3875
+ // If the record is nested, expose the record class as a static property of
3876
+ // the parent class.
3877
+ if (record.parentCtor) {
3878
+ (record.parentCtor as unknown as AnyRecord)[record.name] = record.ctor;
3879
+ }
3880
+ }
3881
+
3882
+ // Second loop: initialize the serializer of every record, initialize the
3883
+ // default value of every struct, and freeze every class so new properties
3884
+ // can't be added to it.
3885
+ for (const record of records) {
3886
+ const clazz = record.ctor as unknown as AnyRecord;
3887
+ const parentTypeDescriptor = (record.parentCtor as unknown as AnyRecord)
3888
+ ?.serializer as StructDescriptor | EnumDescriptor | undefined;
3889
+ switch (record.kind) {
3890
+ case "struct": {
3891
+ // Initializer serializer.
3892
+ const fields = record.fields.map(
3893
+ (f) =>
3894
+ new StructFieldImpl(
3895
+ f.name,
3896
+ f.property,
3897
+ f.number,
3898
+ getSerializerForType(f.type) as InternalSerializer,
3899
+ f.doc ?? "",
3900
+ ),
3901
+ );
3902
+ const serializer = clazz.serializer as StructSerializerImpl;
3903
+ serializer.init(
3904
+ record.name,
3905
+ modulePath,
3906
+ parentTypeDescriptor,
3907
+ record.doc ?? "",
3908
+ fields,
3909
+ record.removedNumbers ?? [],
3910
+ );
3911
+ // Initialize DEFAULT.
3912
+ const { DEFAULT } = clazz;
3913
+ record.initFn(DEFAULT as AnyRecord, {});
3914
+ Object.freeze(DEFAULT);
3915
+ // Define the mutable getters in the Mutable class.
3916
+ const mutableCtor = clazz.Mutable as new (i: unknown) => unknown;
3917
+ for (const field of record.fields) {
3918
+ if (field.mutableGetter) {
3919
+ Object.defineProperty(mutableCtor.prototype, field.mutableGetter, {
3920
+ get: makeMutableGetterFn(field),
3921
+ });
3922
+ }
3923
+ }
3924
+ // Define the search methods in the frozen class.
3925
+ for (const field of record.fields) {
3926
+ if (field.indexable) {
3927
+ record.ctor.prototype[field.indexable.searchMethod] =
3928
+ makeSearchMethod(field);
3929
+ }
3930
+ }
3931
+ // Freeze the frozen class and the mutable class.
3932
+ Object.freeze(record.ctor);
3933
+ Object.freeze(record.ctor.prototype);
3934
+ Object.freeze(clazz.Mutable);
3935
+ Object.freeze((clazz.Mutable as AnyRecord).prototype);
3936
+ break;
3937
+ }
3938
+ case "enum": {
3939
+ const serializer = clazz.serializer as EnumSerializerImpl;
3940
+ const variants = record.variants.map((f) =>
3941
+ f.type
3942
+ ? new EnumWrapperVariantImpl(
3943
+ f.name,
3944
+ f.number,
3945
+ getSerializerForType(f.type) as InternalSerializer,
3946
+ f.doc ?? "",
3947
+ serializer.createFn,
3948
+ )
3949
+ : {
3950
+ name: f.name,
3951
+ number: f.number,
3952
+ constant: clazz[f.name],
3953
+ doc: f.doc ?? "",
3954
+ },
3955
+ );
3956
+ serializer.init(
3957
+ record.name,
3958
+ modulePath,
3959
+ parentTypeDescriptor,
3960
+ record.doc ?? "",
3961
+ variants,
3962
+ record.removedNumbers ?? [],
3963
+ );
3964
+ // Freeze the enum class.
3965
+ Object.freeze(record.ctor);
3966
+ Object.freeze(record.ctor.prototype);
3967
+ break;
3968
+ }
3969
+ }
3970
+ }
3971
+ }
3972
+
3973
+ function makeCreateEnumFunction(
3974
+ enumSpec: EnumSpec,
3975
+ ): (initializer: unknown) => unknown {
3976
+ const { ctor, createValueFn } = enumSpec;
3977
+ const createValue = createValueFn || ((): undefined => undefined);
3978
+ const privateKey = PRIVATE_KEY;
3979
+ return (initializer: unknown) => {
3980
+ if (initializer instanceof ctor) {
3981
+ return initializer;
3982
+ }
3983
+ if (typeof initializer === "string") {
3984
+ const maybeResult = (ctor as unknown as AnyRecord)[initializer];
3985
+ if (maybeResult instanceof ctor) {
3986
+ return maybeResult;
3987
+ }
3988
+ throw new Error(`Constant not found: ${initializer}`);
3989
+ }
3990
+ if (initializer instanceof UnrecognizedEnum) {
3991
+ return new ctor(privateKey, "UNKNOWN", undefined, initializer);
3992
+ }
3993
+ const kind = (initializer as { kind: string }).kind;
3994
+ if (kind === undefined) {
3995
+ throw new Error("Missing entry: kind");
3996
+ }
3997
+ const unrecognized = (initializer as AnyRecord)["^"] as
3998
+ | UnrecognizedEnum
3999
+ | undefined;
4000
+ const value = createValue(initializer);
4001
+ if (value === undefined) {
4002
+ const maybeConstant = (ctor as unknown as AnyRecord)[kind];
4003
+ if (!(maybeConstant instanceof ctor)) {
4004
+ throw new Error(`Wrapper variant not found: ${kind}`);
4005
+ }
4006
+ return unrecognized
4007
+ ? new ctor(privateKey, kind, undefined, unrecognized)
4008
+ : maybeConstant;
4009
+ }
4010
+ return new ctor(privateKey, kind, value, unrecognized);
4011
+ };
4012
+ }
4013
+
4014
+ function makeMutableClassForRecord(
4015
+ structSpec: StructSpec,
4016
+ defaultFrozen: unknown,
4017
+ ): new (initializer?: unknown) => unknown {
4018
+ const { ctor: frozenCtor, initFn } = structSpec;
4019
+ const frozenClass = frozenCtor as unknown as AnyRecord;
4020
+ class Mutable {
4021
+ constructor(initializer: unknown = defaultFrozen) {
4022
+ initFn(this, initializer);
4023
+ if ((initializer as AnyRecord)["^"]) {
4024
+ (this as AnyRecord)["^"] = (initializer as AnyRecord)["^"];
4025
+ }
4026
+ Object.seal(this);
4027
+ }
4028
+
4029
+ toFrozen(): unknown {
4030
+ return (frozenClass.create as (i: unknown) => unknown)(this);
4031
+ }
4032
+
4033
+ toString(): string {
4034
+ const serializer = frozenClass.serializer as Serializer<unknown>;
4035
+ return serializer.toJsonCode(this, "readable");
4036
+ }
4037
+ }
4038
+ return Mutable;
4039
+ }
4040
+
4041
+ function getSerializerForType(type: TypeSpec): Serializer<unknown> {
4042
+ switch (type.kind) {
4043
+ case "array":
4044
+ return arraySerializer(getSerializerForType(type.item), type.keyChain);
4045
+ case "optional":
4046
+ return optionalSerializer(getSerializerForType(type.other));
4047
+ case "primitive":
4048
+ return primitiveSerializer(type.primitive);
4049
+ case "record":
4050
+ return (type.ctor as unknown as AnyRecord)
4051
+ .serializer as Serializer<unknown>;
4052
+ }
4053
+ }
4054
+
4055
+ // The `mutableArray()` getter of the Mutable class returns `this.array` if and
4056
+ // only if `mutableArray()` was never called before or if `this.array` is the
4057
+ // last value returned by `mutableArray()`.
4058
+ // Otherwise, it makes a mutable copy of `this.array`, assigns it to
4059
+ // `this.array` and returns it.
4060
+ const arraysReturnedByMutableGetters = new WeakMap<
4061
+ ReadonlyArray<unknown>,
4062
+ unknown
4063
+ >();
4064
+
4065
+ function makeMutableGetterFn(field: StructFieldSpec): () => unknown {
4066
+ const { property, type } = field;
4067
+ switch (type.kind) {
4068
+ case "array": {
4069
+ class Class {
4070
+ static ret(): unknown {
4071
+ const value = this[property] as readonly unknown[];
4072
+ if (arraysReturnedByMutableGetters.get(value) === this) {
4073
+ return value;
4074
+ }
4075
+ const copy = [...value];
4076
+ arraysReturnedByMutableGetters.set(copy, this);
4077
+ return (this[property] = copy);
4078
+ }
4079
+
4080
+ static [_: string]: unknown;
4081
+ }
4082
+ return Class.ret;
4083
+ }
4084
+ case "record": {
4085
+ const mutableCtor = (type.ctor as unknown as AnyRecord).Mutable as new (
4086
+ i: unknown,
4087
+ ) => unknown;
4088
+ class Class {
4089
+ static ret(): unknown {
4090
+ const value = this[property];
4091
+ if (value instanceof mutableCtor) {
4092
+ return value;
4093
+ }
4094
+ return (this[property] = new mutableCtor(value));
4095
+ }
4096
+
4097
+ static [_: string]: unknown;
4098
+ }
4099
+ return Class.ret;
4100
+ }
4101
+ default: {
4102
+ throw new Error();
4103
+ }
4104
+ }
4105
+ }
4106
+
4107
+ function makeSearchMethod(field: StructFieldSpec): (key: unknown) => unknown {
4108
+ const { property } = field;
4109
+ const indexable = field.indexable!;
4110
+ const { keyFn } = indexable;
4111
+ const keyToHashable = indexable.keyToHashable ?? ((e: unknown): unknown => e);
4112
+ class Class {
4113
+ ret(key: unknown): unknown {
4114
+ const array = this[property] as ReadonlyArray<unknown>;
4115
+ const frozenArrayInfo = frozenArrayRegistry.get(array)!;
4116
+ let { keyFnToIndexing } = frozenArrayInfo;
4117
+ if (!keyFnToIndexing) {
4118
+ frozenArrayInfo.keyFnToIndexing = keyFnToIndexing = //
4119
+ new Map<unknown, Map<unknown, unknown>>();
4120
+ }
4121
+ let hashableToValue = keyFnToIndexing.get(keyFn);
4122
+ if (!hashableToValue) {
4123
+ // The array has not been indexed yet. Index it.
4124
+ hashableToValue = new Map<unknown, unknown>();
4125
+ for (const v of array) {
4126
+ const hashable = keyToHashable(keyFn(v));
4127
+ hashableToValue.set(hashable, v);
4128
+ }
4129
+ keyFnToIndexing.set(keyFn, hashableToValue);
4130
+ }
4131
+ return hashableToValue.get(keyToHashable(key));
4132
+ }
4133
+
4134
+ [_: string]: unknown;
4135
+ }
4136
+ return Class.prototype.ret;
4137
+ }
4138
+
4139
+ function emptyToUndefined(str: string): string | undefined {
4140
+ return str ? str : undefined;
4141
+ }