import { isEmpty } from './commons'; /** * */ /** * 数据库版本 * ! 每次涉及indexedDB的更新都要往上+1 * @type {number} */ const INDEXED_DB_VERSION = 6; export const logWebsocket = (message, direction) => { var open = indexedDB.open('LogWebsocketData', INDEXED_DB_VERSION) open.onupgradeneeded = function () { var db = open.result // 数据库是否存在 if (!db.objectStoreNames.contains('LogStore')) { var store = db.createObjectStore('LogStore', { keyPath: 'id', autoIncrement: true }) store.createIndex('timestamp', 'timestamp', { unique: false }) } else { const logStore = open.transaction.objectStore('LogStore') if (!logStore.indexNames.contains('timestamp')) { logStore.createIndex('timestamp', 'timestamp', { unique: false }) } } } open.onsuccess = function () { var db = open.result var tx = db.transaction('LogStore', 'readwrite') var store = tx.objectStore('LogStore') store.put({ direction, message, _date: new Date().toLocaleString(), timestamp: Date.now() }) tx.oncomplete = function () { db.close() } } }; export const readWebsocketLog = (limit = 20) => { return new Promise((resolve, reject) => { let openRequest = indexedDB.open('LogWebsocketData') openRequest.onupgradeneeded = function () { var db = openRequest.result // 数据库是否存在 if (!db.objectStoreNames.contains('LogStore')) { var store = db.createObjectStore('LogStore', { keyPath: 'id', autoIncrement: true }) store.createIndex('timestamp', 'timestamp', { unique: false }) } else { const logStore = openRequest.transaction.objectStore('LogStore') if (!logStore.indexNames.contains('timestamp')) { logStore.createIndex('timestamp', 'timestamp', { unique: false }) } } } openRequest.onerror = function (e) { reject('Error opening database.') } openRequest.onsuccess = function (e) { let db = e.target.result // 数据库是否存在 if (!db.objectStoreNames.contains('LogStore')) { resolve('Database does not exist.') return } let transaction = db.transaction('LogStore', 'readonly') let store = transaction.objectStore('LogStore') const request = store.openCursor(null, 'prev'); // 从后往前 const results = []; let count = 0; request.onerror = function (e) { reject('Error getting records.') } request.onsuccess = function (e) { const cursor = e.target.result if (cursor) { if (count < limit) { results.unshift(cursor.value) count++ cursor.continue() } else { console.log(JSON.stringify(results)) resolve(results) } } else { console.log(JSON.stringify(results)) resolve(results) } } } }) }; /** * @deprecated */ export const clearWebsocketLog = () => { let openRequest = indexedDB.open('LogWebsocketData') openRequest.onerror = function (e) {} openRequest.onsuccess = function (e) { let db = e.target.result if (!db.objectStoreNames.contains('LogStore')) { return } let transaction = db.transaction('LogStore', 'readwrite') let store = transaction.objectStore('LogStore') // Clear the store let clearRequest = store.clear() clearRequest.onerror = function (e) {} clearRequest.onsuccess = function (e) {} } } export const createIndexedDBStore = (tables, database, keySet = {keyPath: 'key' }) => { var open = indexedDB.open(database, INDEXED_DB_VERSION) // console.trace('createIndexedDBStore'); open.onupgradeneeded = function () { // console.log('createIndexedDBStore onupgradeneeded', database, ) var db = open.result // 数据库是否存在 for (const table of tables) { if (!db.objectStoreNames.contains(table)) { var store = db.createObjectStore(table, keySet) store.createIndex('timestamp', 'timestamp', { unique: false }) } else { const objectStore = open.transaction.objectStore(table) if (!objectStore.indexNames.contains('timestamp')) { objectStore.createIndex('timestamp', 'timestamp', { unique: false }) } } } } }; export const writeIndexDB = (rows, table, database) => { var open = indexedDB.open(database, INDEXED_DB_VERSION) open.onupgradeneeded = function () { // console.log('readIndexDB onupgradeneeded', table, ) var db = open.result // 数据库是否存在 if (!db.objectStoreNames.contains(table)) { var store = db.createObjectStore(table, { keyPath: 'key' }) store.createIndex('timestamp', 'timestamp', { unique: false }) } else { const objectStore = open.transaction.objectStore(table) if (!objectStore.indexNames.contains('timestamp')) { objectStore.createIndex('timestamp', 'timestamp', { unique: false }) } } } open.onsuccess = function () { var db = open.result // 数据库是否存在 if (!db.objectStoreNames.contains(table)) { console.warn(`writeIndexDB > Database does not exist.`, table); return } var tx = db.transaction(table, 'readwrite') var store = tx.objectStore(table) rows.forEach(row => { store.put({ ...row, _date: new Date().toLocaleString(), timestamp: Date.now() }) }); tx.oncomplete = function () { db.close() } } }; /** * Reads data from an IndexedDB object store. * It can read a single record by key, multiple records by an array of keys, or all records. * * @param {string|string[]|null} keys - The key(s) to read. * - If `string`: Reads a single record and returns the data object directly. * - If `string[]`: Reads multiple records and returns a Map of `rowkey` to `data` objects. * - If `null` or `undefined` or `empty string/array`: Reads all records and returns a Map of `rowkey` to `data` objects. * @param {string} table - The name of the IndexedDB object store (table). * @param {string} database - The name of the IndexedDB database. * @returns {Promise>} A promise that resolves with the data. * - Single key: Resolves with the data object or `undefined` if not found. * - Array of keys or All records: Resolves with a `Map` where keys are rowkeys and values are data objects. * The Map will be empty if no records are found. * - Rejects if there's an error opening the database or during the transaction. */ export const readIndexDB = (keys=null, table, database) => { return new Promise((resolve, reject) => { let openRequest = indexedDB.open(database) openRequest.onupgradeneeded = function () { // console.log('readIndexDB onupgradeneeded', table, ) var db = openRequest.result // 数据库是否存在 if (!db.objectStoreNames.contains(table)) { var store = db.createObjectStore(table, { keyPath: 'key' }) store.createIndex('timestamp', 'timestamp', { unique: false }) } else { const store = openRequest.transaction.objectStore(table) if (!store.indexNames.contains('timestamp')) { store.createIndex('timestamp', 'timestamp', { unique: false }) } } } openRequest.onerror = function (e) { console.error(`Error opening database.`, table, e) reject('Error opening database.') } openRequest.onsuccess = function (e) { let db = e.target.result // 数据库是否存在 if (!db.objectStoreNames.contains(table)) { console.warn(`readIndexDB > Database does not exist.`, table); resolve(); return } let transaction = db.transaction(table, 'readonly') let store = transaction.objectStore(table) // read by key // Handle array of keys if (Array.isArray(keys) && keys.length > 0) { const promises = keys.map(key => { return new Promise((innerResolve) => { const getRequest = store.get(key); getRequest.onsuccess = (event) => { const result = event.target.result; if (result) { // console.log(`💾Found record with key ${key}:`, result); innerResolve([key, result]); // Resolve with [key, data] tuple } else { // console.log(`No record found with key ${key}.`); innerResolve(void 0); // Resolve with undefined for non-existent keys } }; getRequest.onerror = (event) => { console.error(`Error getting record with key ${key}:`, event.target.error); innerResolve(undefined); // Resolve with undefined on error, or innerReject if you want to fail fast }; }); }); Promise.all(promises) .then(results => { const resultMap = new Map(); results.forEach(item => { if (item !== undefined) { resultMap.set(item[0], item[1]); // item[0] is key, item[1] is data } }); resolve(resultMap); }) .catch(error => { console.error('Error during batch read:', error); reject(error); // Reject the main promise if Promise.all encounters an error }); } else if (!isEmpty(keys)) { // Handle single key const getRequest = store.get(keys); getRequest.onsuccess = (event) => { const result = event.target.result; if (result) { // console.log(`💾Found record with key ${keys}:`, result); resolve(result); } else { // console.log(`No record found with key ${keys}.`); resolve(); } }; getRequest.onerror = (event) => { console.error(`Error getting record with key ${keys}:`, event.target.error); reject(event.target.error); }; } else { // Handle read all const getAllRequest = store.getAll(); getAllRequest.onsuccess = (event) => { const allData = event.target.result; const resultMap = new Map(); if (allData && allData.length > 0) { allData.forEach(item => { resultMap.set(item.key, item); }); // console.log(`💾Found all records:`, resultMap); resolve(resultMap); } else { // console.log(`No records found.`); resolve(resultMap); // Resolve with an empty Map if no records } }; getAllRequest.onerror = (event) => { console.error(`Error getting all records:`, event.target.error); reject(event.target.error); }; } } }) }; export const deleteIndexDBbyKey = (keys=null, table, database) => { return new Promise((resolve, reject) => { var open = indexedDB.open(database, INDEXED_DB_VERSION) open.onupgradeneeded = function () { // var db = open.result // // 数据库是否存在 // if (!db.objectStoreNames.contains(table)) { // var store = db.createObjectStore(table, { keyPath: 'id', autoIncrement: true }) // } } open.onsuccess = function (e) { let db = e.target.result // 数据库是否存在 if (!db.objectStoreNames.contains(table)) { console.warn('deleteIndexDBbyKey > Database does not exist.', table) resolve(); return } var tx = db.transaction(table, 'readwrite') var store = tx.objectStore(table) if (Array.isArray(keys) && keys.length > 0) { const promises = keys.map((key) => { return new Promise((innerResolve) => { const delRequest = store.delete(key) delRequest.onsuccess = (event) => { const result = event.target.result if (result) { innerResolve() } else { innerResolve(void 0) // Resolve with undefined for non-existent keys } } delRequest.onerror = (event) => { innerResolve(undefined) } }) }) Promise.allSettled(promises) .then((results) => { resolve(results) }) .catch((error) => { reject(error) }) } else if (!isEmpty(keys)) { // Handle single key const delRequest = store.delete(keys); delRequest.onsuccess = (event) => { const result = event.target.result; if (result) { resolve(result); } else { resolve(); } }; delRequest.onerror = (event) => { reject(event.target.error); }; } else { // 删除所有 let clearRequest = store.clear() clearRequest.onsuccess = function (e) { resolve(e.target.result) } clearRequest.onerror = function (e) { reject(e.target.error) } } tx.oncomplete = function () { db.close() } } }) }; function cleanOldData(database, storeNames=[], dateKey = 'timestamp', keySet = { keyPath: 'key' }) { createIndexedDBStore(storeNames, database, keySet); return function (daysToKeep = 7) { return new Promise((resolve, reject) => { let deletedCount = 0 const recordsToDelete = new Set() let openRequest = indexedDB.open(database, INDEXED_DB_VERSION) openRequest.onupgradeneeded = function () { // console.log('----cleanOldData onupgradeneeded----') var db = openRequest.result storeNames.forEach(storeName => { // 数据库是否存在 if (!db.objectStoreNames.contains(storeName)) { var store = db.createObjectStore(storeName, keySet) // var store = db.createObjectStore(storeName, { keyPath: 'id', autoIncrement: true }) store.createIndex('timestamp', 'timestamp', { unique: false }) } else { const logStore = openRequest.transaction.objectStore(storeName) if (!logStore.indexNames.contains('timestamp')) { logStore.createIndex('timestamp', 'timestamp', { unique: false }) } } }) } openRequest.onsuccess = function (e) { let db = e.target.result // 数据库是否存在 // if (!db.objectStoreNames.contains(storeName)) { // resolve('Database does not exist.') // return // } // Calculate the cutoff timestamp for "X days ago" const cutoffTimestamp = Date.now() - daysToKeep * 24 * 60 * 60 * 1000 const objectStoreNames = isEmpty(storeNames) ? db.objectStoreNames : storeNames if (!isEmpty(objectStoreNames)) { const objectStores = Array.from(objectStoreNames).map((storeName) => db.transaction([storeName], 'readwrite').objectStore(storeName)) for (const objectStore of objectStores) { // Identify old data using the date index and primary key ID if (!objectStore.indexNames.contains(`${dateKey}`)) { // Clear the store let clearRequest = objectStore.clear() console.log(`Cleanup complete. clear ${objectStore.name} records.`) resolve() clearRequest.onerror = function (e) {} clearRequest.onsuccess = function (e) {} return } // Get records older than 'daysToKeep' using the index const dateIndex = objectStore.index(`${dateKey}`) const dateRange = IDBKeyRange.upperBound(cutoffTimestamp, false) // Get keys < cutoffTimestamp (strictly older) const dateCursorRequest = dateIndex.openCursor(dateRange) dateCursorRequest.onsuccess = (event) => { const cursor = event.target.result if (cursor) { recordsToDelete.add(cursor.primaryKey) // Add the primary key of the record to the set cursor.continue() } else { const storeName = objectStore.name; // Delete identified data in a new transaction const deleteTransaction = db.transaction([storeName], 'readwrite') const deleteObjectStore = deleteTransaction.objectStore(storeName) deleteTransaction.oncomplete = () => { console.log(`Cleanup complete. Deleted ${deletedCount} records in ${database}.${storeName}.`) resolve(deletedCount) } deleteTransaction.onerror = (event) => { console.error('Deletion transaction error:', event.target.error) reject(event.target.error) } // Convert Set to Array for forEach Array.from(recordsToDelete).forEach((key) => { const deleteRequest = deleteObjectStore.delete(key) deleteRequest.onsuccess = () => { deletedCount++ } deleteRequest.onerror = (event) => { console.warn(`Failed to delete record with key ${key}:`, event.target.error) } }) } } dateCursorRequest.onerror = (event) => { console.error('Error opening date cursor for deletion:', event.target.error) reject(event.target.error) } } } else { console.warn('cleanOldData: No data to delete.', database); } } openRequest.onerror = function (e) { reject('Error opening database:'+database, e) } }) } } export const clean7DaysWebsocketLog = cleanOldData('LogWebsocketData', ['LogStore'], 'timestamp', { keyPath: 'id', autoIncrement: true }); export const clean7DaysMailboxLog = cleanOldData('mailbox', ['dirs', 'maillist', 'listrow', 'mailinfo', 'draft']); /** * 缓存清除策略: 清理7天前的 * - 每次进入 * - 每天半夜 */ export const LAST_SCHEDULED_CLEANUP_DAY_KEY = 'lastScheduledCleanupDay'; // For tracking scheduling export const LAST_EXECUTED_CLEANUP_DAY_KEY = 'lastExecutedCleanupDay'; // For tracking actual execution let cleanupTimeoutId = null; // To store the ID of the setTimeout /** * Determines if the cleanup needs to be scheduled for today. * This is based on when it was *last scheduled* to prevent re-scheduling * if the app was merely refreshed within the same day. * @returns {boolean} True if a new schedule for today is needed. */ function shouldScheduleForToday() { const lastScheduledDay = localStorage.getItem(LAST_SCHEDULED_CLEANUP_DAY_KEY); const today = new Date().toDateString(); // e.g., "Fri Jun 13 2025" return !lastScheduledDay || lastScheduledDay !== today; } /** * Determines if the cleanup was already *executed* today. * This is to prevent running the cleanup task multiple times in one day * if the app stays open past midnight or if it is refreshed. * @returns {boolean} True if the cleanup has not executed today. */ function hasCleanupExecutedToday() { const lastExecutedDay = localStorage.getItem(LAST_EXECUTED_CLEANUP_DAY_KEY); const today = new Date().toDateString(); return lastExecutedDay === today; } /** * Executes the cleanup and updates the last execution timestamp. * This function is designed to be called via requestIdleCallback. */ export async function executeDailyCleanupTask() { // const lastExecutedDay = localStorage.getItem(LAST_EXECUTED_CLEANUP_DAY_KEY) const today = new Date().toDateString() if (!hasCleanupExecutedToday()) { if ('requestIdleCallback' in window) { // console.log(`[${new Date().toLocaleTimeString()}] Scheduling cleanup via requestIdleCallback for execution.`); requestIdleCallback( async (deadline) => { console.log(`[${new Date().toLocaleTimeString()}] Running scheduled cleanup. Time remaining: ${deadline.timeRemaining().toFixed(2)}ms, Did timeout: ${deadline.didTimeout}`) try { await clean7DaysMailboxLog() await clean7DaysWebsocketLog() // Mark that cleanup was successfully executed for today localStorage.setItem(LAST_EXECUTED_CLEANUP_DAY_KEY, today) console.log('Daily cleanup marked as executed for today.') } catch (error) { console.error('Error during scheduled cleanup execution:', error) } }, { timeout: 5000 }, ) // Give it up to 5 seconds to find idle time } else { console.warn('requestIdleCallback not supported. Executing cleanup directly (might cause jank).') // Fallback for very old browsers: run directly. try { await clean7DaysMailboxLog() await clean7DaysWebsocketLog() localStorage.setItem(LAST_EXECUTED_CLEANUP_DAY_KEY, today) console.log('Daily cleanup marked as executed for today (without rIC).') } catch (error) { console.error('Error during direct cleanup execution:', error) } } } else { console.log(`[${new Date().toLocaleTimeString()}] Cleanup already executed today.`) } } /** * Initiates or re-initiates the daily midnight cleanup scheduler. * This function calls itself recursively to set up the next day's schedule. */ export function setupDailyMidnightCleanupScheduler() { if (cleanupTimeoutId) { clearTimeout(cleanupTimeoutId) cleanupTimeoutId = null } const now = new Date() const midnight = new Date(now) // Set to midnight (00:00:00) midnight.setDate(now.getDate() + 1) midnight.setHours(0, 0, 0, 0) const msToMidnight = midnight.getTime() - now.getTime() console.log(`[${new Date().toLocaleTimeString()}] Scheduling next daily cleanup at ${midnight.toLocaleTimeString()}, in ${msToMidnight / (1000 * 60 * 60)} hours.`) // Set the timeout for the next midnight cleanupTimeoutId = setTimeout(async () => { console.log(`[${new Date().toLocaleTimeString()}] Midnight trigger fired.`) if (!hasCleanupExecutedToday()) { await executeDailyCleanupTask() } else { console.log(`[${new Date().toLocaleTimeString()}] Cleanup already executed today, skipping re-execution.`) } setupDailyMidnightCleanupScheduler() }, msToMidnight) }