@delali/narsil 0.2.3-next.35 → 0.2.3

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 (39) hide show
  1. package/dist/adapters/filesystem.mjs +1 -1
  2. package/dist/adapters/indexeddb.mjs +1 -1
  3. package/dist/{chunk-EMI63EXP.mjs → chunk-GB2IIJJG.mjs} +2 -0
  4. package/dist/{chunk-RO4EFWE6.mjs → chunk-GRWFY2HI.mjs} +10 -1
  5. package/dist/{chunk-36ER4WCG.mjs → chunk-JQUL2T6I.mjs} +2 -1
  6. package/dist/{chunk-OOA7OBLA.mjs → chunk-MMSWEEZT.mjs} +69 -163
  7. package/dist/{chunk-GODECMDT.mjs → chunk-PM52SMBT.mjs} +1 -1
  8. package/dist/chunk-WU2UZEVD.mjs +137 -0
  9. package/dist/{client-DAJ_UHOa.d.ts → client-BobVnoIK.d.ts} +2 -2
  10. package/dist/client.d.ts +8 -4
  11. package/dist/client.mjs +2 -0
  12. package/dist/{config-CzTJJili.d.ts → config-Byyl1QqH.d.ts} +2 -2
  13. package/dist/config-fLC4lbmg.d.ts +194 -0
  14. package/dist/{direct-executor-YARCDMJF.mjs → direct-executor-MSJEVPZU.mjs} +2 -2
  15. package/dist/distribution/coordinator/etcd.d.ts +5 -7
  16. package/dist/distribution/coordinator/etcd.mjs +27 -8
  17. package/dist/distribution/coordinator/in-memory.d.ts +2 -2
  18. package/dist/distribution/transport/grpc.mjs +3 -3
  19. package/dist/distribution/transport/in-memory.mjs +1 -1
  20. package/dist/distribution/transport/tcp.mjs +2 -2
  21. package/dist/distribution.d.ts +10 -132
  22. package/dist/distribution.mjs +1089 -339
  23. package/dist/embeddings/openai.mjs +1 -1
  24. package/dist/{engine-DEtRLYpB.d.ts → engine-BE45D5ZG.d.ts} +3 -3
  25. package/dist/index.browser.mjs +71 -23
  26. package/dist/index.d.ts +7 -7
  27. package/dist/index.mjs +5 -4
  28. package/dist/{options-BORJg5XB.d.ts → options-EXYFcem-.d.ts} +9 -0
  29. package/dist/persistence/durability/checkpoint-worker.d.ts +3 -1
  30. package/dist/persistence/durability/checkpoint-worker.mjs +5 -4
  31. package/dist/react.d.ts +4 -4
  32. package/dist/react.mjs +2 -0
  33. package/dist/{schema-emTQfEiX.d.ts → schema-ZRbk8BgH.d.ts} +1 -0
  34. package/dist/{search-Dt4dkzpf.d.ts → search-DHuXwJYk.d.ts} +5 -1
  35. package/dist/server.d.ts +9 -5
  36. package/dist/server.mjs +101 -7
  37. package/dist/{types-BhdINMtE.d.ts → types-B5Tsaalz.d.ts} +19 -2
  38. package/dist/workers/entry.mjs +2 -2
  39. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
- import { createEngineCore, createNarsilFromCore, runEngineQueryStats, runEngineListDocuments, runEngineSuggest, runEnginePreflight, runEngineQuery, createEngineIndex, applyIndexEntry, applyDeleteEntry, DEFAULT_SUGGEST_LIMIT, MAX_SUGGEST_LIMIT, now, validateIndexName, indexConfigKey, putIndexMetadata, getIndexMetadata, validateEntryBatchPayload, createAckMessage, validateEntryPayload, validateForwardBatchPayload, createForwardBatchResultMessage, MAX_PARTITION_COUNT, clampLimit, createForwardBatchMessage, validateForwardBatchResultPayload, createForwardMessage, validateReplicationEntry, MAX_SUGGEST_SCATTER_LIMIT, chunkByBudget, WIRE_BATCH_BUDGET, MAX_INDEX_NAME_LENGTH, SUGGEST_OVERSAMPLE_FACTOR, SUGGEST_OVERSAMPLE_PADDING, createEntryMessage, computeEntryChecksum, createEntryBatchMessage, createInsyncConfirmMessage, validateAckPayload, createInsyncRemoveMessage, validateInsyncConfirmPayload, createInsyncAddMessage } from './chunk-OOA7OBLA.mjs';
2
- import { ReplicationMessageTypes, TransportErrorCodes, ClusterMessageTypes, QueryMessageTypes, TransportError, MAX_MESSAGE_SIZE_BYTES } from './chunk-36ER4WCG.mjs';
3
- import { deserializePayloadV2, createPartitionIndex, resolveProjection, clampRowCount, sortSignatureOf, requireMatchingCursor, decodePageCursor, normalizeSort, encodePageCursor, applyProjection, readSortValues, getLanguage, DEFAULT_PAGE_SIZE, requireWithinResultWindow, fnv1a, FILTER_EXPRESSION_KEYS, MAX_CURSOR_LENGTH, mergePartitionStats, FIELD_FILTER_OPERATORS, MAX_NESTING_DEPTH } from './chunk-RO4EFWE6.mjs';
1
+ import { createEngineCore, createNarsilFromCore, runEngineQueryStats, runEngineListDocuments, runEngineSuggest, runEnginePreflight, runEngineQuery, createEngineIndex, applyIndexEntry, applyDeleteEntry, DEFAULT_SUGGEST_LIMIT, MAX_SUGGEST_LIMIT, now, validateEntryBatchPayload, createAckMessage, validateEntryPayload, validateForwardBatchPayload, createForwardBatchResultMessage, clampLimit, createForwardBatchMessage, validateForwardBatchResultPayload, createForwardMessage, validateReplicationEntry, MAX_SUGGEST_SCATTER_LIMIT, chunkByBudget, WIRE_BATCH_BUDGET, SUGGEST_OVERSAMPLE_FACTOR, SUGGEST_OVERSAMPLE_PADDING, createEntryMessage, computeEntryChecksum, createEntryBatchMessage, createInsyncConfirmMessage, validateAckPayload, createInsyncRemoveMessage, validateInsyncConfirmPayload, createInsyncAddMessage } from './chunk-MMSWEEZT.mjs';
2
+ import { ReplicationMessageTypes, TransportErrorCodes, ClusterMessageTypes, QueryMessageTypes, TransportError, MAX_MESSAGE_SIZE_BYTES } from './chunk-JQUL2T6I.mjs';
3
+ import { validateIndexName, indexConfigKey, putIndexMetadata, getIndexMetadata, MAX_PARTITION_COUNT, MAX_INDEX_NAME_LENGTH } from './chunk-WU2UZEVD.mjs';
4
+ import { deserializePayloadV2, createPartitionIndex, resolveProjection, clampRowCount, sortSignatureOf, requireMatchingCursor, decodePageCursor, normalizeSort, encodePageCursor, applyProjection, readSortValues, getLanguage, DEFAULT_PAGE_SIZE, requireWithinResultWindow, fnv1a, FILTER_EXPRESSION_KEYS, MAX_CURSOR_LENGTH, mergePartitionStats, FIELD_FILTER_OPERATORS, MAX_NESTING_DEPTH } from './chunk-GRWFY2HI.mjs';
4
5
  import './chunk-TGP5M5R7.mjs';
5
6
  import './chunk-OBXS7OZM.mjs';
6
7
  import './chunk-WDL632CN.mjs';
@@ -8,7 +9,7 @@ import { crc32 } from './chunk-KMMJMDUA.mjs';
8
9
  import './chunk-U2BKYEC7.mjs';
9
10
  import { compareCodePoints, toComparableSortValue, compareComparableKeys, compareSortValues } from './chunk-BFIH7OV5.mjs';
10
11
  import { generateId } from './chunk-WTONLDU3.mjs';
11
- import { ErrorCodes, NarsilError, describeError } from './chunk-EMI63EXP.mjs';
12
+ import { ErrorCodes, NarsilError, describeError } from './chunk-GB2IIJJG.mjs';
12
13
  import { encode, decode } from '@msgpack/msgpack';
13
14
 
14
15
  // src/distribution/cluster/controller/types.ts
@@ -26,6 +27,12 @@ function createElectionState() {
26
27
  standbyTimer: null
27
28
  };
28
29
  }
30
+ function clearStandbyTimer(state) {
31
+ if (state.standbyTimer !== null) {
32
+ clearTimeout(state.standbyTimer);
33
+ state.standbyTimer = null;
34
+ }
35
+ }
29
36
  function clearElectionTimers(state) {
30
37
  if (state.renewalTimer !== null) {
31
38
  clearInterval(state.renewalTimer);
@@ -58,7 +65,11 @@ function startRenewalInterval(state, coordinator, nodeId, ttlMs, onLeaseLost) {
58
65
  }, intervalMs);
59
66
  }
60
67
  function scheduleStandbyRetry(state, retryMs, onRetry) {
61
- state.standbyTimer = setTimeout(onRetry, retryMs);
68
+ clearStandbyTimer(state);
69
+ state.standbyTimer = setTimeout(() => {
70
+ state.standbyTimer = null;
71
+ onRetry();
72
+ }, retryMs);
62
73
  }
63
74
 
64
75
  // src/distribution/cluster/allocator/deciders/balance.ts
@@ -397,15 +408,26 @@ function initialAllocate(nodes, indexName, partitionCount, replicationFactor, co
397
408
  };
398
409
  }
399
410
 
411
+ // src/distribution/cluster/last-holders.ts
412
+ function lastHoldersOf(assignment) {
413
+ return assignment.lastHolders ?? [];
414
+ }
415
+ function holdsLastCopy(assignment, nodeId) {
416
+ return lastHoldersOf(assignment).includes(nodeId);
417
+ }
418
+
400
419
  // src/distribution/cluster/allocator/leadership.ts
401
420
  var LEADERSHIP_IMBALANCE_THRESHOLD = 2;
402
421
  function loadOf(nodeId, counts, shares) {
403
422
  const share = shares.get(nodeId);
404
423
  return (counts.get(nodeId) ?? 0) / (share === void 0 || share <= 0 ? 1 : share);
405
424
  }
