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.
Files changed (163) hide show
  1. package/README.md +1 -2
  2. package/bin/alchemy.js +1916 -1934
  3. package/bin/commands/init.ts +29 -2
  4. package/bin/services/execute-alchemy.ts +1 -2
  5. package/bin/services/github-workflow.ts +204 -142
  6. package/lib/alchemy.d.ts +2 -2
  7. package/lib/alchemy.d.ts.map +1 -1
  8. package/lib/alchemy.js.map +1 -1
  9. package/lib/aws/client-props.d.ts +149 -0
  10. package/lib/aws/client-props.d.ts.map +1 -0
  11. package/lib/aws/client-props.js +1 -0
  12. package/lib/aws/client-props.js.map +1 -0
  13. package/lib/aws/credentials.d.ts +68 -5
  14. package/lib/aws/credentials.d.ts.map +1 -1
  15. package/lib/aws/credentials.js +148 -0
  16. package/lib/aws/credentials.js.map +1 -1
  17. package/lib/aws/ec2/internet-gateway-attachment.d.ts +57 -3
  18. package/lib/aws/ec2/internet-gateway-attachment.d.ts.map +1 -1
  19. package/lib/aws/ec2/internet-gateway-attachment.js +57 -3
  20. package/lib/aws/ec2/internet-gateway-attachment.js.map +1 -1
  21. package/lib/aws/ec2/internet-gateway.d.ts +68 -40
  22. package/lib/aws/ec2/internet-gateway.d.ts.map +1 -1
  23. package/lib/aws/ec2/internet-gateway.js +68 -40
  24. package/lib/aws/ec2/internet-gateway.js.map +1 -1
  25. package/lib/aws/ec2/nat-gateway.d.ts +99 -18
  26. package/lib/aws/ec2/nat-gateway.d.ts.map +1 -1
  27. package/lib/aws/ec2/nat-gateway.js +113 -37
  28. package/lib/aws/ec2/nat-gateway.js.map +1 -1
  29. package/lib/aws/ec2/route-table-association.d.ts +1 -1
  30. package/lib/aws/ec2/route-table-association.js +1 -1
  31. package/lib/aws/ec2/route-table.d.ts +64 -1
  32. package/lib/aws/ec2/route-table.d.ts.map +1 -1
  33. package/lib/aws/ec2/route-table.js +64 -1
  34. package/lib/aws/ec2/route-table.js.map +1 -1
  35. package/lib/aws/ec2/route.d.ts +60 -1
  36. package/lib/aws/ec2/route.d.ts.map +1 -1
  37. package/lib/aws/ec2/route.js +60 -1
  38. package/lib/aws/ec2/route.js.map +1 -1
  39. package/lib/aws/ec2/security-group.d.ts +96 -3
  40. package/lib/aws/ec2/security-group.d.ts.map +1 -1
  41. package/lib/aws/ec2/security-group.js +96 -3
  42. package/lib/aws/ec2/security-group.js.map +1 -1
  43. package/lib/aws/ec2/subnet.d.ts +61 -3
  44. package/lib/aws/ec2/subnet.d.ts.map +1 -1
  45. package/lib/aws/ec2/subnet.js +61 -3
  46. package/lib/aws/ec2/subnet.js.map +1 -1
  47. package/lib/aws/ec2/utils.d.ts +10 -2
  48. package/lib/aws/ec2/utils.d.ts.map +1 -1
  49. package/lib/aws/ec2/utils.js +68 -7
  50. package/lib/aws/ec2/utils.js.map +1 -1
  51. package/lib/aws/ec2/vpc.d.ts +50 -2
  52. package/lib/aws/ec2/vpc.d.ts.map +1 -1
  53. package/lib/aws/ec2/vpc.js +50 -2
  54. package/lib/aws/ec2/vpc.js.map +1 -1
  55. package/lib/aws/index.d.ts +2 -0
  56. package/lib/aws/index.d.ts.map +1 -1
  57. package/lib/aws/index.js +2 -0
  58. package/lib/aws/index.js.map +1 -1
  59. package/lib/cloudflare/api-response.js +2 -2
  60. package/lib/cloudflare/api-response.js.map +1 -1
  61. package/lib/cloudflare/api.d.ts +19 -0
  62. package/lib/cloudflare/api.d.ts.map +1 -1
  63. package/lib/cloudflare/api.js +4 -0
  64. package/lib/cloudflare/api.js.map +1 -1
  65. package/lib/cloudflare/assets.d.ts.map +1 -1
  66. package/lib/cloudflare/assets.js +3 -4
  67. package/lib/cloudflare/assets.js.map +1 -1
  68. package/lib/cloudflare/bucket.d.ts +5 -3
  69. package/lib/cloudflare/bucket.d.ts.map +1 -1
  70. package/lib/cloudflare/bucket.js +22 -18
  71. package/lib/cloudflare/bucket.js.map +1 -1
  72. package/lib/cloudflare/bundle/plugin-compat-warning.js +7 -7
  73. package/lib/cloudflare/bundle/plugin-compat-warning.js.map +1 -1
  74. package/lib/cloudflare/cloudflare-env-proxy.js.map +1 -1
  75. package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
  76. package/lib/cloudflare/compatibility-date.gen.js +1 -1
  77. package/lib/cloudflare/kv-namespace.d.ts +2 -2
  78. package/lib/cloudflare/kv-namespace.d.ts.map +1 -1
  79. package/lib/cloudflare/kv-namespace.js.map +1 -1
  80. package/lib/cloudflare/oauth.js +3 -3
  81. package/lib/cloudflare/oauth.js.map +1 -1
  82. package/lib/cloudflare/quick-tunnel.d.ts +1 -1
  83. package/lib/cloudflare/sveltekit/plugin.d.ts +2 -1
  84. package/lib/cloudflare/sveltekit/plugin.d.ts.map +1 -1
  85. package/lib/cloudflare/sveltekit/plugin.js +9 -0
  86. package/lib/cloudflare/sveltekit/plugin.js.map +1 -1
  87. package/lib/cloudflare/website.d.ts.map +1 -1
  88. package/lib/cloudflare/website.js +26 -97
  89. package/lib/cloudflare/website.js.map +1 -1
  90. package/lib/cloudflare/worker-bundle.d.ts.map +1 -1
  91. package/lib/cloudflare/worker-bundle.js +9 -8
  92. package/lib/cloudflare/worker-bundle.js.map +1 -1
  93. package/lib/cloudflare/worker-tail.d.ts.map +1 -1
  94. package/lib/cloudflare/worker-tail.js +6 -6
  95. package/lib/cloudflare/worker-tail.js.map +1 -1
  96. package/lib/resource.d.ts +1 -0
  97. package/lib/resource.d.ts.map +1 -1
  98. package/lib/resource.js +13 -2
  99. package/lib/resource.js.map +1 -1
  100. package/lib/scope.d.ts +24 -23
  101. package/lib/scope.d.ts.map +1 -1
  102. package/lib/scope.js +28 -22
  103. package/lib/scope.js.map +1 -1
  104. package/lib/state/cloudflare-state-store.d.ts.map +1 -1
  105. package/lib/state/cloudflare-state-store.js +2 -1
  106. package/lib/state/cloudflare-state-store.js.map +1 -1
  107. package/lib/util/idempotent-spawn.d.ts +13 -8
  108. package/lib/util/idempotent-spawn.d.ts.map +1 -1
  109. package/lib/util/idempotent-spawn.js +120 -77
  110. package/lib/util/idempotent-spawn.js.map +1 -1
  111. package/lib/util/index.d.ts +2 -0
  112. package/lib/util/index.d.ts.map +1 -0
  113. package/lib/util/index.js +2 -0
  114. package/lib/util/index.js.map +1 -0
  115. package/lib/util/safe-fetch.d.ts +24 -0
  116. package/lib/util/safe-fetch.d.ts.map +1 -1
  117. package/lib/util/safe-fetch.js +72 -0
  118. package/lib/util/safe-fetch.js.map +1 -1
  119. package/package.json +8 -5
  120. package/src/alchemy.ts +2 -2
  121. package/src/aws/client-props.ts +156 -0
  122. package/src/aws/credentials.ts +165 -4
  123. package/src/aws/ec2/internet-gateway-attachment.ts +59 -4
  124. package/src/aws/ec2/internet-gateway.ts +70 -41
  125. package/src/aws/ec2/nat-gateway.ts +123 -33
  126. package/src/aws/ec2/route-table-association.ts +1 -1
  127. package/src/aws/ec2/route-table.ts +66 -2
  128. package/src/aws/ec2/route.ts +62 -2
  129. package/src/aws/ec2/security-group.ts +98 -4
  130. package/src/aws/ec2/subnet.ts +63 -4
  131. package/src/aws/ec2/utils.ts +73 -7
  132. package/src/aws/ec2/vpc.ts +52 -3
  133. package/src/aws/index.ts +2 -0
  134. package/src/cloudflare/api-response.ts +2 -2
  135. package/src/cloudflare/api.ts +23 -0
  136. package/src/cloudflare/assets.ts +3 -4
  137. package/src/cloudflare/bucket.ts +27 -25
  138. package/src/cloudflare/bundle/plugin-compat-warning.ts +7 -7
  139. package/src/cloudflare/cloudflare-env-proxy.ts +2 -2
  140. package/src/cloudflare/compatibility-date.gen.ts +1 -1
  141. package/src/cloudflare/kv-namespace.ts +26 -27
  142. package/src/cloudflare/oauth.ts +3 -3
  143. package/src/cloudflare/sveltekit/plugin.ts +14 -1
  144. package/src/cloudflare/website.ts +30 -111
  145. package/src/cloudflare/worker-bundle.ts +9 -8
  146. package/src/cloudflare/worker-tail.ts +6 -8
  147. package/src/resource.ts +19 -2
  148. package/src/scope.ts +77 -46
  149. package/src/state/cloudflare-state-store.ts +2 -1
  150. package/src/util/idempotent-spawn.ts +142 -99
  151. package/src/util/index.ts +1 -0
  152. package/src/util/safe-fetch.ts +114 -0
  153. package/templates/astro/alchemy.run.ts +4 -2
  154. package/templates/nuxt/alchemy.run.ts +4 -2
  155. package/templates/react-router/alchemy.run.ts +4 -2
  156. package/templates/rwsdk/alchemy.run.ts +3 -3
  157. package/templates/sveltekit/alchemy.run.ts +4 -2
  158. package/templates/tanstack-start/alchemy.run.ts +4 -2
  159. package/templates/typescript/alchemy.run.ts +2 -2
  160. package/templates/vite/alchemy.run.ts +3 -2
  161. package/templates/vite/src/worker.ts +13 -0
  162. package/templates/vite/tsconfig.json +0 -1
  163. 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
