alchemy 2.0.0-beta.22 → 2.0.0-beta.24

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 (186) hide show
  1. package/bin/alchemy.js +12590 -2452
  2. package/bin/alchemy.js.map +1 -1
  3. package/bin/alchemy.ts +2 -0
  4. package/bin/cli.js +18 -0
  5. package/bin/commands/bootstrap.ts +88 -4
  6. package/bin/commands/deploy.ts +54 -20
  7. package/bin/commands/dev.ts +45 -0
  8. package/bin/commands/logs.ts +1 -1
  9. package/bin/commands/tail.ts +1 -1
  10. package/bin/exec.ts +22 -0
  11. package/lib/AWS/Lambda/Function.d.ts.map +1 -1
  12. package/lib/AWS/Lambda/Function.js +25 -21
  13. package/lib/AWS/Lambda/Function.js.map +1 -1
  14. package/lib/AWS/Lambda/HttpServer.d.ts.map +1 -1
  15. package/lib/AWS/Lambda/HttpServer.js +3 -1
  16. package/lib/AWS/Lambda/HttpServer.js.map +1 -1
  17. package/lib/AdoptPolicy.d.ts +2 -2
  18. package/lib/AdoptPolicy.d.ts.map +1 -1
  19. package/lib/AdoptPolicy.js.map +1 -1
  20. package/lib/AlchemyContext.d.ts +1 -0
  21. package/lib/AlchemyContext.d.ts.map +1 -1
  22. package/lib/AlchemyContext.js +1 -0
  23. package/lib/AlchemyContext.js.map +1 -1
  24. package/lib/Build/Command.d.ts.map +1 -1
  25. package/lib/Build/Command.js +8 -1
  26. package/lib/Build/Command.js.map +1 -1
  27. package/lib/Bundle/Bundle.d.ts +21 -9
  28. package/lib/Bundle/Bundle.d.ts.map +1 -1
  29. package/lib/Bundle/Bundle.js +29 -15
  30. package/lib/Bundle/Bundle.js.map +1 -1
  31. package/lib/Cloudflare/Container/Container.d.ts +55 -2
  32. package/lib/Cloudflare/Container/Container.d.ts.map +1 -1
  33. package/lib/Cloudflare/Container/Container.js +55 -2
  34. package/lib/Cloudflare/Container/Container.js.map +1 -1
  35. package/lib/Cloudflare/D1/D1Export.d.ts.map +1 -1
  36. package/lib/Cloudflare/D1/D1Export.js.map +1 -1
  37. package/lib/Cloudflare/D1/D1SqlFile.d.ts.map +1 -1
  38. package/lib/Cloudflare/D1/D1SqlFile.js.map +1 -1
  39. package/lib/Cloudflare/Local/Sidecar.d.ts +27 -0
  40. package/lib/Cloudflare/Local/Sidecar.d.ts.map +1 -0
  41. package/lib/Cloudflare/Local/Sidecar.js +24 -0
  42. package/lib/Cloudflare/Local/Sidecar.js.map +1 -0
  43. package/lib/Cloudflare/Local/SidecarHandlers.d.ts +5 -0
  44. package/lib/Cloudflare/Local/SidecarHandlers.d.ts.map +1 -0
  45. package/lib/Cloudflare/Local/SidecarHandlers.js +119 -0
  46. package/lib/Cloudflare/Local/SidecarHandlers.js.map +1 -0
  47. package/lib/Cloudflare/Local/SidecarServer.d.ts +2 -0
  48. package/lib/Cloudflare/Local/SidecarServer.d.ts.map +1 -0
  49. package/lib/Cloudflare/Local/SidecarServer.js +27 -0
  50. package/lib/Cloudflare/Local/SidecarServer.js.map +1 -0
  51. package/lib/Cloudflare/Queue/Queue.d.ts +34 -4
  52. package/lib/Cloudflare/Queue/Queue.d.ts.map +1 -1
  53. package/lib/Cloudflare/Queue/Queue.js +34 -4
  54. package/lib/Cloudflare/Queue/Queue.js.map +1 -1
  55. package/lib/Cloudflare/Queue/QueueBinding.d.ts +33 -0
  56. package/lib/Cloudflare/Queue/QueueBinding.d.ts.map +1 -1
  57. package/lib/Cloudflare/Queue/QueueBinding.js +33 -0
  58. package/lib/Cloudflare/Queue/QueueBinding.js.map +1 -1
  59. package/lib/Cloudflare/SecretsStore/Secret.d.ts.map +1 -1
  60. package/lib/Cloudflare/SecretsStore/Secret.js +55 -7
  61. package/lib/Cloudflare/SecretsStore/Secret.js.map +1 -1
  62. package/lib/Cloudflare/SecretsStore/SecretsStore.d.ts.map +1 -1
  63. package/lib/Cloudflare/SecretsStore/SecretsStore.js +35 -11
  64. package/lib/Cloudflare/SecretsStore/SecretsStore.js.map +1 -1
  65. package/lib/Cloudflare/StateStore/State.d.ts +35 -0
  66. package/lib/Cloudflare/StateStore/State.d.ts.map +1 -1
  67. package/lib/Cloudflare/StateStore/State.js +187 -45
  68. package/lib/Cloudflare/StateStore/State.js.map +1 -1
  69. package/lib/Cloudflare/StateStore/index.d.ts +1 -1
  70. package/lib/Cloudflare/StateStore/index.d.ts.map +1 -1
  71. package/lib/Cloudflare/StateStore/index.js +1 -1
  72. package/lib/Cloudflare/StateStore/index.js.map +1 -1
  73. package/lib/Cloudflare/Workers/Assets.d.ts +1 -1
  74. package/lib/Cloudflare/Workers/Assets.d.ts.map +1 -1
  75. package/lib/Cloudflare/Workers/Assets.js +6 -1
  76. package/lib/Cloudflare/Workers/Assets.js.map +1 -1
  77. package/lib/Cloudflare/Workers/Compatibility.d.ts +6 -0
  78. package/lib/Cloudflare/Workers/Compatibility.d.ts.map +1 -0
  79. package/lib/Cloudflare/Workers/Compatibility.js +10 -0
  80. package/lib/Cloudflare/Workers/Compatibility.js.map +1 -0
  81. package/lib/Cloudflare/Workers/DurableObjectNamespace.d.ts +103 -6
  82. package/lib/Cloudflare/Workers/DurableObjectNamespace.d.ts.map +1 -1
  83. package/lib/Cloudflare/Workers/DurableObjectNamespace.js +103 -6
  84. package/lib/Cloudflare/Workers/DurableObjectNamespace.js.map +1 -1
  85. package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts +7 -0
  86. package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts.map +1 -0
  87. package/lib/Cloudflare/Workers/LocalWorkerProvider.js +81 -0
  88. package/lib/Cloudflare/Workers/LocalWorkerProvider.js.map +1 -0
  89. package/lib/Cloudflare/Workers/Worker.d.ts +3 -1
  90. package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
  91. package/lib/Cloudflare/Workers/Worker.js +54 -7
  92. package/lib/Cloudflare/Workers/Worker.js.map +1 -1
  93. package/lib/Cloudflare/Workers/WorkerBundle.d.ts +31 -0
  94. package/lib/Cloudflare/Workers/WorkerBundle.d.ts.map +1 -0
  95. package/lib/Cloudflare/Workers/WorkerBundle.js +174 -0
  96. package/lib/Cloudflare/Workers/WorkerBundle.js.map +1 -0
  97. package/lib/Cloudflare/Workers/WorkerName.d.ts +3 -0
  98. package/lib/Cloudflare/Workers/WorkerName.d.ts.map +1 -0
  99. package/lib/Cloudflare/Workers/WorkerName.js +9 -0
  100. package/lib/Cloudflare/Workers/WorkerName.js.map +1 -0
  101. package/lib/Cloudflare/Workers/Workflow.d.ts +94 -0
  102. package/lib/Cloudflare/Workers/Workflow.d.ts.map +1 -1
  103. package/lib/Cloudflare/Workers/Workflow.js +94 -0
  104. package/lib/Cloudflare/Workers/Workflow.js.map +1 -1
  105. package/lib/Cloudflare/Workers/index.d.ts +1 -0
  106. package/lib/Cloudflare/Workers/index.d.ts.map +1 -1
  107. package/lib/Cloudflare/Workers/index.js +1 -0
  108. package/lib/Cloudflare/Workers/index.js.map +1 -1
  109. package/lib/Platform.d.ts +1 -1
  110. package/lib/Platform.d.ts.map +1 -1
  111. package/lib/Platform.js +1 -1
  112. package/lib/Platform.js.map +1 -1
  113. package/lib/Sidecar/Lock.d.ts +24 -0
  114. package/lib/Sidecar/Lock.d.ts.map +1 -0
  115. package/lib/Sidecar/Lock.js +102 -0
  116. package/lib/Sidecar/Lock.js.map +1 -0
  117. package/lib/Sidecar/RpcClient.d.ts +15 -0
  118. package/lib/Sidecar/RpcClient.d.ts.map +1 -0
  119. package/lib/Sidecar/RpcClient.js +75 -0
  120. package/lib/Sidecar/RpcClient.js.map +1 -0
  121. package/lib/Sidecar/RpcHandler.d.ts +48 -0
  122. package/lib/Sidecar/RpcHandler.d.ts.map +1 -0
  123. package/lib/Sidecar/RpcHandler.js +64 -0
  124. package/lib/Sidecar/RpcHandler.js.map +1 -0
  125. package/lib/Sidecar/RpcPaths.d.ts +14 -0
  126. package/lib/Sidecar/RpcPaths.d.ts.map +1 -0
  127. package/lib/Sidecar/RpcPaths.js +22 -0
  128. package/lib/Sidecar/RpcPaths.js.map +1 -0
  129. package/lib/Sidecar/RpcServer.d.ts +10 -0
  130. package/lib/Sidecar/RpcServer.d.ts.map +1 -0
  131. package/lib/Sidecar/RpcServer.js +50 -0
  132. package/lib/Sidecar/RpcServer.js.map +1 -0
  133. package/lib/Sidecar/RpcTransport.d.ts +22 -0
  134. package/lib/Sidecar/RpcTransport.d.ts.map +1 -0
  135. package/lib/Sidecar/RpcTransport.js +81 -0
  136. package/lib/Sidecar/RpcTransport.js.map +1 -0
  137. package/lib/Telemetry/Metrics.d.ts +15 -0
  138. package/lib/Telemetry/Metrics.d.ts.map +1 -1
  139. package/lib/Telemetry/Metrics.js +20 -0
  140. package/lib/Telemetry/Metrics.js.map +1 -1
  141. package/lib/Util/PlatformServices.d.ts +3 -0
  142. package/lib/Util/PlatformServices.d.ts.map +1 -1
  143. package/lib/Util/PlatformServices.js +13 -0
  144. package/lib/Util/PlatformServices.js.map +1 -1
  145. package/lib/Util/layer-scoped.d.ts +16 -0
  146. package/lib/Util/layer-scoped.d.ts.map +1 -0
  147. package/lib/Util/layer-scoped.js +14 -0
  148. package/lib/Util/layer-scoped.js.map +1 -0
  149. package/package.json +4 -2
  150. package/src/AWS/Lambda/Function.ts +30 -24
  151. package/src/AWS/Lambda/HttpServer.ts +3 -1
  152. package/src/AdoptPolicy.ts +9 -9
  153. package/src/AlchemyContext.ts +2 -0
  154. package/src/Build/Command.ts +8 -1
  155. package/src/Bundle/Bundle.ts +65 -25
  156. package/src/Cloudflare/Container/Container.ts +55 -2
  157. package/src/Cloudflare/D1/D1Export.ts +1 -5
  158. package/src/Cloudflare/D1/D1SqlFile.ts +1 -3
  159. package/src/Cloudflare/Local/Sidecar.ts +44 -0
  160. package/src/Cloudflare/Local/SidecarHandlers.ts +182 -0
  161. package/src/Cloudflare/Local/SidecarServer.ts +57 -0
  162. package/src/Cloudflare/Queue/Queue.ts +34 -4
  163. package/src/Cloudflare/Queue/QueueBinding.ts +33 -0
  164. package/src/Cloudflare/SecretsStore/Secret.ts +78 -7
  165. package/src/Cloudflare/SecretsStore/SecretsStore.ts +47 -12
  166. package/src/Cloudflare/StateStore/State.ts +281 -64
  167. package/src/Cloudflare/StateStore/index.ts +1 -1
  168. package/src/Cloudflare/Workers/Assets.ts +10 -1
  169. package/src/Cloudflare/Workers/Compatibility.ts +12 -0
  170. package/src/Cloudflare/Workers/DurableObjectNamespace.ts +103 -6
  171. package/src/Cloudflare/Workers/LocalWorkerProvider.ts +93 -0
  172. package/src/Cloudflare/Workers/Worker.ts +102 -19
  173. package/src/Cloudflare/Workers/WorkerBundle.ts +234 -0
  174. package/src/Cloudflare/Workers/WorkerName.ts +10 -0
  175. package/src/Cloudflare/Workers/Workflow.ts +94 -0
  176. package/src/Cloudflare/Workers/index.ts +1 -0
  177. package/src/Platform.ts +2 -2
  178. package/src/Sidecar/Lock.ts +190 -0
  179. package/src/Sidecar/RpcClient.ts +141 -0
  180. package/src/Sidecar/RpcHandler.ts +169 -0
  181. package/src/Sidecar/RpcPaths.ts +29 -0
  182. package/src/Sidecar/RpcServer.ts +80 -0
  183. package/src/Sidecar/RpcTransport.ts +85 -0
  184. package/src/Telemetry/Metrics.ts +38 -0
  185. package/src/Util/PlatformServices.ts +19 -0
  186. package/src/Util/layer-scoped.ts +25 -0
