function createTodaySync({ storage, getLogin, fetchJson, onRemoteIds, onStatus, createOperationId }) { const prefix = 'stackchain.today-sync.v1.'; const migrationPrefix = 'stackchain.today-sync-migrated.v1.'; let flushing = null; function key() { const login = String(getLogin?.() || '').trim().toLowerCase(); return login ? prefix + encodeURIComponent(login) : ''; } 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(); 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() { if (!key()) return false; try { let plan = await fetchJson('api/v1/today'); const operations = pending(); for (const operation of operations) { 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(); save(rejected.filter(candidate => candidate.operation_id !== operation.operation_id)); onRemoteIds?.(Array.isArray(plan.ids) ? plan.ids : []); onStatus?.('full'); return false; } const remaining = pending(); if (remaining[0]?.operation_id === operation.operation_id) save(remaining.slice(1)); } onRemoteIds?.(Array.isArray(plan.ids) ? plan.ids : []); onStatus?.(pending().length ? 'pending' : 'saved'); return true; } catch (_error) { onStatus?.(pending().length ? 'pending' : 'error'); return false; } } function flush() { if (!flushing) flushing = run().finally(() => { flushing = null; }); return flushing; } return { enqueue, migrate, flush, pending }; } if (typeof module !== 'undefined' && module.exports) module.exports = createTodaySync;