alchemy 2.0.0-beta.36 → 2.0.0-beta.37

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 (198) hide show
  1. package/bin/exec.js +166 -43
  2. package/bin/exec.js.map +1 -1
  3. package/lib/AWS/Lambda/Function.d.ts.map +1 -1
  4. package/lib/AWS/Lambda/Function.js +22 -2
  5. package/lib/AWS/Lambda/Function.js.map +1 -1
  6. package/lib/Apply.d.ts.map +1 -1
  7. package/lib/Apply.js +6 -1
  8. package/lib/Apply.js.map +1 -1
  9. package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDataset.d.ts +54 -0
  10. package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDataset.d.ts.map +1 -0
  11. package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDataset.js +17 -0
  12. package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDataset.js.map +1 -0
  13. package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDatasetBinding.d.ts +37 -0
  14. package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDatasetBinding.d.ts.map +1 -0
  15. package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDatasetBinding.js +45 -0
  16. package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDatasetBinding.js.map +1 -0
  17. package/lib/Cloudflare/AnalyticsEngine/index.d.ts +3 -0
  18. package/lib/Cloudflare/AnalyticsEngine/index.d.ts.map +1 -0
  19. package/lib/Cloudflare/AnalyticsEngine/index.js +3 -0
  20. package/lib/Cloudflare/AnalyticsEngine/index.js.map +1 -0
  21. package/lib/Cloudflare/D1/D1Connection.d.ts +47 -3
  22. package/lib/Cloudflare/D1/D1Connection.d.ts.map +1 -1
  23. package/lib/Cloudflare/D1/D1Connection.js +62 -5
  24. package/lib/Cloudflare/D1/D1Connection.js.map +1 -1
  25. package/lib/Cloudflare/Local/SidecarHandlers.js +9 -1
  26. package/lib/Cloudflare/Local/SidecarHandlers.js.map +1 -1
  27. package/lib/Cloudflare/Providers.d.ts.map +1 -1
  28. package/lib/Cloudflare/Providers.js +4 -1
  29. package/lib/Cloudflare/Providers.js.map +1 -1
  30. package/lib/Cloudflare/R2/R2Bucket.d.ts.map +1 -1
  31. package/lib/Cloudflare/R2/R2Bucket.js +16 -0
  32. package/lib/Cloudflare/R2/R2Bucket.js.map +1 -1
  33. package/lib/Cloudflare/StateStore/Api.d.ts +6 -1
  34. package/lib/Cloudflare/StateStore/Api.d.ts.map +1 -1
  35. package/lib/Cloudflare/StateStore/Api.js +23 -1
  36. package/lib/Cloudflare/StateStore/Api.js.map +1 -1
  37. package/lib/Cloudflare/StateStore/Store.d.ts +16 -1
  38. package/lib/Cloudflare/StateStore/Store.d.ts.map +1 -1
  39. package/lib/Cloudflare/StateStore/Store.js +22 -5
  40. package/lib/Cloudflare/StateStore/Store.js.map +1 -1
  41. package/lib/Cloudflare/Workers/CronEventSource.d.ts +35 -0
  42. package/lib/Cloudflare/Workers/CronEventSource.d.ts.map +1 -0
  43. package/lib/Cloudflare/Workers/CronEventSource.js +55 -0
  44. package/lib/Cloudflare/Workers/CronEventSource.js.map +1 -0
  45. package/lib/Cloudflare/Workers/InferEnv.d.ts +37 -2
  46. package/lib/Cloudflare/Workers/InferEnv.d.ts.map +1 -1
  47. package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts.map +1 -1
  48. package/lib/Cloudflare/Workers/LocalWorkerProvider.js +2 -1
  49. package/lib/Cloudflare/Workers/LocalWorkerProvider.js.map +1 -1
  50. package/lib/Cloudflare/Workers/Rpc.d.ts +76 -3
  51. package/lib/Cloudflare/Workers/Rpc.d.ts.map +1 -1
  52. package/lib/Cloudflare/Workers/Rpc.js +197 -13
  53. package/lib/Cloudflare/Workers/Rpc.js.map +1 -1
  54. package/lib/Cloudflare/Workers/Worker.d.ts +22 -2
  55. package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
  56. package/lib/Cloudflare/Workers/Worker.js +174 -43
  57. package/lib/Cloudflare/Workers/Worker.js.map +1 -1
  58. package/lib/Cloudflare/Workers/WorkerBundle.d.ts.map +1 -1
  59. package/lib/Cloudflare/Workers/WorkerBundle.js +15 -5
  60. package/lib/Cloudflare/Workers/WorkerBundle.js.map +1 -1
  61. package/lib/Cloudflare/Workers/Workflow.d.ts +50 -47
  62. package/lib/Cloudflare/Workers/Workflow.d.ts.map +1 -1
  63. package/lib/Cloudflare/Workers/Workflow.js +41 -35
  64. package/lib/Cloudflare/Workers/Workflow.js.map +1 -1
  65. package/lib/Cloudflare/Workers/index.d.ts +1 -0
  66. package/lib/Cloudflare/Workers/index.d.ts.map +1 -1
  67. package/lib/Cloudflare/Workers/index.js +1 -0
  68. package/lib/Cloudflare/Workers/index.js.map +1 -1
  69. package/lib/Cloudflare/index.d.ts +1 -0
  70. package/lib/Cloudflare/index.d.ts.map +1 -1
  71. package/lib/Cloudflare/index.js +1 -0
  72. package/lib/Cloudflare/index.js.map +1 -1
  73. package/lib/Deploy.d.ts +4 -1
  74. package/lib/Deploy.d.ts.map +1 -1
  75. package/lib/Deploy.js +2 -2
  76. package/lib/Deploy.js.map +1 -1
  77. package/lib/Destroy.d.ts +4 -1
  78. package/lib/Destroy.d.ts.map +1 -1
  79. package/lib/Destroy.js +2 -2
  80. package/lib/Destroy.js.map +1 -1
  81. package/lib/Output.d.ts +45 -5
  82. package/lib/Output.d.ts.map +1 -1
  83. package/lib/Output.js +100 -4
  84. package/lib/Output.js.map +1 -1
  85. package/lib/Plan.d.ts.map +1 -1
  86. package/lib/Plan.js +93 -12
  87. package/lib/Plan.js.map +1 -1
  88. package/lib/Platform.d.ts.map +1 -1
  89. package/lib/Platform.js +16 -3
  90. package/lib/Platform.js.map +1 -1
  91. package/lib/Ref.d.ts +4 -1
  92. package/lib/Ref.d.ts.map +1 -1
  93. package/lib/Ref.js +1 -1
  94. package/lib/Ref.js.map +1 -1
  95. package/lib/Resource.d.ts +8 -0
  96. package/lib/Resource.d.ts.map +1 -1
  97. package/lib/Resource.js +10 -0
  98. package/lib/Resource.js.map +1 -1
  99. package/lib/RuntimeContext.d.ts +3 -1
  100. package/lib/RuntimeContext.d.ts.map +1 -1
  101. package/lib/RuntimeContext.js.map +1 -1
  102. package/lib/Secret.d.ts +50 -0
  103. package/lib/Secret.d.ts.map +1 -0
  104. package/lib/Secret.js +23 -0
  105. package/lib/Secret.js.map +1 -0
  106. package/lib/Serverless/Function.d.ts +9 -1
  107. package/lib/Serverless/Function.d.ts.map +1 -1
  108. package/lib/Stack.d.ts +14 -5
  109. package/lib/Stack.d.ts.map +1 -1
  110. package/lib/Stack.js +31 -19
  111. package/lib/Stack.js.map +1 -1
  112. package/lib/State/HttpStateApi.d.ts +15 -1
  113. package/lib/State/HttpStateApi.d.ts.map +1 -1
  114. package/lib/State/HttpStateApi.js +11 -0
  115. package/lib/State/HttpStateApi.js.map +1 -1
  116. package/lib/State/HttpStateStore.d.ts.map +1 -1
  117. package/lib/State/HttpStateStore.js +11 -0
  118. package/lib/State/HttpStateStore.js.map +1 -1
  119. package/lib/State/InMemoryState.d.ts +2 -2
  120. package/lib/State/InMemoryState.d.ts.map +1 -1
  121. package/lib/State/InMemoryState.js +12 -6
  122. package/lib/State/InMemoryState.js.map +1 -1
  123. package/lib/State/LocalState.d.ts.map +1 -1
  124. package/lib/State/LocalState.js +11 -2
  125. package/lib/State/LocalState.js.map +1 -1
  126. package/lib/State/State.d.ts +21 -0
  127. package/lib/State/State.d.ts.map +1 -1
  128. package/lib/Test/Bun.d.ts.map +1 -1
  129. package/lib/Test/Bun.js +30 -7
  130. package/lib/Test/Bun.js.map +1 -1
  131. package/lib/Test/Core.d.ts +17 -2
  132. package/lib/Test/Core.d.ts.map +1 -1
  133. package/lib/Test/Core.js +29 -11
  134. package/lib/Test/Core.js.map +1 -1
  135. package/lib/Test/Vitest.d.ts.map +1 -1
  136. package/lib/Test/Vitest.js +24 -5
  137. package/lib/Test/Vitest.js.map +1 -1
  138. package/lib/Util/LocalhostDns.d.ts +14 -10
  139. package/lib/Util/LocalhostDns.d.ts.map +1 -1
  140. package/lib/Util/LocalhostDns.js +27 -36
  141. package/lib/Util/LocalhostDns.js.map +1 -1
  142. package/lib/Util/effect.d.ts +7 -0
  143. package/lib/Util/effect.d.ts.map +1 -1
  144. package/lib/Util/effect.js +6 -0
  145. package/lib/Util/effect.js.map +1 -1
  146. package/lib/Variable.d.ts +29 -0
  147. package/lib/Variable.d.ts.map +1 -0
  148. package/lib/Variable.js +9 -0
  149. package/lib/Variable.js.map +1 -0
  150. package/lib/index.d.ts +3 -0
  151. package/lib/index.d.ts.map +1 -1
  152. package/lib/index.js +3 -0
  153. package/lib/index.js.map +1 -1
  154. package/lib/tsconfig.test.tsbuildinfo +1 -1
  155. package/package.json +6 -7
  156. package/src/AWS/Lambda/Function.ts +30 -2
  157. package/src/Apply.ts +8 -1
  158. package/src/Cloudflare/AnalyticsEngine/AnalyticsEngineDataset.ts +82 -0
  159. package/src/Cloudflare/AnalyticsEngine/AnalyticsEngineDatasetBinding.ts +106 -0
  160. package/src/Cloudflare/AnalyticsEngine/index.ts +2 -0
  161. package/src/Cloudflare/D1/D1Connection.ts +95 -12
  162. package/src/Cloudflare/Local/SidecarHandlers.ts +9 -1
  163. package/src/Cloudflare/Providers.ts +5 -0
  164. package/src/Cloudflare/R2/R2Bucket.ts +31 -0
  165. package/src/Cloudflare/StateStore/Api.ts +34 -1
  166. package/src/Cloudflare/StateStore/Store.ts +40 -10
  167. package/src/Cloudflare/Workers/CronEventSource.ts +89 -0
  168. package/src/Cloudflare/Workers/InferEnv.ts +94 -20
  169. package/src/Cloudflare/Workers/LocalWorkerProvider.ts +9 -1
  170. package/src/Cloudflare/Workers/Rpc.ts +227 -18
  171. package/src/Cloudflare/Workers/Worker.ts +277 -63
  172. package/src/Cloudflare/Workers/WorkerBundle.ts +15 -5
  173. package/src/Cloudflare/Workers/Workflow.ts +74 -70
  174. package/src/Cloudflare/Workers/index.ts +1 -0
  175. package/src/Cloudflare/index.ts +1 -0
  176. package/src/Deploy.ts +5 -1
  177. package/src/Destroy.ts +5 -1
  178. package/src/Output.ts +121 -12
  179. package/src/Plan.ts +101 -12
  180. package/src/Platform.ts +19 -4
  181. package/src/Ref.ts +10 -5
  182. package/src/Resource.ts +22 -0
  183. package/src/RuntimeContext.ts +1 -0
  184. package/src/Secret.ts +87 -0
  185. package/src/Serverless/Function.ts +10 -1
  186. package/src/Stack.ts +52 -29
  187. package/src/State/HttpStateApi.ts +21 -0
  188. package/src/State/HttpStateStore.ts +21 -0
  189. package/src/State/InMemoryState.ts +29 -8
  190. package/src/State/LocalState.ts +28 -4
  191. package/src/State/State.ts +21 -0
  192. package/src/Test/Bun.ts +43 -7
  193. package/src/Test/Core.ts +29 -8
  194. package/src/Test/Vitest.ts +33 -4
  195. package/src/Util/LocalhostDns.ts +30 -36
  196. package/src/Util/effect.ts +25 -0
  197. package/src/Variable.ts +44 -0
  198. package/src/index.ts +3 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alchemy",
