@delali/narsil 0.2.4-next.48 → 0.2.4-next.50

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.
Files changed (60) hide show
  1. package/README.md +2 -2
  2. package/dist/adapters/indexeddb.mjs +15 -10
  3. package/dist/{options-BC68VPoE.d.ts → capabilities-JrVQOwi4.d.ts} +55 -47
  4. package/dist/{chunk-BFIH7OV5.mjs → chunk-2GWVQWGA.mjs} +3 -1
  5. package/dist/chunk-AV4IJKD2.mjs +35 -0
  6. package/dist/{chunk-EV45L35L.mjs → chunk-BEIK3EOZ.mjs} +845 -2407
  7. package/dist/chunk-CBLS46IE.mjs +10 -0
  8. package/dist/chunk-CMVG2UEF.mjs +2038 -0
  9. package/dist/chunk-CV5H7XL7.mjs +876 -0
  10. package/dist/{chunk-JQUL2T6I.mjs → chunk-GCM3JFVA.mjs} +1 -2
  11. package/dist/{chunk-XWICBGYT.mjs → chunk-MHE7YWAU.mjs} +3 -5
  12. package/dist/{chunk-UZTT2BKQ.mjs → chunk-ND34I3SV.mjs} +1202 -1881
  13. package/dist/{chunk-PM52SMBT.mjs → chunk-OOSIDHA3.mjs} +2 -1
  14. package/dist/chunk-ORBLD3EL.mjs +6 -0
  15. package/dist/chunk-PIWKZPP7.mjs +64 -0
  16. package/dist/{chunk-4COAUBLJ.mjs → chunk-PMBHH44R.mjs} +124 -8
  17. package/dist/chunk-RP23PWMV.mjs +11 -0
  18. package/dist/chunk-UNEEFTEU.mjs +7 -0
  19. package/dist/{chunk-WTONLDU3.mjs → chunk-WYJXSLER.mjs} +9 -19
  20. package/dist/{chunk-DFZDV5FV.mjs → chunk-X6YZO4MQ.mjs} +19 -15
  21. package/dist/chunk-XE6XR2OE.mjs +23 -0
  22. package/dist/chunk-YEO6G2NG.mjs +6 -0
  23. package/dist/chunk-ZHUQKZBE.mjs +1292 -0
  24. package/dist/{client-C5BkUqB8.d.ts → client-CBOVHdjp.d.ts} +31 -7
  25. package/dist/client.d.ts +7 -7
  26. package/dist/client.mjs +250 -232
  27. package/dist/{config-B-ZI-ft5.d.ts → config-Czh2YNDi.d.ts} +2 -2
  28. package/dist/{config-BZRExglL.d.ts → config-OON7IJ_B.d.ts} +50 -13
  29. package/dist/direct-executor-OREXU72C.mjs +12 -0
  30. package/dist/distribution/coordinator/etcd.d.ts +2 -2
  31. package/dist/distribution/coordinator/etcd.mjs +2 -3
  32. package/dist/distribution/coordinator/in-memory.d.ts +2 -2
  33. package/dist/distribution/coordinator/in-memory.mjs +2 -2
  34. package/dist/distribution/transport/grpc.mjs +7 -9
  35. package/dist/distribution/transport/in-memory.mjs +2 -1
  36. package/dist/distribution/transport/tcp.mjs +3 -2
  37. package/dist/distribution.d.ts +8 -8
  38. package/dist/distribution.mjs +142 -101
  39. package/dist/embeddings/openai.mjs +5 -3
  40. package/dist/{engine-Cjr1uBLa.d.ts → engine-9PtFY50q.d.ts} +125 -69
  41. package/dist/index.browser.mjs +4689 -3330
  42. package/dist/index.d.ts +7 -7
  43. package/dist/index.mjs +14 -8
  44. package/dist/invalidation/broadcast-channel.mjs +2 -1
  45. package/dist/invalidation/filesystem.mjs +3 -4
  46. package/dist/{search-DDHjfXhJ.d.ts → memory-Bzd5xcEW.d.ts} +89 -63
  47. package/dist/persistence/durability/checkpoint-worker.d.ts +1 -1
  48. package/dist/persistence/durability/checkpoint-worker.mjs +14 -8
  49. package/dist/react.d.ts +4 -4
  50. package/dist/react.mjs +7 -5
  51. package/dist/{schema-B7juWg8Q.d.ts → schema-BqRx9t4E.d.ts} +22 -3
  52. package/dist/server.d.ts +47 -42
  53. package/dist/server.mjs +338 -821
  54. package/dist/thread-PZLEO6JL.mjs +310 -0
  55. package/dist/{types-DsSSjY4d.d.ts → types-DXPxaXPO.d.ts} +1 -1
  56. package/dist/vector/hnsw-build-worker.mjs +3 -4
  57. package/dist/vector/search-worker.mjs +4 -123
  58. package/dist/workers/entry.mjs +47 -3
  59. package/package.json +5 -1
  60. package/dist/direct-executor-EOTFGXTO.mjs +0 -8
@@ -1,14 +1,21 @@
1
- import { ReplicationMessageTypes } from './chunk-JQUL2T6I.mjs';
2
- import { MAX_INDEX_NAME_LENGTH } from './chunk-XWICBGYT.mjs';
3
- import { getNestedValue, getLanguage, extractVectorFieldsFromSchema, createDirectExecutor, createPartitionRouter, clampRowCount, DEFAULT_PAGE_SIZE, validateSchema, validateVectorPromotion, collectQueryTermStats, createPartitionManager, createVectorIndex, flattenSchema, isTextFieldType, fnv1aBytes, requireWithinResultWindow, normalizeSort, sortSignatureOf, requireSortableFields, fanOutQuery, applySorting, applyPagination, resolveProjection, countsWithoutScores, fanOutMatchCount, tokenize, decodePageCursor, requireMatchingCursor, encodePageCursor, resolveIndexAnalysis, deserializePayloadV2, deserializePayloadV1, mergePartitionStats, fulltextMatches, mergeFacets, pruneStatsToQueryTerms, createPartitionIndex, fnv1a, createWorkerPool, partitionsIn, detectRuntime, isNodeMainThread, spawnNodeWorker, createSharedFrozenSegment, buildCompactedSegmentPayload, freezeSegmentShared, createFrozenSegment, deserializeMetadata, isCompositePartition, serializeMetadata, kWayMerge, validateRequiredFields, validateDocument, validateDocumentStrict } from './chunk-EV45L35L.mjs';
1
+ import { validateWorkerConfig, validateIndexName, validateBM25Params, validatePartitionConfig, executeQuery, queryBindingOf, executePreflight, executeSuggest, executeListDocuments, validateDocId } from './chunk-ZHUQKZBE.mjs';
2
+ import { ReplicationMessageTypes } from './chunk-GCM3JFVA.mjs';
3
+ import { WIRE_BATCH_BUDGET, resolveVectorText, bindEngineCore } from './chunk-PIWKZPP7.mjs';
4
+ import { STATISTICS_BROADCAST_INTERVAL_MS, MAX_SWEEP_INTERVAL_MS, MIN_SWEEP_INTERVAL_MS, HEAP_PRESSURE_WARNING_FRACTION, FOREIGN_STATS_TTL_MS, HEAP_PRESSURE_REARM_FRACTION, MIN_DOCUMENTS_FOR_SEGMENTS, BATCH_CHUNK_SIZE } from './chunk-XE6XR2OE.mjs';
5
+ import { getNestedValue, generateId, getLanguage, splitWorkerBudget, resolveWorkerCount, createDirectExecutor, createPartitionRouter, createPartitionManager, createVectorIndex, readHeapStatistics, resolveIndexAnalysis, deserializePayloadV2, deserializePayloadV1, resolveRequestThreadCount, createPartitionIndex, createWorkerPool, isNodeMainThread, spawnNodeWorker, CHECKSUM_TIMEOUT_MS, CHECKSUM_YIELD_CHUNK_BYTES, createSharedFrozenSegment, buildCompactedSegmentPayload, freezeSegmentShared, createFrozenSegment, isCompositePartition, WORKER_RESOURCE_LIMITS, deserializeMetadata, DEFAULT_BACKPRESSURE_LIMIT, DEFAULT_REQUEST_TIMEOUT_MS, serializeMetadata, SHUTDOWN_TIMEOUT_MS, CONSECUTIVE_TIMEOUTS_BEFORE_DEATH, encodeSegmentState } from './chunk-BEIK3EOZ.mjs';
6
+ import { extractVectorFieldsFromSchema, validateSchema, validateVectorPromotion, collectQueryTermStats, flattenSchema, isTextFieldType, mergePartitionStats, fnv1a, kWayMerge, mergeFacets, validateRequiredFields, validateDocument, validateDocumentStrict } from './chunk-CMVG2UEF.mjs';
7
+ import { detectRuntime } from './chunk-AV4IJKD2.mjs';
4
8
  import { crc32, crc32Init, crc32Update, crc32Final } from './chunk-KMMJMDUA.mjs';
5
- import { compareCodePoints, compareComparableKeys } from './chunk-BFIH7OV5.mjs';
6
- import { generateId, createRequestId, isValidWorkerResponse } from './chunk-WTONLDU3.mjs';
9
+ import { compareCodePoints } from './chunk-2GWVQWGA.mjs';
10
+ import { createRequestId, isValidWorkerResponse } from './chunk-WYJXSLER.mjs';
7
11
  import { ErrorCodes, NarsilError } from './chunk-P4ELXVJQ.mjs';
8
12
  import { encode, decode } from '@msgpack/msgpack';
9
13
 
14
+ // src/partitioning/constants.ts
15
+ var REBALANCE_CHUNK_SIZE = 1e3;
16
+ var DEFAULT_WRITE_AHEAD_QUEUE_MAX_SIZE = 1e4;
17
+
10
18
  // src/partitioning/rebalancer.ts
