@orpc/client 2.0.0-beta.4 → 2.0.0-beta.41
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 +97 -94
- package/dist/adapters/fetch/index.d.mts +16 -33
- package/dist/adapters/fetch/index.d.ts +16 -33
- package/dist/adapters/fetch/index.mjs +33 -30
- package/dist/adapters/message-port/index.d.mts +13 -8
- package/dist/adapters/message-port/index.d.ts +13 -8
- package/dist/adapters/message-port/index.mjs +27 -17
- package/dist/adapters/standard/index.d.mts +9 -10
- package/dist/adapters/standard/index.d.ts +9 -10
- package/dist/adapters/standard/index.mjs +5 -5
- package/dist/adapters/websocket/index.d.mts +33 -18
- package/dist/adapters/websocket/index.d.ts +33 -18
- package/dist/adapters/websocket/index.mjs +45 -24
- package/dist/index.d.mts +62 -61
- package/dist/index.d.ts +62 -61
- package/dist/index.mjs +47 -54
- package/dist/plugins/index.d.mts +174 -13
- package/dist/plugins/index.d.ts +174 -13
- package/dist/plugins/index.mjs +580 -198
- package/dist/shared/{client.BdItY5DT.d.mts → client.BPa_Mmuh.d.mts} +25 -4
- package/dist/shared/{client.BdItY5DT.d.ts → client.BPa_Mmuh.d.ts} +25 -4
- package/dist/shared/{client.Cby_-GGh.d.mts → client.BjbdW5Rj.d.mts} +2 -2
- package/dist/shared/{client.DXhchJ84.mjs → client.Bstqyaro.mjs} +25 -30
- package/dist/shared/{client.Dnfj8jnT.mjs → client.DD2eNne_.mjs} +20 -15
- package/dist/shared/{client.CPF3hX6O.d.ts → client.DiHfJmHK.d.ts} +2 -2
- package/dist/shared/client.DpkGsZt-.mjs +486 -0
- package/dist/shared/{client.8ug8I-zu.d.mts → client.Dw9_40WO.d.mts} +57 -16
- package/dist/shared/{client.8ug8I-zu.d.ts → client.Dw9_40WO.d.ts} +57 -16
- package/package.json +22 -8
- package/dist/shared/client.DMXKFDyV.mjs +0 -343
package/dist/plugins/index.mjs
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import { toArray, value, splitInHalf, stringifyJSON, isAsyncIteratorObject, defer, loadBytes, allAbortSignal, replicateAsyncIterator, replicateReadableStream, override, AsyncIteratorClass, sleep } from '@orpc/shared';
|
|
2
|
-
import { parseStandardUrl,
|
|
3
|
-
import { ClientPeer, isServerPeerSendMessage, decodePeerMessage } from '@
|
|
4
|
-
import {
|
|
1
|
+
import { toArray, value, splitInHalf, promiseWithResolvers, safeEncodeURIComponent, stringifyJSON, once, isAsyncIteratorObject, defer, captureAsyncContext, loadBytes, allAbortSignal, runWithSignal, replicateAsyncIterator, replicateReadableStream, wrapAsyncIterator, throwIfAborted, isCompressibleContentType, override, AsyncIteratorClass, sleep, AbortError, anyAbortSignal } from '@orpc/shared';
|
|
2
|
+
import { parseStandardUrl, flattenStandardHeader, generateContentDisposition, getEventMeta } from '@standard-server/core';
|
|
3
|
+
import { ClientPeer, isServerPeerSendMessage, decodePeerMessage } from '@standard-server/peer';
|
|
4
|
+
import { toFetchHeaders, toStandardBody } from '@standard-server/fetch';
|
|
5
|
+
import { C as COMMON_ERROR_STATUS_MAP } from '../shared/client.DD2eNne_.mjs';
|
|
5
6
|
|
|
6
|
-
class BatchLinkPluginError extends TypeError {
|
|
7
|
-
}
|
|
8
7
|
class BatchLinkPlugin {
|
|
9
8
|
name = "~batch";
|
|
10
9
|
groups;
|
|
11
10
|
filter;
|
|
11
|
+
scope;
|
|
12
12
|
maxSize;
|
|
13
|
+
wait;
|
|
13
14
|
mode;
|
|
14
15
|
batchUrl;
|
|
15
16
|
maxUrlLength;
|
|
@@ -20,7 +21,9 @@ class BatchLinkPlugin {
|
|
|
20
21
|
constructor(options) {
|
|
21
22
|
this.groups = options.groups;
|
|
22
23
|
this.filter = options.filter ?? (() => true);
|
|
24
|
+
this.scope = options.scope ?? (() => void 0);
|
|
23
25
|
this.maxSize = options.maxSize ?? 10;
|
|
26
|
+
this.wait = options.wait ?? 0;
|
|
24
27
|
this.mode = options.mode ?? "streaming";
|
|
25
28
|
this.batchUrl = options.url ?? ((options2) => {
|
|
26
29
|
const [pathname] = parseStandardUrl(options2[0].request.url);
|
|
@@ -31,7 +34,8 @@ class BatchLinkPlugin {
|
|
|
31
34
|
const headersList = options2.map((o) => o.request.headers);
|
|
32
35
|
const commonHeaders = {};
|
|
33
36
|
for (const headers of headersList) {
|
|
34
|
-
for (const
|
|
37
|
+
for (const key of Object.keys(headers)) {
|
|
38
|
+
const value2 = headers[key];
|
|
35
39
|
if (headersList.every((h) => h[key] === value2)) {
|
|
36
40
|
commonHeaders[key] = value2;
|
|
37
41
|
}
|
|
@@ -41,7 +45,8 @@ class BatchLinkPlugin {
|
|
|
41
45
|
});
|
|
42
46
|
this.mapSubrequest = options.mapSubrequest ?? (({ request }, { headers }) => {
|
|
43
47
|
const subHeaders = { ...request.headers };
|
|
44
|
-
for (const
|
|
48
|
+
for (const key of Object.keys(headers)) {
|
|
49
|
+
const value2 = headers[key];
|
|
45
50
|
if (subHeaders[key] === value2) {
|
|
46
51
|
subHeaders[key] = void 0;
|
|
47
52
|
}
|
|
@@ -51,7 +56,7 @@ class BatchLinkPlugin {
|
|
|
51
56
|
headers: subHeaders
|
|
52
57
|
};
|
|
53
58
|
});
|
|
54
|
-
this.mapSubresponse = (subResponse, batchResponse) => {
|
|
59
|
+
this.mapSubresponse = options.mapSubresponse ?? ((subResponse, batchResponse) => {
|
|
55
60
|
return {
|
|
56
61
|
...subResponse,
|
|
57
62
|
headers: {
|
|
@@ -60,24 +65,34 @@ class BatchLinkPlugin {
|
|
|
60
65
|
...subResponse.headers
|
|
61
66
|
}
|
|
62
67
|
};
|
|
63
|
-
};
|
|
68
|
+
});
|
|
64
69
|
}
|
|
65
70
|
init(options) {
|
|
66
71
|
const transportInterceptor = async (interceptorOptions) => {
|
|
67
|
-
|
|
72
|
+
const { body, signal } = interceptorOptions.request;
|
|
73
|
+
if (body instanceof Blob || body instanceof FormData || body instanceof ReadableStream || isAsyncIteratorObject(body) || signal?.aborted || !value(this.filter, interceptorOptions)) {
|
|
68
74
|
return interceptorOptions.next();
|
|
69
75
|
}
|
|
70
76
|
const group = this.groups.find((group2) => value(group2.condition, interceptorOptions));
|
|
71
77
|
if (!group) {
|
|
72
78
|
return interceptorOptions.next();
|
|
73
79
|
}
|
|
80
|
+
const scope = this.scope(interceptorOptions);
|
|
74
81
|
return new Promise((resolve, reject) => {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
82
|
+
if (!this.queue.size) {
|
|
83
|
+
defer(() => this.processPendingBatches(), this.wait);
|
|
84
|
+
}
|
|
85
|
+
let groups = this.queue.get(scope);
|
|
86
|
+
if (!groups) {
|
|
87
|
+
groups = /* @__PURE__ */ new Map();
|
|
88
|
+
this.queue.set(scope, groups);
|
|
89
|
+
}
|
|
90
|
+
let queue = groups.get(group);
|
|
91
|
+
if (!queue) {
|
|
92
|
+
queue = [];
|
|
93
|
+
groups.set(group, queue);
|
|
78
94
|
}
|
|
79
|
-
queue.push([interceptorOptions, resolve, reject]);
|
|
80
|
-
defer(() => this.processPendingBatches());
|
|
95
|
+
queue.push([interceptorOptions, resolve, reject, captureAsyncContext()]);
|
|
81
96
|
});
|
|
82
97
|
};
|
|
83
98
|
return {
|
|
@@ -85,108 +100,169 @@ class BatchLinkPlugin {
|
|
|
85
100
|
transportInterceptors: [...toArray(options.transportInterceptors), transportInterceptor]
|
|
86
101
|
};
|
|
87
102
|
}
|
|
88
|
-
|
|
89
|
-
const pending =
|
|
103
|
+
processPendingBatches() {
|
|
104
|
+
const pending = [...this.queue.values()];
|
|
90
105
|
this.queue.clear();
|
|
91
|
-
for (const
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
106
|
+
for (const groups of pending) {
|
|
107
|
+
for (const [group, items] of groups) {
|
|
108
|
+
const getItems = items.filter(([options]) => options.request.method === "GET");
|
|
109
|
+
const queryItems = items.filter(([options]) => options.request.method === "QUERY");
|
|
110
|
+
const unsafeItems = items.filter(([options]) => options.request.method !== "GET" && options.request.method !== "QUERY");
|
|
111
|
+
this.executeBatchInOwnContext("GET", group, getItems);
|
|
112
|
+
this.executeBatchInOwnContext("QUERY", group, queryItems);
|
|
113
|
+
this.executeBatchInOwnContext("POST", group, unsafeItems);
|
|
114
|
+
}
|
|
96
115
|
}
|
|
97
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Runs the batch in the async context of its first request instead of the timer's,
|
|
119
|
+
* so the transport and batch options see that caller's request state.
|
|
120
|
+
* `executeBatch` always runs in the context of its first request.
|
|
121
|
+
*/
|
|
122
|
+
async executeBatchInOwnContext(method, group, groupItems) {
|
|
123
|
+
await groupItems[0]?.[3](() => this.executeBatch(method, group, groupItems));
|
|
124
|
+
}
|
|
98
125
|
async executeBatch(method, group, groupItems) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
options.next().then(resolve).catch(reject);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
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();
|
|
126
|
+
try {
|
|
127
|
+
if (groupItems.length === 1) {
|
|
128
|
+
const [options, resolve, reject] = groupItems[0];
|
|
129
|
+
options.next().then(resolve).catch(reject);
|
|
130
|
+
return;
|
|
128
131
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
})();
|
|
132
|
+
const subOptionsList = groupItems.map(([options]) => options);
|
|
133
|
+
const maxSize = await value(this.maxSize, subOptionsList);
|
|
134
|
+
if (groupItems.length > maxSize) {
|
|
135
|
+
const [first, second] = splitInHalf(groupItems);
|
|
136
|
+
await Promise.all([
|
|
137
|
+
this.executeBatch(method, group, first),
|
|
138
|
+
this.executeBatchInOwnContext(method, group, second)
|
|
139
|
+
]);
|
|
140
|
+
return;
|
|
181
141
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
142
|
+
const url = await value(this.batchUrl, subOptionsList);
|
|
143
|
+
const headers = await value(this.batchHeaders, subOptionsList);
|
|
144
|
+
const mode = value(this.mode, subOptionsList);
|
|
145
|
+
let suppressErrorFromCurrentBatch = false;
|
|
146
|
+
const controller = new AbortController();
|
|
147
|
+
const pendingMessages = [];
|
|
148
|
+
const subrequests = groupItems.map(([subOptions]) => this.mapSubrequest(subOptions, { url, headers }));
|
|
149
|
+
let batchResponse;
|
|
150
|
+
let markRequestSent;
|
|
151
|
+
const openRequestIds = /* @__PURE__ */ new Set();
|
|
152
|
+
const cancelledRunningRequestIds = /* @__PURE__ */ new Set();
|
|
153
|
+
let isBatchSent = false;
|
|
154
|
+
const abortIfOnlyCancelledRemain = () => {
|
|
155
|
+
if (openRequestIds.size === 0 && cancelledRunningRequestIds.size > 0) {
|
|
156
|
+
controller.abort();
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
const peer = new ClientPeer(async (message) => {
|
|
160
|
+
pendingMessages.push(message);
|
|
161
|
+
if (message.kind === "request") {
|
|
162
|
+
openRequestIds.add(message.id);
|
|
163
|
+
markRequestSent?.();
|
|
164
|
+
} else if (message.kind === "cancel" && openRequestIds.delete(message.id) && isBatchSent) {
|
|
165
|
+
cancelledRunningRequestIds.add(message.id);
|
|
166
|
+
abortIfOnlyCancelledRemain();
|
|
187
167
|
}
|
|
188
168
|
});
|
|
189
|
-
|
|
169
|
+
for (const [index, [subOptions, resolve, reject]] of groupItems.entries()) {
|
|
170
|
+
const sent = promiseWithResolvers();
|
|
171
|
+
markRequestSent = sent.resolve;
|
|
172
|
+
peer.request(subrequests[index]).then((subResponse) => resolve(this.mapSubresponse(subResponse, batchResponse, subOptions))).catch((error) => {
|
|
173
|
+
if (!suppressErrorFromCurrentBatch) {
|
|
174
|
+
reject(error);
|
|
175
|
+
}
|
|
176
|
+
}).then(sent.resolve);
|
|
177
|
+
await sent.promise;
|
|
178
|
+
}
|
|
179
|
+
if (openRequestIds.size === 0) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
isBatchSent = true;
|
|
183
|
+
try {
|
|
184
|
+
const request = {
|
|
185
|
+
url,
|
|
186
|
+
method,
|
|
187
|
+
headers: { ...headers, "orpc-batch": mode },
|
|
188
|
+
signal: controller.signal
|
|
189
|
+
};
|
|
190
|
+
if (method === "GET") {
|
|
191
|
+
const [pathname, search, hash] = parseStandardUrl(url);
|
|
192
|
+
const dataParam = `data=${safeEncodeURIComponent(stringifyJSON(pendingMessages))}`;
|
|
193
|
+
const newUrl = search ? `${pathname}${search}&${dataParam}${hash ?? ""}` : `${pathname}?${dataParam}${hash ?? ""}`;
|
|
194
|
+
const maxUrlLength = await value(this.maxUrlLength, subOptionsList);
|
|
195
|
+
if (newUrl.length > maxUrlLength) {
|
|
196
|
+
const [first, second] = splitInHalf(groupItems);
|
|
197
|
+
suppressErrorFromCurrentBatch = true;
|
|
198
|
+
await Promise.all([
|
|
199
|
+
this.executeBatch(method, group, first),
|
|
200
|
+
this.executeBatchInOwnContext(method, group, second),
|
|
201
|
+
peer.close()
|
|
202
|
+
]);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
request.url = newUrl;
|
|
206
|
+
} else {
|
|
207
|
+
request.body = pendingMessages;
|
|
208
|
+
}
|
|
209
|
+
batchResponse = await groupItems[0][0].next({
|
|
210
|
+
...subOptionsList[0],
|
|
211
|
+
context: value(group.context, subOptionsList),
|
|
212
|
+
path: value(group.path, subOptionsList) ?? [],
|
|
213
|
+
request,
|
|
214
|
+
signal: controller.signal
|
|
215
|
+
});
|
|
216
|
+
if (batchResponse.status >= 400) {
|
|
217
|
+
const resolveBody = once(() => batchResponse.resolveBody());
|
|
218
|
+
const errorResponse = { ...batchResponse, resolveBody };
|
|
219
|
+
groupItems.forEach(([subOptions, resolve]) => {
|
|
220
|
+
resolve(this.mapSubresponse(errorResponse, batchResponse, subOptions));
|
|
221
|
+
});
|
|
222
|
+
suppressErrorFromCurrentBatch = true;
|
|
223
|
+
await peer.close();
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const body = await batchResponse.resolveBody();
|
|
227
|
+
if (Array.isArray(body) && body.every((v) => isServerPeerSendMessage(v))) {
|
|
228
|
+
for (const message of body) {
|
|
229
|
+
await peer.message(message);
|
|
230
|
+
}
|
|
231
|
+
} else if (body instanceof Blob) {
|
|
232
|
+
await decodeLengthPrefixedBlob(body, peer);
|
|
233
|
+
} else if (body instanceof ReadableStream) {
|
|
234
|
+
await decodeLengthPrefixedStream(body, async (message) => {
|
|
235
|
+
await peer.message(message);
|
|
236
|
+
if (isLastServerMessage(message)) {
|
|
237
|
+
openRequestIds.delete(message.id);
|
|
238
|
+
cancelledRunningRequestIds.delete(message.id);
|
|
239
|
+
abortIfOnlyCancelledRemain();
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
} else {
|
|
243
|
+
throw new TypeError("Invalid batch response format.");
|
|
244
|
+
}
|
|
245
|
+
await peer.close(new TypeError("Batch response is incomplete."));
|
|
246
|
+
} catch (error) {
|
|
247
|
+
await peer.close(error);
|
|
248
|
+
}
|
|
249
|
+
} catch (error) {
|
|
250
|
+
groupItems.forEach(([, , reject]) => reject(error));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function isLastServerMessage(message) {
|
|
255
|
+
switch (message.kind) {
|
|
256
|
+
case "response":
|
|
257
|
+
return message.json.body !== void 0 || message.binary !== void 0 || message.json.headers?.["content-type"] === void 0 && message.json.headers?.["standard-server"] === void 0;
|
|
258
|
+
case "event-stream":
|
|
259
|
+
return message.json.event === "close" || message.json.event === "error";
|
|
260
|
+
case "octet-stream":
|
|
261
|
+
return message.json.close === true;
|
|
262
|
+
case "cancel":
|
|
263
|
+
return true;
|
|
264
|
+
default:
|
|
265
|
+
return false;
|
|
190
266
|
}
|
|
191
267
|
}
|
|
192
268
|
async function decodeLengthPrefixedBlob(blob, peer) {
|
|
@@ -194,24 +270,24 @@ async function decodeLengthPrefixedBlob(blob, peer) {
|
|
|
194
270
|
let offset = 0;
|
|
195
271
|
while (offset < buffer.length) {
|
|
196
272
|
if (offset + 4 > buffer.length) {
|
|
197
|
-
throw new
|
|
273
|
+
throw new TypeError("Invalid batch response: incomplete length header.");
|
|
198
274
|
}
|
|
199
275
|
const view = new DataView(buffer.buffer, buffer.byteOffset + offset, 4);
|
|
200
276
|
const length = view.getUint32(0, false);
|
|
201
277
|
offset += 4;
|
|
202
278
|
if (offset + length > buffer.length) {
|
|
203
|
-
throw new
|
|
279
|
+
throw new TypeError("Invalid batch response: incomplete message.");
|
|
204
280
|
}
|
|
205
281
|
const messageBytes = buffer.subarray(offset, offset + length);
|
|
206
282
|
offset += length;
|
|
207
283
|
const result = decodePeerMessage(messageBytes);
|
|
208
284
|
if (!result.matched || !isServerPeerSendMessage(result.message)) {
|
|
209
|
-
throw new
|
|
285
|
+
throw new TypeError("Invalid batch response: invalid message.");
|
|
210
286
|
}
|
|
211
287
|
await peer.message(result.message);
|
|
212
288
|
}
|
|
213
289
|
}
|
|
214
|
-
async function decodeLengthPrefixedStream(stream,
|
|
290
|
+
async function decodeLengthPrefixedStream(stream, receive) {
|
|
215
291
|
const reader = stream.getReader();
|
|
216
292
|
let buffer = new Uint8Array(0);
|
|
217
293
|
try {
|
|
@@ -226,6 +302,10 @@ async function decodeLengthPrefixedStream(stream, peer) {
|
|
|
226
302
|
while (buffer.length >= 4) {
|
|
227
303
|
const view = new DataView(buffer.buffer, buffer.byteOffset, 4);
|
|
228
304
|
const length = view.getUint32(0, false);
|
|
305
|
+
if (length === 0) {
|
|
306
|
+
buffer = buffer.subarray(4);
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
229
309
|
if (buffer.length < 4 + length) {
|
|
230
310
|
break;
|
|
231
311
|
}
|
|
@@ -233,9 +313,9 @@ async function decodeLengthPrefixedStream(stream, peer) {
|
|
|
233
313
|
buffer = buffer.subarray(4 + length);
|
|
234
314
|
const result = decodePeerMessage(messageBytes);
|
|
235
315
|
if (!result.matched || !isServerPeerSendMessage(result.message)) {
|
|
236
|
-
throw new
|
|
316
|
+
throw new TypeError("Invalid batch response: invalid message.");
|
|
237
317
|
}
|
|
238
|
-
await
|
|
318
|
+
await receive(result.message);
|
|
239
319
|
}
|
|
240
320
|
if (done) {
|
|
241
321
|
break;
|
|
@@ -251,10 +331,14 @@ class DedupeLinkPlugin {
|
|
|
251
331
|
before = ["~batch"];
|
|
252
332
|
groups;
|
|
253
333
|
filter;
|
|
334
|
+
scope;
|
|
335
|
+
wait;
|
|
254
336
|
queue = /* @__PURE__ */ new Map();
|
|
255
337
|
constructor(options) {
|
|
256
338
|
this.groups = options.groups;
|
|
257
|
-
this.filter = options.filter ?? (({ request }) => request.method === "GET");
|
|
339
|
+
this.filter = options.filter ?? (({ request }) => request.method === "GET" || request.method === "QUERY");
|
|
340
|
+
this.scope = options.scope ?? (() => void 0);
|
|
341
|
+
this.wait = options.wait ?? 0;
|
|
258
342
|
}
|
|
259
343
|
init(options) {
|
|
260
344
|
const transportInterceptor = (interceptorOptions) => {
|
|
@@ -265,78 +349,81 @@ class DedupeLinkPlugin {
|
|
|
265
349
|
if (!group) {
|
|
266
350
|
return interceptorOptions.next();
|
|
267
351
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
this.processPendingRequests();
|
|
272
|
-
}
|
|
273
|
-
|
|
352
|
+
const scope = this.scope(interceptorOptions);
|
|
353
|
+
return runWithSignal(interceptorOptions.request.signal, () => new Promise((resolve, reject) => {
|
|
354
|
+
if (!this.queue.size) {
|
|
355
|
+
defer(() => this.processPendingRequests(), this.wait);
|
|
356
|
+
}
|
|
357
|
+
this.enqueue(scope, group, { options: interceptorOptions, resolve, reject, runInOwnContext: captureAsyncContext() });
|
|
358
|
+
}));
|
|
274
359
|
};
|
|
275
360
|
return {
|
|
276
361
|
...options,
|
|
277
362
|
transportInterceptors: [...toArray(options.transportInterceptors), transportInterceptor]
|
|
278
363
|
};
|
|
279
364
|
}
|
|
280
|
-
enqueue(
|
|
281
|
-
let
|
|
365
|
+
enqueue(scope, group, caller) {
|
|
366
|
+
let groups = this.queue.get(scope);
|
|
367
|
+
if (!groups) {
|
|
368
|
+
groups = /* @__PURE__ */ new Map();
|
|
369
|
+
this.queue.set(scope, groups);
|
|
370
|
+
}
|
|
371
|
+
let queue = groups.get(group);
|
|
282
372
|
if (!queue) {
|
|
283
|
-
queue =
|
|
284
|
-
|
|
373
|
+
queue = /* @__PURE__ */ new Map();
|
|
374
|
+
groups.set(group, queue);
|
|
285
375
|
}
|
|
286
|
-
const requestKey = createRequestKey(options.path, options.request);
|
|
287
|
-
const matched = queue.
|
|
376
|
+
const requestKey = createRequestKey(caller.options.path, caller.options.request);
|
|
377
|
+
const matched = queue.get(requestKey);
|
|
288
378
|
if (matched) {
|
|
289
|
-
matched.
|
|
290
|
-
matched.signals.push(options.request.signal);
|
|
291
|
-
matched.resolves.push(resolve);
|
|
292
|
-
matched.rejects.push(reject);
|
|
379
|
+
matched.push(caller);
|
|
293
380
|
return;
|
|
294
381
|
}
|
|
295
|
-
queue.
|
|
296
|
-
requestKey,
|
|
297
|
-
options,
|
|
298
|
-
matchedOptions: [options],
|
|
299
|
-
signals: [options.request.signal],
|
|
300
|
-
resolves: [resolve],
|
|
301
|
-
rejects: [reject]
|
|
302
|
-
});
|
|
382
|
+
queue.set(requestKey, [caller]);
|
|
303
383
|
}
|
|
304
384
|
async processPendingRequests() {
|
|
305
|
-
const pending =
|
|
385
|
+
const pending = [...this.queue.values()];
|
|
306
386
|
this.queue.clear();
|
|
307
387
|
const executions = [];
|
|
308
|
-
for (const
|
|
309
|
-
for (const
|
|
310
|
-
|
|
388
|
+
for (const groups of pending) {
|
|
389
|
+
for (const [group, items] of groups) {
|
|
390
|
+
for (const callers of items.values()) {
|
|
391
|
+
const activeCallers = callers.filter((caller) => !caller.options.request.signal?.aborted);
|
|
392
|
+
const [first] = activeCallers;
|
|
393
|
+
if (first) {
|
|
394
|
+
executions.push(first.runInOwnContext(() => this.execute(group, activeCallers)));
|
|
395
|
+
}
|
|
396
|
+
}
|
|
311
397
|
}
|
|
312
398
|
}
|
|
313
399
|
await Promise.all(executions);
|
|
314
400
|
}
|
|
315
|
-
async execute(group,
|
|
401
|
+
async execute(group, activeCallers) {
|
|
402
|
+
const first = activeCallers[0];
|
|
403
|
+
const matchedOptions = activeCallers.map((caller) => caller.options);
|
|
316
404
|
try {
|
|
317
|
-
if (!shouldDedupe(
|
|
318
|
-
const response2 = await
|
|
319
|
-
|
|
405
|
+
if (!shouldDedupe(matchedOptions)) {
|
|
406
|
+
const response2 = await first.options.next(first.options);
|
|
407
|
+
first.resolve(response2);
|
|
320
408
|
return;
|
|
321
409
|
}
|
|
322
|
-
const context = value(group.context,
|
|
410
|
+
const context = value(group.context, matchedOptions);
|
|
411
|
+
const signals = matchedOptions.map((options) => options.request.signal);
|
|
323
412
|
const request = {
|
|
324
|
-
...
|
|
325
|
-
signal: allAbortSignal(
|
|
413
|
+
...first.options.request,
|
|
414
|
+
signal: allAbortSignal(signals)
|
|
326
415
|
};
|
|
327
|
-
const response = await
|
|
328
|
-
...
|
|
416
|
+
const response = await first.options.next({
|
|
417
|
+
...first.options,
|
|
329
418
|
request,
|
|
330
419
|
signal: request.signal,
|
|
331
420
|
context
|
|
332
421
|
});
|
|
333
|
-
const replicatedResponses = replicateLazyResponse(response,
|
|
334
|
-
|
|
335
|
-
resolve(replicatedResponses.pop());
|
|
336
|
-
}
|
|
422
|
+
const replicatedResponses = replicateLazyResponse(response, signals);
|
|
423
|
+
activeCallers.forEach((caller, index) => caller.resolve(replicatedResponses[index]));
|
|
337
424
|
} catch (error) {
|
|
338
|
-
for (const
|
|
339
|
-
reject(error);
|
|
425
|
+
for (const caller of activeCallers) {
|
|
426
|
+
caller.reject(error);
|
|
340
427
|
}
|
|
341
428
|
}
|
|
342
429
|
}
|
|
@@ -353,42 +440,310 @@ function createRequestKey(path, request) {
|
|
|
353
440
|
url: request.url
|
|
354
441
|
});
|
|
355
442
|
}
|
|
356
|
-
function replicateLazyResponse(response,
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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;
|
|
443
|
+
function replicateLazyResponse(response, signals) {
|
|
444
|
+
let bodiesPromise;
|
|
445
|
+
return signals.map((signal, i) => ({
|
|
446
|
+
...response,
|
|
447
|
+
resolveBody: (hint) => runWithSignal(signal, async () => {
|
|
448
|
+
bodiesPromise ??= response.resolveBody(hint).then((body) => {
|
|
371
449
|
if (isAsyncIteratorObject(body)) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
450
|
+
return replicateAsyncIterator(body, signals.length).map((replica, index) => closeAsyncIteratorOnAbort(replica, signals[index]));
|
|
451
|
+
}
|
|
452
|
+
if (body instanceof ReadableStream) {
|
|
453
|
+
return replicateReadableStream(body, signals.length).map((replica, index) => closeReadableStreamOnAbort(replica, signals[index]));
|
|
454
|
+
}
|
|
455
|
+
return signals.map(() => body);
|
|
456
|
+
});
|
|
457
|
+
return (await bodiesPromise)[i];
|
|
458
|
+
})
|
|
459
|
+
}));
|
|
460
|
+
}
|
|
461
|
+
function closeAsyncIteratorOnAbort(replica, signal) {
|
|
462
|
+
if (!signal) {
|
|
463
|
+
return replica;
|
|
464
|
+
}
|
|
465
|
+
const abort = () => replica.return().catch(() => {
|
|
466
|
+
});
|
|
467
|
+
if (signal.aborted) {
|
|
468
|
+
void abort();
|
|
469
|
+
} else {
|
|
470
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
471
|
+
}
|
|
472
|
+
return wrapAsyncIterator(replica, {
|
|
473
|
+
mapResult(result) {
|
|
474
|
+
throwIfAborted(signal);
|
|
475
|
+
return result;
|
|
476
|
+
},
|
|
477
|
+
onFinish: () => signal.removeEventListener("abort", abort)
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
function closeReadableStreamOnAbort(replica, signal) {
|
|
481
|
+
if (!signal) {
|
|
482
|
+
return replica;
|
|
483
|
+
}
|
|
484
|
+
const reader = replica.getReader();
|
|
485
|
+
const abort = () => reader.cancel(signal.reason).catch(() => {
|
|
486
|
+
});
|
|
487
|
+
if (signal.aborted) {
|
|
488
|
+
void abort();
|
|
489
|
+
} else {
|
|
490
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
491
|
+
}
|
|
492
|
+
return new ReadableStream({
|
|
493
|
+
async pull(controller) {
|
|
494
|
+
try {
|
|
495
|
+
const result = await reader.read();
|
|
496
|
+
throwIfAborted(signal);
|
|
497
|
+
if (result.done) {
|
|
498
|
+
signal.removeEventListener("abort", abort);
|
|
499
|
+
controller.close();
|
|
377
500
|
} else {
|
|
378
|
-
|
|
501
|
+
controller.enqueue(result.value);
|
|
379
502
|
}
|
|
380
|
-
|
|
503
|
+
} catch (error) {
|
|
504
|
+
signal.removeEventListener("abort", abort);
|
|
505
|
+
throw error;
|
|
381
506
|
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
|
|
507
|
+
},
|
|
508
|
+
async cancel(reason) {
|
|
509
|
+
signal.removeEventListener("abort", abort);
|
|
510
|
+
await reader.cancel(reason);
|
|
511
|
+
}
|
|
512
|
+
}, { highWaterMark: 0 });
|
|
385
513
|
}
|
|
386
514
|
function shouldDedupe(items) {
|
|
387
515
|
return items.length >= 2;
|
|
388
516
|
}
|
|
389
517
|
|
|
390
|
-
|
|
518
|
+
const AVG_BYTES_PER_CHAR = 1.2;
|
|
519
|
+
class RequestCompressionLinkPlugin {
|
|
520
|
+
name = "~request-compression";
|
|
521
|
+
/**
|
|
522
|
+
* Compression should be done after batching, to compress the final request
|
|
523
|
+
*/
|
|
524
|
+
after = ["~batch"];
|
|
525
|
+
encoding;
|
|
526
|
+
threshold;
|
|
527
|
+
isCompressibleContentType;
|
|
528
|
+
constructor(options = {}) {
|
|
529
|
+
this.encoding = options.encoding ?? "gzip";
|
|
530
|
+
this.threshold = options.threshold ?? 1024;
|
|
531
|
+
this.isCompressibleContentType = options.isCompressibleContentType ?? isCompressibleContentType;
|
|
532
|
+
}
|
|
533
|
+
init(options) {
|
|
534
|
+
const transportInterceptor = async ({ next, ...interceptorOptions }) => {
|
|
535
|
+
const request = interceptorOptions.request;
|
|
536
|
+
const contentEncoding = flattenStandardHeader(request.headers["content-encoding"])?.trim()?.toLowerCase();
|
|
537
|
+
if (contentEncoding !== void 0) {
|
|
538
|
+
return next();
|
|
539
|
+
}
|
|
540
|
+
if (request.body instanceof ReadableStream) {
|
|
541
|
+
const contentLength = Number(flattenStandardHeader(request.headers["content-length"]));
|
|
542
|
+
if ((!Number.isFinite(contentLength) || contentLength >= this.threshold) && this.isCompressibleContentType(flattenStandardHeader(request.headers["content-type"]), interceptorOptions)) {
|
|
543
|
+
const compressedStream = request.body.pipeThrough(new CompressionStream(this.encoding));
|
|
544
|
+
return next({
|
|
545
|
+
...interceptorOptions,
|
|
546
|
+
request: {
|
|
547
|
+
...interceptorOptions.request,
|
|
548
|
+
body: compressedStream,
|
|
549
|
+
headers: {
|
|
550
|
+
...request.headers,
|
|
551
|
+
"standard-server": "octet-stream",
|
|
552
|
+
"content-length": [],
|
|
553
|
+
"content-encoding": this.encoding
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
} else if (request.body instanceof Blob) {
|
|
559
|
+
if ((!Number.isFinite(request.body.size) || request.body.size >= this.threshold) && this.isCompressibleContentType(request.body.type, interceptorOptions)) {
|
|
560
|
+
const compressedStream = request.body.stream().pipeThrough(new CompressionStream(this.encoding));
|
|
561
|
+
const contentDisposition = request.headers["content-disposition"] ?? generateContentDisposition(
|
|
562
|
+
request.body instanceof File ? request.body.name : "blob"
|
|
563
|
+
);
|
|
564
|
+
return next({
|
|
565
|
+
...interceptorOptions,
|
|
566
|
+
request: {
|
|
567
|
+
...interceptorOptions.request,
|
|
568
|
+
body: compressedStream,
|
|
569
|
+
headers: {
|
|
570
|
+
...request.headers,
|
|
571
|
+
"standard-server": "file",
|
|
572
|
+
"content-type": request.body.type,
|
|
573
|
+
"content-length": [],
|
|
574
|
+
"content-disposition": contentDisposition,
|
|
575
|
+
"content-encoding": this.encoding
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
} else if (request.body instanceof FormData) {
|
|
581
|
+
const PART_OVERHEAD = 64;
|
|
582
|
+
let contentLength = 0;
|
|
583
|
+
for (const [key, value] of request.body) {
|
|
584
|
+
contentLength += PART_OVERHEAD + key.length;
|
|
585
|
+
if (value instanceof Blob) {
|
|
586
|
+
if (!Number.isFinite(value.size)) {
|
|
587
|
+
if (!this.isCompressibleContentType(value.type, interceptorOptions)) {
|
|
588
|
+
contentLength = -Infinity;
|
|
589
|
+
break;
|
|
590
|
+
}
|
|
591
|
+
contentLength = Infinity;
|
|
592
|
+
} else {
|
|
593
|
+
contentLength += this.isCompressibleContentType(value.type, interceptorOptions) ? value.size : -value.size;
|
|
594
|
+
}
|
|
595
|
+
} else {
|
|
596
|
+
contentLength += value.length * AVG_BYTES_PER_CHAR;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (contentLength >= this.threshold) {
|
|
600
|
+
const res = new Response(request.body);
|
|
601
|
+
const compressedStream = res.body.pipeThrough(new CompressionStream(this.encoding));
|
|
602
|
+
return next({
|
|
603
|
+
...interceptorOptions,
|
|
604
|
+
request: {
|
|
605
|
+
...interceptorOptions.request,
|
|
606
|
+
body: compressedStream,
|
|
607
|
+
headers: {
|
|
608
|
+
...request.headers,
|
|
609
|
+
"standard-server": [],
|
|
610
|
+
"content-type": res.headers.get("content-type"),
|
|
611
|
+
"content-length": [],
|
|
612
|
+
"content-encoding": this.encoding
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
} else if (request.body instanceof URLSearchParams) {
|
|
618
|
+
const string = request.body.toString();
|
|
619
|
+
if (string.length * AVG_BYTES_PER_CHAR >= this.threshold) {
|
|
620
|
+
const compressedStream = new Blob([string]).stream().pipeThrough(new CompressionStream(this.encoding));
|
|
621
|
+
return next({
|
|
622
|
+
...interceptorOptions,
|
|
623
|
+
request: {
|
|
624
|
+
...interceptorOptions.request,
|
|
625
|
+
body: compressedStream,
|
|
626
|
+
headers: {
|
|
627
|
+
...request.headers,
|
|
628
|
+
"standard-server": [],
|
|
629
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
630
|
+
"content-length": [],
|
|
631
|
+
"content-encoding": this.encoding
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
} else if (request.body !== void 0 && !isAsyncIteratorObject(request.body)) {
|
|
637
|
+
const string = stringifyJSON(request.body);
|
|
638
|
+
if (string.length * AVG_BYTES_PER_CHAR >= this.threshold) {
|
|
639
|
+
const compressedStream = new Blob([string]).stream().pipeThrough(new CompressionStream(this.encoding));
|
|
640
|
+
return next({
|
|
641
|
+
...interceptorOptions,
|
|
642
|
+
request: {
|
|
643
|
+
...interceptorOptions.request,
|
|
644
|
+
body: compressedStream,
|
|
645
|
+
headers: {
|
|
646
|
+
...request.headers,
|
|
647
|
+
"standard-server": [],
|
|
648
|
+
"content-type": "application/json",
|
|
649
|
+
"content-length": [],
|
|
650
|
+
"content-encoding": this.encoding
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return next();
|
|
657
|
+
};
|
|
658
|
+
return {
|
|
659
|
+
...options,
|
|
660
|
+
transportInterceptors: [
|
|
661
|
+
...toArray(options.transportInterceptors),
|
|
662
|
+
transportInterceptor
|
|
663
|
+
]
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const MAX_CONTENT_ENCODINGS = 5;
|
|
669
|
+
class ResponseCompressionLinkPlugin {
|
|
670
|
+
name = "~response-compression";
|
|
671
|
+
/**
|
|
672
|
+
* Decompression should wrap the final batch response instead of sub-responses.
|
|
673
|
+
*/
|
|
674
|
+
after = ["~batch"];
|
|
675
|
+
encodings;
|
|
676
|
+
constructor(options = {}) {
|
|
677
|
+
this.encodings = options.encodings ?? ["gzip", "deflate"];
|
|
678
|
+
}
|
|
679
|
+
init(options) {
|
|
680
|
+
const acceptEncodingHeader = this.encodings.join(", ");
|
|
681
|
+
const transportInterceptor = async ({ next, ...interceptorOptions }) => {
|
|
682
|
+
const response = await next({
|
|
683
|
+
...interceptorOptions,
|
|
684
|
+
request: {
|
|
685
|
+
...interceptorOptions.request,
|
|
686
|
+
headers: {
|
|
687
|
+
...interceptorOptions.request.headers,
|
|
688
|
+
"accept-encoding": acceptEncodingHeader
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
const encodings = parseContentEncodings(
|
|
693
|
+
flattenStandardHeader(response.headers["content-encoding"])
|
|
694
|
+
);
|
|
695
|
+
if (encodings.length === 0 || !encodings.every(isSupportedEncoding)) {
|
|
696
|
+
return response;
|
|
697
|
+
}
|
|
698
|
+
const decompressedHeaders = {
|
|
699
|
+
...response.headers,
|
|
700
|
+
"content-length": void 0,
|
|
701
|
+
"content-encoding": void 0
|
|
702
|
+
};
|
|
703
|
+
return {
|
|
704
|
+
...response,
|
|
705
|
+
headers: decompressedHeaders,
|
|
706
|
+
async resolveBody(hint) {
|
|
707
|
+
const stream = await response.resolveBody("octet-stream");
|
|
708
|
+
if (!(stream instanceof ReadableStream)) {
|
|
709
|
+
return stream;
|
|
710
|
+
}
|
|
711
|
+
if (encodings.length > MAX_CONTENT_ENCODINGS) {
|
|
712
|
+
throw new TypeError("Too many content encodings.");
|
|
713
|
+
}
|
|
714
|
+
let decompressedStream = stream;
|
|
715
|
+
for (let i = encodings.length - 1; i >= 0; i--) {
|
|
716
|
+
decompressedStream = decompressedStream.pipeThrough(
|
|
717
|
+
new DecompressionStream(encodings[i])
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
const fetchResponse = new Response(decompressedStream, {
|
|
721
|
+
headers: toFetchHeaders(decompressedHeaders)
|
|
722
|
+
});
|
|
723
|
+
return toStandardBody(fetchResponse, { hint });
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
};
|
|
727
|
+
return {
|
|
728
|
+
...options,
|
|
729
|
+
transportInterceptors: [
|
|
730
|
+
...toArray(options.transportInterceptors),
|
|
731
|
+
transportInterceptor
|
|
732
|
+
]
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
const SUPPORTED_ENCODINGS = ["gzip", "deflate", "deflate-raw"];
|
|
737
|
+
function isSupportedEncoding(encoding) {
|
|
738
|
+
return SUPPORTED_ENCODINGS.includes(encoding);
|
|
391
739
|
}
|
|
740
|
+
function parseContentEncodings(header) {
|
|
741
|
+
if (header === void 0) {
|
|
742
|
+
return [];
|
|
743
|
+
}
|
|
744
|
+
return header.split(",").map((part) => part.trim().toLowerCase());
|
|
745
|
+
}
|
|
746
|
+
|
|
392
747
|
class RetryLinkPlugin {
|
|
393
748
|
defaultRetry;
|
|
394
749
|
defaultRetryDelay;
|
|
@@ -422,6 +777,7 @@ class RetryLinkPlugin {
|
|
|
422
777
|
let currentError = initialError;
|
|
423
778
|
while (true) {
|
|
424
779
|
const updatedCallOptions = { ...callOptions, lastEventId };
|
|
780
|
+
let retryDelayMs = 0;
|
|
425
781
|
if (currentError) {
|
|
426
782
|
if (attempt > maxAttempts) {
|
|
427
783
|
throw currentError.error;
|
|
@@ -439,16 +795,11 @@ class RetryLinkPlugin {
|
|
|
439
795
|
if (!shouldRetryBool) {
|
|
440
796
|
throw currentError.error;
|
|
441
797
|
}
|
|
798
|
+
retryDelayMs = await value(retryDelay, attemptOptions);
|
|
442
799
|
callback = onRetry?.(attemptOptions);
|
|
443
800
|
}
|
|
444
801
|
try {
|
|
445
802
|
if (currentError) {
|
|
446
|
-
const retryDelayMs = await value(retryDelay, {
|
|
447
|
-
...updatedCallOptions,
|
|
448
|
-
attempt,
|
|
449
|
-
error: currentError.error,
|
|
450
|
-
lastEventRetry
|
|
451
|
-
});
|
|
452
803
|
await sleep(retryDelayMs, { signal: updatedCallOptions.signal });
|
|
453
804
|
attempt++;
|
|
454
805
|
}
|
|
@@ -484,13 +835,13 @@ class RetryLinkPlugin {
|
|
|
484
835
|
const meta = getEventMeta(error);
|
|
485
836
|
lastEventId = meta?.id ?? lastEventId;
|
|
486
837
|
lastEventRetry = meta?.retry ?? lastEventRetry;
|
|
487
|
-
const
|
|
488
|
-
if (!isAsyncIteratorObject(
|
|
489
|
-
throw new
|
|
490
|
-
"RetryLinkPlugin: Expected an
|
|
838
|
+
const asyncIteratorObject = await callNext({ error });
|
|
839
|
+
if (!isAsyncIteratorObject(asyncIteratorObject)) {
|
|
840
|
+
throw new TypeError(
|
|
841
|
+
"RetryLinkPlugin: Expected an AsyncIteratorObject, got a different type."
|
|
491
842
|
);
|
|
492
843
|
}
|
|
493
|
-
current =
|
|
844
|
+
current = asyncIteratorObject;
|
|
494
845
|
if (isIteratorAborted) {
|
|
495
846
|
await current.return?.();
|
|
496
847
|
throw error;
|
|
@@ -571,4 +922,35 @@ function parseRetryAfterHeader(value2) {
|
|
|
571
922
|
return void 0;
|
|
572
923
|
}
|
|
573
924
|
|
|
574
|
-
|
|
925
|
+
class TimeoutLinkPlugin {
|
|
926
|
+
timeout;
|
|
927
|
+
name = "~timeout";
|
|
928
|
+
/**
|
|
929
|
+
* Should abort if the total retry time exceeds the configured timeout
|
|
930
|
+
*/
|
|
931
|
+
after = ["~retry"];
|
|
932
|
+
constructor(options) {
|
|
933
|
+
this.timeout = options.timeout;
|
|
934
|
+
}
|
|
935
|
+
init(options) {
|
|
936
|
+
const interceptor = async (interceptorOptions) => {
|
|
937
|
+
const timeoutMs = value(this.timeout, interceptorOptions);
|
|
938
|
+
if (timeoutMs === null || timeoutMs === void 0) {
|
|
939
|
+
return interceptorOptions.next();
|
|
940
|
+
}
|
|
941
|
+
const controller = new AbortController();
|
|
942
|
+
const timeoutId = setTimeout(() => {
|
|
943
|
+
controller.abort(new AbortError(`Request timed out after ${timeoutMs}ms`));
|
|
944
|
+
}, timeoutMs);
|
|
945
|
+
const signal = anyAbortSignal([interceptorOptions.signal, controller.signal]);
|
|
946
|
+
try {
|
|
947
|
+
return await interceptorOptions.next({ ...interceptorOptions, signal });
|
|
948
|
+
} finally {
|
|
949
|
+
clearTimeout(timeoutId);
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
return { ...options, interceptors: [interceptor, ...toArray(options.interceptors)] };
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
export { BatchLinkPlugin, RetryLinkPlugin as ClientRetryPlugin, DedupeLinkPlugin, DedupeLinkPlugin as DedupeRequestsPlugin, RequestCompressionLinkPlugin, ResponseCompressionLinkPlugin, RetryAfterLinkPlugin, RetryAfterLinkPlugin as RetryAfterPlugin, RetryLinkPlugin, TimeoutLinkPlugin };
|