mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-08-28 09:11:06 +00:00
fix: fix igdb issues at times
This commit is contained in:
@@ -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 };
|
||||
|
||||
/* ─────────── Progress‑meta 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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user