3
- "version": "2.0.0-beta.36",
3
+ "version": "2.0.0-beta.37",
4
4
  "homepage": "https://alchemy.run",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Sam Goodwin <sam@alchemy.run>",
@@ -275,14 +275,14 @@
275
275
  "@ai-sdk/provider": "^3.0.8",
276
276
  "@aws-sdk/credential-providers": "^3.0.0",
277
277
  "@clack/prompts": "^0.11.0",
278
- "@distilled.cloud/aws": "^0.19.0",
279
- "@distilled.cloud/axiom": "^0.19.0",
280
- "@distilled.cloud/cloudflare": "^0.19.0",
278
+ "@distilled.cloud/aws": "^0.19.1",
279
+ "@distilled.cloud/axiom": "^0.19.1",
280
+ "@distilled.cloud/cloudflare": "^0.19.1",
281
281
  "@distilled.cloud/cloudflare-rolldown-plugin": "0.3.0",
282
282
  "@distilled.cloud/cloudflare-runtime": "0.3.2",
283
283
  "@distilled.cloud/cloudflare-vite-plugin": "0.2.0",
284
- "@distilled.cloud/core": "^0.19.0",
285
- "@distilled.cloud/neon": "^0.19.0",
284
+ "@distilled.cloud/core": "^0.19.1",
285
+ "@distilled.cloud/neon": "^0.19.1",
286
286
  "pg": "^8.13.0",
