function buildLiveRevisionQuery(revisions = {}) { const params = new URLSearchParams(); const tokenPattern = /^[0-9a-f]{16}\.[0-9]{1,20}$/; ['context', 'events', 'notifications'].forEach((section) => { const revision = revisions[section]; if (typeof revision === 'string' && tokenPattern.test(revision)) { params.set(section + '_revision', revision); } }); return params.toString(); } function createContextPoller({ fetchContext, onSnapshot, onError, isHidden = () => false, setTimer = setTimeout, clearTimer = clearTimeout, setDeadlineTimer = setTimeout, clearDeadlineTimer = clearTimeout, intervalMs = 8000, timeoutMs = 12000, }) { let activeRequest = null; let timer = null; let stopped = false; let revisions = {}; let retainedSnapshot = null; function cancelTimer() { if (timer !== null) clearTimer(timer); timer = null; } function schedule() { cancelTimer(); if (stopped || isHidden()) return; timer = setTimer(() => { timer = null; refresh(); }, intervalMs); } function abortError() { const error = new Error('Live update superseded.'); error.name = 'AbortError'; return error; } function supersede(request) { if (!request || request.settled) return; request.superseded = true; request.controller.abort(); request.rejectDeadline(abortError()); } function refresh(options = {}) { if (stopped || isHidden()) return Promise.resolve(null); if (activeRequest && !options.force) return activeRequest.promise; if (activeRequest) supersede(activeRequest); const controller = new AbortController(); let rejectDeadline; const deadlinePromise = new Promise((resolve, reject) => { rejectDeadline = reject; }); const requestState = { controller, deadline: null, promise: null, rejectDeadline, settled: false, superseded: false, }; activeRequest = requestState; requestState.deadline = setDeadlineTimer(() => { if (requestState.settled || requestState.superseded) return; controller.abort(); const error = new Error(`Live update timed out after ${timeoutMs}ms.`); error.name = 'TimeoutError'; rejectDeadline(error); }, timeoutMs); let request; try { request = fetchContext({ ...revisions }, { signal: controller.signal }); } catch (error) { request = Promise.reject(error); } requestState.promise = Promise.race([Promise.resolve(request), deadlinePromise]) .then((snapshot) => { if (activeRequest !== requestState) return retainedSnapshot; const changedSections = ['context', 'events', 'notifications'].filter( (section) => Object.prototype.hasOwnProperty.call(snapshot, section) ); retainedSnapshot = retainedSnapshot ? { ...retainedSnapshot, ...snapshot } : { ...snapshot }; revisions = { ...revisions, ...(snapshot.revisions || {}) }; onSnapshot(retainedSnapshot, changedSections); return retainedSnapshot; }) .catch((error) => { if (activeRequest === requestState && !stopped) onError(error); return null; }) .finally(() => { requestState.settled = true; clearDeadlineTimer(requestState.deadline); if (activeRequest !== requestState) return; activeRequest = null; schedule(); }); return requestState.promise; } function setVisible(visible) { cancelTimer(); if (!visible) return Promise.resolve(null); return refresh({ force: true }); } return { start: refresh, refresh, setVisible, stop() { stopped = true; cancelTimer(); if (activeRequest) { const request = activeRequest; activeRequest = null; supersede(request); } }, }; } createContextPoller.buildRevisionQuery = buildLiveRevisionQuery; if (typeof module !== 'undefined' && module.exports) { module.exports = createContextPoller; }