@resonatehq/cloudflare
Version:
Resonate FaaS handler for Cloudflare Workers (TypeScript)
219 lines (202 loc) • 4.87 kB
text/typescript
import type { ExecutionContext } from "@cloudflare/workers-types";
import {
type Func,
Handler,
HttpNetwork,
JsonEncoder,
NoopHeartbeat,
Registry,
ResonateInner,
type Task,
WallClock,
} from "@resonatehq/sdk";
import {
type Encryptor,
NoopEncryptor,
} from "@resonatehq/sdk/dist/src/encryptor";
export class Resonate {
private registry = new Registry();
private dependencies = new Map<string, any>();
private verbose: boolean;
private encryptor: Encryptor;
private initializer?: (env: Record<string, string>) => Promise<void>;
constructor({
verbose = false,
encryptor = undefined,
}: { verbose?: boolean; encryptor?: Encryptor } = {}) {
this.verbose = verbose;
this.encryptor = encryptor ?? new NoopEncryptor();
}
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 onInitialize(
fn: (env: Record<string, string>) => Promise<void>,
): void {
this.initializer = fn;
}
public setDependency(name: string, obj: any): void {
this.dependencies.set(name, obj);
}
public handlerHttp(): {
fetch: (
request: Request,
env: Record<string, string>,
ctx: ExecutionContext,
) => Promise<Response>;
} {
return {
fetch: async (
request: Request,
env: Record<string, string>,
_ctx: ExecutionContext,
): Promise<Response> => {
if (this.initializer !== undefined) {
await this.initializer(env);
}
try {
if (request.method !== "POST") {
return new Response(
JSON.stringify({ error: "Method not allowed. Use POST." }),
{
status: 405,
},
);
}
const formattedUrl = new URL(request.url);
const url = `${formattedUrl.protocol}//${formattedUrl.host}`;
const body: any = await request.json();
if (!request.body) {
return new Response(
JSON.stringify({
error: "Request body missing.",
}),
{
status: 400,
},
);
}
if (
!body ||
!(body.type === "invoke" || body.type === "resume") ||
!body.task
) {
return new Response(
JSON.stringify({
error:
'Request body must contain "type" and "task" for Resonate invocation.',
}),
{
status: 400,
},
);
}
const encoder = new JsonEncoder();
const network = new HttpNetwork({
headers: {},
timeout: 60 * 1000, // 60s
url: body.href.base,
verbose: this.verbose,
});
const resonateInner = new ResonateInner({
anycastNoPreference: url,
anycastPreference: url,
clock: new WallClock(),
dependencies: this.dependencies,
handler: new Handler(network, encoder, this.encryptor),
heartbeat: new NoopHeartbeat(),
network,
pid: `pid-${Math.random().toString(36).substring(7)}`,
registry: this.registry,
ttl: 30 * 1000, // 30s
unicast: url,
verbose: this.verbose,
});
const task: Task = { kind: "unclaimed", task: body.task };
const completion: Promise<Response> = new Promise((resolve) => {
resonateInner.process(task, (error, status) => {
if (error || !status) {
resolve(
new Response(
JSON.stringify({
error: "Task processing failed",
details: { error, status },
}),
{
status: 500,
},
),
);
return;
}
if (status.kind === "completed") {
resolve(
new Response(
JSON.stringify({
status: "completed",
result: status.promise.value,
requestUrl: url,
}),
{
status: 200,
},
),
);
return;
} else if (status.kind === "suspended") {
resolve(
new Response(
JSON.stringify({
status: "suspended",
requestUrl: url,
}),
{
status: 200,
},
),
);
return;
}
});
});
return completion;
} catch (error) {
return new Response(
JSON.stringify({
error: `Handler failed: ${error}`,
}),
{ status: 500 },
);
}
},
};
}
}