287
287
  "@effect/vitest": ">=4.0.0-beta.60 || >=4.0.0",
288
288
  "@libsql/client": "^0.17.0",
@@ -291,7 +291,6 @@
291
291
  "@smithy/shared-ini-file-loader": "^4.3.4",
292
292
  "@smithy/types": "^4.8.1",
293
293
  "@types/aws-lambda": "^8.10.152",
294
- "@types/libsodium-wrappers": "^0.8.2",
295
294
  "ai": "^6.0.62",
296
295
  "aws4fetch": "^1.0.20",
297
296
  "capnweb": "^0.6.1",
@@ -10,6 +10,7 @@ import * as Context from "effect/Context";
10
10
  import * as Effect from "effect/Effect";
11
11
  import * as FileSystem from "effect/FileSystem";
12
12
  import * as Option from "effect/Option";
13
+ import * as Redacted from "effect/Redacted";
13
14
  import * as Schedule from "effect/Schedule";
14
15
  import * as Stream from "effect/Stream";
15
16
  import type * as rolldown from "rolldown";
@@ -368,7 +369,21 @@ export const Function: Platform<
368
369
  set: (id: string, output: Output.Output) =>
369
370
  Effect.sync(() => {
370
371
  const key = id.replaceAll(/[^a-zA-Z0-9]/g, "_");
371
- env[key] = output.pipe(Output.map((value) => JSON.stringify(value)));
372
+ // Preserve `Redacted`-ness across the Output → Lambda env var
373
+ // round-trip. `JSON.stringify(Redacted)` would emit the literal
374
+ // string `"<redacted>"` and lose the value, so secrets are
375
+ // serialized with a `{_tag: "Redacted", value: ...}` marker
376
+ // that the runtime `get` path detects and rebuilds.
377
+ env[key] = output.pipe(
378
+ Output.map((value) =>
379
+ Redacted.isRedacted(value)
380
+ ? JSON.stringify({
381
+ _tag: "Redacted",
382
+ value: Redacted.value(value),
383
+ })
384
+ : JSON.stringify(value),
385
+ ),
386
+ );
372
387
  return key;
373
388
  }),
374
389
  get: <T>(key: string) =>
