@trevrpc/trevrpc-js 1.0.0 → 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/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
@@ -48,7 +48,7 @@ export function codeName(code) {
48
48
  }
49
49
 
50
50
  /** Creates a TrevRPC status error. */
51
- export function statusError(code, message = "", metadata = {}, cause = undefined) {
51
+ export function statusError(code, message = "", metadata = {}, cause) {
52
52
  return new TrevRpcError(code, message, metadata, { cause });
53
53
  }
54
54
 
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
@@ -4,8 +4,6 @@ import { byteView } from "./bytes.js";
4
4
 
5
5
  export { protobuf };
6
6
 
7
- export const WireVersion = 1;
8
-
9
7
  export const RpcKind = Object.freeze({
10
8
  Unary: 0,
11
9
  ClientStreaming: 1,
@@ -13,11 +11,6 @@ export const RpcKind = Object.freeze({
13
11
  BidirectionalStreaming: 3,
14
12
  });
15
13
 
16
- export const RpcStreamFrameKind = Object.freeze({
17
- Message: 0,
18
- Status: 1,
19
- });
20
-
21
14
  const wireRoot = protobuf.Root.fromJSON({
22
15
  nested: {
23
16
  trevrpc: {
@@ -30,38 +23,32 @@ const wireRoot = protobuf.Root.fromJSON({
30
23
  BidirectionalStreaming: 3,
31
24
  },
32
25
  },
33
- RpcStreamFrameKind: {
34
- values: {
35
- Message: 0,
36
- Status: 1,
37
- },
38
- },
39
26
  RpcRequest: {
40
27
  fields: {
41
- service: { type: "string", id: 1 },
42
- method: { type: "string", id: 2 },
43
- body: { type: "bytes", id: 3 },
28
+ method: { type: "string", id: 1 },
44
29
  metadata: { keyType: "string", type: "bytes", id: 4 },
45
30
  kind: { type: "RpcKind", id: 5 },
46
- version: { type: "uint32", id: 6 },
47
31
  timeoutNanos: { type: "uint64", id: 7 },
32
+ extensions: { rule: "repeated", type: "uint64", id: 8 },
33
+ datagramFlow: { type: "uint64", id: 9 },
48
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
+ ],
49
40
  },
50
- RpcResponse: {
41
+ RpcHeaders: {
51
42
  fields: {
52
- status: { type: "uint32", id: 1 },
53
- message: { type: "string", id: 2 },
54
- body: { type: "bytes", id: 3 },
55
- metadata: { keyType: "string", type: "bytes", id: 4 },
43
+ metadata: { keyType: "string", type: "bytes", id: 1 },
44
+ extensions: { rule: "repeated", type: "uint64", id: 2 },
56
45
  },
57
46
  },
58
- RpcStreamFrame: {
47
+ RpcStatus: {
59
48
  fields: {
60
- kind: { type: "RpcStreamFrameKind", id: 1 },
61
- status: { type: "uint32", id: 2 },
62
- message: { type: "string", id: 3 },
63
- body: { type: "bytes", id: 4 },
64
- 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 },
65
52
  },
66
53
  },
67
54
  },
@@ -70,8 +57,8 @@ const wireRoot = protobuf.Root.fromJSON({
70
57
  });
71
58
 
72
59
  export const RpcRequest = wireRoot.lookupType("trevrpc.RpcRequest");
73
- export const RpcResponse = wireRoot.lookupType("trevrpc.RpcResponse");
74
- export const RpcStreamFrame = wireRoot.lookupType("trevrpc.RpcStreamFrame");
60
+ export const RpcHeaders = wireRoot.lookupType("trevrpc.RpcHeaders");
61
+ export const RpcStatus = wireRoot.lookupType("trevrpc.RpcStatus");
75
62
 
76
63
  const MaxFieldNumber = 0x1fffffff;
77
64
  const MaxUint32 = 0xffffffffn;