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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/adapters/filesystem.mjs +1 -1
  2. package/dist/adapters/indexeddb.mjs +1 -1
  3. package/dist/{chunk-GB2IIJJG.mjs → chunk-P4ELXVJQ.mjs} +2 -4
  4. package/dist/{chunk-MMSWEEZT.mjs → chunk-WASNWIEK.mjs} +1452 -373
  5. package/dist/{chunk-GRWFY2HI.mjs → chunk-XNCJFU2E.mjs} +239 -61
  6. package/dist/{chunk-WU2UZEVD.mjs → chunk-XWICBGYT.mjs} +1 -1
  7. package/dist/{client-BobVnoIK.d.ts → client-C5BkUqB8.d.ts} +26 -3
  8. package/dist/client.d.ts +8 -8
  9. package/dist/client.mjs +12 -6
  10. package/dist/{config-fLC4lbmg.d.ts → config-B-ZI-ft5.d.ts} +2 -2
  11. package/dist/{config-Byyl1QqH.d.ts → config-BZRExglL.d.ts} +20 -3
  12. package/dist/{direct-executor-MSJEVPZU.mjs → direct-executor-IMXNRS7O.mjs} +2 -2
  13. package/dist/distribution/coordinator/etcd.d.ts +2 -2
  14. package/dist/distribution/coordinator/etcd.mjs +1 -1
  15. package/dist/distribution/coordinator/in-memory.d.ts +2 -2
  16. package/dist/distribution/transport/grpc.mjs +1 -1
  17. package/dist/distribution.d.ts +24 -10
  18. package/dist/distribution.mjs +1721 -1129
  19. package/dist/embeddings/openai.mjs +1 -1
  20. package/dist/{engine-BE45D5ZG.d.ts → engine-Cjr1uBLa.d.ts} +47 -24
  21. package/dist/index.browser.mjs +1668 -416
  22. package/dist/index.d.ts +9 -9
  23. package/dist/index.mjs +4 -4
  24. package/dist/{options-EXYFcem-.d.ts → options-BC68VPoE.d.ts} +10 -3
  25. package/dist/persistence/durability/checkpoint-worker.d.ts +3 -1
  26. package/dist/persistence/durability/checkpoint-worker.mjs +4 -4
  27. package/dist/react.d.ts +6 -6
  28. package/dist/react.mjs +2 -4
  29. package/dist/{schema-ZRbk8BgH.d.ts → schema-B7juWg8Q.d.ts} +17 -6
  30. package/dist/{search-DHuXwJYk.d.ts → search-DDHjfXhJ.d.ts} +35 -11
  31. package/dist/server.d.ts +10 -10
  32. package/dist/server.mjs +30 -12
  33. package/dist/{tasks-CnMLpfk-.d.ts → tasks-Bn4R1NPN.d.ts} +1 -1
  34. package/dist/{types-B5Tsaalz.d.ts → types-DsSSjY4d.d.ts} +1 -1
  35. package/dist/workers/entry.mjs +2 -2
  36. package/package.json +1 -1
@@ -3,7 +3,6 @@ import { encode, decode } from '@msgpack/msgpack';
3
3
  // src/errors.ts
