alchemy 0.7.2 → 0.8.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/lib/aws/table.js CHANGED
@@ -45,9 +45,9 @@ export const Table = Resource("dynamo::Table", async function (id, props) {
45
45
  await ignore(ResourceNotFoundException.name, () => client.send(new DeleteTableCommand({
46
46
  TableName: props.tableName,
47
47
  })));
48
- }, (error) => error instanceof ResourceInUseException ||
49
- error instanceof InternalServerError, 10, // Max attempts
50
- 200);
48
+ }, isRetryableError, 10, // Max attempts
49
+ 200 // Initial delay in ms
50
+ );
51
51
  // Wait for table to be deleted
52
52
  let tableDeleted = false;
53
53
  let retryCount = 0;
@@ -143,8 +143,9 @@ export const Table = Resource("dynamo::Table", async function (id, props) {
143
143
  throw error;
144
144
  }
145
145
  }
146
- }, (error) => error instanceof ResourceInUseException, 10, // Max attempts
147
- 200);
146
+ }, isRetryableError, 10, // Max attempts
147
+ 200 // Initial delay in ms
148
+ );
148
149
  // Wait for table to be active with timeout
149
150
  let tableActive = false;
150
151
  let tableDescription;
@@ -185,3 +186,19 @@ export const Table = Resource("dynamo::Table", async function (id, props) {
185
186
  });
186
187
  }
187
188
  });
189
+ const retryableErrors = [
190
+ "ResourceInUseException",
191
+ "ResourceNotFoundException",
192
+ "InternalServerError",
193
+ "ThrottlingException",
194
+ "ProvisionedThroughputExceededException",
195
+ "LimitExceededException",
196
+ "RequestLimitExceeded",
197
+ ];
198
+ function isRetryableError(error) {
199
+ return (error instanceof ResourceInUseException ||
200
+ error instanceof InternalServerError ||
201
+ retryableErrors.includes(error?.name) ||
202
+ retryableErrors.includes(error?.code) ||
203
+ error?.$metadata?.httpStatusCode === 500);
204
+ }
@@ -1,6 +1,7 @@
1
1
  import { deserialize, serialize } from "../serde";
2
2
  import { withExponentialBackoff } from "../util/retry";
3
3
  import { createCloudflareApi, } from "./api";
