fabro/apps/fabro-web/app/lib/cross-tab-sse.ts
Bryan Helmkamp 62f0b3e7d1
refactor(web): improve React Doctor score (#405)
## Summary

Improves the web UI's React Doctor audit score by separating reusable
helpers from React component modules, tightening effect/state ownership,
and extracting real component boundaries in the install wizard, stage
activity view, run-files diff browser, RunDetail route, and Runs
workspace. The branch removes the previously deferred RunDetail and Runs
giant-component diagnostics without changing RunDetail UX, route
contracts, action ordering, or Runs workspace behavior.

| Metric | Main baseline | Initial PR | Current PR |
|--------|---------------|------------|------------|
| React Doctor score | 63 | 71 | 99 |
| React Doctor errors | 123 | 0 | 0 |
| React Doctor warnings | 241 | 163 | 3 |
| React Doctor diagnostics | 364 | 163 | 3 |

## Changes

- Moves exported helper logic out of component files so Fast
Refresh/component-export rules no longer dominate the audit.
- Adds a targeted React Doctor config exception for React Router route
modules, where non-component exports like route metadata are
intentional.
- Refactors low-risk state/effect patterns: keyed interview question
state, reducer-backed editable run title state, event-owned preview
opening, route-keyed insights editor initialization, refresh timer
ownership, and selection/derived list cleanup.
- Reworks `InstallApp` around an install reducer, a controller hook for
install lifecycle state, and focused wizard step components for LLM,
server, object-store, sandbox, and GitHub setup.
- Moves `RunStages` selected-stage activity into a keyed boundary for
panel/debug detail state while preserving stage activity filters across
navigation.
- Extracts the `RunFiles` loaded diff-browser view from route/query
coordination so the route owns data/URL state and the loaded view owns
rendering.
- Splits `RunDetail` into route-local header, actions, tab shell, docked
controls, model, and lifecycle-toast modules; the actions menu now uses
grouped descriptors instead of a large boolean/callback prop matrix.
- Extracts Runs workspace preference ownership into
`useRunsWorkspacePreferences` and moves toolbar rendering into
`RunsToolbar`, leaving the route focused on data, DnD state, filtering,
and view selection.
- Guards `InsightsEditor` query execution with a latest-run id and
timeout cleanup so stale or unmounted mock query runs cannot overwrite
newer results.
- Adds regression coverage for archived-run deletion from RunDetail and
stale-result handling in InsightsEditor.
- Improves semantic/accessibility coverage with labeled controls, native
meter/section semantics, decorative status dots, and clearer unavailable
copy.
- Removes dead UI code and applies local suppressions only where the
rule is a documented false positive or an intentional imperative
integration boundary.

## Remaining React Doctor warnings

Current score is 99 with 0 errors and 3 warnings. The remaining warnings
are intentionally left for separate judgment rather than mechanical
churn:

- `prefer-useReducer` (3): `AutomationsNew`, `InsightsEditor`, and
`CreateSecretForm` need reducers only if they encode real coupled
transitions, not simple field setters.

## Verification

- `cd apps/fabro-web && bun test app/routes/run-detail.test.ts` -> `22
pass`, `0 fail`
- `cd apps/fabro-web && bun test app/routes/insights-editor.test.tsx
app/routes/runs.preferences.test.tsx` -> `7 pass`, `0 fail`
- `cd apps/fabro-web && bun run typecheck`
- `cd apps/fabro-web && bun test --isolate` -> `490 pass`, `0 fail`
- `cd apps/fabro-web && bunx react-doctor@latest --full --json >
/tmp/fabro-react-doctor-runs-insights.json` -> score `99`, `0` errors,
`3` warnings
- Earlier branch verification also included `cd apps/fabro-web && bun
run build`
- `git diff --check`

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 (context not reported, default reasoning) via
[Codex](https://openai.com/codex)
2026-05-25 22:41:37 -04:00

1177 lines
34 KiB
TypeScript

import { queryKeys } from "./query-keys";
import {
createBrowserEventSource,
type EventInvalidation,
type EventPayload,
type EventSourceLike,
type KeyOrMatcher,
type MutateFn,
sseKeyDedupeId,
} from "./sse";
import { getNumber, getString, isRecord, type UnknownRecord } from "./unknown";
export const CROSS_TAB_SSE_CHANNEL = "fabro:sse:v1";
const HEARTBEAT_MS = 1000;
const LEADER_STALE_MS = 4000;
const ELECTION_JITTER_MS = 150;
const MESSAGE_VERSION = 1 as const;
const EVENT_DEDUPE_TTL_MS = 5 * 60 * 1000;
const EVENT_DEDUPE_MAX = 1000;
type TabVisibility = "visible" | "hidden";
type CandidateReason = "hidden-leader" | "stale-leader" | "release" | "no-leader";
interface BaseMessage {
type: string;
version: typeof MESSAGE_VERSION;
tabId: string;
sentAt: number;
}
interface HelloMessage extends BaseMessage {
type: "hello";
}
interface HeartbeatMessage extends BaseMessage {
type: "heartbeat";
leaderId: string;
generation: number;
visibility: TabVisibility;
}
interface CandidateMessage extends BaseMessage {
type: "candidate";
candidateId: string;
candidateGeneration: number;
visibility: TabVisibility;
observedLeaderId: string | null;
observedGeneration: number;
reason: CandidateReason;
}
interface LeaderChangedMessage extends BaseMessage {
type: "leader-changed";
leaderId: string;
generation: number;
visibility: TabVisibility;
}
interface ReleaseMessage extends BaseMessage {
type: "release";
leaderId: string;
generation: number;
}
interface ResyncMessage extends BaseMessage {
type: "resync";
leaderId: string | null;
generation: number;
reason: CandidateReason;
}
interface EventMessage extends BaseMessage {
type: "event";
leaderId: string;
generation: number;
payload: EventPayload;
}
export type CrossTabSseMessage =
| HelloMessage
| HeartbeatMessage
| CandidateMessage
| LeaderChangedMessage
| ReleaseMessage
| ResyncMessage
| EventMessage;
export interface BroadcastChannelLike {
onmessage: ((event: { data: unknown }) => void) | null;
postMessage(message: CrossTabSseMessage): void;
close(): void;
}
interface TimingOptions {
heartbeatMs: number;
leaderStaleMs: number;
electionJitterMs: number;
}
export interface CrossTabSseCoordinatorOptions {
tabId?: string;
channelFactory?: (name: string) => BroadcastChannelLike;
eventSourceFactory?: (url: string) => EventSourceLike;
getVisibility?: () => TabVisibility;
addVisibilityChangeListener?: (handler: () => void) => () => void;
addPagehideListener?: (handler: () => void) => () => void;
now?: () => number;
timing?: Partial<TimingOptions>;
}
interface SubscribeOptions<TPayload extends EventPayload> {
subscriptionKey: string;
mutate: MutateFn;
resolveInvalidation: (payload: TPayload) => EventInvalidation;
resyncKeys: () => KeyOrMatcher[];
fallbackSubscribe: () => () => void;
debounceMs?: number;
}
export interface SubscribeToCrossTabSseOptions<TPayload extends EventPayload>
extends SubscribeOptions<TPayload> {
coordinator?: CrossTabSseCoordinator;
}
interface FallbackEntry {
count: number;
subscribe: () => () => void;
cleanup?: () => void;
}
interface LocalSubscription {
refcount: number;
mutators: Map<MutateFn, number>;
fallbacks: Map<MutateFn, FallbackEntry>;
pendingKeys: Map<string, KeyOrMatcher>;
debounceTimer: ReturnType<typeof setTimeout> | null;
debounceMs: number;
resolveInvalidation: (payload: EventPayload) => EventInvalidation;
resyncKeys: () => KeyOrMatcher[];
}
interface LeaderState {
leaderId: string;
generation: number;
visibility: TabVisibility;
lastSeen: number;
}
class RecentEventCache {
private readonly seen = new Map<string, number>();
constructor(
private readonly maxSize: number,
private readonly ttlMs: number,
) {}
remember(key: string | undefined, now: number): boolean {
if (!key) return true;
this.evictExpired(now);
if (this.seen.has(key)) return false;
this.seen.set(key, now);
while (this.seen.size > this.maxSize) {
const oldest = this.seen.keys().next().value;
if (oldest === undefined) break;
this.seen.delete(oldest);
}
return true;
}
private evictExpired(now: number) {
for (const [key, seenAt] of this.seen) {
if (now - seenAt <= this.ttlMs) return;
this.seen.delete(key);
}
}
}
export class CrossTabSseCoordinator {
readonly tabId: string;
private readonly channelFactory: (name: string) => BroadcastChannelLike;
private readonly getVisibility: () => TabVisibility;
private readonly addVisibilityChangeListener: (handler: () => void) => () => void;
private readonly addPagehideListener: (handler: () => void) => () => void;
private readonly now: () => number;
private readonly timing: TimingOptions;
private readonly recentEvents = new RecentEventCache(EVENT_DEDUPE_MAX, EVENT_DEDUPE_TTL_MS);
private readonly subscriptions = new Map<string, LocalSubscription>();
private readonly candidates = new Map<string, CandidateMessage>();
private sourceFactory: (url: string) => EventSourceLike;
private channel: BroadcastChannelLike | null = null;
private source: EventSourceLike | null = null;
private initialized = false;
private coordinationUnavailable = false;
private fallbackMode = false;
private degradingToFallback = false;
private isLeader = false;
private leader: LeaderState | null = null;
private generation = 0;
private ownCandidate: CandidateMessage | null = null;
private candidateTimer: ReturnType<typeof setTimeout> | null = null;
private noLeaderTimer: ReturnType<typeof setTimeout> | null = null;
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
private leaderCheckTimer: ReturnType<typeof setInterval> | null = null;
private removeVisibilityListener: (() => void) | null = null;
private removePagehideListener: (() => void) | null = null;
constructor(options: CrossTabSseCoordinatorOptions = {}) {
this.tabId = options.tabId ?? createTabId();
this.channelFactory = options.channelFactory ?? createBrowserBroadcastChannel;
this.sourceFactory = options.eventSourceFactory ?? createBrowserEventSource;
this.getVisibility = options.getVisibility ?? getBrowserVisibility;
this.addVisibilityChangeListener =
options.addVisibilityChangeListener ?? addBrowserVisibilityChangeListener;
this.addPagehideListener = options.addPagehideListener ?? addBrowserPagehideListener;
this.now = options.now ?? Date.now;
this.timing = {
heartbeatMs: options.timing?.heartbeatMs ?? HEARTBEAT_MS,
leaderStaleMs: options.timing?.leaderStaleMs ?? LEADER_STALE_MS,
electionJitterMs: options.timing?.electionJitterMs ?? ELECTION_JITTER_MS,
};
}
subscribe<TPayload extends EventPayload>(options: SubscribeOptions<TPayload>): () => void {
const subscription = this.addLocalSubscription(options);
if (this.coordinationUnavailable) {
this.fallbackMode = true;
this.startFallbacksFor(subscription);
} else if (!this.initialized && !this.initialize()) {
this.coordinationUnavailable = true;
this.fallbackMode = true;
this.startFallbacksFor(subscription);
} else if (this.fallbackMode) {
this.startFallbacksFor(subscription);
} else {
this.ensureLeadershipProgress();
}
let active = true;
return () => {
if (!active) return;
active = false;
this.removeLocalSubscription(options.subscriptionKey, options.mutate);
};
}
close() {
this.releaseLeadership({ broadcast: false, resync: false });
this.clearCandidate();
this.clearNoLeaderTimer();
this.shutdownTimersAndChannel();
this.closeFallbacks();
this.clearSubscriptionTimers();
this.subscriptions.clear();
this.candidates.clear();
this.resetIdleState();
}
private initialize(): boolean {
try {
this.channel = this.channelFactory(CROSS_TAB_SSE_CHANNEL);
} catch {
this.channel = null;
return false;
}
this.channel.onmessage = (event) => this.handleMessage(event.data);
this.initialized = true;
this.removeVisibilityListener = this.addVisibilityChangeListener(() => {
this.handleVisibilityChange();
});
this.removePagehideListener = this.addPagehideListener(() => {
this.handlePagehide();
});
this.leaderCheckTimer = setInterval(() => {
this.checkLeaderFreshness();
}, this.timing.heartbeatMs);
return this.post({ type: "hello", version: MESSAGE_VERSION, tabId: this.tabId, sentAt: this.now() });
}
private addLocalSubscription<TPayload extends EventPayload>(
options: SubscribeOptions<TPayload>,
): LocalSubscription {
let subscription = this.subscriptions.get(options.subscriptionKey);
if (!subscription) {
subscription = {
refcount: 0,
mutators: new Map(),
fallbacks: new Map(),
pendingKeys: new Map(),
debounceTimer: null,
debounceMs: options.debounceMs ?? 300,
resolveInvalidation: options.resolveInvalidation as (payload: EventPayload) => EventInvalidation,
resyncKeys: options.resyncKeys,
};
this.subscriptions.set(options.subscriptionKey, subscription);
} else {
subscription.resolveInvalidation =
options.resolveInvalidation as (payload: EventPayload) => EventInvalidation;
subscription.resyncKeys = options.resyncKeys;
subscription.debounceMs = options.debounceMs ?? subscription.debounceMs;
}
subscription.refcount += 1;
subscription.mutators.set(
options.mutate,
(subscription.mutators.get(options.mutate) ?? 0) + 1,
);
const fallback = subscription.fallbacks.get(options.mutate);
if (fallback) {
fallback.count += 1;
fallback.subscribe = options.fallbackSubscribe;
} else {
subscription.fallbacks.set(options.mutate, {
count: 1,
subscribe: options.fallbackSubscribe,
});
}
return subscription;
}
private removeLocalSubscription(subscriptionKey: string, mutate: MutateFn) {
const subscription = this.subscriptions.get(subscriptionKey);
if (!subscription) return;
const mutateCount = subscription.mutators.get(mutate) ?? 0;
if (mutateCount <= 1) {
subscription.mutators.delete(mutate);
} else {
subscription.mutators.set(mutate, mutateCount - 1);
}
const fallback = subscription.fallbacks.get(mutate);
if (fallback) {
fallback.count -= 1;
if (fallback.count <= 0) {
fallback.cleanup?.();
subscription.fallbacks.delete(mutate);
}
}
subscription.refcount -= 1;
if (subscription.refcount <= 0) {
this.clearSubscriptionTimer(subscription);
this.subscriptions.delete(subscriptionKey);
}
if (this.subscriptions.size === 0) {
this.releaseLeadership({ broadcast: true, resync: false });
this.clearCandidate();
this.clearNoLeaderTimer();
this.shutdownTimersAndChannel();
this.resetIdleState();
}
}
private handleMessage(data: unknown) {
const message = parseMessage(data);
if (!message || message.tabId === this.tabId) return;
switch (message.type) {
case "hello":
if (this.isLeader) this.sendHeartbeat();
break;
case "heartbeat":
this.handleLeaderAnnouncement(message, { resyncOnChange: true });
break;
case "candidate":
this.handleCandidate(message);
break;
case "leader-changed":
this.handleLeaderAnnouncement(message, { resyncOnChange: true });
break;
case "release":
this.handleRelease(message);
break;
case "resync":
this.handleResync(message);
break;
case "event":
this.handleBroadcastEvent(message);
break;
}
}
private handleLeaderAnnouncement(
message: HeartbeatMessage | LeaderChangedMessage,
{ resyncOnChange }: { resyncOnChange: boolean },
) {
const incoming: LeaderState = {
leaderId: message.leaderId,
generation: message.generation,
visibility: message.visibility,
lastSeen: this.now(),
};
if (this.isLeader && incoming.leaderId !== this.tabId) {
const own: LeaderState = {
leaderId: this.tabId,
generation: this.generation,
visibility: this.currentVisibility(),
lastSeen: this.now(),
};
if (
incoming.generation > own.generation ||
(incoming.generation === own.generation && leaderHasHigherPriority(incoming, own))
) {
this.releaseLeadership({ broadcast: false, resync: true });
} else {
return;
}
}
const previous = this.leader;
if (this.ownCandidate && incoming.generation === this.ownCandidate.candidateGeneration) {
const sawIncomingCandidate = this.candidates.has(
`${incoming.generation}:${incoming.leaderId}`,
);
const shouldAcceptFreshVisibleLeader =
this.ownCandidate.reason === "no-leader" &&
incoming.visibility === "visible" &&
!sawIncomingCandidate;
if (
!shouldAcceptFreshVisibleLeader &&
!leaderHasHigherPriority(incoming, leaderStateForCandidate(this.ownCandidate, this.now()))
) {
return;
}
}
if (!this.shouldAcceptLeader(incoming)) return;
this.leader = incoming;
this.clearNoLeaderTimer();
this.generation = Math.max(this.generation, incoming.generation);
this.pruneStaleCandidates();
if (this.ownCandidate && incoming.generation >= this.ownCandidate.candidateGeneration) {
this.clearCandidate();
}
const changed =
!previous ||
previous.leaderId !== incoming.leaderId ||
previous.generation !== incoming.generation;
if (changed && resyncOnChange) {
this.resyncAll();
}
if (incoming.visibility === "hidden" && this.currentVisibility() === "visible") {
this.enterCandidacy("hidden-leader", incoming);
}
}
private handleCandidate(message: CandidateMessage) {
this.candidates.set(candidateKey(message), message);
this.pruneStaleCandidates();
if (
this.isLeader &&
message.observedLeaderId === this.tabId &&
message.observedGeneration >= this.generation
) {
this.releaseLeadership({ broadcast: true, resync: true });
}
if (
this.ownCandidate &&
message.candidateGeneration === this.ownCandidate.candidateGeneration &&
candidateHasHigherPriority(message, this.ownCandidate)
) {
this.clearCandidate();
}
}
private handleRelease(message: ReleaseMessage) {
const current = this.leader;
if (
current &&
message.leaderId === current.leaderId &&
message.generation >= current.generation
) {
this.leader = null;
this.generation = Math.max(this.generation, message.generation);
this.pruneStaleCandidates();
this.enterCandidacy("release", {
leaderId: message.leaderId,
generation: message.generation,
visibility: "hidden",
lastSeen: this.now(),
});
}
}
private handleResync(message: ResyncMessage) {
if (this.leader && message.generation < this.leader.generation) return;
this.resyncAll();
}
private handleBroadcastEvent(message: EventMessage) {
if (!this.isCurrentLeader(message.leaderId, message.generation)) return;
if (!this.recentEvents.remember(eventDedupeKey(message.payload), this.now())) return;
this.dispatchPayload(message.payload);
}
private ensureLeadershipProgress() {
if (this.subscriptions.size === 0 || this.isLeader || this.ownCandidate) return;
if (!this.leader) {
this.scheduleNoLeaderCandidacy();
return;
}
if (this.leader.visibility === "hidden" && this.currentVisibility() === "visible") {
this.enterCandidacy("hidden-leader", this.leader);
}
}
private checkLeaderFreshness() {
if (this.subscriptions.size === 0 || this.fallbackMode) return;
if (this.isLeader) return;
const current = this.leader;
if (!current) {
this.scheduleNoLeaderCandidacy();
return;
}
if (!this.leaderIsFresh(current)) {
this.leader = null;
this.generation = Math.max(this.generation, current.generation);
this.pruneStaleCandidates();
this.resyncAll();
this.enterCandidacy("stale-leader", current);
return;
}
if (current.visibility === "hidden" && this.currentVisibility() === "visible") {
this.enterCandidacy("hidden-leader", current);
}
}
private enterCandidacy(reason: CandidateReason, observedLeader: LeaderState | null = this.leader) {
if (this.subscriptions.size === 0 || this.fallbackMode) return;
if (
reason === "no-leader" &&
this.leader &&
this.leader.visibility === "visible" &&
this.leaderIsFresh(this.leader)
) {
return;
}
const observedGeneration = observedLeader?.generation ?? this.generation;
const candidateGeneration = observedGeneration + 1;
if (
this.ownCandidate &&
this.ownCandidate.candidateGeneration >= candidateGeneration
) {
return;
}
this.clearCandidate();
this.clearNoLeaderTimer();
const candidate: CandidateMessage = {
type: "candidate",
version: MESSAGE_VERSION,
tabId: this.tabId,
sentAt: this.now(),
candidateId: this.tabId,
candidateGeneration,
visibility: this.currentVisibility(),
observedLeaderId: observedLeader?.leaderId ?? null,
observedGeneration,
reason,
};
this.ownCandidate = candidate;
this.candidates.set(candidateKey(candidate), candidate);
if (!this.post(candidate)) return;
this.candidateTimer = setTimeout(() => {
this.completeCandidacy(candidate);
}, this.timing.electionJitterMs);
}
private completeCandidacy(candidate: CandidateMessage) {
if (this.ownCandidate !== candidate || this.fallbackMode) return;
for (const other of this.candidates.values()) {
if (
other.candidateGeneration === candidate.candidateGeneration &&
candidateHasHigherPriority(other, candidate)
) {
this.clearCandidate();
return;
}
}
if (this.leader && this.leaderIsFresh(this.leader)) {
if (this.leader.generation > candidate.candidateGeneration) {
this.clearCandidate();
return;
}
if (
this.leader.generation === candidate.candidateGeneration &&
leaderHasHigherPriority(this.leader, leaderStateForCandidate(candidate, this.now()))
) {
this.clearCandidate();
return;
}
}
this.becomeLeader(candidate.candidateGeneration);
}
private becomeLeader(generation: number) {
this.clearCandidate();
this.closeSource();
this.isLeader = true;
this.generation = generation;
this.pruneStaleCandidates();
this.leader = {
leaderId: this.tabId,
generation,
visibility: this.currentVisibility(),
lastSeen: this.now(),
};
const source = this.sourceFactory(queryKeys.system.attachUrl());
this.source = source;
source.onmessage = (message) => {
this.handleLeaderEventSourceMessage(message.data);
};
const announced = this.post({
type: "leader-changed",
version: MESSAGE_VERSION,
tabId: this.tabId,
sentAt: this.now(),
leaderId: this.tabId,
generation,
visibility: this.currentVisibility(),
});
if (!announced) return;
this.startHeartbeat();
this.resyncAll();
}
private handleLeaderEventSourceMessage(data: string) {
if (!this.isLeader) return;
let payload: EventPayload;
try {
payload = JSON.parse(data) as EventPayload;
} catch {
return;
}
if (!this.recentEvents.remember(eventDedupeKey(payload), this.now())) return;
this.dispatchPayload(payload);
this.post({
type: "event",
version: MESSAGE_VERSION,
tabId: this.tabId,
sentAt: this.now(),
leaderId: this.tabId,
generation: this.generation,
payload,
});
}
private dispatchPayload(payload: EventPayload) {
for (const subscription of this.subscriptions.values()) {
const invalidation = subscription.resolveInvalidation(payload);
this.queueInvalidations(subscription, invalidation.keys, {
immediate: invalidation.immediate,
});
}
}
private queueInvalidations(
subscription: LocalSubscription,
keys: KeyOrMatcher[],
{ immediate = false }: { immediate?: boolean } = {},
) {
if (keys.length === 0) return;
for (const key of keys) {
subscription.pendingKeys.set(sseKeyDedupeId(key), key);
}
if (immediate || subscription.debounceMs <= 0) {
this.flushInvalidations(subscription);
return;
}
if (subscription.debounceTimer) {
clearTimeout(subscription.debounceTimer);
}
subscription.debounceTimer = setTimeout(() => {
subscription.debounceTimer = null;
this.flushInvalidations(subscription);
}, subscription.debounceMs);
}
private flushInvalidations(subscription: LocalSubscription) {
if (subscription.pendingKeys.size === 0) return;
const keys = [...subscription.pendingKeys.values()];
subscription.pendingKeys.clear();
for (const mutator of subscription.mutators.keys()) {
for (const key of keys) {
void mutator(key);
}
}
}
private resyncAll() {
for (const subscription of this.subscriptions.values()) {
this.queueInvalidations(subscription, subscription.resyncKeys(), { immediate: true });
}
}
private handleVisibilityChange() {
if (this.isLeader) {
this.sendHeartbeat();
}
if (this.currentVisibility() === "visible") {
this.resyncAll();
if (this.leader?.visibility === "hidden") {
this.enterCandidacy("hidden-leader", this.leader);
}
}
}
private handlePagehide() {
this.releaseLeadership({ broadcast: true, resync: false });
this.clearCandidate();
this.clearNoLeaderTimer();
}
private startHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
}
if (!this.sendHeartbeat()) return;
this.heartbeatTimer = setInterval(() => {
this.sendHeartbeat();
}, this.timing.heartbeatMs);
}
private sendHeartbeat(): boolean {
if (!this.isLeader) return false;
const visibility = this.currentVisibility();
this.leader = {
leaderId: this.tabId,
generation: this.generation,
visibility,
lastSeen: this.now(),
};
return this.post({
type: "heartbeat",
version: MESSAGE_VERSION,
tabId: this.tabId,
sentAt: this.now(),
leaderId: this.tabId,
generation: this.generation,
visibility,
});
}
private releaseLeadership({
broadcast,
resync,
}: {
broadcast: boolean;
resync: boolean;
}) {
if (!this.isLeader && !this.source) return;
const generation = this.generation;
this.closeSource();
this.isLeader = false;
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
this.leader = null;
if (broadcast) {
const released = this.post({
type: "release",
version: MESSAGE_VERSION,
tabId: this.tabId,
sentAt: this.now(),
leaderId: this.tabId,
generation,
});
if (!released) return;
this.post({
type: "resync",
version: MESSAGE_VERSION,
tabId: this.tabId,
sentAt: this.now(),
leaderId: this.tabId,
generation,
reason: "release",
});
}
if (resync) this.resyncAll();
}
private closeSource() {
if (!this.source) return;
this.source.close();
this.source = null;
}
private clearCandidate() {
if (this.candidateTimer) {
clearTimeout(this.candidateTimer);
this.candidateTimer = null;
}
this.ownCandidate = null;
}
private pruneStaleCandidates() {
for (const [key, candidate] of this.candidates) {
if (candidate.candidateGeneration < this.generation) {
this.candidates.delete(key);
}
}
}
private scheduleNoLeaderCandidacy() {
if (
this.noLeaderTimer ||
this.ownCandidate ||
this.isLeader ||
this.leader ||
this.subscriptions.size === 0 ||
this.fallbackMode
) {
return;
}
this.noLeaderTimer = setTimeout(() => {
this.noLeaderTimer = null;
if (!this.leader && !this.isLeader) {
this.enterCandidacy("no-leader");
}
}, this.timing.electionJitterMs);
}
private clearNoLeaderTimer() {
if (!this.noLeaderTimer) return;
clearTimeout(this.noLeaderTimer);
this.noLeaderTimer = null;
}
private shutdownTimersAndChannel() {
this.clearNoLeaderTimer();
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
if (this.leaderCheckTimer) {
clearInterval(this.leaderCheckTimer);
this.leaderCheckTimer = null;
}
this.removeVisibilityListener?.();
this.removeVisibilityListener = null;
this.removePagehideListener?.();
this.removePagehideListener = null;
if (this.channel) {
this.channel.close();
this.channel = null;
}
}
private post(message: CrossTabSseMessage): boolean {
if (!this.channel) {
this.degradeToFallback();
return false;
}
try {
this.channel.postMessage(message);
return true;
} catch {
this.degradeToFallback();
return false;
}
}
private degradeToFallback() {
if (this.degradingToFallback || this.fallbackMode) return;
this.degradingToFallback = true;
this.releaseLeadership({ broadcast: false, resync: false });
this.clearCandidate();
this.shutdownTimersAndChannel();
this.initialized = false;
this.fallbackMode = true;
this.coordinationUnavailable = true;
for (const subscription of this.subscriptions.values()) {
this.startFallbacksFor(subscription);
}
this.degradingToFallback = false;
}
private startFallbacksFor(subscription: LocalSubscription) {
for (const fallback of subscription.fallbacks.values()) {
if (!fallback.cleanup) {
fallback.cleanup = fallback.subscribe();
}
}
}
private closeFallbacks() {
for (const subscription of this.subscriptions.values()) {
for (const fallback of subscription.fallbacks.values()) {
fallback.cleanup?.();
fallback.cleanup = undefined;
}
}
}
private clearSubscriptionTimers() {
for (const subscription of this.subscriptions.values()) {
this.clearSubscriptionTimer(subscription);
}
}
private clearSubscriptionTimer(subscription: LocalSubscription) {
if (!subscription.debounceTimer) return;
clearTimeout(subscription.debounceTimer);
subscription.debounceTimer = null;
}
private resetIdleState() {
this.leader = null;
this.initialized = false;
this.coordinationUnavailable = false;
this.fallbackMode = false;
}
private shouldAcceptLeader(incoming: LeaderState): boolean {
if (!this.leader) return true;
if (incoming.generation > this.leader.generation) return true;
if (incoming.generation < this.leader.generation) return false;
if (incoming.leaderId === this.leader.leaderId) return true;
return leaderHasHigherPriority(incoming, this.leader);
}
private isCurrentLeader(leaderId: string, generation: number): boolean {
return Boolean(
this.leader &&
this.leader.leaderId === leaderId &&
this.leader.generation === generation,
);
}
private currentVisibility(): TabVisibility {
return this.getVisibility() === "hidden" ? "hidden" : "visible";
}
private leaderIsFresh(leader: LeaderState): boolean {
return this.now() - leader.lastSeen <= this.timing.leaderStaleMs;
}
}
const defaultCoordinator = new CrossTabSseCoordinator();
export function createCrossTabSseCoordinator(options: CrossTabSseCoordinatorOptions = {}) {
return new CrossTabSseCoordinator(options);
}
export function subscribeToCrossTabSse<TPayload extends EventPayload>({
coordinator = defaultCoordinator,
...options
}: SubscribeToCrossTabSseOptions<TPayload>): () => void {
return coordinator.subscribe(options);
}
function createBrowserBroadcastChannel(name: string): BroadcastChannelLike {
if (typeof BroadcastChannel === "undefined") {
throw new Error("BroadcastChannel is unavailable");
}
return new BroadcastChannel(name);
}
function createTabId(): string {
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
return crypto.randomUUID();
}
return `tab-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
}
function getBrowserVisibility(): TabVisibility {
if (typeof document === "undefined") return "visible";
return document.visibilityState === "visible" ? "visible" : "hidden";
}
function addBrowserVisibilityChangeListener(handler: () => void): () => void {
if (typeof document === "undefined") return () => {};
document.addEventListener("visibilitychange", handler);
return () => document.removeEventListener("visibilitychange", handler);
}
function addBrowserPagehideListener(handler: () => void): () => void {
if (typeof window === "undefined") return () => {};
window.addEventListener("pagehide", handler);
return () => window.removeEventListener("pagehide", handler);
}
function parseMessage(data: unknown): CrossTabSseMessage | undefined {
if (!isRecord(data)) return undefined;
if (data.version !== MESSAGE_VERSION) return undefined;
const type = getString(data, "type");
const tabId = getString(data, "tabId");
const sentAt = getNumber(data, "sentAt");
if (!type || !tabId || sentAt === undefined) return undefined;
const base = { type, version: MESSAGE_VERSION, tabId, sentAt };
switch (type) {
case "hello":
return { ...base, type: "hello" };
case "heartbeat": {
const triple = parseLeaderTriple(data);
return triple && { ...base, type: "heartbeat", ...triple };
}
case "candidate":
return parseCandidate(data, base);
case "leader-changed": {
const triple = parseLeaderTriple(data);
return triple && { ...base, type: "leader-changed", ...triple };
}
case "release": {
const pair = parseLeaderPair(data);
return pair && { ...base, type: "release", ...pair };
}
case "resync":
return parseResync(data, base);
case "event":
return parseEvent(data, base);
default:
return undefined;
}
}
interface ParsedBase {
version: typeof MESSAGE_VERSION;
tabId: string;
sentAt: number;
}
function parseLeaderPair(data: unknown): { leaderId: string; generation: number } | undefined {
const leaderId = getString(data, "leaderId");
const generation = getNumber(data, "generation");
if (!leaderId || generation === undefined) return undefined;
return { leaderId, generation };
}
function parseLeaderTriple(
data: unknown,
): { leaderId: string; generation: number; visibility: TabVisibility } | undefined {
const pair = parseLeaderPair(data);
const visibility = getString(data, "visibility");
if (!pair || !isVisibility(visibility)) return undefined;
return { ...pair, visibility };
}
function parseCandidate(data: UnknownRecord, base: ParsedBase): CandidateMessage | undefined {
const candidateId = getString(data, "candidateId");
const candidateGeneration = getNumber(data, "candidateGeneration");
const visibility = getString(data, "visibility");
const observedGeneration = getNumber(data, "observedGeneration");
const reason = getString(data, "reason");
const observedLeaderRaw = data.observedLeaderId;
const observedLeaderId =
typeof observedLeaderRaw === "string" ? observedLeaderRaw : observedLeaderRaw === null ? null : undefined;
if (
!candidateId ||
candidateGeneration === undefined ||
!isVisibility(visibility) ||
observedLeaderId === undefined ||
observedGeneration === undefined ||
!isCandidateReason(reason)
) {
return undefined;
}
return {
...base,
type: "candidate",
candidateId,
candidateGeneration,
visibility,
observedLeaderId,
observedGeneration,
reason,
};
}
function parseResync(data: UnknownRecord, base: ParsedBase): ResyncMessage | undefined {
const generation = getNumber(data, "generation");
const reason = getString(data, "reason");
const leaderRaw = data.leaderId;
const leaderId = typeof leaderRaw === "string" ? leaderRaw : leaderRaw === null ? null : undefined;
if (generation === undefined || !isCandidateReason(reason) || leaderId === undefined) {
return undefined;
}
return { ...base, type: "resync", leaderId, generation, reason };
}
function parseEvent(data: UnknownRecord, base: ParsedBase): EventMessage | undefined {
const pair = parseLeaderPair(data);
if (!pair || !isRecord(data.payload)) return undefined;
return { ...base, type: "event", ...pair, payload: data.payload };
}
function isVisibility(value: unknown): value is TabVisibility {
return value === "visible" || value === "hidden";
}
function isCandidateReason(value: unknown): value is CandidateReason {
return (
value === "hidden-leader" ||
value === "stale-leader" ||
value === "release" ||
value === "no-leader"
);
}
function candidateHasHigherPriority(candidate: CandidateMessage, other: CandidateMessage): boolean {
if (candidate.visibility !== other.visibility) return candidate.visibility === "visible";
return candidate.candidateId < other.candidateId;
}
function leaderHasHigherPriority(candidate: LeaderState, other: LeaderState): boolean {
if (candidate.visibility !== other.visibility) return candidate.visibility === "visible";
return candidate.leaderId < other.leaderId;
}
function leaderStateForCandidate(candidate: CandidateMessage, lastSeen: number): LeaderState {
return {
leaderId: candidate.candidateId,
generation: candidate.candidateGeneration,
visibility: candidate.visibility,
lastSeen,
};
}
function candidateKey(candidate: CandidateMessage): string {
return `${candidate.candidateGeneration}:${candidate.candidateId}`;
}
export function eventDedupeKey(payload: EventPayload): string | undefined {
if (typeof payload.id === "string" && payload.id.length > 0) {
return payload.id;
}
const runId = typeof payload.run_id === "string" ? payload.run_id : undefined;
const seq = typeof payload.seq === "number" ? payload.seq : undefined;
const event = typeof payload.event === "string" ? payload.event : undefined;
if (runId && seq != null && event) {
return `${runId}:${seq}:${event}`;
}
return undefined;
}