← back to Marketing Command Center
engine: make GET /today non-blocking + add generation run-lock
5869a28b656d00208d434493807a44a032cf91b4 · 2026-08-15 20:38:17 -0700 · Steve Abrams
GET /api/engine/today would run the full local-LLM caption batch inline
(sequential candidates x qwen3 vision+chat, 45s/90s per-call timeouts), so
the default endpoint blocked for minutes. The panel already dodged this via
?generate=0, but the bare endpoint hung any direct/health caller.
- generate.js: coalesce concurrent generateToday() onto one in-flight run
(run-lock) + export isGenerating(); prevents parallel batches hammering Ollama.
- index.js /today: default now kicks generation in the background and returns
current items immediately with {generating}. ?wait=1 keeps the old sync
behavior; ?generate=0 (panel poll) unchanged.
Files touched
M modules/engine/generate.jsM modules/engine/index.js
Diff
commit 5869a28b656d00208d434493807a44a032cf91b4
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Aug 15 20:38:17 2026 -0700
engine: make GET /today non-blocking + add generation run-lock
GET /api/engine/today would run the full local-LLM caption batch inline
(sequential candidates x qwen3 vision+chat, 45s/90s per-call timeouts), so
the default endpoint blocked for minutes. The panel already dodged this via
?generate=0, but the bare endpoint hung any direct/health caller.
- generate.js: coalesce concurrent generateToday() onto one in-flight run
(run-lock) + export isGenerating(); prevents parallel batches hammering Ollama.
- index.js /today: default now kicks generation in the background and returns
current items immediately with {generating}. ?wait=1 keeps the old sync
behavior; ?generate=0 (panel poll) unchanged.
---
modules/engine/generate.js | 15 +++++++++++++--
modules/engine/index.js | 32 ++++++++++++++++++++++----------
2 files changed, 35 insertions(+), 12 deletions(-)
diff --git a/modules/engine/generate.js b/modules/engine/generate.js
index d69d6cd..6702c8d 100644
--- a/modules/engine/generate.js
+++ b/modules/engine/generate.js
@@ -80,7 +80,18 @@ function buildItem(product, channel, caps, dateStr) {
}
// generateToday() — idempotent daily generation. Returns today's items.
-async function generateToday() {
+// A module-level run-lock coalesces concurrent callers (two "Generate" clicks, a
+// manual /generate + a background /today kick, etc.) onto ONE in-flight batch, so
+// the local Ollama is never hammered by parallel caption runs for the same day.
+let _inFlight = null;
+function generateToday() {
+ if (_inFlight) return _inFlight;
+ _inFlight = _generateToday().finally(() => { _inFlight = null; });
+ return _inFlight;
+}
+function isGenerating() { return _inFlight != null; }
+
+async function _generateToday() {
const dateStr = today();
// idempotent: today already generated → return as-is. But if a NEW channel was
@@ -133,4 +144,4 @@ async function generateToday() {
return store.load().filter(it => it.date === dateStr);
}
-module.exports = { generateToday };
+module.exports = { generateToday, isGenerating };
diff --git a/modules/engine/index.js b/modules/engine/index.js
index ca1b7e9..84fe7f6 100644
--- a/modules/engine/index.js
+++ b/modules/engine/index.js
@@ -69,22 +69,34 @@ module.exports = {
});
});
- // GET /today — today's suggestions. If none exist and generate isn't disabled
- // (?generate=0), ask the generator to build them. Missing generator → empty.
+ // GET /today — today's suggestions.
+ // ?generate=0 → read-only (the panel's 30s poll uses this; never generates).
+ // ?wait=1 → synchronous: block until generation finishes, then return
+ // the built items (the old default behavior; kept for callers
+ // that want the items in the same response).
+ // default → if empty, kick generation in the BACKGROUND and return the
+ // current items immediately with { generating:true }. This
+ // stops the endpoint from blocking for the multi-minute local
+ // LLM batch; the run-lock coalesces repeat hits onto one run.
router.get('/today', async (req, res) => {
const d = today();
let items = store.load().filter(it => it.date === d);
+ const gen = loadGenerator();
if (!items.length && req.query.generate !== '0') {
- const gen = loadGenerator();
- if (!gen) return res.json({ items: [], note: 'generator not built yet' });
- try {
- await gen.generateToday();
- items = store.load().filter(it => it.date === d);
- } catch (e) {
- return res.status(500).json({ items: [], error: e.message });
+ if (!gen) return res.json({ items: [], generating: false, note: 'generator not built yet' });
+ if (req.query.wait === '1') {
+ try {
+ await gen.generateToday();
+ items = store.load().filter(it => it.date === d);
+ } catch (e) {
+ return res.status(500).json({ items: [], error: e.message });
+ }
+ } else {
+ gen.generateToday().catch(e => console.error('[engine] background generateToday failed —', e.message));
+ return res.json({ items, generating: true });
}
}
- res.json({ items });
+ res.json({ items, generating: gen && typeof gen.isGenerating === 'function' ? gen.isGenerating() : false });
});
// POST /generate — force a generation run now (same lazy require).
← a30458a auto-data-snapshot: 2026-08-15T20:14:06 (1 data files) — pub
·
back to Marketing Command Center
·
auto-data-snapshot: 2026-08-16T03:49:55 (1 data files) — pub 6b14b13 →