@delali/narsil 0.3.1-next.72 → 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 (59) 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-D6gjjQyA.d.ts → capabilities-CYeAWYxQ.d.ts} +19 -4
  5. package/dist/{chunk-Y52WZWAU.mjs → chunk-2WLD2NFW.mjs} +4 -4
  6. package/dist/{chunk-QR4OKRVY.mjs → chunk-3YKWSO54.mjs} +8 -3
  7. package/dist/{chunk-KZTI25D3.mjs → chunk-4MQ5F4IL.mjs} +1 -1
  8. package/dist/{chunk-CVPA5Q7R.mjs → chunk-AFKH4ZK3.mjs} +9 -6
  9. package/dist/{chunk-M6XF5HDE.mjs → chunk-AK3BLGCI.mjs} +141 -11
  10. package/dist/{chunk-7QMTA7AA.mjs → chunk-CVFAFZSN.mjs} +7771 -4271
  11. package/dist/{chunk-RV77FR2F.mjs → chunk-I64LXKO3.mjs} +0 -1
  12. package/dist/{chunk-EDMCZLSJ.mjs → chunk-LPBWCHBE.mjs} +193 -13
  13. package/dist/{chunk-P2ZZN32C.mjs → chunk-PU43WDY4.mjs} +4 -2
  14. package/dist/{chunk-HVJYTKK7.mjs → chunk-THMSFOJO.mjs} +365 -774
  15. package/dist/{chunk-LCCL6EEH.mjs → chunk-WVDFG45M.mjs} +92 -94
  16. package/dist/{chunk-3HQPUCCT.mjs → chunk-XW25X6AU.mjs} +66 -237
  17. package/dist/{chunk-2X2ZQOAB.mjs → chunk-ZULSEZCE.mjs} +3 -4
  18. package/dist/{client-C2TiW29y.d.ts → client-D698E5ge.d.ts} +76 -25
  19. package/dist/client.d.ts +7 -7
  20. package/dist/client.mjs +44 -22
  21. package/dist/{config-BLguB-om.d.ts → config-Bv8etwUZ.d.ts} +10 -3
  22. package/dist/{config-C7Ip8Ife.d.ts → config-QXU3HMpq.d.ts} +10 -1
  23. package/dist/direct-executor-5SRDFZAS.mjs +13 -0
  24. package/dist/distribution/coordinator/etcd.d.ts +27 -14
  25. package/dist/distribution/coordinator/etcd.mjs +162 -75
  26. package/dist/distribution/transport/grpc.d.ts +2 -2
  27. package/dist/distribution/transport/grpc.mjs +22 -2
  28. package/dist/distribution/transport/in-memory.d.ts +2 -2
  29. package/dist/distribution/transport/in-memory.mjs +6 -3
  30. package/dist/distribution/transport/tcp.d.ts +2 -2
  31. package/dist/distribution/transport/tcp.mjs +40 -16
  32. package/dist/distribution.d.ts +6 -6
  33. package/dist/distribution.mjs +637 -366
  34. package/dist/embeddings/openai.mjs +1 -1
  35. package/dist/{engine-Dm4446j1.d.ts → engine-D_OzKZ-6.d.ts} +96 -187
  36. package/dist/index.browser.mjs +7179 -4031
  37. package/dist/index.d.ts +5 -5
  38. package/dist/index.mjs +11 -14
  39. package/dist/{memory-CKHTU1CL.d.ts → memory-B4jMmygw.d.ts} +213 -2
  40. package/dist/pattern.mjs +1 -1
  41. package/dist/persistence/durability/checkpoint-worker.d.ts +24 -16
  42. package/dist/persistence/durability/checkpoint-worker.mjs +22 -16
  43. package/dist/react.d.ts +6 -6
  44. package/dist/react.mjs +0 -1
  45. package/dist/server.d.ts +9 -9
  46. package/dist/server.mjs +210 -32
  47. package/dist/{tasks-BECd0b__.d.ts → tasks-DpgCpW1x.d.ts} +3 -3
  48. package/dist/{thread-IBSIZZ6W.mjs → thread-3LNVA43V.mjs} +79 -21
  49. package/dist/{types-BQDlIvfS.d.ts → types-B3b68nfF.d.ts} +1 -1
  50. package/dist/{types-D5OwhtsQ.d.ts → types-Cb9kiY5P.d.ts} +1 -1
  51. package/dist/vector/search-worker.mjs +3 -3
  52. package/dist/workers/entry.mjs +4 -4
  53. package/package.json +7 -7
  54. package/dist/chunk-3L4U6EJQ.mjs +0 -49
  55. package/dist/chunk-IFBKLBGB.mjs +0 -7
  56. package/dist/chunk-XZ4PAREY.mjs +0 -25
  57. package/dist/direct-executor-SVCHXR74.mjs +0 -14
  58. package/dist/serialization/crc32-worker.d.ts +0 -20
  59. package/dist/serialization/crc32-worker.mjs +0 -62
@@ -1,27 +1,29 @@
1
1
  import { MAX_MESSAGE_SIZE_BYTES } from './chunk-UNEEFTEU.mjs';
2
- import { validateIndexName, indexConfigKey, indexSettingsOf, putIndexMetadata, withPartitionCount, getIndexMetadata, getClusterIndexConfig } from './chunk-Y52WZWAU.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-7QMTA7AA.mjs';
4
- import { now, listBindingOf, queryBindingOf, hitsKeptPerGroup, clampLimit, resolveHybridFusion, dedupePinnedEntries, foldGroupReducer } from './chunk-2X2ZQOAB.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';
5
4
  import { ReplicationMessageTypes, TransportErrorCodes, ClusterMessageTypes, QueryMessageTypes, TransportError } from './chunk-GCM3JFVA.mjs';
6
- import { providedDocId, MAX_PARTITION_COUNT, MAX_REPLICATION_FACTOR, WIRE_BATCH_BUDGET } from './chunk-CVPA5Q7R.mjs';
7
- 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-XZ4PAREY.mjs';
8
- import { generateId, deserializePayloadV2, createPartitionIndex, FILTER_EXPRESSION_KEYS, FIELD_FILTER_OPERATORS } from './chunk-HVJYTKK7.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-QR4OKRVY.mjs';
10
- import { getLanguage } from './chunk-KZTI25D3.mjs';
11
- import { validateSchema, DEFAULT_PAGE_SIZE, MAX_SORT_FIELDS, MAX_CURSOR_LENGTH, MAX_TOLERANCE, MAX_PREFIX_LENGTH, RESULT_WINDOW, MIN_POLYGON_POINTS, isLatitude, MAX_LATITUDE_DEGREES, isLongitude, MAX_LONGITUDE_DEGREES, MAX_NESTING_DEPTH } from './chunk-M6XF5HDE.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-LCCL6EEH.mjs';
12
+ import './chunk-WVDFG45M.mjs';
14
13
  import { MIN_OVERSAMPLE } from './chunk-ZJ4CSAB2.mjs';