@@ -301,6 +301,51 @@ export class DurableObjectNamespaceScope extends Context.Service<
301
301
  * };
302
302
  * ```
303
303
  *
304
+ * @section Returning Streams from RPC
305
+ * RPC methods can return an Effect `Stream` and the caller will see
306
+ * the chunks as they're produced. Combine with `Stream.schedule` to
307
+ * pace emission, or with `Stream.fromQueue` to bridge an inbound
308
+ * subscription.
309
+ *
310
+ * @example Streaming sequential numbers
311
+ * ```typescript
312
+ * import * as Schedule from "effect/Schedule";
313
+ * import * as Stream from "effect/Stream";
314
+ *
315
+ * return {
316
+ * tick: (n: number) =>
317
+ * Stream.iterate(0, (i) => i + 1).pipe(
318
+ * Stream.take(n),
319
+ * Stream.schedule(Schedule.spaced("100 millis")),
320
+ * ),
321
+ * };
322
+ * ```
323
+ *
324
+ * @example Forwarding the stream as a chunked HTTP response
325
+ * ```typescript
326
+ * // in a Worker fetch handler
327
+ * const counter = counters.getByName("tick");
328
+ * const stream = counter.tick(5).pipe(
329
+ * Stream.map((i) => `${i}\n`),
330
+ * Stream.encodeText,
331
+ * );
332
+ * return HttpServerResponse.stream(stream, {
333
+ * headers: { "content-type": "text/plain" },
334
+ * });
335
+ * ```
336
+ *
337
+ * @section Worker → DO HTTP forwarding
338
+ * In addition to RPC methods, the typed stub exposes a `fetch`
339
+ * method that forwards an `HttpServerRequest` straight to the DO.
340
+ * The DO's own `fetch` Effect produces the response — useful for
341
+ * WebSocket upgrades and other request-shaped interactions.
342
+ *
343
+ * @example Forwarding an HTTP request to a DO
344
+ * ```typescript
345
+ * const room = rooms.getByName(roomId);
346
+ * return yield* room.fetch(request);
347
+ * ```
348
+ *
304
349
  * @section Accessing Instance State
305
350
  * Each Durable Object instance has its own transactional key-value
306
351
  * storage via `Cloudflare.DurableObjectState`. Use `storage.get` and
@@ -356,14 +401,66 @@ export class DurableObjectNamespaceScope extends Context.Service<
356
401
  * ```
