# `@cfxlabsinc/result/wait-until`

`waitUntil(promise)` runs a side effect without making the caller wait for it.
A service imports it and calls it; nothing has to be handed down from the
request.

```ts
import { waitUntil } from "@cfxlabsinc/result/wait-until";

async create(input: CreateInput): Promise<Result<Thing, ThingError>> {
  const thing = await this.insert(input);
  waitUntil(this.searchCache.warm(thing.id));
  return { ok: true, value: thing };
}
```

## What it is for, and what it is not

The work is **not retried** and its failure reaches **no caller**. It is logged
and sent to Sentry, and that is all. Use it for work the request does not
depend on: warming a cache, a metric, a best-effort notification.

Anything that must happen is not a side effect. Do it in a workflow step, which
retries and which someone can see fail.

**Publishing an event is `waitUntil` work.** `SnsEventPublisher.publish`, which
both event buses send through, hands its SNS call to `waitUntil`, so a caller
is never held up by SNS. It also means an event is best-effort: a send that
fails is logged and reported with the event's name and id, and is not retried,
even inside a Temporal activity that would otherwise have retried it.

## Who holds the work

Whatever is serving the caller keeps the process up until the work settles.
`waitUntil` finds it in this order:

| Where the code is running                                              | What holds the work                                                                                                                    |
| ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| A Lambda handler (`withSentryCapture`, and the HTTP and SNS factories) | The Bun runtime's `context.waitUntil`: the sandbox stays up after the response, until the work settles. Bound with `runWithWaitUntil`. |
| A Next.js request on Vercel or OpenNext                                | The request context Next reads `after()` from, which the host publishes.                                                               |
| A long-lived process (`temporal-worker`, `next start`, a local run)    | Nothing freezes it, so the work simply runs. `drainWaitUntil()` on shutdown waits for what is left.                                    |

A Lambda invocation still ends at the function's timeout, work or no work.

## For a host

- **`runWithWaitUntil(hold, fn)`** binds `hold` for as long as `fn` and what it
  awaits run. It rides on `AsyncLocalStorage`, so two invocations interleaved in
  one process keep their own. `hold` handles a rejection itself; that is where
  an invocation's own logger gets used.
- **`drainWaitUntil()`** waits for the work no host is holding. A long-lived
  process calls it on shutdown, with a cap, since work that never settles would
  otherwise keep it from exiting.
- **`setWaitUntilReporter(report)`** sets where unbound work's failures go and
  returns the previous reporter. The default is `console.error`; a host with
  Sentry points it there. The dashboards do not yet, so a failure in one reaches
  the logs only.

The state lives on `globalThis` under `Symbol.for("@cfxlabsinc/result/wait-until")`. A
bundle that holds this module twice would otherwise bind in one copy and look
in the other.
