alchemy 0.4.0 → 0.5.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 (53) hide show
  1. package/README.md +3 -463
  2. package/lib/ai/data.d.ts +5 -0
  3. package/lib/ai/data.js +3 -0
  4. package/lib/alchemy.d.ts +3 -3
  5. package/lib/alchemy.js +82 -85
  6. package/lib/cloudflare/assets.d.ts +74 -0
  7. package/lib/cloudflare/assets.js +75 -0
  8. package/lib/cloudflare/bindings.d.ts +6 -1
  9. package/lib/cloudflare/bindings.js +6 -0
  10. package/lib/cloudflare/bound.d.ts +2 -1
  11. package/lib/cloudflare/custom-domain.d.ts +1 -15
  12. package/lib/cloudflare/custom-domain.js +1 -15
  13. package/lib/cloudflare/index.d.ts +1 -1
  14. package/lib/cloudflare/index.js +1 -1
  15. package/lib/cloudflare/r2-rest-state-store.js +52 -22
  16. package/lib/cloudflare/worker.d.ts +18 -0
  17. package/lib/cloudflare/worker.js +153 -4
  18. package/lib/internal/docs/providers.js +2 -1
  19. package/lib/os/exec.d.ts +103 -0
  20. package/lib/os/exec.js +103 -0
  21. package/lib/os/index.d.ts +1 -0
  22. package/lib/os/index.js +1 -0
  23. package/lib/secret.d.ts +2 -2
  24. package/lib/secret.js +2 -2
  25. package/lib/web/vitepress/vitepress.js +1 -1
  26. package/package.json +2 -1
  27. package/src/ai/data.ts +10 -0
  28. package/src/alchemy.ts +97 -100
  29. package/src/cloudflare/assets.ts +158 -0
  30. package/src/cloudflare/bindings.ts +11 -2
  31. package/src/cloudflare/bound.ts +4 -1
  32. package/src/cloudflare/custom-domain.ts +1 -15
  33. package/src/cloudflare/index.ts +1 -1
  34. package/src/cloudflare/r2-rest-state-store.ts +78 -34
  35. package/src/cloudflare/worker.ts +261 -6
  36. package/src/internal/docs/providers.ts +2 -1
  37. package/src/os/exec.ts +190 -0
  38. package/src/os/index.ts +1 -0
  39. package/src/secret.ts +2 -2
  40. package/src/web/vitepress/vitepress.ts +1 -1
  41. package/lib/cloudflare/generate-asset-manifest.d.ts +0 -2
  42. package/lib/cloudflare/generate-asset-manifest.js +0 -68
  43. package/lib/cloudflare/static-site-router.d.ts +0 -18
  44. package/lib/cloudflare/static-site-router.js +0 -115
  45. package/lib/cloudflare/static-site.d.ts +0 -211
  46. package/lib/cloudflare/static-site.js +0 -213
  47. package/lib/cloudflare/upload-asset-manifest.d.ts +0 -11
  48. package/lib/cloudflare/upload-asset-manifest.js +0 -56
  49. package/src/cloudflare/generate-asset-manifest.ts +0 -93
  50. package/src/cloudflare/static-site-router.ts +0 -157
  51. package/src/cloudflare/static-site.ts +0 -416
  52. package/src/cloudflare/upload-asset-manifest.ts +0 -85
  53. package/src/web/vitepress/index.md +0 -75
package/lib/alchemy.js CHANGED
@@ -10,7 +10,7 @@ export const alchemy = _alchemy;
10
10
  * Implementation of the alchemy function that handles both application scoping
11
11
  * and template string interpolation.
12
12
  */
