1import * as Cloudflare from "alchemy/Cloudflare";
2import * as Effect from "effect/Effect";
3
4/**
5 * Ephemeral chat room: broadcasts each text message to every connected client.
6 * Uses Durable Object storage of WebSocket attachments so sessions survive hibernation.
7 *
8 * Also demonstrates scheduled events: send `/remind <seconds> <message>` to
9 * schedule a broadcast that fires after the given delay.
10 */
11export default class Room extends Cloudflare.DurableObject<Room>()(
12 "Rooms",
13 Effect.gen(function* () {
14 const state = yield* Cloudflare.DurableObjectState;
15 return Effect.gen(function* () {
16 const sessions = new Map<string, Cloudflare.WebSocket>();
17
18 for (const socket of yield* state.getWebSockets()) {
19 const attachment = socket.deserializeAttachment<{ id: string }>();
20 if (attachment) {
21 sessions.set(attachment.id, socket);
22 }
23 }
24
25 const broadcast = (text: string) =>
26 Effect.gen(function* () {
27 for (const peer of sessions.values()) {
28 yield* peer.send(text);
29 }
30 });
31
32 return {
33 fetch: Effect.gen(function* () {
34 const [response, socket] = yield* Cloudflare.upgrade();
35 const id = crypto.randomUUID();
36 socket.serializeAttachment({ id });
37 sessions.set(id, socket);
38 return response;
39 }),
40 broadcast,
41 alarm: () =>
42 Effect.gen(function* () {
43 const fired = yield* Cloudflare.Workers.processScheduledEvents;
44 for (const event of fired) {
45 const payload = event.payload as { message: string };
46 yield* broadcast(`[reminder] ${payload.message}`);
47 }
48 }),
49 webSocketMessage: Effect.fn(function* (
50 socket: Cloudflare.WebSocket,
51 message: string | ArrayBuffer,
52 ) {
53 const attachment = socket.deserializeAttachment<{ id: string }>();
54 if (!attachment) return;
55 const text =
56 typeof message === "string"
57 ? message
58 : new TextDecoder().decode(message);
59
60 const remindMatch = text.match(/^\/remind\s+(\d+)\s+(.+)$/);
61 if (remindMatch) {
62 const delaySec = parseInt(remindMatch[1], 10);
63 const msg = remindMatch[2];
64 const id = crypto.randomUUID();
65 const runAt = new Date(Date.now() + delaySec * 1000);
66 yield* Cloudflare.Workers.scheduleEvent(id, runAt, {
67 message: msg,
68 });
69 yield* socket.send(
70 `[system] Reminder scheduled in ${delaySec}s: "${msg}"`,
71 );
72 return;
73 }
74
75 const label = attachment.id.slice(0, 8);
76 for (const peer of sessions.values()) {
77 yield* peer.send(`[${label}] ${text}`);
78 }
79 }),
80 webSocketClose: Effect.fn(function* (
81 ws: Cloudflare.WebSocket,
82 code: number,
83 reason: string,
84 _wasClean: boolean,
85 ) {
86 const attachment = ws.deserializeAttachment<{ id: string }>();
87 if (attachment) {
88 sessions.delete(attachment.id);
89 }
90 yield* ws.close(code, reason);
91 }),
92 };
93 });
94 }),
95) {}