@delali/narsil 0.2.4-next.55 → 0.3.0

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 (52) hide show
  1. package/README.md +39 -28
  2. package/dist/adapters/filesystem.mjs +1 -1
  3. package/dist/adapters/indexeddb.mjs +1 -1
  4. package/dist/{capabilities-DlYw0Ov0.d.ts → capabilities-DSsOGz6F.d.ts} +2 -2
  5. package/dist/{chunk-BH5SYB5W.mjs → chunk-24RXHMEL.mjs} +604 -150
  6. package/dist/{chunk-D2S642IV.mjs → chunk-3LHGNRYE.mjs} +1 -1
  7. package/dist/{chunk-TVT5V4E7.mjs → chunk-B2TFROFX.mjs} +299 -168
  8. package/dist/{chunk-L225SHNB.mjs → chunk-L6TNPLAR.mjs} +19 -5
  9. package/dist/{chunk-U2AL3SON.mjs → chunk-THYRHHO2.mjs} +3 -3
  10. package/dist/{chunk-NTZYA3SJ.mjs → chunk-U7KYW4BA.mjs} +3 -3
  11. package/dist/{chunk-FRQVMFY7.mjs → chunk-USKFU7J5.mjs} +1 -0
  12. package/dist/{chunk-4NVPQ6S5.mjs → chunk-UXNWQZST.mjs} +12 -3
  13. package/dist/{chunk-RGS7FMCB.mjs → chunk-VMRR3DQU.mjs} +74 -16
  14. package/dist/{chunk-2GWVQWGA.mjs → chunk-YMZCMH5M.mjs} +45 -1
  15. package/dist/{chunk-UTOFOJOR.mjs → chunk-YNVIUGF5.mjs} +6 -2
  16. package/dist/{chunk-5T6AOR37.mjs → chunk-ZNZBXMP5.mjs} +601 -60
  17. package/dist/{chunk-XNQAIO74.mjs → chunk-ZZT3EAUC.mjs} +177 -309
  18. package/dist/{client-CQUqt9Ir.d.ts → client-BQK6Tf6A.d.ts} +3 -3
  19. package/dist/client.d.ts +8 -8
  20. package/dist/client.mjs +1 -0
  21. package/dist/{config-CNas6wJH.d.ts → config-BhldZgaK.d.ts} +2 -2
  22. package/dist/{config-bXxnJrQc.d.ts → config-DmXULPa-.d.ts} +15 -7
  23. package/dist/direct-executor-ALAVE7FC.mjs +12 -0
  24. package/dist/distribution/coordinator/etcd.d.ts +2 -2
  25. package/dist/distribution/coordinator/etcd.mjs +2 -2
  26. package/dist/distribution/coordinator/in-memory.d.ts +2 -2
  27. package/dist/distribution/coordinator/in-memory.mjs +1 -1
  28. package/dist/distribution/transport/grpc.mjs +1 -1
  29. package/dist/distribution.d.ts +8 -8
  30. package/dist/distribution.mjs +382 -162
  31. package/dist/embeddings/openai.mjs +2 -2
  32. package/dist/{engine-Dj4I91Jv.d.ts → engine-CIOyFozK.d.ts} +15 -12
  33. package/dist/index.browser.mjs +1565 -528
  34. package/dist/index.d.ts +7 -7
  35. package/dist/index.mjs +11 -11
  36. package/dist/invalidation/filesystem.mjs +1 -1
  37. package/dist/{memory-xrXOZMbw.d.ts → memory-yFaVn2xZ.d.ts} +199 -107
  38. package/dist/persistence/durability/checkpoint-worker.d.ts +1 -1
  39. package/dist/persistence/durability/checkpoint-worker.mjs +11 -11
  40. package/dist/react.d.ts +6 -6
  41. package/dist/react.mjs +1 -0
  42. package/dist/{schema-mLpXZBEh.d.ts → schema-d5Afu03A.d.ts} +7 -2
  43. package/dist/server.d.ts +10 -10
  44. package/dist/server.mjs +10 -10
  45. package/dist/{tasks-Df73Tjgq.d.ts → tasks-CH2Dod8w.d.ts} +1 -1
  46. package/dist/{thread-5UP6MGED.mjs → thread-DYHEQRVY.mjs} +14 -10
  47. package/dist/{types-BTIVO8DI.d.ts → types-DAK7mnUZ.d.ts} +1 -1
  48. package/dist/vector/search-worker.d.ts +1 -1
  49. package/dist/vector/search-worker.mjs +4 -4
  50. package/dist/workers/entry.mjs +4 -4
  51. package/package.json +9 -9
  52. package/dist/direct-executor-RBA5O7EN.mjs +0 -12
@@ -1,19 +1,110 @@
1
1
  import { ReplicationMessageTypes } from './chunk-GCM3JFVA.mjs';
2
- import { WIRE_BATCH_BUDGET, resolveVectorText, bindEngineCore, collectRequiredFieldFailures, embedChunkDocuments, providedDocId, admitInsert, asBatchInsertError, embedDocumentFields, assertDocumentCarriesMappedVectors } from './chunk-4NVPQ6S5.mjs';
3
- import { getNestedValue, generateId, getLanguage, splitWorkerBudget, resolveWorkerCount, createDirectExecutor, createPartitionRouter, createCompositePartition, deserializePayloadV2, createSharedFrozenSegment, createPartitionManager, buildSegmentPayload, resolvePartitionInsertOptions, readHeapStatistics, resolveIndexAnalysis, deserializePayloadV1, decodeVectorIndexParts, resolveRequestThreadCount, createPartitionIndex, VECTOR_FILE_MAX_VECTORS, deadBytesFor, mergeFrozenSegments, isCompositePartition, workerResourceLimits, deserializeMetadata, isNodeMainThread, spawnNodeWorker, freezeSegmentShared, DEFAULT_BACKPRESSURE_LIMIT, DEFAULT_REQUEST_TIMEOUT_MS, serializeMetadata, SHUTDOWN_TIMEOUT_MS, encodeSegmentState, CONSECUTIVE_TIMEOUTS_BEFORE_DEATH, decodeVectorFilePayload, documentsOfPayloadV2, countDead, decodeVectorGraphPayload, deadBitsWhere, NO_LIVE_VECTOR_AT_POSITION, createFrozenSegment } from './chunk-XNQAIO74.mjs';
2
+ import { WIRE_BATCH_BUDGET, resolveVectorText, bindEngineCore, collectRequiredFieldFailures, embedChunkDocuments, providedDocId, admitInsert, asBatchInsertError, embedDocumentFields, assertDocumentCarriesMappedVectors } from './chunk-UXNWQZST.mjs';
3
+ import { generateId, getLanguage, splitWorkerBudget, resolveWorkerCount, createDirectExecutor, createPartitionRouter, createCompositePartition, deserializePayloadV2, createSharedFrozenSegment, createPartitionManager, buildSegmentPayload, resolvePartitionInsertOptions, resolveWorkerEntry, missingWorkerEntry, readHeapStatistics, resolveIndexAnalysis, deserializePayloadV1, decodeVectorIndexParts, nodeThreadId, resolveRequestThreadCount, createPartitionIndex, VECTOR_FILE_MAX_VECTORS, deadBytesFor, mergeFrozenSegments, isCompositePartition, workerResourceLimits, deserializeMetadata, isNodeMainThread, spawnNodeWorker, freezeSegmentShared, DEFAULT_BACKPRESSURE_LIMIT, DEFAULT_REQUEST_TIMEOUT_MS, SHUTDOWN_TIMEOUT_MS, serializeMetadata, encodeSegmentState, CONSECUTIVE_TIMEOUTS_BEFORE_DEATH, decodeVectorFilePayload, documentsOfPayloadV2, countDead, decodeVectorGraphPayload, deadBitsWhere, NO_LIVE_VECTOR_AT_POSITION, createFrozenSegment } from './chunk-ZZT3EAUC.mjs';
4
4
  import { detectRuntime } from './chunk-AV4IJKD2.mjs';
