@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
package/README.md
CHANGED
|
@@ -1,3 +1,506 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @effect/platform-cloudflare
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Runs Effect Cluster on [Cloudflare Durable Objects](https://developers.cloudflare.com/durable-objects/). Every entity instance is one Durable Object, the Worker is the edge, and each object's SQLite storage is the system of record.
|
|
4
|
+
|
|
5
|
+
This package is experimental. Its APIs may change in patch releases.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install effect@rc @effect/platform-cloudflare@rc
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
The package ships four Durable Object classes. Re-export them from your Worker entry module and bind each one as a SQLite-backed class:
|
|
16
|
+
|
|
17
|
+
```jsonc
|
|
18
|
+
// wrangler.jsonc
|
|
19
|
+
{
|
|
20
|
+
"name": "my-worker",
|
|
21
|
+
"main": "src/worker.ts",
|
|
22
|
+
"compatibility_date": "2026-08-01",
|
|
23
|
+
"durable_objects": {
|
|
24
|
+
"bindings": [
|
|
25
|
+
{ "name": "CLUSTER_ENTITY", "class_name": "ClusterEntity" },
|
|
26
|
+
{ "name": "CLUSTER_WORKFLOW", "class_name": "ClusterWorkflow" },
|
|
27
|
+
{ "name": "CLUSTER_QUEUE", "class_name": "ClusterDurableQueue" },
|
|
28
|
+
{ "name": "CLUSTER_SINGLETON", "class_name": "ClusterSingleton" },
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
"migrations": [
|
|
32
|
+
{
|
|
33
|
+
"tag": "v1",
|
|
34
|
+
"new_sqlite_classes": [
|
|
35
|
+
"ClusterEntity",
|
|
36
|
+
"ClusterWorkflow",
|
|
37
|
+
"ClusterDurableQueue",
|
|
38
|
+
"ClusterSingleton",
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
"triggers": {
|
|
43
|
+
"crons": ["0 * * * *"],
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
// src/worker.ts
|
|
50
|
+
import { CloudflareCluster, CloudflareDurableObjects } from "@effect/platform-cloudflare"
|
|
51
|
+
import { Effect, Layer, ManagedRuntime, Schema } from "effect"
|
|
52
|
+
import { Entity, Singleton } from "effect/cluster"
|
|
53
|
+
import { Rpc } from "effect/rpc"
|
|
54
|
+
|
|
55
|
+
export {
|
|
56
|
+
ClusterDurableQueue,
|
|
57
|
+
ClusterEntity,
|
|
58
|
+
ClusterSingleton,
|
|
59
|
+
ClusterWorkflow
|
|
60
|
+
} from "@effect/platform-cloudflare/CloudflareDurableObjects"
|
|
61
|
+
|
|
62
|
+
// The same Entity + RpcGroup definitions as on every other cluster path
|
|
63
|
+
const Counter = Entity.make("Counter", [
|
|
64
|
+
Rpc.make("Increment", { success: Schema.Number })
|
|
65
|
+
])
|
|
66
|
+
|
|
67
|
+
const CounterLayer = Counter.toLayer({
|
|
68
|
+
Increment: () => Effect.succeed(1)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const MaintenanceLayer = Singleton.make(
|
|
72
|
+
"hourly-maintenance",
|
|
73
|
+
Effect.logInfo("Running hourly maintenance")
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
const clusterLayer = (env: Env) =>
|
|
77
|
+
Layer.merge(CounterLayer, MaintenanceLayer).pipe(
|
|
78
|
+
Layer.provideMerge(CloudflareCluster.layer({
|
|
79
|
+
entities: [Counter],
|
|
80
|
+
entityNamespace: env.CLUSTER_ENTITY,
|
|
81
|
+
workflowNamespace: env.CLUSTER_WORKFLOW,
|
|
82
|
+
queueNamespace: env.CLUSTER_QUEUE,
|
|
83
|
+
singletonNamespace: env.CLUSTER_SINGLETON
|
|
84
|
+
}))
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
// Build registration lazily inside the first Worker or Durable Object event.
|
|
88
|
+
// Each call builds a fresh ManagedRuntime, so a retry after a failed or
|
|
89
|
+
// stalled build starts over. The first runtime to finish is kept for the
|
|
90
|
+
// lifetime of this Worker isolate.
|
|
91
|
+
let application: ManagedRuntime.ManagedRuntime<any, any> | undefined
|
|
92
|
+
const initialize = CloudflareDurableObjects.setInitializer(async (env: Env) => {
|
|
93
|
+
if (application !== undefined) return application.context()
|
|
94
|
+
const runtime = ManagedRuntime.make(clusterLayer(env))
|
|
95
|
+
const context = await runtime.context().catch(async (error) => {
|
|
96
|
+
await runtime.dispose()
|
|
97
|
+
throw error
|
|
98
|
+
})
|
|
99
|
+
if (application === undefined) {
|
|
100
|
+
application = runtime
|
|
101
|
+
return context
|
|
102
|
+
}
|
|
103
|
+
// A stalled build finished after a replacement: keep the replacement.
|
|
104
|
+
await runtime.dispose()
|
|
105
|
+
return application.context()
|
|
106
|
+
})
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The initializer is the readiness contract shared by all four Durable Object
|
|
110
|
+
classes. Each cold object waits for it through `blockConcurrencyWhile` before
|
|
111
|
+
handling a request or alarm, and concurrent cold objects still build the
|
|
112
|
+
application only once per Worker isolate. If initialization fails, the failure
|
|
113
|
+
is returned to every caller waiting on that build, and the next cold object
|
|
114
|
+
runs the initializer again. Because the initializer runs inside a Durable
|
|
115
|
+
Object constructor's `blockConcurrencyWhile`, which Cloudflare cancels after 30
|
|
116
|
+
seconds, it must complete within 15 seconds. A build still pending after 15
|
|
117
|
+
seconds is treated as abandoned. Only an object that has waited less than 15
|
|
118
|
+
seconds runs the initializer itself, whether it replaces a stalled build or
|
|
119
|
+
retries after a failure, so any replacement also gets at least 15
|
|
120
|
+
seconds. A slow build can therefore run more than once in an isolate, and the
|
|
121
|
+
initializer must tolerate that. The example above keeps the first runtime to
|
|
122
|
+
finish and disposes any other.
|
|
123
|
+
Worker request handlers should call the same `initialize(env)` function before
|
|
124
|
+
using `application`.
|
|
125
|
+
|
|
126
|
+
Applications whose registrations are complete synchronously during module
|
|
127
|
+
evaluation can keep the previous bare re-export pattern without an initializer.
|
|
128
|
+
Asynchronous layer builds must register one before serving traffic.
|
|
129
|
+
|
|
130
|
+
The Cron Trigger wakes the named singleton through its same-Worker binding.
|
|
131
|
+
The call returns after one run, allowing the Durable Object to hibernate; do
|
|
132
|
+
not make the singleton effect a forever loop. A failed run is logged by the
|
|
133
|
+
singleton object and resolves `wake()` with `{ _tag: "Failure", message }`, so
|
|
134
|
+
a rejected `wake()` means the call failed in transit and can be retried.
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
export default {
|
|
138
|
+
scheduled(_controller: ScheduledController, env: Env, ctx: ExecutionContext) {
|
|
139
|
+
const singleton = env.CLUSTER_SINGLETON.getByName("Singleton/hourly-maintenance")
|
|
140
|
+
ctx.waitUntil(singleton.wake())
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`Entity.client` stays the user API. The Worker encodes `(type, id)` into the Durable Object name and resolves the object with `getByName`; an unknown entity type fails at the Worker before any Durable Object is contacted.
|
|
146
|
+
|
|
147
|
+
### Waking a `ClusterCron`
|
|
148
|
+
|
|
149
|
+
`ClusterCron.make` registers a seed singleton named `ClusterCron/<name>` that
|
|
150
|
+
schedules the first run. On the other cluster backends that singleton starts as
|
|
151
|
+
soon as a runner holds its shard. Here a singleton runs only when something
|
|
152
|
+
wakes it, and nothing seeds it for you, so a `ClusterCron` never fires until a
|
|
153
|
+
Cron Trigger wakes its seed singleton.
|
|
154
|
+
|
|
155
|
+
`CloudflareCluster.clusterCronSingletonName(name)` returns that singleton name.
|
|
156
|
+
After the first wake each run schedules the next one through a destination
|
|
157
|
+
alarm. Waking it on every trigger is safe: a run already scheduled for the same
|
|
158
|
+
instant is deduplicated, and the wake restarts a schedule that was lost.
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
const NightlyReport = ClusterCron.make({
|
|
162
|
+
name: "nightly-report",
|
|
163
|
+
cron: Cron.parseUnsafe("0 2 * * *", "UTC"),
|
|
164
|
+
execute: Effect.logInfo("Sending the nightly report")
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
export default {
|
|
168
|
+
scheduled(_controller: ScheduledController, env: Env, ctx: ExecutionContext) {
|
|
169
|
+
const name = CloudflareCluster.clusterCronSingletonName("nightly-report")
|
|
170
|
+
ctx.waitUntil(env.CLUSTER_SINGLETON.getByName(`Singleton/${name}`).wake())
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Merge `NightlyReport` into the layer passed to `CloudflareCluster.layer`, like
|
|
176
|
+
any other singleton.
|
|
177
|
+
|
|
178
|
+
## Deploying with Alchemy
|
|
179
|
+
|
|
180
|
+
[Alchemy](https://alchemy.run) v2 ("Infrastructure as Effects") is the second
|
|
181
|
+
deployment story, behind the optional `@alchemy.run/cloudflare` peer dependency
|
|
182
|
+
(`>=2.0.0-beta.85 <3`). The Wrangler path above never imports it.
|
|
183
|
+
|
|
184
|
+
```sh
|
|
185
|
+
npm install alchemy@beta @alchemy.run/cloudflare@beta
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
With Alchemy there is no wrangler config, no class re-exports, and no
|
|
189
|
+
initializer: `AlchemyCloudflareCluster.make` runs inside the Effect-native
|
|
190
|
+
`Cloudflare.Worker` init program and registers the four Durable Object
|
|
191
|
+
classes itself, so Alchemy owns the bindings, class exports, and SQLite
|
|
192
|
+
migrations across deploys. Cron Triggers stay user-declared through
|
|
193
|
+
`cluster.wake`:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
// src/worker.ts
|
|
197
|
+
import * as Cloudflare from "@alchemy.run/cloudflare"
|
|
198
|
+
import * as AlchemyCloudflareCluster from "@effect/platform-cloudflare/AlchemyCloudflareCluster"
|
|
199
|
+
import * as CloudflareCluster from "@effect/platform-cloudflare/CloudflareCluster"
|
|
200
|
+
import * as Effect from "effect/Effect"
|
|
201
|
+
import * as Layer from "effect/Layer"
|
|
202
|
+
|
|
203
|
+
export default Cloudflare.Worker(
|
|
204
|
+
"MyApp",
|
|
205
|
+
{
|
|
206
|
+
main: import.meta.url
|
|
207
|
+
},
|
|
208
|
+
Effect.gen(function*() {
|
|
209
|
+
const cluster = yield* AlchemyCloudflareCluster.make({
|
|
210
|
+
entities: [Counter],
|
|
211
|
+
layer: Layer.mergeAll(CounterLayer, MaintenanceLayer, NightlyReport)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
yield* Cloudflare.Workers.cron("0 * * * *", cluster.wake("hourly-maintenance"))
|
|
215
|
+
// A ClusterCron fires only after a Cron Trigger wakes its seed singleton
|
|
216
|
+
yield* Cloudflare.Workers.cron(
|
|
217
|
+
"30 * * * *",
|
|
218
|
+
cluster.wake(CloudflareCluster.clusterCronSingletonName("nightly-report"))
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
// routes using Entity.client, etc.
|
|
223
|
+
fetch: cluster.provide(handler)
|
|
224
|
+
}
|
|
225
|
+
}).pipe(Effect.provide(Cloudflare.Workers.CronEventSourceLive))
|
|
226
|
+
)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
// alchemy.run.ts
|
|
231
|
+
import * as Cloudflare from "@alchemy.run/cloudflare"
|
|
232
|
+
import * as Alchemy from "alchemy"
|
|
233
|
+
import * as Effect from "effect/Effect"
|
|
234
|
+
import App from "./src/worker.ts"
|
|
235
|
+
|
|
236
|
+
export default Alchemy.Stack(
|
|
237
|
+
"MyApp",
|
|
238
|
+
{ providers: Cloudflare.providers(), state: Cloudflare.state() },
|
|
239
|
+
Effect.gen(function*() {
|
|
240
|
+
const app = yield* App
|
|
241
|
+
return { url: app.url }
|
|
242
|
+
})
|
|
243
|
+
)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
`make` builds the cluster layer merged with your handler layer into the
|
|
247
|
+
isolate-lifetime scope and hands back `provide` for the Worker's handlers, so
|
|
248
|
+
do not wrap the init program in `Effect.provide` for cluster services — that
|
|
249
|
+
would tear the layer down when init returns. The handle also exposes the four
|
|
250
|
+
native namespace bindings (`entityNamespace`, `workflowNamespace`,
|
|
251
|
+
`queueNamespace`, `singletonNamespace`) as escape hatches. These bindings and
|
|
252
|
+
`context` are runtime-only; reading them during plan evaluation throws a
|
|
253
|
+
diagnostic. During planning, `provide` passes through its Effect and `wake`
|
|
254
|
+
does nothing.
|
|
255
|
+
|
|
256
|
+
A complete runnable example (one entity, one singleton, one Cron Trigger)
|
|
257
|
+
lives at [`examples/alchemy`](./examples/alchemy).
|
|
258
|
+
|
|
259
|
+
### Manual live smoke
|
|
260
|
+
|
|
261
|
+
The live deployment smoke has not yet been verified.
|
|
262
|
+
|
|
263
|
+
Alchemy tracks the published `effect` release, so CI covers this integration
|
|
264
|
+
with typechecks only (plus the manual `Alchemy Canary` workflow against the
|
|
265
|
+
latest `alchemy@beta`). After changing the integration, run one live smoke
|
|
266
|
+
with Cloudflare credentials configured:
|
|
267
|
+
|
|
268
|
+
1. `cd packages/platform/cloudflare/examples/alchemy`
|
|
269
|
+
2. `alchemy deploy` — expect the Worker plus the four SQLite-backed Durable
|
|
270
|
+
Object classes (`ClusterEntity`, `ClusterWorkflow`, `ClusterDurableQueue`,
|
|
271
|
+
`ClusterSingleton`) and the `0 * * * *` Cron Trigger.
|
|
272
|
+
3. `curl https://<worker-url>/counter-1` twice — expect `1` then `2`.
|
|
273
|
+
4. Trigger the cron (or wait for the top of the hour) and check the Worker
|
|
274
|
+
logs for `Running hourly maintenance`.
|
|
275
|
+
5. `alchemy destroy` to clean up.
|
|
276
|
+
|
|
277
|
+
## Worker routes
|
|
278
|
+
|
|
279
|
+
The existing `EntityProxy` / `EntityProxyServer` and `WorkflowProxy` /
|
|
280
|
+
`WorkflowProxyServer` modules remain the route helpers. Define an HTTP or RPC
|
|
281
|
+
surface with the proxy module, then provide its server layer with
|
|
282
|
+
`CloudflareCluster.layer`. Entity proxy handlers call `Entity.client`, and
|
|
283
|
+
workflow proxy handlers call the workflow API, so the Cloudflare layers encode
|
|
284
|
+
the entity or workflow name and resolve the corresponding Durable Object stub.
|
|
285
|
+
There is no runner-fleet proxy on this path.
|
|
286
|
+
|
|
287
|
+
These are Worker routes, not Durable Object routes. The Durable Object classes
|
|
288
|
+
are internal transport: they trust the same-Worker namespace bindings and must
|
|
289
|
+
not be exposed on a public route. HTTP or RPC authentication and authorization
|
|
290
|
+
are user code on the Worker.
|
|
291
|
+
|
|
292
|
+
## Long waits and delivery
|
|
293
|
+
|
|
294
|
+
- A long ask pins the caller. A delayed ask made directly by a Worker also
|
|
295
|
+
keeps the destination RPC open, so it pins the destination too.
|
|
296
|
+
- Caller eviction or deployment drops the in-memory wait even though the
|
|
297
|
+
destination may still run the persisted request.
|
|
298
|
+
- Prefer a tell when no response is needed. For durable long waits, prefer a
|
|
299
|
+
workflow with `DurableClock` and `DurableDeferred`.
|
|
300
|
+
- Stream asks with a future `DeliverAt` are outside v1.
|
|
301
|
+
|
|
302
|
+
## Handler concurrency
|
|
303
|
+
|
|
304
|
+
`Entity.toLayer(..., { concurrency })` applies inside the entity Durable
|
|
305
|
+
Object. The default of 1 runs one handler at a time, a number allows that many
|
|
306
|
+
in-flight handlers per entity, and `"unbounded"` removes the limit. Durable
|
|
307
|
+
Object isolates are single-threaded, so this is interleaving of suspended
|
|
308
|
+
handlers, not parallelism.
|
|
309
|
+
|
|
310
|
+
- Envelope decode, persist-before-run, dedupe, duplicate resume, and alarm
|
|
311
|
+
arming stay serialized at any setting.
|
|
312
|
+
- With `concurrency` above 1, strict mailbox ordering holds per permit, the
|
|
313
|
+
same as the classic runner path: in-flight handlers interleave at every
|
|
314
|
+
suspension point.
|
|
315
|
+
- An ask cycle (entity A asks B while B's handler asks A back) needs
|
|
316
|
+
`concurrency` of at least 2 on the entity receiving the second ask. At the
|
|
317
|
+
default of 1 the cycle deadlocks, matching the classic contract.
|
|
318
|
+
- Replayed mailbox rows and alarm-due runs draw from the same budget as live
|
|
319
|
+
requests.
|
|
320
|
+
|
|
321
|
+
`defectRetryPolicy` and `spanAttributes` also apply inside the entity Durable
|
|
322
|
+
Object. `disableFatalDefects`, `maxIdleTime`, and `mailboxCapacity` are
|
|
323
|
+
accepted for parity and have no effect. Hibernation owns idle entities, and the
|
|
324
|
+
mailbox capacity is fixed at 4096.
|
|
325
|
+
|
|
326
|
+
Defects on this path are always request-local, as if `disableFatalDefects`
|
|
327
|
+
were `true`. A handler defect is retried under `defectRetryPolicy` when one is
|
|
328
|
+
set, then the request gets a terminal defect reply and the handlers are
|
|
329
|
+
rebuilt. Other in-flight requests finish on the build they started on and are
|
|
330
|
+
not replayed.
|
|
331
|
+
|
|
332
|
+
## Entity SQL storage
|
|
333
|
+
|
|
334
|
+
Each entity Durable Object provides `CloudflareCluster.DurableObjectSqlClient`
|
|
335
|
+
to its handler build and handlers: an `SqlClient` on the object's own SQLite
|
|
336
|
+
storage. It is a separate tag, so an app-wide `SqlClient` from the Worker layer
|
|
337
|
+
(a D1 database, say) keeps working inside the handlers.
|
|
338
|
+
|
|
339
|
+
Register the handlers with `Entity.toLayer` and read the client with
|
|
340
|
+
`CloudflareCluster.DurableObjectSqlClient.orDie` in the build. `orDie` has no
|
|
341
|
+
requirements, so the layer needs nothing extra. Yielding
|
|
342
|
+
`DurableObjectSqlClient` directly would leave it as a layer requirement.
|
|
343
|
+
Handlers registered with `Entity.toLayerQueue` get the client the same way.
|
|
344
|
+
|
|
345
|
+
```ts
|
|
346
|
+
import { CloudflareCluster } from "@effect/platform-cloudflare"
|
|
347
|
+
import { Effect, Schema } from "effect"
|
|
348
|
+
import { Entity } from "effect/cluster"
|
|
349
|
+
import { Rpc } from "effect/rpc"
|
|
350
|
+
|
|
351
|
+
const Journal = Entity.make("Journal", [
|
|
352
|
+
Rpc.make("Append", { payload: { entries: Schema.Array(Schema.String) } }),
|
|
353
|
+
Rpc.make("List", { success: Schema.Array(Schema.String) })
|
|
354
|
+
])
|
|
355
|
+
|
|
356
|
+
const build = Effect.gen(function*() {
|
|
357
|
+
const sql = yield* CloudflareCluster.DurableObjectSqlClient.orDie
|
|
358
|
+
yield* sql`CREATE TABLE IF NOT EXISTS journal (seq INTEGER PRIMARY KEY AUTOINCREMENT, entry TEXT NOT NULL)`
|
|
359
|
+
return Journal.of({
|
|
360
|
+
// Every entry commits together, or none do.
|
|
361
|
+
Append: ({ payload }) =>
|
|
362
|
+
sql.withTransaction(
|
|
363
|
+
Effect.forEach(payload.entries, (entry) => sql`INSERT INTO journal (entry) VALUES (${entry})`)
|
|
364
|
+
).pipe(Effect.asVoid, Effect.orDie),
|
|
365
|
+
List: () =>
|
|
366
|
+
sql<{ readonly entry: string }>`SELECT entry FROM journal ORDER BY seq`.pipe(
|
|
367
|
+
Effect.map((rows) => rows.map((row) => row.entry)),
|
|
368
|
+
Effect.orDie
|
|
369
|
+
)
|
|
370
|
+
})
|
|
371
|
+
}).pipe(Effect.orDie)
|
|
372
|
+
|
|
373
|
+
const JournalLayer = Journal.toLayer(build)
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
To run code written against `SqlClient.SqlClient`, such as `SqlSchema` or
|
|
377
|
+
`Model.makeRepository`, provide the entity client as that service locally:
|
|
378
|
+
`Effect.provideService(effect, SqlClient.SqlClient, sql)`.
|
|
379
|
+
|
|
380
|
+
- `orDie` dies on the first handler build when no `DurableObjectSqlClient` is
|
|
381
|
+
provided, such as outside an entity Durable Object or in a test without a
|
|
382
|
+
SQL layer.
|
|
383
|
+
- Rows live and die with the entity's Durable Object. They cannot be queried
|
|
384
|
+
from outside it or from another entity.
|
|
385
|
+
- Tables whose names start with `cluster_` are reserved. The entity mailbox
|
|
386
|
+
uses `cluster_messages` and `cluster_replies`, and `reset` / `clearReplies`
|
|
387
|
+
only touch those tables.
|
|
388
|
+
- A row cannot exceed 2 MB, the same SQLite ceiling as an encoded message.
|
|
389
|
+
- A storage transaction closes the Durable Object's input gate until it
|
|
390
|
+
finishes. Timers and I/O awaited inside `withTransaction` never resume, so
|
|
391
|
+
keep transactions to SQL work.
|
|
392
|
+
- Mailbox writes wait for an open transaction to finish, so a rollback never
|
|
393
|
+
takes another handler's stored reply with it.
|
|
394
|
+
|
|
395
|
+
To test the handlers, run the same layer with `Entity.makeTestClient`,
|
|
396
|
+
providing `DurableObjectSqlClient` from any SQL layer, for example an
|
|
397
|
+
in-memory `@effect/sql-sqlite-node` client:
|
|
398
|
+
|
|
399
|
+
```ts
|
|
400
|
+
import { SqliteClient } from "@effect/sql-sqlite-node"
|
|
401
|
+
import { Effect, Layer } from "effect"
|
|
402
|
+
import { Entity, ShardingConfig } from "effect/cluster"
|
|
403
|
+
import { SqlClient } from "effect/sql"
|
|
404
|
+
|
|
405
|
+
const EntitySqlTest = Layer.effect(
|
|
406
|
+
CloudflareCluster.DurableObjectSqlClient,
|
|
407
|
+
Effect.service(SqlClient.SqlClient)
|
|
408
|
+
).pipe(Layer.provide(SqliteClient.layer({ filename: ":memory:" })))
|
|
409
|
+
|
|
410
|
+
const program = Effect.gen(function*() {
|
|
411
|
+
const makeClient = yield* Entity.makeTestClient(Journal, JournalLayer)
|
|
412
|
+
const journal = yield* makeClient("one")
|
|
413
|
+
yield* journal.Append({ entries: ["a", "b"] })
|
|
414
|
+
return yield* journal.List()
|
|
415
|
+
}).pipe(
|
|
416
|
+
Effect.scoped,
|
|
417
|
+
Effect.provide(Layer.merge(ShardingConfig.layer(), EntitySqlTest))
|
|
418
|
+
)
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
## Restarts and recovery
|
|
422
|
+
|
|
423
|
+
A deploy, a runtime update, or an eviction kills the entity Durable Object.
|
|
424
|
+
Cloudflare does not re-create it by itself: the object stays cold until a
|
|
425
|
+
request, an alarm, or a WebSocket event arrives. To recover without outside
|
|
426
|
+
contact, the entity keeps an alarm in storage while it has something to come
|
|
427
|
+
back for:
|
|
428
|
+
|
|
429
|
+
- While it holds `Entity.keepAlive`, the alarm re-arms every
|
|
430
|
+
`keepAliveHeartbeat` interval (30 seconds by default, set on
|
|
431
|
+
`CloudflareCluster.layer` or `AlchemyCloudflareCluster.make`). After a
|
|
432
|
+
restart the alarm rebuilds the entity, which runs its handler build, and
|
|
433
|
+
restores the hold. `Entity.keepAlive(false)` ends it.
|
|
434
|
+
- While a persisted request runs, the alarm stays armed one interval ahead.
|
|
435
|
+
After a restart it replays every unprocessed row, so an interrupted request
|
|
436
|
+
runs again without a new message. Replay is alarm-driven; any later request
|
|
437
|
+
to the entity replays the rows too.
|
|
438
|
+
|
|
439
|
+
The interval is recovery latency, not a liveness check. At 30 seconds a
|
|
440
|
+
kept-alive entity costs about 2,880 alarm requests a day, which is small next
|
|
441
|
+
to the duration billing of the held object itself.
|
|
442
|
+
|
|
443
|
+
Things to plan for:
|
|
444
|
+
|
|
445
|
+
- After a deploy, kept-alive entities keep running the old code for about 5
|
|
446
|
+
minutes, then die without warning. Schema changes must tolerate old code
|
|
447
|
+
still running against the same storage.
|
|
448
|
+
- Recovery after a deploy takes about those 5 minutes plus one interval. The
|
|
449
|
+
hold cannot see the deploy; only the heartbeat brings the entity back.
|
|
450
|
+
- The keep-alive flag is persisted, the holder count is not. Each handler
|
|
451
|
+
build counts its own holders, so two `EntityResource`s need two releases.
|
|
452
|
+
Classic cluster drops the hold when the first one releases.
|
|
453
|
+
- An `EntityResource` that is not acquired eagerly in the handler build
|
|
454
|
+
leaves the object pinned after a restart until a later
|
|
455
|
+
`Entity.keepAlive(false)`. Classic cluster behaves the same.
|
|
456
|
+
- A deploy that removes an entity type leaves its heartbeat behind. The alarm
|
|
457
|
+
fails, Cloudflare retries it 6 times, and the chain stops; the stored
|
|
458
|
+
keep-alive flag is never cleaned up.
|
|
459
|
+
|
|
460
|
+
## v1 compatibility
|
|
461
|
+
|
|
462
|
+
The status vocabulary is **maps 1:1**, **adapted**, and **out of scope**.
|
|
463
|
+
|
|
464
|
+
| Capability | Status | Rationale |
|
|
465
|
+
| ----------------------------------------------------------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------- |
|
|
466
|
+
| `Entity` + `RpcGroup` definition | adapted | Same definition; handlers register at Worker init onto one shared Durable Object class |
|
|
467
|
+
| `Entity.client` / location-transparent ask-tell | adapted | Worker encodes `(type, id)` and calls `getByName`; there is no `ShardId` routing |
|
|
468
|
+
| `EntityProxy` / `EntityProxyServer` and workflow equivalents | adapted | Worker route helpers encode the name and stub the Durable Object through the Cloudflare layers |
|
|
469
|
+
| Non-`Persisted` RPC | adapted | Best-effort in-request only; it can be lost on hibernation or a crash |
|
|
470
|
+
| `Persisted` ask/tell + mailbox | adapted | Per-entity Durable Object SQLite, persist-before-run, and uuidv7 request ids |
|
|
471
|
+
| `PrimaryKey` dedupe / `Duplicate` resume | maps 1:1 | Same contract |
|
|
472
|
+
| Stream ask `Chunk` / `AckChunk` / `lastSentChunk` / `WithExit` | maps 1:1 | Same reply protocol on Durable Object storage |
|
|
473
|
+
| `clearReplies` / `reset` | maps 1:1 | Same re-run semantics |
|
|
474
|
+
| `DeliverAt` mailbox delivery | adapted | Destination due column and alarm instead of storage polling |
|
|
475
|
+
| Ask + future `DeliverAt` | adapted | Destination may hibernate through `replyTo`; ask pins its caller, and a Worker ask pins the destination too |
|
|
476
|
+
| Worker to Durable Object transport failures | adapted | Typed `PersistenceError`; persisted requests retry retryable errors on a fresh stub, others fail at once |
|
|
477
|
+
| `MailboxFull` / 4096 cap / 2 MB row rejection | maps 1:1 | Same limits; the SQLite row is the hard ceiling |
|
|
478
|
+
| `defectRetryPolicy` then terminal defect | adapted | Rebuilds handlers in the wake; crash or deployment wipes memory and replays unprocessed rows |
|
|
479
|
+
| `Entity.toLayer` `concurrency` | maps 1:1 | Same per-entity handler interleaving contract; storage entry stays serialized at any setting |
|
|
480
|
+
| `Entity.keepAlive` | maps 1:1 | Survives restarts through a heartbeat alarm; the holder count is per handler build, not persisted |
|
|
481
|
+
| `CurrentRunnerAddress` | adapted | Synthetic address for identity and telemetry; no peer dialing |
|
|
482
|
+
| `EntityResource.make` | adapted | External lifetimes such as a browser; close or idle TTL unpins |
|
|
483
|
+
| `DurableObjectSqlClient` in entity handlers (`Entity.toLayer` + `orDie`) | adapted | The entity Durable Object's own SQLite; `cluster_` tables reserved, 2 MB rows, transactions per object |
|
|
484
|
+
| `EntityResource.makeK8sPod` | out of scope | Requires `K8sHttpClient` |
|
|
485
|
+
| `Workflow` / `Activity` / `DurableDeferred` user APIs | maps 1:1 | Unchanged; the engine behind them changes |
|
|
486
|
+
| `CloudflareWorkflowEngine` (`WorkflowEngine.Encoded`) | adapted | Dedicated workflow Durable Object, SQLite, and one alarm |
|
|
487
|
+
| `DurableClock` | adapted | Always durable; there is no short in-memory timer path |
|
|
488
|
+
| `DurableQueue` | adapted | One Durable Object per queue name with SQLite and an alarm watchdog |
|
|
489
|
+
| `PersistedQueue.layerCleanup` | out of scope | Fails explicitly; namespace bindings cannot enumerate queue objects. Completed rows are retained for dedup. |
|
|
490
|
+
| `Singleton` | adapted | Named Durable Object; runs once per wake and then may hibernate |
|
|
491
|
+
| `ClusterCron` | adapted | Per-fire entity ids, `DeliverAt` destination alarms, and a singleton seed |
|
|
492
|
+
| Address `(EntityType, EntityId)` | adapted | Length-prefixed Durable Object name; cold first contact is normal |
|
|
493
|
+
| `Sharding.getSnowflake` | out of scope | Dies; isolates have no fleet-unique machine id. Request ids are uuidv7 |
|
|
494
|
+
| `ShardId` / shard locks / runner ring | out of scope | One-instance-per-id replaces ownership |
|
|
495
|
+
| `MessageStorage` / `RunnerStorage` / `RunnerHealth` / `Runners` as user seams | out of scope | The Durable Object path owns persistence and alarms internally |
|
|
496
|
+
| `HttpRunner` / `SocketRunner` / peer runner fleet | out of scope | Worker edge only |
|
|
497
|
+
| `EntityReaper` / `maxIdleTime` | out of scope | Cloudflare hibernation owns sleep; `keepAlive` holders provide pinning |
|
|
498
|
+
| Activate/deactivate / shard handoff / `EntityNotAssignedToRunner` | out of scope | Whole-wake handlers with no handoff |
|
|
499
|
+
| Park-and-replay caller hibernation | out of scope | Not part of v1 |
|
|
500
|
+
| Stream ask + future `DeliverAt` | out of scope | Delayed asks support non-stream `WithExit` only |
|
|
501
|
+
| External SQL as the system of record | out of scope | Durable Object SQLite is the system of record |
|
|
502
|
+
| Non-Worker long-lived runners as a first-class edge | out of scope | The Worker is the supported edge model |
|
|
503
|
+
|
|
504
|
+
## Documentation
|
|
505
|
+
|
|
506
|
+
- [Effect website](https://effect.website)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# AI docs
|
|
2
|
+
|
|
3
|
+
`LLMS.md` is generated from `ai-docs/src`.
|
|
4
|
+
|
|
5
|
+
## Add content
|
|
6
|
+
|
|
7
|
+
1. Add or update markdown in `ai-docs/src/**/index.md` for section intro text.
|
|
8
|
+
2. Add examples as `.ts` files in the same folder.
|
|
9
|
+
3. Run `pnpm ai-docgen` to regenerate `LLMS.md`.
|
|
10
|
+
|
|
11
|
+
## Source file conventions
|
|
12
|
+
|
|
13
|
+
- Use numeric filename prefixes to control ordering (`10_`, `20_`, etc). Avoid starting with `0` unless explicity requested to do so.
|
|
14
|
+
- Use a top JSDoc block with `@title` and optional description to control rendered title/description.
|
|
15
|
+
- `fixtures` directories are ignored and not included in the generated
|
|
16
|
+
documentation. Use them for any supporting code or data needed for examples.
|
|
17
|
+
|
|
18
|
+
## Example guidelines
|
|
19
|
+
|
|
20
|
+
Before writing an example, look at the existing examples in `ai-docs/src` to
|
|
21
|
+
learn the style and conventions used in this project. Also read the current
|
|
22
|
+
`LLMS.md` to understand the content and style of the documentation.
|
|
23
|
+
|
|
24
|
+
**All code examples should be well commented** explaining the how and why of the
|
|
25
|
+
code, not just what the code is doing. The goal is to teach users how to use the
|
|
26
|
+
API.
|
|
27
|
+
|
|
28
|
+
**Code must represent real world usage and best practices.**
|
|
29
|
+
Do not include toy examples that are not representative of how the API should be
|
|
30
|
+
used in practice.
|
|
31
|
+
|
|
32
|
+
- **Prefer using the service style** of structuring code, as this represents real
|
|
33
|
+
world usage and best practices.
|
|
34
|
+
- Use the `fixtures` directory to illustrate / suggest best practices for
|
|
35
|
+
project structure, file organization, and code organization.
|
|
36
|
+
|
|
37
|
+
Pull requests with only ai documentation changes **DO NOT** need a changeset.
|
|
38
|
+
|
|
39
|
+
## Regeneration
|
|
40
|
+
|
|
41
|
+
- One-shot: `pnpm ai-docgen`
|
|
42
|
+
- Watch mode: `pnpm ai-docgen:watch`
|
|
43
|
+
|
|
44
|
+
`pnpm ai-docgen` regenerates `LLMS.md` files from content in `ai-docs/src`.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ai-docs",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"version": "0.0.0",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@effect/ai-anthropic": "workspace:*",
|
|
8
|
+
"@effect/ai-openai": "workspace:*",
|
|
9
|
+
"@effect/ai-openai-compat": "workspace:*",
|
|
10
|
+
"@effect/ai-openrouter": "workspace:*",
|
|
11
|
+
"@effect/atom-react": "workspace:*",
|
|
12
|
+
"@effect/atom-solid": "workspace:*",
|
|
13
|
+
"@effect/atom-vue": "workspace:*",
|
|
14
|
+
"@effect/opentelemetry": "workspace:*",
|
|
15
|
+
"@effect/platform-bun": "workspace:*",
|
|
16
|
+
"@effect/platform-node": "workspace:*",
|
|
17
|
+
"@effect/sql-clickhouse": "workspace:*",
|
|
18
|
+
"@effect/sql-d1": "workspace:*",
|
|
19
|
+
"@effect/sql-libsql": "workspace:*",
|
|
20
|
+
"@effect/sql-mssql": "workspace:*",
|
|
21
|
+
"@effect/sql-mysql2": "workspace:*",
|
|
22
|
+
"@effect/sql-pg": "workspace:*",
|
|
23
|
+
"@effect/sql-sqlite-bun": "workspace:*",
|
|
24
|
+
"@effect/sql-sqlite-do": "workspace:*",
|
|
25
|
+
"@effect/sql-sqlite-node": "workspace:*",
|
|
26
|
+
"@effect/sql-sqlite-react-native": "workspace:*",
|
|
27
|
+
"@effect/sql-sqlite-wasm": "workspace:*",
|
|
28
|
+
"@effect/vitest": "workspace:*",
|
|
29
|
+
"effect": "workspace:*",
|
|
30
|
+
"hono": "^4.13.17",
|
|
31
|
+
"nodemailer": "^10.3.1"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/nodemailer": "^8.0.2"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Using Effect.gen
|
|
3
|
+
*
|
|
4
|
+
* Use `Effect.gen` to write code in an imperative style similar to async await.
|
|
5
|
+
* You can use `yield*` to access the result of an effect.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Effect, Schema } from "effect"
|
|
9
|
+
|
|
10
|
+
Effect.gen(function*() {
|
|
11
|
+
yield* Effect.log("Starting the file processing...")
|
|
12
|
+
yield* Effect.log("Reading file...")
|
|
13
|
+
|
|
14
|
+
// Always return when raising an error, to ensure typescript understands that
|
|
15
|
+
// the function will not continue executing.
|
|
16
|
+
return yield* new FileProcessingError({ message: "Failed to read the file" })
|
|
17
|
+
}).pipe(
|
|
18
|
+
// Add additional functionality with .pipe
|
|
19
|
+
Effect.catch((error) => Effect.logError(`An error occurred: ${error}`)),
|
|
20
|
+
Effect.withSpan("fileProcessing", {
|
|
21
|
+
attributes: {
|
|
22
|
+
method: "Effect.gen"
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
// Use Schema.TaggedError to define a custom error
|
|
28
|
+
export class FileProcessingError extends Schema.TaggedError<FileProcessingError>()("FileProcessingError", {
|
|
29
|
+
message: Schema.String
|
|
30
|
+
}) {}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Using Effect.fn and Effect.fnUntraced
|
|
3
|
+
*
|
|
4
|
+
* When writing reusable functions that return an Effect, use `Effect.fn` or
|
|
5
|
+
* `Effect.fnUntraced` to use the generator syntax.
|
|
6
|
+
*
|
|
7
|
+
* Use `Effect.fn("name")` when the function should create a tracing span. Prefer
|
|
8
|
+
* `Effect.fnUntraced` when tracing is not needed, particularly for library
|
|
9
|
+
* implementations and hot paths.
|
|
10
|
+
*
|
|
11
|
+
* **Avoid creating functions that only wrap and return an `Effect.gen`**.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { Effect, Schema } from "effect"
|
|
15
|
+
|
|
16
|
+
// Pass a string to Effect.fn, which will improve stack traces and also
|
|
17
|
+
// attach a tracing span (using Effect.withSpan behind the scenes).
|
|
18
|
+
//
|
|
19
|
+
// The name string should match the function name.
|
|
20
|
+
//
|
|
21
|
+
export const effectFunction = Effect.fn("effectFunction")(
|
|
22
|
+
// You can use `Effect.fn.Return` to specify the return type of the function.
|
|
23
|
+
// It accepts the same type parameters as `Effect.Effect`.
|
|
24
|
+
function*(n: number): Effect.fn.Return<string, SomeError> {
|
|
25
|
+
yield* Effect.logInfo("Received number:", n)
|
|
26
|
+
|
|
27
|
+
// Always return when raising an error, to ensure typescript understands that
|
|
28
|
+
// the function will not continue executing.
|
|
29
|
+
return yield* new SomeError({ message: "Failed to read the file" })
|
|
30
|
+
},
|
|
31
|
+
// Add additional functionality by passing in additional arguments.
|
|
32
|
+
// **Do not** use .pipe with Effect.fn
|
|
33
|
+
Effect.catch((error) => Effect.logError(`An error occurred: ${error}`)),
|
|
34
|
+
Effect.annotateLogs({
|
|
35
|
+
method: "effectFunction"
|
|
36
|
+
})
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
// Effect.fnUntraced avoids tracing and stack-frame capture while still reusing
|
|
40
|
+
// the generator body. This is preferred for library functions that do not
|
|
41
|
+
// represent a useful tracing boundary.
|
|
42
|
+
export const validateBatchSize = Effect.fnUntraced(function*(size: number): Effect.fn.Return<number, SomeError> {
|
|
43
|
+
if (!Number.isInteger(size) || size <= 0) {
|
|
44
|
+
return yield* new SomeError({ message: "Batch size must be a positive integer" })
|
|
45
|
+
}
|
|
46
|
+
return size
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// Use Schema.TaggedError to define a custom error
|
|
50
|
+
export class SomeError extends Schema.TaggedError<SomeError>()("SomeError", {
|
|
51
|
+
message: Schema.String
|
|
52
|
+
}) {}
|