357
402
  *
358
403
  * @example Recovering sessions after hibernation
404
+ * Place the rehydration loop **inside the inner `Effect.gen`** so
405
+ * it runs every time the DO instance is reconstructed (including
406
+ * after Cloudflare wakes the DO from hibernation).
407
+ *
359
408
  * ```typescript
360
- * const state = yield* Cloudflare.DurableObjectState;
361
- * const sockets = yield* state.getWebSockets();
409
+ * return Effect.gen(function* () {
410
+ * const state = yield* Cloudflare.DurableObjectState;
411
+ * const sessions = new Map<string, Cloudflare.DurableWebSocket>();
412
+ *
413
+ * // Rehydrate the in-memory session map after hibernation.
414
+ * for (const socket of yield* state.getWebSockets()) {
415
+ * const data = socket.deserializeAttachment<{ id: string }>();
416
+ * if (data) sessions.set(data.id, socket);
417
+ * }
362
418
  *
363
- * for (const socket of sockets) {
364
- * const data = socket.deserializeAttachment<{ id: string }>();
365
- * // re-populate your session map
366
- * }
419
+ * return {
420
+ * fetch: Effect.gen(function* () {
421
+ * const [response, socket] = yield* Cloudflare.upgrade();
422
+ * const id = crypto.randomUUID();
423
+ * socket.serializeAttachment({ id });
424
+ * sessions.set(id, socket);
425
+ * return response;
426
+ * }),
427
+ * webSocketMessage: Effect.fnUntraced(function* (socket, message) {
428
+ * const text =
429
+ * typeof message === "string" ? message : new TextDecoder().decode(message);
430
+ * for (const peer of sessions.values()) {
431
+ * yield* peer.send(text);
432
+ * }
433
+ * }),
434
+ * };
435
+ * });
436
+ * ```
437
+ *
438
+ * @section Scheduled Alarms
439
+ * Each Durable Object can have a single alarm timestamp. Alchemy
440
+ * layers a small SQLite-backed scheduler on top via
441
+ * `Cloudflare.scheduleEvent` and `Cloudflare.processScheduledEvents`,
442
+ * so you can register many named events with arbitrary payloads and
443
+ * fire them from a single `alarm` handler.
444
+ *
445
+ * @example Scheduling and processing events
446
+ * ```typescript
447
+ * // schedule from a request or message handler
448
+ * yield* Cloudflare.scheduleEvent(
449
+ * "reminder-1",
450
+ * new Date(Date.now() + 60_000),
451
+ * { message: "your meeting starts in a minute" },
452
+ * );
453
+ *
454
+ * return {
455
+ * alarm: () =>
456
+ * Effect.gen(function* () {
457
+ * const fired = yield* Cloudflare.processScheduledEvents;
458
+ * for (const event of fired) {
459
+ * const payload = event.payload as { message: string };
460
+ * // dispatch / broadcast / persist...
461
+ * }
462
+ * }),
463
+ * };
367
464
  * ```
