App
API reference for App, app.<function>(), and the Session handle
API reference for App, app.<function>(), and Session — calling deployed Python functions and streaming results.
Public API at a glance
| Name | Description |
|---|---|
App(appId, options) | Reference a deployed app |
app.<fnName>(args?) | Invoke a deployed Python function, get a Session |
session.stream(name) | Get a named SessionStream |
session.doc(key) | Get a synced SessionDocument |
session.request() / requestStream() / complete() | Correlated request/response riding the session |
session.onPhase(handler) | Subscribe to phase changes |
session.whenLive(options?) | Resolve at live; reject with a typed failure |
session.touch() | Record client activity (idle-timeout primitive) |
session.recordings / session.artifacts | DVR recordings and runtime artifacts |
session.end() / session.disconnect() | Terminal release vs. detach-and-stay-reconnectable |
Session.attach(id, options) | Join a running session as a consume-only viewer |
App()
function App(appId: string, options: AppOptions): AppReference a deployed uRun app. This is a lookup, not a deployment — the app must already be deployed. No network call happens until you invoke a function.
| Parameter | Type | Required | Description |
|---|---|---|---|
appId | string | yes | The deployed app name |
options.baseUrl | string | yes | Base URL of your uRun backend (e.g. https://urun.sh) |
options.orgId | string | yes | Your organization ID |
options.jwt | string | — | Advanced/manual user JWT (React WorkOS resolves this for you) |
options.getAccessToken | function | — | Provider for fresh browser-safe JWTs |
options.authProvider | string | — | Auth provider id, e.g. "workos" |
import { App } from '@urun-sh/core'
const app = App('my-app', { baseUrl: 'https://urun.sh', orgId: 'your-org-id' })app.<function>()
app.<fnName>(args?: Record<string, unknown>): SessionInvoke a deployed Python function. Names defined with @app.function become methods on the app. Returns a Session synchronously; allocation and connection happen lazily.
| Parameter | Type | Default | Description |
|---|---|---|---|
args | Record<string, unknown> | {} | Invocation arguments, delivered verbatim to the Python function |
args are invocation arguments only — they never override the function's deploy-time GPU/scheduling config.
const session = app.generate({ prompt: 'A cat walking' })session.stream()
session.stream(name: string): SessionStreamGet (or create) a named media stream. The stream name matches ctx.stream(name) on the Python side.
const video = session.stream('video')
video.on('track', (track) => {
if (track) videoEl.srcObject = new MediaStream([track])
})
// publish a local track upstream (attach = mic audio; attachVideo = camera):
await video.attachVideo(cameraTrack)
await mic.attach(micTrack)
// seek within the recorded (DVR) window, or return to live:
await video.seek(12.5)
await video.seek('live')
// data lane: consume (subscribes this peer) and produce
for await (const msg of video.messages()) render(msg)
await video.emit({ kind: 'note', at: Date.now() })SessionStream has .track (latest inbound MediaStreamTrack | null), .attach/.detach (audio), .attachVideo/.detachVideo (video, source swap via replaceTrack), .seek / .onSeeked, .chunks() (lazy byte-stream read, same surface live and post-seek), .messages()/.emit() (the data lane), and .on('track', …).
session.doc()
session.doc(key: string): SessionDocumentGet (or create) a CRDT document for the session (vanilla Yjs underneath). Every doc syncs independently to the runtime — control (read in Python as ctx.doc("control")), the internal request/response docs, and any app-defined key.
session.doc('control').set({ prompt: { text: 'A dog running' } })
session.doc('control').on('change', (snapshot) => {
console.log(snapshot)
}).set(patch) deep-merges. .get(path?) reads a dotted path. .text(field) returns an append-only text value for efficient token streaming; .synced / .onSynced() report when the server's initial state has applied.
session.request()
session.request(payload, options?): Promise<unknown>
session.requestStream(payload, options?): AsyncIterable<string>
session.complete(payload, options?): Promise<unknown>Correlated request/response riding a session doc — no new transport or endpoint, so an in-flight request survives a reconnect. request awaits a single result. requestStream consumes the runtime's response as a stream of incremental token deltas (the OpenAI stream: true shape) on a dedicated, addressed llm-resp:<id> stream; complete is the non-stream convenience over the same lane, resolving with the terminal body. Rejects are typed (RequestTimeoutError, RequestAbortError, RequestError).
session.onPhase()
session.onPhase(handler: (phase: SessionPhase) => void): () => voidSubscribe to high-level phase changes across the nine phases (idle, queued, unavailable, provisioning, connecting, live, error, ended, expired — see Types). Fires immediately with the current phase, then on each transition. Returns an unsubscribe function.
import { describeSessionPhase } from '@urun-sh/core'
session.onPhase((p) => {
statusEl.textContent = describeSessionPhase(p)
if (p.name === 'live') hideSpinner()
})session.whenLive()
session.whenLive(options?: { timeout?: number; signal?: AbortSignal }): Promise<void>Resolve once the session reaches live; reject on error/ended/expired, timeout (default 45s, 0 disables), or abort. The canonical "is media ready?" gate before publishing a local track. Rejects with a typed SessionFailedError carrying a SessionFailureKind and the full SessionStatus snapshot, so you can render why (backend busy vs. network vs. auth) instead of an opaque error. Also available standalone: import { whenLive } from '@urun-sh/core'.
Session properties & lifecycle
| Member | Description |
|---|---|
session.phase / session.status | Current phase snapshot / full diagnosable status (transport state, raw last admission status, elapsed) |
session.endsAt | The session's deadline (Date | null) when the app declared a maximum length; at that instant the phase becomes expired |
session.mediaTransport | 'webtransport' | 'webrtc' | null — which negotiated transport carries media |
session.presence | Ephemeral who's-here presence over the session's Yjs awareness (clears on disconnect) |
session.touch() | Record client activity now — the idle-timeout "I'm still here" signal (throttled internally) |
session.recordings | DVR recordings for this session: list, get, retain(id, "7d"), pin, unpin, permanent, delete |
session.artifacts | Runtime artifacts: list(), getDownloadUrl(artifactId) (short-lived object-store URL) |
session.recordings requires functionsUrl on AppOptions; accessing it otherwise throws a clear error.
session.end() / session.disconnect()
session.end(): Promise<SessionEndResult> // terminal: ends the server session (idempotent)
session.disconnect(): void // detach this handle only; session stays reconnectableend() terminally releases the session on the control plane and resolves with the acknowledged outcome. disconnect() detaches this handle's media transport without ending the server session.
Session.attach()
Session.attach(sessionId: string, options: AttachOptions): Promise<Session>Join an already-running session as a consume-only viewer. The viewer watches the live producer; it never allocates a GPU, never takes the publisher slot, and never tears down the session on disconnect. Cross-org or unknown ids are rejected.
import { Session } from '@urun-sh/core'
const viewer = await Session.attach(sessionId, {
baseUrl: 'https://urun.sh',
orgId: 'your-org-id',
jwt: await getCurrentUserJwt(),
})
const video = viewer.stream('video')See Transport for the phase model and viewer attach in context.