@delali/narsil 0.3.1-next.71 → 0.3.1-next.73

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 (69) 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/{capabilities-Dt-7B4Wz.d.ts → capabilities-CYeAWYxQ.d.ts} +19 -4
  5. package/dist/{chunk-3JGWULDF.mjs → chunk-2WLD2NFW.mjs} +5 -5
  6. package/dist/{chunk-LMO5TJUS.mjs → chunk-3YKWSO54.mjs} +642 -196
  7. package/dist/{chunk-HWQLTLNG.mjs → chunk-4MQ5F4IL.mjs} +1 -1
  8. package/dist/{chunk-FVLQ63B7.mjs → chunk-AFKH4ZK3.mjs} +30 -6
  9. package/dist/{chunk-653BNMAF.mjs → chunk-AK3BLGCI.mjs} +195 -25
  10. package/dist/{chunk-IOR2K26O.mjs → chunk-CVFAFZSN.mjs} +8000 -4391
  11. package/dist/{chunk-USKFU7J5.mjs → chunk-I64LXKO3.mjs} +1 -1
  12. package/dist/{chunk-OI42GWLR.mjs → chunk-LJYTW7GT.mjs} +57 -11
  13. package/dist/{chunk-2STVUTNA.mjs → chunk-LPBWCHBE.mjs} +194 -13
  14. package/dist/{chunk-YNVIUGF5.mjs → chunk-PU43WDY4.mjs} +4 -2
  15. package/dist/{chunk-M76V3OGJ.mjs → chunk-THMSFOJO.mjs} +1289 -1405
  16. package/dist/{chunk-YJDQ7NLA.mjs → chunk-WVDFG45M.mjs} +93 -95
  17. package/dist/chunk-XW25X6AU.mjs +614 -0
  18. package/dist/chunk-ZKRV5M2P.mjs +75 -0
  19. package/dist/{chunk-P4DSQ6M4.mjs → chunk-ZULSEZCE.mjs} +68 -28
  20. package/dist/{client-COek5GXK.d.ts → client-D698E5ge.d.ts} +77 -26
  21. package/dist/client.d.ts +9 -9
  22. package/dist/client.mjs +45 -22
  23. package/dist/{config-rBpbgTw8.d.ts → config-Bv8etwUZ.d.ts} +11 -4
  24. package/dist/{config-Dfanyqqt.d.ts → config-QXU3HMpq.d.ts} +26 -2
  25. package/dist/direct-executor-5SRDFZAS.mjs +13 -0
  26. package/dist/distribution/coordinator/etcd.d.ts +29 -16
  27. package/dist/distribution/coordinator/etcd.mjs +163 -76
  28. package/dist/distribution/coordinator/in-memory.d.ts +2 -2
  29. package/dist/distribution/coordinator/in-memory.mjs +1 -1
  30. package/dist/distribution/transport/grpc.d.ts +2 -2
  31. package/dist/distribution/transport/grpc.mjs +22 -2
  32. package/dist/distribution/transport/in-memory.d.ts +2 -2
  33. package/dist/distribution/transport/in-memory.mjs +6 -3
  34. package/dist/distribution/transport/tcp.d.ts +2 -2
  35. package/dist/distribution/transport/tcp.mjs +40 -16
  36. package/dist/distribution.d.ts +9 -9
  37. package/dist/distribution.mjs +682 -388
  38. package/dist/embeddings/openai.mjs +1 -1
  39. package/dist/{engine-igk0aMpQ.d.ts → engine-D_OzKZ-6.d.ts} +97 -188
  40. package/dist/index.browser.mjs +11671 -7427
  41. package/dist/index.d.ts +27 -8
  42. package/dist/index.mjs +13 -14
  43. package/dist/invalidation/filesystem.mjs +1 -1
  44. package/dist/{memory-C7UMCMH3.d.ts → memory-B4jMmygw.d.ts} +279 -27
  45. package/dist/pattern-Cledccbl.d.ts +18 -0
  46. package/dist/pattern.d.ts +22 -0
  47. package/dist/pattern.mjs +7 -0
  48. package/dist/persistence/durability/checkpoint-worker.d.ts +41 -17
  49. package/dist/persistence/durability/checkpoint-worker.mjs +24 -16
  50. package/dist/react.d.ts +7 -7
  51. package/dist/react.mjs +1 -1
  52. package/dist/{schema-Bgzr4g7b.d.ts → schema-BBOCdhND.d.ts} +9 -7
  53. package/dist/server.d.ts +11 -11
  54. package/dist/server.mjs +213 -34
  55. package/dist/{tasks-BZt_zes6.d.ts → tasks-DpgCpW1x.d.ts} +3 -3
  56. package/dist/{thread-JEVAYETR.mjs → thread-3LNVA43V.mjs} +94 -27
  57. package/dist/{types-BQDlIvfS.d.ts → types-B3b68nfF.d.ts} +1 -1
  58. package/dist/{types-gOUqhs2F.d.ts → types-BiUmaiWT.d.ts} +1 -1
  59. package/dist/{types-D5OwhtsQ.d.ts → types-Cb9kiY5P.d.ts} +1 -1
  60. package/dist/vector/search-worker.d.ts +1 -1
  61. package/dist/vector/search-worker.mjs +4 -4
  62. package/dist/workers/entry.mjs +4 -4
  63. package/package.json +15 -7
  64. package/dist/chunk-3L4U6EJQ.mjs +0 -49
  65. package/dist/chunk-GIVAGD3Q.mjs +0 -25
  66. package/dist/chunk-IFBKLBGB.mjs +0 -7
  67. package/dist/direct-executor-GYJEKNV5.mjs +0 -12
  68. package/dist/serialization/crc32-worker.d.ts +0 -20
  69. package/dist/serialization/crc32-worker.mjs +0 -62
@@ -1,25 +1,29 @@
1
1
  import { MAX_MESSAGE_SIZE_BYTES } from './chunk-UNEEFTEU.mjs';
2
- import { validateIndexName, indexConfigKey, indexSettingsOf, putIndexMetadata, withPartitionCount, getIndexMetadata, getClusterIndexConfig } from './chunk-3JGWULDF.mjs';
3
- import { requireKnownConfig, resolveRunnableConfig, createEngineCore, startEngineCore, createNarsilFromCore, runEngineQueryStats, runEngineListDocuments, runEngineSuggest, runEnginePreflight, runEngineQuery, createEngineIndex, applyIndexEntry, applyDeleteEntry, validateEntryBatchPayload, createAckMessage, validateEntryPayload, validateForwardBatchPayload, createForwardBatchResultMessage, createForwardBatchMessage, validateForwardBatchResultPayload, createForwardMessage, validateReplicationEntry, computeEntryChecksum, createEntryMessage, createEntryBatchMessage, createInsyncConfirmMessage, validateAckPayload, createInsyncRemoveMessage, validateInsyncConfirmPayload, createInsyncAddMessage } from './chunk-IOR2K26O.mjs';
2
+ import { validateIndexName, indexConfigKey, withPartitionCount, indexSettingsOf, putIndexMetadata, getIndexMetadata, getClusterIndexConfig } from './chunk-2WLD2NFW.mjs';
3
+ import { requireKnownConfig, resolveRunnableConfig, createEngineCore, startEngineCore, createNarsilFromCore, runEngineQueryStats, runEngineListDocuments, runEngineSuggest, runEnginePreflight, runEngineQuery, createEngineIndex, buildArchiveFromMemory, indexMetadataOf, encodeIndexConfiguration, archiveStream, applyIndexEntry, applyDeleteEntry, requireConfigurationWithinSizeLimit, validateEntryBatchPayload, createAckMessage, validateEntryPayload, validateForwardBatchPayload, createForwardBatchResultMessage, memoryCheckpointStore, readSnapshotArchive, createByteReader, openChecksummedEnvelope, loadPartitionSegments, readListedFile, createForwardBatchMessage, validateForwardBatchResultPayload, createForwardMessage, validateReplicationEntry, crc32Init, crc32Final, crc32Update, computeEntryChecksum, createEntryMessage, createEntryBatchMessage, createInsyncConfirmMessage, validateAckPayload, createInsyncRemoveMessage, validateInsyncConfirmPayload, createInsyncAddMessage } from './chunk-CVFAFZSN.mjs';
4
4
  import { ReplicationMessageTypes, TransportErrorCodes, ClusterMessageTypes, QueryMessageTypes, TransportError } from './chunk-GCM3JFVA.mjs';
5
- import { providedDocId, MAX_PARTITION_COUNT, MAX_REPLICATION_FACTOR, WIRE_BATCH_BUDGET } from './chunk-FVLQ63B7.mjs';
6
- import { generateId, deserializePayloadV2, createPartitionIndex, FILTER_EXPRESSION_KEYS, FIELD_FILTER_OPERATORS } from './chunk-M76V3OGJ.mjs';
7
- import { getLanguage } from './chunk-HWQLTLNG.mjs';
8
- import { now, listBindingOf, queryBindingOf, hitsKeptPerGroup, clampLimit, resolveHybridFusion, dedupePinnedEntries, foldGroupReducer } from './chunk-P4DSQ6M4.mjs';
9
- import { DEFAULT_SUGGEST_LIMIT, MAX_SUGGEST_LIMIT, MAX_SUGGEST_SCATTER_LIMIT, MAX_INDEX_NAME_LENGTH, MAX_DOC_ID_LENGTH, SUGGEST_OVERSAMPLE_FACTOR, SUGGEST_OVERSAMPLE_PADDING, INDEX_NAME_PATTERN } from './chunk-GIVAGD3Q.mjs';
10
- import { oversampledShardSize, resolveProjection, clampRowCount, sortSignatureOf, requireMatchingCursor, decodePageCursor, normalizeSort, sortModesOf, encodePageCursor, applyProjection, readSortValues, requireWithinResultWindow, fnv1a, sortSignatureEntry, MAX_FILTER_DEPTH, fnv1aUint32s, mergePartitionStats, MIN_POLYGON_POINTS } from './chunk-LMO5TJUS.mjs';
11
- import { validateSchema, DEFAULT_PAGE_SIZE, MAX_SORT_FIELDS, MAX_CURSOR_LENGTH, MAX_TOLERANCE, MAX_PREFIX_LENGTH, RESULT_WINDOW, MAX_NESTING_DEPTH } from './chunk-653BNMAF.mjs';
5
+ import { providedDocId, MAX_PARTITION_COUNT, MAX_REPLICATION_FACTOR, WIRE_BATCH_BUDGET } from './chunk-AFKH4ZK3.mjs';
6
+ import { generateId, decodeVectorFilePayload, bytesToVectors, isDead, FILTER_EXPRESSION_KEYS, FIELD_FILTER_OPERATORS } from './chunk-THMSFOJO.mjs';
7
+ import { getLanguage } from './chunk-4MQ5F4IL.mjs';
8
+ import { now, listBindingOf, queryBindingOf, hitsKeptPerGroup, clampLimit, resolveHybridFusion, dedupePinnedEntries, foldGroupReducer } from './chunk-ZULSEZCE.mjs';
9
+ import { oversampledShardSize, resolveProjection, clampRowCount, sortSignatureOf, requireMatchingCursor, decodePageCursor, normalizeSort, sortModesOf, encodePageCursor, applyProjection, readSortValues, requireWithinResultWindow, fnv1a, sortSignatureEntry, MAX_FILTER_DEPTH, filterTestCount, MAX_FILTER_TESTS, fnv1aUint32s, mergePartitionStats, MAX_FILTER_ARRAY_SIZE } from './chunk-3YKWSO54.mjs';
10
+ import { DEFAULT_SUGGEST_LIMIT, MAX_SUGGEST_LIMIT, validateSchema, requireCreatableSchema, DEFAULT_PAGE_SIZE, encodeDocument, MAX_SUGGEST_SCATTER_LIMIT, MAX_INDEX_NAME_LENGTH, MAX_SORT_FIELDS, MAX_DOC_ID_LENGTH, MAX_CURSOR_LENGTH, MAX_TOLERANCE, SUGGEST_OVERSAMPLE_FACTOR, SUGGEST_OVERSAMPLE_PADDING, INDEX_NAME_PATTERN, MAX_PREFIX_LENGTH, RESULT_WINDOW, MAX_STORED_VECTOR_DIMENSION, MIN_POLYGON_POINTS, isLatitude, MAX_LATITUDE_DEGREES, isLongitude, MAX_LONGITUDE_DEGREES, MAX_NESTING_DEPTH } from './chunk-AK3BLGCI.mjs';
12
11
  import './chunk-TGP5M5R7.mjs';
