@delali/narsil 0.2.4-next.51 → 0.2.4-next.54

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 (64) hide show
  1. package/README.md +33 -27
  2. package/dist/adapters/filesystem.mjs +1 -1
  3. package/dist/adapters/indexeddb.mjs +1 -1
  4. package/dist/{capabilities-BWm4IITU.d.ts → capabilities-DlYw0Ov0.d.ts} +13 -2
  5. package/dist/{chunk-KMMJMDUA.mjs → chunk-3L4U6EJQ.mjs} +15 -10
  6. package/dist/chunk-4NVPQ6S5.mjs +366 -0
  7. package/dist/{chunk-CMVG2UEF.mjs → chunk-5T6AOR37.mjs} +56 -518
  8. package/dist/{chunk-ND34I3SV.mjs → chunk-BH5SYB5W.mjs} +3295 -2149
  9. package/dist/{chunk-XE6XR2OE.mjs → chunk-D2S642IV.mjs} +4 -2
  10. package/dist/{chunk-P4ELXVJQ.mjs → chunk-FRQVMFY7.mjs} +7 -1
  11. package/dist/chunk-IFBKLBGB.mjs +7 -0
  12. package/dist/chunk-L225SHNB.mjs +513 -0
  13. package/dist/{chunk-MHE7YWAU.mjs → chunk-NTZYA3SJ.mjs} +3 -3
  14. package/dist/{chunk-WYJXSLER.mjs → chunk-POWOTUKJ.mjs} +7 -1
  15. package/dist/{chunk-CV5H7XL7.mjs → chunk-RGS7FMCB.mjs} +53 -10
  16. package/dist/{chunk-ZHUQKZBE.mjs → chunk-TVT5V4E7.mjs} +101 -34
  17. package/dist/chunk-U2AL3SON.mjs +4198 -0
  18. package/dist/chunk-UTOFOJOR.mjs +135 -0
  19. package/dist/{chunk-BEIK3EOZ.mjs → chunk-XNQAIO74.mjs} +5523 -3847
  20. package/dist/chunk-Y3L5MDRV.mjs +96 -0
  21. package/dist/chunk-ZJ4CSAB2.mjs +13 -0
  22. package/dist/{client-B_X6_AbT.d.ts → client-CQUqt9Ir.d.ts} +13 -4
  23. package/dist/client.d.ts +8 -8
  24. package/dist/client.mjs +20 -14
  25. package/dist/{config-Czh2YNDi.d.ts → config-CNas6wJH.d.ts} +2 -2
  26. package/dist/{config-OON7IJ_B.d.ts → config-bXxnJrQc.d.ts} +80 -35
  27. package/dist/direct-executor-RBA5O7EN.mjs +12 -0
  28. package/dist/distribution/coordinator/etcd.d.ts +2 -2
  29. package/dist/distribution/coordinator/etcd.mjs +1 -1
  30. package/dist/distribution/coordinator/in-memory.d.ts +2 -2
  31. package/dist/distribution/transport/grpc.mjs +1 -1
  32. package/dist/distribution.d.ts +8 -8
  33. package/dist/distribution.mjs +32 -17
  34. package/dist/embeddings/openai.mjs +1 -1
  35. package/dist/{engine-9PtFY50q.d.ts → engine-Dj4I91Jv.d.ts} +21 -8
  36. package/dist/index.browser.mjs +12962 -7786
  37. package/dist/index.d.ts +9 -21
  38. package/dist/index.mjs +14 -13
  39. package/dist/{memory-Bzd5xcEW.d.ts → memory-xrXOZMbw.d.ts} +68 -13
  40. package/dist/persistence/durability/checkpoint-worker.d.ts +138 -3
  41. package/dist/persistence/durability/checkpoint-worker.mjs +32 -18
  42. package/dist/react.d.ts +11 -11
  43. package/dist/react.mjs +16 -4
  44. package/dist/{schema-BqRx9t4E.d.ts → schema-mLpXZBEh.d.ts} +43 -12
  45. package/dist/serialization/crc32-worker.mjs +2 -1
  46. package/dist/server.d.ts +13 -10
  47. package/dist/server.mjs +136 -47
  48. package/dist/simd-DW5AtJ6B.d.ts +41 -0
  49. package/dist/{tasks-DT7L1p6w.d.ts → tasks-Df73Tjgq.d.ts} +9 -5
  50. package/dist/{thread-PZLEO6JL.mjs → thread-5UP6MGED.mjs} +131 -43
  51. package/dist/{types-DXPxaXPO.d.ts → types-BTIVO8DI.d.ts} +1 -1
  52. package/dist/vector/search-worker.d.ts +292 -128
  53. package/dist/vector/search-worker.mjs +70 -54
  54. package/dist/workers/entry.mjs +30 -14
  55. package/package.json +52 -31
  56. package/dist/brute-force-DbHYCTc3.d.ts +0 -8
  57. package/dist/chunk-CBLS46IE.mjs +0 -10
  58. package/dist/chunk-PIWKZPP7.mjs +0 -64
  59. package/dist/chunk-PMBHH44R.mjs +0 -492
  60. package/dist/chunk-RP23PWMV.mjs +0 -11
  61. package/dist/chunk-X6YZO4MQ.mjs +0 -1572
  62. package/dist/direct-executor-OREXU72C.mjs +0 -12
  63. package/dist/vector/hnsw-build-worker.d.ts +0 -105
  64. package/dist/vector/hnsw-build-worker.mjs +0 -123
