(function (root, factory) { if (typeof module === 'object' && module.exports) module.exports = factory; else root.createMobileRecentWork = factory; })(typeof globalThis !== 'undefined' ? globalThis : this, function createMobileRecentWork(options) { 'use strict'; const storage = options.storage; const getLogin = options.getLogin; const fetchJson = options.fetchJson; const limit = Math.max(1, Number(options.limit) || 5); const prefix = 'stackchain.mobile-recent-work.v1.'; const repositoryPattern = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/; const kinds = new Set(['issue', 'filed', 'pull', 'review', 'update']); const setTimer = options.setTimeout || setTimeout; const clearTimer = options.clearTimeout || clearTimeout; const debounceMs = Number.isFinite(options.debounceMs) ? Math.max(0, options.debounceMs) : 150; let syncFlight = null; let syncAccount = ''; let debounceTimer = null; function login() { return String(getLogin?.() || '').trim().toLowerCase(); } function key() { const owner = login(); return owner ? prefix + owner : ''; } function normalize(item) { const kind = String(item?.kind || ''); const number = Number(item?.number ?? item?.notification_id); if (!kinds.has(kind) || !Number.isSafeInteger(number) || number < 1) return null; let route = ''; let repository = ''; if (kind === 'update') { route = '#/my-work/update/' + number; } else { repository = String(item?.repository || ''); if (!repositoryPattern.test(repository)) return null; route = '#/my-work/' + kind + '/' + repository + '/' + number; } const title = String(item?.title || item?.subject?.title || '').trim().slice(0, 180); if (!title) return null; return {kind, ...(repository ? {repository} : {}), number, title, route}; } function normalizeList(value) { if (!Array.isArray(value)) return []; const unique = []; for (const candidate of value) { const item = normalize(candidate); if (item && !unique.some(existing => existing.route === item.route)) unique.push(item); if (unique.length === limit) break; } return unique; } function read() { const storageKey = key(); if (!storageKey) return {items:[], pending:[]}; try { const parsed = JSON.parse(storage.getItem(storageKey) || 'null'); if (Array.isArray(parsed)) return {items:normalizeList(parsed), pending:[]}; return { items:normalizeList(parsed?.items), pending:normalizeList(parsed?.pending), }; } catch (_) { return {items:[], pending:[]}; } } function persist(value, accountKey = key()) { if (!accountKey || accountKey !== key()) return false; try { storage.setItem(accountKey, JSON.stringify({ items:normalizeList(value.items), pending:normalizeList(value.pending), })); return true; } catch (_) { return false; } } function announce(value = read(), status = null) { if (!options.status) return; options.status.textContent = status || (value.pending.length ? 'Sync pending.' : ''); } function items() { return read().items; } function state() { const value = read(); return {pending:value.pending.length > 0, pendingCount:value.pending.length}; } function scheduleSync() { if (!fetchJson || !key()) return false; if (debounceTimer) clearTimer(debounceTimer); debounceTimer = setTimer(() => { debounceTimer = null; void sync(); }, debounceMs); return true; } function record(item) { const accountKey = key(); const normalized = normalize(item); if (!accountKey || !normalized) return false; const current = read(); current.items = [normalized, ...current.items.filter(existing => existing.route !== normalized.route)].slice(0, limit); current.pending = [normalized, ...current.pending.filter(existing => existing.route !== normalized.route)].slice(0, limit); if (!persist(current, accountKey)) return false; announce(current); render(); scheduleSync(); return true; } function adopt(snapshot, accountKey, pending = []) { if (key() !== accountKey || !snapshot || !Array.isArray(snapshot.items)) return false; const remote = normalizeList(snapshot.items); const unsent = normalizeList(pending); const value = { items:normalizeList([...unsent, ...remote]), pending:unsent, }; persist(value, accountKey); announce(value); render(); return true; } async function drain(accountKey) { while (key() === accountKey) { const current = read(); if (!current.pending.length) return current; const sending = current.pending[current.pending.length - 1]; announce(current, 'Syncing recent work…'); try { const snapshot = await fetchJson('api/v1/recent-work', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(sending), }); if (key() !== accountKey) return read(); const latest = read(); const pending = latest.pending.filter(item => item.route !== sending.route); if (!adopt(snapshot, accountKey, pending)) throw new Error('Recent work response is invalid.'); } catch (_error) { if (key() === accountKey) announce(read()); return read(); } } return read(); } function sync() { if (debounceTimer) { clearTimer(debounceTimer); debounceTimer = null; } const accountKey = key(); if (!fetchJson || !accountKey || !read().pending.length) return Promise.resolve(read()); if (syncFlight && syncAccount === accountKey) return syncFlight; syncAccount = accountKey; syncFlight = drain(accountKey).finally(() => { if (syncAccount === accountKey) { syncFlight = null; syncAccount = ''; } }); return syncFlight; } async function load() { const accountKey = key(); if (!fetchJson || !accountKey) return read(); try { const snapshot = await fetchJson('api/v1/recent-work'); if (key() !== accountKey) return read(); const current = read(); adopt(snapshot, accountKey, current.pending); return current.pending.length ? sync() : read(); } catch (_error) { if (key() === accountKey) announce(read(), read().pending.length ? null : 'Recent work could not sync.'); return read(); } } function startLifecycle(lifecycle = {}) { const reconcile = () => read().pending.length ? sync() : load(); lifecycle.window?.addEventListener?.('online', () => { void reconcile(); }); lifecycle.document?.addEventListener?.('visibilitychange', () => { if (!lifecycle.document.hidden) void reconcile(); }); return reconcile; } function render() { const recent = items(); const list = options.list; const section = options.section; if (!list || !section || !options.document) return recent.length; const rows = recent.map(item => { const detail = item.kind === 'update' ? 'Update · #' + item.number : item.kind.charAt(0).toUpperCase() + item.kind.slice(1) + ' · ' + item.repository + ' #' + item.number; const button = options.document.createElement('button'); const copy = options.document.createElement('span'); const primary = options.document.createElement('strong'); const secondary = options.document.createElement('small'); primary.textContent = item.title; secondary.textContent = detail; copy.appendChild(primary); copy.appendChild(secondary); button.appendChild(copy); button.setAttribute('type', 'button'); button.setAttribute('data-recent-work-route', item.route); button.setAttribute('aria-label', 'Open ' + item.title + ', ' + detail.toLowerCase().replace(' · ', ' ')); button.addEventListener('click', () => options.openRoute?.(item.route)); return button; }); list.replaceChildren(...rows); section.hidden = rows.length === 0; return rows.length; } return {items, record, render, load, sync, startLifecycle, state}; });