368
465
  *
369
466
  * @section Using from a Worker
@@ -0,0 +1,93 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Redacted from "effect/Redacted";
3
+ import * as Provider from "../../Provider.ts";
4
+ import type { ResourceBinding } from "../../Resource.ts";
5
+ import { Stack } from "../../Stack.ts";
6
+ import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
7
+ import { Sidecar } from "../Local/Sidecar.ts";
8
+ import { getCompatibility } from "./Compatibility.ts";
9
+ import { Worker, type WorkerBinding, type WorkerProps } from "./Worker.ts";
10
+ import { createWorkerName } from "./WorkerName.ts";
11
+
12
+ export const LocalWorkerProvider = () =>
13
+ Provider.effect(
14
+ Worker,
15
+ Effect.gen(function* () {
16
+ const { accountId } = yield* CloudflareEnvironment;
17
+ const stack = yield* Stack;
18
+ const sidecar = yield* Sidecar;
19
+
20
+ const run = Effect.fn(function* (
21
+ id: string,
22
+ props: WorkerProps,
23
+ bindings: ResourceBinding<Worker["Binding"]>[],
24
+ ) {
25
+ const name = yield* createWorkerName(id, props.name);
26
+ const workerBindings: WorkerBinding[] = [];
27
+ const durableObjectNamespaces: Record<string, string> = {};
28
+ for (const { sid, data } of bindings) {
29
+ for (const binding of data.bindings ?? []) {
30
+ workerBindings.push(binding);
31
+ if (binding.type === "durable_object_namespace") {
32
+ durableObjectNamespaces[binding.name] = sid;
33
+ }
34
+ }
35
+ }
36
+ for (const [key, value] of Object.entries(props.env ?? {})) {
37
+ if (Redacted.isRedacted(value)) {
38
+ workerBindings.push({
39
+ type: "secret_text",
40
+ name: key,
41
+ text: Redacted.value(value),
42
+ });
43
+ } else {
44
+ workerBindings.push({
45
+ type: "plain_text",
46
+ name: key,
47
+ text: value,
48
+ });
49
+ }
50
+ }
51
+ const result = yield* sidecar.serve({
52
+ id,
53
+ name,
54
+ main: props.main,
55
+ compatibility: getCompatibility(props),
56
+ entry: props.isExternal
57
+ ? {
58
+ kind: "external",
59
+ }
60
+ : {
61
+ kind: "effect",
62
+ exports: (props.exports ?? {}) as any,
63
+ },
64
+ stack: { name: stack.name, stage: stack.stage },
65
+ bindings: workerBindings,
66
+ durableObjectNamespaces: Object.entries(durableObjectNamespaces).map(
67
+ ([className, namespaceId]) => ({
68
+ className,
69
+ uniqueKey: namespaceId,
70
+ sql: true,
71
+ }),
72
+ ),
73
+ });
74
+ return {
75
+ workerId: name,
76
+ workerName: name,
77
+ logpush: undefined,
78
+ url: result.address,
79
+ tags: [],
80
+ durableObjectNamespaces,
81
+ domains: [],
82
+ accountId,
83
+ } satisfies Worker["Attributes"];
84
+ });
85
+
86
+ return {
87
+ diff: () => Effect.succeed({ action: "update" }),
88
+ create: ({ id, news, bindings }) => run(id, news, bindings),
89
+ update: ({ id, news, bindings }) => run(id, news, bindings),
90
+ delete: ({ output }) => sidecar.stop(output.workerName),
91
+ };
92
+ }),
93
+ );
@@ -22,6 +22,8 @@ import { pathToFileURL } from "node:url";
22
22
  import type * as rolldown from "rolldown";
