gitnexus 1.6.2-rc.2 → 1.6.2-rc.3

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,6 +1,8 @@
1
1
  import fs from 'fs/promises';
2
2
  import { createReadStream, createWriteStream } from 'fs';
3
3
  import { createInterface } from 'readline';
4
+ import { once } from 'events';
5
+ import { finished } from 'stream/promises';
4
6
  import path from 'path';
5
7
  import lbug from '@ladybugdb/core';
6
8
  import { NODE_TABLES, REL_TABLE_NAME, SCHEMA_QUERIES, EMBEDDING_TABLE_NAME, } from './schema.js';
@@ -25,100 +27,82 @@ export const splitRelCsvByLabelPair = async (csvPath, csvDir, validTables, getNo
25
27
  const pairWriteStreams = new Map();
26
28
  let skippedRels = 0;
27
29
  let totalValidRels = 0;
28
- await new Promise((resolve, reject) => {
29
- const inputStream = createReadStream(csvPath, 'utf-8');
30
- const rl = createInterface({
31
- input: inputStream,
32
- crlfDelay: Infinity,
33
- });
34
- // Track which streams are already waiting for drain to prevent
35
- // listener accumulation. rl.pause() is not synchronous — buffered
36
- // line events continue firing after pause(), and without this guard
37
- // each line targeting the same pairKey would add another drain listener.
38
- const waitingForDrain = new Set();
39
- let settled = false;
40
- const cleanup = (err) => {
41
- if (settled)
42
- return;
43
- settled = true;
44
- try {
45
- rl.close();
46
- }
47
- catch { }
48
- try {
49
- inputStream.destroy();
50
- }
51
- catch { }
52
- for (const ws of pairWriteStreams.values()) {
53
- try {
54
- ws.destroy();
55
- }
56
- catch { }
57
- }
58
- reject(err);
59
- };
30
+ const inputStream = createReadStream(csvPath, 'utf-8');
31
+ const rl = createInterface({ input: inputStream, crlfDelay: Infinity });
32
+ // If any pair WriteStream errors (disk full, EMFILE, etc.) or the input
33
+ // stream fails, we need to abort the pending `once(ws, 'drain')` await.
34
+ // An AbortController gives us one signal to cancel all pending waits
35
+ // without a custom state machine.
36
+ const abortOnError = new AbortController();
37
+ let streamError = null;
38
+ const markStreamError = (err) => {
39
+ streamError ??= err;
40
+ abortOnError.abort(err);
41
+ };
42
+ try {
43
+ // `for await (const line of rl)` replaces the old manual
44
+ // on('line')/pause()/resume()/waitingForDrain state machine: readline's
45
+ // async iterator naturally serializes line delivery with our awaits, so
46
+ // at most one ws can be in backpressure at a time and we just await its
47
+ // 'drain' event.
60
48
  let isFirst = true;
61
- rl.on('line', (line) => {
49
+ for await (const line of rl) {
50
+ if (streamError)
51
+ throw streamError;
62
52
  if (isFirst) {
63
53
  relHeader = line;
64
54
  isFirst = false;
65
- return;
55
+ continue;
66
56
  }
67
57
  if (!line.trim())
68
- return;
58
+ continue;
69
59
  const match = line.match(/"([^"]*)","([^"]*)"/);
70
60
  if (!match) {
71
61
  skippedRels++;
72
- return;
62
+ continue;
73
63
  }
74
64
  const fromLabel = getNodeLabel(match[1]);
75
65
  const toLabel = getNodeLabel(match[2]);
76
66
  if (!validTables.has(fromLabel) || !validTables.has(toLabel)) {
77
67
  skippedRels++;
78
- return;
68
+ continue;
79
69
  }
80
70
  const pairKey = `${fromLabel}|${toLabel}`;
81
71
  let ws = pairWriteStreams.get(pairKey);
82
72
  if (!ws) {
83
73
  const pairCsvPath = path.join(csvDir, `rel_${fromLabel}_${toLabel}.csv`);
84
74
  ws = wsFactory(pairCsvPath);
85
- // If any per-pair WriteStream errors (disk full, EMFILE, etc.),
86
- // tear down everything and reject the Promise. Without this handler,
87
- // a stream error while rl is paused waiting for drain would cause
88
- // the drain callback to never fire and the Promise to hang forever.
89
- ws.on('error', cleanup);
90
- ws.write(relHeader + '\n');
75
+ ws.on('error', markStreamError);
91
76
  pairWriteStreams.set(pairKey, ws);
92
77
  relsByPairMeta.set(pairKey, { csvPath: pairCsvPath, rows: 0 });
78
+ if (!ws.write(relHeader + '\n')) {
79
+ await once(ws, 'drain', { signal: abortOnError.signal });
80
+ }
81
+ }
82
+ if (!ws.write(line + '\n')) {
83
+ await once(ws, 'drain', { signal: abortOnError.signal });
93
84
  }
94
- const ok = ws.write(line + '\n');
95
85
  relsByPairMeta.get(pairKey).rows++;
96
86
  totalValidRels++;
97
- // Handle backpressure: pause reading when the write buffer is full,
98
- // resume when the stream drains. Prevents unbounded memory growth
99
- // on repos with millions of relationships.
100
- // Guard with waitingForDrain to ensure only one drain listener is
101
- // registered per stream at a time — rl.pause() doesn't stop buffered
102
- // line events immediately. Only resume when ALL streams have drained
103
- // to avoid writing into still-full streams.
104
- if (!ok && !waitingForDrain.has(pairKey)) {
105
- waitingForDrain.add(pairKey);
106
- rl.pause();
107
- ws.once('drain', () => {
108
- waitingForDrain.delete(pairKey);
109
- if (waitingForDrain.size === 0)
110
- rl.resume();
111
- });
112
- }
113
- });
114
- rl.on('close', () => {
115
- if (!settled) {
116
- settled = true;
117
- resolve();
118
- }
119
- });
120
- rl.on('error', cleanup);
121
- });
87
+ }
88
+ if (streamError)
89
+ throw streamError;
90
+ }
91
+ catch (err) {
92
+ // Tear down everything so no fd is left dangling. If the abort was caused
93
+ // by a stream error, rethrow that error (more actionable than AbortError).
94
+ for (const ws of pairWriteStreams.values())
95
+ ws.destroy();
96
+ inputStream.destroy();
97
+ throw streamError ?? err;
98
+ }
99
+ finally {
100
+ // Readline 'close' fires before the underlying fs.ReadStream releases its
101
+ // fd — on Windows that race caused ENOTEMPTY on the parent dir.
102
+ // stream/promises.finished is the stdlib "wait until this stream is fully
103
+ // closed" primitive and handles both success and error paths.
104
+ await finished(inputStream).catch(() => { });
105
+ }
122
106
  return { relHeader, relsByPairMeta, pairWriteStreams, skippedRels, totalValidRels };
123
107
  };
