@delali/narsil 0.2.4-next.42 → 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 (36) hide show
  1. package/dist/adapters/filesystem.mjs +1 -1
  2. package/dist/adapters/indexeddb.mjs +1 -1
  3. package/dist/{chunk-GB2IIJJG.mjs → chunk-P4ELXVJQ.mjs} +2 -4
  4. package/dist/{chunk-MMSWEEZT.mjs → chunk-WASNWIEK.mjs} +1452 -373
  5. package/dist/{chunk-GRWFY2HI.mjs → chunk-XNCJFU2E.mjs} +239 -61
  6. package/dist/{chunk-WU2UZEVD.mjs → chunk-XWICBGYT.mjs} +1 -1
  7. package/dist/{client-BobVnoIK.d.ts → client-C5BkUqB8.d.ts} +26 -3
  8. package/dist/client.d.ts +8 -8
  9. package/dist/client.mjs +12 -6
  10. package/dist/{config-fLC4lbmg.d.ts → config-B-ZI-ft5.d.ts} +2 -2
  11. package/dist/{config-Byyl1QqH.d.ts → config-BZRExglL.d.ts} +20 -3
  12. package/dist/{direct-executor-MSJEVPZU.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 +1721 -1129
  19. package/dist/embeddings/openai.mjs +1 -1
  20. package/dist/{engine-BE45D5ZG.d.ts → engine-Cjr1uBLa.d.ts} +47 -24
  21. package/dist/index.browser.mjs +1668 -416
  22. package/dist/index.d.ts +9 -9
  23. package/dist/index.mjs +4 -4
  24. package/dist/{options-EXYFcem-.d.ts → options-BC68VPoE.d.ts} +10 -3
  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 +6 -6
  28. package/dist/react.mjs +2 -4
  29. package/dist/{schema-ZRbk8BgH.d.ts → schema-B7juWg8Q.d.ts} +17 -6
  30. package/dist/{search-DHuXwJYk.d.ts → search-DDHjfXhJ.d.ts} +35 -11
  31. package/dist/server.d.ts +10 -10
  32. package/dist/server.mjs +30 -12
  33. package/dist/{tasks-CnMLpfk-.d.ts → tasks-Bn4R1NPN.d.ts} +1 -1
  34. package/dist/{types-B5Tsaalz.d.ts → types-DsSSjY4d.d.ts} +1 -1
  35. package/dist/workers/entry.mjs +2 -2
  36. 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-WU2UZEVD.mjs';
3
- import { getNestedValue, getLanguage, extractVectorFieldsFromSchema, createDirectExecutor, createPartitionRouter, clampRowCount, DEFAULT_PAGE_SIZE, validateSchema, validateVectorPromotion, collectQueryTermStats, createPartitionManager, createVectorIndex, flattenSchema, isTextFieldType, requireWithinResultWindow, normalizeSort, sortSignatureOf, requireSortableFields, fanOutQuery, applySorting, applyPagination, resolveProjection, tokenize, decodePageCursor, requireMatchingCursor, encodePageCursor, resolveIndexAnalysis, deserializePayloadV2, deserializePayloadV1, validateRequiredFields, mergePartitionStats, fulltextMatches, mergeFacets, pruneStatsToQueryTerms, createPartitionIndex, fnv1a, createWorkerPool, validateDocument, validateDocumentStrict, createFrozenSegment, detectRuntime, isNodeMainThread, spawnNodeWorker, createSharedFrozenSegment, buildCompactedSegmentPayload, freezeSegmentShared, deserializeMetadata, isCompositePartition, serializeMetadata, kWayMerge } from './chunk-GRWFY2HI.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-GB2IIJJG.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
@@ -57,7 +57,7 @@ function createRebalancer() {
57
57
  skipClone: true,
58
58
  stopWordOverride: manager.analysis.stopWords,
59
59
  customTokenizer: manager.analysis.customTokenizer,
60
- collectSurfaces: manager.config.surfaceForms === true
60
+ collectSurfaces: manager.config.surfaceForms !== false
61
61
  };
62
62
  for (const partition of currentPartitions) {
63
63
  const docIds = [...partition.docIds()];
@@ -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) {
@@ -952,6 +962,9 @@ function writeHeader(header) {
952
962
  }
953
963
  return buffer;
954
964
  }
965
+ function hasNrslMagic(data) {
966
+ return data.length >= MAGIC_BYTES.length && data[0] === MAGIC_BYTES[0] && data[1] === MAGIC_BYTES[1] && data[2] === MAGIC_BYTES[2] && data[3] === MAGIC_BYTES[3];
967
+ }
955
968
  function readHeader(data) {
956
969
  if (data.length < HEADER_SIZE) {
957
970
  throw new NarsilError(
@@ -987,6 +1000,7 @@ function readHeader(data) {
987
1000
  // src/serialization/envelope.ts
988
1001
  var CURRENT_ENVELOPE_VERSION = 1;
989
1002
  var SNAPSHOT_ENVELOPE_VERSION = 2;
1003
+ var INDEX_SNAPSHOT_ENVELOPE_VERSION = 3;
990
1004
  function parseEngineVersion(version) {
991
1005
  const parts = version.split(".");
992
1006
  return [parseInt(parts[0] ?? "0", 10), parseInt(parts[1] ?? "0", 10), parseInt(parts[2] ?? "0", 10)];
@@ -1131,6 +1145,19 @@ function concatEnvelopeParts(parts) {
1131
1145
  async function unpackEnvelopeBytes(data) {
1132
1146
  return unpackEnvelope(data, SNAPSHOT_ENVELOPE_VERSION);
1133
1147
  }
1148
+ async function packIndexSnapshotEnvelope(payloadBytes) {
1149
+ return packEnvelope(payloadBytes, { checksum: true, envelopeFormatVersion: INDEX_SNAPSHOT_ENVELOPE_VERSION });
1150
+ }
1151
+ async function unpackIndexSnapshotEnvelope(data) {
1152
+ const { header, payloadBytes } = await unpackEnvelope(data, INDEX_SNAPSHOT_ENVELOPE_VERSION);
1153
+ if (header.envelopeFormatVersion !== INDEX_SNAPSHOT_ENVELOPE_VERSION) {
1154
+ throw new NarsilError(
1155
+ ErrorCodes.ENVELOPE_VERSION_MISMATCH,
1156
+ `Expected an index snapshot envelope of format v${INDEX_SNAPSHOT_ENVELOPE_VERSION}, received format v${header.envelopeFormatVersion}`
1157
+ );
1158
+ }
1159
+ return payloadBytes;
1160
+ }
1134
1161
  async function writeMetadataEnvelope(meta, options = {}) {
1135
1162
  const payloadBytes = serializeMetadata(meta);
1136
1163
  return packEnvelope(payloadBytes, options);
@@ -1818,6 +1845,9 @@ function mergeTimeOrderedSegments(ordered, fallback) {
1818
1845
  validateMergedPartition(merged);
1819
1846
  return merged;
1820
1847
  }
1848
+ function countLiveDocuments(ordered) {
1849
+ return resolveWinners(ordered).size;
1850
+ }
1821
1851
  function resolveWinners(ordered) {
1822
1852
  const winner = /* @__PURE__ */ new Map();
1823
1853
  for (let segIndex = 0; segIndex < ordered.length; segIndex += 1) {
@@ -3310,6 +3340,7 @@ async function writeSegmentedCheckpoint(input) {
3310
3340
  });
3311
3341
  }
3312
3342
  const partitions = [];
3343
+ let wholeDocumentCount = input.wholePartitionPayload === void 0 ? null : 0;
3313
3344
  for (const target of input.targets) {
3314
3345
  const priorPartition = priorManifest?.partitions.find((p) => p.partitionId === target.partitionId);
3315
3346
  const priorSeqNo = snapshotCheckpointFor(priorManifest?.checkpoint ?? [], target.partitionId);
@@ -3320,15 +3351,13 @@ async function writeSegmentedCheckpoint(input) {
3320
3351
  priorSeqNo,
3321
3352
  target.lastSeqNo
3322
3353
  );
3323
- partitions.push(
3324
- input.wholePartitionPayload === void 0 ? await writePartition(context, target.partitionId, priorPartition, entries) : await writeWholePartition(
3325
- context,
3326
- target.partitionId,
3327
- priorPartition,
3328
- input.wholePartitionPayload(target.partitionId),
3329
- entries
3330
- )
3331
- );
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
+ }
3332
3361
  }
3333
3362
  carryForwardUncheckpointedPartitions(priorManifest, input.targets, partitions, checkpointByPartition);
3334
3363
  const manifest = {
@@ -3340,6 +3369,7 @@ async function writeSegmentedCheckpoint(input) {
3340
3369
  const parts = await encodeSegmentManifest(manifest);
3341
3370
  await directory.atomicWrite(manifestKey(indexName), [parts.header, parts.payload]);
3342
3371
  await collectGarbage(directory, indexName, priorManifest, manifest);
3372
+ return wholeDocumentCount;
3343
3373
  }
3344
3374
  async function writePartition(context, partitionId, priorPartition, entries) {
3345
3375
  let segments = priorPartition ? [...priorPartition.segments] : [];
@@ -3459,9 +3489,9 @@ async function writeIndexCheckpoint(input) {
3459
3489
  const { directory, metadata, targets, compactionThreshold, manager } = input;
3460
3490
  const offloaded = !input.fromMemory && input.canOffload && metadata.tokenizer === void 0 && metadata.stopWords === void 0 && await runCheckpointOnWorker({ root: directory.root, metadata, targets, compactionThreshold });
3461
3491
  if (offloaded) {
3462
- return;
3492
+ return null;
3463
3493
  }
3464
- await writeSegmentedCheckpoint({
3494
+ return writeSegmentedCheckpoint({
3465
3495
  directory,
3466
3496
  metadata,
3467
3497
  targets,
@@ -3493,9 +3523,171 @@ function createSeqOwner(startSeqNo, primaryTerm = SINGLE_NODE_PRIMARY_TERM) {
3493
3523
  };
3494
3524
  }
3495
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
+
3496
3687
  // src/persistence/durability/types.ts
3497
3688
  var DEFAULT_CHECKPOINT_INTERVAL_MS = 3e5;
3498
3689
  var DEFAULT_CHECKPOINT_MUTATION_THRESHOLD = 1e5;
3690
+ var DEFAULT_ASYNC_FLUSH_INTERVAL_MS = 1e3;
3499
3691
 
3500
3692
  // src/persistence/durability/group-commit.ts
3501
3693
  function createGroupCommitCoordinator(sync) {
@@ -3741,7 +3933,6 @@ function createWalWriter(directory, config) {
3741
3933
  }
3742
3934
 
3743
3935
  // src/persistence/durability/manager.ts
3744
- var DEFAULT_ASYNC_FLUSH_INTERVAL_MS = 1e3;
3745
3936
  function createDurabilityManager(config, hooks, directoryOverride) {
3746
3937
  const directory = createDurableDirectory(config.directory ?? "");
3747
3938
  const canOffloadCheckpoint = directoryOverride === void 0;
@@ -3772,20 +3963,20 @@ function createDurabilityManager(config, hooks, directoryOverride) {
3772
3963
  }
3773
3964
  hooks.onFatalError(error);
3774
3965
  }
3775
- function toError3(err) {
3966
+ function toError4(err) {
3776
3967
  return err instanceof Error ? err : new Error(String(err));
3777
3968
  }
3778
3969
  async function closeWriterReportingFailure(closePromise) {
3779
3970
  try {
3780
3971
  await closePromise;
3781
3972
  } catch (err) {
3782
- markFatal(toError3(err));
3973
+ markFatal(toError4(err));
3783
3974
  }
3784
3975
  }
3785
3976
  function getOrCreateIndexState(indexName) {
3786
3977
  let state = indexes.get(indexName);
3787
3978
  if (state === void 0) {
3788
- state = { partitions: /* @__PURE__ */ new Map(), mutationsSinceCheckpoint: 0, checkpointInFlight: null };
3979
+ state = { partitions: /* @__PURE__ */ new Map(), mutationsSinceCheckpoint: 0, checkpointInFlight: null, unloading: false };
3789
3980
  indexes.set(indexName, state);
3790
3981
  }
3791
3982
  return state;
@@ -3848,7 +4039,7 @@ function createDurabilityManager(config, hooks, directoryOverride) {
3848
4039
  try {
3849
4040
  await partition.walWriter.commit();
3850
4041
  } catch (err) {
3851
- partition.failed = toError3(err);
4042
+ partition.failed = toError4(err);
3852
4043
  markFatal(partition.failed);
3853
4044
  }
3854
4045
  }
@@ -3862,7 +4053,7 @@ function createDurabilityManager(config, hooks, directoryOverride) {
3862
4053
  try {
3863
4054
  await checkpointIndex(indexName);
3864
4055
  } catch (err) {
3865
- markFatal(toError3(err));
4056
+ markFatal(toError4(err));
3866
4057
  }
3867
4058
  }
3868
4059
  }
@@ -3879,7 +4070,7 @@ function createDurabilityManager(config, hooks, directoryOverride) {
3879
4070
  }
3880
4071
  async function checkpointIndex(indexName, fromMemory = false) {
3881
4072
  const indexState = indexes.get(indexName);
3882
- if (indexState === void 0) {
4073
+ if (indexState === void 0 || indexState.unloading) {
3883
4074
  return;
3884
4075
  }
3885
4076
  if (indexState.checkpointInFlight !== null) {
@@ -3895,82 +4086,46 @@ function createDurabilityManager(config, hooks, directoryOverride) {
3895
4086
  return run;
3896
4087
  }
3897
4088
  async function performCheckpoint(indexName, indexState, fromMemory = false) {
3898
- const manager = hooks.getManager(indexName);
3899
- if (manager === void 0) {
3900
- return;
3901
- }
3902
- const metadata = hooks.buildMetadata(indexName);
3903
- if (metadata === void 0) {
3904
- return;
3905
- }
3906
- const targets = [];
3907
- for (let i = 0; i < manager.partitionCount; i += 1) {
3908
- const partition = indexState.partitions.get(i);
3909
- targets.push({
3910
- partitionId: i,
3911
- lastSeqNo: partition?.appliedSeqNo ?? 0,
3912
- primaryTerm: partition?.seqOwner.primaryTerm ?? SINGLE_NODE_PRIMARY_TERM
3913
- });
3914
- if (partition !== void 0) {
3915
- try {
3916
- await partition.walWriter.commit();
3917
- } catch (err) {
3918
- partition.failed = toError3(err);
3919
- markFatal(partition.failed);
3920
- throw partition.failed;
3921
- }
3922
- }
3923
- }
3924
- await writeIndexCheckpoint({
4089
+ await runDurableCheckpoint({
3925
4090
  directory,
3926
- metadata,
3927
- targets,
4091
+ hooks,
4092
+ indexName,
4093
+ indexState,
3928
4094
  compactionThreshold,
3929
- manager,
3930
4095
  canOffload: canOffloadCheckpoint,
3931
- fromMemory
4096
+ fromMemory,
4097
+ queueMetadataWrite,
4098
+ markFatal
3932
4099
  });
3933
- await truncateCoveredSegments(directory, indexName, targets);
3934
- await reclaimWalBeyondCount(directory, indexName, manager.partitionCount, indexState.partitions, markFatal);
3935
- indexState.mutationsSinceCheckpoint = 0;
3936
4100
  }
3937
- async function recoverIndex(indexName) {
3938
- const metadata = await loadMetadata(directory, indexName);
3939
- if (metadata === null) {
3940
- return;
3941
- }
3942
- await hooks.createIndexFromMetadata(metadata);
3943
- const manager = hooks.getManager(indexName);
3944
- if (manager === void 0) {
3945
- return;
3946
- }
3947
- const deps = {
3948
- manager,
3949
- vectorFieldPaths: hooks.getVectorFieldPaths(indexName),
3950
- vectorIndexes: hooks.getVectorIndexes(indexName)
3951
- };
3952
- const checkpoint = await loadSnapshot(directory, indexName, deps);
3953
- const manifest = await readSegmentManifest(directory, indexName);
3954
- if (manifest !== null) {
3955
- await reclaimOrphanedSegments(directory, indexName, manifest);
3956
- }
3957
- for (let partitionId = 0; partitionId < manager.partitionCount; partitionId += 1) {
3958
- const fromSeqNo = snapshotCheckpointFor(checkpoint, partitionId);
3959
- const { highestSeqNo } = await replayWal(directory, indexName, partitionId, fromSeqNo, deps);
3960
- getOrCreatePartition(indexName, partitionId, highestSeqNo);
3961
- }
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
+ );
3962
4114
  }
3963
4115
  return {
3964
4116
  isActive() {
3965
4117
  return true;
3966
4118
  },
3967
- async recover() {
4119
+ async recover(metadataOnly = false) {
3968
4120
  const names = await listPersistedIndexes(directory);
3969
4121
  for (const indexName of names) {
3970
- await recoverIndex(indexName);
4122
+ await recoverIndex(indexName, metadataOnly);
3971
4123
  }
3972
4124
  startCheckpointTimer();
3973
4125
  },
4126
+ recoverIndex(indexName) {
4127
+ return recoverIndex(indexName);
4128
+ },
3974
4129
  highestPersistedSeqNo(indexName, partitionId) {
3975
4130
  return indexes.get(indexName)?.partitions.get(partitionId)?.appliedSeqNo ?? 0;
3976
4131
  },
@@ -3994,7 +4149,7 @@ function createDurabilityManager(config, hooks, directoryOverride) {
3994
4149
  const entry = buildMutationEntry(record, allocatedSeqNo);
3995
4150
  await partition.walWriter.append(entry);
3996
4151
  } catch (err) {
3997
- partition.failed = toError3(err);
4152
+ partition.failed = toError4(err);
3998
4153
  markFatal(partition.failed);
3999
4154
  throw partition.failed;
4000
4155
  }
@@ -4006,7 +4161,7 @@ function createDurabilityManager(config, hooks, directoryOverride) {
4006
4161
  try {
4007
4162
  await partition.walWriter.commit();
4008
4163
  } catch (err) {
4009
- partition.failed = toError3(err);
4164
+ partition.failed = toError4(err);
4010
4165
  markFatal(partition.failed);
4011
4166
  throw partition.failed;
4012
4167
  }
@@ -4016,7 +4171,7 @@ function createDurabilityManager(config, hooks, directoryOverride) {
4016
4171
  startAsyncFlushTimer();
4017
4172
  if (indexState.mutationsSinceCheckpoint >= checkpointMutationThreshold) {
4018
4173
  void checkpointIndex(record.indexName).catch((err) => {
4019
- markFatal(toError3(err));
4174
+ markFatal(toError4(err));
4020
4175
  });
4021
4176
  }
4022
4177
  return allocatedSeqNo;
@@ -4046,15 +4201,23 @@ function createDurabilityManager(config, hooks, directoryOverride) {
4046
4201
  async removeIndex(indexName) {
4047
4202
  const indexState = indexes.get(indexName);
4048
4203
  if (indexState !== void 0) {
4049
- for (const partition of indexState.partitions.values()) {
4050
- await closeWriterReportingFailure(partition.walWriter.close());
4051
- }
4204
+ await drainIndexStateForUnload(indexState, metadataWrites.get(indexName), closeWriterReportingFailure);
4052
4205
  indexes.delete(indexName);
4206
+ metadataWrites.delete(indexName);
4053
4207
  }
4054
4208
  for (const key of await directory.list(`${indexName}/`)) {
4055
4209
  await directory.remove(key);
4056
4210
  }
4057
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
+ },
4058
4221
  async shutdown() {
4059
4222
  shuttingDown = true;
4060
4223
  if (checkpointTimer !== null) {
@@ -4093,7 +4256,13 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
4093
4256
  function getOrCreateIndexState(indexName) {
4094
4257
  let state = indexes.get(indexName);
4095
4258
  if (state === void 0) {
4096
- 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
+ };
4097
4266
  indexes.set(indexName, state);
4098
4267
  }
4099
4268
  return state;
@@ -4124,10 +4293,11 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
4124
4293
  if (manager === void 0 || metadata === void 0) {
4125
4294
  return;
4126
4295
  }
4296
+ const documentCount = manager.countDocuments();
4127
4297
  const seqNoByPartition = /* @__PURE__ */ new Map();
4128
4298
  const primaryTermByPartition = /* @__PURE__ */ new Map();
4129
4299
  for (let i = 0; i < manager.partitionCount; i += 1) {
4130
- seqNoByPartition.set(i, 0);
4300
+ seqNoByPartition.set(i, indexState.appliedSeqNoByPartition.get(i) ?? 0);
4131
4301
  primaryTermByPartition.set(i, SINGLE_NODE_PRIMARY_TERM);
4132
4302
  }
4133
4303
  const { parts } = await buildSnapshotBundleBytes({
@@ -4143,6 +4313,13 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
4143
4313
  primaryTermByPartition
4144
4314
  });
4145
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
+ });
4146
4323
  indexState.mutationsSinceCheckpoint = 0;
4147
4324
  if (publisher !== void 0) {
4148
4325
  const partitionIds = [];
@@ -4152,15 +4329,18 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
4152
4329
  await publisher.publishPartitions(indexName, partitionIds);
4153
4330
  }
4154
4331
  }
4155
- async function checkpointIndex(indexName) {
4332
+ async function checkpointIndex(indexName, forceFresh = false) {
4156
4333
  const indexState = indexes.get(indexName);
4157
- if (indexState === void 0) {
4334
+ if (indexState === void 0 || indexState.unloading) {
4158
4335
  return;
4159
4336
  }
4160
4337
  if (indexState.checkpointInFlight !== null) {
4161
- return indexState.checkpointInFlight;
4338
+ await indexState.checkpointInFlight;
4339
+ if (!forceFresh) {
4340
+ return;
4341
+ }
4162
4342
  }
4163
- const run = performCheckpoint(indexName, indexState).finally(() => {
4343
+ const run = indexState.applyChain.then(() => performCheckpoint(indexName, indexState)).finally(() => {
4164
4344
  indexState.checkpointInFlight = null;
4165
4345
  });
4166
4346
  indexState.checkpointInFlight = run;
@@ -4175,13 +4355,35 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
4175
4355
  );
4176
4356
  return queued;
4177
4357
  }
4178
- async function recoverIndex(indexName) {
4358
+ async function recoverIndex(indexName, metadataOnly = false) {
4179
4359
  const metaBytes = await adapter.load(metadataKey(indexName));
4180
4360
  if (metaBytes === null) {
4181
4361
  return;
4182
4362
  }
4183
4363
  const { metadata } = await readMetadataEnvelope(metaBytes);
4184
- 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
+ }
4185
4387
  const manager = hooks.getManager(indexName);
4186
4388
  if (manager === void 0) {
4187
4389
  return;
@@ -4190,33 +4392,58 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
4190
4392
  if (snapshotBytes === null) {
4191
4393
  return;
4192
4394
  }
4193
- await loadSnapshotBundleBytes(snapshotBytes, {
4395
+ const checkpoint = await loadSnapshotBundleBytes(snapshotBytes, {
4194
4396
  manager,
4195
4397
  vectorFieldPaths: hooks.getVectorFieldPaths(indexName),
4196
4398
  vectorIndexes: hooks.getVectorIndexes(indexName)
4197
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);
4198
4414
  }
4199
4415
  return {
4200
4416
  isActive() {
4201
4417
  return true;
4202
4418
  },
4203
- async recover() {
4419
+ async recover(metadataOnly = false) {
4204
4420
  const keys = await adapter.list("");
4205
4421
  for (const key of keys) {
4206
4422
  if (key.endsWith("/meta")) {
4207
- await recoverIndex(key.slice(0, -"/meta".length));
4423
+ await recoverIndex(key.slice(0, -"/meta".length), metadataOnly);
4208
4424
  }
4209
4425
  }
4210
4426
  startCheckpointTimer();
4211
4427
  },
4212
- highestPersistedSeqNo() {
4213
- return 0;
4428
+ recoverIndex(indexName) {
4429
+ return recoverIndex(indexName);
4430
+ },
4431
+ highestPersistedSeqNo(indexName, partitionId) {
4432
+ return indexes.get(indexName)?.appliedSeqNoByPartition.get(partitionId) ?? 0;
4214
4433
  },
4215
4434
  async recordMutation(record) {
4216
4435
  const indexState = getOrCreateIndexState(record.indexName);
4217
- const buffered = indexState.applyChain.then(() => record.apply());
4218
- indexState.applyChain = buffered.catch(() => void 0);
4219
- 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;
4220
4447
  indexState.mutationsSinceCheckpoint += 1;
4221
4448
  startCheckpointTimer();
4222
4449
  if (indexState.mutationsSinceCheckpoint >= checkpointMutationThreshold) {
@@ -4224,7 +4451,7 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
4224
4451
  hooks.onFatalError(err instanceof Error ? err : new Error(String(err)));
4225
4452
  });
4226
4453
  }
4227
- return 0;
4454
+ return appliedSeqNo;
4228
4455
  },
4229
4456
  persistMetadata(indexName) {
4230
4457
  return queueMetadataWrite(indexName, async () => {
@@ -4240,17 +4467,49 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
4240
4467
  checkpoint(indexName) {
4241
4468
  return checkpointIndex(indexName);
4242
4469
  },
4470
+ checkpointFromMemory(indexName) {
4471
+ return checkpointIndex(indexName, true);
4472
+ },
4243
4473
  async checkpointAll() {
4244
4474
  for (const indexName of [...indexes.keys()]) {
4245
4475
  await checkpointIndex(indexName);
4246
4476
  }
4247
4477
  },
4248
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
+ }
4249
4491
  indexes.delete(indexName);
4492
+ metadataWrites.delete(indexName);
4250
4493
  for (const key of await adapter.list(`${indexName}/`)) {
4251
4494
  await adapter.delete(key);
4252
4495
  }
4253
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
+ },
4254
4513
  async reloadIndex(indexName) {
4255
4514
  const indexState = getOrCreateIndexState(indexName);
4256
4515
  const reload = indexState.applyChain.then(() => recoverIndex(indexName));
@@ -4274,21 +4533,22 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
4274
4533
  }
4275
4534
 
4276
4535
  // src/engine/durability-integration.ts
4277
- function buildMetadata(indexName, hooks) {
4536
+ function buildMetadata(indexName, hooks, documentCount) {
4278
4537
  const config = hooks.getIndexConfig(indexName);
4279
4538
  const manager = hooks.getManager(indexName);
4280
- if (config === void 0 || manager === void 0) {
4539
+ if (config === void 0) {
4281
4540
  return void 0;
4282
4541
  }
4283
4542
  const metadata = {
4284
4543
  indexName,
4285
4544
  schema: config.schema,
4286
4545
  language: config.language,
4287
- partitionCount: manager.partitionCount,
4546
+ partitionCount: manager?.partitionCount ?? config.partitionCount,
4288
4547
  bm25Params: { k1: config.k1, b: config.b },
4289
4548
  createdAt: Date.now(),
4290
4549
  engineVersion: VERSION
4291
4550
  };
4551
+ metadata.documentCount = documentCount ?? config.documentCount;
4292
4552
  if (config.embedding) {
4293
4553
  metadata.embedding = config.embedding;
4294
4554
  }
@@ -4338,8 +4598,9 @@ function createDurabilityIntegration(tier, hooks) {
4338
4598
  getManager: hooks.getManager,
4339
4599
  getVectorFieldPaths: hooks.getVectorFieldPaths,
4340
4600
  getVectorIndexes: hooks.getVectorIndexes,
4341
- buildMetadata: (indexName) => buildMetadata(indexName, hooks),
4601
+ buildMetadata: (indexName, documentCount) => buildMetadata(indexName, hooks, documentCount),
4342
4602
  createIndexFromMetadata: hooks.createIndexFromMetadata,
4603
+ recordCheckpoint: hooks.recordCheckpoint,
4343
4604
  onFatalError: hooks.onFatalError
4344
4605
  };
4345
4606
  const manager = tier.kind === "wal" ? createDurabilityManager(tier.config, managerHooks) : createSnapshotOnlyManager(tier.adapter, tier.config, managerHooks, hooks.checkpointPublisher);
@@ -4378,9 +4639,9 @@ function createDurabilityFromTier(tier, wiring) {
4378
4639
  }
4379
4640
  return createDurabilityIntegration(tier, {
4380
4641
  checkpointPublisher: { publishPartitions: wiring.publishCheckpointedPartitions },
4381
- getManager: (indexName) => wiring.indexRegistry.has(indexName) ? wiring.requireManager(indexName) : void 0,
4642
+ getManager: (indexName) => wiring.indexRegistry.has(indexName) ? wiring.getManager(indexName) : void 0,
4382
4643
  getVectorFieldPaths: (indexName) => wiring.indexRegistry.get(indexName)?.vectorFieldPaths ?? /* @__PURE__ */ new Set(),
4383
- 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(),
4384
4645
  getIndexConfig: (indexName) => {
4385
4646
  const entry = wiring.indexRegistry.get(indexName);
4386
4647
  if (entry === void 0) {
@@ -4393,6 +4654,8 @@ function createDurabilityFromTier(tier, wiring) {
4393
4654
  return {
4394
4655
  ...entry.indexUuid !== null ? { indexUuid: entry.indexUuid } : {},
4395
4656
  ...entry.heldPartitions !== null ? { heldPartitions: entry.heldPartitions } : {},
4657
+ documentCount: entry.documentCount,
4658
+ partitionCount: entry.partitionCount,
4396
4659
  schema: flattenSchema(entry.config.schema),
4397
4660
  language: entry.language.name,
4398
4661
  k1: entry.config.bm25?.k1 ?? 1.2,
@@ -4411,8 +4674,399 @@ function createDurabilityFromTier(tier, wiring) {
4411
4674
  ...entry.config.vectorPromotion !== void 0 ? { vectorPromotion: entry.config.vectorPromotion } : {}
4412
4675
  };
4413
4676
  },
4414
- createIndexFromMetadata: wiring.createIndexFromMetadata,
4415
- onFatalError: wiring.emitFatalError
4677
+ createIndexFromMetadata: wiring.createIndexFromMetadata,
4678
+ recordCheckpoint: wiring.recordCheckpoint,
4679
+ onFatalError: wiring.emitFatalError
4680
+ });
4681
+ }
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
+ }
4416
5070
  });
4417
5071
  }
4418
5072
 
@@ -4441,7 +5095,7 @@ function createInvalidationIntegration(deps) {
4441
5095
  const reloadChains = /* @__PURE__ */ new Map();
4442
5096
  let broadcastTimer = null;
4443
5097
  let stopped = false;
4444
- function toError3(err) {
5098
+ function toError4(err) {
4445
5099
  return err instanceof Error ? err : new Error(String(err));
4446
5100
  }
4447
5101
  function pruneStaleForeignStats(entries) {
@@ -4454,7 +5108,7 @@ function createInvalidationIntegration(deps) {
4454
5108
  }
4455
5109
  function scheduleReload(indexName) {
4456
5110
  const chain = reloadChains.get(indexName) ?? Promise.resolve();
4457
- const next = chain.then(() => reloadIndex(indexName)).catch((err) => onError(toError3(err)));
5111
+ const next = chain.then(() => reloadIndex(indexName)).catch((err) => onError(toError4(err)));
4458
5112
  reloadChains.set(indexName, next);
4459
5113
  }
4460
5114
  function storeForeignStats(indexName, sourceInstanceId, stats) {
@@ -4480,7 +5134,7 @@ function createInvalidationIntegration(deps) {
4480
5134
  }
4481
5135
  storeForeignStats(event.indexName, event.instanceId, event.stats);
4482
5136
  } catch (err) {
4483
- onError(toError3(err));
5137
+ onError(toError4(err));
4484
5138
  }
4485
5139
  }
4486
5140
  async function publishOwnStatistics() {
@@ -4502,7 +5156,7 @@ function createInvalidationIntegration(deps) {
4502
5156
  }
4503
5157
  });
4504
5158
  } catch (err) {
4505
- onError(toError3(err));
5159
+ onError(toError4(err));
4506
5160
  }
4507
5161
  }
4508
5162
  }
@@ -4521,7 +5175,7 @@ function createInvalidationIntegration(deps) {
4521
5175
  sourceInstanceId: instanceId
4522
5176
  });
4523
5177
  } catch (err) {
4524
- onError(toError3(err));
5178
+ onError(toError4(err));
4525
5179
  }
4526
5180
  },
4527
5181
  broadcastStats(indexName) {
@@ -4566,7 +5220,7 @@ function createInvalidationIntegration(deps) {
4566
5220
  try {
4567
5221
  await adapter.shutdown();
4568
5222
  } catch (err) {
4569
- onError(toError3(err));
5223
+ onError(toError4(err));
4570
5224
  }
4571
5225
  }
4572
5226
  };
@@ -4608,7 +5262,7 @@ async function transferIndexToPool(indexName, pool, config, manager) {
4608
5262
  }
4609
5263
 
4610
5264
  // src/engine/orchestration/eligibility.ts
4611
- function toError2(value) {
5265
+ function toError3(value) {
4612
5266
  return value instanceof Error ? value : new Error(String(value));
4613
5267
  }
4614
5268
  function isDeterministicFailure(error) {
@@ -4946,14 +5600,34 @@ async function awaitCompactions(state) {
4946
5600
  var DEFAULT_BACKPRESSURE_LIMIT = 100;
4947
5601
  var DEFAULT_REQUEST_TIMEOUT = 3e4;
4948
5602
  var SHUTDOWN_TIMEOUT = 5e3;
5603
+ var CONSECUTIVE_TIMEOUTS_BEFORE_DEATH = 3;
5604
+ function errorFromEventLike(event) {
5605
+ if (event instanceof Error) {
5606
+ return event;
5607
+ }
5608
+ if (typeof event === "object" && event !== null) {
5609
+ const detail = event;
5610
+ if (detail.error instanceof Error) {
5611
+ return detail.error;
5612
+ }
5613
+ if (typeof detail.message === "string" && detail.message.length > 0) {
5614
+ return new Error(detail.message);
5615
+ }
5616
+ }
5617
+ return new Error("The worker reported an error event carrying no detail");
5618
+ }
4949
5619
  function createWorkerExecutor(worker, config) {
4950
- const backpressureLimit = DEFAULT_BACKPRESSURE_LIMIT;
4951
- const requestTimeout = DEFAULT_REQUEST_TIMEOUT;
5620
+ const backpressureLimit = config?.backpressureLimit ?? DEFAULT_BACKPRESSURE_LIMIT;
5621
+ const requestTimeout = config?.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
4952
5622
  const pending = /* @__PURE__ */ new Map();
5623
+ let deathError = null;
5624
+ let shutdownRequested = false;
5625
+ let consecutiveTimeouts = 0;
4953
5626
  function processResponse(msg) {
4954
5627
  if (!isValidWorkerResponse(msg)) {
4955
5628
  return;
4956
5629
  }
5630
+ consecutiveTimeouts = 0;
4957
5631
  const response = msg;
4958
5632
  const entry = pending.get(response.requestId);
4959
5633
  if (!entry) {
@@ -4967,15 +5641,36 @@ function createWorkerExecutor(worker, config) {
4967
5641
  entry.resolve(response.data);
4968
5642
  }
4969
5643
  }
5644
+ function handleDeath(cause) {
5645
+ if (deathError !== null) {
5646
+ return;
5647
+ }
5648
+ const message = cause instanceof Error ? cause.message : String(cause);
5649
+ deathError = new NarsilError(ErrorCodes.WORKER_CRASHED, `Worker died: ${message}`);
5650
+ for (const [id, entry] of pending) {
5651
+ clearTimeout(entry.timeoutId);
5652
+ entry.reject(deathError);
5653
+ pending.delete(id);
5654
+ }
5655
+ if (!shutdownRequested) {
5656
+ config?.onDeath?.(cause instanceof Error ? cause : new Error(message));
5657
+ }
5658
+ }
4970
5659
  if (typeof worker.on === "function") {
4971
5660
  worker.on("message", (msg) => processResponse(msg));
5661
+ worker.on("error", (cause) => handleDeath(cause));
5662
+ worker.on("exit", (code) => handleDeath(new Error(`Worker exited with code ${String(code)}`)));
4972
5663
  } else if (typeof worker.addEventListener === "function") {
4973
5664
  worker.addEventListener("message", (event) => {
4974
5665
  const msg = event.data;
4975
5666
  processResponse(msg);
4976
5667
  });
5668
+ worker.addEventListener("error", (event) => handleDeath(errorFromEventLike(event)));
4977
5669
  }
4978
5670
  function execute(action) {
5671
+ if (deathError !== null) {
5672
+ return Promise.reject(deathError);
5673
+ }
4979
5674
  if (pending.size >= backpressureLimit) {
4980
5675
  return Promise.reject(
4981
5676
  new NarsilError(ErrorCodes.WORKER_BUSY, `Backpressure limit of ${backpressureLimit} pending requests reached`)
@@ -4987,6 +5682,10 @@ function createWorkerExecutor(worker, config) {
4987
5682
  const timeoutId = setTimeout(() => {
4988
5683
  pending.delete(requestId);
4989
5684
  reject(new NarsilError(ErrorCodes.WORKER_TIMEOUT, `Request ${requestId} timed out after ${requestTimeout}ms`));
5685
+ consecutiveTimeouts += 1;
5686
+ if (consecutiveTimeouts >= CONSECUTIVE_TIMEOUTS_BEFORE_DEATH) {
5687
+ handleDeath(new Error(`no answer to ${CONSECUTIVE_TIMEOUTS_BEFORE_DEATH} consecutive requests`));
5688
+ }
4990
5689
  }, requestTimeout);
4991
5690
  pending.set(requestId, {
4992
5691
  resolve,
@@ -4997,6 +5696,10 @@ function createWorkerExecutor(worker, config) {
4997
5696
  });
4998
5697
  }
4999
5698
  async function shutdown() {
5699
+ shutdownRequested = true;
5700
+ if (deathError !== null) {
5701
+ return;
5702
+ }
5000
5703
  const requestId = createRequestId();
5001
5704
  const shutdownAction = { type: "shutdown", requestId };
5002
5705
  const shutdownPromise = new Promise((resolve, reject) => {
@@ -5038,15 +5741,15 @@ async function createWorkerFactory(entryPoint) {
5038
5741
  const resolvedEntry = resolveEntryPoint();
5039
5742
  if (runtime.supportsWorkerThreads) {
5040
5743
  const workerThreadsModule = await import('worker_threads');
5041
- return function nodeFactory(_workerId) {
5744
+ return function nodeFactory(_workerId, onDeath) {
5042
5745
  const instance = new workerThreadsModule.Worker(new URL(resolvedEntry));
5043
- return createWorkerExecutor(instance);
5746
+ return createWorkerExecutor(instance, { onDeath });
5044
5747
  };
5045
5748
  }
5046
5749
  if (runtime.supportsWebWorkers) {
5047
- return function webFactory(_workerId) {
5750
+ return function webFactory(_workerId, onDeath) {
5048
5751
  const instance = new Worker(resolvedEntry, { type: "module" });
5049
- return createWorkerExecutor(instance);
5752
+ return createWorkerExecutor(instance, { onDeath });
5050
5753
  };
5051
5754
  }
5052
5755
  throw new NarsilError(ErrorCodes.WORKER_CRASHED, `No worker support available on runtime "${runtime.runtime}"`);
@@ -5102,7 +5805,13 @@ async function runPromotion(state, reason) {
5102
5805
  return;
5103
5806
  }
5104
5807
  const factory = await createWorkerFactory();
5105
- const pool = createWorkerPool({ count: state.config?.workers?.count, workerFactory: factory });
5808
+ const pool = createWorkerPool({
5809
+ count: state.config?.workers?.count,
5810
+ workerFactory: factory,
5811
+ onWorkerCrash(workerId, indexNames, error) {
5812
+ state.callbacks?.onWorkerCrash?.(workerId, indexNames, error);
5813
+ }
5814
+ });
5106
5815
  for (const name of promotable) {
5107
5816
  pool.addIndexToAll(name);
5108
5817
  }
@@ -5139,7 +5848,7 @@ async function promoteBeforeBatch(state, indexName, incomingCount) {
5139
5848
  if (!result.shouldPromote) return;
5140
5849
  state.promotionInProgress = true;
5141
5850
  const run = runPromotion(state, result.reason).catch((err) => {
5142
- const error = toError2(err);
5851
+ const error = toError3(err);
5143
5852
  state.promotionBlocked = isDeterministicFailure(error);
5144
5853
  state.promotionInProgress = false;
5145
5854
  state.callbacks?.onPromotionFailure?.(result.reason, error, !state.promotionBlocked);
@@ -5160,7 +5869,7 @@ async function checkPromotion(state) {
5160
5869
  state.promotionInProgress = true;
5161
5870
  setTimeout(() => {
5162
5871
  const run = runPromotion(state, result.reason).catch((err) => {
5163
- const error = toError2(err);
5872
+ const error = toError3(err);
5164
5873
  state.promotionBlocked = isDeterministicFailure(error);
5165
5874
  state.promotionInProgress = false;
5166
5875
  state.callbacks?.onPromotionFailure?.(result.reason, error, !state.promotionBlocked);
@@ -5330,6 +6039,56 @@ function createWorkerOrchestrator(config, executor, promoter, indexRegistry, cal
5330
6039
  state.segmentLedger.clear();
5331
6040
  }
5332
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
+ }
5333
6092
  async function replicate(action) {
5334
6093
  await replicateToWorkers(state, action);
5335
6094
  if (action.type === "attachSegments") {
@@ -5342,6 +6101,9 @@ function createWorkerOrchestrator(config, executor, promoter, indexRegistry, cal
5342
6101
  replicateToWorkers: replicate,
5343
6102
  awaitReplication: (indexName) => awaitReplicationIdle(state, indexName),
5344
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),
5345
6107
  buildSegments: (requests) => buildSegments(state, requests),
5346
6108
  segmentBuildConcurrency: (indexName) => segmentBuildConcurrency(state, indexName),
5347
6109
  searchViaWorker: (indexName, params, globalStats, partitionIds) => searchViaWorker(state, indexName, params, globalStats, partitionIds),
@@ -5353,6 +6115,11 @@ function createWorkerOrchestrator(config, executor, promoter, indexRegistry, cal
5353
6115
  };
5354
6116
  }
5355
6117
 
6118
+ // src/engine/vector-fields.ts
6119
+ function getVectorFieldPaths(schema) {
6120
+ return new Set(extractVectorFieldsFromSchema(schema).keys());
6121
+ }
6122
+
5356
6123
  // src/engine/watermark.ts
5357
6124
  function createWatermarkNotifier(deps) {
5358
6125
  const latchedCapacity = /* @__PURE__ */ new Map();
@@ -5388,10 +6155,7 @@ function createWatermarkNotifier(deps) {
5388
6155
  }
5389
6156
 
5390
6157
  // src/engine/core.ts
5391
- function getVectorFieldPaths(schema) {
5392
- return new Set(extractVectorFieldsFromSchema(schema).keys());
5393
- }
5394
- function createEngineCore(config) {
6158
+ function createEngineCore(config, hooks) {
5395
6159
  const executor = createDirectExecutor();
5396
6160
  const promoter = createExecutionPromoter({
5397
6161
  perIndexThreshold: config?.workers?.promotionThreshold,
@@ -5423,6 +6187,9 @@ function createEngineCore(config) {
5423
6187
  if (handlers) {
5424
6188
  for (const handler of handlers) handler({ workerCount, reason });
5425
6189
  }
6190
+ void Promise.resolve(pluginRegistry.runHook("onWorkerPromote", { workerCount, reason })).catch((err) => {
6191
+ console.warn("onWorkerPromote plugin hook failed:", err instanceof Error ? err.message : String(err));
6192
+ });
5426
6193
  },
5427
6194
  onPromotionFailure(reason, error, retryable) {
5428
6195
  const handlers = eventHandlers.get("workerPromoteFailure");
@@ -5431,6 +6198,14 @@ function createEngineCore(config) {
5431
6198
  return;
5432
6199
  }
5433
6200
  for (const handler of handlers) handler({ reason, error, retryable });
6201
+ },
6202
+ onWorkerCrash(workerId, indexNames, error) {
6203
+ const handlers = eventHandlers.get("workerCrash");
6204
+ if (!handlers || handlers.size === 0) {
6205
+ console.warn(`Worker ${workerId} crashed:`, error);
6206
+ return;
6207
+ }
6208
+ for (const handler of handlers) handler({ workerId, indexNames, error });
5434
6209
  }
5435
6210
  });
5436
6211
  const rebalancer = createRebalancer();
@@ -5464,8 +6239,12 @@ function createEngineCore(config) {
5464
6239
  }
5465
6240
  return manager;
5466
6241
  }
5467
- async function createIndexFromMetadata(metadata) {
5468
- 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
+ }
5469
6248
  return;
5470
6249
  }
5471
6250
  const indexConfig = reconstructSchemaFromMetadata(metadata);
@@ -5494,7 +6273,7 @@ function createEngineCore(config) {
5494
6273
  }
5495
6274
  }
5496
6275
  try {
5497
- executor.createIndex(metadata.indexName, indexConfig, language);
6276
+ if (loadData) executor.createIndex(metadata.indexName, indexConfig, language);
5498
6277
  } catch (err) {
5499
6278
  if (err instanceof NarsilError && err.code === ErrorCodes.CONFIG_INVALID) {
5500
6279
  throw new NarsilError(err.code, `Recovery of index "${metadata.indexName}" failed: ${err.message}`, {
@@ -5512,8 +6291,12 @@ function createEngineCore(config) {
5512
6291
  embeddingAdapterName: adapterName,
5513
6292
  vectorFieldPaths: getVectorFieldPaths(indexConfig.schema),
5514
6293
  indexUuid: metadata.indexUuid ?? null,
5515
- heldPartitions: metadata.heldPartitions ?? null
6294
+ heldPartitions: metadata.heldPartitions ?? null,
6295
+ documentCount: metadata.documentCount ?? 0,
6296
+ partitionCount: metadata.partitionCount
5516
6297
  });
6298
+ if (loadData) await indexState.registerOpen(metadata.indexName);
6299
+ else indexState.registerClosed(metadata.indexName);
5517
6300
  if (metadata.analysisRevision !== language.revision) {
5518
6301
  analysisRebuild.markStale({
5519
6302
  indexName: metadata.indexName,
@@ -5524,9 +6307,12 @@ function createEngineCore(config) {
5524
6307
  }
5525
6308
  }
5526
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
+ }
5527
6313
  let invalidation = null;
5528
6314
  const durability = createDurabilityFromTier(durabilityTier, {
5529
- requireManager,
6315
+ getManager: (indexName) => executor.getManager(indexName),
5530
6316
  indexRegistry,
5531
6317
  createIndexFromMetadata,
5532
6318
  emitFatalError(error) {
@@ -5535,7 +6321,13 @@ function createEngineCore(config) {
5535
6321
  for (const handler of handlers) handler({ error });
5536
6322
  }
5537
6323
  },
5538
- 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
+ }
5539
6331
  });
5540
6332
  invalidation = createInvalidationFromConfig(config, durabilityTier?.kind ?? null, {
5541
6333
  getManager: (indexName) => executor.getManager(indexName) ?? void 0,
@@ -5581,6 +6373,18 @@ function createEngineCore(config) {
5581
6373
  resyncIndex: (indexName, wasPromoted) => orchestrator.resyncIndex(indexName, wasPromoted),
5582
6374
  durabilityManager: durability?.manager ?? null
5583
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
+ });
5584
6388
  const mutationCtx = {
5585
6389
  executor,
5586
6390
  pluginRegistry,
@@ -5634,6 +6438,7 @@ function createEngineCore(config) {
5634
6438
  bufferIfRebalancing,
5635
6439
  watermarkNotifier,
5636
6440
  analysisRebuild,
6441
+ indexState,
5637
6442
  mutationCtx,
5638
6443
  rebalanceCtx
5639
6444
  };
@@ -5792,8 +6597,11 @@ async function createEngineIndex(core, config, name, indexConfig, indexUuid) {
5792
6597
  embeddingAdapterName,
5793
6598
  vectorFieldPaths: getVectorFieldPaths(indexConfig.schema),
5794
6599
  indexUuid: indexUuid ?? null,
5795
- heldPartitions: null
6600
+ heldPartitions: null,
6601
+ documentCount: 0,
6602
+ partitionCount: indexConfig.partitions?.maxPartitions ?? 1
5796
6603
  });
6604
+ await core.indexState.registerOpen(name);
5797
6605
  if (core.durability) {
5798
6606
  await core.durability.manager.persistMetadata(name);
5799
6607
  }
@@ -5802,13 +6610,21 @@ async function createEngineIndex(core, config, name, indexConfig, indexUuid) {
5802
6610
  async function dropEngineIndex(core, name) {
5803
6611
  core.guardShutdown();
5804
6612
  const entry = core.requireIndex(name);
5805
- core.executor.dropIndex(name);
5806
- core.indexRegistry.delete(name);
5807
- core.watermarkNotifier.forget(name);
5808
- core.analysisRebuild.clearStale(name);
5809
- if (core.durability) {
5810
- await core.durability.manager.removeIndex(name);
5811
- }
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
+ });
5812
6628
  await core.pluginRegistry.runHook("onIndexDrop", { indexName: name, config: entry.config });
5813
6629
  }
5814
6630
  function registerEngineEmbeddingAdapter(core, name, adapter) {
@@ -5843,6 +6659,7 @@ function registerEngineEmbeddingAdapter(core, name, adapter) {
5843
6659
  // src/engine/lifecycle.ts
5844
6660
  async function shutdownEngine(core) {
5845
6661
  const { executor, durability, indexRegistry, eventHandlers, orchestrator } = core;
6662
+ core.indexState.dispose();
5846
6663
  for (const [name] of indexRegistry) {
5847
6664
  const manager = executor.getManager(name);
5848
6665
  if (manager) {
@@ -7389,6 +8206,15 @@ async function executeRebalance(manager, indexName, targetPartitionCount, ctx) {
7389
8206
  }
7390
8207
  }
7391
8208
  });
8209
+ try {
8210
+ await ctx.pluginRegistry.runHook("onPartitionSplit", {
8211
+ indexName,
8212
+ oldPartitionCount: oldCount,
8213
+ newPartitionCount: targetPartitionCount
8214
+ });
8215
+ } catch (hookErr) {
8216
+ console.warn("onPartitionSplit plugin hook failed:", hookErr instanceof Error ? hookErr.message : String(hookErr));
8217
+ }
7392
8218
  await replayQueued(manager, indexName, waq, ctx);
7393
8219
  if (ctx.durabilityManager) {
7394
8220
  await ctx.durabilityManager.persistMetadata(indexName);
@@ -7603,7 +8429,7 @@ async function createSnapshot(manager, entry) {
7603
8429
  const config = entry.config;
7604
8430
  const bm25 = config.bm25;
7605
8431
  const { encode: encode9 } = await import('@msgpack/msgpack');
7606
- return encode9({
8432
+ const payload = encode9({
7607
8433
  version: 2,
7608
8434
  schema: config.schema,
7609
8435
  language: entry.language.name,
@@ -7633,15 +8459,17 @@ async function createSnapshot(manager, entry) {
7633
8459
  partitions: partitionBuffers,
7634
8460
  vectorIndexes: vectorPayloads
7635
8461
  });
8462
+ return packIndexSnapshotEnvelope(payload);
7636
8463
  }
7637
8464
  async function restoreFromSnapshot(indexName, data, deps) {
7638
8465
  if (!(data instanceof Uint8Array)) {
7639
8466
  throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Snapshot data must be a Uint8Array");
7640
8467
  }
8468
+ const payloadBytes = hasNrslMagic(data) ? await unpackIndexSnapshotEnvelope(data) : data;
7641
8469
  const { decode: decode9 } = await import('@msgpack/msgpack');
7642
8470
  let decoded;
7643
8471
  try {
7644
- decoded = decode9(data);
8472
+ decoded = decode9(payloadBytes);
7645
8473
  } catch (err) {
7646
8474
  throw new NarsilError(
7647
8475
  ErrorCodes.DOC_VALIDATION_FAILED,
@@ -7723,8 +8551,12 @@ async function restoreFromSnapshot(indexName, data, deps) {
7723
8551
  embeddingAdapterName: adapterName,
7724
8552
  vectorFieldPaths,
7725
8553
  indexUuid: null,
7726
- heldPartitions: null
8554
+ heldPartitions: null,
8555
+ documentCount: 0,
8556
+ partitionCount: envelope.partitions.length
7727
8557
  });
8558
+ await deps.indexState.registerOpen(indexName);
8559
+ const release = await deps.indexState.acquire(indexName, false);
7728
8560
  try {
7729
8561
  const manager = deps.requireManager(indexName);
7730
8562
  while (manager.partitionCount < envelope.partitions.length) {
@@ -7744,26 +8576,36 @@ async function restoreFromSnapshot(indexName, data, deps) {
7744
8576
  }
7745
8577
  }
7746
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
+ }
7747
8596
  } catch (err) {
7748
8597
  try {
7749
8598
  deps.executor.dropIndex(indexName);
7750
- deps.indexRegistry.delete(indexName);
7751
8599
  } catch (_) {
7752
8600
  }
7753
- throw err;
7754
- }
7755
- if (deps.durability) {
7756
- const manager = deps.requireManager(indexName);
7757
- for (let partitionId = 0; partitionId < manager.partitionCount; partitionId++) {
7758
- for (const docId of manager.getPartition(partitionId).docIds()) {
7759
- const document = manager.get(docId);
7760
- if (document !== void 0) {
7761
- await deps.durability.recordInsertOrUpdate(indexName, docId, document, noopApply2);
7762
- }
7763
- }
8601
+ deps.indexRegistry.delete(indexName);
8602
+ deps.indexState.forget(indexName);
8603
+ if (deps.durability) {
8604
+ await deps.durability.manager.removeIndex(indexName).catch(() => void 0);
7764
8605
  }
7765
- await deps.durability.manager.persistMetadata(indexName);
7766
- await deps.durability.manager.checkpoint(indexName);
8606
+ throw err;
8607
+ } finally {
8608
+ release();
7767
8609
  }
7768
8610
  }
7769
8611
  function noopApply2() {
@@ -7836,6 +8678,137 @@ function readProcessMemory() {
7836
8678
  }
7837
8679
  }
7838
8680
 
8681
+ // src/search/cursor-binding.ts
8682
+ var ABSENT_TAG = 0;
8683
+ var NULL_TAG = 1;
8684
+ var FALSE_TAG = 2;
8685
+ var TRUE_TAG = 3;
8686
+ var NUMBER_TAG = 4;
8687
+ var STRING_TAG = 5;
8688
+ var LIST_TAG = 6;
8689
+ var MAP_TAG = 7;
8690
+ var encoder = new TextEncoder();
8691
+ function ensureCapacity(stream, extra) {
8692
+ if (stream.length + extra <= stream.bytes.length) return;
8693
+ const grown = new Uint8Array(Math.max(stream.bytes.length * 2, stream.length + extra));
8694
+ grown.set(stream.bytes.subarray(0, stream.length));
8695
+ stream.bytes = grown;
8696
+ }
8697
+ function writeByte(stream, value) {
8698
+ ensureCapacity(stream, 1);
8699
+ stream.bytes[stream.length] = value;
8700
+ stream.length += 1;
8701
+ }
8702
+ function writeUint32(stream, value) {
8703
+ ensureCapacity(stream, 4);
8704
+ stream.bytes[stream.length] = value >>> 24 & 255;
8705
+ stream.bytes[stream.length + 1] = value >>> 16 & 255;
8706
+ stream.bytes[stream.length + 2] = value >>> 8 & 255;
8707
+ stream.bytes[stream.length + 3] = value & 255;
8708
+ stream.length += 4;
8709
+ }
8710
+ var numberBytes = new DataView(new ArrayBuffer(8));
8711
+ function writeNumber(stream, value) {
8712
+ numberBytes.setFloat64(0, value, false);
8713
+ ensureCapacity(stream, 8);
8714
+ for (let i = 0; i < 8; i++) {
8715
+ stream.bytes[stream.length + i] = numberBytes.getUint8(i);
8716
+ }
8717
+ stream.length += 8;
8718
+ }
8719
+ function writeString(stream, value) {
8720
+ const encoded = encoder.encode(value);
8721
+ writeUint32(stream, encoded.length);
8722
+ ensureCapacity(stream, encoded.length);
8723
+ stream.bytes.set(encoded, stream.length);
8724
+ stream.length += encoded.length;
8725
+ }
8726
+ function writeValue(stream, value) {
8727
+ if (value === void 0) {
8728
+ writeByte(stream, ABSENT_TAG);
8729
+ return;
8730
+ }
8731
+ if (value === null) {
8732
+ writeByte(stream, NULL_TAG);
8733
+ return;
8734
+ }
8735
+ if (typeof value === "boolean") {
8736
+ writeByte(stream, value ? TRUE_TAG : FALSE_TAG);
8737
+ return;
8738
+ }
8739
+ if (typeof value === "number") {
8740
+ writeByte(stream, NUMBER_TAG);
8741
+ writeNumber(stream, value);
8742
+ return;
8743
+ }
8744
+ if (typeof value === "string") {
8745
+ writeByte(stream, STRING_TAG);
8746
+ writeString(stream, value);
8747
+ return;
8748
+ }
8749
+ if (Array.isArray(value)) {
8750
+ writeByte(stream, LIST_TAG);
8751
+ writeUint32(stream, value.length);
8752
+ for (const member of value) {
8753
+ writeValue(stream, member ?? null);
8754
+ }
8755
+ return;
8756
+ }
8757
+ const record = value;
8758
+ const keys = Object.keys(record).sort(compareCodePoints);
8759
+ writeByte(stream, MAP_TAG);
8760
+ writeUint32(stream, keys.length);
8761
+ for (const key of keys) {
8762
+ writeByte(stream, STRING_TAG);
8763
+ writeString(stream, key);
8764
+ writeValue(stream, record[key] ?? null);
8765
+ }
8766
+ }
8767
+ function boundVectorOf(vector) {
8768
+ if (vector === void 0) return void 0;
8769
+ const bound = { ...vector };
8770
+ if (vector.value !== void 0) {
8771
+ bound.value = Array.from(vector.value, Math.fround);
8772
+ }
8773
+ if (vector.text !== void 0) {
8774
+ delete bound.value;
8775
+ }
8776
+ return bound;
8777
+ }
8778
+ var FILTERS_SLOT = 2;
8779
+ var BOUND_SLOT_COUNT = 15;
8780
+ function bindingOf(values) {
8781
+ const stream = { bytes: new Uint8Array(256), length: 0 };
8782
+ for (const value of values) {
8783
+ writeValue(stream, value);
8784
+ }
8785
+ return fnv1aBytes(stream.bytes.subarray(0, stream.length)).toString(16).padStart(8, "0");
8786
+ }
8787
+ function queryBindingOf(params) {
8788
+ return bindingOf([
8789
+ params.term,
8790
+ params.fields,
8791
+ params.filters,
8792
+ params.boost,
8793
+ params.minScore,
8794
+ params.termMatch,
8795
+ params.tolerance,
8796
+ params.prefixLength,
8797
+ params.prefix,
8798
+ params.exact,
8799
+ params.pinned,
8800
+ params.mode,
8801
+ params.hybrid,
8802
+ boundVectorOf(params.vector),
8803
+ params.scoring ?? "local"
8804
+ ]);
8805
+ }
8806
+ function listBindingOf(filters) {
8807
+ const values = new Array(BOUND_SLOT_COUNT).fill(void 0);
8808
+ values[FILTERS_SLOT] = filters;
8809
+ return bindingOf(values);
8810
+ }
8811
+
7839
8812
  // src/engine/list-documents/index.ts
7840
8813
  function clampListLimit(limit) {
7841
8814
  const clamped = Math.max(1, clampLimit(limit));
@@ -7870,7 +8843,7 @@ function collectCandidates(partition, anchor, matches, wanted, out) {
7870
8843
  taken++;
7871
8844
  }
7872
8845
  }
7873
- function pageInIdOrder(partitions, cursor, filtered, limit) {
8846
+ function pageInIdOrder(partitions, cursor, filtered, limit, binding) {
7874
8847
  const anchor = cursor === null ? null : cursor.anchor;
7875
8848
  const candidates = [];
7876
8849
  for (let index = 0; index < partitions.length; index++) {
@@ -7885,10 +8858,10 @@ function pageInIdOrder(partitions, cursor, filtered, limit) {
7885
8858
  candidates.sort(compareCodePoints);
7886
8859
  const hasMore = candidates.length > limit;
7887
8860
  const ids = hasMore ? candidates.slice(0, limit) : candidates;
7888
- const nextCursor = hasMore && ids.length > 0 ? { anchor: ids[ids.length - 1], score: null, sortKey: null, sortSignature: null } : null;
8861
+ const nextCursor = hasMore && ids.length > 0 ? { anchor: ids[ids.length - 1], score: null, sortKey: null, sortSignature: null, binding } : null;
7889
8862
  return { ids, nextCursor };
7890
8863
  }
7891
- function pageInSortOrder(partitions, cursor, filtered, limit, sort, signature, schema) {
8864
+ function pageInSortOrder(partitions, cursor, filtered, limit, sort, signature, schema, binding) {
7892
8865
  const normalized = normalizeSort(sort);
7893
8866
  const fields = normalized.map((entry) => entry.field);
7894
8867
  const directions = normalized.map((entry) => entry.direction);
@@ -7911,7 +8884,7 @@ function pageInSortOrder(partitions, cursor, filtered, limit, sort, signature, s
7911
8884
  const hasMore = merged.length > limit;
7912
8885
  const page = hasMore ? merged.slice(0, limit) : merged;
7913
8886
  const last = page[page.length - 1];
7914
- const nextCursor = hasMore && last !== void 0 ? { anchor: last.id, score: null, sortKey: last.key, sortSignature: signature } : null;
8887
+ const nextCursor = hasMore && last !== void 0 ? { anchor: last.id, score: null, sortKey: last.key, sortSignature: signature, binding } : null;
7915
8888
  return { ids: page.map((entry) => entry.id), nextCursor };
7916
8889
  }
7917
8890
  function countAcrossPartitions(manager, partitionIds, partitions) {
@@ -7930,15 +8903,16 @@ function executeListDocuments(params, context) {
7930
8903
  const limit = clampListLimit(params.limit);
7931
8904
  const signature = sortSignatureOf(params.sort);
7932
8905
  requireSortableFields(params.sort, schema);
8906
+ const binding = listBindingOf(params.filters);
7933
8907
  let cursor = null;
7934
8908
  if (params.cursor !== void 0) {
7935
8909
  cursor = decodePageCursor(params.cursor);
7936
- requireMatchingCursor(cursor, params.cursor, signature, false);
8910
+ requireMatchingCursor(cursor, params.cursor, signature, false, binding);
7937
8911
  }
7938
8912
  const partitions = context.partitionIds === void 0 ? manager.getAllPartitions() : context.partitionIds.map((partitionId) => manager.partitionAt(partitionId)).filter((partition) => partition !== void 0);
7939
8913
  const filtered = params.filters === void 0 ? null : collectFilterMatches(partitions, params.filters, schema);
7940
8914
  const total = filtered === null ? countAcrossPartitions(manager, context.partitionIds, partitions) : filtered.total;
7941
- const page = params.sort === void 0 || signature === null ? pageInIdOrder(partitions, cursor, filtered, limit) : pageInSortOrder(partitions, cursor, filtered, limit, params.sort, signature, schema);
8915
+ const page = params.sort === void 0 || signature === null ? pageInIdOrder(partitions, cursor, filtered, limit, binding) : pageInSortOrder(partitions, cursor, filtered, limit, params.sort, signature, schema, binding);
7942
8916
  const projection = resolveProjection(params.document);
7943
8917
  const documents = [];
7944
8918
  for (const docId of page.ids) {
@@ -8006,18 +8980,14 @@ function applyGrouping(hits, group, getDocument) {
8006
8980
  }
8007
8981
  const result = { values: entry.values, hits: groupHits };
8008
8982
  if (group.reduce) {
8009
- try {
8010
- let accumulator = group.reduce.initialValue();
8011
- for (const hit of groupHits) {
8012
- const doc = getDocument(hit.id);
8013
- if (doc) {
8014
- accumulator = group.reduce.reducer(accumulator, doc, hit.score ?? 0);
8015
- }
8016
- }
8017
- ;
8018
- result.reduced = accumulator;
8019
- } catch (err) {
8020
- result.reducerError = err instanceof Error ? err.message : String(err);
8983
+ const folded = foldGroupReducer(
8984
+ group.reduce,
8985
+ groupHits.map((hit) => ({ document: getDocument(hit.id), score: hit.score ?? 0 }))
8986
+ );
8987
+ if (folded.reducerError !== void 0) {
8988
+ result.reducerError = folded.reducerError;
8989
+ } else {
8990
+ result.reduced = folded.reduced;
8021
8991
  }
8022
8992
  }
8023
8993
  groups.push(result);
@@ -8030,19 +9000,49 @@ function applyGrouping(hits, group, getDocument) {
8030
9000
  const bId = b.hits.length > 0 ? b.hits[0].id : "";
8031
9001
  return compareCodePoints(aId, bId);
8032
9002
  });
9003
+ if (group.limit !== void 0 && Number.isFinite(group.limit) && group.limit >= 0) {
9004
+ const cap = Math.floor(group.limit);
9005
+ if (groups.length > cap) {
9006
+ groups.length = cap;
9007
+ }
9008
+ }
8033
9009
  return groups;
8034
9010
  }
9011
+ function foldGroupReducer(reduce, rows) {
9012
+ try {
9013
+ let accumulator = reduce.initialValue();
9014
+ for (const row of rows) {
9015
+ if (row.document !== void 0) {
9016
+ accumulator = reduce.reducer(accumulator, row.document, row.score);
9017
+ }
9018
+ }
9019
+ return { reduced: accumulator };
9020
+ } catch (err) {
9021
+ return { reducerError: err instanceof Error ? err.message : String(err) };
9022
+ }
9023
+ }
8035
9024
 
8036
9025
  // src/search/pinning.ts
9026
+ function dedupePinnedEntries(pinned) {
9027
+ const deduped = [];
9028
+ const seen = /* @__PURE__ */ new Set();
9029
+ for (const entry of pinned) {
9030
+ if (seen.has(entry.docId)) continue;
9031
+ seen.add(entry.docId);
9032
+ deduped.push(entry);
9033
+ }
9034
+ return deduped;
9035
+ }
8037
9036
  function applyPinning(hits, pinned, resolveDoc) {
8038
9037
  const result = hits.slice();
8039
- const pinnedDocIds = new Set(pinned.map((p) => p.docId));
9038
+ const deduped = dedupePinnedEntries(pinned);
9039
+ const pinnedDocIds = new Set(deduped.map((entry) => entry.docId));
8040
9040
  for (let i = result.length - 1; i >= 0; i--) {
8041
9041
  if (pinnedDocIds.has(result[i].id)) {
8042
9042
  result.splice(i, 1);
8043
9043
  }
8044
9044
  }
8045
- const sorted = pinned.slice().sort((a, b) => a.position - b.position);
9045
+ const sorted = deduped.sort((a, b) => a.position - b.position);
8046
9046
  for (const entry of sorted) {
8047
9047
  const doc = resolveDoc(entry.docId);
8048
9048
  if (!doc) continue;
@@ -8236,17 +9236,7 @@ function applyHighlights(hits, params, language, analysis) {
8236
9236
 
8237
9237
  // src/engine/query/shared.ts
8238
9238
  function partitionsFor(manager, partitionIds) {
8239
- if (partitionIds === void 0) {
8240
- return manager.getAllPartitions();
8241
- }
8242
- const partitions = [];
8243
- for (const partitionId of partitionIds) {
8244
- const partition = manager.partitionAt(partitionId);
8245
- if (partition !== void 0) {
8246
- partitions.push(partition);
8247
- }
8248
- }
8249
- return partitions;
9239
+ return partitionsIn(manager, partitionIds);
8250
9240
  }
8251
9241
  function coverageFor(manager, partitionIds) {
8252
9242
  if (partitionIds === void 0) {
@@ -8340,7 +9330,7 @@ function executeSortedQueryPage(params, context, limit, offset, sortSignature) {
8340
9330
  let anchorId = null;
8341
9331
  if (params.searchAfter !== void 0) {
8342
9332
  const decoded = decodePageCursor(params.searchAfter);
8343
- requireMatchingCursor(decoded, params.searchAfter, sortSignature, true);
9333
+ requireMatchingCursor(decoded, params.searchAfter, sortSignature, true, context.cursorBinding);
8344
9334
  anchorKey = decoded.sortKey;
8345
9335
  anchorId = decoded.anchor;
8346
9336
  }
@@ -8385,7 +9375,13 @@ function executeSortedQueryPage(params, context, limit, offset, sortSignature) {
8385
9375
  const last = page[page.length - 1];
8386
9376
  let cursor;
8387
9377
  if (hasMore && last !== void 0) {
8388
- cursor = encodePageCursor({ anchor: last.id, score: null, sortKey: last.key, sortSignature });
9378
+ cursor = encodePageCursor({
9379
+ anchor: last.id,
9380
+ score: null,
9381
+ sortKey: last.key,
9382
+ sortSignature,
9383
+ binding: context.cursorBinding
9384
+ });
8389
9385
  }
8390
9386
  return {
8391
9387
  hits: page.map((entry) => ({ id: entry.id, document: void 0 })),
@@ -8671,7 +9667,7 @@ async function executeQuery(params, context) {
8671
9667
  if (params.group) {
8672
9668
  groups = applyGrouping(hits, params.group, (docId) => manager.getRef(docId));
8673
9669
  }
8674
- if (params.pinned) {
9670
+ if (params.pinned && params.searchAfter === void 0 && context.partitionIds === void 0) {
8675
9671
  hits = applyPinning(hits, params.pinned, (docId) => {
8676
9672
  const doc = manager.getRef(docId);
8677
9673
  if (!doc) return void 0;
@@ -8686,7 +9682,16 @@ async function executeQuery(params, context) {
8686
9682
  sortKeyOf
8687
9683
  };
8688
9684
  }
8689
- const paged = applyPagination(hits, limit, offset, params.searchAfter, sortContext);
9685
+ const pinnedIds = params.pinned !== void 0 && params.searchAfter === void 0 ? new Set(params.pinned.map((entry) => entry.docId)) : void 0;
9686
+ const paged = applyPagination(
9687
+ hits,
9688
+ limit,
9689
+ offset,
9690
+ context.cursorBinding,
9691
+ params.searchAfter,
9692
+ sortContext,
9693
+ pinnedIds
9694
+ );
8690
9695
  paginated = paged.paginated;
8691
9696
  nextCursor = paged.nextCursor;
8692
9697
  count = fanOutResult.totalMatched;
@@ -8755,6 +9760,11 @@ async function executePreflight(params, context) {
8755
9760
  } else if (isHybridMode && hasGlobalVectorIndex) {
8756
9761
  const result = await executeHybridSearch(params, context, preflightLimit, preflightOffset);
8757
9762
  totalMatched = result.totalMatched;
9763
+ } else if (countsWithoutScores(params)) {
9764
+ totalMatched = fanOutMatchCount(manager, params, language, config.schema, {
9765
+ searchOptions: searchOptionsFor(manager),
9766
+ partitionIds: context.partitionIds
9767
+ });
8758
9768
  } else {
8759
9769
  const scoring = scoringConfigFor(params, context);
8760
9770
  const workerResult = workerSearch ? await workerSearch(indexName, params, broadcastStatsForWorker(params, context, scoring), context.partitionIds) : null;
@@ -8891,105 +9901,132 @@ function broadcastStatsFor(core, options) {
8891
9901
  }
8892
9902
  async function runEngineQuery(core, indexName, params, options) {
8893
9903
  core.guardShutdown();
8894
- const entry = core.requireIndex(indexName);
8895
- const manager = core.requireManager(indexName);
8896
- const resolvedParams = await resolveVectorText(
8897
- scopedParams(params, options),
8898
- entry.embeddingAdapter,
8899
- core.abortController.signal,
8900
- entry.embeddingAdapterName
8901
- );
8902
- await core.pluginRegistry.runHook("beforeSearch", { indexName, params: resolvedParams });
8903
- const workerSearch = core.orchestrator.isPromoted() ? core.orchestrator.searchViaWorker.bind(core.orchestrator) : void 0;
8904
- const result = await executeQuery(resolvedParams, {
8905
- manager,
8906
- language: entry.language,
8907
- config: entry.config,
8908
- workerSearch,
8909
- indexName,
8910
- broadcastStats: broadcastStatsFor(core, options),
8911
- partitionIds: options?.partitionIds
8912
- });
9904
+ const release = await core.indexState.acquire(indexName);
8913
9905
  try {
8914
- 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,
8915
9921
  indexName,
8916
- params: resolvedParams,
8917
- results: result
9922
+ broadcastStats: broadcastStatsFor(core, options),
9923
+ partitionIds: options?.partitionIds,
9924
+ cursorBinding: queryBindingOf(params)
8918
9925
  });
8919
- } catch (err) {
8920
- console.warn("afterSearch plugin hook error:", err);
8921
- }
8922
- if (core.analysisRebuild.isStale(indexName)) {
8923
- 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();
8924
9941
  }
8925
- return result;
8926
9942
  }
8927
9943
  async function runEnginePreflight(core, indexName, params, options) {
8928
9944
  core.guardShutdown();
8929
- const entry = core.requireIndex(indexName);
8930
- const manager = core.requireManager(indexName);
8931
- const resolvedParams = await resolveVectorText(
8932
- scopedParams(params, options),
8933
- entry.embeddingAdapter,
8934
- core.abortController.signal,
8935
- entry.embeddingAdapterName
8936
- );
8937
- const workerSearch = core.orchestrator.isPromoted() ? core.orchestrator.searchViaWorker.bind(core.orchestrator) : void 0;
8938
- const result = await executePreflight(resolvedParams, {
8939
- manager,
8940
- language: entry.language,
8941
- config: entry.config,
8942
- workerSearch,
8943
- indexName,
8944
- broadcastStats: broadcastStatsFor(core, options),
8945
- partitionIds: options?.partitionIds
8946
- });
8947
- if (core.analysisRebuild.isStale(indexName)) {
8948
- 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();
8949
9972
  }
8950
- return result;
8951
9973
  }
8952
9974
  async function runEngineSuggest(core, indexName, params, partitionIds) {
8953
9975
  core.guardShutdown();
8954
- const result = executeSuggest(
8955
- core.requireManager(indexName),
8956
- core.requireIndex(indexName).language,
8957
- params,
8958
- partitionIds
8959
- );
8960
- if (core.analysisRebuild.isStale(indexName)) {
8961
- 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();
8962
9990
  }
8963
- return result;
8964
9991
  }
8965
- function runEngineQueryStats(core, indexName, terms, partitionIds) {
9992
+ async function runEngineQueryStats(core, indexName, terms, partitionIds) {
8966
9993
  core.guardShutdown();
8967
- const entry = core.requireIndex(indexName);
8968
- const manager = core.requireManager(indexName);
8969
- const stats = collectQueryTermStats(
8970
- manager,
8971
- terms.join(" "),
8972
- entry.language,
8973
- {
8974
- stopWords: manager.analysis.stopWords,
8975
- customTokenizer: manager.analysis.customTokenizer
8976
- },
8977
- partitionIds
8978
- );
8979
- return {
8980
- totalDocuments: stats.totalDocuments,
8981
- docFrequencies: stats.docFrequencies,
8982
- totalFieldLengths: stats.totalFieldLengths
8983
- };
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
+ }
8984
10016
  }
8985
10017
  async function runEngineListDocuments(core, indexName, params, partitionIds) {
8986
10018
  core.guardShutdown();
8987
- const entry = core.requireIndex(indexName);
8988
- return executeListDocuments(params, {
8989
- manager: core.requireManager(indexName),
8990
- schema: entry.config.schema,
8991
- partitionIds
8992
- });
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
+ }
8993
10030
  }
8994
10031
 
8995
10032
  // src/narsil/operations.ts
@@ -9009,6 +10046,16 @@ function createNarsilFromCore(core, config) {
9009
10046
  mutationCtx,
9010
10047
  rebalanceCtx
9011
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
+ }
9012
10059
  const narsil = {
9013
10060
  createIndex(name, indexConfig) {
9014
10061
  return createEngineIndex(core, config, name, indexConfig);
@@ -9019,15 +10066,27 @@ function createNarsilFromCore(core, config) {
9019
10066
  dropIndex(name) {
9020
10067
  return dropEngineIndex(core, name);
9021
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
+ },
9022
10079
  listIndexes() {
9023
10080
  const infos = [];
9024
10081
  for (const [name, entry] of indexRegistry) {
9025
10082
  const manager = executor.getManager(name);
9026
10083
  infos.push({
9027
10084
  name,
9028
- documentCount: manager?.countDocuments() ?? 0,
9029
- partitionCount: manager?.partitionCount ?? 0,
10085
+ documentCount: manager?.countDocuments() ?? entry.documentCount,
10086
+ partitionCount: manager?.partitionCount ?? entry.partitionCount,
9030
10087
  language: entry.language.name,
10088
+ state: core.indexState.stateOf(name),
10089
+ reopenCount: core.indexState.reopenCount(name),
9031
10090
  ...core.analysisRebuild.isStale(name) ? { analysisStale: true } : {}
9032
10091
  });
9033
10092
  }
@@ -9038,8 +10097,8 @@ function createNarsilFromCore(core, config) {
9038
10097
  const entry = requireIndex(indexName);
9039
10098
  const manager = executor.getManager(indexName);
9040
10099
  return {
9041
- documentCount: manager?.countDocuments() ?? 0,
9042
- partitionCount: manager?.partitionCount ?? 0,
10100
+ documentCount: manager?.countDocuments() ?? entry.documentCount,
10101
+ partitionCount: manager?.partitionCount ?? entry.partitionCount,
9043
10102
  estimatedMemoryBytes: manager?.estimateMemoryBytes() ?? 0,
9044
10103
  language: entry.language.name,
9045
10104
  schema: entry.config.schema
@@ -9051,47 +10110,48 @@ function createNarsilFromCore(core, config) {
9051
10110
  return executor.getManager(indexName)?.getPartitionStats() ?? [];
9052
10111
  },
9053
10112
  insert(indexName, document, docId, options) {
9054
- return insertDocument(mutationCtx, indexName, document, docId, options);
10113
+ return withOpenIndex(indexName, () => insertDocument(mutationCtx, indexName, document, docId, options));
9055
10114
  },
9056
10115
  insertBatch(indexName, documents, options) {
9057
- return insertDocumentBatch(mutationCtx, indexName, documents, options);
10116
+ return withOpenIndex(indexName, () => insertDocumentBatch(mutationCtx, indexName, documents, options));
9058
10117
  },
9059
10118
  remove(indexName, docId) {
9060
- return removeDocument(mutationCtx, indexName, docId);
10119
+ return withOpenIndex(indexName, () => removeDocument(mutationCtx, indexName, docId));
9061
10120
  },
9062
10121
  removeBatch(indexName, docIds) {
9063
- return removeDocumentBatch(mutationCtx, indexName, docIds);
10122
+ return withOpenIndex(indexName, () => removeDocumentBatch(mutationCtx, indexName, docIds));
9064
10123
  },
9065
10124
  update(indexName, docId, document) {
9066
- return updateDocument(mutationCtx, indexName, docId, document);
10125
+ return withOpenIndex(indexName, () => updateDocument(mutationCtx, indexName, docId, document));
9067
10126
  },
9068
10127
  updateBatch(indexName, updates) {
9069
- return updateDocumentBatch(mutationCtx, indexName, updates);
10128
+ return withOpenIndex(indexName, () => updateDocumentBatch(mutationCtx, indexName, updates));
9070
10129
  },
9071
10130
  async get(indexName, docId) {
9072
- guardShutdown();
9073
- requireIndex(indexName);
9074
- return executor.execute({ type: "get", indexName, docId, requestId: docId });
10131
+ return withOpenIndex(indexName, () => executor.execute({ type: "get", indexName, docId, requestId: docId }));
9075
10132
  },
9076
10133
  async getMultiple(indexName, docIds) {
9077
10134
  guardShutdown();
9078
10135
  requireIndex(indexName);
9079
- const result = /* @__PURE__ */ new Map();
9080
- for (const docId of docIds) {
9081
- const doc = await narsil.get(indexName, docId);
9082
- if (doc !== void 0) result.set(docId, doc);
9083
- }
9084
- 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
+ });
9085
10149
  },
9086
10150
  async has(indexName, docId) {
9087
- guardShutdown();
9088
- requireIndex(indexName);
9089
- return executor.execute({ type: "has", indexName, docId, requestId: docId });
10151
+ return withOpenIndex(indexName, () => executor.execute({ type: "has", indexName, docId, requestId: docId }));
9090
10152
  },
9091
10153
  async countDocuments(indexName) {
9092
- guardShutdown();
9093
- requireIndex(indexName);
9094
- return executor.execute({ type: "count", indexName, requestId: indexName });
10154
+ return withOpenIndex(indexName, () => executor.execute({ type: "count", indexName, requestId: indexName }));
9095
10155
  },
9096
10156
  async listDocuments(indexName, params) {
9097
10157
  return runEngineListDocuments(core, indexName, params ?? {});
@@ -9108,15 +10168,15 @@ function createNarsilFromCore(core, config) {
9108
10168
  async rebuildAnalysis(indexName) {
9109
10169
  guardShutdown();
9110
10170
  requireIndex(indexName);
9111
- await core.analysisRebuild.rebuild(indexName);
10171
+ await withOpenIndex(indexName, () => core.analysisRebuild.rebuild(indexName));
9112
10172
  },
9113
10173
  async snapshot(indexName) {
9114
10174
  guardShutdown();
9115
- return createSnapshot(requireManager(indexName), requireIndex(indexName));
10175
+ return withOpenIndex(indexName, () => createSnapshot(requireManager(indexName), requireIndex(indexName)));
9116
10176
  },
9117
10177
  async restore(indexName, data) {
9118
10178
  guardShutdown();
9119
- return restoreFromSnapshot(indexName, data, {
10179
+ await restoreFromSnapshot(indexName, data, {
9120
10180
  executor,
9121
10181
  indexRegistry,
9122
10182
  getVectorFieldPaths,
@@ -9126,66 +10186,74 @@ function createNarsilFromCore(core, config) {
9126
10186
  embeddingAdapters: core.embeddingAdapters,
9127
10187
  defaultEmbeddingAdapter: config?.embedding ?? null,
9128
10188
  markAnalysisStale: core.analysisRebuild.markStale,
9129
- clearAnalysisStale: core.analysisRebuild.clearStale
10189
+ clearAnalysisStale: core.analysisRebuild.clearStale,
10190
+ indexState: core.indexState
9130
10191
  });
9131
10192
  },
9132
10193
  async checkpoint(indexName) {
9133
10194
  guardShutdown();
9134
10195
  requireIndex(indexName);
9135
- if (durability) {
9136
- await durability.manager.checkpoint(indexName);
9137
- }
10196
+ await withOpenIndex(indexName, async () => {
10197
+ if (durability) await durability.manager.checkpoint(indexName);
10198
+ });
9138
10199
  },
9139
10200
  async clear(indexName) {
9140
10201
  guardShutdown();
9141
10202
  requireIndex(indexName);
9142
- await executor.execute({ type: "clear", indexName, requestId: indexName });
9143
- await orchestrator.replicateToWorkers({ type: "clear", indexName, requestId: `replicate-clear-${indexName}` });
9144
- 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
+ });
9145
10208
  },
9146
10209
  async rebalance(indexName, targetPartitionCount) {
9147
10210
  guardShutdown();
9148
10211
  requireIndex(indexName);
9149
- return executeRebalance(requireManager(indexName), indexName, targetPartitionCount, rebalanceCtx);
10212
+ return withOpenIndex(
10213
+ indexName,
10214
+ () => executeRebalance(requireManager(indexName), indexName, targetPartitionCount, rebalanceCtx)
10215
+ );
9150
10216
  },
9151
10217
  async updatePartitionConfig(indexName, partitionConfig) {
9152
10218
  guardShutdown();
9153
- const entry = requireIndex(indexName);
9154
- const manager = requireManager(indexName);
9155
- if (rebalancingIndexes.has(indexName)) {
9156
- throw new NarsilError(
9157
- ErrorCodes.PARTITION_REBALANCING_BACKPRESSURE,
9158
- `Index "${indexName}" is currently being rebalanced`
9159
- );
9160
- }
9161
- validatePartitionConfig(partitionConfig);
9162
- if (partitionConfig.maxPartitions !== void 0 && partitionConfig.maxPartitions < manager.partitionCount) {
9163
- throw new NarsilError(
9164
- ErrorCodes.PARTITION_CAPACITY_EXCEEDED,
9165
- `maxPartitions (${partitionConfig.maxPartitions}) is less than the current partition count (${manager.partitionCount})`,
9166
- { maxPartitions: partitionConfig.maxPartitions, partitionCount: manager.partitionCount }
9167
- );
9168
- }
9169
- const currentDocCount = manager.countDocuments();
9170
- const newMaxDocs = partitionConfig.maxDocsPerPartition ?? entry.config.partitions?.maxDocsPerPartition;
9171
- if (newMaxDocs !== void 0) {
9172
- const newTotalCapacity = newMaxDocs * manager.partitionCount;
9173
- 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) {
9174
10230
  throw new NarsilError(
9175
10231
  ErrorCodes.PARTITION_CAPACITY_EXCEEDED,
9176
- `New capacity (${newTotalCapacity}) is less than current document count (${currentDocCount})`,
9177
- { newTotalCapacity, currentDocCount }
10232
+ `maxPartitions (${partitionConfig.maxPartitions}) is less than the current partition count (${manager.partitionCount})`,
10233
+ { maxPartitions: partitionConfig.maxPartitions, partitionCount: manager.partitionCount }
9178
10234
  );
9179
10235
  }
9180
- }
9181
- if (!entry.config.partitions) entry.config.partitions = {};
9182
- if (partitionConfig.maxDocsPerPartition !== void 0)
9183
- entry.config.partitions.maxDocsPerPartition = partitionConfig.maxDocsPerPartition;
9184
- if (partitionConfig.maxPartitions !== void 0)
9185
- entry.config.partitions.maxPartitions = partitionConfig.maxPartitions;
9186
- if (partitionConfig.watermark !== void 0) entry.config.partitions.watermark = partitionConfig.watermark;
9187
- if (durability) await durability.manager.persistMetadata(indexName);
9188
- 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
+ });
9189
10257
  },
9190
10258
  async getMemoryStats() {
9191
10259
  let estimatedIndexBytes = 0;
@@ -9193,22 +10261,33 @@ function createNarsilFromCore(core, config) {
9193
10261
  estimatedIndexBytes += executor.getManager(name)?.estimateMemoryBytes() ?? 0;
9194
10262
  }
9195
10263
  const workers = await orchestrator.getWorkerMemoryStats();
9196
- 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
+ };
9197
10273
  },
9198
10274
  async compactVectors(indexName, fieldName) {
9199
10275
  guardShutdown();
9200
10276
  requireIndex(indexName);
9201
- compactVectors(requireManager(indexName), indexName, fieldName);
10277
+ await withOpenIndex(indexName, async () => {
10278
+ compactVectors(requireManager(indexName), indexName, fieldName);
10279
+ });
9202
10280
  },
9203
10281
  async optimizeVectors(indexName, fieldName) {
9204
10282
  guardShutdown();
9205
10283
  requireIndex(indexName);
9206
- await optimizeVectors(requireManager(indexName), indexName, fieldName);
10284
+ await withOpenIndex(indexName, () => optimizeVectors(requireManager(indexName), indexName, fieldName));
9207
10285
  },
9208
10286
  vectorMaintenanceStatus(indexName) {
9209
10287
  guardShutdown();
9210
10288
  requireIndex(indexName);
9211
- return getVectorMaintenanceStatus(requireManager(indexName));
10289
+ const manager = executor.getManager(indexName);
10290
+ return manager === void 0 ? [] : getVectorMaintenanceStatus(manager);
9212
10291
  },
9213
10292
  on(event, handler) {
9214
10293
  const key = event;
@@ -9241,16 +10320,16 @@ function createNarsilFromCore(core, config) {
9241
10320
  async function createNarsil(config) {
9242
10321
  const core = createEngineCore(config);
9243
10322
  if (core.durability) {
9244
- await core.durability.manager.recover();
10323
+ await core.durability.manager.recover(config?.lifecycle !== void 0);
9245
10324
  }
9246
10325
  if (core.invalidation) {
9247
10326
  await core.invalidation.start();
9248
10327
  }
9249
- await core.analysisRebuild.reviewStaleIndexes();
10328
+ if (config?.lifecycle === void 0) await core.analysisRebuild.reviewStaleIndexes();
9250
10329
  return createNarsilFromCore(core, config);
9251
10330
  }
9252
10331
 
9253
10332
  // src/index.ts
9254
10333
  var VERSION = "0.1.0";
9255
10334
 
9256
- export { DEFAULT_SUGGEST_LIMIT, MAX_SUGGEST_LIMIT, MAX_SUGGEST_SCATTER_LIMIT, SUGGEST_OVERSAMPLE_FACTOR, SUGGEST_OVERSAMPLE_PADDING, VERSION, WIRE_BATCH_BUDGET, applyDeleteEntry, applyIndexEntry, chunkByBudget, clampLimit, computeEntryChecksum, createAckMessage, createDurableDirectory, createEngineCore, createEngineIndex, createEntryBatchMessage, createEntryMessage, createForwardBatchMessage, createForwardBatchResultMessage, createForwardMessage, createInsyncAddMessage, createInsyncConfirmMessage, createInsyncRemoveMessage, createNarsil, createNarsilFromCore, now, runEngineListDocuments, runEnginePreflight, runEngineQuery, runEngineQueryStats, runEngineSuggest, validateAckPayload, validateEntryBatchPayload, validateEntryPayload, validateForwardBatchPayload, validateForwardBatchResultPayload, validateInsyncConfirmPayload, validateReplicationEntry, writeSegmentedCheckpoint };
10335
+ export { DEFAULT_SUGGEST_LIMIT, MAX_SUGGEST_LIMIT, MAX_SUGGEST_SCATTER_LIMIT, SUGGEST_OVERSAMPLE_FACTOR, SUGGEST_OVERSAMPLE_PADDING, VERSION, WIRE_BATCH_BUDGET, applyDeleteEntry, applyIndexEntry, chunkByBudget, clampLimit, computeEntryChecksum, createAckMessage, createDurableDirectory, createEngineCore, createEngineIndex, createEntryBatchMessage, createEntryMessage, createForwardBatchMessage, createForwardBatchResultMessage, createForwardMessage, createInsyncAddMessage, createInsyncConfirmMessage, createInsyncRemoveMessage, createNarsil, createNarsilFromCore, dedupePinnedEntries, foldGroupReducer, listBindingOf, now, queryBindingOf, runEngineListDocuments, runEnginePreflight, runEngineQuery, runEngineQueryStats, runEngineSuggest, validateAckPayload, validateEntryBatchPayload, validateEntryPayload, validateForwardBatchPayload, validateForwardBatchResultPayload, validateInsyncConfirmPayload, validateReplicationEntry, writeSegmentedCheckpoint };