126 lines
5.2 KiB
JavaScript
126 lines
5.2 KiB
JavaScript
function createNotificationReadOutbox({
|
|
storage, fetchJson, backgroundSync, coordinator, getOwnerLogin = () => '', now = () => Date.now(), maxItems = 100,
|
|
}) {
|
|
const storageKey = 'stackchain.notification-read-outbox.v1';
|
|
|
|
function read() {
|
|
try {
|
|
const record = JSON.parse(storage?.getItem(storageKey) || 'null');
|
|
if (record?.version !== 1 || !Array.isArray(record.items)) return [];
|
|
return record.items.filter(item => item?.kind === 'notification-read' &&
|
|
Number.isInteger(item.notificationId) && item.notificationId > 0 && item.ownerLogin);
|
|
} catch (_error) { return []; }
|
|
}
|
|
|
|
function write(items, mirror = true) {
|
|
storage?.setItem(storageKey, JSON.stringify({ version: 1, items }));
|
|
coordinator?.notify('notification-read');
|
|
if (mirror && backgroundSync?.reconcile) {
|
|
Promise.resolve(backgroundSync.reconcile(items, 'notification-read'))
|
|
.then(() => items.length ? backgroundSync.requestSync?.() : undefined)
|
|
.catch(() => { /* Foreground reconnect remains available. */ });
|
|
}
|
|
}
|
|
|
|
function itemId(ownerLogin, notificationId) {
|
|
return 'notification-read:' + ownerLogin + ':' + notificationId;
|
|
}
|
|
|
|
async function enqueueDurably(notificationId) {
|
|
notificationId = Number(notificationId);
|
|
const ownerLogin = String(getOwnerLogin() || '').trim();
|
|
if (!ownerLogin) throw new Error('Confirm your Gitea account before queueing this update.');
|
|
if (!Number.isInteger(notificationId) || notificationId <= 0) throw new Error('Choose a valid update.');
|
|
const items = read();
|
|
const id = itemId(ownerLogin, notificationId);
|
|
let item = items.find(candidate => candidate.id === id);
|
|
if (!item) {
|
|
if (items.length >= maxItems) throw new Error('Update acknowledgement queue is full. Reconnect before clearing more updates.');
|
|
item = {
|
|
id, kind: 'notification-read', notificationId, ownerLogin,
|
|
status: 'queued', queuedAt: Number(now()),
|
|
};
|
|
items.push(item);
|
|
write(items, false);
|
|
}
|
|
if (!backgroundSync?.reconcile || !backgroundSync?.requestSync) {
|
|
return { item: { ...item }, background: false, durability: 'foreground-only' };
|
|
}
|
|
try {
|
|
await backgroundSync.reconcile(read(), 'notification-read');
|
|
await backgroundSync.requestSync();
|
|
return { item: { ...item }, background: true, durability: 'background' };
|
|
} catch (error) {
|
|
return { item: { ...item }, background: false, durability: 'foreground-only', error };
|
|
}
|
|
}
|
|
|
|
function suppress(items, login = getOwnerLogin()) {
|
|
const pending = new Set(read().filter(item => item.ownerLogin === String(login || '').trim())
|
|
.map(item => item.notificationId));
|
|
return (items || []).filter(item => !pending.has(Number(item?.notification_id ?? item?.id)));
|
|
}
|
|
|
|
async function send(item, currentLogin) {
|
|
if (!currentLogin || item.ownerLogin !== currentLogin) return { blocked: true };
|
|
if (backgroundSync?.send) return backgroundSync.send(item, currentLogin);
|
|
await fetchJson('api/v1/notifications/' + encodeURIComponent(item.notificationId) + '/read', {
|
|
method: 'PATCH', headers: { Accept: 'application/json' },
|
|
});
|
|
return { confirmed: true };
|
|
}
|
|
|
|
async function flush(currentLogin) {
|
|
currentLogin = String(currentLogin || '').trim();
|
|
const confirmed = [];
|
|
let items = read();
|
|
for (const item of items) {
|
|
if (item.ownerLogin !== currentLogin || item.status === 'attention') continue;
|
|
try {
|
|
const result = await send(item, currentLogin);
|
|
if (result?.blocked || result?.busy) continue;
|
|
if (result?.attention) {
|
|
items = items.map(candidate => candidate.id === item.id ? {
|
|
...candidate, status: 'attention', error: String(result.error?.message || 'Update needs attention').slice(0, 240),
|
|
} : candidate);
|
|
write(items);
|
|
continue;
|
|
}
|
|
confirmed.push(item.notificationId);
|
|
items = items.filter(candidate => candidate.id !== item.id);
|
|
write(items);
|
|
} catch (error) {
|
|
const status = Number(error?.status || 0);
|
|
if (status >= 400 && status < 500) {
|
|
items = items.map(candidate => candidate.id === item.id ? {
|
|
...candidate, status: 'attention', error: String(error?.message || 'Update needs attention').slice(0, 240),
|
|
} : candidate);
|
|
write(items);
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return { confirmed, remaining: read() };
|
|
}
|
|
|
|
function reconcileBackground(records) {
|
|
const states = new Map((records || []).filter(item => item?.kind === 'notification-read')
|
|
.map(item => [item.id, item]));
|
|
const items = read().flatMap(item => {
|
|
const state = states.get(item.id);
|
|
if (state?.status === 'sent') return [];
|
|
if (state?.status === 'attention') return [{
|
|
...item, status: 'attention', error: String(state.error || 'Update needs attention').slice(0, 240),
|
|
}];
|
|
return [item];
|
|
});
|
|
write(items);
|
|
return items;
|
|
}
|
|
|
|
return { enqueueDurably, flush, suppress, reconcileBackground, list: () => read().map(item => ({ ...item })) };
|
|
}
|
|
|
|
if (typeof module !== 'undefined' && module.exports) module.exports = createNotificationReadOutbox;
|