@delali/narsil 0.2.3-next.34 → 0.2.3-next.36

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 (40) hide show
  1. package/README.md +2 -2
  2. package/dist/adapters/filesystem.mjs +1 -1
  3. package/dist/adapters/indexeddb.mjs +1 -1
  4. package/dist/{chunk-EMI63EXP.mjs → chunk-GB2IIJJG.mjs} +2 -0
  5. package/dist/{chunk-RO4EFWE6.mjs → chunk-GRWFY2HI.mjs} +10 -1
  6. package/dist/{chunk-36ER4WCG.mjs → chunk-JQUL2T6I.mjs} +2 -1
  7. package/dist/{chunk-GN7KF675.mjs → chunk-MMSWEEZT.mjs} +88 -164
  8. package/dist/{chunk-GODECMDT.mjs → chunk-PM52SMBT.mjs} +1 -1
  9. package/dist/chunk-WU2UZEVD.mjs +137 -0
  10. package/dist/{client-DQHB_swQ.d.ts → client-BobVnoIK.d.ts} +2 -2
  11. package/dist/client.d.ts +8 -4
  12. package/dist/client.mjs +2 -0
  13. package/dist/{config-ChZ7lfqc.d.ts → config-Byyl1QqH.d.ts} +2 -2
  14. package/dist/config-fLC4lbmg.d.ts +194 -0
  15. package/dist/{direct-executor-YARCDMJF.mjs → direct-executor-MSJEVPZU.mjs} +2 -2
  16. package/dist/distribution/coordinator/etcd.d.ts +5 -7
  17. package/dist/distribution/coordinator/etcd.mjs +27 -8
  18. package/dist/distribution/coordinator/in-memory.d.ts +2 -2
  19. package/dist/distribution/transport/grpc.mjs +3 -3
  20. package/dist/distribution/transport/in-memory.mjs +1 -1
  21. package/dist/distribution/transport/tcp.mjs +2 -2
  22. package/dist/distribution.d.ts +10 -108
  23. package/dist/distribution.mjs +1328 -370
  24. package/dist/embeddings/openai.mjs +1 -1
  25. package/dist/{engine-DScGlT2Z.d.ts → engine-BE45D5ZG.d.ts} +3 -3
  26. package/dist/index.browser.mjs +90 -24
  27. package/dist/index.d.ts +7 -7
  28. package/dist/index.mjs +5 -4
  29. package/dist/{options-BORJg5XB.d.ts → options-EXYFcem-.d.ts} +9 -0
  30. package/dist/persistence/durability/checkpoint-worker.d.ts +3 -1
  31. package/dist/persistence/durability/checkpoint-worker.mjs +5 -4
  32. package/dist/react.d.ts +4 -4
  33. package/dist/react.mjs +2 -0
  34. package/dist/{schema-emTQfEiX.d.ts → schema-ZRbk8BgH.d.ts} +1 -0
  35. package/dist/{search-CQ-9bKxo.d.ts → search-DHuXwJYk.d.ts} +29 -2
  36. package/dist/server.d.ts +9 -5
  37. package/dist/server.mjs +101 -7
  38. package/dist/{types-BhdINMtE.d.ts → types-B5Tsaalz.d.ts} +19 -2
  39. package/dist/workers/entry.mjs +2 -2
  40. 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-GN7KF675.mjs';
2
- import { ReplicationMessageTypes, 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
@@ -237,8 +248,18 @@ function countNodeAssignments(assignments) {
237
248
  }
238
249
  return counts;
239
250
  }
