code-graph-context 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +156 -2
- package/dist/constants.js +167 -0
- package/dist/core/config/fairsquare-framework-schema.js +9 -7
- package/dist/core/config/nestjs-framework-schema.js +60 -43
- package/dist/core/config/schema.js +41 -2
- package/dist/core/embeddings/natural-language-to-cypher.service.js +166 -110
- package/dist/core/parsers/typescript-parser.js +1043 -747
- package/dist/core/parsers/workspace-parser.js +177 -194
- package/dist/core/utils/code-normalizer.js +299 -0
- package/dist/core/utils/file-change-detection.js +17 -2
- package/dist/core/utils/file-utils.js +40 -5
- package/dist/core/utils/graph-factory.js +161 -0
- package/dist/core/utils/shared-utils.js +79 -0
- package/dist/core/workspace/workspace-detector.js +59 -5
- package/dist/mcp/constants.js +141 -8
- package/dist/mcp/handlers/graph-generator.handler.js +1 -0
- package/dist/mcp/handlers/incremental-parse.handler.js +3 -6
- package/dist/mcp/handlers/parallel-import.handler.js +136 -0
- package/dist/mcp/handlers/streaming-import.handler.js +14 -59
- package/dist/mcp/mcp.server.js +1 -1
- package/dist/mcp/services/job-manager.js +5 -8
- package/dist/mcp/services/watch-manager.js +7 -18
- package/dist/mcp/tools/detect-dead-code.tool.js +413 -0
- package/dist/mcp/tools/detect-duplicate-code.tool.js +450 -0
- package/dist/mcp/tools/impact-analysis.tool.js +20 -4
- package/dist/mcp/tools/index.js +4 -0
- package/dist/mcp/tools/parse-typescript-project.tool.js +15 -14
- package/dist/mcp/workers/chunk-worker-pool.js +196 -0
- package/dist/mcp/workers/chunk-worker.types.js +4 -0
- package/dist/mcp/workers/chunk.worker.js +89 -0
- package/dist/mcp/workers/parse-coordinator.js +183 -0
- package/dist/mcp/workers/worker.pool.js +54 -0
- package/dist/storage/neo4j/neo4j.service.js +190 -10
- package/package.json +1 -1
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chunk Worker Pool
|
|
3
|
+
* Manages a pool of chunk workers for parallel parsing.
|
|
4
|
+
* Uses message passing (pull model): workers signal ready, coordinator sends chunks.
|
|
5
|
+
* Streams results as they complete for pipelined importing.
|
|
6
|
+
*/
|
|
7
|
+
import { cpus } from 'os';
|
|
8
|
+
import { dirname, join } from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { Worker } from 'worker_threads';
|
|
11
|
+
import { debugLog } from '../utils.js';
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = dirname(__filename);
|
|
14
|
+
export class ChunkWorkerPool {
|
|
15
|
+
config;
|
|
16
|
+
workers = [];
|
|
17
|
+
chunkQueue = [];
|
|
18
|
+
totalChunks = 0;
|
|
19
|
+
completedChunks = 0;
|
|
20
|
+
totalNodes = 0;
|
|
21
|
+
totalEdges = 0;
|
|
22
|
+
totalFiles = 0;
|
|
23
|
+
startTime = 0;
|
|
24
|
+
resolve = null;
|
|
25
|
+
reject = null;
|
|
26
|
+
onChunkComplete = null;
|
|
27
|
+
pendingCallbacks = [];
|
|
28
|
+
isShuttingDown = false;
|
|
29
|
+
constructor(config) {
|
|
30
|
+
this.config = config;
|
|
31
|
+
process.on('exit', () => {
|
|
32
|
+
this.forceTerminateAll();
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Process chunks in parallel using worker pool.
|
|
37
|
+
* Calls onChunkComplete for EACH result as it arrives (for pipelined importing).
|
|
38
|
+
* Returns final stats when all chunks AND all callbacks are complete.
|
|
39
|
+
*/
|
|
40
|
+
async processChunks(chunks, onChunkComplete) {
|
|
41
|
+
this.startTime = Date.now();
|
|
42
|
+
this.totalChunks = chunks.length;
|
|
43
|
+
this.completedChunks = 0;
|
|
44
|
+
this.totalNodes = 0;
|
|
45
|
+
this.totalEdges = 0;
|
|
46
|
+
this.totalFiles = 0;
|
|
47
|
+
this.onChunkComplete = onChunkComplete;
|
|
48
|
+
this.pendingCallbacks = [];
|
|
49
|
+
this.chunkQueue = chunks.map((files, index) => ({
|
|
50
|
+
type: 'chunk',
|
|
51
|
+
chunkIndex: index,
|
|
52
|
+
totalChunks: chunks.length,
|
|
53
|
+
files,
|
|
54
|
+
}));
|
|
55
|
+
const numWorkers = this.config.numWorkers ?? Math.floor(cpus().length * 0.75);
|
|
56
|
+
const actualWorkers = Math.min(numWorkers, chunks.length);
|
|
57
|
+
debugLog(`Spawning ${actualWorkers} chunk workers for ${chunks.length} chunks`);
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
this.resolve = resolve;
|
|
60
|
+
this.reject = reject;
|
|
61
|
+
for (let i = 0; i < actualWorkers; i++) {
|
|
62
|
+
this.spawnWorker();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
spawnWorker() {
|
|
67
|
+
const workerPath = join(__dirname, 'chunk.worker.js');
|
|
68
|
+
const workerConfig = {
|
|
69
|
+
projectPath: this.config.projectPath,
|
|
70
|
+
tsconfigPath: this.config.tsconfigPath,
|
|
71
|
+
projectId: this.config.projectId,
|
|
72
|
+
projectType: this.config.projectType,
|
|
73
|
+
};
|
|
74
|
+
const worker = new Worker(workerPath, {
|
|
75
|
+
workerData: workerConfig,
|
|
76
|
+
resourceLimits: {
|
|
77
|
+
maxOldGenerationSizeMb: 2048,
|
|
78
|
+
maxYoungGenerationSizeMb: 512,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
const state = { worker, busy: false };
|
|
82
|
+
this.workers.push(state);
|
|
83
|
+
worker.on('message', (msg) => {
|
|
84
|
+
this.handleWorkerMessage(state, msg);
|
|
85
|
+
});
|
|
86
|
+
worker.on('error', (error) => {
|
|
87
|
+
debugLog('Worker error', { error: error.message });
|
|
88
|
+
this.reject?.(error);
|
|
89
|
+
void this.shutdown();
|
|
90
|
+
});
|
|
91
|
+
worker.on('exit', (code) => {
|
|
92
|
+
if (code !== 0 && this.completedChunks < this.totalChunks) {
|
|
93
|
+
this.reject?.(new Error(`Worker exited with code ${code}`));
|
|
94
|
+
void this.shutdown();
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
handleWorkerMessage(state, msg) {
|
|
99
|
+
switch (msg.type) {
|
|
100
|
+
case 'ready':
|
|
101
|
+
state.busy = false;
|
|
102
|
+
this.dispatchNextChunk(state);
|
|
103
|
+
break;
|
|
104
|
+
case 'result':
|
|
105
|
+
this.handleResult(msg);
|
|
106
|
+
break;
|
|
107
|
+
case 'error':
|
|
108
|
+
debugLog(`Chunk ${msg.chunkIndex} failed`, { error: msg.error });
|
|
109
|
+
this.reject?.(new Error(`Chunk ${msg.chunkIndex} failed: ${msg.error}`));
|
|
110
|
+
void this.shutdown();
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
handleResult(msg) {
|
|
115
|
+
this.completedChunks++;
|
|
116
|
+
this.totalNodes += msg.nodes.length;
|
|
117
|
+
this.totalEdges += msg.edges.length;
|
|
118
|
+
this.totalFiles += msg.filesProcessed;
|
|
119
|
+
const result = {
|
|
120
|
+
chunkIndex: msg.chunkIndex,
|
|
121
|
+
nodes: msg.nodes,
|
|
122
|
+
edges: msg.edges,
|
|
123
|
+
filesProcessed: msg.filesProcessed,
|
|
124
|
+
sharedContext: msg.sharedContext,
|
|
125
|
+
deferredEdges: msg.deferredEdges,
|
|
126
|
+
};
|
|
127
|
+
const stats = this.getStats();
|
|
128
|
+
// Fire callback immediately - enables pipelined importing
|
|
129
|
+
if (this.onChunkComplete) {
|
|
130
|
+
const callbackPromise = this.onChunkComplete(result, stats).catch((err) => {
|
|
131
|
+
debugLog(`Import callback failed for chunk ${msg.chunkIndex}`, {
|
|
132
|
+
error: err instanceof Error ? err.message : String(err),
|
|
133
|
+
});
|
|
134
|
+
throw err;
|
|
135
|
+
});
|
|
136
|
+
this.pendingCallbacks.push(callbackPromise);
|
|
137
|
+
}
|
|
138
|
+
// Check if all parsing is done
|
|
139
|
+
if (this.completedChunks === this.totalChunks) {
|
|
140
|
+
this.completeWhenCallbacksDone();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async completeWhenCallbacksDone() {
|
|
144
|
+
try {
|
|
145
|
+
await Promise.all(this.pendingCallbacks);
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
this.reject?.(error instanceof Error ? error : new Error(String(error)));
|
|
149
|
+
await this.shutdown();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
await this.shutdown();
|
|
153
|
+
this.resolve?.(this.getStats());
|
|
154
|
+
}
|
|
155
|
+
getStats() {
|
|
156
|
+
return {
|
|
157
|
+
totalNodes: this.totalNodes,
|
|
158
|
+
totalEdges: this.totalEdges,
|
|
159
|
+
totalFiles: this.totalFiles,
|
|
160
|
+
chunksCompleted: this.completedChunks,
|
|
161
|
+
totalChunks: this.totalChunks,
|
|
162
|
+
elapsedMs: Date.now() - this.startTime,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
dispatchNextChunk(state) {
|
|
166
|
+
if (this.chunkQueue.length === 0) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const chunk = this.chunkQueue.shift();
|
|
170
|
+
state.busy = true;
|
|
171
|
+
state.worker.postMessage(chunk);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Graceful shutdown - lets workers finish cleanup
|
|
175
|
+
* Call this on normal completion
|
|
176
|
+
*/
|
|
177
|
+
async shutdown() {
|
|
178
|
+
if (this.isShuttingDown)
|
|
179
|
+
return;
|
|
180
|
+
this.isShuttingDown = true;
|
|
181
|
+
const exitPromises = this.workers.map(({ worker }) => {
|
|
182
|
+
return new Promise((resolve) => {
|
|
183
|
+
worker.on('exit', () => resolve());
|
|
184
|
+
worker.postMessage({ type: 'terminate' });
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
await Promise.race([Promise.all(exitPromises), new Promise((resolve) => setTimeout(resolve, 15000))]);
|
|
188
|
+
this.forceTerminateAll();
|
|
189
|
+
}
|
|
190
|
+
forceTerminateAll() {
|
|
191
|
+
for (const { worker } of this.workers) {
|
|
192
|
+
worker.terminate();
|
|
193
|
+
}
|
|
194
|
+
this.workers = [];
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chunk Worker
|
|
3
|
+
* Receives file chunks from coordinator, parses them, returns nodes/edges.
|
|
4
|
+
* Each worker creates its own parser with lazyLoad=true for memory efficiency.
|
|
5
|
+
*/
|
|
6
|
+
import { dirname, join } from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { parentPort, workerData } from 'worker_threads';
|
|
9
|
+
// Load environment variables in worker thread
|
|
10
|
+
import dotenv from 'dotenv';
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = dirname(__filename);
|
|
13
|
+
dotenv.config({ path: join(__dirname, '..', '..', '..', '.env') });
|
|
14
|
+
import { ParserFactory } from '../../core/parsers/parser-factory.js';
|
|
15
|
+
const config = workerData;
|
|
16
|
+
let parser = null;
|
|
17
|
+
const sendReady = () => {
|
|
18
|
+
const msg = { type: 'ready' };
|
|
19
|
+
parentPort?.postMessage(msg);
|
|
20
|
+
};
|
|
21
|
+
const sendResult = (result) => {
|
|
22
|
+
const msg = { type: 'result', ...result };
|
|
23
|
+
parentPort?.postMessage(msg);
|
|
24
|
+
};
|
|
25
|
+
const sendError = (chunkIndex, error) => {
|
|
26
|
+
const msg = {
|
|
27
|
+
type: 'error',
|
|
28
|
+
chunkIndex,
|
|
29
|
+
error: error.message,
|
|
30
|
+
stack: error.stack,
|
|
31
|
+
};
|
|
32
|
+
parentPort?.postMessage(msg);
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Initialize parser lazily on first chunk.
|
|
36
|
+
* Uses lazyLoad=true so parser only loads files we give it.
|
|
37
|
+
* projectType is already resolved by coordinator (no auto-detection here).
|
|
38
|
+
*/
|
|
39
|
+
const initParser = () => {
|
|
40
|
+
if (parser)
|
|
41
|
+
return parser;
|
|
42
|
+
parser = ParserFactory.createParser({
|
|
43
|
+
workspacePath: config.projectPath,
|
|
44
|
+
tsConfigPath: config.tsconfigPath,
|
|
45
|
+
projectType: config.projectType,
|
|
46
|
+
projectId: config.projectId,
|
|
47
|
+
lazyLoad: true, // Critical: only load files we're given
|
|
48
|
+
});
|
|
49
|
+
// Defer edge enhancements - coordinator will handle after all chunks complete
|
|
50
|
+
parser.setDeferEdgeEnhancements(true);
|
|
51
|
+
return parser;
|
|
52
|
+
};
|
|
53
|
+
const processChunk = async (files, chunkIndex) => {
|
|
54
|
+
try {
|
|
55
|
+
const p = initParser();
|
|
56
|
+
// Clear any accumulated data from previous chunks
|
|
57
|
+
p.clearParsedData();
|
|
58
|
+
// Parse chunk - skip deferred edge resolution (coordinator handles that)
|
|
59
|
+
const { nodes, edges } = await p.parseChunk(files, true);
|
|
60
|
+
// Get serialized shared context for merging in coordinator
|
|
61
|
+
const sharedContext = p.getSerializedSharedContext();
|
|
62
|
+
// Get deferred edges for cross-chunk resolution
|
|
63
|
+
const deferredEdges = p.getDeferredEdges();
|
|
64
|
+
sendResult({
|
|
65
|
+
chunkIndex,
|
|
66
|
+
nodes,
|
|
67
|
+
edges,
|
|
68
|
+
filesProcessed: files.length,
|
|
69
|
+
sharedContext,
|
|
70
|
+
deferredEdges,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
sendError(chunkIndex, error instanceof Error ? error : new Error(String(error)));
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
parentPort?.on('message', async (msg) => {
|
|
78
|
+
switch (msg.type) {
|
|
79
|
+
case 'chunk':
|
|
80
|
+
await processChunk(msg.files, msg.chunkIndex);
|
|
81
|
+
sendReady();
|
|
82
|
+
break;
|
|
83
|
+
case 'terminate':
|
|
84
|
+
parser?.clearParsedData();
|
|
85
|
+
process.exit(0);
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
sendReady();
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse Coordinator
|
|
3
|
+
* Runs TypeScript parsing in a separate thread to avoid blocking the MCP server.
|
|
4
|
+
* For large projects, spawns a worker pool for parallel chunk parsing.
|
|
5
|
+
*/
|
|
6
|
+
import { dirname, join } from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { parentPort, workerData } from 'worker_threads';
|
|
9
|
+
// Load environment variables in worker thread
|
|
10
|
+
import dotenv from 'dotenv';
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = dirname(__filename);
|
|
13
|
+
dotenv.config({ path: join(__dirname, '..', '..', '..', '.env') });
|
|
14
|
+
import { EmbeddingsService } from '../../core/embeddings/embeddings.service.js';
|
|
15
|
+
import { ParserFactory } from '../../core/parsers/parser-factory.js';
|
|
16
|
+
import { WorkspaceParser } from '../../core/parsers/workspace-parser.js';
|
|
17
|
+
import { debugLog } from '../../core/utils/file-utils.js';
|
|
18
|
+
import { getProjectName, UPSERT_PROJECT_QUERY, UPDATE_PROJECT_STATUS_QUERY } from '../../core/utils/project-id.js';
|
|
19
|
+
import { WorkspaceDetector } from '../../core/workspace/index.js';
|
|
20
|
+
import { Neo4jService, QUERIES } from '../../storage/neo4j/neo4j.service.js';
|
|
21
|
+
import { PARSING } from '../constants.js';
|
|
22
|
+
import { GraphGeneratorHandler } from '../handlers/graph-generator.handler.js';
|
|
23
|
+
import { ParallelImportHandler } from '../handlers/parallel-import.handler.js';
|
|
24
|
+
import { StreamingImportHandler } from '../handlers/streaming-import.handler.js';
|
|
25
|
+
const sendMessage = (msg) => {
|
|
26
|
+
parentPort?.postMessage(msg);
|
|
27
|
+
};
|
|
28
|
+
const sendProgress = (phase, filesProcessed, filesTotal, nodesImported, edgesImported, currentChunk, totalChunks) => {
|
|
29
|
+
sendMessage({
|
|
30
|
+
type: 'progress',
|
|
31
|
+
data: { phase, filesProcessed, filesTotal, nodesImported, edgesImported, currentChunk, totalChunks },
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
const runParser = async () => {
|
|
35
|
+
const config = workerData;
|
|
36
|
+
const startTime = Date.now();
|
|
37
|
+
let resolvedProjectId = config.projectId;
|
|
38
|
+
let neo4jService = null;
|
|
39
|
+
try {
|
|
40
|
+
sendProgress('discovery', 0, 0, 0, 0, 0, 0);
|
|
41
|
+
neo4jService = new Neo4jService();
|
|
42
|
+
const embeddingsService = new EmbeddingsService();
|
|
43
|
+
const graphGeneratorHandler = new GraphGeneratorHandler(neo4jService, embeddingsService);
|
|
44
|
+
const lazyLoad = true;
|
|
45
|
+
const workspaceDetector = new WorkspaceDetector();
|
|
46
|
+
await debugLog('Detecting workspace', { projectPath: config.projectPath });
|
|
47
|
+
const workspaceConfig = await workspaceDetector.detect(config.projectPath);
|
|
48
|
+
await debugLog('Workspace detection result', {
|
|
49
|
+
type: workspaceConfig.type,
|
|
50
|
+
rootPath: workspaceConfig.rootPath,
|
|
51
|
+
packageCount: workspaceConfig.packages.length,
|
|
52
|
+
});
|
|
53
|
+
let detectedProjectType;
|
|
54
|
+
if (config.projectType === 'auto') {
|
|
55
|
+
detectedProjectType = await ParserFactory.detectProjectType(config.projectPath);
|
|
56
|
+
await debugLog('Auto-detected project type', { projectType: detectedProjectType });
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
detectedProjectType = config.projectType;
|
|
60
|
+
}
|
|
61
|
+
let parser;
|
|
62
|
+
if (workspaceConfig.type !== 'single' && workspaceConfig.packages.length > 1) {
|
|
63
|
+
await debugLog('Using WorkspaceParser', {
|
|
64
|
+
type: workspaceConfig.type,
|
|
65
|
+
packageCount: workspaceConfig.packages.length,
|
|
66
|
+
});
|
|
67
|
+
parser = new WorkspaceParser(workspaceConfig, config.projectId, lazyLoad, detectedProjectType);
|
|
68
|
+
resolvedProjectId = parser.getProjectId();
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
await debugLog('Using single project mode');
|
|
72
|
+
parser = ParserFactory.createParser({
|
|
73
|
+
workspacePath: config.projectPath,
|
|
74
|
+
tsConfigPath: config.tsconfigPath,
|
|
75
|
+
projectType: detectedProjectType,
|
|
76
|
+
projectId: config.projectId,
|
|
77
|
+
lazyLoad,
|
|
78
|
+
});
|
|
79
|
+
resolvedProjectId = parser.getProjectId();
|
|
80
|
+
}
|
|
81
|
+
const sourceFiles = await parser.discoverSourceFiles();
|
|
82
|
+
const totalFiles = sourceFiles.length;
|
|
83
|
+
const chunkSize = config.chunkSize > 0 ? config.chunkSize : PARSING.defaultChunkSize;
|
|
84
|
+
graphGeneratorHandler.setProjectId(resolvedProjectId);
|
|
85
|
+
await neo4jService.run(QUERIES.CLEAR_PROJECT, { projectId: resolvedProjectId });
|
|
86
|
+
const projectName = await getProjectName(config.projectPath);
|
|
87
|
+
await neo4jService.run(UPSERT_PROJECT_QUERY, {
|
|
88
|
+
projectId: resolvedProjectId,
|
|
89
|
+
name: projectName,
|
|
90
|
+
path: config.projectPath,
|
|
91
|
+
status: 'parsing',
|
|
92
|
+
});
|
|
93
|
+
await debugLog('Project node created', { projectId: resolvedProjectId, name: projectName });
|
|
94
|
+
let totalNodesImported = 0;
|
|
95
|
+
let totalEdgesImported = 0;
|
|
96
|
+
const onProgress = async (progress) => {
|
|
97
|
+
sendProgress(progress.phase, progress.current, progress.total, progress.details?.nodesCreated ?? 0, progress.details?.edgesCreated ?? 0, progress.details?.chunkIndex ?? 0, progress.details?.totalChunks ?? 0);
|
|
98
|
+
};
|
|
99
|
+
const useParallel = totalFiles >= PARSING.parallelThreshold;
|
|
100
|
+
if (useParallel) {
|
|
101
|
+
await debugLog('Using parallel parsing', { totalFiles });
|
|
102
|
+
const parallelHandler = new ParallelImportHandler(graphGeneratorHandler);
|
|
103
|
+
const result = await parallelHandler.importProjectParallel(parser, sourceFiles, {
|
|
104
|
+
chunkSize,
|
|
105
|
+
projectId: resolvedProjectId,
|
|
106
|
+
projectPath: config.projectPath,
|
|
107
|
+
tsconfigPath: config.tsconfigPath,
|
|
108
|
+
projectType: detectedProjectType,
|
|
109
|
+
onProgress,
|
|
110
|
+
});
|
|
111
|
+
totalNodesImported = result.nodesImported;
|
|
112
|
+
totalEdgesImported = result.edgesImported;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
await debugLog('Using sequential parsing', { totalFiles });
|
|
116
|
+
const streamingHandler = new StreamingImportHandler(graphGeneratorHandler);
|
|
117
|
+
const result = await streamingHandler.importProjectStreaming(parser, {
|
|
118
|
+
chunkSize,
|
|
119
|
+
projectId: resolvedProjectId,
|
|
120
|
+
onProgress,
|
|
121
|
+
});
|
|
122
|
+
totalNodesImported = result.nodesImported;
|
|
123
|
+
totalEdgesImported = result.edgesImported;
|
|
124
|
+
}
|
|
125
|
+
await neo4jService.run(UPDATE_PROJECT_STATUS_QUERY, {
|
|
126
|
+
projectId: resolvedProjectId,
|
|
127
|
+
status: 'complete',
|
|
128
|
+
nodeCount: totalNodesImported,
|
|
129
|
+
edgeCount: totalEdgesImported,
|
|
130
|
+
});
|
|
131
|
+
await debugLog('Project node updated', {
|
|
132
|
+
projectId: resolvedProjectId,
|
|
133
|
+
status: 'complete',
|
|
134
|
+
nodeCount: totalNodesImported,
|
|
135
|
+
edgeCount: totalEdgesImported,
|
|
136
|
+
});
|
|
137
|
+
sendMessage({
|
|
138
|
+
type: 'complete',
|
|
139
|
+
data: {
|
|
140
|
+
nodesImported: totalNodesImported,
|
|
141
|
+
edgesImported: totalEdgesImported,
|
|
142
|
+
elapsedMs: Date.now() - startTime,
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
try {
|
|
148
|
+
const serviceForUpdate = neo4jService ?? new Neo4jService();
|
|
149
|
+
await serviceForUpdate.run(UPDATE_PROJECT_STATUS_QUERY, {
|
|
150
|
+
projectId: resolvedProjectId,
|
|
151
|
+
status: 'failed',
|
|
152
|
+
nodeCount: 0,
|
|
153
|
+
edgeCount: 0,
|
|
154
|
+
});
|
|
155
|
+
if (!neo4jService) {
|
|
156
|
+
await serviceForUpdate.close();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// Ignore errors updating project status on failure
|
|
161
|
+
}
|
|
162
|
+
sendMessage({
|
|
163
|
+
type: 'error',
|
|
164
|
+
error: error.message ?? String(error),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
if (neo4jService) {
|
|
169
|
+
try {
|
|
170
|
+
await neo4jService.close();
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// Ignore cleanup errors
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
runParser().catch((err) => {
|
|
179
|
+
sendMessage({
|
|
180
|
+
type: 'error',
|
|
181
|
+
error: err.message ?? String(err),
|
|
182
|
+
});
|
|
183
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Worker } from 'worker_threads';
|
|
2
|
+
export class ParallelPool {
|
|
3
|
+
workerPath;
|
|
4
|
+
numWorkers;
|
|
5
|
+
constructor(workerPath, numWorkers = 2) {
|
|
6
|
+
this.workerPath = workerPath;
|
|
7
|
+
this.numWorkers = numWorkers;
|
|
8
|
+
}
|
|
9
|
+
async run(items) {
|
|
10
|
+
const start = Date.now();
|
|
11
|
+
const indexBuffer = new SharedArrayBuffer(4);
|
|
12
|
+
const sharedIndex = new Int32Array(indexBuffer);
|
|
13
|
+
const workerPromises = Array.from({ length: this.numWorkers }, (_, id) => this.spawnWorker(id, items, indexBuffer));
|
|
14
|
+
const workerResults = await Promise.all(workerPromises);
|
|
15
|
+
const results = [];
|
|
16
|
+
const workerTaskCounts = [];
|
|
17
|
+
for (const { results: map, count } of workerResults) {
|
|
18
|
+
workerTaskCounts.push(count);
|
|
19
|
+
for (const [i, result] of map) {
|
|
20
|
+
results[i] = result;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
results,
|
|
25
|
+
stats: {
|
|
26
|
+
workerTaskCounts,
|
|
27
|
+
totalTasks: items.length,
|
|
28
|
+
totalTimeMs: Date.now() - start,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
spawnWorker(workerId, items, indexBuffer) {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const worker = new Worker(this.workerPath, {
|
|
35
|
+
workerData: {
|
|
36
|
+
items,
|
|
37
|
+
indexBuffer,
|
|
38
|
+
total: items.length,
|
|
39
|
+
workerId,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
worker.on('message', (result) => {
|
|
43
|
+
worker.terminate();
|
|
44
|
+
resolve(result);
|
|
45
|
+
});
|
|
46
|
+
worker.on('error', reject);
|
|
47
|
+
worker.on('exit', (code) => {
|
|
48
|
+
if (code !== 0) {
|
|
49
|
+
reject(new Error(`Worker ${workerId} exited with code ${code}`));
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|