alchemy 2.0.0-beta.35 → 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 (222) hide show
  1. package/bin/exec.js +176 -26
  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/Cli/main.d.ts.map +1 -1
  10. package/lib/Cli/main.js +2 -0
  11. package/lib/Cli/main.js.map +1 -1
  12. package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDataset.d.ts +54 -0
  13. package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDataset.d.ts.map +1 -0
  14. package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDataset.js +17 -0
  15. package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDataset.js.map +1 -0
  16. package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDatasetBinding.d.ts +37 -0
  17. package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDatasetBinding.d.ts.map +1 -0
  18. package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDatasetBinding.js +45 -0
  19. package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDatasetBinding.js.map +1 -0
  20. package/lib/Cloudflare/AnalyticsEngine/index.d.ts +3 -0
  21. package/lib/Cloudflare/AnalyticsEngine/index.d.ts.map +1 -0
  22. package/lib/Cloudflare/AnalyticsEngine/index.js +3 -0
  23. package/lib/Cloudflare/AnalyticsEngine/index.js.map +1 -0
  24. package/lib/Cloudflare/D1/D1Connection.d.ts +47 -3
  25. package/lib/Cloudflare/D1/D1Connection.d.ts.map +1 -1
  26. package/lib/Cloudflare/D1/D1Connection.js +62 -5
  27. package/lib/Cloudflare/D1/D1Connection.js.map +1 -1
  28. package/lib/Cloudflare/Images/Images.d.ts +103 -0
  29. package/lib/Cloudflare/Images/Images.d.ts.map +1 -0
  30. package/lib/Cloudflare/Images/Images.js +82 -0
  31. package/lib/Cloudflare/Images/Images.js.map +1 -0
  32. package/lib/Cloudflare/Images/ImagesBinding.d.ts +73 -0
  33. package/lib/Cloudflare/Images/ImagesBinding.d.ts.map +1 -0
  34. package/lib/Cloudflare/Images/ImagesBinding.js +83 -0
  35. package/lib/Cloudflare/Images/ImagesBinding.js.map +1 -0
  36. package/lib/Cloudflare/Images/index.d.ts +3 -0
  37. package/lib/Cloudflare/Images/index.d.ts.map +1 -0
  38. package/lib/Cloudflare/Images/index.js +3 -0
  39. package/lib/Cloudflare/Images/index.js.map +1 -0
  40. package/lib/Cloudflare/Local/SidecarHandlers.js +9 -1
  41. package/lib/Cloudflare/Local/SidecarHandlers.js.map +1 -1
  42. package/lib/Cloudflare/Providers.d.ts.map +1 -1
  43. package/lib/Cloudflare/Providers.js +6 -1
  44. package/lib/Cloudflare/Providers.js.map +1 -1
  45. package/lib/Cloudflare/R2/R2Bucket.d.ts +135 -0
  46. package/lib/Cloudflare/R2/R2Bucket.d.ts.map +1 -1
  47. package/lib/Cloudflare/R2/R2Bucket.js +146 -7
  48. package/lib/Cloudflare/R2/R2Bucket.js.map +1 -1
  49. package/lib/Cloudflare/StateStore/Api.d.ts +6 -1
  50. package/lib/Cloudflare/StateStore/Api.d.ts.map +1 -1
  51. package/lib/Cloudflare/StateStore/Api.js +23 -1
  52. package/lib/Cloudflare/StateStore/Api.js.map +1 -1
  53. package/lib/Cloudflare/StateStore/Store.d.ts +16 -1
  54. package/lib/Cloudflare/StateStore/Store.d.ts.map +1 -1
  55. package/lib/Cloudflare/StateStore/Store.js +22 -5
  56. package/lib/Cloudflare/StateStore/Store.js.map +1 -1
  57. package/lib/Cloudflare/Workers/CronEventSource.d.ts +35 -0
  58. package/lib/Cloudflare/Workers/CronEventSource.d.ts.map +1 -0
  59. package/lib/Cloudflare/Workers/CronEventSource.js +55 -0
  60. package/lib/Cloudflare/Workers/CronEventSource.js.map +1 -0
  61. package/lib/Cloudflare/Workers/InferEnv.d.ts +37 -2
  62. package/lib/Cloudflare/Workers/InferEnv.d.ts.map +1 -1
  63. package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts.map +1 -1
  64. package/lib/Cloudflare/Workers/LocalWorkerProvider.js +2 -1
  65. package/lib/Cloudflare/Workers/LocalWorkerProvider.js.map +1 -1
  66. package/lib/Cloudflare/Workers/Rpc.d.ts +76 -3
  67. package/lib/Cloudflare/Workers/Rpc.d.ts.map +1 -1
  68. package/lib/Cloudflare/Workers/Rpc.js +197 -13
  69. package/lib/Cloudflare/Workers/Rpc.js.map +1 -1
  70. package/lib/Cloudflare/Workers/Worker.d.ts +26 -6
  71. package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
  72. package/lib/Cloudflare/Workers/Worker.js +183 -41
  73. package/lib/Cloudflare/Workers/Worker.js.map +1 -1
  74. package/lib/Cloudflare/Workers/WorkerBundle.d.ts.map +1 -1
  75. package/lib/Cloudflare/Workers/WorkerBundle.js +15 -5
  76. package/lib/Cloudflare/Workers/WorkerBundle.js.map +1 -1
  77. package/lib/Cloudflare/Workers/Workflow.d.ts +50 -47
  78. package/lib/Cloudflare/Workers/Workflow.d.ts.map +1 -1
  79. package/lib/Cloudflare/Workers/Workflow.js +41 -35
  80. package/lib/Cloudflare/Workers/Workflow.js.map +1 -1
  81. package/lib/Cloudflare/Workers/index.d.ts +1 -0
  82. package/lib/Cloudflare/Workers/index.d.ts.map +1 -1
  83. package/lib/Cloudflare/Workers/index.js +1 -0
  84. package/lib/Cloudflare/Workers/index.js.map +1 -1
  85. package/lib/Cloudflare/index.d.ts +2 -0
  86. package/lib/Cloudflare/index.d.ts.map +1 -1
  87. package/lib/Cloudflare/index.js +2 -0
  88. package/lib/Cloudflare/index.js.map +1 -1
  89. package/lib/Deploy.d.ts +4 -1
  90. package/lib/Deploy.d.ts.map +1 -1
  91. package/lib/Deploy.js +2 -2
  92. package/lib/Deploy.js.map +1 -1
  93. package/lib/Destroy.d.ts +4 -1
  94. package/lib/Destroy.d.ts.map +1 -1
  95. package/lib/Destroy.js +2 -2
  96. package/lib/Destroy.js.map +1 -1
  97. package/lib/Output.d.ts +45 -5
  98. package/lib/Output.d.ts.map +1 -1
  99. package/lib/Output.js +100 -4
  100. package/lib/Output.js.map +1 -1
  101. package/lib/Plan.d.ts.map +1 -1
  102. package/lib/Plan.js +93 -12
  103. package/lib/Plan.js.map +1 -1
  104. package/lib/Platform.d.ts.map +1 -1
  105. package/lib/Platform.js +16 -3
  106. package/lib/Platform.js.map +1 -1
  107. package/lib/Ref.d.ts +4 -1
  108. package/lib/Ref.d.ts.map +1 -1
  109. package/lib/Ref.js +1 -1
  110. package/lib/Ref.js.map +1 -1
  111. package/lib/Resource.d.ts +8 -0
  112. package/lib/Resource.d.ts.map +1 -1
  113. package/lib/Resource.js +10 -0
  114. package/lib/Resource.js.map +1 -1
  115. package/lib/RuntimeContext.d.ts +3 -1
  116. package/lib/RuntimeContext.d.ts.map +1 -1
  117. package/lib/RuntimeContext.js.map +1 -1
  118. package/lib/Secret.d.ts +50 -0
  119. package/lib/Secret.d.ts.map +1 -0
  120. package/lib/Secret.js +23 -0
  121. package/lib/Secret.js.map +1 -0
  122. package/lib/Serverless/Function.d.ts +9 -1
  123. package/lib/Serverless/Function.d.ts.map +1 -1
  124. package/lib/Stack.d.ts +14 -5
  125. package/lib/Stack.d.ts.map +1 -1
  126. package/lib/Stack.js +31 -19
  127. package/lib/Stack.js.map +1 -1
  128. package/lib/State/HttpStateApi.d.ts +15 -1
  129. package/lib/State/HttpStateApi.d.ts.map +1 -1
  130. package/lib/State/HttpStateApi.js +11 -0
  131. package/lib/State/HttpStateApi.js.map +1 -1
  132. package/lib/State/HttpStateStore.d.ts.map +1 -1
  133. package/lib/State/HttpStateStore.js +11 -0
  134. package/lib/State/HttpStateStore.js.map +1 -1
  135. package/lib/State/InMemoryState.d.ts +2 -2
  136. package/lib/State/InMemoryState.d.ts.map +1 -1
  137. package/lib/State/InMemoryState.js +12 -6
  138. package/lib/State/InMemoryState.js.map +1 -1
  139. package/lib/State/LocalState.d.ts.map +1 -1
  140. package/lib/State/LocalState.js +11 -2
  141. package/lib/State/LocalState.js.map +1 -1
  142. package/lib/State/State.d.ts +21 -0
  143. package/lib/State/State.d.ts.map +1 -1
  144. package/lib/Test/Bun.d.ts.map +1 -1
  145. package/lib/Test/Bun.js +30 -7
  146. package/lib/Test/Bun.js.map +1 -1
  147. package/lib/Test/Core.d.ts +17 -2
  148. package/lib/Test/Core.d.ts.map +1 -1
  149. package/lib/Test/Core.js +29 -11
  150. package/lib/Test/Core.js.map +1 -1
  151. package/lib/Test/Vitest.d.ts.map +1 -1
  152. package/lib/Test/Vitest.js +26 -5
  153. package/lib/Test/Vitest.js.map +1 -1
  154. package/lib/Util/LocalhostDns.d.ts +23 -0
  155. package/lib/Util/LocalhostDns.d.ts.map +1 -0
  156. package/lib/Util/LocalhostDns.js +46 -0
  157. package/lib/Util/LocalhostDns.js.map +1 -0
  158. package/lib/Util/PlatformServices.d.ts.map +1 -1
  159. package/lib/Util/PlatformServices.js +2 -0
  160. package/lib/Util/PlatformServices.js.map +1 -1
  161. package/lib/Util/effect.d.ts +7 -0
  162. package/lib/Util/effect.d.ts.map +1 -1
  163. package/lib/Util/effect.js +6 -0
  164. package/lib/Util/effect.js.map +1 -1
  165. package/lib/Variable.d.ts +29 -0
  166. package/lib/Variable.d.ts.map +1 -0
  167. package/lib/Variable.js +9 -0
  168. package/lib/Variable.js.map +1 -0
  169. package/lib/index.d.ts +3 -0
  170. package/lib/index.d.ts.map +1 -1
  171. package/lib/index.js +3 -0
  172. package/lib/index.js.map +1 -1
  173. package/lib/tsconfig.test.tsbuildinfo +1 -1
  174. package/package.json +7 -7
  175. package/src/AWS/Lambda/Function.ts +30 -2
  176. package/src/Apply.ts +8 -1
  177. package/src/Cli/main.ts +3 -0
  178. package/src/Cloudflare/AnalyticsEngine/AnalyticsEngineDataset.ts +82 -0
  179. package/src/Cloudflare/AnalyticsEngine/AnalyticsEngineDatasetBinding.ts +106 -0
  180. package/src/Cloudflare/AnalyticsEngine/index.ts +2 -0
  181. package/src/Cloudflare/D1/D1Connection.ts +95 -12
  182. package/src/Cloudflare/Images/Images.ts +121 -0
  183. package/src/Cloudflare/Images/ImagesBinding.ts +192 -0
  184. package/src/Cloudflare/Images/index.ts +2 -0
  185. package/src/Cloudflare/Local/SidecarHandlers.ts +9 -1
  186. package/src/Cloudflare/Providers.ts +8 -0
  187. package/src/Cloudflare/R2/R2Bucket.ts +271 -7
  188. package/src/Cloudflare/StateStore/Api.ts +34 -1
  189. package/src/Cloudflare/StateStore/Store.ts +40 -10
  190. package/src/Cloudflare/Workers/CronEventSource.ts +89 -0
  191. package/src/Cloudflare/Workers/InferEnv.ts +94 -16
  192. package/src/Cloudflare/Workers/LocalWorkerProvider.ts +9 -1
  193. package/src/Cloudflare/Workers/Rpc.ts +227 -18
  194. package/src/Cloudflare/Workers/Worker.ts +295 -62
  195. package/src/Cloudflare/Workers/WorkerBundle.ts +15 -5
  196. package/src/Cloudflare/Workers/Workflow.ts +74 -70
  197. package/src/Cloudflare/Workers/index.ts +1 -0
  198. package/src/Cloudflare/index.ts +2 -0
  199. package/src/Deploy.ts +5 -1
  200. package/src/Destroy.ts +5 -1
  201. package/src/Output.ts +121 -12
  202. package/src/Plan.ts +101 -12
  203. package/src/Platform.ts +19 -4
  204. package/src/Ref.ts +10 -5
  205. package/src/Resource.ts +22 -0
  206. package/src/RuntimeContext.ts +1 -0
  207. package/src/Secret.ts +87 -0
  208. package/src/Serverless/Function.ts +10 -1
  209. package/src/Stack.ts +52 -29
  210. package/src/State/HttpStateApi.ts +21 -0
  211. package/src/State/HttpStateStore.ts +21 -0
  212. package/src/State/InMemoryState.ts +29 -8
  213. package/src/State/LocalState.ts +28 -4
  214. package/src/State/State.ts +21 -0
  215. package/src/Test/Bun.ts +43 -7
  216. package/src/Test/Core.ts +29 -8
  217. package/src/Test/Vitest.ts +36 -4
  218. package/src/Util/LocalhostDns.ts +52 -0
  219. package/src/Util/PlatformServices.ts +2 -0
  220. package/src/Util/effect.ts +25 -0
  221. package/src/Variable.ts +44 -0
  222. package/src/index.ts +3 -0
