[object Object]

← back to Wallco Ai

by-color: 5min in-memory TTL cache (FIFO 1k cap, sorted-exclude key) — same PDP rail hex now skips full 1515-design ΔE recompute

97ebc807ea9626a8a0e1fb8def7dd8cc75659b7c · 2026-05-12 18:19:49 -0700 · SteveStudio2

Files touched

Diff

commit 97ebc807ea9626a8a0e1fb8def7dd8cc75659b7c
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date:   Tue May 12 18:19:49 2026 -0700

    by-color: 5min in-memory TTL cache (FIFO 1k cap, sorted-exclude key) — same PDP rail hex now skips full 1515-design ΔE recompute
---
 server.js | 33 +++++++++++++++++++++++++++++++--
 1 file changed, 31 insertions(+), 2 deletions(-)

diff --git a/server.js b/server.js
index 93bb72f..f9290c8 100644
--- a/server.js
+++ b/server.js
@@ -384,15 +384,36 @@ function _dE(a, b) {
   const dL = a[0]-b[0], dA = a[1]-b[1], dB = a[2]-b[2];
   return Math.sqrt(dL*dL + dA*dA + dB*dB);
 }
+// In-memory TTL cache for /api/designs/by-color. The Lab conversion + ΔE sort
+// runs over every DESIGN on every call — the same PDP rail hex + size combo
+// gets hammered as users browse, so cache the JSON body for the same 5min
+// window the response already advertises in Cache-Control. Bounded to 1k keys
+// FIFO so memory can't grow unbounded; invalidated whenever DESIGNS is rebuilt
+// (callers should bump _byColorCacheVer if they mutate DESIGNS in place).
+const _BY_COLOR_TTL_MS = 300_000;
+const _BY_COLOR_MAX = 1000;
+const _byColorCache = new Map();   // key -> { body, expiresAt }
+let   _byColorCacheVer = 0;        // bump to invalidate
 app.get('/api/designs/by-color', (req, res) => {
   const hex = String(req.query.hex || '').trim();
   const n = Math.max(1, Math.min(50, parseInt(req.query.n || '12', 10)));
-  const exclude = new Set(String(req.query.exclude || '').split(',').map(s => parseInt(s.trim(),10)).filter(x => x>0));
+  const excludeArr = String(req.query.exclude || '').split(',').map(s => parseInt(s.trim(),10)).filter(x => x>0).sort((a,b)=>a-b);
+  const exclude = new Set(excludeArr);
   // Optional ?within=category — restricts results to designs in this category.
   // Useful for "more in this palette WITHIN damask" rails on a vertical PDP.
   const within = String(req.query.within || '').toLowerCase().trim();
   const target = _hexToLab(hex);
   if (!target) return res.status(400).json({ ok:false, error:'invalid hex (try ?hex=%23c16d74 or hex=c16d74)' });
+  // Cache key sorts exclude so e.g. exclude=72,5 and exclude=5,72 hit the same entry.
+  const cacheKey = `v${_byColorCacheVer}|${hex.toLowerCase()}|${n}|${within}|${excludeArr.join(',')}`;
+  const now = Date.now();
+  const hit = _byColorCache.get(cacheKey);
+  if (hit && hit.expiresAt > now) {
+    res.setHeader('Cache-Control', 'public, max-age=300, must-revalidate');
+    res.setHeader('X-Cache', 'HIT');
+    return res.json(hit.body);
+  }
+  if (hit) _byColorCache.delete(cacheKey);   // expired — drop before recompute
   const ranked = [];
   for (const d of DESIGNS) {
     if (exclude.has(d.id)) continue;
@@ -403,8 +424,16 @@ app.get('/api/designs/by-color', (req, res) => {
     ranked.push({ id: d.id, title: d.title, category: d.category, handle: d.handle, image_url: d.image_url, dominant_hex: d.dominant_hex, room_mockups: d.room_mockups || [], delta_e: _dE(target, lab), url: `/design/${d.id}` });
   }
   ranked.sort((a,b) => a.delta_e - b.delta_e);
+  const body = { hex, n, within: within || null, count: ranked.length, exclude: [...exclude], results: ranked.slice(0, n).map(r => ({ ...r, delta_e: Math.round(r.delta_e * 10)/10 })) };
+  // FIFO evict if full. Map preserves insertion order, so the first key is the oldest.
+  if (_byColorCache.size >= _BY_COLOR_MAX) {
+    const firstKey = _byColorCache.keys().next().value;
+    if (firstKey !== undefined) _byColorCache.delete(firstKey);
+  }
+  _byColorCache.set(cacheKey, { body, expiresAt: now + _BY_COLOR_TTL_MS });
   res.setHeader('Cache-Control', 'public, max-age=300, must-revalidate');
-  res.json({ hex, n, within: within || null, count: ranked.length, exclude: [...exclude], results: ranked.slice(0, n).map(r => ({ ...r, delta_e: Math.round(r.delta_e * 10)/10 })) });
+  res.setHeader('X-Cache', 'MISS');
+  res.json(body);
 });
 
 // ── /api/designs/random?n=12&category=floral&seed=42

← 66cfe47 drunk-animal overnight generator + /admin/reload-designs  ·  back to Wallco Ai  ·  drunk-animals: fix launchd PATH for PIL + retro-backfill 26 307a374 →