← back to Big Red
feat(big-red/tick2): Shopify lookup 60s TTL cache (next: SKU resolver fallback)
83791703b325f0b5829a1033c20d3af68bc53750 · 2026-05-12 01:27:36 -0700 · Steve Abrams
In-memory Map keyed by SKU, 60s TTL. Wraps lookupSkuLive() so repeat
questions about the same SKU within a minute hit local memory instead
of Shopify Admin GraphQL.
- Cache age surfaced in response as _cache_age_ms when serving from
cache; absent on fresh fetches (so consumers can detect freshness).
- Null results cached too — repeated 'no-such-SKU' queries don't
hammer Shopify any more than valid SKUs do.
- Background sweep every 5 min drops entries older than TTL*4 so the
Map doesn't grow unbounded across long uptime. setInterval().unref()
so it doesn't block shutdown.
Live timing (DWKK-150018 against running big-red):
call 1 (fresh): ~600ms — Shopify roundtrip
call 2 (cache): 52ms — _cache_age_ms: 52
calls 3-6: 34-41ms each — pure cache hits
YOLO tick 2 — next tick (T+30m): expand SKU resolver to fall back into
sister catalogs (scalamandre, sisterparish, etc.) when shopify_products
misses, so retired/unsynced SKUs still resolve.
Files touched
Diff
commit 83791703b325f0b5829a1033c20d3af68bc53750
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue May 12 01:27:36 2026 -0700
feat(big-red/tick2): Shopify lookup 60s TTL cache (next: SKU resolver fallback)
In-memory Map keyed by SKU, 60s TTL. Wraps lookupSkuLive() so repeat
questions about the same SKU within a minute hit local memory instead
of Shopify Admin GraphQL.
- Cache age surfaced in response as _cache_age_ms when serving from
cache; absent on fresh fetches (so consumers can detect freshness).
- Null results cached too — repeated 'no-such-SKU' queries don't
hammer Shopify any more than valid SKUs do.
- Background sweep every 5 min drops entries older than TTL*4 so the
Map doesn't grow unbounded across long uptime. setInterval().unref()
so it doesn't block shutdown.
Live timing (DWKK-150018 against running big-red):
call 1 (fresh): ~600ms — Shopify roundtrip
call 2 (cache): 52ms — _cache_age_ms: 52
calls 3-6: 34-41ms each — pure cache hits
YOLO tick 2 — next tick (T+30m): expand SKU resolver to fall back into
sister catalogs (scalamandre, sisterparish, etc.) when shopify_products
misses, so retired/unsynced SKUs still resolve.
---
server.js | 31 +++++++++++++++++++++++++++++--
1 file changed, 29 insertions(+), 2 deletions(-)
diff --git a/server.js b/server.js
index d31db91..03c7fa8 100644
--- a/server.js
+++ b/server.js
@@ -79,8 +79,28 @@ async function lookupSkuInPg(sku) {
}
}
+// In-memory cache for live Shopify SKU lookups. 60s TTL — long enough to
+// absorb a chatty back-and-forth about the same SKU within a single turn
+// without re-hammering Shopify Admin GraphQL, short enough that a real
+// inventory or price change is reflected within a minute. Map grows boundedly
+// because we sweep entries older than TTL*4 every 5 min (cap insurance).
+const SHOPIFY_TTL_MS = 60_000;
+const SHOPIFY_CACHE = new Map(); // sku -> { result, fetched_at }
+setInterval(() => {
+ const cutoff = Date.now() - SHOPIFY_TTL_MS * 4;
+ let dropped = 0;
+ for (const [k, v] of SHOPIFY_CACHE.entries()) {
+ if (v.fetched_at < cutoff) { SHOPIFY_CACHE.delete(k); dropped++; }
+ }
+ if (dropped > 0) console.log(`[shopify-cache] swept ${dropped} stale entries`);
+}, 5 * 60 * 1000).unref();
+
async function lookupSkuLive(sku) {
if (!SHOPIFY_STORE || !SHOPIFY_TOKEN) return null;
+ const cached = SHOPIFY_CACHE.get(sku);
+ if (cached && (Date.now() - cached.fetched_at) < SHOPIFY_TTL_MS) {
+ return cached.result ? { ...cached.result, _cache_age_ms: Date.now() - cached.fetched_at } : null;
+ }
const query = `{
products(first: 1, query: "sku:${sku}") {
edges { node {
@@ -101,9 +121,14 @@ async function lookupSkuLive(sku) {
if (!r.ok) { console.error('[shopify]', r.status, await r.text().catch(()=>'')); return null; }
const j = await r.json();
const node = j?.data?.products?.edges?.[0]?.node;
- if (!node) return null;
+ if (!node) {
+ // Cache a null too — repeated "no-such-SKU" queries within a minute
+ // shouldn't hammer Shopify any more than valid queries should.
+ SHOPIFY_CACHE.set(sku, { result: null, fetched_at: Date.now() });
+ return null;
+ }
const v = node.variants?.edges?.[0]?.node || {};
- return {
+ const result = {
shopify_id: node.legacyResourceId || node.id,
handle: node.handle,
title: node.title,
@@ -116,6 +141,8 @@ async function lookupSkuLive(sku) {
inventory: v.inventoryQuantity ?? null,
available: v.availableForSale ?? null,
};
+ SHOPIFY_CACHE.set(sku, { result, fetched_at: Date.now() });
+ return result;
} catch (e) {
console.error('[shopify lookup]', e.message);
return null;
← 56b06d5 feat(big-red/tick1): per-mode launcher ring color (next: sho
·
back to Big Red
·
feat(big-red/tick3): SKU resolver fallback into sister catal 6688ef1 →