@@ -2,17 +2,18 @@ import type * as cf from "@cloudflare/workers-types";
2
2
  import cloudflareVite from "@distilled.cloud/cloudflare-vite-plugin";
3
3
  import * as workers from "@distilled.cloud/cloudflare/workers";
4
4
  import * as zones from "@distilled.cloud/cloudflare/zones";
5
+ import * as Cause from "effect/Cause";
5
6
  import * as Context from "effect/Context";
6
7
  import * as Data from "effect/Data";
7
8
  import * as Effect from "effect/Effect";
8
9
  import * as Exit from "effect/Exit";
9
- import * as FileSystem from "effect/FileSystem";
10
10
  import * as Layer from "effect/Layer";
11
11
  import * as Option from "effect/Option";
12
12
  import * as Path from "effect/Path";
13
13
  import * as Redacted from "effect/Redacted";
14
14
  import * as Schedule from "effect/Schedule";
15
15
  import * as Scope from "effect/Scope";
16
+ import * as Stream from "effect/Stream";
16
17
  import { createRequire } from "node:module";
17
18
  import path from "node:path";
18
19
  import { pathToFileURL } from "node:url";
@@ -34,11 +35,16 @@ import {
34
35
  type PlatformProps,
35
36
  type Rpc,
36
37
  } from "../../Platform.ts";
