1import * as Cloudflare from "alchemy/Cloudflare";
2import { Config } from "effect";
3import * as Effect from "effect/Effect";
4import * as Redacted from "effect/Redacted";
5import * as Schedule from "effect/Schedule";
6import { KV } from "./KV.ts";
7import Room from "./Room.ts";
8
9/**
10 * Hard-coded value the integ test asserts on to prove the secret was
11 * bound at plantime and read at runtime through the `Redacted` accessor.
12 */
13export const WORKFLOW_SECRET_VALUE = Redacted.make("wf-secret-abc123");
14
15export default class NotifyWorkflow extends Cloudflare.Workflow<NotifyWorkflow>()(
16 "Notifier",
17 Effect.gen(function* () {
18 const rooms = yield* Room;
19
20 const secret = yield* Config.Redacted("WORKFLOW_SECRET").pipe(
21 Config.withDefault(WORKFLOW_SECRET_VALUE),
22 );
23
24 const kv = yield* Cloudflare.KV.ReadWriteNamespace(KV);
25
26 return Effect.fn(function* (input: { roomId: string; message: string }) {
27 const { roomId, message } = input;
28
29 const stored = yield* Cloudflare.Workflows.task(
30 "kv-roundtrip",
31 Effect.gen(function* () {
32 const key = `workflow:smoke:${roomId}`;
33 yield* kv.put(key, message);
34 // KV is eventually consistent: a read immediately after a write can
35 // briefly miss or return stale data. Re-read until it reflects the
36 // write (bounded, so a genuine failure still surfaces below).
37 const got = yield* kv.get(key).pipe(
38 Effect.repeat({
39 schedule: Schedule.spaced("500 millis"),
40 until: (value) => value === message,
41 times: 10,
42 }),
43 );
44 if (got !== message) {
45 return yield* Effect.die(
46 new Error(
47 `KV roundtrip mismatch: expected "${message}", got "${got ?? "null"}"`,
48 ),
49 );
50 }
51 return got;
52 }).pipe(Effect.orDie),
53 );
54
55 // Resolve the bound secret inside the workflow body. The accessor
56 // returns `Redacted<string>`; unwrap only where the value needs to
57 // leave the workflow (here, in the broadcast + the returned output
58 // so the integ test can assert end-to-end propagation).
59 const secretValue = Redacted.value(secret);
60
61 const processed = yield* Cloudflare.Workflows.task(
62 "process",
63 Effect.succeed({
64 text: `Processed: ${stored}`,
65 secret: secretValue,
66 ts: Date.now(),
67 }),
68 );
69
70 const room = rooms.getByName(roomId);
71 yield* Cloudflare.Workflows.task(
72 "broadcast",
73 room.broadcast(`[workflow] ${processed.text} secret=${secretValue}`),
74 );
75
76 yield* Cloudflare.Workflows.sleep("cooldown", "2 seconds");
77
78 yield* Cloudflare.Workflows.task(
79 "finalize",
80 room.broadcast(`[workflow] complete for ${roomId}`),
81 );
82
83 return processed;
84 });
85 }),
86) {}