15
- import './chunk-P2ZZN32C.mjs';
16
- import { crc32 } from './chunk-3L4U6EJQ.mjs';
17
- import './chunk-IFBKLBGB.mjs';
18
- import './chunk-3HQPUCCT.mjs';
14
+ import './chunk-PU43WDY4.mjs';
15
+ import './chunk-XW25X6AU.mjs';
19
16
  import './chunk-ZKRV5M2P.mjs';
20
17
  import './chunk-U2BKYEC7.mjs';
21
18
  import { compareCodePoints, toComparableSortValue, compareComparableKeys, isSortMode, SORT_MODES, exceedsCodePoints, compareSortValues } from './chunk-LJYTW7GT.mjs';
22
19
  import './chunk-POWOTUKJ.mjs';
23
- import { ErrorCodes, NarsilError, describeError } from './chunk-RV77FR2F.mjs';
20
+ import { ErrorCodes, NarsilError, describeError } from './chunk-I64LXKO3.mjs';
24
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';
25
27
 
26
28
  // src/distribution/cluster/constants.ts
27
29
  var LEADERSHIP_IMBALANCE_THRESHOLD = 2;
@@ -1251,7 +1253,6 @@ var REPLICATION_ENTRY_FIXED_OVERHEAD_BYTES = 40;
1251
1253
  var DEFAULT_LOG_RETENTION_BYTES = 268435456;
1252
1254
  var INSYNC_CAS_ATTEMPTS = 5;
1253
1255
  var SNAPSHOT_CHUNK_SIZE = 65536;
1254
- var MAX_SNAPSHOT_SIZE_BYTES = 2 * 1024 * 1024 * 1024;
1255
1256
 
1256
1257
  // src/distribution/replication/insync.ts
1257
1258
  var REFUSE = { outcome: "refuse" };
@@ -2516,6 +2517,119 @@ function createDataNodeLifecycle(config) {
2516
2517
  };
2517
2518
  return handle;
2518
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
+ }
2519
2633
 
2520
2634
  // src/distribution/cluster-node/local-index-gone.ts
2521
2635
  function localIndexIsGone(err) {
@@ -2583,6 +2697,19 @@ function fail(state, code, message, details = {}) {
2583
2697
  return;
2584
2698
  }
2585
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
+ });
2586
2713
  }
2587
2714
  function applyErrorEnvelope(state, record) {
2588
2715
  const code = typeof record.code === "string" && record.code.length > 0 ? record.code : "UNKNOWN_ERROR";
@@ -2633,19 +2760,17 @@ function applyStartPayload(state, record) {
2633
2760
  fail(state, ErrorCodes.SNAPSHOT_SYNC_HEADER_INVALID, "totalBytes must be a non-negative integer");
2634
2761
  return;
2635
2762
  }
2636
- if (start.totalBytes > MAX_SNAPSHOT_SIZE_BYTES) {
2637
- fail(state, ErrorCodes.SNAPSHOT_SYNC_TOO_LARGE, `totalBytes ${start.totalBytes} exceeds the limit`, {
2638
- totalBytes: start.totalBytes,
2639
- limit: MAX_SNAPSHOT_SIZE_BYTES
2640
- });
2763
+ try {
2764
+ state.spool = openSnapshotSpoolWriter(state.expectation.spoolDirectory);
2765
+ } catch (err) {
2766
+ spoolFailure(state, err);
2641
2767
  return;
2642
2768
  }
2643
2769
  state.header = header;
2644
2770
  state.totalBytes = start.totalBytes;
2645
- state.assembled = new Uint8Array(start.totalBytes);
2646
2771
  }
2647
2772
  function applyChunkPayload(state, payload) {
2648
- if (state.totalBytes === null || state.assembled === null || state.header === null) {
2773
+ if (state.totalBytes === null || state.header === null) {
2649
2774
  fail(state, ErrorCodes.SNAPSHOT_SYNC_FRAME_INVALID, "received chunk before SNAPSHOT_START");
2650
2775
  return;
2651
2776
  }
@@ -2699,7 +2824,16 @@ function applyChunkPayload(state, payload) {
2699
2824
  });
2700
2825
  return;
2701
2826
  }
2702
- 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
+ }
2703
2837
  state.accumulatedBytes = nextBytes;
2704
2838
  state.expectedNextOffset = payload.offset + payload.data.byteLength;
2705
2839
  }
@@ -2777,7 +2911,7 @@ function createSnapshotStreamState(expectation) {
2777
2911
  expectation,
2778
2912
  header: null,
2779
2913
  totalBytes: null,
2780
- assembled: null,
2914
+ spool: null,
2781
2915
  accumulatedBytes: 0,
2782
2916
  expectedNextOffset: 0,
2783
2917
  endPayload: null,
@@ -2832,11 +2966,20 @@ function handleIncomingSnapshotRecord(state, record) {
2832
2966
  }
2833
2967
  fail(state, ErrorCodes.SNAPSHOT_SYNC_FRAME_INVALID, "unrecognised snapshot frame shape");
2834
2968
  }
