# Background tasks

Run heavy or slow work **outside** the request/response cycle: a request returns immediately and
the work happens later, in the background, with **retries** and **de-duplication**. Use it for
anything that would blow the request budget or that should survive the response: processing an
upload, calling an external API, an ingestion pipeline, sending a batch of emails.

A task runs in an **ephemeral isolate**: the platform loads your app's code, runs one handler, and
tears it down. The app does **not** have to be serving HTTP for a task to run.

## Declare handlers with `withTasks`

Wrap your default HTTP handler with `withTasks` and pass a map of named task handlers. The default
export stays your request handler; onvibe reads the task handlers from it.

```ts
import { enqueueTask, withTasks } from "./.onvibe/helpers.ts";

async function handler(req: Request): Promise<Response> {
  const url = new URL(req.url);
  if (req.method === "POST" && url.pathname === "/import") {
    const { fileId } = await req.json();
    const { taskId } = await enqueueTask("process_import", { fileId }, {
      dedupKey: `import:${fileId}`,
    });
    return Response.json({ taskId }, { status: 202 });
  }
  return new Response("hello");
}

export default withTasks(handler, {
  // Each handler gets the payload you passed to enqueueTask. Keep it IDEMPOTENT:
  // a task can run more than once on retry; the source of truth is your database.
  process_import: async ({ fileId }) => {
    const res = await fetch(`https://api.example.com/files/${fileId}`); // external fetch is allowed
    // ...heavy work, write results to your Postgres...
    return { processed: true };
  },
});
```

## Enqueue work with `enqueueTask`

```ts
const { taskId, deduplicated } = await enqueueTask("process_import", { fileId }, {
  dedupKey: "import:42",  // optional: won't enqueue a second task while one with this key is active
  maxAttempts: 3,         // optional: retries on failure (exponential backoff)
});
```

- `enqueueTask(name, data?, opts?)` returns `{ taskId, deduplicated }`. `name` must match a handler
  registered in `withTasks`.
- **De-dup** is scoped to your app: `dedupKey` never collides with other apps.
- Enqueue from anywhere in your handler — a request, a cron, or even another task.

## Check status with `checkTask`

```ts
const status = await checkTask(taskId);
// null if no such task, else:
// { status: "pending" | "running" | "done" | "failed", result?: unknown, error?: string }
if (status?.status === "done") {
  // status.result is whatever the task handler returned
}
```

## Notes

- **Idempotency is your job**: a task may run more than once (retries, timeouts). Make handlers
  safe to re-run; keep the real state in your database.
- **Retries**: failures retry with exponential backoff up to `maxAttempts` (default 3), then the
  task is `failed` (its error is in `checkTask().error`).
- **External network**: task handlers can `fetch` the public internet (RSS, third-party APIs, LLMs).
- **Timeouts**: a task that runs too long is considered stuck and retried.
- Tasks vs cron: **cron** fires your normal handler on a schedule (see scheduled tasks); **tasks**
  run a `withTasks` handler you enqueue on demand, off the request path.
