134 lines
4.4 KiB
JavaScript
134 lines
4.4 KiB
JavaScript
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 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, restore, presetUntil, partition };
|
|
}
|
|
|
|
if (typeof module !== 'undefined' && module.exports) module.exports = createLaterWork;
|