mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2026-02-08 23:44:25 +08:00
cap nhat auth he thong de nhung voi chebichat
This commit is contained in:
@@ -5,41 +5,213 @@ import { safeLocalStorage } from "@/app/utils";
|
||||
const localStorage = safeLocalStorage();
|
||||
|
||||
class IndexedDBStorage implements StateStorage {
|
||||
private isIndexedDBAvailable = true;
|
||||
|
||||
public async getItem(name: string): Promise<string | null> {
|
||||
try {
|
||||
const value = (await get(name)) || localStorage.getItem(name);
|
||||
return value;
|
||||
// First try IndexedDB
|
||||
if (this.isIndexedDBAvailable) {
|
||||
const value = await get(name);
|
||||
if (value !== undefined) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to localStorage
|
||||
const localValue = localStorage.getItem(name);
|
||||
console.log(`[IndexedDB] Retrieved from localStorage for key: ${name}`);
|
||||
return localValue;
|
||||
} catch (error) {
|
||||
console.error(`[IndexedDB] Error getting item ${name}:`, error);
|
||||
this.isIndexedDBAvailable = false;
|
||||
return localStorage.getItem(name);
|
||||
}
|
||||
}
|
||||
|
||||
public async setItem(name: string, value: string): Promise<void> {
|
||||
try {
|
||||
const _value = JSON.parse(value);
|
||||
if (!_value?.state?._hasHydrated) {
|
||||
console.warn("skip setItem", name);
|
||||
// Validate JSON structure
|
||||
let parsedValue;
|
||||
try {
|
||||
parsedValue = JSON.parse(value);
|
||||
} catch (parseError) {
|
||||
console.error(`[IndexedDB] Invalid JSON for key ${name}:`, parseError);
|
||||
// Still try to store the raw value
|
||||
parsedValue = null;
|
||||
}
|
||||
|
||||
// Check if this is a Zustand store with hydration state
|
||||
const isZustandStore =
|
||||
parsedValue &&
|
||||
typeof parsedValue === "object" &&
|
||||
parsedValue.state &&
|
||||
typeof parsedValue.state === "object";
|
||||
|
||||
// For Zustand stores, check hydration status
|
||||
if (isZustandStore) {
|
||||
const hasHydrated = parsedValue.state._hasHydrated;
|
||||
|
||||
// Allow storage if:
|
||||
// 1. Already hydrated, OR
|
||||
// 2. Initial state (not hydrated but has meaningful data)
|
||||
const shouldStore =
|
||||
hasHydrated ||
|
||||
(parsedValue.state.sessions &&
|
||||
parsedValue.state.sessions.length > 0) ||
|
||||
Object.keys(parsedValue.state).length > 2; // More than just _hasHydrated and version
|
||||
|
||||
if (!shouldStore) {
|
||||
console.log(
|
||||
`[IndexedDB] Skipping storage for ${name} - not hydrated and no meaningful data`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Try IndexedDB first
|
||||
if (this.isIndexedDBAvailable) {
|
||||
await set(name, value);
|
||||
console.log(`[IndexedDB] Successfully stored ${name} in IndexedDB`);
|
||||
|
||||
// Also store in localStorage as backup for critical stores
|
||||
if (
|
||||
name.includes("chat") ||
|
||||
name.includes("config") ||
|
||||
name.includes("access")
|
||||
) {
|
||||
localStorage.setItem(name, value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
await set(name, value);
|
||||
} catch (error) {
|
||||
|
||||
// Fallback to localStorage
|
||||
localStorage.setItem(name, value);
|
||||
console.log(
|
||||
`[IndexedDB] Stored ${name} in localStorage (IndexedDB unavailable)`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`[IndexedDB] Error setting item ${name}:`, error);
|
||||
this.isIndexedDBAvailable = false;
|
||||
|
||||
// Always fallback to localStorage on error
|
||||
try {
|
||||
localStorage.setItem(name, value);
|
||||
console.log(`[IndexedDB] Fallback: stored ${name} in localStorage`);
|
||||
} catch (localError) {
|
||||
console.error(
|
||||
`[IndexedDB] Failed to store ${name} in localStorage:`,
|
||||
localError,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async removeItem(name: string): Promise<void> {
|
||||
try {
|
||||
await del(name);
|
||||
// Remove from both storages to ensure cleanup
|
||||
if (this.isIndexedDBAvailable) {
|
||||
await del(name);
|
||||
}
|
||||
localStorage.removeItem(name);
|
||||
console.log(`[IndexedDB] Removed ${name} from both storages`);
|
||||
} catch (error) {
|
||||
console.error(`[IndexedDB] Error removing item ${name}:`, error);
|
||||
this.isIndexedDBAvailable = false;
|
||||
localStorage.removeItem(name);
|
||||
}
|
||||
}
|
||||
|
||||
public async clear(): Promise<void> {
|
||||
try {
|
||||
await clear();
|
||||
} catch (error) {
|
||||
// Clear both storages
|
||||
if (this.isIndexedDBAvailable) {
|
||||
await clear();
|
||||
}
|
||||
localStorage.clear();
|
||||
console.log(`[IndexedDB] Cleared both storages`);
|
||||
} catch (error) {
|
||||
console.error(`[IndexedDB] Error clearing storage:`, error);
|
||||
this.isIndexedDBAvailable = false;
|
||||
localStorage.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Utility method to check storage health
|
||||
public async checkHealth(): Promise<{
|
||||
indexedDB: boolean;
|
||||
localStorage: boolean;
|
||||
}> {
|
||||
const health = {
|
||||
indexedDB: false,
|
||||
localStorage: false,
|
||||
};
|
||||
|
||||
// Test IndexedDB
|
||||
try {
|
||||
await set("health-check", "test");
|
||||
await get("health-check");
|
||||
await del("health-check");
|
||||
health.indexedDB = true;
|
||||
this.isIndexedDBAvailable = true;
|
||||
} catch (error) {
|
||||
console.warn("[IndexedDB] Health check failed:", error);
|
||||
this.isIndexedDBAvailable = false;
|
||||
}
|
||||
|
||||
// Test localStorage
|
||||
try {
|
||||
localStorage.setItem("health-check", "test");
|
||||
localStorage.getItem("health-check");
|
||||
localStorage.removeItem("health-check");
|
||||
health.localStorage = true;
|
||||
} catch (error) {
|
||||
console.warn("[IndexedDB] localStorage health check failed:", error);
|
||||
}
|
||||
|
||||
return health;
|
||||
}
|
||||
|
||||
// Method to migrate data from localStorage to IndexedDB
|
||||
public async migrateFromLocalStorage(): Promise<void> {
|
||||
if (!this.isIndexedDBAvailable) {
|
||||
console.warn("[IndexedDB] Cannot migrate - IndexedDB unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get all store keys from constants
|
||||
const storeKeys = [
|
||||
"chat-next-web-store",
|
||||
"chat-next-web-plugin",
|
||||
"access-control",
|
||||
"app-config",
|
||||
"mask-store",
|
||||
"prompt-store",
|
||||
"chat-update",
|
||||
"sync",
|
||||
"sd-list",
|
||||
"mcp-store",
|
||||
];
|
||||
|
||||
for (const key of storeKeys) {
|
||||
const localValue = localStorage.getItem(key);
|
||||
if (localValue) {
|
||||
try {
|
||||
// Check if already exists in IndexedDB
|
||||
const existingValue = await get(key);
|
||||
if (!existingValue) {
|
||||
await set(key, localValue);
|
||||
console.log(
|
||||
`[IndexedDB] Migrated ${key} from localStorage to IndexedDB`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[IndexedDB] Failed to migrate ${key}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[IndexedDB] Migration failed:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
236
app/utils/storage-debug.ts
Normal file
236
app/utils/storage-debug.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
import { indexedDBStorage } from "./indexedDB-storage";
|
||||
import { StoreKey } from "../constant";
|
||||
|
||||
// Storage debugging utilities
|
||||
export class StorageDebugger {
|
||||
static async checkStorageHealth() {
|
||||
console.log("🔍 Checking storage health...");
|
||||
const health = await indexedDBStorage.checkHealth();
|
||||
|
||||
console.log("📊 Storage Health Report:");
|
||||
console.log(
|
||||
` - IndexedDB: ${health.indexedDB ? "✅ Available" : "❌ Unavailable"}`,
|
||||
);
|
||||
console.log(
|
||||
` - localStorage: ${
|
||||
health.localStorage ? "✅ Available" : "❌ Unavailable"
|
||||
}`,
|
||||
);
|
||||
|
||||
return health;
|
||||
}
|
||||
|
||||
static async listAllStoreData() {
|
||||
console.log("📋 Listing all store data...");
|
||||
|
||||
const storeKeys = Object.values(StoreKey);
|
||||
const storeData: Record<string, any> = {};
|
||||
|
||||
for (const key of storeKeys) {
|
||||
try {
|
||||
const data = await indexedDBStorage.getItem(key);
|
||||
if (data) {
|
||||
const parsed = JSON.parse(data);
|
||||
storeData[key] = {
|
||||
size: data.length,
|
||||
hasState: !!parsed.state,
|
||||
hasHydrated: parsed.state?._hasHydrated || false,
|
||||
lastUpdateTime: parsed.state?.lastUpdateTime || 0,
|
||||
keys: Object.keys(parsed.state || {}),
|
||||
};
|
||||
|
||||
// Special handling for chat store
|
||||
if (key === StoreKey.Chat && parsed.state?.sessions) {
|
||||
storeData[key].sessionCount = parsed.state.sessions.length;
|
||||
storeData[key].currentSessionIndex =
|
||||
parsed.state.currentSessionIndex;
|
||||
}
|
||||
} else {
|
||||
storeData[key] = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error reading store ${key}:`, error);
|
||||
storeData[key] = {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
console.table(storeData);
|
||||
return storeData;
|
||||
}
|
||||
|
||||
static async migrateData() {
|
||||
console.log("🔄 Starting data migration...");
|
||||
await indexedDBStorage.migrateFromLocalStorage();
|
||||
console.log("✅ Migration completed");
|
||||
}
|
||||
|
||||
static async clearStore(storeKey: StoreKey) {
|
||||
console.log(`🗑️ Clearing store: ${storeKey}`);
|
||||
try {
|
||||
await indexedDBStorage.removeItem(storeKey);
|
||||
console.log(`✅ Store ${storeKey} cleared successfully`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to clear store ${storeKey}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
static async clearAllStores() {
|
||||
console.log("🗑️ Clearing all stores...");
|
||||
try {
|
||||
await indexedDBStorage.clear();
|
||||
console.log("✅ All stores cleared successfully");
|
||||
} catch (error) {
|
||||
console.error("❌ Failed to clear all stores:", error);
|
||||
}
|
||||
}
|
||||
|
||||
static async backupStores() {
|
||||
console.log("💾 Creating backup of all stores...");
|
||||
|
||||
const backup: Record<string, any> = {
|
||||
timestamp: new Date().toISOString(),
|
||||
stores: {},
|
||||
};
|
||||
|
||||
const storeKeys = Object.values(StoreKey);
|
||||
|
||||
for (const key of storeKeys) {
|
||||
try {
|
||||
const data = await indexedDBStorage.getItem(key);
|
||||
if (data) {
|
||||
backup.stores[key] = data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error backing up store ${key}:`, error);
|
||||
backup.stores[key] = {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Save backup to localStorage as well
|
||||
try {
|
||||
const backupString = JSON.stringify(backup);
|
||||
localStorage.setItem("store-backup", backupString);
|
||||
console.log("💾 Backup saved to localStorage");
|
||||
|
||||
// Also download as file
|
||||
const blob = new Blob([backupString], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `store-backup-${
|
||||
new Date().toISOString().split("T")[0]
|
||||
}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
console.log("💾 Backup downloaded as file");
|
||||
} catch (error) {
|
||||
console.error("❌ Failed to save backup:", error);
|
||||
}
|
||||
|
||||
return backup;
|
||||
}
|
||||
|
||||
static async restoreFromBackup(backupData: any) {
|
||||
console.log("♻️ Restoring from backup...");
|
||||
|
||||
if (!backupData || !backupData.stores) {
|
||||
throw new Error("Invalid backup data");
|
||||
}
|
||||
|
||||
for (const [key, data] of Object.entries(backupData.stores)) {
|
||||
if (typeof data === "string") {
|
||||
try {
|
||||
await indexedDBStorage.setItem(key, data);
|
||||
console.log(`✅ Restored store: ${key}`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to restore store ${key}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("♻️ Restore completed");
|
||||
}
|
||||
|
||||
static async validateStoreIntegrity() {
|
||||
console.log("🔍 Validating store integrity...");
|
||||
|
||||
const issues: string[] = [];
|
||||
const storeKeys = Object.values(StoreKey);
|
||||
|
||||
for (const key of storeKeys) {
|
||||
try {
|
||||
const data = await indexedDBStorage.getItem(key);
|
||||
if (data) {
|
||||
const parsed = JSON.parse(data);
|
||||
|
||||
// Basic structure validation
|
||||
if (!parsed.state) {
|
||||
issues.push(`${key}: Missing state object`);
|
||||
}
|
||||
|
||||
if (
|
||||
parsed.state &&
|
||||
typeof parsed.state._hasHydrated === "undefined"
|
||||
) {
|
||||
issues.push(`${key}: Missing _hasHydrated flag`);
|
||||
}
|
||||
|
||||
// Store-specific validation
|
||||
if (key === StoreKey.Chat) {
|
||||
if (
|
||||
!parsed.state?.sessions ||
|
||||
!Array.isArray(parsed.state.sessions)
|
||||
) {
|
||||
issues.push(`${key}: Invalid or missing sessions array`);
|
||||
}
|
||||
|
||||
if (typeof parsed.state?.currentSessionIndex !== "number") {
|
||||
issues.push(`${key}: Invalid currentSessionIndex`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
issues.push(
|
||||
`${key}: JSON parse error - ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (issues.length === 0) {
|
||||
console.log("✅ All stores have valid integrity");
|
||||
} else {
|
||||
console.warn("⚠️ Store integrity issues found:");
|
||||
issues.forEach((issue) => console.warn(` - ${issue}`));
|
||||
}
|
||||
|
||||
return issues;
|
||||
}
|
||||
}
|
||||
|
||||
// Global debugging functions for console use
|
||||
if (typeof window !== "undefined") {
|
||||
(window as any).debugStorage = StorageDebugger;
|
||||
|
||||
// Add helpful console messages
|
||||
console.log(`
|
||||
🔧 Storage Debug Utils Available:
|
||||
- debugStorage.checkStorageHealth()
|
||||
- debugStorage.listAllStoreData()
|
||||
- debugStorage.migrateData()
|
||||
- debugStorage.clearStore(StoreKey.Chat)
|
||||
- debugStorage.clearAllStores()
|
||||
- debugStorage.backupStores()
|
||||
- debugStorage.validateStoreIntegrity()
|
||||
|
||||
Example: debugStorage.listAllStoreData()
|
||||
`);
|
||||
}
|
||||
319
app/utils/storage-migration.ts
Normal file
319
app/utils/storage-migration.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
import { StoreKey } from "../constant";
|
||||
import { indexedDBStorage } from "./indexedDB-storage";
|
||||
import { safeLocalStorage } from "../utils";
|
||||
|
||||
const localStorage = safeLocalStorage();
|
||||
|
||||
export class StorageMigration {
|
||||
/**
|
||||
* Initialize storage system and perform necessary migrations
|
||||
*/
|
||||
static async initialize(): Promise<void> {
|
||||
console.log("[StorageMigration] Initializing storage system...");
|
||||
|
||||
try {
|
||||
// Check storage health
|
||||
const health = await indexedDBStorage.checkHealth();
|
||||
|
||||
if (health.indexedDB) {
|
||||
console.log("[StorageMigration] IndexedDB available");
|
||||
|
||||
// Attempt to migrate data from localStorage if needed
|
||||
await this.migrateFromLocalStorageIfNeeded();
|
||||
} else if (health.localStorage) {
|
||||
console.log(
|
||||
"[StorageMigration] IndexedDB unavailable, using localStorage",
|
||||
);
|
||||
} else {
|
||||
console.error("[StorageMigration] No storage available!");
|
||||
throw new Error("No storage mechanism available");
|
||||
}
|
||||
|
||||
// Validate existing data
|
||||
await this.validateAndRepairStores();
|
||||
} catch (error) {
|
||||
console.error("[StorageMigration] Initialization failed:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate data from localStorage to IndexedDB if needed
|
||||
*/
|
||||
private static async migrateFromLocalStorageIfNeeded(): Promise<void> {
|
||||
const storeKeys = Object.values(StoreKey);
|
||||
let migratedCount = 0;
|
||||
|
||||
for (const key of storeKeys) {
|
||||
try {
|
||||
// Check if data exists in localStorage but not in IndexedDB
|
||||
const localData = localStorage.getItem(key);
|
||||
const indexedData = await indexedDBStorage.getItem(key);
|
||||
|
||||
if (localData && !indexedData) {
|
||||
// Validate the data before migration
|
||||
try {
|
||||
const parsed = JSON.parse(localData);
|
||||
if (parsed && typeof parsed === "object") {
|
||||
await indexedDBStorage.setItem(key, localData);
|
||||
migratedCount++;
|
||||
console.log(
|
||||
`[StorageMigration] Migrated ${key} from localStorage to IndexedDB`,
|
||||
);
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.warn(
|
||||
`[StorageMigration] Skipping invalid data for ${key}:`,
|
||||
parseError,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[StorageMigration] Failed to migrate ${key}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (migratedCount > 0) {
|
||||
console.log(
|
||||
`[StorageMigration] Successfully migrated ${migratedCount} stores`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and repair store data if necessary
|
||||
*/
|
||||
private static async validateAndRepairStores(): Promise<void> {
|
||||
const storeKeys = Object.values(StoreKey);
|
||||
|
||||
for (const key of storeKeys) {
|
||||
try {
|
||||
const data = await indexedDBStorage.getItem(key);
|
||||
if (data) {
|
||||
const repaired = await this.repairStoreData(key, data);
|
||||
if (repaired && repaired !== data) {
|
||||
await indexedDBStorage.setItem(key, repaired);
|
||||
console.log(`[StorageMigration] Repaired data for ${key}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[StorageMigration] Failed to validate ${key}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Repair store data if it has known issues
|
||||
*/
|
||||
private static async repairStoreData(
|
||||
storeKey: string,
|
||||
data: string,
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
let modified = false;
|
||||
|
||||
// Ensure basic structure exists
|
||||
if (!parsed.state) {
|
||||
parsed.state = {};
|
||||
modified = true;
|
||||
}
|
||||
|
||||
// Ensure _hasHydrated flag exists
|
||||
if (typeof parsed.state._hasHydrated === "undefined") {
|
||||
parsed.state._hasHydrated = false;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
// Ensure lastUpdateTime exists
|
||||
if (typeof parsed.state.lastUpdateTime === "undefined") {
|
||||
parsed.state.lastUpdateTime = Date.now();
|
||||
modified = true;
|
||||
}
|
||||
|
||||
// Store-specific repairs
|
||||
switch (storeKey) {
|
||||
case StoreKey.Chat:
|
||||
if (this.repairChatStore(parsed.state)) {
|
||||
modified = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case StoreKey.Config:
|
||||
if (this.repairConfigStore(parsed.state)) {
|
||||
modified = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case StoreKey.Access:
|
||||
if (this.repairAccessStore(parsed.state)) {
|
||||
modified = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return modified ? JSON.stringify(parsed) : null;
|
||||
} catch (error) {
|
||||
console.error(`[StorageMigration] Failed to repair ${storeKey}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Repair chat store specific issues
|
||||
*/
|
||||
private static repairChatStore(state: any): boolean {
|
||||
let modified = false;
|
||||
|
||||
// Ensure sessions array exists
|
||||
if (!Array.isArray(state.sessions)) {
|
||||
state.sessions = [];
|
||||
modified = true;
|
||||
}
|
||||
|
||||
// Ensure currentSessionIndex is valid
|
||||
if (
|
||||
typeof state.currentSessionIndex !== "number" ||
|
||||
state.currentSessionIndex < 0 ||
|
||||
state.currentSessionIndex >= state.sessions.length
|
||||
) {
|
||||
state.currentSessionIndex = 0;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
// Ensure each session has required properties
|
||||
state.sessions.forEach((session: any, index: number) => {
|
||||
if (!session.id) {
|
||||
session.id = `session-${Date.now()}-${index}`;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (!Array.isArray(session.messages)) {
|
||||
session.messages = [];
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (!session.mask) {
|
||||
session.mask = { modelConfig: {} };
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (typeof session.lastUpdate !== "number") {
|
||||
session.lastUpdate = Date.now();
|
||||
modified = true;
|
||||
}
|
||||
});
|
||||
|
||||
return modified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Repair config store specific issues
|
||||
*/
|
||||
private static repairConfigStore(state: any): boolean {
|
||||
let modified = false;
|
||||
|
||||
// Ensure basic config properties exist
|
||||
if (!state.theme) {
|
||||
state.theme = "auto";
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (!state.models || !Array.isArray(state.models)) {
|
||||
state.models = [];
|
||||
modified = true;
|
||||
}
|
||||
|
||||
return modified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Repair access store specific issues
|
||||
*/
|
||||
private static repairAccessStore(state: any): boolean {
|
||||
let modified = false;
|
||||
|
||||
// Ensure access code exists
|
||||
if (typeof state.accessCode !== "string") {
|
||||
state.accessCode = "";
|
||||
modified = true;
|
||||
}
|
||||
|
||||
return modified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear corrupted stores and reset to defaults
|
||||
*/
|
||||
static async clearCorruptedStores(): Promise<void> {
|
||||
console.log("[StorageMigration] Clearing corrupted stores...");
|
||||
|
||||
const storeKeys = Object.values(StoreKey);
|
||||
|
||||
for (const key of storeKeys) {
|
||||
try {
|
||||
const data = await indexedDBStorage.getItem(key);
|
||||
if (data) {
|
||||
try {
|
||||
JSON.parse(data);
|
||||
} catch (parseError) {
|
||||
console.warn(`[StorageMigration] Clearing corrupted store ${key}`);
|
||||
await indexedDBStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[StorageMigration] Error checking ${key}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a backup before performing migrations
|
||||
*/
|
||||
static async createPreMigrationBackup(): Promise<string> {
|
||||
console.log("[StorageMigration] Creating pre-migration backup...");
|
||||
|
||||
const backup: Record<string, any> = {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: "pre-migration",
|
||||
stores: {},
|
||||
};
|
||||
|
||||
const storeKeys = Object.values(StoreKey);
|
||||
|
||||
for (const key of storeKeys) {
|
||||
try {
|
||||
const localData = localStorage.getItem(key);
|
||||
const indexedData = await indexedDBStorage.getItem(key);
|
||||
|
||||
backup.stores[key] = {
|
||||
localStorage: localData,
|
||||
indexedDB: indexedData,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`[StorageMigration] Error backing up ${key}:`, error);
|
||||
backup.stores[key] = {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const backupString = JSON.stringify(backup);
|
||||
localStorage.setItem("pre-migration-backup", backupString);
|
||||
|
||||
console.log("[StorageMigration] Backup created successfully");
|
||||
return backupString;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-initialize when module is loaded (client-side only)
|
||||
if (typeof window !== "undefined") {
|
||||
// Wait for the page to load before initializing
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
StorageMigration.initialize().catch(console.error);
|
||||
});
|
||||
} else {
|
||||
StorageMigration.initialize().catch(console.error);
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,35 @@ export function createPersistStore<T extends object, M>(
|
||||
// Gán lại hàm onRehydrateStorage để đánh dấu đã hydrate khi khôi phục dữ liệu
|
||||
persistOptions.onRehydrateStorage = (state) => {
|
||||
oldOonRehydrateStorage?.(state);
|
||||
return () => state.setHasHydrated(true);
|
||||
return async (state, error) => {
|
||||
if (error) {
|
||||
console.error(
|
||||
`[Store] Hydration failed for ${persistOptions.name}:`,
|
||||
error,
|
||||
);
|
||||
} else {
|
||||
console.log(`[Store] Successfully hydrated ${persistOptions.name}`);
|
||||
|
||||
// Check IndexedDB health on first hydration
|
||||
if (typeof window !== "undefined") {
|
||||
try {
|
||||
const { indexedDBStorage } = await import("./indexedDB-storage");
|
||||
const health = await indexedDBStorage.checkHealth();
|
||||
if (!health.indexedDB) {
|
||||
console.warn(
|
||||
`[Store] IndexedDB unavailable for ${persistOptions.name}, using localStorage fallback`,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`[Store] Storage health check failed:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state) {
|
||||
state.setHasHydrated(true);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Tạo store với zustand, kết hợp các middleware và phương thức bổ sung
|
||||
|
||||
Reference in New Issue
Block a user