feat(react-hooks): add useSession hook for reading session channels - #4808
feat(react-hooks): add useSession hook for reading session channels#4808claude[bot] wants to merge 2 commits into
Conversation
🦋 Changeset detectedLatest commit: 3873692 The changes in this PR will be included in the next version bump. This PR includes changesets to release 27 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Preview packages for this PR's head commit These builds are pinned to that head commit; a new push publishes fresh ones and the bot will comment with them itself. Generated by Claude Code |
|
Closing in favor of a combined realtime-streams PR that folds this hook in alongside the start-from-latest subscribe option and the token-refresh work from #4809. As part of that consolidation the hook is being renamed from |
…#4811) ## Summary Realtime streams get a live "last value" mode: subscribe from the latest record instead of replaying the whole history, keep memory bounded, and resume across reloads. Plus a new `useSessionStream` hook for reading a Session's channels from React. ## `useRealtimeStream`: start-from-latest, bounded, resumable ```tsx const { parts, lastEventId } = useRealtimeStream<Frame>(runId, "frames", { from: "latest", // skip history, only new records after connect maxParts: 1, // keep just the most recent (bounded memory) lastEventId: saved, // resume from a persisted cursor (survives reload) onParts: (batch) => save(batch.at(-1)?.id), // per-batch event ids accessToken, }); ``` `from`, `lastEventId` (option and return), and the batching also apply to `streams.read()` and `fetchStream()`. ## `useSessionStream`: read a Session channel from React (new) A read-only hook for a Session's `out` (default) or `in` channel, with the same start / bound / resume options. `useSession` is reserved for two-way (read and write). ```tsx const { records, lastEventId } = useSessionStream<Frame>(sessionId, { io: "out", from: "latest", maxRecords: 5, onRecords: (batch) => {/* each throttled batch, with event ids */}, accessToken, }); ``` ## Access-token refresh Long-lived subscriptions can survive token expiry: pass `refreshAccessToken` and a 401/403 triggers one re-mint and reconnect. With no refresher, auth errors stay terminal exactly as before. ```tsx const { parts } = useRealtimeStream<Frame>(runId, "frames", { accessToken, // called on a 401/403 to mint a fresh public token from your backend refreshAccessToken: async () => { const res = await fetch("/api/realtime-token"); return (await res.json()).token; }, }); ``` It is also available on `useApiClient` / `TriggerAuthContext`, so every hook under a provider shares one refresher. ## Notes Server support (S2 `tail_offset` / Redis `$`, and the start-position header on the run and session SSE routes) ships here; a client passing `from: "latest"` against an older server degrades safely to a full replay. Resume, bounded memory, batched callbacks, and token refresh are client-only. Supersedes #4808 and #4809, folded in here. Verified end to end on an isolated stack: `from: "latest"` on the run and session paths against real S2, `lastEventId` resume across a reload, bounded memory, batched callbacks, and a real 401 to token-refresh to reconnect.
Requested by Matt Aitken · Slack thread
✅ Checklist
Testing
Exercised from a React app against a session created by an agent task: render a component calling
useSession(sessionId, { accessToken }), confirmrecordsaccumulates as the agent writes to theoutchannel, and thatlastControlfires onturn-complete. Then repeat with{ io: "in" }to read the input channel, unmount mid-stream to confirm the request is aborted, and pass a savedlastEventIdback in to confirm the channel resumes from that cursor instead of replaying from the beginning.Changelog
Before: reading a session's channel from React meant reaching for the API client directly. You created the client yourself, called the session stream subscription, and then wrote all of the surrounding React plumbing by hand — accumulating records into state, aborting the request when the component unmounted, throttling updates so a chatty channel didn't re-render on every record, and tracking the cursor yourself if you wanted to resume. Every app that showed session output rebuilt the same code, and none of it shared state with the other realtime hooks.
After:
useSessiondoes that work, and behaves like the realtime hooks already in the package. It subscribes on mount (or whenenabledflips totrue), stops on unmount or when you callstop(), and hands back the records received so far, the last control record, the cursor of the last record seen, and any error.iopicks which channel to read, defaulting to"out", andlastEventIdresumes a channel where a previous session left off.UseSessionInstanceis{ records, lastEventId, lastControl, error, stop }.UseSessionOptionsextends the shared api-client options withid,enabled,io,throttleInMs,timeoutInSeconds,lastEventId,onRecordandonControl.How: the hook wraps the API client's existing session stream subscription rather than introducing a new transport. Record state lives in SWR under the same style of key the realtime hooks use, so instances sharing an
idshare state; the request lifecycle is anAbortControllertorn down on unmount, onstop(), and when the target channel changes; and incoming records go through the same throttled batching queue, so a busy channel coalesces into one render perthrottleInMs(16ms by default).ControlEventis now re-exported from core soonControlcan be typed by consumers, anduseStableRequestCallbackmoved intosrc/utils/souseRealtimeanduseSessionshare the one copy. A patch changeset for@trigger.dev/react-hooksis included.Known limitation: access tokens are static here — there is no refresh path, so a long-lived subscription will fail once its token expires. A follow-up will address that.
Screenshots
n/a — no UI surface.
💯