@orpc/client 2.0.0-beta.3 → 2.0.0-beta.30

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.
Files changed (29) hide show
  1. package/README.md +71 -101
  2. package/dist/adapters/fetch/index.d.mts +10 -5
  3. package/dist/adapters/fetch/index.d.ts +10 -5
  4. package/dist/adapters/fetch/index.mjs +31 -6
  5. package/dist/adapters/message-port/index.d.mts +11 -6
  6. package/dist/adapters/message-port/index.d.ts +11 -6
  7. package/dist/adapters/message-port/index.mjs +20 -15
  8. package/dist/adapters/standard/index.d.mts +6 -7
  9. package/dist/adapters/standard/index.d.ts +6 -7
  10. package/dist/adapters/standard/index.mjs +3 -3
  11. package/dist/adapters/websocket/index.d.mts +19 -14
  12. package/dist/adapters/websocket/index.d.ts +19 -14
  13. package/dist/adapters/websocket/index.mjs +11 -11
  14. package/dist/index.d.mts +49 -48
  15. package/dist/index.d.ts +49 -48
  16. package/dist/index.mjs +32 -49
  17. package/dist/plugins/index.d.mts +124 -12
  18. package/dist/plugins/index.d.ts +124 -12
  19. package/dist/plugins/index.mjs +290 -23
  20. package/dist/shared/{client.Cby_-GGh.d.mts → client.B0-4UDVl.d.mts} +1 -1
  21. package/dist/shared/{client.CPF3hX6O.d.ts → client.BhTyskpV.d.ts} +1 -1
  22. package/dist/shared/{client.DMXKFDyV.mjs → client.BnATe4Ob.mjs} +176 -58
  23. package/dist/shared/{client.8ug8I-zu.d.mts → client.CFVTHyN6.d.mts} +54 -6
  24. package/dist/shared/{client.8ug8I-zu.d.ts → client.CFVTHyN6.d.ts} +54 -6
  25. package/dist/shared/{client.Dnfj8jnT.mjs → client.DcoDDrD4.mjs} +17 -8
  26. package/dist/shared/{client.BdItY5DT.d.mts → client.Qwo2aQOm.d.mts} +18 -0
  27. package/dist/shared/{client.BdItY5DT.d.ts → client.Qwo2aQOm.d.ts} +18 -0
  28. package/dist/shared/{client.DXhchJ84.mjs → client.vEM0O_Eg.mjs} +8 -9
  29. package/package.json +19 -6
@@ -1,10 +1,9 @@
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, once, isAsyncIteratorObject, defer, loadBytes, allAbortSignal, replicateAsyncIterator, replicateReadableStream, isCompressibleContentType, override, AsyncIteratorClass, sleep, AbortError, anyAbortSignal } from '@orpc/shared';
2
+ import { parseStandardUrl, flattenStandardHeader, generateContentDisposition, getEventMeta } from '@standardserver/core';
3
3
  import { ClientPeer, isServerPeerSendMessage, decodePeerMessage } from '@standardserver/peer';
4
- import { C as COMMON_ERROR_STATUS_MAP } from '../shared/client.Dnfj8jnT.mjs';
4
+ import { toFetchHeaders, toStandardBody } from '@standardserver/fetch';
5
+ import { C as COMMON_ERROR_STATUS_MAP } from '../shared/client.DcoDDrD4.mjs';
5
6
 
6
- class BatchLinkPluginError extends TypeError {
7
- }
8
7
  class BatchLinkPlugin {
9
8
  name = "~batch";
10
9
  groups;
@@ -51,7 +50,7 @@ class BatchLinkPlugin {
51
50
  headers: subHeaders
52
51
  };
53
52
  });
54
- this.mapSubresponse = (subResponse, batchResponse) => {
53
+ this.mapSubresponse = options.mapSubresponse ?? ((subResponse, batchResponse) => {
55
54
  return {
56
55
  ...subResponse,
57
56
  headers: {
@@ -60,7 +59,7 @@ class BatchLinkPlugin {
60
59
  ...subResponse.headers
61
60
  }
62
61
  };
63
- };
62
+ });
64
63
  }
