alchemy 0.48.3 → 0.49.1

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 (113) hide show
  1. package/bin/alchemy.mjs +15 -15
  2. package/bin/commands/create.ts +0 -15
  3. package/bin/commands/deploy.ts +2 -0
  4. package/bin/commands/dev.ts +10 -1
  5. package/bin/services/execute-alchemy.ts +9 -0
  6. package/lib/alchemy.d.ts +13 -1
  7. package/lib/alchemy.d.ts.map +1 -1
  8. package/lib/alchemy.js +3 -1
  9. package/lib/alchemy.js.map +1 -1
  10. package/lib/apply.js +22 -18
  11. package/lib/apply.js.map +1 -1
  12. package/lib/aws/s3-state-store.d.ts +0 -1
  13. package/lib/aws/s3-state-store.d.ts.map +1 -1
  14. package/lib/aws/s3-state-store.js +1 -3
  15. package/lib/aws/s3-state-store.js.map +1 -1
  16. package/lib/cloudflare/bindings.d.ts +21 -2
  17. package/lib/cloudflare/bindings.d.ts.map +1 -1
  18. package/lib/cloudflare/bindings.js.map +1 -1
  19. package/lib/cloudflare/bound.d.ts +8 -1
  20. package/lib/cloudflare/bound.d.ts.map +1 -1
  21. package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
  22. package/lib/cloudflare/compatibility-date.gen.js +1 -1
  23. package/lib/cloudflare/index.d.ts +1 -0
  24. package/lib/cloudflare/index.d.ts.map +1 -1
  25. package/lib/cloudflare/index.js +1 -0
  26. package/lib/cloudflare/index.js.map +1 -1
  27. package/lib/cloudflare/miniflare/miniflare-worker-options.d.ts.map +1 -1
  28. package/lib/cloudflare/miniflare/miniflare-worker-options.js +4 -0
  29. package/lib/cloudflare/miniflare/miniflare-worker-options.js.map +1 -1
  30. package/lib/cloudflare/rate-limit.d.ts +63 -0
  31. package/lib/cloudflare/rate-limit.d.ts.map +1 -0
  32. package/lib/cloudflare/rate-limit.js +41 -0
  33. package/lib/cloudflare/rate-limit.js.map +1 -0
  34. package/lib/cloudflare/route.d.ts.map +1 -1
  35. package/lib/cloudflare/route.js +3 -0
  36. package/lib/cloudflare/route.js.map +1 -1
  37. package/lib/cloudflare/tanstack-start.d.ts.map +1 -1
  38. package/lib/cloudflare/tanstack-start.js +10 -1
  39. package/lib/cloudflare/tanstack-start.js.map +1 -1
  40. package/lib/cloudflare/website.d.ts +4 -0
  41. package/lib/cloudflare/website.d.ts.map +1 -1
  42. package/lib/cloudflare/website.js +14 -1
  43. package/lib/cloudflare/website.js.map +1 -1
  44. package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
  45. package/lib/cloudflare/worker-metadata.js +8 -0
  46. package/lib/cloudflare/worker-metadata.js.map +1 -1
  47. package/lib/cloudflare/worker.js +1 -1
  48. package/lib/cloudflare/worker.js.map +1 -1
  49. package/lib/cloudflare/wrangler.json.d.ts +11 -0
  50. package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
  51. package/lib/cloudflare/wrangler.json.js +15 -1
  52. package/lib/cloudflare/wrangler.json.js.map +1 -1
  53. package/lib/context.d.ts +2 -2
  54. package/lib/context.d.ts.map +1 -1
  55. package/lib/destroy.d.ts +3 -1
  56. package/lib/destroy.d.ts.map +1 -1
  57. package/lib/destroy.js +23 -4
  58. package/lib/destroy.js.map +1 -1
  59. package/lib/planetscale/password.js +1 -1
  60. package/lib/planetscale/password.js.map +1 -1
  61. package/lib/resource.d.ts +9 -0
  62. package/lib/resource.d.ts.map +1 -1
  63. package/lib/resource.js +2 -0
  64. package/lib/resource.js.map +1 -1
  65. package/lib/scope.d.ts +15 -0
  66. package/lib/scope.d.ts.map +1 -1
  67. package/lib/scope.js +9 -3
  68. package/lib/scope.js.map +1 -1
  69. package/lib/test/bun.d.ts +1 -24
  70. package/lib/test/bun.d.ts.map +1 -1
  71. package/lib/test/bun.js.map +1 -1
  72. package/lib/test/options.d.ts +37 -0
  73. package/lib/test/options.d.ts.map +1 -0
  74. package/lib/test/options.js +1 -0
  75. package/lib/test/options.js.map +1 -0
  76. package/lib/test/vitest.d.ts +1 -24
  77. package/lib/test/vitest.d.ts.map +1 -1
  78. package/lib/test/vitest.js.map +1 -1
  79. package/lib/util/ignore-matcher.d.ts.map +1 -1
  80. package/lib/util/ignore-matcher.js +1 -3
  81. package/lib/util/ignore-matcher.js.map +1 -1
  82. package/lib/util/safe-fetch.d.ts.map +1 -1
  83. package/lib/util/safe-fetch.js +9 -5
  84. package/lib/util/safe-fetch.js.map +1 -1
  85. package/package.json +2 -2
  86. package/src/alchemy.ts +16 -2
  87. package/src/apply.ts +23 -21
  88. package/src/aws/s3-state-store.ts +2 -2
  89. package/src/cloudflare/bindings.ts +22 -0
  90. package/src/cloudflare/bound.ts +29 -22
  91. package/src/cloudflare/compatibility-date.gen.ts +1 -1
  92. package/src/cloudflare/index.ts +1 -0
  93. package/src/cloudflare/miniflare/miniflare-worker-options.ts +4 -0
  94. package/src/cloudflare/rate-limit.ts +70 -0
  95. package/src/cloudflare/route.ts +4 -0
  96. package/src/cloudflare/tanstack-start.ts +10 -1
  97. package/src/cloudflare/website.ts +20 -1
  98. package/src/cloudflare/worker-metadata.ts +7 -0
  99. package/src/cloudflare/worker.ts +1 -1
  100. package/src/cloudflare/wrangler.json.ts +30 -1
  101. package/src/context.ts +2 -2
  102. package/src/destroy.ts +27 -5
  103. package/src/planetscale/password.ts +1 -1
  104. package/src/resource.ts +11 -0
  105. package/src/scope.ts +21 -4
  106. package/src/test/bun.ts +1 -28
  107. package/src/test/options.ts +42 -0
  108. package/src/test/vitest.ts +1 -28
  109. package/src/util/ignore-matcher.ts +0 -1
  110. package/src/util/safe-fetch.ts +12 -5
  111. package/templates/tanstack-start/alchemy.run.ts +1 -3
  112. package/templates/tanstack-start/package.json +3 -3
  113. package/templates/tanstack-start/vite.config.ts +26 -25
