fix: fix igdb issues at times

This commit is contained in:
2026-06-26 21:33:10 +09:30
parent e10c5fe2c4
commit e0ee41c270
5 changed files with 255 additions and 181 deletions
@@ -10,7 +10,7 @@ import {
import { verboseDebug, verboseInfo, verboseLog } from "@/utils/verboseLog"; import { verboseDebug, verboseInfo, verboseLog } from "@/utils/verboseLog";
import styles from "./styles.css?inline"; import styles from "./styles.css?inline";
import { waitForElm } from "@/seqta/utils/waitForElm"; import { waitForElm } from "@/seqta/utils/waitForElm";
import { runIndexing } from "../indexing/indexer"; import { runIndexing, ensureSchemaCurrent } from "../indexing/indexer";
import { initVectorSearch } from "../search/vector/vectorSearch"; import { initVectorSearch } from "../search/vector/vectorSearch";
import { cleanupSearchBar, mountSearchBar } from "./mountSearchBar"; import { cleanupSearchBar, mountSearchBar } from "./mountSearchBar";
import { IndexedDbManager } from "embeddia"; import { IndexedDbManager } from "embeddia";
@@ -198,6 +198,15 @@ const globalSearchPlugin: Plugin<typeof settings> = {
} }
} }
// 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 { try {
await IndexedDbManager.create("embeddiaDB", "embeddiaObjectStore", { await IndexedDbManager.create("embeddiaDB", "embeddiaObjectStore", {
primaryKey: "id", primaryKey: "id",
@@ -14,71 +14,177 @@ function updateVersion(version: number) {
localStorage.setItem(VERSION_KEY, version.toString()); 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<IDBDatabase> {
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<IDBDatabase> {
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<void> {
invalidateConnection();
localStorage.removeItem(VERSION_KEY);
return deleteDatabaseWithRetries(DB_NAME);
}
function deleteDatabaseWithRetries(
name: string,
maxAttempts = 6,
): Promise<void> {
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<IDBDatabase> {
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<IDBDatabase> { function openDB(): Promise<IDBDatabase> {
if (cachedDb && cachedDb.version >= getCurrentVersion()) { if (cachedDb) {
return Promise.resolve(cachedDb); return Promise.resolve(cachedDb);
} }
if (dbPromise) return dbPromise; if (dbPromise) return dbPromise;
const currentVersion = getCurrentVersion(); dbPromise = openDBInternal();
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);
};
});
return dbPromise; return dbPromise;
} }
@@ -97,45 +203,29 @@ async function getStore(store: string, mode: IDBTransactionMode = "readonly") {
return tx.objectStore(store); return tx.objectStore(store);
} }
function upgradeDB(newStore: string): Promise<void> { async function upgradeDB(newStore: string): Promise<void> {
return new Promise((resolve, reject) => { invalidateConnection();
const currentVersion = getCurrentVersion();
const newVersion = currentVersion + 1;
if (cachedDb) { let baseVersion = 0;
cachedDb.close();
cachedDb = null;
}
dbPromise = null;
const request = indexedDB.open(DB_NAME, newVersion); try {
const db = await openAtCurrentVersion();
request.onupgradeneeded = (event) => { baseVersion = db.version;
const db = request.result; db.close();
if (!db.objectStoreNames.contains(newStore)) {
db.createObjectStore(newStore);
}
updateVersion(event.newVersion || newVersion);
};
request.onsuccess = () => {
cachedDb = request.result;
cachedDb.onclose = () => {
cachedDb = null; cachedDb = null;
dbPromise = null; dbPromise = null;
}; } catch (error) {
console.warn("[DB] Could not probe database version before upgrade:", error);
}
dbPromise = Promise.resolve(request.result); const newVersion = baseVersion + 1;
resolve();
};
request.onerror = () => { try {
console.error("Error upgrading database:", request.error); await openAtVersion(newVersion, newStore);
reject(request.error); } catch (error) {
}; console.error("Error upgrading database:", error);
}); throw error;
}
} }
export async function getAll(store: string): Promise<any[]> { export async function getAll(store: string): Promise<any[]> {
@@ -263,54 +353,23 @@ export async function clear(store: string): Promise<void> {
} }
export async function resetDatabase(): Promise<void> { export async function resetDatabase(): Promise<void> {
// 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) { if (dbPromise) {
try { try {
const db = await dbPromise; const db = await dbPromise;
db.close(); db.close();
} catch (e) { } catch {
// Database might not be open yet, that's okay // Database might not be open yet, that's okay
} }
dbPromise = null;
} }
// Wait a bit for connections to fully close invalidateConnection();
await new Promise(resolve => setTimeout(resolve, 100));
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("betterseqta-reset-search-index"));
}
await new Promise((resolve) => setTimeout(resolve, 200));
return new Promise((resolve, reject) => {
const req = indexedDB.deleteDatabase(DB_NAME);
req.onsuccess = () => {
localStorage.removeItem(VERSION_KEY); localStorage.removeItem(VERSION_KEY);
resolve(); await deleteDatabaseWithRetries(DB_NAME);
};
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);
};
});
} }
@@ -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 { jobs } from "./jobs";
import { decorateIndexItems } from "./renderComponents"; import { decorateIndexItems } from "./renderComponents";
import type { IndexItem, Job, JobContext } from "./types"; import type { IndexItem, Job, JobContext } from "./types";
@@ -6,6 +6,7 @@ import { VectorWorkerManager } from "./worker/vectorWorkerManager";
import { loadDynamicItems } from "../utils/dynamicItems"; import { loadDynamicItems } from "../utils/dynamicItems";
import { getVectorizedItemIds, pruneOrphanVectorEmbeddings } from "./utils"; import { getVectorizedItemIds, pruneOrphanVectorEmbeddings } from "./utils";
import { INDEX_SCHEMA_VERSION, SCHEMA_VERSION_KEY } from "./schemaVersion"; import { INDEX_SCHEMA_VERSION, SCHEMA_VERSION_KEY } from "./schemaVersion";
import { resetSearchIndexes } from "./resetIndexes";
import { verboseDebug, verboseInfo, verboseLog } from '@/utils/verboseLog'; import { verboseDebug, verboseInfo, verboseLog } from '@/utils/verboseLog';
const META_STORE = "meta"; const META_STORE = "meta";
@@ -33,20 +34,9 @@ async function ensureSchemaCurrent(): Promise<void> {
); );
try { try {
await resetDatabase(); await resetSearchIndexes();
} catch (e) { } catch (e) {
console.warn("[Indexer] Failed to reset structured database:", e); console.warn("[Indexer] Failed to reset search indexes:", e);
}
try {
await new Promise<void>((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);
} }
try { try {
@@ -58,6 +48,8 @@ async function ensureSchemaCurrent(): Promise<void> {
return schemaCheckPromise; return schemaCheckPromise;
} }
export { ensureSchemaCurrent };
/* ─────────── Progressmeta helpers ─────────── */ /* ─────────── Progressmeta helpers ─────────── */
async function loadProgress<T = any>(jobId: string): Promise<T | undefined> { async function loadProgress<T = any>(jobId: string): Promise<T | undefined> {
const rec = await get(META_STORE, `progress:${jobId}`); const rec = await get(META_STORE, `progress:${jobId}`);
@@ -34,49 +34,51 @@ const STRUCTURED_DB = "betterseqta-index";
const VECTOR_DB = "embeddiaDB"; const VECTOR_DB = "embeddiaDB";
const STRUCTURED_VERSION_KEY = "betterseqta-index-version"; const STRUCTURED_VERSION_KEY = "betterseqta-index-version";
function deleteIndexedDb(name: string): Promise<void> { function delay(ms: number): Promise<void> {
return new Promise((resolve) => { return new Promise((resolve) => setTimeout(resolve, ms));
let resolved = false; }
const finish = () => {
if (resolved) return;
resolved = true;
resolve();
};
function tryDeleteDatabase(
name: string,
): Promise<"success" | "blocked" | "error"> {
return new Promise((resolve) => {
let req: IDBOpenDBRequest; let req: IDBOpenDBRequest;
try { try {
req = indexedDB.deleteDatabase(name); req = indexedDB.deleteDatabase(name);
} catch (e) { } catch (error) {
console.warn(`[Reset] Could not start delete of ${name}:`, e); console.warn(`[Reset] Could not start delete of ${name}:`, error);
finish(); resolve("error");
return; return;
} }
req.onsuccess = () => finish(); req.onsuccess = () => resolve("success");
req.onerror = () => { req.onerror = () => {
console.warn(`[Reset] Error deleting ${name}:`, req.error); console.warn(`[Reset] Error deleting ${name}:`, req.error);
finish(); resolve("error");
};
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);
}; };
req.onblocked = () => resolve("blocked");
}); });
} }
async function deleteIndexedDb(name: string): Promise<void> {
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<void> { export async function resetSearchIndexes(): Promise<void> {
try { try {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
@@ -96,7 +98,7 @@ export async function resetSearchIndexes(): Promise<void> {
// Give listeners a tick to close any open IDB connections; otherwise // Give listeners a tick to close any open IDB connections; otherwise
// the delete request below comes back with `onblocked`. // the delete request below comes back with `onblocked`.
await new Promise<void>((resolve) => setTimeout(resolve, 150)); await delay(300);
await Promise.allSettled([ await Promise.allSettled([
deleteIndexedDb(STRUCTURED_DB), deleteIndexedDb(STRUCTURED_DB),
@@ -13,6 +13,7 @@ export type ProgressCallback = (data: {
export class VectorWorkerManager { export class VectorWorkerManager {
private static instance: VectorWorkerManager; private static instance: VectorWorkerManager;
private static resetListenerInstalled = false;
private worker: Worker | null = null; private worker: Worker | null = null;
private isInitialized = false; private isInitialized = false;
private readyPromise: Promise<void> | null = null; private readyPromise: Promise<void> | null = null;
@@ -42,6 +43,17 @@ export class VectorWorkerManager {
verboseDebug("Creating new VectorWorkerManager instance"); verboseDebug("Creating new VectorWorkerManager instance");
VectorWorkerManager.instance = new VectorWorkerManager(); 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; return VectorWorkerManager.instance;
} }