@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.
Files changed (57) hide show
  1. package/dist/ActionDurableObject.d.mts +7 -1
  2. package/dist/ActionDurableObject.mjs +7 -7
  3. package/dist/alarm.d.mts +15 -6
  4. package/dist/alarm.mjs +42 -39
  5. package/dist/batches.mjs +0 -1
  6. package/dist/calls.mjs +0 -11
  7. package/dist/events.mjs +0 -15
  8. package/dist/frames.mjs +6 -9
  9. package/dist/idempotency.mjs +2 -8
  10. package/dist/placed.mjs +0 -4
  11. package/dist/sockets.d.mts +0 -4
  12. package/dist/sockets.mjs +7 -14
  13. package/dist/stub.d.mts +4 -4
  14. package/dist/stub.mjs +9 -8
  15. package/dist/subscriptions.mjs +10 -22
  16. package/dist/tasks.mjs +3 -8
  17. package/dist/webhooks.mjs +5 -21
  18. package/docs/ActionDurableObject-events.md +0 -20
  19. package/docs/ActionDurableObject-failures.md +1 -1
  20. package/docs/ActionDurableObject-placement.md +1 -4
  21. package/docs/ActionDurableObject-stub.md +3 -31
  22. package/docs/ActionDurableObject-testing.md +0 -5
  23. package/docs/ActionDurableObject.md +4 -50
  24. package/docs/README.md +1 -1
  25. package/examples/chat-rooms/README.md +2 -2
  26. package/examples/chat-rooms/src/Api.ts +0 -3
  27. package/examples/chat-rooms/src/Rpc.ts +0 -7
  28. package/examples/chat-rooms/src/browser.ts +2 -34
  29. package/examples/chat-rooms/src/chat/Chat.ts +3 -23
  30. package/examples/chat-rooms/src/chat/activity.ts +0 -5
  31. package/examples/chat-rooms/src/chat/authorize.ts +0 -3
  32. package/examples/chat-rooms/src/chat/events.ts +0 -12
  33. package/examples/chat-rooms/src/chat/handlers.ts +0 -6
  34. package/examples/chat-rooms/src/chat/room.ts +2 -7
  35. package/examples/chat-rooms/src/chat/store.ts +0 -6
  36. package/examples/chat-rooms/src/identity/Identity.ts +0 -6
  37. package/examples/chat-rooms/src/identity/members.ts +1 -3
  38. package/examples/chat-rooms/src/identity/verifier.ts +6 -9
  39. package/examples/chat-rooms/src/lobby/Calls.ts +0 -5
  40. package/examples/chat-rooms/src/lobby/Lobby.ts +2 -7
  41. package/examples/chat-rooms/src/lobby/events.ts +0 -5
  42. package/examples/chat-rooms/src/lobby/handlers.ts +0 -3
  43. package/examples/chat-rooms/src/lobby/host.ts +0 -1
  44. package/examples/chat-rooms/src/lobby/store.ts +2 -4
  45. package/examples/chat-rooms/src/platform/kv-previews.ts +6 -5
  46. package/examples/chat-rooms/src/platform/lobby-stubs.ts +0 -15
  47. package/examples/chat-rooms/src/platform/sql-store.ts +1 -13
  48. package/examples/chat-rooms/src/rooms-entrypoint.ts +2 -10
  49. package/examples/chat-rooms/src/routes.ts +0 -25
  50. package/examples/chat-rooms/src/worker.ts +3 -24
  51. package/examples/chat-rooms/test/platform/sql-store.test.ts +0 -5
  52. package/examples/chat-rooms/test/support/socket.ts +0 -9
  53. package/examples/chat-rooms/test/worker.test.ts +17 -24
  54. package/examples/chat-rooms/vite.config.ts +0 -5
  55. package/package.json +3 -3
  56. package/examples/chat-rooms/node_modules/.vite/vitest/CACHEDIR.TAG +0 -3
  57. 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 { RoomName } from "./chat/Chat.js";
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
- // Entrypoint, Wrangler's `main`: the composition root of the Worker and of every object. It
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(RoomName);
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 { RoomName } from "../src/chat/Chat.js";
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: RoomName,
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
- // First in the file, whose Worker module is its own: no request has reached the lobby before, so
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: RoomName, after: number) =>
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, (_, state) => {
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
- // The next post sets the alarm for the day-old message's expiry, already past, so the
550
- // runtime runs it at once: the test waits until it has set the next, for the recent one's.
551
- const recent = yield* json.postMessage({
552
- room: "design",
553
- text: "From today.",
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.21.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.21.0",
42
+ "@gjermundgaraba/effect-actions": "0.23.0",
43
43
  "effect": "4.0.6"
44
44
  },
45
45
  "peerDependencies": {
46
- "@gjermundgaraba/effect-actions": "^0.21.0",
46
+ "@gjermundgaraba/effect-actions": "^0.23.0",
47
47
  "effect": "~4.0.5"
48
48
  },
49
49
  "scripts": {
@@ -1,3 +0,0 @@
1
- Signature: 8a477f597d28d172789f06886806bc55
2
- # This file is a cache directory tag created by Vitest.
3
- # For information about cache directory tags see https://bford.info/cachedir/
@@ -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}}}}}