@gjermundgaraba/effect-actions-cloudflare 0.21.0 → 0.23.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.
- package/dist/ActionDurableObject.d.mts +7 -1
- package/dist/ActionDurableObject.mjs +7 -7
- package/dist/alarm.d.mts +15 -6
- package/dist/alarm.mjs +42 -39
- package/dist/batches.mjs +0 -1
- package/dist/calls.mjs +0 -11
- package/dist/events.mjs +0 -15
- package/dist/frames.mjs +6 -9
- package/dist/idempotency.mjs +2 -8
- package/dist/placed.mjs +0 -4
- package/dist/sockets.d.mts +0 -4
- package/dist/sockets.mjs +7 -14
- package/dist/stub.d.mts +4 -4
- package/dist/stub.mjs +9 -8
- package/dist/subscriptions.mjs +10 -22
- package/dist/tasks.mjs +3 -8
- package/dist/webhooks.mjs +5 -21
- package/docs/ActionDurableObject-events.md +0 -20
- package/docs/ActionDurableObject-failures.md +1 -1
- package/docs/ActionDurableObject-placement.md +1 -4
- package/docs/ActionDurableObject-stub.md +3 -31
- package/docs/ActionDurableObject-testing.md +0 -5
- package/docs/ActionDurableObject.md +4 -50
- package/docs/README.md +1 -1
- package/examples/chat-rooms/README.md +2 -2
- package/examples/chat-rooms/src/Api.ts +0 -3
- package/examples/chat-rooms/src/Rpc.ts +0 -7
- package/examples/chat-rooms/src/browser.ts +2 -34
- package/examples/chat-rooms/src/chat/Chat.ts +3 -23
- package/examples/chat-rooms/src/chat/activity.ts +0 -5
- package/examples/chat-rooms/src/chat/authorize.ts +0 -3
- package/examples/chat-rooms/src/chat/events.ts +0 -12
- package/examples/chat-rooms/src/chat/handlers.ts +0 -6
- package/examples/chat-rooms/src/chat/room.ts +2 -7
- package/examples/chat-rooms/src/chat/store.ts +0 -6
- package/examples/chat-rooms/src/identity/Identity.ts +0 -6
- package/examples/chat-rooms/src/identity/members.ts +1 -3
- package/examples/chat-rooms/src/identity/verifier.ts +6 -9
- package/examples/chat-rooms/src/lobby/Calls.ts +0 -5
- package/examples/chat-rooms/src/lobby/Lobby.ts +2 -7
- package/examples/chat-rooms/src/lobby/events.ts +0 -5
- package/examples/chat-rooms/src/lobby/handlers.ts +0 -3
- package/examples/chat-rooms/src/lobby/host.ts +0 -1
- package/examples/chat-rooms/src/lobby/store.ts +2 -4
- package/examples/chat-rooms/src/platform/kv-previews.ts +6 -5
- package/examples/chat-rooms/src/platform/lobby-stubs.ts +0 -15
- package/examples/chat-rooms/src/platform/sql-store.ts +1 -13
- package/examples/chat-rooms/src/rooms-entrypoint.ts +2 -10
- package/examples/chat-rooms/src/routes.ts +0 -25
- package/examples/chat-rooms/src/worker.ts +3 -24
- package/examples/chat-rooms/test/platform/sql-store.test.ts +0 -5
- package/examples/chat-rooms/test/support/socket.ts +0 -9
- package/examples/chat-rooms/test/worker.test.ts +17 -24
- package/examples/chat-rooms/vite.config.ts +0 -5
- package/package.json +3 -3
- package/examples/chat-rooms/node_modules/.vite/vitest/CACHEDIR.TAG +0 -3
- package/examples/chat-rooms/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
|
@@ -3,7 +3,7 @@ import { Effect, Layer, Schema } from "effect";
|
|
|
3
3
|
import { HttpRouter, HttpServer } from "effect/http";
|
|
4
4
|
|
|
5
5
|
import { Activity } from "./chat/activity.js";
|
|
6
|
-
import {
|
|
6
|
+
import { DemoRoomName } from "./chat/Chat.js";
|
|
7
7
|
import { Room } from "./chat/room.js";
|
|
8
8
|
import { MessageStore } from "./chat/store.js";
|
|
9
9
|
import { layerKvPreviews } from "./platform/kv-previews.js";
|
|
@@ -21,22 +21,10 @@ import {
|
|
|
21
21
|
workerRoutes,
|
|
22
22
|
} from "./routes.js";
|
|
23
23
|
|
|
24
|
-
// The rooms for another Worker, over a service binding: a named entrypoint of this one.
|
|
25
24
|
export { RoomsEntrypoint } from "./rooms-entrypoint.js";
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
// chooses the stores, the room and the stubs, and routes each request.
|
|
26
|
+
const decodeRoom = Schema.decodeUnknownEffect(DemoRoomName);
|
|
29
27
|
|
|
30
|
-
const decodeRoom = Schema.decodeUnknownEffect(RoomName);
|
|
31
|
-
|
|
32
|
-
// A room's Durable Object, bound as CHAT_ROOM in wrangler.jsonc under this export's name. Each
|
|
33
|
-
// instance builds its services, the store, the room and its activity, then its routes, as it is
|
|
34
|
-
// constructed, again after every eviction. The room is the name the Worker reached the object by,
|
|
35
|
-
// `getByName(room)`, decoded once there: `ActionDurableObject.name` dies for an object reached by
|
|
36
|
-
// an id, and the build fails for a name `RoomName` does not list, each a bug in the Worker. Given
|
|
37
|
-
// `rooms`, it refuses a call or a watch whose input names another room. Its `lobby` task tells
|
|
38
|
-
// the lobby of each change, on the alarm until the lobby hears. The alarm expires old messages
|
|
39
|
-
// and tells the lobby, as an expired message can be the room's latest.
|
|
40
28
|
export class ChatRoom extends ActionDurableObject.make({
|
|
41
29
|
placed: rooms,
|
|
42
30
|
services: () =>
|
|
@@ -53,18 +41,12 @@ export class ChatRoom extends ActionDurableObject.make({
|
|
|
53
41
|
),
|
|
54
42
|
}) {}
|
|
55
43
|
|
|
56
|
-
// The lobby's Durable Object, bound as CHAT_LOBBY: one for the Worker, named `lobby`, keeping
|
|
57
|
-
// every room's preview, which each room tells it of and its pages follow.
|
|
58
44
|
export class ChatLobby extends ActionDurableObject.make({
|
|
59
45
|
services: () => layerKvPreviews,
|
|
60
46
|
routes: lobbyRoutes,
|
|
61
47
|
calls: lobbyCalls,
|
|
62
48
|
}) {}
|
|
63
49
|
|
|
64
|
-
// The Worker's own handler, built once, when the module loads, which keeps the lobby's client
|
|
65
|
-
// from stranding as `ActionDurableObject.layerStubClient`'s gotchas say: building it does no I/O,
|
|
66
|
-
// which a Worker refuses outside a request. `toWebHandler` reads no `MaxBodySize`, so the
|
|
67
|
-
// platform limits request bodies.
|
|
68
50
|
const served = HttpRouter.toWebHandler(
|
|
69
51
|
workerRoutes.pipe(
|
|
70
52
|
Layer.provide(layerLobbyHost),
|
|
@@ -72,11 +54,8 @@ const served = HttpRouter.toWebHandler(
|
|
|
72
54
|
)
|
|
73
55
|
);
|
|
74
56
|
|
|
75
|
-
const isRoomName = Schema.is(
|
|
57
|
+
const isRoomName = Schema.is(DemoRoomName);
|
|
76
58
|
|
|
77
|
-
// A page's socket, `/rooms/<room>/rpc` or `/lobby/rpc`, is its object's `/rpc`: forwarding the
|
|
78
|
-
// request forwards its upgrade, and the object answers it. Everything else is the Worker's own,
|
|
79
|
-
// the rooms' calls among them, which it sends on to each room's object itself.
|
|
80
59
|
export default {
|
|
81
60
|
async fetch(request: Request, env: Cloudflare.Env) {
|
|
82
61
|
const url = new URL(request.url);
|
|
@@ -12,8 +12,6 @@ const day = 24 * 60 * 60 * 1000;
|
|
|
12
12
|
|
|
13
13
|
let objects = 0;
|
|
14
14
|
|
|
15
|
-
// The adapter against a room's real SQLite database, inside a fresh object of its own: what it
|
|
16
|
-
// reaches is the Workers runtime, so the runtime is its stand-in.
|
|
17
15
|
const inObject = <A, E>(
|
|
18
16
|
use: (
|
|
19
17
|
store: MessageStore["Service"],
|
|
@@ -39,9 +37,6 @@ const inObject = <A, E>(
|
|
|
39
37
|
);
|
|
40
38
|
});
|
|
41
39
|
|
|
42
|
-
// A clock a millisecond later at each read, which never yields: posts running at once read
|
|
43
|
-
// times of their own, where at one shared time the oldest message and the latest would set the
|
|
44
|
-
// same alarm. Effect's TestClock moves on through a semaphore, which lets each post finish first.
|
|
45
40
|
const tickingFrom = (start: number): Clock.Clock => {
|
|
46
41
|
let now = start;
|
|
47
42
|
const tick = () => (now += 1);
|
|
@@ -6,14 +6,6 @@ import { Socket } from "effect/socket";
|
|
|
6
6
|
|
|
7
7
|
import { protocolVersion, unsupportedProtocol } from "../../src/Rpc.js";
|
|
8
8
|
|
|
9
|
-
// A page's socket to an object, `<at>/rpc` upgraded through the Worker as a browser's would be,
|
|
10
|
-
// naming the version of the rpcs as `connection(url)` does. A test has no network to open one by
|
|
11
|
-
// its URL, so it hands the upgraded socket to Effect's client protocol, the protocol
|
|
12
|
-
// `connection(url)` builds over a URL, its reconnection included, and keeps the socket, which a
|
|
13
|
-
// test listens on for what the object sent. The socket is accepted as Effect's client listens to
|
|
14
|
-
// it, as a browser's socket is constructed with its listeners: accepted before, a close the
|
|
15
|
-
// object sent at once could go unheard. `connects` counts the client's attempts: each after the
|
|
16
|
-
// first would be another upgrade in a browser.
|
|
17
9
|
export const pageSocket = (
|
|
18
10
|
at: string,
|
|
19
11
|
headers: Record<string, string> = {},
|
|
@@ -62,7 +54,6 @@ export const pageSocket = (
|
|
|
62
54
|
return { socket, protocol, connects: () => connects };
|
|
63
55
|
});
|
|
64
56
|
|
|
65
|
-
// A page's socket to a room.
|
|
66
57
|
export const roomSocket = (
|
|
67
58
|
room: string,
|
|
68
59
|
headers: Record<string, string> = {},
|
|
@@ -7,24 +7,22 @@ import * as ActionRpc from "@gjermundgaraba/effect-actions/ActionRpc";
|
|
|
7
7
|
import { evictDurableObject, runInDurableObject } from "cloudflare:test";
|
|
8
8
|
import { env, exports } from "cloudflare:workers";
|
|
9
9
|
import type { Schema } from "effect";
|
|
10
|
-
import { Effect, Schedule, Stream } from "effect";
|
|
10
|
+
import { DateTime, Effect, Schedule, Stream } from "effect";
|
|
11
11
|
import { HttpClient, HttpClientRequest } from "effect/http";
|
|
12
12
|
import { TestClock } from "effect/testing";
|
|
13
13
|
|
|
14
14
|
import { LobbyHttp, RoomHttp } from "../src/Api.js";
|
|
15
15
|
import { followLobby, openRoom } from "../src/browser.js";
|
|
16
16
|
import { MessagePosted, PostMessage } from "../src/chat/Chat.js";
|
|
17
|
-
import type {
|
|
17
|
+
import type { DemoRoomName } from "../src/chat/Chat.js";
|
|
18
18
|
import { Login } from "../src/identity/Identity.js";
|
|
19
19
|
import { RoomRpc, unsupportedProtocol } from "../src/Rpc.js";
|
|
20
20
|
import { pageSocket, roomSocket } from "./support/socket.js";
|
|
21
21
|
|
|
22
22
|
const day = 24 * 60 * 60 * 1000;
|
|
23
23
|
|
|
24
|
-
// Every request goes through the Worker's own `fetch`, as a deployed one would get it.
|
|
25
24
|
const worker = ActionDurableObject.layerHttpClient(exports.default);
|
|
26
25
|
|
|
27
|
-
// The rooms' JSON routes, the Worker's: each call names its room.
|
|
28
26
|
const jsonAs = (token?: string) =>
|
|
29
27
|
ActionHttp.client(RoomHttp, {
|
|
30
28
|
baseUrl: "https://chat.example",
|
|
@@ -37,7 +35,6 @@ const jsonAs = (token?: string) =>
|
|
|
37
35
|
}),
|
|
38
36
|
});
|
|
39
37
|
|
|
40
|
-
// An agent's one connection to the Worker's MCP endpoint, reaching every room.
|
|
41
38
|
const agentAs = (token: string) =>
|
|
42
39
|
ActionMcp.client([PostMessage, MessagePosted], {
|
|
43
40
|
url: "https://chat.example/mcp",
|
|
@@ -47,8 +44,6 @@ const agentAs = (token: string) =>
|
|
|
47
44
|
),
|
|
48
45
|
});
|
|
49
46
|
|
|
50
|
-
// What the runtime does after a call answers, such as a room's alarm, is waited for in real time:
|
|
51
|
-
// a test using this runs under `it.live`, as under the test clock its sleeps never pass.
|
|
52
47
|
const eventually = <A, E>(
|
|
53
48
|
check: Effect.Effect<A, E>,
|
|
54
49
|
until: (value: A) => boolean
|
|
@@ -65,7 +60,7 @@ const arrivedIn = (received: readonly unknown[]) => (count: number) =>
|
|
|
65
60
|
);
|
|
66
61
|
|
|
67
62
|
const pageIn = (
|
|
68
|
-
room:
|
|
63
|
+
room: DemoRoomName,
|
|
69
64
|
token: string,
|
|
70
65
|
headers: Record<string, string> = {}
|
|
71
66
|
) =>
|
|
@@ -92,10 +87,7 @@ const status = (path: string, body: Schema.Json = {}) =>
|
|
|
92
87
|
|
|
93
88
|
const readAll = (room: string) => ({ room, after: 0 });
|
|
94
89
|
|
|
95
|
-
|
|
96
|
-
// a first request ending without I/O is the one that would strand a client built inside it, as
|
|
97
|
-
// `ActionDurableObject.layerStubClient`'s gotchas say.
|
|
98
|
-
describe("the Worker's handler, built as the module loads", () => {
|
|
90
|
+
describe("the Worker's handler, built as the module loads, before any other test of this file reaches the lobby", () => {
|
|
99
91
|
it.effect(
|
|
100
92
|
"answers a first request ending without I/O, then a call reaching the lobby",
|
|
101
93
|
() =>
|
|
@@ -300,7 +292,7 @@ describe("the Worker and its rooms", () => {
|
|
|
300
292
|
text: "Polled in design.",
|
|
301
293
|
});
|
|
302
294
|
|
|
303
|
-
const polled = (room:
|
|
295
|
+
const polled = (room: DemoRoomName, after: number) =>
|
|
304
296
|
ada
|
|
305
297
|
.watch(
|
|
306
298
|
MessagePosted,
|
|
@@ -409,8 +401,6 @@ describe("the Worker and its rooms", () => {
|
|
|
409
401
|
baseUrl: "https://chat.example",
|
|
410
402
|
});
|
|
411
403
|
|
|
412
|
-
// The room tells the lobby from its `lobby` task, which the room's alarm runs once the
|
|
413
|
-
// post has answered: the lobby hears of it a moment later, not as the post returns.
|
|
414
404
|
const { previews } = yield* eventually(lobby.previews(), (answer) =>
|
|
415
405
|
answer.previews.some(
|
|
416
406
|
({ room, latestAt }) => room === "design" && latestAt === posted.at
|
|
@@ -487,7 +477,6 @@ describe("the Worker and its rooms", () => {
|
|
|
487
477
|
)
|
|
488
478
|
);
|
|
489
479
|
|
|
490
|
-
// The lobby hears of the post from the room's `lobby` task, a moment after it answered.
|
|
491
480
|
const latestAt = yield* eventually(
|
|
492
481
|
Effect.promise(
|
|
493
482
|
async () => await exports.RoomsEntrypoint.latestAt("general")
|
|
@@ -536,21 +525,25 @@ describe("the Worker and its rooms", () => {
|
|
|
536
525
|
text: "From yesterday.",
|
|
537
526
|
});
|
|
538
527
|
|
|
528
|
+
const recent = yield* json.postMessage({
|
|
529
|
+
room: "design",
|
|
530
|
+
text: "From today.",
|
|
531
|
+
});
|
|
532
|
+
|
|
539
533
|
yield* Effect.promise(async () => {
|
|
540
|
-
await runInDurableObject(stub, (
|
|
534
|
+
await runInDurableObject(stub, async (room, state) => {
|
|
541
535
|
state.storage.sql.exec(
|
|
542
536
|
"UPDATE messages SET at = at - ? WHERE id = ?",
|
|
543
537
|
day,
|
|
544
538
|
old.id
|
|
545
539
|
);
|
|
546
|
-
});
|
|
547
|
-
});
|
|
548
540
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
541
|
+
await room.run(
|
|
542
|
+
ActionDurableObject.Alarm.use((alarm) =>
|
|
543
|
+
alarm.set(DateTime.makeUnsafe(old.at))
|
|
544
|
+
)
|
|
545
|
+
);
|
|
546
|
+
});
|
|
554
547
|
});
|
|
555
548
|
|
|
556
549
|
yield* eventually(
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { cloudflareTest } from "@cloudflare/vitest-plugin";
|
|
2
2
|
import { defineConfig } from "vite-plus";
|
|
3
3
|
|
|
4
|
-
// Its tests run inside the Workers runtime, on the Worker wrangler.jsonc describes.
|
|
5
4
|
export default defineConfig({
|
|
6
5
|
plugins: [
|
|
7
6
|
cloudflareTest({
|
|
@@ -10,10 +9,6 @@ export default defineConfig({
|
|
|
10
9
|
],
|
|
11
10
|
test: {
|
|
12
11
|
include: ["test/**/*.test.ts"],
|
|
13
|
-
// Why: under @cloudflare/vitest-plugin 1.4.4, the warm-module response for a later file in a
|
|
14
|
-
// pool slot can exceed what the runner's control socket accepts, 41 MB in 709 entries
|
|
15
|
-
// measured on chat-rooms, which closes it with 1009 and loses the file as "Worker exited
|
|
16
|
-
// unexpectedly"; two small files alone did not trigger it.
|
|
17
12
|
fsModuleCache: false,
|
|
18
13
|
},
|
|
19
14
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjermundgaraba/effect-actions-cloudflare",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Serve effect-actions from Cloudflare Durable Objects, with Effect RPC over hibernating WebSockets.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|
|
@@ -39,11 +39,11 @@
|
|
|
39
39
|
"@cloudflare/vitest-plugin": "1.4.7",
|
|
40
40
|
"@cloudflare/workers-types": "5.20261025.1",
|
|
41
41
|
"@effect/vitest": "4.0.6",
|
|
42
|
-
"@gjermundgaraba/effect-actions": "0.
|
|
42
|
+
"@gjermundgaraba/effect-actions": "0.23.0",
|
|
43
43
|
"effect": "4.0.6"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
|
-
"@gjermundgaraba/effect-actions": "^0.
|
|
46
|
+
"@gjermundgaraba/effect-actions": "^0.23.0",
|
|
47
47
|
"effect": "~4.0.5"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":1,"projects":{"":{"files":{"test/worker.test.ts":{"failed":false,"duration":3218,"lastRun":1793050877213},"test/platform/sql-store.test.ts":{"failed":false,"duration":405,"lastRun":1793050877213}}}}}
|