251
+ function countPrimaryAssignments(assignments) {
252
+ const counts = /* @__PURE__ */ new Map();
253
+ for (const assignment of assignments.values()) {
254
+ if (assignment.primary !== null) {
255
+ counts.set(assignment.primary, (counts.get(assignment.primary) ?? 0) + 1);
256
+ }
257
+ }
258
+ return counts;
259
+ }
240
260
  function computeNodeWeights(nodes, assignments) {
241
261
  const assignmentCounts = countNodeAssignments(assignments);
262
+ const primaryCounts = countPrimaryAssignments(assignments);
242
263
  let totalCapacity = 0;
243
264
  for (const node of nodes) {
244
265
  totalCapacity += node.capacity.memoryBytes;
@@ -247,12 +268,14 @@ function computeNodeWeights(nodes, assignments) {
247
268
  const weights = [];
248
269
  for (const node of nodes) {
249
270
  const partitionCount = assignmentCounts.get(node.nodeId) ?? 0;
271
+ const primaryCount = primaryCounts.get(node.nodeId) ?? 0;
250
272
  const normalizedCapacity = averageCapacity > 0 ? node.capacity.memoryBytes / averageCapacity : 1;
251
- const weight = partitionCount / normalizedCapacity;
252
273
  weights.push({
253
274
  nodeId: node.nodeId,
254
- weight,
275
+ weight: partitionCount / normalizedCapacity,
255
276
  partitionCount,
277
+ primaryWeight: primaryCount / normalizedCapacity,
278
+ primaryCount,
256
279
  capacity: node.capacity.memoryBytes
257
280
  });
258
281
  }
@@ -271,6 +294,11 @@ function findBestNode(candidates, weights, deciders, context) {
271
294
  const sorted = [...candidates].sort((a, b) => {
272
295
  const wA = weightByNodeId.get(a);
273
296
  const wB = weightByNodeId.get(b);
297
+ if (context.role === "primary") {
298
+ const primaryA = wA?.primaryWeight ?? 0;
299
+ const primaryB = wB?.primaryWeight ?? 0;
300
+ if (primaryA !== primaryB) return primaryA - primaryB;
301
+ }
274
302
  const weightA = wA?.weight ?? 0;
275
303
  const weightB = wB?.weight ?? 0;
276
304
  if (weightA !== weightB) return weightA - weightB;
@@ -380,6 +408,90 @@ function initialAllocate(nodes, indexName, partitionCount, replicationFactor, co
380
408
  };
381
409
  }
382
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
+
419
+ // src/distribution/cluster/allocator/leadership.ts
420
+ var LEADERSHIP_IMBALANCE_THRESHOLD = 2;
421
+ function loadOf(nodeId, counts, shares) {
422
+ const share = shares.get(nodeId);
423
+ return (counts.get(nodeId) ?? 0) / (share === void 0 || share <= 0 ? 1 : share);
424
+ }
425
+ function heaviestLeader(shares, counts, withoutHandover) {
426
+ let heaviest;
427
+ for (const nodeId of shares.keys()) {
428
+ if (withoutHandover.has(nodeId)) {
429
+ continue;
430
+ }
431
+ if (heaviest === void 0 || loadOf(nodeId, counts, shares) > loadOf(heaviest, counts, shares)) {
432
+ heaviest = nodeId;
433
+ }
434
+ }
435
+ return heaviest;
436
+ }
437
+ function findSwap(assignments, heaviest, counts, shares) {
438
+ const heaviestLoad = loadOf(heaviest, counts, shares);
439
+ let best = null;
440
+ for (const assignment of assignments.values()) {
441
+ if (assignment.state !== "ACTIVE" || assignment.primary !== heaviest) {
442
+ continue;
443
+ }
444
+ for (const candidateNodeId of assignment.inSyncSet) {
445
+ const candidateLoad = loadOf(candidateNodeId, counts, shares);
446
+ if (heaviestLoad - candidateLoad < LEADERSHIP_IMBALANCE_THRESHOLD) {
447
+ continue;
448
+ }
449
+ if (best === null || candidateLoad < best.candidateLoad) {
450
+ best = { assignment, candidateNodeId, candidateLoad };
451
+ }
452
+ }
453
+ }
454
+ return best;
455
+ }
456
+ function applySwap(swap, previousPrimary) {
457
+ const { assignment, candidateNodeId } = swap;
458
+ assignment.primary = candidateNodeId;
459
+ assignment.replicas = [...assignment.replicas.filter((nodeId) => nodeId !== candidateNodeId), previousPrimary];
460
+ assignment.inSyncSet = [
461
+ ...assignment.inSyncSet.filter((nodeId) => nodeId !== candidateNodeId && nodeId !== previousPrimary),
462
+ previousPrimary
463
+ ];
464
+ assignment.primaryTerm += 1;
465
+ }
466
+ function rebalanceLeadership(assignments, shares) {
467
+ const withoutHandover = /* @__PURE__ */ new Set();
468
+ for (let swaps = 0; swaps < assignments.size; swaps++) {
469
+ const counts = countPrimaryAssignments(assignments);
470
+ const heaviest = heaviestLeader(shares, counts, withoutHandover);
471
+ if (heaviest === void 0) {
472
+ return;
473
+ }
474
+ const swap = findSwap(assignments, heaviest, counts, shares);
475
+ if (swap === null) {
476
+ withoutHandover.add(heaviest);
477
+ continue;
478
+ }
479
+ applySwap(swap, heaviest);
480
+ }
481
+ }
482
+ function capacityShares(nodes) {
483
+ const shares = /* @__PURE__ */ new Map();
484
+ let totalCapacity = 0;
485
+ for (const node of nodes) {
486
+ totalCapacity += node.capacity.memoryBytes;
487
+ }
488
+ const averageCapacity = nodes.length > 0 ? totalCapacity / nodes.length : 0;
489
+ for (const node of nodes) {
490
+ shares.set(node.nodeId, averageCapacity > 0 ? node.capacity.memoryBytes / averageCapacity : 1);
491
+ }
492
+ return shares;
493
+ }
494
+
383
495
  // src/distribution/cluster/allocator/rebalance.ts
384
496
  function cloneAssignments(assignments) {
385
497
  const cloned = /* @__PURE__ */ new Map();
@@ -390,20 +502,55 @@ function cloneAssignments(assignments) {
390
502
  inSyncSet: [...assignment.inSyncSet],
391
503
  commitPoint: assignment.commitPoint,
392
504
  state: assignment.state,
393
- primaryTerm: assignment.primaryTerm
505
+ primaryTerm: assignment.primaryTerm,
506
+ ...assignment.lastHolders !== void 0 ? { lastHolders: [...assignment.lastHolders] } : {},
507
+ ...assignment.unassignedReason !== void 0 ? { unassignedReason: assignment.unassignedReason } : {}
394
508
  });
395
509
  }
396
510
  return cloned;
397
511
  }
398
- 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
+ }
399
520
  assignment.primary = null;
400
521
  assignment.replicas = [];
401
522
  assignment.inSyncSet = [];
523
+ assignment.lastHolders = [...holders].sort(compareCodePoints);
402
524
  assignment.state = "UNASSIGNED";
403
525
  }
404
- function handleLostNodes(assignments, activeNodeIds) {
526
+ function primaryLoadOf(nodeId, primaryCounts, shares) {
527
+ const share = shares.get(nodeId);
528
+ return (primaryCounts.get(nodeId) ?? 0) / (share === void 0 || share <= 0 ? 1 : share);
529
+ }
530
+ function leastLoadedCandidate(candidates, primaryCounts, shares) {
531
+ let best;
532
+ for (const candidate of candidates) {
533
+ if (best === void 0) {
534
+ best = candidate;
535
+ continue;
536
+ }
537
+ const bestLoad = primaryLoadOf(best, primaryCounts, shares);
538
+ const candidateLoad = primaryLoadOf(candidate, primaryCounts, shares);
539
+ if (candidateLoad < bestLoad || candidateLoad === bestLoad && candidate < best) {
540
+ best = candidate;
541
+ }
542
+ }
543
+ return best;
544
+ }
545
+ function handleLostNodes(assignments, activeNodeIds, shares) {
546
+ const primaryCounts = countPrimaryAssignments(assignments);
405
547
  for (const assignment of assignments.values()) {
406
- 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);
407
554
  if (primaryWasLost) {
408
555
  assignment.primary = null;
409
556
  }
@@ -411,16 +558,21 @@ function handleLostNodes(assignments, activeNodeIds) {
411
558
  assignment.inSyncSet = assignment.inSyncSet.filter((id) => activeNodeIds.has(id));
412
559
  if (!primaryWasLost) {
413
560
  if (assignment.primary === null) {
414
- markUnassigned(assignment);
561
+ markUnassigned(assignment, lastPrimary, lastInSyncSet);
415
562
  }
416
563
  continue;
417
564
  }
418
565
  const inSyncCandidates = assignment.replicas.filter((id) => assignment.inSyncSet.includes(id));
419
566
  if (inSyncCandidates.length === 0) {
420
- markUnassigned(assignment);
567
+ markUnassigned(assignment, lastPrimary, lastInSyncSet);
421
568
  continue;
422
569
  }
423
- const promoted = inSyncCandidates[0];
570
+ const promoted = leastLoadedCandidate(inSyncCandidates, primaryCounts, shares);
571
+ if (promoted === void 0) {
572
+ markUnassigned(assignment, lastPrimary, lastInSyncSet);
573
+ continue;
574
+ }
575
+ primaryCounts.set(promoted, (primaryCounts.get(promoted) ?? 0) + 1);
424
576
  assignment.primary = promoted;
425
577
  assignment.replicas = assignment.replicas.filter((id) => id !== promoted);
426
578
  assignment.inSyncSet = assignment.inSyncSet.filter((id) => id !== promoted);
@@ -453,7 +605,16 @@ function fillReplicaSlots(assignments, sortedNodes, nodeMap, replicationFactor,
453
605
  }
454
606
  }
455
607
  }
456
- function rebalanceForBalance(assignments, sortedNodes, nodeMap, constraints, deciders) {
608
+ function slotWeightOf(nodeId, shares) {
609
+ const share = shares.get(nodeId);
610
+ return 1 / (share === void 0 || share <= 0 ? 1 : share);
611
+ }
612
+ function moveNarrowsGap(mostLoaded, leastLoaded, shares) {
613
+ const gap = mostLoaded.weight - leastLoaded.weight;
614
+ const afterMove = Math.abs(gap - slotWeightOf(mostLoaded.nodeId, shares) - slotWeightOf(leastLoaded.nodeId, shares));
615
+ return afterMove < gap;
616
+ }
617
+ function rebalanceForBalance(assignments, sortedNodes, nodeMap, constraints, deciders, shares) {
457
618
  let totalSlots = 0;
458
619
  for (const assignment of assignments.values()) {
459
620
  if (assignment.primary !== null) totalSlots++;
@@ -469,35 +630,73 @@ function rebalanceForBalance(assignments, sortedNodes, nodeMap, constraints, dec
469
630
  if (mostLoaded.weight - leastLoaded.weight < REBALANCE_THRESHOLD) {
470
631
  break;
471
632
  }
472
- let moved = false;
473
- for (const [partitionId, assignment] of assignments) {
474
- if (moved) break;
475
- const replicaIndex = assignment.replicas.indexOf(mostLoaded.nodeId);
476
- if (replicaIndex >= 0) {
477
- const removedReplica = assignment.replicas[replicaIndex];
478
- assignment.replicas.splice(replicaIndex, 1);
479
- const nodeAssignmentCounts = countNodeAssignments(assignments);
480
- const moveContext = {
481
- partitionId,
482
- role: "replica",
483
- currentAssignment: assignment,
484
- allAssignments: assignments,
485
- nodeAssignmentCounts,
486
- nodes: nodeMap,
487
- constraints
488
- };
489
- const target = findBestNode([leastLoaded.nodeId], weights, deciders, moveContext);
490
- if (target !== null) {
491
- assignment.replicas.push(target);
492
- moved = true;
493
- } else {
494
- assignment.replicas.splice(replicaIndex, 0, removedReplica);
495
- }
496
- }
633
+ if (!moveNarrowsGap(mostLoaded, leastLoaded, shares)) {
634
+ break;
497
635
  }
636
+ const move = {
637
+ fromNodeId: mostLoaded.nodeId,
638
+ toNodeId: leastLoaded.nodeId,
639
+ weights,
640
+ nodeMap,
641
+ constraints,
642
+ deciders
643
+ };
644
+ const laggingFirst = moveOneReplica(assignments, move, true);
645
+ const moved = laggingFirst || moveOneReplica(assignments, move, false);
498
646
  if (!moved) break;
499
647
  }
500
648
  }
649
+ function moveOneReplica(assignments, move, laggingOnly) {
650
+ for (const [partitionId, assignment] of assignments) {
651
+ const replicaIndex = assignment.replicas.indexOf(move.fromNodeId);
652
+ if (replicaIndex < 0) {
653
+ continue;
654
+ }
655
+ if (laggingOnly && assignment.inSyncSet.includes(move.fromNodeId)) {
656
+ continue;
657
+ }
658
+ const removedReplica = assignment.replicas[replicaIndex];
659
+ assignment.replicas.splice(replicaIndex, 1);
660
+ const moveContext = {
661
+ partitionId,
662
+ role: "replica",
663
+ currentAssignment: assignment,
664
+ allAssignments: assignments,
665
+ nodeAssignmentCounts: countNodeAssignments(assignments),
666
+ nodes: move.nodeMap,
667
+ constraints: move.constraints
668
+ };
669
+ const target = findBestNode([move.toNodeId], move.weights, move.deciders, moveContext);
670
+ if (target === null) {
671
+ assignment.replicas.splice(replicaIndex, 0, removedReplica);
672
+ continue;
673
+ }
674
+ assignment.replicas.push(target);
675
+ return true;
676
+ }
677
+ return false;
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
+ }
689
+ function pruneInSyncSets(assignments) {
690
+ for (const assignment of assignments.values()) {
691
+ if (assignment.state === "UNASSIGNED") {
692
+ continue;
693
+ }
694
+ const stale = assignment.inSyncSet.some((nodeId) => !assignment.replicas.includes(nodeId));
695
+ if (stale) {
696
+ assignment.inSyncSet = assignment.inSyncSet.filter((nodeId) => assignment.replicas.includes(nodeId));
697
+ }
698
+ }
699
+ }
501
700
  function rebalanceAllocate(nodes, currentTable, constraints, deciders) {
502
701
  const sortedNodes = [...nodes].sort((a, b) => {
503
702
  if (a.nodeId < b.nodeId) return -1;
@@ -510,9 +709,13 @@ function rebalanceAllocate(nodes, currentTable, constraints, deciders) {
510
709
  nodeMap.set(node.nodeId, node);
511
710
  }
512
711
  const assignments = cloneAssignments(currentTable.assignments);
513
- handleLostNodes(assignments, activeNodeIds);
712
+ const shares = capacityShares(sortedNodes);
713
+ handleLostNodes(assignments, activeNodeIds, shares);
514
714
  fillReplicaSlots(assignments, sortedNodes, nodeMap, currentTable.replicationFactor, constraints, deciders);
515
- rebalanceForBalance(assignments, sortedNodes, nodeMap, constraints, deciders);
715
+ rebalanceForBalance(assignments, sortedNodes, nodeMap, constraints, deciders, shares);
716
+ rebalanceLeadership(assignments, shares);
717
+ pruneInSyncSets(assignments);
718
+ clearRecoveryStateOnServedPartitions(assignments);
516
719
  const warnings = collectReplicationWarnings(assignments, currentTable.replicationFactor);
517
720
  return {
518
721
  table: {
@@ -561,7 +764,268 @@ function allocate(nodes, currentTable, indexName, partitionCount, replicationFac
561
764
  return rebalanceAllocate(dataNodes, currentTable, constraints, deciders);
562
765
  }
563
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
+
564
1025
  // src/distribution/cluster/controller/event-loop/allocation.ts
1026
+ function holdsDataNode(nodes) {
1027
+ return nodes.some((node) => node.roles.includes("data"));
1028
+ }
565
1029
  var ALLOCATION_CAS_ATTEMPTS = 5;
566
1030
  var ALLOCATION_RETRY_DELAY_MS = 1e3;
567
1031
  var TEARDOWN_CAS_ATTEMPTS = 5;
@@ -603,12 +1067,29 @@ async function runAllocatorForIndex(coordinator, indexName, nodes, isActive, att
603
1067
  { indexName, attempts: ALLOCATION_CAS_ATTEMPTS }
604
1068
  );
605
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
+ }
606
1087
  async function runAllocatorForAllIndexes(state, coordinator, isActive, onError) {
607
1088
  if (!isActive()) {
608
1089
  return;
609
1090
  }
610
1091
  const nodes = await coordinator.listNodes();
611
- if (nodes.length === 0 || !isActive()) {
1092
+ if (!holdsDataNode(nodes) || !isActive()) {
612
1093
  return;
613
1094
  }
614
1095
  let anyIndexFailed = false;
@@ -618,6 +1099,7 @@ async function runAllocatorForAllIndexes(state, coordinator, isActive, onError)
618
1099
  }
619
1100
  try {
620
1101
  await runAllocatorForIndex(coordinator, indexName, nodes, isActive);
1102
+ await recoverIndexPartitions(state, coordinator, indexName, nodes, isActive);
621
1103
  } catch (error) {
622
1104
  anyIndexFailed = true;
623
1105
  if (onError !== void 0) {
@@ -653,7 +1135,7 @@ async function handleSchemaCreated(indexName, coordinator, knownIndexes, isActiv
653
1135
  return;
654
1136
  }
655
1137
  const nodes = await coordinator.listNodes();
656
- if (nodes.length === 0 || !isActive()) {
1138
+ if (!holdsDataNode(nodes) || !isActive()) {
657
1139
  return;
658
1140
  }
659
1141
  const storedTable = await coordinator.getAllocation(indexName);
@@ -744,14 +1226,6 @@ async function handleSchemaEvent(event, coordinator, state, isActive, onError) {
744
1226
  await tearDownAllocation(event.indexName, coordinator, state, isActive, 0, onError);
745
1227
  }
746
1228
  }
747
-
748
- // src/distribution/payload-guards.ts
749
- function isRecord(value) {
750
- return value !== null && typeof value === "object" && !Array.isArray(value);
751
- }
752
- function isValidInteger(value) {
753
- return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value);
754
- }
755
1229
  var INSYNC_CAS_ATTEMPTS = 5;
756
1230
  var REFUSE = { outcome: "refuse" };
757
1231
  var ACCEPT = { outcome: "accept" };
@@ -807,6 +1281,9 @@ async function requestInsyncRemoval(indexName, partitionId, replicaNodeId, prima
807
1281
  }
808
1282
  function handleInsyncRemoval(payload, coordinator) {
809
1283
  return updateInsyncSet(payload, coordinator, (assignment) => {
1284
+ if (assignment.state === "UNASSIGNED") {
1285
+ return REFUSE;
1286
+ }
810
1287
  if (!assignment.inSyncSet.includes(payload.replicaNodeId)) {
811
1288
  return ACCEPT;
812
1289
  }
@@ -940,15 +1417,6 @@ async function handleBootstrapCompleteMessage(message, respond, coordinator, con
940
1417
  });
941
1418
  }
942
1419
  var BOOTSTRAP_CAS_ATTEMPTS = 5;
943
- function ensurePrimaryInSyncSet(assignment, inSyncSet) {
944
- if (assignment.primary === null) {
945
- return inSyncSet;
946
- }
947
- if (inSyncSet.includes(assignment.primary)) {
948
- return inSyncSet;
949
- }
950
- return [assignment.primary, ...inSyncSet];
951
- }
952
1420
  async function processBootstrapComplete(payload, coordinator) {
953
1421
  for (let attempt = 0; attempt < BOOTSTRAP_CAS_ATTEMPTS; attempt++) {
954
1422
  const table = await coordinator.getAllocation(payload.indexName);
@@ -968,13 +1436,10 @@ async function processBootstrapComplete(payload, coordinator) {
968
1436
  if (assignment.state !== "INITIALISING") {
969
1437
  return false;
970
1438
  }
971
- const baseInSyncSet = assignment.inSyncSet.includes(payload.nodeId) ? assignment.inSyncSet : [...assignment.inSyncSet, payload.nodeId];
1439
+ const served = { ...assignment, state: "ACTIVE" };
1440
+ delete served.lastHolders;
972
1441
  const updatedAssignments = new Map(table.assignments);
973
- updatedAssignments.set(payload.partitionId, {
974
- ...assignment,
975
- state: "ACTIVE",
976
- inSyncSet: ensurePrimaryInSyncSet(assignment, baseInSyncSet)
977
- });
1442
+ updatedAssignments.set(payload.partitionId, served);
978
1443
  const written = await coordinator.putAllocation(
979
1444
  payload.indexName,
980
1445
  { ...table, version: table.version + 1, assignments: updatedAssignments },
@@ -1086,7 +1551,7 @@ function handleInsyncRemoveMessage(state, message, respond, coordinator, nodeId,
1086
1551
  await respond(createInsyncConfirmMessage(confirmPayload, nodeId, message.requestId));
1087
1552
  });
1088
1553
  }
1089
- function handleInsyncAddMessage(state, message, respond, coordinator, nodeId, isActive) {
1554
+ function handleInsyncAddMessage(state, message, respond, coordinator, nodeId, isActive, onError) {
1090
1555
  const decoded = decodeMessage(message);
1091
1556
  const payload = validateInsyncAddPayload(decoded);
1092
1557
  if (payload === null) {
@@ -1106,6 +1571,9 @@ function handleInsyncAddMessage(state, message, respond, coordinator, nodeId, is
1106
1571
  }
1107
1572
  const confirmPayload = await handleInsyncAdmission(payload, coordinator);
1108
1573
  await respond(createInsyncConfirmMessage(confirmPayload, nodeId, message.requestId));
1574
+ if (confirmPayload.accepted && isActive()) {
1575
+ scheduleDebouncedAllocation(state, coordinator, isActive, onError);
1576
+ }
1109
1577
  });
1110
1578
  }
1111
1579
  function handleQueuedBootstrapComplete(state, message, respond, coordinator, nodeId, isActive) {
@@ -1126,7 +1594,9 @@ function createEventLoopState(initialIndexNames) {
1126
1594
  knownIndexes: new Set(initialIndexNames),
1127
1595
  debounceTimer: null,
1128
1596
  teardownTimers: /* @__PURE__ */ new Map(),
1129
- insyncQueue: Promise.resolve()
1597
+ insyncQueue: Promise.resolve(),
1598
+ transport: null,
1599
+ controllerNodeId: null
1130
1600
  };
1131
1601
  }
1132
1602
  function clearEventLoopWatchers(state) {
@@ -1155,6 +1625,8 @@ function clearEventLoopWatchers(state) {
1155
1625
  // src/distribution/cluster/controller/event-loop/index.ts
1156
1626
  async function startEventLoop(state, coordinator, transport, nodeId, isActive, onError) {
1157
1627
  clearEventLoopWatchers(state);
1628
+ state.transport = transport;
1629
+ state.controllerNodeId = nodeId;
1158
1630
  try {
1159
1631
  state.unwatchNodes = await coordinator.watchNodes((_event) => {
1160
1632
  scheduleDebouncedAllocation(state, coordinator, isActive, onError);
@@ -1173,7 +1645,7 @@ async function startEventLoop(state, coordinator, transport, nodeId, isActive, o
1173
1645
  if (message.type === ReplicationMessageTypes.INSYNC_REMOVE) {
1174
1646
  handleInsyncRemoveMessage(state, message, respond, coordinator, nodeId, isActive);
1175
1647
  } else if (message.type === ReplicationMessageTypes.INSYNC_ADD) {
1176
- handleInsyncAddMessage(state, message, respond, coordinator, nodeId, isActive);
1648
+ handleInsyncAddMessage(state, message, respond, coordinator, nodeId, isActive, onError);
1177
1649
  } else if (message.type === ClusterMessageTypes.BOOTSTRAP_COMPLETE) {
1178
1650
  handleQueuedBootstrapComplete(state, message, respond, coordinator, nodeId, isActive);
1179
1651
  }
@@ -1187,13 +1659,21 @@ async function startEventLoop(state, coordinator, transport, nodeId, isActive, o
1187
1659
 
1188
1660
  // src/distribution/cluster/controller/index.ts
1189
1661
  function createController(config) {
1190
- const { nodeId, coordinator, transport, leaseTtlMs, standbyRetryMs, knownIndexNames, onError } = config;
1662
+ const { nodeId, coordinator, transport, leaseTtlMs, standbyRetryMs, knownIndexNames, onError, onElectionError } = config;
1191
1663
  let electionState = createElectionState();
1192
1664
  let eventLoopState = createEventLoopState(knownIndexNames);
1193
1665
  let stopped = false;
1194
1666
  function isActive() {
1195
1667
  return electionState.active;
1196
1668
  }
1669
+ function standBy() {
1670
+ if (stopped) {
1671
+ return;
1672
+ }
1673
+ scheduleStandbyRetry(electionState, standbyRetryMs, () => {
1674
+ void runElection();
1675
+ });
1676
+ }
1197
1677
  function stepDown() {
1198
1678
  if (!electionState.active) {
1199
1679
  return;
@@ -1201,12 +1681,7 @@ function createController(config) {
1201
1681
  electionState.active = false;
1202
1682
  clearElectionTimers(electionState);
1203
1683
  clearEventLoopWatchers(eventLoopState);
1204
- if (!stopped) {
1205
- scheduleStandbyRetry(electionState, standbyRetryMs, () => {
1206
- tryElection().catch(() => {
1207
- });
1208
- });
1209
- }
1684
+ standBy();
1210
1685
  }
1211
1686
  async function becomeActive() {
1212
1687
  if (electionState.active) {
@@ -1232,18 +1707,38 @@ function createController(config) {
1232
1707
  throw error;
1233
1708
  }
1234
1709
  }
1235
- async function tryElection() {
1710
+ async function standForElection() {
1236
1711
  if (stopped) {
1237
1712
  return;
1238
1713
  }
1239
1714
  const acquired = await tryAcquireLease(coordinator, nodeId, leaseTtlMs);
1715
+ if (stopped) {
1716
+ if (acquired) {
1717
+ await releaseLease(coordinator);
1718
+ }
1719
+ return;
1720
+ }
1240
1721
  if (acquired) {
1241
1722
  await becomeActive();
1242
- } else {
1243
- scheduleStandbyRetry(electionState, standbyRetryMs, () => {
1244
- tryElection().catch(() => {
1245
- });
1246
- });
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();
1247
1742
  }
1248
1743
  }
1249
1744
  const controller = {
@@ -1252,7 +1747,13 @@ function createController(config) {
1252
1747
  },
1253
1748
  async start() {
1254
1749
  stopped = false;
1255
- await tryElection();
1750
+ clearStandbyTimer(electionState);
1751
+ try {
1752
+ await standForElection();
1753
+ } catch (error) {
1754
+ standBy();
1755
+ throw error;
1756
+ }
1256
1757
  },
1257
1758
  async stop() {
1258
1759
  stopped = true;
@@ -1476,7 +1977,7 @@ function createBootstrapState(indexName, partitionId, primaryNodeId) {
1476
1977
  };
1477
1978
  }
1478
1979
 
1479
- // src/distribution/cluster/node-lifecycle/allocation-watcher.ts
1980
+ // src/distribution/cluster/node-lifecycle/allocation-watcher/state.ts
1480
1981
  function partitionKey(indexName, partitionId) {
1481
1982
  return `${indexName}:${partitionId}`;
1482
1983
  }
@@ -1486,40 +1987,137 @@ function createAllocationWatcherState() {
1486
1987
  debounceTimer: null,
1487
1988
  activeBootstraps: /* @__PURE__ */ new Map(),
1488
1989
  trackedPartitions: /* @__PURE__ */ new Map(),
1990
+ pendingPartitions: /* @__PURE__ */ new Set(),
1489
1991
  pendingTables: /* @__PURE__ */ new Map(),
1490
1992
  restartWaiters: /* @__PURE__ */ new Map(),
1993
+ retainedPartitions: /* @__PURE__ */ new Set(),
1994
+ indexesSeededFromRecord: /* @__PURE__ */ new Set(),
1491
1995
  stopped: false
1492
1996
  };
1493
1997
  }
1494
- async function startAllocationWatcher(state, config) {
1495
- const unwatchAllocation = await config.coordinator.watchAllocation((event) => {
1496
- if (state.stopped) {
1497
- 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);
1498
2046
  }
1499
- 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);
1500
2057
  });
1501
- state.unwatchAllocation = unwatchAllocation;
1502
2058
  }
1503
- function scheduleProcessAllocation(state, config, table) {
1504
- state.pendingTables.set(table.indexName, table);
1505
- if (state.debounceTimer !== null) {
1506
- 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();
1507
2063
  }
1508
- state.debounceTimer = setTimeout(() => {
1509
- state.debounceTimer = null;
1510
- const tablesToProcess = Array.from(state.pendingTables.values());
1511
- state.pendingTables.clear();
1512
- for (const pending of tablesToProcess) {
1513
- 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);
1514
2088
  }
1515
- }, 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);
1516
2111
  }
2112
+
2113
+ // src/distribution/cluster/node-lifecycle/allocation-watcher/table.ts
1517
2114
  function processAllocationChange(state, config, table) {
1518
2115
  if (state.stopped) {
1519
2116
  return;
1520
2117
  }
1521
2118
  const nodeId = config.registration.nodeId;
1522
2119
  const currentKeys = /* @__PURE__ */ new Set();
2120
+ seedRetainedFromRecord(state, config, table.indexName);
1523
2121
  for (const [partitionId, assignment] of table.assignments) {
1524
2122
  if (!isNodeAssigned(assignment, nodeId)) {
1525
2123
  continue;
@@ -1540,8 +2138,34 @@ function processAllocationChange(state, config, table) {
1540
2138
  primaryNodeId: assignment.primary,
1541
2139
  primaryTerm: assignment.primaryTerm
1542
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;
1543
2167
  }
1544
- removeUnassignedPartitions(state, config, table.indexName, currentKeys);
2168
+ return assignment.primary === nodeId || assignment.inSyncSet.includes(nodeId);
1545
2169
  }
1546
2170
  function isNodeAssigned(assignment, nodeId) {
1547
2171
  if (assignment.primary === nodeId) {
@@ -1574,6 +2198,10 @@ function handleExistingAssignment(state, config, existing, assignment, nodeId) {
1574
2198
  }
1575
2199
  return;
1576
2200
  }
2201
+ if (promotedBackAtHigherTerm(existing, assignment, nodeId)) {
2202
+ startBootstrap(state, config, existing.indexName, existing.partitionId, nodeId);
2203
+ return;
2204
+ }
1577
2205
  if (assignment.primary !== null && replicaNeedsResync(state, existing, assignment, nodeId)) {
1578
2206
  startBootstrap(state, config, existing.indexName, existing.partitionId, assignment.primary);
1579
2207
  }
@@ -1593,138 +2221,111 @@ function replicaNeedsResync(state, existing, assignment, nodeId) {
1593
2221
  }
1594
2222
  return !state.activeBootstraps.has(partitionKey(existing.indexName, existing.partitionId));
1595
2223
  }
1596
- function startBootstrap(state, config, indexName, partitionId, primaryNodeId) {
1597
- const key = partitionKey(indexName, partitionId);
1598
- const existingBootstrap = state.activeBootstraps.get(key);
1599
- if (existingBootstrap !== void 0) {
1600
- 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;
1601
2231
  }
1602
- const bootstrapState = createBootstrapState(indexName, partitionId, primaryNodeId);
1603
- state.activeBootstraps.set(key, bootstrapState);
1604
- bootstrapPartition(
1605
- bootstrapState,
1606
- config.coordinator,
1607
- config.transport,
1608
- config.registration.nodeId,
1609
- config.bootstrapRetryBaseMs,
1610
- config.bootstrapRetryMaxMs,
1611
- config.bootstrapMaxRetries,
1612
- config.onBootstrapPartition,
1613
- config.onError
1614
- ).then((succeeded) => {
1615
- if (state.activeBootstraps.get(key) === bootstrapState) {
1616
- 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;
1617
2266
  }
1618
- if (!succeeded) {
1619
- state.trackedPartitions.delete(key);
1620
- void restartBootstrapWhileOutOfSync(state, config, indexName, partitionId);
2267
+ if (currentKeys.has(key)) {
2268
+ state.retainedPartitions.delete(key);
2269
+ continue;
1621
2270
  }
1622
- }).catch(() => {
1623
- if (state.activeBootstraps.get(key) === bootstrapState) {
1624
- state.activeBootstraps.delete(key);
2271
+ if (!copyHasNoFurtherUse(table, partitionId, nodeId)) {
2272
+ continue;
1625
2273
  }
1626
- state.trackedPartitions.delete(key);
1627
- void restartBootstrapWhileOutOfSync(state, config, indexName, partitionId);
1628
- });
2274
+ state.retainedPartitions.delete(key);
2275
+ if (config.onRemovePartition !== void 0) {
2276
+ config.onRemovePartition(table.indexName, partitionId);
2277
+ }
2278
+ }
1629
2279
  }
1630
- function removeUnassignedPartitions(state, config, indexName, currentKeys) {
2280
+ function removeDroppedPartitions(state, config, table, currentKeys) {
1631
2281
  for (const [key, tracked] of state.trackedPartitions) {
1632
- if (tracked.indexName !== indexName) {
2282
+ if (tracked.indexName !== table.indexName) {
1633
2283
  continue;
1634
2284
  }
1635
2285
  if (currentKeys.has(key)) {
1636
2286
  continue;
1637
2287
  }
1638
2288
  state.trackedPartitions.delete(key);
2289
+ state.pendingPartitions.delete(key);
1639
2290
  const activeBootstrap = state.activeBootstraps.get(key);
1640
2291
  if (activeBootstrap !== void 0) {
1641
2292
  abortBootstrapState(activeBootstrap);
1642
2293
  state.activeBootstraps.delete(key);
1643
2294
  }
2295
+ if (keepsCopyForRecovery(table, tracked.partitionId, config.registration.nodeId)) {
2296
+ state.retainedPartitions.add(key);
2297
+ continue;
2298
+ }
2299
+ state.retainedPartitions.delete(key);
1644
2300
  if (config.onRemovePartition !== void 0) {
1645
2301
  config.onRemovePartition(tracked.indexName, tracked.partitionId);
1646
2302
  }
1647
2303
  }
1648
2304
  }
1649
- function stopAllocationWatcher(state) {
1650
- state.stopped = true;
1651
- if (state.unwatchAllocation !== null) {
1652
- state.unwatchAllocation();
1653
- state.unwatchAllocation = null;
1654
- }
1655
- if (state.debounceTimer !== null) {
1656
- clearTimeout(state.debounceTimer);
1657
- state.debounceTimer = null;
1658
- }
1659
- for (const [timer, resolve] of state.restartWaiters) {
1660
- clearTimeout(timer);
1661
- resolve();
1662
- }
1663
- state.restartWaiters.clear();
1664
- for (const bootstrapState of state.activeBootstraps.values()) {
1665
- abortBootstrapState(bootstrapState);
1666
- }
1667
- state.activeBootstraps.clear();
1668
- state.trackedPartitions.clear();
1669
- state.pendingTables.clear();
1670
- }
1671
- function processInitialAllocations(state, config, tables) {
1672
- for (const table of tables) {
1673
- processAllocationChange(state, config, table);
1674
- }
1675
- }
1676
- function waitBeforeBootstrapRestart(state, config) {
1677
- const delayMs = Math.min(config.bootstrapRetryMaxMs, config.bootstrapRetryBaseMs);
1678
- if (delayMs <= 0) {
1679
- return Promise.resolve();
1680
- }
1681
- return new Promise((resolve) => {
1682
- const timer = setTimeout(() => {
1683
- state.restartWaiters.delete(timer);
1684
- resolve();
1685
- }, delayMs);
1686
- timer.unref?.();
1687
- 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);
1688
2313
  });
2314
+ state.unwatchAllocation = unwatchAllocation;
1689
2315
  }
1690
- async function restartBootstrapWhileOutOfSync(state, config, indexName, partitionId) {
1691
- if (state.stopped) {
1692
- return;
1693
- }
1694
- await waitBeforeBootstrapRestart(state, config);
1695
- if (state.stopped) {
1696
- return;
2316
+ function scheduleProcessAllocation(state, config, table) {
2317
+ state.pendingTables.set(table.indexName, table);
2318
+ if (state.debounceTimer !== null) {
2319
+ clearTimeout(state.debounceTimer);
1697
2320
  }
1698
- const nodeId = config.registration.nodeId;
1699
- let table;
1700
- try {
1701
- table = await config.coordinator.getAllocation(indexName);
1702
- } catch (error) {
1703
- if (config.onError !== void 0) {
1704
- config.onError(error);
1705
- }
1706
- if (!state.stopped) {
1707
- void restartBootstrapWhileOutOfSync(state, config, indexName, partitionId);
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);
1708
2327
  }
1709
- return;
1710
- }
1711
- if (state.stopped || table === null) {
1712
- return;
1713
- }
1714
- const assignment = table.assignments.get(partitionId);
1715
- if (assignment === void 0 || assignment.primary === null || assignment.primary === nodeId) {
1716
- return;
1717
- }
1718
- if (!assignment.replicas.includes(nodeId) || assignment.inSyncSet.includes(nodeId)) {
1719
- return;
1720
- }
1721
- if (assignment.state !== "ACTIVE" && assignment.state !== "INITIALISING" && assignment.state !== "MIGRATING") {
1722
- return;
1723
- }
1724
- if (state.activeBootstraps.has(partitionKey(indexName, partitionId))) {
1725
- return;
1726
- }
1727
- startBootstrap(state, config, indexName, partitionId, assignment.primary);
2328
+ }, config.allocationDebounceMs);
1728
2329
  }
1729
2330
 
1730
2331
  // src/distribution/cluster/node-lifecycle/heartbeat.ts
@@ -1756,6 +2357,7 @@ async function stopRegistrationHeartbeat(state) {
1756
2357
  // src/distribution/cluster/node-lifecycle/join.ts
1757
2358
  async function joinCluster(config, watcherState, heartbeatState) {
1758
2359
  await config.coordinator.registerNode(config.registration);
2360
+ config.onRegistered?.();
1759
2361
  startRegistrationHeartbeat(heartbeatState, config);
1760
2362
  const initialTables = await loadInitialAllocations(config);
1761
2363
  processInitialAllocations(watcherState, config, initialTables);
@@ -1763,7 +2365,8 @@ async function joinCluster(config, watcherState, heartbeatState) {
1763
2365
  }
1764
2366
  async function loadInitialAllocations(config) {
1765
2367
  const tables = [];
1766
- for (const indexName of config.knownIndexNames) {
2368
+ const indexNames = /* @__PURE__ */ new Set([...config.knownIndexNames, ...await config.coordinator.listSchemas()]);
2369
+ for (const indexName of indexNames) {
1767
2370
  const table = await config.coordinator.getAllocation(indexName);
1768
2371
  if (table !== null) {
1769
2372
  tables.push(table);
@@ -1787,6 +2390,7 @@ var DEFAULT_NODE_LIFECYCLE_CONFIG = {
1787
2390
  // src/distribution/cluster/node-lifecycle/index.ts
1788
2391
  function createDataNodeLifecycle(config) {
1789
2392
  let status = "stopped";
2393
+ let registered = false;
1790
2394
  let watcherState = createAllocationWatcherState();
1791
2395
  const heartbeatState = createRegistrationHeartbeatState();
1792
2396
  let operationLock = Promise.resolve();
@@ -1813,6 +2417,12 @@ function createDataNodeLifecycle(config) {
1813
2417
  get nodeId() {
1814
2418
  return config.registration.nodeId;
1815
2419
  },
2420
+ get registered() {
2421
+ return registered;
2422
+ },
2423
+ get pendingPartitionCount() {
2424
+ return watcherState.pendingPartitions.size;
2425
+ },
1816
2426
  join() {
1817
2427
  return withLock(async () => {
1818
2428
  if (status === "active" || status === "joining") {
@@ -1831,10 +2441,21 @@ function createDataNodeLifecycle(config) {
1831
2441
  }
1832
2442
  status = "joining";
1833
2443
  try {
1834
- 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
+ );
1835
2455
  status = "active";
1836
2456
  } catch (error) {
1837
2457
  await stopRegistrationHeartbeat(heartbeatState);
2458
+ registered = false;
1838
2459
  status = "stopped";
1839
2460
  throw error;
1840
2461
  }
@@ -1851,6 +2472,7 @@ function createDataNodeLifecycle(config) {
1851
2472
  try {
1852
2473
  await leaveCluster(config);
1853
2474
  } finally {
2475
+ registered = false;
1854
2476
  status = "stopped";
1855
2477
  watcherState = createAllocationWatcherState();
1856
2478
  }
@@ -1869,6 +2491,7 @@ function createDataNodeLifecycle(config) {
1869
2491
  } catch (_) {
1870
2492
  }
1871
2493
  }
2494
+ registered = false;
1872
2495
  status = "shutdown";
1873
2496
  watcherState = createAllocationWatcherState();
1874
2497
  });
@@ -1925,6 +2548,9 @@ async function nodeHasOtherPartitions(indexName, removedPartitionId, deps) {
1925
2548
  if (assignment.replicas.includes(deps.nodeId)) {
1926
2549
  return true;
1927
2550
  }
2551
+ if (holdsLastCopy(assignment, deps.nodeId)) {
2552
+ return true;
2553
+ }
1928
2554
  }
1929
2555
  return false;
1930
2556
  }
@@ -3679,6 +4305,22 @@ async function executeBootstrapSync(state, entry, indexName, partitionId, primar
3679
4305
  return true;
3680
4306
  }
3681
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
+
3682
4324
  // src/distribution/cluster-node/write-routing/assignment.ts
3683
4325
  function resolvePartitionId(docId, partitionCount) {
3684
4326
  return fnv1a(docId) % partitionCount;
@@ -3701,8 +4343,8 @@ function requireAssignedPrimary(assignment, indexName, partitionId) {
3701
4343
  return assignment;
3702
4344
  }
3703
4345
  async function resolvePrimaryAssignment(indexName, docId, deps, requireLocalPrimary) {
3704
- const table = await deps.coordinator.getAllocation(indexName);
3705
- if (table === null || table.assignments.size === 0) {
4346
+ const table = await routableAllocation(deps.coordinator, indexName);
4347
+ if (table === null) {
3706
4348
  if (requireLocalPrimary) {
3707
4349
  throw new NarsilError(
3708
4350
  ErrorCodes.QUERY_ROUTING_FAILED,
@@ -3829,7 +4471,7 @@ async function sendToController(payload, controllerNodeId, deps) {
3829
4471
  }
3830
4472
  return false;
3831
4473
  }
3832
- function withTimeout(work, timeoutMs, fallback) {
4474
+ function withTimeout2(work, timeoutMs, fallback) {
3833
4475
  return new Promise((resolve) => {
3834
4476
  const timer = setTimeout(() => resolve(fallback), timeoutMs);
3835
4477
  timer.unref?.();
@@ -3884,9 +4526,9 @@ async function proposeAdmission(state, deps, indexName, partitionId, assignment,
3884
4526
  appliedSeqNo: cursor.appliedSeqNo,
3885
4527
  commitPoint: log.commitPoint
3886
4528
  };
3887
- 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);
3888
4530
  if (accepted && cursor.syncEpoch !== proposedEpoch) {
3889
- await withTimeout(withdrawAdmission(payload, controllerNodeId, deps), ADMISSION_TIMEOUT_MS, void 0);
4531
+ await withTimeout2(withdrawAdmission(payload, controllerNodeId, deps), ADMISSION_TIMEOUT_MS, void 0);
3890
4532
  }
3891
4533
  } finally {
3892
4534
  clearPendingAdmission(state, indexName, partitionId, replicaNodeId);
@@ -4094,6 +4736,114 @@ async function stopCatchUpPump(state) {
4094
4736
  }
4095
4737
  }
4096
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
+
4097
4847
  // src/distribution/cluster-node/local-engine.ts
4098
4848
  async function createClusterLocalEngine(config) {
4099
4849
  const core = createEngineCore(config);
@@ -4105,10 +4855,15 @@ async function createClusterLocalEngine(config) {
4105
4855
  }
4106
4856
  await core.analysisRebuild.reviewStaleIndexes();
4107
4857
  const engine = createNarsilFromCore(core, config);
4858
+ const heldPartitions = createHeldPartitionRecord(core);
4108
4859
  return Object.assign(engine, {
4109
4860
  createIndexWithUuid: (name, indexConfig, indexUuid) => createEngineIndex(core, config, name, indexConfig, indexUuid),
4110
4861
  indexUuidOf: (indexName) => core.indexRegistry.get(indexName)?.indexUuid,
4111
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),
4112
4867
  applyReplicationEntry: (entry) => applyReplicationEntry(core, entry),
4113
4868
  serializeReplicationPartition: (indexName, partitionId) => serializeReplicationPartition(core, indexName, partitionId),
4114
4869
  restoreReplicationPartition: (indexName, partitionId, bytes, schema, partitionCount) => restoreReplicationPartition(core, engine, indexName, partitionId, bytes, schema, partitionCount),
@@ -4950,6 +5705,28 @@ async function streamValidatedSnapshotRequest(message, sink, request, deps, opti
4950
5705
  }
4951
5706
  await acquireAndStream(message, sink, request, deps, options);
4952
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
+ }
4953
5730
 
4954
5731
  // src/distribution/query/validators/common.ts
4955
5732
  var MAX_PARTITION_IDS = MAX_PARTITION_COUNT;
@@ -6293,7 +7070,8 @@ function distributedResultToLocal(result, documents = /* @__PURE__ */ new Map())
6293
7070
  count: result.totalHits,
6294
7071
  elapsed: 0,
6295
7072
  cursor: result.cursor ?? void 0,
6296
- facets: result.facets !== null ? convertWireFacetsToLocal(result.facets, result.facetErrorBounds) : void 0
7073
+ facets: result.facets !== null ? convertWireFacetsToLocal(result.facets, result.facetErrorBounds) : void 0,
7074
+ coverage: result.coverage
6297
7075
  };
6298
7076
  }
6299
7077
  function convertLocalSortToWire(sort) {
@@ -6601,6 +7379,13 @@ async function handleSyncRequestMessage(message, respond, deps) {
6601
7379
  }
6602
7380
  );
6603
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
+ }
6604
7389
  if (message.sourceId !== deps.nodeId) {
6605
7390
  recordReplicaPosition(
6606
7391
  deps.writeDeps.catchUp,
@@ -6661,6 +7446,55 @@ async function sendSyncEntriesResponse(message, respond, deps, entries) {
6661
7446
  });
6662
7447
  }
6663
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
+
6664
7498
  // src/distribution/cluster-node/types/defaults.ts
6665
7499
  var DEFAULT_PARTITION_COUNT = 5;
6666
7500
  var DEFAULT_REPLICATION_FACTOR = 1;
@@ -7540,8 +8374,8 @@ function collectGroupFailure(failed, docIds, err, indexName, fallbackCode) {
7540
8374
  }
7541
8375
  }
7542
8376
  async function routeInsertBatch(indexName, documents, deps, options) {
7543
- const table = await deps.coordinator.getAllocation(indexName);
7544
- if (table === null || table.assignments.size === 0) {
8377
+ const table = await routableAllocation(deps.coordinator, indexName);
8378
+ if (table === null) {
7545
8379
  return deps.engine.insertBatch(indexName, documents, options);
7546
8380
  }
7547
8381
  const withIds = documents.map((doc) => ({
@@ -7579,8 +8413,8 @@ async function routeInsertBatch(indexName, documents, deps, options) {
7579
8413
  return { succeeded, failed };
7580
8414
  }
7581
8415
  async function routeRemoveBatch(indexName, docIds, deps) {
7582
- const table = await deps.coordinator.getAllocation(indexName);
7583
- if (table === null || table.assignments.size === 0) {
8416
+ const table = await routableAllocation(deps.coordinator, indexName);
8417
+ if (table === null) {
7584
8418
  return deps.engine.removeBatch(indexName, docIds);
7585
8419
  }
7586
8420
  const routed = routeBatchItems(table, indexName, docIds, (docId) => docId, deps.nodeId);
@@ -7608,8 +8442,8 @@ async function routeRemoveBatch(indexName, docIds, deps) {
7608
8442
  return { succeeded, failed };
7609
8443
  }
7610
8444
  async function routeUpdateBatch(indexName, updates, deps) {
7611
- const table = await deps.coordinator.getAllocation(indexName);
7612
- if (table === null || table.assignments.size === 0) {
8445
+ const table = await routableAllocation(deps.coordinator, indexName);
8446
+ if (table === null) {
7613
8447
  return deps.engine.updateBatch(indexName, updates);
7614
8448
  }
7615
8449
  const routed = routeBatchItems(table, indexName, updates, (update) => update.docId, deps.nodeId);
@@ -7710,6 +8544,12 @@ async function routeCreateIndex(name, config, options, coordinator, engine) {
7710
8544
  }
7711
8545
  throw createErr;
7712
8546
  }
8547
+ await waitForServingAllocation(
8548
+ coordinator,
8549
+ name,
8550
+ partitionCount,
8551
+ options?.waitForServingMs ?? DEFAULT_CREATE_INDEX_WAIT_MS
8552
+ );
7713
8553
  }
7714
8554
  async function withdrawPartialIndex(name, coordinator, engine) {
7715
8555
  try {
@@ -7987,6 +8827,9 @@ function createDataNodeHandler(deps) {
7987
8827
  case QueryMessageTypes.PREFLIGHT:
7988
8828
  await handlePreflight(message, respond, deps);
7989
8829
  return;
8830
+ case ClusterMessageTypes.PARTITION_STORES:
8831
+ await handlePartitionStores(message, respond, deps);
8832
+ return;
7990
8833
  default:
7991
8834
  return;
7992
8835
  }
@@ -8242,22 +9085,32 @@ async function fanOutSearch(indexName, params, globalStats, facetShardSize, node
8242
9085
  }
8243
9086
  return Promise.all(promises);
8244
9087
  }
8245
- 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
+ }
8246
9104
  let queriedPartitions = 0;
8247
9105
  let timedOutPartitions = 0;
8248
- let failedPartitions = unavailableCount;
8249
- for (const outcome of outcomes) {
8250
- const partitionCount = outcome.partitionIds.length;
8251
- switch (outcome.status) {
8252
- case "success":
8253
- queriedPartitions += partitionCount;
8254
- break;
8255
- case "timeout":
8256
- timedOutPartitions += partitionCount;
8257
- break;
8258
- case "failed":
8259
- failedPartitions += partitionCount;
8260
- 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;
8261
9114
  }
8262
9115
  }
8263
9116
  return {
@@ -8275,7 +9128,7 @@ async function sendWithTimeout(transport, targets, message, timeout, validate) {
8275
9128
  timerId.unref();
8276
9129
  }
8277
9130
  });
8278
- const sendPromise = sendToFirstReachableTarget(transport, targets, message).then((response) => {
9131
+ const sendPromise = sendToFirstReachableTarget2(transport, targets, message).then((response) => {
8279
9132
  const decoded = decode(response.payload);
8280
9133
  return validate(decoded);
8281
9134
  });
@@ -8294,7 +9147,7 @@ async function resolveTargets(nodeId, deps) {
8294
9147
  const targets = await deps.resolveNodeTargets(nodeId);
8295
9148
  return targets.length > 0 ? targets : [nodeId];
8296
9149
  }
8297
- async function sendToFirstReachableTarget(transport, targets, message) {
9150
+ async function sendToFirstReachableTarget2(transport, targets, message) {
8298
9151
  let lastError;
8299
9152
  for (const target of targets) {
8300
9153
  try {
@@ -8594,9 +9447,11 @@ async function distributedQuery(indexName, params, deps, config, selector) {
8594
9447
  const routing = selectReplicasForQuery(allocationTable, randomSelector);
8595
9448
  const totalPartitions = allocationTable.assignments.size;
8596
9449
  if (routing.unavailablePartitions.length > 0 && !resolvedConfig.allowPartialResults) {
8597
- throw new NarsilError(ErrorCodes.QUERY_NO_ACTIVE_REPLICA, "No active replica for one or more partitions", {
8598
- unavailablePartitions: routing.unavailablePartitions
8599
- });
9450
+ throw new NarsilError(
9451
+ ErrorCodes.QUERY_PARTIAL_FAILURE,
9452
+ "No active replica serves one or more partitions, and this node refuses partial results",
9453
+ { unavailablePartitions: routing.unavailablePartitions }
9454
+ );
8600
9455
  }
8601
9456
  const isHybrid = params.hybrid !== null && params.term !== null && params.vector !== null;
8602
9457
  if (isHybrid && params.searchAfter !== null) {
@@ -8658,7 +9513,7 @@ async function executeSingleFanOut(indexName, params, limit, offset, facetShardS
8658
9513
  if (sortFields !== null) {
8659
9514
  failOutcomesMissingSortValues(outcomes);
8660
9515
  }
8661
- const coverage = buildCoverage(totalPartitions, routing.unavailablePartitions.length, outcomes);
9516
+ const coverage = buildCoverage(totalPartitions, routing.unavailablePartitions, outcomes);
8662
9517
  if (!config.allowPartialResults) {
8663
9518
  const failedCount = coverage.timedOutPartitions + coverage.failedPartitions;
8664
9519
  if (failedCount > 0) {
@@ -8732,9 +9587,7 @@ async function executeHybridQuery(indexName, params, limit, offset, facetShardSi
8732
9587
  fanOutSearch(indexName, textParams, textGlobalStats, facetShardSize, routing.nodeToPartitions, deps, config),
8733
9588
  fanOutSearch(indexName, vectorParams, null, null, routing.nodeToPartitions, deps, config)
8734
9589
  ]);
8735
- const textCoverage = buildCoverage(totalPartitions, routing.unavailablePartitions.length, textOutcomes);
8736
- const vectorCoverage = buildCoverage(totalPartitions, routing.unavailablePartitions.length, vectorOutcomes);
8737
- const coverage = worstCaseCoverage(textCoverage, vectorCoverage);
9590
+ const coverage = buildCoverage(totalPartitions, routing.unavailablePartitions, textOutcomes, vectorOutcomes);
8738
9591
  if (!config.allowPartialResults) {
8739
9592
  const failedCount = coverage.timedOutPartitions + coverage.failedPartitions;
8740
9593
  if (failedCount > 0) {
@@ -8796,29 +9649,6 @@ async function executeHybridQuery(indexName, params, limit, offset, facetShardSi
8796
9649
  coverage
8797
9650
  };
8798
9651
  }
8799
- function worstCaseCoverage(a, b) {
8800
- const failed = Math.max(a.failedPartitions, b.failedPartitions);
8801
- const timedOut = Math.max(a.timedOutPartitions, b.timedOutPartitions);
8802
- const queried = Math.max(0, a.totalPartitions - failed - timedOut);
8803
- return {
8804
- totalPartitions: a.totalPartitions,
8805
- queriedPartitions: queried,
8806
- timedOutPartitions: timedOut,
8807
- failedPartitions: failed
8808
- };
8809
- }
8810
- async function activeAllocation(deps, indexName) {
8811
- const allocation = await deps.config.coordinator.getAllocation(indexName);
8812
- if (allocation === null || allocation.assignments.size === 0) {
8813
- return null;
8814
- }
8815
- for (const [, assignment] of allocation.assignments) {
8816
- if (assignment.state === "ACTIVE") {
8817
- return allocation;
8818
- }
8819
- }
8820
- return null;
8821
- }
8822
9652
  function strictScatterGroups(allocation, indexName) {
8823
9653
  const routing = selectReplicasForQuery(allocation, randomSelector);
8824
9654
  if (routing.unavailablePartitions.length > 0) {
@@ -8830,8 +9660,30 @@ function strictScatterGroups(allocation, indexName) {
8830
9660
  }
8831
9661
  return Array.from(routing.nodeToPartitions.entries(), ([nodeId, partitionIds]) => ({ nodeId, partitionIds }));
8832
9662
  }
9663
+ var READ_FAILURE_BY_TRANSPORT_CODE = {
9664
+ [TransportErrorCodes.CONNECT_FAILED]: ErrorCodes.QUERY_NO_ACTIVE_REPLICA,
9665
+ [TransportErrorCodes.PEER_UNAVAILABLE]: ErrorCodes.QUERY_NO_ACTIVE_REPLICA,
9666
+ [TransportErrorCodes.TIMEOUT]: ErrorCodes.QUERY_NODE_TIMEOUT,
9667
+ [TransportErrorCodes.MESSAGE_TOO_LARGE]: ErrorCodes.QUERY_ROUTING_FAILED,
9668
+ [TransportErrorCodes.DECODE_FAILED]: ErrorCodes.QUERY_ROUTING_FAILED
9669
+ };
9670
+ function readFailure(error, targetNodeId, indexName) {
9671
+ if (error instanceof NarsilError) {
9672
+ return error;
9673
+ }
9674
+ if (error instanceof TransportError) {
9675
+ return new NarsilError(
9676
+ READ_FAILURE_BY_TRANSPORT_CODE[error.code],
9677
+ `Node '${targetNodeId}' did not answer a read request for index '${indexName}'`,
9678
+ { indexName, targetNodeId, transportCode: error.code }
9679
+ );
9680
+ }
9681
+ return error;
9682
+ }
8833
9683
  async function sendReadRequest(deps, targetNodeId, message, indexName, validate) {
8834
- const response = await sendToNode2(deps.config, targetNodeId, message);
9684
+ const response = await sendToNode2(deps.config, targetNodeId, message).catch((error) => {
9685
+ throw readFailure(error, targetNodeId, indexName);
9686
+ });
8835
9687
  const decoded = decode(response.payload);
8836
9688
  if (response.type.endsWith(".error") || response.type === "error" || decoded.error === true) {
8837
9689
  throw new NarsilError(
@@ -8890,7 +9742,7 @@ async function gatherPartitionCounts(deps, indexName, allocation) {
8890
9742
  return { countsByPartition, language };
8891
9743
  }
8892
9744
  async function countCluster(deps, indexName) {
8893
- const allocation = await activeAllocation(deps, indexName);
9745
+ const allocation = await routableAllocation(deps.config.coordinator, indexName);
8894
9746
  if (allocation === null) {
8895
9747
  return deps.engine.countDocuments(indexName);
8896
9748
  }
@@ -8902,7 +9754,7 @@ async function countCluster(deps, indexName) {
8902
9754
  return total;
8903
9755
  }
8904
9756
  async function statsCluster(deps, indexName) {
8905
- const allocation = await activeAllocation(deps, indexName);
9757
+ const allocation = await routableAllocation(deps.config.coordinator, indexName);
8906
9758
  if (allocation === null) {
8907
9759
  return deps.engine.getStats(indexName);
8908
9760
  }
@@ -8928,7 +9780,7 @@ async function statsCluster(deps, indexName) {
8928
9780
  };
8929
9781
  }
8930
9782
  async function partitionStatsCluster(deps, indexName) {
8931
- const allocation = await activeAllocation(deps, indexName);
9783
+ const allocation = await routableAllocation(deps.config.coordinator, indexName);
8932
9784
  if (allocation === null) {
8933
9785
  return deps.engine.getPartitionStats(indexName);
8934
9786
  }
@@ -8982,7 +9834,7 @@ async function gatherLocalPage(deps, indexName, payload, partitionIds, sortField
8982
9834
  return { entries, total: result.total, hasMore: result.cursor !== null };
8983
9835
  }
8984
9836
  async function listCluster(deps, indexName, params) {
8985
- const allocation = await activeAllocation(deps, indexName);
9837
+ const allocation = await routableAllocation(deps.config.coordinator, indexName);
8986
9838
  if (allocation === null) {
8987
9839
  return deps.engine.listDocuments(indexName, params);
8988
9840
  }
@@ -9069,7 +9921,7 @@ function suggestNodeLimit(clientLimit) {
9069
9921
  return Math.min(oversampled, MAX_SUGGEST_SCATTER_LIMIT);
9070
9922
  }
9071
9923
  async function suggestCluster(deps, indexName, params) {
9072
- const allocation = await activeAllocation(deps, indexName);
9924
+ const allocation = await routableAllocation(deps.config.coordinator, indexName);
9073
9925
  if (allocation === null) {
9074
9926
  return deps.engine.suggest(indexName, params);
9075
9927
  }
@@ -9113,7 +9965,7 @@ async function suggestCluster(deps, indexName, params) {
9113
9965
  return result;
9114
9966
  }
9115
9967
  async function preflightCluster(deps, indexName, params) {
9116
- const allocation = await activeAllocation(deps, indexName);
9968
+ const allocation = await routableAllocation(deps.config.coordinator, indexName);
9117
9969
  if (allocation === null) {
9118
9970
  return deps.engine.preflight(indexName, params);
9119
9971
  }
@@ -9148,14 +10000,27 @@ async function preflightCluster(deps, indexName, params) {
9148
10000
 
9149
10001
  // src/distribution/cluster-node/reads/index.ts
9150
10002
  async function readClusterDocuments(deps, indexName, docIds) {
9151
- const allocation = await activeAllocation(deps, indexName);
10003
+ const allocation = await routableAllocation(deps.config.coordinator, indexName);
9152
10004
  if (allocation === null) {
9153
10005
  return deps.engine.getMultiple(indexName, docIds);
9154
10006
  }
9155
10007
  return readDistributedDocuments(deps.config, deps.nodeId, deps.engine, indexName, docIds, allocation);
9156
10008
  }
10009
+ function routingConfig(query) {
10010
+ if (query === void 0) {
10011
+ return void 0;
10012
+ }
10013
+ const config = {};
10014
+ if (query.allowPartialResults !== void 0) {
10015
+ config.allowPartialResults = query.allowPartialResults;
10016
+ }
10017
+ if (query.partitionTimeout !== void 0) {
10018
+ config.partitionTimeout = query.partitionTimeout;
10019
+ }
10020
+ return config;
10021
+ }
9157
10022
  async function queryCluster(deps, indexName, params) {
9158
- const allocation = await activeAllocation(deps, indexName);
10023
+ const allocation = await routableAllocation(deps.config.coordinator, indexName);
9159
10024
  if (allocation === null) {
9160
10025
  return deps.engine.query(indexName, params);
9161
10026
  }
@@ -9166,7 +10031,7 @@ async function queryCluster(deps, indexName, params) {
9166
10031
  getAllocation: (idx) => deps.config.coordinator.getAllocation(idx),
9167
10032
  resolveNodeTargets: deps.resolveNodeTargets
9168
10033
  };
9169
- const distributed = await distributedQuery(indexName, wireParams, queryDeps);
10034
+ const distributed = await distributedQuery(indexName, wireParams, queryDeps, routingConfig(deps.config.query));
9170
10035
  const documents = await fetchDistributedDocuments(
9171
10036
  deps.config,
9172
10037
  deps.nodeId,
@@ -9182,7 +10047,7 @@ async function queryCluster(deps, indexName, params) {
9182
10047
  // src/distribution/cluster-node/clear.ts
9183
10048
  var CLEAR_PAGE_SIZE = 1e3;
9184
10049
  async function clearCluster(readDeps, writeDeps, indexName) {
9185
- const allocation = await activeAllocation(readDeps, indexName);
10050
+ const allocation = await routableAllocation(readDeps.config.coordinator, indexName);
9186
10051
  if (allocation === null) {
9187
10052
  return readDeps.engine.clear(indexName);
9188
10053
  }
@@ -9292,6 +10157,53 @@ function createClusterNodeOperations(deps) {
9292
10157
  };
9293
10158
  }
9294
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
+
9295
10207
  // src/distribution/replication/types.ts
9296
10208
  var DEFAULT_LOG_RETENTION_BYTES = 268435456;
9297
10209
 
@@ -9527,14 +10439,29 @@ function seedReplicationLog(replicationLogs, indexName, partitionId, startSeqNo,
9527
10439
  })
9528
10440
  );
9529
10441
  }
9530
-
9531
- // src/distribution/cluster-node/transport-listener.ts
9532
10442
  var CONTROLLER_MESSAGE_TYPES = /* @__PURE__ */ new Set([
9533
10443
  ReplicationMessageTypes.INSYNC_ADD,
9534
10444
  ReplicationMessageTypes.INSYNC_REMOVE,
9535
10445
  ClusterMessageTypes.BOOTSTRAP_COMPLETE
9536
10446
  ]);
9537
- 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) {
9538
10465
  let controllerHandler = null;
9539
10466
  const transport = {
9540
10467
  send(target, message) {
@@ -9561,9 +10488,17 @@ function createMultiplexedControllerTransport(baseTransport) {
9561
10488
  createHandler(dataHandler) {
9562
10489
  return async (message, respond) => {
9563
10490
  if (CONTROLLER_MESSAGE_TYPES.has(message.type)) {
9564
- if (controllerHandler !== null) {
9565
- 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;
9566
10500
  }
10501
+ await controllerHandler(message, createSingleResponseRespond(respond));
9567
10502
  return;
9568
10503
  }
9569
10504
  await dataHandler(message, respond);
@@ -9584,6 +10519,7 @@ function createSingleResponseRespond(respond) {
9584
10519
 
9585
10520
  // src/distribution/cluster-node/validate-config.ts
9586
10521
  var MAX_WAIT_FOR_ACTIVE_REPLICAS = 255;
10522
+ var MIN_CONTROLLER_LEASE_TTL_MS = 1e3;
9587
10523
  function validateClusterNodeConfig(config) {
9588
10524
  if (config.address.length === 0) {
9589
10525
  throw new NarsilError(ErrorCodes.CONFIG_INVALID, "ClusterNodeConfig.address must not be empty");
@@ -9627,6 +10563,42 @@ function validateClusterNodeConfig(config) {
9627
10563
  );
9628
10564
  }
9629
10565
  }
10566
+ if (config.query !== void 0) {
10567
+ const { allowPartialResults, partitionTimeout } = config.query;
10568
+ if (allowPartialResults !== void 0 && typeof allowPartialResults !== "boolean") {
10569
+ throw new NarsilError(
10570
+ ErrorCodes.CONFIG_INVALID,
10571
+ "ClusterNodeConfig.query.allowPartialResults must be a boolean",
10572
+ {
10573
+ allowPartialResults
10574
+ }
10575
+ );
10576
+ }
10577
+ if (partitionTimeout !== void 0 && (!Number.isInteger(partitionTimeout) || partitionTimeout <= 0)) {
10578
+ throw new NarsilError(
10579
+ ErrorCodes.CONFIG_INVALID,
10580
+ "ClusterNodeConfig.query.partitionTimeout must be a positive integer",
10581
+ { partitionTimeout }
10582
+ );
10583
+ }
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
+ }
9630
10602
  }
9631
10603
 
9632
10604
  // src/distribution/cluster-node/index.ts
@@ -9652,13 +10624,8 @@ async function createClusterNode(config) {
9652
10624
  async function resolveNodeTargets3(targetNodeId) {
9653
10625
  return resolveNodeTargets2(config, targetNodeId);
9654
10626
  }
9655
- const forwardOnError = (error) => {
9656
- if (config.onError === void 0) {
9657
- return;
9658
- }
9659
- const wrapped = error instanceof Error ? error : new Error(String(error));
9660
- config.onError(wrapped);
9661
- };
10627
+ const forwardOnError = createErrorForwarder(config.onError);
10628
+ const forwardAllocationError = createAllocationErrorForwarder(forwardOnError);
9662
10629
  const registration = {
9663
10630
  nodeId,
9664
10631
  address: config.address,
@@ -9669,7 +10636,7 @@ async function createClusterNode(config) {
9669
10636
  };
9670
10637
  const hasDataRole = roles.includes("data");
9671
10638
  const hasControllerRole = roles.includes("controller");
9672
- const controllerTransport = hasDataRole && hasControllerRole ? createMultiplexedControllerTransport(config.transport) : null;
10639
+ const controllerTransport = hasDataRole ? createMultiplexedControllerTransport(config.transport, nodeId) : null;
9673
10640
  const writeDeps = {
9674
10641
  nodeId,
9675
10642
  coordinator: config.coordinator,
@@ -9683,9 +10650,16 @@ async function createClusterNode(config) {
9683
10650
  waitForActiveReplicas: config.replication?.waitForActiveReplicas
9684
10651
  };
9685
10652
  const orphanedIndexes = /* @__PURE__ */ new Set();
9686
- 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
+ }
9687
10659
  let controller = null;
9688
10660
  let unregisterHandler = null;
10661
+ let unregisterNotReadyHandler = null;
10662
+ let serving = false;
9689
10663
  let isShutdown = false;
9690
10664
  let activeOps = 0;
9691
10665
  let drainResolve = null;
@@ -9701,76 +10675,59 @@ async function createClusterNode(config) {
9701
10675
  restoreReplicationPartition: engine.restoreReplicationPartition,
9702
10676
  onError: forwardOnError
9703
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
+ };
9704
10686
  async function bootstrapPartition2(indexName, partitionId, primaryNodeId) {
9705
- 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);
9706
10688
  if (succeeded) {
9707
10689
  await adoptClusterIdentity(engine, config.coordinator, indexName);
10690
+ await engine.recordHeldPartition(indexName, partitionId);
9708
10691
  }
9709
10692
  return succeeded;
9710
10693
  }
9711
- async function preparePrimaryPartition(indexName) {
9712
- const schema = await config.coordinator.getSchema(indexName);
9713
- if (schema === null) {
9714
- return false;
9715
- }
9716
- const allocation = await config.coordinator.getAllocation(indexName);
9717
- if (allocation === null || allocation.assignments.size === 0) {
9718
- return false;
9719
- }
9720
- const existing = engine.listIndexes().find((index) => index.name === indexName);
9721
- if (existing !== void 0) {
9722
- const schemaError = validateRestoredSchema(engine, indexName, nodeId, schema);
9723
- if (schemaError !== null) {
9724
- forwardOnError(schemaError);
9725
- return false;
9726
- }
9727
- return true;
9728
- }
9729
- try {
9730
- await engine.createIndex(indexName, {
9731
- schema,
9732
- 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
9733
10718
  });
9734
- } catch (error) {
9735
- if (!(error instanceof NarsilError) || error.code !== ErrorCodes.INDEX_ALREADY_EXISTS) {
9736
- forwardOnError(error);
9737
- return false;
9738
- }
9739
10719
  }
9740
- return true;
9741
- }
9742
- if (hasDataRole) {
9743
- lifecycle = createDataNodeLifecycle({
9744
- registration,
9745
- coordinator: config.coordinator,
9746
- transport: config.transport,
9747
- knownIndexNames: [],
9748
- bootstrapRetryBaseMs: DEFAULT_NODE_LIFECYCLE_CONFIG.bootstrapRetryBaseMs,
9749
- bootstrapRetryMaxMs: DEFAULT_NODE_LIFECYCLE_CONFIG.bootstrapRetryMaxMs,
9750
- bootstrapMaxRetries: DEFAULT_NODE_LIFECYCLE_CONFIG.bootstrapMaxRetries,
9751
- allocationDebounceMs: DEFAULT_NODE_LIFECYCLE_CONFIG.allocationDebounceMs,
9752
- nodeHeartbeatIntervalMs: DEFAULT_NODE_LIFECYCLE_CONFIG.nodeHeartbeatIntervalMs,
9753
- onBootstrapPartition: bootstrapPartition2,
9754
- onRemovePartition: (indexName, partitionId) => {
9755
- clearBootstrapSyncIndex(bootstrapSyncState, indexName, partitionId);
9756
- replicationLogs.delete(replicationLogKey2(indexName, partitionId));
9757
- void cleanupRemovedPartition(indexName, partitionId, {
9758
- engine,
9759
- coordinator: config.coordinator,
9760
- nodeId,
9761
- onError: forwardOnError
9762
- });
9763
- }
9764
- });
9765
- }
10720
+ });
9766
10721
  if (hasControllerRole) {
9767
10722
  controller = createController({
9768
10723
  nodeId,
9769
10724
  coordinator: config.coordinator,
9770
10725
  transport: controllerTransport?.transport ?? config.transport,
9771
- leaseTtlMs: DEFAULT_CONTROLLER_CONFIG.leaseTtlMs,
9772
- standbyRetryMs: DEFAULT_CONTROLLER_CONFIG.standbyRetryMs,
9773
- 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
9774
10731
  });
9775
10732
  }
9776
10733
  function guardShutdown() {
@@ -9795,21 +10752,16 @@ async function createClusterNode(config) {
9795
10752
  }
9796
10753
  }
9797
10754
  const readDeps = { config, nodeId, engine, resolveNodeTargets: resolveNodeTargets3 };
9798
- const cluster = {
9799
- async getAllocation(indexName) {
9800
- return trackOp(null, () => config.coordinator.getAllocation(indexName));
9801
- },
9802
- getNodeInfo() {
9803
- const status = lifecycle !== null ? lifecycle.status : isShutdown ? "shutdown" : "stopped";
9804
- return { nodeId, roles: [...roles], status };
9805
- },
9806
- isControllerActive() {
9807
- if (controller === null) {
9808
- return false;
9809
- }
9810
- return controller.isActive;
9811
- }
9812
- };
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
+ });
9813
10765
  const node = {
9814
10766
  get nodeId() {
9815
10767
  return nodeId;
@@ -9843,9 +10795,10 @@ async function createClusterNode(config) {
9843
10795
  orphanedIndexes.add(indexName);
9844
10796
  }
9845
10797
  }
9846
- if (lifecycle !== null) {
9847
- await lifecycle.join();
10798
+ if (hasDataRole) {
10799
+ unregisterNotReadyHandler = await config.transport.listen(createNotReadyHandler(nodeId));
9848
10800
  }
10801
+ await lifecycle.join();
9849
10802
  if (controller !== null) {
9850
10803
  await controller.start();
9851
10804
  }
@@ -9863,6 +10816,7 @@ async function createClusterNode(config) {
9863
10816
  unregisterHandler = await config.transport.listen(listener);
9864
10817
  startCatchUpPump(writeDeps.catchUp, writeDeps);
9865
10818
  }
10819
+ serving = true;
9866
10820
  },
9867
10821
  async shutdown() {
9868
10822
  await stopCatchUpPump(writeDeps.catchUp);
@@ -9870,6 +10824,7 @@ async function createClusterNode(config) {
9870
10824
  return;
9871
10825
  }
9872
10826
  isShutdown = true;
10827
+ serving = false;
9873
10828
  if (activeOps > 0) {
9874
10829
  await new Promise((resolve) => {
9875
10830
  drainResolve = resolve;
@@ -9879,12 +10834,15 @@ async function createClusterNode(config) {
9879
10834
  unregisterHandler();
9880
10835
  unregisterHandler = null;
9881
10836
  }
9882
- if (lifecycle !== null) {
9883
- await lifecycle.shutdown();
10837
+ if (unregisterNotReadyHandler !== null) {
10838
+ unregisterNotReadyHandler();
10839
+ unregisterNotReadyHandler = null;
9884
10840
  }
10841
+ await lifecycle.shutdown();
9885
10842
  if (controller !== null) {
9886
10843
  await controller.shutdown();
9887
10844
  }
10845
+ await Promise.all([...heldPartitionWrites]);
9888
10846
  await engine.shutdown();
9889
10847
  }
9890
10848
  };