fix: recover IDB on closing and restore diagram

This commit is contained in:
dayuan.jiang
2026-01-27 12:07:35 +09:00
parent cc6f09615d
commit 8d92474f73
2 changed files with 91 additions and 40 deletions

View File

@@ -47,6 +47,33 @@ interface ChatSessionDB extends DBSchema {
// Database singleton
let dbPromise: Promise<IDBPDatabase<ChatSessionDB>> | null = null
const resetDBPromise = () => {
dbPromise = null
}
const isClosingError = (error: unknown): boolean => {
return (
error instanceof DOMException &&
error.name === "InvalidStateError" &&
/closing/i.test(error.message)
)
}
const withDB = async <T>(
action: (db: IDBPDatabase<ChatSessionDB>) => Promise<T>,
): Promise<T> => {
try {
const db = await getDB()
return await action(db)
} catch (error) {
if (isClosingError(error)) {
resetDBPromise()
const db = await getDB()
return await action(db)
}
throw error
}
}
async function getDB(): Promise<IDBPDatabase<ChatSessionDB>> {
if (!dbPromise) {
@@ -60,7 +87,23 @@ async function getDB(): Promise<IDBPDatabase<ChatSessionDB>> {
}
// Future migrations: if (oldVersion < 2) { ... }
},
terminated() {
resetDBPromise()
},
})
dbPromise
.then((db) => {
db.onversionchange = () => {
db.close()
resetDBPromise()
}
db.onclose = () => {
resetDBPromise()
}
})
.catch(() => {
resetDBPromise()
})
}
return dbPromise
}
@@ -92,27 +135,29 @@ export async function isIndexedDBUsable(): Promise<boolean> {
export async function getAllSessionMetadata(): Promise<SessionMetadata[]> {
if (!isIndexedDBAvailable()) return []
try {
const db = await getDB()
const tx = db.transaction(STORE_NAME, "readonly")
const index = tx.store.index("by-updated")
const metadata: SessionMetadata[] = []
return await withDB(async (db) => {
const tx = db.transaction(STORE_NAME, "readonly")
const index = tx.store.index("by-updated")
const metadata: SessionMetadata[] = []
// Use cursor to read only metadata fields (avoids loading full messages/XML)
let cursor = await index.openCursor(null, "prev") // newest first
while (cursor) {
const s = cursor.value
metadata.push({
id: s.id,
title: s.title,
createdAt: s.createdAt,
updatedAt: s.updatedAt,
messageCount: s.messages.length,
hasDiagram: !!s.diagramXml && s.diagramXml.trim().length > 0,
thumbnailDataUrl: s.thumbnailDataUrl,
})
cursor = await cursor.continue()
}
return metadata
// Use cursor to read only metadata fields (avoids loading full messages/XML)
let cursor = await index.openCursor(null, "prev") // newest first
while (cursor) {
const s = cursor.value
metadata.push({
id: s.id,
title: s.title,
createdAt: s.createdAt,
updatedAt: s.updatedAt,
messageCount: s.messages.length,
hasDiagram:
!!s.diagramXml && s.diagramXml.trim().length > 0,
thumbnailDataUrl: s.thumbnailDataUrl,
})
cursor = await cursor.continue()
}
return metadata
})
} catch (error) {
console.error("Failed to get session metadata:", error)
return []
@@ -122,8 +167,9 @@ export async function getAllSessionMetadata(): Promise<SessionMetadata[]> {
export async function getSession(id: string): Promise<ChatSession | null> {
if (!isIndexedDBAvailable()) return null
try {
const db = await getDB()
return (await db.get(STORE_NAME, id)) || null
return await withDB(async (db) => {
return (await db.get(STORE_NAME, id)) || null
})
} catch (error) {
console.error("Failed to get session:", error)
return null
@@ -133,8 +179,9 @@ export async function getSession(id: string): Promise<ChatSession | null> {
export async function saveSession(session: ChatSession): Promise<boolean> {
if (!isIndexedDBAvailable()) return false
try {
const db = await getDB()
await db.put(STORE_NAME, session)
await withDB(async (db) => {
await db.put(STORE_NAME, session)
})
return true
} catch (error) {
// Handle quota exceeded
@@ -146,8 +193,9 @@ export async function saveSession(session: ChatSession): Promise<boolean> {
await deleteOldestSession()
// Retry once
try {
const db = await getDB()
await db.put(STORE_NAME, session)
await withDB(async (db) => {
await db.put(STORE_NAME, session)
})
return true
} catch (retryError) {
console.error(
@@ -166,8 +214,9 @@ export async function saveSession(session: ChatSession): Promise<boolean> {
export async function deleteSession(id: string): Promise<void> {
if (!isIndexedDBAvailable()) return
try {
const db = await getDB()
await db.delete(STORE_NAME, id)
await withDB(async (db) => {
await db.delete(STORE_NAME, id)
})
} catch (error) {
console.error("Failed to delete session:", error)
}
@@ -176,8 +225,9 @@ export async function deleteSession(id: string): Promise<void> {
export async function getSessionCount(): Promise<number> {
if (!isIndexedDBAvailable()) return 0
try {
const db = await getDB()
return await db.count(STORE_NAME)
return await withDB(async (db) => {
return await db.count(STORE_NAME)
})
} catch (error) {
console.error("Failed to get session count:", error)
return 0
@@ -187,14 +237,15 @@ export async function getSessionCount(): Promise<number> {
export async function deleteOldestSession(): Promise<void> {
if (!isIndexedDBAvailable()) return
try {
const db = await getDB()
const tx = db.transaction(STORE_NAME, "readwrite")
const index = tx.store.index("by-updated")
const cursor = await index.openCursor()
if (cursor) {
await cursor.delete()
}
await tx.done
await withDB(async (db) => {
const tx = db.transaction(STORE_NAME, "readwrite")
const index = tx.store.index("by-updated")
const cursor = await index.openCursor()
if (cursor) {
await cursor.delete()
}
await tx.done
})
} catch (error) {
console.error("Failed to delete oldest session:", error)
}