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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/adapters/filesystem.mjs +1 -1
  2. package/dist/adapters/indexeddb.mjs +1 -1
  3. package/dist/{chunk-TLRUUMVE.mjs → chunk-P4ELXVJQ.mjs} +1 -0
  4. package/dist/{chunk-KEDL6FFZ.mjs → chunk-WASNWIEK.mjs} +1142 -327
  5. package/dist/{chunk-YGQMKTKG.mjs → chunk-XNCJFU2E.mjs} +121 -27
  6. package/dist/{chunk-O4Q5JYNK.mjs → chunk-XWICBGYT.mjs} +1 -1
  7. package/dist/{client-B5uO5jL1.d.ts → client-C5BkUqB8.d.ts} +25 -2
  8. package/dist/client.d.ts +7 -7
  9. package/dist/client.mjs +11 -2
  10. package/dist/{config-NgUCkc6a.d.ts → config-B-ZI-ft5.d.ts} +2 -2
  11. package/dist/{config-B09-cud9.d.ts → config-BZRExglL.d.ts} +20 -3
  12. package/dist/{direct-executor-7O23GEHE.mjs → direct-executor-IMXNRS7O.mjs} +2 -2
  13. package/dist/distribution/coordinator/etcd.d.ts +2 -2
  14. package/dist/distribution/coordinator/etcd.mjs +1 -1
  15. package/dist/distribution/coordinator/in-memory.d.ts +2 -2
  16. package/dist/distribution/transport/grpc.mjs +1 -1
  17. package/dist/distribution.d.ts +24 -10
  18. package/dist/distribution.mjs +1023 -864
  19. package/dist/embeddings/openai.mjs +1 -1
  20. package/dist/{engine-CjLhdDIm.d.ts → engine-Cjr1uBLa.d.ts} +28 -6
  21. package/dist/index.browser.mjs +1237 -330
  22. package/dist/index.d.ts +9 -9
  23. package/dist/index.mjs +4 -4
  24. package/dist/{options-BakECW-r.d.ts → options-BC68VPoE.d.ts} +9 -2
  25. package/dist/persistence/durability/checkpoint-worker.d.ts +3 -1
  26. package/dist/persistence/durability/checkpoint-worker.mjs +4 -4
  27. package/dist/react.d.ts +4 -4
  28. package/dist/react.mjs +1 -0
  29. package/dist/{schema-DXOd96kx.d.ts → schema-B7juWg8Q.d.ts} +1 -1
  30. package/dist/{search-BGouJxH4.d.ts → search-DDHjfXhJ.d.ts} +14 -3
  31. package/dist/server.d.ts +8 -8
  32. package/dist/server.mjs +27 -6
  33. package/dist/{types-BTE33VZS.d.ts → types-DsSSjY4d.d.ts} +1 -1
  34. package/dist/workers/entry.mjs +2 -2
  35. package/package.json +1 -1
