@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
JavaScript
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 };