11
- var CHUNK_SIZE = 1e3;
12
19
  function createRebalancer() {
13
20
  const activeManagers = /* @__PURE__ */ new WeakSet();
14
21
  let activeCount = 0;
@@ -71,7 +78,7 @@ function createRebalancer() {
71
78
  documentsProcessed++;
72
79
  }
73
80
  chunkFill++;
74
- if (chunkFill >= CHUNK_SIZE) {
81
+ if (chunkFill >= REBALANCE_CHUNK_SIZE) {
75
82
  chunkFill = 0;
76
83
  onProgress?.({
77
84
  phase: "moving",
@@ -269,42 +276,18 @@ function validateRequiredFieldsInSchema(required, schema) {
269
276
  }
270
277
  }
271
278
 
272
- // src/workers/promoter.ts
273
- var DEFAULT_PER_INDEX_THRESHOLD = 1e4;
274
- var DEFAULT_TOTAL_THRESHOLD = 5e4;
275
- function createExecutionPromoter(config) {
276
- const perIndexThreshold = config?.perIndexThreshold ?? DEFAULT_PER_INDEX_THRESHOLD;
277
- const totalThreshold = config?.totalThreshold ?? DEFAULT_TOTAL_THRESHOLD;
278
- let promoted = false;
279
- function check(indexes) {
280
- if (promoted) {
281
- return { shouldPromote: false, reason: "" };
282
- }
283
- let totalDocuments = 0;
284
- for (const [indexName, stats] of indexes) {
285
- if (stats.documentCount >= perIndexThreshold) {
286
- return {
287
- shouldPromote: true,
288
- reason: `Index "${indexName}" has ${stats.documentCount} documents, exceeding the per-index threshold of ${perIndexThreshold}`
289
- };
290
- }
291
- totalDocuments += stats.documentCount;
292
- }
293
- if (totalDocuments >= totalThreshold) {
294
- return {
295
- shouldPromote: true,
296
- reason: `Total document count of ${totalDocuments} exceeds the total threshold of ${totalThreshold}`
297
- };
279
+ // src/engine/events.ts
280
+ function emitEngineEvent(eventHandlers, event, payload) {
281
+ const handlers = eventHandlers.get(event);
282
+ if (!handlers || handlers.size === 0) return 0;
283
+ for (const handler of handlers) {
284
+ try {
285
+ handler(payload);
286
+ } catch (err) {
287
+ console.warn(`${event} handler error:`, err instanceof Error ? err.message : String(err));
298
288
  }
299
- return { shouldPromote: false, reason: "" };
300
289
  }
301
- function markPromoted() {
302
- promoted = true;
303
- }
304
- function isPromoted() {
305
- return promoted;
306
- }
307
- return { check, markPromoted, isPromoted };
290
+ return handlers.size;
308
291
  }
309
292
 
310
293
  // src/engine/analysis-rebuild.ts
@@ -328,15 +311,7 @@ function wireAnalysisRebuild(wiring) {
328
311
  await (durability.checkpointFromMemory?.(indexName) ?? durability.checkpoint(indexName));
329
312
  },
330
313
  emit(payload) {
331
- const handlers = wiring.eventHandlers.get("analysisRebuild");
332
- if (!handlers) return;
333
- for (const handler of handlers) {
334
- try {
335
- handler(payload);
336
- } catch (err) {
337
- console.warn("analysisRebuild handler error:", err instanceof Error ? err.message : String(err));
338
- }
339
- }
314
+ emitEngineEvent(wiring.eventHandlers, "analysisRebuild", payload);
340
315
  }
341
316
  });
342
317
  }
@@ -383,7 +358,7 @@ function createAnalysisRebuildCoordinator(config, deps) {
383
358
  throw error;
384
359
  }
385
360
  }
386
- function enqueue2(indexName, entry) {
361
+ function enqueue(indexName, entry) {
387
362
  if (entry.run !== null) {
388
363
  return entry.run;
389
364
  }
@@ -397,7 +372,7 @@ function createAnalysisRebuildCoordinator(config, deps) {
397
372
  }
398
373
  async function announce(indexName, entry) {
399
374
  const documentCount = deps.getManager(indexName)?.countDocuments() ?? 0;
400
- await config?.onStaleAnalysis?.({ ...entry.index, documentCount }, () => enqueue2(indexName, entry));
375
+ await config?.onStaleAnalysis?.({ ...entry.index, documentCount }, () => enqueue(indexName, entry));
401
376
  }
402
377
  async function review(indexName, entry) {
403
378
  if (entry.run !== null || deps.getManager(indexName) === void 0) return;
@@ -413,7 +388,7 @@ function createAnalysisRebuildCoordinator(config, deps) {
413
388
  });
414
389
  }
415
390
  if (rebuildMode === "auto") {
416
- void enqueue2(indexName, entry).catch(() => void 0);
391
+ void enqueue(indexName, entry).catch(() => void 0);
417
392
  }
418
393
  }
419
394
  return {
@@ -447,7 +422,7 @@ function createAnalysisRebuildCoordinator(config, deps) {
447
422
  if (entry === void 0) {
448
423
  return Promise.resolve();
449
424
  }
450
- return enqueue2(indexName, entry);
425
+ return enqueue(indexName, entry);
451
426
  }
452
427
  };
453
428
  }
@@ -532,6 +507,15 @@ function resolveDurabilityTier(config) {
532
507
  return { kind: "snapshot", config: {}, adapter: config.persistence };
533
508
  }
534
509
 
510
+ // src/persistence/durability/constants.ts
511
+ var DEFAULT_CHECKPOINT_INTERVAL_MS = 3e5;
512
+ var DEFAULT_CHECKPOINT_MUTATION_THRESHOLD = 1e5;
513
+ var DEFAULT_ASYNC_FLUSH_INTERVAL_MS = 1e3;
514
+ var DEFAULT_SEGMENT_MAX_BYTES = 67108864;
515
+ var DEFAULT_COMPACTION_THRESHOLD = 12;
516
+ var CHECKPOINT_WORKER_TIMEOUT_MS = 6e5;
517
+ var CHECKPOINT_TIMEOUT_RECOVERY_BACKOFF_MS = 50;
518
+
535
519
  // src/persistence/durability/node-fs.ts
536
520
  var cachedFs = null;
537
521
  var cachedPath = null;
@@ -809,8 +793,6 @@ function buildEntry(fields) {
809
793
  }
810
794
 
811
795
  // src/serialization/checksum-dispatch.ts
812
- var CHECKSUM_TIMEOUT_MS = 12e4;
813
- var YIELD_CHUNK_BYTES = 4 * 1024 * 1024;
814
796
  var workerUsable = true;
815
797
  var mainThreadResolved = false;
816
798
  var onMainThread = true;
@@ -844,8 +826,8 @@ function yieldToEventLoop2() {
844
826
  }
845
827
  async function chunkedChecksum(payload) {
846
828
  let state = crc32Init();
847
- for (let offset = 0; offset < payload.length; offset += YIELD_CHUNK_BYTES) {
848
- const end = Math.min(offset + YIELD_CHUNK_BYTES, payload.length);
829
+ for (let offset = 0; offset < payload.length; offset += CHECKSUM_YIELD_CHUNK_BYTES) {
830
+ const end = Math.min(offset + CHECKSUM_YIELD_CHUNK_BYTES, payload.length);
849
831
  state = crc32Update(state, payload.subarray(offset, end));
850
832
  if (end < payload.length) {
851
833
  await yieldToEventLoop2();
@@ -855,7 +837,7 @@ async function chunkedChecksum(payload) {
855
837
  }
856
838
  function runWorkerChecksum(worker, payload) {
857
839
  return new Promise((resolve, reject) => {
858
- let settled = false;
840
+ let settled2 = false;
859
841
  const timeoutId = setTimeout(
860
842
  () => settle(() => reject(new Error(`Checksum worker timed out after ${CHECKSUM_TIMEOUT_MS}ms`))),
861
843
  CHECKSUM_TIMEOUT_MS
@@ -864,8 +846,8 @@ function runWorkerChecksum(worker, payload) {
864
846
  timeoutId.unref();
865
847
  }
866
848
  function settle(action) {
867
- if (settled) return;
868
- settled = true;
849
+ if (settled2) return;
850
+ settled2 = true;
869
851
  clearTimeout(timeoutId);
870
852
  try {
871
853
  void worker.terminate();
@@ -1437,8 +1419,6 @@ function formatSegmentStart(startSeqNo) {
1437
1419
  }
1438
1420
 
1439
1421
  // src/persistence/durability/checkpoint-worker-dispatch.ts
1440
- var CHECKPOINT_WORKER_TIMEOUT_MS = 6e5;
1441
- var TIMEOUT_RECOVERY_BACKOFF_MS = 50;
1442
1422
  var workerUsable2 = true;
1443
1423
  var pooledWorker = null;
1444
1424
  var workerBusy = false;
@@ -1477,7 +1457,7 @@ function discardWorker(worker) {
1477
1457
  }
1478
1458
  function runWorker(worker, request) {
1479
1459
  return new Promise((resolve) => {
1480
- let settled = false;
1460
+ let settled2 = false;
1481
1461
  const onMessage = (msg) => {
1482
1462
  const response = msg;
1483
1463
  if (response.type === "success") {
@@ -1497,10 +1477,10 @@ function runWorker(worker, request) {
1497
1477
  timeoutId.unref();
1498
1478
  }
1499
1479
  function settle(outcome, discard) {
1500
- if (settled) {
1480
+ if (settled2) {
1501
1481
  return;
1502
1482
  }
1503
- settled = true;
1483
+ settled2 = true;
1504
1484
  clearTimeout(timeoutId);
1505
1485
  worker.off("message", onMessage);
1506
1486
  worker.off("error", onError);
@@ -1551,7 +1531,7 @@ async function runCheckpointOnWorker(request) {
1551
1531
  return true;
1552
1532
  }
1553
1533
  if (outcome.timedOut) {
1554
- await delay(TIMEOUT_RECOVERY_BACKOFF_MS);
1534
+ await delay(CHECKPOINT_TIMEOUT_RECOVERY_BACKOFF_MS);
1555
1535
  }
1556
1536
  return false;
1557
1537
  } finally {
@@ -1571,7 +1551,6 @@ function terminateCheckpointWorker() {
1571
1551
  }
1572
1552
 
1573
1553
  // src/persistence/durability/segment/layout.ts
1574
- var DEFAULT_COMPACTION_THRESHOLD = 12;
1575
1554
  var SEGMENT_ID_WIDTH = 16;
1576
1555
  function manifestKey(indexName) {
1577
1556
  return `${indexName}/manifest`;
@@ -2513,37 +2492,6 @@ function setNestedValue(obj, path, value) {
2513
2492
  }
2514
2493
  current[finalSegment] = value;
2515
2494
  }
2516
-
2517
- // src/distribution/chunking.ts
2518
- var WIRE_BATCH_BUDGET = {
2519
- maxCount: 1e3,
2520
- maxBytes: 8388608,
2521
- overheadBytes: 256
2522
- };
2523
- function chunkByBudget(items, budget) {
2524
- const chunks = [];
2525
- let current = [];
2526
- let currentBytes = 0;
2527
- for (const item of items) {
2528
- const itemBytes = budget.payloadBytesOf(item) + budget.overheadBytes;
2529
- const previous = current[current.length - 1];
2530
- const breaksRun = previous !== void 0 && budget.breaksRun?.(item, previous) === true;
2531
- const overflows = current.length >= budget.maxCount || current.length > 0 && currentBytes + itemBytes > budget.maxBytes;
2532
- if (breaksRun || overflows) {
2533
- chunks.push(current);
2534
- current = [];
2535
- currentBytes = 0;
2536
- }
2537
- current.push(item);
2538
- currentBytes += itemBytes;
2539
- }
2540
- if (current.length > 0) {
2541
- chunks.push(current);
2542
- }
2543
- return chunks;
2544
- }
2545
-
2546
- // src/distribution/replication/codec.ts
2547
2495
  function createEntryMessage(entry, sourceId) {
2548
2496
  const payload = { entry };
2549
2497
  return {
@@ -3266,7 +3214,10 @@ async function writePartitionVectors(input) {
3266
3214
  }
3267
3215
  const vectorIndexes = /* @__PURE__ */ new Map();
3268
3216
  for (const [fieldPath, dimension] of input.vectorFields) {
3269
- vectorIndexes.set(fieldPath, createVectorIndex(fieldPath, dimension, input.config.vectorPromotion));
3217
+ vectorIndexes.set(
3218
+ fieldPath,
3219
+ createVectorIndex(fieldPath, dimension, input.config.vectorPromotion, { enabled: false })
3220
+ );
3270
3221
  }
3271
3222
  for (const ref of input.priorVectors) {
3272
3223
  const vecIndex = vectorIndexes.get(ref.fieldPath);
@@ -3684,11 +3635,6 @@ async function recoverPersistedIndex(deps, indexName, metadataOnly) {
3684
3635
  }
3685
3636
  }
3686
3637
 
3687
- // src/persistence/durability/types.ts
3688
- var DEFAULT_CHECKPOINT_INTERVAL_MS = 3e5;
3689
- var DEFAULT_CHECKPOINT_MUTATION_THRESHOLD = 1e5;
3690
- var DEFAULT_ASYNC_FLUSH_INTERVAL_MS = 1e3;
3691
-
3692
3638
  // src/persistence/durability/group-commit.ts
3693
3639
  function createGroupCommitCoordinator(sync) {
3694
3640
  let inFlight = null;
@@ -3762,7 +3708,6 @@ async function createMarkerWriter(directory, indexName, partitionId) {
3762
3708
  }
3763
3709
 
3764
3710
  // src/persistence/durability/wal-writer.ts
3765
- var DEFAULT_SEGMENT_MAX_BYTES = 67108864;
3766
3711
  var SEGMENT_TAIL_PATTERN = /^\d{16}$/;
3767
3712
  function segmentKey2(indexName, partitionId, startSeqNo) {
3768
3713
  const padded = startSeqNo.toString().padStart(16, "0");
@@ -4680,11 +4625,11 @@ function createDurabilityFromTier(tier, wiring) {
4680
4625
  });
4681
4626
  }
4682
4627
 
4683
- // src/engine/index-state/entry.ts
4628
+ // src/engine/index-state/constants.ts
4684
4629
  var DEFAULT_MAX_REOPEN_WAITERS = 64;
4685
4630
  var MAX_REOPEN_FAILURES = 5;
4686
- var MIN_SWEEP_INTERVAL_MS = 100;
4687
- var MAX_SWEEP_INTERVAL_MS = 6e4;
4631
+
4632
+ // src/engine/index-state/entry.ts
4688
4633
  function createLifecycleEntry(state) {
4689
4634
  return {
4690
4635
  state,
@@ -4958,7 +4903,10 @@ function createIndexStateCoordinator(config, callbacks) {
4958
4903
  await reopen(indexName, false);
4959
4904
  entry = entryOf(indexName);
4960
4905
  }
4961
- if (markActive) entry.lastAccessAt = Date.now();
4906
+ if (markActive) {
4907
+ entry.lastAccessAt = Date.now();
4908
+ callbacks.onAccess?.(indexName);
4909
+ }
4962
4910
  entry.activeOperations += 1;
4963
4911
  try {
4964
4912
  if (opened) await enforceLimits(indexName);
@@ -5061,6 +5009,9 @@ function wireIndexState(wiring) {
5061
5009
  }
5062
5010
  }
5063
5011
  },
5012
+ onAccess(indexName) {
5013
+ wiring.onAccess?.(indexName);
5014
+ },
5064
5015
  canCloseAutomatically(indexName) {
5065
5016
  return !rebalancingIndexes.has(indexName) && !analysisRebuild.isRunning(indexName) && !orchestrator.isIndexBusy(indexName);
5066
5017
  },
@@ -5071,8 +5022,6 @@ function wireIndexState(wiring) {
5071
5022
  }
5072
5023
 
5073
5024
  // src/engine/invalidation.ts
5074
- var STATISTICS_BROADCAST_INTERVAL_MS = 5e3;
5075
- var FOREIGN_STATS_TTL_MS = 6e4;
5076
5025
  function createInvalidationFromConfig(config, tierKind, deps) {
5077
5026
  if (config?.invalidation === void 0) {
5078
5027
  return null;
@@ -5226,41 +5175,222 @@ function createInvalidationIntegration(deps) {
5226
5175
  };
5227
5176
  }
5228
5177
 
5178
+ // src/engine/heap-pressure.ts
5179
+ function createHeapPressureNotifier(deps) {
5180
+ let warned = false;
5181
+ function check(indexName) {
5182
+ const heap = deps.readHeap();
5183
+ if (heap === null) return;
5184
+ const fraction = heap.usedBytes / heap.limitBytes;
5185
+ if (warned) {
5186
+ if (fraction < HEAP_PRESSURE_REARM_FRACTION) warned = false;
5187
+ return;
5188
+ }
5189
+ if (fraction < HEAP_PRESSURE_WARNING_FRACTION) return;
5190
+ warned = true;
5191
+ deps.emit({
5192
+ indexName,
5193
+ heapUsed: heap.usedBytes,
5194
+ heapLimit: heap.limitBytes,
5195
+ estimatedMemoryBytes: deps.estimateIndexBytes(indexName)
5196
+ });
5197
+ }
5198
+ return { check };
5199
+ }
5200
+
5201
+ // src/engine/watermark.ts
5202
+ function createWatermarkNotifier(deps) {
5203
+ const latchedCapacity = /* @__PURE__ */ new Map();
5204
+ function check(indexName) {
5205
+ const partitionConfig = deps.getPartitionConfig(indexName);
5206
+ const maxDocs = partitionConfig?.maxDocsPerPartition;
5207
+ const watermark = partitionConfig?.watermark;
5208
+ if (maxDocs === void 0 || watermark === void 0) {
5209
+ latchedCapacity.delete(indexName);
5210
+ return;
5211
+ }
5212
+ const manager = deps.getManager(indexName);
5213
+ if (!manager) {
5214
+ latchedCapacity.delete(indexName);
5215
+ return;
5216
+ }
5217
+ const partitionCount = manager.partitionCount;
5218
+ const capacity = maxDocs * partitionCount;
5219
+ const documentCount = manager.countDocuments();
5220
+ const threshold = watermark * capacity;
5221
+ if (documentCount < threshold) {
5222
+ latchedCapacity.delete(indexName);
5223
+ return;
5224
+ }
5225
+ if (latchedCapacity.get(indexName) === capacity) return;
5226
+ latchedCapacity.set(indexName, capacity);
5227
+ deps.emit({ indexName, documentCount, capacity, partitionCount });
5228
+ }
5229
+ function forget(indexName) {
5230
+ latchedCapacity.delete(indexName);
5231
+ }
5232
+ return { check, forget };
5233
+ }
5234
+
5235
+ // src/engine/notifiers.ts
5236
+ function wireWatermarkNotifier(wiring) {
5237
+ return createWatermarkNotifier({
5238
+ getManager: wiring.getManager,
5239
+ getPartitionConfig: (indexName) => wiring.indexRegistry.get(indexName)?.config.partitions,
5240
+ emit: (payload) => emitEngineEvent(wiring.eventHandlers, "partitionWatermark", payload)
5241
+ });
5242
+ }
5243
+ function warnUnheardHeapPressure(payload) {
5244
+ console.warn(
5245
+ `Heap at ${payload.heapUsed} of ${payload.heapLimit} bytes after index "${payload.indexName}"; raise --max-old-space-size-percentage or close an idle index`
5246
+ );
5247
+ }
5248
+ function wireHeapPressureNotifier(wiring) {
5249
+ return createHeapPressureNotifier({
5250
+ readHeap: readHeapStatistics,
5251
+ estimateIndexBytes: (indexName) => wiring.getManager(indexName)?.estimateMemoryBytes() ?? 0,
5252
+ emit: (payload) => {
5253
+ if (emitEngineEvent(wiring.eventHandlers, "heapPressure", payload) === 0) warnUnheardHeapPressure(payload);
5254
+ }
5255
+ });
5256
+ }
5257
+ function checkHeapAfterRecovery(core) {
5258
+ const sized = [...core.indexRegistry.keys()].map((indexName) => ({
5259
+ indexName,
5260
+ bytes: core.executor.getManager(indexName)?.estimateMemoryBytes() ?? 0
5261
+ }));
5262
+ sized.sort((a, b) => b.bytes - a.bytes);
5263
+ for (const { indexName } of sized) core.heapPressureNotifier.check(indexName);
5264
+ }
5265
+
5266
+ // src/core/partition/frozen/share.ts
5267
+ function shareFrozenSegment(segment) {
5268
+ if (segment.sharedSnapshot !== null) {
5269
+ return { snapshot: segment.sharedSnapshot, tombstonedDocIds: segment.tombstonedDocIds() };
5270
+ }
5271
+ const payload = encodeSegmentState(segment);
5272
+ const documents = payload.docIds.map((docId) => {
5273
+ const stored = segment.docStore.get(docId);
5274
+ if (stored === void 0) {
5275
+ throw new NarsilError(ErrorCodes.PARTITION_CORRUPTED, `Document "${docId}" vanished while sharing a segment`, {
5276
+ docId
5277
+ });
5278
+ }
5279
+ return stored.fields;
5280
+ });
5281
+ const snapshot = freezeSegmentShared(payload, documents, segment.segmentId);
5282
+ return snapshot === null ? null : { snapshot, tombstonedDocIds: [] };
5283
+ }
5284
+
5229
5285
  // src/engine/worker-resync.ts
5230
- async function transferIndexToPool(indexName, pool, config, manager) {
5231
- const allExecutors = pool.getAllExecutors();
5286
+ function captureComposite(manager, partitionId) {
5287
+ const partition = manager.getPartition(partitionId);
5288
+ if (!isCompositePartition(partition)) return null;
5289
+ const segmentIds = partition.frozenSegmentSizes().map((size) => size.segmentId);
5290
+ const frozen = [];
5291
+ for (const segment of partition.frozenSegmentsById(segmentIds)) {
5292
+ const shared = shareFrozenSegment(segment);
5293
+ if (shared === null) return null;
5294
+ frozen.push(shared);
5295
+ }
5296
+ const live = partition.live.serialize(
5297
+ manager.indexName,
5298
+ manager.partitionCount,
5299
+ manager.language.name,
5300
+ manager.schema
5301
+ );
5302
+ return { partitionId, live, frozen };
5303
+ }
5304
+ function capturePartitions(manager) {
5305
+ const captured = [];
5306
+ for (let partitionId = 0; partitionId < manager.partitionCount; partitionId++) {
5307
+ captured.push(
5308
+ captureComposite(manager, partitionId) ?? {
5309
+ partitionId,
5310
+ live: manager.serializePartition(partitionId),
5311
+ frozen: []
5312
+ }
5313
+ );
5314
+ }
5315
+ return captured;
5316
+ }
5317
+ async function sendPartition(indexName, executors, captured) {
5318
+ await Promise.all(
5319
+ executors.map(
5320
+ (workerExecutor) => workerExecutor.execute({
5321
+ type: "deserialize",
5322
+ indexName,
5323
+ partitionId: captured.partitionId,
5324
+ data: captured.live,
5325
+ requestId: `resync-${indexName}-${captured.partitionId}`
5326
+ })
5327
+ )
5328
+ );
5329
+ if (captured.frozen.length === 0) return;
5330
+ const segments = captured.frozen.map((shared) => ({
5331
+ partitionId: captured.partitionId,
5332
+ snapshot: shared.snapshot,
5333
+ tombstonedDocIds: shared.tombstonedDocIds
5334
+ }));
5335
+ await Promise.all(
5336
+ executors.map(
5337
+ (workerExecutor) => workerExecutor.execute({
5338
+ type: "attachSegments",
5339
+ indexName,
5340
+ segments,
5341
+ requestId: `resync-attach-${indexName}-${captured.partitionId}`
5342
+ })
5343
+ )
5344
+ );
5345
+ }
5346
+ async function transferIndexToPool(indexName, pool, config, manager, analysisStale = false) {
5347
+ await loadIndexOntoWorkers(
5348
+ indexName,
5349
+ pool.getAllExecutors(),
5350
+ config,
5351
+ manager,
5352
+ () => pool.addIndexToAll(indexName),
5353
+ analysisStale
5354
+ );
5355
+ }
5356
+ async function loadIndexOntoWorkers(indexName, executors, config, manager, onDropped, analysisStale = false) {
5357
+ const captured = capturePartitions(manager);
5232
5358
  await Promise.allSettled(
5233
- allExecutors.map(
5359
+ executors.map(
5234
5360
  (workerExecutor) => workerExecutor.execute({ type: "dropIndex", indexName, requestId: `resync-drop-${indexName}` })
5235
5361
  )
5236
5362
  );
5237
- pool.addIndexToAll(indexName);
5363
+ onDropped?.();
5238
5364
  await Promise.all(
5239
- allExecutors.map(
5365
+ executors.map(
5240
5366
  (workerExecutor) => workerExecutor.execute({
5241
5367
  type: "createIndex",
5242
5368
  indexName,
5243
5369
  config,
5244
- requestId: `resync-create-${indexName}`
5370
+ requestId: `resync-create-${indexName}`,
5371
+ analysisStale
5245
5372
  })
5246
5373
  )
5247
5374
  );
5248
- for (let i = 0; i < manager.partitionCount; i++) {
5249
- const serialized = manager.serializePartition(i);
5250
- await Promise.all(
5251
- allExecutors.map(
5252
- (workerExecutor) => workerExecutor.execute({
5253
- type: "deserialize",
5254
- indexName,
5255
- partitionId: i,
5256
- data: serialized,
5257
- requestId: `resync-${indexName}-${i}`
5258
- })
5259
- )
5260
- );
5375
+ for (const partition of captured) {
5376
+ await sendPartition(indexName, executors, partition);
5261
5377
  }
5262
5378
  }
5263
5379
 
5380
+ // src/engine/orchestration/constants.ts
5381
+ var DEFAULT_COPY_THRESHOLD = 1e3;
5382
+ var DEFAULT_COPY_IDLE_TIMEOUT_MS = 3e5;
5383
+ var COMPACTION_SEGMENT_TRIGGER = 8;
5384
+ var IDLE_MERGE_DELAY_MS = 1e3;
5385
+ var LIVE_TAIL_FREEZE_FLOOR = MIN_DOCUMENTS_FOR_SEGMENTS;
5386
+ var LIVE_TAIL_FLUSH_DOCUMENTS = 5e3;
5387
+ var MAX_PENDING_REPLICATION_DOCUMENTS = 2e4;
5388
+ var REPLICATION_WINDOW = 32;
5389
+ var LOAD_BUFFER_YIELD_INTERVAL = 1e3;
5390
+ var MAIN_COPY_LONE_QUERY_DOCUMENTS = 5e4;
5391
+ var POOL_RESTART_DELAY_MS = 1e3;
5392
+ var POOL_RESTART_DELAY_MAX_MS = 6e4;
5393
+
5264
5394
  // src/engine/orchestration/eligibility.ts
5265
5395
  function toError3(value) {
5266
5396
  return value instanceof Error ? value : new Error(String(value));
@@ -5309,7 +5439,7 @@ function workerIneligibility(indexName, config, bootstrapModule) {
5309
5439
  function reportIneligible(state, indexName, error) {
5310
5440
  if (state.reportedIneligible.has(indexName)) return;
5311
5441
  state.reportedIneligible.add(indexName);
5312
- state.callbacks?.onPromotionFailure?.("index-excluded", error, false);
5442
+ state.callbacks?.onCopyLoadFailure?.("index-excluded", error, false);
5313
5443
  }
5314
5444
  function eligibleIndexNames(state) {
5315
5445
  const names = [];
@@ -5323,33 +5453,26 @@ function eligibleIndexNames(state) {
5323
5453
  }
5324
5454
  return names;
5325
5455
  }
5326
- function collectEligibleIndexes(state) {
5327
- const eligible = /* @__PURE__ */ new Map();
5328
- for (const name of eligibleIndexNames(state)) {
5329
- const manager = state.executor.getManager(name);
5330
- eligible.set(name, { documentCount: manager?.countDocuments() ?? 0 });
5456
+
5457
+ // src/engine/orchestration/turn.ts
5458
+ function afterCurrentTurn(callback) {
5459
+ if (typeof setImmediate === "function") {
5460
+ setImmediate(callback);
5461
+ return;
5331
5462
  }
5332
- return eligible;
5463
+ setTimeout(callback, 0);
5464
+ }
5465
+ function yieldToEventLoop3() {
5466
+ return new Promise((resolve) => afterCurrentTurn(resolve));
5333
5467
  }
5334
5468
 
5335
5469
  // src/engine/orchestration/replication.ts
5336
- var MAX_PENDING_REPLICATION_DOCUMENTS = 2e4;
5337
5470
  async function dispatchToWorkers(state, action) {
5338
5471
  const pool = state.workerPool;
5339
5472
  if (!pool) return;
5340
- if (action.type === "createIndex") {
5341
- const ineligibility = workerIneligibility(action.indexName, action.config, state.bootstrapModule);
5342
- if (ineligibility) {
5343
- reportIneligible(state, action.indexName, ineligibility);
5344
- return;
5345
- }
5346
- pool.addIndexToAll(action.indexName);
5347
- state.promotedIndexes.add(action.indexName);
5348
- } else if ("indexName" in action && !state.promotedIndexes.has(action.indexName)) {
5349
- return;
5350
- }
5351
- const allExecutors = pool.getAllExecutors();
5352
- const results = await Promise.allSettled(allExecutors.map((workerExecutor) => workerExecutor.execute(action)));
5473
+ const executors = "indexName" in action ? pool.executorsHolding(action.indexName) : pool.getAllExecutors();
5474
+ if ("indexName" in action && !state.scaledOutIndexes.has(action.indexName)) return;
5475
+ const results = await Promise.allSettled(executors.map((workerExecutor) => workerExecutor.execute(action)));
5353
5476
  for (const result of results) {
5354
5477
  if (result.status === "rejected") {
5355
5478
  if (action.type === "insert" && alreadyPresentOnWorker(result.reason)) {
@@ -5417,6 +5540,13 @@ function recordSegmentBroadcast(state, action) {
5417
5540
  }
5418
5541
  return;
5419
5542
  }
5543
+ if (action.type === "freezeLiveTail") {
5544
+ partitionLedger(state, action.indexName, action.partitionId).push({
5545
+ segmentId: action.snapshot.segmentId,
5546
+ documentCount: action.snapshot.documentCount
5547
+ });
5548
+ return;
5549
+ }
5420
5550
  if (action.type === "swapSegments") {
5421
5551
  const entries = partitionLedger(state, action.indexName, action.partitionId);
5422
5552
  const dropped = new Set(action.dropSegmentIds);
@@ -5429,117 +5559,200 @@ function recordSegmentBroadcast(state, action) {
5429
5559
  function queueFor(state, indexName) {
5430
5560
  let queue = state.replicationQueues.get(indexName);
5431
5561
  if (queue === void 0) {
5432
- queue = { tail: Promise.resolve(), pendingActions: 0, pendingDocuments: 0 };
5562
+ queue = { tail: Promise.resolve(), inFlight: [], pendingActions: 0, pendingDocuments: 0, drainWaiters: [] };
5433
5563
  state.replicationQueues.set(indexName, queue);
5434
5564
  }
5435
5565
  return queue;
5436
5566
  }
5437
- function enqueue(state, indexName, action, weight) {
5567
+ function dispatchGate(queue) {
5568
+ if (queue.inFlight.length < REPLICATION_WINDOW) return Promise.resolve();
5569
+ return queue.inFlight[queue.inFlight.length - REPLICATION_WINDOW];
5570
+ }
5571
+ function enqueueReplication(state, indexName, action) {
5572
+ const weight = documentWeight(action);
5573
+ recordSegmentBroadcast(state, action);
5438
5574
  const queue = queueFor(state, indexName);
5439
5575
  queue.pendingActions += 1;
5440
5576
  queue.pendingDocuments += weight;
5441
- queue.tail = queue.tail.then(async () => {
5577
+ const run = dispatchGate(queue).then(async () => {
5442
5578
  try {
5443
5579
  await dispatchToWorkers(state, action);
5444
5580
  } catch (err) {
5445
5581
  console.warn("Worker replication failed:", err);
5446
5582
  } finally {
5583
+ const at = queue.inFlight.indexOf(run);
5584
+ if (at !== -1) queue.inFlight.splice(at, 1);
5447
5585
  queue.pendingActions -= 1;
5448
5586
  queue.pendingDocuments -= weight;
5587
+ if (queue.pendingDocuments < MAX_PENDING_REPLICATION_DOCUMENTS && queue.drainWaiters.length > 0) {
5588
+ for (const resume of queue.drainWaiters.splice(0)) resume();
5589
+ }
5449
5590
  if (queue.pendingActions === 0 && state.replicationQueues.get(indexName) === queue) {
5450
5591
  state.replicationQueues.delete(indexName);
5451
5592
  }
5452
5593
  }
5453
5594
  });
5595
+ queue.inFlight.push(run);
5596
+ queue.tail = run;
5454
5597
  }
5455
5598
  async function replicateToWorkers(state, action) {
5456
- if (state.promotionInProgress) {
5457
- state.promotionBuffer.push(detachCallerDocuments(action));
5599
+ if (!("indexName" in action)) {
5600
+ if (state.workerPool === null) return;
5601
+ await awaitReplicationIdle(state);
5602
+ await dispatchToWorkers(state, action);
5458
5603
  return;
5459
5604
  }
5460
- if (state.workerPool === null) return;
5461
5605
  const detached = detachCallerDocuments(action);
5462
- if (!("indexName" in detached)) {
5463
- await awaitReplicationIdle(state);
5464
- await dispatchToWorkers(state, detached);
5606
+ const loading = state.copyLoadBuffers.get(action.indexName);
5607
+ if (loading !== void 0) {
5608
+ queueForCopies(state, detached);
5609
+ if (loading.length % LOAD_BUFFER_YIELD_INTERVAL === 0) await yieldToEventLoop3();
5465
5610
  return;
5466
5611
  }
5467
- const weight = documentWeight(detached);
5468
- const queue = state.replicationQueues.get(detached.indexName);
5469
- if (queue !== void 0 && queue.pendingDocuments + weight > MAX_PENDING_REPLICATION_DOCUMENTS) {
5470
- await queue.tail;
5612
+ const queue = state.replicationQueues.get(action.indexName);
5613
+ if (queue !== void 0 && queue.pendingDocuments + documentWeight(detached) > MAX_PENDING_REPLICATION_DOCUMENTS) {
5614
+ await new Promise((resume) => queue.drainWaiters.push(resume));
5615
+ }
5616
+ queueForCopies(state, detached);
5617
+ }
5618
+ function queueForCopies(state, action) {
5619
+ if (!("indexName" in action)) return;
5620
+ const loading = state.copyLoadBuffers.get(action.indexName);
5621
+ if (loading !== void 0) {
5622
+ loading.push(action);
5623
+ return;
5471
5624
  }
5472
- recordSegmentBroadcast(state, detached);
5473
- enqueue(state, detached.indexName, detached, weight);
5625
+ if (state.workerPool === null || !state.scaledOutIndexes.has(action.indexName)) return;
5626
+ enqueueReplication(state, action.indexName, action);
5627
+ }
5628
+ function settled(queue) {
5629
+ return Promise.all([...queue.inFlight, queue.tail]).then(() => void 0);
5474
5630
  }
5475
5631
  async function awaitReplicationIdle(state, indexName) {
5476
5632
  if (indexName !== void 0) {
5477
5633
  const queue = state.replicationQueues.get(indexName);
5478
- if (queue !== void 0) await queue.tail;
5634
+ if (queue !== void 0) await settled(queue);
5479
5635
  return;
5480
5636
  }
5481
5637
  while (state.replicationQueues.size > 0) {
5482
- const tails = Array.from(state.replicationQueues.values(), (queue) => queue.tail);
5483
- await Promise.all(tails);
5638
+ await Promise.all(Array.from(state.replicationQueues.values(), settled));
5484
5639
  }
5485
5640
  }
5486
- async function drainPromotionBuffer(state) {
5487
- while (state.promotionBuffer.length > 0) {
5488
- const action = state.promotionBuffer.shift();
5489
- if (action === void 0) return;
5490
- await dispatchToWorkers(state, action);
5641
+ async function awaitWritesApplied(state, indexName) {
5642
+ let awaitedTransition;
5643
+ let awaitedRepair = null;
5644
+ for (; ; ) {
5645
+ const transition = state.copyTransitions.get(indexName);
5646
+ if (transition !== void 0 && transition !== awaitedTransition) {
5647
+ awaitedTransition = transition;
5648
+ await transition.done;
5649
+ continue;
5650
+ }
5651
+ const repair = state.poolRepair;
5652
+ if (repair !== null && repair !== awaitedRepair) {
5653
+ awaitedRepair = repair;
5654
+ await repair;
5655
+ continue;
5656
+ }
5657
+ await awaitReplicationIdle(state, indexName);
5658
+ return;
5659
+ }
5660
+ }
5661
+
5662
+ // src/engine/orchestration/live-tail.ts
5663
+ function liveTailCount(manager, partitionId) {
5664
+ if (partitionId >= manager.partitionCount) return 0;
5665
+ const partition = manager.getPartition(partitionId);
5666
+ return isCompositePartition(partition) ? partition.live.count() : partition.count();
5667
+ }
5668
+ function freezeTail(state, indexName, manager, partitionId) {
5669
+ const segment = manager.freezeLiveTail(partitionId, (payload, documents) => {
5670
+ const snapshot = freezeSegmentShared(payload, documents);
5671
+ return snapshot === null ? null : createSharedFrozenSegment(snapshot);
5672
+ });
5673
+ if (segment === null || segment.sharedSnapshot === null) return;
5674
+ queueForCopies(state, {
5675
+ type: "freezeLiveTail",
5676
+ indexName,
5677
+ partitionId,
5678
+ snapshot: segment.sharedSnapshot,
5679
+ requestId: createRequestId()
5680
+ });
5681
+ }
5682
+ function freezeLiveTail(state, indexName, manager, partitionId, floor) {
5683
+ if (!state.scaledOutIndexes.has(indexName)) return;
5684
+ if (liveTailCount(manager, partitionId) < floor) return;
5685
+ try {
5686
+ freezeTail(state, indexName, manager, partitionId);
5687
+ } catch (err) {
5688
+ console.warn(`Freezing the live tail of partition ${partitionId} of "${indexName}" failed:`, err);
5689
+ }
5690
+ }
5691
+ function flushGrownTails(state, indexName) {
5692
+ const manager = state.executor.getManager(indexName);
5693
+ if (manager === void 0) return;
5694
+ for (let partitionId = 0; partitionId < manager.partitionCount; partitionId++) {
5695
+ freezeLiveTail(state, indexName, manager, partitionId, LIVE_TAIL_FLUSH_DOCUMENTS);
5491
5696
  }
5492
5697
  }
5493
5698
 
5494
5699
  // src/engine/orchestration/compaction.ts
5495
- var COMPACTION_SEGMENT_TRIGGER = 8;
5496
5700
  function smallestSegmentIds(entries) {
5497
5701
  return [...entries].sort((a, b) => a.documentCount - b.documentCount).slice(0, COMPACTION_SEGMENT_TRIGGER).map((entry) => entry.segmentId);
5498
5702
  }
5703
+ function pickSegmentIds(entries, policy) {
5704
+ if (policy === "idle") {
5705
+ return entries.length >= 2 ? entries.map((entry) => entry.segmentId) : null;
5706
+ }
5707
+ return entries.length >= COMPACTION_SEGMENT_TRIGGER ? smallestSegmentIds(entries) : null;
5708
+ }
5499
5709
  function mainCompositeOf(manager, partitionId) {
5500
5710
  if (partitionId >= manager.partitionCount) return null;
5501
5711
  const partition = manager.getPartition(partitionId);
5502
5712
  return isCompositePartition(partition) ? partition : null;
5503
5713
  }
5504
- function candidateSegmentIds(state, indexName, manager, partitionId) {
5714
+ function candidateSegmentIds(state, indexName, manager, partitionId, policy) {
5505
5715
  const composite = mainCompositeOf(manager, partitionId);
5506
5716
  if (composite !== null) {
5507
5717
  const sizes = composite.frozenSegmentSizes();
5508
- if (sizes.length >= COMPACTION_SEGMENT_TRIGGER) {
5509
- return smallestSegmentIds(
5510
- sizes.map((size) => ({ segmentId: size.segmentId, documentCount: size.liveDocumentCount }))
5511
- );
5512
- }
5718
+ const picks = pickSegmentIds(
5719
+ sizes.map((size) => ({ segmentId: size.segmentId, documentCount: size.liveDocumentCount })),
5720
+ policy
5721
+ );
5722
+ if (picks !== null) return picks;
5513
5723
  }
5514
5724
  const ledger = state.segmentLedger.get(indexName)?.get(partitionId);
5515
- if (ledger !== void 0 && ledger.length >= COMPACTION_SEGMENT_TRIGGER) {
5516
- return smallestSegmentIds(ledger);
5517
- }
5518
- return null;
5725
+ return ledger === void 0 ? null : pickSegmentIds(ledger, policy);
5519
5726
  }
5520
5727
  function mainHoldsAll(composite, segmentIds) {
5521
5728
  if (composite === null) return false;
5522
5729
  const held = new Set(composite.frozenSegmentSizes().map((size) => size.segmentId));
5523
5730
  return segmentIds.every((segmentId) => held.has(segmentId));
5524
5731
  }
5525
- async function compactPartitionSegments2(state, indexName, manager, partitionId, segmentIds) {
5526
- await awaitReplicationIdle(state, indexName);
5527
- let snapshot = null;
5732
+ async function compactOnWorker(state, indexName, partitionId, segmentIds) {
5528
5733
  const pool = state.workerPool;
5529
- const executors = pool !== null && state.promotedIndexes.has(indexName) ? pool.getAllExecutors() : [];
5530
- if (executors.length > 0) {
5531
- try {
5532
- snapshot = await executors[0].execute({
5533
- type: "compactSegments",
5534
- indexName,
5535
- partitionId,
5536
- segmentIds: [...segmentIds],
5537
- requestId: createRequestId()
5538
- });
5539
- } catch (err) {
5540
- console.warn("Worker segment compaction failed, compacting on the main thread:", err);
5541
- }
5734
+ if (pool === null || !state.scaledOutIndexes.has(indexName)) return null;
5735
+ const lease = pool.leaseLeastBusy();
5736
+ if (lease === null) return null;
5737
+ try {
5738
+ return await lease.executor.execute({
5739
+ type: "compactSegments",
5740
+ indexName,
5741
+ partitionId,
5742
+ segmentIds: [...segmentIds],
5743
+ requestId: createRequestId()
5744
+ });
5745
+ } catch (err) {
5746
+ console.warn("Worker segment compaction failed, compacting on the main thread:", err);
5747
+ return null;
5748
+ } finally {
5749
+ lease.release();
5542
5750
  }
5751
+ }
5752
+ async function compactPartitionSegments2(state, indexName, manager, partitionId, segmentIds, policy) {
5753
+ await awaitReplicationIdle(state, indexName);
5754
+ if (policy === "idle" && !state.scaledOutIndexes.has(indexName)) return false;
5755
+ let snapshot = await compactOnWorker(state, indexName, partitionId, segmentIds);
5543
5756
  const composite = mainCompositeOf(manager, partitionId);
5544
5757
  const holdsAll = mainHoldsAll(composite, segmentIds);
5545
5758
  let replacement = null;
@@ -5566,30 +5779,52 @@ async function compactPartitionSegments2(state, indexName, manager, partitionId,
5566
5779
  }
5567
5780
  return true;
5568
5781
  }
5569
- async function compactIndexSegments(state, indexName) {
5782
+ async function compactIndexSegments(state, indexName, policy) {
5570
5783
  const manager = state.executor.getManager(indexName);
5571
5784
  if (!manager) return;
5572
5785
  const partitionIds = /* @__PURE__ */ new Set();
5573
5786
  for (let partitionId = 0; partitionId < manager.partitionCount; partitionId++) partitionIds.add(partitionId);
5574
5787
  for (const partitionId of state.segmentLedger.get(indexName)?.keys() ?? []) partitionIds.add(partitionId);
5575
5788
  for (const partitionId of partitionIds) {
5576
- let picks = candidateSegmentIds(state, indexName, manager, partitionId);
5789
+ if (policy === "idle") {
5790
+ await awaitReplicationIdle(state, indexName);
5791
+ freezeLiveTail(state, indexName, manager, partitionId, LIVE_TAIL_FREEZE_FLOOR);
5792
+ }
5793
+ let picks = candidateSegmentIds(state, indexName, manager, partitionId, policy);
5577
5794
  while (picks !== null) {
5578
- const compacted = await compactPartitionSegments2(state, indexName, manager, partitionId, picks);
5795
+ const compacted = await compactPartitionSegments2(state, indexName, manager, partitionId, picks, policy);
5579
5796
  if (!compacted) break;
5580
- picks = candidateSegmentIds(state, indexName, manager, partitionId);
5797
+ picks = candidateSegmentIds(state, indexName, manager, partitionId, policy);
5581
5798
  }
5582
5799
  }
5583
5800
  }
5584
- function maybeCompactSegments(state, indexName) {
5585
- if (state.compactionsInFlight.has(indexName)) return;
5586
- const run = compactIndexSegments(state, indexName).catch((err) => {
5801
+ function maybeCompactSegments(state, indexName, policy = "ingest") {
5802
+ if (state.compactionsInFlight.has(indexName)) {
5803
+ if (policy === "idle") scheduleIdleMerge(state, indexName);
5804
+ return;
5805
+ }
5806
+ const run = compactIndexSegments(state, indexName, policy).catch((err) => {
5587
5807
  console.warn("Segment compaction failed:", err);
5588
5808
  }).finally(() => {
5589
5809
  state.compactionsInFlight.delete(indexName);
5590
5810
  });
5591
5811
  state.compactionsInFlight.set(indexName, run);
5592
5812
  }
5813
+ function scheduleIdleMerge(state, indexName) {
5814
+ cancelIdleMerge(state, indexName);
5815
+ const timer = setTimeout(() => {
5816
+ state.idleMergeTimers.delete(indexName);
5817
+ maybeCompactSegments(state, indexName, "idle");
5818
+ }, IDLE_MERGE_DELAY_MS);
5819
+ if (typeof timer.unref === "function") timer.unref();
5820
+ state.idleMergeTimers.set(indexName, timer);
5821
+ }
5822
+ function cancelIdleMerge(state, indexName) {
5823
+ const timer = state.idleMergeTimers.get(indexName);
5824
+ if (timer === void 0) return;
5825
+ clearTimeout(timer);
5826
+ state.idleMergeTimers.delete(indexName);
5827
+ }
5593
5828
  async function awaitCompactions(state) {
5594
5829
  while (state.compactionsInFlight.size > 0) {
5595
5830
  await Promise.all([...state.compactionsInFlight.values()]);
@@ -5597,10 +5832,6 @@ async function awaitCompactions(state) {
5597
5832
  }
5598
5833
 
5599
5834
  // src/workers/worker-executor.ts
5600
- var DEFAULT_BACKPRESSURE_LIMIT = 100;
5601
- var DEFAULT_REQUEST_TIMEOUT = 3e4;
5602
- var SHUTDOWN_TIMEOUT = 5e3;
5603
- var CONSECUTIVE_TIMEOUTS_BEFORE_DEATH = 3;
5604
5835
  function errorFromEventLike(event) {
5605
5836
  if (event instanceof Error) {
5606
5837
  return event;
@@ -5618,7 +5849,7 @@ function errorFromEventLike(event) {
5618
5849
  }
5619
5850
  function createWorkerExecutor(worker, config) {
5620
5851
  const backpressureLimit = config?.backpressureLimit ?? DEFAULT_BACKPRESSURE_LIMIT;
5621
- const requestTimeout = config?.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
5852
+ const requestTimeout = config?.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT_MS;
5622
5853
  const pending = /* @__PURE__ */ new Map();
5623
5854
  let deathError = null;
5624
5855
  let shutdownRequested = false;
@@ -5667,7 +5898,7 @@ function createWorkerExecutor(worker, config) {
5667
5898
  });
5668
5899
  worker.addEventListener("error", (event) => handleDeath(errorFromEventLike(event)));
5669
5900
  }
5670
- function execute(action) {
5901
+ function execute(action, transfer) {
5671
5902
  if (deathError !== null) {
5672
5903
  return Promise.reject(deathError);
5673
5904
  }
@@ -5692,7 +5923,8 @@ function createWorkerExecutor(worker, config) {
5692
5923
  reject,
5693
5924
  timeoutId
5694
5925
  });
5695
- worker.postMessage(taggedAction);
5926
+ if (transfer === void 0) worker.postMessage(taggedAction);
5927
+ else worker.postMessage(taggedAction, transfer);
5696
5928
  });
5697
5929
  }
5698
5930
  async function shutdown() {
@@ -5706,7 +5938,7 @@ function createWorkerExecutor(worker, config) {
5706
5938
  const timeoutId = setTimeout(() => {
5707
5939
  pending.delete(requestId);
5708
5940
  reject(new NarsilError(ErrorCodes.WORKER_TIMEOUT, "Shutdown timed out"));
5709
- }, SHUTDOWN_TIMEOUT);
5941
+ }, SHUTDOWN_TIMEOUT_MS);
5710
5942
  pending.set(requestId, {
5711
5943
  resolve: () => resolve(),
5712
5944
  reject,
@@ -5741,8 +5973,11 @@ async function createWorkerFactory(entryPoint) {
5741
5973
  const resolvedEntry = resolveEntryPoint();
5742
5974
  if (runtime.supportsWorkerThreads) {
5743
5975
  const workerThreadsModule = await import('worker_threads');
5744
- return function nodeFactory(_workerId, onDeath) {
5745
- const instance = new workerThreadsModule.Worker(new URL(resolvedEntry));
5976
+ return function nodeFactory(workerId, onDeath) {
5977
+ const instance = new workerThreadsModule.Worker(new URL(resolvedEntry), {
5978
+ resourceLimits: WORKER_RESOURCE_LIMITS,
5979
+ workerData: { workerId }
5980
+ });
5746
5981
  return createWorkerExecutor(instance, { onDeath });
5747
5982
  };
5748
5983
  }
@@ -5755,136 +5990,441 @@ async function createWorkerFactory(entryPoint) {
5755
5990
  throw new NarsilError(ErrorCodes.WORKER_CRASHED, `No worker support available on runtime "${runtime.runtime}"`);
5756
5991
  }
5757
5992
 
5758
- // src/engine/orchestration/promotion.ts
5759
- async function bootstrapWorkers(state, pool, indexNames) {
5760
- const allExecutors = pool.getAllExecutors();
5761
- if (state.bootstrapModule !== void 0) {
5762
- const moduleUrl = state.bootstrapModule;
5763
- await Promise.all(
5764
- allExecutors.map(
5765
- (workerExecutor) => workerExecutor.execute({ type: "bootstrap", moduleUrl, requestId: "promote-bootstrap" })
5766
- )
5767
- );
5768
- }
5769
- for (const name of indexNames) {
5770
- const entry = state.indexRegistry.get(name);
5771
- if (!entry) continue;
5772
- await Promise.all(
5773
- allExecutors.map(
5774
- (workerExecutor) => workerExecutor.execute({
5775
- type: "createIndex",
5776
- indexName: name,
5777
- config: entry.config,
5778
- requestId: `promote-create-${name}`
5779
- })
5780
- )
5781
- );
5782
- const manager = state.executor.getManager(name);
5783
- if (!manager) continue;
5784
- for (let partitionId = 0; partitionId < manager.partitionCount; partitionId++) {
5785
- const serialized = manager.serializePartition(partitionId);
5786
- await Promise.all(
5787
- allExecutors.map(
5788
- (workerExecutor) => workerExecutor.execute({
5789
- type: "deserialize",
5790
- indexName: name,
5791
- partitionId,
5792
- data: serialized,
5793
- requestId: `promote-sync-${name}-${partitionId}`
5794
- })
5795
- )
5796
- );
5993
+ // src/engine/orchestration/vector-copies.ts
5994
+ function holdsIndex(state, indexName) {
5995
+ return state.scaledOutIndexes.has(indexName) || state.copyLoadBuffers.has(indexName);
5996
+ }
5997
+ function sharedCopyHostOf(state) {
5998
+ return {
5999
+ get scratchSlotCount() {
6000
+ return state.workerPool?.workerCount ?? state.keywordWorkerCount;
6001
+ },
6002
+ holdsIndex: (indexName) => holdsIndex(state, indexName),
6003
+ resolvePartition: (indexName, docId) => state.executor.getManager(indexName)?.partitionIdOf(docId),
6004
+ async loadShared(indexName, fieldName, handle, copy) {
6005
+ if (!holdsIndex(state, indexName)) return false;
6006
+ await replicateToWorkers(state, {
6007
+ type: "loadVectorCopy",
6008
+ indexName,
6009
+ fieldName,
6010
+ handle,
6011
+ copy,
6012
+ requestId: createRequestId()
6013
+ });
6014
+ await awaitWritesApplied(state, indexName);
6015
+ return state.scaledOutIndexes.has(indexName);
6016
+ },
6017
+ async drop(indexName, fieldName, handle) {
6018
+ if (!holdsIndex(state, indexName)) return;
6019
+ await replicateToWorkers(state, {
6020
+ type: "dropVectorCopy",
6021
+ indexName,
6022
+ fieldName,
6023
+ handle,
6024
+ requestId: createRequestId()
6025
+ });
5797
6026
  }
5798
- }
6027
+ };
5799
6028
  }
5800
- async function runPromotion(state, reason) {
5801
- try {
5802
- const promotable = eligibleIndexNames(state);
5803
- if (promotable.length === 0) {
5804
- state.promotionBuffer.length = 0;
5805
- return;
6029
+ function refreshVectorCopies(state, indexName) {
6030
+ if (state.vectorCopyPolicy?.host === void 0) return;
6031
+ const manager = state.executor.getManager(indexName);
6032
+ if (manager === void 0) return;
6033
+ for (const vectorIndex of manager.getVectorIndexes().values()) vectorIndex.refreshWorkerCopies();
6034
+ }
6035
+
6036
+ // src/engine/orchestration/request-threads.ts
6037
+ async function announceRequestThreads(state, pool) {
6038
+ const listener = state.requestThreads;
6039
+ if (listener === null) return;
6040
+ for (const { workerId, executor } of pool.executorEntries()) {
6041
+ await listener.onWorkerReady(workerId, executor);
6042
+ }
6043
+ }
6044
+ function scheduleRequestThreadPoolRestart(state) {
6045
+ if (state.requestThreads === null) return;
6046
+ const timer = setTimeout(
6047
+ () => {
6048
+ if (state.requestThreads === null || state.workerPool !== null) return;
6049
+ void ensurePool(state).then(() => scaleOutReadyIndexes(state)).catch((err) => {
6050
+ console.warn("Restarting the request threads failed:", err);
6051
+ scheduleRequestThreadPoolRestart(state);
6052
+ });
6053
+ },
6054
+ Math.max(0, state.poolRetryAt - Date.now())
6055
+ );
6056
+ if (typeof timer.unref === "function") timer.unref();
6057
+ }
6058
+ async function serveRequestsOnWorkers(state, listener) {
6059
+ state.requestThreads = listener;
6060
+ state.keywordWorkerCount = resolveRequestThreadCount(state.config?.workers?.count);
6061
+ if (state.vectorCopyPolicy !== void 0) {
6062
+ state.vectorCopyPolicy.host = sharedCopyHostOf(state);
6063
+ state.vectorCopyPolicy.enabled = state.workersEnabled;
6064
+ }
6065
+ if (state.poolStart !== null) await state.poolStart.catch(() => void 0);
6066
+ const running = state.workerPool;
6067
+ if (running !== null && running.workerCount !== state.keywordWorkerCount) {
6068
+ retirePool(state, running);
6069
+ state.poolRetryAt = 0;
6070
+ }
6071
+ const pool = await ensurePool(state);
6072
+ for (const indexName of state.indexRegistry.keys()) {
6073
+ refreshVectorCopies(state, indexName);
6074
+ if (indexReadyForCopies(state, indexName)) {
6075
+ await scaleOutIndex(state, indexName, copyThresholdReason(state, indexName));
5806
6076
  }
5807
- const factory = await createWorkerFactory();
5808
- const pool = createWorkerPool({
5809
- count: state.config?.workers?.count,
5810
- workerFactory: factory,
5811
- onWorkerCrash(workerId, indexNames, error) {
5812
- state.callbacks?.onWorkerCrash?.(workerId, indexNames, error);
5813
- }
5814
- });
5815
- for (const name of promotable) {
5816
- pool.addIndexToAll(name);
5817
- }
5818
- await bootstrapWorkers(state, pool, promotable);
5819
- state.workerPool = pool;
5820
- for (const name of promotable) {
5821
- state.promotedIndexes.add(name);
5822
- state.awaitingBufferedWrites.add(name);
5823
- }
5824
- state.promoter.markPromoted();
5825
- await drainPromotionBuffer(state);
5826
- state.awaitingBufferedWrites.clear();
5827
- state.callbacks?.onPromotion?.(pool.workerCount, reason);
5828
- } catch (err) {
5829
- state.promotionBuffer.length = 0;
5830
- throw err;
5831
- } finally {
5832
- state.awaitingBufferedWrites.clear();
5833
- state.promotionInProgress = false;
5834
6077
  }
6078
+ return pool.workerCount;
5835
6079
  }
5836
- async function promoteBeforeBatch(state, indexName, incomingCount) {
5837
- if (!state.workersEnabled || state.promotionBlocked || state.workerPool || incomingCount <= 0) return;
5838
- if (state.promotionInProgress) {
5839
- if (state.promotionRun) await state.promotionRun;
6080
+ function requestThreadCountOf(state) {
6081
+ return resolveRequestThreadCount(state.config?.workers?.count);
6082
+ }
6083
+ function stopRequestThreads(state) {
6084
+ const listener = state.requestThreads;
6085
+ if (listener === null) return;
6086
+ state.requestThreads = null;
6087
+ const pool = state.workerPool;
6088
+ if (pool === null) return;
6089
+ for (const { workerId } of pool.executorEntries()) listener.onWorkerGone(workerId);
6090
+ }
6091
+
6092
+ // src/engine/orchestration/repair.ts
6093
+ var COPY_RESTART_REASON = "A request arrived after every worker crashed and the restart delay passed";
6094
+ function nextRestartDelay(state) {
6095
+ const delay2 = state.poolRetryDelayMs;
6096
+ state.poolRetryDelayMs = Math.min(delay2 * 2, POOL_RESTART_DELAY_MAX_MS);
6097
+ return delay2;
6098
+ }
6099
+ function deferPoolRestart(state) {
6100
+ state.poolRetryAt = Date.now() + nextRestartDelay(state);
6101
+ }
6102
+ function retirePool(state, pool) {
6103
+ if (state.workerPool !== pool) return;
6104
+ state.workerPool = null;
6105
+ cancelRepair(state);
6106
+ deferPoolRestart(state);
6107
+ for (const indexName of state.scaledOutIndexes) state.droppedCopies.set(indexName, COPY_RESTART_REASON);
6108
+ state.scaledOutIndexes.clear();
6109
+ state.segmentLedger.clear();
6110
+ for (const { workerId } of pool.executorEntries()) state.requestThreads?.onWorkerGone(workerId);
6111
+ void pool.shutdown().catch(() => void 0);
6112
+ scheduleRequestThreadPoolRestart(state);
6113
+ }
6114
+ function handleWorkerCrash(state, pool, workerId, indexNames, error) {
6115
+ state.callbacks?.onWorkerCrash?.(workerId, indexNames, error);
6116
+ state.requestThreads?.onWorkerGone(workerId);
6117
+ if (pool.getAllExecutors().length === 0) {
6118
+ retirePool(state, pool);
5840
6119
  return;
5841
6120
  }
5842
- if (state.callbacks?.shouldDeferPromotion?.()) return;
5843
- const indexMap = collectEligibleIndexes(state);
5844
- const anticipated = indexMap.get(indexName);
5845
- if (anticipated === void 0) return;
5846
- anticipated.documentCount += incomingCount;
5847
- const result = state.promoter.check(indexMap);
5848
- if (!result.shouldPromote) return;
5849
- state.promotionInProgress = true;
5850
- const run = runPromotion(state, result.reason).catch((err) => {
5851
- const error = toError3(err);
5852
- state.promotionBlocked = isDeterministicFailure(error);
5853
- state.promotionInProgress = false;
5854
- state.callbacks?.onPromotionFailure?.(result.reason, error, !state.promotionBlocked);
5855
- });
5856
- const tracked = run.then(() => {
5857
- state.promotionRun = null;
5858
- });
5859
- state.promotionRun = tracked;
5860
- await tracked;
5861
- }
5862
- async function checkPromotion(state) {
5863
- if (!state.workersEnabled || state.promotionInProgress || state.promotionBlocked || state.workerPool) return;
5864
- if (state.callbacks?.shouldDeferPromotion?.()) return;
5865
- const indexMap = collectEligibleIndexes(state);
5866
- if (indexMap.size === 0) return;
5867
- const result = state.promoter.check(indexMap);
5868
- if (!result.shouldPromote) return;
5869
- state.promotionInProgress = true;
5870
- setTimeout(() => {
5871
- const run = runPromotion(state, result.reason).catch((err) => {
5872
- const error = toError3(err);
5873
- state.promotionBlocked = isDeterministicFailure(error);
5874
- state.promotionInProgress = false;
5875
- state.callbacks?.onPromotionFailure?.(result.reason, error, !state.promotionBlocked);
5876
- });
5877
- state.promotionRun = run.then(() => {
5878
- state.promotionRun = null;
5879
- });
5880
- }, 0);
6121
+ scheduleRepair(state, pool);
6122
+ }
6123
+ function scheduleRepair(state, pool) {
6124
+ if (state.workerPool !== pool || state.repairTimer !== null || state.poolRepair !== null) return;
6125
+ const timer = setTimeout(() => {
6126
+ state.repairTimer = null;
6127
+ void repairPool(state, pool);
6128
+ }, state.poolRetryDelayMs);
6129
+ if (typeof timer.unref === "function") timer.unref();
6130
+ state.repairTimer = timer;
6131
+ }
6132
+ function cancelRepair(state) {
6133
+ if (state.repairTimer === null) return;
6134
+ clearTimeout(state.repairTimer);
6135
+ state.repairTimer = null;
6136
+ }
6137
+ async function loadOntoReplacement(state, replacement, indexName) {
6138
+ const entry = state.indexRegistry.get(indexName);
6139
+ const manager = state.executor.getManager(indexName);
6140
+ if (entry === void 0 || manager === void 0 || !state.scaledOutIndexes.has(indexName)) return;
6141
+ const buffered = [];
6142
+ state.copyLoadBuffers.set(indexName, buffered);
6143
+ try {
6144
+ await loadIndexOntoWorkers(
6145
+ indexName,
6146
+ [replacement.executor],
6147
+ entry.config,
6148
+ manager,
6149
+ void 0,
6150
+ state.callbacks?.isAnalysisStale?.(indexName)
6151
+ );
6152
+ replacement.hold(indexName);
6153
+ } finally {
6154
+ state.copyLoadBuffers.delete(indexName);
6155
+ for (const action of buffered) enqueueReplication(state, indexName, action);
6156
+ }
6157
+ refreshVectorCopies(state, indexName);
6158
+ }
6159
+ async function replaceWorker(state, pool, workerId) {
6160
+ const replacement = pool.spawnReplacement(workerId);
6161
+ if (replacement === null) return;
6162
+ try {
6163
+ if (state.bootstrapModule !== void 0) {
6164
+ await replacement.executor.execute({
6165
+ type: "bootstrap",
6166
+ moduleUrl: state.bootstrapModule,
6167
+ requestId: createRequestId()
6168
+ });
6169
+ }
6170
+ for (const indexName of [...state.scaledOutIndexes]) {
6171
+ await loadOntoReplacement(state, replacement, indexName);
6172
+ }
6173
+ replacement.admit();
6174
+ await state.requestThreads?.onWorkerReady(workerId, replacement.executor);
6175
+ } catch (err) {
6176
+ replacement.abandon();
6177
+ throw err;
6178
+ }
6179
+ }
6180
+ async function replaceDeadWorkers(state, pool) {
6181
+ for (const workerId of pool.deadWorkerIds()) {
6182
+ if (state.workerPool !== pool) return;
6183
+ await replaceWorker(state, pool, workerId);
6184
+ }
6185
+ state.poolRetryDelayMs = POOL_RESTART_DELAY_MS;
6186
+ }
6187
+ function repairPool(state, pool) {
6188
+ if (state.workerPool !== pool) return Promise.resolve();
6189
+ if (state.poolRepair !== null) return state.poolRepair;
6190
+ const run = replaceDeadWorkers(state, pool).catch((err) => {
6191
+ nextRestartDelay(state);
6192
+ console.warn("Replacing a crashed worker failed:", err);
6193
+ }).finally(() => {
6194
+ state.poolRepair = null;
6195
+ if (pool.deadWorkerIds().length > 0) scheduleRepair(state, pool);
6196
+ });
6197
+ state.poolRepair = run;
6198
+ return run;
6199
+ }
6200
+
6201
+ // src/engine/orchestration/scale-out.ts
6202
+ var COPY_RELOAD_REASON = "A request arrived after an idle spell dropped the worker copies";
6203
+ function copiesAllowed(state) {
6204
+ if (!state.workersEnabled || state.scaleOutBlocked || state.shuttingDown) return false;
6205
+ return state.workerPool !== null || Date.now() >= state.poolRetryAt;
6206
+ }
6207
+ async function startPool(state) {
6208
+ eligibleIndexNames(state);
6209
+ const factory = await createWorkerFactory();
6210
+ let started = null;
6211
+ const pool = createWorkerPool({
6212
+ count: state.keywordWorkerCount,
6213
+ workerFactory: factory,
6214
+ onWorkerCrash(workerId, indexNames, error) {
6215
+ if (started !== null) handleWorkerCrash(state, started, workerId, indexNames, error);
6216
+ }
6217
+ });
6218
+ started = pool;
6219
+ try {
6220
+ pool.spawnAll();
6221
+ if (state.bootstrapModule !== void 0) {
6222
+ const moduleUrl = state.bootstrapModule;
6223
+ await Promise.all(
6224
+ pool.getAllExecutors().map(
6225
+ (workerExecutor) => workerExecutor.execute({ type: "bootstrap", moduleUrl, requestId: "copies-bootstrap" })
6226
+ )
6227
+ );
6228
+ }
6229
+ } catch (err) {
6230
+ await pool.shutdown().catch(() => void 0);
6231
+ throw err;
6232
+ }
6233
+ return pool;
6234
+ }
6235
+ async function ensurePool(state) {
6236
+ if (state.workerPool !== null) return state.workerPool;
6237
+ if (state.poolStart === null) {
6238
+ state.poolStart = startPool(state).then(
6239
+ async (pool) => {
6240
+ state.workerPool = pool;
6241
+ state.poolStart = null;
6242
+ try {
6243
+ await announceRequestThreads(state, pool);
6244
+ } catch (err) {
6245
+ retirePool(state, pool);
6246
+ throw err;
6247
+ }
6248
+ return pool;
6249
+ },
6250
+ (err) => {
6251
+ state.poolStart = null;
6252
+ deferPoolRestart(state);
6253
+ throw err;
6254
+ }
6255
+ );
6256
+ }
6257
+ return state.poolStart;
6258
+ }
6259
+ function documentCountOf(state, indexName) {
6260
+ return state.executor.getManager(indexName)?.countDocuments() ?? 0;
6261
+ }
6262
+ function copyThresholdReason(state, indexName) {
6263
+ return `Index "${indexName}" holds ${documentCountOf(state, indexName)} documents, reaching the copy threshold of ${state.copyThreshold}`;
6264
+ }
6265
+ function indexReadyForCopies(state, indexName, incomingCount = 0) {
6266
+ if (!copiesAllowed(state)) return false;
6267
+ if (state.scaledOutIndexes.has(indexName) || state.desyncedIndexes.has(indexName)) return false;
6268
+ if (state.copyTransitions.has(indexName)) return false;
6269
+ if (state.callbacks?.shouldDeferCopies?.()) return false;
6270
+ const entry = state.indexRegistry.get(indexName);
6271
+ if (entry === void 0 || state.executor.getManager(indexName) === void 0) return false;
6272
+ if (documentCountOf(state, indexName) + incomingCount < state.copyThreshold) return false;
6273
+ const ineligibility = workerIneligibility(indexName, entry.config, state.bootstrapModule);
6274
+ if (ineligibility !== null) {
6275
+ reportIneligible(state, indexName, ineligibility);
6276
+ return false;
6277
+ }
6278
+ return true;
6279
+ }
6280
+ async function loadCopies(state, indexName, reason) {
6281
+ const entry = state.indexRegistry.get(indexName);
6282
+ const manager = state.executor.getManager(indexName);
6283
+ if (entry === void 0 || manager === void 0) return;
6284
+ const ineligibility = workerIneligibility(indexName, entry.config, state.bootstrapModule);
6285
+ if (ineligibility !== null) {
6286
+ reportIneligible(state, indexName, ineligibility);
6287
+ return;
6288
+ }
6289
+ const pool = await ensurePool(state);
6290
+ if (state.executor.getManager(indexName) === void 0) return;
6291
+ const dropReason = state.droppedCopies.get(indexName);
6292
+ const reload = state.droppedCopies.delete(indexName);
6293
+ const buffered = [];
6294
+ state.copyLoadBuffers.set(indexName, buffered);
6295
+ try {
6296
+ await transferIndexToPool(indexName, pool, entry.config, manager, state.callbacks?.isAnalysisStale?.(indexName));
6297
+ state.scaledOutIndexes.add(indexName);
6298
+ state.lastAccessAt.set(indexName, Date.now());
6299
+ state.poolRetryDelayMs = POOL_RESTART_DELAY_MS;
6300
+ for (const action of buffered) enqueueReplication(state, indexName, action);
6301
+ } catch (err) {
6302
+ if (dropReason !== void 0) state.droppedCopies.set(indexName, dropReason);
6303
+ throw err;
6304
+ } finally {
6305
+ state.copyLoadBuffers.delete(indexName);
6306
+ }
6307
+ if (reload) state.copyReloadCounts.set(indexName, (state.copyReloadCounts.get(indexName) ?? 0) + 1);
6308
+ refreshVectorCopies(state, indexName);
6309
+ scheduleIdleMerge(state, indexName);
6310
+ state.callbacks?.onCopiesLoaded?.(pool.workerCount, reason);
6311
+ }
6312
+ async function loadAfter(previous, state, indexName, reason) {
6313
+ if (previous !== void 0) await previous.done;
6314
+ if (state.poolRepair !== null) await state.poolRepair;
6315
+ if (!copiesAllowed(state)) return;
6316
+ if (state.scaledOutIndexes.has(indexName) || state.desyncedIndexes.has(indexName)) return;
6317
+ await loadCopies(state, indexName, reason);
6318
+ }
6319
+ function scaleOutIndex(state, indexName, reason) {
6320
+ const previous = state.copyTransitions.get(indexName);
6321
+ if (previous !== void 0 && previous.kind !== "drop") return previous.done;
6322
+ const kind = state.droppedCopies.has(indexName) ? "reload" : "load";
6323
+ const done = loadAfter(previous, state, indexName, reason).catch((err) => {
6324
+ const error = toError3(err);
6325
+ state.scaleOutBlocked = isDeterministicFailure(error);
6326
+ state.callbacks?.onCopyLoadFailure?.(reason, error, !state.scaleOutBlocked);
6327
+ }).finally(() => {
6328
+ if (state.copyTransitions.get(indexName)?.done === done) state.copyTransitions.delete(indexName);
6329
+ });
6330
+ state.copyTransitions.set(indexName, { kind, done });
6331
+ return done;
6332
+ }
6333
+ async function scaleOutReadyIndexes(state) {
6334
+ if (!copiesAllowed(state)) return;
6335
+ for (const indexName of state.indexRegistry.keys()) {
6336
+ if (!indexReadyForCopies(state, indexName)) continue;
6337
+ const reason = copyThresholdReason(state, indexName);
6338
+ setTimeout(() => {
6339
+ void scaleOutIndex(state, indexName, reason);
6340
+ }, 0);
6341
+ }
6342
+ }
6343
+ async function scaleOutBeforeBatch(state, indexName, incomingCount) {
6344
+ if (incomingCount <= 0) return;
6345
+ const pending = state.copyTransitions.get(indexName);
6346
+ if (pending !== void 0) {
6347
+ if (pending.kind === "load") await pending.done;
6348
+ return;
6349
+ }
6350
+ if (!indexReadyForCopies(state, indexName, incomingCount)) return;
6351
+ await scaleOutIndex(state, indexName, copyThresholdReason(state, indexName));
6352
+ }
6353
+
6354
+ // src/engine/orchestration/idle.ts
6355
+ function isIndexBusy(state, indexName) {
6356
+ return state.poolRepair !== null || state.copyTransitions.has(indexName) || state.replicationQueues.has(indexName) || state.compactionsInFlight.has(indexName);
6357
+ }
6358
+ function noteAccess(state, indexName) {
6359
+ state.lastAccessAt.set(indexName, Date.now());
6360
+ if (!copiesAllowed(state)) return;
6361
+ const reloadReason = state.droppedCopies.get(indexName);
6362
+ if (reloadReason !== void 0) {
6363
+ const transition = state.copyTransitions.get(indexName);
6364
+ if (transition !== void 0 && transition.kind !== "drop") return;
6365
+ void scaleOutIndex(state, indexName, reloadReason);
6366
+ return;
6367
+ }
6368
+ if (indexReadyForCopies(state, indexName)) {
6369
+ void scaleOutIndex(state, indexName, copyThresholdReason(state, indexName));
6370
+ }
6371
+ }
6372
+ async function releaseCopies(state, indexName) {
6373
+ const pool = state.workerPool;
6374
+ if (pool === null) return;
6375
+ await awaitReplicationIdle(state, indexName);
6376
+ const cutoff = Date.now() - state.copyIdleTimeoutMs;
6377
+ if ((state.lastAccessAt.get(indexName) ?? Date.now()) > cutoff) return;
6378
+ if (!state.scaledOutIndexes.has(indexName) || state.compactionsInFlight.has(indexName)) return;
6379
+ state.scaledOutIndexes.delete(indexName);
6380
+ state.droppedCopies.set(indexName, COPY_RELOAD_REASON);
6381
+ cancelIdleMerge(state, indexName);
6382
+ const outcomes = await Promise.allSettled(
6383
+ pool.executorsHolding(indexName).map((worker) => worker.execute({ type: "dropIndex", indexName, requestId: `idle-drop-${indexName}` }))
6384
+ );
6385
+ for (const outcome of outcomes) {
6386
+ if (outcome.status === "rejected") console.warn("Dropping an idle worker copy failed:", outcome.reason);
6387
+ }
6388
+ pool.removeIndex(indexName);
6389
+ state.segmentLedger.delete(indexName);
6390
+ }
6391
+ async function dropIdleCopies(state, indexName) {
6392
+ const previous = state.copyTransitions.get(indexName);
6393
+ if (previous !== void 0) return;
6394
+ const done = releaseCopies(state, indexName).catch((err) => {
6395
+ console.warn("Dropping idle worker copies failed:", err);
6396
+ }).finally(() => {
6397
+ if (state.copyTransitions.get(indexName)?.done === done) state.copyTransitions.delete(indexName);
6398
+ });
6399
+ state.copyTransitions.set(indexName, { kind: "drop", done });
6400
+ await done;
6401
+ }
6402
+ async function sweepIdleCopies(state) {
6403
+ const cutoff = Date.now() - state.copyIdleTimeoutMs;
6404
+ for (const indexName of [...state.scaledOutIndexes]) {
6405
+ if ((state.lastAccessAt.get(indexName) ?? Date.now()) > cutoff) continue;
6406
+ if (isIndexBusy(state, indexName)) continue;
6407
+ await dropIdleCopies(state, indexName);
6408
+ }
6409
+ }
6410
+ function startIdleSweep(state) {
6411
+ if (!state.workersEnabled || state.idleSweep !== null) return;
6412
+ const interval = Math.min(MAX_SWEEP_INTERVAL_MS, Math.max(MIN_SWEEP_INTERVAL_MS, state.copyIdleTimeoutMs / 2));
6413
+ const timer = setInterval(() => void sweepIdleCopies(state).catch(() => void 0), interval);
6414
+ if (typeof timer.unref === "function") timer.unref();
6415
+ state.idleSweep = timer;
6416
+ }
6417
+ function stopIdleSweep(state) {
6418
+ if (state.idleSweep === null) return;
6419
+ clearInterval(state.idleSweep);
6420
+ state.idleSweep = null;
5881
6421
  }
5882
6422
 
5883
6423
  // src/engine/orchestration/search.ts
5884
- function partitionsPerWorker(totalPartitions, workerCount) {
5885
- const assignments = Array.from({ length: workerCount }, () => []);
5886
- for (let partitionId = 0; partitionId < totalPartitions; partitionId++) {
5887
- assignments[partitionId % workerCount].push(partitionId);
6424
+ function partitionsPerLease(scope, leaseCount) {
6425
+ const assignments = Array.from({ length: leaseCount }, () => []);
6426
+ for (let at = 0; at < scope.length; at++) {
6427
+ assignments[at % leaseCount].push(scope[at]);
5888
6428
  }
5889
6429
  return assignments;
5890
6430
  }
@@ -5902,67 +6442,74 @@ function mergeWorkerResults(results) {
5902
6442
  facets: workerFacets.length > 0 ? mergeFacets(workerFacets) : void 0
5903
6443
  };
5904
6444
  }
6445
+ function scoresPerPartition(state, indexName, params, globalStats) {
6446
+ const mode = params.scoring ?? state.indexRegistry.get(indexName)?.config.defaultScoring ?? "local";
6447
+ if (mode === "local") return true;
6448
+ return mode === "broadcast" && globalStats !== void 0;
6449
+ }
6450
+ function queryAction(indexName, params, globalStats, partitionIds) {
6451
+ return {
6452
+ type: "query",
6453
+ indexName,
6454
+ params,
6455
+ requestId: createRequestId(),
6456
+ ...partitionIds !== void 0 ? { partitionIds } : {},
6457
+ ...globalStats !== void 0 ? { globalStats } : {}
6458
+ };
6459
+ }
6460
+ async function runSplit(leases, scope, indexName, params, globalStats) {
6461
+ const assignments = partitionsPerLease(scope, leases.length);
6462
+ const results = await Promise.all(
6463
+ assignments.map(
6464
+ (partitionIds, at) => leases[at].executor.execute(queryAction(indexName, params, globalStats, partitionIds)).finally(() => leases[at].release())
6465
+ )
6466
+ );
6467
+ return mergeWorkerResults(results);
6468
+ }
6469
+ function takeMainCopyTurn(state) {
6470
+ if (state.mainCopyQueries === "none" || state.mainCopyTurnTaken) return false;
6471
+ state.mainCopyTurnTaken = true;
6472
+ afterCurrentTurn(() => {
6473
+ state.mainCopyTurnTaken = false;
6474
+ });
6475
+ return true;
6476
+ }
6477
+ function answersFasterOnMainCopy(state, pool, manager) {
6478
+ if (pool.queriesInFlight() > 0) return false;
6479
+ if (manager.countDocuments() > MAIN_COPY_LONE_QUERY_DOCUMENTS) return false;
6480
+ return takeMainCopyTurn(state);
6481
+ }
5905
6482
  async function searchViaWorker(state, indexName, params, globalStats, partitionIds) {
5906
6483
  const pool = state.workerPool;
5907
6484
  if (!pool) return null;
5908
- if (!state.promotedIndexes.has(indexName)) return null;
5909
- if (state.awaitingBufferedWrites.has(indexName)) return null;
5910
- const pendingReplication = state.replicationQueues.get(indexName);
5911
- if (pendingReplication !== void 0 && pendingReplication.pendingActions > 0) return null;
6485
+ if (!state.scaledOutIndexes.has(indexName) || state.copyLoadBuffers.has(indexName)) return null;
5912
6486
  const manager = state.executor.getManager(indexName);
5913
6487
  if (!manager) return null;
5914
- const allExecutors = pool.getAllExecutors();
5915
- if (allExecutors.length === 0) return null;
5916
- const stats = globalStats !== void 0 ? { globalStats } : {};
5917
- if (allExecutors.length === 1) {
5918
- try {
5919
- return await allExecutors[0].execute({
5920
- type: "query",
5921
- indexName,
5922
- params,
5923
- requestId: createRequestId(),
5924
- ...partitionIds !== void 0 ? { partitionIds } : {},
5925
- ...stats
5926
- });
5927
- } catch (err) {
5928
- console.warn("Worker search failed, falling back to local:", err);
5929
- return null;
5930
- }
5931
- }
5932
- const assignments = partitionsPerWorker(manager.partitionCount, allExecutors.length);
5933
- const scopedAssignments = partitionIds === void 0 ? assignments : assignments.map((assigned) => assigned.filter((partitionId) => partitionIds.includes(partitionId)));
5934
- const activeAssignments = [];
5935
- for (let index = 0; index < allExecutors.length; index++) {
5936
- if (scopedAssignments[index].length > 0) {
5937
- activeAssignments.push({ executor: allExecutors[index], partitionIds: scopedAssignments[index] });
5938
- }
5939
- }
5940
- if (activeAssignments.length === 0) {
5941
- return { scored: [], totalMatched: 0 };
5942
- }
6488
+ if (answersFasterOnMainCopy(state, pool, manager)) return null;
6489
+ const scope = partitionIds ?? Array.from({ length: manager.partitionCount }, (_, partitionId) => partitionId);
6490
+ const idle = pool.leaseIdle(scope.length);
6491
+ const leases = [];
5943
6492
  try {
5944
- const results = await Promise.all(
5945
- activeAssignments.map(
5946
- (assignment) => assignment.executor.execute({
5947
- type: "query",
5948
- indexName,
5949
- params,
5950
- requestId: createRequestId(),
5951
- partitionIds: assignment.partitionIds,
5952
- ...stats
5953
- })
5954
- )
5955
- );
5956
- return mergeWorkerResults(results);
6493
+ if (idle.length >= 2 && scoresPerPartition(state, indexName, params, globalStats)) {
6494
+ leases.push(...idle);
6495
+ return await runSplit(leases, scope, indexName, params, globalStats);
6496
+ }
6497
+ for (const lease2 of idle.slice(1)) lease2.release();
6498
+ const lease = idle[0] ?? (takeMainCopyTurn(state) ? null : pool.leaseLeastBusy());
6499
+ if (lease === null) return null;
6500
+ leases.push(lease);
6501
+ return await lease.executor.execute(queryAction(indexName, params, globalStats, partitionIds));
5957
6502
  } catch (err) {
5958
- console.warn("Parallel worker search failed, falling back to local:", err);
6503
+ console.warn("Worker search failed, falling back to local:", err);
5959
6504
  return null;
6505
+ } finally {
6506
+ for (const lease of leases) lease.release();
5960
6507
  }
5961
6508
  }
5962
6509
 
5963
6510
  // src/engine/orchestration/segments.ts
5964
6511
  function segmentBuildConcurrency(state, indexName) {
5965
- if (!state.promotedIndexes.has(indexName)) return 0;
6512
+ if (!state.scaledOutIndexes.has(indexName)) return 0;
5966
6513
  return state.workerPool?.workerCount ?? 0;
5967
6514
  }
5968
6515
  async function buildSegments(state, requests) {
@@ -5979,44 +6526,59 @@ async function buildSegments(state, requests) {
5979
6526
  }
5980
6527
 
5981
6528
  // src/engine/orchestration/index.ts
5982
- function createWorkerOrchestrator(config, executor, promoter, indexRegistry, callbacks) {
6529
+ function workersEnabledByDefault() {
6530
+ return detectRuntime().runtime !== "browser";
6531
+ }
6532
+ function copyIdleTimeoutBeforeClose(closeAfterMs) {
6533
+ return closeAfterMs === void 0 ? DEFAULT_COPY_IDLE_TIMEOUT_MS : Math.min(DEFAULT_COPY_IDLE_TIMEOUT_MS, closeAfterMs);
6534
+ }
6535
+ function createWorkerOrchestrator(config, executor, indexRegistry, callbacks, vectorCopyPolicy) {
5983
6536
  const state = {
5984
6537
  config,
5985
6538
  executor,
5986
- promoter,
5987
6539
  indexRegistry,
5988
6540
  callbacks,
5989
- workersEnabled: config?.workers?.enabled === true,
6541
+ vectorCopyPolicy,
6542
+ workersEnabled: config?.workers?.enabled ?? workersEnabledByDefault(),
6543
+ keywordWorkerCount: splitWorkerBudget(resolveWorkerCount(config?.workers?.count)).keyword,
6544
+ requestThreads: null,
6545
+ copyThreshold: config?.workers?.promotionThreshold ?? DEFAULT_COPY_THRESHOLD,
6546
+ copyIdleTimeoutMs: config?.workers?.idleTimeoutMs ?? copyIdleTimeoutBeforeClose(config?.lifecycle?.idleTimeoutMs),
5990
6547
  bootstrapModule: config?.workers?.bootstrapModule,
5991
- promotionBuffer: [],
5992
- awaitingBufferedWrites: /* @__PURE__ */ new Set(),
5993
6548
  reportedIneligible: /* @__PURE__ */ new Set(),
5994
- promotedIndexes: /* @__PURE__ */ new Set(),
6549
+ scaledOutIndexes: /* @__PURE__ */ new Set(),
6550
+ desyncedIndexes: /* @__PURE__ */ new Set(),
6551
+ copyLoadBuffers: /* @__PURE__ */ new Map(),
6552
+ copyTransitions: /* @__PURE__ */ new Map(),
6553
+ droppedCopies: /* @__PURE__ */ new Map(),
6554
+ lastAccessAt: /* @__PURE__ */ new Map(),
6555
+ copyReloadCounts: /* @__PURE__ */ new Map(),
5995
6556
  replicationQueues: /* @__PURE__ */ new Map(),
5996
6557
  segmentLedger: /* @__PURE__ */ new Map(),
5997
6558
  compactionsInFlight: /* @__PURE__ */ new Map(),
6559
+ idleMergeTimers: /* @__PURE__ */ new Map(),
5998
6560
  workerPool: null,
5999
- promotionInProgress: false,
6000
- promotionBlocked: false,
6001
- promotionRun: null
6561
+ poolStart: null,
6562
+ poolRetryAt: 0,
6563
+ poolRetryDelayMs: POOL_RESTART_DELAY_MS,
6564
+ poolRepair: null,
6565
+ mainCopyTurnTaken: false,
6566
+ mainCopyQueries: config?.workers?.mainCopyQueries,
6567
+ repairTimer: null,
6568
+ scaleOutBlocked: false,
6569
+ shuttingDown: false,
6570
+ idleSweep: null
6002
6571
  };
6003
- async function resyncIndex(indexName, wasPromoted) {
6004
- if (state.promotionRun) {
6005
- await state.promotionRun.catch(() => void 0);
6006
- }
6007
- const pool = state.workerPool;
6008
- if (!pool) return;
6009
- if (!wasPromoted && !state.promotedIndexes.has(indexName)) return;
6010
- const entry = indexRegistry.get(indexName);
6011
- if (!entry) return;
6012
- if (workerIneligibility(indexName, entry.config, state.bootstrapModule)) return;
6013
- const manager = executor.getManager(indexName);
6014
- if (!manager) return;
6015
- state.promotedIndexes.delete(indexName);
6016
- await awaitReplicationIdle(state, indexName);
6017
- state.segmentLedger.delete(indexName);
6018
- await transferIndexToPool(indexName, pool, entry.config, manager);
6019
- state.promotedIndexes.add(indexName);
6572
+ startIdleSweep(state);
6573
+ async function resyncIndex(indexName, wasScaledOut) {
6574
+ state.desyncedIndexes.delete(indexName);
6575
+ if (!wasScaledOut) return;
6576
+ await scaleOutIndex(state, indexName, copyThresholdReason(state, indexName));
6577
+ }
6578
+ async function openIndex(indexName) {
6579
+ state.droppedCopies.delete(indexName);
6580
+ if (!indexReadyForCopies(state, indexName)) return;
6581
+ await scaleOutIndex(state, indexName, copyThresholdReason(state, indexName));
6020
6582
  }
6021
6583
  async function getWorkerMemoryStats() {
6022
6584
  if (!state.workerPool) return [];
@@ -6025,91 +6587,140 @@ function createWorkerOrchestrator(config, executor, promoter, indexRegistry, cal
6025
6587
  workerId: report.workerId,
6026
6588
  heapUsed: report.heapUsed,
6027
6589
  heapTotal: report.heapTotal,
6590
+ heapLimit: report.heapLimit,
6028
6591
  external: report.external
6029
6592
  }));
6030
6593
  }
6594
+ function workerCopies() {
6595
+ const reports = [];
6596
+ for (const indexName of indexRegistry.keys()) {
6597
+ reports.push({
6598
+ indexName,
6599
+ scaledOut: state.scaledOutIndexes.has(indexName),
6600
+ reloadCount: state.copyReloadCounts.get(indexName) ?? 0
6601
+ });
6602
+ }
6603
+ return reports;
6604
+ }
6031
6605
  async function shutdown() {
6606
+ state.shuttingDown = true;
6607
+ stopIdleSweep(state);
6608
+ cancelRepair(state);
6609
+ stopRequestThreads(state);
6610
+ for (const indexName of [...state.idleMergeTimers.keys()]) cancelIdleMerge(state, indexName);
6611
+ await Promise.allSettled([...state.copyTransitions.values()].map((transition) => transition.done));
6612
+ if (state.poolRepair !== null) await state.poolRepair;
6613
+ if (state.poolStart !== null) await state.poolStart.catch(() => void 0);
6032
6614
  if (state.workerPool) {
6033
6615
  await awaitCompactions(state);
6034
6616
  await awaitReplicationIdle(state);
6035
6617
  await state.workerPool.shutdown();
6036
6618
  state.workerPool = null;
6037
- state.promotedIndexes.clear();
6619
+ state.scaledOutIndexes.clear();
6038
6620
  state.replicationQueues.clear();
6039
6621
  state.segmentLedger.clear();
6040
6622
  }
6041
6623
  }
6042
- async function awaitPromotionIdle() {
6043
- while (state.promotionInProgress) {
6044
- const run = state.promotionRun;
6045
- if (run !== null) {
6046
- await run;
6047
- } else {
6048
- await new Promise((resolve) => setTimeout(resolve, 0));
6624
+ async function restoreAfterFailedClose(indexName, failure) {
6625
+ const pool = state.workerPool;
6626
+ const entry = indexRegistry.get(indexName);
6627
+ const manager = executor.getManager(indexName);
6628
+ if (pool !== null && entry !== void 0 && manager !== void 0) {
6629
+ try {
6630
+ await transferIndexToPool(indexName, pool, entry.config, manager, callbacks?.isAnalysisStale?.(indexName));
6631
+ state.scaledOutIndexes.add(indexName);
6632
+ refreshVectorCopies(state, indexName);
6633
+ } catch (repairError) {
6634
+ throw new NarsilError(
6635
+ ErrorCodes.WORKER_CRASHED,
6636
+ `Worker copies for index "${indexName}" could not be restored after close failed`,
6637
+ {
6638
+ indexName,
6639
+ closeError: failure.reason instanceof Error ? failure.reason.message : String(failure.reason),
6640
+ repairError: repairError instanceof Error ? repairError.message : String(repairError)
6641
+ }
6642
+ );
6049
6643
  }
6050
6644
  }
6645
+ if (failure.reason instanceof NarsilError) throw failure.reason;
6646
+ throw new NarsilError(ErrorCodes.WORKER_CRASHED, `A worker could not close index "${indexName}"`, {
6647
+ indexName,
6648
+ cause: failure.reason instanceof Error ? failure.reason.message : String(failure.reason)
6649
+ });
6650
+ }
6651
+ function withdrawCopies(indexName) {
6652
+ const pool = state.workerPool;
6653
+ if (pool === null) return;
6654
+ const holders = pool.executorsHolding(indexName);
6655
+ pool.removeIndex(indexName);
6656
+ state.segmentLedger.delete(indexName);
6657
+ void Promise.allSettled(
6658
+ holders.map((worker) => worker.execute({ type: "dropIndex", indexName, requestId: `desync-drop-${indexName}` }))
6659
+ );
6051
6660
  }
6052
6661
  async function closeIndex(indexName) {
6053
- await awaitPromotionIdle();
6662
+ if (state.poolRepair !== null) await state.poolRepair;
6663
+ const transition = state.copyTransitions.get(indexName);
6664
+ if (transition !== void 0) await transition.done;
6054
6665
  await awaitReplicationIdle(state, indexName);
6055
6666
  await awaitCompactions(state);
6667
+ cancelIdleMerge(state, indexName);
6056
6668
  const pool = state.workerPool;
6057
- if (pool !== null && state.promotedIndexes.has(indexName)) {
6669
+ if (pool !== null && state.scaledOutIndexes.has(indexName)) {
6670
+ state.scaledOutIndexes.delete(indexName);
6058
6671
  const outcomes = await Promise.allSettled(
6059
- pool.getAllExecutors().map((worker) => worker.execute({ type: "dropIndex", indexName, requestId: `close-${indexName}` }))
6672
+ pool.executorsHolding(indexName).map((worker) => worker.execute({ type: "dropIndex", indexName, requestId: `close-${indexName}` }))
6060
6673
  );
6061
6674
  const failure = outcomes.find((outcome) => outcome.status === "rejected");
6062
- if (failure?.status === "rejected") {
6063
- const entry = indexRegistry.get(indexName);
6064
- const manager = executor.getManager(indexName);
6065
- if (entry !== void 0 && manager !== void 0) {
6066
- try {
6067
- await transferIndexToPool(indexName, pool, entry.config, manager);
6068
- } catch (repairError) {
6069
- throw new NarsilError(
6070
- ErrorCodes.WORKER_CRASHED,
6071
- `Worker copies for index "${indexName}" could not be restored after close failed`,
6072
- {
6073
- indexName,
6074
- closeError: failure.reason instanceof Error ? failure.reason.message : String(failure.reason),
6075
- repairError: repairError instanceof Error ? repairError.message : String(repairError)
6076
- }
6077
- );
6078
- }
6079
- }
6080
- if (failure.reason instanceof NarsilError) throw failure.reason;
6081
- throw new NarsilError(ErrorCodes.WORKER_CRASHED, `A worker could not close index "${indexName}"`, {
6082
- indexName,
6083
- cause: failure.reason instanceof Error ? failure.reason.message : String(failure.reason)
6084
- });
6085
- }
6675
+ if (failure?.status === "rejected") await restoreAfterFailedClose(indexName, failure);
6086
6676
  pool.removeIndex(indexName);
6087
6677
  }
6088
- state.promotedIndexes.delete(indexName);
6678
+ state.droppedCopies.delete(indexName);
6679
+ state.desyncedIndexes.delete(indexName);
6680
+ state.lastAccessAt.delete(indexName);
6089
6681
  state.replicationQueues.delete(indexName);
6090
6682
  state.segmentLedger.delete(indexName);
6091
6683
  }
6092
6684
  async function replicate(action) {
6093
6685
  await replicateToWorkers(state, action);
6094
- if (action.type === "attachSegments") {
6095
- maybeCompactSegments(state, action.indexName);
6686
+ if (!("indexName" in action)) return;
6687
+ if (action.type === "attachSegments") maybeCompactSegments(state, action.indexName);
6688
+ if (action.type === "insert" || action.type === "update") flushGrownTails(state, action.indexName);
6689
+ if (state.scaledOutIndexes.has(action.indexName) || state.copyLoadBuffers.has(action.indexName)) {
6690
+ scheduleIdleMerge(state, action.indexName);
6096
6691
  }
6097
6692
  }
6098
6693
  return {
6099
- checkPromotion: () => checkPromotion(state),
6100
- promoteBeforeBatch: (indexName, incomingCount) => promoteBeforeBatch(state, indexName, incomingCount),
6694
+ scaleOutReadyIndexes: () => scaleOutReadyIndexes(state),
6695
+ scaleOutBeforeBatch: (indexName, incomingCount) => scaleOutBeforeBatch(state, indexName, incomingCount),
6101
6696
  replicateToWorkers: replicate,
6102
6697
  awaitReplication: (indexName) => awaitReplicationIdle(state, indexName),
6698
+ awaitWrites: (indexName) => awaitWritesApplied(state, indexName),
6103
6699
  awaitCompactions: () => awaitCompactions(state),
6104
- openIndex: (indexName) => resyncIndex(indexName, true),
6700
+ openIndex,
6105
6701
  closeIndex,
6106
- isIndexBusy: (indexName) => state.promotionInProgress || state.replicationQueues.has(indexName) || state.compactionsInFlight.has(indexName),
6702
+ isIndexBusy: (indexName) => isIndexBusy(state, indexName),
6107
6703
  buildSegments: (requests) => buildSegments(state, requests),
6108
6704
  segmentBuildConcurrency: (indexName) => segmentBuildConcurrency(state, indexName),
6109
6705
  searchViaWorker: (indexName, params, globalStats, partitionIds) => searchViaWorker(state, indexName, params, globalStats, partitionIds),
6110
- isPromoted: () => state.workerPool !== null,
6111
- desyncIndex: (indexName) => state.promotedIndexes.delete(indexName),
6706
+ hasWorkerPool: () => state.workerPool !== null,
6707
+ mainCopyQueries: () => state.mainCopyQueries ?? "lone",
6708
+ shareMainThread: () => {
6709
+ if (state.mainCopyQueries === void 0) state.mainCopyQueries = "none";
6710
+ },
6711
+ serveRequestsOnWorkers: (listener) => serveRequestsOnWorkers(state, listener),
6712
+ requestThreadCount: () => requestThreadCountOf(state),
6713
+ copyIdleTimeoutMs: () => state.copyIdleTimeoutMs,
6714
+ stopRequestThreads: () => stopRequestThreads(state),
6715
+ desyncIndex: (indexName) => {
6716
+ state.desyncedIndexes.add(indexName);
6717
+ const wasScaledOut = state.scaledOutIndexes.delete(indexName);
6718
+ if (wasScaledOut) withdrawCopies(indexName);
6719
+ return wasScaledOut;
6720
+ },
6112
6721
  resyncIndex,
6722
+ noteAccess: (indexName) => noteAccess(state, indexName),
6723
+ workerCopies,
6113
6724
  getWorkerMemoryStats,
6114
6725
  shutdown
6115
6726
  };
@@ -6120,47 +6731,15 @@ function getVectorFieldPaths(schema) {
6120
6731
  return new Set(extractVectorFieldsFromSchema(schema).keys());
6121
6732
  }
6122
6733
 
6123
- // src/engine/watermark.ts
6124
- function createWatermarkNotifier(deps) {
6125
- const latchedCapacity = /* @__PURE__ */ new Map();
6126
- function check(indexName) {
6127
- const partitionConfig = deps.getPartitionConfig(indexName);
6128
- const maxDocs = partitionConfig?.maxDocsPerPartition;
6129
- const watermark = partitionConfig?.watermark;
6130
- if (maxDocs === void 0 || watermark === void 0) {
6131
- latchedCapacity.delete(indexName);
6132
- return;
6133
- }
6134
- const manager = deps.getManager(indexName);
6135
- if (!manager) {
6136
- latchedCapacity.delete(indexName);
6137
- return;
6138
- }
6139
- const partitionCount = manager.partitionCount;
6140
- const capacity = maxDocs * partitionCount;
6141
- const documentCount = manager.countDocuments();
6142
- const threshold = watermark * capacity;
6143
- if (documentCount < threshold) {
6144
- latchedCapacity.delete(indexName);
6145
- return;
6146
- }
6147
- if (latchedCapacity.get(indexName) === capacity) return;
6148
- latchedCapacity.set(indexName, capacity);
6149
- deps.emit({ indexName, documentCount, capacity, partitionCount });
6150
- }
6151
- function forget(indexName) {
6152
- latchedCapacity.delete(indexName);
6153
- }
6154
- return { check, forget };
6155
- }
6156
-
6157
6734
  // src/engine/core.ts
6158
6735
  function createEngineCore(config, hooks) {
6159
- const executor = createDirectExecutor();
6160
- const promoter = createExecutionPromoter({
6161
- perIndexThreshold: config?.workers?.promotionThreshold,
6162
- totalThreshold: config?.workers?.totalPromotionThreshold
6163
- });
6736
+ validateWorkerConfig(config?.workers, config?.lifecycle);
6737
+ const vectorWorkerCount = splitWorkerBudget(resolveWorkerCount(config?.workers?.count)).vector;
6738
+ const vectorCopyPolicy = {
6739
+ enabled: (config?.workers?.enabled ?? workersEnabledByDefault()) && vectorWorkerCount > 0,
6740
+ count: vectorWorkerCount
6741
+ };
6742
+ const executor = createDirectExecutor({ vectorWorkerCopies: vectorCopyPolicy });
6164
6743
  const pluginRegistry = createPluginRegistry();
6165
6744
  if (config?.plugins) {
6166
6745
  for (const plugin of config.plugins) pluginRegistry.register(plugin);
@@ -6178,36 +6757,38 @@ function createEngineCore(config, hooks) {
6178
6757
  const shutdownState = { isShutdown: false };
6179
6758
  const abortController = new AbortController();
6180
6759
  const rebalancingIndexes = /* @__PURE__ */ new Set();
6181
- const orchestrator = createWorkerOrchestrator(config, executor, promoter, indexRegistry, {
6182
- shouldDeferPromotion() {
6183
- return rebalancingIndexes.size > 0;
6184
- },
6185
- onPromotion(workerCount, reason) {
6186
- const handlers = eventHandlers.get("workerPromote");
6187
- if (handlers) {
6188
- for (const handler of handlers) handler({ workerCount, reason });
6189
- }
6190
- void Promise.resolve(pluginRegistry.runHook("onWorkerPromote", { workerCount, reason })).catch((err) => {
6191
- console.warn("onWorkerPromote plugin hook failed:", err instanceof Error ? err.message : String(err));
6192
- });
6193
- },
6194
- onPromotionFailure(reason, error, retryable) {
6195
- const handlers = eventHandlers.get("workerPromoteFailure");
6196
- if (!handlers || handlers.size === 0) {
6197
- console.warn(`Worker promotion failed (${reason}):`, error);
6198
- return;
6760
+ const orchestrator = createWorkerOrchestrator(
6761
+ config,
6762
+ executor,
6763
+ indexRegistry,
6764
+ {
6765
+ shouldDeferCopies() {
6766
+ return rebalancingIndexes.size > 0;
6767
+ },
6768
+ isAnalysisStale(indexName) {
6769
+ return analysisRebuild.isStale(indexName);
6770
+ },
6771
+ onCopiesLoaded(workerCount, reason) {
6772
+ emitEngineEvent(eventHandlers, "workerPromote", { workerCount, reason });
6773
+ void Promise.resolve(pluginRegistry.runHook("onWorkerPromote", { workerCount, reason })).catch(
6774
+ (err) => {
6775
+ console.warn("onWorkerPromote plugin hook failed:", err instanceof Error ? err.message : String(err));
6776
+ }
6777
+ );
6778
+ },
6779
+ onCopyLoadFailure(reason, error, retryable) {
6780
+ if (emitEngineEvent(eventHandlers, "workerPromoteFailure", { reason, error, retryable }) === 0) {
6781
+ console.warn(`Loading worker copies failed (${reason}):`, error);
6782
+ }
6783
+ },
6784
+ onWorkerCrash(workerId, indexNames, error) {
6785
+ if (emitEngineEvent(eventHandlers, "workerCrash", { workerId, indexNames, error }) === 0) {
6786
+ console.warn(`Worker ${workerId} crashed:`, error);
6787
+ }
6199
6788
  }
6200
- for (const handler of handlers) handler({ reason, error, retryable });
6201
6789
  },
6202
- onWorkerCrash(workerId, indexNames, error) {
6203
- const handlers = eventHandlers.get("workerCrash");
6204
- if (!handlers || handlers.size === 0) {
6205
- console.warn(`Worker ${workerId} crashed:`, error);
6206
- return;
6207
- }
6208
- for (const handler of handlers) handler({ workerId, indexNames, error });
6209
- }
6210
- });
6790
+ vectorCopyPolicy
6791
+ );
6211
6792
  const rebalancer = createRebalancer();
6212
6793
  const rebalanceRouter = createPartitionRouter();
6213
6794
  const waqMap = /* @__PURE__ */ new Map();
@@ -6316,10 +6897,7 @@ function createEngineCore(config, hooks) {
6316
6897
  indexRegistry,
6317
6898
  createIndexFromMetadata,
6318
6899
  emitFatalError(error) {
6319
- const handlers = eventHandlers.get("durabilityError");
6320
- if (handlers) {
6321
- for (const handler of handlers) handler({ error });
6322
- }
6900
+ emitEngineEvent(eventHandlers, "durabilityError", { error });
6323
6901
  },
6324
6902
  publishCheckpointedPartitions: (indexName, partitions) => invalidation?.publishPartitions(indexName, partitions) ?? Promise.resolve(),
6325
6903
  recordCheckpoint(indexName, documentCount, partitionCount) {
@@ -6342,29 +6920,18 @@ function createEngineCore(config, hooks) {
6342
6920
  },
6343
6921
  reloadIndex: (indexName) => durability?.manager.reloadIndex?.(indexName) ?? Promise.resolve(),
6344
6922
  onError(error) {
6345
- const handlers = eventHandlers.get("invalidationError");
6346
- if (!handlers || handlers.size === 0) {
6923
+ if (emitEngineEvent(eventHandlers, "invalidationError", { error }) === 0) {
6347
6924
  console.warn("Invalidation error:", error);
6348
- return;
6349
- }
6350
- for (const handler of handlers) handler({ error });
6351
- }
6352
- });
6353
- const watermarkNotifier = createWatermarkNotifier({
6354
- getManager: (indexName) => executor.getManager(indexName),
6355
- getPartitionConfig: (indexName) => indexRegistry.get(indexName)?.config.partitions,
6356
- emit(payload) {
6357
- const handlers = eventHandlers.get("partitionWatermark");
6358
- if (!handlers) return;
6359
- for (const handler of handlers) {
6360
- try {
6361
- handler(payload);
6362
- } catch (err) {
6363
- console.warn("partitionWatermark handler error:", err instanceof Error ? err.message : String(err));
6364
- }
6365
6925
  }
6366
6926
  }
6367
6927
  });
6928
+ const notifierWiring = {
6929
+ eventHandlers,
6930
+ indexRegistry,
6931
+ getManager: (indexName) => executor.getManager(indexName)
6932
+ };
6933
+ const watermarkNotifier = wireWatermarkNotifier(notifierWiring);
6934
+ const heapPressureNotifier = wireHeapPressureNotifier(notifierWiring);
6368
6935
  const analysisRebuild = wireAnalysisRebuild({
6369
6936
  config: config?.analysis,
6370
6937
  eventHandlers,
@@ -6383,7 +6950,11 @@ function createEngineCore(config, hooks) {
6383
6950
  rebalancingIndexes,
6384
6951
  requireManager,
6385
6952
  onClose: hooks?.onIndexClose,
6386
- onOpen: hooks?.onIndexOpen
6953
+ onOpen: (indexName) => {
6954
+ heapPressureNotifier.check(indexName);
6955
+ return hooks?.onIndexOpen?.(indexName);
6956
+ },
6957
+ onAccess: (indexName) => orchestrator.noteAccess(indexName)
6387
6958
  });
6388
6959
  const mutationCtx = {
6389
6960
  executor,
@@ -6396,11 +6967,13 @@ function createEngineCore(config, hooks) {
6396
6967
  requireIndex,
6397
6968
  requireManager,
6398
6969
  bufferIfRebalancing,
6970
+ awaitRebalanceReplay: (indexName) => waqMap.get(indexName)?.whenReplayed() ?? Promise.resolve(),
6399
6971
  isRebalancing: (indexName) => rebalancingIndexes.has(indexName),
6400
6972
  pendingRebalanceWrites: (indexName) => waqMap.get(indexName)?.size ?? 0,
6401
6973
  rebalanceTargetPartitionCount: (indexName) => rebalanceTargets.get(indexName),
6402
6974
  bufferedDocState: (indexName, docId) => waqMap.get(indexName)?.bufferedDocState(docId),
6403
- checkWatermark: watermarkNotifier.check
6975
+ checkWatermark: watermarkNotifier.check,
6976
+ checkHeapPressure: heapPressureNotifier.check
6404
6977
  };
6405
6978
  const rebalanceCtx = {
6406
6979
  rebalancer,
@@ -6417,7 +6990,6 @@ function createEngineCore(config, hooks) {
6417
6990
  };
6418
6991
  return {
6419
6992
  executor,
6420
- promoter,
6421
6993
  pluginRegistry,
6422
6994
  durability,
6423
6995
  invalidation,
@@ -6437,6 +7009,7 @@ function createEngineCore(config, hooks) {
6437
7009
  requireManager,
6438
7010
  bufferIfRebalancing,
6439
7011
  watermarkNotifier,
7012
+ heapPressureNotifier,
6440
7013
  analysisRebuild,
6441
7014
  indexState,
6442
7015
  mutationCtx,
@@ -6444,108 +7017,18 @@ function createEngineCore(config, hooks) {
6444
7017
  };
6445
7018
  }
6446
7019
 
6447
- // src/engine/validation.ts
6448
- var INDEX_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
6449
- var MAX_DOC_ID_LENGTH = 512;
6450
- var BATCH_CHUNK_SIZE = 1e3;
6451
- var DEFAULT_LIMIT = DEFAULT_PAGE_SIZE;
6452
- var DEFAULT_OFFSET = 0;
6453
- function now() {
6454
- if (typeof performance !== "undefined" && typeof performance.now === "function") {
6455
- return performance.now();
6456
- }
6457
- return Date.now();
6458
- }
6459
- function validateIndexName(name) {
6460
- if (!name || name.length === 0) {
6461
- throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, "Index name must not be empty", { indexName: name });
6462
- }
6463
- if (name.length > MAX_INDEX_NAME_LENGTH) {
6464
- throw new NarsilError(
6465
- ErrorCodes.INDEX_NOT_FOUND,
6466
- `Index name must not exceed ${MAX_INDEX_NAME_LENGTH} characters`,
6467
- {
6468
- indexName: name,
6469
- length: name.length
6470
- }
6471
- );
6472
- }
6473
- if (!INDEX_NAME_PATTERN.test(name)) {
6474
- throw new NarsilError(
6475
- ErrorCodes.INDEX_NOT_FOUND,
6476
- `Index name "${name}" contains invalid characters; use alphanumeric, dots, hyphens, and underscores only`,
6477
- { indexName: name }
6478
- );
6479
- }
6480
- if (name.includes("..")) {
6481
- throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, `Index name "${name}" must not contain ".."`, { indexName: name });
6482
- }
6483
- }
6484
- function validateDocId(docId) {
6485
- if (!docId || docId.length === 0) {
6486
- throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Document ID must not be empty", { docId });
6487
- }
6488
- if (docId.length > MAX_DOC_ID_LENGTH) {
6489
- throw new NarsilError(
6490
- ErrorCodes.DOC_VALIDATION_FAILED,
6491
- `Document ID must not exceed ${MAX_DOC_ID_LENGTH} characters`,
6492
- {
6493
- docId,
6494
- length: docId.length
6495
- }
6496
- );
6497
- }
6498
- if (docId.includes("\0")) {
6499
- throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Document ID must not contain null bytes", { docId });
7020
+ // src/engine/index-lifecycle.ts
7021
+ async function createEngineIndex(core, config, name, indexConfig, indexUuid) {
7022
+ core.guardShutdown();
7023
+ validateIndexName(name);
7024
+ if (core.indexRegistry.has(name)) {
7025
+ throw new NarsilError(ErrorCodes.INDEX_ALREADY_EXISTS, `Index "${name}" already exists`, { indexName: name });
6500
7026
  }
6501
- }
6502
- function validatePartitionConfig(partitions) {
6503
- const { maxDocsPerPartition, maxPartitions, watermark } = partitions;
6504
- if (maxDocsPerPartition !== void 0 && (!Number.isInteger(maxDocsPerPartition) || maxDocsPerPartition < 1)) {
6505
- throw new NarsilError(ErrorCodes.CONFIG_INVALID, "partitions.maxDocsPerPartition must be a positive integer", {
6506
- maxDocsPerPartition
6507
- });
6508
- }
6509
- if (maxPartitions !== void 0 && (!Number.isInteger(maxPartitions) || maxPartitions < 1)) {
6510
- throw new NarsilError(ErrorCodes.CONFIG_INVALID, "partitions.maxPartitions must be a positive integer", {
6511
- maxPartitions
6512
- });
6513
- }
6514
- if (watermark !== void 0 && (typeof watermark !== "number" || !(watermark > 0) || watermark > 1)) {
6515
- throw new NarsilError(ErrorCodes.CONFIG_INVALID, "partitions.watermark must be above 0 and at most 1", {
6516
- watermark
6517
- });
6518
- }
6519
- }
6520
- function validateBM25Params(bm25) {
6521
- if (bm25 === void 0) return;
6522
- const { k1, b } = bm25;
6523
- if (k1 !== void 0 && (!Number.isFinite(k1) || k1 < 0)) {
6524
- throw new NarsilError(ErrorCodes.CONFIG_INVALID, "bm25.k1 must be a finite number at or above 0", { k1 });
6525
- }
6526
- if (b !== void 0 && (!Number.isFinite(b) || b < 0 || b > 1)) {
6527
- throw new NarsilError(ErrorCodes.CONFIG_INVALID, "bm25.b must be between 0 and 1", { b });
6528
- }
6529
- }
6530
- function clampLimit(limit) {
6531
- return clampRowCount(limit, DEFAULT_LIMIT);
6532
- }
6533
- function clampOffset(offset) {
6534
- return clampRowCount(offset, DEFAULT_OFFSET);
6535
- }
6536
-
6537
- // src/engine/index-lifecycle.ts
6538
- async function createEngineIndex(core, config, name, indexConfig, indexUuid) {
6539
- core.guardShutdown();
6540
- validateIndexName(name);
6541
- if (core.indexRegistry.has(name)) {
6542
- throw new NarsilError(ErrorCodes.INDEX_ALREADY_EXISTS, `Index "${name}" already exists`, { indexName: name });
6543
- }
6544
- validateSchema(indexConfig.schema);
6545
- validateVectorPromotion(indexConfig.vectorPromotion);
6546
- validateBM25Params(indexConfig.bm25);
6547
- if (indexConfig.partitions) {
6548
- validatePartitionConfig(indexConfig.partitions);
7027
+ validateSchema(indexConfig.schema);
7028
+ validateVectorPromotion(indexConfig.vectorPromotion);
7029
+ validateBM25Params(indexConfig.bm25);
7030
+ if (indexConfig.partitions) {
7031
+ validatePartitionConfig(indexConfig.partitions);
6549
7032
  }
6550
7033
  if (core.durability) {
6551
7034
  if (indexConfig.tokenizer !== void 0 && typeof indexConfig.tokenizer !== "string") {
@@ -7037,6 +7520,12 @@ async function embedChunkDocuments(entry, documents, chunkStart, chunkEnd, signa
7037
7520
  }
7038
7521
  }
7039
7522
 
7523
+ // src/engine/mutations/write-visibility.ts
7524
+ async function awaitWriteVisibility(ctx, indexName) {
7525
+ await ctx.awaitRebalanceReplay(indexName);
7526
+ await ctx.orchestrator.awaitWrites(indexName);
7527
+ }
7528
+
7040
7529
  // src/engine/mutations/insert.ts
7041
7530
  async function insertDocument(ctx, indexName, document, docId, options) {
7042
7531
  ctx.guardShutdown();
@@ -7122,6 +7611,8 @@ async function insertDocument(ctx, indexName, document, docId, options) {
7122
7611
  }
7123
7612
  if (buffered) {
7124
7613
  ctx.checkWatermark(indexName);
7614
+ ctx.checkHeapPressure(indexName);
7615
+ if (options?.wait === true) await awaitWriteVisibility(ctx, indexName);
7125
7616
  return resolvedDocId;
7126
7617
  }
7127
7618
  try {
@@ -7144,7 +7635,9 @@ async function insertDocument(ctx, indexName, document, docId, options) {
7144
7635
  }
7145
7636
  }
7146
7637
  ctx.checkWatermark(indexName);
7147
- await ctx.orchestrator.checkPromotion();
7638
+ ctx.checkHeapPressure(indexName);
7639
+ await ctx.orchestrator.scaleOutReadyIndexes();
7640
+ if (options?.wait === true) await awaitWriteVisibility(ctx, indexName);
7148
7641
  return resolvedDocId;
7149
7642
  }
7150
7643
 
@@ -7298,7 +7791,6 @@ async function applyAdmittedDocuments(ctx, indexName, admitted, options, failed)
7298
7791
  }
7299
7792
 
7300
7793
  // src/engine/mutations/segment-replication.ts
7301
- var MIN_DOCUMENTS_FOR_SEGMENTS = 64;
7302
7794
  function shardCount(documentCount, workers) {
7303
7795
  if (workers <= 1) return 1;
7304
7796
  return Math.max(1, Math.min(workers, Math.ceil(documentCount / MIN_DOCUMENTS_FOR_SEGMENTS)));
@@ -7558,7 +8050,9 @@ async function insertBatchViaSegments(ctx, indexName, documents, options) {
7558
8050
  vecIndexes.get(fieldPath)?.scheduleBuild();
7559
8051
  }
7560
8052
  ctx.checkWatermark(indexName);
7561
- await ctx.orchestrator.checkPromotion();
8053
+ ctx.checkHeapPressure(indexName);
8054
+ await ctx.orchestrator.scaleOutReadyIndexes();
8055
+ if (options?.wait === true) await awaitWriteVisibility(ctx, indexName);
7562
8056
  return { succeeded: outcome.succeeded, failed };
7563
8057
  }
7564
8058
 
@@ -7566,7 +8060,7 @@ async function insertBatchViaSegments(ctx, indexName, documents, options) {
7566
8060
  async function insertDocumentBatch(ctx, indexName, documents, options) {
7567
8061
  ctx.guardShutdown();
7568
8062
  const entry = ctx.requireIndex(indexName);
7569
- await ctx.orchestrator.promoteBeforeBatch(indexName, documents.length);
8063
+ await ctx.orchestrator.scaleOutBeforeBatch(indexName, documents.length);
7570
8064
  if (documents.length >= MIN_DOCUMENTS_FOR_SEGMENTS && !ctx.isRebalancing(indexName) && ctx.orchestrator.segmentBuildConcurrency(indexName) > 0) {
7571
8065
  return insertBatchViaSegments(ctx, indexName, documents, options);
7572
8066
  }
@@ -7720,12 +8214,14 @@ async function insertDocumentBatch(ctx, indexName, documents, options) {
7720
8214
  }
7721
8215
  }
7722
8216
  ctx.checkWatermark(indexName);
7723
- await ctx.orchestrator.checkPromotion();
8217
+ ctx.checkHeapPressure(indexName);
8218
+ await ctx.orchestrator.scaleOutReadyIndexes();
8219
+ if (options?.wait === true) await awaitWriteVisibility(ctx, indexName);
7724
8220
  return { succeeded, failed };
7725
8221
  }
7726
8222
 
7727
8223
  // src/engine/mutations/remove.ts
7728
- async function removeDocument(ctx, indexName, docId) {
8224
+ async function removeDocument(ctx, indexName, docId, options) {
7729
8225
  ctx.guardShutdown();
7730
8226
  ctx.requireIndex(indexName);
7731
8227
  validateDocId(docId);
@@ -7764,6 +8260,7 @@ async function removeDocument(ctx, indexName, docId) {
7764
8260
  await applyRemove();
7765
8261
  }
7766
8262
  if (buffered) {
8263
+ if (options?.wait === true) await awaitWriteVisibility(ctx, indexName);
7767
8264
  return;
7768
8265
  }
7769
8266
  try {
@@ -7777,8 +8274,9 @@ async function removeDocument(ctx, indexName, docId) {
7777
8274
  docId,
7778
8275
  requestId: `replicate-remove-${docId}`
7779
8276
  });
8277
+ if (options?.wait === true) await awaitWriteVisibility(ctx, indexName);
7780
8278
  }
7781
- async function removeDocumentBatch(ctx, indexName, docIds) {
8279
+ async function removeDocumentBatch(ctx, indexName, docIds, options) {
7782
8280
  ctx.guardShutdown();
7783
8281
  ctx.requireIndex(indexName);
7784
8282
  const succeeded = [];
@@ -7806,6 +8304,7 @@ async function removeDocumentBatch(ctx, indexName, docIds) {
7806
8304
  } finally {
7807
8305
  manager.endBatchRemove();
7808
8306
  }
8307
+ if (options?.wait === true) await awaitWriteVisibility(ctx, indexName);
7809
8308
  return { succeeded, failed };
7810
8309
  }
7811
8310
 
@@ -7823,7 +8322,7 @@ function prepareUpdatePartitionDoc(document, extractedVectors) {
7823
8322
  }
7824
8323
  return { partitionDoc };
7825
8324
  }
7826
- async function updateDocument(ctx, indexName, docId, document) {
8325
+ async function updateDocument(ctx, indexName, docId, document, options) {
7827
8326
  ctx.guardShutdown();
7828
8327
  const entry = ctx.requireIndex(indexName);
7829
8328
  validateDocId(docId);
@@ -7925,6 +8424,8 @@ async function updateDocument(ctx, indexName, docId, document) {
7925
8424
  await applyUpdate();
7926
8425
  }
7927
8426
  if (buffered) {
8427
+ ctx.checkHeapPressure(indexName);
8428
+ if (options?.wait === true) await awaitWriteVisibility(ctx, indexName);
7928
8429
  return;
7929
8430
  }
7930
8431
  try {
@@ -7951,8 +8452,10 @@ async function updateDocument(ctx, indexName, docId, document) {
7951
8452
  vecIndex.scheduleBuild();
7952
8453
  }
7953
8454
  }
8455
+ ctx.checkHeapPressure(indexName);
8456
+ if (options?.wait === true) await awaitWriteVisibility(ctx, indexName);
7954
8457
  }
7955
- async function updateDocumentBatch(ctx, indexName, updates) {
8458
+ async function updateDocumentBatch(ctx, indexName, updates, options) {
7956
8459
  ctx.guardShutdown();
7957
8460
  const entry = ctx.requireIndex(indexName);
7958
8461
  const succeeded = [];
@@ -7991,14 +8494,19 @@ async function updateDocumentBatch(ctx, indexName, updates) {
7991
8494
  vecIndex.scheduleBuild();
7992
8495
  }
7993
8496
  }
8497
+ if (options?.wait === true) await awaitWriteVisibility(ctx, indexName);
7994
8498
  return { succeeded, failed };
7995
8499
  }
7996
8500
 
7997
8501
  // src/partitioning/write-ahead-queue.ts
7998
- function createWriteAheadQueue(maxSize = 1e4) {
8502
+ function createWriteAheadQueue(maxSize = DEFAULT_WRITE_AHEAD_QUEUE_MAX_SIZE) {
7999
8503
  const entries = [];
8000
8504
  const docStates = /* @__PURE__ */ new Map();
8001
8505
  let nextSequence = 1;
8506
+ let resolveReplayed = () => void 0;
8507
+ const replayed = new Promise((resolve) => {
8508
+ resolveReplayed = resolve;
8509
+ });
8002
8510
  return {
8003
8511
  push(entry) {
8004
8512
  if (entries.length >= maxSize) {
@@ -8025,6 +8533,12 @@ function createWriteAheadQueue(maxSize = 1e4) {
8025
8533
  bufferedDocState(docId) {
8026
8534
  return docStates.get(docId);
8027
8535
  },
8536
+ whenReplayed() {
8537
+ return replayed;
8538
+ },
8539
+ markReplayed() {
8540
+ resolveReplayed();
8541
+ },
8028
8542
  get size() {
8029
8543
  return entries.length;
8030
8544
  },
@@ -8198,12 +8712,11 @@ async function executeRebalance(manager, indexName, targetPartitionCount, ctx) {
8198
8712
  const oldCount = manager.partitionCount;
8199
8713
  await ctx.rebalancer.rebalance(manager, targetPartitionCount, ctx.router, (progress) => {
8200
8714
  if (progress.phase === "complete") {
8201
- const handlers = ctx.eventHandlers.get("partitionRebalance");
8202
- if (handlers) {
8203
- for (const handler of handlers) {
8204
- handler({ indexName, oldCount, newCount: targetPartitionCount });
8205
- }
8206
- }
8715
+ emitEngineEvent(ctx.eventHandlers, "partitionRebalance", {
8716
+ indexName,
8717
+ oldCount,
8718
+ newCount: targetPartitionCount
8719
+ });
8207
8720
  }
8208
8721
  });
8209
8722
  try {
@@ -8243,6 +8756,7 @@ async function executeRebalance(manager, indexName, targetPartitionCount, ctx) {
8243
8756
  ctx.rebalancingIndexes.delete(indexName);
8244
8757
  ctx.rebalanceTargets.delete(indexName);
8245
8758
  ctx.waqMap.delete(indexName);
8759
+ waq.markReplayed();
8246
8760
  }
8247
8761
  ctx.checkWatermark(indexName);
8248
8762
  }
@@ -8670,6 +9184,7 @@ function readProcessMemory() {
8670
9184
  return {
8671
9185
  heapUsed: usage.heapUsed,
8672
9186
  heapTotal: usage.heapTotal,
9187
+ heapLimit: readHeapStatistics()?.limitBytes ?? null,
8673
9188
  external: usage.external,
8674
9189
  rss: usage.rss
8675
9190
  };
@@ -8678,1209 +9193,6 @@ function readProcessMemory() {
8678
9193
  }
8679
9194
  }
8680
9195
 
8681
- // src/search/cursor-binding.ts
8682
- var ABSENT_TAG = 0;
8683
- var NULL_TAG = 1;
8684
- var FALSE_TAG = 2;
8685
- var TRUE_TAG = 3;
8686
- var NUMBER_TAG = 4;
8687
- var STRING_TAG = 5;
8688
- var LIST_TAG = 6;
8689
- var MAP_TAG = 7;
8690
- var encoder = new TextEncoder();
8691
- function ensureCapacity(stream, extra) {
8692
- if (stream.length + extra <= stream.bytes.length) return;
8693
- const grown = new Uint8Array(Math.max(stream.bytes.length * 2, stream.length + extra));
8694
- grown.set(stream.bytes.subarray(0, stream.length));
8695
- stream.bytes = grown;
8696
- }
8697
- function writeByte(stream, value) {
8698
- ensureCapacity(stream, 1);
8699
- stream.bytes[stream.length] = value;
8700
- stream.length += 1;
8701
- }
8702
- function writeUint32(stream, value) {
8703
- ensureCapacity(stream, 4);
8704
- stream.bytes[stream.length] = value >>> 24 & 255;
8705
- stream.bytes[stream.length + 1] = value >>> 16 & 255;
8706
- stream.bytes[stream.length + 2] = value >>> 8 & 255;
8707
- stream.bytes[stream.length + 3] = value & 255;
8708
- stream.length += 4;
8709
- }
8710
- var numberBytes = new DataView(new ArrayBuffer(8));
8711
- function writeNumber(stream, value) {
8712
- numberBytes.setFloat64(0, value, false);
8713
- ensureCapacity(stream, 8);
8714
- for (let i = 0; i < 8; i++) {
8715
- stream.bytes[stream.length + i] = numberBytes.getUint8(i);
8716
- }
8717
- stream.length += 8;
8718
- }
8719
- function writeString(stream, value) {
8720
- const encoded = encoder.encode(value);
8721
- writeUint32(stream, encoded.length);
8722
- ensureCapacity(stream, encoded.length);
8723
- stream.bytes.set(encoded, stream.length);
8724
- stream.length += encoded.length;
8725
- }
8726
- function writeValue(stream, value) {
8727
- if (value === void 0) {
8728
- writeByte(stream, ABSENT_TAG);
8729
- return;
8730
- }
8731
- if (value === null) {
8732
- writeByte(stream, NULL_TAG);
8733
- return;
8734
- }
8735
- if (typeof value === "boolean") {
8736
- writeByte(stream, value ? TRUE_TAG : FALSE_TAG);
8737
- return;
8738
- }
8739
- if (typeof value === "number") {
8740
- writeByte(stream, NUMBER_TAG);
8741
- writeNumber(stream, value);
8742
- return;
8743
- }
8744
- if (typeof value === "string") {
8745
- writeByte(stream, STRING_TAG);
8746
- writeString(stream, value);
8747
- return;
8748
- }
8749
- if (Array.isArray(value)) {
8750
- writeByte(stream, LIST_TAG);
8751
- writeUint32(stream, value.length);
8752
- for (const member of value) {
8753
- writeValue(stream, member ?? null);
8754
- }
8755
- return;
8756
- }
8757
- const record = value;
8758
- const keys = Object.keys(record).sort(compareCodePoints);
8759
- writeByte(stream, MAP_TAG);
8760
- writeUint32(stream, keys.length);
8761
- for (const key of keys) {
8762
- writeByte(stream, STRING_TAG);
8763
- writeString(stream, key);
8764
- writeValue(stream, record[key] ?? null);
8765
- }
8766
- }
8767
- function boundVectorOf(vector) {
8768
- if (vector === void 0) return void 0;
8769
- const bound = { ...vector };
8770
- if (vector.value !== void 0) {
8771
- bound.value = Array.from(vector.value, Math.fround);
8772
- }
8773
- if (vector.text !== void 0) {
8774
- delete bound.value;
8775
- }
8776
- return bound;
8777
- }
8778
- var FILTERS_SLOT = 2;
8779
- var BOUND_SLOT_COUNT = 15;
8780
- function bindingOf(values) {
8781
- const stream = { bytes: new Uint8Array(256), length: 0 };
8782
- for (const value of values) {
8783
- writeValue(stream, value);
8784
- }
8785
- return fnv1aBytes(stream.bytes.subarray(0, stream.length)).toString(16).padStart(8, "0");
8786
- }
8787
- function queryBindingOf(params) {
8788
- return bindingOf([
8789
- params.term,
8790
- params.fields,
8791
- params.filters,
8792
- params.boost,
8793
- params.minScore,
8794
- params.termMatch,
8795
- params.tolerance,
8796
- params.prefixLength,
8797
- params.prefix,
8798
- params.exact,
8799
- params.pinned,
8800
- params.mode,
8801
- params.hybrid,
8802
- boundVectorOf(params.vector),
8803
- params.scoring ?? "local"
8804
- ]);
8805
- }
8806
- function listBindingOf(filters) {
8807
- const values = new Array(BOUND_SLOT_COUNT).fill(void 0);
8808
- values[FILTERS_SLOT] = filters;
8809
- return bindingOf(values);
8810
- }
8811
-
8812
- // src/engine/list-documents/index.ts
8813
- function clampListLimit(limit) {
8814
- const clamped = Math.max(1, clampLimit(limit));
8815
- requireWithinResultWindow(clamped, 0);
8816
- return clamped;
8817
- }
8818
- function firstIdAfter(sorted, anchor) {
8819
- let low = 0;
8820
- let high = sorted.length;
8821
- while (low < high) {
8822
- const mid = low + high >>> 1;
8823
- if (compareCodePoints(sorted[mid], anchor) <= 0) low = mid + 1;
8824
- else high = mid;
8825
- }
8826
- return low;
8827
- }
8828
- function collectFilterMatches(partitions, filters, schema) {
8829
- const perPartition = partitions.map((partition) => partition.filterMatches(filters, schema));
8830
- let total = 0;
8831
- for (const matches of perPartition) total += matches.count;
8832
- return { matchesFor: (partitionIndex) => perPartition[partitionIndex] ?? null, total };
8833
- }
8834
- function collectCandidates(partition, anchor, matches, wanted, out) {
8835
- const sorted = partition.sortedDocIds();
8836
- let index = anchor === null ? 0 : firstIdAfter(sorted, anchor);
8837
- let taken = 0;
8838
- while (index < sorted.length && taken < wanted) {
8839
- const docId = sorted[index];
8840
- index++;
8841
- if (matches !== null && !matches.hasExternal(docId)) continue;
8842
- out.push(docId);
8843
- taken++;
8844
- }
8845
- }
8846
- function pageInIdOrder(partitions, cursor, filtered, limit, binding) {
8847
- const anchor = cursor === null ? null : cursor.anchor;
8848
- const candidates = [];
8849
- for (let index = 0; index < partitions.length; index++) {
8850
- collectCandidates(
8851
- partitions[index],
8852
- anchor,
8853
- filtered === null ? null : filtered.matchesFor(index),
8854
- limit + 1,
8855
- candidates
8856
- );
8857
- }
8858
- candidates.sort(compareCodePoints);
8859
- const hasMore = candidates.length > limit;
8860
- const ids = hasMore ? candidates.slice(0, limit) : candidates;
8861
- const nextCursor = hasMore && ids.length > 0 ? { anchor: ids[ids.length - 1], score: null, sortKey: null, sortSignature: null, binding } : null;
8862
- return { ids, nextCursor };
8863
- }
8864
- function pageInSortOrder(partitions, cursor, filtered, limit, sort, signature, schema, binding) {
8865
- const normalized = normalizeSort(sort);
8866
- const fields = normalized.map((entry) => entry.field);
8867
- const directions = normalized.map((entry) => entry.direction);
8868
- const flatSchema = flattenSchema(schema);
8869
- const fieldTypes = fields.map((field) => flatSchema[field]);
8870
- const request = {
8871
- fields,
8872
- directions,
8873
- fieldTypes,
8874
- limit: limit + 1,
8875
- anchorKey: cursor === null ? null : cursor.sortKey,
8876
- anchorId: cursor === null ? null : cursor.anchor
8877
- };
8878
- const merged = [];
8879
- for (let index = 0; index < partitions.length; index++) {
8880
- const matches = filtered === null ? null : filtered.matchesFor(index);
8881
- for (const entry of partitions[index].sortedPage({ ...request, matches })) merged.push(entry);
8882
- }
8883
- merged.sort((a, b) => compareComparableKeys(a.key, b.key, directions) || compareCodePoints(a.id, b.id));
8884
- const hasMore = merged.length > limit;
8885
- const page = hasMore ? merged.slice(0, limit) : merged;
8886
- const last = page[page.length - 1];
8887
- const nextCursor = hasMore && last !== void 0 ? { anchor: last.id, score: null, sortKey: last.key, sortSignature: signature, binding } : null;
8888
- return { ids: page.map((entry) => entry.id), nextCursor };
8889
- }
8890
- function countAcrossPartitions(manager, partitionIds, partitions) {
8891
- if (partitionIds === void 0) {
8892
- return manager.countDocuments();
8893
- }
8894
- let total = 0;
8895
- for (const partition of partitions) {
8896
- total += partition.count();
8897
- }
8898
- return total;
8899
- }
8900
- function executeListDocuments(params, context) {
8901
- const { manager, schema } = context;
8902
- const startTime = now();
8903
- const limit = clampListLimit(params.limit);
8904
- const signature = sortSignatureOf(params.sort);
8905
- requireSortableFields(params.sort, schema);
8906
- const binding = listBindingOf(params.filters);
8907
- let cursor = null;
8908
- if (params.cursor !== void 0) {
8909
- cursor = decodePageCursor(params.cursor);
8910
- requireMatchingCursor(cursor, params.cursor, signature, false, binding);
8911
- }
8912
- const partitions = context.partitionIds === void 0 ? manager.getAllPartitions() : context.partitionIds.map((partitionId) => manager.partitionAt(partitionId)).filter((partition) => partition !== void 0);
8913
- const filtered = params.filters === void 0 ? null : collectFilterMatches(partitions, params.filters, schema);
8914
- const total = filtered === null ? countAcrossPartitions(manager, context.partitionIds, partitions) : filtered.total;
8915
- const page = params.sort === void 0 || signature === null ? pageInIdOrder(partitions, cursor, filtered, limit, binding) : pageInSortOrder(partitions, cursor, filtered, limit, params.sort, signature, schema, binding);
8916
- const projection = resolveProjection(params.document);
8917
- const documents = [];
8918
- for (const docId of page.ids) {
8919
- if (projection.kind === "none") {
8920
- documents.push({ id: docId, document: {} });
8921
- continue;
8922
- }
8923
- const stored = manager.get(docId, projection);
8924
- if (stored === void 0) continue;
8925
- documents.push({ id: docId, document: stored });
8926
- }
8927
- if (page.nextCursor === null) {
8928
- for (const partition of partitions) partition.releaseSortedDocIds();
8929
- }
8930
- return {
8931
- documents,
8932
- cursor: page.nextCursor === null ? null : encodePageCursor(page.nextCursor),
8933
- total,
8934
- elapsed: now() - startTime
8935
- };
8936
- }
8937
-
8938
- // src/search/grouping.ts
8939
- function getNestedValue2(obj, path) {
8940
- const segments = path.split(".");
8941
- let current = obj;
8942
- for (const segment of segments) {
8943
- if (current === null || current === void 0 || typeof current !== "object") {
8944
- return void 0;
8945
- }
8946
- if (!Object.hasOwn(current, segment)) {
8947
- return void 0;
8948
- }
8949
- current = current[segment];
8950
- }
8951
- return current;
8952
- }
8953
- function applyGrouping(hits, group, getDocument) {
8954
- if (!group.fields || group.fields.length === 0) {
8955
- return [{ values: {}, hits }];
8956
- }
8957
- const groupMap = /* @__PURE__ */ new Map();
8958
- for (const hit of hits) {
8959
- const doc = getDocument(hit.id);
8960
- const fieldValues = [];
8961
- const valuesRecord = {};
8962
- for (const field of group.fields) {
8963
- const value = doc ? getNestedValue2(doc, field) : void 0;
8964
- fieldValues.push(value);
8965
- valuesRecord[field] = value;
8966
- }
8967
- const compositeKey = JSON.stringify(fieldValues);
8968
- const existing = groupMap.get(compositeKey);
8969
- if (existing) {
8970
- existing.hits.push(hit);
8971
- } else {
8972
- groupMap.set(compositeKey, { values: valuesRecord, hits: [hit] });
8973
- }
8974
- }
8975
- const groups = [];
8976
- for (const entry of groupMap.values()) {
8977
- let groupHits = entry.hits;
8978
- if (group.maxPerGroup !== void 0 && group.maxPerGroup > 0) {
8979
- groupHits = groupHits.slice(0, group.maxPerGroup);
8980
- }
8981
- const result = { values: entry.values, hits: groupHits };
8982
- if (group.reduce) {
8983
- const folded = foldGroupReducer(
8984
- group.reduce,
8985
- groupHits.map((hit) => ({ document: getDocument(hit.id), score: hit.score ?? 0 }))
8986
- );
8987
- if (folded.reducerError !== void 0) {
8988
- result.reducerError = folded.reducerError;
8989
- } else {
8990
- result.reduced = folded.reduced;
8991
- }
8992
- }
8993
- groups.push(result);
8994
- }
8995
- groups.sort((a, b) => {
8996
- const aScore = a.hits.length > 0 ? a.hits[0].score ?? 0 : 0;
8997
- const bScore = b.hits.length > 0 ? b.hits[0].score ?? 0 : 0;
8998
- if (aScore !== bScore) return bScore - aScore;
8999
- const aId = a.hits.length > 0 ? a.hits[0].id : "";
9000
- const bId = b.hits.length > 0 ? b.hits[0].id : "";
9001
- return compareCodePoints(aId, bId);
9002
- });
9003
- if (group.limit !== void 0 && Number.isFinite(group.limit) && group.limit >= 0) {
9004
- const cap = Math.floor(group.limit);
9005
- if (groups.length > cap) {
9006
- groups.length = cap;
9007
- }
9008
- }
9009
- return groups;
9010
- }
9011
- function foldGroupReducer(reduce, rows) {
9012
- try {
9013
- let accumulator = reduce.initialValue();
9014
- for (const row of rows) {
9015
- if (row.document !== void 0) {
9016
- accumulator = reduce.reducer(accumulator, row.document, row.score);
9017
- }
9018
- }
9019
- return { reduced: accumulator };
9020
- } catch (err) {
9021
- return { reducerError: err instanceof Error ? err.message : String(err) };
9022
- }
9023
- }
9024
-
9025
- // src/search/pinning.ts
9026
- function dedupePinnedEntries(pinned) {
9027
- const deduped = [];
9028
- const seen = /* @__PURE__ */ new Set();
9029
- for (const entry of pinned) {
9030
- if (seen.has(entry.docId)) continue;
9031
- seen.add(entry.docId);
9032
- deduped.push(entry);
9033
- }
9034
- return deduped;
9035
- }
9036
- function applyPinning(hits, pinned, resolveDoc) {
9037
- const result = hits.slice();
9038
- const deduped = dedupePinnedEntries(pinned);
9039
- const pinnedDocIds = new Set(deduped.map((entry) => entry.docId));
9040
- for (let i = result.length - 1; i >= 0; i--) {
9041
- if (pinnedDocIds.has(result[i].id)) {
9042
- result.splice(i, 1);
9043
- }
9044
- }
9045
- const sorted = deduped.sort((a, b) => a.position - b.position);
9046
- for (const entry of sorted) {
9047
- const doc = resolveDoc(entry.docId);
9048
- if (!doc) continue;
9049
- const pos = Math.max(0, Math.min(entry.position, result.length));
9050
- result.splice(pos, 0, doc);
9051
- }
9052
- return result;
9053
- }
9054
-
9055
- // src/highlighting/highlighter.ts
9056
- function buildTokenCharOffsets(text, originalTokens) {
9057
- const offsets = [];
9058
- let searchFrom = 0;
9059
- for (const token of originalTokens) {
9060
- const idx = text.indexOf(token, searchFrom);
9061
- if (idx === -1) continue;
9062
- offsets.push({ start: idx, end: idx + token.length });
9063
- searchFrom = idx + token.length;
9064
- }
9065
- return offsets;
9066
- }
9067
- function mergeOverlapping(ranges) {
9068
- if (ranges.length === 0) return [];
9069
- const sorted = [...ranges].sort((a, b) => a.start - b.start);
9070
- const merged = [sorted[0]];
9071
- for (let i = 1; i < sorted.length; i++) {
9072
- const prev = merged[merged.length - 1];
9073
- const curr = sorted[i];
9074
- if (curr.start <= prev.end) {
9075
- prev.end = Math.max(prev.end, curr.end);
9076
- } else {
9077
- merged.push(curr);
9078
- }
9079
- }
9080
- return merged;
9081
- }
9082
- function findDensestWindow(positions, text, windowSize) {
9083
- if (positions.length === 0) {
9084
- return { start: 0, end: Math.min(windowSize, text.length) };
9085
- }
9086
- let bestStart = 0;
9087
- let bestCount = 0;
9088
- for (const pos of positions) {
9089
- const windowStart = Math.max(0, pos.start - Math.floor(windowSize / 2));
9090
- const windowEnd = Math.min(text.length, windowStart + windowSize);
9091
- const adjustedStart = Math.max(0, windowEnd - windowSize);
9092
- let count = 0;
9093
- for (const p of positions) {
9094
- if (p.start >= adjustedStart && p.end <= windowEnd) {
9095
- count++;
9096
- }
9097
- }
9098
- if (count > bestCount) {
9099
- bestCount = count;
9100
- bestStart = adjustedStart;
9101
- }
9102
- }
9103
- return { start: bestStart, end: Math.min(text.length, bestStart + windowSize) };
9104
- }
9105
- function applySnippetTruncation(taggedText, originalText, positions, maxLen, preTag, postTag) {
9106
- if (maxLen <= 0 || originalText.length <= maxLen) {
9107
- return { snippet: taggedText, adjustedPositions: positions };
9108
- }
9109
- const window = findDensestWindow(positions, originalText, maxLen);
9110
- const snippetRaw = originalText.substring(window.start, window.end);
9111
- const adjustedPositions = [];
9112
- for (const pos of positions) {
9113
- if (pos.start >= window.start && pos.end <= window.end) {
9114
- adjustedPositions.push({
9115
- start: pos.start - window.start,
9116
- end: pos.end - window.start
9117
- });
9118
- }
9119
- }
9120
- let snippet = snippetRaw;
9121
- const sortedDesc = [...adjustedPositions].sort((a, b) => b.start - a.start);
9122
- for (const pos of sortedDesc) {
9123
- snippet = snippet.substring(0, pos.start) + preTag + snippet.substring(pos.start, pos.end) + postTag + snippet.substring(pos.end);
9124
- }
9125
- const needsLeadingEllipsis = window.start > 0;
9126
- const needsTrailingEllipsis = window.end < originalText.length;
9127
- if (needsLeadingEllipsis) snippet = `...${snippet}`;
9128
- if (needsTrailingEllipsis) snippet = `${snippet}...`;
9129
- return { snippet, adjustedPositions };
9130
- }
9131
- function highlightField(text, queryTokens, language, options) {
9132
- const preTag = options?.preTag ?? "<mark>";
9133
- const postTag = options?.postTag ?? "</mark>";
9134
- const maxSnippetLength = options?.maxSnippetLength ?? 200;
9135
- if (text.length === 0) {
9136
- return { snippet: text, positions: [] };
9137
- }
9138
- const fieldResult = tokenize(text, language, { stem: false, removeStopWords: false });
9139
- if (fieldResult.tokens.length === 0) {
9140
- return { snippet: text, positions: [] };
9141
- }
9142
- const lowercaseText = text.normalize("NFC").toLowerCase();
9143
- const charOffsets = buildTokenCharOffsets(lowercaseText, fieldResult.originalTokens);
9144
- const stemmedQueryTokens = queryTokens.map((qt) => {
9145
- if (language.stemmer) {
9146
- return language.stemmer(qt.token.toLowerCase());
9147
- }
9148
- return qt.token.toLowerCase();
9149
- });
9150
- const matchedRanges = [];
9151
- const prefixToken = options?.prefixToken;
9152
- for (let i = 0; i < fieldResult.tokens.length; i++) {
9153
- if (i >= charOffsets.length) break;
9154
- const fieldToken = fieldResult.tokens[i].token;
9155
- const stemmedField = language.stemmer ? language.stemmer(fieldToken) : fieldToken;
9156
- if (prefixToken !== void 0 && fieldToken.startsWith(prefixToken)) {
9157
- matchedRanges.push({ start: charOffsets[i].start, end: charOffsets[i].end });
9158
- continue;
9159
- }
9160
- for (const stemmedQuery of stemmedQueryTokens) {
9161
- if (stemmedField === stemmedQuery) {
9162
- matchedRanges.push({ start: charOffsets[i].start, end: charOffsets[i].end });
9163
- break;
9164
- }
9165
- }
9166
- }
9167
- const mergedPositions = mergeOverlapping(matchedRanges);
9168
- if (mergedPositions.length === 0) {
9169
- if (maxSnippetLength > 0 && text.length > maxSnippetLength) {
9170
- const truncated = text.substring(0, maxSnippetLength);
9171
- return { snippet: `${truncated}...`, positions: [] };
9172
- }
9173
- return { snippet: text, positions: [] };
9174
- }
9175
- if (maxSnippetLength > 0 && text.length > maxSnippetLength) {
9176
- const { snippet, adjustedPositions } = applySnippetTruncation(
9177
- text,
9178
- text,
9179
- mergedPositions,
9180
- maxSnippetLength,
9181
- preTag,
9182
- postTag
9183
- );
9184
- return { snippet, positions: adjustedPositions };
9185
- }
9186
- let result = text;
9187
- const sortedDesc = [...mergedPositions].sort((a, b) => b.start - a.start);
9188
- for (const pos of sortedDesc) {
9189
- result = result.substring(0, pos.start) + preTag + result.substring(pos.start, pos.end) + postTag + result.substring(pos.end);
9190
- }
9191
- return { snippet: result, positions: mergedPositions };
9192
- }
9193
-
9194
- // src/engine/query/highlight.ts
9195
- function applyHighlights(hits, params, language, analysis) {
9196
- if (!params.highlight) return;
9197
- const queryTokenResult = tokenize(params.term ?? "", language, {
9198
- stem: true,
9199
- removeStopWords: true,
9200
- customTokenizer: analysis.customTokenizer,
9201
- stopWordOverride: analysis.stopWords
9202
- });
9203
- let prefixToken;
9204
- if (params.prefix === true && params.exact !== true && queryTokenResult.tokens.length > 0) {
9205
- const lastPosition = queryTokenResult.tokens[queryTokenResult.tokens.length - 1].position;
9206
- const unstemmed = tokenize(params.term ?? "", language, {
9207
- stem: false,
9208
- removeStopWords: true,
9209
- customTokenizer: analysis.customTokenizer,
9210
- stopWordOverride: analysis.stopWords
9211
- });
9212
- for (const t of unstemmed.tokens) {
9213
- if (t.position === lastPosition) {
9214
- prefixToken = t.token;
9215
- break;
9216
- }
9217
- }
9218
- }
9219
- for (const hit of hits) {
9220
- const highlights = {};
9221
- for (const field of params.highlight.fields) {
9222
- const doc = hit.document;
9223
- const fieldValue = doc[field];
9224
- if (typeof fieldValue === "string") {
9225
- highlights[field] = highlightField(fieldValue, queryTokenResult.tokens, language, {
9226
- preTag: params.highlight.preTag,
9227
- postTag: params.highlight.postTag,
9228
- maxSnippetLength: params.highlight.maxSnippetLength,
9229
- prefixToken
9230
- });
9231
- }
9232
- }
9233
- hit.highlights = highlights;
9234
- }
9235
- }
9236
-
9237
- // src/engine/query/shared.ts
9238
- function partitionsFor(manager, partitionIds) {
9239
- return partitionsIn(manager, partitionIds);
9240
- }
9241
- function coverageFor(manager, partitionIds) {
9242
- if (partitionIds === void 0) {
9243
- return {
9244
- totalPartitions: manager.partitionCount,
9245
- queriedPartitions: manager.partitionCount,
9246
- timedOutPartitions: 0,
9247
- failedPartitions: 0
9248
- };
9249
- }
9250
- const queriedPartitions = partitionsFor(manager, partitionIds).length;
9251
- return {
9252
- totalPartitions: partitionIds.length,
9253
- queriedPartitions,
9254
- timedOutPartitions: 0,
9255
- failedPartitions: partitionIds.length - queriedPartitions
9256
- };
9257
- }
9258
- function partitionsForVectorSearch(manager, vecIndex, partitionIds) {
9259
- if (partitionIds === void 0) {
9260
- return void 0;
9261
- }
9262
- if (!vecIndex.partitionsKnown()) {
9263
- vecIndex.assignPartitions((docId) => manager.partitionIdOf(docId));
9264
- }
9265
- return new Set(partitionIds);
9266
- }
9267
- function scoringConfigFor(params, context) {
9268
- const scoringMode = params.scoring ?? context.config.defaultScoring ?? "local";
9269
- if (scoringMode !== "broadcast") {
9270
- return { scoringMode, partitionIds: context.partitionIds };
9271
- }
9272
- return { scoringMode, globalStats: context.broadcastStats?.(context.indexName), partitionIds: context.partitionIds };
9273
- }
9274
- function broadcastStatsForWorker(params, context, scoring) {
9275
- if (scoring.scoringMode !== "broadcast" || scoring.globalStats === void 0) {
9276
- return void 0;
9277
- }
9278
- const term = params.term;
9279
- if (term === void 0 || term.trim().length === 0) {
9280
- return void 0;
9281
- }
9282
- return pruneStatsToQueryTerms(scoring.globalStats, term, context.language, context.manager.analysis);
9283
- }
9284
- function searchOptionsFor(manager) {
9285
- return {
9286
- bm25Params: manager.config.bm25,
9287
- stopWords: manager.analysis.stopWords,
9288
- customTokenizer: manager.analysis.customTokenizer
9289
- };
9290
- }
9291
- function collectFilterDocIds(manager, params, schema, partitionIds) {
9292
- const filterDocIds = /* @__PURE__ */ new Set();
9293
- if (!params.filters) return filterDocIds;
9294
- for (const partition of partitionsFor(manager, partitionIds)) {
9295
- const partitionFiltered = partition.applyFilters(params.filters, schema);
9296
- for (const docId of partitionFiltered) {
9297
- filterDocIds.add(docId);
9298
- }
9299
- }
9300
- return filterDocIds;
9301
- }
9302
- function vectorResultsToScored(results) {
9303
- return results.map((r) => ({
9304
- docId: r.docId,
9305
- score: r.score,
9306
- termFrequencies: {},
9307
- fieldLengths: {},
9308
- idf: {}
9309
- }));
9310
- }
9311
- function resolveVectorIndex(manager, fieldName) {
9312
- return manager.getVectorIndexes().get(fieldName);
9313
- }
9314
- function clampAlpha(alpha) {
9315
- if (alpha === void 0) return 0.5;
9316
- if (!Number.isFinite(alpha)) return 0.5;
9317
- if (alpha < 0) return 0;
9318
- if (alpha > 1) return 1;
9319
- return alpha;
9320
- }
9321
-
9322
- // src/engine/query/sorted.ts
9323
- function sortsWithoutScores(params) {
9324
- return params.includeScores !== true && params.includeScoreComponents !== true && params.minScore === void 0 && params.group === void 0 && params.pinned === void 0 && (params.termMatch === void 0 || params.termMatch === "any");
9325
- }
9326
- function executeSortedQueryPage(params, context, limit, offset, sortSignature) {
9327
- const { manager, language, config } = context;
9328
- const schema = config.schema;
9329
- let anchorKey = null;
9330
- let anchorId = null;
9331
- if (params.searchAfter !== void 0) {
9332
- const decoded = decodePageCursor(params.searchAfter);
9333
- requireMatchingCursor(decoded, params.searchAfter, sortSignature, true, context.cursorBinding);
9334
- anchorKey = decoded.sortKey;
9335
- anchorId = decoded.anchor;
9336
- }
9337
- const normalized = normalizeSort(params.sort);
9338
- const fields = normalized.map((entry) => entry.field);
9339
- const directions = normalized.map((entry) => entry.direction);
9340
- const flatSchema = flattenSchema(schema);
9341
- const fieldTypes = fields.map((field) => flatSchema[field]);
9342
- const options = searchOptionsFor(manager);
9343
- const partitions = partitionsFor(manager, context.partitionIds);
9344
- const partitionLimit = offset + limit + 1;
9345
- const merged = [];
9346
- const partitionFacets = [];
9347
- let count = 0;
9348
- for (const partition of partitions) {
9349
- const matches = fulltextMatches(partition, params, language, schema, options);
9350
- if (params.facets !== void 0) {
9351
- partitionFacets.push(
9352
- partition.computeFacets(
9353
- matches === null ? /* @__PURE__ */ new Set() : { ordinalBitset: matches.ordinalBitset() },
9354
- params.facets,
9355
- schema
9356
- )
9357
- );
9358
- }
9359
- if (matches === null || matches.count === 0) continue;
9360
- count += matches.count;
9361
- const page2 = partition.sortedPage({
9362
- fields,
9363
- directions,
9364
- fieldTypes,
9365
- limit: partitionLimit,
9366
- anchorKey,
9367
- anchorId,
9368
- matches
9369
- });
9370
- for (const entry of page2) merged.push(entry);
9371
- }
9372
- merged.sort((a, b) => compareComparableKeys(a.key, b.key, directions) || compareCodePoints(a.id, b.id));
9373
- const hasMore = merged.length > offset + limit;
9374
- const page = merged.slice(offset, offset + limit);
9375
- const last = page[page.length - 1];
9376
- let cursor;
9377
- if (hasMore && last !== void 0) {
9378
- cursor = encodePageCursor({
9379
- anchor: last.id,
9380
- score: null,
9381
- sortKey: last.key,
9382
- sortSignature,
9383
- binding: context.cursorBinding
9384
- });
9385
- }
9386
- return {
9387
- hits: page.map((entry) => ({ id: entry.id, document: void 0 })),
9388
- count,
9389
- cursor,
9390
- facets: partitionFacets.length > 0 ? mergeFacets(partitionFacets) : void 0
9391
- };
9392
- }
9393
-
9394
- // src/search/fusion.ts
9395
- function reciprocalRankFusion(lists, options) {
9396
- const k = options.k > 0 ? options.k : 60;
9397
- const scores = /* @__PURE__ */ new Map();
9398
- const docData = /* @__PURE__ */ new Map();
9399
- for (const list of lists) {
9400
- for (let rank = 0; rank < list.length; rank++) {
9401
- const doc = list[rank];
9402
- const rrfContribution = 1 / (k + rank + 1);
9403
- scores.set(doc.docId, (scores.get(doc.docId) ?? 0) + rrfContribution);
9404
- if (!docData.has(doc.docId)) {
9405
- docData.set(doc.docId, doc);
9406
- }
9407
- }
9408
- }
9409
- const result = [];
9410
- for (const [docId, score] of scores) {
9411
- const original = docData.get(docId);
9412
- if (!original) continue;
9413
- result.push({
9414
- docId,
9415
- score,
9416
- termFrequencies: original.termFrequencies,
9417
- fieldLengths: original.fieldLengths,
9418
- idf: original.idf
9419
- });
9420
- }
9421
- result.sort((a, b) => b.score - a.score || compareCodePoints(a.docId, b.docId));
9422
- return result;
9423
- }
9424
- function linearCombination(textResults, vectorResults, options) {
9425
- const { alpha } = options;
9426
- const normalizedText = minMaxNormalize(textResults);
9427
- const normalizedVector = minMaxNormalize(vectorResults);
9428
- const textScoreMap = /* @__PURE__ */ new Map();
9429
- for (const doc of normalizedText) {
9430
- textScoreMap.set(doc.docId, doc.score);
9431
- }
9432
- const textDocMap = /* @__PURE__ */ new Map();
9433
- for (const doc of textResults) {
9434
- textDocMap.set(doc.docId, doc);
9435
- }
9436
- const vectorScoreMap = /* @__PURE__ */ new Map();
9437
- for (const doc of normalizedVector) {
9438
- vectorScoreMap.set(doc.docId, doc.score);
9439
- }
9440
- const allDocIds = /* @__PURE__ */ new Set();
9441
- for (const doc of textResults) allDocIds.add(doc.docId);
9442
- for (const doc of vectorResults) allDocIds.add(doc.docId);
9443
- const result = [];
9444
- for (const docId of allDocIds) {
9445
- const tScore = textScoreMap.get(docId) ?? 0;
9446
- const vScore = vectorScoreMap.get(docId) ?? 0;
9447
- const combined = alpha * vScore + (1 - alpha) * tScore;
9448
- const original = textDocMap.get(docId);
9449
- result.push({
9450
- docId,
9451
- score: combined,
9452
- termFrequencies: original?.termFrequencies ?? {},
9453
- fieldLengths: original?.fieldLengths ?? {},
9454
- idf: original?.idf ?? {}
9455
- });
9456
- }
9457
- result.sort((a, b) => b.score - a.score || compareCodePoints(a.docId, b.docId));
9458
- return result;
9459
- }
9460
- function minMaxNormalize(docs) {
9461
- if (docs.length === 0) return [];
9462
- let min = docs[0].score;
9463
- let max = docs[0].score;
9464
- for (let i = 1; i < docs.length; i++) {
9465
- if (docs[i].score < min) min = docs[i].score;
9466
- if (docs[i].score > max) max = docs[i].score;
9467
- }
9468
- const range = max - min;
9469
- if (range === 0) {
9470
- return docs.map((d) => ({ docId: d.docId, score: 1 }));
9471
- }
9472
- return docs.map((d) => ({ docId: d.docId, score: (d.score - min) / range }));
9473
- }
9474
-
9475
- // src/engine/query/vector.ts
9476
- async function executeVectorSearch(params, manager, config, limit, offset, partitionIds) {
9477
- const vectorConfig = params.vector;
9478
- if (!vectorConfig || !vectorConfig.value) {
9479
- return { scored: [], totalMatched: 0 };
9480
- }
9481
- const vecIndex = resolveVectorIndex(manager, vectorConfig.field);
9482
- if (!vecIndex) {
9483
- return { scored: [], totalMatched: 0 };
9484
- }
9485
- let filterDocIds;
9486
- let filterPartitions;
9487
- if (params.filters) {
9488
- filterDocIds = collectFilterDocIds(manager, params, config.schema, partitionIds);
9489
- if (filterDocIds.size === 0) {
9490
- return { scored: [], totalMatched: 0 };
9491
- }
9492
- } else {
9493
- filterPartitions = partitionsForVectorSearch(manager, vecIndex, partitionIds);
9494
- }
9495
- const queryVec = new Float32Array(vectorConfig.value);
9496
- const k = limit + offset + 1;
9497
- const results = await vecIndex.searchParallel(queryVec, k, {
9498
- metric: vectorConfig.metric ?? "cosine",
9499
- minSimilarity: vectorConfig.similarity ?? -Infinity,
9500
- ...filterDocIds !== void 0 ? { filterDocIds } : {},
9501
- ...filterPartitions !== void 0 ? { filterPartitions } : {},
9502
- efSearch: vectorConfig.efSearch
9503
- });
9504
- const scored = vectorResultsToScored(results);
9505
- return { scored, totalMatched: scored.length };
9506
- }
9507
- async function executeHybridSearch(params, context, limit, offset) {
9508
- const { manager, language, config, workerSearch, indexName } = context;
9509
- const { vector: _vector, mode: _mode, hybrid: _hybrid, ...textOnlyParams } = params;
9510
- let filterDocIds;
9511
- if (params.filters) {
9512
- filterDocIds = collectFilterDocIds(manager, params, config.schema, context.partitionIds);
9513
- if (filterDocIds.size === 0) {
9514
- return { scored: [], totalMatched: 0 };
9515
- }
9516
- }
9517
- let textFanOutResult;
9518
- const scoring = scoringConfigFor(params, context);
9519
- const textWorkerResult = workerSearch ? await workerSearch(
9520
- indexName,
9521
- textOnlyParams,
9522
- broadcastStatsForWorker(textOnlyParams, context, scoring),
9523
- context.partitionIds
9524
- ) : null;
9525
- if (textWorkerResult) {
9526
- textFanOutResult = textWorkerResult;
9527
- } else {
9528
- textFanOutResult = await fanOutQuery(
9529
- manager,
9530
- textOnlyParams,
9531
- language,
9532
- config.schema,
9533
- scoring,
9534
- searchOptionsFor(manager)
9535
- );
9536
- }
9537
- const vectorConfig = params.vector;
9538
- if (!vectorConfig || !vectorConfig.value) {
9539
- return {
9540
- scored: textFanOutResult.scored,
9541
- totalMatched: textFanOutResult.totalMatched,
9542
- facets: textFanOutResult.facets
9543
- };
9544
- }
9545
- let vectorScored = [];
9546
- const vecIndex = resolveVectorIndex(manager, vectorConfig.field);
9547
- if (vecIndex) {
9548
- const queryVec = new Float32Array(vectorConfig.value);
9549
- const vectorK = limit + offset + 1;
9550
- const filterPartitions = filterDocIds === void 0 ? partitionsForVectorSearch(manager, vecIndex, context.partitionIds) : void 0;
9551
- const vectorResults = await vecIndex.searchParallel(queryVec, vectorK, {
9552
- metric: vectorConfig.metric ?? "cosine",
9553
- minSimilarity: vectorConfig.similarity ?? -Infinity,
9554
- ...filterDocIds !== void 0 ? { filterDocIds } : {},
9555
- ...filterPartitions !== void 0 ? { filterPartitions } : {},
9556
- efSearch: vectorConfig.efSearch
9557
- });
9558
- vectorScored = vectorResultsToScored(vectorResults);
9559
- }
9560
- const hybridConfig = params.hybrid ?? {};
9561
- const strategy = hybridConfig.strategy ?? "rrf";
9562
- let fusedScored;
9563
- if (strategy === "rrf") {
9564
- const rrfK = hybridConfig.k !== void 0 && hybridConfig.k > 0 ? hybridConfig.k : 60;
9565
- fusedScored = reciprocalRankFusion([textFanOutResult.scored, vectorScored], { k: rrfK });
9566
- } else {
9567
- const alpha = clampAlpha(hybridConfig.alpha);
9568
- fusedScored = linearCombination(textFanOutResult.scored, vectorScored, { alpha });
9569
- }
9570
- if (params.minScore !== void 0 && params.minScore > 0) {
9571
- const threshold = params.minScore;
9572
- fusedScored = fusedScored.filter((doc) => doc.score >= threshold);
9573
- }
9574
- return {
9575
- scored: fusedScored,
9576
- totalMatched: fusedScored.length,
9577
- facets: textFanOutResult.facets
9578
- };
9579
- }
9580
-
9581
- // src/engine/query/index.ts
9582
- async function executeQuery(params, context) {
9583
- const { manager, language, config, workerSearch, indexName } = context;
9584
- const startTime = now();
9585
- const limit = clampLimit(params.limit);
9586
- const offset = clampOffset(params.offset);
9587
- requireWithinResultWindow(limit, offset);
9588
- const sortFields = normalizeSort(params.sort);
9589
- const sortSignature = sortSignatureOf(params.sort);
9590
- const hasTerm = params.term !== void 0 && params.term.trim().length > 0;
9591
- const hasVector = params.vector !== void 0 && params.vector.value !== void 0;
9592
- const isHybridMode = params.mode === "hybrid" || hasTerm && hasVector;
9593
- const isVectorOnly = (params.mode === "vector" || hasVector && !hasTerm) && !isHybridMode;
9594
- if (sortSignature !== null && (isHybridMode || params.hybrid !== void 0)) {
9595
- throw new NarsilError(
9596
- ErrorCodes.SEARCH_INVALID_MODE,
9597
- "A hybrid query cannot carry a sort, because fusion defines the order of hybrid results",
9598
- { sort: sortFields.map((entry) => entry.field) }
9599
- );
9600
- }
9601
- requireSortableFields(params.sort, config.schema);
9602
- let paginated;
9603
- let nextCursor;
9604
- let count;
9605
- let facets;
9606
- let groups;
9607
- if (sortSignature !== null && hasTerm && !isVectorOnly && !isHybridMode && sortsWithoutScores(params)) {
9608
- const page = executeSortedQueryPage(params, context, limit, offset, sortSignature);
9609
- paginated = page.hits;
9610
- nextCursor = page.cursor;
9611
- count = page.count;
9612
- facets = page.facets;
9613
- } else {
9614
- const requestedVectorField = params.vector?.field;
9615
- const hasGlobalVectorIndex = requestedVectorField !== void 0 && manager.getVectorIndexes().has(requestedVectorField);
9616
- let fanOutResult;
9617
- if (isVectorOnly && hasGlobalVectorIndex) {
9618
- fanOutResult = await executeVectorSearch(params, manager, config, limit, offset, context.partitionIds);
9619
- } else if (isHybridMode && hasGlobalVectorIndex) {
9620
- fanOutResult = await executeHybridSearch(params, context, limit, offset);
9621
- } else {
9622
- const scoring = scoringConfigFor(params, context);
9623
- const workerResult = workerSearch ? await workerSearch(indexName, params, broadcastStatsForWorker(params, context, scoring), context.partitionIds) : null;
9624
- if (workerResult) {
9625
- fanOutResult = workerResult;
9626
- } else {
9627
- fanOutResult = await fanOutQuery(manager, params, language, config.schema, scoring, searchOptionsFor(manager));
9628
- }
9629
- }
9630
- const needsFullHits = params.sort !== void 0 || params.group !== void 0 || params.pinned !== void 0 || params.searchAfter !== void 0;
9631
- let hits;
9632
- if (needsFullHits) {
9633
- hits = fanOutResult.scored.map((scored) => ({
9634
- id: scored.docId,
9635
- score: scored.score,
9636
- document: void 0,
9637
- scoreComponents: params.includeScoreComponents ? { termFrequencies: scored.termFrequencies, fieldLengths: scored.fieldLengths, idf: scored.idf } : void 0
9638
- }));
9639
- } else {
9640
- const end = Math.min(offset + limit + 1, fanOutResult.scored.length);
9641
- hits = new Array(end);
9642
- for (let i = 0; i < end; i++) {
9643
- const scored = fanOutResult.scored[i];
9644
- hits[i] = {
9645
- id: scored.docId,
9646
- score: scored.score,
9647
- document: void 0,
9648
- scoreComponents: params.includeScoreComponents ? { termFrequencies: scored.termFrequencies, fieldLengths: scored.fieldLengths, idf: scored.idf } : void 0
9649
- };
9650
- }
9651
- }
9652
- const sortFieldNames = sortFields.map((entry) => entry.field);
9653
- const sortFlatSchema = sortFieldNames.length === 0 ? {} : flattenSchema(config.schema);
9654
- const sortFieldTypes = sortFieldNames.map((field) => sortFlatSchema[field]);
9655
- const sortKeyCache = /* @__PURE__ */ new Map();
9656
- const sortKeyOf = (docId) => {
9657
- let key = sortKeyCache.get(docId);
9658
- if (key === void 0) {
9659
- key = manager.sortValues(docId, sortFieldNames, sortFieldTypes);
9660
- sortKeyCache.set(docId, key);
9661
- }
9662
- return key;
9663
- };
9664
- if (params.sort) {
9665
- hits = applySorting(hits, params.sort, sortKeyOf);
9666
- }
9667
- if (params.group) {
9668
- groups = applyGrouping(hits, params.group, (docId) => manager.getRef(docId));
9669
- }
9670
- if (params.pinned && params.searchAfter === void 0 && context.partitionIds === void 0) {
9671
- hits = applyPinning(hits, params.pinned, (docId) => {
9672
- const doc = manager.getRef(docId);
9673
- if (!doc) return void 0;
9674
- return { id: docId, score: 0, document: doc };
9675
- });
9676
- }
9677
- let sortContext;
9678
- if (sortSignature !== null) {
9679
- sortContext = {
9680
- signature: sortSignature,
9681
- directions: sortFields.map((entry) => entry.direction),
9682
- sortKeyOf
9683
- };
9684
- }
9685
- const pinnedIds = params.pinned !== void 0 && params.searchAfter === void 0 ? new Set(params.pinned.map((entry) => entry.docId)) : void 0;
9686
- const paged = applyPagination(
9687
- hits,
9688
- limit,
9689
- offset,
9690
- context.cursorBinding,
9691
- params.searchAfter,
9692
- sortContext,
9693
- pinnedIds
9694
- );
9695
- paginated = paged.paginated;
9696
- nextCursor = paged.nextCursor;
9697
- count = fanOutResult.totalMatched;
9698
- facets = fanOutResult.facets;
9699
- if (sortSignature !== null && params.includeScores !== true) {
9700
- for (const hit of hits) hit.score = void 0;
9701
- }
9702
- }
9703
- const projection = resolveProjection(params.document);
9704
- if (projection.kind === "none") {
9705
- for (const hit of paginated) {
9706
- hit.document = {};
9707
- }
9708
- } else {
9709
- for (const hit of paginated) {
9710
- hit.document = manager.get(hit.id, projection) ?? {};
9711
- }
9712
- }
9713
- if (groups) {
9714
- for (const group of groups) {
9715
- for (const hit of group.hits) {
9716
- if (projection.kind === "none") {
9717
- hit.document = {};
9718
- continue;
9719
- }
9720
- hit.document = manager.get(hit.id, projection) ?? {};
9721
- }
9722
- }
9723
- }
9724
- if (params.highlight) {
9725
- applyHighlights(paginated, params, language, manager.analysis);
9726
- }
9727
- const elapsed = now() - startTime;
9728
- return {
9729
- hits: paginated,
9730
- count,
9731
- elapsed,
9732
- cursor: nextCursor,
9733
- facets,
9734
- groups,
9735
- coverage: coverageFor(manager, context.partitionIds)
9736
- };
9737
- }
9738
- async function executePreflight(params, context) {
9739
- const { manager, language, config, workerSearch, indexName } = context;
9740
- const startTime = now();
9741
- const hasTerm = params.term !== void 0 && params.term.trim().length > 0;
9742
- const hasVector = params.vector !== void 0 && params.vector.value !== void 0;
9743
- const isHybridMode = params.mode === "hybrid" || hasTerm && hasVector;
9744
- const isVectorOnly = (params.mode === "vector" || hasVector && !hasTerm) && !isHybridMode;
9745
- const requestedVectorField = params.vector?.field;
9746
- const hasGlobalVectorIndex = requestedVectorField !== void 0 && manager.getVectorIndexes().has(requestedVectorField);
9747
- let totalMatched;
9748
- const preflightLimit = 1e3;
9749
- const preflightOffset = 0;
9750
- if (isVectorOnly && hasGlobalVectorIndex) {
9751
- const result = await executeVectorSearch(
9752
- params,
9753
- manager,
9754
- config,
9755
- preflightLimit,
9756
- preflightOffset,
9757
- context.partitionIds
9758
- );
9759
- totalMatched = result.totalMatched;
9760
- } else if (isHybridMode && hasGlobalVectorIndex) {
9761
- const result = await executeHybridSearch(params, context, preflightLimit, preflightOffset);
9762
- totalMatched = result.totalMatched;
9763
- } else if (countsWithoutScores(params)) {
9764
- totalMatched = fanOutMatchCount(manager, params, language, config.schema, {
9765
- searchOptions: searchOptionsFor(manager),
9766
- partitionIds: context.partitionIds
9767
- });
9768
- } else {
9769
- const scoring = scoringConfigFor(params, context);
9770
- const workerResult = workerSearch ? await workerSearch(indexName, params, broadcastStatsForWorker(params, context, scoring), context.partitionIds) : null;
9771
- if (workerResult) {
9772
- totalMatched = workerResult.totalMatched;
9773
- } else {
9774
- const fanOutResult = await fanOutQuery(
9775
- manager,
9776
- params,
9777
- language,
9778
- config.schema,
9779
- scoring,
9780
- searchOptionsFor(manager)
9781
- );
9782
- totalMatched = fanOutResult.totalMatched;
9783
- }
9784
- }
9785
- const elapsed = now() - startTime;
9786
- return { count: totalMatched, elapsed };
9787
- }
9788
-
9789
- // src/engine/resolve-vector-text.ts
9790
- async function resolveVectorText(params, embeddingAdapter, signal, embeddingAdapterName) {
9791
- if (!params.vector) return params;
9792
- if (params.vector.text === void 0 && params.vector.value === void 0) return params;
9793
- if (params.vector.value !== void 0 && params.vector.text === void 0) return params;
9794
- if (params.vector.text !== void 0 && params.vector.value !== void 0) {
9795
- throw new NarsilError(ErrorCodes.EMBEDDING_CONFIG_INVALID, "Vector query cannot have both 'text' and 'value'");
9796
- }
9797
- if (!embeddingAdapter) {
9798
- throw new NarsilError(
9799
- ErrorCodes.EMBEDDING_CONFIG_INVALID,
9800
- embeddingAdapterName ? `Vector query with 'text' needs embedding adapter "${embeddingAdapterName}", which is not registered on this engine. Register it with registerEmbeddingAdapter or supply a raw vector.` : "Vector query with 'text' requires an embedding adapter on the index or instance",
9801
- embeddingAdapterName ? { adapter: embeddingAdapterName } : void 0
9802
- );
9803
- }
9804
- if (typeof params.vector.text !== "string") {
9805
- throw new NarsilError(
9806
- ErrorCodes.DOC_VALIDATION_FAILED,
9807
- `Vector query 'text' must be a string, got ${typeof params.vector.text}`
9808
- );
9809
- }
9810
- if (params.vector.text.length === 0) {
9811
- throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Vector query 'text' must not be empty");
9812
- }
9813
- const raw = await embeddingAdapter.embed(params.vector.text, "query", signal);
9814
- if (!(raw instanceof Float32Array)) {
9815
- throw new NarsilError(
9816
- ErrorCodes.EMBEDDING_FAILED,
9817
- `Adapter returned ${typeof raw} for query embedding, expected Float32Array`
9818
- );
9819
- }
9820
- if (raw.length !== embeddingAdapter.dimensions) {
9821
- throw new NarsilError(
9822
- ErrorCodes.EMBEDDING_DIMENSION_MISMATCH,
9823
- `Adapter returned ${raw.length}-dimensional vector for query, expected ${embeddingAdapter.dimensions}`,
9824
- { expected: embeddingAdapter.dimensions, actual: raw.length }
9825
- );
9826
- }
9827
- const resolved = { ...params.vector, value: Array.from(raw) };
9828
- delete resolved.text;
9829
- return { ...params, vector: resolved };
9830
- }
9831
-
9832
- // src/engine/suggest.ts
9833
- var DEFAULT_SUGGEST_LIMIT = 10;
9834
- var MAX_SUGGEST_LIMIT = 100;
9835
- var SUGGEST_OVERSAMPLE_FACTOR = 1.5;
9836
- var SUGGEST_OVERSAMPLE_PADDING = 10;
9837
- var MAX_SUGGEST_SCATTER_LIMIT = Math.ceil(MAX_SUGGEST_LIMIT * SUGGEST_OVERSAMPLE_FACTOR) + SUGGEST_OVERSAMPLE_PADDING;
9838
- function executeSuggest(manager, language, params, partitionIds) {
9839
- const t0 = performance.now();
9840
- const ceiling = partitionIds === void 0 ? MAX_SUGGEST_LIMIT : MAX_SUGGEST_SCATTER_LIMIT;
9841
- const limit = Math.max(1, Math.min(clampRowCount(params.limit, DEFAULT_SUGGEST_LIMIT), ceiling));
9842
- const rawPrefix = params.prefix.trim();
9843
- if (rawPrefix.length === 0) {
9844
- return { terms: [], elapsed: performance.now() - t0 };
9845
- }
9846
- const unstemmed = tokenize(rawPrefix, language, { stem: false, removeStopWords: false });
9847
- const lastToken = unstemmed.tokens.length > 0 ? unstemmed.tokens[unstemmed.tokens.length - 1].token : rawPrefix.toLowerCase();
9848
- if (lastToken.length === 0) {
9849
- return { terms: [], elapsed: performance.now() - t0 };
9850
- }
9851
- const stemmed = language.stemmer ? language.stemmer(lastToken) : lastToken;
9852
- const merged = /* @__PURE__ */ new Map();
9853
- const perPartitionLimit = limit * 2;
9854
- const partitions = partitionIds === void 0 ? manager.getAllPartitions() : partitionIds.map((partitionId) => manager.partitionAt(partitionId)).filter((partition) => partition !== void 0);
9855
- for (const partition of partitions) {
9856
- for (const suggestion of partition.suggestTerms(lastToken, stemmed, perPartitionLimit)) {
9857
- let entry = merged.get(suggestion.token);
9858
- if (!entry) {
9859
- entry = { documentFrequency: 0, surfaceOccurrences: /* @__PURE__ */ new Map() };
9860
- merged.set(suggestion.token, entry);
9861
- }
9862
- entry.documentFrequency += suggestion.documentFrequency;
9863
- for (const s of suggestion.surfaces) {
9864
- entry.surfaceOccurrences.set(s.surface, (entry.surfaceOccurrences.get(s.surface) ?? 0) + s.occurrences);
9865
- }
9866
- }
9867
- }
9868
- const terms = Array.from(merged.values()).map((entry) => ({ term: pickDisplaySurface(entry.surfaceOccurrences), documentFrequency: entry.documentFrequency })).sort((a, b) => b.documentFrequency - a.documentFrequency || compareCodePoints(a.term, b.term));
9869
- if (terms.length > limit) terms.length = limit;
9870
- return { terms, elapsed: performance.now() - t0 };
9871
- }
9872
- function pickDisplaySurface(surfaceOccurrences) {
9873
- let best = "";
9874
- let bestCount = -1;
9875
- for (const [surface, occurrences] of surfaceOccurrences) {
9876
- if (occurrences > bestCount || occurrences === bestCount && compareCodePoints(surface, best) < 0) {
9877
- best = surface;
9878
- bestCount = occurrences;
9879
- }
9880
- }
9881
- return best;
9882
- }
9883
-
9884
9196
  // src/narsil/reads.ts
9885
9197
  function scopedParams(params, options) {
9886
9198
  if (options?.globalStats === void 0) {
@@ -9912,7 +9224,7 @@ async function runEngineQuery(core, indexName, params, options) {
9912
9224
  entry.embeddingAdapterName
9913
9225
  );
9914
9226
  await core.pluginRegistry.runHook("beforeSearch", { indexName, params: resolvedParams });
9915
- const workerSearch = core.orchestrator.isPromoted() ? core.orchestrator.searchViaWorker.bind(core.orchestrator) : void 0;
9227
+ const workerSearch = core.orchestrator.hasWorkerPool() ? core.orchestrator.searchViaWorker.bind(core.orchestrator) : void 0;
9916
9228
  const result = await executeQuery(resolvedParams, {
9917
9229
  manager,
9918
9230
  language: entry.language,
@@ -9952,7 +9264,7 @@ async function runEnginePreflight(core, indexName, params, options) {
9952
9264
  core.abortController.signal,
9953
9265
  entry.embeddingAdapterName
9954
9266
  );
9955
- const workerSearch = core.orchestrator.isPromoted() ? core.orchestrator.searchViaWorker.bind(core.orchestrator) : void 0;
9267
+ const workerSearch = core.orchestrator.hasWorkerPool() ? core.orchestrator.searchViaWorker.bind(core.orchestrator) : void 0;
9956
9268
  const result = await executePreflight(resolvedParams, {
9957
9269
  manager,
9958
9270
  language: entry.language,
@@ -10115,17 +9427,22 @@ function createNarsilFromCore(core, config) {
10115
9427
  insertBatch(indexName, documents, options) {
10116
9428
  return withOpenIndex(indexName, () => insertDocumentBatch(mutationCtx, indexName, documents, options));
10117
9429
  },
10118
- remove(indexName, docId) {
10119
- return withOpenIndex(indexName, () => removeDocument(mutationCtx, indexName, docId));
9430
+ remove(indexName, docId, options) {
9431
+ return withOpenIndex(indexName, () => removeDocument(mutationCtx, indexName, docId, options));
9432
+ },
9433
+ removeBatch(indexName, docIds, options) {
9434
+ return withOpenIndex(indexName, () => removeDocumentBatch(mutationCtx, indexName, docIds, options));
10120
9435
  },
10121
- removeBatch(indexName, docIds) {
10122
- return withOpenIndex(indexName, () => removeDocumentBatch(mutationCtx, indexName, docIds));
9436
+ update(indexName, docId, document, options) {
9437
+ return withOpenIndex(indexName, () => updateDocument(mutationCtx, indexName, docId, document, options));
10123
9438
  },
10124
- update(indexName, docId, document) {
10125
- return withOpenIndex(indexName, () => updateDocument(mutationCtx, indexName, docId, document));
9439
+ updateBatch(indexName, updates, options) {
9440
+ return withOpenIndex(indexName, () => updateDocumentBatch(mutationCtx, indexName, updates, options));
10126
9441
  },
10127
- updateBatch(indexName, updates) {
10128
- return withOpenIndex(indexName, () => updateDocumentBatch(mutationCtx, indexName, updates));
9442
+ waitForWrites(indexName) {
9443
+ guardShutdown();
9444
+ requireIndex(indexName);
9445
+ return orchestrator.awaitWrites(indexName);
10129
9446
  },
10130
9447
  async get(indexName, docId) {
10131
9448
  return withOpenIndex(indexName, () => executor.execute({ type: "get", indexName, docId, requestId: docId }));
@@ -10189,6 +9506,7 @@ function createNarsilFromCore(core, config) {
10189
9506
  clearAnalysisStale: core.analysisRebuild.clearStale,
10190
9507
  indexState: core.indexState
10191
9508
  });
9509
+ core.heapPressureNotifier.check(indexName);
10192
9510
  },
10193
9511
  async checkpoint(indexName) {
10194
9512
  guardShutdown();
@@ -10268,6 +9586,7 @@ function createNarsilFromCore(core, config) {
10268
9586
  openIndexCount: lifecycleCounts.open,
10269
9587
  closedIndexCount: lifecycleCounts.closed,
10270
9588
  reopenCount: lifecycleCounts.reopens,
9589
+ workerCopies: orchestrator.workerCopies(),
10271
9590
  workers
10272
9591
  };
10273
9592
  },
@@ -10313,6 +9632,7 @@ function createNarsilFromCore(core, config) {
10313
9632
  await shutdownEngine(core);
10314
9633
  }
10315
9634
  };
9635
+ bindEngineCore(narsil, core);
10316
9636
  return narsil;
10317
9637
  }
10318
9638
 
@@ -10321,6 +9641,7 @@ async function createNarsil(config) {
10321
9641
  const core = createEngineCore(config);
10322
9642
  if (core.durability) {
10323
9643
  await core.durability.manager.recover(config?.lifecycle !== void 0);
9644
+ checkHeapAfterRecovery(core);
10324
9645
  }
10325
9646
  if (core.invalidation) {
10326
9647
  await core.invalidation.start();
@@ -10332,4 +9653,4 @@ async function createNarsil(config) {
10332
9653
  // src/index.ts
10333
9654
  var VERSION = "0.1.0";
10334
9655
 
10335
- export { DEFAULT_SUGGEST_LIMIT, MAX_SUGGEST_LIMIT, MAX_SUGGEST_SCATTER_LIMIT, SUGGEST_OVERSAMPLE_FACTOR, SUGGEST_OVERSAMPLE_PADDING, VERSION, WIRE_BATCH_BUDGET, applyDeleteEntry, applyIndexEntry, chunkByBudget, clampLimit, computeEntryChecksum, createAckMessage, createDurableDirectory, createEngineCore, createEngineIndex, createEntryBatchMessage, createEntryMessage, createForwardBatchMessage, createForwardBatchResultMessage, createForwardMessage, createInsyncAddMessage, createInsyncConfirmMessage, createInsyncRemoveMessage, createNarsil, createNarsilFromCore, dedupePinnedEntries, foldGroupReducer, listBindingOf, now, queryBindingOf, runEngineListDocuments, runEnginePreflight, runEngineQuery, runEngineQueryStats, runEngineSuggest, validateAckPayload, validateEntryBatchPayload, validateEntryPayload, validateForwardBatchPayload, validateForwardBatchResultPayload, validateInsyncConfirmPayload, validateReplicationEntry, writeSegmentedCheckpoint };
9656
+ export { VERSION, applyDeleteEntry, applyIndexEntry, checkHeapAfterRecovery, computeEntryChecksum, createAckMessage, createDurableDirectory, createEngineCore, createEngineIndex, createEntryBatchMessage, createEntryMessage, createForwardBatchMessage, createForwardBatchResultMessage, createForwardMessage, createInsyncAddMessage, createInsyncConfirmMessage, createInsyncRemoveMessage, createNarsil, createNarsilFromCore, runEngineListDocuments, runEnginePreflight, runEngineQuery, runEngineQueryStats, runEngineSuggest, validateAckPayload, validateEntryBatchPayload, validateEntryPayload, validateForwardBatchPayload, validateForwardBatchResultPayload, validateInsyncConfirmPayload, validateReplicationEntry, writeSegmentedCheckpoint };