@delali/narsil 0.3.1-next.72 → 0.3.1-next.74

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 (70) 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-CAMzmkAg.d.ts} +19 -4
  5. package/dist/{chunk-P2ZZN32C.mjs → chunk-2E35UM3G.mjs} +2 -12
  6. package/dist/chunk-3DDUNI6N.mjs +48 -0
  7. package/dist/{chunk-KZTI25D3.mjs → chunk-4MQ5F4IL.mjs} +1 -1
  8. package/dist/{chunk-HVJYTKK7.mjs → chunk-BVBIQQKG.mjs} +947 -1327
  9. package/dist/{chunk-2X2ZQOAB.mjs → chunk-CTOSGRM7.mjs} +101 -21
  10. package/dist/{chunk-7QMTA7AA.mjs → chunk-CUZOX6CN.mjs} +7784 -4276
  11. package/dist/chunk-I23G3IZC.mjs +593 -0
  12. package/dist/{chunk-RV77FR2F.mjs → chunk-I64LXKO3.mjs} +0 -1
  13. package/dist/chunk-KEHK2O6P.mjs +13 -0
  14. package/dist/{chunk-CVPA5Q7R.mjs → chunk-LX4JERXI.mjs} +9 -6
  15. package/dist/{chunk-EDMCZLSJ.mjs → chunk-NOZUQM7J.mjs} +193 -13
  16. package/dist/{chunk-LCCL6EEH.mjs → chunk-PO53FV4Q.mjs} +93 -94
  17. package/dist/chunk-T5ZWEWVD.mjs +658 -0
  18. package/dist/{chunk-Y52WZWAU.mjs → chunk-TVFLHM7M.mjs} +4 -4
  19. package/dist/{chunk-QR4OKRVY.mjs → chunk-VKX2BPXD.mjs} +147 -642
  20. package/dist/{chunk-3HQPUCCT.mjs → chunk-XW25X6AU.mjs} +66 -237
  21. package/dist/{chunk-M6XF5HDE.mjs → chunk-Z3DQPOJQ.mjs} +127 -30
  22. package/dist/{client-C2TiW29y.d.ts → client-C-I70ArH.d.ts} +77 -26
  23. package/dist/client.d.ts +9 -9
  24. package/dist/client.mjs +44 -22
  25. package/dist/{config-BLguB-om.d.ts → config-C6Iw5rCX.d.ts} +11 -4
  26. package/dist/{config-C7Ip8Ife.d.ts → config-DJPH3IQB.d.ts} +11 -2
  27. package/dist/direct-executor-KFAGILJC.mjs +17 -0
  28. package/dist/distribution/coordinator/etcd.d.ts +29 -16
  29. package/dist/distribution/coordinator/etcd.mjs +162 -75
  30. package/dist/distribution/coordinator/in-memory.d.ts +2 -2
  31. package/dist/distribution/transport/grpc.d.ts +2 -2
  32. package/dist/distribution/transport/grpc.mjs +22 -2
  33. package/dist/distribution/transport/in-memory.d.ts +2 -2
  34. package/dist/distribution/transport/in-memory.mjs +6 -3
  35. package/dist/distribution/transport/tcp.d.ts +2 -2
  36. package/dist/distribution/transport/tcp.mjs +40 -16
  37. package/dist/distribution.d.ts +9 -9
  38. package/dist/distribution.mjs +647 -366
  39. package/dist/embeddings/openai.mjs +1 -1
  40. package/dist/{engine-Dm4446j1.d.ts → engine-B8eRPO7m.d.ts} +97 -188
  41. package/dist/index.browser.mjs +12132 -8592
  42. package/dist/index.d.ts +27 -8
  43. package/dist/index.mjs +15 -14
  44. package/dist/{memory-CKHTU1CL.d.ts → memory-DJEwWoCi.d.ts} +1204 -968
  45. package/dist/partial-words-DTIHO8G4.d.ts +19 -0
  46. package/dist/partial-words.d.ts +22 -0
  47. package/dist/partial-words.mjs +11 -0
  48. package/dist/pattern.mjs +1 -1
  49. package/dist/persistence/durability/checkpoint-worker.d.ts +57 -29
  50. package/dist/persistence/durability/checkpoint-worker.mjs +26 -16
  51. package/dist/react.d.ts +7 -7
  52. package/dist/react.mjs +0 -1
  53. package/dist/{schema-BBOCdhND.d.ts → schema-DgyDijhf.d.ts} +6 -2
  54. package/dist/server.d.ts +11 -11
  55. package/dist/server.mjs +212 -32
  56. package/dist/{tasks-BECd0b__.d.ts → tasks-BVmAKYBh.d.ts} +3 -3
  57. package/dist/{thread-IBSIZZ6W.mjs → thread-W6DPKEF6.mjs} +82 -21
  58. package/dist/{types-BQDlIvfS.d.ts → types-B3b68nfF.d.ts} +1 -1
  59. package/dist/{types-D5OwhtsQ.d.ts → types-Cb9kiY5P.d.ts} +1 -1
  60. package/dist/{types-BiUmaiWT.d.ts → types-ChrVy4lH.d.ts} +1 -1
  61. package/dist/vector/search-worker.d.ts +1 -1
  62. package/dist/vector/search-worker.mjs +4 -3
  63. package/dist/workers/entry.mjs +4 -4
  64. package/package.json +15 -7
  65. package/dist/chunk-3L4U6EJQ.mjs +0 -49
  66. package/dist/chunk-IFBKLBGB.mjs +0 -7
  67. package/dist/chunk-XZ4PAREY.mjs +0 -25
  68. package/dist/direct-executor-SVCHXR74.mjs +0 -14
  69. package/dist/serialization/crc32-worker.d.ts +0 -20
  70. package/dist/serialization/crc32-worker.mjs +0 -62
@@ -1,27 +1,33 @@
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-TVFLHM7M.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-CUZOX6CN.mjs';
4
+ import { now, listBindingOf, requireSurfaceFormsForPartialFields, queryBindingOf, hitsKeptPerGroup, clampLimit, resolveHybridFusion, dedupePinnedEntries, foldGroupReducer } from './chunk-CTOSGRM7.mjs';
5
5
  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';
6
+ import { providedDocId, MAX_PARTITION_COUNT, MAX_REPLICATION_FACTOR, WIRE_BATCH_BUDGET } from './chunk-LX4JERXI.mjs';
7
+ import { generateId, decodeVectorFilePayload, bytesToVectors, isDead, FILTER_EXPRESSION_KEYS, FIELD_FILTER_OPERATORS } from './chunk-BVBIQQKG.mjs';
8
+ 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-VKX2BPXD.mjs';
9
+ import { getLanguage } from './chunk-4MQ5F4IL.mjs';
10
+ import { DEFAULT_SUGGEST_LIMIT, MAX_SUGGEST_LIMIT, DEFAULT_PAGE_SIZE, 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 } from './chunk-3DDUNI6N.mjs';
12
11
  import './chunk-TGP5M5R7.mjs';
13
- import './chunk-LCCL6EEH.mjs';
12
+ import './chunk-PO53FV4Q.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-2E35UM3G.mjs';
15
+ import './chunk-XW25X6AU.mjs';
19
16
  import './chunk-ZKRV5M2P.mjs';
17
+ import './chunk-T5ZWEWVD.mjs';
18
+ import './chunk-I23G3IZC.mjs';
19
+ import { validateSchema, requireCreatableSchema, encodeDocument, MAX_STORED_VECTOR_DIMENSION, MIN_POLYGON_POINTS, isLatitude, MAX_LATITUDE_DEGREES, isLongitude, MAX_LONGITUDE_DEGREES, MAX_NESTING_DEPTH } from './chunk-Z3DQPOJQ.mjs';
20
+ import './chunk-KEHK2O6P.mjs';
20
21
  import './chunk-U2BKYEC7.mjs';
21
22
  import { compareCodePoints, toComparableSortValue, compareComparableKeys, isSortMode, SORT_MODES, exceedsCodePoints, compareSortValues } from './chunk-LJYTW7GT.mjs';
22
23
  import './chunk-POWOTUKJ.mjs';
23
- import { ErrorCodes, NarsilError, describeError } from './chunk-RV77FR2F.mjs';
24
+ import { ErrorCodes, NarsilError, describeError } from './chunk-I64LXKO3.mjs';
24
25
  import { encode, decode } from '@msgpack/msgpack';
26
+ import { randomUUID } from 'crypto';
27
+ import { openSync, writeSync, closeSync, rmSync, readSync } from 'fs';
28
+ import { mkdtemp, rm, mkdir } from 'fs/promises';
29
+ import { tmpdir } from 'os';
30
+ import { join } from 'path';
25
31
 
