gitnexus 1.2.4 → 1.2.6
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/dist/cli/index.js +1 -1
- package/dist/cli/wiki.js +1 -1
- package/dist/core/wiki/generator.d.ts +6 -0
- package/dist/core/wiki/generator.js +27 -10
- package/dist/core/wiki/llm-client.d.ts +6 -2
- package/dist/core/wiki/llm-client.js +53 -4
- package/dist/mcp/core/kuzu-adapter.d.ts +17 -9
- package/dist/mcp/core/kuzu-adapter.js +99 -25
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -53,7 +53,7 @@ program
|
|
|
53
53
|
.command('wiki [path]')
|
|
54
54
|
.description('Generate repository wiki from knowledge graph')
|
|
55
55
|
.option('-f, --force', 'Force full regeneration even if up to date')
|
|
56
|
-
.option('--model <model>', 'LLM model name (default:
|
|
56
|
+
.option('--model <model>', 'LLM model name (default: minimax/minimax-m2.5)')
|
|
57
57
|
.option('--base-url <url>', 'LLM API base URL (default: OpenAI)')
|
|
58
58
|
.option('--api-key <key>', 'LLM API key (saved to ~/.gitnexus/config.json)')
|
|
59
59
|
.option('--concurrency <n>', 'Parallel LLM calls (default: 3)', '3')
|
package/dist/cli/wiki.js
CHANGED
|
@@ -141,7 +141,7 @@ export const wikiCommand = async (inputPath, options) => {
|
|
|
141
141
|
let defaultModel;
|
|
142
142
|
if (choice === '2') {
|
|
143
143
|
baseUrl = 'https://openrouter.ai/api/v1';
|
|
144
|
-
defaultModel = '
|
|
144
|
+
defaultModel = 'minimax/minimax-m2.5';
|
|
145
145
|
}
|
|
146
146
|
else if (choice === '3') {
|
|
147
147
|
baseUrl = await prompt(' Base URL (e.g. http://localhost:11434/v1): ');
|
|
@@ -44,6 +44,12 @@ export declare class WikiGenerator {
|
|
|
44
44
|
private onProgress;
|
|
45
45
|
private failedModules;
|
|
46
46
|
constructor(repoPath: string, storagePath: string, kuzuPath: string, llmConfig: LLMConfig, options?: WikiOptions, onProgress?: ProgressCallback);
|
|
47
|
+
private lastPercent;
|
|
48
|
+
/**
|
|
49
|
+
* Create streaming options that report LLM progress to the progress bar.
|
|
50
|
+
* Uses the last known percent so streaming doesn't reset the bar backwards.
|
|
51
|
+
*/
|
|
52
|
+
private streamOpts;
|
|
47
53
|
/**
|
|
48
54
|
* Main entry point. Runs the full pipeline or incremental update.
|
|
49
55
|
*/
|
|
@@ -41,7 +41,26 @@ export class WikiGenerator {
|
|
|
41
41
|
this.llmConfig = llmConfig;
|
|
42
42
|
this.maxTokensPerModule = options.maxTokensPerModule ?? DEFAULT_MAX_TOKENS_PER_MODULE;
|
|
43
43
|
this.concurrency = options.concurrency ?? 3;
|
|
44
|
-
|
|
44
|
+
const progressFn = onProgress || (() => { });
|
|
45
|
+
this.onProgress = (phase, percent, detail) => {
|
|
46
|
+
if (percent > 0)
|
|
47
|
+
this.lastPercent = percent;
|
|
48
|
+
progressFn(phase, percent, detail);
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
lastPercent = 0;
|
|
52
|
+
/**
|
|
53
|
+
* Create streaming options that report LLM progress to the progress bar.
|
|
54
|
+
* Uses the last known percent so streaming doesn't reset the bar backwards.
|
|
55
|
+
*/
|
|
56
|
+
streamOpts(label, fixedPercent) {
|
|
57
|
+
return {
|
|
58
|
+
onChunk: (chars) => {
|
|
59
|
+
const tokens = Math.round(chars / 4);
|
|
60
|
+
const pct = fixedPercent ?? this.lastPercent;
|
|
61
|
+
this.onProgress('stream', pct, `${label} (${tokens} tok)`);
|
|
62
|
+
},
|
|
63
|
+
};
|
|
45
64
|
}
|
|
46
65
|
/**
|
|
47
66
|
* Main entry point. Runs the full pipeline or incremental update.
|
|
@@ -153,8 +172,7 @@ export class WikiGenerator {
|
|
|
153
172
|
}
|
|
154
173
|
catch (err) {
|
|
155
174
|
this.failedModules.push(node.name);
|
|
156
|
-
|
|
157
|
-
reportProgress(node.name);
|
|
175
|
+
reportProgress(`Failed: ${node.name}`);
|
|
158
176
|
return 0;
|
|
159
177
|
}
|
|
160
178
|
});
|
|
@@ -172,8 +190,7 @@ export class WikiGenerator {
|
|
|
172
190
|
}
|
|
173
191
|
catch (err) {
|
|
174
192
|
this.failedModules.push(node.name);
|
|
175
|
-
|
|
176
|
-
reportProgress(node.name);
|
|
193
|
+
reportProgress(`Failed: ${node.name}`);
|
|
177
194
|
}
|
|
178
195
|
}
|
|
179
196
|
// Phase 3: Generate overview
|
|
@@ -216,7 +233,7 @@ export class WikiGenerator {
|
|
|
216
233
|
FILE_LIST: fileList,
|
|
217
234
|
DIRECTORY_TREE: dirTree,
|
|
218
235
|
});
|
|
219
|
-
const response = await callLLM(prompt, this.llmConfig, GROUPING_SYSTEM_PROMPT);
|
|
236
|
+
const response = await callLLM(prompt, this.llmConfig, GROUPING_SYSTEM_PROMPT, this.streamOpts('Grouping files', 15));
|
|
220
237
|
const grouping = this.parseGroupingResponse(response.content, files);
|
|
221
238
|
// Convert to tree nodes
|
|
222
239
|
const tree = [];
|
|
@@ -353,7 +370,7 @@ export class WikiGenerator {
|
|
|
353
370
|
INCOMING_CALLS: formatCallEdges(interCalls.incoming),
|
|
354
371
|
PROCESSES: formatProcesses(processes),
|
|
355
372
|
});
|
|
356
|
-
const response = await callLLM(prompt, this.llmConfig, MODULE_SYSTEM_PROMPT);
|
|
373
|
+
const response = await callLLM(prompt, this.llmConfig, MODULE_SYSTEM_PROMPT, this.streamOpts(node.name));
|
|
357
374
|
// Write page with front matter
|
|
358
375
|
const pageContent = `# ${node.name}\n\n${response.content}`;
|
|
359
376
|
await fs.writeFile(path.join(this.wikiDir, `${node.slug}.md`), pageContent, 'utf-8');
|
|
@@ -389,7 +406,7 @@ export class WikiGenerator {
|
|
|
389
406
|
CROSS_MODULE_CALLS: formatCallEdges(crossCalls),
|
|
390
407
|
CROSS_PROCESSES: formatProcesses(processes),
|
|
391
408
|
});
|
|
392
|
-
const response = await callLLM(prompt, this.llmConfig, PARENT_SYSTEM_PROMPT);
|
|
409
|
+
const response = await callLLM(prompt, this.llmConfig, PARENT_SYSTEM_PROMPT, this.streamOpts(node.name));
|
|
393
410
|
const pageContent = `# ${node.name}\n\n${response.content}`;
|
|
394
411
|
await fs.writeFile(path.join(this.wikiDir, `${node.slug}.md`), pageContent, 'utf-8');
|
|
395
412
|
}
|
|
@@ -425,7 +442,7 @@ export class WikiGenerator {
|
|
|
425
442
|
MODULE_EDGES: edgesText,
|
|
426
443
|
TOP_PROCESSES: formatProcesses(topProcesses),
|
|
427
444
|
});
|
|
428
|
-
const response = await callLLM(prompt, this.llmConfig, OVERVIEW_SYSTEM_PROMPT);
|
|
445
|
+
const response = await callLLM(prompt, this.llmConfig, OVERVIEW_SYSTEM_PROMPT, this.streamOpts('Generating overview', 88));
|
|
429
446
|
const pageContent = `# ${path.basename(this.repoPath)} — Wiki\n\n${response.content}`;
|
|
430
447
|
await fs.writeFile(path.join(this.wikiDir, 'overview.md'), pageContent, 'utf-8');
|
|
431
448
|
}
|
|
@@ -699,7 +716,7 @@ export class WikiGenerator {
|
|
|
699
716
|
// On rate limit, reduce concurrency temporarily
|
|
700
717
|
if (err.message?.includes('429')) {
|
|
701
718
|
activeConcurrency = Math.max(1, activeConcurrency - 1);
|
|
702
|
-
this.onProgress('modules',
|
|
719
|
+
this.onProgress('modules', this.lastPercent, `Rate limited — concurrency → ${activeConcurrency}`);
|
|
703
720
|
// Re-queue the item
|
|
704
721
|
idx--;
|
|
705
722
|
setTimeout(next, 5000);
|
|
@@ -29,8 +29,12 @@ export declare function resolveLLMConfig(overrides?: Partial<LLMConfig>): Promis
|
|
|
29
29
|
* Estimate token count from text (rough heuristic: ~4 chars per token).
|
|
30
30
|
*/
|
|
31
31
|
export declare function estimateTokens(text: string): number;
|
|
32
|
+
export interface CallLLMOptions {
|
|
33
|
+
onChunk?: (charsReceived: number) => void;
|
|
34
|
+
}
|
|
32
35
|
/**
|
|
33
36
|
* Call an OpenAI-compatible LLM API.
|
|
34
|
-
*
|
|
37
|
+
* Uses streaming when onChunk callback is provided for real-time progress.
|
|
38
|
+
* Retries up to 3 times on transient failures (429, 5xx, network errors).
|
|
35
39
|
*/
|
|
36
|
-
export declare function callLLM(prompt: string, config: LLMConfig, systemPrompt?: string): Promise<LLMResponse>;
|
|
40
|
+
export declare function callLLM(prompt: string, config: LLMConfig, systemPrompt?: string, options?: CallLLMOptions): Promise<LLMResponse>;
|
|
@@ -25,11 +25,11 @@ export async function resolveLLMConfig(overrides) {
|
|
|
25
25
|
baseUrl: overrides?.baseUrl
|
|
26
26
|
|| process.env.GITNEXUS_LLM_BASE_URL
|
|
27
27
|
|| savedConfig.baseUrl
|
|
28
|
-
|| 'https://
|
|
28
|
+
|| 'https://openrouter.ai/api/v1',
|
|
29
29
|
model: overrides?.model
|
|
30
30
|
|| process.env.GITNEXUS_MODEL
|
|
31
31
|
|| savedConfig.model
|
|
32
|
-
|| '
|
|
32
|
+
|| 'minimax/minimax-m2.5',
|
|
33
33
|
maxTokens: overrides?.maxTokens ?? 16_384,
|
|
34
34
|
temperature: overrides?.temperature ?? 0,
|
|
35
35
|
};
|
|
@@ -42,21 +42,25 @@ export function estimateTokens(text) {
|
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
44
|
* Call an OpenAI-compatible LLM API.
|
|
45
|
-
*
|
|
45
|
+
* Uses streaming when onChunk callback is provided for real-time progress.
|
|
46
|
+
* Retries up to 3 times on transient failures (429, 5xx, network errors).
|
|
46
47
|
*/
|
|
47
|
-
export async function callLLM(prompt, config, systemPrompt) {
|
|
48
|
+
export async function callLLM(prompt, config, systemPrompt, options) {
|
|
48
49
|
const messages = [];
|
|
49
50
|
if (systemPrompt) {
|
|
50
51
|
messages.push({ role: 'system', content: systemPrompt });
|
|
51
52
|
}
|
|
52
53
|
messages.push({ role: 'user', content: prompt });
|
|
53
54
|
const url = `${config.baseUrl.replace(/\/+$/, '')}/chat/completions`;
|
|
55
|
+
const useStream = !!options?.onChunk;
|
|
54
56
|
const body = {
|
|
55
57
|
model: config.model,
|
|
56
58
|
messages,
|
|
57
59
|
max_tokens: config.maxTokens,
|
|
58
60
|
temperature: config.temperature,
|
|
59
61
|
};
|
|
62
|
+
if (useStream)
|
|
63
|
+
body.stream = true;
|
|
60
64
|
const MAX_RETRIES = 3;
|
|
61
65
|
let lastError = null;
|
|
62
66
|
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
@@ -85,6 +89,11 @@ export async function callLLM(prompt, config, systemPrompt) {
|
|
|
85
89
|
}
|
|
86
90
|
throw new Error(`LLM API error (${response.status}): ${errorText.slice(0, 500)}`);
|
|
87
91
|
}
|
|
92
|
+
// Streaming path
|
|
93
|
+
if (useStream && response.body) {
|
|
94
|
+
return await readSSEStream(response.body, options.onChunk);
|
|
95
|
+
}
|
|
96
|
+
// Non-streaming path
|
|
88
97
|
const json = await response.json();
|
|
89
98
|
const choice = json.choices?.[0];
|
|
90
99
|
if (!choice?.message?.content) {
|
|
@@ -108,6 +117,46 @@ export async function callLLM(prompt, config, systemPrompt) {
|
|
|
108
117
|
}
|
|
109
118
|
throw lastError || new Error('LLM call failed after retries');
|
|
110
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Read an SSE stream from an OpenAI-compatible streaming response.
|
|
122
|
+
*/
|
|
123
|
+
async function readSSEStream(body, onChunk) {
|
|
124
|
+
const decoder = new TextDecoder();
|
|
125
|
+
const reader = body.getReader();
|
|
126
|
+
let content = '';
|
|
127
|
+
let buffer = '';
|
|
128
|
+
while (true) {
|
|
129
|
+
const { done, value } = await reader.read();
|
|
130
|
+
if (done)
|
|
131
|
+
break;
|
|
132
|
+
buffer += decoder.decode(value, { stream: true });
|
|
133
|
+
const lines = buffer.split('\n');
|
|
134
|
+
buffer = lines.pop() || '';
|
|
135
|
+
for (const line of lines) {
|
|
136
|
+
const trimmed = line.trim();
|
|
137
|
+
if (!trimmed || !trimmed.startsWith('data: '))
|
|
138
|
+
continue;
|
|
139
|
+
const data = trimmed.slice(6);
|
|
140
|
+
if (data === '[DONE]')
|
|
141
|
+
continue;
|
|
142
|
+
try {
|
|
143
|
+
const parsed = JSON.parse(data);
|
|
144
|
+
const delta = parsed.choices?.[0]?.delta?.content;
|
|
145
|
+
if (delta) {
|
|
146
|
+
content += delta;
|
|
147
|
+
onChunk(content.length);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// Skip malformed SSE chunks
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (!content) {
|
|
156
|
+
throw new Error('LLM returned empty streaming response');
|
|
157
|
+
}
|
|
158
|
+
return { content };
|
|
159
|
+
}
|
|
111
160
|
function sleep(ms) {
|
|
112
161
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
113
162
|
}
|
|
@@ -1,26 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* KuzuDB Adapter (Connection Pool)
|
|
3
3
|
*
|
|
4
|
-
* Manages a pool of KuzuDB
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Manages a pool of KuzuDB databases keyed by repoId, each with
|
|
5
|
+
* multiple Connection objects for safe concurrent query execution.
|
|
6
|
+
*
|
|
7
|
+
* KuzuDB Connections are NOT thread-safe — a single Connection
|
|
8
|
+
* segfaults if concurrent .query() calls hit it simultaneously.
|
|
9
|
+
* This adapter provides a checkout/return connection pool so each
|
|
10
|
+
* concurrent query gets its own Connection from the same Database.
|
|
11
|
+
*
|
|
12
|
+
* @see https://docs.kuzudb.com/concurrency — multiple Connections
|
|
13
|
+
* from the same Database is the officially supported concurrency pattern.
|
|
7
14
|
*/
|
|
8
15
|
/**
|
|
9
|
-
* Initialize (or reuse) a connection for a specific repo.
|
|
16
|
+
* Initialize (or reuse) a Database + connection pool for a specific repo.
|
|
10
17
|
* Retries on lock errors (e.g., when `gitnexus analyze` is running).
|
|
11
18
|
*/
|
|
12
19
|
export declare const initKuzu: (repoId: string, dbPath: string) => Promise<void>;
|
|
13
20
|
/**
|
|
14
|
-
* Execute a query on a specific repo's connection
|
|
21
|
+
* Execute a query on a specific repo's connection pool.
|
|
22
|
+
* Automatically checks out a connection, runs the query, and returns it.
|
|
15
23
|
*/
|
|
16
24
|
export declare const executeQuery: (repoId: string, cypher: string) => Promise<any[]>;
|
|
17
25
|
/**
|
|
18
|
-
* Close one or all
|
|
19
|
-
* If repoId is provided, close only that
|
|
20
|
-
* If omitted, close all
|
|
26
|
+
* Close one or all repo pools.
|
|
27
|
+
* If repoId is provided, close only that repo's connections.
|
|
28
|
+
* If omitted, close all repos.
|
|
21
29
|
*/
|
|
22
30
|
export declare const closeKuzu: (repoId?: string) => Promise<void>;
|
|
23
31
|
/**
|
|
24
|
-
* Check if a specific repo's
|
|
32
|
+
* Check if a specific repo's pool is active
|
|
25
33
|
*/
|
|
26
34
|
export declare const isKuzuReady: (repoId: string) => boolean;
|
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* KuzuDB Adapter (Connection Pool)
|
|
3
3
|
*
|
|
4
|
-
* Manages a pool of KuzuDB
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Manages a pool of KuzuDB databases keyed by repoId, each with
|
|
5
|
+
* multiple Connection objects for safe concurrent query execution.
|
|
6
|
+
*
|
|
7
|
+
* KuzuDB Connections are NOT thread-safe — a single Connection
|
|
8
|
+
* segfaults if concurrent .query() calls hit it simultaneously.
|
|
9
|
+
* This adapter provides a checkout/return connection pool so each
|
|
10
|
+
* concurrent query gets its own Connection from the same Database.
|
|
11
|
+
*
|
|
12
|
+
* @see https://docs.kuzudb.com/concurrency — multiple Connections
|
|
13
|
+
* from the same Database is the officially supported concurrency pattern.
|
|
7
14
|
*/
|
|
8
15
|
import fs from 'fs/promises';
|
|
9
16
|
import kuzu from 'kuzu';
|
|
10
17
|
const pool = new Map();
|
|
18
|
+
/** Max repos in the pool (LRU eviction) */
|
|
11
19
|
const MAX_POOL_SIZE = 5;
|
|
20
|
+
/** Idle timeout before closing a repo's connections */
|
|
12
21
|
const IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
22
|
+
/** Max connections per repo (caps concurrent queries per repo) */
|
|
23
|
+
const MAX_CONNS_PER_REPO = 8;
|
|
24
|
+
/** Connections created eagerly on init */
|
|
25
|
+
const INITIAL_CONNS_PER_REPO = 2;
|
|
13
26
|
let idleTimer = null;
|
|
14
27
|
/**
|
|
15
28
|
* Start the idle cleanup timer (runs every 60s)
|
|
@@ -25,13 +38,12 @@ function ensureIdleTimer() {
|
|
|
25
38
|
}
|
|
26
39
|
}
|
|
27
40
|
}, 60_000);
|
|
28
|
-
// Don't keep the process alive just for this timer
|
|
29
41
|
if (idleTimer && typeof idleTimer === 'object' && 'unref' in idleTimer) {
|
|
30
42
|
idleTimer.unref();
|
|
31
43
|
}
|
|
32
44
|
}
|
|
33
45
|
/**
|
|
34
|
-
* Evict the least-recently-used
|
|
46
|
+
* Evict the least-recently-used repo if pool is at capacity
|
|
35
47
|
*/
|
|
36
48
|
function evictLRU() {
|
|
37
49
|
if (pool.size < MAX_POOL_SIZE)
|
|
@@ -49,26 +61,42 @@ function evictLRU() {
|
|
|
49
61
|
}
|
|
50
62
|
}
|
|
51
63
|
/**
|
|
52
|
-
* Close a
|
|
64
|
+
* Close all connections for a repo and remove it from the pool
|
|
53
65
|
*/
|
|
54
66
|
function closeOne(repoId) {
|
|
55
67
|
const entry = pool.get(repoId);
|
|
56
68
|
if (!entry)
|
|
57
69
|
return;
|
|
58
|
-
|
|
59
|
-
|
|
70
|
+
for (const conn of entry.available) {
|
|
71
|
+
try {
|
|
72
|
+
conn.close();
|
|
73
|
+
}
|
|
74
|
+
catch { }
|
|
60
75
|
}
|
|
61
|
-
catch { }
|
|
62
76
|
try {
|
|
63
77
|
entry.db.close();
|
|
64
78
|
}
|
|
65
79
|
catch { }
|
|
66
80
|
pool.delete(repoId);
|
|
67
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Create a new Connection from a repo's Database.
|
|
84
|
+
* Silences stdout to prevent native module output from corrupting MCP stdio.
|
|
85
|
+
*/
|
|
86
|
+
function createConnection(db) {
|
|
87
|
+
const origWrite = process.stdout.write;
|
|
88
|
+
process.stdout.write = (() => true);
|
|
89
|
+
try {
|
|
90
|
+
return new kuzu.Connection(db);
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
process.stdout.write = origWrite;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
68
96
|
const LOCK_RETRY_ATTEMPTS = 3;
|
|
69
97
|
const LOCK_RETRY_DELAY_MS = 2000;
|
|
70
98
|
/**
|
|
71
|
-
* Initialize (or reuse) a connection for a specific repo.
|
|
99
|
+
* Initialize (or reuse) a Database + connection pool for a specific repo.
|
|
72
100
|
* Retries on lock errors (e.g., when `gitnexus analyze` is running).
|
|
73
101
|
*/
|
|
74
102
|
export const initKuzu = async (repoId, dbPath) => {
|
|
@@ -90,17 +118,19 @@ export const initKuzu = async (repoId, dbPath) => {
|
|
|
90
118
|
// avoids lock conflicts when `gitnexus analyze` is writing.
|
|
91
119
|
let lastError = null;
|
|
92
120
|
for (let attempt = 1; attempt <= LOCK_RETRY_ATTEMPTS; attempt++) {
|
|
93
|
-
// Silence stdout during KuzuDB init — native module may write to stdout
|
|
94
|
-
// which corrupts the MCP stdio protocol.
|
|
95
121
|
const origWrite = process.stdout.write;
|
|
96
122
|
process.stdout.write = (() => true);
|
|
97
123
|
try {
|
|
98
124
|
const db = new kuzu.Database(dbPath, 0, // bufferManagerSize (default)
|
|
99
125
|
false, // enableCompression (default)
|
|
100
126
|
true);
|
|
101
|
-
const conn = new kuzu.Connection(db);
|
|
102
127
|
process.stdout.write = origWrite;
|
|
103
|
-
|
|
128
|
+
// Pre-create a small pool of connections
|
|
129
|
+
const available = [];
|
|
130
|
+
for (let i = 0; i < INITIAL_CONNS_PER_REPO; i++) {
|
|
131
|
+
available.push(createConnection(db));
|
|
132
|
+
}
|
|
133
|
+
pool.set(repoId, { db, available, checkedOut: 0, waiters: [], lastUsed: Date.now(), dbPath });
|
|
104
134
|
ensureIdleTimer();
|
|
105
135
|
return;
|
|
106
136
|
}
|
|
@@ -111,7 +141,6 @@ export const initKuzu = async (repoId, dbPath) => {
|
|
|
111
141
|
|| lastError.message.includes('lock');
|
|
112
142
|
if (!isLockError || attempt === LOCK_RETRY_ATTEMPTS)
|
|
113
143
|
break;
|
|
114
|
-
// Wait before retrying — analyze may be mid-rebuild
|
|
115
144
|
await new Promise(resolve => setTimeout(resolve, LOCK_RETRY_DELAY_MS * attempt));
|
|
116
145
|
}
|
|
117
146
|
}
|
|
@@ -119,7 +148,47 @@ export const initKuzu = async (repoId, dbPath) => {
|
|
|
119
148
|
`Retry later. (${lastError?.message || 'unknown error'})`);
|
|
120
149
|
};
|
|
121
150
|
/**
|
|
122
|
-
*
|
|
151
|
+
* Checkout a connection from the pool.
|
|
152
|
+
* Returns an available connection, or creates a new one if under the cap.
|
|
153
|
+
* If all connections are busy and at cap, queues the caller until one is returned.
|
|
154
|
+
*/
|
|
155
|
+
function checkout(entry) {
|
|
156
|
+
// Fast path: grab an available connection
|
|
157
|
+
if (entry.available.length > 0) {
|
|
158
|
+
entry.checkedOut++;
|
|
159
|
+
return Promise.resolve(entry.available.pop());
|
|
160
|
+
}
|
|
161
|
+
// Grow the pool if under the cap
|
|
162
|
+
const totalConns = entry.available.length + entry.checkedOut;
|
|
163
|
+
if (totalConns < MAX_CONNS_PER_REPO) {
|
|
164
|
+
entry.checkedOut++;
|
|
165
|
+
return Promise.resolve(createConnection(entry.db));
|
|
166
|
+
}
|
|
167
|
+
// At capacity — queue the caller. checkin() will resolve this when
|
|
168
|
+
// a connection is returned, handing it directly to the next waiter.
|
|
169
|
+
return new Promise(resolve => {
|
|
170
|
+
entry.waiters.push(resolve);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Return a connection to the pool after use.
|
|
175
|
+
* If there are queued waiters, hand the connection directly to the next one
|
|
176
|
+
* instead of putting it back in the available array (avoids race conditions).
|
|
177
|
+
*/
|
|
178
|
+
function checkin(entry, conn) {
|
|
179
|
+
if (entry.waiters.length > 0) {
|
|
180
|
+
// Hand directly to the next waiter — no intermediate available state
|
|
181
|
+
const waiter = entry.waiters.shift();
|
|
182
|
+
waiter(conn);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
entry.checkedOut--;
|
|
186
|
+
entry.available.push(conn);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Execute a query on a specific repo's connection pool.
|
|
191
|
+
* Automatically checks out a connection, runs the query, and returns it.
|
|
123
192
|
*/
|
|
124
193
|
export const executeQuery = async (repoId, cypher) => {
|
|
125
194
|
const entry = pool.get(repoId);
|
|
@@ -127,22 +196,27 @@ export const executeQuery = async (repoId, cypher) => {
|
|
|
127
196
|
throw new Error(`KuzuDB not initialized for repo "${repoId}". Call initKuzu first.`);
|
|
128
197
|
}
|
|
129
198
|
entry.lastUsed = Date.now();
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
199
|
+
const conn = await checkout(entry);
|
|
200
|
+
try {
|
|
201
|
+
const queryResult = await conn.query(cypher);
|
|
202
|
+
const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
|
|
203
|
+
const rows = await result.getAll();
|
|
204
|
+
return rows;
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
checkin(entry, conn);
|
|
208
|
+
}
|
|
134
209
|
};
|
|
135
210
|
/**
|
|
136
|
-
* Close one or all
|
|
137
|
-
* If repoId is provided, close only that
|
|
138
|
-
* If omitted, close all
|
|
211
|
+
* Close one or all repo pools.
|
|
212
|
+
* If repoId is provided, close only that repo's connections.
|
|
213
|
+
* If omitted, close all repos.
|
|
139
214
|
*/
|
|
140
215
|
export const closeKuzu = async (repoId) => {
|
|
141
216
|
if (repoId) {
|
|
142
217
|
closeOne(repoId);
|
|
143
218
|
return;
|
|
144
219
|
}
|
|
145
|
-
// Close all
|
|
146
220
|
for (const id of [...pool.keys()]) {
|
|
147
221
|
closeOne(id);
|
|
148
222
|
}
|
|
@@ -152,6 +226,6 @@ export const closeKuzu = async (repoId) => {
|
|
|
152
226
|
}
|
|
153
227
|
};
|
|
154
228
|
/**
|
|
155
|
-
* Check if a specific repo's
|
|
229
|
+
* Check if a specific repo's pool is active
|
|
156
230
|
*/
|
|
157
231
|
export const isKuzuReady = (repoId) => pool.has(repoId);
|
package/package.json
CHANGED