Harden dashboard realtime state and controls

This commit is contained in:
jjbrunton committed 2026-08-08 15:41:58 +01:00
1 parent 4b3f00729f
commit d108903174
21 files changed
+422 -29

No files matched your search

@@ -1,6 +1,6 @@
import { useEffect, useReducer, useRef, useState } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import type { ActivityItem, ChatSyncStatus, DashboardEvent } from '../../../../../packages/api-contracts/src/index.js';
import { useDashboardRealtime } from '../../app/realtime-provider.js';
import {
createLiveConsoleState,
reduceLiveConsoleState,
@@ -9,12 +9,7 @@ import {
type ConsoleLevelFilter,
} from './live-console-model.js';
import { useActivityQuery, useFetchAllChatsMutation, useLogsQuery, useSyncStatusQuery, useTriggerChatSyncMutation } from './operations-queries.js';
import {
createBrowserEventSource,
createDashboardRealtimeClient,
queryKeysForDashboardEvent,
type RealtimeConnectionSnapshot,
} from './realtime-client.js';
import type { RealtimeConnectionSnapshot } from './realtime-client.js';
import './operations.css';
function formatTimestamp(timestamp: number | string): string {
@@ -73,7 +68,7 @@ export interface OperationsPageProps {
}
export function OperationsPage({ realtimeEnabled = true }: OperationsPageProps) {
const queryClient = useQueryClient();
const realtime = useDashboardRealtime();
const activityQuery = useActivityQuery();
const logsQuery = useLogsQuery();
const syncQuery = useSyncStatusQuery();
@@ -84,10 +79,7 @@ export function OperationsPage({ realtimeEnabled = true }: OperationsPageProps)
reduceLiveConsoleState,
createLiveConsoleState(500),
);
const [connection, setConnection] = useState<RealtimeConnectionSnapshot>({
status: 'idle',
isStale: true,
});
const connection = realtimeEnabled ? realtime.connection : { status: 'idle', isStale: true } as const;
const consoleEnd = useRef<HTMLDivElement>(null);
useEffect(() => {
@@ -106,10 +98,7 @@ export function OperationsPage({ realtimeEnabled = true }: OperationsPageProps)
}, [logsQuery.data]);
useEffect(() => {
if (!realtimeEnabled || typeof EventSource === 'undefined') {
setConnection({ status: 'idle', isStale: true });
return;
}
if (!realtimeEnabled) return;
function onEvent(event: DashboardEvent): void {
if (event.type === 'log.created') {
const entry: ConsoleEntry = {
@@ -121,18 +110,9 @@ export function OperationsPage({ realtimeEnabled = true }: OperationsPageProps)
};
dispatchConsole({ type: 'append', entry });
}
for (const queryKey of queryKeysForDashboardEvent(event)) {
void queryClient.invalidateQueries({ queryKey });
}
}
const client = createDashboardRealtimeClient({
sourceFactory: createBrowserEventSource,
onEvent,
onStateChange: setConnection,
});
client.start();
return () => client.stop();
}, [queryClient, realtimeEnabled]);
return realtime.subscribe(onEvent);
}, [realtime, realtimeEnabled]);
const visibleEntries = selectVisibleConsoleEntries(consoleState);
const syncBusy = syncQuery.data?.syncing === true || syncMutation.isPending || fetchAllMutation.isPending;
@@ -40,6 +40,7 @@ export interface DashboardRealtimeClientOptions {
url?: string;
sourceFactory(url: string): DashboardEventSource;
onEvent(event: DashboardEvent): void;
onConnected?(context: { isReconnect: boolean }): void;
onStateChange?(snapshot: RealtimeConnectionSnapshot): void;
onInvalidEvent?(value: unknown): void;
}
@@ -54,6 +55,7 @@ export function createDashboardRealtimeClient(
): DashboardRealtimeClient {
let source: DashboardEventSource | undefined;
let snapshot: RealtimeConnectionSnapshot = { status: 'idle', isStale: true };
let hasConnected = false;
function update(next: RealtimeConnectionSnapshot): void {
snapshot = next;
@@ -89,6 +91,8 @@ export function createDashboardRealtimeClient(
source = options.sourceFactory(options.url ?? '/api/events/stream');
source.addEventListener('open', () => {
update({ ...snapshot, status: 'connected', isStale: false });
options.onConnected?.({ isReconnect: hasConnected });
hasConnected = true;
});
source.addEventListener('error', () => {
update({ ...snapshot, status: 'reconnecting', isStale: true });
@@ -124,7 +128,7 @@ export function queryKeysForDashboardEvent(event: DashboardEvent): readonly Quer
case 'sync.failed':
return [['operations', 'sync-status'], ['conversations'], ['media']];
case 'reddit.session.changed':
return [['reddit', 'session']];
return [['dashboard', 'login', 'status']];
case 'log.created':
return [];
}
@@ -0,0 +1,54 @@
export interface PollingScheduler {
setInterval(callback: () => void, intervalMs: number): unknown;
clearInterval(handle: unknown): void;
}
export interface VisibilitySource {
isVisible(): boolean;
subscribe(listener: () => void): () => void;
}
export interface VisibilityAwarePollingFallback {
start(): void;
stop(): void;
setStale(stale: boolean): void;
}
export function createVisibilityAwarePollingFallback(options: {
intervalMs: number;
poll(): void;
scheduler: PollingScheduler;
visibility: VisibilitySource;
}): VisibilityAwarePollingFallback {
let stale = false;
let timer: unknown;
let unsubscribe: (() => void) | undefined;
const pollIfNeeded = () => { if (stale && options.visibility.isVisible()) options.poll(); };
return {
start() {
if (timer !== undefined) return;
timer = options.scheduler.setInterval(pollIfNeeded, options.intervalMs);
unsubscribe = options.visibility.subscribe(pollIfNeeded);
},
stop() {
if (timer !== undefined) options.scheduler.clearInterval(timer);
timer = undefined;
unsubscribe?.();
unsubscribe = undefined;
},
setStale(value) { stale = value; },
};
}
export const browserVisibilitySource: VisibilitySource = {
isVisible: () => document.visibilityState === 'visible',
subscribe(listener) {
document.addEventListener('visibilitychange', listener);
return () => document.removeEventListener('visibilitychange', listener);
},
};
export const browserPollingScheduler: PollingScheduler = {
setInterval: (callback, intervalMs) => window.setInterval(callback, intervalMs),
clearInterval: (handle) => { if (typeof handle === 'number') window.clearInterval(handle); },
};