2969
+ function abandonSnapshotStream(state) {
2970
+ fail(state, ErrorCodes.SNAPSHOT_SYNC_ABORTED, "the snapshot stream was abandoned before it finished");
2971
+ releaseSpool(state);
2972
+ }
2835
2973
  function finalizeSnapshotStream(state) {
2974
+ const result = settleSnapshotStream(state);
2975
+ if (!result.ok) releaseSpool(state);
2976
+ return result;
2977
+ }
2978
+ function settleSnapshotStream(state) {
2836
2979
  if (state.failure !== null) {
2837
2980
  return state.failure;
2838
2981
  }
2839
- if (state.header === null || state.totalBytes === null || state.assembled === null) {
2982
+ if (state.header === null || state.totalBytes === null) {
2840
2983
  return {
2841
2984
  ok: false,
2842
2985
  code: ErrorCodes.SNAPSHOT_SYNC_CHUNK_MISSING,
@@ -2868,9 +3011,28 @@ function finalizeSnapshotStream(state) {
2868
3011
  }
2869
3012
  };
2870
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;
2871
3033
  return {
2872
3034
  ok: true,
2873
- bytes: state.assembled,
3035
+ snapshot: spool.finish(),
2874
3036
  header: state.header,
2875
3037
  end: state.endPayload
2876
3038
  };
@@ -3081,7 +3243,11 @@ async function fetchSnapshotFromTarget(indexName, partitionId, target, deadline,
3081
3243
  requestId: generateId(),
3082
3244
  payload: encode(requestPayload)
3083
3245
  };
3084
- const streamState = createSnapshotStreamState({ indexName, partitionId: null });
3246
+ const streamState = createSnapshotStreamState({
3247
+ indexName,
3248
+ partitionId: null,
3249
+ spoolDirectory: deps.spoolDirectory ?? defaultSpoolDirectory()
3250
+ });
3085
3251
  const remainingMs = deadline - Date.now();
3086
3252
  if (remainingMs <= 0) {
3087
3253
  return {
@@ -3097,6 +3263,7 @@ async function fetchSnapshotFromTarget(indexName, partitionId, target, deadline,
3097
3263
  });
3098
3264
  await withDeadline(streamWork, remainingMs, indexName, "stream");
3099
3265
  } catch (err) {
3266
+ abandonSnapshotStream(streamState);
3100
3267
  if (err instanceof NarsilError && err.code === ErrorCodes.SNAPSHOT_SYNC_TIMEOUT) {
3101
3268
  return toStreamFailure(err, ErrorCodes.SNAPSHOT_SYNC_TIMEOUT, "snapshot stream deadline exceeded");
3102
3269
  }
@@ -3105,20 +3272,7 @@ async function fetchSnapshotFromTarget(indexName, partitionId, target, deadline,
3105
3272
  }
3106
3273
  return toStreamFailure(err, ErrorCodes.SNAPSHOT_SYNC_PRIMARY_ERROR, "snapshot fetch raised non-transport error");
3107
3274
  }
3108
- const result = finalizeSnapshotStream(streamState);
3109
- if (!result.ok) {
3110
- return result;
3111
- }
3112
- const computedChecksum = crc32(result.bytes);
3113
- if (computedChecksum !== result.header.checksum) {
3114
- return {
3115
- ok: false,
3116
- code: ErrorCodes.SNAPSHOT_SYNC_CHECKSUM_MISMATCH,
3117
- message: "computed checksum does not match header checksum",
3118
- details: { computed: computedChecksum, header: result.header.checksum }
3119
- };
3120
- }
3121
- return result;
3275
+ return finalizeSnapshotStream(streamState);
3122
3276
  }
3123
3277
  async function withDeadline(work, ms, indexName, phase) {
3124
3278
  let timer = null;
@@ -3267,9 +3421,9 @@ function surfaceAborted(deps, indexName, primaryNodeId) {
3267
3421
  })
3268
3422
  );
3269
3423
  }
