@orpc/standard-server-peer 0.0.0-next.e385fb7 → 0.0.0-next.e4b0df6

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.
package/README.md CHANGED
@@ -30,7 +30,8 @@
30
30
  - **🔗 End-to-End Type Safety**: Ensure type-safe inputs, outputs, and errors from client to server.
31
31
  - **📘 First-Class OpenAPI**: Built-in support that fully adheres to the OpenAPI standard.
32
32
  - **📝 Contract-First Development**: Optionally define your API contract before implementation.
33
- - **⚙️ Framework Integrations**: Seamlessly integrate with TanStack Query (React, Vue, Solid, Svelte), Pinia Colada, and more.
33
+ - **🔍 First-Class OpenTelemetry**: Seamlessly integrate with OpenTelemetry for observability.
34
+ - **⚙️ Framework Integrations**: Seamlessly integrate with TanStack Query (React, Vue, Solid, Svelte, Angular), SWR, Pinia Colada, and more.
34
35
  - **🚀 Server Actions**: Fully compatible with React Server Actions on Next.js, TanStack Start, and other platforms.
35
36
  - **🔠 Standard Schema Support**: Works out of the box with Zod, Valibot, ArkType, and other schema validators.
36
37
  - **🗃️ Native Types**: Supports native types like Date, File, Blob, BigInt, URL, and more.
@@ -38,7 +39,6 @@
38
39
  - **📡 SSE & Streaming**: Enjoy full type-safe support for SSE and streaming.
39
40
  - **🌍 Multi-Runtime Support**: Fast and lightweight on Cloudflare, Deno, Bun, Node.js, and beyond.
40
41
  - **🔌 Extendability**: Easily extend functionality with plugins, middleware, and interceptors.
41
- - **🛡️ Reliability**: Well-tested, TypeScript-based, production-ready, and MIT licensed.
42
42
 
43
43
  ## Documentation
44
44
 