13
- import './chunk-YJDQ7NLA.mjs';
12
+ import './chunk-WVDFG45M.mjs';
14
13
  import { MIN_OVERSAMPLE } from './chunk-ZJ4CSAB2.mjs';
15
- import './chunk-YNVIUGF5.mjs';
16
- import { crc32 } from './chunk-3L4U6EJQ.mjs';
17
- import './chunk-IFBKLBGB.mjs';
14
+ import './chunk-PU43WDY4.mjs';
15
+ import './chunk-XW25X6AU.mjs';
16
+ import './chunk-ZKRV5M2P.mjs';
18
17
  import './chunk-U2BKYEC7.mjs';
19
- import { compareCodePoints, toComparableSortValue, compareComparableKeys, isSortMode, SORT_MODES, compareSortValues } from './chunk-OI42GWLR.mjs';
18
+ import { compareCodePoints, toComparableSortValue, compareComparableKeys, isSortMode, SORT_MODES, exceedsCodePoints, compareSortValues } from './chunk-LJYTW7GT.mjs';
20
19
  import './chunk-POWOTUKJ.mjs';
21
- import { ErrorCodes, NarsilError, describeError } from './chunk-USKFU7J5.mjs';
20
+ import { ErrorCodes, NarsilError, describeError } from './chunk-I64LXKO3.mjs';
22
21
  import { encode, decode } from '@msgpack/msgpack';
22
+ import { randomUUID } from 'crypto';
23
+ import { openSync, writeSync, closeSync, rmSync, readSync } from 'fs';
24
+ import { mkdtemp, rm, mkdir } from 'fs/promises';
25
+ import { tmpdir } from 'os';
26
+ import { join } from 'path';
23
27
 
24
28
  // src/distribution/cluster/constants.ts
25
29
  var LEADERSHIP_IMBALANCE_THRESHOLD = 2;
@@ -1249,7 +1253,6 @@ var REPLICATION_ENTRY_FIXED_OVERHEAD_BYTES = 40;
1249
1253
  var DEFAULT_LOG_RETENTION_BYTES = 268435456;
1250
1254
  var INSYNC_CAS_ATTEMPTS = 5;
1251
1255
  var SNAPSHOT_CHUNK_SIZE = 65536;
1252
- var MAX_SNAPSHOT_SIZE_BYTES = 2 * 1024 * 1024 * 1024;
1253
1256
 
1254
1257
  // src/distribution/replication/insync.ts
1255
1258
  var REFUSE = { outcome: "refuse" };
@@ -2514,6 +2517,119 @@ function createDataNodeLifecycle(config) {
2514
2517
  };
2515
2518
  return handle;
2516
2519
  }
2520
+ var SPOOL_FILE_PREFIX = "narsil-snapshot-";
2521
+ var DURABLE_SPOOL_DIRECTORY = ".snapshot-transfer";
2522
+ var TEMPORARY_SPOOL_DIRECTORY_PREFIX = "narsil-snapshot-transfer-";
2523
+ var OWNER_ONLY_FILE_MODE = 384;
2524
+ var OWNER_ONLY_DIRECTORY_MODE = 448;
2525
+ function defaultSpoolDirectory() {
2526
+ return tmpdir();
2527
+ }
2528
+ function spoolFilePath(directory) {
2529
+ return join(directory, `${SPOOL_FILE_PREFIX}${randomUUID()}`);
2530
+ }
2531
+ async function* readSpoolFile(path) {
2532
+ const descriptor = openSync(path, "r");
2533
+ try {
2534
+ for (; ; ) {
2535
+ const buffer = new Uint8Array(SNAPSHOT_CHUNK_SIZE);
2536
+ const bytesRead = readSync(descriptor, buffer, 0, buffer.byteLength, null);
2537
+ if (bytesRead === 0) return;
2538
+ yield bytesRead === buffer.byteLength ? buffer : buffer.subarray(0, bytesRead);
2539
+ }
2540
+ } finally {
2541
+ closeSync(descriptor);
2542
+ }
2543
+ }
2544
+ function spooledFile(path, totalBytes, checksum) {
2545
+ let discarded = false;
2546
+ return {
2547
+ totalBytes,
2548
+ checksum,
2549
+ read: () => readSpoolFile(path),
2550
+ async discard() {
2551
+ if (discarded) return;
2552
+ discarded = true;
2553
+ removeQuietly(path);
2554
+ }
2555
+ };
2556
+ }
2557
+ function openSnapshotSpoolWriter(directory) {
2558
+ const path = spoolFilePath(directory);
2559
+ let descriptor = openSync(path, "wx", OWNER_ONLY_FILE_MODE);
2560
+ let checksumState = crc32Init();
2561
+ let totalBytes = 0;
2562
+ const close = () => {
2563
+ if (descriptor === null) return;
2564
+ const closing = descriptor;
2565
+ descriptor = null;
2566
+ closeSync(closing);
2567
+ };
2568
+ return {
2569
+ write(bytes) {
2570
+ if (descriptor === null) {
2571
+ throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "The snapshot spool file is already closed", {
2572
+ path
2573
+ });
2574
+ }
2575
+ let offset = 0;
2576
+ while (offset < bytes.byteLength) {
2577
+ offset += writeSync(descriptor, bytes, offset, bytes.byteLength - offset);
2578
+ }
2579
+ checksumState = crc32Update(checksumState, bytes);
2580
+ totalBytes += bytes.byteLength;
2581
+ },
2582
+ checksum() {
2583
+ return crc32Final(checksumState);
2584
+ },
2585
+ finish() {
2586
+ close();
2587
+ return spooledFile(path, totalBytes, crc32Final(checksumState));
2588
+ },
2589
+ discard() {
2590
+ try {
2591
+ close();
2592
+ } finally {
2593
+ removeQuietly(path);
2594
+ }
2595
+ }
2596
+ };
2597
+ }
2598
+ function removeQuietly(path) {
2599
+ try {
2600
+ rmSync(path, { force: true });
2601
+ } catch (_) {
2602
+ }
2603
+ }
2604
+ async function spoolSnapshotStream(directory, stream) {
2605
+ const reader = stream.getReader();
2606
+ let writer = null;
2607
+ try {
2608
+ writer = openSnapshotSpoolWriter(directory);
2609
+ for (; ; ) {
2610
+ const step = await reader.read();
2611
+ if (step.done) break;
2612
+ if (step.value.byteLength > 0) writer.write(step.value);
2613
+ }
2614
+ const spooled = writer.finish();
2615
+ writer = null;
2616
+ return spooled;
2617
+ } catch (err) {
2618
+ writer?.discard();
2619
+ await reader.cancel().catch(() => void 0);
2620
+ throw err;
2621
+ } finally {
2622
+ reader.releaseLock();
2623
+ }
2624
+ }
2625
+ async function openSnapshotSpoolDirectory(durabilityDirectory) {
2626
+ const path = durabilityDirectory === void 0 ? await mkdtemp(join(tmpdir(), TEMPORARY_SPOOL_DIRECTORY_PREFIX)) : join(durabilityDirectory, DURABLE_SPOOL_DIRECTORY);
2627
+ if (durabilityDirectory !== void 0) {
2628
+ await rm(path, { recursive: true, force: true });
2629
+ await mkdir(path, { recursive: true, mode: OWNER_ONLY_DIRECTORY_MODE });
2630
+ }
2631
+ return { path, dispose: () => rm(path, { recursive: true, force: true }) };
2632
+ }
2517
2633
 
2518
2634
  // src/distribution/cluster-node/local-index-gone.ts
2519
2635
  function localIndexIsGone(err) {
@@ -2581,6 +2697,19 @@ function fail(state, code, message, details = {}) {
2581
2697
  return;
2582
2698
  }
2583
2699
  state.failure = { ok: false, code, message, details };
2700
+ releaseSpool(state);
2701
+ }
2702
+ function releaseSpool(state) {
2703
+ const spool = state.spool;
2704
+ if (spool === null) return;
2705
+ state.spool = null;
2706
+ spool.discard();
2707
+ }
2708
+ function spoolFailure(state, err) {
2709
+ const cause = err instanceof Error ? err.message : String(err);
2710
+ fail(state, ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, `the snapshot could not be written to local storage: ${cause}`, {
2711
+ cause
2712
+ });
2584
2713
  }
2585
2714
  function applyErrorEnvelope(state, record) {
2586
2715
  const code = typeof record.code === "string" && record.code.length > 0 ? record.code : "UNKNOWN_ERROR";
@@ -2631,19 +2760,17 @@ function applyStartPayload(state, record) {
2631
2760
  fail(state, ErrorCodes.SNAPSHOT_SYNC_HEADER_INVALID, "totalBytes must be a non-negative integer");
2632
2761
  return;
2633
2762
  }
2634
- if (start.totalBytes > MAX_SNAPSHOT_SIZE_BYTES) {
2635
- fail(state, ErrorCodes.SNAPSHOT_SYNC_TOO_LARGE, `totalBytes ${start.totalBytes} exceeds the limit`, {
2636
- totalBytes: start.totalBytes,
2637
- limit: MAX_SNAPSHOT_SIZE_BYTES
2638
- });
2763
+ try {
2764
+ state.spool = openSnapshotSpoolWriter(state.expectation.spoolDirectory);
2765
+ } catch (err) {
2766
+ spoolFailure(state, err);
2639
2767
  return;
2640
2768
  }
2641
2769
  state.header = header;
2642
2770
  state.totalBytes = start.totalBytes;
2643
- state.assembled = new Uint8Array(start.totalBytes);
2644
2771
  }
2645
2772
  function applyChunkPayload(state, payload) {
2646
- if (state.totalBytes === null || state.assembled === null || state.header === null) {
2773
+ if (state.totalBytes === null || state.header === null) {
2647
2774
  fail(state, ErrorCodes.SNAPSHOT_SYNC_FRAME_INVALID, "received chunk before SNAPSHOT_START");
2648
2775
  return;
2649
2776
  }
@@ -2697,7 +2824,16 @@ function applyChunkPayload(state, payload) {
2697
2824
  });
2698
2825
  return;
2699
2826
  }
2700
- state.assembled.set(payload.data, payload.offset);
2827
+ if (state.spool === null) {
2828
+ fail(state, ErrorCodes.SNAPSHOT_SYNC_FRAME_INVALID, "received chunk after the snapshot stream closed");
2829
+ return;
2830
+ }
2831
+ try {
2832
+ state.spool.write(payload.data);
2833
+ } catch (err) {
2834
+ spoolFailure(state, err);
2835
+ return;
2836
+ }
2701
2837
  state.accumulatedBytes = nextBytes;
2702
2838
  state.expectedNextOffset = payload.offset + payload.data.byteLength;
2703
2839
  }
@@ -2775,7 +2911,7 @@ function createSnapshotStreamState(expectation) {
2775
2911
  expectation,
2776
2912
  header: null,
2777
2913
  totalBytes: null,
2778
- assembled: null,
2914
+ spool: null,
2779
2915
  accumulatedBytes: 0,
2780
2916
  expectedNextOffset: 0,
2781
2917
  endPayload: null,
@@ -2830,11 +2966,20 @@ function handleIncomingSnapshotRecord(state, record) {
2830
2966
  }
2831
2967
  fail(state, ErrorCodes.SNAPSHOT_SYNC_FRAME_INVALID, "unrecognised snapshot frame shape");
2832
2968
  }
