@orpc/standard-server-peer 1.7.10 → 1.8.0

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,6 +30,7 @@
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
+ - **🔍 First-Class OpenTelemetry**: Seamlessly integrate with OpenTelemetry for observability.
33
34
  - **⚙️ Framework Integrations**: Seamlessly integrate with TanStack Query (React, Vue, Solid, Svelte, Angular), 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.
@@ -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
 
@@ -50,6 +50,7 @@ You can find the full documentation [here](https://orpc.unnoq.com).
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
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.
53
54
  - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Deeply integrate oRPC with [NestJS](https://nestjs.com/).
54
55
  - [@orpc/react](https://www.npmjs.com/package/@orpc/react): Utilities for integrating oRPC with React and React Server Actions.
55
56
  - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): [TanStack Query](https://tanstack.com/query/latest) integration.
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;
@@ -77,9 +90,14 @@ declare class AsyncIdQueue<T> {
77
90
  assertOpen(id: string): void;
78
91
  }
79
92
 
80
- declare function toEventIterator(queue: AsyncIdQueue<EventIteratorPayload>, id: string, cleanup: AsyncIteratorClassCleanupFn): AsyncIteratorClass<unknown>;
93
+ interface ToEventIteratorOptions extends SetSpanErrorOptions {
94
+ }
95
+ declare function toEventIterator(queue: AsyncIdQueue<EventIteratorPayload>, id: string, cleanup: AsyncIteratorClassCleanupFn, options?: ToEventIteratorOptions): AsyncIteratorClass<unknown>;
81
96
  declare function resolveEventIterator(iterator: AsyncIterator<any>, callback: (payload: EventIteratorPayload) => Promise<'next' | 'abort'>): Promise<void>;
82
97
 
98
+ interface ServerPeerHandleRequestFn {
99
+ (request: StandardRequest): Promise<StandardResponse>;
100
+ }
83
101
  interface ServerPeerCloseOptions extends AsyncIdQueueCloseOptions$1 {
84
102
  /**
85
103
  * Should abort or not?
@@ -89,16 +107,28 @@ interface ServerPeerCloseOptions extends AsyncIdQueueCloseOptions$1 {
89
107
  abort?: boolean;
90
108
  }
91
109
  declare class ServerPeer {
110
+ /**
111
+ * Queue of event iterator messages sent from client, awaiting consumption
112
+ */
92
113
  private readonly clientEventIteratorQueue;
114
+ /**
115
+ * Map of active client request controllers, should be synced to request signal
116
+ */
93
117
  private readonly clientControllers;
94
118
  private readonly send;
95
119
  constructor(send: EncodedMessageSendFn);
96
120
  get length(): number;
97
121
  open(id: string): AbortController;
98
- message(raw: EncodedMessage): Promise<[id: string, StandardRequest | undefined]>;
122
+ /**
123
+ * @todo This method will return Promise<void> in the next major version.
124
+ */
125
+ message(raw: EncodedMessage, handleRequest?: ServerPeerHandleRequestFn): Promise<[id: string, StandardRequest | undefined]>;
126
+ /**
127
+ * @deprecated Please pass the `handleRequest` (second arg) function to the `message` method instead.
128
+ */
99
129
  response(id: string, response: StandardResponse): Promise<void>;
100
130
  close({ abort, ...options }?: ServerPeerCloseOptions): void;
101
131
  }
102
132
 
103
133
  export { ClientPeer, MessageType, ServerPeer, decodeRequestMessage, decodeResponseMessage, encodeRequestMessage, encodeResponseMessage, resolveEventIterator, toEventIterator };
104
- export type { ClientPeerCloseOptions, DecodedRequestMessage, DecodedResponseMessage, EncodedMessage, EncodedMessageSendFn, EventIteratorEvent, EventIteratorPayload, RequestMessageMap, ResponseMessageMap, ServerPeerCloseOptions };
134
+ 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;
@@ -77,9 +90,14 @@ declare class AsyncIdQueue<T> {
77
90
  assertOpen(id: string): void;
78
91
  }
79
92
 
80
- declare function toEventIterator(queue: AsyncIdQueue<EventIteratorPayload>, id: string, cleanup: AsyncIteratorClassCleanupFn): AsyncIteratorClass<unknown>;
93
+ interface ToEventIteratorOptions extends SetSpanErrorOptions {
94
+ }
95
+ declare function toEventIterator(queue: AsyncIdQueue<EventIteratorPayload>, id: string, cleanup: AsyncIteratorClassCleanupFn, options?: ToEventIteratorOptions): AsyncIteratorClass<unknown>;
81
96
  declare function resolveEventIterator(iterator: AsyncIterator<any>, callback: (payload: EventIteratorPayload) => Promise<'next' | 'abort'>): Promise<void>;
82
97
 
98
+ interface ServerPeerHandleRequestFn {
99
+ (request: StandardRequest): Promise<StandardResponse>;
100
+ }
83
101
  interface ServerPeerCloseOptions extends AsyncIdQueueCloseOptions$1 {
84
102
  /**
85
103
  * Should abort or not?
@@ -89,16 +107,28 @@ interface ServerPeerCloseOptions extends AsyncIdQueueCloseOptions$1 {
89
107
  abort?: boolean;
90
108
  }
91
109
  declare class ServerPeer {
110
+ /**
111
+ * Queue of event iterator messages sent from client, awaiting consumption
112
+ */
92
113
  private readonly clientEventIteratorQueue;
114
+ /**
115
+ * Map of active client request controllers, should be synced to request signal
116
+ */
93
117
  private readonly clientControllers;
94
118
  private readonly send;
95
119
  constructor(send: EncodedMessageSendFn);
96
120
  get length(): number;
97
121
  open(id: string): AbortController;
98
- message(raw: EncodedMessage): Promise<[id: string, StandardRequest | undefined]>;
122
+ /**
123
+ * @todo This method will return Promise<void> in the next major version.
124
+ */
125
+ message(raw: EncodedMessage, handleRequest?: ServerPeerHandleRequestFn): Promise<[id: string, StandardRequest | undefined]>;
126
+ /**
127
+ * @deprecated Please pass the `handleRequest` (second arg) function to the `message` method instead.
128
+ */
99
129
  response(id: string, response: StandardResponse): Promise<void>;
100
130
  close({ abort, ...options }?: ServerPeerCloseOptions): void;
101
131
  }
102
132
 
103
133
  export { ClientPeer, MessageType, ServerPeer, decodeRequestMessage, decodeResponseMessage, encodeRequestMessage, encodeResponseMessage, resolveEventIterator, toEventIterator };
104
- export type { ClientPeerCloseOptions, DecodedRequestMessage, DecodedResponseMessage, EncodedMessage, EncodedMessageSendFn, EventIteratorEvent, EventIteratorPayload, RequestMessageMap, ResponseMessageMap, ServerPeerCloseOptions };
134
+ 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, 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
4
  var MessageType = /* @__PURE__ */ ((MessageType2) => {
5
5
  MessageType2[MessageType2["REQUEST"] = 1] = "REQUEST";
@@ -179,80 +179,138 @@ async function decodeRawMessage(raw) {
179
179
  };
180
180
  }
181
181
 
182
- function toEventIterator(queue, id, cleanup) {
182
+ function toEventIterator(queue, id, cleanup, options = {}) {
183
+ let span;
183
184
  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);
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 };
190
196
  }
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);
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;
199
206
  }
200
- throw error;
201
- }
202
- case "done": {
203
- let data = item.data;
204
- if (item.meta && isTypescriptObject(data)) {
205
- 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 };
206
214
  }
