@orpc/client 2.0.0-beta.15 → 2.0.0-beta.17

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
@@ -175,7 +175,7 @@ Like what we build over at [middleapi](https://github.com/middleapi)? You can he
175
175
  <a href="https://github.com/illarionvk?ref=orpc" target="_blank" rel="noopener" title="Illarion Koperski"><img src="https://avatars.githubusercontent.com/u/5012724?u=7cfa13652f7ac5fb3c56d880e3eb3fbe40c3ea34&amp;v=4" width="32" height="32" alt="Illarion Koperski" /></a>
176
176
  <a href="https://github.com/steelbrain?ref=orpc" target="_blank" rel="noopener" title="Anees Iqbal"><img src="https://avatars.githubusercontent.com/u/4278113?u=22b80b5399eed68ac76cd58b02961b0481f1db11&amp;v=4" width="32" height="32" alt="Anees Iqbal" /></a>
177
177
  <a href="https://github.com/Scrumplex?ref=orpc" target="_blank" rel="noopener" title="Sefa Eyeoglu"><img src="https://avatars.githubusercontent.com/u/11587657?u=ab503582165c0bbff0cca47ce31c9450bb1553c9&amp;v=4" width="32" height="32" alt="Sefa Eyeoglu" /></a>
178
- <a href="https://github.com/nattstack?ref=orpc" target="_blank" rel="noopener" title="Natt"><img src="https://avatars.githubusercontent.com/u/31426677?u=fa9dbb8b3e66eb0ea3c88db5dc07f31c8c5418fe&amp;v=4" width="32" height="32" alt="Natt" /></a>
178
+ <a href="https://github.com/nattstack?ref=orpc" target="_blank" rel="noopener" title="natt"><img src="https://avatars.githubusercontent.com/u/31426677?u=fa9dbb8b3e66eb0ea3c88db5dc07f31c8c5418fe&amp;v=4" width="32" height="32" alt="natt" /></a>
179
179
  <a href="https://github.com/ChromeGG?ref=orpc" target="_blank" rel="noopener" title="Adam Tkaczyk"><img src="https://avatars.githubusercontent.com/u/39050595?u=a58ca6042a6950e94e6e92442db76ef584279bc0&amp;v=4" width="32" height="32" alt="Adam Tkaczyk" /></a>
180
180
  <a href="https://github.com/plancraft?ref=orpc" target="_blank" rel="noopener" title="plancraft"><img src="https://avatars.githubusercontent.com/u/46482287?v=4" width="32" height="32" alt="plancraft" /></a>
181
181
  </p>
@@ -1,4 +1,4 @@
1
- import { sortPlugins, value, intercept } from '@orpc/shared';
1
+ import { sortPlugins, value, intercept, once } from '@orpc/shared';
2
2
  import { toFetchBody, toFetchHeaders, toStandardLazyResponse } from '@standardserver/fetch';
3
3
  import { S as StandardLink, R as RPCLinkCodec } from '../../shared/client.BRJnOJ0R.mjs';
4
4
  import '@standardserver/core';
@@ -21,6 +21,25 @@ class CompositeFetchLinkTransportPlugin {
21
21
  }
22
22
  }
23
23
 
