alchemy 2.0.0-beta.34 → 2.0.0-beta.35
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.
- package/bin/cli.js +8 -7
- package/bin/exec.js +226 -52
- package/bin/exec.js.map +1 -1
- package/lib/AWS/AuthProvider.d.ts +7 -6
- package/lib/AWS/AuthProvider.d.ts.map +1 -1
- package/lib/AWS/AuthProvider.js +143 -134
- package/lib/AWS/AuthProvider.js.map +1 -1
- package/lib/AWS/Bootstrap.d.ts +2 -2
- package/lib/AWS/Bootstrap.d.ts.map +1 -1
- package/lib/AWS/EC2/hosted.d.ts +1 -1
- package/lib/AWS/Environment.d.ts +25 -3
- package/lib/AWS/Environment.d.ts.map +1 -1
- package/lib/AWS/Environment.js +47 -3
- package/lib/AWS/Environment.js.map +1 -1
- package/lib/AWS/IAM/OpenIDConnectProvider.d.ts +14 -6
- package/lib/AWS/IAM/OpenIDConnectProvider.d.ts.map +1 -1
- package/lib/AWS/IAM/OpenIDConnectProvider.js +6 -3
- package/lib/AWS/IAM/OpenIDConnectProvider.js.map +1 -1
- package/lib/AWS/IAM/Policy.d.ts +1 -1
- package/lib/AWS/IAM/Policy.d.ts.map +1 -1
- package/lib/AWS/IAM/Policy.js.map +1 -1
- package/lib/AWS/Lambda/BucketEventSource.d.ts +1 -1
- package/lib/AWS/Lambda/BucketEventSource.d.ts.map +1 -1
- package/lib/AWS/Lambda/EventBridgeEventSource.d.ts +1 -1
- package/lib/AWS/Lambda/EventBridgeEventSource.d.ts.map +1 -1
- package/lib/AWS/Lambda/QueueEventSource.d.ts +1 -1
- package/lib/AWS/Lambda/QueueEventSource.d.ts.map +1 -1
- package/lib/AWS/Lambda/StreamEventSource.d.ts +1 -1
- package/lib/AWS/Lambda/StreamEventSource.d.ts.map +1 -1
- package/lib/AWS/Lambda/TableEventSource.d.ts +1 -1
- package/lib/AWS/Lambda/TableEventSource.d.ts.map +1 -1
- package/lib/AWS/Lambda/TopicEventSource.d.ts +1 -1
- package/lib/AWS/Lambda/TopicEventSource.d.ts.map +1 -1
- package/lib/AWS/Providers.d.ts +1 -1
- package/lib/AWS/Providers.d.ts.map +1 -1
- package/lib/AWS/Providers.js +2 -1
- package/lib/AWS/Providers.js.map +1 -1
- package/lib/Auth/Credentials.d.ts +21 -6
- package/lib/Auth/Credentials.d.ts.map +1 -1
- package/lib/Auth/Credentials.js +30 -31
- package/lib/Auth/Credentials.js.map +1 -1
- package/lib/Auth/Profile.d.ts +31 -34
- package/lib/Auth/Profile.d.ts.map +1 -1
- package/lib/Auth/Profile.js +46 -31
- package/lib/Auth/Profile.js.map +1 -1
- package/lib/Axiom/AuthProvider.d.ts +2 -1
- package/lib/Axiom/AuthProvider.d.ts.map +1 -1
- package/lib/Axiom/AuthProvider.js +149 -143
- package/lib/Axiom/AuthProvider.js.map +1 -1
- package/lib/Axiom/Credentials.d.ts +2 -1
- package/lib/Axiom/Credentials.d.ts.map +1 -1
- package/lib/Axiom/Credentials.js +4 -4
- package/lib/Axiom/Credentials.js.map +1 -1
- package/lib/Axiom/Providers.d.ts +1 -1
- package/lib/Axiom/Providers.d.ts.map +1 -1
- package/lib/Axiom/Providers.js +3 -1
- package/lib/Axiom/Providers.js.map +1 -1
- package/lib/Cli/LoggingCli.d.ts.map +1 -1
- package/lib/Cli/LoggingCli.js +105 -7
- package/lib/Cli/LoggingCli.js.map +1 -1
- package/lib/Cli/checkVersion.d.ts +13 -0
- package/lib/Cli/checkVersion.d.ts.map +1 -0
- package/lib/Cli/checkVersion.js +47 -0
- package/lib/Cli/checkVersion.js.map +1 -0
- package/lib/Cli/commands/_shared.d.ts +2 -2
- package/lib/Cli/commands/_shared.d.ts.map +1 -1
- package/lib/Cli/commands/_shared.js +3 -3
- package/lib/Cli/commands/_shared.js.map +1 -1
- package/lib/Cli/commands/aws.d.ts +6 -0
- package/lib/Cli/commands/aws.d.ts.map +1 -0
- package/lib/Cli/commands/aws.js +63 -0
- package/lib/Cli/commands/aws.js.map +1 -0
- package/lib/Cli/commands/cloudflare.d.ts +17 -0
- package/lib/Cli/commands/cloudflare.d.ts.map +1 -0
- package/lib/Cli/commands/cloudflare.js +111 -0
- package/lib/Cli/commands/cloudflare.js.map +1 -0
- package/lib/Cli/commands/deploy.d.ts +6 -5
- package/lib/Cli/commands/deploy.d.ts.map +1 -1
- package/lib/Cli/commands/deploy.js +10 -10
- package/lib/Cli/commands/deploy.js.map +1 -1
- package/lib/Cli/commands/dev.d.ts.map +1 -1
- package/lib/Cli/commands/dev.js +16 -16
- package/lib/Cli/commands/dev.js.map +1 -1
- package/lib/Cli/commands/login.d.ts +2 -1
- package/lib/Cli/commands/login.d.ts.map +1 -1
- package/lib/Cli/commands/login.js +9 -8
- package/lib/Cli/commands/login.js.map +1 -1
- package/lib/Cli/commands/logs.d.ts +1 -1
- package/lib/Cli/commands/logs.d.ts.map +1 -1
- package/lib/Cli/commands/logs.js +7 -7
- package/lib/Cli/commands/logs.js.map +1 -1
- package/lib/Cli/commands/profile.d.ts +3 -1
- package/lib/Cli/commands/profile.d.ts.map +1 -1
- package/lib/Cli/commands/profile.js +14 -11
- package/lib/Cli/commands/profile.js.map +1 -1
- package/lib/Cli/commands/state.d.ts +1 -1
- package/lib/Cli/commands/state.d.ts.map +1 -1
- package/lib/Cli/commands/state.js +8 -8
- package/lib/Cli/commands/state.js.map +1 -1
- package/lib/Cli/commands/tail.d.ts +1 -1
- package/lib/Cli/commands/tail.d.ts.map +1 -1
- package/lib/Cli/commands/tail.js +7 -7
- package/lib/Cli/commands/tail.js.map +1 -1
- package/lib/Cli/exec.d.ts.map +1 -1
- package/lib/Cli/exec.js +5 -3
- package/lib/Cli/exec.js.map +1 -1
- package/lib/Cli/main.d.ts.map +1 -1
- package/lib/Cli/main.js +17 -5
- package/lib/Cli/main.js.map +1 -1
- package/lib/Cli/selectCli.d.ts +12 -0
- package/lib/Cli/selectCli.d.ts.map +1 -0
- package/lib/Cli/selectCli.js +32 -0
- package/lib/Cli/selectCli.js.map +1 -0
- package/lib/Cloudflare/Auth/AuthProvider.d.ts +2 -1
- package/lib/Cloudflare/Auth/AuthProvider.d.ts.map +1 -1
- package/lib/Cloudflare/Auth/AuthProvider.js +235 -214
- package/lib/Cloudflare/Auth/AuthProvider.js.map +1 -1
- package/lib/Cloudflare/CloudflareEnvironment.d.ts +2 -1
- package/lib/Cloudflare/CloudflareEnvironment.d.ts.map +1 -1
- package/lib/Cloudflare/CloudflareEnvironment.js +3 -2
- package/lib/Cloudflare/CloudflareEnvironment.js.map +1 -1
- package/lib/Cloudflare/Credentials.d.ts +2 -1
- package/lib/Cloudflare/Credentials.d.ts.map +1 -1
- package/lib/Cloudflare/Credentials.js +4 -4
- package/lib/Cloudflare/Credentials.js.map +1 -1
- package/lib/Cloudflare/D1/D1Clone.d.ts +2 -1
- package/lib/Cloudflare/D1/D1Clone.d.ts.map +1 -1
- package/lib/Cloudflare/D1/D1Database.d.ts +2 -1
- package/lib/Cloudflare/D1/D1Database.d.ts.map +1 -1
- package/lib/Cloudflare/D1/D1Import.d.ts +3 -1
- package/lib/Cloudflare/D1/D1Import.d.ts.map +1 -1
- package/lib/Cloudflare/Local/Sidecar.d.ts +11 -4
- package/lib/Cloudflare/Local/Sidecar.d.ts.map +1 -1
- package/lib/Cloudflare/Local/Sidecar.js +8 -6
- package/lib/Cloudflare/Local/Sidecar.js.map +1 -1
- package/lib/Cloudflare/Local/SidecarHandlers.d.ts.map +1 -1
- package/lib/Cloudflare/Local/SidecarHandlers.js +1 -5
- package/lib/Cloudflare/Local/SidecarHandlers.js.map +1 -1
- package/lib/Cloudflare/Local/SidecarServer.js +5 -2
- package/lib/Cloudflare/Local/SidecarServer.js.map +1 -1
- package/lib/Cloudflare/Logs.d.ts +5 -0
- package/lib/Cloudflare/Logs.d.ts.map +1 -1
- package/lib/Cloudflare/Logs.js +70 -1
- package/lib/Cloudflare/Logs.js.map +1 -1
- package/lib/Cloudflare/Providers.d.ts +1 -1
- package/lib/Cloudflare/Providers.d.ts.map +1 -1
- package/lib/Cloudflare/Providers.js +4 -1
- package/lib/Cloudflare/Providers.js.map +1 -1
- package/lib/Cloudflare/Queue/Queue.d.ts +1 -1
- package/lib/Cloudflare/Queue/Queue.js +1 -1
- package/lib/Cloudflare/Queue/QueueConsumer.d.ts +6 -2
- package/lib/Cloudflare/Queue/QueueConsumer.d.ts.map +1 -1
- package/lib/Cloudflare/Queue/QueueConsumer.js +194 -53
- package/lib/Cloudflare/Queue/QueueConsumer.js.map +1 -1
- package/lib/Cloudflare/Queue/QueueEventSource.d.ts +106 -0
- package/lib/Cloudflare/Queue/QueueEventSource.d.ts.map +1 -0
- package/lib/Cloudflare/Queue/QueueEventSource.js +145 -0
- package/lib/Cloudflare/Queue/QueueEventSource.js.map +1 -0
- package/lib/Cloudflare/Queue/index.d.ts +1 -0
- package/lib/Cloudflare/Queue/index.d.ts.map +1 -1
- package/lib/Cloudflare/Queue/index.js +1 -0
- package/lib/Cloudflare/Queue/index.js.map +1 -1
- package/lib/Cloudflare/R2/R2Bucket.d.ts +94 -0
- package/lib/Cloudflare/R2/R2Bucket.d.ts.map +1 -1
- package/lib/Cloudflare/R2/R2Bucket.js +186 -2
- package/lib/Cloudflare/R2/R2Bucket.js.map +1 -1
- package/lib/Cloudflare/StateStore/State.d.ts +4 -3
- package/lib/Cloudflare/StateStore/State.d.ts.map +1 -1
- package/lib/Cloudflare/StateStore/State.js +16 -8
- package/lib/Cloudflare/StateStore/State.js.map +1 -1
- package/lib/Cloudflare/VpcService/VpcServiceRef.d.ts +1 -1
- package/lib/Cloudflare/VpcService/VpcServiceRef.d.ts.map +1 -1
- package/lib/Cloudflare/Website/StaticSite.d.ts.map +1 -1
- package/lib/Cloudflare/Website/StaticSite.js +10 -1
- package/lib/Cloudflare/Website/StaticSite.js.map +1 -1
- package/lib/Cloudflare/Workers/Assets.d.ts +1 -1
- package/lib/Cloudflare/Workers/Assets.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/LocalWorkerProvider.js +11 -1
- package/lib/Cloudflare/Workers/LocalWorkerProvider.js.map +1 -1
- package/lib/Cloudflare/Workers/Worker.d.ts +9 -7
- package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Worker.js +62 -142
- package/lib/Cloudflare/Workers/Worker.js.map +1 -1
- package/lib/Cloudflare/Workers/WorkerBundle.d.ts +2 -0
- package/lib/Cloudflare/Workers/WorkerBundle.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/WorkerBundle.js +21 -7
- package/lib/Cloudflare/Workers/WorkerBundle.js.map +1 -1
- package/lib/Cloudflare/Zone.d.ts +14 -0
- package/lib/Cloudflare/Zone.d.ts.map +1 -0
- package/lib/Cloudflare/Zone.js +44 -0
- package/lib/Cloudflare/Zone.js.map +1 -0
- package/lib/Drizzle/Schema.d.ts.map +1 -1
- package/lib/Drizzle/Schema.js +20 -5
- package/lib/Drizzle/Schema.js.map +1 -1
- package/lib/GitHub/AuthProvider.d.ts +3 -1
- package/lib/GitHub/AuthProvider.d.ts.map +1 -1
- package/lib/GitHub/AuthProvider.js +114 -108
- package/lib/GitHub/AuthProvider.js.map +1 -1
- package/lib/GitHub/Credentials.d.ts +2 -1
- package/lib/GitHub/Credentials.d.ts.map +1 -1
- package/lib/GitHub/Credentials.js +4 -4
- package/lib/GitHub/Credentials.js.map +1 -1
- package/lib/GitHub/Providers.d.ts +1 -1
- package/lib/GitHub/Providers.d.ts.map +1 -1
- package/lib/GitHub/Providers.js +3 -1
- package/lib/GitHub/Providers.js.map +1 -1
- package/lib/GitHub/Secrets.d.ts.map +1 -1
- package/lib/GitHub/Secrets.js +14 -1
- package/lib/GitHub/Secrets.js.map +1 -1
- package/lib/GitHub/Variables.d.ts +40 -0
- package/lib/GitHub/Variables.d.ts.map +1 -0
- package/lib/GitHub/Variables.js +28 -0
- package/lib/GitHub/Variables.js.map +1 -0
- package/lib/GitHub/index.d.ts +1 -0
- package/lib/GitHub/index.d.ts.map +1 -1
- package/lib/GitHub/index.js +1 -0
- package/lib/GitHub/index.js.map +1 -1
- package/lib/Neon/AuthProvider.d.ts +3 -1
- package/lib/Neon/AuthProvider.d.ts.map +1 -1
- package/lib/Neon/AuthProvider.js +87 -80
- package/lib/Neon/AuthProvider.js.map +1 -1
- package/lib/Neon/Credentials.d.ts +2 -1
- package/lib/Neon/Credentials.d.ts.map +1 -1
- package/lib/Neon/Credentials.js +4 -4
- package/lib/Neon/Credentials.js.map +1 -1
- package/lib/Neon/NeonEnvironment.d.ts +2 -1
- package/lib/Neon/NeonEnvironment.d.ts.map +1 -1
- package/lib/Neon/NeonEnvironment.js +3 -2
- package/lib/Neon/NeonEnvironment.js.map +1 -1
- package/lib/Neon/Project.d.ts +16 -0
- package/lib/Neon/Project.d.ts.map +1 -1
- package/lib/Neon/Project.js +25 -0
- package/lib/Neon/Project.js.map +1 -1
- package/lib/Neon/Providers.d.ts +1 -1
- package/lib/Neon/Providers.d.ts.map +1 -1
- package/lib/Neon/Providers.js +3 -1
- package/lib/Neon/Providers.js.map +1 -1
- package/lib/Neon/api.d.ts +1 -0
- package/lib/Neon/api.d.ts.map +1 -1
- package/lib/Neon/api.js +1 -0
- package/lib/Neon/api.js.map +1 -1
- package/lib/Sidecar/RpcClient.d.ts.map +1 -1
- package/lib/Sidecar/RpcClient.js +8 -5
- package/lib/Sidecar/RpcClient.js.map +1 -1
- package/lib/Sidecar/RpcPaths.d.ts +5 -0
- package/lib/Sidecar/RpcPaths.d.ts.map +1 -1
- package/lib/Sidecar/RpcPaths.js +10 -1
- package/lib/Sidecar/RpcPaths.js.map +1 -1
- package/lib/Sidecar/RpcServer.d.ts +24 -4
- package/lib/Sidecar/RpcServer.d.ts.map +1 -1
- package/lib/Sidecar/RpcServer.js +70 -15
- package/lib/Sidecar/RpcServer.js.map +1 -1
- package/lib/Sidecar/RpcServerBun.d.ts +4 -0
- package/lib/Sidecar/RpcServerBun.d.ts.map +1 -0
- package/lib/Sidecar/RpcServerBun.js +28 -0
- package/lib/Sidecar/RpcServerBun.js.map +1 -0
- package/lib/Sidecar/RpcServerNode.d.ts +4 -0
- package/lib/Sidecar/RpcServerNode.d.ts.map +1 -0
- package/lib/Sidecar/RpcServerNode.js +39 -0
- package/lib/Sidecar/RpcServerNode.js.map +1 -0
- package/lib/Sidecar/RpcServices.d.ts +6 -0
- package/lib/Sidecar/RpcServices.d.ts.map +1 -0
- package/lib/Sidecar/RpcServices.js +29 -0
- package/lib/Sidecar/RpcServices.js.map +1 -0
- package/lib/Stack.d.ts +5 -3
- package/lib/Stack.d.ts.map +1 -1
- package/lib/Stack.js +3 -1
- package/lib/Stack.js.map +1 -1
- package/lib/Test/Core.d.ts.map +1 -1
- package/lib/Test/Core.js +5 -4
- package/lib/Test/Core.js.map +1 -1
- package/lib/Util/PlatformServices.js +1 -1
- package/lib/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +15 -12
- package/src/AWS/AuthProvider.ts +269 -259
- package/src/AWS/Environment.ts +57 -3
- package/src/AWS/IAM/OpenIDConnectProvider.ts +19 -8
- package/src/AWS/IAM/Policy.ts +1 -1
- package/src/AWS/Providers.ts +2 -0
- package/src/Auth/Credentials.ts +82 -46
- package/src/Auth/Profile.ts +123 -69
- package/src/Axiom/AuthProvider.ts +263 -255
- package/src/Axiom/Credentials.ts +3 -4
- package/src/Axiom/Providers.ts +4 -0
- package/src/Cli/LoggingCli.ts +127 -11
- package/src/Cli/checkVersion.ts +57 -0
- package/src/Cli/commands/_shared.ts +4 -4
- package/src/Cli/commands/aws.ts +127 -0
- package/src/Cli/commands/cloudflare.ts +222 -0
- package/src/Cli/commands/deploy.ts +11 -10
- package/src/Cli/commands/dev.ts +28 -33
- package/src/Cli/commands/login.ts +9 -12
- package/src/Cli/commands/logs.ts +8 -8
- package/src/Cli/commands/profile.ts +14 -16
- package/src/Cli/commands/state.ts +8 -8
- package/src/Cli/commands/tail.ts +8 -8
- package/src/Cli/exec.ts +7 -3
- package/src/Cli/main.ts +20 -5
- package/src/Cli/selectCli.ts +33 -0
- package/src/Cloudflare/Auth/AuthProvider.ts +445 -430
- package/src/Cloudflare/CloudflareEnvironment.ts +3 -2
- package/src/Cloudflare/Credentials.ts +3 -4
- package/src/Cloudflare/Local/Sidecar.ts +13 -10
- package/src/Cloudflare/Local/SidecarHandlers.ts +8 -13
- package/src/Cloudflare/Local/SidecarServer.ts +6 -1
- package/src/Cloudflare/Logs.ts +115 -1
- package/src/Cloudflare/Providers.ts +6 -0
- package/src/Cloudflare/Queue/Queue.ts +1 -1
- package/src/Cloudflare/Queue/QueueConsumer.ts +262 -60
- package/src/Cloudflare/Queue/QueueEventSource.ts +234 -0
- package/src/Cloudflare/Queue/index.ts +1 -0
- package/src/Cloudflare/R2/R2Bucket.ts +337 -2
- package/src/Cloudflare/StateStore/State.ts +25 -16
- package/src/Cloudflare/Website/StaticSite.ts +11 -1
- package/src/Cloudflare/Workers/LocalWorkerProvider.ts +9 -1
- package/src/Cloudflare/Workers/Worker.ts +83 -203
- package/src/Cloudflare/Workers/WorkerBundle.ts +38 -12
- package/src/Cloudflare/Zone.ts +88 -0
- package/src/Drizzle/Schema.ts +21 -5
- package/src/GitHub/AuthProvider.ts +221 -217
- package/src/GitHub/Credentials.ts +3 -4
- package/src/GitHub/Providers.ts +4 -0
- package/src/GitHub/Secrets.ts +25 -1
- package/src/GitHub/Variables.ts +54 -0
- package/src/GitHub/index.ts +1 -0
- package/src/Neon/AuthProvider.ts +160 -154
- package/src/Neon/Credentials.ts +3 -4
- package/src/Neon/NeonEnvironment.ts +3 -2
- package/src/Neon/Project.ts +42 -0
- package/src/Neon/Providers.ts +4 -0
- package/src/Neon/api.ts +2 -0
- package/src/Sidecar/RpcClient.ts +8 -7
- package/src/Sidecar/RpcPaths.ts +11 -1
- package/src/Sidecar/RpcServer.ts +97 -24
- package/src/Sidecar/RpcServerBun.ts +41 -0
- package/src/Sidecar/RpcServerNode.ts +51 -0
- package/src/Sidecar/RpcServices.ts +48 -0
- package/src/Stack.ts +8 -0
- package/src/Test/Core.ts +10 -4
- package/src/Util/PlatformServices.ts +1 -1
- package/lib/Cli/commands/bootstrap.d.ts +0 -20
- package/lib/Cli/commands/bootstrap.d.ts.map +0 -1
- package/lib/Cli/commands/bootstrap.js +0 -107
- package/lib/Cli/commands/bootstrap.js.map +0 -1
- package/lib/Sidecar/RpcTransport.d.ts +0 -22
- package/lib/Sidecar/RpcTransport.d.ts.map +0 -1
- package/lib/Sidecar/RpcTransport.js +0 -81
- package/lib/Sidecar/RpcTransport.js.map +0 -1
- package/lib/Test/TestCli.d.ts +0 -19
- package/lib/Test/TestCli.d.ts.map +0 -1
- package/lib/Test/TestCli.js +0 -175
- package/lib/Test/TestCli.js.map +0 -1
- package/src/Cli/commands/bootstrap.ts +0 -216
- package/src/Sidecar/RpcTransport.ts +0 -85
- package/src/Test/TestCli.ts +0 -221
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import * as queues from "@distilled.cloud/cloudflare/queues";
|
|
2
2
|
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Option from "effect/Option";
|
|
4
|
+
import * as Schedule from "effect/Schedule";
|
|
5
|
+
import * as Stream from "effect/Stream";
|
|
3
6
|
import { isResolved } from "../../Diff.ts";
|
|
4
7
|
import * as Provider from "../../Provider.ts";
|
|
5
8
|
import { Resource } from "../../Resource.ts";
|
|
6
9
|
import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
|
|
7
10
|
import type { Providers } from "../Providers.ts";
|
|
8
11
|
|
|
12
|
+
// ~60s budget — Worker reconcile uploads typically land in 2–10s,
|
|
13
|
+
// but a fresh container/asset deploy can stretch that.
|
|
14
|
+
const queueHandlerReadinessSchedule = Schedule.spaced("2 seconds").pipe(
|
|
15
|
+
Schedule.both(Schedule.recurs(30)),
|
|
16
|
+
);
|
|
17
|
+
|
|
9
18
|
export type QueueConsumerProps = {
|
|
10
19
|
/**
|
|
11
20
|
* The queue ID to attach the consumer to.
|
|
@@ -68,6 +77,11 @@ export type QueueConsumer = Resource<
|
|
|
68
77
|
* Register a Worker as a consumer of a Queue. The Worker's `queue()`
|
|
69
78
|
* handler will be invoked with batches of messages.
|
|
70
79
|
*
|
|
80
|
+
* Cloudflare allows at most one Worker consumer per queue (HTTP-pull
|
|
81
|
+
* consumers can coexist). The reconciler enforces this: if the queue
|
|
82
|
+
* already has a Worker consumer pointing at a different script, the
|
|
83
|
+
* deploy fails with a clear error rather than silently adopting it.
|
|
84
|
+
*
|
|
71
85
|
* @section Registering a Consumer
|
|
72
86
|
* @example Basic consumer
|
|
73
87
|
* ```typescript
|
|
@@ -97,6 +111,20 @@ export const QueueConsumer = Resource<QueueConsumer>(
|
|
|
97
111
|
"Cloudflare.QueueConsumer",
|
|
98
112
|
);
|
|
99
113
|
|
|
114
|
+
type ObservedConsumer = {
|
|
115
|
+
consumerId: string;
|
|
116
|
+
script: string | undefined;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const toObserved = (c: {
|
|
120
|
+
consumerId?: string | null;
|
|
121
|
+
script?: string | null;
|
|
122
|
+
type?: "worker" | "http_pull" | null;
|
|
123
|
+
}): ObservedConsumer | undefined =>
|
|
124
|
+
c.consumerId && c.type === "worker"
|
|
125
|
+
? { consumerId: c.consumerId, script: c.script ?? undefined }
|
|
126
|
+
: undefined;
|
|
127
|
+
|
|
100
128
|
export const QueueConsumerProvider = () =>
|
|
101
129
|
Provider.effect(
|
|
102
130
|
QueueConsumer,
|
|
@@ -106,7 +134,21 @@ export const QueueConsumerProvider = () =>
|
|
|
106
134
|
const getConsumer = yield* queues.getConsumer;
|
|
107
135
|
const updateConsumer = yield* queues.updateConsumer;
|
|
108
136
|
const deleteConsumer = yield* queues.deleteConsumer;
|
|
109
|
-
|
|
137
|
+
|
|
138
|
+
// Cloudflare allows a single Worker consumer per queue, so the
|
|
139
|
+
// first match in the paginated stream is the only one. Using
|
|
140
|
+
// `.items` defeats single-page lookups that would otherwise
|
|
141
|
+
// miss late-arriving consumers under eventual consistency.
|
|
142
|
+
const findWorkerConsumer = (
|
|
143
|
+
acct: string,
|
|
144
|
+
queueId: string,
|
|
145
|
+
): Effect.Effect<ObservedConsumer | undefined, any, any> =>
|
|
146
|
+
queues.listConsumers.items({ accountId: acct, queueId }).pipe(
|
|
147
|
+
Stream.map(toObserved),
|
|
148
|
+
Stream.filter((c): c is ObservedConsumer => c !== undefined),
|
|
149
|
+
Stream.runHead,
|
|
150
|
+
Effect.map(Option.getOrUndefined),
|
|
151
|
+
);
|
|
110
152
|
|
|
111
153
|
return {
|
|
112
154
|
stables: ["consumerId", "accountId"],
|
|
@@ -115,18 +157,29 @@ export const QueueConsumerProvider = () =>
|
|
|
115
157
|
if ((output?.accountId ?? accountId) !== accountId) {
|
|
116
158
|
return { action: "replace" } as const;
|
|
117
159
|
}
|
|
118
|
-
// Queue change requires replacement
|
|
160
|
+
// Queue change requires replacement — consumerId is bound
|
|
161
|
+
// to a queue and the API has no "move consumer" verb.
|
|
119
162
|
if (output?.queueId && news.queueId !== output.queueId) {
|
|
120
|
-
return { action: "replace" } as const;
|
|
121
|
-
}
|
|
122
|
-
// Script change requires replacement
|
|
123
|
-
if (output?.scriptName && news.scriptName !== output.scriptName) {
|
|
124
|
-
return { action: "replace" } as const;
|
|
163
|
+
return { action: "replace", deleteFirst: true } as const;
|
|
125
164
|
}
|
|
126
|
-
// Settings
|
|
165
|
+
// Settings / DLQ / script drift is an update. We DON'T
|
|
166
|
+
// escalate scriptName changes to `replace` because the
|
|
167
|
+
// engine resolves cross-resource Output<string> refs (a
|
|
168
|
+
// sibling Worker's `workerName`) lazily — when the upstream
|
|
169
|
+
// Worker is created in the same plan, `news` is partially
|
|
170
|
+
// unresolved at diff time and `isResolved(news)` short-
|
|
171
|
+
// circuits up top. Falling through to "update" lets the
|
|
172
|
+
// engine call reconcile with fully-resolved `news`, where
|
|
173
|
+
// we detect script drift and rebuild the consumer in
|
|
174
|
+
// place (Cloudflare's PUT silently ignores `script_name`
|
|
175
|
+
// changes, so reconcile does delete-then-create).
|
|
127
176
|
if (
|
|
128
177
|
JSON.stringify(olds.settings ?? {}) !==
|
|
129
|
-
|
|
178
|
+
JSON.stringify(news.settings ?? {}) ||
|
|
179
|
+
(olds.deadLetterQueue ?? undefined) !==
|
|
180
|
+
(news.deadLetterQueue ?? undefined) ||
|
|
181
|
+
(output?.scriptName !== undefined &&
|
|
182
|
+
news.scriptName !== output.scriptName)
|
|
130
183
|
) {
|
|
131
184
|
return { action: "update" } as const;
|
|
132
185
|
}
|
|
@@ -136,33 +189,101 @@ export const QueueConsumerProvider = () =>
|
|
|
136
189
|
const queueId =
|
|
137
190
|
output?.queueId ?? (news.queueId as unknown as string);
|
|
138
191
|
|
|
139
|
-
// Observe —
|
|
140
|
-
//
|
|
141
|
-
// deletes or partial state-persistence failures
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
192
|
+
// Observe — prefer the cached consumerId, then fall back to
|
|
193
|
+
// listConsumers (paginated) to recover from out-of-band
|
|
194
|
+
// deletes or partial state-persistence failures. Track
|
|
195
|
+
// whether the observation came from the cached id or the
|
|
196
|
+
// list scan: a different-script worker consumer found via
|
|
197
|
+
// the list scan is potentially foreign (state was lost,
|
|
198
|
+
// someone else attached the consumer), and silently
|
|
199
|
+
// updating it could clobber another team's wiring.
|
|
200
|
+
let observed: ObservedConsumer | undefined;
|
|
201
|
+
let owned = false;
|
|
146
202
|
if (output?.consumerId) {
|
|
147
|
-
|
|
203
|
+
const fetched = yield* getConsumer({
|
|
148
204
|
accountId: acct,
|
|
149
|
-
queueId
|
|
205
|
+
queueId,
|
|
150
206
|
consumerId: output.consumerId,
|
|
151
|
-
}).pipe(
|
|
207
|
+
}).pipe(
|
|
208
|
+
Effect.catchTag("ConsumerNotFound", () =>
|
|
209
|
+
Effect.succeed(undefined),
|
|
210
|
+
),
|
|
211
|
+
);
|
|
212
|
+
if (fetched) {
|
|
213
|
+
observed = toObserved(fetched);
|
|
214
|
+
owned = observed !== undefined;
|
|
215
|
+
}
|
|
152
216
|
}
|
|
153
217
|
if (!observed) {
|
|
154
|
-
|
|
218
|
+
observed = yield* findWorkerConsumer(acct, queueId);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Owned consumer pointing at a different script: rebuild
|
|
222
|
+
// it in place. Cloudflare's PUT consumer silently ignores
|
|
223
|
+
// `script_name` changes on existing consumers (the live
|
|
224
|
+
// record stays pinned to the original worker), and the
|
|
225
|
+
// platform allows only one Worker consumer per queue, so
|
|
226
|
+
// the only path to re-point is delete-then-create.
|
|
227
|
+
if (
|
|
228
|
+
owned &&
|
|
229
|
+
observed &&
|
|
230
|
+
observed.script !== undefined &&
|
|
231
|
+
observed.script !== news.scriptName
|
|
232
|
+
) {
|
|
233
|
+
yield* deleteConsumer({
|
|
155
234
|
accountId: acct,
|
|
156
235
|
queueId,
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
236
|
+
consumerId: observed.consumerId,
|
|
237
|
+
}).pipe(Effect.catchTag("ConsumerNotFound", () => Effect.void));
|
|
238
|
+
// Wait for Cloudflare's worker subsystem to drop its
|
|
239
|
+
// claim on the old script so createConsumer below
|
|
240
|
+
// doesn't race the queue↔script propagation lag.
|
|
241
|
+
yield* getConsumer({
|
|
242
|
+
accountId: acct,
|
|
243
|
+
queueId,
|
|
244
|
+
consumerId: observed.consumerId,
|
|
245
|
+
}).pipe(
|
|
246
|
+
Effect.flatMap(() => Effect.fail("still-attached" as const)),
|
|
247
|
+
Effect.catchTag("ConsumerNotFound", () => Effect.void),
|
|
248
|
+
Effect.retry({
|
|
249
|
+
while: (e) => e === "still-attached",
|
|
250
|
+
schedule: Schedule.spaced("1 second").pipe(
|
|
251
|
+
Schedule.both(Schedule.recurs(30)),
|
|
252
|
+
),
|
|
253
|
+
}),
|
|
254
|
+
Effect.ignore,
|
|
160
255
|
);
|
|
256
|
+
observed = undefined;
|
|
257
|
+
owned = false;
|
|
161
258
|
}
|
|
162
259
|
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
260
|
+
// Refuse to take over a foreign consumer on the state-loss
|
|
261
|
+
// path. With `owned=false` we found this via the list scan
|
|
262
|
+
// and the script mismatch means it belongs to another
|
|
263
|
+
// resource or was created out-of-band — silent adoption
|
|
264
|
+
// would clobber that.
|
|
265
|
+
if (
|
|
266
|
+
observed &&
|
|
267
|
+
!owned &&
|
|
268
|
+
observed.script !== undefined &&
|
|
269
|
+
observed.script !== news.scriptName
|
|
270
|
+
) {
|
|
271
|
+
return yield* Effect.die(
|
|
272
|
+
`Cloudflare queue "${queueId}" already has a worker ` +
|
|
273
|
+
`consumer for script "${observed.script}", but this ` +
|
|
274
|
+
`resource is configured for "${news.scriptName}" and ` +
|
|
275
|
+
`local state for the consumer was missing. Each queue ` +
|
|
276
|
+
`can have only one worker consumer — delete the ` +
|
|
277
|
+
`existing one, update scriptName to match, or restore ` +
|
|
278
|
+
`the consumer's state entry before redeploying.`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Ensure — create if missing. ConsumerAlreadyExists is the
|
|
283
|
+
// race signal: another reconcile or peer beat us to it.
|
|
284
|
+
// Re-run the lookup; the paginated stream tolerates the
|
|
285
|
+
// single-page eventual-consistency window the previous
|
|
286
|
+
// implementation missed.
|
|
166
287
|
let consumerId: string;
|
|
167
288
|
if (!observed) {
|
|
168
289
|
const created = yield* createConsumer({
|
|
@@ -173,41 +294,79 @@ export const QueueConsumerProvider = () =>
|
|
|
173
294
|
deadLetterQueue: news.deadLetterQueue,
|
|
174
295
|
settings: news.settings,
|
|
175
296
|
}).pipe(
|
|
176
|
-
|
|
297
|
+
// The sibling Worker resource pre-creates a placeholder
|
|
298
|
+
// script with no `queue` handler; Cloudflare returns
|
|
299
|
+
// code 11001 until the real reconcile uploads the
|
|
300
|
+
// handler. Retry until the upload propagates (capped),
|
|
301
|
+
// then surface a real failure if it never does.
|
|
302
|
+
Effect.tapError((e) =>
|
|
303
|
+
e._tag === "QueueHandlerMissing"
|
|
304
|
+
? Effect.logDebug(
|
|
305
|
+
`QueueConsumer create: worker ` +
|
|
306
|
+
`"${news.scriptName}" has no queue handler ` +
|
|
307
|
+
`yet (code 11001), retrying`,
|
|
308
|
+
)
|
|
309
|
+
: Effect.void,
|
|
310
|
+
),
|
|
311
|
+
Effect.retry({
|
|
312
|
+
while: (e) => e._tag === "QueueHandlerMissing",
|
|
313
|
+
schedule: queueHandlerReadinessSchedule,
|
|
314
|
+
}),
|
|
315
|
+
Effect.catchTag("ConsumerAlreadyExists", (cause) =>
|
|
177
316
|
Effect.gen(function* () {
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
317
|
+
const match = yield* findWorkerConsumer(acct, queueId);
|
|
318
|
+
if (!match) {
|
|
319
|
+
return yield* Effect.die(
|
|
320
|
+
`Cloudflare reported a worker consumer already ` +
|
|
321
|
+
`exists on queue "${queueId}", but listConsumers ` +
|
|
322
|
+
`returned none. Retry the deploy; if this ` +
|
|
323
|
+
`persists, the queue is in an inconsistent ` +
|
|
324
|
+
`state. Underlying error: ${cause.message}`,
|
|
325
|
+
);
|
|
187
326
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
327
|
+
if (
|
|
328
|
+
match.script !== undefined &&
|
|
329
|
+
match.script !== news.scriptName
|
|
330
|
+
) {
|
|
331
|
+
return yield* Effect.die(
|
|
332
|
+
`Cloudflare queue "${queueId}" already has a ` +
|
|
333
|
+
`worker consumer for script "${match.script}", ` +
|
|
334
|
+
`but this resource is configured for ` +
|
|
335
|
+
`"${news.scriptName}". Each queue can have only ` +
|
|
336
|
+
`one worker consumer — delete the existing one ` +
|
|
337
|
+
`or update scriptName to match before redeploying.`,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
return match;
|
|
191
341
|
}),
|
|
192
342
|
),
|
|
193
343
|
);
|
|
194
344
|
consumerId = created.consumerId!;
|
|
195
345
|
} else {
|
|
196
|
-
consumerId = observed.consumerId
|
|
197
|
-
// Sync — update settings and dead-letter target on the
|
|
198
|
-
// existing consumer. The Cloudflare API replaces all mutable
|
|
199
|
-
// fields per call, so always issue this so adoption converges.
|
|
200
|
-
yield* updateConsumer({
|
|
201
|
-
accountId: acct,
|
|
202
|
-
queueId,
|
|
203
|
-
consumerId,
|
|
204
|
-
scriptName: news.scriptName,
|
|
205
|
-
type: "worker",
|
|
206
|
-
settings: news.settings,
|
|
207
|
-
deadLetterQueue: news.deadLetterQueue,
|
|
208
|
-
});
|
|
346
|
+
consumerId = observed.consumerId;
|
|
209
347
|
}
|
|
210
348
|
|
|
349
|
+
// Sync — Cloudflare replaces all mutable fields on
|
|
350
|
+
// updateConsumer, so always issue this so adoption converges
|
|
351
|
+
// and settings drift gets corrected on every reconcile.
|
|
352
|
+
// updateConsumer hits the same "queue handler missing" race
|
|
353
|
+
// window as create when the worker is mid-upload, so apply
|
|
354
|
+
// the same bounded retry.
|
|
355
|
+
yield* updateConsumer({
|
|
356
|
+
accountId: acct,
|
|
357
|
+
queueId,
|
|
358
|
+
consumerId,
|
|
359
|
+
scriptName: news.scriptName,
|
|
360
|
+
type: "worker",
|
|
361
|
+
settings: news.settings,
|
|
362
|
+
deadLetterQueue: news.deadLetterQueue,
|
|
363
|
+
}).pipe(
|
|
364
|
+
Effect.retry({
|
|
365
|
+
while: (e) => e._tag === "QueueHandlerMissing",
|
|
366
|
+
schedule: queueHandlerReadinessSchedule,
|
|
367
|
+
}),
|
|
368
|
+
);
|
|
369
|
+
|
|
211
370
|
return {
|
|
212
371
|
consumerId,
|
|
213
372
|
queueId,
|
|
@@ -220,26 +379,69 @@ export const QueueConsumerProvider = () =>
|
|
|
220
379
|
accountId: output.accountId,
|
|
221
380
|
queueId: output.queueId,
|
|
222
381
|
consumerId: output.consumerId,
|
|
223
|
-
}).pipe(Effect.
|
|
382
|
+
}).pipe(Effect.catchTag("ConsumerNotFound", () => Effect.void));
|
|
383
|
+
|
|
384
|
+
// Block until Cloudflare's worker subsystem stops claiming
|
|
385
|
+
// the script as a queue consumer. Without this the
|
|
386
|
+
// sibling Worker.delete races on `QueueConsumerConflict`
|
|
387
|
+
// (code 10064) — `deleteConsumer` returns success on the
|
|
388
|
+
// queue subsystem before the script-side view propagates.
|
|
389
|
+
yield* getConsumer({
|
|
390
|
+
accountId: output.accountId,
|
|
391
|
+
queueId: output.queueId,
|
|
392
|
+
consumerId: output.consumerId,
|
|
393
|
+
}).pipe(
|
|
394
|
+
Effect.flatMap(() => Effect.fail("still-attached" as const)),
|
|
395
|
+
Effect.catchTag("ConsumerNotFound", () => Effect.void),
|
|
396
|
+
Effect.retry({
|
|
397
|
+
while: (e) => e === "still-attached",
|
|
398
|
+
schedule: Schedule.spaced("1 second").pipe(
|
|
399
|
+
Schedule.both(Schedule.recurs(30)),
|
|
400
|
+
),
|
|
401
|
+
}),
|
|
402
|
+
Effect.ignore,
|
|
403
|
+
);
|
|
224
404
|
}),
|
|
225
405
|
read: Effect.fn(function* ({ output }) {
|
|
226
406
|
if (output?.consumerId) {
|
|
227
|
-
|
|
407
|
+
const fetched = yield* getConsumer({
|
|
228
408
|
accountId: output.accountId,
|
|
229
409
|
queueId: output.queueId,
|
|
230
410
|
consumerId: output.consumerId,
|
|
231
411
|
}).pipe(
|
|
232
|
-
Effect.
|
|
233
|
-
|
|
412
|
+
Effect.catchTag("ConsumerNotFound", () =>
|
|
413
|
+
Effect.succeed(undefined),
|
|
414
|
+
),
|
|
415
|
+
);
|
|
416
|
+
if (fetched) {
|
|
417
|
+
return {
|
|
418
|
+
consumerId: fetched.consumerId!,
|
|
234
419
|
queueId: output.queueId,
|
|
235
420
|
scriptName:
|
|
236
|
-
("script" in
|
|
237
|
-
?
|
|
421
|
+
("script" in fetched && typeof fetched.script === "string"
|
|
422
|
+
? fetched.script
|
|
238
423
|
: output.scriptName) ?? output.scriptName,
|
|
239
424
|
accountId: output.accountId,
|
|
240
|
-
}
|
|
241
|
-
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
// Fallback: a state loss can leave us without a consumerId
|
|
429
|
+
// even though the consumer is still alive on Cloudflare. The
|
|
430
|
+
// queue allows only one worker consumer, so finding it via
|
|
431
|
+
// listConsumers is unambiguous.
|
|
432
|
+
if (output?.queueId && output?.accountId) {
|
|
433
|
+
const match = yield* findWorkerConsumer(
|
|
434
|
+
output.accountId,
|
|
435
|
+
output.queueId,
|
|
242
436
|
);
|
|
437
|
+
if (match) {
|
|
438
|
+
return {
|
|
439
|
+
consumerId: match.consumerId,
|
|
440
|
+
queueId: output.queueId,
|
|
441
|
+
scriptName: match.script ?? output.scriptName,
|
|
442
|
+
accountId: output.accountId,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
243
445
|
}
|
|
244
446
|
return undefined;
|
|
245
447
|
}),
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import type * as cf from "@cloudflare/workers-types";
|
|
2
|
+
import * as Cause from "effect/Cause";
|
|
3
|
+
import * as Context from "effect/Context";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as Layer from "effect/Layer";
|
|
6
|
+
import * as Stream from "effect/Stream";
|
|
7
|
+
import * as Binding from "../../Binding.ts";
|
|
8
|
+
import type { ResourceLike } from "../../Resource.ts";
|
|
9
|
+
import { RuntimeContext } from "../../RuntimeContext.ts";
|
|
10
|
+
import type { FunctionContext } from "../../Serverless/Function.ts";
|
|
11
|
+
import { isWorker, isWorkerEvent } from "../Workers/Worker.ts";
|
|
12
|
+
import type { Queue } from "./Queue.ts";
|
|
13
|
+
import { QueueConsumer } from "./QueueConsumer.ts";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Subscriber settings — the same shape Cloudflare's `QueueConsumer`
|
|
17
|
+
* accepts. `messages(queue, props).subscribe(...)` passes these
|
|
18
|
+
* through to the auto-created `Cloudflare.QueueConsumer` so a single
|
|
19
|
+
* call captures both runtime and deploy-time intent.
|
|
20
|
+
*/
|
|
21
|
+
export interface MessagesProps {
|
|
22
|
+
/** Maximum messages per batch. */
|
|
23
|
+
batchSize?: number;
|
|
24
|
+
/** Maximum concurrent invocations. */
|
|
25
|
+
maxConcurrency?: number;
|
|
26
|
+
/** Maximum delivery attempts before dead-lettering. */
|
|
27
|
+
maxRetries?: number;
|
|
28
|
+
/** Wait time, in ms, before flushing a partial batch. */
|
|
29
|
+
maxWaitTimeMs?: number;
|
|
30
|
+
/** Backoff, in seconds, applied to a retry. */
|
|
31
|
+
retryDelay?: number;
|
|
32
|
+
/** Optional dead-letter queue name. */
|
|
33
|
+
deadLetterQueue?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A single queue message handed to the subscribe handler. Mirrors
|
|
38
|
+
* Cloudflare's runtime `Message<Body>` shape so per-message
|
|
39
|
+
* `ack()` / `retry()` semantics match the platform docs.
|
|
40
|
+
*/
|
|
41
|
+
export type QueueMessage<Body = unknown> = cf.Message<Body>;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Subscribe to a Cloudflare Queue with an Effect stream handler.
|
|
45
|
+
*
|
|
46
|
+
* Mirrors `AWS.SQS.messages(queue).subscribe(...)` on the
|
|
47
|
+
* Cloudflare side. Wires both halves of the consumer in one call:
|
|
48
|
+
*
|
|
49
|
+
* - **Runtime**: registers a `queue` event listener on the Worker.
|
|
50
|
+
* Each batch is piped through `process` as a `Stream.Stream`.
|
|
51
|
+
* - **Deploy-time**: yields a `Cloudflare.QueueConsumer` resource
|
|
52
|
+
* so Cloudflare actually dispatches messages from `queue` to
|
|
53
|
+
* this Worker. No manual `QueueConsumer` wiring needed in
|
|
54
|
+
* `alchemy.run.ts`.
|
|
55
|
+
*
|
|
56
|
+
* Acking semantics: if `process` succeeds, every message in the
|
|
57
|
+
* batch is `ack()`ed; if it fails, every message is `retry()`ed
|
|
58
|
+
* and Cloudflare applies `maxRetries` / `retryDelay` from the
|
|
59
|
+
* settings before dead-lettering. Per-message control is still
|
|
60
|
+
* available by calling `msg.ack()` / `msg.retry()` inside the
|
|
61
|
+
* handler.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```typescript
|
|
65
|
+
* import * as Cloudflare from "alchemy/Cloudflare";
|
|
66
|
+
* import * as Effect from "effect/Effect";
|
|
67
|
+
* import * as Stream from "effect/Stream";
|
|
68
|
+
*
|
|
69
|
+
* yield* Cloudflare.messages<MyEvent>(queueResource, {
|
|
70
|
+
* batchSize: 25,
|
|
71
|
+
* maxRetries: 3,
|
|
72
|
+
* }).subscribe((stream) =>
|
|
73
|
+
* Stream.runForEach(stream, (msg) =>
|
|
74
|
+
* Effect.log(`event ${msg.body.id}`),
|
|
75
|
+
* ),
|
|
76
|
+
* );
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export const messages = <Body = unknown>(
|
|
80
|
+
queue: Queue,
|
|
81
|
+
props: MessagesProps = {},
|
|
82
|
+
) => ({
|
|
83
|
+
subscribe: <Req = never>(
|
|
84
|
+
process: (
|
|
85
|
+
stream: Stream.Stream<QueueMessage<Body>>,
|
|
86
|
+
) => Effect.Effect<void, unknown, Req>,
|
|
87
|
+
) =>
|
|
88
|
+
QueueEventSource.use((source) => source<Body, Req>(queue, props, process)),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// `Req` is the handler's requirements. The service registers the
|
|
92
|
+
// handler with the Worker's runtime context, where the runtime
|
|
93
|
+
// machinery provides bindings and `WorkerEnvironment` when the
|
|
94
|
+
// dispatch fires — so the requirement is satisfied at handler
|
|
95
|
+
// invocation, NOT at subscribe time. We drop `Req` from the return
|
|
96
|
+
// to keep init effects clean (mirrors `AWS.SQS.QueueEventSourceService`).
|
|
97
|
+
export type QueueEventSourceService = <Body = unknown, Req = never>(
|
|
98
|
+
queue: Queue,
|
|
99
|
+
props: MessagesProps,
|
|
100
|
+
process: (
|
|
101
|
+
stream: Stream.Stream<QueueMessage<Body>>,
|
|
102
|
+
) => Effect.Effect<void, unknown, Req>,
|
|
103
|
+
) => Effect.Effect<void, never, never>;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Service tag for the Cloudflare Queue event source. Provided by
|
|
107
|
+
* {@link QueueEventSourceLive} on the Worker's runtime layer.
|
|
108
|
+
*/
|
|
109
|
+
export class QueueEventSource extends Context.Service<
|
|
110
|
+
QueueEventSource,
|
|
111
|
+
QueueEventSourceService
|
|
112
|
+
>()("Cloudflare.Queue.QueueEventSource") {}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Deploy-time policy that yields a `Cloudflare.QueueConsumer`
|
|
116
|
+
* resource pointing the host Worker at the queue. Provided in
|
|
117
|
+
* `Cloudflare.providers()` and used by {@link QueueEventSourceLive}
|
|
118
|
+
* via `yield* QueueEventSourcePolicy(...)`. At runtime the policy
|
|
119
|
+
* is absent, so the call is a no-op.
|
|
120
|
+
*/
|
|
121
|
+
export class QueueEventSourcePolicy extends Binding.Policy<
|
|
122
|
+
QueueEventSourcePolicy,
|
|
123
|
+
(queue: Queue, props: MessagesProps) => Effect.Effect<void>
|
|
124
|
+
>()("Cloudflare.Queue.QueueEventSource") {}
|
|
125
|
+
|
|
126
|
+
export const QueueEventSourcePolicyLive = QueueEventSourcePolicy.layer.succeed(
|
|
127
|
+
// Cast: yielding `QueueConsumer(...)` requires the Cloudflare
|
|
128
|
+
// `Providers` services, which the deploy-time stack provides
|
|
129
|
+
// when this policy runs (the worker's init scope inherits the
|
|
130
|
+
// stack's services). `Binding.Policy.layer.succeed` types the
|
|
131
|
+
// body as `Effect<void, never, never>` to keep most policies
|
|
132
|
+
// simple; we step around that constraint here because we
|
|
133
|
+
// genuinely need to spawn a sibling resource at deploy time.
|
|
134
|
+
((host: ResourceLike, queue: Queue, props: MessagesProps) =>
|
|
135
|
+
Effect.gen(function* () {
|
|
136
|
+
if (!isWorker(host)) {
|
|
137
|
+
return yield* Effect.die(
|
|
138
|
+
`Cloudflare.messages(...).subscribe(...) is only supported on ` +
|
|
139
|
+
`Cloudflare.Worker hosts (got '${host.Type}').`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
// Yield the QueueConsumer resource as a sibling of the
|
|
143
|
+
// Worker. The engine creates / updates / destroys it
|
|
144
|
+
// alongside the Worker's lifecycle; the consumer's
|
|
145
|
+
// reconciler waits for the Worker upload to expose the
|
|
146
|
+
// `queue` handler before completing (see PR #257 for the
|
|
147
|
+
// 11001 retry).
|
|
148
|
+
yield* QueueConsumer(`${queue.LogicalId}Consumer`, {
|
|
149
|
+
queueId: queue.queueId,
|
|
150
|
+
scriptName: host.workerName,
|
|
151
|
+
settings: props,
|
|
152
|
+
deadLetterQueue: props.deadLetterQueue,
|
|
153
|
+
});
|
|
154
|
+
})) as unknown as (
|
|
155
|
+
host: ResourceLike,
|
|
156
|
+
queue: Queue,
|
|
157
|
+
props: MessagesProps,
|
|
158
|
+
) => Effect.Effect<void>,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Runtime layer for {@link messages}. Wires each
|
|
163
|
+
* `messages(queue).subscribe(...)` call in the Worker init phase to
|
|
164
|
+
* a `queue` event listener on the runtime context, and asks the
|
|
165
|
+
* deploy-time policy ({@link QueueEventSourcePolicy}, provided in
|
|
166
|
+
* `Cloudflare.providers()`) to yield the matching
|
|
167
|
+
* `Cloudflare.QueueConsumer` resource.
|
|
168
|
+
*
|
|
169
|
+
* Provide alongside other Cloudflare runtime layers (e.g.
|
|
170
|
+
* `QueueBindingLive`) on the Worker effect.
|
|
171
|
+
*/
|
|
172
|
+
export const QueueEventSourceLive = Layer.effect(
|
|
173
|
+
QueueEventSource,
|
|
174
|
+
Effect.gen(function* () {
|
|
175
|
+
const policy = yield* QueueEventSourcePolicy;
|
|
176
|
+
return Effect.fn(function* <Body, Req>(
|
|
177
|
+
queue: Queue,
|
|
178
|
+
props: MessagesProps,
|
|
179
|
+
process: (
|
|
180
|
+
stream: Stream.Stream<QueueMessage<Body>>,
|
|
181
|
+
) => Effect.Effect<void, unknown, Req>,
|
|
182
|
+
) {
|
|
183
|
+
// Deploy-time: ensure the QueueConsumer resource exists. At
|
|
184
|
+
// runtime this Layer's `policy` resolves to the no-op variant
|
|
185
|
+
// (Binding.Policy provides that automatically via
|
|
186
|
+
// `Effect.serviceOption`), so this becomes a no-op.
|
|
187
|
+
yield* policy(queue, props);
|
|
188
|
+
|
|
189
|
+
// Resolve the runtime context per-call rather than at layer
|
|
190
|
+
// construction. Capturing it on the layer would leak the
|
|
191
|
+
// requirement past `PlatformServices` exclusion when the
|
|
192
|
+
// Worker typechecks its init effect.
|
|
193
|
+
const ctx = (yield* RuntimeContext) as unknown as FunctionContext;
|
|
194
|
+
// Capture the queue-name accessor once; the listener body
|
|
195
|
+
// re-resolves it per event via `yield* QueueName`. A worker
|
|
196
|
+
// can consume multiple queues — each subscribe registers its
|
|
197
|
+
// own listener and they all see every queue event, so the
|
|
198
|
+
// queue-name match is what scopes the handler.
|
|
199
|
+
const QueueName = yield* queue.queueName;
|
|
200
|
+
|
|
201
|
+
yield* ctx.listen<void, Req>((event) => {
|
|
202
|
+
if (!isWorkerEvent(event) || event.type !== "queue") return;
|
|
203
|
+
const batch = event.input as cf.MessageBatch<Body>;
|
|
204
|
+
|
|
205
|
+
return Effect.gen(function* () {
|
|
206
|
+
const queueName = yield* QueueName;
|
|
207
|
+
if (batch.queue !== queueName) return;
|
|
208
|
+
|
|
209
|
+
yield* process(Stream.fromIterable(batch.messages)).pipe(
|
|
210
|
+
Effect.tap(() =>
|
|
211
|
+
Effect.sync(() => {
|
|
212
|
+
for (const msg of batch.messages) msg.ack();
|
|
213
|
+
}),
|
|
214
|
+
),
|
|
215
|
+
Effect.onError((cause) =>
|
|
216
|
+
Effect.sync(() => {
|
|
217
|
+
// Surface the failure so the operator sees what
|
|
218
|
+
// tripped the retry path; without this the only
|
|
219
|
+
// signal is the message reappearing on the next
|
|
220
|
+
// attempt.
|
|
221
|
+
console.error(
|
|
222
|
+
`[QueueEventSource] handler failed on queue ` +
|
|
223
|
+
`"${queueName}": ${Cause.pretty(cause)}`,
|
|
224
|
+
);
|
|
225
|
+
for (const msg of batch.messages) msg.retry();
|
|
226
|
+
}),
|
|
227
|
+
),
|
|
228
|
+
Effect.catchCause(() => Effect.void),
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
}) as QueueEventSourceService;
|
|
233
|
+
}),
|
|
234
|
+
);
|