# Async and readiness

Some operations are async on the server. The create response
returns immediately with the resource in a non-terminal status;
downstream operations must wait for the resource to reach its
terminal status before proceeding.

## Canonical wait shape

```typescript
async function waitUntilReady<T extends { status: string }>(
  fetcher: () => Promise<{ data: T }>,
  isTerminal: (status: string) => 'ready' | 'failed' | 'pending',
  { intervalMs = 1000, timeoutMs = 60_000 } = {},
): Promise<T> {
  const deadline = Date.now() + timeoutMs
  while (true) {
    const { data } = await fetcher()
    const state = isTerminal(data.status)
    if (state === 'ready') return data
    if (state === 'failed') {
      throw new Error(`failed: ${JSON.stringify(data)}`)
    }
    if (Date.now() > deadline) {
      throw new Error(`timed out waiting for ready`)
    }
    await new Promise((r) => setTimeout(r, intervalMs))
  }
}
```

The shape is the same across resources; only the terminal-
success / terminal-failure status values differ.

## Per-resource readiness signals

The terminal-success and terminal-failure status values are
resource-specific. Common examples:

| Resource                          | Terminal success | Terminal failure        |
|-----------------------------------|------------------|-------------------------|
| Datalake (create + migrate)       | `ready`          | — (no terminal failure) |
| AWS Lambda tool deploy            | `create_complete`| `create_failed`         |
| Workflow run                      | `completed`      | `failed`                |

**A data-activation-client run is NOT on this list — it is not a
`waitUntilReady` status-poll target.** `.ingest` / `.ingestFile` /
`.runManually` return a `202` + a `batch_id` (not a saved row and not
a pollable status enum); progress is tracked through the run record
and the DAC's log rows for that `batch_id` — a dataset row carries no
`batch_id` of its own, it points at its log row — not by polling a
`success`/`error` status. See `data_activation_clients.md` §6.

The Datalake row is the odd one: its top-level `status` enum is
`new | processing | ready` only — there is no terminal failure
value. A migration that fails leaves the datalake at
`processing` (or `new` if it never started). The failure
surfaces in the admin console's Migration Logs view, which is
not exposed over the SDK. Consumers polling for datalake
readiness must therefore rely on `timeoutMs` to bound the wait;
the `'failed'` branch of `waitUntilReady` never fires for this
resource.

The AWS Lambda tool row keys on `tool.body.stack_status` (the
CloudFormation deploy status nested under the polymorphic
`body`), not on the top-level `tool.status` enum (which is
`draft | active | inactive | error | marked_for_deletion`).
`tool.status` flips to `'active'` after a successful deploy
but `stack_status` is the more precise readiness signal: its
terminal-success value is the CloudFormation `create_complete`
(`update_complete` on a redeploy), and a failed deploy lands on
a `*_failed` state (`create_failed`, `rollback_failed`, …) — see
`tools.md` §2 for the per-body field set.

Each resource's MD §5 (Lifecycle) names its terminal-success
and terminal-failure status enums. If your resource isn't in
the table above, its MD is authoritative.

## Downstream resources MUST wait

Downstream operations (creating child resources, running
workflows, invoking tools) MUST NOT proceed until the parent
resource is in a terminal-success status. Creating a child
resource against an unready parent typically surfaces as a
500 (server-internal) or a 422 with a "parent not ready"
detail — both are recoverable but indicate a missing wait in
the caller.

## Idempotency on create

`create()` calls are NOT idempotent by default. Repeating a
`create()` with the same body yields a **409 Conflict** on
uniqueness fields (typically the name within scope — uniqueness
collisions are 409, not 422; see `errors.md`). Consumers handling
retry should:

1. Catch the thrown error after the call returns (see `errors.md`).
2. Inspect for a uniqueness `detail` on a `source.pointer`
   that names the uniqueness key (usually `/name` or `/slug`).
3. Treat as "already created" and fetch the existing row.

This is the "idempotent retry path": use it wherever local
state may have diverged from server state (e.g. a prior run
wrote to the server but failed to capture the response
locally).

## Polling cost

Polling at 1s intervals via `get()` on the resource is cheap
for most resources but
becomes expensive at high concurrency. For long-running
operations (lambda deployments, large data-activation-client
runs), back off the interval (e.g. 2s → 5s → 10s) after the
first ~30s of waiting.

## What the SDK does NOT auto-poll

The typed client returns the immediate `create()` response — it
does NOT block until the resource reaches terminal status.
Consumers explicitly write the wait loop above where it
matters. This is intentional: not every caller wants to
block (some kick off async work and return; some need the
ready signal before downstream).