24
+ const GET_SUPPORTED_DUPLEX_MODE = once(() => {
25
+ try {
26
+ let duplex;
27
+ void new Request(
28
+ "https://example.com",
29
+ {
30
+ method: "POST",
31
+ body: new ReadableStream(),
32
+ get duplex() {
33
+ duplex = "half";
34
+ return "half";
35
+ }
36
+ }
37
+ );
38
+ return duplex;
39
+ } catch {
40
+ return void 0;
41
+ }
42
+ });
24
43
  class FetchLinkTransport {
25
44
  origin;
26
45
  fetch;
@@ -47,6 +66,12 @@ class FetchLinkTransport {
47
66
  signal: options.signal,
48
67
  redirect: "manual"
49
68
  };
69
+ if (body instanceof ReadableStream) {
70
+ const duplex = GET_SUPPORTED_DUPLEX_MODE();
71
+ if (duplex !== void 0) {
72
+ init.duplex = duplex;
73
+ }
74
+ }
50
75
  const response = await intercept(
51
76
  this.fetchInterceptors,
52
77
  { ...options, url, path, init },
@@ -136,6 +136,61 @@ declare class DedupeLinkPlugin<T extends ClientContext> implements StandardLinkP
136
136
  private execute;
137
137
  }
138
138
 
139
+ interface RequestCompressionLinkPluginOptions<_T extends ClientContext> {
140
+ /**
141
+ * The compression scheme to use for request compression.
142
+ *
143
+ * @default 'gzip'
144
+ */
145
+ encoding?: 'gzip' | 'deflate' | 'deflate-raw';
146
+ /**
147
+ * The minimum request size in bytes required to trigger compression.
148
+ * Requests smaller than this threshold will not be compressed to avoid overhead.
149
+ * If the request size cannot be determined, compression will still be applied.
150
+ *
151
+ * @default 1024 (1KB)
152
+ */
153
+ threshold?: number;
154
+ }
155
+ declare class RequestCompressionLinkPlugin<T extends ClientContext> implements StandardLinkPlugin<T> {
156
+ name: string;
157
+ /**
158
+ * Compression should be done after batching, to compress the final request
159
+ */
160
+ after: string[];
161
+ private readonly encoding;
162
+ private readonly threshold;
163
+ constructor(options?: RequestCompressionLinkPluginOptions<T>);
164
+ init(options: StandardLinkOptions<T>): StandardLinkOptions<T>;
165
+ }
166
+
167
+ interface ResponseCompressionLinkPluginOptions<_T extends ClientContext> {
168
+ /**
169
+ * Compression schemes to advertise via Accept-Encoding, in preference order.
170
+ * Only schemes that can be decompressed by this plugin should be listed.
171
+ *
172
+ * @default ['gzip', 'deflate']
173
+ */
174
+ encodings?: readonly ('gzip' | 'deflate' | 'deflate-raw')[];
175
+ }
176
+ /**
177
+ * Advertises Accept-Encoding on requests and decompresses response bodies
178
+ * based on the Content-Encoding header. Works at the standard link level,
179
+ * so it supports all adapters.
180
+ *
181
+ * @see {@link https://orpc.dev/docs/plugins/response-compression Response Compression Plugin Docs}
182
+ */
183
+ declare class ResponseCompressionLinkPlugin<T extends ClientContext> implements StandardLinkPlugin<T> {
184
+ name: string;
185
+ /**
186
+ * Decompression should wrap the final batch response instead of sub-responses.
187
+ */
188
+ after: string[];
189
+ private readonly encodings;
190
+ constructor(options?: ResponseCompressionLinkPluginOptions<T>);
191
+ init(options: StandardLinkOptions<T>): StandardLinkOptions<T>;
192
+ }
193
+
139
194
  interface RetryLinkPluginAttemptOptions<T extends RetryLinkPluginContext> extends StandardLinkInterceptorOptions<T> {
140
195
  /**
141
196
  * Latest retry delay advertised by the server via event metadata.
@@ -236,5 +291,5 @@ declare class RetryAfterLinkPlugin<T extends ClientContext> implements StandardL
236
291
  init(options: StandardLinkOptions<T>): StandardLinkOptions<T>;
237
292
  }
238
293
 
239
- export { BatchLinkPlugin, BatchLinkPluginError, DedupeLinkPlugin, RetryAfterLinkPlugin, RetryLinkPlugin };
240
- export type { BatchLinkPluginGroup, BatchLinkPluginMode, BatchLinkPluginOptions, DedupeLinkPluginGroup, DedupeLinkPluginOptions, RetryAfterLinkPluginOptions, RetryLinkPluginAttemptOptions, RetryLinkPluginContext, RetryLinkPluginOptions };
294
+ export { BatchLinkPlugin, BatchLinkPluginError, DedupeLinkPlugin, RequestCompressionLinkPlugin, ResponseCompressionLinkPlugin, RetryAfterLinkPlugin, RetryLinkPlugin };
295
+ export type { BatchLinkPluginGroup, BatchLinkPluginMode, BatchLinkPluginOptions, DedupeLinkPluginGroup, DedupeLinkPluginOptions, RequestCompressionLinkPluginOptions, ResponseCompressionLinkPluginOptions, RetryAfterLinkPluginOptions, RetryLinkPluginAttemptOptions, RetryLinkPluginContext, RetryLinkPluginOptions };
@@ -136,6 +136,61 @@ declare class DedupeLinkPlugin<T extends ClientContext> implements StandardLinkP
136
136
  private execute;
137
137
  }
138
138
 
139
+ interface RequestCompressionLinkPluginOptions<_T extends ClientContext> {
140
+ /**
141
+ * The compression scheme to use for request compression.
142
+ *
143
+ * @default 'gzip'
144
+ */
145
+ encoding?: 'gzip' | 'deflate' | 'deflate-raw';
146
+ /**
147
+ * The minimum request size in bytes required to trigger compression.
148
+ * Requests smaller than this threshold will not be compressed to avoid overhead.
149
+ * If the request size cannot be determined, compression will still be applied.
150
+ *
151
+ * @default 1024 (1KB)
152
+ */
153
+ threshold?: number;
154
+ }
155
+ declare class RequestCompressionLinkPlugin<T extends ClientContext> implements StandardLinkPlugin<T> {
156
+ name: string;
157
+ /**
158
+ * Compression should be done after batching, to compress the final request
159
+ */
160
+ after: string[];
161
+ private readonly encoding;
162
+ private readonly threshold;
163
+ constructor(options?: RequestCompressionLinkPluginOptions<T>);
164
+ init(options: StandardLinkOptions<T>): StandardLinkOptions<T>;
165
+ }
166
+
167
+ interface ResponseCompressionLinkPluginOptions<_T extends ClientContext> {
168
+ /**
169
+ * Compression schemes to advertise via Accept-Encoding, in preference order.
170
+ * Only schemes that can be decompressed by this plugin should be listed.
171
+ *
172
+ * @default ['gzip', 'deflate']
173
+ */
174
+ encodings?: readonly ('gzip' | 'deflate' | 'deflate-raw')[];
175
+ }
176
+ /**
177
+ * Advertises Accept-Encoding on requests and decompresses response bodies
178
+ * based on the Content-Encoding header. Works at the standard link level,
179
+ * so it supports all adapters.
180
+ *
181
+ * @see {@link https://orpc.dev/docs/plugins/response-compression Response Compression Plugin Docs}
182
+ */
183
+ declare class ResponseCompressionLinkPlugin<T extends ClientContext> implements StandardLinkPlugin<T> {
184
+ name: string;
185
+ /**
186
+ * Decompression should wrap the final batch response instead of sub-responses.
187
+ */
188
+ after: string[];
189
+ private readonly encodings;
190
+ constructor(options?: ResponseCompressionLinkPluginOptions<T>);
191
+ init(options: StandardLinkOptions<T>): StandardLinkOptions<T>;
192
+ }
193
+
139
194
  interface RetryLinkPluginAttemptOptions<T extends RetryLinkPluginContext> extends StandardLinkInterceptorOptions<T> {
140
195
  /**
141
196
  * Latest retry delay advertised by the server via event metadata.
@@ -236,5 +291,5 @@ declare class RetryAfterLinkPlugin<T extends ClientContext> implements StandardL
236
291
  init(options: StandardLinkOptions<T>): StandardLinkOptions<T>;
237
292
  }
238
293
 
239
- export { BatchLinkPlugin, BatchLinkPluginError, DedupeLinkPlugin, RetryAfterLinkPlugin, RetryLinkPlugin };
240
- export type { BatchLinkPluginGroup, BatchLinkPluginMode, BatchLinkPluginOptions, DedupeLinkPluginGroup, DedupeLinkPluginOptions, RetryAfterLinkPluginOptions, RetryLinkPluginAttemptOptions, RetryLinkPluginContext, RetryLinkPluginOptions };
294
+ export { BatchLinkPlugin, BatchLinkPluginError, DedupeLinkPlugin, RequestCompressionLinkPlugin, ResponseCompressionLinkPlugin, RetryAfterLinkPlugin, RetryLinkPlugin };
295
+ export type { BatchLinkPluginGroup, BatchLinkPluginMode, BatchLinkPluginOptions, DedupeLinkPluginGroup, DedupeLinkPluginOptions, RequestCompressionLinkPluginOptions, ResponseCompressionLinkPluginOptions, RetryAfterLinkPluginOptions, RetryLinkPluginAttemptOptions, RetryLinkPluginContext, RetryLinkPluginOptions };
@@ -1,6 +1,7 @@
1
- import { toArray, value, splitInHalf, stringifyJSON, isAsyncIteratorObject, defer, loadBytes, allAbortSignal, replicateAsyncIterator, replicateReadableStream, override, AsyncIteratorClass, sleep } from '@orpc/shared';
2
- import { parseStandardUrl, getEventMeta, flattenStandardHeader } from '@standardserver/core';
1
+ import { toArray, value, splitInHalf, stringifyJSON, isAsyncIteratorObject, defer, loadBytes, allAbortSignal, replicateAsyncIterator, replicateReadableStream, isCompressibleContentType, override, AsyncIteratorClass, sleep } from '@orpc/shared';
2
+ import { parseStandardUrl, flattenStandardHeader, generateContentDisposition, getEventMeta } from '@standardserver/core';
3
3
  import { ClientPeer, isServerPeerSendMessage, decodePeerMessage } from '@standardserver/peer';
4
+ import { toFetchHeaders, toStandardBody } from '@standardserver/fetch';
4
5
  import { C as COMMON_ERROR_STATUS_MAP } from '../shared/client.Dnfj8jnT.mjs';
5
6
 
6
7
  class BatchLinkPluginError extends TypeError {
@@ -226,6 +227,10 @@ async function decodeLengthPrefixedStream(stream, peer) {
226
227
  while (buffer.length >= 4) {
227
228
  const view = new DataView(buffer.buffer, buffer.byteOffset, 4);
228
229
  const length = view.getUint32(0, false);
230
+ if (length === 0) {
231
+ buffer = buffer.subarray(4);
232
+ continue;
233
+ }
229
234
  if (buffer.length < 4 + length) {
230
235
  break;
231
236
  }
@@ -387,6 +392,229 @@ function shouldDedupe(items) {
387
392
  return items.length >= 2;
388
393
  }
389
394
 
395
+ const AVG_BYTES_PER_CHAR = 1.2;
396
+ class RequestCompressionLinkPlugin {
397
+ name = "~request-compression";
398
+ /**
399
+ * Compression should be done after batching, to compress the final request
400
+ */
401
+ after = ["~batch"];
402
+ encoding;
403
+ threshold;
404
+ constructor(options = {}) {
405
+ this.encoding = options.encoding ?? "gzip";
406
+ this.threshold = options.threshold ?? 1024;
407
+ }
408
+ init(options) {
409
+ const transportInterceptor = async ({ next, ...interceptorOptions }) => {
410
+ const request = interceptorOptions.request;
411
+ const contentEncoding = flattenStandardHeader(request.headers["content-encoding"])?.trim()?.toLowerCase();
412
+ if (contentEncoding !== void 0) {
413
+ return next();
414
+ }
415
+ if (request.body instanceof ReadableStream) {
416
+ const contentLength = Number(flattenStandardHeader(request.headers["content-length"]));
417
+ if ((!Number.isFinite(contentLength) || contentLength >= this.threshold) && isCompressibleContentType(flattenStandardHeader(request.headers["content-type"]))) {
418
+ const compressedStream = request.body.pipeThrough(new CompressionStream(this.encoding));
419
+ return next({
420
+ ...interceptorOptions,
421
+ request: {
422
+ ...interceptorOptions.request,
423
+ body: compressedStream,
424
+ headers: {
425
+ ...request.headers,
426
+ "standard-server": "octet-stream",
427
+ "content-length": [],
428
+ "content-encoding": this.encoding
429
+ }
430
+ }
431
+ });
432
+ }
433
+ } else if (request.body instanceof Blob) {
434
+ if ((!Number.isFinite(request.body.size) || request.body.size >= this.threshold) && isCompressibleContentType(request.body.type)) {
435
+ const compressedStream = request.body.stream().pipeThrough(new CompressionStream(this.encoding));
436
+ const contentDisposition = request.headers["content-disposition"] ?? generateContentDisposition(
437
+ request.body instanceof File ? request.body.name : "blob"
438
+ );
439
+ return next({
440
+ ...interceptorOptions,
441
+ request: {
442
+ ...interceptorOptions.request,
443
+ body: compressedStream,
444
+ headers: {
445
+ ...request.headers,
446
+ "standard-server": "file",
447
+ "content-type": request.body.type,
448
+ "content-length": [],
449
+ "content-disposition": contentDisposition,
450
+ "content-encoding": this.encoding
451
+ }
452
+ }
453
+ });
454
+ }
455
+ } else if (request.body instanceof FormData) {
456
+ const PART_OVERHEAD = 64;
457
+ let contentLength = 0;
458
+ for (const [key, value] of request.body) {
459
+ contentLength += PART_OVERHEAD + key.length;
460
+ if (value instanceof Blob) {
461
+ if (!Number.isFinite(value.size)) {
462
+ if (!isCompressibleContentType(value.type)) {
463
+ contentLength = -Infinity;
464
+ break;
465
+ }
466
+ contentLength = Infinity;
467
+ } else {
468
+ contentLength += isCompressibleContentType(value.type) ? value.size : -value.size;
469
+ }
470
+ } else {
471
+ contentLength += value.length * AVG_BYTES_PER_CHAR;
472
+ }
473
+ }
474
+ if (contentLength >= this.threshold) {
475
+ const res = new Response(request.body);
476
+ const compressedStream = res.body.pipeThrough(new CompressionStream(this.encoding));
477
+ return next({
478
+ ...interceptorOptions,
479
+ request: {
480
+ ...interceptorOptions.request,
481
+ body: compressedStream,
482
+ headers: {
483
+ ...request.headers,
484
+ "standard-server": [],
485
+ "content-type": res.headers.get("content-type"),
486
+ "content-length": [],
487
+ "content-encoding": this.encoding
488
+ }
489
+ }
490
+ });
491
+ }
492
+ } else if (request.body instanceof URLSearchParams) {
493
+ const string = request.body.toString();
494
+ if (string.length * AVG_BYTES_PER_CHAR >= this.threshold) {
495
+ const compressedStream = new Blob([string]).stream().pipeThrough(new CompressionStream(this.encoding));
496
+ return next({
497
+ ...interceptorOptions,
498
+ request: {
499
+ ...interceptorOptions.request,
500
+ body: compressedStream,
501
+ headers: {
502
+ ...request.headers,
503
+ "standard-server": [],
504
+ "content-type": "application/x-www-form-urlencoded",
505
+ "content-length": [],
506
+ "content-encoding": this.encoding
507
+ }
508
+ }
509
+ });
510
+ }
511
+ } else if (request.body !== void 0 && !isAsyncIteratorObject(request.body)) {
512
+ const string = stringifyJSON(request.body);
513
+ if (string.length * AVG_BYTES_PER_CHAR >= this.threshold) {
514
+ const compressedStream = new Blob([string]).stream().pipeThrough(new CompressionStream(this.encoding));
515
+ return next({
516
+ ...interceptorOptions,
517
+ request: {
518
+ ...interceptorOptions.request,
519
+ body: compressedStream,
520
+ headers: {
521
+ ...request.headers,
522
+ "standard-server": [],
523
+ "content-type": "application/json",
524
+ "content-length": [],
525
+ "content-encoding": this.encoding
526
+ }
527
+ }
528
+ });
529
+ }
530
+ }
531
+ return next();
532
+ };
533
+ return {
534
+ ...options,
535
+ transportInterceptors: [
536
+ ...toArray(options.transportInterceptors),
537
+ transportInterceptor
538
+ ]
539
+ };
540
+ }
541
+ }
542
+
543
+ class ResponseCompressionLinkPlugin {
544
+ name = "~response-compression";
545
+ /**
546
+ * Decompression should wrap the final batch response instead of sub-responses.
547
+ */
548
+ after = ["~batch"];
549
+ encodings;
550
+ constructor(options = {}) {
551
+ this.encodings = options.encodings ?? ["gzip", "deflate"];
552
+ }
553
+ init(options) {
554
+ const acceptEncodingHeader = this.encodings.join(", ");
555
+ const transportInterceptor = async ({ next, ...interceptorOptions }) => {
556
+ const response = await next({
557
+ ...interceptorOptions,
558
+ request: {
559
+ ...interceptorOptions.request,
560
+ headers: {
561
+ ...interceptorOptions.request.headers,
562
+ "accept-encoding": acceptEncodingHeader
563
+ }
564
+ }
565
+ });
566
+ const encodings = parseContentEncodings(
567
+ flattenStandardHeader(response.headers["content-encoding"])
568
+ );
569
+ if (encodings.length === 0 || !encodings.every(isSupportedEncoding)) {
570
+ return response;
571
+ }
572
+ const decompressedHeaders = {
573
+ ...response.headers,
574
+ "content-length": void 0,
575
+ "content-encoding": void 0
576
+ };
577
+ return {
578
+ ...response,
579
+ headers: decompressedHeaders,
580
+ async resolveBody(hint) {
581
+ const stream = await response.resolveBody("octet-stream");
582
+ if (!(stream instanceof ReadableStream)) {
583
+ return stream;
584
+ }
585
+ let decompressedStream = stream;
586
+ for (let i = encodings.length - 1; i >= 0; i--) {
587
+ decompressedStream = decompressedStream.pipeThrough(
588
+ new DecompressionStream(encodings[i])
589
+ );
590
+ }
591
+ const fetchResponse = new Response(decompressedStream, {
592
+ headers: toFetchHeaders(decompressedHeaders)
593
+ });
594
+ return toStandardBody(fetchResponse, { hint });
595
+ }
596
+ };
597
+ };
598
+ return {
599
+ ...options,
600
+ transportInterceptors: [
601
+ ...toArray(options.transportInterceptors),
602
+ transportInterceptor
603
+ ]
604
+ };
605
+ }
606
+ }
607
+ const SUPPORTED_ENCODINGS = ["gzip", "deflate", "deflate-raw"];
608
+ function isSupportedEncoding(encoding) {
609
+ return SUPPORTED_ENCODINGS.includes(encoding);
610
+ }
611
+ function parseContentEncodings(header) {
612
+ if (header === void 0) {
613
+ return [];
614
+ }
615
+ return header.split(",").map((part) => part.trim().toLowerCase());
616
+ }
617
+
390
618
  class RetryLinkPlugin {
391
619
  defaultRetry;
392
620
  defaultRetryDelay;
@@ -569,4 +797,4 @@ function parseRetryAfterHeader(value2) {
569
797
  return void 0;
570
798
  }
571
799
 
572
- export { BatchLinkPlugin, BatchLinkPluginError, DedupeLinkPlugin, RetryAfterLinkPlugin, RetryLinkPlugin };
800
+ export { BatchLinkPlugin, BatchLinkPluginError, DedupeLinkPlugin, RequestCompressionLinkPlugin, ResponseCompressionLinkPlugin, RetryAfterLinkPlugin, RetryLinkPlugin };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@orpc/client",
3
3
  "type": "module",
4
- "version": "2.0.0-beta.15",
4
+ "version": "2.0.0-beta.17",
5
5
  "license": "MIT",
6
6
  "homepage": "https://orpc.dev",
7
7
  "repository": {
@@ -50,10 +50,10 @@
50
50
  "dist"
51
51
  ],
52
52
  "dependencies": {
53
- "@standardserver/core": "^0.0.32",
54
- "@standardserver/fetch": "^0.0.32",
55
- "@standardserver/peer": "^0.0.32",
56
- "@orpc/shared": "2.0.0-beta.15"
53
+ "@standardserver/core": "^0.4.2",
54
+ "@standardserver/fetch": "^0.4.2",
55
+ "@standardserver/peer": "^0.4.2",
56
+ "@orpc/shared": "2.0.0-beta.17"
57
57
  },
58
58
  "devDependencies": {
59
59
  "zod": "^4.4.3"