perf: cache broadcast audience counts
This commit is contained in:
@@ -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 = "";
|
||||
|
||||
Reference in New Issue
Block a user