@gjermundgaraba/effect-actions-cloudflare 0.0.0-stage → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +54 -2
  3. package/dist/ActionDurableObject.d.mts +240 -0
  4. package/dist/ActionDurableObject.mjs +190 -0
  5. package/dist/alarm.d.mts +42 -0
  6. package/dist/alarm.mjs +155 -0
  7. package/dist/batches.mjs +10 -0
  8. package/dist/calls.d.mts +9 -0
  9. package/dist/calls.mjs +92 -0
  10. package/dist/events.mjs +131 -0
  11. package/dist/fetcher.d.mts +17 -0
  12. package/dist/fetcher.mjs +19 -0
  13. package/dist/frames.mjs +33 -0
  14. package/dist/idempotency.mjs +40 -0
  15. package/dist/placed.d.mts +47 -0
  16. package/dist/placed.mjs +53 -0
  17. package/dist/sockets.d.mts +115 -0
  18. package/dist/sockets.mjs +243 -0
  19. package/dist/state.d.mts +18 -0
  20. package/dist/state.mjs +18 -0
  21. package/dist/stub.d.mts +59 -0
  22. package/dist/stub.mjs +75 -0
  23. package/dist/subscriptions.mjs +104 -0
  24. package/dist/tasks.d.mts +24 -0
  25. package/dist/tasks.mjs +31 -0
  26. package/dist/webhooks.d.mts +36 -0
  27. package/dist/webhooks.mjs +108 -0
  28. package/docs/ActionDurableObject-events.md +153 -0
  29. package/docs/ActionDurableObject-failures.md +18 -0
  30. package/docs/ActionDurableObject-own-class.md +9 -0
  31. package/docs/ActionDurableObject-placement.md +41 -0
  32. package/docs/ActionDurableObject-stub.md +135 -0
  33. package/docs/ActionDurableObject-testing.md +35 -0
  34. package/docs/ActionDurableObject.md +235 -0
  35. package/docs/README.md +28 -0
  36. package/examples/chat-rooms/README.md +97 -0
  37. package/examples/chat-rooms/src/Api.ts +14 -0
  38. package/examples/chat-rooms/src/Rpc.ts +22 -0
  39. package/examples/chat-rooms/src/browser.ts +107 -0
  40. package/examples/chat-rooms/src/chat/Chat.ts +95 -0
  41. package/examples/chat-rooms/src/chat/activity.ts +16 -0
  42. package/examples/chat-rooms/src/chat/authorize.ts +22 -0
  43. package/examples/chat-rooms/src/chat/events.ts +64 -0
  44. package/examples/chat-rooms/src/chat/handlers.ts +38 -0
  45. package/examples/chat-rooms/src/chat/room.ts +12 -0
  46. package/examples/chat-rooms/src/chat/store.ts +25 -0
  47. package/examples/chat-rooms/src/env.d.ts +12 -0
  48. package/examples/chat-rooms/src/identity/Identity.ts +21 -0
  49. package/examples/chat-rooms/src/identity/members.ts +8 -0
  50. package/examples/chat-rooms/src/identity/verifier.ts +24 -0
  51. package/examples/chat-rooms/src/lobby/Calls.ts +14 -0
  52. package/examples/chat-rooms/src/lobby/Lobby.ts +32 -0
  53. package/examples/chat-rooms/src/lobby/events.ts +17 -0
  54. package/examples/chat-rooms/src/lobby/handlers.ts +34 -0
  55. package/examples/chat-rooms/src/lobby/host.ts +10 -0
  56. package/examples/chat-rooms/src/lobby/store.ts +18 -0
  57. package/examples/chat-rooms/src/platform/kv-previews.ts +35 -0
  58. package/examples/chat-rooms/src/platform/lobby-stubs.ts +46 -0
  59. package/examples/chat-rooms/src/platform/sql-store.ts +110 -0
  60. package/examples/chat-rooms/src/rooms-entrypoint.ts +33 -0
  61. package/examples/chat-rooms/src/routes.ts +89 -0
  62. package/examples/chat-rooms/src/worker.ts +99 -0
  63. package/examples/chat-rooms/test/platform/sql-store.test.ts +132 -0
  64. package/examples/chat-rooms/test/support/socket.ts +70 -0
  65. package/examples/chat-rooms/test/worker.test.ts +579 -0
  66. package/examples/chat-rooms/tsconfig.json +22 -0
  67. package/examples/chat-rooms/vite.config.ts +19 -0
  68. package/examples/chat-rooms/wrangler.jsonc +15 -0
  69. package/package.json +49 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gjermund Garaba
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,55 @@
1
- # Temporary Holding Version
1
+ # effect-actions-cloudflare
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ `@gjermundgaraba/effect-actions-cloudflare` serves Effect RPC from Cloudflare Durable Objects, over WebSockets the runtime holds while an object hibernates and over an object's stub, and calls it from a Worker: any plain `RpcGroup`, with no action at all, served by Effect's own `RpcServer`. [effect-actions](https://github.com/gjermundgaraba/effect-actions) bindings are the option: the same routes `ActionRpc.layer`, `ActionHttp.layer` and `ActionMcp.layerHttp` serve elsewhere, in an object, with events followed over its sockets, over MCP or by webhook across its hibernation. It is released with `@gjermundgaraba/effect-actions`, at the same version, and requires it.
4
+
5
+ ```sh
6
+ pnpm add @gjermundgaraba/effect-actions @gjermundgaraba/effect-actions-cloudflare effect@~4.0.5
7
+ ```
8
+
9
+ Its declarations name the Workers runtime's types, such as `Fetcher`: generate them with `wrangler types`, or install `@cloudflare/workers-types`, as a Worker project does.
10
+
11
+ Serve an Effect `RpcGroup` from an object, over WebSockets the runtime holds while it hibernates:
12
+
13
+ ```ts
14
+ class ChatRoom extends ActionDurableObject.make({
15
+ services: () =>
16
+ Layer.mergeAll(layerSql, Layer.effect(Room)(ActionDurableObject.name)),
17
+ routes: RpcServer.layer(RoomRpcs).pipe(
18
+ Layer.provide(handlers),
19
+ Layer.provide(ActionDurableObject.layerWebSocket({ path: "/rpc" }))
20
+ ),
21
+ }) {}
22
+ ```
23
+
24
+ Or serve actions there, authenticated, as `ActionRpc.layer` serves them elsewhere:
25
+
26
+ ```ts
27
+ routes: ActionRpc.layer(RoomRpc, room).pipe(
28
+ Layer.provide(ActionDurableObject.layerWebSocket({ path: "/rpc" })),
29
+ Layer.provide(authenticate),
30
+ ),
31
+ ```
32
+
33
+ Call an object's own API, an Effect `RpcGroup` it serves as its `calls`, from a Worker or another object with Cloudflare's own RPC, a new stub for each call, its declared errors typed:
34
+
35
+ ```ts
36
+ Effect.gen(function* () {
37
+ const lobby = yield* ActionDurableObject.client(LobbyCalls);
38
+
39
+ return yield* lobby(() => env.CHAT_LOBBY.getByName("lobby")).previews();
40
+ });
41
+ ```
42
+
43
+ Call an object's routes from a Worker with the core's own clients, through the stub:
44
+
45
+ ```ts
46
+ ActionHttp.client(RoomHttp, { baseUrl: "https://room" }).pipe(
47
+ Effect.provide(
48
+ ActionDurableObject.layerHttpClient(env.CHAT_ROOM.getByName("design"))
49
+ )
50
+ );
51
+ ```
52
+
53
+ Its pages are in [docs/](docs/README.md), with an example, [chat-rooms](examples/chat-rooms/): one Durable Object per room, served over hibernating WebSockets, JSON and its stub, with an alarm and a Worker in front. In a project that installs it, both are in `node_modules/@gjermundgaraba/effect-actions-cloudflare/`.
54
+
55
+ MIT licensed.
@@ -0,0 +1,240 @@
1
+ import { State, name } from "./state.mjs";
2
+ import { Alarm } from "./alarm.mjs";
3
+ import { StubRequest } from "./calls.mjs";
4
+ import { AnyPlacement, Placement, PlacementKeys, placed, reaching } from "./placed.mjs";
5
+ import { LayerWebSocketOptions, Sockets, layerWebSocket, refuseUpgrade } from "./sockets.mjs";
6
+ import { Tasks } from "./tasks.mjs";
7
+ import { LayerWebhooksOptions, Waits, layerWebhooks } from "./webhooks.mjs";
8
+ import { Client, Stub, client, layerStubClient } from "./stub.mjs";
9
+ import { layerHttpClient } from "./fetcher.mjs";
10
+ import { DurableObject } from "cloudflare:workers";
11
+ import { Effect, Layer, Scope } from "effect";
12
+ import { HttpRouter, HttpServer, HttpServerRequest } from "effect/http";
13
+ import { RpcMessage, RpcServer } from "effect/rpc";
14
+ import * as Event from "@gjermundgaraba/effect-actions/Event";
15
+ import * as Action from "@gjermundgaraba/effect-actions/Action";
16
+ type ServedPerRequest = Scope.Scope | HttpServerRequest.HttpServerRequest;
17
+ type ObjectServices = State | Alarm | Tasks | Event.Events | Action.Idempotency | Sockets | Waits;
18
+ type Allowed<A, S> = ObjectServices | S | HttpRouter.HttpRouter | Layer.Success<typeof HttpServer.layerServices> | HttpRouter.Request<"Requires", ServedPerRequest | ObjectServices | A | S> | HttpRouter.Request<"GlobalRequires", ServedPerRequest | ObjectServices | A | S> | HttpRouter.Request<"Error", unknown> | HttpRouter.Request<"GlobalError", unknown>;
19
+ /** `handlers`' and `make`'s options. */
20
+ export interface Options<S = never, SE = never, SR = never, A = never, E = never, R = never, AE = never, AR = never, CA = never, CE = never, CR = never> {
21
+ /**
22
+ * The object's startup services, such as a store over `State`'s `storage.sql`: built once per
23
+ * instance, before the routes, again after every eviction, and never finalized. The routes,
24
+ * every request, the alarm and `run` share that one build, and an implementation the services
25
+ * build, such as an in-process `Action.client`'s, is the one the routes serve. They may
26
+ * require `State`, `Alarm`, `Tasks`, `Event.Events` and `Action.Idempotency` alone, the last
27
+ * keeping the answers of calls given an idempotency key in the object's storage. A store over
28
+ * the object's storage, what the object is, such as its name, a store scheduling its expiries,
29
+ * or a handler announcing an event, is a Layer requiring them.
30
+ *
31
+ * **Gotchas**
32
+ *
33
+ * - A service only the routes need may be provided to them instead, but the alarm and `run` see only these: one they read too belongs here.
34
+ * - A module-level Layer reading `env` from `cloudflare:workers` as the module evaluates finds it unset: read it as the Layer builds or serves, or give `make` a function of the instance's `env`.
35
+ * - They build under the instance's gate, which holds every event of the object until they end: one calling another object as it builds, rather than only making its client, waits on that object's gate, and two objects calling each other as they build wait on each other until the runtime resets both, 30 seconds on. Call other objects from a handler or the alarm.
36
+ */
37
+ readonly services?: Layer.Layer<S, SE, SR> | undefined;
38
+ /**
39
+ * What `fetch` serves: routes, as `HttpRouter.toWebHandler` serves them, such as
40
+ * `ActionRpc.layer` over `layerWebSocket` and `ActionHttp.layer`. Built after the services, again
41
+ * after every eviction, and never finalized. They may require `State`, `Alarm`, `Tasks`,
42
+ * `Event.Events`, `Action.Idempotency`, `Sockets`, `Waits` and the services, as Layers and as
43
+ * handlers; any other request-time requirement must be provided by their middleware or
44
+ * authentication, or be a service they provide themselves. Without them, `fetch` answers 404, and
45
+ * no protocol accepts a socket.
46
+ *
47
+ * **Gotchas**
48
+ *
49
+ * - Router middleware and `HttpRouter.provideRequest` around the routes run for a socket's upgrade request alone: a service only they provide is missing from every call over the socket, which dies with `Internal server error`; list an `RpcMiddleware` in `ActionRpc.layer`'s `middleware`, or make it a startup service.
50
+ * - `toWebHandler` reads no `HttpServerRequest.MaxBodySize`: the platform limits request bodies.
51
+ */
52
+ readonly routes?: Layer.Layer<A, E, R> | undefined;
53
+ /**
54
+ * What the object serves over its stub, on `effectActionsRpc`: an rpc server, such as
55
+ * `RpcServer.layer(group)` with its handlers, or `ActionRpc.layer`, given the stub's protocol,
56
+ * over which each call of `effectActionsRpc` is one call, its messages in and its responses out,
57
+ * as values Workers RPC's structured clone copies, each schema's plain form
58
+ * (`Schema.toCodecIso`), never a JSON string. Built after the services, beside the routes,
59
+ * again after every eviction, and never finalized. It may require the protocol, `State`,
60
+ * `Alarm`, `Tasks`, `Event.Events`, `Action.Idempotency` and the services; every call's own
61
+ * requirement must be provided by its middleware or be one of those. Without it, a call over
62
+ * the stub rejects once the object is built, naming the option.
63
+ *
64
+ * **Gotchas**
65
+ *
66
+ * - The calls serve one group on the one protocol `make` and `handlers` give them: serve several groups as one, `group.merge(other)`.
67
+ * - Calls running no rpc server on the protocol, such as a group's handlers given without `RpcServer.layer(group)`, fail the object's build, naming it.
68
+ * - A `Date`, bytes, `undefined` and an `Error` defect cross as themselves; an `Option`, an exit and a tagged error as their tagged objects, a `Map` and a `Set` as arrays.
69
+ * - A schema with no JSON form, such as `Schema.instanceOf(Date)`, works over the stub, not over `layerWebSocket`, which speaks JSON; an `ActionRpc` binding's values cross as JSON over either.
70
+ * - A value the clone refuses, such as a class instance in a `Schema.Unknown`, or a defect that is a class instance other than an `Error`, fails the whole call with workerd's `DataCloneError`, whatever its exit.
71
+ * - Unary: a call answers once every request it carries has its exit, so an rpc that streams is answered with a defect at its first chunk, naming it: stream over `layerWebSocket`.
72
+ * - An `ActionRpc.layer` here fails its build when the binding serves a streaming action or an event type: list the others in its `actions`.
73
+ * - A defect Effect's server sends as fatal to the whole client, such as a handler's die, is answered as the exit of each request of the call still without one, a die: the call fails with that defect alone, and the client's next call is answered.
74
+ * - In a raw `effectActionsRpc` call batching several requests, a handler's die before another request's answer gives that request two exits, the defect first, which Effect's client takes, then its own; `layerStubClient` sends one request per call and never meets it.
75
+ * - Nothing authenticates a call: a stub is reachable only through a binding, so the caller is the Worker or another object of the deployment, and one acting for a principal forwards it in a header, with `RpcClient.withHeaders`, read by an `RpcMiddleware` or an `ActionRpc` binding's authentication, or in the payload.
76
+ * - A plain group suits an object's own API, which only its Worker and other objects call; an `ActionRpc.layer` suits actions served on other surfaces too, called with `ActionRpc.client` over `layerStubClient`.
77
+ */
78
+ readonly calls?: Layer.Layer<CA, CE, CR> | undefined;
79
+ /**
80
+ * What runs at the time `Alarm.set` gave, given its `scheduledTime` and `retryCount`, 0 at that
81
+ * time and counting each failed or cut-off run as it is retried. It may require the services,
82
+ * `State`, `Alarm`, `Tasks`, `Event.Events`, `Action.Idempotency` and a `Scope`, closed when it
83
+ * ends. It runs through `run`, once the services and routes are built.
84
+ *
85
+ * **Gotchas**
86
+ *
87
+ * - A failure is logged and retried 2 seconds later, doubling, six times, as the runtime retries a plain object's alarm, then hourly until a run succeeds, never dropped: the object's `alarm` resolves, so the runtime records no failure.
88
+ * - A run an eviction, a deploy or a crash cuts off runs again at its retry's time, counted in `retryCount` as a failure, so a run may repeat: make what it sends idempotent.
89
+ * - It runs once for every time `Alarm.set` gave before its run, at the earliest: the option sets its next one itself.
90
+ */
91
+ readonly alarm?: ((info: AlarmInvocationInfo) => Effect.Effect<void, AE, AR>) | undefined;
92
+ /**
93
+ * Work the object does at least once, by name, such as telling another object of a change:
94
+ * `Tasks.runAfter(name, change)` marks it due now in the object's storage and runs the change,
95
+ * and the object's alarm runs it, after the call, until it succeeds. Each may require what
96
+ * `alarm` may.
97
+ */
98
+ readonly tasks?: {
99
+ readonly [name: string]: Effect.Effect<void, unknown, NoInfer<S> | State | Alarm | Tasks | Event.Events | Action.Idempotency | Scope.Scope>;
100
+ } | undefined;
101
+ /**
102
+ * The placement whose objects these are, `placed`'s answer, which the Worker serves: each call
103
+ * and subscription of one of its contracts, on every surface, sockets and the stub included, is
104
+ * refused `Forbidden` where its input's key names another object.
105
+ */
106
+ readonly placed?: AnyPlacement | undefined;
107
+ /**
108
+ * Whether to leave out the request logger `HttpRouter.toWebHandler` adds by default, which logs
109
+ * every request the routes serve, a socket's upgrade included: Effect's default, kept.
110
+ */
111
+ readonly disableLogger?: boolean | undefined;
112
+ }
113
+ type HandlerOf<K extends keyof DurableObject> = NonNullable<DurableObject[K]>;
114
+ /**
115
+ * The handlers `handlers` returns and the class `make` returns defines, each taking the runtime's
116
+ * own arguments, so a Durable Object class delegates those it serves this way and writes the
117
+ * others itself. Each awaits the object's build, started by the first of them called, and
118
+ * rejects with its failure.
119
+ */
120
+ export interface Handlers<S = never> {
121
+ /** Serves the routes, or answers 404 when there are none. */
122
+ fetch(...args: Parameters<HandlerOf<"fetch">>): Promise<Response>;
123
+ /**
124
+ * Hands the message to the protocol whose socket it is, and resolves for a socket no protocol
125
+ * accepted.
126
+ */
127
+ webSocketMessage(...args: Parameters<HandlerOf<"webSocketMessage">>): Promise<void>;
128
+ /**
129
+ * Completes the closing handshake of any socket, replying with the client's code and reason,
130
+ * and ends the socket's protocol client, if a protocol accepted it.
131
+ */
132
+ webSocketClose(...args: Parameters<HandlerOf<"webSocketClose">>): Promise<void>;
133
+ /** Ends the socket's protocol client, and resolves for a socket no protocol accepted. */
134
+ webSocketError(...args: Parameters<HandlerOf<"webSocketError">>): Promise<void>;
135
+ /**
136
+ * Answers one call over the object's stub, the messages `layerStubClient` sends, with its
137
+ * responses, as values, through the `calls` option's server, once every request it carries
138
+ * has its exit, or rejects without `calls`. Workers RPC calls it by this name, the package's,
139
+ * so it does not collide with the class's own methods.
140
+ */
141
+ effectActionsRpc(request: StubRequest): Promise<readonly RpcMessage.FromServerEncoded[]>;
142
+ /**
143
+ * Runs, through `run`, each time the object's schedule holds that has come: the `alarm`
144
+ * option's, which `Alarm.set` gave, and the package's own.
145
+ */
146
+ alarm(...args: Parameters<HandlerOf<"alarm">>): Promise<void>;
147
+ /**
148
+ * Runs `effect` in the object, with the services the instance built, `State`, `Alarm`,
149
+ * `Tasks`, `Event.Events`, `Action.Idempotency` and a `Scope`, closed when it ends, once the
150
+ * services and routes are built: what the alarm runs through, and what the object's own methods
151
+ * run their Effects with, such as a method a Worker calls over the object's stub. A Layer the
152
+ * effect provides itself, or an `Action.client` it acquires, is built for that call alone: give
153
+ * what calls share as `services`.
154
+ *
155
+ * **Gotchas**
156
+ *
157
+ * - A Durable Object's public methods are callable over its stub, the `run` of a class `make` returns too, but it takes an Effect, which no stub can send: a caller over the stub gets a rejection, nothing more.
158
+ */
159
+ run<A, E>(effect: Effect.Effect<A, E, S | State | Alarm | Tasks | Event.Events | Action.Idempotency | Scope.Scope>): Promise<A>;
160
+ }
161
+ /**
162
+ * The object's lifecycle, for a Durable Object class that cannot extend the one `make` returns:
163
+ * given its `ctx`, the handlers that build `services`, then `calls`, with the stub's protocol,
164
+ * and `routes`, with `Sockets`, once per instance, as it is constructed, with the object's own
165
+ * services provided, its `Event.Events`' logs kept in its storage, under `effect-actions/log/`,
166
+ * and its current sources' revisions beside them, so a cursor outlives an eviction, and share
167
+ * that one build across every event, which awaits it. `fetch` serves the routes, the socket
168
+ * handlers dispatch through `Sockets`, `effectActionsRpc` through the calls' server, `alarm` runs
169
+ * the times `Alarm` keeps and `run` runs an Effect in the object.
170
+ * Delegate the handlers the class serves this way, as methods:
171
+ * `effectActionsRpc(request: StubRequest)` returning the handlers' own.
172
+ *
173
+ * **Gotchas**
174
+ *
175
+ * - An instance lasts until the runtime evicts it, and the next event builds everything again on a new one: nothing the services, calls or routes build is finalized, and in-memory state is lost, so keep state in storage.
176
+ * - A build that fails is dropped with all it built, and the events waiting on it are rejected with its failure: the next event builds again, under the same gate.
177
+ * - The events arriving while a build runs, such as the one building again after a failure, run once it ends, one after another, each to its end before the next: a slow one holds those after it.
178
+ * - An instance builds as it is constructed, under `blockConcurrencyWhile`, so the runtime holds every event, the first included, until the build ends, then delivers them one by one, as to a built instance: a build longer than 30 seconds resets the object.
179
+ * - Every Effect of the object runs under Effect's microtask scheduler, `new Scheduler.MixedScheduler("sync")`, where its default's timers would let the next event in between any two steps: CPU-heavy work holds every other event until it ends.
180
+ * - Calls still interleave where a plain method's would: while one waits on I/O, another object or a timer, such as `Effect.sleep`, the next event runs, so serialize a read and the write that depends on it with a `Semaphore`, or make both one synchronous `storage.kv` or `storage.sql` step.
181
+ * - Without `routes`, `fetch` answers 404; without `calls`, `effectActionsRpc` rejects once the object is built, naming the option.
182
+ * - A socket the class accepts itself, with `State`'s `acceptWebSocket`, carries no protocol's tag: the socket handlers resolve for it, `webSocketClose` once it has replied to the client's close, so the class answers the sockets carrying its own tag in `webSocketMessage` and delegates the rest.
183
+ * - Delegate `alarm` too: the object's one alarm is the package's, which also ends `layerWebhooks`' long waits, and workerd refuses `storage.setAlarm`, which `Alarm.set` calls, on a class without an `alarm` method, where celld 0.6.2 accepts it and drops the alarm when it is due.
184
+ * - Its `Event.Events` reads a check or current source when a subscriber subscribes and on each `notify`, never on its `nextPoll`, a timer that would keep the object from hibernating: an upstream outside the object notifies it through a call, such as a method a Worker calls over its stub.
185
+ * - Its `Event.Events` keeps a logged occurrence's data as its payload's JSON: one that does not encode as it is published is logged then, and one that no longer decodes, such as after a deployment changed the payload, is reported as it is read; each is sent as a gap.
186
+ * - Provide no other `Event.Events`, such as the core's `Event.layerMemory`, to the services or routes: it shadows the object's, its logs lost at each eviction and its check sources read on a timer that keeps the object awake.
187
+ * - A log's entries live beside the application's data: a store deleting every key deletes them too, and a subscriber resuming from one of their cursors receives a gap.
188
+ * - A current source's revisions live beside them, under `effect-actions/revisions/`: a store deleting every key starts them again, and until the object is evicted their ids repeat ones its watches have seen, which drop them.
189
+ * - Two log sources of event types sharing a name die as the second attaches, naming it: each log is kept under its event's name.
190
+ * - A log source fed by `emit` dies as it attaches, naming `publish`: an evicted object would miss what the stream sends meanwhile, with no gap, and its open stream would keep the object awake.
191
+ * - A subscriber's read that fails is tried again 5 seconds later, on a timer, until one succeeds: a source failing for good keeps the object awake, billed, and sends each subscriber a `Failed` each time.
192
+ * - A poll's `subscription` hook is held until 5 minutes after its last poll, on a timer that keeps the object awake, and an eviction tears down no hook: one it ends is set up again as its subscription resumes, its teardown never run.
193
+ *
194
+ * Guide: `docs/ActionDurableObject-own-class.md` in this package.
195
+ */
196
+ export declare const handlers: <S = never, SE = never, SR extends State | Alarm | Tasks | Event.Events | Action.Idempotency = never, A = never, E = never, R extends Allowed<A, S> = never, AE = never, AR extends S | State | Alarm | Tasks | Event.Events | Action.Idempotency | Scope.Scope = never, CA = never, CE = never, CR extends RpcServer.Protocol | S | State | Alarm | Tasks | Event.Events | Action.Idempotency = never>(ctx: DurableObjectState, options: Options<S, SE, SR, A, E, R, AE, AR, CA, CE, CR>) => Handlers<S>;
197
+ /**
198
+ * An instance of a class `make` returns: a Durable Object with the handlers it defines, each
199
+ * taking the runtime's own arguments, so a class extending it overrides them as `Handlers`
200
+ * declares them, returning a promise, and reaches `make`'s through `super`, its `env` the one its
201
+ * `services` function takes.
202
+ */
203
+ export type Instance<S = never, Env = Cloudflare.Env> = Handlers<S> & DurableObject<Env>;
204
+ /**
205
+ * `make` with `services`, a function of the instance's `env`, whose parameter's type is the
206
+ * class's `env`: the class `make` returns otherwise.
207
+ *
208
+ * Guide: `docs/ActionDurableObject.md` in this package.
209
+ */
210
+ export declare function make<S, SE, SR extends State | Alarm | Tasks | Event.Events | Action.Idempotency, A = never, E = never, R extends Allowed<A, S> = never, AE = never, AR extends S | State | Alarm | Tasks | Event.Events | Action.Idempotency | Scope.Scope = never, CA = never, CE = never, CR extends RpcServer.Protocol | S | State | Alarm | Tasks | Event.Events | Action.Idempotency = never, Env = Cloudflare.Env>(options: Omit<Options<S, SE, SR, A, E, R, AE, AR, CA, CE, CR>, "services"> & {
211
+ /**
212
+ * The object's startup services, as `handlers`' `services`, returned by a function of the
213
+ * instance's `env`, `this.env`, called as each instance is constructed: a service reads a
214
+ * binding or a variable from the `env` it is given, whose type, the function's parameter's,
215
+ * is the class's `env`.
216
+ */
217
+ readonly services: (env: Env) => Layer.Layer<S, SE, SR>;
218
+ }): new (ctx: DurableObjectState, env: Env) => Instance<S, Env>;
219
+ /**
220
+ * A Durable Object class delegating every handler, and `run`, to `handlers` over its own `ctx`,
221
+ * with the services `services` returns for its `env`: it serves `routes` on `fetch`, the
222
+ * sockets their protocols accept on `webSocketMessage`, `webSocketClose` and `webSocketError`,
223
+ * and `calls` on `effectActionsRpc`, the method a caller's `layerStubClient` or `client` calls
224
+ * over the stub, and runs `alarm`. Export it, or a class extending it, under the name the
225
+ * Wrangler configuration binds; a class extending it overrides only the handlers it has reason
226
+ * to write, and reaches `make`'s through `super`.
227
+ *
228
+ * **Gotchas**
229
+ *
230
+ * - Its socket handlers resolve for a socket no protocol accepted, such as one a class extending it accepts itself, `webSocketClose` once it has replied to the client's close: an override answers its own sockets and calls `super` for the rest.
231
+ * - The `services` function's parameter type is the class's `env`, so an application's own `Env` needs no augmenting of `Cloudflare.Env`; services reading no `env` are `() => layer`, and a class without one has `Cloudflare.Env`.
232
+ * - It always has `alarm`, the package's, which runs the `alarm` option at the time `Alarm.set` gave: an override reaches it with `await super.alarm(info)`, and one that does not call it runs none of the times `Alarm` keeps.
233
+ *
234
+ * Guide: `docs/ActionDurableObject.md` in this package; in an application:
235
+ * `examples/chat-rooms/`.
236
+ */
237
+ export declare function make<S = never, SE = never, SR extends State | Alarm | Tasks | Event.Events | Action.Idempotency = never, A = never, E = never, R extends Allowed<A, S> = never, AE = never, AR extends S | State | Alarm | Tasks | Event.Events | Action.Idempotency | Scope.Scope = never, CA = never, CE = never, CR extends RpcServer.Protocol | S | State | Alarm | Tasks | Event.Events | Action.Idempotency = never>(options: Omit<Options<S, SE, SR, A, E, R, AE, AR, CA, CE, CR>, "services"> & {
238
+ readonly services?: ((env: Cloudflare.Env) => Layer.Layer<S, SE, SR>) | undefined;
239
+ }): new (ctx: DurableObjectState, env: Cloudflare.Env) => Instance<S>;
240
+ export { Alarm, type AnyPlacement, type Client, type LayerWebSocketOptions, type LayerWebhooksOptions, type Placement, type PlacementKeys, type Sockets, State, type Stub, type StubRequest, Tasks, type Waits, client, layerHttpClient, layerStubClient, layerWebSocket, layerWebhooks, name, placed, reaching, refuseUpgrade };
@@ -0,0 +1,190 @@
1
+ import { State, name } from "./state.mjs";
2
+ import { Alarm, alarmOf, applicationOwner, makeSchedule } from "./alarm.mjs";
3
+ import { makeStubProtocol } from "./calls.mjs";
4
+ import { layerEvents } from "./events.mjs";
5
+ import { layerIdempotency } from "./idempotency.mjs";
6
+ import { layerInputCheck, placed, reaching } from "./placed.mjs";
7
+ import { Sockets, layerWebSocket, refuseUpgrade } from "./sockets.mjs";
8
+ import { Tasks, makeTasks } from "./tasks.mjs";
9
+ import { Waits, clockOwner, layerWebhooks, makeWaits } from "./webhooks.mjs";
10
+ import { client, layerStubClient } from "./stub.mjs";
11
+ import { layerHttpClient } from "./fetcher.mjs";
12
+ import { DurableObject } from "cloudflare:workers";
13
+ import { Cause, Context, Effect, Exit, Function, Layer, Scheduler, Scope } from "effect";
14
+ import { HttpEffect, HttpMiddleware, HttpRouter, HttpServer } from "effect/http";
15
+ import { RpcServer } from "effect/rpc";
16
+ // src/durable/ActionDurableObject.ts
17
+ const scheduler = new Scheduler.MixedScheduler("sync");
18
+ const handlersOf = (ctx, options) => {
19
+ const services = options.services ?? Layer.empty;
20
+ const routes = options.routes;
21
+ const { calls } = options;
22
+ const state = Layer.succeed(State, ctx);
23
+ const schedule = makeSchedule(ctx);
24
+ const { waits, wakeDue } = makeWaits(schedule);
25
+ const tasks = makeTasks(schedule, options.tasks ?? {});
26
+ const build = Effect.gen(function* () {
27
+ const memoMap = yield* Layer.makeMemoMap;
28
+ const scope = yield* Scope.make();
29
+ return yield* Effect.gen(function* () {
30
+ const context = yield* Layer.buildWithMemoMap(services.pipe(Layer.provideMerge(Layer.mergeAll(Layer.succeed(Alarm)(alarmOf(schedule)), Layer.succeed(Waits)(waits), Layer.succeed(Tasks)(tasks.tasks), layerEvents, layerIdempotency(ctx))), Layer.provideMerge(state), Layer.provideMerge(Layer.merge(Layer.succeed(Scheduler.Scheduler)(scheduler), options.placed === void 0 ? Layer.empty : layerInputCheck(options.placed, ctx.id.name)))), memoMap, scope);
31
+ const given = Layer.succeedContext(context);
32
+ yield* schedule.restore;
33
+ const call = calls === void 0 ? async () => {
34
+ throw new Error("This Durable Object serves no rpcs over its stub: give handlers or make a calls Layer");
35
+ } : yield* Effect.gen(function* () {
36
+ const stub = yield* Effect.provideContext(makeStubProtocol, context);
37
+ yield* Layer.buildWithMemoMap(calls.pipe(Layer.provide(Layer.succeed(RpcServer.Protocol)(stub.protocol)), Layer.provide(given)), memoMap, scope);
38
+ yield* stub.served;
39
+ return async (request) => await Effect.runPromise(stub.serve(request));
40
+ });
41
+ const sockets = yield* Layer.buildWithMemoMap(Sockets.layer.pipe(Layer.provide(given)), memoMap, scope);
42
+ const serve = routes === void 0 ? async () => new Response(null, { status: 404 }) : yield* Effect.gen(function* () {
43
+ const routed = yield* Layer.buildWithMemoMap(routes.pipe(Layer.provideMerge(Layer.succeedContext(Context.merge(context, sockets))), Layer.provide(HttpServer.layerServices), Layer.provideMerge(HttpRouter.layer)), memoMap, scope);
44
+ return yield* Effect.sync(() => HttpEffect.toWebHandlerWith(routed)(Context.get(routed, HttpRouter.HttpRouter).asHttpEffect(), options.disableLogger === true ? void 0 : HttpMiddleware.logger));
45
+ });
46
+ return {
47
+ context,
48
+ call,
49
+ sockets: Context.get(sockets, Sockets),
50
+ serve
51
+ };
52
+ }).pipe(Effect.onError((cause) => Scope.close(scope, Exit.failCause(cause))));
53
+ });
54
+ let instance;
55
+ let building = false;
56
+ const waiting = [];
57
+ const start = () => {
58
+ building = true;
59
+ (async () => {
60
+ const built = await ctx.blockConcurrencyWhile(async () => {
61
+ const exit = await Effect.runPromiseExit(build, { scheduler });
62
+ building = false;
63
+ if (Exit.isFailure(exit)) {
64
+ for (const next of waiting.splice(0)) next.fail(exit.cause);
65
+ return;
66
+ }
67
+ instance = exit.value;
68
+ return instance;
69
+ });
70
+ if (built === void 0) return;
71
+ for (let next = waiting.shift(); next !== void 0; next = waiting.shift()) await next.use(built);
72
+ })();
73
+ };
74
+ start();
75
+ const withBuilt = async (use) => {
76
+ const held = instance;
77
+ if (held !== void 0) return await use(held);
78
+ const { promise, resolve, reject } = Promise.withResolvers();
79
+ waiting.push({
80
+ use: async (built) => {
81
+ const used = (async () => await use(built))();
82
+ resolve(used);
83
+ await used.then(Function.constUndefined, Function.constUndefined);
84
+ },
85
+ fail: (cause) => {
86
+ reject(Cause.squash(cause));
87
+ }
88
+ });
89
+ if (!building) start();
90
+ return await promise;
91
+ };
92
+ const provided = ({ context }, effect) => Effect.scopedWith((scope) => Effect.provideContext(effect, context.pipe(Context.add(Scope.Scope, scope), Context.add(Layer.CurrentMemoMap, Layer.makeMemoMapUnsafe()))));
93
+ const run = async (effect) => await withBuilt(async (built) => await Effect.runPromise(provided(built, effect)));
94
+ const dispatch = async (event) => {
95
+ await withBuilt(async ({ sockets }) => {
96
+ await Effect.runPromise(Effect.asVoid(event(sockets)));
97
+ });
98
+ };
99
+ return {
100
+ fetch: async (request) => await withBuilt(async ({ serve }) => await serve(request)),
101
+ webSocketMessage: async (socket, message) => {
102
+ await dispatch((sockets) => sockets.message(socket, message));
103
+ },
104
+ webSocketClose: async (socket, code, reason) => {
105
+ await dispatch((sockets) => sockets.close(socket, code, reason));
106
+ },
107
+ webSocketError: async (socket) => {
108
+ await dispatch((sockets) => sockets.error(socket));
109
+ },
110
+ effectActionsRpc: async (request) => await withBuilt(async ({ call }) => await call(request)),
111
+ alarm: async () => {
112
+ await run(schedule.runDue({
113
+ [applicationOwner]: (info) => options.alarm?.(info) ?? Effect.void,
114
+ [clockOwner]: () => wakeDue,
115
+ ...tasks.owners
116
+ }));
117
+ },
118
+ run
119
+ };
120
+ };
121
+ /**
122
+ * The object's lifecycle, for a Durable Object class that cannot extend the one `make` returns:
123
+ * given its `ctx`, the handlers that build `services`, then `calls`, with the stub's protocol,
124
+ * and `routes`, with `Sockets`, once per instance, as it is constructed, with the object's own
125
+ * services provided, its `Event.Events`' logs kept in its storage, under `effect-actions/log/`,
126
+ * and its current sources' revisions beside them, so a cursor outlives an eviction, and share
127
+ * that one build across every event, which awaits it. `fetch` serves the routes, the socket
128
+ * handlers dispatch through `Sockets`, `effectActionsRpc` through the calls' server, `alarm` runs
129
+ * the times `Alarm` keeps and `run` runs an Effect in the object.
130
+ * Delegate the handlers the class serves this way, as methods:
131
+ * `effectActionsRpc(request: StubRequest)` returning the handlers' own.
132
+ *
133
+ * **Gotchas**
134
+ *
135
+ * - An instance lasts until the runtime evicts it, and the next event builds everything again on a new one: nothing the services, calls or routes build is finalized, and in-memory state is lost, so keep state in storage.
136
+ * - A build that fails is dropped with all it built, and the events waiting on it are rejected with its failure: the next event builds again, under the same gate.
137
+ * - The events arriving while a build runs, such as the one building again after a failure, run once it ends, one after another, each to its end before the next: a slow one holds those after it.
138
+ * - An instance builds as it is constructed, under `blockConcurrencyWhile`, so the runtime holds every event, the first included, until the build ends, then delivers them one by one, as to a built instance: a build longer than 30 seconds resets the object.
139
+ * - Every Effect of the object runs under Effect's microtask scheduler, `new Scheduler.MixedScheduler("sync")`, where its default's timers would let the next event in between any two steps: CPU-heavy work holds every other event until it ends.
140
+ * - Calls still interleave where a plain method's would: while one waits on I/O, another object or a timer, such as `Effect.sleep`, the next event runs, so serialize a read and the write that depends on it with a `Semaphore`, or make both one synchronous `storage.kv` or `storage.sql` step.
141
+ * - Without `routes`, `fetch` answers 404; without `calls`, `effectActionsRpc` rejects once the object is built, naming the option.
142
+ * - A socket the class accepts itself, with `State`'s `acceptWebSocket`, carries no protocol's tag: the socket handlers resolve for it, `webSocketClose` once it has replied to the client's close, so the class answers the sockets carrying its own tag in `webSocketMessage` and delegates the rest.
143
+ * - Delegate `alarm` too: the object's one alarm is the package's, which also ends `layerWebhooks`' long waits, and workerd refuses `storage.setAlarm`, which `Alarm.set` calls, on a class without an `alarm` method, where celld 0.6.2 accepts it and drops the alarm when it is due.
144
+ * - Its `Event.Events` reads a check or current source when a subscriber subscribes and on each `notify`, never on its `nextPoll`, a timer that would keep the object from hibernating: an upstream outside the object notifies it through a call, such as a method a Worker calls over its stub.
145
+ * - Its `Event.Events` keeps a logged occurrence's data as its payload's JSON: one that does not encode as it is published is logged then, and one that no longer decodes, such as after a deployment changed the payload, is reported as it is read; each is sent as a gap.
146
+ * - Provide no other `Event.Events`, such as the core's `Event.layerMemory`, to the services or routes: it shadows the object's, its logs lost at each eviction and its check sources read on a timer that keeps the object awake.
147
+ * - A log's entries live beside the application's data: a store deleting every key deletes them too, and a subscriber resuming from one of their cursors receives a gap.
148
+ * - A current source's revisions live beside them, under `effect-actions/revisions/`: a store deleting every key starts them again, and until the object is evicted their ids repeat ones its watches have seen, which drop them.
149
+ * - Two log sources of event types sharing a name die as the second attaches, naming it: each log is kept under its event's name.
150
+ * - A log source fed by `emit` dies as it attaches, naming `publish`: an evicted object would miss what the stream sends meanwhile, with no gap, and its open stream would keep the object awake.
151
+ * - A subscriber's read that fails is tried again 5 seconds later, on a timer, until one succeeds: a source failing for good keeps the object awake, billed, and sends each subscriber a `Failed` each time.
152
+ * - A poll's `subscription` hook is held until 5 minutes after its last poll, on a timer that keeps the object awake, and an eviction tears down no hook: one it ends is set up again as its subscription resumes, its teardown never run.
153
+ *
154
+ * Guide: `docs/ActionDurableObject-own-class.md` in this package.
155
+ */
156
+ const handlers = (ctx, options) => handlersOf(ctx, options);
157
+ function make(options) {
158
+ return class ActionDurableObject extends DurableObject {
159
+ #handlers;
160
+ constructor(ctx, env) {
161
+ super(ctx, env);
162
+ this.#handlers = handlersOf(this.ctx, {
163
+ ...options,
164
+ services: options.services?.(this.env)
165
+ });
166
+ }
167
+ async fetch(...args) {
168
+ return await this.#handlers.fetch(...args);
169
+ }
170
+ async webSocketMessage(...args) {
171
+ await this.#handlers.webSocketMessage(...args);
172
+ }
173
+ async webSocketClose(...args) {
174
+ await this.#handlers.webSocketClose(...args);
175
+ }
176
+ async webSocketError(...args) {
177
+ await this.#handlers.webSocketError(...args);
178
+ }
179
+ async effectActionsRpc(request) {
180
+ return await this.#handlers.effectActionsRpc(request);
181
+ }
182
+ async alarm(info) {
183
+ await this.#handlers.alarm(info);
184
+ }
185
+ async run(effect) {
186
+ return await this.#handlers.run(effect);
187
+ }
188
+ };
189
+ }
190
+ export { Alarm, State, Tasks, client, handlers, layerHttpClient, layerStubClient, layerWebSocket, layerWebhooks, make, name, placed, reaching, refuseUpgrade };
@@ -0,0 +1,42 @@
1
+ import { State } from "./state.mjs";
2
+ import { Context, DateTime, Effect, Layer } from "effect";
3
+ declare const Alarm_base: Context.ServiceClass<Alarm, "effect-actions-cloudflare/ActionDurableObject/Alarm", {
4
+ /**
5
+ * Runs the object's `alarm` option no later than `at`: the earliest time set since it last
6
+ * ran, or a failed run's retry if earlier, so each deadline sets its own. A failed run's
7
+ * retry stays unless it was cleared as it ran with no time set afterwards, or a time set as
8
+ * it ran is earlier.
9
+ *
10
+ * **Gotchas**
11
+ *
12
+ * - A later time leaves an earlier one in place, so the option runs early: postpone with `clear`, then `set`.
13
+ */
14
+ readonly set: (at: DateTime.DateTime) => Effect.Effect<void>;
15
+ /** Cancels the time set, if any. */
16
+ readonly clear: Effect.Effect<void>;
17
+ }>;
18
+ /**
19
+ * When the object's `alarm` option runs: `set(at)` has it run no later than `at`, and `clear`
20
+ * cancels it. A startup service of the object's services, routes, calls and `run`, as `State`
21
+ * is. The object's one runtime alarm is the package's, set for the earliest of the times it
22
+ * keeps, the package's own work among them, so an application never sets it itself.
23
+ *
24
+ * **Gotchas**
25
+ *
26
+ * - `State`'s `storage.setAlarm` replaces the package's alarm: the times it keeps then run at that alarm, early or late, or never, and the `alarm` option runs only for a time `Alarm.set` gave, so schedule with `Alarm`.
27
+ * - A firing before every time it keeps, as a repeated one or `runDurableObjectAlarm` in a test, runs none and sets the alarm again: a test runs a time once it has come.
28
+ * - A time already past runs at once; the option runs once for every time set before its run, and sets its next itself.
29
+ * - The times live in the object's storage, under `effect-actions/alarm/`, beside the application's data: a store deleting every key deletes them too.
30
+ */
31
+ export declare class Alarm extends Alarm_base {
32
+ /**
33
+ * The application's alarm, over the schedule of the instance whose `State` it is given.
34
+ *
35
+ * **Gotchas**
36
+ *
37
+ * - Only `handlers`' and `make`'s `alarm` runs the times it keeps: provide this layer alone only where nothing calls `set`, as to a store under test.
38
+ * - Each build makes a schedule of its own, which takes no turns with the object's: a `set` through it while the object's alarm runs may be lost, so an object's own code uses the `Alarm` `handlers` and `make` provide.
39
+ */
40
+ static readonly layer: Layer.Layer<Alarm, never, State>;
41
+ }
42
+ export {};