@@ -40,14 +40,36 @@ type SchemaDefinition = {
40
40
  [field: string]: FieldType | SchemaDefinition;
41
41
  };
42
42
  /**
43
- * How the engine stores vectors once a field is promoted to an HNSW graph.
43
+ * How the engine codes vectors once a field is promoted to an HNSW graph.
44
44
  *
45
- * `sq8` quantises each component to a signed byte, which cuts memory to a
46
- * quarter and costs a small amount of recall. `none` keeps full precision.
45
+ * Every `osq` mode applies optimised scalar quantization, which centres each
46
+ * vector on the field's centroid and then fits an interval to that vector
47
+ * alone. The digit in the mode name gives the bits that each dimension's code
48
+ * holds, so `osq8` stores a byte per dimension, while `osq4`, `osq2`, and
49
+ * `osq1` store four, two, and one bit. A search ranks its candidates by their
50
+ * codes before it re-scores the nearest of them against the full-precision
51
+ * vectors. `none` stores no code, so the engine ranks by the vectors alone.
52
+ *
53
+ * When the configuration leaves this unset, the engine takes `osq4` at 384
54
+ * dimensions and above and `osq8` below 384.
55
+ *
56
+ * @public
57
+ */
58
+ type VectorQuantizationMode = 'osq8' | 'osq4' | 'osq2' | 'osq1' | 'none';
59
+ /**
60
+ * Where a vector field keeps its full-precision vectors.
61
+ *
62
+ * A `memory` field holds them in memory. A `disk` field keeps its codes and
63
+ * its graph in memory, while it reads a full-precision vector from the
64
+ * field's checkpoint file whenever a search re-scores that candidate or a
65
+ * document read returns it. A `disk` field therefore needs filesystem
66
+ * durability. Asking for one on an engine without it fails with
67
+ * `CONFIG_INVALID`. When the configuration leaves this unset, an engine with
68
+ * filesystem durability takes `disk` and every other engine takes `memory`.
47
69
  *
48
70
  * @public
49
71
  */
50
- type VectorQuantizationMode = 'sq8' | 'none';
72
+ type VectorStorageMode = 'memory' | 'disk';
51
73
  /**
52
74
  * Controls when a vector field moves from a brute-force scan to an HNSW graph,
53
75
  * and how that graph is built.
@@ -74,8 +96,10 @@ interface VectorIndexConfig {
74
96
  efConstruction?: number;
75
97
  metric?: 'cosine' | 'dotProduct' | 'euclidean';
76
98
  };
77
- /** The engine stores the promoted vectors at this precision, and it quantises to `sq8` by default. Set `none` to keep full precision. */
99
+ /** The engine codes the promoted vectors in this mode. When this is unset, it picks the mode by the field's dimension. Set `none` to rank by the vectors alone. */
78
100
  quantization?: VectorQuantizationMode;
101
+ /** The engine keeps the field's full-precision vectors here. When this is unset, it picks `disk` under filesystem durability and `memory` on every other engine. */
102
+ storage?: VectorStorageMode;
79
103
  }
80
104
  /**
81
105
  * Connects an index to an embedding adapter and says which fields feed it.
@@ -190,19 +214,26 @@ interface CustomTokenizer {
190
214
  }>;
191
215
  }
192
216
  /**
193
- * How an index splits its documents across partitions as it grows.
217
+ * How many partitions an index opens with, and how many documents it accepts.
194
218
  *
195
- * The engine adds a partition once a partition passes its watermark, and it
196
- * refuses a write once every partition is full and the ceiling is reached.
219
+ * An index opens with `maxPartitions` partitions and keeps that count until
220
+ * {@link Narsil.rebalance} changes it, because the engine adds no partition on
221
+ * its own. Set `watermark` to learn through the `partitionWatermark` event
222
+ * when an index fills its capacity.
197
223
  *
198
224
  * @public
199
225
  */
