@trevrpc/trevrpc-js 1.3.2 → 1.5.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/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
+ }
@@ -0,0 +1,102 @@
1
+ import { normalizeMetadata } from "./metadata.js";
2
+ import { Code, TrevRpcError, invalidArgument } from "./status.js";
3
+
4
+ /**
5
+ * Checks a list of call interceptors from the options of a channel or client, and returns it as a
6
+ * frozen array; undefined or null is no interceptors. Throws InvalidArgument otherwise.
7
+ */
8
+ export function interceptorList(interceptors, what) {
9
+ if (interceptors == null) {
10
+ return Object.freeze([]);
11
+ }
12
+ if (typeof interceptors[Symbol.iterator] !== "function" || typeof interceptors === "string") {
13
+ throw invalidArgument(`${what} must be a list of functions`);
14
+ }
15
+ const list = [...interceptors];
16
+ if (!list.every((interceptor) => typeof interceptor === "function")) {
17
+ throw invalidArgument(`${what} must be a list of functions`);
18
+ }
19
+ return Object.freeze(list);
20
+ }
21
+
22
+ /**
23
+ * Runs `interceptors` in order, then starts the call with `start(options)`, passing the options
24
+ * with the metadata the interceptors left, and returns what `start` returns. The finish functions
25
+ * the interceptors returned run once, innermost first, with `{ code, error }` when the call ends:
26
+ * when an interceptor or `start` fails, when `start` settles if `ended` is undefined, and when
27
+ * `ended(result)` settles otherwise, such as a streaming call's final status.
28
+ */
29
+ export async function interceptCall(interceptors, info, options, start, ended) {
30
+ const finishes = [];
31
+ let result;
32
+ try {
33
+ const call = new InterceptedCall(info, options);
34
+ for (const interceptor of interceptors) {
35
+ const finish = await interceptor(call);
36
+ if (finish != null) {
37
+ if (typeof finish !== "function") {
38
+ throw new TypeError("a call interceptor must return a function or nothing");
39
+ }
40
+ finishes.push(finish);
41
+ }
42
+ }
43
+ result = await start({ ...options, metadata: normalizeMetadata(call.metadata) });
44
+ } catch (error) {
45
+ runFinishes(finishes, error);
46
+ throw error;
47
+ }
48
+ if (ended === undefined) {
49
+ runFinishes(finishes, undefined);
50
+ } else if (finishes.length > 0) {
51
+ ended(result).then(
52
+ () => runFinishes(finishes, undefined),
53
+ (error) => runFinishes(finishes, error),
54
+ );
55
+ }
56
+ return result;
57
+ }
58
+
59
+ /** What an interceptor sees of a call before it starts, and may change. */
60
+ class InterceptedCall {
61
+ constructor({ method, service, name, kind }, { metadata, timeoutMs, signal }) {
62
+ this.method = method;
63
+ this.service = service;
64
+ this.name = name;
65
+ this.kind = kind;
66
+ /** The request metadata; interceptors may add, replace and delete entries. */
67
+ this.metadata = { ...metadata };
68
+ this.timeoutMs = timeoutMs;
69
+ this.signal = signal;
70
+ Object.seal(this);
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Calls each finish function, innermost first, with how the call ended. A finish function that
76
+ * throws does not stop the others; its error is reported as uncaught.
77
+ */
78
+ function runFinishes(finishes, error) {
79
+ if (finishes.length === 0) {
80
+ return;
81
+ }
82
+ const end = Object.freeze(
83
+ error === undefined
84
+ ? { code: Code.Ok, error: undefined }
85
+ : { code: error instanceof TrevRpcError ? error.code : Code.Unknown, error },
86
+ );
87
+ for (let index = finishes.length - 1; index >= 0; index -= 1) {
88
+ try {
89
+ finishes[index](end);
90
+ } catch (failure) {
91
+ reportUncaught(failure);
92
+ }
93
+ }
94
+ }
95
+
96
+ function reportUncaught(error) {
97
+ if (typeof globalThis.reportError === "function") {
98
+ globalThis.reportError(error);
99
+ } else {
100
+ console.error(error);
101
+ }
102
+ }
package/src/status.js CHANGED
@@ -37,6 +37,10 @@ export class TrevRpcError extends Error {
37
37
  }
38
38
  }
39
39
 
40
+ // A discriminant for libraries that match errors by tag, such as Effect's `catchTag`. It is on the
41
+ // prototype, so errors carry no extra own property.
42
+ Object.defineProperty(TrevRpcError.prototype, "_tag", { value: "TrevRpcError" });
43
+
40
44
  /** Converts a number into a known status code, defaulting unknown values to Unknown. */
41
45
  export function codeFromNumber(code) {
42
46
  return Number.isInteger(code) && code >= 0 && code < CodeNames.length ? code : Code.Unknown;
@@ -17,9 +17,6 @@ const StreamCompressionStart = 3;
17
17
  /** The default memory budget of one channel's stream contexts, in bytes. */
18
18
  export const DefaultStreamCompressionMemory = 64 * 1024 * 1024;
19
19
 
20
- /** The key of a transport's StreamBudget; a Channel sets it from its connect options. */
21
- export const StreamBudgetKey = Symbol("trevrpc.streamBudget");
22
-
23
20
  /** The memory stream contexts may hold, shared by the calls of one channel. */
24
21
  export class StreamBudget {
25
22
  #available;
@@ -54,9 +51,12 @@ export class StreamBudget {
54
51
 
55
52
  const defaultBudgets = new WeakMap();
56
53
 
57
- /** The stream budget of a transport: a Channel's own, or a default one per other transport. */
54
+ /**
55
+ * The stream budget of a transport: a Channel's own, which a transport that wraps one forwards, or
56
+ * a default one per other transport.
57
+ */
58
58
  export function streamBudget(transport) {
59
- const budget = transport[StreamBudgetKey];
59
+ const budget = transport.streamCompressionBudget;
60
60
  if (budget !== undefined) {
61
61
  return budget;
62
62
  }
package/src/wire.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import protobuf from "protobufjs";
2
2
 
3
3
  import { byteView } from "./bytes.js";
4
+ import { setInt64Representation } from "./int64.js";
4
5
 
5
6
  export { protobuf };
6
7
 
@@ -102,9 +103,16 @@ export function preflightWireMessage(messageType, body) {
102
103
  scanMessage(messageType, byteView(body), 0);
103
104
  }
104
105
 
105
- /** Creates a protobuf root from a JSON namespace definition. */
106
- export function createRoot(json) {
107
- return protobuf.Root.fromJSON(json);
106
+ /**
107
+ * Creates a protobuf root from a JSON namespace definition. `options.int64` sets how calls decode
108
+ * its messages' 64-bit integers (see `setInt64Representation`); without it they stay Long objects.
109
+ */
110
+ export function createRoot(json, options = {}) {
111
+ const root = protobuf.Root.fromJSON(json);
112
+ if (options.int64 !== undefined) {
113
+ setInt64Representation(root, options.int64);
114
+ }
115
+ return root;
108
116
  }
109
117
 
110
118
  function scanMessage(messageType, bytes, depth) {
@@ -183,15 +191,25 @@ function scanMapEntry(field, bytes, depth) {
183
191
  if (fieldNumber === 1) {
184
192
  scanSyntheticField(field.keyType, `${field.name}.key`, bytes, cursor, wireType, depth);
185
193
  } else if (fieldNumber === 2) {
186
- scanSyntheticField(field.type, `${field.name}.value`, bytes, cursor, wireType, depth);
194
+ const messageType = field.resolvedType?.fieldsById != null ? field.resolvedType : undefined;
195
+ scanSyntheticField(
196
+ field.type,
197
+ `${field.name}.value`,
198
+ bytes,
199
+ cursor,
200
+ wireType,
201
+ depth,
202
+ messageType,
203
+ );
187
204
  } else {
188
205
  skipUnknownField(bytes, cursor, fieldNumber, wireType, depth);
189
206
  }
190
207
  }
191
208
  }
192
209
 
193
- function scanSyntheticField(type, name, bytes, cursor, wireType, depth) {
194
- const expected = scalarWireType(type);
210
+ /** Scans a map key or value: a scalar of `type`, or a message of `messageType` when it is set. */
211
+ function scanSyntheticField(type, name, bytes, cursor, wireType, depth, messageType) {
212
+ const expected = messageType === undefined ? scalarWireType(type) : 2;
195
213
  if (wireType !== expected) {
196
214
  throw malformedWire(`field ${name} used incompatible protobuf wire type ${wireType}`);
197
215
  }
@@ -206,7 +224,9 @@ function scanSyntheticField(type, name, bytes, cursor, wireType, depth) {
206
224
  skipFixed(bytes, cursor, 4, name);
207
225
  } else if (wireType === 2) {
208
226
  const value = readLengthDelimited(bytes, cursor, `truncated protobuf field ${name}`);
209
- if (type === "string") {
227
+ if (messageType !== undefined) {
228
+ scanMessage(messageType, value, depth + 1);
229
+ } else if (type === "string") {
210
230
  validateUtf8(value, name);
211
231
  }
212
232
  } else {
@@ -234,6 +254,10 @@ function scanPackedField(field, bytes) {
234
254
  }
235
255
 
236
256
  function acceptedWireTypes(field) {
257
+ // A map entry is a length-delimited message, whatever its value type.
258
+ if (field.map) {
259
+ return [2];
260
+ }
237
261
  const expected = field.resolvedType?.fieldsById != null ? 2 : scalarWireType(field.type);
238
262
  return field.repeated && isPackableField(field) ? [expected, 2] : [expected];
239
263
  }
package/src/wkt.js ADDED
@@ -0,0 +1,137 @@
1
+ // Helpers for the well-known types google.protobuf.Timestamp, Duration, Struct, Value and
2
+ // ListValue, whose declarations generated code imports from the runtime.
3
+ import { int64ToBigInt } from "./int64.js";
4
+ import { invalidArgument } from "./status.js";
5
+
6
+ /** google.protobuf.NullValue. */
7
+ export const NullValue = Object.freeze({ NULL_VALUE: 0 });
8
+
9
+ const MsPerSecond = 1000n;
10
+ const NanosPerMs = 1_000_000;
11
+
12
+ /** Returns the Timestamp of a Date. */
13
+ export function timestampFromDate(date) {
14
+ return timestampFromMs(date.getTime());
15
+ }
16
+
17
+ /** Returns the Timestamp of milliseconds since the Unix epoch. */
18
+ export function timestampFromMs(ms) {
19
+ checkFinite(ms, "timestamp");
20
+ const whole = Math.floor(ms);
21
+ const seconds = Math.floor(whole / 1000);
22
+ return { seconds: BigInt(seconds), nanos: (whole - seconds * 1000) * NanosPerMs };
23
+ }
24
+
25
+ /** Returns the Date of a Timestamp. */
26
+ export function timestampDate(timestamp) {
27
+ return new Date(timestampMs(timestamp));
28
+ }
29
+
30
+ /** Returns the milliseconds since the Unix epoch of a Timestamp, rounded down. */
31
+ export function timestampMs(timestamp) {
32
+ return toMs(timestamp, Math.floor);
33
+ }
34
+
35
+ /** Returns the Duration of a number of milliseconds. */
36
+ export function durationFromMs(ms) {
37
+ checkFinite(ms, "duration");
38
+ const whole = Math.trunc(ms);
39
+ const seconds = Math.trunc(whole / 1000);
40
+ // Seconds and nanos share the duration's sign; `+ 0` turns -0 into 0.
41
+ return { seconds: BigInt(seconds), nanos: (whole - seconds * 1000) * NanosPerMs + 0 };
42
+ }
43
+
44
+ /** Returns the milliseconds of a Duration, rounded towards zero. */
45
+ export function durationMs(duration) {
46
+ return toMs(duration, Math.trunc);
47
+ }
48
+
49
+ /** Returns the Struct of a JSON object. */
50
+ export function structFromJson(json) {
51
+ if (json === null || typeof json !== "object" || Array.isArray(json)) {
52
+ throw invalidArgument("a Struct is made from a JSON object");
53
+ }
54
+ const fields = {};
55
+ for (const [key, value] of Object.entries(json)) {
56
+ fields[key] = valueFromJson(value);
57
+ }
58
+ return { fields };
59
+ }
60
+
61
+ /** Returns the JSON object of a Struct. */
62
+ export function structToJson(struct) {
63
+ const json = {};
64
+ for (const [key, value] of Object.entries(struct?.fields ?? {})) {
65
+ json[key] = valueToJson(value);
66
+ }
67
+ return json;
68
+ }
69
+
70
+ /** Returns the Value of a JSON value. */
71
+ export function valueFromJson(json) {
72
+ if (json === null) {
73
+ return { nullValue: NullValue.NULL_VALUE };
74
+ }
75
+ switch (typeof json) {
76
+ case "number":
77
+ return { numberValue: json };
78
+ case "string":
79
+ return { stringValue: json };
80
+ case "boolean":
81
+ return { boolValue: json };
82
+ case "object":
83
+ return Array.isArray(json)
84
+ ? { listValue: { values: json.map(valueFromJson) } }
85
+ : { structValue: structFromJson(json) };
86
+ default:
87
+ throw invalidArgument(`a Value cannot hold a ${typeof json}`);
88
+ }
89
+ }
90
+
91
+ /** Returns the JSON value of a Value; a Value with no kind set is null. */
92
+ export function valueToJson(value) {
93
+ switch (valueKind(value)) {
94
+ case "numberValue":
95
+ return value.numberValue;
96
+ case "stringValue":
97
+ return value.stringValue;
98
+ case "boolValue":
99
+ return value.boolValue;
100
+ case "structValue":
101
+ return structToJson(value.structValue);
102
+ case "listValue":
103
+ return (value.listValue.values ?? []).map(valueToJson);
104
+ default:
105
+ return null;
106
+ }
107
+ }
108
+
109
+ const ValueKinds = [
110
+ "nullValue",
111
+ "numberValue",
112
+ "stringValue",
113
+ "boolValue",
114
+ "structValue",
115
+ "listValue",
116
+ ];
117
+
118
+ /**
119
+ * The member of a Value's `kind` oneof that is set. A decoded Value reads the defaults of the
120
+ * other members from its prototype, so only own properties count.
121
+ */
122
+ function valueKind(value) {
123
+ return ValueKinds.find(
124
+ (kind) => value != null && Object.hasOwn(value, kind) && value[kind] != null,
125
+ );
126
+ }
127
+
128
+ /** The milliseconds of a Timestamp or Duration, with sub-millisecond nanos rounded by `round`. */
129
+ function toMs({ seconds, nanos } = {}, round) {
130
+ return Number(int64ToBigInt(seconds) * MsPerSecond) + round((nanos ?? 0) / NanosPerMs);
131
+ }
132
+
133
+ function checkFinite(ms, what) {
134
+ if (!Number.isFinite(ms)) {
135
+ throw invalidArgument(`a ${what} needs a finite number of milliseconds, not ${ms}`);
136
+ }
137
+ }