@delali/narsil 0.2.2-next.29 → 0.2.3-next.32

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 (66) hide show
  1. package/README.md +12 -9
  2. package/dist/adapters/filesystem.d.ts +1 -1
  3. package/dist/adapters/filesystem.mjs +1 -1
  4. package/dist/adapters/indexeddb.d.ts +1 -1
  5. package/dist/adapters/indexeddb.mjs +1 -1
  6. package/dist/adapters/memory.d.ts +1 -1
  7. package/dist/{adapters-CVqI5NBt.d.ts → adapters-79OIa4xm.d.ts} +1 -1
  8. package/dist/brute-force-DbHYCTc3.d.ts +8 -0
  9. package/dist/chunk-BFIH7OV5.mjs +185 -0
  10. package/dist/chunk-C6VVLMRX.mjs +63 -0
  11. package/dist/{chunk-UNJZ7ZDB.mjs → chunk-EMI63EXP.mjs} +33 -2
  12. package/dist/chunk-MFFAJ2NR.mjs +135 -0
  13. package/dist/chunk-OBXS7OZM.mjs +376 -0
  14. package/dist/{chunk-ASI6AOW2.mjs → chunk-RO4EFWE6.mjs} +7833 -3524
  15. package/dist/{chunk-WLR5BFL3.mjs → chunk-TEAJO7CU.mjs} +3067 -2087
  16. package/dist/{chunk-S7RJJMHI.mjs → chunk-WDL632CN.mjs} +699 -222
  17. package/dist/{chunk-BKAI2WXI.mjs → chunk-WTONLDU3.mjs} +5 -0
  18. package/dist/client-DQHB_swQ.d.ts +793 -0
  19. package/dist/client.d.ts +6 -0
  20. package/dist/client.mjs +843 -0
  21. package/dist/{config-DXypXAfN.d.ts → config-ChZ7lfqc.d.ts} +3 -3
  22. package/dist/direct-executor-YARCDMJF.mjs +8 -0
  23. package/dist/distribution/coordinator/etcd.d.ts +3 -3
  24. package/dist/distribution/coordinator/etcd.mjs +471 -2
  25. package/dist/distribution/coordinator/in-memory.d.ts +3 -3
  26. package/dist/distribution/coordinator/in-memory.mjs +286 -1
  27. package/dist/distribution/transport/grpc.d.ts +45 -0
  28. package/dist/distribution/transport/grpc.mjs +586 -0
  29. package/dist/distribution/transport/in-memory.d.ts +118 -0
  30. package/dist/distribution/transport/in-memory.mjs +214 -0
  31. package/dist/distribution/transport/tcp.d.ts +27 -0
  32. package/dist/distribution/transport/tcp.mjs +595 -0
  33. package/dist/distribution.d.ts +241 -192
  34. package/dist/distribution.mjs +6620 -4795
  35. package/dist/embeddings/openai.d.ts +1 -1
  36. package/dist/embeddings/openai.mjs +1 -1
  37. package/dist/{engine-CO9Wnfie.d.ts → engine-DScGlT2Z.d.ts} +30 -3
  38. package/dist/index.browser.mjs +10035 -3914
  39. package/dist/index.d.ts +8 -8
  40. package/dist/index.mjs +8 -5
  41. package/dist/invalidation/broadcast-channel.d.ts +1 -1
  42. package/dist/invalidation/filesystem.d.ts +1 -1
  43. package/dist/invalidation/filesystem.mjs +3 -1
  44. package/dist/invalidation/noop.d.ts +1 -1
  45. package/dist/options-BORJg5XB.d.ts +234 -0
  46. package/dist/persistence/durability/checkpoint-worker.d.ts +4 -2
  47. package/dist/persistence/durability/checkpoint-worker.mjs +8 -5
  48. package/dist/react.d.ts +340 -0
  49. package/dist/react.mjs +687 -0
  50. package/dist/{schema-CE6S_aAr.d.ts → schema-emTQfEiX.d.ts} +21 -7
  51. package/dist/{search-DLBtiPdQ.d.ts → search-CQ-9bKxo.d.ts} +226 -14
  52. package/dist/server.d.ts +42 -356
  53. package/dist/server.mjs +493 -106
  54. package/dist/tasks-CnMLpfk-.d.ts +274 -0
  55. package/dist/types-BQDlIvfS.d.ts +34 -0
  56. package/dist/{types-BV3nAKTL.d.ts → types-Bfhhnh3G.d.ts} +23 -1
  57. package/dist/types-D5OwhtsQ.d.ts +97 -0
  58. package/dist/vector/hnsw-build-worker.d.ts +2 -7
  59. package/dist/vector/hnsw-build-worker.mjs +2 -1
  60. package/dist/vector/search-worker.d.ts +373 -0
  61. package/dist/vector/search-worker.mjs +279 -0
  62. package/dist/workers/entry.mjs +43 -25
  63. package/package.json +42 -2
  64. package/dist/chunk-FKK76OR7.mjs +0 -456
  65. package/dist/chunk-RQLXC2YS.mjs +0 -269
  66. package/dist/direct-executor-3W4QRA4E.mjs +0 -5
package/dist/server.mjs CHANGED
@@ -1,6 +1,19 @@
1
- import { NarsilError, ErrorCodes } from './chunk-UNJZ7ZDB.mjs';
1
+ import { ServerErrorCodes, NarsilError, ErrorCodes } from './chunk-EMI63EXP.mjs';
2
+ export { ServerErrorCodes } from './chunk-EMI63EXP.mjs';
2
3
  import { randomUUID } from 'crypto';
3
4
 
5
+ // src/server/capabilities.ts
6
+ var ASYNC_IMPORT_CAPABILITY = "documents.import.async";
7
+ var TASK_CANCEL_CAPABILITY = "tasks.cancel";
8
+ var TASK_FILTER_CAPABILITY = "tasks.filter";
9
+ var REBUILD_ANALYSIS_CAPABILITY = "indexes.rebuildAnalysis";
10
+ var SERVER_CAPABILITIES = [
11
+ ASYNC_IMPORT_CAPABILITY,
12
+ TASK_CANCEL_CAPABILITY,
13
+ TASK_FILTER_CAPABILITY,
14
+ REBUILD_ANALYSIS_CAPABILITY
15
+ ];
16
+
4
17
  // src/server/cors.ts