@@ -377,7 +392,20 @@ export const Function: Platform<
377
392
  .pipe(
378
393
  Effect.flatMap((val) =>
379
394
  Effect.try({
380
- try: () => JSON.parse(val) as T,
395
+ try: () => {
396
+ const value = JSON.parse(val);
397
+ if (
398
+ value !== null &&
399
+ typeof value === "object" &&
400
+ (value as { _tag?: unknown })._tag === "Redacted" &&
401
+ "value" in (value as object)
402
+ ) {
403
+ return Redacted.make(
404
+ (value as { value: unknown }).value,
405
+ ) as unknown as T;
406
+ }
407
+ return value as T;
408
+ },
381
409
  catch: () => val, // assume it's just a string
382
410
  }),
383
411
  ),
package/src/Apply.ts CHANGED
@@ -185,7 +185,14 @@ export const apply = <P extends Plan>(
185
185
  const outputs = Object.fromEntries(
186
186
  Object.entries(tracker).map(([fqn, t]) => [fqn, t.output]),
187
187
  );
188
- return yield* Output.evaluate(plan.output, outputs);
188
+ const resolved = yield* Output.evaluate(plan.output, outputs);
189
+
190
+ // Persist the stack's evaluated outputs so cross-stack references
191
+ // (`yield* OtherStack` / `OtherStack.stage.<name>` / `Output.stackRef`)
192
+ // can read them back out of the state store.
193
+ yield* state.setOutput({ stack: stackName, stage, value: resolved });
194
+
195
+ return resolved;
189
196
  }).pipe(
190
197
  ensureArtifactStore,
191
198
  Effect.withSpan("apply", {
@@ -0,0 +1,82 @@
1
+ import * as Effect from "effect/Effect";
2
+ import { AnalyticsEngineDatasetBinding } from "./AnalyticsEngineDatasetBinding.ts";
3
+
4
+ type AnalyticsEngineDatasetTypeId = typeof AnalyticsEngineDatasetTypeId;
5
+ const AnalyticsEngineDatasetTypeId =
6
+ "Cloudflare.AnalyticsEngineDataset" as const;
7
+
8
+ export type AnalyticsEngineDatasetProps = {
9
+ /**
10
+ * Dataset name. If omitted, the logical ID is used.
11
+ */
12
+ dataset?: string;
13
+ };
14
+
15
+ /**
16
+ * A Cloudflare Workers Analytics Engine dataset binding.
17
+ *
18
+ * Analytics Engine datasets are configured as Worker bindings. The binding
19
+ * exposes `writeDataPoint()` at runtime and does not require separate
20
+ * provisioning through the Cloudflare API.
21
+ *
22
+ * @resource
23
+ *
24
+ * @section Binding to a Worker
25
+ * @example Basic Analytics Engine binding
26
+ * ```typescript
27
+ * const Analytics = yield* Cloudflare.AnalyticsEngineDataset("Analytics", {
28
+ * dataset: "app-events",
29
+ * });
30
+ *
31
+ * export const Worker = Cloudflare.Worker("Worker", {
32
+ * main: "./src/worker.ts",
33
+ * bindings: { Analytics },
34
+ * });
35
+ * ```
36
+ *
37
+ * @example Effect-style worker
38
+ * ```typescript
39
+ * const analytics = yield* Cloudflare.AnalyticsEngineDataset.bind(Analytics);
40
+ * yield* analytics.writeDataPoint({ blobs: ["signup"] });
41
+ * ```
42
+ */
43
+ export type AnalyticsEngineDataset = {
44
+ kind: AnalyticsEngineDatasetTypeId;
45
+ name: string;
46
+ dataset: string;
47
+ };
48
+
49
+ export const isAnalyticsEngineDataset = (
50
+ value: unknown,
51
+ ): value is AnalyticsEngineDataset =>
52
+ typeof value === "object" &&
53
+ value !== null &&
54
+ "kind" in value &&
55
+ (value as AnalyticsEngineDataset).kind === AnalyticsEngineDatasetTypeId;
56
+
57
+ export const AnalyticsEngineDataset: {
58
+ (
59
+ name: string,
60
+ props?: AnalyticsEngineDatasetProps,
61
+ ): Effect.Effect<AnalyticsEngineDataset>;
62
+ /**
63
+ * Bind Analytics Engine to the surrounding Worker, returning an
64
+ * Effect-native client with access to the native Workers runtime binding.
65
+ */
66
+ bind: typeof AnalyticsEngineDatasetBinding.bind;
67
+ } = Object.assign(
68
+ Effect.fnUntraced(function* (
69
+ name: string,
70
+ props?: AnalyticsEngineDatasetProps,
71
+ ) {
72
+ return {
73
+ kind: AnalyticsEngineDatasetTypeId,
74
+ name,
75
+ dataset: props?.dataset ?? name,
76
+ } satisfies AnalyticsEngineDataset;
77
+ }),
78
+ {
79
+ bind: (...args: Parameters<typeof AnalyticsEngineDatasetBinding.bind>) =>
80
+ AnalyticsEngineDatasetBinding.bind(...args),
81
+ },
82
+ );
@@ -0,0 +1,106 @@
1
+ import * as Data from "effect/Data";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Layer from "effect/Layer";
4
+ import * as Binding from "../../Binding.ts";
5
+ import type { ResourceLike } from "../../Resource.ts";
6
+ import { isWorker, WorkerEnvironment } from "../Workers/Worker.ts";
7
+ import type { AnalyticsEngineDataset as AnalyticsEngineDatasetLike } from "./AnalyticsEngineDataset.ts";
8
+
9
+ export interface AnalyticsEngineDataPoint {
10
+ indexes?: string[];
11
+ blobs?: string[];
12
+ doubles?: number[];
13
+ }
14
+
15
+ export interface RuntimeAnalyticsEngineDataset {
16
+ writeDataPoint(dataPoint: AnalyticsEngineDataPoint): void;
17
+ }
18
+
19
+ export class AnalyticsEngineDatasetError extends Data.TaggedError(
20
+ "AnalyticsEngineDatasetError",
21
+ )<{
22
+ message: string;
23
+ cause: Error;
24
+ }> {}
25
+
26
+ export interface AnalyticsEngineDatasetClient {
27
+ raw: Effect.Effect<RuntimeAnalyticsEngineDataset, never, WorkerEnvironment>;
28
+ writeDataPoint(
29
+ dataPoint: AnalyticsEngineDataPoint,
30
+ ): Effect.Effect<void, AnalyticsEngineDatasetError, WorkerEnvironment>;
31
+ }
32
+
33
+ export class AnalyticsEngineDatasetBinding extends Binding.Service<
34
+ AnalyticsEngineDatasetBinding,
35
+ (
36
+ dataset: AnalyticsEngineDatasetLike,
37
+ ) => Effect.Effect<AnalyticsEngineDatasetClient>
38
+ >()("Cloudflare.AnalyticsEngineDataset.Binding") {}
39
+
40
+ export const AnalyticsEngineDatasetBindingLive = Layer.effect(
41
+ AnalyticsEngineDatasetBinding,
42
+ Effect.gen(function* () {
43
+ const bind = yield* AnalyticsEngineDatasetBindingPolicy;
44
+
45
+ return Effect.fnUntraced(function* (dataset: AnalyticsEngineDatasetLike) {
46
+ yield* bind(dataset);
47
+
48
+ const raw = WorkerEnvironment.asEffect().pipe(
49
+ Effect.map(
50
+ (env) =>
51
+ (env as Record<string, RuntimeAnalyticsEngineDataset>)[
52
+ dataset.name
53
+ ]!,
54
+ ),
55
+ );
56
+
57
+ return {
58
+ raw,
59
+ writeDataPoint: (dataPoint) =>
60
+ raw.pipe(
61
+ Effect.flatMap((raw) =>
62
+ Effect.try({
63
+ try: () => raw.writeDataPoint(dataPoint),
64
+ catch: (error: any) =>
65
+ new AnalyticsEngineDatasetError({
66
+ message: error?.message ?? "Unknown error",
67
+ cause: error,
68
+ }),
69
+ }),
70
+ ),
71
+ ),
72
+ } satisfies AnalyticsEngineDatasetClient;
73
+ });
74
+ }),
75
+ );
76
+
77
+ export class AnalyticsEngineDatasetBindingPolicy extends Binding.Policy<
78
+ AnalyticsEngineDatasetBindingPolicy,
79
+ (dataset: AnalyticsEngineDatasetLike) => Effect.Effect<void>
80
+ >()("Cloudflare.AnalyticsEngineDataset.Binding") {}
81
+
82
+ export const AnalyticsEngineDatasetBindingPolicyLive =
83
+ AnalyticsEngineDatasetBindingPolicy.layer.succeed(
84
+ Effect.fnUntraced(function* (
85
+ host: ResourceLike,
86
+ dataset: AnalyticsEngineDatasetLike,
87
+ ) {
88
+ if (isWorker(host)) {
89
+ yield* host.bind(dataset.name, {
90
+ bindings: [
91
+ {
92
+ type: "analytics_engine",
93
+ name: dataset.name,
94
+ dataset: dataset.dataset,
95
+ },
96
+ ],
97
+ });
98
+ } else {
99
+ return yield* Effect.die(
100
+ new Error(
101
+ `AnalyticsEngineDatasetBinding does not support runtime '${host.Type}'`,
102
+ ),
103
+ );
104
+ }
105
+ }),
106
+ );
@@ -0,0 +1,2 @@
1
+ export * from "./AnalyticsEngineDataset.ts";
2
+ export * from "./AnalyticsEngineDatasetBinding.ts";
@@ -7,6 +7,86 @@ import { WorkerEnvironment } from "../Workers/Worker.ts";
7
7
  import type { D1Database } from "./D1Database.ts";
8
8
  import { DatabaseBinding } from "./D1DatabaseBinding.ts";
9
9
 
10
+ /**
11
+ * Effect-native wrapper around a Cloudflare D1 prepared statement.
12
+ *
13
+ * Construction (`prepare`) and binding (`bind`) are synchronous —
14
+ * they're plan builders and don't touch D1. Only the executors
15
+ * (`all`, `first`, `run`, `raw`) round-trip to the database, and
16
+ * those return Effects so they participate in the Effect runtime.
17
+ */
18
+ export class D1PreparedStatement {
19
+ /** @internal */
20
+ constructor(
21
+ private readonly query: string,
22
+ private readonly binds: ReadonlyArray<unknown>,
23
+ private readonly rawEff: Effect.Effect<runtime.D1Database>,
24
+ ) {}
25
+
26
+ /**
27
+ * Return a new prepared statement bound to `values`. Subsequent
28
+ * calls replace the previous binding, matching the underlying
29
+ * Cloudflare runtime semantics.
30
+ */
31
+ bind(...values: unknown[]): D1PreparedStatement {
32
+ return new D1PreparedStatement(this.query, values, this.rawEff);
33
+ }
34
+
35
+ /** Run the query and return all matching rows. */
36
+ all<T = unknown>(): Effect.Effect<runtime.D1Result<T>> {
37
+ return this.withRuntime((stmt) => stmt.all<T>());
38
+ }
39
+
40
+ /** Run the query and return the first row, or `null` if no rows. */
41
+ first<T = unknown>(): Effect.Effect<T | null>;
42
+ first<T = unknown>(column: string): Effect.Effect<T | null>;
43
+ first<T = unknown>(column?: string): Effect.Effect<T | null> {
44
+ return this.withRuntime((stmt) =>
45
+ column !== undefined ? stmt.first<T>(column) : stmt.first<T>(),
46
+ );
47
+ }
48
+
49
+ /** Run the query as a mutation; returns row metadata. */
50
+ run<T = unknown>(): Effect.Effect<runtime.D1Result<T>> {
51
+ return this.withRuntime((stmt) => stmt.run<T>());
52
+ }
53
+
54
+ /** Run the query and return rows as flat arrays. */
55
+ raw<T = unknown[]>(): Effect.Effect<T[]>;
56
+ raw<T = unknown[]>(options: {
57
+ columnNames: true;
58
+ }): Effect.Effect<[string[], ...T[]]>;
59
+ raw<T = unknown[]>(options?: {
60
+ columnNames: true;
61
+ }): Effect.Effect<T[] | [string[], ...T[]]> {
62
+ return this.withRuntime((stmt) =>
63
+ options
64
+ ? stmt.raw<T>(options)
65
+ : (stmt.raw<T>() as Promise<T[] | [string[], ...T[]]>),
66
+ );
67
+ }
68
+
69
+ /**
70
+ * Materialize the underlying Cloudflare prepared statement against
71
+ * a concrete D1 binding. Used by `D1ConnectionClient.batch` to
72
+ * collect statements for a single transactional call.
73
+ *
74
+ * @internal
75
+ */
76
+ _build(raw: runtime.D1Database): runtime.D1PreparedStatement {
77
+ const stmt = raw.prepare(this.query);
78
+ return this.binds.length > 0 ? stmt.bind(...this.binds) : stmt;
79
+ }
80
+
81
+ private withRuntime<A>(
82
+ fn: (stmt: runtime.D1PreparedStatement) => Promise<A>,
83
+ ): Effect.Effect<A> {
84
+ return Effect.flatMap(this.rawEff, (raw) =>
85
+ Effect.promise(() => fn(this._build(raw))),
86
+ );
87
+ }
88
+ }
89
+
10
90
  export interface D1ConnectionClient {
11
91
  /**
12
92
  * An Effect that resolves to the raw underlying Cloudflare D1Database binding.
@@ -14,9 +94,11 @@ export interface D1ConnectionClient {
14
94
  */
15
95
  raw: Effect.Effect<runtime.D1Database>;
16
96
  /**
17
- * Prepare a SQL query statement for later execution.
97
+ * Prepare a SQL statement. Returns synchronously — the network
98
+ * round-trip happens when you yield one of the statement's
99
+ * executors (`all`, `first`, `run`, `raw`).
18
100
  */
19
- prepare: (query: string) => Effect.Effect<runtime.D1PreparedStatement>;
101
+ prepare: (query: string) => D1PreparedStatement;
20
102
  /**
21
103
  * Execute raw SQL without prepared statements.
22
104
  */
@@ -26,7 +108,7 @@ export interface D1ConnectionClient {
26
108
  * Statements execute sequentially and are rolled back on failure.
27
109
  */
28
110
  batch: <T = unknown>(
29
- statements: runtime.D1PreparedStatement[],
111
+ statements: D1PreparedStatement[],
30
112
  ) => Effect.Effect<runtime.D1Result<T>[]>;
31
113
  }
32
114
 
@@ -42,22 +124,23 @@ export const D1ConnectionLive = Layer.effect(
42
124
 
43
125
  return Effect.fn(function* (database: D1Database) {
44
126
  yield* Policy(database);
45
- const d1 = yield* Effect.serviceOption(WorkerEnvironment).pipe(
127
+ const rawEff = yield* Effect.serviceOption(WorkerEnvironment).pipe(
46
128
  Effect.map(Option.getOrUndefined),
47
129
  Effect.map((env) => env?.[database.LogicalId]! as runtime.D1Database),
48
130
  Effect.cached,
49
131
  );
50
132
 
51
133
  return {
52
- raw: d1,
53
- prepare: (query: string) =>
54
- d1.pipe(Effect.map((d1) => d1.prepare(query))),
134
+ raw: rawEff,
135
+ prepare: (query: string) => new D1PreparedStatement(query, [], rawEff),
55
136
  exec: (query: string) =>
56
- d1.pipe(Effect.flatMap((d1) => Effect.promise(() => d1.exec(query)))),
57
- batch: <T = unknown>(statements: runtime.D1PreparedStatement[]) =>
58
- d1.pipe(
59
- Effect.flatMap((d1) =>
60
- Effect.promise(() => d1.batch<T>(statements)),
137
+ Effect.flatMap(rawEff, (raw) =>
138
+ Effect.promise(() => raw.exec(query)),
139
+ ),
140
+ batch: <T = unknown>(statements: D1PreparedStatement[]) =>
141
+ Effect.flatMap(rawEff, (raw) =>
142
+ Effect.promise(() =>
143
+ raw.batch<T>(statements.map((s) => s._build(raw))),
61
144
  ),
62
145
  ),
63
146
  } satisfies D1ConnectionClient;
@@ -169,7 +169,15 @@ function bundleOutputToWorkerModules(
169
169
  ): WorkerModule[] {
170
170
  const modules: WorkerModule[] = [];
171
171
  for (const file of bundle.files) {
172
- if (file.path.endsWith(".map") || file.content instanceof Uint8Array) {
172
+ if (file.path.endsWith(".map")) {
173
+ continue;
174
+ }
175
+ if (file.content instanceof Uint8Array) {
176
+ modules.push({
177
+ name: file.path,
178
+ type: file.path.endsWith(".wasm") ? "Wasm" : "Data",
179
+ content: file.content,
180
+ });
173
181
  continue;
174
182
  }
175
183
  modules.push({
@@ -13,6 +13,7 @@ import { ProfileLive } from "../Auth/Profile.ts";
13
13
  import { Random, RandomProvider } from "../Random.ts";
14
14
  import * as Access from "./Access.ts";
15
15
  import * as AiGateway from "./AiGateway/index.ts";
16
+ import * as AnalyticsEngine from "./AnalyticsEngine/index.ts";
16
17
  import * as ApiToken from "./ApiToken/index.ts";
17
18
  import * as Artifacts from "./Artifacts/index.ts";
18
19
  import { CloudflareAuth } from "./Auth/AuthProvider.ts";
@@ -50,6 +51,7 @@ export const providers = () =>
50
51
  ApiToken.UserApiToken,
51
52
  AiGateway.AiGateway,
52
53
  AiGateway.AiGatewayBindingPolicy,
54
+ AnalyticsEngine.AnalyticsEngineDatasetBindingPolicy,
53
55
  Artifacts.ArtifactsBindingPolicy,
54
56
  Command,
55
57
  Containers.Container,
@@ -73,6 +75,7 @@ export const providers = () =>
73
75
  VpcService.VpcService,
74
76
  Random,
75
77
  Workers.BindWorkerPolicy,
78
+ Workers.CronEventSourcePolicy,
76
79
  Workers.FetchPolicy,
77
80
  Workers.Worker,
78
81
  Workflows.WorkflowResource,
@@ -84,6 +87,7 @@ export const providers = () =>
84
87
  ApiToken.UserApiTokenProvider(),
85
88
  AiGateway.AiGatewayProvider(),
86
89
  AiGateway.AiGatewayBindingPolicyLive,
90
+ AnalyticsEngine.AnalyticsEngineDatasetBindingPolicyLive,
87
91
  Artifacts.ArtifactsBindingPolicyLive,
88
92
  Containers.ContainerProvider(),
89
93
  D1.D1ConnectionPolicyLive,
@@ -105,6 +109,7 @@ export const providers = () =>
105
109
  Tunnel.TunnelProvider(),
106
110
  VpcService.VpcServiceProvider(),
107
111
  Workers.BindWorkerPolicyLive,
112
+ Workers.CronEventSourcePolicyLive,
108
113
  Workers.FetchPolicyLive,
109
114
  Workers.WorkerProvider(),
110
115
  Workflows.WorkflowProvider(),
@@ -1,6 +1,7 @@
1
1
  import * as r2 from "@distilled.cloud/cloudflare/r2";
2
2
  import * as Effect from "effect/Effect";
3
3
  import * as Schedule from "effect/Schedule";
4
+ import * as Stream from "effect/Stream";
4
5
 
5
6
  import { deepEqual, isResolved } from "../../Diff.ts";
6
7
  import { createPhysicalName } from "../../PhysicalName.ts";
@@ -355,6 +356,7 @@ export const R2BucketProvider = () =>
355
356
  const patchBucket = yield* r2.patchBucket;
356
357
  const deleteBucket = yield* r2.deleteBucket;
357
358
  const getBucket = yield* r2.getBucket;
359
+ const deleteObjects = yield* r2.deleteObjects;
358
360
  const listBucketDomainCustoms = yield* r2.listBucketDomainCustoms;
359
361
  const createBucketDomainCustom = yield* r2.createBucketDomainCustom;
360
362
  const updateBucketDomainCustom = yield* r2.updateBucketDomainCustom;
@@ -362,6 +364,34 @@ export const R2BucketProvider = () =>
362
364
  const getBucketLifecycle = yield* r2.getBucketLifecycle;
363
365
  const putBucketLifecycle = yield* r2.putBucketLifecycle;
364
366
 
367
+ const emptyBucket = (
368
+ bucketName: string,
369
+ jurisdiction: R2Bucket.Jurisdiction,
370
+ ) =>
371
+ r2.listObjects
372
+ .items({
373
+ accountId,
374
+ bucketName,
375
+ cfR2Jurisdiction: jurisdiction,
376
+ perPage: 1000,
377
+ })
378
+ .pipe(
379
+ Stream.filter(
380
+ (o): o is typeof o & { key: string } =>
381
+ typeof o.key === "string" && o.key !== "",
382
+ ),
383
+ Stream.map((o) => o.key),
384
+ Stream.runForEachArray((chunk) =>
385
+ deleteObjects({
386
+ accountId,
387
+ bucketName,
388
+ cfR2Jurisdiction: jurisdiction,
389
+ body: [...chunk],
390
+ }),
391
+ ),
392
+ Effect.catchTag("NoSuchBucket", () => Effect.void),
393
+ );
394
+
365
395
  const createBucketName = (id: string, name: string | undefined) =>
366
396
  Effect.gen(function* () {
367
397
  if (name) return name;
@@ -683,6 +713,7 @@ export const R2BucketProvider = () =>
683
713
  Effect.catchIf(isMissingCustomDomainOrBucket, () => Effect.void),
684
714
  );
685
715
  }
716
+ yield* emptyBucket(output.bucketName, output.jurisdiction);
686
717
  yield* deleteBucket({
687
718
  accountId: output.accountId,
688
719
  bucketName: output.bucketName,
@@ -29,7 +29,7 @@ export const STATE_STORE_SCRIPT_NAME = "alchemy-state-store" as const;
29
29
  * compare against this constant; a mismatch (or 404) triggers a
30
30
  * forced redeploy via the bootstrap flow.
31
31
  */
32
- export const STATE_STORE_VERSION = 3 as const;
32
+ export const STATE_STORE_VERSION = 4 as const;
33
33
 
34
34
  /**
35
35
  * Hard-coded OTLP/HTTP endpoints. Point at the public ingest relay
@@ -245,6 +245,39 @@ export default Worker(
245
245
  }),
246
246
  ),
247
247
  )
248
+ .handle("getStackOutput", ({ params }) =>
249
+ store
250
+ .getByName(params.stack)
251
+ .getOutput({ stage: params.stage })
252
+ .pipe(
253
+ Effect.withSpan("state_store.getStackOutput", {
254
+ attributes: {
255
+ "alchemy.state_store.op": "getStackOutput",
256
+ "alchemy.state_store.stack": params.stack,
257
+ "alchemy.state_store.stage": params.stage,
258
+ },
259
+ }),
260
+ ),
261
+ )
262
+ .handle("setStackOutput", ({ params, payload }) =>
263
+ store
264
+ .getByName(params.stack)
265
+ .setOutput({ stage: params.stage, value: payload as any })
266
+ .pipe(
267
+ Effect.tap(() =>
268
+ store
269
+ .getByName(Store.ROOT_DO_NAME)
270
+ .registerStack({ stack: params.stack }),
271
+ ),
272
+ Effect.withSpan("state_store.setStackOutput", {
273
+ attributes: {
274
+ "alchemy.state_store.op": "setStackOutput",
275
+ "alchemy.state_store.stack": params.stack,
276
+ "alchemy.state_store.stage": params.stage,
277
+ },
278
+ }),
279
+ ),
280
+ )
248
281
  .handle("deleteStack", ({ params, query }) =>
249
282
  store
250
283
  .getByName(params.stack)
@@ -36,7 +36,7 @@ export default class Store extends DurableObjectNamespace<Store>()(
36
36
  ),
37
37
  ).pipe(Effect.orDie);
38
38
 
39
- const encryptValue = (value: ResourceState) =>
39
+ const encryptValue = (value: unknown) =>
40
40
  Effect.tryPromise(async () => {
41
41
  const plaintext = new TextEncoder().encode(
42
42
  JSON.stringify(encodeState(value)),
@@ -188,15 +188,39 @@ export default class Store extends DurableObjectNamespace<Store>()(
188
188
  deleteStack: ({ stage }: { stage?: string } = {}) =>
189
189
  stage === undefined
190
190
  ? storage.deleteAll()
191
- : storage
192
- .list<string>({ prefix: stagePrefix(stage) })
193
- .pipe(
194
- Effect.flatMap((entries) =>
195
- entries.size === 0
196
- ? Effect.void
197
- : storage.delete([...entries.keys()]).pipe(Effect.asVoid),
198
- ),
199
- ),
191
+ : storage.list<string>({ prefix: stagePrefix(stage) }).pipe(
192
+ Effect.flatMap((entries) => {
193
+ const keys = [...entries.keys(), stackOutputKey(stage)];
194
+ return storage.delete(keys).pipe(Effect.asVoid);
195
+ }),
196
+ ),
197
+
198
+ /**
199
+ * (Stack DO only) Read the persisted stack output for `stage`.
200
+ * Returns `undefined` when the stage has not been deployed.
201
+ */
202
+ getOutput: ({ stage }: { stage: string }) =>
203
+ storage
204
+ .get<string>(stackOutputKey(stage))
205
+ .pipe(
206
+ Effect.flatMap((entry) =>
207
+ entry == null ? Effect.succeed(undefined) : decryptEntry(entry),
208
+ ),
209
+ ),
210
+
211
+ /**
212
+ * (Stack DO only) Persist the resolved stack output for
213
+ * `stage`. Returns the stored value unchanged.
214
+ */
215
+ setOutput: ({ stage, value }: { stage: string; value: any }) =>
216
+ encryptValue(value).pipe(
217
+ Effect.flatMap((encrypted) =>
218
+ storage
219
+ .put<string>(stackOutputKey(stage), encrypted)
220
+ .pipe(Effect.asVoid),
221
+ ),
222
+ Effect.map(() => value),
223
+ ),
200
224
 
201
225
  /**
202
226
  * (Stack DO only) Return every resource in a stage whose
@@ -236,6 +260,9 @@ const SEP = "\x00";
236
260
  /** Key prefix for resource entries in a stack DO. */
237
261
  const RESOURCE_PREFIX = `r${SEP}`;
238
262
 
263
+ /** Key prefix for stack-output entries in a stack DO. */
264
+ const STACK_OUTPUT_PREFIX = `o${SEP}`;
265
+
239
266
  /** Key prefix for stack-index entries in the root DO. */
240
267
  const STACK_INDEX_PREFIX = "s:";
241
268
 
@@ -249,6 +276,9 @@ const resourceKey = (stage: string, fqn: string) =>
249
276
  /** Prefix matching every resource key inside a specific stage. */
250
277
  const stagePrefix = (stage: string) => `${RESOURCE_PREFIX}${stage}${SEP}`;
251
278
 
279
+ /** Build the stack-output key inside a *stack DO*. */
280
+ const stackOutputKey = (stage: string) => `${STACK_OUTPUT_PREFIX}${stage}`;
281
+
252
282
  /**
253
283
  * Parse a resource key back into its (stage, fqn) tuple. Returns
254
284
  * undefined for keys that do not match the expected shape.