React hook that manages a JetStream ephemeral OrderedConsumer subscription for a chat dialog stream, handling connection lifecycle, reconnection with sequence resumption, and NATS client sharing. ## Key Components ### `useJetStreamDialogSubscription` The single exported hook. Accepts `UseJetStreamDialogSubscriptionOptions` and returns `UseJetStreamDialogSubscriptionReturn`. **State tracked:** | State | Description | |---|---| | `isConnected` | Whether the NATS WebSocket connection is live | | `isSubscribed` | Whether the JetStream consumer is active | | `reconnectionCount` | Increments whenever the live tail is re-established: a NATS reconnect, a JetStream ordered-consumer recreation, a resync after the page was hidden, or a host reporting either via `resyncSignal`. Callers refetch persisted history on every increment; one absence counts once even when two sources report it | | `currentStreamSeq` | Highest stream sequence number observed | **Host-supplied resync (`resyncSignal` option):** A counter the embedder bumps when it knows, from outside the page, that the tail may have missed something — an absence the page could not observe (a native shell whose window visibility never reaches `document.visibilityState`), or a write made on the page's behalf while it was away. Any increase is treated exactly like a reconnect. The value at mount is a baseline, rebased on every `dialogId` change so switching to a conversation with a lower count cannot swallow later resyncs. **Internal refs (stable across renders):** - `clientRef` — shared `NatsClient` instance - `lastRecoveryReportRef` — timestamp floor so a flapping consumer cannot storm callers with refetches - `resyncTimerRef` — trailing-edge timer collapsing resync requests within `RESYNC_COALESCE_MS`; cleared on dialog change and unmount so a request never crosses a conversation boundary - `lastResyncSignalRef` — baseline for `resyncSignal`, rebased per dialog - `subscriptionRef` — active `JetStreamSubscriptionHandle` - `highestStreamSeqRef` — tracks max stream sequence for gap-free resume **Effect order (declaration order matters):** 1. **Connection effect** — acquires/releases the shared NATS client when `enabled` or `wsUrl` changes; drives the `startConnectionLifecycle` which emits status events. 2. **Dialog reset effect** — resets `highestStreamSeqRef`, rebases the `resyncSignal` baseline, and drops any pending resync when `dialogId` changes, so a new dialog never inherits a stale start sequence or the previous dialog's refetch. 3. **Visibility effect** — after the page has been hidden longer than `RESYNC_AFTER_HIDDEN_MS`, returning to view requests a resync: a hidden page can stop receiving without the socket ever closing, and nothing else reports that. 4. **Host-signal effect** — an increase in `resyncSignal` requests a resync on the same path, for absences and out-of-band writes the page cannot see. 5. **Subscription effect** — creates or recreates the ephemeral consumer when `isConnected`, `dialogId`, `topic`, `streamName`, or the reconnect counter change. **Deliver policy logic:** ```text highestStreamSeqRef set → DeliverPolicy.ByStartSequence @ (highestSeq + 1) optStartSeq provided → DeliverPolicy.ByStartSequence @ (optStartSeq + 1) neither → DeliverPolicy.New (live tail only) ``` ## Usage Example ```typescript import { useJetStreamDialogSubscription } from './use-jetstream-dialog-subscription' function ChatPane({ dialogId }: { dialogId: string }) { const { isConnected, isSubscribed, currentStreamSeq } = useJetStreamDialogSubscription({ enabled: true, dialogId, topic: 'chunks', optStartSeq: 42, // resume after sequence 42 getNatsWsUrl: () => 'wss://nats.example.com', onEvent: (payload, topic) => { console.log(`[${topic}] seq=${payload.streamSeq}`, payload) }, onConnect: () => console.log('NATS connected'), onDisconnect: () => console.log('NATS disconnected'), inactiveThresholdMs: 3 * 60_000, // 3-minute consumer TTL }) return (
{isConnected ? '🟢' : '🔴'} connected |{' '} {isSubscribed ? 'subscribed' : 'pending'} | seq {currentStreamSeq ?? '–'}
) } ``` ## Reconnection Behavior On reconnect the consumer is **not** recreated from `optStartSeq`; instead it resumes from `highestStreamSeqRef.current + 1`, guaranteeing no chunk is replayed or skipped. Only a `dialogId` change resets that pointer back to `optStartSeq` (or live-tail). Consumer recreations reported by the client (`onRecovered`) are counted separately from connection reconnects and do **not** re-run the subscription effect — nats.ws has already rebuilt the consumer by then, so recreating ours would be churn that can feed itself. Both counts surface to callers summed as `reconnectionCount`, throttled by `RECOVERY_REPORT_FLOOR_MS`. The hook retries only on `closed` / `disconnected` NATS status events. Protocol-level `-ERR` events (e.g. permission violations on `CONSUMER.CREATE`) are logged as warnings without triggering the reconnection loop. > **Source:** [`use-jetstream-dialog-subscription.ts`](https://github.com/flamingo-stack/openframe-oss-lib/blob/main/use-jetstream-dialog-subscription.ts)