Skip to content
Subako TypeScript SDK
Esc
navigateopen⌘Jpreview
On this page

The session connection

connect() opens a session, keeps its log, and reconnects when the stream drops.

connect gives you a live view of one session. It reads the log, opens the stream, and holds on to both. Whether you are rendering a chat or waiting for a job to finish, everything comes off this object — you do not keep a copy of the messages yourself.

const session = subako.sessions.connect(sessionId, { history: true }); // subako.connect on a session client

await session.ready; // the history is read and the stream is open

What it holds

session.state.status; // "connecting" | "open" | "reconnecting" | "closed" | "failed"
session.state.error; // the failure, set when the status is "failed"
session.state.lastSeq; // number | undefined
session.state.events; // every event seen, in seq order
session.state.transcript; // the messages of the log, each call joined to its result and approval
session.state.approvals; // the approvals no decision has settled
session.state.isRunning; // a run queued or under way

state is immutable. You get a new object on every event and every status change, and the same object for as long as nothing changes. events follows the same rule: a new array when an event arrives, the same one otherwise, and the events inside are never copied. That is what makes it safe to hand straight to a renderer:

const state = useSyncExternalStore(session.subscribe, () => session.state);
state.transcript; // what this render draws

state.events is this client’s copy of the log: raw wire bodies in seq order. The connection does not interpret them. It joins no messages and builds no strings, so what an event means stays your decision. Reading the log covers the three helpers that do the interpreting.

Driving the session

The same object is how you drive the session, so nothing else has to hold state:

await session.send("book me a room"); // posts a user turn
await session.cancel(); // stops the active run at its next checkpoint
await session.resolveApproval(callId, "deny", "not this one"); // "allow" | "deny"

session.transcript; // the same three readings, taken off the connection
session.approvals;
session.isRunning;

Those three appear in two places, and it is one value either way: derived from state.events on first read, and recomputed only when that array changes. Read them off state when you hold a snapshot — inside a render, or in a subscribe listener. Read them off the connection anywhere else, where you want the latest rather than the one that render was woken for.

Iterating and subscribing

const stop = session.subscribe((state) => render(state)); // returns an unsubscribe

for await (const event of session) {
	// The events so far, then live ones. `break` stops iterating, not the connection.
}

session.close(); // ends the stream; the clients on it are deleted
session.reconnect(); // only after a failure: opens the stream again on this same connection

Subscribers are called synchronously after every change, in seq order, with the states that follow their subscription. A listener that throws is reported with console.error and delivery carries on.

Options

PropType
history?boolean

Reads the log to its end first, oldest first; the stream then starts at the last seq. This is what a chat that draws the conversation so far wants.

Typeboolean
afterSeq?number

Otherwise: where the stream starts. Omit for the live head, or pass `0` to replay the log.

Typenumber
signal?AbortSignal

Aborting closes the connection, as `close()` does.

TypeAbortSignal
maxRetries?number

Retries of the request that opens the stream, as for any call.

Typenumber
maxReconnects?number

Consecutive failed reconnects tolerated. Defaults to 5.

Typenumber

A connection with neither history nor afterSeq starts at the live head: it carries what happens from now on, and nothing that happened before.

What happens when it drops

The server tails the log forever, so the connection ends only when you end it.

  • Opening the stream is a request like any other. It is retried under maxRetries on the same terms, and a refusal such as a 404 fails the connection right away, with ready rejecting and state.error set.

  • Once open, every close is followed by a wait and a fresh connection carrying Last-Event-ID, whether the server closed cleanly or the connection dropped. The server opens every stream with a cursor frame naming where it starts, so a reconnect resumes from there even when the connection carried no event at all. The wait is the server’s own retry: value when it sends one, else 3 seconds, doubling up to 30 seconds while reconnects keep failing and resetting as soon as an event arrives. After maxReconnects consecutive failed reconnects the status is failed and state.error holds the last failure. A reconnect the server refuses outright fails at once: a 404, or a 401 that a freshly resolved token did not settle.

  • A connection that failed can be opened again. reconnect() starts the stream over from lastSeq on the same connection object — the same subscribers, the same log, the same clients — with the reconnect budget reset and ready a fresh promise that resolves when the stream is open. The tool clients the failure sent away register again once it is, as they would on a connection opened from scratch. In any other status, a closed connection included, it does nothing. Nothing calls it for you: it is what a “reconnect” button in a chat is wired to.

Browser support

The SDK calls AbortSignal.any to join your signal to its own timeout, which puts the browser floor at Safari 17.4, Chrome 116, and Firefox 124.

Aborting with your own AbortSignal rethrows the abort reason, untouched and never wrapped in a SubakoError — whatever you passed to abort(reason), or the AbortError DOMException the platform supplies when you passed nothing. A caller that aborts deliberately can therefore tell its own cancellation from a failure without inspecting a message.

Was this page helpful?