4
4
  var ErrorCodes = {
5
5
  SCHEMA_INVALID_TYPE: "SCHEMA_INVALID_TYPE",
6
- SCHEMA_MISSING_FIELD: "SCHEMA_MISSING_FIELD",
7
6
  SCHEMA_DEPTH_EXCEEDED: "SCHEMA_DEPTH_EXCEEDED",
8
7
  SCHEMA_INVALID_VECTOR_DIMENSION: "SCHEMA_INVALID_VECTOR_DIMENSION",
9
8
  SCHEMA_INVALID_GEOPOINT: "SCHEMA_INVALID_GEOPOINT",
@@ -13,6 +12,7 @@ var ErrorCodes = {
13
12
  INDEX_NOT_FOUND: "INDEX_NOT_FOUND",
14
13
  INDEX_ALREADY_EXISTS: "INDEX_ALREADY_EXISTS",
15
14
  INDEX_ORPHANED: "INDEX_ORPHANED",
15
+ INDEX_REOPEN_CAPACITY_EXHAUSTED: "INDEX_REOPEN_CAPACITY_EXHAUSTED",
16
16
  PARTITION_CORRUPTED: "PARTITION_CORRUPTED",
17
17
  PARTITION_REBALANCING_BACKPRESSURE: "PARTITION_REBALANCING_BACKPRESSURE",
18
18
  WORKER_CRASHED: "WORKER_CRASHED",
@@ -25,7 +25,6 @@ var ErrorCodes = {
25
25
  PERSISTENCE_WAL_CORRUPT: "PERSISTENCE_WAL_CORRUPT",
26
26
  PERSISTENCE_FSYNC_FAILED: "PERSISTENCE_FSYNC_FAILED",
27
27
  SEARCH_INVALID_FIELD: "SEARCH_INVALID_FIELD",
28
- SEARCH_INVALID_VECTOR_SIZE: "SEARCH_INVALID_VECTOR_SIZE",
29
28
  VECTOR_DIMENSION_MISMATCH: "VECTOR_DIMENSION_MISMATCH",
30
29
  SEARCH_INVALID_FILTER: "SEARCH_INVALID_FILTER",
31
30
  SEARCH_INVALID_MODE: "SEARCH_INVALID_MODE",
@@ -49,8 +48,6 @@ var ErrorCodes = {
49
48
  ALLOCATION_NO_DATA_NODES: "ALLOCATION_NO_DATA_NODES",
50
49
  ALLOCATION_INVALID_CONFIG: "ALLOCATION_INVALID_CONFIG",
51
50
  ALLOCATION_FAILED: "ALLOCATION_FAILED",
52
- CONTROLLER_LEASE_LOST: "CONTROLLER_LEASE_LOST",
53
- CONTROLLER_NOT_ACTIVE: "CONTROLLER_NOT_ACTIVE",
54
51
  CONTROLLER_METADATA_INVALID: "CONTROLLER_METADATA_INVALID",
55
52
  NODE_BOOTSTRAP_FAILED: "NODE_BOOTSTRAP_FAILED",
56
53
  NODE_ALREADY_JOINED: "NODE_ALREADY_JOINED",
@@ -106,6 +103,7 @@ var ServerErrorCodes = {
106
103
  TASK_NOT_FOUND: "TASK_NOT_FOUND",
107
104
  TASK_NOT_CANCELLABLE: "TASK_NOT_CANCELLABLE",
108
105
  TASK_OWNED_BY_ANOTHER_INSTANCE: "TASK_OWNED_BY_ANOTHER_INSTANCE",
106
+ TASK_INTERRUPTED: "TASK_INTERRUPTED",
109
107
  TOO_MANY_REQUESTS: "TOO_MANY_REQUESTS",
110
108
  HOOK_ERROR: "HOOK_ERROR",
111
109
  INTERNAL_ERROR: "INTERNAL_ERROR"
@@ -2491,6 +2489,9 @@ function metadataToWire(meta) {
2491
2489
  if (meta.vectorFields) {
2492
2490
  wire.vector_fields = meta.vectorFields;
2493
2491
  }
2492
+ if (isNonNegativeInteger(meta.documentCount) && Number.isSafeInteger(meta.documentCount)) {
2493
+ wire.document_count = meta.documentCount;
2494
+ }
2494
2495
  if (meta.embedding) {
2495
2496
  wire.embedding = meta.embedding.adapter !== void 0 ? { adapter: meta.embedding.adapter, fields: meta.embedding.fields } : { fields: meta.embedding.fields };
2496
2497
  }
@@ -2564,6 +2565,9 @@ function wireToMetadata(raw) {
2564
2565
  if (raw.vector_fields) {
2565
2566
  meta.vectorFields = raw.vector_fields;
2566
2567
  }
2568
+ if (isNonNegativeInteger(raw.document_count) && Number.isSafeInteger(raw.document_count)) {
2569
+ meta.documentCount = raw.document_count;
2570
+ }
2567
2571
  if (raw.embedding && typeof raw.embedding === "object" && typeof raw.embedding.fields === "object") {
2568
2572
  meta.embedding = typeof raw.embedding.adapter === "string" ? { adapter: raw.embedding.adapter, fields: raw.embedding.fields } : { fields: raw.embedding.fields };
2569
2573
  }
@@ -2622,7 +2626,7 @@ function wireToMetadata(raw) {
2622
2626
  if (isPositiveInteger2(promotion.threshold)) {
2623
2627
  restored.threshold = promotion.threshold;
2624
2628
  }
2625
- if (isPositiveInteger2(promotion.filter_threshold)) {
2629
+ if (isSelectivityRatio(promotion.filter_threshold)) {
2626
2630
  restored.filterThreshold = promotion.filter_threshold;
2627
2631
  }
2628
2632
  if (Object.keys(hnswConfig).length > 0) {
@@ -2649,6 +2653,9 @@ function isNonNegativeInteger(value) {
2649
2653
  function isPositiveInteger2(value) {
2650
2654
  return typeof value === "number" && Number.isInteger(value) && value >= 1;
2651
2655
  }
2656
+ function isSelectivityRatio(value) {
2657
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
2658
+ }
2652
2659
  function isWatermarkFraction(value) {
2653
2660
  return typeof value === "number" && value > 0 && value <= 1;
2654
2661
  }
@@ -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
 
@@ -8079,7 +8161,7 @@ function createRebalancer() {
8079
8161
  skipClone: true,
8080
8162
  stopWordOverride: manager.analysis.stopWords,
8081
8163
  customTokenizer: manager.analysis.customTokenizer,
8082
- collectSurfaces: manager.config.surfaceForms === true
8164
+ collectSurfaces: manager.config.surfaceForms !== false
8083
8165
  };
8084
8166
  for (const partition of currentPartitions) {
8085
8167
  const docIds = [...partition.docIds()];
@@ -8140,8 +8222,7 @@ function createRebalancer() {
8140
8222
  var FNV_OFFSET_BASIS = 2166136261;
8141
8223
  var FNV_PRIME = 16777619;
8142
8224
  var encoder = new TextEncoder();
8143
- function fnv1a(input) {
8144
- const bytes = encoder.encode(input);
8225
+ function fnv1aBytes(bytes) {
8145
8226
  let hash = FNV_OFFSET_BASIS;
8146
8227
  for (let i = 0; i < bytes.length; i++) {
8147
8228
  hash ^= bytes[i];
@@ -8149,6 +8230,9 @@ function fnv1a(input) {
8149
8230
  }
8150
8231
  return hash >>> 0;
8151
8232
  }
8233
+ function fnv1a(input) {
8234
+ return fnv1aBytes(encoder.encode(input));
8235
+ }
8152
8236
 
8153
8237
  // src/partitioning/router.ts
8154
8238
  function createPartitionRouter() {
@@ -8439,6 +8523,9 @@ function createFrozenDocTable(idTable, lengthColumns, documents, tombstones) {
8439
8523
  internalIdCapacity() {
8440
8524
  return capacity;
8441
8525
  },
8526
+ contentBytes() {
8527
+ return documents.byteLength;
8528
+ },
8442
8529
  resolver() {
8443
8530
  return resolver;
8444
8531
  }
@@ -8466,21 +8553,32 @@ function wrapEncodedDocumentTable(data) {
8466
8553
  const { blob, offsets } = data;
8467
8554
  const count = offsets.length - 1;
8468
8555
  const decoded = new Array(count);
8556
+ const encodedBytes = blob.byteLength + offsets.byteLength;
8557
+ let decodedBytes = 0;
8469
8558
  return {
8470
8559
  count,
8560
+ get byteLength() {
8561
+ return encodedBytes + decodedBytes;
8562
+ },
8471
8563
  docAt(ordinal) {
8472
8564
  let document = decoded[ordinal];
8473
8565
  if (document === void 0) {
8474
8566
  document = decode(blob.slice(offsets[ordinal], offsets[ordinal + 1]));
8475
8567
  decoded[ordinal] = document;
8568
+ decodedBytes += documentValueBytes(document);
8476
8569
  }
8477
8570
  return document;
8478
8571
  }
8479
8572
  };
8480
8573
  }
8481
8574
  function wrapDocumentArray(documents) {
8575
+ let residentBytes = 0;
8576
+ for (const document of documents) {
8577
+ residentBytes += documentValueBytes(document);
8578
+ }
8482
8579
  return {
8483
8580
  count: documents.length,
8581
+ byteLength: residentBytes,
8484
8582
  docAt(ordinal) {
8485
8583
  return documents[ordinal];
8486
8584
  }
@@ -10189,7 +10287,7 @@ function applySorting(hits, sort, sortKeyOf) {
10189
10287
  }
10190
10288
 
10191
10289
  // src/search/cursor.ts
10192
- var CURSOR_VERSION = 2;
10290
+ var CURSOR_VERSION = 3;
10193
10291
  var MAX_CURSOR_LENGTH = 40960;
10194
10292
  var MAX_SORT_FIELDS = 8;
10195
10293
  var MAX_SORT_FIELD_NAME_LENGTH = 255;
@@ -10244,6 +10342,7 @@ function encodePageCursor(cursor) {
10244
10342
  } else if (cursor.score !== null) {
10245
10343
  payload.s = cursor.score;
10246
10344
  }
10345
+ payload.q = cursor.binding;
10247
10346
  return encodeCursorText(JSON.stringify(payload));
10248
10347
  }
10249
10348
  function decodeSortKey(value, cursor) {
@@ -10288,8 +10387,11 @@ function decodePageCursor(cursor) {
10288
10387
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
10289
10388
  throw invalidCursor(cursor, "expected an object");
10290
10389
  }
10291
- const { v, a, s, k, o } = parsed;
10390
+ const { v, a, s, k, o, q } = parsed;
10292
10391
  if (v !== CURSOR_VERSION) throw invalidCursor(cursor, `unsupported cursor version ${String(v)}`);
10392
+ if (typeof q !== "string" || !/^[0-9a-f]{8}$/.test(q)) {
10393
+ throw invalidCursor(cursor, '"q" must be 8 lowercase hex digits');
10394
+ }
10293
10395
  if (typeof a !== "string" || a.length === 0) throw invalidCursor(cursor, '"a" must be a non-empty string');
10294
10396
  if (exceedsCodePoints(a, MAX_ANCHOR_CODE_POINTS)) {
10295
10397
  throw invalidCursor(cursor, '"a" is too long to be a document id');
@@ -10307,10 +10409,14 @@ function decodePageCursor(cursor) {
10307
10409
  anchor: a,
10308
10410
  score: typeof s === "number" ? s : null,
10309
10411
  sortKey,
10310
- sortSignature: typeof o === "string" ? o : null
10412
+ sortSignature: typeof o === "string" ? o : null,
10413
+ binding: q
10311
10414
  };
10312
10415
  }
10313
- function requireMatchingCursor(cursor, encoded, signature, scoreAnchored) {
10416
+ function requireMatchingCursor(cursor, encoded, signature, scoreAnchored, binding) {
10417
+ if (cursor.binding !== binding) {
10418
+ throw invalidCursor(encoded, "the cursor belongs to a different query");
10419
+ }
10314
10420
  if (signature !== null) {
10315
10421
  if (cursor.sortSignature !== signature) {
10316
10422
  throw invalidCursor(encoded, "the cursor belongs to a different sort order");
@@ -10344,6 +10450,12 @@ function requireWithinResultWindow(limit, offset) {
10344
10450
  { limit, offset, window: RESULT_WINDOW }
10345
10451
  );
10346
10452
  }
10453
+ function lastOrganicResult(sliced, pinnedIds) {
10454
+ for (let i = sliced.length - 1; i >= 0; i--) {
10455
+ if (pinnedIds === void 0 || !pinnedIds.has(sliced[i].id)) return sliced[i];
10456
+ }
10457
+ return void 0;
10458
+ }
10347
10459
  function ordersAfterAnchor(result, anchor, sort) {
10348
10460
  if (sort !== void 0 && anchor.sortKey !== null) {
10349
10461
  const comparison = compareSortValues(sort.sortKeyOf(result.id), anchor.sortKey, sort.directions) || compareCodePoints(result.id, anchor.anchor);
@@ -10354,10 +10466,10 @@ function ordersAfterAnchor(result, anchor, sort) {
10354
10466
  if (result.score < anchor.score) return true;
10355
10467
  return result.score === anchor.score && compareCodePoints(result.id, anchor.anchor) > 0;
10356
10468
  }
10357
- function applyPagination(results, limit, offset, cursor, sort) {
10469
+ function applyPagination(results, limit, offset, binding, cursor, sort, pinnedIds) {
10358
10470
  const decoded = cursor ? decodePageCursor(cursor) : null;
10359
10471
  if (decoded !== null && cursor !== void 0) {
10360
- requireMatchingCursor(decoded, cursor, sort?.signature ?? null, true);
10472
+ requireMatchingCursor(decoded, cursor, sort?.signature ?? null, true, binding);
10361
10473
  }
10362
10474
  if (limit <= 0) {
10363
10475
  return { paginated: [] };
@@ -10376,18 +10488,20 @@ function applyPagination(results, limit, offset, cursor, sort) {
10376
10488
  const sliced = results.slice(afterOffset, afterOffset + limit);
10377
10489
  let nextCursor;
10378
10490
  const hasMore = afterOffset + limit < results.length;
10379
- if (hasMore && sliced.length > 0) {
10380
- const lastResult = sliced[sliced.length - 1];
10491
+ const lastResult = lastOrganicResult(sliced, pinnedIds);
10492
+ if (hasMore && lastResult !== void 0) {
10381
10493
  nextCursor = sort !== void 0 ? encodePageCursor({
10382
10494
  anchor: lastResult.id,
10383
10495
  score: null,
10384
10496
  sortKey: sort.sortKeyOf(lastResult.id).map(toComparableSortValue),
10385
- sortSignature: sort.signature
10497
+ sortSignature: sort.signature,
10498
+ binding
10386
10499
  }) : encodePageCursor({
10387
10500
  anchor: lastResult.id,
10388
10501
  score: lastResult.score ?? null,
10389
10502
  sortKey: null,
10390
- sortSignature: null
10503
+ sortSignature: null,
10504
+ binding
10391
10505
  });
10392
10506
  }
10393
10507
  return { paginated: sliced, nextCursor };
@@ -10587,9 +10701,24 @@ function queryTermSatisfied(queryToken, indexTokens, tolerance, exact) {
10587
10701
  return false;
10588
10702
  }
10589
10703
 
10704
+ // src/partitioning/partition-selection.ts
10705
+ function partitionsIn(manager, partitionIds) {
10706
+ if (partitionIds === void 0) {
10707
+ return manager.getAllPartitions();
10708
+ }
10709
+ const partitions = [];
10710
+ for (const partitionId of partitionIds) {
10711
+ const partition = manager.partitionAt(partitionId);
10712
+ if (partition !== void 0) {
10713
+ partitions.push(partition);
10714
+ }
10715
+ }
10716
+ return partitions;
10717
+ }
10718
+
10590
10719
  // src/partitioning/fan-out.ts
10591
10720
  async function fanOutQuery(manager, params, language, schema, config, searchOptions) {
10592
- const partitions = config.partitionIds ? config.partitionIds.map((id) => manager.partitionAt(id)).filter((p) => p !== void 0) : manager.getAllPartitions();
10721
+ const partitions = partitionsIn(manager, config.partitionIds);
10593
10722
  if (partitions.length === 0) {
10594
10723
  return { scored: [], totalMatched: 0 };
10595
10724
  }
@@ -11018,6 +11147,19 @@ function createPartitionManager(indexName, config, language, router, initialPart
11018
11147
  return manager;
11019
11148
  }
11020
11149
 
11150
+ // src/partitioning/match-count.ts
11151
+ function countsWithoutScores(params) {
11152
+ return params.minScore === void 0 && (params.termMatch === void 0 || params.termMatch === "any");
11153
+ }
11154
+ function fanOutMatchCount(manager, params, language, schema, options) {
11155
+ let count = 0;
11156
+ for (const partition of partitionsIn(manager, options?.partitionIds)) {
11157
+ const matches = fulltextMatches(partition, params, language, schema, options?.searchOptions);
11158
+ if (matches !== null) count += matches.count;
11159
+ }
11160
+ return count;
11161
+ }
11162
+
11021
11163
  // src/vector/scalar-quantization-calibration.ts
11022
11164
  var PADDING_FACTOR = 0.01;
11023
11165
  function computeCalibrationBounds(vectors, dimensions) {
@@ -11247,7 +11389,7 @@ function createScalarQuantizer(dimensions, ordinalSource) {
11247
11389
  let liveCount = 0;
11248
11390
  let constants = deriveSq8Constants(0, 0, dimensions);
11249
11391
  let calibrated = false;
11250
- function ensureCapacity2(needed) {
11392
+ function ensureCapacity3(needed) {
11251
11393
  if (needed <= capacity) return;
11252
11394
  let newCap = capacity === 0 ? INITIAL_CAPACITY2 : capacity;
11253
11395
  while (newCap < needed) newCap *= 2;
@@ -11377,7 +11519,7 @@ function createScalarQuantizer(dimensions, ordinalSource) {
11377
11519
  liveCount = 0;
11378
11520
  for (const [docId, vec] of collected) {
11379
11521
  const ord = resolveOrdinal(docId);
11380
- ensureCapacity2(ord + 1);
11522
+ ensureCapacity3(ord + 1);
11381
11523
  quantizeInto(ord, vec);
11382
11524
  storeEntry(docId, ord);
11383
11525
  }
@@ -11387,7 +11529,7 @@ function createScalarQuantizer(dimensions, ordinalSource) {
11387
11529
  calibrateFromVectors([vector]);
11388
11530
  }
11389
11531
  const ord = resolveOrdinal(docId);
11390
- ensureCapacity2(ord + 1);
11532
+ ensureCapacity3(ord + 1);
11391
11533
  quantizeInto(ord, vector);
11392
11534
  storeEntry(docId, ord);
11393
11535
  },
@@ -11454,7 +11596,7 @@ function createScalarQuantizer(dimensions, ordinalSource) {
11454
11596
  },
11455
11597
  restoreEntry(docId, quantized, sum, sumSq) {
11456
11598
  const ord = resolveOrdinal(docId);
11457
- ensureCapacity2(ord + 1);
11599
+ ensureCapacity3(ord + 1);
11458
11600
  const base = ord * dimensions;
11459
11601
  quantizedArena.set(quantized.subarray(0, dimensions), base);
11460
11602
  sums[ord] = sum;
@@ -11605,7 +11747,7 @@ function createVectorStore() {
11605
11747
  arena = new Float32Array(simd.memory.buffer);
11606
11748
  }
11607
11749
  }
11608
- function ensureCapacity2(needed) {
11750
+ function ensureCapacity3(needed) {
11609
11751
  if (needed <= capacity) return;
11610
11752
  let newCap = capacity === 0 ? INITIAL_CAPACITY3 : capacity;
11611
11753
  while (newCap < needed) newCap *= 2;
@@ -11687,7 +11829,7 @@ function createVectorStore() {
11687
11829
  recycledOrds.delete(docId);
11688
11830
  docToOrd.set(docId, recycled);
11689
11831
  ordToDoc[recycled] = docId;
11690
- ensureCapacity2(recycled + 1);
11832
+ ensureCapacity3(recycled + 1);
11691
11833
  writeVector(recycled, vector);
11692
11834
  recordPartition(recycled, partitionId, false);
11693
11835
  liveCount++;
@@ -11696,7 +11838,7 @@ function createVectorStore() {
11696
11838
  const ord = ordToDoc.length;
11697
11839
  docToOrd.set(docId, ord);
11698
11840
  ordToDoc.push(docId);
11699
- ensureCapacity2(ord + 1);
11841
+ ensureCapacity3(ord + 1);
11700
11842
  writeVector(ord, vector);
11701
11843
  recordPartition(ord, partitionId, false);
11702
11844
  liveCount++;
@@ -11862,7 +12004,7 @@ function createVectorStore() {
11862
12004
  }
11863
12005
  if (snapshot.dimension === 0 || snapshot.slots === 0) return;
11864
12006
  initStorage(snapshot.dimension);
11865
- ensureCapacity2(snapshot.slots);
12007
+ ensureCapacity3(snapshot.slots);
11866
12008
  arena.set(snapshot.vectors.subarray(0, snapshot.slots * snapshot.dimension), scratchFloatLength);
11867
12009
  mags.set(snapshot.magnitudes.subarray(0, Math.min(snapshot.slots, mags.length)));
11868
12010
  ensurePartitionSlots(snapshot.slots);
@@ -13015,20 +13157,43 @@ function resolveWorkerCount(requested) {
13015
13157
  }
13016
13158
  function createWorkerPool(config) {
13017
13159
  const workerCount = resolveWorkerCount(config.count);
13018
- const workers = [];
13160
+ const workers = /* @__PURE__ */ new Map();
13161
+ const deadSlots = /* @__PURE__ */ new Set();
13019
13162
  const indexAssignment = /* @__PURE__ */ new Map();
13020
13163
  let isShutdown = false;
13164
+ function handleWorkerDeath(slotIndex, error) {
13165
+ if (isShutdown || deadSlots.has(slotIndex)) {
13166
+ return;
13167
+ }
13168
+ deadSlots.add(slotIndex);
13169
+ const slot = workers.get(slotIndex);
13170
+ const indexNames = slot ? [...slot.indexes].sort() : [];
13171
+ workers.delete(slotIndex);
13172
+ config.onWorkerCrash?.(slotIndex, indexNames, error);
13173
+ }
13021
13174
  function ensureWorker(slotIndex) {
13022
- if (!workers[slotIndex]) {
13023
- workers[slotIndex] = {
13024
- executor: config.workerFactory(slotIndex),
13175
+ if (deadSlots.has(slotIndex)) {
13176
+ return void 0;
13177
+ }
13178
+ let slot = workers.get(slotIndex);
13179
+ if (!slot) {
13180
+ slot = {
13181
+ executor: config.workerFactory(slotIndex, (error) => handleWorkerDeath(slotIndex, error)),
13025
13182
  indexes: /* @__PURE__ */ new Set()
13026
13183
  };
13184
+ workers.set(slotIndex, slot);
13027
13185
  }
13028
- return workers[slotIndex];
13186
+ return slot;
13029
13187
  }
13030
13188
  function assignSlot(indexName) {
13031
- return fnv1a(indexName) % workerCount;
13189
+ const preferred = fnv1a(indexName) % workerCount;
13190
+ for (let probe = 0; probe < workerCount; probe++) {
13191
+ const candidate = (preferred + probe) % workerCount;
13192
+ if (!deadSlots.has(candidate)) {
13193
+ return candidate;
13194
+ }
13195
+ }
13196
+ throw new NarsilError(ErrorCodes.WORKER_CRASHED, `Every worker in the pool has crashed`);
13032
13197
  }
13033
13198
  function addIndex(indexName) {
13034
13199
  if (isShutdown) {
@@ -13039,18 +13204,14 @@ function createWorkerPool(config) {
13039
13204
  }
13040
13205
  const slotIndex = assignSlot(indexName);
13041
13206
  const slot = ensureWorker(slotIndex);
13207
+ if (!slot) {
13208
+ throw new NarsilError(ErrorCodes.WORKER_CRASHED, `Worker ${slotIndex} for index "${indexName}" has crashed`);
13209
+ }
13042
13210
  slot.indexes.add(indexName);
13043
13211
  indexAssignment.set(indexName, slotIndex);
13044
13212
  }
13045
13213
  function removeIndex(indexName) {
13046
- const slotIndex = indexAssignment.get(indexName);
13047
- if (slotIndex === void 0) {
13048
- return;
13049
- }
13050
- const slot = workers[slotIndex];
13051
- if (slot) {
13052
- slot.indexes.delete(indexName);
13053
- }
13214
+ for (const slot of workers.values()) slot.indexes.delete(indexName);
13054
13215
  indexAssignment.delete(indexName);
13055
13216
  }
13056
13217
  function getExecutor(indexName) {
@@ -13061,15 +13222,22 @@ function createWorkerPool(config) {
13061
13222
  if (slotIndex === void 0) {
13062
13223
  throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, `Index "${indexName}" is not registered in the worker pool`);
13063
13224
  }
13064
- return workers[slotIndex].executor;
13225
+ const slot = workers.get(slotIndex);
13226
+ if (slot) {
13227
+ return slot.executor;
13228
+ }
13229
+ for (const survivor of workers.values()) {
13230
+ if (survivor.indexes.has(indexName)) {
13231
+ return survivor.executor;
13232
+ }
13233
+ }
13234
+ throw new NarsilError(ErrorCodes.WORKER_CRASHED, `Every worker holding index "${indexName}" has crashed`);
13065
13235
  }
13066
13236
  async function getMemoryStats() {
13067
13237
  const indices = [];
13068
13238
  const pending = [];
13069
- for (let i = 0; i < workers.length; i++) {
13070
- const slot = workers[i];
13071
- if (!slot) continue;
13072
- indices.push(i);
13239
+ for (const [slotIndex, slot] of workers) {
13240
+ indices.push(slotIndex);
13073
13241
  pending.push(
13074
13242
  slot.executor.execute({ type: "memoryReport", requestId: createRequestId() }).catch(() => null)
13075
13243
  );
@@ -13092,7 +13260,7 @@ function createWorkerPool(config) {
13092
13260
  return;
13093
13261
  }
13094
13262
  isShutdown = true;
13095
- const shutdownPromises = workers.filter(Boolean).map((slot) => {
13263
+ const shutdownPromises = [...workers.values()].map((slot) => {
13096
13264
  const timeoutPromise = new Promise((resolve) => {
13097
13265
  setTimeout(resolve, 5e3);
13098
13266
  });
@@ -13102,17 +13270,16 @@ function createWorkerPool(config) {
13102
13270
  indexAssignment.clear();
13103
13271
  }
13104
13272
  function getAllExecutors() {
13105
- return workers.filter(Boolean).map((slot) => slot.executor);
13273
+ return [...workers.values()].map((slot) => slot.executor);
13106
13274
  }
13107
13275
  function addIndexToAll(indexName) {
13108
13276
  if (isShutdown) {
13109
13277
  throw new NarsilError(ErrorCodes.WORKER_CRASHED, "Worker pool has been shut down");
13110
13278
  }
13111
13279
  for (let i = 0; i < workerCount; i++) {
13112
- const slot = ensureWorker(i);
13113
- slot.indexes.add(indexName);
13280
+ ensureWorker(i)?.indexes.add(indexName);
13114
13281
  }
13115
- indexAssignment.set(indexName, 0);
13282
+ indexAssignment.set(indexName, assignSlot(indexName));
13116
13283
  }
13117
13284
  return {
13118
13285
  getExecutor,
@@ -14242,6 +14409,9 @@ function createDirectExecutor() {
14242
14409
  }
14243
14410
  function dropIndex(indexName) {
14244
14411
  const entry = requireIndex(indexName);
14412
+ for (const vectorIndex of entry.vectorIndexes.values()) {
14413
+ vectorIndex.dispose();
14414
+ }
14245
14415
  for (const partition of entry.manager.getAllPartitions()) {
14246
14416
  partition.clear();
14247
14417
  }
@@ -14365,6 +14535,12 @@ function createDirectExecutor() {
14365
14535
  }
14366
14536
  case "preflight": {
14367
14537
  const entry = requireIndex(action.indexName);
14538
+ if (countsWithoutScores(action.params)) {
14539
+ const count = fanOutMatchCount(entry.manager, action.params, entry.language, entry.config.schema, {
14540
+ searchOptions: entry.searchOptions
14541
+ });
14542
+ return { count };
14543
+ }
14368
14544
  const result = await fanOutQuery(
14369
14545
  entry.manager,
14370
14546
  action.params,
@@ -14530,7 +14706,7 @@ function createAnalysisRebuildCoordinator(config, deps) {
14530
14706
  async function rebuildNow(indexName, entry) {
14531
14707
  const manager = deps.getManager(indexName);
14532
14708
  if (manager === void 0) {
14533
- stale.delete(indexName);
14709
+ entry.run = null;
14534
14710
  return;
14535
14711
  }
14536
14712
  const partitionCount = manager.partitionCount;
@@ -14582,6 +14758,23 @@ function createAnalysisRebuildCoordinator(config, deps) {
14582
14758
  const documentCount = deps.getManager(indexName)?.countDocuments() ?? 0;
14583
14759
  await config?.onStaleAnalysis?.({ ...entry.index, documentCount }, () => enqueue2(indexName, entry));
14584
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
+ }
14585
14778
  return {
14586
14779
  markStale(index) {
14587
14780
  const existing = stale.get(index.indexName);
@@ -14596,23 +14789,16 @@ function createAnalysisRebuildCoordinator(config, deps) {
14596
14789
  isStale(indexName) {
14597
14790
  return stale.has(indexName);
14598
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
+ },
14599
14799
  async reviewStaleIndexes() {
14600
14800
  for (const [indexName, entry] of [...stale]) {
14601
- if (entry.run !== null) continue;
14602
- try {
14603
- await announce(indexName, entry);
14604
- } catch (err) {
14605
- deps.emit({
14606
- indexName,
14607
- status: "failed",
14608
- partitionsRebuilt: 0,
14609
- partitionCount: deps.getManager(indexName)?.partitionCount ?? 0,
14610
- error: toError(err)
14611
- });
14612
- }
14613
- if (rebuildMode === "auto") {
14614
- void enqueue2(indexName, entry).catch(() => void 0);
14615
- }
14801
+ await review(indexName, entry);
14616
14802
  }
14617
14803
  },
14618
14804
  rebuild(indexName) {
@@ -15178,6 +15364,9 @@ function writeHeader(header) {
15178
15364
  }
15179
15365
  return buffer;
15180
15366
  }
15367
+ function hasNrslMagic(data) {
15368
+ return data.length >= MAGIC_BYTES.length && data[0] === MAGIC_BYTES[0] && data[1] === MAGIC_BYTES[1] && data[2] === MAGIC_BYTES[2] && data[3] === MAGIC_BYTES[3];
15369
+ }
15181
15370
  function readHeader(data) {
15182
15371
  if (data.length < HEADER_SIZE) {
15183
15372
  throw new NarsilError(
@@ -15213,6 +15402,7 @@ function readHeader(data) {
15213
15402
  // src/serialization/envelope.ts
15214
15403
  var CURRENT_ENVELOPE_VERSION = 1;
15215
15404
  var SNAPSHOT_ENVELOPE_VERSION = 2;
15405
+ var INDEX_SNAPSHOT_ENVELOPE_VERSION = 3;
15216
15406
  function parseEngineVersion(version) {
15217
15407
  const parts = version.split(".");
15218
15408
  return [parseInt(parts[0] ?? "0", 10), parseInt(parts[1] ?? "0", 10), parseInt(parts[2] ?? "0", 10)];
@@ -15357,6 +15547,19 @@ function concatEnvelopeParts(parts) {
15357
15547
  async function unpackEnvelopeBytes(data) {
15358
15548
  return unpackEnvelope(data, SNAPSHOT_ENVELOPE_VERSION);
15359
15549
  }
15550
+ async function packIndexSnapshotEnvelope(payloadBytes) {
15551
+ return packEnvelope(payloadBytes, { checksum: true, envelopeFormatVersion: INDEX_SNAPSHOT_ENVELOPE_VERSION });
15552
+ }
15553
+ async function unpackIndexSnapshotEnvelope(data) {
15554
+ const { header, payloadBytes } = await unpackEnvelope(data, INDEX_SNAPSHOT_ENVELOPE_VERSION);
15555
+ if (header.envelopeFormatVersion !== INDEX_SNAPSHOT_ENVELOPE_VERSION) {
15556
+ throw new NarsilError(
15557
+ ErrorCodes.ENVELOPE_VERSION_MISMATCH,
15558
+ `Expected an index snapshot envelope of format v${INDEX_SNAPSHOT_ENVELOPE_VERSION}, received format v${header.envelopeFormatVersion}`
15559
+ );
15560
+ }
15561
+ return payloadBytes;
15562
+ }
15360
15563
  async function writeMetadataEnvelope(meta, options = {}) {
15361
15564
  const payloadBytes = serializeMetadata(meta);
15362
15565
  return packEnvelope(payloadBytes, options);
@@ -16044,6 +16247,9 @@ function mergeTimeOrderedSegments(ordered, fallback) {
16044
16247
  validateMergedPartition(merged);
16045
16248
  return merged;
16046
16249
  }
16250
+ function countLiveDocuments(ordered) {
16251
+ return resolveWinners(ordered).size;
16252
+ }
16047
16253
  function resolveWinners(ordered) {
16048
16254
  const winner = /* @__PURE__ */ new Map();
16049
16255
  for (let segIndex = 0; segIndex < ordered.length; segIndex += 1) {
@@ -17297,6 +17503,7 @@ async function writeSegmentedCheckpoint(input) {
17297
17503
  });
17298
17504
  }
17299
17505
  const partitions = [];
17506
+ let wholeDocumentCount = input.wholePartitionPayload === void 0 ? null : 0;
17300
17507
  for (const target of input.targets) {
17301
17508
  const priorPartition = priorManifest?.partitions.find((p) => p.partitionId === target.partitionId);
17302
17509
  const priorSeqNo = snapshotCheckpointFor(priorManifest?.checkpoint ?? [], target.partitionId);
@@ -17307,15 +17514,13 @@ async function writeSegmentedCheckpoint(input) {
17307
17514
  priorSeqNo,
17308
17515
  target.lastSeqNo
17309
17516
  );
17310
- partitions.push(
17311
- input.wholePartitionPayload === void 0 ? await writePartition(context, target.partitionId, priorPartition, entries) : await writeWholePartition(
17312
- context,
17313
- target.partitionId,
17314
- priorPartition,
17315
- input.wholePartitionPayload(target.partitionId),
17316
- entries
17317
- )
17318
- );
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
+ }
17319
17524
  }
17320
17525
  carryForwardUncheckpointedPartitions(priorManifest, input.targets, partitions, checkpointByPartition);
17321
17526
  const manifest = {
@@ -17327,6 +17532,7 @@ async function writeSegmentedCheckpoint(input) {
17327
17532
  const parts = await encodeSegmentManifest(manifest);
17328
17533
  await directory.atomicWrite(manifestKey(indexName), [parts.header, parts.payload]);
17329
17534
  await collectGarbage(directory, indexName, priorManifest, manifest);
17535
+ return wholeDocumentCount;
17330
17536
  }
17331
17537
  async function writePartition(context, partitionId, priorPartition, entries) {
17332
17538
  let segments = priorPartition ? [...priorPartition.segments] : [];
@@ -17446,9 +17652,9 @@ async function writeIndexCheckpoint(input) {
17446
17652
  const { directory, metadata, targets, compactionThreshold, manager } = input;
17447
17653
  const offloaded = !input.fromMemory && input.canOffload && metadata.tokenizer === void 0 && metadata.stopWords === void 0 && await runCheckpointOnWorker({ root: directory.root, metadata, targets, compactionThreshold });
17448
17654
  if (offloaded) {
17449
- return;
17655
+ return null;
17450
17656
  }
17451
- await writeSegmentedCheckpoint({
17657
+ return writeSegmentedCheckpoint({
17452
17658
  directory,
17453
17659
  metadata,
17454
17660
  targets,
@@ -17480,9 +17686,171 @@ function createSeqOwner(startSeqNo, primaryTerm = SINGLE_NODE_PRIMARY_TERM) {
17480
17686
  };
17481
17687
  }
17482
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
+
17483
17850
  // src/persistence/durability/types.ts
17484
17851
  var DEFAULT_CHECKPOINT_INTERVAL_MS = 3e5;
17485
17852
  var DEFAULT_CHECKPOINT_MUTATION_THRESHOLD = 1e5;
17853
+ var DEFAULT_ASYNC_FLUSH_INTERVAL_MS = 1e3;
17486
17854
 
17487
17855
  // src/persistence/durability/group-commit.ts
17488
17856
  function createGroupCommitCoordinator(sync) {
@@ -17728,7 +18096,6 @@ function createWalWriter(directory, config) {
17728
18096
  }
17729
18097
 
17730
18098
  // src/persistence/durability/manager.ts
17731
- var DEFAULT_ASYNC_FLUSH_INTERVAL_MS = 1e3;
17732
18099
  function createDurabilityManager(config, hooks, directoryOverride) {
17733
18100
  const directory = createDurableDirectory(config.directory ?? "");
17734
18101
  const canOffloadCheckpoint = directoryOverride === void 0;
@@ -17759,20 +18126,20 @@ function createDurabilityManager(config, hooks, directoryOverride) {
17759
18126
  }
17760
18127
  hooks.onFatalError(error);
17761
18128
  }
17762
- function toError3(err) {
18129
+ function toError4(err) {
17763
18130
  return err instanceof Error ? err : new Error(String(err));
17764
18131
  }
17765
18132
  async function closeWriterReportingFailure(closePromise) {
17766
18133
  try {
17767
18134
  await closePromise;
17768
18135
  } catch (err) {
17769
- markFatal(toError3(err));
18136
+ markFatal(toError4(err));
17770
18137
  }
17771
18138
  }
17772
18139
  function getOrCreateIndexState(indexName) {
17773
18140
  let state = indexes.get(indexName);
17774
18141
  if (state === void 0) {
17775
- state = { partitions: /* @__PURE__ */ new Map(), mutationsSinceCheckpoint: 0, checkpointInFlight: null };
18142
+ state = { partitions: /* @__PURE__ */ new Map(), mutationsSinceCheckpoint: 0, checkpointInFlight: null, unloading: false };
17776
18143
  indexes.set(indexName, state);
17777
18144
  }
17778
18145
  return state;
@@ -17835,7 +18202,7 @@ function createDurabilityManager(config, hooks, directoryOverride) {
17835
18202
  try {
17836
18203
  await partition.walWriter.commit();
17837
18204
  } catch (err) {
17838
- partition.failed = toError3(err);
18205
+ partition.failed = toError4(err);
17839
18206
  markFatal(partition.failed);
17840
18207
  }
17841
18208
  }
@@ -17849,7 +18216,7 @@ function createDurabilityManager(config, hooks, directoryOverride) {
17849
18216
  try {
17850
18217
  await checkpointIndex(indexName);
17851
18218
  } catch (err) {
17852
- markFatal(toError3(err));
18219
+ markFatal(toError4(err));
17853
18220
  }
17854
18221
  }
17855
18222
  }
@@ -17866,7 +18233,7 @@ function createDurabilityManager(config, hooks, directoryOverride) {
17866
18233
  }
17867
18234
  async function checkpointIndex(indexName, fromMemory = false) {
17868
18235
  const indexState = indexes.get(indexName);
17869
- if (indexState === void 0) {
18236
+ if (indexState === void 0 || indexState.unloading) {
17870
18237
  return;
17871
18238
  }
17872
18239
  if (indexState.checkpointInFlight !== null) {
@@ -17882,82 +18249,46 @@ function createDurabilityManager(config, hooks, directoryOverride) {
17882
18249
  return run;
17883
18250
  }
17884
18251
  async function performCheckpoint(indexName, indexState, fromMemory = false) {
17885
- const manager = hooks.getManager(indexName);
17886
- if (manager === void 0) {
17887
- return;
17888
- }
17889
- const metadata = hooks.buildMetadata(indexName);
17890
- if (metadata === void 0) {
17891
- return;
17892
- }
17893
- const targets = [];
17894
- for (let i = 0; i < manager.partitionCount; i += 1) {
17895
- const partition = indexState.partitions.get(i);
17896
- targets.push({
17897
- partitionId: i,
17898
- lastSeqNo: partition?.appliedSeqNo ?? 0,
17899
- primaryTerm: partition?.seqOwner.primaryTerm ?? SINGLE_NODE_PRIMARY_TERM
17900
- });
17901
- if (partition !== void 0) {
17902
- try {
17903
- await partition.walWriter.commit();
17904
- } catch (err) {
17905
- partition.failed = toError3(err);
17906
- markFatal(partition.failed);
17907
- throw partition.failed;
17908
- }
17909
- }
17910
- }
17911
- await writeIndexCheckpoint({
18252
+ await runDurableCheckpoint({
17912
18253
  directory,
17913
- metadata,
17914
- targets,
18254
+ hooks,
18255
+ indexName,
18256
+ indexState,
17915
18257
  compactionThreshold,
17916
- manager,
17917
18258
  canOffload: canOffloadCheckpoint,
17918
- fromMemory
18259
+ fromMemory,
18260
+ queueMetadataWrite,
18261
+ markFatal
17919
18262
  });
17920
- await truncateCoveredSegments(directory, indexName, targets);
17921
- await reclaimWalBeyondCount(directory, indexName, manager.partitionCount, indexState.partitions, markFatal);
17922
- indexState.mutationsSinceCheckpoint = 0;
17923
18263
  }
17924
- async function recoverIndex(indexName) {
17925
- const metadata = await loadMetadata(directory, indexName);
17926
- if (metadata === null) {
17927
- return;
17928
- }
17929
- await hooks.createIndexFromMetadata(metadata);
17930
- const manager = hooks.getManager(indexName);
17931
- if (manager === void 0) {
17932
- return;
17933
- }
17934
- const deps = {
17935
- manager,
17936
- vectorFieldPaths: hooks.getVectorFieldPaths(indexName),
17937
- vectorIndexes: hooks.getVectorIndexes(indexName)
17938
- };
17939
- const checkpoint = await loadSnapshot(directory, indexName, deps);
17940
- const manifest = await readSegmentManifest(directory, indexName);
17941
- if (manifest !== null) {
17942
- await reclaimOrphanedSegments(directory, indexName, manifest);
17943
- }
17944
- for (let partitionId = 0; partitionId < manager.partitionCount; partitionId += 1) {
17945
- const fromSeqNo = snapshotCheckpointFor(checkpoint, partitionId);
17946
- const { highestSeqNo } = await replayWal(directory, indexName, partitionId, fromSeqNo, deps);
17947
- getOrCreatePartition(indexName, partitionId, highestSeqNo);
17948
- }
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
+ );
17949
18277
  }
17950
18278
  return {
17951
18279
  isActive() {
17952
18280
  return true;
17953
18281
  },
17954
- async recover() {
18282
+ async recover(metadataOnly = false) {
17955
18283
  const names = await listPersistedIndexes(directory);
17956
18284
  for (const indexName of names) {
17957
- await recoverIndex(indexName);
18285
+ await recoverIndex(indexName, metadataOnly);
17958
18286
  }
17959
18287
  startCheckpointTimer();
17960
18288
  },
18289
+ recoverIndex(indexName) {
18290
+ return recoverIndex(indexName);
18291
+ },
17961
18292
  highestPersistedSeqNo(indexName, partitionId) {
17962
18293
  return indexes.get(indexName)?.partitions.get(partitionId)?.appliedSeqNo ?? 0;
17963
18294
  },
@@ -17981,7 +18312,7 @@ function createDurabilityManager(config, hooks, directoryOverride) {
17981
18312
  const entry = buildMutationEntry(record, allocatedSeqNo);
17982
18313
  await partition.walWriter.append(entry);
17983
18314
  } catch (err) {
17984
- partition.failed = toError3(err);
18315
+ partition.failed = toError4(err);
17985
18316
  markFatal(partition.failed);
17986
18317
  throw partition.failed;
17987
18318
  }
@@ -17993,7 +18324,7 @@ function createDurabilityManager(config, hooks, directoryOverride) {
17993
18324
  try {
17994
18325
  await partition.walWriter.commit();
17995
18326
  } catch (err) {
17996
- partition.failed = toError3(err);
18327
+ partition.failed = toError4(err);
17997
18328
  markFatal(partition.failed);
17998
18329
  throw partition.failed;
17999
18330
  }
@@ -18003,7 +18334,7 @@ function createDurabilityManager(config, hooks, directoryOverride) {
18003
18334
  startAsyncFlushTimer();
18004
18335
  if (indexState.mutationsSinceCheckpoint >= checkpointMutationThreshold) {
18005
18336
  void checkpointIndex(record.indexName).catch((err) => {
18006
- markFatal(toError3(err));
18337
+ markFatal(toError4(err));
18007
18338
  });
18008
18339
  }
18009
18340
  return allocatedSeqNo;
@@ -18033,15 +18364,23 @@ function createDurabilityManager(config, hooks, directoryOverride) {
18033
18364
  async removeIndex(indexName) {
18034
18365
  const indexState = indexes.get(indexName);
18035
18366
  if (indexState !== void 0) {
18036
- for (const partition of indexState.partitions.values()) {
18037
- await closeWriterReportingFailure(partition.walWriter.close());
18038
- }
18367
+ await drainIndexStateForUnload(indexState, metadataWrites.get(indexName), closeWriterReportingFailure);
18039
18368
  indexes.delete(indexName);
18369
+ metadataWrites.delete(indexName);
18040
18370
  }
18041
18371
  for (const key of await directory.list(`${indexName}/`)) {
18042
18372
  await directory.remove(key);
18043
18373
  }
18044
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
+ },
18045
18384
  async shutdown() {
18046
18385
  shuttingDown = true;
18047
18386
  if (checkpointTimer !== null) {
@@ -18080,7 +18419,13 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
18080
18419
  function getOrCreateIndexState(indexName) {
18081
18420
  let state = indexes.get(indexName);
18082
18421
  if (state === void 0) {
18083
- 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
+ };
18084
18429
  indexes.set(indexName, state);
18085
18430
  }
18086
18431
  return state;
@@ -18111,10 +18456,11 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
18111
18456
  if (manager === void 0 || metadata === void 0) {
18112
18457
  return;
18113
18458
  }
18459
+ const documentCount = manager.countDocuments();
18114
18460
  const seqNoByPartition = /* @__PURE__ */ new Map();
18115
18461
  const primaryTermByPartition = /* @__PURE__ */ new Map();
18116
18462
  for (let i = 0; i < manager.partitionCount; i += 1) {
18117
- seqNoByPartition.set(i, 0);
18463
+ seqNoByPartition.set(i, indexState.appliedSeqNoByPartition.get(i) ?? 0);
18118
18464
  primaryTermByPartition.set(i, SINGLE_NODE_PRIMARY_TERM);
18119
18465
  }
18120
18466
  const { parts } = await buildSnapshotBundleBytes({
@@ -18130,6 +18476,13 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
18130
18476
  primaryTermByPartition
18131
18477
  });
18132
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
+ });
18133
18486
  indexState.mutationsSinceCheckpoint = 0;
18134
18487
  if (publisher !== void 0) {
18135
18488
  const partitionIds = [];
@@ -18139,15 +18492,18 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
18139
18492
  await publisher.publishPartitions(indexName, partitionIds);
18140
18493
  }
18141
18494
  }
18142
- async function checkpointIndex(indexName) {
18495
+ async function checkpointIndex(indexName, forceFresh = false) {
18143
18496
  const indexState = indexes.get(indexName);
18144
- if (indexState === void 0) {
18497
+ if (indexState === void 0 || indexState.unloading) {
18145
18498
  return;
18146
18499
  }
18147
18500
  if (indexState.checkpointInFlight !== null) {
18148
- return indexState.checkpointInFlight;
18501
+ await indexState.checkpointInFlight;
18502
+ if (!forceFresh) {
18503
+ return;
18504
+ }
18149
18505
  }
18150
- const run = performCheckpoint(indexName, indexState).finally(() => {
18506
+ const run = indexState.applyChain.then(() => performCheckpoint(indexName, indexState)).finally(() => {
18151
18507
  indexState.checkpointInFlight = null;
18152
18508
  });
18153
18509
  indexState.checkpointInFlight = run;
@@ -18162,13 +18518,35 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
18162
18518
  );
18163
18519
  return queued;
18164
18520
  }
18165
- async function recoverIndex(indexName) {
18521
+ async function recoverIndex(indexName, metadataOnly = false) {
18166
18522
  const metaBytes = await adapter.load(metadataKey(indexName));
18167
18523
  if (metaBytes === null) {
18168
18524
  return;
18169
18525
  }
18170
18526
  const { metadata } = await readMetadataEnvelope(metaBytes);
18171
- 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
+ }
18172
18550
  const manager = hooks.getManager(indexName);
18173
18551
  if (manager === void 0) {
18174
18552
  return;
@@ -18177,33 +18555,58 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
18177
18555
  if (snapshotBytes === null) {
18178
18556
  return;
18179
18557
  }
18180
- await loadSnapshotBundleBytes(snapshotBytes, {
18558
+ const checkpoint = await loadSnapshotBundleBytes(snapshotBytes, {
18181
18559
  manager,
18182
18560
  vectorFieldPaths: hooks.getVectorFieldPaths(indexName),
18183
18561
  vectorIndexes: hooks.getVectorIndexes(indexName)
18184
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);
18185
18577
  }
18186
18578
  return {
18187
18579
  isActive() {
18188
18580
  return true;
18189
18581
  },
18190
- async recover() {
18582
+ async recover(metadataOnly = false) {
18191
18583
  const keys = await adapter.list("");
18192
18584
  for (const key of keys) {
18193
18585
  if (key.endsWith("/meta")) {
18194
- await recoverIndex(key.slice(0, -"/meta".length));
18586
+ await recoverIndex(key.slice(0, -"/meta".length), metadataOnly);
18195
18587
  }
18196
18588
  }
18197
18589
  startCheckpointTimer();
18198
18590
  },
18199
- highestPersistedSeqNo() {
18200
- return 0;
18591
+ recoverIndex(indexName) {
18592
+ return recoverIndex(indexName);
18593
+ },
18594
+ highestPersistedSeqNo(indexName, partitionId) {
18595
+ return indexes.get(indexName)?.appliedSeqNoByPartition.get(partitionId) ?? 0;
18201
18596
  },
18202
18597
  async recordMutation(record) {
18203
18598
  const indexState = getOrCreateIndexState(record.indexName);
18204
- const buffered = indexState.applyChain.then(() => record.apply());
18205
- indexState.applyChain = buffered.catch(() => void 0);
18206
- 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;
18207
18610
  indexState.mutationsSinceCheckpoint += 1;
18208
18611
  startCheckpointTimer();
18209
18612
  if (indexState.mutationsSinceCheckpoint >= checkpointMutationThreshold) {
@@ -18211,7 +18614,7 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
18211
18614
  hooks.onFatalError(err instanceof Error ? err : new Error(String(err)));
18212
18615
  });
18213
18616
  }
18214
- return 0;
18617
+ return appliedSeqNo;
18215
18618
  },
18216
18619
  persistMetadata(indexName) {
18217
18620
  return queueMetadataWrite(indexName, async () => {
@@ -18227,17 +18630,49 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
18227
18630
  checkpoint(indexName) {
18228
18631
  return checkpointIndex(indexName);
18229
18632
  },
18633
+ checkpointFromMemory(indexName) {
18634
+ return checkpointIndex(indexName, true);
18635
+ },
18230
18636
  async checkpointAll() {
18231
18637
  for (const indexName of [...indexes.keys()]) {
18232
18638
  await checkpointIndex(indexName);
18233
18639
  }
18234
18640
  },
18235
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
+ }
18236
18654
  indexes.delete(indexName);
18655
+ metadataWrites.delete(indexName);
18237
18656
  for (const key of await adapter.list(`${indexName}/`)) {
18238
18657
  await adapter.delete(key);
18239
18658
  }
18240
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
+ },
18241
18676
  async reloadIndex(indexName) {
18242
18677
  const indexState = getOrCreateIndexState(indexName);
18243
18678
  const reload = indexState.applyChain.then(() => recoverIndex(indexName));
@@ -18261,21 +18696,22 @@ function createSnapshotOnlyManager(adapter, config, hooks, publisher) {
18261
18696
  }
18262
18697
 
18263
18698
  // src/engine/durability-integration.ts
18264
- function buildMetadata(indexName, hooks) {
18699
+ function buildMetadata(indexName, hooks, documentCount) {
18265
18700
  const config = hooks.getIndexConfig(indexName);
18266
18701
  const manager = hooks.getManager(indexName);
18267
- if (config === void 0 || manager === void 0) {
18702
+ if (config === void 0) {
18268
18703
  return void 0;
18269
18704
  }
18270
18705
  const metadata = {
18271
18706
  indexName,
18272
18707
  schema: config.schema,
18273
18708
  language: config.language,
18274
- partitionCount: manager.partitionCount,
18709
+ partitionCount: manager?.partitionCount ?? config.partitionCount,
18275
18710
  bm25Params: { k1: config.k1, b: config.b },
18276
18711
  createdAt: Date.now(),
18277
18712
  engineVersion: VERSION
18278
18713
  };
18714
+ metadata.documentCount = documentCount ?? config.documentCount;
18279
18715
  if (config.embedding) {
18280
18716
  metadata.embedding = config.embedding;
18281
18717
  }
@@ -18325,8 +18761,9 @@ function createDurabilityIntegration(tier, hooks) {
18325
18761
  getManager: hooks.getManager,
18326
18762
  getVectorFieldPaths: hooks.getVectorFieldPaths,
18327
18763
  getVectorIndexes: hooks.getVectorIndexes,
18328
- buildMetadata: (indexName) => buildMetadata(indexName, hooks),
18764
+ buildMetadata: (indexName, documentCount) => buildMetadata(indexName, hooks, documentCount),
18329
18765
  createIndexFromMetadata: hooks.createIndexFromMetadata,
18766
+ recordCheckpoint: hooks.recordCheckpoint,
18330
18767
  onFatalError: hooks.onFatalError
18331
18768
  };
18332
18769
  const manager = tier.kind === "wal" ? createDurabilityManager(tier.config, managerHooks) : createSnapshotOnlyManager(tier.adapter, tier.config, managerHooks, hooks.checkpointPublisher);
@@ -18365,9 +18802,9 @@ function createDurabilityFromTier(tier, wiring) {
18365
18802
  }
18366
18803
  return createDurabilityIntegration(tier, {
18367
18804
  checkpointPublisher: { publishPartitions: wiring.publishCheckpointedPartitions },
18368
- getManager: (indexName) => wiring.indexRegistry.has(indexName) ? wiring.requireManager(indexName) : void 0,
18805
+ getManager: (indexName) => wiring.indexRegistry.has(indexName) ? wiring.getManager(indexName) : void 0,
18369
18806
  getVectorFieldPaths: (indexName) => wiring.indexRegistry.get(indexName)?.vectorFieldPaths ?? /* @__PURE__ */ new Set(),
18370
- 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(),
18371
18808
  getIndexConfig: (indexName) => {
18372
18809
  const entry = wiring.indexRegistry.get(indexName);
18373
18810
  if (entry === void 0) {
@@ -18380,6 +18817,8 @@ function createDurabilityFromTier(tier, wiring) {
18380
18817
  return {
18381
18818
  ...entry.indexUuid !== null ? { indexUuid: entry.indexUuid } : {},
18382
18819
  ...entry.heldPartitions !== null ? { heldPartitions: entry.heldPartitions } : {},
18820
+ documentCount: entry.documentCount,
18821
+ partitionCount: entry.partitionCount,
18383
18822
  schema: flattenSchema(entry.config.schema),
18384
18823
  language: entry.language.name,
18385
18824
  k1: entry.config.bm25?.k1 ?? 1.2,
@@ -18399,10 +18838,401 @@ function createDurabilityFromTier(tier, wiring) {
18399
18838
  };
18400
18839
  },
18401
18840
  createIndexFromMetadata: wiring.createIndexFromMetadata,
18841
+ recordCheckpoint: wiring.recordCheckpoint,
18402
18842
  onFatalError: wiring.emitFatalError
18403
18843
  });
18404
18844
  }
18405
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
+
18406
19236
  // src/engine/invalidation.ts
18407
19237
  var STATISTICS_BROADCAST_INTERVAL_MS = 5e3;
18408
19238
  var FOREIGN_STATS_TTL_MS = 6e4;
@@ -18428,7 +19258,7 @@ function createInvalidationIntegration(deps) {
18428
19258
  const reloadChains = /* @__PURE__ */ new Map();
18429
19259
  let broadcastTimer = null;
18430
19260
  let stopped = false;
18431
- function toError3(err) {
19261
+ function toError4(err) {
18432
19262
  return err instanceof Error ? err : new Error(String(err));
18433
19263
  }
18434
19264
  function pruneStaleForeignStats(entries) {
@@ -18441,7 +19271,7 @@ function createInvalidationIntegration(deps) {
18441
19271
  }
18442
19272
  function scheduleReload(indexName) {
18443
19273
  const chain = reloadChains.get(indexName) ?? Promise.resolve();
18444
- const next = chain.then(() => reloadIndex(indexName)).catch((err) => onError(toError3(err)));
19274
+ const next = chain.then(() => reloadIndex(indexName)).catch((err) => onError(toError4(err)));
18445
19275
  reloadChains.set(indexName, next);
18446
19276
  }
18447
19277
  function storeForeignStats(indexName, sourceInstanceId, stats) {
@@ -18467,7 +19297,7 @@ function createInvalidationIntegration(deps) {
18467
19297
  }
18468
19298
  storeForeignStats(event.indexName, event.instanceId, event.stats);
18469
19299
  } catch (err) {
18470
- onError(toError3(err));
19300
+ onError(toError4(err));
18471
19301
  }
18472
19302
  }
18473
19303
  async function publishOwnStatistics() {
@@ -18489,7 +19319,7 @@ function createInvalidationIntegration(deps) {
18489
19319
  }
18490
19320
  });
18491
19321
  } catch (err) {
18492
- onError(toError3(err));
19322
+ onError(toError4(err));
18493
19323
  }
18494
19324
  }
18495
19325
  }
@@ -18508,7 +19338,7 @@ function createInvalidationIntegration(deps) {
18508
19338
  sourceInstanceId: instanceId
18509
19339
  });
18510
19340
  } catch (err) {
18511
- onError(toError3(err));
19341
+ onError(toError4(err));
18512
19342
  }
18513
19343
  },
18514
19344
  broadcastStats(indexName) {
@@ -18553,7 +19383,7 @@ function createInvalidationIntegration(deps) {
18553
19383
  try {
18554
19384
  await adapter.shutdown();
18555
19385
  } catch (err) {
18556
- onError(toError3(err));
19386
+ onError(toError4(err));
18557
19387
  }
18558
19388
  }
18559
19389
  };
@@ -18595,7 +19425,7 @@ async function transferIndexToPool(indexName, pool, config, manager) {
18595
19425
  }
18596
19426
 
18597
19427
  // src/engine/orchestration/eligibility.ts
18598
- function toError2(value) {
19428
+ function toError3(value) {
18599
19429
  return value instanceof Error ? value : new Error(String(value));
18600
19430
  }
18601
19431
  function isDeterministicFailure(error) {
@@ -18933,14 +19763,34 @@ async function awaitCompactions(state) {
18933
19763
  var DEFAULT_BACKPRESSURE_LIMIT = 100;
18934
19764
  var DEFAULT_REQUEST_TIMEOUT = 3e4;
18935
19765
  var SHUTDOWN_TIMEOUT = 5e3;
19766
+ var CONSECUTIVE_TIMEOUTS_BEFORE_DEATH = 3;
19767
+ function errorFromEventLike(event) {
19768
+ if (event instanceof Error) {
19769
+ return event;
19770
+ }
19771
+ if (typeof event === "object" && event !== null) {
19772
+ const detail = event;
19773
+ if (detail.error instanceof Error) {
19774
+ return detail.error;
19775
+ }
19776
+ if (typeof detail.message === "string" && detail.message.length > 0) {
19777
+ return new Error(detail.message);
19778
+ }
19779
+ }
19780
+ return new Error("The worker reported an error event carrying no detail");
19781
+ }
18936
19782
  function createWorkerExecutor(worker, config) {
18937
- const backpressureLimit = DEFAULT_BACKPRESSURE_LIMIT;
18938
- const requestTimeout = DEFAULT_REQUEST_TIMEOUT;
19783
+ const backpressureLimit = config?.backpressureLimit ?? DEFAULT_BACKPRESSURE_LIMIT;
19784
+ const requestTimeout = config?.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
18939
19785
  const pending = /* @__PURE__ */ new Map();
19786
+ let deathError = null;
19787
+ let shutdownRequested = false;
19788
+ let consecutiveTimeouts = 0;
18940
19789
  function processResponse(msg) {
18941
19790
  if (!isValidWorkerResponse(msg)) {
18942
19791
  return;
18943
19792
  }
19793
+ consecutiveTimeouts = 0;
18944
19794
  const response = msg;
18945
19795
  const entry = pending.get(response.requestId);
18946
19796
  if (!entry) {
@@ -18954,15 +19804,36 @@ function createWorkerExecutor(worker, config) {
18954
19804
  entry.resolve(response.data);
18955
19805
  }
18956
19806
  }
19807
+ function handleDeath(cause) {
19808
+ if (deathError !== null) {
19809
+ return;
19810
+ }
19811
+ const message = cause instanceof Error ? cause.message : String(cause);
19812
+ deathError = new NarsilError(ErrorCodes.WORKER_CRASHED, `Worker died: ${message}`);
19813
+ for (const [id, entry] of pending) {
19814
+ clearTimeout(entry.timeoutId);
19815
+ entry.reject(deathError);
19816
+ pending.delete(id);
19817
+ }
19818
+ if (!shutdownRequested) {
19819
+ config?.onDeath?.(cause instanceof Error ? cause : new Error(message));
19820
+ }
19821
+ }
18957
19822
  if (typeof worker.on === "function") {
18958
19823
  worker.on("message", (msg) => processResponse(msg));
19824
+ worker.on("error", (cause) => handleDeath(cause));
19825
+ worker.on("exit", (code) => handleDeath(new Error(`Worker exited with code ${String(code)}`)));
18959
19826
  } else if (typeof worker.addEventListener === "function") {
18960
19827
  worker.addEventListener("message", (event) => {
18961
19828
  const msg = event.data;
18962
19829
  processResponse(msg);
18963
19830
  });
19831
+ worker.addEventListener("error", (event) => handleDeath(errorFromEventLike(event)));
18964
19832
  }
18965
19833
  function execute(action) {
19834
+ if (deathError !== null) {
19835
+ return Promise.reject(deathError);
19836
+ }
18966
19837
  if (pending.size >= backpressureLimit) {
18967
19838
  return Promise.reject(
18968
19839
  new NarsilError(ErrorCodes.WORKER_BUSY, `Backpressure limit of ${backpressureLimit} pending requests reached`)
@@ -18974,6 +19845,10 @@ function createWorkerExecutor(worker, config) {
18974
19845
  const timeoutId = setTimeout(() => {
18975
19846
  pending.delete(requestId);
18976
19847
  reject2(new NarsilError(ErrorCodes.WORKER_TIMEOUT, `Request ${requestId} timed out after ${requestTimeout}ms`));
19848
+ consecutiveTimeouts += 1;
19849
+ if (consecutiveTimeouts >= CONSECUTIVE_TIMEOUTS_BEFORE_DEATH) {
19850
+ handleDeath(new Error(`no answer to ${CONSECUTIVE_TIMEOUTS_BEFORE_DEATH} consecutive requests`));
19851
+ }
18977
19852
  }, requestTimeout);
18978
19853
  pending.set(requestId, {
18979
19854
  resolve,
@@ -18984,6 +19859,10 @@ function createWorkerExecutor(worker, config) {
18984
19859
  });
18985
19860
  }
18986
19861
  async function shutdown() {
19862
+ shutdownRequested = true;
19863
+ if (deathError !== null) {
19864
+ return;
19865
+ }
18987
19866
  const requestId = createRequestId();
18988
19867
  const shutdownAction = { type: "shutdown", requestId };
18989
19868
  const shutdownPromise = new Promise((resolve, reject2) => {
@@ -19024,9 +19903,9 @@ async function createWorkerFactory(entryPoint) {
19024
19903
  const runtime = detectRuntime();
19025
19904
  const resolvedEntry = resolveEntryPoint();
19026
19905
  if (runtime.supportsWebWorkers) {
19027
- return function webFactory(_workerId) {
19906
+ return function webFactory(_workerId, onDeath) {
19028
19907
  const instance = new Worker(resolvedEntry, { type: "module" });
19029
- return createWorkerExecutor(instance);
19908
+ return createWorkerExecutor(instance, { onDeath });
19030
19909
  };
19031
19910
  }
19032
19911
  throw new NarsilError(ErrorCodes.WORKER_CRASHED, `No worker support available on runtime "${runtime.runtime}"`);
@@ -19082,7 +19961,13 @@ async function runPromotion(state, reason) {
19082
19961
  return;
19083
19962
  }
19084
19963
  const factory = await createWorkerFactory();
19085
- const pool = createWorkerPool({ count: state.config?.workers?.count, workerFactory: factory });
19964
+ const pool = createWorkerPool({
19965
+ count: state.config?.workers?.count,
19966
+ workerFactory: factory,
19967
+ onWorkerCrash(workerId, indexNames, error) {
19968
+ state.callbacks?.onWorkerCrash?.(workerId, indexNames, error);
19969
+ }
19970
+ });
19086
19971
  for (const name of promotable) {
19087
19972
  pool.addIndexToAll(name);
19088
19973
  }
@@ -19119,7 +20004,7 @@ async function promoteBeforeBatch(state, indexName, incomingCount) {
19119
20004
  if (!result.shouldPromote) return;
19120
20005
  state.promotionInProgress = true;
19121
20006
  const run = runPromotion(state, result.reason).catch((err) => {
19122
- const error = toError2(err);
20007
+ const error = toError3(err);
19123
20008
  state.promotionBlocked = isDeterministicFailure(error);
19124
20009
  state.promotionInProgress = false;
19125
20010
  state.callbacks?.onPromotionFailure?.(result.reason, error, !state.promotionBlocked);
@@ -19140,7 +20025,7 @@ async function checkPromotion(state) {
19140
20025
  state.promotionInProgress = true;
19141
20026
  setTimeout(() => {
19142
20027
  const run = runPromotion(state, result.reason).catch((err) => {
19143
- const error = toError2(err);
20028
+ const error = toError3(err);
19144
20029
  state.promotionBlocked = isDeterministicFailure(error);
19145
20030
  state.promotionInProgress = false;
19146
20031
  state.callbacks?.onPromotionFailure?.(result.reason, error, !state.promotionBlocked);
@@ -19310,6 +20195,56 @@ function createWorkerOrchestrator(config, executor, promoter, indexRegistry, cal
19310
20195
  state.segmentLedger.clear();
19311
20196
  }
19312
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
+ }
19313
20248
  async function replicate(action) {
19314
20249
  await replicateToWorkers(state, action);
19315
20250
  if (action.type === "attachSegments") {
@@ -19322,6 +20257,9 @@ function createWorkerOrchestrator(config, executor, promoter, indexRegistry, cal
19322
20257
  replicateToWorkers: replicate,
19323
20258
  awaitReplication: (indexName) => awaitReplicationIdle(state, indexName),
19324
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),
19325
20263
  buildSegments: (requests) => buildSegments(state, requests),
19326
20264
  segmentBuildConcurrency: (indexName) => segmentBuildConcurrency(state, indexName),
19327
20265
  searchViaWorker: (indexName, params, globalStats, partitionIds) => searchViaWorker(state, indexName, params, globalStats, partitionIds),
@@ -19333,6 +20271,11 @@ function createWorkerOrchestrator(config, executor, promoter, indexRegistry, cal
19333
20271
  };
19334
20272
  }
19335
20273
 
20274
+ // src/engine/vector-fields.ts
20275
+ function getVectorFieldPaths(schema) {
20276
+ return new Set(extractVectorFieldsFromSchema(schema).keys());
20277
+ }
20278
+
19336
20279
  // src/engine/watermark.ts
19337
20280
  function createWatermarkNotifier(deps) {
19338
20281
  const latchedCapacity = /* @__PURE__ */ new Map();
@@ -19368,10 +20311,7 @@ function createWatermarkNotifier(deps) {
19368
20311
  }
19369
20312
 
19370
20313
  // src/engine/core.ts
19371
- function getVectorFieldPaths(schema) {
19372
- return new Set(extractVectorFieldsFromSchema(schema).keys());
19373
- }
19374
- function createEngineCore(config) {
20314
+ function createEngineCore(config, hooks) {
19375
20315
  const executor = createDirectExecutor();
19376
20316
  const promoter = createExecutionPromoter({
19377
20317
  perIndexThreshold: config?.workers?.promotionThreshold,
@@ -19403,6 +20343,9 @@ function createEngineCore(config) {
19403
20343
  if (handlers) {
19404
20344
  for (const handler of handlers) handler({ workerCount, reason });
19405
20345
  }
20346
+ void Promise.resolve(pluginRegistry.runHook("onWorkerPromote", { workerCount, reason })).catch((err) => {
20347
+ console.warn("onWorkerPromote plugin hook failed:", err instanceof Error ? err.message : String(err));
20348
+ });
19406
20349
  },
19407
20350
  onPromotionFailure(reason, error, retryable) {
19408
20351
  const handlers = eventHandlers.get("workerPromoteFailure");
@@ -19411,6 +20354,14 @@ function createEngineCore(config) {
19411
20354
  return;
19412
20355
  }
19413
20356
  for (const handler of handlers) handler({ reason, error, retryable });
20357
+ },
20358
+ onWorkerCrash(workerId, indexNames, error) {
20359
+ const handlers = eventHandlers.get("workerCrash");
20360
+ if (!handlers || handlers.size === 0) {
20361
+ console.warn(`Worker ${workerId} crashed:`, error);
20362
+ return;
20363
+ }
20364
+ for (const handler of handlers) handler({ workerId, indexNames, error });
19414
20365
  }
19415
20366
  });
19416
20367
  const rebalancer = createRebalancer();
@@ -19444,8 +20395,12 @@ function createEngineCore(config) {
19444
20395
  }
19445
20396
  return manager;
19446
20397
  }
19447
- async function createIndexFromMetadata(metadata) {
19448
- 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
+ }
19449
20404
  return;
19450
20405
  }
19451
20406
  const indexConfig = reconstructSchemaFromMetadata(metadata);
@@ -19474,7 +20429,7 @@ function createEngineCore(config) {
19474
20429
  }
19475
20430
  }
19476
20431
  try {
19477
- executor.createIndex(metadata.indexName, indexConfig, language);
20432
+ if (loadData) executor.createIndex(metadata.indexName, indexConfig, language);
19478
20433
  } catch (err) {
19479
20434
  if (err instanceof NarsilError && err.code === ErrorCodes.CONFIG_INVALID) {
19480
20435
  throw new NarsilError(err.code, `Recovery of index "${metadata.indexName}" failed: ${err.message}`, {
@@ -19492,8 +20447,12 @@ function createEngineCore(config) {
19492
20447
  embeddingAdapterName: adapterName,
19493
20448
  vectorFieldPaths: getVectorFieldPaths(indexConfig.schema),
19494
20449
  indexUuid: metadata.indexUuid ?? null,
19495
- heldPartitions: metadata.heldPartitions ?? null
20450
+ heldPartitions: metadata.heldPartitions ?? null,
20451
+ documentCount: metadata.documentCount ?? 0,
20452
+ partitionCount: metadata.partitionCount
19496
20453
  });
20454
+ if (loadData) await indexState.registerOpen(metadata.indexName);
20455
+ else indexState.registerClosed(metadata.indexName);
19497
20456
  if (metadata.analysisRevision !== language.revision) {
19498
20457
  analysisRebuild.markStale({
19499
20458
  indexName: metadata.indexName,
@@ -19504,9 +20463,12 @@ function createEngineCore(config) {
19504
20463
  }
19505
20464
  }
19506
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
+ }
19507
20469
  let invalidation = null;
19508
20470
  const durability = createDurabilityFromTier(durabilityTier, {
19509
- requireManager,
20471
+ getManager: (indexName) => executor.getManager(indexName),
19510
20472
  indexRegistry,
19511
20473
  createIndexFromMetadata,
19512
20474
  emitFatalError(error) {
@@ -19515,7 +20477,13 @@ function createEngineCore(config) {
19515
20477
  for (const handler of handlers) handler({ error });
19516
20478
  }
19517
20479
  },
19518
- 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
+ }
19519
20487
  });
19520
20488
  invalidation = createInvalidationFromConfig(config, durabilityTier?.kind ?? null, {
19521
20489
  getManager: (indexName) => executor.getManager(indexName) ?? void 0,
@@ -19561,6 +20529,18 @@ function createEngineCore(config) {
19561
20529
  resyncIndex: (indexName, wasPromoted) => orchestrator.resyncIndex(indexName, wasPromoted),
19562
20530
  durabilityManager: durability?.manager ?? null
19563
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
+ });
19564
20544
  const mutationCtx = {
19565
20545
  executor,
19566
20546
  pluginRegistry,
@@ -19614,6 +20594,7 @@ function createEngineCore(config) {
19614
20594
  bufferIfRebalancing,
19615
20595
  watermarkNotifier,
19616
20596
  analysisRebuild,
20597
+ indexState,
19617
20598
  mutationCtx,
19618
20599
  rebalanceCtx
19619
20600
  };
@@ -19773,8 +20754,11 @@ async function createEngineIndex(core, config, name, indexConfig, indexUuid) {
19773
20754
  embeddingAdapterName,
19774
20755
  vectorFieldPaths: getVectorFieldPaths(indexConfig.schema),
19775
20756
  indexUuid: null,
19776
- heldPartitions: null
20757
+ heldPartitions: null,
20758
+ documentCount: 0,
20759
+ partitionCount: indexConfig.partitions?.maxPartitions ?? 1
19777
20760
  });
20761
+ await core.indexState.registerOpen(name);
19778
20762
  if (core.durability) {
19779
20763
  await core.durability.manager.persistMetadata(name);
19780
20764
  }
@@ -19783,13 +20767,21 @@ async function createEngineIndex(core, config, name, indexConfig, indexUuid) {
19783
20767
  async function dropEngineIndex(core, name) {
19784
20768
  core.guardShutdown();
19785
20769
  const entry = core.requireIndex(name);
19786
- core.executor.dropIndex(name);
19787
- core.indexRegistry.delete(name);
19788
- core.watermarkNotifier.forget(name);
19789
- core.analysisRebuild.clearStale(name);
19790
- if (core.durability) {
19791
- await core.durability.manager.removeIndex(name);
19792
- }
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
+ });
19793
20785
  await core.pluginRegistry.runHook("onIndexDrop", { indexName: name, config: entry.config });
19794
20786
  }
19795
20787
  function registerEngineEmbeddingAdapter(core, name, adapter) {
@@ -19824,6 +20816,7 @@ function registerEngineEmbeddingAdapter(core, name, adapter) {
19824
20816
  // src/engine/lifecycle.ts
19825
20817
  async function shutdownEngine(core) {
19826
20818
  const { executor, durability, indexRegistry, eventHandlers, orchestrator } = core;
20819
+ core.indexState.dispose();
19827
20820
  for (const [name] of indexRegistry) {
19828
20821
  const manager = executor.getManager(name);
19829
20822
  if (manager) {
@@ -21370,6 +22363,15 @@ async function executeRebalance(manager, indexName, targetPartitionCount, ctx) {
21370
22363
  }
21371
22364
  }
21372
22365
  });
22366
+ try {
22367
+ await ctx.pluginRegistry.runHook("onPartitionSplit", {
22368
+ indexName,
22369
+ oldPartitionCount: oldCount,
22370
+ newPartitionCount: targetPartitionCount
22371
+ });
22372
+ } catch (hookErr) {
22373
+ console.warn("onPartitionSplit plugin hook failed:", hookErr instanceof Error ? hookErr.message : String(hookErr));
22374
+ }
21373
22375
  await replayQueued(manager, indexName, waq, ctx);
21374
22376
  if (ctx.durabilityManager) {
21375
22377
  await ctx.durabilityManager.persistMetadata(indexName);
@@ -21584,7 +22586,7 @@ async function createSnapshot(manager, entry) {
21584
22586
  const config = entry.config;
21585
22587
  const bm25 = config.bm25;
21586
22588
  const { encode: encode14 } = await import('@msgpack/msgpack');
21587
- return encode14({
22589
+ const payload = encode14({
21588
22590
  version: 2,
21589
22591
  schema: config.schema,
21590
22592
  language: entry.language.name,
@@ -21614,15 +22616,17 @@ async function createSnapshot(manager, entry) {
21614
22616
  partitions: partitionBuffers,
21615
22617
  vectorIndexes: vectorPayloads
21616
22618
  });
22619
+ return packIndexSnapshotEnvelope(payload);
21617
22620
  }
21618
22621
  async function restoreFromSnapshot(indexName, data, deps) {
21619
22622
  if (!(data instanceof Uint8Array)) {
21620
22623
  throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Snapshot data must be a Uint8Array");
21621
22624
  }
22625
+ const payloadBytes = hasNrslMagic(data) ? await unpackIndexSnapshotEnvelope(data) : data;
21622
22626
  const { decode: decode14 } = await import('@msgpack/msgpack');
21623
22627
  let decoded;
21624
22628
  try {
21625
- decoded = decode14(data);
22629
+ decoded = decode14(payloadBytes);
21626
22630
  } catch (err) {
21627
22631
  throw new NarsilError(
21628
22632
  ErrorCodes.DOC_VALIDATION_FAILED,
@@ -21704,8 +22708,12 @@ async function restoreFromSnapshot(indexName, data, deps) {
21704
22708
  embeddingAdapterName: adapterName,
21705
22709
  vectorFieldPaths: vectorFieldPaths2,
21706
22710
  indexUuid: null,
21707
- heldPartitions: null
22711
+ heldPartitions: null,
22712
+ documentCount: 0,
22713
+ partitionCount: envelope.partitions.length
21708
22714
  });
22715
+ await deps.indexState.registerOpen(indexName);
22716
+ const release = await deps.indexState.acquire(indexName, false);
21709
22717
  try {
21710
22718
  const manager = deps.requireManager(indexName);
21711
22719
  while (manager.partitionCount < envelope.partitions.length) {
@@ -21725,26 +22733,36 @@ async function restoreFromSnapshot(indexName, data, deps) {
21725
22733
  }
21726
22734
  }
21727
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
+ }
21728
22753
  } catch (err) {
21729
22754
  try {
21730
22755
  deps.executor.dropIndex(indexName);
21731
- deps.indexRegistry.delete(indexName);
21732
22756
  } catch (_) {
21733
22757
  }
21734
- throw err;
21735
- }
21736
- if (deps.durability) {
21737
- const manager = deps.requireManager(indexName);
21738
- for (let partitionId = 0; partitionId < manager.partitionCount; partitionId++) {
21739
- for (const docId of manager.getPartition(partitionId).docIds()) {
21740
- const document = manager.get(docId);
21741
- if (document !== void 0) {
21742
- await deps.durability.recordInsertOrUpdate(indexName, docId, document, noopApply2);
21743
- }
21744
- }
22758
+ deps.indexRegistry.delete(indexName);
22759
+ deps.indexState.forget(indexName);
22760
+ if (deps.durability) {
22761
+ await deps.durability.manager.removeIndex(indexName).catch(() => void 0);
21745
22762
  }
21746
- await deps.durability.manager.persistMetadata(indexName);
21747
- await deps.durability.manager.checkpoint(indexName);
22763
+ throw err;
22764
+ } finally {
22765
+ release();
21748
22766
  }
21749
22767
  }
21750
22768
  function noopApply2() {
@@ -21817,6 +22835,137 @@ function readProcessMemory() {
21817
22835
  }
21818
22836
  }
21819
22837
 
22838
+ // src/search/cursor-binding.ts
22839
+ var ABSENT_TAG = 0;
22840
+ var NULL_TAG = 1;
22841
+ var FALSE_TAG = 2;
22842
+ var TRUE_TAG = 3;
22843
+ var NUMBER_TAG = 4;
22844
+ var STRING_TAG = 5;
22845
+ var LIST_TAG = 6;
22846
+ var MAP_TAG = 7;
22847
+ var encoder4 = new TextEncoder();
22848
+ function ensureCapacity2(stream, extra) {
22849
+ if (stream.length + extra <= stream.bytes.length) return;
22850
+ const grown = new Uint8Array(Math.max(stream.bytes.length * 2, stream.length + extra));
22851
+ grown.set(stream.bytes.subarray(0, stream.length));
22852
+ stream.bytes = grown;
22853
+ }
22854
+ function writeByte(stream, value) {
22855
+ ensureCapacity2(stream, 1);
22856
+ stream.bytes[stream.length] = value;
22857
+ stream.length += 1;
22858
+ }
22859
+ function writeUint32(stream, value) {
22860
+ ensureCapacity2(stream, 4);
22861
+ stream.bytes[stream.length] = value >>> 24 & 255;
22862
+ stream.bytes[stream.length + 1] = value >>> 16 & 255;
22863
+ stream.bytes[stream.length + 2] = value >>> 8 & 255;
22864
+ stream.bytes[stream.length + 3] = value & 255;
22865
+ stream.length += 4;
22866
+ }
22867
+ var numberBytes = new DataView(new ArrayBuffer(8));
22868
+ function writeNumber(stream, value) {
22869
+ numberBytes.setFloat64(0, value, false);
22870
+ ensureCapacity2(stream, 8);
22871
+ for (let i = 0; i < 8; i++) {
22872
+ stream.bytes[stream.length + i] = numberBytes.getUint8(i);
22873
+ }
22874
+ stream.length += 8;
22875
+ }
22876
+ function writeString(stream, value) {
22877
+ const encoded = encoder4.encode(value);
22878
+ writeUint32(stream, encoded.length);
22879
+ ensureCapacity2(stream, encoded.length);
22880
+ stream.bytes.set(encoded, stream.length);
22881
+ stream.length += encoded.length;
22882
+ }
22883
+ function writeValue(stream, value) {
22884
+ if (value === void 0) {
22885
+ writeByte(stream, ABSENT_TAG);
22886
+ return;
22887
+ }
22888
+ if (value === null) {
22889
+ writeByte(stream, NULL_TAG);
22890
+ return;
22891
+ }
22892
+ if (typeof value === "boolean") {
22893
+ writeByte(stream, value ? TRUE_TAG : FALSE_TAG);
22894
+ return;
22895
+ }
22896
+ if (typeof value === "number") {
22897
+ writeByte(stream, NUMBER_TAG);
22898
+ writeNumber(stream, value);
22899
+ return;
22900
+ }
22901
+ if (typeof value === "string") {
22902
+ writeByte(stream, STRING_TAG);
22903
+ writeString(stream, value);
22904
+ return;
22905
+ }
22906
+ if (Array.isArray(value)) {
22907
+ writeByte(stream, LIST_TAG);
22908
+ writeUint32(stream, value.length);
22909
+ for (const member of value) {
22910
+ writeValue(stream, member ?? null);
22911
+ }
22912
+ return;
22913
+ }
22914
+ const record = value;
22915
+ const keys = Object.keys(record).sort(compareCodePoints);
22916
+ writeByte(stream, MAP_TAG);
22917
+ writeUint32(stream, keys.length);
22918
+ for (const key of keys) {
22919
+ writeByte(stream, STRING_TAG);
22920
+ writeString(stream, key);
22921
+ writeValue(stream, record[key] ?? null);
22922
+ }
22923
+ }
22924
+ function boundVectorOf(vector) {
22925
+ if (vector === void 0) return void 0;
22926
+ const bound = { ...vector };
22927
+ if (vector.value !== void 0) {
22928
+ bound.value = Array.from(vector.value, Math.fround);
22929
+ }
22930
+ if (vector.text !== void 0) {
22931
+ delete bound.value;
22932
+ }
22933
+ return bound;
22934
+ }
22935
+ var FILTERS_SLOT = 2;
22936
+ var BOUND_SLOT_COUNT = 15;
22937
+ function bindingOf(values) {
22938
+ const stream = { bytes: new Uint8Array(256), length: 0 };
22939
+ for (const value of values) {
22940
+ writeValue(stream, value);
22941
+ }
22942
+ return fnv1aBytes(stream.bytes.subarray(0, stream.length)).toString(16).padStart(8, "0");
22943
+ }
22944
+ function queryBindingOf(params) {
22945
+ return bindingOf([
22946
+ params.term,
22947
+ params.fields,
22948
+ params.filters,
22949
+ params.boost,
22950
+ params.minScore,
22951
+ params.termMatch,
22952
+ params.tolerance,
22953
+ params.prefixLength,
22954
+ params.prefix,
22955
+ params.exact,
22956
+ params.pinned,
22957
+ params.mode,
22958
+ params.hybrid,
22959
+ boundVectorOf(params.vector),
22960
+ params.scoring ?? "local"
22961
+ ]);
22962
+ }
22963
+ function listBindingOf(filters) {
22964
+ const values = new Array(BOUND_SLOT_COUNT).fill(void 0);
22965
+ values[FILTERS_SLOT] = filters;
22966
+ return bindingOf(values);
22967
+ }
22968
+
21820
22969
  // src/engine/list-documents/index.ts
21821
22970
  function clampListLimit(limit) {
21822
22971
  const clamped = Math.max(1, clampLimit(limit));
@@ -21851,7 +23000,7 @@ function collectCandidates(partition, anchor, matches, wanted, out) {
21851
23000
  taken++;
21852
23001
  }
21853
23002
  }
21854
- function pageInIdOrder(partitions, cursor, filtered, limit) {
23003
+ function pageInIdOrder(partitions, cursor, filtered, limit, binding) {
21855
23004
  const anchor = cursor === null ? null : cursor.anchor;
21856
23005
  const candidates = [];
21857
23006
  for (let index = 0; index < partitions.length; index++) {
@@ -21866,10 +23015,10 @@ function pageInIdOrder(partitions, cursor, filtered, limit) {
21866
23015
  candidates.sort(compareCodePoints);
21867
23016
  const hasMore = candidates.length > limit;
21868
23017
  const ids = hasMore ? candidates.slice(0, limit) : candidates;
21869
- const nextCursor = hasMore && ids.length > 0 ? { anchor: ids[ids.length - 1], score: null, sortKey: null, sortSignature: null } : null;
23018
+ const nextCursor = hasMore && ids.length > 0 ? { anchor: ids[ids.length - 1], score: null, sortKey: null, sortSignature: null, binding } : null;
21870
23019
  return { ids, nextCursor };
21871
23020
  }
21872
- function pageInSortOrder(partitions, cursor, filtered, limit, sort, signature, schema) {
23021
+ function pageInSortOrder(partitions, cursor, filtered, limit, sort, signature, schema, binding) {
21873
23022
  const normalized = normalizeSort(sort);
21874
23023
  const fields = normalized.map((entry) => entry.field);
21875
23024
  const directions = normalized.map((entry) => entry.direction);
@@ -21892,7 +23041,7 @@ function pageInSortOrder(partitions, cursor, filtered, limit, sort, signature, s
21892
23041
  const hasMore = merged.length > limit;
21893
23042
  const page = hasMore ? merged.slice(0, limit) : merged;
21894
23043
  const last = page[page.length - 1];
21895
- const nextCursor = hasMore && last !== void 0 ? { anchor: last.id, score: null, sortKey: last.key, sortSignature: signature } : null;
23044
+ const nextCursor = hasMore && last !== void 0 ? { anchor: last.id, score: null, sortKey: last.key, sortSignature: signature, binding } : null;
21896
23045
  return { ids: page.map((entry) => entry.id), nextCursor };
21897
23046
  }
21898
23047
  function countAcrossPartitions(manager, partitionIds, partitions) {
@@ -21911,15 +23060,16 @@ function executeListDocuments(params, context) {
21911
23060
  const limit = clampListLimit(params.limit);
21912
23061
  const signature = sortSignatureOf(params.sort);
21913
23062
  requireSortableFields(params.sort, schema);
23063
+ const binding = listBindingOf(params.filters);
21914
23064
  let cursor = null;
21915
23065
  if (params.cursor !== void 0) {
21916
23066
  cursor = decodePageCursor(params.cursor);
21917
- requireMatchingCursor(cursor, params.cursor, signature, false);
23067
+ requireMatchingCursor(cursor, params.cursor, signature, false, binding);
21918
23068
  }
21919
23069
  const partitions = context.partitionIds === void 0 ? manager.getAllPartitions() : context.partitionIds.map((partitionId) => manager.partitionAt(partitionId)).filter((partition) => partition !== void 0);
21920
23070
  const filtered = params.filters === void 0 ? null : collectFilterMatches(partitions, params.filters, schema);
21921
23071
  const total = filtered === null ? countAcrossPartitions(manager, context.partitionIds, partitions) : filtered.total;
21922
- const page = params.sort === void 0 || signature === null ? pageInIdOrder(partitions, cursor, filtered, limit) : pageInSortOrder(partitions, cursor, filtered, limit, params.sort, signature, schema);
23072
+ const page = params.sort === void 0 || signature === null ? pageInIdOrder(partitions, cursor, filtered, limit, binding) : pageInSortOrder(partitions, cursor, filtered, limit, params.sort, signature, schema, binding);
21923
23073
  const projection = resolveProjection(params.document);
21924
23074
  const documents = [];
21925
23075
  for (const docId of page.ids) {
@@ -21987,18 +23137,14 @@ function applyGrouping(hits, group, getDocument) {
21987
23137
  }
21988
23138
  const result = { values: entry.values, hits: groupHits };
21989
23139
  if (group.reduce) {
21990
- try {
21991
- let accumulator = group.reduce.initialValue();
21992
- for (const hit of groupHits) {
21993
- const doc = getDocument(hit.id);
21994
- if (doc) {
21995
- accumulator = group.reduce.reducer(accumulator, doc, hit.score ?? 0);
21996
- }
21997
- }
21998
- ;
21999
- result.reduced = accumulator;
22000
- } catch (err) {
22001
- result.reducerError = err instanceof Error ? err.message : String(err);
23140
+ const folded = foldGroupReducer(
23141
+ group.reduce,
23142
+ groupHits.map((hit) => ({ document: getDocument(hit.id), score: hit.score ?? 0 }))
23143
+ );
23144
+ if (folded.reducerError !== void 0) {
23145
+ result.reducerError = folded.reducerError;
23146
+ } else {
23147
+ result.reduced = folded.reduced;
22002
23148
  }
22003
23149
  }
22004
23150
  groups.push(result);
@@ -22011,19 +23157,49 @@ function applyGrouping(hits, group, getDocument) {
22011
23157
  const bId = b.hits.length > 0 ? b.hits[0].id : "";
22012
23158
  return compareCodePoints(aId, bId);
22013
23159
  });
23160
+ if (group.limit !== void 0 && Number.isFinite(group.limit) && group.limit >= 0) {
23161
+ const cap = Math.floor(group.limit);
23162
+ if (groups.length > cap) {
23163
+ groups.length = cap;
23164
+ }
23165
+ }
22014
23166
  return groups;
22015
23167
  }
23168
+ function foldGroupReducer(reduce, rows) {
23169
+ try {
23170
+ let accumulator = reduce.initialValue();
23171
+ for (const row of rows) {
23172
+ if (row.document !== void 0) {
23173
+ accumulator = reduce.reducer(accumulator, row.document, row.score);
23174
+ }
23175
+ }
23176
+ return { reduced: accumulator };
23177
+ } catch (err) {
23178
+ return { reducerError: err instanceof Error ? err.message : String(err) };
23179
+ }
23180
+ }
22016
23181
 
22017
23182
  // src/search/pinning.ts
23183
+ function dedupePinnedEntries(pinned) {
23184
+ const deduped = [];
23185
+ const seen = /* @__PURE__ */ new Set();
23186
+ for (const entry of pinned) {
23187
+ if (seen.has(entry.docId)) continue;
23188
+ seen.add(entry.docId);
23189
+ deduped.push(entry);
23190
+ }
23191
+ return deduped;
23192
+ }
22018
23193
  function applyPinning(hits, pinned, resolveDoc) {
22019
23194
  const result = hits.slice();
22020
- const pinnedDocIds = new Set(pinned.map((p) => p.docId));
23195
+ const deduped = dedupePinnedEntries(pinned);
23196
+ const pinnedDocIds = new Set(deduped.map((entry) => entry.docId));
22021
23197
  for (let i = result.length - 1; i >= 0; i--) {
22022
23198
  if (pinnedDocIds.has(result[i].id)) {
22023
23199
  result.splice(i, 1);
22024
23200
  }
22025
23201
  }
22026
- const sorted = pinned.slice().sort((a, b) => a.position - b.position);
23202
+ const sorted = deduped.sort((a, b) => a.position - b.position);
22027
23203
  for (const entry of sorted) {
22028
23204
  const doc = resolveDoc(entry.docId);
22029
23205
  if (!doc) continue;
@@ -22217,17 +23393,7 @@ function applyHighlights(hits, params, language, analysis) {
22217
23393
 
22218
23394
  // src/engine/query/shared.ts
22219
23395
  function partitionsFor(manager, partitionIds) {
22220
- if (partitionIds === void 0) {
22221
- return manager.getAllPartitions();
22222
- }
22223
- const partitions = [];
22224
- for (const partitionId of partitionIds) {
22225
- const partition = manager.partitionAt(partitionId);
22226
- if (partition !== void 0) {
22227
- partitions.push(partition);
22228
- }
22229
- }
22230
- return partitions;
23396
+ return partitionsIn(manager, partitionIds);
22231
23397
  }
22232
23398
  function coverageFor(manager, partitionIds) {
22233
23399
  if (partitionIds === void 0) {
@@ -22321,7 +23487,7 @@ function executeSortedQueryPage(params, context, limit, offset, sortSignature) {
22321
23487
  let anchorId = null;
22322
23488
  if (params.searchAfter !== void 0) {
22323
23489
  const decoded = decodePageCursor(params.searchAfter);
22324
- requireMatchingCursor(decoded, params.searchAfter, sortSignature, true);
23490
+ requireMatchingCursor(decoded, params.searchAfter, sortSignature, true, context.cursorBinding);
22325
23491
  anchorKey = decoded.sortKey;
22326
23492
  anchorId = decoded.anchor;
22327
23493
  }
@@ -22366,7 +23532,13 @@ function executeSortedQueryPage(params, context, limit, offset, sortSignature) {
22366
23532
  const last = page[page.length - 1];
22367
23533
  let cursor;
22368
23534
  if (hasMore && last !== void 0) {
22369
- cursor = encodePageCursor({ anchor: last.id, score: null, sortKey: last.key, sortSignature });
23535
+ cursor = encodePageCursor({
23536
+ anchor: last.id,
23537
+ score: null,
23538
+ sortKey: last.key,
23539
+ sortSignature,
23540
+ binding: context.cursorBinding
23541
+ });
22370
23542
  }
22371
23543
  return {
22372
23544
  hits: page.map((entry) => ({ id: entry.id, document: void 0 })),
@@ -22652,7 +23824,7 @@ async function executeQuery(params, context) {
22652
23824
  if (params.group) {
22653
23825
  groups = applyGrouping(hits, params.group, (docId) => manager.getRef(docId));
22654
23826
  }
22655
- if (params.pinned) {
23827
+ if (params.pinned && params.searchAfter === void 0 && context.partitionIds === void 0) {
22656
23828
  hits = applyPinning(hits, params.pinned, (docId) => {
22657
23829
  const doc = manager.getRef(docId);
22658
23830
  if (!doc) return void 0;
@@ -22667,7 +23839,16 @@ async function executeQuery(params, context) {
22667
23839
  sortKeyOf
22668
23840
  };
22669
23841
  }
22670
- const paged = applyPagination(hits, limit, offset, params.searchAfter, sortContext);
23842
+ const pinnedIds = params.pinned !== void 0 && params.searchAfter === void 0 ? new Set(params.pinned.map((entry) => entry.docId)) : void 0;
23843
+ const paged = applyPagination(
23844
+ hits,
23845
+ limit,
23846
+ offset,
23847
+ context.cursorBinding,
23848
+ params.searchAfter,
23849
+ sortContext,
23850
+ pinnedIds
23851
+ );
22671
23852
  paginated = paged.paginated;
22672
23853
  nextCursor = paged.nextCursor;
22673
23854
  count = fanOutResult.totalMatched;
@@ -22736,6 +23917,11 @@ async function executePreflight(params, context) {
22736
23917
  } else if (isHybridMode && hasGlobalVectorIndex) {
22737
23918
  const result = await executeHybridSearch(params, context, preflightLimit, preflightOffset);
22738
23919
  totalMatched = result.totalMatched;
23920
+ } else if (countsWithoutScores(params)) {
23921
+ totalMatched = fanOutMatchCount(manager, params, language, config.schema, {
23922
+ searchOptions: searchOptionsFor(manager),
23923
+ partitionIds: context.partitionIds
23924
+ });
22739
23925
  } else {
22740
23926
  const scoring = scoringConfigFor(params, context);
22741
23927
  const workerResult = workerSearch ? await workerSearch(indexName, params, broadcastStatsForWorker(params, context, scoring), context.partitionIds) : null;
@@ -22864,83 +24050,107 @@ function broadcastStatsFor(core, options) {
22864
24050
  }
22865
24051
  async function runEngineQuery(core, indexName, params, options) {
22866
24052
  core.guardShutdown();
22867
- const entry = core.requireIndex(indexName);
22868
- const manager = core.requireManager(indexName);
22869
- const resolvedParams = await resolveVectorText(
22870
- scopedParams(params),
22871
- entry.embeddingAdapter,
22872
- core.abortController.signal,
22873
- entry.embeddingAdapterName
22874
- );
22875
- await core.pluginRegistry.runHook("beforeSearch", { indexName, params: resolvedParams });
22876
- const workerSearch = core.orchestrator.isPromoted() ? core.orchestrator.searchViaWorker.bind(core.orchestrator) : void 0;
22877
- const result = await executeQuery(resolvedParams, {
22878
- manager,
22879
- language: entry.language,
22880
- config: entry.config,
22881
- workerSearch,
22882
- indexName,
22883
- broadcastStats: broadcastStatsFor(core),
22884
- partitionIds: options?.partitionIds
22885
- });
24053
+ const release = await core.indexState.acquire(indexName);
22886
24054
  try {
22887
- 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,
22888
24070
  indexName,
22889
- params: resolvedParams,
22890
- results: result
24071
+ broadcastStats: broadcastStatsFor(core, options),
24072
+ partitionIds: options?.partitionIds,
24073
+ cursorBinding: queryBindingOf(params)
22891
24074
  });
22892
- } catch (err) {
22893
- console.warn("afterSearch plugin hook error:", err);
22894
- }
22895
- if (core.analysisRebuild.isStale(indexName)) {
22896
- 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();
22897
24090
  }
22898
- return result;
22899
24091
  }
22900
24092
  async function runEnginePreflight(core, indexName, params, options) {
22901
24093
  core.guardShutdown();
22902
- const entry = core.requireIndex(indexName);
22903
- const manager = core.requireManager(indexName);
22904
- const resolvedParams = await resolveVectorText(
22905
- scopedParams(params),
22906
- entry.embeddingAdapter,
22907
- core.abortController.signal,
22908
- entry.embeddingAdapterName
22909
- );
22910
- const workerSearch = core.orchestrator.isPromoted() ? core.orchestrator.searchViaWorker.bind(core.orchestrator) : void 0;
22911
- const result = await executePreflight(resolvedParams, {
22912
- manager,
22913
- language: entry.language,
22914
- config: entry.config,
22915
- workerSearch,
22916
- indexName,
22917
- broadcastStats: broadcastStatsFor(core),
22918
- partitionIds: options?.partitionIds
22919
- });
22920
- if (core.analysisRebuild.isStale(indexName)) {
22921
- 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();
22922
24121
  }
22923
- return result;
22924
24122
  }
22925
24123
  async function runEngineSuggest(core, indexName, params, partitionIds) {
22926
24124
  core.guardShutdown();
22927
- const result = executeSuggest(
22928
- core.requireManager(indexName),
22929
- core.requireIndex(indexName).language,
22930
- params);
22931
- if (core.analysisRebuild.isStale(indexName)) {
22932
- 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();
22933
24139
  }
22934
- return result;
22935
24140
  }
22936
24141
  async function runEngineListDocuments(core, indexName, params, partitionIds) {
22937
24142
  core.guardShutdown();
22938
- const entry = core.requireIndex(indexName);
22939
- return executeListDocuments(params, {
22940
- manager: core.requireManager(indexName),
22941
- schema: entry.config.schema,
22942
- partitionIds
22943
- });
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
+ }
22944
24154
  }
22945
24155
 
22946
24156
  // src/narsil/operations.ts
@@ -22960,6 +24170,16 @@ function createNarsilFromCore(core, config) {
22960
24170
  mutationCtx,
22961
24171
  rebalanceCtx
22962
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
+ }
22963
24183
  const narsil = {
22964
24184
  createIndex(name, indexConfig) {
22965
24185
  return createEngineIndex(core, config, name, indexConfig);
@@ -22970,15 +24190,27 @@ function createNarsilFromCore(core, config) {
22970
24190
  dropIndex(name) {
22971
24191
  return dropEngineIndex(core, name);
22972
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
+ },
22973
24203
  listIndexes() {
22974
24204
  const infos = [];
22975
24205
  for (const [name, entry] of indexRegistry) {
22976
24206
  const manager = executor.getManager(name);
22977
24207
  infos.push({
22978
24208
  name,
22979
- documentCount: manager?.countDocuments() ?? 0,
22980
- partitionCount: manager?.partitionCount ?? 0,
24209
+ documentCount: manager?.countDocuments() ?? entry.documentCount,
24210
+ partitionCount: manager?.partitionCount ?? entry.partitionCount,
22981
24211
  language: entry.language.name,
24212
+ state: core.indexState.stateOf(name),
24213
+ reopenCount: core.indexState.reopenCount(name),
22982
24214
  ...core.analysisRebuild.isStale(name) ? { analysisStale: true } : {}
22983
24215
  });
22984
24216
  }
@@ -22989,8 +24221,8 @@ function createNarsilFromCore(core, config) {
22989
24221
  const entry = requireIndex(indexName);
22990
24222
  const manager = executor.getManager(indexName);
22991
24223
  return {
22992
- documentCount: manager?.countDocuments() ?? 0,
22993
- partitionCount: manager?.partitionCount ?? 0,
24224
+ documentCount: manager?.countDocuments() ?? entry.documentCount,
24225
+ partitionCount: manager?.partitionCount ?? entry.partitionCount,
22994
24226
  estimatedMemoryBytes: manager?.estimateMemoryBytes() ?? 0,
22995
24227
  language: entry.language.name,
22996
24228
  schema: entry.config.schema
@@ -23002,47 +24234,48 @@ function createNarsilFromCore(core, config) {
23002
24234
  return executor.getManager(indexName)?.getPartitionStats() ?? [];
23003
24235
  },
23004
24236
  insert(indexName, document, docId, options) {
23005
- return insertDocument(mutationCtx, indexName, document, docId, options);
24237
+ return withOpenIndex(indexName, () => insertDocument(mutationCtx, indexName, document, docId, options));
23006
24238
  },
23007
24239
  insertBatch(indexName, documents, options) {
23008
- return insertDocumentBatch(mutationCtx, indexName, documents, options);
24240
+ return withOpenIndex(indexName, () => insertDocumentBatch(mutationCtx, indexName, documents, options));
23009
24241
  },
23010
24242
  remove(indexName, docId) {
23011
- return removeDocument(mutationCtx, indexName, docId);
24243
+ return withOpenIndex(indexName, () => removeDocument(mutationCtx, indexName, docId));
23012
24244
  },
23013
24245
  removeBatch(indexName, docIds) {
23014
- return removeDocumentBatch(mutationCtx, indexName, docIds);
24246
+ return withOpenIndex(indexName, () => removeDocumentBatch(mutationCtx, indexName, docIds));
23015
24247
  },
23016
24248
  update(indexName, docId, document) {
23017
- return updateDocument(mutationCtx, indexName, docId, document);
24249
+ return withOpenIndex(indexName, () => updateDocument(mutationCtx, indexName, docId, document));
23018
24250
  },
23019
24251
  updateBatch(indexName, updates) {
23020
- return updateDocumentBatch(mutationCtx, indexName, updates);
24252
+ return withOpenIndex(indexName, () => updateDocumentBatch(mutationCtx, indexName, updates));
23021
24253
  },
23022
24254
  async get(indexName, docId) {
23023
- guardShutdown();
23024
- requireIndex(indexName);
23025
- return executor.execute({ type: "get", indexName, docId, requestId: docId });
24255
+ return withOpenIndex(indexName, () => executor.execute({ type: "get", indexName, docId, requestId: docId }));
23026
24256
  },
23027
24257
  async getMultiple(indexName, docIds) {
23028
24258
  guardShutdown();
23029
24259
  requireIndex(indexName);
23030
- const result = /* @__PURE__ */ new Map();
23031
- for (const docId of docIds) {
23032
- const doc = await narsil.get(indexName, docId);
23033
- if (doc !== void 0) result.set(docId, doc);
23034
- }
23035
- 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
+ });
23036
24273
  },
23037
24274
  async has(indexName, docId) {
23038
- guardShutdown();
23039
- requireIndex(indexName);
23040
- return executor.execute({ type: "has", indexName, docId, requestId: docId });
24275
+ return withOpenIndex(indexName, () => executor.execute({ type: "has", indexName, docId, requestId: docId }));
23041
24276
  },
23042
24277
  async countDocuments(indexName) {
23043
- guardShutdown();
23044
- requireIndex(indexName);
23045
- return executor.execute({ type: "count", indexName, requestId: indexName });
24278
+ return withOpenIndex(indexName, () => executor.execute({ type: "count", indexName, requestId: indexName }));
23046
24279
  },
23047
24280
  async listDocuments(indexName, params) {
23048
24281
  return runEngineListDocuments(core, indexName, params ?? {});
@@ -23059,15 +24292,15 @@ function createNarsilFromCore(core, config) {
23059
24292
  async rebuildAnalysis(indexName) {
23060
24293
  guardShutdown();
23061
24294
  requireIndex(indexName);
23062
- await core.analysisRebuild.rebuild(indexName);
24295
+ await withOpenIndex(indexName, () => core.analysisRebuild.rebuild(indexName));
23063
24296
  },
23064
24297
  async snapshot(indexName) {
23065
24298
  guardShutdown();
23066
- return createSnapshot(requireManager(indexName), requireIndex(indexName));
24299
+ return withOpenIndex(indexName, () => createSnapshot(requireManager(indexName), requireIndex(indexName)));
23067
24300
  },
23068
24301
  async restore(indexName, data) {
23069
24302
  guardShutdown();
23070
- return restoreFromSnapshot(indexName, data, {
24303
+ await restoreFromSnapshot(indexName, data, {
23071
24304
  executor,
23072
24305
  indexRegistry,
23073
24306
  getVectorFieldPaths,
@@ -23077,66 +24310,74 @@ function createNarsilFromCore(core, config) {
23077
24310
  embeddingAdapters: core.embeddingAdapters,
23078
24311
  defaultEmbeddingAdapter: config?.embedding ?? null,
23079
24312
  markAnalysisStale: core.analysisRebuild.markStale,
23080
- clearAnalysisStale: core.analysisRebuild.clearStale
24313
+ clearAnalysisStale: core.analysisRebuild.clearStale,
24314
+ indexState: core.indexState
23081
24315
  });
23082
24316
  },
23083
24317
  async checkpoint(indexName) {
23084
24318
  guardShutdown();
23085
24319
  requireIndex(indexName);
23086
- if (durability) {
23087
- await durability.manager.checkpoint(indexName);
23088
- }
24320
+ await withOpenIndex(indexName, async () => {
24321
+ if (durability) await durability.manager.checkpoint(indexName);
24322
+ });
23089
24323
  },
23090
24324
  async clear(indexName) {
23091
24325
  guardShutdown();
23092
24326
  requireIndex(indexName);
23093
- await executor.execute({ type: "clear", indexName, requestId: indexName });
23094
- await orchestrator.replicateToWorkers({ type: "clear", indexName, requestId: `replicate-clear-${indexName}` });
23095
- 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
+ });
23096
24332
  },
23097
24333
  async rebalance(indexName, targetPartitionCount) {
23098
24334
  guardShutdown();
23099
24335
  requireIndex(indexName);
23100
- return executeRebalance(requireManager(indexName), indexName, targetPartitionCount, rebalanceCtx);
24336
+ return withOpenIndex(
24337
+ indexName,
24338
+ () => executeRebalance(requireManager(indexName), indexName, targetPartitionCount, rebalanceCtx)
24339
+ );
23101
24340
  },
23102
24341
  async updatePartitionConfig(indexName, partitionConfig) {
23103
24342
  guardShutdown();
23104
- const entry = requireIndex(indexName);
23105
- const manager = requireManager(indexName);
23106
- if (rebalancingIndexes.has(indexName)) {
23107
- throw new NarsilError(
23108
- ErrorCodes.PARTITION_REBALANCING_BACKPRESSURE,
23109
- `Index "${indexName}" is currently being rebalanced`
23110
- );
23111
- }
23112
- validatePartitionConfig(partitionConfig);
23113
- if (partitionConfig.maxPartitions !== void 0 && partitionConfig.maxPartitions < manager.partitionCount) {
23114
- throw new NarsilError(
23115
- ErrorCodes.PARTITION_CAPACITY_EXCEEDED,
23116
- `maxPartitions (${partitionConfig.maxPartitions}) is less than the current partition count (${manager.partitionCount})`,
23117
- { maxPartitions: partitionConfig.maxPartitions, partitionCount: manager.partitionCount }
23118
- );
23119
- }
23120
- const currentDocCount = manager.countDocuments();
23121
- const newMaxDocs = partitionConfig.maxDocsPerPartition ?? entry.config.partitions?.maxDocsPerPartition;
23122
- if (newMaxDocs !== void 0) {
23123
- const newTotalCapacity = newMaxDocs * manager.partitionCount;
23124
- 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) {
23125
24354
  throw new NarsilError(
23126
24355
  ErrorCodes.PARTITION_CAPACITY_EXCEEDED,
23127
- `New capacity (${newTotalCapacity}) is less than current document count (${currentDocCount})`,
23128
- { newTotalCapacity, currentDocCount }
24356
+ `maxPartitions (${partitionConfig.maxPartitions}) is less than the current partition count (${manager.partitionCount})`,
24357
+ { maxPartitions: partitionConfig.maxPartitions, partitionCount: manager.partitionCount }
23129
24358
  );
23130
24359
  }
23131
- }
23132
- if (!entry.config.partitions) entry.config.partitions = {};
23133
- if (partitionConfig.maxDocsPerPartition !== void 0)
23134
- entry.config.partitions.maxDocsPerPartition = partitionConfig.maxDocsPerPartition;
23135
- if (partitionConfig.maxPartitions !== void 0)
23136
- entry.config.partitions.maxPartitions = partitionConfig.maxPartitions;
23137
- if (partitionConfig.watermark !== void 0) entry.config.partitions.watermark = partitionConfig.watermark;
23138
- if (durability) await durability.manager.persistMetadata(indexName);
23139
- 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
+ });
23140
24381
  },
23141
24382
  async getMemoryStats() {
23142
24383
  let estimatedIndexBytes = 0;
@@ -23144,22 +24385,33 @@ function createNarsilFromCore(core, config) {
23144
24385
  estimatedIndexBytes += executor.getManager(name)?.estimateMemoryBytes() ?? 0;
23145
24386
  }
23146
24387
  const workers = await orchestrator.getWorkerMemoryStats();
23147
- 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
+ };
23148
24397
  },
23149
24398
  async compactVectors(indexName, fieldName) {
23150
24399
  guardShutdown();
23151
24400
  requireIndex(indexName);
23152
- compactVectors(requireManager(indexName), indexName, fieldName);
24401
+ await withOpenIndex(indexName, async () => {
24402
+ compactVectors(requireManager(indexName), indexName, fieldName);
24403
+ });
23153
24404
  },
23154
24405
  async optimizeVectors(indexName, fieldName) {
23155
24406
  guardShutdown();
23156
24407
  requireIndex(indexName);
23157
- await optimizeVectors(requireManager(indexName), indexName, fieldName);
24408
+ await withOpenIndex(indexName, () => optimizeVectors(requireManager(indexName), indexName, fieldName));
23158
24409
  },
23159
24410
  vectorMaintenanceStatus(indexName) {
23160
24411
  guardShutdown();
23161
24412
  requireIndex(indexName);
23162
- return getVectorMaintenanceStatus(requireManager(indexName));
24413
+ const manager = executor.getManager(indexName);
24414
+ return manager === void 0 ? [] : getVectorMaintenanceStatus(manager);
23163
24415
  },
23164
24416
  on(event, handler) {
23165
24417
  const key = event;
@@ -23192,12 +24444,12 @@ function createNarsilFromCore(core, config) {
23192
24444
  async function createNarsil(config) {
23193
24445
  const core = createEngineCore(config);
23194
24446
  if (core.durability) {
23195
- await core.durability.manager.recover();
24447
+ await core.durability.manager.recover(config?.lifecycle !== void 0);
23196
24448
  }
23197
24449
  if (core.invalidation) {
23198
24450
  await core.invalidation.start();
23199
24451
  }
23200
- await core.analysisRebuild.reviewStaleIndexes();
24452
+ if (config?.lifecycle === void 0) await core.analysisRebuild.reviewStaleIndexes();
23201
24453
  return createNarsilFromCore(core, config);
23202
24454
  }
23203
24455