13
- function _alchemy(...args) {
13
+ async function _alchemy(...args) {
14
14
  if (typeof args[0] === "string") {
15
15
  const [appName, options] = args;
16
16
  const root = scope(undefined, {
@@ -19,9 +19,8 @@ function _alchemy(...args) {
19
19
  stage: options?.stage,
20
20
  });
21
21
  if (options?.phase === "destroy") {
22
- return destroy(root).finally(() => {
23
- process.exit(0);
24
- });
22
+ await destroy(root);
23
+ return process.exit(0);
25
24
  }
26
25
  return root;
27
26
  }
@@ -32,92 +31,90 @@ function _alchemy(...args) {
32
31
  ? secondLine.match(/^(\s*)/)?.[1]?.length || 0
33
32
  : 0;
34
33
  const indent = " ".repeat(leadingSpaces);
35
- return (async () => {
36
- const { isFileCollection, isFileRef } = await import("./fs");
37
- const appendices = {};
38
- const stringValues = await Promise.all(values.map(async function resolve(value) {
39
- if (typeof value === "string") {
40
- return indent + value;
41
- }
42
- else if (value === null) {
43
- return "null";
44
- }
45
- else if (value === undefined) {
46
- return "undefined";
47
- }
48
- else if (typeof value === "number" ||
49
- typeof value === "boolean" ||
50
- typeof value === "bigint") {
51
- return value.toString();
52
- }
53
- else if (value instanceof Promise) {
54
- return resolve(await value);
34
+ const { isFileCollection, isFileRef } = await import("./fs");
35
+ const appendices = {};
36
+ const stringValues = await Promise.all(values.map(async function resolve(value) {
37
+ if (typeof value === "string") {
38
+ return indent + value;
39
+ }
40
+ else if (value === null) {
41
+ return "null";
42
+ }
43
+ else if (value === undefined) {
44
+ return "undefined";
45
+ }
46
+ else if (typeof value === "number" ||
47
+ typeof value === "boolean" ||
48
+ typeof value === "bigint") {
49
+ return value.toString();
50
+ }
51
+ else if (value instanceof Promise) {
52
+ return resolve(await value);
53
+ }
54
+ else if (isFileRef(value)) {
55
+ if (!(value.path in appendices)) {
56
+ appendices[value.path] = await fs.readFile(value.path, "utf-8");
55
57
  }
56
- else if (isFileRef(value)) {
57
- if (!(value.path in appendices)) {
58
- appendices[value.path] = await fs.readFile(value.path, "utf-8");
59
- }
58
+ return `[${path.basename(value.path)}](${value.path})`;
59
+ }
60
+ else if (isFileCollection(value)) {
61
+ return Object.entries(value.files)
62
+ .map(([filePath, content]) => {
63
+ appendices[filePath] = content;
64
+ return `[${path.basename(filePath)}](${filePath})`;
65
+ })
66
+ .join("\n\n");
67
+ }
68
+ else if (Array.isArray(value)) {
69
+ return (await Promise.all(value.map(async (value, i) => `${i}. ${await resolve(value)}`))).join("\n");
70
+ }
71
+ else if (typeof value === "object" &&
72
+ typeof value.path === "string") {
73
+ if (typeof value.content === "string") {
74
+ appendices[value.path] = value.content;
60
75
  return `[${path.basename(value.path)}](${value.path})`;
61
76
  }
62
- else if (isFileCollection(value)) {
63
- return Object.entries(value.files)
64
- .map(([filePath, content]) => {
65
- appendices[filePath] = content;
66
- return `[${path.basename(filePath)}](${filePath})`;
67
- })
68
- .join("\n\n");
69
- }
70
- else if (Array.isArray(value)) {
71
- return (await Promise.all(value.map(async (value, i) => `${i}. ${await resolve(value)}`))).join("\n");
72
- }
73
- else if (typeof value === "object" &&
74
- typeof value.path === "string") {
75
- if (typeof value.content === "string") {
76
- appendices[value.path] = value.content;
77
- return `[${path.basename(value.path)}](${value.path})`;
78
- }
79
- else {
80
- appendices[value.path] = await fs.readFile(value.path, "utf-8");
81
- return `[${path.basename(value.path)}](${value.path})`;
82
- }
83
- }
84
- else if (typeof value === "object") {
85
- return (await Promise.all(Object.entries(value).map(async ([key, value]) => {
86
- return `* ${key}: ${await resolve(value)}`;
87
- }))).join("\n");
88
- }
89
77
  else {
90
- // TODO: support other types
91
- console.log(value);
92
- throw new Error(`Unsupported value type: ${value}`);
78
+ appendices[value.path] = await fs.readFile(value.path, "utf-8");
79
+ return `[${path.basename(value.path)}](${value.path})`;
93
80
  }
94
- }));
95
- // Construct the string template by joining template parts with interpolated values
96
- const lines = template
97
- .map((part) => part
98
- .split("\n")
99
- .map((line) => line.startsWith(indent) ? line.slice(indent.length) : line)
100
- .join("\n"))
101
- .flatMap((part, i) => i < stringValues.length ? [part, stringValues[i] ?? ""] : [part])
102
- .join("")
103
- .split("\n");
104
- // Collect and sort appendices by file path
105
- return [
106
- // format the user prompt and trim the first line if it's empty
107
- lines.length > 1 && lines[0].replaceAll(" ", "").length === 0
108
- ? lines.slice(1).join("\n")
109
- : lines.join("\n"),
110
- // sort appendices by path and include at the end of the prompt
111
- Object.entries(appendices)
112
- .sort(([a], [b]) => a.localeCompare(b))
113
- .map(([filePath, content]) => {
114
- const extension = path.extname(filePath).slice(1);
115
- const codeTag = extension ? extension : "";
116
- return `// ${filePath}\n\`\`\`${codeTag}\n${content}\n\`\`\``;
117
- })
118
- .join("\n\n"),
119
- ].join("\n");
120
- })();
81
+ }
82
+ else if (typeof value === "object") {
83
+ return (await Promise.all(Object.entries(value).map(async ([key, value]) => {
84
+ return `* ${key}: ${await resolve(value)}`;
85
+ }))).join("\n");
86
+ }
87
+ else {
88
+ // TODO: support other types
89
+ console.log(value);
90
+ throw new Error(`Unsupported value type: ${value}`);
91
+ }
92
+ }));
93
+ // Construct the string template by joining template parts with interpolated values
94
+ const lines = template
95
+ .map((part) => part
96
+ .split("\n")
97
+ .map((line) => line.startsWith(indent) ? line.slice(indent.length) : line)
98
+ .join("\n"))
99
+ .flatMap((part, i) => i < stringValues.length ? [part, stringValues[i] ?? ""] : [part])
100
+ .join("")
101
+ .split("\n");
102
+ // Collect and sort appendices by file path
103
+ return [
104
+ // format the user prompt and trim the first line if it's empty
105
+ lines.length > 1 && lines[0].replaceAll(" ", "").length === 0
106
+ ? lines.slice(1).join("\n")
107
+ : lines.join("\n"),
108
+ // sort appendices by path and include at the end of the prompt
109
+ Object.entries(appendices)
110
+ .sort(([a], [b]) => a.localeCompare(b))
111
+ .map(([filePath, content]) => {
112
+ const extension = path.extname(filePath).slice(1);
113
+ const codeTag = extension ? extension : "";
114
+ return `// ${filePath}\n\`\`\`${codeTag}\n${content}\n\`\`\``;
115
+ })
116
+ .join("\n\n"),
117
+ ].join("\n");
121
118
  }
122
119
  }
123
120
  _alchemy.destroy = destroy;
@@ -0,0 +1,74 @@
1
+ import type { Context } from "../context";
2
+ import { Resource } from "../resource";
3
+ /**
4
+ * Properties for creating or updating Assets
5
+ */
6
+ export interface AssetsProps {
7
+ /**
8
+ * Path to a directory containing static assets to be uploaded
9
+ * These files will be served by Cloudflare's Workers runtime
10
+ */
11
+ path: string;
12
+ }
13
+ /**
14
+ * Output returned after Assets creation/update
15
+ */
16
+ export interface Assets extends Resource<"cloudflare::Asset">, AssetsProps {
17
+ /**
18
+ * The type of binding
19
+ */
20
+ type: "assets";
21
+ /**
22
+ * The ID of the assets bundle
23
+ */
24
+ id: string;
25
+ /**
26
+ * Asset files that were found
27
+ */
28
+ files: AssetFile[];
29
+ /**
30
+ * Time at which the assets were created
31
+ */
32
+ createdAt: number;
33
+ /**
34
+ * Time at which the assets were last updated
35
+ */
36
+ updatedAt: number;
37
+ }
38
+ /**
39
+ * Represents a single asset file
40
+ */
41
+ export interface AssetFile {
42
+ /**
43
+ * Path relative to the assets directory
44
+ */
45
+ path: string;
46
+ /**
47
+ * Full filesystem path to the file
48
+ */
49
+ filePath: string;
50
+ /**
51
+ * Content type of the file
52
+ */
53
+ contentType: string;
54
+ }
55
+ /**
56
+ * Cloudflare Assets represent a collection of static files that can be uploaded and served
57
+ * by Cloudflare Workers.
58
+ *
59
+ * @example
60
+ * // Create a basic assets bundle from a local directory
61
+ * const staticAssets = await Assets("static", {
62
+ * path: "./src/assets"
63
+ * });
64
+ *
65
+ * // Use these assets with a worker
66
+ * const worker = await Worker("frontend", {
67
+ * name: "frontend-worker",
68
+ * entrypoint: "./src/worker.ts",
69
+ * bindings: {
70
+ * ASSETS: staticAssets
71
+ * }
72
+ * });
73
+ */
74
+ export declare const Assets: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | ((this: Context<Assets>, id: string, props: AssetsProps) => Promise<Assets>);
@@ -0,0 +1,75 @@
1
+ import * as fs from "fs/promises";
2
+ import * as path from "path";
3
+ import { Resource } from "../resource";
4
+ import { getContentType } from "../util/content-type";
5
+ /**
6
+ * Cloudflare Assets represent a collection of static files that can be uploaded and served
7
+ * by Cloudflare Workers.
8
+ *
9
+ * @example
10
+ * // Create a basic assets bundle from a local directory
11
+ * const staticAssets = await Assets("static", {
12
+ * path: "./src/assets"
13
+ * });
14
+ *
15
+ * // Use these assets with a worker
16
+ * const worker = await Worker("frontend", {
17
+ * name: "frontend-worker",
18
+ * entrypoint: "./src/worker.ts",
19
+ * bindings: {
20
+ * ASSETS: staticAssets
21
+ * }
22
+ * });
23
+ */
24
+ export const Assets = Resource("cloudflare::Asset", {
25
+ alwaysUpdate: true,
26
+ }, async function (id, props) {
27
+ if (this.phase === "delete") {
28
+ return this.destroy();
29
+ }
30
+ try {
31
+ // Check if the assets directory exists
32
+ const stats = await fs.stat(props.path);
33
+ if (!stats.isDirectory()) {
34
+ throw new Error(`Assets path ${props.path} is not a directory`);
35
+ }
36
+ }
37
+ catch (error) {
38
+ throw new Error(`Assets directory ${props.path} does not exist or is not accessible`);
39
+ }
40
+ // Recursively get all files in the assets directory
41
+ const filesList = await getFilesRecursively(props.path);
42
+ // Create asset file objects
43
+ const files = filesList.map((filePath) => {
44
+ const relativePath = path.relative(props.path, filePath);
45
+ const normalizedPath = relativePath.split(path.sep).join("/"); // Ensure forward slashes for URLs
46
+ return {
47
+ path: normalizedPath,
48
+ filePath,
49
+ contentType: getContentType(filePath),
50
+ };
51
+ });
52
+ // Get current timestamp
53
+ const now = Date.now();
54
+ // Construct the output
55
+ return this({
56
+ id,
57
+ type: "assets",
58
+ path: props.path,
59
+ files,
60
+ createdAt: this.output?.createdAt || now,
61
+ updatedAt: now,
62
+ });
63
+ });
64
+ // Helper functions for file operations
65
+ async function getFilesRecursively(dir) {
66
+ const files = await fs.readdir(dir, { withFileTypes: true });
67
+ const allFiles = await Promise.all(files.map(async (file) => {
68
+ const path = `${dir}/${file.name}`;
69
+ if (file.isDirectory()) {
70
+ return getFilesRecursively(path);
71
+ }
72
+ return path;
73
+ }));
74
+ return allFiles.flat();
75
+ }
@@ -4,6 +4,7 @@
4
4
  * https://developers.cloudflare.com/api/resources/workers/subresources/scripts/methods/update/
5
5
  */
6
6
  import type { Secret } from "../secret";
7
+ import type { Assets } from "./assets";
7
8
  import type { R2Bucket } from "./bucket";
8
9
  import type { DurableObjectNamespace } from "./durable-object-namespace";
9
10
  import type { KVNamespace } from "./kv-namespace";
@@ -14,8 +15,12 @@ export type Bindings = {
14
15
  /**
15
16
  * L2 Binding Resources.
16
17
  */
17
- export type Binding = DurableObjectNamespace | KVNamespace | Worker | R2Bucket | Secret | string;
18
+ export type Binding = DurableObjectNamespace | KVNamespace | Worker | R2Bucket | Secret | string | Assets;
18
19
  export declare function isDurableObjectNamespace(binding: Binding): binding is DurableObjectNamespace;
20
+ /**
21
+ * Check if a binding is an Assets resource
22
+ */
23
+ export declare function isAssets(binding: Binding): binding is Assets;
19
24
  /**
20
25
  * Union type for all Worker binding types (API spec)
21
26
  */
@@ -1,3 +1,9 @@
1
1
  export function isDurableObjectNamespace(binding) {
2
2
  return (typeof binding === "object" && binding.type === "durable_object_namespace");
3
3
  }
4
+ /**
5
+ * Check if a binding is an Assets resource
6
+ */
7
+ export function isAssets(binding) {
8
+ return typeof binding === "object" && binding.type === "assets";
9
+ }
@@ -1,7 +1,8 @@
1
1
  import type { Secret } from "../secret";
2
+ import type { Assets } from "./assets";
2
3
  import type { Binding } from "./bindings";
3
4
  import type { R2Bucket as _R2Bucket } from "./bucket";
4
5
  import type { DurableObjectNamespace as _DurableObjectNamespace } from "./durable-object-namespace";
5
6
  import type { KVNamespace as _KVNamespace } from "./kv-namespace";
6
7
  import type { Worker as _Worker } from "./worker";
7
- export type Bound<T extends Binding> = T extends _DurableObjectNamespace ? DurableObjectNamespace : T extends _KVNamespace ? KVNamespace : T extends _Worker ? Worker : T extends _R2Bucket ? R2Bucket : T extends Secret ? string : never;
8
+ export type Bound<T extends Binding> = T extends _DurableObjectNamespace ? DurableObjectNamespace : T extends _KVNamespace ? KVNamespace : T extends _Worker ? Worker : T extends _R2Bucket ? R2Bucket : T extends Secret ? string : T extends Assets ? Service : Service;
@@ -41,8 +41,7 @@ export interface CustomDomain extends Resource<"cloudflare::CustomDomain">, Cust
41
41
  }
42
42
  /**
43
43
  * Configure custom domain for a Cloudflare Worker using the Cloudflare Custom Domains API
44
- * This attaches a worker (either a standard Worker or the one backing a StaticSite)
45
- * to a specific hostname within a zone.
44
+ * This attaches a worker to a specific hostname within a zone.
46
45
  *
47
46
  * @example
48
47
  * // Bind a domain to a standard Cloudflare Worker
@@ -57,19 +56,6 @@ export interface CustomDomain extends Resource<"cloudflare::CustomDomain">, Cust
57
56
  * workerName: apiWorker.name // Use the name from the Worker resource
58
57
  * });
59
58
  *
60
- * @example
61
- * // Bind a domain to a Cloudflare Static Site
62
- * const mySite = await StaticSite("landing-page", {
63
- * name: "my-landing-page-site", // This becomes the underlying worker name
64
- * dir: "./dist/landing-page"
65
- * });
66
- *
67
- * const siteDomain = await CustomDomain("site-domain-binding", {
68
- * name: "www.example.com",
69
- * zoneId: "YOUR_ZONE_ID", // Replace with actual Zone ID
70
- * workerName: mySite.name // Use the name from the StaticSite resource
71
- * });
72
- *
73
59
  * @see https://developers.cloudflare.com/api/resources/workers/subresources/domains/
74
60
  */
75
61
  export declare const CustomDomain: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | ((this: Context<CustomDomain>, logicalId: string, props: CustomDomainProps) => Promise<CustomDomain>);
@@ -2,8 +2,7 @@ import { Resource } from "../resource";
2
2
  import { CloudflareApi, createCloudflareApi, handleApiError } from "./api";
3
3
  /**
4
4
  * Configure custom domain for a Cloudflare Worker using the Cloudflare Custom Domains API
5
- * This attaches a worker (either a standard Worker or the one backing a StaticSite)
6
- * to a specific hostname within a zone.
5
+ * This attaches a worker to a specific hostname within a zone.
7
6
  *
8
7
  * @example
9
8
  * // Bind a domain to a standard Cloudflare Worker
@@ -18,19 +17,6 @@ import { CloudflareApi, createCloudflareApi, handleApiError } from "./api";
18
17
  * workerName: apiWorker.name // Use the name from the Worker resource
19
18
  * });
20
19
  *
21
- * @example
22
- * // Bind a domain to a Cloudflare Static Site
23
- * const mySite = await StaticSite("landing-page", {
24
- * name: "my-landing-page-site", // This becomes the underlying worker name
25
- * dir: "./dist/landing-page"
26
- * });
27
- *
28
- * const siteDomain = await CustomDomain("site-domain-binding", {
29
- * name: "www.example.com",
30
- * zoneId: "YOUR_ZONE_ID", // Replace with actual Zone ID
31
- * workerName: mySite.name // Use the name from the StaticSite resource
32
- * });
33
- *
34
20
  * @see https://developers.cloudflare.com/api/resources/workers/subresources/domains/
35
21
  */
36
22
  export const CustomDomain = Resource("cloudflare::CustomDomain", async function (logicalId, // Changed param name from id to logicalId for clarity
@@ -1,5 +1,6 @@
1
1
  export * from "./account-api-token";
2
2
  export * from "./api";
3
+ export * from "./assets";
3
4
  export * from "./bindings";
4
5
  export * from "./bucket";
5
6
  export * from "./custom-domain";
@@ -8,7 +9,6 @@ export * from "./durable-object-namespace";
8
9
  export * from "./kv-namespace";
9
10
  export * from "./permission-groups";
10
11
  export * from "./r2-rest-state-store";
11
- export * from "./static-site";
12
12
  export * from "./worker";
13
13
  export * from "./wrangler.json";
14
14
  export * from "./zone";
@@ -1,5 +1,6 @@
1
1
  export * from "./account-api-token";
2
2
  export * from "./api";
3
+ export * from "./assets";
3
4
  export * from "./bindings";
4
5
  export * from "./bucket";
5
6
  export * from "./custom-domain";
@@ -8,7 +9,6 @@ export * from "./durable-object-namespace";
8
9
  export * from "./kv-namespace";
9
10
  export * from "./permission-groups";
10
11
  export * from "./r2-rest-state-store";
11
- export * from "./static-site";
12
12
  export * from "./worker";
13
13
  export * from "./wrangler.json";
14
14
  export * from "./zone";
@@ -74,13 +74,22 @@ export class R2RestStateStore {
74
74
  params.append("cursor", cursor);
75
75
  }
76
76
  const listPath = `/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects?${params.toString()}`;
77
- const response = await this.api.get(listPath);
78
- if (!response.ok) {
79
- const errorData = await response.json().catch(() => ({
80
- errors: [{ message: response.statusText }],
81
- }));
82
- throw new Error(`Error listing R2 objects: ${errorData.errors?.[0]?.message || response.statusText}`);
83
- }
77
+ const response = await withExponentialBackoff(async () => {
78
+ const response = await this.api.get(listPath);
79
+ if (!response.ok) {
80
+ const errorData = await response.json().catch(() => ({
81
+ errors: [{ message: response.statusText }],
82
+ }));
83
+ throw new Error(`Error listing R2 objects: ${errorData.errors?.[0]?.message || response.statusText}`);
84
+ }
85
+ return response;
86
+ },
87
+ // Retry on transient errors
88
+ (error) => error.message?.includes("502") ||
89
+ error.message?.includes("503") ||
90
+ error.message?.includes("timeout"), 5, // 5 retry attempts
91
+ 1000 // Start with 1 second delay
92
+ );
84
93
  const data = (await response.json());
85
94
  // The result structure may be under "result" key in Cloudflare's API
86
95
  const result = data.result || data;
@@ -114,15 +123,27 @@ export class R2RestStateStore {
114
123
  async get(key) {
115
124
  await this.ensureInitialized();
116
125
  try {
117
- const response = await this.api.get(`/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects/${this.getObjectKey(key)}`);
118
- if (!response.ok) {
119
- if (response.status === 404) {
120
- return undefined;
126
+ const response = await withExponentialBackoff(async () => {
127
+ const response = await this.api.get(`/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects/${this.getObjectKey(key)}`);
128
+ if (!response.ok) {
129
+ if (response.status === 404) {
130
+ return response;
131
+ }
132
+ const errorData = await response.json().catch(() => ({
133
+ errors: [{ message: response.statusText }],
134
+ }));
135
+ throw new Error(`Error getting R2 object: ${errorData.errors?.[0]?.message || response.statusText}`);
121
136
  }
122
- const errorData = await response.json().catch(() => ({
123
- errors: [{ message: response.statusText }],
124
- }));
125
- throw new Error(`Error getting R2 object: ${errorData.errors?.[0]?.message || response.statusText}`);
137
+ return response;
138
+ },
139
+ // Retry on transient errors
140
+ (error) => error.message?.includes("502") ||
141
+ error.message?.includes("503") ||
142
+ error.message?.includes("timeout"), 5, // 5 retry attempts
143
+ 1000 // Start with 1 second delay
144
+ );
145
+ if (response.status === 404) {
146
+ return undefined;
126
147
  }
127
148
  // Parse and deserialize the state data
128
149
  const rawData = await response.json();
@@ -208,13 +229,22 @@ export class R2RestStateStore {
208
229
  */
209
230
  async delete(key) {
210
231
  await this.ensureInitialized();
211
- const response = await this.api.delete(`/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects/${this.getObjectKey(key)}`);
212
- if (!response.ok && response.status !== 404) {
213
- const errorData = await response.json().catch(() => ({
214
- errors: [{ message: response.statusText }],
215
- }));
216
- throw new Error(`Error deleting from R2: ${errorData.errors?.[0]?.message || response.statusText}`);
217
- }
232
+ await withExponentialBackoff(async () => {
233
+ const response = await this.api.delete(`/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects/${this.getObjectKey(key)}`);
234
+ if (!response.ok && response.status !== 404) {
235
+ const errorData = await response.json().catch(() => ({
236
+ errors: [{ message: response.statusText }],
237
+ }));
238
+ throw new Error(`Error deleting from R2: ${errorData.errors?.[0]?.message || response.statusText}`);
239
+ }
240
+ return response;
241
+ },
242
+ // Retry on transient errors
243
+ (error) => error.message?.includes("502") ||
244
+ error.message?.includes("503") ||
245
+ error.message?.includes("timeout"), 5, // 5 retry attempts
246
+ 1000 // Start with 1 second delay
247
+ );
218
248
  }
219
249
  /**
220
250
  * Convert key for storage by replacing slashes with colons
@@ -74,6 +74,10 @@ export interface WorkerProps<B extends Bindings = Bindings> {
74
74
  * Migrations to apply to the worker
75
75
  */
76
76
  migrations?: SingleStepMigration;
77
+ /**
78
+ * Whether to adopt the Worker if it already exists when creating
79
+ */
80
+ adopt?: boolean;
77
81
  }
78
82
  /**
79
83
  * Output returned after Worker creation/update
@@ -166,6 +170,20 @@ export interface Worker<B extends Bindings = Bindings> extends Resource<"cloudfl
166
170
  * }
167
171
  * });
168
172
  *
173
+ * @example
174
+ * // Create a worker with static assets:
175
+ * const staticAssets = await Assets("static", {
176
+ * path: "./src/assets"
177
+ * });
178
+ *
179
+ * const frontendWorker = await Worker("frontend", {
180
+ * name: "frontend-worker",
181
+ * entrypoint: "./src/worker.ts",
182
+ * bindings: {
183
+ * ASSETS: staticAssets
184
+ * }
185
+ * });
186
+ *
169
187
  * @see https://developers.cloudflare.com/workers/
170
188
  */
171
189
  export declare const Worker: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | (<const B extends Bindings>(this: Context<Worker<NoInfer<B>>>, id: string, props: WorkerProps<B>) => Promise<Worker<B>>);