| 1 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 2 | import * as Config from "effect/Config"; |
| 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as Redacted from "effect/Redacted"; |
| 5 | import { KV } from "./KV.ts"; |
| 6 | |
| 7 | /** |
| 8 | * Hard-coded value the integ test asserts on to prove the secret was |
| 9 | * bound at plantime and read at runtime through the `Redacted` accessor. |
| 10 | */ |
| 11 | export const WORKFLOW_SECRET_VALUE = "wf-secret-abc123"; |
| 12 | |
| 13 | export default class NotifyWorkflow extends Cloudflare.Workflow<NotifyWorkflow>()( |
| 14 | "Notifier", |
| 15 | Effect.gen(function* () { |
| 16 | // Bind a `secret_text` on the workflow at plantime. Using a literal |
| 17 | // (instead of `Config.Redacted("WORKFLOW_SECRET")`) keeps the integ |
| 18 | // test self-contained — no `.env` setup required. |
| 19 | const secret = yield* Config.Redacted("WORKFLOW_SECRET").pipe( |
| 20 | Config.withDefault(Redacted.make(WORKFLOW_SECRET_VALUE)), |
| 21 | ); |
| 22 | // Regression guard for https://github.com/alchemy-run/alchemy/pull/71 |
| 23 | // |
| 24 | // The kv binding internally yields `Cloudflare.Workers.WorkerEnvironment` — |
| 25 | // before that PR, accessing `WorkerEnvironment` inside a workflow body |
| 26 | // crashed because `provideService(WorkerEnvironment, env)` was applied |
| 27 | // to the outer `Effect.succeed(body)` wrapper (a no-op) instead of |
| 28 | // `body` itself in `Workflow.ts`. Exercising `kv.put` / `kv.get` from |
| 29 | // inside a `task` keeps the integ test catching any future regression. |
| 30 | const kv = yield* Cloudflare.KV.ReadWriteNamespace(KV); |
| 31 | |
| 32 | return Effect.fn(function* (input: { roomId: string; message: string }) { |
| 33 | const { roomId, message } = input; |
| 34 | |
| 35 | const stored = yield* Cloudflare.Workflows.task( |
| 36 | "kv-roundtrip", |
| 37 | Effect.gen(function* () { |
| 38 | const key = `workflow:smoke:${roomId}`; |
| 39 | yield* kv.put(key, message); |
| 40 | const got = yield* kv.get(key); |
| 41 | if (got !== message) { |
| 42 | return yield* Effect.die( |
| 43 | new Error( |
| 44 | `KV roundtrip mismatch: expected "${message}", got "${got ?? "null"}"`, |
| 45 | ), |
| 46 | ); |
| 47 | } |
| 48 | return got; |
| 49 | }).pipe(Effect.orDie), |
| 50 | ); |
| 51 | |
| 52 | // Resolve the bound secret inside the workflow body. The accessor |
| 53 | // returns `Redacted<string>`; unwrap only where the value needs to |
| 54 | // leave the workflow (here, in the broadcast + the returned output |
| 55 | // so the integ test can assert end-to-end propagation). |
| 56 | const secretValue = Redacted.value(secret); |
| 57 | |
| 58 | const processed = yield* Cloudflare.Workflows.task( |
| 59 | "process", |
| 60 | Effect.succeed({ |
| 61 | text: `Processed: ${stored}`, |
| 62 | secret: secretValue, |
| 63 | ts: Date.now(), |
| 64 | }), |
| 65 | ); |
| 66 | |
| 67 | // const room = rooms.getByName(roomId); |
| 68 | // yield* Cloudflare.Workflows.task( |
| 69 | // "broadcast", |
| 70 | // room.broadcast(`[workflow] ${processed.text} secret=${secretValue}`), |
| 71 | // ); |
| 72 | |
| 73 | // yield* Cloudflare.Workflows.sleep("cooldown", "2 seconds"); |
| 74 | |
| 75 | // yield* Cloudflare.Workflows.task( |
| 76 | // "finalize", |
| 77 | // room.broadcast(`[workflow] complete for ${roomId}`), |
| 78 | // ); |
| 79 | |
| 80 | return processed; |
| 81 | }); |
| 82 | }), |
| 83 | ) {} |