@trevrpc/trevrpc-js 1.3.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.d.ts CHANGED
@@ -2,8 +2,138 @@ import type { INamespace, Root } from "protobufjs";
2
2
 
3
3
  export const protobuf: typeof import("protobufjs");
4
4
 
5
+ /**
6
+ * How decoded messages represent 64-bit integer fields: `bigint`, `number` (exact up to 2^53),
7
+ * decimal `string`, or protobuf.js's `long` (a Long object). 64-bit map keys are decimal strings
8
+ * in every representation.
9
+ */
10
+ export type Int64Representation = "bigint" | "number" | "string" | "long";
11
+
12
+ export interface CreateRootOptions {
13
+ /**
14
+ * How calls decode the 64-bit integers of the root's messages; a field's `jstype` option
15
+ * (`JS_STRING` or `JS_NUMBER`) overrides it. Without it they stay Long objects. Generated code
16
+ * passes the generator's `int64` option.
17
+ */
18
+ int64?: Int64Representation;
19
+ }
20
+
5
21
  /** Creates a protobuf root from a JSON namespace definition. */
6
- export function createRoot(json: INamespace): Root;
22
+ export function createRoot(json: INamespace, options?: CreateRootOptions): Root;
23
+
24
+ /** A 64-bit integer in any representation, including a Long. */
25
+ type Int64Value = bigint | number | string | { low: number; high: number; unsigned?: boolean };
26
+
27
+ /** A decoded google.protobuf.Timestamp message. */
28
+ export interface Timestamp {
29
+ seconds: bigint;
30
+ nanos: number;
31
+ }
32
+
33
+ /** A google.protobuf.Timestamp message to send. */
34
+ export interface TimestampInit {
35
+ seconds?: bigint | number | string;
36
+ nanos?: number;
37
+ }
38
+
39
+ /** A decoded google.protobuf.Duration message. */
40
+ export interface Duration {
41
+ seconds: bigint;
42
+ nanos: number;
43
+ }
44
+
45
+ /** A google.protobuf.Duration message to send. */
46
+ export interface DurationInit {
47
+ seconds?: bigint | number | string;
48
+ nanos?: number;
49
+ }
50
+
51
+ /** A decoded google.protobuf.Empty message. */
52
+ export interface Empty {}
53
+
54
+ /** A google.protobuf.Empty message to send. */
55
+ export interface EmptyInit {}
56
+
57
+ /** Values of the google.protobuf.NullValue enum. */
58
+ export const NullValue: Readonly<{ NULL_VALUE: 0 }>;
59
+ /** A value of the google.protobuf.NullValue enum. */
60
+ export type NullValue = (typeof NullValue)[keyof typeof NullValue];
61
+
62
+ /** A decoded google.protobuf.Struct message: a JSON object; see `structToJson`. */
63
+ export interface Struct {
64
+ fields: Record<string, Value>;
65
+ }
66
+
67
+ /** A google.protobuf.Struct message to send; see `structFromJson`. */
68
+ export interface StructInit {
69
+ fields?: Record<string, ValueInit>;
70
+ }
71
+
72
+ /**
73
+ * A decoded google.protobuf.Value message: a JSON value, which sets one member of the `kind`
74
+ * oneof; see `valueToJson`.
75
+ */
76
+ export interface Value {
77
+ nullValue: NullValue;
78
+ numberValue: number;
79
+ stringValue: string;
80
+ boolValue: boolean;
81
+ structValue: Struct | null;
82
+ listValue: ListValue | null;
83
+ }
84
+
85
+ /** A google.protobuf.Value message to send; see `valueFromJson`. */
86
+ export interface ValueInit {
87
+ nullValue?: NullValue;
88
+ numberValue?: number;
89
+ stringValue?: string;
90
+ boolValue?: boolean;
91
+ structValue?: StructInit | null;
92
+ listValue?: ListValueInit | null;
93
+ }
94
+
95
+ /** A decoded google.protobuf.ListValue message: a JSON array. */
96
+ export interface ListValue {
97
+ values: Value[];
98
+ }
99
+
100
+ /** A google.protobuf.ListValue message to send. */
101
+ export interface ListValueInit {
102
+ values?: ValueInit[];
103
+ }
104
+
105
+ export type JsonValue = null | boolean | number | string | JsonValue[] | JsonObject;
106
+ export type JsonObject = { [key: string]: JsonValue };
107
+
108
+ /** Returns the Timestamp of a Date, to the millisecond. */
109
+ export function timestampFromDate(date: Date): Timestamp;
110
+ /** Returns the Timestamp of milliseconds since the Unix epoch. */
111
+ export function timestampFromMs(ms: number): Timestamp;
112
+ /** Returns the Date of a Timestamp, whatever the representation of its seconds. */
113
+ export function timestampDate(timestamp: {
114
+ seconds?: Int64Value | null;
115
+ nanos?: number | null;
116
+ }): Date;
117
+ /** Returns the milliseconds since the Unix epoch of a Timestamp, rounded down. */
118
+ export function timestampMs(timestamp: {
119
+ seconds?: Int64Value | null;
120
+ nanos?: number | null;
121
+ }): number;
122
+ /** Returns the Duration of a number of milliseconds. */
123
+ export function durationFromMs(ms: number): Duration;
124
+ /** Returns the milliseconds of a Duration, rounded towards zero. */
125
+ export function durationMs(duration: {
126
+ seconds?: Int64Value | null;
127
+ nanos?: number | null;
128
+ }): number;
129
+ /** Returns the Struct of a JSON object. */
130
+ export function structFromJson(json: JsonObject): StructInit;
131
+ /** Returns the JSON object of a Struct. */
132
+ export function structToJson(struct: StructInit): JsonObject;
133
+ /** Returns the Value of a JSON value. */
134
+ export function valueFromJson(json: JsonValue): ValueInit;
135
+ /** Returns the JSON value of a Value; one with no member of `kind` set is null. */
136
+ export function valueToJson(value: ValueInit): JsonValue;
7
137
 
