UNPKG

@resonatehq/aws

Version:

Resonate FaaS handler for AWS Lambda Functions (TypeScript)

185 lines (169 loc) 4.13 kB
import { type Func, Handler, HttpNetwork, JsonEncoder, NoopHeartbeat, Registry, ResonateInner, type Task, WallClock, } from "@resonatehq/sdk"; import type { LambdaFunctionURLHandler, LambdaFunctionURLResult, } from "aws-lambda"; export class Resonate { private registry = new Registry(); private verbose: boolean; constructor({ verbose = false }: { verbose?: boolean } = {}) { this.verbose = verbose; } public register<F extends Func>( name: string, func: F, options?: { version?: number; }, ): void; public register<F extends Func>( func: F, options?: { version?: number; }, ): void; public register<F extends Func>( nameOrFunc: string | F, funcOrOptions?: | F | { version?: number; }, maybeOptions: { version?: number; } = {}, ): void { const { version = 1 } = (typeof funcOrOptions === "object" ? funcOrOptions : maybeOptions) ?? {}; const func = typeof nameOrFunc === "function" ? nameOrFunc : (funcOrOptions as F); const name = typeof nameOrFunc === "string" ? nameOrFunc : func.name; this.registry.add(func, name, version); } public httpHandler(): LambdaFunctionURLHandler { return async ( event, _context, _callback, ): Promise<LambdaFunctionURLResult> => { try { if (event.requestContext.http.method !== "POST") { return { statusCode: 405, body: JSON.stringify({ error: "Method not allowed. Use POST." }), }; } // Ensure required headers const proto = event.headers["x-forwarded-proto"]; const host = event.headers.host; if (!proto || !host) { return { statusCode: 400, body: JSON.stringify({ error: "Missing required headers: x-forwarded-proto or host.", }), }; } // Construct full invocation URL const url = `${proto}://${host}${event.requestContext.http.path ?? ""}`; // Ensure body exists if (!event.body) { return { statusCode: 400, body: JSON.stringify({ error: "Request body missing." }), }; } // Parse JSON body const body = JSON.parse(event.body); // Validate task structure if ( !body || !(body.type === "invoke" || body.type === "resume") || !body.task ) { return { statusCode: 400, body: JSON.stringify({ error: 'Request body must contain "type" and "task" for Resonate invocation.', }), }; } const encoder = new JsonEncoder(); const network = new HttpNetwork({ headers: {}, timeout: 60 * 1000, url: body.href.base, verbose: this.verbose, }); const resonateInner = new ResonateInner({ anycastNoPreference: url, anycastPreference: url, clock: new WallClock(), dependencies: new Map(), handler: new Handler(network, encoder), heartbeat: new NoopHeartbeat(), network, pid: `pid-${Math.random().toString(36).substring(7)}`, registry: this.registry, ttl: 30 * 1000, unicast: url, verbose: this.verbose, }); // Create unclaimed task const task: Task = { kind: "unclaimed", task: body.task }; // Process the task and await result const result = new Promise<LambdaFunctionURLResult>((resolve) => { resonateInner.process(task, (error, status) => { if (error || !status) { resolve({ statusCode: 500, body: JSON.stringify({ error: "Task processing failed", details: { error, status }, }), }); return; } if (status.kind === "completed") { resolve({ statusCode: 200, body: JSON.stringify({ status: "completed", result: status.promise.value, requestUrl: url, }), }); } else { resolve({ statusCode: 200, body: JSON.stringify({ status: "suspended", requestUrl: url, }), }); } }); }); return result; } catch (error) { return { statusCode: 500, body: JSON.stringify({ error: `Handler failed: ${error}`, }), }; } }; } }