(function (root, factory) { if (typeof module === 'object' && module.exports) module.exports = factory; else root.createMobileQueuePriority = factory; })(typeof self !== 'undefined' ? self : this, function createMobileQueuePriority(options = {}) { const DEFAULT_ORDER = [ 'attention', 'today', 'update', 'agenda', 'following', 'authored', 'filed', 'later', 'draft', ]; const storage = options.storage; const getLogin = options.getLogin || (() => ''); const fetchJson = options.fetchJson; const prefix = 'stackchain-mobile-queue-priority-v1:'; const labels = options.labels || {}; const documentRef = options.document || (typeof document !== 'undefined' ? document : null); const setTimer = options.setTimeout || setTimeout; const clearTimer = options.clearTimeout || clearTimeout; const debounceMs = Number.isFinite(options.debounceMs) ? Math.max(0, options.debounceMs) : 150; const retryBaseMs = Number.isFinite(options.retryBaseMs) ? Math.max(1, options.retryBaseMs) : 1000; const retryMaxMs = Number.isFinite(options.retryMaxMs) ? Math.max(retryBaseMs, options.retryMaxMs) : 30000; let memory = null; const syncFlights = new Map(); let debounceTimer = null; let retryTimer = null; let retryAccount = ''; let retryAttempts = 0; function key() { const login = String(getLogin() || '').trim().toLowerCase(); return login ? prefix + encodeURIComponent(login) : ''; } function valid(order) { return Array.isArray(order) && order.length === DEFAULT_ORDER.length && new Set(order).size === DEFAULT_ORDER.length && order.every(name => DEFAULT_ORDER.includes(name) && typeof name === 'string'); } function fresh() { return {revision:0, order:DEFAULT_ORDER.slice(), pending:false, status:'ready', remote:null}; } function read() { const accountKey = key(); if (!accountKey || !storage) return fresh(); if (memory?.key === accountKey) return memory.value; let value = fresh(); try { const saved = JSON.parse(storage.getItem(accountKey) || 'null'); if (valid(saved)) value = {revision:0, order:saved.slice(), pending:true, status:'pending', remote:null}; else if (saved && valid(saved.order) && Number.isInteger(saved.revision) && saved.revision >= 0) { value = { revision:saved.revision, order:saved.order.slice(), pending:Boolean(saved.pending), status:saved.status === 'conflict' ? 'conflict' : (saved.pending ? 'pending' : 'ready'), remote:saved.remote && valid(saved.remote.order) ? { revision:Number(saved.remote.revision) || 0, order:saved.remote.order.slice(), } : null, }; } } catch (_error) {} memory = {key:accountKey, value}; return value; } function persist(value) { const accountKey = key(); if (!accountKey || !storage) return false; memory = {key:accountKey, value}; try { storage.setItem(accountKey, JSON.stringify(value)); return true; } catch (_error) { return false; } } function snapshot() { const value = read(); return { revision:value.revision, order:value.order.slice(), pending:value.pending, status:value.status, remote:value.remote ? {revision:value.remote.revision, order:value.remote.order.slice()} : null, }; } function announce(value) { if (options.status) { options.status.textContent = ({pending:'Sync pending.', conflict:'Routine order changed on another device.', syncing:'Syncing routine order…', error:'Routine order could not sync.', ready:''})[value.status] || ''; } options.onState?.(snapshot()); } function getOrder() { return read().order.slice(); } function save(order) { if (!key() || !valid(order)) return false; const current = read(); const value = {revision:current.revision, order:order.slice(), pending:true, status:'pending', remote:null}; if (!persist(value)) return false; options.onChange?.(order.slice()); announce(value); scheduleSync(); return true; } function scheduleSync() { if (!fetchJson || !key()) return false; if (debounceTimer) clearTimer(debounceTimer); debounceTimer = setTimer(() => { debounceTimer = null; void sync(); }, debounceMs); return true; } function clearRetry() { if (retryTimer) clearTimer(retryTimer); retryTimer = null; retryAccount = ''; retryAttempts = 0; } function transient(error) { const status = Number(error?.status) || 0; return status === 0 || status === 408 || status === 425 || status === 429 || status >= 500; } function scheduleRetry(accountKey) { if (retryTimer && retryAccount !== accountKey) clearRetry(); if (retryTimer || key() !== accountKey) return false; const delay = Math.min(retryMaxMs, retryBaseMs * (2 ** retryAttempts)); retryAttempts += 1; retryAccount = accountKey; retryTimer = setTimer(() => { retryTimer = null; retryAccount = ''; const current = read(); if (key() !== accountKey || !current.pending || current.status === 'conflict') return; void sync(); }, delay); return true; } function move(name, delta) { const order = getOrder(); const index = order.indexOf(name); const next = index + (Number(delta) < 0 ? -1 : 1); if (index < 0 || next < 0 || next >= order.length) return order; [order[index], order[next]] = [order[next], order[index]]; save(order); return order.slice(); } function reset() { const order = DEFAULT_ORDER.slice(); if (fetchJson && key()) save(order); else { const accountKey = key(); if (accountKey && storage) { try { storage.removeItem(accountKey); } catch (_error) {} } memory = null; options.onChange?.(order.slice()); } return order; } function adopt(snapshotValue, accountKey = key()) { if (!accountKey || key() !== accountKey) return snapshot(); if (!snapshotValue || !Number.isInteger(snapshotValue.revision) || snapshotValue.revision < 0 || !valid(snapshotValue.order)) { throw new Error('Queue priority response is invalid.'); } const value = {revision:snapshotValue.revision, order:snapshotValue.order.slice(), pending:false, status:'ready', remote:null}; clearRetry(); persist(value); options.onChange?.(value.order.slice()); announce(value); render(); return snapshot(); } async function load() { const accountKey = key(); if (!fetchJson || !accountKey) return snapshot(); try { const remote = await fetchJson('api/v1/queue-priority'); if (key() !== accountKey) return snapshot(); const local = read(); if (local.pending) { local.remote = valid(remote?.order) ? {revision:remote.revision, order:remote.order.slice()} : null; persist(local); return sync(); } return adopt(remote, accountKey); } catch (_error) { if (key() !== accountKey) return snapshot(); const current = read(); current.status = current.pending ? 'pending' : 'error'; persist(current); announce(current); return snapshot(); } } async function drain(accountKey) { while (key() === accountKey) { const current = read(); if (!current.pending) return snapshot(); const sent = {revision:current.revision, order:current.order.slice()}; current.status = 'syncing'; persist(current); announce(current); try { const saved = await fetchJson('api/v1/queue-priority', { method:'PUT', headers:{'Content-Type':'application/json'}, body:JSON.stringify(sent), }); if (key() !== accountKey) return snapshot(); if (!saved || !Number.isInteger(saved.revision) || !valid(saved.order)) { throw new Error('Queue priority response is invalid.'); } const latest = read(); if (latest.order.some((name, index) => name !== sent.order[index])) { latest.revision = saved.revision; latest.pending = true; latest.status = 'pending'; latest.remote = null; persist(latest); announce(latest); continue; } return adopt(saved, accountKey); } catch (error) { if (key() !== accountKey) return snapshot(); const latest = read(); const remote = error?.status === 409 && error?.payload?.detail?.snapshot; if (remote && valid(remote.order) && Number.isInteger(remote.revision)) { clearRetry(); latest.status = 'conflict'; latest.pending = true; latest.remote = {revision:remote.revision, order:remote.order.slice()}; } else { latest.status = 'pending'; latest.pending = true; if (transient(error)) scheduleRetry(accountKey); } persist(latest); announce(latest); render(); return snapshot(); } } return snapshot(); } function sync() { if (debounceTimer) { clearTimer(debounceTimer); debounceTimer = null; } const accountKey = key(); const current = read(); if (!fetchJson || !accountKey || !current.pending) return Promise.resolve(snapshot()); if (syncFlights.has(accountKey)) return syncFlights.get(accountKey); const flight = drain(accountKey).finally(() => { if (syncFlights.get(accountKey) === flight) syncFlights.delete(accountKey); }); syncFlights.set(accountKey, flight); return flight; } async function useLocal() { const current = read(); if (!current.remote) return snapshot(); current.revision = current.remote.revision; current.remote = null; current.pending = true; current.status = 'pending'; persist(current); return sync(); } function useRemote() { const current = read(); return current.remote ? adopt(current.remote) : snapshot(); } function displayName(name) { return labels[name] || name.charAt(0).toUpperCase() + name.slice(1); } function render() { if (!options.list || !documentRef) return getOrder(); const order = getOrder(); const signedIn = Boolean(key()); const rows = order.map((name, index) => { const row = documentRef.createElement('div'); row.setAttribute('data-queue-priority', name); row.setAttribute('class', 'mobile-queue-priority-row'); const label = documentRef.createElement('span'); label.textContent = displayName(name); const controls = documentRef.createElement('span'); controls.setAttribute('class', 'mobile-queue-priority-controls'); const earlier = documentRef.createElement('button'); earlier.textContent = 'Earlier'; earlier.setAttribute('type', 'button'); earlier.setAttribute('aria-label', 'Move ' + displayName(name) + ' earlier'); earlier.disabled = !signedIn || index === 0; earlier.addEventListener('click', () => { move(name, -1); render(); }); const later = documentRef.createElement('button'); later.textContent = 'Later'; later.setAttribute('type', 'button'); later.setAttribute('aria-label', 'Move ' + displayName(name) + ' later'); later.disabled = !signedIn || index === order.length - 1; later.addEventListener('click', () => { move(name, 1); render(); }); controls.append(earlier, later); row.append(label, controls); return row; }); options.list.replaceChildren(...rows); if (options.resetButton) options.resetButton.disabled = !signedIn; if (options.conflict) options.conflict.hidden = read().status !== 'conflict'; return order; } function start() { options.resetButton?.addEventListener('click', () => { reset(); render(); }); options.keepLocalButton?.addEventListener('click', () => { void useLocal(); }); options.useRemoteButton?.addEventListener('click', () => { useRemote(); }); return render(); } function startLifecycle(lifecycle = {}) { const windowObject = lifecycle.window; const lifecycleDocument = lifecycle.document; const reconcile = () => { if (!key()) return Promise.resolve(snapshot()); return read().pending ? sync() : load(); }; windowObject?.addEventListener?.('online', () => { void reconcile(); }); lifecycleDocument?.addEventListener?.('visibilitychange', () => { if (!lifecycleDocument.hidden) void reconcile(); }); return reconcile; } return {getOrder, move, reset, render, start, startLifecycle, load, sync, scheduleSync, useLocal, useRemote, state:snapshot, defaultOrder:() => DEFAULT_ORDER.slice()}; });