8
138
  export const Code: Readonly<{
9
139
  Ok: 0;
@@ -65,6 +195,59 @@ export interface CallOptions {
65
195
  onResponseMetadata?(metadata: Metadata): void;
66
196
  }
67
197
 
198
+ /** The kind of an RPC, as in its generated descriptor. */
199
+ export type CallKind = "unary" | "serverStreaming" | "clientStreaming" | "bidirectionalStreaming";
200
+
201
+ /** What a call interceptor sees of a call before it starts. */
202
+ export interface InterceptedCall {
203
+ /** The method's full path, such as `/example.greeter.Greeter/SayHello`. */
204
+ readonly method: string;
205
+ /** The service's full name, such as `example.greeter.Greeter`. */
206
+ readonly service: string;
207
+ /** The method's name, such as `SayHello`. */
208
+ readonly name: string;
209
+ readonly kind: CallKind;
210
+ /**
211
+ * The call's request metadata. An interceptor may add, replace and delete entries, such as a
212
+ * refreshed token; the result is validated after the last interceptor, and invalid metadata
213
+ * fails the call with InvalidArgument.
214
+ */
215
+ metadata: Record<string, MetadataValue>;
216
+ /** The call's deadline, which starts once the interceptors are done. */
217
+ readonly timeoutMs: number | undefined;
218
+ /** The call's abort signal, which an interceptor that waits can watch. */
219
+ readonly signal: AbortSignal | undefined;
220
+ }
221
+
222
+ /** How an intercepted call ended. */
223
+ export interface CallEnd {
224
+ /** `Code.Ok` when the call succeeded; the code of `error` otherwise, or `Code.Unknown`. */
225
+ readonly code: StatusCode;
226
+ /** What failed the call, usually a TrevRpcError; undefined when it succeeded. */
227
+ readonly error: unknown;
228
+ }
229
+
230
+ /** Called once when an intercepted call ends. A finish function that throws is reported, not rethrown. */
231
+ export type CallFinish = (end: CallEnd) => void;
232
+
233
+ /**
234
+ * Runs before each call of a client, in order with the others. It may change the call's metadata,
235
+ * wait (for example for a token refresh), and return a function that is called once when the call
236
+ * ends: when a unary call settles, and when a streaming call receives its final status or fails,
237
+ * including when it is cancelled. Finish functions run innermost first. A streaming call that is
238
+ * neither read to its end nor closed does not end. An interceptor that throws fails the call with
239
+ * what it threw, before it starts; the finish functions of earlier interceptors still run.
240
+ */
241
+ export type CallInterceptor = (
242
+ call: InterceptedCall,
243
+ ) => void | CallFinish | Promise<void | CallFinish>;
244
+
245
+ /** Options of a service client: defaults for its calls, and interceptors that run around them. */
246
+ export interface ClientOptions extends CallOptions {
247
+ /** Interceptors for the client's calls, after those of its transport, such as a Channel's. */
248
+ interceptors?: readonly CallInterceptor[];
249
+ }
250
+
68
251
  export interface TransportStream {
69
252
  readable: ReadableStream<Uint8Array>;
70
253
  writable: WritableStream<Uint8Array>;
@@ -83,6 +266,8 @@ export interface DatagramTransport {
83
266
  }
84
267
 
85
268
  declare const compressionBrand: unique symbol;
269
+ declare const connectionMetadataBrand: unique symbol;
270
+ declare const streamCompressionBudgetBrand: unique symbol;
86
271
 
87
272
  /**
88
273
  * Compression settings, made by `createCompression()` from `@trevrpc/trevrpc-js/zstd`: the zstd
@@ -92,6 +277,22 @@ export interface Compression {
92
277
  readonly [compressionBrand]: true;
93
278
  }
94
279
 
280
+ /**
281
+ * A Channel's connection metadata, which `Channel.setConnectionMetadata()` replaces. See
282
+ * `Transport.connectionMetadata`.
283
+ */
284
+ export interface ConnectionMetadataSource {
285
+ readonly [connectionMetadataBrand]: true;
286
+ }
287
+
288
+ /**
289
+ * A Channel's stream compression memory budget, from `ConnectOptions.streamCompressionMemory`.
290
+ * See `Transport.streamCompressionBudget`.
291
+ */
292
+ export interface StreamCompressionBudget {
293
+ readonly [streamCompressionBudgetBrand]: true;
294
+ }
295
+
95
296
  /**
96
297
  * Anything that opens a bidirectional byte stream per call, such as a Channel or WebTransport.
97
298
  * Calls to methods that mark a direction unreliable use the `datagrams` of the session that opens
@@ -103,6 +304,22 @@ export interface Transport {
103
304
  readonly datagrams?: DatagramTransport;
104
305
  /** Compression settings for calls on this transport; absent without compression. */
105
306
  readonly compression?: Compression;
307
+ /**
308
+ * The connection metadata calls on this transport carry; a Channel's own. A transport that
309
+ * wraps a Channel forwards it, or its calls carry no connection metadata.
310
+ */
311
+ readonly connectionMetadata?: ConnectionMetadataSource;
312
+ /**
313
+ * The memory budget of the stream compression contexts of calls on this transport; a Channel's
314
+ * own. A transport that wraps a Channel forwards it, or its calls get a default budget of their
315
+ * own.
316
+ */
317
+ readonly streamCompressionBudget?: StreamCompressionBudget;
318
+ /**
319
+ * Interceptors that clients created on this transport run around every call, before their own;
320
+ * a Channel's own. A transport that wraps a Channel forwards them, or its clients skip them.
321
+ */
322
+ readonly interceptors?: readonly CallInterceptor[];
106
323
  /**
107
324
  * Like `createBidirectionalStream()`, but also returns the session that opened the stream.
108
325
  * Calls negotiate dictionaries and open datagram flows per session, so transports whose session
@@ -211,6 +428,8 @@ export interface ClientStreamingCall<TRequest, TResponse> {
211
428
  * the call and reject with what they threw.
212
429
  */
213
430
  sendAll(requests: Iterable<TRequest> | AsyncIterable<TRequest>): Promise<void>;
431
+ /** Settles with the final status, or rejects with the error that ended the call. */
432
+ readonly status: Promise<StreamStatus>;
214
433
  /** Closes the request stream and returns the final response. */
215
434
  closeAndRecv(): Promise<TResponse>;
216
435
  /** Cancels the call unless it already finished. */
@@ -276,12 +495,15 @@ export type ServiceClient = Record<
276
495
  (requestOrOptions?: unknown, options?: CallOptions) => Promise<unknown>
277
496
  >;
278
497
 
279
- /** Creates a service client from a generated service descriptor. */
498
+ /**
499
+ * Creates a service client from a generated service descriptor. Its calls run the transport's
500
+ * interceptors, then those of `options.interceptors`.
501
+ */
280
502
  export function createServiceClient<TClient extends ServiceClient = ServiceClient>(
281
503
  transport: Transport,
282
504
  service: RpcServiceDescriptor,
283
505
  root: Root,
284
- options?: CallOptions,
506
+ options?: ClientOptions,
285
507
  ): TClient;
286
508
 
287
509
  export interface WebTransportCertificateHash {
@@ -289,6 +511,22 @@ export interface WebTransportCertificateHash {
289
511
  value: ArrayBuffer | ArrayBufferView;
290
512
  }
291
513
 
514
+ export interface FetchServerCertificateHashesOptions {
515
+ signal?: AbortSignal;
516
+ /** The fetch implementation to use instead of the global one. */
517
+ fetch?: (input: string | URL, init?: RequestInit) => Promise<Response>;
518
+ }
519
+
520
+ /**
521
+ * Fetches the certificate hashes a TrevRPC development server publishes, as JSON such as
522
+ * `[{"algorithm":"sha-256","value":"<hex>"}]`, for `serverCertificateHashes`. A 404 yields no
523
+ * hashes; any other failure rejects with Unavailable.
524
+ */
525
+ export function fetchServerCertificateHashes(
526
+ url: string | URL,
527
+ options?: FetchServerCertificateHashesOptions,
528
+ ): Promise<WebTransportCertificateHash[]>;
529
+
292
530
  export interface WebTransportSessionLike extends Transport {
293
531
  readonly ready: Promise<unknown>;
294
532
  /**
@@ -326,6 +564,14 @@ export interface ConnectOptions {
326
564
  * only: the WebSocket fallback needs a certificate the browser trusts.
327
565
  */
328
566
  serverCertificateHashes?: readonly WebTransportCertificateHash[];
567
+ /**
568
+ * A URL that publishes the server's development certificate hashes, such as a path that the
569
+ * development web server proxies to the TrevRPC server's DevelopmentCertificate. Every
570
+ * WebTransport attempt fetches it, so reconnects follow a rotated certificate, and adds its
571
+ * hashes to `serverCertificateHashes`; a 404 adds none, so the same build works against a server
572
+ * with a trusted certificate. A failed fetch fails the WebTransport attempt.
573
+ */
574
+ serverCertificateHashesUrl?: string | URL;
329
575
  /**
330
576
  * WebTransport implementation to use instead of the global one. It is constructed with
331
577
  * `protocols: ["trevrpc-1"]`, the wire version, which cannot be changed.
@@ -350,13 +596,18 @@ export interface ConnectOptions {
350
596
  * metadata. Replace it with `Channel.setConnectionMetadata()`.
351
597
  */
352
598
  connectionMetadata?: MetadataInput;
353
- /** Cancels the initial connection attempt only. */
599
+ /**
600
+ * Interceptors that every client created on the channel runs around its calls, before the
601
+ * client's own; see `CallInterceptor`.
602
+ */
603
+ interceptors?: readonly CallInterceptor[];
604
+ /** Cancels the initial connection attempt of `connect()` only; `createChannel()` ignores it. */
354
605
  signal?: AbortSignal;
355
606
  /** Receives channel state transitions. */
356
607
  onStateChange?(change: ChannelStateChange): void;
357
608
  }
358
609
 
359
- export type ChannelState = "ready" | "reconnecting" | "closed";
610
+ export type ChannelState = "connecting" | "ready" | "reconnecting" | "closed";
360
611
 
361
612
  export interface ChannelStateChange {
362
613
  state: ChannelState;
@@ -371,13 +622,26 @@ export interface ChannelStateChange {
371
622
  transport?: TransportKind;
372
623
  }
373
624
 
374
- /** Opens a channel to a TrevRPC server and waits for its session to be ready. */
625
+ /**
626
+ * Opens a channel to a TrevRPC server and waits for its session to be ready. It rejects, leaving
627
+ * nothing behind, if the first attempt fails.
628
+ */
375
629
  export function connect(url: string | URL, options?: ConnectOptions): Promise<Channel>;
376
630
 
631
+ /**
632
+ * Returns a channel to a TrevRPC server that connects in the background, so that clients can be
633
+ * created on it right away. Calls started before its first session is ready wait for that attempt
634
+ * (within their own deadline and signal); a failed attempt fails them, and the channel goes on
635
+ * reconnecting with backoff. Close the channel to give up; the `signal` option does not apply.
636
+ * Throws if an option is invalid.
637
+ */
638
+ export function createChannel(url: string | URL, options?: ConnectOptions): Channel;
639
+
377
640
  /**
378
641
  * A session that reconnects in the background after it is lost: WebTransport, or QMux over
379
- * WebSocket when WebTransport is unavailable or blocked. Calls fail fast with Unavailable while it
380
- * reconnects and are never replayed.
642
+ * WebSocket when WebTransport is unavailable or blocked. Calls started while it first connects wait
643
+ * for that attempt; afterwards they fail fast with Unavailable while it reconnects and are never
644
+ * replayed.
381
645
  */
382
646
  export class Channel implements Transport {
383
647
  private constructor();
@@ -386,10 +650,16 @@ export class Channel implements Transport {
386
650
  static connect(url: string | URL, options?: ConnectOptions): Promise<Channel>;
387
651
 
388
652
  readonly state: ChannelState;
389
- /** The transport of the current or last session. */
390
- readonly transport: TransportKind;
653
+ /** The transport of the current or last session; undefined before the first. */
654
+ readonly transport: TransportKind | undefined;
391
655
  /** The compression settings from the connect options. */
392
656
  readonly compression?: Compression;
657
+ /** The connection metadata of the channel's calls, which a wrapping Transport forwards. */
658
+ readonly connectionMetadata: ConnectionMetadataSource;
659
+ /** The stream compression memory budget of the channel's calls, which a wrapping Transport forwards. */
660
+ readonly streamCompressionBudget: StreamCompressionBudget;
661
+ /** The interceptors from the connect options, which a wrapping Transport forwards. */
662
+ readonly interceptors: readonly CallInterceptor[];
393
663
  /**
394
664
  * Replaces the channel's connection metadata (see `ConnectOptions.connectionMetadata`); empty or
395
665
  * missing metadata removes it. Calls started after this use the new metadata, and each
package/src/index.js CHANGED
@@ -1,4 +1,18 @@
1
- export { Channel, connect } from "./channel.js";
1
+ export { fetchServerCertificateHashes } from "./certificate-hashes.js";
2
+ export { Channel, connect, createChannel } from "./channel.js";
2
3
  export { createServiceClient } from "./client.js";
3
4
  export { Code, TrevRpcError, codeName } from "./status.js";
4
5
  export { createRoot, protobuf } from "./wire.js";
6
+ export {
7
+ NullValue,
8
+ durationFromMs,
9
+ durationMs,
10
+ structFromJson,
11
+ structToJson,
12
+ timestampDate,
13
+ timestampFromDate,
14
+ timestampFromMs,
15
+ timestampMs,
16
+ valueFromJson,
17
+ valueToJson,
18
+ } from "./wkt.js";
package/src/int64.js ADDED
@@ -0,0 +1,288 @@
1
+ import protobuf from "protobufjs";
2
+
3
+ import { invalidArgument } from "./status.js";
4
+
5
+ /**
6
+ * How decoded 64-bit integer fields are represented: `bigint`, `number` (exact up to 2^53),
7
+ * decimal `string`, or protobuf.js's `long` (a Long object, as protobuf.js decodes them).
8
+ */
9
+ export const Int64Representations = Object.freeze(["bigint", "number", "string", "long"]);
10
+
11
+ /** The representation of each root's 64-bit fields, for roots made with createRoot(). */
12
+ const rootRepresentations = new WeakMap();
13
+ /** The conversion plan of each message type, or null when its messages need none. */
14
+ const plans = new WeakMap();
15
+
16
+ const Int64Types = new Set(["int64", "uint64", "sint64", "fixed64", "sfixed64"]);
17
+ const UnsignedTypes = new Set(["uint64", "fixed64"]);
18
+
19
+ /**
20
+ * Sets how the decoded messages of `root`'s types represent 64-bit integers. Without it they keep
21
+ * protobuf.js's Long objects. A field's `jstype` option (`JS_STRING` or `JS_NUMBER`) overrides it.
22
+ */
23
+ export function setInt64Representation(root, representation) {
24
+ if (!Int64Representations.includes(representation)) {
25
+ throw invalidArgument(
26
+ `int64 must be one of ${Int64Representations.join(", ")}, not ${JSON.stringify(representation)}`,
27
+ );
28
+ }
29
+ rootRepresentations.set(root, representation);
30
+ }
31
+
32
+ /**
33
+ * Returns a function that converts, in place, the 64-bit integers of a message of `type` that
34
+ * protobuf.js decoded, as its root's representation says, and returns the message; or undefined
35
+ * when its messages need no conversion. 64-bit map keys, which protobuf.js decodes as 8-byte hash
36
+ * strings, become decimal strings in every representation.
37
+ */
38
+ export function int64Converter(type) {
39
+ const plan = planFor(type);
40
+ return plan === null ? undefined : (message) => applyPlan(plan, message);
41
+ }
42
+
43
+ /**
44
+ * Converts a 64-bit integer, as a bigint, a number, a decimal string, or a Long-like object with
45
+ * `low`, `high` and `unsigned`, to a bigint. Null and undefined are zero.
46
+ */
47
+ export function int64ToBigInt(value, unsigned = value?.unsigned === true) {
48
+ switch (typeof value) {
49
+ case "bigint":
50
+ return value;
51
+ case "number":
52
+ return BigInt(Math.trunc(value));
53
+ case "string":
54
+ return BigInt(value);
55
+ default: {
56
+ if (value == null) {
57
+ return 0n;
58
+ }
59
+ const bits = (BigInt(value.high >>> 0) << 32n) | BigInt(value.low >>> 0);
60
+ return unsigned ? bits : BigInt.asIntN(64, bits);
61
+ }
62
+ }
63
+ }
64
+
65
+ function planFor(type) {
66
+ const cached = plans.get(type);
67
+ if (cached !== undefined) {
68
+ return cached;
69
+ }
70
+ const representation = rootRepresentations.get(type.root);
71
+ if (representation === undefined) {
72
+ plans.set(type, null);
73
+ return null;
74
+ }
75
+ buildPlans(type, representation);
76
+ return plans.get(type);
77
+ }
78
+
79
+ /**
80
+ * Builds the plans of `start` and every message type reachable from it. A type needs a plan when
81
+ * it has a 64-bit field to convert, or a message field whose type needs one; that is settled for
82
+ * every reachable type at once, so recursive types are handled.
83
+ */
84
+ function buildPlans(start, representation) {
85
+ const steps = new Map();
86
+ const pending = [start];
87
+ steps.set(start, undefined);
88
+ while (pending.length > 0) {
89
+ const type = pending.pop();
90
+ const typeSteps = [];
91
+ for (const field of type.fieldsArray) {
92
+ field.resolve();
93
+ const step = fieldStep(type, field, representation);
94
+ if (step !== undefined) {
95
+ typeSteps.push(step);
96
+ }
97
+ if (field.resolvedType instanceof protobuf.Type && !steps.has(field.resolvedType)) {
98
+ steps.set(field.resolvedType, undefined);
99
+ pending.push(field.resolvedType);
100
+ }
101
+ }
102
+ steps.set(type, typeSteps);
103
+ }
104
+
105
+ // A type needs a plan when it converts a value itself or holds a type that needs one.
106
+ const needed = new Set();
107
+ for (let changed = true; changed;) {
108
+ changed = false;
109
+ for (const [type, typeSteps] of steps) {
110
+ if (
111
+ !needed.has(type) &&
112
+ typeSteps.some((step) => convertsItself(step) || needed.has(step.type))
113
+ ) {
114
+ needed.add(type);
115
+ changed = true;
116
+ }
117
+ }
118
+ }
119
+
120
+ const built = new Map();
121
+ for (const type of steps.keys()) {
122
+ if (!plans.has(type)) {
123
+ built.set(type, needed.has(type) ? [] : null);
124
+ restoreNullDefaults(type);
125
+ }
126
+ }
127
+ for (const [type, plan] of built) {
128
+ plans.set(type, plan);
129
+ }
130
+ for (const [type, plan] of built) {
131
+ if (plan === null) {
132
+ continue;
133
+ }
134
+ for (const step of steps.get(type)) {
135
+ const nested = step.type === undefined ? undefined : plans.get(step.type);
136
+ if (!convertsItself(step) && nested == null) {
137
+ continue;
138
+ }
139
+ plan.push({ ...step, nested: nested ?? undefined });
140
+ }
141
+ patchDefaults(type, plan);
142
+ }
143
+ }
144
+
145
+ /** Reports whether a step converts values or keys of its own, not only nested messages. */
146
+ function convertsItself(step) {
147
+ return step.convert !== undefined || step.convertKey !== undefined;
148
+ }
149
+
150
+ /**
151
+ * The conversion step of one field of `type`, or undefined when it holds neither a 64-bit integer
152
+ * nor a message. `convert` converts its 64-bit values, `convertKey` its 64-bit map keys, and
153
+ * `type` is the message type of its values.
154
+ */
155
+ function fieldStep(type, field, representation) {
156
+ const messageType = field.resolvedType instanceof protobuf.Type ? field.resolvedType : undefined;
157
+ const convert = Int64Types.has(field.type)
158
+ ? valueConverter(fieldRepresentation(field, representation), UnsignedTypes.has(field.type))
159
+ : undefined;
160
+ if (field.map) {
161
+ const convertKey = Int64Types.has(field.keyType)
162
+ ? keyConverter(UnsignedTypes.has(field.keyType))
163
+ : undefined;
164
+ if (convert === undefined && convertKey === undefined && messageType === undefined) {
165
+ return undefined;
166
+ }
167
+ return { name: field.name, shape: "map", convert, convertKey, type: messageType };
168
+ }
169
+ if (convert === undefined && messageType === undefined) {
170
+ return undefined;
171
+ }
172
+ const shape = field.repeated ? "repeated" : "single";
173
+ return { name: field.name, shape, convert, type: messageType };
174
+ }
175
+
176
+ /** The representation of a 64-bit field: its `jstype` option's, or the root's. */
177
+ function fieldRepresentation(field, representation) {
178
+ switch (field.options?.jstype) {
179
+ case "JS_STRING":
180
+ return "string";
181
+ case "JS_NUMBER":
182
+ return "number";
183
+ default:
184
+ return representation;
185
+ }
186
+ }
187
+
188
+ /** Returns a function converting a decoded 64-bit value, or undefined for Long objects. */
189
+ function valueConverter(representation, unsigned) {
190
+ switch (representation) {
191
+ case "bigint":
192
+ return (value) => int64ToBigInt(value, unsigned);
193
+ case "number":
194
+ return (value) => Number(int64ToBigInt(value, unsigned));
195
+ case "string":
196
+ return (value) => int64ToBigInt(value, unsigned).toString();
197
+ default:
198
+ return undefined;
199
+ }
200
+ }
201
+
202
+ /** Returns a function converting a decoded 64-bit map key, an 8-byte hash, to decimal. */
203
+ function keyConverter(unsigned) {
204
+ return (key) => int64ToBigInt(protobuf.util.longFromHash(key, unsigned), unsigned).toString();
205
+ }
206
+
207
+ /**
208
+ * Makes unset proto3 `optional` 64-bit fields of `type`'s messages read null, as other fields with
209
+ * presence do: protobuf.js turns their null default into a Long zero.
210
+ */
211
+ function restoreNullDefaults(type) {
212
+ for (const field of type.fieldsArray) {
213
+ if (Int64Types.has(field.type) && field.options?.proto3_optional && !field.repeated) {
214
+ type.ctor.prototype[field.name] = null;
215
+ }
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Converts the defaults that `type`'s messages read from their prototype for unset singular 64-bit
221
+ * fields, so that an unset field reads like a set one.
222
+ */
223
+ function patchDefaults(type, plan) {
224
+ const prototype = type.ctor.prototype;
225
+ for (const step of plan) {
226
+ // A field with presence, such as a proto3 optional one, reads null when unset.
227
+ if (step.shape === "single" && step.convert !== undefined && prototype[step.name] != null) {
228
+ prototype[step.name] = step.convert(prototype[step.name]);
229
+ }
230
+ }
231
+ }
232
+
233
+ function applyPlan(plan, message) {
234
+ for (const step of plan) {
235
+ if (!Object.hasOwn(message, step.name)) {
236
+ continue;
237
+ }
238
+ const value = message[step.name];
239
+ if (value == null) {
240
+ continue;
241
+ }
242
+ switch (step.shape) {
243
+ case "single":
244
+ message[step.name] =
245
+ step.convert === undefined ? applyPlan(step.nested, value) : step.convert(value);
246
+ break;
247
+ case "repeated":
248
+ for (let index = 0; index < value.length; index += 1) {
249
+ value[index] =
250
+ step.convert === undefined
251
+ ? applyPlan(step.nested, value[index])
252
+ : step.convert(value[index]);
253
+ }
254
+ break;
255
+ case "map":
256
+ if (step.convertKey === undefined) {
257
+ convertMapValues(step, value);
258
+ } else {
259
+ message[step.name] = convertMap(step, value);
260
+ }
261
+ break;
262
+ }
263
+ }
264
+ return message;
265
+ }
266
+
267
+ /** Returns a copy of a decoded map with converted keys and values. */
268
+ function convertMap(step, map) {
269
+ const converted = {};
270
+ for (const [key, value] of Object.entries(map)) {
271
+ converted[step.convertKey(key)] = convertMapValue(step, value);
272
+ }
273
+ return converted;
274
+ }
275
+
276
+ /** Converts the values of a decoded map in place. */
277
+ function convertMapValues(step, map) {
278
+ for (const [key, value] of Object.entries(map)) {
279
+ map[key] = convertMapValue(step, value);
280
+ }
281
+ }
282
+
283
+ function convertMapValue(step, value) {
284
+ if (step.convert !== undefined) {
285
+ return step.convert(value);
286
+ }
287
+ return step.nested !== undefined && value != null ? applyPlan(step.nested, value) : value;
288
+ }