26
32
  // src/distribution/cluster/constants.ts
27
33
  var LEADERSHIP_IMBALANCE_THRESHOLD = 2;
@@ -1251,7 +1257,6 @@ var REPLICATION_ENTRY_FIXED_OVERHEAD_BYTES = 40;
1251
1257
  var DEFAULT_LOG_RETENTION_BYTES = 268435456;
1252
1258
  var INSYNC_CAS_ATTEMPTS = 5;
1253
1259
  var SNAPSHOT_CHUNK_SIZE = 65536;
1254
- var MAX_SNAPSHOT_SIZE_BYTES = 2 * 1024 * 1024 * 1024;
1255
1260
 
1256
1261
  // src/distribution/replication/insync.ts
1257
1262
  var REFUSE = { outcome: "refuse" };
@@ -2516,6 +2521,119 @@ function createDataNodeLifecycle(config) {
2516
2521
  };
2517
2522
  return handle;
2518
2523
  }
2524
+ var SPOOL_FILE_PREFIX = "narsil-snapshot-";
2525
+ var DURABLE_SPOOL_DIRECTORY = ".snapshot-transfer";
2526
+ var TEMPORARY_SPOOL_DIRECTORY_PREFIX = "narsil-snapshot-transfer-";
2527
+ var OWNER_ONLY_FILE_MODE = 384;
2528
+ var OWNER_ONLY_DIRECTORY_MODE = 448;
2529
+ function defaultSpoolDirectory() {
2530
+ return tmpdir();
2531
+ }
2532
+ function spoolFilePath(directory) {
2533
+ return join(directory, `${SPOOL_FILE_PREFIX}${randomUUID()}`);
2534
+ }
2535
+ async function* readSpoolFile(path) {
2536
+ const descriptor = openSync(path, "r");
2537
+ try {
2538
+ for (; ; ) {
2539
+ const buffer = new Uint8Array(SNAPSHOT_CHUNK_SIZE);
2540
+ const bytesRead = readSync(descriptor, buffer, 0, buffer.byteLength, null);
2541
+ if (bytesRead === 0) return;
2542
+ yield bytesRead === buffer.byteLength ? buffer : buffer.subarray(0, bytesRead);
2543
+ }
2544
+ } finally {
2545
+ closeSync(descriptor);
2546
+ }
2547
+ }
2548
+ function spooledFile(path, totalBytes, checksum) {
2549
+ let discarded = false;
2550
+ return {
2551
+ totalBytes,
2552
+ checksum,
2553
+ read: () => readSpoolFile(path),
2554
+ async discard() {
2555
+ if (discarded) return;
2556
+ discarded = true;
2557
+ removeQuietly(path);
2558
+ }
2559
+ };
2560
+ }
2561
+ function openSnapshotSpoolWriter(directory) {
2562
+ const path = spoolFilePath(directory);
2563
+ let descriptor = openSync(path, "wx", OWNER_ONLY_FILE_MODE);
2564
+ let checksumState = crc32Init();
2565
+ let totalBytes = 0;
2566
+ const close = () => {
2567
+ if (descriptor === null) return;
2568
+ const closing = descriptor;
2569
+ descriptor = null;
2570
+ closeSync(closing);
2571
+ };
2572
+ return {
2573
+ write(bytes) {
2574
+ if (descriptor === null) {
2575
+ throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "The snapshot spool file is already closed", {
2576
+ path
2577
+ });
2578
+ }
2579
+ let offset = 0;
2580
+ while (offset < bytes.byteLength) {
2581
+ offset += writeSync(descriptor, bytes, offset, bytes.byteLength - offset);
2582
+ }
2583
+ checksumState = crc32Update(checksumState, bytes);
2584
+ totalBytes += bytes.byteLength;
2585
+ },
2586
+ checksum() {
2587
+ return crc32Final(checksumState);
2588
+ },
2589
+ finish() {
2590
+ close();
2591
+ return spooledFile(path, totalBytes, crc32Final(checksumState));
2592
+ },
2593
+ discard() {
2594
+ try {
2595
+ close();
2596
+ } finally {
2597
+ removeQuietly(path);
2598
+ }
2599
+ }
2600
+ };
2601
+ }
2602
+ function removeQuietly(path) {
2603
+ try {
2604
+ rmSync(path, { force: true });
2605
+ } catch (_) {
2606
+ }
2607
+ }
2608
+ async function spoolSnapshotStream(directory, stream) {
2609
+ const reader = stream.getReader();
2610
+ let writer = null;
2611
+ try {
2612
+ writer = openSnapshotSpoolWriter(directory);
2613
+ for (; ; ) {
2614
+ const step = await reader.read();
2615
+ if (step.done) break;
2616
+ if (step.value.byteLength > 0) writer.write(step.value);
2617
+ }
2618
+ const spooled = writer.finish();
2619
+ writer = null;
2620
+ return spooled;
2621
+ } catch (err) {
2622
+ writer?.discard();
2623
+ await reader.cancel().catch(() => void 0);
2624
+ throw err;
2625
+ } finally {
2626
+ reader.releaseLock();
2627
+ }
2628
+ }
2629
+ async function openSnapshotSpoolDirectory(durabilityDirectory) {
2630
+ const path = durabilityDirectory === void 0 ? await mkdtemp(join(tmpdir(), TEMPORARY_SPOOL_DIRECTORY_PREFIX)) : join(durabilityDirectory, DURABLE_SPOOL_DIRECTORY);
2631
+ if (durabilityDirectory !== void 0) {
2632
+ await rm(path, { recursive: true, force: true });
2633
+ await mkdir(path, { recursive: true, mode: OWNER_ONLY_DIRECTORY_MODE });
2634
+ }
2635
+ return { path, dispose: () => rm(path, { recursive: true, force: true }) };
2636
+ }
2519
2637
 
2520
2638
  // src/distribution/cluster-node/local-index-gone.ts
2521
2639
  function localIndexIsGone(err) {
@@ -2583,6 +2701,19 @@ function fail(state, code, message, details = {}) {
2583
2701
  return;
2584
2702
  }
2585
2703
  state.failure = { ok: false, code, message, details };
2704
+ releaseSpool(state);
2705
+ }
2706
+ function releaseSpool(state) {
2707
+ const spool = state.spool;
2708
+ if (spool === null) return;
2709
+ state.spool = null;
2710
+ spool.discard();
2711
+ }
2712
+ function spoolFailure(state, err) {
2713
+ const cause = err instanceof Error ? err.message : String(err);
2714
+ fail(state, ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, `the snapshot could not be written to local storage: ${cause}`, {
2715
+ cause
2716
+ });
2586
2717
  }
2587
2718
  function applyErrorEnvelope(state, record) {
2588
2719
  const code = typeof record.code === "string" && record.code.length > 0 ? record.code : "UNKNOWN_ERROR";
@@ -2633,19 +2764,17 @@ function applyStartPayload(state, record) {
2633
2764
  fail(state, ErrorCodes.SNAPSHOT_SYNC_HEADER_INVALID, "totalBytes must be a non-negative integer");
2634
2765
  return;
2635
2766
  }
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
- });
2767
+ try {
2768
+ state.spool = openSnapshotSpoolWriter(state.expectation.spoolDirectory);
2769
+ } catch (err) {
2770
+ spoolFailure(state, err);
2641
2771
  return;
2642
2772
  }
2643
2773
  state.header = header;
2644
2774
  state.totalBytes = start.totalBytes;
2645
- state.assembled = new Uint8Array(start.totalBytes);
2646
2775
  }
2647
2776
  function applyChunkPayload(state, payload) {
2648
- if (state.totalBytes === null || state.assembled === null || state.header === null) {
2777
+ if (state.totalBytes === null || state.header === null) {
2649
2778
  fail(state, ErrorCodes.SNAPSHOT_SYNC_FRAME_INVALID, "received chunk before SNAPSHOT_START");
2650
2779
  return;
2651
2780
  }
@@ -2699,7 +2828,16 @@ function applyChunkPayload(state, payload) {
2699
2828
  });
2700
2829
  return;
2701
2830
  }
2702
- state.assembled.set(payload.data, payload.offset);
2831
+ if (state.spool === null) {
2832
+ fail(state, ErrorCodes.SNAPSHOT_SYNC_FRAME_INVALID, "received chunk after the snapshot stream closed");
2833
+ return;
2834
+ }
2835
+ try {
2836
+ state.spool.write(payload.data);
2837
+ } catch (err) {
2838
+ spoolFailure(state, err);
2839
+ return;
2840
+ }
2703
2841
  state.accumulatedBytes = nextBytes;
2704
2842
  state.expectedNextOffset = payload.offset + payload.data.byteLength;