4
+ import { CloudflareApiError, handleApiError } from "./api-error";
4
5
  /**
5
6
  * State store implementation using Cloudflare R2 API
6
7
  * Uses R2 for immediate consistency compared to KV's eventual consistency
@@ -69,17 +70,12 @@ export class R2RestStateStore {
69
70
  const response = await withExponentialBackoff(async () => {
70
71
  const response = await this.api.get(listPath);
71
72
  if (!response.ok) {
72
- const errorData = await response.json().catch(() => ({
73
- errors: [{ message: response.statusText }],
74
- }));
75
- throw new Error(`Error listing R2 objects: ${errorData.errors?.[0]?.message || response.statusText}`);
73
+ await handleApiError(response, "list", "bucket", this.bucketName);
76
74
  }
77
75
  return response;
78
76
  },
79
77
  // Retry on transient errors
80
- (error) => error.message?.includes("502") ||
81
- error.message?.includes("503") ||
82
- error.message?.includes("timeout"), 5, // 5 retry attempts
78
+ isRetryableError, 5, // 5 retry attempts
83
79
  1000 // Start with 1 second delay
84
80
  );
85
81
  const data = (await response.json());
@@ -117,21 +113,13 @@ export class R2RestStateStore {
117
113
  try {
118
114
  const response = await withExponentialBackoff(async () => {
119
115
  const response = await this.api.get(`/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects/${this.getObjectKey(key)}`);
120
- if (!response.ok) {
121
- if (response.status === 404) {
122
- return response;
123
- }
124
- const errorData = await response.json().catch(() => ({
125
- errors: [{ message: response.statusText }],
126
- }));
127
- throw new Error(`Error getting R2 object: ${errorData.errors?.[0]?.message || response.statusText}`);
116
+ if (!response.ok && response.status !== 404) {
117
+ await handleApiError(response, "get", "object", key);
128
118
  }
129
119
  return response;
130
120
  },
131
121
  // Retry on transient errors
132
- (error) => error.message?.includes("502") ||
133
- error.message?.includes("503") ||
134
- error.message?.includes("timeout"), 5, // 5 retry attempts
122
+ isRetryableError, 5, // 5 retry attempts
135
123
  1000 // Start with 1 second delay
136
124
  );
137
125
  if (response.status === 404) {
@@ -202,15 +190,12 @@ export class R2RestStateStore {
202
190
  },
203
191
  });
204
192
  if (!response.ok) {
205
- const errorData = await response.json().catch(() => ({
206
- errors: [{ message: response.statusText }],
207
- }));
208
- throw new Error(`Error writing to R2: ${errorData.errors?.[0]?.message || response.statusText}`);
193
+ await handleApiError(response, "put", "object", objectKey);
209
194
  }
210
195
  return response;
211
196
  },
212
197
  // Retry on transient errors
213
- (error) => error.message?.includes("503") || error.message?.includes("timeout"), 5, // 5 retry attempts
198
+ isRetryableError, 5, // 5 retry attempts
214
199
  1000 // Start with 1 second delay
215
200
  );
216
201
  }
@@ -224,17 +209,10 @@ export class R2RestStateStore {
224
209
  await withExponentialBackoff(async () => {
225
210
  const response = await this.api.delete(`/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects/${this.getObjectKey(key)}`);
226
211
  if (!response.ok && response.status !== 404) {
227
- const errorData = await response.json().catch(() => ({
228
- errors: [{ message: response.statusText }],
229
- }));
230
- throw new Error(`Error deleting from R2: ${errorData.errors?.[0]?.message || response.statusText}`);
212
+ await handleApiError(response, "delete", "object", key);
231
213
  }
232
214
  return response;
233
- },
234
- // Retry on transient errors
235
- (error) => error.message?.includes("502") ||
236
- error.message?.includes("503") ||
237
- error.message?.includes("timeout"), 5, // 5 retry attempts
215
+ }, isRetryableError, 5, // 5 retry attempts
238
216
  1000 // Start with 1 second delay
239
217
  );
240
218
  }
@@ -275,3 +253,13 @@ export class R2RestStateStore {
275
253
  }
276
254
  }
277
255
  }
256
+ function isRetryableError(error) {
257
+ if (error instanceof CloudflareApiError) {
258
+ return (error.status === 500 ||
259
+ error.status === 502 ||
260
+ error.status === 503 ||
261
+ error.message.includes("timeout") ||
262
+ error.message.includes("internal error"));
263
+ }
264
+ return false;
265
+ }
@@ -431,6 +431,10 @@ async function bundleWorkerScript(props) {
431
431
  minify: true,
432
432
  options: {
433
433
  keepNames: true, // Important for Durable Object classes
434
+ loader: {
435
+ ".sql": "text",
436
+ ".json": "json",
437
+ },
434
438
  },
435
439
  };
436
440
  // Merge with user-provided options
@@ -94,14 +94,21 @@ export interface Bundle extends Resource<"esbuild::Bundle"> {
94
94
  */
95
95
  export declare const Bundle: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | ((this: Context<Bundle>, id: string, props: BundleProps) => Promise<Bundle>);
