diff --git a/src/actions/EmailActions.js b/src/actions/EmailActions.js index 39c5f6b..bad97d2 100644 --- a/src/actions/EmailActions.js +++ b/src/actions/EmailActions.js @@ -1,6 +1,7 @@ import { fetchJSON, postForm, postJSON } from '@/utils/request'; import { API_HOST, API_HOST_V3, DATE_FORMAT, DATEEND_FORMAT, DATETIME_FORMAT, EMAIL_HOST } from '@/config'; -import { buildTree, groupBy, isEmpty, objectMapper, omitEmpty, readIndexDB, uniqWith, writeIndexDB } from '@/utils/commons'; +import { buildTree, groupBy, isEmpty, objectMapper, omitEmpty, uniqWith } from '@/utils/commons'; +import { readIndexDB, writeIndexDB } from '@/utils/indexedDB'; import dayjs from 'dayjs'; export const parseHTMLString = (html, needText = false) => { diff --git a/src/channel/realTimeAPI.js b/src/channel/realTimeAPI.js index 76850a3..6a6526e 100644 --- a/src/channel/realTimeAPI.js +++ b/src/channel/realTimeAPI.js @@ -2,7 +2,7 @@ import { webSocket } from 'rxjs/webSocket'; import { of, timer, concatMap, EMPTY, takeWhile, concat } from 'rxjs'; import { filter, buffer, map, tap, retryWhen, retry, delay, take, catchError } from 'rxjs/operators'; import { v4 as uuid } from 'uuid'; -import { logWebsocket } from '@/utils/commons'; +import { logWebsocket } from '@/utils/indexedDB'; export class RealTimeAPI { constructor(param, onOpenCallback, onCloseCallback, onRetryCallback) { diff --git a/src/hooks/useEmail.js b/src/hooks/useEmail.js index a2cc727..12968a1 100644 --- a/src/hooks/useEmail.js +++ b/src/hooks/useEmail.js @@ -1,5 +1,6 @@ import { useState, useEffect, useCallback } from 'react' -import { isEmpty, objectMapper, olog, readIndexDB, } from '@/utils/commons' +import { isEmpty, objectMapper, olog, } from '@/utils/commons' +import { readIndexDB } from '@/utils/indexedDB' import { getEmailDetailAction, postResendEmailAction, getSalesSignatureAction, getEmailOrderAction, queryEmailListAction, getEmailTemplateAction, saveEmailDraftOrSendAction, updateEmailAction } from '@/actions/EmailActions' import { App } from 'antd' import useConversationStore from '@/stores/ConversationStore'; diff --git a/src/main.jsx b/src/main.jsx index 9b99f35..c1c56a4 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -33,6 +33,8 @@ import '@/assets/index.css' import CustomerRelation from '@/views/customer_relation/index' import NewEmail from './views/NewEmail' +import { executeDailyCleanupTask, setupDailyMidnightCleanupScheduler } from '@/utils/indexedDB' + useAuthStore.getState().loadUserSession() const isMobileApp = @@ -140,3 +142,13 @@ createRoot(root).render( , ) + + +// --- Global Setup After React App Mounts --- +// This part will run once when the application script is loaded and executed. +document.addEventListener('DOMContentLoaded', () => { + console.log(`[${new Date().toLocaleTimeString()}] Application fully loaded. Initiating global daily cleanup checks.`); + + executeDailyCleanupTask(); + setupDailyMidnightCleanupScheduler(); +}); diff --git a/src/stores/ConversationStore.js b/src/stores/ConversationStore.js index 05a2e23..a78ae0b 100644 --- a/src/stores/ConversationStore.js +++ b/src/stores/ConversationStore.js @@ -1,6 +1,7 @@ import { create } from 'zustand'; import { RealTimeAPI } from '@/channel/realTimeAPI'; -import { olog, isEmpty, groupBy, sortArrayByOrder, logWebsocket, pick, sortKeys, omit, sortObjectsByKeysMap, clean7DaysWebsocketLog, createIndexedDBStore } from '@/utils/commons'; +import { olog, isEmpty, groupBy, sortArrayByOrder, pick, sortKeys, omit, sortObjectsByKeysMap } from '@/utils/commons'; +import { logWebsocket, clean7DaysWebsocketLog } from '@/utils/indexedDB' import { receivedMsgTypeMapped, handleNotification } from '@/channel/bubbleMsgUtils'; import { fetchConversationsList, fetchTemplates, fetchConversationsSearch, UNREAD_MARK, fetchTags } from '@/actions/ConversationActions'; import { devtools } from 'zustand/middleware'; @@ -589,8 +590,6 @@ export const useConversationStore = create( setTags(myTags); setInitial(true); - // 登录后, 执行一下清除日志缓存 - clean7DaysWebsocketLog(); }, reset: () => set(initialConversationState), diff --git a/src/stores/EmailSlice.js b/src/stores/EmailSlice.js index fae232d..6234e55 100644 --- a/src/stores/EmailSlice.js +++ b/src/stores/EmailSlice.js @@ -1,5 +1,6 @@ import { getEmailDirAction, getRootMailboxDirAction } from '@/actions/EmailActions' -import { buildTree, isEmpty, readIndexDB, writeIndexDB, createIndexedDBStore, clean7DaysMailboxLog } from '@/utils/commons' +import { buildTree, isEmpty } from '@/utils/commons' +import { readIndexDB, writeIndexDB, createIndexedDBStore, clean7DaysMailboxLog } from '@/utils/indexedDB'; /** * Email @@ -129,8 +130,6 @@ const emailSlice = (set, get) => ({ setCurrentMailboxOPI(opi_sn) setCurrentMailboxDEI(dei_sn) getOPIEmailDir(opi_sn, userIdStr) - // 登录后, 执行一下清除缓存 - clean7DaysMailboxLog(); }, }) diff --git a/src/utils/commons.js b/src/utils/commons.js index ef297e1..40e3d7e 100644 --- a/src/utils/commons.js +++ b/src/utils/commons.js @@ -675,403 +675,3 @@ export const buildTree = (list, keyMap={ rootKeys: [], ignoreKeys: [] }) => { return treeRoots } - -/** - * - */ -const INDEXED_DB_VERSION = 4; -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) => { - var open = indexedDB.open(database, INDEXED_DB_VERSION) - open.onupgradeneeded = function () { - console.log('readIndexDB onupgradeneeded', database, ) - var db = open.result - // 数据库是否存在 - for (const table of tables) { - 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 }) - } - } - } - } -}; - -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 - 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 logStore = openRequest.transaction.objectStore(table) - if (!logStore.indexNames.contains('timestamp')) { - logStore.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)) { - resolve('Database does not exist.') - 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 = (key, table, database) => { - 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 () { - var db = open.result - var tx = db.transaction(table, 'readwrite') - var store = tx.objectStore(table) - store.delete(key) - tx.oncomplete = function () { - db.close() - } - } -}; - -function cleanOldData(database, storeNames=[], dateKey = 'timestamp') { - 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 () { - // var db = openRequest.result - // 数据库是否存在 - // if (!db.objectStoreNames.contains(storeName)) { - // 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) - } - } - } - } - openRequest.onerror = function (e) { - reject('Error opening database:'+database, e) - } - }) - } -} - -export const clean7DaysWebsocketLog = cleanOldData('LogWebsocketData', ['LogStore']); -export const clean7DaysMailboxLog = cleanOldData('mailbox'); diff --git a/src/utils/indexedDB.js b/src/utils/indexedDB.js new file mode 100644 index 0000000..38e2a5c --- /dev/null +++ b/src/utils/indexedDB.js @@ -0,0 +1,517 @@ +import { isEmpty } from './commons'; +/** + * + */ +const INDEXED_DB_VERSION = 4; +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) => { + var open = indexedDB.open(database, INDEXED_DB_VERSION) + open.onupgradeneeded = function () { + console.log('readIndexDB onupgradeneeded', database, ) + var db = open.result + // 数据库是否存在 + for (const table of tables) { + 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 }) + } + } + } + } +}; + +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 + 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 logStore = openRequest.transaction.objectStore(table) + if (!logStore.indexNames.contains('timestamp')) { + logStore.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)) { + resolve('Database does not exist.') + 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 = (key, table, database) => { + 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 () { + var db = open.result + var tx = db.transaction(table, 'readwrite') + var store = tx.objectStore(table) + store.delete(key) + tx.oncomplete = function () { + db.close() + } + } +}; + +function cleanOldData(database, storeNames=[], dateKey = 'timestamp') { + 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 () { + var db = openRequest.result + storeNames.forEach(storeName => { + // 数据库是否存在 + if (!db.objectStoreNames.contains(storeName)) { + 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) + } + } + } + } + openRequest.onerror = function (e) { + reject('Error opening database:'+database, e) + } + }) + } +} + +export const clean7DaysWebsocketLog = cleanOldData('LogWebsocketData', ['LogStore']); +export const clean7DaysMailboxLog = cleanOldData('mailbox'); + + +/** + * 缓存清除策略: 清理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) +} + diff --git a/src/utils/pagespy.js b/src/utils/pagespy.js index 44ed588..16e6731 100644 --- a/src/utils/pagespy.js +++ b/src/utils/pagespy.js @@ -1,4 +1,5 @@ -import { loadScript, readWebsocketLog } from '@/utils/commons' +import { loadScript } from '@/utils/commons' +import { readWebsocketLog } from '@/utils/indexedDB' import { BUILD_VERSION, BUILD_DATE } from '@/config' export const loadPageSpy = (title) => { diff --git a/src/views/AuthApp.jsx b/src/views/AuthApp.jsx index db22305..eedf3cb 100644 --- a/src/views/AuthApp.jsx +++ b/src/views/AuthApp.jsx @@ -24,7 +24,7 @@ import '@/assets/App.css' import 'react-chat-elements/dist/main.css' import EmailFetch from './Conversations/Online/Components/EmailFetch' import FetchEmailWorker from './../workers/fetchEmailWorker?worker&url' -import { clearWebsocketLog, readWebsocketLog } from '@/utils/commons' +import { readWebsocketLog } from '@/utils/indexedDB' import { useGlobalNotify } from '@/hooks/useGlobalNotify' import GeneratePaymentDrawer from './Conversations/Online/Components/GeneratePaymentDrawer' import GenerateAutoDocDrawer from './Conversations/Online/Components/GenerateAutoDocDrawer' diff --git a/src/views/Conversations/Online/Components/EmailDetailInline.jsx b/src/views/Conversations/Online/Components/EmailDetailInline.jsx index a726b78..11f9b1b 100644 --- a/src/views/Conversations/Online/Components/EmailDetailInline.jsx +++ b/src/views/Conversations/Online/Components/EmailDetailInline.jsx @@ -57,25 +57,25 @@ const EmailDetailInline = ({ mailID, emailMsg = {}, disabled = false, variant, s const { mai_sn, id } = emailMsg.msgOrigin?.email || emailMsg.msgOrigin || {} // const mailID = mai_sn || id - const [action, setAction] = useState('') + // const [action, setAction] = useState('') - const [openEmailEditor, setOpenEmailEditor] = useState(false) - const [fromEmail, setFromEmail] = useState('') - useEffect(() => { - setOpenEmailEditor(false) + // const [openEmailEditor, setOpenEmailEditor] = useState(false) + // const [fromEmail, setFromEmail] = useState('') + // useEffect(() => { + // setOpenEmailEditor(false) - return () => {} - }, [mailID]) + // return () => {} + // }, [mailID]) const onOpenEditor = (msgOrigin, action='reply') => { openPopup(`/email/${action}/${mailID || 0}`, `${action}-${mailID || 0}`) if (typeof props.onOpenEditor === 'function') { props.onOpenEditor(msgOrigin, action); } else { - const { from, to } = msgOrigin - setOpenEmailEditor(true) - setFromEmail(action === 'edit' ? from : to) - setAction(action) + // const { from, to } = msgOrigin + // setOpenEmailEditor(true) + // setFromEmail(action === 'edit' ? from : to) + // setAction(action) // // setOpen(false) } } diff --git a/src/views/Conversations/Online/Input/EmailEditorPopup.jsx b/src/views/Conversations/Online/Input/EmailEditorPopup.jsx index 284aad2..31ffb49 100644 --- a/src/views/Conversations/Online/Input/EmailEditorPopup.jsx +++ b/src/views/Conversations/Online/Input/EmailEditorPopup.jsx @@ -10,7 +10,8 @@ import useAuthStore from '@/stores/AuthStore'; import LexicalEditor from '@/components/LexicalEditor'; import { v4 as uuid } from 'uuid'; -import { cloneDeep, debounce, isEmpty, writeIndexDB, } from '@/utils/commons'; +import { cloneDeep, debounce, isEmpty, } from '@/utils/commons'; +import { writeIndexDB } from '@/utils/indexedDB'; import './EmailEditor.css'; import { postSendEmail } from '@/actions/EmailActions'; import { sentMsgTypeMapped, } from '@/channel/bubbleMsgUtils'; diff --git a/src/views/NewEmail.jsx b/src/views/NewEmail.jsx index 8de9a68..85f5914 100644 --- a/src/views/NewEmail.jsx +++ b/src/views/NewEmail.jsx @@ -10,7 +10,9 @@ import useAuthStore from '@/stores/AuthStore' import LexicalEditor from '@/components/LexicalEditor' import { v4 as uuid } from 'uuid' -import { cloneDeep, debounce, isEmpty, writeIndexDB, readIndexDB, deleteIndexDBbyKey, olog, omitEmpty } from '@/utils/commons' +import { cloneDeep, debounce, isEmpty, olog, omitEmpty } from '@/utils/commons' +import { writeIndexDB, readIndexDB, deleteIndexDBbyKey, } from '@/utils/indexedDB'; + import '@/views/Conversations/Online/Input/EmailEditor.css' import { deleteEmailAttachmentAction, parseHTMLString, postSendEmail, saveEmailDraftOrSendAction } from '@/actions/EmailActions' import { sentMsgTypeMapped } from '@/channel/bubbleMsgUtils'