fix: hydrate worker DB in server mode + fix LadybugDB getAll API mismatch (#398) (#404)

Fix server/bridge mode leaving the web UI with 0 nodes and broken
Query/Processes/embeddings by hydrating the worker-side LadybugDB
and BM25 indexes after loading graph data from the backend.

Also fix LadybugDB QueryResult API mismatch where result.getAll()
does not exist in some @ladybugdb/wasm-core versions — falls back
to getAllObjects() or getAllRows().
This commit is contained in:
0xfabs 2026-03-21 01:39:25 +01:00 • committed by GitHub
parent 01ff2a7540
commit 11575cf6c8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 104 additions and 27 deletions

View file

@ -40,6 +40,7 @@ const AppContent = () => {
availableRepos,
setAvailableRepos,
switchRepo,
hydrateWorkerFromServer,
} = useAppState();
const graphCanvasRef = useRef<GraphCanvasHandle>(null);
@ -157,21 +158,31 @@ const AppContent = () => {
// Transition directly to exploring view
setViewMode('exploring');
setProgress(null);
// Initialize agent if LLM is configured
if (getActiveProviderConfig()) {
initializeAgent(projectName);
}
// Hydrate the worker-side DB (LadybugDB + BM25) so Query/Processes/embeddings work
hydrateWorkerFromServer(result.nodes, result.relationships, result.fileContents).then(() => {
// Initialize agent if LLM is configured
if (getActiveProviderConfig()) {
initializeAgent(projectName);
}
// Auto-start embeddings
startEmbeddings().catch((err) => {
if (err?.name === 'WebGPUNotAvailableError' || err?.message?.includes('WebGPU')) {
startEmbeddings('wasm').catch(console.warn);
} else {
console.warn('Embeddings auto-start failed:', err);
// Auto-start embeddings (now that LadybugDB is ready)
startEmbeddings().catch((err) => {
if (err?.name === 'WebGPUNotAvailableError' || err?.message?.includes('WebGPU')) {
startEmbeddings('wasm').catch(console.warn);
} else {
console.warn('Embeddings auto-start failed:', err);
}
});
}).catch((err) => {
console.warn('Worker hydration failed (non-fatal):', err);
// Still initialize agent even if hydration fails
if (getActiveProviderConfig()) {
initializeAgent(projectName);
}
});
}, [setViewMode, setGraph, setFileContents, setProjectName, initializeAgent, startEmbeddings]);
}, [setViewMode, setGraph, setFileContents, setProjectName, setProgress, initializeAgent, startEmbeddings, hydrateWorkerFromServer]);
// Auto-connect when ?server query param is present (bookmarkable shortcut)
const autoConnectRan = useRef(false);

View file

@ -190,7 +190,7 @@ export const loadGraphToLbug = async (
for (const tableName of NODE_TABLES) {
try {
const countRes = await conn.query(`MATCH (n:${tableName}) RETURN count(n) AS cnt`);
const countRows = await countRes.getAll();
const countRows = await (countRes.getAll?.() ?? countRes.getAllObjects?.() ?? countRes.getAllRows?.() ?? []);
const countRow = countRows[0];
const count = countRow ? (countRow.cnt ?? countRow[0] ?? 0) : 0;
totalNodes += Number(count);
@ -293,8 +293,8 @@ export const executeQuery = async (cypher: string): Promise<any[]> => {
});
}
// Collect all rows
const allRows = await result.getAll();
// Collect all rows (handle API differences across LadybugDB versions)
const allRows = await (result.getAll?.() ?? result.getAllObjects?.() ?? result.getAllRows?.() ?? []);
const rows: any[] = [];
for (const row of allRows) {
// Convert tuple to named object if we have column names and row is array
@ -331,7 +331,7 @@ export const getLbugStats = async (): Promise<{ nodes: number; edges: number }>
for (const tableName of NODE_TABLES) {
try {
const nodeResult = await conn.query(`MATCH (n:${tableName}) RETURN count(n) AS cnt`);
const nodeRows = await nodeResult.getAll();
const nodeRows = await (nodeResult.getAll?.() ?? nodeResult.getAllObjects?.() ?? nodeResult.getAllRows?.() ?? []);
const nodeRow = nodeRows[0];
totalNodes += Number(nodeRow?.cnt ?? nodeRow?.[0] ?? 0);
} catch {
@ -343,7 +343,7 @@ export const getLbugStats = async (): Promise<{ nodes: number; edges: number }>
let totalEdges = 0;
try {
const edgeResult = await conn.query(`MATCH ()-[r:${REL_TABLE_NAME}]->() RETURN count(r) AS cnt`);
const edgeRows = await edgeResult.getAll();
const edgeRows = await (edgeResult.getAll?.() ?? edgeResult.getAllObjects?.() ?? edgeResult.getAllRows?.() ?? []);
const edgeRow = edgeRows[0];
totalEdges = Number(edgeRow?.cnt ?? edgeRow?.[0] ?? 0);
} catch {
@ -408,7 +408,7 @@ export const executePrepared = async (
const result = await conn.execute(stmt, params);
const rows = await result.getAll();
const rows = await (result.getAll?.() ?? result.getAllObjects?.() ?? result.getAllRows?.() ?? []);
await stmt.close();
return rows;
@ -472,7 +472,7 @@ export const testArrayParams = async (): Promise<{ success: boolean; error?: str
for (const tableName of NODE_TABLES) {
try {
const nodeResult = await conn.query(`MATCH (n:${tableName}) RETURN n.id AS id LIMIT 1`);
const nodeRows = await nodeResult.getAll();
const nodeRows = await (nodeResult.getAll?.() ?? nodeResult.getAllObjects?.() ?? nodeResult.getAllRows?.() ?? []);
const nodeRow = nodeRows[0];
if (nodeRow) {
testNodeId = nodeRow.id ?? nodeRow[0];
@ -509,7 +509,7 @@ export const testArrayParams = async (): Promise<{ success: boolean; error?: str
const verifyResult = await conn.query(
`MATCH (e:${EMBEDDING_TABLE_NAME} {nodeId: '${testNodeId}'}) RETURN e.embedding AS emb`
);
const verifyRows = await verifyResult.getAll();
const verifyRows = await (verifyResult.getAll?.() ?? verifyResult.getAllObjects?.() ?? verifyResult.getAllRows?.() ?? []);
const verifyRow = verifyRows[0];
const storedEmb = verifyRow?.emb ?? verifyRow?.[0];

View file

@ -125,6 +125,7 @@ interface AppState {
runPipelineFromFiles: (files: FileEntry[], onProgress: (p: PipelineProgress) => void, clusteringConfig?: ProviderConfig) => Promise<PipelineResult>;
runQuery: (cypher: string) => Promise<any[]>;
isDatabaseReady: () => Promise<boolean>;
hydrateWorkerFromServer: (nodes: any[], relationships: any[], fileContents: Record<string, string>) => Promise<void>;
// Embedding state
embeddingStatus: EmbeddingStatus;
@ -482,6 +483,16 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
}
}, []);
const hydrateWorkerFromServer = useCallback(async (
nodes: any[],
relationships: any[],
fileContents: Record<string, string>
): Promise<void> => {
const api = apiRef.current;
if (!api) throw new Error('Worker not initialized');
await api.hydrateFromServerData(nodes, relationships, fileContents);
}, []);
// Embedding methods
const startEmbeddings = useCallback(async (forceDevice?: 'webgpu' | 'wasm'): Promise<void> => {
const api = apiRef.current;
@ -1018,15 +1029,23 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
setFileContents(fileMap);
setViewMode('exploring');
setProgress(null);
if (getActiveProviderConfig()) initializeAgent(pName);
// Hydrate the worker-side DB (LadybugDB + BM25) so Query/Processes/embeddings work
hydrateWorkerFromServer(result.nodes, result.relationships, result.fileContents).then(() => {
if (getActiveProviderConfig()) initializeAgent(pName);
startEmbeddings().catch((err) => {
if (err?.name === 'WebGPUNotAvailableError' || err?.message?.includes('WebGPU')) {
startEmbeddings('wasm').catch(console.warn);
} else {
console.warn('Embeddings auto-start failed:', err);
}
startEmbeddings().catch((err) => {
if (err?.name === 'WebGPUNotAvailableError' || err?.message?.includes('WebGPU')) {
startEmbeddings('wasm').catch(console.warn);
} else {
console.warn('Embeddings auto-start failed:', err);
}
});
}).catch((err) => {
console.warn('Worker hydration failed (non-fatal):', err);
// Still initialize agent even if hydration fails
if (getActiveProviderConfig()) initializeAgent(pName);
});
} catch (err) {
console.error('Repo switch failed:', err);
@ -1037,7 +1056,7 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
});
setTimeout(() => { setViewMode('exploring'); setProgress(null); }, 3000);
}
}, [serverBaseUrl, setProgress, setViewMode, setProjectName, setGraph, setFileContents, initializeAgent, startEmbeddings, setHighlightedNodeIds, clearAIToolHighlights, clearBlastRadius, setSelectedNode, setQueryResult, setCodeReferences, setCodePanelOpen, setCodeReferenceFocus]);
}, [serverBaseUrl, setProgress, setViewMode, setProjectName, setGraph, setFileContents, initializeAgent, startEmbeddings, hydrateWorkerFromServer, setHighlightedNodeIds, clearAIToolHighlights, clearBlastRadius, setSelectedNode, setQueryResult, setCodeReferences, setCodePanelOpen, setCodeReferenceFocus]);
const removeCodeReference = useCallback((id: string) => {
setCodeReferences(prev => {
@ -1142,6 +1161,7 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
runPipelineFromFiles,
runQuery,
isDatabaseReady,
hydrateWorkerFromServer,
// Embedding state and methods
embeddingStatus,
embeddingProgress,

View file

@ -1,5 +1,7 @@
import * as Comlink from 'comlink';
import { runIngestionPipeline, runPipelineFromFiles } from '../core/ingestion/pipeline';
import { createKnowledgeGraph } from '../core/graph/graph';
import type { GraphNode, GraphRelationship } from '../core/graph/types';
import { PipelineProgress, SerializablePipelineResult, serializePipelineResult } from '../types/pipeline';
import { FileEntry } from '../services/zip';
import {
@ -207,6 +209,50 @@ const workerApi = {
return serializePipelineResult(result);
},
/**
* Hydrate the worker-side database and indexes from server-loaded data.
* This is the missing step when using server/bridge mode — the main thread
* builds the React graph, but the worker's LadybugDB + BM25 stay empty.
*/
async hydrateFromServerData(
nodes: GraphNode[],
relationships: GraphRelationship[],
fileContents: Record<string, string>
): Promise<void> {
// 1. Build a KnowledgeGraph the same way the pipeline does
const graph = createKnowledgeGraph();
for (const node of nodes) graph.addNode(node);
for (const rel of relationships) graph.addRelationship(rel);
// 2. Store file contents for grep/read tools
storedFileContents = new Map(Object.entries(fileContents));
// 3. Build BM25 keyword index
const bm25DocCount = buildBM25Index(storedFileContents);
if (import.meta.env.DEV) {
console.log(`🔍 BM25 index built (server mode): ${bm25DocCount} documents`);
}
// 4. Set currentGraphResult so the agent context builder works
currentGraphResult = { graph, fileContents: storedFileContents };
// 5. Load graph into LadybugDB for Cypher queries (optional — gracefully degrades)
try {
const lbug = await getLbugAdapter();
await lbug.loadGraphToLbug(graph, storedFileContents);
if (import.meta.env.DEV) {
const stats = await lbug.getLbugStats();
console.log('✅ LadybugDB hydrated (server mode):', stats);
}
} catch (err) {
// LadybugDB is optional — silently continue without it
if (import.meta.env.DEV) {
console.warn('⚠️ LadybugDB hydration failed (non-fatal):', err);
}
}
},
/**
* Execute a Cypher query against the LadybugDB database
* @param cypher - The Cypher query string