/** * Offline message queue for Workshop panel. * * Persists undelivered job submissions to localStorage so they survive * page refreshes and are replayed when the server comes back online. */ const _QUEUE_KEY = "timmy_workshop_queue"; const _MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours — auto-expire old items export const STATUS = { QUEUED: "queued", DELIVERED: "delivered", FAILED: "failed", }; function _load() { try { const raw = localStorage.getItem(_QUEUE_KEY); return raw ? JSON.parse(raw) : []; } catch { return []; } } function _save(items) { try { localStorage.setItem(_QUEUE_KEY, JSON.stringify(items)); } catch { /* localStorage unavailable — degrade silently */ } } function _uid() { return `msg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; } /** LocalStorage-backed message queue for Workshop job submissions. */ export const messageQueue = { /** Add a payload. Returns the created item (with id and status). */ enqueue(payload) { const item = { id: _uid(), payload, queuedAt: new Date().toISOString(), status: STATUS.QUEUED, }; const items = _load(); items.push(item); _save(items); return item; }, /** Mark a message as delivered and remove it from storage. */ markDelivered(id) { _save(_load().filter((i) => i.id !== id)); }, /** Mark a message as permanently failed (kept for 24h for visibility). */ markFailed(id) { _save( _load().map((i) => i.id === id ? { ...i, status: STATUS.FAILED } : i ) ); }, /** All messages waiting to be delivered. */ getPending() { return _load().filter((i) => i.status === STATUS.QUEUED); }, /** Total queued (QUEUED status only) count. */ pendingCount() { return this.getPending().length; }, /** Drop expired failed items (> 24h old). */ prune() { const cutoff = Date.now() - _MAX_AGE_MS; _save( _load().filter( (i) => i.status === STATUS.QUEUED || (i.status === STATUS.FAILED && new Date(i.queuedAt).getTime() > cutoff) ) ); }, };