Fix pausing log auto-refresh and cancel stale reads

This commit is contained in:
2026-10-04 17:19:15 -04:00
parent 2adadb0d05
commit 2413c29fc0
3 changed files with 268 additions and 48 deletions
+91
View File
@@ -0,0 +1,91 @@
import { pollVisible } from './poll.js';
export interface LogResponse {
ok: boolean;
key?: string;
path?: string;
lines?: string[];
size?: number;
mtime?: string | null;
truncated?: boolean;
missing?: boolean;
reason?: string;
}
interface Selection { server: string; key: string; lines: number }
interface Options {
onData: (key: string, response: LogResponse) => void;
onError: (message: string) => void;
onLoading: (loading: boolean) => void;
fetcher?: typeof fetch;
}
/** Pausing freezes the displayed tail, including replies already in flight. */
export function createLogRefresh(options: Options) {
const request = options.fetcher ?? fetch;
let selection: Selection | null = null;
let automatic = true;
let interval = 5000;
let stopPolling: (() => void) | undefined;
let pending: { controller: AbortController; promise: Promise<void> } | null = null;
let generation = 0;
let disposed = false;
function cancel() {
generation++;
pending?.controller.abort();
pending = null;
options.onLoading(false);
}
function refresh(): Promise<void> {
if (disposed || !selection?.key) return Promise.resolve();
if (pending) return pending.promise;
const target = selection;
const version = generation;
const current = () => !disposed && generation === version && selection === target;
const entry = { controller: new AbortController(), promise: Promise.resolve() };
pending = entry;
options.onLoading(true);
entry.promise = (async () => {
try {
const response = await request(`/api/servers/${encodeURIComponent(target.server)}/logs/${encodeURIComponent(target.key)}?lines=${target.lines}`,
{ signal: entry.controller.signal, cache: 'no-store' });
const body = await response.json() as LogResponse;
if (!response.ok || !body.ok) throw new Error(body.reason ?? 'Could not refresh log.');
if (current()) { options.onError(''); options.onData(target.key, body); }
} catch (error) {
if (current() && !entry.controller.signal.aborted) options.onError(error instanceof Error ? error.message : 'Could not refresh log.');
} finally {
if (pending === entry) pending = null;
if (current()) options.onLoading(false);
}
})();
return entry.promise;
}
function configure(target: Selection, enabled: boolean, pollMs: number) {
if (disposed) return;
const changed = !selection || selection.server !== target.server || selection.key !== target.key || selection.lines !== target.lines;
if (!changed && automatic === enabled && interval === pollMs) return;
stopPolling?.(); stopPolling = undefined;
const pausing = automatic && !enabled;
automatic = enabled;
interval = pollMs;
if (changed || pausing) cancel();
if (changed) { selection = target; options.onError(''); }
if (!selection?.key) return;
if (automatic) stopPolling = pollVisible(refresh, interval);
// Changing the selected log is an explicit load, even while paused.
else if (changed) void refresh();
}
function dispose() {
if (disposed) return;
disposed = true;
stopPolling?.(); stopPolling = undefined;
cancel();
}
return { configure, refresh, dispose };
}