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 +0 -8
- package/lib/cloudflare/api.js +14 -3
- package/lib/cloudflare/bucket.d.ts +16 -2
- package/lib/cloudflare/bucket.js +15 -6
- package/lib/cloudflare/worker.js +3 -0
- package/lib/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/apply.ts +0 -9
- package/src/cloudflare/api.ts +22 -5
- package/src/cloudflare/bucket.ts +24 -9
- package/src/cloudflare/worker.ts +5 -0
- package/src/index.ts +1 -0
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";
|
package/lib/cloudflare/api.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
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
|
-
*
|
|
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>);
|
package/lib/cloudflare/bucket.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
50
|
-
|
|
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);
|
package/lib/cloudflare/worker.js
CHANGED
|
@@ -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
package/package.json
CHANGED
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";
|
package/src/cloudflare/api.ts
CHANGED
|
@@ -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
|
-
//
|
|
244
|
-
return
|
|
245
|
-
|
|
246
|
-
|
|
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
|
/**
|
package/src/cloudflare/bucket.ts
CHANGED
|
@@ -40,11 +40,18 @@ export interface BucketProps extends CloudflareApiOptions {
|
|
|
40
40
|
allowPublicAccess?: boolean;
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
* Whether to delete the bucket
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
126
|
-
|
|
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);
|
package/src/cloudflare/worker.ts
CHANGED
|
@@ -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`,
|