You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Global-sales/src/utils/indexedDB.js

589 lines
22 KiB
JavaScript

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<any|Map<string, any>>} 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)
}