207
- return { value: data, done: true };
208
215
  }
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
- };
216
+ } catch (e) {
217
+ if (!(e instanceof ErrorEvent)) {
218
+ setSpanError(span, e, options);
227
219
  }
228
- })();
220
+ throw e;
221
+ }
222
+ }, async (reason) => {
229
223
  try {
230
- const direction = await callback(payload);
231
- if (payload.event === "done" || payload.event === "error") {
232
- return;
224
+ if (reason !== "next") {
225
+ span?.addEvent("cancelled");
233
226
  }
234
- 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;
235
270
  try {
236
- await iterator.return?.();
237
- } 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;
238
290
  }
239
- return;
240
- }
241
- } catch (err) {
242
- try {
243
- await iterator.return?.();
244
- } catch {
245
291
  }
246
- throw err;
247
292
  }
248
- }
293
+ );
249
294
  }
250
295
 
251
296
  class ClientPeer {
252
297
  idGenerator = new SequentialIdGenerator();
298
+ /**
299
+ * Queue of responses sent from server, awaiting consumption
300
+ */
253
301
  responseQueue = new AsyncIdQueue();
302
+ /**
303
+ * Queue of event iterator messages sent from server, awaiting consumption
304
+ */
254
305
  serverEventIteratorQueue = new AsyncIdQueue();
306
+ /**
307
+ * Controllers used to signal that the client should stop sending event iterator messages
308
+ */
255
309
  serverControllers = /* @__PURE__ */ new Map();
310
+ /**
311
+ * Cleanup functions invoked when the request/response is closed
312
+ */
313
+ cleanupFns = /* @__PURE__ */ new Map();
256
314
  send;
257
315
  constructor(send) {
258
316
  this.send = async (id, ...rest) => encodeRequestMessage(id, ...rest).then(async (raw) => {
@@ -262,48 +320,85 @@ class ClientPeer {
262
320
  });
263
321
  }
264
322
  get length() {
265
- 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;
266
324
  }
267
325
  open(id) {
268
326
  this.serverEventIteratorQueue.open(id);
269
327
  this.responseQueue.open(id);
270
328
  const controller = new AbortController();
271
329
  this.serverControllers.set(id, controller);
330
+ this.cleanupFns.set(id, []);
272
331
  return controller;
273
332
  }
274
333
  async request(request) {
275
334
  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 () => {
335
+ return runWithSpan(
336
+ { name: "send_peer_request", signal },
337
+ async () => {
283
338
  if (signal?.aborted) {
284
- await this.send(id, MessageType.ABORT_SIGNAL, void 0);
285
- this.close({ id, reason: signal.reason });
286
- return;
339
+ throw signal.reason;
287
340
  }
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";
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);
299
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;
300
399
  }
301
- }).catch((err) => {
302
- this.close({ id, reason: err });
303
- reject(err);
304
- });
305
- this.responseQueue.pull(id).then(resolve).catch(reject);
306
- });
400
+ }
401
+ );
307
402
  }