96
96
  export declare function bundle(props: BundleProps): Promise<esbuild.BuildResult<{
97
- bundle: boolean;
97
+ entryPoints: string[];
98
+ outfile: string;
99
+ bundle: true;
100
+ format: "iife" | "cjs" | "esm" | undefined;
101
+ target: string | string[] | undefined;
102
+ minify: boolean | undefined;
103
+ sourcemap: boolean | "inline" | "external" | "both" | undefined;
104
+ external: string[];
105
+ platform: "browser" | "node" | "neutral" | undefined;
106
+ metafile: true;
107
+ write: true;
98
108
  splitting?: boolean;
99
109
  preserveSymlinks?: boolean;
100
- outfile: string;
101
- metafile: boolean;
102
110
  outdir?: string;
103
111
  outbase?: string;
104
- external: string[];
105
112
  packages?: "bundle" | "external";
106
113
  alias?: Record<string, string>;
107
114
  loader?: {
@@ -110,7 +117,6 @@ export declare function bundle(props: BundleProps): Promise<esbuild.BuildResult<
110
117
  resolveExtensions?: string[];
111
118
  mainFields?: string[];
112
119
  conditions?: string[];
113
- write: boolean;
114
120
  allowOverwrite?: boolean;
115
121
  tsconfig?: string;
116
122
  outExtension?: {
@@ -127,30 +133,21 @@ export declare function bundle(props: BundleProps): Promise<esbuild.BuildResult<
127
133
  footer?: {
128
134
  [type: string]: string;
129
135
  };
130
- entryPoints: string[] | Record<string, string> | {
131
- in: string;
132
- out: string;
133
- }[];
134
136
  stdin?: esbuild.StdinOptions;
135
137
  plugins?: esbuild.Plugin[];
136
138
  absWorkingDir?: string;
137
139
  nodePaths?: string[];
138
- sourcemap: boolean | "linked" | "inline" | "external" | "both";
139
140
  legalComments?: "none" | "inline" | "eof" | "linked" | "external";
140
141
  sourceRoot?: string;
141
142
  sourcesContent?: boolean;
142
- format: esbuild.Format;
143
143
  globalName?: string;
144
- target: string | string[];
145
144
  supported?: Record<string, boolean>;
146
- platform: esbuild.Platform;
147
145
  mangleProps?: RegExp;
148
146
  reserveProps?: RegExp;
149
147
  mangleQuoted?: boolean;
150
148
  mangleCache?: Record<string, string | false>;
151
149
  drop?: esbuild.Drop[];
152
150
  dropLabels?: string[];
153
- minify: boolean;
154
151
  minifyWhitespace?: boolean;
155
152
  minifyIdentifiers?: boolean;
156
153
  minifySyntax?: boolean;
@@ -47,8 +47,8 @@ export const Bundle = Resource("esbuild::Bundle", {
47
47
  });
48
48
  export async function bundle(props) {
49
49
  const outputPath = getOutputPath(props);
50
- // Build the bundle
51
50
  return await esbuild.build({
51
+ ...props.options,
52
52
  entryPoints: [props.entryPoint],
53
53
  outfile: outputPath,
54
54
  bundle: true,
@@ -56,11 +56,14 @@ export async function bundle(props) {
56
56
  target: props.target,
57
57
  minify: props.minify,
58
58
  sourcemap: props.sourcemap,
59
- external: props.external,
59
+ external: [
60
+ "node:async_hooks",
61
+ ...(props.external ?? []),
62
+ ...(props.options?.external ?? []),
63
+ ],
60
64
  platform: props.platform,
61
65
  metafile: true,
62
66
  write: true,
63
- ...props.options,
64
67
  });
65
68
  }
66
69
  function getOutputPath(props) {
@@ -35,7 +35,7 @@ export async function processFrontmatterFiles(directoryPath, linkPrefix) {
35
35
  const frontmatter = frontmatterMatch[1];
36
36
  const orderMatch = frontmatter.match(/order:\s*(\d+)/);
37
37
  if (orderMatch) {
38
- order = parseInt(orderMatch[1]);
38
+ order = parseFloat(orderMatch[1]);
39
39
  }
40
40
  const titleMatch = frontmatter.match(/title:\s*(.+)/);
41
41
  if (titleMatch) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alchemy",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "module": "./lib/index.js",
6
6
  "scripts": {
package/src/aws/table.ts CHANGED
@@ -143,7 +143,7 @@ export const Table = Resource(
143
143
  async function (
144
144
  this: Context<Table>,
145
145
  id: string,
146
- props: TableProps,
146
+ props: TableProps
147
147
  ): Promise<Table> {
148
148
  const client = new DynamoDBClient({});
149
149
 
@@ -154,15 +154,13 @@ export const Table = Resource(
154
154
  client.send(
155
155
  new DeleteTableCommand({
156
156
  TableName: props.tableName,
157
- }),
158
- ),
157
+ })
158
+ )
159
159
  );
160
160
  },
161
- (error) =>
162
- error instanceof ResourceInUseException ||
163
- error instanceof InternalServerError,
161
+ isRetryableError,
164
162
  10, // Max attempts
165
- 200, // Initial delay in ms
163
+ 200 // Initial delay in ms
166
164
  );
167
165
 
168
166
  // Wait for table to be deleted
@@ -175,7 +173,7 @@ export const Table = Resource(
175
173
  await client.send(
176
174
  new DescribeTableCommand({
177
175
  TableName: props.tableName,
178
- }),
176
+ })
179
177
  );
180
178
  // If we get here, table still exists
181
179
  retryCount++;
@@ -193,7 +191,7 @@ export const Table = Resource(
193
191
 
194
192
  if (!tableDeleted) {
195
193
  throw new Error(
196
- `Timed out waiting for table ${props.tableName} to be deleted`,
194
+ `Timed out waiting for table ${props.tableName} to be deleted`
197
195
  );
198
196
  }
199
197
 
@@ -233,7 +231,7 @@ export const Table = Resource(
233
231
  const describeResponse = await client.send(
234
232
  new DescribeTableCommand({
235
233
  TableName: props.tableName,
236
- }),
234
+ })
237
235
  );
238
236
 
239
237
  // If table exists and is ACTIVE, no need to create it
@@ -267,16 +265,16 @@ export const Table = Resource(
267
265
  Value,
268
266
  }))
269
267
  : undefined,
270
- }),
268
+ })
271
269
  );
