feat: 缓存策略: 自动清除7天前的, 每天检查
parent
5d41b44270
commit
10e6b56446
@ -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<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 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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue