@delali/narsil 0.2.4-next.43 → 0.2.4-next.44

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 (35) hide show
  1. package/dist/adapters/filesystem.mjs +1 -1
  2. package/dist/adapters/indexeddb.mjs +1 -1
  3. package/dist/{chunk-TLRUUMVE.mjs → chunk-P4ELXVJQ.mjs} +1 -0
  4. package/dist/{chunk-KEDL6FFZ.mjs → chunk-WASNWIEK.mjs} +1142 -327
  5. package/dist/{chunk-YGQMKTKG.mjs → chunk-XNCJFU2E.mjs} +121 -27
  6. package/dist/{chunk-O4Q5JYNK.mjs → chunk-XWICBGYT.mjs} +1 -1
  7. package/dist/{client-B5uO5jL1.d.ts → client-C5BkUqB8.d.ts} +25 -2
  8. package/dist/client.d.ts +7 -7
  9. package/dist/client.mjs +11 -2
  10. package/dist/{config-NgUCkc6a.d.ts → config-B-ZI-ft5.d.ts} +2 -2
  11. package/dist/{config-B09-cud9.d.ts → config-BZRExglL.d.ts} +20 -3
  12. package/dist/{direct-executor-7O23GEHE.mjs → direct-executor-IMXNRS7O.mjs} +2 -2
  13. package/dist/distribution/coordinator/etcd.d.ts +2 -2
  14. package/dist/distribution/coordinator/etcd.mjs +1 -1
  15. package/dist/distribution/coordinator/in-memory.d.ts +2 -2
  16. package/dist/distribution/transport/grpc.mjs +1 -1
  17. package/dist/distribution.d.ts +24 -10
  18. package/dist/distribution.mjs +1023 -864
  19. package/dist/embeddings/openai.mjs +1 -1
  20. package/dist/{engine-CjLhdDIm.d.ts → engine-Cjr1uBLa.d.ts} +28 -6
  21. package/dist/index.browser.mjs +1237 -330
  22. package/dist/index.d.ts +9 -9
  23. package/dist/index.mjs +4 -4
  24. package/dist/{options-BakECW-r.d.ts → options-BC68VPoE.d.ts} +9 -2
  25. package/dist/persistence/durability/checkpoint-worker.d.ts +3 -1
  26. package/dist/persistence/durability/checkpoint-worker.mjs +4 -4
  27. package/dist/react.d.ts +4 -4
  28. package/dist/react.mjs +1 -0
  29. package/dist/{schema-DXOd96kx.d.ts → schema-B7juWg8Q.d.ts} +1 -1
  30. package/dist/{search-BGouJxH4.d.ts → search-DDHjfXhJ.d.ts} +14 -3
  31. package/dist/server.d.ts +8 -8
  32. package/dist/server.mjs +27 -6
  33. package/dist/{types-BTE33VZS.d.ts → types-DsSSjY4d.d.ts} +1 -1
  34. package/dist/workers/entry.mjs +2 -2
  35. package/package.json +1 -1
@@ -1,10 +1,10 @@
1
1
  import { ReplicationMessageTypes } from './chunk-JQUL2T6I.mjs';
2
- import { MAX_INDEX_NAME_LENGTH } from './chunk-O4Q5JYNK.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, validateRequiredFields, mergePartitionStats, fulltextMatches, mergeFacets, pruneStatsToQueryTerms, createPartitionIndex, fnv1a, createWorkerPool, partitionsIn, validateDocument, validateDocumentStrict, createFrozenSegment, detectRuntime, isNodeMainThread, spawnNodeWorker, createSharedFrozenSegment, buildCompactedSegmentPayload, freezeSegmentShared, deserializeMetadata, isCompositePartition, serializeMetadata, kWayMerge } from './chunk-YGQMKTKG.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-XNCJFU2E.mjs';
4
4
  import { crc32, crc32Init, crc32Update, crc32Final } from './chunk-KMMJMDUA.mjs';
5
5
  import { compareCodePoints, compareComparableKeys } from './chunk-BFIH7OV5.mjs';
6
6
  import { generateId, createRequestId, isValidWorkerResponse } from './chunk-WTONLDU3.mjs';
7
- import { ErrorCodes, NarsilError } from './chunk-TLRUUMVE.mjs';
7
+ import { ErrorCodes, NarsilError } from './chunk-P4ELXVJQ.mjs';
8
8
  import { encode, decode } from '@msgpack/msgpack';
9
9
 
10
10
  // src/partitioning/rebalancer.ts