308
403
  async message(raw) {
309
404
  const [id, type, payload] = await decodeResponseMessage(raw);
@@ -320,31 +415,19 @@ class ClientPeer {
320
415
  if (!this.responseQueue.isOpen(id)) {
321
416
  return;
322
417
  }
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
- }
418
+ this.responseQueue.push(id, payload);
340
419
  }
341
420
  close(options = {}) {
342
421
  if (options.id !== void 0) {
343
422
  this.serverControllers.get(options.id)?.abort(options.reason);
344
423
  this.serverControllers.delete(options.id);
424
+ this.cleanupFns.get(options.id)?.forEach((fn) => fn());
425
+ this.cleanupFns.delete(options.id);
345
426
  } else {
346
427
  this.serverControllers.forEach((c) => c.abort(options.reason));
347
428
  this.serverControllers.clear();
429
+ this.cleanupFns.forEach((fns) => fns.forEach((fn) => fn()));
430
+ this.cleanupFns.clear();
348
431
  }
349
432
  this.responseQueue.close(options);
350
433
  this.serverEventIteratorQueue.close(options);
@@ -352,7 +435,13 @@ class ClientPeer {
352
435
  }
353
436
 
354
437
  class ServerPeer {
438
+ /**
439
+ * Queue of event iterator messages sent from client, awaiting consumption
440
+ */
355
441
  clientEventIteratorQueue = new AsyncIdQueue();
442
+ /**
443
+ * Map of active client request controllers, should be synced to request signal
444
+ */
356
445
  clientControllers = /* @__PURE__ */ new Map();
357
446
  send;
358
447
  constructor(send) {
@@ -371,10 +460,13 @@ class ServerPeer {
371
460
  this.clientControllers.set(id, controller);
372
461
  return controller;
373
462
  }
374
- async message(raw) {
463
+ /**
464
+ * @todo This method will return Promise<void> in the next major version.
465
+ */
466
+ async message(raw, handleRequest) {
375
467
  const [id, type, payload] = await decodeRequestMessage(raw);
376
468
  if (type === MessageType.ABORT_SIGNAL) {
377
- this.close({ id });
469
+ this.close({ id, reason: new AbortError("Client aborted the request") });
378
470
  return [id, void 0];
379
471
  }
380
472
  if (type === MessageType.EVENT_ITERATOR) {
@@ -384,28 +476,66 @@ class ServerPeer {
384
476
  return [id, void 0];
385
477
  }
386
478
  const clientController = this.open(id);
479
+ const signal = clientController.signal;
387
480
  const request = {
388
481
  ...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
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
395
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
+ }
396
521
  return [id, request];
397
522
  }
523
+ /**
524
+ * @deprecated Please pass the `handleRequest` (second arg) function to the `message` method instead.
525
+ */
398
526
  async response(id, response) {
399
527
  const signal = this.clientControllers.get(id)?.signal;
400
528
  if (!signal || signal.aborted) {
401
529
  return;
402
530
  }
403
- await this.send(id, MessageType.RESPONSE, response).then(async () => {
531
+ try {
532
+ await this.send(id, MessageType.RESPONSE, response);
404
533
  if (!signal.aborted && isAsyncIteratorObject(response.body)) {
405
- if (response.body instanceof experimental_HibernationEventIterator) {
534
+ if (response.body instanceof HibernationEventIterator) {
406
535
  response.body.hibernationCallback?.(id);
407
536
  } else {
408
- await resolveEventIterator(response.body, async (payload) => {
537
+ const iterator = response.body;
538
+ await resolveEventIterator(iterator, async (payload) => {
409
539
  if (signal.aborted) {
410
540
  return "abort";
411
541
  }
@@ -415,20 +545,20 @@ class ServerPeer {
415
545
  }
416
546
  }
417
547
  this.close({ id, abort: false });
418
- }).catch((reason) => {
548
+ } catch (reason) {
419
549
  this.close({ id, reason, abort: false });
420
550
  throw reason;
421
- });
551
+ }
422
552
  }
423
553
  close({ abort = true, ...options } = {}) {
424
554
  if (options.id === void 0) {
425
555
  if (abort) {
426
- this.clientControllers.forEach((c) => c.abort());
556
+ this.clientControllers.forEach((c) => c.abort(options.reason));
427
557
  }
428
558
  this.clientControllers.clear();
429
559
  } else {
430
560
  if (abort) {
431
- this.clientControllers.get(options.id)?.abort();
561
+ this.clientControllers.get(options.id)?.abort(options.reason);
432
562
  }
433
563
  this.clientControllers.delete(options.id);
434
564
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@orpc/standard-server-peer",
3
3
  "type": "module",
4
- "version": "1.7.10",
4
+ "version": "1.8.0",
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": "1.7.10",
27
- "@orpc/standard-server": "1.7.10"
26
+ "@orpc/shared": "1.8.0",
27
+ "@orpc/standard-server": "1.8.0"
28
28
  },
29
29
  "scripts": {
30
30
  "build": "unbuild",