38
+ import { isYieldableEffectLike } from "../../Util/effect.ts";
37
39
  import * as Provider from "../../Provider.ts";
38
40
  import { Resource, type ResourceBinding } from "../../Resource.ts";
39
41
  import * as Serverless from "../../Serverless/index.ts";
40
42
  import { Stack } from "../../Stack.ts";
41
43
  import type { AiGateway } from "../AiGateway/AiGateway.ts";
44
+ import {
45
+ isAnalyticsEngineDataset,
46
+ type AnalyticsEngineDataset,
47
+ } from "../AnalyticsEngine/AnalyticsEngineDataset.ts";
42
48
  import {
43
49
  isArtifacts as isArtifactsBinding,
44
50
  type Artifacts as ArtifactsBinding,
@@ -46,7 +52,14 @@ import {
46
52
  import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
47
53
  import { D1Database } from "../D1/D1Database.ts";
48
54
  import { fromCloudflareFetcher } from "../Fetcher.ts";
49
- import type { HyperdriveDevOrigin } from "../Hyperdrive/Hyperdrive.ts";
55
+ import type {
56
+ Hyperdrive,
57
+ HyperdriveDevOrigin,
58
+ } from "../Hyperdrive/Hyperdrive.ts";
59
+ import {
60
+ isImages as isImagesBinding,
61
+ type Images as ImagesBinding,
62
+ } from "../Images/Images.ts";
50
63
  import type { KVNamespace } from "../KV/KVNamespace.ts";
51
64
  import { SidecarLive } from "../Local/Sidecar.ts";
52
65
  import { CloudflareLogs } from "../Logs.ts";
@@ -70,7 +83,14 @@ import {
70
83
  import { workersHttpHandler } from "./HttpServer.ts";
71
84
  import { LocalWorkerProvider } from "./LocalWorkerProvider.ts";
72
85
  import { Request } from "./Request.ts";
73
- import { makeRpcStub } from "./Rpc.ts";
86
+ import {
87
+ encodeRpcError,
88
+ ErrorTag,
89
+ makeRpcStub,
90
+ toRpcStream,
91
+ type RpcErrorEnvelope,
92
+ type RpcStreamEnvelope,
93
+ } from "./Rpc.ts";
74
94
  import { WorkerBundle } from "./WorkerBundle.ts";
75
95
  import { createWorkerName } from "./WorkerName.ts";
76
96
 
@@ -181,7 +201,11 @@ export type WorkerBindingResource =
181
201
  | KVNamespace
182
202
  | CloudflareQueue
183
203
  | AiGateway
204
+ | AnalyticsEngineDataset
184
205
  | ArtifactsBinding
206
+ | ImagesBinding
207
+ | Hyperdrive
208
+ | Worker
185
209
  | DurableObjectNamespaceLike<any>;
186
210
 
187
211
  export type WorkerBindings = {
@@ -272,6 +296,12 @@ export interface WorkerProps<
272
296
  >;
273
297
  exports?: string[];
274
298
  bindings?: Bindings;
299
+ /**
300
+ * Cron expressions that trigger the Worker's scheduled handler.
301
+ *
302
+ * Pass an empty array to remove all Cron Triggers.
303
+ */
304
+ crons?: string[];
275
305
  /**
276
306
  * One or more custom hostnames (e.g. `"app.example.com"`) to bind to this
277
307
  * Worker. The Cloudflare Zone is inferred from the hostname — the zone must
@@ -310,6 +340,7 @@ export type Worker<Bindings extends WorkerBindings = any> = Resource<
310
340
  durableObjectNamespaces: Record<string, string>;
311
341
  accountId: string;
312
342
  domains: { hostname: string; id: string; zoneId: string }[];
343
+ crons: string[];
313
344
  hash?: {
314
345
  assets: string | undefined;
315
346
  bundle: string | undefined;
@@ -319,6 +350,7 @@ export type Worker<Bindings extends WorkerBindings = any> = Resource<
319
350
  {
320
351
  bindings?: WorkerBinding[];
321
352
  containers?: { className: string }[];
353
+ crons?: string[];
322
354
  hyperdrives?: Record<string, Required<HyperdriveDevOrigin>>;
323
355
  },
324
356
  Providers
@@ -717,8 +749,11 @@ export const Worker: Platform<
717
749
  const bindingEff = props.bindings?.[bindingName] as
718
750
  | WorkerBindingResource
719
751
  | Effect.Effect<WorkerBindingResource>;
720
- const binding = Effect.isEffect(bindingEff)
721
- ? yield* bindingEff
752
+ // Bindings can be passed as a plain resource value, an Effect that
753
+ // yields a resource, or an effect-class (e.g. a `Cloudflare.Worker`
754
+ // class). Resolve the yieldable forms before deriving binding metadata.
755
+ const binding = isYieldableEffectLike(bindingEff)
756
+ ? ((yield* bindingEff as Effect.Effect<unknown>) as WorkerBindingResource)
722
757
  : bindingEff;
723
758
 
724
759
  const bindingMeta: InputProps<WorkerBinding> | undefined = isAssets(
@@ -734,48 +769,73 @@ export const Worker: Platform<
734
769
  name: bindingName,
735
770
  namespace: binding.namespace,
736
771
  } as any)
737
- : isDurableObjectNamespaceLike(binding)
772
+ : isImagesBinding(binding)
738
773
  ? {
739
- type: "durable_object_namespace",
774
+ type: "images",
740
775
  name: bindingName,
741
- className: binding.className ?? binding.name,
742
776
  }
743
- : binding.Type === "Cloudflare.D1Database"
777
+ : isAnalyticsEngineDataset(binding)
744
778
  ? {
745
- type: "d1",
746
- id: binding.databaseId,
779
+ type: "analytics_engine",
747
780
  name: bindingName,
781
+ dataset: binding.dataset,
748
782
  }
749
- : binding.Type === "Cloudflare.R2Bucket"
783
+ : isDurableObjectNamespaceLike(binding)
750
784
  ? {
751
- type: "r2_bucket",
785
+ type: "durable_object_namespace",
752
786
  name: bindingName,
753
- bucketName: binding.bucketName,
754
- jurisdiction: binding.jurisdiction.pipe(
755
- Output.map((jurisdiction) =>
756
- jurisdiction === "default" ? undefined : jurisdiction,
757
- ),
758
- ),
787
+ className: binding.className ?? binding.name,
759
788
  }
760
- : binding.Type === "Cloudflare.KVNamespace"
789
+ : binding.Type === "Cloudflare.D1Database"
761
790
  ? {
762
- type: "kv_namespace",
791
+ type: "d1",
792
+ id: binding.databaseId,
763
793
  name: bindingName,
764
- namespaceId: binding.namespaceId,
765
794
  }
766
- : binding.Type === "Cloudflare.Queue"
795
+ : binding.Type === "Cloudflare.R2Bucket"
767
796
  ? {
768
- type: "queue",
797
+ type: "r2_bucket",
769
798
  name: bindingName,
770
- queueName: binding.queueName,
799
+ bucketName: binding.bucketName,
800
+ jurisdiction: binding.jurisdiction.pipe(
801
+ Output.map((jurisdiction) =>
802
+ jurisdiction === "default"
803
+ ? undefined
804
+ : jurisdiction,
805
+ ),
806
+ ),
771
807
  }
772
- : binding.Type === "Cloudflare.AiGateway"
808
+ : binding.Type === "Cloudflare.KVNamespace"
773
809
  ? {
774
- type: "ai",
810
+ type: "kv_namespace",
775
811
  name: bindingName,
812
+ namespaceId: binding.namespaceId,
776
813
  }
777
- : // TODO(sam): handle others
778
- undefined;
814
+ : binding.Type === "Cloudflare.Queue"
815
+ ? {
816
+ type: "queue",
817
+ name: bindingName,
818
+ queueName: binding.queueName,
819
+ }
820
+ : binding.Type === "Cloudflare.AiGateway"
821
+ ? {
822
+ type: "ai",
823
+ name: bindingName,
824
+ }
825
+ : binding.Type === "Cloudflare.Hyperdrive"
826
+ ? {
827
+ type: "hyperdrive",
828
+ name: bindingName,
829
+ id: binding.hyperdriveId,
830
+ }
831
+ : isWorker(binding)
832
+ ? {
833
+ type: "service",
834
+ name: bindingName,
835
+ service: binding.workerName,
836
+ }
837
+ : // TODO(sam): handle others
838
+ undefined;
779
839
 
780
840
  if (bindingMeta) {
781
841
  yield* resource.bind`${bindingName}`({
@@ -791,6 +851,7 @@ export const Worker: Platform<
791
851
  const listeners: Effect.Effect<Serverless.FunctionListener>[] = [];
792
852
  const exports: Record<string, any> = {};
793
853
  const env: Record<string, any> = {};
854
+ let userShape: Record<string, unknown> | undefined;
794
855
 
795
856
  const ctx = {
796
857
  Type: WorkerTypeId,
@@ -857,7 +918,14 @@ export const Worker: Platform<
857
918
  }),
858
919
  serve: <Req = never>(
859
920
  handler: HttpEffect<Req> | Effect.Effect<HttpEffect<Req>>,
860
- ) => ctx.listen(workersHttpHandler(handler)),
921
+ options?: { shape?: Record<string, unknown> },
922
+ ) => {
923
+ // Capture the user's full default-export shape so `exports` can
924
+ // expose any non-handler methods on it as RPC methods on the
925
+ // deployed `WorkerEntrypoint` subclass — see `__rpc__` below.
926
+ if (options?.shape) userShape = options.shape;
927
+ return ctx.listen(workersHttpHandler(handler));
928
+ },
861
929
  listen: ((
862
930
  handler:
863
931
  | Serverless.FunctionListener
@@ -877,6 +945,35 @@ export const Worker: Platform<
877
945
  concurrency: "unbounded",
878
946
  });
879
947
  const services = yield* Effect.context();
948
+
949
+ // Provide the per-request runtime layer (services + `WorkerExecutionContext`
950
+ // + a fresh `ExecutionContext`/`Scope`) to a user effect, then run it.
951
+ // This is the bridge between Cloudflare's request-time callback and
952
+ // the user's Effect-native handler. Used by both the standard
953
+ // `handle()` dispatch (fetch/scheduled/…) and the per-RPC dispatchers
954
+ // below — keeping them on a single layer-provision path.
955
+ const runUserEffect = <A, E>(
956
+ eff: Effect.Effect<A, E>,
957
+ context: cf.ExecutionContext,
958
+ ): Promise<Exit.Exit<A, E>> => {
959
+ const scope = Scope.makeUnsafe();
960
+ return eff
961
+ .pipe(
962
+ Effect.provideContext(services),
963
+ Scope.provide(scope),
964
+ Effect.provide(Layer.succeed(WorkerExecutionContext, context)),
965
+ Effect.provide(
966
+ Layer.succeed(ExecutionContext, { scope, cache: {} }),
967
+ ),
968
+ Effect.runPromiseExit,
969
+ )
970
+ .finally(() =>
971
+ context.waitUntil(
972
+ Effect.runPromise(Scope.close(scope, Exit.void)),
973
+ ),
974
+ );
975
+ };
976
+
880
977
  const handle =
881
978
  (type: WorkerEvent["type"]) =>
882
979
  (request: any, env: unknown, context: cf.ExecutionContext) => {
@@ -890,36 +987,76 @@ export const Worker: Platform<
890
987
  for (const handler of handlers) {
891
988
  const eff = handler(event);
892
989
  if (Effect.isEffect(eff)) {
893
- const scope = Scope.makeUnsafe();
894
- return eff
895
- .pipe(
896
- Effect.provideContext(services),
897
- Scope.provide(scope),
898
- Effect.provide(
899
- Layer.succeed(WorkerExecutionContext, context),
900
- ),
901
- Effect.provide(
902
- Layer.succeed(ExecutionContext, {
903
- scope,
904
- cache: {},
905
- }),
906
- ),
907
- Effect.runPromise,
908
- )
909
- .finally(() =>
910
- context.waitUntil(
911
- Effect.runPromise(Scope.close(scope, Exit.void)),
912
- ),
913
- );
990
+ return runUserEffect(
991
+ eff as Effect.Effect<unknown, unknown>,
992
+ context,
993
+ ).then((exit) => {
994
+ if (exit._tag === "Success") return exit.value;
995
+ throw Cause.squash(exit.cause);
996
+ });
914
997
  }
915
998
  }
916
999
  return Promise.reject(new Error("No event handler found"));
917
1000
  };
1001
+
1002
+ // RPC method dispatchers — one per non-handler method on the user's
1003
+ // shape. Each dispatcher is invoked by the WorkerEntrypoint bridge
1004
+ // as `dispatcher(args, ctx)`: `args` are the user-facing call args,
1005
+ // `ctx` is the `this.ctx` that Cloudflare hands the bridge per RPC
1006
+ // request. The dispatcher runs the user effect with the same runtime
1007
+ // layer the fetch path uses, then envelope-encodes the result so
1008
+ // `Effect.fail` round-trips as `RpcErrorEnvelope` and `Stream` as
1009
+ // `RpcStreamEnvelope` (consumers wrap the binding with
1010
+ // `toPromiseApi`/`bindWorker` to decode).
1011
+ const exportedHandlerSet = new Set<string>(ExportedHandlerMethods);
1012
+ const rpcDispatchers: Record<
1013
+ string,
1014
+ (args: unknown[], ctx: cf.ExecutionContext) => Promise<unknown>
1015
+ > = {};
1016
+ if (userShape) {
1017
+ for (const [name, value] of Object.entries(userShape)) {
1018
+ if (exportedHandlerSet.has(name)) continue;
1019
+ if (typeof value !== "function") continue;
1020
+ rpcDispatchers[name] = async (
1021
+ args: unknown[],
1022
+ context: cf.ExecutionContext,
1023
+ ) => {
1024
+ const result = (value as (...a: unknown[]) => unknown)(...args);
1025
+ if (!Effect.isEffect(result)) return result;
1026
+ const exit = await runUserEffect(
1027
+ result as Effect.Effect<unknown, unknown>,
1028
+ context,
1029
+ );
1030
+ if (exit._tag === "Success") {
1031
+ if (Stream.isStream(exit.value)) {
1032
+ return await Effect.runPromise(
1033
+ toRpcStream(exit.value) as Effect.Effect<RpcStreamEnvelope>,
1034
+ );
1035
+ }
1036
+ return exit.value;
1037
+ }
1038
+ const failReason = exit.cause.reasons.find(Cause.isFailReason);
1039
+ if (failReason) {
1040
+ return {
1041
+ _tag: ErrorTag,
1042
+ error: encodeRpcError(failReason.error),
1043
+ } satisfies RpcErrorEnvelope;
1044
+ }
1045
+ const dieReason = exit.cause.reasons.find(Cause.isDieReason);
1046
+ throw (
1047
+ dieReason?.defect ??
1048
+ new Error("RPC method failed with an unexpected cause")
1049
+ );
1050
+ };
1051
+ }
1052
+ }
1053
+
918
1054
  return {
919
1055
  ...exports,
920
1056
  default: Object.fromEntries(
921
1057
  ExportedHandlerMethods.map((method) => [method, handle(method)]),
922
1058
  ),
1059
+ __rpc__: rpcDispatchers,
923
1060
  };
924
1061
  }),
925
1062
  };
@@ -932,7 +1069,10 @@ export const bindWorker = Effect.fnUntraced(function* <Shape, Req = never>(
932
1069
  | (Worker & Rpc<Shape>)
933
1070
  | Effect.Effect<Worker & Rpc<Shape>, never, Req>,
934
1071
  ) {
935
- const worker = Effect.isEffect(workerEff) ? yield* workerEff : workerEff;
1072
+ // Worker classes and regular Effects are both yieldable here.
1073
+ const worker = isYieldableEffectLike(workerEff)
1074
+ ? yield* workerEff as Effect.Effect<Worker & Rpc<Shape>, never, Req>
1075
+ : workerEff;
936
1076
  const self = yield* Worker;
937
1077
  yield* self.bind`${worker}`({
938
1078
  bindings: [
@@ -944,13 +1084,14 @@ export const bindWorker = Effect.fnUntraced(function* <Shape, Req = never>(
944
1084
  ],
945
1085
  });
946
1086
 
947
- const workerBinding = WorkerEnvironment.asEffect().pipe(
948
- Effect.map((env) => env[worker.LogicalId]),
1087
+ // `bindWorker` runs at *init* phase (both at plantime and at runtime
1088
+ // cold-start). `WorkerEnvironment` only exists at exec phase on the
1089
+ // deployed worker, so we hand `makeRpcStub` an `Effect<stub>` that
1090
+ // resolves the binding lazily on each method call.
1091
+ const stubEff = WorkerEnvironment.asEffect().pipe(
1092
+ Effect.map((env) => (env as Record<string, unknown>)[worker.LogicalId]),
949
1093
  );
950
-
951
- const fetcher = workerBinding.pipe(Effect.map(fromCloudflareFetcher));
952
- // TODO(sam): update makeRpcStub to support lazily evaluating the Effect<Fetcher>
953
- return makeRpcStub<Shape>(fetcher);
1094
+ return makeRpcStub<Shape>(stubEff);
954
1095
  });
955
1096
 
956
1097
  export class BindWorkerPolicy extends Binding.Policy<
@@ -1018,6 +1159,12 @@ function getDurableObjectBindings(
1018
1159
  );
1019
1160
  }
1020
1161
 
1162
+ export function getCronBindings(
1163
+ bindings: ReadonlyArray<ResourceBinding<Worker["Binding"]>>,
1164
+ ) {
1165
+ return Array.from(new Set(bindings.flatMap((b) => b.data.crons ?? [])));
1166
+ }
1167
+
1021
1168
  function getDurableObjectTagMap(tags: ReadonlyArray<string>) {
1022
1169
  return Object.fromEntries(
1023
1170
  tags.flatMap((tag) => {
@@ -1059,20 +1206,20 @@ export const LiveWorkerProvider = () =>
1059
1206
  Provider.effect(
1060
1207
  Worker,
1061
1208
  Effect.gen(function* () {
1062
- const fs = yield* FileSystem.FileSystem;
1063
1209
  const path = yield* Path.Path;
1064
1210
 
1065
1211
  const { accountId } = yield* CloudflareEnvironment;
1066
- const virtualEntryPlugin = yield* Bundle.virtualEntryPlugin;
1067
1212
  const bundler = yield* WorkerBundle;
1068
1213
  const stack = yield* Stack;
1069
1214
 
1070
1215
  const createScriptSubdomain = yield* workers.createScriptSubdomain;
1071
1216
  const deleteScript = yield* workers.deleteScript;
1072
1217
  const getScriptSubdomain = yield* workers.getScriptSubdomain;
1218
+ const getScriptSchedule = yield* workers.getScriptSchedule;
1073
1219
  const getScriptSettings = yield* workers.getScriptScriptAndVersionSetting;
1074
1220
  const getSubdomain = yield* workers.getSubdomain;
1075
1221
  const putScript = yield* workers.putScript;
1222
+ const putScriptSchedule = yield* workers.putScriptSchedule;
1076
1223
  const putDomain = yield* workers.putDomain;
1077
1224
  const listDomains = yield* workers.listDomains;
1078
1225
  const deleteDomain = yield* workers.deleteDomain;
@@ -1120,6 +1267,60 @@ export const LiveWorkerProvider = () =>
1120
1267
  ),
1121
1268
  );
1122
1269
 
1270
+ const normalizeCrons = (crons: string[] | undefined): string[] =>
1271
+ Array.from(new Set(crons ?? []));
1272
+
1273
+ const getWorkerCrons = (scriptName: string) =>
1274
+ getScriptSchedule({
1275
+ accountId,
1276
+ scriptName,
1277
+ }).pipe(
1278
+ Effect.map((response) =>
1279
+ normalizeCrons(response.schedules.map((schedule) => schedule.cron)),
1280
+ ),
1281
+ Effect.catchTag("WorkerNotFound", () => Effect.succeed([])),
1282
+ );
1283
+
1284
+ const reconcileCrons = (
1285
+ scriptName: string,
1286
+ desired: string[],
1287
+ previous: string[],
1288
+ session: ScopedPlanStatusSession,
1289
+ ) =>
1290
+ Effect.gen(function* () {
1291
+ const live = yield* getWorkerCrons(scriptName);
1292
+ const desiredSorted = [...desired].sort();
1293
+ const liveSorted = [...live].sort();
1294
+ const changed =
1295
+ desiredSorted.length !== liveSorted.length ||
1296
+ desiredSorted.some((cron, index) => cron !== liveSorted[index]);
1297
+
1298
+ if (!changed) return live;
1299
+
1300
+ if (desired.length > 0 || previous.length > 0 || live.length > 0) {
1301
+ yield* session.note(
1302
+ `Reconciling Cron Triggers (${desired.length}) ...`,
1303
+ );
1304
+ }
1305
+
1306
+ const result = yield* putScriptSchedule({
1307
+ accountId,
1308
+ scriptName,
1309
+ body: desired.map((cron) => ({ cron })),
1310
+ }).pipe(
1311
+ Effect.retry({
1312
+ while: (error: { _tag?: string }) =>
1313
+ error?._tag === "WorkerNotFound",
1314
+ schedule: Schedule.exponential(200).pipe(
1315
+ Schedule.both(Schedule.recurs(15)),
1316
+ ),
1317
+ }),
1318
+ );
1319
+ return normalizeCrons(
1320
+ result.schedules.map((schedule) => schedule.cron),
1321
+ );
1322
+ });
1323
+
1123
1324
  /**
1124
1325
  * Infer the Cloudflare Zone ID for a given hostname by listing the
1125
1326
  * account's zones and matching the hostname against each zone's name —
@@ -1939,6 +2140,12 @@ export const LiveWorkerProvider = () =>
1939
2140
  desiredDomains,
1940
2141
  previousDomains,
1941
2142
  );
2143
+ const crons = yield* reconcileCrons(
2144
+ name,
2145
+ normalizeCrons([...getCronBindings(bindings), ...(news.crons ?? [])]),
2146
+ output?.crons ?? [],
2147
+ session,
2148
+ );
1942
2149
  return {
1943
2150
  workerId: worker.id ?? name,
1944
2151
  workerName: name,
@@ -1951,6 +2158,7 @@ export const LiveWorkerProvider = () =>
1951
2158
  durableObjectNamespaces,
1952
2159
  accountId,
1953
2160
  domains,
2161
+ crons,
1954
2162
  hash,
1955
2163
  } satisfies Worker["Attributes"];
1956
2164
  });
@@ -2006,7 +2214,13 @@ export const LiveWorkerProvider = () =>
2006
2214
 
2007
2215
  return Worker.Provider.of({
2008
2216
  stables: ["workerId", "workerName"],
2009
- diff: Effect.fnUntraced(function* ({ id, news, olds, output }) {
2217
+ diff: Effect.fnUntraced(function* ({
2218
+ id,
2219
+ news,
2220
+ olds,
2221
+ output,
2222
+ newBindings,
2223
+ }) {
2010
2224
  if (!isResolved(news)) return undefined;
2011
2225
  if ((output?.accountId ?? accountId) !== accountId) {
2012
2226
  return { action: "replace" };
@@ -2028,7 +2242,23 @@ export const LiveWorkerProvider = () =>
2028
2242
  const domainsChanged =
2029
2243
  newDomains.length !== oldDomains.length ||
2030
2244
  newDomains.some((d, i) => d !== oldDomains[i]);
2031
- if (domainsChanged || (yield* hasChanged(id, news, output))) {
2245
+ const newCrons = normalizeCrons([
2246
+ ...(Array.isArray(newBindings)
2247
+ ? getCronBindings(
2248
+ newBindings as ResourceBinding<Worker["Binding"]>[],
2249
+ )
2250
+ : []),
2251
+ ...(news.crons ?? []),
2252
+ ]).sort();
2253
+ const oldCrons = [...(output?.crons ?? [])].sort();
2254
+ const cronsChanged =
2255
+ newCrons.length !== oldCrons.length ||
2256
+ newCrons.some((cron, index) => cron !== oldCrons[index]);
2257
+ if (
2258
+ domainsChanged ||
2259
+ cronsChanged ||
2260
+ (yield* hasChanged(id, news, output))
2261
+ ) {
2032
2262
  return {
2033
2263
  action: "update",
2034
2264
  stables:
@@ -2154,6 +2384,7 @@ export const LiveWorkerProvider = () =>
2154
2384
  durableObjectNamespaces,
2155
2385
  accountId,
2156
2386
  domains: [],
2387
+ crons: [],
2157
2388
  } satisfies Worker["Attributes"];
2158
2389
  }),
2159
2390
  read: Effect.fnUntraced(
@@ -2185,6 +2416,7 @@ export const LiveWorkerProvider = () =>
2185
2416
  service: workerName,
2186
2417
  }).pipe(Effect.map((r) => r.result ?? [])),
2187
2418
  ]);
2419
+ const crons = yield* getWorkerCrons(workerName);
2188
2420
  yield* Effect.logInfo(
2189
2421
  `Cloudflare Worker read: found ${workerName}`,
2190
2422
  );
@@ -2205,6 +2437,7 @@ export const LiveWorkerProvider = () =>
2205
2437
  ? [{ id: d.id, hostname: d.hostname, zoneId: d.zoneId }]
2206
2438
  : [],
2207
2439
  ),
2440
+ crons,
2208
2441
  } satisfies Worker["Attributes"];
2209
2442
 
2210
2443
  // Centralized ownership decision: the engine routes `read`'s
@@ -158,11 +158,11 @@ import * as Logger from "effect/Logger";
158
158
  import * as Context from "effect/Context";
159
159
  import * as Stream from "effect/Stream";
160
160
 
161
- import { env, DurableObject${hasWfClasses ? ", WorkflowEntrypoint" : ""} } from "cloudflare:workers";
161
+ import { env, DurableObject, WorkerEntrypoint${hasWfClasses ? ", WorkflowEntrypoint" : ""} } from "cloudflare:workers";
162
162
  import { MinimumLogLevel } from "effect/References";
163
163
  import { NodeServices } from "@effect/platform-node";
164
164
  import { Stack } from "alchemy/Stack";
165
- import { WorkerEnvironment, makeDurableObjectBridge${hasWfClasses ? ", makeWorkflowBridge" : ""}, ExportedHandlerMethods } from "alchemy/Cloudflare";
165
+ import { WorkerEnvironment, makeDurableObjectBridge, makeWorkerBridge${hasWfClasses ? ", makeWorkflowBridge" : ""}, ExportedHandlerMethods } from "alchemy/Cloudflare";
166
166
 
167
167
  import entry from "${importPath}";
168
168
 
@@ -223,10 +223,20 @@ let exportsPromise;
223
223
  // we cache it synchronously (??=) to guarnatee only one initialization ever happens
224
224
  const getExports = () => (exportsPromise ??= Effect.runPromise(exportsEffect))
225
225
  const getExport = (name) => getExports().then(exports => exports[name]?.make)
226
- const worker = () => getExports().then(exports => exports.default)
226
+ const getDefault = () => getExports().then(exports => exports.default)
227
+ const getRpc = () => getExports().then(exports => exports.__rpc__ ?? {})
227
228
 
228
- export default Object.fromEntries(ExportedHandlerMethods.map(
229
- method => [method, async (...args) => (await worker())[method](...args)])
229
+ // Bridge the user's default-export shape onto a real \`WorkerEntrypoint\`
230
+ // subclass so Cloudflare service bindings can dispatch both the standard
231
+ // handler methods (fetch, scheduled, …) and any user-defined RPC methods.
232
+ // RPC method results are wire-encoded by \`runtimeContext.exports\`;
233
+ // consumers unwrap them with \`Cloudflare.toPromiseApi(env.X)\` (Promise
234
+ // API) or \`bindWorker(WorkerClass)\` (Effect API).
235
+ export default makeWorkerBridge(
236
+ WorkerEntrypoint,
237
+ ExportedHandlerMethods,
238
+ getDefault,
239
+ getRpc,
230
240
  );
231
241
 
232
242
  // export class proxy stubs for Durable Objects and Workflows