← back to New Engine
fix 'connecting' hang: add response cache (single-flight + serve-stale) so the slow canonical-DB catalog query isn't re-run every refresh; slower 15s poll, pause-when-hidden, clear loading state
fe0b37050a591cd5e4b6eaf340f2ec352d6ac576 · 2026-07-28 17:15:43 -0700 · Steve Abrams
Files touched
M public/index.htmlM server.js
Diff
commit fe0b37050a591cd5e4b6eaf340f2ec352d6ac576
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Jul 28 17:15:43 2026 -0700
fix 'connecting' hang: add response cache (single-flight + serve-stale) so the slow canonical-DB catalog query isn't re-run every refresh; slower 15s poll, pause-when-hidden, clear loading state
---
public/index.html | 7 +++++--
server.js | 43 +++++++++++++++++++++++++++++++++++++------
2 files changed, 42 insertions(+), 8 deletions(-)
diff --git a/public/index.html b/public/index.html
index a797f00..57962c4 100644
--- a/public/index.html
+++ b/public/index.html
@@ -120,7 +120,7 @@ a.open:hover{text-shadow:0 0 8px rgba(200,169,106,.6)}
<div class="subline" id="subline">N E W E S T F I R S T · L I V E · A U T O - R E F R E S H</div>
<div class="list" id="list"></div>
- <div class="hint" id="hint">connecting…</div>
+ <div class="hint" id="hint">loading newest arrivals… (first load may take a few seconds)</div>
</div>
<script>
const $ = id => document.getElementById(id);
@@ -181,6 +181,8 @@ function paint(records){
}
let lastRecords=[];
async function tick(){
+ if(document.hidden) return; // don't poll a backgrounded tab
+ if(!lastRecords.length) $('hint').textContent='loading newest arrivals… (first load may take a few seconds)';
try{
const r=await fetch('/api/new-items?'+qs(),{cache:'no-store'});
const d=await r.json();
@@ -213,7 +215,8 @@ fetch('/api/facets',{cache:'no-store'}).then(r=>r.json()).then(d=>{
sel.value=state.vendor; // restore selection now that options exist
}).catch(()=>{});
-tick(); setInterval(tick,6000);
+tick(); setInterval(tick,15000);
+document.addEventListener('visibilitychange',()=>{ if(!document.hidden) tick(); }); // refresh on return
</script>
</body>
</html>
diff --git a/server.js b/server.js
index 776cc98..ee9a96c 100644
--- a/server.js
+++ b/server.js
@@ -103,6 +103,34 @@ function authed(req) {
return u === AUTH_USER && rest.join(':') === AUTH_PASS;
}
+// ---- response cache (single-flight + serve-stale) --------------------------
+// The catalog query is a full seq-scan + sort of ~81k rows on the SHARED canonical
+// dw_unified (no created_at index), so it costs ~5-8s. Without a cache, the 6s
+// auto-refresh × every viewer would hammer that DB relentlessly. This cache makes
+// repeat/auto-refresh hits instant and collapses concurrent identical requests into
+// ONE db call (single-flight). On a stale hit it serves the stale copy immediately
+// and refreshes in the background — the UI never blocks after the first load.
+function makeCache(ttlMs, producer) {
+ const store = new Map(); // key -> { json, ts }
+ const inflight = new Map(); // key -> Promise<json>
+ return function get(key, arg, cb) {
+ const hit = store.get(key);
+ const fresh = hit && (Date.now() - hit.ts) < ttlMs;
+ if (fresh) return cb(null, hit.json, 'cache');
+ if (!inflight.has(key)) {
+ const pr = new Promise((resolve, reject) =>
+ producer(arg, (err, json) => err ? reject(err) : resolve(json)));
+ pr.then(json => store.set(key, { json, ts: Date.now() }), () => {});
+ pr.finally(() => inflight.delete(key));
+ inflight.set(key, pr);
+ }
+ if (hit) return cb(null, hit.json, 'stale'); // serve stale now; refresh continues
+ inflight.get(key).then(json => cb(null, json, 'live'), err => cb(err));
+ };
+}
+const itemsCache = makeCache(30000, fetchItems);
+const facetsCache = makeCache(300000, (_arg, cb) => fetchFacets(cb));
+
http.createServer((req, res) => {
const u = new URL(req.url, 'http://x');
let p = decodeURIComponent(u.pathname);
@@ -114,9 +142,9 @@ http.createServer((req, res) => {
return res.end('auth required');
}
- // Vendor facets for the filter dropdown.
+ // Vendor facets for the filter dropdown (cached 5 min).
if (p === '/api/facets') {
- fetchFacets((err, json) => {
+ facetsCache('vendors', null, (err, json) => {
if (!err && json) { res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=120' }); return res.end(json); }
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end('{"vendors":[]}');
@@ -124,11 +152,14 @@ http.createServer((req, res) => {
return;
}
- // Live newest feed (with filters). On any DB failure, fall back to the frozen snapshot
- // (wrapped to match the {records,total} contract) so the front end always gets valid data.
+ // Live newest feed (with filters), cached 30s per distinct filter. On any DB failure,
+ // fall back to the frozen snapshot (matching the {records,total} contract).
if (p === '/api/new-items') {
- fetchItems(u.searchParams, (err, json) => {
- if (!err && json) { res.writeHead(200, { 'Content-Type': 'application/json', 'X-Data-Source': 'live', 'Cache-Control': 'no-store' }); return res.end(json); }
+ const sp = u.searchParams;
+ const key = ['items', sp.get('limit') || '60', sp.get('q') || '', sp.get('vendor') || '',
+ sp.get('sort') || 'newest', sp.get('since') || 'all'].join('');
+ itemsCache(key, sp, (err, json, src) => {
+ if (!err && json) { res.writeHead(200, { 'Content-Type': 'application/json', 'X-Data-Source': src || 'live', 'Cache-Control': 'no-store' }); return res.end(json); }
fs.readFile(path.join(ROOT, 'data.json'), (e, buf) => {
if (e) { res.writeHead(502); return res.end('no live db and no snapshot'); }
// Guard the parse: a corrupt snapshot must not throw in this async callback and
← 40f95bb deploy.conf: HEALTH_URL -> public https domain (surfaces on
·
back to New Engine
·
viewer standard: add Grid/List toggle + per-field on/off men 1e55589 →