mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-05 19:24:39 +00:00
feat: global search bug fixes and performance improvements
This commit is contained in:
@@ -24,7 +24,6 @@
|
|||||||
searchHotkey: string
|
searchHotkey: string
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// Make searchHotkey reactive to setting changes
|
|
||||||
let currentSearchHotkey = $state(initialSearchHotkey);
|
let currentSearchHotkey = $state(initialSearchHotkey);
|
||||||
|
|
||||||
let commandsFuse = $state<Fuse<StaticCommandItem>>();
|
let commandsFuse = $state<Fuse<StaticCommandItem>>();
|
||||||
@@ -177,7 +176,7 @@
|
|||||||
isLoading = false;
|
isLoading = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const debouncedPerformSearch = debounce(performSearch, 10);
|
const debouncedPerformSearch = debounce(performSearch, 200);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (commandPalleteOpen) {
|
if (commandPalleteOpen) {
|
||||||
|
|||||||
@@ -126,6 +126,19 @@ const globalSearchPlugin: Plugin<typeof settings> = {
|
|||||||
|
|
||||||
initVectorSearch();
|
initVectorSearch();
|
||||||
|
|
||||||
|
// Warm up vector worker in background to improve initial response time
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const workerManager = VectorWorkerManager.getInstance();
|
||||||
|
console.debug("[Global Search] Warming up vector worker...");
|
||||||
|
// Just ensure the worker is ready, don't process anything yet
|
||||||
|
await workerManager.processItems([], () => {});
|
||||||
|
console.debug("[Global Search] Vector worker warmed up successfully");
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[Global Search] Vector worker warm-up failed:", error);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
// Add debug helpers to window for troubleshooting
|
// Add debug helpers to window for troubleshooting
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.globalSearchDebug = {
|
window.globalSearchDebug = {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import browser from "webextension-polyfill";
|
|||||||
export function mountSearchBar(
|
export function mountSearchBar(
|
||||||
titleElement: Element,
|
titleElement: Element,
|
||||||
api: any,
|
api: any,
|
||||||
appRef: { current: any },
|
appRef: { current: any; storageChangeHandler?: any },
|
||||||
) {
|
) {
|
||||||
if (titleElement.querySelector(".search-trigger")) {
|
if (titleElement.querySelector(".search-trigger")) {
|
||||||
return;
|
return;
|
||||||
@@ -49,6 +49,9 @@ export function mountSearchBar(
|
|||||||
|
|
||||||
browser.storage.onChanged.addListener(handleStorageChange);
|
browser.storage.onChanged.addListener(handleStorageChange);
|
||||||
|
|
||||||
|
// Store reference to cleanup function for proper removal
|
||||||
|
appRef.storageChangeHandler = handleStorageChange;
|
||||||
|
|
||||||
const searchRoot = document.createElement("div");
|
const searchRoot = document.createElement("div");
|
||||||
document.body.appendChild(searchRoot);
|
document.body.appendChild(searchRoot);
|
||||||
const searchRootShadow = searchRoot.attachShadow({ mode: "open" });
|
const searchRootShadow = searchRoot.attachShadow({ mode: "open" });
|
||||||
@@ -69,7 +72,7 @@ export function mountSearchBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cleanupSearchBar(appRef: { current: any }) {
|
export function cleanupSearchBar(appRef: { current: any; storageChangeHandler?: any }) {
|
||||||
if (appRef.current) {
|
if (appRef.current) {
|
||||||
try {
|
try {
|
||||||
unmount(appRef.current);
|
unmount(appRef.current);
|
||||||
@@ -94,6 +97,8 @@ export function cleanupSearchBar(appRef: { current: any }) {
|
|||||||
// Clean up vector worker
|
// Clean up vector worker
|
||||||
VectorWorkerManager.getInstance().terminate();
|
VectorWorkerManager.getInstance().terminate();
|
||||||
|
|
||||||
// Remove storage listener
|
if (appRef.storageChangeHandler) {
|
||||||
browser.storage.onChanged.removeListener(() => {});
|
browser.storage.onChanged.removeListener(appRef.storageChangeHandler);
|
||||||
|
appRef.storageChangeHandler = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,19 +3,22 @@ const META_STORE = "meta";
|
|||||||
const VERSION_KEY = "betterseqta-index-version";
|
const VERSION_KEY = "betterseqta-index-version";
|
||||||
|
|
||||||
let dbPromise: Promise<IDBDatabase> | null = null;
|
let dbPromise: Promise<IDBDatabase> | null = null;
|
||||||
|
let cachedDb: IDBDatabase | null = null;
|
||||||
|
|
||||||
// Get the current version from localStorage or start at 1
|
|
||||||
function getCurrentVersion(): number {
|
function getCurrentVersion(): number {
|
||||||
const storedVersion = localStorage.getItem(VERSION_KEY);
|
const storedVersion = localStorage.getItem(VERSION_KEY);
|
||||||
return storedVersion ? parseInt(storedVersion, 10) : 1;
|
return storedVersion ? parseInt(storedVersion, 10) : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the version in localStorage
|
|
||||||
function updateVersion(version: number) {
|
function updateVersion(version: number) {
|
||||||
localStorage.setItem(VERSION_KEY, version.toString());
|
localStorage.setItem(VERSION_KEY, version.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
function openDB(): Promise<IDBDatabase> {
|
function openDB(): Promise<IDBDatabase> {
|
||||||
|
if (cachedDb && cachedDb.version >= getCurrentVersion()) {
|
||||||
|
return Promise.resolve(cachedDb);
|
||||||
|
}
|
||||||
|
|
||||||
if (dbPromise) return dbPromise;
|
if (dbPromise) return dbPromise;
|
||||||
|
|
||||||
const currentVersion = getCurrentVersion();
|
const currentVersion = getCurrentVersion();
|
||||||
@@ -26,8 +29,11 @@ function openDB(): Promise<IDBDatabase> {
|
|||||||
try {
|
try {
|
||||||
request = indexedDB.open(DB_NAME, currentVersion);
|
request = indexedDB.open(DB_NAME, currentVersion);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// If there's a version error, try to delete the database and start fresh
|
|
||||||
console.warn("Database version conflict, recreating database...");
|
console.warn("Database version conflict, recreating database...");
|
||||||
|
if (cachedDb) {
|
||||||
|
cachedDb.close();
|
||||||
|
cachedDb = null;
|
||||||
|
}
|
||||||
indexedDB.deleteDatabase(DB_NAME);
|
indexedDB.deleteDatabase(DB_NAME);
|
||||||
localStorage.removeItem(VERSION_KEY);
|
localStorage.removeItem(VERSION_KEY);
|
||||||
request = indexedDB.open(DB_NAME, 1);
|
request = indexedDB.open(DB_NAME, 1);
|
||||||
@@ -38,22 +44,37 @@ function openDB(): Promise<IDBDatabase> {
|
|||||||
const db = request.result;
|
const db = request.result;
|
||||||
const existingStores = Array.from(db.objectStoreNames);
|
const existingStores = Array.from(db.objectStoreNames);
|
||||||
|
|
||||||
// Always ensure META_STORE exists
|
|
||||||
if (!existingStores.includes(META_STORE)) {
|
if (!existingStores.includes(META_STORE)) {
|
||||||
db.createObjectStore(META_STORE);
|
db.createObjectStore(META_STORE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update version in localStorage to match the database
|
|
||||||
updateVersion(event.newVersion || 1);
|
updateVersion(event.newVersion || 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
request.onsuccess = () => resolve(request.result);
|
request.onsuccess = () => {
|
||||||
|
if (cachedDb && cachedDb !== request.result) {
|
||||||
|
cachedDb.close();
|
||||||
|
}
|
||||||
|
cachedDb = request.result;
|
||||||
|
|
||||||
|
cachedDb.onclose = () => {
|
||||||
|
cachedDb = null;
|
||||||
|
dbPromise = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
resolve(request.result);
|
||||||
|
};
|
||||||
|
|
||||||
request.onerror = () => {
|
request.onerror = () => {
|
||||||
console.error("Error opening database:", request.error);
|
console.error("Error opening database:", request.error);
|
||||||
// If there's an error, try to recover by deleting and recreating
|
|
||||||
|
if (cachedDb) {
|
||||||
|
cachedDb.close();
|
||||||
|
cachedDb = null;
|
||||||
|
}
|
||||||
indexedDB.deleteDatabase(DB_NAME);
|
indexedDB.deleteDatabase(DB_NAME);
|
||||||
localStorage.removeItem(VERSION_KEY);
|
localStorage.removeItem(VERSION_KEY);
|
||||||
|
dbPromise = null;
|
||||||
reject(request.error);
|
reject(request.error);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -64,11 +85,12 @@ function openDB(): Promise<IDBDatabase> {
|
|||||||
async function getStore(store: string, mode: IDBTransactionMode = "readonly") {
|
async function getStore(store: string, mode: IDBTransactionMode = "readonly") {
|
||||||
const db = await openDB();
|
const db = await openDB();
|
||||||
|
|
||||||
// Create store dynamically if needed
|
|
||||||
if (!db.objectStoreNames.contains(store)) {
|
if (!db.objectStoreNames.contains(store)) {
|
||||||
db.close();
|
|
||||||
await upgradeDB(store);
|
await upgradeDB(store);
|
||||||
return getStore(store, mode);
|
|
||||||
|
const upgradedDb = await openDB();
|
||||||
|
const tx = upgradedDb.transaction(store, mode);
|
||||||
|
return tx.objectStore(store);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tx = db.transaction(store, mode);
|
const tx = db.transaction(store, mode);
|
||||||
@@ -80,11 +102,11 @@ function upgradeDB(newStore: string): Promise<void> {
|
|||||||
const currentVersion = getCurrentVersion();
|
const currentVersion = getCurrentVersion();
|
||||||
const newVersion = currentVersion + 1;
|
const newVersion = currentVersion + 1;
|
||||||
|
|
||||||
// Close any existing connections
|
if (cachedDb) {
|
||||||
if (dbPromise) {
|
cachedDb.close();
|
||||||
dbPromise.then((db) => db.close());
|
cachedDb = null;
|
||||||
dbPromise = null;
|
|
||||||
}
|
}
|
||||||
|
dbPromise = null;
|
||||||
|
|
||||||
const request = indexedDB.open(DB_NAME, newVersion);
|
const request = indexedDB.open(DB_NAME, newVersion);
|
||||||
|
|
||||||
@@ -93,11 +115,18 @@ function upgradeDB(newStore: string): Promise<void> {
|
|||||||
if (!db.objectStoreNames.contains(newStore)) {
|
if (!db.objectStoreNames.contains(newStore)) {
|
||||||
db.createObjectStore(newStore);
|
db.createObjectStore(newStore);
|
||||||
}
|
}
|
||||||
// Update version in localStorage
|
|
||||||
updateVersion(event.newVersion || newVersion);
|
updateVersion(event.newVersion || newVersion);
|
||||||
};
|
};
|
||||||
|
|
||||||
request.onsuccess = () => {
|
request.onsuccess = () => {
|
||||||
|
cachedDb = request.result;
|
||||||
|
|
||||||
|
cachedDb.onclose = () => {
|
||||||
|
cachedDb = null;
|
||||||
|
dbPromise = null;
|
||||||
|
};
|
||||||
|
|
||||||
dbPromise = Promise.resolve(request.result);
|
dbPromise = Promise.resolve(request.result);
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
@@ -183,11 +212,17 @@ export async function clear(store: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to reset the database if needed
|
|
||||||
export async function resetDatabase(): Promise<void> {
|
export async function resetDatabase(): Promise<void> {
|
||||||
|
if (cachedDb) {
|
||||||
|
cachedDb.close();
|
||||||
|
cachedDb = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (dbPromise) {
|
if (dbPromise) {
|
||||||
const db = await dbPromise;
|
try {
|
||||||
db.close();
|
const db = await dbPromise;
|
||||||
|
db.close();
|
||||||
|
} catch (e) {}
|
||||||
dbPromise = null;
|
dbPromise = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -276,116 +276,102 @@ export async function runIndexing(): Promise<void> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasStreamingJobs) {
|
let allItemsInPrimaryStores = await loadAllStoredItems();
|
||||||
const allItemsInPrimaryStores = await loadAllStoredItems();
|
|
||||||
|
|
||||||
if (allItemsInPrimaryStores.length > 0) {
|
if (allItemsInPrimaryStores.length > 0) {
|
||||||
console.debug(
|
console.debug(
|
||||||
`%c[Indexer] Sending ${allItemsInPrimaryStores.length} items from primary stores to worker for vectorization check...`,
|
`%c[Indexer] Sending ${allItemsInPrimaryStores.length} items from primary stores to worker for vectorization check...`,
|
||||||
"color: #4ea1ff",
|
"color: #4ea1ff",
|
||||||
);
|
);
|
||||||
dispatchProgress(completedJobs, totalSteps, true, "Starting vectorization of stored items");
|
dispatchProgress(completedJobs, totalSteps, true, "Starting vectorization of stored items");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const workerManager = VectorWorkerManager.getInstance();
|
const workerManager = VectorWorkerManager.getInstance();
|
||||||
await workerManager.processItems(allItemsInPrimaryStores, (progress) => {
|
await workerManager.processItems(allItemsInPrimaryStores, (progress) => {
|
||||||
let detailMessage = progress.message || "";
|
let detailMessage = progress.message || "";
|
||||||
if (
|
if (
|
||||||
progress.status === "processing" &&
|
progress.status === "processing" &&
|
||||||
progress.total &&
|
progress.total &&
|
||||||
progress.processed !== undefined
|
progress.processed !== undefined
|
||||||
) {
|
) {
|
||||||
detailMessage = `Vectorizing: ${progress.processed} / ${progress.total}`;
|
detailMessage = `Vectorizing: ${progress.processed} / ${progress.total}`;
|
||||||
} else if (progress.status === "complete") {
|
} else if (progress.status === "complete") {
|
||||||
detailMessage = "Vectorization complete";
|
detailMessage = "Vectorization complete";
|
||||||
completedJobs++;
|
completedJobs++;
|
||||||
|
dispatchProgress(
|
||||||
|
completedJobs,
|
||||||
|
totalSteps,
|
||||||
|
false,
|
||||||
|
"Indexing finished",
|
||||||
|
detailMessage
|
||||||
|
);
|
||||||
|
} else if (progress.status === "error") {
|
||||||
|
detailMessage = `Vectorization error: ${progress.message}`;
|
||||||
|
dispatchProgress(
|
||||||
|
completedJobs,
|
||||||
|
totalSteps,
|
||||||
|
false,
|
||||||
|
"Vectorization failed",
|
||||||
|
detailMessage,
|
||||||
|
);
|
||||||
|
} else if (progress.status === "started") {
|
||||||
|
detailMessage = `Vectorization started for ${progress.total} items`;
|
||||||
|
} else if (progress.status === "cancelled") {
|
||||||
|
detailMessage = `Vectorization cancelled: ${progress.message}`;
|
||||||
|
dispatchProgress(
|
||||||
|
completedJobs,
|
||||||
|
totalSteps,
|
||||||
|
false,
|
||||||
|
"Vectorization cancelled",
|
||||||
|
detailMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress.status !== "complete" && progress.status !== "error" && progress.status !== "cancelled") {
|
||||||
dispatchProgress(
|
dispatchProgress(
|
||||||
completedJobs,
|
completedJobs,
|
||||||
totalSteps,
|
totalSteps,
|
||||||
false,
|
true,
|
||||||
"Indexing finished",
|
"Vectorization in progress",
|
||||||
detailMessage
|
|
||||||
);
|
|
||||||
} else if (progress.status === "error") {
|
|
||||||
detailMessage = `Vectorization error: ${progress.message}`;
|
|
||||||
dispatchProgress(
|
|
||||||
completedJobs,
|
|
||||||
totalSteps,
|
|
||||||
false,
|
|
||||||
"Vectorization failed",
|
|
||||||
detailMessage,
|
detailMessage,
|
||||||
);
|
);
|
||||||
} else if (progress.status === "started") {
|
}
|
||||||
detailMessage = `Vectorization started for ${progress.total} items`;
|
});
|
||||||
} else if (progress.status === "cancelled") {
|
|
||||||
detailMessage = `Vectorization cancelled: ${progress.message}`;
|
|
||||||
dispatchProgress(
|
|
||||||
completedJobs,
|
|
||||||
totalSteps,
|
|
||||||
false,
|
|
||||||
"Vectorization cancelled",
|
|
||||||
detailMessage,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progress.status !== "complete" && progress.status !== "error" && progress.status !== "cancelled") {
|
|
||||||
dispatchProgress(
|
|
||||||
completedJobs,
|
|
||||||
totalSteps,
|
|
||||||
true,
|
|
||||||
"Vectorization in progress",
|
|
||||||
detailMessage,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
console.debug(
|
|
||||||
"%c[Indexer] Vectorization task for stored items sent to worker.",
|
|
||||||
"color: green",
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
`%c[Indexer] ❌ Failed to send items to vector worker:`,
|
|
||||||
"color: red",
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
dispatchProgress(
|
|
||||||
completedJobs,
|
|
||||||
totalSteps,
|
|
||||||
false,
|
|
||||||
"Vectorization failed",
|
|
||||||
String(error),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.debug(
|
console.debug(
|
||||||
"%c[Indexer] No items found in primary stores to send for vectorization.",
|
"%c[Indexer] Vectorization task for stored items sent to worker.",
|
||||||
"color: gray",
|
"color: green",
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`%c[Indexer] ❌ Failed to send items to vector worker:`,
|
||||||
|
"color: red",
|
||||||
|
error,
|
||||||
);
|
);
|
||||||
completedJobs++;
|
|
||||||
dispatchProgress(
|
dispatchProgress(
|
||||||
completedJobs,
|
completedJobs,
|
||||||
totalSteps,
|
totalSteps,
|
||||||
false,
|
false,
|
||||||
"Indexing finished (no items for vectorization)",
|
"Vectorization failed",
|
||||||
|
String(error),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.debug(
|
console.debug(
|
||||||
"%c[Indexer] Skipping bulk vectorization - streaming jobs will handle vectorization",
|
"%c[Indexer] No items found in primary stores to send for vectorization.",
|
||||||
"color: #4ea1ff",
|
"color: gray",
|
||||||
);
|
);
|
||||||
completedJobs++;
|
completedJobs++;
|
||||||
dispatchProgress(
|
dispatchProgress(
|
||||||
completedJobs,
|
completedJobs,
|
||||||
totalSteps,
|
totalSteps,
|
||||||
false,
|
false,
|
||||||
"Indexing finished (streaming vectorization active)",
|
"Indexing finished (no items for vectorization)",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
stopHeartbeat();
|
stopHeartbeat();
|
||||||
|
|
||||||
const allItemsInPrimaryStores = await loadAllStoredItems();
|
allItemsInPrimaryStores = await loadAllStoredItems();
|
||||||
allItemsInPrimaryStores.forEach(item => {
|
allItemsInPrimaryStores.forEach(item => {
|
||||||
const jobDef = jobs[item.category] || Object.values(jobs).find(j => j.id === item.category) || jobs[item.renderComponentId];
|
const jobDef = jobs[item.category] || Object.values(jobs).find(j => j.id === item.category) || jobs[item.renderComponentId];
|
||||||
if (jobDef) {
|
if (jobDef) {
|
||||||
|
|||||||
@@ -8,18 +8,20 @@ import { renderComponentMap } from "../renderComponents";
|
|||||||
import { jobs } from "../jobs";
|
import { jobs } from "../jobs";
|
||||||
|
|
||||||
const RATE_LIMIT_CONFIG = {
|
const RATE_LIMIT_CONFIG = {
|
||||||
minDelay: 50,
|
minDelay: 30,
|
||||||
maxDelay: 5000,
|
maxDelay: 3000,
|
||||||
baseDelay: 200,
|
baseDelay: 150,
|
||||||
backoffMultiplier: 1.5,
|
backoffMultiplier: 1.3,
|
||||||
maxRetries: 3,
|
maxRetries: 3,
|
||||||
adaptiveBatchSize: true,
|
adaptiveBatchSize: true,
|
||||||
minBatchSize: 10,
|
minBatchSize: 15,
|
||||||
maxBatchSize: 100,
|
maxBatchSize: 150,
|
||||||
baseBatchSize: 50,
|
baseBatchSize: 75,
|
||||||
vectorBatchSize: 5,
|
vectorBatchSize: 10,
|
||||||
parallelRequests: 5,
|
parallelRequests: 8,
|
||||||
parallelDelay: 100,
|
parallelDelay: 50,
|
||||||
|
circuitBreakerThreshold: 5,
|
||||||
|
circuitBreakerResetTime: 30000,
|
||||||
};
|
};
|
||||||
|
|
||||||
interface MessagesProgress {
|
interface MessagesProgress {
|
||||||
@@ -33,6 +35,9 @@ interface MessagesProgress {
|
|||||||
processedIds: string[];
|
processedIds: string[];
|
||||||
streamingStarted: boolean;
|
streamingStarted: boolean;
|
||||||
totalEstimated: number;
|
totalEstimated: number;
|
||||||
|
circuitBreakerOpen: boolean;
|
||||||
|
circuitBreakerOpenTime: number;
|
||||||
|
consecutiveFailures: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchMessages = async (offset = 0, limit = 100) => {
|
const fetchMessages = async (offset = 0, limit = 100) => {
|
||||||
@@ -99,50 +104,38 @@ function calculateAdaptiveDelay(
|
|||||||
progress: MessagesProgress,
|
progress: MessagesProgress,
|
||||||
responseTime: number,
|
responseTime: number,
|
||||||
): number {
|
): number {
|
||||||
const { currentDelay, failedRequests, lastSuccessTime } = progress;
|
const {
|
||||||
|
currentDelay,
|
||||||
|
failedRequests,
|
||||||
|
lastSuccessTime,
|
||||||
|
circuitBreakerOpen,
|
||||||
|
consecutiveFailures,
|
||||||
|
} = progress;
|
||||||
const timeSinceLastSuccess = Date.now() - lastSuccessTime;
|
const timeSinceLastSuccess = Date.now() - lastSuccessTime;
|
||||||
|
|
||||||
if (failedRequests > 0 || responseTime > 2000) {
|
if (circuitBreakerOpen) {
|
||||||
|
return RATE_LIMIT_CONFIG.maxDelay;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (consecutiveFailures > 2 || failedRequests > 3 || responseTime > 3000) {
|
||||||
return Math.min(
|
return Math.min(
|
||||||
currentDelay * RATE_LIMIT_CONFIG.backoffMultiplier,
|
currentDelay *
|
||||||
|
(RATE_LIMIT_CONFIG.backoffMultiplier + consecutiveFailures * 0.2),
|
||||||
RATE_LIMIT_CONFIG.maxDelay,
|
RATE_LIMIT_CONFIG.maxDelay,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (responseTime < 500 && timeSinceLastSuccess > 10000) {
|
if (
|
||||||
return Math.max(currentDelay * 0.8, RATE_LIMIT_CONFIG.minDelay);
|
responseTime < 300 &&
|
||||||
|
timeSinceLastSuccess > 5000 &&
|
||||||
|
consecutiveFailures === 0
|
||||||
|
) {
|
||||||
|
return Math.max(currentDelay * 0.7, RATE_LIMIT_CONFIG.minDelay);
|
||||||
}
|
}
|
||||||
|
|
||||||
return currentDelay;
|
return currentDelay;
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateAdaptiveBatchSize(
|
|
||||||
progress: MessagesProgress,
|
|
||||||
responseTime: number,
|
|
||||||
): number {
|
|
||||||
if (!RATE_LIMIT_CONFIG.adaptiveBatchSize) {
|
|
||||||
return progress.currentBatchSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { currentBatchSize, failedRequests } = progress;
|
|
||||||
|
|
||||||
if (failedRequests > 2 || responseTime > 3000) {
|
|
||||||
return Math.max(
|
|
||||||
Math.floor(currentBatchSize * 0.7),
|
|
||||||
RATE_LIMIT_CONFIG.minBatchSize,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (failedRequests === 0 && responseTime < 1000) {
|
|
||||||
return Math.min(
|
|
||||||
Math.floor(currentBatchSize * 1.2),
|
|
||||||
RATE_LIMIT_CONFIG.maxBatchSize,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentBatchSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function estimateMessageCount(): Promise<number> {
|
async function estimateMessageCount(): Promise<number> {
|
||||||
try {
|
try {
|
||||||
const firstBatch = await fetchMessages(0, 100);
|
const firstBatch = await fetchMessages(0, 100);
|
||||||
@@ -157,6 +150,73 @@ async function estimateMessageCount(): Promise<number> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function calculateAdaptiveBatchSize(
|
||||||
|
progress: MessagesProgress,
|
||||||
|
responseTime: number,
|
||||||
|
): number {
|
||||||
|
if (!RATE_LIMIT_CONFIG.adaptiveBatchSize) {
|
||||||
|
return progress.currentBatchSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
currentBatchSize,
|
||||||
|
failedRequests,
|
||||||
|
circuitBreakerOpen,
|
||||||
|
consecutiveFailures,
|
||||||
|
} = progress;
|
||||||
|
|
||||||
|
if (circuitBreakerOpen) {
|
||||||
|
return RATE_LIMIT_CONFIG.minBatchSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (consecutiveFailures > 1 || failedRequests > 2 || responseTime > 2500) {
|
||||||
|
return Math.max(
|
||||||
|
Math.floor(currentBatchSize * 0.6),
|
||||||
|
RATE_LIMIT_CONFIG.minBatchSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failedRequests === 0 && responseTime < 800 && consecutiveFailures === 0) {
|
||||||
|
return Math.min(
|
||||||
|
Math.floor(currentBatchSize * 1.4),
|
||||||
|
RATE_LIMIT_CONFIG.maxBatchSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentBatchSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkCircuitBreaker(progress: MessagesProgress): boolean {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (
|
||||||
|
!progress.circuitBreakerOpen &&
|
||||||
|
progress.consecutiveFailures >= RATE_LIMIT_CONFIG.circuitBreakerThreshold
|
||||||
|
) {
|
||||||
|
progress.circuitBreakerOpen = true;
|
||||||
|
progress.circuitBreakerOpenTime = now;
|
||||||
|
console.warn(
|
||||||
|
`[Messages job] Circuit breaker opened due to ${progress.consecutiveFailures} consecutive failures`,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
progress.circuitBreakerOpen &&
|
||||||
|
now - progress.circuitBreakerOpenTime >
|
||||||
|
RATE_LIMIT_CONFIG.circuitBreakerResetTime
|
||||||
|
) {
|
||||||
|
progress.circuitBreakerOpen = false;
|
||||||
|
progress.consecutiveFailures = 0;
|
||||||
|
console.info(
|
||||||
|
`[Messages job] Circuit breaker closed after ${RATE_LIMIT_CONFIG.circuitBreakerResetTime}ms`,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return progress.circuitBreakerOpen;
|
||||||
|
}
|
||||||
|
|
||||||
async function processMessagesInParallel(
|
async function processMessagesInParallel(
|
||||||
messages: any[],
|
messages: any[],
|
||||||
existingIds: Set<string>,
|
existingIds: Set<string>,
|
||||||
@@ -173,21 +233,19 @@ async function processMessagesInParallel(
|
|||||||
let consecutiveExisting = 0;
|
let consecutiveExisting = 0;
|
||||||
const updatedProgress = { ...progress };
|
const updatedProgress = { ...progress };
|
||||||
|
|
||||||
// Filter out messages older than 2 years
|
|
||||||
const twoYearsAgo = Date.now() - 2 * 365 * 24 * 60 * 60 * 1000;
|
const twoYearsAgo = Date.now() - 2 * 365 * 24 * 60 * 60 * 1000;
|
||||||
let shouldStop = false;
|
let shouldStop = false;
|
||||||
|
|
||||||
const messagesToProcess = messages.filter((msg) => {
|
const messagesToProcess = messages.filter((msg) => {
|
||||||
const id = msg.id.toString();
|
const id = msg.id.toString();
|
||||||
const messageDate = new Date(msg.date).getTime();
|
const messageDate = new Date(msg.date).getTime();
|
||||||
|
|
||||||
// If we encounter a message older than 2 years, we should stop processing
|
|
||||||
// since messages are sorted by date descending
|
|
||||||
if (messageDate < twoYearsAgo) {
|
if (messageDate < twoYearsAgo) {
|
||||||
|
//! older than 2 years ago
|
||||||
shouldStop = true;
|
shouldStop = true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingIds.has(id) || processedIdsSet.has(id)) {
|
if (existingIds.has(id) || processedIdsSet.has(id)) {
|
||||||
consecutiveExisting++;
|
consecutiveExisting++;
|
||||||
return false;
|
return false;
|
||||||
@@ -320,6 +378,9 @@ export const messagesJob: Job = {
|
|||||||
processedIds: [],
|
processedIds: [],
|
||||||
streamingStarted: false,
|
streamingStarted: false,
|
||||||
totalEstimated: 0,
|
totalEstimated: 0,
|
||||||
|
circuitBreakerOpen: false,
|
||||||
|
circuitBreakerOpenTime: 0,
|
||||||
|
consecutiveFailures: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const existingIds = new Set((await ctx.getStoredItems()).map((i) => i.id));
|
const existingIds = new Set((await ctx.getStoredItems()).map((i) => i.id));
|
||||||
@@ -451,6 +512,14 @@ export const messagesJob: Job = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
while (!progress.done) {
|
while (!progress.done) {
|
||||||
|
if (checkCircuitBreaker(progress)) {
|
||||||
|
console.warn(
|
||||||
|
"[Messages job] Circuit breaker is open, skipping processing",
|
||||||
|
);
|
||||||
|
await delay(RATE_LIMIT_CONFIG.maxDelay);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
await delay(progress.currentDelay);
|
await delay(progress.currentDelay);
|
||||||
requestStartTime = Date.now();
|
requestStartTime = Date.now();
|
||||||
|
|
||||||
@@ -459,6 +528,8 @@ export const messagesJob: Job = {
|
|||||||
list = await fetchMessages(progress.offset, progress.currentBatchSize);
|
list = await fetchMessages(progress.offset, progress.currentBatchSize);
|
||||||
const responseTime = Date.now() - requestStartTime;
|
const responseTime = Date.now() - requestStartTime;
|
||||||
|
|
||||||
|
progress.consecutiveFailures = 0;
|
||||||
|
|
||||||
progress.currentDelay = calculateAdaptiveDelay(progress, responseTime);
|
progress.currentDelay = calculateAdaptiveDelay(progress, responseTime);
|
||||||
progress.currentBatchSize = calculateAdaptiveBatchSize(
|
progress.currentBatchSize = calculateAdaptiveBatchSize(
|
||||||
progress,
|
progress,
|
||||||
@@ -467,6 +538,7 @@ export const messagesJob: Job = {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[Messages job] list fetch failed:", e);
|
console.error("[Messages job] list fetch failed:", e);
|
||||||
progress.failedRequests++;
|
progress.failedRequests++;
|
||||||
|
progress.consecutiveFailures++;
|
||||||
progress.currentDelay = Math.min(
|
progress.currentDelay = Math.min(
|
||||||
progress.currentDelay * RATE_LIMIT_CONFIG.backoffMultiplier,
|
progress.currentDelay * RATE_LIMIT_CONFIG.backoffMultiplier,
|
||||||
RATE_LIMIT_CONFIG.maxDelay,
|
RATE_LIMIT_CONFIG.maxDelay,
|
||||||
@@ -479,6 +551,7 @@ export const messagesJob: Job = {
|
|||||||
|
|
||||||
if (list.status !== "200") {
|
if (list.status !== "200") {
|
||||||
progress.failedRequests++;
|
progress.failedRequests++;
|
||||||
|
progress.consecutiveFailures++;
|
||||||
|
|
||||||
progress.processedIds = Array.from(processedIdsSet);
|
progress.processedIds = Array.from(processedIdsSet);
|
||||||
await ctx.setProgress(progress);
|
await ctx.setProgress(progress);
|
||||||
@@ -507,7 +580,6 @@ export const messagesJob: Job = {
|
|||||||
|
|
||||||
itemsToStream.push(...processedItems);
|
itemsToStream.push(...processedItems);
|
||||||
|
|
||||||
// Update consecutive existing counter
|
|
||||||
consecutiveExisting = newConsecutiveExisting;
|
consecutiveExisting = newConsecutiveExisting;
|
||||||
if (consecutiveExisting >= 20) {
|
if (consecutiveExisting >= 20) {
|
||||||
progress.done = true;
|
progress.done = true;
|
||||||
@@ -529,14 +601,17 @@ export const messagesJob: Job = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dispatch incremental search update if we processed new items
|
|
||||||
if (processedItems.length > 0) {
|
if (processedItems.length > 0) {
|
||||||
try {
|
try {
|
||||||
const currentItems = await loadAllStoredItems();
|
const currentItems = await loadAllStoredItems();
|
||||||
currentItems.forEach(item => {
|
currentItems.forEach((item) => {
|
||||||
const jobDef = jobs[item.category] || Object.values(jobs).find(j => j.id === item.category) || jobs[item.renderComponentId];
|
const jobDef =
|
||||||
|
jobs[item.category] ||
|
||||||
|
Object.values(jobs).find((j) => j.id === item.category) ||
|
||||||
|
jobs[item.renderComponentId];
|
||||||
if (jobDef) {
|
if (jobDef) {
|
||||||
const renderComponent = renderComponentMap[jobDef.renderComponentId];
|
const renderComponent =
|
||||||
|
renderComponentMap[jobDef.renderComponentId];
|
||||||
if (renderComponent) {
|
if (renderComponent) {
|
||||||
item.renderComponent = renderComponent;
|
item.renderComponent = renderComponent;
|
||||||
}
|
}
|
||||||
@@ -545,11 +620,21 @@ export const messagesJob: Job = {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
loadDynamicItems(currentItems);
|
loadDynamicItems(currentItems);
|
||||||
window.dispatchEvent(new CustomEvent("dynamic-items-updated", {
|
window.dispatchEvent(
|
||||||
detail: { incremental: true, jobId: "messages", newItemCount: processedItems.length, streaming: true }
|
new CustomEvent("dynamic-items-updated", {
|
||||||
}));
|
detail: {
|
||||||
|
incremental: true,
|
||||||
|
jobId: "messages",
|
||||||
|
newItemCount: processedItems.length,
|
||||||
|
streaming: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("[Messages job] Failed to dispatch incremental search update:", error);
|
console.warn(
|
||||||
|
"[Messages job] Failed to dispatch incremental search update:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -596,6 +681,9 @@ export const messagesJob: Job = {
|
|||||||
processedIds: [],
|
processedIds: [],
|
||||||
streamingStarted: false,
|
streamingStarted: false,
|
||||||
totalEstimated: 0,
|
totalEstimated: 0,
|
||||||
|
circuitBreakerOpen: false,
|
||||||
|
circuitBreakerOpenTime: 0,
|
||||||
|
consecutiveFailures: 0,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
progress.processedIds = Array.from(processedIdsSet);
|
progress.processedIds = Array.from(processedIdsSet);
|
||||||
|
|||||||
@@ -309,10 +309,7 @@ export const notificationsJob: Job = {
|
|||||||
await delay(NOTIFICATIONS_RATE_LIMIT.batchDelay);
|
await delay(NOTIFICATIONS_RATE_LIMIT.batchDelay);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { success, item } = await processNotification(
|
const { success, item } = await processNotification(notif, ctx);
|
||||||
notif,
|
|
||||||
ctx,
|
|
||||||
);
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
if (progress.retryQueue.length < 10) {
|
if (progress.retryQueue.length < 10) {
|
||||||
progress.retryQueue.push(notif.notificationID);
|
progress.retryQueue.push(notif.notificationID);
|
||||||
@@ -371,27 +368,42 @@ export const notificationsJob: Job = {
|
|||||||
if (progressUpdateCounter >= 5) {
|
if (progressUpdateCounter >= 5) {
|
||||||
await ctx.setProgress(progress);
|
await ctx.setProgress(progress);
|
||||||
progressUpdateCounter = 0;
|
progressUpdateCounter = 0;
|
||||||
|
|
||||||
if (items.length > 0) {
|
if (items.length > 0) {
|
||||||
try {
|
try {
|
||||||
const currentItems = await loadAllStoredItems();
|
const currentItems = await loadAllStoredItems();
|
||||||
currentItems.forEach(item => {
|
currentItems.forEach((item) => {
|
||||||
const jobDef = jobs[item.category] || Object.values(jobs).find(j => j.id === item.category) || jobs[item.renderComponentId];
|
const jobDef =
|
||||||
|
jobs[item.category] ||
|
||||||
|
Object.values(jobs).find((j) => j.id === item.category) ||
|
||||||
|
jobs[item.renderComponentId];
|
||||||
if (jobDef) {
|
if (jobDef) {
|
||||||
const renderComponent = renderComponentMap[jobDef.renderComponentId];
|
const renderComponent =
|
||||||
|
renderComponentMap[jobDef.renderComponentId];
|
||||||
if (renderComponent) {
|
if (renderComponent) {
|
||||||
item.renderComponent = renderComponent;
|
item.renderComponent = renderComponent;
|
||||||
}
|
}
|
||||||
} else if (renderComponentMap[item.renderComponentId]) {
|
} else if (renderComponentMap[item.renderComponentId]) {
|
||||||
item.renderComponent = renderComponentMap[item.renderComponentId];
|
item.renderComponent =
|
||||||
|
renderComponentMap[item.renderComponentId];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
loadDynamicItems(currentItems);
|
loadDynamicItems(currentItems);
|
||||||
window.dispatchEvent(new CustomEvent("dynamic-items-updated", {
|
window.dispatchEvent(
|
||||||
detail: { incremental: true, jobId: "notifications", newItemCount: items.length, streaming: true }
|
new CustomEvent("dynamic-items-updated", {
|
||||||
}));
|
detail: {
|
||||||
|
incremental: true,
|
||||||
|
jobId: "notifications",
|
||||||
|
newItemCount: items.length,
|
||||||
|
streaming: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("[Notifications job] Failed to dispatch incremental search update:", error);
|
console.warn(
|
||||||
|
"[Notifications job] Failed to dispatch incremental search update:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { IndexItem } from "../types";
|
|||||||
let vectorIndex: EmbeddingIndex | null = null;
|
let vectorIndex: EmbeddingIndex | null = null;
|
||||||
let isInitialized = false;
|
let isInitialized = false;
|
||||||
let currentAbortController: AbortController | null = null;
|
let currentAbortController: AbortController | null = null;
|
||||||
let loadedItemIds = new Set<string>(); // Track loaded items to prevent duplicates
|
let loadedItemIds = new Set<string>();
|
||||||
|
|
||||||
let streamingSession: {
|
let streamingSession: {
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
@@ -26,22 +26,19 @@ async function initWorker() {
|
|||||||
await initializeModel();
|
await initializeModel();
|
||||||
vectorIndex = new EmbeddingIndex([]);
|
vectorIndex = new EmbeddingIndex([]);
|
||||||
|
|
||||||
// Load existing items but track them to prevent duplicates
|
|
||||||
const stored = await vectorIndex.getAllObjectsFromIndexedDB();
|
const stored = await vectorIndex.getAllObjectsFromIndexedDB();
|
||||||
if (stored.length > 0) {
|
if (stored.length > 0) {
|
||||||
console.debug(`Found ${stored.length} existing items in IndexedDB`);
|
console.debug(`Found ${stored.length} existing items in IndexedDB`);
|
||||||
|
|
||||||
// Clear any existing items from memory first
|
|
||||||
loadedItemIds.clear();
|
loadedItemIds.clear();
|
||||||
|
|
||||||
// Add items and track their IDs
|
|
||||||
stored.forEach((item) => {
|
stored.forEach((item) => {
|
||||||
if (item.id && !loadedItemIds.has(item.id)) {
|
if (item.id && !loadedItemIds.has(item.id)) {
|
||||||
vectorIndex!.add(item);
|
vectorIndex!.add(item);
|
||||||
loadedItemIds.add(item.id);
|
loadedItemIds.add(item.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.debug(
|
console.debug(
|
||||||
`Vector index loaded ${loadedItemIds.size} unique items from IndexedDB.`,
|
`Vector index loaded ${loadedItemIds.size} unique items from IndexedDB.`,
|
||||||
);
|
);
|
||||||
@@ -168,7 +165,6 @@ async function processStreamingItems() {
|
|||||||
streamingSession.batchSize,
|
streamingSession.batchSize,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Use our tracking set for more efficient deduplication
|
|
||||||
const unprocessedItems = batchToProcess.filter((item) => {
|
const unprocessedItems = batchToProcess.filter((item) => {
|
||||||
return item.id && !loadedItemIds.has(item.id);
|
return item.id && !loadedItemIds.has(item.id);
|
||||||
});
|
});
|
||||||
@@ -190,12 +186,12 @@ async function processStreamingItems() {
|
|||||||
try {
|
try {
|
||||||
successfullyVectorized.forEach((item) => {
|
successfullyVectorized.forEach((item) => {
|
||||||
vectorIndex!.add(item);
|
vectorIndex!.add(item);
|
||||||
loadedItemIds.add(item.id); // Track the added item
|
loadedItemIds.add(item.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
streamingSession.totalProcessed % (streamingSession.batchSize * 15) ===
|
streamingSession.totalProcessed % 50 === 0 ||
|
||||||
0
|
loadedItemIds.size % 200 === 0
|
||||||
) {
|
) {
|
||||||
await vectorIndex!.saveIndex("indexedDB");
|
await vectorIndex!.saveIndex("indexedDB");
|
||||||
console.debug(
|
console.debug(
|
||||||
@@ -328,7 +324,6 @@ async function processItems(items: IndexItem[], signal: AbortSignal) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use our tracking set for more efficient deduplication
|
|
||||||
const unprocessedItems = items.filter((item) => {
|
const unprocessedItems = items.filter((item) => {
|
||||||
if (signal.aborted) return false;
|
if (signal.aborted) return false;
|
||||||
return item.id && !loadedItemIds.has(item.id);
|
return item.id && !loadedItemIds.has(item.id);
|
||||||
@@ -347,15 +342,22 @@ async function processItems(items: IndexItem[], signal: AbortSignal) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (unprocessedItems.length === 0) {
|
if (unprocessedItems.length === 0) {
|
||||||
console.debug(`No new items to process. ${loadedItemIds.size} items already in index.`);
|
console.debug(
|
||||||
|
`No new items to process. ${loadedItemIds.size} items already in index.`,
|
||||||
|
);
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
type: "progress",
|
type: "progress",
|
||||||
data: { status: "complete", message: `No new items to process (${loadedItemIds.size} items already indexed)` },
|
data: {
|
||||||
|
status: "complete",
|
||||||
|
message: `No new items to process (${loadedItemIds.size} items already indexed)`,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug(`Starting processing of ${unprocessedItems.length} items (${items.length - unprocessedItems.length} already processed).`);
|
console.debug(
|
||||||
|
`Starting processing of ${unprocessedItems.length} items (${items.length - unprocessedItems.length} already processed).`,
|
||||||
|
);
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
type: "progress",
|
type: "progress",
|
||||||
data: {
|
data: {
|
||||||
@@ -402,7 +404,7 @@ async function processItems(items: IndexItem[], signal: AbortSignal) {
|
|||||||
try {
|
try {
|
||||||
successfullyVectorized.forEach((item) => {
|
successfullyVectorized.forEach((item) => {
|
||||||
vectorIndex!.add(item);
|
vectorIndex!.add(item);
|
||||||
loadedItemIds.add(item.id); // Track the added item
|
loadedItemIds.add(item.id);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error adding batch to index:", e);
|
console.error("Error adding batch to index:", e);
|
||||||
@@ -425,15 +427,22 @@ async function processItems(items: IndexItem[], signal: AbortSignal) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (
|
||||||
await vectorIndex!.saveIndex("indexedDB");
|
(i / BATCH_SIZE + 1) % 3 === 0 ||
|
||||||
console.debug(`Saved index after processing batch ${i / BATCH_SIZE + 1} (${loadedItemIds.size} total unique items)`);
|
i + BATCH_SIZE >= unprocessedItems.length
|
||||||
} catch (e) {
|
) {
|
||||||
console.error("Error saving index batch:", e);
|
try {
|
||||||
self.postMessage({
|
await vectorIndex!.saveIndex("indexedDB");
|
||||||
type: "progress",
|
console.debug(
|
||||||
data: { status: "error", message: `Error saving index batch: ${e}` },
|
`Saved index after processing batch ${i / BATCH_SIZE + 1} (${loadedItemIds.size} total unique items)`,
|
||||||
});
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error saving index batch:", e);
|
||||||
|
self.postMessage({
|
||||||
|
type: "progress",
|
||||||
|
data: { status: "error", message: `Error saving index batch: ${e}` },
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
processedCount += batch.length;
|
processedCount += batch.length;
|
||||||
@@ -448,7 +457,9 @@ async function processItems(items: IndexItem[], signal: AbortSignal) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug(`Processing complete. Total unique items in index: ${loadedItemIds.size}`);
|
console.debug(
|
||||||
|
`Processing complete. Total unique items in index: ${loadedItemIds.size}`,
|
||||||
|
);
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
type: "progress",
|
type: "progress",
|
||||||
data: {
|
data: {
|
||||||
@@ -462,35 +473,32 @@ async function processItems(items: IndexItem[], signal: AbortSignal) {
|
|||||||
|
|
||||||
async function resetWorker() {
|
async function resetWorker() {
|
||||||
console.debug("Resetting vector worker state...");
|
console.debug("Resetting vector worker state...");
|
||||||
|
|
||||||
// Clear tracking
|
|
||||||
loadedItemIds.clear();
|
loadedItemIds.clear();
|
||||||
|
|
||||||
// Reset streaming session
|
|
||||||
if (streamingSession?.isActive) {
|
if (streamingSession?.isActive) {
|
||||||
streamingSession.isActive = false;
|
streamingSession.isActive = false;
|
||||||
streamingSession = null;
|
streamingSession = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset vector index
|
|
||||||
if (vectorIndex) {
|
if (vectorIndex) {
|
||||||
try {
|
try {
|
||||||
// Save current state before reset
|
|
||||||
await vectorIndex.saveIndex("indexedDB");
|
await vectorIndex.saveIndex("indexedDB");
|
||||||
console.debug("Saved index before reset");
|
console.debug("Saved index before reset");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Error saving index before reset:", e);
|
console.warn("Error saving index before reset:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reinitialize
|
|
||||||
isInitialized = false;
|
isInitialized = false;
|
||||||
vectorIndex = null;
|
vectorIndex = null;
|
||||||
|
|
||||||
await initWorker();
|
await initWorker();
|
||||||
|
|
||||||
console.debug(`Vector worker reset complete. Loaded ${loadedItemIds.size} items.`);
|
console.debug(
|
||||||
|
`Vector worker reset complete. Loaded ${loadedItemIds.size} items.`,
|
||||||
|
);
|
||||||
|
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
type: "progress",
|
type: "progress",
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ export class VectorWorkerManager {
|
|||||||
private isInitialized = false;
|
private isInitialized = false;
|
||||||
private readyPromise: Promise<void> | null = null;
|
private readyPromise: Promise<void> | null = null;
|
||||||
private progressCallback: ProgressCallback | null = null;
|
private progressCallback: ProgressCallback | null = null;
|
||||||
|
private initializationMutex = false;
|
||||||
|
private idleTimer: NodeJS.Timeout | null = null;
|
||||||
|
private lastActivityTime = 0;
|
||||||
|
|
||||||
private streamingSession: {
|
private streamingSession: {
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
@@ -23,7 +26,9 @@ export class VectorWorkerManager {
|
|||||||
batchBuffer: IndexItem[];
|
batchBuffer: IndexItem[];
|
||||||
batchSize: number;
|
batchSize: number;
|
||||||
flushTimer: NodeJS.Timeout | null;
|
flushTimer: NodeJS.Timeout | null;
|
||||||
jobId?: string; // Track which job owns the session
|
jobId?: string;
|
||||||
|
inactivityTimer: NodeJS.Timeout | null;
|
||||||
|
lastActivityTime: number;
|
||||||
} | null = null;
|
} | null = null;
|
||||||
|
|
||||||
private constructor() {}
|
private constructor() {}
|
||||||
@@ -43,13 +48,12 @@ export class VectorWorkerManager {
|
|||||||
console.debug("Lazy-loading vector worker...");
|
console.debug("Lazy-loading vector worker...");
|
||||||
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
// Terminate any existing worker before creating a new one
|
|
||||||
if (this.worker) {
|
if (this.worker) {
|
||||||
console.debug("Terminating existing worker before creating new one");
|
console.debug("Terminating existing worker before creating new one");
|
||||||
this.worker.terminate();
|
this.worker.terminate();
|
||||||
this.worker = null;
|
this.worker = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug("Creating new vector worker instance");
|
console.debug("Creating new vector worker instance");
|
||||||
this.worker = vectorWorker();
|
this.worker = vectorWorker();
|
||||||
|
|
||||||
@@ -62,8 +66,7 @@ export class VectorWorkerManager {
|
|||||||
this.worker = null;
|
this.worker = null;
|
||||||
}
|
}
|
||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
// Don't reset readyPromise here to prevent race conditions
|
|
||||||
// It will be reset when a new initialization is attempted
|
|
||||||
reject(new Error("Worker initialization timed out"));
|
reject(new Error("Worker initialization timed out"));
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
@@ -75,6 +78,7 @@ export class VectorWorkerManager {
|
|||||||
case "ready":
|
case "ready":
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
|
this.updateActivity(); // Start idle timer after initialization
|
||||||
console.debug("Vector worker initialized and ready.");
|
console.debug("Vector worker initialized and ready.");
|
||||||
resolve();
|
resolve();
|
||||||
break;
|
break;
|
||||||
@@ -89,11 +93,16 @@ export class VectorWorkerManager {
|
|||||||
if (this.streamingSession?.isActive) {
|
if (this.streamingSession?.isActive) {
|
||||||
this.endStreamingSession();
|
this.endStreamingSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dispatch search update when vectorization completes
|
window.dispatchEvent(
|
||||||
window.dispatchEvent(new CustomEvent("dynamic-items-updated", {
|
new CustomEvent("dynamic-items-updated", {
|
||||||
detail: { incremental: true, jobId: "vectorization", vectorUpdate: true }
|
detail: {
|
||||||
}));
|
incremental: true,
|
||||||
|
jobId: "vectorization",
|
||||||
|
vectorUpdate: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -128,35 +137,73 @@ export class VectorWorkerManager {
|
|||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
this.readyPromise = null;
|
this.readyPromise = null;
|
||||||
this.progressCallback = null;
|
this.progressCallback = null;
|
||||||
|
this.initializationMutex = false;
|
||||||
|
this.clearIdleTimer();
|
||||||
if (this.streamingSession?.isActive) {
|
if (this.streamingSession?.isActive) {
|
||||||
this.endStreamingSession();
|
this.endStreamingSession();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private startIdleTimer() {
|
||||||
|
this.clearIdleTimer();
|
||||||
|
this.idleTimer = setTimeout(() => {
|
||||||
|
if (!this.streamingSession?.isActive && this.isInitialized) {
|
||||||
|
console.debug("[VectorWorker] Auto-shutting down due to 2 minutes of inactivity");
|
||||||
|
this.resetWorkerState();
|
||||||
|
}
|
||||||
|
}, 120000); // 2 minutes
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearIdleTimer() {
|
||||||
|
if (this.idleTimer) {
|
||||||
|
clearTimeout(this.idleTimer);
|
||||||
|
this.idleTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateActivity() {
|
||||||
|
this.lastActivityTime = Date.now();
|
||||||
|
this.startIdleTimer();
|
||||||
|
}
|
||||||
|
|
||||||
private async ensureReady() {
|
private async ensureReady() {
|
||||||
// If we already have a ready promise, wait for it regardless of outcome
|
if (this.initializationMutex) {
|
||||||
|
while (this.initializationMutex) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isInitialized && this.worker) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.readyPromise) {
|
if (this.readyPromise) {
|
||||||
try {
|
try {
|
||||||
await this.readyPromise;
|
await this.readyPromise;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If the previous initialization failed, reset state and try again
|
console.warn(
|
||||||
console.warn("Previous worker initialization failed, resetting state and retrying...", error);
|
"Previous worker initialization failed, resetting state and retrying...",
|
||||||
|
error,
|
||||||
|
);
|
||||||
this.resetWorkerState();
|
this.resetWorkerState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Double-check if we're actually ready after waiting
|
|
||||||
if (this.isInitialized && this.worker) {
|
if (this.isInitialized && this.worker) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're not ready and there's no active promise, create one
|
if (!this.readyPromise && !this.initializationMutex) {
|
||||||
if (!this.readyPromise) {
|
|
||||||
console.warn("Worker not initialized, attempting init...");
|
console.warn("Worker not initialized, attempting init...");
|
||||||
this.readyPromise = this.initWorker();
|
this.initializationMutex = true;
|
||||||
|
try {
|
||||||
|
this.readyPromise = this.initWorker();
|
||||||
|
await this.readyPromise;
|
||||||
|
} finally {
|
||||||
|
this.initializationMutex = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.readyPromise;
|
|
||||||
if (!this.isInitialized || !this.worker) {
|
if (!this.isInitialized || !this.worker) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Vector Worker is not available after initialization attempt.",
|
"Vector Worker is not available after initialization attempt.",
|
||||||
@@ -165,27 +212,61 @@ export class VectorWorkerManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async processItems(items: IndexItem[], onProgress?: ProgressCallback) {
|
async processItems(items: IndexItem[], onProgress?: ProgressCallback) {
|
||||||
|
// Only initialize worker if we actually have items to process
|
||||||
|
if (items.length === 0) {
|
||||||
|
if (onProgress) {
|
||||||
|
onProgress({
|
||||||
|
status: "complete",
|
||||||
|
message: "No items to process"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueItems = items.filter((item, index, arr) => {
|
||||||
|
return arr.findIndex((i) => i.id === item.id) === index;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (uniqueItems.length !== items.length) {
|
||||||
|
console.debug(
|
||||||
|
`Filtered out ${items.length - uniqueItems.length} duplicate items before processing`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If after deduplication we have no items, don't initialize worker
|
||||||
|
if (uniqueItems.length === 0) {
|
||||||
|
if (onProgress) {
|
||||||
|
onProgress({
|
||||||
|
status: "complete",
|
||||||
|
message: "No unique items to process after deduplication"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await this.ensureReady();
|
await this.ensureReady();
|
||||||
|
|
||||||
// Don't allow regular processing if streaming is active
|
|
||||||
if (this.streamingSession?.isActive) {
|
if (this.streamingSession?.isActive) {
|
||||||
console.warn("Cannot process items while streaming session is active");
|
console.warn("Cannot process items while streaming session is active");
|
||||||
if (onProgress) {
|
if (onProgress) {
|
||||||
onProgress({
|
onProgress({
|
||||||
status: "error",
|
status: "error",
|
||||||
message: "Cannot process items while streaming session is active"
|
message: "Cannot process items while streaming session is active",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.progressCallback = onProgress || null;
|
this.progressCallback = onProgress || null;
|
||||||
|
this.updateActivity();
|
||||||
|
|
||||||
console.debug(`Sending ${items.length} items to worker for processing.`);
|
console.debug(
|
||||||
|
`Sending ${uniqueItems.length} unique items to worker for processing.`,
|
||||||
|
);
|
||||||
|
|
||||||
this.worker!.postMessage({
|
this.worker!.postMessage({
|
||||||
type: "process",
|
type: "process",
|
||||||
data: { items: items },
|
data: { items: uniqueItems },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,19 +276,22 @@ export class VectorWorkerManager {
|
|||||||
batchSize: number = 10,
|
batchSize: number = 10,
|
||||||
jobId?: string,
|
jobId?: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
// Only initialize if we expect items to process
|
||||||
|
if (totalExpectedItems === 0) {
|
||||||
|
console.debug("[VectorWorker] No items expected, not starting streaming session");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await this.ensureReady();
|
await this.ensureReady();
|
||||||
|
|
||||||
// Check if another job already has an active streaming session
|
|
||||||
if (this.streamingSession?.isActive) {
|
if (this.streamingSession?.isActive) {
|
||||||
if (this.streamingSession.jobId !== jobId) {
|
if (this.streamingSession.jobId !== jobId) {
|
||||||
console.warn(`Cannot start streaming session for job ${jobId} - job ${this.streamingSession.jobId} already has an active session`);
|
console.warn(
|
||||||
if (onProgress) {
|
`Ending existing streaming session for job ${this.streamingSession.jobId} to start new session for job ${jobId}`,
|
||||||
onProgress({
|
);
|
||||||
status: "error",
|
await this.endStreamingSession();
|
||||||
message: `Another job (${this.streamingSession.jobId}) already has an active streaming session`
|
|
||||||
});
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
}
|
|
||||||
return;
|
|
||||||
} else {
|
} else {
|
||||||
console.debug(`Streaming session for job ${jobId} already active`);
|
console.debug(`Streaming session for job ${jobId} already active`);
|
||||||
return;
|
return;
|
||||||
@@ -215,6 +299,7 @@ export class VectorWorkerManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.progressCallback = onProgress || null;
|
this.progressCallback = onProgress || null;
|
||||||
|
this.updateActivity();
|
||||||
|
|
||||||
this.streamingSession = {
|
this.streamingSession = {
|
||||||
isActive: true,
|
isActive: true,
|
||||||
@@ -224,6 +309,8 @@ export class VectorWorkerManager {
|
|||||||
batchSize,
|
batchSize,
|
||||||
flushTimer: null,
|
flushTimer: null,
|
||||||
jobId,
|
jobId,
|
||||||
|
inactivityTimer: null,
|
||||||
|
lastActivityTime: Date.now(),
|
||||||
};
|
};
|
||||||
|
|
||||||
console.debug(
|
console.debug(
|
||||||
@@ -252,7 +339,34 @@ export class VectorWorkerManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.streamingSession.batchBuffer.push(...items);
|
const uniqueItems = items.filter((item, index, arr) => {
|
||||||
|
return arr.findIndex((i) => i.id === item.id) === index;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (uniqueItems.length !== items.length) {
|
||||||
|
console.debug(
|
||||||
|
`[Streaming] Filtered out ${items.length - uniqueItems.length} duplicate items before streaming`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uniqueItems.length > 0) {
|
||||||
|
this.streamingSession.batchBuffer.push(...uniqueItems);
|
||||||
|
this.streamingSession.lastActivityTime = Date.now();
|
||||||
|
this.updateActivity(); // Update worker activity
|
||||||
|
|
||||||
|
if (this.streamingSession.inactivityTimer) {
|
||||||
|
clearTimeout(this.streamingSession.inactivityTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.streamingSession.inactivityTimer = setTimeout(() => {
|
||||||
|
if (this.streamingSession?.isActive) {
|
||||||
|
console.debug(
|
||||||
|
"[VectorWorker] Auto-ending streaming session due to inactivity",
|
||||||
|
);
|
||||||
|
this.endStreamingSession();
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.streamingSession.batchBuffer.length >=
|
this.streamingSession.batchBuffer.length >=
|
||||||
@@ -313,6 +427,10 @@ export class VectorWorkerManager {
|
|||||||
clearTimeout(this.streamingSession.flushTimer);
|
clearTimeout(this.streamingSession.flushTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.streamingSession.inactivityTimer) {
|
||||||
|
clearTimeout(this.streamingSession.inactivityTimer);
|
||||||
|
}
|
||||||
|
|
||||||
this.streamingSession.isActive = false;
|
this.streamingSession.isActive = false;
|
||||||
|
|
||||||
this.worker!.postMessage({
|
this.worker!.postMessage({
|
||||||
@@ -337,6 +455,7 @@ export class VectorWorkerManager {
|
|||||||
return this.streamItems([item]);
|
return this.streamItems([item]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
isStreamingActive(): boolean {
|
isStreamingActive(): boolean {
|
||||||
return this.streamingSession?.isActive ?? false;
|
return this.streamingSession?.isActive ?? false;
|
||||||
}
|
}
|
||||||
@@ -364,15 +483,15 @@ export class VectorWorkerManager {
|
|||||||
|
|
||||||
async resetWorker(): Promise<void> {
|
async resetWorker(): Promise<void> {
|
||||||
console.debug("Resetting vector worker...");
|
console.debug("Resetting vector worker...");
|
||||||
|
|
||||||
if (this.streamingSession?.isActive) {
|
if (this.streamingSession?.isActive) {
|
||||||
await this.endStreamingSession();
|
await this.endStreamingSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.ensureReady();
|
await this.ensureReady();
|
||||||
|
|
||||||
this.worker!.postMessage({ type: "reset" });
|
this.worker!.postMessage({ type: "reset" });
|
||||||
|
|
||||||
console.debug("Reset command sent to worker");
|
console.debug("Reset command sent to worker");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user