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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/adapters/filesystem.mjs +1 -1
  2. package/dist/adapters/indexeddb.mjs +1 -1
  3. package/dist/{chunk-TLRUUMVE.mjs → chunk-P4ELXVJQ.mjs} +1 -0
  4. package/dist/{chunk-KEDL6FFZ.mjs → chunk-WASNWIEK.mjs} +1142 -327
  5. package/dist/{chunk-YGQMKTKG.mjs → chunk-XNCJFU2E.mjs} +121 -27
  6. package/dist/{chunk-O4Q5JYNK.mjs → chunk-XWICBGYT.mjs} +1 -1
  7. package/dist/{client-B5uO5jL1.d.ts → client-C5BkUqB8.d.ts} +25 -2
  8. package/dist/client.d.ts +7 -7
  9. package/dist/client.mjs +11 -2
  10. package/dist/{config-NgUCkc6a.d.ts → config-B-ZI-ft5.d.ts} +2 -2
  11. package/dist/{config-B09-cud9.d.ts → config-BZRExglL.d.ts} +20 -3
  12. package/dist/{direct-executor-7O23GEHE.mjs → direct-executor-IMXNRS7O.mjs} +2 -2
  13. package/dist/distribution/coordinator/etcd.d.ts +2 -2
  14. package/dist/distribution/coordinator/etcd.mjs +1 -1
  15. package/dist/distribution/coordinator/in-memory.d.ts +2 -2
  16. package/dist/distribution/transport/grpc.mjs +1 -1
  17. package/dist/distribution.d.ts +24 -10
  18. package/dist/distribution.mjs +1023 -864
  19. package/dist/embeddings/openai.mjs +1 -1
  20. package/dist/{engine-CjLhdDIm.d.ts → engine-Cjr1uBLa.d.ts} +28 -6
  21. package/dist/index.browser.mjs +1237 -330
  22. package/dist/index.d.ts +9 -9
  23. package/dist/index.mjs +4 -4
  24. package/dist/{options-BakECW-r.d.ts → options-BC68VPoE.d.ts} +9 -2
  25. package/dist/persistence/durability/checkpoint-worker.d.ts +3 -1
  26. package/dist/persistence/durability/checkpoint-worker.mjs +4 -4
  27. package/dist/react.d.ts +4 -4
  28. package/dist/react.mjs +1 -0
  29. package/dist/{schema-DXOd96kx.d.ts → schema-B7juWg8Q.d.ts} +1 -1
  30. package/dist/{search-BGouJxH4.d.ts → search-DDHjfXhJ.d.ts} +14 -3
  31. package/dist/server.d.ts +8 -8
  32. package/dist/server.mjs +27 -6
  33. package/dist/{types-BTE33VZS.d.ts → types-DsSSjY4d.d.ts} +1 -1
  34. package/dist/workers/entry.mjs +2 -2
  35. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
- import { createEngineCore, createNarsilFromCore, runEngineQueryStats, runEngineListDocuments, runEngineSuggest, runEnginePreflight, runEngineQuery, createEngineIndex, applyIndexEntry, applyDeleteEntry, DEFAULT_SUGGEST_LIMIT, MAX_SUGGEST_LIMIT, now, listBindingOf, validateEntryBatchPayload, createAckMessage, validateEntryPayload, validateForwardBatchPayload, createForwardBatchResultMessage, queryBindingOf, foldGroupReducer, clampLimit, createForwardBatchMessage, validateForwardBatchResultPayload, createForwardMessage, validateReplicationEntry, MAX_SUGGEST_SCATTER_LIMIT, chunkByBudget, WIRE_BATCH_BUDGET, dedupePinnedEntries, SUGGEST_OVERSAMPLE_FACTOR, SUGGEST_OVERSAMPLE_PADDING, createEntryMessage, computeEntryChecksum, createEntryBatchMessage, createInsyncConfirmMessage, validateAckPayload, createInsyncRemoveMessage, validateInsyncConfirmPayload, createInsyncAddMessage } from './chunk-KEDL6FFZ.mjs';
1
+ import { createEngineCore, createNarsilFromCore, runEngineQueryStats, runEngineListDocuments, runEngineSuggest, runEnginePreflight, runEngineQuery, createEngineIndex, applyIndexEntry, applyDeleteEntry, DEFAULT_SUGGEST_LIMIT, MAX_SUGGEST_LIMIT, now, listBindingOf, validateEntryBatchPayload, createAckMessage, validateEntryPayload, validateForwardBatchPayload, createForwardBatchResultMessage, queryBindingOf, foldGroupReducer, clampLimit, createForwardBatchMessage, validateForwardBatchResultPayload, createForwardMessage, validateReplicationEntry, MAX_SUGGEST_SCATTER_LIMIT, chunkByBudget, WIRE_BATCH_BUDGET, computeEntryChecksum, dedupePinnedEntries, SUGGEST_OVERSAMPLE_FACTOR, SUGGEST_OVERSAMPLE_PADDING, createEntryMessage, createEntryBatchMessage, createInsyncConfirmMessage, validateAckPayload, createInsyncRemoveMessage, validateInsyncConfirmPayload, createInsyncAddMessage } from './chunk-WASNWIEK.mjs';
2
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-O4Q5JYNK.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, RESULT_WINDOW, FIELD_FILTER_OPERATORS, MAX_NESTING_DEPTH } from './chunk-YGQMKTKG.mjs';
3
+ import { validateIndexName, indexConfigKey, putIndexMetadata, getIndexMetadata, MAX_PARTITION_COUNT, MAX_INDEX_NAME_LENGTH } from './chunk-XWICBGYT.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, RESULT_WINDOW, FIELD_FILTER_OPERATORS, MAX_NESTING_DEPTH } from './chunk-XNCJFU2E.mjs';
5
5
  import './chunk-TGP5M5R7.mjs';
6
6
  import './chunk-OBXS7OZM.mjs';
7
7
  import './chunk-WDL632CN.mjs';
@@ -9,7 +9,7 @@ import { crc32 } from './chunk-KMMJMDUA.mjs';
9
9
  import './chunk-U2BKYEC7.mjs';
10
10
  import { compareCodePoints, toComparableSortValue, compareComparableKeys, compareSortValues } from './chunk-BFIH7OV5.mjs';
11
11
  import { generateId } from './chunk-WTONLDU3.mjs';
12
- import { ErrorCodes, NarsilError, describeError } from './chunk-TLRUUMVE.mjs';
12
+ import { ErrorCodes, NarsilError, describeError } from './chunk-P4ELXVJQ.mjs';
13
13
  import { encode, decode } from '@msgpack/msgpack';
14
14
 
15
15
  // src/distribution/cluster/controller/types.ts
@@ -4618,7 +4618,7 @@ function batchBytes(entries) {
4618
4618
  function replicaHasFallenOutOfRetention(log, cursor) {
4619
4619
  const oldest = log.oldestSeqNo;
4620
4620
  if (oldest === void 0) {
4621
- return false;
4621
+ return cursor.appliedSeqNo < log.localLogEnd;
4622
4622
  }
4623
4623
  return cursor.appliedSeqNo + 1 < oldest;
4624
4624
  }
@@ -4844,473 +4844,766 @@ async function writeHeldPartitions(core, indexName, next, indexesBehindOnDisk) {
4844
4844
  indexesBehindOnDisk.delete(indexName);
4845
4845
  }
4846
4846
 
4847
- // src/distribution/cluster-node/local-engine.ts
4848
- async function createClusterLocalEngine(config) {
4849
- const core = createEngineCore(config);
4850
- if (core.durability !== null) {
4851
- await core.durability.manager.recover();
4852
- }
4853
- if (core.invalidation !== null) {
4854
- await core.invalidation.start();
4855
- }
4856
- await core.analysisRebuild.reviewStaleIndexes();
4857
- const engine = createNarsilFromCore(core, config);
4858
- const heldPartitions = createHeldPartitionRecord(core);
4859
- return Object.assign(engine, {
4860
- createIndexWithUuid: (name, indexConfig, indexUuid) => createEngineIndex(core, config, name, indexConfig, indexUuid),
4861
- indexUuidOf: (indexName) => core.indexRegistry.get(indexName)?.indexUuid,
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),
4867
- applyReplicationEntry: (entry) => applyReplicationEntry(core, entry),
4868
- serializeReplicationPartition: (indexName, partitionId) => serializeReplicationPartition(core, indexName, partitionId),
4869
- restoreReplicationPartition: (indexName, partitionId, bytes, schema, partitionCount) => restoreReplicationPartition(core, engine, indexName, partitionId, bytes, schema, partitionCount),
4870
- queryPartitions: (indexName, params, partitionIds, globalStats) => runEngineQuery(core, indexName, params, { partitionIds, globalStats }),
4871
- preflightPartitions: (indexName, params, partitionIds) => runEnginePreflight(core, indexName, params, { partitionIds }),
4872
- suggestPartitions: (indexName, params, partitionIds) => runEngineSuggest(core, indexName, params, partitionIds),
4873
- listPartitions: (indexName, params, partitionIds) => runEngineListDocuments(core, indexName, params, partitionIds),
4874
- collectQueryStats: (indexName, terms, partitionIds) => runEngineQueryStats(core, indexName, terms, partitionIds)
4875
- });
4876
- }
4877
- async function stampIndexUuid(core, indexName, indexUuid) {
4878
- const entry = core.indexRegistry.get(indexName);
4879
- if (entry === void 0 || entry.indexUuid === indexUuid) {
4880
- return;
4881
- }
4882
- core.indexRegistry.set(indexName, { ...entry, indexUuid });
4883
- if (core.durability !== null) {
4884
- await core.durability.manager.persistMetadata(indexName);
4885
- }
4886
- }
4887
- async function serializeReplicationPartition(core, indexName, partitionId) {
4888
- core.guardShutdown();
4889
- return core.requireManager(indexName).serializePartitionToBytes(partitionId);
4890
- }
4891
- async function restoreReplicationPartition(core, engine, indexName, partitionId, bytes, schema, partitionCount) {
4892
- core.guardShutdown();
4893
- validatePartitionRestoreTarget(indexName, partitionId, partitionCount);
4894
- let partition;
4895
- try {
4896
- partition = deserializePayloadV2(bytes);
4897
- } catch (err) {
4898
- const cause = err instanceof Error ? err.message : String(err);
4899
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, `partition snapshot decode failed: ${cause}`, {
4900
- indexName,
4901
- partitionId,
4902
- cause
4903
- });
4904
- }
4905
- validatePartitionSnapshotPayload(indexName, partitionId, partitionCount, schema, partition);
4906
- const trackPositions = core.indexRegistry.get(indexName)?.config.trackPositions ?? true;
4907
- createPartitionIndex(partitionId, trackPositions).deserialize(partition, schema);
4908
- const restoreIndex = await ensureReplicationIndex(core, engine, indexName, schema, partition.language, partitionCount);
4909
- if (restoreIndex.created) {
4910
- await core.orchestrator.replicateToWorkers({
4911
- type: "createIndex",
4912
- indexName,
4913
- config: restoreIndex.config,
4914
- requestId: `replicate-partition-index-${indexName}`
4915
- });
4916
- }
4917
- const manager = core.requireManager(indexName);
4918
- manager.deserializePartition(partitionId, partition);
4919
- await core.orchestrator.replicateToWorkers({
4920
- type: "deserialize",
4921
- indexName,
4922
- partitionId,
4923
- data: partition,
4924
- requestId: `replicate-partition-restore-${indexName}-${partitionId}`
4925
- });
4926
- }
4927
- function validatePartitionRestoreTarget(indexName, partitionId, partitionCount) {
4928
- if (!Number.isInteger(partitionId) || partitionId < 0) {
4929
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partitionId must be a non-negative integer", {
4930
- indexName,
4931
- partitionId
4932
- });
4933
- }
4934
- if (!Number.isInteger(partitionCount) || partitionCount <= 0 || partitionCount > MAX_PARTITION_COUNT) {
4935
- throw new NarsilError(
4936
- ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED,
4937
- `partitionCount must be an integer between 1 and ${MAX_PARTITION_COUNT}`,
4938
- { indexName, partitionCount }
4939
- );
4940
- }
4941
- if (partitionId >= partitionCount) {
4942
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partitionId is outside partitionCount", {
4943
- indexName,
4944
- partitionId,
4945
- partitionCount
4946
- });
4947
- }
4847
+ // src/distribution/replication/types.ts
4848
+ var DEFAULT_LOG_RETENTION_BYTES = 268435456;
4849
+
4850
+ // src/distribution/replication/log.ts
4851
+ var ENTRY_FIXED_OVERHEAD_BYTES2 = 40;
4852
+ function estimateEntrySize(entry) {
4853
+ return ENTRY_FIXED_OVERHEAD_BYTES2 + entry.indexName.length + entry.documentId.length + (entry.document?.byteLength ?? 0);
4948
4854
  }
