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
@@ -143,12 +143,19 @@ export const CommandProvider = () =>
143
143
  if (!output) {
144
144
  return undefined;
145
145
  }
146
+ // Recompute the output path against the *current* cwd. State
147
+ // may have been written by a different machine (e.g. CI) and
148
+ // the absolute path baked into `output.outdir` won't exist
149
+ // locally. As long as the path resolved against this
150
+ // machine's cwd is present, return the refreshed output so
151
+ // downstream consumers (e.g. `Cloudflare.Worker` assets)
152
+ // don't try to read a stale, foreign-machine path.
146
153
  const outputPath = getOutputPath(olds);
147
154
  const exists = yield* fs.exists(outputPath);
148
155
  if (!exists) {
149
156
  return undefined;
150
157
  }
151
- return output;
158
+ return { ...output, outdir: outputPath };
152
159
  }),
153
160
  create: Effect.fnUntraced(function* ({ news, session }) {
154
161
  const hash = yield* hashDirectory(news);
@@ -1,8 +1,7 @@
1
- import * as Data from "effect/Data";
2
1
  import * as Effect from "effect/Effect";
3
2
  import * as Path from "effect/Path";
4
3
  import * as Queue from "effect/Queue";
5
- import * as Result from "effect/Result";
4
+ import * as Schema from "effect/Schema";
6
5
  import * as Stream from "effect/Stream";
7
6
  import assert from "node:assert";
8
7
  import * as rolldown from "rolldown";
@@ -45,10 +44,32 @@ export interface BundleFile {
45
44
  readonly hash: string;
46
45
  }
47
46
 
48
- export class BundleError extends Data.TaggedError("BundleError")<{
49
- readonly message: string;
50
- readonly cause?: unknown;
51
- }> {}
47
+ export class BundleError extends Schema.TaggedErrorClass<BundleError>()(
48
+ "BundleError",
49
+ {
50
+ message: Schema.String,
51
+ cause: Schema.optional(Schema.DefectWithStack),
52
+ },
53
+ ) {}
54
+
55
+ export type BundleWatchEvent =
56
+ | BundleWatchEvent.Start
57
+ | BundleWatchEvent.Success
58
+ | BundleWatchEvent.Error;
59
+
60
+ export declare namespace BundleWatchEvent {
61
+ interface Start {
62
+ readonly _tag: "Start";
63
+ }
64
+ interface Success {
65
+ readonly _tag: "Success";
66
+ readonly output: BundleOutput;
67
+ }
68
+ interface Error {
69
+ readonly _tag: "Error";
70
+ readonly error: BundleError;
71
+ }
72
+ }
52
73
 
53
74
  /**
54
75
  * Build a bundle using rolldown from the given input options and output options.
@@ -93,8 +114,15 @@ export const watch = (
93
114
  inputOptions: rolldown.InputOptions,
94
115
  outputOptions?: rolldown.OutputOptions,
95
116
  extra?: BundleExtraOptions,
96
- ): Stream.Stream<Result.Result<BundleOutput, BundleError>> =>
97
- Stream.callback<Result.Result<rolldown.OutputBundle, BundleError>>((queue) =>
117
+ ): Stream.Stream<BundleWatchEvent> =>
118
+ Stream.callback<
119
+ | BundleWatchEvent.Start
120
+ | BundleWatchEvent.Error
121
+ | {
122
+ readonly _tag: "Success";
123
+ readonly output: rolldown.OutputBundle;
124
+ }
125
+ >((queue) =>
98
126
  Effect.acquireRelease(
99
127
  Effect.sync(() => {
100
128
  const watcher = rolldown.watch({
@@ -104,8 +132,16 @@ export const watch = (
104
132
  // The watcher event listener does not receive the bundle output, so we grab it using a plugin.
105
133
  {
106
134
  name: "alchemy:watch-bundle",
135
+ watchChange() {
136
+ Queue.offerUnsafe(queue, {
137
+ _tag: "Start",
138
+ });
139
+ },
107
140
  generateBundle(_outputOptions, bundle) {
108
- Queue.offerUnsafe(queue, Result.succeed(bundle));
141
+ Queue.offerUnsafe(queue, {
142
+ _tag: "Success",
143
+ output: bundle,
144
+ });
109
145
  },
110
146
  },
111
147
  ],
@@ -113,10 +149,10 @@ export const watch = (
113
149
  });
114
150
  watcher.on("event", (event) => {
115
151
  if (event.code === "ERROR") {
116
- Queue.offerUnsafe(
117
- queue,
118
- Result.fail(bundleErrorFromUnknown(event.error)),
119
- );
152
+ Queue.offerUnsafe(queue, {
153
+ _tag: "Error",
154
+ error: bundleErrorFromUnknown(event.error),
155
+ });
120
156
  } else if (event.code === "BUNDLE_END") {
121
157
  // This must be called to avoid resource leaks.
122
158
  event.result.close().catch(() => {});
@@ -127,23 +163,27 @@ export const watch = (
127
163
  (watcher) => Effect.promise(() => watcher.close()),
128
164
  ),
129
165
  ).pipe(
130
- Stream.mapEffect((result) =>
166
+ Stream.mapEffect((event) =>
131
167
  Effect.gen(function* () {
132
- if (result._tag === "Failure") {
133
- return Result.fail(result.failure);
168
+ if (event._tag !== "Success") {
169
+ return event;
134
170
  }
135
- return yield* bundleOutputFromRolldownOutputBundle(result.success).pipe(
136
- Effect.map(Result.succeed),
137
- Effect.catch((error) => Effect.succeed(Result.fail(error))),
171
+ return yield* bundleOutputFromRolldownOutputBundle(event.output).pipe(
172
+ Effect.map(
173
+ (output): BundleWatchEvent.Success => ({
174
+ _tag: "Success",
175
+ output,
176
+ }),
177
+ ),
178
+ Effect.catch((error) =>
179
+ Effect.succeed<BundleWatchEvent.Error>({
180
+ _tag: "Error",
181
+ error: bundleErrorFromUnknown(error),
182
+ }),
183
+ ),
138
184
  );
139
185
  }),
140
186
  ),
141
- Stream.changesWith((left, right) => {
142
- if (left._tag === "Success" && right._tag === "Success") {
143
- return left.success.hash === right.success.hash;
144
- }
145
- return false;
146
- }),
147
187
  );
148
188
 
149
189
  const ENTRY_MODULE_ID = "virtual:alchemy-entry";
@@ -118,8 +118,10 @@ export type Container = {
118
118
  * @section Configuration
119
119
  * The props object accepts `main` (entrypoint file), `instanceType`
120
120
  * (compute size), `runtime` (`"bun"` or `"node"`), and
121
- * `observability` settings. Use `Stack.useSync` to vary config by
122
- * stage.
121
+ * `observability` settings. Use `Stack.useSync` to read the
122
+ * surrounding stack at declaration time and pick a beefier
123
+ * `instanceType` in prod while keeping the cheap `dev` instance for
124
+ * preview environments.
123
125
  *
124
126
  * @example Stage-dependent configuration
125
127
  * ```typescript
@@ -133,6 +135,57 @@ export type Container = {
133
135
  * ) {}
134
136
  * ```
135
137
  *
138
+ * @section Stack-level wiring
139
+ * The `.make()` `export default` is the side-effect that registers
140
+ * the container's runtime. It must be reachable from your
141
+ * `alchemy.run.ts` so the bundler emits the runtime entrypoint.
142
+ * Provide it on the Stack's generator with `Effect.provide`.
143
+ *
144
+ * @example Wiring SandboxLive into the Stack
145
+ * ```typescript
146
+ * // alchemy.run.ts
147
+ * import SandboxLive from "./src/Sandbox.runtime.ts";
148
+ *
149
+ * export default Alchemy.Stack(
150
+ * "MyApp",
151
+ * { providers: Cloudflare.providers(), state: Cloudflare.state() },
152
+ * Effect.gen(function* () {
153
+ * const worker = yield* Worker;
154
+ * return { url: worker.url };
155
+ * }).pipe(Effect.provide(SandboxLive)),
156
+ * );
157
+ * ```
158
+ *
159
+ * @section Calling from a Durable Object
160
+ * Use `Cloudflare.Container.bind(Sandbox)` in the **outer** init
161
+ * phase of a Durable Object — only the class is imported, so the
162
+ * DO bundle stays tiny. Then `Cloudflare.start(sandbox)` in the
163
+ * **inner** per-instance phase ensures the container is running
164
+ * and gives you a typed handle that exposes every method declared
165
+ * on the container's shape **plus** a `getTcpPort` helper.
166
+ *
167
+ * @example Binding and starting a container from a DO
168
+ * ```typescript
169
+ * export default class Agent extends Cloudflare.DurableObjectNamespace<Agent>()(
170
+ * "Agents",
171
+ * Effect.gen(function* () {
172
+ * // OUTER (init): only the class is referenced — the runtime
173
+ * // implementation in `Sandbox.runtime.ts` is tree-shaken out
174
+ * // of this DO's bundle.
175
+ * const sandbox = yield* Cloudflare.Container.bind(Sandbox);
176
+ *
177
+ * return Effect.gen(function* () {
178
+ * // INNER (per-instance): start the container and expose RPC.
179
+ * const container = yield* Cloudflare.start(sandbox);
180
+ *
181
+ * return {
182
+ * exec: (cmd: string) => container.exec(cmd),
183
+ * };
184
+ * });
185
+ * }),
186
+ * ) {}
187
+ * ```
188
+ *
136
189
  * @section Starting from a Durable Object
137
190
  * Use `Cloudflare.Container.bind` in the outer init phase to bind
138
191
  * the container class, then `Cloudflare.start` in the inner
@@ -35,11 +35,7 @@ export const exportD1Database = (
35
35
 
36
36
  const poll = (
37
37
  currentBookmark?: string,
38
- ): Effect.Effect<
39
- ExportD1DatabaseResult,
40
- d1.ExportDatabaseError,
41
- never
42
- > =>
38
+ ): Effect.Effect<ExportD1DatabaseResult, d1.ExportDatabaseError, never> =>
43
39
  Effect.gen(function* () {
44
40
  const data = yield* exportDb({
45
41
  accountId: options.accountId,
@@ -29,9 +29,7 @@ export const listSqlFiles = (directory: string) =>
29
29
  return a.localeCompare(b);
30
30
  });
31
31
 
32
- return yield* Effect.all(
33
- sqlFiles.map((id) => readSqlFile(directory, id)),
34
- );
32
+ return yield* Effect.all(sqlFiles.map((id) => readSqlFile(directory, id)));
35
33
  });
36
34
 
37
35
  /**
@@ -0,0 +1,44 @@
1
+ import {
2
+ ServeError,
3
+ type ServeResult,
4
+ } from "@distilled.cloud/cloudflare-runtime/Server";
5
+ import * as Worker from "@distilled.cloud/cloudflare-runtime/Worker";
6
+ import * as Effect from "effect/Effect";
7
+ import * as Schema from "effect/Schema";
8
+ import { BundleError } from "../../Bundle/Bundle.ts";
9
+ import * as RpcClient from "../../Sidecar/RpcClient.ts";
10
+ import { defineSchema } from "../../Sidecar/RpcHandler.ts";
11
+ import type { WorkerBinding } from "../Workers/Worker.ts";
12
+ import type { WorkerBundleOptions } from "../Workers/WorkerBundle.ts";
13
+
14
+ export interface ServeOptions extends WorkerBundleOptions {
15
+ name: string;
16
+ bindings: WorkerBinding[];
17
+ durableObjectNamespaces: Worker.DurableObjectNamespace[];
18
+ }
19
+
20
+ export const SidecarSchema = defineSchema<Sidecar["Service"]>({
21
+ serve: {
22
+ success: Schema.Struct({
23
+ name: Schema.String,
24
+ address: Schema.String,
25
+ }),
26
+ error: Schema.Union([ServeError, BundleError]),
27
+ },
28
+ stop: { success: Schema.Void, error: Schema.Never },
29
+ });
30
+
31
+ export class Sidecar extends RpcClient.RpcClientService<
32
+ Sidecar,
33
+ {
34
+ readonly serve: (
35
+ options: ServeOptions,
36
+ ) => Effect.Effect<ServeResult, ServeError | BundleError>;
37
+ readonly stop: (name: string) => Effect.Effect<void>;
38
+ }
39
+ >()("Sidecar") {}
40
+
41
+ export const SidecarLive = RpcClient.layer(Sidecar, {
42
+ main: import.meta.resolve("./SidecarServer.ts", import.meta.url),
43
+ schema: SidecarSchema,
44
+ });
@@ -0,0 +1,182 @@
1
+ import * as Server from "@distilled.cloud/cloudflare-runtime/Server";
2
+ import type { WorkerModule } from "@distilled.cloud/cloudflare-runtime/WorkerModule";
3
+ import * as Cause from "effect/Cause";
4
+ import * as Deferred from "effect/Deferred";
5
+ import * as Effect from "effect/Effect";
6
+ import * as Exit from "effect/Exit";
7
+ import * as Fiber from "effect/Fiber";
8
+ import * as Hash from "effect/Hash";
9
+ import * as Layer from "effect/Layer";
10
+ import * as Result from "effect/Result";
11
+ import * as Scope from "effect/Scope";
12
+ import * as Stream from "effect/Stream";
13
+ import * as Bundle from "../../Bundle/Bundle.ts";
14
+ import { WorkerBundle } from "../Workers/WorkerBundle.ts";
15
+ import { Sidecar, type ServeOptions } from "./Sidecar.ts";
16
+
17
+ export const SidecarHandlers = Layer.effect(
18
+ Sidecar,
19
+ Effect.gen(function* () {
20
+ const bundle = yield* WorkerBundle;
21
+ const server = yield* Server.Server;
22
+
23
+ const rootScope = yield* Effect.scope;
24
+ const serverScopes = new Map<string, Scope.Closeable>();
25
+
26
+ const serveScoped = Effect.fnUntraced(function* (
27
+ worker: ServeOptions,
28
+ modules: WorkerModule[],
29
+ ) {
30
+ const scope = yield* Effect.flatMap(Effect.scope, Scope.fork);
31
+ const result = yield* server
32
+ .serve({
33
+ name: worker.id.toLowerCase(),
34
+ compatibilityDate: worker.compatibility.date,
35
+ compatibilityFlags: worker.compatibility.flags,
36
+ bindings: worker.bindings,
37
+ durableObjectNamespaces: worker.durableObjectNamespaces,
38
+ modules,
39
+ })
40
+ .pipe(Scope.provide(scope));
41
+ const previous = serverScopes.get(worker.name);
42
+ if (previous) {
43
+ yield* Effect.forkDetach(Scope.close(previous, Exit.void));
44
+ }
45
+ serverScopes.set(worker.name, scope);
46
+ return result;
47
+ });
48
+
49
+ const watchers = new Map<
50
+ string,
51
+ {
52
+ hash: number;
53
+ fiber: Fiber.Fiber<
54
+ Server.ServeResult,
55
+ Server.ServeError | Bundle.BundleError
56
+ >;
57
+ scope: Scope.Closeable;
58
+ }
59
+ >();
60
+
61
+ const serveFiber = Effect.fnUntraced(function* (worker: ServeOptions) {
62
+ const result = yield* Deferred.make<
63
+ Server.ServeResult,
64
+ Server.ServeError | Bundle.BundleError
65
+ >();
66
+ let start = Date.now();
67
+ yield* bundle.watch(worker).pipe(
68
+ Stream.mapEffect((event) =>
69
+ event._tag === "Error" && !Deferred.isDoneUnsafe(result)
70
+ ? Effect.fail(event.error)
71
+ : Effect.succeed(event),
72
+ ),
73
+ Stream.tap((event) => {
74
+ if (event._tag === "Start") {
75
+ start = Date.now();
76
+ if (Deferred.isDoneUnsafe(result)) {
77
+ return Effect.log(`[${worker.id}] Rebuilding`);
78
+ }
79
+ } else if (event._tag === "Error") {
80
+ return Effect.logError(`[${worker.id}] Bundle error`, event.error);
81
+ }
82
+ return Effect.void;
83
+ }),
84
+ Stream.filterMap((event) =>
85
+ event._tag === "Success"
86
+ ? Result.succeed(event.output)
87
+ : Result.failVoid,
88
+ ),
89
+ Stream.map(bundleOutputToWorkerModules),
90
+ Stream.mapEffect((modules) =>
91
+ serveScoped(worker, modules).pipe(
92
+ Effect.exit,
93
+ Effect.tap((exit) => {
94
+ const isDone = Deferred.isDoneUnsafe(result);
95
+ if (exit._tag === "Success") {
96
+ return Effect.log(
97
+ `[${worker.id}] ${isDone ? "Updated" : "Started"} in ${Math.round(Date.now() - start)}ms`,
98
+ );
99
+ } else if (isDone) {
100
+ return Effect.logError(
101
+ `[${worker.id}] Error`,
102
+ Cause.squash(exit.cause),
103
+ );
104
+ }
105
+ return Effect.void;
106
+ }),
107
+ Effect.tap((exit) => Deferred.complete(result, exit)),
108
+ ),
109
+ ),
110
+ Stream.onExit((exit) =>
111
+ exit._tag === "Failure" && !Deferred.isDoneUnsafe(result)
112
+ ? Deferred.failCause(result, exit.cause)
113
+ : Effect.void,
114
+ ),
115
+ Stream.runDrain,
116
+ Effect.forkScoped,
117
+ );
118
+ return yield* Deferred.await(result);
119
+ });
120
+
121
+ return Sidecar.of({
122
+ serve: Effect.fn(function* (worker: ServeOptions) {
123
+ const hash = Hash.structure(worker);
124
+ const existing = watchers.get(worker.name);
125
+ if (existing) {
126
+ if (existing.hash === hash) {
127
+ yield* Effect.log(
128
+ `[${worker.id}] No changes, using existing watcher`,
129
+ );
130
+ return yield* Fiber.join(existing.fiber);
131
+ }
132
+ yield* Effect.log(
133
+ `[${worker.id}] Changes detected, interrupting existing watcher`,
134
+ );
135
+ yield* Fiber.interrupt(existing.fiber);
136
+ yield* Scope.close(existing.scope, Exit.void);
137
+ watchers.delete(worker.name);
138
+ }
139
+ const scope = yield* Scope.fork(rootScope);
140
+ const fiber = yield* serveFiber(worker).pipe(
141
+ Effect.forkDetach,
142
+ Scope.provide(scope),
143
+ );
144
+ watchers.set(worker.name, { hash, fiber, scope });
145
+ return yield* Fiber.join(fiber).pipe(
146
+ Effect.onExit((exit) =>
147
+ Effect.sync(() => {
148
+ if (exit._tag === "Failure") {
149
+ watchers.delete(worker.name);
150
+ }
151
+ }),
152
+ ),
153
+ );
154
+ }),
155
+ stop: Effect.fn(function* (name: string) {
156
+ const watcher = watchers.get(name);
157
+ if (watcher) {
158
+ yield* Fiber.interrupt(watcher.fiber);
159
+ yield* Scope.close(watcher.scope, Exit.void);
160
+ watchers.delete(name);
161
+ }
162
+ }),
163
+ });
164
+ }),
165
+ );
166
+
167
+ function bundleOutputToWorkerModules(
168
+ bundle: Bundle.BundleOutput,
169
+ ): WorkerModule[] {
170
+ const modules: WorkerModule[] = [];
171
+ for (const file of bundle.files) {
172
+ if (file.path.endsWith(".map") || file.content instanceof Uint8Array) {
173
+ continue;
174
+ }
175
+ modules.push({
176
+ name: file.path,
177
+ type: "ESModule",
178
+ content: file.content,
179
+ });
180
+ }
181
+ return modules;
182
+ }
@@ -0,0 +1,57 @@
1
+ import * as RuntimeServices from "@distilled.cloud/cloudflare-runtime/RuntimeServices";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Layer from "effect/Layer";
4
+ import * as Path from "effect/Path";
5
+ import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
6
+ import * as AlchemyContext from "../../AlchemyContext.ts";
7
+ import { AuthProviders } from "../../Auth/AuthProvider.ts";
8
+ import * as RpcServer from "../../Sidecar/RpcServer.ts";
9
+ import {
10
+ httpServer,
11
+ PlatformServices,
12
+ runMain,
13
+ } from "../../Util/PlatformServices.ts";
14
+ import { CloudflareAuth } from "../Auth/AuthProvider.ts";
15
+ import * as CloudflareEnvironment from "../CloudflareEnvironment.ts";
16
+ import * as Credentials from "../Credentials.ts";
17
+ import { Sidecar, SidecarSchema } from "./Sidecar.ts";
18
+ import { SidecarHandlers } from "./SidecarHandlers.ts";
19
+
20
+ const apiServices = Layer.merge(
21
+ Credentials.fromAuthProvider(),
22
+ CloudflareEnvironment.fromProfile(),
23
+ ).pipe(
24
+ Layer.provide(CloudflareAuth),
25
+ Layer.provide(Layer.succeed(AuthProviders, {})),
26
+ );
27
+
28
+ const runtimeServices = SidecarHandlers.pipe(
29
+ Layer.provide(
30
+ Layer.unwrap(
31
+ Effect.gen(function* () {
32
+ const { accountId } =
33
+ yield* CloudflareEnvironment.CloudflareEnvironment;
34
+ const { dotAlchemy } = yield* AlchemyContext.AlchemyContext;
35
+ const path = yield* Path.Path;
36
+ return RuntimeServices.layer({
37
+ accountId,
38
+ storage: path.join(dotAlchemy, "local"),
39
+ });
40
+ }),
41
+ ),
42
+ ),
43
+ Layer.provide(
44
+ Layer.mergeAll(apiServices, FetchHttpClient.layer, httpServer()),
45
+ ),
46
+ );
47
+
48
+ const services = Layer.provideMerge(
49
+ Layer.provideMerge(runtimeServices, RpcServer.layerServices(import.meta.url)),
50
+ PlatformServices,
51
+ );
52
+
53
+ RpcServer.makeRpcServer(Sidecar.asEffect(), SidecarSchema).pipe(
54
+ Effect.provide(services),
55
+ Effect.scoped,
56
+ runMain,
57
+ );
@@ -49,12 +49,42 @@ export type Queue = Resource<
49
49
  * ```
50
50
  *
51
51
  * @section Binding to a Worker
52
+ * In an Effect-style Worker, use `Cloudflare.QueueBinding.bind` in
53
+ * the init phase and provide `Cloudflare.QueueBindingLive` in the
54
+ * runtime layer. The returned `QueueSender` exposes `send` and
55
+ * `sendBatch`.
56
+ *
52
57
  * @example Sending messages from a Worker
53
58
  * ```typescript
54
- * // In your Worker definition, add the queue to bindings:
55
- * const Worker = Cloudflare.Worker("Worker", {
56
- * bindings: { MY_QUEUE: queue },
57
- * });
59
+ * import * as Cloudflare from "alchemy/Cloudflare";
60
+ * import * as Effect from "effect/Effect";
61
+ * import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest";
62
+ * import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
63
+ *
64
+ * export const Queue = Cloudflare.Queue("Queue");
65
+ *
66
+ * export default Cloudflare.Worker(
67
+ * "Worker",
68
+ * { main: import.meta.path },
69
+ * Effect.gen(function* () {
70
+ * const queue = yield* Cloudflare.QueueBinding.bind(Queue);
71
+ *
72
+ * return {
73
+ * fetch: Effect.gen(function* () {
74
+ * const request = yield* HttpServerRequest;
75
+ * if (request.url === "/queue/send" && request.method === "POST") {
76
+ * const text = yield* request.text;
77
+ * yield* queue.send({ text, sentAt: Date.now() }).pipe(Effect.orDie);
78
+ * return yield* HttpServerResponse.json(
79
+ * { sent: { text } },
80
+ * { status: 202 },
81
+ * );
82
+ * }
83
+ * return HttpServerResponse.text("Not Found", { status: 404 });
84
+ * }),
85
+ * };
86
+ * }).pipe(Effect.provide(Cloudflare.QueueBindingLive)),
87
+ * );
58
88
  * ```
59
89
  */
60
90
  export const Queue = Resource<Queue>("Cloudflare.Queue")({
@@ -26,6 +26,39 @@ export class QueueSendError extends Data.TaggedError("QueueSendError")<{
26
26
  cause?: unknown;
27
27
  }> {}
28
28
 
29
+ /**
30
+ * Binding service that turns a {@link Queue} resource into a typed
31
+ * {@link QueueSender} you can call from a Worker's runtime Effect.
32
+ *
33
+ * @section Sending Messages
34
+ * Bind the queue in the Worker's init phase, then use `send` for
35
+ * single messages or `sendBatch` for many messages in one call.
36
+ * Messages can be any JSON-serializable value.
37
+ *
38
+ * @example Producer route
39
+ * ```typescript
40
+ * const queue = yield* Cloudflare.QueueBinding.bind(Queue);
41
+ *
42
+ * return {
43
+ * fetch: Effect.gen(function* () {
44
+ * yield* queue.send({ text: "hi", sentAt: Date.now() });
45
+ * return HttpServerResponse.empty({ status: 202 });
46
+ * }),
47
+ * };
48
+ * ```
49
+ *
50
+ * @example Sending a batch
51
+ * ```typescript
52
+ * yield* queue.sendBatch([
53
+ * { body: { event: "click", id: 1 } },
54
+ * { body: { event: "click", id: 2 } },
55
+ * { body: "raw text", contentType: "text" },
56
+ * ]);
57
+ * ```
58
+ *
59
+ * Provide {@link QueueBindingLive} in the worker's runtime layer to
60
+ * resolve the underlying Cloudflare queue at request time.
61
+ */
29
62
  export class QueueBinding extends Binding.Service<
30
63
  QueueBinding,
31
64
  (queue: Queue) => Effect.Effect<QueueSender>