jexidb 2.1.6 → 2.1.7

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/Database.cjs CHANGED
@@ -1,7 +1,6 @@
1
1
  'use strict';
2
2
 
3
3
  var events = require('events');
4
- var asyncMutex = require('async-mutex');
5
4
  var fs = require('fs');
6
5
  var readline = require('readline');
7
6
  var path = require('path');
@@ -124,6 +123,182 @@ AsyncGenerator.prototype["function" == typeof Symbol && Symbol.asyncIterator ||
124
123
  return this._invoke("return", e);
125
124
  };
126
125
 
126
+ const E_CANCELED = new Error('request for lock canceled');
127
+
128
+ var __awaiter$2 = function (thisArg, _arguments, P, generator) {
129
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
130
+ return new (P || (P = Promise))(function (resolve, reject) {
131
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
132
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
133
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
134
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
135
+ });
136
+ };
137
+ class Semaphore {
138
+ constructor(_value, _cancelError = E_CANCELED) {
139
+ this._value = _value;
140
+ this._cancelError = _cancelError;
141
+ this._queue = [];
142
+ this._weightedWaiters = [];
143
+ }
144
+ acquire(weight = 1, priority = 0) {
145
+ if (weight <= 0)
146
+ throw new Error(`invalid weight ${weight}: must be positive`);
147
+ return new Promise((resolve, reject) => {
148
+ const task = { resolve, reject, weight, priority };
149
+ const i = findIndexFromEnd(this._queue, (other) => priority <= other.priority);
150
+ if (i === -1 && weight <= this._value) {
151
+ // Needs immediate dispatch, skip the queue
152
+ this._dispatchItem(task);
153
+ }
154
+ else {
155
+ this._queue.splice(i + 1, 0, task);
156
+ }
157
+ });
158
+ }
159
+ runExclusive(callback_1) {
160
+ return __awaiter$2(this, arguments, void 0, function* (callback, weight = 1, priority = 0) {
161
+ const [value, release] = yield this.acquire(weight, priority);
162
+ try {
163
+ return yield callback(value);
164
+ }
165
+ finally {
166
+ release();
167
+ }
168
+ });
169
+ }
170
+ waitForUnlock(weight = 1, priority = 0) {
171
+ if (weight <= 0)
172
+ throw new Error(`invalid weight ${weight}: must be positive`);
173
+ if (this._couldLockImmediately(weight, priority)) {
174
+ return Promise.resolve();
175
+ }
176
+ else {
177
+ return new Promise((resolve) => {
178
+ if (!this._weightedWaiters[weight - 1])
179
+ this._weightedWaiters[weight - 1] = [];
180
+ insertSorted(this._weightedWaiters[weight - 1], { resolve, priority });
181
+ });
182
+ }
183
+ }
184
+ isLocked() {
185
+ return this._value <= 0;
186
+ }
187
+ getValue() {
188
+ return this._value;
189
+ }
190
+ setValue(value) {
191
+ this._value = value;
192
+ this._dispatchQueue();
193
+ }
194
+ release(weight = 1) {
195
+ if (weight <= 0)
196
+ throw new Error(`invalid weight ${weight}: must be positive`);
197
+ this._value += weight;
198
+ this._dispatchQueue();
199
+ }
200
+ cancel() {
201
+ this._queue.forEach((entry) => entry.reject(this._cancelError));
202
+ this._queue = [];
203
+ }
204
+ _dispatchQueue() {
205
+ this._drainUnlockWaiters();
206
+ while (this._queue.length > 0 && this._queue[0].weight <= this._value) {
207
+ this._dispatchItem(this._queue.shift());
208
+ this._drainUnlockWaiters();
209
+ }
210
+ }
211
+ _dispatchItem(item) {
212
+ const previousValue = this._value;
213
+ this._value -= item.weight;
214
+ item.resolve([previousValue, this._newReleaser(item.weight)]);
215
+ }
216
+ _newReleaser(weight) {
217
+ let called = false;
218
+ return () => {
219
+ if (called)
220
+ return;
221
+ called = true;
222
+ this.release(weight);
223
+ };
224
+ }
225
+ _drainUnlockWaiters() {
226
+ if (this._queue.length === 0) {
227
+ for (let weight = this._value; weight > 0; weight--) {
228
+ const waiters = this._weightedWaiters[weight - 1];
229
+ if (!waiters)
230
+ continue;
231
+ waiters.forEach((waiter) => waiter.resolve());
232
+ this._weightedWaiters[weight - 1] = [];
233
+ }
234
+ }
235
+ else {
236
+ const queuedPriority = this._queue[0].priority;
237
+ for (let weight = this._value; weight > 0; weight--) {
238
+ const waiters = this._weightedWaiters[weight - 1];
239
+ if (!waiters)
240
+ continue;
241
+ const i = waiters.findIndex((waiter) => waiter.priority <= queuedPriority);
242
+ (i === -1 ? waiters : waiters.splice(0, i))
243
+ .forEach((waiter => waiter.resolve()));
244
+ }
245
+ }
246
+ }
247
+ _couldLockImmediately(weight, priority) {
248
+ return (this._queue.length === 0 || this._queue[0].priority < priority) &&
249
+ weight <= this._value;
250
+ }
251
+ }
252
+ function insertSorted(a, v) {
253
+ const i = findIndexFromEnd(a, (other) => v.priority <= other.priority);
254
+ a.splice(i + 1, 0, v);
255
+ }
256
+ function findIndexFromEnd(a, predicate) {
257
+ for (let i = a.length - 1; i >= 0; i--) {
258
+ if (predicate(a[i])) {
259
+ return i;
260
+ }
261
+ }
262
+ return -1;
263
+ }
264
+
265
+ var __awaiter$1 = function (thisArg, _arguments, P, generator) {
266
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
267
+ return new (P || (P = Promise))(function (resolve, reject) {
268
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
269
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
270
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
271
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
272
+ });
273
+ };
274
+ class Mutex {
275
+ constructor(cancelError) {
276
+ this._semaphore = new Semaphore(1, cancelError);
277
+ }
278
+ acquire() {
279
+ return __awaiter$1(this, arguments, void 0, function* (priority = 0) {
280
+ const [, releaser] = yield this._semaphore.acquire(1, priority);
281
+ return releaser;
282
+ });
283
+ }
284
+ runExclusive(callback, priority = 0) {
285
+ return this._semaphore.runExclusive(() => callback(), 1, priority);
286
+ }
287
+ isLocked() {
288
+ return this._semaphore.isLocked();
289
+ }
290
+ waitForUnlock(priority = 0) {
291
+ return this._semaphore.waitForUnlock(1, priority);
292
+ }
293
+ release() {
294
+ if (this._semaphore.isLocked())
295
+ this._semaphore.release();
296
+ }
297
+ cancel() {
298
+ return this._semaphore.cancel();
299
+ }
300
+ }
301
+
127
302
  const aliasToCanonical = {
128
303
  '>': '$gt',
129
304
  '>=': '$gte',
@@ -246,7 +421,7 @@ class IndexManager {
246
421
 
247
422
  // CRITICAL: Use database mutex to prevent deadlocks
248
423
  // If no database mutex provided, create a local one (for backward compatibility)
249
- this.mutex = databaseMutex || new asyncMutex.Mutex();
424
+ this.mutex = databaseMutex || new Mutex();
250
425
  this.indexedFields = [];
251
426
  this.setIndexesConfig(this.opts.indexes);
252
427
  }
@@ -3249,6 +3424,266 @@ class Serializer {
3249
3424
  }
3250
3425
  }
3251
3426
 
