@trevrpc/trevrpc-js 1.4.0 → 1.5.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 +46 -0
- package/package.json +1 -1
- package/src/index.d.ts +8 -0
- package/src/status.js +4 -0
package/README.md
CHANGED
|
@@ -142,6 +142,48 @@ call.
|
|
|
142
142
|
|
|
143
143
|
- To find the final status while the caller only sends, a client-streaming or bidirectional call reads the response ahead of `recv()` while its request side is open: at most 32 frames or 1 MiB (plus one frame of at most `maxFrameSize`) are held; beyond that reading pauses and flow control applies until `recv()` takes messages. Responses read ahead are delivered in order, including before an error that ended the stream.
|
|
144
144
|
|
|
145
|
+
### Rendering long streams
|
|
146
|
+
|
|
147
|
+
Once response messages are buffered, `for await` hands them over without returning to the event loop: each `next()` resolves as a microtask, as with any async iterator over a `fetch` body. When the network delivers faster than the page consumes, which is always the case on localhost, a loop over a long stream runs to its end before the browser renders anything, timers fire, or input is handled. The client does not yield on its own, because a timer-based yield would slow streams in hidden tabs and add latency for consumers that do not render.
|
|
148
|
+
|
|
149
|
+
A page that renders a stream should batch messages per animation frame and, after a time budget, yield with a timer so that frame can render:
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
let batch = [];
|
|
153
|
+
let frame;
|
|
154
|
+
const flush = () => {
|
|
155
|
+
frame = undefined;
|
|
156
|
+
store.append(batch);
|
|
157
|
+
batch = [];
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
let sliceStart = performance.now();
|
|
161
|
+
try {
|
|
162
|
+
for await (const row of stream) {
|
|
163
|
+
batch.push(row);
|
|
164
|
+
frame ??= requestAnimationFrame(flush);
|
|
165
|
+
if (performance.now() - sliceStart > 8) {
|
|
166
|
+
if (document.hidden) {
|
|
167
|
+
cancelAnimationFrame(frame);
|
|
168
|
+
flush();
|
|
169
|
+
} else {
|
|
170
|
+
await new Promise((resolve) => setTimeout(resolve));
|
|
171
|
+
}
|
|
172
|
+
sliceStart = performance.now();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} finally {
|
|
176
|
+
if (frame !== undefined) {
|
|
177
|
+
cancelAnimationFrame(frame);
|
|
178
|
+
flush();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
- `scheduler.yield()` does not help: Chromium resumes its continuation before rendering, so every message still lands in one frame.
|
|
184
|
+
- Hidden tabs run no animation frames and throttle timers to about one per second, so while `document.hidden` the loop applies the batch itself after each time budget instead of yielding. It holds at most one budget's worth of messages, and a stream in a background tab keeps its speed.
|
|
185
|
+
- If the stream fails or the loop is left early, which cancels the call, the `finally` still applies the messages received before.
|
|
186
|
+
|
|
145
187
|
### Connections
|
|
146
188
|
|
|
147
189
|
`connect()` opens a session and resolves when it is ready; it rejects if no transport can establish one. After that the `Channel` re-establishes the session in the background with exponential backoff whenever it is lost. Calls fail fast with `Unavailable` while `channel.state` is `"reconnecting"` and are never replayed; use `await channel.waitUntilReady()` or the `onStateChange` option to follow the state. `channel.close()` stops reconnecting.
|
|
@@ -202,6 +244,10 @@ const client = new GreeterClient(channel, { interceptors: [logCalls] });
|
|
|
202
244
|
- An interceptor that throws fails the call with what it threw before it starts; the finish functions of earlier interceptors still run.
|
|
203
245
|
- A client-streaming call has a `status` promise too, like the other streaming calls.
|
|
204
246
|
|
|
247
|
+
### Effect
|
|
248
|
+
|
|
249
|
+
The separate package [`@trevrpc/effect`](effect) wraps a generated client for [Effect](https://effect.website): `createEffectClient(GreeterService, new GreeterClient(channel))` returns a client whose unary calls are effects, whose server-streaming calls are streams, and whose client-streaming and bidirectional calls take their requests as a stream. Interrupting an effect or stream cancels its call. `TrevRpcError` has a `_tag` of `"TrevRpcError"`, so `Effect.catchTag` matches it.
|
|
250
|
+
|
|
205
251
|
### Compression
|
|
206
252
|
|
|
207
253
|
Messages can travel compressed with zstd and a dictionary trained on them (see [Compression](https://trev.zip/llc/TrevRPC/wiki/Compression)). The codec lives in a separate entry point, `@trevrpc/trevrpc-js/zstd`, which loads libzstd as WebAssembly (about 85 KB gzipped), so applications without compression never load it:
|
package/package.json
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -166,6 +166,8 @@ export type Metadata = Record<string, Uint8Array>;
|
|
|
166
166
|
|
|
167
167
|
/** Error carrying a TrevRPC status code, message, and metadata. */
|
|
168
168
|
export class TrevRpcError extends Error {
|
|
169
|
+
/** Always `"TrevRpcError"`, for matching errors by tag, as Effect's `catchTag` does. */
|
|
170
|
+
readonly _tag: "TrevRpcError";
|
|
169
171
|
readonly code: StatusCode;
|
|
170
172
|
readonly statusMessage: string;
|
|
171
173
|
readonly metadata: Metadata;
|
|
@@ -391,6 +393,9 @@ export interface StreamStatus {
|
|
|
391
393
|
/**
|
|
392
394
|
* Responses of a server-streaming call. Iteration ends after the final OK status and throws a
|
|
393
395
|
* TrevRpcError for any other outcome. Leaving the loop early cancels the call.
|
|
396
|
+
*
|
|
397
|
+
* Buffered messages are delivered without returning to the event loop, so a page that renders a
|
|
398
|
+
* long stream must yield to let the browser render; see "Rendering long streams" in the guide.
|
|
394
399
|
*/
|
|
395
400
|
export interface ResponseStream<TResponse> extends AsyncIterable<TResponse> {
|
|
396
401
|
/** Settles with the final status, or rejects with the error that ended the call. */
|
|
@@ -443,6 +448,9 @@ export interface ClientStreamingCall<TRequest, TResponse> {
|
|
|
443
448
|
* Response datagrams are delivered after the response headers, merged with the stream's messages
|
|
444
449
|
* in arrival order, and dropped when the call's queue is full (32 messages or 1 MiB) or the call
|
|
445
450
|
* has ended. Without datagrams, such as over WebSocket, everything stays on the stream.
|
|
451
|
+
*
|
|
452
|
+
* Like a `ResponseStream`, iteration delivers buffered messages without returning to the event
|
|
453
|
+
* loop.
|
|
446
454
|
*/
|
|
447
455
|
export interface BidirectionalStreamingCall<TRequest, TResponse> extends AsyncIterable<TResponse> {
|
|
448
456
|
/** Settles with the final status, or rejects with the error that ended the call. */
|
package/src/status.js
CHANGED
|
@@ -37,6 +37,10 @@ export class TrevRpcError extends Error {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
// A discriminant for libraries that match errors by tag, such as Effect's `catchTag`. It is on the
|
|
41
|
+
// prototype, so errors carry no extra own property.
|
|
42
|
+
Object.defineProperty(TrevRpcError.prototype, "_tag", { value: "TrevRpcError" });
|
|
43
|
+
|
|
40
44
|
/** Converts a number into a known status code, defaulting unknown values to Unknown. */
|
|
41
45
|
export function codeFromNumber(code) {
|
|
42
46
|
return Number.isInteger(code) && code >= 0 && code < CodeNames.length ? code : Code.Unknown;
|