200
226
  interface PartitionConfig {
201
- /** The engine refuses a write that would push one partition past this many documents. */
227
+ /**
228
+ * The index accepts this many documents for each of its partitions, so its
229
+ * capacity is this number multiplied by its partition count. A write beyond
230
+ * that capacity raises `PARTITION_CAPACITY_EXCEEDED`. Leave the field unset
231
+ * to accept any number.
232
+ */
202
233
  maxDocsPerPartition?: number;
203
- /** The index grows to this many partitions and no further. */
234
+ /** The index opens with this many partitions, 1 by default, and no rebalance may take it higher. */
204
235
  maxPartitions?: number;
205
- /** The engine adds a partition once one fills this fraction of `maxDocsPerPartition`, from 0 to 1. */
236
+ /** The engine emits `partitionWatermark` once the index fills this share of its capacity, above 0 and at most 1. */
206
237
  watermark?: number;
207
238
  }
208
239
  /**
@@ -250,4 +281,4 @@ interface InsertOptions extends WriteOptions {
250
281
  skipClone?: boolean;
251
282
  }
252
283
 
253
- export type { AnyDocument as A, BM25Params as B, CustomTokenizer as C, EmbeddingFieldConfig as E, FieldType as F, IndexConfig as I, PartitionConfig as P, SchemaDefinition as S, VectorIndexConfig as V, WriteOptions as W, ScoringMode as a, InsertOptions as b, StopWordOverride as c, VectorQuantizationMode as d };
284
+ export type { AnyDocument as A, BM25Params as B, CustomTokenizer as C, EmbeddingFieldConfig as E, FieldType as F, IndexConfig as I, PartitionConfig as P, SchemaDefinition as S, VectorIndexConfig as V, WriteOptions as W, InsertOptions as a, StopWordOverride as b, ScoringMode as c, VectorQuantizationMode as d, VectorStorageMode as e };
@@ -1,4 +1,5 @@
1
- import { crc32 } from '../chunk-KMMJMDUA.mjs';
1
+ import { crc32 } from '../chunk-3L4U6EJQ.mjs';
2
+ import '../chunk-IFBKLBGB.mjs';
2
3
 
3
4
  // src/serialization/crc32-worker.ts
4
5
  function handleRequest(raw) {
package/dist/server.d.ts CHANGED
@@ -1,14 +1,14 @@
1
- import { b as ServerOptions, N as NarsilServer } from './capabilities-BWm4IITU.js';
2
- export { A as ASYNC_IMPORT_CAPABILITY, C as CorsOptions, E as ErrorEnvelope, I as INDEX_LIFECYCLE_CAPABILITY, O as OnRequestHook, R as REBUILD_ANALYSIS_CAPABILITY, c as RequestContext, d as RequestDenial, S as SERVER_CAPABILITIES, e as ServerLimits, T as TASK_CANCEL_CAPABILITY, a as TASK_FILTER_CAPABILITY } from './capabilities-BWm4IITU.js';
3
- import { a as Narsil } from './engine-9PtFY50q.js';
4
- import { d as TaskProgress, b as ImportResult, h as TaskStore, e as TaskRecord } from './tasks-DT7L1p6w.js';
5
- export { C as CapabilitiesResponse, a as CreateIndexEmbedding, i as CreateIndexRequest, H as HttpIndexConfig, I as ImportError, M as MemoryStatsResponse, T as TaskListPage, c as TaskListQuery, f as TaskStatus, g as TaskType } from './tasks-DT7L1p6w.js';
6
- export { d as ServerErrorCodes } from './memory-Bzd5xcEW.js';
1
+ export { C as ClientErrorCode, a as ClientErrorCodes, E as ErrorCode, b as ErrorCodes, N as NarsilError, c as NarsilErrorCode, S as ServerErrorCode, d as ServerErrorCodes, i as isNarsilError } from './memory-xrXOZMbw.js';
2
+ import { b as ServerOptions, N as NarsilServer } from './capabilities-DlYw0Ov0.js';
3
+ export { A as ASYNC_IMPORT_CAPABILITY, C as CorsOptions, E as ErrorEnvelope, I as INDEX_LIFECYCLE_CAPABILITY, O as OnRequestHook, R as REBUILD_ANALYSIS_CAPABILITY, c as RequestContext, d as RequestDenial, S as SERVER_CAPABILITIES, e as ServerLimits, T as TASK_CANCEL_CAPABILITY, a as TASK_FILTER_CAPABILITY } from './capabilities-DlYw0Ov0.js';
4
+ import { a as Narsil } from './engine-Dj4I91Jv.js';
5
+ import { d as TaskProgress, b as ImportResult, h as TaskStore, e as TaskRecord } from './tasks-Df73Tjgq.js';
6
+ export { C as CapabilitiesResponse, a as CreateIndexEmbedding, i as CreateIndexRequest, H as HttpIndexConfig, I as ImportError, M as MemoryStatsResponse, T as TaskListPage, c as TaskListQuery, f as TaskStatus, g as TaskType } from './tasks-Df73Tjgq.js';
7
+ import './schema-mLpXZBEh.js';
7
8
  import './adapters-79OIa4xm.js';
8
- import './config-Czh2YNDi.js';
9
- import './config-OON7IJ_B.js';
10
- import './schema-BqRx9t4E.js';
11
- import './types-DXPxaXPO.js';
9
+ import './config-CNas6wJH.js';
10
+ import './config-bXxnJrQc.js';
11
+ import './types-BTIVO8DI.js';
12
12
  import './types-D5OwhtsQ.js';
13
13
 
14
14
  /**
@@ -59,6 +59,9 @@ type TaskOperation = (ctx: TaskContext) => Promise<void>;
59
59
  * @param options - Address, CORS, request limits, the authentication hook, and
60
60
  * the task store. Omit it to bind to loopback with the default limits.
61
61
  * @returns The server, ready for {@link NarsilServer.listen}.
62
+ * @throws A `NarsilError` with `CONFIG_INVALID` where the first argument is
63
+ * some value other than an engine, and where a field of `options.limits` holds
64
+ * a value outside its range.
62
65
  *
63
66
  * @public
64
67
  */
