@orpc/client 1.14.6 → 2.0.0-beta.1
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 +74 -106
- package/dist/adapters/fetch/index.d.mts +55 -36
- package/dist/adapters/fetch/index.d.ts +55 -36
- package/dist/adapters/fetch/index.mjs +50 -28
- package/dist/adapters/message-port/index.d.mts +25 -30
- package/dist/adapters/message-port/index.d.ts +25 -30
- package/dist/adapters/message-port/index.mjs +33 -29
- package/dist/adapters/standard/index.d.mts +58 -9
- package/dist/adapters/standard/index.d.ts +58 -9
- package/dist/adapters/standard/index.mjs +5 -5
- package/dist/adapters/websocket/index.d.mts +113 -21
- package/dist/adapters/websocket/index.d.ts +113 -21
- package/dist/adapters/websocket/index.mjs +95 -37
- package/dist/index.d.mts +84 -159
- package/dist/index.d.ts +84 -159
- package/dist/index.mjs +76 -34
- package/dist/plugins/index.d.mts +125 -132
- package/dist/plugins/index.d.ts +125 -132
- package/dist/plugins/index.mjs +373 -284
- package/dist/shared/client.8ug8I-zu.d.mts +167 -0
- package/dist/shared/client.8ug8I-zu.d.ts +167 -0
- package/dist/shared/client.BdItY5DT.d.mts +111 -0
- package/dist/shared/client.BdItY5DT.d.ts +111 -0
- package/dist/shared/client.CPF3hX6O.d.ts +96 -0
- package/dist/shared/client.Cby_-GGh.d.mts +96 -0
- package/dist/shared/client.DMXKFDyV.mjs +343 -0
- package/dist/shared/client.DXhchJ84.mjs +174 -0
- package/dist/shared/client.Dnfj8jnT.mjs +92 -0
- package/package.json +7 -7
- package/dist/shared/client.2jUAqzYU.d.ts +0 -45
- package/dist/shared/client.B3pNRBih.d.ts +0 -91
- package/dist/shared/client.BFAVy68H.d.mts +0 -91
- package/dist/shared/client.BLtwTQUg.mjs +0 -40
- package/dist/shared/client.CpCa3si8.d.mts +0 -45
- package/dist/shared/client.D9eWXdBV.mjs +0 -174
- package/dist/shared/client.DLhbktiD.mjs +0 -404
- package/dist/shared/client.i2uoJbEp.d.mts +0 -83
- package/dist/shared/client.i2uoJbEp.d.ts +0 -83
package/dist/plugins/index.mjs
CHANGED
|
@@ -1,270 +1,400 @@
|
|
|
1
|
-
import { isAsyncIteratorObject, defer,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { C as
|
|
1
|
+
import { toArray, value, splitInHalf, stringifyJSON, isAsyncIteratorObject, defer, loadBytes, allAbortSignal, replicateAsyncIterator, replicateReadableStream, override, AsyncIteratorClass, sleep } from '@orpc/shared';
|
|
2
|
+
import { parseStandardUrl, getEventMeta, flattenStandardHeader } from '@standardserver/core';
|
|
3
|
+
import { ClientPeer, isServerPeerSendMessage, decodePeerMessage } from '@standardserver/peer';
|
|
4
|
+
import { C as COMMON_ERROR_STATUS_MAP } from '../shared/client.Dnfj8jnT.mjs';
|
|
5
5
|
|
|
6
|
+
class BatchLinkPluginError extends TypeError {
|
|
7
|
+
}
|
|
6
8
|
class BatchLinkPlugin {
|
|
9
|
+
name = "~batch";
|
|
7
10
|
groups;
|
|
11
|
+
filter;
|
|
8
12
|
maxSize;
|
|
13
|
+
mode;
|
|
9
14
|
batchUrl;
|
|
10
15
|
maxUrlLength;
|
|
11
16
|
batchHeaders;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
pending;
|
|
16
|
-
order = 5e6;
|
|
17
|
+
mapSubrequest;
|
|
18
|
+
mapSubresponse;
|
|
19
|
+
queue = /* @__PURE__ */ new Map();
|
|
17
20
|
constructor(options) {
|
|
18
21
|
this.groups = options.groups;
|
|
19
|
-
this.
|
|
22
|
+
this.filter = options.filter ?? (() => true);
|
|
20
23
|
this.maxSize = options.maxSize ?? 10;
|
|
21
|
-
this.maxUrlLength = options.maxUrlLength ?? 2083;
|
|
22
24
|
this.mode = options.mode ?? "streaming";
|
|
23
|
-
this.batchUrl = options.url ?? ((
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
this.batchUrl = options.url ?? ((options2) => {
|
|
26
|
+
const [pathname] = parseStandardUrl(options2[0].request.url);
|
|
27
|
+
return `${pathname}/__batch__`;
|
|
28
|
+
});
|
|
29
|
+
this.maxUrlLength = options.maxUrlLength ?? 2083;
|
|
30
|
+
this.batchHeaders = options.headers ?? (async (options2) => {
|
|
31
|
+
const headersList = options2.map((o) => o.request.headers);
|
|
32
|
+
const commonHeaders = {};
|
|
33
|
+
for (const headers of headersList) {
|
|
34
|
+
for (const [key, value2] of Object.entries(headers)) {
|
|
35
|
+
if (headersList.every((h) => h[key] === value2)) {
|
|
36
|
+
commonHeaders[key] = value2;
|
|
37
|
+
}
|
|
29
38
|
}
|
|
30
39
|
}
|
|
31
|
-
return
|
|
40
|
+
return commonHeaders;
|
|
32
41
|
});
|
|
33
|
-
this.
|
|
34
|
-
const
|
|
35
|
-
for (const [key, value2] of Object.entries(
|
|
36
|
-
if (
|
|
37
|
-
|
|
42
|
+
this.mapSubrequest = options.mapSubrequest ?? (({ request }, { headers }) => {
|
|
43
|
+
const subHeaders = { ...request.headers };
|
|
44
|
+
for (const [key, value2] of Object.entries(headers)) {
|
|
45
|
+
if (subHeaders[key] === value2) {
|
|
46
|
+
subHeaders[key] = void 0;
|
|
38
47
|
}
|
|
39
48
|
}
|
|
40
49
|
return {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
headers,
|
|
44
|
-
body: request.body,
|
|
45
|
-
signal: request.signal
|
|
50
|
+
...request,
|
|
51
|
+
headers: subHeaders
|
|
46
52
|
};
|
|
47
53
|
});
|
|
48
|
-
this.
|
|
54
|
+
this.mapSubresponse = (subResponse, batchResponse) => {
|
|
55
|
+
return {
|
|
56
|
+
...subResponse,
|
|
57
|
+
headers: {
|
|
58
|
+
...batchResponse.headers,
|
|
59
|
+
// low-priority
|
|
60
|
+
...subResponse.headers
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
};
|
|
49
64
|
}
|
|
50
65
|
init(options) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return options2.next();
|
|
55
|
-
}
|
|
56
|
-
return options2.next({
|
|
57
|
-
...options2,
|
|
58
|
-
request: {
|
|
59
|
-
...options2.request,
|
|
60
|
-
headers: {
|
|
61
|
-
...options2.request.headers,
|
|
62
|
-
"x-orpc-batch": void 0
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
options.clientInterceptors.push((options2) => {
|
|
68
|
-
if (this.exclude(options2) || options2.request.body instanceof Blob || options2.request.body instanceof FormData || isAsyncIteratorObject(options2.request.body) || options2.request.signal?.aborted) {
|
|
69
|
-
return options2.next();
|
|
66
|
+
const transportInterceptor = async (interceptorOptions) => {
|
|
67
|
+
if (interceptorOptions.request.body instanceof Blob || interceptorOptions.request.body instanceof ReadableStream || isAsyncIteratorObject(interceptorOptions.request.body) || interceptorOptions.request.signal?.aborted || !value(this.filter, interceptorOptions)) {
|
|
68
|
+
return interceptorOptions.next();
|
|
70
69
|
}
|
|
71
|
-
const group = this.groups.find((group2) => group2.condition
|
|
70
|
+
const group = this.groups.find((group2) => value(group2.condition, interceptorOptions));
|
|
72
71
|
if (!group) {
|
|
73
|
-
return
|
|
72
|
+
return interceptorOptions.next();
|
|
74
73
|
}
|
|
75
74
|
return new Promise((resolve, reject) => {
|
|
76
|
-
this
|
|
77
|
-
|
|
75
|
+
const queue = this.queue.get(group) ?? [];
|
|
76
|
+
if (!this.queue.has(group)) {
|
|
77
|
+
this.queue.set(group, queue);
|
|
78
|
+
}
|
|
79
|
+
queue.push([interceptorOptions, resolve, reject]);
|
|
80
|
+
defer(() => this.processPendingBatches());
|
|
78
81
|
});
|
|
79
|
-
}
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
...options,
|
|
85
|
+
transportInterceptors: [...toArray(options.transportInterceptors), transportInterceptor]
|
|
86
|
+
};
|
|
80
87
|
}
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
items.push([options, resolve, reject]);
|
|
85
|
-
} else {
|
|
86
|
-
this.pending.set(group, [[options, resolve, reject]]);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
async #processPendingBatches() {
|
|
90
|
-
const pending = this.pending;
|
|
91
|
-
this.pending = /* @__PURE__ */ new Map();
|
|
88
|
+
async processPendingBatches() {
|
|
89
|
+
const pending = new Map(this.queue);
|
|
90
|
+
this.queue.clear();
|
|
92
91
|
for (const [group, items] of pending) {
|
|
93
92
|
const getItems = items.filter(([options]) => options.request.method === "GET");
|
|
94
93
|
const restItems = items.filter(([options]) => options.request.method !== "GET");
|
|
95
|
-
this
|
|
96
|
-
this
|
|
94
|
+
this.executeBatch("GET", group, getItems);
|
|
95
|
+
this.executeBatch("POST", group, restItems);
|
|
97
96
|
}
|
|
98
97
|
}
|
|
99
|
-
async
|
|
98
|
+
async executeBatch(method, group, groupItems) {
|
|
100
99
|
if (!groupItems.length) {
|
|
101
100
|
return;
|
|
102
101
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
if (groupItems.length === 1) {
|
|
103
|
+
const [options, resolve, reject] = groupItems[0];
|
|
104
|
+
options.next().then(resolve).catch(reject);
|
|
106
105
|
return;
|
|
107
106
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
this
|
|
114
|
-
this
|
|
115
|
-
|
|
107
|
+
const subOptionsList = groupItems.map(([options]) => options);
|
|
108
|
+
const maxSize = await value(this.maxSize, subOptionsList);
|
|
109
|
+
if (groupItems.length > maxSize) {
|
|
110
|
+
const [first, second] = splitInHalf(groupItems);
|
|
111
|
+
await Promise.all([
|
|
112
|
+
this.executeBatch(method, group, first),
|
|
113
|
+
this.executeBatch(method, group, second)
|
|
114
|
+
]);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const url = await value(this.batchUrl, subOptionsList);
|
|
118
|
+
const headers = await value(this.batchHeaders, subOptionsList);
|
|
119
|
+
const mode = value(this.mode, subOptionsList);
|
|
120
|
+
let suppressErrorFromCurrentBatch = false;
|
|
121
|
+
const controller = new AbortController();
|
|
122
|
+
const pendingMessages = [];
|
|
123
|
+
let batchResponse;
|
|
124
|
+
const peer = new ClientPeer(async (message) => {
|
|
125
|
+
pendingMessages.push(message);
|
|
126
|
+
if (message.kind === "cancel" && pendingMessages.filter((m) => m.kind === "cancel").length === groupItems.length) {
|
|
127
|
+
controller.abort();
|
|
116
128
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
129
|
+
if (message.kind === "request" && pendingMessages.filter((m) => m.kind === "request").length === groupItems.length) {
|
|
130
|
+
(async () => {
|
|
131
|
+
try {
|
|
132
|
+
const request = {
|
|
133
|
+
url,
|
|
134
|
+
method,
|
|
135
|
+
headers: { ...headers, "orpc-batch": mode },
|
|
136
|
+
signal: controller.signal
|
|
137
|
+
};
|
|
138
|
+
if (method === "GET") {
|
|
139
|
+
const [pathname, search, hash] = parseStandardUrl(url);
|
|
140
|
+
const dataParam = `data=${encodeURIComponent(stringifyJSON(pendingMessages))}`;
|
|
141
|
+
const newUrl = search ? `${pathname}${search}&${dataParam}${hash ?? ""}` : `${pathname}?${dataParam}${hash ?? ""}`;
|
|
142
|
+
const maxUrlLength = await value(this.maxUrlLength, subOptionsList);
|
|
143
|
+
if (newUrl.length > maxUrlLength) {
|
|
144
|
+
const [first, second] = splitInHalf(groupItems);
|
|
145
|
+
suppressErrorFromCurrentBatch = true;
|
|
146
|
+
await Promise.all([
|
|
147
|
+
this.executeBatch(method, group, first),
|
|
148
|
+
this.executeBatch(method, group, second),
|
|
149
|
+
peer.close()
|
|
150
|
+
]);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
request.url = newUrl;
|
|
154
|
+
} else {
|
|
155
|
+
request.body = pendingMessages;
|
|
156
|
+
}
|
|
157
|
+
batchResponse = await groupItems[0][0].next({
|
|
158
|
+
...subOptionsList[0],
|
|
159
|
+
context: value(group.context, subOptionsList),
|
|
160
|
+
path: value(group.path, subOptionsList) ?? [],
|
|
161
|
+
request,
|
|
162
|
+
signal: controller.signal
|
|
163
|
+
});
|
|
164
|
+
const body = await batchResponse.resolveBody();
|
|
165
|
+
if (Array.isArray(body) && body.every((v) => isServerPeerSendMessage(v))) {
|
|
166
|
+
for (const message2 of body) {
|
|
167
|
+
await peer.message(message2);
|
|
168
|
+
}
|
|
169
|
+
} else if (body instanceof Blob) {
|
|
170
|
+
await decodeLengthPrefixedBlob(body, peer);
|
|
171
|
+
} else if (body instanceof ReadableStream) {
|
|
172
|
+
await decodeLengthPrefixedStream(body, peer);
|
|
173
|
+
} else {
|
|
174
|
+
throw new BatchLinkPluginError("Invalid batch response format.");
|
|
175
|
+
}
|
|
176
|
+
await peer.close(new BatchLinkPluginError("Batch response is incomplete."));
|
|
177
|
+
} catch (error) {
|
|
178
|
+
await peer.close(error);
|
|
179
|
+
}
|
|
180
|
+
})();
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
groupItems.forEach(([subOptions, resolve, reject]) => {
|
|
184
|
+
peer.request(this.mapSubrequest(subOptions, { url, headers })).then((subResponse) => resolve(this.mapSubresponse(subResponse, batchResponse, subOptions))).catch((error) => {
|
|
185
|
+
if (!suppressErrorFromCurrentBatch) {
|
|
186
|
+
reject(error);
|
|
187
|
+
}
|
|
125
188
|
});
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async function decodeLengthPrefixedBlob(blob, peer) {
|
|
193
|
+
const buffer = await loadBytes(blob);
|
|
194
|
+
let offset = 0;
|
|
195
|
+
while (offset < buffer.length) {
|
|
196
|
+
if (offset + 4 > buffer.length) {
|
|
197
|
+
throw new BatchLinkPluginError("Invalid batch response: incomplete length header.");
|
|
198
|
+
}
|
|
199
|
+
const view = new DataView(buffer.buffer, buffer.byteOffset + offset, 4);
|
|
200
|
+
const length = view.getUint32(0, false);
|
|
201
|
+
offset += 4;
|
|
202
|
+
if (offset + length > buffer.length) {
|
|
203
|
+
throw new BatchLinkPluginError("Invalid batch response: incomplete message.");
|
|
204
|
+
}
|
|
205
|
+
const messageBytes = buffer.subarray(offset, offset + length);
|
|
206
|
+
offset += length;
|
|
207
|
+
const result = decodePeerMessage(messageBytes);
|
|
208
|
+
if (!result.matched || !isServerPeerSendMessage(result.message)) {
|
|
209
|
+
throw new BatchLinkPluginError("Invalid batch response: invalid message.");
|
|
210
|
+
}
|
|
211
|
+
await peer.message(result.message);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async function decodeLengthPrefixedStream(stream, peer) {
|
|
215
|
+
const reader = stream.getReader();
|
|
216
|
+
let buffer = new Uint8Array(0);
|
|
217
|
+
try {
|
|
218
|
+
while (true) {
|
|
219
|
+
const { done, value: chunk } = await reader.read();
|
|
220
|
+
if (chunk) {
|
|
221
|
+
const newBuffer = new Uint8Array(buffer.length + chunk.length);
|
|
222
|
+
newBuffer.set(buffer);
|
|
223
|
+
newBuffer.set(chunk, buffer.length);
|
|
224
|
+
buffer = newBuffer;
|
|
132
225
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
context: group.context,
|
|
139
|
-
input: group.input,
|
|
140
|
-
path: toArray(group.path)
|
|
141
|
-
});
|
|
142
|
-
const parsed = parseBatchResponse({ ...lazyResponse, body: await lazyResponse.body() });
|
|
143
|
-
for await (const item of parsed) {
|
|
144
|
-
batchItems[item.index]?.[1]({ ...item, body: () => Promise.resolve(item.body) });
|
|
226
|
+
while (buffer.length >= 4) {
|
|
227
|
+
const view = new DataView(buffer.buffer, buffer.byteOffset, 4);
|
|
228
|
+
const length = view.getUint32(0, false);
|
|
229
|
+
if (buffer.length < 4 + length) {
|
|
230
|
+
break;
|
|
145
231
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
}
|
|
232
|
+
const messageBytes = buffer.subarray(4, 4 + length);
|
|
233
|
+
buffer = buffer.subarray(4 + length);
|
|
234
|
+
const result = decodePeerMessage(messageBytes);
|
|
235
|
+
if (!result.matched || !isServerPeerSendMessage(result.message)) {
|
|
236
|
+
throw new BatchLinkPluginError("Invalid batch response: invalid message.");
|
|
153
237
|
}
|
|
154
|
-
|
|
238
|
+
await peer.message(result.message);
|
|
155
239
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
for (const [, , reject] of batchItems) {
|
|
159
|
-
reject(error);
|
|
240
|
+
if (done) {
|
|
241
|
+
break;
|
|
160
242
|
}
|
|
161
243
|
}
|
|
244
|
+
} finally {
|
|
245
|
+
reader.releaseLock();
|
|
162
246
|
}
|
|
163
247
|
}
|
|
164
248
|
|
|
165
|
-
class
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
249
|
+
class DedupeLinkPlugin {
|
|
250
|
+
name = "~dedupe";
|
|
251
|
+
before = ["~batch"];
|
|
252
|
+
groups;
|
|
253
|
+
filter;
|
|
254
|
+
queue = /* @__PURE__ */ new Map();
|
|
171
255
|
constructor(options) {
|
|
172
|
-
this
|
|
173
|
-
this
|
|
256
|
+
this.groups = options.groups;
|
|
257
|
+
this.filter = options.filter ?? (({ request }) => request.method === "GET");
|
|
174
258
|
}
|
|
175
259
|
init(options) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
return options2.next();
|
|
260
|
+
const transportInterceptor = (interceptorOptions) => {
|
|
261
|
+
if (!canDedupeRequest(interceptorOptions.request) || !value(this.filter, interceptorOptions)) {
|
|
262
|
+
return interceptorOptions.next();
|
|
180
263
|
}
|
|
181
|
-
const group = this
|
|
264
|
+
const group = this.groups.find((group2) => value(group2.condition, interceptorOptions));
|
|
182
265
|
if (!group) {
|
|
183
|
-
return
|
|
266
|
+
return interceptorOptions.next();
|
|
184
267
|
}
|
|
185
268
|
return new Promise((resolve, reject) => {
|
|
186
|
-
this
|
|
187
|
-
defer(() =>
|
|
269
|
+
this.enqueue(group, interceptorOptions, resolve, reject);
|
|
270
|
+
defer(() => {
|
|
271
|
+
this.processPendingRequests();
|
|
272
|
+
});
|
|
188
273
|
});
|
|
189
|
-
}
|
|
274
|
+
};
|
|
275
|
+
return {
|
|
276
|
+
...options,
|
|
277
|
+
transportInterceptors: [...toArray(options.transportInterceptors), transportInterceptor]
|
|
278
|
+
};
|
|
190
279
|
}
|
|
191
|
-
|
|
192
|
-
let queue = this
|
|
280
|
+
enqueue(group, options, resolve, reject) {
|
|
281
|
+
let queue = this.queue.get(group);
|
|
193
282
|
if (!queue) {
|
|
194
|
-
|
|
283
|
+
queue = [];
|
|
284
|
+
this.queue.set(group, queue);
|
|
195
285
|
}
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
body: item.options.request.body,
|
|
199
|
-
headers: item.options.request.headers,
|
|
200
|
-
method: item.options.request.method,
|
|
201
|
-
url: item.options.request.url
|
|
202
|
-
});
|
|
203
|
-
const requestString2 = stringifyJSON({
|
|
204
|
-
body: options.request.body,
|
|
205
|
-
headers: options.request.headers,
|
|
206
|
-
method: options.request.method,
|
|
207
|
-
url: options.request.url
|
|
208
|
-
});
|
|
209
|
-
return requestString1 === requestString2;
|
|
210
|
-
});
|
|
286
|
+
const requestKey = createRequestKey(options.path, options.request);
|
|
287
|
+
const matched = queue.find((item) => item.requestKey === requestKey);
|
|
211
288
|
if (matched) {
|
|
289
|
+
matched.matchedOptions.push(options);
|
|
212
290
|
matched.signals.push(options.request.signal);
|
|
213
291
|
matched.resolves.push(resolve);
|
|
214
292
|
matched.rejects.push(reject);
|
|
215
|
-
|
|
216
|
-
queue.push({
|
|
217
|
-
options,
|
|
218
|
-
signals: [options.request.signal],
|
|
219
|
-
resolves: [resolve],
|
|
220
|
-
rejects: [reject]
|
|
221
|
-
});
|
|
293
|
+
return;
|
|
222
294
|
}
|
|
295
|
+
queue.push({
|
|
296
|
+
requestKey,
|
|
297
|
+
options,
|
|
298
|
+
matchedOptions: [options],
|
|
299
|
+
signals: [options.request.signal],
|
|
300
|
+
resolves: [resolve],
|
|
301
|
+
rejects: [reject]
|
|
302
|
+
});
|
|
223
303
|
}
|
|
224
|
-
async
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
);
|
|
304
|
+
async processPendingRequests() {
|
|
305
|
+
const pending = new Map(this.queue);
|
|
306
|
+
this.queue.clear();
|
|
307
|
+
const executions = [];
|
|
308
|
+
for (const [group, items] of pending) {
|
|
309
|
+
for (const item of items) {
|
|
310
|
+
executions.push(this.execute(group, item));
|
|
231
311
|
}
|
|
232
312
|
}
|
|
233
|
-
|
|
234
|
-
await Promise.all(promises);
|
|
313
|
+
await Promise.all(executions);
|
|
235
314
|
}
|
|
236
|
-
async
|
|
315
|
+
async execute(group, item) {
|
|
237
316
|
try {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
317
|
+
if (!shouldDedupe(item.matchedOptions)) {
|
|
318
|
+
const response2 = await item.options.next(item.options);
|
|
319
|
+
item.resolves[0]?.(response2);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const context = value(group.context, item.matchedOptions);
|
|
323
|
+
const request = {
|
|
324
|
+
...item.options.request,
|
|
325
|
+
signal: allAbortSignal(item.signals)
|
|
241
326
|
};
|
|
242
|
-
const response = await options.next({
|
|
243
|
-
...options,
|
|
244
|
-
request
|
|
245
|
-
signal:
|
|
246
|
-
context
|
|
327
|
+
const response = await item.options.next({
|
|
328
|
+
...item.options,
|
|
329
|
+
request,
|
|
330
|
+
signal: request.signal,
|
|
331
|
+
context
|
|
247
332
|
});
|
|
248
|
-
const replicatedResponses =
|
|
249
|
-
for (const resolve of resolves) {
|
|
250
|
-
resolve(replicatedResponses.
|
|
333
|
+
const replicatedResponses = replicateLazyResponse(response, item.resolves.length);
|
|
334
|
+
for (const resolve of item.resolves) {
|
|
335
|
+
resolve(replicatedResponses.pop());
|
|
251
336
|
}
|
|
252
337
|
} catch (error) {
|
|
253
|
-
for (const reject of rejects) {
|
|
338
|
+
for (const reject of item.rejects) {
|
|
254
339
|
reject(error);
|
|
255
340
|
}
|
|
256
341
|
}
|
|
257
342
|
}
|
|
258
343
|
}
|
|
344
|
+
function canDedupeRequest(request) {
|
|
345
|
+
return !(request.body instanceof Blob || request.body instanceof FormData || request.body instanceof URLSearchParams || request.body instanceof ReadableStream || isAsyncIteratorObject(request.body) || request.signal?.aborted);
|
|
346
|
+
}
|
|
347
|
+
function createRequestKey(path, request) {
|
|
348
|
+
return stringifyJSON({
|
|
349
|
+
path,
|
|
350
|
+
body: request.body,
|
|
351
|
+
headers: request.headers,
|
|
352
|
+
method: request.method,
|
|
353
|
+
url: request.url
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
function replicateLazyResponse(response, count) {
|
|
357
|
+
const replicated = [];
|
|
358
|
+
let bodyPromise;
|
|
359
|
+
let replicatedAsyncIterators;
|
|
360
|
+
let replicatedReadableStream;
|
|
361
|
+
for (let i = 0; i < count; i++) {
|
|
362
|
+
let resolvedBody;
|
|
363
|
+
replicated.push({
|
|
364
|
+
...response,
|
|
365
|
+
resolveBody: async (hint) => {
|
|
366
|
+
if (resolvedBody) {
|
|
367
|
+
return resolvedBody.body;
|
|
368
|
+
}
|
|
369
|
+
bodyPromise ??= response.resolveBody(hint);
|
|
370
|
+
const body = await bodyPromise;
|
|
371
|
+
if (isAsyncIteratorObject(body)) {
|
|
372
|
+
replicatedAsyncIterators ??= replicateAsyncIterator(body, count);
|
|
373
|
+
resolvedBody = { body: replicatedAsyncIterators.pop() };
|
|
374
|
+
} else if (body instanceof ReadableStream) {
|
|
375
|
+
replicatedReadableStream ??= replicateReadableStream(body, count);
|
|
376
|
+
resolvedBody = { body: replicatedReadableStream.pop() };
|
|
377
|
+
} else {
|
|
378
|
+
resolvedBody = { body };
|
|
379
|
+
}
|
|
380
|
+
return resolvedBody.body;
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
return replicated;
|
|
385
|
+
}
|
|
386
|
+
function shouldDedupe(items) {
|
|
387
|
+
return items.length >= 2;
|
|
388
|
+
}
|
|
259
389
|
|
|
260
|
-
class
|
|
390
|
+
class RetryLinkPluginInvalidEventIteratorRetryResponse extends Error {
|
|
261
391
|
}
|
|
262
|
-
class
|
|
392
|
+
class RetryLinkPlugin {
|
|
263
393
|
defaultRetry;
|
|
264
394
|
defaultRetryDelay;
|
|
265
395
|
defaultShouldRetry;
|
|
266
396
|
defaultOnRetry;
|
|
267
|
-
|
|
397
|
+
name = "~retry";
|
|
268
398
|
constructor(options = {}) {
|
|
269
399
|
this.defaultRetry = options.default?.retry ?? 0;
|
|
270
400
|
this.defaultRetryDelay = options.default?.retryDelay ?? ((o) => o.lastEventRetry ?? 2e3);
|
|
@@ -272,33 +402,33 @@ class ClientRetryPlugin {
|
|
|
272
402
|
this.defaultOnRetry = options.default?.onRetry;
|
|
273
403
|
}
|
|
274
404
|
init(options) {
|
|
275
|
-
|
|
276
|
-
|
|
405
|
+
const interceptor = async (interceptorOptions) => {
|
|
406
|
+
const { next, ...callOptions } = interceptorOptions;
|
|
277
407
|
const maxAttempts = await value(
|
|
278
|
-
|
|
279
|
-
|
|
408
|
+
callOptions.context.retry ?? this.defaultRetry,
|
|
409
|
+
callOptions
|
|
280
410
|
);
|
|
281
|
-
const retryDelay =
|
|
282
|
-
const shouldRetry =
|
|
283
|
-
const onRetry =
|
|
411
|
+
const retryDelay = callOptions.context.retryDelay ?? this.defaultRetryDelay;
|
|
412
|
+
const shouldRetry = callOptions.context.shouldRetry ?? this.defaultShouldRetry;
|
|
413
|
+
const onRetry = callOptions.context.onRetry ?? this.defaultOnRetry;
|
|
284
414
|
if (maxAttempts <= 0) {
|
|
285
|
-
return
|
|
415
|
+
return next(callOptions);
|
|
286
416
|
}
|
|
287
|
-
let lastEventId =
|
|
417
|
+
let lastEventId = callOptions.lastEventId;
|
|
288
418
|
let lastEventRetry;
|
|
289
419
|
let callback;
|
|
290
|
-
let
|
|
291
|
-
const
|
|
420
|
+
let attempt = 1;
|
|
421
|
+
const callNext = async (initialError) => {
|
|
292
422
|
let currentError = initialError;
|
|
293
423
|
while (true) {
|
|
294
|
-
const
|
|
424
|
+
const updatedCallOptions = { ...callOptions, lastEventId };
|
|
295
425
|
if (currentError) {
|
|
296
|
-
if (
|
|
426
|
+
if (attempt > maxAttempts) {
|
|
297
427
|
throw currentError.error;
|
|
298
428
|
}
|
|
299
429
|
const attemptOptions = {
|
|
300
|
-
...
|
|
301
|
-
|
|
430
|
+
...updatedCallOptions,
|
|
431
|
+
attempt,
|
|
302
432
|
error: currentError.error,
|
|
303
433
|
lastEventRetry
|
|
304
434
|
};
|
|
@@ -310,16 +440,23 @@ class ClientRetryPlugin {
|
|
|
310
440
|
throw currentError.error;
|
|
311
441
|
}
|
|
312
442
|
callback = onRetry?.(attemptOptions);
|
|
313
|
-
const retryDelayMs = await value(retryDelay, attemptOptions);
|
|
314
|
-
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
|
315
|
-
attemptIndex++;
|
|
316
443
|
}
|
|
317
444
|
try {
|
|
445
|
+
if (currentError) {
|
|
446
|
+
const retryDelayMs = await value(retryDelay, {
|
|
447
|
+
...updatedCallOptions,
|
|
448
|
+
attempt,
|
|
449
|
+
error: currentError.error,
|
|
450
|
+
lastEventRetry
|
|
451
|
+
});
|
|
452
|
+
await sleep(retryDelayMs, { signal: updatedCallOptions.signal });
|
|
453
|
+
attempt++;
|
|
454
|
+
}
|
|
318
455
|
currentError = void 0;
|
|
319
|
-
return await
|
|
456
|
+
return await next(updatedCallOptions);
|
|
320
457
|
} catch (error) {
|
|
321
458
|
currentError = { error };
|
|
322
|
-
if (
|
|
459
|
+
if (updatedCallOptions.signal?.aborted) {
|
|
323
460
|
throw error;
|
|
324
461
|
}
|
|
325
462
|
} finally {
|
|
@@ -328,13 +465,13 @@ class ClientRetryPlugin {
|
|
|
328
465
|
}
|
|
329
466
|
}
|
|
330
467
|
};
|
|
331
|
-
const output = await
|
|
468
|
+
const output = await callNext();
|
|
332
469
|
if (!isAsyncIteratorObject(output)) {
|
|
333
470
|
return output;
|
|
334
471
|
}
|
|
335
472
|
let current = output;
|
|
336
473
|
let isIteratorAborted = false;
|
|
337
|
-
return
|
|
474
|
+
return override(() => current, new AsyncIteratorClass(
|
|
338
475
|
async () => {
|
|
339
476
|
while (true) {
|
|
340
477
|
try {
|
|
@@ -347,10 +484,10 @@ class ClientRetryPlugin {
|
|
|
347
484
|
const meta = getEventMeta(error);
|
|
348
485
|
lastEventId = meta?.id ?? lastEventId;
|
|
349
486
|
lastEventRetry = meta?.retry ?? lastEventRetry;
|
|
350
|
-
const maybeEventIterator = await
|
|
487
|
+
const maybeEventIterator = await callNext({ error });
|
|
351
488
|
if (!isAsyncIteratorObject(maybeEventIterator)) {
|
|
352
|
-
throw new
|
|
353
|
-
"
|
|
489
|
+
throw new RetryLinkPluginInvalidEventIteratorRetryResponse(
|
|
490
|
+
"RetryLinkPlugin: Expected an Event Iterator, got a non-Event Iterator"
|
|
354
491
|
);
|
|
355
492
|
}
|
|
356
493
|
current = maybeEventIterator;
|
|
@@ -361,30 +498,30 @@ class ClientRetryPlugin {
|
|
|
361
498
|
}
|
|
362
499
|
}
|
|
363
500
|
},
|
|
364
|
-
async (
|
|
501
|
+
async ({ kind }) => {
|
|
365
502
|
isIteratorAborted = true;
|
|
366
|
-
if (
|
|
503
|
+
if (kind === "cancelled") {
|
|
367
504
|
await current.return?.();
|
|
368
505
|
}
|
|
369
506
|
}
|
|
370
507
|
));
|
|
371
|
-
}
|
|
508
|
+
};
|
|
509
|
+
return { ...options, interceptors: [interceptor, ...toArray(options.interceptors)] };
|
|
372
510
|
}
|
|
373
511
|
}
|
|
374
512
|
|
|
375
|
-
class
|
|
513
|
+
class RetryAfterLinkPlugin {
|
|
376
514
|
condition;
|
|
377
515
|
maxAttempts;
|
|
378
516
|
timeout;
|
|
379
|
-
|
|
517
|
+
name = "~retry-after";
|
|
380
518
|
constructor(options = {}) {
|
|
381
|
-
this.condition = options.condition ?? ((response) => response.status ===
|
|
519
|
+
this.condition = options.condition ?? ((response) => response.status === COMMON_ERROR_STATUS_MAP.TOO_MANY_REQUESTS || response.status === COMMON_ERROR_STATUS_MAP.SERVICE_UNAVAILABLE);
|
|
382
520
|
this.maxAttempts = options.maxAttempts ?? 3;
|
|
383
521
|
this.timeout = options.timeout ?? 5 * 60 * 1e3;
|
|
384
522
|
}
|
|
385
523
|
init(options) {
|
|
386
|
-
|
|
387
|
-
options.clientInterceptors.push(async (interceptorOptions) => {
|
|
524
|
+
const interceptor = async (interceptorOptions) => {
|
|
388
525
|
const startTime = Date.now();
|
|
389
526
|
let attemptCount = 0;
|
|
390
527
|
while (true) {
|
|
@@ -393,8 +530,8 @@ class RetryAfterPlugin {
|
|
|
393
530
|
if (!value(this.condition, response, interceptorOptions)) {
|
|
394
531
|
return response;
|
|
395
532
|
}
|
|
396
|
-
const retryAfterHeader =
|
|
397
|
-
const retryAfterMs =
|
|
533
|
+
const retryAfterHeader = flattenStandardHeader(response.headers["retry-after"]);
|
|
534
|
+
const retryAfterMs = parseRetryAfterHeader(retryAfterHeader);
|
|
398
535
|
if (retryAfterMs === void 0) {
|
|
399
536
|
return response;
|
|
400
537
|
}
|
|
@@ -406,80 +543,32 @@ class RetryAfterPlugin {
|
|
|
406
543
|
if (elapsedTime + retryAfterMs > timeoutMs) {
|
|
407
544
|
return response;
|
|
408
545
|
}
|
|
409
|
-
|
|
546
|
+
try {
|
|
547
|
+
await sleep(retryAfterMs, { signal: interceptorOptions.signal });
|
|
548
|
+
} catch {
|
|
549
|
+
}
|
|
410
550
|
if (interceptorOptions.signal?.aborted) {
|
|
411
551
|
return response;
|
|
412
552
|
}
|
|
413
553
|
}
|
|
414
|
-
}
|
|
554
|
+
};
|
|
555
|
+
return { ...options, transportInterceptors: [interceptor, ...toArray(options.transportInterceptors)] };
|
|
415
556
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
const seconds = Number(value2);
|
|
422
|
-
if (Number.isFinite(seconds)) {
|
|
423
|
-
return Math.max(0, seconds * 1e3);
|
|
424
|
-
}
|
|
425
|
-
const retryDate = Date.parse(value2);
|
|
426
|
-
if (!Number.isNaN(retryDate)) {
|
|
427
|
-
return Math.max(0, retryDate - Date.now());
|
|
428
|
-
}
|
|
557
|
+
}
|
|
558
|
+
function parseRetryAfterHeader(value2) {
|
|
559
|
+
value2 = value2?.trim();
|
|
560
|
+
if (!value2) {
|
|
429
561
|
return void 0;
|
|
430
562
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
resolve();
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
|
-
let timeout;
|
|
438
|
-
const onAbort = () => {
|
|
439
|
-
clearTimeout(timeout);
|
|
440
|
-
timeout = void 0;
|
|
441
|
-
resolve();
|
|
442
|
-
};
|
|
443
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
444
|
-
timeout = setTimeout(() => {
|
|
445
|
-
signal?.removeEventListener("abort", onAbort);
|
|
446
|
-
resolve();
|
|
447
|
-
}, ms);
|
|
448
|
-
});
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
class SimpleCsrfProtectionLinkPlugin {
|
|
453
|
-
headerName;
|
|
454
|
-
headerValue;
|
|
455
|
-
exclude;
|
|
456
|
-
constructor(options = {}) {
|
|
457
|
-
this.headerName = options.headerName ?? "x-csrf-token";
|
|
458
|
-
this.headerValue = options.headerValue ?? "orpc";
|
|
459
|
-
this.exclude = options.exclude ?? false;
|
|
563
|
+
const seconds = Number(value2);
|
|
564
|
+
if (Number.isFinite(seconds)) {
|
|
565
|
+
return Math.max(0, seconds * 1e3);
|
|
460
566
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
options.clientInterceptors.push(async (options2) => {
|
|
465
|
-
const excluded = await value(this.exclude, options2);
|
|
466
|
-
if (excluded) {
|
|
467
|
-
return options2.next();
|
|
468
|
-
}
|
|
469
|
-
const headerName = await value(this.headerName, options2);
|
|
470
|
-
const headerValue = await value(this.headerValue, options2);
|
|
471
|
-
return options2.next({
|
|
472
|
-
...options2,
|
|
473
|
-
request: {
|
|
474
|
-
...options2.request,
|
|
475
|
-
headers: {
|
|
476
|
-
...options2.request.headers,
|
|
477
|
-
[headerName]: headerValue
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
});
|
|
481
|
-
});
|
|
567
|
+
const retryDate = Date.parse(value2);
|
|
568
|
+
if (!Number.isNaN(retryDate)) {
|
|
569
|
+
return Math.max(0, retryDate - Date.now());
|
|
482
570
|
}
|
|
571
|
+
return void 0;
|
|
483
572
|
}
|
|
484
573
|
|
|
485
|
-
export { BatchLinkPlugin,
|
|
574
|
+
export { BatchLinkPlugin, BatchLinkPluginError, DedupeLinkPlugin, RetryAfterLinkPlugin, RetryLinkPlugin, RetryLinkPluginInvalidEventIteratorRetryResponse };
|