perf: cache broadcast audience counts

This commit is contained in:
3252a8
2026-06-07 23:15:44 +03:00
parent d1c4a6de80
commit 4b8f939a25
7 changed files with 244 additions and 28 deletions
+1
View File
@@ -518,6 +518,7 @@
if (typeof window !== "undefined") {
window.addEventListener("popstate", onPopState);
}
void broadcastStore.loadCounts();
return () => {
if (motionMql) motionMql.removeEventListener("change", onMotionChange);
if (compactMql) {
@@ -8,15 +8,23 @@
export let at;
const broadcastStore = getContext("broadcastStore");
$: ({ broadcastTarget, broadcastText, broadcastBusy, broadcastResult, broadcastCounts } =
$broadcastStore);
$: ({
broadcastTarget,
broadcastText,
broadcastBusy,
broadcastResult,
broadcastCounts,
broadcastCountsLoading,
} = $broadcastStore);
const BROADCAST_TARGET_OPTIONS = broadcastStore.BROADCAST_TARGET_OPTIONS;
// Append the resolved audience size to each option once counts are loaded.
$: targetOptions = BROADCAST_TARGET_OPTIONS.map((option) => {
const count = broadcastCounts?.[option.value];
return count == null ? option : { ...option, label: `${option.label} (${count})` };
if (count != null) return { ...option, label: `${option.label} (${count})` };
if (broadcastCountsLoading) return { ...option, label: `${option.label} (...)` };
return option;
});
onMount(() => {
@@ -1,12 +1,20 @@
import { writable } from "svelte/store";
export function createBroadcastStore({ api, onToast, at }) {
const COUNTS_CACHE_TTL_MS = 30_000;
const COUNTS_DISPLAY_CACHE_TTL_MS = 5 * 60_000;
const COUNTS_STORAGE_KEY = "remnawave-admin:broadcast-audience-counts";
let countsPromise = null;
const cachedCounts = readStoredCounts();
const state = writable({
broadcastTarget: "all",
broadcastText: "",
broadcastBusy: false,
broadcastResult: null,
broadcastCounts: null,
broadcastCounts: cachedCounts?.counts || null,
broadcastCountsLoading: false,
broadcastCountsLoadedAt: cachedCounts?.loadedAt || 0,
});
const BROADCAST_TARGET_OPTIONS = [
@@ -28,17 +36,70 @@ export function createBroadcastStore({ api, onToast, at }) {
},
];
async function loadCounts() {
function countsAreFresh(stateSnapshot) {
return (
stateSnapshot.broadcastCounts &&
Date.now() - Number(stateSnapshot.broadcastCountsLoadedAt || 0) < COUNTS_CACHE_TTL_MS
);
}
function readStoredCounts() {
try {
const res = await api("/admin/broadcast/audience-counts");
if (res?.ok && res.counts) {
state.update((s) => ({ ...s, broadcastCounts: res.counts }));
}
if (typeof window === "undefined" || !window.sessionStorage) return null;
const raw = window.sessionStorage.getItem(COUNTS_STORAGE_KEY);
if (!raw) return null;
const payload = JSON.parse(raw);
const loadedAt = Number(payload?.loadedAt || 0);
if (!payload?.counts || Date.now() - loadedAt > COUNTS_DISPLAY_CACHE_TTL_MS) return null;
return { counts: payload.counts, loadedAt };
} catch {
// Counts are advisory; ignore failures and keep plain labels.
return null;
}
}
function writeStoredCounts(counts, loadedAt) {
try {
if (typeof window === "undefined" || !window.sessionStorage) return;
window.sessionStorage.setItem(COUNTS_STORAGE_KEY, JSON.stringify({ counts, loadedAt }));
} catch {
// Ignore storage quota/privacy errors; in-memory counts still work.
}
}
async function loadCounts({ force = false } = {}) {
let shouldLoad = false;
state.update((s) => {
if (!force && countsAreFresh(s)) return s;
if (countsPromise || s.broadcastCountsLoading) return s;
shouldLoad = true;
return { ...s, broadcastCountsLoading: true };
});
if (!shouldLoad) return countsPromise || Promise.resolve();
countsPromise = (async () => {
try {
const res = await api("/admin/broadcast/audience-counts");
if (res?.ok && res.counts) {
const loadedAt = Date.now();
state.update((s) => ({
...s,
broadcastCounts: res.counts,
broadcastCountsLoadedAt: loadedAt,
}));
writeStoredCounts(res.counts, loadedAt);
}
} catch {
// Counts are advisory; ignore failures and keep existing/plain labels.
} finally {
state.update((s) => ({ ...s, broadcastCountsLoading: false }));
countsPromise = null;
}
})();
return countsPromise;
}
async function runBroadcast() {
let text = "";
let target = "";