jexidb 2.1.7 → 2.1.9
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 +319 -1
- package/package.json +1 -1
- package/src/Database.mjs +50 -0
- package/src/FileHandler.mjs +38 -1
- package/src/managers/IndexManager.mjs +266 -0
package/dist/Database.cjs
CHANGED
|
@@ -2154,6 +2154,247 @@ class IndexManager {
|
|
|
2154
2154
|
return candidateLines.size > 0;
|
|
2155
2155
|
}
|
|
2156
2156
|
|
|
2157
|
+
/**
|
|
2158
|
+
* Evaluate many index-only existence checks while sharing the same field data.
|
|
2159
|
+
* Returns a map keyed by the criteria id so callers can reuse a single scan per field.
|
|
2160
|
+
*
|
|
2161
|
+
* @param {string} fieldName - Indexed field name
|
|
2162
|
+
* @param {Array<Object>} criteriaArray - Array of { id, terms, options }
|
|
2163
|
+
* @param {Object} opts - { limit, allowPartial } (limit only applies when allowPartial is true)
|
|
2164
|
+
* @returns {Object<string, boolean>} - Map of criteria id to boolean existence
|
|
2165
|
+
*/
|
|
2166
|
+
multiExists(fieldName, criteriaArray, opts = {}) {
|
|
2167
|
+
const results = {};
|
|
2168
|
+
if (!Array.isArray(criteriaArray) || criteriaArray.length === 0) {
|
|
2169
|
+
return results;
|
|
2170
|
+
}
|
|
2171
|
+
const prepared = [];
|
|
2172
|
+
for (let i = 0; i < criteriaArray.length; i++) {
|
|
2173
|
+
const entry = criteriaArray[i] || {};
|
|
2174
|
+
const key = entry.id !== undefined && entry.id !== null ? String(entry.id) : `criteria-${i}`;
|
|
2175
|
+
results[key] = false;
|
|
2176
|
+
prepared.push({
|
|
2177
|
+
id: key,
|
|
2178
|
+
terms: entry.terms,
|
|
2179
|
+
options: entry.options || {}
|
|
2180
|
+
});
|
|
2181
|
+
}
|
|
2182
|
+
if (!fieldName || typeof fieldName !== 'string') {
|
|
2183
|
+
return results;
|
|
2184
|
+
}
|
|
2185
|
+
const fieldIndex = this.index?.data?.[fieldName];
|
|
2186
|
+
if (!fieldIndex || typeof fieldIndex !== 'object') {
|
|
2187
|
+
return results;
|
|
2188
|
+
}
|
|
2189
|
+
const termManager = this.database?.termManager;
|
|
2190
|
+
const isTermMappingField = Boolean(termManager && termManager.termMappingFields && termManager.termMappingFields.includes(fieldName));
|
|
2191
|
+
const normalizedLimit = Number.isFinite(opts.limit) && opts.limit > 0 ? Math.floor(opts.limit) : Infinity;
|
|
2192
|
+
const allowPartial = Boolean(opts.allowPartial);
|
|
2193
|
+
const positiveTarget = allowPartial ? Math.max(1, normalizedLimit) : Infinity;
|
|
2194
|
+
const termCache = new Map();
|
|
2195
|
+
let caseInsensitiveLookup = null;
|
|
2196
|
+
let termMappingLookup = null;
|
|
2197
|
+
const normalizeValues = value => {
|
|
2198
|
+
if (value === null || value === undefined) {
|
|
2199
|
+
return [];
|
|
2200
|
+
}
|
|
2201
|
+
if (Array.isArray(value)) {
|
|
2202
|
+
return value.slice();
|
|
2203
|
+
}
|
|
2204
|
+
return [value];
|
|
2205
|
+
};
|
|
2206
|
+
const buildCaseInsensitiveLookup = () => {
|
|
2207
|
+
const map = new Map();
|
|
2208
|
+
for (const key in fieldIndex) {
|
|
2209
|
+
const lowerKey = key.toLowerCase();
|
|
2210
|
+
if (!map.has(lowerKey)) {
|
|
2211
|
+
map.set(lowerKey, key);
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
return map;
|
|
2215
|
+
};
|
|
2216
|
+
const buildTermMappingLookup = () => {
|
|
2217
|
+
const map = new Map();
|
|
2218
|
+
const termToId = termManager?.termToId;
|
|
2219
|
+
if (termToId) {
|
|
2220
|
+
for (const [termStr, id] of termToId.entries()) {
|
|
2221
|
+
const lower = termStr.toLowerCase();
|
|
2222
|
+
if (!map.has(lower)) {
|
|
2223
|
+
map.set(lower, String(id));
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
return map;
|
|
2228
|
+
};
|
|
2229
|
+
const resolveTermKey = (value, caseInsensitive) => {
|
|
2230
|
+
if (value === null || value === undefined) {
|
|
2231
|
+
return null;
|
|
2232
|
+
}
|
|
2233
|
+
if (isTermMappingField) {
|
|
2234
|
+
if (typeof value === 'string') {
|
|
2235
|
+
if (caseInsensitive) {
|
|
2236
|
+
if (!termMappingLookup) {
|
|
2237
|
+
termMappingLookup = buildTermMappingLookup();
|
|
2238
|
+
}
|
|
2239
|
+
return termMappingLookup.get(value.toLowerCase()) ?? null;
|
|
2240
|
+
}
|
|
2241
|
+
const termId = termManager?.getTermIdWithoutIncrement(String(value));
|
|
2242
|
+
if (termId === undefined || termId === null) {
|
|
2243
|
+
return null;
|
|
2244
|
+
}
|
|
2245
|
+
return String(termId);
|
|
2246
|
+
}
|
|
2247
|
+
return String(value);
|
|
2248
|
+
}
|
|
2249
|
+
if (caseInsensitive && typeof value === 'string') {
|
|
2250
|
+
if (!caseInsensitiveLookup) {
|
|
2251
|
+
caseInsensitiveLookup = buildCaseInsensitiveLookup();
|
|
2252
|
+
}
|
|
2253
|
+
return caseInsensitiveLookup.get(value.toLowerCase()) ?? null;
|
|
2254
|
+
}
|
|
2255
|
+
return String(value);
|
|
2256
|
+
};
|
|
2257
|
+
const ensureTermEntry = termKey => {
|
|
2258
|
+
if (!termCache.has(termKey)) {
|
|
2259
|
+
const data = fieldIndex[termKey];
|
|
2260
|
+
const entry = {
|
|
2261
|
+
data,
|
|
2262
|
+
hasData: Boolean(data && (data.set && data.set.size > 0 || data.ranges && data.ranges.length > 0)),
|
|
2263
|
+
lineArray: null
|
|
2264
|
+
};
|
|
2265
|
+
termCache.set(termKey, entry);
|
|
2266
|
+
}
|
|
2267
|
+
return termCache.get(termKey);
|
|
2268
|
+
};
|
|
2269
|
+
const getLineArray = entry => {
|
|
2270
|
+
if (!entry.hasData || !entry.data) return [];
|
|
2271
|
+
if (!entry.lineArray) {
|
|
2272
|
+
entry.lineArray = this._getAllLineNumbers(entry.data);
|
|
2273
|
+
}
|
|
2274
|
+
return entry.lineArray;
|
|
2275
|
+
};
|
|
2276
|
+
const applyExcludes = (lineSet, excludeKeys) => {
|
|
2277
|
+
if (excludeKeys.length === 0) {
|
|
2278
|
+
return lineSet.size > 0;
|
|
2279
|
+
}
|
|
2280
|
+
for (const excludeKey of excludeKeys) {
|
|
2281
|
+
const excludeEntry = ensureTermEntry(excludeKey);
|
|
2282
|
+
if (!excludeEntry.hasData) continue;
|
|
2283
|
+
const excludeLines = getLineArray(excludeEntry);
|
|
2284
|
+
for (const line of excludeLines) {
|
|
2285
|
+
lineSet.delete(line);
|
|
2286
|
+
}
|
|
2287
|
+
if (lineSet.size === 0) {
|
|
2288
|
+
return false;
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
return lineSet.size > 0;
|
|
2292
|
+
};
|
|
2293
|
+
let successes = 0;
|
|
2294
|
+
for (const criterion of prepared) {
|
|
2295
|
+
if (successes >= positiveTarget) {
|
|
2296
|
+
break;
|
|
2297
|
+
}
|
|
2298
|
+
const {
|
|
2299
|
+
id,
|
|
2300
|
+
terms,
|
|
2301
|
+
options
|
|
2302
|
+
} = criterion;
|
|
2303
|
+
const {
|
|
2304
|
+
$all = false,
|
|
2305
|
+
caseInsensitive = false,
|
|
2306
|
+
excludes = []
|
|
2307
|
+
} = options;
|
|
2308
|
+
const normalizedTerms = normalizeValues(terms);
|
|
2309
|
+
if (normalizedTerms.length === 0) {
|
|
2310
|
+
continue;
|
|
2311
|
+
}
|
|
2312
|
+
const normalizedExcludes = normalizeValues(excludes);
|
|
2313
|
+
const resolvedKeys = [];
|
|
2314
|
+
const seenKeys = new Set();
|
|
2315
|
+
let missingRequiredTerm = false;
|
|
2316
|
+
for (const term of normalizedTerms) {
|
|
2317
|
+
const termKey = resolveTermKey(term, caseInsensitive);
|
|
2318
|
+
if (termKey === null) {
|
|
2319
|
+
if ($all) {
|
|
2320
|
+
missingRequiredTerm = true;
|
|
2321
|
+
break;
|
|
2322
|
+
}
|
|
2323
|
+
continue;
|
|
2324
|
+
}
|
|
2325
|
+
if (!seenKeys.has(termKey)) {
|
|
2326
|
+
seenKeys.add(termKey);
|
|
2327
|
+
resolvedKeys.push(termKey);
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
if ($all && missingRequiredTerm) {
|
|
2331
|
+
continue;
|
|
2332
|
+
}
|
|
2333
|
+
if (resolvedKeys.length === 0) {
|
|
2334
|
+
continue;
|
|
2335
|
+
}
|
|
2336
|
+
const excludeKeySet = [];
|
|
2337
|
+
const seenExcludeKeys = new Set();
|
|
2338
|
+
for (const excludeTerm of normalizedExcludes) {
|
|
2339
|
+
const excludeKey = resolveTermKey(excludeTerm, caseInsensitive);
|
|
2340
|
+
if (excludeKey && !seenExcludeKeys.has(excludeKey)) {
|
|
2341
|
+
seenExcludeKeys.add(excludeKey);
|
|
2342
|
+
excludeKeySet.push(excludeKey);
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
let match = false;
|
|
2346
|
+
if ($all) {
|
|
2347
|
+
let intersection = null;
|
|
2348
|
+
let failed = false;
|
|
2349
|
+
for (const termKey of resolvedKeys) {
|
|
2350
|
+
const entry = ensureTermEntry(termKey);
|
|
2351
|
+
if (!entry.hasData) {
|
|
2352
|
+
failed = true;
|
|
2353
|
+
break;
|
|
2354
|
+
}
|
|
2355
|
+
const lines = getLineArray(entry);
|
|
2356
|
+
if (lines.length === 0) {
|
|
2357
|
+
failed = true;
|
|
2358
|
+
break;
|
|
2359
|
+
}
|
|
2360
|
+
const currentSet = new Set(lines);
|
|
2361
|
+
if (intersection === null) {
|
|
2362
|
+
intersection = currentSet;
|
|
2363
|
+
} else {
|
|
2364
|
+
intersection = new Set([...intersection].filter(value => currentSet.has(value)));
|
|
2365
|
+
if (intersection.size === 0) {
|
|
2366
|
+
failed = true;
|
|
2367
|
+
break;
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
if (!failed && intersection && intersection.size > 0) {
|
|
2372
|
+
match = applyExcludes(intersection, excludeKeySet);
|
|
2373
|
+
}
|
|
2374
|
+
} else if (excludeKeySet.length === 0) {
|
|
2375
|
+
match = resolvedKeys.some(termKey => ensureTermEntry(termKey).hasData);
|
|
2376
|
+
} else {
|
|
2377
|
+
const candidates = new Set();
|
|
2378
|
+
for (const termKey of resolvedKeys) {
|
|
2379
|
+
const entry = ensureTermEntry(termKey);
|
|
2380
|
+
if (!entry.hasData) continue;
|
|
2381
|
+
const lines = getLineArray(entry);
|
|
2382
|
+
for (const line of lines) {
|
|
2383
|
+
candidates.add(line);
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
if (candidates.size > 0) {
|
|
2387
|
+
match = applyExcludes(candidates, excludeKeySet);
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
results[id] = Boolean(match);
|
|
2391
|
+
if (match) {
|
|
2392
|
+
successes += 1;
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
return results;
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2157
2398
|
// Ultra-fast load with minimal conversions
|
|
2158
2399
|
load(index) {
|
|
2159
2400
|
// CRITICAL FIX: Check if index is already loaded by looking for actual data, not just empty field structures
|
|
@@ -4126,6 +4367,37 @@ class FileHandler {
|
|
|
4126
4367
|
// Global I/O limiter to prevent file descriptor exhaustion in concurrent operations
|
|
4127
4368
|
this.readLimiter = pLimit(opts.maxConcurrentReads || 4);
|
|
4128
4369
|
}
|
|
4370
|
+
_getIoTimeoutMs(override) {
|
|
4371
|
+
if (typeof override === 'number') return override;
|
|
4372
|
+
if (typeof this.opts.ioTimeoutMs === 'number') return this.opts.ioTimeoutMs;
|
|
4373
|
+
return 0;
|
|
4374
|
+
}
|
|
4375
|
+
async _withIoTimeout(fn, timeoutMs, onTimeout) {
|
|
4376
|
+
if (!timeoutMs || timeoutMs <= 0) {
|
|
4377
|
+
return fn();
|
|
4378
|
+
}
|
|
4379
|
+
let timeoutId;
|
|
4380
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
4381
|
+
timeoutId = setTimeout(() => {
|
|
4382
|
+
if (onTimeout) {
|
|
4383
|
+
try {
|
|
4384
|
+
onTimeout();
|
|
4385
|
+
} catch {}
|
|
4386
|
+
}
|
|
4387
|
+
const err = new Error(`I/O timeout after ${timeoutMs}ms`);
|
|
4388
|
+
err.code = 'ETIMEDOUT';
|
|
4389
|
+
reject(err);
|
|
4390
|
+
}, timeoutMs);
|
|
4391
|
+
});
|
|
4392
|
+
try {
|
|
4393
|
+
return await Promise.race([fn(), timeoutPromise]);
|
|
4394
|
+
} finally {
|
|
4395
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
async _readWithTimeout(fd, buffer, offset, length, position, timeoutMs) {
|
|
4399
|
+
return this._withIoTimeout(() => fd.read(buffer, offset, length, position), timeoutMs, () => fd.close().catch(() => {}));
|
|
4400
|
+
}
|
|
4129
4401
|
async truncate(offset) {
|
|
4130
4402
|
try {
|
|
4131
4403
|
await fs.promises.access(this.file, fs.constants.F_OK);
|
|
@@ -4230,6 +4502,7 @@ class FileHandler {
|
|
|
4230
4502
|
if (!(await this.exists())) {
|
|
4231
4503
|
return Buffer.alloc(0); // Return empty buffer if file doesn't exist
|
|
4232
4504
|
}
|
|
4505
|
+
const timeoutMs = this._getIoTimeoutMs();
|
|
4233
4506
|
let fd = await fs.promises.open(this.file, 'r');
|
|
4234
4507
|
try {
|
|
4235
4508
|
// CRITICAL FIX: Check file size before attempting to read
|
|
@@ -4254,7 +4527,7 @@ class FileHandler {
|
|
|
4254
4527
|
let buffer = Buffer.alloc(length);
|
|
4255
4528
|
const {
|
|
4256
4529
|
bytesRead
|
|
4257
|
-
} = await
|
|
4530
|
+
} = await this._readWithTimeout(fd, buffer, 0, length, start, timeoutMs);
|
|
4258
4531
|
await fd.close();
|
|
4259
4532
|
|
|
4260
4533
|
// CRITICAL FIX: Ensure we read the expected amount of data
|
|
@@ -11624,6 +11897,51 @@ class Database extends events.EventEmitter {
|
|
|
11624
11897
|
}
|
|
11625
11898
|
}
|
|
11626
11899
|
|
|
11900
|
+
/**
|
|
11901
|
+
* Run batched index-only existence checks using a single connection per field.
|
|
11902
|
+
* Each entry must specify { field, terms, options?, id? } so the result map stays stable.
|
|
11903
|
+
*
|
|
11904
|
+
* @param {Array<Object>} criteriaArray
|
|
11905
|
+
* @param {Object} opts
|
|
11906
|
+
* @returns {Promise<Object<string, boolean>>}
|
|
11907
|
+
*/
|
|
11908
|
+
async multiExists(criteriaArray, opts = {}) {
|
|
11909
|
+
this._validateInitialization('multiExists');
|
|
11910
|
+
if (!Array.isArray(criteriaArray) || criteriaArray.length === 0) {
|
|
11911
|
+
return {};
|
|
11912
|
+
}
|
|
11913
|
+
if (!this.indexManager) {
|
|
11914
|
+
return {};
|
|
11915
|
+
}
|
|
11916
|
+
const results = {};
|
|
11917
|
+
const perField = new Map();
|
|
11918
|
+
for (let i = 0; i < criteriaArray.length; i++) {
|
|
11919
|
+
const raw = criteriaArray[i] || {};
|
|
11920
|
+
const fieldName = raw.field || raw.fieldName || raw.fieldname;
|
|
11921
|
+
const terms = raw.terms ?? raw.value;
|
|
11922
|
+
const id = raw.id !== undefined && raw.id !== null ? String(raw.id) : `criteria-${i}`;
|
|
11923
|
+
const options = raw.options;
|
|
11924
|
+
results[id] = false;
|
|
11925
|
+
if (!fieldName || typeof fieldName !== 'string') {
|
|
11926
|
+
continue;
|
|
11927
|
+
}
|
|
11928
|
+
const bucket = perField.get(fieldName) || [];
|
|
11929
|
+
bucket.push({
|
|
11930
|
+
id,
|
|
11931
|
+
terms,
|
|
11932
|
+
options
|
|
11933
|
+
});
|
|
11934
|
+
perField.set(fieldName, bucket);
|
|
11935
|
+
}
|
|
11936
|
+
for (const [field, entries] of perField) {
|
|
11937
|
+
const fieldResults = this.indexManager.multiExists(field, entries, opts);
|
|
11938
|
+
for (const [id, value] of Object.entries(fieldResults)) {
|
|
11939
|
+
results[id] = value;
|
|
11940
|
+
}
|
|
11941
|
+
}
|
|
11942
|
+
return results;
|
|
11943
|
+
}
|
|
11944
|
+
|
|
11627
11945
|
/**
|
|
11628
11946
|
* Check if any records exist using full query criteria
|
|
11629
11947
|
* Uses index intersection when possible for maximum performance
|
package/package.json
CHANGED
package/src/Database.mjs
CHANGED
|
@@ -3516,6 +3516,56 @@ class Database extends EventEmitter {
|
|
|
3516
3516
|
}
|
|
3517
3517
|
}
|
|
3518
3518
|
|
|
3519
|
+
/**
|
|
3520
|
+
* Run batched index-only existence checks using a single connection per field.
|
|
3521
|
+
* Each entry must specify { field, terms, options?, id? } so the result map stays stable.
|
|
3522
|
+
*
|
|
3523
|
+
* @param {Array<Object>} criteriaArray
|
|
3524
|
+
* @param {Object} opts
|
|
3525
|
+
* @returns {Promise<Object<string, boolean>>}
|
|
3526
|
+
*/
|
|
3527
|
+
async multiExists(criteriaArray, opts = {}) {
|
|
3528
|
+
this._validateInitialization('multiExists')
|
|
3529
|
+
|
|
3530
|
+
if (!Array.isArray(criteriaArray) || criteriaArray.length === 0) {
|
|
3531
|
+
return {}
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3534
|
+
if (!this.indexManager) {
|
|
3535
|
+
return {}
|
|
3536
|
+
}
|
|
3537
|
+
|
|
3538
|
+
const results = {}
|
|
3539
|
+
const perField = new Map()
|
|
3540
|
+
|
|
3541
|
+
for (let i = 0; i < criteriaArray.length; i++) {
|
|
3542
|
+
const raw = criteriaArray[i] || {}
|
|
3543
|
+
const fieldName = raw.field || raw.fieldName || raw.fieldname
|
|
3544
|
+
const terms = raw.terms ?? raw.value
|
|
3545
|
+
const id = raw.id !== undefined && raw.id !== null ? String(raw.id) : `criteria-${i}`
|
|
3546
|
+
const options = raw.options
|
|
3547
|
+
|
|
3548
|
+
results[id] = false
|
|
3549
|
+
|
|
3550
|
+
if (!fieldName || typeof fieldName !== 'string') {
|
|
3551
|
+
continue
|
|
3552
|
+
}
|
|
3553
|
+
|
|
3554
|
+
const bucket = perField.get(fieldName) || []
|
|
3555
|
+
bucket.push({ id, terms, options })
|
|
3556
|
+
perField.set(fieldName, bucket)
|
|
3557
|
+
}
|
|
3558
|
+
|
|
3559
|
+
for (const [field, entries] of perField) {
|
|
3560
|
+
const fieldResults = this.indexManager.multiExists(field, entries, opts)
|
|
3561
|
+
for (const [id, value] of Object.entries(fieldResults)) {
|
|
3562
|
+
results[id] = value
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3566
|
+
return results
|
|
3567
|
+
}
|
|
3568
|
+
|
|
3519
3569
|
/**
|
|
3520
3570
|
* Check if any records exist using full query criteria
|
|
3521
3571
|
* Uses index intersection when possible for maximum performance
|
package/src/FileHandler.mjs
CHANGED
|
@@ -15,6 +15,42 @@ export default class FileHandler {
|
|
|
15
15
|
this.readLimiter = pLimit(opts.maxConcurrentReads || 4)
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
_getIoTimeoutMs(override) {
|
|
19
|
+
if (typeof override === 'number') return override
|
|
20
|
+
if (typeof this.opts.ioTimeoutMs === 'number') return this.opts.ioTimeoutMs
|
|
21
|
+
return 0
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async _withIoTimeout(fn, timeoutMs, onTimeout) {
|
|
25
|
+
if (!timeoutMs || timeoutMs <= 0) {
|
|
26
|
+
return fn()
|
|
27
|
+
}
|
|
28
|
+
let timeoutId
|
|
29
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
30
|
+
timeoutId = setTimeout(() => {
|
|
31
|
+
if (onTimeout) {
|
|
32
|
+
try { onTimeout() } catch {}
|
|
33
|
+
}
|
|
34
|
+
const err = new Error(`I/O timeout after ${timeoutMs}ms`)
|
|
35
|
+
err.code = 'ETIMEDOUT'
|
|
36
|
+
reject(err)
|
|
37
|
+
}, timeoutMs)
|
|
38
|
+
})
|
|
39
|
+
try {
|
|
40
|
+
return await Promise.race([fn(), timeoutPromise])
|
|
41
|
+
} finally {
|
|
42
|
+
if (timeoutId) clearTimeout(timeoutId)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async _readWithTimeout(fd, buffer, offset, length, position, timeoutMs) {
|
|
47
|
+
return this._withIoTimeout(
|
|
48
|
+
() => fd.read(buffer, offset, length, position),
|
|
49
|
+
timeoutMs,
|
|
50
|
+
() => fd.close().catch(() => {})
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
18
54
|
async truncate(offset) {
|
|
19
55
|
try {
|
|
20
56
|
await fs.promises.access(this.file, fs.constants.F_OK)
|
|
@@ -128,6 +164,7 @@ export default class FileHandler {
|
|
|
128
164
|
return Buffer.alloc(0) // Return empty buffer if file doesn't exist
|
|
129
165
|
}
|
|
130
166
|
|
|
167
|
+
const timeoutMs = this._getIoTimeoutMs()
|
|
131
168
|
let fd = await fs.promises.open(this.file, 'r')
|
|
132
169
|
try {
|
|
133
170
|
// CRITICAL FIX: Check file size before attempting to read
|
|
@@ -151,7 +188,7 @@ export default class FileHandler {
|
|
|
151
188
|
}
|
|
152
189
|
|
|
153
190
|
let buffer = Buffer.alloc(length)
|
|
154
|
-
const { bytesRead } = await
|
|
191
|
+
const { bytesRead } = await this._readWithTimeout(fd, buffer, 0, length, start, timeoutMs)
|
|
155
192
|
await fd.close()
|
|
156
193
|
|
|
157
194
|
// CRITICAL FIX: Ensure we read the expected amount of data
|
|
@@ -1812,6 +1812,272 @@ export default class IndexManager {
|
|
|
1812
1812
|
|
|
1813
1813
|
return candidateLines.size > 0;
|
|
1814
1814
|
}
|
|
1815
|
+
|
|
1816
|
+
/**
|
|
1817
|
+
* Evaluate many index-only existence checks while sharing the same field data.
|
|
1818
|
+
* Returns a map keyed by the criteria id so callers can reuse a single scan per field.
|
|
1819
|
+
*
|
|
1820
|
+
* @param {string} fieldName - Indexed field name
|
|
1821
|
+
* @param {Array<Object>} criteriaArray - Array of { id, terms, options }
|
|
1822
|
+
* @param {Object} opts - { limit, allowPartial } (limit only applies when allowPartial is true)
|
|
1823
|
+
* @returns {Object<string, boolean>} - Map of criteria id to boolean existence
|
|
1824
|
+
*/
|
|
1825
|
+
multiExists(fieldName, criteriaArray, opts = {}) {
|
|
1826
|
+
const results = {}
|
|
1827
|
+
if (!Array.isArray(criteriaArray) || criteriaArray.length === 0) {
|
|
1828
|
+
return results
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
const prepared = []
|
|
1832
|
+
for (let i = 0; i < criteriaArray.length; i++) {
|
|
1833
|
+
const entry = criteriaArray[i] || {}
|
|
1834
|
+
const key = entry.id !== undefined && entry.id !== null ? String(entry.id) : `criteria-${i}`
|
|
1835
|
+
results[key] = false
|
|
1836
|
+
prepared.push({
|
|
1837
|
+
id: key,
|
|
1838
|
+
terms: entry.terms,
|
|
1839
|
+
options: entry.options || {}
|
|
1840
|
+
})
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
if (!fieldName || typeof fieldName !== 'string') {
|
|
1844
|
+
return results
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
const fieldIndex = this.index?.data?.[fieldName]
|
|
1848
|
+
if (!fieldIndex || typeof fieldIndex !== 'object') {
|
|
1849
|
+
return results
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
const termManager = this.database?.termManager
|
|
1853
|
+
const isTermMappingField = Boolean(termManager && termManager.termMappingFields && termManager.termMappingFields.includes(fieldName))
|
|
1854
|
+
|
|
1855
|
+
const normalizedLimit = Number.isFinite(opts.limit) && opts.limit > 0 ? Math.floor(opts.limit) : Infinity
|
|
1856
|
+
const allowPartial = Boolean(opts.allowPartial)
|
|
1857
|
+
const positiveTarget = allowPartial ? Math.max(1, normalizedLimit) : Infinity
|
|
1858
|
+
|
|
1859
|
+
const termCache = new Map()
|
|
1860
|
+
let caseInsensitiveLookup = null
|
|
1861
|
+
let termMappingLookup = null
|
|
1862
|
+
|
|
1863
|
+
const normalizeValues = (value) => {
|
|
1864
|
+
if (value === null || value === undefined) {
|
|
1865
|
+
return []
|
|
1866
|
+
}
|
|
1867
|
+
if (Array.isArray(value)) {
|
|
1868
|
+
return value.slice()
|
|
1869
|
+
}
|
|
1870
|
+
return [value]
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
const buildCaseInsensitiveLookup = () => {
|
|
1874
|
+
const map = new Map()
|
|
1875
|
+
for (const key in fieldIndex) {
|
|
1876
|
+
const lowerKey = key.toLowerCase()
|
|
1877
|
+
if (!map.has(lowerKey)) {
|
|
1878
|
+
map.set(lowerKey, key)
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
return map
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
const buildTermMappingLookup = () => {
|
|
1885
|
+
const map = new Map()
|
|
1886
|
+
const termToId = termManager?.termToId
|
|
1887
|
+
if (termToId) {
|
|
1888
|
+
for (const [termStr, id] of termToId.entries()) {
|
|
1889
|
+
const lower = termStr.toLowerCase()
|
|
1890
|
+
if (!map.has(lower)) {
|
|
1891
|
+
map.set(lower, String(id))
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
return map
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
const resolveTermKey = (value, caseInsensitive) => {
|
|
1899
|
+
if (value === null || value === undefined) {
|
|
1900
|
+
return null
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
if (isTermMappingField) {
|
|
1904
|
+
if (typeof value === 'string') {
|
|
1905
|
+
if (caseInsensitive) {
|
|
1906
|
+
if (!termMappingLookup) {
|
|
1907
|
+
termMappingLookup = buildTermMappingLookup()
|
|
1908
|
+
}
|
|
1909
|
+
return termMappingLookup.get(value.toLowerCase()) ?? null
|
|
1910
|
+
}
|
|
1911
|
+
const termId = termManager?.getTermIdWithoutIncrement(String(value))
|
|
1912
|
+
if (termId === undefined || termId === null) {
|
|
1913
|
+
return null
|
|
1914
|
+
}
|
|
1915
|
+
return String(termId)
|
|
1916
|
+
}
|
|
1917
|
+
return String(value)
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
if (caseInsensitive && typeof value === 'string') {
|
|
1921
|
+
if (!caseInsensitiveLookup) {
|
|
1922
|
+
caseInsensitiveLookup = buildCaseInsensitiveLookup()
|
|
1923
|
+
}
|
|
1924
|
+
return caseInsensitiveLookup.get(value.toLowerCase()) ?? null
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
return String(value)
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
const ensureTermEntry = (termKey) => {
|
|
1931
|
+
if (!termCache.has(termKey)) {
|
|
1932
|
+
const data = fieldIndex[termKey]
|
|
1933
|
+
const entry = {
|
|
1934
|
+
data,
|
|
1935
|
+
hasData: Boolean(data && ((data.set && data.set.size > 0) || (data.ranges && data.ranges.length > 0))),
|
|
1936
|
+
lineArray: null
|
|
1937
|
+
}
|
|
1938
|
+
termCache.set(termKey, entry)
|
|
1939
|
+
}
|
|
1940
|
+
return termCache.get(termKey)
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
const getLineArray = (entry) => {
|
|
1944
|
+
if (!entry.hasData || !entry.data) return []
|
|
1945
|
+
if (!entry.lineArray) {
|
|
1946
|
+
entry.lineArray = this._getAllLineNumbers(entry.data)
|
|
1947
|
+
}
|
|
1948
|
+
return entry.lineArray
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
const applyExcludes = (lineSet, excludeKeys) => {
|
|
1952
|
+
if (excludeKeys.length === 0) {
|
|
1953
|
+
return lineSet.size > 0
|
|
1954
|
+
}
|
|
1955
|
+
for (const excludeKey of excludeKeys) {
|
|
1956
|
+
const excludeEntry = ensureTermEntry(excludeKey)
|
|
1957
|
+
if (!excludeEntry.hasData) continue
|
|
1958
|
+
const excludeLines = getLineArray(excludeEntry)
|
|
1959
|
+
for (const line of excludeLines) {
|
|
1960
|
+
lineSet.delete(line)
|
|
1961
|
+
}
|
|
1962
|
+
if (lineSet.size === 0) {
|
|
1963
|
+
return false
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
return lineSet.size > 0
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
let successes = 0
|
|
1970
|
+
|
|
1971
|
+
for (const criterion of prepared) {
|
|
1972
|
+
if (successes >= positiveTarget) {
|
|
1973
|
+
break
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
const { id, terms, options } = criterion
|
|
1977
|
+
const { $all = false, caseInsensitive = false, excludes = [] } = options
|
|
1978
|
+
|
|
1979
|
+
const normalizedTerms = normalizeValues(terms)
|
|
1980
|
+
if (normalizedTerms.length === 0) {
|
|
1981
|
+
continue
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
const normalizedExcludes = normalizeValues(excludes)
|
|
1985
|
+
|
|
1986
|
+
const resolvedKeys = []
|
|
1987
|
+
const seenKeys = new Set()
|
|
1988
|
+
let missingRequiredTerm = false
|
|
1989
|
+
|
|
1990
|
+
for (const term of normalizedTerms) {
|
|
1991
|
+
const termKey = resolveTermKey(term, caseInsensitive)
|
|
1992
|
+
if (termKey === null) {
|
|
1993
|
+
if ($all) {
|
|
1994
|
+
missingRequiredTerm = true
|
|
1995
|
+
break
|
|
1996
|
+
}
|
|
1997
|
+
continue
|
|
1998
|
+
}
|
|
1999
|
+
if (!seenKeys.has(termKey)) {
|
|
2000
|
+
seenKeys.add(termKey)
|
|
2001
|
+
resolvedKeys.push(termKey)
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
if ($all && missingRequiredTerm) {
|
|
2006
|
+
continue
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
if (resolvedKeys.length === 0) {
|
|
2010
|
+
continue
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
const excludeKeySet = []
|
|
2014
|
+
const seenExcludeKeys = new Set()
|
|
2015
|
+
for (const excludeTerm of normalizedExcludes) {
|
|
2016
|
+
const excludeKey = resolveTermKey(excludeTerm, caseInsensitive)
|
|
2017
|
+
if (excludeKey && !seenExcludeKeys.has(excludeKey)) {
|
|
2018
|
+
seenExcludeKeys.add(excludeKey)
|
|
2019
|
+
excludeKeySet.push(excludeKey)
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
let match = false
|
|
2024
|
+
|
|
2025
|
+
if ($all) {
|
|
2026
|
+
let intersection = null
|
|
2027
|
+
let failed = false
|
|
2028
|
+
|
|
2029
|
+
for (const termKey of resolvedKeys) {
|
|
2030
|
+
const entry = ensureTermEntry(termKey)
|
|
2031
|
+
if (!entry.hasData) {
|
|
2032
|
+
failed = true
|
|
2033
|
+
break
|
|
2034
|
+
}
|
|
2035
|
+
const lines = getLineArray(entry)
|
|
2036
|
+
if (lines.length === 0) {
|
|
2037
|
+
failed = true
|
|
2038
|
+
break
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
const currentSet = new Set(lines)
|
|
2042
|
+
if (intersection === null) {
|
|
2043
|
+
intersection = currentSet
|
|
2044
|
+
} else {
|
|
2045
|
+
intersection = new Set([...intersection].filter(value => currentSet.has(value)))
|
|
2046
|
+
if (intersection.size === 0) {
|
|
2047
|
+
failed = true
|
|
2048
|
+
break
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
if (!failed && intersection && intersection.size > 0) {
|
|
2054
|
+
match = applyExcludes(intersection, excludeKeySet)
|
|
2055
|
+
}
|
|
2056
|
+
} else if (excludeKeySet.length === 0) {
|
|
2057
|
+
match = resolvedKeys.some(termKey => ensureTermEntry(termKey).hasData)
|
|
2058
|
+
} else {
|
|
2059
|
+
const candidates = new Set()
|
|
2060
|
+
for (const termKey of resolvedKeys) {
|
|
2061
|
+
const entry = ensureTermEntry(termKey)
|
|
2062
|
+
if (!entry.hasData) continue
|
|
2063
|
+
const lines = getLineArray(entry)
|
|
2064
|
+
for (const line of lines) {
|
|
2065
|
+
candidates.add(line)
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
if (candidates.size > 0) {
|
|
2069
|
+
match = applyExcludes(candidates, excludeKeySet)
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
results[id] = Boolean(match)
|
|
2074
|
+
if (match) {
|
|
2075
|
+
successes += 1
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
return results
|
|
2080
|
+
}
|
|
1815
2081
|
|
|
1816
2082
|
// Ultra-fast load with minimal conversions
|
|
1817
2083
|
load(index) {
|