const PREFIX = 'ihaleist_offline_mutations:'; const LIMIT = 100; const activeReplays = new Map(); const ownerKey = owner => `${PREFIX}${encodeURIComponent(String(owner || '').trim().toLowerCase())}`; function readStorage(owner) { if (typeof localStorage === 'undefined' || !String(owner || '').trim()) return []; try { const parsed = JSON.parse(localStorage.getItem(ownerKey(owner)) || '[]'); return Array.isArray(parsed) ? parsed.filter(item => item && typeof item === 'object') : []; } catch { return []; } } function writeStorage(owner, rows) { if (typeof localStorage === 'undefined' || !String(owner || '').trim()) return false; try { localStorage.setItem(ownerKey(owner), JSON.stringify(rows)); return true; } catch { return false; } } export function offlineMutationCount(owner) { return readStorage(owner).length; } export function listOfflineMutations(owner) { return readStorage(owner).map(item => ({ ...item })); } /** * Adds a tender-state write to the account-scoped durable queue. Consecutive * changes for the same tender are merged so reconnecting never replays stale * intermediate drafts. Tokens and document bytes are deliberately excluded. */ export function enqueueTenderMutation(owner, { key, patch, snapshot, version }) { if (!String(owner || '').trim() || !String(key || '').trim() || !patch || typeof patch !== 'object' || Array.isArray(patch)) { throw new Error('Çevrimdışı kayıt için ihale ve değişiklik bilgisi gerekli.'); } const rows = readStorage(owner); const index = rows.findIndex(item => item.kind === 'tender-state' && item.key === key); const previous = index >= 0 ? rows[index] : null; if (!previous && rows.length >= LIMIT) { throw new Error('Çevrimdışı kayıt kuyruğu dolu. Bağlantı kurulduktan sonra yeniden deneyin.'); } const next = { id: previous?.id || `offline-${crypto.randomUUID()}`, kind: 'tender-state', key, patch: { ...(previous?.patch || {}), ...patch }, snapshot: snapshot && typeof snapshot === 'object' ? snapshot : {}, version: Number.isInteger(version) && version >= 0 ? version : 0, createdAt: previous?.createdAt || new Date().toISOString(), updatedAt: new Date().toISOString(), attempts: previous?.attempts || 0, revision: (Number.isInteger(previous?.revision) ? previous.revision : 0) + 1, }; if (index >= 0) rows[index] = next; else rows.push(next); if (!writeStorage(owner, rows)) throw new Error('Çevrimdışı kayıt kuyruğu yazılamadı.'); return next; } export function removeOfflineMutation(owner, id) { const rows = readStorage(owner); return writeStorage(owner, rows.filter(item => item.id !== id)); } export function markOfflineMutationAttempt(owner, id) { const rows = readStorage(owner); const next = rows.map(item => item.id === id ? { ...item, attempts: (item.attempts || 0) + 1, updatedAt: new Date().toISOString() } : item); writeStorage(owner, next); return next.find(item => item.id === id) || null; } function acknowledgeOfflineMutation(owner, item, record) { const rows = readStorage(owner); const index = rows.findIndex(row => row.id === item.id); if (index < 0) return true; const current = rows[index]; if ((current.revision || 0) === (item.revision || 0)) { rows.splice(index, 1); } else { const nextVersion = Number(record?.version); rows[index] = { ...current, version: Number.isInteger(nextVersion) && nextVersion >= 0 ? nextVersion : current.version, }; } return writeStorage(owner, rows); } async function runReplay(owner, send) { const rows = listOfflineMutations(owner); const result = { sent: 0, pending: rows.length, error: null }; for (const item of rows) { markOfflineMutationAttempt(owner, item.id); try { const record = await send(item); if (!acknowledgeOfflineMutation(owner, item, record)) { throw new Error('Çevrimdışı kayıt kuyruğu güncellenemedi.'); } result.sent += 1; } catch (error) { result.error = error; break; } } result.pending = offlineMutationCount(owner); return result; } /** Replay in order; concurrent reconnect signals share one replay operation. */ export function replayOfflineMutations(owner, send) { const replayKey = ownerKey(owner); const active = activeReplays.get(replayKey); if (active) return active; const replay = runReplay(owner, send).finally(() => { if (activeReplays.get(replayKey) === replay) activeReplays.delete(replayKey); }); activeReplays.set(replayKey, replay); return replay; } export function clearOfflineMutations(owner) { if (typeof localStorage === 'undefined') return false; try { localStorage.removeItem(ownerKey(owner)); return true; } catch { return false; } }