@effect/platform-cloudflare 0.0.0-stage → 4.1.3
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/AGENTS.md +434 -0
- package/CLAUDE.md +434 -0
- package/LICENSE +21 -0
- package/README.md +505 -2
- package/ai-docs/README.md +44 -0
- package/ai-docs/package.json +36 -0
- package/ai-docs/src/01_effect/01_basics/01_effect-gen.ts +30 -0
- package/ai-docs/src/01_effect/01_basics/02_effect-fn.ts +52 -0
- package/ai-docs/src/01_effect/01_basics/10_creating-effects.ts +74 -0
- package/ai-docs/src/01_effect/01_basics/index.md +7 -0
- package/ai-docs/src/01_effect/02_schema/10_schema-basics.ts +43 -0
- package/ai-docs/src/01_effect/02_schema/index.md +7 -0
- package/ai-docs/src/01_effect/03_services/01_service.ts +45 -0
- package/ai-docs/src/01_effect/03_services/10_reference.ts +10 -0
- package/ai-docs/src/01_effect/03_services/20_layer-composition.ts +70 -0
- package/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts +66 -0
- package/ai-docs/src/01_effect/03_services/index.md +5 -0
- package/ai-docs/src/01_effect/04_errors/01_error-handling.ts +30 -0
- package/ai-docs/src/01_effect/04_errors/10_catch-tags.ts +24 -0
- package/ai-docs/src/01_effect/04_errors/20_reason-errors.ts +64 -0
- package/ai-docs/src/01_effect/04_errors/index.md +1 -0
- package/ai-docs/src/01_effect/05_resources/10_acquire-release.ts +105 -0
- package/ai-docs/src/01_effect/05_resources/20_layer-side-effects.ts +31 -0
- package/ai-docs/src/01_effect/05_resources/30_layer-map.ts +86 -0
- package/ai-docs/src/01_effect/05_resources/index.md +3 -0
- package/ai-docs/src/01_effect/06_running/10_run-main.ts +30 -0
- package/ai-docs/src/01_effect/06_running/20_layer-launch.ts +27 -0
- package/ai-docs/src/01_effect/06_running/index.md +1 -0
- package/ai-docs/src/01_effect/07_pubsub/10_pubsub.ts +56 -0
- package/ai-docs/src/01_effect/07_pubsub/index.md +3 -0
- package/ai-docs/src/03_stream/10_creating-streams.ts +103 -0
- package/ai-docs/src/03_stream/20_consuming-streams.ts +137 -0
- package/ai-docs/src/03_stream/30_encoding.ts +163 -0
- package/ai-docs/src/03_stream/index.md +4 -0
- package/ai-docs/src/04_integration/10_managed-runtime.ts +129 -0
- package/ai-docs/src/04_integration/index.md +5 -0
- package/ai-docs/src/05_batching/10_request-resolver.ts +89 -0
- package/ai-docs/src/05_batching/index.md +3 -0
- package/ai-docs/src/06_schedule/10_schedules.ts +111 -0
- package/ai-docs/src/06_schedule/index.md +3 -0
- package/ai-docs/src/07_datetime/10_creating-and-formatting.ts +30 -0
- package/ai-docs/src/07_datetime/20_time-zones.ts +44 -0
- package/ai-docs/src/07_datetime/index.md +5 -0
- package/ai-docs/src/08_observability/10_logging.ts +66 -0
- package/ai-docs/src/08_observability/20_otlp-tracing.ts +95 -0
- package/ai-docs/src/08_observability/index.md +7 -0
- package/ai-docs/src/09_testing/10_effect-tests.ts +55 -0
- package/ai-docs/src/09_testing/20_layer-tests.ts +138 -0
- package/ai-docs/src/09_testing/index.md +1 -0
- package/ai-docs/src/10_predicate/01_basics.ts +14 -0
- package/ai-docs/src/10_predicate/index.md +9 -0
- package/ai-docs/src/40_sql/10_basics.ts +175 -0
- package/ai-docs/src/40_sql/index.md +6 -0
- package/ai-docs/src/50_http-client/10_basics.ts +102 -0
- package/ai-docs/src/50_http-client/index.md +3 -0
- package/ai-docs/src/51_http-server/10_basics.ts +118 -0
- package/ai-docs/src/51_http-server/20_testing.ts +100 -0
- package/ai-docs/src/51_http-server/fixtures/api/Api.ts +14 -0
- package/ai-docs/src/51_http-server/fixtures/api/Authorization.ts +36 -0
- package/ai-docs/src/51_http-server/fixtures/api/System.ts +10 -0
- package/ai-docs/src/51_http-server/fixtures/api/Users.ts +107 -0
- package/ai-docs/src/51_http-server/fixtures/domain/User.ts +21 -0
- package/ai-docs/src/51_http-server/fixtures/domain/UserErrors.ts +22 -0
- package/ai-docs/src/51_http-server/fixtures/server/Authorization.ts +37 -0
- package/ai-docs/src/51_http-server/fixtures/server/Users/http.ts +80 -0
- package/ai-docs/src/51_http-server/fixtures/server/Users.ts +183 -0
- package/ai-docs/src/51_http-server/index.md +3 -0
- package/ai-docs/src/52_graphql/10_basics.ts +117 -0
- package/ai-docs/src/52_graphql/20_testing.ts +45 -0
- package/ai-docs/src/52_graphql/30_batching.ts +118 -0
- package/ai-docs/src/52_graphql/40_aliased-batching.ts +124 -0
- package/ai-docs/src/52_graphql/50_errors-and-retries.ts +74 -0
- package/ai-docs/src/52_graphql/index.md +6 -0
- package/ai-docs/src/60_child-process/10_working-with-child-processes.ts +117 -0
- package/ai-docs/src/60_child-process/index.md +3 -0
- package/ai-docs/src/70_cli/10_basics.ts +162 -0
- package/ai-docs/src/70_cli/index.md +5 -0
- package/ai-docs/src/71_ai/10_language-model.ts +156 -0
- package/ai-docs/src/71_ai/20_tools.ts +226 -0
- package/ai-docs/src/71_ai/30_chat.ts +158 -0
- package/ai-docs/src/71_ai/fixtures/domain/LaunchPlan.ts +9 -0
- package/ai-docs/src/71_ai/index.md +5 -0
- package/ai-docs/src/80_cluster/10_entities.ts +97 -0
- package/ai-docs/src/80_cluster/index.md +4 -0
- package/ai-docs/src/index.md +10 -0
- package/ai-docs/tsconfig.json +28 -0
- package/dist/AlchemyCloudflareCluster.d.ts +134 -0
- package/dist/AlchemyCloudflareCluster.d.ts.map +1 -0
- package/dist/AlchemyCloudflareCluster.js +111 -0
- package/dist/AlchemyCloudflareCluster.js.map +1 -0
- package/dist/CloudflareCluster.d.ts +196 -0
- package/dist/CloudflareCluster.d.ts.map +1 -0
- package/dist/CloudflareCluster.js +533 -0
- package/dist/CloudflareCluster.js.map +1 -0
- package/dist/CloudflareDurableObjectPrograms.d.ts +279 -0
- package/dist/CloudflareDurableObjectPrograms.d.ts.map +1 -0
- package/dist/CloudflareDurableObjectPrograms.js +295 -0
- package/dist/CloudflareDurableObjectPrograms.js.map +1 -0
- package/dist/CloudflareDurableObjects.d.ts +149 -0
- package/dist/CloudflareDurableObjects.d.ts.map +1 -0
- package/dist/CloudflareDurableObjects.js +337 -0
- package/dist/CloudflareDurableObjects.js.map +1 -0
- package/dist/CloudflarePersistedQueue.d.ts +36 -0
- package/dist/CloudflarePersistedQueue.d.ts.map +1 -0
- package/dist/CloudflarePersistedQueue.js +122 -0
- package/dist/CloudflarePersistedQueue.js.map +1 -0
- package/dist/CloudflareWorkflowEngine.d.ts +72 -0
- package/dist/CloudflareWorkflowEngine.d.ts.map +1 -0
- package/dist/CloudflareWorkflowEngine.js +187 -0
- package/dist/CloudflareWorkflowEngine.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/alchemyCluster.d.ts +2 -0
- package/dist/internal/alchemyCluster.d.ts.map +1 -0
- package/dist/internal/alchemyCluster.js +75 -0
- package/dist/internal/alchemyCluster.js.map +1 -0
- package/dist/internal/boundedMap.d.ts +3 -0
- package/dist/internal/boundedMap.d.ts.map +1 -0
- package/dist/internal/boundedMap.js +15 -0
- package/dist/internal/boundedMap.js.map +1 -0
- package/dist/internal/cluster.d.ts +57 -0
- package/dist/internal/cluster.d.ts.map +1 -0
- package/dist/internal/cluster.js +2 -0
- package/dist/internal/cluster.js.map +1 -0
- package/dist/internal/clusterName.d.ts +2 -0
- package/dist/internal/clusterName.d.ts.map +1 -0
- package/dist/internal/clusterName.js +16 -0
- package/dist/internal/clusterName.js.map +1 -0
- package/dist/internal/entityKeepAlive.d.ts +2 -0
- package/dist/internal/entityKeepAlive.d.ts.map +1 -0
- package/dist/internal/entityKeepAlive.js +73 -0
- package/dist/internal/entityKeepAlive.js.map +1 -0
- package/dist/internal/entityMailbox.d.ts +2 -0
- package/dist/internal/entityMailbox.d.ts.map +1 -0
- package/dist/internal/entityMailbox.js +160 -0
- package/dist/internal/entityMailbox.js.map +1 -0
- package/dist/internal/entityRegistry.d.ts +16 -0
- package/dist/internal/entityRegistry.d.ts.map +1 -0
- package/dist/internal/entityRegistry.js +22 -0
- package/dist/internal/entityRegistry.js.map +1 -0
- package/dist/internal/entityReply.d.ts +2 -0
- package/dist/internal/entityReply.d.ts.map +1 -0
- package/dist/internal/entityReply.js +35 -0
- package/dist/internal/entityReply.js.map +1 -0
- package/dist/internal/entityRuntime.d.ts +2 -0
- package/dist/internal/entityRuntime.d.ts.map +1 -0
- package/dist/internal/entityRuntime.js +823 -0
- package/dist/internal/entityRuntime.js.map +1 -0
- package/dist/internal/entitySql.d.ts +2 -0
- package/dist/internal/entitySql.d.ts.map +1 -0
- package/dist/internal/entitySql.js +29 -0
- package/dist/internal/entitySql.js.map +1 -0
- package/dist/internal/entityStorage.d.ts +2 -0
- package/dist/internal/entityStorage.d.ts.map +1 -0
- package/dist/internal/entityStorage.js +98 -0
- package/dist/internal/entityStorage.js.map +1 -0
- package/dist/internal/entityWire.d.ts +2 -0
- package/dist/internal/entityWire.d.ts.map +1 -0
- package/dist/internal/entityWire.js +146 -0
- package/dist/internal/entityWire.js.map +1 -0
- package/dist/internal/queueRuntime.d.ts +2 -0
- package/dist/internal/queueRuntime.d.ts.map +1 -0
- package/dist/internal/queueRuntime.js +88 -0
- package/dist/internal/queueRuntime.js.map +1 -0
- package/dist/internal/queueStorage.d.ts +2 -0
- package/dist/internal/queueStorage.d.ts.map +1 -0
- package/dist/internal/queueStorage.js +129 -0
- package/dist/internal/queueStorage.js.map +1 -0
- package/dist/internal/registry.d.ts +10 -0
- package/dist/internal/registry.d.ts.map +1 -0
- package/dist/internal/registry.js +24 -0
- package/dist/internal/registry.js.map +1 -0
- package/dist/internal/singletonRegistry.d.ts +2 -0
- package/dist/internal/singletonRegistry.d.ts.map +1 -0
- package/dist/internal/singletonRegistry.js +9 -0
- package/dist/internal/singletonRegistry.js.map +1 -0
- package/dist/internal/singletonRuntime.d.ts +2 -0
- package/dist/internal/singletonRuntime.d.ts.map +1 -0
- package/dist/internal/singletonRuntime.js +42 -0
- package/dist/internal/singletonRuntime.js.map +1 -0
- package/dist/internal/singletonStorage.d.ts +2 -0
- package/dist/internal/singletonStorage.d.ts.map +1 -0
- package/dist/internal/singletonStorage.js +34 -0
- package/dist/internal/singletonStorage.js.map +1 -0
- package/dist/internal/transport.d.ts +2 -0
- package/dist/internal/transport.d.ts.map +1 -0
- package/dist/internal/transport.js +49 -0
- package/dist/internal/transport.js.map +1 -0
- package/dist/internal/workflowRegistry.d.ts +2 -0
- package/dist/internal/workflowRegistry.d.ts.map +1 -0
- package/dist/internal/workflowRegistry.js +29 -0
- package/dist/internal/workflowRegistry.js.map +1 -0
- package/dist/internal/workflowRuntime.d.ts +2 -0
- package/dist/internal/workflowRuntime.d.ts.map +1 -0
- package/dist/internal/workflowRuntime.js +322 -0
- package/dist/internal/workflowRuntime.js.map +1 -0
- package/dist/internal/workflowStorage.d.ts +2 -0
- package/dist/internal/workflowStorage.d.ts.map +1 -0
- package/dist/internal/workflowStorage.js +113 -0
- package/dist/internal/workflowStorage.js.map +1 -0
- package/dist/internal/workflowWire.d.ts +2 -0
- package/dist/internal/workflowWire.d.ts.map +1 -0
- package/dist/internal/workflowWire.js +52 -0
- package/dist/internal/workflowWire.js.map +1 -0
- package/package.json +74 -3
- package/src/AlchemyCloudflareCluster.ts +200 -0
- package/src/CloudflareCluster.ts +704 -0
- package/src/CloudflareDurableObjectPrograms.ts +543 -0
- package/src/CloudflareDurableObjects.ts +430 -0
- package/src/CloudflarePersistedQueue.ts +189 -0
- package/src/CloudflareWorkflowEngine.ts +220 -0
- package/src/index.ts +38 -0
- package/src/internal/alchemyCluster.ts +120 -0
- package/src/internal/boundedMap.ts +15 -0
- package/src/internal/cluster.ts +57 -0
- package/src/internal/clusterName.ts +19 -0
- package/src/internal/entityKeepAlive.ts +121 -0
- package/src/internal/entityMailbox.ts +324 -0
- package/src/internal/entityRegistry.ts +46 -0
- package/src/internal/entityReply.ts +54 -0
- package/src/internal/entityRuntime.ts +1318 -0
- package/src/internal/entitySql.ts +60 -0
- package/src/internal/entityStorage.ts +168 -0
- package/src/internal/entityWire.ts +204 -0
- package/src/internal/queueRuntime.ts +174 -0
- package/src/internal/queueStorage.ts +255 -0
- package/src/internal/registry.ts +31 -0
- package/src/internal/singletonRegistry.ts +21 -0
- package/src/internal/singletonRuntime.ts +88 -0
- package/src/internal/singletonStorage.ts +64 -0
- package/src/internal/transport.ts +67 -0
- package/src/internal/workflowRegistry.ts +78 -0
- package/src/internal/workflowRuntime.ts +443 -0
- package/src/internal/workflowStorage.ts +234 -0
- package/src/internal/workflowWire.ts +123 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Durable Object classes behind `CloudflareCluster.layer`.
|
|
3
|
+
*
|
|
4
|
+
* A Worker using the Cloudflare cluster re-exports these four classes from its
|
|
5
|
+
* entry module and binds each one in `wrangler.jsonc` as a SQLite-backed
|
|
6
|
+
* Durable Object class. The cluster resolves objects through the same-Worker
|
|
7
|
+
* namespace bindings only; none of these classes serve a public route, and any
|
|
8
|
+
* direct `fetch` of an object is rejected.
|
|
9
|
+
*
|
|
10
|
+
* @stability experimental
|
|
11
|
+
* @since 4.1.3
|
|
12
|
+
*/
|
|
13
|
+
import { DurableObject } from "cloudflare:workers"
|
|
14
|
+
import * as Effect from "effect/Effect"
|
|
15
|
+
import {
|
|
16
|
+
type ClusterDurableQueueProgram,
|
|
17
|
+
type ClusterEntityProgram,
|
|
18
|
+
type ClusterSingletonProgram,
|
|
19
|
+
type ClusterWorkflowProgram,
|
|
20
|
+
type ClusterWorkflowRunOptions,
|
|
21
|
+
type ClusterWorkflowWakeMode,
|
|
22
|
+
type DurableQueueItem,
|
|
23
|
+
type EntityDeliveryOptions,
|
|
24
|
+
type EntityInvokeResult,
|
|
25
|
+
makeClusterDurableQueueProgram,
|
|
26
|
+
makeClusterEntityProgram,
|
|
27
|
+
makeClusterSingletonProgram,
|
|
28
|
+
makeClusterWorkflowProgram,
|
|
29
|
+
type SingletonWakeResult
|
|
30
|
+
} from "./CloudflareDurableObjectPrograms.ts"
|
|
31
|
+
|
|
32
|
+
const notExposed = (className: string) => () => {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`@effect/platform-cloudflare: ${className} is not exposed over fetch, use the same-Worker namespace binding`
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Initializes the Worker application before a Cloudflare cluster Durable
|
|
40
|
+
* Object handles requests or alarms. The initializer is shared by all four
|
|
41
|
+
* Durable Object classes and runs once per Worker isolate unless a build fails
|
|
42
|
+
* or stalls.
|
|
43
|
+
*
|
|
44
|
+
* **Details**
|
|
45
|
+
*
|
|
46
|
+
* The initializer runs inside a Durable Object constructor's
|
|
47
|
+
* `blockConcurrencyWhile`, which Cloudflare cancels after 30 seconds. It must
|
|
48
|
+
* complete within 15 seconds: a build still pending after that is treated as
|
|
49
|
+
* abandoned, and a waiting object runs the initializer again.
|
|
50
|
+
*
|
|
51
|
+
* @stability experimental
|
|
52
|
+
* @category models
|
|
53
|
+
* @since 4.1.3
|
|
54
|
+
*/
|
|
55
|
+
export type Initializer<Env = unknown, A = unknown> = (env: Env) => PromiseLike<A>
|
|
56
|
+
|
|
57
|
+
interface InitializationAttempt {
|
|
58
|
+
readonly startedAt: number
|
|
59
|
+
exit: {
|
|
60
|
+
readonly _tag: "Success"
|
|
61
|
+
readonly value: unknown
|
|
62
|
+
} | {
|
|
63
|
+
readonly _tag: "Failure"
|
|
64
|
+
readonly error: unknown
|
|
65
|
+
} | undefined
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Durable Object waiters run inside `blockConcurrencyWhile`, which Cloudflare
|
|
69
|
+
// cancels after 30 seconds. A build still pending after 15 seconds is treated
|
|
70
|
+
// as abandoned. Only a Durable Object waiter that has itself waited less than
|
|
71
|
+
// 15 seconds starts a build, whether it takes over a stale one or retries after
|
|
72
|
+
// a failure. The first build and any replacement therefore both get at least
|
|
73
|
+
// 15 seconds.
|
|
74
|
+
const staleInitializationMillis = 15_000
|
|
75
|
+
const durableObjectTakeoverMillis = 15_000
|
|
76
|
+
const initializationPollMillis = 20
|
|
77
|
+
|
|
78
|
+
let initializer: Initializer | undefined
|
|
79
|
+
let initialization: InitializationAttempt | undefined
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Registers the Worker application initializer used by the Cloudflare cluster
|
|
83
|
+
* Durable Object classes.
|
|
84
|
+
*
|
|
85
|
+
* **Details**
|
|
86
|
+
*
|
|
87
|
+
* Call this once from the Worker entry module with the same lazy application
|
|
88
|
+
* build used by its request handlers. Every entity, workflow, queue, and
|
|
89
|
+
* singleton object blocks requests and alarms until that build completes. A
|
|
90
|
+
* failure is reported to every caller already waiting on that build, and the
|
|
91
|
+
* next cold caller runs the initializer again. The initializer must complete
|
|
92
|
+
* within 15 seconds. A build still pending after that is treated as abandoned,
|
|
93
|
+
* and a waiter runs the initializer itself, so a build can run more than once
|
|
94
|
+
* in an isolate. A Durable Object waiter starts a build only while it has
|
|
95
|
+
* waited less than 15 seconds, so any replacement keeps at least 15 seconds of
|
|
96
|
+
* the 30 second `blockConcurrencyWhile` limit. The returned function joins the
|
|
97
|
+
* same readiness state from Worker handlers.
|
|
98
|
+
*
|
|
99
|
+
* @stability experimental
|
|
100
|
+
* @category initialization
|
|
101
|
+
* @since 4.1.3
|
|
102
|
+
*/
|
|
103
|
+
export const setInitializer = <Env, A>(handler: Initializer<Env, A>): (env: Env) => Promise<A> => {
|
|
104
|
+
if (initializer !== undefined) {
|
|
105
|
+
throw new Error("@effect/platform-cloudflare: the Durable Object initializer is already set")
|
|
106
|
+
}
|
|
107
|
+
initializer = handler as Initializer
|
|
108
|
+
// Worker handlers are not bounded by `blockConcurrencyWhile`, so they may
|
|
109
|
+
// take over a stale build however long they have waited.
|
|
110
|
+
return (env) => waitForInitialization(env, Infinity) as Promise<A>
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const waitForInitialization = async (env: unknown, takeoverMillis: number): Promise<unknown> => {
|
|
114
|
+
if (initializer === undefined) return
|
|
115
|
+
const waitStartedAt = Date.now()
|
|
116
|
+
const isFresh = (): boolean => Date.now() - waitStartedAt < takeoverMillis
|
|
117
|
+
const canTakeOver = (attempt: InitializationAttempt): boolean => isStale(attempt) && isFresh()
|
|
118
|
+
// A waiter too old to take over keeps following the current attempt.
|
|
119
|
+
const keepsWaiting = (attempt: InitializationAttempt): boolean =>
|
|
120
|
+
attempt.exit === undefined && initialization === attempt && !canTakeOver(attempt)
|
|
121
|
+
while (true) {
|
|
122
|
+
const attempt = initialization
|
|
123
|
+
if (attempt === undefined ? isFresh() : canTakeOver(attempt)) {
|
|
124
|
+
return runInitializer(initializer, env)
|
|
125
|
+
}
|
|
126
|
+
if (attempt === undefined) {
|
|
127
|
+
// The attempt this waiter followed was replaced by one that already
|
|
128
|
+
// failed. A waiter this old leaves the retry to a fresher caller and
|
|
129
|
+
// follows the attempt that caller starts.
|
|
130
|
+
await scheduler.wait(initializationPollMillis)
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
// Cloudflare promises belong to the request context that created them. A
|
|
134
|
+
// fresh wait in this context observes the shared plain-JavaScript state
|
|
135
|
+
// without awaiting the first Durable Object's promise from another event.
|
|
136
|
+
while (keepsWaiting(attempt)) {
|
|
137
|
+
await scheduler.wait(initializationPollMillis)
|
|
138
|
+
}
|
|
139
|
+
if (attempt.exit === undefined) continue
|
|
140
|
+
if (attempt.exit._tag === "Failure") throw attempt.exit.error
|
|
141
|
+
return attempt.exit.value
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const isStale = (attempt: InitializationAttempt): boolean =>
|
|
146
|
+
attempt.exit === undefined && Date.now() - attempt.startedAt >= staleInitializationMillis
|
|
147
|
+
|
|
148
|
+
const runInitializer = async (run: Initializer, env: unknown): Promise<unknown> => {
|
|
149
|
+
const attempt: InitializationAttempt = { startedAt: Date.now(), exit: undefined }
|
|
150
|
+
initialization = attempt
|
|
151
|
+
try {
|
|
152
|
+
const value = await run(env)
|
|
153
|
+
attempt.exit = { _tag: "Success", value }
|
|
154
|
+
// An abandoned build that settles late still makes the isolate ready.
|
|
155
|
+
if (initialization?.exit?._tag !== "Success") initialization = attempt
|
|
156
|
+
return value
|
|
157
|
+
} catch (error) {
|
|
158
|
+
attempt.exit = { _tag: "Failure", error }
|
|
159
|
+
// Waiters holding this attempt see the failure; the next cold caller retries.
|
|
160
|
+
if (initialization === attempt) initialization = undefined
|
|
161
|
+
throw error
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const blockOnInitialization = (
|
|
166
|
+
ctx: DurableObjectState,
|
|
167
|
+
env: unknown,
|
|
168
|
+
after?: (() => Promise<unknown>) | undefined
|
|
169
|
+
): void => {
|
|
170
|
+
void ctx.blockConcurrencyWhile(async () => {
|
|
171
|
+
await waitForInitialization(env, durableObjectTakeoverMillis)
|
|
172
|
+
if (after !== undefined) await after()
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The shared entity class. One instance holds one entity address; the handlers
|
|
178
|
+
* for every `EntityType` are registered at Worker init.
|
|
179
|
+
*
|
|
180
|
+
* **Details**
|
|
181
|
+
*
|
|
182
|
+
* The constructor stays cheap: it opens SQLite, ensures the mailbox tables,
|
|
183
|
+
* and re-arms the single alarm from the earliest pending `deliver_at`, or the
|
|
184
|
+
* keep-alive heartbeat when the entity holds `Entity.keepAlive`. User
|
|
185
|
+
* handlers are never built in the constructor; requests and alarms wait for
|
|
186
|
+
* the registered {@link setInitializer} callback, then handlers are built once
|
|
187
|
+
* per wake. Each build gets a `DurableObjectSqlClient` on this object's SQLite
|
|
188
|
+
* storage; tables whose names start with `cluster_` are reserved for the mailbox.
|
|
189
|
+
*
|
|
190
|
+
* @stability experimental
|
|
191
|
+
* @category durable objects
|
|
192
|
+
* @since 4.1.3
|
|
193
|
+
*/
|
|
194
|
+
export class ClusterEntity extends DurableObject<unknown> {
|
|
195
|
+
// blockConcurrencyWhile completes this assignment before any RPC or alarm.
|
|
196
|
+
#program!: ClusterEntityProgram
|
|
197
|
+
|
|
198
|
+
constructor(ctx: DurableObjectState, env: unknown) {
|
|
199
|
+
super(ctx, env)
|
|
200
|
+
blockOnInitialization(ctx, env, async () => {
|
|
201
|
+
this.#program = await Effect.runPromise(makeClusterEntityProgram(ctx))
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
override alarm(): Promise<void> {
|
|
206
|
+
return Effect.runPromise(this.#program.alarm())
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** @internal Keeps this object non-hibernateable while entity resources have holders. */
|
|
210
|
+
hold(): Promise<void> {
|
|
211
|
+
return Effect.runPromise(this.#program.hold())
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** @internal Same-Worker RPC transport used by `CloudflareCluster.layer`. */
|
|
215
|
+
invoke(envelopeText: string, discard: boolean, delivery?: EntityDeliveryOptions): Promise<EntityInvokeResult> {
|
|
216
|
+
return Effect.runPromise(this.#program.invoke(envelopeText, discard, delivery))
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** @internal Acknowledges a streamed chunk. */
|
|
220
|
+
acknowledge(requestId: string, replyId: string): Promise<ReadonlyArray<string>> {
|
|
221
|
+
return Effect.runPromise(this.#program.acknowledge(requestId, replyId))
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** @internal Interrupts a handler execution and completes its persisted ask, if any. */
|
|
225
|
+
interrupt(storageRequestId: string, clientRequestId = storageRequestId): Promise<void> {
|
|
226
|
+
return Effect.runPromise(this.#program.interrupt(storageRequestId, clientRequestId))
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** @internal Clears stored replies so a reset request replays from scratch. */
|
|
230
|
+
reset(requestId: string): Promise<void> {
|
|
231
|
+
return Effect.runPromise(this.#program.reset(requestId))
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** @internal Completes an in-memory delayed ask owned by this entity object. */
|
|
235
|
+
deliverReply(requestId: string, reply: string): Promise<boolean> {
|
|
236
|
+
return Effect.runPromise(this.#program.deliverReply(requestId, reply))
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
override fetch: () => never = notExposed("ClusterEntity")
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The workflow execution class behind `CloudflareWorkflowEngine`. One
|
|
244
|
+
* instance holds one workflow execution: run state, activity results keyed
|
|
245
|
+
* `${name}/${attempt}`, durable deferred exits, and the clock due table.
|
|
246
|
+
*
|
|
247
|
+
* **Details**
|
|
248
|
+
*
|
|
249
|
+
* The constructor stays cheap: it opens SQLite, ensures the workflow tables,
|
|
250
|
+
* and re-arms the single alarm from the earliest pending clock. Workflow
|
|
251
|
+
* requests and alarms wait for the registered {@link setInitializer} callback
|
|
252
|
+
* before handlers are looked up in the module-level registry and built once
|
|
253
|
+
* per wake.
|
|
254
|
+
*
|
|
255
|
+
* @stability experimental
|
|
256
|
+
* @category durable objects
|
|
257
|
+
* @since 4.1.3
|
|
258
|
+
*/
|
|
259
|
+
export class ClusterWorkflow extends DurableObject<unknown> {
|
|
260
|
+
// blockConcurrencyWhile completes this assignment before any RPC or alarm.
|
|
261
|
+
#program!: ClusterWorkflowProgram
|
|
262
|
+
|
|
263
|
+
constructor(ctx: DurableObjectState, env: unknown) {
|
|
264
|
+
super(ctx, env)
|
|
265
|
+
blockOnInitialization(ctx, env, async () => {
|
|
266
|
+
this.#program = await Effect.runPromise(makeClusterWorkflowProgram(ctx))
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** @internal Same-Worker RPC transport used by `CloudflareWorkflowEngine`. */
|
|
271
|
+
run(payload: string, options: ClusterWorkflowRunOptions): Promise<string> {
|
|
272
|
+
return Effect.runPromise(this.#program.run(payload, options))
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** @internal */
|
|
276
|
+
poll(): Promise<string | undefined> {
|
|
277
|
+
return Effect.runPromise(this.#program.poll())
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** @internal */
|
|
281
|
+
resume(mode?: ClusterWorkflowWakeMode): Promise<void> {
|
|
282
|
+
return Effect.runPromise(this.#program.resume(mode))
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** @internal */
|
|
286
|
+
interrupt(): Promise<void> {
|
|
287
|
+
return Effect.runPromise(this.#program.interrupt())
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** @internal */
|
|
291
|
+
interruptUnsafe(): Promise<void> {
|
|
292
|
+
return Effect.runPromise(this.#program.interruptUnsafe())
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** @internal Records a durable deferred exit and resumes the execution. */
|
|
296
|
+
deferredDone(name: string, exit: string): Promise<void> {
|
|
297
|
+
return Effect.runPromise(this.#program.deferredDone(name, exit))
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** @internal Persists a durable clock and arms the single alarm. */
|
|
301
|
+
scheduleClock(name: string, deferredName: string, wakeUp: number): Promise<void> {
|
|
302
|
+
return Effect.runPromise(this.#program.scheduleClock(name, deferredName, wakeUp))
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
override alarm(): Promise<void> {
|
|
306
|
+
return Effect.runPromise(this.#program.alarm())
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
override fetch: () => never = notExposed("ClusterWorkflow")
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* The durable queue class behind the `PersistedQueue` implementation used by
|
|
314
|
+
* `DurableQueue`. One instance holds one named queue.
|
|
315
|
+
*
|
|
316
|
+
* **Details**
|
|
317
|
+
*
|
|
318
|
+
* The constructor stays cheap: it opens SQLite, ensures the queue table, and
|
|
319
|
+
* re-arms the single alarm from the earliest pending lease expiry. Items are
|
|
320
|
+
* leased to takers for a bounded time; the alarm watchdog expires overdue
|
|
321
|
+
* leases so an item whose worker died is redelivered. Requests and alarms wait
|
|
322
|
+
* for the registered {@link setInitializer} callback first.
|
|
323
|
+
*
|
|
324
|
+
* @stability experimental
|
|
325
|
+
* @category durable objects
|
|
326
|
+
* @since 4.1.3
|
|
327
|
+
*/
|
|
328
|
+
export class ClusterDurableQueue extends DurableObject<unknown> {
|
|
329
|
+
// blockConcurrencyWhile completes this assignment before any RPC or alarm.
|
|
330
|
+
#program!: ClusterDurableQueueProgram
|
|
331
|
+
|
|
332
|
+
constructor(ctx: DurableObjectState, env: unknown) {
|
|
333
|
+
super(ctx, env)
|
|
334
|
+
blockOnInitialization(ctx, env, async () => {
|
|
335
|
+
this.#program = await Effect.runPromise(makeClusterDurableQueueProgram(ctx))
|
|
336
|
+
})
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** @internal Same-Worker RPC transport used by `CloudflarePersistedQueue.layer`. */
|
|
340
|
+
offer(
|
|
341
|
+
id: string,
|
|
342
|
+
element: string,
|
|
343
|
+
delayMillis?: number,
|
|
344
|
+
deduplicate?: "untilExecuted" | "untilRemoved"
|
|
345
|
+
): Promise<void> {
|
|
346
|
+
return Effect.runPromise(this.#program.offer(id, element, delayMillis, deduplicate))
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** @internal Waits until an item is available, then leases it to the caller. */
|
|
350
|
+
take(takerId: string, maxAttempts: number, leaseMillis: number): Promise<DurableQueueItem> {
|
|
351
|
+
return Effect.runPromise(this.#program.take(takerId, maxAttempts, leaseMillis))
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** @internal Cancels a waiting take, releasing an item already leased to it. */
|
|
355
|
+
cancelTake(takerId: string): Promise<void> {
|
|
356
|
+
return Effect.runPromise(this.#program.cancelTake(takerId))
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** @internal Completes the item while the taker still owns its lease. */
|
|
360
|
+
complete(id: string, takerId: string): Promise<void> {
|
|
361
|
+
return Effect.runPromise(this.#program.complete(id, takerId))
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* @internal Records a failed attempt and requeues the item, or marks it failed
|
|
366
|
+
* after its final attempt or a terminal failure. A requeued item stays hidden
|
|
367
|
+
* for `retryDelayMillis`.
|
|
368
|
+
*/
|
|
369
|
+
fail(id: string, lastFailure: string, takerId: string, terminal?: boolean, retryDelayMillis?: number): Promise<void> {
|
|
370
|
+
return Effect.runPromise(this.#program.fail(id, lastFailure, takerId, terminal, retryDelayMillis))
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** @internal Requeues the item and refunds the attempt its lease counted. */
|
|
374
|
+
release(id: string, takerId: string): Promise<void> {
|
|
375
|
+
return Effect.runPromise(this.#program.release(id, takerId))
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** @internal Extends the lease of an item still being processed. */
|
|
379
|
+
extend(id: string, leaseMillis: number, takerId: string): Promise<void> {
|
|
380
|
+
return Effect.runPromise(this.#program.extend(id, leaseMillis, takerId))
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
override alarm(): Promise<void> {
|
|
384
|
+
return Effect.runPromise(this.#program.alarm())
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
override fetch: () => never = notExposed("ClusterDurableQueue")
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* The singleton class. One object holds one registered singleton under the
|
|
392
|
+
* name `Singleton/<name>` and is woken by a Worker Cron Trigger.
|
|
393
|
+
*
|
|
394
|
+
* **Details**
|
|
395
|
+
*
|
|
396
|
+
* The constructor opens SQLite, ensures the singleton state table, and
|
|
397
|
+
* re-arms the watchdog alarm for a wake interrupted by isolate loss. `wake()`
|
|
398
|
+
* runs the registered effect once and returns; it never appends
|
|
399
|
+
* `Effect.never`, so Cloudflare may hibernate the object afterward. A failed
|
|
400
|
+
* run is logged and resolves `wake()` with a `Failure`, so only a transport
|
|
401
|
+
* failure rejects it. Requests
|
|
402
|
+
* and alarms wait for the registered {@link setInitializer} callback before
|
|
403
|
+
* consulting the singleton registry.
|
|
404
|
+
*
|
|
405
|
+
* @stability experimental
|
|
406
|
+
* @category durable objects
|
|
407
|
+
* @since 4.1.3
|
|
408
|
+
*/
|
|
409
|
+
export class ClusterSingleton extends DurableObject<unknown> {
|
|
410
|
+
// blockConcurrencyWhile completes this assignment before any RPC or alarm.
|
|
411
|
+
#program!: ClusterSingletonProgram
|
|
412
|
+
|
|
413
|
+
constructor(ctx: DurableObjectState, env: unknown) {
|
|
414
|
+
super(ctx, env)
|
|
415
|
+
blockOnInitialization(ctx, env, async () => {
|
|
416
|
+
this.#program = await Effect.runPromise(makeClusterSingletonProgram(ctx))
|
|
417
|
+
})
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** @internal Runs one Cron Trigger fire, coalescing a concurrent duplicate. */
|
|
421
|
+
wake(): Promise<SingletonWakeResult> {
|
|
422
|
+
return Effect.runPromise(this.#program.wake())
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
override alarm(): Promise<void> {
|
|
426
|
+
return Effect.runPromise(this.#program.alarm())
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
override fetch: () => never = notExposed("ClusterSingleton")
|
|
430
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs persisted queues on the dedicated queue Durable Object class.
|
|
3
|
+
*
|
|
4
|
+
* On this path one queue name is one Durable Object: the Worker encodes the
|
|
5
|
+
* queue name into a Durable Object name with the same length-prefix scheme as
|
|
6
|
+
* entities and resolves the object through the queue namespace binding. Items,
|
|
7
|
+
* their attempt counts, and their in-flight leases live on the object's SQLite
|
|
8
|
+
* storage behind its single alarm; a take with no available item waits inside
|
|
9
|
+
* the object until an offer, a retry, or an expired lease produces one.
|
|
10
|
+
*
|
|
11
|
+
* Delivery is at-least-once: a taken item is leased for a bounded time and the
|
|
12
|
+
* lease is refreshed while the handler runs, so an item whose worker died is
|
|
13
|
+
* redelivered once the alarm watchdog expires the lease. Leasing an item counts
|
|
14
|
+
* an attempt, so a worker that dies still consumes one, and an expired lease on
|
|
15
|
+
* the last attempt marks the item failed.
|
|
16
|
+
* Offer delays use the object's clock and persist an initial visibility
|
|
17
|
+
* deadline; the alarm wakes waiting takers when that deadline expires.
|
|
18
|
+
*
|
|
19
|
+
* This backs the `DurableQueue` user API; `CloudflareCluster.layer` already
|
|
20
|
+
* includes this layer.
|
|
21
|
+
*
|
|
22
|
+
* A failed item stays hidden on the object for the configured retry delay, so
|
|
23
|
+
* it does not hold a consumer while other items are ready. An element that
|
|
24
|
+
* fails to decode is marked failed immediately.
|
|
25
|
+
*
|
|
26
|
+
* `PersistedQueue.layerCleanup` is unsupported because the namespace binding
|
|
27
|
+
* cannot enumerate queue objects. Completed and failed rows remain retained for
|
|
28
|
+
* deduplication. A `deduplicate: "untilExecuted"` offer replaces such a row
|
|
29
|
+
* with a new element, which keeps only the latest failure.
|
|
30
|
+
*
|
|
31
|
+
* @stability experimental
|
|
32
|
+
* @since 4.1.3
|
|
33
|
+
*/
|
|
34
|
+
import * as Cause from "effect/Cause"
|
|
35
|
+
import * as Duration from "effect/Duration"
|
|
36
|
+
import * as Effect from "effect/Effect"
|
|
37
|
+
import * as Exit from "effect/Exit"
|
|
38
|
+
import * as Layer from "effect/Layer"
|
|
39
|
+
import * as PersistedQueue from "effect/persistence/PersistedQueue"
|
|
40
|
+
import * as Schedule from "effect/Schedule"
|
|
41
|
+
import { encodeName } from "./internal/clusterName.ts"
|
|
42
|
+
import type { QueueItem } from "./internal/queueStorage.ts"
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The queue Durable Object namespace binding the store is built from.
|
|
46
|
+
*
|
|
47
|
+
* @stability experimental
|
|
48
|
+
* @category layers
|
|
49
|
+
* @since 4.1.3
|
|
50
|
+
*/
|
|
51
|
+
export interface LayerOptions {
|
|
52
|
+
readonly queueNamespace: DurableObjectNamespace
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface QueueStub {
|
|
56
|
+
readonly offer: (
|
|
57
|
+
id: string,
|
|
58
|
+
element: string,
|
|
59
|
+
delayMillis?: number,
|
|
60
|
+
deduplicate?: "untilExecuted" | "untilRemoved"
|
|
61
|
+
) => Promise<void>
|
|
62
|
+
readonly take: (takerId: string, maxAttempts: number, leaseMillis: number) => Promise<QueueItem>
|
|
63
|
+
readonly cancelTake: (takerId: string) => Promise<void>
|
|
64
|
+
readonly complete: (id: string, takerId: string) => Promise<void>
|
|
65
|
+
readonly fail: (
|
|
66
|
+
id: string,
|
|
67
|
+
lastFailure: string,
|
|
68
|
+
takerId: string,
|
|
69
|
+
terminal?: boolean,
|
|
70
|
+
retryDelayMillis?: number
|
|
71
|
+
) => Promise<void>
|
|
72
|
+
readonly release: (id: string, takerId: string) => Promise<void>
|
|
73
|
+
readonly extend: (id: string, leaseMillis: number, takerId: string) => Promise<void>
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const leaseMillis = 120_000
|
|
77
|
+
const leaseRefreshMillis = 30_000
|
|
78
|
+
|
|
79
|
+
const attempt = (run: () => Promise<void>) =>
|
|
80
|
+
Effect.promise(run).pipe(
|
|
81
|
+
Effect.sandbox,
|
|
82
|
+
Effect.retry({ times: 5, schedule: Schedule.exponential(100, 1.5) })
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
const finalize = (run: () => Promise<void>): Effect.Effect<void> => Effect.orDie(attempt(run))
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Creates the `PersistedQueueStore` backed by the queue Durable Object
|
|
89
|
+
* namespace binding.
|
|
90
|
+
*
|
|
91
|
+
* @stability experimental
|
|
92
|
+
* @category constructors
|
|
93
|
+
* @since 4.1.3
|
|
94
|
+
*/
|
|
95
|
+
export const make = (options: LayerOptions): PersistedQueue.PersistedQueueStore["Service"] => {
|
|
96
|
+
const stubFor = (name: string): QueueStub =>
|
|
97
|
+
options.queueNamespace.getByName(encodeName("PersistedQueue", name)) as unknown as QueueStub
|
|
98
|
+
|
|
99
|
+
return PersistedQueue.PersistedQueueStore.of({
|
|
100
|
+
cleanup: () =>
|
|
101
|
+
Effect.fail(
|
|
102
|
+
new PersistedQueue.PersistedQueueError({
|
|
103
|
+
message:
|
|
104
|
+
"Cloudflare persisted queue cleanup is unsupported: the namespace binding cannot enumerate queue objects"
|
|
105
|
+
})
|
|
106
|
+
),
|
|
107
|
+
offer: ({ deduplicate, delay, element, id, isCustomId, name }) =>
|
|
108
|
+
Effect.tryPromise({
|
|
109
|
+
try: () =>
|
|
110
|
+
stubFor(name).offer(
|
|
111
|
+
id,
|
|
112
|
+
JSON.stringify(element),
|
|
113
|
+
delay === undefined ? 0 : Duration.toMillis(delay),
|
|
114
|
+
// generated ids are never offered twice
|
|
115
|
+
isCustomId ? deduplicate : undefined
|
|
116
|
+
),
|
|
117
|
+
catch: (cause) =>
|
|
118
|
+
new PersistedQueue.PersistedQueueError({
|
|
119
|
+
message: "Failed to offer element to persisted queue",
|
|
120
|
+
cause
|
|
121
|
+
})
|
|
122
|
+
}),
|
|
123
|
+
take: ({ maxAttempts, name, retryDelay }) =>
|
|
124
|
+
// Uninterruptible outside `restore` so the release finalizer is always
|
|
125
|
+
// registered once an item is leased; an interrupt while still waiting
|
|
126
|
+
// cancels the take by taker id, releasing an item that was already
|
|
127
|
+
// leased to it on the object.
|
|
128
|
+
Effect.uninterruptibleMask((restore) =>
|
|
129
|
+
Effect.gen(function*() {
|
|
130
|
+
const takerId = crypto.randomUUID()
|
|
131
|
+
// A broken take RPC means the object was evicted while this taker
|
|
132
|
+
// waited; retrying re-enters the queue with nothing lost.
|
|
133
|
+
const item = yield* restore(
|
|
134
|
+
Effect.promise(() => stubFor(name).take(takerId, maxAttempts, leaseMillis)).pipe(
|
|
135
|
+
Effect.sandbox,
|
|
136
|
+
Effect.tapCause((cause) => Effect.logWarning("PersistedQueue take failed, retrying", cause)),
|
|
137
|
+
Effect.retry(Schedule.spaced(500)),
|
|
138
|
+
Effect.orDie
|
|
139
|
+
)
|
|
140
|
+
).pipe(
|
|
141
|
+
Effect.onInterrupt(() => Effect.ignore(attempt(() => stubFor(name).cancelTake(takerId))))
|
|
142
|
+
)
|
|
143
|
+
yield* Effect.addFinalizer(Exit.match({
|
|
144
|
+
onFailure: (cause) =>
|
|
145
|
+
Cause.hasInterruptsOnly(cause)
|
|
146
|
+
? finalize(() => stubFor(name).release(item.id, takerId))
|
|
147
|
+
: PersistedQueue.isDeadLetterCause(cause)
|
|
148
|
+
? finalize(() => stubFor(name).fail(item.id, Cause.pretty(cause), takerId, true))
|
|
149
|
+
: Effect.flatMap(retryDelay(item.attempts + 1), (delay) =>
|
|
150
|
+
// The delay follows the attempt count including this one.
|
|
151
|
+
finalize(() =>
|
|
152
|
+
stubFor(name).fail(item.id, Cause.pretty(cause), takerId, false, Duration.toMillis(delay))
|
|
153
|
+
)),
|
|
154
|
+
onSuccess: () => finalize(() => stubFor(name).complete(item.id, takerId))
|
|
155
|
+
}))
|
|
156
|
+
yield* Effect.promise(() => stubFor(name).extend(item.id, leaseMillis, takerId)).pipe(
|
|
157
|
+
Effect.sandbox,
|
|
158
|
+
Effect.ignore,
|
|
159
|
+
Effect.schedule(Schedule.spaced(leaseRefreshMillis)),
|
|
160
|
+
Effect.forkScoped,
|
|
161
|
+
Effect.interruptible
|
|
162
|
+
)
|
|
163
|
+
return {
|
|
164
|
+
id: item.id,
|
|
165
|
+
attempts: item.attempts + 1,
|
|
166
|
+
element: JSON.parse(item.element)
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
)
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Layer that provides the `PersistedQueueFactory` backed by the queue Durable
|
|
175
|
+
* Object namespace binding.
|
|
176
|
+
*
|
|
177
|
+
* **Details**
|
|
178
|
+
*
|
|
179
|
+
* `CloudflareCluster.layer` already includes this layer; use it directly only
|
|
180
|
+
* when persisted queues are needed without the rest of the cluster.
|
|
181
|
+
*
|
|
182
|
+
* @stability experimental
|
|
183
|
+
* @category layers
|
|
184
|
+
* @since 4.1.3
|
|
185
|
+
*/
|
|
186
|
+
export const layer = (options: LayerOptions): Layer.Layer<PersistedQueue.PersistedQueueFactory> =>
|
|
187
|
+
PersistedQueue.layer.pipe(
|
|
188
|
+
Layer.provide(Layer.succeed(PersistedQueue.PersistedQueueStore)(make(options)))
|
|
189
|
+
)
|