@orpc/standard-server-peer 0.0.0-next.de7ca70 → 0.0.0-next.df70448
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 +7 -2
- package/dist/index.d.mts +38 -6
- package/dist/index.d.ts +38 -6
- package/dist/index.mjs +256 -119
- package/package.json +3 -3
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
|
-
-
|
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
|
70
|
-
private readonly
|
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
|
-
|
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
|
-
|
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
|
70
|
-
private readonly
|
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
|
-
|
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
|
-
|
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,
|
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(
|
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 */, {
|
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
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
data =
|
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
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
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
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
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
|
-
|
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
|
-
|
231
|
-
|
232
|
-
return;
|
231
|
+
if (reason !== "next") {
|
232
|
+
span?.addEvent("cancelled");
|
233
233
|
}
|
234
|
-
|
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
|
237
|
-
|
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) /
|
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
|
-
|
277
|
-
|
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
|
-
|
285
|
-
this.close({ id, reason: signal.reason });
|
286
|
-
return;
|
346
|
+
throw signal.reason;
|
287
347
|
}
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
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
|
-
}
|
302
|
-
|
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
|
-
|
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
|
-
|
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
|
390
|
-
body: isEventIteratorHeaders(payload.headers) ? toEventIterator(
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
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
|
-
|
538
|
+
try {
|
539
|
+
await this.send(id, MessageType.RESPONSE, response);
|
404
540
|
if (!signal.aborted && isAsyncIteratorObject(response.body)) {
|
405
|
-
if (response.body instanceof
|
541
|
+
if (response.body instanceof HibernationEventIterator) {
|
406
542
|
response.body.hibernationCallback?.(id);
|
407
543
|
} else {
|
408
|
-
|
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
|
-
}
|
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.
|
4
|
+
"version": "0.0.0-next.df70448",
|
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/
|
27
|
-
"@orpc/
|
26
|
+
"@orpc/shared": "0.0.0-next.df70448",
|
27
|
+
"@orpc/standard-server": "0.0.0-next.df70448"
|
28
28
|
},
|
29
29
|
"scripts": {
|
30
30
|
"build": "unbuild",
|