5
18
  function resolveCors(cors) {
6
19
  if (!cors) return null;
@@ -29,20 +42,20 @@ function corsWriter(cors) {
29
42
  }
30
43
 
31
44
  // src/server/errors.ts
32
- var ServerErrorCodes = {
33
- INVALID_REQUEST: "INVALID_REQUEST",
34
- INVALID_JSON: "INVALID_JSON",
35
- EMPTY_BODY: "EMPTY_BODY",
36
- PAYLOAD_TOO_LARGE: "PAYLOAD_TOO_LARGE",
37
- UNSUPPORTED_MEDIA_TYPE: "UNSUPPORTED_MEDIA_TYPE",
38
- NOT_FOUND: "NOT_FOUND",
39
- TASK_NOT_FOUND: "TASK_NOT_FOUND",
40
- TOO_MANY_REQUESTS: "TOO_MANY_REQUESTS",
41
- HOOK_ERROR: "HOOK_ERROR",
42
- INTERNAL_ERROR: "INTERNAL_ERROR",
43
- REQUEST_ABORTED: "REQUEST_ABORTED"
44
- };
45
45
  var STATUS_BY_CODE = {
46
+ INVALID_REQUEST: 400,
47
+ INVALID_JSON: 400,
48
+ EMPTY_BODY: 400,
49
+ PAYLOAD_TOO_LARGE: 413,
50
+ NOT_FOUND: 404,
51
+ TASK_NOT_FOUND: 404,
52
+ TASK_NOT_CANCELLABLE: 409,
53
+ TASK_OWNED_BY_ANOTHER_INSTANCE: 409,
54
+ TOO_MANY_REQUESTS: 429,
55
+ HOOK_ERROR: 500,
56
+ INTERNAL_ERROR: 500,
57
+ ENVELOPE_INVALID_MAGIC: 400,
58
+ ENVELOPE_VERSION_MISMATCH: 400,
46
59
  SCHEMA_INVALID_TYPE: 400,
47
60
  SCHEMA_MISSING_FIELD: 400,
48
61
  SCHEMA_DEPTH_EXCEEDED: 400,
@@ -56,6 +69,7 @@ var STATUS_BY_CODE = {
56
69
  SEARCH_INVALID_FILTER: 400,
57
70
  SEARCH_INVALID_MODE: 400,
58
71
  SEARCH_INVALID_CURSOR: 400,
72
+ SEARCH_RESULT_WINDOW_EXCEEDED: 400,
59
73
  LANGUAGE_NOT_SUPPORTED: 400,
60
74
  CONFIG_INVALID: 400,
61
75
  EMBEDDING_DIMENSION_MISMATCH: 400,
@@ -64,6 +78,7 @@ var STATUS_BY_CODE = {
64
78
  INDEX_NOT_FOUND: 404,
65
79
  DOC_NOT_FOUND: 404,
66
80
  INDEX_ALREADY_EXISTS: 409,
81
+ INDEX_ORPHANED: 409,
67
82
  DOC_ALREADY_EXISTS: 409,
68
83
  PARTITION_REBALANCING_BACKPRESSURE: 409,
69
84
  PARTITION_CAPACITY_EXCEEDED: 409,
@@ -82,6 +97,7 @@ var STATUS_BY_CODE = {
82
97
  QUERY_PARTIAL_FAILURE: 503,
83
98
  QUERY_NODE_TIMEOUT: 503,
84
99
  QUERY_NO_ACTIVE_REPLICA: 503,
100
+ CLUSTER_OPERATION_UNSUPPORTED: 501,
85
101
  REPLICATION_ENTRY_INVALID: 503,
86
102
  REPLICATION_INSYNC_REMOVAL_FAILED: 503,
87
103
  REPLICATION_ROLLBACK_FAILED: 503,
@@ -92,7 +108,44 @@ var STATUS_BY_CODE = {
92
108
  REPLICATION_SYNC_FAILED: 503,
93
109
  PARTITION_NOT_PRIMARY: 503,
94
110
  PARTITION_UNASSIGNED: 503,
95
- INSUFFICIENT_REPLICAS: 503
111
+ INSUFFICIENT_REPLICAS: 503,
112
+ TRANSPORT_DEPENDENCY_MISSING: 500,
113
+ ALLOCATION_INVALID_CONFIG: 400,
114
+ ALLOCATION_NO_DATA_NODES: 503,
115
+ ALLOCATION_FAILED: 503,
116
+ CONTROLLER_LEASE_LOST: 503,
117
+ CONTROLLER_NOT_ACTIVE: 503,
118
+ NODE_ALREADY_JOINED: 409,
119
+ NODE_NOT_JOINED: 503,
120
+ NODE_BOOTSTRAP_FAILED: 503,
121
+ COORDINATOR_DEPENDENCY_MISSING: 500,
122
+ SNAPSHOT_SYNC_UNAUTHORIZED: 403,
123
+ SNAPSHOT_SYNC_REQUEST_INVALID: 400,
124
+ SNAPSHOT_SYNC_HEADER_INVALID: 400,
125
+ SNAPSHOT_SYNC_FRAME_INVALID: 400,
126
+ SNAPSHOT_SYNC_INDEX_NOT_FOUND: 404,
127
+ SNAPSHOT_SYNC_TOO_LARGE: 413,
128
+ SNAPSHOT_SYNC_CHUNK_SIZE_EXCEEDED: 413,
129
+ SNAPSHOT_SYNC_ABORTED: 409,
130
+ SNAPSHOT_SYNC_CAPACITY_EXHAUSTED: 503,
131
+ SNAPSHOT_SYNC_SNAPSHOT_FAILED: 503,
132
+ SNAPSHOT_SYNC_DECODE_FAILED: 503,
133
+ SNAPSHOT_SYNC_HEADER_MISMATCH: 503,
134
+ SNAPSHOT_SYNC_CHUNK_OUT_OF_ORDER: 503,
135
+ SNAPSHOT_SYNC_CHUNK_OVERFLOW: 503,
136
+ SNAPSHOT_SYNC_CHUNK_MISSING: 503,
137
+ SNAPSHOT_SYNC_END_MISSING: 503,
138
+ SNAPSHOT_SYNC_CHECKSUM_MISMATCH: 503,
139
+ SNAPSHOT_SYNC_PRIMARY_ERROR: 503,
140
+ SNAPSHOT_SYNC_NO_TARGETS: 503,
141
+ SNAPSHOT_SYNC_TRANSPORT_FAILED: 503,
142
+ SNAPSHOT_SYNC_SCHEMA_UNAVAILABLE: 503,
143
+ SNAPSHOT_SYNC_RESTORE_FAILED: 503,
144
+ SNAPSHOT_SYNC_RESTORE_CLEANUP_FAILED: 503,
145
+ SNAPSHOT_SYNC_TIMEOUT: 503,
146
+ SNAPSHOT_SYNC_ALLOCATION_UNAVAILABLE: 503,
147
+ SNAPSHOT_SYNC_NOT_ASSIGNED: 503,
148
+ CONTROLLER_METADATA_INVALID: 500
96
149
  };
97
150
  function httpStatusForNarsilError(code) {
98
151
  return STATUS_BY_CODE[code] ?? 500;
@@ -111,6 +164,17 @@ function toHttpError(err) {
111
164
  };
112
165
  }
113
166
 
167
+ // src/json-encoding.ts
168
+ function toPlainNumbers(key, value) {
169
+ if (ArrayBuffer.isView(value) && !(value instanceof DataView)) {
170
+ return Array.from(value);
171
+ }
172
+ return value;
173
+ }
174
+ function encodeJson(value) {
175
+ return JSON.stringify(value, toPlainNumbers);
176
+ }
177
+
114
178
  // src/server/response.ts
115
179
  var STATUS_TEXT = {
116
180
  200: "200 OK",
@@ -149,7 +213,7 @@ function streamBody(res, abort, body, status, contentType) {
149
213
  });
150
214
  }
151
215
  function sendJson(res, data, status = 200, abort) {
152
- const payload = JSON.stringify(data);
216
+ const payload = encodeJson(data);
153
217
  if (abort && payload !== void 0 && Buffer.byteLength(payload) >= STREAM_THRESHOLD) {
154
218
  streamBody(res, abort, Buffer.from(payload, "utf8"), status, "application/json");
155
219
  return;
@@ -160,7 +224,7 @@ function sendJson(res, data, status = 200, abort) {
160
224
  }
161
225
  function sendError(res, status, code, message, details) {
162
226
  const body = { error: details ? { code, message, details } : { code, message } };
163
- const payload = JSON.stringify(body);
227
+ const payload = encodeJson(body);
164
228
  res.cork(() => {
165
229
  res.writeStatus(statusLine(status)).writeHeader("Content-Type", "application/json").end(payload);
166
230
  });
@@ -220,6 +284,7 @@ function serializeBatchResult(result) {
220
284
  }
221
285
 
222
286
  // src/server/validation.ts
287
+ var MAX_LIST_SORT_FIELDS = 8;
223
288
  function isPlainObject(value) {
224
289
  return typeof value === "object" && value !== null && !Array.isArray(value);
225
290
  }
@@ -278,6 +343,45 @@ function validateMultiGet(body, maxFetch) {
278
343
  }
279
344
  return null;
280
345
  }
346
+ function validateList(params, maxFetch) {
347
+ if (params.cursor !== void 0 && typeof params.cursor !== "string") {
348
+ return { message: 'Field "cursor" must be a string' };
349
+ }
350
+ if (params.limit !== void 0 && typeof params.limit !== "number") {
351
+ return { message: 'Field "limit" must be a number' };
352
+ }
353
+ if (typeof params.limit === "number" && params.limit > maxFetch) {
354
+ return {
355
+ message: `Field "limit" exceeds the maximum of ${maxFetch} documents per request`,
356
+ details: { value: params.limit, limit: maxFetch }
357
+ };
358
+ }
359
+ return validateListSort(params.sort);
360
+ }
361
+ function validateListSort(sort) {
362
+ if (sort === void 0) return null;
363
+ if (typeof sort !== "object" || sort === null) {
364
+ return { message: 'Field "sort" must be an object keyed by field name, or a list of fields' };
365
+ }
366
+ const entries = Array.isArray(sort) ? sort.map(
367
+ (entry) => typeof entry === "object" && entry !== null ? [entry.field, entry.direction] : [entry, void 0]
368
+ ) : Object.entries(sort);
369
+ if (entries.length > MAX_LIST_SORT_FIELDS) {
370
+ return {
371
+ message: `Field "sort" exceeds the maximum of ${MAX_LIST_SORT_FIELDS} fields`,
372
+ details: { count: entries.length, limit: MAX_LIST_SORT_FIELDS }
373
+ };
374
+ }
375
+ for (const [field, direction] of entries) {
376
+ if (typeof field !== "string" || field.length === 0) {
377
+ return { message: 'Every entry of "sort" must name a field', details: { value: field } };
378
+ }
379
+ if (direction !== "asc" && direction !== "desc") {
380
+ return { message: `Field "sort.${field}" must be "asc" or "desc"`, details: { value: direction } };
381
+ }
382
+ }
383
+ return null;
384
+ }
281
385
  function validateBatch(body) {
282
386
  const action = body.action ?? "insert";
283
387
  if (action === "insert") {
@@ -310,13 +414,63 @@ function validateRebalance(body) {
310
414
  }
311
415
  return null;
312
416
  }
417
+ var TASK_TYPES = ["optimizeVectors", "rebalance", "restore", "import", "rebuildAnalysis"];
418
+ var TASK_STATUSES = ["queued", "running", "succeeded", "failed", "cancelled"];
419
+ function parseCountParam(raw, field, minimum) {
420
+ if (raw === null) return minimum;
421
+ const value = Number(raw);
422
+ if (!Number.isInteger(value) || value < minimum) {
423
+ return { message: `Query parameter "${field}" must be an integer of ${minimum} or more`, details: { value: raw } };
424
+ }
425
+ return value;
426
+ }
427
+ function parseEnumParam(raw, field, allowed) {
428
+ if (raw === null) return void 0;
429
+ const requested = raw.split(",").map((entry) => entry.trim());
430
+ const unknown = requested.filter((entry) => !allowed.includes(entry));
431
+ if (requested.length === 0 || unknown.length > 0) {
432
+ return {
433
+ message: `Query parameter "${field}" accepts only ${allowed.join(", ")}`,
434
+ details: { unknown }
435
+ };
436
+ }
437
+ return requested;
438
+ }
439
+ function isFailure(value) {
440
+ return isPlainObject(value) && typeof value.message === "string";
441
+ }
442
+ function parseTaskListQuery(params, maxPageSize) {
443
+ const type = parseEnumParam(params.get("type"), "type", TASK_TYPES);
444
+ if (isFailure(type)) return { failure: type };
445
+ const status = parseEnumParam(params.get("status"), "status", TASK_STATUSES);
446
+ if (isFailure(status)) return { failure: status };
447
+ const from = parseCountParam(params.get("from"), "from", 0);
448
+ if (isFailure(from)) return { failure: from };
449
+ const limit = parseCountParam(params.get("limit"), "limit", 1);
450
+ if (isFailure(limit)) return { failure: limit };
451
+ if (params.get("limit") !== null && limit > maxPageSize) {
452
+ return {
453
+ failure: {
454
+ message: `Query parameter "limit" exceeds the maximum page size of ${maxPageSize}`,
455
+ details: { value: limit, limit: maxPageSize }
456
+ }
457
+ };
458
+ }
459
+ const indexName = params.get("indexName");
460
+ return {
461
+ query: {
462
+ ...indexName === null ? {} : { indexName },
463
+ ...type === void 0 ? {} : { type },
464
+ ...status === void 0 ? {} : { status },
465
+ from,
466
+ ...params.get("limit") === null ? {} : { limit }
467
+ }
468
+ };
469
+ }
313
470
 
314
471
  // src/server/handlers/admin.ts
315
- function taskResponse(record) {
316
- return { taskId: record.id, type: record.type, status: record.status };
317
- }
318
472
  function createAdminHandlers(deps) {
319
- const { engine, tasks } = deps;
473
+ const { engine, tasks, limits } = deps;
320
474
  async function checkpoint(ctx) {
321
475
  try {
322
476
  await engine.checkpoint(ctx.params[0]);
@@ -382,7 +536,18 @@ function createAdminHandlers(deps) {
382
536
  return;
383
537
  }
384
538
  const record = await tasks.start("optimizeVectors", name, () => engine.optimizeVectors(name, body.field));
385
- respondJson(ctx, taskResponse(record), 202);
539
+ respondJson(ctx, record, 202);
540
+ }
541
+ async function rebuildAnalysis(ctx) {
542
+ const name = ctx.params[0];
543
+ try {
544
+ requireIndexExists(name);
545
+ } catch (err) {
546
+ respondError(ctx, err);
547
+ return;
548
+ }
549
+ const record = await tasks.start("rebuildAnalysis", name, () => engine.rebuildAnalysis(name));
550
+ respondJson(ctx, record, 202);
386
551
  }
387
552
  async function rebalance(ctx) {
388
553
  const body = parseJsonOptional(ctx);
@@ -401,7 +566,7 @@ function createAdminHandlers(deps) {
401
566
  return;
402
567
  }
403
568
  const record = await tasks.start("rebalance", name, () => engine.rebalance(name, target));
404
- respondJson(ctx, taskResponse(record), 202);
569
+ respondJson(ctx, record, 202);
405
570
  }
406
571
  async function restore(ctx) {
407
572
  const raw = ctx.rawBody;
@@ -412,7 +577,7 @@ function createAdminHandlers(deps) {
412
577
  const name = ctx.params[0];
413
578
  const bytes = new Uint8Array(raw);
414
579
  const record = await tasks.start("restore", name, () => engine.restore(name, bytes));
415
- respondJson(ctx, taskResponse(record), 202);
580
+ respondJson(ctx, record, 202);
416
581
  }
417
582
  async function getTask(ctx) {
418
583
  const record = await tasks.get(ctx.params[0]);
@@ -423,7 +588,41 @@ function createAdminHandlers(deps) {
423
588
  respondJson(ctx, record);
424
589
  }
425
590
  async function listTasks(ctx) {
426
- respondJson(ctx, { tasks: await tasks.list() });
591
+ const parsed = parseTaskListQuery(ctx.query, limits.maxTaskPageSize);
592
+ if ("failure" in parsed) {
593
+ rejectInvalid(ctx, parsed.failure);
594
+ return;
595
+ }
596
+ respondJson(ctx, await tasks.list(parsed.query));
597
+ }
598
+ async function cancelTask(ctx) {
599
+ const taskId = ctx.params[0];
600
+ const { outcome, record } = await tasks.cancel(taskId);
601
+ if (outcome === "not-found") {
602
+ sendError(ctx.res, 404, ServerErrorCodes.TASK_NOT_FOUND, `Task "${taskId}" not found`);
603
+ return;
604
+ }
605
+ if (outcome === "already-finished") {
606
+ sendError(
607
+ ctx.res,
608
+ 409,
609
+ ServerErrorCodes.TASK_NOT_CANCELLABLE,
610
+ `Task "${taskId}" has already finished`,
611
+ record === null ? void 0 : { status: record.status }
612
+ );
613
+ return;
614
+ }
615
+ if (outcome === "owned-by-another-instance") {
616
+ sendError(
617
+ ctx.res,
618
+ 409,
619
+ ServerErrorCodes.TASK_OWNED_BY_ANOTHER_INSTANCE,
620
+ `Task "${taskId}" runs on another server instance, which is the only one that can stop it`,
621
+ record === null ? void 0 : { owner: record.owner }
622
+ );
623
+ return;
624
+ }
625
+ respondJson(ctx, record, 202);
427
626
  }
428
627
  return {
429
628
  checkpoint,
@@ -434,12 +633,22 @@ function createAdminHandlers(deps) {
434
633
  memory,
435
634
  optimize,
436
635
  rebalance,
636
+ rebuildAnalysis,
437
637
  restore,
438
638
  getTask,
439
- listTasks
639
+ listTasks,
640
+ cancelTask
440
641
  };
441
642
  }
442
643
 
644
+ // src/server/handlers/capabilities.ts
645
+ function createCapabilitiesHandler() {
646
+ function report(ctx) {
647
+ respondJson(ctx, { capabilities: [...SERVER_CAPABILITIES] });
648
+ }
649
+ return { report };
650
+ }
651
+
443
652
  // src/server/handlers/documents.ts
444
653
  function createDocumentHandlers(deps) {
445
654
  const { engine, limits } = deps;
@@ -545,6 +754,20 @@ function createDocumentHandlers(deps) {
545
754
  respondError(ctx, err);
546
755
  }
547
756
  }
757
+ async function list(ctx) {
758
+ const params = parseJson(ctx);
759
+ if (!params) return;
760
+ const failure = validateList(params, limits.maxFetchDocuments);
761
+ if (failure) {
762
+ rejectInvalid(ctx, failure);
763
+ return;
764
+ }
765
+ try {
766
+ respondJson(ctx, await engine.listDocuments(ctx.params[0], params));
767
+ } catch (err) {
768
+ respondError(ctx, err);
769
+ }
770
+ }
548
771
  async function batch(ctx) {
549
772
  const body = parseJson(ctx);
550
773
  if (!body) return;
@@ -569,7 +792,7 @@ function createDocumentHandlers(deps) {
569
792
  respondError(ctx, err);
570
793
  }
571
794
  }
572
- return { insert, get, exists, put, patch, remove, count, multiGet, batch };
795
+ return { insert, get, exists, put, patch, remove, count, list, multiGet, batch };
573
796
  }
574
797
  function indexDocNotFound(docId) {
575
798
  return new NarsilError(ErrorCodes.DOC_NOT_FOUND, `Document "${docId}" not found`, { docId });
@@ -593,10 +816,11 @@ function createHealthHandlers(deps) {
593
816
  // src/server/ndjson.ts
594
817
  var NEWLINE = 10;
595
818
  var CARRIAGE_RETURN = 13;
596
- var NdjsonLineTooLongError = class extends Error {
819
+ var NdjsonLineTooLongError = class extends NarsilError {
597
820
  constructor(lineNumber) {
598
- super(`NDJSON line ${lineNumber} exceeds the configured per-line size limit`);
599
- this.lineNumber = lineNumber;
821
+ super(ServerErrorCodes.PAYLOAD_TOO_LARGE, `NDJSON line ${lineNumber} exceeds the configured per-line size limit`, {
822
+ line: lineNumber
823
+ });
600
824
  this.name = "NdjsonLineTooLongError";
601
825
  }
602
826
  };
@@ -612,7 +836,7 @@ function* iterateNdjson(buffer, maxLineBytes) {
612
836
  if (contentEnd > start && buffer[contentEnd - 1] === CARRIAGE_RETURN) contentEnd--;
613
837
  if (contentEnd > start) {
614
838
  lineNumber++;
615
- yield { lineNumber, text: buffer.toString("utf8", start, contentEnd) };
839
+ yield { lineNumber, text: buffer.toString("utf8", start, contentEnd), bytesConsumed: Math.min(end + 1, length) };
616
840
  }
617
841
  start = end + 1;
618
842
  }
@@ -625,71 +849,110 @@ function yieldToEventLoop() {
625
849
  function isPlainObject2(value) {
626
850
  return typeof value === "object" && value !== null && !Array.isArray(value);
627
851
  }
852
+ async function runImport(engine, options) {
853
+ const { indexName, body, maxLineBytes, batchSize, maxErrors, signal, onProgress } = options;
854
+ const bytesTotal = body.length;
855
+ const errors = [];
856
+ let indexed = 0;
857
+ let failed = 0;
858
+ let bytesProcessed = 0;
859
+ let pending = [];
860
+ const recordFailure = (error) => {
861
+ failed += 1;
862
+ if (errors.length < maxErrors) errors.push(error);
863
+ };
864
+ const flush = async () => {
865
+ if (pending.length === 0) return;
866
+ const documents = pending.map((entry) => entry.document);
867
+ pending = [];
868
+ const result = await engine.insertBatch(indexName, documents, { skipClone: true });
869
+ indexed += result.succeeded.length;
870
+ for (const failure of result.failed) {
871
+ const serialized = failure.error instanceof NarsilError ? serializeNarsilError(failure.error) : { code: ServerErrorCodes.INTERNAL_ERROR, message: String(failure.error) };
872
+ recordFailure({ docId: failure.docId, code: serialized.code, message: serialized.message });
873
+ }
874
+ };
875
+ const report = () => {
876
+ onProgress?.({ indexed, failed, bytesProcessed, bytesTotal });
877
+ };
878
+ for (const line of iterateNdjson(body, maxLineBytes)) {
879
+ bytesProcessed = line.bytesConsumed;
880
+ let parsed;
881
+ try {
882
+ parsed = JSON.parse(line.text);
883
+ } catch {
884
+ recordFailure({ line: line.lineNumber, code: ServerErrorCodes.INVALID_JSON, message: "Line is not valid JSON" });
885
+ continue;
886
+ }
887
+ if (!isPlainObject2(parsed)) {
888
+ recordFailure({
889
+ line: line.lineNumber,
890
+ code: ServerErrorCodes.INVALID_REQUEST,
891
+ message: "Line is not a JSON object"
892
+ });
893
+ continue;
894
+ }
895
+ pending.push({ document: parsed, line: line.lineNumber });
896
+ if (pending.length >= batchSize) {
897
+ signal?.throwIfAborted();
898
+ await flush();
899
+ report();
900
+ await yieldToEventLoop();
901
+ signal?.throwIfAborted();
902
+ }
903
+ }
904
+ await flush();
905
+ bytesProcessed = bytesTotal;
906
+ report();
907
+ return { indexed, failed, errors, errorsTruncated: failed > errors.length };
908
+ }
628
909
  function createImportHandler(deps) {
629
- const { engine, limits } = deps;
910
+ const { engine, limits, tasks } = deps;
630
911
  return async function importNdjson(ctx) {
631
912
  const raw = ctx.rawBody;
632
913
  if (!raw || raw.length === 0) {
633
914
  sendError(ctx.res, 400, ServerErrorCodes.EMPTY_BODY, "Request body is empty");
634
915
  return;
635
916
  }
636
- const name = ctx.params[0];
637
- const errors = [];
638
- let indexed = 0;
639
- let pending = [];
640
- const flush = async () => {
641
- if (pending.length === 0) return true;
642
- const documents = pending.map((p) => p.document);
917
+ const indexName = ctx.params[0];
918
+ const runOptions = {
919
+ indexName,
920
+ body: raw,
921
+ maxLineBytes: limits.maxLineBytes,
922
+ batchSize: limits.importBatchSize,
923
+ maxErrors: limits.maxImportErrors
924
+ };
925
+ if (ctx.query.get("async") === "true") {
643
926
  try {
644
- const result = await engine.insertBatch(name, documents);
645
- indexed += result.succeeded.length;
646
- for (const failure of result.failed) {
647
- const serialized = failure.error instanceof NarsilError ? serializeNarsilError(failure.error) : { code: "INTERNAL_ERROR", message: String(failure.error) };
648
- errors.push({ docId: failure.docId, code: serialized.code, message: serialized.message });
649
- }
650
- pending = [];
651
- return true;
927
+ await engine.countDocuments(indexName);
652
928
  } catch (err) {
653
929
  respondError(ctx, err);
654
- return false;
930
+ return;
655
931
  }
656
- };
657
- try {
658
- for (const { lineNumber, text } of iterateNdjson(raw, limits.maxLineBytes)) {
659
- let parsed;
660
- try {
661
- parsed = JSON.parse(text);
662
- } catch {
663
- errors.push({ line: lineNumber, code: ServerErrorCodes.INVALID_JSON, message: "Line is not valid JSON" });
664
- continue;
665
- }
666
- if (!isPlainObject2(parsed)) {
667
- errors.push({
668
- line: lineNumber,
669
- code: ServerErrorCodes.INVALID_REQUEST,
670
- message: "Line is not a JSON object"
932
+ const record = await tasks.start(
933
+ "import",
934
+ indexName,
935
+ async (context) => {
936
+ const result = await runImport(engine, {
937
+ ...runOptions,
938
+ signal: context.signal,
939
+ onProgress: context.reportProgress
671
940
  });
672
- continue;
673
- }
674
- pending.push({ document: parsed, line: lineNumber });
675
- if (pending.length >= limits.importBatchSize) {
676
- if (!await flush()) return;
677
- if (ctx.abort.aborted) return;
678
- await yieldToEventLoop();
679
- if (ctx.abort.aborted) return;
680
- }
681
- }
941
+ context.reportResult(result);
942
+ },
943
+ { indexed: 0, failed: 0, bytesProcessed: 0, bytesTotal: raw.length }
944
+ );
945
+ respondJson(ctx, record, 202);
946
+ return;
947
+ }
948
+ const controller = new AbortController();
949
+ ctx.abort.onAbort(() => controller.abort());
950
+ try {
951
+ respondJson(ctx, await runImport(engine, { ...runOptions, signal: controller.signal }));
682
952
  } catch (err) {
683
- if (err instanceof NdjsonLineTooLongError) {
684
- sendError(ctx.res, 413, ServerErrorCodes.PAYLOAD_TOO_LARGE, err.message, { line: err.lineNumber });
685
- return;
686
- }
953
+ if (ctx.abort.aborted) return;
687
954
  respondError(ctx, err);
688
- return;
689
955
  }
690
- if (!await flush()) return;
691
- if (ctx.abort.aborted) return;
692
- respondJson(ctx, { indexed, failed: errors.length, errors });
693
956
  };
694
957
  }
695
958
 
@@ -924,6 +1187,14 @@ var ConcurrencyGate = class {
924
1187
  if (this.inFlight > 0) this.inFlight--;
925
1188
  }
926
1189
  };
1190
+ function decodePathParameter(raw) {
1191
+ if (!raw.includes("%")) return raw;
1192
+ try {
1193
+ return decodeURIComponent(raw);
1194
+ } catch {
1195
+ return null;
1196
+ }
1197
+ }
927
1198
  function isDenial(value) {
928
1199
  return typeof value === "object" && value !== null && "status" in value;
929
1200
  }
@@ -950,7 +1221,15 @@ function createRouteRunner(deps) {
950
1221
  const gated = !opts.skipHooks;
951
1222
  return (res, req) => {
952
1223
  const params = [];
953
- for (let i = 0; i < paramCount; i++) params.push(req.getParameter(i) ?? "");
1224
+ let malformed = null;
1225
+ for (let i = 0; i < paramCount; i++) {
1226
+ const decoded = decodePathParameter(req.getParameter(i) ?? "");
1227
+ if (decoded === null) {
1228
+ malformed = req.getParameter(i) ?? "";
1229
+ break;
1230
+ }
1231
+ params.push(decoded);
1232
+ }
954
1233
  const query = new URLSearchParams(req.getQuery() ?? "");
955
1234
  const contentType = req.getHeader("content-type");
956
1235
  let hookCtx = null;
@@ -968,12 +1247,22 @@ function createRouteRunner(deps) {
968
1247
  }
969
1248
  if (writeCors) writeCors(res, req);
970
1249
  const abort = initAbortHandler(res);
1250
+ if (malformed !== null) {
1251
+ sendError(
1252
+ res,
1253
+ 400,
1254
+ ServerErrorCodes.INVALID_REQUEST,
1255
+ "A path segment holds a percent-escape the server cannot decode",
1256
+ { segment: malformed }
1257
+ );
1258
+ return;
1259
+ }
971
1260
  const bodyPromise = needsBody ? readBody(res, opts.maxBytes, abort) : Promise.resolve(null);
972
1261
  const hookPromise = hookCtx && onRequest ? runHook(res, hookCtx, onRequest) : Promise.resolve(true);
973
1262
  Promise.all([bodyPromise, hookPromise]).then(async ([rawBody, allowed]) => {
974
1263
  if (abort.aborted || !allowed) return;
975
1264
  if (gated && !gate.tryAcquire()) {
976
- sendError(res, 503, ServerErrorCodes.TOO_MANY_REQUESTS, "The server is at capacity; retry shortly");
1265
+ sendError(res, 429, ServerErrorCodes.TOO_MANY_REQUESTS, "The server is at capacity; retry shortly");
977
1266
  return;
978
1267
  }
979
1268
  try {
@@ -1071,14 +1360,38 @@ var InMemoryTaskStore = class {
1071
1360
  };
1072
1361
  var RUNNING_TTL_MS = 24 * 60 * 60 * 1e3;
1073
1362
  var TERMINAL_TTL_MS = 60 * 60 * 1e3;
1363
+ var PROGRESS_WRITE_INTERVAL_MS = 250;
1364
+ var DEFAULT_TASK_PAGE_SIZE = 20;
1365
+ function byNewestFirst(left, right) {
1366
+ if (left.createdAt !== right.createdAt) return right.createdAt - left.createdAt;
1367
+ return left.id.localeCompare(right.id);
1368
+ }
1369
+ function matchesQuery(record, query) {
1370
+ if (query.indexName !== void 0 && record.indexName !== query.indexName) return false;
1371
+ if (query.type !== void 0 && query.type.length > 0 && !query.type.includes(record.type)) return false;
1372
+ if (query.status !== void 0 && query.status.length > 0 && !query.status.includes(record.status)) return false;
1373
+ return true;
1374
+ }
1074
1375
  var TaskRegistry = class {
1075
- constructor(store, instanceId) {
1376
+ constructor(store, instanceId, maxConcurrent = 0) {
1076
1377
  this.store = store;
1077
1378
  this.instanceId = instanceId;
1379
+ this.maxConcurrent = maxConcurrent;
1078
1380
  }
1381
+ live = /* @__PURE__ */ new Map();
1079
1382
  /** Records a task as running and drives `op` to completion in the background.
1080
- * Returns the record once it is persisted so the caller can respond 202. */
1081
- async start(type, indexName, op) {
1383
+ * Returns the record once it is persisted so the caller can respond 202.
1384
+ * @throws A `NarsilError` with `TOO_MANY_REQUESTS` where this instance
1385
+ * already drives {@link ServerLimits.maxConcurrentTasks} tasks, because each
1386
+ * one holds its own working set until it finishes. */
1387
+ async start(type, indexName, op, progress) {
1388
+ if (this.maxConcurrent > 0 && this.live.size >= this.maxConcurrent) {
1389
+ throw new NarsilError(
1390
+ ServerErrorCodes.TOO_MANY_REQUESTS,
1391
+ `This instance already drives ${this.maxConcurrent} tasks; poll one to completion before starting another`,
1392
+ { running: this.live.size, limit: this.maxConcurrent }
1393
+ );
1394
+ }
1082
1395
  const now = Date.now();
1083
1396
  const record = {
1084
1397
  id: randomUUID(),
@@ -1087,17 +1400,54 @@ var TaskRegistry = class {
1087
1400
  owner: this.instanceId,
1088
1401
  status: "running",
1089
1402
  createdAt: now,
1090
- startedAt: now
1403
+ startedAt: now,
1404
+ ...progress === void 0 ? {} : { progress }
1091
1405
  };
1092
- await this.store.set(record, RUNNING_TTL_MS);
1093
- void this.drive(record, op);
1406
+ const task = {
1407
+ record,
1408
+ controller: new AbortController(),
1409
+ writes: Promise.resolve(),
1410
+ lastProgressWriteAt: now,
1411
+ settled: false
1412
+ };
1413
+ this.live.set(record.id, task);
1414
+ await this.enqueueWrite(task, record, RUNNING_TTL_MS);
1415
+ void this.drive(task, op);
1094
1416
  return record;
1095
1417
  }
1096
1418
  get(id) {
1097
1419
  return this.store.get(id);
1098
1420
  }
1099
- list() {
1100
- return this.store.list();
1421
+ /** Reads one page of task records, newest first, keeping only what the query
1422
+ * matches. Filtering happens here rather than in the store, so every custom
1423
+ * {@link TaskStore} works unchanged. */
1424
+ async list(query = {}) {
1425
+ const from = Math.max(0, Math.trunc(query.from ?? 0));
1426
+ const limit = Math.max(1, Math.trunc(query.limit ?? DEFAULT_TASK_PAGE_SIZE));
1427
+ const matched = (await this.store.list()).filter((record) => matchesQuery(record, query)).sort(byNewestFirst);
1428
+ const page = matched.slice(from, from + limit);
1429
+ const consumed = from + page.length;
1430
+ return { tasks: page, total: matched.length, from, limit, next: consumed < matched.length ? consumed : null };
1431
+ }
1432
+ /** Asks a running task to stop. The work stops between units, so the task
1433
+ * reaches `cancelled` only once it actually stopped, and a request that
1434
+ * arrives too late leaves it `succeeded`. */
1435
+ async cancel(id) {
1436
+ const stored = await this.store.get(id);
1437
+ if (!stored) return { outcome: "not-found", record: null };
1438
+ if (stored.status !== "running" && stored.status !== "queued") {
1439
+ return { outcome: "already-finished", record: stored };
1440
+ }
1441
+ const task = this.live.get(id);
1442
+ if (!task) {
1443
+ const outcome = stored.owner === this.instanceId ? "already-finished" : "owned-by-another-instance";
1444
+ return { outcome, record: stored };
1445
+ }
1446
+ task.controller.abort(new Error("The task was cancelled"));
1447
+ const record = { ...task.record, cancelRequestedAt: Date.now() };
1448
+ task.record = record;
1449
+ await this.enqueueWrite(task, record, RUNNING_TTL_MS);
1450
+ return { outcome: "cancelling", record };
1101
1451
  }
1102
1452
  /** Fails this instance's previously-running tasks after a restart. A task can
1103
1453
  * only be advanced by the process that owns it, so its own running tasks from
@@ -1127,20 +1477,45 @@ var TaskRegistry = class {
1127
1477
  }
1128
1478
  }
1129
1479
  }
1130
- async drive(record, op) {
1131
- const result = { ...record };
1480
+ enqueueWrite(task, record, ttlMs) {
1481
+ task.writes = task.writes.then(
1482
+ () => this.store.set(record, ttlMs),
1483
+ () => this.store.set(record, ttlMs)
1484
+ );
1485
+ return task.writes.catch(() => {
1486
+ });
1487
+ }
1488
+ reportProgress(task, progress) {
1489
+ if (task.settled) return;
1490
+ task.record = { ...task.record, progress };
1491
+ const now = Date.now();
1492
+ if (now - task.lastProgressWriteAt < PROGRESS_WRITE_INTERVAL_MS) return;
1493
+ task.lastProgressWriteAt = now;
1494
+ void this.enqueueWrite(task, task.record, RUNNING_TTL_MS);
1495
+ }
1496
+ async drive(task, op) {
1497
+ const context = {
1498
+ signal: task.controller.signal,
1499
+ reportProgress: (progress) => this.reportProgress(task, progress),
1500
+ reportResult: (result) => {
1501
+ task.record = { ...task.record, result };
1502
+ }
1503
+ };
1504
+ let settled;
1132
1505
  try {
1133
- await op();
1134
- result.status = "succeeded";
1506
+ await op(context);
1507
+ settled = { ...task.record, status: "succeeded" };
1135
1508
  } catch (err) {
1136
- result.status = "failed";
1137
- result.error = err instanceof NarsilError ? serializeNarsilError(err) : { code: "INTERNAL_ERROR", message: "The operation failed" };
1138
- }
1139
- result.completedAt = Date.now();
1140
- try {
1141
- await this.store.set(result, TERMINAL_TTL_MS);
1142
- } catch {
1509
+ settled = task.controller.signal.aborted ? { ...task.record, status: "cancelled" } : {
1510
+ ...task.record,
1511
+ status: "failed",
1512
+ error: err instanceof NarsilError ? serializeNarsilError(err) : { code: "INTERNAL_ERROR", message: "The operation failed" }
1513
+ };
1143
1514
  }
1515
+ task.settled = true;
1516
+ task.record = { ...settled, completedAt: Date.now() };
1517
+ await this.enqueueWrite(task, task.record, TERMINAL_TTL_MS);
1518
+ this.live.delete(task.record.id);
1144
1519
  }
1145
1520
  };
1146
1521
 
@@ -1154,7 +1529,10 @@ function resolveLimits(limits) {
1154
1529
  importBatchSize: limits?.importBatchSize ?? 1e3,
1155
1530
  maxConcurrentRequests: limits?.maxConcurrentRequests ?? 0,
1156
1531
  maxResultWindow: limits?.maxResultWindow ?? 1e4,
1157
- maxFetchDocuments: limits?.maxFetchDocuments ?? 1e4
1532
+ maxFetchDocuments: limits?.maxFetchDocuments ?? 1e4,
1533
+ maxImportErrors: limits?.maxImportErrors ?? 100,
1534
+ maxTaskPageSize: limits?.maxTaskPageSize ?? 1e3,
1535
+ maxConcurrentTasks: limits?.maxConcurrentTasks ?? 4
1158
1536
  };
1159
1537
  }
1160
1538
  function resolveBuild(build) {
@@ -1192,10 +1570,11 @@ var NarsilHttpServer = class {
1192
1570
  }
1193
1571
  const taskStore = options.taskStore ?? new InMemoryTaskStore();
1194
1572
  const instanceId = options.instanceId ?? randomUUID();
1573
+ const limits = resolveLimits(options.limits);
1195
1574
  this.deps = {
1196
1575
  engine,
1197
- tasks: new TaskRegistry(taskStore, instanceId),
1198
- limits: resolveLimits(options.limits),
1576
+ tasks: new TaskRegistry(taskStore, instanceId, limits.maxConcurrentTasks),
1577
+ limits,
1199
1578
  isReady: () => this.ready,
1200
1579
  build: resolveBuild(options.build)
1201
1580
  };
@@ -1244,6 +1623,7 @@ var NarsilHttpServer = class {
1244
1623
  const admin = createAdminHandlers(this.deps);
1245
1624
  const health = createHealthHandlers(this.deps);
1246
1625
  const version = createVersionHandler(this.deps);
1626
+ const capabilities = createCapabilitiesHandler();
1247
1627
  const importNdjson = createImportHandler(this.deps);
1248
1628
  const json = (handler, opts) => run(handler, opts);
1249
1629
  if (cors) {
@@ -1260,9 +1640,11 @@ var NarsilHttpServer = class {
1260
1640
  app.get("/readyz", json(health.readyz, { maxBytes: 0, skipHooks: true }));
1261
1641
  app.get("/health", json(health.livez, { maxBytes: 0, skipHooks: true }));
1262
1642
  app.get("/version", json(version.report, { maxBytes: 0, skipHooks: true }));
1643
+ app.get("/capabilities", json(capabilities.report, { maxBytes: 0, skipHooks: true }));
1263
1644
  app.get("/stats/memory", json(admin.memory, { maxBytes: 0 }));
1264
1645
  app.get("/tasks", json(admin.listTasks, { maxBytes: 0 }));
1265
1646
  app.get("/tasks/:id", json(admin.getTask, { paramCount: 1, maxBytes: 0 }));
1647
+ app.post("/tasks/:id/_cancel", json(admin.cancelTask, { paramCount: 1, maxBytes: 0 }));
1266
1648
  app.post("/indexes", json(idx.create, { needsBody: true, maxBytes: maxBodyBytes }));
1267
1649
  app.get("/indexes", json(idx.list, { maxBytes: 0 }));
1268
1650
  app.del("/indexes/:name", json(idx.drop, { paramCount: 1, maxBytes: 0 }));
@@ -1275,6 +1657,10 @@ var NarsilHttpServer = class {
1275
1657
  "/indexes/:name/documents/_batch",
1276
1658
  json(doc.batch, { paramCount: 1, needsBody: true, maxBytes: maxBodyBytes })
1277
1659
  );
1660
+ app.post(
1661
+ "/indexes/:name/documents/_list",
1662
+ json(doc.list, { paramCount: 1, needsBody: true, maxBytes: maxBodyBytes })
1663
+ );
1278
1664
  app.post(
1279
1665
  "/indexes/:name/documents/_multi-get",
1280
1666
  json(doc.multiGet, { paramCount: 1, needsBody: true, maxBytes: maxBodyBytes })
@@ -1298,6 +1684,7 @@ var NarsilHttpServer = class {
1298
1684
  );
1299
1685
  app.post("/indexes/:name/suggest", json(search.suggest, { paramCount: 1, needsBody: true, maxBytes: maxBodyBytes }));
1300
1686
  app.post("/indexes/:name/_checkpoint", json(admin.checkpoint, { paramCount: 1, maxBytes: 0 }));
1687
+ app.post("/indexes/:name/_rebuild-analysis", json(admin.rebuildAnalysis, { paramCount: 1, maxBytes: 0 }));
1301
1688
  app.get("/indexes/:name/snapshot", json(admin.snapshot, { paramCount: 1, maxBytes: 0 }));
1302
1689
  app.get("/indexes/:name/vector-maintenance", json(admin.vectorMaintenance, { paramCount: 1, maxBytes: 0 }));
1303
1690
  app.post(
@@ -1329,4 +1716,4 @@ function createServer(engine, options) {
1329
1716
  return new NarsilHttpServer(engine, options);
1330
1717
  }
1331
1718
 
1332
- export { InMemoryTaskStore, ServerErrorCodes, createServer, httpStatusForNarsilError };
1719
+ export { ASYNC_IMPORT_CAPABILITY, InMemoryTaskStore, REBUILD_ANALYSIS_CAPABILITY, SERVER_CAPABILITIES, TASK_CANCEL_CAPABILITY, TASK_FILTER_CAPABILITY, createServer, httpStatusForNarsilError };