feat(web): stale-while-revalidate caches to eliminate tab-switch flicker
Build / build (push) Failing after 4m51s
Build / build (push) Failing after 4m51s
Sidebar tabs, Settings, and drill-in detail pages re-fetched on every
visit (loading=true + onMount), flashing an empty skeleton frame on each
navigation. Add an SWR cache layer so revisiting a view renders cached
data instantly while refreshing in the background.
- resourceCache.ts: single-value + keyed (per-id) SWR cache factories
- caches.ts: per-resource cache instances; resetAllCaches() on logout
- eventsSnapshot.ts: warm-seed snapshot for the SSE/paginated events page
- List/sidebar pages read $cache.value via $derived, refresh() on mount;
mutations refresh the cache
- Settings forms seed once from settingsCache (edit-safe) and refetch
after save (PUT /api/settings returns {status}, not the Settings object)
- Detail [id] pages warm-seed per id; apps/[id] seeds {workload,containers},
resets non-seeded panels on warm nav, clears workload on 404, and
invalidates its cache entry on delete
Deferred (still cold-fetch): triggers/[id] (webhook secret + multi-fetch
body gate), apps/new (create wizard).
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
import { toasts } from '$lib/stores/toast';
|
||||
import { connectGlobalEvents, toEventLogEntry, type SSEConnection, type EventLogSSEPayload } from '$lib/sse';
|
||||
import type { EventLogEntry, EventLogStats } from '$lib/types';
|
||||
import { getEventsSnapshot, saveEventsSnapshot } from '$lib/stores/eventsSnapshot';
|
||||
import EventLogEntryComponent from '$lib/components/EventLogEntry.svelte';
|
||||
import EventLogFilter from '$lib/components/EventLogFilter.svelte';
|
||||
import EmptyState from '$lib/components/EmptyState.svelte';
|
||||
@@ -18,9 +19,13 @@
|
||||
|
||||
// ── State ─────────────────────────────────────────────────────
|
||||
|
||||
let events = $state<EventLogEntry[]>([]);
|
||||
let stats = $state<EventLogStats>({ info: 0, warn: 0, error: 0, total: 0 });
|
||||
let loading = $state(true);
|
||||
// Warm-start from the last-known snapshot so switching back to this tab
|
||||
// paints the previous events immediately instead of a skeleton; a
|
||||
// background refresh in onMount then brings it current. See eventsSnapshot.ts.
|
||||
const seed = getEventsSnapshot();
|
||||
let events = $state<EventLogEntry[]>(seed?.events ?? []);
|
||||
let stats = $state<EventLogStats>(seed?.stats ?? { info: 0, warn: 0, error: 0, total: 0 });
|
||||
let loading = $state((seed?.events.length ?? 0) === 0);
|
||||
let loadingMore = $state(false);
|
||||
let hasMore = $state(true);
|
||||
let newEventIds = $state<Set<number>>(new Set());
|
||||
@@ -61,7 +66,10 @@
|
||||
const currentOffset = append ? offset : 0;
|
||||
if (append) {
|
||||
loadingMore = true;
|
||||
} else {
|
||||
} else if (events.length === 0) {
|
||||
// Only show the skeleton on a true cold load. When we already have
|
||||
// (seeded or previously loaded) events, refresh in place so the list
|
||||
// never blanks out — this is what removes the tab-switch flicker.
|
||||
loading = true;
|
||||
}
|
||||
|
||||
@@ -135,6 +143,13 @@
|
||||
loadEvents();
|
||||
}
|
||||
|
||||
// Mirror the current list + stats into the module snapshot so the next
|
||||
// visit can warm-start. Cheap (stores references); keeps the snapshot fresh
|
||||
// through loads, SSE pushes, and deletes without touching each mutation site.
|
||||
$effect(() => {
|
||||
saveEventsSnapshot({ events, stats });
|
||||
});
|
||||
|
||||
// ── Client-side text filter ──────────────────────────────────
|
||||
|
||||
const filteredEvents = $derived(
|
||||
|
||||
Reference in New Issue
Block a user