@trevrpc/trevrpc-js 1.1.0 → 1.3.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 +34 -5
- package/package.json +12 -4
- package/src/call.js +401 -58
- package/src/channel.js +47 -10
- package/src/client.js +93 -22
- package/src/compression.js +330 -0
- package/src/connection-metadata.js +114 -0
- package/src/datagram.js +48 -27
- package/src/framing.js +23 -6
- package/src/generator.js +134 -14
- package/src/index.d.ts +75 -14
- package/src/qmux/session.js +2 -1
- package/src/stream-compression.js +298 -0
- package/src/wire.js +43 -11
- package/src/zstd.d.ts +35 -0
- package/src/zstd.js +407 -0
package/src/index.d.ts
CHANGED
|
@@ -53,8 +53,8 @@ export interface CallOptions {
|
|
|
53
53
|
/** Cancels the call. */
|
|
54
54
|
signal?: AbortSignal;
|
|
55
55
|
/**
|
|
56
|
-
* Called with the response headers, empty when the server sent none, as soon as the first
|
|
57
|
-
*
|
|
56
|
+
* Called with the response headers, empty when the server sent none, as soon as they, the first
|
|
57
|
+
* message or the final status arrive. A callback that throws fails the call.
|
|
58
58
|
*/
|
|
59
59
|
onResponseHeaders?(headers: Metadata): void;
|
|
60
60
|
/**
|
|
@@ -82,6 +82,16 @@ export interface DatagramTransport {
|
|
|
82
82
|
readonly writable?: WritableStream<Uint8Array>;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
declare const compressionBrand: unique symbol;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Compression settings, made by `createCompression()` from `@trevrpc/trevrpc-js/zstd`: the zstd
|
|
89
|
+
* dictionaries a channel holds and the message types each compresses. See `ConnectOptions`.
|
|
90
|
+
*/
|
|
91
|
+
export interface Compression {
|
|
92
|
+
readonly [compressionBrand]: true;
|
|
93
|
+
}
|
|
94
|
+
|
|
85
95
|
/**
|
|
86
96
|
* Anything that opens a bidirectional byte stream per call, such as a Channel or WebTransport.
|
|
87
97
|
* Calls to methods that mark a direction unreliable use the `datagrams` of the session that opens
|
|
@@ -91,14 +101,17 @@ export interface Transport {
|
|
|
91
101
|
createBidirectionalStream(): Promise<TransportStream>;
|
|
92
102
|
/** Datagrams of the session that the next stream opens on; absent without datagrams. */
|
|
93
103
|
readonly datagrams?: DatagramTransport;
|
|
104
|
+
/** Compression settings for calls on this transport; absent without compression. */
|
|
105
|
+
readonly compression?: Compression;
|
|
94
106
|
/**
|
|
95
|
-
* Like `createBidirectionalStream()`, but also returns the
|
|
96
|
-
*
|
|
97
|
-
* never mixes the stream of one session
|
|
107
|
+
* Like `createBidirectionalStream()`, but also returns the session that opened the stream.
|
|
108
|
+
* Calls negotiate dictionaries and open datagram flows per session, so transports whose session
|
|
109
|
+
* can change, such as a Channel, implement it so a call never mixes the stream of one session
|
|
110
|
+
* with the state of another.
|
|
98
111
|
*/
|
|
99
|
-
|
|
112
|
+
createBidirectionalStreamWithSession?(): Promise<{
|
|
100
113
|
stream: TransportStream;
|
|
101
|
-
datagrams?: DatagramTransport;
|
|
114
|
+
session: { readonly datagrams?: DatagramTransport };
|
|
102
115
|
}>;
|
|
103
116
|
}
|
|
104
117
|
|
|
@@ -124,6 +137,24 @@ export interface RpcMethodDescriptor {
|
|
|
124
137
|
* client accepts them as datagrams on any session that has them.
|
|
125
138
|
*/
|
|
126
139
|
unreliableResponses?: boolean;
|
|
140
|
+
/**
|
|
141
|
+
* The method's request messages may be stream-compressed (`compression: COMPRESSION_STREAM` in
|
|
142
|
+
* the `trevrpc.method` option, on a method that streams requests, whose request type has no
|
|
143
|
+
* field marked `debug_redact`). Calls on a transport with compression then compress them with one zstd
|
|
144
|
+
* context per call, once the server agrees.
|
|
145
|
+
*/
|
|
146
|
+
compressStreamRequests?: boolean;
|
|
147
|
+
/** The method's response messages may be stream-compressed; see `compressStreamRequests`. */
|
|
148
|
+
compressStreamResponses?: boolean;
|
|
149
|
+
/**
|
|
150
|
+
* The method's request messages may be compressed without a dictionary: one at a time, from 256
|
|
151
|
+
* bytes, where no dictionary applies to their type and the server decodes it. The generator
|
|
152
|
+
* sets it for `COMPRESSION_MESSAGE` and `COMPRESSION_STREAM`, except for message types with a
|
|
153
|
+
* field marked `debug_redact`.
|
|
154
|
+
*/
|
|
155
|
+
compressRequests?: boolean;
|
|
156
|
+
/** The method's response messages may be compressed without a dictionary; see `compressRequests`. */
|
|
157
|
+
compressResponses?: boolean;
|
|
127
158
|
}
|
|
128
159
|
|
|
129
160
|
export interface RpcServiceDescriptor {
|
|
@@ -148,8 +179,9 @@ export interface ResponseStream<TResponse> extends AsyncIterable<TResponse> {
|
|
|
148
179
|
/** Settles with the final status, or rejects with the error that ended the call. */
|
|
149
180
|
readonly status: Promise<StreamStatus>;
|
|
150
181
|
/**
|
|
151
|
-
* The response headers, empty when the server sent none. Settles when the first
|
|
152
|
-
*
|
|
182
|
+
* The response headers, empty when the server sent none. Settles when they, the first message or
|
|
183
|
+
* the final status arrive, before the first message is received, or rejects with the error that
|
|
184
|
+
* ended the call.
|
|
153
185
|
*/
|
|
154
186
|
readonly headers: Promise<Metadata>;
|
|
155
187
|
/** Cancels the call unless it already finished. */
|
|
@@ -197,8 +229,9 @@ export interface BidirectionalStreamingCall<TRequest, TResponse> extends AsyncIt
|
|
|
197
229
|
/** Settles with the final status, or rejects with the error that ended the call. */
|
|
198
230
|
readonly status: Promise<StreamStatus>;
|
|
199
231
|
/**
|
|
200
|
-
* The response headers, empty when the server sent none. Settles when the first
|
|
201
|
-
*
|
|
232
|
+
* The response headers, empty when the server sent none. Settles when they, the first message or
|
|
233
|
+
* the final status arrive, before the first message is received, or rejects with the error that
|
|
234
|
+
* ended the call.
|
|
202
235
|
*/
|
|
203
236
|
readonly headers: Promise<Metadata>;
|
|
204
237
|
/**
|
|
@@ -298,6 +331,25 @@ export interface ConnectOptions {
|
|
|
298
331
|
* `protocols: ["trevrpc-1"]`, the wire version, which cannot be changed.
|
|
299
332
|
*/
|
|
300
333
|
WebTransport?: new (url: string | URL, options?: object) => WebTransportSessionLike;
|
|
334
|
+
/**
|
|
335
|
+
* Compresses messages with the dictionaries these settings hold, from `createCompression()` in
|
|
336
|
+
* `@trevrpc/trevrpc-js/zstd`. Dictionaries are negotiated with the server per session, so a
|
|
337
|
+
* message whose dictionary the server lacks travels uncompressed. Compression settings also
|
|
338
|
+
* enable stream compression on methods that allow it, even without dictionaries.
|
|
339
|
+
*/
|
|
340
|
+
compression?: Compression;
|
|
341
|
+
/**
|
|
342
|
+
* The memory, in bytes, the stream compression contexts of this channel's calls may hold.
|
|
343
|
+
* Defaults to 64 MiB; 0 turns stream compression off. Calls beyond it compress per message.
|
|
344
|
+
*/
|
|
345
|
+
streamCompressionMemory?: number;
|
|
346
|
+
/**
|
|
347
|
+
* Request metadata for all of the channel's calls, such as a bearer token, which crosses each
|
|
348
|
+
* connection once instead of in every request. A call's own `metadata` wins over an entry with
|
|
349
|
+
* the same key. Keys are lowercase ASCII and the `trevrpc-` prefix is reserved, as for call
|
|
350
|
+
* metadata. Replace it with `Channel.setConnectionMetadata()`.
|
|
351
|
+
*/
|
|
352
|
+
connectionMetadata?: MetadataInput;
|
|
301
353
|
/** Cancels the initial connection attempt only. */
|
|
302
354
|
signal?: AbortSignal;
|
|
303
355
|
/** Receives channel state transitions. */
|
|
@@ -336,11 +388,20 @@ export class Channel implements Transport {
|
|
|
336
388
|
readonly state: ChannelState;
|
|
337
389
|
/** The transport of the current or last session. */
|
|
338
390
|
readonly transport: TransportKind;
|
|
391
|
+
/** The compression settings from the connect options. */
|
|
392
|
+
readonly compression?: Compression;
|
|
393
|
+
/**
|
|
394
|
+
* Replaces the channel's connection metadata (see `ConnectOptions.connectionMetadata`); empty or
|
|
395
|
+
* missing metadata removes it. Calls started after this use the new metadata, and each
|
|
396
|
+
* connection sends it again before referring to it. Throws if it is invalid, and then keeps the
|
|
397
|
+
* old.
|
|
398
|
+
*/
|
|
399
|
+
setConnectionMetadata(metadata?: MetadataInput): void;
|
|
339
400
|
createBidirectionalStream(): Promise<TransportStream>;
|
|
340
|
-
/** Opens a stream together with the
|
|
341
|
-
|
|
401
|
+
/** Opens a stream together with the session that carries it. */
|
|
402
|
+
createBidirectionalStreamWithSession(): Promise<{
|
|
342
403
|
stream: TransportStream;
|
|
343
|
-
datagrams?: DatagramTransport;
|
|
404
|
+
session: { readonly datagrams?: DatagramTransport };
|
|
344
405
|
}>;
|
|
345
406
|
/** Resolves once the channel is ready, immediately if it already is. */
|
|
346
407
|
waitUntilReady(): Promise<void>;
|
package/src/qmux/session.js
CHANGED
|
@@ -66,7 +66,8 @@ export class QMuxWebSocketSession {
|
|
|
66
66
|
#pending = new FrameWriter();
|
|
67
67
|
#flushScheduled = false;
|
|
68
68
|
#waiters = new Set();
|
|
69
|
-
|
|
69
|
+
/** The highest QX_PING request sequence received, -1 before the first: a peer may start at 0. */
|
|
70
|
+
#pingReceived = -1;
|
|
70
71
|
#closedError = null;
|
|
71
72
|
#lastActivity = Date.now();
|
|
72
73
|
#idleTimer;
|
|
@@ -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/wire.js
CHANGED
|
@@ -11,6 +11,13 @@ export const RpcKind = Object.freeze({
|
|
|
11
11
|
BidirectionalStreaming: 3,
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
+
/** The values of `RpcRequest.stream_compression`. */
|
|
15
|
+
export const StreamCompression = Object.freeze({
|
|
16
|
+
None: 0,
|
|
17
|
+
Enabled: 1,
|
|
18
|
+
Optimistic: 2,
|
|
19
|
+
});
|
|
20
|
+
|
|
14
21
|
const wireRoot = protobuf.Root.fromJSON({
|
|
15
22
|
nested: {
|
|
16
23
|
trevrpc: {
|
|
@@ -23,25 +30,48 @@ const wireRoot = protobuf.Root.fromJSON({
|
|
|
23
30
|
BidirectionalStreaming: 3,
|
|
24
31
|
},
|
|
25
32
|
},
|
|
33
|
+
StreamCompression: {
|
|
34
|
+
values: {
|
|
35
|
+
STREAM_COMPRESSION_NONE: 0,
|
|
36
|
+
STREAM_COMPRESSION_ENABLED: 1,
|
|
37
|
+
STREAM_COMPRESSION_OPTIMISTIC: 2,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
26
40
|
RpcRequest: {
|
|
27
41
|
fields: {
|
|
28
42
|
method: { type: "string", id: 1 },
|
|
29
|
-
metadata: { keyType: "string", type: "bytes", id:
|
|
30
|
-
kind: { type: "RpcKind", id:
|
|
31
|
-
timeoutNanos: { type: "uint64", id:
|
|
32
|
-
|
|
33
|
-
|
|
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 },
|
|
34
51
|
},
|
|
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
|
-
],
|
|
40
52
|
},
|
|
41
53
|
RpcHeaders: {
|
|
42
54
|
fields: {
|
|
43
55
|
metadata: { keyType: "string", type: "bytes", id: 1 },
|
|
44
|
-
|
|
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 },
|
|
45
75
|
},
|
|
46
76
|
},
|
|
47
77
|
RpcStatus: {
|
|
@@ -49,6 +79,7 @@ const wireRoot = protobuf.Root.fromJSON({
|
|
|
49
79
|
code: { type: "uint32", id: 1 },
|
|
50
80
|
message: { type: "string", id: 2 },
|
|
51
81
|
metadata: { keyType: "string", type: "bytes", id: 3 },
|
|
82
|
+
refusedStreamCompression: { type: "bool", id: 4 },
|
|
52
83
|
},
|
|
53
84
|
},
|
|
54
85
|
},
|
|
@@ -58,6 +89,7 @@ const wireRoot = protobuf.Root.fromJSON({
|
|
|
58
89
|
|
|
59
90
|
export const RpcRequest = wireRoot.lookupType("trevrpc.RpcRequest");
|
|
60
91
|
export const RpcHeaders = wireRoot.lookupType("trevrpc.RpcHeaders");
|
|
92
|
+
export const RpcAnswer = wireRoot.lookupType("trevrpc.RpcAnswer");
|
|
61
93
|
export const RpcStatus = wireRoot.lookupType("trevrpc.RpcStatus");
|
|
62
94
|
|
|
63
95
|
const MaxFieldNumber = 0x1fffffff;
|
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>;
|