@trevrpc/trevrpc-js 0.2.3 → 1.1.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 +127 -46
- package/package.json +12 -45
- package/src/bytes.js +10 -0
- package/src/call.js +520 -0
- package/src/channel.js +258 -288
- package/src/client.js +543 -1254
- package/src/datagram.js +336 -0
- package/src/framing.js +151 -548
- package/src/generator.js +66 -109
- package/src/index.d.ts +235 -463
- package/src/index.js +4 -3
- package/src/qmux/frames.js +262 -0
- package/src/qmux/params.js +104 -0
- package/src/qmux/session.js +739 -0
- package/src/race.js +90 -0
- package/src/status.js +24 -70
- package/src/varint.js +73 -0
- package/src/wire.js +21 -89
- package/examples/greeter/README.md +0 -35
- package/examples/greeter/client.js +0 -172
- package/examples/greeter/greeter.node.trevrpc.d.ts +0 -53
- package/examples/greeter/greeter.node.trevrpc.js +0 -9
- package/examples/greeter/greeter.proto +0 -18
- package/examples/greeter/greeter.trevrpc.d.ts +0 -55
- package/examples/greeter/greeter.trevrpc.js +0 -103
- package/examples/greeter/index.html +0 -140
- package/examples/greeter/static-server.js +0 -168
- package/src/advanced.d.ts +0 -29
- package/src/advanced.js +0 -1
- package/src/browser-channel.js +0 -49
- package/src/connect.js +0 -6
- package/src/connect.node.js +0 -6
- package/src/index.node.d.ts +0 -10
- package/src/index.node.js +0 -3
- package/src/native-loader.js +0 -118
- package/src/node-advanced.d.ts +0 -27
- package/src/node-advanced.js +0 -1
- package/src/node-generated.d.ts +0 -24
- package/src/node-generated.js +0 -126
- package/src/node-index.d.ts +0 -1
- package/src/node-index.js +0 -7
- package/src/node.d.ts +0 -298
- package/src/node.js +0 -1456
- package/src/runtime.js +0 -66
- package/src/server-response.js +0 -49
- package/src/timer.js +0 -29
- package/src/webtransport.js +0 -639
package/src/race.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { cancelled, statusFromTransportError, unavailable } from "./status.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Dials `primary`, adds `fallback` once the primary fails or `delayMs` passes, and resolves with
|
|
5
|
+
* `{ session, transport }` from the first to connect. The loser is abandoned and closed if it
|
|
6
|
+
* connects anyway. Each candidate is `{ transport, dial(signal) }`.
|
|
7
|
+
*/
|
|
8
|
+
export function dialRace(primary, fallback, { delayMs, signal } = {}) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const attempts = new Map();
|
|
11
|
+
const errors = [];
|
|
12
|
+
let settled = false;
|
|
13
|
+
let fallbackStarted = false;
|
|
14
|
+
let timer;
|
|
15
|
+
|
|
16
|
+
const settle = (winner) => {
|
|
17
|
+
settled = true;
|
|
18
|
+
clearTimeout(timer);
|
|
19
|
+
signal?.removeEventListener("abort", onAbort);
|
|
20
|
+
for (const [candidate, controller] of attempts) {
|
|
21
|
+
if (candidate !== winner) {
|
|
22
|
+
controller.abort();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const start = (candidate) => {
|
|
27
|
+
const controller = new AbortController();
|
|
28
|
+
attempts.set(candidate, controller);
|
|
29
|
+
let dialing;
|
|
30
|
+
try {
|
|
31
|
+
// Dialing starts synchronously, so a session exists as soon as the race begins.
|
|
32
|
+
dialing = Promise.resolve(candidate.dial(controller.signal));
|
|
33
|
+
} catch (error) {
|
|
34
|
+
dialing = Promise.reject(error);
|
|
35
|
+
}
|
|
36
|
+
dialing.then(
|
|
37
|
+
(session) => {
|
|
38
|
+
if (settled) {
|
|
39
|
+
closeQuietly(session);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
settle(candidate);
|
|
43
|
+
resolve({ session, transport: candidate.transport });
|
|
44
|
+
},
|
|
45
|
+
(error) => {
|
|
46
|
+
attempts.delete(candidate);
|
|
47
|
+
if (settled) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
errors.push(`${candidate.transport}: ${statusFromTransportError(error).message}`);
|
|
51
|
+
if (candidate === primary) {
|
|
52
|
+
startFallback();
|
|
53
|
+
} else if (attempts.size === 0) {
|
|
54
|
+
settle(null);
|
|
55
|
+
reject(unavailable(`transport unavailable: ${errors.join("; ")}`));
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
const startFallback = () => {
|
|
61
|
+
if (!fallbackStarted && !settled) {
|
|
62
|
+
fallbackStarted = true;
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
start(fallback);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const onAbort = () => {
|
|
68
|
+
if (!settled) {
|
|
69
|
+
settle(null);
|
|
70
|
+
reject(cancelled("connection cancelled", signal.reason));
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (signal?.aborted) {
|
|
75
|
+
onAbort();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
79
|
+
start(primary);
|
|
80
|
+
timer = setTimeout(startFallback, delayMs);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function closeQuietly(session) {
|
|
85
|
+
try {
|
|
86
|
+
session.close();
|
|
87
|
+
} catch {
|
|
88
|
+
// The session may already be gone.
|
|
89
|
+
}
|
|
90
|
+
}
|
package/src/status.js
CHANGED
|
@@ -18,82 +18,38 @@ export const Code = Object.freeze({
|
|
|
18
18
|
Unauthenticated: 16,
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
-
const
|
|
22
|
-
"Ok",
|
|
23
|
-
"Cancelled",
|
|
24
|
-
"Unknown",
|
|
25
|
-
"InvalidArgument",
|
|
26
|
-
"DeadlineExceeded",
|
|
27
|
-
"NotFound",
|
|
28
|
-
"AlreadyExists",
|
|
29
|
-
"PermissionDenied",
|
|
30
|
-
"ResourceExhausted",
|
|
31
|
-
"FailedPrecondition",
|
|
32
|
-
"Aborted",
|
|
33
|
-
"OutOfRange",
|
|
34
|
-
"Unimplemented",
|
|
35
|
-
"Internal",
|
|
36
|
-
"Unavailable",
|
|
37
|
-
"DataLoss",
|
|
38
|
-
"Unauthenticated",
|
|
39
|
-
]);
|
|
21
|
+
const CodeNames = Object.freeze(Object.keys(Code));
|
|
40
22
|
|
|
41
23
|
/** Error carrying a TrevRPC status code, message, and metadata. */
|
|
42
24
|
export class TrevRpcError extends Error {
|
|
43
25
|
/** Creates a TrevRPC status error. */
|
|
44
26
|
constructor(code, message = "", metadata = {}, options = {}) {
|
|
45
27
|
const normalizedCode = codeFromNumber(code);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
28
|
+
const name = codeName(normalizedCode);
|
|
29
|
+
super(
|
|
30
|
+
message === "" ? name : `${name}: ${message}`,
|
|
31
|
+
options.cause === undefined ? undefined : { cause: options.cause },
|
|
32
|
+
);
|
|
49
33
|
this.name = "TrevRpcError";
|
|
50
34
|
this.code = normalizedCode;
|
|
51
35
|
this.statusMessage = message;
|
|
52
36
|
this.metadata = metadata;
|
|
53
|
-
if (Number.isInteger(options.nativeCode)) {
|
|
54
|
-
this.nativeCode = options.nativeCode;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Resource-exhausted status reported when a frame exceeds the configured size limit. */
|
|
60
|
-
export class FrameTooLargeError extends TrevRpcError {
|
|
61
|
-
/** Creates a frame size error. */
|
|
62
|
-
constructor(length, max) {
|
|
63
|
-
super(Code.ResourceExhausted, `frame length ${length} exceeds maximum ${max}`);
|
|
64
|
-
this.name = "FrameTooLargeError";
|
|
65
|
-
this.length = length;
|
|
66
|
-
this.max = max;
|
|
67
37
|
}
|
|
68
38
|
}
|
|
69
39
|
|
|
70
40
|
/** Converts a number into a known status code, defaulting unknown values to Unknown. */
|
|
71
41
|
export function codeFromNumber(code) {
|
|
72
|
-
return Number.isInteger(code) && code >= 0 && code <
|
|
42
|
+
return Number.isInteger(code) && code >= 0 && code < CodeNames.length ? code : Code.Unknown;
|
|
73
43
|
}
|
|
74
44
|
|
|
75
45
|
/** Returns the canonical status code name. */
|
|
76
46
|
export function codeName(code) {
|
|
77
|
-
return
|
|
47
|
+
return CodeNames[codeFromNumber(code)];
|
|
78
48
|
}
|
|
79
49
|
|
|
80
50
|
/** Creates a TrevRPC status error. */
|
|
81
|
-
export function statusError(code, message = "", metadata = {}) {
|
|
82
|
-
return new TrevRpcError(code, message, metadata);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Builds a status error from an RPC response. */
|
|
86
|
-
export function statusFromResponse(response) {
|
|
87
|
-
if (response == null) {
|
|
88
|
-
return statusError(Code.Internal, "missing RPC response");
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return statusError(response.status ?? Code.Ok, response.message ?? "", response.metadata ?? {});
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Reports whether a status-like value is OK. */
|
|
95
|
-
export function isOkStatus(status) {
|
|
96
|
-
return status != null && codeFromNumber(status.code) === Code.Ok;
|
|
51
|
+
export function statusError(code, message = "", metadata = {}, cause) {
|
|
52
|
+
return new TrevRpcError(code, message, metadata, { cause });
|
|
97
53
|
}
|
|
98
54
|
|
|
99
55
|
/** Creates an internal error status. */
|
|
@@ -102,8 +58,13 @@ export function internal(message) {
|
|
|
102
58
|
}
|
|
103
59
|
|
|
104
60
|
/** Creates an invalid-argument status. */
|
|
105
|
-
export function invalidArgument(message) {
|
|
106
|
-
return statusError(Code.InvalidArgument, message);
|
|
61
|
+
export function invalidArgument(message, cause) {
|
|
62
|
+
return statusError(Code.InvalidArgument, message, {}, cause);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Creates a failed-precondition status. */
|
|
66
|
+
export function failedPrecondition(message) {
|
|
67
|
+
return statusError(Code.FailedPrecondition, message);
|
|
107
68
|
}
|
|
108
69
|
|
|
109
70
|
/** Creates a deadline-exceeded status. */
|
|
@@ -112,8 +73,8 @@ export function deadlineExceeded(message) {
|
|
|
112
73
|
}
|
|
113
74
|
|
|
114
75
|
/** Creates an unavailable status. */
|
|
115
|
-
export function unavailable(message) {
|
|
116
|
-
return statusError(Code.Unavailable, message);
|
|
76
|
+
export function unavailable(message, cause) {
|
|
77
|
+
return statusError(Code.Unavailable, message, {}, cause);
|
|
117
78
|
}
|
|
118
79
|
|
|
119
80
|
/** Creates a resource-exhausted status. */
|
|
@@ -122,24 +83,17 @@ export function resourceExhausted(message) {
|
|
|
122
83
|
}
|
|
123
84
|
|
|
124
85
|
/** Creates a cancelled status. */
|
|
125
|
-
export function cancelled(message) {
|
|
126
|
-
return statusError(Code.Cancelled, message);
|
|
86
|
+
export function cancelled(message, cause) {
|
|
87
|
+
return statusError(Code.Cancelled, message, {}, cause);
|
|
127
88
|
}
|
|
128
89
|
|
|
129
|
-
/** Converts a transport
|
|
90
|
+
/** Converts a transport failure into a status error; status errors pass through. */
|
|
130
91
|
export function statusFromTransportError(error) {
|
|
131
92
|
if (error instanceof TrevRpcError) {
|
|
132
93
|
return error;
|
|
133
94
|
}
|
|
134
|
-
|
|
135
95
|
if (error?.name === "AbortError") {
|
|
136
|
-
return
|
|
96
|
+
return cancelled("transport closed locally", error);
|
|
137
97
|
}
|
|
138
|
-
|
|
139
|
-
return new TrevRpcError(
|
|
140
|
-
Code.Unavailable,
|
|
141
|
-
`transport unavailable: ${error?.message ?? String(error)}`,
|
|
142
|
-
{},
|
|
143
|
-
{ cause: error },
|
|
144
|
-
);
|
|
98
|
+
return unavailable(`transport unavailable: ${error?.message ?? String(error)}`, error);
|
|
145
99
|
}
|
package/src/varint.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// QUIC variable-length integers (RFC 9000 §16), shared by the frame layer and QMux. Values below
|
|
2
|
+
// 2^53 are numbers; larger codepoints are bigints.
|
|
3
|
+
|
|
4
|
+
const MaxSafe = BigInt(Number.MAX_SAFE_INTEGER);
|
|
5
|
+
|
|
6
|
+
/** Encoded length of a varint. */
|
|
7
|
+
export function varintLength(value) {
|
|
8
|
+
if (typeof value === "number" && value >= 0 && value < 0x4000_0000) {
|
|
9
|
+
return value < 0x40 ? 1 : value < 0x4000 ? 2 : 4;
|
|
10
|
+
}
|
|
11
|
+
const v = typeof value === "bigint" ? value : BigInt(value);
|
|
12
|
+
if (v < 0x40n) {
|
|
13
|
+
return 1;
|
|
14
|
+
}
|
|
15
|
+
if (v < 0x4000n) {
|
|
16
|
+
return 2;
|
|
17
|
+
}
|
|
18
|
+
if (v < 0x4000_0000n) {
|
|
19
|
+
return 4;
|
|
20
|
+
}
|
|
21
|
+
if (v < 0x4000_0000_0000_0000n) {
|
|
22
|
+
return 8;
|
|
23
|
+
}
|
|
24
|
+
throw new RangeError(`varint ${v} exceeds 2^62 - 1`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Writes a varint at offset and returns the offset after it. */
|
|
28
|
+
export function writeVarint(bytes, offset, value) {
|
|
29
|
+
const length = varintLength(value);
|
|
30
|
+
if (typeof value === "number" && length < 8 && Number.isInteger(value)) {
|
|
31
|
+
// Fast path without BigInt for the common small values.
|
|
32
|
+
let v = value;
|
|
33
|
+
for (let i = length - 1; i >= 0; i -= 1) {
|
|
34
|
+
bytes[offset + i] = v % 256;
|
|
35
|
+
v = Math.floor(v / 256);
|
|
36
|
+
}
|
|
37
|
+
bytes[offset] |= [0, 0x40, 0, 0x80][length - 1];
|
|
38
|
+
return offset + length;
|
|
39
|
+
}
|
|
40
|
+
let v = typeof value === "bigint" ? value : BigInt(value);
|
|
41
|
+
for (let i = length - 1; i >= 0; i -= 1) {
|
|
42
|
+
bytes[offset + i] = Number(v & 0xffn);
|
|
43
|
+
v >>= 8n;
|
|
44
|
+
}
|
|
45
|
+
bytes[offset] |= [0, 0x40, 0, 0x80, 0, 0, 0, 0xc0][length - 1];
|
|
46
|
+
return offset + length;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Reads a varint at offset. Returns `{ value, next }`, or null when the bytes end first. Values
|
|
51
|
+
* above 2^53 - 1 are bigints.
|
|
52
|
+
*/
|
|
53
|
+
export function readVarint(bytes, offset) {
|
|
54
|
+
if (offset >= bytes.length) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const length = 1 << (bytes[offset] >> 6);
|
|
58
|
+
if (offset + length > bytes.length) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
if (length < 8) {
|
|
62
|
+
let value = bytes[offset] & 0x3f;
|
|
63
|
+
for (let i = 1; i < length; i += 1) {
|
|
64
|
+
value = value * 256 + bytes[offset + i];
|
|
65
|
+
}
|
|
66
|
+
return { value, next: offset + length };
|
|
67
|
+
}
|
|
68
|
+
let value = BigInt(bytes[offset] & 0x3f);
|
|
69
|
+
for (let i = 1; i < 8; i += 1) {
|
|
70
|
+
value = (value << 8n) | BigInt(bytes[offset + i]);
|
|
71
|
+
}
|
|
72
|
+
return { value: value <= MaxSafe ? Number(value) : value, next: offset + 8 };
|
|
73
|
+
}
|
package/src/wire.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import protobuf from "protobufjs";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import { byteView } from "./bytes.js";
|
|
4
4
|
|
|
5
|
-
export
|
|
5
|
+
export { protobuf };
|
|
6
6
|
|
|
7
7
|
export const RpcKind = Object.freeze({
|
|
8
8
|
Unary: 0,
|
|
@@ -11,12 +11,7 @@ export const RpcKind = Object.freeze({
|
|
|
11
11
|
BidirectionalStreaming: 3,
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
Message: 0,
|
|
16
|
-
Status: 1,
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
export const wireRoot = protobuf.Root.fromJSON({
|
|
14
|
+
const wireRoot = protobuf.Root.fromJSON({
|
|
20
15
|
nested: {
|
|
21
16
|
trevrpc: {
|
|
22
17
|
nested: {
|
|
@@ -28,38 +23,32 @@ export const wireRoot = protobuf.Root.fromJSON({
|
|
|
28
23
|
BidirectionalStreaming: 3,
|
|
29
24
|
},
|
|
30
25
|
},
|
|
31
|
-
RpcStreamFrameKind: {
|
|
32
|
-
values: {
|
|
33
|
-
Message: 0,
|
|
34
|
-
Status: 1,
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
26
|
RpcRequest: {
|
|
38
27
|
fields: {
|
|
39
|
-
|
|
40
|
-
method: { type: "string", id: 2 },
|
|
41
|
-
body: { type: "bytes", id: 3 },
|
|
28
|
+
method: { type: "string", id: 1 },
|
|
42
29
|
metadata: { keyType: "string", type: "bytes", id: 4 },
|
|
43
30
|
kind: { type: "RpcKind", id: 5 },
|
|
44
|
-
version: { type: "uint32", id: 6 },
|
|
45
31
|
timeoutNanos: { type: "uint64", id: 7 },
|
|
32
|
+
extensions: { rule: "repeated", type: "uint64", id: 8 },
|
|
33
|
+
datagramFlow: { type: "uint64", id: 9 },
|
|
46
34
|
},
|
|
35
|
+
// Fields 2 (method name), 3 (request body) and 6 (wire version) must never be reused.
|
|
36
|
+
reserved: [
|
|
37
|
+
[2, 3],
|
|
38
|
+
[6, 6],
|
|
39
|
+
],
|
|
47
40
|
},
|
|
48
|
-
|
|
41
|
+
RpcHeaders: {
|
|
49
42
|
fields: {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
body: { type: "bytes", id: 3 },
|
|
53
|
-
metadata: { keyType: "string", type: "bytes", id: 4 },
|
|
43
|
+
metadata: { keyType: "string", type: "bytes", id: 1 },
|
|
44
|
+
extensions: { rule: "repeated", type: "uint64", id: 2 },
|
|
54
45
|
},
|
|
55
46
|
},
|
|
56
|
-
|
|
47
|
+
RpcStatus: {
|
|
57
48
|
fields: {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
body: { type: "bytes", id: 4 },
|
|
62
|
-
metadata: { keyType: "string", type: "bytes", id: 5 },
|
|
49
|
+
code: { type: "uint32", id: 1 },
|
|
50
|
+
message: { type: "string", id: 2 },
|
|
51
|
+
metadata: { keyType: "string", type: "bytes", id: 3 },
|
|
63
52
|
},
|
|
64
53
|
},
|
|
65
54
|
},
|
|
@@ -68,71 +57,24 @@ export const wireRoot = protobuf.Root.fromJSON({
|
|
|
68
57
|
});
|
|
69
58
|
|
|
70
59
|
export const RpcRequest = wireRoot.lookupType("trevrpc.RpcRequest");
|
|
71
|
-
export const
|
|
72
|
-
export const
|
|
60
|
+
export const RpcHeaders = wireRoot.lookupType("trevrpc.RpcHeaders");
|
|
61
|
+
export const RpcStatus = wireRoot.lookupType("trevrpc.RpcStatus");
|
|
73
62
|
|
|
74
63
|
const MaxFieldNumber = 0x1fffffff;
|
|
75
64
|
const MaxUint32 = 0xffffffffn;
|
|
76
65
|
const MaxUint64 = 0xffffffffffffffffn;
|
|
77
66
|
const fatalUtf8Decoder = new TextDecoder("utf-8", { fatal: true });
|
|
78
67
|
|
|
79
|
-
/** Internal wire-validation failure used by production codec paths. */
|
|
80
|
-
export class WireValidationError extends Error {
|
|
81
|
-
constructor(reason, message) {
|
|
82
|
-
super(message);
|
|
83
|
-
this.name = "WireValidationError";
|
|
84
|
-
this.reason = reason;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
68
|
/** Checks protobuf tags and known-field wire types before protobuf.js decoding. */
|
|
89
69
|
export function preflightWireMessage(messageType, body) {
|
|
90
70
|
scanMessage(messageType, byteView(body), 0);
|
|
91
71
|
}
|
|
92
72
|
|
|
93
|
-
/** Validates TrevRPC protocol enums and versions after protobuf decoding. */
|
|
94
|
-
export function validateWireMessage(messageType, message) {
|
|
95
|
-
if (messageType === RpcRequest) {
|
|
96
|
-
if (message.version !== WireVersion) {
|
|
97
|
-
throw new WireValidationError(
|
|
98
|
-
"unsupported_wire_version",
|
|
99
|
-
`unsupported wire version ${message.version}`,
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
if (!Object.values(RpcKind).includes(message.kind)) {
|
|
103
|
-
throw new WireValidationError("unsupported_rpc_kind", `unsupported RPC kind ${message.kind}`);
|
|
104
|
-
}
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
messageType === RpcStreamFrame &&
|
|
110
|
-
message.kind !== RpcStreamFrameKind.Message &&
|
|
111
|
-
message.kind !== RpcStreamFrameKind.Status
|
|
112
|
-
) {
|
|
113
|
-
throw new WireValidationError(
|
|
114
|
-
"unsupported_frame_kind",
|
|
115
|
-
`unsupported stream frame kind ${message.kind}`,
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
73
|
/** Creates a protobuf root from a JSON namespace definition. */
|
|
121
74
|
export function createRoot(json) {
|
|
122
75
|
return protobuf.Root.fromJSON(json);
|
|
123
76
|
}
|
|
124
77
|
|
|
125
|
-
/** Creates a stream message frame carrying a protobuf body. */
|
|
126
|
-
export function messageFrame(body) {
|
|
127
|
-
return {
|
|
128
|
-
kind: RpcStreamFrameKind.Message,
|
|
129
|
-
status: 0,
|
|
130
|
-
message: "",
|
|
131
|
-
body,
|
|
132
|
-
metadata: {},
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
78
|
function scanMessage(messageType, bytes, depth) {
|
|
137
79
|
if (depth > 64) {
|
|
138
80
|
throw malformedWire("protobuf message nesting exceeded limit");
|
|
@@ -389,15 +331,5 @@ function validateUtf8(bytes, name) {
|
|
|
389
331
|
}
|
|
390
332
|
|
|
391
333
|
function malformedWire(message) {
|
|
392
|
-
return new
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
function byteView(value) {
|
|
396
|
-
if (value instanceof Uint8Array) {
|
|
397
|
-
return value;
|
|
398
|
-
}
|
|
399
|
-
if (ArrayBuffer.isView(value)) {
|
|
400
|
-
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
401
|
-
}
|
|
402
|
-
return new Uint8Array(value ?? 0);
|
|
334
|
+
return new Error(message);
|
|
403
335
|
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# Greeter example
|
|
2
|
-
|
|
3
|
-
This directory contains the generated Milestone 6 split:
|
|
4
|
-
|
|
5
|
-
- `greeter.trevrpc.js` and `.d.ts`: protobuf root, descriptors, typed clients, and factories; browser-safe.
|
|
6
|
-
- `greeter.node.trevrpc.js` and `.d.ts`: typed Node handler interfaces and `registerGreeterServer`.
|
|
7
|
-
|
|
8
|
-
Browser code must import only `greeter.trevrpc.js`. Node servers import registration from `greeter.node.trevrpc.js` and use `createUnaryResponse` or `createStreamingResponse` when returning metadata or a custom terminal status. A direct protobuf object such as `{ message: "hello" }` remains a protobuf response.
|
|
9
|
-
|
|
10
|
-
## Run the browser page
|
|
11
|
-
|
|
12
|
-
From `trevrpc-js`:
|
|
13
|
-
|
|
14
|
-
```sh
|
|
15
|
-
npm run example:greeter
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
Open `http://127.0.0.1:8080/examples/greeter/` and use a WebTransport greeter endpoint:
|
|
19
|
-
|
|
20
|
-
```text
|
|
21
|
-
https://127.0.0.1:50051/trevrpc # Go default
|
|
22
|
-
https://127.0.0.1:5000/trevrpc # Rust default
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
The path selects the WebTransport session; TrevRPC routing remains inside the request frame. The example policies allow origin `http://127.0.0.1:8080` and use bearer token `trevrpc-example-token`.
|
|
26
|
-
|
|
27
|
-
Both example servers write their development certificate to `~/.config/trevrpc/trevrpc-example-cert.pem`. Override it with `TREVRPC_EXAMPLE_CERT`. The static server computes the SHA-256 DER hash and the browser client passes it through `serverCertificateHashes`.
|
|
28
|
-
|
|
29
|
-
For a temporary self-signed certificate, calculate the hash with:
|
|
30
|
-
|
|
31
|
-
```sh
|
|
32
|
-
openssl x509 -in server.pem -outform der | openssl dgst -sha256 -binary | openssl base64
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
The client fully consumes server streams, then reads terminal metadata from `stream.status`. It calls `close()` when abandoning streams. Client `timeoutMs` is absolute across setup, upload, and receive, while `streamIdleTimeoutMs` is a separate inactivity bound.
|
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import { TrevRpcError, connect } from "../../src/index.js";
|
|
2
|
-
import { GreeterClient } from "./greeter.trevrpc.js";
|
|
3
|
-
|
|
4
|
-
const form = document.querySelector("#client-form");
|
|
5
|
-
const output = document.querySelector("#output");
|
|
6
|
-
const submit = document.querySelector("#submit");
|
|
7
|
-
|
|
8
|
-
void loadExampleDefaults();
|
|
9
|
-
|
|
10
|
-
form.addEventListener("submit", (event) => {
|
|
11
|
-
event.preventDefault();
|
|
12
|
-
void runFromForm(new FormData(form));
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
for (const preset of document.querySelectorAll("[data-url]")) {
|
|
16
|
-
preset.addEventListener("click", () => {
|
|
17
|
-
form.elements.url.value = preset.dataset.url;
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function runFromForm(data) {
|
|
22
|
-
output.textContent = "";
|
|
23
|
-
submit.disabled = true;
|
|
24
|
-
|
|
25
|
-
const url = String(data.get("url") ?? "").trim();
|
|
26
|
-
const name = String(data.get("name") ?? "TrevRPC").trim() || "TrevRPC";
|
|
27
|
-
const token = String(data.get("token") ?? "").trim();
|
|
28
|
-
const certificateHash = String(data.get("certificate-hash") ?? "").trim();
|
|
29
|
-
|
|
30
|
-
let channel;
|
|
31
|
-
try {
|
|
32
|
-
log(`connecting to ${url}`);
|
|
33
|
-
channel = await connect(url, {
|
|
34
|
-
...browserConnectOptions(certificateHash),
|
|
35
|
-
timeoutMs: 10_000,
|
|
36
|
-
});
|
|
37
|
-
const client = new GreeterClient(channel, {
|
|
38
|
-
timeoutMs: 5_000,
|
|
39
|
-
metadata: token === "" ? {} : { authorization: `Bearer ${token}` },
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const unary = await client.sayHello({ name });
|
|
43
|
-
log(`SayHello: ${unary.message}`);
|
|
44
|
-
|
|
45
|
-
const replies = await client.lotsOfReplies({ name });
|
|
46
|
-
for await (const reply of replies) {
|
|
47
|
-
log(`LotsOfReplies: ${reply.message}`);
|
|
48
|
-
}
|
|
49
|
-
const replyStatus = await replies.status;
|
|
50
|
-
log(
|
|
51
|
-
`LotsOfReplies status: ${replyStatus.code} ${replyStatus.message}; trailers=${Object.keys(replyStatus.metadata).join(",")}`,
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
const greetings = await client.lotsOfGreetings();
|
|
55
|
-
await greetings.send({ name: `${name} client stream 1` });
|
|
56
|
-
await greetings.send({ name: `${name} client stream 2` });
|
|
57
|
-
const summary = await greetings.closeAndRecvWithResponse();
|
|
58
|
-
log(
|
|
59
|
-
`LotsOfGreetings: ${summary.message.message}; trailers=${Object.keys(summary.metadata).join(",")}`,
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
const bidi = await client.bidiHello();
|
|
63
|
-
try {
|
|
64
|
-
for (const suffix of ["bidi 1", "bidi 2"]) {
|
|
65
|
-
await bidi.send({ name: `${name} ${suffix}` });
|
|
66
|
-
|
|
67
|
-
const reply = await bidi.recv();
|
|
68
|
-
if (reply == null) {
|
|
69
|
-
throw new Error("BidiHello response stream ended early");
|
|
70
|
-
}
|
|
71
|
-
log(`BidiHello: ${reply.message}`);
|
|
72
|
-
}
|
|
73
|
-
await bidi.closeSend();
|
|
74
|
-
|
|
75
|
-
for (;;) {
|
|
76
|
-
const reply = await bidi.recv();
|
|
77
|
-
if (reply == null) {
|
|
78
|
-
break;
|
|
79
|
-
}
|
|
80
|
-
log(`BidiHello: ${reply.message}`);
|
|
81
|
-
}
|
|
82
|
-
} finally {
|
|
83
|
-
await bidi.close();
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
log("complete");
|
|
87
|
-
} catch (error) {
|
|
88
|
-
logError(error);
|
|
89
|
-
} finally {
|
|
90
|
-
submit.disabled = false;
|
|
91
|
-
channel?.close({ closeCode: 0, reason: "example complete" });
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function browserConnectOptions(certificateHash) {
|
|
96
|
-
if (certificateHash === "") {
|
|
97
|
-
return {};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
serverCertificateHashes: [
|
|
102
|
-
{
|
|
103
|
-
algorithm: "sha-256",
|
|
104
|
-
value: parseSha256Hash(certificateHash),
|
|
105
|
-
},
|
|
106
|
-
],
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
async function loadExampleDefaults() {
|
|
111
|
-
try {
|
|
112
|
-
const response = await fetch("./certificate-hash.json", { cache: "no-store" });
|
|
113
|
-
if (!response.ok) {
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const config = await response.json();
|
|
118
|
-
if (typeof config.bearerToken === "string" && form.elements.token.value === "") {
|
|
119
|
-
form.elements.token.value = config.bearerToken;
|
|
120
|
-
}
|
|
121
|
-
if (config.exists && typeof config.sha256Base64 === "string") {
|
|
122
|
-
form.elements["certificate-hash"].value = config.sha256Base64;
|
|
123
|
-
log(`loaded certificate hash from ${config.path}`);
|
|
124
|
-
} else if (typeof config.path === "string") {
|
|
125
|
-
log(`certificate not found at ${config.path}; start a Go or Rust example server first`);
|
|
126
|
-
}
|
|
127
|
-
} catch (error) {
|
|
128
|
-
log(`could not load local certificate hash: ${error?.message ?? String(error)}`);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function parseSha256Hash(value) {
|
|
133
|
-
const normalized = value
|
|
134
|
-
.trim()
|
|
135
|
-
.replace(/^sha-?256[=:]/i, "")
|
|
136
|
-
.trim();
|
|
137
|
-
const hex = normalized.replace(/[:\s]/g, "");
|
|
138
|
-
|
|
139
|
-
if (/^[0-9a-fA-F]{64}$/.test(hex)) {
|
|
140
|
-
const bytes = new Uint8Array(32);
|
|
141
|
-
for (let index = 0; index < bytes.length; index += 1) {
|
|
142
|
-
bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
|
|
143
|
-
}
|
|
144
|
-
return bytes;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const base64 = normalized.replace(/-/g, "+").replace(/_/g, "/");
|
|
148
|
-
const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), "=");
|
|
149
|
-
const binary = atob(padded);
|
|
150
|
-
if (binary.length !== 32) {
|
|
151
|
-
throw new Error("certificate hash must decode to 32 bytes of SHA-256 data");
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const bytes = new Uint8Array(binary.length);
|
|
155
|
-
for (let index = 0; index < binary.length; index += 1) {
|
|
156
|
-
bytes[index] = binary.charCodeAt(index);
|
|
157
|
-
}
|
|
158
|
-
return bytes;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function log(message) {
|
|
162
|
-
output.textContent += `${message}\n`;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function logError(error) {
|
|
166
|
-
if (error instanceof TrevRpcError) {
|
|
167
|
-
log(`RPC error: ${error.code}: ${error.statusMessage || error.message}`);
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
log(`error: ${error?.message ?? String(error)}`);
|
|
172
|
-
}
|