feat(web): stale-while-revalidate caches to eliminate tab-switch flicker
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:
2026-06-08 15:39:25 +03:00
parent 6b45ed62bb
commit 192204a51c
31 changed files with 1139 additions and 493 deletions
+7 -15
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import * as api from '$lib/api';
import type { RedeployTrigger } from '$lib/api';
import { triggersCache } from '$lib/stores/caches';
import { IconPlus, IconRefresh } from '$lib/components/icons';
import ForgeHero from '$lib/components/ForgeHero.svelte';
import { t } from '$lib/i18n';
@@ -13,9 +13,11 @@
const KNOWN_KINDS = ['registry', 'git', 'manual', 'schedule', 'webhook', 'logscan'] as const;
type KnownKind = (typeof KNOWN_KINDS)[number];
let triggers = $state<RedeployTrigger[]>([]);
let loading = $state(true);
let error = $state('');
// Cache-backed (stale-while-revalidate) so revisiting this tab renders the
// list immediately instead of flashing the cold skeleton. See caches.ts.
const triggers = $derived<RedeployTrigger[]>($triggersCache.value);
const loading = $derived($triggersCache.loading);
const error = $derived($triggersCache.error);
let kindFilter = $state<'all' | KnownKind | string>('all');
const filtered = $derived(
@@ -69,17 +71,7 @@
}
}
async function load(): Promise<void> {
loading = true;
error = '';
try {
triggers = await api.listTriggers();
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to load triggers';
} finally {
loading = false;
}
}
const load = () => triggersCache.refresh();
onMount(load);
</script>