@orpc/client
Advanced tools
@@ -1,2 +0,2 @@ | ||
| import { sortPlugins, value, intercept } from '@orpc/shared'; | ||
| import { sortPlugins, value, intercept, once } from '@orpc/shared'; | ||
| import { toFetchBody, toFetchHeaders, toStandardLazyResponse } from '@standardserver/fetch'; | ||
@@ -24,2 +24,21 @@ import { S as StandardLink, R as RPCLinkCodec } from '../../shared/client.BRJnOJ0R.mjs'; | ||
| const GET_SUPPORTED_DUPLEX_MODE = once(() => { | ||
| try { | ||
| let duplex; | ||
| void new Request( | ||
| "https://example.com", | ||
| { | ||
| method: "POST", | ||
| body: new ReadableStream(), | ||
| get duplex() { | ||
| duplex = "half"; | ||
| return "half"; | ||
| } | ||
| } | ||
| ); | ||
| return duplex; | ||
| } catch { | ||
| return void 0; | ||
| } | ||
| }); | ||
| class FetchLinkTransport { | ||
@@ -51,2 +70,8 @@ origin; | ||
| }; | ||
| if (body instanceof ReadableStream) { | ||
| const duplex = GET_SUPPORTED_DUPLEX_MODE(); | ||
| if (duplex !== void 0) { | ||
| init.duplex = duplex; | ||
| } | ||
| } | ||
| const response = await intercept( | ||
@@ -53,0 +78,0 @@ this.fetchInterceptors, |
@@ -139,2 +139,57 @@ import { Value, Promisable } from '@orpc/shared'; | ||
| interface RequestCompressionLinkPluginOptions<_T extends ClientContext> { | ||
| /** | ||
| * The compression scheme to use for request compression. | ||
| * | ||
| * @default 'gzip' | ||
| */ | ||
| encoding?: 'gzip' | 'deflate' | 'deflate-raw'; | ||
| /** | ||
| * The minimum request size in bytes required to trigger compression. | ||
| * Requests smaller than this threshold will not be compressed to avoid overhead. | ||
| * If the request size cannot be determined, compression will still be applied. | ||
| * | ||
| * @default 1024 (1KB) | ||
| */ | ||
| threshold?: number; | ||
| } | ||
| declare class RequestCompressionLinkPlugin<T extends ClientContext> implements StandardLinkPlugin<T> { | ||
| name: string; | ||
| /** | ||
| * Compression should be done after batching, to compress the final request | ||
| */ | ||
| after: string[]; | ||
| private readonly encoding; | ||
| private readonly threshold; | ||
| constructor(options?: RequestCompressionLinkPluginOptions<T>); | ||
| init(options: StandardLinkOptions<T>): StandardLinkOptions<T>; | ||
| } | ||
| interface ResponseCompressionLinkPluginOptions<_T extends ClientContext> { | ||
| /** | ||
| * Compression schemes to advertise via Accept-Encoding, in preference order. | ||
| * Only schemes that can be decompressed by this plugin should be listed. | ||
| * | ||
| * @default ['gzip', 'deflate'] | ||
| */ | ||
| encodings?: readonly ('gzip' | 'deflate' | 'deflate-raw')[]; | ||
| } | ||
| /** | ||
| * Advertises Accept-Encoding on requests and decompresses response bodies | ||
| * based on the Content-Encoding header. Works at the standard link level, | ||
| * so it supports all adapters. | ||
| * | ||
| * @see {@link https://orpc.dev/docs/plugins/response-compression Response Compression Plugin Docs} | ||
| */ | ||
| declare class ResponseCompressionLinkPlugin<T extends ClientContext> implements StandardLinkPlugin<T> { | ||
| name: string; | ||
| /** | ||
| * Decompression should wrap the final batch response instead of sub-responses. | ||
| */ | ||
| after: string[]; | ||
| private readonly encodings; | ||
| constructor(options?: ResponseCompressionLinkPluginOptions<T>); | ||
| init(options: StandardLinkOptions<T>): StandardLinkOptions<T>; | ||
| } | ||
| interface RetryLinkPluginAttemptOptions<T extends RetryLinkPluginContext> extends StandardLinkInterceptorOptions<T> { | ||
@@ -240,3 +295,3 @@ /** | ||
| export { BatchLinkPlugin, BatchLinkPluginError, DedupeLinkPlugin, RetryAfterLinkPlugin, RetryLinkPlugin }; | ||
| export type { BatchLinkPluginGroup, BatchLinkPluginMode, BatchLinkPluginOptions, DedupeLinkPluginGroup, DedupeLinkPluginOptions, RetryAfterLinkPluginOptions, RetryLinkPluginAttemptOptions, RetryLinkPluginContext, RetryLinkPluginOptions }; | ||
| export { BatchLinkPlugin, BatchLinkPluginError, DedupeLinkPlugin, RequestCompressionLinkPlugin, ResponseCompressionLinkPlugin, RetryAfterLinkPlugin, RetryLinkPlugin }; | ||
| export type { BatchLinkPluginGroup, BatchLinkPluginMode, BatchLinkPluginOptions, DedupeLinkPluginGroup, DedupeLinkPluginOptions, RequestCompressionLinkPluginOptions, ResponseCompressionLinkPluginOptions, RetryAfterLinkPluginOptions, RetryLinkPluginAttemptOptions, RetryLinkPluginContext, RetryLinkPluginOptions }; |
@@ -139,2 +139,57 @@ import { Value, Promisable } from '@orpc/shared'; | ||
| interface RequestCompressionLinkPluginOptions<_T extends ClientContext> { | ||
| /** | ||
| * The compression scheme to use for request compression. | ||
| * | ||
| * @default 'gzip' | ||
| */ | ||
| encoding?: 'gzip' | 'deflate' | 'deflate-raw'; | ||
| /** | ||
| * The minimum request size in bytes required to trigger compression. | ||
| * Requests smaller than this threshold will not be compressed to avoid overhead. | ||
| * If the request size cannot be determined, compression will still be applied. | ||
| * | ||
| * @default 1024 (1KB) | ||
| */ | ||
| threshold?: number; | ||
| } | ||
| declare class RequestCompressionLinkPlugin<T extends ClientContext> implements StandardLinkPlugin<T> { | ||
| name: string; | ||
| /** | ||
| * Compression should be done after batching, to compress the final request | ||
| */ | ||
| after: string[]; | ||
| private readonly encoding; | ||
| private readonly threshold; | ||
| constructor(options?: RequestCompressionLinkPluginOptions<T>); | ||
| init(options: StandardLinkOptions<T>): StandardLinkOptions<T>; | ||
| } | ||
| interface ResponseCompressionLinkPluginOptions<_T extends ClientContext> { | ||
| /** | ||
| * Compression schemes to advertise via Accept-Encoding, in preference order. | ||
| * Only schemes that can be decompressed by this plugin should be listed. | ||
| * | ||
| * @default ['gzip', 'deflate'] | ||
| */ | ||
| encodings?: readonly ('gzip' | 'deflate' | 'deflate-raw')[]; | ||
| } | ||
| /** | ||
| * Advertises Accept-Encoding on requests and decompresses response bodies | ||
| * based on the Content-Encoding header. Works at the standard link level, | ||
| * so it supports all adapters. | ||
| * | ||
| * @see {@link https://orpc.dev/docs/plugins/response-compression Response Compression Plugin Docs} | ||
| */ | ||
| declare class ResponseCompressionLinkPlugin<T extends ClientContext> implements StandardLinkPlugin<T> { | ||
| name: string; | ||
| /** | ||
| * Decompression should wrap the final batch response instead of sub-responses. | ||
| */ | ||
| after: string[]; | ||
| private readonly encodings; | ||
| constructor(options?: ResponseCompressionLinkPluginOptions<T>); | ||
| init(options: StandardLinkOptions<T>): StandardLinkOptions<T>; | ||
| } | ||
| interface RetryLinkPluginAttemptOptions<T extends RetryLinkPluginContext> extends StandardLinkInterceptorOptions<T> { | ||
@@ -240,3 +295,3 @@ /** | ||
| export { BatchLinkPlugin, BatchLinkPluginError, DedupeLinkPlugin, RetryAfterLinkPlugin, RetryLinkPlugin }; | ||
| export type { BatchLinkPluginGroup, BatchLinkPluginMode, BatchLinkPluginOptions, DedupeLinkPluginGroup, DedupeLinkPluginOptions, RetryAfterLinkPluginOptions, RetryLinkPluginAttemptOptions, RetryLinkPluginContext, RetryLinkPluginOptions }; | ||
| export { BatchLinkPlugin, BatchLinkPluginError, DedupeLinkPlugin, RequestCompressionLinkPlugin, ResponseCompressionLinkPlugin, RetryAfterLinkPlugin, RetryLinkPlugin }; | ||
| export type { BatchLinkPluginGroup, BatchLinkPluginMode, BatchLinkPluginOptions, DedupeLinkPluginGroup, DedupeLinkPluginOptions, RequestCompressionLinkPluginOptions, ResponseCompressionLinkPluginOptions, RetryAfterLinkPluginOptions, RetryLinkPluginAttemptOptions, RetryLinkPluginContext, RetryLinkPluginOptions }; |
+227
-3
@@ -1,4 +0,5 @@ | ||
| import { toArray, value, splitInHalf, stringifyJSON, isAsyncIteratorObject, defer, loadBytes, allAbortSignal, replicateAsyncIterator, replicateReadableStream, override, AsyncIteratorClass, sleep } from '@orpc/shared'; | ||
| import { parseStandardUrl, getEventMeta, flattenStandardHeader } from '@standardserver/core'; | ||
| import { toArray, value, splitInHalf, stringifyJSON, isAsyncIteratorObject, defer, loadBytes, allAbortSignal, replicateAsyncIterator, replicateReadableStream, isCompressibleContentType, override, AsyncIteratorClass, sleep } from '@orpc/shared'; | ||
| import { parseStandardUrl, flattenStandardHeader, generateContentDisposition, getEventMeta } from '@standardserver/core'; | ||
| import { ClientPeer, isServerPeerSendMessage, decodePeerMessage } from '@standardserver/peer'; | ||
| import { toFetchHeaders, toStandardBody } from '@standardserver/fetch'; | ||
| import { C as COMMON_ERROR_STATUS_MAP } from '../shared/client.Dnfj8jnT.mjs'; | ||
@@ -390,2 +391,225 @@ | ||
| const AVG_BYTES_PER_CHAR = 1.2; | ||
| class RequestCompressionLinkPlugin { | ||
| name = "~request-compression"; | ||
| /** | ||
| * Compression should be done after batching, to compress the final request | ||
| */ | ||
| after = ["~batch"]; | ||
| encoding; | ||
| threshold; | ||
| constructor(options = {}) { | ||
| this.encoding = options.encoding ?? "gzip"; | ||
| this.threshold = options.threshold ?? 1024; | ||
| } | ||
| init(options) { | ||
| const transportInterceptor = async ({ next, ...interceptorOptions }) => { | ||
| const request = interceptorOptions.request; | ||
| const contentEncoding = flattenStandardHeader(request.headers["content-encoding"])?.trim()?.toLowerCase(); | ||
| if (contentEncoding !== void 0) { | ||
| return next(); | ||
| } | ||
| if (request.body instanceof ReadableStream) { | ||
| const contentLength = Number(flattenStandardHeader(request.headers["content-length"])); | ||
| if ((!Number.isFinite(contentLength) || contentLength >= this.threshold) && isCompressibleContentType(flattenStandardHeader(request.headers["content-type"]))) { | ||
| const compressedStream = request.body.pipeThrough(new CompressionStream(this.encoding)); | ||
| return next({ | ||
| ...interceptorOptions, | ||
| request: { | ||
| ...interceptorOptions.request, | ||
| body: compressedStream, | ||
| headers: { | ||
| ...request.headers, | ||
| "standard-server": "octet-stream", | ||
| "content-length": [], | ||
| "content-encoding": this.encoding | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| } else if (request.body instanceof Blob) { | ||
| if ((!Number.isFinite(request.body.size) || request.body.size >= this.threshold) && isCompressibleContentType(request.body.type)) { | ||
| const compressedStream = request.body.stream().pipeThrough(new CompressionStream(this.encoding)); | ||
| const contentDisposition = request.headers["content-disposition"] ?? generateContentDisposition( | ||
| request.body instanceof File ? request.body.name : "blob" | ||
| ); | ||
| return next({ | ||
| ...interceptorOptions, | ||
| request: { | ||
| ...interceptorOptions.request, | ||
| body: compressedStream, | ||
| headers: { | ||
| ...request.headers, | ||
| "standard-server": "file", | ||
| "content-type": request.body.type, | ||
| "content-length": [], | ||
| "content-disposition": contentDisposition, | ||
| "content-encoding": this.encoding | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| } else if (request.body instanceof FormData) { | ||
| const PART_OVERHEAD = 64; | ||
| let contentLength = 0; | ||
| for (const [key, value] of request.body) { | ||
| contentLength += PART_OVERHEAD + key.length; | ||
| if (value instanceof Blob) { | ||
| if (!Number.isFinite(value.size)) { | ||
| if (!isCompressibleContentType(value.type)) { | ||
| contentLength = -Infinity; | ||
| break; | ||
| } | ||
| contentLength = Infinity; | ||
| } else { | ||
| contentLength += isCompressibleContentType(value.type) ? value.size : -value.size; | ||
| } | ||
| } else { | ||
| contentLength += value.length * AVG_BYTES_PER_CHAR; | ||
| } | ||
| } | ||
| if (contentLength >= this.threshold) { | ||
| const res = new Response(request.body); | ||
| const compressedStream = res.body.pipeThrough(new CompressionStream(this.encoding)); | ||
| return next({ | ||
| ...interceptorOptions, | ||
| request: { | ||
| ...interceptorOptions.request, | ||
| body: compressedStream, | ||
| headers: { | ||
| ...request.headers, | ||
| "standard-server": [], | ||
| "content-type": res.headers.get("content-type"), | ||
| "content-length": [], | ||
| "content-encoding": this.encoding | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| } else if (request.body instanceof URLSearchParams) { | ||
| const string = request.body.toString(); | ||
| if (string.length * AVG_BYTES_PER_CHAR >= this.threshold) { | ||
| const compressedStream = new Blob([string]).stream().pipeThrough(new CompressionStream(this.encoding)); | ||
| return next({ | ||
| ...interceptorOptions, | ||
| request: { | ||
| ...interceptorOptions.request, | ||
| body: compressedStream, | ||
| headers: { | ||
| ...request.headers, | ||
| "standard-server": [], | ||
| "content-type": "application/x-www-form-urlencoded", | ||
| "content-length": [], | ||
| "content-encoding": this.encoding | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| } else if (request.body !== void 0 && !isAsyncIteratorObject(request.body)) { | ||
| const string = stringifyJSON(request.body); | ||
| if (string.length * AVG_BYTES_PER_CHAR >= this.threshold) { | ||
| const compressedStream = new Blob([string]).stream().pipeThrough(new CompressionStream(this.encoding)); | ||
| return next({ | ||
| ...interceptorOptions, | ||
| request: { | ||
| ...interceptorOptions.request, | ||
| body: compressedStream, | ||
| headers: { | ||
| ...request.headers, | ||
| "standard-server": [], | ||
| "content-type": "application/json", | ||
| "content-length": [], | ||
| "content-encoding": this.encoding | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| } | ||
| return next(); | ||
| }; | ||
| return { | ||
| ...options, | ||
| transportInterceptors: [ | ||
| ...toArray(options.transportInterceptors), | ||
| transportInterceptor | ||
| ] | ||
| }; | ||
| } | ||
| } | ||
| class ResponseCompressionLinkPlugin { | ||
| name = "~response-compression"; | ||
| /** | ||
| * Decompression should wrap the final batch response instead of sub-responses. | ||
| */ | ||
| after = ["~batch"]; | ||
| encodings; | ||
| constructor(options = {}) { | ||
| this.encodings = options.encodings ?? ["gzip", "deflate"]; | ||
| } | ||
| init(options) { | ||
| const acceptEncodingHeader = this.encodings.join(", "); | ||
| const transportInterceptor = async ({ next, ...interceptorOptions }) => { | ||
| const response = await next({ | ||
| ...interceptorOptions, | ||
| request: { | ||
| ...interceptorOptions.request, | ||
| headers: { | ||
| ...interceptorOptions.request.headers, | ||
| "accept-encoding": acceptEncodingHeader | ||
| } | ||
| } | ||
| }); | ||
| const encodings = parseContentEncodings( | ||
| flattenStandardHeader(response.headers["content-encoding"]) | ||
| ); | ||
| if (encodings.length === 0 || !encodings.every(isSupportedEncoding)) { | ||
| return response; | ||
| } | ||
| const decompressedHeaders = { | ||
| ...response.headers, | ||
| "content-length": void 0, | ||
| "content-encoding": void 0 | ||
| }; | ||
| return { | ||
| ...response, | ||
| headers: decompressedHeaders, | ||
| async resolveBody(hint) { | ||
| const stream = await response.resolveBody("octet-stream"); | ||
| if (!(stream instanceof ReadableStream)) { | ||
| return stream; | ||
| } | ||
| let decompressedStream = stream; | ||
| for (let i = encodings.length - 1; i >= 0; i--) { | ||
| decompressedStream = decompressedStream.pipeThrough( | ||
| new DecompressionStream(encodings[i]) | ||
| ); | ||
| } | ||
| const fetchResponse = new Response(decompressedStream, { | ||
| headers: toFetchHeaders(decompressedHeaders) | ||
| }); | ||
| return toStandardBody(fetchResponse, { hint }); | ||
| } | ||
| }; | ||
| }; | ||
| return { | ||
| ...options, | ||
| transportInterceptors: [ | ||
| ...toArray(options.transportInterceptors), | ||
| transportInterceptor | ||
| ] | ||
| }; | ||
| } | ||
| } | ||
| const SUPPORTED_ENCODINGS = ["gzip", "deflate", "deflate-raw"]; | ||
| function isSupportedEncoding(encoding) { | ||
| return SUPPORTED_ENCODINGS.includes(encoding); | ||
| } | ||
| function parseContentEncodings(header) { | ||
| if (header === void 0) { | ||
| return []; | ||
| } | ||
| return header.split(",").map((part) => part.trim().toLowerCase()); | ||
| } | ||
| class RetryLinkPlugin { | ||
@@ -573,2 +797,2 @@ defaultRetry; | ||
| export { BatchLinkPlugin, BatchLinkPluginError, DedupeLinkPlugin, RetryAfterLinkPlugin, RetryLinkPlugin }; | ||
| export { BatchLinkPlugin, BatchLinkPluginError, DedupeLinkPlugin, RequestCompressionLinkPlugin, ResponseCompressionLinkPlugin, RetryAfterLinkPlugin, RetryLinkPlugin }; |
+5
-5
| { | ||
| "name": "@orpc/client", | ||
| "type": "module", | ||
| "version": "2.0.0-beta.15", | ||
| "version": "2.0.0-beta.16", | ||
| "license": "MIT", | ||
@@ -53,6 +53,6 @@ "homepage": "https://orpc.dev", | ||
| "dependencies": { | ||
| "@standardserver/core": "^0.0.32", | ||
| "@standardserver/fetch": "^0.0.32", | ||
| "@standardserver/peer": "^0.0.32", | ||
| "@orpc/shared": "2.0.0-beta.15" | ||
| "@standardserver/core": "^0.2.0", | ||
| "@standardserver/fetch": "^0.2.0", | ||
| "@standardserver/peer": "^0.2.0", | ||
| "@orpc/shared": "2.0.0-beta.16" | ||
| }, | ||
@@ -59,0 +59,0 @@ "devDependencies": { |
+1
-1
@@ -178,3 +178,3 @@ <h1 align="center">oRPC - Typesafe APIs Made Simple 🪄</h1> | ||
| <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&v=4" width="32" height="32" alt="Sefa Eyeoglu" /></a> | ||
| <a href="https://github.com/nattstack?ref=orpc" target="_blank" rel="noopener" title="Natt"><img src="https://avatars.githubusercontent.com/u/31426677?u=fa9dbb8b3e66eb0ea3c88db5dc07f31c8c5418fe&v=4" width="32" height="32" alt="Natt" /></a> | ||
| <a href="https://github.com/nattstack?ref=orpc" target="_blank" rel="noopener" title="NΛTT"><img src="https://avatars.githubusercontent.com/u/31426677?u=fa9dbb8b3e66eb0ea3c88db5dc07f31c8c5418fe&v=4" width="32" height="32" alt="NΛTT" /></a> | ||
| <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&v=4" width="32" height="32" alt="Adam Tkaczyk" /></a> | ||
@@ -181,0 +181,0 @@ <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> |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
192034
7.32%2882
11.66%1
Infinity%+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
Updated
Updated
Updated
Updated