alchemy 0.61.0 → 0.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -2
- package/bin/alchemy.js +1406 -1464
- package/bin/commands/init.ts +29 -2
- package/bin/services/execute-alchemy.ts +1 -2
- package/lib/alchemy.d.ts +2 -2
- package/lib/alchemy.d.ts.map +1 -1
- package/lib/alchemy.js.map +1 -1
- package/lib/aws/client-props.d.ts +149 -0
- package/lib/aws/client-props.d.ts.map +1 -0
- package/lib/aws/client-props.js +1 -0
- package/lib/aws/client-props.js.map +1 -0
- package/lib/aws/credentials.d.ts +68 -5
- package/lib/aws/credentials.d.ts.map +1 -1
- package/lib/aws/credentials.js +148 -0
- package/lib/aws/credentials.js.map +1 -1
- package/lib/aws/ec2/internet-gateway-attachment.d.ts +57 -3
- package/lib/aws/ec2/internet-gateway-attachment.d.ts.map +1 -1
- package/lib/aws/ec2/internet-gateway-attachment.js +57 -3
- package/lib/aws/ec2/internet-gateway-attachment.js.map +1 -1
- package/lib/aws/ec2/internet-gateway.d.ts +68 -40
- package/lib/aws/ec2/internet-gateway.d.ts.map +1 -1
- package/lib/aws/ec2/internet-gateway.js +68 -40
- package/lib/aws/ec2/internet-gateway.js.map +1 -1
- package/lib/aws/ec2/nat-gateway.d.ts +99 -18
- package/lib/aws/ec2/nat-gateway.d.ts.map +1 -1
- package/lib/aws/ec2/nat-gateway.js +113 -37
- package/lib/aws/ec2/nat-gateway.js.map +1 -1
- package/lib/aws/ec2/route-table-association.d.ts +1 -1
- package/lib/aws/ec2/route-table-association.js +1 -1
- package/lib/aws/ec2/route-table.d.ts +64 -1
- package/lib/aws/ec2/route-table.d.ts.map +1 -1
- package/lib/aws/ec2/route-table.js +64 -1
- package/lib/aws/ec2/route-table.js.map +1 -1
- package/lib/aws/ec2/route.d.ts +60 -1
- package/lib/aws/ec2/route.d.ts.map +1 -1
- package/lib/aws/ec2/route.js +60 -1
- package/lib/aws/ec2/route.js.map +1 -1
- package/lib/aws/ec2/security-group.d.ts +96 -3
- package/lib/aws/ec2/security-group.d.ts.map +1 -1
- package/lib/aws/ec2/security-group.js +96 -3
- package/lib/aws/ec2/security-group.js.map +1 -1
- package/lib/aws/ec2/subnet.d.ts +61 -3
- package/lib/aws/ec2/subnet.d.ts.map +1 -1
- package/lib/aws/ec2/subnet.js +61 -3
- package/lib/aws/ec2/subnet.js.map +1 -1
- package/lib/aws/ec2/utils.d.ts +10 -2
- package/lib/aws/ec2/utils.d.ts.map +1 -1
- package/lib/aws/ec2/utils.js +68 -7
- package/lib/aws/ec2/utils.js.map +1 -1
- package/lib/aws/ec2/vpc.d.ts +50 -2
- package/lib/aws/ec2/vpc.d.ts.map +1 -1
- package/lib/aws/ec2/vpc.js +50 -2
- package/lib/aws/ec2/vpc.js.map +1 -1
- package/lib/aws/index.d.ts +2 -0
- package/lib/aws/index.d.ts.map +1 -1
- package/lib/aws/index.js +2 -0
- package/lib/aws/index.js.map +1 -1
- package/lib/cloudflare/api-response.js +2 -2
- package/lib/cloudflare/api-response.js.map +1 -1
- package/lib/cloudflare/api.d.ts +19 -0
- package/lib/cloudflare/api.d.ts.map +1 -1
- package/lib/cloudflare/api.js +4 -0
- package/lib/cloudflare/api.js.map +1 -1
- package/lib/cloudflare/assets.d.ts.map +1 -1
- package/lib/cloudflare/assets.js +3 -4
- package/lib/cloudflare/assets.js.map +1 -1
- package/lib/cloudflare/bucket.d.ts +5 -3
- package/lib/cloudflare/bucket.d.ts.map +1 -1
- package/lib/cloudflare/bucket.js +22 -18
- package/lib/cloudflare/bucket.js.map +1 -1
- package/lib/cloudflare/bundle/plugin-compat-warning.js +7 -7
- package/lib/cloudflare/bundle/plugin-compat-warning.js.map +1 -1
- package/lib/cloudflare/cloudflare-env-proxy.js.map +1 -1
- package/lib/cloudflare/kv-namespace.d.ts +2 -2
- package/lib/cloudflare/kv-namespace.d.ts.map +1 -1
- package/lib/cloudflare/kv-namespace.js.map +1 -1
- package/lib/cloudflare/oauth.js +3 -3
- package/lib/cloudflare/oauth.js.map +1 -1
- package/lib/cloudflare/quick-tunnel.d.ts +1 -1
- package/lib/cloudflare/website.d.ts.map +1 -1
- package/lib/cloudflare/website.js +26 -97
- package/lib/cloudflare/website.js.map +1 -1
- package/lib/cloudflare/worker-bundle.d.ts.map +1 -1
- package/lib/cloudflare/worker-bundle.js +9 -8
- package/lib/cloudflare/worker-bundle.js.map +1 -1
- package/lib/cloudflare/worker-tail.d.ts.map +1 -1
- package/lib/cloudflare/worker-tail.js +6 -6
- package/lib/cloudflare/worker-tail.js.map +1 -1
- package/lib/resource.d.ts +1 -0
- package/lib/resource.d.ts.map +1 -1
- package/lib/resource.js +13 -2
- package/lib/resource.js.map +1 -1
- package/lib/scope.d.ts +24 -23
- package/lib/scope.d.ts.map +1 -1
- package/lib/scope.js +25 -20
- package/lib/scope.js.map +1 -1
- package/lib/state/cloudflare-state-store.d.ts.map +1 -1
- package/lib/state/cloudflare-state-store.js +2 -1
- package/lib/state/cloudflare-state-store.js.map +1 -1
- package/lib/util/idempotent-spawn.d.ts +10 -8
- package/lib/util/idempotent-spawn.d.ts.map +1 -1
- package/lib/util/idempotent-spawn.js +97 -78
- package/lib/util/idempotent-spawn.js.map +1 -1
- package/lib/util/index.d.ts +2 -0
- package/lib/util/index.d.ts.map +1 -0
- package/lib/util/index.js +2 -0
- package/lib/util/index.js.map +1 -0
- package/lib/util/safe-fetch.d.ts +24 -0
- package/lib/util/safe-fetch.d.ts.map +1 -1
- package/lib/util/safe-fetch.js +72 -0
- package/lib/util/safe-fetch.js.map +1 -1
- package/package.json +8 -5
- package/src/alchemy.ts +2 -2
- package/src/aws/client-props.ts +156 -0
- package/src/aws/credentials.ts +165 -4
- package/src/aws/ec2/internet-gateway-attachment.ts +59 -4
- package/src/aws/ec2/internet-gateway.ts +70 -41
- package/src/aws/ec2/nat-gateway.ts +123 -33
- package/src/aws/ec2/route-table-association.ts +1 -1
- package/src/aws/ec2/route-table.ts +66 -2
- package/src/aws/ec2/route.ts +62 -2
- package/src/aws/ec2/security-group.ts +98 -4
- package/src/aws/ec2/subnet.ts +63 -4
- package/src/aws/ec2/utils.ts +73 -7
- package/src/aws/ec2/vpc.ts +52 -3
- package/src/aws/index.ts +2 -0
- package/src/cloudflare/api-response.ts +2 -2
- package/src/cloudflare/api.ts +23 -0
- package/src/cloudflare/assets.ts +3 -4
- package/src/cloudflare/bucket.ts +27 -25
- package/src/cloudflare/bundle/plugin-compat-warning.ts +7 -7
- package/src/cloudflare/cloudflare-env-proxy.ts +2 -2
- package/src/cloudflare/kv-namespace.ts +26 -27
- package/src/cloudflare/oauth.ts +3 -3
- package/src/cloudflare/website.ts +30 -111
- package/src/cloudflare/worker-bundle.ts +9 -8
- package/src/cloudflare/worker-tail.ts +6 -8
- package/src/resource.ts +19 -2
- package/src/scope.ts +73 -43
- package/src/state/cloudflare-state-store.ts +2 -1
- package/src/util/idempotent-spawn.ts +117 -100
- package/src/util/index.ts +1 -0
- package/src/util/safe-fetch.ts +114 -0
|
@@ -1,8 +1,25 @@
|
|
|
1
|
-
import find from "find-process";
|
|
2
1
|
import { spawn } from "node:child_process";
|
|
3
2
|
import fs from "node:fs";
|
|
4
3
|
import fsp from "node:fs/promises";
|
|
5
4
|
import path from "node:path";
|
|
5
|
+
import { AsyncMutex } from "./mutex.ts";
|
|
6
|
+
|
|
7
|
+
export interface IdempotentSpawnOptions {
|
|
8
|
+
cmd: string;
|
|
9
|
+
cwd?: string;
|
|
10
|
+
env?: Record<string, string>;
|
|
11
|
+
log: string;
|
|
12
|
+
stateFile?: string;
|
|
13
|
+
overlapBytes?: number;
|
|
14
|
+
resume?: boolean;
|
|
15
|
+
isSameProcess?: (pid: number) => Promise<boolean>;
|
|
16
|
+
processName?: string;
|
|
17
|
+
extract?: (line: string) => string | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* If true, the child's stdout and stderr will not be mirrored to this process's console.
|
|
20
|
+
*/
|
|
21
|
+
quiet?: boolean;
|
|
22
|
+
}
|
|
6
23
|
|
|
7
24
|
/**
|
|
8
25
|
* Idempotently ensure a long-lived child is running with stdout/stderr -> files,
|
|
@@ -13,6 +30,7 @@ import path from "node:path";
|
|
|
13
30
|
export async function idempotentSpawn({
|
|
14
31
|
cmd,
|
|
15
32
|
cwd,
|
|
33
|
+
env,
|
|
16
34
|
stateFile = "state.json",
|
|
17
35
|
overlapBytes = 0,
|
|
18
36
|
resume = false,
|
|
@@ -21,25 +39,9 @@ export async function idempotentSpawn({
|
|
|
21
39
|
extract,
|
|
22
40
|
isSameProcess,
|
|
23
41
|
quiet = false,
|
|
24
|
-
}: {
|
|
25
|
-
cmd: string;
|
|
26
|
-
cwd?: string;
|
|
27
|
-
log: string;
|
|
28
|
-
stateFile?: string;
|
|
29
|
-
overlapBytes?: number;
|
|
30
|
-
resume?: boolean;
|
|
31
|
-
isSameProcess?: (pid: number) => Promise<boolean>;
|
|
32
|
-
processName?: string;
|
|
33
|
-
extract?: (line: string) => string | undefined;
|
|
34
|
-
/**
|
|
35
|
-
* If true, the child's stdout and stderr will not be mirrored to this process's console.
|
|
36
|
-
*/
|
|
37
|
-
quiet?: boolean;
|
|
38
|
-
}): Promise<string | undefined> {
|
|
42
|
+
}: IdempotentSpawnOptions): Promise<string | undefined> {
|
|
39
43
|
if (!processName && !isSameProcess) {
|
|
40
|
-
|
|
41
|
-
"Either processName or isSameProcess must be provided to resume a process",
|
|
42
|
-
);
|
|
44
|
+
processName = cmd.split(" ")[0];
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
const outPath = log;
|
|
@@ -52,13 +54,22 @@ export async function idempotentSpawn({
|
|
|
52
54
|
Promise.withResolvers<string | undefined>();
|
|
53
55
|
|
|
54
56
|
if (!extract) {
|
|
55
|
-
console.warn(
|
|
56
|
-
"No extract function provided, will not return a value. This is probably a bug.",
|
|
57
|
-
);
|
|
58
57
|
resolveExtracted(undefined);
|
|
59
58
|
}
|
|
60
59
|
|
|
61
|
-
|
|
60
|
+
await ensureChildRunning();
|
|
61
|
+
|
|
62
|
+
const write = quiet
|
|
63
|
+
? () => false
|
|
64
|
+
: (buf: Buffer) => process.stdout.write(buf);
|
|
65
|
+
|
|
66
|
+
// Start followers (stdout/stderr) and mirror to this process
|
|
67
|
+
await followFilePersisted(outPath, {
|
|
68
|
+
stateKey: `${path.resolve(outPath)}::stdout`,
|
|
69
|
+
write,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return extracted;
|
|
62
73
|
|
|
63
74
|
function isPidAlive(pid: number) {
|
|
64
75
|
if (!pid || Number.isNaN(pid)) return false;
|
|
@@ -96,7 +107,7 @@ export async function idempotentSpawn({
|
|
|
96
107
|
shell: true,
|
|
97
108
|
cwd,
|
|
98
109
|
stdio: ["ignore", out.fd, out.fd], // stdout/stderr -> files (OS-level)
|
|
99
|
-
env
|
|
110
|
+
env,
|
|
100
111
|
detached: false,
|
|
101
112
|
});
|
|
102
113
|
|
|
@@ -117,6 +128,7 @@ export async function idempotentSpawn({
|
|
|
117
128
|
if (isPidAlive(pid)) return pid;
|
|
118
129
|
if (await isSameProcess?.(pid)) return pid;
|
|
119
130
|
if (processName) {
|
|
131
|
+
const { default: find } = await import("find-process");
|
|
120
132
|
const processes = await find("pid", pid);
|
|
121
133
|
if (processes.length > 1) {
|
|
122
134
|
console.warn(
|
|
@@ -124,7 +136,9 @@ export async function idempotentSpawn({
|
|
|
124
136
|
);
|
|
125
137
|
}
|
|
126
138
|
if (processes.length > 0) {
|
|
127
|
-
|
|
139
|
+
if (processes[0].name.startsWith(processName)) {
|
|
140
|
+
return pid;
|
|
141
|
+
}
|
|
128
142
|
}
|
|
129
143
|
}
|
|
130
144
|
}
|
|
@@ -140,9 +154,9 @@ export async function idempotentSpawn({
|
|
|
140
154
|
async function followFilePersisted(
|
|
141
155
|
logPath: string,
|
|
142
156
|
{
|
|
143
|
-
write
|
|
157
|
+
write,
|
|
144
158
|
chunkSize = 64 * 1024,
|
|
145
|
-
tickMs = 100,
|
|
159
|
+
// tickMs = 100,
|
|
146
160
|
}: {
|
|
147
161
|
stateKey: string;
|
|
148
162
|
write: (buf: Buffer) => boolean;
|
|
@@ -168,64 +182,23 @@ export async function idempotentSpawn({
|
|
|
168
182
|
: st.size;
|
|
169
183
|
|
|
170
184
|
let closed = false;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
async function drain() {
|
|
190
|
-
while (true) {
|
|
191
|
-
const cur = await fh.stat().catch(() => null);
|
|
192
|
-
if (!cur) break;
|
|
193
|
-
|
|
194
|
-
if (cur.size < offset) offset = 0; // truncated
|
|
195
|
-
|
|
196
|
-
if (offset > 0 && extract) {
|
|
197
|
-
// read from start to offset
|
|
198
|
-
const fullBuffer = Buffer.allocUnsafe(offset);
|
|
199
|
-
await fh.read({
|
|
200
|
-
position: 0,
|
|
201
|
-
buffer: fullBuffer,
|
|
202
|
-
length: offset,
|
|
203
|
-
});
|
|
204
|
-
const content = fullBuffer.toString("utf8");
|
|
205
|
-
const lines = content.split("\n");
|
|
206
|
-
for (const line of lines) {
|
|
207
|
-
const extracted = extract(line);
|
|
208
|
-
if (extracted) {
|
|
209
|
-
resolveExtracted(extracted);
|
|
210
|
-
break;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const toRead = Math.min(chunkSize, cur.size - offset);
|
|
216
|
-
if (toRead <= 0) break;
|
|
217
|
-
|
|
218
|
-
const { bytesRead, buffer } = await fh.read({
|
|
219
|
-
position: offset,
|
|
220
|
-
buffer: Buffer.allocUnsafe(toRead),
|
|
221
|
-
length: toRead,
|
|
222
|
-
});
|
|
223
|
-
if (!bytesRead) break;
|
|
224
|
-
|
|
225
|
-
offset += bytesRead;
|
|
226
|
-
write(buffer.subarray(0, bytesRead));
|
|
227
|
-
}
|
|
228
|
-
await persist();
|
|
185
|
+
const drainMutex = new AsyncMutex();
|
|
186
|
+
|
|
187
|
+
// we read the log file and stream in chunks, not lines
|
|
188
|
+
// so we need to keep track of the remainder of the last line
|
|
189
|
+
let remainder: string | undefined;
|
|
190
|
+
|
|
191
|
+
// if we are resuming, then re-parse the log lines to extract the value
|
|
192
|
+
if (offset > 0) {
|
|
193
|
+
// read from start to offset
|
|
194
|
+
const fullBuffer = Buffer.allocUnsafe(offset);
|
|
195
|
+
await fh.read({
|
|
196
|
+
position: 0,
|
|
197
|
+
buffer: fullBuffer,
|
|
198
|
+
length: offset,
|
|
199
|
+
});
|
|
200
|
+
const content = fullBuffer.toString("utf8");
|
|
201
|
+
parse(content);
|
|
229
202
|
}
|
|
230
203
|
|
|
231
204
|
await drain();
|
|
@@ -263,24 +236,68 @@ export async function idempotentSpawn({
|
|
|
263
236
|
await fh.close().catch(() => {});
|
|
264
237
|
await persist();
|
|
265
238
|
};
|
|
266
|
-
}
|
|
267
239
|
|
|
268
|
-
|
|
240
|
+
async function persist() {
|
|
241
|
+
try {
|
|
242
|
+
const cur = await fh.stat();
|
|
243
|
+
state = {
|
|
244
|
+
...state,
|
|
245
|
+
offset,
|
|
246
|
+
ino: Number(cur.ino ?? ino),
|
|
247
|
+
dev: Number(cur.dev ?? dev),
|
|
248
|
+
size: cur.size,
|
|
249
|
+
mtimeMs: cur.mtimeMs,
|
|
250
|
+
};
|
|
251
|
+
await writeJsonAtomic(stateFile, state);
|
|
252
|
+
} catch {
|
|
253
|
+
// If the file vanished mid-rotation, we'll catch up on the next event.
|
|
254
|
+
}
|
|
255
|
+
}
|
|
269
256
|
|
|
270
|
-
|
|
257
|
+
function parse(content: string) {
|
|
258
|
+
const lines = content.split("\n");
|
|
259
|
+
if (extract) {
|
|
260
|
+
if (lines.length > 0) {
|
|
261
|
+
remainder = lines[lines.length - 1];
|
|
262
|
+
}
|
|
263
|
+
// parse all lines except the last one (which may be a partial line)
|
|
264
|
+
for (const line of lines.slice(0, -1)) {
|
|
265
|
+
const extracted = extract(line);
|
|
266
|
+
if (extracted) {
|
|
267
|
+
resolveExtracted(extracted);
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
271
273
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
274
|
+
function drain() {
|
|
275
|
+
return drainMutex.lock(async () => {
|
|
276
|
+
while (true) {
|
|
277
|
+
const cur = await fh.stat().catch(() => null);
|
|
278
|
+
if (!cur) break;
|
|
276
279
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
write,
|
|
281
|
-
});
|
|
282
|
-
}
|
|
280
|
+
if (cur.size < offset) offset = 0; // truncated
|
|
281
|
+
const toRead = Math.min(chunkSize, cur.size - offset);
|
|
282
|
+
if (toRead <= 0) break;
|
|
283
283
|
|
|
284
|
-
|
|
285
|
-
|
|
284
|
+
const { bytesRead, buffer } = await fh.read({
|
|
285
|
+
position: offset,
|
|
286
|
+
buffer: Buffer.allocUnsafe(toRead),
|
|
287
|
+
length: toRead,
|
|
288
|
+
});
|
|
289
|
+
if (!bytesRead) break;
|
|
290
|
+
|
|
291
|
+
const chunk = buffer.subarray(0, bytesRead);
|
|
292
|
+
|
|
293
|
+
// write the newly read content to the stream
|
|
294
|
+
write(chunk);
|
|
295
|
+
// parse the previous remainder and the new chunk
|
|
296
|
+
parse(`${remainder || ""}${chunk.toString("utf8")}`);
|
|
297
|
+
offset += bytesRead;
|
|
298
|
+
}
|
|
299
|
+
await persist();
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
286
303
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./safe-fetch.ts";
|
package/src/util/safe-fetch.ts
CHANGED
|
@@ -69,3 +69,117 @@ async function dispatcher() {
|
|
|
69
69
|
}
|
|
70
70
|
return agent.value;
|
|
71
71
|
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Fetch with exponential backoff retry logic for 404 responses, expecting 200 OK.
|
|
75
|
+
* This is a convenience wrapper around fetchAndExpectStatus with expectedStatus=200.
|
|
76
|
+
*
|
|
77
|
+
* @param input URL or Request object
|
|
78
|
+
* @param init RequestInit options
|
|
79
|
+
* @param maxAttempts Maximum number of retry attempts (default: 10)
|
|
80
|
+
* @param maxWaitTime Maximum total wait time in milliseconds (default: 30000)
|
|
81
|
+
* @returns The successful Response
|
|
82
|
+
*/
|
|
83
|
+
export async function fetchAndExpectOK(
|
|
84
|
+
input: string | URL | Request,
|
|
85
|
+
init?: RequestInit,
|
|
86
|
+
maxAttempts = 10,
|
|
87
|
+
maxWaitTime = 30000,
|
|
88
|
+
): Promise<Response> {
|
|
89
|
+
return fetchAndExpectStatus(input, init, 200, maxAttempts, maxWaitTime);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Fetch and expect a specific status code with retry logic.
|
|
94
|
+
* Cloudflare's control plane is eventually consistent, so resources may
|
|
95
|
+
* return 404 immediately after creation before becoming available.
|
|
96
|
+
*
|
|
97
|
+
* @param input URL or Request object
|
|
98
|
+
* @param init RequestInit options
|
|
99
|
+
* @param expectedStatus Expected HTTP status code (default: 200)
|
|
100
|
+
* @param maxAttempts Maximum number of retry attempts (default: 10)
|
|
101
|
+
* @param maxWaitTime Maximum total wait time in milliseconds (default: 30000)
|
|
102
|
+
* @returns The successful Response
|
|
103
|
+
*/
|
|
104
|
+
export async function fetchAndExpectStatus(
|
|
105
|
+
input: string | URL | Request,
|
|
106
|
+
init?: RequestInit,
|
|
107
|
+
expectedStatus = 200,
|
|
108
|
+
maxAttempts = 20,
|
|
109
|
+
maxWaitTime = 30000,
|
|
110
|
+
): Promise<Response> {
|
|
111
|
+
let attempt = 0;
|
|
112
|
+
let lastError: Error | undefined;
|
|
113
|
+
|
|
114
|
+
while (attempt < maxAttempts) {
|
|
115
|
+
try {
|
|
116
|
+
const response = await safeFetch(input, init);
|
|
117
|
+
|
|
118
|
+
// If we get the expected status, return it
|
|
119
|
+
if (response.status === expectedStatus) {
|
|
120
|
+
return response;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// If we get a 500, 404 when expecting something else, retry with backoff
|
|
124
|
+
if (
|
|
125
|
+
response.status >= 500 ||
|
|
126
|
+
(response.status === 404 && expectedStatus !== 404)
|
|
127
|
+
) {
|
|
128
|
+
attempt++;
|
|
129
|
+
|
|
130
|
+
if (attempt >= maxAttempts) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`Expected status ${expectedStatus} but got 404 after ${maxAttempts} attempts. This may indicate eventual consistency issues.`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const backoffTime = Math.min(attempt * 1500, maxWaitTime / maxAttempts);
|
|
137
|
+
|
|
138
|
+
console.log(
|
|
139
|
+
`Expected status ${expectedStatus} but got 404, retrying in ${backoffTime}ms (attempt ${attempt}/${maxAttempts})...`,
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
await new Promise((resolve) => setTimeout(resolve, backoffTime));
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// For other status codes, throw immediately
|
|
147
|
+
throw new Error(
|
|
148
|
+
`Expected status ${expectedStatus} but got ${response.status}: ${response.statusText}`,
|
|
149
|
+
);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (error instanceof Error) {
|
|
152
|
+
lastError = error;
|
|
153
|
+
|
|
154
|
+
// If it's not an HTTP error, retry with backoff
|
|
155
|
+
if (
|
|
156
|
+
!error.message.startsWith("Expected status") &&
|
|
157
|
+
!error.message.startsWith("HTTP ")
|
|
158
|
+
) {
|
|
159
|
+
attempt++;
|
|
160
|
+
|
|
161
|
+
if (attempt >= maxAttempts) {
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const backoffTime = Math.min(
|
|
166
|
+
attempt * 1500,
|
|
167
|
+
maxWaitTime / maxAttempts,
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
console.log(
|
|
171
|
+
`Network error, retrying in ${backoffTime}ms (attempt ${attempt}/${maxAttempts}): ${error.message}`,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
await new Promise((resolve) => setTimeout(resolve, backoffTime));
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// For status code mismatches, throw immediately
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
throw lastError || new Error(`Failed after ${maxAttempts} attempts`);
|
|
185
|
+
}
|