5
- import { validateWorkerConfig, validateIndexName, validateBM25Params, validatePartitionConfig, executeQuery, queryBindingOf, executePreflight, executeSuggest, executeListDocuments, validateDocId } from './chunk-TVT5V4E7.mjs';
6
- import { BATCH_CHUNK_SIZE, STATISTICS_BROADCAST_INTERVAL_MS, MAX_SWEEP_INTERVAL_MS, MIN_SWEEP_INTERVAL_MS, HEAP_PRESSURE_WARNING_FRACTION, FOREIGN_STATS_TTL_MS, MIN_DOCUMENTS_FOR_SEGMENTS, HEAP_PRESSURE_REARM_FRACTION } from './chunk-D2S642IV.mjs';
7
- import { collectQueryTermStats, mergePartitionStats, fnv1a, kWayMerge, mergeFacets } from './chunk-5T6AOR37.mjs';
8
- import { extractVectorFieldsFromSchema, validateSchema, validateVectorPromotion, validateVectorStorage, flattenSchema, isTextFieldType, validateRequiredFields, validateDocument, validateDocumentStrict } from './chunk-L225SHNB.mjs';
9
- import { releaseLocksHeldBy } from './chunk-U2AL3SON.mjs';
5
+ import { validateWorkerConfig, validateIndexName, validateBM25Params, validatePartitionConfig, executeQuery, queryBindingOf, executePreflight, executeSuggest, executeListDocuments, validateDocId } from './chunk-B2TFROFX.mjs';
6
+ import { BATCH_CHUNK_SIZE, STATISTICS_BROADCAST_INTERVAL_MS, MAX_SWEEP_INTERVAL_MS, MIN_SWEEP_INTERVAL_MS, HEAP_PRESSURE_WARNING_FRACTION, FOREIGN_STATS_TTL_MS, MIN_DOCUMENTS_FOR_SEGMENTS, HEAP_PRESSURE_REARM_FRACTION } from './chunk-3LHGNRYE.mjs';
7
+ import { getNestedValue, collectQueryTermStats, mergePartitionStats, oversampledFacetConfig, fnv1a, kWayMerge, mergeFacets } from './chunk-ZNZBXMP5.mjs';
8
+ import { extractVectorFieldsFromSchema, validateSchema, validateVectorPromotion, validateVectorStorage, flattenSchema, isTextFieldType, validateRequiredFields, validateDocument, validateDocumentStrict } from './chunk-L6TNPLAR.mjs';
9
+ import { releaseLocksHeldBy } from './chunk-THYRHHO2.mjs';
10
+ import { requireSearchBackendSettings } from './chunk-YNVIUGF5.mjs';
10
11
  import { crc32, nativeCrc32, crc32Final, crc32Init, crc32Update } from './chunk-3L4U6EJQ.mjs';
11
12
  import { CHECKSUM_TIMEOUT_MS, CHECKSUM_YIELD_CHUNK_BYTES } from './chunk-IFBKLBGB.mjs';
12
- import { compareCodePoints } from './chunk-2GWVQWGA.mjs';
13
+ import { compareCodePoints } from './chunk-YMZCMH5M.mjs';
13
14
  import { createRequestId, isValidWorkerResponse, threadSlotOfWorker } from './chunk-POWOTUKJ.mjs';
14
- import { NarsilError, ErrorCodes } from './chunk-FRQVMFY7.mjs';
15
+ import { NarsilError, ErrorCodes } from './chunk-USKFU7J5.mjs';
15
16
  import { encode, decode } from '@msgpack/msgpack';
16
17
 
18
+ // src/engine/config-keys.ts
19
+ var NARSIL_CONFIG_KEYS = {
20
+ persistence: true,
21
+ invalidation: true,
22
+ plugins: true,
23
+ idGenerator: true,
24
+ workers: true,
25
+ embedding: true,
26
+ embeddingAdapters: true,
27
+ durability: true,
28
+ analysis: true,
29
+ lifecycle: true
30
+ };
31
+ var DURABILITY_CONFIG_KEYS = {
32
+ tier: true,
33
+ directory: true,
34
+ mode: true,
35
+ flushIntervalMs: true,
36
+ segmentMaxBytes: true,
37
+ checkpointIntervalMs: true,
38
+ checkpointMutationThreshold: true,
39
+ compactionThreshold: true
40
+ };
41
+ var WORKER_CONFIG_KEYS = {
42
+ enabled: true,
43
+ count: true,
44
+ promotionThreshold: true,
45
+ idleTimeoutMs: true,
46
+ mainCopyQueries: true,
47
+ bootstrapModule: true
48
+ };
49
+ var LIFECYCLE_CONFIG_KEYS = {
50
+ idleTimeoutMs: true,
51
+ maxOpenIndexes: true,
52
+ maxOpenBytes: true,
53
+ maxReopenWaiters: true
54
+ };
55
+ var ANALYSIS_CONFIG_KEYS = {
56
+ rebuild: true,
57
+ onStaleAnalysis: true
58
+ };
59
+ function requireKnownKeys(section, value, known) {
60
+ if (value === void 0 || value === null || typeof value !== "object") return;
61
+ for (const key of Object.keys(value)) {
62
+ if (Object.hasOwn(known, key)) continue;
63
+ throw new NarsilError(
64
+ ErrorCodes.CONFIG_INVALID,
65
+ `The configuration sets "${section}${key}", which is no setting that createNarsil accepts. The settings at that level are ${Object.keys(known).join(", ")}`,
66
+ { setting: `${section}${key}`, settings: Object.keys(known) }
67
+ );
68
+ }
69
+ }
70
+ function requireNamedPlugins(plugins) {
71
+ if (plugins === void 0) return;
72
+ if (!Array.isArray(plugins)) {
73
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, 'The configuration sets "plugins" to something other than a list');
74
+ }
75
+ plugins.forEach((plugin, position) => {
76
+ const name = plugin?.name;
77
+ if (typeof name !== "string" || name.trim().length === 0) {
78
+ throw new NarsilError(
79
+ ErrorCodes.CONFIG_INVALID,
80
+ `The plugin at position ${position} carries no name, and the engine names each plugin in its errors and event payloads`,
81
+ { position }
82
+ );
83
+ }
84
+ });
85
+ }
86
+ function requireKnownConfig(config) {
87
+ if (config === void 0) return;
88
+ requireKnownKeys("", config, NARSIL_CONFIG_KEYS);
89
+ requireKnownKeys("durability.", config.durability, DURABILITY_CONFIG_KEYS);
90
+ requireKnownKeys("workers.", config.workers, WORKER_CONFIG_KEYS);
91
+ requireKnownKeys("lifecycle.", config.lifecycle, LIFECYCLE_CONFIG_KEYS);
92
+ requireKnownKeys("analysis.", config.analysis, ANALYSIS_CONFIG_KEYS);
93
+ requireNamedPlugins(config.plugins);
94
+ }
95
+
96
+ // src/engine/orchestration/turn.ts
97
+ function afterCurrentTurn(callback) {
98
+ if (typeof setImmediate === "function") {
99
+ setImmediate(callback);
100
+ return;
101
+ }
102
+ setTimeout(callback, 0);
103
+ }
104
+ function yieldToEventLoop() {
105
+ return new Promise((resolve) => afterCurrentTurn(resolve));
106
+ }
107
+
17
108
  // src/partitioning/constants.ts
18
109
  var REBALANCE_CHUNK_SIZE = 1e3;
19
110
  var DEFAULT_WRITE_AHEAD_QUEUE_MAX_SIZE = 1e4;
@@ -88,7 +179,7 @@ function createRebalancer() {
88
179
  documentsProcessed,
89
180
  documentsTotal
90
181
  });
91
- await new Promise((resolve) => setTimeout(resolve, 0));
182
+ await yieldToEventLoop();
92
183
  }
93
184
  }
94
185
  }
@@ -98,7 +189,7 @@ function createRebalancer() {
98
189
  documentsProcessed,
99
190
  documentsTotal
100
191
  });
101
- await new Promise((resolve) => setTimeout(resolve, 0));
192
+ await yieldToEventLoop();
102
193
  }