272
270
  } else {
273
271
  throw error;
274
272
  }
275
273
  }
276
274
  },
277
- (error) => error instanceof ResourceInUseException,
275
+ isRetryableError,
278
276
  10, // Max attempts
279
- 200, // Initial delay in ms
277
+ 200 // Initial delay in ms
280
278
  );
281
279
 
282
280
  // Wait for table to be active with timeout
@@ -290,7 +288,7 @@ export const Table = Resource(
290
288
  const response = await client.send(
291
289
  new DescribeTableCommand({
292
290
  TableName: props.tableName,
293
- }),
291
+ })
294
292
  );
295
293
 
296
294
  tableActive = response.Table?.TableStatus === "ACTIVE";
@@ -313,7 +311,7 @@ export const Table = Resource(
313
311
 
314
312
  if (!tableActive) {
315
313
  throw new Error(
316
- `Timed out waiting for table ${props.tableName} to become active`,
314
+ `Timed out waiting for table ${props.tableName} to become active`
317
315
  );
318
316
  }
319
317
 
@@ -324,5 +322,25 @@ export const Table = Resource(
324
322
  tableId: tableDescription!.TableId!,
325
323
  });
326
324
  }
327
- },
325
+ }
328
326
  );
327
+
328
+ const retryableErrors = [
329
+ "ResourceInUseException",
330
+ "ResourceNotFoundException",
331
+ "InternalServerError",
332
+ "ThrottlingException",
333
+ "ProvisionedThroughputExceededException",
334
+ "LimitExceededException",
335
+ "RequestLimitExceeded",
336
+ ];
337
+
338
+ function isRetryableError(error: any) {
339
+ return (
340
+ error instanceof ResourceInUseException ||
341
+ error instanceof InternalServerError ||
342
+ retryableErrors.includes(error?.name) ||
343
+ retryableErrors.includes(error?.code) ||
344
+ error?.$metadata?.httpStatusCode === 500
345
+ );
346
+ }
@@ -7,6 +7,7 @@ import {
7
7
  type CloudflareApiOptions,
8
8
  createCloudflareApi,
9
9
  } from "./api";
10
+ import { CloudflareApiError, handleApiError } from "./api-error";
10
11
 
