Files
notify-bridge/frontend/src/lib/stores/caches.svelte.ts
T
alexei.dolgolyov ba199f24bd feat: deferred dispatch, release-check provider, settings polish
- Defer quiet-hours dispatches into new deferred_dispatch table; drain
  job + periodic catch-up scan re-fire at window end with coalescing on
  (link, event_type, collection_id).
- Add ON DELETE SET NULL migration on event_log_id and partial unique
  index on (link_id, collection_id, event_type) WHERE status='pending'.
- Add release-check provider abstraction (Gitea/GitHub) with SSRF-safe
  URL validation, settings UI cassette, and scheduled polling.
- Replace importlib-only version lookup with version.py helper that
  prefers the higher of installed metadata vs source pyproject so stale
  editable dev installs stop misreporting.
- Aurora frontend polish: MetaStrip component, ReleaseCassette,
  EventDetailModal expansion, and i18n additions.
2026-05-12 02:58:07 +03:00

246 lines
7.5 KiB
TypeScript

/**
* 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<ServiceProvider>('/providers');
/** Notification targets — used by Trackers, Targets page. */
export const targetsCache = createEntityCache<NotificationTarget>('/targets');
/** Notification trackers — used by Dashboard, Trackers page. */
export const notificationTrackersCache = createEntityCache<Tracker>('/notification-trackers');
/** Tracking configs — used by Trackers, Tracking Configs page. */
export const trackingConfigsCache = createEntityCache<TrackingConfig>('/tracking-configs');
/** Template configs — used by Trackers, Template Configs page. */
export const templateConfigsCache = createEntityCache<TemplateConfig>('/template-configs');
/** Telegram bots — used by Targets, Command Trackers, Bots page. */
export const telegramBotsCache = createEntityCache<TelegramBot>('/telegram-bots');
/** Email bots — used by Targets, Bots page. */
export const emailBotsCache = createEntityCache<EmailBot>('/email-bots');
/** Matrix bots — used by Targets, Bots page. */
export const matrixBotsCache = createEntityCache<MatrixBot>('/matrix-bots');
/** Command configs — used by Command Trackers, Command Configs page. */
export const commandConfigsCache = createEntityCache<CommandConfig>('/command-configs');
/** Command template configs — used by Command Configs, Command Template Configs page. */
export const commandTemplateConfigsCache = createEntityCache<CommandTemplateConfig>('/command-template-configs');
/** Command trackers — used by Command Trackers page. */
export const commandTrackersCache = createEntityCache<CommandTracker>('/command-trackers');
/** Actions — used by Actions page. */
export const actionsCache = createEntityCache<Action>('/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<string, ProviderCapabilities>;
/** 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<CapabilitiesMap>({});
let fetchedAt = $state(0);
let inflight: Promise<CapabilitiesMap> | null = null;
const TTL = 60_000;
return {
get items() { return data; },
async fetch(force = false): Promise<CapabilitiesMap> {
if (!force && Object.keys(data).length > 0 && Date.now() - fetchedAt < TTL) return data;
if (inflight) return inflight;
inflight = (async () => {
try {
data = await api<CapabilitiesMap>('/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<string>('');
let fetchedAt = $state(0);
let inflight: Promise<string> | null = null;
const TTL = 300_000;
return {
get value() { return data; },
invalidate() { fetchedAt = 0; },
async fetch(force = false): Promise<string> {
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<ReleaseStatus | null>(null);
let fetchedAt = $state(0);
let inflight: Promise<ReleaseStatus | null> | 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<ReleaseStatus | null> {
if (!force && fetchedAt > 0 && Date.now() - fetchedAt < TTL) return data;
if (inflight) return inflight;
inflight = (async () => {
try {
data = await api<ReleaseStatus>('/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<string[]>(['en', 'ru']);
let fetchedAt = $state(0);
let inflight: Promise<string[]> | null = null;
const TTL = 300_000;
return {
get items() { return data; },
async fetch(force = false): Promise<string[]> {
if (!force && fetchedAt > 0 && Date.now() - fetchedAt < TTL) return data;
if (inflight) return inflight;
inflight = (async () => {
try {
data = await api<string[]>('/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<void> {
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();
}