gitnexus 1.2.5 → 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.
|
@@ -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