function createLaterWork({ storage, getLogin, now = () => new Date(), setTimer = setTimeout, clearTimer = clearTimeout, onWake = () => {}, onChange = () => {}, onExpire = () => {} }) { const prefix = 'stackchain.later-work.v1.'; let timer = null; function identity(item) { if (!item) return ''; const kind = item.is_review ? 'review' : (item.kind || 'work'); const number = Number.isInteger(item.number) ? item.number : ''; const notification = Number.isInteger(item.notification_id) ? item.notification_id : ''; return [kind, item.repository || '', number, notification].join(':'); } function storageKey() { const login = String(getLogin?.() || '').trim().toLowerCase(); return login ? prefix + encodeURIComponent(login) : ''; } function read() { const key = storageKey(); if (!key || !storage) return {}; try { const value = JSON.parse(storage.getItem(key) || '{}'); return value && typeof value === 'object' && !Array.isArray(value) ? value : {}; } catch (_error) { return {}; } } function write(records) { const key = storageKey(); if (!key || !storage) return false; try { if (Object.keys(records).length) storage.setItem(key, JSON.stringify(records)); else storage.removeItem(key); return true; } catch (_error) { return false; } } function defer(item, until) { const key = storageKey(); const id = identity(item); const wake = new Date(until); if (!key) return 'unavailable'; if (!id || Number.isNaN(wake.getTime()) || wake <= now()) return 'invalid'; const records = read(); const wakeAt = wake.toISOString(); records[id] = wakeAt; if (!write(records)) return 'unavailable'; onChange('defer', id, wakeAt); return 'deferred'; } function deferMany(items, until) { const key = storageKey(); const wake = new Date(until); const entries = (items || []).map(item => [identity(item), item]); if (!key) return 'unavailable'; if (!entries.length || entries.some(([id]) => !id) || Number.isNaN(wake.getTime()) || wake <= now()) { return 'invalid'; } const records = read(); const wakeAt = wake.toISOString(); entries.forEach(([id]) => { records[id] = wakeAt; }); if (!write(records)) return 'unavailable'; entries.forEach(([id]) => onChange('defer', id, wakeAt)); return 'deferred'; } function presetUntil(preset) { const current = new Date(now()); if (preset === 'today') return new Date(current.getTime() + 4 * 60 * 60 * 1000); if (preset === 'tomorrow') { const wake = new Date(current); wake.setDate(wake.getDate() + 1); wake.setHours(9, 0, 0, 0); return wake; } return new Date(NaN); } function restore(item) { const id = identity(item); const records = read(); if (!id || !Object.prototype.hasOwnProperty.call(records, id)) return false; delete records[id]; if (!write(records)) return false; onChange('restore', id, null); return true; } function adopt(records) { if (!records || typeof records !== 'object' || Array.isArray(records)) return false; const normalized = {}; Object.entries(records).forEach(([id, wake]) => { const wakeTime = new Date(wake).getTime(); if (id && Number.isFinite(wakeTime)) normalized[id] = new Date(wakeTime).toISOString(); }); return write(normalized); } function schedule(wakeTimes, current) { if (timer !== null) clearTimer(timer); timer = null; if (!wakeTimes.length) return; const delay = Math.max(0, Math.min(...wakeTimes) - current); timer = setTimer(() => { timer = null; onWake(); }, Math.min(delay, 2147483647)); } function partition(items, { pruneMissing = true } = {}) { const current = now().getTime(); const records = read(); const available = new Map((items || []).map(item => [identity(item), item])); const retained = {}; const wakeTimes = []; const expired = []; Object.entries(records).forEach(([id, wake]) => { const wakeTime = new Date(wake).getTime(); if (!Number.isFinite(wakeTime) || wakeTime <= current) { if (Number.isFinite(wakeTime)) expired.push(id); return; } if (pruneMissing && !available.has(id)) return; retained[id] = new Date(wakeTime).toISOString(); wakeTimes.push(wakeTime); }); if (JSON.stringify(retained) !== JSON.stringify(records) && write(retained) && expired.length) { onExpire(expired); } schedule(wakeTimes, current); const deferredIds = new Set(Object.keys(retained)); const active = (items || []).filter(item => !deferredIds.has(identity(item))); const later = (items || []).flatMap(item => { const wake = retained[identity(item)]; return wake ? [{ ...item, deferred_until: wake }] : []; }); return { active, later }; } return { identity, read, adopt, defer, deferMany, restore, presetUntil, partition }; } if (typeof module !== 'undefined' && module.exports) module.exports = createLaterWork;