@orpc/client 2.0.0-beta.4 → 2.0.0-beta.40

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