package/dist/server.mjs CHANGED
@@ -1,12 +1,14 @@
1
- import { DEFAULT_MAX_RETAINED_TASKS, loadUWebSockets, resolveCors, authorizerFor, createSharedGateBuffer, gateSlotOfWorker, createRouteRunner, corsWriter, createSharedRequestGate, MAIN_THREAD_GATE_SLOT, registerServerRoutes, DEFAULT_MAX_CONCURRENT_TASKS, DEFAULT_MAX_TASK_PAGE_SIZE, DEFAULT_MAX_IMPORT_ERRORS, DEFAULT_MAX_FETCH_DOCUMENTS, DEFAULT_MAX_RESULT_WINDOW, DEFAULT_MAX_CONCURRENT_REQUESTS, DEFAULT_IMPORT_BATCH_SIZE, DEFAULT_MAX_LINE_BYTES, DEFAULT_MAX_IMPORT_BYTES, DEFAULT_MAX_BODY_BYTES, RUNNING_TASK_TTL_MS, DEFAULT_TASK_PAGE_SIZE, TERMINAL_TASK_TTL_MS, TASK_PROGRESS_WRITE_INTERVAL_MS, serializeNarsilError, acceptorOf, createDocumentReadHandlers, sendError, respondError, respondJson, MAX_INDEX_TOUCH_INTERVAL_MS, INDEX_TOUCH_INTERVALS_PER_IDLE_TIMEOUT, clearGateSlot, parseJson, validateCreateIndex, rejectInvalid, validateBatch, serializeBatchResult, validateDocumentBody, validateSuggest, validateQuery, parseTaskListQuery, parseJsonOptional, validateRebalance, sendBinary, badRequest, SERVER_CAPABILITIES } from './chunk-CV5H7XL7.mjs';
2
- export { ASYNC_IMPORT_CAPABILITY, INDEX_LIFECYCLE_CAPABILITY, REBUILD_ANALYSIS_CAPABILITY, SERVER_CAPABILITIES, TASK_CANCEL_CAPABILITY, TASK_FILTER_CAPABILITY, httpStatusForNarsilError } from './chunk-CV5H7XL7.mjs';
3
- import { validateIndexName } from './chunk-MHE7YWAU.mjs';
4
- import { engineCoreOf, resolveVectorText } from './chunk-PIWKZPP7.mjs';
5
- import './chunk-XE6XR2OE.mjs';
1
+ import { DEFAULT_MAX_RETAINED_TASKS, loadUWebSockets, resolveCors, authorizerFor, createSharedGateBuffer, gateSlotOfWorker, createRouteRunner, corsWriter, createSharedRequestGate, MAIN_THREAD_GATE_SLOT, registerServerRoutes, DEFAULT_MAX_CONCURRENT_TASKS, DEFAULT_MAX_TASK_PAGE_SIZE, DEFAULT_MAX_IMPORT_ERRORS, DEFAULT_MAX_FETCH_DOCUMENTS, DEFAULT_MAX_RESULT_WINDOW, DEFAULT_MAX_CONCURRENT_REQUESTS, DEFAULT_IMPORT_BATCH_SIZE, DEFAULT_MAX_LINE_BYTES, DEFAULT_MAX_IMPORT_BYTES, DEFAULT_MAX_BODY_BYTES, RUNNING_TASK_TTL_MS, DEFAULT_TASK_PAGE_SIZE, TERMINAL_TASK_TTL_MS, TASK_PROGRESS_WRITE_INTERVAL_MS, serializeNarsilError, createVersionHandler, acceptorOf, createDocumentReadHandlers, sendError, respondError, respondJson, MAX_INDEX_TOUCH_INTERVAL_MS, INDEX_TOUCH_INTERVALS_PER_IDLE_TIMEOUT, clearGateSlot, parseJson, validateCreateIndex, rejectInvalid, validateBatch, serializeBatchResult, validateDocumentBody, validateSuggest, validateQuery, parseTaskListQuery, parseJsonOptional, validateRebalance, sendBinary, IMPORT_FLUSHES_IN_FLIGHT, badRequest, SERVER_CAPABILITIES, toHttpError, MIN_UNBOUNDED_SERVER_LIMIT, MIN_COUNTED_SERVER_LIMIT } from './chunk-RGS7FMCB.mjs';
2
+ export { ASYNC_IMPORT_CAPABILITY, INDEX_LIFECYCLE_CAPABILITY, REBUILD_ANALYSIS_CAPABILITY, SERVER_CAPABILITIES, TASK_CANCEL_CAPABILITY, TASK_FILTER_CAPABILITY, httpStatusForNarsilError } from './chunk-RGS7FMCB.mjs';
3
+ import { validateIndexName } from './chunk-NTZYA3SJ.mjs';
4
+ import { engineCoreOf, providedDocId, resolveVectorText } from './chunk-4NVPQ6S5.mjs';
6
5
  import { detectRuntime } from './chunk-AV4IJKD2.mjs';