4949
- function validatePartitionSnapshotPayload(indexName, partitionId, partitionCount, schema, partition) {
4950
- if (partition.indexName !== indexName) {
4951
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partition snapshot indexName mismatch", {
4952
- expectedIndexName: indexName,
4953
- receivedIndexName: partition.indexName
4954
- });
4955
- }
4956
- if (partition.partitionId !== partitionId) {
4957
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partition snapshot partitionId mismatch", {
4958
- indexName,
4959
- expectedPartitionId: partitionId,
4960
- receivedPartitionId: partition.partitionId
4961
- });
4962
- }
4963
- if (partition.totalPartitions !== partitionCount) {
4964
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partition snapshot partition count mismatch", {
4965
- indexName,
4966
- partitionId,
4967
- expectedPartitionCount: partitionCount,
4968
- receivedPartitionCount: partition.totalPartitions
4969
- });
4970
- }
4971
- const expectedSchema = flattenSchema(schema);
4972
- for (const [field, expectedType] of Object.entries(expectedSchema)) {
4973
- if (partition.schema[field] !== expectedType) {
4974
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partition snapshot schema mismatch", {
4975
- indexName,
4976
- partitionId,
4977
- field,
4978
- expected: expectedType,
4979
- received: partition.schema[field] ?? "(absent)"
4980
- });
4981
- }
4855
+ var computeChecksum = computeEntryChecksum;
4856
+ function createReplicationLog(partitionId, config) {
4857
+ const retentionBytes = config?.logRetentionBytes ?? DEFAULT_LOG_RETENTION_BYTES;
4858
+ let nextSeqNo = config?.startSeqNo ?? 1;
4859
+ let localLogEnd = nextSeqNo - 1;
4860
+ let localLogEndPrimaryTerm = config?.lastPrimaryTerm ?? 0;
4861
+ let commitPoint = nextSeqNo - 1;
4862
+ let entries = [];
4863
+ let headIndex = 0;
4864
+ let totalSizeBytes = 0;
4865
+ function liveCount() {
4866
+ return entries.length - headIndex;
4982
4867
  }
4983
- for (const field of Object.keys(partition.schema)) {
4984
- if (!(field in expectedSchema)) {
4985
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partition snapshot schema contains extra field", {
4986
- indexName,
4987
- partitionId,
4988
- field
4989
- });
4868
+ function compact() {
4869
+ if (headIndex > 0) {
4870
+ entries = entries.slice(headIndex);
4871
+ headIndex = 0;
4990
4872
  }
4991
4873
  }
4992
- }
4993
- async function ensureReplicationIndex(core, engine, indexName, schema, languageName, partitionCount) {
4994
- const language = getLanguage(languageName);
4995
- const existing = core.indexRegistry.get(indexName);
4996
- let created = false;
4997
- if (existing === void 0) {
4998
- const indexConfig = {
4999
- schema,
5000
- language: language.name,
5001
- partitions: { maxPartitions: partitionCount }
5002
- };
5003
- try {
5004
- await engine.createIndex(indexName, indexConfig);
5005
- created = true;
5006
- } catch (err) {
5007
- if (!(err instanceof NarsilError) || err.code !== ErrorCodes.INDEX_ALREADY_EXISTS) {
5008
- throw err;
4874
+ function findIndexBySeqNo(seqNo) {
4875
+ let low = headIndex;
4876
+ let high = entries.length - 1;
4877
+ while (low <= high) {
4878
+ const mid = low + high >>> 1;
4879
+ const midSeqNo = entries[mid].seqNo;
4880
+ if (midSeqNo < seqNo) {
4881
+ low = mid + 1;
4882
+ } else if (midSeqNo > seqNo) {
4883
+ high = mid - 1;
4884
+ } else {
4885
+ return mid;
5009
4886
  }
5010
4887
  }
4888
+ return low;
5011
4889
  }
5012
- const entry = core.requireIndex(indexName);
5013
- if (entry.language.name !== language.name) {
5014
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partition snapshot language mismatch", {
5015
- indexName,
5016
- expectedLanguage: entry.language.name,
5017
- receivedLanguage: language.name
5018
- });
5019
- }
5020
- validateExistingSchema(indexName, schema, entry.config.schema);
5021
- const manager = core.requireManager(indexName);
5022
- if (manager.partitionCount > partitionCount) {
5023
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "local index has more partitions than allocation", {
5024
- indexName,
5025
- localPartitionCount: manager.partitionCount,
5026
- allocationPartitionCount: partitionCount
5027
- });
5028
- }
5029
- if (entry.config.partitions === void 0) {
5030
- entry.config.partitions = { maxPartitions: partitionCount };
5031
- } else if ((entry.config.partitions.maxPartitions ?? manager.partitionCount) < partitionCount) {
5032
- entry.config.partitions.maxPartitions = partitionCount;
4890
+ function evictOldEntries() {
4891
+ while (liveCount() > 0 && totalSizeBytes > retentionBytes) {
4892
+ totalSizeBytes -= estimateEntrySize(entries[headIndex]);
4893
+ entries[headIndex] = null;
4894
+ headIndex += 1;
4895
+ }
4896
+ if (totalSizeBytes < 0) {
4897
+ totalSizeBytes = 0;
4898
+ }
4899
+ if (headIndex >= 256 || headIndex > liveCount()) {
4900
+ compact();
4901
+ }
5033
4902
  }
5034
- while (manager.partitionCount < partitionCount) {
5035
- manager.addPartition();
4903
+ function assertEntryFitsRetention(entry) {
4904
+ const entrySize = ENTRY_FIXED_OVERHEAD_BYTES2 + entry.indexName.length + entry.documentId.length + (entry.document?.byteLength ?? 0);
4905
+ if (entrySize > retentionBytes) {
4906
+ throw new NarsilError(
4907
+ ErrorCodes.REPLICATION_LOG_FULL,
4908
+ `Replication log entry of ${entrySize} bytes exceeds the log retention limit of ${retentionBytes} bytes and cannot be buffered`,
4909
+ { partitionId, entrySize, retentionBytes, seqNo: entry.seqNo }
4910
+ );
4911
+ }
5036
4912
  }
5037
- return { config: entry.config, created };
5038
- }
5039
- function validateExistingSchema(indexName, expected, actual) {
5040
- const expectedSchema = flattenSchema(expected);
5041
- const actualSchema = flattenSchema(actual);
5042
- for (const [field, expectedType] of Object.entries(expectedSchema)) {
5043
- if (actualSchema[field] !== expectedType) {
5044
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "local index schema mismatch", {
5045
- indexName,
5046
- field,
5047
- expected: expectedType,
5048
- received: actualSchema[field] ?? "(absent)"
5049
- });
4913
+ function storeEntry(entry) {
4914
+ entries.push(entry);
4915
+ if (entry.seqNo >= localLogEnd) {
4916
+ localLogEnd = entry.seqNo;
4917
+ localLogEndPrimaryTerm = entry.primaryTerm;
5050
4918
  }
4919
+ totalSizeBytes += estimateEntrySize(entry);
4920
+ evictOldEntries();
4921
+ return { ...entry };
5051
4922
  }
5052
- for (const field of Object.keys(actualSchema)) {
5053
- if (!(field in expectedSchema)) {
5054
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "local index schema contains extra field", {
5055
- indexName,
5056
- field
5057
- });
4923
+ function readEntry(seqNo) {
4924
+ if (liveCount() === 0) return void 0;
4925
+ const idx = findIndexBySeqNo(seqNo);
4926
+ if (idx < entries.length && entries[idx].seqNo === seqNo) {
4927
+ return { ...entries[idx] };
5058
4928
  }
4929
+ return void 0;
5059
4930
  }
5060
- }
5061
- function flattenSchema(schema) {
5062
- const flat = {};
5063
- flattenSchemaInto(schema, "", flat);
5064
- return flat;
5065
- }
5066
- function flattenSchemaInto(schema, prefix, flat) {
5067
- for (const [field, value] of Object.entries(schema)) {
5068
- const path = prefix.length === 0 ? field : `${prefix}.${field}`;
5069
- if (isNestedSchema(value)) {
5070
- flattenSchemaInto(value, path, flat);
5071
- continue;
5072
- }
5073
- flat[path] = value;
4931
+ function verifyEntryChecksum(entry) {
4932
+ const expected = computeChecksum({
4933
+ seqNo: entry.seqNo,
4934
+ primaryTerm: entry.primaryTerm,
4935
+ operation: entry.operation,
4936
+ partitionId: entry.partitionId,
4937
+ indexName: entry.indexName,
4938
+ documentId: entry.documentId,
4939
+ document: entry.document
4940
+ });
4941
+ return expected === entry.checksum;
5074
4942
  }
5075
- }
5076
- function isNestedSchema(value) {
5077
- return typeof value === "object" && value !== null;
5078
- }
5079
- async function applyReplicationEntry(core, entry) {
5080
- core.guardShutdown();
5081
- const indexEntry = core.requireIndex(entry.indexName);
5082
- const manager = core.requireManager(entry.indexName);
5083
- const vecIndexes = manager.getVectorIndexes();
5084
- if (entry.operation === "INDEX") {
5085
- applyIndexEntry(entry, manager, indexEntry.vectorFieldPaths, vecIndexes);
5086
- const appliedDocument = manager.get(entry.documentId);
5087
- if (appliedDocument !== void 0) {
5088
- await core.orchestrator.replicateToWorkers({
5089
- type: "insert",
5090
- indexName: entry.indexName,
5091
- docId: entry.documentId,
5092
- document: appliedDocument,
5093
- requestId: `replicate-entry-insert-${entry.indexName}-${entry.partitionId}-${entry.seqNo}`
5094
- });
4943
+ return {
4944
+ append(partial) {
4945
+ assertEntryFitsRetention(partial);
4946
+ const seqNo = nextSeqNo;
4947
+ nextSeqNo += 1;
4948
+ const entryWithoutChecksum = {
4949
+ seqNo,
4950
+ primaryTerm: partial.primaryTerm,
4951
+ operation: partial.operation,
4952
+ partitionId,
4953
+ indexName: partial.indexName,
4954
+ documentId: partial.documentId,
4955
+ document: partial.document
4956
+ };
4957
+ const checksum = computeChecksum(entryWithoutChecksum);
4958
+ const entry = { ...entryWithoutChecksum, checksum };
4959
+ return storeEntry(entry);
4960
+ },
4961
+ appendCommitted(entry) {
4962
+ if (!Number.isSafeInteger(entry.seqNo) || entry.seqNo < 1) {
4963
+ throw new NarsilError(
4964
+ ErrorCodes.REPLICATION_ENTRY_INVALID,
4965
+ `Invalid replication sequence number ${entry.seqNo}`,
4966
+ {
4967
+ seqNo: entry.seqNo,
4968
+ partitionId
4969
+ }
4970
+ );
4971
+ }
4972
+ if (entry.partitionId !== partitionId) {
4973
+ throw new NarsilError(
4974
+ ErrorCodes.REPLICATION_ENTRY_INVALID,
4975
+ `Replication entry partition ${entry.partitionId} does not match log partition ${partitionId}`,
4976
+ { entryPartitionId: entry.partitionId, logPartitionId: partitionId }
4977
+ );
4978
+ }
4979
+ const existing = readEntry(entry.seqNo);
4980
+ if (existing !== void 0) {
4981
+ if (existing.checksum !== entry.checksum) {
4982
+ throw new NarsilError(
4983
+ ErrorCodes.REPLICATION_ENTRY_INVALID,
4984
+ `Conflicting replication entry at sequence number ${entry.seqNo}`,
4985
+ { seqNo: entry.seqNo, partitionId }
4986
+ );
4987
+ }
4988
+ return existing;
4989
+ }
4990
+ if (entry.seqNo !== nextSeqNo) {
4991
+ throw new NarsilError(
4992
+ ErrorCodes.REPLICATION_ENTRY_INVALID,
4993
+ `Out-of-order replication entry ${entry.seqNo}; expected ${nextSeqNo}`,
4994
+ { seqNo: entry.seqNo, expectedSeqNo: nextSeqNo, partitionId }
4995
+ );
4996
+ }
4997
+ if (!verifyEntryChecksum(entry)) {
4998
+ throw new NarsilError(
4999
+ ErrorCodes.REPLICATION_ENTRY_CORRUPT,
5000
+ `Invalid checksum for replication entry ${entry.seqNo}`,
5001
+ { seqNo: entry.seqNo, partitionId }
5002
+ );
5003
+ }
5004
+ assertEntryFitsRetention(entry);
5005
+ nextSeqNo = entry.seqNo + 1;
5006
+ return storeEntry({ ...entry });
5007
+ },
5008
+ getEntriesFrom(fromSeqNo) {
5009
+ if (liveCount() === 0) return [];
5010
+ const idx = findIndexBySeqNo(fromSeqNo);
5011
+ if (idx >= entries.length) return [];
5012
+ return entries.slice(idx).map((e) => ({ ...e }));
5013
+ },
5014
+ getEntry(seqNo) {
5015
+ return readEntry(seqNo);
5016
+ },
5017
+ verifyChecksum(entry) {
5018
+ return verifyEntryChecksum(entry);
5019
+ },
5020
+ get localLogEnd() {
5021
+ return localLogEnd;
5022
+ },
5023
+ get localLogEndPrimaryTerm() {
5024
+ return localLogEndPrimaryTerm;
5025
+ },
5026
+ get commitPoint() {
5027
+ return commitPoint;
5028
+ },
5029
+ advanceCommitPoint(seqNo) {
5030
+ const bounded = Math.min(seqNo, localLogEnd);
5031
+ if (bounded > commitPoint) {
5032
+ commitPoint = bounded;
5033
+ }
5034
+ },
5035
+ get oldestSeqNo() {
5036
+ if (liveCount() === 0) return void 0;
5037
+ return entries[headIndex].seqNo;
5038
+ },
5039
+ get newestSeqNo() {
5040
+ if (liveCount() === 0) return void 0;
5041
+ return entries[entries.length - 1].seqNo;
5042
+ },
5043
+ get entryCount() {
5044
+ return liveCount();
5045
+ },
5046
+ get sizeBytes() {
5047
+ return totalSizeBytes;
5048
+ },
5049
+ clear() {
5050
+ entries = [];
5051
+ headIndex = 0;
5052
+ totalSizeBytes = 0;
5053
+ localLogEnd = nextSeqNo - 1;
5054
+ commitPoint = nextSeqNo - 1;
5095
5055
  }
5096
- return;
5097
- }
5098
- applyDeleteEntry(entry, manager, vecIndexes);
5099
- await core.orchestrator.replicateToWorkers({
5100
- type: "remove",
5101
- indexName: entry.indexName,
5102
- docId: entry.documentId,
5103
- requestId: `replicate-entry-remove-${entry.indexName}-${entry.partitionId}-${entry.seqNo}`
5104
- });
5056
+ };
5105
5057
  }
5106
5058
 
5107
- // src/distribution/cluster-node/local-index-reconcile.ts
5108
- async function reconcileLocalIndexes(deps) {
5109
- const dispositions = /* @__PURE__ */ new Map();
5110
- for (const index of deps.engine.listIndexes()) {
5111
- const disposition = await reconcileOne(deps, index.name);
5112
- dispositions.set(index.name, disposition);
5113
- }
5114
- return dispositions;
5059
+ // src/distribution/cluster-node/replication-logs.ts
5060
+ function replicationLogKey(indexName, partitionId) {
5061
+ return `${indexName}:${partitionId}`;
5115
5062
  }
5116
- async function adoptClusterIdentity(engine, coordinator, indexName) {
5117
- if (engine.indexUuidOf(indexName) !== null) {
5118
- return;
5119
- }
5120
- const metadata = await getIndexMetadata(coordinator, indexName);
5121
- if (metadata === null) {
5122
- return;
5063
+ function getReplicationLog(replicationLogs, indexName, partitionId, replicationConfig) {
5064
+ const key = replicationLogKey(indexName, partitionId);
5065
+ let log = replicationLogs.get(key);
5066
+ if (log === void 0) {
5067
+ log = createReplicationLog(partitionId, { logRetentionBytes: replicationConfig?.logRetentionBytes });
5068
+ replicationLogs.set(key, log);
5123
5069
  }
5124
- await engine.stampIndexUuid(indexName, metadata.indexUuid);
5070
+ return log;
5125
5071
  }
5126
- function orphanedIndexError(nodeId, indexName) {
5127
- return new NarsilError(
5128
- ErrorCodes.INDEX_ORPHANED,
5129
- `Node '${nodeId}' holds a local copy of index '${indexName}' the cluster does not claim, so it answers nothing for that name. Drop it to reclaim the space`,
5130
- { indexName, nodeId }
5072
+ function seedReplicationLog(replicationLogs, indexName, partitionId, startSeqNo, lastPrimaryTerm = 0, replicationConfig) {
5073
+ replicationLogs.set(
5074
+ replicationLogKey(indexName, partitionId),
5075
+ createReplicationLog(partitionId, {
5076
+ startSeqNo,
5077
+ lastPrimaryTerm,
5078
+ logRetentionBytes: replicationConfig?.logRetentionBytes
5079
+ })
5131
5080
  );
5132
5081
  }
5133
- async function reconcileOne(deps, indexName) {
5134
- const localUuid = deps.engine.indexUuidOf(indexName);
5135
- let clusterUuid;
5136
- try {
5137
- clusterUuid = (await getIndexMetadata(deps.coordinator, indexName))?.indexUuid ?? null;
5138
- } catch (error) {
5139
- report(deps, indexName, `its metadata could not be read: ${describeError(error)}`);
5140
- return "orphaned";
5141
- }
5142
- if (clusterUuid === null) {
5143
- report(deps, indexName, "the coordinator holds no metadata for that name");
5144
- return "orphaned";
5145
- }
5146
- if (localUuid === clusterUuid) {
5147
- return "adopted";
5082
+ function deleteIndexReplicationLogs(replicationLogs, indexName) {
5083
+ const prefix = `${indexName}:`;
5084
+ for (const key of replicationLogs.keys()) {
5085
+ if (key.startsWith(prefix)) replicationLogs.delete(key);
5148
5086
  }
5149
- if (localUuid === null || localUuid === void 0) {
5150
- await deps.engine.stampIndexUuid(indexName, clusterUuid);
5151
- return "adopted";
5087
+ }
5088
+
5089
+ // src/distribution/cluster-node/local-replication.ts
5090
+ async function seedReopenedPrimaryLogs(input) {
5091
+ const allocation = await input.coordinator.getAllocation(input.indexName);
5092
+ if (allocation === null) return;
5093
+ for (const partitionId of input.engine.heldPartitionsOf(input.indexName) ?? []) {
5094
+ const assignment = allocation.assignments.get(partitionId);
5095
+ if (assignment?.primary !== input.nodeId) continue;
5096
+ const floor = Math.max(input.engine.highestPersistedSeqNoOf(input.indexName, partitionId), assignment.commitPoint);
5097
+ seedReplicationLog(
5098
+ input.replicationLogs,
5099
+ input.indexName,
5100
+ partitionId,
5101
+ floor + 1,
5102
+ assignment.primaryTerm,
5103
+ input.replicationConfig
5104
+ );
5152
5105
  }
5153
- await deps.engine.dropIndex(indexName);
5154
- return "superseded";
5155
5106
  }
5156
- function report(deps, indexName, reason) {
5157
- if (deps.onError === void 0) {
5158
- return;
5107
+ async function applyLocalReplicationEntry(core, entry) {
5108
+ core.guardShutdown();
5109
+ const release = await core.indexState.acquire(entry.indexName);
5110
+ try {
5111
+ const indexEntry = core.requireIndex(entry.indexName);
5112
+ const manager = core.requireManager(entry.indexName);
5113
+ const vecIndexes = manager.getVectorIndexes();
5114
+ if (entry.operation === "INDEX") {
5115
+ applyIndexEntry(entry, manager, indexEntry.vectorFieldPaths, vecIndexes);
5116
+ const appliedDocument = manager.get(entry.documentId);
5117
+ if (appliedDocument !== void 0) {
5118
+ await core.orchestrator.replicateToWorkers({
5119
+ type: "insert",
5120
+ indexName: entry.indexName,
5121
+ docId: entry.documentId,
5122
+ document: appliedDocument,
5123
+ requestId: `replicate-entry-insert-${entry.indexName}-${entry.partitionId}-${entry.seqNo}`
5124
+ });
5125
+ }
5126
+ return;
5127
+ }
5128
+ applyDeleteEntry(entry, manager, vecIndexes);
5129
+ await core.orchestrator.replicateToWorkers({
5130
+ type: "remove",
5131
+ indexName: entry.indexName,
5132
+ docId: entry.documentId,
5133
+ requestId: `replicate-entry-remove-${entry.indexName}-${entry.partitionId}-${entry.seqNo}`
5134
+ });
5135
+ } finally {
5136
+ release();
5159
5137
  }
5160
- deps.onError(
5161
- new NarsilError(
5162
- ErrorCodes.INDEX_ORPHANED,
5163
- `Node '${deps.nodeId}' holds a local copy of index '${indexName}' the cluster does not claim, because ${reason}. The node keeps the data and serves none of it until an operator drops it.`,
5164
- { indexName, nodeId: deps.nodeId }
5165
- )
5166
- );
5167
5138
  }
5168
5139
 
5169
- // src/distribution/cluster-node/snapshot-auth.ts
5170
- var BOOTSTRAPPABLE_STATES = /* @__PURE__ */ new Set(["ACTIVE", "INITIALISING", "MIGRATING"]);
5171
- async function authorizeSnapshotRequest(coordinator, indexName, sourceId) {
5172
- let allocation;
5173
- try {
5174
- allocation = await coordinator.getAllocation(indexName);
5175
- } catch (err) {
5176
- const errMessage = err instanceof Error ? err.message : String(err);
5177
- return {
5178
- outcome: "denied",
5179
- code: ErrorCodes.SNAPSHOT_SYNC_ALLOCATION_UNAVAILABLE,
5180
- reason: `allocation lookup failed: ${errMessage}`
5181
- };
5140
+ // src/distribution/cluster-node/local-engine.ts
5141
+ async function createClusterLocalEngine(config, hooks) {
5142
+ const core = createEngineCore(config, hooks);
5143
+ if (core.durability !== null) {
5144
+ await core.durability.manager.recover(config?.lifecycle !== void 0);
5182
5145
  }
5183
- if (allocation === null) {
5184
- return {
5185
- outcome: "denied",
5186
- code: ErrorCodes.SNAPSHOT_SYNC_UNAUTHORIZED,
5187
- reason: `index '${indexName}' has no allocation`
5188
- };
5146
+ if (core.invalidation !== null) {
5147
+ await core.invalidation.start();
5189
5148
  }
5190
- let sawAssignment = false;
5191
- for (const assignment of allocation.assignments.values()) {
5192
- const isAssigned = assignment.primary === sourceId || assignment.replicas.includes(sourceId);
5193
- if (!isAssigned) {
5194
- continue;
5195
- }
5196
- sawAssignment = true;
5197
- if (!BOOTSTRAPPABLE_STATES.has(assignment.state)) {
5198
- continue;
5149
+ if (config?.lifecycle === void 0) await core.analysisRebuild.reviewStaleIndexes();
5150
+ const engine = createNarsilFromCore(core, config);
5151
+ const heldPartitions = createHeldPartitionRecord(core);
5152
+ return Object.assign(engine, {
5153
+ createIndexWithUuid: (name, indexConfig, indexUuid) => createEngineIndex(core, config, name, indexConfig, indexUuid),
5154
+ acquireIndexForReplication: (indexName) => core.indexState.acquire(indexName, false),
5155
+ indexUuidOf: (indexName) => core.indexRegistry.get(indexName)?.indexUuid,
5156
+ stampIndexUuid: (indexName, indexUuid) => stampIndexUuid(core, indexName, indexUuid),
5157
+ highestPersistedSeqNoOf: (indexName, partitionId) => core.durability?.manager.highestPersistedSeqNo(indexName, partitionId) ?? 0,
5158
+ heldPartitionsOf: (indexName) => heldPartitions.held(indexName),
5159
+ recordHeldPartition: (indexName, partitionId) => heldPartitions.record(indexName, partitionId),
5160
+ forgetHeldPartition: (indexName, partitionId) => heldPartitions.forget(indexName, partitionId),
5161
+ applyReplicationEntry: (entry) => applyLocalReplicationEntry(core, entry),
5162
+ serializeReplicationPartition: (indexName, partitionId) => serializeReplicationPartition(core, indexName, partitionId),
5163
+ restoreReplicationPartition: (indexName, partitionId, bytes, schema, partitionCount) => restoreReplicationPartition(core, engine, indexName, partitionId, bytes, schema, partitionCount),
5164
+ queryPartitions: (indexName, params, partitionIds, globalStats) => runEngineQuery(core, indexName, params, { partitionIds, globalStats }),
5165
+ preflightPartitions: (indexName, params, partitionIds) => runEnginePreflight(core, indexName, params, { partitionIds }),
5166
+ suggestPartitions: (indexName, params, partitionIds) => runEngineSuggest(core, indexName, params, partitionIds),
5167
+ listPartitions: (indexName, params, partitionIds) => runEngineListDocuments(core, indexName, params, partitionIds),
5168
+ collectQueryStats: (indexName, terms, partitionIds) => runEngineQueryStats(core, indexName, terms, partitionIds),
5169
+ partitionStatsForRead: async (indexName) => {
5170
+ const release = await core.indexState.acquire(indexName);
5171
+ try {
5172
+ return core.requireManager(indexName).getPartitionStats();
5173
+ } finally {
5174
+ release();
5175
+ }
5199
5176
  }
5200
- return { outcome: "authorized" };
5177
+ });
5178
+ }
5179
+ async function stampIndexUuid(core, indexName, indexUuid) {
5180
+ const entry = core.indexRegistry.get(indexName);
5181
+ if (entry === void 0 || entry.indexUuid === indexUuid) {
5182
+ return;
5201
5183
  }
5202
- if (sawAssignment) {
5203
- return {
5204
- outcome: "denied",
5205
- code: ErrorCodes.SNAPSHOT_SYNC_NOT_ASSIGNED,
5206
- reason: `node '${sourceId}' is listed for index '${indexName}' but not in a bootstrappable state`
5207
- };
5184
+ core.indexRegistry.set(indexName, { ...entry, indexUuid });
5185
+ if (core.durability !== null) {
5186
+ await core.durability.manager.persistMetadata(indexName);
5208
5187
  }
5209
- return {
5210
- outcome: "denied",
5211
- code: ErrorCodes.SNAPSHOT_SYNC_UNAUTHORIZED,
5212
- reason: `node '${sourceId}' is not an assigned replica for index '${indexName}'`
5213
- };
5214
5188
  }
5215
-
5216
- // src/distribution/cluster-node/snapshot-cache.ts
5217
- var DEFAULT_MAX_CONCURRENT_SNAPSHOTS = 2;
5218
- var DEFAULT_MAX_PER_SOURCE_SNAPSHOTS = 1;
5219
- var DEFAULT_MAX_STREAMS_PER_INDEX = 4;
5220
- function createSnapshotCacheState(maxConcurrent = DEFAULT_MAX_CONCURRENT_SNAPSHOTS, maxPerSource = DEFAULT_MAX_PER_SOURCE_SNAPSHOTS, maxStreamsPerIndex = DEFAULT_MAX_STREAMS_PER_INDEX) {
5221
- if (!Number.isInteger(maxConcurrent) || maxConcurrent <= 0) {
5222
- throw new NarsilError(ErrorCodes.CONFIG_INVALID, "maxConcurrent must be a positive integer", {
5223
- maxConcurrent
5224
- });
5189
+ async function serializeReplicationPartition(core, indexName, partitionId) {
5190
+ core.guardShutdown();
5191
+ const release = await core.indexState.acquire(indexName, false);
5192
+ try {
5193
+ return core.requireManager(indexName).serializePartitionToBytes(partitionId);
5194
+ } finally {
5195
+ release();
5225
5196
  }
5226
- if (!Number.isInteger(maxPerSource) || maxPerSource <= 0) {
5227
- throw new NarsilError(ErrorCodes.CONFIG_INVALID, "maxPerSource must be a positive integer", {
5228
- maxPerSource
5197
+ }
5198
+ async function restoreReplicationPartition(core, engine, indexName, partitionId, bytes, schema, partitionCount) {
5199
+ core.guardShutdown();
5200
+ validatePartitionRestoreTarget(indexName, partitionId, partitionCount);
5201
+ let partition;
5202
+ try {
5203
+ partition = deserializePayloadV2(bytes);
5204
+ } catch (err) {
5205
+ const cause = err instanceof Error ? err.message : String(err);
5206
+ throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, `partition snapshot decode failed: ${cause}`, {
5207
+ indexName,
5208
+ partitionId,
5209
+ cause
5229
5210
  });
5230
5211
  }
5231
- if (!Number.isInteger(maxStreamsPerIndex) || maxStreamsPerIndex <= 0) {
5232
- throw new NarsilError(ErrorCodes.CONFIG_INVALID, "maxStreamsPerIndex must be a positive integer", {
5233
- maxStreamsPerIndex
5212
+ validatePartitionSnapshotPayload(indexName, partitionId, partitionCount, schema, partition);
5213
+ const trackPositions = core.indexRegistry.get(indexName)?.config.trackPositions ?? true;
5214
+ createPartitionIndex(partitionId, trackPositions).deserialize(partition, schema);
5215
+ let release = core.indexRegistry.has(indexName) ? await core.indexState.acquire(indexName, false) : null;
5216
+ try {
5217
+ const restoreIndex = await ensureReplicationIndex(
5218
+ core,
5219
+ engine,
5220
+ indexName,
5221
+ schema,
5222
+ partition.language,
5223
+ partitionCount
5224
+ );
5225
+ if (release === null) release = await core.indexState.acquire(indexName, false);
5226
+ if (restoreIndex.created) {
5227
+ await core.orchestrator.replicateToWorkers({
5228
+ type: "createIndex",
5229
+ indexName,
5230
+ config: restoreIndex.config,
5231
+ requestId: `replicate-partition-index-${indexName}`
5232
+ });
5233
+ }
5234
+ const manager = core.requireManager(indexName);
5235
+ manager.deserializePartition(partitionId, partition);
5236
+ await core.orchestrator.replicateToWorkers({
5237
+ type: "deserialize",
5238
+ indexName,
5239
+ partitionId,
5240
+ data: partition,
5241
+ requestId: `replicate-partition-restore-${indexName}-${partitionId}`
5234
5242
  });
5243
+ } finally {
5244
+ release?.();
5235
5245
  }
5236
- return {
5237
- cache: /* @__PURE__ */ new Map(),
5238
- active: 0,
5239
- perSource: /* @__PURE__ */ new Map(),
5240
- streamActive: /* @__PURE__ */ new Map(),
5241
- maxConcurrent,
5242
- maxPerSource,
5243
- maxStreamsPerIndex
5244
- };
5245
- }
5246
- function sourceSlotKey(sourceId, indexName) {
5247
- return `${sourceId}\0${indexName}`;
5248
5246
  }
5249
- function acquireSourceSlot(state, sourceId, indexName) {
5250
- const key = sourceSlotKey(sourceId, indexName);
5251
- const current = state.perSource.get(key) ?? 0;
5252
- if (current >= state.maxPerSource) {
5247
+ function validatePartitionRestoreTarget(indexName, partitionId, partitionCount) {
5248
+ if (!Number.isInteger(partitionId) || partitionId < 0) {
5249
+ throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partitionId must be a non-negative integer", {
5250
+ indexName,
5251
+ partitionId
5252
+ });
5253
+ }
5254
+ if (!Number.isInteger(partitionCount) || partitionCount <= 0 || partitionCount > MAX_PARTITION_COUNT) {
5253
5255
  throw new NarsilError(
5254
- ErrorCodes.SNAPSHOT_SYNC_CAPACITY_EXHAUSTED,
5255
- `snapshot producer per-source cap reached for '${sourceId}' index '${indexName}'`,
5256
- { sourceId, indexName, cap: state.maxPerSource }
5256
+ ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED,
5257
+ `partitionCount must be an integer between 1 and ${MAX_PARTITION_COUNT}`,
5258
+ { indexName, partitionCount }
5257
5259
  );
5258
5260
  }
5259
- state.perSource.set(key, current + 1);
5260
- return { key, released: false };
5261
+ if (partitionId >= partitionCount) {
5262
+ throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partitionId is outside partitionCount", {
5263
+ indexName,
5264
+ partitionId,
5265
+ partitionCount
5266
+ });
5267
+ }
5261
5268
  }
5262
- function releaseSourceSlot(state, handle) {
5263
- if (handle.released) {
5264
- return;
5269
+ function validatePartitionSnapshotPayload(indexName, partitionId, partitionCount, schema, partition) {
5270
+ if (partition.indexName !== indexName) {
5271
+ throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partition snapshot indexName mismatch", {
5272
+ expectedIndexName: indexName,
5273
+ receivedIndexName: partition.indexName
5274
+ });
5265
5275
  }
5266
- handle.released = true;
5267
- const current = state.perSource.get(handle.key);
5268
- if (current === void 0) {
5269
- return;
5276
+ if (partition.partitionId !== partitionId) {
5277
+ throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partition snapshot partitionId mismatch", {
5278
+ indexName,
5279
+ expectedPartitionId: partitionId,
5280
+ receivedPartitionId: partition.partitionId
5281
+ });
5270
5282
  }
5271
- if (current <= 1) {
5272
- state.perSource.delete(handle.key);
5273
- return;
5283
+ if (partition.totalPartitions !== partitionCount) {
5284
+ throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partition snapshot partition count mismatch", {
5285
+ indexName,
5286
+ partitionId,
5287
+ expectedPartitionCount: partitionCount,
5288
+ receivedPartitionCount: partition.totalPartitions
5289
+ });
5274
5290
  }
5275
- state.perSource.set(handle.key, current - 1);
5276
- }
5277
- function acquireStreamSlot(state, indexName) {
5278
- const current = state.streamActive.get(indexName) ?? 0;
5279
- if (current >= state.maxStreamsPerIndex) {
5280
- throw new NarsilError(
5281
- ErrorCodes.SNAPSHOT_SYNC_CAPACITY_EXHAUSTED,
5282
- `snapshot stream fan-out cap reached for index '${indexName}'`,
5283
- { indexName, cap: state.maxStreamsPerIndex }
5284
- );
5291
+ const expectedSchema = flattenSchema(schema);
5292
+ for (const [field, expectedType] of Object.entries(expectedSchema)) {
5293
+ if (partition.schema[field] !== expectedType) {
5294
+ throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partition snapshot schema mismatch", {
5295
+ indexName,
5296
+ partitionId,
5297
+ field,
5298
+ expected: expectedType,
5299
+ received: partition.schema[field] ?? "(absent)"
5300
+ });
5301
+ }
5302
+ }
5303
+ for (const field of Object.keys(partition.schema)) {
5304
+ if (!(field in expectedSchema)) {
5305
+ throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partition snapshot schema contains extra field", {
5306
+ indexName,
5307
+ partitionId,
5308
+ field
5309
+ });
5310
+ }
5285
5311
  }
5286
- state.streamActive.set(indexName, current + 1);
5287
- return { indexName, released: false };
5288
5312
  }
5289
- function releaseStreamSlot(state, handle) {
5290
- if (handle.released) {
5291
- return;
5313
+ async function ensureReplicationIndex(core, engine, indexName, schema, languageName, partitionCount) {
5314
+ const language = getLanguage(languageName);
5315
+ const existing = core.indexRegistry.get(indexName);
5316
+ let created = false;
5317
+ if (existing === void 0) {
5318
+ const indexConfig = {
5319
+ schema,
5320
+ language: language.name,
5321
+ partitions: { maxPartitions: partitionCount }
5322
+ };
5323
+ try {
5324
+ await engine.createIndex(indexName, indexConfig);
5325
+ created = true;
5326
+ } catch (err) {
5327
+ if (!(err instanceof NarsilError) || err.code !== ErrorCodes.INDEX_ALREADY_EXISTS) {
5328
+ throw err;
5329
+ }
5330
+ }
5292
5331
  }
5293
- handle.released = true;
5294
- const current = state.streamActive.get(handle.indexName);
5295
- if (current === void 0) {
5296
- return;
5332
+ const entry = core.requireIndex(indexName);
5333
+ if (entry.language.name !== language.name) {
5334
+ throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partition snapshot language mismatch", {
5335
+ indexName,
5336
+ expectedLanguage: entry.language.name,
5337
+ receivedLanguage: language.name
5338
+ });
5297
5339
  }
5298
- if (current <= 1) {
5299
- state.streamActive.delete(handle.indexName);
5300
- return;
5340
+ validateExistingSchema(indexName, schema, entry.config.schema);
5341
+ const manager = core.requireManager(indexName);
5342
+ if (manager.partitionCount > partitionCount) {
5343
+ throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "local index has more partitions than allocation", {
5344
+ indexName,
5345
+ localPartitionCount: manager.partitionCount,
5346
+ allocationPartitionCount: partitionCount
5347
+ });
5301
5348
  }
5302
- state.streamActive.set(handle.indexName, current - 1);
5303
- }
5304
- async function acquireSnapshotBuild(state, indexName, builder) {
5305
- const existing = state.cache.get(indexName);
5306
- if (existing !== void 0) {
5307
- return existing.promise;
5349
+ if (entry.config.partitions === void 0) {
5350
+ entry.config.partitions = { maxPartitions: partitionCount };
5351
+ } else if ((entry.config.partitions.maxPartitions ?? manager.partitionCount) < partitionCount) {
5352
+ entry.config.partitions.maxPartitions = partitionCount;
5308
5353
  }
5309
- if (state.active >= state.maxConcurrent) {
5310
- throw new NarsilError(
5311
- ErrorCodes.SNAPSHOT_SYNC_CAPACITY_EXHAUSTED,
5312
- "snapshot producer capacity exhausted; retry later",
5313
- { active: state.active, max: state.maxConcurrent }
5354
+ while (manager.partitionCount < partitionCount) {
5355
+ manager.addPartition();
5356
+ }
5357
+ return { config: entry.config, created };
5358
+ }
5359
+ function validateExistingSchema(indexName, expected, actual) {
5360
+ const expectedSchema = flattenSchema(expected);
5361
+ const actualSchema = flattenSchema(actual);
5362
+ for (const [field, expectedType] of Object.entries(expectedSchema)) {
5363
+ if (actualSchema[field] !== expectedType) {
5364
+ throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "local index schema mismatch", {
5365
+ indexName,
5366
+ field,
5367
+ expected: expectedType,
5368
+ received: actualSchema[field] ?? "(absent)"
5369
+ });
5370
+ }
5371
+ }
5372
+ for (const field of Object.keys(actualSchema)) {
5373
+ if (!(field in expectedSchema)) {
5374
+ throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "local index schema contains extra field", {
5375
+ indexName,
5376
+ field
5377
+ });
5378
+ }
5379
+ }
5380
+ }
5381
+ function flattenSchema(schema) {
5382
+ const flat = {};
5383
+ flattenSchemaInto(schema, "", flat);
5384
+ return flat;
5385
+ }
5386
+ function flattenSchemaInto(schema, prefix, flat) {
5387
+ for (const [field, value] of Object.entries(schema)) {
5388
+ const path = prefix.length === 0 ? field : `${prefix}.${field}`;
5389
+ if (isNestedSchema(value)) {
5390
+ flattenSchemaInto(value, path, flat);
5391
+ continue;
5392
+ }
5393
+ flat[path] = value;
5394
+ }
5395
+ }
5396
+ function isNestedSchema(value) {
5397
+ return typeof value === "object" && value !== null;
5398
+ }
5399
+
5400
+ // src/distribution/cluster-node/local-index-reconcile.ts
5401
+ async function reconcileLocalIndexes(deps) {
5402
+ const dispositions = /* @__PURE__ */ new Map();
5403
+ for (const index of deps.engine.listIndexes()) {
5404
+ const disposition = await reconcileOne(deps, index.name);
5405
+ dispositions.set(index.name, disposition);
5406
+ }
5407
+ return dispositions;
5408
+ }
5409
+ async function adoptClusterIdentity(engine, coordinator, indexName) {
5410
+ if (engine.indexUuidOf(indexName) !== null) {
5411
+ return;
5412
+ }
5413
+ const metadata = await getIndexMetadata(coordinator, indexName);
5414
+ if (metadata === null) {
5415
+ return;
5416
+ }
5417
+ await engine.stampIndexUuid(indexName, metadata.indexUuid);
5418
+ }
5419
+ function orphanedIndexError(nodeId, indexName) {
5420
+ return new NarsilError(
5421
+ ErrorCodes.INDEX_ORPHANED,
5422
+ `Node '${nodeId}' holds a local copy of index '${indexName}' the cluster does not claim, so it answers nothing for that name. Drop it to reclaim the space`,
5423
+ { indexName, nodeId }
5424
+ );
5425
+ }
5426
+ async function reconcileOne(deps, indexName) {
5427
+ const localUuid = deps.engine.indexUuidOf(indexName);
5428
+ let clusterUuid;
5429
+ try {
5430
+ clusterUuid = (await getIndexMetadata(deps.coordinator, indexName))?.indexUuid ?? null;
5431
+ } catch (error) {
5432
+ report(deps, indexName, `its metadata could not be read: ${describeError(error)}`);
5433
+ return "orphaned";
5434
+ }
5435
+ if (clusterUuid === null) {
5436
+ report(deps, indexName, "the coordinator holds no metadata for that name");
5437
+ return "orphaned";
5438
+ }
5439
+ if (localUuid === clusterUuid) {
5440
+ return "adopted";
5441
+ }
5442
+ if (localUuid === null || localUuid === void 0) {
5443
+ await deps.engine.stampIndexUuid(indexName, clusterUuid);
5444
+ return "adopted";
5445
+ }
5446
+ await deps.engine.dropIndex(indexName);
5447
+ return "superseded";
5448
+ }
5449
+ function report(deps, indexName, reason) {
5450
+ if (deps.onError === void 0) {
5451
+ return;
5452
+ }
5453
+ deps.onError(
5454
+ new NarsilError(
5455
+ ErrorCodes.INDEX_ORPHANED,
5456
+ `Node '${deps.nodeId}' holds a local copy of index '${indexName}' the cluster does not claim, because ${reason}. The node keeps the data and serves none of it until an operator drops it.`,
5457
+ { indexName, nodeId: deps.nodeId }
5458
+ )
5459
+ );
5460
+ }
5461
+
5462
+ // src/distribution/cluster-node/snapshot-auth.ts
5463
+ var BOOTSTRAPPABLE_STATES = /* @__PURE__ */ new Set(["ACTIVE", "INITIALISING", "MIGRATING"]);
5464
+ async function authorizeSnapshotRequest(coordinator, indexName, sourceId) {
5465
+ let allocation;
5466
+ try {
5467
+ allocation = await coordinator.getAllocation(indexName);
5468
+ } catch (err) {
5469
+ const errMessage = err instanceof Error ? err.message : String(err);
5470
+ return {
5471
+ outcome: "denied",
5472
+ code: ErrorCodes.SNAPSHOT_SYNC_ALLOCATION_UNAVAILABLE,
5473
+ reason: `allocation lookup failed: ${errMessage}`
5474
+ };
5475
+ }
5476
+ if (allocation === null) {
5477
+ return {
5478
+ outcome: "denied",
5479
+ code: ErrorCodes.SNAPSHOT_SYNC_UNAUTHORIZED,
5480
+ reason: `index '${indexName}' has no allocation`
5481
+ };
5482
+ }
5483
+ let sawAssignment = false;
5484
+ for (const assignment of allocation.assignments.values()) {
5485
+ const isAssigned = assignment.primary === sourceId || assignment.replicas.includes(sourceId);
5486
+ if (!isAssigned) {
5487
+ continue;
5488
+ }
5489
+ sawAssignment = true;
5490
+ if (!BOOTSTRAPPABLE_STATES.has(assignment.state)) {
5491
+ continue;
5492
+ }
5493
+ return { outcome: "authorized" };
5494
+ }
5495
+ if (sawAssignment) {
5496
+ return {
5497
+ outcome: "denied",
5498
+ code: ErrorCodes.SNAPSHOT_SYNC_NOT_ASSIGNED,
5499
+ reason: `node '${sourceId}' is listed for index '${indexName}' but not in a bootstrappable state`
5500
+ };
5501
+ }
5502
+ return {
5503
+ outcome: "denied",
5504
+ code: ErrorCodes.SNAPSHOT_SYNC_UNAUTHORIZED,
5505
+ reason: `node '${sourceId}' is not an assigned replica for index '${indexName}'`
5506
+ };
5507
+ }
5508
+
5509
+ // src/distribution/cluster-node/snapshot-cache.ts
5510
+ var DEFAULT_MAX_CONCURRENT_SNAPSHOTS = 2;
5511
+ var DEFAULT_MAX_PER_SOURCE_SNAPSHOTS = 1;
5512
+ var DEFAULT_MAX_STREAMS_PER_INDEX = 4;
5513
+ function createSnapshotCacheState(maxConcurrent = DEFAULT_MAX_CONCURRENT_SNAPSHOTS, maxPerSource = DEFAULT_MAX_PER_SOURCE_SNAPSHOTS, maxStreamsPerIndex = DEFAULT_MAX_STREAMS_PER_INDEX) {
5514
+ if (!Number.isInteger(maxConcurrent) || maxConcurrent <= 0) {
5515
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, "maxConcurrent must be a positive integer", {
5516
+ maxConcurrent
5517
+ });
5518
+ }
5519
+ if (!Number.isInteger(maxPerSource) || maxPerSource <= 0) {
5520
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, "maxPerSource must be a positive integer", {
5521
+ maxPerSource
5522
+ });
5523
+ }
5524
+ if (!Number.isInteger(maxStreamsPerIndex) || maxStreamsPerIndex <= 0) {
5525
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, "maxStreamsPerIndex must be a positive integer", {
5526
+ maxStreamsPerIndex
5527
+ });
5528
+ }
5529
+ return {
5530
+ cache: /* @__PURE__ */ new Map(),
5531
+ active: 0,
5532
+ perSource: /* @__PURE__ */ new Map(),
5533
+ streamActive: /* @__PURE__ */ new Map(),
5534
+ maxConcurrent,
5535
+ maxPerSource,
5536
+ maxStreamsPerIndex
5537
+ };
5538
+ }
5539
+ function sourceSlotKey(sourceId, indexName) {
5540
+ return `${sourceId}\0${indexName}`;
5541
+ }
5542
+ function acquireSourceSlot(state, sourceId, indexName) {
5543
+ const key = sourceSlotKey(sourceId, indexName);
5544
+ const current = state.perSource.get(key) ?? 0;
5545
+ if (current >= state.maxPerSource) {
5546
+ throw new NarsilError(
5547
+ ErrorCodes.SNAPSHOT_SYNC_CAPACITY_EXHAUSTED,
5548
+ `snapshot producer per-source cap reached for '${sourceId}' index '${indexName}'`,
5549
+ { sourceId, indexName, cap: state.maxPerSource }
5550
+ );
5551
+ }
5552
+ state.perSource.set(key, current + 1);
5553
+ return { key, released: false };
5554
+ }
5555
+ function releaseSourceSlot(state, handle) {
5556
+ if (handle.released) {
5557
+ return;
5558
+ }
5559
+ handle.released = true;
5560
+ const current = state.perSource.get(handle.key);
5561
+ if (current === void 0) {
5562
+ return;
5563
+ }
5564
+ if (current <= 1) {
5565
+ state.perSource.delete(handle.key);
5566
+ return;
5567
+ }
5568
+ state.perSource.set(handle.key, current - 1);
5569
+ }
5570
+ function acquireStreamSlot(state, indexName) {
5571
+ const current = state.streamActive.get(indexName) ?? 0;
5572
+ if (current >= state.maxStreamsPerIndex) {
5573
+ throw new NarsilError(
5574
+ ErrorCodes.SNAPSHOT_SYNC_CAPACITY_EXHAUSTED,
5575
+ `snapshot stream fan-out cap reached for index '${indexName}'`,
5576
+ { indexName, cap: state.maxStreamsPerIndex }
5577
+ );
5578
+ }
5579
+ state.streamActive.set(indexName, current + 1);
5580
+ return { indexName, released: false };
5581
+ }
5582
+ function releaseStreamSlot(state, handle) {
5583
+ if (handle.released) {
5584
+ return;
5585
+ }
5586
+ handle.released = true;
5587
+ const current = state.streamActive.get(handle.indexName);
5588
+ if (current === void 0) {
5589
+ return;
5590
+ }
5591
+ if (current <= 1) {
5592
+ state.streamActive.delete(handle.indexName);
5593
+ return;
5594
+ }
5595
+ state.streamActive.set(handle.indexName, current - 1);
5596
+ }
5597
+ async function acquireSnapshotBuild(state, indexName, builder) {
5598
+ const existing = state.cache.get(indexName);
5599
+ if (existing !== void 0) {
5600
+ return existing.promise;
5601
+ }
5602
+ if (state.active >= state.maxConcurrent) {
5603
+ throw new NarsilError(
5604
+ ErrorCodes.SNAPSHOT_SYNC_CAPACITY_EXHAUSTED,
5605
+ "snapshot producer capacity exhausted; retry later",
5606
+ { active: state.active, max: state.maxConcurrent }
5314
5607
  );
5315
5608
  }
5316
5609
  state.active += 1;
@@ -7429,7 +7722,7 @@ async function handleFetch(message, respond, deps) {
7429
7722
  async function handleStats(message, respond, deps) {
7430
7723
  const decoded = decode(message.payload);
7431
7724
  const payload = validateStatsPayload(decoded);
7432
- const resultPayload = deps.engine.collectQueryStats(
7725
+ const resultPayload = await deps.engine.collectQueryStats(
7433
7726
  payload.indexName,
7434
7727
  payload.terms,
7435
7728
  payload.partitionIds
@@ -7470,7 +7763,7 @@ function convertLocalFacetsToWire2(facets) {
7470
7763
  async function handleCount(message, respond, deps) {
7471
7764
  const payload = validateCountPayload(decode(message.payload));
7472
7765
  const requested = new Set(payload.partitionIds);
7473
- const partitions = deps.engine.getPartitionStats(payload.indexName).filter((partition) => requested.has(partition.partitionId)).map((partition) => ({
7766
+ const partitions = (await deps.engine.partitionStatsForRead(payload.indexName)).filter((partition) => requested.has(partition.partitionId)).map((partition) => ({
7474
7767
  partitionId: partition.partitionId,
7475
7768
  documentCount: partition.documentCount,
7476
7769
  estimatedMemoryBytes: partition.estimatedMemoryBytes
@@ -7620,59 +7913,65 @@ async function handleSyncRequestMessage(message, respond, deps) {
7620
7913
  }
7621
7914
  );
7622
7915
  }
7623
- if (!deps.engine.listIndexes().some((index) => index.name === request.indexName)) {
7916
+ const localIndex = deps.engine.listIndexes().find((index) => index.name === request.indexName);
7917
+ if (localIndex === void 0) {
7624
7918
  throw new NarsilError(
7625
7919
  ErrorCodes.SNAPSHOT_SYNC_INDEX_NOT_FOUND,
7626
7920
  `Node '${deps.nodeId}' holds no copy of index '${request.indexName}' yet, so it cannot serve a sync for partition ${request.partitionId}`,
7627
7921
  { indexName: request.indexName, partitionId: request.partitionId, primaryNodeId: deps.nodeId }
7628
7922
  );
7629
7923
  }
7630
- if (message.sourceId !== deps.nodeId) {
7631
- recordReplicaPosition(
7632
- deps.writeDeps.catchUp,
7633
- request.indexName,
7634
- request.partitionId,
7635
- message.sourceId,
7636
- request.lastSeqNo
7637
- );
7638
- }
7639
- const log = deps.writeDeps.getReplicationLog(request.indexName, request.partitionId);
7640
- const tier = decideSyncTier(log, request.lastSeqNo);
7641
- if (tier === "incremental") {
7642
- await sendSyncEntriesResponse(message, respond, deps, log.getEntriesFrom(request.lastSeqNo + 1));
7643
- return;
7644
- }
7645
- const snapshotSeqNo = log.localLogEnd;
7646
- const sink = createSingleResponseSink(respond);
7647
- await streamValidatedSnapshotRequest(
7648
- message,
7649
- sink,
7650
- { indexName: request.indexName, partitionId: request.partitionId },
7651
- {
7652
- nodeId: deps.nodeId,
7653
- engine: deps.engine,
7654
- coordinator: deps.coordinator,
7655
- state: deps.snapshotSyncState,
7656
- resolveHeaderMetadata: deps.resolveHeaderMetadata
7657
- },
7658
- {
7659
- metadata: {
7660
- partitionId: request.partitionId,
7661
- lastSeqNo: snapshotSeqNo,
7662
- primaryTerm: assignment.primaryTerm
7663
- },
7664
- closeOnEnd: false,
7665
- disableBuildCache: true,
7666
- buildSnapshot: async () => {
7667
- const bytes = await deps.engine.serializeReplicationPartition(request.indexName, request.partitionId);
7668
- return { bytes, checksum: crc32(bytes) };
7924
+ const release = await deps.engine.acquireIndexForReplication(request.indexName);
7925
+ try {
7926
+ if (message.sourceId !== deps.nodeId) {
7927
+ recordReplicaPosition(
7928
+ deps.writeDeps.catchUp,
7929
+ request.indexName,
7930
+ request.partitionId,
7931
+ message.sourceId,
7932
+ request.lastSeqNo
7933
+ );
7934
+ }
7935
+ const log = deps.writeDeps.getReplicationLog(request.indexName, request.partitionId);
7936
+ const tier = decideSyncTier(log, request.lastSeqNo);
7937
+ if (tier === "incremental") {
7938
+ await sendSyncEntriesResponse(message, respond, deps, log.getEntriesFrom(request.lastSeqNo + 1));
7939
+ return;
7940
+ }
7941
+ const snapshotSeqNo = log.localLogEnd;
7942
+ const sink = createSingleResponseSink(respond);
7943
+ await streamValidatedSnapshotRequest(
7944
+ message,
7945
+ sink,
7946
+ { indexName: request.indexName, partitionId: request.partitionId },
7947
+ {
7948
+ nodeId: deps.nodeId,
7949
+ engine: deps.engine,
7950
+ coordinator: deps.coordinator,
7951
+ state: deps.snapshotSyncState,
7952
+ resolveHeaderMetadata: deps.resolveHeaderMetadata
7669
7953
  },
7670
- afterSnapshot: async (trailingSink) => {
7671
- const entries = log.getEntriesFrom(snapshotSeqNo + 1);
7672
- await sendSyncEntriesResponse(message, trailingSink, deps, entries);
7954
+ {
7955
+ metadata: {
7956
+ partitionId: request.partitionId,
7957
+ lastSeqNo: snapshotSeqNo,
7958
+ primaryTerm: assignment.primaryTerm
7959
+ },
7960
+ closeOnEnd: false,
7961
+ disableBuildCache: true,
7962
+ buildSnapshot: async () => {
7963
+ const bytes = await deps.engine.serializeReplicationPartition(request.indexName, request.partitionId);
7964
+ return { bytes, checksum: crc32(bytes) };
7965
+ },
7966
+ afterSnapshot: async (trailingSink) => {
7967
+ const entries = log.getEntriesFrom(snapshotSeqNo + 1);
7968
+ await sendSyncEntriesResponse(message, trailingSink, deps, entries);
7969
+ }
7673
7970
  }
7674
- }
7675
- );
7971
+ );
7972
+ } finally {
7973
+ release();
7974
+ }
7676
7975
  }
7677
7976
  async function sendSyncEntriesResponse(message, respond, deps, entries) {
7678
7977
  const payload = {
@@ -10120,7 +10419,7 @@ async function gatherPartitionCounts(deps, indexName, allocation) {
10120
10419
  groups.map(async (group) => {
10121
10420
  if (group.nodeId === deps.nodeId) {
10122
10421
  const requested = new Set(group.partitionIds);
10123
- const partitions = deps.engine.getPartitionStats(indexName).filter((partition) => requested.has(partition.partitionId)).map((partition) => ({
10422
+ const partitions = (await deps.engine.partitionStatsForRead(indexName)).filter((partition) => requested.has(partition.partitionId)).map((partition) => ({
10124
10423
  partitionId: partition.partitionId,
10125
10424
  documentCount: partition.documentCount,
10126
10425
  estimatedMemoryBytes: partition.estimatedMemoryBytes
@@ -10198,7 +10497,7 @@ async function statsCluster(deps, indexName) {
10198
10497
  async function partitionStatsCluster(deps, indexName) {
10199
10498
  const allocation = await routableAllocation(deps.config.coordinator, indexName);
10200
10499
  if (allocation === null) {
10201
- return deps.engine.getPartitionStats(indexName);
10500
+ return deps.engine.partitionStatsForRead(indexName);
10202
10501
  }
10203
10502
  const { countsByPartition } = await gatherPartitionCounts(deps, indexName, allocation);
10204
10503
  return Array.from(countsByPartition.entries()).map(([partitionId, entry]) => ({
@@ -10486,391 +10785,234 @@ async function clearCluster(readDeps, writeDeps, indexName) {
10486
10785
  }
10487
10786
  for (; ; ) {
10488
10787
  const page = await listCluster(readDeps, indexName, { limit: CLEAR_PAGE_SIZE, document: false });
10489
- if (page.documents.length === 0) {
10490
- return;
10491
- }
10492
- const result = await routeRemoveBatch(
10493
- indexName,
10494
- page.documents.map((listed) => listed.id),
10495
- writeDeps
10496
- );
10497
- if (result.succeeded.length === 0) {
10498
- const firstFailure = result.failed[0];
10499
- throw new NarsilError(
10500
- ErrorCodes.QUERY_ROUTING_FAILED,
10501
- `Clearing index '${indexName}' stalled: no document in the current page could be removed`,
10502
- {
10503
- indexName,
10504
- remaining: page.total,
10505
- firstFailureDocId: firstFailure?.docId,
10506
- firstFailureCode: firstFailure?.error.code,
10507
- firstFailureMessage: firstFailure?.error.message
10508
- }
10509
- );
10510
- }
10511
- }
10512
- }
10513
-
10514
- // src/distribution/cluster-node/node-operations.ts
10515
- function createClusterNodeOperations(deps) {
10516
- const { trackOp, readDeps, writeDeps, engine, coordinator } = deps;
10517
- return {
10518
- async createIndex(name, indexConfig, options) {
10519
- return trackOp(name, () => routeCreateIndex(name, indexConfig, options, coordinator, engine));
10520
- },
10521
- async insert(indexName, document, docId, options) {
10522
- return trackOp(indexName, () => routeInsert(indexName, document, docId, writeDeps, options));
10523
- },
10524
- async insertBatch(indexName, documents, options) {
10525
- return trackOp(indexName, () => routeInsertBatch(indexName, documents, writeDeps, options));
10526
- },
10527
- async remove(indexName, docId) {
10528
- return trackOp(indexName, () => routeRemove(indexName, docId, writeDeps));
10529
- },
10530
- async removeBatch(indexName, docIds) {
10531
- return trackOp(indexName, () => routeRemoveBatch(indexName, docIds, writeDeps));
10532
- },
10533
- async update(indexName, docId, document) {
10534
- return trackOp(indexName, () => routeUpdate(indexName, docId, document, writeDeps));
10535
- },
10536
- async updateBatch(indexName, updates) {
10537
- return trackOp(indexName, () => routeUpdateBatch(indexName, updates, writeDeps));
10538
- },
10539
- async dropIndex(name) {
10540
- return trackOp(null, async () => {
10541
- await routeDropIndex(name, coordinator, engine);
10542
- deps.forgetIndex(name);
10543
- });
10544
- },
10545
- async clear(indexName) {
10546
- return trackOp(indexName, () => clearCluster(readDeps, writeDeps, indexName));
10547
- },
10548
- async countDocuments(indexName) {
10549
- return trackOp(indexName, () => countCluster(readDeps, indexName));
10550
- },
10551
- async listDocuments(indexName, params) {
10552
- return trackOp(indexName, () => listCluster(readDeps, indexName, params ?? {}));
10553
- },
10554
- async suggest(indexName, params) {
10555
- return trackOp(indexName, () => suggestCluster(readDeps, indexName, params));
10556
- },
10557
- async preflight(indexName, params) {
10558
- return trackOp(indexName, () => preflightCluster(readDeps, indexName, params));
10559
- },
10560
- async getStats(indexName) {
10561
- return trackOp(indexName, () => statsCluster(readDeps, indexName));
10562
- },
10563
- async getPartitionStats(indexName) {
10564
- return trackOp(indexName, () => partitionStatsCluster(readDeps, indexName));
10565
- },
10566
- async checkpoint(indexName) {
10567
- return trackOp(indexName, () => engine.checkpoint(indexName));
10568
- },
10569
- async getMemoryStats() {
10570
- return trackOp(null, () => engine.getMemoryStats());
10571
- },
10572
- on(event, handler) {
10573
- engine.on(event, handler);
10574
- },
10575
- off(event, handler) {
10576
- engine.off(event, handler);
10577
- },
10578
- async query(indexName, params) {
10579
- return trackOp(indexName, () => queryCluster(readDeps, indexName, params));
10580
- },
10581
- async get(indexName, docId) {
10582
- return trackOp(indexName, async () => (await readClusterDocuments(readDeps, indexName, [docId])).get(docId));
10583
- },
10584
- async getMultiple(indexName, docIds) {
10585
- return trackOp(indexName, () => readClusterDocuments(readDeps, indexName, docIds));
10586
- },
10587
- async has(indexName, docId) {
10588
- return trackOp(indexName, async () => (await readClusterDocuments(readDeps, indexName, [docId])).has(docId));
10589
- }
10590
- };
10591
- }
10592
-
10593
- // src/distribution/cluster-node/primary-partition.ts
10594
- async function preparePrimaryPartition(indexName, partitionId, deps) {
10595
- const schema = await deps.coordinator.getSchema(indexName);
10596
- if (schema === null) {
10597
- return false;
10598
- }
10599
- const allocation = await deps.coordinator.getAllocation(indexName);
10600
- if (allocation === null || allocation.assignments.size === 0) {
10601
- return false;
10602
- }
10603
- const existing = deps.engine.listIndexes().find((index) => index.name === indexName);
10604
- if (existing === void 0) {
10605
- try {
10606
- await deps.engine.createIndex(indexName, {
10607
- schema,
10608
- partitions: { maxPartitions: allocation.assignments.size }
10609
- });
10610
- } catch (error) {
10611
- if (!(error instanceof NarsilError) || error.code !== ErrorCodes.INDEX_ALREADY_EXISTS) {
10612
- deps.onError(error);
10613
- return false;
10614
- }
10615
- }
10616
- } else {
10617
- const schemaError = validateRestoredSchema(deps.engine, indexName, deps.nodeId, schema);
10618
- if (schemaError !== null) {
10619
- deps.onError(schemaError);
10620
- return false;
10621
- }
10622
- }
10623
- resumeNumberingAboveOwnCopy(indexName, partitionId, allocation.assignments.get(partitionId), deps);
10624
- return true;
10625
- }
10626
- function resumeNumberingAboveOwnCopy(indexName, partitionId, assignment, deps) {
10627
- if (assignment === void 0) {
10628
- return;
10629
- }
10630
- if (deps.replicationLogPosition(indexName, partitionId) > 0) {
10631
- return;
10632
- }
10633
- const floor = Math.max(deps.engine.highestPersistedSeqNoOf(indexName, partitionId), assignment.commitPoint);
10634
- if (floor <= 0) {
10635
- return;
10636
- }
10637
- deps.seedReplicationLog(indexName, partitionId, floor + 1, assignment.primaryTerm);
10638
- }
10639
-
10640
- // src/distribution/replication/types.ts
10641
- var DEFAULT_LOG_RETENTION_BYTES = 268435456;
10642
-
10643
- // src/distribution/replication/log.ts
10644
- var ENTRY_FIXED_OVERHEAD_BYTES2 = 40;
10645
- function estimateEntrySize(entry) {
10646
- return ENTRY_FIXED_OVERHEAD_BYTES2 + entry.indexName.length + entry.documentId.length + (entry.document?.byteLength ?? 0);
10647
- }
10648
- var computeChecksum = computeEntryChecksum;
10649
- function createReplicationLog(partitionId, config) {
10650
- const retentionBytes = config?.logRetentionBytes ?? DEFAULT_LOG_RETENTION_BYTES;
10651
- let nextSeqNo = config?.startSeqNo ?? 1;
10652
- let localLogEnd = nextSeqNo - 1;
10653
- let localLogEndPrimaryTerm = config?.lastPrimaryTerm ?? 0;
10654
- let commitPoint = nextSeqNo - 1;
10655
- let entries = [];
10656
- let headIndex = 0;
10657
- let totalSizeBytes = 0;
10658
- function liveCount() {
10659
- return entries.length - headIndex;
10660
- }
10661
- function compact() {
10662
- if (headIndex > 0) {
10663
- entries = entries.slice(headIndex);
10664
- headIndex = 0;
10665
- }
10666
- }
10667
- function findIndexBySeqNo(seqNo) {
10668
- let low = headIndex;
10669
- let high = entries.length - 1;
10670
- while (low <= high) {
10671
- const mid = low + high >>> 1;
10672
- const midSeqNo = entries[mid].seqNo;
10673
- if (midSeqNo < seqNo) {
10674
- low = mid + 1;
10675
- } else if (midSeqNo > seqNo) {
10676
- high = mid - 1;
10677
- } else {
10678
- return mid;
10679
- }
10680
- }
10681
- return low;
10682
- }
10683
- function evictOldEntries() {
10684
- while (liveCount() > 0 && totalSizeBytes > retentionBytes) {
10685
- totalSizeBytes -= estimateEntrySize(entries[headIndex]);
10686
- entries[headIndex] = null;
10687
- headIndex += 1;
10688
- }
10689
- if (totalSizeBytes < 0) {
10690
- totalSizeBytes = 0;
10691
- }
10692
- if (headIndex >= 256 || headIndex > liveCount()) {
10693
- compact();
10694
- }
10695
- }
10696
- function assertEntryFitsRetention(entry) {
10697
- const entrySize = ENTRY_FIXED_OVERHEAD_BYTES2 + entry.indexName.length + entry.documentId.length + (entry.document?.byteLength ?? 0);
10698
- if (entrySize > retentionBytes) {
10699
- throw new NarsilError(
10700
- ErrorCodes.REPLICATION_LOG_FULL,
10701
- `Replication log entry of ${entrySize} bytes exceeds the log retention limit of ${retentionBytes} bytes and cannot be buffered`,
10702
- { partitionId, entrySize, retentionBytes, seqNo: entry.seqNo }
10703
- );
10704
- }
10705
- }
10706
- function storeEntry(entry) {
10707
- entries.push(entry);
10708
- if (entry.seqNo >= localLogEnd) {
10709
- localLogEnd = entry.seqNo;
10710
- localLogEndPrimaryTerm = entry.primaryTerm;
10711
- }
10712
- totalSizeBytes += estimateEntrySize(entry);
10713
- evictOldEntries();
10714
- return { ...entry };
10715
- }
10716
- function readEntry(seqNo) {
10717
- if (liveCount() === 0) return void 0;
10718
- const idx = findIndexBySeqNo(seqNo);
10719
- if (idx < entries.length && entries[idx].seqNo === seqNo) {
10720
- return { ...entries[idx] };
10721
- }
10722
- return void 0;
10723
- }
10724
- function verifyEntryChecksum(entry) {
10725
- const expected = computeChecksum({
10726
- seqNo: entry.seqNo,
10727
- primaryTerm: entry.primaryTerm,
10728
- operation: entry.operation,
10729
- partitionId: entry.partitionId,
10730
- indexName: entry.indexName,
10731
- documentId: entry.documentId,
10732
- document: entry.document
10733
- });
10734
- return expected === entry.checksum;
10735
- }
10736
- return {
10737
- append(partial) {
10738
- assertEntryFitsRetention(partial);
10739
- const seqNo = nextSeqNo;
10740
- nextSeqNo += 1;
10741
- const entryWithoutChecksum = {
10742
- seqNo,
10743
- primaryTerm: partial.primaryTerm,
10744
- operation: partial.operation,
10745
- partitionId,
10746
- indexName: partial.indexName,
10747
- documentId: partial.documentId,
10748
- document: partial.document
10749
- };
10750
- const checksum = computeChecksum(entryWithoutChecksum);
10751
- const entry = { ...entryWithoutChecksum, checksum };
10752
- return storeEntry(entry);
10753
- },
10754
- appendCommitted(entry) {
10755
- if (!Number.isSafeInteger(entry.seqNo) || entry.seqNo < 1) {
10756
- throw new NarsilError(
10757
- ErrorCodes.REPLICATION_ENTRY_INVALID,
10758
- `Invalid replication sequence number ${entry.seqNo}`,
10759
- {
10760
- seqNo: entry.seqNo,
10761
- partitionId
10762
- }
10763
- );
10764
- }
10765
- if (entry.partitionId !== partitionId) {
10766
- throw new NarsilError(
10767
- ErrorCodes.REPLICATION_ENTRY_INVALID,
10768
- `Replication entry partition ${entry.partitionId} does not match log partition ${partitionId}`,
10769
- { entryPartitionId: entry.partitionId, logPartitionId: partitionId }
10770
- );
10771
- }
10772
- const existing = readEntry(entry.seqNo);
10773
- if (existing !== void 0) {
10774
- if (existing.checksum !== entry.checksum) {
10775
- throw new NarsilError(
10776
- ErrorCodes.REPLICATION_ENTRY_INVALID,
10777
- `Conflicting replication entry at sequence number ${entry.seqNo}`,
10778
- { seqNo: entry.seqNo, partitionId }
10779
- );
10788
+ if (page.documents.length === 0) {
10789
+ return;
10790
+ }
10791
+ const result = await routeRemoveBatch(
10792
+ indexName,
10793
+ page.documents.map((listed) => listed.id),
10794
+ writeDeps
10795
+ );
10796
+ if (result.succeeded.length === 0) {
10797
+ const firstFailure = result.failed[0];
10798
+ throw new NarsilError(
10799
+ ErrorCodes.QUERY_ROUTING_FAILED,
10800
+ `Clearing index '${indexName}' stalled: no document in the current page could be removed`,
10801
+ {
10802
+ indexName,
10803
+ remaining: page.total,
10804
+ firstFailureDocId: firstFailure?.docId,
10805
+ firstFailureCode: firstFailure?.error.code,
10806
+ firstFailureMessage: firstFailure?.error.message
10780
10807
  }
10781
- return existing;
10782
- }
10783
- if (entry.seqNo !== nextSeqNo) {
10784
- throw new NarsilError(
10785
- ErrorCodes.REPLICATION_ENTRY_INVALID,
10786
- `Out-of-order replication entry ${entry.seqNo}; expected ${nextSeqNo}`,
10787
- { seqNo: entry.seqNo, expectedSeqNo: nextSeqNo, partitionId }
10788
- );
10789
- }
10790
- if (!verifyEntryChecksum(entry)) {
10791
- throw new NarsilError(
10792
- ErrorCodes.REPLICATION_ENTRY_CORRUPT,
10793
- `Invalid checksum for replication entry ${entry.seqNo}`,
10794
- { seqNo: entry.seqNo, partitionId }
10795
- );
10796
- }
10797
- assertEntryFitsRetention(entry);
10798
- nextSeqNo = entry.seqNo + 1;
10799
- return storeEntry({ ...entry });
10808
+ );
10809
+ }
10810
+ }
10811
+ }
10812
+
10813
+ // src/distribution/cluster-node/node-operations.ts
10814
+ function createClusterNodeOperations(deps) {
10815
+ const { trackOp, readDeps, writeDeps, engine, coordinator } = deps;
10816
+ return {
10817
+ async createIndex(name, indexConfig, options) {
10818
+ return trackOp(name, () => routeCreateIndex(name, indexConfig, options, coordinator, engine));
10800
10819
  },
10801
- getEntriesFrom(fromSeqNo) {
10802
- if (liveCount() === 0) return [];
10803
- const idx = findIndexBySeqNo(fromSeqNo);
10804
- if (idx >= entries.length) return [];
10805
- return entries.slice(idx).map((e) => ({ ...e }));
10820
+ async insert(indexName, document, docId, options) {
10821
+ return trackOp(indexName, () => routeInsert(indexName, document, docId, writeDeps, options));
10806
10822
  },
10807
- getEntry(seqNo) {
10808
- return readEntry(seqNo);
10823
+ async insertBatch(indexName, documents, options) {
10824
+ return trackOp(indexName, () => routeInsertBatch(indexName, documents, writeDeps, options));
10809
10825
  },
10810
- verifyChecksum(entry) {
10811
- return verifyEntryChecksum(entry);
10826
+ async remove(indexName, docId) {
10827
+ return trackOp(indexName, () => routeRemove(indexName, docId, writeDeps));
10812
10828
  },
10813
- get localLogEnd() {
10814
- return localLogEnd;
10829
+ async removeBatch(indexName, docIds) {
10830
+ return trackOp(indexName, () => routeRemoveBatch(indexName, docIds, writeDeps));
10815
10831
  },
10816
- get localLogEndPrimaryTerm() {
10817
- return localLogEndPrimaryTerm;
10832
+ async update(indexName, docId, document) {
10833
+ return trackOp(indexName, () => routeUpdate(indexName, docId, document, writeDeps));
10818
10834
  },
10819
- get commitPoint() {
10820
- return commitPoint;
10835
+ async updateBatch(indexName, updates) {
10836
+ return trackOp(indexName, () => routeUpdateBatch(indexName, updates, writeDeps));
10821
10837
  },
10822
- advanceCommitPoint(seqNo) {
10823
- const bounded = Math.min(seqNo, localLogEnd);
10824
- if (bounded > commitPoint) {
10825
- commitPoint = bounded;
10826
- }
10838
+ async dropIndex(name) {
10839
+ return deps.transitionIndex(name, async () => {
10840
+ await routeDropIndex(name, coordinator, engine);
10841
+ deps.forgetIndex(name);
10842
+ });
10827
10843
  },
10828
- get oldestSeqNo() {
10829
- if (liveCount() === 0) return void 0;
10830
- return entries[headIndex].seqNo;
10844
+ async clear(indexName) {
10845
+ return trackOp(indexName, () => clearCluster(readDeps, writeDeps, indexName));
10831
10846
  },
10832
- get newestSeqNo() {
10833
- if (liveCount() === 0) return void 0;
10834
- return entries[entries.length - 1].seqNo;
10847
+ async countDocuments(indexName) {
10848
+ return trackOp(indexName, () => countCluster(readDeps, indexName));
10835
10849
  },
10836
- get entryCount() {
10837
- return liveCount();
10850
+ async listDocuments(indexName, params) {
10851
+ return trackOp(indexName, () => listCluster(readDeps, indexName, params ?? {}));
10838
10852
  },
10839
- get sizeBytes() {
10840
- return totalSizeBytes;
10853
+ async suggest(indexName, params) {
10854
+ return trackOp(indexName, () => suggestCluster(readDeps, indexName, params));
10841
10855
  },
10842
- clear() {
10843
- entries = [];
10844
- headIndex = 0;
10845
- totalSizeBytes = 0;
10846
- localLogEnd = nextSeqNo - 1;
10847
- commitPoint = nextSeqNo - 1;
10856
+ async preflight(indexName, params) {
10857
+ return trackOp(indexName, () => preflightCluster(readDeps, indexName, params));
10858
+ },
10859
+ async getStats(indexName) {
10860
+ return trackOp(indexName, () => statsCluster(readDeps, indexName));
10861
+ },
10862
+ async getPartitionStats(indexName) {
10863
+ return trackOp(indexName, () => partitionStatsCluster(readDeps, indexName));
10864
+ },
10865
+ async checkpoint(indexName) {
10866
+ return trackOp(indexName, () => engine.checkpoint(indexName));
10867
+ },
10868
+ async getMemoryStats() {
10869
+ return trackOp(null, () => engine.getMemoryStats());
10870
+ },
10871
+ on(event, handler) {
10872
+ engine.on(event, handler);
10873
+ },
10874
+ off(event, handler) {
10875
+ engine.off(event, handler);
10876
+ },
10877
+ async query(indexName, params) {
10878
+ return trackOp(indexName, () => queryCluster(readDeps, indexName, params));
10879
+ },
10880
+ async get(indexName, docId) {
10881
+ return trackOp(indexName, async () => (await readClusterDocuments(readDeps, indexName, [docId])).get(docId));
10882
+ },
10883
+ async getMultiple(indexName, docIds) {
10884
+ return trackOp(indexName, () => readClusterDocuments(readDeps, indexName, docIds));
10885
+ },
10886
+ async has(indexName, docId) {
10887
+ return trackOp(indexName, async () => (await readClusterDocuments(readDeps, indexName, [docId])).has(docId));
10848
10888
  }
10849
10889
  };
10850
10890
  }
10851
10891
 
10852
- // src/distribution/cluster-node/replication-logs.ts
10853
- function replicationLogKey(indexName, partitionId) {
10854
- return `${indexName}:${partitionId}`;
10892
+ // src/distribution/cluster-node/operation-tracker.ts
10893
+ function createClusterOperationTracker(config) {
10894
+ let activeOperations = 0;
10895
+ let drainResolve = null;
10896
+ const activeByIndex = /* @__PURE__ */ new Map();
10897
+ const indexDrainResolvers = /* @__PURE__ */ new Map();
10898
+ const transitions = /* @__PURE__ */ new Map();
10899
+ function releaseIndex(indexName) {
10900
+ const remaining = (activeByIndex.get(indexName) ?? 1) - 1;
10901
+ if (remaining > 0) {
10902
+ activeByIndex.set(indexName, remaining);
10903
+ return;
10904
+ }
10905
+ activeByIndex.delete(indexName);
10906
+ for (const resolve of indexDrainResolvers.get(indexName) ?? []) resolve();
10907
+ indexDrainResolvers.delete(indexName);
10908
+ }
10909
+ async function track(indexName, action) {
10910
+ config.guard();
10911
+ if (indexName !== null) config.assertIndex(indexName);
10912
+ activeOperations += 1;
10913
+ let indexCounted = false;
10914
+ try {
10915
+ if (indexName !== null) {
10916
+ const transition2 = transitions.get(indexName);
10917
+ if (transition2 !== void 0) await transition2;
10918
+ config.assertIndex(indexName);
10919
+ activeByIndex.set(indexName, (activeByIndex.get(indexName) ?? 0) + 1);
10920
+ indexCounted = true;
10921
+ }
10922
+ return await action();
10923
+ } finally {
10924
+ if (indexName !== null && indexCounted) releaseIndex(indexName);
10925
+ activeOperations -= 1;
10926
+ if (activeOperations === 0 && drainResolve !== null) {
10927
+ drainResolve();
10928
+ drainResolve = null;
10929
+ }
10930
+ }
10931
+ }
10932
+ async function waitForIndexDrain(indexName) {
10933
+ if ((activeByIndex.get(indexName) ?? 0) === 0) return;
10934
+ await new Promise((resolve) => {
10935
+ let resolvers = indexDrainResolvers.get(indexName);
10936
+ if (resolvers === void 0) {
10937
+ resolvers = /* @__PURE__ */ new Set();
10938
+ indexDrainResolvers.set(indexName, resolvers);
10939
+ }
10940
+ resolvers.add(resolve);
10941
+ });
10942
+ }
10943
+ function transition(indexName, action) {
10944
+ return track(null, async () => {
10945
+ const previous = transitions.get(indexName) ?? Promise.resolve();
10946
+ const run = previous.catch(() => void 0).then(async () => {
10947
+ await waitForIndexDrain(indexName);
10948
+ return action();
10949
+ });
10950
+ const tail = run.then(
10951
+ () => void 0,
10952
+ () => void 0
10953
+ );
10954
+ transitions.set(indexName, tail);
10955
+ try {
10956
+ return await run;
10957
+ } finally {
10958
+ if (transitions.get(indexName) === tail) transitions.delete(indexName);
10959
+ }
10960
+ });
10961
+ }
10962
+ async function drain() {
10963
+ if (activeOperations === 0) return;
10964
+ await new Promise((resolve) => {
10965
+ drainResolve = resolve;
10966
+ });
10967
+ }
10968
+ return { track, transition, drain };
10855
10969
  }
10856
- function getReplicationLog(replicationLogs, indexName, partitionId, replicationConfig) {
10857
- const key = replicationLogKey(indexName, partitionId);
10858
- let log = replicationLogs.get(key);
10859
- if (log === void 0) {
10860
- log = createReplicationLog(partitionId, { logRetentionBytes: replicationConfig?.logRetentionBytes });
10861
- replicationLogs.set(key, log);
10970
+
10971
+ // src/distribution/cluster-node/primary-partition.ts
10972
+ async function preparePrimaryPartition(indexName, partitionId, deps) {
10973
+ const schema = await deps.coordinator.getSchema(indexName);
10974
+ if (schema === null) {
10975
+ return false;
10862
10976
  }
10863
- return log;
10977
+ const allocation = await deps.coordinator.getAllocation(indexName);
10978
+ if (allocation === null || allocation.assignments.size === 0) {
10979
+ return false;
10980
+ }
10981
+ const existing = deps.engine.listIndexes().find((index) => index.name === indexName);
10982
+ if (existing === void 0) {
10983
+ try {
10984
+ await deps.engine.createIndex(indexName, {
10985
+ schema,
10986
+ partitions: { maxPartitions: allocation.assignments.size }
10987
+ });
10988
+ } catch (error) {
10989
+ if (!(error instanceof NarsilError) || error.code !== ErrorCodes.INDEX_ALREADY_EXISTS) {
10990
+ deps.onError(error);
10991
+ return false;
10992
+ }
10993
+ }
10994
+ } else {
10995
+ const schemaError = validateRestoredSchema(deps.engine, indexName, deps.nodeId, schema);
10996
+ if (schemaError !== null) {
10997
+ deps.onError(schemaError);
10998
+ return false;
10999
+ }
11000
+ }
11001
+ resumeNumberingAboveOwnCopy(indexName, partitionId, allocation.assignments.get(partitionId), deps);
11002
+ return true;
10864
11003
  }
10865
- function seedReplicationLog(replicationLogs, indexName, partitionId, startSeqNo, lastPrimaryTerm = 0, replicationConfig) {
10866
- replicationLogs.set(
10867
- replicationLogKey(indexName, partitionId),
10868
- createReplicationLog(partitionId, {
10869
- startSeqNo,
10870
- lastPrimaryTerm,
10871
- logRetentionBytes: replicationConfig?.logRetentionBytes
10872
- })
10873
- );
11004
+ function resumeNumberingAboveOwnCopy(indexName, partitionId, assignment, deps) {
11005
+ if (assignment === void 0) {
11006
+ return;
11007
+ }
11008
+ if (deps.replicationLogPosition(indexName, partitionId) > 0) {
11009
+ return;
11010
+ }
11011
+ const floor = Math.max(deps.engine.highestPersistedSeqNoOf(indexName, partitionId), assignment.commitPoint);
11012
+ if (floor <= 0) {
11013
+ return;
11014
+ }
11015
+ deps.seedReplicationLog(indexName, partitionId, floor + 1, assignment.primaryTerm);
10874
11016
  }
10875
11017
  var CONTROLLER_MESSAGE_TYPES = /* @__PURE__ */ new Set([
10876
11018
  ReplicationMessageTypes.INSYNC_ADD,
@@ -11041,10 +11183,25 @@ async function createClusterNode(config) {
11041
11183
  const nodeId = config.nodeId ?? generateId();
11042
11184
  const roles = config.roles ?? ["data", "coordinator", "controller"];
11043
11185
  const capacity = config.capacity ?? DEFAULT_CAPACITY;
11044
- const engine = await createClusterLocalEngine(config.engine);
11045
11186
  const bootstrapSyncState = createBootstrapSyncState();
11046
11187
  const snapshotSyncHandlerState = createSnapshotSyncHandlerState();
11047
11188
  const replicationLogs = /* @__PURE__ */ new Map();
11189
+ let localEngine = null;
11190
+ const engine = await createClusterLocalEngine(config.engine, {
11191
+ async onIndexOpen(indexName) {
11192
+ if (localEngine === null) return;
11193
+ await seedReopenedPrimaryLogs({
11194
+ indexName,
11195
+ nodeId,
11196
+ engine: localEngine,
11197
+ coordinator: config.coordinator,
11198
+ replicationLogs,
11199
+ replicationConfig: config.replication
11200
+ });
11201
+ },
11202
+ onIndexClose: (indexName) => deleteIndexReplicationLogs(replicationLogs, indexName)
11203
+ });
11204
+ localEngine = engine;
11048
11205
  function replicationLogKey2(indexName, partitionId) {
11049
11206
  return replicationLogKey(indexName, partitionId);
11050
11207
  }
@@ -11094,8 +11251,6 @@ async function createClusterNode(config) {
11094
11251
  let unregisterNotReadyHandler = null;
11095
11252
  let serving = false;
11096
11253
  let isShutdown = false;
11097
- let activeOps = 0;
11098
- let drainResolve = null;
11099
11254
  const bootstrapSyncDeps = {
11100
11255
  engine,
11101
11256
  coordinator: config.coordinator,
@@ -11168,22 +11323,14 @@ async function createClusterNode(config) {
11168
11323
  throw new NarsilError(ErrorCodes.NODE_NOT_JOINED, `Cluster node '${nodeId}' has been shut down`, { nodeId });
11169
11324
  }
11170
11325
  }
11171
- async function trackOp(indexName, fn) {
11172
- guardShutdown();
11173
- if (indexName !== null && orphanedIndexes.has(indexName)) {
11174
- throw orphanedIndexError(nodeId, indexName);
11175
- }
11176
- activeOps++;
11177
- try {
11178
- return await fn();
11179
- } finally {
11180
- activeOps--;
11181
- if (activeOps === 0 && drainResolve !== null) {
11182
- drainResolve();
11183
- drainResolve = null;
11184
- }
11326
+ const operationTracker = createClusterOperationTracker({
11327
+ guard: guardShutdown,
11328
+ assertIndex(indexName) {
11329
+ if (orphanedIndexes.has(indexName)) throw orphanedIndexError(nodeId, indexName);
11185
11330
  }
11186
- }
11331
+ });
11332
+ const trackOp = operationTracker.track;
11333
+ const transitionIndex = operationTracker.transition;
11187
11334
  const readDeps = { config, nodeId, engine, resolveNodeTargets: resolveNodeTargets3 };
11188
11335
  const cluster = createClusterNamespace({
11189
11336
  nodeId,
@@ -11202,13 +11349,17 @@ async function createClusterNode(config) {
11202
11349
  get roles() {
11203
11350
  return roles;
11204
11351
  },
11352
+ open: (indexName) => trackOp(indexName, () => engine.open(indexName)),
11353
+ close: (indexName) => transitionIndex(indexName, () => engine.close(indexName)),
11205
11354
  ...createClusterNodeOperations({
11206
11355
  trackOp,
11356
+ transitionIndex,
11207
11357
  readDeps,
11208
11358
  writeDeps,
11209
11359
  engine,
11210
11360
  coordinator: config.coordinator,
11211
11361
  forgetIndex: (indexName) => {
11362
+ deleteIndexReplicationLogs(replicationLogs, indexName);
11212
11363
  if (engine.listIndexes().every((index) => index.name !== indexName)) {
11213
11364
  orphanedIndexes.delete(indexName);
11214
11365
  }
@@ -11258,11 +11409,7 @@ async function createClusterNode(config) {
11258
11409
  }
11259
11410
  isShutdown = true;
11260
11411
  serving = false;
11261
- if (activeOps > 0) {
11262
- await new Promise((resolve) => {
11263
- drainResolve = resolve;
11264
- });
11265
- }
11412
+ await operationTracker.drain();
11266
11413
  if (unregisterHandler !== null) {
11267
11414
  unregisterHandler();
11268
11415
  unregisterHandler = null;
@@ -11285,14 +11432,20 @@ async function createClusterNode(config) {
11285
11432
  if (partitionId === null) {
11286
11433
  return fallback;
11287
11434
  }
11288
- const allocation = await config.coordinator.getAllocation(indexName);
11289
- const assignment = allocation?.assignments.get(partitionId);
11290
- const log = getReplicationLog2(indexName, partitionId);
11291
- return {
11292
- partitionId,
11293
- primaryTerm: assignment?.primaryTerm ?? fallback.primaryTerm,
11294
- lastSeqNo: log.newestSeqNo ?? 0
11295
- };
11435
+ const localIndex = engine.listIndexes().find((index) => index.name === indexName);
11436
+ const release = localIndex !== void 0 ? await engine.acquireIndexForReplication(indexName) : null;
11437
+ try {
11438
+ const allocation = await config.coordinator.getAllocation(indexName);
11439
+ const assignment = allocation?.assignments.get(partitionId);
11440
+ const log = getReplicationLog2(indexName, partitionId);
11441
+ return {
11442
+ partitionId,
11443
+ primaryTerm: assignment?.primaryTerm ?? fallback.primaryTerm,
11444
+ lastSeqNo: log.newestSeqNo ?? 0
11445
+ };
11446
+ } finally {
11447
+ release?.();
11448
+ }
11296
11449
  }
11297
11450
  }
11298
11451
 
@@ -11306,6 +11459,12 @@ function unsupported(operation) {
11306
11459
  }
11307
11460
  function clusterNodeEngine(node, options) {
11308
11461
  return {
11462
+ open(indexName) {
11463
+ return node.open(indexName);
11464
+ },
11465
+ close(indexName) {
11466
+ return node.close(indexName);
11467
+ },
11309
11468
  async createIndex(name, config) {
11310
11469
  return node.createIndex(name, config, options?.createIndex);
11311
11470
  },