diff --git a/frontend/context-poller.js b/frontend/context-poller.js new file mode 100644 index 0000000..4159a70 --- /dev/null +++ b/frontend/context-poller.js @@ -0,0 +1,73 @@ +function createContextPoller({ + fetchContext, + onSnapshot, + onError, + isHidden = () => false, + setTimer = setTimeout, + clearTimer = clearTimeout, + intervalMs = 8000, +}) { + let inFlight = null; + let timer = null; + let stopped = false; + + function cancelTimer() { + if (timer !== null) clearTimer(timer); + timer = null; + } + + function schedule() { + cancelTimer(); + if (stopped || isHidden()) return; + timer = setTimer(() => { + timer = null; + refresh(); + }, intervalMs); + } + + function refresh() { + if (stopped || isHidden()) return Promise.resolve(null); + if (inFlight) return inFlight; + + let request; + try { + request = fetchContext(); + } catch (error) { + request = Promise.reject(error); + } + inFlight = Promise.resolve(request) + .then((snapshot) => { + onSnapshot(snapshot); + return snapshot; + }) + .catch((error) => { + onError(error); + return null; + }) + .finally(() => { + inFlight = null; + schedule(); + }); + return inFlight; + } + + function setVisible(visible) { + cancelTimer(); + if (!visible) return Promise.resolve(null); + return refresh(); + } + + return { + start: refresh, + refresh, + setVisible, + stop() { + stopped = true; + cancelTimer(); + }, + }; +} + +if (typeof module !== 'undefined' && module.exports) { + module.exports = createContextPoller; +} diff --git a/frontend/index.html b/frontend/index.html index 905a7bd..0dad9a2 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -56,7 +56,7 @@ textarea { resize: vertical; min-height: 120px; } .event:last-child { border-bottom: 0; } .my-work { grid-column: 1 / -1; } .my-work-header { display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap; } -.work-filters { display:flex; gap:8px; } +.work-filters { display:flex; gap:8px; flex-wrap:wrap; } .work-filter { min-height: 44px; } .work-filter[aria-pressed="true"] { border-color:var(--accent); background:#1d4f7a; } .my-work-list { display:grid; grid-template-columns:repeat(auto-fit,minmax(260px,1fr)); gap:10px; } @@ -69,7 +69,7 @@ textarea { resize: vertical; min-height: 120px; } .my-work { margin:0; } .my-work-list { grid-template-columns:1fr; } .work-filters { width:100%; } - .work-filter { flex:1; } + .work-filter { flex:1 1 calc(50% - 8px); } } .obi { width:14px; height:14px; background: url('data:image/svg+xml;utf8,') center/contain no-repeat; display:inline-block; } .footer { padding: 12px; text-align: center; color:#4e6b8a; font-size:12px; } @@ -96,10 +96,10 @@ textarea { resize: vertical; min-height: 120px; }
Loading assigned work…
- - - - + + + +
@@ -195,6 +195,7 @@ textarea { resize: vertical; min-height: 120px; } + ' in html + assert "createContextPoller({" in html + assert "renderContextSnapshot(data)" in html + assert "renderRepoMix(qs('#repo-mix'), data)" in html + assert "setInterval(tickWidgets, 2000)" not in html + assert "updateRepoMix(qs('#repo-mix'), fetch)" not in html + assert "setInterval(load, 8000)" not in html + assert "document.addEventListener('visibilitychange'" in html \ No newline at end of file