@@ -347,7 +347,7 @@ function createAnalysisRebuildCoordinator(config, deps) {
347
347
  async function rebuildNow(indexName, entry) {
348
348
  const manager = deps.getManager(indexName);
349
349
  if (manager === void 0) {
350
- stale.delete(indexName);
350
+ entry.run = null;
351
351
  return;
352
352
  }
353
353
  const partitionCount = manager.partitionCount;
@@ -399,6 +399,23 @@ function createAnalysisRebuildCoordinator(config, deps) {
399
399
  const documentCount = deps.getManager(indexName)?.countDocuments() ?? 0;
400
400
  await config?.onStaleAnalysis?.({ ...entry.index, documentCount }, () => enqueue2(indexName, entry));
401
401
  }
402
+ async function review(indexName, entry) {
403
+ if (entry.run !== null || deps.getManager(indexName) === void 0) return;
404
+ try {
405
+ await announce(indexName, entry);
406
+ } catch (err) {
407
+ deps.emit({
408
+ indexName,
409
+ status: "failed",
410
+ partitionsRebuilt: 0,
411
+ partitionCount: deps.getManager(indexName)?.partitionCount ?? 0,
412
+ error: toError(err)
413
+ });
414
+ }
415
+ if (rebuildMode === "auto") {
416
+ void enqueue2(indexName, entry).catch(() => void 0);
417
+ }
418
+ }
402
419
  return {
403
420
  markStale(index) {
404
421
  const existing = stale.get(index.indexName);
@@ -413,23 +430,16 @@ function createAnalysisRebuildCoordinator(config, deps) {
413
430
  isStale(indexName) {
414
431
  return stale.has(indexName);
415
432
  },
433
+ isRunning(indexName) {
434
+ return stale.get(indexName)?.run !== null && stale.get(indexName)?.run !== void 0;
435
+ },
436
+ review(indexName) {
437
+ const entry = stale.get(indexName);
438
+ return entry === void 0 ? Promise.resolve() : review(indexName, entry);
439
+ },
416
440
  async reviewStaleIndexes() {
417
441
  for (const [indexName, entry] of [...stale]) {
418
- if (entry.run !== null) continue;
419
- try {
420
- await announce(indexName, entry);
421
- } catch (err) {
422
- deps.emit({
423
- indexName,
424
- status: "failed",
425
- partitionsRebuilt: 0,
426
- partitionCount: deps.getManager(indexName)?.partitionCount ?? 0,
427
- error: toError(err)
428
- });
429
- }
430
- if (rebuildMode === "auto") {
431
- void enqueue2(indexName, entry).catch(() => void 0);
432
- }
442
+ await review(indexName, entry);
433
443
  }
434
444
  },
435
445
  rebuild(indexName) {
@@ -1835,6 +1845,9 @@ function mergeTimeOrderedSegments(ordered, fallback) {
1835
1845
  validateMergedPartition(merged);
1836
1846
  return merged;
1837
1847
  }
1848
+ function countLiveDocuments(ordered) {
1849
+ return resolveWinners(ordered).size;
1850
+ }
1838
1851
  function resolveWinners(ordered) {
1839
1852
  const winner = /* @__PURE__ */ new Map();
1840
1853
  for (let segIndex = 0; segIndex < ordered.length; segIndex += 1) {
@@ -3327,6 +3340,7 @@ async function writeSegmentedCheckpoint(input) {
3327
3340
  });
3328
3341
  }
3329
3342
  const partitions = [];
3343
+ let wholeDocumentCount = input.wholePartitionPayload === void 0 ? null : 0;
3330
3344
  for (const target of input.targets) {
3331
3345
  const priorPartition = priorManifest?.partitions.find((p) => p.partitionId === target.partitionId);
3332
3346
  const priorSeqNo = snapshotCheckpointFor(priorManifest?.checkpoint ?? [], target.partitionId);
@@ -3337,15 +3351,13 @@ async function writeSegmentedCheckpoint(input) {
3337
3351
  priorSeqNo,
3338
3352
  target.lastSeqNo
3339
3353
  );
3340
- partitions.push(
3341
- input.wholePartitionPayload === void 0 ? await writePartition(context, target.partitionId, priorPartition, entries) : await writeWholePartition(
3342
- context,
3343
- target.partitionId,
3344
- priorPartition,
3345
- input.wholePartitionPayload(target.partitionId),
3346
- entries
3347
- )
3348
- );
3354
+ if (input.wholePartitionPayload === void 0) {
3355
+ partitions.push(await writePartition(context, target.partitionId, priorPartition, entries));
3356
+ } else {
3357
+ const whole = input.wholePartitionPayload(target.partitionId);
3358
+ wholeDocumentCount = (wholeDocumentCount ?? 0) + whole.docCount;
3359
+ partitions.push(await writeWholePartition(context, target.partitionId, priorPartition, whole, entries));
3360
+ }
3349
3361
  }
3350
3362
  carryForwardUncheckpointedPartitions(priorManifest, input.targets, partitions, checkpointByPartition);
3351
3363
  const manifest = {
@@ -3357,6 +3369,7 @@ async function writeSegmentedCheckpoint(input) {
3357
3369
  const parts = await encodeSegmentManifest(manifest);
3358
3370
  await directory.atomicWrite(manifestKey(indexName), [parts.header, parts.payload]);
3359
3371
  await collectGarbage(directory, indexName, priorManifest, manifest);
3372
+ return wholeDocumentCount;
3360
3373
  }
3361
3374
  async function writePartition(context, partitionId, priorPartition, entries) {
3362
3375
  let segments = priorPartition ? [...priorPartition.segments] : [];
@@ -3476,9 +3489,9 @@ async function writeIndexCheckpoint(input) {
3476
3489
  const { directory, metadata, targets, compactionThreshold, manager } = input;
3477
3490
  const offloaded = !input.fromMemory && input.canOffload && metadata.tokenizer === void 0 && metadata.stopWords === void 0 && await runCheckpointOnWorker({ root: directory.root, metadata, targets, compactionThreshold });
3478
3491
  if (offloaded) {
3479
- return;
3492
+ return null;
3480
3493
  }
3481
- await writeSegmentedCheckpoint({
3494
+ return writeSegmentedCheckpoint({
3482
3495
  directory,
3483
3496
  metadata,
3484
3497
  targets,
@@ -3510,9 +3523,171 @@ function createSeqOwner(startSeqNo, primaryTerm = SINGLE_NODE_PRIMARY_TERM) {
3510
3523
  };
3511
3524
  }
3512
3525
 
3526
+ // src/persistence/durability/checkpoint-run.ts
3527
+ function toError2(error) {
3528
+ return error instanceof Error ? error : new Error(String(error));
3529
+ }
3530
+ async function runDurableCheckpoint(input) {
3531
+ const manager = input.hooks.getManager(input.indexName);
3532
+ if (manager === void 0) {
3533
+ return;
3534
+ }
3535
+ const metadata = input.hooks.buildMetadata(input.indexName);
3536
+ if (metadata === void 0) {
3537
+ return;
3538
+ }
3539
+ const targets = [];
3540
+ const documentCount = manager.countDocuments();
3541
+ for (let i = 0; i < manager.partitionCount; i += 1) {
3542
+ const partition = input.indexState.partitions.get(i);
3543
+ targets.push({
3544
+ partitionId: i,
3545
+ lastSeqNo: partition?.appliedSeqNo ?? 0,
3546
+ primaryTerm: partition?.seqOwner.primaryTerm ?? SINGLE_NODE_PRIMARY_TERM
3547
+ });
3548
+ }
3549
+ for (let i = 0; i < manager.partitionCount; i += 1) {
3550
+ const partition = input.indexState.partitions.get(i);
3551
+ if (partition === void 0) {
3552
+ continue;
3553
+ }
3554
+ try {
3555
+ await partition.walWriter.commit();
3556
+ } catch (error) {
3557
+ partition.failed = toError2(error);
3558
+ input.markFatal(partition.failed);
3559
+ throw partition.failed;
3560
+ }
3561
+ }
3562
+ const writtenDocumentCount = await writeIndexCheckpoint({
3563
+ directory: input.directory,
3564
+ metadata,
3565
+ targets,
3566
+ compactionThreshold: input.compactionThreshold,
3567
+ manager,
3568
+ canOffload: input.canOffload,
3569
+ fromMemory: input.fromMemory
3570
+ });
3571
+ const checkpointDocumentCount = writtenDocumentCount ?? documentCount;
3572
+ await input.queueMetadataWrite(input.indexName, async () => {
3573
+ const checkpointMetadata = input.hooks.buildMetadata(input.indexName, checkpointDocumentCount);
3574
+ if (checkpointMetadata === void 0) {
3575
+ return;
3576
+ }
3577
+ const bytes = await writeMetadataEnvelope(checkpointMetadata, { checksum: true });
3578
+ await input.directory.atomicWrite(`${input.indexName}/meta`, bytes);
3579
+ input.hooks.recordCheckpoint?.(input.indexName, checkpointDocumentCount, manager.partitionCount);
3580
+ });
3581
+ await truncateCoveredSegments(input.directory, input.indexName, targets);
3582
+ await reclaimWalBeyondCount(
3583
+ input.directory,
3584
+ input.indexName,
3585
+ manager.partitionCount,
3586
+ input.indexState.partitions,
3587
+ input.markFatal
3588
+ );
3589
+ input.indexState.mutationsSinceCheckpoint = 0;
3590
+ }
3591
+
3592
+ // src/persistence/durability/manager-state.ts
3593
+ async function drainIndexStateForUnload(indexState, pendingMetadataWrite, closeWalWriter) {
3594
+ indexState.unloading = true;
3595
+ try {
3596
+ if (indexState.checkpointInFlight !== null) await indexState.checkpointInFlight;
3597
+ await pendingMetadataWrite;
3598
+ for (const partition of indexState.partitions.values()) {
3599
+ await closeWalWriter(partition.walWriter.close());
3600
+ }
3601
+ } catch (error) {
3602
+ indexState.unloading = false;
3603
+ throw error;
3604
+ }
3605
+ }
3606
+
3607
+ // src/persistence/durability/checkpoint-count.ts
3608
+ async function countCheckpointDocuments(directory, indexName) {
3609
+ const manifest = await readSegmentManifest(directory, indexName);
3610
+ if (manifest !== null) {
3611
+ let total = 0;
3612
+ for (const partition of manifest.partitions) {
3613
+ const ordered = [];
3614
+ for (const segment of partition.segments) {
3615
+ ordered.push(await readSegmentContents(directory, segment.key));
3616
+ }
3617
+ total += countLiveDocuments(ordered);
3618
+ }
3619
+ return total;
3620
+ }
3621
+ const bytes = await directory.read(legacySnapshotKey(indexName));
3622
+ if (bytes === null) {
3623
+ return 0;
3624
+ }
3625
+ return countSnapshotBundleDocuments(bytes);
3626
+ }
3627
+ async function countSnapshotBundleDocuments(bytes) {
3628
+ const bundle = await decodeSnapshotBundle(bytes);
3629
+ let total = 0;
3630
+ for (const partitionBytes of bundle.partitions) {
3631
+ total += Object.keys(deserializePayloadV2(partitionBytes).documents).length;
3632
+ }
3633
+ return total;
3634
+ }
3635
+
3636
+ // src/persistence/durability/recover-index.ts
3637
+ async function recoverPersistedIndex(deps, indexName, metadataOnly) {
3638
+ const { directory, hooks } = deps;
3639
+ const metadata = await loadMetadata(directory, indexName);
3640
+ if (metadata === null) {
3641
+ return;
3642
+ }
3643
+ let derivedCount;
3644
+ if (metadataOnly && metadata.documentCount === void 0) {
3645
+ try {
3646
+ derivedCount = await countCheckpointDocuments(directory, indexName);
3647
+ metadata.documentCount = derivedCount;
3648
+ } catch {
3649
+ derivedCount = void 0;
3650
+ }
3651
+ }
3652
+ await hooks.createIndexFromMetadata(metadata, !metadataOnly);
3653
+ if (metadataOnly) {
3654
+ if (derivedCount !== void 0) {
3655
+ await deps.queueMetadataWrite(indexName, async () => {
3656
+ const upgraded = hooks.buildMetadata(indexName, derivedCount);
3657
+ if (upgraded === void 0) {
3658
+ return;
3659
+ }
3660
+ const bytes = await writeMetadataEnvelope(upgraded, { checksum: true });
3661
+ await directory.atomicWrite(`${indexName}/meta`, bytes);
3662
+ }).catch(() => void 0);
3663
+ }
3664
+ return;
3665
+ }
3666
+ const manager = hooks.getManager(indexName);
3667
+ if (manager === void 0) {
3668
+ return;
3669
+ }
3670
+ const replayDeps = {
3671
+ manager,
3672
+ vectorFieldPaths: hooks.getVectorFieldPaths(indexName),
3673
+ vectorIndexes: hooks.getVectorIndexes(indexName)
3674
+ };
3675
+ const checkpoint = await loadSnapshot(directory, indexName, replayDeps);
3676
+ const manifest = await readSegmentManifest(directory, indexName);
3677
+ if (manifest !== null) {
3678
+ await reclaimOrphanedSegments(directory, indexName, manifest);
3679
+ }
3680
+ for (let partitionId = 0; partitionId < manager.partitionCount; partitionId += 1) {
3681
+ const fromSeqNo = snapshotCheckpointFor(checkpoint, partitionId);
3682
+ const { highestSeqNo } = await replayWal(directory, indexName, partitionId, fromSeqNo, replayDeps);
3683
+ deps.registerPartition(indexName, partitionId, highestSeqNo);
3684
+ }
3685
+ }
3686
+
3513
3687
  // src/persistence/durability/types.ts
3514
3688
  var DEFAULT_CHECKPOINT_INTERVAL_MS = 3e5;
3515
3689
  var DEFAULT_CHECKPOINT_MUTATION_THRESHOLD = 1e5;
3690
+ var DEFAULT_ASYNC_FLUSH_INTERVAL_MS = 1e3;
3516
3691
 
3517
3692
  // src/persistence/durability/group-commit.ts
3518
3693
  function createGroupCommitCoordinator(sync) {
@@ -3758,7 +3933,6 @@ function createWalWriter(directory, config) {
3758
3933
  }
3759
3934
 
3760
3935
  // src/persistence/durability/manager.ts
3761
- var DEFAULT_ASYNC_FLUSH_INTERVAL_MS = 1e3;
3762
3936
  function createDurabilityManager(config, hooks, directoryOverride) {
3763
3937
  const directory = createDurableDirectory(config.directory ?? "");
3764
3938
  const canOffloadCheckpoint = directoryOverride === void 0;
@@ -3789,20 +3963,20 @@ function createDurabilityManager(config, hooks, directoryOverride) {
3789
3963
  }
3790
3964
  hooks.onFatalError(error);
3791
3965
  }
3792
- function toError3(err) {
3966
+ function toError4(err) {
3793
3967
  return err instanceof Error ? err : new Error(String(err));
3794
3968
  }
3795
3969
  async function closeWriterReportingFailure(closePromise) {
3796
3970
  try {
3797
3971
  await closePromise;
3798
3972
  } catch (err) {
3799
- markFatal(toError3(err));
3973
+ markFatal(toError4(err));
3800
3974
  }
3801
3975
  }
3802
3976
  function getOrCreateIndexState(indexName) {
3803
3977
  let state = indexes.get(indexName);
3804
3978
  if (state === void 0) {
3805
- state = { partitions: /* @__PURE__ */ new Map(), mutationsSinceCheckpoint: 0, checkpointInFlight: null };
3979
+ state = { partitions: /* @__PURE__ */ new Map(), mutationsSinceCheckpoint: 0, checkpointInFlight: null, unloading: false };
3806
3980
  indexes.set(indexName, state);
3807
3981
  }
3808
3982
  return state;
@@ -3865,7 +4039,7 @@ function createDurabilityManager(config, hooks, directoryOverride) {
3865
4039
  try {
3866
4040
  await partition.walWriter.commit();
3867
4041
  } catch (err) {
3868
- partition.failed = toError3(err);
4042
+ partition.failed = toError4(err);
3869
4043
  markFatal(partition.failed);
3870
4044
  }
3871
4045
  }
@@ -3879,7 +4053,7 @@ function createDurabilityManager(config, hooks, directoryOverride) {
3879
4053
  try {
3880
4054
  await checkpointIndex(indexName);
3881
4055
  } catch (err) {
3882
- markFatal(toError3(err));
4056
+ markFatal(toError4(err));
3883
4057
  }
3884
4058
  }
3885
4059
  }
@@ -3896,7 +4070,7 @@ function createDurabilityManager(config, hooks, directoryOverride) {
3896
4070
  }
3897
4071
  async function checkpointIndex(indexName, fromMemory = false) {
3898
4072
  const indexState = indexes.get(indexName);
3899
- if (indexState === void 0) {
4073
+ if (indexState === void 0 || indexState.unloading) {
3900
4074
  return;
3901
4075
  }
3902
4076
  if (indexState.checkpointInFlight !== null) {
@@ -3912,82 +4086,46 @@ function createDurabilityManager(config, hooks, directoryOverride) {
3912
4086
  return run;
3913
4087
  }
3914
4088
  async function performCheckpoint(indexName, indexState, fromMemory = false) {
3915
- const manager = hooks.getManager(indexName);
3916
- if (manager === void 0) {
3917
- return;
3918
- }
3919
- const metadata = hooks.buildMetadata(indexName);
3920
- if (metadata === void 0) {
3921
- return;
3922
- }
3923
- const targets = [];
3924
- for (let i = 0; i < manager.partitionCount; i += 1) {
3925
- const partition = indexState.partitions.get(i);
3926
- targets.push({
3927
- partitionId: i,
3928
- lastSeqNo: partition?.appliedSeqNo ?? 0,
3929
- primaryTerm: partition?.seqOwner.primaryTerm ?? SINGLE_NODE_PRIMARY_TERM
3930
- });
3931
- if (partition !== void 0) {
3932
- try {
3933
- await partition.walWriter.commit();
3934
- } catch (err) {
3935
- partition.failed = toError3(err);
3936
- markFatal(partition.failed);
3937
- throw partition.failed;
3938
- }
3939
- }
3940
- }
3941
- await writeIndexCheckpoint({
4089
+ await runDurableCheckpoint({
3942
4090
  directory,
3943
- metadata,
3944
- targets,
4091
+ hooks,
4092
+ indexName,
4093
+ indexState,
3945
4094
  compactionThreshold,
3946
- manager,
3947
4095
  canOffload: canOffloadCheckpoint,
3948
- fromMemory
4096
+ fromMemory,
4097
+ queueMetadataWrite,
4098
+ markFatal
3949
4099
  });
3950
- await truncateCoveredSegments(directory, indexName, targets);
3951
- await reclaimWalBeyondCount(directory, indexName, manager.partitionCount, indexState.partitions, markFatal);
3952
- indexState.mutationsSinceCheckpoint = 0;
3953
4100
  }
3954
- async function recoverIndex(indexName) {
3955
- const metadata = await loadMetadata(directory, indexName);
3956
- if (metadata === null) {
3957
- return;
3958
- }
3959
- await hooks.createIndexFromMetadata(metadata);
3960
- const manager = hooks.getManager(indexName);
3961
- if (manager === void 0) {
3962
- return;
3963
- }
3964
- const deps = {
3965
- manager,
3966
- vectorFieldPaths: hooks.getVectorFieldPaths(indexName),
3967
- vectorIndexes: hooks.getVectorIndexes(indexName)
3968
- };
3969
- const checkpoint = await loadSnapshot(directory, indexName, deps);
3970
- const manifest = await readSegmentManifest(directory, indexName);
3971
- if (manifest !== null) {
3972
- await reclaimOrphanedSegments(directory, indexName, manifest);
3973
- }
3974
- for (let partitionId = 0; partitionId < manager.partitionCount; partitionId += 1) {
3975
- const fromSeqNo = snapshotCheckpointFor(checkpoint, partitionId);
3976
- const { highestSeqNo } = await replayWal(directory, indexName, partitionId, fromSeqNo, deps);
3977
- getOrCreatePartition(indexName, partitionId, highestSeqNo);
3978
- }
4101
+ function recoverIndex(indexName, metadataOnly = false) {
4102
+ return recoverPersistedIndex(
4103
+ {
4104
+ directory,
4105
+ hooks,
4106
+ registerPartition: (name, partitionId, startSeqNo) => {
4107
+ getOrCreatePartition(name, partitionId, startSeqNo);
4108
+ },
4109
+ queueMetadataWrite
4110
+ },
4111
+ indexName,
4112
+ metadataOnly
4113
+ );
3979
4114
  }
3980
4115
  return {
3981
4116
  isActive() {
3982
4117
  return true;
3983
4118
  },
3984
- async recover() {
4119
+ async recover(metadataOnly = false) {
3985
4120
  const names = await listPersistedIndexes(directory);
3986
4121
  for (const indexName of names) {
3987
- await recoverIndex(indexName);
4122
+ await recoverIndex(indexName, metadataOnly);
3988
4123
  }
3989
4124
  startCheckpointTimer();
3990
4125
  },
4126
+ recoverIndex(indexName) {
4127
+ return recoverIndex(indexName);
4128
+ },
3991
4129
  highestPersistedSeqNo(indexName, partitionId) {
3992
4130
  return indexes.get(indexName)?.partitions.get(partitionId)?.appliedSeqNo ?? 0;
3993
4131
  },
@@ -4011,7 +4149,7 @@ function createDurabilityManager(config, hooks, directoryOverride) {
4011
4149
  const entry = buildMutationEntry(record, allocatedSeqNo);
4012
4150
  await partition.walWriter.append(entry);
4013
4151
  } catch (err) {
4014
- partition.failed = toError3(err);
4152
+ partition.failed = toError4(err);
4015
4153
  markFatal(partition.failed);
4016
4154
  throw partition.failed;
4017
4155
  }
@@ -4023,7 +4161,7 @@ function createDurabilityManager(config, hooks, directoryOverride) {
4023
4161
  try {
4024
4162
  await partition.walWriter.commit();
4025
4163
  } catch (err) {
4026
- partition.failed = toError3(err);
4164
+ partition.failed = toError4(err);
4027
4165
  markFatal(partition.failed);
4028
4166
  throw partition.failed;
4029
4167
  }
@@ -4033,7 +4171,7 @@ function createDurabilityManager(config, hooks, directoryOverride) {
4033
4171
  startAsyncFlushTimer();
4034
4172
  if (indexState.mutationsSinceCheckpoint >= checkpointMutationThreshold) {
4035
4173
  void checkpointIndex(record.indexName).catch((err) => {
4036
- markFatal(toError3(err));
4174
+ markFatal(toError4(err));
4037
4175
  });
4038
4176
  }
4039
4177
  return allocatedSeqNo;
@@ -4063,15 +4201,23 @@ function createDurabilityManager(config, hooks, directoryOverride) {
4063
4201
  async removeIndex(indexName) {
4064
4202
  const indexState = indexes.get(indexName);
4065
4203
  if (indexState !== void 0) {
4066
- for (const partition of indexState.partitions.values()) {
4067
- await closeWriterReportingFailure(partition.walWriter.close());
4068
- }
4204
+ await drainIndexStateForUnload(indexState, metadataWrites.get(indexName), closeWriterReportingFailure);
4069
4205
  indexes.delete(indexName);
4206
+ metadataWrites.delete(indexName);
4070
4207
  }
4071
4208
  for (const key of await directory.list(`${indexName}/`)) {
4072
4209
  await directory.remove(key);
4073
4210
  }
4074
4211
  },
4212
+ async unloadIndex(indexName) {
4213
+ const indexState = indexes.get(indexName);
4214
+ if (indexState === void 0) {
4215
+ return;
4216
+ }
4217
+ await drainIndexStateForUnload(indexState, metadataWrites.get(indexName), (close) => close);
4218
+ indexes.delete(indexName);
4219
+ metadataWrites.delete(indexName);
4220
+ },
4075
4221
  async shutdown() {
4076
4222
  shuttingDown = true;
4077
4223
  if (checkpointTimer !== null) {
@@ -4110,7 +4256,13 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
4110
4256
  function getOrCreateIndexState(indexName) {
4111
4257
  let state = indexes.get(indexName);
4112
4258
  if (state === void 0) {
4113
- state = { mutationsSinceCheckpoint: 0, checkpointInFlight: null, applyChain: Promise.resolve() };
4259
+ state = {
4260
+ mutationsSinceCheckpoint: 0,
4261
+ checkpointInFlight: null,
4262
+ applyChain: Promise.resolve(),
4263
+ unloading: false,
4264
+ appliedSeqNoByPartition: /* @__PURE__ */ new Map()
4265
+ };
4114
4266
  indexes.set(indexName, state);
4115
4267
  }
4116
4268
  return state;
@@ -4141,10 +4293,11 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
4141
4293
  if (manager === void 0 || metadata === void 0) {
4142
4294
  return;
4143
4295
  }
4296
+ const documentCount = manager.countDocuments();
4144
4297
  const seqNoByPartition = /* @__PURE__ */ new Map();
4145
4298
  const primaryTermByPartition = /* @__PURE__ */ new Map();
4146
4299
  for (let i = 0; i < manager.partitionCount; i += 1) {
4147
- seqNoByPartition.set(i, 0);
4300
+ seqNoByPartition.set(i, indexState.appliedSeqNoByPartition.get(i) ?? 0);
4148
4301
  primaryTermByPartition.set(i, SINGLE_NODE_PRIMARY_TERM);
4149
4302
  }
4150
4303
  const { parts } = await buildSnapshotBundleBytes({
@@ -4160,6 +4313,13 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
4160
4313
  primaryTermByPartition
4161
4314
  });
4162
4315
  await adapter.save(snapshotStorageKey(indexName), concatEnvelopeParts(parts));
4316
+ await queueMetadataWrite(indexName, async () => {
4317
+ const checkpointMetadata = hooks.buildMetadata(indexName, documentCount);
4318
+ if (checkpointMetadata === void 0) return;
4319
+ const metadataBytes = await writeMetadataEnvelope(checkpointMetadata, { checksum: true });
4320
+ await adapter.save(metadataKey(indexName), metadataBytes);
4321
+ hooks.recordCheckpoint?.(indexName, documentCount, manager.partitionCount);
4322
+ });
4163
4323
  indexState.mutationsSinceCheckpoint = 0;
4164
4324
  if (publisher !== void 0) {
4165
4325
  const partitionIds = [];
@@ -4169,15 +4329,18 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
4169
4329
  await publisher.publishPartitions(indexName, partitionIds);
4170
4330
  }
4171
4331
  }
4172
- async function checkpointIndex(indexName) {
4332
+ async function checkpointIndex(indexName, forceFresh = false) {
4173
4333
  const indexState = indexes.get(indexName);
4174
- if (indexState === void 0) {
4334
+ if (indexState === void 0 || indexState.unloading) {
4175
4335
  return;
4176
4336
  }
4177
4337
  if (indexState.checkpointInFlight !== null) {
4178
- return indexState.checkpointInFlight;
4338
+ await indexState.checkpointInFlight;
4339
+ if (!forceFresh) {
4340
+ return;
4341
+ }
4179
4342
  }
4180
- const run = performCheckpoint(indexName, indexState).finally(() => {
4343
+ const run = indexState.applyChain.then(() => performCheckpoint(indexName, indexState)).finally(() => {
4181
4344
  indexState.checkpointInFlight = null;
4182
4345
  });
4183
4346
  indexState.checkpointInFlight = run;
@@ -4192,13 +4355,35 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
4192
4355
  );
4193
4356
  return queued;
4194
4357
  }
4195
- async function recoverIndex(indexName) {
4358
+ async function recoverIndex(indexName, metadataOnly = false) {
4196
4359
  const metaBytes = await adapter.load(metadataKey(indexName));
4197
4360
  if (metaBytes === null) {
4198
4361
  return;
4199
4362
  }
4200
4363
  const { metadata } = await readMetadataEnvelope(metaBytes);
4201
- await hooks.createIndexFromMetadata(metadata);
4364
+ let derivedCount;
4365
+ if (metadataOnly && metadata.documentCount === void 0) {
4366
+ try {
4367
+ derivedCount = await countStoredBundleDocuments(indexName);
4368
+ metadata.documentCount = derivedCount;
4369
+ } catch {
4370
+ derivedCount = void 0;
4371
+ }
4372
+ }
4373
+ await hooks.createIndexFromMetadata(metadata, !metadataOnly);
4374
+ if (metadataOnly) {
4375
+ if (derivedCount !== void 0) {
4376
+ await queueMetadataWrite(indexName, async () => {
4377
+ const upgraded = hooks.buildMetadata(indexName, derivedCount);
4378
+ if (upgraded === void 0) {
4379
+ return;
4380
+ }
4381
+ const bytes = await writeMetadataEnvelope(upgraded, { checksum: true });
4382
+ await adapter.save(metadataKey(indexName), bytes);
4383
+ }).catch(() => void 0);
4384
+ }
4385
+ return;
4386
+ }
4202
4387
  const manager = hooks.getManager(indexName);
4203
4388
  if (manager === void 0) {
4204
4389
  return;
@@ -4207,33 +4392,58 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
4207
4392
  if (snapshotBytes === null) {
4208
4393
  return;
4209
4394
  }
4210
- await loadSnapshotBundleBytes(snapshotBytes, {
4395
+ const checkpoint = await loadSnapshotBundleBytes(snapshotBytes, {
4211
4396
  manager,
4212
4397
  vectorFieldPaths: hooks.getVectorFieldPaths(indexName),
4213
4398
  vectorIndexes: hooks.getVectorIndexes(indexName)
4214
4399
  });
4400
+ const indexState = getOrCreateIndexState(indexName);
4401
+ for (let partitionId = 0; partitionId < manager.partitionCount; partitionId += 1) {
4402
+ const persistedSeqNo = snapshotCheckpointFor(checkpoint, partitionId);
4403
+ if (persistedSeqNo > 0) {
4404
+ indexState.appliedSeqNoByPartition.set(partitionId, persistedSeqNo);
4405
+ }
4406
+ }
4407
+ }
4408
+ async function countStoredBundleDocuments(indexName) {
4409
+ const snapshotBytes = await adapter.load(snapshotStorageKey(indexName));
4410
+ if (snapshotBytes === null) {
4411
+ return 0;
4412
+ }
4413
+ return countSnapshotBundleDocuments(snapshotBytes);
4215
4414
  }
4216
4415
  return {
4217
4416
  isActive() {
4218
4417
  return true;
4219
4418
  },
4220
- async recover() {
4419
+ async recover(metadataOnly = false) {
4221
4420
  const keys = await adapter.list("");
4222
4421
  for (const key of keys) {
4223
4422
  if (key.endsWith("/meta")) {
4224
- await recoverIndex(key.slice(0, -"/meta".length));
4423
+ await recoverIndex(key.slice(0, -"/meta".length), metadataOnly);
4225
4424
  }
4226
4425
  }
4227
4426
  startCheckpointTimer();
4228
4427
  },
4229
- highestPersistedSeqNo() {
4230
- return 0;
4428
+ recoverIndex(indexName) {
4429
+ return recoverIndex(indexName);
4430
+ },
4431
+ highestPersistedSeqNo(indexName, partitionId) {
4432
+ return indexes.get(indexName)?.appliedSeqNoByPartition.get(partitionId) ?? 0;
4231
4433
  },
4232
4434
  async recordMutation(record) {
4233
4435
  const indexState = getOrCreateIndexState(record.indexName);
4234
- const buffered = indexState.applyChain.then(() => record.apply());
4235
- indexState.applyChain = buffered.catch(() => void 0);
4236
- await buffered;
4436
+ const buffered = indexState.applyChain.then(async () => {
4437
+ await record.apply();
4438
+ const appliedSeqNo2 = (indexState.appliedSeqNoByPartition.get(record.partitionId) ?? 0) + 1;
4439
+ indexState.appliedSeqNoByPartition.set(record.partitionId, appliedSeqNo2);
4440
+ return appliedSeqNo2;
4441
+ });
4442
+ indexState.applyChain = buffered.then(
4443
+ () => void 0,
4444
+ () => void 0
4445
+ );
4446
+ const appliedSeqNo = await buffered;
4237
4447
  indexState.mutationsSinceCheckpoint += 1;
4238
4448
  startCheckpointTimer();
4239
4449
  if (indexState.mutationsSinceCheckpoint >= checkpointMutationThreshold) {
@@ -4241,7 +4451,7 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
4241
4451
  hooks.onFatalError(err instanceof Error ? err : new Error(String(err)));
4242
4452
  });
4243
4453
  }
4244
- return 0;
4454
+ return appliedSeqNo;
4245
4455
  },
4246
4456
  persistMetadata(indexName) {
4247
4457
  return queueMetadataWrite(indexName, async () => {
@@ -4257,17 +4467,49 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
4257
4467
  checkpoint(indexName) {
4258
4468
  return checkpointIndex(indexName);
4259
4469
  },
4470
+ checkpointFromMemory(indexName) {
4471
+ return checkpointIndex(indexName, true);
4472
+ },
4260
4473
  async checkpointAll() {
4261
4474
  for (const indexName of [...indexes.keys()]) {
4262
4475
  await checkpointIndex(indexName);
4263
4476
  }
4264
4477
  },
4265
4478
  async removeIndex(indexName) {
4479
+ const indexState = indexes.get(indexName);
4480
+ if (indexState !== void 0) {
4481
+ indexState.unloading = true;
4482
+ try {
4483
+ await indexState.applyChain;
4484
+ if (indexState.checkpointInFlight !== null) await indexState.checkpointInFlight;
4485
+ await metadataWrites.get(indexName);
4486
+ } catch (error) {
4487
+ indexState.unloading = false;
4488
+ throw error;
4489
+ }
4490
+ }
4266
4491
  indexes.delete(indexName);
4492
+ metadataWrites.delete(indexName);
4267
4493
  for (const key of await adapter.list(`${indexName}/`)) {
4268
4494
  await adapter.delete(key);
4269
4495
  }
4270
4496
  },
4497
+ async unloadIndex(indexName) {
4498
+ const indexState = indexes.get(indexName);
4499
+ if (indexState !== void 0) {
4500
+ indexState.unloading = true;
4501
+ try {
4502
+ await indexState.applyChain;
4503
+ if (indexState.checkpointInFlight !== null) await indexState.checkpointInFlight;
4504
+ await metadataWrites.get(indexName);
4505
+ } catch (error) {
4506
+ indexState.unloading = false;
4507
+ throw error;
4508
+ }
4509
+ }
4510
+ indexes.delete(indexName);
4511
+ metadataWrites.delete(indexName);
4512
+ },
4271
4513
  async reloadIndex(indexName) {
4272
4514
  const indexState = getOrCreateIndexState(indexName);
4273
4515
  const reload = indexState.applyChain.then(() => recoverIndex(indexName));
@@ -4291,21 +4533,22 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
4291
4533
  }
4292
4534
 
4293
4535
  // src/engine/durability-integration.ts
4294
- function buildMetadata(indexName, hooks) {
4536
+ function buildMetadata(indexName, hooks, documentCount) {
4295
4537
  const config = hooks.getIndexConfig(indexName);
4296
4538
  const manager = hooks.getManager(indexName);
4297
- if (config === void 0 || manager === void 0) {
4539
+ if (config === void 0) {
4298
4540
  return void 0;
4299
4541
  }
4300
4542
  const metadata = {
4301
4543
  indexName,
4302
4544
  schema: config.schema,
4303
4545
  language: config.language,
4304
- partitionCount: manager.partitionCount,
4546
+ partitionCount: manager?.partitionCount ?? config.partitionCount,
4305
4547
  bm25Params: { k1: config.k1, b: config.b },
4306
4548
  createdAt: Date.now(),
4307
4549
  engineVersion: VERSION
4308
4550
  };
4551
+ metadata.documentCount = documentCount ?? config.documentCount;
4309
4552
  if (config.embedding) {
4310
4553
  metadata.embedding = config.embedding;
4311
4554
  }
@@ -4355,8 +4598,9 @@ function createDurabilityIntegration(tier, hooks) {
4355
4598
  getManager: hooks.getManager,
4356
4599
  getVectorFieldPaths: hooks.getVectorFieldPaths,
4357
4600
  getVectorIndexes: hooks.getVectorIndexes,
4358
- buildMetadata: (indexName) => buildMetadata(indexName, hooks),
4601
+ buildMetadata: (indexName, documentCount) => buildMetadata(indexName, hooks, documentCount),
4359
4602
  createIndexFromMetadata: hooks.createIndexFromMetadata,
4603
+ recordCheckpoint: hooks.recordCheckpoint,
4360
4604
  onFatalError: hooks.onFatalError
4361
4605
  };
4362
4606
  const manager = tier.kind === "wal" ? createDurabilityManager(tier.config, managerHooks) : createSnapshotOnlyManager(tier.adapter, tier.config, managerHooks, hooks.checkpointPublisher);
@@ -4395,9 +4639,9 @@ function createDurabilityFromTier(tier, wiring) {
4395
4639
  }
4396
4640
  return createDurabilityIntegration(tier, {
4397
4641
  checkpointPublisher: { publishPartitions: wiring.publishCheckpointedPartitions },
4398
- getManager: (indexName) => wiring.indexRegistry.has(indexName) ? wiring.requireManager(indexName) : void 0,
4642
+ getManager: (indexName) => wiring.indexRegistry.has(indexName) ? wiring.getManager(indexName) : void 0,
4399
4643
  getVectorFieldPaths: (indexName) => wiring.indexRegistry.get(indexName)?.vectorFieldPaths ?? /* @__PURE__ */ new Set(),
4400
- getVectorIndexes: (indexName) => wiring.indexRegistry.has(indexName) ? wiring.requireManager(indexName).getVectorIndexes() : /* @__PURE__ */ new Map(),
4644
+ getVectorIndexes: (indexName) => (wiring.indexRegistry.has(indexName) ? wiring.getManager(indexName)?.getVectorIndexes() : void 0) ?? /* @__PURE__ */ new Map(),
4401
4645
  getIndexConfig: (indexName) => {
4402
4646
  const entry = wiring.indexRegistry.get(indexName);
4403
4647
  if (entry === void 0) {
@@ -4410,6 +4654,8 @@ function createDurabilityFromTier(tier, wiring) {
4410
4654
  return {
4411
4655
  ...entry.indexUuid !== null ? { indexUuid: entry.indexUuid } : {},
4412
4656
  ...entry.heldPartitions !== null ? { heldPartitions: entry.heldPartitions } : {},
4657
+ documentCount: entry.documentCount,
4658
+ partitionCount: entry.partitionCount,
4413
4659
  schema: flattenSchema(entry.config.schema),
4414
4660
  language: entry.language.name,
4415
4661
  k1: entry.config.bm25?.k1 ?? 1.2,
@@ -4429,10 +4675,401 @@ function createDurabilityFromTier(tier, wiring) {
4429
4675
  };
4430
4676
  },
4431
4677
  createIndexFromMetadata: wiring.createIndexFromMetadata,
4678
+ recordCheckpoint: wiring.recordCheckpoint,
4432
4679
  onFatalError: wiring.emitFatalError
4433
4680
  });
4434
4681
  }
4435
4682
 
4683
+ // src/engine/index-state/entry.ts
4684
+ var DEFAULT_MAX_REOPEN_WAITERS = 64;
4685
+ var MAX_REOPEN_FAILURES = 5;
4686
+ var MIN_SWEEP_INTERVAL_MS = 100;
4687
+ var MAX_SWEEP_INTERVAL_MS = 6e4;
4688
+ function createLifecycleEntry(state) {
4689
+ return {
4690
+ state,
4691
+ lastAccessAt: Date.now(),
4692
+ activeOperations: 0,
4693
+ reopenCount: 0,
4694
+ reopenFailures: 0,
4695
+ nextReopenAt: 0,
4696
+ cachedError: null,
4697
+ reopenPromise: null,
4698
+ waiterCount: 0,
4699
+ closePromise: null,
4700
+ dropPromise: null,
4701
+ drainResolvers: /* @__PURE__ */ new Set()
4702
+ };
4703
+ }
4704
+ function validateLimit(name, value, allowZero = false) {
4705
+ if (value === void 0) return;
4706
+ const minimum = allowZero ? 0 : 1;
4707
+ if (!Number.isSafeInteger(value) || value < minimum) {
4708
+ const requirement = allowZero ? "a non-negative safe integer" : "a positive safe integer";
4709
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, `${name} must be ${requirement}`, {
4710
+ field: name,
4711
+ value
4712
+ });
4713
+ }
4714
+ }
4715
+ function cachedRecoveryError(error, indexName) {
4716
+ if (error instanceof NarsilError) return error;
4717
+ return new NarsilError(ErrorCodes.PERSISTENCE_LOAD_FAILED, `Index "${indexName}" could not be reopened`, {
4718
+ indexName,
4719
+ cause: error instanceof Error ? error.message : String(error)
4720
+ });
4721
+ }
4722
+
4723
+ // src/engine/index-state/index.ts
4724
+ function createIndexStateCoordinator(config, callbacks) {
4725
+ validateLimit("lifecycle.idleTimeoutMs", config?.idleTimeoutMs);
4726
+ validateLimit("lifecycle.maxOpenIndexes", config?.maxOpenIndexes);
4727
+ validateLimit("lifecycle.maxOpenBytes", config?.maxOpenBytes);
4728
+ validateLimit("lifecycle.maxReopenWaiters", config?.maxReopenWaiters, true);
4729
+ const entries = /* @__PURE__ */ new Map();
4730
+ const waiterLimit = config?.maxReopenWaiters ?? DEFAULT_MAX_REOPEN_WAITERS;
4731
+ let sweepTimer = null;
4732
+ function entryOf(indexName) {
4733
+ const entry = entries.get(indexName);
4734
+ if (entry === void 0) {
4735
+ throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, `Index "${indexName}" does not exist`, { indexName });
4736
+ }
4737
+ return entry;
4738
+ }
4739
+ function publicState(entry) {
4740
+ return entry.state === "closing" || entry.state === "dropping" ? "open" : entry.state;
4741
+ }
4742
+ async function waitForDrain(entry) {
4743
+ if (entry.activeOperations === 0) return;
4744
+ await new Promise((resolve) => entry.drainResolvers.add(resolve));
4745
+ }
4746
+ function markClosed(entry) {
4747
+ entry.state = "closed";
4748
+ entry.cachedError = null;
4749
+ entry.reopenFailures = 0;
4750
+ }
4751
+ function releaseEntry(entry) {
4752
+ entry.activeOperations -= 1;
4753
+ if (entry.activeOperations !== 0) return;
4754
+ for (const resolve of entry.drainResolvers) resolve();
4755
+ entry.drainResolvers.clear();
4756
+ }
4757
+ async function waitForTransition(indexName, entry, transition) {
4758
+ if (entry.waiterCount >= waiterLimit) {
4759
+ throw new NarsilError(
4760
+ ErrorCodes.INDEX_REOPEN_CAPACITY_EXHAUSTED,
4761
+ `Index "${indexName}" has reached its reopen waiter limit`,
4762
+ { indexName, maximum: waiterLimit }
4763
+ );
4764
+ }
4765
+ entry.waiterCount += 1;
4766
+ try {
4767
+ await transition;
4768
+ } finally {
4769
+ entry.waiterCount -= 1;
4770
+ }
4771
+ }
4772
+ async function awaitPendingTransitions(indexName) {
4773
+ let entry = entryOf(indexName);
4774
+ while (true) {
4775
+ if (entry.state === "dropping" && entry.dropPromise !== null) {
4776
+ await entry.dropPromise;
4777
+ entry = entryOf(indexName);
4778
+ continue;
4779
+ }
4780
+ if (entry.state === "closing" && entry.closePromise !== null) {
4781
+ await waitForTransition(
4782
+ indexName,
4783
+ entry,
4784
+ entry.closePromise.catch(() => void 0)
4785
+ );
4786
+ entry = entryOf(indexName);
4787
+ continue;
4788
+ }
4789
+ return entry;
4790
+ }
4791
+ }
4792
+ async function reopen(indexName, explicit) {
4793
+ const entry = await awaitPendingTransitions(indexName);
4794
+ if (entry.state === "open") return;
4795
+ if (explicit) {
4796
+ entry.reopenFailures = 0;
4797
+ entry.nextReopenAt = 0;
4798
+ entry.cachedError = null;
4799
+ if (entry.state === "reopen-failed") entry.state = "closed";
4800
+ }
4801
+ if (entry.state === "reopen-failed") throw entry.cachedError;
4802
+ if (entry.reopenPromise !== null) {
4803
+ await waitForTransition(indexName, entry, entry.reopenPromise);
4804
+ return;
4805
+ }
4806
+ if (!explicit && entry.cachedError !== null && Date.now() < entry.nextReopenAt) {
4807
+ throw entry.cachedError;
4808
+ }
4809
+ const run = callbacks.reopen(indexName).then(() => {
4810
+ entry.state = "open";
4811
+ entry.reopenCount += 1;
4812
+ entry.reopenFailures = 0;
4813
+ entry.nextReopenAt = 0;
4814
+ entry.cachedError = null;
4815
+ }).catch((error) => {
4816
+ entry.reopenFailures += 1;
4817
+ entry.cachedError = cachedRecoveryError(error, indexName);
4818
+ entry.nextReopenAt = Date.now() + 100 * 2 ** (entry.reopenFailures - 1);
4819
+ entry.state = entry.reopenFailures >= MAX_REOPEN_FAILURES ? "reopen-failed" : "closed";
4820
+ throw entry.cachedError;
4821
+ }).finally(() => {
4822
+ entry.reopenPromise = null;
4823
+ });
4824
+ entry.reopenPromise = run;
4825
+ await run;
4826
+ }
4827
+ async function closeEntry(indexName, automatic) {
4828
+ let entry = entryOf(indexName);
4829
+ while (true) {
4830
+ if (entry.state === "dropping" && entry.dropPromise !== null) {
4831
+ if (automatic) return false;
4832
+ await entry.dropPromise;
4833
+ entry = entryOf(indexName);
4834
+ continue;
4835
+ }
4836
+ if (entry.reopenPromise !== null) {
4837
+ if (automatic) return false;
4838
+ await entry.reopenPromise.catch(() => void 0);
4839
+ entry = entryOf(indexName);
4840
+ continue;
4841
+ }
4842
+ break;
4843
+ }
4844
+ if (entry.state === "closed" || entry.state === "reopen-failed") return true;
4845
+ if (entry.state === "closing" && entry.closePromise !== null) {
4846
+ if (!automatic) await entry.closePromise;
4847
+ return !automatic;
4848
+ }
4849
+ if (automatic && (entry.activeOperations > 0 || !callbacks.canCloseAutomatically(indexName))) return false;
4850
+ entry.state = "closing";
4851
+ let irreversible = false;
4852
+ const run = (async () => {
4853
+ await waitForDrain(entry);
4854
+ await callbacks.close(indexName, () => {
4855
+ irreversible = true;
4856
+ });
4857
+ markClosed(entry);
4858
+ })().catch((error) => {
4859
+ if (irreversible) markClosed(entry);
4860
+ else entry.state = "open";
4861
+ throw error;
4862
+ }).finally(() => {
4863
+ entry.closePromise = null;
4864
+ });
4865
+ entry.closePromise = run;
4866
+ if (automatic) {
4867
+ await run.catch(() => void 0);
4868
+ return publicState(entry) === "closed";
4869
+ }
4870
+ await run;
4871
+ return true;
4872
+ }
4873
+ async function dropEntry(indexName, action) {
4874
+ let entry = entryOf(indexName);
4875
+ while (true) {
4876
+ if (entry.dropPromise !== null) {
4877
+ await entry.dropPromise;
4878
+ return;
4879
+ }
4880
+ if (entry.reopenPromise !== null) {
4881
+ await entry.reopenPromise.catch(() => void 0);
4882
+ entry = entryOf(indexName);
4883
+ continue;
4884
+ }
4885
+ if (entry.closePromise !== null) {
4886
+ await entry.closePromise.catch(() => void 0);
4887
+ entry = entryOf(indexName);
4888
+ continue;
4889
+ }
4890
+ break;
4891
+ }
4892
+ const previousState = entry.state;
4893
+ entry.state = "dropping";
4894
+ const run = (async () => {
4895
+ await waitForDrain(entry);
4896
+ await action();
4897
+ entries.delete(indexName);
4898
+ })().catch((error) => {
4899
+ entry.state = previousState;
4900
+ throw error;
4901
+ }).finally(() => {
4902
+ entry.dropPromise = null;
4903
+ });
4904
+ entry.dropPromise = run;
4905
+ await run;
4906
+ }
4907
+ function exceedsLimits() {
4908
+ let open = 0;
4909
+ let bytes = 0;
4910
+ for (const [name, entry] of entries) {
4911
+ if (entry.state !== "open") continue;
4912
+ open += 1;
4913
+ bytes += callbacks.estimateBytes(name);
4914
+ }
4915
+ return config?.maxOpenIndexes !== void 0 && open > config.maxOpenIndexes || config?.maxOpenBytes !== void 0 && bytes > config.maxOpenBytes;
4916
+ }
4917
+ async function enforceLimits(protectedName) {
4918
+ if (config?.maxOpenIndexes === void 0 && config?.maxOpenBytes === void 0) return;
4919
+ while (exceedsLimits()) {
4920
+ const candidates = [...entries].filter(([name, entry]) => name !== protectedName && entry.state === "open" && entry.activeOperations === 0).sort((left, right) => left[1].lastAccessAt - right[1].lastAccessAt);
4921
+ const candidate = candidates[0];
4922
+ if (candidate === void 0 || !await closeEntry(candidate[0], true)) return;
4923
+ }
4924
+ }
4925
+ async function sweep() {
4926
+ const timeout = config?.idleTimeoutMs;
4927
+ if (timeout !== void 0 && timeout > 0) {
4928
+ const cutoff = Date.now() - timeout;
4929
+ for (const [name, entry] of entries) {
4930
+ if (entry.state === "open" && entry.lastAccessAt <= cutoff) await closeEntry(name, true);
4931
+ }
4932
+ }
4933
+ await enforceLimits();
4934
+ }
4935
+ if (config !== void 0) {
4936
+ const requested = config.idleTimeoutMs === void 0 ? MAX_SWEEP_INTERVAL_MS : config.idleTimeoutMs / 2;
4937
+ const interval = Math.min(MAX_SWEEP_INTERVAL_MS, Math.max(MIN_SWEEP_INTERVAL_MS, requested));
4938
+ sweepTimer = setInterval(() => void sweep().catch(() => void 0), interval);
4939
+ if (typeof sweepTimer.unref === "function") sweepTimer.unref();
4940
+ }
4941
+ return {
4942
+ async registerOpen(indexName) {
4943
+ entries.set(indexName, createLifecycleEntry("open"));
4944
+ await enforceLimits(indexName);
4945
+ },
4946
+ registerClosed(indexName) {
4947
+ entries.set(indexName, createLifecycleEntry("closed"));
4948
+ },
4949
+ forget(indexName) {
4950
+ entries.delete(indexName);
4951
+ },
4952
+ async acquire(indexName, markActive = true) {
4953
+ let entry = entryOf(indexName);
4954
+ const opened = entry.state !== "open";
4955
+ await reopen(indexName, false);
4956
+ entry = entryOf(indexName);
4957
+ while (entry.state !== "open") {
4958
+ await reopen(indexName, false);
4959
+ entry = entryOf(indexName);
4960
+ }
4961
+ if (markActive) entry.lastAccessAt = Date.now();
4962
+ entry.activeOperations += 1;
4963
+ try {
4964
+ if (opened) await enforceLimits(indexName);
4965
+ } catch (error) {
4966
+ releaseEntry(entry);
4967
+ throw error;
4968
+ }
4969
+ let released = false;
4970
+ return () => {
4971
+ if (released) return;
4972
+ released = true;
4973
+ releaseEntry(entry);
4974
+ };
4975
+ },
4976
+ async open(indexName) {
4977
+ await reopen(indexName, true);
4978
+ let entry = entryOf(indexName);
4979
+ while (entry.state !== "open") {
4980
+ await reopen(indexName, true);
4981
+ entry = entryOf(indexName);
4982
+ }
4983
+ entry.lastAccessAt = Date.now();
4984
+ entry.activeOperations += 1;
4985
+ try {
4986
+ await enforceLimits(indexName);
4987
+ } finally {
4988
+ releaseEntry(entry);
4989
+ }
4990
+ },
4991
+ async close(indexName) {
4992
+ await closeEntry(indexName, false);
4993
+ },
4994
+ drop(indexName, action) {
4995
+ return dropEntry(indexName, action);
4996
+ },
4997
+ stateOf(indexName) {
4998
+ return publicState(entryOf(indexName));
4999
+ },
5000
+ reopenCount(indexName) {
5001
+ return entryOf(indexName).reopenCount;
5002
+ },
5003
+ counts() {
5004
+ let open = 0;
5005
+ let closed = 0;
5006
+ let reopens = 0;
5007
+ for (const entry of entries.values()) {
5008
+ if (entry.state === "open" || entry.state === "closing") open += 1;
5009
+ else closed += 1;
5010
+ reopens += entry.reopenCount;
5011
+ }
5012
+ return { open, closed, reopens };
5013
+ },
5014
+ dispose() {
5015
+ if (sweepTimer !== null) clearInterval(sweepTimer);
5016
+ sweepTimer = null;
5017
+ entries.clear();
5018
+ }
5019
+ };
5020
+ }
5021
+
5022
+ // src/engine/index-state-wiring.ts
5023
+ function wireIndexState(wiring) {
5024
+ const { durability, executor, orchestrator, analysisRebuild, indexRegistry, rebalancingIndexes } = wiring;
5025
+ return createIndexStateCoordinator(wiring.config, {
5026
+ async reopen(indexName) {
5027
+ if (durability === null) {
5028
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, "An index needs durability before it can reopen");
5029
+ }
5030
+ try {
5031
+ await durability.manager.recoverIndex(indexName);
5032
+ await analysisRebuild.review(indexName);
5033
+ await orchestrator.openIndex(indexName);
5034
+ await wiring.onOpen?.(indexName);
5035
+ } catch (error) {
5036
+ await orchestrator.closeIndex(indexName).catch(() => void 0);
5037
+ await durability.manager.unloadIndex(indexName).catch(() => void 0);
5038
+ if (executor.getManager(indexName) !== void 0) executor.dropIndex(indexName);
5039
+ throw error;
5040
+ }
5041
+ },
5042
+ async close(indexName, markIrreversible) {
5043
+ if (durability === null) {
5044
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, "An index needs durability before it can close");
5045
+ }
5046
+ const manager = wiring.requireManager(indexName);
5047
+ const documentCount = manager.countDocuments();
5048
+ const partitionCount = manager.partitionCount;
5049
+ await (durability.manager.checkpointFromMemory?.(indexName) ?? durability.manager.checkpoint(indexName));
5050
+ await orchestrator.closeIndex(indexName);
5051
+ await durability.manager.unloadIndex(indexName);
5052
+ markIrreversible();
5053
+ try {
5054
+ await wiring.onClose?.(indexName);
5055
+ } finally {
5056
+ if (executor.getManager(indexName) !== void 0) executor.dropIndex(indexName);
5057
+ const entry = indexRegistry.get(indexName);
5058
+ if (entry !== void 0) {
5059
+ entry.documentCount = documentCount;
5060
+ entry.partitionCount = partitionCount;
5061
+ }
5062
+ }
5063
+ },
5064
+ canCloseAutomatically(indexName) {
5065
+ return !rebalancingIndexes.has(indexName) && !analysisRebuild.isRunning(indexName) && !orchestrator.isIndexBusy(indexName);
5066
+ },
5067
+ estimateBytes(indexName) {
5068
+ return executor.getManager(indexName)?.estimateMemoryBytes() ?? 0;
5069
+ }
5070
+ });
5071
+ }
5072
+
4436
5073
  // src/engine/invalidation.ts
4437
5074
  var STATISTICS_BROADCAST_INTERVAL_MS = 5e3;
4438
5075
  var FOREIGN_STATS_TTL_MS = 6e4;
@@ -4458,7 +5095,7 @@ function createInvalidationIntegration(deps) {
4458
5095
  const reloadChains = /* @__PURE__ */ new Map();
4459
5096
  let broadcastTimer = null;
4460
5097
  let stopped = false;
4461
- function toError3(err) {
5098
+ function toError4(err) {
4462
5099
  return err instanceof Error ? err : new Error(String(err));
4463
5100
  }
4464
5101
  function pruneStaleForeignStats(entries) {
@@ -4471,7 +5108,7 @@ function createInvalidationIntegration(deps) {
4471
5108
  }
4472
5109
  function scheduleReload(indexName) {
4473
5110
  const chain = reloadChains.get(indexName) ?? Promise.resolve();
4474
- const next = chain.then(() => reloadIndex(indexName)).catch((err) => onError(toError3(err)));
5111
+ const next = chain.then(() => reloadIndex(indexName)).catch((err) => onError(toError4(err)));
4475
5112
  reloadChains.set(indexName, next);
4476
5113
  }
4477
5114
  function storeForeignStats(indexName, sourceInstanceId, stats) {
@@ -4497,7 +5134,7 @@ function createInvalidationIntegration(deps) {
4497
5134
  }
4498
5135
  storeForeignStats(event.indexName, event.instanceId, event.stats);
4499
5136
  } catch (err) {
4500
- onError(toError3(err));
5137
+ onError(toError4(err));
4501
5138
  }
4502
5139
  }
4503
5140
  async function publishOwnStatistics() {
@@ -4519,7 +5156,7 @@ function createInvalidationIntegration(deps) {
4519
5156
  }
4520
5157
  });
4521
5158
  } catch (err) {
4522
- onError(toError3(err));
5159
+ onError(toError4(err));
4523
5160
  }
4524
5161
  }
4525
5162
  }
@@ -4538,7 +5175,7 @@ function createInvalidationIntegration(deps) {
4538
5175
  sourceInstanceId: instanceId
4539
5176
  });
4540
5177
  } catch (err) {
4541
- onError(toError3(err));
5178
+ onError(toError4(err));
4542
5179
  }
4543
5180
  },
4544
5181
  broadcastStats(indexName) {
@@ -4583,7 +5220,7 @@ function createInvalidationIntegration(deps) {
4583
5220
  try {
4584
5221
  await adapter.shutdown();
4585
5222
  } catch (err) {
4586
- onError(toError3(err));
5223
+ onError(toError4(err));
4587
5224
  }
4588
5225
  }
4589
5226
  };
@@ -4625,7 +5262,7 @@ async function transferIndexToPool(indexName, pool, config, manager) {
4625
5262
  }
4626
5263
 
4627
5264
  // src/engine/orchestration/eligibility.ts
4628
- function toError2(value) {
5265
+ function toError3(value) {
4629
5266
  return value instanceof Error ? value : new Error(String(value));
4630
5267
  }
4631
5268
  function isDeterministicFailure(error) {
@@ -5211,7 +5848,7 @@ async function promoteBeforeBatch(state, indexName, incomingCount) {
5211
5848
  if (!result.shouldPromote) return;
5212
5849
  state.promotionInProgress = true;
5213
5850
  const run = runPromotion(state, result.reason).catch((err) => {
5214
- const error = toError2(err);
5851
+ const error = toError3(err);
5215
5852
  state.promotionBlocked = isDeterministicFailure(error);
5216
5853
  state.promotionInProgress = false;
5217
5854
  state.callbacks?.onPromotionFailure?.(result.reason, error, !state.promotionBlocked);
@@ -5232,7 +5869,7 @@ async function checkPromotion(state) {
5232
5869
  state.promotionInProgress = true;
5233
5870
  setTimeout(() => {
5234
5871
  const run = runPromotion(state, result.reason).catch((err) => {
5235
- const error = toError2(err);
5872
+ const error = toError3(err);
5236
5873
  state.promotionBlocked = isDeterministicFailure(error);
5237
5874
  state.promotionInProgress = false;
5238
5875
  state.callbacks?.onPromotionFailure?.(result.reason, error, !state.promotionBlocked);
@@ -5402,6 +6039,56 @@ function createWorkerOrchestrator(config, executor, promoter, indexRegistry, cal
5402
6039
  state.segmentLedger.clear();
5403
6040
  }
5404
6041
  }
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));
6049
+ }
6050
+ }
6051
+ }
6052
+ async function closeIndex(indexName) {
6053
+ await awaitPromotionIdle();
6054
+ await awaitReplicationIdle(state, indexName);
6055
+ await awaitCompactions(state);
6056
+ const pool = state.workerPool;
6057
+ if (pool !== null && state.promotedIndexes.has(indexName)) {
6058
+ const outcomes = await Promise.allSettled(
6059
+ pool.getAllExecutors().map((worker) => worker.execute({ type: "dropIndex", indexName, requestId: `close-${indexName}` }))
6060
+ );
6061
+ 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
+ }
6086
+ pool.removeIndex(indexName);
6087
+ }
6088
+ state.promotedIndexes.delete(indexName);
6089
+ state.replicationQueues.delete(indexName);
6090
+ state.segmentLedger.delete(indexName);
6091
+ }
5405
6092
  async function replicate(action) {
5406
6093
  await replicateToWorkers(state, action);
5407
6094
  if (action.type === "attachSegments") {
@@ -5414,6 +6101,9 @@ function createWorkerOrchestrator(config, executor, promoter, indexRegistry, cal
5414
6101
  replicateToWorkers: replicate,
5415
6102
  awaitReplication: (indexName) => awaitReplicationIdle(state, indexName),
5416
6103
  awaitCompactions: () => awaitCompactions(state),
6104
+ openIndex: (indexName) => resyncIndex(indexName, true),
6105
+ closeIndex,
6106
+ isIndexBusy: (indexName) => state.promotionInProgress || state.replicationQueues.has(indexName) || state.compactionsInFlight.has(indexName),
5417
6107
  buildSegments: (requests) => buildSegments(state, requests),
5418
6108
  segmentBuildConcurrency: (indexName) => segmentBuildConcurrency(state, indexName),
5419
6109
  searchViaWorker: (indexName, params, globalStats, partitionIds) => searchViaWorker(state, indexName, params, globalStats, partitionIds),
@@ -5425,6 +6115,11 @@ function createWorkerOrchestrator(config, executor, promoter, indexRegistry, cal
5425
6115
  };
5426
6116
  }
5427
6117
 
6118
+ // src/engine/vector-fields.ts
6119
+ function getVectorFieldPaths(schema) {
6120
+ return new Set(extractVectorFieldsFromSchema(schema).keys());
6121
+ }
6122
+
5428
6123
  // src/engine/watermark.ts
5429
6124
  function createWatermarkNotifier(deps) {
5430
6125
  const latchedCapacity = /* @__PURE__ */ new Map();
@@ -5460,10 +6155,7 @@ function createWatermarkNotifier(deps) {
5460
6155
  }
5461
6156
 
5462
6157
  // src/engine/core.ts
5463
- function getVectorFieldPaths(schema) {
5464
- return new Set(extractVectorFieldsFromSchema(schema).keys());
5465
- }
5466
- function createEngineCore(config) {
6158
+ function createEngineCore(config, hooks) {
5467
6159
  const executor = createDirectExecutor();
5468
6160
  const promoter = createExecutionPromoter({
5469
6161
  perIndexThreshold: config?.workers?.promotionThreshold,
@@ -5547,8 +6239,12 @@ function createEngineCore(config) {
5547
6239
  }
5548
6240
  return manager;
5549
6241
  }
5550
- async function createIndexFromMetadata(metadata) {
5551
- if (indexRegistry.has(metadata.indexName)) {
6242
+ async function createIndexFromMetadata(metadata, loadData) {
6243
+ const existing = indexRegistry.get(metadata.indexName);
6244
+ if (existing !== void 0) {
6245
+ if (loadData && executor.getManager(metadata.indexName) === void 0) {
6246
+ executor.createIndex(metadata.indexName, existing.config, existing.language);
6247
+ }
5552
6248
  return;
5553
6249
  }
5554
6250
  const indexConfig = reconstructSchemaFromMetadata(metadata);
@@ -5577,7 +6273,7 @@ function createEngineCore(config) {
5577
6273
  }
5578
6274
  }
5579
6275
  try {
5580
- executor.createIndex(metadata.indexName, indexConfig, language);
6276
+ if (loadData) executor.createIndex(metadata.indexName, indexConfig, language);
5581
6277
  } catch (err) {
5582
6278
  if (err instanceof NarsilError && err.code === ErrorCodes.CONFIG_INVALID) {
5583
6279
  throw new NarsilError(err.code, `Recovery of index "${metadata.indexName}" failed: ${err.message}`, {
@@ -5595,8 +6291,12 @@ function createEngineCore(config) {
5595
6291
  embeddingAdapterName: adapterName,
5596
6292
  vectorFieldPaths: getVectorFieldPaths(indexConfig.schema),
5597
6293
  indexUuid: metadata.indexUuid ?? null,
5598
- heldPartitions: metadata.heldPartitions ?? null
6294
+ heldPartitions: metadata.heldPartitions ?? null,
6295
+ documentCount: metadata.documentCount ?? 0,
6296
+ partitionCount: metadata.partitionCount
5599
6297
  });
6298
+ if (loadData) await indexState.registerOpen(metadata.indexName);
6299
+ else indexState.registerClosed(metadata.indexName);
5600
6300
  if (metadata.analysisRevision !== language.revision) {
5601
6301
  analysisRebuild.markStale({
5602
6302
  indexName: metadata.indexName,
@@ -5607,9 +6307,12 @@ function createEngineCore(config) {
5607
6307
  }
5608
6308
  }
5609
6309
  const durabilityTier = config !== void 0 ? resolveDurabilityTier(config) : null;
6310
+ if (config?.lifecycle !== void 0 && durabilityTier === null) {
6311
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, "Index lifecycle settings require durability");
6312
+ }
5610
6313
  let invalidation = null;
5611
6314
  const durability = createDurabilityFromTier(durabilityTier, {
5612
- requireManager,
6315
+ getManager: (indexName) => executor.getManager(indexName),
5613
6316
  indexRegistry,
5614
6317
  createIndexFromMetadata,
5615
6318
  emitFatalError(error) {
@@ -5618,7 +6321,13 @@ function createEngineCore(config) {
5618
6321
  for (const handler of handlers) handler({ error });
5619
6322
  }
5620
6323
  },
5621
- publishCheckpointedPartitions: (indexName, partitions) => invalidation?.publishPartitions(indexName, partitions) ?? Promise.resolve()
6324
+ publishCheckpointedPartitions: (indexName, partitions) => invalidation?.publishPartitions(indexName, partitions) ?? Promise.resolve(),
6325
+ recordCheckpoint(indexName, documentCount, partitionCount) {
6326
+ const entry = indexRegistry.get(indexName);
6327
+ if (entry === void 0) return;
6328
+ entry.documentCount = documentCount;
6329
+ entry.partitionCount = partitionCount;
6330
+ }
5622
6331
  });
5623
6332
  invalidation = createInvalidationFromConfig(config, durabilityTier?.kind ?? null, {
5624
6333
  getManager: (indexName) => executor.getManager(indexName) ?? void 0,
@@ -5664,6 +6373,18 @@ function createEngineCore(config) {
5664
6373
  resyncIndex: (indexName, wasPromoted) => orchestrator.resyncIndex(indexName, wasPromoted),
5665
6374
  durabilityManager: durability?.manager ?? null
5666
6375
  });
6376
+ const indexState = wireIndexState({
6377
+ config: config?.lifecycle,
6378
+ durability,
6379
+ executor,
6380
+ orchestrator,
6381
+ analysisRebuild,
6382
+ indexRegistry,
6383
+ rebalancingIndexes,
6384
+ requireManager,
6385
+ onClose: hooks?.onIndexClose,
6386
+ onOpen: hooks?.onIndexOpen
6387
+ });
5667
6388
  const mutationCtx = {
5668
6389
  executor,
5669
6390
  pluginRegistry,
@@ -5717,6 +6438,7 @@ function createEngineCore(config) {
5717
6438
  bufferIfRebalancing,
5718
6439
  watermarkNotifier,
5719
6440
  analysisRebuild,
6441
+ indexState,
5720
6442
  mutationCtx,
5721
6443
  rebalanceCtx
5722
6444
  };
@@ -5875,8 +6597,11 @@ async function createEngineIndex(core, config, name, indexConfig, indexUuid) {
5875
6597
  embeddingAdapterName,
5876
6598
  vectorFieldPaths: getVectorFieldPaths(indexConfig.schema),
5877
6599
  indexUuid: indexUuid ?? null,
5878
- heldPartitions: null
6600
+ heldPartitions: null,
6601
+ documentCount: 0,
6602
+ partitionCount: indexConfig.partitions?.maxPartitions ?? 1
5879
6603
  });
6604
+ await core.indexState.registerOpen(name);
5880
6605
  if (core.durability) {
5881
6606
  await core.durability.manager.persistMetadata(name);
5882
6607
  }
@@ -5885,13 +6610,21 @@ async function createEngineIndex(core, config, name, indexConfig, indexUuid) {
5885
6610
  async function dropEngineIndex(core, name) {
5886
6611
  core.guardShutdown();
5887
6612
  const entry = core.requireIndex(name);
5888
- core.executor.dropIndex(name);
5889
- core.indexRegistry.delete(name);
5890
- core.watermarkNotifier.forget(name);
5891
- core.analysisRebuild.clearStale(name);
5892
- if (core.durability) {
5893
- await core.durability.manager.removeIndex(name);
5894
- }
6613
+ await core.indexState.drop(name, async () => {
6614
+ await core.orchestrator.closeIndex(name);
6615
+ if (core.durability) {
6616
+ try {
6617
+ await core.durability.manager.removeIndex(name);
6618
+ } catch (error) {
6619
+ await core.orchestrator.openIndex(name).catch(() => void 0);
6620
+ throw error;
6621
+ }
6622
+ }
6623
+ if (core.executor.getManager(name) !== void 0) core.executor.dropIndex(name);
6624
+ core.indexRegistry.delete(name);
6625
+ core.watermarkNotifier.forget(name);
6626
+ core.analysisRebuild.clearStale(name);
6627
+ });
5895
6628
  await core.pluginRegistry.runHook("onIndexDrop", { indexName: name, config: entry.config });
5896
6629
  }
5897
6630
  function registerEngineEmbeddingAdapter(core, name, adapter) {
@@ -5926,6 +6659,7 @@ function registerEngineEmbeddingAdapter(core, name, adapter) {
5926
6659
  // src/engine/lifecycle.ts
5927
6660
  async function shutdownEngine(core) {
5928
6661
  const { executor, durability, indexRegistry, eventHandlers, orchestrator } = core;
6662
+ core.indexState.dispose();
5929
6663
  for (const [name] of indexRegistry) {
5930
6664
  const manager = executor.getManager(name);
5931
6665
  if (manager) {
@@ -7817,8 +8551,12 @@ async function restoreFromSnapshot(indexName, data, deps) {
7817
8551
  embeddingAdapterName: adapterName,
7818
8552
  vectorFieldPaths,
7819
8553
  indexUuid: null,
7820
- heldPartitions: null
8554
+ heldPartitions: null,
8555
+ documentCount: 0,
8556
+ partitionCount: envelope.partitions.length
7821
8557
  });
8558
+ await deps.indexState.registerOpen(indexName);
8559
+ const release = await deps.indexState.acquire(indexName, false);
7822
8560
  try {
7823
8561
  const manager = deps.requireManager(indexName);
7824
8562
  while (manager.partitionCount < envelope.partitions.length) {
@@ -7838,26 +8576,36 @@ async function restoreFromSnapshot(indexName, data, deps) {
7838
8576
  }
7839
8577
  }
7840
8578
  }
8579
+ const entry = deps.indexRegistry.get(indexName);
8580
+ if (entry !== void 0) {
8581
+ entry.documentCount = manager.countDocuments();
8582
+ entry.partitionCount = manager.partitionCount;
8583
+ }
8584
+ if (deps.durability) {
8585
+ for (let partitionId = 0; partitionId < manager.partitionCount; partitionId++) {
8586
+ for (const docId of manager.getPartition(partitionId).docIds()) {
8587
+ const document = manager.get(docId);
8588
+ if (document !== void 0) {
8589
+ await deps.durability.recordInsertOrUpdate(indexName, docId, document, noopApply2);
8590
+ }
8591
+ }
8592
+ }
8593
+ await deps.durability.manager.persistMetadata(indexName);
8594
+ await deps.durability.manager.checkpoint(indexName);
8595
+ }
7841
8596
  } catch (err) {
7842
8597
  try {
7843
8598
  deps.executor.dropIndex(indexName);
7844
- deps.indexRegistry.delete(indexName);
7845
8599
  } catch (_) {
7846
8600
  }
7847
- throw err;
7848
- }
7849
- if (deps.durability) {
7850
- const manager = deps.requireManager(indexName);
7851
- for (let partitionId = 0; partitionId < manager.partitionCount; partitionId++) {
7852
- for (const docId of manager.getPartition(partitionId).docIds()) {
7853
- const document = manager.get(docId);
7854
- if (document !== void 0) {
7855
- await deps.durability.recordInsertOrUpdate(indexName, docId, document, noopApply2);
7856
- }
7857
- }
8601
+ deps.indexRegistry.delete(indexName);
8602
+ deps.indexState.forget(indexName);
8603
+ if (deps.durability) {
8604
+ await deps.durability.manager.removeIndex(indexName).catch(() => void 0);
7858
8605
  }
7859
- await deps.durability.manager.persistMetadata(indexName);
7860
- await deps.durability.manager.checkpoint(indexName);
8606
+ throw err;
8607
+ } finally {
8608
+ release();
7861
8609
  }
7862
8610
  }
7863
8611
  function noopApply2() {
@@ -9153,107 +9901,132 @@ function broadcastStatsFor(core, options) {
9153
9901
  }
9154
9902
  async function runEngineQuery(core, indexName, params, options) {
9155
9903
  core.guardShutdown();
9156
- const entry = core.requireIndex(indexName);
9157
- const manager = core.requireManager(indexName);
9158
- const resolvedParams = await resolveVectorText(
9159
- scopedParams(params, options),
9160
- entry.embeddingAdapter,
9161
- core.abortController.signal,
9162
- entry.embeddingAdapterName
9163
- );
9164
- await core.pluginRegistry.runHook("beforeSearch", { indexName, params: resolvedParams });
9165
- const workerSearch = core.orchestrator.isPromoted() ? core.orchestrator.searchViaWorker.bind(core.orchestrator) : void 0;
9166
- const result = await executeQuery(resolvedParams, {
9167
- manager,
9168
- language: entry.language,
9169
- config: entry.config,
9170
- workerSearch,
9171
- indexName,
9172
- broadcastStats: broadcastStatsFor(core, options),
9173
- partitionIds: options?.partitionIds,
9174
- cursorBinding: queryBindingOf(params)
9175
- });
9904
+ const release = await core.indexState.acquire(indexName);
9176
9905
  try {
9177
- await core.pluginRegistry.runHook("afterSearch", {
9906
+ const entry = core.requireIndex(indexName);
9907
+ const manager = core.requireManager(indexName);
9908
+ const resolvedParams = await resolveVectorText(
9909
+ scopedParams(params, options),
9910
+ entry.embeddingAdapter,
9911
+ core.abortController.signal,
9912
+ entry.embeddingAdapterName
9913
+ );
9914
+ await core.pluginRegistry.runHook("beforeSearch", { indexName, params: resolvedParams });
9915
+ const workerSearch = core.orchestrator.isPromoted() ? core.orchestrator.searchViaWorker.bind(core.orchestrator) : void 0;
9916
+ const result = await executeQuery(resolvedParams, {
9917
+ manager,
9918
+ language: entry.language,
9919
+ config: entry.config,
9920
+ workerSearch,
9178
9921
  indexName,
9179
- params: resolvedParams,
9180
- results: result
9922
+ broadcastStats: broadcastStatsFor(core, options),
9923
+ partitionIds: options?.partitionIds,
9924
+ cursorBinding: queryBindingOf(params)
9181
9925
  });
9182
- } catch (err) {
9183
- console.warn("afterSearch plugin hook error:", err);
9184
- }
9185
- if (core.analysisRebuild.isStale(indexName)) {
9186
- result.analysisStale = true;
9926
+ try {
9927
+ await core.pluginRegistry.runHook("afterSearch", {
9928
+ indexName,
9929
+ params: resolvedParams,
9930
+ results: result
9931
+ });
9932
+ } catch (err) {
9933
+ console.warn("afterSearch plugin hook error:", err);
9934
+ }
9935
+ if (core.analysisRebuild.isStale(indexName)) {
9936
+ result.analysisStale = true;
9937
+ }
9938
+ return result;
9939
+ } finally {
9940
+ release();
9187
9941
  }
9188
- return result;
9189
9942
  }
9190
9943
  async function runEnginePreflight(core, indexName, params, options) {
9191
9944
  core.guardShutdown();
9192
- const entry = core.requireIndex(indexName);
9193
- const manager = core.requireManager(indexName);
9194
- const resolvedParams = await resolveVectorText(
9195
- scopedParams(params, options),
9196
- entry.embeddingAdapter,
9197
- core.abortController.signal,
9198
- entry.embeddingAdapterName
9199
- );
9200
- const workerSearch = core.orchestrator.isPromoted() ? core.orchestrator.searchViaWorker.bind(core.orchestrator) : void 0;
9201
- const result = await executePreflight(resolvedParams, {
9202
- manager,
9203
- language: entry.language,
9204
- config: entry.config,
9205
- workerSearch,
9206
- indexName,
9207
- broadcastStats: broadcastStatsFor(core, options),
9208
- partitionIds: options?.partitionIds,
9209
- cursorBinding: queryBindingOf(params)
9210
- });
9211
- if (core.analysisRebuild.isStale(indexName)) {
9212
- result.analysisStale = true;
9945
+ const release = await core.indexState.acquire(indexName);
9946
+ try {
9947
+ const entry = core.requireIndex(indexName);
9948
+ const manager = core.requireManager(indexName);
9949
+ const resolvedParams = await resolveVectorText(
9950
+ scopedParams(params, options),
9951
+ entry.embeddingAdapter,
9952
+ core.abortController.signal,
9953
+ entry.embeddingAdapterName
9954
+ );
9955
+ const workerSearch = core.orchestrator.isPromoted() ? core.orchestrator.searchViaWorker.bind(core.orchestrator) : void 0;
9956
+ const result = await executePreflight(resolvedParams, {
9957
+ manager,
9958
+ language: entry.language,
9959
+ config: entry.config,
9960
+ workerSearch,
9961
+ indexName,
9962
+ broadcastStats: broadcastStatsFor(core, options),
9963
+ partitionIds: options?.partitionIds,
9964
+ cursorBinding: queryBindingOf(params)
9965
+ });
9966
+ if (core.analysisRebuild.isStale(indexName)) {
9967
+ result.analysisStale = true;
9968
+ }
9969
+ return result;
9970
+ } finally {
9971
+ release();
9213
9972
  }
9214
- return result;
9215
9973
  }
9216
9974
  async function runEngineSuggest(core, indexName, params, partitionIds) {
9217
9975
  core.guardShutdown();
9218
- const result = executeSuggest(
9219
- core.requireManager(indexName),
9220
- core.requireIndex(indexName).language,
9221
- params,
9222
- partitionIds
9223
- );
9224
- if (core.analysisRebuild.isStale(indexName)) {
9225
- result.analysisStale = true;
9976
+ const release = await core.indexState.acquire(indexName);
9977
+ try {
9978
+ const result = executeSuggest(
9979
+ core.requireManager(indexName),
9980
+ core.requireIndex(indexName).language,
9981
+ params,
9982
+ partitionIds
9983
+ );
9984
+ if (core.analysisRebuild.isStale(indexName)) {
9985
+ result.analysisStale = true;
9986
+ }
9987
+ return result;
9988
+ } finally {
9989
+ release();
9226
9990
  }
9227
- return result;
9228
9991
  }
9229
- function runEngineQueryStats(core, indexName, terms, partitionIds) {
9992
+ async function runEngineQueryStats(core, indexName, terms, partitionIds) {
9230
9993
  core.guardShutdown();
9231
- const entry = core.requireIndex(indexName);
9232
- const manager = core.requireManager(indexName);
9233
- const stats = collectQueryTermStats(
9234
- manager,
9235
- terms.join(" "),
9236
- entry.language,
9237
- {
9238
- stopWords: manager.analysis.stopWords,
9239
- customTokenizer: manager.analysis.customTokenizer
9240
- },
9241
- partitionIds
9242
- );
9243
- return {
9244
- totalDocuments: stats.totalDocuments,
9245
- docFrequencies: stats.docFrequencies,
9246
- totalFieldLengths: stats.totalFieldLengths
9247
- };
9994
+ const release = await core.indexState.acquire(indexName);
9995
+ try {
9996
+ const entry = core.requireIndex(indexName);
9997
+ const manager = core.requireManager(indexName);
9998
+ const stats = collectQueryTermStats(
9999
+ manager,
10000
+ terms.join(" "),
10001
+ entry.language,
10002
+ {
10003
+ stopWords: manager.analysis.stopWords,
10004
+ customTokenizer: manager.analysis.customTokenizer
10005
+ },
10006
+ partitionIds
10007
+ );
10008
+ return {
10009
+ totalDocuments: stats.totalDocuments,
10010
+ docFrequencies: stats.docFrequencies,
10011
+ totalFieldLengths: stats.totalFieldLengths
10012
+ };
10013
+ } finally {
10014
+ release();
10015
+ }
9248
10016
  }
9249
10017
  async function runEngineListDocuments(core, indexName, params, partitionIds) {
9250
10018
  core.guardShutdown();
9251
- const entry = core.requireIndex(indexName);
9252
- return executeListDocuments(params, {
9253
- manager: core.requireManager(indexName),
9254
- schema: entry.config.schema,
9255
- partitionIds
9256
- });
10019
+ const release = await core.indexState.acquire(indexName);
10020
+ try {
10021
+ const entry = core.requireIndex(indexName);
10022
+ return executeListDocuments(params, {
10023
+ manager: core.requireManager(indexName),
10024
+ schema: entry.config.schema,
10025
+ partitionIds
10026
+ });
10027
+ } finally {
10028
+ release();
10029
+ }
9257
10030
  }
9258
10031
 
9259
10032
  // src/narsil/operations.ts
@@ -9273,6 +10046,16 @@ function createNarsilFromCore(core, config) {
9273
10046
  mutationCtx,
9274
10047
  rebalanceCtx
9275
10048
  } = core;
10049
+ async function withOpenIndex(indexName, action) {
10050
+ guardShutdown();
10051
+ requireIndex(indexName);
10052
+ const release = await core.indexState.acquire(indexName);
10053
+ try {
10054
+ return await action();
10055
+ } finally {
10056
+ release();
10057
+ }
10058
+ }
9276
10059
  const narsil = {
9277
10060
  createIndex(name, indexConfig) {
9278
10061
  return createEngineIndex(core, config, name, indexConfig);
@@ -9283,15 +10066,27 @@ function createNarsilFromCore(core, config) {
9283
10066
  dropIndex(name) {
9284
10067
  return dropEngineIndex(core, name);
9285
10068
  },
10069
+ open(indexName) {
10070
+ guardShutdown();
10071
+ requireIndex(indexName);
10072
+ return core.indexState.open(indexName);
10073
+ },
10074
+ close(indexName) {
10075
+ guardShutdown();
10076
+ requireIndex(indexName);
10077
+ return core.indexState.close(indexName);
10078
+ },
9286
10079
  listIndexes() {
9287
10080
  const infos = [];
9288
10081
  for (const [name, entry] of indexRegistry) {
9289
10082
  const manager = executor.getManager(name);
9290
10083
  infos.push({
9291
10084
  name,
9292
- documentCount: manager?.countDocuments() ?? 0,
9293
- partitionCount: manager?.partitionCount ?? 0,
10085
+ documentCount: manager?.countDocuments() ?? entry.documentCount,
10086
+ partitionCount: manager?.partitionCount ?? entry.partitionCount,
9294
10087
  language: entry.language.name,
10088
+ state: core.indexState.stateOf(name),
10089
+ reopenCount: core.indexState.reopenCount(name),
9295
10090
  ...core.analysisRebuild.isStale(name) ? { analysisStale: true } : {}
9296
10091
  });
9297
10092
  }
@@ -9302,8 +10097,8 @@ function createNarsilFromCore(core, config) {
9302
10097
  const entry = requireIndex(indexName);
9303
10098
  const manager = executor.getManager(indexName);
9304
10099
  return {
9305
- documentCount: manager?.countDocuments() ?? 0,
9306
- partitionCount: manager?.partitionCount ?? 0,
10100
+ documentCount: manager?.countDocuments() ?? entry.documentCount,
10101
+ partitionCount: manager?.partitionCount ?? entry.partitionCount,
9307
10102
  estimatedMemoryBytes: manager?.estimateMemoryBytes() ?? 0,
9308
10103
  language: entry.language.name,
9309
10104
  schema: entry.config.schema
@@ -9315,47 +10110,48 @@ function createNarsilFromCore(core, config) {
9315
10110
  return executor.getManager(indexName)?.getPartitionStats() ?? [];
9316
10111
  },
9317
10112
  insert(indexName, document, docId, options) {
9318
- return insertDocument(mutationCtx, indexName, document, docId, options);
10113
+ return withOpenIndex(indexName, () => insertDocument(mutationCtx, indexName, document, docId, options));
9319
10114
  },
9320
10115
  insertBatch(indexName, documents, options) {
9321
- return insertDocumentBatch(mutationCtx, indexName, documents, options);
10116
+ return withOpenIndex(indexName, () => insertDocumentBatch(mutationCtx, indexName, documents, options));
9322
10117
  },
9323
10118
  remove(indexName, docId) {
9324
- return removeDocument(mutationCtx, indexName, docId);
10119
+ return withOpenIndex(indexName, () => removeDocument(mutationCtx, indexName, docId));
9325
10120
  },
9326
10121
  removeBatch(indexName, docIds) {
9327
- return removeDocumentBatch(mutationCtx, indexName, docIds);
10122
+ return withOpenIndex(indexName, () => removeDocumentBatch(mutationCtx, indexName, docIds));
9328
10123
  },
9329
10124
  update(indexName, docId, document) {
9330
- return updateDocument(mutationCtx, indexName, docId, document);
10125
+ return withOpenIndex(indexName, () => updateDocument(mutationCtx, indexName, docId, document));
9331
10126
  },
9332
10127
  updateBatch(indexName, updates) {
9333
- return updateDocumentBatch(mutationCtx, indexName, updates);
10128
+ return withOpenIndex(indexName, () => updateDocumentBatch(mutationCtx, indexName, updates));
9334
10129
  },
9335
10130
  async get(indexName, docId) {
9336
- guardShutdown();
9337
- requireIndex(indexName);
9338
- return executor.execute({ type: "get", indexName, docId, requestId: docId });
10131
+ return withOpenIndex(indexName, () => executor.execute({ type: "get", indexName, docId, requestId: docId }));
9339
10132
  },
9340
10133
  async getMultiple(indexName, docIds) {
9341
10134
  guardShutdown();
9342
10135
  requireIndex(indexName);
9343
- const result = /* @__PURE__ */ new Map();
9344
- for (const docId of docIds) {
9345
- const doc = await narsil.get(indexName, docId);
9346
- if (doc !== void 0) result.set(docId, doc);
9347
- }
9348
- return result;
10136
+ return withOpenIndex(indexName, async () => {
10137
+ const result = /* @__PURE__ */ new Map();
10138
+ for (const docId of docIds) {
10139
+ const doc = await executor.execute({
10140
+ type: "get",
10141
+ indexName,
10142
+ docId,
10143
+ requestId: docId
10144
+ });
10145
+ if (doc !== void 0) result.set(docId, doc);
10146
+ }
10147
+ return result;
10148
+ });
9349
10149
  },
9350
10150
  async has(indexName, docId) {
9351
- guardShutdown();
9352
- requireIndex(indexName);
9353
- return executor.execute({ type: "has", indexName, docId, requestId: docId });
10151
+ return withOpenIndex(indexName, () => executor.execute({ type: "has", indexName, docId, requestId: docId }));
9354
10152
  },
9355
10153
  async countDocuments(indexName) {
9356
- guardShutdown();
9357
- requireIndex(indexName);
9358
- return executor.execute({ type: "count", indexName, requestId: indexName });
10154
+ return withOpenIndex(indexName, () => executor.execute({ type: "count", indexName, requestId: indexName }));
9359
10155
  },
9360
10156
  async listDocuments(indexName, params) {
9361
10157
  return runEngineListDocuments(core, indexName, params ?? {});
@@ -9372,15 +10168,15 @@ function createNarsilFromCore(core, config) {
9372
10168
  async rebuildAnalysis(indexName) {
9373
10169
  guardShutdown();
9374
10170
  requireIndex(indexName);
9375
- await core.analysisRebuild.rebuild(indexName);
10171
+ await withOpenIndex(indexName, () => core.analysisRebuild.rebuild(indexName));
9376
10172
  },
9377
10173
  async snapshot(indexName) {
9378
10174
  guardShutdown();
9379
- return createSnapshot(requireManager(indexName), requireIndex(indexName));
10175
+ return withOpenIndex(indexName, () => createSnapshot(requireManager(indexName), requireIndex(indexName)));
9380
10176
  },
9381
10177
  async restore(indexName, data) {
9382
10178
  guardShutdown();
9383
- return restoreFromSnapshot(indexName, data, {
10179
+ await restoreFromSnapshot(indexName, data, {
9384
10180
  executor,
9385
10181
  indexRegistry,
9386
10182
  getVectorFieldPaths,
@@ -9390,66 +10186,74 @@ function createNarsilFromCore(core, config) {
9390
10186
  embeddingAdapters: core.embeddingAdapters,
9391
10187
  defaultEmbeddingAdapter: config?.embedding ?? null,
9392
10188
  markAnalysisStale: core.analysisRebuild.markStale,
9393
- clearAnalysisStale: core.analysisRebuild.clearStale
10189
+ clearAnalysisStale: core.analysisRebuild.clearStale,
10190
+ indexState: core.indexState
9394
10191
  });
9395
10192
  },
9396
10193
  async checkpoint(indexName) {
9397
10194
  guardShutdown();
9398
10195
  requireIndex(indexName);
9399
- if (durability) {
9400
- await durability.manager.checkpoint(indexName);
9401
- }
10196
+ await withOpenIndex(indexName, async () => {
10197
+ if (durability) await durability.manager.checkpoint(indexName);
10198
+ });
9402
10199
  },
9403
10200
  async clear(indexName) {
9404
10201
  guardShutdown();
9405
10202
  requireIndex(indexName);
9406
- await executor.execute({ type: "clear", indexName, requestId: indexName });
9407
- await orchestrator.replicateToWorkers({ type: "clear", indexName, requestId: `replicate-clear-${indexName}` });
9408
- core.watermarkNotifier.forget(indexName);
10203
+ await withOpenIndex(indexName, async () => {
10204
+ await executor.execute({ type: "clear", indexName, requestId: indexName });
10205
+ await orchestrator.replicateToWorkers({ type: "clear", indexName, requestId: `replicate-clear-${indexName}` });
10206
+ core.watermarkNotifier.forget(indexName);
10207
+ });
9409
10208
  },
9410
10209
  async rebalance(indexName, targetPartitionCount) {
9411
10210
  guardShutdown();
9412
10211
  requireIndex(indexName);
9413
- return executeRebalance(requireManager(indexName), indexName, targetPartitionCount, rebalanceCtx);
10212
+ return withOpenIndex(
10213
+ indexName,
10214
+ () => executeRebalance(requireManager(indexName), indexName, targetPartitionCount, rebalanceCtx)
10215
+ );
9414
10216
  },
9415
10217
  async updatePartitionConfig(indexName, partitionConfig) {
9416
10218
  guardShutdown();
9417
- const entry = requireIndex(indexName);
9418
- const manager = requireManager(indexName);
9419
- if (rebalancingIndexes.has(indexName)) {
9420
- throw new NarsilError(
9421
- ErrorCodes.PARTITION_REBALANCING_BACKPRESSURE,
9422
- `Index "${indexName}" is currently being rebalanced`
9423
- );
9424
- }
9425
- validatePartitionConfig(partitionConfig);
9426
- if (partitionConfig.maxPartitions !== void 0 && partitionConfig.maxPartitions < manager.partitionCount) {
9427
- throw new NarsilError(
9428
- ErrorCodes.PARTITION_CAPACITY_EXCEEDED,
9429
- `maxPartitions (${partitionConfig.maxPartitions}) is less than the current partition count (${manager.partitionCount})`,
9430
- { maxPartitions: partitionConfig.maxPartitions, partitionCount: manager.partitionCount }
9431
- );
9432
- }
9433
- const currentDocCount = manager.countDocuments();
9434
- const newMaxDocs = partitionConfig.maxDocsPerPartition ?? entry.config.partitions?.maxDocsPerPartition;
9435
- if (newMaxDocs !== void 0) {
9436
- const newTotalCapacity = newMaxDocs * manager.partitionCount;
9437
- if (newTotalCapacity < currentDocCount) {
10219
+ return withOpenIndex(indexName, async () => {
10220
+ const entry = requireIndex(indexName);
10221
+ const manager = requireManager(indexName);
10222
+ if (rebalancingIndexes.has(indexName)) {
10223
+ throw new NarsilError(
10224
+ ErrorCodes.PARTITION_REBALANCING_BACKPRESSURE,
10225
+ `Index "${indexName}" is currently being rebalanced`
10226
+ );
10227
+ }
10228
+ validatePartitionConfig(partitionConfig);
10229
+ if (partitionConfig.maxPartitions !== void 0 && partitionConfig.maxPartitions < manager.partitionCount) {
9438
10230
  throw new NarsilError(
9439
10231
  ErrorCodes.PARTITION_CAPACITY_EXCEEDED,
9440
- `New capacity (${newTotalCapacity}) is less than current document count (${currentDocCount})`,
9441
- { newTotalCapacity, currentDocCount }
10232
+ `maxPartitions (${partitionConfig.maxPartitions}) is less than the current partition count (${manager.partitionCount})`,
10233
+ { maxPartitions: partitionConfig.maxPartitions, partitionCount: manager.partitionCount }
9442
10234
  );
9443
10235
  }
9444
- }
9445
- if (!entry.config.partitions) entry.config.partitions = {};
9446
- if (partitionConfig.maxDocsPerPartition !== void 0)
9447
- entry.config.partitions.maxDocsPerPartition = partitionConfig.maxDocsPerPartition;
9448
- if (partitionConfig.maxPartitions !== void 0)
9449
- entry.config.partitions.maxPartitions = partitionConfig.maxPartitions;
9450
- if (partitionConfig.watermark !== void 0) entry.config.partitions.watermark = partitionConfig.watermark;
9451
- if (durability) await durability.manager.persistMetadata(indexName);
9452
- core.watermarkNotifier.check(indexName);
10236
+ const currentDocCount = manager.countDocuments();
10237
+ const newMaxDocs = partitionConfig.maxDocsPerPartition ?? entry.config.partitions?.maxDocsPerPartition;
10238
+ if (newMaxDocs !== void 0) {
10239
+ const newTotalCapacity = newMaxDocs * manager.partitionCount;
10240
+ if (newTotalCapacity < currentDocCount) {
10241
+ throw new NarsilError(
10242
+ ErrorCodes.PARTITION_CAPACITY_EXCEEDED,
10243
+ `New capacity (${newTotalCapacity}) is less than current document count (${currentDocCount})`,
10244
+ { newTotalCapacity, currentDocCount }
10245
+ );
10246
+ }
10247
+ }
10248
+ if (!entry.config.partitions) entry.config.partitions = {};
10249
+ if (partitionConfig.maxDocsPerPartition !== void 0)
10250
+ entry.config.partitions.maxDocsPerPartition = partitionConfig.maxDocsPerPartition;
10251
+ if (partitionConfig.maxPartitions !== void 0)
10252
+ entry.config.partitions.maxPartitions = partitionConfig.maxPartitions;
10253
+ if (partitionConfig.watermark !== void 0) entry.config.partitions.watermark = partitionConfig.watermark;
10254
+ if (durability) await durability.manager.persistMetadata(indexName);
10255
+ core.watermarkNotifier.check(indexName);
10256
+ });
9453
10257
  },
9454
10258
  async getMemoryStats() {
9455
10259
  let estimatedIndexBytes = 0;
@@ -9457,22 +10261,33 @@ function createNarsilFromCore(core, config) {
9457
10261
  estimatedIndexBytes += executor.getManager(name)?.estimateMemoryBytes() ?? 0;
9458
10262
  }
9459
10263
  const workers = await orchestrator.getWorkerMemoryStats();
9460
- return { process: readProcessMemory(), estimatedIndexBytes, workers };
10264
+ const lifecycleCounts = core.indexState.counts();
10265
+ return {
10266
+ process: readProcessMemory(),
10267
+ estimatedIndexBytes,
10268
+ openIndexCount: lifecycleCounts.open,
10269
+ closedIndexCount: lifecycleCounts.closed,
10270
+ reopenCount: lifecycleCounts.reopens,
10271
+ workers
10272
+ };
9461
10273
  },
9462
10274
  async compactVectors(indexName, fieldName) {
9463
10275
  guardShutdown();
9464
10276
  requireIndex(indexName);
9465
- compactVectors(requireManager(indexName), indexName, fieldName);
10277
+ await withOpenIndex(indexName, async () => {
10278
+ compactVectors(requireManager(indexName), indexName, fieldName);
10279
+ });
9466
10280
  },
9467
10281
  async optimizeVectors(indexName, fieldName) {
9468
10282
  guardShutdown();
9469
10283
  requireIndex(indexName);
9470
- await optimizeVectors(requireManager(indexName), indexName, fieldName);
10284
+ await withOpenIndex(indexName, () => optimizeVectors(requireManager(indexName), indexName, fieldName));
9471
10285
  },
9472
10286
  vectorMaintenanceStatus(indexName) {
9473
10287
  guardShutdown();
9474
10288
  requireIndex(indexName);
9475
- return getVectorMaintenanceStatus(requireManager(indexName));
10289
+ const manager = executor.getManager(indexName);
10290
+ return manager === void 0 ? [] : getVectorMaintenanceStatus(manager);
9476
10291
  },
9477
10292
  on(event, handler) {
9478
10293
  const key = event;
@@ -9505,12 +10320,12 @@ function createNarsilFromCore(core, config) {
9505
10320
  async function createNarsil(config) {
9506
10321
  const core = createEngineCore(config);
9507
10322
  if (core.durability) {
9508
- await core.durability.manager.recover();
10323
+ await core.durability.manager.recover(config?.lifecycle !== void 0);
9509
10324
  }
9510
10325
  if (core.invalidation) {
9511
10326
  await core.invalidation.start();
9512
10327
  }
9513
- await core.analysisRebuild.reviewStaleIndexes();
10328
+ if (config?.lifecycle === void 0) await core.analysisRebuild.reviewStaleIndexes();
9514
10329
  return createNarsilFromCore(core, config);
9515
10330
  }
9516
10331