jexidb 2.1.5 → 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/README.md +460 -449
- package/dist/Database.cjs +1321 -259
- package/package.json +3 -2
- package/src/Database.mjs +674 -136
- package/src/FileHandler.mjs +92 -32
- package/src/managers/QueryManager.mjs +13 -7
- package/src/managers/TermManager.mjs +7 -0
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
|
|
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.
|
|
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.
|
|
4963
|
+
return this.readLimiter(() => this._readWithStreamingRetry(criteria, options, matchesCriteria, serializer));
|
|
4529
4964
|
}
|
|
4530
4965
|
}
|
|
4531
|
-
async
|
|
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);
|
|
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
|
+
});
|
|
5001
|
+
}
|
|
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 (
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
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
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
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
|
-
}
|
|
4601
|
-
|
|
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) {
|
|
@@ -5667,16 +6154,19 @@ class QueryManager {
|
|
|
5667
6154
|
return this.matchesCriteria(record, criteria, options);
|
|
5668
6155
|
}, this.serializer || null);
|
|
5669
6156
|
|
|
6157
|
+
// SPACE OPTIMIZATION: Restore term IDs to terms for user (unless disabled)
|
|
6158
|
+
const resultsWithTerms = options.restoreTerms !== false ? results.map(record => this.database.restoreTermIdsAfterDeserialization(record)) : results;
|
|
6159
|
+
|
|
5670
6160
|
// Apply ordering if specified
|
|
5671
6161
|
if (options.orderBy) {
|
|
5672
6162
|
const [field, direction = 'asc'] = options.orderBy.split(' ');
|
|
5673
|
-
|
|
6163
|
+
resultsWithTerms.sort((a, b) => {
|
|
5674
6164
|
if (a[field] > b[field]) return direction === 'asc' ? 1 : -1;
|
|
5675
6165
|
if (a[field] < b[field]) return direction === 'asc' ? -1 : 1;
|
|
5676
6166
|
return 0;
|
|
5677
6167
|
});
|
|
5678
6168
|
}
|
|
5679
|
-
return
|
|
6169
|
+
return resultsWithTerms;
|
|
5680
6170
|
}
|
|
5681
6171
|
|
|
5682
6172
|
/**
|
|
@@ -5765,7 +6255,6 @@ class QueryManager {
|
|
|
5765
6255
|
const ranges = this.database.getRanges(fileLineNumbers);
|
|
5766
6256
|
if (ranges.length > 0) {
|
|
5767
6257
|
const groupedRanges = await this.database.fileHandler.groupedRanges(ranges);
|
|
5768
|
-
const fs = await import('fs');
|
|
5769
6258
|
const fd = await fs.promises.open(this.database.fileHandler.file, 'r');
|
|
5770
6259
|
try {
|
|
5771
6260
|
for (const groupedRange of groupedRanges) {
|
|
@@ -5779,6 +6268,7 @@ class QueryManager {
|
|
|
5779
6268
|
try {
|
|
5780
6269
|
const record = this.database.serializer.deserialize(row.line);
|
|
5781
6270
|
const recordWithTerms = options.restoreTerms !== false ? this.database.restoreTermIdsAfterDeserialization(record) : record;
|
|
6271
|
+
recordWithTerms._ = row._;
|
|
5782
6272
|
results.push(recordWithTerms);
|
|
5783
6273
|
if (limit && results.length >= limit) break;
|
|
5784
6274
|
} catch (error) {
|
|
@@ -5817,6 +6307,7 @@ class QueryManager {
|
|
|
5817
6307
|
const record = this.database.writeBuffer[writeBufferIndex];
|
|
5818
6308
|
if (record) {
|
|
5819
6309
|
const recordWithTerms = options.restoreTerms !== false ? this.database.restoreTermIdsAfterDeserialization(record) : record;
|
|
6310
|
+
recordWithTerms._ = lineNumber;
|
|
5820
6311
|
results.push(recordWithTerms);
|
|
5821
6312
|
}
|
|
5822
6313
|
}
|
|
@@ -6497,7 +6988,7 @@ class QueryManager {
|
|
|
6497
6988
|
if (typeof condition === 'object' && !Array.isArray(condition)) {
|
|
6498
6989
|
const operators = Object.keys(condition);
|
|
6499
6990
|
for (const op of operators) {
|
|
6500
|
-
if (!['$in', '$nin', '$contains', '$all', '>', '>=', '<', '<=', '!=', 'contains', 'regex'].includes(op)) {
|
|
6991
|
+
if (!['$in', '$nin', '$contains', '$all', '$exists', '>', '>=', '<', '<=', '!=', 'contains', 'regex'].includes(op)) {
|
|
6501
6992
|
throw new Error(`Operator '${op}' is not supported in strict mode for field '${field}'.`);
|
|
6502
6993
|
}
|
|
6503
6994
|
}
|
|
@@ -7592,6 +8083,13 @@ class TermManager {
|
|
|
7592
8083
|
const orphanedCount = stats.orphanedTerms;
|
|
7593
8084
|
const totalTerms = stats.totalTerms;
|
|
7594
8085
|
|
|
8086
|
+
// SAFETY: If all terms are marked as orphaned, it likely means counts
|
|
8087
|
+
// haven't been rebuilt after loading from disk. Skip cleanup to avoid
|
|
8088
|
+
// wiping valid term mappings.
|
|
8089
|
+
if (totalTerms > 0 && orphanedCount === totalTerms) {
|
|
8090
|
+
return 0;
|
|
8091
|
+
}
|
|
8092
|
+
|
|
7595
8093
|
// Only cleanup if conditions are met
|
|
7596
8094
|
const shouldCleanup = orphanedCount >= minOrphanCount &&
|
|
7597
8095
|
// Minimum orphan count
|
|
@@ -8094,7 +8592,7 @@ class Database extends events.EventEmitter {
|
|
|
8094
8592
|
this.initializeManagers();
|
|
8095
8593
|
|
|
8096
8594
|
// Initialize file mutex for thread safety
|
|
8097
|
-
this.fileMutex = new
|
|
8595
|
+
this.fileMutex = new Mutex();
|
|
8098
8596
|
|
|
8099
8597
|
// Initialize performance tracking
|
|
8100
8598
|
this.performanceStats = {
|
|
@@ -9971,6 +10469,16 @@ class Database extends events.EventEmitter {
|
|
|
9971
10469
|
// CRITICAL FIX: Validate state before find operation
|
|
9972
10470
|
this.validateState();
|
|
9973
10471
|
|
|
10472
|
+
// Check for index-only optimization
|
|
10473
|
+
if (options.indexOnly && this._canUseIndexOnlyForExists(criteria)) {
|
|
10474
|
+
if (this.opts.debugMode) {
|
|
10475
|
+
console.log(`⚡ find() using INDEX-ONLY optimization for: ${JSON.stringify(criteria)}`);
|
|
10476
|
+
}
|
|
10477
|
+
// Return records using index-only lookup (but we need to fetch actual records)
|
|
10478
|
+
// For now, this just ensures we use optimized path in QueryManager
|
|
10479
|
+
options._forceIndexOnly = true;
|
|
10480
|
+
}
|
|
10481
|
+
|
|
9974
10482
|
// OPTIMIZATION: Find searches writeBuffer directly
|
|
9975
10483
|
|
|
9976
10484
|
const startTime = Date.now();
|
|
@@ -10681,94 +11189,147 @@ class Database extends events.EventEmitter {
|
|
|
10681
11189
|
let count = 0;
|
|
10682
11190
|
const startTime = Date.now();
|
|
10683
11191
|
|
|
10684
|
-
//
|
|
10685
|
-
if (
|
|
10686
|
-
|
|
10687
|
-
|
|
10688
|
-
|
|
10689
|
-
|
|
10690
|
-
|
|
10691
|
-
|
|
10692
|
-
|
|
10693
|
-
|
|
10694
|
-
|
|
10695
|
-
|
|
10696
|
-
|
|
10697
|
-
|
|
10698
|
-
|
|
10699
|
-
|
|
10700
|
-
|
|
10701
|
-
|
|
10702
|
-
|
|
10703
|
-
|
|
10704
|
-
|
|
10705
|
-
|
|
10706
|
-
|
|
10707
|
-
|
|
10708
|
-
|
|
10709
|
-
|
|
10710
|
-
|
|
10711
|
-
|
|
10712
|
-
|
|
10713
|
-
|
|
10714
|
-
|
|
10715
|
-
|
|
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.`);
|
|
10716
11275
|
}
|
|
10717
11276
|
} else {
|
|
10718
|
-
throw new Error(
|
|
11277
|
+
throw new Error('Cannot rebuild index: schema missing, file uses array format, and opts.fields not provided. The .idx.jdb file is corrupted.');
|
|
10719
11278
|
}
|
|
10720
11279
|
} else {
|
|
10721
|
-
|
|
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
|
+
}
|
|
10722
11285
|
}
|
|
10723
|
-
|
|
10724
|
-
|
|
10725
|
-
this.serializer.initializeSchema(firstRecord, true);
|
|
11286
|
+
break;
|
|
11287
|
+
} catch (error) {
|
|
10726
11288
|
if (this.opts.debugMode) {
|
|
10727
|
-
console.
|
|
11289
|
+
console.error('❌ Failed to auto-detect schema:', error.message);
|
|
10728
11290
|
}
|
|
11291
|
+
throw error;
|
|
10729
11292
|
}
|
|
10730
|
-
break;
|
|
10731
|
-
} catch (error) {
|
|
10732
|
-
if (this.opts.debugMode) {
|
|
10733
|
-
console.error('❌ Failed to auto-detect schema:', error.message);
|
|
10734
|
-
}
|
|
10735
|
-
throw error;
|
|
10736
11293
|
}
|
|
10737
11294
|
}
|
|
10738
11295
|
}
|
|
10739
|
-
}
|
|
10740
|
-
|
|
10741
|
-
|
|
10742
|
-
_iteratorError = err;
|
|
10743
|
-
} finally {
|
|
10744
|
-
try {
|
|
10745
|
-
if (_iteratorAbruptCompletion && _iterator.return != null) {
|
|
10746
|
-
await _iterator.return();
|
|
10747
|
-
}
|
|
11296
|
+
} catch (err) {
|
|
11297
|
+
_didIteratorError = true;
|
|
11298
|
+
_iteratorError = err;
|
|
10748
11299
|
} finally {
|
|
10749
|
-
|
|
10750
|
-
|
|
11300
|
+
try {
|
|
11301
|
+
if (_iteratorAbruptCompletion && _iterator.return != null) {
|
|
11302
|
+
await _iterator.return();
|
|
11303
|
+
}
|
|
11304
|
+
} finally {
|
|
11305
|
+
if (_didIteratorError) {
|
|
11306
|
+
throw _iteratorError;
|
|
11307
|
+
}
|
|
10751
11308
|
}
|
|
10752
11309
|
}
|
|
11310
|
+
stream.destroy();
|
|
10753
11311
|
}
|
|
10754
|
-
stream.destroy();
|
|
10755
|
-
}
|
|
10756
11312
|
|
|
10757
|
-
|
|
10758
|
-
|
|
10759
|
-
|
|
10760
|
-
|
|
10761
|
-
|
|
10762
|
-
|
|
10763
|
-
|
|
10764
|
-
|
|
10765
|
-
|
|
10766
|
-
|
|
10767
|
-
|
|
10768
|
-
|
|
10769
|
-
|
|
10770
|
-
|
|
10771
|
-
|
|
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;
|
|
10772
11333
|
var _iteratorAbruptCompletion2 = false;
|
|
10773
11334
|
var _didIteratorError2 = false;
|
|
10774
11335
|
var _iteratorError2;
|
|
@@ -10776,18 +11337,19 @@ class Database extends events.EventEmitter {
|
|
|
10776
11337
|
for (var _iterator2 = _asyncIterator(rl), _step2; _iteratorAbruptCompletion2 = !(_step2 = await _iterator2.next()).done; _iteratorAbruptCompletion2 = false) {
|
|
10777
11338
|
const line = _step2.value;
|
|
10778
11339
|
{
|
|
11340
|
+
if (controller.signal.aborted) break;
|
|
10779
11341
|
if (line && line.trim()) {
|
|
10780
11342
|
try {
|
|
10781
11343
|
// Record the offset for this line
|
|
10782
11344
|
this.offsets.push(currentOffset);
|
|
10783
11345
|
const record = this.serializer.deserialize(line);
|
|
10784
11346
|
const recordWithTerms = this.restoreTermIdsAfterDeserialization(record);
|
|
10785
|
-
await this.indexManager.add(recordWithTerms, count);
|
|
10786
|
-
|
|
11347
|
+
await this.indexManager.add(recordWithTerms, count + localCount);
|
|
11348
|
+
localCount++;
|
|
10787
11349
|
} catch (error) {
|
|
10788
11350
|
// Skip invalid lines
|
|
10789
11351
|
if (this.opts.debugMode) {
|
|
10790
|
-
console.log(`⚠️ Rebuild: Failed to deserialize line ${count}:`, error.message);
|
|
11352
|
+
console.log(`⚠️ Rebuild: Failed to deserialize line ${count + localCount}:`, error.message);
|
|
10791
11353
|
}
|
|
10792
11354
|
}
|
|
10793
11355
|
}
|
|
@@ -10809,27 +11371,31 @@ class Database extends events.EventEmitter {
|
|
|
10809
11371
|
}
|
|
10810
11372
|
}
|
|
10811
11373
|
}
|
|
10812
|
-
|
|
11374
|
+
count += localCount;
|
|
10813
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);
|
|
10814
11387
|
}
|
|
10815
|
-
|
|
10816
|
-
|
|
10817
|
-
|
|
10818
|
-
|
|
10819
|
-
|
|
10820
|
-
|
|
10821
|
-
|
|
10822
|
-
|
|
10823
|
-
}
|
|
10824
|
-
|
|
10825
|
-
// Save the rebuilt index
|
|
10826
|
-
await this._saveIndexDataToFile();
|
|
10827
|
-
} catch (error) {
|
|
10828
|
-
if (this.opts.debugMode) {
|
|
10829
|
-
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
|
+
}
|
|
10830
11396
|
}
|
|
10831
|
-
|
|
10832
|
-
|
|
11397
|
+
});
|
|
11398
|
+
return count;
|
|
10833
11399
|
}
|
|
10834
11400
|
|
|
10835
11401
|
/**
|
|
@@ -11022,35 +11588,245 @@ class Database extends events.EventEmitter {
|
|
|
11022
11588
|
}
|
|
11023
11589
|
|
|
11024
11590
|
/**
|
|
11025
|
-
* Check if any records exist
|
|
11026
|
-
*
|
|
11027
|
-
*
|
|
11028
|
-
* @param {string}
|
|
11029
|
-
* @param {string|Array<string>} terms - Single term or array of terms
|
|
11591
|
+
* Check if any records exist matching the given criteria (ultra-fast when using indexed fields)
|
|
11592
|
+
*
|
|
11593
|
+
* @param {string|object} fieldName - Indexed field name (legacy) OR query criteria object (new)
|
|
11594
|
+
* @param {string|Array<string>} terms - Single term or array of terms (when using legacy syntax)
|
|
11030
11595
|
* @param {Object} options - Options: { $all: true/false, caseInsensitive: true/false, excludes: Array<string> }
|
|
11031
11596
|
* @returns {Promise<boolean>} - True if at least one match exists
|
|
11032
|
-
*
|
|
11597
|
+
*
|
|
11033
11598
|
* @example
|
|
11034
|
-
* //
|
|
11035
|
-
* const exists = await db.exists('nameTerms',
|
|
11036
|
-
*
|
|
11599
|
+
* // Legacy syntax - ultra-fast index-only check
|
|
11600
|
+
* const exists = await db.exists('nameTerms', 'tv');
|
|
11601
|
+
* const existsAll = await db.exists('nameTerms', ['tv', 'globo'], { $all: true });
|
|
11602
|
+
*
|
|
11037
11603
|
* @example
|
|
11038
|
-
* //
|
|
11039
|
-
* const exists = await db.exists(
|
|
11604
|
+
* // New syntax - full query criteria support
|
|
11605
|
+
* const exists = await db.exists({ mediaType: 'live', status: 'active' });
|
|
11606
|
+
* const existsOr = await db.exists({ mediaType: ['live', 'vod'] });
|
|
11040
11607
|
*/
|
|
11041
|
-
async exists(
|
|
11608
|
+
async exists(fieldNameOrCriteria, terms, options = {}) {
|
|
11042
11609
|
this._validateInitialization('exists');
|
|
11043
|
-
|
|
11610
|
+
|
|
11611
|
+
// Detect syntax: new criteria object vs legacy field/terms
|
|
11612
|
+
if (typeof fieldNameOrCriteria === 'object' && fieldNameOrCriteria !== null && !Array.isArray(fieldNameOrCriteria)) {
|
|
11613
|
+
// New syntax: exists(criteria)
|
|
11614
|
+
const criteria = fieldNameOrCriteria;
|
|
11615
|
+
return this._existsWithCriteria(criteria);
|
|
11616
|
+
} else if (typeof fieldNameOrCriteria === 'string' || fieldNameOrCriteria === null || Array.isArray(fieldNameOrCriteria)) {
|
|
11617
|
+
// Legacy syntax: exists(fieldName, terms, options)
|
|
11618
|
+
// Also handle invalid inputs (null, array) for backward compatibility
|
|
11619
|
+
const fieldName = fieldNameOrCriteria;
|
|
11620
|
+
return this.indexManager.exists(fieldName, terms, options);
|
|
11621
|
+
} else {
|
|
11622
|
+
// Invalid input type
|
|
11623
|
+
throw new Error('First parameter must be a string (fieldName) or object (criteria)');
|
|
11624
|
+
}
|
|
11625
|
+
}
|
|
11626
|
+
|
|
11627
|
+
/**
|
|
11628
|
+
* Check if any records exist using full query criteria
|
|
11629
|
+
* Uses index intersection when possible for maximum performance
|
|
11630
|
+
* @private
|
|
11631
|
+
* @param {object} criteria - Query criteria object
|
|
11632
|
+
* @returns {Promise<boolean>} - True if at least one match exists
|
|
11633
|
+
*/
|
|
11634
|
+
|
|
11635
|
+
async _existsWithCriteria(criteria) {
|
|
11636
|
+
if (criteria === null || criteria === undefined || typeof criteria !== 'object' || Array.isArray(criteria)) {
|
|
11637
|
+
throw new Error('Criteria must be a non-null object');
|
|
11638
|
+
}
|
|
11639
|
+
|
|
11640
|
+
// Check if criteria is empty (should match all records)
|
|
11641
|
+
const criteriaFields = Object.keys(criteria);
|
|
11642
|
+
if (criteriaFields.length === 0) {
|
|
11643
|
+
// Empty criteria matches all records - check if any exist
|
|
11644
|
+
try {
|
|
11645
|
+
const result = await this.find({}, {
|
|
11646
|
+
limit: 1
|
|
11647
|
+
});
|
|
11648
|
+
return result.length > 0;
|
|
11649
|
+
} catch (error) {
|
|
11650
|
+
return false;
|
|
11651
|
+
}
|
|
11652
|
+
}
|
|
11653
|
+
|
|
11654
|
+
// 🚀 OPTIMIZATION: Try index-only existence check for simple criteria
|
|
11655
|
+
if (this._canUseIndexOnlyForExists(criteria)) {
|
|
11656
|
+
if (this.opts.debugMode) {
|
|
11657
|
+
console.log(`⚡ exists() using INDEX-ONLY optimization for: ${JSON.stringify(criteria)}`);
|
|
11658
|
+
}
|
|
11659
|
+
return this._existsIndexOnly(criteria);
|
|
11660
|
+
}
|
|
11661
|
+
|
|
11662
|
+
// 🎯 FALLBACK: Use the same find() logic for complex criteria or non-indexed fields
|
|
11663
|
+
// This ensures exists() uses identical logic to find() for all criteria processing
|
|
11664
|
+
try {
|
|
11665
|
+
const result = await this.find(criteria, {
|
|
11666
|
+
limit: 1
|
|
11667
|
+
});
|
|
11668
|
+
return result.length > 0;
|
|
11669
|
+
} catch (error) {
|
|
11670
|
+
// If find() fails (e.g., strict mode violations), no records exist
|
|
11671
|
+
return false;
|
|
11672
|
+
}
|
|
11673
|
+
}
|
|
11674
|
+
|
|
11675
|
+
/**
|
|
11676
|
+
* Check if criteria can use index-only existence check
|
|
11677
|
+
* @private
|
|
11678
|
+
* @param {object} criteria - Query criteria
|
|
11679
|
+
* @returns {boolean} - True if can use index-only
|
|
11680
|
+
*/
|
|
11681
|
+
_canUseIndexOnlyForExists(criteria) {
|
|
11682
|
+
// Must have indexes configured
|
|
11683
|
+
if (!this.opts.indexes) return false;
|
|
11684
|
+
|
|
11685
|
+
// All fields in criteria must be indexed
|
|
11686
|
+
const criteriaFields = Object.keys(criteria);
|
|
11687
|
+
const allFieldsIndexed = criteriaFields.every(field => this.opts.indexes[field]);
|
|
11688
|
+
if (!allFieldsIndexed) return false;
|
|
11689
|
+
|
|
11690
|
+
// No complex operators allowed (only simple equality)
|
|
11691
|
+
for (const [field, value] of Object.entries(criteria)) {
|
|
11692
|
+
// Allow simple values (string, number, boolean)
|
|
11693
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
11694
|
+
continue;
|
|
11695
|
+
}
|
|
11696
|
+
|
|
11697
|
+
// Allow arrays (OR logic)
|
|
11698
|
+
if (Array.isArray(value)) {
|
|
11699
|
+
continue;
|
|
11700
|
+
}
|
|
11701
|
+
|
|
11702
|
+
// Reject any object (complex operators)
|
|
11703
|
+
if (typeof value === 'object') {
|
|
11704
|
+
return false;
|
|
11705
|
+
}
|
|
11706
|
+
}
|
|
11707
|
+
return true;
|
|
11708
|
+
}
|
|
11709
|
+
|
|
11710
|
+
/**
|
|
11711
|
+
* Perform index-only existence check (ultra-fast, no disk I/O)
|
|
11712
|
+
* @private
|
|
11713
|
+
* @param {object} criteria - Simple criteria with only indexed fields
|
|
11714
|
+
* @returns {boolean} - True if any records match the criteria
|
|
11715
|
+
*/
|
|
11716
|
+
_existsIndexOnly(criteria) {
|
|
11717
|
+
const criteriaEntries = Object.entries(criteria);
|
|
11718
|
+
|
|
11719
|
+
// For single field criteria, use direct indexManager.exists()
|
|
11720
|
+
if (criteriaEntries.length === 1) {
|
|
11721
|
+
const [field, value] = criteriaEntries[0];
|
|
11722
|
+
return this.indexManager.exists(field, value);
|
|
11723
|
+
}
|
|
11724
|
+
|
|
11725
|
+
// For multiple field criteria, implement index intersection (AND logic)
|
|
11726
|
+
let intersection = null;
|
|
11727
|
+
for (const [field, value] of criteriaEntries) {
|
|
11728
|
+
// Get line numbers for this field/value combination
|
|
11729
|
+
const fieldLines = this._getIndexLinesForFieldValue(field, value);
|
|
11730
|
+
if (intersection === null) {
|
|
11731
|
+
// First field - start with all its lines
|
|
11732
|
+
intersection = new Set(fieldLines);
|
|
11733
|
+
} else {
|
|
11734
|
+
// Intersect with previous results
|
|
11735
|
+
const currentLines = new Set(fieldLines);
|
|
11736
|
+
for (const line of Array.from(intersection)) {
|
|
11737
|
+
if (!currentLines.has(line)) {
|
|
11738
|
+
intersection.delete(line);
|
|
11739
|
+
}
|
|
11740
|
+
}
|
|
11741
|
+
}
|
|
11742
|
+
|
|
11743
|
+
// Early exit if no matches possible
|
|
11744
|
+
if (intersection.size === 0) {
|
|
11745
|
+
return false;
|
|
11746
|
+
}
|
|
11747
|
+
}
|
|
11748
|
+
return intersection.size > 0;
|
|
11749
|
+
}
|
|
11750
|
+
|
|
11751
|
+
/**
|
|
11752
|
+
* Get line numbers for a field/value combination using index-only lookup
|
|
11753
|
+
* @private
|
|
11754
|
+
* @param {string} field - Field name
|
|
11755
|
+
* @param {*} value - Field value (string, number, or array)
|
|
11756
|
+
* @returns {Array<number>} Array of line numbers
|
|
11757
|
+
*/
|
|
11758
|
+
_getIndexLinesForFieldValue(field, value) {
|
|
11759
|
+
// For arrays (OR logic), check each value
|
|
11760
|
+
if (Array.isArray(value)) {
|
|
11761
|
+
const allLines = new Set();
|
|
11762
|
+
for (const singleValue of value) {
|
|
11763
|
+
const lines = this._getSingleFieldLines(field, singleValue);
|
|
11764
|
+
lines.forEach(line => allLines.add(line));
|
|
11765
|
+
}
|
|
11766
|
+
return Array.from(allLines);
|
|
11767
|
+
}
|
|
11768
|
+
|
|
11769
|
+
// For single values
|
|
11770
|
+
return this._getSingleFieldLines(field, value);
|
|
11771
|
+
}
|
|
11772
|
+
|
|
11773
|
+
/**
|
|
11774
|
+
* Get line numbers for a single field/value using index lookup
|
|
11775
|
+
* @private
|
|
11776
|
+
* @param {string} field - Field name
|
|
11777
|
+
* @param {*} value - Single field value
|
|
11778
|
+
* @returns {Array<number>} Array of line numbers
|
|
11779
|
+
*/
|
|
11780
|
+
_getSingleFieldLines(field, value) {
|
|
11781
|
+
// Use indexManager.exists() logic but return line numbers instead of boolean
|
|
11782
|
+
const fieldIndex = this.indexManager?.index?.data?.[field];
|
|
11783
|
+
if (!fieldIndex) return [];
|
|
11784
|
+
const fieldType = this.opts.indexes[field];
|
|
11785
|
+
const isTermMapped = this.termManager && this.termManager.termMappingFields && this.termManager.termMappingFields.includes(field);
|
|
11786
|
+
let searchKey;
|
|
11787
|
+
if (fieldType === 'array:string') {
|
|
11788
|
+
// For array:string fields, match records that contain this value
|
|
11789
|
+
searchKey = isTermMapped ? this.termManager.getTermIdWithoutIncrement(String(value)) : String(value);
|
|
11790
|
+
} else {
|
|
11791
|
+
// For simple fields, exact match
|
|
11792
|
+
searchKey = isTermMapped ? this.termManager.getTermIdWithoutIncrement(String(value)) : String(value);
|
|
11793
|
+
}
|
|
11794
|
+
if (searchKey === null || searchKey === undefined) {
|
|
11795
|
+
return [];
|
|
11796
|
+
}
|
|
11797
|
+
const termData = fieldIndex[searchKey];
|
|
11798
|
+
if (!termData) return [];
|
|
11799
|
+
|
|
11800
|
+
// Extract all line numbers from termData (similar to indexManager._getAllLineNumbers)
|
|
11801
|
+
const lines = new Set();
|
|
11802
|
+
|
|
11803
|
+
// Check Set (most common case)
|
|
11804
|
+
if (termData.set && termData.set.size > 0) {
|
|
11805
|
+
for (const line of termData.set) {
|
|
11806
|
+
lines.add(line);
|
|
11807
|
+
}
|
|
11808
|
+
}
|
|
11809
|
+
|
|
11810
|
+
// Check ranges
|
|
11811
|
+
if (termData.ranges && termData.ranges.length > 0) {
|
|
11812
|
+
for (const range of termData.ranges) {
|
|
11813
|
+
for (let line = range.start; line <= range.end; line++) {
|
|
11814
|
+
lines.add(line);
|
|
11815
|
+
}
|
|
11816
|
+
}
|
|
11817
|
+
}
|
|
11818
|
+
return Array.from(lines);
|
|
11044
11819
|
}
|
|
11045
11820
|
|
|
11046
11821
|
/**
|
|
11047
11822
|
* Calculate coverage for grouped include/exclude term sets
|
|
11048
11823
|
* @param {string} fieldName - Name of the indexed field
|
|
11049
11824
|
* @param {Array<object>} groups - Array of { terms, excludes } objects
|
|
11825
|
+
* @param {object} filterCriteria - Optional filter criteria
|
|
11050
11826
|
* @param {object} options - Optional settings
|
|
11051
11827
|
* @returns {Promise<number>} Coverage percentage between 0 and 100
|
|
11052
11828
|
*/
|
|
11053
|
-
async coverage(fieldName, groups, options = {}) {
|
|
11829
|
+
async coverage(fieldName, groups, filterCriteria = null, options = {}) {
|
|
11054
11830
|
this._validateInitialization('coverage');
|
|
11055
11831
|
if (typeof fieldName !== 'string' || !fieldName.trim()) {
|
|
11056
11832
|
throw new Error('fieldName must be a non-empty string');
|
|
@@ -11073,6 +11849,31 @@ class Database extends events.EventEmitter {
|
|
|
11073
11849
|
if (!fieldIndex) {
|
|
11074
11850
|
return 0;
|
|
11075
11851
|
}
|
|
11852
|
+
|
|
11853
|
+
// Validate filter criteria
|
|
11854
|
+
let filteredLines = null;
|
|
11855
|
+
if (filterCriteria && typeof filterCriteria === 'object') {
|
|
11856
|
+
if (Array.isArray(filterCriteria)) {
|
|
11857
|
+
throw new Error('filterCriteria must be an object, not an array');
|
|
11858
|
+
}
|
|
11859
|
+
|
|
11860
|
+
// Get filtered records using QueryManager for consistency
|
|
11861
|
+
try {
|
|
11862
|
+
const filteredRecords = await this.queryManager.find(filterCriteria, {
|
|
11863
|
+
limit: null,
|
|
11864
|
+
// Get all matching records for coverage calculation
|
|
11865
|
+
indexedQueryMode: this.opts.indexedQueryMode,
|
|
11866
|
+
allowNonIndexed: true
|
|
11867
|
+
});
|
|
11868
|
+
filteredLines = new Set(filteredRecords.map(record => record._));
|
|
11869
|
+
if (filteredLines.size === 0) {
|
|
11870
|
+
return 0; // No records match the filter
|
|
11871
|
+
}
|
|
11872
|
+
} catch (error) {
|
|
11873
|
+
// If filtering fails, return 0 (no coverage possible)
|
|
11874
|
+
return 0;
|
|
11875
|
+
}
|
|
11876
|
+
}
|
|
11076
11877
|
const isTermMapped = this.termManager && this.termManager.termMappingFields && this.termManager.termMappingFields.includes(fieldName);
|
|
11077
11878
|
const normalizeTerm = term => {
|
|
11078
11879
|
if (term === undefined || term === null) {
|
|
@@ -11120,10 +11921,20 @@ class Database extends events.EventEmitter {
|
|
|
11120
11921
|
groupMatched = false;
|
|
11121
11922
|
break;
|
|
11122
11923
|
}
|
|
11924
|
+
|
|
11925
|
+
// Apply filter if specified
|
|
11926
|
+
let validLineNumbers = lineNumbers;
|
|
11927
|
+
if (filteredLines) {
|
|
11928
|
+
validLineNumbers = lineNumbers.filter(line => filteredLines.has(line));
|
|
11929
|
+
if (validLineNumbers.length === 0) {
|
|
11930
|
+
groupMatched = false;
|
|
11931
|
+
break;
|
|
11932
|
+
}
|
|
11933
|
+
}
|
|
11123
11934
|
if (candidateLines === null) {
|
|
11124
|
-
candidateLines = new Set(
|
|
11935
|
+
candidateLines = new Set(validLineNumbers);
|
|
11125
11936
|
} else {
|
|
11126
|
-
const termSet = new Set(
|
|
11937
|
+
const termSet = new Set(validLineNumbers);
|
|
11127
11938
|
for (const line of Array.from(candidateLines)) {
|
|
11128
11939
|
if (!termSet.has(line)) {
|
|
11129
11940
|
candidateLines.delete(line);
|
|
@@ -11151,7 +11962,13 @@ class Database extends events.EventEmitter {
|
|
|
11151
11962
|
if (!excludeLines || excludeLines.length === 0) {
|
|
11152
11963
|
continue;
|
|
11153
11964
|
}
|
|
11154
|
-
|
|
11965
|
+
|
|
11966
|
+
// Apply filter to exclude lines if specified
|
|
11967
|
+
let validExcludeLines = excludeLines;
|
|
11968
|
+
if (filteredLines) {
|
|
11969
|
+
validExcludeLines = excludeLines.filter(line => filteredLines.has(line));
|
|
11970
|
+
}
|
|
11971
|
+
for (const line of validExcludeLines) {
|
|
11155
11972
|
if (!candidateLines.size) {
|
|
11156
11973
|
break;
|
|
11157
11974
|
}
|
|
@@ -11359,56 +12176,11 @@ class Database extends events.EventEmitter {
|
|
|
11359
12176
|
}
|
|
11360
12177
|
}
|
|
11361
12178
|
const groupedRanges = await this.fileHandler.groupedRanges(ranges);
|
|
11362
|
-
const fs = await import('fs');
|
|
11363
12179
|
const fd = await fs.promises.open(this.fileHandler.file, 'r');
|
|
11364
12180
|
try {
|
|
11365
12181
|
for (const groupedRange of groupedRanges) {
|
|
11366
|
-
|
|
11367
|
-
|
|
11368
|
-
var _iteratorError3;
|
|
11369
|
-
try {
|
|
11370
|
-
for (var _iterator3 = _asyncIterator(this.fileHandler.readGroupedRange(groupedRange, fd)), _step3; _iteratorAbruptCompletion3 = !(_step3 = await _iterator3.next()).done; _iteratorAbruptCompletion3 = false) {
|
|
11371
|
-
const row = _step3.value;
|
|
11372
|
-
{
|
|
11373
|
-
try {
|
|
11374
|
-
const record = this.serializer.deserialize(row.line);
|
|
11375
|
-
|
|
11376
|
-
// Get line number from the row, fallback to start offset mapping
|
|
11377
|
-
let lineNumber = row._ !== null && row._ !== undefined ? row._ : startToLineNumber.get(row.start) ?? 0;
|
|
11378
|
-
|
|
11379
|
-
// Restore term IDs to terms
|
|
11380
|
-
const recordWithTerms = this.restoreTermIdsAfterDeserialization(record);
|
|
11381
|
-
|
|
11382
|
-
// Add line number
|
|
11383
|
-
recordWithTerms._ = lineNumber;
|
|
11384
|
-
|
|
11385
|
-
// Add score if includeScore is true (default is true)
|
|
11386
|
-
if (opts.includeScore !== false) {
|
|
11387
|
-
recordWithTerms.score = scoresByLineNumber.get(lineNumber) || 0;
|
|
11388
|
-
}
|
|
11389
|
-
results.push(recordWithTerms);
|
|
11390
|
-
} catch (error) {
|
|
11391
|
-
// Skip invalid lines
|
|
11392
|
-
if (this.opts.debugMode) {
|
|
11393
|
-
console.error('Error deserializing record in score():', error);
|
|
11394
|
-
}
|
|
11395
|
-
}
|
|
11396
|
-
}
|
|
11397
|
-
}
|
|
11398
|
-
} catch (err) {
|
|
11399
|
-
_didIteratorError3 = true;
|
|
11400
|
-
_iteratorError3 = err;
|
|
11401
|
-
} finally {
|
|
11402
|
-
try {
|
|
11403
|
-
if (_iteratorAbruptCompletion3 && _iterator3.return != null) {
|
|
11404
|
-
await _iterator3.return();
|
|
11405
|
-
}
|
|
11406
|
-
} finally {
|
|
11407
|
-
if (_didIteratorError3) {
|
|
11408
|
-
throw _iteratorError3;
|
|
11409
|
-
}
|
|
11410
|
-
}
|
|
11411
|
-
}
|
|
12182
|
+
const rangeResults = await this._readGroupedRangeWithRetry(groupedRange, fd, startToLineNumber, scoresByLineNumber, opts);
|
|
12183
|
+
results.push(...rangeResults);
|
|
11412
12184
|
}
|
|
11413
12185
|
} finally {
|
|
11414
12186
|
await fd.close();
|
|
@@ -11448,6 +12220,300 @@ class Database extends events.EventEmitter {
|
|
|
11448
12220
|
return results;
|
|
11449
12221
|
}
|
|
11450
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
|
+
|
|
11451
12517
|
/**
|
|
11452
12518
|
* Wait for all pending operations to complete
|
|
11453
12519
|
*/
|
|
@@ -11757,7 +12823,6 @@ class Database extends events.EventEmitter {
|
|
|
11757
12823
|
|
|
11758
12824
|
// Method 1: Try to read the entire file and filter
|
|
11759
12825
|
if (this.fileHandler.exists()) {
|
|
11760
|
-
const fs = await import('fs');
|
|
11761
12826
|
const fileContent = await fs.promises.readFile(this.normalizedFile, 'utf8');
|
|
11762
12827
|
const lines = fileContent.split('\n').filter(line => line.trim());
|
|
11763
12828
|
for (let i = 0; i < lines.length && i < this.offsets.length; i++) {
|
|
@@ -12457,10 +13522,9 @@ class Database extends events.EventEmitter {
|
|
|
12457
13522
|
return;
|
|
12458
13523
|
}
|
|
12459
13524
|
_this._offsetRecoveryInProgress = true;
|
|
12460
|
-
const fsModule = _this._fsModule || (_this._fsModule = yield _awaitAsyncGenerator(import('fs')));
|
|
12461
13525
|
let fd;
|
|
12462
13526
|
try {
|
|
12463
|
-
fd = yield _awaitAsyncGenerator(
|
|
13527
|
+
fd = yield _awaitAsyncGenerator(fs.promises.open(_this.fileHandler.file, 'r'));
|
|
12464
13528
|
} catch (error) {
|
|
12465
13529
|
_this._offsetRecoveryInProgress = false;
|
|
12466
13530
|
if (_this.opts.debugMode) {
|
|
@@ -12763,16 +13827,15 @@ class Database extends events.EventEmitter {
|
|
|
12763
13827
|
// OPTIMIZATION: Use ranges instead of reading entire file
|
|
12764
13828
|
const ranges = _this2.getRanges(map);
|
|
12765
13829
|
const groupedRanges = yield _awaitAsyncGenerator(_this2.fileHandler.groupedRanges(ranges));
|
|
12766
|
-
const fs = yield _awaitAsyncGenerator(import('fs'));
|
|
12767
13830
|
const fd = yield _awaitAsyncGenerator(fs.promises.open(_this2.fileHandler.file, 'r'));
|
|
12768
13831
|
try {
|
|
12769
13832
|
for (const groupedRange of groupedRanges) {
|
|
12770
|
-
var
|
|
12771
|
-
var
|
|
12772
|
-
var
|
|
13833
|
+
var _iteratorAbruptCompletion7 = false;
|
|
13834
|
+
var _didIteratorError7 = false;
|
|
13835
|
+
var _iteratorError7;
|
|
12773
13836
|
try {
|
|
12774
|
-
for (var
|
|
12775
|
-
const row =
|
|
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;
|
|
12776
13839
|
{
|
|
12777
13840
|
if (options.limit && count >= options.limit) {
|
|
12778
13841
|
break;
|
|
@@ -12846,28 +13909,28 @@ class Database extends events.EventEmitter {
|
|
|
12846
13909
|
}
|
|
12847
13910
|
}
|
|
12848
13911
|
if (!_this2._offsetRecoveryInProgress) {
|
|
12849
|
-
var
|
|
12850
|
-
var
|
|
12851
|
-
var
|
|
13912
|
+
var _iteratorAbruptCompletion8 = false;
|
|
13913
|
+
var _didIteratorError8 = false;
|
|
13914
|
+
var _iteratorError8;
|
|
12852
13915
|
try {
|
|
12853
|
-
for (var
|
|
12854
|
-
const recoveredEntry =
|
|
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;
|
|
12855
13918
|
{
|
|
12856
13919
|
yield recoveredEntry;
|
|
12857
13920
|
count++;
|
|
12858
13921
|
}
|
|
12859
13922
|
}
|
|
12860
13923
|
} catch (err) {
|
|
12861
|
-
|
|
12862
|
-
|
|
13924
|
+
_didIteratorError8 = true;
|
|
13925
|
+
_iteratorError8 = err;
|
|
12863
13926
|
} finally {
|
|
12864
13927
|
try {
|
|
12865
|
-
if (
|
|
12866
|
-
yield _awaitAsyncGenerator(
|
|
13928
|
+
if (_iteratorAbruptCompletion8 && _iterator8.return != null) {
|
|
13929
|
+
yield _awaitAsyncGenerator(_iterator8.return());
|
|
12867
13930
|
}
|
|
12868
13931
|
} finally {
|
|
12869
|
-
if (
|
|
12870
|
-
throw
|
|
13932
|
+
if (_didIteratorError8) {
|
|
13933
|
+
throw _iteratorError8;
|
|
12871
13934
|
}
|
|
12872
13935
|
}
|
|
12873
13936
|
}
|
|
@@ -12879,16 +13942,16 @@ class Database extends events.EventEmitter {
|
|
|
12879
13942
|
}
|
|
12880
13943
|
}
|
|
12881
13944
|
} catch (err) {
|
|
12882
|
-
|
|
12883
|
-
|
|
13945
|
+
_didIteratorError7 = true;
|
|
13946
|
+
_iteratorError7 = err;
|
|
12884
13947
|
} finally {
|
|
12885
13948
|
try {
|
|
12886
|
-
if (
|
|
12887
|
-
yield _awaitAsyncGenerator(
|
|
13949
|
+
if (_iteratorAbruptCompletion7 && _iterator7.return != null) {
|
|
13950
|
+
yield _awaitAsyncGenerator(_iterator7.return());
|
|
12888
13951
|
}
|
|
12889
13952
|
} finally {
|
|
12890
|
-
if (
|
|
12891
|
-
throw
|
|
13953
|
+
if (_didIteratorError7) {
|
|
13954
|
+
throw _iteratorError7;
|
|
12892
13955
|
}
|
|
12893
13956
|
}
|
|
12894
13957
|
}
|
|
@@ -12954,12 +14017,12 @@ class Database extends events.EventEmitter {
|
|
|
12954
14017
|
if (options.limit && count >= options.limit) {
|
|
12955
14018
|
break;
|
|
12956
14019
|
}
|
|
12957
|
-
var
|
|
12958
|
-
var
|
|
12959
|
-
var
|
|
14020
|
+
var _iteratorAbruptCompletion9 = false;
|
|
14021
|
+
var _didIteratorError9 = false;
|
|
14022
|
+
var _iteratorError9;
|
|
12960
14023
|
try {
|
|
12961
|
-
for (var
|
|
12962
|
-
const row =
|
|
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;
|
|
12963
14026
|
{
|
|
12964
14027
|
if (options.limit && count >= options.limit) {
|
|
12965
14028
|
break;
|
|
@@ -13040,28 +14103,28 @@ class Database extends events.EventEmitter {
|
|
|
13040
14103
|
}
|
|
13041
14104
|
}
|
|
13042
14105
|
if (!_this2._offsetRecoveryInProgress) {
|
|
13043
|
-
var
|
|
13044
|
-
var
|
|
13045
|
-
var
|
|
14106
|
+
var _iteratorAbruptCompletion0 = false;
|
|
14107
|
+
var _didIteratorError0 = false;
|
|
14108
|
+
var _iteratorError0;
|
|
13046
14109
|
try {
|
|
13047
|
-
for (var
|
|
13048
|
-
const recoveredEntry =
|
|
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;
|
|
13049
14112
|
{
|
|
13050
14113
|
yield recoveredEntry;
|
|
13051
14114
|
count++;
|
|
13052
14115
|
}
|
|
13053
14116
|
}
|
|
13054
14117
|
} catch (err) {
|
|
13055
|
-
|
|
13056
|
-
|
|
14118
|
+
_didIteratorError0 = true;
|
|
14119
|
+
_iteratorError0 = err;
|
|
13057
14120
|
} finally {
|
|
13058
14121
|
try {
|
|
13059
|
-
if (
|
|
13060
|
-
yield _awaitAsyncGenerator(
|
|
14122
|
+
if (_iteratorAbruptCompletion0 && _iterator0.return != null) {
|
|
14123
|
+
yield _awaitAsyncGenerator(_iterator0.return());
|
|
13061
14124
|
}
|
|
13062
14125
|
} finally {
|
|
13063
|
-
if (
|
|
13064
|
-
throw
|
|
14126
|
+
if (_didIteratorError0) {
|
|
14127
|
+
throw _iteratorError0;
|
|
13065
14128
|
}
|
|
13066
14129
|
}
|
|
13067
14130
|
}
|
|
@@ -13073,16 +14136,16 @@ class Database extends events.EventEmitter {
|
|
|
13073
14136
|
}
|
|
13074
14137
|
}
|
|
13075
14138
|
} catch (err) {
|
|
13076
|
-
|
|
13077
|
-
|
|
14139
|
+
_didIteratorError9 = true;
|
|
14140
|
+
_iteratorError9 = err;
|
|
13078
14141
|
} finally {
|
|
13079
14142
|
try {
|
|
13080
|
-
if (
|
|
13081
|
-
yield _awaitAsyncGenerator(
|
|
14143
|
+
if (_iteratorAbruptCompletion9 && _iterator9.return != null) {
|
|
14144
|
+
yield _awaitAsyncGenerator(_iterator9.return());
|
|
13082
14145
|
}
|
|
13083
14146
|
} finally {
|
|
13084
|
-
if (
|
|
13085
|
-
throw
|
|
14147
|
+
if (_didIteratorError9) {
|
|
14148
|
+
throw _iteratorError9;
|
|
13086
14149
|
}
|
|
13087
14150
|
}
|
|
13088
14151
|
}
|
|
@@ -13137,12 +14200,12 @@ class Database extends events.EventEmitter {
|
|
|
13137
14200
|
|
|
13138
14201
|
try {
|
|
13139
14202
|
// Always use walk() now that the bug is fixed - it works for both small and large datasets
|
|
13140
|
-
var
|
|
13141
|
-
var
|
|
13142
|
-
var
|
|
14203
|
+
var _iteratorAbruptCompletion1 = false;
|
|
14204
|
+
var _didIteratorError1 = false;
|
|
14205
|
+
var _iteratorError1;
|
|
13143
14206
|
try {
|
|
13144
|
-
for (var
|
|
13145
|
-
const entry =
|
|
14207
|
+
for (var _iterator1 = _asyncIterator(_this3.walk(criteria, options)), _step1; _iteratorAbruptCompletion1 = !(_step1 = yield _awaitAsyncGenerator(_iterator1.next())).done; _iteratorAbruptCompletion1 = false) {
|
|
14208
|
+
const entry = _step1.value;
|
|
13146
14209
|
{
|
|
13147
14210
|
processedCount++;
|
|
13148
14211
|
|
|
@@ -13202,16 +14265,16 @@ class Database extends events.EventEmitter {
|
|
|
13202
14265
|
|
|
13203
14266
|
// Process remaining records in buffers
|
|
13204
14267
|
} catch (err) {
|
|
13205
|
-
|
|
13206
|
-
|
|
14268
|
+
_didIteratorError1 = true;
|
|
14269
|
+
_iteratorError1 = err;
|
|
13207
14270
|
} finally {
|
|
13208
14271
|
try {
|
|
13209
|
-
if (
|
|
13210
|
-
yield _awaitAsyncGenerator(
|
|
14272
|
+
if (_iteratorAbruptCompletion1 && _iterator1.return != null) {
|
|
14273
|
+
yield _awaitAsyncGenerator(_iterator1.return());
|
|
13211
14274
|
}
|
|
13212
14275
|
} finally {
|
|
13213
|
-
if (
|
|
13214
|
-
throw
|
|
14276
|
+
if (_didIteratorError1) {
|
|
14277
|
+
throw _iteratorError1;
|
|
13215
14278
|
}
|
|
13216
14279
|
}
|
|
13217
14280
|
}
|
|
@@ -13404,7 +14467,6 @@ class Database extends events.EventEmitter {
|
|
|
13404
14467
|
// If the .idx.jdb file exists and has data, and we're trying to save empty index,
|
|
13405
14468
|
// skip the save to prevent corruption
|
|
13406
14469
|
if (isEmpty && !this.offsets?.length) {
|
|
13407
|
-
const fs = await import('fs');
|
|
13408
14470
|
if (fs.existsSync(idxPath)) {
|
|
13409
14471
|
try {
|
|
13410
14472
|
const existingData = JSON.parse(await fs.promises.readFile(idxPath, 'utf8'));
|