@softerist/heuristic-mcp 3.0.15 → 3.0.16
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 +104 -104
- package/config.jsonc +173 -173
- package/features/ann-config.js +131 -0
- package/features/clear-cache.js +84 -0
- package/features/find-similar-code.js +291 -0
- package/features/hybrid-search.js +544 -0
- package/features/index-codebase.js +3268 -0
- package/features/lifecycle.js +1189 -0
- package/features/package-version.js +302 -0
- package/features/register.js +408 -0
- package/features/resources.js +156 -0
- package/features/set-workspace.js +265 -0
- package/index.js +96 -96
- package/lib/cache-ops.js +22 -22
- package/lib/cache-utils.js +565 -565
- package/lib/cache.js +1870 -1870
- package/lib/call-graph.js +396 -396
- package/lib/cli.js +1 -1
- package/lib/config.js +517 -517
- package/lib/constants.js +39 -39
- package/lib/embed-query-process.js +7 -7
- package/lib/embedding-process.js +7 -7
- package/lib/embedding-worker.js +299 -299
- package/lib/ignore-patterns.js +316 -316
- package/lib/json-worker.js +14 -14
- package/lib/json-writer.js +337 -337
- package/lib/logging.js +164 -164
- package/lib/memory-logger.js +13 -13
- package/lib/onnx-backend.js +193 -193
- package/lib/project-detector.js +84 -84
- package/lib/server-lifecycle.js +165 -165
- package/lib/settings-editor.js +754 -754
- package/lib/tokenizer.js +256 -256
- package/lib/utils.js +428 -428
- package/lib/vector-store-binary.js +627 -627
- package/lib/vector-store-sqlite.js +95 -95
- package/lib/workspace-env.js +28 -28
- package/mcp_config.json +9 -9
- package/package.json +86 -75
- package/scripts/clear-cache.js +20 -0
- package/scripts/download-model.js +43 -0
- package/scripts/mcp-launcher.js +49 -0
- package/scripts/postinstall.js +12 -0
- package/search-configs.js +36 -36
- package/.prettierrc +0 -7
- package/debug-pids.js +0 -30
- package/eslint.config.js +0 -36
- package/specs/plan.md +0 -23
- package/vitest.config.js +0 -39
package/lib/json-writer.js
CHANGED
|
@@ -1,337 +1,337 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
|
|
3
|
-
function isTypedArray(x) {
|
|
4
|
-
return x && ArrayBuffer.isView(x) && !(x instanceof DataView);
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
function onceDrainOrError(stream) {
|
|
8
|
-
return new Promise((resolve, reject) => {
|
|
9
|
-
const onDrain = () => cleanup(resolve);
|
|
10
|
-
const onError = (err) => cleanup(() => reject(err));
|
|
11
|
-
|
|
12
|
-
const cleanup = (fn) => {
|
|
13
|
-
stream.off('drain', onDrain);
|
|
14
|
-
stream.off('error', onError);
|
|
15
|
-
fn();
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
stream.once('drain', onDrain);
|
|
19
|
-
stream.once('error', onError);
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Streaming JSON array writer optimized for:
|
|
25
|
-
* - TypedArray vectors streamed (no per-item vector allocation)
|
|
26
|
-
* - backpressure safety
|
|
27
|
-
* - configurable float rounding + flush threshold
|
|
28
|
-
* - compact mode when indent === '' (no forced newlines)
|
|
29
|
-
* - safe cleanup on failure (abort)
|
|
30
|
-
* - optional native TypedArray.join(',') fast-path when rounding is disabled
|
|
31
|
-
*/
|
|
32
|
-
export class StreamingJsonWriter {
|
|
33
|
-
/**
|
|
34
|
-
* @param {string} filePath
|
|
35
|
-
* @param {object} [opts]
|
|
36
|
-
* @param {number} [opts.highWaterMark] Stream internal buffer size.
|
|
37
|
-
* @param {number|null} [opts.floatDigits] Round floats to N digits. null disables rounding.
|
|
38
|
-
* @param {number} [opts.flushChars] Flush threshold for the internal string buffer.
|
|
39
|
-
* @param {string} [opts.indent] Indent prefix per item ("" for compact, " " for pretty).
|
|
40
|
-
* @param {boolean} [opts.assumeFinite] Skip NaN/Infinity checks (unsafe if false data).
|
|
41
|
-
* @param {boolean} [opts.checkFinite] If set, overrides assumeFinite (true = check, false = skip).
|
|
42
|
-
* @param {boolean} [opts.noMutation] Avoid temporary mutation when stripping vector.
|
|
43
|
-
* @param {number} [opts.joinThreshold] Max elements to use single join() string.
|
|
44
|
-
* @param {number} [opts.joinChunkSize] Elements per join() chunk when chunking.
|
|
45
|
-
*/
|
|
46
|
-
constructor(
|
|
47
|
-
filePath,
|
|
48
|
-
{
|
|
49
|
-
highWaterMark = 256 * 1024,
|
|
50
|
-
floatDigits = 6,
|
|
51
|
-
flushChars = 256 * 1024,
|
|
52
|
-
indent = '',
|
|
53
|
-
assumeFinite,
|
|
54
|
-
checkFinite,
|
|
55
|
-
noMutation = false,
|
|
56
|
-
joinThreshold = 8192,
|
|
57
|
-
joinChunkSize = 2048,
|
|
58
|
-
} = {}
|
|
59
|
-
) {
|
|
60
|
-
this.filePath = filePath;
|
|
61
|
-
this.highWaterMark =
|
|
62
|
-
Number.isInteger(highWaterMark) && highWaterMark > 8 * 1024 ? highWaterMark : 256 * 1024;
|
|
63
|
-
this.flushChars =
|
|
64
|
-
Number.isInteger(flushChars) && flushChars > 8 * 1024 ? flushChars : 256 * 1024;
|
|
65
|
-
this.indent = typeof indent === 'string' ? indent : '';
|
|
66
|
-
this.pretty = this.indent.length > 0;
|
|
67
|
-
this.assumeFinite = typeof checkFinite === 'boolean' ? !checkFinite : !!assumeFinite;
|
|
68
|
-
this.noMutation = !!noMutation;
|
|
69
|
-
this.joinThreshold =
|
|
70
|
-
Number.isInteger(joinThreshold) && joinThreshold > 0 ? joinThreshold : 8192;
|
|
71
|
-
this.joinChunkSize =
|
|
72
|
-
Number.isInteger(joinChunkSize) && joinChunkSize > 0 ? joinChunkSize : 2048;
|
|
73
|
-
|
|
74
|
-
this._prefixFirst = this.pretty ? this.indent : '';
|
|
75
|
-
this._prefixNext = this.pretty ? ',\n' + this.indent : ',';
|
|
76
|
-
|
|
77
|
-
this.stream = null;
|
|
78
|
-
this.first = true;
|
|
79
|
-
this._streamError = null;
|
|
80
|
-
|
|
81
|
-
// Formatter + fast-path flag
|
|
82
|
-
this._useJoinFastPath = floatDigits === null;
|
|
83
|
-
|
|
84
|
-
if (!this._useJoinFastPath) {
|
|
85
|
-
const digitsOk = Number.isInteger(floatDigits) && floatDigits >= 0 && floatDigits <= 12;
|
|
86
|
-
const d = digitsOk ? floatDigits : 6;
|
|
87
|
-
const scale = 10 ** d;
|
|
88
|
-
if (this.assumeFinite) {
|
|
89
|
-
this._formatFn = (x) => String(Math.round(x * scale) / scale);
|
|
90
|
-
} else {
|
|
91
|
-
this._formatFn = (x) => {
|
|
92
|
-
if (!Number.isFinite(x)) return '0';
|
|
93
|
-
return String(Math.round(x * scale) / scale);
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
} else {
|
|
97
|
-
if (this.assumeFinite) {
|
|
98
|
-
this._formatFn = (x) => String(x);
|
|
99
|
-
} else {
|
|
100
|
-
this._formatFn = (x) => {
|
|
101
|
-
if (!Number.isFinite(x)) return '0';
|
|
102
|
-
return String(x);
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async writeStart() {
|
|
109
|
-
if (this.stream) return;
|
|
110
|
-
|
|
111
|
-
this.stream = fs.createWriteStream(this.filePath, {
|
|
112
|
-
flags: 'w',
|
|
113
|
-
encoding: 'utf8',
|
|
114
|
-
highWaterMark: this.highWaterMark,
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
this.stream.on('error', (err) => {
|
|
118
|
-
this._streamError = err;
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
await new Promise((resolve, reject) => {
|
|
122
|
-
if (this.stream.fd !== null) return resolve();
|
|
123
|
-
this.stream.once('open', resolve);
|
|
124
|
-
this.stream.once('error', reject);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
const p = this._writeRaw(this.pretty ? '[\n' : '[');
|
|
128
|
-
if (p) await p;
|
|
129
|
-
this.first = true;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Best-effort early shutdown (use in catch/finally blocks).
|
|
134
|
-
* Destroys the stream to avoid fd leaks when writeEnd() is not reached.
|
|
135
|
-
*/
|
|
136
|
-
abort(err) {
|
|
137
|
-
if (!this.stream) return;
|
|
138
|
-
try {
|
|
139
|
-
this._streamError = err || this._streamError || new Error('StreamingJsonWriter aborted');
|
|
140
|
-
this.stream.destroy(this._streamError);
|
|
141
|
-
} catch {
|
|
142
|
-
// ignore
|
|
143
|
-
} finally {
|
|
144
|
-
this.stream = null;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
async drain() {
|
|
149
|
-
if (!this.stream || !this.stream.writableNeedDrain) return;
|
|
150
|
-
await onceDrainOrError(this.stream);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
writeItem(item) {
|
|
154
|
-
if (!this.stream) throw new Error('StreamingJsonWriter not started. Call writeStart() first.');
|
|
155
|
-
if (this._streamError) throw this._streamError;
|
|
156
|
-
|
|
157
|
-
const prefix = this.first ? this._prefixFirst : this._prefixNext;
|
|
158
|
-
this.first = false;
|
|
159
|
-
|
|
160
|
-
const vec = item?.vector;
|
|
161
|
-
|
|
162
|
-
if (isTypedArray(vec)) {
|
|
163
|
-
const base = this.noMutation
|
|
164
|
-
? this._stringifyWithoutMutation(item, vec)
|
|
165
|
-
: this._stringifyWithoutVector(item, vec);
|
|
166
|
-
const hasBase = typeof base === 'string' && base.length > 0 && base !== '{}';
|
|
167
|
-
const header = hasBase ? `${prefix}${base.slice(0, -1)},"vector":` : `${prefix}{"vector":`;
|
|
168
|
-
|
|
169
|
-
return this._chain(this._writeRaw(header), () =>
|
|
170
|
-
this._chain(this._writeTypedArray(vec), () => this._writeRaw('}'))
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return this._writeRaw(prefix + JSON.stringify(item));
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
async writeEnd() {
|
|
178
|
-
if (!this.stream) return;
|
|
179
|
-
if (this._streamError) throw this._streamError;
|
|
180
|
-
|
|
181
|
-
const p = this._writeRaw(this.pretty ? '\n]\n' : ']\n');
|
|
182
|
-
if (p) await p;
|
|
183
|
-
|
|
184
|
-
await new Promise((resolve, reject) => {
|
|
185
|
-
this.stream.once('error', reject);
|
|
186
|
-
this.stream.end(resolve);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
this.stream = null;
|
|
190
|
-
this._streamError = null;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
_chain(promise, next) {
|
|
194
|
-
if (promise) return promise.then(() => next());
|
|
195
|
-
return next();
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
_stringifyWithoutVector(item, vec) {
|
|
199
|
-
let base;
|
|
200
|
-
let restored = false;
|
|
201
|
-
|
|
202
|
-
try {
|
|
203
|
-
const prev = item.vector;
|
|
204
|
-
item.vector = undefined;
|
|
205
|
-
base = JSON.stringify(item);
|
|
206
|
-
item.vector = prev;
|
|
207
|
-
restored = true;
|
|
208
|
-
} catch {
|
|
209
|
-
base = JSON.stringify(item, (key, val) =>
|
|
210
|
-
key === 'vector' && val === vec ? undefined : val
|
|
211
|
-
);
|
|
212
|
-
} finally {
|
|
213
|
-
if (!restored) {
|
|
214
|
-
try {
|
|
215
|
-
item.vector = vec;
|
|
216
|
-
} catch {
|
|
217
|
-
// ignore
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return base;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
_stringifyWithoutMutation(item, vec) {
|
|
226
|
-
try {
|
|
227
|
-
const rest = { ...item };
|
|
228
|
-
delete rest.vector;
|
|
229
|
-
return JSON.stringify(rest);
|
|
230
|
-
} catch {
|
|
231
|
-
return JSON.stringify(item, (key, val) =>
|
|
232
|
-
key === 'vector' && val === vec ? undefined : val
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Core write method.
|
|
239
|
-
* Returns null on synchronous success (fast path).
|
|
240
|
-
* Returns a Promise only when backpressure is hit (slow path).
|
|
241
|
-
*/
|
|
242
|
-
_writeRaw(str) {
|
|
243
|
-
if (this._streamError) throw this._streamError;
|
|
244
|
-
|
|
245
|
-
const ok = this.stream.write(str);
|
|
246
|
-
if (ok) return null;
|
|
247
|
-
|
|
248
|
-
return onceDrainOrError(this.stream).then(() => {
|
|
249
|
-
if (this._streamError) throw this._streamError;
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
_writeTypedArray(vec) {
|
|
254
|
-
return this._chain(this._writeRaw('['), () => this._writeTypedArrayBody(vec));
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
_writeTypedArrayBody(vec) {
|
|
258
|
-
if (this._useJoinFastPath) {
|
|
259
|
-
if (!this.assumeFinite && !this._allFinite(vec)) {
|
|
260
|
-
return this._writeFormatted(vec);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
if (vec.length <= this.joinThreshold) {
|
|
264
|
-
return this._chain(this._writeRaw(vec.join(',')), () => this._writeRaw(']'));
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
return this._writeJoinChunks(vec);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
return this._writeFormatted(vec);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
_allFinite(vec) {
|
|
274
|
-
for (let i = 0; i < vec.length; i++) {
|
|
275
|
-
if (!Number.isFinite(vec[i])) return false;
|
|
276
|
-
}
|
|
277
|
-
return true;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
_writeJoinChunks(vec) {
|
|
281
|
-
const len = vec.length;
|
|
282
|
-
if (len === 0) return this._writeRaw(']');
|
|
283
|
-
|
|
284
|
-
let i = 0;
|
|
285
|
-
const chunkSize = this.joinChunkSize;
|
|
286
|
-
|
|
287
|
-
const writeNext = () => {
|
|
288
|
-
while (i < len) {
|
|
289
|
-
const end = Math.min(len, i + chunkSize);
|
|
290
|
-
let chunk = vec.subarray(i, end).join(',');
|
|
291
|
-
if (i !== 0) chunk = ',' + chunk;
|
|
292
|
-
i = end;
|
|
293
|
-
|
|
294
|
-
const pending = this._writeRaw(chunk);
|
|
295
|
-
if (pending) return pending.then(writeNext);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
return this._writeRaw(']');
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
return writeNext();
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
_writeFormatted(vec) {
|
|
305
|
-
const len = vec.length;
|
|
306
|
-
if (len === 0) return this._writeRaw(']');
|
|
307
|
-
|
|
308
|
-
let i = 0;
|
|
309
|
-
let buf = '';
|
|
310
|
-
const FLUSH_AT = this.flushChars;
|
|
311
|
-
const format = this._formatFn;
|
|
312
|
-
|
|
313
|
-
const writeNext = () => {
|
|
314
|
-
while (i < len) {
|
|
315
|
-
if (i) buf += ',';
|
|
316
|
-
buf += format(vec[i]);
|
|
317
|
-
i += 1;
|
|
318
|
-
|
|
319
|
-
if (buf.length >= FLUSH_AT) {
|
|
320
|
-
const pending = this._writeRaw(buf);
|
|
321
|
-
buf = '';
|
|
322
|
-
if (pending) return pending.then(writeNext);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
if (buf) {
|
|
327
|
-
const pending = this._writeRaw(buf);
|
|
328
|
-
buf = '';
|
|
329
|
-
if (pending) return pending.then(() => this._writeRaw(']'));
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
return this._writeRaw(']');
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
return writeNext();
|
|
336
|
-
}
|
|
337
|
-
}
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
|
|
3
|
+
function isTypedArray(x) {
|
|
4
|
+
return x && ArrayBuffer.isView(x) && !(x instanceof DataView);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function onceDrainOrError(stream) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const onDrain = () => cleanup(resolve);
|
|
10
|
+
const onError = (err) => cleanup(() => reject(err));
|
|
11
|
+
|
|
12
|
+
const cleanup = (fn) => {
|
|
13
|
+
stream.off('drain', onDrain);
|
|
14
|
+
stream.off('error', onError);
|
|
15
|
+
fn();
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
stream.once('drain', onDrain);
|
|
19
|
+
stream.once('error', onError);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Streaming JSON array writer optimized for:
|
|
25
|
+
* - TypedArray vectors streamed (no per-item vector allocation)
|
|
26
|
+
* - backpressure safety
|
|
27
|
+
* - configurable float rounding + flush threshold
|
|
28
|
+
* - compact mode when indent === '' (no forced newlines)
|
|
29
|
+
* - safe cleanup on failure (abort)
|
|
30
|
+
* - optional native TypedArray.join(',') fast-path when rounding is disabled
|
|
31
|
+
*/
|
|
32
|
+
export class StreamingJsonWriter {
|
|
33
|
+
/**
|
|
34
|
+
* @param {string} filePath
|
|
35
|
+
* @param {object} [opts]
|
|
36
|
+
* @param {number} [opts.highWaterMark] Stream internal buffer size.
|
|
37
|
+
* @param {number|null} [opts.floatDigits] Round floats to N digits. null disables rounding.
|
|
38
|
+
* @param {number} [opts.flushChars] Flush threshold for the internal string buffer.
|
|
39
|
+
* @param {string} [opts.indent] Indent prefix per item ("" for compact, " " for pretty).
|
|
40
|
+
* @param {boolean} [opts.assumeFinite] Skip NaN/Infinity checks (unsafe if false data).
|
|
41
|
+
* @param {boolean} [opts.checkFinite] If set, overrides assumeFinite (true = check, false = skip).
|
|
42
|
+
* @param {boolean} [opts.noMutation] Avoid temporary mutation when stripping vector.
|
|
43
|
+
* @param {number} [opts.joinThreshold] Max elements to use single join() string.
|
|
44
|
+
* @param {number} [opts.joinChunkSize] Elements per join() chunk when chunking.
|
|
45
|
+
*/
|
|
46
|
+
constructor(
|
|
47
|
+
filePath,
|
|
48
|
+
{
|
|
49
|
+
highWaterMark = 256 * 1024,
|
|
50
|
+
floatDigits = 6,
|
|
51
|
+
flushChars = 256 * 1024,
|
|
52
|
+
indent = '',
|
|
53
|
+
assumeFinite,
|
|
54
|
+
checkFinite,
|
|
55
|
+
noMutation = false,
|
|
56
|
+
joinThreshold = 8192,
|
|
57
|
+
joinChunkSize = 2048,
|
|
58
|
+
} = {}
|
|
59
|
+
) {
|
|
60
|
+
this.filePath = filePath;
|
|
61
|
+
this.highWaterMark =
|
|
62
|
+
Number.isInteger(highWaterMark) && highWaterMark > 8 * 1024 ? highWaterMark : 256 * 1024;
|
|
63
|
+
this.flushChars =
|
|
64
|
+
Number.isInteger(flushChars) && flushChars > 8 * 1024 ? flushChars : 256 * 1024;
|
|
65
|
+
this.indent = typeof indent === 'string' ? indent : '';
|
|
66
|
+
this.pretty = this.indent.length > 0;
|
|
67
|
+
this.assumeFinite = typeof checkFinite === 'boolean' ? !checkFinite : !!assumeFinite;
|
|
68
|
+
this.noMutation = !!noMutation;
|
|
69
|
+
this.joinThreshold =
|
|
70
|
+
Number.isInteger(joinThreshold) && joinThreshold > 0 ? joinThreshold : 8192;
|
|
71
|
+
this.joinChunkSize =
|
|
72
|
+
Number.isInteger(joinChunkSize) && joinChunkSize > 0 ? joinChunkSize : 2048;
|
|
73
|
+
|
|
74
|
+
this._prefixFirst = this.pretty ? this.indent : '';
|
|
75
|
+
this._prefixNext = this.pretty ? ',\n' + this.indent : ',';
|
|
76
|
+
|
|
77
|
+
this.stream = null;
|
|
78
|
+
this.first = true;
|
|
79
|
+
this._streamError = null;
|
|
80
|
+
|
|
81
|
+
// Formatter + fast-path flag
|
|
82
|
+
this._useJoinFastPath = floatDigits === null;
|
|
83
|
+
|
|
84
|
+
if (!this._useJoinFastPath) {
|
|
85
|
+
const digitsOk = Number.isInteger(floatDigits) && floatDigits >= 0 && floatDigits <= 12;
|
|
86
|
+
const d = digitsOk ? floatDigits : 6;
|
|
87
|
+
const scale = 10 ** d;
|
|
88
|
+
if (this.assumeFinite) {
|
|
89
|
+
this._formatFn = (x) => String(Math.round(x * scale) / scale);
|
|
90
|
+
} else {
|
|
91
|
+
this._formatFn = (x) => {
|
|
92
|
+
if (!Number.isFinite(x)) return '0';
|
|
93
|
+
return String(Math.round(x * scale) / scale);
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
if (this.assumeFinite) {
|
|
98
|
+
this._formatFn = (x) => String(x);
|
|
99
|
+
} else {
|
|
100
|
+
this._formatFn = (x) => {
|
|
101
|
+
if (!Number.isFinite(x)) return '0';
|
|
102
|
+
return String(x);
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async writeStart() {
|
|
109
|
+
if (this.stream) return;
|
|
110
|
+
|
|
111
|
+
this.stream = fs.createWriteStream(this.filePath, {
|
|
112
|
+
flags: 'w',
|
|
113
|
+
encoding: 'utf8',
|
|
114
|
+
highWaterMark: this.highWaterMark,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
this.stream.on('error', (err) => {
|
|
118
|
+
this._streamError = err;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
await new Promise((resolve, reject) => {
|
|
122
|
+
if (this.stream.fd !== null) return resolve();
|
|
123
|
+
this.stream.once('open', resolve);
|
|
124
|
+
this.stream.once('error', reject);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const p = this._writeRaw(this.pretty ? '[\n' : '[');
|
|
128
|
+
if (p) await p;
|
|
129
|
+
this.first = true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Best-effort early shutdown (use in catch/finally blocks).
|
|
134
|
+
* Destroys the stream to avoid fd leaks when writeEnd() is not reached.
|
|
135
|
+
*/
|
|
136
|
+
abort(err) {
|
|
137
|
+
if (!this.stream) return;
|
|
138
|
+
try {
|
|
139
|
+
this._streamError = err || this._streamError || new Error('StreamingJsonWriter aborted');
|
|
140
|
+
this.stream.destroy(this._streamError);
|
|
141
|
+
} catch {
|
|
142
|
+
// ignore
|
|
143
|
+
} finally {
|
|
144
|
+
this.stream = null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async drain() {
|
|
149
|
+
if (!this.stream || !this.stream.writableNeedDrain) return;
|
|
150
|
+
await onceDrainOrError(this.stream);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
writeItem(item) {
|
|
154
|
+
if (!this.stream) throw new Error('StreamingJsonWriter not started. Call writeStart() first.');
|
|
155
|
+
if (this._streamError) throw this._streamError;
|
|
156
|
+
|
|
157
|
+
const prefix = this.first ? this._prefixFirst : this._prefixNext;
|
|
158
|
+
this.first = false;
|
|
159
|
+
|
|
160
|
+
const vec = item?.vector;
|
|
161
|
+
|
|
162
|
+
if (isTypedArray(vec)) {
|
|
163
|
+
const base = this.noMutation
|
|
164
|
+
? this._stringifyWithoutMutation(item, vec)
|
|
165
|
+
: this._stringifyWithoutVector(item, vec);
|
|
166
|
+
const hasBase = typeof base === 'string' && base.length > 0 && base !== '{}';
|
|
167
|
+
const header = hasBase ? `${prefix}${base.slice(0, -1)},"vector":` : `${prefix}{"vector":`;
|
|
168
|
+
|
|
169
|
+
return this._chain(this._writeRaw(header), () =>
|
|
170
|
+
this._chain(this._writeTypedArray(vec), () => this._writeRaw('}'))
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return this._writeRaw(prefix + JSON.stringify(item));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async writeEnd() {
|
|
178
|
+
if (!this.stream) return;
|
|
179
|
+
if (this._streamError) throw this._streamError;
|
|
180
|
+
|
|
181
|
+
const p = this._writeRaw(this.pretty ? '\n]\n' : ']\n');
|
|
182
|
+
if (p) await p;
|
|
183
|
+
|
|
184
|
+
await new Promise((resolve, reject) => {
|
|
185
|
+
this.stream.once('error', reject);
|
|
186
|
+
this.stream.end(resolve);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
this.stream = null;
|
|
190
|
+
this._streamError = null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
_chain(promise, next) {
|
|
194
|
+
if (promise) return promise.then(() => next());
|
|
195
|
+
return next();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
_stringifyWithoutVector(item, vec) {
|
|
199
|
+
let base;
|
|
200
|
+
let restored = false;
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const prev = item.vector;
|
|
204
|
+
item.vector = undefined;
|
|
205
|
+
base = JSON.stringify(item);
|
|
206
|
+
item.vector = prev;
|
|
207
|
+
restored = true;
|
|
208
|
+
} catch {
|
|
209
|
+
base = JSON.stringify(item, (key, val) =>
|
|
210
|
+
key === 'vector' && val === vec ? undefined : val
|
|
211
|
+
);
|
|
212
|
+
} finally {
|
|
213
|
+
if (!restored) {
|
|
214
|
+
try {
|
|
215
|
+
item.vector = vec;
|
|
216
|
+
} catch {
|
|
217
|
+
// ignore
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return base;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
_stringifyWithoutMutation(item, vec) {
|
|
226
|
+
try {
|
|
227
|
+
const rest = { ...item };
|
|
228
|
+
delete rest.vector;
|
|
229
|
+
return JSON.stringify(rest);
|
|
230
|
+
} catch {
|
|
231
|
+
return JSON.stringify(item, (key, val) =>
|
|
232
|
+
key === 'vector' && val === vec ? undefined : val
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Core write method.
|
|
239
|
+
* Returns null on synchronous success (fast path).
|
|
240
|
+
* Returns a Promise only when backpressure is hit (slow path).
|
|
241
|
+
*/
|
|
242
|
+
_writeRaw(str) {
|
|
243
|
+
if (this._streamError) throw this._streamError;
|
|
244
|
+
|
|
245
|
+
const ok = this.stream.write(str);
|
|
246
|
+
if (ok) return null;
|
|
247
|
+
|
|
248
|
+
return onceDrainOrError(this.stream).then(() => {
|
|
249
|
+
if (this._streamError) throw this._streamError;
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
_writeTypedArray(vec) {
|
|
254
|
+
return this._chain(this._writeRaw('['), () => this._writeTypedArrayBody(vec));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
_writeTypedArrayBody(vec) {
|
|
258
|
+
if (this._useJoinFastPath) {
|
|
259
|
+
if (!this.assumeFinite && !this._allFinite(vec)) {
|
|
260
|
+
return this._writeFormatted(vec);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (vec.length <= this.joinThreshold) {
|
|
264
|
+
return this._chain(this._writeRaw(vec.join(',')), () => this._writeRaw(']'));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return this._writeJoinChunks(vec);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return this._writeFormatted(vec);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
_allFinite(vec) {
|
|
274
|
+
for (let i = 0; i < vec.length; i++) {
|
|
275
|
+
if (!Number.isFinite(vec[i])) return false;
|
|
276
|
+
}
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
_writeJoinChunks(vec) {
|
|
281
|
+
const len = vec.length;
|
|
282
|
+
if (len === 0) return this._writeRaw(']');
|
|
283
|
+
|
|
284
|
+
let i = 0;
|
|
285
|
+
const chunkSize = this.joinChunkSize;
|
|
286
|
+
|
|
287
|
+
const writeNext = () => {
|
|
288
|
+
while (i < len) {
|
|
289
|
+
const end = Math.min(len, i + chunkSize);
|
|
290
|
+
let chunk = vec.subarray(i, end).join(',');
|
|
291
|
+
if (i !== 0) chunk = ',' + chunk;
|
|
292
|
+
i = end;
|
|
293
|
+
|
|
294
|
+
const pending = this._writeRaw(chunk);
|
|
295
|
+
if (pending) return pending.then(writeNext);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return this._writeRaw(']');
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
return writeNext();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
_writeFormatted(vec) {
|
|
305
|
+
const len = vec.length;
|
|
306
|
+
if (len === 0) return this._writeRaw(']');
|
|
307
|
+
|
|
308
|
+
let i = 0;
|
|
309
|
+
let buf = '';
|
|
310
|
+
const FLUSH_AT = this.flushChars;
|
|
311
|
+
const format = this._formatFn;
|
|
312
|
+
|
|
313
|
+
const writeNext = () => {
|
|
314
|
+
while (i < len) {
|
|
315
|
+
if (i) buf += ',';
|
|
316
|
+
buf += format(vec[i]);
|
|
317
|
+
i += 1;
|
|
318
|
+
|
|
319
|
+
if (buf.length >= FLUSH_AT) {
|
|
320
|
+
const pending = this._writeRaw(buf);
|
|
321
|
+
buf = '';
|
|
322
|
+
if (pending) return pending.then(writeNext);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (buf) {
|
|
327
|
+
const pending = this._writeRaw(buf);
|
|
328
|
+
buf = '';
|
|
329
|
+
if (pending) return pending.then(() => this._writeRaw(']'));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return this._writeRaw(']');
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
return writeNext();
|
|
336
|
+
}
|
|
337
|
+
}
|