@@ -12,6 +12,7 @@ var ErrorCodes = {
12
12
  INDEX_NOT_FOUND: "INDEX_NOT_FOUND",
13
13
  INDEX_ALREADY_EXISTS: "INDEX_ALREADY_EXISTS",
14
14
  INDEX_ORPHANED: "INDEX_ORPHANED",
15
+ INDEX_REOPEN_CAPACITY_EXHAUSTED: "INDEX_REOPEN_CAPACITY_EXHAUSTED",
15
16
  PARTITION_CORRUPTED: "PARTITION_CORRUPTED",
16
17
  PARTITION_REBALANCING_BACKPRESSURE: "PARTITION_REBALANCING_BACKPRESSURE",
17
18
  WORKER_CRASHED: "WORKER_CRASHED",
@@ -2488,6 +2489,9 @@ function metadataToWire(meta) {
2488
2489
  if (meta.vectorFields) {
2489
2490
  wire.vector_fields = meta.vectorFields;
2490
2491
  }
2492
+ if (isNonNegativeInteger(meta.documentCount) && Number.isSafeInteger(meta.documentCount)) {
2493
+ wire.document_count = meta.documentCount;
2494
+ }
2491
2495
  if (meta.embedding) {
2492
2496
  wire.embedding = meta.embedding.adapter !== void 0 ? { adapter: meta.embedding.adapter, fields: meta.embedding.fields } : { fields: meta.embedding.fields };
2493
2497
  }
@@ -2561,6 +2565,9 @@ function wireToMetadata(raw) {
2561
2565
  if (raw.vector_fields) {
2562
2566
  meta.vectorFields = raw.vector_fields;
2563
2567
  }
2568
+ if (isNonNegativeInteger(raw.document_count) && Number.isSafeInteger(raw.document_count)) {
2569
+ meta.documentCount = raw.document_count;
2570
+ }
2564
2571
  if (raw.embedding && typeof raw.embedding === "object" && typeof raw.embedding.fields === "object") {
2565
2572
  meta.embedding = typeof raw.embedding.adapter === "string" ? { adapter: raw.embedding.adapter, fields: raw.embedding.fields } : { fields: raw.embedding.fields };
2566
2573
  }
@@ -3139,6 +3146,46 @@ function documentWithOwnBuffers(document) {
3139
3146
  if (!sharesABiggerBuffer(document)) return document;
3140
3147
  return withOwnBuffers(document);
3141
3148
  }
3149
+ var STRING_HEADER_BYTES = 16;
3150
+ var TWO_BYTE_SAMPLE_LIMIT = 16;
3151
+ var NUMBER_BYTES = 16;
3152
+ var PRIMITIVE_SLOT_BYTES = 4;
3153
+ var ARRAY_HEADER_BYTES = 32;
3154
+ var ARRAY_SLOT_BYTES = 8;
3155
+ var OBJECT_HEADER_BYTES = 40;
3156
+ var PROPERTY_SLOT_BYTES = 8;
3157
+ var TYPED_ARRAY_HEADER_BYTES = 96;
3158
+ function stringBytes(value) {
3159
+ const sampled = Math.min(value.length, TWO_BYTE_SAMPLE_LIMIT);
3160
+ for (let i = 0; i < sampled; i += 1) {
3161
+ if (value.charCodeAt(i) > 255) {
3162
+ return STRING_HEADER_BYTES + value.length * 2;
3163
+ }
3164
+ }
3165
+ return STRING_HEADER_BYTES + value.length;
3166
+ }
3167
+ function documentValueBytes(value) {
3168
+ if (typeof value === "string") return stringBytes(value);
3169
+ if (typeof value === "number") return NUMBER_BYTES;
3170
+ if (value === null || value === void 0 || typeof value === "boolean") return PRIMITIVE_SLOT_BYTES;
3171
+ if (ArrayBuffer.isView(value)) return TYPED_ARRAY_HEADER_BYTES + value.byteLength;
3172
+ if (Array.isArray(value)) {
3173
+ let bytes = ARRAY_HEADER_BYTES + value.length * ARRAY_SLOT_BYTES;
3174
+ for (let i = 0; i < value.length; i += 1) {
3175
+ bytes += documentValueBytes(value[i]);
3176
+ }
3177
+ return bytes;
3178
+ }
3179
+ if (typeof value === "object") {
3180
+ const source = value;
3181
+ let bytes = OBJECT_HEADER_BYTES;
3182
+ for (const key of Object.keys(source)) {
3183
+ bytes += PROPERTY_SLOT_BYTES + documentValueBytes(source[key]);
3184
+ }
3185
+ return bytes;
3186
+ }
3187
+ return PRIMITIVE_SLOT_BYTES;
3188
+ }
3142
3189
  function createDocumentStore() {
3143
3190
  const docs = /* @__PURE__ */ new Map();
3144
3191
  const forwardMap = /* @__PURE__ */ new Map();
@@ -3146,6 +3193,19 @@ function createDocumentStore() {
3146
3193
  const lengthColumns = /* @__PURE__ */ new Map();
3147
3194
  let nextInternalId = 0;
3148
3195
  let sortedIds = null;
3196
+ let storedContentBytes = 0;
3197
+ function replaceStored(docId, document, fieldLengths) {
3198
+ const existing = docs.get(docId);
3199
+ if (existing !== void 0) {
3200
+ storedContentBytes -= documentValueBytes(existing.fields);
3201
+ } else {
3202
+ sortedIds = null;
3203
+ }
3204
+ writeFieldLengths(assignInternalId(docId), fieldLengths);
3205
+ const fields = documentWithOwnBuffers(document);
3206
+ storedContentBytes += documentValueBytes(fields);
3207
+ docs.set(docId, { fields, fieldLengths });
3208
+ }
3149
3209
  function writeFieldLengths(internalId, fieldLengths) {
3150
3210
  for (const fieldName of Object.keys(fieldLengths)) {
3151
3211
  let column = lengthColumns.get(fieldName);
@@ -3182,21 +3242,21 @@ function createDocumentStore() {
3182
3242
  };
3183
3243
  return {
3184
3244
  store(docId, document, fieldLengths) {
3185
- if (!docs.has(docId)) sortedIds = null;
3186
- writeFieldLengths(assignInternalId(docId), fieldLengths);
3187
- docs.set(docId, { fields: documentWithOwnBuffers(document), fieldLengths });
3245
+ replaceStored(docId, document, fieldLengths);
3188
3246
  },
3189
3247
  storeRef(docId, document, fieldLengths) {
3190
- if (!docs.has(docId)) sortedIds = null;
3191
- writeFieldLengths(assignInternalId(docId), fieldLengths);
3192
- docs.set(docId, { fields: documentWithOwnBuffers(document), fieldLengths });
3248
+ replaceStored(docId, document, fieldLengths);
3193
3249
  },
3194
3250
  get(docId) {
3195
3251
  return docs.get(docId);
3196
3252
  },
3197
3253
  remove(docId) {
3254
+ const existing = docs.get(docId);
3198
3255
  const removed = docs.delete(docId);
3199
3256
  if (removed) {
3257
+ if (existing !== void 0) {
3258
+ storedContentBytes -= documentValueBytes(existing.fields);
3259
+ }
3200
3260
  sortedIds = null;
3201
3261
  const internalId = forwardMap.get(docId);
3202
3262
  if (internalId !== void 0) {
@@ -3233,6 +3293,7 @@ function createDocumentStore() {
3233
3293
  docs.clear();
3234
3294
  lengthColumns.clear();
3235
3295
  clearMappings();
3296
+ storedContentBytes = 0;
3236
3297
  },
3237
3298
  serialize() {
3238
3299
  const result = {};
@@ -3245,10 +3306,12 @@ function createDocumentStore() {
3245
3306
  docs.clear();
3246
3307
  lengthColumns.clear();
3247
3308
  clearMappings();
3309
+ storedContentBytes = 0;
3248
3310
  for (const docId of Object.keys(data)) {
3249
3311
  const stored = data[docId];
3250
3312
  writeFieldLengths(assignInternalId(docId), stored.fieldLengths);
3251
3313
  const fields = documentWithOwnBuffers(stored.fields);
3314
+ storedContentBytes += documentValueBytes(fields);
3252
3315
  docs.set(docId, fields === stored.fields ? stored : { fields, fieldLengths: stored.fieldLengths });
3253
3316
  }
3254
3317
  },
@@ -3272,6 +3335,9 @@ function createDocumentStore() {
3272
3335
  internalIdCapacity() {
3273
3336
  return nextInternalId;
3274
3337
  },
3338
+ contentBytes() {
3339
+ return storedContentBytes;
3340
+ },
3275
3341
  resolver() {
3276
3342
  return idResolver;
3277
3343
  }
@@ -5803,7 +5869,7 @@ var BYTES_PER_NUMBER_SLOT = 8;
5803
5869
  var BYTES_PER_BOOLEAN_SLOT = 1;
5804
5870
  var BYTES_PER_MIXED_SLOT = 8;
5805
5871
  var BYTES_PER_STRING_CODE_UNIT = 2;
5806
- var STRING_HEADER_BYTES = 24;
5872
+ var STRING_HEADER_BYTES2 = 24;
5807
5873
  function grow(length, wanted) {
5808
5874
  let next = length === 0 ? 64 : length;
5809
5875
  while (next <= wanted) next *= 2;
@@ -5925,7 +5991,7 @@ function createValueStore(initialKind) {
5925
5991
  for (const value of mixed) {
5926
5992
  if (typeof value !== "string") continue;
5927
5993
  if (value.length < SORT_VALUE_MAX_CODE_POINTS) continue;
5928
- bytes += STRING_HEADER_BYTES + value.length * BYTES_PER_STRING_CODE_UNIT;
5994
+ bytes += STRING_HEADER_BYTES2 + value.length * BYTES_PER_STRING_CODE_UNIT;
5929
5995
  }
5930
5996
  return bytes;
5931
5997
  }
@@ -6264,15 +6330,17 @@ function sortColumnBytes(state) {
6264
6330
  }
6265
6331
 
6266
6332
  // src/core/partition/memory.ts
6267
- var AVG_DOC_OVERHEAD = 350;
6268
- var POSTING_ENTRY_SIZE = 24;
6269
- var PER_TERM_OVERHEAD = 180;
6270
- var SURFACE_ENTRY_OVERHEAD = 140;
6271
- var FIELD_ENTRY_OVERHEAD = 42;
6333
+ var DOC_ENTRY_OVERHEAD_BYTES = 260;
6334
+ var LENGTH_COLUMN_SLOT_BYTES = 6;
6335
+ var POSTING_ENTRY_BYTES = 40;
6336
+ var POSITIONS_ARRAY_OVERHEAD_BYTES = 88;
6337
+ var POSITION_BYTES = 8;
6338
+ var PER_TERM_OVERHEAD_BYTES = 180;
6339
+ var SURFACE_ENTRY_OVERHEAD_BYTES = 140;
6340
+ var FIELD_ENTRY_OVERHEAD_BYTES = 42;
6272
6341
  function estimatePartitionBytes(state) {
6273
6342
  const docCount = state.docStore.count();
6274
6343
  if (docCount === 0) return 0;
6275
- let bytes = docCount * AVG_DOC_OVERHEAD;
6276
6344
  const docFreqs = state.stats.docFrequencies;
6277
6345
  let totalPostings = 0;
6278
6346
  let termCount = 0;
@@ -6280,13 +6348,27 @@ function estimatePartitionBytes(state) {
6280
6348
  totalPostings += docFreqs[term];
6281
6349
  termCount++;
6282
6350
  }
6283
- bytes += totalPostings * POSTING_ENTRY_SIZE;
6284
- bytes += termCount * PER_TERM_OVERHEAD;
6285
- bytes += state.surfaceRegistry.size() * SURFACE_ENTRY_OVERHEAD;
6286
- bytes += docCount * state.numericIndexes.size * FIELD_ENTRY_OVERHEAD;
6287
- bytes += docCount * state.booleanIndexes.size * FIELD_ENTRY_OVERHEAD;
6288
- bytes += docCount * state.enumIndexes.size * FIELD_ENTRY_OVERHEAD;
6289
- bytes += docCount * state.geoIndexes.size * FIELD_ENTRY_OVERHEAD;
6351
+ const fieldLengthTotals = state.stats.totalFieldLengths;
6352
+ let totalTokenOccurrences = 0;
6353
+ let textFieldCount = 0;
6354
+ for (const field in fieldLengthTotals) {
6355
+ totalTokenOccurrences += fieldLengthTotals[field];
6356
+ textFieldCount++;
6357
+ }
6358
+ let bytes = state.docStore.contentBytes();
6359
+ bytes += docCount * DOC_ENTRY_OVERHEAD_BYTES;
6360
+ bytes += textFieldCount * state.docStore.internalIdCapacity() * LENGTH_COLUMN_SLOT_BYTES;
6361
+ bytes += totalPostings * POSTING_ENTRY_BYTES;
6362
+ if (state.trackPositions) {
6363
+ bytes += totalPostings * POSITIONS_ARRAY_OVERHEAD_BYTES;
6364
+ bytes += totalTokenOccurrences * POSITION_BYTES;
6365
+ }
6366
+ bytes += termCount * PER_TERM_OVERHEAD_BYTES;
6367
+ bytes += state.surfaceRegistry.size() * SURFACE_ENTRY_OVERHEAD_BYTES;
6368
+ bytes += docCount * state.numericIndexes.size * FIELD_ENTRY_OVERHEAD_BYTES;
6369
+ bytes += docCount * state.booleanIndexes.size * FIELD_ENTRY_OVERHEAD_BYTES;
6370
+ bytes += docCount * state.enumIndexes.size * FIELD_ENTRY_OVERHEAD_BYTES;
6371
+ bytes += docCount * state.geoIndexes.size * FIELD_ENTRY_OVERHEAD_BYTES;
6290
6372
  return bytes + sortColumnBytes(state);
6291
6373
  }
6292
6374
 
@@ -8441,6 +8523,9 @@ function createFrozenDocTable(idTable, lengthColumns, documents, tombstones) {
8441
8523
  internalIdCapacity() {
8442
8524
  return capacity;
8443
8525
  },
8526
+ contentBytes() {
8527
+ return documents.byteLength;
8528
+ },
8444
8529
  resolver() {
8445
8530
  return resolver;
8446
8531
  }
@@ -8468,21 +8553,32 @@ function wrapEncodedDocumentTable(data) {
8468
8553
  const { blob, offsets } = data;
8469
8554
  const count = offsets.length - 1;
8470
8555
  const decoded = new Array(count);
8556
+ const encodedBytes = blob.byteLength + offsets.byteLength;
8557
+ let decodedBytes = 0;
8471
8558
  return {
8472
8559
  count,
8560
+ get byteLength() {
8561
+ return encodedBytes + decodedBytes;
8562
+ },
8473
8563
  docAt(ordinal) {
8474
8564
  let document = decoded[ordinal];
8475
8565
  if (document === void 0) {
8476
8566
  document = decode(blob.slice(offsets[ordinal], offsets[ordinal + 1]));
8477
8567
  decoded[ordinal] = document;
8568
+ decodedBytes += documentValueBytes(document);
8478
8569
  }
8479
8570
  return document;
8480
8571
  }
8481
8572
  };
8482
8573
  }
8483
8574
  function wrapDocumentArray(documents) {
8575
+ let residentBytes = 0;
8576
+ for (const document of documents) {
8577
+ residentBytes += documentValueBytes(document);
8578
+ }
8484
8579
  return {
8485
8580
  count: documents.length,
8581
+ byteLength: residentBytes,
8486
8582
  docAt(ordinal) {
8487
8583
  return documents[ordinal];
8488
8584
  }
@@ -13115,11 +13211,7 @@ function createWorkerPool(config) {
13115
13211
  indexAssignment.set(indexName, slotIndex);
13116
13212
  }
13117
13213
  function removeIndex(indexName) {
13118
- const slotIndex = indexAssignment.get(indexName);
13119
- if (slotIndex === void 0) {
13120
- return;
13121
- }
13122
- workers.get(slotIndex)?.indexes.delete(indexName);
13214
+ for (const slot of workers.values()) slot.indexes.delete(indexName);
13123
13215
  indexAssignment.delete(indexName);
13124
13216
  }
13125
13217
  function getExecutor(indexName) {
@@ -14317,6 +14409,9 @@ function createDirectExecutor() {
14317
14409
  }
14318
14410
  function dropIndex(indexName) {
14319
14411
  const entry = requireIndex(indexName);
14412
+ for (const vectorIndex of entry.vectorIndexes.values()) {
14413
+ vectorIndex.dispose();
14414
+ }
14320
14415
  for (const partition of entry.manager.getAllPartitions()) {
14321
14416
  partition.clear();
14322
14417
  }
@@ -14611,7 +14706,7 @@ function createAnalysisRebuildCoordinator(config, deps) {
14611
14706
  async function rebuildNow(indexName, entry) {
14612
14707
  const manager = deps.getManager(indexName);
14613
14708
  if (manager === void 0) {
14614
- stale.delete(indexName);
14709
+ entry.run = null;
14615
14710
  return;
14616
14711
  }
14617
14712
  const partitionCount = manager.partitionCount;
@@ -14663,6 +14758,23 @@ function createAnalysisRebuildCoordinator(config, deps) {
14663
14758
  const documentCount = deps.getManager(indexName)?.countDocuments() ?? 0;
14664
14759
  await config?.onStaleAnalysis?.({ ...entry.index, documentCount }, () => enqueue2(indexName, entry));
14665
14760
  }
14761
+ async function review(indexName, entry) {
14762
+ if (entry.run !== null || deps.getManager(indexName) === void 0) return;
14763
+ try {
14764
+ await announce(indexName, entry);
14765
+ } catch (err) {
14766
+ deps.emit({
14767
+ indexName,
14768
+ status: "failed",
14769
+ partitionsRebuilt: 0,
14770
+ partitionCount: deps.getManager(indexName)?.partitionCount ?? 0,
14771
+ error: toError(err)
14772
+ });
14773
+ }
14774
+ if (rebuildMode === "auto") {
14775
+ void enqueue2(indexName, entry).catch(() => void 0);
14776
+ }
14777
+ }
14666
14778
  return {
14667
14779
  markStale(index) {
14668
14780
  const existing = stale.get(index.indexName);
@@ -14677,23 +14789,16 @@ function createAnalysisRebuildCoordinator(config, deps) {
14677
14789
  isStale(indexName) {
14678
14790
  return stale.has(indexName);
14679
14791
  },
14792
+ isRunning(indexName) {
14793
+ return stale.get(indexName)?.run !== null && stale.get(indexName)?.run !== void 0;
14794
+ },
14795
+ review(indexName) {
14796
+ const entry = stale.get(indexName);
14797
+ return entry === void 0 ? Promise.resolve() : review(indexName, entry);
14798
+ },
14680
14799
  async reviewStaleIndexes() {
14681
14800
  for (const [indexName, entry] of [...stale]) {
14682
- if (entry.run !== null) continue;
14683
- try {
14684
- await announce(indexName, entry);
14685
- } catch (err) {
14686
- deps.emit({
14687
- indexName,
14688
- status: "failed",
14689
- partitionsRebuilt: 0,
14690
- partitionCount: deps.getManager(indexName)?.partitionCount ?? 0,
14691
- error: toError(err)
14692
- });
14693
- }
14694
- if (rebuildMode === "auto") {
14695
- void enqueue2(indexName, entry).catch(() => void 0);
14696
- }
14801
+ await review(indexName, entry);
14697
14802
  }
14698
14803
  },
14699
14804
  rebuild(indexName) {
@@ -16142,6 +16247,9 @@ function mergeTimeOrderedSegments(ordered, fallback) {
16142
16247
  validateMergedPartition(merged);
16143
16248
  return merged;
16144
16249
  }
16250
+ function countLiveDocuments(ordered) {
16251
+ return resolveWinners(ordered).size;
16252
+ }
16145
16253
  function resolveWinners(ordered) {
16146
16254
  const winner = /* @__PURE__ */ new Map();
16147
16255
  for (let segIndex = 0; segIndex < ordered.length; segIndex += 1) {
@@ -17395,6 +17503,7 @@ async function writeSegmentedCheckpoint(input) {
17395
17503
  });
17396
17504
  }
17397
17505
  const partitions = [];
17506
+ let wholeDocumentCount = input.wholePartitionPayload === void 0 ? null : 0;
17398
17507
  for (const target of input.targets) {
17399
17508
  const priorPartition = priorManifest?.partitions.find((p) => p.partitionId === target.partitionId);
17400
17509
  const priorSeqNo = snapshotCheckpointFor(priorManifest?.checkpoint ?? [], target.partitionId);
@@ -17405,15 +17514,13 @@ async function writeSegmentedCheckpoint(input) {
17405
17514
  priorSeqNo,
17406
17515
  target.lastSeqNo
17407
17516
  );
17408
- partitions.push(
17409
- input.wholePartitionPayload === void 0 ? await writePartition(context, target.partitionId, priorPartition, entries) : await writeWholePartition(
17410
- context,
17411
- target.partitionId,
17412
- priorPartition,
17413
- input.wholePartitionPayload(target.partitionId),
17414
- entries
17415
- )
17416
- );
17517
+ if (input.wholePartitionPayload === void 0) {
17518
+ partitions.push(await writePartition(context, target.partitionId, priorPartition, entries));
17519
+ } else {
17520
+ const whole = input.wholePartitionPayload(target.partitionId);
17521
+ wholeDocumentCount = (wholeDocumentCount ?? 0) + whole.docCount;
17522
+ partitions.push(await writeWholePartition(context, target.partitionId, priorPartition, whole, entries));
17523
+ }
17417
17524
  }
17418
17525
  carryForwardUncheckpointedPartitions(priorManifest, input.targets, partitions, checkpointByPartition);
17419
17526
  const manifest = {
@@ -17425,6 +17532,7 @@ async function writeSegmentedCheckpoint(input) {
17425
17532
  const parts = await encodeSegmentManifest(manifest);
17426
17533
  await directory.atomicWrite(manifestKey(indexName), [parts.header, parts.payload]);
17427
17534
  await collectGarbage(directory, indexName, priorManifest, manifest);
17535
+ return wholeDocumentCount;
17428
17536
  }
17429
17537
  async function writePartition(context, partitionId, priorPartition, entries) {
17430
17538
  let segments = priorPartition ? [...priorPartition.segments] : [];
@@ -17544,9 +17652,9 @@ async function writeIndexCheckpoint(input) {
17544
17652
  const { directory, metadata, targets, compactionThreshold, manager } = input;
17545
17653
  const offloaded = !input.fromMemory && input.canOffload && metadata.tokenizer === void 0 && metadata.stopWords === void 0 && await runCheckpointOnWorker({ root: directory.root, metadata, targets, compactionThreshold });
17546
17654
  if (offloaded) {
17547
- return;
17655
+ return null;
17548
17656
  }
17549
- await writeSegmentedCheckpoint({
17657
+ return writeSegmentedCheckpoint({
17550
17658
  directory,
17551
17659
  metadata,
17552
17660
  targets,
@@ -17578,9 +17686,171 @@ function createSeqOwner(startSeqNo, primaryTerm = SINGLE_NODE_PRIMARY_TERM) {
17578
17686
  };
17579
17687
  }
17580
17688
 
17689
+ // src/persistence/durability/checkpoint-run.ts
17690
+ function toError2(error) {
17691
+ return error instanceof Error ? error : new Error(String(error));
17692
+ }
17693
+ async function runDurableCheckpoint(input) {
17694
+ const manager = input.hooks.getManager(input.indexName);
17695
+ if (manager === void 0) {
17696
+ return;
17697
+ }
17698
+ const metadata = input.hooks.buildMetadata(input.indexName);
17699
+ if (metadata === void 0) {
17700
+ return;
17701
+ }
17702
+ const targets = [];
17703
+ const documentCount = manager.countDocuments();
17704
+ for (let i = 0; i < manager.partitionCount; i += 1) {
17705
+ const partition = input.indexState.partitions.get(i);
17706
+ targets.push({
17707
+ partitionId: i,
17708
+ lastSeqNo: partition?.appliedSeqNo ?? 0,
17709
+ primaryTerm: partition?.seqOwner.primaryTerm ?? SINGLE_NODE_PRIMARY_TERM
17710
+ });
17711
+ }
17712
+ for (let i = 0; i < manager.partitionCount; i += 1) {
17713
+ const partition = input.indexState.partitions.get(i);
17714
+ if (partition === void 0) {
17715
+ continue;
17716
+ }
17717
+ try {
17718
+ await partition.walWriter.commit();
17719
+ } catch (error) {
17720
+ partition.failed = toError2(error);
17721
+ input.markFatal(partition.failed);
17722
+ throw partition.failed;
17723
+ }
17724
+ }
17725
+ const writtenDocumentCount = await writeIndexCheckpoint({
17726
+ directory: input.directory,
17727
+ metadata,
17728
+ targets,
17729
+ compactionThreshold: input.compactionThreshold,
17730
+ manager,
17731
+ canOffload: input.canOffload,
17732
+ fromMemory: input.fromMemory
17733
+ });
17734
+ const checkpointDocumentCount = writtenDocumentCount ?? documentCount;
17735
+ await input.queueMetadataWrite(input.indexName, async () => {
17736
+ const checkpointMetadata = input.hooks.buildMetadata(input.indexName, checkpointDocumentCount);
17737
+ if (checkpointMetadata === void 0) {
17738
+ return;
17739
+ }
17740
+ const bytes = await writeMetadataEnvelope(checkpointMetadata, { checksum: true });
17741
+ await input.directory.atomicWrite(`${input.indexName}/meta`, bytes);
17742
+ input.hooks.recordCheckpoint?.(input.indexName, checkpointDocumentCount, manager.partitionCount);
17743
+ });
17744
+ await truncateCoveredSegments(input.directory, input.indexName, targets);
17745
+ await reclaimWalBeyondCount(
17746
+ input.directory,
17747
+ input.indexName,
17748
+ manager.partitionCount,
17749
+ input.indexState.partitions,
17750
+ input.markFatal
17751
+ );
17752
+ input.indexState.mutationsSinceCheckpoint = 0;
17753
+ }
17754
+
17755
+ // src/persistence/durability/manager-state.ts
17756
+ async function drainIndexStateForUnload(indexState, pendingMetadataWrite, closeWalWriter) {
17757
+ indexState.unloading = true;
17758
+ try {
17759
+ if (indexState.checkpointInFlight !== null) await indexState.checkpointInFlight;
17760
+ await pendingMetadataWrite;
17761
+ for (const partition of indexState.partitions.values()) {
17762
+ await closeWalWriter(partition.walWriter.close());
17763
+ }
17764
+ } catch (error) {
17765
+ indexState.unloading = false;
17766
+ throw error;
17767
+ }
17768
+ }
17769
+
17770
+ // src/persistence/durability/checkpoint-count.ts
17771
+ async function countCheckpointDocuments(directory, indexName) {
17772
+ const manifest = await readSegmentManifest(directory, indexName);
17773
+ if (manifest !== null) {
17774
+ let total = 0;
17775
+ for (const partition of manifest.partitions) {
17776
+ const ordered = [];
17777
+ for (const segment of partition.segments) {
17778
+ ordered.push(await readSegmentContents(directory, segment.key));
17779
+ }
17780
+ total += countLiveDocuments(ordered);
17781
+ }
17782
+ return total;
17783
+ }
17784
+ const bytes = await directory.read(legacySnapshotKey(indexName));
17785
+ if (bytes === null) {
17786
+ return 0;
17787
+ }
17788
+ return countSnapshotBundleDocuments(bytes);
17789
+ }
17790
+ async function countSnapshotBundleDocuments(bytes) {
17791
+ const bundle = await decodeSnapshotBundle(bytes);
17792
+ let total = 0;
17793
+ for (const partitionBytes of bundle.partitions) {
17794
+ total += Object.keys(deserializePayloadV2(partitionBytes).documents).length;
17795
+ }
17796
+ return total;
17797
+ }
17798
+
17799
+ // src/persistence/durability/recover-index.ts
17800
+ async function recoverPersistedIndex(deps, indexName, metadataOnly) {
17801
+ const { directory, hooks } = deps;
17802
+ const metadata = await loadMetadata(directory, indexName);
17803
+ if (metadata === null) {
17804
+ return;
17805
+ }
17806
+ let derivedCount;
17807
+ if (metadataOnly && metadata.documentCount === void 0) {
17808
+ try {
17809
+ derivedCount = await countCheckpointDocuments(directory, indexName);
17810
+ metadata.documentCount = derivedCount;
17811
+ } catch {
17812
+ derivedCount = void 0;
17813
+ }
17814
+ }
17815
+ await hooks.createIndexFromMetadata(metadata, !metadataOnly);
17816
+ if (metadataOnly) {
17817
+ if (derivedCount !== void 0) {
17818
+ await deps.queueMetadataWrite(indexName, async () => {
17819
+ const upgraded = hooks.buildMetadata(indexName, derivedCount);
17820
+ if (upgraded === void 0) {
17821
+ return;
17822
+ }
17823
+ const bytes = await writeMetadataEnvelope(upgraded, { checksum: true });
17824
+ await directory.atomicWrite(`${indexName}/meta`, bytes);
17825
+ }).catch(() => void 0);
17826
+ }
17827
+ return;
17828
+ }
17829
+ const manager = hooks.getManager(indexName);
17830
+ if (manager === void 0) {
17831
+ return;
17832
+ }
17833
+ const replayDeps = {
17834
+ manager,
17835
+ vectorFieldPaths: hooks.getVectorFieldPaths(indexName),
17836
+ vectorIndexes: hooks.getVectorIndexes(indexName)
17837
+ };
17838
+ const checkpoint = await loadSnapshot(directory, indexName, replayDeps);
17839
+ const manifest = await readSegmentManifest(directory, indexName);
17840
+ if (manifest !== null) {
17841
+ await reclaimOrphanedSegments(directory, indexName, manifest);
17842
+ }
17843
+ for (let partitionId = 0; partitionId < manager.partitionCount; partitionId += 1) {
17844
+ const fromSeqNo = snapshotCheckpointFor(checkpoint, partitionId);
17845
+ const { highestSeqNo } = await replayWal(directory, indexName, partitionId, fromSeqNo, replayDeps);
17846
+ deps.registerPartition(indexName, partitionId, highestSeqNo);
17847
+ }
17848
+ }
17849
+
17581
17850
  // src/persistence/durability/types.ts
17582
17851
  var DEFAULT_CHECKPOINT_INTERVAL_MS = 3e5;
17583
17852
  var DEFAULT_CHECKPOINT_MUTATION_THRESHOLD = 1e5;
17853
+ var DEFAULT_ASYNC_FLUSH_INTERVAL_MS = 1e3;
17584
17854
 
17585
17855
  // src/persistence/durability/group-commit.ts
17586
17856
  function createGroupCommitCoordinator(sync) {
@@ -17826,7 +18096,6 @@ function createWalWriter(directory, config) {
17826
18096
  }
17827
18097
 
17828
18098
  // src/persistence/durability/manager.ts
17829
- var DEFAULT_ASYNC_FLUSH_INTERVAL_MS = 1e3;
17830
18099
  function createDurabilityManager(config, hooks, directoryOverride) {
17831
18100
  const directory = createDurableDirectory(config.directory ?? "");
17832
18101
  const canOffloadCheckpoint = directoryOverride === void 0;
@@ -17857,20 +18126,20 @@ function createDurabilityManager(config, hooks, directoryOverride) {
17857
18126
  }
17858
18127
  hooks.onFatalError(error);
17859
18128
  }
17860
- function toError3(err) {
18129
+ function toError4(err) {
17861
18130
  return err instanceof Error ? err : new Error(String(err));
17862
18131
  }
17863
18132
  async function closeWriterReportingFailure(closePromise) {
17864
18133
  try {
17865
18134
  await closePromise;
17866
18135
  } catch (err) {
17867
- markFatal(toError3(err));
18136
+ markFatal(toError4(err));
17868
18137
  }
17869
18138
  }
17870
18139
  function getOrCreateIndexState(indexName) {
17871
18140
  let state = indexes.get(indexName);
17872
18141
  if (state === void 0) {
17873
- state = { partitions: /* @__PURE__ */ new Map(), mutationsSinceCheckpoint: 0, checkpointInFlight: null };
18142
+ state = { partitions: /* @__PURE__ */ new Map(), mutationsSinceCheckpoint: 0, checkpointInFlight: null, unloading: false };
17874
18143
  indexes.set(indexName, state);
17875
18144
  }
17876
18145
  return state;
@@ -17933,7 +18202,7 @@ function createDurabilityManager(config, hooks, directoryOverride) {
17933
18202
  try {
17934
18203
  await partition.walWriter.commit();
17935
18204
  } catch (err) {
17936
- partition.failed = toError3(err);
18205
+ partition.failed = toError4(err);
17937
18206
  markFatal(partition.failed);
17938
18207
  }
17939
18208
  }
@@ -17947,7 +18216,7 @@ function createDurabilityManager(config, hooks, directoryOverride) {
17947
18216
  try {
17948
18217
  await checkpointIndex(indexName);
17949
18218
  } catch (err) {
17950
- markFatal(toError3(err));
18219
+ markFatal(toError4(err));
17951
18220
  }
17952
18221
  }
17953
18222
  }
@@ -17964,7 +18233,7 @@ function createDurabilityManager(config, hooks, directoryOverride) {
17964
18233
  }
17965
18234
  async function checkpointIndex(indexName, fromMemory = false) {
17966
18235
  const indexState = indexes.get(indexName);
17967
- if (indexState === void 0) {
18236
+ if (indexState === void 0 || indexState.unloading) {
17968
18237
  return;
17969
18238
  }
17970
18239
  if (indexState.checkpointInFlight !== null) {
@@ -17980,82 +18249,46 @@ function createDurabilityManager(config, hooks, directoryOverride) {
17980
18249
  return run;
17981
18250
  }
17982
18251
  async function performCheckpoint(indexName, indexState, fromMemory = false) {
17983
- const manager = hooks.getManager(indexName);
17984
- if (manager === void 0) {
17985
- return;
17986
- }
17987
- const metadata = hooks.buildMetadata(indexName);
17988
- if (metadata === void 0) {
17989
- return;
17990
- }
17991
- const targets = [];
17992
- for (let i = 0; i < manager.partitionCount; i += 1) {
17993
- const partition = indexState.partitions.get(i);
17994
- targets.push({
17995
- partitionId: i,
17996
- lastSeqNo: partition?.appliedSeqNo ?? 0,
17997
- primaryTerm: partition?.seqOwner.primaryTerm ?? SINGLE_NODE_PRIMARY_TERM
17998
- });
17999
- if (partition !== void 0) {
18000
- try {
18001
- await partition.walWriter.commit();
18002
- } catch (err) {
18003
- partition.failed = toError3(err);
18004
- markFatal(partition.failed);
18005
- throw partition.failed;
18006
- }
18007
- }
18008
- }
18009
- await writeIndexCheckpoint({
18252
+ await runDurableCheckpoint({
18010
18253
  directory,
18011
- metadata,
18012
- targets,
18254
+ hooks,
18255
+ indexName,
18256
+ indexState,
18013
18257
  compactionThreshold,
18014
- manager,
18015
18258
  canOffload: canOffloadCheckpoint,
18016
- fromMemory
18259
+ fromMemory,
18260
+ queueMetadataWrite,
18261
+ markFatal
18017
18262
  });
18018
- await truncateCoveredSegments(directory, indexName, targets);
18019
- await reclaimWalBeyondCount(directory, indexName, manager.partitionCount, indexState.partitions, markFatal);
18020
- indexState.mutationsSinceCheckpoint = 0;
18021
18263
  }
18022
- async function recoverIndex(indexName) {
18023
- const metadata = await loadMetadata(directory, indexName);
18024
- if (metadata === null) {
18025
- return;
18026
- }
18027
- await hooks.createIndexFromMetadata(metadata);
18028
- const manager = hooks.getManager(indexName);
18029
- if (manager === void 0) {
18030
- return;
18031
- }
18032
- const deps = {
18033
- manager,
18034
- vectorFieldPaths: hooks.getVectorFieldPaths(indexName),
18035
- vectorIndexes: hooks.getVectorIndexes(indexName)
18036
- };
18037
- const checkpoint = await loadSnapshot(directory, indexName, deps);
18038
- const manifest = await readSegmentManifest(directory, indexName);
18039
- if (manifest !== null) {
18040
- await reclaimOrphanedSegments(directory, indexName, manifest);
18041
- }
18042
- for (let partitionId = 0; partitionId < manager.partitionCount; partitionId += 1) {
18043
- const fromSeqNo = snapshotCheckpointFor(checkpoint, partitionId);
18044
- const { highestSeqNo } = await replayWal(directory, indexName, partitionId, fromSeqNo, deps);
18045
- getOrCreatePartition(indexName, partitionId, highestSeqNo);
18046
- }
18264
+ function recoverIndex(indexName, metadataOnly = false) {
18265
+ return recoverPersistedIndex(
18266
+ {
18267
+ directory,
18268
+ hooks,
18269
+ registerPartition: (name, partitionId, startSeqNo) => {
18270
+ getOrCreatePartition(name, partitionId, startSeqNo);
18271
+ },
18272
+ queueMetadataWrite
18273
+ },
18274
+ indexName,
18275
+ metadataOnly
18276
+ );
18047
18277
  }
18048
18278
  return {
18049
18279
  isActive() {
18050
18280
  return true;
18051
18281
  },
18052
- async recover() {
18282
+ async recover(metadataOnly = false) {
18053
18283
  const names = await listPersistedIndexes(directory);
18054
18284
  for (const indexName of names) {
18055
- await recoverIndex(indexName);
18285
+ await recoverIndex(indexName, metadataOnly);
18056
18286
  }
18057
18287
  startCheckpointTimer();
18058
18288
  },
18289
+ recoverIndex(indexName) {
18290
+ return recoverIndex(indexName);
18291
+ },
18059
18292
  highestPersistedSeqNo(indexName, partitionId) {
18060
18293
  return indexes.get(indexName)?.partitions.get(partitionId)?.appliedSeqNo ?? 0;
18061
18294
  },
@@ -18079,7 +18312,7 @@ function createDurabilityManager(config, hooks, directoryOverride) {
18079
18312
  const entry = buildMutationEntry(record, allocatedSeqNo);
18080
18313
  await partition.walWriter.append(entry);
18081
18314
  } catch (err) {
18082
- partition.failed = toError3(err);
18315
+ partition.failed = toError4(err);
18083
18316
  markFatal(partition.failed);
18084
18317
  throw partition.failed;
18085
18318
  }
@@ -18091,7 +18324,7 @@ function createDurabilityManager(config, hooks, directoryOverride) {
18091
18324
  try {
18092
18325
  await partition.walWriter.commit();
18093
18326
  } catch (err) {
18094
- partition.failed = toError3(err);
18327
+ partition.failed = toError4(err);
18095
18328
  markFatal(partition.failed);
18096
18329
  throw partition.failed;
18097
18330
  }
@@ -18101,7 +18334,7 @@ function createDurabilityManager(config, hooks, directoryOverride) {
18101
18334
  startAsyncFlushTimer();
18102
18335
  if (indexState.mutationsSinceCheckpoint >= checkpointMutationThreshold) {
18103
18336
  void checkpointIndex(record.indexName).catch((err) => {
18104
- markFatal(toError3(err));
18337
+ markFatal(toError4(err));
18105
18338
  });
18106
18339
  }
18107
18340
  return allocatedSeqNo;
@@ -18131,15 +18364,23 @@ function createDurabilityManager(config, hooks, directoryOverride) {
18131
18364
  async removeIndex(indexName) {
18132
18365
  const indexState = indexes.get(indexName);
18133
18366
  if (indexState !== void 0) {
18134
- for (const partition of indexState.partitions.values()) {
18135
- await closeWriterReportingFailure(partition.walWriter.close());
18136
- }
18367
+ await drainIndexStateForUnload(indexState, metadataWrites.get(indexName), closeWriterReportingFailure);
18137
18368
  indexes.delete(indexName);
18369
+ metadataWrites.delete(indexName);
18138
18370
  }
18139
18371
  for (const key of await directory.list(`${indexName}/`)) {
18140
18372
  await directory.remove(key);
18141
18373
  }
18142
18374
  },
18375
+ async unloadIndex(indexName) {
18376
+ const indexState = indexes.get(indexName);
18377
+ if (indexState === void 0) {
18378
+ return;
18379
+ }
18380
+ await drainIndexStateForUnload(indexState, metadataWrites.get(indexName), (close) => close);
18381
+ indexes.delete(indexName);
18382
+ metadataWrites.delete(indexName);
18383
+ },
18143
18384
  async shutdown() {
18144
18385
  shuttingDown = true;
18145
18386
  if (checkpointTimer !== null) {
@@ -18178,7 +18419,13 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
18178
18419
  function getOrCreateIndexState(indexName) {
18179
18420
  let state = indexes.get(indexName);
18180
18421
  if (state === void 0) {
18181
- state = { mutationsSinceCheckpoint: 0, checkpointInFlight: null, applyChain: Promise.resolve() };
18422
+ state = {
18423
+ mutationsSinceCheckpoint: 0,
18424
+ checkpointInFlight: null,
18425
+ applyChain: Promise.resolve(),
18426
+ unloading: false,
18427
+ appliedSeqNoByPartition: /* @__PURE__ */ new Map()
18428
+ };
18182
18429
  indexes.set(indexName, state);
18183
18430
  }
18184
18431
  return state;
@@ -18209,10 +18456,11 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
18209
18456
  if (manager === void 0 || metadata === void 0) {
18210
18457
  return;
18211
18458
  }
18459
+ const documentCount = manager.countDocuments();
18212
18460
  const seqNoByPartition = /* @__PURE__ */ new Map();
18213
18461
  const primaryTermByPartition = /* @__PURE__ */ new Map();
18214
18462
  for (let i = 0; i < manager.partitionCount; i += 1) {
18215
- seqNoByPartition.set(i, 0);
18463
+ seqNoByPartition.set(i, indexState.appliedSeqNoByPartition.get(i) ?? 0);
18216
18464
  primaryTermByPartition.set(i, SINGLE_NODE_PRIMARY_TERM);
18217
18465
  }
18218
18466
  const { parts } = await buildSnapshotBundleBytes({
@@ -18228,6 +18476,13 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
18228
18476
  primaryTermByPartition
18229
18477
  });
18230
18478
  await adapter.save(snapshotStorageKey(indexName), concatEnvelopeParts(parts));
18479
+ await queueMetadataWrite(indexName, async () => {
18480
+ const checkpointMetadata = hooks.buildMetadata(indexName, documentCount);
18481
+ if (checkpointMetadata === void 0) return;
18482
+ const metadataBytes = await writeMetadataEnvelope(checkpointMetadata, { checksum: true });
18483
+ await adapter.save(metadataKey(indexName), metadataBytes);
18484
+ hooks.recordCheckpoint?.(indexName, documentCount, manager.partitionCount);
18485
+ });
18231
18486
  indexState.mutationsSinceCheckpoint = 0;
18232
18487
  if (publisher !== void 0) {
18233
18488
  const partitionIds = [];
@@ -18237,15 +18492,18 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
18237
18492
  await publisher.publishPartitions(indexName, partitionIds);
18238
18493
  }
18239
18494
  }
18240
- async function checkpointIndex(indexName) {
18495
+ async function checkpointIndex(indexName, forceFresh = false) {
18241
18496
  const indexState = indexes.get(indexName);
18242
- if (indexState === void 0) {
18497
+ if (indexState === void 0 || indexState.unloading) {
18243
18498
  return;
18244
18499
  }
18245
18500
  if (indexState.checkpointInFlight !== null) {
18246
- return indexState.checkpointInFlight;
18501
+ await indexState.checkpointInFlight;
18502
+ if (!forceFresh) {
18503
+ return;
18504
+ }
18247
18505
  }
18248
- const run = performCheckpoint(indexName, indexState).finally(() => {
18506
+ const run = indexState.applyChain.then(() => performCheckpoint(indexName, indexState)).finally(() => {
18249
18507
  indexState.checkpointInFlight = null;
18250
18508
  });
18251
18509
  indexState.checkpointInFlight = run;
@@ -18260,13 +18518,35 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
18260
18518
  );
18261
18519
  return queued;
18262
18520
  }
18263
- async function recoverIndex(indexName) {
18521
+ async function recoverIndex(indexName, metadataOnly = false) {
18264
18522
  const metaBytes = await adapter.load(metadataKey(indexName));
18265
18523
  if (metaBytes === null) {
18266
18524
  return;
18267
18525
  }
18268
18526
  const { metadata } = await readMetadataEnvelope(metaBytes);
18269
- await hooks.createIndexFromMetadata(metadata);
18527
+ let derivedCount;
18528
+ if (metadataOnly && metadata.documentCount === void 0) {
18529
+ try {
18530
+ derivedCount = await countStoredBundleDocuments(indexName);
18531
+ metadata.documentCount = derivedCount;
18532
+ } catch {
18533
+ derivedCount = void 0;
18534
+ }
18535
+ }
18536
+ await hooks.createIndexFromMetadata(metadata, !metadataOnly);
18537
+ if (metadataOnly) {
18538
+ if (derivedCount !== void 0) {
18539
+ await queueMetadataWrite(indexName, async () => {
18540
+ const upgraded = hooks.buildMetadata(indexName, derivedCount);
18541
+ if (upgraded === void 0) {
18542
+ return;
18543
+ }
18544
+ const bytes = await writeMetadataEnvelope(upgraded, { checksum: true });
18545
+ await adapter.save(metadataKey(indexName), bytes);
18546
+ }).catch(() => void 0);
18547
+ }
18548
+ return;
18549
+ }
18270
18550
  const manager = hooks.getManager(indexName);
18271
18551
  if (manager === void 0) {
18272
18552
  return;
@@ -18275,33 +18555,58 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
18275
18555
  if (snapshotBytes === null) {
18276
18556
  return;
18277
18557
  }
18278
- await loadSnapshotBundleBytes(snapshotBytes, {
18558
+ const checkpoint = await loadSnapshotBundleBytes(snapshotBytes, {
18279
18559
  manager,
18280
18560
  vectorFieldPaths: hooks.getVectorFieldPaths(indexName),
18281
18561
  vectorIndexes: hooks.getVectorIndexes(indexName)
18282
18562
  });
18563
+ const indexState = getOrCreateIndexState(indexName);
18564
+ for (let partitionId = 0; partitionId < manager.partitionCount; partitionId += 1) {
18565
+ const persistedSeqNo = snapshotCheckpointFor(checkpoint, partitionId);
18566
+ if (persistedSeqNo > 0) {
18567
+ indexState.appliedSeqNoByPartition.set(partitionId, persistedSeqNo);
18568
+ }
18569
+ }
18570
+ }
18571
+ async function countStoredBundleDocuments(indexName) {
18572
+ const snapshotBytes = await adapter.load(snapshotStorageKey(indexName));
18573
+ if (snapshotBytes === null) {
18574
+ return 0;
18575
+ }
18576
+ return countSnapshotBundleDocuments(snapshotBytes);
18283
18577
  }
18284
18578
  return {
18285
18579
  isActive() {
18286
18580
  return true;
18287
18581
  },
18288
- async recover() {
18582
+ async recover(metadataOnly = false) {
18289
18583
  const keys = await adapter.list("");
18290
18584
  for (const key of keys) {
18291
18585
  if (key.endsWith("/meta")) {
18292
- await recoverIndex(key.slice(0, -"/meta".length));
18586
+ await recoverIndex(key.slice(0, -"/meta".length), metadataOnly);
18293
18587
  }
18294
18588
  }
18295
18589
  startCheckpointTimer();
18296
18590
  },
18297
- highestPersistedSeqNo() {
18298
- return 0;
18591
+ recoverIndex(indexName) {
18592
+ return recoverIndex(indexName);
18593
+ },
18594
+ highestPersistedSeqNo(indexName, partitionId) {
18595
+ return indexes.get(indexName)?.appliedSeqNoByPartition.get(partitionId) ?? 0;
18299
18596
  },
18300
18597
  async recordMutation(record) {
18301
18598
  const indexState = getOrCreateIndexState(record.indexName);
18302
- const buffered = indexState.applyChain.then(() => record.apply());
18303
- indexState.applyChain = buffered.catch(() => void 0);
18304
- await buffered;
18599
+ const buffered = indexState.applyChain.then(async () => {
18600
+ await record.apply();
18601
+ const appliedSeqNo2 = (indexState.appliedSeqNoByPartition.get(record.partitionId) ?? 0) + 1;
18602
+ indexState.appliedSeqNoByPartition.set(record.partitionId, appliedSeqNo2);
18603
+ return appliedSeqNo2;
18604
+ });
18605
+ indexState.applyChain = buffered.then(
18606
+ () => void 0,
18607
+ () => void 0
18608
+ );
18609
+ const appliedSeqNo = await buffered;
18305
18610
  indexState.mutationsSinceCheckpoint += 1;
18306
18611
  startCheckpointTimer();
18307
18612
  if (indexState.mutationsSinceCheckpoint >= checkpointMutationThreshold) {
@@ -18309,7 +18614,7 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
18309
18614
  hooks.onFatalError(err instanceof Error ? err : new Error(String(err)));
18310
18615
  });
18311
18616
  }
18312
- return 0;
18617
+ return appliedSeqNo;
18313
18618
  },
18314
18619
  persistMetadata(indexName) {
18315
18620
  return queueMetadataWrite(indexName, async () => {
@@ -18325,17 +18630,49 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
18325
18630
  checkpoint(indexName) {
18326
18631
  return checkpointIndex(indexName);
18327
18632
  },
18633
+ checkpointFromMemory(indexName) {
18634
+ return checkpointIndex(indexName, true);
18635
+ },
18328
18636
  async checkpointAll() {
18329
18637
  for (const indexName of [...indexes.keys()]) {
18330
18638
  await checkpointIndex(indexName);
18331
18639
  }
18332
18640
  },
18333
18641
  async removeIndex(indexName) {
18642
+ const indexState = indexes.get(indexName);
18643
+ if (indexState !== void 0) {
18644
+ indexState.unloading = true;
18645
+ try {
18646
+ await indexState.applyChain;
18647
+ if (indexState.checkpointInFlight !== null) await indexState.checkpointInFlight;
18648
+ await metadataWrites.get(indexName);
18649
+ } catch (error) {
18650
+ indexState.unloading = false;
18651
+ throw error;
18652
+ }
18653
+ }
18334
18654
  indexes.delete(indexName);
18655
+ metadataWrites.delete(indexName);
18335
18656
  for (const key of await adapter.list(`${indexName}/`)) {
18336
18657
  await adapter.delete(key);
18337
18658
  }
18338
18659
  },
18660
+ async unloadIndex(indexName) {
18661
+ const indexState = indexes.get(indexName);
18662
+ if (indexState !== void 0) {
18663
+ indexState.unloading = true;
18664
+ try {
18665
+ await indexState.applyChain;
18666
+ if (indexState.checkpointInFlight !== null) await indexState.checkpointInFlight;
18667
+ await metadataWrites.get(indexName);
18668
+ } catch (error) {
18669
+ indexState.unloading = false;
18670
+ throw error;
18671
+ }
18672
+ }
18673
+ indexes.delete(indexName);
18674
+ metadataWrites.delete(indexName);
18675
+ },
18339
18676
  async reloadIndex(indexName) {
18340
18677
  const indexState = getOrCreateIndexState(indexName);
18341
18678
  const reload = indexState.applyChain.then(() => recoverIndex(indexName));
@@ -18359,21 +18696,22 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
18359
18696
  }
18360
18697
 
18361
18698
  // src/engine/durability-integration.ts
18362
- function buildMetadata(indexName, hooks) {
18699
+ function buildMetadata(indexName, hooks, documentCount) {
18363
18700
  const config = hooks.getIndexConfig(indexName);
18364
18701
  const manager = hooks.getManager(indexName);
18365
- if (config === void 0 || manager === void 0) {
18702
+ if (config === void 0) {
18366
18703
  return void 0;
18367
18704
  }
18368
18705
  const metadata = {
18369
18706
  indexName,
18370
18707
  schema: config.schema,
18371
18708
  language: config.language,
18372
- partitionCount: manager.partitionCount,
18709
+ partitionCount: manager?.partitionCount ?? config.partitionCount,
18373
18710
  bm25Params: { k1: config.k1, b: config.b },
18374
18711
  createdAt: Date.now(),
18375
18712
  engineVersion: VERSION
18376
18713
  };
18714
+ metadata.documentCount = documentCount ?? config.documentCount;
18377
18715
  if (config.embedding) {
18378
18716
  metadata.embedding = config.embedding;
18379
18717
  }
@@ -18423,8 +18761,9 @@ function createDurabilityIntegration(tier, hooks) {
18423
18761
  getManager: hooks.getManager,
18424
18762
  getVectorFieldPaths: hooks.getVectorFieldPaths,
18425
18763
  getVectorIndexes: hooks.getVectorIndexes,
18426
- buildMetadata: (indexName) => buildMetadata(indexName, hooks),
18764
+ buildMetadata: (indexName, documentCount) => buildMetadata(indexName, hooks, documentCount),
18427
18765
  createIndexFromMetadata: hooks.createIndexFromMetadata,
18766
+ recordCheckpoint: hooks.recordCheckpoint,
18428
18767
  onFatalError: hooks.onFatalError
18429
18768
  };
18430
18769
  const manager = tier.kind === "wal" ? createDurabilityManager(tier.config, managerHooks) : createSnapshotOnlyManager(tier.adapter, tier.config, managerHooks, hooks.checkpointPublisher);
@@ -18463,9 +18802,9 @@ function createDurabilityFromTier(tier, wiring) {
18463
18802
  }
18464
18803
  return createDurabilityIntegration(tier, {
18465
18804
  checkpointPublisher: { publishPartitions: wiring.publishCheckpointedPartitions },
18466
- getManager: (indexName) => wiring.indexRegistry.has(indexName) ? wiring.requireManager(indexName) : void 0,
18805
+ getManager: (indexName) => wiring.indexRegistry.has(indexName) ? wiring.getManager(indexName) : void 0,
18467
18806
  getVectorFieldPaths: (indexName) => wiring.indexRegistry.get(indexName)?.vectorFieldPaths ?? /* @__PURE__ */ new Set(),
18468
- getVectorIndexes: (indexName) => wiring.indexRegistry.has(indexName) ? wiring.requireManager(indexName).getVectorIndexes() : /* @__PURE__ */ new Map(),
18807
+ getVectorIndexes: (indexName) => (wiring.indexRegistry.has(indexName) ? wiring.getManager(indexName)?.getVectorIndexes() : void 0) ?? /* @__PURE__ */ new Map(),
18469
18808
  getIndexConfig: (indexName) => {
18470
18809
  const entry = wiring.indexRegistry.get(indexName);
18471
18810
  if (entry === void 0) {
@@ -18478,6 +18817,8 @@ function createDurabilityFromTier(tier, wiring) {
18478
18817
  return {
18479
18818
  ...entry.indexUuid !== null ? { indexUuid: entry.indexUuid } : {},
18480
18819
  ...entry.heldPartitions !== null ? { heldPartitions: entry.heldPartitions } : {},
18820
+ documentCount: entry.documentCount,
18821
+ partitionCount: entry.partitionCount,
18481
18822
  schema: flattenSchema(entry.config.schema),
18482
18823
  language: entry.language.name,
18483
18824
  k1: entry.config.bm25?.k1 ?? 1.2,
@@ -18497,10 +18838,401 @@ function createDurabilityFromTier(tier, wiring) {
18497
18838
  };
18498
18839
  },
18499
18840
  createIndexFromMetadata: wiring.createIndexFromMetadata,
18841
+ recordCheckpoint: wiring.recordCheckpoint,
18500
18842
  onFatalError: wiring.emitFatalError
18501
18843
  });
18502
18844
  }
18503
18845
 
18846
+ // src/engine/index-state/entry.ts
18847
+ var DEFAULT_MAX_REOPEN_WAITERS = 64;
18848
+ var MAX_REOPEN_FAILURES = 5;
18849
+ var MIN_SWEEP_INTERVAL_MS = 100;
18850
+ var MAX_SWEEP_INTERVAL_MS = 6e4;
18851
+ function createLifecycleEntry(state) {
18852
+ return {
18853
+ state,
18854
+ lastAccessAt: Date.now(),
18855
+ activeOperations: 0,
18856
+ reopenCount: 0,
18857
+ reopenFailures: 0,
18858
+ nextReopenAt: 0,
18859
+ cachedError: null,
18860
+ reopenPromise: null,
18861
+ waiterCount: 0,
18862
+ closePromise: null,
18863
+ dropPromise: null,
18864
+ drainResolvers: /* @__PURE__ */ new Set()
18865
+ };
18866
+ }
18867
+ function validateLimit(name, value, allowZero = false) {
18868
+ if (value === void 0) return;
18869
+ const minimum = allowZero ? 0 : 1;
18870
+ if (!Number.isSafeInteger(value) || value < minimum) {
18871
+ const requirement = allowZero ? "a non-negative safe integer" : "a positive safe integer";
18872
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, `${name} must be ${requirement}`, {
18873
+ field: name,
18874
+ value
18875
+ });
18876
+ }
18877
+ }
18878
+ function cachedRecoveryError(error, indexName) {
18879
+ if (error instanceof NarsilError) return error;
18880
+ return new NarsilError(ErrorCodes.PERSISTENCE_LOAD_FAILED, `Index "${indexName}" could not be reopened`, {
18881
+ indexName,
18882
+ cause: error instanceof Error ? error.message : String(error)
18883
+ });
18884
+ }
18885
+
18886
+ // src/engine/index-state/index.ts
18887
+ function createIndexStateCoordinator(config, callbacks) {
18888
+ validateLimit("lifecycle.idleTimeoutMs", config?.idleTimeoutMs);
18889
+ validateLimit("lifecycle.maxOpenIndexes", config?.maxOpenIndexes);
18890
+ validateLimit("lifecycle.maxOpenBytes", config?.maxOpenBytes);
18891
+ validateLimit("lifecycle.maxReopenWaiters", config?.maxReopenWaiters, true);
18892
+ const entries = /* @__PURE__ */ new Map();
18893
+ const waiterLimit = config?.maxReopenWaiters ?? DEFAULT_MAX_REOPEN_WAITERS;
18894
+ let sweepTimer = null;
18895
+ function entryOf(indexName) {
18896
+ const entry = entries.get(indexName);
18897
+ if (entry === void 0) {
18898
+ throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, `Index "${indexName}" does not exist`, { indexName });
18899
+ }
18900
+ return entry;
18901
+ }
18902
+ function publicState(entry) {
18903
+ return entry.state === "closing" || entry.state === "dropping" ? "open" : entry.state;
18904
+ }
18905
+ async function waitForDrain(entry) {
18906
+ if (entry.activeOperations === 0) return;
18907
+ await new Promise((resolve) => entry.drainResolvers.add(resolve));
18908
+ }
18909
+ function markClosed(entry) {
18910
+ entry.state = "closed";
18911
+ entry.cachedError = null;
18912
+ entry.reopenFailures = 0;
18913
+ }
18914
+ function releaseEntry(entry) {
18915
+ entry.activeOperations -= 1;
18916
+ if (entry.activeOperations !== 0) return;
18917
+ for (const resolve of entry.drainResolvers) resolve();
18918
+ entry.drainResolvers.clear();
18919
+ }
18920
+ async function waitForTransition(indexName, entry, transition) {
18921
+ if (entry.waiterCount >= waiterLimit) {
18922
+ throw new NarsilError(
18923
+ ErrorCodes.INDEX_REOPEN_CAPACITY_EXHAUSTED,
18924
+ `Index "${indexName}" has reached its reopen waiter limit`,
18925
+ { indexName, maximum: waiterLimit }
18926
+ );
18927
+ }
18928
+ entry.waiterCount += 1;
18929
+ try {
18930
+ await transition;
18931
+ } finally {
18932
+ entry.waiterCount -= 1;
18933
+ }
18934
+ }
18935
+ async function awaitPendingTransitions(indexName) {
18936
+ let entry = entryOf(indexName);
18937
+ while (true) {
18938
+ if (entry.state === "dropping" && entry.dropPromise !== null) {
18939
+ await entry.dropPromise;
18940
+ entry = entryOf(indexName);
18941
+ continue;
18942
+ }
18943
+ if (entry.state === "closing" && entry.closePromise !== null) {
18944
+ await waitForTransition(
18945
+ indexName,
18946
+ entry,
18947
+ entry.closePromise.catch(() => void 0)
18948
+ );
18949
+ entry = entryOf(indexName);
18950
+ continue;
18951
+ }
18952
+ return entry;
18953
+ }
18954
+ }
18955
+ async function reopen(indexName, explicit) {
18956
+ const entry = await awaitPendingTransitions(indexName);
18957
+ if (entry.state === "open") return;
18958
+ if (explicit) {
18959
+ entry.reopenFailures = 0;
18960
+ entry.nextReopenAt = 0;
18961
+ entry.cachedError = null;
18962
+ if (entry.state === "reopen-failed") entry.state = "closed";
18963
+ }
18964
+ if (entry.state === "reopen-failed") throw entry.cachedError;
18965
+ if (entry.reopenPromise !== null) {
18966
+ await waitForTransition(indexName, entry, entry.reopenPromise);
18967
+ return;
18968
+ }
18969
+ if (!explicit && entry.cachedError !== null && Date.now() < entry.nextReopenAt) {
18970
+ throw entry.cachedError;
18971
+ }
18972
+ const run = callbacks.reopen(indexName).then(() => {
18973
+ entry.state = "open";
18974
+ entry.reopenCount += 1;
18975
+ entry.reopenFailures = 0;
18976
+ entry.nextReopenAt = 0;
18977
+ entry.cachedError = null;
18978
+ }).catch((error) => {
18979
+ entry.reopenFailures += 1;
18980
+ entry.cachedError = cachedRecoveryError(error, indexName);
18981
+ entry.nextReopenAt = Date.now() + 100 * 2 ** (entry.reopenFailures - 1);
18982
+ entry.state = entry.reopenFailures >= MAX_REOPEN_FAILURES ? "reopen-failed" : "closed";
18983
+ throw entry.cachedError;
18984
+ }).finally(() => {
18985
+ entry.reopenPromise = null;
18986
+ });
18987
+ entry.reopenPromise = run;
18988
+ await run;
18989
+ }
18990
+ async function closeEntry(indexName, automatic) {
18991
+ let entry = entryOf(indexName);
18992
+ while (true) {
18993
+ if (entry.state === "dropping" && entry.dropPromise !== null) {
18994
+ if (automatic) return false;
18995
+ await entry.dropPromise;
18996
+ entry = entryOf(indexName);
18997
+ continue;
18998
+ }
18999
+ if (entry.reopenPromise !== null) {
19000
+ if (automatic) return false;
19001
+ await entry.reopenPromise.catch(() => void 0);
19002
+ entry = entryOf(indexName);
19003
+ continue;
19004
+ }
19005
+ break;
19006
+ }
19007
+ if (entry.state === "closed" || entry.state === "reopen-failed") return true;
19008
+ if (entry.state === "closing" && entry.closePromise !== null) {
19009
+ if (!automatic) await entry.closePromise;
19010
+ return !automatic;
19011
+ }
19012
+ if (automatic && (entry.activeOperations > 0 || !callbacks.canCloseAutomatically(indexName))) return false;
19013
+ entry.state = "closing";
19014
+ let irreversible = false;
19015
+ const run = (async () => {
19016
+ await waitForDrain(entry);
19017
+ await callbacks.close(indexName, () => {
19018
+ irreversible = true;
19019
+ });
19020
+ markClosed(entry);
19021
+ })().catch((error) => {
19022
+ if (irreversible) markClosed(entry);
19023
+ else entry.state = "open";
19024
+ throw error;
19025
+ }).finally(() => {
19026
+ entry.closePromise = null;
19027
+ });
19028
+ entry.closePromise = run;
19029
+ if (automatic) {
19030
+ await run.catch(() => void 0);
19031
+ return publicState(entry) === "closed";
19032
+ }
19033
+ await run;
19034
+ return true;
19035
+ }
19036
+ async function dropEntry(indexName, action) {
19037
+ let entry = entryOf(indexName);
19038
+ while (true) {
19039
+ if (entry.dropPromise !== null) {
19040
+ await entry.dropPromise;
19041
+ return;
19042
+ }
19043
+ if (entry.reopenPromise !== null) {
19044
+ await entry.reopenPromise.catch(() => void 0);
19045
+ entry = entryOf(indexName);
19046
+ continue;
19047
+ }
19048
+ if (entry.closePromise !== null) {
19049
+ await entry.closePromise.catch(() => void 0);
19050
+ entry = entryOf(indexName);
19051
+ continue;
19052
+ }
19053
+ break;
19054
+ }
19055
+ const previousState = entry.state;
19056
+ entry.state = "dropping";
19057
+ const run = (async () => {
19058
+ await waitForDrain(entry);
19059
+ await action();
19060
+ entries.delete(indexName);
19061
+ })().catch((error) => {
19062
+ entry.state = previousState;
19063
+ throw error;
19064
+ }).finally(() => {
19065
+ entry.dropPromise = null;
19066
+ });
19067
+ entry.dropPromise = run;
19068
+ await run;
19069
+ }
19070
+ function exceedsLimits() {
19071
+ let open = 0;
19072
+ let bytes = 0;
19073
+ for (const [name, entry] of entries) {
19074
+ if (entry.state !== "open") continue;
19075
+ open += 1;
19076
+ bytes += callbacks.estimateBytes(name);
19077
+ }
19078
+ return config?.maxOpenIndexes !== void 0 && open > config.maxOpenIndexes || config?.maxOpenBytes !== void 0 && bytes > config.maxOpenBytes;
19079
+ }
19080
+ async function enforceLimits(protectedName) {
19081
+ if (config?.maxOpenIndexes === void 0 && config?.maxOpenBytes === void 0) return;
19082
+ while (exceedsLimits()) {
19083
+ const candidates = [...entries].filter(([name, entry]) => name !== protectedName && entry.state === "open" && entry.activeOperations === 0).sort((left, right) => left[1].lastAccessAt - right[1].lastAccessAt);
19084
+ const candidate = candidates[0];
19085
+ if (candidate === void 0 || !await closeEntry(candidate[0], true)) return;
19086
+ }
19087
+ }
19088
+ async function sweep() {
19089
+ const timeout = config?.idleTimeoutMs;
19090
+ if (timeout !== void 0 && timeout > 0) {
19091
+ const cutoff = Date.now() - timeout;
19092
+ for (const [name, entry] of entries) {
19093
+ if (entry.state === "open" && entry.lastAccessAt <= cutoff) await closeEntry(name, true);
19094
+ }
19095
+ }
19096
+ await enforceLimits();
19097
+ }
19098
+ if (config !== void 0) {
19099
+ const requested = config.idleTimeoutMs === void 0 ? MAX_SWEEP_INTERVAL_MS : config.idleTimeoutMs / 2;
19100
+ const interval = Math.min(MAX_SWEEP_INTERVAL_MS, Math.max(MIN_SWEEP_INTERVAL_MS, requested));
19101
+ sweepTimer = setInterval(() => void sweep().catch(() => void 0), interval);
19102
+ if (typeof sweepTimer.unref === "function") sweepTimer.unref();
19103
+ }
19104
+ return {
19105
+ async registerOpen(indexName) {
19106
+ entries.set(indexName, createLifecycleEntry("open"));
19107
+ await enforceLimits(indexName);
19108
+ },
19109
+ registerClosed(indexName) {
19110
+ entries.set(indexName, createLifecycleEntry("closed"));
19111
+ },
19112
+ forget(indexName) {
19113
+ entries.delete(indexName);
19114
+ },
19115
+ async acquire(indexName, markActive = true) {
19116
+ let entry = entryOf(indexName);
19117
+ const opened = entry.state !== "open";
19118
+ await reopen(indexName, false);
19119
+ entry = entryOf(indexName);
19120
+ while (entry.state !== "open") {
19121
+ await reopen(indexName, false);
19122
+ entry = entryOf(indexName);
19123
+ }
19124
+ if (markActive) entry.lastAccessAt = Date.now();
19125
+ entry.activeOperations += 1;
19126
+ try {
19127
+ if (opened) await enforceLimits(indexName);
19128
+ } catch (error) {
19129
+ releaseEntry(entry);
19130
+ throw error;
19131
+ }
19132
+ let released = false;
19133
+ return () => {
19134
+ if (released) return;
19135
+ released = true;
19136
+ releaseEntry(entry);
19137
+ };
19138
+ },
19139
+ async open(indexName) {
19140
+ await reopen(indexName, true);
19141
+ let entry = entryOf(indexName);
19142
+ while (entry.state !== "open") {
19143
+ await reopen(indexName, true);
19144
+ entry = entryOf(indexName);
19145
+ }
19146
+ entry.lastAccessAt = Date.now();
19147
+ entry.activeOperations += 1;
19148
+ try {
19149
+ await enforceLimits(indexName);
19150
+ } finally {
19151
+ releaseEntry(entry);
19152
+ }
19153
+ },
19154
+ async close(indexName) {
19155
+ await closeEntry(indexName, false);
19156
+ },
19157
+ drop(indexName, action) {
19158
+ return dropEntry(indexName, action);
19159
+ },
19160
+ stateOf(indexName) {
19161
+ return publicState(entryOf(indexName));
19162
+ },
19163
+ reopenCount(indexName) {
19164
+ return entryOf(indexName).reopenCount;
19165
+ },
19166
+ counts() {
19167
+ let open = 0;
19168
+ let closed = 0;
19169
+ let reopens = 0;
19170
+ for (const entry of entries.values()) {
19171
+ if (entry.state === "open" || entry.state === "closing") open += 1;
19172
+ else closed += 1;
19173
+ reopens += entry.reopenCount;
19174
+ }
19175
+ return { open, closed, reopens };
19176
+ },
19177
+ dispose() {
19178
+ if (sweepTimer !== null) clearInterval(sweepTimer);
19179
+ sweepTimer = null;
19180
+ entries.clear();
19181
+ }
19182
+ };
19183
+ }
19184
+
19185
+ // src/engine/index-state-wiring.ts
19186
+ function wireIndexState(wiring) {
19187
+ const { durability, executor, orchestrator, analysisRebuild, indexRegistry, rebalancingIndexes } = wiring;
19188
+ return createIndexStateCoordinator(wiring.config, {
19189
+ async reopen(indexName) {
19190
+ if (durability === null) {
19191
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, "An index needs durability before it can reopen");
19192
+ }
19193
+ try {
19194
+ await durability.manager.recoverIndex(indexName);
19195
+ await analysisRebuild.review(indexName);
19196
+ await orchestrator.openIndex(indexName);
19197
+ await wiring.onOpen?.(indexName);
19198
+ } catch (error) {
19199
+ await orchestrator.closeIndex(indexName).catch(() => void 0);
19200
+ await durability.manager.unloadIndex(indexName).catch(() => void 0);
19201
+ if (executor.getManager(indexName) !== void 0) executor.dropIndex(indexName);
19202
+ throw error;
19203
+ }
19204
+ },
19205
+ async close(indexName, markIrreversible) {
19206
+ if (durability === null) {
19207
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, "An index needs durability before it can close");
19208
+ }
19209
+ const manager = wiring.requireManager(indexName);
19210
+ const documentCount = manager.countDocuments();
19211
+ const partitionCount = manager.partitionCount;
19212
+ await (durability.manager.checkpointFromMemory?.(indexName) ?? durability.manager.checkpoint(indexName));
19213
+ await orchestrator.closeIndex(indexName);
19214
+ await durability.manager.unloadIndex(indexName);
19215
+ markIrreversible();
19216
+ try {
19217
+ await wiring.onClose?.(indexName);
19218
+ } finally {
19219
+ if (executor.getManager(indexName) !== void 0) executor.dropIndex(indexName);
19220
+ const entry = indexRegistry.get(indexName);
19221
+ if (entry !== void 0) {
19222
+ entry.documentCount = documentCount;
19223
+ entry.partitionCount = partitionCount;
19224
+ }
19225
+ }
19226
+ },
19227
+ canCloseAutomatically(indexName) {
19228
+ return !rebalancingIndexes.has(indexName) && !analysisRebuild.isRunning(indexName) && !orchestrator.isIndexBusy(indexName);
19229
+ },
19230
+ estimateBytes(indexName) {
19231
+ return executor.getManager(indexName)?.estimateMemoryBytes() ?? 0;
19232
+ }
19233
+ });
19234
+ }
19235
+
18504
19236
  // src/engine/invalidation.ts
18505
19237
  var STATISTICS_BROADCAST_INTERVAL_MS = 5e3;
18506
19238
  var FOREIGN_STATS_TTL_MS = 6e4;
@@ -18526,7 +19258,7 @@ function createInvalidationIntegration(deps) {
18526
19258
  const reloadChains = /* @__PURE__ */ new Map();
18527
19259
  let broadcastTimer = null;
18528
19260
  let stopped = false;
18529
- function toError3(err) {
19261
+ function toError4(err) {
18530
19262
  return err instanceof Error ? err : new Error(String(err));
18531
19263
  }
18532
19264
  function pruneStaleForeignStats(entries) {
@@ -18539,7 +19271,7 @@ function createInvalidationIntegration(deps) {
18539
19271
  }
18540
19272
  function scheduleReload(indexName) {
18541
19273
  const chain = reloadChains.get(indexName) ?? Promise.resolve();
18542
- const next = chain.then(() => reloadIndex(indexName)).catch((err) => onError(toError3(err)));
19274
+ const next = chain.then(() => reloadIndex(indexName)).catch((err) => onError(toError4(err)));
18543
19275
  reloadChains.set(indexName, next);
18544
19276
  }
18545
19277
  function storeForeignStats(indexName, sourceInstanceId, stats) {
@@ -18565,7 +19297,7 @@ function createInvalidationIntegration(deps) {
18565
19297
  }
18566
19298
  storeForeignStats(event.indexName, event.instanceId, event.stats);
18567
19299
  } catch (err) {
18568
- onError(toError3(err));
19300
+ onError(toError4(err));
18569
19301
  }
18570
19302
  }
18571
19303
  async function publishOwnStatistics() {
@@ -18587,7 +19319,7 @@ function createInvalidationIntegration(deps) {
18587
19319
  }
18588
19320
  });
18589
19321
  } catch (err) {
18590
- onError(toError3(err));
19322
+ onError(toError4(err));
18591
19323
  }
18592
19324
  }
18593
19325
  }
@@ -18606,7 +19338,7 @@ function createInvalidationIntegration(deps) {
18606
19338
  sourceInstanceId: instanceId
18607
19339
  });
18608
19340
  } catch (err) {
18609
- onError(toError3(err));
19341
+ onError(toError4(err));
18610
19342
  }
18611
19343
  },
18612
19344
  broadcastStats(indexName) {
@@ -18651,7 +19383,7 @@ function createInvalidationIntegration(deps) {
18651
19383
  try {
18652
19384
  await adapter.shutdown();
18653
19385
  } catch (err) {
18654
- onError(toError3(err));
19386
+ onError(toError4(err));
18655
19387
  }
18656
19388
  }
18657
19389
  };
@@ -18693,7 +19425,7 @@ async function transferIndexToPool(indexName, pool, config, manager) {
18693
19425
  }
18694
19426
 
18695
19427
  // src/engine/orchestration/eligibility.ts
18696
- function toError2(value) {
19428
+ function toError3(value) {
18697
19429
  return value instanceof Error ? value : new Error(String(value));
18698
19430
  }
18699
19431
  function isDeterministicFailure(error) {
@@ -19272,7 +20004,7 @@ async function promoteBeforeBatch(state, indexName, incomingCount) {
19272
20004
  if (!result.shouldPromote) return;
19273
20005
  state.promotionInProgress = true;
19274
20006
  const run = runPromotion(state, result.reason).catch((err) => {
19275
- const error = toError2(err);
20007
+ const error = toError3(err);
19276
20008
  state.promotionBlocked = isDeterministicFailure(error);
19277
20009
  state.promotionInProgress = false;
19278
20010
  state.callbacks?.onPromotionFailure?.(result.reason, error, !state.promotionBlocked);
@@ -19293,7 +20025,7 @@ async function checkPromotion(state) {
19293
20025
  state.promotionInProgress = true;
19294
20026
  setTimeout(() => {
19295
20027
  const run = runPromotion(state, result.reason).catch((err) => {
19296
- const error = toError2(err);
20028
+ const error = toError3(err);
19297
20029
  state.promotionBlocked = isDeterministicFailure(error);
19298
20030
  state.promotionInProgress = false;
19299
20031
  state.callbacks?.onPromotionFailure?.(result.reason, error, !state.promotionBlocked);
@@ -19463,6 +20195,56 @@ function createWorkerOrchestrator(config, executor, promoter, indexRegistry, cal
19463
20195
  state.segmentLedger.clear();
19464
20196
  }
19465
20197
  }
20198
+ async function awaitPromotionIdle() {
20199
+ while (state.promotionInProgress) {
20200
+ const run = state.promotionRun;
20201
+ if (run !== null) {
20202
+ await run;
20203
+ } else {
20204
+ await new Promise((resolve) => setTimeout(resolve, 0));
20205
+ }
20206
+ }
20207
+ }
20208
+ async function closeIndex(indexName) {
20209
+ await awaitPromotionIdle();
20210
+ await awaitReplicationIdle(state, indexName);
20211
+ await awaitCompactions(state);
20212
+ const pool = state.workerPool;
20213
+ if (pool !== null && state.promotedIndexes.has(indexName)) {
20214
+ const outcomes = await Promise.allSettled(
20215
+ pool.getAllExecutors().map((worker) => worker.execute({ type: "dropIndex", indexName, requestId: `close-${indexName}` }))
20216
+ );
20217
+ const failure = outcomes.find((outcome) => outcome.status === "rejected");
20218
+ if (failure?.status === "rejected") {
20219
+ const entry = indexRegistry.get(indexName);
20220
+ const manager = executor.getManager(indexName);
20221
+ if (entry !== void 0 && manager !== void 0) {
20222
+ try {
20223
+ await transferIndexToPool(indexName, pool, entry.config, manager);
20224
+ } catch (repairError) {
20225
+ throw new NarsilError(
20226
+ ErrorCodes.WORKER_CRASHED,
20227
+ `Worker copies for index "${indexName}" could not be restored after close failed`,
20228
+ {
20229
+ indexName,
20230
+ closeError: failure.reason instanceof Error ? failure.reason.message : String(failure.reason),
20231
+ repairError: repairError instanceof Error ? repairError.message : String(repairError)
20232
+ }
20233
+ );
20234
+ }
20235
+ }
20236
+ if (failure.reason instanceof NarsilError) throw failure.reason;
20237
+ throw new NarsilError(ErrorCodes.WORKER_CRASHED, `A worker could not close index "${indexName}"`, {
20238
+ indexName,
20239
+ cause: failure.reason instanceof Error ? failure.reason.message : String(failure.reason)
20240
+ });
20241
+ }
20242
+ pool.removeIndex(indexName);
20243
+ }
20244
+ state.promotedIndexes.delete(indexName);
20245
+ state.replicationQueues.delete(indexName);
20246
+ state.segmentLedger.delete(indexName);
20247
+ }
19466
20248
  async function replicate(action) {
19467
20249
  await replicateToWorkers(state, action);
19468
20250
  if (action.type === "attachSegments") {
@@ -19475,6 +20257,9 @@ function createWorkerOrchestrator(config, executor, promoter, indexRegistry, cal
19475
20257
  replicateToWorkers: replicate,
19476
20258
  awaitReplication: (indexName) => awaitReplicationIdle(state, indexName),
19477
20259
  awaitCompactions: () => awaitCompactions(state),
20260
+ openIndex: (indexName) => resyncIndex(indexName, true),
20261
+ closeIndex,
20262
+ isIndexBusy: (indexName) => state.promotionInProgress || state.replicationQueues.has(indexName) || state.compactionsInFlight.has(indexName),
19478
20263
  buildSegments: (requests) => buildSegments(state, requests),
19479
20264
  segmentBuildConcurrency: (indexName) => segmentBuildConcurrency(state, indexName),
19480
20265
  searchViaWorker: (indexName, params, globalStats, partitionIds) => searchViaWorker(state, indexName, params, globalStats, partitionIds),
@@ -19486,6 +20271,11 @@ function createWorkerOrchestrator(config, executor, promoter, indexRegistry, cal
19486
20271
  };
19487
20272
  }
19488
20273
 
20274
+ // src/engine/vector-fields.ts
20275
+ function getVectorFieldPaths(schema) {
20276
+ return new Set(extractVectorFieldsFromSchema(schema).keys());
20277
+ }
20278
+
19489
20279
  // src/engine/watermark.ts
19490
20280
  function createWatermarkNotifier(deps) {
19491
20281
  const latchedCapacity = /* @__PURE__ */ new Map();
@@ -19521,10 +20311,7 @@ function createWatermarkNotifier(deps) {
19521
20311
  }
19522
20312
 
19523
20313
  // src/engine/core.ts
19524
- function getVectorFieldPaths(schema) {
19525
- return new Set(extractVectorFieldsFromSchema(schema).keys());
19526
- }
19527
- function createEngineCore(config) {
20314
+ function createEngineCore(config, hooks) {
19528
20315
  const executor = createDirectExecutor();
19529
20316
  const promoter = createExecutionPromoter({
19530
20317
  perIndexThreshold: config?.workers?.promotionThreshold,
@@ -19608,8 +20395,12 @@ function createEngineCore(config) {
19608
20395
  }
19609
20396
  return manager;
19610
20397
  }
19611
- async function createIndexFromMetadata(metadata) {
19612
- if (indexRegistry.has(metadata.indexName)) {
20398
+ async function createIndexFromMetadata(metadata, loadData) {
20399
+ const existing = indexRegistry.get(metadata.indexName);
20400
+ if (existing !== void 0) {
20401
+ if (loadData && executor.getManager(metadata.indexName) === void 0) {
20402
+ executor.createIndex(metadata.indexName, existing.config, existing.language);
20403
+ }
19613
20404
  return;
19614
20405
  }
19615
20406
  const indexConfig = reconstructSchemaFromMetadata(metadata);
@@ -19638,7 +20429,7 @@ function createEngineCore(config) {
19638
20429
  }
19639
20430
  }
19640
20431
  try {
19641
- executor.createIndex(metadata.indexName, indexConfig, language);
20432
+ if (loadData) executor.createIndex(metadata.indexName, indexConfig, language);
19642
20433
  } catch (err) {
19643
20434
  if (err instanceof NarsilError && err.code === ErrorCodes.CONFIG_INVALID) {
19644
20435
  throw new NarsilError(err.code, `Recovery of index "${metadata.indexName}" failed: ${err.message}`, {
@@ -19656,8 +20447,12 @@ function createEngineCore(config) {
19656
20447
  embeddingAdapterName: adapterName,
19657
20448
  vectorFieldPaths: getVectorFieldPaths(indexConfig.schema),
19658
20449
  indexUuid: metadata.indexUuid ?? null,
19659
- heldPartitions: metadata.heldPartitions ?? null
20450
+ heldPartitions: metadata.heldPartitions ?? null,
20451
+ documentCount: metadata.documentCount ?? 0,
20452
+ partitionCount: metadata.partitionCount
19660
20453
  });
20454
+ if (loadData) await indexState.registerOpen(metadata.indexName);
20455
+ else indexState.registerClosed(metadata.indexName);
19661
20456
  if (metadata.analysisRevision !== language.revision) {
19662
20457
  analysisRebuild.markStale({
19663
20458
  indexName: metadata.indexName,
@@ -19668,9 +20463,12 @@ function createEngineCore(config) {
19668
20463
  }
19669
20464
  }
19670
20465
  const durabilityTier = config !== void 0 ? resolveDurabilityTier(config) : null;
20466
+ if (config?.lifecycle !== void 0 && durabilityTier === null) {
20467
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, "Index lifecycle settings require durability");
20468
+ }
19671
20469
  let invalidation = null;
19672
20470
  const durability = createDurabilityFromTier(durabilityTier, {
19673
- requireManager,
20471
+ getManager: (indexName) => executor.getManager(indexName),
19674
20472
  indexRegistry,
19675
20473
  createIndexFromMetadata,
19676
20474
  emitFatalError(error) {
@@ -19679,7 +20477,13 @@ function createEngineCore(config) {
19679
20477
  for (const handler of handlers) handler({ error });
19680
20478
  }
19681
20479
  },
19682
- publishCheckpointedPartitions: (indexName, partitions) => invalidation?.publishPartitions(indexName, partitions) ?? Promise.resolve()
20480
+ publishCheckpointedPartitions: (indexName, partitions) => invalidation?.publishPartitions(indexName, partitions) ?? Promise.resolve(),
20481
+ recordCheckpoint(indexName, documentCount, partitionCount) {
20482
+ const entry = indexRegistry.get(indexName);
20483
+ if (entry === void 0) return;
20484
+ entry.documentCount = documentCount;
20485
+ entry.partitionCount = partitionCount;
20486
+ }
19683
20487
  });
19684
20488
  invalidation = createInvalidationFromConfig(config, durabilityTier?.kind ?? null, {
19685
20489
  getManager: (indexName) => executor.getManager(indexName) ?? void 0,
@@ -19725,6 +20529,18 @@ function createEngineCore(config) {
19725
20529
  resyncIndex: (indexName, wasPromoted) => orchestrator.resyncIndex(indexName, wasPromoted),
19726
20530
  durabilityManager: durability?.manager ?? null
19727
20531
  });
20532
+ const indexState = wireIndexState({
20533
+ config: config?.lifecycle,
20534
+ durability,
20535
+ executor,
20536
+ orchestrator,
20537
+ analysisRebuild,
20538
+ indexRegistry,
20539
+ rebalancingIndexes,
20540
+ requireManager,
20541
+ onClose: hooks?.onIndexClose,
20542
+ onOpen: hooks?.onIndexOpen
20543
+ });
19728
20544
  const mutationCtx = {
19729
20545
  executor,
19730
20546
  pluginRegistry,
@@ -19778,6 +20594,7 @@ function createEngineCore(config) {
19778
20594
  bufferIfRebalancing,
19779
20595
  watermarkNotifier,
19780
20596
  analysisRebuild,
20597
+ indexState,
19781
20598
  mutationCtx,
19782
20599
  rebalanceCtx
19783
20600
  };
@@ -19937,8 +20754,11 @@ async function createEngineIndex(core, config, name, indexConfig, indexUuid) {
19937
20754
  embeddingAdapterName,
19938
20755
  vectorFieldPaths: getVectorFieldPaths(indexConfig.schema),
19939
20756
  indexUuid: null,
19940
- heldPartitions: null
20757
+ heldPartitions: null,
20758
+ documentCount: 0,
20759
+ partitionCount: indexConfig.partitions?.maxPartitions ?? 1
19941
20760
  });
20761
+ await core.indexState.registerOpen(name);
19942
20762
  if (core.durability) {
19943
20763
  await core.durability.manager.persistMetadata(name);
19944
20764
  }
@@ -19947,13 +20767,21 @@ async function createEngineIndex(core, config, name, indexConfig, indexUuid) {
19947
20767
  async function dropEngineIndex(core, name) {
19948
20768
  core.guardShutdown();
19949
20769
  const entry = core.requireIndex(name);
19950
- core.executor.dropIndex(name);
19951
- core.indexRegistry.delete(name);
19952
- core.watermarkNotifier.forget(name);
19953
- core.analysisRebuild.clearStale(name);
19954
- if (core.durability) {
19955
- await core.durability.manager.removeIndex(name);
19956
- }
20770
+ await core.indexState.drop(name, async () => {
20771
+ await core.orchestrator.closeIndex(name);
20772
+ if (core.durability) {
20773
+ try {
20774
+ await core.durability.manager.removeIndex(name);
20775
+ } catch (error) {
20776
+ await core.orchestrator.openIndex(name).catch(() => void 0);
20777
+ throw error;
20778
+ }
20779
+ }
20780
+ if (core.executor.getManager(name) !== void 0) core.executor.dropIndex(name);
20781
+ core.indexRegistry.delete(name);
20782
+ core.watermarkNotifier.forget(name);
20783
+ core.analysisRebuild.clearStale(name);
20784
+ });
19957
20785
  await core.pluginRegistry.runHook("onIndexDrop", { indexName: name, config: entry.config });
19958
20786
  }
19959
20787
  function registerEngineEmbeddingAdapter(core, name, adapter) {
@@ -19988,6 +20816,7 @@ function registerEngineEmbeddingAdapter(core, name, adapter) {
19988
20816
  // src/engine/lifecycle.ts
19989
20817
  async function shutdownEngine(core) {
19990
20818
  const { executor, durability, indexRegistry, eventHandlers, orchestrator } = core;
20819
+ core.indexState.dispose();
19991
20820
  for (const [name] of indexRegistry) {
19992
20821
  const manager = executor.getManager(name);
19993
20822
  if (manager) {
@@ -21879,8 +22708,12 @@ async function restoreFromSnapshot(indexName, data, deps) {
21879
22708
  embeddingAdapterName: adapterName,
21880
22709
  vectorFieldPaths: vectorFieldPaths2,
21881
22710
  indexUuid: null,
21882
- heldPartitions: null
22711
+ heldPartitions: null,
22712
+ documentCount: 0,
22713
+ partitionCount: envelope.partitions.length
21883
22714
  });
22715
+ await deps.indexState.registerOpen(indexName);
22716
+ const release = await deps.indexState.acquire(indexName, false);
21884
22717
  try {
21885
22718
  const manager = deps.requireManager(indexName);
21886
22719
  while (manager.partitionCount < envelope.partitions.length) {
@@ -21900,26 +22733,36 @@ async function restoreFromSnapshot(indexName, data, deps) {
21900
22733
  }
21901
22734
  }
21902
22735
  }
22736
+ const entry = deps.indexRegistry.get(indexName);
22737
+ if (entry !== void 0) {
22738
+ entry.documentCount = manager.countDocuments();
22739
+ entry.partitionCount = manager.partitionCount;
22740
+ }
22741
+ if (deps.durability) {
22742
+ for (let partitionId = 0; partitionId < manager.partitionCount; partitionId++) {
22743
+ for (const docId of manager.getPartition(partitionId).docIds()) {
22744
+ const document = manager.get(docId);
22745
+ if (document !== void 0) {
22746
+ await deps.durability.recordInsertOrUpdate(indexName, docId, document, noopApply2);
22747
+ }
22748
+ }
22749
+ }
22750
+ await deps.durability.manager.persistMetadata(indexName);
22751
+ await deps.durability.manager.checkpoint(indexName);
22752
+ }
21903
22753
  } catch (err) {
21904
22754
  try {
21905
22755
  deps.executor.dropIndex(indexName);
21906
- deps.indexRegistry.delete(indexName);
21907
22756
  } catch (_) {
21908
22757
  }
21909
- throw err;
21910
- }
21911
- if (deps.durability) {
21912
- const manager = deps.requireManager(indexName);
21913
- for (let partitionId = 0; partitionId < manager.partitionCount; partitionId++) {
21914
- for (const docId of manager.getPartition(partitionId).docIds()) {
21915
- const document = manager.get(docId);
21916
- if (document !== void 0) {
21917
- await deps.durability.recordInsertOrUpdate(indexName, docId, document, noopApply2);
21918
- }
21919
- }
22758
+ deps.indexRegistry.delete(indexName);
22759
+ deps.indexState.forget(indexName);
22760
+ if (deps.durability) {
22761
+ await deps.durability.manager.removeIndex(indexName).catch(() => void 0);
21920
22762
  }
21921
- await deps.durability.manager.persistMetadata(indexName);
21922
- await deps.durability.manager.checkpoint(indexName);
22763
+ throw err;
22764
+ } finally {
22765
+ release();
21923
22766
  }
21924
22767
  }
21925
22768
  function noopApply2() {
@@ -23207,85 +24050,107 @@ function broadcastStatsFor(core, options) {
23207
24050
  }
23208
24051
  async function runEngineQuery(core, indexName, params, options) {
23209
24052
  core.guardShutdown();
23210
- const entry = core.requireIndex(indexName);
23211
- const manager = core.requireManager(indexName);
23212
- const resolvedParams = await resolveVectorText(
23213
- scopedParams(params),
23214
- entry.embeddingAdapter,
23215
- core.abortController.signal,
23216
- entry.embeddingAdapterName
23217
- );
23218
- await core.pluginRegistry.runHook("beforeSearch", { indexName, params: resolvedParams });
23219
- const workerSearch = core.orchestrator.isPromoted() ? core.orchestrator.searchViaWorker.bind(core.orchestrator) : void 0;
23220
- const result = await executeQuery(resolvedParams, {
23221
- manager,
23222
- language: entry.language,
23223
- config: entry.config,
23224
- workerSearch,
23225
- indexName,
23226
- broadcastStats: broadcastStatsFor(core),
23227
- partitionIds: options?.partitionIds,
23228
- cursorBinding: queryBindingOf(params)
23229
- });
24053
+ const release = await core.indexState.acquire(indexName);
23230
24054
  try {
23231
- await core.pluginRegistry.runHook("afterSearch", {
24055
+ const entry = core.requireIndex(indexName);
24056
+ const manager = core.requireManager(indexName);
24057
+ const resolvedParams = await resolveVectorText(
24058
+ scopedParams(params, options),
24059
+ entry.embeddingAdapter,
24060
+ core.abortController.signal,
24061
+ entry.embeddingAdapterName
24062
+ );
24063
+ await core.pluginRegistry.runHook("beforeSearch", { indexName, params: resolvedParams });
24064
+ const workerSearch = core.orchestrator.isPromoted() ? core.orchestrator.searchViaWorker.bind(core.orchestrator) : void 0;
24065
+ const result = await executeQuery(resolvedParams, {
24066
+ manager,
24067
+ language: entry.language,
24068
+ config: entry.config,
24069
+ workerSearch,
23232
24070
  indexName,
23233
- params: resolvedParams,
23234
- results: result
24071
+ broadcastStats: broadcastStatsFor(core, options),
24072
+ partitionIds: options?.partitionIds,
24073
+ cursorBinding: queryBindingOf(params)
23235
24074
  });
23236
- } catch (err) {
23237
- console.warn("afterSearch plugin hook error:", err);
23238
- }
23239
- if (core.analysisRebuild.isStale(indexName)) {
23240
- result.analysisStale = true;
24075
+ try {
24076
+ await core.pluginRegistry.runHook("afterSearch", {
24077
+ indexName,
24078
+ params: resolvedParams,
24079
+ results: result
24080
+ });
24081
+ } catch (err) {
24082
+ console.warn("afterSearch plugin hook error:", err);
24083
+ }
24084
+ if (core.analysisRebuild.isStale(indexName)) {
24085
+ result.analysisStale = true;
24086
+ }
24087
+ return result;
24088
+ } finally {
24089
+ release();
23241
24090
  }
23242
- return result;
23243
24091
  }
23244
24092
  async function runEnginePreflight(core, indexName, params, options) {
23245
24093
  core.guardShutdown();
23246
- const entry = core.requireIndex(indexName);
23247
- const manager = core.requireManager(indexName);
23248
- const resolvedParams = await resolveVectorText(
23249
- scopedParams(params),
23250
- entry.embeddingAdapter,
23251
- core.abortController.signal,
23252
- entry.embeddingAdapterName
23253
- );
23254
- const workerSearch = core.orchestrator.isPromoted() ? core.orchestrator.searchViaWorker.bind(core.orchestrator) : void 0;
23255
- const result = await executePreflight(resolvedParams, {
23256
- manager,
23257
- language: entry.language,
23258
- config: entry.config,
23259
- workerSearch,
23260
- indexName,
23261
- broadcastStats: broadcastStatsFor(core),
23262
- partitionIds: options?.partitionIds,
23263
- cursorBinding: queryBindingOf(params)
23264
- });
23265
- if (core.analysisRebuild.isStale(indexName)) {
23266
- result.analysisStale = true;
24094
+ const release = await core.indexState.acquire(indexName);
24095
+ try {
24096
+ const entry = core.requireIndex(indexName);
24097
+ const manager = core.requireManager(indexName);
24098
+ const resolvedParams = await resolveVectorText(
24099
+ scopedParams(params, options),
24100
+ entry.embeddingAdapter,
24101
+ core.abortController.signal,
24102
+ entry.embeddingAdapterName
24103
+ );
24104
+ const workerSearch = core.orchestrator.isPromoted() ? core.orchestrator.searchViaWorker.bind(core.orchestrator) : void 0;
24105
+ const result = await executePreflight(resolvedParams, {
24106
+ manager,
24107
+ language: entry.language,
24108
+ config: entry.config,
24109
+ workerSearch,
24110
+ indexName,
24111
+ broadcastStats: broadcastStatsFor(core, options),
24112
+ partitionIds: options?.partitionIds,
24113
+ cursorBinding: queryBindingOf(params)
24114
+ });
24115
+ if (core.analysisRebuild.isStale(indexName)) {
24116
+ result.analysisStale = true;
24117
+ }
24118
+ return result;
24119
+ } finally {
24120
+ release();
23267
24121
  }
23268
- return result;
23269
24122
  }
23270
24123
  async function runEngineSuggest(core, indexName, params, partitionIds) {
23271
24124
  core.guardShutdown();
23272
- const result = executeSuggest(
23273
- core.requireManager(indexName),
23274
- core.requireIndex(indexName).language,
23275
- params);
23276
- if (core.analysisRebuild.isStale(indexName)) {
23277
- result.analysisStale = true;
24125
+ const release = await core.indexState.acquire(indexName);
24126
+ try {
24127
+ const result = executeSuggest(
24128
+ core.requireManager(indexName),
24129
+ core.requireIndex(indexName).language,
24130
+ params,
24131
+ partitionIds
24132
+ );
24133
+ if (core.analysisRebuild.isStale(indexName)) {
24134
+ result.analysisStale = true;
24135
+ }
24136
+ return result;
24137
+ } finally {
24138
+ release();
23278
24139
  }
23279
- return result;
23280
24140
  }
23281
24141
  async function runEngineListDocuments(core, indexName, params, partitionIds) {
23282
24142
  core.guardShutdown();
23283
- const entry = core.requireIndex(indexName);
23284
- return executeListDocuments(params, {
23285
- manager: core.requireManager(indexName),
23286
- schema: entry.config.schema,
23287
- partitionIds
23288
- });
24143
+ const release = await core.indexState.acquire(indexName);
24144
+ try {
24145
+ const entry = core.requireIndex(indexName);
24146
+ return executeListDocuments(params, {
24147
+ manager: core.requireManager(indexName),
24148
+ schema: entry.config.schema,
24149
+ partitionIds
24150
+ });
24151
+ } finally {
24152
+ release();
24153
+ }
23289
24154
  }
23290
24155
 
23291
24156
  // src/narsil/operations.ts
@@ -23305,6 +24170,16 @@ function createNarsilFromCore(core, config) {
23305
24170
  mutationCtx,
23306
24171
  rebalanceCtx
23307
24172
  } = core;
24173
+ async function withOpenIndex(indexName, action) {
24174
+ guardShutdown();
24175
+ requireIndex(indexName);
24176
+ const release = await core.indexState.acquire(indexName);
24177
+ try {
24178
+ return await action();
24179
+ } finally {
24180
+ release();
24181
+ }
24182
+ }
23308
24183
  const narsil = {
23309
24184
  createIndex(name, indexConfig) {
23310
24185
  return createEngineIndex(core, config, name, indexConfig);
@@ -23315,15 +24190,27 @@ function createNarsilFromCore(core, config) {
23315
24190
  dropIndex(name) {
23316
24191
  return dropEngineIndex(core, name);
23317
24192
  },
24193
+ open(indexName) {
24194
+ guardShutdown();
24195
+ requireIndex(indexName);
24196
+ return core.indexState.open(indexName);
24197
+ },
24198
+ close(indexName) {
24199
+ guardShutdown();
24200
+ requireIndex(indexName);
24201
+ return core.indexState.close(indexName);
24202
+ },
23318
24203
  listIndexes() {
23319
24204
  const infos = [];
23320
24205
  for (const [name, entry] of indexRegistry) {
23321
24206
  const manager = executor.getManager(name);
23322
24207
  infos.push({
23323
24208
  name,
23324
- documentCount: manager?.countDocuments() ?? 0,
23325
- partitionCount: manager?.partitionCount ?? 0,
24209
+ documentCount: manager?.countDocuments() ?? entry.documentCount,
24210
+ partitionCount: manager?.partitionCount ?? entry.partitionCount,
23326
24211
  language: entry.language.name,
24212
+ state: core.indexState.stateOf(name),
24213
+ reopenCount: core.indexState.reopenCount(name),
23327
24214
  ...core.analysisRebuild.isStale(name) ? { analysisStale: true } : {}
23328
24215
  });
23329
24216
  }
@@ -23334,8 +24221,8 @@ function createNarsilFromCore(core, config) {
23334
24221
  const entry = requireIndex(indexName);
23335
24222
  const manager = executor.getManager(indexName);
23336
24223
  return {
23337
- documentCount: manager?.countDocuments() ?? 0,
23338
- partitionCount: manager?.partitionCount ?? 0,
24224
+ documentCount: manager?.countDocuments() ?? entry.documentCount,
24225
+ partitionCount: manager?.partitionCount ?? entry.partitionCount,
23339
24226
  estimatedMemoryBytes: manager?.estimateMemoryBytes() ?? 0,
23340
24227
  language: entry.language.name,
23341
24228
  schema: entry.config.schema
@@ -23347,47 +24234,48 @@ function createNarsilFromCore(core, config) {
23347
24234
  return executor.getManager(indexName)?.getPartitionStats() ?? [];
23348
24235
  },
23349
24236
  insert(indexName, document, docId, options) {
23350
- return insertDocument(mutationCtx, indexName, document, docId, options);
24237
+ return withOpenIndex(indexName, () => insertDocument(mutationCtx, indexName, document, docId, options));
23351
24238
  },
23352
24239
  insertBatch(indexName, documents, options) {
23353
- return insertDocumentBatch(mutationCtx, indexName, documents, options);
24240
+ return withOpenIndex(indexName, () => insertDocumentBatch(mutationCtx, indexName, documents, options));
23354
24241
  },
23355
24242
  remove(indexName, docId) {
23356
- return removeDocument(mutationCtx, indexName, docId);
24243
+ return withOpenIndex(indexName, () => removeDocument(mutationCtx, indexName, docId));
23357
24244
  },
23358
24245
  removeBatch(indexName, docIds) {
23359
- return removeDocumentBatch(mutationCtx, indexName, docIds);
24246
+ return withOpenIndex(indexName, () => removeDocumentBatch(mutationCtx, indexName, docIds));
23360
24247
  },
23361
24248
  update(indexName, docId, document) {
23362
- return updateDocument(mutationCtx, indexName, docId, document);
24249
+ return withOpenIndex(indexName, () => updateDocument(mutationCtx, indexName, docId, document));
23363
24250
  },
23364
24251
  updateBatch(indexName, updates) {
23365
- return updateDocumentBatch(mutationCtx, indexName, updates);
24252
+ return withOpenIndex(indexName, () => updateDocumentBatch(mutationCtx, indexName, updates));
23366
24253
  },
23367
24254
  async get(indexName, docId) {
23368
- guardShutdown();
23369
- requireIndex(indexName);
23370
- return executor.execute({ type: "get", indexName, docId, requestId: docId });
24255
+ return withOpenIndex(indexName, () => executor.execute({ type: "get", indexName, docId, requestId: docId }));
23371
24256
  },
23372
24257
  async getMultiple(indexName, docIds) {
23373
24258
  guardShutdown();
23374
24259
  requireIndex(indexName);
23375
- const result = /* @__PURE__ */ new Map();
23376
- for (const docId of docIds) {
23377
- const doc = await narsil.get(indexName, docId);
23378
- if (doc !== void 0) result.set(docId, doc);
23379
- }
23380
- return result;
24260
+ return withOpenIndex(indexName, async () => {
24261
+ const result = /* @__PURE__ */ new Map();
24262
+ for (const docId of docIds) {
24263
+ const doc = await executor.execute({
24264
+ type: "get",
24265
+ indexName,
24266
+ docId,
24267
+ requestId: docId
24268
+ });
24269
+ if (doc !== void 0) result.set(docId, doc);
24270
+ }
24271
+ return result;
24272
+ });
23381
24273
  },
23382
24274
  async has(indexName, docId) {
23383
- guardShutdown();
23384
- requireIndex(indexName);
23385
- return executor.execute({ type: "has", indexName, docId, requestId: docId });
24275
+ return withOpenIndex(indexName, () => executor.execute({ type: "has", indexName, docId, requestId: docId }));
23386
24276
  },
23387
24277
  async countDocuments(indexName) {
23388
- guardShutdown();
23389
- requireIndex(indexName);
23390
- return executor.execute({ type: "count", indexName, requestId: indexName });
24278
+ return withOpenIndex(indexName, () => executor.execute({ type: "count", indexName, requestId: indexName }));
23391
24279
  },
23392
24280
  async listDocuments(indexName, params) {
23393
24281
  return runEngineListDocuments(core, indexName, params ?? {});
@@ -23404,15 +24292,15 @@ function createNarsilFromCore(core, config) {
23404
24292
  async rebuildAnalysis(indexName) {
23405
24293
  guardShutdown();
23406
24294
  requireIndex(indexName);
23407
- await core.analysisRebuild.rebuild(indexName);
24295
+ await withOpenIndex(indexName, () => core.analysisRebuild.rebuild(indexName));
23408
24296
  },
23409
24297
  async snapshot(indexName) {
23410
24298
  guardShutdown();
23411
- return createSnapshot(requireManager(indexName), requireIndex(indexName));
24299
+ return withOpenIndex(indexName, () => createSnapshot(requireManager(indexName), requireIndex(indexName)));
23412
24300
  },
23413
24301
  async restore(indexName, data) {
23414
24302
  guardShutdown();
23415
- return restoreFromSnapshot(indexName, data, {
24303
+ await restoreFromSnapshot(indexName, data, {
23416
24304
  executor,
23417
24305
  indexRegistry,
23418
24306
  getVectorFieldPaths,
@@ -23422,66 +24310,74 @@ function createNarsilFromCore(core, config) {
23422
24310
  embeddingAdapters: core.embeddingAdapters,
23423
24311
  defaultEmbeddingAdapter: config?.embedding ?? null,
23424
24312
  markAnalysisStale: core.analysisRebuild.markStale,
23425
- clearAnalysisStale: core.analysisRebuild.clearStale
24313
+ clearAnalysisStale: core.analysisRebuild.clearStale,
24314
+ indexState: core.indexState
23426
24315
  });
23427
24316
  },
23428
24317
  async checkpoint(indexName) {
23429
24318
  guardShutdown();
23430
24319
  requireIndex(indexName);
23431
- if (durability) {
23432
- await durability.manager.checkpoint(indexName);
23433
- }
24320
+ await withOpenIndex(indexName, async () => {
24321
+ if (durability) await durability.manager.checkpoint(indexName);
24322
+ });
23434
24323
  },
23435
24324
  async clear(indexName) {
23436
24325
  guardShutdown();
23437
24326
  requireIndex(indexName);
23438
- await executor.execute({ type: "clear", indexName, requestId: indexName });
23439
- await orchestrator.replicateToWorkers({ type: "clear", indexName, requestId: `replicate-clear-${indexName}` });
23440
- core.watermarkNotifier.forget(indexName);
24327
+ await withOpenIndex(indexName, async () => {
24328
+ await executor.execute({ type: "clear", indexName, requestId: indexName });
24329
+ await orchestrator.replicateToWorkers({ type: "clear", indexName, requestId: `replicate-clear-${indexName}` });
24330
+ core.watermarkNotifier.forget(indexName);
24331
+ });
23441
24332
  },
23442
24333
  async rebalance(indexName, targetPartitionCount) {
23443
24334
  guardShutdown();
23444
24335
  requireIndex(indexName);
23445
- return executeRebalance(requireManager(indexName), indexName, targetPartitionCount, rebalanceCtx);
24336
+ return withOpenIndex(
24337
+ indexName,
24338
+ () => executeRebalance(requireManager(indexName), indexName, targetPartitionCount, rebalanceCtx)
24339
+ );
23446
24340
  },
23447
24341
  async updatePartitionConfig(indexName, partitionConfig) {
23448
24342
  guardShutdown();
23449
- const entry = requireIndex(indexName);
23450
- const manager = requireManager(indexName);
23451
- if (rebalancingIndexes.has(indexName)) {
23452
- throw new NarsilError(
23453
- ErrorCodes.PARTITION_REBALANCING_BACKPRESSURE,
23454
- `Index "${indexName}" is currently being rebalanced`
23455
- );
23456
- }
23457
- validatePartitionConfig(partitionConfig);
23458
- if (partitionConfig.maxPartitions !== void 0 && partitionConfig.maxPartitions < manager.partitionCount) {
23459
- throw new NarsilError(
23460
- ErrorCodes.PARTITION_CAPACITY_EXCEEDED,
23461
- `maxPartitions (${partitionConfig.maxPartitions}) is less than the current partition count (${manager.partitionCount})`,
23462
- { maxPartitions: partitionConfig.maxPartitions, partitionCount: manager.partitionCount }
23463
- );
23464
- }
23465
- const currentDocCount = manager.countDocuments();
23466
- const newMaxDocs = partitionConfig.maxDocsPerPartition ?? entry.config.partitions?.maxDocsPerPartition;
23467
- if (newMaxDocs !== void 0) {
23468
- const newTotalCapacity = newMaxDocs * manager.partitionCount;
23469
- if (newTotalCapacity < currentDocCount) {
24343
+ return withOpenIndex(indexName, async () => {
24344
+ const entry = requireIndex(indexName);
24345
+ const manager = requireManager(indexName);
24346
+ if (rebalancingIndexes.has(indexName)) {
24347
+ throw new NarsilError(
24348
+ ErrorCodes.PARTITION_REBALANCING_BACKPRESSURE,
24349
+ `Index "${indexName}" is currently being rebalanced`
24350
+ );
24351
+ }
24352
+ validatePartitionConfig(partitionConfig);
24353
+ if (partitionConfig.maxPartitions !== void 0 && partitionConfig.maxPartitions < manager.partitionCount) {
23470
24354
  throw new NarsilError(
23471
24355
  ErrorCodes.PARTITION_CAPACITY_EXCEEDED,
23472
- `New capacity (${newTotalCapacity}) is less than current document count (${currentDocCount})`,
23473
- { newTotalCapacity, currentDocCount }
24356
+ `maxPartitions (${partitionConfig.maxPartitions}) is less than the current partition count (${manager.partitionCount})`,
24357
+ { maxPartitions: partitionConfig.maxPartitions, partitionCount: manager.partitionCount }
23474
24358
  );
23475
24359
  }
23476
- }
23477
- if (!entry.config.partitions) entry.config.partitions = {};
23478
- if (partitionConfig.maxDocsPerPartition !== void 0)
23479
- entry.config.partitions.maxDocsPerPartition = partitionConfig.maxDocsPerPartition;
23480
- if (partitionConfig.maxPartitions !== void 0)
23481
- entry.config.partitions.maxPartitions = partitionConfig.maxPartitions;
23482
- if (partitionConfig.watermark !== void 0) entry.config.partitions.watermark = partitionConfig.watermark;
23483
- if (durability) await durability.manager.persistMetadata(indexName);
23484
- core.watermarkNotifier.check(indexName);
24360
+ const currentDocCount = manager.countDocuments();
24361
+ const newMaxDocs = partitionConfig.maxDocsPerPartition ?? entry.config.partitions?.maxDocsPerPartition;
24362
+ if (newMaxDocs !== void 0) {
24363
+ const newTotalCapacity = newMaxDocs * manager.partitionCount;
24364
+ if (newTotalCapacity < currentDocCount) {
24365
+ throw new NarsilError(
24366
+ ErrorCodes.PARTITION_CAPACITY_EXCEEDED,
24367
+ `New capacity (${newTotalCapacity}) is less than current document count (${currentDocCount})`,
24368
+ { newTotalCapacity, currentDocCount }
24369
+ );
24370
+ }
24371
+ }
24372
+ if (!entry.config.partitions) entry.config.partitions = {};
24373
+ if (partitionConfig.maxDocsPerPartition !== void 0)
24374
+ entry.config.partitions.maxDocsPerPartition = partitionConfig.maxDocsPerPartition;
24375
+ if (partitionConfig.maxPartitions !== void 0)
24376
+ entry.config.partitions.maxPartitions = partitionConfig.maxPartitions;
24377
+ if (partitionConfig.watermark !== void 0) entry.config.partitions.watermark = partitionConfig.watermark;
24378
+ if (durability) await durability.manager.persistMetadata(indexName);
24379
+ core.watermarkNotifier.check(indexName);
24380
+ });
23485
24381
  },
23486
24382
  async getMemoryStats() {
23487
24383
  let estimatedIndexBytes = 0;
@@ -23489,22 +24385,33 @@ function createNarsilFromCore(core, config) {
23489
24385
  estimatedIndexBytes += executor.getManager(name)?.estimateMemoryBytes() ?? 0;
23490
24386
  }
23491
24387
  const workers = await orchestrator.getWorkerMemoryStats();
23492
- return { process: readProcessMemory(), estimatedIndexBytes, workers };
24388
+ const lifecycleCounts = core.indexState.counts();
24389
+ return {
24390
+ process: readProcessMemory(),
24391
+ estimatedIndexBytes,
24392
+ openIndexCount: lifecycleCounts.open,
24393
+ closedIndexCount: lifecycleCounts.closed,
24394
+ reopenCount: lifecycleCounts.reopens,
24395
+ workers
24396
+ };
23493
24397
  },
23494
24398
  async compactVectors(indexName, fieldName) {
23495
24399
  guardShutdown();
23496
24400
  requireIndex(indexName);
23497
- compactVectors(requireManager(indexName), indexName, fieldName);
24401
+ await withOpenIndex(indexName, async () => {
24402
+ compactVectors(requireManager(indexName), indexName, fieldName);
24403
+ });
23498
24404
  },
23499
24405
  async optimizeVectors(indexName, fieldName) {
23500
24406
  guardShutdown();
23501
24407
  requireIndex(indexName);
23502
- await optimizeVectors(requireManager(indexName), indexName, fieldName);
24408
+ await withOpenIndex(indexName, () => optimizeVectors(requireManager(indexName), indexName, fieldName));
23503
24409
  },
23504
24410
  vectorMaintenanceStatus(indexName) {
23505
24411
  guardShutdown();
23506
24412
  requireIndex(indexName);
23507
- return getVectorMaintenanceStatus(requireManager(indexName));
24413
+ const manager = executor.getManager(indexName);
24414
+ return manager === void 0 ? [] : getVectorMaintenanceStatus(manager);
23508
24415
  },
23509
24416
  on(event, handler) {
23510
24417
  const key = event;
@@ -23537,12 +24444,12 @@ function createNarsilFromCore(core, config) {
23537
24444
  async function createNarsil(config) {
23538
24445
  const core = createEngineCore(config);
23539
24446
  if (core.durability) {
23540
- await core.durability.manager.recover();
24447
+ await core.durability.manager.recover(config?.lifecycle !== void 0);
23541
24448
  }
23542
24449
  if (core.invalidation) {
23543
24450
  await core.invalidation.start();
23544
24451
  }
23545
- await core.analysisRebuild.reviewStaleIndexes();
24452
+ if (config?.lifecycle === void 0) await core.analysisRebuild.reviewStaleIndexes();
23546
24453
  return createNarsilFromCore(core, config);
23547
24454
  }
23548
24455