23
23
  import Sonda from "sonda/rolldown";
24
24
  import type * as vite from "vite";
25
+ import { AdoptPolicy } from "../../AdoptPolicy.ts";
26
+ import { AlchemyContext } from "../../AlchemyContext.ts";
25
27
  import * as Artifacts from "../../Artifacts.ts";
26
28
  import * as Binding from "../../Binding.ts";
27
29
  import {
@@ -53,6 +55,7 @@ import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
53
55
  import { D1Database } from "../D1/D1Database.ts";
54
56
  import { fromCloudflareFetcher } from "../Fetcher.ts";
55
57
  import type { KVNamespace } from "../KV/KVNamespace.ts";
58
+ import { SidecarLive } from "../Local/Sidecar.ts";
56
59
  import { CloudflareLogs } from "../Logs.ts";
57
60
  import type { Providers } from "../Providers.ts";
58
61
  import type { Queue as CloudflareQueue } from "../Queue/Queue.ts";
@@ -72,6 +75,7 @@ import {
72
75
  type DurableObjectNamespaceLike,
73
76
  } from "./DurableObjectNamespace.ts";
74
77
  import { workersHttpHandler } from "./HttpServer.ts";
78
+ import { LocalWorkerProvider } from "./LocalWorkerProvider.ts";
75
79
  import { Request } from "./Request.ts";
76
80
  import { makeRpcStub } from "./Rpc.ts";
77
81
  import { isWorkflowExport } from "./Workflow.ts";
@@ -966,7 +970,30 @@ function getDurableObjectTagMap(tags: ReadonlyArray<string>) {
966
970
  );
967
971
  }
968
972
 
