/** * Singleton entity caches for all shared entities. * * Import from here in page components: * import { providersCache, targetsCache } from '$lib/stores/caches.svelte'; */ import { createEntityCache } from './entity-cache.svelte'; import { api } from '$lib/api'; import type { ServiceProvider, NotificationTarget, Tracker, TrackingConfig, TemplateConfig, TelegramBot, EmailBot, MatrixBot, CommandConfig, CommandTemplateConfig, CommandTracker, Action, ReleaseStatus, } from '$lib/types'; /** Service providers — used by Dashboard, Trackers, Command Trackers, Providers page. */ export const providersCache = createEntityCache('/providers'); /** Notification targets — used by Trackers, Targets page. */ export const targetsCache = createEntityCache('/targets'); /** Notification trackers — used by Dashboard, Trackers page. */ export const notificationTrackersCache = createEntityCache('/notification-trackers'); /** Tracking configs — used by Trackers, Tracking Configs page. */ export const trackingConfigsCache = createEntityCache('/tracking-configs'); /** Template configs — used by Trackers, Template Configs page. */ export const templateConfigsCache = createEntityCache('/template-configs'); /** Telegram bots — used by Targets, Command Trackers, Bots page. */ export const telegramBotsCache = createEntityCache('/telegram-bots'); /** Email bots — used by Targets, Bots page. */ export const emailBotsCache = createEntityCache('/email-bots'); /** Matrix bots — used by Targets, Bots page. */ export const matrixBotsCache = createEntityCache('/matrix-bots'); /** Command configs — used by Command Trackers, Command Configs page. */ export const commandConfigsCache = createEntityCache('/command-configs'); /** Command template configs — used by Command Configs, Command Template Configs page. */ export const commandTemplateConfigsCache = createEntityCache('/command-template-configs'); /** Command trackers — used by Command Trackers page. */ export const commandTrackersCache = createEntityCache('/command-trackers'); /** Actions — used by Actions page. */ export const actionsCache = createEntityCache('/actions'); export interface SlotDef { name: string; description: string; required?: boolean; } export interface CommandDef { name: string; description?: string; } export interface ActionTypeDef { key: string; name: string; description?: string; [key: string]: unknown; } export interface ProviderCapabilities { notification_slots?: SlotDef[]; command_slots?: SlotDef[]; commands?: CommandDef[]; action_types?: ActionTypeDef[]; [key: string]: unknown; } export type CapabilitiesMap = Record; /** Provider capabilities — used by Template Configs, Command Configs. * Dedups concurrent fetches so two fast navigations do not double-hit the API. */ export const capabilitiesCache = (() => { let data = $state({}); let fetchedAt = $state(0); let inflight: Promise | null = null; const TTL = 60_000; return { get items() { return data; }, async fetch(force = false): Promise { if (!force && Object.keys(data).length > 0 && Date.now() - fetchedAt < TTL) return data; if (inflight) return inflight; inflight = (async () => { try { data = await api('/providers/capabilities'); fetchedAt = Date.now(); return data; } finally { inflight = null; } })(); return inflight; }, }; })(); /** Configured external base URL — used to render absolute webhook URLs. * Available to all authenticated users. Empty string when unset. */ export const externalUrlCache = (() => { let data = $state(''); let fetchedAt = $state(0); let inflight: Promise | null = null; const TTL = 300_000; return { get value() { return data; }, invalidate() { fetchedAt = 0; }, async fetch(force = false): Promise { if (!force && fetchedAt > 0 && Date.now() - fetchedAt < TTL) return data; if (inflight) return inflight; inflight = (async () => { try { const res = await api<{ external_url: string }>('/settings/external-url'); data = (res?.external_url || '').replace(/\/+$/, ''); fetchedAt = Date.now(); return data; } finally { inflight = null; } })(); return inflight; }, }; })(); /** Upstream release status — drives the sidebar badge and Settings cassette. */ export const releaseStatusCache = (() => { let data = $state(null); let fetchedAt = $state(0); let inflight: Promise | null = null; // 5 min TTL — fresh enough that "Check now" feels instant on revisit, // long enough that route changes don't hammer the endpoint. const TTL = 300_000; return { get value() { return data; }, invalidate() { fetchedAt = 0; }, clear() { data = null; fetchedAt = 0; inflight = null; }, set(next: ReleaseStatus | null) { data = next; fetchedAt = Date.now(); }, async fetch(force = false): Promise { if (!force && fetchedAt > 0 && Date.now() - fetchedAt < TTL) return data; if (inflight) return inflight; inflight = (async () => { try { data = await api('/settings/release'); fetchedAt = Date.now(); return data; } catch { // Swallow — the badge falls back to its default "no status" state. return data; } finally { inflight = null; } })(); return inflight; }, }; })(); /** Supported template locales — fetched from app settings. */ export const supportedLocalesCache = (() => { let data = $state(['en', 'ru']); let fetchedAt = $state(0); let inflight: Promise | null = null; const TTL = 300_000; return { get items() { return data; }, async fetch(force = false): Promise { if (!force && fetchedAt > 0 && Date.now() - fetchedAt < TTL) return data; if (inflight) return inflight; inflight = (async () => { try { data = await api('/settings/locales'); fetchedAt = Date.now(); return data; } finally { inflight = null; } })(); return inflight; }, }; })(); /** * All caches keyed by entity type — for search palette and crosslink resolution. */ export const allCaches = { providers: providersCache, targets: targetsCache, notification_trackers: notificationTrackersCache, tracking_configs: trackingConfigsCache, template_configs: templateConfigsCache, telegram_bots: telegramBotsCache, email_bots: emailBotsCache, matrix_bots: matrixBotsCache, command_configs: commandConfigsCache, command_template_configs: commandTemplateConfigsCache, command_trackers: commandTrackersCache, actions: actionsCache, } as const; /** * Fetch all caches in parallel. Used by search palette. */ export async function fetchAllCaches(): Promise { await Promise.all(Object.values(allCaches).map(c => c.fetch())); } /** * Invalidate all entity caches. Useful on logout. * * Singleton state caches (release status, external URL, supported locales) * live outside `allCaches` because their shape differs from entity caches — * we clear them explicitly so a returning user as a different role can't * briefly see the previous user's cached payload. */ export function clearAllCaches(): void { Object.values(allCaches).forEach(c => c.clear()); releaseStatusCache.clear(); }