From e0ee41c270ee9a50f8196b5fb2c06540c8e47ba3 Mon Sep 17 00:00:00 2001 From: Aden Linday Date: Fri, 26 Jun 2026 21:33:10 +0930 Subject: [PATCH] fix: fix igdb issues at times --- .../built-in/globalSearch/src/core/index.ts | 11 +- .../built-in/globalSearch/src/indexing/db.ts | 329 +++++++++++------- .../globalSearch/src/indexing/indexer.ts | 20 +- .../globalSearch/src/indexing/resetIndexes.ts | 64 ++-- .../indexing/worker/vectorWorkerManager.ts | 12 + 5 files changed, 255 insertions(+), 181 deletions(-) diff --git a/src/plugins/built-in/globalSearch/src/core/index.ts b/src/plugins/built-in/globalSearch/src/core/index.ts index 45dcb524..d8b14fff 100644 --- a/src/plugins/built-in/globalSearch/src/core/index.ts +++ b/src/plugins/built-in/globalSearch/src/core/index.ts @@ -10,7 +10,7 @@ import { import { verboseDebug, verboseInfo, verboseLog } from "@/utils/verboseLog"; import styles from "./styles.css?inline"; import { waitForElm } from "@/seqta/utils/waitForElm"; -import { runIndexing } from "../indexing/indexer"; +import { runIndexing, ensureSchemaCurrent } from "../indexing/indexer"; import { initVectorSearch } from "../search/vector/vectorSearch"; import { cleanupSearchBar, mountSearchBar } from "./mountSearchBar"; import { IndexedDbManager } from "embeddia"; @@ -198,6 +198,15 @@ const globalSearchPlugin: Plugin = { } } + // Run schema migration before any IndexedDB connections are opened. + // If this runs later (during indexing), embeddiaDB and betterseqta-index + // may already be open and delete requests come back blocked. + try { + await ensureSchemaCurrent(); + } catch (error) { + console.warn("[Global Search] Schema check failed:", error); + } + try { await IndexedDbManager.create("embeddiaDB", "embeddiaObjectStore", { primaryKey: "id", diff --git a/src/plugins/built-in/globalSearch/src/indexing/db.ts b/src/plugins/built-in/globalSearch/src/indexing/db.ts index 6ca106ac..6f622d84 100644 --- a/src/plugins/built-in/globalSearch/src/indexing/db.ts +++ b/src/plugins/built-in/globalSearch/src/indexing/db.ts @@ -14,71 +14,177 @@ function updateVersion(version: number) { localStorage.setItem(VERSION_KEY, version.toString()); } +function invalidateConnection(): void { + if (cachedDb) { + cachedDb.close(); + cachedDb = null; + } + dbPromise = null; +} + +function attachConnection(db: IDBDatabase): void { + if (cachedDb && cachedDb !== db) { + cachedDb.close(); + } + cachedDb = db; + cachedDb.onclose = () => { + cachedDb = null; + dbPromise = null; + }; + updateVersion(db.version); +} + +function setupUpgradeHandler( + request: IDBOpenDBRequest, + extraStore?: string, +): void { + request.onupgradeneeded = (event) => { + const db = request.result; + + if (!Array.from(db.objectStoreNames).includes(META_STORE)) { + db.createObjectStore(META_STORE); + } + + if (extraStore && !db.objectStoreNames.contains(extraStore)) { + db.createObjectStore(extraStore); + } + + if (event.newVersion != null) { + updateVersion(event.newVersion); + } + }; +} + +function openAtVersion(version: number, extraStore?: string): Promise { + return new Promise((resolve, reject) => { + let request: IDBOpenDBRequest; + + try { + request = indexedDB.open(DB_NAME, version); + } catch (error) { + reject(error); + return; + } + + setupUpgradeHandler(request, extraStore); + + request.onsuccess = () => { + attachConnection(request.result); + resolve(request.result); + }; + + request.onerror = () => reject(request.error); + }); +} + +function openAtCurrentVersion(): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME); + + setupUpgradeHandler(request); + + request.onsuccess = () => { + attachConnection(request.result); + resolve(request.result); + }; + + request.onerror = () => reject(request.error); + }); +} + +function wipeDatabase(): Promise { + invalidateConnection(); + localStorage.removeItem(VERSION_KEY); + return deleteDatabaseWithRetries(DB_NAME); +} + +function deleteDatabaseWithRetries( + name: string, + maxAttempts = 6, +): Promise { + return new Promise((resolve) => { + const attemptDelete = (attempt: number) => { + let req: IDBOpenDBRequest; + try { + req = indexedDB.deleteDatabase(name); + } catch (error) { + console.warn(`[DB] Could not start delete of ${name}:`, error); + resolve(); + return; + } + + req.onsuccess = () => resolve(); + + req.onerror = () => { + console.warn(`[DB] Error deleting ${name}:`, req.error); + if (attempt + 1 < maxAttempts) { + setTimeout(() => attemptDelete(attempt + 1), 150 * (attempt + 1)); + return; + } + resolve(); + }; + + req.onblocked = () => { + console.warn( + `[DB] Delete of ${name} blocked (attempt ${attempt + 1}/${maxAttempts}); waiting for connections to close`, + ); + if (attempt + 1 < maxAttempts) { + setTimeout(() => attemptDelete(attempt + 1), 200 * (attempt + 1)); + return; + } + resolve(); + }; + }; + + attemptDelete(0); + }); +} + +export function closeSearchDatabase(): void { + invalidateConnection(); +} + +if (typeof window !== "undefined") { + window.addEventListener("betterseqta-reset-search-index", () => { + closeSearchDatabase(); + }); +} + +async function openDBInternal(): Promise { + const storedVersion = getCurrentVersion(); + + try { + return await openAtVersion(storedVersion); + } catch (error) { + const domError = error as DOMException | undefined; + + if (domError?.name === "VersionError") { + console.warn( + "[DB] localStorage version out of sync with IndexedDB; opening current version", + ); + invalidateConnection(); + try { + return await openAtCurrentVersion(); + } catch (fallbackError) { + console.warn("[DB] Fallback open failed, recreating database:", fallbackError); + } + } else { + console.error("Error opening database:", error); + } + + await wipeDatabase(); + return openAtVersion(1); + } +} + function openDB(): Promise { - if (cachedDb && cachedDb.version >= getCurrentVersion()) { + if (cachedDb) { return Promise.resolve(cachedDb); } if (dbPromise) return dbPromise; - const currentVersion = getCurrentVersion(); - - dbPromise = new Promise((resolve, reject) => { - let request: IDBOpenDBRequest; - - try { - request = indexedDB.open(DB_NAME, currentVersion); - } catch (e) { - console.warn("Database version conflict, recreating database..."); - if (cachedDb) { - cachedDb.close(); - cachedDb = null; - } - indexedDB.deleteDatabase(DB_NAME); - localStorage.removeItem(VERSION_KEY); - request = indexedDB.open(DB_NAME, 1); - updateVersion(1); - } - - request.onupgradeneeded = (event) => { - const db = request.result; - const existingStores = Array.from(db.objectStoreNames); - - if (!existingStores.includes(META_STORE)) { - db.createObjectStore(META_STORE); - } - - updateVersion(event.newVersion || 1); - }; - - request.onsuccess = () => { - if (cachedDb && cachedDb !== request.result) { - cachedDb.close(); - } - cachedDb = request.result; - - cachedDb.onclose = () => { - cachedDb = null; - dbPromise = null; - }; - - resolve(request.result); - }; - - request.onerror = () => { - console.error("Error opening database:", request.error); - - if (cachedDb) { - cachedDb.close(); - cachedDb = null; - } - indexedDB.deleteDatabase(DB_NAME); - localStorage.removeItem(VERSION_KEY); - dbPromise = null; - reject(request.error); - }; - }); - + dbPromise = openDBInternal(); return dbPromise; } @@ -97,45 +203,29 @@ async function getStore(store: string, mode: IDBTransactionMode = "readonly") { return tx.objectStore(store); } -function upgradeDB(newStore: string): Promise { - return new Promise((resolve, reject) => { - const currentVersion = getCurrentVersion(); - const newVersion = currentVersion + 1; +async function upgradeDB(newStore: string): Promise { + invalidateConnection(); - if (cachedDb) { - cachedDb.close(); - cachedDb = null; - } + let baseVersion = 0; + + try { + const db = await openAtCurrentVersion(); + baseVersion = db.version; + db.close(); + cachedDb = null; dbPromise = null; + } catch (error) { + console.warn("[DB] Could not probe database version before upgrade:", error); + } - const request = indexedDB.open(DB_NAME, newVersion); + const newVersion = baseVersion + 1; - request.onupgradeneeded = (event) => { - const db = request.result; - if (!db.objectStoreNames.contains(newStore)) { - db.createObjectStore(newStore); - } - - updateVersion(event.newVersion || newVersion); - }; - - request.onsuccess = () => { - cachedDb = request.result; - - cachedDb.onclose = () => { - cachedDb = null; - dbPromise = null; - }; - - dbPromise = Promise.resolve(request.result); - resolve(); - }; - - request.onerror = () => { - console.error("Error upgrading database:", request.error); - reject(request.error); - }; - }); + try { + await openAtVersion(newVersion, newStore); + } catch (error) { + console.error("Error upgrading database:", error); + throw error; + } } export async function getAll(store: string): Promise { @@ -263,54 +353,23 @@ export async function clear(store: string): Promise { } export async function resetDatabase(): Promise { - // Close cached database connection - if (cachedDb) { - try { - cachedDb.close(); - } catch (e) { - console.warn("[DB] Error closing cached database:", e); - } - cachedDb = null; - } - - // Close pending database promise if (dbPromise) { try { const db = await dbPromise; db.close(); - } catch (e) { + } catch { // Database might not be open yet, that's okay } - dbPromise = null; } - // Wait a bit for connections to fully close - await new Promise(resolve => setTimeout(resolve, 100)); + invalidateConnection(); - return new Promise((resolve, reject) => { - const req = indexedDB.deleteDatabase(DB_NAME); - req.onsuccess = () => { - localStorage.removeItem(VERSION_KEY); - resolve(); - }; - req.onerror = () => { - console.error("[DB] Error deleting database:", req.error); - reject(req.error); - }; - req.onblocked = () => { - console.warn("[DB] Database deletion blocked - waiting for connections to close"); - // Wait a bit longer and try again - setTimeout(() => { - const retryReq = indexedDB.deleteDatabase(DB_NAME); - retryReq.onsuccess = () => { - localStorage.removeItem(VERSION_KEY); - resolve(); - }; - retryReq.onerror = () => reject(retryReq.error); - retryReq.onblocked = () => { - reject(new Error(`Database is still open. Please close other tabs/windows and try again.`)); - }; - }, 500); - }; - }); + if (typeof window !== "undefined") { + window.dispatchEvent(new CustomEvent("betterseqta-reset-search-index")); + } + + await new Promise((resolve) => setTimeout(resolve, 200)); + + localStorage.removeItem(VERSION_KEY); + await deleteDatabaseWithRetries(DB_NAME); } diff --git a/src/plugins/built-in/globalSearch/src/indexing/indexer.ts b/src/plugins/built-in/globalSearch/src/indexing/indexer.ts index 8a489b5b..5a80310f 100644 --- a/src/plugins/built-in/globalSearch/src/indexing/indexer.ts +++ b/src/plugins/built-in/globalSearch/src/indexing/indexer.ts @@ -1,4 +1,4 @@ -import { applyStoreDiff, get, getAll, put, remove, resetDatabase } from "./db"; +import { applyStoreDiff, get, getAll, put, remove } from "./db"; import { jobs } from "./jobs"; import { decorateIndexItems } from "./renderComponents"; import type { IndexItem, Job, JobContext } from "./types"; @@ -6,6 +6,7 @@ import { VectorWorkerManager } from "./worker/vectorWorkerManager"; import { loadDynamicItems } from "../utils/dynamicItems"; import { getVectorizedItemIds, pruneOrphanVectorEmbeddings } from "./utils"; import { INDEX_SCHEMA_VERSION, SCHEMA_VERSION_KEY } from "./schemaVersion"; +import { resetSearchIndexes } from "./resetIndexes"; import { verboseDebug, verboseInfo, verboseLog } from '@/utils/verboseLog'; const META_STORE = "meta"; @@ -33,20 +34,9 @@ async function ensureSchemaCurrent(): Promise { ); try { - await resetDatabase(); + await resetSearchIndexes(); } catch (e) { - console.warn("[Indexer] Failed to reset structured database:", e); - } - - try { - await new Promise((resolve) => { - const req = indexedDB.deleteDatabase("embeddiaDB"); - req.onsuccess = () => resolve(); - req.onerror = () => resolve(); - req.onblocked = () => resolve(); - }); - } catch (e) { - console.warn("[Indexer] Failed to reset embeddiaDB:", e); + console.warn("[Indexer] Failed to reset search indexes:", e); } try { @@ -58,6 +48,8 @@ async function ensureSchemaCurrent(): Promise { return schemaCheckPromise; } +export { ensureSchemaCurrent }; + /* ─────────── Progress‑meta helpers ─────────── */ async function loadProgress(jobId: string): Promise { const rec = await get(META_STORE, `progress:${jobId}`); diff --git a/src/plugins/built-in/globalSearch/src/indexing/resetIndexes.ts b/src/plugins/built-in/globalSearch/src/indexing/resetIndexes.ts index c4d30b75..80fe39f1 100644 --- a/src/plugins/built-in/globalSearch/src/indexing/resetIndexes.ts +++ b/src/plugins/built-in/globalSearch/src/indexing/resetIndexes.ts @@ -34,49 +34,51 @@ const STRUCTURED_DB = "betterseqta-index"; const VECTOR_DB = "embeddiaDB"; const STRUCTURED_VERSION_KEY = "betterseqta-index-version"; -function deleteIndexedDb(name: string): Promise { - return new Promise((resolve) => { - let resolved = false; - const finish = () => { - if (resolved) return; - resolved = true; - resolve(); - }; +function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} +function tryDeleteDatabase( + name: string, +): Promise<"success" | "blocked" | "error"> { + return new Promise((resolve) => { let req: IDBOpenDBRequest; try { req = indexedDB.deleteDatabase(name); - } catch (e) { - console.warn(`[Reset] Could not start delete of ${name}:`, e); - finish(); + } catch (error) { + console.warn(`[Reset] Could not start delete of ${name}:`, error); + resolve("error"); return; } - req.onsuccess = () => finish(); + req.onsuccess = () => resolve("success"); req.onerror = () => { console.warn(`[Reset] Error deleting ${name}:`, req.error); - finish(); - }; - req.onblocked = () => { - // Connections are still open in another tab. Wait briefly, retry, - // then resolve regardless so we never hang the caller forever. - console.warn( - `[Reset] Delete of ${name} blocked; will retry then resolve.`, - ); - setTimeout(() => { - try { - const retry = indexedDB.deleteDatabase(name); - retry.onsuccess = () => finish(); - retry.onerror = () => finish(); - retry.onblocked = () => finish(); - } catch { - finish(); - } - }, 600); + resolve("error"); }; + req.onblocked = () => resolve("blocked"); }); } +async function deleteIndexedDb(name: string): Promise { + const maxAttempts = 6; + + for (let attempt = 0; attempt < maxAttempts; attempt++) { + const result = await tryDeleteDatabase(name); + if (result === "success") return; + + if (result === "blocked") { + console.warn( + `[Reset] Delete of ${name} blocked (attempt ${attempt + 1}/${maxAttempts}); waiting for connections to close`, + ); + } + + await delay(200 * (attempt + 1)); + } + + console.warn(`[Reset] Gave up deleting ${name} after ${maxAttempts} attempts`); +} + export async function resetSearchIndexes(): Promise { try { if (typeof window !== "undefined") { @@ -96,7 +98,7 @@ export async function resetSearchIndexes(): Promise { // Give listeners a tick to close any open IDB connections; otherwise // the delete request below comes back with `onblocked`. - await new Promise((resolve) => setTimeout(resolve, 150)); + await delay(300); await Promise.allSettled([ deleteIndexedDb(STRUCTURED_DB), diff --git a/src/plugins/built-in/globalSearch/src/indexing/worker/vectorWorkerManager.ts b/src/plugins/built-in/globalSearch/src/indexing/worker/vectorWorkerManager.ts index c1de4196..01bff330 100644 --- a/src/plugins/built-in/globalSearch/src/indexing/worker/vectorWorkerManager.ts +++ b/src/plugins/built-in/globalSearch/src/indexing/worker/vectorWorkerManager.ts @@ -13,6 +13,7 @@ export type ProgressCallback = (data: { export class VectorWorkerManager { private static instance: VectorWorkerManager; + private static resetListenerInstalled = false; private worker: Worker | null = null; private isInitialized = false; private readyPromise: Promise | null = null; @@ -42,6 +43,17 @@ export class VectorWorkerManager { verboseDebug("Creating new VectorWorkerManager instance"); VectorWorkerManager.instance = new VectorWorkerManager(); } + + if ( + !VectorWorkerManager.resetListenerInstalled && + typeof window !== "undefined" + ) { + VectorWorkerManager.resetListenerInstalled = true; + window.addEventListener("betterseqta-reset-search-index", () => { + VectorWorkerManager.getInstance().terminate(); + }); + } + return VectorWorkerManager.instance; }