65
64
  init(options) {
66
65
  const transportInterceptor = async (interceptorOptions) => {
@@ -90,9 +89,11 @@ class BatchLinkPlugin {
90
89
  this.queue.clear();
91
90
  for (const [group, items] of pending) {
92
91
  const getItems = items.filter(([options]) => options.request.method === "GET");
93
- const restItems = items.filter(([options]) => options.request.method !== "GET");
92
+ const queryItems = items.filter(([options]) => options.request.method === "QUERY");
93
+ const unsafeItems = items.filter(([options]) => options.request.method !== "GET" && options.request.method !== "QUERY");
94
94
  this.executeBatch("GET", group, getItems);
95
- this.executeBatch("POST", group, restItems);
95
+ this.executeBatch("QUERY", group, queryItems);
96
+ this.executeBatch("POST", group, unsafeItems);
96
97
  }
97
98
  }
98
99
  async executeBatch(method, group, groupItems) {
@@ -161,6 +162,16 @@ class BatchLinkPlugin {
161
162
  request,
162
163
  signal: controller.signal
163
164
  });
165
+ if (batchResponse.status >= 400) {
166
+ suppressErrorFromCurrentBatch = true;
167
+ const resolveBody = once(() => batchResponse.resolveBody());
168
+ const errorResponse = { ...batchResponse, resolveBody };
169
+ groupItems.forEach(([subOptions, resolve]) => {
170
+ resolve(this.mapSubresponse(errorResponse, batchResponse, subOptions));
171
+ });
172
+ await peer.close();
173
+ return;
174
+ }
164
175
  const body = await batchResponse.resolveBody();
165
176
  if (Array.isArray(body) && body.every((v) => isServerPeerSendMessage(v))) {
166
177
  for (const message2 of body) {
@@ -171,9 +182,9 @@ class BatchLinkPlugin {
171
182
  } else if (body instanceof ReadableStream) {
172
183
  await decodeLengthPrefixedStream(body, peer);
173
184
  } else {
174
- throw new BatchLinkPluginError("Invalid batch response format.");
185
+ throw new TypeError("Invalid batch response format.");
175
186
  }
176
- await peer.close(new BatchLinkPluginError("Batch response is incomplete."));
187
+ await peer.close(new TypeError("Batch response is incomplete."));
177
188
  } catch (error) {
178
189
  await peer.close(error);
179
190
  }
@@ -194,19 +205,19 @@ async function decodeLengthPrefixedBlob(blob, peer) {
194
205
  let offset = 0;
195
206
  while (offset < buffer.length) {
196
207
  if (offset + 4 > buffer.length) {
197
- throw new BatchLinkPluginError("Invalid batch response: incomplete length header.");
208
+ throw new TypeError("Invalid batch response: incomplete length header.");
198
209
  }
199
210
  const view = new DataView(buffer.buffer, buffer.byteOffset + offset, 4);
200
211
  const length = view.getUint32(0, false);
201
212
  offset += 4;
202
213
  if (offset + length > buffer.length) {
203
- throw new BatchLinkPluginError("Invalid batch response: incomplete message.");
214
+ throw new TypeError("Invalid batch response: incomplete message.");
204
215
  }
205
216
  const messageBytes = buffer.subarray(offset, offset + length);
206
217
  offset += length;
207
218
  const result = decodePeerMessage(messageBytes);
208
219
  if (!result.matched || !isServerPeerSendMessage(result.message)) {
209
- throw new BatchLinkPluginError("Invalid batch response: invalid message.");
220
+ throw new TypeError("Invalid batch response: invalid message.");
210
221
  }
211
222
  await peer.message(result.message);
212
223
  }
@@ -226,6 +237,10 @@ async function decodeLengthPrefixedStream(stream, peer) {
226
237
  while (buffer.length >= 4) {
227
238
  const view = new DataView(buffer.buffer, buffer.byteOffset, 4);
228
239
  const length = view.getUint32(0, false);
240
+ if (length === 0) {
241
+ buffer = buffer.subarray(4);
242
+ continue;
243
+ }
229
244
  if (buffer.length < 4 + length) {
230
245
  break;
231
246
  }
@@ -233,7 +248,7 @@ async function decodeLengthPrefixedStream(stream, peer) {
233
248
  buffer = buffer.subarray(4 + length);
234
249
  const result = decodePeerMessage(messageBytes);
235
250
  if (!result.matched || !isServerPeerSendMessage(result.message)) {
236
- throw new BatchLinkPluginError("Invalid batch response: invalid message.");
251
+ throw new TypeError("Invalid batch response: invalid message.");
237
252
  }
238
253
  await peer.message(result.message);
239
254
  }
@@ -254,7 +269,7 @@ class DedupeLinkPlugin {
254
269
  queue = /* @__PURE__ */ new Map();
255
270
  constructor(options) {
256
271
  this.groups = options.groups;
257
- this.filter = options.filter ?? (({ request }) => request.method === "GET");
272
+ this.filter = options.filter ?? (({ request }) => request.method === "GET" || request.method === "QUERY");
258
273
  }
259
274
  init(options) {
260
275
  const transportInterceptor = (interceptorOptions) => {
@@ -387,8 +402,229 @@ function shouldDedupe(items) {
387
402
  return items.length >= 2;
388
403
  }
389
404
 
390
- class RetryLinkPluginInvalidEventIteratorRetryResponse extends Error {
405
+ const AVG_BYTES_PER_CHAR = 1.2;
406
+ class RequestCompressionLinkPlugin {
407
+ name = "~request-compression";
408
+ /**
409
+ * Compression should be done after batching, to compress the final request
410
+ */
411
+ after = ["~batch"];
412
+ encoding;
413
+ threshold;
414
+ constructor(options = {}) {
415
+ this.encoding = options.encoding ?? "gzip";
416
+ this.threshold = options.threshold ?? 1024;
417
+ }
418
+ init(options) {
419
+ const transportInterceptor = async ({ next, ...interceptorOptions }) => {
420
+ const request = interceptorOptions.request;
421
+ const contentEncoding = flattenStandardHeader(request.headers["content-encoding"])?.trim()?.toLowerCase();
422
+ if (contentEncoding !== void 0) {
423
+ return next();
424
+ }
425
+ if (request.body instanceof ReadableStream) {
426
+ const contentLength = Number(flattenStandardHeader(request.headers["content-length"]));
427
+ if ((!Number.isFinite(contentLength) || contentLength >= this.threshold) && isCompressibleContentType(flattenStandardHeader(request.headers["content-type"]))) {
428
+ const compressedStream = request.body.pipeThrough(new CompressionStream(this.encoding));
429
+ return next({
430
+ ...interceptorOptions,
431
+ request: {
432
+ ...interceptorOptions.request,
433
+ body: compressedStream,
434
+ headers: {
435
+ ...request.headers,
436
+ "standard-server": "octet-stream",
437
+ "content-length": [],
438
+ "content-encoding": this.encoding
439
+ }
440
+ }
441
+ });
442
+ }
443
+ } else if (request.body instanceof Blob) {
444
+ if ((!Number.isFinite(request.body.size) || request.body.size >= this.threshold) && isCompressibleContentType(request.body.type)) {
445
+ const compressedStream = request.body.stream().pipeThrough(new CompressionStream(this.encoding));
446
+ const contentDisposition = request.headers["content-disposition"] ?? generateContentDisposition(
447
+ request.body instanceof File ? request.body.name : "blob"
448
+ );
449
+ return next({
450
+ ...interceptorOptions,
451
+ request: {
452
+ ...interceptorOptions.request,
453
+ body: compressedStream,
454
+ headers: {
455
+ ...request.headers,
456
+ "standard-server": "file",
457
+ "content-type": request.body.type,
458
+ "content-length": [],
459
+ "content-disposition": contentDisposition,
460
+ "content-encoding": this.encoding
461
+ }
462
+ }
463
+ });
464
+ }
465
+ } else if (request.body instanceof FormData) {
466
+ const PART_OVERHEAD = 64;
467
+ let contentLength = 0;
468
+ for (const [key, value] of request.body) {
469
+ contentLength += PART_OVERHEAD + key.length;
470
+ if (value instanceof Blob) {
471
+ if (!Number.isFinite(value.size)) {
472
+ if (!isCompressibleContentType(value.type)) {
473
+ contentLength = -Infinity;
474
+ break;
475
+ }
476
+ contentLength = Infinity;
477
+ } else {
478
+ contentLength += isCompressibleContentType(value.type) ? value.size : -value.size;
479
+ }
480
+ } else {
481
+ contentLength += value.length * AVG_BYTES_PER_CHAR;
482
+ }
483
+ }
484
+ if (contentLength >= this.threshold) {
485
+ const res = new Response(request.body);
486
+ const compressedStream = res.body.pipeThrough(new CompressionStream(this.encoding));
487
+ return next({
488
+ ...interceptorOptions,
489
+ request: {
490
+ ...interceptorOptions.request,
491
+ body: compressedStream,
492
+ headers: {
493
+ ...request.headers,
494
+ "standard-server": [],
495
+ "content-type": res.headers.get("content-type"),
496
+ "content-length": [],
497
+ "content-encoding": this.encoding
498
+ }
499
+ }
500
+ });
501
+ }
502
+ } else if (request.body instanceof URLSearchParams) {
503
+ const string = request.body.toString();
504
+ if (string.length * AVG_BYTES_PER_CHAR >= this.threshold) {
505
+ const compressedStream = new Blob([string]).stream().pipeThrough(new CompressionStream(this.encoding));
506
+ return next({
507
+ ...interceptorOptions,
508
+ request: {
509
+ ...interceptorOptions.request,
510
+ body: compressedStream,
511
+ headers: {
512
+ ...request.headers,
513
+ "standard-server": [],
514
+ "content-type": "application/x-www-form-urlencoded",
515
+ "content-length": [],
516
+ "content-encoding": this.encoding
517
+ }
518
+ }
519
+ });
520
+ }
521
+ } else if (request.body !== void 0 && !isAsyncIteratorObject(request.body)) {
522
+ const string = stringifyJSON(request.body);
523
+ if (string.length * AVG_BYTES_PER_CHAR >= this.threshold) {
524
+ const compressedStream = new Blob([string]).stream().pipeThrough(new CompressionStream(this.encoding));
525
+ return next({
526
+ ...interceptorOptions,
527
+ request: {
528
+ ...interceptorOptions.request,
529
+ body: compressedStream,
530
+ headers: {
531
+ ...request.headers,
532
+ "standard-server": [],
533
+ "content-type": "application/json",
534
+ "content-length": [],
535
+ "content-encoding": this.encoding
536
+ }
537
+ }
538
+ });
539
+ }
540
+ }
541
+ return next();
542
+ };
543
+ return {
544
+ ...options,
545
+ transportInterceptors: [
546
+ ...toArray(options.transportInterceptors),
547
+ transportInterceptor
548
+ ]
549
+ };
550
+ }
391
551
  }
552
+
553
+ class ResponseCompressionLinkPlugin {
554
+ name = "~response-compression";
555
+ /**
556
+ * Decompression should wrap the final batch response instead of sub-responses.
557
+ */
558
+ after = ["~batch"];
559
+ encodings;
560
+ constructor(options = {}) {
561
+ this.encodings = options.encodings ?? ["gzip", "deflate"];
562
+ }
563
+ init(options) {
564
+ const acceptEncodingHeader = this.encodings.join(", ");
565
+ const transportInterceptor = async ({ next, ...interceptorOptions }) => {
566
+ const response = await next({
567
+ ...interceptorOptions,
568
+ request: {
569
+ ...interceptorOptions.request,
570
+ headers: {
571
+ ...interceptorOptions.request.headers,
572
+ "accept-encoding": acceptEncodingHeader
573
+ }
574
+ }
575
+ });
576
+ const encodings = parseContentEncodings(
577
+ flattenStandardHeader(response.headers["content-encoding"])
578
+ );
579
+ if (encodings.length === 0 || !encodings.every(isSupportedEncoding)) {
580
+ return response;
581
+ }
582
+ const decompressedHeaders = {
583
+ ...response.headers,
584
+ "content-length": void 0,
585
+ "content-encoding": void 0
586
+ };
587
+ return {
588
+ ...response,
589
+ headers: decompressedHeaders,
590
+ async resolveBody(hint) {
591
+ const stream = await response.resolveBody("octet-stream");
592
+ if (!(stream instanceof ReadableStream)) {
593
+ return stream;
594
+ }
595
+ let decompressedStream = stream;
596
+ for (let i = encodings.length - 1; i >= 0; i--) {
597
+ decompressedStream = decompressedStream.pipeThrough(
598
+ new DecompressionStream(encodings[i])
599
+ );
600
+ }
601
+ const fetchResponse = new Response(decompressedStream, {
602
+ headers: toFetchHeaders(decompressedHeaders)
603
+ });
604
+ return toStandardBody(fetchResponse, { hint });
605
+ }
606
+ };
607
+ };
608
+ return {
609
+ ...options,
610
+ transportInterceptors: [
611
+ ...toArray(options.transportInterceptors),
612
+ transportInterceptor
613
+ ]
614
+ };
615
+ }
616
+ }
617
+ const SUPPORTED_ENCODINGS = ["gzip", "deflate", "deflate-raw"];
618
+ function isSupportedEncoding(encoding) {
619
+ return SUPPORTED_ENCODINGS.includes(encoding);
620
+ }
621
+ function parseContentEncodings(header) {
622
+ if (header === void 0) {
623
+ return [];
624
+ }
625
+ return header.split(",").map((part) => part.trim().toLowerCase());
626
+ }
627
+
392
628
  class RetryLinkPlugin {
393
629
  defaultRetry;
394
630
  defaultRetryDelay;
@@ -484,13 +720,13 @@ class RetryLinkPlugin {
484
720
  const meta = getEventMeta(error);
485
721
  lastEventId = meta?.id ?? lastEventId;
486
722
  lastEventRetry = meta?.retry ?? lastEventRetry;
487
- const maybeEventIterator = await callNext({ error });
488
- if (!isAsyncIteratorObject(maybeEventIterator)) {
489
- throw new RetryLinkPluginInvalidEventIteratorRetryResponse(
490
- "RetryLinkPlugin: Expected an Event Iterator, got a non-Event Iterator"
723
+ const asyncIteratorObject = await callNext({ error });
724
+ if (!isAsyncIteratorObject(asyncIteratorObject)) {
725
+ throw new TypeError(
726
+ "RetryLinkPlugin: Expected an AsyncIteratorObject, got a different type."
491
727
  );
492
728
  }
493
- current = maybeEventIterator;
729
+ current = asyncIteratorObject;
494
730
  if (isIteratorAborted) {
495
731
  await current.return?.();
496
732
  throw error;
@@ -571,4 +807,35 @@ function parseRetryAfterHeader(value2) {
571
807
  return void 0;
572
808
  }
573
809
 
574
- export { BatchLinkPlugin, BatchLinkPluginError, DedupeLinkPlugin, RetryAfterLinkPlugin, RetryLinkPlugin, RetryLinkPluginInvalidEventIteratorRetryResponse };
810
+ class TimeoutLinkPlugin {
811
+ timeout;
812
+ name = "~timeout";
813
+ /**
814
+ * Should abort if the total retry time exceeds the configured timeout
815
+ */
816
+ after = ["~retry"];
817
+ constructor(options) {
818
+ this.timeout = options.timeout;
819
+ }
820
+ init(options) {
821
+ const interceptor = async (interceptorOptions) => {
822
+ const timeoutMs = value(this.timeout, interceptorOptions);
823
+ if (timeoutMs === null || timeoutMs === void 0) {
824
+ return interceptorOptions.next();
825
+ }
826
+ const controller = new AbortController();
827
+ const timeoutId = setTimeout(() => {
828
+ controller.abort(new AbortError(`Request timed out after ${timeoutMs}ms`));
829
+ }, timeoutMs);
830
+ const signal = anyAbortSignal([interceptorOptions.signal, controller.signal]);
831
+ try {
832
+ return await interceptorOptions.next({ ...interceptorOptions, signal });
833
+ } finally {
834
+ clearTimeout(timeoutId);
835
+ }
836
+ };
837
+ return { ...options, interceptors: [interceptor, ...toArray(options.interceptors)] };
838
+ }
839
+ }
840
+
841
+ export { BatchLinkPlugin, RetryLinkPlugin as ClientRetryPlugin, DedupeLinkPlugin, DedupeLinkPlugin as DedupeRequestsPlugin, RequestCompressionLinkPlugin, ResponseCompressionLinkPlugin, RetryAfterLinkPlugin, RetryAfterLinkPlugin as RetryAfterPlugin, RetryLinkPlugin, TimeoutLinkPlugin };
@@ -1,6 +1,6 @@
1
1
  import { Promisable, OrderablePlugin, Interceptor } from '@orpc/shared';
2
2
  import { StandardRequest, StandardLazyResponse } from '@standardserver/core';
3
- import { C as ClientContext, a as ClientOptions, A as AnyORPCError, b as ClientLink } from './client.8ug8I-zu.mjs';
3
+ import { C as ClientContext, a as ClientOptions, A as AnyORPCError, b as ClientLink } from './client.CFVTHyN6.mjs';
4
4
 
5
5
  type StandardLinkCodecDecodedResponse = {
6
6
  kind: 'output';
@@ -1,6 +1,6 @@
1
1
  import { Promisable, OrderablePlugin, Interceptor } from '@orpc/shared';
2
2
  import { StandardRequest, StandardLazyResponse } from '@standardserver/core';
3
- import { C as ClientContext, a as ClientOptions, A as AnyORPCError, b as ClientLink } from './client.8ug8I-zu.js';
3
+ import { C as ClientContext, a as ClientOptions, A as AnyORPCError, b as ClientLink } from './client.CFVTHyN6.js';
4
4
 
5
5
  type StandardLinkCodecDecodedResponse = {
6
6
  kind: 'output';