169 lines
5.5 KiB
JavaScript
169 lines
5.5 KiB
JavaScript
function createTodayWork({ storage, getLogin, limit = 5 }) {
|
|
const prefix = 'stackchain.today-work.v1.';
|
|
const planningPrefix = 'stackchain.today-planning.v1.';
|
|
|
|
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 ownerKey(keyPrefix) {
|
|
const login = String(getLogin?.() || '').trim().toLowerCase();
|
|
return login ? keyPrefix + encodeURIComponent(login) : '';
|
|
}
|
|
|
|
function storageKey() {
|
|
return ownerKey(prefix);
|
|
}
|
|
|
|
function planningKey() {
|
|
return ownerKey(planningPrefix);
|
|
}
|
|
|
|
function read() {
|
|
const key = storageKey();
|
|
if (!key || !storage) return [];
|
|
try {
|
|
const value = JSON.parse(storage.getItem(key) || '[]');
|
|
return Array.isArray(value) ? value.filter(id => typeof id === 'string' && id) : [];
|
|
} catch (_error) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function write(ids) {
|
|
const key = storageKey();
|
|
if (!key || !storage) return false;
|
|
try {
|
|
if (ids.length) storage.setItem(key, JSON.stringify(ids));
|
|
else storage.removeItem(key);
|
|
return true;
|
|
} catch (_error) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function planning() {
|
|
const key = planningKey();
|
|
if (!key || !storage) return { capacity_minutes: null, estimates: {} };
|
|
try {
|
|
const value = JSON.parse(storage.getItem(key) || 'null');
|
|
const capacity = Number.isInteger(value?.capacity_minutes) && value.capacity_minutes > 0
|
|
? value.capacity_minutes : null;
|
|
const ids = new Set(read());
|
|
const estimates = Object.fromEntries(Object.entries(value?.estimates || {}).filter(([id, minutes]) =>
|
|
ids.has(id) && Number.isInteger(minutes) && minutes > 0
|
|
));
|
|
return { capacity_minutes: capacity, estimates };
|
|
} catch (_error) {
|
|
return { capacity_minutes: null, estimates: {} };
|
|
}
|
|
}
|
|
|
|
function replacePlanning(value) {
|
|
const key = planningKey();
|
|
if (!key || !storage) return false;
|
|
const ids = new Set(read());
|
|
const capacity = Number.isInteger(value?.capacity_minutes) && value.capacity_minutes > 0
|
|
? value.capacity_minutes : null;
|
|
const estimates = Object.fromEntries(Object.entries(value?.estimates || {}).filter(([id, minutes]) =>
|
|
ids.has(id) && Number.isInteger(minutes) && minutes > 0
|
|
));
|
|
try {
|
|
storage.setItem(key, JSON.stringify({ capacity_minutes: capacity, estimates }));
|
|
return true;
|
|
} catch (_error) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function add(item) {
|
|
if (!storageKey()) return 'unavailable';
|
|
const id = identity(item);
|
|
if (!id) return 'unavailable';
|
|
const ids = read();
|
|
if (ids.includes(id)) return 'exists';
|
|
if (ids.length >= limit) return 'full';
|
|
ids.push(id);
|
|
return write(ids) ? 'added' : 'unavailable';
|
|
}
|
|
|
|
function replace(ids) {
|
|
const unique = [];
|
|
for (const id of ids || []) {
|
|
if (typeof id === 'string' && id && !unique.includes(id) && unique.length < limit) {
|
|
unique.push(id);
|
|
}
|
|
}
|
|
const saved = write(unique);
|
|
if (saved && planningKey()) replacePlanning(planning());
|
|
return saved;
|
|
}
|
|
|
|
function remove(item) {
|
|
const id = identity(item);
|
|
const ids = read();
|
|
const next = ids.filter(candidate => candidate !== id);
|
|
const saved = next.length !== ids.length && write(next);
|
|
if (saved) replacePlanning(planning());
|
|
return saved;
|
|
}
|
|
|
|
function move(item, direction) {
|
|
const ids = read();
|
|
const index = ids.indexOf(identity(item));
|
|
const target = direction === 'up' ? index - 1 : direction === 'down' ? index + 1 : -1;
|
|
if (index < 0 || target < 0 || target >= ids.length) return false;
|
|
[ids[index], ids[target]] = [ids[target], ids[index]];
|
|
return write(ids);
|
|
}
|
|
|
|
function reconcile(items, { pruneMissing = false, onPrune } = {}) {
|
|
const available = new Map((items || []).map(item => [identity(item), item]));
|
|
const ids = read();
|
|
const retained = pruneMissing ? ids.filter(id => available.has(id)) : ids;
|
|
const retired = ids.filter(id => !retained.includes(id));
|
|
if (retired.length) {
|
|
const reported = onPrune?.(retired);
|
|
if (reported !== false) {
|
|
write(retained);
|
|
replacePlanning(planning());
|
|
}
|
|
}
|
|
return retained.flatMap(id => available.has(id) ? [available.get(id)] : []);
|
|
}
|
|
|
|
function contains(item) {
|
|
return read().includes(identity(item));
|
|
}
|
|
|
|
function position(item) {
|
|
const ids = read();
|
|
const index = ids.indexOf(identity(item));
|
|
return {
|
|
can_up: index > 0,
|
|
can_down: index >= 0 && index < ids.length - 1,
|
|
};
|
|
}
|
|
|
|
function runway(items, currentIndex = 0) {
|
|
const plan = planning();
|
|
const minutes = (items || []).slice(Math.max(0, currentIndex)).map(item => plan.estimates[identity(item)] || null);
|
|
const total = values => values.some(value => value === null) ? null :
|
|
values.reduce((sum, value) => sum + value, 0);
|
|
return {
|
|
current_minutes: minutes[0] || null,
|
|
remaining_minutes: total(minutes),
|
|
future_minutes: total(minutes.slice(1)),
|
|
capacity_minutes: plan.capacity_minutes,
|
|
};
|
|
}
|
|
|
|
return { identity, read, replace, planning, replacePlanning, runway, add, remove, move, reconcile, contains, position, limit };
|
|
}
|
|
|
|
if (typeof module !== 'undefined' && module.exports) module.exports = createTodayWork;
|