feat: add offline tender mutation queue replay

This commit is contained in:
2026-09-14 17:42:31 +03:00
parent 9b3809e21b
commit b578c2d3ba
4 changed files with 179 additions and 3 deletions
+93
View File
@@ -0,0 +1,93 @@
const PREFIX = 'ihaleist_offline_mutations:';
const LIMIT = 100;
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.slice(-LIMIT)));
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;
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,
};
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;
}
/** Replay in order; a failed item remains queued and stops the batch. */
export async function replayOfflineMutations(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 {
await send(item);
removeOfflineMutation(owner, item.id);
result.sent += 1;
result.pending -= 1;
} catch (error) {
result.error = error;
break;
}
}
return result;
}
export function clearOfflineMutations(owner) {
if (typeof localStorage === 'undefined') return false;
try { localStorage.removeItem(ownerKey(owner)); return true; } catch { return false; }
}