Preserve loaded logs across same-server fleet updates
This commit is contained in:
@@ -37,6 +37,7 @@
|
|||||||
let stickToBottom = $state(true);
|
let stickToBottom = $state(true);
|
||||||
|
|
||||||
const refresh = createLogRefresh({
|
const refresh = createLogRefresh({
|
||||||
|
onReset: () => { data = {}; refreshError = ''; },
|
||||||
onLoading: value => { loading = value; },
|
onLoading: value => { loading = value; },
|
||||||
onError: message => { refreshError = message; },
|
onError: message => { refreshError = message; },
|
||||||
onData: (key, body) => {
|
onData: (key, body) => {
|
||||||
@@ -88,7 +89,6 @@
|
|||||||
}
|
}
|
||||||
parts.push({ text: line.slice(start), match: false }); return parts;
|
parts.push({ text: line.slice(start), match: false }); return parts;
|
||||||
}
|
}
|
||||||
$effect(() => { server; data = {}; refreshError = ''; });
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const target = { server, key: active, lines };
|
const target = { server, key: active, lines };
|
||||||
const auto = autoRefresh;
|
const auto = autoRefresh;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export interface LogResponse {
|
|||||||
|
|
||||||
interface Selection { server: string; key: string; lines: number }
|
interface Selection { server: string; key: string; lines: number }
|
||||||
interface Options {
|
interface Options {
|
||||||
|
onReset: () => void;
|
||||||
onData: (key: string, response: LogResponse) => void;
|
onData: (key: string, response: LogResponse) => void;
|
||||||
onError: (message: string) => void;
|
onError: (message: string) => void;
|
||||||
onLoading: (loading: boolean) => void;
|
onLoading: (loading: boolean) => void;
|
||||||
@@ -73,6 +74,9 @@ export function createLogRefresh(options: Options) {
|
|||||||
automatic = enabled;
|
automatic = enabled;
|
||||||
interval = pollMs;
|
interval = pollMs;
|
||||||
if (changed || pausing) cancel();
|
if (changed || pausing) cancel();
|
||||||
|
// Fleet updates can replace the parent server object without changing
|
||||||
|
// this identity. Only navigation to a different server invalidates logs.
|
||||||
|
if (selection?.server !== target.server) options.onReset();
|
||||||
if (changed) { selection = target; options.onError(''); }
|
if (changed) { selection = target; options.onError(''); }
|
||||||
if (!selection?.key) return;
|
if (!selection?.key) return;
|
||||||
if (automatic) stopPolling = pollVisible(refresh, interval);
|
if (automatic) stopPolling = pollVisible(refresh, interval);
|
||||||
|
|||||||
@@ -20,9 +20,10 @@ afterEach(() => { controller?.dispose(); vi.useRealTimers(); vi.unstubAllGlobals
|
|||||||
|
|
||||||
function setup() {
|
function setup() {
|
||||||
const fetcher = vi.fn<typeof fetch>();
|
const fetcher = vi.fn<typeof fetch>();
|
||||||
|
const onReset = vi.fn();
|
||||||
const onData = vi.fn(); const onError = vi.fn(); const onLoading = vi.fn();
|
const onData = vi.fn(); const onError = vi.fn(); const onLoading = vi.fn();
|
||||||
controller = createLogRefresh({ fetcher, onData, onError, onLoading });
|
controller = createLogRefresh({ fetcher, onReset, onData, onError, onLoading });
|
||||||
return { fetcher, onData, onError, onLoading };
|
return { fetcher, onReset, onData, onError, onLoading };
|
||||||
}
|
}
|
||||||
const visibility = (state: string) => {
|
const visibility = (state: string) => {
|
||||||
documentStub.visibilityState = state;
|
documentStub.visibilityState = state;
|
||||||
@@ -30,6 +31,36 @@ const visibility = (state: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe('log refresh controls', () => {
|
describe('log refresh controls', () => {
|
||||||
|
it('retains a manually loaded log through same-server fleet updates and resets only on server navigation', async () => {
|
||||||
|
const fetcher = vi.fn<typeof fetch>().mockImplementation(async () => response('loaded log'));
|
||||||
|
let displayed: string[] = [];
|
||||||
|
const reset = vi.fn(() => { displayed = []; });
|
||||||
|
controller = createLogRefresh({ fetcher, onReset: reset,
|
||||||
|
onData: (_key, body) => { displayed = body.lines ?? []; }, onError: vi.fn(), onLoading: vi.fn() });
|
||||||
|
controller.configure(target, true, 5000);
|
||||||
|
await controller.refresh();
|
||||||
|
controller.configure(target, false, 5000);
|
||||||
|
await controller.refresh();
|
||||||
|
expect(displayed).toEqual(['loaded log']);
|
||||||
|
reset.mockClear();
|
||||||
|
// The parent supplies fresh props whenever Fleet publishes a snapshot.
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
controller.configure({ ...target }, false, 5000);
|
||||||
|
await vi.advanceTimersByTimeAsync(5000);
|
||||||
|
expect(displayed).toEqual(['loaded log']);
|
||||||
|
}
|
||||||
|
expect(reset).not.toHaveBeenCalled();
|
||||||
|
expect(fetcher).toHaveBeenCalledTimes(2);
|
||||||
|
controller.configure({ ...target, key: 'games_zm' }, false, 5000);
|
||||||
|
await controller.refresh();
|
||||||
|
expect(reset).not.toHaveBeenCalled();
|
||||||
|
controller.configure({ ...target, server: 'different-server' }, false, 5000);
|
||||||
|
expect(reset).toHaveBeenCalledTimes(1);
|
||||||
|
expect(displayed).toEqual([]);
|
||||||
|
await controller.refresh();
|
||||||
|
expect(displayed).toEqual(['loaded log']);
|
||||||
|
});
|
||||||
|
|
||||||
it('pauses without fetching again and remains paused across intervals and visibility changes', async () => {
|
it('pauses without fetching again and remains paused across intervals and visibility changes', async () => {
|
||||||
const { fetcher, onData, onLoading } = setup();
|
const { fetcher, onData, onLoading } = setup();
|
||||||
fetcher.mockResolvedValueOnce(response('saved log'));
|
fetcher.mockResolvedValueOnce(response('saved log'));
|
||||||
|
|||||||
Reference in New Issue
Block a user