alchemy 0.5.0 → 0.5.2

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/apply.js CHANGED
@@ -48,14 +48,6 @@ export async function apply(resource, props, options) {
48
48
  }
49
49
  return state.output;
50
50
  }
51
- const diff = await import("diff");
52
- const yaml = await import("yaml");
53
- const old = yaml.stringify(oldProps);
54
- const news = yaml.stringify(newProps);
55
- const patch = diff.createPatch(resource.ID, old, news);
56
- // console.log(patch);
57
- // if (resource.ID === "what-is-alchemy") {
58
- // }
59
51
  }
60
52
  const phase = state.status === "creating" ? "create" : "update";
61
53
  state.status = phase === "create" ? "creating" : "updating";
@@ -1,3 +1,4 @@
1
+ import { withExponentialBackoff } from "../util/retry";
1
2
  import { getCloudflareHeaders } from "./auth";
2
3
  /**
3
4
  * Custom error class for Cloudflare API errors
@@ -151,11 +152,21 @@ export class CloudflareApi {
151
152
  delete combinedHeaders["Content-Type"];
152
153
  }
153
154
  const url = `${this.baseUrl}${path}`;
154
- // Make the request
155
- return fetch(url, {
155
+ // Use withExponentialBackoff for automatic retry on network errors
156
+ return withExponentialBackoff(() => fetch(url, {
156
157
  ...init,
157
158
  headers: combinedHeaders,
158
- });
159
+ }), (error) => {
160
+ // Only retry on network-related errors
161
+ const errorMsg = error.message || "";
162
+ const isNetworkError = errorMsg.includes("socket connection was closed") ||
163
+ errorMsg.includes("ECONNRESET") ||
164
+ errorMsg.includes("ETIMEDOUT") ||
165
+ errorMsg.includes("ECONNREFUSED");
166
+ return isNetworkError;
167
+ }, 5, // Maximum 5 attempts (1 initial + 4 retries)
168
+ 1000 // Start with 1s delay, will exponentially increase
169
+ );
159
170
  }
160
171
  /**
161
172
  * Helper for GET requests
@@ -28,11 +28,17 @@ export interface BucketProps extends CloudflareApiOptions {
28
28
  */
29
29
  allowPublicAccess?: boolean;
30
30
  /**
31
- * Whether to delete the bucket and its contents during resource deletion
31
+ * Whether to delete the bucket.
32
32
  * If set to false, the bucket will remain but the resource will be removed from state
33
- * Default: true
33
+ *
34
+ * @default true
34
35
  */
35
36
  delete?: boolean;
37
+ /**
38
+ * Whether to empty the bucket and delete all objects during resource deletion
39
+ * @default false
40
+ */
41
+ empty?: boolean;
36
42
  }
37
43
  /**
38
44
  * Output returned after R2 Bucket creation/update
@@ -85,6 +91,14 @@ export interface R2Bucket extends Resource<"cloudflare::R2Bucket">, BucketProps
85
91
  * jurisdiction: "fedramp"
86
92
  * });
87
93
  *
94
+ * @example
95
+ * // Create a bucket that will be automatically emptied when deleted
96
+ * // This will delete all objects in the bucket before deleting the bucket itself
97
+ * const temporaryBucket = await R2Bucket("temp-storage", {
98
+ * name: "temp-storage",
99
+ * empty: true // All objects will be deleted when this resource is destroyed
100
+ * });
101
+ *
88
102
  * @see https://developers.cloudflare.com/r2/buckets/
89
103
  */
90
104
  export declare const R2Bucket: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | ((this: Context<R2Bucket>, id: string, props: BucketProps) => Promise<R2Bucket>);
@@ -35,19 +35,28 @@ import { CloudflareAccountId, CloudflareApi, CloudflareApiError, createCloudflar
35
35
  * jurisdiction: "fedramp"
36
36
  * });
37
37
  *
38
+ * @example
39
+ * // Create a bucket that will be automatically emptied when deleted
40
+ * // This will delete all objects in the bucket before deleting the bucket itself
41
+ * const temporaryBucket = await R2Bucket("temp-storage", {
42
+ * name: "temp-storage",
43
+ * empty: true // All objects will be deleted when this resource is destroyed
44
+ * });
45
+ *
38
46
  * @see https://developers.cloudflare.com/r2/buckets/
