const DB_NAME = 'shine-ui-messages-v1'; const DB_VERSION = 3; const STORE_MESSAGES = 'messages_by_profile'; const LEGACY_STORE_MESSAGES = 'messages'; const ACTIVE_PROFILE_STORAGE_KEY = 'shine-ui-active-profile-v1'; const LEGACY_SESSION_STORAGE_KEY = 'shine-ui-current-session-v1'; function normalizeOwnerLogin(value) { return String(value || '').trim().toLowerCase(); } function storageKey(ownerLogin, messageKey) { const owner = normalizeOwnerLogin(ownerLogin); const key = String(messageKey || '').trim(); return owner && key ? `${owner}|${key}` : ''; } function migrationOwnerLogin() { try { const active = normalizeOwnerLogin(localStorage.getItem(ACTIVE_PROFILE_STORAGE_KEY)); if (active) return active; const legacy = JSON.parse(localStorage.getItem(LEGACY_SESSION_STORAGE_KEY) || '{}'); return normalizeOwnerLogin(legacy?.login); } catch { return ''; } } function ensureIndexes(store) { if (!store.indexNames.contains('by_chat')) store.createIndex('by_chat', 'chatId', { unique: false }); if (!store.indexNames.contains('by_ts')) store.createIndex('by_ts', 'ts', { unique: false }); if (!store.indexNames.contains('by_owner')) store.createIndex('by_owner', 'ownerLogin', { unique: false }); } function openDb() { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION); request.onupgradeneeded = () => { const db = request.result; let store; if (!db.objectStoreNames.contains(STORE_MESSAGES)) { store = db.createObjectStore(STORE_MESSAGES, { keyPath: 'storageKey' }); } else { store = request.transaction.objectStore(STORE_MESSAGES); } ensureIndexes(store); // Однократная миграция старого single-profile кэша в пространство текущего профиля. if (db.objectStoreNames.contains(LEGACY_STORE_MESSAGES)) { const owner = migrationOwnerLogin(); if (owner) { const legacy = request.transaction.objectStore(LEGACY_STORE_MESSAGES); const cursorReq = legacy.openCursor(); cursorReq.onsuccess = () => { const cursor = cursorReq.result; if (!cursor) return; const row = cursor.value || {}; const messageKey = String(row.messageKey || '').trim(); const rowOwner = normalizeOwnerLogin(row.ownerLogin) || owner; const key = storageKey(rowOwner, messageKey); if (key) store.put({ ...row, ownerLogin: rowOwner, storageKey: key }); cursor.continue(); }; } } }; request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error || new Error('IndexedDB open failed')); }); } async function withStore(mode, callback) { const db = await openDb(); try { return await new Promise((resolve, reject) => { const tx = db.transaction(STORE_MESSAGES, mode); const store = tx.objectStore(STORE_MESSAGES); const result = callback(store, tx); tx.oncomplete = () => resolve(result); tx.onerror = () => reject(tx.error || new Error('IndexedDB transaction failed')); tx.onabort = () => reject(tx.error || new Error('IndexedDB transaction aborted')); }); } finally { db.close(); } } export async function putStoredMessage(record) { if (!record || !record.messageKey) return; const ownerLogin = normalizeOwnerLogin(record.ownerLogin); const key = storageKey(ownerLogin, record.messageKey); if (!key) return; await withStore('readwrite', (store) => { store.put({ ...record, ownerLogin, storageKey: key }); }); } export async function deleteStoredMessage(messageKey, ownerLogin = '') { const key = storageKey(ownerLogin, messageKey); if (!key) return; await withStore('readwrite', (store) => { store.delete(key); }); } export async function listStoredMessages(ownerLogin = '') { const owner = normalizeOwnerLogin(ownerLogin); if (!owner) return []; return withStore('readonly', (store) => new Promise((resolve, reject) => { const req = store.index('by_owner').getAll(owner); req.onsuccess = () => resolve(Array.isArray(req.result) ? req.result : []); req.onerror = () => reject(req.error || new Error('IndexedDB getAll by owner failed')); })); } export async function clearStoredMessages(ownerLogin = '') { const owner = normalizeOwnerLogin(ownerLogin); if (!owner) { await new Promise((resolve, reject) => { const request = indexedDB.deleteDatabase(DB_NAME); request.onsuccess = () => resolve(); request.onerror = () => reject(request.error || new Error('IndexedDB delete failed')); request.onblocked = () => reject(new Error('IndexedDB delete blocked')); }); return; } await withStore('readwrite', (store) => new Promise((resolve, reject) => { const index = store.index('by_owner'); const req = index.openKeyCursor(IDBKeyRange.only(owner)); req.onsuccess = () => { const cursor = req.result; if (!cursor) { resolve(); return; } store.delete(cursor.primaryKey); cursor.continue(); }; req.onerror = () => reject(req.error || new Error('IndexedDB clear by owner failed')); })); }