Long-running operations
Some platform work — deploying an application, shipping storage between hosts — does not finish inside one HTTP request. For these, the SDK never blocks the kick-off call. Instead the method returns an Operation<T>: the kick-off result plus a waiter you call when (and if) you want to block until the work lands.
const op = await client.applications.deploy(appId, { canonical });// The POST has already happened — op.resource is the accepted result.const finalState = await op.waitUntilReady();The Operation abstraction
Section titled “The Operation abstraction”interface Operation<T> { /** The kick-off result (e.g. the newly cut release, the upload grant). */ readonly resource: T; /** Polls to a terminal state; throws WaiterError on timeout or failure. */ waitUntilReady(opts?: WaitOptions): Promise<OperationState>;}
interface WaitOptions { timeoutMs?: number; // default 300_000 (5 minutes) pollIntervalMs?: number; // fixed interval; default is backed-off (1s → 10s, ×1.5) signal?: AbortSignal; // aborts the wait between polls onProgress?: (state: OperationState) => void;}
interface OperationState { phase: string; // operation-specific, e.g. 'deploying_serving' terminal: boolean; raw: unknown; // the full status response from the last poll}Semantics, exactly as implemented:
- Kick-off returns immediately. The HTTP request that starts the work completes before you get the
Operation. You can ignore the waiter entirely and poll yourself with the corresponding status method (listed per operation below). - Polling cadence. The first poll happens immediately. Without
pollIntervalMs, the interval starts at 1 s and backs off ×1.5 to a 10 s ceiling. PassingpollIntervalMsfixes the interval and disables backoff. - Progress.
onProgressis invoked after every poll with the latestOperationState, including the final one. The SDK never renders anything — progress display is yours. - Success resolves with the final
OperationState. - Failure or timeout throws
WaiterError, which carries the last observed state:code: 'WAITER_FAILED'— the operation reached a terminal failure phase.code: 'WAITER_TIMEOUT'—timeoutMselapsed first.- Either way,
error.lastStateis the lastOperationStateanderror.lastState.rawis the full last status response — log it; it usually says why.
import { WaiterError } from '@edgible-team/sdk';
try { await op.waitUntilReady({ timeoutMs: 600_000 });} catch (err) { if (err instanceof WaiterError) { console.error(`gave up in phase '${err.lastState.phase}'`, err.lastState.raw); } throw err;}The operations
Section titled “The operations”These methods on the standard client (createClient from @edgible-team/sdk) return an Operation:
| Method | resource | Status poll | Success phase | Failure phases |
|---|---|---|---|---|
client.applications.deploy(applicationId, { canonical }) | ApplicationDeclarationApplied (the newly cut immutable release — the declaration version IS the release, ADR-0008) | client.applications.getDeploymentStatus(applicationId) | ready | error, degraded |
client.storage.promote({ applicationId, name, mobility? }) | PromoteRecord | client.storage.getPromote(promoteId) | completed | failed, cancelled |
client.storage.createUpload({ applicationId, name, … }) | StorageUploadGrant — includes the one-time sessionToken, never returned by later reads | client.storage.getUpload({ applicationId, uploadId }) | completed | failed, cancelled |
Storage upload is a session machine: the kick-off mints the session (phases run pending → awaiting-upload → … → completed); you stream the tarball to resource.ingestUrl with resource.sessionToken while the waiter tracks the session.
Platform admin tooling (
@edgible-team/sdk/admin, admin credentials required) exposes further long-running operations — cloud-host provisioning and decommission, and migrations — which follow the sameOperationpattern. They are part of the internal/operator reference, not the public developer surface.
Worked example: deploy and wait
Section titled “Worked example: deploy and wait”import { createClient, WaiterError } from '@edgible-team/sdk';
const client = createClient({ env: 'prod' });
const op = await client.applications.deploy(applicationId, { canonical: declaration });console.log(`accepted as declaration v${op.resource.version}`);
try { const final = await op.waitUntilReady({ timeoutMs: 600_000, onProgress: (state) => console.log(`deployment: ${state.phase}`), }); console.log(`done: ${final.phase}`); // 'ready'} catch (err) { if (err instanceof WaiterError) { console.error(`deploy ended in '${err.lastState.phase}'`, err.lastState.raw); } throw err;}Typical progress output:
accepted as declaration v3deployment: deployingdeployment: deploying_servingdeployment: serving_readydeployment: deploying_gatewaydeployment: readydone: readyUse the deployment-state FSM as your readiness signal, not the runtime status field. The waiter polls GET /applications/{id}/deployment-status, a finite-state machine driven by the devices as they apply the declaration (pending → deploying → deploying_serving → serving_ready → deploying_gateway → ready). The application record’s runtime status (deployed/deploying/…) lags it — don’t poll that. degraded means some device errored or degraded and is sticky until that device reports again; the waiter treats it as terminal failure.
Timeouts and cancellation
Section titled “Timeouts and cancellation”The knobs that exist:
timeoutMs(perwaitUntilReadycall, default 300 000 ms) — overall deadline for the wait. Large storage transfers can legitimately exceed five minutes; raise it.signal— anAbortSignalstops the waiter between polls; the promise rejects with the signal’s reason. This cancels your wait only — the server-side work continues.pollIntervalMs— fix the cadence (e.g.1000for snappier progress in a CLI); omit for the default backoff.- Per-request options on the kick-off — every kick-off method accepts a second/third
RequestOptionsargument ({ signal, timeoutMs, idempotencyKey }) that applies to the kick-off HTTP request itself, not to the waiter.
To cancel the server-side work where the platform supports it:
- Uploads:
client.storage.cancelUpload({ applicationId, uploadId }). - Migrations:
client.migrations.cancel(migrationId).
There is no cancel for deploys or promotes (past kick-off) — abandoning the waiter just means you stop watching.
Because the Operation is decoupled from the waiter, “fire and check later” needs no special API: keep the id from op.resource and call the status method (table above) from any process, any time.
See also
Section titled “See also”- Errors — the
EdgibleErrorhierarchy, branching oncode,requestId. - Idempotency & safe re-runs — what happens if you re-issue a kick-off.