@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.
@@ -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
+ }
@@ -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
+ }