@orpc/standard-server-peer 0.0.0-next.df717fa → 0.0.0-next.e02ffab

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