function createTodaySync({ storage, getLogin, fetchJson, onRemoteIds, onStatus, createOperationId, createChannel, setTimer = globalThis.setTimeout, clearTimer = globalThis.clearTimeout, retryBaseMs = 1000, retryMaxMs = 30000 }) { const prefix = 'stackchain.today-sync.v1.'; const migrationPrefix = 'stackchain.today-sync-migrated.v1.'; const snapshotPrefix = 'stackchain.today-sync-snapshot.v1.'; let flushing = null; let channel = null; let channelKey = ''; let retryTimer = null; let retryAttempt = 0; function cancelRetry() { if (retryTimer !== null) clearTimer?.(retryTimer); retryTimer = null; } function scheduleRetry(error, ownerKey) { if (retryTimer !== null || !pending().length || !ownerKey) return; const advised = Number(error?.retryAfter); const delayMs = Number.isFinite(advised) && advised >= 0 ? advised * 1000 : Math.min(retryMaxMs, retryBaseMs * (2 ** retryAttempt)); retryAttempt += 1; onStatus?.('retrying', { delayMs }); retryTimer = setTimer?.(async () => { retryTimer = null; if (key() !== ownerKey) return false; return flush(); }, delayMs); retryTimer?.unref?.(); } function key() { const login = String(getLogin?.() || '').trim().toLowerCase(); return login ? prefix + encodeURIComponent(login) : ''; } function snapshotKey() { const storageKey = key(); return storageKey ? snapshotPrefix + storageKey.slice(prefix.length) : ''; } function savedRevision() { try { const snapshot = JSON.parse(storage?.getItem(snapshotKey()) || 'null'); return Number.isInteger(snapshot?.revision) ? snapshot.revision : -1; } catch (_error) { return -1; } } function adopt(plan, broadcast = true) { if (!Number.isInteger(plan?.revision) || !Array.isArray(plan?.ids)) return false; if (plan.revision < savedRevision()) return false; try { storage?.setItem(snapshotKey(), JSON.stringify({ revision: plan.revision, ids: plan.ids })); } catch (_error) { // A storage quota failure must not prevent the current tab from using server truth. } onRemoteIds?.(plan.ids); if (broadcast) channel?.postMessage({ revision: plan.revision, ids: plan.ids }); return true; } function ensureChannel() { const storageKey = key(); if (!storageKey || channelKey === storageKey) return; channel?.close?.(); const factory = createChannel || (globalThis.window?.BroadcastChannel ? name => new globalThis.window.BroadcastChannel(name) : null); channelKey = storageKey; channel = factory?.('stackchain-today-' + storageKey.slice(prefix.length)) || null; channel?.addEventListener?.('message', event => { if (key() === storageKey) adopt(event.data, false); }); } function pending() { const storageKey = key(); if (!storageKey || !storage) return []; try { const value = JSON.parse(storage.getItem(storageKey) || '[]'); return Array.isArray(value) ? value.filter(operation => operation && typeof operation.operation_id === 'string' && ['add', 'remove', 'move'].includes(operation.action) && typeof operation.item_id === 'string' ) : []; } catch (_error) { return []; } } function save(operations) { const storageKey = key(); if (!storageKey || !storage) return false; try { if (operations.length) storage.setItem(storageKey, JSON.stringify(operations)); else storage.removeItem(storageKey); return true; } catch (_error) { return false; } } function operationId() { if (createOperationId) return createOperationId(); if (globalThis.crypto?.randomUUID) return globalThis.crypto.randomUUID(); return Date.now().toString(36) + '-' + Math.random().toString(36).slice(2); } function enqueue(action, itemId, direction = null) { const operations = pending(); if (action === 'remove' && operations.some(operation => operation.action === 'remove' && operation.item_id === itemId )) { onStatus?.('pending'); return true; } operations.push({ operation_id: operationId(), action, item_id: itemId, direction }); const saved = save(operations); onStatus?.(saved ? 'pending' : 'error'); return saved; } function migrate(ids) { const storageKey = key(); if (!storageKey || !storage) return false; const marker = migrationPrefix + storageKey.slice(prefix.length); try { if (storage.getItem(marker)) return false; for (const id of ids || []) enqueue('add', id); storage.setItem(marker, '1'); return true; } catch (_error) { return false; } } async function run() { const ownerKey = key(); if (!ownerKey) return false; ensureChannel(); try { let plan = await fetchJson('api/v1/today'); let operations = pending(); let hadConflict = false; while (operations.length) { if (key() !== ownerKey) return false; const operation = operations[0]; try { plan = await fetchJson('api/v1/today', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(operation), }); } catch (error) { if (error?.status !== 409) throw error; const rejected = pending(); if (!save(rejected.filter(candidate => candidate.operation_id !== operation.operation_id))) { throw new Error('Could not persist rejected Today operation'); } hadConflict = true; operations = pending(); continue; } const remaining = pending(); const delivered = remaining.findIndex(candidate => candidate.operation_id === operation.operation_id); if (delivered >= 0 && !save(remaining.filter((_, index) => index !== delivered))) { throw new Error('Could not persist Today delivery receipt'); } operations = pending(); } adopt(plan); onStatus?.(pending().length ? 'pending' : hadConflict ? 'full' : 'saved'); retryAttempt = 0; cancelRetry(); return !hadConflict; } catch (error) { if (pending().length) scheduleRetry(error, ownerKey); else onStatus?.('error'); return false; } } function flush() { if (!flushing) flushing = run().finally(() => { flushing = null; }); return flushing; } function startLifecycle({ window: windowObject, document: documentObject }) { windowObject?.addEventListener?.('online', flush); documentObject?.addEventListener?.('visibilitychange', () => documentObject.hidden ? false : flush() ); } return { enqueue, migrate, flush, pending, startLifecycle }; } if (typeof module !== 'undefined' && module.exports) module.exports = createTodaySync;