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 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 fd.read(buffer, 0, length, start);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jexidb",
3
- "version": "2.1.7",
3
+ "version": "2.1.9",
4
4
  "type": "module",
5
5
  "description": "JexiDB is a pure JS NPM library for managing data on disk efficiently, without the need for a server.",
6
6
  "main": "./dist/Database.cjs",
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
@@ -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 fd.read(buffer, 0, length, start)
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) {