UNPKG

@orpc/client

Version:

<div align="center"> <image align="center" src="https://orpc.unnoq.com/logo.webp" width=280 alt="oRPC logo" /> </div>

391 lines (382 loc) • 13.8 kB
import { toArray, runWithSpan, ORPC_NAME, isAsyncIteratorObject, asyncIteratorWithSpan, intercept, getGlobalOtelConfig, isObject, value, stringifyJSON } from '@orpc/shared'; import { mergeStandardHeaders, ErrorEvent } from '@orpc/standard-server'; import { C as COMMON_ORPC_ERROR_DEFS, b as isORPCErrorStatus, c as isORPCErrorJson, d as createORPCErrorFromJson, O as ORPCError, m as mapEventIterator, t as toORPCError } from './client.txdq_i5V.mjs'; class CompositeStandardLinkPlugin { plugins; constructor(plugins = []) { this.plugins = [...plugins].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); } init(options) { for (const plugin of this.plugins) { plugin.init?.(options); } } } class StandardLink { constructor(codec, sender, options = {}) { this.codec = codec; this.sender = sender; const plugin = new CompositeStandardLinkPlugin(options.plugins); plugin.init(options); this.interceptors = toArray(options.interceptors); this.clientInterceptors = toArray(options.clientInterceptors); } interceptors; clientInterceptors; call(path, input, options) { return runWithSpan( { name: `${ORPC_NAME}.${path.join("/")}`, signal: options.signal }, (span) => { span?.setAttribute("rpc.system", ORPC_NAME); span?.setAttribute("rpc.method", path.join(".")); if (isAsyncIteratorObject(input)) { input = asyncIteratorWithSpan( { name: "consume_event_iterator_input", signal: options.signal }, input ); } return intercept(this.interceptors, { ...options, path, input }, async ({ path: path2, input: input2, ...options2 }) => { const otelConfig = getGlobalOtelConfig(); let otelContext; const currentSpan = otelConfig?.trace.getActiveSpan() ?? span; if (currentSpan && otelConfig) { otelContext = otelConfig?.trace.setSpan(otelConfig.context.active(), currentSpan); } const request = await runWithSpan( { name: "encode_request", context: otelContext }, () => this.codec.encode(path2, input2, options2) ); const response = await intercept( this.clientInterceptors, { ...options2, input: input2, path: path2, request }, ({ input: input3, path: path3, request: request2, ...options3 }) => { return runWithSpan( { name: "send_request", signal: options3.signal, context: otelContext }, () => this.sender.call(request2, options3, path3, input3) ); } ); const output = await runWithSpan( { name: "decode_response", context: otelContext }, () => this.codec.decode(response, options2, path2, input2) ); if (isAsyncIteratorObject(output)) { return asyncIteratorWithSpan( { name: "consume_event_iterator_output", signal: options2.signal }, output ); } return output; }); } ); } } const STANDARD_RPC_JSON_SERIALIZER_BUILT_IN_TYPES = { BIGINT: 0, DATE: 1, NAN: 2, UNDEFINED: 3, URL: 4, REGEXP: 5, SET: 6, MAP: 7 }; class StandardRPCJsonSerializer { customSerializers; constructor(options = {}) { this.customSerializers = options.customJsonSerializers ?? []; if (this.customSerializers.length !== new Set(this.customSerializers.map((custom) => custom.type)).size) { throw new Error("Custom serializer type must be unique."); } } serialize(data, segments = [], meta = [], maps = [], blobs = []) { for (const custom of this.customSerializers) { if (custom.condition(data)) { const result = this.serialize(custom.serialize(data), segments, meta, maps, blobs); meta.push([custom.type, ...segments]); return result; } } if (data instanceof Blob) { maps.push(segments); blobs.push(data); return [data, meta, maps, blobs]; } if (typeof data === "bigint") { meta.push([STANDARD_RPC_JSON_SERIALIZER_BUILT_IN_TYPES.BIGINT, ...segments]); return [data.toString(), meta, maps, blobs]; } if (data instanceof Date) { meta.push([STANDARD_RPC_JSON_SERIALIZER_BUILT_IN_TYPES.DATE, ...segments]); if (Number.isNaN(data.getTime())) { return [null, meta, maps, blobs]; } return [data.toISOString(), meta, maps, blobs]; } if (Number.isNaN(data)) { meta.push([STANDARD_RPC_JSON_SERIALIZER_BUILT_IN_TYPES.NAN, ...segments]); return [null, meta, maps, blobs]; } if (data instanceof URL) { meta.push([STANDARD_RPC_JSON_SERIALIZER_BUILT_IN_TYPES.URL, ...segments]); return [data.toString(), meta, maps, blobs]; } if (data instanceof RegExp) { meta.push([STANDARD_RPC_JSON_SERIALIZER_BUILT_IN_TYPES.REGEXP, ...segments]); return [data.toString(), meta, maps, blobs]; } if (data instanceof Set) { const result = this.serialize(Array.from(data), segments, meta, maps, blobs); meta.push([STANDARD_RPC_JSON_SERIALIZER_BUILT_IN_TYPES.SET, ...segments]); return result; } if (data instanceof Map) { const result = this.serialize(Array.from(data.entries()), segments, meta, maps, blobs); meta.push([STANDARD_RPC_JSON_SERIALIZER_BUILT_IN_TYPES.MAP, ...segments]); return result; } if (Array.isArray(data)) { const json = data.map((v, i) => { if (v === void 0) { meta.push([STANDARD_RPC_JSON_SERIALIZER_BUILT_IN_TYPES.UNDEFINED, ...segments, i]); return v; } return this.serialize(v, [...segments, i], meta, maps, blobs)[0]; }); return [json, meta, maps, blobs]; } if (isObject(data)) { const json = {}; for (const k in data) { if (k === "toJSON" && typeof data[k] === "function") { continue; } json[k] = this.serialize(data[k], [...segments, k], meta, maps, blobs)[0]; } return [json, meta, maps, blobs]; } return [data, meta, maps, blobs]; } deserialize(json, meta, maps, getBlob) { const ref = { data: json }; if (maps && getBlob) { maps.forEach((segments, i) => { let currentRef = ref; let preSegment = "data"; segments.forEach((segment) => { currentRef = currentRef[preSegment]; preSegment = segment; }); currentRef[preSegment] = getBlob(i); }); } for (const item of meta) { const type = item[0]; let currentRef = ref; let preSegment = "data"; for (let i = 1; i < item.length; i++) { currentRef = currentRef[preSegment]; preSegment = item[i]; } for (const custom of this.customSerializers) { if (custom.type === type) { currentRef[preSegment] = custom.deserialize(currentRef[preSegment]); break; } } switch (type) { case STANDARD_RPC_JSON_SERIALIZER_BUILT_IN_TYPES.BIGINT: currentRef[preSegment] = BigInt(currentRef[preSegment]); break; case STANDARD_RPC_JSON_SERIALIZER_BUILT_IN_TYPES.DATE: currentRef[preSegment] = new Date(currentRef[preSegment] ?? "Invalid Date"); break; case STANDARD_RPC_JSON_SERIALIZER_BUILT_IN_TYPES.NAN: currentRef[preSegment] = Number.NaN; break; case STANDARD_RPC_JSON_SERIALIZER_BUILT_IN_TYPES.UNDEFINED: currentRef[preSegment] = void 0; break; case STANDARD_RPC_JSON_SERIALIZER_BUILT_IN_TYPES.URL: currentRef[preSegment] = new URL(currentRef[preSegment]); break; case STANDARD_RPC_JSON_SERIALIZER_BUILT_IN_TYPES.REGEXP: { const [, pattern, flags] = currentRef[preSegment].match(/^\/(.*)\/([a-z]*)$/); currentRef[preSegment] = new RegExp(pattern, flags); break; } case STANDARD_RPC_JSON_SERIALIZER_BUILT_IN_TYPES.SET: currentRef[preSegment] = new Set(currentRef[preSegment]); break; case STANDARD_RPC_JSON_SERIALIZER_BUILT_IN_TYPES.MAP: currentRef[preSegment] = new Map(currentRef[preSegment]); break; } } return ref.data; } } function toHttpPath(path) { return `/${path.map(encodeURIComponent).join("/")}`; } function getMalformedResponseErrorCode(status) { return Object.entries(COMMON_ORPC_ERROR_DEFS).find(([, def]) => def.status === status)?.[0] ?? "MALFORMED_ORPC_ERROR_RESPONSE"; } class StandardRPCLinkCodec { constructor(serializer, options) { this.serializer = serializer; this.baseUrl = options.url; this.maxUrlLength = options.maxUrlLength ?? 2083; this.fallbackMethod = options.fallbackMethod ?? "POST"; this.expectedMethod = options.method ?? this.fallbackMethod; this.headers = options.headers ?? {}; } baseUrl; maxUrlLength; fallbackMethod; expectedMethod; headers; async encode(path, input, options) { const expectedMethod = await value(this.expectedMethod, options, path, input); let headers = await value(this.headers, options, path, input); const baseUrl = await value(this.baseUrl, options, path, input); const url = new URL(baseUrl); url.pathname = `${url.pathname.replace(/\/$/, "")}${toHttpPath(path)}`; if (options.lastEventId !== void 0) { headers = mergeStandardHeaders(headers, { "last-event-id": options.lastEventId }); } const serialized = this.serializer.serialize(input); if (expectedMethod === "GET" && !(serialized instanceof FormData) && !isAsyncIteratorObject(serialized)) { const maxUrlLength = await value(this.maxUrlLength, options, path, input); const getUrl = new URL(url); getUrl.searchParams.append("data", stringifyJSON(serialized)); if (getUrl.toString().length <= maxUrlLength) { return { body: void 0, method: expectedMethod, headers, url: getUrl, signal: options.signal }; } } return { url, method: expectedMethod === "GET" ? this.fallbackMethod : expectedMethod, headers, body: serialized, signal: options.signal }; } async decode(response) { const isOk = !isORPCErrorStatus(response.status); const deserialized = await (async () => { let isBodyOk = false; try { const body = await response.body(); isBodyOk = true; return this.serializer.deserialize(body); } catch (error) { if (!isBodyOk) { throw new Error("Cannot parse response body, please check the response body and content-type.", { cause: error }); } throw new Error("Invalid RPC response format.", { cause: error }); } })(); if (!isOk) { if (isORPCErrorJson(deserialized)) { throw createORPCErrorFromJson(deserialized); } throw new ORPCError(getMalformedResponseErrorCode(response.status), { status: response.status, data: { ...response, body: deserialized } }); } return deserialized; } } class StandardRPCSerializer { constructor(jsonSerializer) { this.jsonSerializer = jsonSerializer; } serialize(data) { if (isAsyncIteratorObject(data)) { return mapEventIterator(data, { value: async (value) => this.#serialize(value, false), error: async (e) => { return new ErrorEvent({ data: this.#serialize(toORPCError(e).toJSON(), false), cause: e }); } }); } return this.#serialize(data, true); } #serialize(data, enableFormData) { const [json, meta_, maps, blobs] = this.jsonSerializer.serialize(data); const meta = meta_.length === 0 ? void 0 : meta_; if (!enableFormData || blobs.length === 0) { return { json, meta }; } const form = new FormData(); form.set("data", stringifyJSON({ json, meta, maps })); blobs.forEach((blob, i) => { form.set(i.toString(), blob); }); return form; } deserialize(data) { if (isAsyncIteratorObject(data)) { return mapEventIterator(data, { value: async (value) => this.#deserialize(value), error: async (e) => { if (!(e instanceof ErrorEvent)) { return e; } const deserialized = this.#deserialize(e.data); if (isORPCErrorJson(deserialized)) { return createORPCErrorFromJson(deserialized, { cause: e }); } return new ErrorEvent({ data: deserialized, cause: e }); } }); } return this.#deserialize(data); } #deserialize(data) { if (data === void 0) { return void 0; } if (!(data instanceof FormData)) { return this.jsonSerializer.deserialize(data.json, data.meta ?? []); } const serialized = JSON.parse(data.get("data")); return this.jsonSerializer.deserialize( serialized.json, serialized.meta ?? [], serialized.maps, (i) => data.get(i.toString()) ); } } class StandardRPCLink extends StandardLink { constructor(linkClient, options) { const jsonSerializer = new StandardRPCJsonSerializer(options); const serializer = new StandardRPCSerializer(jsonSerializer); const linkCodec = new StandardRPCLinkCodec(serializer, options); super(linkCodec, linkClient, options); } } export { CompositeStandardLinkPlugin as C, StandardLink as S, STANDARD_RPC_JSON_SERIALIZER_BUILT_IN_TYPES as a, StandardRPCJsonSerializer as b, StandardRPCLink as c, StandardRPCLinkCodec as d, StandardRPCSerializer as e, getMalformedResponseErrorCode as g, toHttpPath as t };