refactor: optimize support polling

This commit is contained in:
3252a8
2026-05-21 13:41:01 +03:00
parent f69f6546f0
commit e7301e2c48
2 changed files with 133 additions and 28 deletions
+130 -27
View File
@@ -1,6 +1,15 @@
import { writable } from "svelte/store";
export function createSupportStore({ api, t, showToast }) {
const ACTIVE_POLL_MS = 15_000;
const BACKGROUND_POLL_MS = 45_000;
const IDLE_POLL_MS = 120_000;
const PAUSED_POLL_MS = 300_000;
const HIDDEN_POLL_MS = 300_000;
const ERROR_POLL_MS = 90_000;
const IDLE_AFTER_EMPTY_POLLS = 3;
const PAUSE_AFTER_EMPTY_POLLS = 6;
const state = writable({
tickets: [],
openedTicketId: null,
@@ -19,14 +28,56 @@ export function createSupportStore({ api, t, showToast }) {
});
let pollTimer = null;
let pollIncludeList = false;
let pollingEnabled = false;
let pollInFlight = false;
let supportActive = false;
let emptyUnreadPolls = 0;
let lastUnreadCount = 0;
let visibilityHandler = null;
let listRequestSeq = 0;
let listPromise = null;
let listPromiseKey = "";
let unreadPromise = null;
function hydrateUnread(value) {
function updateUnreadBackoff(value, countEmptyPoll = false) {
const next = Math.max(0, Number(value || 0));
if (countEmptyPoll && next === 0 && next === lastUnreadCount) emptyUnreadPolls += 1;
else if (next > 0 || next !== lastUnreadCount) emptyUnreadPolls = 0;
lastUnreadCount = next;
return next;
}
function nextPollDelay() {
if (supportActive) return ACTIVE_POLL_MS;
if (lastUnreadCount > 0) return BACKGROUND_POLL_MS;
if (emptyUnreadPolls >= PAUSE_AFTER_EMPTY_POLLS) return PAUSED_POLL_MS;
if (emptyUnreadPolls >= IDLE_AFTER_EMPTY_POLLS) return IDLE_POLL_MS;
return BACKGROUND_POLL_MS;
}
function clearPollTimer() {
if (!pollTimer) return;
if (typeof window !== "undefined") window.clearTimeout(pollTimer);
pollTimer = null;
}
function schedulePoll(delayMs = nextPollDelay()) {
if (!pollingEnabled || typeof window === "undefined") return;
clearPollTimer();
pollTimer = window.setTimeout(runPollTick, Math.max(0, Number(delayMs) || 0));
}
function currentOpenedTicketId() {
let opened = null;
state.update((s) => {
opened = s.openedTicketId;
return s;
});
return opened;
}
function hydrateUnread(value) {
const next = updateUnreadBackoff(value);
state.update((s) => ({
...s,
unreadCount: next,
@@ -213,22 +264,24 @@ export function createSupportStore({ api, t, showToast }) {
await refreshUnread();
}
async function refreshUnread() {
async function refreshUnread(options = {}) {
if (unreadPromise) return unreadPromise;
state.update((s) => ({ ...s, unreadLoading: true }));
const silent = options.silent === true;
if (!silent) state.update((s) => ({ ...s, unreadLoading: true }));
unreadPromise = (async () => {
try {
const res = await api("/support/unread");
if (res?.ok) {
const unreadCount = updateUnreadBackoff(res.unread, options.countEmpty === true);
state.update((s) => ({
...s,
unreadCount: Math.max(0, Number(res.unread || 0)),
unreadCount,
unreadLoaded: true,
}));
}
return res;
} finally {
state.update((s) => ({ ...s, unreadLoading: false }));
if (!silent) state.update((s) => ({ ...s, unreadLoading: false }));
unreadPromise = null;
}
})();
@@ -240,33 +293,82 @@ export function createSupportStore({ api, t, showToast }) {
loadList({ force: true, showLoading: true });
}
function startPolling(options = {}) {
const includeList = options.includeList !== false;
pollIncludeList = pollIncludeList || includeList;
if (pollTimer || typeof window === "undefined") return;
state.update((s) => ({ ...s, polling: true }));
const tick = async () => {
if (document.visibilityState === "visible") {
await refreshUnread();
if (!pollIncludeList) return;
let opened = null;
state.update((s) => {
opened = s.openedTicketId;
return s;
});
async function runPollTick() {
pollTimer = null;
if (!pollingEnabled || typeof document === "undefined") return;
if (document.visibilityState !== "visible") {
schedulePoll(HIDDEN_POLL_MS);
return;
}
if (pollInFlight) {
schedulePoll(ACTIVE_POLL_MS);
return;
}
pollInFlight = true;
let failed = false;
try {
await refreshUnread({ silent: true, countEmpty: true });
if (supportActive) {
const opened = currentOpenedTicketId();
if (opened) await refreshCurrentTicket(opened);
else await loadList({ silent: true });
}
};
pollTimer = window.setInterval(tick, 15000);
document.addEventListener("visibilitychange", tick);
} catch (_error) {
failed = true;
} finally {
pollInFlight = false;
if (pollingEnabled) schedulePoll(failed ? ERROR_POLL_MS : nextPollDelay());
}
}
function setActive(active) {
const next = Boolean(active);
if (supportActive === next) return;
supportActive = next;
if (supportActive) emptyUnreadPolls = 0;
if (pollingEnabled) schedulePoll(supportActive ? ACTIVE_POLL_MS : nextPollDelay());
}
function startPolling(options = {}) {
const includeList = options.includeList !== false;
if (typeof window === "undefined") return;
pollingEnabled = true;
if (includeList) supportActive = true;
state.update((s) => (s.polling ? s : { ...s, polling: true }));
if (!visibilityHandler && typeof document !== "undefined") {
visibilityHandler = () => {
if (!pollingEnabled) return;
if (document.visibilityState === "visible") {
emptyUnreadPolls = 0;
schedulePoll(0);
} else {
schedulePoll(HIDDEN_POLL_MS);
}
};
document.addEventListener("visibilitychange", visibilityHandler);
}
if (!pollTimer && !pollInFlight) {
schedulePoll(supportActive ? ACTIVE_POLL_MS : nextPollDelay());
} else if (supportActive) {
schedulePoll(ACTIVE_POLL_MS);
}
}
function stopVisibilityListener() {
if (!visibilityHandler || typeof document === "undefined") return;
document.removeEventListener("visibilitychange", visibilityHandler);
visibilityHandler = null;
}
function closePolling() {
if (pollTimer) window.clearInterval(pollTimer);
pollTimer = null;
pollIncludeList = false;
state.update((s) => ({ ...s, polling: false }));
pollingEnabled = false;
supportActive = false;
pollInFlight = false;
emptyUnreadPolls = 0;
clearPollTimer();
stopVisibilityListener();
state.update((s) => (s.polling ? { ...s, polling: false } : s));
}
return {
@@ -281,6 +383,7 @@ export function createSupportStore({ api, t, showToast }) {
markRead,
refreshUnread,
setStatusFilter,
setActive,
startPolling,
closePolling,
};