2705
2843
  }
@@ -2777,7 +2915,7 @@ function createSnapshotStreamState(expectation) {
2777
2915
  expectation,
2778
2916
  header: null,
2779
2917
  totalBytes: null,
2780
- assembled: null,
2918
+ spool: null,
2781
2919
  accumulatedBytes: 0,
2782
2920
  expectedNextOffset: 0,
2783
2921
  endPayload: null,
@@ -2832,11 +2970,20 @@ function handleIncomingSnapshotRecord(state, record) {
2832
2970
  }
2833
2971
  fail(state, ErrorCodes.SNAPSHOT_SYNC_FRAME_INVALID, "unrecognised snapshot frame shape");
2834
2972
  }
2973
+ function abandonSnapshotStream(state) {
2974
+ fail(state, ErrorCodes.SNAPSHOT_SYNC_ABORTED, "the snapshot stream was abandoned before it finished");
2975
+ releaseSpool(state);
2976
+ }
2835
2977
  function finalizeSnapshotStream(state) {
2978
+ const result = settleSnapshotStream(state);
2979
+ if (!result.ok) releaseSpool(state);
2980
+ return result;
2981
+ }
2982
+ function settleSnapshotStream(state) {
2836
2983
  if (state.failure !== null) {
2837
2984
  return state.failure;
2838
2985
  }
2839
- if (state.header === null || state.totalBytes === null || state.assembled === null) {
2986
+ if (state.header === null || state.totalBytes === null) {
2840
2987
  return {
2841
2988
  ok: false,
2842
2989
  code: ErrorCodes.SNAPSHOT_SYNC_CHUNK_MISSING,
@@ -2868,9 +3015,28 @@ function finalizeSnapshotStream(state) {
2868
3015
  }
2869
3016
  };
2870
3017
  }
3018
+ const spool = state.spool;
3019
+ if (spool === null) {
3020
+ return {
3021
+ ok: false,
3022
+ code: ErrorCodes.SNAPSHOT_SYNC_FRAME_INVALID,
3023
+ message: "the snapshot stream was already settled",
3024
+ details: { indexName: state.expectation.indexName }
3025
+ };
3026
+ }
3027
+ const computed = spool.checksum();
3028
+ if (computed !== state.header.checksum) {
3029
+ return {
3030
+ ok: false,
3031
+ code: ErrorCodes.SNAPSHOT_SYNC_CHECKSUM_MISMATCH,
3032
+ message: "computed checksum does not match header checksum",
3033
+ details: { computed, header: state.header.checksum }
3034
+ };
3035
+ }
3036
+ state.spool = null;
2871
3037
  return {
2872
3038
  ok: true,
2873
- bytes: state.assembled,
3039
+ snapshot: spool.finish(),
2874
3040
  header: state.header,
2875
3041
  end: state.endPayload
2876
3042
  };
@@ -3081,7 +3247,11 @@ async function fetchSnapshotFromTarget(indexName, partitionId, target, deadline,
3081
3247
  requestId: generateId(),
3082
3248
  payload: encode(requestPayload)
3083
3249
  };
3084
- const streamState = createSnapshotStreamState({ indexName, partitionId: null });
3250
+ const streamState = createSnapshotStreamState({
3251
+ indexName,
3252
+ partitionId: null,
3253
+ spoolDirectory: deps.spoolDirectory ?? defaultSpoolDirectory()
3254
+ });
3085
3255
  const remainingMs = deadline - Date.now();
3086
3256
  if (remainingMs <= 0) {
3087
3257
  return {
@@ -3097,6 +3267,7 @@ async function fetchSnapshotFromTarget(indexName, partitionId, target, deadline,
3097
3267
  });
3098
3268
  await withDeadline(streamWork, remainingMs, indexName, "stream");
3099
3269
  } catch (err) {
3270
+ abandonSnapshotStream(streamState);
3100
3271
  if (err instanceof NarsilError && err.code === ErrorCodes.SNAPSHOT_SYNC_TIMEOUT) {
3101
3272
  return toStreamFailure(err, ErrorCodes.SNAPSHOT_SYNC_TIMEOUT, "snapshot stream deadline exceeded");
3102
3273
  }
@@ -3105,20 +3276,7 @@ async function fetchSnapshotFromTarget(indexName, partitionId, target, deadline,
3105
3276
  }
3106
3277
  return toStreamFailure(err, ErrorCodes.SNAPSHOT_SYNC_PRIMARY_ERROR, "snapshot fetch raised non-transport error");
3107
3278
  }
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;
3279
+ return finalizeSnapshotStream(streamState);
3122
3280
  }
3123
3281
  async function withDeadline(work, ms, indexName, phase) {
3124
3282
  let timer = null;
@@ -3267,9 +3425,9 @@ function surfaceAborted(deps, indexName, primaryNodeId) {
3267
3425
  })
3268
3426
  );
3269
3427
  }
