Skip to main content

resonate-supabase-deployments-typescript

Build Resonate workflows on Supabase Edge Functions (TypeScript/Deno) using the Supabase shim, start/probe endpoints, and optional DB progress tracking.

跳到安装

来源信息

仓库
resonatehq/resonate-skills
最近来源活动
2026年8月22日 13:53
检测到的 SKILL.md 语言
英语
星标
6
分支
0

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
name
resonate-supabase-deployments-typescript
description
Build Resonate workflows on Supabase Edge Functions (TypeScript/Deno) using the Supabase shim, start/probe endpoints, and optional DB progress tracking.
license
Apache-2.0
# Resonate Supabase TypeScript ## Overview Build durable workflows on Supabase Edge Functions using Resonate, with start/probe endpoints and optional progress tracking in Supabase. Related: [`resonate-server-postgres`](../resonate-server-postgres/SKILL.md) — the server these workers talk to. Use it for installing `resonate-pg`, the `pg_cron` timer dependency, `invoke`'s 24-hour default timeout, grants, retention, and the known gaps. ## Summary - Build durable workflows on Supabase Edge Functions using the Resonate Supabase shim. - Use when you need long-running, reliable, stepwise workflows triggered by HTTP or external events. - Outputs a `flows/index.ts` workflow module plus start/probe endpoint usage patterns. ## Preconditions / Assumptions - Supabase project with `supabase/functions` directory. - Supabase Edge Functions runtime (Deno) is available. - Resonate Server URL is known and reachable. - Assumes a recent stable Resonate Server and `@resonatehq/supabase` shim. ## Inputs - `RESONATE_URL`: Resonate Server URL (Supabase secret or env var). - `SUPABASE_URL`: Supabase project URL (for DB access). - `SUPABASE_SERVICE_ROLE_KEY`: Service key for server-side DB operations. - Workflow name (function name to register). - Execution ID scheme (uuid or domain id). ## Outputs - `supabase/functions/flows/index.ts` with registered workflows and handler. - HTTP endpoints for `start` and `probe` invocation. - Optional DB table for progress tracking. ## CRITICAL: Architecture & Communication Direction **The edge function initiates communication with the Resonate server, NOT vice versa.** When triggered by an HTTP request, the edge function wakes up and polls the Resonate server for state. The shim handles all HTTP communication internally - you just write normal Resonate code. ``` HTTP Request → Edge Function wakes up → Polls Resonate Server → Executes workflow step → Returns ``` **The Resonate server does NOT call your edge function.** Your edge function calls the server. ## CRITICAL: Constructor & Handler Patterns ### Constructor - Reads from Environment Variables ```ts // Option 1: Let shim read from env vars (RECOMMENDED) const resonate = new Resonate(); // Reads RESONATE_URL and RESONATE_TOKEN automatically // Option 2: Explicit configuration const resonate = new Resonate({ url: Deno.env.get("RESONATE_URL")!, token: Deno.env.get("RESONATE_TOKEN") }); ``` **Note:** The TypeScript types may show `{ verbose?, encryptor? }` - this is because url/token are read from env vars by default. Trust the skill documentation over type hints. ### Handler - Direct Call, NOT a Factory ```ts // ✅ CORRECT - Direct call Deno.serve((req) => resonate.handler(req)); // ❌ WRONG - Factory pattern (old/incorrect) Deno.serve((req) => resonate.handler()(req)); ``` ## Minimal Complete Example This is the simplest working pattern for a Supabase edge function with Resonate: ```ts // supabase/functions/flows/index.ts import { Resonate, Context } from "@resonatehq/supabase"; const resonate = new Resonate(); function* myWorkflow(ctx: Context, name: string): Generator<any, string, any> { yield* ctx.run(() => console.log(`Hello, ${name}!`)); yield* ctx.sleep(1000); return `Completed for ${name}`; } resonate.register("myWorkflow", myWorkflow); Deno.serve((req) => resonate.handler(req)); ``` **That's it.** No complex wrappers, no HTTP routing, no state management. The shim handles everything. ## Core concepts (minimal) - Supabase Edge Functions are short-lived; Resonate makes them durable via checkpoints. - The Resonate Supabase shim exposes `Resonate` and `Context` and provides `resonate.handler(req)`. - `start` and `probe` endpoints manage top-level workflow execution. - `context.id` is the execution id (passed via `start`), useful for DB correlation. - Durable steps are all `yield*` calls on `Context`. ## Procedure ### 1) Confirm the Supabase function layout Intent: keep entrypoints consistent with Resonate shim routing. Ensure this structure exists: ``` supabase/ |-- config.toml `-- functions/ |-- flows/ | |-- deno.json | `-- index.ts |-- probe/ | |-- deno.json | `-- index.ts `-- start/ |-- deno.json `-- index.ts ``` Do not modify `start/` and `probe/` if they are provided by your template; only edit `flows/index.ts`. ### 2) Implement workflows in `flows/index.ts` Intent: register durable functions and expose the Resonate handler. ```ts // supabase/functions/flows/index.ts import { Resonate, type Context } from "@resonatehq/supabase"; import { createClient } from "https://esm.sh/@supabase/supabase-js@2"; // Initialize Resonate - reads RESONATE_URL from env by default // For token auth, pass token explicitly or set RESONATE_TOKEN env var const resonate = new Resonate({ url: Deno.env.get("RESONATE_URL")!, token: Deno.env.get("RESONATE_TOKEN") // JWT token if auth required }); const supabase = createClient( Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")! ); resonate.setDependency("supabase", supabase); function* processOrder(ctx: Context, orderId: string) { const supabase = ctx.getDependency("supabase"); // persist progress yield* ctx.run(async () => { await supabase.from("order_progress").upsert({ id: ctx.id, order_id: orderId, status: "started", updated_at: new Date().toISOString(), }); }); const order = yield* ctx.run(loadOrder, orderId); yield* ctx.run(async () => { await supabase.from("order_progress").update({ status: "loaded", updated_at: new Date().toISOString(), }).eq("id", ctx.id); }); // The SDK generates the promise id — create it first, then store the id so // the approving client can find it. const approval = yield* ctx.promise<boolean>(); yield* ctx.run(async () => { await supabase.from("orders") .update({ approval_promise_id: approval.id }) .eq("id", ctx.id); }); const ok = yield* approval; if (!ok) throw new Error("rejected"); return { orderId, status: "approved" }; } async function loadOrder(_: Context, orderId: string) { return { id: orderId }; } resonate.register("processOrder", processOrder); Deno.serve((req) => resonate.handler(req)); ``` ### 3) Trigger a workflow via `start` Intent: start durable execution from your app or curl. ```ts const response = await fetch(`${SUPABASE_URL}/functions/v1/start`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${SUPABASE_ANON_KEY}`, }, body: JSON.stringify({ uuid: `order/${orderId}`, func: "processOrder", args: [orderId], }), }); const { uuid } = await response.json(); ``` ### 4) Check status via `probe` Intent: poll or gate UI with execution state. ```ts const response = await fetch(`${SUPABASE_URL}/functions/v1/probe`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${SUPABASE_ANON_KEY}`, }, body: JSON.stringify({ uuid: `order/${orderId}` }), }); const { status, value } = await response.json(); ``` ### 5) Resolve external promises (HITL or webhook) Intent: resume a blocked workflow. ```ts await fetch(`${RESONATE_URL}/promises/${promiseId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ state: "RESOLVED", value: { data: "true" } }), }); ``` ## Starting Workflows There are two patterns for starting workflows in Supabase Edge Functions: ### Pattern A: Use the start/probe template (Recommended) If your template provides `start/` and `probe/` functions, use them: ```ts // From your frontend or another service const response = await fetch(`${SUPABASE_URL}/functions/v1/start`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${SUPABASE_ANON_KEY}`, }, body: JSON.stringify({ uuid: `countdown-${crypto.randomUUID()}`, func: "durableCountdown", args: [name, durationMinutes, targetTime], }), }); ``` The `start/` function internally calls `resonate.run()` for you. ### Pattern B: Custom Deno.serve() with resonate.run() If you need custom routing or the template doesn't fit your needs: ```ts // supabase/functions/flows/index.ts import { Resonate, type Context } from "@resonatehq/supabase"; const resonate = new Resonate({ url: Deno.env.get("RESONATE_URL")!, token: Deno.env.get("RESONATE_TOKEN") }); function* durableCountdown(ctx: Context, name: string, durationMinutes: number) { let remaining = durationMinutes; while (remaining > 0) { yield* ctx.run(sendWebhook, { type: "tick", name, remaining }); yield* ctx.sleep(60 * 1000); remaining--; } yield* ctx.run(sendWebhook, { type: "complete", name }); return { status: "completed", name }; } resonate.register("durableCountdown", durableCountdown); // Custom request handler Deno.serve(async (req) => { const url = new URL(req.url); // Start a new workflow if (req.method === "POST" && url.pathname.endsWith("/start")) { const { name, durationMinutes } = await req.json(); const promiseId = `countdown-${crypto.randomUUID()}`; // This starts AND executes the workflow resonate.run(promiseId, durableCountdown, name, durationMinutes); return new Response(JSON.stringify({ promiseId }), { headers: { "Content-Type": "application/json" } }); } // List active workflows by querying Resonate server directly if (req.method === "GET" && url.pathname.endsWith("/list")) { const resonateUrl = Deno.env.get("RESONATE_URL")!; const token = Deno.env.get("RESONATE_TOKEN"); const response = await fetch( `${resonateUrl}/promises?id=countdown-*&state=pending&limit=50`, { headers: { "Content-Type": "application/json", ...(token && { "Authorization": `Bearer ${token}` }) } } ); const data = await response.json(); return new Response(JSON.stringify(data), { headers: { "Content-Type": "application/json" } }); } // Default: let Resonate shim handle workflow execution callbacks return resonate.handler(req); // Direct call, NOT a factory }); ``` ### Key Insight: resonate.run() vs HTTP API **Do NOT create promises via direct HTTP API to start workflows.** ```ts // ❌ WRONG - This creates a promise but doesn't execute workflow code await fetch(`${RESONATE_URL}/promises`, { method: "POST", body: JSON.stringify({ id: "my-workflow", timeout: 86400000 }) }); // ✅ CORRECT - This creates the promise AND executes the workflow resonate.run("my-workflow", myWorkflowFunction, arg1, arg2); ``` The HTTP API (`POST /promises`) is for creating standalone promises that will be resolved externally (like human-in-the-loop). To actually RUN workflow code, you must use `resonate.run()`, `resonate.rpc()`, or the `start/` template endpoint. ## Code patterns ### Durable sleep ```ts function* reminder(ctx: Context, userId: string) { yield* ctx.run(sendEmail, userId); yield* ctx.sleep(24 * 60 * 60 * 1000); yield* ctx.run(sendFollowUp, userId); } ``` ### Structured concurrency (fork-join) ```ts function* validate(ctx: Context, orderId: string) { const a = yield* ctx.beginRun(checkInventory, orderId);
在 GitHub 查看
这个 SKILL.md 很大,SkillsMP 这里只预览前一段内容。 在 GitHub 查看