3270
- async function executeEngineRestore(engine, indexName, primaryNodeId, bytes, remainingMs) {
3424
+ async function executeEngineRestore(engine, indexName, primaryNodeId, source, remainingMs) {
3271
3425
  try {
3272
- await withDeadline(engine.restore(indexName, bytes), remainingMs, indexName, "restore");
3426
+ await withDeadline(engine.restore(indexName, source), remainingMs, indexName, "restore");
3273
3427
  return null;
3274
3428
  } catch (err) {
3275
3429
  if (err instanceof NarsilError && err.code === ErrorCodes.SNAPSHOT_SYNC_TIMEOUT) {
@@ -3555,9 +3709,9 @@ function validateSyncEntryForApply(entry, indexName, partitionId, expectedSeqNo,
3555
3709
  }
3556
3710
  return null;
3557
3711
  }
3558
- function createLiveSyncFrameState(indexName, partitionId) {
3712
+ function createLiveSyncFrameState(indexName, partitionId, spoolDirectory) {
3559
3713
  return {
3560
- snapshotState: createSnapshotStreamState({ indexName, partitionId }),
3714
+ snapshotState: createSnapshotStreamState({ indexName, partitionId, spoolDirectory }),
3561
3715
  syncEntries: [],
3562
3716
  sawSnapshotFrame: false,
3563
3717
  error: null
@@ -3659,35 +3813,25 @@ async function applyLiveIncrementalSync(indexName, partitionId, target, entriesP
3659
3813
  async function applyLiveSnapshotSync(state, entry, indexName, partitionId, target, frameState, coordinatorConfig, partitionCount, deadline, deps) {
3660
3814
  const finalized = finalizeSnapshotStream(frameState.snapshotState);
3661
3815
  if (!finalized.ok) {
3662
- return { ok: false, error: streamFailureToError(finalized) };
3816
+ return { ok: false, error: streamFailureToError({ ...finalized, details: { ...finalized.details, target } }) };
3663
3817
  }
3664
- const computedChecksum = crc32(finalized.bytes);
3665
- if (computedChecksum !== finalized.header.checksum) {
3666
- return {
3667
- ok: false,
3668
- error: new NarsilError(
3669
- ErrorCodes.SNAPSHOT_SYNC_CHECKSUM_MISMATCH,
3670
- "computed checksum does not match header checksum",
3671
- {
3672
- computed: computedChecksum,
3673
- header: finalized.header.checksum,
3674
- target
3675
- }
3676
- )
3677
- };
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();
3678
3834
  }
3679
- const restoreError = await restoreLiveSnapshotPartition(
3680
- state,
3681
- entry,
3682
- indexName,
3683
- partitionId,
3684
- target,
3685
- finalized.bytes,
3686
- coordinatorConfig,
3687
- partitionCount,
3688
- deadline,
3689
- deps
3690
- );
3691
3835
  if (restoreError !== null) {
3692
3836
  return { ok: false, error: restoreError };
3693
3837
  }
@@ -3711,7 +3855,7 @@ async function applyLiveSnapshotSync(state, entry, indexName, partitionId, targe
3711
3855
  snapshotHeader: finalized.header
3712
3856
  };
3713
3857
  }
3714
- 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) {
3715
3859
  const remainingMs = deadline - Date.now();
3716
3860
  if (remainingMs <= 0) {
3717
3861
  return new NarsilError(
@@ -3735,7 +3879,7 @@ async function restoreLiveSnapshotPartition(state, entry, indexName, partitionId
3735
3879
  }
3736
3880
  try {
3737
3881
  await withDeadline(
3738
- deps.restoreReplicationPartition(indexName, partitionId, bytes, coordinatorConfig, partitionCount),
3882
+ deps.restoreReplicationPartition(indexName, partitionId, source, coordinatorConfig, partitionCount),
3739
3883
  remainingMs,
3740
3884
  indexName,
3741
3885
  "partition-restore"
@@ -3927,6 +4071,27 @@ async function syncFromAnyTarget(state, entry, indexName, partitionId, primaryNo
3927
4071
  return { ok: false, error: lastError };
3928
4072
  }
3929
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) {
3930
4095
  const logState = readLocalLogState(indexName, partitionId, deps);
3931
4096
  const requestPayload = {
3932
4097
  indexName,
@@ -3940,7 +4105,6 @@ async function syncFromTarget(state, entry, indexName, partitionId, target, coor
3940
4105
  requestId: generateId(),
3941
4106
  payload: encode(requestPayload)
3942
4107
  };
3943
- const frameState = createLiveSyncFrameState(indexName, partitionId);
3944
4108
  const remainingMs = deadline - Date.now();
3945
4109
  if (remainingMs <= 0) {
3946
4110
  return {
@@ -4172,7 +4336,7 @@ async function fetchSchemaAndPrepare(entry, indexName, primaryNodeId, deps) {
4172
4336
  }
4173
4337
  return configResult.config.schema;
4174
4338
  }
4175
- async function applyRestore(state, entry, indexName, primaryNodeId, bytes, coordinatorSchema, deadline, deps) {
4339
+ async function applyRestore(state, entry, indexName, primaryNodeId, source, coordinatorSchema, deadline, deps) {
4176
4340
  const remainingMs = deadline - Date.now();
4177
4341
  if (remainingMs <= 0) {
4178
4342
  surfaceError(
@@ -4191,7 +4355,7 @@ async function applyRestore(state, entry, indexName, primaryNodeId, bytes, coord
4191
4355
  surfaceAborted(deps, indexName, primaryNodeId);
4192
4356
  return false;
4193
4357
  }
4194
- const restoreError = await executeEngineRestore(deps.engine, indexName, primaryNodeId, bytes, remainingMs);
4358
+ const restoreError = await executeEngineRestore(deps.engine, indexName, primaryNodeId, source, remainingMs);
4195
4359
  if (restoreError !== null) {
4196
4360
  surfaceError(deps, indexName, primaryNodeId, restoreError);
4197
4361
  return false;
@@ -4312,7 +4476,11 @@ async function executeBootstrapSync(state, entry, indexName, partitionId, primar
4312
4476
  surfaceError(deps, indexName, primaryNodeId, targetsResult);
4313
4477
  return false;
4314
4478
  }
4315
- const fetchDeps = { transport: deps.transport, sourceNodeId: deps.sourceNodeId };
4479
+ const fetchDeps = {
4480
+ transport: deps.transport,
4481
+ sourceNodeId: deps.sourceNodeId,
4482
+ spoolDirectory: deps.snapshotSpoolDirectory
4483
+ };
4316
4484
  const fetchResult = await fetchSnapshotFromAnyTarget(
4317
4485
  indexName,
4318
4486
  primaryNodeId,
@@ -4339,20 +4507,25 @@ async function executeBootstrapSync(state, entry, indexName, partitionId, primar
4339
4507
  );
4340
4508
  return false;
4341
4509
  }
4342
- if (abortCheck()) {
4343
- surfaceAborted(deps, indexName, primaryNodeId);
4344
- 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();
4345
4528
  }
4346
- const restoreSucceeded = await applyRestore(
4347
- state,
4348
- entry,
4349
- indexName,
4350
- primaryNodeId,
4351
- fetchResult.bytes,
4352
- schemaResult,
4353
- deadline,
4354
- deps
4355
- );
4356
4529
  if (!restoreSucceeded) {
4357
4530
  return false;
4358
4531
  }
@@ -5216,173 +5389,40 @@ async function applyLocalReplicationEntry(core, entry) {
5216
5389
  }
5217
5390
  }
5218
5391
 
5219
- // src/distribution/cluster-node/local-engine.ts
5220
- async function createClusterLocalEngine(config, hooks) {
5221
- requireKnownConfig(config);
5222
- const runnableConfig = await resolveRunnableConfig(config);
5223
- const core = createEngineCore(runnableConfig, hooks, "partition");
5224
- await startEngineCore(core, runnableConfig);
5225
- core.orchestrator.shareMainThread();
5226
- const engine = createNarsilFromCore(core, runnableConfig);
5227
- const heldPartitions = createHeldPartitionRecord(core);
5228
- return Object.assign(engine, {
5229
- createIndex: (name, indexConfig) => createEngineIndex(core, runnableConfig, name, indexConfig, { adapterBinding: "deferred" }),
5230
- createIndexWithUuid: (name, indexConfig, indexUuid) => createEngineIndex(core, runnableConfig, name, indexConfig, { indexUuid }),
5231
- acquireIndexForReplication: (indexName) => core.indexState.acquire(indexName, false),
5232
- indexUuidOf: (indexName) => core.indexRegistry.get(indexName)?.indexUuid,
5233
- stampIndexUuid: (indexName, indexUuid) => stampIndexUuid(core, indexName, indexUuid),
5234
- highestPersistedSeqNoOf: (indexName, partitionId) => core.durability?.manager.highestPersistedSeqNo(indexName, partitionId) ?? 0,
5235
- heldPartitionsOf: (indexName) => heldPartitions.held(indexName),
5236
- recordHeldPartition: (indexName, partitionId) => heldPartitions.record(indexName, partitionId),
5237
- forgetHeldPartition: (indexName, partitionId) => heldPartitions.forget(indexName, partitionId),
5238
- applyReplicationEntry: (entry) => applyLocalReplicationEntry(core, entry),
5239
- serializeReplicationPartition: (indexName, partitionId) => serializeReplicationPartition(core, indexName, partitionId),
5240
- restoreReplicationPartition: (indexName, partitionId, bytes, indexConfig, partitionCount) => restoreReplicationPartition(core, engine, indexName, partitionId, bytes, indexConfig, partitionCount),
5241
- queryPartitions: (indexName, params, partitionIds, globalStats) => runEngineQuery(core, indexName, params, { partitionIds, globalStats }),
5242
- preflightPartitions: (indexName, params, partitionIds) => runEnginePreflight(core, indexName, params, { partitionIds }),
5243
- suggestPartitions: (indexName, params, partitionIds) => runEngineSuggest(core, indexName, params, partitionIds),
5244
- listPartitions: (indexName, params, partitionIds) => runEngineListDocuments(core, indexName, params, partitionIds),
5245
- collectQueryStats: (indexName, terms, partitionIds) => runEngineQueryStats(core, indexName, terms, partitionIds),
5246
- partitionStatsForRead: async (indexName) => {
5247
- const release = await core.indexState.acquire(indexName);
5248
- try {
5249
- return core.requireManager(indexName).getPartitionStats();
5250
- } finally {
5251
- release();
5252
- }
5253
- }
5254
- });
5255
- }
5256
- async function stampIndexUuid(core, indexName, indexUuid) {
5257
- const entry = core.indexRegistry.get(indexName);
5258
- if (entry === void 0 || entry.indexUuid === indexUuid) {
5259
- return;
5260
- }
5261
- core.indexRegistry.set(indexName, { ...entry, indexUuid });
5262
- if (core.durability !== null) {
5263
- await core.durability.manager.persistMetadata(indexName);
5264
- }
5265
- }
5266
- async function serializeReplicationPartition(core, indexName, partitionId) {
5267
- core.guardShutdown();
5268
- const release = await core.indexState.acquire(indexName, false);
5269
- try {
5270
- return core.requireManager(indexName).serializePartitionToBytes(partitionId);
5271
- } finally {
5272
- release();
5273
- }
5392
+ // src/distribution/cluster-node/replication-index.ts
5393
+ function isNestedSchema(value) {
5394
+ return typeof value === "object" && value !== null;
5274
5395
  }
5275
- async function restoreReplicationPartition(core, engine, indexName, partitionId, bytes, indexConfig, partitionCount) {
5276
- core.guardShutdown();
5277
- validatePartitionRestoreTarget(indexName, partitionId, partitionCount);
5278
- const schema = indexConfig.schema;
5279
- let partition;
5280
- try {
5281
- partition = deserializePayloadV2(bytes);
5282
- } catch (err) {
5283
- const cause = err instanceof Error ? err.message : String(err);
5284
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, `partition snapshot decode failed: ${cause}`, {
5285
- indexName,
5286
- partitionId,
5287
- cause
5288
- });
5289
- }
5290
- validatePartitionSnapshotPayload(indexName, partitionId, partitionCount, schema, partition);
5291
- const trackPositions = core.indexRegistry.get(indexName)?.config.trackPositions ?? true;
5292
- createPartitionIndex(partitionId, trackPositions).deserialize(partition, schema);
5293
- let release = core.indexRegistry.has(indexName) ? await core.indexState.acquire(indexName, false) : null;
5294
- try {
5295
- const restoreIndex = await ensureReplicationIndex(
5296
- core,
5297
- engine,
5298
- indexName,
5299
- indexConfig,
5300
- partition.language,
5301
- partitionCount
5302
- );
5303
- if (release === null) release = await core.indexState.acquire(indexName, false);
5304
- if (restoreIndex.created) {
5305
- await core.orchestrator.replicateToWorkers({
5306
- type: "createIndex",
5307
- indexName,
5308
- config: restoreIndex.config,
5309
- requestId: `replicate-partition-index-${indexName}`
5310
- });
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;
5311
5402
  }
5312
- const manager = core.requireManager(indexName);
5313
- manager.deserializePartition(partitionId, partition);
5314
- await core.orchestrator.replicateToWorkers({
5315
- type: "deserialize",
5316
- indexName,
5317
- partitionId,
5318
- data: partition,
5319
- requestId: `replicate-partition-restore-${indexName}-${partitionId}`
5320
- });
5321
- } finally {
5322
- release?.();
5403
+ flat[path] = value;
5323
5404
  }
5324
5405
  }
5325
- function validatePartitionRestoreTarget(indexName, partitionId, partitionCount) {
5326
- if (!Number.isInteger(partitionId) || partitionId < 0) {
5327
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partitionId must be a non-negative integer", {
5328
- indexName,
5329
- partitionId
5330
- });
5331
- }
5332
- if (!Number.isInteger(partitionCount) || partitionCount <= 0 || partitionCount > MAX_PARTITION_COUNT) {
5333
- throw new NarsilError(
5334
- ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED,
5335
- `partitionCount must be an integer between 1 and ${MAX_PARTITION_COUNT}`,
5336
- { indexName, partitionCount }
5337
- );
5338
- }
5339
- if (partitionId >= partitionCount) {
5340
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partitionId is outside partitionCount", {
5341
- indexName,
5342
- partitionId,
5343
- partitionCount
5344
- });
5345
- }
5406
+ function flattenSchema(schema) {
5407
+ const flat = {};
5408
+ flattenSchemaInto(schema, "", flat);
5409
+ return flat;
5346
5410
  }
5347
- function validatePartitionSnapshotPayload(indexName, partitionId, partitionCount, schema, partition) {
5348
- if (partition.indexName !== indexName) {
5349
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partition snapshot indexName mismatch", {
5350
- expectedIndexName: indexName,
5351
- receivedIndexName: partition.indexName
5352
- });
5353
- }
5354
- if (partition.partitionId !== partitionId) {
5355
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partition snapshot partitionId mismatch", {
5356
- indexName,
5357
- expectedPartitionId: partitionId,
5358
- receivedPartitionId: partition.partitionId
5359
- });
5360
- }
5361
- if (partition.totalPartitions !== partitionCount) {
5362
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "partition snapshot partition count mismatch", {
5363
- indexName,
5364
- partitionId,
5365
- expectedPartitionCount: partitionCount,
5366
- receivedPartitionCount: partition.totalPartitions
5367
- });
5368
- }
5369
- const expectedSchema = flattenSchema(schema);
5370
- for (const [field, expectedType] of Object.entries(expectedSchema)) {
5371
- if (partition.schema[field] !== expectedType) {
5372
- 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`, {
5373
5415
  indexName,
5374
- partitionId,
5375
5416
  field,
5376
5417
  expected: expectedType,
5377
- received: partition.schema[field] ?? "(absent)"
5418
+ received: actual[field] ?? "(absent)"
5378
5419
  });
5379
5420
  }
5380
5421
  }
5381
- for (const field of Object.keys(partition.schema)) {
5382
- if (!(field in expectedSchema)) {
5383
- 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`, {
5384
5425
  indexName,
5385
- partitionId,
5386
5426
  field
5387
5427
  });
5388
5428
  }
@@ -5414,7 +5454,12 @@ async function ensureReplicationIndex(core, engine, indexName, coordinatorConfig
5414
5454
  receivedLanguage: language.name
5415
5455
  });
5416
5456
  }
5417
- validateExistingSchema(indexName, coordinatorConfig.schema, entry.config.schema);
5457
+ requireSameFlatSchema(
5458
+ indexName,
5459
+ flattenSchema(coordinatorConfig.schema),
5460
+ flattenSchema(entry.config.schema),
5461
+ "local index"
5462
+ );
5418
5463
  const manager = core.requireManager(indexName);
5419
5464
  if (manager.partitionCount > partitionCount) {
5420
5465
  throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "local index has more partitions than allocation", {
@@ -5433,45 +5478,222 @@ async function ensureReplicationIndex(core, engine, indexName, coordinatorConfig
5433
5478
  }
5434
5479
  return { config: entry.config, created };
5435
5480
  }
5436
- function validateExistingSchema(indexName, expected, actual) {
5437
- const expectedSchema = flattenSchema(expected);
5438
- const actualSchema = flattenSchema(actual);
5439
- for (const [field, expectedType] of Object.entries(expectedSchema)) {
5440
- if (actualSchema[field] !== expectedType) {
5441
- throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "local index schema mismatch", {
5442
- indexName,
5443
- field,
5444
- expected: expectedType,
5445
- 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
5446
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
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
+ }
5447
5613
  }
5614
+ vectorIndex.scheduleBuild();
5448
5615
  }
5449
- for (const field of Object.keys(actualSchema)) {
5450
- if (!(field in expectedSchema)) {
5451
- 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",
5452
5635
  indexName,
5453
- field
5636
+ config: restoreIndex.config,
5637
+ requestId: `replicate-partition-index-${indexName}`
5454
5638
  });
5455
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?.();
5456
5648
  }
5457
5649
  }
5458
- function flattenSchema(schema) {
5459
- const flat = {};
5460
- flattenSchemaInto(schema, "", flat);
5461
- return flat;
5462
- }
5463
- function flattenSchemaInto(schema, prefix, flat) {
5464
- for (const [field, value] of Object.entries(schema)) {
5465
- const path = prefix.length === 0 ? field : `${prefix}.${field}`;
5466
- if (isNestedSchema(value)) {
5467
- flattenSchemaInto(value, path, flat);
5468
- 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
+ }
5469
5685
  }
5470
- flat[path] = value;
5471
- }
5686
+ });
5472
5687
  }
5473
- function isNestedSchema(value) {
5474
- 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
+ }
5475
5697
  }
5476
5698
 
5477
5699
  // src/distribution/cluster-node/local-index-reconcile.ts
@@ -5584,6 +5806,25 @@ async function authorizeSnapshotRequest(coordinator, indexName, sourceId) {
5584
5806
  }
5585
5807
 
5586
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
+ }
5587
5828
  function createSnapshotCacheState(maxConcurrent = DEFAULT_MAX_CONCURRENT_SNAPSHOTS, maxPerSource = DEFAULT_MAX_PER_SOURCE_SNAPSHOTS, maxStreamsPerIndex = DEFAULT_MAX_STREAMS_PER_INDEX) {
5588
5829
  if (!Number.isInteger(maxConcurrent) || maxConcurrent <= 0) {
5589
5830
  throw new NarsilError(ErrorCodes.CONFIG_INVALID, "maxConcurrent must be a positive integer", {
@@ -5671,7 +5912,7 @@ function releaseStreamSlot(state, handle) {
5671
5912
  async function acquireSnapshotBuild(state, indexName, builder) {
5672
5913
  const existing = state.cache.get(indexName);
5673
5914
  if (existing !== void 0) {
5674
- return existing.promise;
5915
+ return joinSnapshotBuild(existing);
5675
5916
  }
5676
5917
  if (state.active >= state.maxConcurrent) {
5677
5918
  throw new NarsilError(
@@ -5694,7 +5935,7 @@ async function acquireSnapshotBuild(state, indexName, builder) {
5694
5935
  state.active = Math.max(0, state.active - 1);
5695
5936
  };
5696
5937
  const rawBuild = (async () => builder())();
5697
- const entry = { promise: rawBuild };
5938
+ const entry = { promise: rawBuild, holders: 0 };
5698
5939
  entry.promise = rawBuild.then(
5699
5940
  (result) => {
5700
5941
  clearEntry(entry);
@@ -5706,7 +5947,7 @@ async function acquireSnapshotBuild(state, indexName, builder) {
5706
5947
  }
5707
5948
  );
5708
5949
  state.cache.set(indexName, entry);
5709
- return entry.promise;
5950
+ return joinSnapshotBuild(entry);
5710
5951
  }
5711
5952
  var SNAPSHOT_SYNC_ERROR_TYPE = `${ReplicationMessageTypes.SNAPSHOT_SYNC_REQUEST}.error`;
5712
5953
  function createSingleResponseSink(respond) {
@@ -5720,8 +5961,7 @@ function createSingleResponseSink(respond) {
5720
5961
  return sink;
5721
5962
  }
5722
5963
  async function streamSnapshotToReplica(sink, nodeId, requestId, indexName, build, metadata, options = {}) {
5723
- const snapshotBytes = build.bytes;
5724
- const totalBytes = snapshotBytes.byteLength;
5964
+ const totalBytes = build.totalBytes;
5725
5965
  const checksum = build.checksum;
5726
5966
  const header = {
5727
5967
  lastSeqNo: metadata.lastSeqNo,
@@ -5737,22 +5977,23 @@ async function streamSnapshotToReplica(sink, nodeId, requestId, indexName, build
5737
5977
  await yieldToEventLoop();
5738
5978
  let offset = 0;
5739
5979
  let lastYieldAt = now2();
5740
- while (offset < totalBytes) {
5741
- const end = Math.min(offset + SNAPSHOT_CHUNK_SIZE, totalBytes);
5742
- const chunk = snapshotBytes.subarray(offset, end);
5743
- const chunkPayload = {
5744
- partitionId: metadata.partitionId,
5745
- indexName,
5746
- offset,
5747
- data: chunk
5748
- };
5749
- const chunkBytes = encode(chunkPayload);
5750
- assertMessageWithinLimit(chunkBytes, "SNAPSHOT_CHUNK");
5751
- await respondMessage(sink, ReplicationMessageTypes.SNAPSHOT_CHUNK, nodeId, requestId, chunkBytes);
5752
- offset = end;
5753
- if (offset < totalBytes && now2() - lastYieldAt >= SNAPSHOT_CHUNK_YIELD_INTERVAL_MS) {
5754
- await yieldToEventLoop();
5755
- 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
+ }
5756
5997
  }
5757
5998
  }
5758
5999
  await yieldToEventLoop();
@@ -5833,6 +6074,26 @@ async function defaultSnapshotHeaderMetadataProvider(coordinator, indexName) {
5833
6074
  lastSeqNo: SNAPSHOT_HEADER_SENTINEL_SEQNO
5834
6075
  };
5835
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
+ }
5836
6097
  async function resolveHeaderMetadata(deps, indexName, partitionId) {
5837
6098
  const provider = deps.resolveHeaderMetadata;
5838
6099
  if (provider === void 0) {
@@ -5862,55 +6123,56 @@ async function acquireAndStream(message, sink, request, deps, options) {
5862
6123
  return;
5863
6124
  }
5864
6125
  try {
5865
- let build;
6126
+ let lease;
5866
6127
  try {
5867
6128
  const buildForRequest = async () => buildSnapshotForRequest(deps, indexName, options);
5868
- 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);
5869
6130
  } catch (err) {
5870
6131
  const code = err instanceof NarsilError ? err.code : ErrorCodes.SNAPSHOT_SYNC_SNAPSHOT_FAILED;
5871
6132
  const errMessage = err instanceof Error ? err.message : String(err);
5872
6133
  await respondError(sink, deps.nodeId, message.requestId, code, errMessage);
5873
6134
  return;
5874
6135
  }
5875
- if (build.bytes.byteLength > MAX_SNAPSHOT_SIZE_BYTES) {
5876
- await respondError(
5877
- sink,
5878
- deps.nodeId,
5879
- message.requestId,
5880
- ErrorCodes.SNAPSHOT_SYNC_TOO_LARGE,
5881
- `Snapshot size ${build.bytes.byteLength} exceeds the ${MAX_SNAPSHOT_SIZE_BYTES} byte limit`
5882
- );
5883
- return;
5884
- }
5885
- let streamHandle;
5886
- try {
5887
- streamHandle = acquireStreamSlot(deps.state, indexName);
5888
- } catch (err) {
5889
- const code = err instanceof NarsilError ? err.code : ErrorCodes.SNAPSHOT_SYNC_CAPACITY_EXHAUSTED;
5890
- const errMessage = err instanceof Error ? err.message : String(err);
5891
- await respondError(sink, deps.nodeId, message.requestId, code, errMessage);
5892
- return;
5893
- }
5894
6136
  try {
5895
- const metadata = options.metadata ?? await resolveHeaderMetadata(deps, indexName, request.partitionId ?? null);
5896
- await streamSnapshotToReplica(sink, deps.nodeId, message.requestId, indexName, build, metadata, {
5897
- closeOnEnd: options.closeOnEnd
5898
- });
5899
- await options.afterSnapshot?.(sink);
5900
- if (options.closeOnEnd === false) {
5901
- sink.closed = true;
5902
- }
6137
+ await streamLeasedSnapshot(message, sink, request, deps, options, lease.build);
5903
6138
  } finally {
5904
- releaseStreamSlot(deps.state, streamHandle);
6139
+ await lease.release();
5905
6140
  }
5906
6141
  } finally {
5907
6142
  releaseSourceSlot(deps.state, sourceHandle);
5908
6143
  }
5909
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
+ }
5910
6170
  async function buildSnapshot(deps, indexName) {
5911
- let bytes;
5912
6171
  try {
5913
- bytes = await deps.engine.snapshot(indexName);
6172
+ return await spoolSnapshotStream(
6173
+ deps.spoolDirectory ?? defaultSpoolDirectory(),
6174
+ await deps.engine.snapshot(indexName)
6175
+ );
5914
6176
  } catch (err) {
5915
6177
  if (err instanceof NarsilError) {
5916
6178
  throw err;
@@ -5921,8 +6183,6 @@ async function buildSnapshot(deps, indexName) {
5921
6183
  cause: errMessage
5922
6184
  });
5923
6185
  }
5924
- const checksum = bytes.byteLength <= MAX_SNAPSHOT_SIZE_BYTES ? crc32(bytes) : 0;
5925
- return { bytes, checksum };
5926
6186
  }
5927
6187
  function buildSnapshotForRequest(deps, indexName, options) {
5928
6188
  if (options.buildSnapshot !== void 0) {
@@ -6131,7 +6391,6 @@ var MAX_FACET_VALUE_LENGTH = 1024;
6131
6391
  var MAX_GROUPS_PER_RESPONSE = 65536;
6132
6392
  var MAX_FACET_SIZE = 1e3;
6133
6393
  var DEFAULT_MAX_FACET_BUCKETS = 100;
6134
- var MAX_VECTOR_DIMENSION = 8192;
6135
6394
  var MAX_VECTOR_TEXT_LENGTH = 16384;
6136
6395
 
6137
6396
  // src/distribution/query/validators/common.ts
@@ -6867,11 +7126,11 @@ function validateVectorParams(value) {
6867
7126
  );
6868
7127
  }
6869
7128
  const dims = value.value.length;
6870
- if (dims === 0 || dims > MAX_VECTOR_DIMENSION) {
7129
+ if (dims === 0 || dims > MAX_STORED_VECTOR_DIMENSION) {
6871
7130
  throwInvalid(
6872
7131
  CONFIG_INVALID,
6873
- `Invalid SearchPayload: "params.vector.value" length must be between 1 and ${MAX_VECTOR_DIMENSION}`,
6874
- { 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 }
6875
7134
  );
6876
7135
  }
6877
7136
  for (let i = 0; i < dims; i++) {
@@ -8073,6 +8332,8 @@ function toWireSortValue2(value) {
8073
8332
  if (typeof value === "boolean") return value;
8074
8333
  return null;
8075
8334
  }
8335
+
8336
+ // src/distribution/replication/sync-primary.ts
8076
8337
  function decideSyncTier(log, replicaLastSeqNo) {
8077
8338
  const oldest = log.oldestSeqNo;
8078
8339
  if (oldest === void 0) {
@@ -8194,7 +8455,8 @@ async function handleSyncRequestMessage(message, respond, deps) {
8194
8455
  engine: deps.engine,
8195
8456
  coordinator: deps.coordinator,
8196
8457
  state: deps.snapshotSyncState,
8197
- resolveHeaderMetadata: deps.resolveHeaderMetadata
8458
+ resolveHeaderMetadata: deps.resolveHeaderMetadata,
8459
+ spoolDirectory: deps.snapshotSpoolDirectory
8198
8460
  },
8199
8461
  {
8200
8462
  metadata: {
@@ -8205,11 +8467,16 @@ async function handleSyncRequestMessage(message, respond, deps) {
8205
8467
  closeOnEnd: false,
8206
8468
  disableBuildCache: true,
8207
8469
  buildSnapshot: async () => {
8208
- const bytes = await deps.engine.serializeReplicationPartition(request.indexName, request.partitionId);
8209
- 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 };
8210
8477
  },
8211
- afterSnapshot: async (trailingSink) => {
8212
- const entries = log.getEntriesFrom(snapshotSeqNo + 1);
8478
+ afterSnapshot: async (trailingSink, build) => {
8479
+ const entries = log.getEntriesFrom((build.lastSeqNo ?? snapshotSeqNo) + 1);
8213
8480
  await sendSyncEntriesResponse(message, trailingSink, deps, entries);
8214
8481
  }
8215
8482
  }
@@ -8325,7 +8592,7 @@ async function forwardInsertToRemote(indexName, document, docId, primaryNodeId,
8325
8592
  indexName,
8326
8593
  documentId: docId,
8327
8594
  operation: "insert",
8328
- document: encode(document),
8595
+ document: encodeDocument(document),
8329
8596
  updateFields: null
8330
8597
  };
8331
8598
  const message = createForwardMessage(payload, deps.nodeId);
@@ -8345,7 +8612,7 @@ async function forwardUpdateToRemote(indexName, document, docId, primaryNodeId,
8345
8612
  indexName,
8346
8613
  documentId: docId,
8347
8614
  operation: "update",
8348
- document: encode(document),
8615
+ document: encodeDocument(document),
8349
8616
  updateFields: null
8350
8617
  };
8351
8618
  const message = createForwardMessage(payload, deps.nodeId);
@@ -8371,6 +8638,8 @@ async function sendToNode(nodeId, message, deps) {
8371
8638
  details: { targetNodeId: nodeId }
8372
8639
  });
8373
8640
  }
8641
+
8642
+ // src/distribution/cluster-node/write-routing/replication.ts
8374
8643
  async function assertPrimaryWriteAuthority(entry, deps) {
8375
8644
  const table = await deps.coordinator.getAllocation(entry.indexName);
8376
8645
  const currentAssignment = table?.assignments.get(entry.partitionId);
@@ -8400,7 +8669,7 @@ function appendIndexReplicationEntry(indexName, partitionId, assignment, documen
8400
8669
  partitionId,
8401
8670
  indexName,
8402
8671
  documentId,
8403
- document: encode(document)
8672
+ document: encodeDocument(document)
8404
8673
  });
8405
8674
  }
8406
8675
  function appendDeleteReplicationEntry(indexName, partitionId, assignment, documentId, deps) {
@@ -8884,7 +9153,7 @@ function toWireOperation(item) {
8884
9153
  return {
8885
9154
  documentId: item.documentId,
8886
9155
  operation: item.operation,
8887
- document: item.document === null ? null : encode(item.document),
9156
+ document: item.document === null ? null : encodeDocument(item.document),
8888
9157
  updateFields: null
8889
9158
  };
8890
9159
  }
@@ -9291,9 +9560,14 @@ function validateCreateIndexOptions(partitionCount, replicationFactor) {
9291
9560
  async function routeCreateIndex(name, requestedConfig, options, coordinator, engine) {
9292
9561
  validateIndexName(name);
9293
9562
  const config = { ...requestedConfig, schema: validateSchema(requestedConfig.schema) };
9563
+ requireCreatableSchema(config.schema);
9294
9564
  const partitionCount = options?.partitionCount ?? DEFAULT_PARTITION_COUNT;
9295
9565
  const replicationFactor = options?.replicationFactor ?? DEFAULT_REPLICATION_FACTOR;
9296
9566
  validateCreateIndexOptions(partitionCount, replicationFactor);
9567
+ requireConfigurationWithinSizeLimit(
9568
+ withPartitionCount(config, partitionCount),
9569
+ getLanguage(config.language ?? "english")
9570
+ );
9297
9571
  const constraints = {
9298
9572
  zoneAwareness: false,
9299
9573
  zoneAttribute: "zone",
@@ -9574,7 +9848,8 @@ function createDataNodeHandler(deps) {
9574
9848
  engine: deps.engine,
9575
9849
  coordinator: deps.coordinator,
9576
9850
  state: deps.snapshotSyncState,
9577
- resolveHeaderMetadata: deps.resolveHeaderMetadata
9851
+ resolveHeaderMetadata: deps.resolveHeaderMetadata,
9852
+ spoolDirectory: deps.snapshotSpoolDirectory
9578
9853
  });
9579
9854
  return;
9580
9855
  }
@@ -11529,6 +11804,10 @@ async function createClusterNode(config) {
11529
11804
  onIndexClose: (indexName) => deleteIndexReplicationLogs(replicationLogs, indexName)
11530
11805
  });
11531
11806
  localEngine = engine;
11807
+ const snapshotSpool = await openSnapshotSpoolDirectory(config.engine?.durability?.directory).catch(async (err) => {
11808
+ await engine.shutdown();
11809
+ throw err;
11810
+ });
11532
11811
  function replicationLogKey2(indexName, partitionId) {
11533
11812
  return replicationLogKey(indexName, partitionId);
11534
11813
  }
@@ -11588,7 +11867,8 @@ async function createClusterNode(config) {
11588
11867
  resetReplicationLog: seedReplicationLog2,
11589
11868
  applyReplicationEntry: engine.applyReplicationEntry,
11590
11869
  restoreReplicationPartition: engine.restoreReplicationPartition,
11591
- onError: forwardOnError
11870
+ onError: forwardOnError,
11871
+ snapshotSpoolDirectory: snapshotSpool.path
11592
11872
  };
11593
11873
  const primaryPartitionDeps = {
11594
11874
  engine,
@@ -11720,6 +12000,7 @@ async function createClusterNode(config) {
11720
12000
  coordinator: config.coordinator,
11721
12001
  writeDeps,
11722
12002
  snapshotSyncState: snapshotSyncHandlerState,
12003
+ snapshotSpoolDirectory: snapshotSpool.path,
11723
12004
  resolveHeaderMetadata: resolveSnapshotHeaderMetadata,
11724
12005
  isBootstrapSynced: (indexName, partitionId) => hasCompletedBootstrapSync(bootstrapSyncState, indexName, partitionId)
11725
12006
  });
@@ -11751,28 +12032,16 @@ async function createClusterNode(config) {
11751
12032
  }
11752
12033
  await Promise.all([...heldPartitionWrites]);
11753
12034
  await engine.shutdown();
12035
+ await snapshotSpool.dispose();
11754
12036
  }
11755
12037
  };
11756
12038
  return node;
11757
- async function resolveSnapshotHeaderMetadata(indexName, partitionId) {
11758
- const fallback = await defaultSnapshotHeaderMetadataProvider(config.coordinator, indexName);
11759
- if (partitionId === null) {
11760
- return fallback;
11761
- }
11762
- const localIndex = engine.listIndexes().find((index) => index.name === indexName);
11763
- const release = localIndex !== void 0 ? await engine.acquireIndexForReplication(indexName) : null;
11764
- try {
11765
- const allocation = await config.coordinator.getAllocation(indexName);
11766
- const assignment = allocation?.assignments.get(partitionId);
11767
- const log = getReplicationLog2(indexName, partitionId);
11768
- return {
11769
- partitionId,
11770
- primaryTerm: assignment?.primaryTerm ?? fallback.primaryTerm,
11771
- lastSeqNo: log.newestSeqNo ?? 0
11772
- };
11773
- } finally {
11774
- release?.();
11775
- }
12039
+ function resolveSnapshotHeaderMetadata(indexName, partitionId) {
12040
+ return nodeSnapshotHeaderMetadata(
12041
+ { coordinator: config.coordinator, engine, getReplicationLog: getReplicationLog2 },
12042
+ indexName,
12043
+ partitionId
12044
+ );
11776
12045
  }
11777
12046
  }
11778
12047
 
@@ -11875,6 +12144,8 @@ function clusterNodeEngine(node, options) {
11875
12144
  rebuildAnalysis: () => unsupported("rebuildAnalysis"),
11876
12145
  snapshot: () => unsupported("snapshot"),
11877
12146
  restore: () => unsupported("restore"),
12147
+ exportDocuments: () => unsupported("exportDocuments"),
12148
+ importDocumentExport: () => unsupported("importDocumentExport"),
11878
12149
  rebalance: () => unsupported("rebalance"),
11879
12150
  updatePartitionConfig: () => unsupported("updatePartitionConfig"),
11880
12151
  compactVectors: () => unsupported("compactVectors"),