7
- import './chunk-CBLS46IE.mjs';
8
- import { NarsilError, ServerErrorCodes, ErrorCodes } from './chunk-P4ELXVJQ.mjs';
9
- export { ServerErrorCodes } from './chunk-P4ELXVJQ.mjs';
6
+ import './chunk-D2S642IV.mjs';
7
+ import './chunk-L225SHNB.mjs';
8
+ import './chunk-UTOFOJOR.mjs';
9
+ import { startIdleHeapCleanup } from './chunk-Y3L5MDRV.mjs';
10
+ import { NarsilError, ErrorCodes, ServerErrorCodes, isNarsilError } from './chunk-FRQVMFY7.mjs';
11
+ export { ClientErrorCodes, ErrorCodes, NarsilError, ServerErrorCodes, isNarsilError } from './chunk-FRQVMFY7.mjs';
10
12
  import { randomUUID } from 'crypto';
11
13
  import { MessageChannel } from 'worker_threads';
12
14
 
@@ -444,10 +446,7 @@ async function runImport(engine, options) {
444
446
  failed += 1;
445
447
  if (errors.length < maxErrors) errors.push(error);
446
448
  };
447
- const flush = async () => {
448
- if (pending.length === 0) return;
449
- const documents = pending.map((entry) => entry.document);
450
- pending = [];
449
+ const flushDocuments = async (documents) => {
451
450
  const result = await engine.insertBatch(indexName, documents, { skipClone: true });
452
451
  indexed += result.succeeded.length;
453
452
  for (const failure of result.failed) {
@@ -455,36 +454,74 @@ async function runImport(engine, options) {
455
454
  recordFailure({ docId: failure.docId, code: serialized.code, message: serialized.message });
456
455
  }
457
456
  };
457
+ const inFlight = /* @__PURE__ */ new Set();
458
+ let flushError;
459
+ const startFlush = () => {
460
+ if (pending.length === 0) return;
461
+ const documents = pending.map((entry) => entry.document);
462
+ pending = [];
463
+ const run = flushDocuments(documents).catch((err) => {
464
+ flushError ??= err;
465
+ }).finally(() => inFlight.delete(run));
466
+ inFlight.add(run);
467
+ };
468
+ const settleFlushes = async () => {
469
+ await Promise.all(inFlight);
470
+ if (flushError !== void 0) throw flushError;
471
+ };
458
472
  const report = () => {
459
473
  onProgress?.({ indexed, failed, bytesProcessed, bytesTotal });
460
474
  };
461
- for (const line of iterateNdjson(body, maxLineBytes)) {
462
- bytesProcessed = line.bytesConsumed;
463
- let parsed;
464
- try {
465
- parsed = JSON.parse(line.text);
466
- } catch {
467
- recordFailure({ line: line.lineNumber, code: ServerErrorCodes.INVALID_JSON, message: "Line is not valid JSON" });
468
- continue;
469
- }
470
- if (!isPlainObject(parsed)) {
471
- recordFailure({
472
- line: line.lineNumber,
473
- code: ServerErrorCodes.INVALID_REQUEST,
474
- message: "Line is not a JSON object"
475
- });
476
- continue;
475
+ const seenIds = /* @__PURE__ */ new Set();
476
+ const repeated = [];
477
+ try {
478
+ for (const line of iterateNdjson(body, maxLineBytes)) {
479
+ bytesProcessed = line.bytesConsumed;
480
+ let parsed;
481
+ try {
482
+ parsed = JSON.parse(line.text);
483
+ } catch {
484
+ recordFailure({ line: line.lineNumber, code: ServerErrorCodes.INVALID_JSON, message: "Line is not valid JSON" });
485
+ continue;
486
+ }
487
+ if (!isPlainObject(parsed)) {
488
+ recordFailure({
489
+ line: line.lineNumber,
490
+ code: ServerErrorCodes.INVALID_REQUEST,
491
+ message: "Line is not a JSON object"
492
+ });
493
+ continue;
494
+ }
495
+ const docId = providedDocId(parsed);
496
+ if (docId !== void 0) {
497
+ if (seenIds.has(docId)) {
498
+ repeated.push({ document: parsed, line: line.lineNumber });
499
+ continue;
500
+ }
501
+ seenIds.add(docId);
502
+ }
503
+ pending.push({ document: parsed, line: line.lineNumber });
504
+ if (pending.length >= batchSize) {
505
+ signal?.throwIfAborted();
506
+ startFlush();
507
+ if (inFlight.size >= IMPORT_FLUSHES_IN_FLIGHT) await Promise.race(inFlight);
508
+ if (flushError !== void 0) await settleFlushes();
509
+ report();
510
+ await yieldToEventLoop();
511
+ signal?.throwIfAborted();
512
+ }
477
513
  }
478
- pending.push({ document: parsed, line: line.lineNumber });
479
- if (pending.length >= batchSize) {
480
- signal?.throwIfAborted();
481
- await flush();
482
- report();
483
- await yieldToEventLoop();
514
+ startFlush();
515
+ await settleFlushes();
516
+ for (let start = 0; start < repeated.length; start += batchSize) {
484
517
  signal?.throwIfAborted();
518
+ await flushDocuments(repeated.slice(start, start + batchSize).map((entry) => entry.document));
485
519
  }
520
+ } catch (err) {
521
+ await Promise.allSettled(inFlight);
522
+ report();
523
+ throw err;
486
524
  }
487
- await flush();
488
525
  bytesProcessed = bytesTotal;
489
526
  report();
490
527
  return { indexed, failed, errors, errorsTruncated: failed > errors.length };
@@ -711,12 +748,48 @@ function createSearchHandlers(deps) {
711
748
  return { search, preflight, suggest };
712
749
  }
713
750
 
714
- // src/server/handlers/version.ts
715
- function createVersionHandler(deps) {
716
- function report(ctx) {
717
- respondJson(ctx, { name: "narsil", ...deps.build });
751
+ // src/server/options-validation.ts
752
+ var ENGINE_METHOD_SAMPLE = ["createIndex", "listIndexes", "insert", "query", "get", "shutdown"];
753
+ var LIMITS_UNBOUNDED_AT_ZERO = [
754
+ "maxBodyBytes",
755
+ "maxImportBytes",
756
+ "maxLineBytes",
757
+ "maxImportErrors",
758
+ "maxConcurrentRequests",
759
+ "maxConcurrentTasks"
760
+ ];
761
+ var COUNTED_LIMITS = ["importBatchSize", "maxResultWindow", "maxFetchDocuments", "maxTaskPageSize"];
762
+ function assertServerEngine(engine) {
763
+ const candidate = engine;
764
+ if (typeof candidate !== "object" || candidate === null) {
765
+ throw new NarsilError(
766
+ ErrorCodes.CONFIG_INVALID,
767
+ "The first argument to createServer must be the engine that createNarsil returns, with the options second",
768
+ { received: candidate === null ? "null" : typeof candidate }
769
+ );
718
770
  }
719
- return { report };
771
+ const members = candidate;
772
+ const missing = ENGINE_METHOD_SAMPLE.filter((method) => typeof members[method] !== "function");
773
+ if (missing.length === 0) return;
774
+ throw new NarsilError(
775
+ ErrorCodes.CONFIG_INVALID,
776
+ `The first argument to createServer offers no ${missing.join(", ")}, so it is some value other than the engine that createNarsil returns`,
777
+ { missing }
778
+ );
779
+ }
780
+ function assertLimit(field, value, minimum) {
781
+ if (value === void 0) return;
782
+ if (Number.isInteger(value) && value >= minimum) return;
783
+ throw new NarsilError(
784
+ ErrorCodes.CONFIG_INVALID,
785
+ `limits.${field} must be a whole number of at least ${minimum}; however, this configuration sets it to ${String(value)}`,
786
+ { field, value, minimum }
787
+ );
788
+ }
789
+ function assertServerLimits(limits) {
790
+ if (limits === void 0) return;
791
+ for (const field of LIMITS_UNBOUNDED_AT_ZERO) assertLimit(field, limits[field], MIN_UNBOUNDED_SERVER_LIMIT);
792
+ for (const field of COUNTED_LIMITS) assertLimit(field, limits[field], MIN_COUNTED_SERVER_LIMIT);
720
793
  }
721
794
 
722
795
  // src/server/request-threads/captured-response.ts
@@ -804,7 +877,7 @@ async function answerRequest(handlers, request) {
804
877
  params: request.params,
805
878
  query: new URLSearchParams(request.query),
806
879
  contentType: request.contentType,
807
- rawBody: request.rawBody === null ? null : Buffer.from(request.rawBody),
880
+ rawBody: request.rawBody === null ? null : Buffer.from(request.rawBody.buffer, request.rawBody.byteOffset, request.rawBody.byteLength),
808
881
  abort: NO_ABORT,
809
882
  hookContext: request.hookContext
810
883
  };
@@ -848,8 +921,8 @@ async function answerEmbed(core, request) {
848
921
  error: null
849
922
  };
850
923
  } catch (err) {
851
- const error = err instanceof NarsilError ? serializeNarsilError(err) : { code: "INTERNAL_ERROR", message: err instanceof Error ? err.message : String(err) };
852
- return { type: "embedded", requestId: request.requestId, value: null, error };
924
+ if (!isNarsilError(err)) console.warn("Embedding a query for a request thread failed:", err);
925
+ return { type: "embedded", requestId: request.requestId, value: null, error: toHttpError(err).body };
853
926
  }
854
927
  }
855
928
  function touchIntervalFor(copyIdleTimeoutMs) {
@@ -952,6 +1025,12 @@ async function startRequestThreads(options) {
952
1025
  };
953
1026
  }
954
1027
 
1028
+ // src/server/task-status.ts
1029
+ var TERMINAL_TASK_STATUSES = /* @__PURE__ */ new Set(["succeeded", "failed", "cancelled"]);
1030
+ function isTerminalTaskStatus(status) {
1031
+ return TERMINAL_TASK_STATUSES.has(status);
1032
+ }
1033
+
955
1034
  // src/server/task-store.ts
956
1035
  var InMemoryTaskStore = class {
957
1036
  /**
@@ -1004,7 +1083,7 @@ var InMemoryTaskStore = class {
1004
1083
  if (this.records.size <= this.maxRetained) return;
1005
1084
  for (const [id, record] of this.records) {
1006
1085
  if (this.records.size <= this.maxRetained) break;
1007
- if (record.status === "succeeded" || record.status === "failed") this.records.delete(id);
1086
+ if (isTerminalTaskStatus(record.status)) this.records.delete(id);
1008
1087
  }
1009
1088
  }
1010
1089
  };
@@ -1081,7 +1160,7 @@ var TaskRegistry = class {
1081
1160
  async cancel(id) {
1082
1161
  const stored = await this.store.get(id);
1083
1162
  if (!stored) return { outcome: "not-found", record: null };
1084
- if (stored.status !== "running" && stored.status !== "queued") {
1163
+ if (isTerminalTaskStatus(stored.status)) {
1085
1164
  return { outcome: "already-finished", record: stored };
1086
1165
  }
1087
1166
  const task = this.live.get(id);
@@ -1110,7 +1189,7 @@ var TaskRegistry = class {
1110
1189
  }
1111
1190
  for (const record of records) {
1112
1191
  if (record.owner !== this.instanceId) continue;
1113
- if (record.status !== "running" && record.status !== "queued") continue;
1192
+ if (isTerminalTaskStatus(record.status)) continue;
1114
1193
  const failed = {
1115
1194
  ...record,
1116
1195
  status: "failed",
@@ -1262,7 +1341,10 @@ var NarsilHttpServer = class {
1262
1341
  listenSocket = null;
1263
1342
  ready = false;
1264
1343
  threads = null;
1344
+ stopIdleHeapCleanup = () => void 0;
1265
1345
  constructor(engine, options = {}) {
1346
+ assertServerEngine(engine);
1347
+ assertServerLimits(options.limits);
1266
1348
  this.host = options.host ?? "127.0.0.1";
1267
1349
  this.port = options.port ?? 9876;
1268
1350
  this.options = options;
@@ -1301,10 +1383,13 @@ var NarsilHttpServer = class {
1301
1383
  });
1302
1384
  });
1303
1385
  await this.deps.tasks.reconcile();
1386
+ this.stopIdleHeapCleanup = startIdleHeapCleanup();
1304
1387
  this.ready = true;
1305
1388
  }
1306
1389
  async close() {
1307
1390
  this.ready = false;
1391
+ this.stopIdleHeapCleanup();
1392
+ this.stopIdleHeapCleanup = () => void 0;
1308
1393
  if (this.listenSocket && this.uws) {
1309
1394
  this.uws.us_listen_socket_close(this.listenSocket);
1310
1395
  this.listenSocket = null;
@@ -1315,6 +1400,10 @@ var NarsilHttpServer = class {
1315
1400
  await threads.stop();
1316
1401
  }
1317
1402
  }
1403
+ async shutdown() {
1404
+ await this.close();
1405
+ await this.engine.shutdown();
1406
+ }
1318
1407
  get listeningPort() {
1319
1408
  if (!this.listenSocket || !this.uws) return -1;
1320
1409
  return this.uws.us_socket_local_port(this.listenSocket);
@@ -1336,7 +1425,7 @@ var NarsilHttpServer = class {
1336
1425
  writeCors: cors ? corsWriter(cors) : void 0
1337
1426
  });
1338
1427
  registerServerRoutes(app, run, handlers, limits, cors);
1339
- if (core === void 0 || !detectRuntime().supportsWorkerThreads) return;
1428
+ if (core === void 0 || threadCount === 0 || !detectRuntime().supportsWorkerThreads) return;
1340
1429
  try {
1341
1430
  this.threads = await startRequestThreads({ core, app, handlers, authorize, gate, limits, build, cors });
1342
1431
  } catch (err) {
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Reports whether the runtime loaded the SIMD module that speeds up vector
3
+ * distance work.
4
+ *
5
+ * Vector search works either way, so read this to explain a difference in
6
+ * search latency between two hosts rather than to decide whether to search.
7
+ *
8
+ * @returns True once the SIMD module has loaded.
9
+ *
10
+ * @public
11
+ */
12
+ declare function isSimdAvailable(): boolean;
13
+ /**
14
+ * The distance kernels bound to one memory, which read their operands at byte
15
+ * offsets inside that memory so that a caller measures against a vector where
16
+ * it already lies.
17
+ *
18
+ * @internal
19
+ */
20
+ interface ArenaSimd {
21
+ /** The kernels read every operand out of this memory. */
22
+ memory: WebAssembly.Memory;
23
+ /** Returns the dot product of two float32 vectors at the given byte offsets. */
24
+ dot_product: (ptrA: number, ptrB: number, len: number) => number;
25
+ /** Returns the length of the float32 vector at the given byte offset. */
26
+ magnitude: (ptr: number, len: number) => number;
27
+ /** Returns the squared euclidean distance between two float32 vectors at the given byte offsets. */
28
+ squared_euclidean_distance: (ptrA: number, ptrB: number, len: number) => number;
29
+ /** Returns the integer dot product of two byte code vectors at the given byte offsets. */
30
+ dot_u8: (ptrA: number, ptrB: number, len: number) => number;
31
+ /** Returns the integer squared distance between two byte code vectors at the given byte offsets. */
32
+ sqdist_u8: (ptrA: number, ptrB: number, len: number) => number;
33
+ /** Returns the sum of level products of two 4-bit codes at the given byte offsets, each holding two levels per byte. */
34
+ osq_dot_nibbles_4x4: (ptrA: number, ptrB: number, bytes: number) => number;
35
+ /** Returns the sum of level products of a 1-bit or 2-bit code and a query staged as four bit planes. Each plane spans `planeBytes`, with one bit for each of the code's lowest level bits. */
36
+ osq_dot_bits: (ptrDoc: number, ptrPlanes: number, planeBytes: number, docBits: number) => number;
37
+ /** Returns the sum of level products of two 1-bit codes or two 2-bit codes at the given byte offsets. */
38
+ osq_dot_bits_pair: (ptrA: number, ptrB: number, bytes: number, bits: number) => number;
39
+ }
40
+
41
+ export { type ArenaSimd as A, isSimdAvailable as i };
@@ -1,4 +1,4 @@
1
- import { M as MemoryStats } from './memory-Bzd5xcEW.js';
1
+ import { M as MemoryStats } from './memory-xrXOZMbw.js';
2
2
 
3
3
  /** Declarative index configuration accepted over HTTP. Function-valued engine
4
4
  * options (custom tokenizer, stopWords-as-function, group reducer, embedding
@@ -144,12 +144,16 @@ type TaskType = 'optimizeVectors' | 'rebalance' | 'restore' | 'import' | 'rebuil
144
144
  /**
145
145
  * Where a long-running operation stands.
146
146
  *
147
- * A task reaches `cancelled` only when the work itself stopped early, so a
148
- * cancel that arrives too late leaves the task `succeeded`.
147
+ * A task starts `running`, because the server begins the work as it accepts
148
+ * the request. Where the server already drives
149
+ * {@link ServerLimits.maxConcurrentTasks} tasks, it answers 429
150
+ * `TOO_MANY_REQUESTS` and starts none. A task reaches `cancelled` only where
151
+ * the work itself stops early, so a cancel that arrives too late leaves the
152
+ * task `succeeded`.
149
153
  *
150
154
  * @public
151
155
  */
152
- type TaskStatus = 'queued' | 'running' | 'succeeded' | 'failed' | 'cancelled';
156
+ type TaskStatus = 'running' | 'succeeded' | 'failed' | 'cancelled';
153
157
  /**
154
158
  * How far a running import has gone, which is what a progress bar reads.
155
159
  *
@@ -192,7 +196,7 @@ interface TaskRecord {
192
196
  owner: string;
193
197
  /** The server accepted the task at this many milliseconds since the epoch. */
194
198
  createdAt: number;
195
- /** Work started at this many milliseconds since the epoch, and a queued task omits it. */
199
+ /** Work started at this many milliseconds since the epoch. */
196
200
  startedAt?: number;
197
201
  /** Work ended at this many milliseconds since the epoch, and a task omits it until it reaches a final status. */
198
202
  completedAt?: number;