3270
- async function executeEngineRestore(engine, indexName, primaryNodeId, bytes, remainingMs) {
3428
+ async function executeEngineRestore(engine, indexName, primaryNodeId, source, remainingMs) {
3271
3429
  try {
3272
- await withDeadline(engine.restore(indexName, bytes), remainingMs, indexName, "restore");
3430
+ await withDeadline(engine.restore(indexName, source), remainingMs, indexName, "restore");
3273
3431
  return null;
3274
3432
  } catch (err) {
3275
3433
  if (err instanceof NarsilError && err.code === ErrorCodes.SNAPSHOT_SYNC_TIMEOUT) {
@@ -3555,9 +3713,9 @@ function validateSyncEntryForApply(entry, indexName, partitionId, expectedSeqNo,
3555
3713
  }
3556
3714
  return null;
3557
3715
  }
3558
- function createLiveSyncFrameState(indexName, partitionId) {
3716
+ function createLiveSyncFrameState(indexName, partitionId, spoolDirectory) {
3559
3717
  return {
3560
- snapshotState: createSnapshotStreamState({ indexName, partitionId }),
3718
+ snapshotState: createSnapshotStreamState({ indexName, partitionId, spoolDirectory }),
3561
3719
  syncEntries: [],
3562
3720
  sawSnapshotFrame: false,
3563
3721
  error: null
@@ -3659,35 +3817,25 @@ async function applyLiveIncrementalSync(indexName, partitionId, target, entriesP
3659
3817
  async function applyLiveSnapshotSync(state, entry, indexName, partitionId, target, frameState, coordinatorConfig, partitionCount, deadline, deps) {
3660
3818
  const finalized = finalizeSnapshotStream(frameState.snapshotState);
3661
3819
  if (!finalized.ok) {
3662
- return { ok: false, error: streamFailureToError(finalized) };
3820
+ return { ok: false, error: streamFailureToError({ ...finalized, details: { ...finalized.details, target } }) };
3663
3821
  }
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
- };
3822
+ let restoreError;
3823
+ try {
3824
+ restoreError = await restoreLiveSnapshotPartition(
3825
+ state,
3826
+ entry,
3827
+ indexName,
3828
+ partitionId,
3829
+ target,
3830
+ finalized.snapshot.read(),
3831
+ coordinatorConfig,
3832
+ partitionCount,
3833
+ deadline,
3834
+ deps
3835
+ );
3836
+ } finally {
3837
+ await finalized.snapshot.discard();
3678
3838
  }
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
3839
  if (restoreError !== null) {
3692
3840
  return { ok: false, error: restoreError };
3693
3841
  }
@@ -3711,7 +3859,7 @@ async function applyLiveSnapshotSync(state, entry, indexName, partitionId, targe
3711
3859
  snapshotHeader: finalized.header
3712
3860
  };
3713
3861
  }
3714
- async function restoreLiveSnapshotPartition(state, entry, indexName, partitionId, primaryNodeId, bytes, coordinatorConfig, partitionCount, deadline, deps) {
3862
+ async function restoreLiveSnapshotPartition(state, entry, indexName, partitionId, primaryNodeId, source, coordinatorConfig, partitionCount, deadline, deps) {
3715
3863
  const remainingMs = deadline - Date.now();
3716
3864
  if (remainingMs <= 0) {
3717
3865
  return new NarsilError(
@@ -3735,7 +3883,7 @@ async function restoreLiveSnapshotPartition(state, entry, indexName, partitionId
3735
3883
  }
3736
3884
  try {
3737
3885
  await withDeadline(
3738
- deps.restoreReplicationPartition(indexName, partitionId, bytes, coordinatorConfig, partitionCount),
3886
+ deps.restoreReplicationPartition(indexName, partitionId, source, coordinatorConfig, partitionCount),
3739
3887
  remainingMs,
3740
3888
  indexName,
3741
3889
  "partition-restore"
@@ -3927,6 +4075,27 @@ async function syncFromAnyTarget(state, entry, indexName, partitionId, primaryNo
3927
4075
  return { ok: false, error: lastError };
3928
4076
  }
3929
4077
  async function syncFromTarget(state, entry, indexName, partitionId, target, coordinatorConfig, partitionCount, deadline, deps, abortCheck) {
4078
+ const spoolDirectory = deps.snapshotSpoolDirectory ?? defaultSpoolDirectory();
4079
+ const frameState = createLiveSyncFrameState(indexName, partitionId, spoolDirectory);
4080
+ try {
4081
+ return await syncFrameStateFromTarget(
4082
+ state,
4083
+ entry,
4084
+ indexName,
4085
+ partitionId,
4086
+ target,
4087
+ coordinatorConfig,
4088
+ partitionCount,
4089
+ deadline,
4090
+ deps,
4091
+ abortCheck,
4092
+ frameState
4093
+ );
4094
+ } finally {
4095
+ abandonSnapshotStream(frameState.snapshotState);
4096
+ }
4097
+ }
4098
+ async function syncFrameStateFromTarget(state, entry, indexName, partitionId, target, coordinatorConfig, partitionCount, deadline, deps, abortCheck, frameState) {
3930
4099
  const logState = readLocalLogState(indexName, partitionId, deps);
3931
4100
  const requestPayload = {
3932
4101
  indexName,
@@ -3940,7 +4109,6 @@ async function syncFromTarget(state, entry, indexName, partitionId, target, coor
3940
4109
  requestId: generateId(),
3941
4110
  payload: encode(requestPayload)
3942
4111
  };
3943
- const frameState = createLiveSyncFrameState(indexName, partitionId);
3944
4112
  const remainingMs = deadline - Date.now();
3945
4113
  if (remainingMs <= 0) {
3946
4114
  return {
@@ -4172,7 +4340,7 @@ async function fetchSchemaAndPrepare(entry, indexName, primaryNodeId, deps) {
4172
4340
  }
4173
4341
  return configResult.config.schema;
4174
4342
  }
4175
- async function applyRestore(state, entry, indexName, primaryNodeId, bytes, coordinatorSchema, deadline, deps) {
4343
+ async function applyRestore(state, entry, indexName, primaryNodeId, source, coordinatorSchema, deadline, deps) {
4176
4344
  const remainingMs = deadline - Date.now();
4177
4345
  if (remainingMs <= 0) {
4178
4346
  surfaceError(
@@ -4191,7 +4359,7 @@ async function applyRestore(state, entry, indexName, primaryNodeId, bytes, coord
4191
4359
  surfaceAborted(deps, indexName, primaryNodeId);
4192
4360
  return false;
4193
4361
  }
4194
- const restoreError = await executeEngineRestore(deps.engine, indexName, primaryNodeId, bytes, remainingMs);
4362
+ const restoreError = await executeEngineRestore(deps.engine, indexName, primaryNodeId, source, remainingMs);
4195
4363
  if (restoreError !== null) {
4196
4364
  surfaceError(deps, indexName, primaryNodeId, restoreError);
4197
4365
  return false;
@@ -4312,7 +4480,11 @@ async function executeBootstrapSync(state, entry, indexName, partitionId, primar
4312
4480
  surfaceError(deps, indexName, primaryNodeId, targetsResult);
4313
4481
  return false;
4314
4482
  }
4315
- const fetchDeps = { transport: deps.transport, sourceNodeId: deps.sourceNodeId };
4483
+ const fetchDeps = {
4484
+ transport: deps.transport,
4485
+ sourceNodeId: deps.sourceNodeId,
4486
+ spoolDirectory: deps.snapshotSpoolDirectory
4487
+ };
4316
4488
  const fetchResult = await fetchSnapshotFromAnyTarget(
4317
4489
  indexName,
4318
4490
  primaryNodeId,
@@ -4339,20 +4511,25 @@ async function executeBootstrapSync(state, entry, indexName, partitionId, primar
4339
4511
  );
4340
4512
  return false;
4341
4513
  }
4342
- if (abortCheck()) {
4343
- surfaceAborted(deps, indexName, primaryNodeId);
4344
- return false;
4514
+ let restoreSucceeded;
4515
+ try {
4516
+ if (abortCheck()) {
4517
+ surfaceAborted(deps, indexName, primaryNodeId);
4518
+ return false;
4519
+ }
4520
+ restoreSucceeded = await applyRestore(
4521
+ state,
4522
+ entry,
4523
+ indexName,
4524
+ primaryNodeId,
4525
+ fetchResult.snapshot.read(),
4526
+ schemaResult,
4527
+ deadline,
4528
+ deps
4529
+ );
4530
+ } finally {
4531
+ await fetchResult.snapshot.discard();
4345
4532
  }
4346
- const restoreSucceeded = await applyRestore(
4347
- state,
4348
- entry,
4349
- indexName,
4350
- primaryNodeId,
4351
- fetchResult.bytes,
4352
- schemaResult,
4353
- deadline,
4354
- deps
4355
- );
4356
4533
  if (!restoreSucceeded) {
4357
4534
  return false;
4358
4535
  }
@@ -5216,173 +5393,40 @@ async function applyLocalReplicationEntry(core, entry) {
5216
5393
  }
5217
5394
  }
5218
5395
 
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
- }
5396
+ // src/distribution/cluster-node/replication-index.ts
5397
+ function isNestedSchema(value) {
5398
+ return typeof value === "object" && value !== null;
5274
5399
  }
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
- });
5400
+ function flattenSchemaInto(schema, prefix, flat) {
5401
+ for (const [field, value] of Object.entries(schema)) {
5402
+ const path = prefix.length === 0 ? field : `${prefix}.${field}`;
5403
+ if (isNestedSchema(value)) {
5404
+ flattenSchemaInto(value, path, flat);
5405
+ continue;
5311
5406
  }
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?.();
5407
+ flat[path] = value;
5323
5408
  }
5324
5409
  }
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
- }
5410
+ function flattenSchema(schema) {
5411
+ const flat = {};
5412
+ flattenSchemaInto(schema, "", flat);
5413
+ return flat;
5346
5414
  }
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", {
5415
+ function requireSameFlatSchema(indexName, expected, actual, what) {
5416
+ for (const [field, expectedType] of Object.entries(expected)) {
5417
+ if (actual[field] !== expectedType) {
5418
+ throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, `${what} schema mismatch`, {
5373
5419
  indexName,
5374
- partitionId,
5375
5420
  field,
5376
5421
  expected: expectedType,
5377
- received: partition.schema[field] ?? "(absent)"
5422
+ received: actual[field] ?? "(absent)"
5378
5423
  });
5379
5424
  }
5380
5425
  }
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", {
5426
+ for (const field of Object.keys(actual)) {
5427
+ if (!(field in expected)) {
5428
+ throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, `${what} schema contains extra field`, {
5384
5429
  indexName,
5385
- partitionId,
5386
5430
  field
5387
5431
  });
5388
5432
  }
@@ -5414,7 +5458,12 @@ async function ensureReplicationIndex(core, engine, indexName, coordinatorConfig
5414
5458
  receivedLanguage: language.name
5415
5459
  });
5416
5460
  }
5417
- validateExistingSchema(indexName, coordinatorConfig.schema, entry.config.schema);
5461
+ requireSameFlatSchema(
5462
+ indexName,
5463
+ flattenSchema(coordinatorConfig.schema),
5464
+ flattenSchema(entry.config.schema),
5465
+ "local index"
5466
+ );
5418
5467
  const manager = core.requireManager(indexName);
5419
5468
  if (manager.partitionCount > partitionCount) {
5420
5469
  throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, "local index has more partitions than allocation", {
@@ -5433,45 +5482,222 @@ async function ensureReplicationIndex(core, engine, indexName, coordinatorConfig
5433
5482
  }
5434
5483
  return { config: entry.config, created };
5435
5484
  }
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)"
5485
+
5486
+ // src/distribution/cluster-node/partition-archive.ts
5487
+ var VECTOR_FILE_ENVELOPE_VERSION = 2;
5488
+ async function archiveReplicationPartition(core, indexName, partitionId, lastSeqNo) {
5489
+ core.guardShutdown();
5490
+ const release = await core.indexState.acquire(indexName, false);
5491
+ try {
5492
+ const entry = core.requireIndex(indexName);
5493
+ const manager = core.requireManager(indexName);
5494
+ let captured = 0;
5495
+ const checkpoint = await buildArchiveFromMemory({
5496
+ indexName,
5497
+ metadata: indexMetadataOf(indexName, entry, manager.partitionCount),
5498
+ manager,
5499
+ vectorIndexes: manager.getVectorIndexes(),
5500
+ appliedSeqNo: () => {
5501
+ captured = lastSeqNo();
5502
+ return captured;
5503
+ },
5504
+ partitionId
5505
+ });
5506
+ const config = await encodeIndexConfiguration(entry);
5507
+ return { stream: archiveStream({ ...checkpoint, config, sourceIndexName: indexName }), lastSeqNo: captured };
5508
+ } finally {
5509
+ release();
5510
+ }
5511
+ }
5512
+ function restoreFailed(message, details) {
5513
+ throw new NarsilError(ErrorCodes.SNAPSHOT_SYNC_RESTORE_FAILED, message, details);
5514
+ }
5515
+ function requireRestoreTarget(indexName, partitionId, partitionCount) {
5516
+ if (!Number.isInteger(partitionId) || partitionId < 0) {
5517
+ restoreFailed("partitionId must be a non-negative integer", { indexName, partitionId });
5518
+ }
5519
+ if (!Number.isInteger(partitionCount) || partitionCount <= 0 || partitionCount > MAX_PARTITION_COUNT) {
5520
+ restoreFailed(`partitionCount must be an integer between 1 and ${MAX_PARTITION_COUNT}`, {
5521
+ indexName,
5522
+ partitionCount
5523
+ });
5524
+ }
5525
+ if (partitionId >= partitionCount) {
5526
+ restoreFailed("partitionId is outside partitionCount", { indexName, partitionId, partitionCount });
5527
+ }
5528
+ }
5529
+ async function receiveArchive(indexName, partitionId, source, schema) {
5530
+ const store = memoryCheckpointStore();
5531
+ let language = "";
5532
+ let received = null;
5533
+ await readSnapshotArchive(createByteReader(source), {
5534
+ config: async (bytes) => {
5535
+ const { payload } = await openChecksummedEnvelope(bytes, {
5536
+ envelopeFormatVersion: 5,
5537
+ what: "index configuration",
5538
+ allowsCompression: true
5446
5539
  });
5540
+ const raw = decode(payload);
5541
+ if (typeof raw.language !== "string") restoreFailed("partition archive names no language", { indexName });
5542
+ language = raw.language;
5543
+ },
5544
+ manifest: async (manifest, sourceName) => {
5545
+ const listed = manifest.partitions.map((partition) => partition.partitionId);
5546
+ if (listed.length !== 1 || listed[0] !== partitionId) {
5547
+ restoreFailed("partition archive lists other partitions", { indexName, partitionId, listed });
5548
+ }
5549
+ if (manifest.vectors.some((field) => field.graphParts.length > 0)) {
5550
+ restoreFailed("partition archive holds a graph", { indexName, partitionId });
5551
+ }
5552
+ requireSameFlatSchema(indexName, schema, manifest.schema, "partition snapshot");
5553
+ const rename = (key) => `${indexName}/${key.slice(sourceName.length + 1)}`;
5554
+ received = {
5555
+ ...manifest,
5556
+ partitions: manifest.partitions.map((partition) => ({
5557
+ ...partition,
5558
+ segments: partition.segments.map((segment) => ({ ...segment, key: rename(segment.key) }))
5559
+ })),
5560
+ vectors: manifest.vectors.map((field) => ({
5561
+ ...field,
5562
+ files: field.files.map((file) => ({ ...file, key: rename(file.key) }))
5563
+ }))
5564
+ };
5565
+ },
5566
+ file: async (name, _ref, bytes) => store.write(`${indexName}/${name}`, [bytes])
5567
+ });
5568
+ if (received === null) restoreFailed("partition archive holds no manifest", { indexName, partitionId });
5569
+ return { language, manifest: received, store };
5570
+ }
5571
+ function emptyPartition(manager, partitionId) {
5572
+ return {
5573
+ indexName: manager.indexName,
5574
+ partitionId,
5575
+ totalPartitions: manager.partitionCount,
5576
+ language: manager.language.name,
5577
+ schema: {},
5578
+ docCount: 0,
5579
+ avgDocLength: 0,
5580
+ documents: {},
5581
+ invertedIndex: {},
5582
+ fieldIndexes: { numeric: {}, boolean: {}, enum: {}, geopoint: {} },
5583
+ statistics: { totalDocuments: 0, totalFieldLengths: {}, averageFieldLengths: {}, docFrequencies: {} }
5584
+ };
5585
+ }
5586
+ async function replacePartition(manager, partitionId, archive) {
5587
+ const vectorIndexes = manager.getVectorIndexes();
5588
+ const held = [...manager.getPartition(partitionId).docIds()];
5589
+ for (const vectorIndex of vectorIndexes.values()) {
5590
+ for (const docId of held) if (vectorIndex.has(docId)) vectorIndex.remove(docId);
5591
+ }
5592
+ manager.deserializePartition(partitionId, emptyPartition(manager, partitionId));
5593
+ const entry = archive.manifest.partitions[0];
5594
+ if (entry !== void 0) {
5595
+ await loadPartitionSegments(archive.store, entry, manager, {
5596
+ schema: archive.manifest.schema,
5597
+ decodesEveryDocument: true
5598
+ });
5599
+ }
5600
+ for (const field of archive.manifest.vectors) {
5601
+ const vectorIndex = vectorIndexes.get(field.fieldPath);
5602
+ if (vectorIndex === void 0) continue;
5603
+ for (const file of field.files) {
5604
+ const bytes = await readListedFile(archive.store, file.key, { bytes: file.bytes, what: "vector file" });
5605
+ const { payload } = await openChecksummedEnvelope(bytes, {
5606
+ envelopeFormatVersion: VECTOR_FILE_ENVELOPE_VERSION,
5607
+ what: `vector file "${file.key}"`,
5608
+ allowsCompression: false
5609
+ });
5610
+ const decoded = decodeVectorFilePayload(decode(payload));
5611
+ const vectors = bytesToVectors(decoded.vectors);
5612
+ for (let i = 0; i < decoded.docIds.length; i++) {
5613
+ if (isDead(file.dead, i)) continue;
5614
+ const vector = vectors.slice(i * decoded.dimension, (i + 1) * decoded.dimension);
5615
+ vectorIndex.insert(decoded.docIds[i], vector, partitionId);
5616
+ }
5447
5617
  }
5618
+ vectorIndex.scheduleBuild();
5448
5619
  }
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", {
5620
+ }
5621
+ async function restoreReplicationPartition(core, engine, indexName, partitionId, source, indexConfig, partitionCount) {
5622
+ core.guardShutdown();
5623
+ requireRestoreTarget(indexName, partitionId, partitionCount);
5624
+ const archive = await receiveArchive(indexName, partitionId, source, flattenSchema(indexConfig.schema));
5625
+ let release = core.indexRegistry.has(indexName) ? await core.indexState.acquire(indexName, false) : null;
5626
+ try {
5627
+ const restoreIndex = await ensureReplicationIndex(
5628
+ core,
5629
+ engine,
5630
+ indexName,
5631
+ indexConfig,
5632
+ archive.language,
5633
+ partitionCount
5634
+ );
5635
+ if (release === null) release = await core.indexState.acquire(indexName, false);
5636
+ if (restoreIndex.created) {
5637
+ await core.orchestrator.replicateToWorkers({
5638
+ type: "createIndex",
5452
5639
  indexName,
5453
- field
5640
+ config: restoreIndex.config,
5641
+ requestId: `replicate-partition-index-${indexName}`
5454
5642
  });
5455
5643
  }
5644
+ const wasScaledOut = core.orchestrator.desyncIndex(indexName);
5645
+ try {
5646
+ await replacePartition(core.requireManager(indexName), partitionId, archive);
5647
+ } finally {
5648
+ await core.orchestrator.resyncIndex(indexName, wasScaledOut);
5649
+ }
5650
+ } finally {
5651
+ release?.();
5456
5652
  }
5457
5653
  }
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;
5654
+
5655
+ // src/distribution/cluster-node/local-engine.ts
5656
+ async function createClusterLocalEngine(config, hooks) {
5657
+ requireKnownConfig(config);
5658
+ const runnableConfig = await resolveRunnableConfig(config);
5659
+ const core = createEngineCore(runnableConfig, hooks, "partition");
5660
+ await startEngineCore(core, runnableConfig);
5661
+ core.orchestrator.shareMainThread();
5662
+ const engine = createNarsilFromCore(core, runnableConfig);
5663
+ const heldPartitions = createHeldPartitionRecord(core);
5664
+ return Object.assign(engine, {
5665
+ createIndex: (name, indexConfig) => createEngineIndex(core, runnableConfig, name, indexConfig, { adapterBinding: "deferred" }),
5666
+ createIndexWithUuid: (name, indexConfig, indexUuid) => createEngineIndex(core, runnableConfig, name, indexConfig, { indexUuid }),
5667
+ acquireIndexForReplication: (indexName) => core.indexState.acquire(indexName, false),
5668
+ indexUuidOf: (indexName) => core.indexRegistry.get(indexName)?.indexUuid,
5669
+ stampIndexUuid: (indexName, indexUuid) => stampIndexUuid(core, indexName, indexUuid),
5670
+ highestPersistedSeqNoOf: (indexName, partitionId) => core.durability?.manager.highestPersistedSeqNo(indexName, partitionId) ?? 0,
5671
+ heldPartitionsOf: (indexName) => heldPartitions.held(indexName),
5672
+ recordHeldPartition: (indexName, partitionId) => heldPartitions.record(indexName, partitionId),
5673
+ forgetHeldPartition: (indexName, partitionId) => heldPartitions.forget(indexName, partitionId),
5674
+ applyReplicationEntry: (entry) => applyLocalReplicationEntry(core, entry),
5675
+ archiveReplicationPartition: (indexName, partitionId, lastSeqNo) => archiveReplicationPartition(core, indexName, partitionId, lastSeqNo),
5676
+ restoreReplicationPartition: (indexName, partitionId, source, indexConfig, partitionCount) => restoreReplicationPartition(core, engine, indexName, partitionId, source, indexConfig, partitionCount),
5677
+ queryPartitions: (indexName, params, partitionIds, globalStats) => runEngineQuery(core, indexName, params, { partitionIds, globalStats }),
5678
+ preflightPartitions: (indexName, params, partitionIds) => runEnginePreflight(core, indexName, params, { partitionIds }),
5679
+ suggestPartitions: (indexName, params, partitionIds) => runEngineSuggest(core, indexName, params, partitionIds),
5680
+ listPartitions: (indexName, params, partitionIds) => runEngineListDocuments(core, indexName, params, partitionIds),
5681
+ collectQueryStats: (indexName, terms, partitionIds) => runEngineQueryStats(core, indexName, terms, partitionIds),
5682
+ partitionStatsForRead: async (indexName) => {
5683
+ const release = await core.indexState.acquire(indexName);
5684
+ try {
5685
+ return core.requireManager(indexName).getPartitionStats();
5686
+ } finally {
5687
+ release();
5688
+ }
5469
5689
  }
5470
- flat[path] = value;
5471
- }
5690
+ });
5472
5691
  }
5473
- function isNestedSchema(value) {
5474
- return typeof value === "object" && value !== null;
5692
+ async function stampIndexUuid(core, indexName, indexUuid) {
5693
+ const entry = core.indexRegistry.get(indexName);
5694
+ if (entry === void 0 || entry.indexUuid === indexUuid) {
5695
+ return;
5696
+ }
5697
+ core.indexRegistry.set(indexName, { ...entry, indexUuid });
5698
+ if (core.durability !== null) {
5699
+ await core.durability.manager.persistMetadata(indexName);
5700
+ }
5475
5701
  }
5476
5702
 
5477
5703
  // src/distribution/cluster-node/local-index-reconcile.ts
@@ -5584,6 +5810,25 @@ async function authorizeSnapshotRequest(coordinator, indexName, sourceId) {
5584
5810
  }
5585
5811
 
5586
5812
  // src/distribution/cluster-node/snapshot-cache.ts
5813
+ function soleSnapshotLease(build) {
5814
+ return { build, release: () => build.discard() };
5815
+ }
5816
+ function sharedLease(entry, build) {
5817
+ let released = false;
5818
+ return {
5819
+ build,
5820
+ async release() {
5821
+ if (released) return;
5822
+ released = true;
5823
+ entry.holders -= 1;
5824
+ if (entry.holders === 0) await build.discard();
5825
+ }
5826
+ };
5827
+ }
5828
+ async function joinSnapshotBuild(entry) {
5829
+ entry.holders += 1;
5830
+ return sharedLease(entry, await entry.promise);
5831
+ }
5587
5832
  function createSnapshotCacheState(maxConcurrent = DEFAULT_MAX_CONCURRENT_SNAPSHOTS, maxPerSource = DEFAULT_MAX_PER_SOURCE_SNAPSHOTS, maxStreamsPerIndex = DEFAULT_MAX_STREAMS_PER_INDEX) {
5588
5833
  if (!Number.isInteger(maxConcurrent) || maxConcurrent <= 0) {
5589
5834
  throw new NarsilError(ErrorCodes.CONFIG_INVALID, "maxConcurrent must be a positive integer", {
@@ -5671,7 +5916,7 @@ function releaseStreamSlot(state, handle) {
5671
5916
  async function acquireSnapshotBuild(state, indexName, builder) {
5672
5917
  const existing = state.cache.get(indexName);
5673
5918
  if (existing !== void 0) {
5674
- return existing.promise;
5919
+ return joinSnapshotBuild(existing);
5675
5920
  }
5676
5921
  if (state.active >= state.maxConcurrent) {
5677
5922
  throw new NarsilError(
@@ -5694,7 +5939,7 @@ async function acquireSnapshotBuild(state, indexName, builder) {
5694
5939
  state.active = Math.max(0, state.active - 1);
5695
5940
  };
5696
5941
  const rawBuild = (async () => builder())();
5697
- const entry = { promise: rawBuild };
5942
+ const entry = { promise: rawBuild, holders: 0 };
5698
5943
  entry.promise = rawBuild.then(
5699
5944
  (result) => {
5700
5945
  clearEntry(entry);
@@ -5706,7 +5951,7 @@ async function acquireSnapshotBuild(state, indexName, builder) {
5706
5951
  }
5707
5952
  );
5708
5953
  state.cache.set(indexName, entry);
5709
- return entry.promise;
5954
+ return joinSnapshotBuild(entry);
5710
5955
  }
5711
5956
  var SNAPSHOT_SYNC_ERROR_TYPE = `${ReplicationMessageTypes.SNAPSHOT_SYNC_REQUEST}.error`;
5712
5957
  function createSingleResponseSink(respond) {
@@ -5720,8 +5965,7 @@ function createSingleResponseSink(respond) {
5720
5965
  return sink;
5721
5966
  }
5722
5967
  async function streamSnapshotToReplica(sink, nodeId, requestId, indexName, build, metadata, options = {}) {
5723
- const snapshotBytes = build.bytes;
5724
- const totalBytes = snapshotBytes.byteLength;
5968
+ const totalBytes = build.totalBytes;
5725
5969
  const checksum = build.checksum;
5726
5970
  const header = {
5727
5971
  lastSeqNo: metadata.lastSeqNo,
@@ -5737,22 +5981,23 @@ async function streamSnapshotToReplica(sink, nodeId, requestId, indexName, build
5737
5981
  await yieldToEventLoop();
5738
5982
  let offset = 0;
5739
5983
  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();
5984
+ for await (const source of build.read()) {
5985
+ for (let from = 0; from < source.byteLength; from += SNAPSHOT_CHUNK_SIZE) {
5986
+ const chunk = source.subarray(from, Math.min(from + SNAPSHOT_CHUNK_SIZE, source.byteLength));
5987
+ const chunkPayload = {
5988
+ partitionId: metadata.partitionId,
5989
+ indexName,
5990
+ offset,
5991
+ data: chunk
5992
+ };
5993
+ const chunkBytes = encode(chunkPayload);
5994
+ assertMessageWithinLimit(chunkBytes, "SNAPSHOT_CHUNK");
5995
+ await respondMessage(sink, ReplicationMessageTypes.SNAPSHOT_CHUNK, nodeId, requestId, chunkBytes);
5996
+ offset += chunk.byteLength;
5997
+ if (offset < totalBytes && now2() - lastYieldAt >= SNAPSHOT_CHUNK_YIELD_INTERVAL_MS) {
5998
+ await yieldToEventLoop();
5999
+ lastYieldAt = now2();
6000
+ }
5756
6001
  }
5757
6002
  }
5758
6003
  await yieldToEventLoop();
@@ -5833,6 +6078,26 @@ async function defaultSnapshotHeaderMetadataProvider(coordinator, indexName) {
5833
6078
  lastSeqNo: SNAPSHOT_HEADER_SENTINEL_SEQNO
5834
6079
  };
5835
6080
  }
6081
+ async function nodeSnapshotHeaderMetadata(deps, indexName, partitionId) {
6082
+ const fallback = await defaultSnapshotHeaderMetadataProvider(deps.coordinator, indexName);
6083
+ if (partitionId === null) {
6084
+ return fallback;
6085
+ }
6086
+ const localIndex = deps.engine.listIndexes().find((index) => index.name === indexName);
6087
+ const release = localIndex !== void 0 ? await deps.engine.acquireIndexForReplication(indexName) : null;
6088
+ try {
6089
+ const allocation = await deps.coordinator.getAllocation(indexName);
6090
+ const assignment = allocation?.assignments.get(partitionId);
6091
+ const log = deps.getReplicationLog(indexName, partitionId);
6092
+ return {
6093
+ partitionId,
6094
+ primaryTerm: assignment?.primaryTerm ?? fallback.primaryTerm,
6095
+ lastSeqNo: log.newestSeqNo ?? 0
6096
+ };
6097
+ } finally {
6098
+ release?.();
6099
+ }
6100
+ }
5836
6101
  async function resolveHeaderMetadata(deps, indexName, partitionId) {
5837
6102
  const provider = deps.resolveHeaderMetadata;
5838
6103
  if (provider === void 0) {
@@ -5862,55 +6127,56 @@ async function acquireAndStream(message, sink, request, deps, options) {
5862
6127
  return;
5863
6128
  }
5864
6129
  try {
5865
- let build;
6130
+ let lease;
5866
6131
  try {
5867
6132
  const buildForRequest = async () => buildSnapshotForRequest(deps, indexName, options);
5868
- build = options.disableBuildCache === true ? await buildForRequest() : await acquireSnapshotBuild(deps.state, indexName, buildForRequest);
6133
+ lease = options.disableBuildCache === true ? soleSnapshotLease(await buildForRequest()) : await acquireSnapshotBuild(deps.state, indexName, buildForRequest);
5869
6134
  } catch (err) {
5870
6135
  const code = err instanceof NarsilError ? err.code : ErrorCodes.SNAPSHOT_SYNC_SNAPSHOT_FAILED;
5871
6136
  const errMessage = err instanceof Error ? err.message : String(err);
5872
6137
  await respondError(sink, deps.nodeId, message.requestId, code, errMessage);
5873
6138
  return;
5874
6139
  }
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
6140
  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
- }
6141
+ await streamLeasedSnapshot(message, sink, request, deps, options, lease.build);
5903
6142
  } finally {
5904
- releaseStreamSlot(deps.state, streamHandle);
6143
+ await lease.release();
5905
6144
  }
5906
6145
  } finally {
5907
6146
  releaseSourceSlot(deps.state, sourceHandle);
5908
6147
  }
5909
6148
  }
6149
+ async function streamLeasedSnapshot(message, sink, request, deps, options, build) {
6150
+ const { indexName } = request;
6151
+ let streamHandle;
6152
+ try {
6153
+ streamHandle = acquireStreamSlot(deps.state, indexName);
6154
+ } catch (err) {
6155
+ const code = err instanceof NarsilError ? err.code : ErrorCodes.SNAPSHOT_SYNC_CAPACITY_EXHAUSTED;
6156
+ const errMessage = err instanceof Error ? err.message : String(err);
6157
+ await respondError(sink, deps.nodeId, message.requestId, code, errMessage);
6158
+ return;
6159
+ }
6160
+ try {
6161
+ const resolved = options.metadata ?? await resolveHeaderMetadata(deps, indexName, request.partitionId ?? null);
6162
+ const metadata = build.lastSeqNo === void 0 ? resolved : { ...resolved, lastSeqNo: build.lastSeqNo };
6163
+ await streamSnapshotToReplica(sink, deps.nodeId, message.requestId, indexName, build, metadata, {
6164
+ closeOnEnd: options.closeOnEnd
6165
+ });
6166
+ await options.afterSnapshot?.(sink, build);
6167
+ if (options.closeOnEnd === false) {
6168
+ sink.closed = true;
6169
+ }
6170
+ } finally {
6171
+ releaseStreamSlot(deps.state, streamHandle);
6172
+ }
6173
+ }
5910
6174
  async function buildSnapshot(deps, indexName) {
5911
- let bytes;
5912
6175
  try {
5913
- bytes = await deps.engine.snapshot(indexName);
6176
+ return await spoolSnapshotStream(
6177
+ deps.spoolDirectory ?? defaultSpoolDirectory(),
6178
+ await deps.engine.snapshot(indexName)
6179
+ );
5914
6180
  } catch (err) {
5915
6181
  if (err instanceof NarsilError) {
5916
6182
  throw err;
@@ -5921,8 +6187,6 @@ async function buildSnapshot(deps, indexName) {
5921
6187
  cause: errMessage
5922
6188
  });
5923
6189
  }
5924
- const checksum = bytes.byteLength <= MAX_SNAPSHOT_SIZE_BYTES ? crc32(bytes) : 0;
5925
- return { bytes, checksum };
5926
6190
  }
5927
6191
  function buildSnapshotForRequest(deps, indexName, options) {
5928
6192
  if (options.buildSnapshot !== void 0) {
@@ -6131,7 +6395,6 @@ var MAX_FACET_VALUE_LENGTH = 1024;
6131
6395
  var MAX_GROUPS_PER_RESPONSE = 65536;
6132
6396
  var MAX_FACET_SIZE = 1e3;
6133
6397
  var DEFAULT_MAX_FACET_BUCKETS = 100;
6134
- var MAX_VECTOR_DIMENSION = 8192;
6135
6398
  var MAX_VECTOR_TEXT_LENGTH = 16384;
6136
6399
 
6137
6400
  // src/distribution/query/validators/common.ts
@@ -6867,11 +7130,11 @@ function validateVectorParams(value) {
6867
7130
  );
6868
7131
  }
6869
7132
  const dims = value.value.length;
6870
- if (dims === 0 || dims > MAX_VECTOR_DIMENSION) {
7133
+ if (dims === 0 || dims > MAX_STORED_VECTOR_DIMENSION) {
6871
7134
  throwInvalid(
6872
7135
  CONFIG_INVALID,
6873
- `Invalid SearchPayload: "params.vector.value" length must be between 1 and ${MAX_VECTOR_DIMENSION}`,
6874
- { length: dims, limit: MAX_VECTOR_DIMENSION }
7136
+ `Invalid SearchPayload: "params.vector.value" length must be between 1 and ${MAX_STORED_VECTOR_DIMENSION}`,
7137
+ { length: dims, limit: MAX_STORED_VECTOR_DIMENSION }
6875
7138
  );
6876
7139
  }
6877
7140
  for (let i = 0; i < dims; i++) {
@@ -7062,6 +7325,9 @@ function validateParams(params) {
7062
7325
  if (params.prefix !== null) {
7063
7326
  validateBooleanParam(params.prefix, "prefix");
7064
7327
  }
7328
+ if (params.partialWords !== null && params.partialWords !== void 0) {
7329
+ validateBooleanParam(params.partialWords, "partialWords");
7330
+ }
7065
7331
  if (params.exact !== null) {
7066
7332
  validateBooleanParam(params.exact, "exact");
7067
7333
  }
@@ -7671,6 +7937,7 @@ function wireParamsToLocal(wire, facetShardSize) {
7671
7937
  termMatch: wire.termMatch ?? void 0,
7672
7938
  prefixLength: wire.prefixLength ?? void 0,
7673
7939
  prefix: wire.prefix ?? void 0,
7940
+ partialWords: wire.partialWords ?? void 0,
7674
7941
  exact: wire.exact ?? void 0,
7675
7942
  pinned: wire.pinned !== null ? wire.pinned.map((entry) => ({ docId: entry.docId, position: entry.position })) : void 0,
7676
7943
  mode: wire.mode ?? void 0,
@@ -7766,6 +8033,7 @@ function localParamsToWire(params) {
7766
8033
  termMatch: params.termMatch ?? null,
7767
8034
  prefixLength: params.prefixLength ?? null,
7768
8035
  prefix: params.prefix ?? null,
8036
+ partialWords: params.partialWords ?? null,
7769
8037
  exact: params.exact ?? null,
7770
8038
  pinned: params.pinned !== void 0 ? params.pinned.map((entry) => ({ docId: entry.docId, position: entry.position })) : null,
7771
8039
  mode: params.mode ?? null,
@@ -8073,6 +8341,8 @@ function toWireSortValue2(value) {
8073
8341
  if (typeof value === "boolean") return value;
8074
8342
  return null;
8075
8343
  }
8344
+
8345
+ // src/distribution/replication/sync-primary.ts
8076
8346
  function decideSyncTier(log, replicaLastSeqNo) {
8077
8347
  const oldest = log.oldestSeqNo;
8078
8348
  if (oldest === void 0) {
@@ -8194,7 +8464,8 @@ async function handleSyncRequestMessage(message, respond, deps) {
8194
8464
  engine: deps.engine,
8195
8465
  coordinator: deps.coordinator,
8196
8466
  state: deps.snapshotSyncState,
8197
- resolveHeaderMetadata: deps.resolveHeaderMetadata
8467
+ resolveHeaderMetadata: deps.resolveHeaderMetadata,
8468
+ spoolDirectory: deps.snapshotSpoolDirectory
8198
8469
  },
8199
8470
  {
8200
8471
  metadata: {
@@ -8205,11 +8476,16 @@ async function handleSyncRequestMessage(message, respond, deps) {
8205
8476
  closeOnEnd: false,
8206
8477
  disableBuildCache: true,
8207
8478
  buildSnapshot: async () => {
8208
- const bytes = await deps.engine.serializeReplicationPartition(request.indexName, request.partitionId);
8209
- return { bytes, checksum: crc32(bytes) };
8479
+ const archive = await deps.engine.archiveReplicationPartition(
8480
+ request.indexName,
8481
+ request.partitionId,
8482
+ () => log.localLogEnd
8483
+ );
8484
+ const directory = deps.snapshotSpoolDirectory ?? defaultSpoolDirectory();
8485
+ return { ...await spoolSnapshotStream(directory, archive.stream), lastSeqNo: archive.lastSeqNo };
8210
8486
  },
8211
- afterSnapshot: async (trailingSink) => {
8212
- const entries = log.getEntriesFrom(snapshotSeqNo + 1);
8487
+ afterSnapshot: async (trailingSink, build) => {
8488
+ const entries = log.getEntriesFrom((build.lastSeqNo ?? snapshotSeqNo) + 1);
8213
8489
  await sendSyncEntriesResponse(message, trailingSink, deps, entries);
8214
8490
  }
8215
8491
  }
@@ -8325,7 +8601,7 @@ async function forwardInsertToRemote(indexName, document, docId, primaryNodeId,
8325
8601
  indexName,
8326
8602
  documentId: docId,
8327
8603
  operation: "insert",
8328
- document: encode(document),
8604
+ document: encodeDocument(document),
8329
8605
  updateFields: null
8330
8606
  };
8331
8607
  const message = createForwardMessage(payload, deps.nodeId);
@@ -8345,7 +8621,7 @@ async function forwardUpdateToRemote(indexName, document, docId, primaryNodeId,
8345
8621
  indexName,
8346
8622
  documentId: docId,
8347
8623
  operation: "update",
8348
- document: encode(document),
8624
+ document: encodeDocument(document),
8349
8625
  updateFields: null
8350
8626
  };
8351
8627
  const message = createForwardMessage(payload, deps.nodeId);
@@ -8371,6 +8647,8 @@ async function sendToNode(nodeId, message, deps) {
8371
8647
  details: { targetNodeId: nodeId }
8372
8648
  });
8373
8649
  }
8650
+
8651
+ // src/distribution/cluster-node/write-routing/replication.ts
8374
8652
  async function assertPrimaryWriteAuthority(entry, deps) {
8375
8653
  const table = await deps.coordinator.getAllocation(entry.indexName);
8376
8654
  const currentAssignment = table?.assignments.get(entry.partitionId);
@@ -8400,7 +8678,7 @@ function appendIndexReplicationEntry(indexName, partitionId, assignment, documen
8400
8678
  partitionId,
8401
8679
  indexName,
8402
8680
  documentId,
8403
- document: encode(document)
8681
+ document: encodeDocument(document)
8404
8682
  });
8405
8683
  }
8406
8684
  function appendDeleteReplicationEntry(indexName, partitionId, assignment, documentId, deps) {
@@ -8884,7 +9162,7 @@ function toWireOperation(item) {
8884
9162
  return {
8885
9163
  documentId: item.documentId,
8886
9164
  operation: item.operation,
8887
- document: item.document === null ? null : encode(item.document),
9165
+ document: item.document === null ? null : encodeDocument(item.document),
8888
9166
  updateFields: null
8889
9167
  };
8890
9168
  }
@@ -9291,9 +9569,15 @@ function validateCreateIndexOptions(partitionCount, replicationFactor) {
9291
9569
  async function routeCreateIndex(name, requestedConfig, options, coordinator, engine) {
9292
9570
  validateIndexName(name);
9293
9571
  const config = { ...requestedConfig, schema: validateSchema(requestedConfig.schema) };
9572
+ requireCreatableSchema(config.schema);
9573
+ requireSurfaceFormsForPartialFields(config.schema, config.surfaceForms);
9294
9574
  const partitionCount = options?.partitionCount ?? DEFAULT_PARTITION_COUNT;
9295
9575
  const replicationFactor = options?.replicationFactor ?? DEFAULT_REPLICATION_FACTOR;
9296
9576
  validateCreateIndexOptions(partitionCount, replicationFactor);
9577
+ requireConfigurationWithinSizeLimit(
9578
+ withPartitionCount(config, partitionCount),
9579
+ getLanguage(config.language ?? "english")
9580
+ );
9297
9581
  const constraints = {
9298
9582
  zoneAwareness: false,
9299
9583
  zoneAttribute: "zone",
@@ -9574,7 +9858,8 @@ function createDataNodeHandler(deps) {
9574
9858
  engine: deps.engine,
9575
9859
  coordinator: deps.coordinator,
9576
9860
  state: deps.snapshotSyncState,
9577
- resolveHeaderMetadata: deps.resolveHeaderMetadata
9861
+ resolveHeaderMetadata: deps.resolveHeaderMetadata,
9862
+ spoolDirectory: deps.snapshotSpoolDirectory
9578
9863
  });
9579
9864
  return;
9580
9865
  }
@@ -11529,6 +11814,10 @@ async function createClusterNode(config) {
11529
11814
  onIndexClose: (indexName) => deleteIndexReplicationLogs(replicationLogs, indexName)
11530
11815
  });
11531
11816
  localEngine = engine;
11817
+ const snapshotSpool = await openSnapshotSpoolDirectory(config.engine?.durability?.directory).catch(async (err) => {
11818
+ await engine.shutdown();
11819
+ throw err;
11820
+ });
11532
11821
  function replicationLogKey2(indexName, partitionId) {
11533
11822
  return replicationLogKey(indexName, partitionId);
11534
11823
  }
@@ -11588,7 +11877,8 @@ async function createClusterNode(config) {
11588
11877
  resetReplicationLog: seedReplicationLog2,
11589
11878
  applyReplicationEntry: engine.applyReplicationEntry,
11590
11879
  restoreReplicationPartition: engine.restoreReplicationPartition,
11591
- onError: forwardOnError
11880
+ onError: forwardOnError,
11881
+ snapshotSpoolDirectory: snapshotSpool.path
11592
11882
  };
11593
11883
  const primaryPartitionDeps = {
11594
11884
  engine,
@@ -11720,6 +12010,7 @@ async function createClusterNode(config) {
11720
12010
  coordinator: config.coordinator,
11721
12011
  writeDeps,
11722
12012
  snapshotSyncState: snapshotSyncHandlerState,
12013
+ snapshotSpoolDirectory: snapshotSpool.path,
11723
12014
  resolveHeaderMetadata: resolveSnapshotHeaderMetadata,
11724
12015
  isBootstrapSynced: (indexName, partitionId) => hasCompletedBootstrapSync(bootstrapSyncState, indexName, partitionId)
11725
12016
  });
@@ -11751,28 +12042,16 @@ async function createClusterNode(config) {
11751
12042
  }
11752
12043
  await Promise.all([...heldPartitionWrites]);
11753
12044
  await engine.shutdown();
12045
+ await snapshotSpool.dispose();
11754
12046
  }
11755
12047
  };
11756
12048
  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
- }
12049
+ function resolveSnapshotHeaderMetadata(indexName, partitionId) {
12050
+ return nodeSnapshotHeaderMetadata(
12051
+ { coordinator: config.coordinator, engine, getReplicationLog: getReplicationLog2 },
12052
+ indexName,
12053
+ partitionId
12054
+ );
11776
12055
  }
11777
12056
  }
11778
12057
 
@@ -11875,6 +12154,8 @@ function clusterNodeEngine(node, options) {
11875
12154
  rebuildAnalysis: () => unsupported("rebuildAnalysis"),
11876
12155
  snapshot: () => unsupported("snapshot"),
11877
12156
  restore: () => unsupported("restore"),
12157
+ exportDocuments: () => unsupported("exportDocuments"),
12158
+ importDocumentExport: () => unsupported("importDocumentExport"),
11878
12159
  rebalance: () => unsupported("rebalance"),
11879
12160
  updatePartitionConfig: () => unsupported("updatePartitionConfig"),
11880
12161
  compactVectors: () => unsupported("compactVectors"),