3427
+ const objectToString = Object.prototype.toString;
3428
+
3429
+ const isError = value => objectToString.call(value) === '[object Error]';
3430
+
3431
+ const errorMessages = new Set([
3432
+ 'network error', // Chrome
3433
+ 'Failed to fetch', // Chrome
3434
+ 'NetworkError when attempting to fetch resource.', // Firefox
3435
+ 'The Internet connection appears to be offline.', // Safari 16
3436
+ 'Network request failed', // `cross-fetch`
3437
+ 'fetch failed', // Undici (Node.js)
3438
+ 'terminated', // Undici (Node.js)
3439
+ ' A network error occurred.', // Bun (WebKit)
3440
+ 'Network connection lost', // Cloudflare Workers (fetch)
3441
+ ]);
3442
+
3443
+ function isNetworkError(error) {
3444
+ const isValid = error
3445
+ && isError(error)
3446
+ && error.name === 'TypeError'
3447
+ && typeof error.message === 'string';
3448
+
3449
+ if (!isValid) {
3450
+ return false;
3451
+ }
3452
+
3453
+ const {message, stack} = error;
3454
+
3455
+ // Safari 17+ has generic message but no stack for network errors
3456
+ if (message === 'Load failed') {
3457
+ return stack === undefined
3458
+ // Sentry adds its own stack trace to the fetch error, so also check for that
3459
+ || '__sentry_captured__' in error;
3460
+ }
3461
+
3462
+ // Deno network errors start with specific text
3463
+ if (message.startsWith('error sending request for url')) {
3464
+ return true;
3465
+ }
3466
+
3467
+ // Standard network error messages
3468
+ return errorMessages.has(message);
3469
+ }
3470
+
3471
+ function validateRetries(retries) {
3472
+ if (typeof retries === 'number') {
3473
+ if (retries < 0) {
3474
+ throw new TypeError('Expected `retries` to be a non-negative number.');
3475
+ }
3476
+
3477
+ if (Number.isNaN(retries)) {
3478
+ throw new TypeError('Expected `retries` to be a valid number or Infinity, got NaN.');
3479
+ }
3480
+ } else if (retries !== undefined) {
3481
+ throw new TypeError('Expected `retries` to be a number or Infinity.');
3482
+ }
3483
+ }
3484
+
3485
+ function validateNumberOption(name, value, {min = 0, allowInfinity = false} = {}) {
3486
+ if (value === undefined) {
3487
+ return;
3488
+ }
3489
+
3490
+ if (typeof value !== 'number' || Number.isNaN(value)) {
3491
+ throw new TypeError(`Expected \`${name}\` to be a number${allowInfinity ? ' or Infinity' : ''}.`);
3492
+ }
3493
+
3494
+ if (!allowInfinity && !Number.isFinite(value)) {
3495
+ throw new TypeError(`Expected \`${name}\` to be a finite number.`);
3496
+ }
3497
+
3498
+ if (value < min) {
3499
+ throw new TypeError(`Expected \`${name}\` to be \u2265 ${min}.`);
3500
+ }
3501
+ }
3502
+
3503
+ class AbortError extends Error {
3504
+ constructor(message) {
3505
+ super();
3506
+
3507
+ if (message instanceof Error) {
3508
+ this.originalError = message;
3509
+ ({message} = message);
3510
+ } else {
3511
+ this.originalError = new Error(message);
3512
+ this.originalError.stack = this.stack;
3513
+ }
3514
+
3515
+ this.name = 'AbortError';
3516
+ this.message = message;
3517
+ }
3518
+ }
3519
+
3520
+ function calculateDelay(retriesConsumed, options) {
3521
+ const attempt = Math.max(1, retriesConsumed + 1);
3522
+ const random = options.randomize ? (Math.random() + 1) : 1;
3523
+
3524
+ let timeout = Math.round(random * options.minTimeout * (options.factor ** (attempt - 1)));
3525
+ timeout = Math.min(timeout, options.maxTimeout);
3526
+
3527
+ return timeout;
3528
+ }
3529
+
3530
+ function calculateRemainingTime(start, max) {
3531
+ if (!Number.isFinite(max)) {
3532
+ return max;
3533
+ }
3534
+
3535
+ return max - (performance.now() - start);
3536
+ }
3537
+
3538
+ async function onAttemptFailure({error, attemptNumber, retriesConsumed, startTime, options}) {
3539
+ const normalizedError = error instanceof Error
3540
+ ? error
3541
+ : new TypeError(`Non-error was thrown: "${error}". You should only throw errors.`);
3542
+
3543
+ if (normalizedError instanceof AbortError) {
3544
+ throw normalizedError.originalError;
3545
+ }
3546
+
3547
+ const retriesLeft = Number.isFinite(options.retries)
3548
+ ? Math.max(0, options.retries - retriesConsumed)
3549
+ : options.retries;
3550
+
3551
+ const maxRetryTime = options.maxRetryTime ?? Number.POSITIVE_INFINITY;
3552
+
3553
+ const context = Object.freeze({
3554
+ error: normalizedError,
3555
+ attemptNumber,
3556
+ retriesLeft,
3557
+ retriesConsumed,
3558
+ });
3559
+
3560
+ await options.onFailedAttempt(context);
3561
+
3562
+ if (calculateRemainingTime(startTime, maxRetryTime) <= 0) {
3563
+ throw normalizedError;
3564
+ }
3565
+
3566
+ const consumeRetry = await options.shouldConsumeRetry(context);
3567
+
3568
+ const remainingTime = calculateRemainingTime(startTime, maxRetryTime);
3569
+
3570
+ if (remainingTime <= 0 || retriesLeft <= 0) {
3571
+ throw normalizedError;
3572
+ }
3573
+
3574
+ if (normalizedError instanceof TypeError && !isNetworkError(normalizedError)) {
3575
+ if (consumeRetry) {
3576
+ throw normalizedError;
3577
+ }
3578
+
3579
+ options.signal?.throwIfAborted();
3580
+ return false;
3581
+ }
3582
+
3583
+ if (!await options.shouldRetry(context)) {
3584
+ throw normalizedError;
3585
+ }
3586
+
3587
+ if (!consumeRetry) {
3588
+ options.signal?.throwIfAborted();
3589
+ return false;
3590
+ }
3591
+
3592
+ const delayTime = calculateDelay(retriesConsumed, options);
3593
+ const finalDelay = Math.min(delayTime, remainingTime);
3594
+
3595
+ options.signal?.throwIfAborted();
3596
+
3597
+ if (finalDelay > 0) {
3598
+ await new Promise((resolve, reject) => {
3599
+ const onAbort = () => {
3600
+ clearTimeout(timeoutToken);
3601
+ options.signal?.removeEventListener('abort', onAbort);
3602
+ reject(options.signal.reason);
3603
+ };
3604
+
3605
+ const timeoutToken = setTimeout(() => {
3606
+ options.signal?.removeEventListener('abort', onAbort);
3607
+ resolve();
3608
+ }, finalDelay);
3609
+
3610
+ if (options.unref) {
3611
+ timeoutToken.unref?.();
3612
+ }
3613
+
3614
+ options.signal?.addEventListener('abort', onAbort, {once: true});
3615
+ });
3616
+ }
3617
+
3618
+ options.signal?.throwIfAborted();
3619
+
3620
+ return true;
3621
+ }
3622
+
3623
+ async function pRetry(input, options = {}) {
3624
+ options = {...options};
3625
+
3626
+ validateRetries(options.retries);
3627
+
3628
+ if (Object.hasOwn(options, 'forever')) {
3629
+ throw new Error('The `forever` option is no longer supported. For many use-cases, you can set `retries: Infinity` instead.');
3630
+ }
3631
+
3632
+ options.retries ??= 10;
3633
+ options.factor ??= 2;
3634
+ options.minTimeout ??= 1000;
3635
+ options.maxTimeout ??= Number.POSITIVE_INFINITY;
3636
+ options.maxRetryTime ??= Number.POSITIVE_INFINITY;
3637
+ options.randomize ??= false;
3638
+ options.onFailedAttempt ??= () => {};
3639
+ options.shouldRetry ??= () => true;
3640
+ options.shouldConsumeRetry ??= () => true;
3641
+
3642
+ // Validate numeric options and normalize edge cases
3643
+ validateNumberOption('factor', options.factor, {min: 0, allowInfinity: false});
3644
+ validateNumberOption('minTimeout', options.minTimeout, {min: 0, allowInfinity: false});
3645
+ validateNumberOption('maxTimeout', options.maxTimeout, {min: 0, allowInfinity: true});
3646
+ validateNumberOption('maxRetryTime', options.maxRetryTime, {min: 0, allowInfinity: true});
3647
+
3648
+ // Treat non-positive factor as 1 to avoid zero backoff or negative behavior
3649
+ if (!(options.factor > 0)) {
3650
+ options.factor = 1;
3651
+ }
3652
+
3653
+ options.signal?.throwIfAborted();
3654
+
3655
+ let attemptNumber = 0;
3656
+ let retriesConsumed = 0;
3657
+ const startTime = performance.now();
3658
+
3659
+ while (Number.isFinite(options.retries) ? retriesConsumed <= options.retries : true) {
3660
+ attemptNumber++;
3661
+
3662
+ try {
3663
+ options.signal?.throwIfAborted();
3664
+
3665
+ const result = await input(attemptNumber);
3666
+
3667
+ options.signal?.throwIfAborted();
3668
+
3669
+ return result;
3670
+ } catch (error) {
3671
+ if (await onAttemptFailure({
3672
+ error,
3673
+ attemptNumber,
3674
+ retriesConsumed,
3675
+ startTime,
3676
+ options,
3677
+ })) {
3678
+ retriesConsumed++;
3679
+ }
3680
+ }
3681
+ }
3682
+
3683
+ // Should not reach here, but in case it does, throw an error
3684
+ throw new Error('Retry attempts exhausted without throwing an error.');
3685
+ }
3686
+
3252
3687
  /**
3253
3688
  * OperationQueue - Queue system for database operations
3254
3689
  * Resolves race conditions between concurrent operations
@@ -4521,14 +4956,50 @@ class FileHandler {
4521
4956
  // Add a small delay to ensure any pending operations complete
4522
4957
  await new Promise(resolve => setTimeout(resolve, 5));
4523
4958
  // Use global read limiter to prevent file descriptor exhaustion
4524
- return this.readLimiter(() => this._readWithStreamingInternal(criteria, options, matchesCriteria, serializer));
4959
+ return this.readLimiter(() => this._readWithStreamingRetry(criteria, options, matchesCriteria, serializer));
4525
4960
  });
4526
4961
  } else {
4527
4962
  // Use global read limiter to prevent file descriptor exhaustion
4528
- return this.readLimiter(() => this._readWithStreamingInternal(criteria, options, matchesCriteria, serializer));
4963
+ return this.readLimiter(() => this._readWithStreamingRetry(criteria, options, matchesCriteria, serializer));
4964
+ }
4965
+ }
4966
+ async _readWithStreamingRetry(criteria, options = {}, matchesCriteria, serializer = null) {
4967
+ // If no timeout configured, use original implementation without retry
4968
+ if (!options.ioTimeoutMs) {
4969
+ return this._readWithStreamingInternal(criteria, options, matchesCriteria, serializer);
4529
4970
  }
4971
+ const timeoutMs = options.ioTimeoutMs || 5000; // Default 5s timeout per attempt
4972
+ const maxRetries = options.maxRetries || 3;
4973
+ return pRetry(async attempt => {
4974
+ const controller = new AbortController();
4975
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
4976
+ try {
4977
+ const results = await this._readWithStreamingInternal(criteria, options, matchesCriteria, serializer, controller.signal);
4978
+ return results;
4979
+ } catch (error) {
4980
+ if (error.name === 'AbortError' || error.code === 'ETIMEDOUT') {
4981
+ if (this.opts.debugMode) {
4982
+ console.log(`⚠️ Streaming read attempt ${attempt} timed out, retrying...`);
4983
+ }
4984
+ throw error; // p-retry will retry
4985
+ }
4986
+ // For other errors, don't retry
4987
+ throw new pRetry.AbortError(error);
4988
+ } finally {
4989
+ clearTimeout(timeout);
4990
+ }
4991
+ }, {
4992
+ retries: maxRetries,
4993
+ minTimeout: 100,
4994
+ maxTimeout: 1000,
4995
+ onFailedAttempt: error => {
4996
+ if (this.opts.debugMode) {
4997
+ console.log(`Streaming read failed (attempt ${error.attemptNumber}), ${error.retriesLeft} retries left`);
4998
+ }
4999
+ }
5000
+ });
4530
5001
  }
4531
- async _readWithStreamingInternal(criteria, options = {}, matchesCriteria, serializer = null) {
5002
+ async _readWithStreamingInternal(criteria, options = {}, matchesCriteria, serializer = null, signal = null) {
4532
5003
  const {
4533
5004
  limit,
4534
5005
  skip = 0
@@ -4558,6 +5029,14 @@ class FileHandler {
4558
5029
  crlfDelay: Infinity // Better performance
4559
5030
  });
4560
5031
 
5032
+ // Handle abort signal
5033
+ if (signal) {
5034
+ signal.addEventListener('abort', () => {
5035
+ stream.destroy();
5036
+ rl.close();
5037
+ });
5038
+ }
5039
+
4561
5040
  // Process line by line
4562
5041
  var _iteratorAbruptCompletion3 = false;
4563
5042
  var _didIteratorError3 = false;
@@ -4566,41 +5045,46 @@ class FileHandler {
4566
5045
  for (var _iterator3 = _asyncIterator(rl), _step3; _iteratorAbruptCompletion3 = !(_step3 = await _iterator3.next()).done; _iteratorAbruptCompletion3 = false) {
4567
5046
  const line = _step3.value;
4568
5047
  {
4569
- if (lineNumber >= skip) {
4570
- try {
4571
- let record;
4572
- if (serializer && typeof serializer.deserialize === 'function') {
4573
- // Use serializer for deserialization
4574
- record = serializer.deserialize(line);
4575
- } else {
4576
- // Fallback to JSON.parse for backward compatibility
4577
- record = JSON.parse(line);
4578
- }
4579
- if (record && matchesCriteria(record, criteria)) {
4580
- // Return raw data - term mapping will be handled by Database layer
4581
- results.push({
4582
- ...record,
4583
- _: lineNumber
4584
- });
4585
- matched++;
5048
+ if (signal && signal.aborted) {
5049
+ break; // Stop if aborted
5050
+ }
5051
+ lineNumber++;
4586
5052
 
4587
- // Check if we've reached the limit
4588
- if (results.length >= limit) {
4589
- break;
4590
- }
4591
- }
4592
- } catch (error) {
4593
- // CRITICAL FIX: Only log errors if they're not expected during concurrent operations
4594
- // Don't log JSON parsing errors that occur during file writes
4595
- if (this.opts && this.opts.debugMode && !error.message.includes('Unexpected')) {
4596
- console.log(`Error reading line ${lineNumber}:`, error.message);
5053
+ // Skip lines that were already processed in previous attempts
5054
+ if (lineNumber <= skip) {
5055
+ skipped++;
5056
+ continue;
5057
+ }
5058
+ try {
5059
+ let record;
5060
+ if (serializer && typeof serializer.deserialize === 'function') {
5061
+ // Use serializer for deserialization
5062
+ record = serializer.deserialize(line);
5063
+ } else {
5064
+ // Fallback to JSON.parse for backward compatibility
5065
+ record = JSON.parse(line);
5066
+ }
5067
+ if (record && matchesCriteria(record, criteria)) {
5068
+ // Return raw data - term mapping will be handled by Database layer
5069
+ results.push({
5070
+ ...record,
5071
+ _: lineNumber
5072
+ });
5073
+ matched++;
5074
+
5075
+ // Check if we've reached the limit
5076
+ if (results.length >= limit) {
5077
+ break;
4597
5078
  }
4598
- // Ignore invalid lines - they may be partial writes
4599
5079
  }
4600
- } else {
4601
- skipped++;
5080
+ } catch (error) {
5081
+ // CRITICAL FIX: Only log errors if they're not expected during concurrent operations
5082
+ // Don't log JSON parsing errors that occur during file writes
5083
+ if (this.opts && this.opts.debugMode && !error.message.includes('Unexpected')) {
5084
+ console.log(`Error reading line ${lineNumber}:`, error.message);
5085
+ }
5086
+ // Ignore invalid lines - they may be partial writes
4602
5087
  }
4603
- lineNumber++;
4604
5088
  processed++;
4605
5089
  }
4606
5090
  }
@@ -4623,6 +5107,10 @@ class FileHandler {
4623
5107
  }
4624
5108
  return results;
4625
5109
  } catch (error) {
5110
+ if (error.message === 'AbortError') {
5111
+ // Return partial results if aborted
5112
+ return results;
5113
+ }
4626
5114
  console.error('Error in readWithStreaming:', error);
4627
5115
  throw error;
4628
5116
  }
@@ -5513,7 +6001,6 @@ class QueryManager {
5513
6001
  // OPTIMIZATION: Use ranges instead of reading entire file
5514
6002
  const ranges = this.database.getRanges(batch);
5515
6003
  const groupedRanges = await this.fileHandler.groupedRanges(ranges);
5516
- const fs = await import('fs');
5517
6004
  const fd = await fs.promises.open(this.fileHandler.file, 'r');
5518
6005
  try {
5519
6006
  for (const groupedRange of groupedRanges) {
@@ -5768,7 +6255,6 @@ class QueryManager {
5768
6255
  const ranges = this.database.getRanges(fileLineNumbers);
5769
6256
  if (ranges.length > 0) {
5770
6257
  const groupedRanges = await this.database.fileHandler.groupedRanges(ranges);
5771
- const fs = await import('fs');
5772
6258
  const fd = await fs.promises.open(this.database.fileHandler.file, 'r');
5773
6259
  try {
5774
6260
  for (const groupedRange of groupedRanges) {
@@ -8106,7 +8592,7 @@ class Database extends events.EventEmitter {
8106
8592
  this.initializeManagers();
8107
8593
 
8108
8594
  // Initialize file mutex for thread safety
8109
- this.fileMutex = new asyncMutex.Mutex();
8595
+ this.fileMutex = new Mutex();
8110
8596
 
8111
8597
  // Initialize performance tracking
8112
8598
  this.performanceStats = {
@@ -10703,94 +11189,147 @@ class Database extends events.EventEmitter {
10703
11189
  let count = 0;
10704
11190
  const startTime = Date.now();
10705
11191
 
10706
- // Auto-detect schema from first line if not initialized
10707
- if (!this.serializer.schemaManager.isInitialized) {
10708
- const fs = await import('fs');
10709
- const readline = await import('readline');
10710
- const stream = fs.createReadStream(this.fileHandler.file, {
10711
- highWaterMark: 64 * 1024,
10712
- encoding: 'utf8'
10713
- });
10714
- const rl = readline.createInterface({
10715
- input: stream,
10716
- crlfDelay: Infinity
10717
- });
10718
- var _iteratorAbruptCompletion = false;
10719
- var _didIteratorError = false;
10720
- var _iteratorError;
10721
- try {
10722
- for (var _iterator = _asyncIterator(rl), _step; _iteratorAbruptCompletion = !(_step = await _iterator.next()).done; _iteratorAbruptCompletion = false) {
10723
- const line = _step.value;
10724
- {
10725
- if (line && line.trim()) {
10726
- try {
10727
- const firstRecord = JSON.parse(line);
10728
- if (Array.isArray(firstRecord)) {
10729
- // Try to infer schema from opts.fields if available
10730
- if (this.opts.fields && typeof this.opts.fields === 'object') {
10731
- const fieldNames = Object.keys(this.opts.fields);
10732
- if (fieldNames.length >= firstRecord.length) {
10733
- // Use first N fields from opts.fields to match array length
10734
- const schema = fieldNames.slice(0, firstRecord.length);
10735
- this.serializer.initializeSchema(schema);
10736
- if (this.opts.debugMode) {
10737
- console.log(`🔍 Inferred schema from opts.fields: ${schema.join(', ')}`);
11192
+ // Use retry for the streaming rebuild only if timeout is configured
11193
+ if (this.opts.ioTimeoutMs && this.opts.ioTimeoutMs > 0) {
11194
+ count = await this._rebuildIndexesWithRetry();
11195
+ } else {
11196
+ // Use original logic without retry for backward compatibility
11197
+ count = await this._rebuildIndexesOriginal();
11198
+ }
11199
+
11200
+ // Update indexManager totalLines
11201
+ if (this.indexManager) {
11202
+ this.indexManager.setTotalLines(this.offsets.length);
11203
+ }
11204
+ this._indexRebuildNeeded = false;
11205
+ if (this.opts.debugMode) {
11206
+ console.log(`✅ Index rebuilt from ${count} records in ${Date.now() - startTime}ms`);
11207
+ }
11208
+
11209
+ // Save the rebuilt index
11210
+ await this._saveIndexDataToFile();
11211
+ } catch (error) {
11212
+ if (this.opts.debugMode) {
11213
+ console.error('❌ Failed to rebuild indexes:', error.message);
11214
+ }
11215
+ // Don't throw - queries will fall back to streaming
11216
+ }
11217
+ }
11218
+
11219
+ /**
11220
+ * Rebuild indexes with retry logic to handle I/O hangs
11221
+ * @private
11222
+ */
11223
+ async _rebuildIndexesWithRetry() {
11224
+ // If no timeout configured, use original implementation without retry
11225
+ if (!this.opts.ioTimeoutMs) {
11226
+ return this._rebuildIndexesOriginal();
11227
+ }
11228
+ const timeoutMs = this.opts.ioTimeoutMs || 10000; // Longer timeout for rebuild
11229
+ const maxRetries = this.opts.maxRetries || 3;
11230
+ let count = 0;
11231
+ await pRetry(async attempt => {
11232
+ const controller = new AbortController();
11233
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
11234
+ try {
11235
+ // Auto-detect schema from first line if not initialized
11236
+ if (!this.serializer.schemaManager.isInitialized) {
11237
+ const stream = fs.createReadStream(this.fileHandler.file, {
11238
+ highWaterMark: 64 * 1024,
11239
+ encoding: 'utf8'
11240
+ });
11241
+ const rl = readline.createInterface({
11242
+ input: stream,
11243
+ crlfDelay: Infinity
11244
+ });
11245
+
11246
+ // Handle abort
11247
+ controller.signal.addEventListener('abort', () => {
11248
+ stream.destroy(new Error('AbortError'));
11249
+ rl.close();
11250
+ });
11251
+ var _iteratorAbruptCompletion = false;
11252
+ var _didIteratorError = false;
11253
+ var _iteratorError;
11254
+ try {
11255
+ for (var _iterator = _asyncIterator(rl), _step; _iteratorAbruptCompletion = !(_step = await _iterator.next()).done; _iteratorAbruptCompletion = false) {
11256
+ const line = _step.value;
11257
+ {
11258
+ if (controller.signal.aborted) break;
11259
+ if (line && line.trim()) {
11260
+ try {
11261
+ const firstRecord = JSON.parse(line);
11262
+ if (Array.isArray(firstRecord)) {
11263
+ // Try to infer schema from opts.fields if available
11264
+ if (this.opts.fields && typeof this.opts.fields === 'object') {
11265
+ const fieldNames = Object.keys(this.opts.fields);
11266
+ if (fieldNames.length >= firstRecord.length) {
11267
+ // Use first N fields from opts.fields to match array length
11268
+ const schema = fieldNames.slice(0, firstRecord.length);
11269
+ this.serializer.initializeSchema(schema);
11270
+ if (this.opts.debugMode) {
11271
+ console.log(`🔍 Inferred schema from opts.fields: ${schema.join(', ')}`);
11272
+ }
11273
+ } else {
11274
+ throw new Error(`Cannot rebuild index: array has ${firstRecord.length} elements but opts.fields only defines ${fieldNames.length} fields. Schema must be explicitly provided.`);
10738
11275
  }
10739
11276
  } else {
10740
- throw new Error(`Cannot rebuild index: array has ${firstRecord.length} elements but opts.fields only defines ${fieldNames.length} fields. Schema must be explicitly provided.`);
11277
+ throw new Error('Cannot rebuild index: schema missing, file uses array format, and opts.fields not provided. The .idx.jdb file is corrupted.');
10741
11278
  }
10742
11279
  } else {
10743
- throw new Error('Cannot rebuild index: schema missing, file uses array format, and opts.fields not provided. The .idx.jdb file is corrupted.');
11280
+ // Object format, initialize from object keys
11281
+ this.serializer.initializeSchema(firstRecord, true);
11282
+ if (this.opts.debugMode) {
11283
+ console.log(`🔍 Auto-detected schema from object: ${Object.keys(firstRecord).join(', ')}`);
11284
+ }
10744
11285
  }
10745
- } else {
10746
- // Object format, initialize from object keys
10747
- this.serializer.initializeSchema(firstRecord, true);
11286
+ break;
11287
+ } catch (error) {
10748
11288
  if (this.opts.debugMode) {
10749
- console.log(`🔍 Auto-detected schema from object: ${Object.keys(firstRecord).join(', ')}`);
11289
+ console.error('❌ Failed to auto-detect schema:', error.message);
10750
11290
  }
11291
+ throw error;
10751
11292
  }
10752
- break;
10753
- } catch (error) {
10754
- if (this.opts.debugMode) {
10755
- console.error('❌ Failed to auto-detect schema:', error.message);
10756
- }
10757
- throw error;
10758
11293
  }
10759
11294
  }
10760
11295
  }
10761
- }
10762
- } catch (err) {
10763
- _didIteratorError = true;
10764
- _iteratorError = err;
10765
- } finally {
10766
- try {
10767
- if (_iteratorAbruptCompletion && _iterator.return != null) {
10768
- await _iterator.return();
10769
- }
11296
+ } catch (err) {
11297
+ _didIteratorError = true;
11298
+ _iteratorError = err;
10770
11299
  } finally {
10771
- if (_didIteratorError) {
10772
- throw _iteratorError;
11300
+ try {
11301
+ if (_iteratorAbruptCompletion && _iterator.return != null) {
11302
+ await _iterator.return();
11303
+ }
11304
+ } finally {
11305
+ if (_didIteratorError) {
11306
+ throw _iteratorError;
11307
+ }
10773
11308
  }
10774
11309
  }
11310
+ stream.destroy();
10775
11311
  }
10776
- stream.destroy();
10777
- }
10778
11312
 
10779
- // Use streaming to read records without loading everything into memory
10780
- // Also rebuild offsets while we're at it
10781
- const fs = await import('fs');
10782
- const readline = await import('readline');
10783
- this.offsets = [];
10784
- let currentOffset = 0;
10785
- const stream = fs.createReadStream(this.fileHandler.file, {
10786
- highWaterMark: 64 * 1024,
10787
- encoding: 'utf8'
10788
- });
10789
- const rl = readline.createInterface({
10790
- input: stream,
10791
- crlfDelay: Infinity
10792
- });
10793
- try {
11313
+ // Use streaming to read records without loading everything into memory
11314
+ // Also rebuild offsets while we're at it
11315
+
11316
+ this.offsets = [];
11317
+ let currentOffset = 0;
11318
+ const stream = fs.createReadStream(this.fileHandler.file, {
11319
+ highWaterMark: 64 * 1024,
11320
+ encoding: 'utf8'
11321
+ });
11322
+ const rl = readline.createInterface({
11323
+ input: stream,
11324
+ crlfDelay: Infinity
11325
+ });
11326
+
11327
+ // Handle abort
11328
+ controller.signal.addEventListener('abort', () => {
11329
+ stream.destroy(new Error('AbortError'));
11330
+ rl.close();
11331
+ });
11332
+ let localCount = 0;
10794
11333
  var _iteratorAbruptCompletion2 = false;
10795
11334
  var _didIteratorError2 = false;
10796
11335
  var _iteratorError2;
@@ -10798,18 +11337,19 @@ class Database extends events.EventEmitter {
10798
11337
  for (var _iterator2 = _asyncIterator(rl), _step2; _iteratorAbruptCompletion2 = !(_step2 = await _iterator2.next()).done; _iteratorAbruptCompletion2 = false) {
10799
11338
  const line = _step2.value;
10800
11339
  {
11340
+ if (controller.signal.aborted) break;
10801
11341
  if (line && line.trim()) {
10802
11342
  try {
10803
11343
  // Record the offset for this line
10804
11344
  this.offsets.push(currentOffset);
10805
11345
  const record = this.serializer.deserialize(line);
10806
11346
  const recordWithTerms = this.restoreTermIdsAfterDeserialization(record);
10807
- await this.indexManager.add(recordWithTerms, count);
10808
- count++;
11347
+ await this.indexManager.add(recordWithTerms, count + localCount);
11348
+ localCount++;
10809
11349
  } catch (error) {
10810
11350
  // Skip invalid lines
10811
11351
  if (this.opts.debugMode) {
10812
- console.log(`⚠️ Rebuild: Failed to deserialize line ${count}:`, error.message);
11352
+ console.log(`⚠️ Rebuild: Failed to deserialize line ${count + localCount}:`, error.message);
10813
11353
  }
10814
11354
  }
10815
11355
  }
@@ -10831,27 +11371,31 @@ class Database extends events.EventEmitter {
10831
11371
  }
10832
11372
  }
10833
11373
  }
10834
- } finally {
11374
+ count += localCount;
10835
11375
  stream.destroy();
11376
+ } catch (error) {
11377
+ if (error.name === 'AbortError' || error.code === 'ETIMEDOUT') {
11378
+ if (this.opts.debugMode) {
11379
+ console.log(`⚠️ Index rebuild attempt ${attempt} timed out, retrying...`);
11380
+ }
11381
+ throw error; // p-retry will retry
11382
+ }
11383
+ // For other errors, don't retry
11384
+ throw new pRetry.AbortError(error);
11385
+ } finally {
11386
+ clearTimeout(timeout);
10836
11387
  }
10837
-
10838
- // Update indexManager totalLines
10839
- if (this.indexManager) {
10840
- this.indexManager.setTotalLines(this.offsets.length);
10841
- }
10842
- this._indexRebuildNeeded = false;
10843
- if (this.opts.debugMode) {
10844
- console.log(`✅ Index rebuilt from ${count} records in ${Date.now() - startTime}ms`);
10845
- }
10846
-
10847
- // Save the rebuilt index
10848
- await this._saveIndexDataToFile();
10849
- } catch (error) {
10850
- if (this.opts.debugMode) {
10851
- console.error('❌ Failed to rebuild indexes:', error.message);
11388
+ }, {
11389
+ retries: maxRetries,
11390
+ minTimeout: 200,
11391
+ maxTimeout: 2000,
11392
+ onFailedAttempt: error => {
11393
+ if (this.opts.debugMode) {
11394
+ console.log(`Index rebuild failed (attempt ${error.attemptNumber}), ${error.retriesLeft} retries left`);
11395
+ }
10852
11396
  }
10853
- // Don't throw - queries will fall back to streaming
10854
- }
11397
+ });
11398
+ return count;
10855
11399
  }
10856
11400
 
10857
11401
  /**
@@ -11632,56 +12176,11 @@ class Database extends events.EventEmitter {
11632
12176
  }
11633
12177
  }
11634
12178
  const groupedRanges = await this.fileHandler.groupedRanges(ranges);
11635
- const fs = await import('fs');
11636
12179
  const fd = await fs.promises.open(this.fileHandler.file, 'r');
11637
12180
  try {
11638
12181
  for (const groupedRange of groupedRanges) {
11639
- var _iteratorAbruptCompletion3 = false;
11640
- var _didIteratorError3 = false;
11641
- var _iteratorError3;
11642
- try {
11643
- for (var _iterator3 = _asyncIterator(this.fileHandler.readGroupedRange(groupedRange, fd)), _step3; _iteratorAbruptCompletion3 = !(_step3 = await _iterator3.next()).done; _iteratorAbruptCompletion3 = false) {
11644
- const row = _step3.value;
11645
- {
11646
- try {
11647
- const record = this.serializer.deserialize(row.line);
11648
-
11649
- // Get line number from the row, fallback to start offset mapping
11650
- let lineNumber = row._ !== null && row._ !== undefined ? row._ : startToLineNumber.get(row.start) ?? 0;
11651
-
11652
- // Restore term IDs to terms
11653
- const recordWithTerms = this.restoreTermIdsAfterDeserialization(record);
11654
-
11655
- // Add line number
11656
- recordWithTerms._ = lineNumber;
11657
-
11658
- // Add score if includeScore is true (default is true)
11659
- if (opts.includeScore !== false) {
11660
- recordWithTerms.score = scoresByLineNumber.get(lineNumber) || 0;
11661
- }
11662
- results.push(recordWithTerms);
11663
- } catch (error) {
11664
- // Skip invalid lines
11665
- if (this.opts.debugMode) {
11666
- console.error('Error deserializing record in score():', error);
11667
- }
11668
- }
11669
- }
11670
- }
11671
- } catch (err) {
11672
- _didIteratorError3 = true;
11673
- _iteratorError3 = err;
11674
- } finally {
11675
- try {
11676
- if (_iteratorAbruptCompletion3 && _iterator3.return != null) {
11677
- await _iterator3.return();
11678
- }
11679
- } finally {
11680
- if (_didIteratorError3) {
11681
- throw _iteratorError3;
11682
- }
11683
- }
11684
- }
12182
+ const rangeResults = await this._readGroupedRangeWithRetry(groupedRange, fd, startToLineNumber, scoresByLineNumber, opts);
12183
+ results.push(...rangeResults);
11685
12184
  }
11686
12185
  } finally {
11687
12186
  await fd.close();
@@ -11721,6 +12220,300 @@ class Database extends events.EventEmitter {
11721
12220
  return results;
11722
12221
  }
11723
12222
 
12223
+ /**
12224
+ * Read a grouped range with retry logic to handle I/O hangs
12225
+ * @private
12226
+ */
12227
+ async _readGroupedRangeWithRetry(groupedRange, fd, startToLineNumber, scoresByLineNumber, opts) {
12228
+ // If no timeout configured, use original implementation without retry
12229
+ if (!this.opts.ioTimeoutMs) {
12230
+ return this._readGroupedRangeOriginal(groupedRange, fd, startToLineNumber, scoresByLineNumber, opts);
12231
+ }
12232
+ const timeoutMs = this.opts.ioTimeoutMs || 3000; // Shorter timeout for range reads
12233
+ const maxRetries = this.opts.maxRetries || 3;
12234
+ const results = [];
12235
+ await pRetry(async attempt => {
12236
+ const controller = new AbortController();
12237
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
12238
+ try {
12239
+ // Collect results from the generator
12240
+ const rangeResults = [];
12241
+ const generator = this.fileHandler.readGroupedRange(groupedRange, fd);
12242
+
12243
+ // Handle abort
12244
+ controller.signal.addEventListener('abort', () => {
12245
+ generator.return(); // Close the generator
12246
+ });
12247
+ var _iteratorAbruptCompletion3 = false;
12248
+ var _didIteratorError3 = false;
12249
+ var _iteratorError3;
12250
+ try {
12251
+ for (var _iterator3 = _asyncIterator(generator), _step3; _iteratorAbruptCompletion3 = !(_step3 = await _iterator3.next()).done; _iteratorAbruptCompletion3 = false) {
12252
+ const row = _step3.value;
12253
+ {
12254
+ if (controller.signal.aborted) break;
12255
+ try {
12256
+ const record = this.serializer.deserialize(row.line);
12257
+
12258
+ // Get line number from the row, fallback to start offset mapping
12259
+ let lineNumber = row._ !== null && row._ !== undefined ? row._ : startToLineNumber.get(row.start) ?? 0;
12260
+
12261
+ // Restore term IDs to terms
12262
+ const recordWithTerms = this.restoreTermIdsAfterDeserialization(record);
12263
+
12264
+ // Add line number
12265
+ recordWithTerms._ = lineNumber;
12266
+
12267
+ // Add score if includeScore is true (default is true)
12268
+ if (opts.includeScore !== false) {
12269
+ recordWithTerms.score = scoresByLineNumber.get(lineNumber) || 0;
12270
+ }
12271
+ rangeResults.push(recordWithTerms);
12272
+ } catch (error) {
12273
+ // Skip invalid lines
12274
+ if (this.opts.debugMode) {
12275
+ console.error('Error deserializing record in score():', error);
12276
+ }
12277
+ }
12278
+ }
12279
+ }
12280
+ } catch (err) {
12281
+ _didIteratorError3 = true;
12282
+ _iteratorError3 = err;
12283
+ } finally {
12284
+ try {
12285
+ if (_iteratorAbruptCompletion3 && _iterator3.return != null) {
12286
+ await _iterator3.return();
12287
+ }
12288
+ } finally {
12289
+ if (_didIteratorError3) {
12290
+ throw _iteratorError3;
12291
+ }
12292
+ }
12293
+ }
12294
+ results.push(...rangeResults);
12295
+ } catch (error) {
12296
+ if (error.name === 'AbortError' || error.code === 'ETIMEDOUT') {
12297
+ if (this.opts.debugMode) {
12298
+ console.log(`⚠️ Score range read attempt ${attempt} timed out, retrying...`);
12299
+ }
12300
+ throw error; // p-retry will retry
12301
+ }
12302
+ // For other errors, don't retry
12303
+ throw new pRetry.AbortError(error);
12304
+ } finally {
12305
+ clearTimeout(timeout);
12306
+ }
12307
+ }, {
12308
+ retries: maxRetries,
12309
+ minTimeout: 100,
12310
+ maxTimeout: 500,
12311
+ onFailedAttempt: error => {
12312
+ if (this.opts.debugMode) {
12313
+ console.log(`Score range read failed (attempt ${error.attemptNumber}), ${error.retriesLeft} retries left`);
12314
+ }
12315
+ }
12316
+ });
12317
+ return results;
12318
+ }
12319
+
12320
+ /**
12321
+ * Original read grouped range logic without retry (for backward compatibility)
12322
+ * @private
12323
+ */
12324
+ async _readGroupedRangeOriginal(groupedRange, fd, startToLineNumber, scoresByLineNumber, opts) {
12325
+ const results = [];
12326
+
12327
+ // Collect results from the generator
12328
+ const rangeResults = [];
12329
+ const generator = this.fileHandler.readGroupedRange(groupedRange, fd);
12330
+ var _iteratorAbruptCompletion4 = false;
12331
+ var _didIteratorError4 = false;
12332
+ var _iteratorError4;
12333
+ try {
12334
+ for (var _iterator4 = _asyncIterator(generator), _step4; _iteratorAbruptCompletion4 = !(_step4 = await _iterator4.next()).done; _iteratorAbruptCompletion4 = false) {
12335
+ const row = _step4.value;
12336
+ {
12337
+ try {
12338
+ const record = this.serializer.deserialize(row.line);
12339
+
12340
+ // Get line number from the row, fallback to start offset mapping
12341
+ let lineNumber = row._ !== null && row._ !== undefined ? row._ : startToLineNumber.get(row.start) ?? 0;
12342
+
12343
+ // Restore term IDs to terms
12344
+ const recordWithTerms = this.restoreTermIdsAfterDeserialization(record);
12345
+
12346
+ // Add line number
12347
+ recordWithTerms._ = lineNumber;
12348
+
12349
+ // Add score if includeScore is true (default is true)
12350
+ if (opts.includeScore !== false) {
12351
+ recordWithTerms.score = scoresByLineNumber.get(lineNumber) || 0;
12352
+ }
12353
+ rangeResults.push(recordWithTerms);
12354
+ } catch (error) {
12355
+ // Skip invalid lines
12356
+ if (this.opts.debugMode) {
12357
+ console.error('Error deserializing record in score():', error);
12358
+ }
12359
+ }
12360
+ }
12361
+ }
12362
+ } catch (err) {
12363
+ _didIteratorError4 = true;
12364
+ _iteratorError4 = err;
12365
+ } finally {
12366
+ try {
12367
+ if (_iteratorAbruptCompletion4 && _iterator4.return != null) {
12368
+ await _iterator4.return();
12369
+ }
12370
+ } finally {
12371
+ if (_didIteratorError4) {
12372
+ throw _iteratorError4;
12373
+ }
12374
+ }
12375
+ }
12376
+ results.push(...rangeResults);
12377
+ return results;
12378
+ }
12379
+
12380
+ /**
12381
+ * Original rebuild indexes logic without retry (for backward compatibility)
12382
+ * @private
12383
+ */
12384
+ async _rebuildIndexesOriginal() {
12385
+ let count = 0;
12386
+
12387
+ // Auto-detect schema from first line if not initialized
12388
+ if (!this.serializer.schemaManager.isInitialized) {
12389
+ const stream = fs.createReadStream(this.fileHandler.file, {
12390
+ highWaterMark: 64 * 1024,
12391
+ encoding: 'utf8'
12392
+ });
12393
+ const rl = readline.createInterface({
12394
+ input: stream,
12395
+ crlfDelay: Infinity
12396
+ });
12397
+ var _iteratorAbruptCompletion5 = false;
12398
+ var _didIteratorError5 = false;
12399
+ var _iteratorError5;
12400
+ try {
12401
+ for (var _iterator5 = _asyncIterator(rl), _step5; _iteratorAbruptCompletion5 = !(_step5 = await _iterator5.next()).done; _iteratorAbruptCompletion5 = false) {
12402
+ const line = _step5.value;
12403
+ {
12404
+ if (line && line.trim()) {
12405
+ try {
12406
+ const firstRecord = JSON.parse(line);
12407
+ if (Array.isArray(firstRecord)) {
12408
+ // Try to infer schema from opts.fields if available
12409
+ if (this.opts.fields && typeof this.opts.fields === 'object') {
12410
+ const fieldNames = Object.keys(this.opts.fields);
12411
+ if (fieldNames.length >= firstRecord.length) {
12412
+ // Use first N fields from opts.fields to match array length
12413
+ const schema = fieldNames.slice(0, firstRecord.length);
12414
+ this.serializer.initializeSchema(schema);
12415
+ if (this.opts.debugMode) {
12416
+ console.log(`🔍 Inferred schema from opts.fields: ${schema.join(', ')}`);
12417
+ }
12418
+ } else {
12419
+ throw new Error(`Cannot rebuild index: array has ${firstRecord.length} elements but opts.fields only defines ${fieldNames.length} fields. Schema must be explicitly provided.`);
12420
+ }
12421
+ } else {
12422
+ throw new Error('Cannot rebuild index: schema missing, file uses array format, and opts.fields not provided. The .idx.jdb file is corrupted.');
12423
+ }
12424
+ } else {
12425
+ // Object format, initialize from object keys
12426
+ this.serializer.initializeSchema(firstRecord, true);
12427
+ if (this.opts.debugMode) {
12428
+ console.log(`🔍 Auto-detected schema from object: ${Object.keys(firstRecord).join(', ')}`);
12429
+ }
12430
+ }
12431
+ break;
12432
+ } catch (error) {
12433
+ if (this.opts.debugMode) {
12434
+ console.error('❌ Failed to auto-detect schema:', error.message);
12435
+ }
12436
+ throw error;
12437
+ }
12438
+ }
12439
+ }
12440
+ }
12441
+ } catch (err) {
12442
+ _didIteratorError5 = true;
12443
+ _iteratorError5 = err;
12444
+ } finally {
12445
+ try {
12446
+ if (_iteratorAbruptCompletion5 && _iterator5.return != null) {
12447
+ await _iterator5.return();
12448
+ }
12449
+ } finally {
12450
+ if (_didIteratorError5) {
12451
+ throw _iteratorError5;
12452
+ }
12453
+ }
12454
+ }
12455
+ stream.destroy();
12456
+ }
12457
+
12458
+ // Use streaming to read records without loading everything into memory
12459
+ // Also rebuild offsets while we're at it
12460
+ this.offsets = [];
12461
+ let currentOffset = 0;
12462
+ const stream = fs.createReadStream(this.fileHandler.file, {
12463
+ highWaterMark: 64 * 1024,
12464
+ encoding: 'utf8'
12465
+ });
12466
+ const rl = readline.createInterface({
12467
+ input: stream,
12468
+ crlfDelay: Infinity
12469
+ });
12470
+ try {
12471
+ var _iteratorAbruptCompletion6 = false;
12472
+ var _didIteratorError6 = false;
12473
+ var _iteratorError6;
12474
+ try {
12475
+ for (var _iterator6 = _asyncIterator(rl), _step6; _iteratorAbruptCompletion6 = !(_step6 = await _iterator6.next()).done; _iteratorAbruptCompletion6 = false) {
12476
+ const line = _step6.value;
12477
+ {
12478
+ if (line && line.trim()) {
12479
+ try {
12480
+ // Record the offset for this line
12481
+ this.offsets.push(currentOffset);
12482
+ const record = this.serializer.deserialize(line);
12483
+ const recordWithTerms = this.restoreTermIdsAfterDeserialization(record);
12484
+ await this.indexManager.add(recordWithTerms, count);
12485
+ count++;
12486
+ } catch (error) {
12487
+ // Skip invalid lines
12488
+ if (this.opts.debugMode) {
12489
+ console.log(`⚠️ Rebuild: Failed to deserialize line ${count}:`, error.message);
12490
+ }
12491
+ }
12492
+ }
12493
+ // Update offset for next line (including newline character)
12494
+ currentOffset += Buffer.byteLength(line, 'utf8') + 1;
12495
+ }
12496
+ }
12497
+ } catch (err) {
12498
+ _didIteratorError6 = true;
12499
+ _iteratorError6 = err;
12500
+ } finally {
12501
+ try {
12502
+ if (_iteratorAbruptCompletion6 && _iterator6.return != null) {
12503
+ await _iterator6.return();
12504
+ }
12505
+ } finally {
12506
+ if (_didIteratorError6) {
12507
+ throw _iteratorError6;
12508
+ }
12509
+ }
12510
+ }
12511
+ } finally {
12512
+ stream.destroy();
12513
+ }
12514
+ return count;
12515
+ }
12516
+
11724
12517
  /**
11725
12518
  * Wait for all pending operations to complete
11726
12519
  */
@@ -12030,7 +12823,6 @@ class Database extends events.EventEmitter {
12030
12823
 
12031
12824
  // Method 1: Try to read the entire file and filter
12032
12825
  if (this.fileHandler.exists()) {
12033
- const fs = await import('fs');
12034
12826
  const fileContent = await fs.promises.readFile(this.normalizedFile, 'utf8');
12035
12827
  const lines = fileContent.split('\n').filter(line => line.trim());
12036
12828
  for (let i = 0; i < lines.length && i < this.offsets.length; i++) {
@@ -12730,10 +13522,9 @@ class Database extends events.EventEmitter {
12730
13522
  return;
12731
13523
  }
12732
13524
  _this._offsetRecoveryInProgress = true;
12733
- const fsModule = _this._fsModule || (_this._fsModule = yield _awaitAsyncGenerator(import('fs')));
12734
13525
  let fd;
12735
13526
  try {
12736
- fd = yield _awaitAsyncGenerator(fsModule.promises.open(_this.fileHandler.file, 'r'));
13527
+ fd = yield _awaitAsyncGenerator(fs.promises.open(_this.fileHandler.file, 'r'));
12737
13528
  } catch (error) {
12738
13529
  _this._offsetRecoveryInProgress = false;
12739
13530
  if (_this.opts.debugMode) {
@@ -13036,16 +13827,15 @@ class Database extends events.EventEmitter {
13036
13827
  // OPTIMIZATION: Use ranges instead of reading entire file
13037
13828
  const ranges = _this2.getRanges(map);
13038
13829
  const groupedRanges = yield _awaitAsyncGenerator(_this2.fileHandler.groupedRanges(ranges));
13039
- const fs = yield _awaitAsyncGenerator(import('fs'));
13040
13830
  const fd = yield _awaitAsyncGenerator(fs.promises.open(_this2.fileHandler.file, 'r'));
13041
13831
  try {
13042
13832
  for (const groupedRange of groupedRanges) {
13043
- var _iteratorAbruptCompletion4 = false;
13044
- var _didIteratorError4 = false;
13045
- var _iteratorError4;
13833
+ var _iteratorAbruptCompletion7 = false;
13834
+ var _didIteratorError7 = false;
13835
+ var _iteratorError7;
13046
13836
  try {
13047
- for (var _iterator4 = _asyncIterator(_this2.fileHandler.readGroupedRange(groupedRange, fd)), _step4; _iteratorAbruptCompletion4 = !(_step4 = yield _awaitAsyncGenerator(_iterator4.next())).done; _iteratorAbruptCompletion4 = false) {
13048
- const row = _step4.value;
13837
+ for (var _iterator7 = _asyncIterator(_this2.fileHandler.readGroupedRange(groupedRange, fd)), _step7; _iteratorAbruptCompletion7 = !(_step7 = yield _awaitAsyncGenerator(_iterator7.next())).done; _iteratorAbruptCompletion7 = false) {
13838
+ const row = _step7.value;
13049
13839
  {
13050
13840
  if (options.limit && count >= options.limit) {
13051
13841
  break;
@@ -13119,28 +13909,28 @@ class Database extends events.EventEmitter {
13119
13909
  }
13120
13910
  }
13121
13911
  if (!_this2._offsetRecoveryInProgress) {
13122
- var _iteratorAbruptCompletion5 = false;
13123
- var _didIteratorError5 = false;
13124
- var _iteratorError5;
13912
+ var _iteratorAbruptCompletion8 = false;
13913
+ var _didIteratorError8 = false;
13914
+ var _iteratorError8;
13125
13915
  try {
13126
- for (var _iterator5 = _asyncIterator(_this2._streamingRecoveryGenerator(criteria, options, count, map, remainingSkip)), _step5; _iteratorAbruptCompletion5 = !(_step5 = yield _awaitAsyncGenerator(_iterator5.next())).done; _iteratorAbruptCompletion5 = false) {
13127
- const recoveredEntry = _step5.value;
13916
+ for (var _iterator8 = _asyncIterator(_this2._streamingRecoveryGenerator(criteria, options, count, map, remainingSkip)), _step8; _iteratorAbruptCompletion8 = !(_step8 = yield _awaitAsyncGenerator(_iterator8.next())).done; _iteratorAbruptCompletion8 = false) {
13917
+ const recoveredEntry = _step8.value;
13128
13918
  {
13129
13919
  yield recoveredEntry;
13130
13920
  count++;
13131
13921
  }
13132
13922
  }
13133
13923
  } catch (err) {
13134
- _didIteratorError5 = true;
13135
- _iteratorError5 = err;
13924
+ _didIteratorError8 = true;
13925
+ _iteratorError8 = err;
13136
13926
  } finally {
13137
13927
  try {
13138
- if (_iteratorAbruptCompletion5 && _iterator5.return != null) {
13139
- yield _awaitAsyncGenerator(_iterator5.return());
13928
+ if (_iteratorAbruptCompletion8 && _iterator8.return != null) {
13929
+ yield _awaitAsyncGenerator(_iterator8.return());
13140
13930
  }
13141
13931
  } finally {
13142
- if (_didIteratorError5) {
13143
- throw _iteratorError5;
13932
+ if (_didIteratorError8) {
13933
+ throw _iteratorError8;
13144
13934
  }
13145
13935
  }
13146
13936
  }
@@ -13152,16 +13942,16 @@ class Database extends events.EventEmitter {
13152
13942
  }
13153
13943
  }
13154
13944
  } catch (err) {
13155
- _didIteratorError4 = true;
13156
- _iteratorError4 = err;
13945
+ _didIteratorError7 = true;
13946
+ _iteratorError7 = err;
13157
13947
  } finally {
13158
13948
  try {
13159
- if (_iteratorAbruptCompletion4 && _iterator4.return != null) {
13160
- yield _awaitAsyncGenerator(_iterator4.return());
13949
+ if (_iteratorAbruptCompletion7 && _iterator7.return != null) {
13950
+ yield _awaitAsyncGenerator(_iterator7.return());
13161
13951
  }
13162
13952
  } finally {
13163
- if (_didIteratorError4) {
13164
- throw _iteratorError4;
13953
+ if (_didIteratorError7) {
13954
+ throw _iteratorError7;
13165
13955
  }
13166
13956
  }
13167
13957
  }
@@ -13227,12 +14017,12 @@ class Database extends events.EventEmitter {
13227
14017
  if (options.limit && count >= options.limit) {
13228
14018
  break;
13229
14019
  }
13230
- var _iteratorAbruptCompletion6 = false;
13231
- var _didIteratorError6 = false;
13232
- var _iteratorError6;
14020
+ var _iteratorAbruptCompletion9 = false;
14021
+ var _didIteratorError9 = false;
14022
+ var _iteratorError9;
13233
14023
  try {
13234
- for (var _iterator6 = _asyncIterator(_this2.fileHandler.readGroupedRange(groupedRange, fd)), _step6; _iteratorAbruptCompletion6 = !(_step6 = yield _awaitAsyncGenerator(_iterator6.next())).done; _iteratorAbruptCompletion6 = false) {
13235
- const row = _step6.value;
14024
+ for (var _iterator9 = _asyncIterator(_this2.fileHandler.readGroupedRange(groupedRange, fd)), _step9; _iteratorAbruptCompletion9 = !(_step9 = yield _awaitAsyncGenerator(_iterator9.next())).done; _iteratorAbruptCompletion9 = false) {
14025
+ const row = _step9.value;
13236
14026
  {
13237
14027
  if (options.limit && count >= options.limit) {
13238
14028
  break;
@@ -13313,28 +14103,28 @@ class Database extends events.EventEmitter {
13313
14103
  }
13314
14104
  }
13315
14105
  if (!_this2._offsetRecoveryInProgress) {
13316
- var _iteratorAbruptCompletion7 = false;
13317
- var _didIteratorError7 = false;
13318
- var _iteratorError7;
14106
+ var _iteratorAbruptCompletion0 = false;
14107
+ var _didIteratorError0 = false;
14108
+ var _iteratorError0;
13319
14109
  try {
13320
- for (var _iterator7 = _asyncIterator(_this2._streamingRecoveryGenerator(criteria, options, count, map, remainingSkip)), _step7; _iteratorAbruptCompletion7 = !(_step7 = yield _awaitAsyncGenerator(_iterator7.next())).done; _iteratorAbruptCompletion7 = false) {
13321
- const recoveredEntry = _step7.value;
14110
+ for (var _iterator0 = _asyncIterator(_this2._streamingRecoveryGenerator(criteria, options, count, map, remainingSkip)), _step0; _iteratorAbruptCompletion0 = !(_step0 = yield _awaitAsyncGenerator(_iterator0.next())).done; _iteratorAbruptCompletion0 = false) {
14111
+ const recoveredEntry = _step0.value;
13322
14112
  {
13323
14113
  yield recoveredEntry;
13324
14114
  count++;
13325
14115
  }
13326
14116
  }
13327
14117
  } catch (err) {
13328
- _didIteratorError7 = true;
13329
- _iteratorError7 = err;
14118
+ _didIteratorError0 = true;
14119
+ _iteratorError0 = err;
13330
14120
  } finally {
13331
14121
  try {
13332
- if (_iteratorAbruptCompletion7 && _iterator7.return != null) {
13333
- yield _awaitAsyncGenerator(_iterator7.return());
14122
+ if (_iteratorAbruptCompletion0 && _iterator0.return != null) {
14123
+ yield _awaitAsyncGenerator(_iterator0.return());
13334
14124
  }
13335
14125
  } finally {
13336
- if (_didIteratorError7) {
13337
- throw _iteratorError7;
14126
+ if (_didIteratorError0) {
14127
+ throw _iteratorError0;
13338
14128
  }
13339
14129
  }
13340
14130
  }
@@ -13346,16 +14136,16 @@ class Database extends events.EventEmitter {
13346
14136
  }
13347
14137
  }
13348
14138
  } catch (err) {
13349
- _didIteratorError6 = true;
13350
- _iteratorError6 = err;
14139
+ _didIteratorError9 = true;
14140
+ _iteratorError9 = err;
13351
14141
  } finally {
13352
14142
  try {
13353
- if (_iteratorAbruptCompletion6 && _iterator6.return != null) {
13354
- yield _awaitAsyncGenerator(_iterator6.return());
14143
+ if (_iteratorAbruptCompletion9 && _iterator9.return != null) {
14144
+ yield _awaitAsyncGenerator(_iterator9.return());
13355
14145
  }
13356
14146
  } finally {
13357
- if (_didIteratorError6) {
13358
- throw _iteratorError6;
14147
+ if (_didIteratorError9) {
14148
+ throw _iteratorError9;
13359
14149
  }
13360
14150
  }
13361
14151
  }
@@ -13410,12 +14200,12 @@ class Database extends events.EventEmitter {
13410
14200
 
13411
14201
  try {
13412
14202
  // Always use walk() now that the bug is fixed - it works for both small and large datasets
13413
- var _iteratorAbruptCompletion8 = false;
13414
- var _didIteratorError8 = false;
13415
- var _iteratorError8;
14203
+ var _iteratorAbruptCompletion1 = false;
14204
+ var _didIteratorError1 = false;
14205
+ var _iteratorError1;
13416
14206
  try {
13417
- for (var _iterator8 = _asyncIterator(_this3.walk(criteria, options)), _step8; _iteratorAbruptCompletion8 = !(_step8 = yield _awaitAsyncGenerator(_iterator8.next())).done; _iteratorAbruptCompletion8 = false) {
13418
- const entry = _step8.value;
14207
+ for (var _iterator1 = _asyncIterator(_this3.walk(criteria, options)), _step1; _iteratorAbruptCompletion1 = !(_step1 = yield _awaitAsyncGenerator(_iterator1.next())).done; _iteratorAbruptCompletion1 = false) {
14208
+ const entry = _step1.value;
13419
14209
  {
13420
14210
  processedCount++;
13421
14211
 
@@ -13475,16 +14265,16 @@ class Database extends events.EventEmitter {
13475
14265
 
13476
14266
  // Process remaining records in buffers
13477
14267
  } catch (err) {
13478
- _didIteratorError8 = true;
13479
- _iteratorError8 = err;
14268
+ _didIteratorError1 = true;
14269
+ _iteratorError1 = err;
13480
14270
  } finally {
13481
14271
  try {
13482
- if (_iteratorAbruptCompletion8 && _iterator8.return != null) {
13483
- yield _awaitAsyncGenerator(_iterator8.return());
14272
+ if (_iteratorAbruptCompletion1 && _iterator1.return != null) {
14273
+ yield _awaitAsyncGenerator(_iterator1.return());
13484
14274
  }
13485
14275
  } finally {
13486
- if (_didIteratorError8) {
13487
- throw _iteratorError8;
14276
+ if (_didIteratorError1) {
14277
+ throw _iteratorError1;
13488
14278
  }
13489
14279
  }
13490
14280
  }
@@ -13677,7 +14467,6 @@ class Database extends events.EventEmitter {
13677
14467
  // If the .idx.jdb file exists and has data, and we're trying to save empty index,
13678
14468
  // skip the save to prevent corruption
13679
14469
  if (isEmpty && !this.offsets?.length) {
13680
- const fs = await import('fs');
13681
14470
  if (fs.existsSync(idxPath)) {
13682
14471
  try {
13683
14472
  const existingData = JSON.parse(await fs.promises.readFile(idxPath, 'utf8'));