2969
+ function abandonSnapshotStream(state) {
2970
+ fail(state, ErrorCodes.SNAPSHOT_SYNC_ABORTED, "the snapshot stream was abandoned before it finished");
2971
+ releaseSpool(state);
2972
+ }
2833
2973
  function finalizeSnapshotStream(state) {
2974
+ const result = settleSnapshotStream(state);
2975
+ if (!result.ok) releaseSpool(state);
2976
+ return result;
2977
+ }
2978
+ function settleSnapshotStream(state) {
2834
2979
  if (state.failure !== null) {
2835
2980
  return state.failure;
2836
2981
  }
2837
- if (state.header === null || state.totalBytes === null || state.assembled === null) {
2982
+ if (state.header === null || state.totalBytes === null) {
2838
2983
  return {
2839
2984
  ok: false,
2840
2985
  code: ErrorCodes.SNAPSHOT_SYNC_CHUNK_MISSING,
@@ -2866,9 +3011,28 @@ function finalizeSnapshotStream(state) {
2866
3011
  }
2867
3012
  };
2868
3013
  }
3014
+ const spool = state.spool;
3015
+ if (spool === null) {
3016
+ return {
3017
+ ok: false,
3018
+ code: ErrorCodes.SNAPSHOT_SYNC_FRAME_INVALID,
3019
+ message: "the snapshot stream was already settled",
3020
+ details: { indexName: state.expectation.indexName }
3021
+ };
3022
+ }
3023
+ const computed = spool.checksum();
3024
+ if (computed !== state.header.checksum) {
3025
+ return {
3026
+ ok: false,
3027
+ code: ErrorCodes.SNAPSHOT_SYNC_CHECKSUM_MISMATCH,
3028
+ message: "computed checksum does not match header checksum",
3029
+ details: { computed, header: state.header.checksum }
3030
+ };
3031
+ }
3032
+ state.spool = null;
2869
3033
  return {
2870
3034
  ok: true,
2871
- bytes: state.assembled,
3035
+ snapshot: spool.finish(),
2872
3036
  header: state.header,
2873
3037
  end: state.endPayload
2874
3038
  };
@@ -3079,7 +3243,11 @@ async function fetchSnapshotFromTarget(indexName, partitionId, target, deadline,
3079
3243
  requestId: generateId(),
3080
3244
  payload: encode(requestPayload)
3081
3245
  };
3082
- const streamState = createSnapshotStreamState({ indexName, partitionId: null });
3246
+ const streamState = createSnapshotStreamState({
3247
+ indexName,
3248
+ partitionId: null,
3249
+ spoolDirectory: deps.spoolDirectory ?? defaultSpoolDirectory()
3250
+ });
3083
3251
  const remainingMs = deadline - Date.now();
3084
3252
  if (remainingMs <= 0) {
3085
3253
  return {
@@ -3095,6 +3263,7 @@ async function fetchSnapshotFromTarget(indexName, partitionId, target, deadline,
3095
3263
  });
3096
3264
  await withDeadline(streamWork, remainingMs, indexName, "stream");
3097
3265
  } catch (err) {
3266
+ abandonSnapshotStream(streamState);
3098
3267
  if (err instanceof NarsilError && err.code === ErrorCodes.SNAPSHOT_SYNC_TIMEOUT) {
3099
3268
  return toStreamFailure(err, ErrorCodes.SNAPSHOT_SYNC_TIMEOUT, "snapshot stream deadline exceeded");
3100
3269
  }
@@ -3103,20 +3272,7 @@ async function fetchSnapshotFromTarget(indexName, partitionId, target, deadline,
3103
3272
  }
3104
3273
  return toStreamFailure(err, ErrorCodes.SNAPSHOT_SYNC_PRIMARY_ERROR, "snapshot fetch raised non-transport error");
3105
3274
  }
3106
- const result = finalizeSnapshotStream(streamState);
3107
- if (!result.ok) {
3108
- return result;
3109
- }
3110
- const computedChecksum = crc32(result.bytes);
3111
- if (computedChecksum !== result.header.checksum) {
3112
- return {
3113
- ok: false,
3114
- code: ErrorCodes.SNAPSHOT_SYNC_CHECKSUM_MISMATCH,
3115
- message: "computed checksum does not match header checksum",
3116
- details: { computed: computedChecksum, header: result.header.checksum }
3117
- };
3118
- }
3119
- return result;
3275
+ return finalizeSnapshotStream(streamState);
3120
3276
  }
3121
3277
  async function withDeadline(work, ms, indexName, phase) {
3122
3278
  let timer = null;
@@ -3265,9 +3421,9 @@ function surfaceAborted(deps, indexName, primaryNodeId) {
3265
3421
  })
3266
3422
  );
3267
3423
  }
