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
آخر نشاط في المصدر
٢٢ أغسطس ٢٠٢٦ في ١٣:٥٣
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٦
التفرعات
٠

خيارات التثبيت

يُحدَّد 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