973
+ const selectLayer = <
974
+ LayerLive extends Layer.Layer<any, any, any>,
975
+ LayerDev extends Layer.Layer<any, any, any>,
976
+ >(input: {
977
+ live: () => LayerLive;
978
+ dev: () => LayerDev;
979
+ }): Layer.Layer<
980
+ Layer.Success<LayerLive | LayerDev>,
981
+ Layer.Error<LayerLive | LayerDev>,
982
+ Layer.Services<LayerLive | LayerDev> | AlchemyContext
983
+ > =>
984
+ Layer.unwrap(
985
+ AlchemyContext.useSync((context) =>
986
+ context.dev ? input.dev() : input.live(),
987
+ ),
988
+ );
989
+
969
990
  export const WorkerProvider = () =>
991
+ selectLayer({
992
+ live: LiveWorkerProvider,
993
+ dev: () => Layer.provide(LocalWorkerProvider(), SidecarLive),
994
+ });
995
+
996
+ export const LiveWorkerProvider = () =>
970
997
  Provider.effect(
971
998
  Worker,
972
999
  Effect.gen(function* () {
@@ -1094,27 +1121,83 @@ export const WorkerProvider = () =>
1094
1121
 
1095
1122
  if (desired.length === 0) return [];
1096
1123
 
1124
+ const adoptEnabled = yield* Effect.serviceOption(AdoptPolicy).pipe(
1125
+ Effect.map(Option.getOrElse(() => false)),
1126
+ );
1127
+
1097
1128
  const zoneCache = new Map<string, string>();
1129
+
1130
+ /**
1131
+ * Attach `hostname` to `scriptName`, recovering from a "hostname
1132
+ * already in use" conflict when adoption is enabled.
1133
+ *
1134
+ * The Cloudflare API rejects a `PUT /workers/domains` whose
1135
+ * hostname is already registered (even when the existing entry
1136
+ * points at the same Worker, which is exactly what happens
1137
+ * after a state-store wipe). The SDK surfaces this as the
1138
+ * unmatched `UnknownCloudflareError` channel because there's
1139
+ * no dedicated error tag for it. With adoption opted-in we
1140
+ * list domains for the hostname; if the existing entry
1141
+ * already targets *this* Worker we adopt it in place. We
1142
+ * never transfer a hostname off another Worker — that would
1143
+ * silently re-route someone else's traffic.
1144
+ */
1145
+ const attachDomain = Effect.fnUntraced(function* (hostname: string) {
1146
+ const zoneId = yield* inferZoneIdForHostname(hostname, zoneCache);
1147
+ return yield* putDomain({
1148
+ accountId,
1149
+ hostname,
1150
+ service: scriptName,
1151
+ zoneId,
1152
+ }).pipe(
1153
+ Effect.map((res) => ({
1154
+ hostname,
1155
+ id: res.id ?? "",
1156
+ zoneId: res.zoneId ?? zoneId,
1157
+ })),
1158
+ Effect.catchTag("UnknownCloudflareError", (err) =>
1159
+ adoptEnabled && /already in use/i.test(err.message)
1160
+ ? adoptDomainIfSameService(hostname, zoneId, err)
1161
+ : Effect.fail(err),
1162
+ ),
1163
+ );
1164
+ });
1165
+
1166
+ const adoptDomainIfSameService = Effect.fnUntraced(function* (
1167
+ hostname: string,
1168
+ zoneId: string,
1169
+ originalError: { message: string },
1170
+ ) {
1171
+ const existing = yield* listDomains({
1172
+ accountId,
1173
+ hostname,
1174
+ }).pipe(
1175
+ Effect.map((r) =>
1176
+ (r.result ?? []).find((d) => d.hostname === hostname),
1177
+ ),
1178
+ );
1179
+ if (existing?.id && existing.service === scriptName) {
1180
+ return {
1181
+ hostname,
1182
+ id: existing.id,
1183
+ zoneId: existing.zoneId ?? zoneId,
1184
+ };
1185
+ }
1186
+ // The hostname exists but on a different Worker. Refuse to
1187
+ // hijack it; surface a more actionable message than the raw
1188
+ // Cloudflare error.
1189
+ return yield* Effect.die(
1190
+ new Error(
1191
+ `Cannot attach hostname '${hostname}' to Worker '${scriptName}': ` +
1192
+ `it is already attached to Worker '${existing?.service ?? "<unknown>"}'. ` +
1193
+ `Detach it from that Worker first, or pick a different hostname. ` +
1194
+ `(Cloudflare reported: ${originalError.message})`,
1195
+ ),
1196
+ );
1197
+ });
1198
+
1098
1199
  const applied = yield* Effect.all(
1099
- desired.map((hostname) =>
1100
- Effect.gen(function* () {
1101
- const zoneId = yield* inferZoneIdForHostname(
1102
- hostname,
1103
- zoneCache,
1104
- );
1105
- const res = yield* putDomain({
1106
- accountId,
1107
- hostname,
1108
- service: scriptName,
1109
- zoneId,
1110
- });
1111
- return {
1112
- hostname,
1113
- id: res.id ?? "",
1114
- zoneId: res.zoneId ?? zoneId,
1115
- };
1116
- }),
1117
- ),
1200
+ desired.map(attachDomain),
1118
1201
  { concurrency: "unbounded" },
1119
1202
  );
1120
1203
  return applied;
@@ -0,0 +1,234 @@
1
+ import cloudflareRolldown from "@distilled.cloud/cloudflare-rolldown-plugin";
2
+ import * as Effect from "effect/Effect";
3
+ import * as FileSystem from "effect/FileSystem";
4
+ import { flow } from "effect/Function";
5
+ import type * as Path from "effect/Path";
6
+ import * as Stream from "effect/Stream";
7
+ import type * as rolldown from "rolldown";
8
+ import * as Artifacts from "../../Artifacts.ts";
9
+ import * as Bundle from "../../Bundle/Bundle.ts";
10
+ import { findCwdForBundle } from "../../Bundle/TempRoot.ts";
11
+ import { Self } from "../../Self.ts";
12
+ import {
13
+ isDurableObjectExport,
14
+ type DurableObjectExport,
15
+ } from "./DurableObjectNamespace.ts";
16
+ import { isWorkflowExport, type WorkflowExport } from "./Workflow.ts";
17
+
18
+ export interface WorkerBundleOptions {
19
+ id: string;
20
+ main: string;
21
+ compatibility: {
22
+ date: string;
23
+ flags: string[];
24
+ };
25
+ entry:
26
+ | {
27
+ kind: "external";
28
+ }
29
+ | {
30
+ kind: "effect";
31
+ exports: Record<string, DurableObjectExport | WorkflowExport>;
32
+ };
33
+ stack: { name: string; stage: string };
34
+ }
35
+
36
+ export const WorkerBundle = Effect.gen(function* () {
37
+ const fs = yield* FileSystem.FileSystem;
38
+ const context = yield* Effect.context<FileSystem.FileSystem | Path.Path>();
39
+ const virtualEntryPlugin = yield* Bundle.virtualEntryPlugin;
40
+
41
+ const makeOptions = Effect.fnUntraced(function* (
42
+ options: WorkerBundleOptions,
43
+ ) {
44
+ const realMain = yield* fs.realPath(options.main).pipe(
45
+ Effect.mapError(
46
+ (cause) =>
47
+ new Bundle.BundleError({
48
+ message: `Failed to find real path for bundle: ${options.main}`,
49
+ cause,
50
+ }),
51
+ ),
52
+ );
53
+ const inputOptions: rolldown.InputOptions = {
54
+ input: realMain,
55
+ cwd: yield* findCwdForBundle(realMain).pipe(
56
+ Effect.mapError(
57
+ (cause) =>
58
+ new Bundle.BundleError({
59
+ message: `Failed to find cwd for bundle: ${realMain}`,
60
+ cause,
61
+ }),
62
+ ),
63
+ Effect.provide(context),
64
+ ),
65
+ plugins: [
66
+ cloudflareRolldown({
67
+ compatibilityDate: options.compatibility.date,
68
+ compatibilityFlags: options.compatibility.flags,
69
+ }),
70
+ options.entry.kind === "effect"
71
+ ? virtualEntryPlugin(
72
+ makeEffectVirtualEntry(options.entry.exports, options.stack),
73
+ )
74
+ : undefined,
75
+ ],
76
+ checks: {
77
+ // Suppress unresolved import warnings for unrelated AWS packages
78
+ unresolvedImport: false,
79
+ },
80
+ };
81
+ const outputOptions: rolldown.OutputOptions = {
82
+ format: "esm",
83
+ sourcemap: "hidden",
84
+ minify: true,
85
+ keepNames: true,
86
+ dir: `.alchemy/bundles/${options.id}`,
87
+ };
88
+ return { inputOptions, outputOptions };
89
+ });
90
+
91
+ return {
92
+ build: flow(
93
+ makeOptions,
94
+ Effect.flatMap((resolved) =>
95
+ Bundle.build(resolved.inputOptions, resolved.outputOptions),
96
+ ),
97
+ Artifacts.cached("build"),
98
+ ),
99
+ watch: flow(
100
+ makeOptions,
101
+ Stream.fromEffect,
102
+ Stream.flatMap((resolved) =>
103
+ Bundle.watch(resolved.inputOptions, resolved.outputOptions),
104
+ ),
105
+ ),
106
+ };
107
+ });
108
+
109
+ const makeEffectVirtualEntry = (
110
+ exports: Record<string, DurableObjectExport | WorkflowExport>,
111
+ stack: { name: string; stage: string },
112
+ ) => {
113
+ const durableObjectClasses: string[] = [];
114
+ const workflowClasses: string[] = [];
115
+ for (const [className, entry] of Object.entries(exports)) {
116
+ if (isDurableObjectExport(entry)) {
117
+ durableObjectClasses.push(className);
118
+ } else if (isWorkflowExport(entry)) {
119
+ workflowClasses.push(className);
120
+ }
121
+ }
122
+ return (importPath: string) => `import * as Config from "effect/Config";
123
+ import * as ConfigProvider from "effect/ConfigProvider";
124
+ import * as Console from "effect/Console";
125
+ import * as Effect from "effect/Effect";
126
+ import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
127
+ import * as Layer from "effect/Layer";
128
+ import * as Logger from "effect/Logger";
129
+ import * as Context from "effect/Context";
130
+ import * as Stream from "effect/Stream";
131
+
132
+ import { env, DurableObject${workflowClasses.length > 0 ? ", WorkflowEntrypoint" : ""} } from "cloudflare:workers";
133
+ import { MinimumLogLevel } from "effect/References";
134
+ import { NodeServices } from "@effect/platform-node";
135
+ import { Stack } from "alchemy/Stack";
136
+ import { WorkerEnvironment, makeDurableObjectBridge${workflowClasses.length > 0 ? ", makeWorkflowBridge" : ""}, ExportedHandlerMethods } from "alchemy/Cloudflare";
137
+
138
+ import entry from "${importPath}";
139
+
140
+ const tag = Context.Service("${Self.key}")
141
+ const layer =
142
+ typeof entry?.build === "function"
143
+ ? entry
144
+ : Layer.effect(tag, typeof entry?.asEffect === "function" ? entry.asEffect() : entry);
145
+
146
+ const platform = Layer.mergeAll(
147
+ NodeServices.layer,
148
+ FetchHttpClient.layer,
149
+ // TODO(sam): wire this up to telemetry more directly
150
+ Logger.layer([Logger.consolePretty()]),
151
+ );
152
+
153
+ const stack = Layer.succeed(
154
+ Stack,
155
+ {
156
+ name: "${stack.name}",
157
+ stage: "${stack.stage}",
158
+ bindings: {},
159
+ resources: {}
160
+ }
161
+ );
162
+
163
+ const exportsEffect = tag.asEffect().pipe(
164
+ Effect.flatMap(func => func.ExecutionContext.exports),
165
+ Effect.map(exports => exports),
166
+ Effect.provide(
167
+ layer.pipe(
168
+ Layer.provideMerge(stack),
169
+ // TODO(sam): additional credentials?
170
+ Layer.provideMerge(platform),
171
+ Layer.provideMerge(
172
+ Layer.succeed(
173
+ ConfigProvider.ConfigProvider,
174
+ ConfigProvider.orElse(
175
+ ConfigProvider.fromUnknown({ ALCHEMY_PHASE: "runtime" }),
176
+ ConfigProvider.fromUnknown(env),
177
+ ),
178
+ )
179
+ ),
180
+ Layer.provideMerge(
181
+ Layer.succeed(
182
+ WorkerEnvironment,
183
+ env,
184
+ )
185
+ ),
186
+ Layer.provideMerge(
187
+ Layer.succeed(
188
+ MinimumLogLevel,
189
+ env.DEBUG ? "Debug" : "Info",
190
+ )
191
+ ),
192
+ )
193
+ ),
194
+ Effect.scoped
195
+ );
196
+
197
+ // TODO(sam): we could kick this off during module init, but any I/O will break deploy
198
+ // let exportsPromise = Effect.runPromise(exportsEffect);
199
+
200
+ // for now, we delay initializing the worker until the first request
201
+ let exportsPromise;
202
+
203
+ // don't initialize the workerEffect during module init because Cloudflare does not allow I/O during module init
204
+ // we cache it synchronously (??=) to guarnatee only one initialization ever happens
205
+ const getExports = () => (exportsPromise ??= Effect.runPromise(exportsEffect))
206
+ const getExport = (name) => getExports().then(exports => exports[name]?.make)
207
+ const worker = () => getExports().then(exports => exports.default)
208
+
209
+ export default Object.fromEntries(ExportedHandlerMethods.map(
210
+ method => [method, async (...args) => (await worker())[method](...args)])
211
+ ) satisfies Required<cf.ExportedHandler>;
212
+
213
+ // export class proxy stubs for Durable Objects and Workflows
214
+ ${[
215
+ ...(durableObjectClasses.length > 0
216
+ ? [
217
+ "const DurableObjectBridge = makeDurableObjectBridge(DurableObject, getExport);",
218
+ ...durableObjectClasses.map(
219
+ (id) =>
220
+ `export class ${id} extends DurableObjectBridge("${id}") {}`,
221
+ ),
222
+ ]
223
+ : []),
224
+ ...(workflowClasses.length > 0
225
+ ? [
226
+ "const WorkflowBridgeFn = makeWorkflowBridge(WorkflowEntrypoint, getExport);",
227
+ ...workflowClasses.map(
228
+ (id) => `export class ${id} extends WorkflowBridgeFn("${id}") {}`,
229
+ ),
230
+ ]
231
+ : []),
232
+ ].join("\n")}
233
+ `;
234
+ };
@@ -0,0 +1,10 @@
1
+ import * as Effect from "effect/Effect";
2
+ import { createPhysicalName } from "../../PhysicalName.ts";
3
+
4
+ export const createWorkerName = (id: string, name: string | undefined) =>
5
+ name
6
+ ? Effect.succeed(name)
7
+ : createPhysicalName({
8
+ id,
9
+ maxLength: 54,
10
+ }).pipe(Effect.map((name) => name.toLowerCase()));