406
- function heaviestLeader(shares, counts) {
425
+ function heaviestLeader(shares, counts, withoutHandover) {
407
426
  let heaviest;
408
427
  for (const nodeId of shares.keys()) {
428
+ if (withoutHandover.has(nodeId)) {
429
+ continue;
430
+ }
409
431
  if (heaviest === void 0 || loadOf(nodeId, counts, shares) > loadOf(heaviest, counts, shares)) {
410
432
  heaviest = nodeId;
411
433
  }
@@ -435,19 +457,24 @@ function applySwap(swap, previousPrimary) {
435
457
  const { assignment, candidateNodeId } = swap;
436
458
  assignment.primary = candidateNodeId;
437
459
  assignment.replicas = [...assignment.replicas.filter((nodeId) => nodeId !== candidateNodeId), previousPrimary];
438
- assignment.inSyncSet = assignment.inSyncSet.filter((nodeId) => nodeId !== candidateNodeId);
460
+ assignment.inSyncSet = [
461
+ ...assignment.inSyncSet.filter((nodeId) => nodeId !== candidateNodeId && nodeId !== previousPrimary),
462
+ previousPrimary
463
+ ];
439
464
  assignment.primaryTerm += 1;
440
465
  }
441
466
  function rebalanceLeadership(assignments, shares) {
467
+ const withoutHandover = /* @__PURE__ */ new Set();
442
468
  for (let swaps = 0; swaps < assignments.size; swaps++) {
443
469
  const counts = countPrimaryAssignments(assignments);
444
- const heaviest = heaviestLeader(shares, counts);
470
+ const heaviest = heaviestLeader(shares, counts, withoutHandover);
445
471
  if (heaviest === void 0) {
446
472
  return;
447
473
  }
448
474
  const swap = findSwap(assignments, heaviest, counts, shares);
449
475
  if (swap === null) {
450
- return;
476
+ withoutHandover.add(heaviest);
477
+ continue;
451
478
  }
452
479
  applySwap(swap, heaviest);
453
480
  }
@@ -475,15 +502,25 @@ function cloneAssignments(assignments) {
475
502
  inSyncSet: [...assignment.inSyncSet],
476
503
  commitPoint: assignment.commitPoint,
477
504
  state: assignment.state,
478
- primaryTerm: assignment.primaryTerm
505
+ primaryTerm: assignment.primaryTerm,
506
+ ...assignment.lastHolders !== void 0 ? { lastHolders: [...assignment.lastHolders] } : {},
507
+ ...assignment.unassignedReason !== void 0 ? { unassignedReason: assignment.unassignedReason } : {}
479
508
  });
480
509
  }
481
510
  return cloned;
482
511
  }
483
- function markUnassigned(assignment) {
512
+ function markUnassigned(assignment, lastPrimary, lastInSyncSet) {
513
+ const holders = new Set(lastHoldersOf(assignment));
514
+ for (const nodeId of lastInSyncSet) {
515
+ holders.add(nodeId);
516
+ }
517
+ if (lastPrimary !== null) {
518
+ holders.add(lastPrimary);
519
+ }
484
520
  assignment.primary = null;
485
521
  assignment.replicas = [];
486
522
  assignment.inSyncSet = [];
523
+ assignment.lastHolders = [...holders].sort(compareCodePoints);
487
524
  assignment.state = "UNASSIGNED";
488
525
  }
489
526
  function primaryLoadOf(nodeId, primaryCounts, shares) {
@@ -508,7 +545,12 @@ function leastLoadedCandidate(candidates, primaryCounts, shares) {
508
545
  function handleLostNodes(assignments, activeNodeIds, shares) {
509
546
  const primaryCounts = countPrimaryAssignments(assignments);
510
547
  for (const assignment of assignments.values()) {
511
- const primaryWasLost = assignment.primary !== null && !activeNodeIds.has(assignment.primary);
548
+ if (assignment.state === "UNASSIGNED") {
549
+ continue;
550
+ }
551
+ const lastPrimary = assignment.primary;
552
+ const lastInSyncSet = [...assignment.inSyncSet];
553
+ const primaryWasLost = lastPrimary !== null && !activeNodeIds.has(lastPrimary);
512
554
  if (primaryWasLost) {
513
555
  assignment.primary = null;
514
556
  }
@@ -516,18 +558,18 @@ function handleLostNodes(assignments, activeNodeIds, shares) {
516
558
  assignment.inSyncSet = assignment.inSyncSet.filter((id) => activeNodeIds.has(id));
517
559
  if (!primaryWasLost) {
518
560
  if (assignment.primary === null) {
519
- markUnassigned(assignment);
561
+ markUnassigned(assignment, lastPrimary, lastInSyncSet);
520
562
  }
521
563
  continue;
522
564
  }
523
565
  const inSyncCandidates = assignment.replicas.filter((id) => assignment.inSyncSet.includes(id));
524
566
  if (inSyncCandidates.length === 0) {
525
- markUnassigned(assignment);
567
+ markUnassigned(assignment, lastPrimary, lastInSyncSet);
526
568
  continue;
527
569
  }
528
570
  const promoted = leastLoadedCandidate(inSyncCandidates, primaryCounts, shares);
529
571
  if (promoted === void 0) {
530
- markUnassigned(assignment);
572
+ markUnassigned(assignment, lastPrimary, lastInSyncSet);
531
573
  continue;
532
574
  }
533
575
  primaryCounts.set(promoted, (primaryCounts.get(promoted) ?? 0) + 1);
@@ -634,8 +676,21 @@ function moveOneReplica(assignments, move, laggingOnly) {
634
676
  }
635
677
  return false;
636
678
  }
679
+ function clearRecoveryStateOnServedPartitions(assignments) {
680
+ for (const assignment of assignments.values()) {
681
+ if (assignment.state !== "UNASSIGNED") {
682
+ delete assignment.unassignedReason;
683
+ }
684
+ if (assignment.state === "ACTIVE") {
685
+ delete assignment.lastHolders;
686
+ }
687
+ }
688
+ }
637
689
  function pruneInSyncSets(assignments) {
638
690
  for (const assignment of assignments.values()) {
691
+ if (assignment.state === "UNASSIGNED") {
692
+ continue;
693
+ }
639
694
  const stale = assignment.inSyncSet.some((nodeId) => !assignment.replicas.includes(nodeId));
640
695
  if (stale) {
641
696
  assignment.inSyncSet = assignment.inSyncSet.filter((nodeId) => assignment.replicas.includes(nodeId));
@@ -660,6 +715,7 @@ function rebalanceAllocate(nodes, currentTable, constraints, deciders) {
660
715
  rebalanceForBalance(assignments, sortedNodes, nodeMap, constraints, deciders, shares);
661
716
  rebalanceLeadership(assignments, shares);
662
717
  pruneInSyncSets(assignments);
718
+ clearRecoveryStateOnServedPartitions(assignments);
663
719
  const warnings = collectReplicationWarnings(assignments, currentTable.replicationFactor);
664
720
  return {
665
721
  table: {
@@ -708,7 +764,268 @@ function allocate(nodes, currentTable, indexName, partitionCount, replicationFac
708
764
  return rebalanceAllocate(dataNodes, currentTable, constraints, deciders);
709
765
  }
710
766
 
767
+ // src/distribution/payload-guards.ts
768
+ function isRecord(value) {
769
+ return value !== null && typeof value === "object" && !Array.isArray(value);
770
+ }
771
+ function isValidInteger(value) {
772
+ return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value);
773
+ }
774
+
775
+ // src/distribution/cluster/partition-stores.ts
776
+ function validatePartitionStoresPayload(decoded) {
777
+ if (!isRecord(decoded)) {
778
+ return null;
779
+ }
780
+ if (typeof decoded.indexName !== "string" || decoded.indexName.length === 0) {
781
+ return null;
782
+ }
783
+ return { indexName: decoded.indexName };
784
+ }
785
+ function validatePartitionStoresResultPayload(decoded) {
786
+ if (!isRecord(decoded)) {
787
+ return null;
788
+ }
789
+ if (typeof decoded.indexName !== "string" || decoded.indexName.length === 0) {
790
+ return null;
791
+ }
792
+ if (decoded.indexUuid !== null && typeof decoded.indexUuid !== "string") {
793
+ return null;
794
+ }
795
+ if (!Array.isArray(decoded.partitionIds)) {
796
+ return null;
797
+ }
798
+ const partitionIds = [];
799
+ for (const partitionId of decoded.partitionIds) {
800
+ if (typeof partitionId !== "number" || !Number.isInteger(partitionId) || partitionId < 0) {
801
+ return null;
802
+ }
803
+ partitionIds.push(partitionId);
804
+ }
805
+ return { indexName: decoded.indexName, indexUuid: decoded.indexUuid, partitionIds };
806
+ }
807
+ function createPartitionStoresMessage(payload, sourceNodeId, requestId) {
808
+ return {
809
+ type: ClusterMessageTypes.PARTITION_STORES,
810
+ sourceId: sourceNodeId,
811
+ requestId,
812
+ payload: encode(payload)
813
+ };
814
+ }
815
+ function createPartitionStoresResultMessage(result, sourceNodeId, requestId) {
816
+ return {
817
+ type: ClusterMessageTypes.PARTITION_STORES,
818
+ sourceId: sourceNodeId,
819
+ requestId,
820
+ payload: encode(result)
821
+ };
822
+ }
823
+
824
+ // src/distribution/cluster/controller/event-loop/unassigned-recovery.ts
825
+ var RECOVERY_CAS_ATTEMPTS = 5;
826
+ var PARTITION_STORES_TIMEOUT_MS = 5e3;
827
+ function liveHoldersOfUnassignedPartitions(table, liveNodeIds) {
828
+ const holders = /* @__PURE__ */ new Set();
829
+ for (const assignment of table.assignments.values()) {
830
+ if (assignment.state !== "UNASSIGNED") {
831
+ continue;
832
+ }
833
+ for (const nodeId of lastHoldersOf(assignment)) {
834
+ if (liveNodeIds.has(nodeId)) {
835
+ holders.add(nodeId);
836
+ }
837
+ }
838
+ }
839
+ return [...holders].sort(compareCodePoints);
840
+ }
841
+ function hasRecoverablePartition(table) {
842
+ for (const assignment of table.assignments.values()) {
843
+ if (assignment.state === "UNASSIGNED" && lastHoldersOf(assignment).length > 0) {
844
+ return true;
845
+ }
846
+ }
847
+ return false;
848
+ }
849
+ function reasonFor(assignment, liveNodeIds, answers) {
850
+ const holders = lastHoldersOf(assignment);
851
+ if (holders.length === 0) {
852
+ return void 0;
853
+ }
854
+ if (holders.some((nodeId) => !liveNodeIds.has(nodeId))) {
855
+ return "HOLDER_OFFLINE";
856
+ }
857
+ const given = holders.map((nodeId) => answers.get(nodeId));
858
+ if (given.some((answer) => answer === void 0 || answer.kind === "unreachable")) {
859
+ return "HOLDER_UNREACHABLE";
860
+ }
861
+ if (given.some((answer) => answer?.kind === "identityMismatch")) {
862
+ return "HOLDER_IDENTITY_MISMATCH";
863
+ }
864
+ return "HOLDER_WITHOUT_DATA";
865
+ }
866
+ function targetsFor(deps, targetNodeId) {
867
+ const targets = [targetNodeId];
868
+ const address = deps.addressByNodeId.get(targetNodeId);
869
+ if (address !== void 0 && address.length > 0 && address !== targetNodeId) {
870
+ targets.push(address);
871
+ }
872
+ return targets;
873
+ }
874
+ function withTimeout(work, timeoutMs, fallback) {
875
+ return new Promise((resolve) => {
876
+ const timer = setTimeout(() => resolve(fallback), timeoutMs);
877
+ timer.unref?.();
878
+ work.then((value) => {
879
+ clearTimeout(timer);
880
+ resolve(value);
881
+ }).catch(() => {
882
+ clearTimeout(timer);
883
+ resolve(fallback);
884
+ });
885
+ });
886
+ }
887
+ async function sendToFirstReachableTarget(deps, targetNodeId, message) {
888
+ for (const target of targetsFor(deps, targetNodeId)) {
889
+ const response = await withTimeout(deps.transport.send(target, message), PARTITION_STORES_TIMEOUT_MS, null);
890
+ if (response !== null) {
891
+ return response;
892
+ }
893
+ }
894
+ return null;
895
+ }
896
+ async function askNodeForStores(deps, targetNodeId, indexName, indexUuid) {
897
+ const message = createPartitionStoresMessage({ indexName }, deps.controllerNodeId, generateId());
898
+ const response = await sendToFirstReachableTarget(deps, targetNodeId, message);
899
+ if (response === null) {
900
+ return { kind: "unreachable" };
901
+ }
902
+ let answer;
903
+ try {
904
+ answer = validatePartitionStoresResultPayload(decode(response.payload));
905
+ } catch (_) {
906
+ return { kind: "unreachable" };
907
+ }
908
+ if (answer === null || answer.indexName !== indexName) {
909
+ return { kind: "unreachable" };
910
+ }
911
+ if (answer.indexUuid !== indexUuid) {
912
+ return { kind: "identityMismatch" };
913
+ }
914
+ return { kind: "held", partitionIds: new Set(answer.partitionIds) };
915
+ }
916
+ function promoteAssignment(assignment, nodeId) {
917
+ const promoted = {
918
+ ...assignment,
919
+ primary: nodeId,
920
+ replicas: [],
921
+ inSyncSet: [],
922
+ primaryTerm: assignment.primaryTerm + 1,
923
+ state: "INITIALISING"
924
+ };
925
+ delete promoted.unassignedReason;
926
+ return promoted;
927
+ }
928
+ function holderThatAnswered(assignment, partitionId, answers) {
929
+ const holders = [...lastHoldersOf(assignment)].sort(compareCodePoints);
930
+ return holders.find((nodeId) => {
931
+ const answer = answers.get(nodeId);
932
+ return answer?.kind === "held" && answer.partitionIds.has(partitionId);
933
+ });
934
+ }
935
+ function rewriteAssignment(assignment, partitionId, liveNodeIds, answers) {
936
+ const holder = holderThatAnswered(assignment, partitionId, answers);
937
+ if (holder !== void 0) {
938
+ return { assignment: promoteAssignment(assignment, holder), promoted: true };
939
+ }
940
+ const reason = reasonFor(assignment, liveNodeIds, answers);
941
+ if (reason === assignment.unassignedReason) {
942
+ return null;
943
+ }
944
+ if (reason === void 0) {
945
+ const cleared = { ...assignment };
946
+ delete cleared.unassignedReason;
947
+ return { assignment: cleared, promoted: false };
948
+ }
949
+ return { assignment: { ...assignment, unassignedReason: reason }, promoted: false };
950
+ }
951
+ async function writeRecoveryOutcome(deps, indexName, liveNodeIds, answers) {
952
+ for (let attempt = 0; attempt < RECOVERY_CAS_ATTEMPTS; attempt++) {
953
+ if (!deps.isActive()) {
954
+ return false;
955
+ }
956
+ const table = await deps.coordinator.getAllocation(indexName);
957
+ if (table === null) {
958
+ return false;
959
+ }
960
+ const rewritten = /* @__PURE__ */ new Map();
961
+ let anyPromoted = false;
962
+ for (const [partitionId, assignment] of table.assignments) {
963
+ if (assignment.state !== "UNASSIGNED") {
964
+ continue;
965
+ }
966
+ const outcome = rewriteAssignment(assignment, partitionId, liveNodeIds, answers);
967
+ if (outcome === null) {
968
+ continue;
969
+ }
970
+ rewritten.set(partitionId, outcome.assignment);
971
+ anyPromoted = anyPromoted || outcome.promoted;
972
+ }
973
+ if (rewritten.size === 0) {
974
+ return false;
975
+ }
976
+ const assignments = new Map(table.assignments);
977
+ for (const [partitionId, assignment] of rewritten) {
978
+ assignments.set(partitionId, assignment);
979
+ }
980
+ if (!deps.isActive()) {
981
+ return false;
982
+ }
983
+ const written = await deps.coordinator.putAllocation(
984
+ indexName,
985
+ { ...table, version: table.version + 1, assignments },
986
+ table.version
987
+ );
988
+ if (written) {
989
+ return anyPromoted;
990
+ }
991
+ }
992
+ return false;
993
+ }
994
+ async function recoverUnassignedPartitions(coordinator, transport, indexName, controllerNodeId, nodes, isActive) {
995
+ const addressByNodeId = new Map(nodes.map((node) => [node.nodeId, node.address]));
996
+ const liveNodeIds = new Set(addressByNodeId.keys());
997
+ const deps = { coordinator, transport, controllerNodeId, addressByNodeId, isActive };
998
+ if (!isActive()) {
999
+ return false;
1000
+ }
1001
+ const table = await coordinator.getAllocation(indexName);
1002
+ if (table === null) {
1003
+ return false;
1004
+ }
1005
+ if (!hasRecoverablePartition(table) || !isActive()) {
1006
+ return false;
1007
+ }
1008
+ const answers = /* @__PURE__ */ new Map();
1009
+ const candidateNodeIds = liveHoldersOfUnassignedPartitions(table, liveNodeIds);
1010
+ if (candidateNodeIds.length > 0) {
1011
+ const metadata = await getIndexMetadata(coordinator, indexName);
1012
+ if (metadata === null || !isActive()) {
1013
+ return false;
1014
+ }
1015
+ const given = await Promise.all(
1016
+ candidateNodeIds.map((nodeId) => askNodeForStores(deps, nodeId, indexName, metadata.indexUuid))
1017
+ );
1018
+ for (let index = 0; index < candidateNodeIds.length; index += 1) {
1019
+ answers.set(candidateNodeIds[index], given[index]);
1020
+ }
1021
+ }
1022
+ return writeRecoveryOutcome(deps, indexName, liveNodeIds, answers);
1023
+ }
1024
+
711
1025
  // src/distribution/cluster/controller/event-loop/allocation.ts
1026
+ function holdsDataNode(nodes) {
1027
+ return nodes.some((node) => node.roles.includes("data"));
1028
+ }
712
1029
  var ALLOCATION_CAS_ATTEMPTS = 5;
713
1030
  var ALLOCATION_RETRY_DELAY_MS = 1e3;
714
1031
  var TEARDOWN_CAS_ATTEMPTS = 5;
@@ -750,12 +1067,29 @@ async function runAllocatorForIndex(coordinator, indexName, nodes, isActive, att
750
1067
  { indexName, attempts: ALLOCATION_CAS_ATTEMPTS }
751
1068
  );
752
1069
  }
1070
+ async function recoverIndexPartitions(state, coordinator, indexName, nodes, isActive) {
1071
+ const { transport, controllerNodeId } = state;
1072
+ if (transport === null || controllerNodeId === null) {
1073
+ return;
1074
+ }
1075
+ const promoted = await recoverUnassignedPartitions(
1076
+ coordinator,
1077
+ transport,
1078
+ indexName,
1079
+ controllerNodeId,
1080
+ nodes,
1081
+ isActive
1082
+ );
1083
+ if (promoted) {
1084
+ await runAllocatorForIndex(coordinator, indexName, nodes, isActive);
1085
+ }
1086
+ }
753
1087
  async function runAllocatorForAllIndexes(state, coordinator, isActive, onError) {
754
1088
  if (!isActive()) {
755
1089
  return;
756
1090
  }
757
1091
  const nodes = await coordinator.listNodes();
758
- if (nodes.length === 0 || !isActive()) {
1092
+ if (!holdsDataNode(nodes) || !isActive()) {
759
1093
  return;
760
1094
  }
761
1095
  let anyIndexFailed = false;
@@ -765,6 +1099,7 @@ async function runAllocatorForAllIndexes(state, coordinator, isActive, onError)
765
1099
  }
766
1100
  try {
767
1101
  await runAllocatorForIndex(coordinator, indexName, nodes, isActive);
1102
+ await recoverIndexPartitions(state, coordinator, indexName, nodes, isActive);
768
1103
  } catch (error) {
769
1104
  anyIndexFailed = true;
770
1105
  if (onError !== void 0) {
@@ -800,7 +1135,7 @@ async function handleSchemaCreated(indexName, coordinator, knownIndexes, isActiv
800
1135
  return;
801
1136
  }
802
1137
  const nodes = await coordinator.listNodes();
803
- if (nodes.length === 0 || !isActive()) {
1138
+ if (!holdsDataNode(nodes) || !isActive()) {
804
1139
  return;
805
1140
  }
806
1141
  const storedTable = await coordinator.getAllocation(indexName);
@@ -891,14 +1226,6 @@ async function handleSchemaEvent(event, coordinator, state, isActive, onError) {
891
1226
  await tearDownAllocation(event.indexName, coordinator, state, isActive, 0, onError);
892
1227
  }
893
1228
  }
894
-
895
- // src/distribution/payload-guards.ts
896
- function isRecord(value) {
897
- return value !== null && typeof value === "object" && !Array.isArray(value);
898
- }
899
- function isValidInteger(value) {
900
- return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value);
901
- }
902
1229
  var INSYNC_CAS_ATTEMPTS = 5;
903
1230
  var REFUSE = { outcome: "refuse" };
904
1231
  var ACCEPT = { outcome: "accept" };
@@ -954,6 +1281,9 @@ async function requestInsyncRemoval(indexName, partitionId, replicaNodeId, prima
954
1281
  }
955
1282
  function handleInsyncRemoval(payload, coordinator) {
956
1283
  return updateInsyncSet(payload, coordinator, (assignment) => {
1284
+ if (assignment.state === "UNASSIGNED") {
1285
+ return REFUSE;
1286
+ }
957
1287
  if (!assignment.inSyncSet.includes(payload.replicaNodeId)) {
958
1288
  return ACCEPT;
959
1289
  }
@@ -1106,8 +1436,10 @@ async function processBootstrapComplete(payload, coordinator) {
1106
1436
  if (assignment.state !== "INITIALISING") {
1107
1437
  return false;
1108
1438
  }
1439
+ const served = { ...assignment, state: "ACTIVE" };
1440
+ delete served.lastHolders;
1109
1441
  const updatedAssignments = new Map(table.assignments);
1110
- updatedAssignments.set(payload.partitionId, { ...assignment, state: "ACTIVE" });
1442
+ updatedAssignments.set(payload.partitionId, served);
1111
1443
  const written = await coordinator.putAllocation(
1112
1444
  payload.indexName,
1113
1445
  { ...table, version: table.version + 1, assignments: updatedAssignments },
@@ -1262,7 +1594,9 @@ function createEventLoopState(initialIndexNames) {
1262
1594
  knownIndexes: new Set(initialIndexNames),
1263
1595
  debounceTimer: null,
1264
1596
  teardownTimers: /* @__PURE__ */ new Map(),
1265
- insyncQueue: Promise.resolve()
1597
+ insyncQueue: Promise.resolve(),
1598
+ transport: null,
1599
+ controllerNodeId: null
1266
1600
  };
1267
1601
  }
1268
1602
  function clearEventLoopWatchers(state) {
@@ -1291,6 +1625,8 @@ function clearEventLoopWatchers(state) {
1291
1625
  // src/distribution/cluster/controller/event-loop/index.ts
1292
1626
  async function startEventLoop(state, coordinator, transport, nodeId, isActive, onError) {
1293
1627
  clearEventLoopWatchers(state);
1628
+ state.transport = transport;
1629
+ state.controllerNodeId = nodeId;
1294
1630
  try {
1295
1631
  state.unwatchNodes = await coordinator.watchNodes((_event) => {
1296
1632
  scheduleDebouncedAllocation(state, coordinator, isActive, onError);
@@ -1323,13 +1659,21 @@ async function startEventLoop(state, coordinator, transport, nodeId, isActive, o
1323
1659
 
1324
1660
  // src/distribution/cluster/controller/index.ts
1325
1661
  function createController(config) {
1326
- const { nodeId, coordinator, transport, leaseTtlMs, standbyRetryMs, knownIndexNames, onError } = config;
1662
+ const { nodeId, coordinator, transport, leaseTtlMs, standbyRetryMs, knownIndexNames, onError, onElectionError } = config;
1327
1663
  let electionState = createElectionState();
1328
1664
  let eventLoopState = createEventLoopState(knownIndexNames);
1329
1665
  let stopped = false;
1330
1666
  function isActive() {
1331
1667
  return electionState.active;
1332
1668
  }
1669
+ function standBy() {
1670
+ if (stopped) {
1671
+ return;
1672
+ }
1673
+ scheduleStandbyRetry(electionState, standbyRetryMs, () => {
1674
+ void runElection();
1675
+ });
1676
+ }
1333
1677
  function stepDown() {
1334
1678
  if (!electionState.active) {
1335
1679
  return;
@@ -1337,12 +1681,7 @@ function createController(config) {
1337
1681
  electionState.active = false;
1338
1682
  clearElectionTimers(electionState);
1339
1683
  clearEventLoopWatchers(eventLoopState);
1340
- if (!stopped) {
1341
- scheduleStandbyRetry(electionState, standbyRetryMs, () => {
1342
- tryElection().catch(() => {
1343
- });
1344
- });
1345
- }
1684
+ standBy();
1346
1685
  }
1347
1686
  async function becomeActive() {
1348
1687
  if (electionState.active) {
@@ -1368,18 +1707,38 @@ function createController(config) {
1368
1707
  throw error;
1369
1708
  }
1370
1709
  }
1371
- async function tryElection() {
1710
+ async function standForElection() {
1372
1711
  if (stopped) {
1373
1712
  return;
1374
1713
  }
1375
1714
  const acquired = await tryAcquireLease(coordinator, nodeId, leaseTtlMs);
1715
+ if (stopped) {
1716
+ if (acquired) {
1717
+ await releaseLease(coordinator);
1718
+ }
1719
+ return;
1720
+ }
1376
1721
  if (acquired) {
1377
1722
  await becomeActive();
1378
- } else {
1379
- scheduleStandbyRetry(electionState, standbyRetryMs, () => {
1380
- tryElection().catch(() => {
1381
- });
1382
- });
1723
+ return;
1724
+ }
1725
+ standBy();
1726
+ }
1727
+ function reportElectionFailure(error) {
1728
+ if (onElectionError === void 0) {
1729
+ return;
1730
+ }
1731
+ try {
1732
+ onElectionError(error);
1733
+ } catch (_) {
1734
+ }
1735
+ }
1736
+ async function runElection() {
1737
+ try {
1738
+ await standForElection();
1739
+ } catch (error) {
1740
+ reportElectionFailure(error);
1741
+ standBy();
1383
1742
  }
1384
1743
  }
1385
1744
  const controller = {
@@ -1388,7 +1747,13 @@ function createController(config) {
1388
1747
  },
1389
1748
  async start() {
1390
1749
  stopped = false;
1391
- await tryElection();
1750
+ clearStandbyTimer(electionState);
1751
+ try {
1752
+ await standForElection();
1753
+ } catch (error) {
1754
+ standBy();
1755
+ throw error;
1756
+ }
1392
1757
  },
1393
1758
  async stop() {
1394
1759
  stopped = true;
@@ -1612,7 +1977,7 @@ function createBootstrapState(indexName, partitionId, primaryNodeId) {
1612
1977
  };
1613
1978
  }
1614
1979
 
1615
- // src/distribution/cluster/node-lifecycle/allocation-watcher.ts
1980
+ // src/distribution/cluster/node-lifecycle/allocation-watcher/state.ts
1616
1981
  function partitionKey(indexName, partitionId) {
1617
1982
  return `${indexName}:${partitionId}`;
1618
1983
  }
@@ -1622,40 +1987,137 @@ function createAllocationWatcherState() {
1622
1987
  debounceTimer: null,
1623
1988
  activeBootstraps: /* @__PURE__ */ new Map(),
1624
1989
  trackedPartitions: /* @__PURE__ */ new Map(),
1990
+ pendingPartitions: /* @__PURE__ */ new Set(),
1625
1991
  pendingTables: /* @__PURE__ */ new Map(),
1626
1992
  restartWaiters: /* @__PURE__ */ new Map(),
1993
+ retainedPartitions: /* @__PURE__ */ new Set(),
1994
+ indexesSeededFromRecord: /* @__PURE__ */ new Set(),
1627
1995
  stopped: false
1628
1996
  };
1629
1997
  }
1630
- async function startAllocationWatcher(state, config) {
1631
- const unwatchAllocation = await config.coordinator.watchAllocation((event) => {
1632
- if (state.stopped) {
1633
- return;
1998
+ function stopAllocationWatcher(state) {
1999
+ state.stopped = true;
2000
+ if (state.unwatchAllocation !== null) {
2001
+ state.unwatchAllocation();
2002
+ state.unwatchAllocation = null;
2003
+ }
2004
+ if (state.debounceTimer !== null) {
2005
+ clearTimeout(state.debounceTimer);
2006
+ state.debounceTimer = null;
2007
+ }
2008
+ for (const [timer, resolve] of state.restartWaiters) {
2009
+ clearTimeout(timer);
2010
+ resolve();
2011
+ }
2012
+ state.restartWaiters.clear();
2013
+ for (const bootstrapState of state.activeBootstraps.values()) {
2014
+ abortBootstrapState(bootstrapState);
2015
+ }
2016
+ state.activeBootstraps.clear();
2017
+ state.trackedPartitions.clear();
2018
+ state.pendingPartitions.clear();
2019
+ state.retainedPartitions.clear();
2020
+ state.indexesSeededFromRecord.clear();
2021
+ state.pendingTables.clear();
2022
+ }
2023
+
2024
+ // src/distribution/cluster/node-lifecycle/allocation-watcher/bootstrap-restart.ts
2025
+ function startBootstrap(state, config, indexName, partitionId, primaryNodeId) {
2026
+ const key = partitionKey(indexName, partitionId);
2027
+ const existingBootstrap = state.activeBootstraps.get(key);
2028
+ if (existingBootstrap !== void 0) {
2029
+ abortBootstrapState(existingBootstrap);
2030
+ }
2031
+ const bootstrapState = createBootstrapState(indexName, partitionId, primaryNodeId);
2032
+ state.activeBootstraps.set(key, bootstrapState);
2033
+ bootstrapPartition(
2034
+ bootstrapState,
2035
+ config.coordinator,
2036
+ config.transport,
2037
+ config.registration.nodeId,
2038
+ config.bootstrapRetryBaseMs,
2039
+ config.bootstrapRetryMaxMs,
2040
+ config.bootstrapMaxRetries,
2041
+ config.onBootstrapPartition,
2042
+ config.onError
2043
+ ).then((succeeded) => {
2044
+ if (state.activeBootstraps.get(key) === bootstrapState) {
2045
+ state.activeBootstraps.delete(key);
1634
2046
  }
1635
- scheduleProcessAllocation(state, config, event.table);
2047
+ if (!succeeded) {
2048
+ state.trackedPartitions.delete(key);
2049
+ void restartBootstrapWhileOutOfSync(state, config, indexName, partitionId);
2050
+ }
2051
+ }).catch(() => {
2052
+ if (state.activeBootstraps.get(key) === bootstrapState) {
2053
+ state.activeBootstraps.delete(key);
2054
+ }
2055
+ state.trackedPartitions.delete(key);
2056
+ void restartBootstrapWhileOutOfSync(state, config, indexName, partitionId);
1636
2057
  });
1637
- state.unwatchAllocation = unwatchAllocation;
1638
2058
  }
1639
- function scheduleProcessAllocation(state, config, table) {
1640
- state.pendingTables.set(table.indexName, table);
1641
- if (state.debounceTimer !== null) {
1642
- clearTimeout(state.debounceTimer);
2059
+ function waitBeforeBootstrapRestart(state, config) {
2060
+ const delayMs = Math.min(config.bootstrapRetryMaxMs, config.bootstrapRetryBaseMs);
2061
+ if (delayMs <= 0) {
2062
+ return Promise.resolve();
1643
2063
  }
1644
- state.debounceTimer = setTimeout(() => {
1645
- state.debounceTimer = null;
1646
- const tablesToProcess = Array.from(state.pendingTables.values());
1647
- state.pendingTables.clear();
1648
- for (const pending of tablesToProcess) {
1649
- processAllocationChange(state, config, pending);
2064
+ return new Promise((resolve) => {
2065
+ const timer = setTimeout(() => {
2066
+ state.restartWaiters.delete(timer);
2067
+ resolve();
2068
+ }, delayMs);
2069
+ timer.unref?.();
2070
+ state.restartWaiters.set(timer, resolve);
2071
+ });
2072
+ }
2073
+ async function restartBootstrapWhileOutOfSync(state, config, indexName, partitionId) {
2074
+ if (state.stopped) {
2075
+ return;
2076
+ }
2077
+ await waitBeforeBootstrapRestart(state, config);
2078
+ if (state.stopped) {
2079
+ return;
2080
+ }
2081
+ const nodeId = config.registration.nodeId;
2082
+ let table;
2083
+ try {
2084
+ table = await config.coordinator.getAllocation(indexName);
2085
+ } catch (error) {
2086
+ if (config.onError !== void 0) {
2087
+ config.onError(error);
1650
2088
  }
1651
- }, config.allocationDebounceMs);
2089
+ if (!state.stopped) {
2090
+ void restartBootstrapWhileOutOfSync(state, config, indexName, partitionId);
2091
+ }
2092
+ return;
2093
+ }
2094
+ if (state.stopped || table === null) {
2095
+ return;
2096
+ }
2097
+ const assignment = table.assignments.get(partitionId);
2098
+ if (assignment === void 0 || assignment.primary === null || assignment.primary === nodeId) {
2099
+ return;
2100
+ }
2101
+ if (!assignment.replicas.includes(nodeId) || assignment.inSyncSet.includes(nodeId)) {
2102
+ return;
2103
+ }
2104
+ if (assignment.state !== "ACTIVE" && assignment.state !== "INITIALISING" && assignment.state !== "MIGRATING") {
2105
+ return;
2106
+ }
2107
+ if (state.activeBootstraps.has(partitionKey(indexName, partitionId))) {
2108
+ return;
2109
+ }
2110
+ startBootstrap(state, config, indexName, partitionId, assignment.primary);
1652
2111
  }
2112
+
2113
+ // src/distribution/cluster/node-lifecycle/allocation-watcher/table.ts
1653
2114
  function processAllocationChange(state, config, table) {
1654
2115
  if (state.stopped) {
1655
2116
  return;
1656
2117
  }
1657
2118
  const nodeId = config.registration.nodeId;
1658
2119
  const currentKeys = /* @__PURE__ */ new Set();
2120
+ seedRetainedFromRecord(state, config, table.indexName);
1659
2121
  for (const [partitionId, assignment] of table.assignments) {
1660
2122
  if (!isNodeAssigned(assignment, nodeId)) {
1661
2123
  continue;
@@ -1676,8 +2138,34 @@ function processAllocationChange(state, config, table) {
1676
2138
  primaryNodeId: assignment.primary,
1677
2139
  primaryTerm: assignment.primaryTerm
1678
2140
  });
2141
+ if (servesPartition(assignment, nodeId)) {
2142
+ state.pendingPartitions.delete(key);
2143
+ } else {
2144
+ state.pendingPartitions.add(key);
2145
+ }
2146
+ if (config.onHoldPartition !== void 0 && alreadyHoldsData(assignment, nodeId)) {
2147
+ config.onHoldPartition(table.indexName, partitionId);
2148
+ }
2149
+ }
2150
+ removeDroppedPartitions(state, config, table, currentKeys);
2151
+ releaseSupersededCopies(state, config, table, currentKeys);
2152
+ }
2153
+ function processInitialAllocations(state, config, tables) {
2154
+ for (const table of tables) {
2155
+ processAllocationChange(state, config, table);
2156
+ }
2157
+ }
2158
+ function servesPartition(assignment, nodeId) {
2159
+ if (assignment.state === "INITIALISING" || assignment.state === "UNASSIGNED") {
2160
+ return false;
2161
+ }
2162
+ return assignment.primary === nodeId || assignment.inSyncSet.includes(nodeId);
2163
+ }
2164
+ function alreadyHoldsData(assignment, nodeId) {
2165
+ if (assignment.state !== "ACTIVE") {
2166
+ return false;
1679
2167
  }
1680
- removeUnassignedPartitions(state, config, table.indexName, currentKeys);
2168
+ return assignment.primary === nodeId || assignment.inSyncSet.includes(nodeId);
1681
2169
  }
1682
2170
  function isNodeAssigned(assignment, nodeId) {
1683
2171
  if (assignment.primary === nodeId) {
@@ -1710,6 +2198,10 @@ function handleExistingAssignment(state, config, existing, assignment, nodeId) {
1710
2198
  }
1711
2199
  return;
1712
2200
  }
2201
+ if (promotedBackAtHigherTerm(existing, assignment, nodeId)) {
2202
+ startBootstrap(state, config, existing.indexName, existing.partitionId, nodeId);
2203
+ return;
2204
+ }
1713
2205
  if (assignment.primary !== null && replicaNeedsResync(state, existing, assignment, nodeId)) {
1714
2206
  startBootstrap(state, config, existing.indexName, existing.partitionId, assignment.primary);
1715
2207
  }
@@ -1729,138 +2221,111 @@ function replicaNeedsResync(state, existing, assignment, nodeId) {
1729
2221
  }
1730
2222
  return !state.activeBootstraps.has(partitionKey(existing.indexName, existing.partitionId));
1731
2223
  }
1732
- function startBootstrap(state, config, indexName, partitionId, primaryNodeId) {
1733
- const key = partitionKey(indexName, partitionId);
1734
- const existingBootstrap = state.activeBootstraps.get(key);
1735
- if (existingBootstrap !== void 0) {
1736
- abortBootstrapState(existingBootstrap);
2224
+ function promotedBackAtHigherTerm(existing, assignment, nodeId) {
2225
+ return assignment.primary === nodeId && assignment.state === "INITIALISING" && assignment.primaryTerm > existing.primaryTerm;
2226
+ }
2227
+ function keepsCopyForRecovery(table, partitionId, nodeId) {
2228
+ const assignment = table.assignments.get(partitionId);
2229
+ if (assignment === void 0) {
2230
+ return false;
1737
2231
  }
1738
- const bootstrapState = createBootstrapState(indexName, partitionId, primaryNodeId);
1739
- state.activeBootstraps.set(key, bootstrapState);
1740
- bootstrapPartition(
1741
- bootstrapState,
1742
- config.coordinator,
1743
- config.transport,
1744
- config.registration.nodeId,
1745
- config.bootstrapRetryBaseMs,
1746
- config.bootstrapRetryMaxMs,
1747
- config.bootstrapMaxRetries,
1748
- config.onBootstrapPartition,
1749
- config.onError
1750
- ).then((succeeded) => {
1751
- if (state.activeBootstraps.get(key) === bootstrapState) {
1752
- state.activeBootstraps.delete(key);
2232
+ return holdsLastCopy(assignment, nodeId);
2233
+ }
2234
+ function seedRetainedFromRecord(state, config, indexName) {
2235
+ if (config.retainedPartitionIds === void 0 || state.indexesSeededFromRecord.has(indexName)) {
2236
+ return;
2237
+ }
2238
+ state.indexesSeededFromRecord.add(indexName);
2239
+ for (const partitionId of config.retainedPartitionIds(indexName)) {
2240
+ state.retainedPartitions.add(partitionKey(indexName, partitionId));
2241
+ }
2242
+ }
2243
+ function retainedPartitionIdOf(key, indexName) {
2244
+ if (!key.startsWith(`${indexName}:`)) {
2245
+ return null;
2246
+ }
2247
+ const partitionId = Number(key.slice(indexName.length + 1));
2248
+ return Number.isInteger(partitionId) ? partitionId : null;
2249
+ }
2250
+ function copyHasNoFurtherUse(table, partitionId, nodeId) {
2251
+ if (table.assignments.size === 0) {
2252
+ return true;
2253
+ }
2254
+ const assignment = table.assignments.get(partitionId);
2255
+ if (assignment === void 0) {
2256
+ return false;
2257
+ }
2258
+ return assignment.state === "ACTIVE" && assignment.primary !== null && assignment.primary !== nodeId;
2259
+ }
2260
+ function releaseSupersededCopies(state, config, table, currentKeys) {
2261
+ const nodeId = config.registration.nodeId;
2262
+ for (const key of [...state.retainedPartitions]) {
2263
+ const partitionId = retainedPartitionIdOf(key, table.indexName);
2264
+ if (partitionId === null) {
2265
+ continue;
1753
2266
  }
1754
- if (!succeeded) {
1755
- state.trackedPartitions.delete(key);
1756
- void restartBootstrapWhileOutOfSync(state, config, indexName, partitionId);
2267
+ if (currentKeys.has(key)) {
2268
+ state.retainedPartitions.delete(key);
2269
+ continue;
1757
2270
  }
1758
- }).catch(() => {
1759
- if (state.activeBootstraps.get(key) === bootstrapState) {
1760
- state.activeBootstraps.delete(key);
2271
+ if (!copyHasNoFurtherUse(table, partitionId, nodeId)) {
2272
+ continue;
1761
2273
  }
1762
- state.trackedPartitions.delete(key);
1763
- void restartBootstrapWhileOutOfSync(state, config, indexName, partitionId);
1764
- });
2274
+ state.retainedPartitions.delete(key);
2275
+ if (config.onRemovePartition !== void 0) {
2276
+ config.onRemovePartition(table.indexName, partitionId);
2277
+ }
2278
+ }
1765
2279
  }
1766
- function removeUnassignedPartitions(state, config, indexName, currentKeys) {
2280
+ function removeDroppedPartitions(state, config, table, currentKeys) {
1767
2281
  for (const [key, tracked] of state.trackedPartitions) {
1768
- if (tracked.indexName !== indexName) {
2282
+ if (tracked.indexName !== table.indexName) {
1769
2283
  continue;
1770
2284
  }
1771
2285
  if (currentKeys.has(key)) {
1772
2286
  continue;
1773
2287
  }
1774
2288
  state.trackedPartitions.delete(key);
2289
+ state.pendingPartitions.delete(key);
1775
2290
  const activeBootstrap = state.activeBootstraps.get(key);
1776
2291
  if (activeBootstrap !== void 0) {
1777
2292
  abortBootstrapState(activeBootstrap);
1778
2293
  state.activeBootstraps.delete(key);
1779
2294
  }
2295
+ if (keepsCopyForRecovery(table, tracked.partitionId, config.registration.nodeId)) {
2296
+ state.retainedPartitions.add(key);
2297
+ continue;
2298
+ }
2299
+ state.retainedPartitions.delete(key);
1780
2300
  if (config.onRemovePartition !== void 0) {
1781
2301
  config.onRemovePartition(tracked.indexName, tracked.partitionId);
1782
2302
  }
1783
2303
  }
1784
2304
  }
1785
- function stopAllocationWatcher(state) {
1786
- state.stopped = true;
1787
- if (state.unwatchAllocation !== null) {
1788
- state.unwatchAllocation();
1789
- state.unwatchAllocation = null;
1790
- }
1791
- if (state.debounceTimer !== null) {
1792
- clearTimeout(state.debounceTimer);
1793
- state.debounceTimer = null;
1794
- }
1795
- for (const [timer, resolve] of state.restartWaiters) {
1796
- clearTimeout(timer);
1797
- resolve();
1798
- }
1799
- state.restartWaiters.clear();
1800
- for (const bootstrapState of state.activeBootstraps.values()) {
1801
- abortBootstrapState(bootstrapState);
1802
- }
1803
- state.activeBootstraps.clear();
1804
- state.trackedPartitions.clear();
1805
- state.pendingTables.clear();
1806
- }
1807
- function processInitialAllocations(state, config, tables) {
1808
- for (const table of tables) {
1809
- processAllocationChange(state, config, table);
1810
- }
1811
- }
1812
- function waitBeforeBootstrapRestart(state, config) {
1813
- const delayMs = Math.min(config.bootstrapRetryMaxMs, config.bootstrapRetryBaseMs);
1814
- if (delayMs <= 0) {
1815
- return Promise.resolve();
1816
- }
1817
- return new Promise((resolve) => {
1818
- const timer = setTimeout(() => {
1819
- state.restartWaiters.delete(timer);
1820
- resolve();
1821
- }, delayMs);
1822
- timer.unref?.();
1823
- state.restartWaiters.set(timer, resolve);
2305
+
2306
+ // src/distribution/cluster/node-lifecycle/allocation-watcher/index.ts
2307
+ async function startAllocationWatcher(state, config) {
2308
+ const unwatchAllocation = await config.coordinator.watchAllocation((event) => {
2309
+ if (state.stopped) {
2310
+ return;
2311
+ }
2312
+ scheduleProcessAllocation(state, config, event.table);
1824
2313
  });
2314
+ state.unwatchAllocation = unwatchAllocation;
1825
2315
  }
1826
- async function restartBootstrapWhileOutOfSync(state, config, indexName, partitionId) {
1827
- if (state.stopped) {
1828
- return;
1829
- }
1830
- await waitBeforeBootstrapRestart(state, config);
1831
- if (state.stopped) {
1832
- return;
1833
- }
1834
- const nodeId = config.registration.nodeId;
1835
- let table;
1836
- try {
1837
- table = await config.coordinator.getAllocation(indexName);
1838
- } catch (error) {
1839
- if (config.onError !== void 0) {
1840
- config.onError(error);
1841
- }
1842
- if (!state.stopped) {
1843
- void restartBootstrapWhileOutOfSync(state, config, indexName, partitionId);
1844
- }
1845
- return;
1846
- }
1847
- if (state.stopped || table === null) {
1848
- return;
1849
- }
1850
- const assignment = table.assignments.get(partitionId);
1851
- if (assignment === void 0 || assignment.primary === null || assignment.primary === nodeId) {
1852
- return;
1853
- }
1854
- if (!assignment.replicas.includes(nodeId) || assignment.inSyncSet.includes(nodeId)) {
1855
- return;
1856
- }
1857
- if (assignment.state !== "ACTIVE" && assignment.state !== "INITIALISING" && assignment.state !== "MIGRATING") {
1858
- return;
1859
- }
1860
- if (state.activeBootstraps.has(partitionKey(indexName, partitionId))) {
1861
- return;
2316
+ function scheduleProcessAllocation(state, config, table) {
2317
+ state.pendingTables.set(table.indexName, table);
2318
+ if (state.debounceTimer !== null) {
2319
+ clearTimeout(state.debounceTimer);
1862
2320
  }
1863
- startBootstrap(state, config, indexName, partitionId, assignment.primary);
2321
+ state.debounceTimer = setTimeout(() => {
2322
+ state.debounceTimer = null;
2323
+ const tablesToProcess = Array.from(state.pendingTables.values());
2324
+ state.pendingTables.clear();
2325
+ for (const pending of tablesToProcess) {
2326
+ processAllocationChange(state, config, pending);
2327
+ }
2328
+ }, config.allocationDebounceMs);
1864
2329
  }
1865
2330
 
1866
2331
  // src/distribution/cluster/node-lifecycle/heartbeat.ts
@@ -1892,6 +2357,7 @@ async function stopRegistrationHeartbeat(state) {
1892
2357
  // src/distribution/cluster/node-lifecycle/join.ts
1893
2358
  async function joinCluster(config, watcherState, heartbeatState) {
1894
2359
  await config.coordinator.registerNode(config.registration);
2360
+ config.onRegistered?.();
1895
2361
  startRegistrationHeartbeat(heartbeatState, config);
1896
2362
  const initialTables = await loadInitialAllocations(config);
1897
2363
  processInitialAllocations(watcherState, config, initialTables);
@@ -1899,7 +2365,8 @@ async function joinCluster(config, watcherState, heartbeatState) {
1899
2365
  }
1900
2366
  async function loadInitialAllocations(config) {
1901
2367
  const tables = [];
1902
- for (const indexName of config.knownIndexNames) {
2368
+ const indexNames = /* @__PURE__ */ new Set([...config.knownIndexNames, ...await config.coordinator.listSchemas()]);
2369
+ for (const indexName of indexNames) {
1903
2370
  const table = await config.coordinator.getAllocation(indexName);
1904
2371
  if (table !== null) {
1905
2372
  tables.push(table);
@@ -1923,6 +2390,7 @@ var DEFAULT_NODE_LIFECYCLE_CONFIG = {
1923
2390
  // src/distribution/cluster/node-lifecycle/index.ts
1924
2391
  function createDataNodeLifecycle(config) {
1925
2392
  let status = "stopped";
2393
+ let registered = false;
1926
2394
  let watcherState = createAllocationWatcherState();
1927
2395
  const heartbeatState = createRegistrationHeartbeatState();
1928
2396
  let operationLock = Promise.resolve();
@@ -1949,6 +2417,12 @@ function createDataNodeLifecycle(config) {
1949
2417
  get nodeId() {
1950
2418
  return config.registration.nodeId;
1951
2419
  },
2420
+ get registered() {
2421
+ return registered;
2422
+ },
2423
+ get pendingPartitionCount() {
2424
+ return watcherState.pendingPartitions.size;
2425
+ },
1952
2426
  join() {
1953
2427
  return withLock(async () => {
1954
2428
  if (status === "active" || status === "joining") {
@@ -1967,10 +2441,21 @@ function createDataNodeLifecycle(config) {
1967
2441
  }
1968
2442
  status = "joining";
1969
2443
  try {
1970
- await joinCluster(config, watcherState, heartbeatState);
2444
+ await joinCluster(
2445
+ {
2446
+ ...config,
2447
+ onRegistered: () => {
2448
+ registered = true;
2449
+ config.onRegistered?.();
2450
+ }
2451
+ },
2452
+ watcherState,
2453
+ heartbeatState
2454
+ );
1971
2455
  status = "active";
1972
2456
  } catch (error) {
1973
2457
  await stopRegistrationHeartbeat(heartbeatState);
2458
+ registered = false;
1974
2459
  status = "stopped";
1975
2460
  throw error;
1976
2461
  }
@@ -1987,6 +2472,7 @@ function createDataNodeLifecycle(config) {
1987
2472
  try {
1988
2473
  await leaveCluster(config);
1989
2474
  } finally {
2475
+ registered = false;
1990
2476
  status = "stopped";
1991
2477
  watcherState = createAllocationWatcherState();
1992
2478
  }
@@ -2005,6 +2491,7 @@ function createDataNodeLifecycle(config) {
2005
2491
  } catch (_) {
2006
2492
  }
2007
2493
  }
2494
+ registered = false;
2008
2495
  status = "shutdown";
2009
2496
  watcherState = createAllocationWatcherState();
2010
2497
  });
@@ -2061,6 +2548,9 @@ async function nodeHasOtherPartitions(indexName, removedPartitionId, deps) {
2061
2548
  if (assignment.replicas.includes(deps.nodeId)) {
2062
2549
  return true;
2063
2550
  }
2551
+ if (holdsLastCopy(assignment, deps.nodeId)) {
2552
+ return true;
2553
+ }
2064
2554
  }
2065
2555
  return false;
2066
2556
  }
@@ -3815,6 +4305,22 @@ async function executeBootstrapSync(state, entry, indexName, partitionId, primar
3815
4305
  return true;
3816
4306
  }
3817
4307
 
4308
+ // src/distribution/cluster-node/routable-allocation.ts
4309
+ async function routableAllocation(coordinator, indexName) {
4310
+ const table = await coordinator.getAllocation(indexName);
4311
+ if (table !== null && table.assignments.size > 0) {
4312
+ return table;
4313
+ }
4314
+ if (await getIndexMetadata(coordinator, indexName) === null) {
4315
+ return null;
4316
+ }
4317
+ throw new NarsilError(
4318
+ ErrorCodes.QUERY_ROUTING_FAILED,
4319
+ `The cluster owns index '${indexName}' and holds no allocation table for it yet, so this node cannot place a read or a write`,
4320
+ { indexName }
4321
+ );
4322
+ }
4323
+
3818
4324
  // src/distribution/cluster-node/write-routing/assignment.ts
3819
4325
  function resolvePartitionId(docId, partitionCount) {
3820
4326
  return fnv1a(docId) % partitionCount;
@@ -3837,8 +4343,8 @@ function requireAssignedPrimary(assignment, indexName, partitionId) {
3837
4343
  return assignment;
3838
4344
  }
3839
4345
  async function resolvePrimaryAssignment(indexName, docId, deps, requireLocalPrimary) {
3840
- const table = await deps.coordinator.getAllocation(indexName);
3841
- if (table === null || table.assignments.size === 0) {
4346
+ const table = await routableAllocation(deps.coordinator, indexName);
4347
+ if (table === null) {
3842
4348
  if (requireLocalPrimary) {
3843
4349
  throw new NarsilError(
3844
4350
  ErrorCodes.QUERY_ROUTING_FAILED,
@@ -3965,7 +4471,7 @@ async function sendToController(payload, controllerNodeId, deps) {
3965
4471
  }
3966
4472
  return false;
3967
4473
  }
3968
- function withTimeout(work, timeoutMs, fallback) {
4474
+ function withTimeout2(work, timeoutMs, fallback) {
3969
4475
  return new Promise((resolve) => {
3970
4476
  const timer = setTimeout(() => resolve(fallback), timeoutMs);
3971
4477
  timer.unref?.();
@@ -4020,9 +4526,9 @@ async function proposeAdmission(state, deps, indexName, partitionId, assignment,
4020
4526
  appliedSeqNo: cursor.appliedSeqNo,
4021
4527
  commitPoint: log.commitPoint
4022
4528
  };
4023
- const accepted = await withTimeout(sendToController(payload, controllerNodeId, deps), ADMISSION_TIMEOUT_MS, false);
4529
+ const accepted = await withTimeout2(sendToController(payload, controllerNodeId, deps), ADMISSION_TIMEOUT_MS, false);
4024
4530
  if (accepted && cursor.syncEpoch !== proposedEpoch) {
4025
- await withTimeout(withdrawAdmission(payload, controllerNodeId, deps), ADMISSION_TIMEOUT_MS, void 0);
4531
+ await withTimeout2(withdrawAdmission(payload, controllerNodeId, deps), ADMISSION_TIMEOUT_MS, void 0);
4026
4532
  }
4027
4533
  } finally {
4028
4534
  clearPendingAdmission(state, indexName, partitionId, replicaNodeId);
@@ -4230,6 +4736,114 @@ async function stopCatchUpPump(state) {
4230
4736
  }
4231
4737
  }
4232
4738
 
4739
+ // src/distribution/cluster-node/cluster-namespace.ts
4740
+ function readinessOf(deps) {
4741
+ if (deps.isShutdown()) {
4742
+ return "LEAVING";
4743
+ }
4744
+ const lifecycle = deps.lifecycle();
4745
+ if (lifecycle.status === "leaving" || lifecycle.status === "shutdown") {
4746
+ return "LEAVING";
4747
+ }
4748
+ if (!lifecycle.registered) {
4749
+ return "STARTING";
4750
+ }
4751
+ if (lifecycle.status !== "active" || !deps.isServing() || lifecycle.pendingPartitionCount > 0) {
4752
+ return "JOINING";
4753
+ }
4754
+ return "SERVING";
4755
+ }
4756
+ function createClusterNamespace(deps) {
4757
+ return {
4758
+ async getAllocation(indexName) {
4759
+ return deps.trackOp(null, () => deps.coordinator.getAllocation(indexName));
4760
+ },
4761
+ getNodeInfo() {
4762
+ return { nodeId: deps.nodeId, roles: [...deps.roles], status: deps.lifecycle().status };
4763
+ },
4764
+ isControllerActive() {
4765
+ return deps.controller()?.isActive ?? false;
4766
+ },
4767
+ getReadiness() {
4768
+ return readinessOf(deps);
4769
+ },
4770
+ async listNodes() {
4771
+ return deps.trackOp(null, () => deps.coordinator.listNodes());
4772
+ },
4773
+ async getControllerNodeId() {
4774
+ return deps.trackOp(null, () => deps.coordinator.getLeaseHolder(CONTROLLER_LEASE_KEY));
4775
+ }
4776
+ };
4777
+ }
4778
+
4779
+ // src/distribution/cluster-node/error-forwarding.ts
4780
+ function createErrorForwarder(onError) {
4781
+ return (error) => {
4782
+ if (onError === void 0) {
4783
+ return;
4784
+ }
4785
+ onError(error instanceof Error ? error : new Error(String(error)));
4786
+ };
4787
+ }
4788
+ function createAllocationErrorForwarder(forwardOnError) {
4789
+ return (indexName, error) => {
4790
+ const code = error instanceof NarsilError ? error.code : ErrorCodes.ALLOCATION_FAILED;
4791
+ const details = error instanceof NarsilError ? error.details : {};
4792
+ const message = error instanceof Error ? error.message : String(error);
4793
+ forwardOnError(
4794
+ new NarsilError(code, `The controller could not allocate index '${indexName}': ${message}`, {
4795
+ ...details,
4796
+ indexName
4797
+ })
4798
+ );
4799
+ };
4800
+ }
4801
+
4802
+ // src/distribution/cluster-node/held-partitions.ts
4803
+ function createHeldPartitionRecord(core) {
4804
+ const writesByIndex = /* @__PURE__ */ new Map();
4805
+ const indexesBehindOnDisk = /* @__PURE__ */ new Set();
4806
+ function queueWrite(indexName, next) {
4807
+ const previous = writesByIndex.get(indexName) ?? Promise.resolve();
4808
+ const write = previous.then(() => writeHeldPartitions(core, indexName, next, indexesBehindOnDisk));
4809
+ writesByIndex.set(
4810
+ indexName,
4811
+ write.catch(() => void 0)
4812
+ );
4813
+ return write;
4814
+ }
4815
+ return {
4816
+ record(indexName, partitionId) {
4817
+ return queueWrite(indexName, (held) => held.includes(partitionId) ? null : [...held, partitionId]);
4818
+ },
4819
+ forget(indexName, partitionId) {
4820
+ return queueWrite(indexName, (held) => held.includes(partitionId) ? held.filter((id) => id !== partitionId) : null);
4821
+ },
4822
+ held(indexName) {
4823
+ return core.indexRegistry.get(indexName)?.heldPartitions ?? void 0;
4824
+ }
4825
+ };
4826
+ }
4827
+ async function writeHeldPartitions(core, indexName, next, indexesBehindOnDisk) {
4828
+ const entry = core.indexRegistry.get(indexName);
4829
+ if (entry === void 0) {
4830
+ return;
4831
+ }
4832
+ const updated = next(entry.heldPartitions ?? []);
4833
+ if (updated === null && !indexesBehindOnDisk.has(indexName)) {
4834
+ return;
4835
+ }
4836
+ if (updated !== null) {
4837
+ core.indexRegistry.set(indexName, { ...entry, heldPartitions: [...updated].sort((a, b) => a - b) });
4838
+ }
4839
+ if (core.durability === null) {
4840
+ return;
4841
+ }
4842
+ indexesBehindOnDisk.add(indexName);
4843
+ await core.durability.manager.persistMetadata(indexName);
4844
+ indexesBehindOnDisk.delete(indexName);
4845
+ }
4846
+
4233
4847
  // src/distribution/cluster-node/local-engine.ts
4234
4848
  async function createClusterLocalEngine(config) {
4235
4849
  const core = createEngineCore(config);
@@ -4241,10 +4855,15 @@ async function createClusterLocalEngine(config) {
4241
4855
  }
4242
4856
  await core.analysisRebuild.reviewStaleIndexes();
4243
4857
  const engine = createNarsilFromCore(core, config);
4858
+ const heldPartitions = createHeldPartitionRecord(core);
4244
4859
  return Object.assign(engine, {
4245
4860
  createIndexWithUuid: (name, indexConfig, indexUuid) => createEngineIndex(core, config, name, indexConfig, indexUuid),
4246
4861
  indexUuidOf: (indexName) => core.indexRegistry.get(indexName)?.indexUuid,
4247
4862
  stampIndexUuid: (indexName, indexUuid) => stampIndexUuid(core, indexName, indexUuid),
4863
+ highestPersistedSeqNoOf: (indexName, partitionId) => core.durability?.manager.highestPersistedSeqNo(indexName, partitionId) ?? 0,
4864
+ heldPartitionsOf: (indexName) => heldPartitions.held(indexName),
4865
+ recordHeldPartition: (indexName, partitionId) => heldPartitions.record(indexName, partitionId),
4866
+ forgetHeldPartition: (indexName, partitionId) => heldPartitions.forget(indexName, partitionId),
4248
4867
  applyReplicationEntry: (entry) => applyReplicationEntry(core, entry),
4249
4868
  serializeReplicationPartition: (indexName, partitionId) => serializeReplicationPartition(core, indexName, partitionId),
4250
4869
  restoreReplicationPartition: (indexName, partitionId, bytes, schema, partitionCount) => restoreReplicationPartition(core, engine, indexName, partitionId, bytes, schema, partitionCount),
@@ -5086,6 +5705,28 @@ async function streamValidatedSnapshotRequest(message, sink, request, deps, opti
5086
5705
  }
5087
5706
  await acquireAndStream(message, sink, request, deps, options);
5088
5707
  }
5708
+ function describeLocalCopy(deps, indexName) {
5709
+ const absent = { indexName, indexUuid: null, partitionIds: [] };
5710
+ if (!deps.engine.listIndexes().some((index) => index.name === indexName)) {
5711
+ return absent;
5712
+ }
5713
+ const indexUuid = deps.engine.indexUuidOf(indexName);
5714
+ if (indexUuid === null || indexUuid === void 0) {
5715
+ return absent;
5716
+ }
5717
+ const held = new Set(deps.engine.heldPartitionsOf(indexName) ?? []);
5718
+ for (const partition of deps.engine.getPartitionStats(indexName)) {
5719
+ if (partition.documentCount > 0) {
5720
+ held.add(partition.partitionId);
5721
+ }
5722
+ }
5723
+ return { indexName, indexUuid, partitionIds: [...held].sort((left, right) => left - right) };
5724
+ }
5725
+ async function handlePartitionStores(message, respond, deps) {
5726
+ const payload = validatePartitionStoresPayload(decode(message.payload));
5727
+ const result = payload === null ? { indexName: "", indexUuid: null, partitionIds: [] } : describeLocalCopy(deps, payload.indexName);
5728
+ await respond(createPartitionStoresResultMessage(result, deps.nodeId, message.requestId));
5729
+ }
5089
5730
 
5090
5731
  // src/distribution/query/validators/common.ts
5091
5732
  var MAX_PARTITION_IDS = MAX_PARTITION_COUNT;
@@ -6738,6 +7379,13 @@ async function handleSyncRequestMessage(message, respond, deps) {
6738
7379
  }
6739
7380
  );
6740
7381
  }
7382
+ if (!deps.engine.listIndexes().some((index) => index.name === request.indexName)) {
7383
+ throw new NarsilError(
7384
+ ErrorCodes.SNAPSHOT_SYNC_INDEX_NOT_FOUND,
7385
+ `Node '${deps.nodeId}' holds no copy of index '${request.indexName}' yet, so it cannot serve a sync for partition ${request.partitionId}`,
7386
+ { indexName: request.indexName, partitionId: request.partitionId, primaryNodeId: deps.nodeId }
7387
+ );
7388
+ }
6741
7389
  if (message.sourceId !== deps.nodeId) {
6742
7390
  recordReplicaPosition(
6743
7391
  deps.writeDeps.catchUp,
@@ -6798,6 +7446,55 @@ async function sendSyncEntriesResponse(message, respond, deps, entries) {
6798
7446
  });
6799
7447
  }
6800
7448
 
7449
+ // src/distribution/cluster-node/allocation-wait.ts
7450
+ var DEFAULT_CREATE_INDEX_WAIT_MS = 3e4;
7451
+ function everyPartitionActive(table, partitionCount) {
7452
+ if (table === null || table.assignments.size < partitionCount) {
7453
+ return false;
7454
+ }
7455
+ for (const assignment of table.assignments.values()) {
7456
+ if (assignment.state !== "ACTIVE") {
7457
+ return false;
7458
+ }
7459
+ }
7460
+ return true;
7461
+ }
7462
+ async function allocationPossible(coordinator) {
7463
+ const nodes = await coordinator.listNodes();
7464
+ return nodes.some((node) => node.roles.includes("controller")) && nodes.some((node) => node.roles.includes("data"));
7465
+ }
7466
+ async function waitForServingAllocation(coordinator, indexName, partitionCount, timeoutMs) {
7467
+ if (timeoutMs <= 0) {
7468
+ return;
7469
+ }
7470
+ let settle = () => void 0;
7471
+ const outcome = new Promise((resolve) => {
7472
+ settle = resolve;
7473
+ });
7474
+ const deadline = setTimeout(settle, timeoutMs);
7475
+ deadline.unref?.();
7476
+ let unwatch = () => void 0;
7477
+ try {
7478
+ unwatch = await coordinator.watchAllocation((event) => {
7479
+ if (event.indexName === indexName && everyPartitionActive(event.table, partitionCount)) {
7480
+ settle();
7481
+ }
7482
+ });
7483
+ if (everyPartitionActive(await coordinator.getAllocation(indexName), partitionCount)) {
7484
+ return;
7485
+ }
7486
+ if (!await allocationPossible(coordinator)) {
7487
+ return;
7488
+ }
7489
+ await outcome;
7490
+ } catch (_) {
7491
+ return;
7492
+ } finally {
7493
+ clearTimeout(deadline);
7494
+ unwatch();
7495
+ }
7496
+ }
7497
+
6801
7498
  // src/distribution/cluster-node/types/defaults.ts
6802
7499
  var DEFAULT_PARTITION_COUNT = 5;
6803
7500
  var DEFAULT_REPLICATION_FACTOR = 1;
@@ -7677,8 +8374,8 @@ function collectGroupFailure(failed, docIds, err, indexName, fallbackCode) {
7677
8374
  }
7678
8375
  }
7679
8376
  async function routeInsertBatch(indexName, documents, deps, options) {
7680
- const table = await deps.coordinator.getAllocation(indexName);
7681
- if (table === null || table.assignments.size === 0) {
8377
+ const table = await routableAllocation(deps.coordinator, indexName);
8378
+ if (table === null) {
7682
8379
  return deps.engine.insertBatch(indexName, documents, options);
7683
8380
  }
7684
8381
  const withIds = documents.map((doc) => ({
@@ -7716,8 +8413,8 @@ async function routeInsertBatch(indexName, documents, deps, options) {
7716
8413
  return { succeeded, failed };
7717
8414
  }
7718
8415
  async function routeRemoveBatch(indexName, docIds, deps) {
7719
- const table = await deps.coordinator.getAllocation(indexName);
7720
- if (table === null || table.assignments.size === 0) {
8416
+ const table = await routableAllocation(deps.coordinator, indexName);
8417
+ if (table === null) {
7721
8418
  return deps.engine.removeBatch(indexName, docIds);
7722
8419
  }
7723
8420
  const routed = routeBatchItems(table, indexName, docIds, (docId) => docId, deps.nodeId);
@@ -7745,8 +8442,8 @@ async function routeRemoveBatch(indexName, docIds, deps) {
7745
8442
  return { succeeded, failed };
7746
8443
  }
7747
8444
  async function routeUpdateBatch(indexName, updates, deps) {
7748
- const table = await deps.coordinator.getAllocation(indexName);
7749
- if (table === null || table.assignments.size === 0) {
8445
+ const table = await routableAllocation(deps.coordinator, indexName);
8446
+ if (table === null) {
7750
8447
  return deps.engine.updateBatch(indexName, updates);
7751
8448
  }
7752
8449
  const routed = routeBatchItems(table, indexName, updates, (update) => update.docId, deps.nodeId);
@@ -7847,6 +8544,12 @@ async function routeCreateIndex(name, config, options, coordinator, engine) {
7847
8544
  }
7848
8545
  throw createErr;
7849
8546
  }
8547
+ await waitForServingAllocation(
8548
+ coordinator,
8549
+ name,
8550
+ partitionCount,
8551
+ options?.waitForServingMs ?? DEFAULT_CREATE_INDEX_WAIT_MS
8552
+ );
7850
8553
  }
7851
8554
  async function withdrawPartialIndex(name, coordinator, engine) {
7852
8555
  try {
@@ -8124,6 +8827,9 @@ function createDataNodeHandler(deps) {
8124
8827
  case QueryMessageTypes.PREFLIGHT:
8125
8828
  await handlePreflight(message, respond, deps);
8126
8829
  return;
8830
+ case ClusterMessageTypes.PARTITION_STORES:
8831
+ await handlePartitionStores(message, respond, deps);
8832
+ return;
8127
8833
  default:
8128
8834
  return;
8129
8835
  }
@@ -8379,22 +9085,32 @@ async function fanOutSearch(indexName, params, globalStats, facetShardSize, node
8379
9085
  }
8380
9086
  return Promise.all(promises);
8381
9087
  }
8382
- function buildCoverage(totalPartitions, unavailableCount, outcomes) {
9088
+ var OUTCOME_SEVERITY = { success: 0, timeout: 1, failed: 2 };
9089
+ function buildCoverage(totalPartitions, unavailablePartitions, ...outcomeSets) {
9090
+ const worstByPartition = /* @__PURE__ */ new Map();
9091
+ for (const partitionId of unavailablePartitions) {
9092
+ worstByPartition.set(partitionId, "failed");
9093
+ }
9094
+ for (const outcomes of outcomeSets) {
9095
+ for (const outcome of outcomes) {
9096
+ for (const partitionId of outcome.partitionIds) {
9097
+ const worstSoFar = worstByPartition.get(partitionId);
9098
+ if (worstSoFar === void 0 || OUTCOME_SEVERITY[outcome.status] > OUTCOME_SEVERITY[worstSoFar]) {
9099
+ worstByPartition.set(partitionId, outcome.status);
9100
+ }
9101
+ }
9102
+ }
9103
+ }
8383
9104
  let queriedPartitions = 0;
8384
9105
  let timedOutPartitions = 0;
8385
- let failedPartitions = unavailableCount;
8386
- for (const outcome of outcomes) {
8387
- const partitionCount = outcome.partitionIds.length;
8388
- switch (outcome.status) {
8389
- case "success":
8390
- queriedPartitions += partitionCount;
8391
- break;
8392
- case "timeout":
8393
- timedOutPartitions += partitionCount;
8394
- break;
8395
- case "failed":
8396
- failedPartitions += partitionCount;
8397
- break;
9106
+ let failedPartitions = 0;
9107
+ for (const status of worstByPartition.values()) {
9108
+ if (status === "success") {
9109
+ queriedPartitions += 1;
9110
+ } else if (status === "timeout") {
9111
+ timedOutPartitions += 1;
9112
+ } else {
9113
+ failedPartitions += 1;
8398
9114
  }
8399
9115
  }
8400
9116
  return {
@@ -8412,7 +9128,7 @@ async function sendWithTimeout(transport, targets, message, timeout, validate) {
8412
9128
  timerId.unref();
8413
9129
  }
8414
9130
  });
8415
- const sendPromise = sendToFirstReachableTarget(transport, targets, message).then((response) => {
9131
+ const sendPromise = sendToFirstReachableTarget2(transport, targets, message).then((response) => {
8416
9132
  const decoded = decode(response.payload);
8417
9133
  return validate(decoded);
8418
9134
  });
@@ -8431,7 +9147,7 @@ async function resolveTargets(nodeId, deps) {
8431
9147
  const targets = await deps.resolveNodeTargets(nodeId);
8432
9148
  return targets.length > 0 ? targets : [nodeId];
8433
9149
  }
8434
- async function sendToFirstReachableTarget(transport, targets, message) {
9150
+ async function sendToFirstReachableTarget2(transport, targets, message) {
8435
9151
  let lastError;
8436
9152
  for (const target of targets) {
8437
9153
  try {
@@ -8797,7 +9513,7 @@ async function executeSingleFanOut(indexName, params, limit, offset, facetShardS
8797
9513
  if (sortFields !== null) {
8798
9514
  failOutcomesMissingSortValues(outcomes);
8799
9515
  }
8800
- const coverage = buildCoverage(totalPartitions, routing.unavailablePartitions.length, outcomes);
9516
+ const coverage = buildCoverage(totalPartitions, routing.unavailablePartitions, outcomes);
8801
9517
  if (!config.allowPartialResults) {
8802
9518
  const failedCount = coverage.timedOutPartitions + coverage.failedPartitions;
8803
9519
  if (failedCount > 0) {
@@ -8871,9 +9587,7 @@ async function executeHybridQuery(indexName, params, limit, offset, facetShardSi
8871
9587
  fanOutSearch(indexName, textParams, textGlobalStats, facetShardSize, routing.nodeToPartitions, deps, config),
8872
9588
  fanOutSearch(indexName, vectorParams, null, null, routing.nodeToPartitions, deps, config)
8873
9589
  ]);
8874
- const textCoverage = buildCoverage(totalPartitions, routing.unavailablePartitions.length, textOutcomes);
8875
- const vectorCoverage = buildCoverage(totalPartitions, routing.unavailablePartitions.length, vectorOutcomes);
8876
- const coverage = worstCaseCoverage(textCoverage, vectorCoverage);
9590
+ const coverage = buildCoverage(totalPartitions, routing.unavailablePartitions, textOutcomes, vectorOutcomes);
8877
9591
  if (!config.allowPartialResults) {
8878
9592
  const failedCount = coverage.timedOutPartitions + coverage.failedPartitions;
8879
9593
  if (failedCount > 0) {
@@ -8935,32 +9649,6 @@ async function executeHybridQuery(indexName, params, limit, offset, facetShardSi
8935
9649
  coverage
8936
9650
  };
8937
9651
  }
8938
- function worstCaseCoverage(a, b) {
8939
- const failed = Math.max(a.failedPartitions, b.failedPartitions);
8940
- const timedOut = Math.max(a.timedOutPartitions, b.timedOutPartitions);
8941
- const queried = Math.max(0, a.totalPartitions - failed - timedOut);
8942
- return {
8943
- totalPartitions: a.totalPartitions,
8944
- queriedPartitions: queried,
8945
- timedOutPartitions: timedOut,
8946
- failedPartitions: failed
8947
- };
8948
- }
8949
- function servesAnyPartition(allocation) {
8950
- if (allocation === null || allocation.assignments.size === 0) {
8951
- return false;
8952
- }
8953
- for (const [, assignment] of allocation.assignments) {
8954
- if (assignment.state === "ACTIVE") {
8955
- return true;
8956
- }
8957
- }
8958
- return false;
8959
- }
8960
- async function activeAllocation(deps, indexName) {
8961
- const allocation = await deps.config.coordinator.getAllocation(indexName);
8962
- return servesAnyPartition(allocation) ? allocation : null;
8963
- }
8964
9652
  function strictScatterGroups(allocation, indexName) {
8965
9653
  const routing = selectReplicasForQuery(allocation, randomSelector);
8966
9654
  if (routing.unavailablePartitions.length > 0) {
@@ -9054,7 +9742,7 @@ async function gatherPartitionCounts(deps, indexName, allocation) {
9054
9742
  return { countsByPartition, language };
9055
9743
  }
9056
9744
  async function countCluster(deps, indexName) {
9057
- const allocation = await activeAllocation(deps, indexName);
9745
+ const allocation = await routableAllocation(deps.config.coordinator, indexName);
9058
9746
  if (allocation === null) {
9059
9747
  return deps.engine.countDocuments(indexName);
9060
9748
  }
@@ -9066,7 +9754,7 @@ async function countCluster(deps, indexName) {
9066
9754
  return total;
9067
9755
  }
9068
9756
  async function statsCluster(deps, indexName) {
9069
- const allocation = await activeAllocation(deps, indexName);
9757
+ const allocation = await routableAllocation(deps.config.coordinator, indexName);
9070
9758
  if (allocation === null) {
9071
9759
  return deps.engine.getStats(indexName);
9072
9760
  }
@@ -9092,7 +9780,7 @@ async function statsCluster(deps, indexName) {
9092
9780
  };
9093
9781
  }
9094
9782
  async function partitionStatsCluster(deps, indexName) {
9095
- const allocation = await activeAllocation(deps, indexName);
9783
+ const allocation = await routableAllocation(deps.config.coordinator, indexName);
9096
9784
  if (allocation === null) {
9097
9785
  return deps.engine.getPartitionStats(indexName);
9098
9786
  }
@@ -9146,7 +9834,7 @@ async function gatherLocalPage(deps, indexName, payload, partitionIds, sortField
9146
9834
  return { entries, total: result.total, hasMore: result.cursor !== null };
9147
9835
  }
9148
9836
  async function listCluster(deps, indexName, params) {
9149
- const allocation = await activeAllocation(deps, indexName);
9837
+ const allocation = await routableAllocation(deps.config.coordinator, indexName);
9150
9838
  if (allocation === null) {
9151
9839
  return deps.engine.listDocuments(indexName, params);
9152
9840
  }
@@ -9233,7 +9921,7 @@ function suggestNodeLimit(clientLimit) {
9233
9921
  return Math.min(oversampled, MAX_SUGGEST_SCATTER_LIMIT);
9234
9922
  }
9235
9923
  async function suggestCluster(deps, indexName, params) {
9236
- const allocation = await activeAllocation(deps, indexName);
9924
+ const allocation = await routableAllocation(deps.config.coordinator, indexName);
9237
9925
  if (allocation === null) {
9238
9926
  return deps.engine.suggest(indexName, params);
9239
9927
  }
@@ -9277,7 +9965,7 @@ async function suggestCluster(deps, indexName, params) {
9277
9965
  return result;
9278
9966
  }
9279
9967
  async function preflightCluster(deps, indexName, params) {
9280
- const allocation = await activeAllocation(deps, indexName);
9968
+ const allocation = await routableAllocation(deps.config.coordinator, indexName);
9281
9969
  if (allocation === null) {
9282
9970
  return deps.engine.preflight(indexName, params);
9283
9971
  }
@@ -9312,7 +10000,7 @@ async function preflightCluster(deps, indexName, params) {
9312
10000
 
9313
10001
  // src/distribution/cluster-node/reads/index.ts
9314
10002
  async function readClusterDocuments(deps, indexName, docIds) {
9315
- const allocation = await activeAllocation(deps, indexName);
10003
+ const allocation = await routableAllocation(deps.config.coordinator, indexName);
9316
10004
  if (allocation === null) {
9317
10005
  return deps.engine.getMultiple(indexName, docIds);
9318
10006
  }
@@ -9331,23 +10019,11 @@ function routingConfig(query) {
9331
10019
  }
9332
10020
  return config;
9333
10021
  }
9334
- async function queryLocalCopy(deps, indexName, params, allocation) {
9335
- const result = await deps.engine.query(indexName, params);
9336
- if (allocation === null || allocation.assignments.size === 0) {
9337
- return result;
9338
- }
9339
- const totalPartitions = allocation.assignments.size;
9340
- return {
9341
- ...result,
9342
- coverage: { totalPartitions, queriedPartitions: 0, timedOutPartitions: 0, failedPartitions: totalPartitions }
9343
- };
9344
- }
9345
10022
  async function queryCluster(deps, indexName, params) {
9346
- const table = await deps.config.coordinator.getAllocation(indexName);
9347
- if (!servesAnyPartition(table)) {
9348
- return queryLocalCopy(deps, indexName, params, table);
10023
+ const allocation = await routableAllocation(deps.config.coordinator, indexName);
10024
+ if (allocation === null) {
10025
+ return deps.engine.query(indexName, params);
9349
10026
  }
9350
- const allocation = table;
9351
10027
  const wireParams = localParamsToWire(params);
9352
10028
  const queryDeps = {
9353
10029
  transport: deps.config.transport,
@@ -9371,7 +10047,7 @@ async function queryCluster(deps, indexName, params) {
9371
10047
  // src/distribution/cluster-node/clear.ts
9372
10048
  var CLEAR_PAGE_SIZE = 1e3;
9373
10049
  async function clearCluster(readDeps, writeDeps, indexName) {
9374
- const allocation = await activeAllocation(readDeps, indexName);
10050
+ const allocation = await routableAllocation(readDeps.config.coordinator, indexName);
9375
10051
  if (allocation === null) {
9376
10052
  return readDeps.engine.clear(indexName);
9377
10053
  }
@@ -9481,6 +10157,53 @@ function createClusterNodeOperations(deps) {
9481
10157
  };
9482
10158
  }
9483
10159
 
10160
+ // src/distribution/cluster-node/primary-partition.ts
10161
+ async function preparePrimaryPartition(indexName, partitionId, deps) {
10162
+ const schema = await deps.coordinator.getSchema(indexName);
10163
+ if (schema === null) {
10164
+ return false;
10165
+ }
10166
+ const allocation = await deps.coordinator.getAllocation(indexName);
10167
+ if (allocation === null || allocation.assignments.size === 0) {
10168
+ return false;
10169
+ }
10170
+ const existing = deps.engine.listIndexes().find((index) => index.name === indexName);
10171
+ if (existing === void 0) {
10172
+ try {
10173
+ await deps.engine.createIndex(indexName, {
10174
+ schema,
10175
+ partitions: { maxPartitions: allocation.assignments.size }
10176
+ });
10177
+ } catch (error) {
10178
+ if (!(error instanceof NarsilError) || error.code !== ErrorCodes.INDEX_ALREADY_EXISTS) {
10179
+ deps.onError(error);
10180
+ return false;
10181
+ }
10182
+ }
10183
+ } else {
10184
+ const schemaError = validateRestoredSchema(deps.engine, indexName, deps.nodeId, schema);
10185
+ if (schemaError !== null) {
10186
+ deps.onError(schemaError);
10187
+ return false;
10188
+ }
10189
+ }
10190
+ resumeNumberingAboveOwnCopy(indexName, partitionId, allocation.assignments.get(partitionId), deps);
10191
+ return true;
10192
+ }
10193
+ function resumeNumberingAboveOwnCopy(indexName, partitionId, assignment, deps) {
10194
+ if (assignment === void 0) {
10195
+ return;
10196
+ }
10197
+ if (deps.replicationLogPosition(indexName, partitionId) > 0) {
10198
+ return;
10199
+ }
10200
+ const floor = Math.max(deps.engine.highestPersistedSeqNoOf(indexName, partitionId), assignment.commitPoint);
10201
+ if (floor <= 0) {
10202
+ return;
10203
+ }
10204
+ deps.seedReplicationLog(indexName, partitionId, floor + 1, assignment.primaryTerm);
10205
+ }
10206
+
9484
10207
  // src/distribution/replication/types.ts
9485
10208
  var DEFAULT_LOG_RETENTION_BYTES = 268435456;
9486
10209
 
@@ -9716,14 +10439,29 @@ function seedReplicationLog(replicationLogs, indexName, partitionId, startSeqNo,
9716
10439
  })
9717
10440
  );
9718
10441
  }
9719
-
9720
- // src/distribution/cluster-node/transport-listener.ts
9721
10442
  var CONTROLLER_MESSAGE_TYPES = /* @__PURE__ */ new Set([
9722
10443
  ReplicationMessageTypes.INSYNC_ADD,
9723
10444
  ReplicationMessageTypes.INSYNC_REMOVE,
9724
10445
  ClusterMessageTypes.BOOTSTRAP_COMPLETE
9725
10446
  ]);
9726
- function createMultiplexedControllerTransport(baseTransport) {
10447
+ function refuseMessage(nodeId, message, respond, code, text) {
10448
+ return respond({
10449
+ type: `${message.type}.error`,
10450
+ sourceId: nodeId,
10451
+ requestId: message.requestId,
10452
+ payload: encode({ error: true, code, message: text })
10453
+ });
10454
+ }
10455
+ function createNotReadyHandler(nodeId) {
10456
+ return (message, respond) => refuseMessage(
10457
+ nodeId,
10458
+ message,
10459
+ respond,
10460
+ ErrorCodes.NODE_NOT_READY,
10461
+ `Node '${nodeId}' has yet to finish joining the cluster, so it serves nothing`
10462
+ );
10463
+ }
10464
+ function createMultiplexedControllerTransport(baseTransport, nodeId) {
9727
10465
  let controllerHandler = null;
9728
10466
  const transport = {
9729
10467
  send(target, message) {
@@ -9750,9 +10488,17 @@ function createMultiplexedControllerTransport(baseTransport) {
9750
10488
  createHandler(dataHandler) {
9751
10489
  return async (message, respond) => {
9752
10490
  if (CONTROLLER_MESSAGE_TYPES.has(message.type)) {
9753
- if (controllerHandler !== null) {
9754
- await controllerHandler(message, createSingleResponseRespond(respond));
10491
+ if (controllerHandler === null) {
10492
+ await refuseMessage(
10493
+ nodeId,
10494
+ message,
10495
+ respond,
10496
+ ErrorCodes.NODE_NOT_CONTROLLER,
10497
+ `Node '${nodeId}' holds no controller lease, so it answers no controller message`
10498
+ );
10499
+ return;
9755
10500
  }
10501
+ await controllerHandler(message, createSingleResponseRespond(respond));
9756
10502
  return;
9757
10503
  }
9758
10504
  await dataHandler(message, respond);
@@ -9773,6 +10519,7 @@ function createSingleResponseRespond(respond) {
9773
10519
 
9774
10520
  // src/distribution/cluster-node/validate-config.ts
9775
10521
  var MAX_WAIT_FOR_ACTIVE_REPLICAS = 255;
10522
+ var MIN_CONTROLLER_LEASE_TTL_MS = 1e3;
9776
10523
  function validateClusterNodeConfig(config) {
9777
10524
  if (config.address.length === 0) {
9778
10525
  throw new NarsilError(ErrorCodes.CONFIG_INVALID, "ClusterNodeConfig.address must not be empty");
@@ -9835,6 +10582,23 @@ function validateClusterNodeConfig(config) {
9835
10582
  );
9836
10583
  }
9837
10584
  }
10585
+ if (config.controller !== void 0) {
10586
+ const { leaseTtlMs, standbyRetryMs } = config.controller;
10587
+ if (leaseTtlMs !== void 0 && (!Number.isInteger(leaseTtlMs) || leaseTtlMs < MIN_CONTROLLER_LEASE_TTL_MS)) {
10588
+ throw new NarsilError(
10589
+ ErrorCodes.CONFIG_INVALID,
10590
+ `ClusterNodeConfig.controller.leaseTtlMs must be an integer of at least ${MIN_CONTROLLER_LEASE_TTL_MS}`,
10591
+ { leaseTtlMs }
10592
+ );
10593
+ }
10594
+ if (standbyRetryMs !== void 0 && (!Number.isInteger(standbyRetryMs) || standbyRetryMs <= 0)) {
10595
+ throw new NarsilError(
10596
+ ErrorCodes.CONFIG_INVALID,
10597
+ "ClusterNodeConfig.controller.standbyRetryMs must be a positive integer",
10598
+ { standbyRetryMs }
10599
+ );
10600
+ }
10601
+ }
9838
10602
  }
9839
10603
 
9840
10604
  // src/distribution/cluster-node/index.ts
@@ -9860,13 +10624,8 @@ async function createClusterNode(config) {
9860
10624
  async function resolveNodeTargets3(targetNodeId) {
9861
10625
  return resolveNodeTargets2(config, targetNodeId);
9862
10626
  }
9863
- const forwardOnError = (error) => {
9864
- if (config.onError === void 0) {
9865
- return;
9866
- }
9867
- const wrapped = error instanceof Error ? error : new Error(String(error));
9868
- config.onError(wrapped);
9869
- };
10627
+ const forwardOnError = createErrorForwarder(config.onError);
10628
+ const forwardAllocationError = createAllocationErrorForwarder(forwardOnError);
9870
10629
  const registration = {
9871
10630
  nodeId,
9872
10631
  address: config.address,
@@ -9877,7 +10636,7 @@ async function createClusterNode(config) {
9877
10636
  };
9878
10637
  const hasDataRole = roles.includes("data");
9879
10638
  const hasControllerRole = roles.includes("controller");
9880
- const controllerTransport = hasDataRole && hasControllerRole ? createMultiplexedControllerTransport(config.transport) : null;
10639
+ const controllerTransport = hasDataRole ? createMultiplexedControllerTransport(config.transport, nodeId) : null;
9881
10640
  const writeDeps = {
9882
10641
  nodeId,
9883
10642
  coordinator: config.coordinator,
@@ -9891,9 +10650,16 @@ async function createClusterNode(config) {
9891
10650
  waitForActiveReplicas: config.replication?.waitForActiveReplicas
9892
10651
  };
9893
10652
  const orphanedIndexes = /* @__PURE__ */ new Set();
9894
- let lifecycle = null;
10653
+ const heldPartitionWrites = /* @__PURE__ */ new Set();
10654
+ function trackHeldPartitionWrite(write) {
10655
+ const settled = write.catch(forwardOnError);
10656
+ heldPartitionWrites.add(settled);
10657
+ void settled.finally(() => heldPartitionWrites.delete(settled));
10658
+ }
9895
10659
  let controller = null;
9896
10660
  let unregisterHandler = null;
10661
+ let unregisterNotReadyHandler = null;
10662
+ let serving = false;
9897
10663
  let isShutdown = false;
9898
10664
  let activeOps = 0;
9899
10665
  let drainResolve = null;
@@ -9909,76 +10675,59 @@ async function createClusterNode(config) {
9909
10675
  restoreReplicationPartition: engine.restoreReplicationPartition,
9910
10676
  onError: forwardOnError
9911
10677
  };
10678
+ const primaryPartitionDeps = {
10679
+ engine,
10680
+ coordinator: config.coordinator,
10681
+ nodeId,
10682
+ seedReplicationLog: seedReplicationLog2,
10683
+ replicationLogPosition: (indexName, partitionId) => getReplicationLog2(indexName, partitionId).newestSeqNo ?? 0,
10684
+ onError: forwardOnError
10685
+ };
9912
10686
  async function bootstrapPartition2(indexName, partitionId, primaryNodeId) {
9913
- const succeeded = primaryNodeId === nodeId ? await preparePrimaryPartition(indexName) : await runBootstrapSync(bootstrapSyncState, indexName, partitionId, primaryNodeId, bootstrapSyncDeps);
10687
+ const succeeded = primaryNodeId === nodeId ? await preparePrimaryPartition(indexName, partitionId, primaryPartitionDeps) : await runBootstrapSync(bootstrapSyncState, indexName, partitionId, primaryNodeId, bootstrapSyncDeps);
9914
10688
  if (succeeded) {
9915
10689
  await adoptClusterIdentity(engine, config.coordinator, indexName);
10690
+ await engine.recordHeldPartition(indexName, partitionId);
9916
10691
  }
9917
10692
  return succeeded;
9918
10693
  }
9919
- async function preparePrimaryPartition(indexName) {
9920
- const schema = await config.coordinator.getSchema(indexName);
9921
- if (schema === null) {
9922
- return false;
9923
- }
9924
- const allocation = await config.coordinator.getAllocation(indexName);
9925
- if (allocation === null || allocation.assignments.size === 0) {
9926
- return false;
9927
- }
9928
- const existing = engine.listIndexes().find((index) => index.name === indexName);
9929
- if (existing !== void 0) {
9930
- const schemaError = validateRestoredSchema(engine, indexName, nodeId, schema);
9931
- if (schemaError !== null) {
9932
- forwardOnError(schemaError);
9933
- return false;
9934
- }
9935
- return true;
9936
- }
9937
- try {
9938
- await engine.createIndex(indexName, {
9939
- schema,
9940
- partitions: { maxPartitions: allocation.assignments.size }
10694
+ const lifecycle = createDataNodeLifecycle({
10695
+ registration,
10696
+ coordinator: config.coordinator,
10697
+ transport: config.transport,
10698
+ knownIndexNames: [],
10699
+ bootstrapRetryBaseMs: DEFAULT_NODE_LIFECYCLE_CONFIG.bootstrapRetryBaseMs,
10700
+ bootstrapRetryMaxMs: DEFAULT_NODE_LIFECYCLE_CONFIG.bootstrapRetryMaxMs,
10701
+ bootstrapMaxRetries: DEFAULT_NODE_LIFECYCLE_CONFIG.bootstrapMaxRetries,
10702
+ allocationDebounceMs: DEFAULT_NODE_LIFECYCLE_CONFIG.allocationDebounceMs,
10703
+ nodeHeartbeatIntervalMs: DEFAULT_NODE_LIFECYCLE_CONFIG.nodeHeartbeatIntervalMs,
10704
+ onBootstrapPartition: bootstrapPartition2,
10705
+ onHoldPartition: (indexName, partitionId) => {
10706
+ trackHeldPartitionWrite(engine.recordHeldPartition(indexName, partitionId));
10707
+ },
10708
+ retainedPartitionIds: (indexName) => engine.heldPartitionsOf(indexName) ?? [],
10709
+ onRemovePartition: (indexName, partitionId) => {
10710
+ clearBootstrapSyncIndex(bootstrapSyncState, indexName, partitionId);
10711
+ replicationLogs.delete(replicationLogKey2(indexName, partitionId));
10712
+ trackHeldPartitionWrite(engine.forgetHeldPartition(indexName, partitionId));
10713
+ void cleanupRemovedPartition(indexName, partitionId, {
10714
+ engine,
10715
+ coordinator: config.coordinator,
10716
+ nodeId,
10717
+ onError: forwardOnError
9941
10718
  });
9942
- } catch (error) {
9943
- if (!(error instanceof NarsilError) || error.code !== ErrorCodes.INDEX_ALREADY_EXISTS) {
9944
- forwardOnError(error);
9945
- return false;
9946
- }
9947
10719
  }
9948
- return true;
9949
- }
9950
- if (hasDataRole) {
9951
- lifecycle = createDataNodeLifecycle({
9952
- registration,
9953
- coordinator: config.coordinator,
9954
- transport: config.transport,
9955
- knownIndexNames: [],
9956
- bootstrapRetryBaseMs: DEFAULT_NODE_LIFECYCLE_CONFIG.bootstrapRetryBaseMs,
9957
- bootstrapRetryMaxMs: DEFAULT_NODE_LIFECYCLE_CONFIG.bootstrapRetryMaxMs,
9958
- bootstrapMaxRetries: DEFAULT_NODE_LIFECYCLE_CONFIG.bootstrapMaxRetries,
9959
- allocationDebounceMs: DEFAULT_NODE_LIFECYCLE_CONFIG.allocationDebounceMs,
9960
- nodeHeartbeatIntervalMs: DEFAULT_NODE_LIFECYCLE_CONFIG.nodeHeartbeatIntervalMs,
9961
- onBootstrapPartition: bootstrapPartition2,
9962
- onRemovePartition: (indexName, partitionId) => {
9963
- clearBootstrapSyncIndex(bootstrapSyncState, indexName, partitionId);
9964
- replicationLogs.delete(replicationLogKey2(indexName, partitionId));
9965
- void cleanupRemovedPartition(indexName, partitionId, {
9966
- engine,
9967
- coordinator: config.coordinator,
9968
- nodeId,
9969
- onError: forwardOnError
9970
- });
9971
- }
9972
- });
9973
- }
10720
+ });
9974
10721
  if (hasControllerRole) {
9975
10722
  controller = createController({
9976
10723
  nodeId,
9977
10724
  coordinator: config.coordinator,
9978
10725
  transport: controllerTransport?.transport ?? config.transport,
9979
- leaseTtlMs: DEFAULT_CONTROLLER_CONFIG.leaseTtlMs,
9980
- standbyRetryMs: DEFAULT_CONTROLLER_CONFIG.standbyRetryMs,
9981
- knownIndexNames: []
10726
+ leaseTtlMs: config.controller?.leaseTtlMs ?? DEFAULT_CONTROLLER_CONFIG.leaseTtlMs,
10727
+ standbyRetryMs: config.controller?.standbyRetryMs ?? DEFAULT_CONTROLLER_CONFIG.standbyRetryMs,
10728
+ knownIndexNames: [],
10729
+ onError: forwardAllocationError,
10730
+ onElectionError: forwardOnError
9982
10731
  });
9983
10732
  }
9984
10733
  function guardShutdown() {
@@ -10003,21 +10752,16 @@ async function createClusterNode(config) {
10003
10752
  }
10004
10753
  }
10005
10754
  const readDeps = { config, nodeId, engine, resolveNodeTargets: resolveNodeTargets3 };
10006
- const cluster = {
10007
- async getAllocation(indexName) {
10008
- return trackOp(null, () => config.coordinator.getAllocation(indexName));
10009
- },
10010
- getNodeInfo() {
10011
- const status = lifecycle !== null ? lifecycle.status : isShutdown ? "shutdown" : "stopped";
10012
- return { nodeId, roles: [...roles], status };
10013
- },
10014
- isControllerActive() {
10015
- if (controller === null) {
10016
- return false;
10017
- }
10018
- return controller.isActive;
10019
- }
10020
- };
10755
+ const cluster = createClusterNamespace({
10756
+ nodeId,
10757
+ roles,
10758
+ coordinator: config.coordinator,
10759
+ lifecycle: () => lifecycle,
10760
+ controller: () => controller,
10761
+ isShutdown: () => isShutdown,
10762
+ isServing: () => serving,
10763
+ trackOp
10764
+ });
10021
10765
  const node = {
10022
10766
  get nodeId() {
10023
10767
  return nodeId;
@@ -10051,9 +10795,10 @@ async function createClusterNode(config) {
10051
10795
  orphanedIndexes.add(indexName);
10052
10796
  }
10053
10797
  }
10054
- if (lifecycle !== null) {
10055
- await lifecycle.join();
10798
+ if (hasDataRole) {
10799
+ unregisterNotReadyHandler = await config.transport.listen(createNotReadyHandler(nodeId));
10056
10800
  }
10801
+ await lifecycle.join();
10057
10802
  if (controller !== null) {
10058
10803
  await controller.start();
10059
10804
  }
@@ -10071,6 +10816,7 @@ async function createClusterNode(config) {
10071
10816
  unregisterHandler = await config.transport.listen(listener);
10072
10817
  startCatchUpPump(writeDeps.catchUp, writeDeps);
10073
10818
  }
10819
+ serving = true;
10074
10820
  },
10075
10821
  async shutdown() {
10076
10822
  await stopCatchUpPump(writeDeps.catchUp);
@@ -10078,6 +10824,7 @@ async function createClusterNode(config) {
10078
10824
  return;
10079
10825
  }
10080
10826
  isShutdown = true;
10827
+ serving = false;
10081
10828
  if (activeOps > 0) {
10082
10829
  await new Promise((resolve) => {
10083
10830
  drainResolve = resolve;
@@ -10087,12 +10834,15 @@ async function createClusterNode(config) {
10087
10834
  unregisterHandler();
10088
10835
  unregisterHandler = null;
10089
10836
  }
10090
- if (lifecycle !== null) {
10091
- await lifecycle.shutdown();
10837
+ if (unregisterNotReadyHandler !== null) {
10838
+ unregisterNotReadyHandler();
10839
+ unregisterNotReadyHandler = null;
10092
10840
  }
10841
+ await lifecycle.shutdown();
10093
10842
  if (controller !== null) {
10094
10843
  await controller.shutdown();
10095
10844
  }
10845
+ await Promise.all([...heldPartitionWrites]);
10096
10846
  await engine.shutdown();
10097
10847
  }
10098
10848
  };