|
| 1 | +import { DurableObject } from "cloudflare:workers"; |
| 2 | +import { type ResolvedProvider, runCode } from "@cloudflare/codemode"; |
| 3 | +import { type DurableObjectStorageLike, Workspace } from "@cloudflare/computer"; |
| 4 | +import { IsolateJavaScriptBackend } from "@cloudflare/computer/backends/javascript"; |
| 5 | + |
| 6 | +import { |
| 7 | + type ExecWorkspaceLike, |
| 8 | + WorkspaceCodemodeDispatcher, |
| 9 | + WorkspaceCodemodeExecutor, |
| 10 | +} from "./workspace-executor.js"; |
| 11 | + |
| 12 | +const ROOT = "/workspace"; |
| 13 | +const EXAMPLE_FILE = `${ROOT}/codemode.txt`; |
| 14 | + |
| 15 | +export class CodemodeExample extends DurableObject<Env> { |
| 16 | + readonly #workspace: Workspace; |
| 17 | + readonly #executor: WorkspaceCodemodeExecutor; |
| 18 | + #ready?: Promise<void>; |
| 19 | + #runQueue: Promise<void> = Promise.resolve(); |
| 20 | + |
| 21 | + constructor(ctx: DurableObjectState, env: Env) { |
| 22 | + super(ctx, env); |
| 23 | + const dispatcher = new WorkspaceCodemodeDispatcher(); |
| 24 | + this.#workspace = new Workspace({ |
| 25 | + storage: ctx.storage as unknown as DurableObjectStorageLike, |
| 26 | + waitUntil: ctx.waitUntil.bind(ctx), |
| 27 | + backends: [ |
| 28 | + new IsolateJavaScriptBackend({ |
| 29 | + id: "codemode-javascript", |
| 30 | + loader: env.LOADER, |
| 31 | + root: ROOT, |
| 32 | + access: "read-write", |
| 33 | + trustedModules: { "ws:codemode-adapter": dispatcher }, |
| 34 | + }), |
| 35 | + ], |
| 36 | + }); |
| 37 | + this.#executor = new WorkspaceCodemodeExecutor({ |
| 38 | + workspace: this.#workspace as unknown as ExecWorkspaceLike, |
| 39 | + dispatcher, |
| 40 | + }); |
| 41 | + } |
| 42 | + |
| 43 | + run() { |
| 44 | + const run = this.#runQueue.then( |
| 45 | + () => this.#runOnce(), |
| 46 | + () => this.#runOnce(), |
| 47 | + ); |
| 48 | + this.#runQueue = run.then( |
| 49 | + () => undefined, |
| 50 | + () => undefined, |
| 51 | + ); |
| 52 | + return run; |
| 53 | + } |
| 54 | + |
| 55 | + async #runOnce() { |
| 56 | + await this.#ensureReady(); |
| 57 | + const before = await this.#workspace.fs.readFile(EXAMPLE_FILE, "utf8"); |
| 58 | + const code = `async () => { |
| 59 | + const fs = await import("node:fs/promises"); |
| 60 | + const before = await fs.readFile(${JSON.stringify(EXAMPLE_FILE)}, "utf8"); |
| 61 | + const after = String(await demo.next(Number(before))); |
| 62 | + await fs.writeFile(${JSON.stringify(EXAMPLE_FILE)}, after); |
| 63 | + return { before, after }; |
| 64 | + }`; |
| 65 | + const providers: ResolvedProvider[] = [ |
| 66 | + { |
| 67 | + name: "demo", |
| 68 | + fns: { next: async (value) => (Number(value) + 1) % 1_000_000 }, |
| 69 | + }, |
| 70 | + ]; |
| 71 | + let result: { result?: unknown; logs?: string[]; error?: string }; |
| 72 | + try { |
| 73 | + result = await runCode({ executor: this.#executor, providers, code }); |
| 74 | + } catch (error) { |
| 75 | + result = { error: error instanceof Error ? error.message : String(error) }; |
| 76 | + } |
| 77 | + const after = await this.#workspace.fs.readFile(EXAMPLE_FILE, "utf8"); |
| 78 | + return { ok: result.error === undefined, before, after, result }; |
| 79 | + } |
| 80 | + |
| 81 | + #ensureReady() { |
| 82 | + if (this.#ready) return this.#ready; |
| 83 | + const ready = (async () => { |
| 84 | + await this.#workspace.fs.mkdir(ROOT, { recursive: true }); |
| 85 | + try { |
| 86 | + await this.#workspace.fs.stat(EXAMPLE_FILE); |
| 87 | + } catch (error) { |
| 88 | + if ((error as { code?: string }).code !== "ENOENT") throw error; |
| 89 | + await this.#workspace.fs.writeFile(EXAMPLE_FILE, "0"); |
| 90 | + } |
| 91 | + })(); |
| 92 | + const guarded = ready.catch((error) => { |
| 93 | + if (this.#ready === guarded) this.#ready = undefined; |
| 94 | + throw error; |
| 95 | + }); |
| 96 | + this.#ready = guarded; |
| 97 | + return guarded; |
| 98 | + } |
| 99 | +} |
| 100 | + |
| 101 | +interface CodemodeExampleStub { |
| 102 | + run(): Promise<{ ok: boolean; [key: string]: unknown }>; |
| 103 | +} |
| 104 | + |
| 105 | +export default { |
| 106 | + async fetch(request: Request, env: Env) { |
| 107 | + const url = new URL(request.url); |
| 108 | + if (request.method !== "POST" || url.pathname !== "/run") { |
| 109 | + return new Response("POST /run to execute the Codemode example.\n", { |
| 110 | + status: 405, |
| 111 | + headers: { allow: "POST", "content-type": "text/plain; charset=utf-8" }, |
| 112 | + }); |
| 113 | + } |
| 114 | + try { |
| 115 | + const stub = env.CodemodeExample.get( |
| 116 | + env.CodemodeExample.idFromName("example"), |
| 117 | + ) as unknown as CodemodeExampleStub; |
| 118 | + const result = await stub.run(); |
| 119 | + return Response.json(result, { status: result.ok ? 200 : 422 }); |
| 120 | + } catch (error) { |
| 121 | + return Response.json( |
| 122 | + { error: error instanceof Error ? error.message : String(error) }, |
| 123 | + { status: 500 }, |
| 124 | + ); |
| 125 | + } |
| 126 | + }, |
| 127 | +} satisfies ExportedHandler<Env>; |
0 commit comments