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 connections keyed by repoId.
5
- * Connections are lazily opened on first query and evicted
6
- * after idle timeout or when pool exceeds max size (LRU).
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 connections.
19
- * If repoId is provided, close only that connection.
20
- * If omitted, close all connections in the pool.
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 connection is active
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 connections keyed by repoId.
5
- * Connections are lazily opened on first query and evicted
6
- * after idle timeout or when pool exceeds max size (LRU).
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 connection if pool is at capacity
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 single pool entry
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
- try {
59
- entry.conn.close();
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
- pool.set(repoId, { db, conn, lastUsed: Date.now(), dbPath });
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
- * Execute a query on a specific repo's connection
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 queryResult = await entry.conn.query(cypher);
131
- const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
132
- const rows = await result.getAll();
133
- return rows;
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 connections.
137
- * If repoId is provided, close only that connection.
138
- * If omitted, close all connections in the pool.
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 connection is active
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.2.5",
3
+ "version": "1.2.6",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",