103
194
  onProgress?.({
104
195
  phase: "swapping",
@@ -125,13 +216,43 @@ function createRebalancer() {
125
216
  }
126
217
 
127
218
  // src/plugins/registry.ts
219
+ var WRITE_REJECTING_HOOKS = /* @__PURE__ */ new Set(["beforeInsert", "beforeUpdate", "beforeRemove"]);
128
220
  function isThenable(value) {
129
221
  return value !== null && typeof value === "object" && typeof value.then === "function";
130
222
  }
131
- async function continueAsync(pending, hooks, startIndex, context) {
223
+ function thrownText(err, plugin, hookName) {
224
+ if (err instanceof Error) return err.message;
225
+ try {
226
+ return String(err);
227
+ } catch {
228
+ return `The ${hookName} hook of plugin "${plugin}" rejects the write`;
229
+ }
230
+ }
231
+ function asWriteRejection(err, plugin, hookName) {
232
+ if (err instanceof NarsilError) return err;
233
+ return new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, thrownText(err, plugin, hookName), {
234
+ plugin,
235
+ hook: hookName
236
+ });
237
+ }
238
+ function invoke(hook, hookName, context) {
239
+ const rejectsWrites = WRITE_REJECTING_HOOKS.has(hookName);
240
+ let result;
241
+ try {
242
+ result = hook.run(context);
243
+ } catch (err) {
244
+ throw rejectsWrites ? asWriteRejection(err, hook.plugin, hookName) : err;
245
+ }
246
+ if (!isThenable(result)) return null;
247
+ if (!rejectsWrites) return result;
248
+ return Promise.resolve(result).catch((err) => {
249
+ throw asWriteRejection(err, hook.plugin, hookName);
250
+ });
251
+ }
252
+ async function continueAsync(pending, hooks, startIndex, hookName, context) {
132
253
  await pending;
133
254
  for (let i = startIndex; i < hooks.length; i++) {
134
- await hooks[i](context);
255
+ await invoke(hooks[i], hookName, context);
135
256
  }
136
257
  }
137
258
  function createPluginRegistry() {
@@ -148,14 +269,14 @@ function createPluginRegistry() {
148
269
  for (const plugin of plugins) {
149
270
  const hook = plugin[hookName];
150
271
  if (typeof hook === "function") {
151
- hooks.push(hook.bind(plugin));
272
+ hooks.push({ plugin: plugin.name, run: hook.bind(plugin) });
152
273
  }
153
274
  }
154
275
  if (hooks.length === 0) return;
155
276
  for (let i = 0; i < hooks.length; i++) {
156
- const result = hooks[i](context);
157
- if (isThenable(result)) {
158
- return continueAsync(result, hooks, i + 1, context);
277
+ const pending = invoke(hooks[i], hookName, context);
278
+ if (pending !== null) {
279
+ return continueAsync(pending, hooks, i + 1, hookName, context);
159
280
  }
160
281
  }
161
282
  }
@@ -294,7 +415,7 @@ function emitEngineEvent(eventHandlers, event, payload) {
294
415
  }
295
416
 
296
417
  // src/engine/analysis-rebuild.ts
297
- function yieldToEventLoop() {
418
+ function yieldToEventLoop2() {
298
419
  return new Promise((resolve) => {
299
420
  setTimeout(resolve, 0);
300
421
  });
@@ -335,7 +456,7 @@ function createAnalysisRebuildCoordinator(config, deps) {
335
456
  for (let partitionId = 0; partitionId < manager.partitionCount; partitionId += 1) {
336
457
  manager.rebuildTextIndex(partitionId);
337
458
  entry.partitionsRebuilt = partitionId + 1;
338
- await yieldToEventLoop();
459
+ await yieldToEventLoop2();
339
460
  }
340
461
  stale.delete(indexName);
341
462
  await deps.persistAnalysisRevision(indexName);
@@ -514,6 +635,10 @@ function resolveDurabilityTier(config) {
514
635
  var DEFAULT_CHECKPOINT_INTERVAL_MS = 3e5;
515
636
  var DEFAULT_CHECKPOINT_MUTATION_THRESHOLD = 1e5;
516
637
  var DEFAULT_ASYNC_FLUSH_INTERVAL_MS = 1e3;
638
+ var DIRECTORY_IDENTITY_CHECK_INTERVAL_MS = 1e3;
639
+ var DIRECTORY_LOCK_ATTEMPTS = 3;
640
+ var DIRECTORY_LOCK_SETTLE_MS = 50;
641
+ var DIRECTORY_LOCK_PROCESS_START_TOLERANCE_MS = 1e3;
517
642
  var DEFAULT_SEGMENT_MAX_BYTES = 67108864;
518
643
  var DEFAULT_COMPACTION_THRESHOLD = 12;
519
644
  var COMPACTION_LARGEST_MERGED_DOCUMENT_COUNT = 2e5;
@@ -542,6 +667,163 @@ async function getPath() {
542
667
  return cachedPath;
543
668
  }
544
669
 
670
+ // src/persistence/durability/directory-lock.ts
671
+ var LOCK_FILE_NAME = ".narsil.lock";
672
+ var HARD_LINK_UNSUPPORTED_CODES = /* @__PURE__ */ new Set(["EPERM", "ENOTSUP", "EOPNOTSUPP", "ENOSYS"]);
673
+ var lockedDirectories = /* @__PURE__ */ new Set();
674
+ function errnoOf(err) {
675
+ return err?.code;
676
+ }
677
+ function processIsRunning(pid) {
678
+ try {
679
+ process.kill(pid, 0);
680
+ return true;
681
+ } catch (err) {
682
+ return errnoOf(err) === "EPERM";
683
+ }
684
+ }
685
+ function integerOrNull(text) {
686
+ const value = Number.parseInt(text ?? "", 10);
687
+ return Number.isSafeInteger(value) ? value : null;
688
+ }
689
+ function parseHolder(text) {
690
+ const [pidText, threadText, startText, nonceText] = text.split("\n");
691
+ const pid = integerOrNull(pidText);
692
+ if (pid === null || pid <= 0) return null;
693
+ const nonce = nonceText?.trim() ?? "";
694
+ return {
695
+ pid,
696
+ threadId: integerOrNull(threadText),
697
+ processStartMs: integerOrNull(startText),
698
+ nonce: nonce.length > 0 ? nonce : null
699
+ };
700
+ }
701
+ function formatHolder(holder) {
702
+ return `${holder.pid}
703
+ ${holder.threadId ?? ""}
704
+ ${holder.processStartMs ?? ""}
705
+ ${holder.nonce ?? ""}
706
+ `;
707
+ }
708
+ async function readHolder(fs, lockPath) {
709
+ try {
710
+ return parseHolder(await fs.readFile(lockPath, "utf8")) ?? "unreadable";
711
+ } catch (err) {
712
+ if (errnoOf(err) === "ENOENT") return "absent";
713
+ throw err;
714
+ }
715
+ }
716
+ async function settledHolder(fs, lockPath) {
717
+ const first = await readHolder(fs, lockPath);
718
+ if (first !== "unreadable") return first;
719
+ await new Promise((resolve) => setTimeout(resolve, DIRECTORY_LOCK_SETTLE_MS));
720
+ return readHolder(fs, lockPath);
721
+ }
722
+ function holderIsLive(holder, self) {
723
+ if (holder.pid !== self.pid) return processIsRunning(holder.pid);
724
+ if (holder.processStartMs === null || self.processStartMs === null) return false;
725
+ return Math.abs(holder.processStartMs - self.processStartMs) <= DIRECTORY_LOCK_PROCESS_START_TOLERANCE_MS;
726
+ }
727
+ function heldElsewhere(root, holder, lockPath) {
728
+ return new NarsilError(
729
+ ErrorCodes.CONFIG_INVALID,
730
+ `The durability directory "${root}" is in use by ${holder}, and only one engine may write a write-ahead log directory. Point this engine at another directory, or shut the other engine down first. Where no engine uses the directory, delete "${lockPath}"`,
731
+ { directory: root }
732
+ );
733
+ }
734
+ function holderLabel(holder, self) {
735
+ if (holder.pid !== self.pid) return `process ${holder.pid}`;
736
+ return holder.threadId === self.threadId ? "another copy of Narsil in this thread" : "another thread of this process";
737
+ }
738
+ async function createLockFileExclusively(fs, lockPath, content) {
739
+ try {
740
+ await fs.writeFile(lockPath, content, { flag: "wx" });
741
+ return true;
742
+ } catch (err) {
743
+ if (errnoOf(err) === "EEXIST") return false;
744
+ throw err;
745
+ }
746
+ }
747
+ async function createLockFile(fs, lockPath, draftPath, content) {
748
+ await fs.writeFile(draftPath, content, { flag: "w" });
749
+ try {
750
+ await fs.link(draftPath, lockPath);
751
+ return true;
752
+ } catch (err) {
753
+ const code = errnoOf(err);
754
+ if (code === "EEXIST") return false;
755
+ if (code !== void 0 && HARD_LINK_UNSUPPORTED_CODES.has(code)) {
756
+ return createLockFileExclusively(fs, lockPath, content);
757
+ }
758
+ throw err;
759
+ } finally {
760
+ await fs.unlink(draftPath).catch(() => void 0);
761
+ }
762
+ }
763
+ async function acquire(fs, root, lockPath, self) {
764
+ const draftPath = `${lockPath}.${self.pid}.${self.threadId ?? 0}`;
765
+ const content = formatHolder(self);
766
+ for (let attempt = 0; attempt < DIRECTORY_LOCK_ATTEMPTS; attempt++) {
767
+ if (await createLockFile(fs, lockPath, draftPath, content)) return;
768
+ const holder = await settledHolder(fs, lockPath);
769
+ if (typeof holder === "object" && holderIsLive(holder, self)) {
770
+ throw heldElsewhere(root, holderLabel(holder, self), lockPath);
771
+ }
772
+ await fs.unlink(lockPath).catch((err) => {
773
+ if (errnoOf(err) !== "ENOENT") throw err;
774
+ });
775
+ }
776
+ throw heldElsewhere(root, "another process", lockPath);
777
+ }
778
+ async function describeSelf() {
779
+ return {
780
+ pid: process.pid,
781
+ threadId: await nodeThreadId(),
782
+ processStartMs: Math.round(Date.now() - process.uptime() * 1e3),
783
+ nonce: globalThis.crypto.randomUUID()
784
+ };
785
+ }
786
+ async function lockDirectory(root) {
787
+ const fs = await getFs();
788
+ const pathMod = await getPath();
789
+ const resolvedRoot = pathMod.resolve(root);
790
+ const lockPath = pathMod.join(resolvedRoot, LOCK_FILE_NAME);
791
+ const self = await describeSelf();
792
+ let directoryKey = null;
793
+ try {
794
+ await fs.mkdir(resolvedRoot, { recursive: true });
795
+ const stat = await fs.stat(resolvedRoot, { bigint: true });
796
+ const key = `${stat.dev}:${stat.ino}`;
797
+ if (lockedDirectories.has(key)) throw heldElsewhere(root, "another engine in this process", lockPath);
798
+ lockedDirectories.add(key);
799
+ directoryKey = key;
800
+ await acquire(fs, root, lockPath, self);
801
+ } catch (err) {
802
+ if (directoryKey !== null) lockedDirectories.delete(directoryKey);
803
+ if (err instanceof NarsilError) throw err;
804
+ throw new NarsilError(
805
+ ErrorCodes.PERSISTENCE_SAVE_FAILED,
806
+ `The engine cannot lock the durability directory "${root}"`,
807
+ { directory: root, cause: err instanceof Error ? err.message : String(err) }
808
+ );
809
+ }
810
+ const heldKey = directoryKey;
811
+ let released = false;
812
+ async function holds() {
813
+ const holder = await readHolder(fs, lockPath);
814
+ return typeof holder === "object" && holder.nonce === self.nonce;
815
+ }
816
+ return {
817
+ holds,
818
+ async release() {
819
+ if (released) return;
820
+ released = true;
821
+ lockedDirectories.delete(heldKey);
822
+ if (await holds().catch(() => false)) await fs.unlink(lockPath).catch(() => void 0);
823
+ }
824
+ };
825
+ }
826
+
545
827
  // src/persistence/durability/durable-filesystem.ts
546
828
  function validateRelativeKey(key) {
547
829
  if (key.length === 0) {
@@ -627,6 +909,20 @@ function createDurableDirectory(root) {
627
909
  get root() {
628
910
  return root;
629
911
  },
912
+ lock() {
913
+ return lockDirectory(root);
914
+ },
915
+ async identity() {
916
+ const fs = await getFs();
917
+ const pathMod = await getPath();
918
+ try {
919
+ const stat = await fs.stat(pathMod.resolve(root), { bigint: true });
920
+ return `${stat.dev}:${stat.ino}`;
921
+ } catch (err) {
922
+ if (err.code === "ENOENT") return null;
923
+ throw err;
924
+ }
925
+ },
630
926
  async appendHandle(key) {
631
927
  const { path: filePath, pathMod } = await resolve(key);
632
928
  const fs = await getFs();
@@ -817,22 +1113,20 @@ async function isOnMainThread() {
817
1113
  }
818
1114
  return onMainThread;
819
1115
  }
820
- function resolveWorkerEntryPoint() {
821
- const base = import.meta.url;
822
- const distIndex = base.lastIndexOf("/dist/");
823
- if (distIndex !== -1) {
824
- return new URL("serialization/crc32-worker.mjs", base.slice(0, distIndex + 6)).href;
825
- }
826
- return base.replace(/\/src\/serialization\/[^/]+$/, "/dist/serialization/crc32-worker.mjs");
827
- }
828
1116
  async function spawnWorker() {
1117
+ const entryPoint = resolveWorkerEntry(
1118
+ import.meta.url,
1119
+ /\/src\/serialization\/[^/]+$/,
1120
+ "serialization/crc32-worker.mjs"
1121
+ );
1122
+ if (entryPoint === null) return null;
829
1123
  try {
830
- return await spawnNodeWorker(new URL(resolveWorkerEntryPoint()));
1124
+ return await spawnNodeWorker(new URL(entryPoint));
831
1125
  } catch {
832
1126
  return null;
833
1127
  }
834
1128
  }
835
- function yieldToEventLoop2() {
1129
+ function yieldToEventLoop3() {
836
1130
  if (typeof setImmediate === "function") {
837
1131
  return new Promise((resolve) => setImmediate(resolve));
838
1132
  }
@@ -844,7 +1138,7 @@ async function advanceChecksum(start, payload) {
844
1138
  const end = Math.min(offset + CHECKSUM_YIELD_CHUNK_BYTES, payload.length);
845
1139
  state = crc32Update(state, payload.subarray(offset, end));
846
1140
  if (end < payload.length) {
847
- await yieldToEventLoop2();
1141
+ await yieldToEventLoop3();
848
1142
  }
849
1143
  }
850
1144
  return state;
@@ -2132,7 +2426,7 @@ function attachDocuments(manager, partitionId, segment) {
2132
2426
  }
2133
2427
  manager.attachFrozenSegment(partitionId, createSharedFrozenSegment(snapshot));
2134
2428
  }
2135
- function yieldToEventLoop3() {
2429
+ function yieldToEventLoop4() {
2136
2430
  return new Promise((resolve) => setTimeout(resolve, 0));
2137
2431
  }
2138
2432
  async function loadPartitionSegmentBySegment(directory, partition, manager) {
@@ -2142,7 +2436,7 @@ async function loadPartitionSegmentBySegment(directory, partition, manager) {
2142
2436
  addSegmentSchema(agreedSchema, segment.schema);
2143
2437
  removeWhatTheSegmentReplaces(manager, segment);
2144
2438
  attachDocuments(manager, partition.partitionId, segment);
2145
- await yieldToEventLoop3();
2439
+ await yieldToEventLoop4();
2146
2440
  }
2147
2441
  }
2148
2442
 
@@ -3807,17 +4101,15 @@ var workerBusy = false;
3807
4101
  var spawnedWorkerCount = 0;
3808
4102
  var workerIdleMs = CHECKPOINT_WORKER_IDLE_MS;
3809
4103
  var idleTimer = null;
3810
- function resolveWorkerEntryPoint2() {
3811
- const base = import.meta.url;
3812
- const distIndex = base.lastIndexOf("/dist/");
3813
- if (distIndex !== -1) {
3814
- return new URL("persistence/durability/checkpoint-worker.mjs", base.slice(0, distIndex + 6)).href;
3815
- }
3816
- return base.replace(/\/src\/persistence\/durability\/[^/]+$/, "/dist/persistence/durability/checkpoint-worker.mjs");
3817
- }
3818
4104
  async function spawnWorker2() {
4105
+ const entryPoint = resolveWorkerEntry(
4106
+ import.meta.url,
4107
+ /\/src\/persistence\/durability\/[^/]+$/,
4108
+ "persistence/durability/checkpoint-worker.mjs"
4109
+ );
4110
+ if (entryPoint === null) return null;
3819
4111
  try {
3820
- const worker = await spawnNodeWorker(new URL(resolveWorkerEntryPoint2()));
4112
+ const worker = await spawnNodeWorker(new URL(entryPoint));
3821
4113
  if (worker === null) {
3822
4114
  return null;
3823
4115
  }
@@ -4048,11 +4340,29 @@ async function garbageThatNoFieldStillReads(directory, vectorIndexes, keys) {
4048
4340
  return unread;
4049
4341
  }
4050
4342
  async function runDurableCheckpoint(input) {
4051
- let recordsLeftInLog = await writeOneBoundedCheckpoint(input);
4052
- while (recordsLeftInLog > 0 && input.mayContinue()) {
4053
- recordsLeftInLog = await writeOneBoundedCheckpoint(input);
4343
+ try {
4344
+ let recordsLeftInLog = await writeOneBoundedCheckpoint(input);
4345
+ while (recordsLeftInLog > 0 && input.mayContinue()) {
4346
+ recordsLeftInLog = await writeOneBoundedCheckpoint(input);
4347
+ }
4348
+ } catch (err) {
4349
+ if (!isSystemError(err)) throw err;
4350
+ const error = new NarsilError(
4351
+ ErrorCodes.PERSISTENCE_SAVE_FAILED,
4352
+ `The engine cannot write the checkpoint of index "${input.indexName}" to the durability directory "${input.directory.root}"`,
4353
+ {
4354
+ indexName: input.indexName,
4355
+ directory: input.directory.root,
4356
+ cause: err.message
4357
+ }
4358
+ );
4359
+ input.markFatal(error);
4360
+ throw error;
4054
4361
  }
4055
4362
  }
4363
+ function isSystemError(err) {
4364
+ return err instanceof Error && !(err instanceof NarsilError) && typeof err.code === "string";
4365
+ }
4056
4366
  async function writeOneBoundedCheckpoint(input) {
4057
4367
  const manager = input.hooks.getManager(input.indexName);
4058
4368
  if (manager === void 0) {
@@ -4132,6 +4442,94 @@ function recordsBeyond(indexState, targets) {
4132
4442
  return records;
4133
4443
  }
4134
4444
 
4445
+ // src/persistence/durability/directory-watch.ts
4446
+ function unrefInterval(tick, intervalMs) {
4447
+ const timer = setInterval(tick, intervalMs);
4448
+ if (typeof timer.unref === "function") timer.unref();
4449
+ return timer;
4450
+ }
4451
+ function createDirectoryWatch(directory, isStopped, onLost) {
4452
+ let expected = null;
4453
+ let timer = null;
4454
+ let lock = null;
4455
+ function cannotCheck(err) {
4456
+ return new NarsilError(
4457
+ ErrorCodes.PERSISTENCE_SAVE_FAILED,
4458
+ `The engine cannot check the durability directory "${directory.root}", so it cannot confirm that a write is durable`,
4459
+ { directory: directory.root, cause: err instanceof Error ? err.message : String(err) }
4460
+ );
4461
+ }
4462
+ async function currentIdentity() {
4463
+ try {
4464
+ return await directory.identity?.() ?? null;
4465
+ } catch (err) {
4466
+ return cannotCheck(err);
4467
+ }
4468
+ }
4469
+ async function lockTakenOver() {
4470
+ if (lock === null) return null;
4471
+ try {
4472
+ if (await lock.holds()) return null;
4473
+ } catch (err) {
4474
+ return cannotCheck(err);
4475
+ }
4476
+ return new NarsilError(
4477
+ ErrorCodes.PERSISTENCE_SAVE_FAILED,
4478
+ `The lock file of the durability directory "${directory.root}" records another engine, which may be writing the same log`,
4479
+ { directory: directory.root }
4480
+ );
4481
+ }
4482
+ async function verify() {
4483
+ if (directory.identity === void 0 || isStopped()) return true;
4484
+ const current = await currentIdentity();
4485
+ if (current instanceof Error) {
4486
+ onLost(current);
4487
+ return false;
4488
+ }
4489
+ if (expected === null) {
4490
+ expected = current;
4491
+ } else if (current !== expected) {
4492
+ onLost(
4493
+ new NarsilError(
4494
+ ErrorCodes.PERSISTENCE_SAVE_FAILED,
4495
+ `The durability directory "${directory.root}" is missing or is a different directory from the one that this instance opens at start-up, so a write to it is not durable`,
4496
+ { directory: directory.root }
4497
+ )
4498
+ );
4499
+ return false;
4500
+ }
4501
+ const takenOver = await lockTakenOver();
4502
+ if (takenOver === null) return true;
4503
+ onLost(takenOver);
4504
+ return false;
4505
+ }
4506
+ function stop() {
4507
+ if (timer !== null) {
4508
+ clearInterval(timer);
4509
+ timer = null;
4510
+ }
4511
+ }
4512
+ return {
4513
+ async claim() {
4514
+ if (lock === null && directory.lock !== void 0) lock = await directory.lock();
4515
+ },
4516
+ async release() {
4517
+ const held = lock;
4518
+ lock = null;
4519
+ await held?.release();
4520
+ },
4521
+ verify,
4522
+ async verifyOnce() {
4523
+ if (expected === null && directory.identity !== void 0) await verify();
4524
+ },
4525
+ start() {
4526
+ if (directory.identity === void 0 || timer !== null || isStopped()) return;
4527
+ timer = unrefInterval(() => void verify(), DIRECTORY_IDENTITY_CHECK_INTERVAL_MS);
4528
+ },
4529
+ stop
4530
+ };
4531
+ }
4532
+
4135
4533
  // src/persistence/durability/manager-state.ts
4136
4534
  async function drainIndexStateForUnload(indexState, pendingMetadataWrite, closeWalWriter) {
4137
4535
  indexState.unloading = true;
@@ -4604,11 +5002,13 @@ function createDurabilityManager(config, hooks, directoryOverride) {
4604
5002
  let asyncFlushTimer = null;
4605
5003
  let shuttingDown = false;
4606
5004
  let fatalError = null;
5005
+ const directoryWatch = createDirectoryWatch(directory, () => shuttingDown || fatalError !== null, markFatal);
4607
5006
  function markFatal(error) {
4608
5007
  if (fatalError !== null) {
4609
5008
  return;
4610
5009
  }
4611
5010
  fatalError = error;
5011
+ directoryWatch.stop();
4612
5012
  if (checkpointTimer !== null) {
4613
5013
  clearInterval(checkpointTimer);
4614
5014
  checkpointTimer = null;
@@ -4672,23 +5072,13 @@ function createDurabilityManager(config, hooks, directoryOverride) {
4672
5072
  if (checkpointTimer !== null || shuttingDown || fatalError !== null || checkpointIntervalMs <= 0) {
4673
5073
  return;
4674
5074
  }
4675
- checkpointTimer = setInterval(() => {
4676
- void runScheduledCheckpoints();
4677
- }, checkpointIntervalMs);
4678
- if (typeof checkpointTimer.unref === "function") {
4679
- checkpointTimer.unref();
4680
- }
5075
+ checkpointTimer = unrefInterval(() => void runScheduledCheckpoints(), checkpointIntervalMs);
4681
5076
  }
4682
5077
  function startAsyncFlushTimer() {
4683
5078
  if (mode !== "async" || asyncFlushTimer !== null || shuttingDown || fatalError !== null || flushIntervalMs <= 0) {
4684
5079
  return;
4685
5080
  }
4686
- asyncFlushTimer = setInterval(() => {
4687
- void flushAllPartitions();
4688
- }, flushIntervalMs);
4689
- if (typeof asyncFlushTimer.unref === "function") {
4690
- asyncFlushTimer.unref();
4691
- }
5081
+ asyncFlushTimer = unrefInterval(() => void flushAllPartitions(), flushIntervalMs);
4692
5082
  }
4693
5083
  async function flushAllPartitions() {
4694
5084
  if (fatalError !== null) {
@@ -4750,6 +5140,8 @@ function createDurabilityManager(config, hooks, directoryOverride) {
4750
5140
  return run;
4751
5141
  }
4752
5142
  async function performCheckpoint(indexName, indexState, fromMemory = false) {
5143
+ await directoryWatch.verify();
5144
+ if (fatalError !== null) throw fatalError;
4753
5145
  await runDurableCheckpoint({
4754
5146
  directory,
4755
5147
  hooks,
@@ -4799,10 +5191,14 @@ function createDurabilityManager(config, hooks, directoryOverride) {
4799
5191
  }
4800
5192
  await stallWhileCheckpointsFallBehind(indexState);
4801
5193
  }
4802
- if (recordedByIndex.size > 0) {
4803
- startCheckpointTimer();
4804
- startAsyncFlushTimer();
4805
- }
5194
+ if (recordedByIndex.size === 0) return outcomes;
5195
+ if (mode === "sync") await directoryWatch.verify();
5196
+ const lost = mode === "sync" ? fatalError : null;
5197
+ if (lost !== null) return outcomes.map((outcome) => outcome.ok ? { ok: false, error: lost } : outcome);
5198
+ await directoryWatch.verifyOnce();
5199
+ directoryWatch.start();
5200
+ startCheckpointTimer();
5201
+ startAsyncFlushTimer();
4806
5202
  return outcomes;
4807
5203
  }
4808
5204
  function recoverIndex(indexName, metadataOnly = false) {
@@ -4824,10 +5220,13 @@ function createDurabilityManager(config, hooks, directoryOverride) {
4824
5220
  return true;
4825
5221
  },
4826
5222
  async recover(metadataOnly = false) {
5223
+ await directoryWatch.claim();
4827
5224
  const names = await listPersistedIndexes(directory);
4828
5225
  for (const indexName of names) {
4829
5226
  await recoverIndex(indexName, metadataOnly);
4830
5227
  }
5228
+ await directoryWatch.verify();
5229
+ directoryWatch.start();
4831
5230
  startCheckpointTimer();
4832
5231
  },
4833
5232
  recoverIndex(indexName) {
@@ -4889,6 +5288,7 @@ function createDurabilityManager(config, hooks, directoryOverride) {
4889
5288
  },
4890
5289
  async shutdown() {
4891
5290
  shuttingDown = true;
5291
+ directoryWatch.stop();
4892
5292
  if (checkpointTimer !== null) {
4893
5293
  clearInterval(checkpointTimer);
4894
5294
  checkpointTimer = null;
@@ -4907,6 +5307,7 @@ function createDurabilityManager(config, hooks, directoryOverride) {
4907
5307
  }
4908
5308
  indexes.clear();
4909
5309
  terminateCheckpointWorker();
5310
+ await directoryWatch.release();
4910
5311
  }
4911
5312
  };
4912
5313
  }
@@ -6298,18 +6699,6 @@ function eligibleIndexNames(state) {
6298
6699
  return names;
6299
6700
  }
6300
6701
 
6301
- // src/engine/orchestration/turn.ts
6302
- function afterCurrentTurn(callback) {
6303
- if (typeof setImmediate === "function") {
6304
- setImmediate(callback);
6305
- return;
6306
- }
6307
- setTimeout(callback, 0);
6308
- }
6309
- function yieldToEventLoop4() {
6310
- return new Promise((resolve) => afterCurrentTurn(resolve));
6311
- }
6312
-
6313
6702
  // src/engine/orchestration/replication.ts
6314
6703
  async function dispatchToWorkers(state, action) {
6315
6704
  const pool = state.workerPool;
@@ -6467,7 +6856,7 @@ async function replicateToWorkers(state, action) {
6467
6856
  const loading = state.copyLoadBuffers.get(action.indexName);
6468
6857
  if (loading !== void 0) {
6469
6858
  queueForCopies(state, detached);
6470
- if (loading.length % LOAD_BUFFER_YIELD_INTERVAL === 0) await yieldToEventLoop4();
6859
+ if (loading.length % LOAD_BUFFER_YIELD_INTERVAL === 0) await yieldToEventLoop();
6471
6860
  return;
6472
6861
  }
6473
6862
  const queue = state.replicationQueues.get(action.indexName);
@@ -6909,17 +7298,25 @@ function createWorkerExecutor(worker, config) {
6909
7298
  }
6910
7299
 
6911
7300
  // src/workers/factory.ts
6912
- function resolveEntryPoint() {
6913
- const base = import.meta.url;
6914
- const distIndex = base.lastIndexOf("/dist/");
6915
- if (distIndex !== -1) {
6916
- return new URL("workers/entry.mjs", base.slice(0, distIndex + 6)).href;
7301
+ async function entryFileExists(entry) {
7302
+ if (!entry.startsWith("file:")) return true;
7303
+ const [{ access }, { fileURLToPath }] = await Promise.all([import('fs/promises'), import('url')]);
7304
+ try {
7305
+ await access(fileURLToPath(entry));
7306
+ return true;
7307
+ } catch {
7308
+ return false;
6917
7309
  }
6918
- return base.replace(/\/src\/workers\/[^/]+$/, "/dist/workers/entry.mjs");
7310
+ }
7311
+ async function requireWorkerEntry() {
7312
+ const entry = resolveWorkerEntry(import.meta.url, /\/src\/workers\/[^/]+$/, "workers/entry.mjs");
7313
+ if (entry === null) throw missingWorkerEntry(import.meta.url);
7314
+ if (import.meta.url.includes("/dist/") && !await entryFileExists(entry)) throw missingWorkerEntry(import.meta.url);
7315
+ return entry;
6919
7316
  }
6920
7317
  async function createWorkerFactory(entryPoint) {
6921
7318
  const runtime = detectRuntime();
6922
- const resolvedEntry = resolveEntryPoint();
7319
+ const resolvedEntry = await requireWorkerEntry();
6923
7320
  if (runtime.supportsWorkerThreads) {
6924
7321
  const workerThreadsModule = await import('worker_threads');
6925
7322
  return function nodeFactory(workerId, onDeath, onGone) {
@@ -7119,10 +7516,13 @@ function createWorkerPool(config) {
7119
7516
  }
7120
7517
  isShutdown = true;
7121
7518
  const shutdownPromises = [...threadsStillRunning].map((executor) => {
7519
+ let timeoutId;
7122
7520
  const timeoutPromise = new Promise((resolve) => {
7123
- setTimeout(resolve, 5e3);
7521
+ timeoutId = setTimeout(resolve, SHUTDOWN_TIMEOUT_MS);
7124
7522
  });
7125
- return Promise.race([shutDownUntilTheThreadIsGone(executor), timeoutPromise]);
7523
+ return Promise.race([shutDownUntilTheThreadIsGone(executor), timeoutPromise]).finally(
7524
+ () => clearTimeout(timeoutId)
7525
+ );
7126
7526
  });
7127
7527
  await Promise.allSettled(shutdownPromises);
7128
7528
  indexAssignment.clear();
@@ -7553,8 +7953,12 @@ async function ensurePool(state) {
7553
7953
  function documentCountOf(state, indexName) {
7554
7954
  return state.executor.getManager(indexName)?.countDocuments() ?? 0;
7555
7955
  }
7556
- function copyThresholdReason(state, indexName) {
7557
- return `Index "${indexName}" holds ${documentCountOf(state, indexName)} documents, reaching the copy threshold of ${state.copyThreshold}`;
7956
+ function copyThresholdReason(state, indexName, incomingCount = 0) {
7957
+ const held = documentCountOf(state, indexName);
7958
+ if (incomingCount <= 0) {
7959
+ return `Index "${indexName}" holds ${held} documents, reaching the copy threshold of ${state.copyThreshold}`;
7960
+ }
7961
+ return `Index "${indexName}" holds ${held} documents, and the batch that the engine is writing adds ${incomingCount}, for ${held + incomingCount} against the copy threshold of ${state.copyThreshold}`;
7558
7962
  }
7559
7963
  function indexReadyForCopies(state, indexName, incomingCount = 0) {
7560
7964
  if (!copiesAllowed(state)) return false;
@@ -7643,7 +8047,7 @@ async function scaleOutBeforeBatch(state, indexName, incomingCount) {
7643
8047
  return;
7644
8048
  }
7645
8049
  if (!indexReadyForCopies(state, indexName, incomingCount)) return;
7646
- await scaleOutIndex(state, indexName, copyThresholdReason(state, indexName));
8050
+ await scaleOutIndex(state, indexName, copyThresholdReason(state, indexName, incomingCount));
7647
8051
  }
7648
8052
 
7649
8053
  // src/engine/orchestration/idle.ts
@@ -7723,7 +8127,7 @@ function partitionsPerLease(scope, leaseCount) {
7723
8127
  }
7724
8128
  return assignments;
7725
8129
  }
7726
- function mergeWorkerResults(results) {
8130
+ function mergeWorkerResults(results, facetConfig) {
7727
8131
  const merged = kWayMerge(results.map((result) => result.scored));
7728
8132
  let totalMatched = 0;
7729
8133
  const workerFacets = [];
@@ -7734,7 +8138,7 @@ function mergeWorkerResults(results) {
7734
8138
  return {
7735
8139
  scored: merged,
7736
8140
  totalMatched,
7737
- facets: workerFacets.length > 0 ? mergeFacets(workerFacets) : void 0
8141
+ facets: facetConfig !== void 0 && workerFacets.length > 0 ? mergeFacets(workerFacets, facetConfig) : void 0
7738
8142
  };
7739
8143
  }
7740
8144
  function scoresPerPartition(state, indexName, params, globalStats) {
@@ -7754,12 +8158,13 @@ function queryAction(indexName, params, globalStats, partitionIds) {
7754
8158
  }
7755
8159
  async function runSplit(leases, scope, indexName, params, globalStats) {
7756
8160
  const assignments = partitionsPerLease(scope, leases.length);
8161
+ const workerParams = params.facets !== void 0 ? { ...params, facets: oversampledFacetConfig(params.facets) } : params;
7757
8162
  const results = await Promise.all(
7758
8163
  assignments.map(
7759
- (partitionIds, at) => leases[at].executor.execute(queryAction(indexName, params, globalStats, partitionIds)).finally(() => leases[at].release())
8164
+ (partitionIds, at) => leases[at].executor.execute(queryAction(indexName, workerParams, globalStats, partitionIds)).finally(() => leases[at].release())
7760
8165
  )
7761
8166
  );
7762
- return mergeWorkerResults(results);
8167
+ return mergeWorkerResults(results, params.facets);
7763
8168
  }
7764
8169
  function takeMainCopyTurn(state) {
7765
8170
  if (state.mainCopyQueries === "none" || state.mainCopyTurnTaken) return false;
@@ -8119,7 +8524,7 @@ function createEngineCore(config, hooks) {
8119
8524
  const rebalanceTargets = /* @__PURE__ */ new Map();
8120
8525
  function guardShutdown() {
8121
8526
  if (shutdownState.isShutdown) {
8122
- throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, "This Narsil instance has been shut down");
8527
+ throw new NarsilError(ErrorCodes.INSTANCE_SHUT_DOWN, "This Narsil instance has been shut down");
8123
8528
  }
8124
8529
  }
8125
8530
  function requireIndex(indexName) {
@@ -8286,6 +8691,82 @@ function createEngineCore(config, hooks) {
8286
8691
  };
8287
8692
  }
8288
8693
 
8694
+ // src/engine/lifecycle.ts
8695
+ async function startEngineCore(core, config) {
8696
+ try {
8697
+ if (core.durability) {
8698
+ await core.durability.manager.recover(config?.lifecycle !== void 0);
8699
+ checkHeapAfterRecovery(core);
8700
+ }
8701
+ if (core.invalidation) await core.invalidation.start();
8702
+ if (config?.lifecycle === void 0) await core.analysisRebuild.reviewStaleIndexes();
8703
+ } catch (err) {
8704
+ await core.durability?.manager.shutdown().catch(() => void 0);
8705
+ await shutdownEngine(core).catch(() => void 0);
8706
+ throw err;
8707
+ }
8708
+ }
8709
+ async function shutdownEngine(core) {
8710
+ const { executor, durability, indexRegistry, eventHandlers, orchestrator } = core;
8711
+ core.indexState.dispose();
8712
+ for (const [name] of indexRegistry) {
8713
+ const manager = executor.getManager(name);
8714
+ if (manager) {
8715
+ for (const [, vectorIndex] of manager.getVectorIndexes()) {
8716
+ vectorIndex.dispose();
8717
+ }
8718
+ }
8719
+ }
8720
+ if (durability) {
8721
+ await durability.manager.shutdown();
8722
+ }
8723
+ if (core.invalidation) {
8724
+ await core.invalidation.shutdown();
8725
+ }
8726
+ const adaptersToShutdown = /* @__PURE__ */ new Set();
8727
+ for (const [, entry] of indexRegistry) {
8728
+ if (entry.embeddingAdapter?.shutdown) {
8729
+ adaptersToShutdown.add(entry.embeddingAdapter);
8730
+ }
8731
+ }
8732
+ for (const adapter of adaptersToShutdown) {
8733
+ try {
8734
+ await adapter.shutdown?.();
8735
+ } catch {
8736
+ }
8737
+ }
8738
+ await orchestrator.shutdown();
8739
+ await executor.shutdown();
8740
+ eventHandlers.clear();
8741
+ indexRegistry.clear();
8742
+ }
8743
+
8744
+ // src/engine/startup-checks.ts
8745
+ var announcedMissingWorkerEntry = false;
8746
+ function withoutWorkers(config, reason) {
8747
+ if (!announcedMissingWorkerEntry) {
8748
+ announcedMissingWorkerEntry = true;
8749
+ console.warn(
8750
+ `Narsil answers every query on this thread, because it finds no worker entry beside its own module at "${String(reason.details.moduleUrl)}", which happens where a bundler folds @delali/narsil into an application bundle. Keep @delali/narsil outside the bundle to use worker threads, or set workers.enabled to false to silence this notice.`
8751
+ );
8752
+ }
8753
+ return { ...config, workers: { ...config?.workers, enabled: false } };
8754
+ }
8755
+ async function resolveRunnableConfig(config) {
8756
+ requireSearchBackendSettings();
8757
+ if (!(config?.workers?.enabled ?? workersEnabledByDefault())) return config;
8758
+ const runtime = detectRuntime();
8759
+ if (!runtime.supportsWorkerThreads && !runtime.supportsWebWorkers) return config;
8760
+ try {
8761
+ await requireWorkerEntry();
8762
+ return config;
8763
+ } catch (err) {
8764
+ const missingEntry = err instanceof NarsilError && err.code === ErrorCodes.CONFIG_INVALID;
8765
+ if (!missingEntry || config?.workers?.enabled === true) throw err;
8766
+ return withoutWorkers(config, err);
8767
+ }
8768
+ }
8769
+
8289
8770
  // src/engine/index-lifecycle.ts
8290
8771
  async function runLifecycleHook(core, hookName, context) {
8291
8772
  try {
@@ -8419,42 +8900,6 @@ function registerEngineEmbeddingAdapter(core, name, adapter) {
8419
8900
  }
8420
8901
  }
8421
8902
 
8422
- // src/engine/lifecycle.ts
8423
- async function shutdownEngine(core) {
8424
- const { executor, durability, indexRegistry, eventHandlers, orchestrator } = core;
8425
- core.indexState.dispose();
8426
- for (const [name] of indexRegistry) {
8427
- const manager = executor.getManager(name);
8428
- if (manager) {
8429
- for (const [, vectorIndex] of manager.getVectorIndexes()) {
8430
- vectorIndex.dispose();
8431
- }
8432
- }
8433
- }
8434
- if (durability) {
8435
- await durability.manager.shutdown();
8436
- }
8437
- if (core.invalidation) {
8438
- await core.invalidation.shutdown();
8439
- }
8440
- const adaptersToShutdown = /* @__PURE__ */ new Set();
8441
- for (const [, entry] of indexRegistry) {
8442
- if (entry.embeddingAdapter?.shutdown) {
8443
- adaptersToShutdown.add(entry.embeddingAdapter);
8444
- }
8445
- }
8446
- for (const adapter of adaptersToShutdown) {
8447
- try {
8448
- await adapter.shutdown?.();
8449
- } catch {
8450
- }
8451
- }
8452
- await orchestrator.shutdown();
8453
- await executor.shutdown();
8454
- eventHandlers.clear();
8455
- indexRegistry.clear();
8456
- }
8457
-
8458
8903
  // src/engine/mutations/durable-rollback.ts
8459
8904
  function rollbackFailed(docId, originalError, rollbackError) {
8460
8905
  throw new NarsilError(
@@ -8822,6 +9267,7 @@ async function applyAdmittedDocuments(ctx, indexName, admitted, options, failed)
8822
9267
  }
8823
9268
  const applied = chunkStart + BATCH_CHUNK_SIZE;
8824
9269
  if (applied < admitted.length) {
9270
+ ctx.checkHeapPressure(indexName);
8825
9271
  await new Promise((r) => setTimeout(r, 0));
8826
9272
  }
8827
9273
  }
@@ -9061,6 +9507,7 @@ async function recordMergedDocuments(ctx, indexName, built, frozen, memberIndexe
9061
9507
  succeededIds.add(doc.docId);
9062
9508
  }
9063
9509
  if (index + 1 < chunks.length) {
9510
+ ctx.checkHeapPressure(indexName);
9064
9511
  await new Promise((r) => setTimeout(r, 0));
9065
9512
  }
9066
9513
  }
@@ -9272,6 +9719,7 @@ async function insertDocumentBatch(ctx, indexName, documents, options) {
9272
9719
  }
9273
9720
  if (prepared.length > 0) await applyPrepared(prepared);
9274
9721
  if (chunkEnd < documents.length) {
9722
+ ctx.checkHeapPressure(indexName);
9275
9723
  await new Promise((r) => setTimeout(r, 0));
9276
9724
  }
9277
9725
  }
@@ -9469,6 +9917,18 @@ async function settleUpdate(ctx, indexName, prepared) {
9469
9917
  async function prepareUpdate(ctx, indexName, entry, docId, document) {
9470
9918
  ctx.guardShutdown();
9471
9919
  validateDocId(docId);
9920
+ const manager = ctx.requireManager(indexName);
9921
+ if (ctx.pluginRegistry.hasHooks("beforeUpdate")) {
9922
+ await ctx.pluginRegistry.runHook("beforeUpdate", {
9923
+ indexName,
9924
+ docId,
9925
+ oldDocument: manager.get(docId) ?? {},
9926
+ newDocument: document
9927
+ });
9928
+ }
9929
+ if (entry.config.required && entry.config.required.length > 0) {
9930
+ validateRequiredFields(document, entry.config.required);
9931
+ }
9472
9932
  if (entry.config.embedding) {
9473
9933
  if (entry.embeddingAdapter) {
9474
9934
  await embedDocumentFields(
@@ -9485,16 +9945,9 @@ async function prepareUpdate(ctx, indexName, entry, docId, document) {
9485
9945
  );
9486
9946
  }
9487
9947
  }
9488
- const manager = ctx.requireManager(indexName);
9489
9948
  const oldDocument = manager.get(docId);
9490
9949
  const oldPartitionDoc = manager.getRef(docId);
9491
9950
  const rollbackDoc = oldPartitionDoc ? structuredClone(oldPartitionDoc) : void 0;
9492
- await ctx.pluginRegistry.runHook("beforeUpdate", {
9493
- indexName,
9494
- docId,
9495
- oldDocument: oldDocument ?? {},
9496
- newDocument: document
9497
- });
9498
9951
  const vecIndexes = manager.getVectorIndexes();
9499
9952
  const extractedVectors = /* @__PURE__ */ new Map();
9500
9953
  if (vecIndexes.size > 0) {
@@ -9613,6 +10066,7 @@ async function updateDocumentBatch(ctx, indexName, updates, options) {
9613
10066
  succeeded.push(update.docId);
9614
10067
  }
9615
10068
  if (chunkEnd < updates.length) {
10069
+ ctx.checkHeapPressure(indexName);
9616
10070
  await new Promise((r) => setTimeout(r, 0));
9617
10071
  }
9618
10072
  }
@@ -9857,7 +10311,7 @@ async function executeRebalance(manager, indexName, targetPartitionCount, ctx) {
9857
10311
  await replayQueued(manager, indexName, waq, ctx);
9858
10312
  if (ctx.durabilityManager) {
9859
10313
  await ctx.durabilityManager.persistMetadata(indexName);
9860
- await ctx.durabilityManager.checkpoint(indexName);
10314
+ await (ctx.durabilityManager.checkpointFromMemory?.(indexName) ?? ctx.durabilityManager.checkpoint(indexName));
9861
10315
  await replayQueued(manager, indexName, waq, ctx);
9862
10316
  }
9863
10317
  await ctx.orchestrator.resyncIndex(indexName, wasPromoted);
@@ -10336,6 +10790,10 @@ function readProcessMemory() {
10336
10790
  }
10337
10791
 
10338
10792
  // src/narsil/reads.ts
10793
+ async function enterRead(core, indexName) {
10794
+ core.guardShutdown();
10795
+ if (core.rebalancingIndexes.has(indexName)) await yieldToEventLoop();
10796
+ }
10339
10797
  function scopedParams(params, options) {
10340
10798
  if (options?.globalStats === void 0) {
10341
10799
  return params;
@@ -10354,7 +10812,7 @@ function broadcastStatsFor(core, options) {
10354
10812
  return (name) => invalidation.broadcastStats(name);
10355
10813
  }
10356
10814
  async function runEngineQuery(core, indexName, params, options) {
10357
- core.guardShutdown();
10815
+ await enterRead(core, indexName);
10358
10816
  const release = await core.indexState.acquire(indexName);
10359
10817
  try {
10360
10818
  const entry = core.requireIndex(indexName);
@@ -10395,7 +10853,7 @@ async function runEngineQuery(core, indexName, params, options) {
10395
10853
  }
10396
10854
  }
10397
10855
  async function runEnginePreflight(core, indexName, params, options) {
10398
- core.guardShutdown();
10856
+ await enterRead(core, indexName);
10399
10857
  const release = await core.indexState.acquire(indexName);
10400
10858
  try {
10401
10859
  const entry = core.requireIndex(indexName);
@@ -10428,7 +10886,7 @@ async function runEnginePreflight(core, indexName, params, options) {
10428
10886
  }
10429
10887
  }
10430
10888
  async function runEngineSuggest(core, indexName, params, partitionIds) {
10431
- core.guardShutdown();
10889
+ await enterRead(core, indexName);
10432
10890
  const release = await core.indexState.acquire(indexName);
10433
10891
  try {
10434
10892
  const result = executeSuggest(
@@ -10446,7 +10904,7 @@ async function runEngineSuggest(core, indexName, params, partitionIds) {
10446
10904
  }
10447
10905
  }
10448
10906
  async function runEngineQueryStats(core, indexName, terms, partitionIds) {
10449
- core.guardShutdown();
10907
+ await enterRead(core, indexName);
10450
10908
  const release = await core.indexState.acquire(indexName);
10451
10909
  try {
10452
10910
  const entry = core.requireIndex(indexName);
@@ -10471,13 +10929,14 @@ async function runEngineQueryStats(core, indexName, terms, partitionIds) {
10471
10929
  }
10472
10930
  }
10473
10931
  async function runEngineListDocuments(core, indexName, params, partitionIds) {
10474
- core.guardShutdown();
10932
+ await enterRead(core, indexName);
10475
10933
  const release = await core.indexState.acquire(indexName);
10476
10934
  try {
10477
10935
  const entry = core.requireIndex(indexName);
10478
10936
  return executeListDocuments(params, {
10479
10937
  manager: core.requireManager(indexName),
10480
10938
  schema: entry.config.schema,
10939
+ strict: entry.config.strict,
10481
10940
  partitionIds
10482
10941
  });
10483
10942
  } finally {
@@ -10784,19 +11243,14 @@ function createNarsilFromCore(core, config) {
10784
11243
 
10785
11244
  // src/narsil/index.ts
10786
11245
  async function createNarsil(config) {
10787
- const core = createEngineCore(config);
10788
- if (core.durability) {
10789
- await core.durability.manager.recover(config?.lifecycle !== void 0);
10790
- checkHeapAfterRecovery(core);
10791
- }
10792
- if (core.invalidation) {
10793
- await core.invalidation.start();
10794
- }
10795
- if (config?.lifecycle === void 0) await core.analysisRebuild.reviewStaleIndexes();
10796
- return createNarsilFromCore(core, config);
11246
+ requireKnownConfig(config);
11247
+ const runnableConfig = await resolveRunnableConfig(config);
11248
+ const core = createEngineCore(runnableConfig);
11249
+ await startEngineCore(core, runnableConfig);
11250
+ return createNarsilFromCore(core, runnableConfig);
10797
11251
  }
10798
11252
 
10799
11253
  // src/index.ts
10800
11254
  var VERSION = "0.1.0";
10801
11255
 
10802
- export { CHECKPOINT_WORKER_HEARTBEAT_MS, VERSION, applyDeleteEntry, applyIndexEntry, checkHeapAfterRecovery, computeEntryChecksum, createAckMessage, createDurableDirectory, createEngineCore, createEngineIndex, createEntryBatchMessage, createEntryMessage, createForwardBatchMessage, createForwardBatchResultMessage, createForwardMessage, createInsyncAddMessage, createInsyncConfirmMessage, createInsyncRemoveMessage, createNarsil, createNarsilFromCore, runEngineListDocuments, runEnginePreflight, runEngineQuery, runEngineQueryStats, runEngineSuggest, validateAckPayload, validateEntryBatchPayload, validateEntryPayload, validateForwardBatchPayload, validateForwardBatchResultPayload, validateInsyncConfirmPayload, validateReplicationEntry, writeCheckpointSegments };
11256
+ export { CHECKPOINT_WORKER_HEARTBEAT_MS, VERSION, applyDeleteEntry, applyIndexEntry, computeEntryChecksum, createAckMessage, createDurableDirectory, createEngineCore, createEngineIndex, createEntryBatchMessage, createEntryMessage, createForwardBatchMessage, createForwardBatchResultMessage, createForwardMessage, createInsyncAddMessage, createInsyncConfirmMessage, createInsyncRemoveMessage, createNarsil, createNarsilFromCore, requireKnownConfig, resolveRunnableConfig, runEngineListDocuments, runEnginePreflight, runEngineQuery, runEngineQueryStats, runEngineSuggest, startEngineCore, validateAckPayload, validateEntryBatchPayload, validateEntryPayload, validateForwardBatchPayload, validateForwardBatchResultPayload, validateInsyncConfirmPayload, validateReplicationEntry, writeCheckpointSegments };