128 lines
4.7 KiB
JavaScript
128 lines
4.7 KiB
JavaScript
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; }
|
||
}
|