@delali/narsil 0.2.4-next.47 → 0.2.4-next.49

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 (48) hide show
  1. package/README.md +2 -2
  2. package/dist/adapters/indexeddb.mjs +15 -10
  3. package/dist/{chunk-BFIH7OV5.mjs → chunk-2GWVQWGA.mjs} +3 -1
  4. package/dist/{chunk-UZTT2BKQ.mjs → chunk-56PJBIJ6.mjs} +891 -542
  5. package/dist/chunk-CBLS46IE.mjs +10 -0
  6. package/dist/{chunk-JQUL2T6I.mjs → chunk-GCM3JFVA.mjs} +1 -2
  7. package/dist/{chunk-4COAUBLJ.mjs → chunk-HGJC5L65.mjs} +4 -7
  8. package/dist/{chunk-DFZDV5FV.mjs → chunk-JXHUHFJ5.mjs} +27 -14
  9. package/dist/{chunk-XWICBGYT.mjs → chunk-KGRYQVZ5.mjs} +2 -5
  10. package/dist/{chunk-EV45L35L.mjs → chunk-M6JGMKA5.mjs} +391 -183
  11. package/dist/{chunk-PM52SMBT.mjs → chunk-OOSIDHA3.mjs} +2 -1
  12. package/dist/chunk-ORBLD3EL.mjs +6 -0
  13. package/dist/chunk-OWCFK42Q.mjs +30 -0
  14. package/dist/{chunk-WTONLDU3.mjs → chunk-RDGXYRF4.mjs} +1 -0
  15. package/dist/chunk-UNEEFTEU.mjs +7 -0
  16. package/dist/chunk-YEO6G2NG.mjs +6 -0
  17. package/dist/{client-C5BkUqB8.d.ts → client-DX7xPcNA.d.ts} +1 -1
  18. package/dist/client.d.ts +5 -5
  19. package/dist/client.mjs +224 -222
  20. package/dist/{config-B-ZI-ft5.d.ts → config-B41W23Us.d.ts} +1 -1
  21. package/dist/{config-BZRExglL.d.ts → config-Co-zAXqO.d.ts} +27 -11
  22. package/dist/direct-executor-HRTDWMVB.mjs +9 -0
  23. package/dist/distribution/coordinator/etcd.mjs +4 -7
  24. package/dist/distribution/coordinator/in-memory.mjs +2 -2
  25. package/dist/distribution/transport/grpc.mjs +7 -9
  26. package/dist/distribution/transport/in-memory.mjs +2 -1
  27. package/dist/distribution/transport/tcp.mjs +3 -2
  28. package/dist/distribution.d.ts +5 -5
  29. package/dist/distribution.mjs +121 -97
  30. package/dist/embeddings/openai.mjs +5 -3
  31. package/dist/{engine-Cjr1uBLa.d.ts → engine-CABZg25p.d.ts} +12 -8
  32. package/dist/index.browser.mjs +1514 -894
  33. package/dist/index.d.ts +5 -5
  34. package/dist/index.mjs +9 -8
  35. package/dist/invalidation/broadcast-channel.mjs +2 -1
  36. package/dist/invalidation/filesystem.mjs +3 -4
  37. package/dist/{options-BC68VPoE.d.ts → options-Dtk99qUs.d.ts} +1 -1
  38. package/dist/persistence/durability/checkpoint-worker.mjs +9 -8
  39. package/dist/react.d.ts +3 -3
  40. package/dist/react.mjs +7 -5
  41. package/dist/{search-DDHjfXhJ.d.ts → search-FjWhZN3f.d.ts} +20 -3
  42. package/dist/server.d.ts +6 -6
  43. package/dist/server.mjs +43 -30
  44. package/dist/vector/hnsw-build-worker.mjs +2 -4
  45. package/dist/vector/search-worker.mjs +3 -4
  46. package/dist/workers/entry.mjs +2 -2
  47. package/package.json +1 -1
  48. package/dist/direct-executor-EOTFGXTO.mjs +0 -8
@@ -1,14 +1,17 @@
1
- import { ReplicationMessageTypes } from './chunk-JQUL2T6I.mjs';
2
- import { MAX_INDEX_NAME_LENGTH } from './chunk-XWICBGYT.mjs';
3
- import { getNestedValue, getLanguage, extractVectorFieldsFromSchema, createDirectExecutor, createPartitionRouter, clampRowCount, DEFAULT_PAGE_SIZE, validateSchema, validateVectorPromotion, collectQueryTermStats, createPartitionManager, createVectorIndex, flattenSchema, isTextFieldType, fnv1aBytes, requireWithinResultWindow, normalizeSort, sortSignatureOf, requireSortableFields, fanOutQuery, applySorting, applyPagination, resolveProjection, countsWithoutScores, fanOutMatchCount, tokenize, decodePageCursor, requireMatchingCursor, encodePageCursor, resolveIndexAnalysis, deserializePayloadV2, deserializePayloadV1, mergePartitionStats, fulltextMatches, mergeFacets, pruneStatsToQueryTerms, createPartitionIndex, fnv1a, createWorkerPool, partitionsIn, detectRuntime, isNodeMainThread, spawnNodeWorker, createSharedFrozenSegment, buildCompactedSegmentPayload, freezeSegmentShared, createFrozenSegment, deserializeMetadata, isCompositePartition, serializeMetadata, kWayMerge, validateRequiredFields, validateDocument, validateDocumentStrict } from './chunk-EV45L35L.mjs';
1
+ import { ReplicationMessageTypes } from './chunk-GCM3JFVA.mjs';
2
+ import { WIRE_BATCH_BUDGET, DEFAULT_LIMIT, MAX_INDEX_NAME_LENGTH, INDEX_NAME_PATTERN, DEFAULT_SUGGEST_LIMIT, MAX_SUGGEST_LIMIT, MAX_SUGGEST_SCATTER_LIMIT, STATISTICS_BROADCAST_INTERVAL_MS, MAX_SWEEP_INTERVAL_MS, DEFAULT_OFFSET, MIN_SWEEP_INTERVAL_MS, FOREIGN_STATS_TTL_MS, MIN_DOCUMENTS_FOR_SEGMENTS, BATCH_CHUNK_SIZE, MAX_DOC_ID_LENGTH } from './chunk-OWCFK42Q.mjs';
3
+ import { getNestedValue, getLanguage, extractVectorFieldsFromSchema, clampRowCount, splitWorkerBudget, resolveWorkerCount, createDirectExecutor, createPartitionRouter, validateSchema, validateVectorPromotion, collectQueryTermStats, createPartitionManager, createVectorIndex, detectRuntime, flattenSchema, isTextFieldType, fnv1aBytes, requireWithinResultWindow, normalizeSort, sortSignatureOf, requireSortableFields, fanOutQuery, applySorting, applyPagination, resolveProjection, countsWithoutScores, fanOutMatchCount, tokenize, decodePageCursor, requireMatchingCursor, encodePageCursor, resolveIndexAnalysis, deserializePayloadV2, deserializePayloadV1, mergePartitionStats, fulltextMatches, mergeFacets, pruneStatsToQueryTerms, createPartitionIndex, fnv1a, partitionsIn, isNodeMainThread, spawnNodeWorker, CHECKSUM_TIMEOUT_MS, CHECKSUM_YIELD_CHUNK_BYTES, createSharedFrozenSegment, buildCompactedSegmentPayload, freezeSegmentShared, createFrozenSegment, isCompositePartition, deserializeMetadata, serializeMetadata, createWorkerPool, WORKER_RESOURCE_LIMITS, DEFAULT_BACKPRESSURE_LIMIT, DEFAULT_REQUEST_TIMEOUT_MS, SHUTDOWN_TIMEOUT_MS, CONSECUTIVE_TIMEOUTS_BEFORE_DEATH, encodeSegmentState, validateRequiredFields, kWayMerge, validateDocument, validateDocumentStrict } from './chunk-M6JGMKA5.mjs';
4
4
  import { crc32, crc32Init, crc32Update, crc32Final } from './chunk-KMMJMDUA.mjs';
5
- import { compareCodePoints, compareComparableKeys } from './chunk-BFIH7OV5.mjs';
6
- import { generateId, createRequestId, isValidWorkerResponse } from './chunk-WTONLDU3.mjs';
5
+ import { compareCodePoints, compareComparableKeys } from './chunk-2GWVQWGA.mjs';
6
+ import { generateId, createRequestId, isValidWorkerResponse } from './chunk-RDGXYRF4.mjs';
7
7
  import { ErrorCodes, NarsilError } from './chunk-P4ELXVJQ.mjs';
8
8
  import { encode, decode } from '@msgpack/msgpack';
9
9
 
10
+ // src/partitioning/constants.ts
11
+ var REBALANCE_CHUNK_SIZE = 1e3;
12
+ var DEFAULT_WRITE_AHEAD_QUEUE_MAX_SIZE = 1e4;
13
+
10
14
  // src/partitioning/rebalancer.ts
