/** * arcrun logic component Worker * * POST / → JSON input → WASM (WASI preview1 stdin/stdout) → JSON output * * WASM is statically bundled at build time via wrangler.toml [[wasm_modules]]. * Each logic component gets its own Worker at {name}.arcrun.dev. */ import componentWasm from '../component.wasm' assert { type: 'webassembly' }; import { Hono } from 'hono'; import { cors } from 'hono/cors'; const app = new Hono(); app.use('*', cors()); app.get('/', (c) => c.json({ ok: true, component: COMPONENT_ID })); app.post('/', async (c) => { let input: unknown; try { input = await c.req.json(); } catch { return c.json({ success: false, error: 'request body must be JSON' }, 400); } try { const result = await runWasm(componentWasm, input); return c.json(result); } catch (e) { return c.json({ success: false, error: e instanceof Error ? e.message : String(e) }, 500); } }); export default app; // ── WASM runner (WASI preview1 stdin/stdout) ───────────────────────────────── declare const COMPONENT_ID: string; // injected via [vars] in wrangler.toml async function runWasm(wasmModule: WebAssembly.Module, input: unknown): Promise { const stdinBytes = new TextEncoder().encode(JSON.stringify(input)); let stdinOffset = 0; const stdoutChunks: Uint8Array[] = []; let memory: WebAssembly.Memory | null = null; const getView = () => new DataView(memory!.buffer); const wasi: WebAssembly.Imports = { wasi_snapshot_preview1: { fd_write(fd: number, iovs: number, iovs_len: number, nwritten_ptr: number): number { if (fd !== 1 && fd !== 2) return 76; // ENOSYS const view = getView(); let total = 0; for (let i = 0; i < iovs_len; i++) { const base = view.getUint32(iovs + i * 8, true); const len = view.getUint32(iovs + i * 8 + 4, true); if (len === 0) continue; const chunk = new Uint8Array(memory!.buffer, base, len); const copy = new Uint8Array(len); copy.set(chunk); if (fd === 1) stdoutChunks.push(copy); total += len; } view.setUint32(nwritten_ptr, total, true); return 0; }, fd_read(fd: number, iovs: number, iovs_len: number, nread_ptr: number): number { if (fd !== 0) return 76; const view = getView(); let total = 0; for (let i = 0; i < iovs_len; i++) { const base = view.getUint32(iovs + i * 8, true); const len = view.getUint32(iovs + i * 8 + 4, true); const remaining = stdinBytes.length - stdinOffset; if (remaining <= 0) break; const toCopy = Math.min(len, remaining); new Uint8Array(memory!.buffer, base, toCopy).set( stdinBytes.subarray(stdinOffset, stdinOffset + toCopy) ); stdinOffset += toCopy; total += toCopy; } view.setUint32(nread_ptr, total, true); return 0; }, proc_exit(code: number): never { throw new Error(`wasm exit: ${code}`); }, random_get(ptr: number, len: number): number { crypto.getRandomValues(new Uint8Array(memory!.buffer, ptr, len)); return 0; }, fd_seek: () => 76, fd_close: () => 0, fd_fdstat_get: () => 76, fd_prestat_get: () => 76, fd_prestat_dir_name: () => 76, environ_get: () => 0, environ_sizes_get: (cp: number, sp: number) => { if (memory) { const v = getView(); v.setUint32(cp,0,true); v.setUint32(sp,0,true); } return 0; }, args_get: () => 0, args_sizes_get: (ap: number, bp: number) => { if (memory) { const v = getView(); v.setUint32(ap,0,true); v.setUint32(bp,0,true); } return 0; }, clock_time_get: (_id: number, _prec: bigint, tp: number) => { if (memory) getView().setBigUint64(tp, BigInt(Date.now()) * 1_000_000n, true); return 0; }, clock_res_get: () => 76, poll_oneoff: () => 76, sched_yield: () => 0, proc_raise: () => 76, sock_accept: () => 76, sock_recv: () => 76, sock_send: () => 76, sock_shutdown: () => 76, path_open: () => 76, path_create_directory: () => 76, path_remove_directory: () => 76, path_rename: () => 76, path_unlink_file: () => 76, path_filestat_get: () => 76, path_readlink: () => 76, path_symlink: () => 76, path_link: () => 76, }, // u6u host functions (no-op for pure logic components) u6u: { http_request: () => 1 }, }; const instance = await WebAssembly.instantiate(wasmModule, wasi); memory = instance.exports.memory as WebAssembly.Memory; const start = (instance.exports._start ?? instance.exports.main) as () => void; if (typeof start !== 'function') throw new Error('WASM missing _start or main export'); try { start(); } catch (e) { if (!(e instanceof Error && e.message === 'wasm exit: 0')) throw e; } const decoder = new TextDecoder(); const total = stdoutChunks.reduce((n, c) => n + c.length, 0); const merged = new Uint8Array(total); let off = 0; for (const chunk of stdoutChunks) { merged.set(chunk, off); off += chunk.length; } const stdout = decoder.decode(merged).trim(); if (!stdout) throw new Error('WASM component produced no output'); return JSON.parse(stdout); }