refactor: optimize support polling
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user