@@ -49,14 +49,14 @@ You can find the full documentation [here](https://orpc.unnoq.com).
49
49
  - [@orpc/contract](https://www.npmjs.com/package/@orpc/contract): Build your API contract.
50
50
  - [@orpc/server](https://www.npmjs.com/package/@orpc/server): Build your API or implement API contract.
51
51
  - [@orpc/client](https://www.npmjs.com/package/@orpc/client): Consume your API on the client with type-safety.
52
- - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Deeply integrate oRPC with NestJS.
52
+ - [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi): Generate OpenAPI specs and handle OpenAPI requests.
53
+ - [@orpc/otel](https://www.npmjs.com/package/@orpc/otel): [OpenTelemetry](https://opentelemetry.io/) integration for observability.
54
+ - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Deeply integrate oRPC with [NestJS](https://nestjs.com/).
53
55
  - [@orpc/react](https://www.npmjs.com/package/@orpc/react): Utilities for integrating oRPC with React and React Server Actions.
54
- - [@orpc/react-query](https://www.npmjs.com/package/@orpc/react-query): Integration with [React Query](https://tanstack.com/query/latest/docs/framework/react/overview).
55
- - [@orpc/vue-query](https://www.npmjs.com/package/@orpc/vue-query): Integration with [Vue Query](https://tanstack.com/query/latest/docs/framework/vue/overview).
56
- - [@orpc/solid-query](https://www.npmjs.com/package/@orpc/solid-query): Integration with [Solid Query](https://tanstack.com/query/latest/docs/framework/solid/overview).
57
- - [@orpc/svelte-query](https://www.npmjs.com/package/@orpc/svelte-query): Integration with [Svelte Query](https://tanstack.com/query/latest/docs/framework/svelte/overview).
56
+ - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): [TanStack Query](https://tanstack.com/query/latest) integration.
57
+ - [@orpc/experimental-react-swr](https://www.npmjs.com/package/@orpc/experimental-react-swr): [SWR](https://swr.vercel.app/) integration.
58
58
  - [@orpc/vue-colada](https://www.npmjs.com/package/@orpc/vue-colada): Integration with [Pinia Colada](https://pinia-colada.esm.dev/).
59
- - [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi): Generate OpenAPI specs and handle OpenAPI requests.
59
+ - [@orpc/hey-api](https://www.npmjs.com/package/@orpc/hey-api): [Hey API](https://heyapi.dev/) integration.
60
60
  - [@orpc/zod](https://www.npmjs.com/package/@orpc/zod): More schemas that [Zod](https://zod.dev/) doesn't support yet.
61
61
  - [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot): OpenAPI spec generation from [Valibot](https://valibot.dev/).
62
62
  - [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype): OpenAPI spec generation from [ArkType](https://arktype.io/).
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
- import { Promisable, AsyncIdQueueCloseOptions as AsyncIdQueueCloseOptions$1, CreateAsyncIteratorObjectCleanupFn } from '@orpc/shared';
2
- import { StandardRequest, StandardResponse, EventMeta, StandardHeaders } from '@orpc/standard-server';
1
+ import { Promisable, AsyncIdQueueCloseOptions as AsyncIdQueueCloseOptions$1, SetSpanErrorOptions, AsyncIteratorClassCleanupFn, AsyncIteratorClass } from '@orpc/shared';
2
+ import { StandardRequest, StandardResponse, EventMeta } from '@orpc/standard-server';
3
3
 
4
- type EncodedMessage = string | ArrayBufferLike | Blob;
4
+ type EncodedMessage = string | ArrayBufferLike | Uint8Array;
5
5
  interface EncodedMessageSendFn {
6
6
  (message: EncodedMessage): Promisable<void>;
7
7
  }
@@ -16,13 +16,26 @@ interface ClientPeerCloseOptions extends AsyncIdQueueCloseOptions$1 {
16
16
  }
17
17
  declare class ClientPeer {
18
18
  private readonly idGenerator;
19
+ /**
20
+ * Queue of responses sent from server, awaiting consumption
21
+ */
19
22
  private readonly responseQueue;
23
+ /**
24
+ * Queue of event iterator messages sent from server, awaiting consumption
25
+ */
20
26
  private readonly serverEventIteratorQueue;
27
+ /**
28
+ * Controllers used to signal that the client should stop sending event iterator messages
29
+ */
21
30
  private readonly serverControllers;
31
+ /**
32
+ * Cleanup functions invoked when the request/response is closed
33
+ */
34
+ private readonly cleanupFns;
22
35
  private readonly send;
23
36
  constructor(send: EncodedMessageSendFn);
24
37
  get length(): number;
25
- open(id: number): AbortController;
38
+ open(id: string): AbortController;
26
39
  request(request: StandardRequest): Promise<StandardResponse>;
27
40
  message(raw: EncodedMessage): Promise<void>;
28
41
  close(options?: AsyncIdQueueCloseOptions$1): void;
@@ -51,36 +64,42 @@ interface ResponseMessageMap {
51
64
  [MessageType.ABORT_SIGNAL]: void;
52
65
  }
53
66
  type DecodedMessageUnion<TMap extends RequestMessageMap | ResponseMessageMap> = {
54
- [K in keyof TMap]: [id: number, type: K, payload: TMap[K]];
67
+ [K in keyof TMap]: [id: string, type: K, payload: TMap[K]];
55
68
  }[keyof TMap];
56
69
  type DecodedRequestMessage = DecodedMessageUnion<RequestMessageMap>;
57
70
  type DecodedResponseMessage = DecodedMessageUnion<ResponseMessageMap>;
58
- declare function encodeRequestMessage<T extends keyof RequestMessageMap>(id: number, type: T, payload: RequestMessageMap[T]): Promise<EncodedMessage>;
71
+ declare function encodeRequestMessage<T extends keyof RequestMessageMap>(id: string, type: T, payload: RequestMessageMap[T]): Promise<EncodedMessage>;
59
72
  declare function decodeRequestMessage(raw: EncodedMessage): Promise<DecodedRequestMessage>;
60
- declare function encodeResponseMessage<T extends keyof ResponseMessageMap>(id: number, type: T, payload: ResponseMessageMap[T]): Promise<EncodedMessage>;
73
+ declare function encodeResponseMessage<T extends keyof ResponseMessageMap>(id: string, type: T, payload: ResponseMessageMap[T]): Promise<EncodedMessage>;
61
74
  declare function decodeResponseMessage(raw: EncodedMessage): Promise<DecodedResponseMessage>;
62
- declare function isEventIteratorHeaders(headers: StandardHeaders): boolean;
63
75
 
64
76
  interface AsyncIdQueueCloseOptions {
65
- id?: number;
66
- reason?: Error;
77
+ id?: string;
78
+ reason?: unknown;
67
79
  }
68
80
  declare class AsyncIdQueue<T> {
69
81
  private readonly openIds;
70
- private readonly items;
71
- private readonly pendingPulls;
82
+ private readonly queues;
83
+ private readonly waiters;
72
84
  get length(): number;
73
- open(id: number): void;
74
- isOpen(id: number): boolean;
75
- push(id: number, item: T): void;
76
- pull(id: number): Promise<T>;
85
+ get waiterIds(): string[];
86
+ hasBufferedItems(id: string): boolean;
87
+ open(id: string): void;
88
+ isOpen(id: string): boolean;
89
+ push(id: string, item: T): void;
90
+ pull(id: string): Promise<T>;
77
91
  close({ id, reason }?: AsyncIdQueueCloseOptions): void;
78
- assertOpen(id: number): void;
92
+ assertOpen(id: string): void;
79
93
  }
80
94
 
81
- declare function toEventIterator(queue: AsyncIdQueue<EventIteratorPayload>, id: number, cleanup: CreateAsyncIteratorObjectCleanupFn): AsyncGenerator;
95
+ interface ToEventIteratorOptions extends SetSpanErrorOptions {
96
+ }
97
+ declare function toEventIterator(queue: AsyncIdQueue<EventIteratorPayload>, id: string, cleanup: AsyncIteratorClassCleanupFn, options?: ToEventIteratorOptions): AsyncIteratorClass<unknown>;
82
98
  declare function resolveEventIterator(iterator: AsyncIterator<any>, callback: (payload: EventIteratorPayload) => Promise<'next' | 'abort'>): Promise<void>;
83
99
 
100
+ interface ServerPeerHandleRequestFn {
101
+ (request: StandardRequest): Promise<StandardResponse>;
102
+ }
84
103
  interface ServerPeerCloseOptions extends AsyncIdQueueCloseOptions$1 {
85
104
  /**
86
105
  * Should abort or not?
@@ -90,16 +109,28 @@ interface ServerPeerCloseOptions extends AsyncIdQueueCloseOptions$1 {
90
109
  abort?: boolean;
91
110
  }
92
111
  declare class ServerPeer {
112
+ /**
113
+ * Queue of event iterator messages sent from client, awaiting consumption
114
+ */
93
115
  private readonly clientEventIteratorQueue;
116
+ /**
117
+ * Map of active client request controllers, should be synced to request signal
118
+ */
94
119
  private readonly clientControllers;
95
120
  private readonly send;
96
121
  constructor(send: EncodedMessageSendFn);
97
122
  get length(): number;
98
- open(id: number): AbortController;
99
- message(raw: EncodedMessage): Promise<[id: number, StandardRequest | undefined]>;
100
- response(id: number, response: StandardResponse): Promise<void>;
123
+ open(id: string): AbortController;
124
+ /**
125
+ * @todo This method will return Promise<void> in the next major version.
126
+ */
127
+ message(raw: EncodedMessage, handleRequest?: ServerPeerHandleRequestFn): Promise<[id: string, StandardRequest | undefined]>;
128
+ /**
129
+ * @deprecated Please pass the `handleRequest` (second arg) function to the `message` method instead.
130
+ */
131
+ response(id: string, response: StandardResponse): Promise<void>;
101
132
  close({ abort, ...options }?: ServerPeerCloseOptions): void;
102
133
  }
103
134
 
104
- export { ClientPeer, MessageType, ServerPeer, decodeRequestMessage, decodeResponseMessage, encodeRequestMessage, encodeResponseMessage, isEventIteratorHeaders, resolveEventIterator, toEventIterator };
105
- export type { ClientPeerCloseOptions, DecodedRequestMessage, DecodedResponseMessage, EncodedMessage, EncodedMessageSendFn, EventIteratorEvent, EventIteratorPayload, RequestMessageMap, ResponseMessageMap, ServerPeerCloseOptions };
135
+ export { ClientPeer, MessageType, ServerPeer, decodeRequestMessage, decodeResponseMessage, encodeRequestMessage, encodeResponseMessage, resolveEventIterator, toEventIterator };
136
+ export type { ClientPeerCloseOptions, DecodedRequestMessage, DecodedResponseMessage, EncodedMessage, EncodedMessageSendFn, EventIteratorEvent, EventIteratorPayload, RequestMessageMap, ResponseMessageMap, ServerPeerCloseOptions, ServerPeerHandleRequestFn, ToEventIteratorOptions };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { Promisable, AsyncIdQueueCloseOptions as AsyncIdQueueCloseOptions$1, CreateAsyncIteratorObjectCleanupFn } from '@orpc/shared';
2
- import { StandardRequest, StandardResponse, EventMeta, StandardHeaders } from '@orpc/standard-server';
1
+ import { Promisable, AsyncIdQueueCloseOptions as AsyncIdQueueCloseOptions$1, SetSpanErrorOptions, AsyncIteratorClassCleanupFn, AsyncIteratorClass } from '@orpc/shared';
2
+ import { StandardRequest, StandardResponse, EventMeta } from '@orpc/standard-server';
3
3
 
4
- type EncodedMessage = string | ArrayBufferLike | Blob;
4
+ type EncodedMessage = string | ArrayBufferLike | Uint8Array;
5
5
  interface EncodedMessageSendFn {
6
6
  (message: EncodedMessage): Promisable<void>;
7
7
  }
@@ -16,13 +16,26 @@ interface ClientPeerCloseOptions extends AsyncIdQueueCloseOptions$1 {
16
16
  }
17
17
  declare class ClientPeer {
18
18
  private readonly idGenerator;
19
+ /**
20
+ * Queue of responses sent from server, awaiting consumption
21
+ */
19
22
  private readonly responseQueue;
23
+ /**
24
+ * Queue of event iterator messages sent from server, awaiting consumption
25
+ */
20
26
  private readonly serverEventIteratorQueue;
27
+ /**
28
+ * Controllers used to signal that the client should stop sending event iterator messages
29
+ */
21
30
  private readonly serverControllers;
31
+ /**
32
+ * Cleanup functions invoked when the request/response is closed
33
+ */
34
+ private readonly cleanupFns;
22
35
  private readonly send;
23
36
  constructor(send: EncodedMessageSendFn);
24
37
  get length(): number;
25
- open(id: number): AbortController;
38
+ open(id: string): AbortController;
26
39
  request(request: StandardRequest): Promise<StandardResponse>;
27
40
  message(raw: EncodedMessage): Promise<void>;
28
41
  close(options?: AsyncIdQueueCloseOptions$1): void;
@@ -51,36 +64,42 @@ interface ResponseMessageMap {
51
64
  [MessageType.ABORT_SIGNAL]: void;
52
65
  }
53
66
  type DecodedMessageUnion<TMap extends RequestMessageMap | ResponseMessageMap> = {
54
- [K in keyof TMap]: [id: number, type: K, payload: TMap[K]];
67
+ [K in keyof TMap]: [id: string, type: K, payload: TMap[K]];
55
68
  }[keyof TMap];
56
69
  type DecodedRequestMessage = DecodedMessageUnion<RequestMessageMap>;
57
70
  type DecodedResponseMessage = DecodedMessageUnion<ResponseMessageMap>;
58
- declare function encodeRequestMessage<T extends keyof RequestMessageMap>(id: number, type: T, payload: RequestMessageMap[T]): Promise<EncodedMessage>;
71
+ declare function encodeRequestMessage<T extends keyof RequestMessageMap>(id: string, type: T, payload: RequestMessageMap[T]): Promise<EncodedMessage>;
59
72
  declare function decodeRequestMessage(raw: EncodedMessage): Promise<DecodedRequestMessage>;
60
- declare function encodeResponseMessage<T extends keyof ResponseMessageMap>(id: number, type: T, payload: ResponseMessageMap[T]): Promise<EncodedMessage>;
73
+ declare function encodeResponseMessage<T extends keyof ResponseMessageMap>(id: string, type: T, payload: ResponseMessageMap[T]): Promise<EncodedMessage>;
61
74
  declare function decodeResponseMessage(raw: EncodedMessage): Promise<DecodedResponseMessage>;
62
- declare function isEventIteratorHeaders(headers: StandardHeaders): boolean;
63
75
 
64
76
  interface AsyncIdQueueCloseOptions {
65
- id?: number;
66
- reason?: Error;
77
+ id?: string;
78
+ reason?: unknown;
67
79
  }
68
80
  declare class AsyncIdQueue<T> {
69
81
  private readonly openIds;
70
- private readonly items;
71
- private readonly pendingPulls;
82
+ private readonly queues;
83
+ private readonly waiters;
72
84
  get length(): number;
73
- open(id: number): void;
74
- isOpen(id: number): boolean;
75
- push(id: number, item: T): void;
76
- pull(id: number): Promise<T>;
85
+ get waiterIds(): string[];
86
+ hasBufferedItems(id: string): boolean;
87
+ open(id: string): void;
88
+ isOpen(id: string): boolean;
89
+ push(id: string, item: T): void;
90
+ pull(id: string): Promise<T>;
77
91
  close({ id, reason }?: AsyncIdQueueCloseOptions): void;
78
- assertOpen(id: number): void;
92
+ assertOpen(id: string): void;
79
93
  }
80
94
 
81
- declare function toEventIterator(queue: AsyncIdQueue<EventIteratorPayload>, id: number, cleanup: CreateAsyncIteratorObjectCleanupFn): AsyncGenerator;
95
+ interface ToEventIteratorOptions extends SetSpanErrorOptions {
96
+ }
97
+ declare function toEventIterator(queue: AsyncIdQueue<EventIteratorPayload>, id: string, cleanup: AsyncIteratorClassCleanupFn, options?: ToEventIteratorOptions): AsyncIteratorClass<unknown>;
82
98
  declare function resolveEventIterator(iterator: AsyncIterator<any>, callback: (payload: EventIteratorPayload) => Promise<'next' | 'abort'>): Promise<void>;
83
99
 
100
+ interface ServerPeerHandleRequestFn {
101
+ (request: StandardRequest): Promise<StandardResponse>;
102
+ }
84
103
  interface ServerPeerCloseOptions extends AsyncIdQueueCloseOptions$1 {
85
104
  /**
86
105
  * Should abort or not?
@@ -90,16 +109,28 @@ interface ServerPeerCloseOptions extends AsyncIdQueueCloseOptions$1 {
90
109
  abort?: boolean;
91
110
  }
92
111
  declare class ServerPeer {
112
+ /**
113
+ * Queue of event iterator messages sent from client, awaiting consumption
114
+ */
93
115
  private readonly clientEventIteratorQueue;
116
+ /**
117
+ * Map of active client request controllers, should be synced to request signal
118
+ */
94
119
  private readonly clientControllers;
95
120
  private readonly send;
96
121
  constructor(send: EncodedMessageSendFn);
97
122
  get length(): number;
98
- open(id: number): AbortController;
99
- message(raw: EncodedMessage): Promise<[id: number, StandardRequest | undefined]>;
100
- response(id: number, response: StandardResponse): Promise<void>;
123
+ open(id: string): AbortController;
124
+ /**
125
+ * @todo This method will return Promise<void> in the next major version.
126
+ */
127
+ message(raw: EncodedMessage, handleRequest?: ServerPeerHandleRequestFn): Promise<[id: string, StandardRequest | undefined]>;
128
+ /**
129
+ * @deprecated Please pass the `handleRequest` (second arg) function to the `message` method instead.
130
+ */
131
+ response(id: string, response: StandardResponse): Promise<void>;
101
132
  close({ abort, ...options }?: ServerPeerCloseOptions): void;
102
133
  }
103
134
 
104
- export { ClientPeer, MessageType, ServerPeer, decodeRequestMessage, decodeResponseMessage, encodeRequestMessage, encodeResponseMessage, isEventIteratorHeaders, resolveEventIterator, toEventIterator };
105
- export type { ClientPeerCloseOptions, DecodedRequestMessage, DecodedResponseMessage, EncodedMessage, EncodedMessageSendFn, EventIteratorEvent, EventIteratorPayload, RequestMessageMap, ResponseMessageMap, ServerPeerCloseOptions };
135
+ export { ClientPeer, MessageType, ServerPeer, decodeRequestMessage, decodeResponseMessage, encodeRequestMessage, encodeResponseMessage, resolveEventIterator, toEventIterator };
136
+ export type { ClientPeerCloseOptions, DecodedRequestMessage, DecodedResponseMessage, EncodedMessage, EncodedMessageSendFn, EventIteratorEvent, EventIteratorPayload, RequestMessageMap, ResponseMessageMap, ServerPeerCloseOptions, ServerPeerHandleRequestFn, ToEventIteratorOptions };
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { isAsyncIteratorObject, stringifyJSON, createAsyncIteratorObject, isTypescriptObject, SequentialIdGenerator, AsyncIdQueue } from '@orpc/shared';
2
- import { flattenHeader, getFilenameFromContentDisposition, generateContentDisposition, withEventMeta, ErrorEvent, getEventMeta } from '@orpc/standard-server';
1
+ import { isAsyncIteratorObject, stringifyJSON, readAsBuffer, AsyncIteratorClass, startSpan, runInSpanContext, isTypescriptObject, setSpanError, runWithSpan, SequentialIdGenerator, AsyncIdQueue, getGlobalOtelConfig, clone, AbortError } from '@orpc/shared';
2
+ import { generateContentDisposition, flattenHeader, getFilenameFromContentDisposition, withEventMeta, ErrorEvent, getEventMeta, isEventIteratorHeaders, HibernationEventIterator } from '@orpc/standard-server';
3
3
 
4
4
  var MessageType = /* @__PURE__ */ ((MessageType2) => {
5
5
  MessageType2[MessageType2["REQUEST"] = 1] = "REQUEST";
@@ -22,7 +22,7 @@ async function encodeRequestMessage(id, type, payload) {
22
22
  return encodeRawMessage({ i: id, t: type, p: payload });
23
23
  }
24
24
  const request = payload;
25
- const { body: processedBody, headers: processedHeaders } = await prepareBodyAndHeadersForSerialization(
25
+ const { body: processedBody, headers: processedHeaders } = await serializeBodyAndHeaders(
26
26
  request.body,
27
27
  request.headers
28
28
  );
@@ -42,7 +42,7 @@ async function encodeRequestMessage(id, type, payload) {
42
42
  return encodeRawMessage(baseMessage);
43
43
  }
44
44
  async function decodeRequestMessage(raw) {
45
- const { json: message, blobData } = await decodeRawMessage(raw);
45
+ const { json: message, buffer } = await decodeRawMessage(raw);
46
46
  const id = message.i;
47
47
  const type = message.t;
48
48
  if (type === 3 /* EVENT_ITERATOR */) {
@@ -54,20 +54,7 @@ async function decodeRequestMessage(raw) {
54
54
  }
55
55
  const payload = message.p;
56
56
  const headers = payload.h ?? {};
57
- let body = payload.b;
58
- const contentType = flattenHeader(headers["content-type"]);
59
- if (blobData) {
60
- const contentDisposition = flattenHeader(headers["content-disposition"]);
61
- if (contentDisposition === void 0 && contentType?.startsWith("multipart/form-data")) {
62
- const tempRes = new Response(blobData, { headers: { "content-type": contentType } });
63
- body = await tempRes.formData();
64
- } else {
65
- const filename = getFilenameFromContentDisposition(contentDisposition) ?? "blob";
66
- body = new File([blobData], filename, { type: contentType });
67
- }
68
- } else if (contentType?.startsWith("application/x-www-form-urlencoded") && typeof body === "string") {
69
- body = new URLSearchParams(body);
70
- }
57
+ const body = await deserializeBody(headers, payload.b, buffer);
71
58
  return [id, 1 /* REQUEST */, { url: new URL(payload.u, "orpc:/"), headers, method: payload.m ?? "POST", body }];
72
59
  }
73
60
  async function encodeResponseMessage(id, type, payload) {
@@ -84,7 +71,7 @@ async function encodeResponseMessage(id, type, payload) {
84
71
  return encodeRawMessage({ i: id, t: type, p: void 0 });
85
72
  }
86
73
  const response = payload;
87
- const { body: processedBody, headers: processedHeaders } = await prepareBodyAndHeadersForSerialization(
74
+ const { body: processedBody, headers: processedHeaders } = await serializeBodyAndHeaders(
88
75
  response.body,
89
76
  response.headers
90
77
  );
@@ -103,7 +90,7 @@ async function encodeResponseMessage(id, type, payload) {
103
90
  return encodeRawMessage(baseMessage);
104
91
  }
105
92
  async function decodeResponseMessage(raw) {
106
- const { json: message, blobData } = await decodeRawMessage(raw);
93
+ const { json: message, buffer } = await decodeRawMessage(raw);
107
94
  const id = message.i;
108
95
  const type = message.t;
109
96
  if (type === 3 /* EVENT_ITERATOR */) {
@@ -115,23 +102,10 @@ async function decodeResponseMessage(raw) {
115
102
  }
116
103
  const payload = message.p;
117
104
  const headers = payload.h ?? {};
118
- let body = payload.b;
119
- const contentType = flattenHeader(headers["content-type"]);
120
- if (blobData) {
121
- const contentDisposition = flattenHeader(headers["content-disposition"]);
122
- if (contentDisposition === void 0 && contentType?.startsWith("multipart/form-data")) {
123
- const tempRes = new Response(blobData, { headers: { "content-type": contentType } });
124
- body = await tempRes.formData();
125
- } else {
126
- const filename = getFilenameFromContentDisposition(contentDisposition) ?? "blob";
127
- body = new File([blobData], filename, { type: contentType });
128
- }
129
- } else if (contentType?.startsWith("application/x-www-form-urlencoded") && typeof body === "string") {
130
- body = new URLSearchParams(body);
131
- }
105
+ const body = await deserializeBody(headers, payload.b, buffer);
132
106
  return [id, 2 /* RESPONSE */, { status: payload.s ?? 200, headers, body }];
133
107
  }
134
- async function prepareBodyAndHeadersForSerialization(body, originalHeaders) {
108
+ async function serializeBodyAndHeaders(body, originalHeaders) {
135
109
  const headers = { ...originalHeaders };
136
110
  const originalContentDisposition = headers["content-disposition"];
137
111
  delete headers["content-type"];
@@ -159,113 +133,184 @@ async function prepareBodyAndHeadersForSerialization(body, originalHeaders) {
159
133
  }
160
134
  return { body, headers };
161
135
  }
162
- function isEventIteratorHeaders(headers) {
163
- return Boolean(flattenHeader(headers["content-type"])?.startsWith("text/event-stream") && headers["content-disposition"] === void 0);
136
+ async function deserializeBody(headers, body, buffer) {
137
+ const contentType = flattenHeader(headers["content-type"]);
138
+ const contentDisposition = flattenHeader(headers["content-disposition"]);
139
+ if (typeof contentDisposition === "string") {
140
+ const filename = getFilenameFromContentDisposition(contentDisposition) ?? "blob";
141
+ return new File(buffer === void 0 ? [] : [buffer], filename, { type: contentType });
142
+ }
143
+ if (contentType?.startsWith("multipart/form-data")) {
144
+ const tempRes = new Response(buffer, { headers: { "content-type": contentType } });
145
+ return tempRes.formData();
146
+ }
147
+ if (contentType?.startsWith("application/x-www-form-urlencoded") && typeof body === "string") {
148
+ return new URLSearchParams(body);
149
+ }
150
+ return body;
164
151
  }
165
152
  const JSON_AND_BINARY_DELIMITER = 255;
166
- async function encodeRawMessage(data, blobData) {
153
+ async function encodeRawMessage(data, blob) {
167
154
  const json = stringifyJSON(data);
168
- if (blobData === void 0) {
155
+ if (blob === void 0 || blob.size === 0) {
169
156
  return json;
170
157
  }
171
- return new Blob([
158
+ return readAsBuffer(new Blob([
172
159
  new TextEncoder().encode(json),
173
160
  new Uint8Array([JSON_AND_BINARY_DELIMITER]),
174
- blobData
175
- ]);
161
+ blob
162
+ ]));
176
163
  }
177
164
  async function decodeRawMessage(raw) {
178
165
  if (typeof raw === "string") {
179
166
  return { json: JSON.parse(raw) };
180
167
  }
181
- const buffer = new Uint8Array(raw instanceof Blob ? await raw.arrayBuffer() : raw);
168
+ const buffer = raw instanceof Uint8Array ? raw : new Uint8Array(raw);
182
169
  const delimiterIndex = buffer.indexOf(JSON_AND_BINARY_DELIMITER);
183
170
  if (delimiterIndex === -1) {
184
171
  const jsonPart2 = new TextDecoder().decode(buffer);
185
172
  return { json: JSON.parse(jsonPart2) };
186
173
  }
187
- const jsonPart = new TextDecoder().decode(buffer.slice(0, delimiterIndex));
188
- const blobData = buffer.slice(delimiterIndex + 1);
174
+ const jsonPart = new TextDecoder().decode(buffer.subarray(0, delimiterIndex));
175
+ const bufferPart = buffer.subarray(delimiterIndex + 1);
189
176
  return {
190
177
  json: JSON.parse(jsonPart),
191
- blobData: blobData.buffer
178
+ buffer: bufferPart
192
179
  };
193
180
  }
194
181
 
195
- function toEventIterator(queue, id, cleanup) {
196
- return createAsyncIteratorObject(async () => {
197
- const item = await queue.pull(id);
198
- switch (item.event) {
199
- case "message": {
200
- let data = item.data;
201
- if (item.meta && isTypescriptObject(data)) {
202
- data = withEventMeta(data, item.meta);
182
+ function toEventIterator(queue, id, cleanup, options = {}) {
183
+ let span;
184
+ return new AsyncIteratorClass(async () => {
185
+ span ??= startSpan("consume_event_iterator_stream");
186
+ try {
187
+ const item = await runInSpanContext(span, () => queue.pull(id));
188
+ switch (item.event) {
189
+ case "message": {
190
+ let data = item.data;
191
+ if (item.meta && isTypescriptObject(data)) {
192
+ data = withEventMeta(data, item.meta);
193
+ }
194
+ span?.addEvent("message");
195
+ return { value: data, done: false };
203
196
  }
204
- return { value: data, done: false };
205
- }
206
- case "error": {
207
- let error = new ErrorEvent({
208
- data: item.data
209
- });
210
- if (item.meta) {
211
- error = withEventMeta(error, item.meta);
197
+ case "error": {
198
+ let error = new ErrorEvent({
199
+ data: item.data
200
+ });
201
+ if (item.meta) {
202
+ error = withEventMeta(error, item.meta);
203
+ }
204
+ span?.addEvent("error");
205
+ throw error;
212
206
  }
213
- throw error;
214
- }
215
- case "done": {
216
- let data = item.data;
217
- if (item.meta && isTypescriptObject(data)) {
218
- data = withEventMeta(data, item.meta);
207
+ case "done": {
208
+ let data = item.data;
209
+ if (item.meta && isTypescriptObject(data)) {
210
+ data = withEventMeta(data, item.meta);
211
+ }
212
+ span?.addEvent("done");
213
+ return { value: data, done: true };
219
214
  }
220
- return { value: data, done: true };
221
215
  }
222
- }
223
- }, cleanup);
224
- }
225
- async function resolveEventIterator(iterator, callback) {
226
- while (true) {
227
- const payload = await (async () => {
228
- try {
229
- const { value, done } = await iterator.next();
230
- if (done) {
231
- return { event: "done", data: value, meta: getEventMeta(value) };
232
- }
233
- return { event: "message", data: value, meta: getEventMeta(value) };
234
- } catch (err) {
235
- return {
236
- meta: getEventMeta(err),
237
- event: "error",
238
- data: err instanceof ErrorEvent ? err.data : void 0
239
- };
216
+ } catch (e) {
217
+ if (!(e instanceof ErrorEvent)) {
218
+ setSpanError(span, e, options);
240
219
  }
241
- })();
220
+ throw e;
221
+ }
222
+ }, async (reason) => {
242
223
  try {
243
- const direction = await callback(payload);
244
- if (payload.event === "done" || payload.event === "error") {
245
- return;
224
+ if (reason !== "next") {
225
+ span?.addEvent("cancelled");
246
226
  }
247
- if (direction === "abort") {
227
+ await runInSpanContext(span, () => cleanup(reason));
228
+ } catch (e) {
229
+ setSpanError(span, e, options);
230
+ throw e;
231
+ } finally {
232
+ span?.end();
233
+ }
234
+ });
235
+ }
236
+ function resolveEventIterator(iterator, callback) {
237
+ return runWithSpan(
238
+ { name: "stream_event_iterator" },
239
+ async (span) => {
240
+ while (true) {
241
+ const payload = await (async () => {
242
+ try {
243
+ const { value, done } = await iterator.next();
244
+ if (done) {
245
+ span?.addEvent("done");
246
+ return { event: "done", data: value, meta: getEventMeta(value) };
247
+ }
248
+ span?.addEvent("message");
249
+ return { event: "message", data: value, meta: getEventMeta(value) };
250
+ } catch (err) {
251
+ if (err instanceof ErrorEvent) {
252
+ span?.addEvent("error");
253
+ return {
254
+ event: "error",
255
+ data: err.data,
256
+ meta: getEventMeta(err)
257
+ };
258
+ } else {
259
+ try {
260
+ await callback({ event: "error", data: void 0 });
261
+ } catch (err2) {
262
+ setSpanError(span, err);
263
+ throw err2;
264
+ }
265
+ throw err;
266
+ }
267
+ }
268
+ })();
269
+ let isInvokeCleanupFn = false;
248
270
  try {
249
- await iterator.return?.();
250
- } catch {
271
+ const direction = await callback(payload);
272
+ if (payload.event === "done" || payload.event === "error") {
273
+ return;
274
+ }
275
+ if (direction === "abort") {
276
+ isInvokeCleanupFn = true;
277
+ await iterator.return?.();
278
+ return;
279
+ }
280
+ } catch (err) {
281
+ if (!isInvokeCleanupFn) {
282
+ try {
283
+ await iterator.return?.();
284
+ } catch (err2) {
285
+ setSpanError(span, err);
286
+ throw err2;
287
+ }
288
+ }
289
+ throw err;
251
290
  }
252
- return;
253
- }
254
- } catch (err) {
255
- try {
256
- await iterator.return?.();
257
- } catch {
258
291
  }
259
- throw err;
260
292
  }
261
- }
293
+ );
262
294
  }
263
295
 
264
296
  class ClientPeer {
265
297
  idGenerator = new SequentialIdGenerator();
298
+ /**
299
+ * Queue of responses sent from server, awaiting consumption
300
+ */
266
301
  responseQueue = new AsyncIdQueue();
302
+ /**
303
+ * Queue of event iterator messages sent from server, awaiting consumption
304
+ */
267
305
  serverEventIteratorQueue = new AsyncIdQueue();
306
+ /**
307
+ * Controllers used to signal that the client should stop sending event iterator messages
308
+ */
268
309
  serverControllers = /* @__PURE__ */ new Map();
310
+ /**
311
+ * Cleanup functions invoked when the request/response is closed
312
+ */
313
+ cleanupFns = /* @__PURE__ */ new Map();
269
314
  send;
270
315
  constructor(send) {
271
316
  this.send = async (id, ...rest) => encodeRequestMessage(id, ...rest).then(async (raw) => {
@@ -275,48 +320,85 @@ class ClientPeer {
275
320
  });
276
321
  }
277
322
  get length() {
278
- return (+this.responseQueue.length + this.serverEventIteratorQueue.length + this.serverControllers.size) / 3;
323
+ return (+this.responseQueue.length + this.serverEventIteratorQueue.length + this.serverControllers.size + this.cleanupFns.size) / 4;
279
324
  }
280
325
  open(id) {
281
326
  this.serverEventIteratorQueue.open(id);
282
327
  this.responseQueue.open(id);
283
328
  const controller = new AbortController();
284
329
  this.serverControllers.set(id, controller);
330
+ this.cleanupFns.set(id, []);
285
331
  return controller;
286
332
  }
287
333
  async request(request) {
288
334
  const signal = request.signal;
289
- if (signal?.aborted) {
290
- throw signal.reason;
291
- }
292
- const id = this.idGenerator.generate();
293
- const serverController = this.open(id);
294
- return new Promise((resolve, reject) => {
295
- this.send(id, MessageType.REQUEST, request).then(async () => {
335
+ return runWithSpan(
336
+ { name: "send_peer_request", signal },
337
+ async () => {
296
338
  if (signal?.aborted) {
297
- await this.send(id, MessageType.ABORT_SIGNAL, void 0);
298
- this.close({ id, reason: signal.reason });
299
- return;
339
+ throw signal.reason;
300
340
  }
301
- signal?.addEventListener("abort", async () => {
302
- await this.send(id, MessageType.ABORT_SIGNAL, void 0);
303
- this.close({ id, reason: signal.reason });
304
- }, { once: true });
305
- if (isAsyncIteratorObject(request.body)) {
306
- await resolveEventIterator(request.body, async (payload) => {
307
- if (serverController.signal.aborted) {
308
- return "abort";
309
- }
310
- await this.send(id, MessageType.EVENT_ITERATOR, payload);
311
- return "next";
341
+ const id = this.idGenerator.generate();
342
+ const serverController = this.open(id);
343
+ try {
344
+ const otelConfig = getGlobalOtelConfig();
345
+ if (otelConfig) {
346
+ const headers = clone(request.headers);
347
+ otelConfig.propagation.inject(otelConfig.context.active(), headers);
348
+ request = { ...request, headers };
349
+ }
350
+ await this.send(id, MessageType.REQUEST, request);
351
+ if (signal?.aborted) {
352
+ await this.send(id, MessageType.ABORT_SIGNAL, void 0);
353
+ throw signal.reason;
354
+ }
355
+ let abortListener;
356
+ signal?.addEventListener("abort", abortListener = async () => {
357
+ await this.send(id, MessageType.ABORT_SIGNAL, void 0);
358
+ this.close({ id, reason: signal.reason });
359
+ }, { once: true });
360
+ this.cleanupFns.get(id)?.push(() => {
361
+ signal?.removeEventListener("abort", abortListener);
312
362
  });
363
+ if (isAsyncIteratorObject(request.body)) {
364
+ const iterator = request.body;
365
+ void resolveEventIterator(iterator, async (payload) => {
366
+ if (serverController.signal.aborted) {
367
+ return "abort";
368
+ }
369
+ await this.send(id, MessageType.EVENT_ITERATOR, payload);
370
+ return "next";
371
+ });
372
+ }
373
+ const response = await this.responseQueue.pull(id);
374
+ if (isEventIteratorHeaders(response.headers)) {
375
+ const iterator = toEventIterator(
376
+ this.serverEventIteratorQueue,
377
+ id,
378
+ async (reason) => {
379
+ try {
380
+ if (reason !== "next") {
381
+ await this.send(id, MessageType.ABORT_SIGNAL, void 0);
382
+ }
383
+ } finally {
384
+ this.close({ id });
385
+ }
386
+ },
387
+ { signal }
388
+ );
389
+ return {
390
+ ...response,
391
+ body: iterator
392
+ };
393
+ }
394
+ this.close({ id });
395
+ return response;
396
+ } catch (err) {
397
+ this.close({ id, reason: err });
398
+ throw err;
313
399
  }
314
- }).catch((err) => {
315
- this.close({ id, reason: err });
316
- reject(err);
317
- });
318
- this.responseQueue.pull(id).then(resolve).catch(reject);
319
- });
400
+ }
401
+ );
320
402
  }
321
403
  async message(raw) {
322
404
  const [id, type, payload] = await decodeResponseMessage(raw);
@@ -333,31 +415,19 @@ class ClientPeer {
333
415
  if (!this.responseQueue.isOpen(id)) {
334
416
  return;
335
417
  }
336
- if (isEventIteratorHeaders(payload.headers)) {
337
- this.responseQueue.push(id, {
338
- ...payload,
339
- body: toEventIterator(this.serverEventIteratorQueue, id, async (reason) => {
340
- try {
341
- if (reason !== "next") {
342
- await this.send(id, MessageType.ABORT_SIGNAL, void 0);
343
- }
344
- } finally {
345
- this.close({ id });
346
- }
347
- })
348
- });
349
- } else {
350
- this.responseQueue.push(id, payload);
351
- this.close({ id });
352
- }
418
+ this.responseQueue.push(id, payload);
353
419
  }
354
420
  close(options = {}) {
355
421
  if (options.id !== void 0) {
356
422
  this.serverControllers.get(options.id)?.abort(options.reason);
357
423
  this.serverControllers.delete(options.id);
424
+ this.cleanupFns.get(options.id)?.forEach((fn) => fn());
425
+ this.cleanupFns.delete(options.id);
358
426
  } else {
359
427
  this.serverControllers.forEach((c) => c.abort(options.reason));
360
428
  this.serverControllers.clear();
429
+ this.cleanupFns.forEach((fns) => fns.forEach((fn) => fn()));
430
+ this.cleanupFns.clear();
361
431
  }
362
432
  this.responseQueue.close(options);
363
433
  this.serverEventIteratorQueue.close(options);
@@ -365,7 +435,13 @@ class ClientPeer {
365
435
  }
366
436
 
367
437
  class ServerPeer {
438
+ /**
439
+ * Queue of event iterator messages sent from client, awaiting consumption
440
+ */
368
441
  clientEventIteratorQueue = new AsyncIdQueue();
442
+ /**
443
+ * Map of active client request controllers, should be synced to request signal
444
+ */
369
445
  clientControllers = /* @__PURE__ */ new Map();
370
446
  send;
371
447
  constructor(send) {
@@ -384,10 +460,13 @@ class ServerPeer {
384
460
  this.clientControllers.set(id, controller);
385
461
  return controller;
386
462
  }
387
- async message(raw) {
463
+ /**
464
+ * @todo This method will return Promise<void> in the next major version.
465
+ */
466
+ async message(raw, handleRequest) {
388
467
  const [id, type, payload] = await decodeRequestMessage(raw);
389
468
  if (type === MessageType.ABORT_SIGNAL) {
390
- this.close({ id });
469
+ this.close({ id, reason: new AbortError("Client aborted the request") });
391
470
  return [id, void 0];
392
471
  }
393
472
  if (type === MessageType.EVENT_ITERATOR) {
@@ -397,47 +476,89 @@ class ServerPeer {
397
476
  return [id, void 0];
398
477
  }
399
478
  const clientController = this.open(id);
479
+ const signal = clientController.signal;
400
480
  const request = {
401
481
  ...payload,
402
- signal: clientController.signal,
403
- body: isEventIteratorHeaders(payload.headers) ? toEventIterator(this.clientEventIteratorQueue, id, async (reason) => {
404
- if (reason !== "next") {
405
- await this.send(id, MessageType.ABORT_SIGNAL, void 0);
406
- }
407
- }) : payload.body
482
+ signal,
483
+ body: isEventIteratorHeaders(payload.headers) ? toEventIterator(
484
+ this.clientEventIteratorQueue,
485
+ id,
486
+ async (reason) => {
487
+ if (reason !== "next") {
488
+ await this.send(id, MessageType.ABORT_SIGNAL, void 0);
489
+ }
490
+ },
491
+ { signal }
492
+ ) : payload.body
408
493
  };
494
+ if (handleRequest) {
495
+ let context;
496
+ const otelConfig = getGlobalOtelConfig();
497
+ if (otelConfig) {
498
+ context = otelConfig.propagation.extract(otelConfig.context.active(), request.headers);
499
+ }
500
+ await runWithSpan(
501
+ { name: "receive_peer_request", context },
502
+ async () => {
503
+ const response = await runWithSpan(
504
+ { name: "handle_request" },
505
+ async () => {
506
+ try {
507
+ return await handleRequest(request);
508
+ } catch (reason) {
509
+ this.close({ id, reason, abort: false });
510
+ throw reason;
511
+ }
512
+ }
513
+ );
514
+ await runWithSpan(
515
+ { name: "send_peer_response" },
516
+ () => this.response(id, response)
517
+ );
518
+ }
519
+ );
520
+ }
409
521
  return [id, request];
410
522
  }
523
+ /**
524
+ * @deprecated Please pass the `handleRequest` (second arg) function to the `message` method instead.
525
+ */
411
526
  async response(id, response) {
412
527
  const signal = this.clientControllers.get(id)?.signal;
413
528
  if (!signal || signal.aborted) {
414
529
  return;
415
530
  }
416
- await this.send(id, MessageType.RESPONSE, response).then(async () => {
531
+ try {
532
+ await this.send(id, MessageType.RESPONSE, response);
417
533
  if (!signal.aborted && isAsyncIteratorObject(response.body)) {
418
- await resolveEventIterator(response.body, async (payload) => {
419
- if (signal.aborted) {
420
- return "abort";
421
- }
422
- await this.send(id, MessageType.EVENT_ITERATOR, payload);
423
- return "next";
424
- });
534
+ if (response.body instanceof HibernationEventIterator) {
535
+ response.body.hibernationCallback?.(id);
536
+ } else {
537
+ const iterator = response.body;
538
+ await resolveEventIterator(iterator, async (payload) => {
539
+ if (signal.aborted) {
540
+ return "abort";
541
+ }
542
+ await this.send(id, MessageType.EVENT_ITERATOR, payload);
543
+ return "next";
544
+ });
545
+ }
425
546
  }
426
547
  this.close({ id, abort: false });
427
- }).catch((reason) => {
548
+ } catch (reason) {
428
549
  this.close({ id, reason, abort: false });
429
550
  throw reason;
430
- });
551
+ }
431
552
  }
432
553
  close({ abort = true, ...options } = {}) {
433
554
  if (options.id === void 0) {
434
555
  if (abort) {
435
- this.clientControllers.forEach((c) => c.abort());
556
+ this.clientControllers.forEach((c) => c.abort(options.reason));
436
557
  }
437
558
  this.clientControllers.clear();
438
559
  } else {
439
560
  if (abort) {
440
- this.clientControllers.get(options.id)?.abort();
561
+ this.clientControllers.get(options.id)?.abort(options.reason);
441
562
  }
442
563
  this.clientControllers.delete(options.id);
443
564
  }
@@ -445,4 +566,4 @@ class ServerPeer {
445
566
  }
446
567
  }
447
568
 
448
- export { ClientPeer, MessageType, ServerPeer, decodeRequestMessage, decodeResponseMessage, encodeRequestMessage, encodeResponseMessage, isEventIteratorHeaders, resolveEventIterator, toEventIterator };
569
+ export { ClientPeer, MessageType, ServerPeer, decodeRequestMessage, decodeResponseMessage, encodeRequestMessage, encodeResponseMessage, resolveEventIterator, toEventIterator };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@orpc/standard-server-peer",
3
3
  "type": "module",
4
- "version": "0.0.0-next.e385fb7",
4
+ "version": "0.0.0-next.e4b0df6",
5
5
  "license": "MIT",
6
6
  "homepage": "https://unnoq.com",
7
7
  "repository": {
@@ -23,8 +23,8 @@
23
23
  "dist"
24
24
  ],
25
25
  "dependencies": {
26
- "@orpc/shared": "0.0.0-next.e385fb7",
27
- "@orpc/standard-server": "0.0.0-next.e385fb7"
26
+ "@orpc/shared": "0.0.0-next.e4b0df6",
27
+ "@orpc/standard-server": "0.0.0-next.e4b0df6"
28
28
  },
29
29
  "scripts": {
30
30
  "build": "unbuild",