3268
- async function executeEngineRestore(engine, indexName, primaryNodeId, bytes, remainingMs) {
3424
+ async function executeEngineRestore(engine, indexName, primaryNodeId, source, remainingMs) {
3269
3425
  try {
3270
- await withDeadline(engine.restore(indexName, bytes), remainingMs, indexName, "restore");
3426
+ await withDeadline(engine.restore(indexName, source), remainingMs, indexName, "restore");
3271
3427
  return null;
3272
3428
  } catch (err) {
3273
3429
  if (err instanceof NarsilError && err.code === ErrorCodes.SNAPSHOT_SYNC_TIMEOUT) {
@@ -3553,9 +3709,9 @@ function validateSyncEntryForApply(entry, indexName, partitionId, expectedSeqNo,
3553
3709
  }
3554
3710
  return null;
3555
3711
  }
3556
- function createLiveSyncFrameState(indexName, partitionId) {
3712
+ function createLiveSyncFrameState(indexName, partitionId, spoolDirectory) {
3557
3713
  return {
3558
- snapshotState: createSnapshotStreamState({ indexName, partitionId }),
3714
+ snapshotState: createSnapshotStreamState({ indexName, partitionId, spoolDirectory }),
3559
3715
  syncEntries: [],
3560
3716
  sawSnapshotFrame: false,
3561
3717
  error: null
@@ -3657,35 +3813,25 @@ async function applyLiveIncrementalSync(indexName, partitionId, target, entriesP
3657
3813
  async function applyLiveSnapshotSync(state, entry, indexName, partitionId, target, frameState, coordinatorConfig, partitionCount, deadline, deps) {
3658
3814
  const finalized = finalizeSnapshotStream(frameState.snapshotState);
3659
3815
  if (!finalized.ok) {
3660
- return { ok: false, error: streamFailureToError(finalized) };
3816
+ return { ok: false, error: streamFailureToError({ ...finalized, details: { ...finalized.details, target } }) };
3661
3817
  }
3662
- const computedChecksum = crc32(finalized.bytes);
3663
- if (computedChecksum !== finalized.header.checksum) {
3664
- return {
3665
- ok: false,
3666
- error: new NarsilError(
3667
- ErrorCodes.SNAPSHOT_SYNC_CHECKSUM_MISMATCH,
3668
- "computed checksum does not match header checksum",
3669
- {
3670
- computed: computedChecksum,
3671
- header: finalized.header.checksum,
3672
- target
3673
- }
3674
- )
3675
- };
3818
+ let restoreError;
3819
+ try {
3820
+ restoreError = await restoreLiveSnapshotPartition(
3821
+ state,
3822
+ entry,
3823
+ indexName,
3824
+ partitionId,
3825
+ target,
3826
+ finalized.snapshot.read(),
3827
+ coordinatorConfig,
3828
+ partitionCount,
3829
+ deadline,
3830
+ deps
3831
+ );
3832
+ } finally {
3833
+ await finalized.snapshot.discard();
3676
3834
  }
3677
- const restoreError = await restoreLiveSnapshotPartition(
3678
- state,
3679
- entry,
3680
- indexName,
3681
- partitionId,
3682
- target,
3683
- finalized.bytes,
3684
- coordinatorConfig,
3685
- partitionCount,
3686
- deadline,
3687
- deps
3688
- );
3689
3835
  if (restoreError !== null) {
3690
3836
  return { ok: false, error: restoreError };
3691
3837
  }
@@ -3709,7 +3855,7 @@ async function applyLiveSnapshotSync(state, entry, indexName, partitionId, targe
3709
3855
  snapshotHeader: finalized.header
3710
3856
  };
3711
3857
  }
3712
- async function restoreLiveSnapshotPartition(state, entry, indexName, partitionId, primaryNodeId, bytes, coordinatorConfig, partitionCount, deadline, deps) {
3858
+ async function restoreLiveSnapshotPartition(state, entry, indexName, partitionId, primaryNodeId, source, coordinatorConfig, partitionCount, deadline, deps) {
3713
3859
  const remainingMs = deadline - Date.now();
3714
3860
  if (remainingMs <= 0) {
3715
3861
  return new NarsilError(
@@ -3733,7 +3879,7 @@ async function restoreLiveSnapshotPartition(state, entry, indexName, partitionId
3733
3879
  }
3734
3880
  try {
3735
3881
  await withDeadline(
3736
- deps.restoreReplicationPartition(indexName, partitionId, bytes, coordinatorConfig, partitionCount),
3882
+ deps.restoreReplicationPartition(indexName, partitionId, source, coordinatorConfig, partitionCount),
3737
3883
  remainingMs,
3738
3884
  indexName,
3739
3885
  "partition-restore"
@@ -3925,6 +4071,27 @@ async function syncFromAnyTarget(state, entry, indexName, partitionId, primaryNo
3925
4071
  return { ok: false, error: lastError };
3926
4072
  }
3927
4073
  async function syncFromTarget(state, entry, indexName, partitionId, target, coordinatorConfig, partitionCount, deadline, deps, abortCheck) {
4074
+ const spoolDirectory = deps.snapshotSpoolDirectory ?? defaultSpoolDirectory();
4075
+ const frameState = createLiveSyncFrameState(indexName, partitionId, spoolDirectory);
4076
+ try {
4077
+ return await syncFrameStateFromTarget(
4078
+ state,
4079
+ entry,
4080
+ indexName,
4081
+ partitionId,
4082
+ target,
4083
+ coordinatorConfig,
4084
+ partitionCount,
4085
+ deadline,
4086
+ deps,
4087
+ abortCheck,
4088
+ frameState
4089
+ );
4090
+ } finally {
4091
+ abandonSnapshotStream(frameState.snapshotState);
4092
+ }
4093
+ }
4094
+ async function syncFrameStateFromTarget(state, entry, indexName, partitionId, target, coordinatorConfig, partitionCount, deadline, deps, abortCheck, frameState) {
3928
4095
  const logState = readLocalLogState(indexName, partitionId, deps);
3929
4096
  const requestPayload = {
3930
4097
  indexName,
@@ -3938,7 +4105,6 @@ async function syncFromTarget(state, entry, indexName, partitionId, target, coor
3938
4105
  requestId: generateId(),
3939
4106
  payload: encode(requestPayload)
3940
4107
  };
3941
- const frameState = createLiveSyncFrameState(indexName, partitionId);
3942
4108
  const remainingMs = deadline - Date.now();
3943
4109
  if (remainingMs <= 0) {
3944
4110
  return {
@@ -4170,7 +4336,7 @@ async function fetchSchemaAndPrepare(entry, indexName, primaryNodeId, deps) {
4170
4336
  }
4171
4337
  return configResult.config.schema;
4172
4338
  }
4173
- async function applyRestore(state, entry, indexName, primaryNodeId, bytes, coordinatorSchema, deadline, deps) {
4339
+ async function applyRestore(state, entry, indexName, primaryNodeId, source, coordinatorSchema, deadline, deps) {
4174
4340
  const remainingMs = deadline - Date.now();
4175
4341
  if (remainingMs <= 0) {
4176
4342
  surfaceError(
@@ -4189,7 +4355,7 @@ async function applyRestore(state, entry, indexName, primaryNodeId, bytes, coord
4189
4355
  surfaceAborted(deps, indexName, primaryNodeId);
4190
4356
  return false;
4191
4357
  }
4192
- const restoreError = await executeEngineRestore(deps.engine, indexName, primaryNodeId, bytes, remainingMs);
4358
+ const restoreError = await executeEngineRestore(deps.engine, indexName, primaryNodeId, source, remainingMs);
4193
4359
  if (restoreError !== null) {
4194
4360
  surfaceError(deps, indexName, primaryNodeId, restoreError);
4195
4361
  return false;
@@ -4310,7 +4476,11 @@ async function executeBootstrapSync(state, entry, indexName, partitionId, primar
4310
4476
  surfaceError(deps, indexName, primaryNodeId, targetsResult);
4311
4477
  return false;
4312
4478
  }
4313
- const fetchDeps = { transport: deps.transport, sourceNodeId: deps.sourceNodeId };
4479
+ const fetchDeps = {
4480
+ transport: deps.transport,
4481
+ sourceNodeId: deps.sourceNodeId,
4482
+ spoolDirectory: deps.snapshotSpoolDirectory
4483
+ };
4314
4484
  const fetchResult = await fetchSnapshotFromAnyTarget(
4315
4485
  indexName,
4316
4486
  primaryNodeId,
@@ -4337,20 +4507,25 @@ async function executeBootstrapSync(state, entry, indexName, partitionId, primar
4337
4507
  );
4338
4508
  return false;
4339
4509
  }
4340
- if (abortCheck()) {
4341
- surfaceAborted(deps, indexName, primaryNodeId);
4342
- return false;
4510
+ let restoreSucceeded;
4511
+ try {
4512
+ if (abortCheck()) {
4513
+ surfaceAborted(deps, indexName, primaryNodeId);
4514
+ return false;
4515
+ }
4516
+ restoreSucceeded = await applyRestore(
4517
+ state,
4518
+ entry,
4519
+ indexName,
4520
+ primaryNodeId,
4521
+ fetchResult.snapshot.read(),
4522
+ schemaResult,
4523
+ deadline,
4524
+ deps
4525
+ );
4526
+ } finally {
4527
+ await fetchResult.snapshot.discard();
4343
4528
  }
4344
- const restoreSucceeded = await applyRestore(
4345
- state,
4346
- entry,
4347
- indexName,
4348
- primaryNodeId,
4349
- fetchResult.bytes,
4350
- schemaResult,
4351
- deadline,
4352
- deps
4353
- );
4354
4529
  if (!restoreSucceeded) {
4355
4530
  return false;
4356
4531
  }
@@ -5214,173 +5389,40 @@ async function applyLocalReplicationEntry(core, entry) {
5214
5389
  }
5215
5390
  }
5216
5391
 
5217
- // src/distribution/cluster-node/local-engine.ts
5218
- async function createClusterLocalEngine(config, hooks) {
5219
- requireKnownConfig(config);
5220
- const runnableConfig = await resolveRunnableConfig(config);
5221
- const core = createEngineCore(runnableConfig, hooks, "partition");
5222
- await startEngineCore(core, runnableConfig);
5223
- core.orchestrator.shareMainThread();
5224
- const engine = createNarsilFromCore(core, runnableConfig);
5225
- const heldPartitions = createHeldPartitionRecord(core);
5226
- return Object.assign(engine, {
5227
- createIndex: (name, indexConfig) => createEngineIndex(core, runnableConfig, name, indexConfig, { adapterBinding: "deferred" }),
5228
- createIndexWithUuid: (name, indexConfig, indexUuid) => createEngineIndex(core, runnableConfig, name, indexConfig, { indexUuid }),
5229
- acquireIndexForReplication: (indexName) => core.indexState.acquire(indexName, false),
5230
- indexUuidOf: (indexName) => core.indexRegistry.get(indexName)?.indexUuid,
5231
- stampIndexUuid: (indexName, indexUuid) => stampIndexUuid(core, indexName, indexUuid),
5232
- highestPersistedSeqNoOf: (indexName, partitionId) => core.durability?.manager.highestPersistedSeqNo(indexName, partitionId) ?? 0,
5233
- heldPartitionsOf: (indexName) => heldPartitions.held(indexName),
5234
- recordHeldPartition: (indexName, partitionId) => heldPartitions.record(indexName, partitionId),
5235
- forgetHeldPartition: (indexName, partitionId) => heldPartitions.forget(indexName, partitionId),
5236
- applyReplicationEntry: (entry) => applyLocalReplicationEntry(core, entry),
5237
- serializeReplicationPartition: (indexName, partitionId) => serializeReplicationPartition(core, indexName, partitionId),
5238
- restoreReplicationPartition: (indexName, partitionId, bytes, indexConfig, partitionCount) => restoreReplicationPartition(core, engine, indexName, partitionId, bytes, indexConfig, partitionCount),
5239
- queryPartitions: (indexName, params, partitionIds, globalStats) => runEngineQuery(core, indexName, params, { partitionIds, globalStats }),
5240
- preflightPartitions: (indexName, params, partitionIds) => runEnginePreflight(core, indexName, params, { partitionIds }),
5241
- suggestPartitions: (indexName, params, partitionIds) => runEngineSuggest(core, indexName, params, partitionIds),
5242
- listPartitions: (indexName, params, partitionIds) => runEngineListDocuments(core, indexName, params, partitionIds),
5243
- collectQueryStats: (indexName, terms, partitionIds) => runEngineQueryStats(core, indexName, terms, partitionIds),
5244
- partitionStatsForRead: async (indexName) => {
5245
- const release = await core.indexState.acquire(indexName);
5246
- try {
5247
- return core.requireManager(indexName).getPartitionStats();
5248
- } finally {
5249
- release();
5250
- }
5251
- }
5252
- });
5253
- }
5254
- async function stampIndexUuid(core, indexName, indexUuid) {
5255
- const entry = core.indexRegistry.get(indexName);
5256
- if (entry === void 0 || entry.indexUuid === indexUuid) {
5257
- return;
5258
- }
5259
- core.indexRegistry.set(indexName, { ...entry, indexUuid });
5260
- if (core.durability !== null) {
5261
- await core.durability.manager.persistMetadata(indexName);
5262
- }
5263
- }
5264
- async function serializeReplicationPartition(core, indexName, partitionId) {
5265
- core.guardShutdown();
5266
- const release = await core.indexState.acquire(indexName, false);
5267
- try {
5268
- return core.requireManager(indexName).serializePartitionToBytes(partitionId);
5269
- } finally {
5270
- release();
5271
- }
5392
+ // src/distribution/cluster-node/replication-index.ts
5393
+ function isNestedSchema(value) {
5394
+ return typeof value === "object" && value !== null;
5272
5395
  }
5273
- async function restoreReplicationPartition(core, engine, indexName, partitionId, bytes, indexConfig, partitionCount) {
5274
- core.guardShutdown();
5275
- validatePartitionRestoreTarget(indexName, partitionId, partitionCount);
5276
- const schema = indexConfig.schema;
5277
- let partition;
5278
- try {
5279
- partition = deserializePayloadV2(bytes);
5280
- } catch (err) {
5281
- const cause = err instanceof Error ? err.message : String(err);
5282
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, `partition snapshot decode failed: ${cause}`, {
5283
- indexName,
5284
- partitionId,
5285
- cause
5286
- });
5287
- }
5288
- validatePartitionSnapshotPayload(indexName, partitionId, partitionCount, schema, partition);
5289
- const trackPositions = core.indexRegistry.get(indexName)?.config.trackPositions ?? true;
5290
- createPartitionIndex(partitionId, trackPositions).deserialize(partition, schema);
5291
- let release = core.indexRegistry.has(indexName) ? await core.indexState.acquire(indexName, false) : null;
5292
- try {
5293
- const restoreIndex = await ensureReplicationIndex(
5294
- core,
5295
- engine,
5296
- indexName,
5297
- indexConfig,
5298
- partition.language,
5299
- partitionCount
5300
- );
5301
- if (release === null) release = await core.indexState.acquire(indexName, false);
5302
- if (restoreIndex.created) {
5303
- await core.orchestrator.replicateToWorkers({
5304
- type: "createIndex",
5305
- indexName,
5306
- config: restoreIndex.config,
5307
- requestId: `replicate-partition-index-${indexName}`
5308
- });
5396
+ function flattenSchemaInto(schema, prefix, flat) {
5397
+ for (const [field, value] of Object.entries(schema)) {
5398
+ const path = prefix.length === 0 ? field : `${prefix}.${field}`;
5399
+ if (isNestedSchema(value)) {
5400
+ flattenSchemaInto(value, path, flat);
5401
+ continue;
5309
5402
  }
5310
- const manager = core.requireManager(indexName);
5311
- manager.deserializePartition(partitionId, partition);
5312
- await core.orchestrator.replicateToWorkers({
5313
- type: "deserialize",
5314
- indexName,
5315
- partitionId,
5316
- data: partition,
5317
- requestId: `replicate-partition-restore-${indexName}-${partitionId}`
5318
- });
5319
- } finally {
5320
- release?.();
5403
+ flat[path] = value;
5321
5404
  }
5322
5405
  }
5323
- function validatePartitionRestoreTarget(indexName, partitionId, partitionCount) {
5324
- if (!Number.isInteger(partitionId) || partitionId < 0) {
5325
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partitionId must be a non-negative integer", {
5326
- indexName,
5327
- partitionId
5328
- });
5329
- }
5330
- if (!Number.isInteger(partitionCount) || partitionCount <= 0 || partitionCount > MAX_PARTITION_COUNT) {
5331
- throw new NarsilError(
5332
- ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED,
5333
- `partitionCount must be an integer between 1 and ${MAX_PARTITION_COUNT}`,
5334
- { indexName, partitionCount }
5335
- );
5336
- }
5337
- if (partitionId >= partitionCount) {
5338
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partitionId is outside partitionCount", {
5339
- indexName,
5340
- partitionId,
5341
- partitionCount
5342
- });
5343
- }
5406
+ function flattenSchema(schema) {
5407
+ const flat = {};
5408
+ flattenSchemaInto(schema, "", flat);
5409
+ return flat;
5344
5410
  }
5345
- function validatePartitionSnapshotPayload(indexName, partitionId, partitionCount, schema, partition) {
5346
- if (partition.indexName !== indexName) {
5347
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partition snapshot indexName mismatch", {
5348
- expectedIndexName: indexName,
5349
- receivedIndexName: partition.indexName
5350
- });
5351
- }
5352
- if (partition.partitionId !== partitionId) {
5353
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partition snapshot partitionId mismatch", {
5354
- indexName,
5355
- expectedPartitionId: partitionId,
5356
- receivedPartitionId: partition.partitionId
5357
- });
5358
- }
5359
- if (partition.totalPartitions !== partitionCount) {
5360
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partition snapshot partition count mismatch", {
5361
- indexName,
5362
- partitionId,
5363
- expectedPartitionCount: partitionCount,
5364
- receivedPartitionCount: partition.totalPartitions
5365
- });
5366
- }
5367
- const expectedSchema = flattenSchema(schema);
5368
- for (const [field, expectedType] of Object.entries(expectedSchema)) {
5369
- if (partition.schema[field] !== expectedType) {
5370
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partition snapshot schema mismatch", {
5411
+ function requireSameFlatSchema(indexName, expected, actual, what) {
5412
+ for (const [field, expectedType] of Object.entries(expected)) {
5413
+ if (actual[field] !== expectedType) {
5414
+ throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, `${what} schema mismatch`, {
5371
5415
  indexName,
5372
- partitionId,
5373
5416
  field,
5374
5417
  expected: expectedType,
5375
- received: partition.schema[field] ?? "(absent)"
5418
+ received: actual[field] ?? "(absent)"
5376
5419
  });
5377
5420
  }
5378
5421
  }
5379
- for (const field of Object.keys(partition.schema)) {
5380
- if (!(field in expectedSchema)) {
5381
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partition snapshot schema contains extra field", {
5422
+ for (const field of Object.keys(actual)) {
5423
+ if (!(field in expected)) {
5424
+ throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, `${what} schema contains extra field`, {
5382
5425
  indexName,
5383
- partitionId,
5384
5426
  field
5385
5427
  });
5386
5428
  }
@@ -5412,7 +5454,12 @@ async function ensureReplicationIndex(core, engine, indexName, coordinatorConfig
5412
5454
  receivedLanguage: language.name
5413
5455
  });
5414
5456
  }
5415
- validateExistingSchema(indexName, coordinatorConfig.schema, entry.config.schema);
5457
+ requireSameFlatSchema(
5458
+ indexName,
5459
+ flattenSchema(coordinatorConfig.schema),
5460
+ flattenSchema(entry.config.schema),
5461
+ "local index"
5462
+ );
5416
5463
  const manager = core.requireManager(indexName);
5417
5464
  if (manager.partitionCount > partitionCount) {
5418
5465
  throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "local index has more partitions than allocation", {
@@ -5431,45 +5478,222 @@ async function ensureReplicationIndex(core, engine, indexName, coordinatorConfig
5431
5478
  }
5432
5479
  return { config: entry.config, created };
5433
5480
  }
5434
- function validateExistingSchema(indexName, expected, actual) {
5435
- const expectedSchema = flattenSchema(expected);
5436
- const actualSchema = flattenSchema(actual);
5437
- for (const [field, expectedType] of Object.entries(expectedSchema)) {
5438
- if (actualSchema[field] !== expectedType) {
5439
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "local index schema mismatch", {
5440
- indexName,
5441
- field,
5442
- expected: expectedType,
5443
- received: actualSchema[field] ?? "(absent)"
5481
+
5482
+ // src/distribution/cluster-node/partition-archive.ts
5483
+ var VECTOR_FILE_ENVELOPE_VERSION = 2;
5484
+ async function archiveReplicationPartition(core, indexName, partitionId, lastSeqNo) {
5485
+ core.guardShutdown();
5486
+ const release = await core.indexState.acquire(indexName, false);
5487
+ try {
5488
+ const entry = core.requireIndex(indexName);
5489
+ const manager = core.requireManager(indexName);
5490
+ let captured = 0;
5491
+ const checkpoint = await buildArchiveFromMemory({
5492
+ indexName,
5493
+ metadata: indexMetadataOf(indexName, entry, manager.partitionCount),
5494
+ manager,
5495
+ vectorIndexes: manager.getVectorIndexes(),
5496
+ appliedSeqNo: () => {
5497
+ captured = lastSeqNo();
5498
+ return captured;
5499
+ },
5500
+ partitionId
5501
+ });
5502
+ const config = await encodeIndexConfiguration(entry);
5503
+ return { stream: archiveStream({ ...checkpoint, config, sourceIndexName: indexName }), lastSeqNo: captured };
5504
+ } finally {
5505
+ release();
5506
+ }
5507
+ }
5508
+ function restoreFailed(message, details) {
5509
+ throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, message, details);
5510
+ }
5511
+ function requireRestoreTarget(indexName, partitionId, partitionCount) {
5512
+ if (!Number.isInteger(partitionId) || partitionId < 0) {
5513
+ restoreFailed("partitionId must be a non-negative integer", { indexName, partitionId });
5514
+ }
5515
+ if (!Number.isInteger(partitionCount) || partitionCount <= 0 || partitionCount > MAX_PARTITION_COUNT) {
5516
+ restoreFailed(`partitionCount must be an integer between 1 and ${MAX_PARTITION_COUNT}`, {
5517
+ indexName,
5518
+ partitionCount
5519
+ });
5520
+ }
5521
+ if (partitionId >= partitionCount) {
5522
+ restoreFailed("partitionId is outside partitionCount", { indexName, partitionId, partitionCount });
5523
+ }
5524
+ }
5525
+ async function receiveArchive(indexName, partitionId, source, schema) {
5526
+ const store = memoryCheckpointStore();
5527
+ let language = "";
5528
+ let received = null;
5529
+ await readSnapshotArchive(createByteReader(source), {
5530
+ config: async (bytes) => {
5531
+ const { payload } = await openChecksummedEnvelope(bytes, {
5532
+ envelopeFormatVersion: 5,
5533
+ what: "index configuration",
5534
+ allowsCompression: true
5535
+ });
5536
+ const raw = decode(payload);
5537
+ if (typeof raw.language !== "string") restoreFailed("partition archive names no language", { indexName });
5538
+ language = raw.language;
5539
+ },
5540
+ manifest: async (manifest, sourceName) => {
5541
+ const listed = manifest.partitions.map((partition) => partition.partitionId);
5542
+ if (listed.length !== 1 || listed[0] !== partitionId) {
5543
+ restoreFailed("partition archive lists other partitions", { indexName, partitionId, listed });
5544
+ }
5545
+ if (manifest.vectors.some((field) => field.graphParts.length > 0)) {
5546
+ restoreFailed("partition archive holds a graph", { indexName, partitionId });
5547
+ }
5548
+ requireSameFlatSchema(indexName, schema, manifest.schema, "partition snapshot");
5549
+ const rename = (key) => `${indexName}/${key.slice(sourceName.length + 1)}`;
5550
+ received = {
5551
+ ...manifest,
5552
+ partitions: manifest.partitions.map((partition) => ({
5553
+ ...partition,
5554
+ segments: partition.segments.map((segment) => ({ ...segment, key: rename(segment.key) }))
5555
+ })),
5556
+ vectors: manifest.vectors.map((field) => ({
5557
+ ...field,
5558
+ files: field.files.map((file) => ({ ...file, key: rename(file.key) }))
5559
+ }))
5560
+ };
5561
+ },
5562
+ file: async (name, _ref, bytes) => store.write(`${indexName}/${name}`, [bytes])
5563
+ });
5564
+ if (received === null) restoreFailed("partition archive holds no manifest", { indexName, partitionId });
5565
+ return { language, manifest: received, store };
5566
+ }
5567
+ function emptyPartition(manager, partitionId) {
5568
+ return {
5569
+ indexName: manager.indexName,
5570
+ partitionId,
5571
+ totalPartitions: manager.partitionCount,
5572
+ language: manager.language.name,
5573
+ schema: {},
5574
+ docCount: 0,
5575
+ avgDocLength: 0,
5576
+ documents: {},
5577
+ invertedIndex: {},
5578
+ fieldIndexes: { numeric: {}, boolean: {}, enum: {}, geopoint: {} },
5579
+ statistics: { totalDocuments: 0, totalFieldLengths: {}, averageFieldLengths: {}, docFrequencies: {} }
5580
+ };
5581
+ }
5582
+ async function replacePartition(manager, partitionId, archive) {
5583
+ const vectorIndexes = manager.getVectorIndexes();
5584
+ const held = [...manager.getPartition(partitionId).docIds()];
5585
+ for (const vectorIndex of vectorIndexes.values()) {
5586
+ for (const docId of held) if (vectorIndex.has(docId)) vectorIndex.remove(docId);
5587
+ }
5588
+ manager.deserializePartition(partitionId, emptyPartition(manager, partitionId));
5589
+ const entry = archive.manifest.partitions[0];
5590
+ if (entry !== void 0) {
5591
+ await loadPartitionSegments(archive.store, entry, manager, {
5592
+ schema: archive.manifest.schema,
5593
+ decodesEveryDocument: true
5594
+ });
5595
+ }
5596
+ for (const field of archive.manifest.vectors) {
5597
+ const vectorIndex = vectorIndexes.get(field.fieldPath);
5598
+ if (vectorIndex === void 0) continue;
5599
+ for (const file of field.files) {
5600
+ const bytes = await readListedFile(archive.store, file.key, { bytes: file.bytes, what: "vector file" });
5601
+ const { payload } = await openChecksummedEnvelope(bytes, {
5602
+ envelopeFormatVersion: VECTOR_FILE_ENVELOPE_VERSION,
5603
+ what: `vector file "${file.key}"`,
5604
+ allowsCompression: false
5444
5605
  });
5606
+ const decoded = decodeVectorFilePayload(decode(payload));
5607
+ const vectors = bytesToVectors(decoded.vectors);
5608
+ for (let i = 0; i < decoded.docIds.length; i++) {
5609
+ if (isDead(file.dead, i)) continue;
5610
+ const vector = vectors.slice(i * decoded.dimension, (i + 1) * decoded.dimension);
5611
+ vectorIndex.insert(decoded.docIds[i], vector, partitionId);
5612
+ }
5445
5613
  }
5614
+ vectorIndex.scheduleBuild();
5446
5615
  }
5447
- for (const field of Object.keys(actualSchema)) {
5448
- if (!(field in expectedSchema)) {
5449
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "local index schema contains extra field", {
5616
+ }
5617
+ async function restoreReplicationPartition(core, engine, indexName, partitionId, source, indexConfig, partitionCount) {
5618
+ core.guardShutdown();
5619
+ requireRestoreTarget(indexName, partitionId, partitionCount);
5620
+ const archive = await receiveArchive(indexName, partitionId, source, flattenSchema(indexConfig.schema));
5621
+ let release = core.indexRegistry.has(indexName) ? await core.indexState.acquire(indexName, false) : null;
5622
+ try {
5623
+ const restoreIndex = await ensureReplicationIndex(
5624
+ core,
5625
+ engine,
5626
+ indexName,
5627
+ indexConfig,
5628
+ archive.language,
5629
+ partitionCount
5630
+ );
5631
+ if (release === null) release = await core.indexState.acquire(indexName, false);
5632
+ if (restoreIndex.created) {
5633
+ await core.orchestrator.replicateToWorkers({
5634
+ type: "createIndex",
5450
5635
  indexName,
5451
- field
5636
+ config: restoreIndex.config,
5637
+ requestId: `replicate-partition-index-${indexName}`
5452
5638
  });
5453
5639
  }
5640
+ const wasScaledOut = core.orchestrator.desyncIndex(indexName);
5641
+ try {
5642
+ await replacePartition(core.requireManager(indexName), partitionId, archive);
5643
+ } finally {
5644
+ await core.orchestrator.resyncIndex(indexName, wasScaledOut);
5645
+ }
5646
+ } finally {
5647
+ release?.();
5454
5648
  }
5455
5649
  }
5456
- function flattenSchema(schema) {
5457
- const flat = {};
5458
- flattenSchemaInto(schema, "", flat);
5459
- return flat;
5460
- }
5461
- function flattenSchemaInto(schema, prefix, flat) {
5462
- for (const [field, value] of Object.entries(schema)) {
5463
- const path = prefix.length === 0 ? field : `${prefix}.${field}`;
5464
- if (isNestedSchema(value)) {
5465
- flattenSchemaInto(value, path, flat);
5466
- continue;
5650
+
5651
+ // src/distribution/cluster-node/local-engine.ts
5652
+ async function createClusterLocalEngine(config, hooks) {
5653
+ requireKnownConfig(config);
5654
+ const runnableConfig = await resolveRunnableConfig(config);
5655
+ const core = createEngineCore(runnableConfig, hooks, "partition");
5656
+ await startEngineCore(core, runnableConfig);
5657
+ core.orchestrator.shareMainThread();
5658
+ const engine = createNarsilFromCore(core, runnableConfig);
5659
+ const heldPartitions = createHeldPartitionRecord(core);
5660
+ return Object.assign(engine, {
5661
+ createIndex: (name, indexConfig) => createEngineIndex(core, runnableConfig, name, indexConfig, { adapterBinding: "deferred" }),
5662
+ createIndexWithUuid: (name, indexConfig, indexUuid) => createEngineIndex(core, runnableConfig, name, indexConfig, { indexUuid }),
5663
+ acquireIndexForReplication: (indexName) => core.indexState.acquire(indexName, false),
5664
+ indexUuidOf: (indexName) => core.indexRegistry.get(indexName)?.indexUuid,
5665
+ stampIndexUuid: (indexName, indexUuid) => stampIndexUuid(core, indexName, indexUuid),
5666
+ highestPersistedSeqNoOf: (indexName, partitionId) => core.durability?.manager.highestPersistedSeqNo(indexName, partitionId) ?? 0,
5667
+ heldPartitionsOf: (indexName) => heldPartitions.held(indexName),
5668
+ recordHeldPartition: (indexName, partitionId) => heldPartitions.record(indexName, partitionId),
5669
+ forgetHeldPartition: (indexName, partitionId) => heldPartitions.forget(indexName, partitionId),
5670
+ applyReplicationEntry: (entry) => applyLocalReplicationEntry(core, entry),
5671
+ archiveReplicationPartition: (indexName, partitionId, lastSeqNo) => archiveReplicationPartition(core, indexName, partitionId, lastSeqNo),
5672
+ restoreReplicationPartition: (indexName, partitionId, source, indexConfig, partitionCount) => restoreReplicationPartition(core, engine, indexName, partitionId, source, indexConfig, partitionCount),
5673
+ queryPartitions: (indexName, params, partitionIds, globalStats) => runEngineQuery(core, indexName, params, { partitionIds, globalStats }),
5674
+ preflightPartitions: (indexName, params, partitionIds) => runEnginePreflight(core, indexName, params, { partitionIds }),
5675
+ suggestPartitions: (indexName, params, partitionIds) => runEngineSuggest(core, indexName, params, partitionIds),
5676
+ listPartitions: (indexName, params, partitionIds) => runEngineListDocuments(core, indexName, params, partitionIds),
5677
+ collectQueryStats: (indexName, terms, partitionIds) => runEngineQueryStats(core, indexName, terms, partitionIds),
5678
+ partitionStatsForRead: async (indexName) => {
5679
+ const release = await core.indexState.acquire(indexName);
5680
+ try {
5681
+ return core.requireManager(indexName).getPartitionStats();
5682
+ } finally {
5683
+ release();
5684
+ }
5467
5685
  }
5468
- flat[path] = value;
5469
- }
5686
+ });
5470
5687
  }
5471
- function isNestedSchema(value) {
5472
- return typeof value === "object" && value !== null;
5688
+ async function stampIndexUuid(core, indexName, indexUuid) {
5689
+ const entry = core.indexRegistry.get(indexName);
5690
+ if (entry === void 0 || entry.indexUuid === indexUuid) {
5691
+ return;
5692
+ }
5693
+ core.indexRegistry.set(indexName, { ...entry, indexUuid });
5694
+ if (core.durability !== null) {
5695
+ await core.durability.manager.persistMetadata(indexName);
5696
+ }
5473
5697
  }
5474
5698
 
5475
5699
  // src/distribution/cluster-node/local-index-reconcile.ts
@@ -5582,6 +5806,25 @@ async function authorizeSnapshotRequest(coordinator, indexName, sourceId) {
5582
5806
  }
5583
5807
 
5584
5808
  // src/distribution/cluster-node/snapshot-cache.ts
5809
+ function soleSnapshotLease(build) {
5810
+ return { build, release: () => build.discard() };
5811
+ }
5812
+ function sharedLease(entry, build) {
5813
+ let released = false;
5814
+ return {
5815
+ build,
5816
+ async release() {
5817
+ if (released) return;
5818
+ released = true;
5819
+ entry.holders -= 1;
5820
+ if (entry.holders === 0) await build.discard();
5821
+ }
5822
+ };
5823
+ }
5824
+ async function joinSnapshotBuild(entry) {
5825
+ entry.holders += 1;
5826
+ return sharedLease(entry, await entry.promise);
5827
+ }
5585
5828
  function createSnapshotCacheState(maxConcurrent = DEFAULT_MAX_CONCURRENT_SNAPSHOTS, maxPerSource = DEFAULT_MAX_PER_SOURCE_SNAPSHOTS, maxStreamsPerIndex = DEFAULT_MAX_STREAMS_PER_INDEX) {
5586
5829
  if (!Number.isInteger(maxConcurrent) || maxConcurrent <= 0) {
5587
5830
  throw new NarsilError(ErrorCodes.CONFIG_INVALID, "maxConcurrent must be a positive integer", {
@@ -5669,7 +5912,7 @@ function releaseStreamSlot(state, handle) {
5669
5912
  async function acquireSnapshotBuild(state, indexName, builder) {
5670
5913
  const existing = state.cache.get(indexName);
5671
5914
  if (existing !== void 0) {
5672
- return existing.promise;
5915
+ return joinSnapshotBuild(existing);
5673
5916
  }
5674
5917
  if (state.active >= state.maxConcurrent) {
5675
5918
  throw new NarsilError(
@@ -5692,7 +5935,7 @@ async function acquireSnapshotBuild(state, indexName, builder) {
5692
5935
  state.active = Math.max(0, state.active - 1);
5693
5936
  };
5694
5937
  const rawBuild = (async () => builder())();
5695
- const entry = { promise: rawBuild };
5938
+ const entry = { promise: rawBuild, holders: 0 };
5696
5939
  entry.promise = rawBuild.then(
5697
5940
  (result) => {
5698
5941
  clearEntry(entry);
@@ -5704,7 +5947,7 @@ async function acquireSnapshotBuild(state, indexName, builder) {
5704
5947
  }
5705
5948
  );
5706
5949
  state.cache.set(indexName, entry);
5707
- return entry.promise;
5950
+ return joinSnapshotBuild(entry);
5708
5951
  }
5709
5952
  var SNAPSHOT_SYNC_ERROR_TYPE = `${ReplicationMessageTypes.SNAPSHOT_SYNC_REQUEST}.error`;
5710
5953
  function createSingleResponseSink(respond) {
@@ -5718,8 +5961,7 @@ function createSingleResponseSink(respond) {
5718
5961
  return sink;
5719
5962
  }
5720
5963
  async function streamSnapshotToReplica(sink, nodeId, requestId, indexName, build, metadata, options = {}) {
5721
- const snapshotBytes = build.bytes;
5722
- const totalBytes = snapshotBytes.byteLength;
5964
+ const totalBytes = build.totalBytes;
5723
5965
  const checksum = build.checksum;
5724
5966
  const header = {
5725
5967
  lastSeqNo: metadata.lastSeqNo,
@@ -5735,22 +5977,23 @@ async function streamSnapshotToReplica(sink, nodeId, requestId, indexName, build
5735
5977
  await yieldToEventLoop();
5736
5978
  let offset = 0;
5737
5979
  let lastYieldAt = now2();
5738
- while (offset < totalBytes) {
5739
- const end = Math.min(offset + SNAPSHOT_CHUNK_SIZE, totalBytes);
5740
- const chunk = snapshotBytes.subarray(offset, end);
5741
- const chunkPayload = {
5742
- partitionId: metadata.partitionId,
5743
- indexName,
5744
- offset,
5745
- data: chunk
5746
- };
5747
- const chunkBytes = encode(chunkPayload);
5748
- assertMessageWithinLimit(chunkBytes, "SNAPSHOT_CHUNK");
5749
- await respondMessage(sink, ReplicationMessageTypes.SNAPSHOT_CHUNK, nodeId, requestId, chunkBytes);
5750
- offset = end;
5751
- if (offset < totalBytes && now2() - lastYieldAt >= SNAPSHOT_CHUNK_YIELD_INTERVAL_MS) {
5752
- await yieldToEventLoop();
5753
- lastYieldAt = now2();
5980
+ for await (const source of build.read()) {
5981
+ for (let from = 0; from < source.byteLength; from += SNAPSHOT_CHUNK_SIZE) {
5982
+ const chunk = source.subarray(from, Math.min(from + SNAPSHOT_CHUNK_SIZE, source.byteLength));
5983
+ const chunkPayload = {
5984
+ partitionId: metadata.partitionId,
5985
+ indexName,
5986
+ offset,
5987
+ data: chunk
5988
+ };
5989
+ const chunkBytes = encode(chunkPayload);
5990
+ assertMessageWithinLimit(chunkBytes, "SNAPSHOT_CHUNK");
5991
+ await respondMessage(sink, ReplicationMessageTypes.SNAPSHOT_CHUNK, nodeId, requestId, chunkBytes);
5992
+ offset += chunk.byteLength;
5993
+ if (offset < totalBytes && now2() - lastYieldAt >= SNAPSHOT_CHUNK_YIELD_INTERVAL_MS) {
5994
+ await yieldToEventLoop();
5995
+ lastYieldAt = now2();
5996
+ }
5754
5997
  }
5755
5998
  }
5756
5999
  await yieldToEventLoop();
@@ -5831,6 +6074,26 @@ async function defaultSnapshotHeaderMetadataProvider(coordinator, indexName) {
5831
6074
  lastSeqNo: SNAPSHOT_HEADER_SENTINEL_SEQNO
5832
6075
  };
5833
6076
  }
6077
+ async function nodeSnapshotHeaderMetadata(deps, indexName, partitionId) {
6078
+ const fallback = await defaultSnapshotHeaderMetadataProvider(deps.coordinator, indexName);
6079
+ if (partitionId === null) {
6080
+ return fallback;
6081
+ }
6082
+ const localIndex = deps.engine.listIndexes().find((index) => index.name === indexName);
6083
+ const release = localIndex !== void 0 ? await deps.engine.acquireIndexForReplication(indexName) : null;
6084
+ try {
6085
+ const allocation = await deps.coordinator.getAllocation(indexName);
6086
+ const assignment = allocation?.assignments.get(partitionId);
6087
+ const log = deps.getReplicationLog(indexName, partitionId);
6088
+ return {
6089
+ partitionId,
6090
+ primaryTerm: assignment?.primaryTerm ?? fallback.primaryTerm,
6091
+ lastSeqNo: log.newestSeqNo ?? 0
6092
+ };
6093
+ } finally {
6094
+ release?.();
6095
+ }
6096
+ }
5834
6097
  async function resolveHeaderMetadata(deps, indexName, partitionId) {
5835
6098
  const provider = deps.resolveHeaderMetadata;
5836
6099
  if (provider === void 0) {
@@ -5860,55 +6123,56 @@ async function acquireAndStream(message, sink, request, deps, options) {
5860
6123
  return;
5861
6124
  }
5862
6125
  try {
5863
- let build;
6126
+ let lease;
5864
6127
  try {
5865
6128
  const buildForRequest = async () => buildSnapshotForRequest(deps, indexName, options);
5866
- build = options.disableBuildCache === true ? await buildForRequest() : await acquireSnapshotBuild(deps.state, indexName, buildForRequest);
6129
+ lease = options.disableBuildCache === true ? soleSnapshotLease(await buildForRequest()) : await acquireSnapshotBuild(deps.state, indexName, buildForRequest);
5867
6130
  } catch (err) {
5868
6131
  const code = err instanceof NarsilError ? err.code : ErrorCodes.SNAPSHOT_SYNC_SNAPSHOT_FAILED;
5869
6132
  const errMessage = err instanceof Error ? err.message : String(err);
5870
6133
  await respondError(sink, deps.nodeId, message.requestId, code, errMessage);
5871
6134
  return;
5872
6135
  }
5873
- if (build.bytes.byteLength > MAX_SNAPSHOT_SIZE_BYTES) {
5874
- await respondError(
5875
- sink,
5876
- deps.nodeId,
5877
- message.requestId,
5878
- ErrorCodes.SNAPSHOT_SYNC_TOO_LARGE,
5879
- `Snapshot size ${build.bytes.byteLength} exceeds the ${MAX_SNAPSHOT_SIZE_BYTES} byte limit`
5880
- );
5881
- return;
5882
- }
5883
- let streamHandle;
5884
6136
  try {
5885
- streamHandle = acquireStreamSlot(deps.state, indexName);
5886
- } catch (err) {
5887
- const code = err instanceof NarsilError ? err.code : ErrorCodes.SNAPSHOT_SYNC_CAPACITY_EXHAUSTED;
5888
- const errMessage = err instanceof Error ? err.message : String(err);
5889
- await respondError(sink, deps.nodeId, message.requestId, code, errMessage);
5890
- return;
5891
- }
5892
- try {
5893
- const metadata = options.metadata ?? await resolveHeaderMetadata(deps, indexName, request.partitionId ?? null);
5894
- await streamSnapshotToReplica(sink, deps.nodeId, message.requestId, indexName, build, metadata, {
5895
- closeOnEnd: options.closeOnEnd
5896
- });
5897
- await options.afterSnapshot?.(sink);
5898
- if (options.closeOnEnd === false) {
5899
- sink.closed = true;
5900
- }
6137
+ await streamLeasedSnapshot(message, sink, request, deps, options, lease.build);
5901
6138
  } finally {
5902
- releaseStreamSlot(deps.state, streamHandle);
6139
+ await lease.release();
5903
6140
  }
5904
6141
  } finally {
5905
6142
  releaseSourceSlot(deps.state, sourceHandle);
5906
6143
  }
5907
6144
  }
6145
+ async function streamLeasedSnapshot(message, sink, request, deps, options, build) {
6146
+ const { indexName } = request;
6147
+ let streamHandle;
6148
+ try {
6149
+ streamHandle = acquireStreamSlot(deps.state, indexName);
6150
+ } catch (err) {
6151
+ const code = err instanceof NarsilError ? err.code : ErrorCodes.SNAPSHOT_SYNC_CAPACITY_EXHAUSTED;
6152
+ const errMessage = err instanceof Error ? err.message : String(err);
6153
+ await respondError(sink, deps.nodeId, message.requestId, code, errMessage);
6154
+ return;
6155
+ }
6156
+ try {
6157
+ const resolved = options.metadata ?? await resolveHeaderMetadata(deps, indexName, request.partitionId ?? null);
6158
+ const metadata = build.lastSeqNo === void 0 ? resolved : { ...resolved, lastSeqNo: build.lastSeqNo };
6159
+ await streamSnapshotToReplica(sink, deps.nodeId, message.requestId, indexName, build, metadata, {
6160
+ closeOnEnd: options.closeOnEnd
6161
+ });
6162
+ await options.afterSnapshot?.(sink, build);
6163
+ if (options.closeOnEnd === false) {
6164
+ sink.closed = true;
6165
+ }
6166
+ } finally {
6167
+ releaseStreamSlot(deps.state, streamHandle);
6168
+ }
6169
+ }
5908
6170
  async function buildSnapshot(deps, indexName) {
5909
- let bytes;
5910
6171
  try {
5911
- bytes = await deps.engine.snapshot(indexName);
6172
+ return await spoolSnapshotStream(
6173
+ deps.spoolDirectory ?? defaultSpoolDirectory(),
6174
+ await deps.engine.snapshot(indexName)
6175
+ );
5912
6176
  } catch (err) {
5913
6177
  if (err instanceof NarsilError) {
5914
6178
  throw err;
@@ -5919,8 +6183,6 @@ async function buildSnapshot(deps, indexName) {
5919
6183
  cause: errMessage
5920
6184
  });
5921
6185
  }
5922
- const checksum = bytes.byteLength <= MAX_SNAPSHOT_SIZE_BYTES ? crc32(bytes) : 0;
5923
- return { bytes, checksum };
5924
6186
  }
5925
6187
  function buildSnapshotForRequest(deps, indexName, options) {
5926
6188
  if (options.buildSnapshot !== void 0) {
@@ -6104,7 +6366,6 @@ var MAX_FACETS = 64;
6104
6366
  var MAX_FACET_RANGES = 1e3;
6105
6367
  var MAX_FETCH_DOCUMENT_IDS = 1e4;
6106
6368
  var MAX_FILTER_FIELDS = 256;
6107
- var MAX_FILTER_ARRAY_SIZE = 65536;
6108
6369
  var MAX_FILTER_STRING_LENGTH = 1024;
6109
6370
  var MAX_LIMIT = 1e4;
6110
6371
  var MAX_OFFSET = 1e4;
@@ -6130,7 +6391,6 @@ var MAX_FACET_VALUE_LENGTH = 1024;
6130
6391
  var MAX_GROUPS_PER_RESPONSE = 65536;
6131
6392
  var MAX_FACET_SIZE = 1e3;
6132
6393
  var DEFAULT_MAX_FACET_BUCKETS = 100;
6133
- var MAX_VECTOR_DIMENSION = 8192;
6134
6394
  var MAX_VECTOR_TEXT_LENGTH = 16384;
6135
6395
 
6136
6396
  // src/distribution/query/validators/common.ts
@@ -6337,18 +6597,16 @@ function validateFetchResultPayload(decoded) {
6337
6597
 
6338
6598
  // src/distribution/query/validators/filters.ts
6339
6599
  var GEO_UNITS = ["km", "mi", "m"];
6600
+ function throwTooLong(value, fieldLabel, errorCode) {
6601
+ throwInvalid(
6602
+ errorCode,
6603
+ `Invalid payload: "${fieldLabel}" exceeds the maximum length of ${MAX_FILTER_STRING_LENGTH} code points`,
6604
+ { length: [...value].length, limit: MAX_FILTER_STRING_LENGTH }
6605
+ );
6606
+ }
6340
6607
  function validateScalarPrimitive(value, fieldLabel, errorCode) {
6341
6608
  if (typeof value === "string") {
6342
- if (value.length > MAX_FILTER_STRING_LENGTH) {
6343
- throwInvalid(
6344
- errorCode,
6345
- `Invalid payload: "${fieldLabel}" exceeds maximum length of ${MAX_FILTER_STRING_LENGTH}`,
6346
- {
6347
- length: value.length,
6348
- limit: MAX_FILTER_STRING_LENGTH
6349
- }
6350
- );
6351
- }
6609
+ if (exceedsCodePoints(value, MAX_FILTER_STRING_LENGTH)) throwTooLong(value, fieldLabel, errorCode);
6352
6610
  return value;
6353
6611
  }
6354
6612
  if (typeof value === "number") {
@@ -6366,12 +6624,7 @@ function validateBoundedString(value, fieldLabel, errorCode) {
6366
6624
  if (typeof value !== "string") {
6367
6625
  throwInvalid(errorCode, `Invalid payload: "${fieldLabel}" must be a string`);
6368
6626
  }
6369
- if (value.length > MAX_FILTER_STRING_LENGTH) {
6370
- throwInvalid(errorCode, `Invalid payload: "${fieldLabel}" exceeds maximum length of ${MAX_FILTER_STRING_LENGTH}`, {
6371
- length: value.length,
6372
- limit: MAX_FILTER_STRING_LENGTH
6373
- });
6374
- }
6627
+ if (exceedsCodePoints(value, MAX_FILTER_STRING_LENGTH)) throwTooLong(value, fieldLabel, errorCode);
6375
6628
  return value;
6376
6629
  }
6377
6630
  function validateNumericOperand(value, fieldLabel, errorCode) {
@@ -6380,6 +6633,20 @@ function validateNumericOperand(value, fieldLabel, errorCode) {
6380
6633
  }
6381
6634
  return value;
6382
6635
  }
6636
+ function validateCoordinates(lat, lon, fieldLabel, errorCode) {
6637
+ if (!isLatitude(validateNumericOperand(lat, `${fieldLabel}.lat`, errorCode))) {
6638
+ throwInvalid(
6639
+ errorCode,
6640
+ `Invalid payload: "${fieldLabel}.lat" must be from -${MAX_LATITUDE_DEGREES} to ${MAX_LATITUDE_DEGREES}`
6641
+ );
6642
+ }
6643
+ if (!isLongitude(validateNumericOperand(lon, `${fieldLabel}.lon`, errorCode))) {
6644
+ throwInvalid(
6645
+ errorCode,
6646
+ `Invalid payload: "${fieldLabel}.lon" must be from -${MAX_LONGITUDE_DEGREES} to ${MAX_LONGITUDE_DEGREES}`
6647
+ );
6648
+ }
6649
+ }
6383
6650
  function validateBooleanOperand(value, fieldLabel, errorCode) {
6384
6651
  if (typeof value !== "boolean") {
6385
6652
  throwInvalid(errorCode, `Invalid payload: "${fieldLabel}" must be a boolean`);
@@ -6440,8 +6707,7 @@ function validateGeoRadius(value, fieldLabel, errorCode) {
6440
6707
  if (!isRecord2(value)) {
6441
6708
  throwInvalid(errorCode, `Invalid payload: "${fieldLabel}" must be an object`);
6442
6709
  }
6443
- validateNumericOperand(value.lat, `${fieldLabel}.lat`, errorCode);
6444
- validateNumericOperand(value.lon, `${fieldLabel}.lon`, errorCode);
6710
+ validateCoordinates(value.lat, value.lon, fieldLabel, errorCode);
6445
6711
  validateNumericOperand(value.distance, `${fieldLabel}.distance`, errorCode);
6446
6712
  if (value.distance < 0) {
6447
6713
  throwInvalid(errorCode, `Invalid payload: "${fieldLabel}.distance" must be non-negative`);
@@ -6483,8 +6749,7 @@ function validateGeoPolygon(value, fieldLabel, errorCode) {
6483
6749
  if (!isRecord2(point)) {
6484
6750
  throwInvalid(errorCode, `Invalid payload: "${fieldLabel}.points[${i}]" must be an object`);
6485
6751
  }
6486
- validateNumericOperand(point.lat, `${fieldLabel}.points[${i}].lat`, errorCode);
6487
- validateNumericOperand(point.lon, `${fieldLabel}.points[${i}].lon`, errorCode);
6752
+ validateCoordinates(point.lat, point.lon, `${fieldLabel}.points[${i}]`, errorCode);
6488
6753
  }
6489
6754
  if (value.inside !== void 0) {
6490
6755
  validateBooleanOperand(value.inside, `${fieldLabel}.inside`, errorCode);
@@ -6504,10 +6769,14 @@ function validateLeafOperator(opKey, opValue, leafLabel, errorCode) {
6504
6769
  validateBetween(opValue, opLabel, errorCode);
6505
6770
  return;
6506
6771
  }
6507
- if (opKey === "startsWith" || opKey === "endsWith") {
6772
+ if (opKey === "startsWith" || opKey === "endsWith" || opKey === "contains") {
6508
6773
  validateBoundedString(opValue, opLabel, errorCode);
6509
6774
  return;
6510
6775
  }
6776
+ if (opKey === "caseFold") {
6777
+ validateBooleanOperand(opValue, opLabel, errorCode);
6778
+ return;
6779
+ }
6511
6780
  if (opKey === "in" || opKey === "nin") {
6512
6781
  validateStringList(opValue, opLabel, errorCode);
6513
6782
  return;
@@ -6625,6 +6894,18 @@ function validateFilterExpression(value, fieldLabel, errorCode = SEARCH_INVALID_
6625
6894
  validateFilterExpression(clauseValue, `${fieldLabel}.not`, errorCode, depth + 1);
6626
6895
  }
6627
6896
  }
6897
+ if (depth > 0) return;
6898
+ const tests = filterTestCount(value);
6899
+ if (tests > MAX_FILTER_TESTS) {
6900
+ throwInvalid(
6901
+ errorCode,
6902
+ `Invalid payload: "${fieldLabel}" holds ${tests} tests, above the maximum of ${MAX_FILTER_TESTS}`,
6903
+ {
6904
+ tests,
6905
+ limit: MAX_FILTER_TESTS
6906
+ }
6907
+ );
6908
+ }
6628
6909
  }
6629
6910
 
6630
6911
  // src/distribution/query/validators/search-options.ts
@@ -6845,11 +7126,11 @@ function validateVectorParams(value) {
6845
7126
  );
6846
7127
  }
6847
7128
  const dims = value.value.length;
6848
- if (dims === 0 || dims > MAX_VECTOR_DIMENSION) {
7129
+ if (dims === 0 || dims > MAX_STORED_VECTOR_DIMENSION) {
6849
7130
  throwInvalid(
6850
7131
  CONFIG_INVALID,
6851
- `Invalid SearchPayload: "params.vector.value" length must be between 1 and ${MAX_VECTOR_DIMENSION}`,
6852
- { length: dims, limit: MAX_VECTOR_DIMENSION }
7132
+ `Invalid SearchPayload: "params.vector.value" length must be between 1 and ${MAX_STORED_VECTOR_DIMENSION}`,
7133
+ { length: dims, limit: MAX_STORED_VECTOR_DIMENSION }
6853
7134
  );
6854
7135
  }
6855
7136
  for (let i = 0; i < dims; i++) {
@@ -8051,6 +8332,8 @@ function toWireSortValue2(value) {
8051
8332
  if (typeof value === "boolean") return value;
8052
8333
  return null;
8053
8334
  }
8335
+
8336
+ // src/distribution/replication/sync-primary.ts
8054
8337
  function decideSyncTier(log, replicaLastSeqNo) {
8055
8338
  const oldest = log.oldestSeqNo;
8056
8339
  if (oldest === void 0) {
@@ -8172,7 +8455,8 @@ async function handleSyncRequestMessage(message, respond, deps) {
8172
8455
  engine: deps.engine,
8173
8456
  coordinator: deps.coordinator,
8174
8457
  state: deps.snapshotSyncState,
8175
- resolveHeaderMetadata: deps.resolveHeaderMetadata
8458
+ resolveHeaderMetadata: deps.resolveHeaderMetadata,
8459
+ spoolDirectory: deps.snapshotSpoolDirectory
8176
8460
  },
8177
8461
  {
8178
8462
  metadata: {
@@ -8183,11 +8467,16 @@ async function handleSyncRequestMessage(message, respond, deps) {
8183
8467
  closeOnEnd: false,
8184
8468
  disableBuildCache: true,
8185
8469
  buildSnapshot: async () => {
8186
- const bytes = await deps.engine.serializeReplicationPartition(request.indexName, request.partitionId);
8187
- return { bytes, checksum: crc32(bytes) };
8470
+ const archive = await deps.engine.archiveReplicationPartition(
8471
+ request.indexName,
8472
+ request.partitionId,
8473
+ () => log.localLogEnd
8474
+ );
8475
+ const directory = deps.snapshotSpoolDirectory ?? defaultSpoolDirectory();
8476
+ return { ...await spoolSnapshotStream(directory, archive.stream), lastSeqNo: archive.lastSeqNo };
8188
8477
  },
8189
- afterSnapshot: async (trailingSink) => {
8190
- const entries = log.getEntriesFrom(snapshotSeqNo + 1);
8478
+ afterSnapshot: async (trailingSink, build) => {
8479
+ const entries = log.getEntriesFrom((build.lastSeqNo ?? snapshotSeqNo) + 1);
8191
8480
  await sendSyncEntriesResponse(message, trailingSink, deps, entries);
8192
8481
  }
8193
8482
  }
@@ -8303,7 +8592,7 @@ async function forwardInsertToRemote(indexName, document, docId, primaryNodeId,
8303
8592
  indexName,
8304
8593
  documentId: docId,
8305
8594
  operation: "insert",
8306
- document: encode(document),
8595
+ document: encodeDocument(document),
8307
8596
  updateFields: null
8308
8597
  };
8309
8598
  const message = createForwardMessage(payload, deps.nodeId);
@@ -8323,7 +8612,7 @@ async function forwardUpdateToRemote(indexName, document, docId, primaryNodeId,
8323
8612
  indexName,
8324
8613
  documentId: docId,
8325
8614
  operation: "update",
8326
- document: encode(document),
8615
+ document: encodeDocument(document),
8327
8616
  updateFields: null
8328
8617
  };
8329
8618
  const message = createForwardMessage(payload, deps.nodeId);
@@ -8349,6 +8638,8 @@ async function sendToNode(nodeId, message, deps) {
8349
8638
  details: { targetNodeId: nodeId }
8350
8639
  });
8351
8640
  }
8641
+
8642
+ // src/distribution/cluster-node/write-routing/replication.ts
8352
8643
  async function assertPrimaryWriteAuthority(entry, deps) {
8353
8644
  const table = await deps.coordinator.getAllocation(entry.indexName);
8354
8645
  const currentAssignment = table?.assignments.get(entry.partitionId);
@@ -8378,7 +8669,7 @@ function appendIndexReplicationEntry(indexName, partitionId, assignment, documen
8378
8669
  partitionId,
8379
8670
  indexName,
8380
8671
  documentId,
8381
- document: encode(document)
8672
+ document: encodeDocument(document)
8382
8673
  });
8383
8674
  }
8384
8675
  function appendDeleteReplicationEntry(indexName, partitionId, assignment, documentId, deps) {
@@ -8862,7 +9153,7 @@ function toWireOperation(item) {
8862
9153
  return {
8863
9154
  documentId: item.documentId,
8864
9155
  operation: item.operation,
8865
- document: item.document === null ? null : encode(item.document),
9156
+ document: item.document === null ? null : encodeDocument(item.document),
8866
9157
  updateFields: null
8867
9158
  };
8868
9159
  }
@@ -9269,9 +9560,14 @@ function validateCreateIndexOptions(partitionCount, replicationFactor) {
9269
9560
  async function routeCreateIndex(name, requestedConfig, options, coordinator, engine) {
9270
9561
  validateIndexName(name);
9271
9562
  const config = { ...requestedConfig, schema: validateSchema(requestedConfig.schema) };
9563
+ requireCreatableSchema(config.schema);
9272
9564
  const partitionCount = options?.partitionCount ?? DEFAULT_PARTITION_COUNT;
9273
9565
  const replicationFactor = options?.replicationFactor ?? DEFAULT_REPLICATION_FACTOR;
9274
9566
  validateCreateIndexOptions(partitionCount, replicationFactor);
9567
+ requireConfigurationWithinSizeLimit(
9568
+ withPartitionCount(config, partitionCount),
9569
+ getLanguage(config.language ?? "english")
9570
+ );
9275
9571
  const constraints = {
9276
9572
  zoneAwareness: false,
9277
9573
  zoneAttribute: "zone",
@@ -9552,7 +9848,8 @@ function createDataNodeHandler(deps) {
9552
9848
  engine: deps.engine,
9553
9849
  coordinator: deps.coordinator,
9554
9850
  state: deps.snapshotSyncState,
9555
- resolveHeaderMetadata: deps.resolveHeaderMetadata
9851
+ resolveHeaderMetadata: deps.resolveHeaderMetadata,
9852
+ spoolDirectory: deps.snapshotSpoolDirectory
9556
9853
  });
9557
9854
  return;
9558
9855
  }
@@ -9824,6 +10121,7 @@ var QUERY_REJECTION_CODES = /* @__PURE__ */ new Set([
9824
10121
  ErrorCodes.SEARCH_INVALID_MODE,
9825
10122
  ErrorCodes.SEARCH_INVALID_CURSOR,
9826
10123
  ErrorCodes.SEARCH_RESULT_WINDOW_EXCEEDED,
10124
+ ErrorCodes.SEARCH_WORK_CAP_EXCEEDED,
9827
10125
  ErrorCodes.VECTOR_DIMENSION_MISMATCH,
9828
10126
  ErrorCodes.CONFIG_INVALID
9829
10127
  ]);
@@ -11506,6 +11804,10 @@ async function createClusterNode(config) {
11506
11804
  onIndexClose: (indexName) => deleteIndexReplicationLogs(replicationLogs, indexName)
11507
11805
  });
11508
11806
  localEngine = engine;
11807
+ const snapshotSpool = await openSnapshotSpoolDirectory(config.engine?.durability?.directory).catch(async (err) => {
11808
+ await engine.shutdown();
11809
+ throw err;
11810
+ });
11509
11811
  function replicationLogKey2(indexName, partitionId) {
11510
11812
  return replicationLogKey(indexName, partitionId);
11511
11813
  }
@@ -11565,7 +11867,8 @@ async function createClusterNode(config) {
11565
11867
  resetReplicationLog: seedReplicationLog2,
11566
11868
  applyReplicationEntry: engine.applyReplicationEntry,
11567
11869
  restoreReplicationPartition: engine.restoreReplicationPartition,
11568
- onError: forwardOnError
11870
+ onError: forwardOnError,
11871
+ snapshotSpoolDirectory: snapshotSpool.path
11569
11872
  };
11570
11873
  const primaryPartitionDeps = {
11571
11874
  engine,
@@ -11697,6 +12000,7 @@ async function createClusterNode(config) {
11697
12000
  coordinator: config.coordinator,
11698
12001
  writeDeps,
11699
12002
  snapshotSyncState: snapshotSyncHandlerState,
12003
+ snapshotSpoolDirectory: snapshotSpool.path,
11700
12004
  resolveHeaderMetadata: resolveSnapshotHeaderMetadata,
11701
12005
  isBootstrapSynced: (indexName, partitionId) => hasCompletedBootstrapSync(bootstrapSyncState, indexName, partitionId)
11702
12006
  });
@@ -11728,28 +12032,16 @@ async function createClusterNode(config) {
11728
12032
  }
11729
12033
  await Promise.all([...heldPartitionWrites]);
11730
12034
  await engine.shutdown();
12035
+ await snapshotSpool.dispose();
11731
12036
  }
11732
12037
  };
11733
12038
  return node;
11734
- async function resolveSnapshotHeaderMetadata(indexName, partitionId) {
11735
- const fallback = await defaultSnapshotHeaderMetadataProvider(config.coordinator, indexName);
11736
- if (partitionId === null) {
11737
- return fallback;
11738
- }
11739
- const localIndex = engine.listIndexes().find((index) => index.name === indexName);
11740
- const release = localIndex !== void 0 ? await engine.acquireIndexForReplication(indexName) : null;
11741
- try {
11742
- const allocation = await config.coordinator.getAllocation(indexName);
11743
- const assignment = allocation?.assignments.get(partitionId);
11744
- const log = getReplicationLog2(indexName, partitionId);
11745
- return {
11746
- partitionId,
11747
- primaryTerm: assignment?.primaryTerm ?? fallback.primaryTerm,
11748
- lastSeqNo: log.newestSeqNo ?? 0
11749
- };
11750
- } finally {
11751
- release?.();
11752
- }
12039
+ function resolveSnapshotHeaderMetadata(indexName, partitionId) {
12040
+ return nodeSnapshotHeaderMetadata(
12041
+ { coordinator: config.coordinator, engine, getReplicationLog: getReplicationLog2 },
12042
+ indexName,
12043
+ partitionId
12044
+ );
11753
12045
  }
11754
12046
  }
11755
12047
 
@@ -11852,6 +12144,8 @@ function clusterNodeEngine(node, options) {
11852
12144
  rebuildAnalysis: () => unsupported("rebuildAnalysis"),
11853
12145
  snapshot: () => unsupported("snapshot"),
11854
12146
  restore: () => unsupported("restore"),
12147
+ exportDocuments: () => unsupported("exportDocuments"),
12148
+ importDocumentExport: () => unsupported("importDocumentExport"),
11855
12149
  rebalance: () => unsupported("rebalance"),
11856
12150
  updatePartitionConfig: () => unsupported("updatePartitionConfig"),
11857
12151
  compactVectors: () => unsupported("compactVectors"),