@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/README.md +67 -9
- package/package.json +2 -2
- package/src/certificate-hashes.js +57 -0
- package/src/channel.js +172 -54
- package/src/client.js +70 -22
- package/src/connection-metadata.js +5 -5
- package/src/generator.js +275 -68
- package/src/index.d.ts +280 -10
- package/src/index.js +15 -1
- package/src/int64.js +288 -0
- package/src/interceptor.js +102 -0
- package/src/stream-compression.js +5 -5
- package/src/wire.js +31 -7
- package/src/wkt.js +137 -0
|
@@ -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
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
194
|
-
|
|
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 (
|
|
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
|
+
}
|