mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-08 22:22:52 +00:00
Merge pull request #400 from JayceeB1/fix/server-mode-worker-hydration
closes issue #398
This commit is contained in:
commit
0a200a51cb
12 changed files with 1286 additions and 70 deletions
1072
gitnexus-web/package-lock.json
generated
1072
gitnexus-web/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -6,7 +6,8 @@
|
|||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@huggingface/transformers": "^3.0.0",
|
||||
|
|
@ -65,6 +66,7 @@
|
|||
"tree-sitter-wasms": "^0.1.13",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.0",
|
||||
"vite-plugin-static-copy": "^3.1.4"
|
||||
"vite-plugin-static-copy": "^3.1.4",
|
||||
"vitest": "^4.0.18"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ const AppContent = () => {
|
|||
isRightPanelOpen,
|
||||
runPipeline,
|
||||
runPipelineFromFiles,
|
||||
hydrateServerGraph,
|
||||
isSettingsPanelOpen,
|
||||
setSettingsPanelOpen,
|
||||
refreshLLMSettings,
|
||||
|
|
@ -133,7 +134,7 @@ const AppContent = () => {
|
|||
}
|
||||
}, [setViewMode, setGraph, setFileContents, setProgress, setProjectName, runPipelineFromFiles, startEmbeddings, initializeAgent]);
|
||||
|
||||
const handleServerConnect = useCallback((result: ConnectToServerResult) => {
|
||||
const handleServerConnect = useCallback(async (result: ConnectToServerResult) => {
|
||||
// Extract project name from repoPath
|
||||
const repoPath = result.repoInfo.repoPath;
|
||||
const projectName = repoPath.split('/').pop() || 'server-project';
|
||||
|
|
@ -156,9 +157,14 @@ const AppContent = () => {
|
|||
}
|
||||
setFileContents(fileMap);
|
||||
|
||||
// Transition directly to exploring view
|
||||
setViewMode('exploring');
|
||||
setProgress(null);
|
||||
try {
|
||||
await hydrateServerGraph(result);
|
||||
|
||||
// Transition directly to exploring view
|
||||
setViewMode('exploring');
|
||||
} finally {
|
||||
setProgress(null);
|
||||
}
|
||||
|
||||
// Hydrate the worker-side DB (LadybugDB + BM25) so Query/Processes/embeddings work
|
||||
hydrateWorkerFromServer(result.nodes, result.relationships, result.fileContents).then(() => {
|
||||
|
|
@ -182,7 +188,7 @@ const AppContent = () => {
|
|||
initializeAgent(projectName);
|
||||
}
|
||||
});
|
||||
}, [setViewMode, setGraph, setFileContents, setProjectName, setProgress, initializeAgent, startEmbeddings, hydrateWorkerFromServer]);
|
||||
}, [setViewMode, setGraph, setFileContents, setProjectName, setProgress, initializeAgent, startEmbeddings, hydrateServerGraph, hydrateWorkerFromServer]);
|
||||
|
||||
// Auto-connect when ?server query param is present (bookmarkable shortcut)
|
||||
const autoConnectRan = useRef(false);
|
||||
|
|
@ -214,7 +220,7 @@ const AppContent = () => {
|
|||
setProgress({ phase: 'extracting', percent: 97, message: 'Processing...', detail: 'Extracting file contents' });
|
||||
}
|
||||
}).then(async (result) => {
|
||||
handleServerConnect(result);
|
||||
await handleServerConnect(result);
|
||||
|
||||
// Store server URL and fetch available repos for the repo switcher
|
||||
setServerBaseUrl(baseUrl);
|
||||
|
|
@ -257,7 +263,7 @@ const AppContent = () => {
|
|||
onFileSelect={handleFileSelect}
|
||||
onGitClone={handleGitClone}
|
||||
onServerConnect={async (result, serverUrl) => {
|
||||
handleServerConnect(result);
|
||||
await handleServerConnect(result);
|
||||
if (serverUrl) {
|
||||
const baseUrl = normalizeServerUrl(serverUrl);
|
||||
setServerBaseUrl(baseUrl);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
NodeTableName,
|
||||
} from './schema';
|
||||
import { generateAllCSVs } from './csv-generator';
|
||||
import { getQueryRows } from './query-result';
|
||||
|
||||
// Holds the reference to the dynamically loaded module
|
||||
let lbug: any = null;
|
||||
|
|
@ -190,7 +191,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?.() ?? countRes.getAllObjects?.() ?? countRes.getAllRows?.() ?? []);
|
||||
const countRows = await getQueryRows(countRes);
|
||||
const countRow = countRows[0];
|
||||
const count = countRow ? (countRow.cnt ?? countRow[0] ?? 0) : 0;
|
||||
totalNodes += Number(count);
|
||||
|
|
@ -293,8 +294,8 @@ export const executeQuery = async (cypher: string): Promise<any[]> => {
|
|||
});
|
||||
}
|
||||
|
||||
// Collect all rows (handle API differences across LadybugDB versions)
|
||||
const allRows = await (result.getAll?.() ?? result.getAllObjects?.() ?? result.getAllRows?.() ?? []);
|
||||
// Collect all rows
|
||||
const allRows = await getQueryRows(result);
|
||||
const rows: any[] = [];
|
||||
for (const row of allRows) {
|
||||
// Convert tuple to named object if we have column names and row is array
|
||||
|
|
@ -331,7 +332,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?.() ?? nodeResult.getAllObjects?.() ?? nodeResult.getAllRows?.() ?? []);
|
||||
const nodeRows = await getQueryRows(nodeResult);
|
||||
const nodeRow = nodeRows[0];
|
||||
totalNodes += Number(nodeRow?.cnt ?? nodeRow?.[0] ?? 0);
|
||||
} catch {
|
||||
|
|
@ -343,7 +344,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?.() ?? edgeResult.getAllObjects?.() ?? edgeResult.getAllRows?.() ?? []);
|
||||
const edgeRows = await getQueryRows(edgeResult);
|
||||
const edgeRow = edgeRows[0];
|
||||
totalEdges = Number(edgeRow?.cnt ?? edgeRow?.[0] ?? 0);
|
||||
} catch {
|
||||
|
|
@ -408,7 +409,7 @@ export const executePrepared = async (
|
|||
|
||||
const result = await conn.execute(stmt, params);
|
||||
|
||||
const rows = await (result.getAll?.() ?? result.getAllObjects?.() ?? result.getAllRows?.() ?? []);
|
||||
const rows = await getQueryRows(result);
|
||||
|
||||
await stmt.close();
|
||||
return rows;
|
||||
|
|
@ -472,7 +473,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?.() ?? nodeResult.getAllObjects?.() ?? nodeResult.getAllRows?.() ?? []);
|
||||
const nodeRows = await getQueryRows(nodeResult);
|
||||
const nodeRow = nodeRows[0];
|
||||
if (nodeRow) {
|
||||
testNodeId = nodeRow.id ?? nodeRow[0];
|
||||
|
|
@ -509,7 +510,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?.() ?? verifyResult.getAllObjects?.() ?? verifyResult.getAllRows?.() ?? []);
|
||||
const verifyRows = await getQueryRows(verifyResult);
|
||||
const verifyRow = verifyRows[0];
|
||||
const storedEmb = verifyRow?.emb ?? verifyRow?.[0];
|
||||
|
||||
|
|
|
|||
41
gitnexus-web/src/core/lbug/query-result.test.ts
Normal file
41
gitnexus-web/src/core/lbug/query-result.test.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { getQueryRows } from './query-result';
|
||||
|
||||
describe('getQueryRows', () => {
|
||||
it('prefers getAllObjects when available', async () => {
|
||||
const rows = [{ name: 'foo' }];
|
||||
const getAllObjects = vi.fn().mockResolvedValue(rows);
|
||||
const getAllRows = vi.fn().mockResolvedValue([{ name: 'bar' }]);
|
||||
const getAll = vi.fn().mockResolvedValue([['baz']]);
|
||||
|
||||
await expect(getQueryRows({ getAllObjects, getAllRows, getAll })).resolves.toEqual(rows);
|
||||
|
||||
expect(getAllObjects).toHaveBeenCalledTimes(1);
|
||||
expect(getAllRows).not.toHaveBeenCalled();
|
||||
expect(getAll).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('falls back to getAllRows when getAllObjects is absent', async () => {
|
||||
const rows = [{ name: 'bar' }];
|
||||
const getAllRows = vi.fn().mockResolvedValue(rows);
|
||||
const getAll = vi.fn().mockResolvedValue([['baz']]);
|
||||
|
||||
await expect(getQueryRows({ getAllRows, getAll })).resolves.toEqual(rows);
|
||||
|
||||
expect(getAllRows).toHaveBeenCalledTimes(1);
|
||||
expect(getAll).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('falls back to getAll as a final fallback', async () => {
|
||||
const rows = [['baz']];
|
||||
const getAll = vi.fn().mockResolvedValue(rows);
|
||||
|
||||
await expect(getQueryRows({ getAll })).resolves.toEqual(rows);
|
||||
expect(getAll).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('throws when no supported query API is exposed', async () => {
|
||||
await expect(getQueryRows({})).rejects.toThrow('Unsupported LadybugDB QueryResult shape');
|
||||
});
|
||||
});
|
||||
21
gitnexus-web/src/core/lbug/query-result.ts
Normal file
21
gitnexus-web/src/core/lbug/query-result.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
export const getQueryRows = async (result: unknown): Promise<any[]> => {
|
||||
if (!result || typeof result !== 'object') return [];
|
||||
|
||||
const queryResult = result as {
|
||||
getAllObjects?: () => Promise<any[]>;
|
||||
getAllRows?: () => Promise<any[]>;
|
||||
getAll?: () => Promise<any[]>;
|
||||
};
|
||||
|
||||
if (typeof queryResult.getAllObjects === 'function') {
|
||||
return await queryResult.getAllObjects();
|
||||
}
|
||||
if (typeof queryResult.getAllRows === 'function') {
|
||||
return await queryResult.getAllRows();
|
||||
}
|
||||
if (typeof queryResult.getAll === 'function') {
|
||||
return await queryResult.getAll();
|
||||
}
|
||||
|
||||
throw new Error('Unsupported LadybugDB QueryResult shape');
|
||||
};
|
||||
|
|
@ -123,6 +123,7 @@ interface AppState {
|
|||
// Worker API (shared across app)
|
||||
runPipeline: (file: File, onProgress: (p: PipelineProgress) => void, clusteringConfig?: ProviderConfig) => Promise<PipelineResult>;
|
||||
runPipelineFromFiles: (files: FileEntry[], onProgress: (p: PipelineProgress) => void, clusteringConfig?: ProviderConfig) => Promise<PipelineResult>;
|
||||
hydrateServerGraph: (result: ConnectToServerResult) => Promise<void>;
|
||||
runQuery: (cypher: string) => Promise<any[]>;
|
||||
isDatabaseReady: () => Promise<boolean>;
|
||||
hydrateWorkerFromServer: (nodes: any[], relationships: any[], fileContents: Record<string, string>) => Promise<void>;
|
||||
|
|
@ -467,6 +468,16 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
|
|||
return deserializePipelineResult(serializedResult, createKnowledgeGraph);
|
||||
}, []);
|
||||
|
||||
const hydrateServerGraph = useCallback(async (result: ConnectToServerResult): Promise<void> => {
|
||||
const api = apiRef.current;
|
||||
if (!api) throw new Error('Worker not initialized');
|
||||
await api.hydrateServerGraph({
|
||||
nodes: result.nodes,
|
||||
relationships: result.relationships,
|
||||
fileContents: result.fileContents,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const runQuery = useCallback(async (cypher: string): Promise<any[]> => {
|
||||
const api = apiRef.current;
|
||||
if (!api) throw new Error('Worker not initialized');
|
||||
|
|
@ -1028,6 +1039,8 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
|
|||
for (const [p, c] of Object.entries(result.fileContents)) fileMap.set(p, c);
|
||||
setFileContents(fileMap);
|
||||
|
||||
await hydrateServerGraph(result);
|
||||
|
||||
setViewMode('exploring');
|
||||
setProgress(null);
|
||||
|
||||
|
|
@ -1056,7 +1069,7 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
|
|||
});
|
||||
setTimeout(() => { setViewMode('exploring'); setProgress(null); }, 3000);
|
||||
}
|
||||
}, [serverBaseUrl, setProgress, setViewMode, setProjectName, setGraph, setFileContents, initializeAgent, startEmbeddings, hydrateWorkerFromServer, setHighlightedNodeIds, clearAIToolHighlights, clearBlastRadius, setSelectedNode, setQueryResult, setCodeReferences, setCodePanelOpen, setCodeReferenceFocus]);
|
||||
}, [serverBaseUrl, setProgress, setViewMode, setProjectName, setGraph, setFileContents, initializeAgent, startEmbeddings, hydrateServerGraph, hydrateWorkerFromServer, setHighlightedNodeIds, clearAIToolHighlights, clearBlastRadius, setSelectedNode, setQueryResult, setCodeReferences, setCodePanelOpen, setCodeReferenceFocus]);
|
||||
|
||||
const removeCodeReference = useCallback((id: string) => {
|
||||
setCodeReferences(prev => {
|
||||
|
|
@ -1159,6 +1172,7 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
|
|||
switchRepo,
|
||||
runPipeline,
|
||||
runPipelineFromFiles,
|
||||
hydrateServerGraph,
|
||||
runQuery,
|
||||
isDatabaseReady,
|
||||
hydrateWorkerFromServer,
|
||||
|
|
|
|||
4
gitnexus-web/src/types/lbug-wasm.d.ts
vendored
4
gitnexus-web/src/types/lbug-wasm.d.ts
vendored
|
|
@ -12,7 +12,9 @@ declare module '@ladybugdb/wasm-core' {
|
|||
close(): Promise<void>;
|
||||
}
|
||||
export interface QueryResult {
|
||||
getAll(): Promise<any[]>;
|
||||
getAll?(): Promise<any[]>;
|
||||
getAllRows?(): Promise<any[]>;
|
||||
getAllObjects?(): Promise<any[]>;
|
||||
hasNext(): Promise<boolean>;
|
||||
getNext(): Promise<any>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { enrichClustersBatch, ClusterMemberInfo, ClusterEnrichment } from '../co
|
|||
import { CommunityNode } from '../core/ingestion/community-processor';
|
||||
import { PipelineResult } from '../types/pipeline';
|
||||
import { buildCodebaseContext, type CodebaseContext } from '../core/llm/context-builder';
|
||||
import { hydrateSerializedServerGraph } from './server-graph-hydration';
|
||||
import {
|
||||
buildBM25Index,
|
||||
searchBM25,
|
||||
|
|
@ -56,6 +57,29 @@ let enrichmentCancelled = false;
|
|||
// Chat cancellation flag
|
||||
let chatCancelled = false;
|
||||
|
||||
const loadResultIntoWorkerState = async (result: PipelineResult): Promise<void> => {
|
||||
currentGraphResult = result;
|
||||
storedFileContents = result.fileContents;
|
||||
|
||||
const bm25DocCount = buildBM25Index(storedFileContents);
|
||||
if (import.meta.env.DEV) {
|
||||
console.log(`🔍 BM25 index built: ${bm25DocCount} documents`);
|
||||
}
|
||||
|
||||
try {
|
||||
const lbug = await getLbugAdapter();
|
||||
await lbug.loadGraphToLbug(result.graph, result.fileContents);
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
const stats = await lbug.getLbugStats();
|
||||
console.log('LadybugDB loaded:', stats);
|
||||
console.log('📁 Stored', storedFileContents.size, 'files for grep/read tools');
|
||||
}
|
||||
} catch {
|
||||
// LadybugDB is optional - silently continue without it
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// HTTP helpers for backend mode
|
||||
// ============================================================
|
||||
|
|
@ -163,18 +187,7 @@ const workerApi = {
|
|||
console.log('🔧 runPipeline called with clusteringConfig:', !!clusteringConfig);
|
||||
// Run the actual pipeline
|
||||
const result = await runIngestionPipeline(file, onProgress);
|
||||
currentGraphResult = result;
|
||||
|
||||
// Store file contents for grep/read tools (full content, not truncated)
|
||||
storedFileContents = result.fileContents;
|
||||
|
||||
// Build BM25 index for keyword search (instant, ~100ms)
|
||||
const bm25DocCount = buildBM25Index(storedFileContents);
|
||||
if (import.meta.env.DEV) {
|
||||
console.log(`🔍 BM25 index built: ${bm25DocCount} documents`);
|
||||
}
|
||||
|
||||
// Load graph into LadybugDB for querying (optional - gracefully degrades)
|
||||
// Load graph into local indexes for query/AI features.
|
||||
try {
|
||||
onProgress({
|
||||
phase: 'complete',
|
||||
|
|
@ -186,15 +199,7 @@ const workerApi = {
|
|||
nodesCreated: result.graph.nodeCount,
|
||||
},
|
||||
});
|
||||
|
||||
const lbug = await getLbugAdapter();
|
||||
await lbug.loadGraphToLbug(result.graph, result.fileContents);
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
const stats = await lbug.getLbugStats();
|
||||
console.log('LadybugDB loaded:', stats);
|
||||
console.log('📁 Stored', storedFileContents.size, 'files for grep/read tools');
|
||||
}
|
||||
await loadResultIntoWorkerState(result);
|
||||
} catch {
|
||||
// LadybugDB is optional - silently continue without it
|
||||
}
|
||||
|
|
@ -311,18 +316,7 @@ const workerApi = {
|
|||
|
||||
// Run the pipeline
|
||||
const result = await runPipelineFromFiles(files, onProgress);
|
||||
currentGraphResult = result;
|
||||
|
||||
// Store file contents for grep/read tools (full content, not truncated)
|
||||
storedFileContents = result.fileContents;
|
||||
|
||||
// Build BM25 index for keyword search (instant, ~100ms)
|
||||
const bm25DocCount = buildBM25Index(storedFileContents);
|
||||
if (import.meta.env.DEV) {
|
||||
console.log(`🔍 BM25 index built: ${bm25DocCount} documents`);
|
||||
}
|
||||
|
||||
// Load graph into LadybugDB for querying (optional - gracefully degrades)
|
||||
// Load graph into local indexes for query/AI features.
|
||||
try {
|
||||
onProgress({
|
||||
phase: 'complete',
|
||||
|
|
@ -334,15 +328,7 @@ const workerApi = {
|
|||
nodesCreated: result.graph.nodeCount,
|
||||
},
|
||||
});
|
||||
|
||||
const lbug = await getLbugAdapter();
|
||||
await lbug.loadGraphToLbug(result.graph, result.fileContents);
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
const stats = await lbug.getLbugStats();
|
||||
console.log('LadybugDB loaded:', stats);
|
||||
console.log('📁 Stored', storedFileContents.size, 'files for grep/read tools');
|
||||
}
|
||||
await loadResultIntoWorkerState(result);
|
||||
} catch {
|
||||
// LadybugDB is optional - silently continue without it
|
||||
}
|
||||
|
|
@ -357,6 +343,10 @@ const workerApi = {
|
|||
return serializePipelineResult(result);
|
||||
},
|
||||
|
||||
async hydrateServerGraph(serialized: SerializablePipelineResult): Promise<void> {
|
||||
await hydrateSerializedServerGraph(serialized, loadResultIntoWorkerState);
|
||||
},
|
||||
|
||||
// ============================================================
|
||||
// Embedding Pipeline Methods
|
||||
// ============================================================
|
||||
|
|
|
|||
58
gitnexus-web/src/workers/server-graph-hydration.test.ts
Normal file
58
gitnexus-web/src/workers/server-graph-hydration.test.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { SerializablePipelineResult } from '../types/pipeline';
|
||||
import { buildPipelineResultFromSerialized, hydrateSerializedServerGraph } from './server-graph-hydration';
|
||||
|
||||
describe('server graph hydration helpers', () => {
|
||||
const serialized: SerializablePipelineResult = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'file:src/foo.ts',
|
||||
label: 'File' as const,
|
||||
properties: {
|
||||
name: 'foo.ts',
|
||||
filePath: 'src/foo.ts',
|
||||
content: 'export function foo() {}',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'func:src/foo.ts:foo',
|
||||
label: 'Function' as const,
|
||||
properties: {
|
||||
name: 'foo',
|
||||
filePath: 'src/foo.ts',
|
||||
},
|
||||
},
|
||||
],
|
||||
relationships: [
|
||||
{
|
||||
source: 'file:src/foo.ts',
|
||||
target: 'func:src/foo.ts:foo',
|
||||
type: 'CONTAINS' as const,
|
||||
properties: { type: 'CONTAINS' },
|
||||
},
|
||||
],
|
||||
fileContents: {
|
||||
'src/foo.ts': 'export function foo() {}',
|
||||
},
|
||||
};
|
||||
|
||||
it('rebuilds a graph and file map from serialized server payloads', () => {
|
||||
const result = buildPipelineResultFromSerialized(serialized);
|
||||
|
||||
expect(result.graph.nodeCount).toBe(2);
|
||||
expect(result.graph.relationshipCount).toBe(1);
|
||||
expect(result.fileContents.get('src/foo.ts')).toBe('export function foo() {}');
|
||||
});
|
||||
|
||||
it('delegates the rebuilt result to the worker-side loader', async () => {
|
||||
const loadResult = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
const result = await hydrateSerializedServerGraph(serialized, loadResult);
|
||||
|
||||
expect(loadResult).toHaveBeenCalledTimes(1);
|
||||
expect(loadResult).toHaveBeenCalledWith(result);
|
||||
expect(result.graph.nodeCount).toBe(2);
|
||||
expect(result.fileContents.size).toBe(1);
|
||||
});
|
||||
});
|
||||
24
gitnexus-web/src/workers/server-graph-hydration.ts
Normal file
24
gitnexus-web/src/workers/server-graph-hydration.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { createKnowledgeGraph } from '../core/graph/graph';
|
||||
import type { PipelineResult, SerializablePipelineResult } from '../types/pipeline';
|
||||
|
||||
export const buildPipelineResultFromSerialized = (
|
||||
serialized: SerializablePipelineResult,
|
||||
): PipelineResult => {
|
||||
const graph = createKnowledgeGraph();
|
||||
serialized.nodes.forEach((node) => graph.addNode(node));
|
||||
serialized.relationships.forEach((relationship) => graph.addRelationship(relationship));
|
||||
|
||||
return {
|
||||
graph,
|
||||
fileContents: new Map(Object.entries(serialized.fileContents)),
|
||||
};
|
||||
};
|
||||
|
||||
export const hydrateSerializedServerGraph = async (
|
||||
serialized: SerializablePipelineResult,
|
||||
loadResult: (result: PipelineResult) => Promise<void>,
|
||||
): Promise<PipelineResult> => {
|
||||
const result = buildPipelineResultFromSerialized(serialized);
|
||||
await loadResult(result);
|
||||
return result;
|
||||
};
|
||||
|
|
@ -20,5 +20,6 @@
|
|||
},
|
||||
"types": ["vite/client"]
|
||||
},
|
||||
"include": ["src"]
|
||||
"include": ["src"],
|
||||
"exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue