@trevrpc/trevrpc-js 1.0.0 → 1.2.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 +86 -18
- package/bin/protoc-gen-trevrpc-js.js +1 -1
- package/package.json +17 -12
- package/src/call.js +616 -40
- package/src/channel.js +190 -18
- package/src/client.js +555 -106
- package/src/compression.js +330 -0
- package/src/connection-metadata.js +114 -0
- package/src/datagram.js +357 -0
- package/src/framing.js +124 -110
- package/src/generator.js +147 -16
- package/src/index.d.ts +223 -17
- package/src/qmux/frames.js +262 -0
- package/src/qmux/params.js +104 -0
- package/src/qmux/session.js +740 -0
- package/src/race.js +90 -0
- package/src/status.js +1 -1
- package/src/stream-compression.js +298 -0
- package/src/varint.js +73 -0
- package/src/wire.js +47 -28
- package/src/zstd.d.ts +35 -0
- package/src/zstd.js +407 -0
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
|
|
51
|
+
export function statusError(code, message = "", metadata = {}, cause) {
|
|
52
52
|
return new TrevRpcError(code, message, metadata, { cause });
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
// Stream compression: each direction of a call may compress its messages with one zstd context
|
|
2
|
+
// that lasts as long as the call, sent as ZSTD_STREAM_MESSAGE frames (see the "Stream Compression"
|
|
3
|
+
// section of the wire format). A client offers it in its REQUEST on methods that allow it, and
|
|
4
|
+
// may compress its requests once the server's ANSWER accepts them, or from the start when it offers
|
|
5
|
+
// optimistically. Contexts hold memory for the whole call, so every channel has a budget for them.
|
|
6
|
+
import { encodeMessage, parseStreamChunk } from "./compression.js";
|
|
7
|
+
import { FrameType } from "./framing.js";
|
|
8
|
+
import { internal, resourceExhausted } from "./status.js";
|
|
9
|
+
import { readVarint, varintLength, writeVarint } from "./varint.js";
|
|
10
|
+
import { StreamCompression } from "./wire.js";
|
|
11
|
+
|
|
12
|
+
/** The window of every stream context, as a base-2 logarithm: the wire format fixes it at 64 KiB. */
|
|
13
|
+
const StreamWindowLog = 16;
|
|
14
|
+
/** A direction's context starts with this message; earlier ones are compressed per message. */
|
|
15
|
+
const StreamCompressionStart = 3;
|
|
16
|
+
|
|
17
|
+
/** The default memory budget of one channel's stream contexts, in bytes. */
|
|
18
|
+
export const DefaultStreamCompressionMemory = 64 * 1024 * 1024;
|
|
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
|
+
/** The memory stream contexts may hold, shared by the calls of one channel. */
|
|
24
|
+
export class StreamBudget {
|
|
25
|
+
#available;
|
|
26
|
+
|
|
27
|
+
constructor(bytes = DefaultStreamCompressionMemory) {
|
|
28
|
+
if (!(Number.isSafeInteger(bytes) && bytes >= 0)) {
|
|
29
|
+
throw new TypeError("streamCompressionMemory must be a non-negative integer number of bytes");
|
|
30
|
+
}
|
|
31
|
+
this.#available = bytes;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Whether `bytes` fit now. */
|
|
35
|
+
fits(bytes) {
|
|
36
|
+
return bytes <= this.#available;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Sets aside `bytes`, returning a function that gives them back, or undefined if they do not fit. */
|
|
40
|
+
reserve(bytes) {
|
|
41
|
+
if (bytes > this.#available) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
this.#available -= bytes;
|
|
45
|
+
let held = true;
|
|
46
|
+
return () => {
|
|
47
|
+
if (held) {
|
|
48
|
+
held = false;
|
|
49
|
+
this.#available += bytes;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const defaultBudgets = new WeakMap();
|
|
56
|
+
|
|
57
|
+
/** The stream budget of a transport: a Channel's own, or a default one per other transport. */
|
|
58
|
+
export function streamBudget(transport) {
|
|
59
|
+
const budget = transport[StreamBudgetKey];
|
|
60
|
+
if (budget !== undefined) {
|
|
61
|
+
return budget;
|
|
62
|
+
}
|
|
63
|
+
let fallback = defaultBudgets.get(transport);
|
|
64
|
+
if (fallback === undefined) {
|
|
65
|
+
fallback = new StreamBudget();
|
|
66
|
+
defaultBudgets.set(transport, fallback);
|
|
67
|
+
}
|
|
68
|
+
return fallback;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The stream compression of one call on the client. `directions` is `{ requests, responses }`:
|
|
73
|
+
* the directions the method allows it on. `session` picks dictionaries for request contexts, and
|
|
74
|
+
* `accept` maps the dictionary IDs the call accepts on its responses to their dictionaries, which
|
|
75
|
+
* response frames may start from.
|
|
76
|
+
*/
|
|
77
|
+
export class CallStreams {
|
|
78
|
+
#compression;
|
|
79
|
+
#budget;
|
|
80
|
+
#requests;
|
|
81
|
+
#responses;
|
|
82
|
+
#session;
|
|
83
|
+
#requestType;
|
|
84
|
+
#accept;
|
|
85
|
+
/** Whether the REQUEST offered stream compression. */
|
|
86
|
+
#offered = false;
|
|
87
|
+
/** Whether requests may be stream-compressed: the call is optimistic, or the server accepted. */
|
|
88
|
+
#requestsEnabled = false;
|
|
89
|
+
#requestsSent = 0;
|
|
90
|
+
/** Whether the request direction stays per-message for the rest of the call. */
|
|
91
|
+
#requestsStopped = false;
|
|
92
|
+
#encoder;
|
|
93
|
+
#decoder;
|
|
94
|
+
/** Whether a response frame is open, so the next non-empty chunk continues it. */
|
|
95
|
+
#inFrame = false;
|
|
96
|
+
#closed = false;
|
|
97
|
+
#releases = [];
|
|
98
|
+
|
|
99
|
+
constructor(compression, budget, directions, { session, requestType, accept }) {
|
|
100
|
+
this.#compression = compression;
|
|
101
|
+
this.#budget = budget;
|
|
102
|
+
this.#requests = directions.requests;
|
|
103
|
+
this.#responses = directions.responses;
|
|
104
|
+
this.#session = session;
|
|
105
|
+
this.#requestType = requestType;
|
|
106
|
+
this.#accept = accept;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Sets the REQUEST's `streamCompression` offer, and returns whether it is optimistic. A call
|
|
111
|
+
* whose responses may be compressed first reserves their decompressing context, and offers
|
|
112
|
+
* nothing if that does not fit; a call that only compresses requests offers so the server can
|
|
113
|
+
* answer. With `optimistic`, a call whose requests may be compressed offers optimistically if
|
|
114
|
+
* the budget has room for their compressing context now: its requests are enabled from the
|
|
115
|
+
* start, and the server accepts or refuses the call at dispatch.
|
|
116
|
+
*/
|
|
117
|
+
offer(fields, optimistic = false) {
|
|
118
|
+
if (this.#responses) {
|
|
119
|
+
const release = this.#budget.reserve(this.#compression.streamDecoderCost(StreamWindowLog));
|
|
120
|
+
if (release === undefined) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
this.#releases.push(release);
|
|
124
|
+
}
|
|
125
|
+
this.#offered = true;
|
|
126
|
+
if (
|
|
127
|
+
optimistic &&
|
|
128
|
+
this.#requests &&
|
|
129
|
+
this.#budget.fits(this.#compression.streamEncoderCost(StreamWindowLog))
|
|
130
|
+
) {
|
|
131
|
+
fields.streamCompression = StreamCompression.Optimistic;
|
|
132
|
+
this.#requestsEnabled = true;
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
fields.streamCompression = StreamCompression.Enabled;
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Records that the server's ANSWER accepted stream-compressed requests. */
|
|
140
|
+
accept() {
|
|
141
|
+
if (this.#offered && this.#requests) {
|
|
142
|
+
this.#requestsEnabled = true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Encodes a request message: through the direction's context once it exists, and per message,
|
|
148
|
+
* with `dictionary`, before. The context starts with the direction's third message if the
|
|
149
|
+
* server's answer allows it and its memory fits the budget.
|
|
150
|
+
*/
|
|
151
|
+
encode(body, dictionary) {
|
|
152
|
+
if (!this.#requests) {
|
|
153
|
+
return encodeMessage(body, dictionary);
|
|
154
|
+
}
|
|
155
|
+
this.#requestsSent += 1;
|
|
156
|
+
if (
|
|
157
|
+
this.#encoder === undefined &&
|
|
158
|
+
!this.#requestsStopped &&
|
|
159
|
+
this.#requestsEnabled &&
|
|
160
|
+
this.#requestsSent >= StreamCompressionStart
|
|
161
|
+
) {
|
|
162
|
+
this.#startEncoder();
|
|
163
|
+
}
|
|
164
|
+
if (this.#encoder === undefined) {
|
|
165
|
+
return encodeMessage(body, dictionary);
|
|
166
|
+
}
|
|
167
|
+
const chunk = this.#encoder.encode(body);
|
|
168
|
+
const payload = new Uint8Array(varintLength(body.byteLength) + chunk.byteLength);
|
|
169
|
+
payload.set(chunk, writeVarint(payload, 0, body.byteLength));
|
|
170
|
+
return { type: FrameType.ZstdStreamMessage, payload, size: body.byteLength };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#startEncoder() {
|
|
174
|
+
const release = this.#budget.reserve(this.#compression.streamEncoderCost(StreamWindowLog));
|
|
175
|
+
if (release === undefined) {
|
|
176
|
+
this.#requestsStopped = true;
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
this.#encoder = this.#compression.streamEncoder(
|
|
181
|
+
this.#session.requestDictionary(this.#requestType),
|
|
182
|
+
StreamWindowLog,
|
|
183
|
+
);
|
|
184
|
+
} catch {
|
|
185
|
+
release();
|
|
186
|
+
this.#requestsStopped = true;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
this.#releases.push(release);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Decompresses the payload of a ZSTD_STREAM_MESSAGE frame on the response. A message above
|
|
194
|
+
* maxSize is ResourceExhausted, found before decompressing; every other fault is Internal, and
|
|
195
|
+
* the call cannot continue after it.
|
|
196
|
+
*/
|
|
197
|
+
receive(payload, maxSize) {
|
|
198
|
+
if (!(this.#offered && this.#responses) || this.#closed) {
|
|
199
|
+
throw internal("stream-compressed message where stream compression is not enabled");
|
|
200
|
+
}
|
|
201
|
+
const size = readVarint(payload, 0);
|
|
202
|
+
if (size === null || typeof size.value !== "number") {
|
|
203
|
+
throw internal("invalid ZSTD_STREAM_MESSAGE payload");
|
|
204
|
+
}
|
|
205
|
+
if (size.value > maxSize) {
|
|
206
|
+
throw resourceExhausted(
|
|
207
|
+
`compressed message of ${size.value} bytes exceeds maximum ${maxSize}`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
const chunk = payload.subarray(size.next);
|
|
211
|
+
const info = parseStreamChunk(chunk, !this.#inFrame, StreamWindowLog);
|
|
212
|
+
let frameDictionary;
|
|
213
|
+
if (info.startsFrame) {
|
|
214
|
+
frameDictionary = null;
|
|
215
|
+
if (info.dictionaryId !== 0) {
|
|
216
|
+
frameDictionary = this.#accept?.get(info.dictionaryId);
|
|
217
|
+
if (frameDictionary === undefined) {
|
|
218
|
+
throw internal(
|
|
219
|
+
`stream-compressed message uses dictionary ${info.dictionaryId}, which is not accepted`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
let message;
|
|
225
|
+
try {
|
|
226
|
+
this.#decoder ??= this.#compression.streamDecoder(StreamWindowLog);
|
|
227
|
+
message = this.#decoder.decode(chunk, size.value, frameDictionary);
|
|
228
|
+
} catch {
|
|
229
|
+
throw internal("invalid ZSTD_STREAM_MESSAGE payload");
|
|
230
|
+
}
|
|
231
|
+
if (chunk.byteLength > 0) {
|
|
232
|
+
this.#inFrame = !info.endsFrame;
|
|
233
|
+
}
|
|
234
|
+
return message;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Frees the call's contexts and returns their memory; every way a call ends gets here. */
|
|
238
|
+
close() {
|
|
239
|
+
this.#closed = true;
|
|
240
|
+
this.#encoder?.close();
|
|
241
|
+
this.#decoder?.close();
|
|
242
|
+
this.#encoder = undefined;
|
|
243
|
+
this.#decoder = undefined;
|
|
244
|
+
this.#requestsStopped = true;
|
|
245
|
+
for (const release of this.#releases.splice(0)) {
|
|
246
|
+
release();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** The most request bytes an optimistic call keeps for a replay. */
|
|
252
|
+
export const MaxReplayBytes = 1 << 20;
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* The requests an optimistic call has sent while the server has not yet accepted or refused it:
|
|
256
|
+
* the marshalled messages, before compression, so a refused call can be replayed. They are held
|
|
257
|
+
* within MaxReplayBytes and the channel's stream budget; a request that does not fit waits for
|
|
258
|
+
* the decision. `decided` settles with "accepted", "refused" or "ended".
|
|
259
|
+
*/
|
|
260
|
+
export class ReplayCopies {
|
|
261
|
+
#budget;
|
|
262
|
+
#bodies = [];
|
|
263
|
+
#bytes = 0;
|
|
264
|
+
#releases = [];
|
|
265
|
+
#resolve;
|
|
266
|
+
decided = new Promise((resolve) => {
|
|
267
|
+
this.#resolve = resolve;
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
constructor(budget) {
|
|
271
|
+
this.#budget = budget;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Keeps `body`, or returns false if it does not fit; the sender then waits for `decided`. */
|
|
275
|
+
keep(body) {
|
|
276
|
+
if (this.#bytes + body.byteLength > MaxReplayBytes) {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
const release = this.#budget.reserve(body.byteLength);
|
|
280
|
+
if (release === undefined) {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
this.#releases.push(release);
|
|
284
|
+
this.#bodies.push(body);
|
|
285
|
+
this.#bytes += body.byteLength;
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Settles the decision, returns the memory, and returns the kept messages in order. */
|
|
290
|
+
settle(outcome) {
|
|
291
|
+
for (const release of this.#releases.splice(0)) {
|
|
292
|
+
release();
|
|
293
|
+
}
|
|
294
|
+
this.#bytes = 0;
|
|
295
|
+
this.#resolve(outcome);
|
|
296
|
+
return this.#bodies.splice(0);
|
|
297
|
+
}
|
|
298
|
+
}
|
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,9 +11,11 @@ export const RpcKind = Object.freeze({
|
|
|
13
11
|
BidirectionalStreaming: 3,
|
|
14
12
|
});
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
/** The values of `RpcRequest.stream_compression`. */
|
|
15
|
+
export const StreamCompression = Object.freeze({
|
|
16
|
+
None: 0,
|
|
17
|
+
Enabled: 1,
|
|
18
|
+
Optimistic: 2,
|
|
19
19
|
});
|
|
20
20
|
|
|
21
21
|
const wireRoot = protobuf.Root.fromJSON({
|
|
@@ -30,38 +30,56 @@ const wireRoot = protobuf.Root.fromJSON({
|
|
|
30
30
|
BidirectionalStreaming: 3,
|
|
31
31
|
},
|
|
32
32
|
},
|
|
33
|
-
|
|
33
|
+
StreamCompression: {
|
|
34
34
|
values: {
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
STREAM_COMPRESSION_NONE: 0,
|
|
36
|
+
STREAM_COMPRESSION_ENABLED: 1,
|
|
37
|
+
STREAM_COMPRESSION_OPTIMISTIC: 2,
|
|
37
38
|
},
|
|
38
39
|
},
|
|
39
40
|
RpcRequest: {
|
|
40
41
|
fields: {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
method: { type: "string", id: 1 },
|
|
43
|
+
metadata: { keyType: "string", type: "bytes", id: 2 },
|
|
44
|
+
kind: { type: "RpcKind", id: 3 },
|
|
45
|
+
timeoutNanos: { type: "uint64", id: 4 },
|
|
46
|
+
datagramFlow: { type: "uint64", id: 5 },
|
|
47
|
+
acceptDictionaries: { rule: "repeated", type: "uint32", id: 6 },
|
|
48
|
+
dictionaryQuery: { rule: "repeated", type: "DictionaryInfo", id: 7 },
|
|
49
|
+
streamCompression: { type: "StreamCompression", id: 8 },
|
|
50
|
+
connectionMetadata: { type: "ConnectionMetadata", id: 9 },
|
|
48
51
|
},
|
|
49
52
|
},
|
|
50
|
-
|
|
53
|
+
RpcHeaders: {
|
|
51
54
|
fields: {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
metadata: { keyType: "string", type: "bytes", id: 1 },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
RpcAnswer: {
|
|
59
|
+
fields: {
|
|
60
|
+
dictionaries: { rule: "repeated", type: "DictionaryInfo", id: 1 },
|
|
61
|
+
streamCompression: { type: "bool", id: 2 },
|
|
62
|
+
connectionMetadata: { type: "uint32", id: 3 },
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
ConnectionMetadata: {
|
|
66
|
+
fields: {
|
|
67
|
+
id: { type: "uint32", id: 1 },
|
|
68
|
+
metadata: { keyType: "string", type: "bytes", id: 2 },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
DictionaryInfo: {
|
|
72
|
+
fields: {
|
|
73
|
+
id: { type: "uint32", id: 1 },
|
|
74
|
+
fingerprint: { type: "bytes", id: 2 },
|
|
56
75
|
},
|
|
57
76
|
},
|
|
58
|
-
|
|
77
|
+
RpcStatus: {
|
|
59
78
|
fields: {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
metadata: { keyType: "string", type: "bytes", id: 5 },
|
|
79
|
+
code: { type: "uint32", id: 1 },
|
|
80
|
+
message: { type: "string", id: 2 },
|
|
81
|
+
metadata: { keyType: "string", type: "bytes", id: 3 },
|
|
82
|
+
refusedStreamCompression: { type: "bool", id: 4 },
|
|
65
83
|
},
|
|
66
84
|
},
|
|
67
85
|
},
|
|
@@ -70,8 +88,9 @@ const wireRoot = protobuf.Root.fromJSON({
|
|
|
70
88
|
});
|
|
71
89
|
|
|
72
90
|
export const RpcRequest = wireRoot.lookupType("trevrpc.RpcRequest");
|
|
73
|
-
export const
|
|
74
|
-
export const
|
|
91
|
+
export const RpcHeaders = wireRoot.lookupType("trevrpc.RpcHeaders");
|
|
92
|
+
export const RpcAnswer = wireRoot.lookupType("trevrpc.RpcAnswer");
|
|
93
|
+
export const RpcStatus = wireRoot.lookupType("trevrpc.RpcStatus");
|
|
75
94
|
|
|
76
95
|
const MaxFieldNumber = 0x1fffffff;
|
|
77
96
|
const MaxUint32 = 0xffffffffn;
|
package/src/zstd.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Compression } from "./index.js";
|
|
2
|
+
|
|
3
|
+
/** A zstd dictionary and the protobuf message types it compresses. */
|
|
4
|
+
export interface DictionaryOptions {
|
|
5
|
+
/**
|
|
6
|
+
* The dictionary in zstd's format, as written by `zstd --train`. Its header carries the
|
|
7
|
+
* Dictionary_ID that identifies it to peers, which must never name different dictionaries.
|
|
8
|
+
*/
|
|
9
|
+
data: ArrayBuffer | ArrayBufferView;
|
|
10
|
+
/**
|
|
11
|
+
* Full names of the protobuf message types the dictionary compresses, such as
|
|
12
|
+
* "example.v1.Position". A message type may appear in one dictionary only. A dictionary without
|
|
13
|
+
* messages is still offered to servers, which may compress with it, but the channel never does;
|
|
14
|
+
* adding a new dictionary that way first lets clients hold it before servers use it.
|
|
15
|
+
*/
|
|
16
|
+
messages?: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CompressionOptions {
|
|
20
|
+
/** The dictionaries the channel holds. */
|
|
21
|
+
dictionaries?: readonly DictionaryOptions[];
|
|
22
|
+
/**
|
|
23
|
+
* The zstd level of outgoing messages; defaults to 1. The module keeps only the strategies of
|
|
24
|
+
* levels 1 to 4, and higher levels fall back to them.
|
|
25
|
+
*/
|
|
26
|
+
level?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Creates compression settings for `connect()`. It loads libzstd as WebAssembly (about 85 KB
|
|
31
|
+
* gzipped), digests the dictionaries, and rejects with a TypeError for an invalid dictionary, a
|
|
32
|
+
* dictionary ID used twice, or a message type in two dictionaries. Settings without dictionaries
|
|
33
|
+
* enable stream compression and compression without a dictionary alone.
|
|
34
|
+
*/
|
|
35
|
+
export function createCompression(options?: CompressionOptions): Promise<Compression>;
|