Session Lifecycle
The nine honest phases, scale-to-zero waking UX, Session expiry, idle handling, and DVR recordings
Sessions are honest by default. The SDK never hides what a GPU Session is doing behind one opaque spinner: waking, waiting for capacity, expiring, and idling are all first-class, typed states with drop-in UI. This page is the end-to-end lifecycle toolkit.
The nine phases
idle → queued | unavailable | provisioning → connecting → live → ended | expired
↘ error| Phase | Meaning |
|---|---|
idle | Created, not yet admitted or connected |
queued | Waiting in the admission queue (phase.queue = { position, depth }) |
unavailable | No runtime exists to serve this function — non-terminal; recovers when one appears |
provisioning | Admitted; compute is being allocated/warmed |
connecting | Transport is establishing the media Session |
live | First media track has arrived |
error | Admission or transport failed (phase.error.kind names why) |
ended | Intentionally disconnected / closed |
expired | Reached its declared maximum length — terminal and honest, not an error |
Three helpers cover most phase UI: describeSessionPhase(p) returns honest default copy ("Starting model…", a queue position, "Runtime unavailable — …"); isWakingPhase(p) names the pre-live waiting set; whenLive(session) / session.whenLive() is the "is media ready?" gate that rejects with a typed SessionFailedError (kind + full SessionStatus) instead of an opaque failure. UrunSessionStatus renders the phase line; UrunSessionGate gates children on live.
Scale-to-zero waking
Apps scale to zero, so a first Session may wake a runtime. The SDK makes that wait honest and short:
useUrunPrewake({ function: 'generate' })— the intent signal. While a page that will start a Session is mounted, it pings the advisory prewake (on mount, then every 60s) so capacity is already warming when the user clicks start. Never throws, never affects the Session flow.useSessionWake(session)— a live{ waking, state, reason, seconds }view of the pre-live wait, counted fromphase.wakingSinceas one continuous clock acrossqueued/unavailable/provisioning(never resetting per sub-phase).UrunSessionWaking— the drop-in "waking (Ns)" element; renders nothing when not waking.
import { useUrunPrewake, UrunSessionWaking } from '@urun-sh/react'
function GeneratePage({ session }) {
useUrunPrewake({ function: 'generate' }) // warm while the page shows intent
return <UrunSessionWaking session={session} />
}While waiting, phase.runtime reports why: unavailable (no runtime registered), starting (cold-starting/warming), or busy (all runtimes serving other Sessions), plus a frontend-safe reason.
Session clock and expiry
A function can declare a maximum Session length; the platform then ends the Session honestly at its deadline with the terminal phase expired — distinct from error because nothing failed.
session.endsAt/useSessionEndsAt(session)— the deadline as aDate, ornullwhen no maximum was declared.UrunSessionClock— an mm:ss countdown to the deadline; renders nothing without one (compose it into any header unconditionally), flagsdata-urgentin the last minute.UrunSessionEnded— the terminal ended/expired surface, with anonNewSessionhook to start fresh.
Idle warning
Attached-but-inert Sessions time out (platform default; Apps configure or opt out server-side). The SDK auto-stamps pointer/key/visibility activity into the Session's awareness; before the kick, the server raises a warning in the control doc:
useSessionIdle(session)— the raised warning ({ warning, deadlineEpochS, idleSinceEpochS }), ornullin the steady state.UrunIdleWarning— the "Are you still there?" overlay with a countdown; its button confirms presence viasession.touch().session.touch()— record application-specific activity explicitly (throttled internally). Vanilla (non-React) applications can wire DOM activity withcreateActivityTrackerfrom@urun-sh/core.
import { UrunSessionClock, UrunSessionEnded, UrunIdleWarning } from '@urun-sh/react'
function Shell({ session, children }) {
return (
<>
<header><UrunSessionClock session={session} /></header>
{children}
<UrunIdleWarning session={session} />
<UrunSessionEnded session={session} onNewSession={() => location.reload()} />
</>
)
}Recordings: the DVR
Session streams are recorded by default — every Session is replayable, and stream.seek(t) addresses the recording while seek('live') snaps back to the live edge. Retention is explicit:
- Recordings start in the short-lived
tempbucket;retain(id, '7d')keeps one for a duration,pin(id)moves it to the durable bucket,permanent(id)locks it,delete(id)removes it. - Mutations are async intents: a recording reports
pending_pin/pending_delete/ … until the reconciler settles it — surface that (isPendingRecording(r)) instead of assuming completion.
const recordings = await session.recordings.list() // this Session's recordings
await session.recordings.retain(recordings[0].id, '7d')Replay after end. Recording playback outlives the Session: attaching to an ended Session whose recording exists (Session.attach(id)) answers a replay Session — the platform replays the recording through the normal Session stream surface (no GPU runtime is woken), so the same <Video> / track rendering works unchanged. Drive playback with stream('video').seek(0). Video streams replay today; an ended Session with no recording stays an honest dead end rather than a faked replay.
const replay = await Session.attach(endedSessionId, options) // replay Session for an ended id
await replay.stream('video').seek(0) // play the recording from the startsession.recordings requires functionsUrl on your AppOptions (a clear error tells you if it's missing); the standalone RecordingsClient from @urun-sh/core covers non-Session (org-scoped) access. Files the runtime produces (e.g. rendered outputs) surface separately as artifacts: session.artifacts.list() and getDownloadUrl(artifactId) return short-lived object-store URLs (RuntimeArtifactsClient is the standalone form).
Related:
Was this page helpful?