/** * ───────────────────────────────────────────── * SPB Drive — Personal Cloud Drive * ───────────────────────────────────────────── * Author : Simon-Pierre Boucher * Contact : contact@spboucher.ai * File : src/preview/queue.mjs * Purpose : Tiny in-process job queue (concurrency 2) for transcodes/conversions * License : MIT © Simon-Pierre Boucher * ───────────────────────────────────────────── */ const CONCURRENCY = 2; /** @type {Map, status: string}>} */ const jobs = new Map(); const waiting = []; let running = 0; function pump() { while (running < CONCURRENCY && waiting.length > 0) { const next = waiting.shift(); running += 1; next.entry.status = 'running'; Promise.resolve() .then(next.fn) .then( (val) => { next.entry.status = 'done'; next.resolve(val); }, (err) => { next.entry.status = 'error'; next.entry.error = String(err?.message ?? err); next.reject(err); }, ) .finally(() => { running -= 1; setTimeout(() => jobs.delete(next.key), 60_000); pump(); }); } } /** * Enqueue work keyed by a cache key — duplicate keys share one promise, * so a conversion runs at most once per unique blob. * @param {string} key e.g. `office:` * @param {() => Promise} fn * @returns {Promise} */ export function enqueue(key, fn) { const existing = jobs.get(key); if (existing) return existing.promise; const entry = { status: 'queued', error: null }; entry.promise = new Promise((resolve, reject) => { waiting.push({ key, fn, resolve, reject, entry }); }); entry.promise.catch(() => {}); // observed by callers; avoid unhandled warnings jobs.set(key, entry); pump(); return entry.promise; } /** Status for a key: 'queued' | 'running' | 'done' | 'error' | null. */ export function jobStatus(key) { const entry = jobs.get(key); return entry ? { status: entry.status, error: entry.error ?? null } : null; } /** Current queue depth (for /healthz). */ export function queueDepth() { return waiting.length + running; }