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 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<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 {
await IndexedDbManager.create("embeddiaDB", "embeddiaObjectStore", {
primaryKey: "id",
@@ -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<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> {
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<void> {
return new Promise((resolve, reject) => {
const currentVersion = getCurrentVersion();
const newVersion = currentVersion + 1;
async function upgradeDB(newStore: string): Promise<void> {
invalidateConnection();
if (cachedDb) {
cachedDb.close();
cachedDb = null;
}
dbPromise = null;
let baseVersion = 0;
const request = indexedDB.open(DB_NAME, newVersion);
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 = () => {
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);
}
dbPromise = Promise.resolve(request.result);
resolve();
};
const newVersion = baseVersion + 1;
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<any[]> {
@@ -263,54 +353,23 @@ export async function clear(store: string): 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) {
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();
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);
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);
};
});
await deleteDatabaseWithRetries(DB_NAME);
}
@@ -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<void> {
);
try {
await resetDatabase();
await resetSearchIndexes();
} catch (e) {
console.warn("[Indexer] Failed to reset structured database:", 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);
console.warn("[Indexer] Failed to reset search indexes:", e);
}
try {
@@ -58,6 +48,8 @@ async function ensureSchemaCurrent(): Promise<void> {
return schemaCheckPromise;
}
export { ensureSchemaCurrent };
/* ─────────── Progressmeta helpers ─────────── */
async function loadProgress<T = any>(jobId: string): Promise<T | undefined> {
const rec = await get(META_STORE, `progress:${jobId}`);
@@ -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<void> {
return new Promise((resolve) => {
let resolved = false;
const finish = () => {
if (resolved) return;
resolved = true;
resolve();
};
function delay(ms: number): Promise<void> {
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<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> {
try {
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
// the delete request below comes back with `onblocked`.
await new Promise<void>((resolve) => setTimeout(resolve, 150));
await delay(300);
await Promise.allSettled([
deleteIndexedDb(STRUCTURED_DB),
@@ -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<void> | 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;
}