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.
Files changed (143) hide show
  1. package/README.md +1 -2
  2. package/bin/alchemy.js +1406 -1464
  3. package/bin/commands/init.ts +29 -2
  4. package/bin/services/execute-alchemy.ts +1 -2
  5. package/lib/alchemy.d.ts +2 -2
  6. package/lib/alchemy.d.ts.map +1 -1
  7. package/lib/alchemy.js.map +1 -1
  8. package/lib/aws/client-props.d.ts +149 -0
  9. package/lib/aws/client-props.d.ts.map +1 -0
  10. package/lib/aws/client-props.js +1 -0
  11. package/lib/aws/client-props.js.map +1 -0
  12. package/lib/aws/credentials.d.ts +68 -5
  13. package/lib/aws/credentials.d.ts.map +1 -1
  14. package/lib/aws/credentials.js +148 -0
  15. package/lib/aws/credentials.js.map +1 -1
  16. package/lib/aws/ec2/internet-gateway-attachment.d.ts +57 -3
  17. package/lib/aws/ec2/internet-gateway-attachment.d.ts.map +1 -1
  18. package/lib/aws/ec2/internet-gateway-attachment.js +57 -3
  19. package/lib/aws/ec2/internet-gateway-attachment.js.map +1 -1
  20. package/lib/aws/ec2/internet-gateway.d.ts +68 -40
  21. package/lib/aws/ec2/internet-gateway.d.ts.map +1 -1
  22. package/lib/aws/ec2/internet-gateway.js +68 -40
  23. package/lib/aws/ec2/internet-gateway.js.map +1 -1
  24. package/lib/aws/ec2/nat-gateway.d.ts +99 -18
  25. package/lib/aws/ec2/nat-gateway.d.ts.map +1 -1
  26. package/lib/aws/ec2/nat-gateway.js +113 -37
  27. package/lib/aws/ec2/nat-gateway.js.map +1 -1
  28. package/lib/aws/ec2/route-table-association.d.ts +1 -1
  29. package/lib/aws/ec2/route-table-association.js +1 -1
  30. package/lib/aws/ec2/route-table.d.ts +64 -1
  31. package/lib/aws/ec2/route-table.d.ts.map +1 -1
  32. package/lib/aws/ec2/route-table.js +64 -1
  33. package/lib/aws/ec2/route-table.js.map +1 -1
  34. package/lib/aws/ec2/route.d.ts +60 -1
  35. package/lib/aws/ec2/route.d.ts.map +1 -1
  36. package/lib/aws/ec2/route.js +60 -1
  37. package/lib/aws/ec2/route.js.map +1 -1
  38. package/lib/aws/ec2/security-group.d.ts +96 -3
  39. package/lib/aws/ec2/security-group.d.ts.map +1 -1
  40. package/lib/aws/ec2/security-group.js +96 -3
  41. package/lib/aws/ec2/security-group.js.map +1 -1
  42. package/lib/aws/ec2/subnet.d.ts +61 -3
  43. package/lib/aws/ec2/subnet.d.ts.map +1 -1
  44. package/lib/aws/ec2/subnet.js +61 -3
  45. package/lib/aws/ec2/subnet.js.map +1 -1
  46. package/lib/aws/ec2/utils.d.ts +10 -2
  47. package/lib/aws/ec2/utils.d.ts.map +1 -1
  48. package/lib/aws/ec2/utils.js +68 -7
  49. package/lib/aws/ec2/utils.js.map +1 -1
  50. package/lib/aws/ec2/vpc.d.ts +50 -2
  51. package/lib/aws/ec2/vpc.d.ts.map +1 -1
  52. package/lib/aws/ec2/vpc.js +50 -2
  53. package/lib/aws/ec2/vpc.js.map +1 -1
  54. package/lib/aws/index.d.ts +2 -0
  55. package/lib/aws/index.d.ts.map +1 -1
  56. package/lib/aws/index.js +2 -0
  57. package/lib/aws/index.js.map +1 -1
  58. package/lib/cloudflare/api-response.js +2 -2
  59. package/lib/cloudflare/api-response.js.map +1 -1
  60. package/lib/cloudflare/api.d.ts +19 -0
  61. package/lib/cloudflare/api.d.ts.map +1 -1
  62. package/lib/cloudflare/api.js +4 -0
  63. package/lib/cloudflare/api.js.map +1 -1
  64. package/lib/cloudflare/assets.d.ts.map +1 -1
  65. package/lib/cloudflare/assets.js +3 -4
  66. package/lib/cloudflare/assets.js.map +1 -1
  67. package/lib/cloudflare/bucket.d.ts +5 -3
  68. package/lib/cloudflare/bucket.d.ts.map +1 -1
  69. package/lib/cloudflare/bucket.js +22 -18
  70. package/lib/cloudflare/bucket.js.map +1 -1
  71. package/lib/cloudflare/bundle/plugin-compat-warning.js +7 -7
  72. package/lib/cloudflare/bundle/plugin-compat-warning.js.map +1 -1
  73. package/lib/cloudflare/cloudflare-env-proxy.js.map +1 -1
  74. package/lib/cloudflare/kv-namespace.d.ts +2 -2
  75. package/lib/cloudflare/kv-namespace.d.ts.map +1 -1
  76. package/lib/cloudflare/kv-namespace.js.map +1 -1
  77. package/lib/cloudflare/oauth.js +3 -3
  78. package/lib/cloudflare/oauth.js.map +1 -1
  79. package/lib/cloudflare/quick-tunnel.d.ts +1 -1
  80. package/lib/cloudflare/website.d.ts.map +1 -1
  81. package/lib/cloudflare/website.js +26 -97
  82. package/lib/cloudflare/website.js.map +1 -1
  83. package/lib/cloudflare/worker-bundle.d.ts.map +1 -1
  84. package/lib/cloudflare/worker-bundle.js +9 -8
  85. package/lib/cloudflare/worker-bundle.js.map +1 -1
  86. package/lib/cloudflare/worker-tail.d.ts.map +1 -1
  87. package/lib/cloudflare/worker-tail.js +6 -6
  88. package/lib/cloudflare/worker-tail.js.map +1 -1
  89. package/lib/resource.d.ts +1 -0
  90. package/lib/resource.d.ts.map +1 -1
  91. package/lib/resource.js +13 -2
  92. package/lib/resource.js.map +1 -1
  93. package/lib/scope.d.ts +24 -23
  94. package/lib/scope.d.ts.map +1 -1
  95. package/lib/scope.js +25 -20
  96. package/lib/scope.js.map +1 -1
  97. package/lib/state/cloudflare-state-store.d.ts.map +1 -1
  98. package/lib/state/cloudflare-state-store.js +2 -1
  99. package/lib/state/cloudflare-state-store.js.map +1 -1
  100. package/lib/util/idempotent-spawn.d.ts +10 -8
  101. package/lib/util/idempotent-spawn.d.ts.map +1 -1
  102. package/lib/util/idempotent-spawn.js +97 -78
  103. package/lib/util/idempotent-spawn.js.map +1 -1
  104. package/lib/util/index.d.ts +2 -0
  105. package/lib/util/index.d.ts.map +1 -0
  106. package/lib/util/index.js +2 -0
  107. package/lib/util/index.js.map +1 -0
  108. package/lib/util/safe-fetch.d.ts +24 -0
  109. package/lib/util/safe-fetch.d.ts.map +1 -1
  110. package/lib/util/safe-fetch.js +72 -0
  111. package/lib/util/safe-fetch.js.map +1 -1
  112. package/package.json +8 -5
  113. package/src/alchemy.ts +2 -2
  114. package/src/aws/client-props.ts +156 -0
  115. package/src/aws/credentials.ts +165 -4
  116. package/src/aws/ec2/internet-gateway-attachment.ts +59 -4
  117. package/src/aws/ec2/internet-gateway.ts +70 -41
  118. package/src/aws/ec2/nat-gateway.ts +123 -33
  119. package/src/aws/ec2/route-table-association.ts +1 -1
  120. package/src/aws/ec2/route-table.ts +66 -2
  121. package/src/aws/ec2/route.ts +62 -2
  122. package/src/aws/ec2/security-group.ts +98 -4
  123. package/src/aws/ec2/subnet.ts +63 -4
  124. package/src/aws/ec2/utils.ts +73 -7
  125. package/src/aws/ec2/vpc.ts +52 -3
  126. package/src/aws/index.ts +2 -0
  127. package/src/cloudflare/api-response.ts +2 -2
  128. package/src/cloudflare/api.ts +23 -0
  129. package/src/cloudflare/assets.ts +3 -4
  130. package/src/cloudflare/bucket.ts +27 -25
  131. package/src/cloudflare/bundle/plugin-compat-warning.ts +7 -7
  132. package/src/cloudflare/cloudflare-env-proxy.ts +2 -2
  133. package/src/cloudflare/kv-namespace.ts +26 -27
  134. package/src/cloudflare/oauth.ts +3 -3
  135. package/src/cloudflare/website.ts +30 -111
  136. package/src/cloudflare/worker-bundle.ts +9 -8
  137. package/src/cloudflare/worker-tail.ts +6 -8
  138. package/src/resource.ts +19 -2
  139. package/src/scope.ts +73 -43
  140. package/src/state/cloudflare-state-store.ts +2 -1
  141. package/src/util/idempotent-spawn.ts +117 -100
  142. package/src/util/index.ts +1 -0
  143. 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
- throw new Error(
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
- // ------------------------ helpers ------------------------
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: process.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
- return processes[0].name.startsWith(processName);
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 = (buf: Buffer) => process.stdout.write(buf),
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
- 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();
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
- // ------------------------ main flow ------------------------
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
- await ensureChildRunning();
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
- if (!quiet) {
273
- const write = quiet
274
- ? () => false
275
- : (buf: Buffer) => process.stdout.write(buf);
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
- // Start followers (stdout/stderr) and mirror to this process
278
- await followFilePersisted(outPath, {
279
- stateKey: `${path.resolve(outPath)}::stdout`,
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
- // Return a stopper for this instance's mirroring (child keeps running)
285
- return extracted;
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";
@@ -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
+ }