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
+3 -1
View File
@@ -300,6 +300,7 @@
: plans; : plans;
$: devicesEnabled = Boolean(appSettings?.my_devices_enabled); $: devicesEnabled = Boolean(appSettings?.my_devices_enabled);
$: supportEnabled = Boolean(appSettings?.support_tickets_enabled ?? true); $: supportEnabled = Boolean(appSettings?.support_tickets_enabled ?? true);
$: supportStore.setActive(Boolean(mode === "app" && screen === "support" && supportEnabled));
$: subscription = data?.subscription || DEV_MOCK.data.subscription; $: subscription = data?.subscription || DEV_MOCK.data.subscription;
$: hasActiveTariffSubscription = Boolean( $: hasActiveTariffSubscription = Boolean(
tariffMode && subscription?.active && subscription?.tariff_key tariffMode && subscription?.active && subscription?.tariff_key
@@ -851,8 +852,9 @@
if (payload.settings?.support_tickets_enabled !== false) { if (payload.settings?.support_tickets_enabled !== false) {
if (typeof payload.support_unread_count !== "undefined") { if (typeof payload.support_unread_count !== "undefined") {
supportStore.hydrateUnread(payload.support_unread_count); supportStore.hydrateUnread(payload.support_unread_count);
} else {
void supportStore.refreshUnread();
} }
void supportStore.refreshUnread();
supportStore.startPolling({ includeList: false }); supportStore.startPolling({ includeList: false });
} }
if (section === "support" && initialSupportTicketId) { if (section === "support" && initialSupportTicketId) {
+130 -27
View File
@@ -1,6 +1,15 @@
import { writable } from "svelte/store"; import { writable } from "svelte/store";
export function createSupportStore({ api, t, showToast }) { 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({ const state = writable({
tickets: [], tickets: [],
openedTicketId: null, openedTicketId: null,
@@ -19,14 +28,56 @@ export function createSupportStore({ api, t, showToast }) {
}); });
let pollTimer = null; 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 listRequestSeq = 0;
let listPromise = null; let listPromise = null;
let listPromiseKey = ""; let listPromiseKey = "";
let unreadPromise = null; let unreadPromise = null;
function hydrateUnread(value) { function updateUnreadBackoff(value, countEmptyPoll = false) {
const next = Math.max(0, Number(value || 0)); 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) => ({ state.update((s) => ({
...s, ...s,
unreadCount: next, unreadCount: next,
@@ -213,22 +264,24 @@ export function createSupportStore({ api, t, showToast }) {
await refreshUnread(); await refreshUnread();
} }
async function refreshUnread() { async function refreshUnread(options = {}) {
if (unreadPromise) return unreadPromise; 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 () => { unreadPromise = (async () => {
try { try {
const res = await api("/support/unread"); const res = await api("/support/unread");
if (res?.ok) { if (res?.ok) {
const unreadCount = updateUnreadBackoff(res.unread, options.countEmpty === true);
state.update((s) => ({ state.update((s) => ({
...s, ...s,
unreadCount: Math.max(0, Number(res.unread || 0)), unreadCount,
unreadLoaded: true, unreadLoaded: true,
})); }));
} }
return res; return res;
} finally { } finally {
state.update((s) => ({ ...s, unreadLoading: false })); if (!silent) state.update((s) => ({ ...s, unreadLoading: false }));
unreadPromise = null; unreadPromise = null;
} }
})(); })();
@@ -240,33 +293,82 @@ export function createSupportStore({ api, t, showToast }) {
loadList({ force: true, showLoading: true }); loadList({ force: true, showLoading: true });
} }
function startPolling(options = {}) { async function runPollTick() {
const includeList = options.includeList !== false; pollTimer = null;
pollIncludeList = pollIncludeList || includeList; if (!pollingEnabled || typeof document === "undefined") return;
if (pollTimer || typeof window === "undefined") return; if (document.visibilityState !== "visible") {
state.update((s) => ({ ...s, polling: true })); schedulePoll(HIDDEN_POLL_MS);
const tick = async () => { return;
if (document.visibilityState === "visible") { }
await refreshUnread(); if (pollInFlight) {
if (!pollIncludeList) return; schedulePoll(ACTIVE_POLL_MS);
let opened = null; return;
state.update((s) => { }
opened = s.openedTicketId;
return s; pollInFlight = true;
}); let failed = false;
try {
await refreshUnread({ silent: true, countEmpty: true });
if (supportActive) {
const opened = currentOpenedTicketId();
if (opened) await refreshCurrentTicket(opened); if (opened) await refreshCurrentTicket(opened);
else await loadList({ silent: true }); else await loadList({ silent: true });
} }
}; } catch (_error) {
pollTimer = window.setInterval(tick, 15000); failed = true;
document.addEventListener("visibilitychange", tick); } 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() { function closePolling() {
if (pollTimer) window.clearInterval(pollTimer); pollingEnabled = false;
pollTimer = null; supportActive = false;
pollIncludeList = false; pollInFlight = false;
state.update((s) => ({ ...s, polling: false })); emptyUnreadPolls = 0;
clearPollTimer();
stopVisibilityListener();
state.update((s) => (s.polling ? { ...s, polling: false } : s));
} }
return { return {
@@ -281,6 +383,7 @@ export function createSupportStore({ api, t, showToast }) {
markRead, markRead,
refreshUnread, refreshUnread,
setStatusFilter, setStatusFilter,
setActive,
startPolling, startPolling,
closePolling, closePolling,
}; };