124
108
  let db = null;
@@ -332,15 +316,13 @@ export const loadGraphToLbug = async (graph, repoPath, storagePath, onProgress)
332
316
  }
333
317
  // Bulk COPY relationships — split by FROM→TO label pair (LadybugDB requires it)
334
318
  const { relHeader, relsByPairMeta, pairWriteStreams, skippedRels, totalValidRels } = await splitRelCsvByLabelPair(csvResult.relCsvPath, csvDir, validTables, getNodeLabel);
335
- // Close all per-pair write streams before COPY
336
- await Promise.all(Array.from(pairWriteStreams.values()).map((ws) => new Promise((resolve, reject) => {
337
- const onError = (err) => reject(err);
338
- ws.on('error', onError);
339
- ws.end(() => {
340
- ws.removeListener('error', onError);
341
- resolve();
342
- });
343
- })));
319
+ // Close all per-pair write streams before COPY. `stream/promises.finished`
320
+ // resolves on the stream's 'finish' event and rejects on 'error' — replaces
321
+ // a hand-rolled promisification with the stdlib primitive.
322
+ await Promise.all(Array.from(pairWriteStreams.values()).map(async (ws) => {
323
+ ws.end();
324
+ await finished(ws);
325
+ }));
344
326
  const insertedRels = totalValidRels;
345
327
  const warnings = [];
346
328
  if (insertedRels > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.2-rc.2",
3
+ "version": "1.6.2-rc.3",
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",