11
12
  /**
12
13
  * Options for CloudflareR2StateStore
@@ -106,21 +107,13 @@ export class R2RestStateStore implements StateStore {
106
107
  const response = await this.api.get(listPath);
107
108
 
108
109
  if (!response.ok) {
109
- const errorData: any = await response.json().catch(() => ({
110
- errors: [{ message: response.statusText }],
111
- }));
112
- throw new Error(
113
- `Error listing R2 objects: ${errorData.errors?.[0]?.message || response.statusText}`
114
- );
110
+ await handleApiError(response, "list", "bucket", this.bucketName);
115
111
  }
116
112
 
117
113
  return response;
118
114
  },
119
115
  // Retry on transient errors
120
- (error) =>
121
- error.message?.includes("502") ||
122
- error.message?.includes("503") ||
123
- error.message?.includes("timeout"),
116
+ isRetryableError,
124
117
  5, // 5 retry attempts
125
118
  1000 // Start with 1 second delay
126
119
  );
@@ -173,26 +166,14 @@ export class R2RestStateStore implements StateStore {
173
166
  `/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects/${this.getObjectKey(key)}`
174
167
  );
175
168
 
176
- if (!response.ok) {
177
- if (response.status === 404) {
178
- return response;
179
- }
180
-
181
- const errorData: any = await response.json().catch(() => ({
182
- errors: [{ message: response.statusText }],
183
- }));
184
- throw new Error(
185
- `Error getting R2 object: ${errorData.errors?.[0]?.message || response.statusText}`
186
- );
169
+ if (!response.ok && response.status !== 404) {
170
+ await handleApiError(response, "get", "object", key);
187
171
  }
188
172
 
189
173
  return response;
190
174
  },
191
175
  // Retry on transient errors
192
- (error) =>
193
- error.message?.includes("502") ||
194
- error.message?.includes("503") ||
195
- error.message?.includes("timeout"),
176
+ isRetryableError,
196
177
  5, // 5 retry attempts
197
178
  1000 // Start with 1 second delay
198
179
  );
@@ -280,19 +261,12 @@ export class R2RestStateStore implements StateStore {
280
261
  );
281
262
 
282
263
  if (!response.ok) {
283
- const errorData: any = await response.json().catch(() => ({
284
- errors: [{ message: response.statusText }],
285
- }));
286
- throw new Error(
287
- `Error writing to R2: ${errorData.errors?.[0]?.message || response.statusText}`
288
- );
264
+ await handleApiError(response, "put", "object", objectKey);
289
265
  }
290
-
291
266
  return response;
292
267
  },
293
268
  // Retry on transient errors
294
- (error) =>
295
- error.message?.includes("503") || error.message?.includes("timeout"),
269
+ isRetryableError,
296
270
  5, // 5 retry attempts
297
271
  1000 // Start with 1 second delay
298
272
  );
@@ -313,21 +287,12 @@ export class R2RestStateStore implements StateStore {
313
287
  );
314
288
 
315
289
  if (!response.ok && response.status !== 404) {
316
- const errorData: any = await response.json().catch(() => ({
317
- errors: [{ message: response.statusText }],
318
- }));
319
- throw new Error(
320
- `Error deleting from R2: ${errorData.errors?.[0]?.message || response.statusText}`
321
- );
290
+ await handleApiError(response, "delete", "object", key);
322
291
  }
323
292
 
324
293
  return response;
325
294
  },
326
- // Retry on transient errors
327
- (error) =>
328
- error.message?.includes("502") ||
329
- error.message?.includes("503") ||
330
- error.message?.includes("timeout"),
295
+ isRetryableError,
331
296
  5, // 5 retry attempts
332
297
  1000 // Start with 1 second delay
333
298
  );
@@ -373,3 +338,16 @@ export class R2RestStateStore implements StateStore {
373
338
  }
374
339
  }
375
340
  }
341
+
342
+ function isRetryableError(error: any): boolean {
343
+ if (error instanceof CloudflareApiError) {
344
+ return (
345
+ error.status === 500 ||
346
+ error.status === 502 ||
347
+ error.status === 503 ||
348
+ error.message.includes("timeout") ||
349
+ error.message.includes("internal error")
350
+ );
351
+ }
352
+ return false;
353
+ }
@@ -720,6 +720,10 @@ async function bundleWorkerScript<B extends Bindings>(props: WorkerProps) {
720
720
  minify: true,
721
721
  options: {
722
722
  keepNames: true, // Important for Durable Object classes
723
+ loader: {
724
+ ".sql": "text",
725
+ ".json": "json",
726
+ },
723
727
  },
724
728
  };
725
729
 
@@ -116,7 +116,7 @@ export const Bundle = Resource(
116
116
  async function (
117
117
  this: Context<Bundle>,
118
118
  id: string,
119
- props: BundleProps,
119
+ props: BundleProps
120
120
  ): Promise<Bundle> {
121
121
  // Determine output path
122
122
  const outputPath = getOutputPath(props);
@@ -145,13 +145,13 @@ export const Bundle = Resource(
145
145
  path: outputPath,
146
146
  hash,
147
147
  });
148
- },
148
+ }
149
149
  );
150
150
 
151
151
  export async function bundle(props: BundleProps) {
152
152
  const outputPath = getOutputPath(props);
153
- // Build the bundle
154
153
  return await esbuild.build({
154
+ ...props.options,
155
155
  entryPoints: [props.entryPoint],
156
156
  outfile: outputPath,
157
157
  bundle: true,
@@ -159,11 +159,14 @@ export async function bundle(props: BundleProps) {
159
159
  target: props.target,
160
160
  minify: props.minify,
161
161
  sourcemap: props.sourcemap,
162
- external: props.external,
162
+ external: [
163
+ "node:async_hooks",
164
+ ...(props.external ?? []),
165
+ ...(props.options?.external ?? []),
166
+ ],
163
167
  platform: props.platform,
164
168
  metafile: true,
165
169
  write: true,
166
- ...props.options,
167
170
  });
168
171
  }
169
172
 
@@ -172,7 +175,7 @@ function getOutputPath(props: BundleProps) {
172
175
  props.outfile ||
173
176
  path.join(
174
177
  props.outdir || "dist",
175
- path.basename(props.entryPoint, path.extname(props.entryPoint)) + ".js",
178
+ path.basename(props.entryPoint, path.extname(props.entryPoint)) + ".js"
176
179
  )
177
180
  );
178
181
  }
@@ -58,7 +58,7 @@ export async function processFrontmatterFiles(
58
58
  const frontmatter = frontmatterMatch[1];
59
59
  const orderMatch = frontmatter.match(/order:\s*(\d+)/);
60
60
  if (orderMatch) {
61
- order = parseInt(orderMatch[1]);
61
+ order = parseFloat(orderMatch[1]);
62
62
  }
63
63
  const titleMatch = frontmatter.match(/title:\s*(.+)/);
64
64
  if (titleMatch) {