@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trevrpc/trevrpc-js",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Browser WebTransport client and protobuf.js code generator for TrevRPC",
5
5
  "homepage": "https://trev.zip/llc/TrevRPC",
6
6
  "bugs": {
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;