package/src/apply.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { alchemy } from "./alchemy.ts";
2
2
  import { context } from "./context.ts";
3
- import { destroy } from "./destroy.ts";
3
+ import { destroy, DestroyStrategy } from "./destroy.ts";
4
4
  import {
5
5
  PROVIDERS,
6
6
  ResourceFQN,
@@ -92,6 +92,7 @@ async function _apply<Out extends Resource>(
92
92
  [ResourceKind]: resource[ResourceKind],
93
93
  [ResourceScope]: scope,
94
94
  [ResourceSeq]: resource[ResourceSeq],
95
+ [DestroyStrategy]: provider.options?.destroyStrategy ?? "sequential",
95
96
  },
96
97
  // deps: [...deps],
97
98
  props,
@@ -113,9 +114,14 @@ async function _apply<Out extends Resource>(
113
114
  });
114
115
  if (
115
116
  JSON.stringify(oldProps) === JSON.stringify(newProps) &&
116
- alwaysUpdate !== true
117
+ alwaysUpdate !== true &&
118
+ !scope.force
117
119
  ) {
118
- scope.skip();
120
+ const innerScope = new Scope({
121
+ parent: scope,
122
+ scopeName: resource[ResourceID],
123
+ });
124
+ innerScope.skip();
119
125
  if (!quiet) {
120
126
  logger.task(resource[ResourceFQN], {
121
127
  prefix: "skipped",
@@ -125,12 +131,7 @@ async function _apply<Out extends Resource>(
125
131
  status: "success",
126
132
  });
127
133
  }
128
- options?.resolveInnerScope?.(
129
- new Scope({
130
- parent: scope,
131
- scopeName: resource[ResourceID],
132
- }),
133
- );
134
+ options?.resolveInnerScope?.(innerScope);
134
135
  scope.telemetryClient.record({
135
136
  event: "resource.skip",
136
137
  resource: resource[ResourceKind],
@@ -174,7 +175,7 @@ async function _apply<Out extends Resource>(
174
175
  props: state.oldProps,
175
176
  state,
176
177
  isReplacement: false,
177
- replace: async (force?: boolean) => {
178
+ replace: (force?: boolean) => {
178
179
  if (phase === "create") {
179
180
  throw new Error(
180
181
  `Resource ${resource[ResourceKind]} ${resource[ResourceFQN]} cannot be replaced in create phase.`,
@@ -186,17 +187,6 @@ async function _apply<Out extends Resource>(
186
187
  );
187
188
  }
188
189
 
189
- if (force) {
190
- await destroy(resource, {
191
- quiet: scope.quiet,
192
- strategy: "sequential",
193
- replace: {
194
- props: state.oldProps,
195
- output: resource,
196
- },
197
- });
198
- }
199
-
200
190
  isReplaced = true;
201
191
  throw new ReplacedSignal(force);
202
192
  },
@@ -209,6 +199,7 @@ async function _apply<Out extends Resource>(
209
199
  {
210
200
  isResource: true,
211
201
  parent: scope,
202
+ destroyStrategy: provider.options?.destroyStrategy ?? "sequential",
212
203
  },
213
204
  async (scope) => {
214
205
  options?.resolveInnerScope?.(scope);
@@ -223,6 +214,17 @@ async function _apply<Out extends Resource>(
223
214
  );
224
215
  }
225
216
 
217
+ if (error.force) {
218
+ await destroy(resource, {
219
+ quiet: scope.quiet,
220
+ strategy: resource[DestroyStrategy] ?? "sequential",
221
+ replace: {
222
+ props: state.oldProps,
223
+ output: resource,
224
+ },
225
+ });
226
+ }
227
+
226
228
  output = await alchemy.run(
227
229
  resource[ResourceID],
228
230
  { isResource: true, parent: scope },
@@ -9,7 +9,7 @@ import {
9
9
  } from "@aws-sdk/client-s3";
10
10
  import { ResourceScope } from "../resource.ts";
11
11
  import type { Scope } from "../scope.ts";
12
- import { serialize, deserialize } from "../serde.ts";
12
+ import { deserialize, serialize } from "../serde.ts";
13
13
  import type { State, StateStore } from "../state.ts";
14
14
  import { ignore } from "../util/ignore.ts";
15
15
  import { retry } from "./retry.ts";
@@ -55,7 +55,7 @@ export class S3StateStore implements StateStore {
55
55
  */
56
56
  constructor(
57
57
  public readonly scope: Scope,
58
- private readonly options: S3StateStoreOptions = {},
58
+ options: S3StateStoreOptions = {},
59
59
  ) {
60
60
  // Use the scope's chain to build the prefix, similar to how FileSystemStateStore builds its directory
61
61
  const scopePath = scope.chain.join("/");
@@ -20,6 +20,7 @@ import type { Images } from "./images.ts";
20
20
  import type { KVNamespaceResource } from "./kv-namespace.ts";
21
21
  import type { PipelineResource } from "./pipeline.ts";
22
22
  import type { QueueResource } from "./queue.ts";
23
+ import type { RateLimit } from "./rate-limit.ts";
23
24
  import type { SecretKey } from "./secret-key.ts";
24
25
  import type { Secret as CloudflareSecret } from "./secret.ts";
25
26
  import type { VectorizeIndexResource } from "./vectorize-index.ts";
@@ -56,6 +57,7 @@ export type Binding =
56
57
  | KVNamespaceResource
57
58
  | PipelineResource
58
59
  | QueueResource
60
+ | RateLimit
59
61
  | R2BucketResource
60
62
  | {
61
63
  type: "kv_namespace";
@@ -108,6 +110,7 @@ export type WorkerBindingSpec =
108
110
  | WorkerBindingPipeline
109
111
  | WorkerBindingPlainText
110
112
  | WorkerBindingQueue
113
+ | WorkerBindingRateLimit
111
114
  | WorkerBindingR2Bucket
112
115
  | WorkerBindingSecretKey
113
116
  | WorkerBindingSecretText
@@ -284,6 +287,25 @@ export interface WorkerBindingQueue {
284
287
  queue_name: string;
285
288
  }
286
289
 
290
+ /**
291
+ * Rate Limit binding type
292
+ */
293
+ export interface WorkerBindingRateLimit {
294
+ /** The name of the binding */
295
+ name: string;
296
+ /** Type identifier for Rate Limit binding */
297
+ type: "ratelimit";
298
+ /** Namespace ID for the rate limit */
299
+ namespace_id: string;
300
+ /** Simple rate limiting configuration */
301
+ simple: {
302
+ /** Maximum number of requests */
303
+ limit: number;
304
+ /** Time period in seconds */
305
+ period: 60 | 10;
306
+ };
307
+ }
308
+
287
309
  /**
288
310
  * R2 Bucket binding type
289
311
  */
@@ -15,6 +15,7 @@ import type { HyperdriveResource as _Hyperdrive } from "./hyperdrive.ts";
15
15
  import type { Images as _Images } from "./images.ts";
16
16
  import type { PipelineResource as _Pipeline } from "./pipeline.ts";
17
17
  import type { QueueResource as _Queue } from "./queue.ts";
18
+ import type { RateLimit } from "./rate-limit.ts";
18
19
  import type { SecretKey } from "./secret-key.ts";
19
20
  import type { Secret as CloudflareSecret } from "./secret.ts";
20
21
  import type { VectorizeIndexResource as _VectorizeIndex } from "./vectorize-index.ts";
@@ -74,25 +75,31 @@ export type Bound<T extends Binding> = T extends _DurableObjectNamespace<
74
75
  ? AnalyticsEngineDataset
75
76
  : T extends _Pipeline<infer R>
76
77
  ? Pipeline<R>
77
- : T extends string
78
- ? string
79
- : T extends BrowserRendering
80
- ? Fetcher
81
- : T extends _Ai<infer M>
82
- ? Ai<M>
83
- : T extends _Images
84
- ? ImagesBinding
85
- : T extends _VersionMetadata
86
- ? WorkerVersionMetadata
87
- : T extends Self
88
- ? Service
89
- : T extends Json<infer T>
90
- ? T
91
- : T extends _Container<
92
- infer Obj
93
- >
94
- ? DurableObjectNamespace<
95
- Obj &
96
- Rpc.DurableObjectBranded
97
- >
98
- : Service;
78
+ : T extends RateLimit
79
+ ? {
80
+ limit(options: {
81
+ key: string;
82
+ }): Promise<{ success: boolean }>;
83
+ }
84
+ : T extends string
85
+ ? string
86
+ : T extends BrowserRendering
87
+ ? Fetcher
88
+ : T extends _Ai<infer M>
89
+ ? Ai<M>
90
+ : T extends _Images
91
+ ? ImagesBinding
92
+ : T extends _VersionMetadata
93
+ ? WorkerVersionMetadata
94
+ : T extends Self
95
+ ? Service
96
+ : T extends Json<infer T>
97
+ ? T
98
+ : T extends _Container<
99
+ infer Obj
100
+ >
101
+ ? DurableObjectNamespace<
102
+ Obj &
103
+ Rpc.DurableObjectBranded
104
+ >
105
+ : Service;
@@ -4,4 +4,4 @@
4
4
  /**
5
5
  * The default Cloudflare Workers compatibility date, set based on the latest workerd release at the time of build.
6
6
  */
7
- export const DEFAULT_COMPATIBILITY_DATE = "2025-07-15";
7
+ export const DEFAULT_COMPATIBILITY_DATE = "2025-07-19";
@@ -36,6 +36,7 @@ export * from "./permission-groups.ts";
36
36
  export * from "./pipeline.ts";
37
37
  export * from "./queue-consumer.ts";
38
38
  export * from "./queue.ts";
39
+ export * from "./rate-limit.ts";
39
40
  export * from "./react-router.ts";
40
41
  export * from "./redirect-rule.ts";
41
42
  export * from "./redwood.ts";
@@ -563,6 +563,10 @@ export async function buildMiniflareWorkerOptions({
563
563
  }
564
564
  break;
565
565
  }
566
+ case "ratelimit": {
567
+ (options.ratelimits ??= {})[name] = binding;
568
+ break;
569
+ }
566
570
  default: {
567
571
  assertNever(binding);
568
572
  }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Base interface for rate limiting configuration.
3
+ */
4
+ export interface RateLimitProps {
5
+ /**
6
+ * A positive integer that uniquely defines this rate limiting configuration (e.g., namespace_id = 999).
7
+ */
8
+ namespace_id: number;
9
+ /**
10
+ * Simple rate limiting configuration.
11
+ */
12
+ simple: {
13
+ /**
14
+ * The limit (number of requests or API calls) to be applied. This is incremented when you call the limit() function in your Worker.
15
+ */
16
+ limit: number;
17
+ /**
18
+ * The period, in seconds, to measure increments to the limit over. Must be either 10 or 60.
19
+ */
20
+ period: 60 | 10;
21
+ };
22
+ }
23
+
24
+ /**
25
+ * RateLimit binding for Cloudflare Workers.
26
+ */
27
+ export interface RateLimit extends RateLimitProps {
28
+ type: "ratelimit";
29
+ }
30
+
31
+ /**
32
+ * Rate limiting binding for Cloudflare Workers.
33
+ *
34
+ * The RateLimit binding provides access to Cloudflare's rate limiting functionality,
35
+ * allowing you to implement rate limiting directly in your Workers without needing
36
+ * to rely on external services.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * import { Worker, RateLimit } from "alchemy/cloudflare";
41
+ *
42
+ * const rateLimit = RateLimit({
43
+ * namespace_id: 1001,
44
+ * simple: {
45
+ * limit: 1500,
46
+ * period: 60
47
+ * }
48
+ * });
49
+ *
50
+ * await Worker("my-worker", {
51
+ * name: "my-worker",
52
+ * entrypoint: "./src/worker.ts",
53
+ * bindings: {
54
+ * MY_RATE_LIMIT: rateLimit
55
+ * }
56
+ * });
57
+ * ```
58
+ *
59
+ * @see https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/
60
+ *
61
+ * Creates a rate limiting binding for Cloudflare Workers.
62
+ * @returns A RateLimit binding object.
63
+ */
64
+ export function RateLimit(options: RateLimitProps): RateLimit {
65
+ return {
66
+ type: "ratelimit",
67
+ namespace_id: options.namespace_id,
68
+ simple: options.simple,
69
+ };
70
+ }
@@ -282,6 +282,10 @@ async function updateRoute(
282
282
  },
283
283
  );
284
284
 
285
+ if (updateResponse.status === 404) {
286
+ return await createRoute(api, zoneId, pattern, script);
287
+ }
288
+
285
289
  if (!updateResponse.ok) {
286
290
  return await handleApiError(updateResponse, "updating", "Route", pattern);
287
291
  }
@@ -17,7 +17,16 @@ export async function TanStackStart<B extends Bindings>(
17
17
  ): Promise<TanStackStart<B>> {
18
18
  return Website(id, {
19
19
  ...props,
20
- command: props?.command ?? "bun run build",
20
+ command: props?.command ?? "vite build",
21
+ commandEnv: {
22
+ // TODO(sam): should probably be set for all Websites, or at least all Vite Websites
23
+ // needs to be set or else jsxDev is included in the bundle and breaks in production
24
+ NODE_ENV: process.env.NODE_ENV ?? "production",
25
+ },
26
+ noBundle: true,
27
+ dev: props?.dev ?? {
28
+ command: "vite dev",
29
+ },
21
30
  wrangler: props?.wrangler ?? true,
22
31
  main: props?.main ?? ".output/server/index.mjs",
23
32
  compatibilityFlags: ["nodejs_compat", ...(props?.compatibilityFlags ?? [])],
@@ -2,6 +2,7 @@ import path from "node:path";
2
2
  import { alchemy } from "../alchemy.ts";
3
3
  import { Exec } from "../os/exec.ts";
4
4
  import { Scope } from "../scope.ts";
5
+ import { isSecret } from "../secret.ts";
5
6
  import { detectPackageManager } from "../util/detect-package-manager.ts";
6
7
  import { Assets } from "./assets.ts";
7
8
  import type { Bindings } from "./bindings.ts";
@@ -18,6 +19,11 @@ export interface WebsiteProps<B extends Bindings>
18
19
  */
19
20
  command?: string;
20
21
 
22
+ /**
23
+ * Additional environment variables to set when running the command
24
+ */
25
+ commandEnv?: Record<string, string>;
26
+
21
27
  /**
22
28
  * Whether to memoize the command (only re-run if the command changes)
23
29
  *
@@ -224,7 +230,20 @@ export default {
224
230
  await Exec("build", {
225
231
  cwd,
226
232
  command: props.command,
227
- env: props.env,
233
+ env: {
234
+ ...(props.env ?? {}),
235
+ ...(props.commandEnv ?? {}),
236
+ ...Object.fromEntries(
237
+ Object.entries(props.bindings ?? {}).flatMap(([key, value]) => {
238
+ if (isSecret(value)) {
239
+ return [[key, value.unencrypted]];
240
+ } else if (typeof value === "string") {
241
+ return [[key, value]];
242
+ }
243
+ return [];
244
+ }),
245
+ ),
246
+ },
228
247
  memoize: props.memoize,
229
248
  });
230
249
  }
@@ -565,6 +565,13 @@ export async function prepareWorkerMetadata(
565
565
  configureClassMigration(bindingName, binding);
566
566
  }
567
567
  (meta.containers ??= []).push({ class_name: binding.className });
568
+ } else if (binding.type === "ratelimit") {
569
+ meta.bindings.push({
570
+ type: "ratelimit",
571
+ name: bindingName,
572
+ namespace_id: binding.namespace_id.toString(),
573
+ simple: binding.simple,
574
+ });
568
575
  } else {
569
576
  assertNever(
570
577
  binding,
@@ -1166,7 +1166,7 @@ export const _Worker = Resource(
1166
1166
 
1167
1167
  if (oldName !== newName) {
1168
1168
  if (dispatchNamespace) {
1169
- await this.replace(true);
1169
+ this.replace(true);
1170
1170
  } else {
1171
1171
  const renameResponse = await api.patch(
1172
1172
  `/accounts/${api.accountId}/workers/services/${oldName}`,
@@ -4,13 +4,21 @@ import type { Context } from "../context.ts";
4
4
  import { formatJson } from "../fs/static-json-file.ts";
5
5
  import { Resource } from "../resource.ts";
6
6
  import { assertNever } from "../util/assert-never.ts";
7
- import { Self, type Bindings } from "./bindings.ts";
7
+ import {
8
+ Self,
9
+ type Bindings,
10
+ type WorkerBindingRateLimit,
11
+ } from "./bindings.ts";
8
12
  import type { DurableObjectNamespace } from "./durable-object-namespace.ts";
9
13
  import type { EventSource } from "./event-source.ts";
10
14
  import { isQueueEventSource } from "./event-source.ts";
11
15
  import { isQueue } from "./queue.ts";
12
16
  import type { Worker, WorkerProps } from "./worker.ts";
13
17
 
18
+ type WranglerJsonRateLimit = Omit<WorkerBindingRateLimit, "type"> & {
19
+ type: "rate_limit";
20
+ };
21
+
14
22
  /**
15
23
  * Properties for wrangler.json configuration file
16
24
  */
@@ -447,6 +455,13 @@ export interface WranglerJsonSpec {
447
455
  namespace: string;
448
456
  experimental_remote?: boolean;
449
457
  }[];
458
+
459
+ /**
460
+ * Unsafe bindings section for experimental features
461
+ */
462
+ unsafe?: {
463
+ bindings: WranglerJsonRateLimit[];
464
+ };
450
465
  }
451
466
 
452
467
  /**
@@ -534,6 +549,7 @@ function processBindings(
534
549
  namespace: string;
535
550
  experimental_remote?: boolean;
536
551
  }[] = [];
552
+ const unsafeBindings: WranglerJsonRateLimit[] = [];
537
553
  const containers: {
538
554
  class_name: string;
539
555
  }[] = [];
@@ -721,6 +737,13 @@ function processBindings(
721
737
  namespace: binding.namespaceName,
722
738
  experimental_remote: true,
723
739
  });
740
+ } else if (binding.type === "ratelimit") {
741
+ unsafeBindings.push({
742
+ name: bindingName,
743
+ type: "rate_limit",
744
+ namespace_id: binding.namespace_id.toString(),
745
+ simple: binding.simple,
746
+ });
724
747
  } else if (binding.type === "secret_key") {
725
748
  // no-op
726
749
  } else if (binding.type === "container") {
@@ -802,4 +825,10 @@ function processBindings(
802
825
  if (dispatchNamespaces.length > 0) {
803
826
  spec.dispatch_namespaces = dispatchNamespaces;
804
827
  }
828
+
829
+ if (unsafeBindings.length > 0) {
830
+ spec.unsafe = {
831
+ bindings: unsafeBindings,
832
+ };
833
+ }
805
834
  }
package/src/context.ts CHANGED
@@ -57,7 +57,7 @@ export interface BaseContext<Out extends Resource> {
57
57
  * Indicate that this resource is being replaced.
58
58
  * This will cause the resource to be deleted at the end of the stack's CREATE phase.
59
59
  */
60
- replace(force?: boolean): Promise<never>;
60
+ replace(force?: boolean): never;
61
61
  /**
62
62
  * Terminate the resource lifecycle handler and destroy the resource.
63
63
  *
@@ -102,7 +102,7 @@ export function context<
102
102
  seq: number;
103
103
  props: Props;
104
104
  state: State<Kind, Props, Out>;
105
- replace: (force?: boolean) => Promise<never>;
105
+ replace: (force?: boolean) => never;
106
106
  isReplacement?: boolean;
107
107
  }): Context<Out> {
108
108
  type InternalSymbols =
package/src/destroy.ts CHANGED
@@ -16,9 +16,13 @@ import { logger } from "./util/logger.ts";
16
16
 
17
17
  export class DestroyedSignal extends Error {}
18
18
 
19
+ export type DestroyStrategy = "sequential" | "parallel";
20
+
21
+ export const DestroyStrategy = Symbol.for("alchemy::DestroyStrategy");
22
+
19
23
  export interface DestroyOptions {
20
24
  quiet?: boolean;
21
- strategy?: "sequential" | "parallel";
25
+ strategy?: DestroyStrategy;
22
26
  replace?: {
23
27
  props?: ResourceProps | undefined;
24
28
  output?: Resource<string>;
@@ -40,7 +44,7 @@ export async function destroy<Type extends string>(
40
44
  if (isScopeArgs(args)) {
41
45
  const [scope] = args;
42
46
  const options = {
43
- strategy: "sequential",
47
+ strategy: scope.destroyStrategy ?? "sequential",
44
48
  ...(args[1] ?? {}),
45
49
  } satisfies DestroyOptions;
46
50
 
@@ -118,7 +122,20 @@ export async function destroy<Type extends string>(
118
122
  fqn: instance[ResourceFQN],
119
123
  seq: instance[ResourceSeq],
120
124
  props: options?.replace?.props ?? state.props,
121
- state,
125
+ state: options?.replace?.output
126
+ ? {
127
+ output: options.replace.output,
128
+ status: "deleting",
129
+ oldProps: options.replace.props,
130
+ data: {},
131
+ kind: instance[ResourceKind],
132
+ id: instance[ResourceID],
133
+ fqn: instance[ResourceFQN],
134
+ seq: instance[ResourceSeq],
135
+ props: options.replace.props,
136
+ }
137
+ : state,
138
+ // TODO(sam|michael): should this always be false or !!options?.replace
122
139
  isReplacement: false,
123
140
  replace: () => {
124
141
  throw new Error("Cannot replace a resource that is being deleted");
@@ -134,6 +151,7 @@ export async function destroy<Type extends string>(
134
151
  // TODO(sam): this is an awful hack to differentiate between naked scopes and resources
135
152
  isResource: instance[ResourceKind] !== "alchemy::Scope",
136
153
  parent: scope,
154
+ destroyStrategy: instance[DestroyStrategy] ?? "sequential",
137
155
  },
138
156
  async (scope) => {
139
157
  nestedScope = options?.replace?.props == null ? scope : undefined;
@@ -151,13 +169,17 @@ export async function destroy<Type extends string>(
151
169
  }
152
170
  }
153
171
 
172
+ const destroyOptions = {
173
+ ...options,
174
+ strategy: instance[DestroyStrategy] ?? "sequential",
175
+ };
154
176
  if (nestedScope) {
155
- await destroy(nestedScope, options);
177
+ await destroy(nestedScope, destroyOptions);
156
178
  }
157
179
 
158
180
  if (options?.replace == null) {
159
181
  if (nestedScope) {
160
- await destroy(nestedScope, options);
182
+ await destroy(nestedScope, destroyOptions);
161
183
  }
162
184
  await scope.deleteResource(instance[ResourceID]);
163
185
  } else {
@@ -306,7 +306,7 @@ export const Password = Resource(
306
306
  this.output.cidrs.length === props.cidrs.length &&
307
307
  this.output.cidrs.every((cidr, i) => cidr === props.cidrs![i])))
308
308
  ) {
309
- return await this.replace();
309
+ return this.replace();
310
310
  }
311
311
  const updateResponse = await api.patch(
312
312
  `/organizations/${props.organizationId}/databases/${databaseName}/branches/${branchName}/passwords/${this.output.id}`,
package/src/resource.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { apply } from "./apply.ts";
2
2
  import type { Context } from "./context.ts";
3
+ import { DestroyStrategy } from "./destroy.ts";
3
4
  import { Scope as _Scope, type Scope } from "./scope.ts";
4
5
 
5
6
  declare global {
@@ -59,6 +60,13 @@ export interface ProviderOptions {
59
60
  * If true, the resource will be updated even if the inputs have not changed.
60
61
  */
61
62
  alwaysUpdate: boolean;
63
+
64
+ /**
65
+ * The strategy to use when destroying the resource.
66
+ *
67
+ * @default "sequential"
68
+ */
69
+ destroyStrategy?: DestroyStrategy;
62
70
  }
63
71
 
64
72
  export type ResourceProps = {
@@ -81,6 +89,7 @@ export interface PendingResource<Out = unknown> extends Promise<Out> {
81
89
  [ResourceFQN]: ResourceFQN;
82
90
  [ResourceScope]: Scope;
83
91
  [ResourceSeq]: number;
92
+ [DestroyStrategy]: DestroyStrategy;
84
93
  [InnerResourceScope]: Promise<Scope>;
85
94
  }
86
95
 
@@ -90,6 +99,7 @@ export interface Resource<Kind extends ResourceKind = ResourceKind> {
90
99
  [ResourceFQN]: ResourceFQN;
91
100
  [ResourceScope]: Scope;
92
101
  [ResourceSeq]: number;
102
+ [DestroyStrategy]: DestroyStrategy;
93
103
  }
94
104
 
95
105
  // helper for semantic syntax highlighting (color as a type/class instead of function/value)
@@ -167,6 +177,7 @@ export function Resource<
167
177
  [ResourceFQN]: scope.fqn(resourceID),
168
178
  [ResourceSeq]: seq,
169
179
  [ResourceScope]: scope,
180
+ [DestroyStrategy]: options?.destroyStrategy ?? "sequential",
170
181
  [InnerResourceScope]: new Promise<Scope>((resolve) => {
171
182
  resolveInnerScope = resolve;
172
183
  }),