@orpc/server 2.0.0-beta.49 → 2.0.0-beta.50
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/dist/adapters/aws-lambda/handler.d.ts +17 -15
- package/dist/adapters/aws-lambda/handler.js +23 -23
- package/dist/adapters/aws-lambda/index.d.ts +4 -16
- package/dist/adapters/aws-lambda/index.js +2 -2
- package/dist/adapters/aws-lambda/rpc-handler.d.ts +9 -9
- package/dist/adapters/aws-lambda/rpc-handler.js +20 -17
- package/dist/adapters/crossws/handler.d.ts +44 -42
- package/dist/adapters/crossws/handler.js +53 -60
- package/dist/adapters/crossws/index.d.ts +3 -2
- package/dist/adapters/crossws/index.js +2 -2
- package/dist/adapters/crossws/rpc-handler.d.ts +10 -10
- package/dist/adapters/crossws/rpc-handler.js +17 -14
- package/dist/adapters/fastify/handler.d.ts +17 -15
- package/dist/adapters/fastify/handler.js +20 -20
- package/dist/adapters/fastify/index.d.ts +3 -2
- package/dist/adapters/fastify/index.js +2 -2
- package/dist/adapters/fastify/rpc-handler.d.ts +9 -9
- package/dist/adapters/fastify/rpc-handler.js +16 -13
- package/dist/adapters/fetch/handler.d.ts +19 -17
- package/dist/adapters/fetch/handler.js +22 -22
- package/dist/adapters/fetch/index.d.ts +4 -7
- package/dist/adapters/fetch/index.js +3 -7
- package/dist/adapters/fetch/rpc-handler.d.ts +9 -9
- package/dist/adapters/fetch/rpc-handler.js +18 -15
- package/dist/adapters/message-port/handler.d.ts +58 -60
- package/dist/adapters/message-port/handler.js +72 -86
- package/dist/adapters/message-port/index.d.ts +3 -2
- package/dist/adapters/message-port/index.js +2 -2
- package/dist/adapters/message-port/rpc-handler.d.ts +9 -9
- package/dist/adapters/message-port/rpc-handler.js +17 -14
- package/dist/adapters/node/handler.d.ts +17 -15
- package/dist/adapters/node/handler.js +20 -20
- package/dist/adapters/node/index.d.ts +4 -7
- package/dist/adapters/node/index.js +3 -7
- package/dist/adapters/node/rpc-handler.d.ts +9 -9
- package/dist/adapters/node/rpc-handler.js +17 -14
- package/dist/adapters/standard/codec.d.ts +13 -13
- package/dist/adapters/standard/handler.d.ts +49 -49
- package/dist/adapters/standard/handler.js +205 -205
- package/dist/adapters/standard/index.d.ts +7 -6
- package/dist/adapters/standard/index.js +4 -5
- package/dist/adapters/standard/plugin.d.ts +25 -31
- package/dist/adapters/standard/plugin.js +14 -16
- package/dist/adapters/standard/rpc-handler-codec.d.ts +39 -43
- package/dist/adapters/standard/rpc-handler-codec.js +50 -51
- package/dist/adapters/standard/rpc-matcher.d.ts +40 -40
- package/dist/adapters/standard/rpc-matcher.js +103 -149
- package/dist/adapters/standard/utils.d.ts +8 -8
- package/dist/adapters/standard/utils.js +7 -5
- package/dist/adapters/standard-peer/index.d.ts +2 -1
- package/dist/adapters/standard-peer/index.js +1 -1
- package/dist/adapters/standard-peer/utils.d.ts +6 -4
- package/dist/adapters/standard-peer/utils.js +12 -6
- package/dist/adapters/websocket/handler.d.ts +52 -50
- package/dist/adapters/websocket/handler.js +75 -85
- package/dist/adapters/websocket/index.d.ts +3 -2
- package/dist/adapters/websocket/index.js +2 -2
- package/dist/adapters/websocket/rpc-handler.d.ts +9 -9
- package/dist/adapters/websocket/rpc-handler.js +17 -14
- package/dist/builder-variants.d.ts +195 -195
- package/dist/builder.d.ts +103 -104
- package/dist/builder.js +198 -224
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +4 -2
- package/dist/context.d.ts +3 -3
- package/dist/extensions/callable.d.ts +11 -11
- package/dist/extensions/callable.js +6 -5
- package/dist/helpers/base64url.d.ts +1 -1
- package/dist/helpers/base64url.js +44 -49
- package/dist/helpers/cookie.d.ts +10 -11
- package/dist/helpers/cookie.js +56 -63
- package/dist/helpers/encryption.d.ts +1 -1
- package/dist/helpers/encryption.js +80 -70
- package/dist/helpers/index.d.ts +5 -4
- package/dist/helpers/index.js +4 -4
- package/dist/helpers/signing.d.ts +1 -1
- package/dist/helpers/signing.js +72 -82
- package/dist/implementer-procedure.d.ts +12 -13
- package/dist/implementer-procedure.js +37 -44
- package/dist/implementer-router.d.ts +30 -36
- package/dist/implementer-router.js +92 -111
- package/dist/implementer.d.ts +8 -8
- package/dist/implementer.js +28 -35
- package/dist/index.d.ts +22 -61
- package/dist/index.js +16 -48
- package/dist/lazy.d.ts +14 -14
- package/dist/lazy.js +19 -30
- package/dist/middleware-decorated.d.ts +8 -9
- package/dist/middleware-decorated.js +66 -70
- package/dist/middleware.d.ts +34 -34
- package/dist/plugins/batch.d.ts +75 -75
- package/dist/plugins/batch.js +345 -379
- package/dist/plugins/cors.d.ts +60 -64
- package/dist/plugins/cors.js +118 -111
- package/dist/plugins/get-method-csrf-protection.d.ts +8 -6
- package/dist/plugins/get-method-csrf-protection.js +47 -63
- package/dist/plugins/head-method.d.ts +11 -9
- package/dist/plugins/head-method.js +97 -94
- package/dist/plugins/index.d.ts +15 -39
- package/dist/plugins/index.js +14 -29
- package/dist/plugins/method-override.d.ts +30 -28
- package/dist/plugins/method-override.js +53 -49
- package/dist/plugins/request-compression.d.ts +11 -9
- package/dist/plugins/request-compression.js +52 -79
- package/dist/plugins/request-headers.d.ts +10 -8
- package/dist/plugins/request-headers.js +27 -29
- package/dist/plugins/request-limit.d.ts +21 -19
- package/dist/plugins/request-limit.js +60 -67
- package/dist/plugins/response-compression.d.ts +42 -39
- package/dist/plugins/response-compression.js +150 -202
- package/dist/plugins/response-headers.d.ts +15 -13
- package/dist/plugins/response-headers.js +43 -46
- package/dist/plugins/rethrow.d.ts +25 -23
- package/dist/plugins/rethrow.js +46 -58
- package/dist/plugins/signal-sync.d.ts +7 -5
- package/dist/plugins/signal-sync.js +71 -86
- package/dist/plugins/timeout.d.ts +29 -27
- package/dist/plugins/timeout.js +61 -58
- package/dist/procedure-client.d.ts +14 -15
- package/dist/procedure-client.js +149 -166
- package/dist/procedure-decorated.d.ts +9 -9
- package/dist/procedure-decorated.js +36 -44
- package/dist/procedure-utils.d.ts +10 -30
- package/dist/procedure-utils.js +39 -40
- package/dist/procedure.d.ts +60 -60
- package/dist/procedure.js +18 -24
- package/dist/router-client.d.ts +9 -11
- package/dist/router-client.js +33 -41
- package/dist/router-hidden.js +10 -12
- package/dist/router-utils.d.ts +13 -30
- package/dist/router-utils.js +146 -191
- package/dist/router.d.ts +12 -26
- package/package.json +10 -10
- package/dist/adapters/standard/codec.js +0 -1
- package/dist/builder-variants.js +0 -1
- package/dist/context.js +0 -1
- package/dist/middleware.js +0 -1
- package/dist/router-hidden.d.ts +0 -5
- package/dist/router.js +0 -1
package/dist/plugins/batch.js
CHANGED
|
@@ -1,405 +1,371 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { encodePeerMessage, isClientPeerSendMessage, isPeerMessage
|
|
1
|
+
import { isAsyncIteratorObject, listenAbort, promiseWithResolvers, toArray, value } from "@orpc/shared";
|
|
2
|
+
import { ORPCError } from "@orpc/client";
|
|
3
|
+
import { flattenStandardHeader, parseStandardUrl } from "@standard-server/core";
|
|
4
|
+
import { BATCH_HEADER, DEFAULT_BATCH_MAX_SIZE } from "@orpc/client/plugins";
|
|
5
|
+
import { ServerPeer, encodePeerMessage, isClientPeerSendMessage, isPeerMessage } from "@standard-server/peer";
|
|
6
|
+
// src/plugins/batch.ts
|
|
6
7
|
/**
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
* Content type for batch responses that use the length-prefixed binary framing,
|
|
9
|
+
* which they do in `streaming` mode, or when any sub-response contains binary or is a stream.
|
|
10
|
+
*
|
|
11
|
+
* Decoding is driven by the `standard-server` body hint, not this header,
|
|
12
|
+
* so it only serves to describe the payload to logs, proxies, and dev tools.
|
|
13
|
+
*/
|
|
14
|
+
const BATCH_CONTENT_TYPE = "application/vnd.orpc.batch";
|
|
14
15
|
const textEncoder = /* @__PURE__ */ new TextEncoder();
|
|
15
16
|
function encodeLengthPrefix(length) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
const prefix = /* @__PURE__ */ new Uint8Array(4);
|
|
18
|
+
new DataView(prefix.buffer).setUint32(0, length, false);
|
|
19
|
+
return prefix;
|
|
19
20
|
}
|
|
20
21
|
/**
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
* Encodes a message as a length-prefixed frame: its 4-byte big-endian length, then its bytes.
|
|
23
|
+
*/
|
|
23
24
|
async function encodeFrame(message) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
const encoded = await encodePeerMessage(message);
|
|
26
|
+
const bytes = typeof encoded === "string" ? textEncoder.encode(encoded) : encoded;
|
|
27
|
+
return [encodeLengthPrefix(bytes.byteLength), bytes];
|
|
27
28
|
}
|
|
28
29
|
/**
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
* Whether a header describes a subresponse's own body, which the batch response does not share.
|
|
31
|
+
*/
|
|
31
32
|
function isBodyHeader(name) {
|
|
32
|
-
|
|
33
|
+
return name.startsWith("content-") || name === "standard-server";
|
|
33
34
|
}
|
|
34
35
|
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
* Whether untrusted batch JSON is a list of client peer messages.
|
|
37
|
+
* `isPeerMessage` must run first, since `isClientPeerSendMessage` does not check `id` or `binary`.
|
|
38
|
+
*/
|
|
38
39
|
function isClientPeerSendMessages(maybe) {
|
|
39
|
-
|
|
40
|
-
maybe.every((m) => isPeerMessage(m) && isClientPeerSendMessage(m)));
|
|
40
|
+
return Array.isArray(maybe) && maybe.every((m) => isPeerMessage(m) && isClientPeerSendMessage(m));
|
|
41
41
|
}
|
|
42
42
|
/**
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
* Errors should not occur at the routing interceptor level.
|
|
44
|
+
* Reject the promise so it can be handled by the unhandledRejection handler
|
|
45
|
+
* for global logging or error handling.
|
|
46
|
+
*/
|
|
47
47
|
function internalError(err) {
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
Promise.reject(err);
|
|
49
|
+
return {
|
|
50
|
+
status: 500,
|
|
51
|
+
headers: {},
|
|
52
|
+
body: "Internal server error"
|
|
53
|
+
};
|
|
50
54
|
}
|
|
51
55
|
/**
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
* Resolves with the first subresponse head, or `undefined` if none has one.
|
|
57
|
+
*/
|
|
54
58
|
function firstSubresponseHead(heads) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
void Promise.all(heads).then(() => resolve(undefined));
|
|
60
|
-
});
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
for (const promise of heads) promise.then((head) => head && resolve(head));
|
|
61
|
+
Promise.all(heads).then(() => resolve(void 0));
|
|
62
|
+
});
|
|
61
63
|
}
|
|
62
64
|
/**
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
* Sets the distinct cookies of every source on the headers.
|
|
66
|
+
*/
|
|
65
67
|
function liftCookies(headers, sources) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
const cookies = new Set(sources.flatMap((source) => toArray(source["set-cookie"])));
|
|
69
|
+
return cookies.size > 0 ? {
|
|
70
|
+
...headers,
|
|
71
|
+
"set-cookie": [...cookies]
|
|
72
|
+
} : headers;
|
|
68
73
|
}
|
|
69
74
|
/**
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
* Builds the batch response headers: the headers the subresponses all have with the same value, as
|
|
76
|
+
* the client does with the headers every subrequest shares, then the batch headers, which win, and
|
|
77
|
+
* the distinct cookies and `vary` fields of both. A cache must vary on whatever any subresponse varies on.
|
|
78
|
+
*/
|
|
74
79
|
function liftSubresponseHeaders(batchHeaders, subresponseHeaders) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
if (varies.size > 0) {
|
|
98
|
-
headers.vary = varies.has('*') ? '*' : [...varies.values()].join(', ');
|
|
99
|
-
}
|
|
100
|
-
return headers;
|
|
80
|
+
const [first = {}, ...rest] = subresponseHeaders;
|
|
81
|
+
const shared = {};
|
|
82
|
+
for (const [name, value] of Object.entries(first)) {
|
|
83
|
+
if (value === void 0 || isBodyHeader(name)) continue;
|
|
84
|
+
const flattened = flattenStandardHeader(value);
|
|
85
|
+
if (rest.every((other) => flattenStandardHeader(other[name]) === flattened)) shared[name] = value;
|
|
86
|
+
}
|
|
87
|
+
const headers = liftCookies({
|
|
88
|
+
...shared,
|
|
89
|
+
...batchHeaders
|
|
90
|
+
}, [batchHeaders, ...subresponseHeaders]);
|
|
91
|
+
const varies = /* @__PURE__ */ new Map();
|
|
92
|
+
for (const value of [batchHeaders, ...subresponseHeaders].flatMap((other) => toArray(other.vary))) for (const field of value.split(",")) {
|
|
93
|
+
const name = field.trim();
|
|
94
|
+
const key = name.toLowerCase();
|
|
95
|
+
if (name && !varies.has(key)) varies.set(key, name);
|
|
96
|
+
}
|
|
97
|
+
if (varies.size > 0) headers.vary = varies.has("*") ? "*" : [...varies.values()].join(", ");
|
|
98
|
+
return headers;
|
|
101
99
|
}
|
|
102
100
|
/**
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
clearKeepAlive();
|
|
375
|
-
await peer.close(reason);
|
|
376
|
-
},
|
|
377
|
-
});
|
|
378
|
-
const flushed = Promise.all(bufferedMessages.splice(0).map(enqueueMessage));
|
|
379
|
-
// DO NOT await here to block streaming response
|
|
380
|
-
Promise.all([flushed, subrequests])
|
|
381
|
-
.then(async () => {
|
|
382
|
-
clearKeepAlive();
|
|
383
|
-
streamController.close();
|
|
384
|
-
await peer.close();
|
|
385
|
-
})
|
|
386
|
-
.catch(async (error) => {
|
|
387
|
-
clearKeepAlive();
|
|
388
|
-
streamController.error(error);
|
|
389
|
-
await peer.close(error);
|
|
390
|
-
});
|
|
391
|
-
return {
|
|
392
|
-
matched: true,
|
|
393
|
-
response: {
|
|
394
|
-
status,
|
|
395
|
-
headers: framedHeaders,
|
|
396
|
-
body,
|
|
397
|
-
},
|
|
398
|
-
};
|
|
399
|
-
};
|
|
400
|
-
return {
|
|
401
|
-
...options,
|
|
402
|
-
routingInterceptors: [routingInterceptor, ...toArray(options.routingInterceptors)],
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
}
|
|
101
|
+
* Handles batch requests sent by the client Batch Link Plugin, splitting each
|
|
102
|
+
* batch into sub-requests and sending their responses back together.
|
|
103
|
+
*
|
|
104
|
+
* @remarks
|
|
105
|
+
* The client Batch Link Plugin's `mode` picks how each batch responds: `buffered` (default) waits for every
|
|
106
|
+
* sub-response and carries their cookies and shared headers; `streaming` starts once the first is ready and
|
|
107
|
+
* carries only its cookies.
|
|
108
|
+
*
|
|
109
|
+
* **Note**: HTTP/2 and later already multiplex requests over a single connection, which often makes this plugin unnecessary.
|
|
110
|
+
*
|
|
111
|
+
* @see {@link https://orpc.dev/docs/plugins/batch | Batch Plugin}
|
|
112
|
+
*/
|
|
113
|
+
var BatchHandlerPlugin = class {
|
|
114
|
+
name = "~batch";
|
|
115
|
+
/**
|
|
116
|
+
* Run batch interceptors inside the tracing interceptors
|
|
117
|
+
* so each subrequest gets its own span instead of sharing one batch-level span.
|
|
118
|
+
*/
|
|
119
|
+
after = ["~tracing"];
|
|
120
|
+
maxSize;
|
|
121
|
+
mapSubrequest;
|
|
122
|
+
mapSubresponse;
|
|
123
|
+
successStatus;
|
|
124
|
+
headers;
|
|
125
|
+
keepAliveEnabled;
|
|
126
|
+
keepAliveInterval;
|
|
127
|
+
constructor(options = {}) {
|
|
128
|
+
this.maxSize = options.maxSize ?? DEFAULT_BATCH_MAX_SIZE;
|
|
129
|
+
this.mapSubrequest = options.mapSubrequest ?? ((subRequest, { request: batchRequest }) => ({
|
|
130
|
+
...subRequest,
|
|
131
|
+
headers: {
|
|
132
|
+
...subRequest.headers,
|
|
133
|
+
/**
|
|
134
|
+
* The batch request headers win over the subrequest ones. They are the only ones the
|
|
135
|
+
* transport actually saw, so headers the browser injects on its own, such as `cookie`
|
|
136
|
+
* or `origin`, cannot be overridden by a subrequest, which is just request payload.
|
|
137
|
+
*/
|
|
138
|
+
...batchRequest.headers,
|
|
139
|
+
[BATCH_HEADER]: void 0
|
|
140
|
+
}
|
|
141
|
+
}));
|
|
142
|
+
this.mapSubresponse = options.mapSubresponse ?? ((subresponse, { headers: batchHeaders }) => {
|
|
143
|
+
const headers = {};
|
|
144
|
+
for (const [name, value] of Object.entries(subresponse.headers)) if (name !== "set-cookie" && flattenStandardHeader(value) !== flattenStandardHeader(batchHeaders[name])) headers[name] = value;
|
|
145
|
+
return {
|
|
146
|
+
...subresponse,
|
|
147
|
+
headers
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
this.successStatus = options.successStatus ?? 207;
|
|
151
|
+
this.headers = options.headers ?? {};
|
|
152
|
+
this.keepAliveEnabled = options.keepAlive !== false;
|
|
153
|
+
this.keepAliveInterval = (typeof options.keepAlive === "object" ? options.keepAlive.interval : void 0) ?? 15e3;
|
|
154
|
+
}
|
|
155
|
+
init(options) {
|
|
156
|
+
const routingInterceptor = async (interceptorOptions) => {
|
|
157
|
+
const batchHeader = flattenStandardHeader(interceptorOptions.request.headers[BATCH_HEADER]);
|
|
158
|
+
if (batchHeader === void 0) return interceptorOptions.next();
|
|
159
|
+
const streaming = batchHeader === "streaming";
|
|
160
|
+
let messages;
|
|
161
|
+
try {
|
|
162
|
+
if (interceptorOptions.request.method === "GET") {
|
|
163
|
+
const [, search] = parseStandardUrl(interceptorOptions.request.url);
|
|
164
|
+
const data = new URLSearchParams(search).getAll("data").at(-1);
|
|
165
|
+
if (!data) return {
|
|
166
|
+
matched: true,
|
|
167
|
+
response: {
|
|
168
|
+
status: 400,
|
|
169
|
+
headers: {},
|
|
170
|
+
body: "Missing data parameter for batch request"
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
const mightBeMessages = JSON.parse(data);
|
|
174
|
+
if (!isClientPeerSendMessages(mightBeMessages)) return {
|
|
175
|
+
matched: true,
|
|
176
|
+
response: {
|
|
177
|
+
status: 400,
|
|
178
|
+
headers: {},
|
|
179
|
+
body: "Invalid batch request data parameter"
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
messages = mightBeMessages;
|
|
183
|
+
} else {
|
|
184
|
+
const mightBeMessages = await interceptorOptions.request.resolveBody();
|
|
185
|
+
if (!isClientPeerSendMessages(mightBeMessages)) return {
|
|
186
|
+
matched: true,
|
|
187
|
+
response: {
|
|
188
|
+
status: 400,
|
|
189
|
+
headers: {},
|
|
190
|
+
body: "Invalid batch request body"
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
messages = mightBeMessages;
|
|
194
|
+
}
|
|
195
|
+
} catch (error) {
|
|
196
|
+
return {
|
|
197
|
+
matched: true,
|
|
198
|
+
response: {
|
|
199
|
+
status: 400,
|
|
200
|
+
headers: {},
|
|
201
|
+
body: error instanceof ORPCError ? error.message : "Invalid batch request"
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const outerMethod = interceptorOptions.request.method;
|
|
206
|
+
if ((outerMethod === "GET" || outerMethod === "QUERY") && messages.some((message) => message.kind === "request" && message.json.method !== outerMethod)) return {
|
|
207
|
+
matched: true,
|
|
208
|
+
response: {
|
|
209
|
+
status: 400,
|
|
210
|
+
headers: {},
|
|
211
|
+
body: `${outerMethod} batch requests only accept ${outerMethod} sub-requests`
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
const maxSize = await value(this.maxSize, interceptorOptions);
|
|
215
|
+
if (messages.length > maxSize) return {
|
|
216
|
+
matched: true,
|
|
217
|
+
response: {
|
|
218
|
+
status: 413,
|
|
219
|
+
headers: {},
|
|
220
|
+
body: "Batch request size exceeds the maximum allowed size"
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
const status = await value(this.successStatus, interceptorOptions);
|
|
224
|
+
const headers = await value(this.headers, interceptorOptions);
|
|
225
|
+
const bufferedMessages = [];
|
|
226
|
+
let streamController;
|
|
227
|
+
let keepAliveTimer;
|
|
228
|
+
const ready = messages.map(() => promiseWithResolvers());
|
|
229
|
+
const heads = ready.map(({ promise }) => promise);
|
|
230
|
+
const lifting = streaming ? firstSubresponseHead(heads).then((head) => ({
|
|
231
|
+
batchHeaders: liftCookies(headers, [headers, head?.headers ?? {}]),
|
|
232
|
+
streams: true
|
|
233
|
+
})) : Promise.all(heads).then((all) => {
|
|
234
|
+
const present = all.filter((head) => head !== void 0);
|
|
235
|
+
return {
|
|
236
|
+
batchHeaders: liftSubresponseHeaders(headers, present.map((head) => head.headers)),
|
|
237
|
+
streams: present.some((head) => head.stream)
|
|
238
|
+
};
|
|
239
|
+
});
|
|
240
|
+
const clearKeepAlive = () => {
|
|
241
|
+
if (keepAliveTimer !== void 0) {
|
|
242
|
+
clearInterval(keepAliveTimer);
|
|
243
|
+
keepAliveTimer = void 0;
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
const scheduleKeepAlive = () => {
|
|
247
|
+
if (!this.keepAliveEnabled) return;
|
|
248
|
+
clearKeepAlive();
|
|
249
|
+
keepAliveTimer = setInterval(() => {
|
|
250
|
+
try {
|
|
251
|
+
streamController.enqueue(encodeLengthPrefix(0));
|
|
252
|
+
} catch {
|
|
253
|
+
clearKeepAlive();
|
|
254
|
+
}
|
|
255
|
+
}, this.keepAliveInterval);
|
|
256
|
+
};
|
|
257
|
+
const enqueueMessage = async (message) => {
|
|
258
|
+
for (const chunk of await encodeFrame(message)) streamController.enqueue(chunk);
|
|
259
|
+
scheduleKeepAlive();
|
|
260
|
+
};
|
|
261
|
+
const peer = new ServerPeer(async (message) => {
|
|
262
|
+
if (streamController === void 0) bufferedMessages.push(message);
|
|
263
|
+
else await enqueueMessage(message);
|
|
264
|
+
});
|
|
265
|
+
const handleIndividualRequest = async (request, markReady) => {
|
|
266
|
+
let response;
|
|
267
|
+
try {
|
|
268
|
+
request = this.mapSubrequest(request, interceptorOptions);
|
|
269
|
+
const result = await interceptorOptions.next({
|
|
270
|
+
...interceptorOptions,
|
|
271
|
+
request
|
|
272
|
+
});
|
|
273
|
+
response = result.matched ? result.response : {
|
|
274
|
+
status: 404,
|
|
275
|
+
headers: {},
|
|
276
|
+
body: "No procedure matched"
|
|
277
|
+
};
|
|
278
|
+
} catch (err) {
|
|
279
|
+
response = internalError(err);
|
|
280
|
+
}
|
|
281
|
+
markReady({
|
|
282
|
+
headers: response.headers,
|
|
283
|
+
stream: response.body instanceof ReadableStream || isAsyncIteratorObject(response.body)
|
|
284
|
+
});
|
|
285
|
+
const { batchHeaders } = await lifting;
|
|
286
|
+
try {
|
|
287
|
+
return this.mapSubresponse(response, {
|
|
288
|
+
status,
|
|
289
|
+
headers: batchHeaders
|
|
290
|
+
}, interceptorOptions);
|
|
291
|
+
} catch (err) {
|
|
292
|
+
return internalError(err);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
const runSubrequests = async () => {
|
|
296
|
+
const promise = Promise.all(messages.map((msg, i) => {
|
|
297
|
+
const { resolve } = ready[i];
|
|
298
|
+
return peer.message(msg, (request) => handleIndividualRequest(request, resolve)).finally(() => resolve(void 0));
|
|
299
|
+
}));
|
|
300
|
+
const unlisten = listenAbort(interceptorOptions.request.signal, (reason) => void peer.close(reason));
|
|
301
|
+
try {
|
|
302
|
+
await promise;
|
|
303
|
+
} finally {
|
|
304
|
+
unlisten();
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
const subrequests = runSubrequests();
|
|
308
|
+
const { batchHeaders, streams } = await lifting;
|
|
309
|
+
const framedHeaders = {
|
|
310
|
+
...batchHeaders,
|
|
311
|
+
"content-type": batchHeaders["content-type"] ?? "application/vnd.orpc.batch"
|
|
312
|
+
};
|
|
313
|
+
if (!streams) {
|
|
314
|
+
await subrequests;
|
|
315
|
+
await peer.close();
|
|
316
|
+
if (bufferedMessages.some((msg) => msg.binary !== void 0)) {
|
|
317
|
+
const chunks = (await Promise.all(bufferedMessages.map(encodeFrame))).flat();
|
|
318
|
+
return {
|
|
319
|
+
matched: true,
|
|
320
|
+
response: {
|
|
321
|
+
status,
|
|
322
|
+
headers: framedHeaders,
|
|
323
|
+
body: new Blob(chunks)
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
matched: true,
|
|
329
|
+
response: {
|
|
330
|
+
status,
|
|
331
|
+
headers: batchHeaders,
|
|
332
|
+
body: bufferedMessages
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
const body = new ReadableStream({
|
|
337
|
+
start(controller) {
|
|
338
|
+
streamController = controller;
|
|
339
|
+
scheduleKeepAlive();
|
|
340
|
+
},
|
|
341
|
+
async cancel(reason) {
|
|
342
|
+
clearKeepAlive();
|
|
343
|
+
await peer.close(reason);
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
const flushed = Promise.all(bufferedMessages.splice(0).map(enqueueMessage));
|
|
347
|
+
Promise.all([flushed, subrequests]).then(async () => {
|
|
348
|
+
clearKeepAlive();
|
|
349
|
+
streamController.close();
|
|
350
|
+
await peer.close();
|
|
351
|
+
}).catch(async (error) => {
|
|
352
|
+
clearKeepAlive();
|
|
353
|
+
streamController.error(error);
|
|
354
|
+
await peer.close(error);
|
|
355
|
+
});
|
|
356
|
+
return {
|
|
357
|
+
matched: true,
|
|
358
|
+
response: {
|
|
359
|
+
status,
|
|
360
|
+
headers: framedHeaders,
|
|
361
|
+
body
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
};
|
|
365
|
+
return {
|
|
366
|
+
...options,
|
|
367
|
+
routingInterceptors: [routingInterceptor, ...toArray(options.routingInterceptors)]
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
export { BATCH_CONTENT_TYPE, BatchHandlerPlugin };
|