- 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<{
43
+ extracted: Promise<string | undefined>;
44
+ stop: () => Promise<void>;
45
+ }> {
39
46
  if (!processName && !isSameProcess) {
40
- throw new Error(
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
- // ------------------------ helpers ------------------------
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: process.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
- return processes[0].name.startsWith(processName);
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 = (buf: Buffer) => process.stdout.write(buf),
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
- async function persist() {
173
- try {
174
- const cur = await fh.stat();
175
- state = {
176
- ...state,
177
- offset,
178
- ino: Number(cur.ino ?? ino),
179
- dev: Number(cur.dev ?? dev),
180
- size: cur.size,
181
- mtimeMs: cur.mtimeMs,
182
- };
183
- await writeJsonAtomic(stateFile, state);
184
- } catch {
185
- // If the file vanished mid-rotation, we'll catch up on the next event.
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
- // ------------------------ main flow ------------------------
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
- await ensureChildRunning();
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
- if (!quiet) {
273
- const write = quiet
274
- ? () => false
275
- : (buf: Buffer) => process.stdout.write(buf);
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
- // Start followers (stdout/stderr) and mirror to this process
278
- await followFilePersisted(outPath, {
279
- stateKey: `${path.resolve(outPath)}::stdout`,
280
- write,
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
- // Return a stopper for this instance's mirroring (child keeps running)
285
- return extracted;
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";
@@ -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("my-alchemy-app");
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("my-alchemy-app");
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("my-alchemy-app");
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("my-alchemy-app");
5
+ const app = await alchemy("{projectName}");
6
6
 
7
7
  const database = await D1Database("database", {
8
- name: "my-alchemy-app-db",
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: "my-alchemy-app-website",
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("my-alchemy-app");
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("my-alchemy-app");
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("my-alchemy-app");
6
+ const app = await alchemy("{projectName}");
7
7
 
8
8
  export const worker = await Worker("worker", {
9
- name: "my-alchemy-app",
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("my-alchemy-app");
6
+ const app = await alchemy("{projectName}");
7
7
 
8
8
  export const worker = await Vite("website", {
9
- entrypoint: "worker/index.ts",
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
+
@@ -5,7 +5,6 @@
5
5
  "include": [
6
6
  "types/**/*.ts",
7
7
  "src/**/*.ts",
8
- "worker/**/*.ts",
9
8
  "alchemy.run.ts"
10
9
  ],
11
10
  "compilerOptions": {
@@ -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>;