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
@@ -2,6 +2,7 @@ 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";
@@ -12,6 +13,7 @@ import * as Path from "effect/Path";
12
13
  import * as Redacted from "effect/Redacted";
13
14
  import * as Schedule from "effect/Schedule";
14
15
  import * as Scope from "effect/Scope";
16
+ import * as Stream from "effect/Stream";
15
17
  import { createRequire } from "node:module";
16
18
  import path from "node:path";
17
19
  import { pathToFileURL } from "node:url";
@@ -33,11 +35,16 @@ import {
33
35
  type PlatformProps,
34
36
  type Rpc,
35
37
  } from "../../Platform.ts";
38
+ import { isYieldableEffectLike } from "../../Util/effect.ts";
36
39
  import * as Provider from "../../Provider.ts";
37
40
  import { Resource, type ResourceBinding } from "../../Resource.ts";
38
41
  import * as Serverless from "../../Serverless/index.ts";
39
42
  import { Stack } from "../../Stack.ts";
40
43
  import type { AiGateway } from "../AiGateway/AiGateway.ts";
44
+ import {
45
+ isAnalyticsEngineDataset,
46
+ type AnalyticsEngineDataset,
47
+ } from "../AnalyticsEngine/AnalyticsEngineDataset.ts";
41
48
  import {
42
49
  isArtifacts as isArtifactsBinding,
43
50
  type Artifacts as ArtifactsBinding,
@@ -76,7 +83,14 @@ import {
76
83
  import { workersHttpHandler } from "./HttpServer.ts";
77
84
  import { LocalWorkerProvider } from "./LocalWorkerProvider.ts";
78
85
  import { Request } from "./Request.ts";
79
- 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";
80
94
  import { WorkerBundle } from "./WorkerBundle.ts";
81
95
  import { createWorkerName } from "./WorkerName.ts";
82
96
 
@@ -187,9 +201,11 @@ export type WorkerBindingResource =
187
201
  | KVNamespace
188
202
  | CloudflareQueue
189
203
  | AiGateway
204
+ | AnalyticsEngineDataset
190
205
  | ArtifactsBinding
191
206
  | ImagesBinding
192
207
  | Hyperdrive
208
+ | Worker
193
209
  | DurableObjectNamespaceLike<any>;
194
210
 
195
211
  export type WorkerBindings = {
@@ -280,6 +296,12 @@ export interface WorkerProps<
280
296
  >;
281
297
  exports?: string[];
282
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[];
283
305
  /**
284
306
  * One or more custom hostnames (e.g. `"app.example.com"`) to bind to this
285
307
  * Worker. The Cloudflare Zone is inferred from the hostname — the zone must
@@ -318,6 +340,7 @@ export type Worker<Bindings extends WorkerBindings = any> = Resource<
318
340
  durableObjectNamespaces: Record<string, string>;
319
341
  accountId: string;
320
342
  domains: { hostname: string; id: string; zoneId: string }[];
343
+ crons: string[];
321
344
  hash?: {
322
345
  assets: string | undefined;
323
346
  bundle: string | undefined;
@@ -327,6 +350,7 @@ export type Worker<Bindings extends WorkerBindings = any> = Resource<
327
350
  {
328
351
  bindings?: WorkerBinding[];
329
352
  containers?: { className: string }[];
353
+ crons?: string[];
330
354
  hyperdrives?: Record<string, Required<HyperdriveDevOrigin>>;
331
355
  },
332
356
  Providers
@@ -725,8 +749,11 @@ export const Worker: Platform<
725
749
  const bindingEff = props.bindings?.[bindingName] as
726
750
  | WorkerBindingResource
727
751
  | Effect.Effect<WorkerBindingResource>;
728
- const binding = Effect.isEffect(bindingEff)
729
- ? 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)
730
757
  : bindingEff;
731
758
 
732
759
  const bindingMeta: InputProps<WorkerBinding> | undefined = isAssets(
@@ -747,56 +774,68 @@ export const Worker: Platform<
747
774
  type: "images",
748
775
  name: bindingName,
749
776
  }
750
- : isDurableObjectNamespaceLike(binding)
777
+ : isAnalyticsEngineDataset(binding)
751
778
  ? {
752
- type: "durable_object_namespace",
779
+ type: "analytics_engine",
753
780
  name: bindingName,
754
- className: binding.className ?? binding.name,
781
+ dataset: binding.dataset,
755
782
  }
756
- : binding.Type === "Cloudflare.D1Database"
783
+ : isDurableObjectNamespaceLike(binding)
757
784
  ? {
758
- type: "d1",
759
- id: binding.databaseId,
785
+ type: "durable_object_namespace",
760
786
  name: bindingName,
787
+ className: binding.className ?? binding.name,
761
788
  }
762
- : binding.Type === "Cloudflare.R2Bucket"
789
+ : binding.Type === "Cloudflare.D1Database"
763
790
  ? {
764
- type: "r2_bucket",
791
+ type: "d1",
792
+ id: binding.databaseId,
765
793
  name: bindingName,
766
- bucketName: binding.bucketName,
767
- jurisdiction: binding.jurisdiction.pipe(
768
- Output.map((jurisdiction) =>
769
- jurisdiction === "default"
770
- ? undefined
771
- : jurisdiction,
772
- ),
773
- ),
774
794
  }
775
- : binding.Type === "Cloudflare.KVNamespace"
795
+ : binding.Type === "Cloudflare.R2Bucket"
776
796
  ? {
777
- type: "kv_namespace",
797
+ type: "r2_bucket",
778
798
  name: bindingName,
779
- namespaceId: binding.namespaceId,
799
+ bucketName: binding.bucketName,
800
+ jurisdiction: binding.jurisdiction.pipe(
801
+ Output.map((jurisdiction) =>
802
+ jurisdiction === "default"
803
+ ? undefined
804
+ : jurisdiction,
805
+ ),
806
+ ),
780
807
  }
781
- : binding.Type === "Cloudflare.Queue"
808
+ : binding.Type === "Cloudflare.KVNamespace"
782
809
  ? {
783
- type: "queue",
810
+ type: "kv_namespace",
784
811
  name: bindingName,
785
- queueName: binding.queueName,
812
+ namespaceId: binding.namespaceId,
786
813
  }
787
- : binding.Type === "Cloudflare.AiGateway"
814
+ : binding.Type === "Cloudflare.Queue"
788
815
  ? {
789
- type: "ai",
816
+ type: "queue",
790
817
  name: bindingName,
818
+ queueName: binding.queueName,
791
819
  }
792
- : binding.Type === "Cloudflare.Hyperdrive"
820
+ : binding.Type === "Cloudflare.AiGateway"
793
821
  ? {
794
- type: "hyperdrive",
822
+ type: "ai",
795
823
  name: bindingName,
796
- id: binding.hyperdriveId,
797
824
  }
798
- : // TODO(sam): handle others
799
- undefined;
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;
800
839
 
801
840
  if (bindingMeta) {
802
841
  yield* resource.bind`${bindingName}`({
@@ -812,6 +851,7 @@ export const Worker: Platform<
812
851
  const listeners: Effect.Effect<Serverless.FunctionListener>[] = [];
813
852
  const exports: Record<string, any> = {};
814
853
  const env: Record<string, any> = {};
854
+ let userShape: Record<string, unknown> | undefined;
815
855
 
816
856
  const ctx = {
817
857
  Type: WorkerTypeId,
@@ -878,7 +918,14 @@ export const Worker: Platform<
878
918
  }),
879
919
  serve: <Req = never>(
880
920
  handler: HttpEffect<Req> | Effect.Effect<HttpEffect<Req>>,
881
- ) => 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
+ },
882
929
  listen: ((
883
930
  handler:
884
931
  | Serverless.FunctionListener
@@ -898,6 +945,35 @@ export const Worker: Platform<
898
945
  concurrency: "unbounded",
899
946
  });
900
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
+
901
977
  const handle =
902
978
  (type: WorkerEvent["type"]) =>
903
979
  (request: any, env: unknown, context: cf.ExecutionContext) => {
@@ -911,36 +987,76 @@ export const Worker: Platform<
911
987
  for (const handler of handlers) {
912
988
  const eff = handler(event);
913
989
  if (Effect.isEffect(eff)) {
914
- const scope = Scope.makeUnsafe();
915
- return eff
916
- .pipe(
917
- Effect.provideContext(services),
918
- Scope.provide(scope),
919
- Effect.provide(
920
- Layer.succeed(WorkerExecutionContext, context),
921
- ),
922
- Effect.provide(
923
- Layer.succeed(ExecutionContext, {
924
- scope,
925
- cache: {},
926
- }),
927
- ),
928
- Effect.runPromise,
929
- )
930
- .finally(() =>
931
- context.waitUntil(
932
- Effect.runPromise(Scope.close(scope, Exit.void)),
933
- ),
934
- );
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
+ });
935
997
  }
936
998
  }
937
999
  return Promise.reject(new Error("No event handler found"));
938
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
+
939
1054
  return {
940
1055
  ...exports,
941
1056
  default: Object.fromEntries(
942
1057
  ExportedHandlerMethods.map((method) => [method, handle(method)]),
943
1058
  ),
1059
+ __rpc__: rpcDispatchers,
944
1060
  };
945
1061
  }),
946
1062
  };
@@ -953,7 +1069,10 @@ export const bindWorker = Effect.fnUntraced(function* <Shape, Req = never>(
953
1069
  | (Worker & Rpc<Shape>)
954
1070
  | Effect.Effect<Worker & Rpc<Shape>, never, Req>,
955
1071
  ) {
956
- 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;
957
1076
  const self = yield* Worker;
958
1077
  yield* self.bind`${worker}`({
959
1078
  bindings: [
@@ -965,13 +1084,14 @@ export const bindWorker = Effect.fnUntraced(function* <Shape, Req = never>(
965
1084
  ],
966
1085
  });
967
1086
 
968
- const workerBinding = WorkerEnvironment.asEffect().pipe(
969
- 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]),
970
1093
  );
971
-
972
- const fetcher = workerBinding.pipe(Effect.map(fromCloudflareFetcher));
973
- // TODO(sam): update makeRpcStub to support lazily evaluating the Effect<Fetcher>
974
- return makeRpcStub<Shape>(fetcher);
1094
+ return makeRpcStub<Shape>(stubEff);
975
1095
  });
976
1096
 
977
1097
  export class BindWorkerPolicy extends Binding.Policy<
@@ -1039,6 +1159,12 @@ function getDurableObjectBindings(
1039
1159
  );
1040
1160
  }
1041
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
+
1042
1168
  function getDurableObjectTagMap(tags: ReadonlyArray<string>) {
1043
1169
  return Object.fromEntries(
1044
1170
  tags.flatMap((tag) => {
@@ -1089,9 +1215,11 @@ export const LiveWorkerProvider = () =>
1089
1215
  const createScriptSubdomain = yield* workers.createScriptSubdomain;
1090
1216
  const deleteScript = yield* workers.deleteScript;
1091
1217
  const getScriptSubdomain = yield* workers.getScriptSubdomain;
1218
+ const getScriptSchedule = yield* workers.getScriptSchedule;
1092
1219
  const getScriptSettings = yield* workers.getScriptScriptAndVersionSetting;
1093
1220
  const getSubdomain = yield* workers.getSubdomain;
1094
1221
  const putScript = yield* workers.putScript;
1222
+ const putScriptSchedule = yield* workers.putScriptSchedule;
1095
1223
  const putDomain = yield* workers.putDomain;
1096
1224
  const listDomains = yield* workers.listDomains;
1097
1225
  const deleteDomain = yield* workers.deleteDomain;
@@ -1139,6 +1267,60 @@ export const LiveWorkerProvider = () =>
1139
1267
  ),
1140
1268
  );
1141
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
+
1142
1324
  /**
1143
1325
  * Infer the Cloudflare Zone ID for a given hostname by listing the
1144
1326
  * account's zones and matching the hostname against each zone's name —
@@ -1958,6 +2140,12 @@ export const LiveWorkerProvider = () =>
1958
2140
  desiredDomains,
1959
2141
  previousDomains,
1960
2142
  );
2143
+ const crons = yield* reconcileCrons(
2144
+ name,
2145
+ normalizeCrons([...getCronBindings(bindings), ...(news.crons ?? [])]),
2146
+ output?.crons ?? [],
2147
+ session,
2148
+ );
1961
2149
  return {
1962
2150
  workerId: worker.id ?? name,
1963
2151
  workerName: name,
@@ -1970,6 +2158,7 @@ export const LiveWorkerProvider = () =>
1970
2158
  durableObjectNamespaces,
1971
2159
  accountId,
1972
2160
  domains,
2161
+ crons,
1973
2162
  hash,
1974
2163
  } satisfies Worker["Attributes"];
1975
2164
  });
@@ -2025,7 +2214,13 @@ export const LiveWorkerProvider = () =>
2025
2214
 
2026
2215
  return Worker.Provider.of({
2027
2216
  stables: ["workerId", "workerName"],
2028
- diff: Effect.fnUntraced(function* ({ id, news, olds, output }) {
2217
+ diff: Effect.fnUntraced(function* ({
2218
+ id,
2219
+ news,
2220
+ olds,
2221
+ output,
2222
+ newBindings,
2223
+ }) {
2029
2224
  if (!isResolved(news)) return undefined;
2030
2225
  if ((output?.accountId ?? accountId) !== accountId) {
2031
2226
  return { action: "replace" };
@@ -2047,7 +2242,23 @@ export const LiveWorkerProvider = () =>
2047
2242
  const domainsChanged =
2048
2243
  newDomains.length !== oldDomains.length ||
2049
2244
  newDomains.some((d, i) => d !== oldDomains[i]);
2050
- 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
+ ) {
2051
2262
  return {
2052
2263
  action: "update",
2053
2264
  stables:
@@ -2173,6 +2384,7 @@ export const LiveWorkerProvider = () =>
2173
2384
  durableObjectNamespaces,
2174
2385
  accountId,
2175
2386
  domains: [],
2387
+ crons: [],
2176
2388
  } satisfies Worker["Attributes"];
2177
2389
  }),
2178
2390
  read: Effect.fnUntraced(
@@ -2204,6 +2416,7 @@ export const LiveWorkerProvider = () =>
2204
2416
  service: workerName,
2205
2417
  }).pipe(Effect.map((r) => r.result ?? [])),
2206
2418
  ]);
2419
+ const crons = yield* getWorkerCrons(workerName);
2207
2420
  yield* Effect.logInfo(
2208
2421
  `Cloudflare Worker read: found ${workerName}`,
2209
2422
  );
@@ -2224,6 +2437,7 @@ export const LiveWorkerProvider = () =>
2224
2437
  ? [{ id: d.id, hostname: d.hostname, zoneId: d.zoneId }]
2225
2438
  : [],
2226
2439
  ),
2440
+ crons,
2227
2441
  } satisfies Worker["Attributes"];
2228
2442
 
2229
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