39
47
  */
40
48
  export const R2Bucket = Resource("cloudflare::R2Bucket", async function (id, props) {
41
- const [api, r2Client] = await Promise.all([
42
- createCloudflareApi(props),
43
- createR2Client(props),
44
- ]);
49
+ const api = await createCloudflareApi(props);
45
50
  const bucketName = props.name || this.id;
46
51
  if (this.phase === "delete") {
47
52
  console.log("Deleting R2 bucket:", bucketName);
48
53
  if (props.delete !== false) {
49
- // Empty the bucket first by deleting all objects
50
- await emptyBucket(r2Client, bucketName, props.jurisdiction);
54
+ if (props.empty) {
55
+ console.log("Emptying R2 bucket:", bucketName);
56
+ const r2Client = await createR2Client(props);
57
+ // Empty the bucket first by deleting all objects
58
+ await emptyBucket(r2Client, bucketName, props.jurisdiction);
59
+ }
51
60
  // Delete R2 bucket
52
61
  console.log("Deleting R2 bucket:", bucketName);
53
62
  await deleteBucket(api, bucketName, props);
@@ -557,6 +557,7 @@ async function uploadAssets(api, workerName, assets) {
557
557
  // Process each bucket of files
558
558
  for (const bucket of buckets) {
559
559
  const formData = new FormData();
560
+ let totalBytes = 0;
560
561
  // Add each file in the bucket to the form
561
562
  for (const fileHash of bucket) {
562
563
  // Find the file with this hash
@@ -575,8 +576,10 @@ async function uploadAssets(api, workerName, assets) {
575
576
  const blob = new Blob([base64Content], {
576
577
  type: getContentType(file.filePath),
577
578
  });
579
+ totalBytes += blob.size;
578
580
  formData.append(fileHash, blob, fileHash);
579
581
  }
582
+ console.log(`Uploading ${totalBytes} bytes of assets`);
580
583
  // Upload this batch of files
581
584
  const uploadResponse = await api.post(`/accounts/${api.accountId}/workers/assets/upload?base64=true`, formData, {
582
585
  headers: {
package/lib/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export type * from "./context";
2
2
  export * from "./resource";
3
+ export type * from "./scope";
3
4
  export * from "./secret";
4
5
  export * from "./state";
5
6
  export * from "./util/ignore";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alchemy",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "type": "module",
5
5
  "module": "./lib/index.js",
6
6
  "scripts": {
package/src/apply.ts CHANGED
@@ -70,15 +70,6 @@ export async function apply<Out extends Resource>(
70
70
  }
71
71
  return state.output as Awaited<Out>;
72
72
  }
73
-
74
- const diff = await import("diff");
75
- const yaml = await import("yaml");
76
- const old = yaml.stringify(oldProps);
77
- const news = yaml.stringify(newProps);
78
- const patch = diff.createPatch(resource.ID, old, news);
79
- // console.log(patch);
80
- // if (resource.ID === "what-is-alchemy") {
81
- // }
82
73
  }
83
74
 
84
75
  const phase = state.status === "creating" ? "create" : "update";
@@ -1,4 +1,5 @@
1
1
  import type { Secret } from "../secret";
2
+ import { withExponentialBackoff } from "../util/retry";
2
3
  import { type CloudflareAuthOptions, getCloudflareHeaders } from "./auth";
3
4
 
4
5
  /**
@@ -240,11 +241,27 @@ export class CloudflareApi {
240
241
 
241
242
  const url = `${this.baseUrl}${path}`;
242
243
 
243
- // Make the request
244
- return fetch(url, {
245
- ...init,
246
- headers: combinedHeaders,
247
- });
244
+ // Use withExponentialBackoff for automatic retry on network errors
245
+ return withExponentialBackoff(
246
+ () =>
247
+ fetch(url, {
248
+ ...init,
249
+ headers: combinedHeaders,
250
+ }),
251
+ (error) => {
252
+ // Only retry on network-related errors
253
+ const errorMsg = (error as Error).message || "";
254
+ const isNetworkError =
255
+ errorMsg.includes("socket connection was closed") ||
256
+ errorMsg.includes("ECONNRESET") ||
257
+ errorMsg.includes("ETIMEDOUT") ||
258
+ errorMsg.includes("ECONNREFUSED");
259
+
260
+ return isNetworkError;
261
+ },
262
+ 5, // Maximum 5 attempts (1 initial + 4 retries)
263
+ 1000 // Start with 1s delay, will exponentially increase
264
+ );
248
265
  }
249
266
 
250
267
  /**
@@ -40,11 +40,18 @@ export interface BucketProps extends CloudflareApiOptions {
40
40
  allowPublicAccess?: boolean;
41
41
 
42
42
  /**
43
- * Whether to delete the bucket and its contents during resource deletion
43
+ * Whether to delete the bucket.
44
44
  * If set to false, the bucket will remain but the resource will be removed from state
45
- * Default: true
45
+ *
46
+ * @default true
46
47
  */
47
48
  delete?: boolean;
49
+
50
+ /**
51
+ * Whether to empty the bucket and delete all objects during resource deletion
52
+ * @default false
53
+ */
54
+ empty?: boolean;
48
55
  }
49
56
 
50
57
  /**
@@ -103,6 +110,14 @@ export interface R2Bucket
103
110
  * jurisdiction: "fedramp"
104
111
  * });
105
112
  *
113
+ * @example
114
+ * // Create a bucket that will be automatically emptied when deleted
115
+ * // This will delete all objects in the bucket before deleting the bucket itself
116
+ * const temporaryBucket = await R2Bucket("temp-storage", {
117
+ * name: "temp-storage",
118
+ * empty: true // All objects will be deleted when this resource is destroyed
119
+ * });
120
+ *
106
121
  * @see https://developers.cloudflare.com/r2/buckets/
107
122
  */
108
123
  export const R2Bucket = Resource(
@@ -112,18 +127,18 @@ export const R2Bucket = Resource(
112
127
  id: string,
113
128
  props: BucketProps
114
129
  ): Promise<R2Bucket> {
115
- const [api, r2Client] = await Promise.all([
116
- createCloudflareApi(props),
117
- createR2Client(props),
118
- ]);
119
-
130
+ const api = await createCloudflareApi(props);
120
131
  const bucketName = props.name || this.id;
121
132
 
122
133
  if (this.phase === "delete") {
123
134
  console.log("Deleting R2 bucket:", bucketName);
124
135
  if (props.delete !== false) {
125
- // Empty the bucket first by deleting all objects
126
- await emptyBucket(r2Client, bucketName, props.jurisdiction);
136
+ if (props.empty) {
137
+ console.log("Emptying R2 bucket:", bucketName);
138
+ const r2Client = await createR2Client(props);
139
+ // Empty the bucket first by deleting all objects
140
+ await emptyBucket(r2Client, bucketName, props.jurisdiction);
141
+ }
127
142
 
128
143
  // Delete R2 bucket
129
144
  console.log("Deleting R2 bucket:", bucketName);
@@ -978,6 +978,8 @@ async function uploadAssets(
978
978
  for (const bucket of buckets) {
979
979
  const formData = new FormData();
980
980
 
981
+ let totalBytes = 0;
982
+
981
983
  // Add each file in the bucket to the form
982
984
  for (const fileHash of bucket) {
983
985
  // Find the file with this hash
@@ -1000,9 +1002,12 @@ async function uploadAssets(
1000
1002
  const blob = new Blob([base64Content], {
1001
1003
  type: getContentType(file.filePath),
1002
1004
  });
1005
+ totalBytes += blob.size;
1003
1006
  formData.append(fileHash, blob, fileHash);
1004
1007
  }
1005
1008
 
1009
+ console.log(`Uploading ${totalBytes} bytes of assets`);
1010
+
1006
1011
  // Upload this batch of files
1007
1012
  const uploadResponse = await api.post(
1008
1013
  `/accounts/${api.accountId}/workers/assets/upload?base64=true`,
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export type * from "./context";
2
2
  export * from "./resource";
3
+ export type * from "./scope";
3
4
  export * from "./secret";
4
5
  export * from "./state";
5
6
  export * from "./util/ignore";