11
- var CHUNK_SIZE = 1e3;
12
15
  function createRebalancer() {
13
16
  const activeManagers = /* @__PURE__ */ new WeakSet();
14
17
  let activeCount = 0;
@@ -71,7 +74,7 @@ function createRebalancer() {
71
74
  documentsProcessed++;
72
75
  }
73
76
  chunkFill++;
74
- if (chunkFill >= CHUNK_SIZE) {
77
+ if (chunkFill >= REBALANCE_CHUNK_SIZE) {
75
78
  chunkFill = 0;
76
79
  onProgress?.({
77
80
  phase: "moving",
@@ -269,44 +272,6 @@ function validateRequiredFieldsInSchema(required, schema) {
269
272
  }
270
273
  }
271
274
 
272
- // src/workers/promoter.ts
273
- var DEFAULT_PER_INDEX_THRESHOLD = 1e4;
274
- var DEFAULT_TOTAL_THRESHOLD = 5e4;
275
- function createExecutionPromoter(config) {
276
- const perIndexThreshold = config?.perIndexThreshold ?? DEFAULT_PER_INDEX_THRESHOLD;
277
- const totalThreshold = config?.totalThreshold ?? DEFAULT_TOTAL_THRESHOLD;
278
- let promoted = false;
279
- function check(indexes) {
280
- if (promoted) {
281
- return { shouldPromote: false, reason: "" };
282
- }
283
- let totalDocuments = 0;
284
- for (const [indexName, stats] of indexes) {
285
- if (stats.documentCount >= perIndexThreshold) {
286
- return {
287
- shouldPromote: true,
288
- reason: `Index "${indexName}" has ${stats.documentCount} documents, exceeding the per-index threshold of ${perIndexThreshold}`
289
- };
290
- }
291
- totalDocuments += stats.documentCount;
292
- }
293
- if (totalDocuments >= totalThreshold) {
294
- return {
295
- shouldPromote: true,
296
- reason: `Total document count of ${totalDocuments} exceeds the total threshold of ${totalThreshold}`
297
- };
298
- }
299
- return { shouldPromote: false, reason: "" };
300
- }
301
- function markPromoted() {
302
- promoted = true;
303
- }
304
- function isPromoted() {
305
- return promoted;
306
- }
307
- return { check, markPromoted, isPromoted };
308
- }
309
-
310
275
  // src/engine/analysis-rebuild.ts
311
276
  function yieldToEventLoop() {
312
277
  return new Promise((resolve) => {
@@ -383,7 +348,7 @@ function createAnalysisRebuildCoordinator(config, deps) {
383
348
  throw error;
384
349
  }
385
350
  }
386
- function enqueue2(indexName, entry) {
351
+ function enqueue(indexName, entry) {
387
352
  if (entry.run !== null) {
388
353
  return entry.run;
389
354
  }
@@ -397,7 +362,7 @@ function createAnalysisRebuildCoordinator(config, deps) {
397
362
  }
398
363
  async function announce(indexName, entry) {
399
364
  const documentCount = deps.getManager(indexName)?.countDocuments() ?? 0;
400
- await config?.onStaleAnalysis?.({ ...entry.index, documentCount }, () => enqueue2(indexName, entry));
365
+ await config?.onStaleAnalysis?.({ ...entry.index, documentCount }, () => enqueue(indexName, entry));
401
366
  }
402
367
  async function review(indexName, entry) {
403
368
  if (entry.run !== null || deps.getManager(indexName) === void 0) return;
@@ -413,7 +378,7 @@ function createAnalysisRebuildCoordinator(config, deps) {
413
378
  });
414
379
  }
415
380
  if (rebuildMode === "auto") {
416
- void enqueue2(indexName, entry).catch(() => void 0);
381
+ void enqueue(indexName, entry).catch(() => void 0);
417
382
  }
418
383
  }
419
384
  return {
@@ -447,7 +412,7 @@ function createAnalysisRebuildCoordinator(config, deps) {
447
412
  if (entry === void 0) {
448
413
  return Promise.resolve();
449
414
  }
450
- return enqueue2(indexName, entry);
415
+ return enqueue(indexName, entry);
451
416
  }
452
417
  };
453
418
  }
@@ -532,6 +497,15 @@ function resolveDurabilityTier(config) {
532
497
  return { kind: "snapshot", config: {}, adapter: config.persistence };
533
498
  }
534
499
 
500
+ // src/persistence/durability/constants.ts
501
+ var DEFAULT_CHECKPOINT_INTERVAL_MS = 3e5;
502
+ var DEFAULT_CHECKPOINT_MUTATION_THRESHOLD = 1e5;
503
+ var DEFAULT_ASYNC_FLUSH_INTERVAL_MS = 1e3;
504
+ var DEFAULT_SEGMENT_MAX_BYTES = 67108864;
505
+ var DEFAULT_COMPACTION_THRESHOLD = 12;
506
+ var CHECKPOINT_WORKER_TIMEOUT_MS = 6e5;
507
+ var CHECKPOINT_TIMEOUT_RECOVERY_BACKOFF_MS = 50;
508
+
535
509
  // src/persistence/durability/node-fs.ts
536
510
  var cachedFs = null;
537
511
  var cachedPath = null;
@@ -809,8 +783,6 @@ function buildEntry(fields) {
809
783
  }
810
784
 
811
785
  // src/serialization/checksum-dispatch.ts
812
- var CHECKSUM_TIMEOUT_MS = 12e4;
813
- var YIELD_CHUNK_BYTES = 4 * 1024 * 1024;
814
786
  var workerUsable = true;
815
787
  var mainThreadResolved = false;
816
788
  var onMainThread = true;
@@ -844,8 +816,8 @@ function yieldToEventLoop2() {
844
816
  }
845
817
  async function chunkedChecksum(payload) {
846
818
  let state = crc32Init();
847
- for (let offset = 0; offset < payload.length; offset += YIELD_CHUNK_BYTES) {
848
- const end = Math.min(offset + YIELD_CHUNK_BYTES, payload.length);
819
+ for (let offset = 0; offset < payload.length; offset += CHECKSUM_YIELD_CHUNK_BYTES) {
820
+ const end = Math.min(offset + CHECKSUM_YIELD_CHUNK_BYTES, payload.length);
849
821
  state = crc32Update(state, payload.subarray(offset, end));
850
822
  if (end < payload.length) {
851
823
  await yieldToEventLoop2();
@@ -1437,8 +1409,6 @@ function formatSegmentStart(startSeqNo) {
1437
1409
  }
1438
1410
 
1439
1411
  // src/persistence/durability/checkpoint-worker-dispatch.ts
1440
- var CHECKPOINT_WORKER_TIMEOUT_MS = 6e5;
1441
- var TIMEOUT_RECOVERY_BACKOFF_MS = 50;
1442
1412
  var workerUsable2 = true;
1443
1413
  var pooledWorker = null;
1444
1414
  var workerBusy = false;
@@ -1551,7 +1521,7 @@ async function runCheckpointOnWorker(request) {
1551
1521
  return true;
1552
1522
  }
1553
1523
  if (outcome.timedOut) {
1554
- await delay(TIMEOUT_RECOVERY_BACKOFF_MS);
1524
+ await delay(CHECKPOINT_TIMEOUT_RECOVERY_BACKOFF_MS);
1555
1525
  }
1556
1526
  return false;
1557
1527
  } finally {
@@ -1571,7 +1541,6 @@ function terminateCheckpointWorker() {
1571
1541
  }
1572
1542
 
1573
1543
  // src/persistence/durability/segment/layout.ts
1574
- var DEFAULT_COMPACTION_THRESHOLD = 12;
1575
1544
  var SEGMENT_ID_WIDTH = 16;
1576
1545
  function manifestKey(indexName) {
1577
1546
  return `${indexName}/manifest`;
@@ -2513,37 +2482,6 @@ function setNestedValue(obj, path, value) {
2513
2482
  }
2514
2483
  current[finalSegment] = value;
2515
2484
  }
2516
-
2517
- // src/distribution/chunking.ts
2518
- var WIRE_BATCH_BUDGET = {
2519
- maxCount: 1e3,
2520
- maxBytes: 8388608,
2521
- overheadBytes: 256
2522
- };
2523
- function chunkByBudget(items, budget) {
2524
- const chunks = [];
2525
- let current = [];
2526
- let currentBytes = 0;
2527
- for (const item of items) {
2528
- const itemBytes = budget.payloadBytesOf(item) + budget.overheadBytes;
2529
- const previous = current[current.length - 1];
2530
- const breaksRun = previous !== void 0 && budget.breaksRun?.(item, previous) === true;
2531
- const overflows = current.length >= budget.maxCount || current.length > 0 && currentBytes + itemBytes > budget.maxBytes;
2532
- if (breaksRun || overflows) {
2533
- chunks.push(current);
2534
- current = [];
2535
- currentBytes = 0;
2536
- }
2537
- current.push(item);
2538
- currentBytes += itemBytes;
2539
- }
2540
- if (current.length > 0) {
2541
- chunks.push(current);
2542
- }
2543
- return chunks;
2544
- }
2545
-
2546
- // src/distribution/replication/codec.ts
2547
2485
  function createEntryMessage(entry, sourceId) {
2548
2486
  const payload = { entry };
2549
2487
  return {
@@ -3266,7 +3204,10 @@ async function writePartitionVectors(input) {
3266
3204
  }
3267
3205
  const vectorIndexes = /* @__PURE__ */ new Map();
3268
3206
  for (const [fieldPath, dimension] of input.vectorFields) {
3269
- vectorIndexes.set(fieldPath, createVectorIndex(fieldPath, dimension, input.config.vectorPromotion));
3207
+ vectorIndexes.set(
3208
+ fieldPath,
3209
+ createVectorIndex(fieldPath, dimension, input.config.vectorPromotion, { enabled: false })
3210
+ );
3270
3211
  }
3271
3212
  for (const ref of input.priorVectors) {
3272
3213
  const vecIndex = vectorIndexes.get(ref.fieldPath);
@@ -3684,11 +3625,6 @@ async function recoverPersistedIndex(deps, indexName, metadataOnly) {
3684
3625
  }
3685
3626
  }
3686
3627
 
3687
- // src/persistence/durability/types.ts
3688
- var DEFAULT_CHECKPOINT_INTERVAL_MS = 3e5;
3689
- var DEFAULT_CHECKPOINT_MUTATION_THRESHOLD = 1e5;
3690
- var DEFAULT_ASYNC_FLUSH_INTERVAL_MS = 1e3;
3691
-
3692
3628
  // src/persistence/durability/group-commit.ts
3693
3629
  function createGroupCommitCoordinator(sync) {
3694
3630
  let inFlight = null;
@@ -3762,7 +3698,6 @@ async function createMarkerWriter(directory, indexName, partitionId) {
3762
3698
  }
3763
3699
 
3764
3700
  // src/persistence/durability/wal-writer.ts
3765
- var DEFAULT_SEGMENT_MAX_BYTES = 67108864;
3766
3701
  var SEGMENT_TAIL_PATTERN = /^\d{16}$/;
3767
3702
  function segmentKey2(indexName, partitionId, startSeqNo) {
3768
3703
  const padded = startSeqNo.toString().padStart(16, "0");
@@ -4680,11 +4615,11 @@ function createDurabilityFromTier(tier, wiring) {
4680
4615
  });
4681
4616
  }
4682
4617
 
4683
- // src/engine/index-state/entry.ts
4618
+ // src/engine/index-state/constants.ts
4684
4619
  var DEFAULT_MAX_REOPEN_WAITERS = 64;
4685
4620
  var MAX_REOPEN_FAILURES = 5;
4686
- var MIN_SWEEP_INTERVAL_MS = 100;
4687
- var MAX_SWEEP_INTERVAL_MS = 6e4;
4621
+
4622
+ // src/engine/index-state/entry.ts
4688
4623
  function createLifecycleEntry(state) {
4689
4624
  return {
4690
4625
  state,
@@ -4958,7 +4893,10 @@ function createIndexStateCoordinator(config, callbacks) {
4958
4893
  await reopen(indexName, false);
4959
4894
  entry = entryOf(indexName);
4960
4895
  }
4961
- if (markActive) entry.lastAccessAt = Date.now();
4896
+ if (markActive) {
4897
+ entry.lastAccessAt = Date.now();
4898
+ callbacks.onAccess?.(indexName);
4899
+ }
4962
4900
  entry.activeOperations += 1;
4963
4901
  try {
4964
4902
  if (opened) await enforceLimits(indexName);
@@ -5061,6 +4999,9 @@ function wireIndexState(wiring) {
5061
4999
  }
5062
5000
  }
5063
5001
  },
5002
+ onAccess(indexName) {
5003
+ wiring.onAccess?.(indexName);
5004
+ },
5064
5005
  canCloseAutomatically(indexName) {
5065
5006
  return !rebalancingIndexes.has(indexName) && !analysisRebuild.isRunning(indexName) && !orchestrator.isIndexBusy(indexName);
5066
5007
  },
@@ -5071,8 +5012,6 @@ function wireIndexState(wiring) {
5071
5012
  }
5072
5013
 
5073
5014
  // src/engine/invalidation.ts
5074
- var STATISTICS_BROADCAST_INTERVAL_MS = 5e3;
5075
- var FOREIGN_STATS_TTL_MS = 6e4;
5076
5015
  function createInvalidationFromConfig(config, tierKind, deps) {
5077
5016
  if (config?.invalidation === void 0) {
5078
5017
  return null;
@@ -5226,17 +5165,99 @@ function createInvalidationIntegration(deps) {
5226
5165
  };
5227
5166
  }
5228
5167
 
5168
+ // src/core/partition/frozen/share.ts
5169
+ function shareFrozenSegment(segment) {
5170
+ if (segment.sharedSnapshot !== null) {
5171
+ return { snapshot: segment.sharedSnapshot, tombstonedDocIds: segment.tombstonedDocIds() };
5172
+ }
5173
+ const payload = encodeSegmentState(segment);
5174
+ const documents = payload.docIds.map((docId) => {
5175
+ const stored = segment.docStore.get(docId);
5176
+ if (stored === void 0) {
5177
+ throw new NarsilError(ErrorCodes.PARTITION_CORRUPTED, `Document "${docId}" vanished while sharing a segment`, {
5178
+ docId
5179
+ });
5180
+ }
5181
+ return stored.fields;
5182
+ });
5183
+ const snapshot = freezeSegmentShared(payload, documents, segment.segmentId);
5184
+ return snapshot === null ? null : { snapshot, tombstonedDocIds: [] };
5185
+ }
5186
+
5229
5187
  // src/engine/worker-resync.ts
5188
+ function captureComposite(manager, partitionId) {
5189
+ const partition = manager.getPartition(partitionId);
5190
+ if (!isCompositePartition(partition)) return null;
5191
+ const segmentIds = partition.frozenSegmentSizes().map((size) => size.segmentId);
5192
+ const frozen = [];
5193
+ for (const segment of partition.frozenSegmentsById(segmentIds)) {
5194
+ const shared = shareFrozenSegment(segment);
5195
+ if (shared === null) return null;
5196
+ frozen.push(shared);
5197
+ }
5198
+ const live = partition.live.serialize(
5199
+ manager.indexName,
5200
+ manager.partitionCount,
5201
+ manager.language.name,
5202
+ manager.schema
5203
+ );
5204
+ return { partitionId, live, frozen };
5205
+ }
5206
+ function capturePartitions(manager) {
5207
+ const captured = [];
5208
+ for (let partitionId = 0; partitionId < manager.partitionCount; partitionId++) {
5209
+ captured.push(
5210
+ captureComposite(manager, partitionId) ?? {
5211
+ partitionId,
5212
+ live: manager.serializePartition(partitionId),
5213
+ frozen: []
5214
+ }
5215
+ );
5216
+ }
5217
+ return captured;
5218
+ }
5219
+ async function sendPartition(indexName, executors, captured) {
5220
+ await Promise.all(
5221
+ executors.map(
5222
+ (workerExecutor) => workerExecutor.execute({
5223
+ type: "deserialize",
5224
+ indexName,
5225
+ partitionId: captured.partitionId,
5226
+ data: captured.live,
5227
+ requestId: `resync-${indexName}-${captured.partitionId}`
5228
+ })
5229
+ )
5230
+ );
5231
+ if (captured.frozen.length === 0) return;
5232
+ const segments = captured.frozen.map((shared) => ({
5233
+ partitionId: captured.partitionId,
5234
+ snapshot: shared.snapshot,
5235
+ tombstonedDocIds: shared.tombstonedDocIds
5236
+ }));
5237
+ await Promise.all(
5238
+ executors.map(
5239
+ (workerExecutor) => workerExecutor.execute({
5240
+ type: "attachSegments",
5241
+ indexName,
5242
+ segments,
5243
+ requestId: `resync-attach-${indexName}-${captured.partitionId}`
5244
+ })
5245
+ )
5246
+ );
5247
+ }
5230
5248
  async function transferIndexToPool(indexName, pool, config, manager) {
5231
- const allExecutors = pool.getAllExecutors();
5249
+ await loadIndexOntoWorkers(indexName, pool.getAllExecutors(), config, manager, () => pool.addIndexToAll(indexName));
5250
+ }
5251
+ async function loadIndexOntoWorkers(indexName, executors, config, manager, onDropped) {
5252
+ const captured = capturePartitions(manager);
5232
5253
  await Promise.allSettled(
5233
- allExecutors.map(
5254
+ executors.map(
5234
5255
  (workerExecutor) => workerExecutor.execute({ type: "dropIndex", indexName, requestId: `resync-drop-${indexName}` })
5235
5256
  )
5236
5257
  );
5237
- pool.addIndexToAll(indexName);
5258
+ onDropped?.();
5238
5259
  await Promise.all(
5239
- allExecutors.map(
5260
+ executors.map(
5240
5261
  (workerExecutor) => workerExecutor.execute({
5241
5262
  type: "createIndex",
5242
5263
  indexName,
@@ -5245,22 +5266,25 @@ async function transferIndexToPool(indexName, pool, config, manager) {
5245
5266
  })
5246
5267
  )
5247
5268
  );
5248
- for (let i = 0; i < manager.partitionCount; i++) {
5249
- const serialized = manager.serializePartition(i);
5250
- await Promise.all(
5251
- allExecutors.map(
5252
- (workerExecutor) => workerExecutor.execute({
5253
- type: "deserialize",
5254
- indexName,
5255
- partitionId: i,
5256
- data: serialized,
5257
- requestId: `resync-${indexName}-${i}`
5258
- })
5259
- )
5260
- );
5269
+ for (const partition of captured) {
5270
+ await sendPartition(indexName, executors, partition);
5261
5271
  }
5262
5272
  }
5263
5273
 
5274
+ // src/engine/orchestration/constants.ts
5275
+ var DEFAULT_COPY_THRESHOLD = 1e3;
5276
+ var DEFAULT_COPY_IDLE_TIMEOUT_MS = 3e5;
5277
+ var COMPACTION_SEGMENT_TRIGGER = 8;
5278
+ var IDLE_MERGE_DELAY_MS = 1e3;
5279
+ var LIVE_TAIL_FREEZE_FLOOR = MIN_DOCUMENTS_FOR_SEGMENTS;
5280
+ var LIVE_TAIL_FLUSH_DOCUMENTS = 5e3;
5281
+ var MAX_PENDING_REPLICATION_DOCUMENTS = 2e4;
5282
+ var REPLICATION_WINDOW = 32;
5283
+ var LOAD_BUFFER_YIELD_INTERVAL = 1e3;
5284
+ var MAIN_COPY_LONE_QUERY_DOCUMENTS = 5e4;
5285
+ var POOL_RESTART_DELAY_MS = 1e3;
5286
+ var POOL_RESTART_DELAY_MAX_MS = 6e4;
5287
+
5264
5288
  // src/engine/orchestration/eligibility.ts
5265
5289
  function toError3(value) {
5266
5290
  return value instanceof Error ? value : new Error(String(value));
@@ -5309,7 +5333,7 @@ function workerIneligibility(indexName, config, bootstrapModule) {
5309
5333
  function reportIneligible(state, indexName, error) {
5310
5334
  if (state.reportedIneligible.has(indexName)) return;
5311
5335
  state.reportedIneligible.add(indexName);
5312
- state.callbacks?.onPromotionFailure?.("index-excluded", error, false);
5336
+ state.callbacks?.onCopyLoadFailure?.("index-excluded", error, false);
5313
5337
  }
5314
5338
  function eligibleIndexNames(state) {
5315
5339
  const names = [];
@@ -5323,33 +5347,26 @@ function eligibleIndexNames(state) {
5323
5347
  }
5324
5348
  return names;
5325
5349
  }
5326
- function collectEligibleIndexes(state) {
5327
- const eligible = /* @__PURE__ */ new Map();
5328
- for (const name of eligibleIndexNames(state)) {
5329
- const manager = state.executor.getManager(name);
5330
- eligible.set(name, { documentCount: manager?.countDocuments() ?? 0 });
5350
+
5351
+ // src/engine/orchestration/turn.ts
5352
+ function afterCurrentTurn(callback) {
5353
+ if (typeof setImmediate === "function") {
5354
+ setImmediate(callback);
5355
+ return;
5331
5356
  }
5332
- return eligible;
5357
+ setTimeout(callback, 0);
5358
+ }
5359
+ function yieldToEventLoop3() {
5360
+ return new Promise((resolve) => afterCurrentTurn(resolve));
5333
5361
  }
5334
5362
 
5335
5363
  // src/engine/orchestration/replication.ts
5336
- var MAX_PENDING_REPLICATION_DOCUMENTS = 2e4;
5337
5364
  async function dispatchToWorkers(state, action) {
5338
5365
  const pool = state.workerPool;
5339
5366
  if (!pool) return;
5340
- if (action.type === "createIndex") {
5341
- const ineligibility = workerIneligibility(action.indexName, action.config, state.bootstrapModule);
5342
- if (ineligibility) {
5343
- reportIneligible(state, action.indexName, ineligibility);
5344
- return;
5345
- }
5346
- pool.addIndexToAll(action.indexName);
5347
- state.promotedIndexes.add(action.indexName);
5348
- } else if ("indexName" in action && !state.promotedIndexes.has(action.indexName)) {
5349
- return;
5350
- }
5351
- const allExecutors = pool.getAllExecutors();
5352
- const results = await Promise.allSettled(allExecutors.map((workerExecutor) => workerExecutor.execute(action)));
5367
+ const executors = "indexName" in action ? pool.executorsHolding(action.indexName) : pool.getAllExecutors();
5368
+ if ("indexName" in action && !state.scaledOutIndexes.has(action.indexName)) return;
5369
+ const results = await Promise.allSettled(executors.map((workerExecutor) => workerExecutor.execute(action)));
5353
5370
  for (const result of results) {
5354
5371
  if (result.status === "rejected") {
5355
5372
  if (action.type === "insert" && alreadyPresentOnWorker(result.reason)) {
@@ -5417,6 +5434,13 @@ function recordSegmentBroadcast(state, action) {
5417
5434
  }
5418
5435
  return;
5419
5436
  }
5437
+ if (action.type === "freezeLiveTail") {
5438
+ partitionLedger(state, action.indexName, action.partitionId).push({
5439
+ segmentId: action.snapshot.segmentId,
5440
+ documentCount: action.snapshot.documentCount
5441
+ });
5442
+ return;
5443
+ }
5420
5444
  if (action.type === "swapSegments") {
5421
5445
  const entries = partitionLedger(state, action.indexName, action.partitionId);
5422
5446
  const dropped = new Set(action.dropSegmentIds);
@@ -5429,48 +5453,71 @@ function recordSegmentBroadcast(state, action) {
5429
5453
  function queueFor(state, indexName) {
5430
5454
  let queue = state.replicationQueues.get(indexName);
5431
5455
  if (queue === void 0) {
5432
- queue = { tail: Promise.resolve(), pendingActions: 0, pendingDocuments: 0 };
5456
+ queue = { tail: Promise.resolve(), inFlight: [], pendingActions: 0, pendingDocuments: 0, drainWaiters: [] };
5433
5457
  state.replicationQueues.set(indexName, queue);
5434
5458
  }
5435
5459
  return queue;
5436
5460
  }
5437
- function enqueue(state, indexName, action, weight) {
5461
+ function dispatchGate(queue) {
5462
+ if (queue.inFlight.length < REPLICATION_WINDOW) return Promise.resolve();
5463
+ return queue.inFlight[queue.inFlight.length - REPLICATION_WINDOW];
5464
+ }
5465
+ function enqueueReplication(state, indexName, action) {
5466
+ const weight = documentWeight(action);
5467
+ recordSegmentBroadcast(state, action);
5438
5468
  const queue = queueFor(state, indexName);
5439
5469
  queue.pendingActions += 1;
5440
5470
  queue.pendingDocuments += weight;
5441
- queue.tail = queue.tail.then(async () => {
5471
+ const run = dispatchGate(queue).then(async () => {
5442
5472
  try {
5443
5473
  await dispatchToWorkers(state, action);
5444
5474
  } catch (err) {
5445
5475
  console.warn("Worker replication failed:", err);
5446
5476
  } finally {
5477
+ const at = queue.inFlight.indexOf(run);
5478
+ if (at !== -1) queue.inFlight.splice(at, 1);
5447
5479
  queue.pendingActions -= 1;
5448
5480
  queue.pendingDocuments -= weight;
5481
+ if (queue.pendingDocuments < MAX_PENDING_REPLICATION_DOCUMENTS && queue.drainWaiters.length > 0) {
5482
+ for (const resume of queue.drainWaiters.splice(0)) resume();
5483
+ }
5449
5484
  if (queue.pendingActions === 0 && state.replicationQueues.get(indexName) === queue) {
5450
5485
  state.replicationQueues.delete(indexName);
5451
5486
  }
5452
5487
  }
5453
5488
  });
5489
+ queue.inFlight.push(run);
5490
+ queue.tail = run;
5454
5491
  }
5455
5492
  async function replicateToWorkers(state, action) {
5456
- if (state.promotionInProgress) {
5457
- state.promotionBuffer.push(detachCallerDocuments(action));
5493
+ if (!("indexName" in action)) {
5494
+ if (state.workerPool === null) return;
5495
+ await awaitReplicationIdle(state);
5496
+ await dispatchToWorkers(state, action);
5458
5497
  return;
5459
5498
  }
5460
- if (state.workerPool === null) return;
5461
5499
  const detached = detachCallerDocuments(action);
5462
- if (!("indexName" in detached)) {
5463
- await awaitReplicationIdle(state);
5464
- await dispatchToWorkers(state, detached);
5500
+ const loading = state.copyLoadBuffers.get(action.indexName);
5501
+ if (loading !== void 0) {
5502
+ queueForCopies(state, detached);
5503
+ if (loading.length % LOAD_BUFFER_YIELD_INTERVAL === 0) await yieldToEventLoop3();
5465
5504
  return;
5466
5505
  }
5467
- const weight = documentWeight(detached);
5468
- const queue = state.replicationQueues.get(detached.indexName);
5469
- if (queue !== void 0 && queue.pendingDocuments + weight > MAX_PENDING_REPLICATION_DOCUMENTS) {
5470
- await queue.tail;
5506
+ const queue = state.replicationQueues.get(action.indexName);
5507
+ if (queue !== void 0 && queue.pendingDocuments + documentWeight(detached) > MAX_PENDING_REPLICATION_DOCUMENTS) {
5508
+ await new Promise((resume) => queue.drainWaiters.push(resume));
5509
+ }
5510
+ queueForCopies(state, detached);
5511
+ }
5512
+ function queueForCopies(state, action) {
5513
+ if (!("indexName" in action)) return;
5514
+ const loading = state.copyLoadBuffers.get(action.indexName);
5515
+ if (loading !== void 0) {
5516
+ loading.push(action);
5517
+ return;
5471
5518
  }
5472
- recordSegmentBroadcast(state, detached);
5473
- enqueue(state, detached.indexName, detached, weight);
5519
+ if (state.workerPool === null || !state.scaledOutIndexes.has(action.indexName)) return;
5520
+ enqueueReplication(state, action.indexName, action);
5474
5521
  }
5475
5522
  async function awaitReplicationIdle(state, indexName) {
5476
5523
  if (indexName !== void 0) {
@@ -5483,63 +5530,101 @@ async function awaitReplicationIdle(state, indexName) {
5483
5530
  await Promise.all(tails);
5484
5531
  }
5485
5532
  }
5486
- async function drainPromotionBuffer(state) {
5487
- while (state.promotionBuffer.length > 0) {
5488
- const action = state.promotionBuffer.shift();
5489
- if (action === void 0) return;
5490
- await dispatchToWorkers(state, action);
5533
+
5534
+ // src/engine/orchestration/live-tail.ts
5535
+ function liveTailCount(manager, partitionId) {
5536
+ if (partitionId >= manager.partitionCount) return 0;
5537
+ const partition = manager.getPartition(partitionId);
5538
+ return isCompositePartition(partition) ? partition.live.count() : partition.count();
5539
+ }
5540
+ function freezeTail(state, indexName, manager, partitionId) {
5541
+ const segment = manager.freezeLiveTail(partitionId, (payload, documents) => {
5542
+ const snapshot = freezeSegmentShared(payload, documents);
5543
+ return snapshot === null ? null : createSharedFrozenSegment(snapshot);
5544
+ });
5545
+ if (segment === null || segment.sharedSnapshot === null) return;
5546
+ queueForCopies(state, {
5547
+ type: "freezeLiveTail",
5548
+ indexName,
5549
+ partitionId,
5550
+ snapshot: segment.sharedSnapshot,
5551
+ requestId: createRequestId()
5552
+ });
5553
+ }
5554
+ function freezeLiveTail(state, indexName, manager, partitionId, floor) {
5555
+ if (!state.scaledOutIndexes.has(indexName)) return;
5556
+ if (liveTailCount(manager, partitionId) < floor) return;
5557
+ try {
5558
+ freezeTail(state, indexName, manager, partitionId);
5559
+ } catch (err) {
5560
+ console.warn(`Freezing the live tail of partition ${partitionId} of "${indexName}" failed:`, err);
5561
+ }
5562
+ }
5563
+ function flushGrownTails(state, indexName) {
5564
+ const manager = state.executor.getManager(indexName);
5565
+ if (manager === void 0) return;
5566
+ for (let partitionId = 0; partitionId < manager.partitionCount; partitionId++) {
5567
+ freezeLiveTail(state, indexName, manager, partitionId, LIVE_TAIL_FLUSH_DOCUMENTS);
5491
5568
  }
5492
5569
  }
5493
5570
 
5494
5571
  // src/engine/orchestration/compaction.ts
5495
- var COMPACTION_SEGMENT_TRIGGER = 8;
5496
5572
  function smallestSegmentIds(entries) {
5497
5573
  return [...entries].sort((a, b) => a.documentCount - b.documentCount).slice(0, COMPACTION_SEGMENT_TRIGGER).map((entry) => entry.segmentId);
5498
5574
  }
5575
+ function pickSegmentIds(entries, policy) {
5576
+ if (policy === "idle") {
5577
+ return entries.length >= 2 ? entries.map((entry) => entry.segmentId) : null;
5578
+ }
5579
+ return entries.length >= COMPACTION_SEGMENT_TRIGGER ? smallestSegmentIds(entries) : null;
5580
+ }
5499
5581
  function mainCompositeOf(manager, partitionId) {
5500
5582
  if (partitionId >= manager.partitionCount) return null;
5501
5583
  const partition = manager.getPartition(partitionId);
5502
5584
  return isCompositePartition(partition) ? partition : null;
5503
5585
  }
5504
- function candidateSegmentIds(state, indexName, manager, partitionId) {
5586
+ function candidateSegmentIds(state, indexName, manager, partitionId, policy) {
5505
5587
  const composite = mainCompositeOf(manager, partitionId);
5506
5588
  if (composite !== null) {
5507
5589
  const sizes = composite.frozenSegmentSizes();
5508
- if (sizes.length >= COMPACTION_SEGMENT_TRIGGER) {
5509
- return smallestSegmentIds(
5510
- sizes.map((size) => ({ segmentId: size.segmentId, documentCount: size.liveDocumentCount }))
5511
- );
5512
- }
5590
+ const picks = pickSegmentIds(
5591
+ sizes.map((size) => ({ segmentId: size.segmentId, documentCount: size.liveDocumentCount })),
5592
+ policy
5593
+ );
5594
+ if (picks !== null) return picks;
5513
5595
  }
5514
5596
  const ledger = state.segmentLedger.get(indexName)?.get(partitionId);
5515
- if (ledger !== void 0 && ledger.length >= COMPACTION_SEGMENT_TRIGGER) {
5516
- return smallestSegmentIds(ledger);
5517
- }
5518
- return null;
5597
+ return ledger === void 0 ? null : pickSegmentIds(ledger, policy);
5519
5598
  }
5520
5599
  function mainHoldsAll(composite, segmentIds) {
5521
5600
  if (composite === null) return false;
5522
5601
  const held = new Set(composite.frozenSegmentSizes().map((size) => size.segmentId));
5523
5602
  return segmentIds.every((segmentId) => held.has(segmentId));
5524
5603
  }
5525
- async function compactPartitionSegments2(state, indexName, manager, partitionId, segmentIds) {
5526
- await awaitReplicationIdle(state, indexName);
5527
- let snapshot = null;
5604
+ async function compactOnWorker(state, indexName, partitionId, segmentIds) {
5528
5605
  const pool = state.workerPool;
5529
- const executors = pool !== null && state.promotedIndexes.has(indexName) ? pool.getAllExecutors() : [];
5530
- if (executors.length > 0) {
5531
- try {
5532
- snapshot = await executors[0].execute({
5533
- type: "compactSegments",
5534
- indexName,
5535
- partitionId,
5536
- segmentIds: [...segmentIds],
5537
- requestId: createRequestId()
5538
- });
5539
- } catch (err) {
5540
- console.warn("Worker segment compaction failed, compacting on the main thread:", err);
5541
- }
5606
+ if (pool === null || !state.scaledOutIndexes.has(indexName)) return null;
5607
+ const lease = pool.leaseLeastBusy();
5608
+ if (lease === null) return null;
5609
+ try {
5610
+ return await lease.executor.execute({
5611
+ type: "compactSegments",
5612
+ indexName,
5613
+ partitionId,
5614
+ segmentIds: [...segmentIds],
5615
+ requestId: createRequestId()
5616
+ });
5617
+ } catch (err) {
5618
+ console.warn("Worker segment compaction failed, compacting on the main thread:", err);
5619
+ return null;
5620
+ } finally {
5621
+ lease.release();
5542
5622
  }
5623
+ }
5624
+ async function compactPartitionSegments2(state, indexName, manager, partitionId, segmentIds, policy) {
5625
+ await awaitReplicationIdle(state, indexName);
5626
+ if (policy === "idle" && !state.scaledOutIndexes.has(indexName)) return false;
5627
+ let snapshot = await compactOnWorker(state, indexName, partitionId, segmentIds);
5543
5628
  const composite = mainCompositeOf(manager, partitionId);
5544
5629
  const holdsAll = mainHoldsAll(composite, segmentIds);
5545
5630
  let replacement = null;
@@ -5566,30 +5651,52 @@ async function compactPartitionSegments2(state, indexName, manager, partitionId,
5566
5651
  }
5567
5652
  return true;
5568
5653
  }
5569
- async function compactIndexSegments(state, indexName) {
5654
+ async function compactIndexSegments(state, indexName, policy) {
5570
5655
  const manager = state.executor.getManager(indexName);
5571
5656
  if (!manager) return;
5572
5657
  const partitionIds = /* @__PURE__ */ new Set();
5573
5658
  for (let partitionId = 0; partitionId < manager.partitionCount; partitionId++) partitionIds.add(partitionId);
5574
5659
  for (const partitionId of state.segmentLedger.get(indexName)?.keys() ?? []) partitionIds.add(partitionId);
5575
5660
  for (const partitionId of partitionIds) {
5576
- let picks = candidateSegmentIds(state, indexName, manager, partitionId);
5661
+ if (policy === "idle") {
5662
+ await awaitReplicationIdle(state, indexName);
5663
+ freezeLiveTail(state, indexName, manager, partitionId, LIVE_TAIL_FREEZE_FLOOR);
5664
+ }
5665
+ let picks = candidateSegmentIds(state, indexName, manager, partitionId, policy);
5577
5666
  while (picks !== null) {
5578
- const compacted = await compactPartitionSegments2(state, indexName, manager, partitionId, picks);
5667
+ const compacted = await compactPartitionSegments2(state, indexName, manager, partitionId, picks, policy);
5579
5668
  if (!compacted) break;
5580
- picks = candidateSegmentIds(state, indexName, manager, partitionId);
5669
+ picks = candidateSegmentIds(state, indexName, manager, partitionId, policy);
5581
5670
  }
5582
5671
  }
5583
5672
  }
5584
- function maybeCompactSegments(state, indexName) {
5585
- if (state.compactionsInFlight.has(indexName)) return;
5586
- const run = compactIndexSegments(state, indexName).catch((err) => {
5673
+ function maybeCompactSegments(state, indexName, policy = "ingest") {
5674
+ if (state.compactionsInFlight.has(indexName)) {
5675
+ if (policy === "idle") scheduleIdleMerge(state, indexName);
5676
+ return;
5677
+ }
5678
+ const run = compactIndexSegments(state, indexName, policy).catch((err) => {
5587
5679
  console.warn("Segment compaction failed:", err);
5588
5680
  }).finally(() => {
5589
5681
  state.compactionsInFlight.delete(indexName);
5590
5682
  });
5591
5683
  state.compactionsInFlight.set(indexName, run);
5592
5684
  }
5685
+ function scheduleIdleMerge(state, indexName) {
5686
+ cancelIdleMerge(state, indexName);
5687
+ const timer = setTimeout(() => {
5688
+ state.idleMergeTimers.delete(indexName);
5689
+ maybeCompactSegments(state, indexName, "idle");
5690
+ }, IDLE_MERGE_DELAY_MS);
5691
+ if (typeof timer.unref === "function") timer.unref();
5692
+ state.idleMergeTimers.set(indexName, timer);
5693
+ }
5694
+ function cancelIdleMerge(state, indexName) {
5695
+ const timer = state.idleMergeTimers.get(indexName);
5696
+ if (timer === void 0) return;
5697
+ clearTimeout(timer);
5698
+ state.idleMergeTimers.delete(indexName);
5699
+ }
5593
5700
  async function awaitCompactions(state) {
5594
5701
  while (state.compactionsInFlight.size > 0) {
5595
5702
  await Promise.all([...state.compactionsInFlight.values()]);
@@ -5597,10 +5704,6 @@ async function awaitCompactions(state) {
5597
5704
  }
5598
5705
 
5599
5706
  // src/workers/worker-executor.ts
5600
- var DEFAULT_BACKPRESSURE_LIMIT = 100;
5601
- var DEFAULT_REQUEST_TIMEOUT = 3e4;
5602
- var SHUTDOWN_TIMEOUT = 5e3;
5603
- var CONSECUTIVE_TIMEOUTS_BEFORE_DEATH = 3;
5604
5707
  function errorFromEventLike(event) {
5605
5708
  if (event instanceof Error) {
5606
5709
  return event;
@@ -5618,7 +5721,7 @@ function errorFromEventLike(event) {
5618
5721
  }
5619
5722
  function createWorkerExecutor(worker, config) {
5620
5723
  const backpressureLimit = config?.backpressureLimit ?? DEFAULT_BACKPRESSURE_LIMIT;
5621
- const requestTimeout = config?.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
5724
+ const requestTimeout = config?.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT_MS;
5622
5725
  const pending = /* @__PURE__ */ new Map();
5623
5726
  let deathError = null;
5624
5727
  let shutdownRequested = false;
@@ -5706,7 +5809,7 @@ function createWorkerExecutor(worker, config) {
5706
5809
  const timeoutId = setTimeout(() => {
5707
5810
  pending.delete(requestId);
5708
5811
  reject(new NarsilError(ErrorCodes.WORKER_TIMEOUT, "Shutdown timed out"));
5709
- }, SHUTDOWN_TIMEOUT);
5812
+ }, SHUTDOWN_TIMEOUT_MS);
5710
5813
  pending.set(requestId, {
5711
5814
  resolve: () => resolve(),
5712
5815
  reject,
@@ -5742,7 +5845,9 @@ async function createWorkerFactory(entryPoint) {
5742
5845
  if (runtime.supportsWorkerThreads) {
5743
5846
  const workerThreadsModule = await import('worker_threads');
5744
5847
  return function nodeFactory(_workerId, onDeath) {
5745
- const instance = new workerThreadsModule.Worker(new URL(resolvedEntry));
5848
+ const instance = new workerThreadsModule.Worker(new URL(resolvedEntry), {
5849
+ resourceLimits: WORKER_RESOURCE_LIMITS
5850
+ });
5746
5851
  return createWorkerExecutor(instance, { onDeath });
5747
5852
  };
5748
5853
  }
@@ -5755,136 +5860,323 @@ async function createWorkerFactory(entryPoint) {
5755
5860
  throw new NarsilError(ErrorCodes.WORKER_CRASHED, `No worker support available on runtime "${runtime.runtime}"`);
5756
5861
  }
5757
5862
 
5758
- // src/engine/orchestration/promotion.ts
5759
- async function bootstrapWorkers(state, pool, indexNames) {
5760
- const allExecutors = pool.getAllExecutors();
5761
- if (state.bootstrapModule !== void 0) {
5762
- const moduleUrl = state.bootstrapModule;
5763
- await Promise.all(
5764
- allExecutors.map(
5765
- (workerExecutor) => workerExecutor.execute({ type: "bootstrap", moduleUrl, requestId: "promote-bootstrap" })
5766
- )
5767
- );
5863
+ // src/engine/orchestration/repair.ts
5864
+ var COPY_RESTART_REASON = "A request arrived after every worker crashed and the restart delay passed";
5865
+ function nextRestartDelay(state) {
5866
+ const delay2 = state.poolRetryDelayMs;
5867
+ state.poolRetryDelayMs = Math.min(delay2 * 2, POOL_RESTART_DELAY_MAX_MS);
5868
+ return delay2;
5869
+ }
5870
+ function deferPoolRestart(state) {
5871
+ state.poolRetryAt = Date.now() + nextRestartDelay(state);
5872
+ }
5873
+ function retirePool(state, pool) {
5874
+ if (state.workerPool !== pool) return;
5875
+ state.workerPool = null;
5876
+ cancelRepair(state);
5877
+ deferPoolRestart(state);
5878
+ for (const indexName of state.scaledOutIndexes) state.droppedCopies.set(indexName, COPY_RESTART_REASON);
5879
+ state.scaledOutIndexes.clear();
5880
+ state.segmentLedger.clear();
5881
+ void pool.shutdown().catch(() => void 0);
5882
+ }
5883
+ function handleWorkerCrash(state, pool, workerId, indexNames, error) {
5884
+ state.callbacks?.onWorkerCrash?.(workerId, indexNames, error);
5885
+ if (pool.getAllExecutors().length === 0) {
5886
+ retirePool(state, pool);
5887
+ return;
5768
5888
  }
5769
- for (const name of indexNames) {
5770
- const entry = state.indexRegistry.get(name);
5771
- if (!entry) continue;
5772
- await Promise.all(
5773
- allExecutors.map(
5774
- (workerExecutor) => workerExecutor.execute({
5775
- type: "createIndex",
5776
- indexName: name,
5777
- config: entry.config,
5778
- requestId: `promote-create-${name}`
5779
- })
5780
- )
5781
- );
5782
- const manager = state.executor.getManager(name);
5783
- if (!manager) continue;
5784
- for (let partitionId = 0; partitionId < manager.partitionCount; partitionId++) {
5785
- const serialized = manager.serializePartition(partitionId);
5889
+ scheduleRepair(state, pool);
5890
+ }
5891
+ function scheduleRepair(state, pool) {
5892
+ if (state.workerPool !== pool || state.repairTimer !== null || state.poolRepair !== null) return;
5893
+ const timer = setTimeout(() => {
5894
+ state.repairTimer = null;
5895
+ void repairPool(state, pool);
5896
+ }, state.poolRetryDelayMs);
5897
+ if (typeof timer.unref === "function") timer.unref();
5898
+ state.repairTimer = timer;
5899
+ }
5900
+ function cancelRepair(state) {
5901
+ if (state.repairTimer === null) return;
5902
+ clearTimeout(state.repairTimer);
5903
+ state.repairTimer = null;
5904
+ }
5905
+ async function loadOntoReplacement(state, replacement, indexName) {
5906
+ const entry = state.indexRegistry.get(indexName);
5907
+ const manager = state.executor.getManager(indexName);
5908
+ if (entry === void 0 || manager === void 0 || !state.scaledOutIndexes.has(indexName)) return;
5909
+ const buffered = [];
5910
+ state.copyLoadBuffers.set(indexName, buffered);
5911
+ try {
5912
+ await loadIndexOntoWorkers(indexName, [replacement.executor], entry.config, manager);
5913
+ replacement.hold(indexName);
5914
+ } finally {
5915
+ state.copyLoadBuffers.delete(indexName);
5916
+ for (const action of buffered) enqueueReplication(state, indexName, action);
5917
+ }
5918
+ }
5919
+ async function replaceWorker(state, pool, workerId) {
5920
+ const replacement = pool.spawnReplacement(workerId);
5921
+ if (replacement === null) return;
5922
+ try {
5923
+ if (state.bootstrapModule !== void 0) {
5924
+ await replacement.executor.execute({
5925
+ type: "bootstrap",
5926
+ moduleUrl: state.bootstrapModule,
5927
+ requestId: createRequestId()
5928
+ });
5929
+ }
5930
+ for (const indexName of [...state.scaledOutIndexes]) {
5931
+ await loadOntoReplacement(state, replacement, indexName);
5932
+ }
5933
+ replacement.admit();
5934
+ } catch (err) {
5935
+ replacement.abandon();
5936
+ throw err;
5937
+ }
5938
+ }
5939
+ async function replaceDeadWorkers(state, pool) {
5940
+ for (const workerId of pool.deadWorkerIds()) {
5941
+ if (state.workerPool !== pool) return;
5942
+ await replaceWorker(state, pool, workerId);
5943
+ }
5944
+ state.poolRetryDelayMs = POOL_RESTART_DELAY_MS;
5945
+ }
5946
+ function repairPool(state, pool) {
5947
+ if (state.workerPool !== pool) return Promise.resolve();
5948
+ if (state.poolRepair !== null) return state.poolRepair;
5949
+ const run = replaceDeadWorkers(state, pool).catch((err) => {
5950
+ nextRestartDelay(state);
5951
+ console.warn("Replacing a crashed worker failed:", err);
5952
+ }).finally(() => {
5953
+ state.poolRepair = null;
5954
+ if (pool.deadWorkerIds().length > 0) scheduleRepair(state, pool);
5955
+ });
5956
+ state.poolRepair = run;
5957
+ return run;
5958
+ }
5959
+
5960
+ // src/engine/orchestration/scale-out.ts
5961
+ var COPY_RELOAD_REASON = "A request arrived after an idle spell dropped the worker copies";
5962
+ function copiesAllowed(state) {
5963
+ if (!state.workersEnabled || state.scaleOutBlocked) return false;
5964
+ return state.workerPool !== null || Date.now() >= state.poolRetryAt;
5965
+ }
5966
+ async function startPool(state) {
5967
+ eligibleIndexNames(state);
5968
+ const factory = await createWorkerFactory();
5969
+ let started = null;
5970
+ const pool = createWorkerPool({
5971
+ count: state.keywordWorkerCount,
5972
+ workerFactory: factory,
5973
+ onWorkerCrash(workerId, indexNames, error) {
5974
+ if (started !== null) handleWorkerCrash(state, started, workerId, indexNames, error);
5975
+ }
5976
+ });
5977
+ started = pool;
5978
+ try {
5979
+ pool.spawnAll();
5980
+ if (state.bootstrapModule !== void 0) {
5981
+ const moduleUrl = state.bootstrapModule;
5786
5982
  await Promise.all(
5787
- allExecutors.map(
5788
- (workerExecutor) => workerExecutor.execute({
5789
- type: "deserialize",
5790
- indexName: name,
5791
- partitionId,
5792
- data: serialized,
5793
- requestId: `promote-sync-${name}-${partitionId}`
5794
- })
5983
+ pool.getAllExecutors().map(
5984
+ (workerExecutor) => workerExecutor.execute({ type: "bootstrap", moduleUrl, requestId: "copies-bootstrap" })
5795
5985
  )
5796
5986
  );
5797
5987
  }
5988
+ } catch (err) {
5989
+ await pool.shutdown().catch(() => void 0);
5990
+ throw err;
5798
5991
  }
5992
+ return pool;
5799
5993
  }
5800
- async function runPromotion(state, reason) {
5801
- try {
5802
- const promotable = eligibleIndexNames(state);
5803
- if (promotable.length === 0) {
5804
- state.promotionBuffer.length = 0;
5805
- return;
5806
- }
5807
- const factory = await createWorkerFactory();
5808
- const pool = createWorkerPool({
5809
- count: state.config?.workers?.count,
5810
- workerFactory: factory,
5811
- onWorkerCrash(workerId, indexNames, error) {
5812
- state.callbacks?.onWorkerCrash?.(workerId, indexNames, error);
5994
+ async function ensurePool(state) {
5995
+ if (state.workerPool !== null) return state.workerPool;
5996
+ if (state.poolStart === null) {
5997
+ state.poolStart = startPool(state).then(
5998
+ (pool) => {
5999
+ state.workerPool = pool;
6000
+ state.poolStart = null;
6001
+ return pool;
6002
+ },
6003
+ (err) => {
6004
+ state.poolStart = null;
6005
+ deferPoolRestart(state);
6006
+ throw err;
5813
6007
  }
5814
- });
5815
- for (const name of promotable) {
5816
- pool.addIndexToAll(name);
5817
- }
5818
- await bootstrapWorkers(state, pool, promotable);
5819
- state.workerPool = pool;
5820
- for (const name of promotable) {
5821
- state.promotedIndexes.add(name);
5822
- state.awaitingBufferedWrites.add(name);
5823
- }
5824
- state.promoter.markPromoted();
5825
- await drainPromotionBuffer(state);
5826
- state.awaitingBufferedWrites.clear();
5827
- state.callbacks?.onPromotion?.(pool.workerCount, reason);
6008
+ );
6009
+ }
6010
+ return state.poolStart;
6011
+ }
6012
+ function documentCountOf(state, indexName) {
6013
+ return state.executor.getManager(indexName)?.countDocuments() ?? 0;
6014
+ }
6015
+ function copyThresholdReason(state, indexName) {
6016
+ return `Index "${indexName}" holds ${documentCountOf(state, indexName)} documents, reaching the copy threshold of ${state.copyThreshold}`;
6017
+ }
6018
+ function indexReadyForCopies(state, indexName, incomingCount = 0) {
6019
+ if (!copiesAllowed(state)) return false;
6020
+ if (state.scaledOutIndexes.has(indexName) || state.desyncedIndexes.has(indexName)) return false;
6021
+ if (state.copyTransitions.has(indexName)) return false;
6022
+ if (state.callbacks?.shouldDeferCopies?.()) return false;
6023
+ const entry = state.indexRegistry.get(indexName);
6024
+ if (entry === void 0 || state.executor.getManager(indexName) === void 0) return false;
6025
+ if (documentCountOf(state, indexName) + incomingCount < state.copyThreshold) return false;
6026
+ const ineligibility = workerIneligibility(indexName, entry.config, state.bootstrapModule);
6027
+ if (ineligibility !== null) {
6028
+ reportIneligible(state, indexName, ineligibility);
6029
+ return false;
6030
+ }
6031
+ return true;
6032
+ }
6033
+ async function loadCopies(state, indexName, reason) {
6034
+ const entry = state.indexRegistry.get(indexName);
6035
+ const manager = state.executor.getManager(indexName);
6036
+ if (entry === void 0 || manager === void 0) return;
6037
+ const ineligibility = workerIneligibility(indexName, entry.config, state.bootstrapModule);
6038
+ if (ineligibility !== null) {
6039
+ reportIneligible(state, indexName, ineligibility);
6040
+ return;
6041
+ }
6042
+ const pool = await ensurePool(state);
6043
+ if (state.executor.getManager(indexName) === void 0) return;
6044
+ const dropReason = state.droppedCopies.get(indexName);
6045
+ const reload = state.droppedCopies.delete(indexName);
6046
+ const buffered = [];
6047
+ state.copyLoadBuffers.set(indexName, buffered);
6048
+ try {
6049
+ await transferIndexToPool(indexName, pool, entry.config, manager);
6050
+ state.scaledOutIndexes.add(indexName);
6051
+ state.lastAccessAt.set(indexName, Date.now());
6052
+ state.poolRetryDelayMs = POOL_RESTART_DELAY_MS;
6053
+ for (const action of buffered) enqueueReplication(state, indexName, action);
5828
6054
  } catch (err) {
5829
- state.promotionBuffer.length = 0;
6055
+ if (dropReason !== void 0) state.droppedCopies.set(indexName, dropReason);
5830
6056
  throw err;
5831
6057
  } finally {
5832
- state.awaitingBufferedWrites.clear();
5833
- state.promotionInProgress = false;
6058
+ state.copyLoadBuffers.delete(indexName);
6059
+ }
6060
+ if (reload) state.copyReloadCounts.set(indexName, (state.copyReloadCounts.get(indexName) ?? 0) + 1);
6061
+ scheduleIdleMerge(state, indexName);
6062
+ state.callbacks?.onCopiesLoaded?.(pool.workerCount, reason);
6063
+ }
6064
+ async function loadAfter(previous, state, indexName, reason) {
6065
+ if (previous !== void 0) await previous.done;
6066
+ if (state.poolRepair !== null) await state.poolRepair;
6067
+ if (!copiesAllowed(state)) return;
6068
+ if (state.scaledOutIndexes.has(indexName) || state.desyncedIndexes.has(indexName)) return;
6069
+ await loadCopies(state, indexName, reason);
6070
+ }
6071
+ function scaleOutIndex(state, indexName, reason) {
6072
+ const previous = state.copyTransitions.get(indexName);
6073
+ if (previous !== void 0 && previous.kind !== "drop") return previous.done;
6074
+ const kind = state.droppedCopies.has(indexName) ? "reload" : "load";
6075
+ const done = loadAfter(previous, state, indexName, reason).catch((err) => {
6076
+ const error = toError3(err);
6077
+ state.scaleOutBlocked = isDeterministicFailure(error);
6078
+ state.callbacks?.onCopyLoadFailure?.(reason, error, !state.scaleOutBlocked);
6079
+ }).finally(() => {
6080
+ if (state.copyTransitions.get(indexName)?.done === done) state.copyTransitions.delete(indexName);
6081
+ });
6082
+ state.copyTransitions.set(indexName, { kind, done });
6083
+ return done;
6084
+ }
6085
+ async function scaleOutReadyIndexes(state) {
6086
+ if (!copiesAllowed(state)) return;
6087
+ for (const indexName of state.indexRegistry.keys()) {
6088
+ if (!indexReadyForCopies(state, indexName)) continue;
6089
+ const reason = copyThresholdReason(state, indexName);
6090
+ setTimeout(() => {
6091
+ void scaleOutIndex(state, indexName, reason);
6092
+ }, 0);
6093
+ }
6094
+ }
6095
+ async function scaleOutBeforeBatch(state, indexName, incomingCount) {
6096
+ if (incomingCount <= 0) return;
6097
+ const pending = state.copyTransitions.get(indexName);
6098
+ if (pending !== void 0) {
6099
+ if (pending.kind === "load") await pending.done;
6100
+ return;
5834
6101
  }
6102
+ if (!indexReadyForCopies(state, indexName, incomingCount)) return;
6103
+ await scaleOutIndex(state, indexName, copyThresholdReason(state, indexName));
5835
6104
  }
5836
- async function promoteBeforeBatch(state, indexName, incomingCount) {
5837
- if (!state.workersEnabled || state.promotionBlocked || state.workerPool || incomingCount <= 0) return;
5838
- if (state.promotionInProgress) {
5839
- if (state.promotionRun) await state.promotionRun;
6105
+
6106
+ // src/engine/orchestration/idle.ts
6107
+ function isIndexBusy(state, indexName) {
6108
+ return state.poolRepair !== null || state.copyTransitions.has(indexName) || state.replicationQueues.has(indexName) || state.compactionsInFlight.has(indexName);
6109
+ }
6110
+ function noteAccess(state, indexName) {
6111
+ state.lastAccessAt.set(indexName, Date.now());
6112
+ if (!copiesAllowed(state)) return;
6113
+ const reloadReason = state.droppedCopies.get(indexName);
6114
+ if (reloadReason !== void 0) {
6115
+ const transition = state.copyTransitions.get(indexName);
6116
+ if (transition !== void 0 && transition.kind !== "drop") return;
6117
+ void scaleOutIndex(state, indexName, reloadReason);
5840
6118
  return;
5841
6119
  }
5842
- if (state.callbacks?.shouldDeferPromotion?.()) return;
5843
- const indexMap = collectEligibleIndexes(state);
5844
- const anticipated = indexMap.get(indexName);
5845
- if (anticipated === void 0) return;
5846
- anticipated.documentCount += incomingCount;
5847
- const result = state.promoter.check(indexMap);
5848
- if (!result.shouldPromote) return;
5849
- state.promotionInProgress = true;
5850
- const run = runPromotion(state, result.reason).catch((err) => {
5851
- const error = toError3(err);
5852
- state.promotionBlocked = isDeterministicFailure(error);
5853
- state.promotionInProgress = false;
5854
- state.callbacks?.onPromotionFailure?.(result.reason, error, !state.promotionBlocked);
5855
- });
5856
- const tracked = run.then(() => {
5857
- state.promotionRun = null;
6120
+ if (indexReadyForCopies(state, indexName)) {
6121
+ void scaleOutIndex(state, indexName, copyThresholdReason(state, indexName));
6122
+ }
6123
+ }
6124
+ async function releaseCopies(state, indexName) {
6125
+ const pool = state.workerPool;
6126
+ if (pool === null) return;
6127
+ await awaitReplicationIdle(state, indexName);
6128
+ const cutoff = Date.now() - state.copyIdleTimeoutMs;
6129
+ if ((state.lastAccessAt.get(indexName) ?? Date.now()) > cutoff) return;
6130
+ if (!state.scaledOutIndexes.has(indexName) || state.compactionsInFlight.has(indexName)) return;
6131
+ state.scaledOutIndexes.delete(indexName);
6132
+ state.droppedCopies.set(indexName, COPY_RELOAD_REASON);
6133
+ cancelIdleMerge(state, indexName);
6134
+ const outcomes = await Promise.allSettled(
6135
+ pool.executorsHolding(indexName).map((worker) => worker.execute({ type: "dropIndex", indexName, requestId: `idle-drop-${indexName}` }))
6136
+ );
6137
+ for (const outcome of outcomes) {
6138
+ if (outcome.status === "rejected") console.warn("Dropping an idle worker copy failed:", outcome.reason);
6139
+ }
6140
+ pool.removeIndex(indexName);
6141
+ state.segmentLedger.delete(indexName);
6142
+ }
6143
+ async function dropIdleCopies(state, indexName) {
6144
+ const previous = state.copyTransitions.get(indexName);
6145
+ if (previous !== void 0) return;
6146
+ const done = releaseCopies(state, indexName).catch((err) => {
6147
+ console.warn("Dropping idle worker copies failed:", err);
6148
+ }).finally(() => {
6149
+ if (state.copyTransitions.get(indexName)?.done === done) state.copyTransitions.delete(indexName);
5858
6150
  });
5859
- state.promotionRun = tracked;
5860
- await tracked;
5861
- }
5862
- async function checkPromotion(state) {
5863
- if (!state.workersEnabled || state.promotionInProgress || state.promotionBlocked || state.workerPool) return;
5864
- if (state.callbacks?.shouldDeferPromotion?.()) return;
5865
- const indexMap = collectEligibleIndexes(state);
5866
- if (indexMap.size === 0) return;
5867
- const result = state.promoter.check(indexMap);
5868
- if (!result.shouldPromote) return;
5869
- state.promotionInProgress = true;
5870
- setTimeout(() => {
5871
- const run = runPromotion(state, result.reason).catch((err) => {
5872
- const error = toError3(err);
5873
- state.promotionBlocked = isDeterministicFailure(error);
5874
- state.promotionInProgress = false;
5875
- state.callbacks?.onPromotionFailure?.(result.reason, error, !state.promotionBlocked);
5876
- });
5877
- state.promotionRun = run.then(() => {
5878
- state.promotionRun = null;
5879
- });
5880
- }, 0);
6151
+ state.copyTransitions.set(indexName, { kind: "drop", done });
6152
+ await done;
6153
+ }
6154
+ async function sweepIdleCopies(state) {
6155
+ const cutoff = Date.now() - state.copyIdleTimeoutMs;
6156
+ for (const indexName of [...state.scaledOutIndexes]) {
6157
+ if ((state.lastAccessAt.get(indexName) ?? Date.now()) > cutoff) continue;
6158
+ if (isIndexBusy(state, indexName)) continue;
6159
+ await dropIdleCopies(state, indexName);
6160
+ }
6161
+ }
6162
+ function startIdleSweep(state) {
6163
+ if (!state.workersEnabled || state.idleSweep !== null) return;
6164
+ const interval = Math.min(MAX_SWEEP_INTERVAL_MS, Math.max(MIN_SWEEP_INTERVAL_MS, state.copyIdleTimeoutMs / 2));
6165
+ const timer = setInterval(() => void sweepIdleCopies(state).catch(() => void 0), interval);
6166
+ if (typeof timer.unref === "function") timer.unref();
6167
+ state.idleSweep = timer;
6168
+ }
6169
+ function stopIdleSweep(state) {
6170
+ if (state.idleSweep === null) return;
6171
+ clearInterval(state.idleSweep);
6172
+ state.idleSweep = null;
5881
6173
  }
5882
6174
 
5883
6175
  // src/engine/orchestration/search.ts
5884
- function partitionsPerWorker(totalPartitions, workerCount) {
5885
- const assignments = Array.from({ length: workerCount }, () => []);
5886
- for (let partitionId = 0; partitionId < totalPartitions; partitionId++) {
5887
- assignments[partitionId % workerCount].push(partitionId);
6176
+ function partitionsPerLease(scope, leaseCount) {
6177
+ const assignments = Array.from({ length: leaseCount }, () => []);
6178
+ for (let at = 0; at < scope.length; at++) {
6179
+ assignments[at % leaseCount].push(scope[at]);
5888
6180
  }
5889
6181
  return assignments;
5890
6182
  }
@@ -5902,67 +6194,76 @@ function mergeWorkerResults(results) {
5902
6194
  facets: workerFacets.length > 0 ? mergeFacets(workerFacets) : void 0
5903
6195
  };
5904
6196
  }
6197
+ function scoresPerPartition(state, indexName, params, globalStats) {
6198
+ const mode = params.scoring ?? state.indexRegistry.get(indexName)?.config.defaultScoring ?? "local";
6199
+ if (mode === "local") return true;
6200
+ return mode === "broadcast" && globalStats !== void 0;
6201
+ }
6202
+ function queryAction(indexName, params, globalStats, partitionIds) {
6203
+ return {
6204
+ type: "query",
6205
+ indexName,
6206
+ params,
6207
+ requestId: createRequestId(),
6208
+ ...partitionIds !== void 0 ? { partitionIds } : {},
6209
+ ...globalStats !== void 0 ? { globalStats } : {}
6210
+ };
6211
+ }
6212
+ async function runSplit(leases, scope, indexName, params, globalStats) {
6213
+ const assignments = partitionsPerLease(scope, leases.length);
6214
+ const results = await Promise.all(
6215
+ assignments.map(
6216
+ (partitionIds, at) => leases[at].executor.execute(queryAction(indexName, params, globalStats, partitionIds)).finally(() => leases[at].release())
6217
+ )
6218
+ );
6219
+ return mergeWorkerResults(results);
6220
+ }
6221
+ function takeMainCopyTurn(state) {
6222
+ if (state.mainCopyTurnTaken) return false;
6223
+ state.mainCopyTurnTaken = true;
6224
+ afterCurrentTurn(() => {
6225
+ state.mainCopyTurnTaken = false;
6226
+ });
6227
+ return true;
6228
+ }
6229
+ function answersFasterOnMainCopy(state, pool, manager) {
6230
+ if (pool.queriesInFlight() > 0) return false;
6231
+ if (manager.countDocuments() > MAIN_COPY_LONE_QUERY_DOCUMENTS) return false;
6232
+ return takeMainCopyTurn(state);
6233
+ }
5905
6234
  async function searchViaWorker(state, indexName, params, globalStats, partitionIds) {
5906
6235
  const pool = state.workerPool;
5907
6236
  if (!pool) return null;
5908
- if (!state.promotedIndexes.has(indexName)) return null;
5909
- if (state.awaitingBufferedWrites.has(indexName)) return null;
6237
+ if (!state.scaledOutIndexes.has(indexName) || state.copyLoadBuffers.has(indexName)) return null;
5910
6238
  const pendingReplication = state.replicationQueues.get(indexName);
5911
6239
  if (pendingReplication !== void 0 && pendingReplication.pendingActions > 0) return null;
5912
6240
  const manager = state.executor.getManager(indexName);
5913
6241
  if (!manager) return null;
5914
- const allExecutors = pool.getAllExecutors();
5915
- if (allExecutors.length === 0) return null;
5916
- const stats = globalStats !== void 0 ? { globalStats } : {};
5917
- if (allExecutors.length === 1) {
5918
- try {
5919
- return await allExecutors[0].execute({
5920
- type: "query",
5921
- indexName,
5922
- params,
5923
- requestId: createRequestId(),
5924
- ...partitionIds !== void 0 ? { partitionIds } : {},
5925
- ...stats
5926
- });
5927
- } catch (err) {
5928
- console.warn("Worker search failed, falling back to local:", err);
5929
- return null;
5930
- }
5931
- }
5932
- const assignments = partitionsPerWorker(manager.partitionCount, allExecutors.length);
5933
- const scopedAssignments = partitionIds === void 0 ? assignments : assignments.map((assigned) => assigned.filter((partitionId) => partitionIds.includes(partitionId)));
5934
- const activeAssignments = [];
5935
- for (let index = 0; index < allExecutors.length; index++) {
5936
- if (scopedAssignments[index].length > 0) {
5937
- activeAssignments.push({ executor: allExecutors[index], partitionIds: scopedAssignments[index] });
5938
- }
5939
- }
5940
- if (activeAssignments.length === 0) {
5941
- return { scored: [], totalMatched: 0 };
5942
- }
6242
+ if (answersFasterOnMainCopy(state, pool, manager)) return null;
6243
+ const scope = partitionIds ?? Array.from({ length: manager.partitionCount }, (_, partitionId) => partitionId);
6244
+ const idle = pool.leaseIdle(scope.length);
6245
+ const leases = [];
5943
6246
  try {
5944
- const results = await Promise.all(
5945
- activeAssignments.map(
5946
- (assignment) => assignment.executor.execute({
5947
- type: "query",
5948
- indexName,
5949
- params,
5950
- requestId: createRequestId(),
5951
- partitionIds: assignment.partitionIds,
5952
- ...stats
5953
- })
5954
- )
5955
- );
5956
- return mergeWorkerResults(results);
6247
+ if (idle.length >= 2 && scoresPerPartition(state, indexName, params, globalStats)) {
6248
+ leases.push(...idle);
6249
+ return await runSplit(leases, scope, indexName, params, globalStats);
6250
+ }
6251
+ for (const lease2 of idle.slice(1)) lease2.release();
6252
+ const lease = idle[0] ?? (takeMainCopyTurn(state) ? null : pool.leaseLeastBusy());
6253
+ if (lease === null) return null;
6254
+ leases.push(lease);
6255
+ return await lease.executor.execute(queryAction(indexName, params, globalStats, partitionIds));
5957
6256
  } catch (err) {
5958
- console.warn("Parallel worker search failed, falling back to local:", err);
6257
+ console.warn("Worker search failed, falling back to local:", err);
5959
6258
  return null;
6259
+ } finally {
6260
+ for (const lease of leases) lease.release();
5960
6261
  }
5961
6262
  }
5962
6263
 
5963
6264
  // src/engine/orchestration/segments.ts
5964
6265
  function segmentBuildConcurrency(state, indexName) {
5965
- if (!state.promotedIndexes.has(indexName)) return 0;
6266
+ if (!state.scaledOutIndexes.has(indexName)) return 0;
5966
6267
  return state.workerPool?.workerCount ?? 0;
5967
6268
  }
5968
6269
  async function buildSegments(state, requests) {
@@ -5979,44 +6280,55 @@ async function buildSegments(state, requests) {
5979
6280
  }
5980
6281
 
5981
6282
  // src/engine/orchestration/index.ts
5982
- function createWorkerOrchestrator(config, executor, promoter, indexRegistry, callbacks) {
6283
+ function workersEnabledByDefault() {
6284
+ return detectRuntime().runtime !== "browser";
6285
+ }
6286
+ function copyIdleTimeoutBeforeClose(closeAfterMs) {
6287
+ return closeAfterMs === void 0 ? DEFAULT_COPY_IDLE_TIMEOUT_MS : Math.min(DEFAULT_COPY_IDLE_TIMEOUT_MS, closeAfterMs);
6288
+ }
6289
+ function createWorkerOrchestrator(config, executor, indexRegistry, callbacks) {
5983
6290
  const state = {
5984
6291
  config,
5985
6292
  executor,
5986
- promoter,
5987
6293
  indexRegistry,
5988
6294
  callbacks,
5989
- workersEnabled: config?.workers?.enabled === true,
6295
+ workersEnabled: config?.workers?.enabled ?? workersEnabledByDefault(),
6296
+ keywordWorkerCount: splitWorkerBudget(resolveWorkerCount(config?.workers?.count)).keyword,
6297
+ copyThreshold: config?.workers?.promotionThreshold ?? DEFAULT_COPY_THRESHOLD,
6298
+ copyIdleTimeoutMs: config?.workers?.idleTimeoutMs ?? copyIdleTimeoutBeforeClose(config?.lifecycle?.idleTimeoutMs),
5990
6299
  bootstrapModule: config?.workers?.bootstrapModule,
5991
- promotionBuffer: [],
5992
- awaitingBufferedWrites: /* @__PURE__ */ new Set(),
5993
6300
  reportedIneligible: /* @__PURE__ */ new Set(),
5994
- promotedIndexes: /* @__PURE__ */ new Set(),
6301
+ scaledOutIndexes: /* @__PURE__ */ new Set(),
6302
+ desyncedIndexes: /* @__PURE__ */ new Set(),
6303
+ copyLoadBuffers: /* @__PURE__ */ new Map(),
6304
+ copyTransitions: /* @__PURE__ */ new Map(),
6305
+ droppedCopies: /* @__PURE__ */ new Map(),
6306
+ lastAccessAt: /* @__PURE__ */ new Map(),
6307
+ copyReloadCounts: /* @__PURE__ */ new Map(),
5995
6308
  replicationQueues: /* @__PURE__ */ new Map(),
5996
6309
  segmentLedger: /* @__PURE__ */ new Map(),
5997
6310
  compactionsInFlight: /* @__PURE__ */ new Map(),
6311
+ idleMergeTimers: /* @__PURE__ */ new Map(),
5998
6312
  workerPool: null,
5999
- promotionInProgress: false,
6000
- promotionBlocked: false,
6001
- promotionRun: null
6313
+ poolStart: null,
6314
+ poolRetryAt: 0,
6315
+ poolRetryDelayMs: POOL_RESTART_DELAY_MS,
6316
+ poolRepair: null,
6317
+ mainCopyTurnTaken: false,
6318
+ repairTimer: null,
6319
+ scaleOutBlocked: false,
6320
+ idleSweep: null
6002
6321
  };
6003
- async function resyncIndex(indexName, wasPromoted) {
6004
- if (state.promotionRun) {
6005
- await state.promotionRun.catch(() => void 0);
6006
- }
6007
- const pool = state.workerPool;
6008
- if (!pool) return;
6009
- if (!wasPromoted && !state.promotedIndexes.has(indexName)) return;
6010
- const entry = indexRegistry.get(indexName);
6011
- if (!entry) return;
6012
- if (workerIneligibility(indexName, entry.config, state.bootstrapModule)) return;
6013
- const manager = executor.getManager(indexName);
6014
- if (!manager) return;
6015
- state.promotedIndexes.delete(indexName);
6016
- await awaitReplicationIdle(state, indexName);
6017
- state.segmentLedger.delete(indexName);
6018
- await transferIndexToPool(indexName, pool, entry.config, manager);
6019
- state.promotedIndexes.add(indexName);
6322
+ startIdleSweep(state);
6323
+ async function resyncIndex(indexName, wasScaledOut) {
6324
+ state.desyncedIndexes.delete(indexName);
6325
+ if (!wasScaledOut) return;
6326
+ await scaleOutIndex(state, indexName, copyThresholdReason(state, indexName));
6327
+ }
6328
+ async function openIndex(indexName) {
6329
+ state.droppedCopies.delete(indexName);
6330
+ if (!indexReadyForCopies(state, indexName)) return;
6331
+ await scaleOutIndex(state, indexName, copyThresholdReason(state, indexName));
6020
6332
  }
6021
6333
  async function getWorkerMemoryStats() {
6022
6334
  if (!state.workerPool) return [];
@@ -6028,93 +6340,222 @@ function createWorkerOrchestrator(config, executor, promoter, indexRegistry, cal
6028
6340
  external: report.external
6029
6341
  }));
6030
6342
  }
6343
+ function workerCopies() {
6344
+ const reports = [];
6345
+ for (const indexName of indexRegistry.keys()) {
6346
+ reports.push({
6347
+ indexName,
6348
+ scaledOut: state.scaledOutIndexes.has(indexName),
6349
+ reloadCount: state.copyReloadCounts.get(indexName) ?? 0
6350
+ });
6351
+ }
6352
+ return reports;
6353
+ }
6031
6354
  async function shutdown() {
6355
+ stopIdleSweep(state);
6356
+ cancelRepair(state);
6357
+ for (const indexName of [...state.idleMergeTimers.keys()]) cancelIdleMerge(state, indexName);
6358
+ await Promise.allSettled([...state.copyTransitions.values()].map((transition) => transition.done));
6359
+ if (state.poolRepair !== null) await state.poolRepair;
6360
+ if (state.poolStart !== null) await state.poolStart.catch(() => void 0);
6032
6361
  if (state.workerPool) {
6033
6362
  await awaitCompactions(state);
6034
6363
  await awaitReplicationIdle(state);
6035
6364
  await state.workerPool.shutdown();
6036
6365
  state.workerPool = null;
6037
- state.promotedIndexes.clear();
6366
+ state.scaledOutIndexes.clear();
6038
6367
  state.replicationQueues.clear();
6039
6368
  state.segmentLedger.clear();
6040
6369
  }
6041
6370
  }
6042
- async function awaitPromotionIdle() {
6043
- while (state.promotionInProgress) {
6044
- const run = state.promotionRun;
6045
- if (run !== null) {
6046
- await run;
6047
- } else {
6048
- await new Promise((resolve) => setTimeout(resolve, 0));
6371
+ async function restoreAfterFailedClose(indexName, failure) {
6372
+ const pool = state.workerPool;
6373
+ const entry = indexRegistry.get(indexName);
6374
+ const manager = executor.getManager(indexName);
6375
+ if (pool !== null && entry !== void 0 && manager !== void 0) {
6376
+ try {
6377
+ await transferIndexToPool(indexName, pool, entry.config, manager);
6378
+ state.scaledOutIndexes.add(indexName);
6379
+ } catch (repairError) {
6380
+ throw new NarsilError(
6381
+ ErrorCodes.WORKER_CRASHED,
6382
+ `Worker copies for index "${indexName}" could not be restored after close failed`,
6383
+ {
6384
+ indexName,
6385
+ closeError: failure.reason instanceof Error ? failure.reason.message : String(failure.reason),
6386
+ repairError: repairError instanceof Error ? repairError.message : String(repairError)
6387
+ }
6388
+ );
6049
6389
  }
6050
6390
  }
6391
+ if (failure.reason instanceof NarsilError) throw failure.reason;
6392
+ throw new NarsilError(ErrorCodes.WORKER_CRASHED, `A worker could not close index "${indexName}"`, {
6393
+ indexName,
6394
+ cause: failure.reason instanceof Error ? failure.reason.message : String(failure.reason)
6395
+ });
6051
6396
  }
6052
6397
  async function closeIndex(indexName) {
6053
- await awaitPromotionIdle();
6398
+ if (state.poolRepair !== null) await state.poolRepair;
6399
+ const transition = state.copyTransitions.get(indexName);
6400
+ if (transition !== void 0) await transition.done;
6054
6401
  await awaitReplicationIdle(state, indexName);
6055
6402
  await awaitCompactions(state);
6403
+ cancelIdleMerge(state, indexName);
6056
6404
  const pool = state.workerPool;
6057
- if (pool !== null && state.promotedIndexes.has(indexName)) {
6405
+ if (pool !== null && state.scaledOutIndexes.has(indexName)) {
6406
+ state.scaledOutIndexes.delete(indexName);
6058
6407
  const outcomes = await Promise.allSettled(
6059
- pool.getAllExecutors().map((worker) => worker.execute({ type: "dropIndex", indexName, requestId: `close-${indexName}` }))
6408
+ pool.executorsHolding(indexName).map((worker) => worker.execute({ type: "dropIndex", indexName, requestId: `close-${indexName}` }))
6060
6409
  );
6061
6410
  const failure = outcomes.find((outcome) => outcome.status === "rejected");
6062
- if (failure?.status === "rejected") {
6063
- const entry = indexRegistry.get(indexName);
6064
- const manager = executor.getManager(indexName);
6065
- if (entry !== void 0 && manager !== void 0) {
6066
- try {
6067
- await transferIndexToPool(indexName, pool, entry.config, manager);
6068
- } catch (repairError) {
6069
- throw new NarsilError(
6070
- ErrorCodes.WORKER_CRASHED,
6071
- `Worker copies for index "${indexName}" could not be restored after close failed`,
6072
- {
6073
- indexName,
6074
- closeError: failure.reason instanceof Error ? failure.reason.message : String(failure.reason),
6075
- repairError: repairError instanceof Error ? repairError.message : String(repairError)
6076
- }
6077
- );
6078
- }
6079
- }
6080
- if (failure.reason instanceof NarsilError) throw failure.reason;
6081
- throw new NarsilError(ErrorCodes.WORKER_CRASHED, `A worker could not close index "${indexName}"`, {
6082
- indexName,
6083
- cause: failure.reason instanceof Error ? failure.reason.message : String(failure.reason)
6084
- });
6085
- }
6411
+ if (failure?.status === "rejected") await restoreAfterFailedClose(indexName, failure);
6086
6412
  pool.removeIndex(indexName);
6087
6413
  }
6088
- state.promotedIndexes.delete(indexName);
6414
+ state.droppedCopies.delete(indexName);
6415
+ state.desyncedIndexes.delete(indexName);
6416
+ state.lastAccessAt.delete(indexName);
6089
6417
  state.replicationQueues.delete(indexName);
6090
6418
  state.segmentLedger.delete(indexName);
6091
6419
  }
6092
6420
  async function replicate(action) {
6093
6421
  await replicateToWorkers(state, action);
6094
- if (action.type === "attachSegments") {
6095
- maybeCompactSegments(state, action.indexName);
6422
+ if (!("indexName" in action)) return;
6423
+ if (action.type === "attachSegments") maybeCompactSegments(state, action.indexName);
6424
+ if (action.type === "insert" || action.type === "update") flushGrownTails(state, action.indexName);
6425
+ if (state.scaledOutIndexes.has(action.indexName) || state.copyLoadBuffers.has(action.indexName)) {
6426
+ scheduleIdleMerge(state, action.indexName);
6096
6427
  }
6097
6428
  }
6098
6429
  return {
6099
- checkPromotion: () => checkPromotion(state),
6100
- promoteBeforeBatch: (indexName, incomingCount) => promoteBeforeBatch(state, indexName, incomingCount),
6430
+ scaleOutReadyIndexes: () => scaleOutReadyIndexes(state),
6431
+ scaleOutBeforeBatch: (indexName, incomingCount) => scaleOutBeforeBatch(state, indexName, incomingCount),
6101
6432
  replicateToWorkers: replicate,
6102
6433
  awaitReplication: (indexName) => awaitReplicationIdle(state, indexName),
6103
6434
  awaitCompactions: () => awaitCompactions(state),
6104
- openIndex: (indexName) => resyncIndex(indexName, true),
6435
+ openIndex,
6105
6436
  closeIndex,
6106
- isIndexBusy: (indexName) => state.promotionInProgress || state.replicationQueues.has(indexName) || state.compactionsInFlight.has(indexName),
6437
+ isIndexBusy: (indexName) => isIndexBusy(state, indexName),
6107
6438
  buildSegments: (requests) => buildSegments(state, requests),
6108
6439
  segmentBuildConcurrency: (indexName) => segmentBuildConcurrency(state, indexName),
6109
6440
  searchViaWorker: (indexName, params, globalStats, partitionIds) => searchViaWorker(state, indexName, params, globalStats, partitionIds),
6110
- isPromoted: () => state.workerPool !== null,
6111
- desyncIndex: (indexName) => state.promotedIndexes.delete(indexName),
6441
+ hasWorkerPool: () => state.workerPool !== null,
6442
+ desyncIndex: (indexName) => {
6443
+ state.desyncedIndexes.add(indexName);
6444
+ return state.scaledOutIndexes.delete(indexName);
6445
+ },
6112
6446
  resyncIndex,
6447
+ noteAccess: (indexName) => noteAccess(state, indexName),
6448
+ workerCopies,
6113
6449
  getWorkerMemoryStats,
6114
6450
  shutdown
6115
6451
  };
6116
6452
  }
6117
6453
 
6454
+ // src/engine/validation.ts
6455
+ function now() {
6456
+ if (typeof performance !== "undefined" && typeof performance.now === "function") {
6457
+ return performance.now();
6458
+ }
6459
+ return Date.now();
6460
+ }
6461
+ function validateIndexName(name) {
6462
+ if (!name || name.length === 0) {
6463
+ throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, "Index name must not be empty", { indexName: name });
6464
+ }
6465
+ if (name.length > MAX_INDEX_NAME_LENGTH) {
6466
+ throw new NarsilError(
6467
+ ErrorCodes.INDEX_NOT_FOUND,
6468
+ `Index name must not exceed ${MAX_INDEX_NAME_LENGTH} characters`,
6469
+ {
6470
+ indexName: name,
6471
+ length: name.length
6472
+ }
6473
+ );
6474
+ }
6475
+ if (!INDEX_NAME_PATTERN.test(name)) {
6476
+ throw new NarsilError(
6477
+ ErrorCodes.INDEX_NOT_FOUND,
6478
+ `Index name "${name}" contains invalid characters; use alphanumeric, dots, hyphens, and underscores only`,
6479
+ { indexName: name }
6480
+ );
6481
+ }
6482
+ if (name.includes("..")) {
6483
+ throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, `Index name "${name}" must not contain ".."`, { indexName: name });
6484
+ }
6485
+ }
6486
+ function validateDocId(docId) {
6487
+ if (!docId || docId.length === 0) {
6488
+ throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Document ID must not be empty", { docId });
6489
+ }
6490
+ if (docId.length > MAX_DOC_ID_LENGTH) {
6491
+ throw new NarsilError(
6492
+ ErrorCodes.DOC_VALIDATION_FAILED,
6493
+ `Document ID must not exceed ${MAX_DOC_ID_LENGTH} characters`,
6494
+ {
6495
+ docId,
6496
+ length: docId.length
6497
+ }
6498
+ );
6499
+ }
6500
+ if (docId.includes("\0")) {
6501
+ throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Document ID must not contain null bytes", { docId });
6502
+ }
6503
+ }
6504
+ function validatePartitionConfig(partitions) {
6505
+ const { maxDocsPerPartition, maxPartitions, watermark } = partitions;
6506
+ if (maxDocsPerPartition !== void 0 && (!Number.isInteger(maxDocsPerPartition) || maxDocsPerPartition < 1)) {
6507
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, "partitions.maxDocsPerPartition must be a positive integer", {
6508
+ maxDocsPerPartition
6509
+ });
6510
+ }
6511
+ if (maxPartitions !== void 0 && (!Number.isInteger(maxPartitions) || maxPartitions < 1)) {
6512
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, "partitions.maxPartitions must be a positive integer", {
6513
+ maxPartitions
6514
+ });
6515
+ }
6516
+ if (watermark !== void 0 && (typeof watermark !== "number" || !(watermark > 0) || watermark > 1)) {
6517
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, "partitions.watermark must be above 0 and at most 1", {
6518
+ watermark
6519
+ });
6520
+ }
6521
+ }
6522
+ function requirePositiveSafeInteger(field, value) {
6523
+ if (value === void 0) return;
6524
+ if (!Number.isSafeInteger(value) || value < 1) {
6525
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, `${field} must be a positive safe integer`, { field, value });
6526
+ }
6527
+ }
6528
+ function validateWorkerConfig(workers, lifecycle) {
6529
+ if (workers === void 0) return;
6530
+ requirePositiveSafeInteger("workers.count", workers.count);
6531
+ requirePositiveSafeInteger("workers.promotionThreshold", workers.promotionThreshold);
6532
+ requirePositiveSafeInteger("workers.idleTimeoutMs", workers.idleTimeoutMs);
6533
+ const closeAfter = lifecycle?.idleTimeoutMs;
6534
+ if (workers.idleTimeoutMs !== void 0 && closeAfter !== void 0 && workers.idleTimeoutMs > closeAfter) {
6535
+ throw new NarsilError(
6536
+ ErrorCodes.CONFIG_INVALID,
6537
+ "workers.idleTimeoutMs must be at most lifecycle.idleTimeoutMs, so that an index drops its copies before it closes",
6538
+ { workersIdleTimeoutMs: workers.idleTimeoutMs, lifecycleIdleTimeoutMs: closeAfter }
6539
+ );
6540
+ }
6541
+ }
6542
+ function validateBM25Params(bm25) {
6543
+ if (bm25 === void 0) return;
6544
+ const { k1, b } = bm25;
6545
+ if (k1 !== void 0 && (!Number.isFinite(k1) || k1 < 0)) {
6546
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, "bm25.k1 must be a finite number at or above 0", { k1 });
6547
+ }
6548
+ if (b !== void 0 && (!Number.isFinite(b) || b < 0 || b > 1)) {
6549
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, "bm25.b must be between 0 and 1", { b });
6550
+ }
6551
+ }
6552
+ function clampLimit(limit) {
6553
+ return clampRowCount(limit, DEFAULT_LIMIT);
6554
+ }
6555
+ function clampOffset(offset) {
6556
+ return clampRowCount(offset, DEFAULT_OFFSET);
6557
+ }
6558
+
6118
6559
  // src/engine/vector-fields.ts
6119
6560
  function getVectorFieldPaths(schema) {
6120
6561
  return new Set(extractVectorFieldsFromSchema(schema).keys());
@@ -6156,10 +6597,13 @@ function createWatermarkNotifier(deps) {
6156
6597
 
6157
6598
  // src/engine/core.ts
6158
6599
  function createEngineCore(config, hooks) {
6159
- const executor = createDirectExecutor();
6160
- const promoter = createExecutionPromoter({
6161
- perIndexThreshold: config?.workers?.promotionThreshold,
6162
- totalThreshold: config?.workers?.totalPromotionThreshold
6600
+ validateWorkerConfig(config?.workers, config?.lifecycle);
6601
+ const vectorWorkerCount = splitWorkerBudget(resolveWorkerCount(config?.workers?.count)).vector;
6602
+ const executor = createDirectExecutor({
6603
+ vectorWorkerCopies: {
6604
+ enabled: (config?.workers?.enabled ?? workersEnabledByDefault()) && vectorWorkerCount > 0,
6605
+ count: vectorWorkerCount
6606
+ }
6163
6607
  });
6164
6608
  const pluginRegistry = createPluginRegistry();
6165
6609
  if (config?.plugins) {
@@ -6178,11 +6622,11 @@ function createEngineCore(config, hooks) {
6178
6622
  const shutdownState = { isShutdown: false };
6179
6623
  const abortController = new AbortController();
6180
6624
  const rebalancingIndexes = /* @__PURE__ */ new Set();
6181
- const orchestrator = createWorkerOrchestrator(config, executor, promoter, indexRegistry, {
6182
- shouldDeferPromotion() {
6625
+ const orchestrator = createWorkerOrchestrator(config, executor, indexRegistry, {
6626
+ shouldDeferCopies() {
6183
6627
  return rebalancingIndexes.size > 0;
6184
6628
  },
6185
- onPromotion(workerCount, reason) {
6629
+ onCopiesLoaded(workerCount, reason) {
6186
6630
  const handlers = eventHandlers.get("workerPromote");
6187
6631
  if (handlers) {
6188
6632
  for (const handler of handlers) handler({ workerCount, reason });
@@ -6191,10 +6635,10 @@ function createEngineCore(config, hooks) {
6191
6635
  console.warn("onWorkerPromote plugin hook failed:", err instanceof Error ? err.message : String(err));
6192
6636
  });
6193
6637
  },
6194
- onPromotionFailure(reason, error, retryable) {
6638
+ onCopyLoadFailure(reason, error, retryable) {
6195
6639
  const handlers = eventHandlers.get("workerPromoteFailure");
6196
6640
  if (!handlers || handlers.size === 0) {
6197
- console.warn(`Worker promotion failed (${reason}):`, error);
6641
+ console.warn(`Loading worker copies failed (${reason}):`, error);
6198
6642
  return;
6199
6643
  }
6200
6644
  for (const handler of handlers) handler({ reason, error, retryable });
@@ -6383,7 +6827,8 @@ function createEngineCore(config, hooks) {
6383
6827
  rebalancingIndexes,
6384
6828
  requireManager,
6385
6829
  onClose: hooks?.onIndexClose,
6386
- onOpen: hooks?.onIndexOpen
6830
+ onOpen: hooks?.onIndexOpen,
6831
+ onAccess: (indexName) => orchestrator.noteAccess(indexName)
6387
6832
  });
6388
6833
  const mutationCtx = {
6389
6834
  executor,
@@ -6417,7 +6862,6 @@ function createEngineCore(config, hooks) {
6417
6862
  };
6418
6863
  return {
6419
6864
  executor,
6420
- promoter,
6421
6865
  pluginRegistry,
6422
6866
  durability,
6423
6867
  invalidation,
@@ -6444,96 +6888,6 @@ function createEngineCore(config, hooks) {
6444
6888
  };
6445
6889
  }
6446
6890
 
6447
- // src/engine/validation.ts
6448
- var INDEX_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
6449
- var MAX_DOC_ID_LENGTH = 512;
6450
- var BATCH_CHUNK_SIZE = 1e3;
6451
- var DEFAULT_LIMIT = DEFAULT_PAGE_SIZE;
6452
- var DEFAULT_OFFSET = 0;
6453
- function now() {
6454
- if (typeof performance !== "undefined" && typeof performance.now === "function") {
6455
- return performance.now();
6456
- }
6457
- return Date.now();
6458
- }
6459
- function validateIndexName(name) {
6460
- if (!name || name.length === 0) {
6461
- throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, "Index name must not be empty", { indexName: name });
6462
- }
6463
- if (name.length > MAX_INDEX_NAME_LENGTH) {
6464
- throw new NarsilError(
6465
- ErrorCodes.INDEX_NOT_FOUND,
6466
- `Index name must not exceed ${MAX_INDEX_NAME_LENGTH} characters`,
6467
- {
6468
- indexName: name,
6469
- length: name.length
6470
- }
6471
- );
6472
- }
6473
- if (!INDEX_NAME_PATTERN.test(name)) {
6474
- throw new NarsilError(
6475
- ErrorCodes.INDEX_NOT_FOUND,
6476
- `Index name "${name}" contains invalid characters; use alphanumeric, dots, hyphens, and underscores only`,
6477
- { indexName: name }
6478
- );
6479
- }
6480
- if (name.includes("..")) {
6481
- throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, `Index name "${name}" must not contain ".."`, { indexName: name });
6482
- }
6483
- }
6484
- function validateDocId(docId) {
6485
- if (!docId || docId.length === 0) {
6486
- throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Document ID must not be empty", { docId });
6487
- }
6488
- if (docId.length > MAX_DOC_ID_LENGTH) {
6489
- throw new NarsilError(
6490
- ErrorCodes.DOC_VALIDATION_FAILED,
6491
- `Document ID must not exceed ${MAX_DOC_ID_LENGTH} characters`,
6492
- {
6493
- docId,
6494
- length: docId.length
6495
- }
6496
- );
6497
- }
6498
- if (docId.includes("\0")) {
6499
- throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Document ID must not contain null bytes", { docId });
6500
- }
6501
- }
6502
- function validatePartitionConfig(partitions) {
6503
- const { maxDocsPerPartition, maxPartitions, watermark } = partitions;
6504
- if (maxDocsPerPartition !== void 0 && (!Number.isInteger(maxDocsPerPartition) || maxDocsPerPartition < 1)) {
6505
- throw new NarsilError(ErrorCodes.CONFIG_INVALID, "partitions.maxDocsPerPartition must be a positive integer", {
6506
- maxDocsPerPartition
6507
- });
6508
- }
6509
- if (maxPartitions !== void 0 && (!Number.isInteger(maxPartitions) || maxPartitions < 1)) {
6510
- throw new NarsilError(ErrorCodes.CONFIG_INVALID, "partitions.maxPartitions must be a positive integer", {
6511
- maxPartitions
6512
- });
6513
- }
6514
- if (watermark !== void 0 && (typeof watermark !== "number" || !(watermark > 0) || watermark > 1)) {
6515
- throw new NarsilError(ErrorCodes.CONFIG_INVALID, "partitions.watermark must be above 0 and at most 1", {
6516
- watermark
6517
- });
6518
- }
6519
- }
6520
- function validateBM25Params(bm25) {
6521
- if (bm25 === void 0) return;
6522
- const { k1, b } = bm25;
6523
- if (k1 !== void 0 && (!Number.isFinite(k1) || k1 < 0)) {
6524
- throw new NarsilError(ErrorCodes.CONFIG_INVALID, "bm25.k1 must be a finite number at or above 0", { k1 });
6525
- }
6526
- if (b !== void 0 && (!Number.isFinite(b) || b < 0 || b > 1)) {
6527
- throw new NarsilError(ErrorCodes.CONFIG_INVALID, "bm25.b must be between 0 and 1", { b });
6528
- }
6529
- }
6530
- function clampLimit(limit) {
6531
- return clampRowCount(limit, DEFAULT_LIMIT);
6532
- }
6533
- function clampOffset(offset) {
6534
- return clampRowCount(offset, DEFAULT_OFFSET);
6535
- }
6536
-
6537
6891
  // src/engine/index-lifecycle.ts
6538
6892
  async function createEngineIndex(core, config, name, indexConfig, indexUuid) {
6539
6893
  core.guardShutdown();
@@ -7144,7 +7498,7 @@ async function insertDocument(ctx, indexName, document, docId, options) {
7144
7498
  }
7145
7499
  }
7146
7500
  ctx.checkWatermark(indexName);
7147
- await ctx.orchestrator.checkPromotion();
7501
+ await ctx.orchestrator.scaleOutReadyIndexes();
7148
7502
  return resolvedDocId;
7149
7503
  }
7150
7504
 
@@ -7298,7 +7652,6 @@ async function applyAdmittedDocuments(ctx, indexName, admitted, options, failed)
7298
7652
  }
7299
7653
 
7300
7654
  // src/engine/mutations/segment-replication.ts
7301
- var MIN_DOCUMENTS_FOR_SEGMENTS = 64;
7302
7655
  function shardCount(documentCount, workers) {
7303
7656
  if (workers <= 1) return 1;
7304
7657
  return Math.max(1, Math.min(workers, Math.ceil(documentCount / MIN_DOCUMENTS_FOR_SEGMENTS)));
@@ -7558,7 +7911,7 @@ async function insertBatchViaSegments(ctx, indexName, documents, options) {
7558
7911
  vecIndexes.get(fieldPath)?.scheduleBuild();
7559
7912
  }
7560
7913
  ctx.checkWatermark(indexName);
7561
- await ctx.orchestrator.checkPromotion();
7914
+ await ctx.orchestrator.scaleOutReadyIndexes();
7562
7915
  return { succeeded: outcome.succeeded, failed };
7563
7916
  }
7564
7917
 
@@ -7566,7 +7919,7 @@ async function insertBatchViaSegments(ctx, indexName, documents, options) {
7566
7919
  async function insertDocumentBatch(ctx, indexName, documents, options) {
7567
7920
  ctx.guardShutdown();
7568
7921
  const entry = ctx.requireIndex(indexName);
7569
- await ctx.orchestrator.promoteBeforeBatch(indexName, documents.length);
7922
+ await ctx.orchestrator.scaleOutBeforeBatch(indexName, documents.length);
7570
7923
  if (documents.length >= MIN_DOCUMENTS_FOR_SEGMENTS && !ctx.isRebalancing(indexName) && ctx.orchestrator.segmentBuildConcurrency(indexName) > 0) {
7571
7924
  return insertBatchViaSegments(ctx, indexName, documents, options);
7572
7925
  }
@@ -7720,7 +8073,7 @@ async function insertDocumentBatch(ctx, indexName, documents, options) {
7720
8073
  }
7721
8074
  }
7722
8075
  ctx.checkWatermark(indexName);
7723
- await ctx.orchestrator.checkPromotion();
8076
+ await ctx.orchestrator.scaleOutReadyIndexes();
7724
8077
  return { succeeded, failed };
7725
8078
  }
7726
8079
 
@@ -7995,7 +8348,7 @@ async function updateDocumentBatch(ctx, indexName, updates) {
7995
8348
  }
7996
8349
 
7997
8350
  // src/partitioning/write-ahead-queue.ts
7998
- function createWriteAheadQueue(maxSize = 1e4) {
8351
+ function createWriteAheadQueue(maxSize = DEFAULT_WRITE_AHEAD_QUEUE_MAX_SIZE) {
7999
8352
  const entries = [];
8000
8353
  const docStates = /* @__PURE__ */ new Map();
8001
8354
  let nextSequence = 1;
@@ -9830,11 +10183,6 @@ async function resolveVectorText(params, embeddingAdapter, signal, embeddingAdap
9830
10183
  }
9831
10184
 
9832
10185
  // src/engine/suggest.ts
9833
- var DEFAULT_SUGGEST_LIMIT = 10;
9834
- var MAX_SUGGEST_LIMIT = 100;
9835
- var SUGGEST_OVERSAMPLE_FACTOR = 1.5;
9836
- var SUGGEST_OVERSAMPLE_PADDING = 10;
9837
- var MAX_SUGGEST_SCATTER_LIMIT = Math.ceil(MAX_SUGGEST_LIMIT * SUGGEST_OVERSAMPLE_FACTOR) + SUGGEST_OVERSAMPLE_PADDING;
9838
10186
  function executeSuggest(manager, language, params, partitionIds) {
9839
10187
  const t0 = performance.now();
9840
10188
  const ceiling = partitionIds === void 0 ? MAX_SUGGEST_LIMIT : MAX_SUGGEST_SCATTER_LIMIT;
@@ -9912,7 +10260,7 @@ async function runEngineQuery(core, indexName, params, options) {
9912
10260
  entry.embeddingAdapterName
9913
10261
  );
9914
10262
  await core.pluginRegistry.runHook("beforeSearch", { indexName, params: resolvedParams });
9915
- const workerSearch = core.orchestrator.isPromoted() ? core.orchestrator.searchViaWorker.bind(core.orchestrator) : void 0;
10263
+ const workerSearch = core.orchestrator.hasWorkerPool() ? core.orchestrator.searchViaWorker.bind(core.orchestrator) : void 0;
9916
10264
  const result = await executeQuery(resolvedParams, {
9917
10265
  manager,
9918
10266
  language: entry.language,
@@ -9952,7 +10300,7 @@ async function runEnginePreflight(core, indexName, params, options) {
9952
10300
  core.abortController.signal,
9953
10301
  entry.embeddingAdapterName
9954
10302
  );
9955
- const workerSearch = core.orchestrator.isPromoted() ? core.orchestrator.searchViaWorker.bind(core.orchestrator) : void 0;
10303
+ const workerSearch = core.orchestrator.hasWorkerPool() ? core.orchestrator.searchViaWorker.bind(core.orchestrator) : void 0;
9956
10304
  const result = await executePreflight(resolvedParams, {
9957
10305
  manager,
9958
10306
  language: entry.language,
@@ -10268,6 +10616,7 @@ function createNarsilFromCore(core, config) {
10268
10616
  openIndexCount: lifecycleCounts.open,
10269
10617
  closedIndexCount: lifecycleCounts.closed,
10270
10618
  reopenCount: lifecycleCounts.reopens,
10619
+ workerCopies: orchestrator.workerCopies(),
10271
10620
  workers
10272
10621
  };
10273
10622
  },
@@ -10332,4 +10681,4 @@ async function createNarsil(config) {
10332
10681
  // src/index.ts
10333
10682
  var VERSION = "0.1.0";
10334
10683
 
10335
- export { DEFAULT_SUGGEST_LIMIT, MAX_SUGGEST_LIMIT, MAX_SUGGEST_SCATTER_LIMIT, SUGGEST_OVERSAMPLE_FACTOR, SUGGEST_OVERSAMPLE_PADDING, VERSION, WIRE_BATCH_BUDGET, applyDeleteEntry, applyIndexEntry, chunkByBudget, clampLimit, computeEntryChecksum, createAckMessage, createDurableDirectory, createEngineCore, createEngineIndex, createEntryBatchMessage, createEntryMessage, createForwardBatchMessage, createForwardBatchResultMessage, createForwardMessage, createInsyncAddMessage, createInsyncConfirmMessage, createInsyncRemoveMessage, createNarsil, createNarsilFromCore, dedupePinnedEntries, foldGroupReducer, listBindingOf, now, queryBindingOf, runEngineListDocuments, runEnginePreflight, runEngineQuery, runEngineQueryStats, runEngineSuggest, validateAckPayload, validateEntryBatchPayload, validateEntryPayload, validateForwardBatchPayload, validateForwardBatchResultPayload, validateInsyncConfirmPayload, validateReplicationEntry, writeSegmentedCheckpoint };
10684
+ export { VERSION, applyDeleteEntry, applyIndexEntry, clampLimit, computeEntryChecksum, createAckMessage, createDurableDirectory, createEngineCore, createEngineIndex, createEntryBatchMessage, createEntryMessage, createForwardBatchMessage, createForwardBatchResultMessage, createForwardMessage, createInsyncAddMessage, createInsyncConfirmMessage, createInsyncRemoveMessage, createNarsil, createNarsilFromCore, dedupePinnedEntries, foldGroupReducer, listBindingOf, now, queryBindingOf, runEngineListDocuments, runEnginePreflight, runEngineQuery, runEngineQueryStats, runEngineSuggest, validateAckPayload, validateEntryBatchPayload, validateEntryPayload, validateForwardBatchPayload, validateForwardBatchResultPayload, validateInsyncConfirmPayload, validateReplicationEntry, writeSegmentedCheckpoint };