alchemy 0.61.0 → 0.62.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 +1 -2
- package/bin/alchemy.js +1916 -1934
- package/bin/commands/init.ts +29 -2
- package/bin/services/execute-alchemy.ts +1 -2
- package/bin/services/github-workflow.ts +204 -142
- 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/compatibility-date.gen.d.ts +1 -1
- package/lib/cloudflare/compatibility-date.gen.js +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/sveltekit/plugin.d.ts +2 -1
- package/lib/cloudflare/sveltekit/plugin.d.ts.map +1 -1
- package/lib/cloudflare/sveltekit/plugin.js +9 -0
- package/lib/cloudflare/sveltekit/plugin.js.map +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 +28 -22
- 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 +13 -8
- package/lib/util/idempotent-spawn.d.ts.map +1 -1
- package/lib/util/idempotent-spawn.js +120 -77
- 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/compatibility-date.gen.ts +1 -1
- package/src/cloudflare/kv-namespace.ts +26 -27
- package/src/cloudflare/oauth.ts +3 -3
- package/src/cloudflare/sveltekit/plugin.ts +14 -1
- 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 +77 -46
- package/src/state/cloudflare-state-store.ts +2 -1
- package/src/util/idempotent-spawn.ts +142 -99
- package/src/util/index.ts +1 -0
- package/src/util/safe-fetch.ts +114 -0
- package/templates/astro/alchemy.run.ts +4 -2
- package/templates/nuxt/alchemy.run.ts +4 -2
- package/templates/react-router/alchemy.run.ts +4 -2
- package/templates/rwsdk/alchemy.run.ts +3 -3
- package/templates/sveltekit/alchemy.run.ts +4 -2
- package/templates/tanstack-start/alchemy.run.ts +4 -2
- package/templates/typescript/alchemy.run.ts +2 -2
- package/templates/vite/alchemy.run.ts +3 -2
- package/templates/vite/src/worker.ts +13 -0
- package/templates/vite/tsconfig.json +0 -1
- package/templates/vite/worker/index.ts +0 -12
|
@@ -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,12 @@ export async function idempotentSpawn({
|
|
|
21
39
|
extract,
|
|
22
40
|
isSameProcess,
|
|
23
41
|
quiet = false,
|
|
24
|
-
}: {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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<{
|
|
43
|
+
extracted: Promise<string | undefined>;
|
|
44
|
+
stop: () => Promise<void>;
|
|
45
|
+
}> {
|
|
39
46
|
if (!processName && !isSameProcess) {
|
|
40
|
-
|
|
41
|
-
"Either processName or isSameProcess must be provided to resume a process",
|
|
42
|
-
);
|
|
47
|
+
processName = cmd.split(" ")[0];
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
const outPath = log;
|
|
@@ -52,13 +57,25 @@ export async function idempotentSpawn({
|
|
|
52
57
|
Promise.withResolvers<string | undefined>();
|
|
53
58
|
|
|
54
59
|
if (!extract) {
|
|
55
|
-
console.warn(
|
|
56
|
-
"No extract function provided, will not return a value. This is probably a bug.",
|
|
57
|
-
);
|
|
58
60
|
resolveExtracted(undefined);
|
|
59
61
|
}
|
|
60
62
|
|
|
61
|
-
|
|
63
|
+
const pid = await ensureChildRunning();
|
|
64
|
+
|
|
65
|
+
const write = quiet
|
|
66
|
+
? () => false
|
|
67
|
+
: (buf: Buffer) => process.stdout.write(buf);
|
|
68
|
+
|
|
69
|
+
// Start followers (stdout/stderr) and mirror to this process
|
|
70
|
+
await followFilePersisted(outPath, {
|
|
71
|
+
stateKey: `${path.resolve(outPath)}::stdout`,
|
|
72
|
+
write,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
extracted,
|
|
77
|
+
stop: async () => (pid ? await kill(pid) : undefined),
|
|
78
|
+
};
|
|
62
79
|
|
|
63
80
|
function isPidAlive(pid: number) {
|
|
64
81
|
if (!pid || Number.isNaN(pid)) return false;
|
|
@@ -96,7 +113,7 @@ export async function idempotentSpawn({
|
|
|
96
113
|
shell: true,
|
|
97
114
|
cwd,
|
|
98
115
|
stdio: ["ignore", out.fd, out.fd], // stdout/stderr -> files (OS-level)
|
|
99
|
-
env
|
|
116
|
+
env,
|
|
100
117
|
detached: false,
|
|
101
118
|
});
|
|
102
119
|
|
|
@@ -117,6 +134,7 @@ export async function idempotentSpawn({
|
|
|
117
134
|
if (isPidAlive(pid)) return pid;
|
|
118
135
|
if (await isSameProcess?.(pid)) return pid;
|
|
119
136
|
if (processName) {
|
|
137
|
+
const { default: find } = await import("find-process");
|
|
120
138
|
const processes = await find("pid", pid);
|
|
121
139
|
if (processes.length > 1) {
|
|
122
140
|
console.warn(
|
|
@@ -124,7 +142,9 @@ export async function idempotentSpawn({
|
|
|
124
142
|
);
|
|
125
143
|
}
|
|
126
144
|
if (processes.length > 0) {
|
|
127
|
-
|
|
145
|
+
if (processes[0].name.startsWith(processName)) {
|
|
146
|
+
return pid;
|
|
147
|
+
}
|
|
128
148
|
}
|
|
129
149
|
}
|
|
130
150
|
}
|
|
@@ -140,9 +160,9 @@ export async function idempotentSpawn({
|
|
|
140
160
|
async function followFilePersisted(
|
|
141
161
|
logPath: string,
|
|
142
162
|
{
|
|
143
|
-
write
|
|
163
|
+
write,
|
|
144
164
|
chunkSize = 64 * 1024,
|
|
145
|
-
tickMs = 100,
|
|
165
|
+
// tickMs = 100,
|
|
146
166
|
}: {
|
|
147
167
|
stateKey: string;
|
|
148
168
|
write: (buf: Buffer) => boolean;
|
|
@@ -168,64 +188,23 @@ export async function idempotentSpawn({
|
|
|
168
188
|
: st.size;
|
|
169
189
|
|
|
170
190
|
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();
|
|
191
|
+
const drainMutex = new AsyncMutex();
|
|
192
|
+
|
|
193
|
+
// we read the log file and stream in chunks, not lines
|
|
194
|
+
// so we need to keep track of the remainder of the last line
|
|
195
|
+
let remainder: string | undefined;
|
|
196
|
+
|
|
197
|
+
// if we are resuming, then re-parse the log lines to extract the value
|
|
198
|
+
if (offset > 0) {
|
|
199
|
+
// read from start to offset
|
|
200
|
+
const fullBuffer = Buffer.allocUnsafe(offset);
|
|
201
|
+
await fh.read({
|
|
202
|
+
position: 0,
|
|
203
|
+
buffer: fullBuffer,
|
|
204
|
+
length: offset,
|
|
205
|
+
});
|
|
206
|
+
const content = fullBuffer.toString("utf8");
|
|
207
|
+
parse(content);
|
|
229
208
|
}
|
|
230
209
|
|
|
231
210
|
await drain();
|
|
@@ -263,24 +242,88 @@ export async function idempotentSpawn({
|
|
|
263
242
|
await fh.close().catch(() => {});
|
|
264
243
|
await persist();
|
|
265
244
|
};
|
|
266
|
-
}
|
|
267
245
|
|
|
268
|
-
|
|
246
|
+
async function persist() {
|
|
247
|
+
try {
|
|
248
|
+
const cur = await fh.stat();
|
|
249
|
+
state = {
|
|
250
|
+
...state,
|
|
251
|
+
offset,
|
|
252
|
+
ino: Number(cur.ino ?? ino),
|
|
253
|
+
dev: Number(cur.dev ?? dev),
|
|
254
|
+
size: cur.size,
|
|
255
|
+
mtimeMs: cur.mtimeMs,
|
|
256
|
+
};
|
|
257
|
+
await writeJsonAtomic(stateFile, state);
|
|
258
|
+
} catch {
|
|
259
|
+
// If the file vanished mid-rotation, we'll catch up on the next event.
|
|
260
|
+
}
|
|
261
|
+
}
|
|
269
262
|
|
|
270
|
-
|
|
263
|
+
function parse(content: string) {
|
|
264
|
+
const lines = content.split("\n");
|
|
265
|
+
if (extract) {
|
|
266
|
+
if (lines.length > 0) {
|
|
267
|
+
remainder = lines[lines.length - 1];
|
|
268
|
+
}
|
|
269
|
+
// parse all lines except the last one (which may be a partial line)
|
|
270
|
+
for (const line of lines.slice(0, -1)) {
|
|
271
|
+
const extracted = extract(line);
|
|
272
|
+
if (extracted) {
|
|
273
|
+
resolveExtracted(extracted);
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
271
279
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
280
|
+
function drain() {
|
|
281
|
+
return drainMutex.lock(async () => {
|
|
282
|
+
while (true) {
|
|
283
|
+
const cur = await fh.stat().catch(() => null);
|
|
284
|
+
if (!cur) break;
|
|
276
285
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
286
|
+
if (cur.size < offset) offset = 0; // truncated
|
|
287
|
+
const toRead = Math.min(chunkSize, cur.size - offset);
|
|
288
|
+
if (toRead <= 0) break;
|
|
289
|
+
|
|
290
|
+
const { bytesRead, buffer } = await fh.read({
|
|
291
|
+
position: offset,
|
|
292
|
+
buffer: Buffer.allocUnsafe(toRead),
|
|
293
|
+
length: toRead,
|
|
294
|
+
});
|
|
295
|
+
if (!bytesRead) break;
|
|
296
|
+
|
|
297
|
+
const chunk = buffer.subarray(0, bytesRead);
|
|
298
|
+
|
|
299
|
+
// write the newly read content to the stream
|
|
300
|
+
write(chunk);
|
|
301
|
+
// parse the previous remainder and the new chunk
|
|
302
|
+
parse(`${remainder || ""}${chunk.toString("utf8")}`);
|
|
303
|
+
offset += bytesRead;
|
|
304
|
+
}
|
|
305
|
+
await persist();
|
|
306
|
+
});
|
|
307
|
+
}
|
|
282
308
|
}
|
|
283
309
|
|
|
284
|
-
|
|
285
|
-
|
|
310
|
+
async function kill(pid: number) {
|
|
311
|
+
// 1. Kill with SIGTERM
|
|
312
|
+
try {
|
|
313
|
+
process.kill(pid, "SIGTERM");
|
|
314
|
+
} catch {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
// 2. Detect if it's still running.
|
|
318
|
+
// The process appears to remain in the process table even after it's exited, so `isPidAlive` will return true.
|
|
319
|
+
// However, if it's in the table, `find-process` will return it with a name of "<defunct>", so we can detect that instead.
|
|
320
|
+
const { default: find } = await import("find-process");
|
|
321
|
+
const processes = await find("pid", pid);
|
|
322
|
+
if (processes.some((p) => p.name !== "<defunct>")) {
|
|
323
|
+
// 3. If it's still running, kill with SIGKILL
|
|
324
|
+
try {
|
|
325
|
+
process.kill(pid, "SIGKILL");
|
|
326
|
+
} catch {}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
286
329
|
}
|
|
@@ -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
|
+
}
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
import alchemy from "alchemy";
|
|
4
4
|
import { Astro } from "alchemy/cloudflare";
|
|
5
5
|
|
|
6
|
-
const app = await alchemy("
|
|
6
|
+
const app = await alchemy("{projectName}");
|
|
7
7
|
|
|
8
|
-
export const worker = await Astro("website"
|
|
8
|
+
export const worker = await Astro("website", {
|
|
9
|
+
name: `${app.name}-${app.stage}-website`,
|
|
10
|
+
});
|
|
9
11
|
|
|
10
12
|
console.log({
|
|
11
13
|
url: worker.url,
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
import alchemy from "alchemy";
|
|
4
4
|
import { Nuxt } from "alchemy/cloudflare";
|
|
5
5
|
|
|
6
|
-
const app = await alchemy("
|
|
6
|
+
const app = await alchemy("{projectName}");
|
|
7
7
|
|
|
8
|
-
export const worker = await Nuxt("website"
|
|
8
|
+
export const worker = await Nuxt("website", {
|
|
9
|
+
name: `${app.name}-${app.stage}-website`,
|
|
10
|
+
});
|
|
9
11
|
|
|
10
12
|
console.log({
|
|
11
13
|
url: worker.url,
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
import alchemy from "alchemy";
|
|
4
4
|
import { ReactRouter } from "alchemy/cloudflare";
|
|
5
5
|
|
|
6
|
-
const app = await alchemy("
|
|
6
|
+
const app = await alchemy("{projectName}");
|
|
7
7
|
|
|
8
|
-
export const worker = await ReactRouter("website"
|
|
8
|
+
export const worker = await ReactRouter("website", {
|
|
9
|
+
name: `${app.name}-${app.stage}-website`,
|
|
10
|
+
});
|
|
9
11
|
|
|
10
12
|
console.log({
|
|
11
13
|
url: worker.url,
|
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
import alchemy from "alchemy";
|
|
3
3
|
import { D1Database, DurableObjectNamespace, Redwood } from "alchemy/cloudflare";
|
|
4
4
|
|
|
5
|
-
const app = await alchemy("
|
|
5
|
+
const app = await alchemy("{projectName}");
|
|
6
6
|
|
|
7
7
|
const database = await D1Database("database", {
|
|
8
|
-
name:
|
|
8
|
+
name: `${app.name}-${app.stage}-database`,
|
|
9
9
|
migrationsDir: "migrations",
|
|
10
10
|
});
|
|
11
11
|
|
|
12
12
|
export const worker = await Redwood("website", {
|
|
13
|
-
name:
|
|
13
|
+
name: `${app.name}-${app.stage}-website`,
|
|
14
14
|
bindings: {
|
|
15
15
|
AUTH_SECRET_KEY: alchemy.secret(process.env.AUTH_SECRET_KEY),
|
|
16
16
|
DB: database,
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
import alchemy from "alchemy";
|
|
4
4
|
import { SvelteKit } from "alchemy/cloudflare";
|
|
5
5
|
|
|
6
|
-
const app = await alchemy("
|
|
6
|
+
const app = await alchemy("{projectName}");
|
|
7
7
|
|
|
8
|
-
export const worker = await SvelteKit("website"
|
|
8
|
+
export const worker = await SvelteKit("website", {
|
|
9
|
+
name: `${app.name}-${app.stage}-website`,
|
|
10
|
+
});
|
|
9
11
|
|
|
10
12
|
console.log({
|
|
11
13
|
url: worker.url,
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
import alchemy from "alchemy";
|
|
4
4
|
import { TanStackStart } from "alchemy/cloudflare";
|
|
5
5
|
|
|
6
|
-
const app = await alchemy("
|
|
6
|
+
const app = await alchemy("{projectName}");
|
|
7
7
|
|
|
8
|
-
export const worker = await TanStackStart("website"
|
|
8
|
+
export const worker = await TanStackStart("website", {
|
|
9
|
+
name: `${app.name}-${app.stage}-website`,
|
|
10
|
+
});
|
|
9
11
|
|
|
10
12
|
console.log({
|
|
11
13
|
url: worker.url,
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
import alchemy from "alchemy";
|
|
4
4
|
import { Worker } from "alchemy/cloudflare";
|
|
5
5
|
|
|
6
|
-
const app = await alchemy("
|
|
6
|
+
const app = await alchemy("{projectName}");
|
|
7
7
|
|
|
8
8
|
export const worker = await Worker("worker", {
|
|
9
|
-
name:
|
|
9
|
+
name: `${app.name}-${app.stage}-website`,
|
|
10
10
|
entrypoint: "src/worker.ts",
|
|
11
11
|
});
|
|
12
12
|
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
import alchemy from "alchemy";
|
|
4
4
|
import { Vite } from "alchemy/cloudflare";
|
|
5
5
|
|
|
6
|
-
const app = await alchemy("
|
|
6
|
+
const app = await alchemy("{projectName}");
|
|
7
7
|
|
|
8
8
|
export const worker = await Vite("website", {
|
|
9
|
-
|
|
9
|
+
name: `${app.name}-${app.stage}-website`,
|
|
10
|
+
entrypoint: "src/worker.ts",
|
|
10
11
|
});
|
|
11
12
|
|
|
12
13
|
console.log({
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
fetch(request) {
|
|
3
|
+
const url = new URL(request.url);
|
|
4
|
+
|
|
5
|
+
if (url.pathname.startsWith("/api/")) {
|
|
6
|
+
return Response.json({
|
|
7
|
+
name: "Cloudflare",
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
return new Response(null, { status: 404 });
|
|
11
|
+
},
|
|
12
|
+
} satisfies ExportedHandler<Env>;
|
|
13
|
+
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export default {
|
|
2
|
-
fetch(request) {
|
|
3
|
-
const url = new URL(request.url);
|
|
4
|
-
|
|
5
|
-
if (url.pathname.startsWith("/api/")) {
|
|
6
|
-
return Response.json({
|
|
7
|
-
name: "Cloudflare",
|
|
8
|
-
});
|
|
9
|
-
}
|
|
10
|
-
return new Response(null, { status: 404 });
|
|
11
|
-
},
|
|
12
|
-
} satisfies ExportedHandler<Env>;
|