@delali/narsil 0.2.4-next.48 → 0.2.4-next.50

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 (60) hide show
  1. package/README.md +2 -2
  2. package/dist/adapters/indexeddb.mjs +15 -10
  3. package/dist/{options-BC68VPoE.d.ts → capabilities-JrVQOwi4.d.ts} +55 -47
  4. package/dist/{chunk-BFIH7OV5.mjs → chunk-2GWVQWGA.mjs} +3 -1
  5. package/dist/chunk-AV4IJKD2.mjs +35 -0
  6. package/dist/{chunk-EV45L35L.mjs → chunk-BEIK3EOZ.mjs} +845 -2407
  7. package/dist/chunk-CBLS46IE.mjs +10 -0
  8. package/dist/chunk-CMVG2UEF.mjs +2038 -0
  9. package/dist/chunk-CV5H7XL7.mjs +876 -0
  10. package/dist/{chunk-JQUL2T6I.mjs → chunk-GCM3JFVA.mjs} +1 -2
  11. package/dist/{chunk-XWICBGYT.mjs → chunk-MHE7YWAU.mjs} +3 -5
  12. package/dist/{chunk-UZTT2BKQ.mjs → chunk-ND34I3SV.mjs} +1202 -1881
  13. package/dist/{chunk-PM52SMBT.mjs → chunk-OOSIDHA3.mjs} +2 -1
  14. package/dist/chunk-ORBLD3EL.mjs +6 -0
  15. package/dist/chunk-PIWKZPP7.mjs +64 -0
  16. package/dist/{chunk-4COAUBLJ.mjs → chunk-PMBHH44R.mjs} +124 -8
  17. package/dist/chunk-RP23PWMV.mjs +11 -0
  18. package/dist/chunk-UNEEFTEU.mjs +7 -0
  19. package/dist/{chunk-WTONLDU3.mjs → chunk-WYJXSLER.mjs} +9 -19
  20. package/dist/{chunk-DFZDV5FV.mjs → chunk-X6YZO4MQ.mjs} +19 -15
  21. package/dist/chunk-XE6XR2OE.mjs +23 -0
  22. package/dist/chunk-YEO6G2NG.mjs +6 -0
  23. package/dist/chunk-ZHUQKZBE.mjs +1292 -0
  24. package/dist/{client-C5BkUqB8.d.ts → client-CBOVHdjp.d.ts} +31 -7
  25. package/dist/client.d.ts +7 -7
  26. package/dist/client.mjs +250 -232
  27. package/dist/{config-B-ZI-ft5.d.ts → config-Czh2YNDi.d.ts} +2 -2
  28. package/dist/{config-BZRExglL.d.ts → config-OON7IJ_B.d.ts} +50 -13
  29. package/dist/direct-executor-OREXU72C.mjs +12 -0
  30. package/dist/distribution/coordinator/etcd.d.ts +2 -2
  31. package/dist/distribution/coordinator/etcd.mjs +2 -3
  32. package/dist/distribution/coordinator/in-memory.d.ts +2 -2
  33. package/dist/distribution/coordinator/in-memory.mjs +2 -2
  34. package/dist/distribution/transport/grpc.mjs +7 -9
  35. package/dist/distribution/transport/in-memory.mjs +2 -1
  36. package/dist/distribution/transport/tcp.mjs +3 -2
  37. package/dist/distribution.d.ts +8 -8
  38. package/dist/distribution.mjs +142 -101
  39. package/dist/embeddings/openai.mjs +5 -3
  40. package/dist/{engine-Cjr1uBLa.d.ts → engine-9PtFY50q.d.ts} +125 -69
  41. package/dist/index.browser.mjs +4689 -3330
  42. package/dist/index.d.ts +7 -7
  43. package/dist/index.mjs +14 -8
  44. package/dist/invalidation/broadcast-channel.mjs +2 -1
  45. package/dist/invalidation/filesystem.mjs +3 -4
  46. package/dist/{search-DDHjfXhJ.d.ts → memory-Bzd5xcEW.d.ts} +89 -63
  47. package/dist/persistence/durability/checkpoint-worker.d.ts +1 -1
  48. package/dist/persistence/durability/checkpoint-worker.mjs +14 -8
  49. package/dist/react.d.ts +4 -4
  50. package/dist/react.mjs +7 -5
  51. package/dist/{schema-B7juWg8Q.d.ts → schema-BqRx9t4E.d.ts} +22 -3
  52. package/dist/server.d.ts +47 -42
  53. package/dist/server.mjs +338 -821
  54. package/dist/thread-PZLEO6JL.mjs +310 -0
  55. package/dist/{types-DsSSjY4d.d.ts → types-DXPxaXPO.d.ts} +1 -1
  56. package/dist/vector/hnsw-build-worker.mjs +3 -4
  57. package/dist/vector/search-worker.mjs +4 -123
  58. package/dist/workers/entry.mjs +47 -3
  59. package/package.json +5 -1
  60. package/dist/direct-executor-EOTFGXTO.mjs +0 -8
@@ -0,0 +1,876 @@
1
+ import { MAX_SORT_FIELDS } from './chunk-CBLS46IE.mjs';
2
+ import { ServerErrorCodes, NarsilError, ErrorCodes } from './chunk-P4ELXVJQ.mjs';
3
+
4
+ // src/server/capabilities.ts
5
+ var ASYNC_IMPORT_CAPABILITY = "documents.import.async";
6
+ var TASK_CANCEL_CAPABILITY = "tasks.cancel";
7
+ var TASK_FILTER_CAPABILITY = "tasks.filter";
8
+ var REBUILD_ANALYSIS_CAPABILITY = "indexes.rebuildAnalysis";
9
+ var INDEX_LIFECYCLE_CAPABILITY = "indexes.lifecycle";
10
+ var SERVER_CAPABILITIES = [
11
+ ASYNC_IMPORT_CAPABILITY,
12
+ TASK_CANCEL_CAPABILITY,
13
+ TASK_FILTER_CAPABILITY,
14
+ REBUILD_ANALYSIS_CAPABILITY,
15
+ INDEX_LIFECYCLE_CAPABILITY
16
+ ];
17
+
18
+ // src/server/concurrency-gate.ts
19
+ var MAIN_THREAD_GATE_SLOT = 0;
20
+ function gateSlotOfWorker(workerId) {
21
+ return workerId + 1;
22
+ }
23
+ function createSharedGateBuffer(slotCount) {
24
+ return new SharedArrayBuffer(slotCount * Int32Array.BYTES_PER_ELEMENT);
25
+ }
26
+ function clearGateSlot(buffer, slot) {
27
+ const counters = new Int32Array(buffer);
28
+ if (slot >= 0 && slot < counters.length) Atomics.store(counters, slot, 0);
29
+ }
30
+ function createSharedRequestGate(buffer, max, slot) {
31
+ const counters = new Int32Array(buffer);
32
+ function inFlight() {
33
+ let total = 0;
34
+ for (let at = 0; at < counters.length; at++) total += Atomics.load(counters, at);
35
+ return total;
36
+ }
37
+ return {
38
+ tryAcquire() {
39
+ if (max <= 0) return true;
40
+ if (inFlight() >= max) return false;
41
+ Atomics.add(counters, slot, 1);
42
+ if (inFlight() <= max) return true;
43
+ Atomics.sub(counters, slot, 1);
44
+ return false;
45
+ },
46
+ release() {
47
+ if (max <= 0) return;
48
+ if (Atomics.load(counters, slot) > 0) Atomics.sub(counters, slot, 1);
49
+ }
50
+ };
51
+ }
52
+
53
+ // src/server/cors.ts
54
+ function resolveCors(cors) {
55
+ if (!cors) return null;
56
+ if (cors === true) {
57
+ return { origin: "*", methods: "GET, POST, PUT, PATCH, DELETE, OPTIONS", headers: "Content-Type, Authorization" };
58
+ }
59
+ return {
60
+ origin: cors.origin ?? "*",
61
+ methods: cors.methods?.join(", ") ?? "GET, POST, PUT, PATCH, DELETE, OPTIONS",
62
+ headers: cors.headers?.join(", ") ?? "Content-Type, Authorization"
63
+ };
64
+ }
65
+ function matchOrigin(cors, requestOrigin) {
66
+ if (cors.origin === "*") return "*";
67
+ if (typeof cors.origin === "string") return cors.origin;
68
+ return cors.origin.includes(requestOrigin) ? requestOrigin : null;
69
+ }
70
+ function writeCorsOrigin(res, cors, requestOrigin) {
71
+ const allowed = matchOrigin(cors, requestOrigin);
72
+ if (!allowed) return;
73
+ res.writeHeader("Access-Control-Allow-Origin", allowed);
74
+ if (allowed !== "*") res.writeHeader("Vary", "Origin");
75
+ }
76
+ function corsWriter(cors) {
77
+ return (res, req) => writeCorsOrigin(res, cors, req.getHeader("origin"));
78
+ }
79
+
80
+ // src/server/errors.ts
81
+ var STATUS_BY_CODE = {
82
+ INVALID_REQUEST: 400,
83
+ INVALID_JSON: 400,
84
+ EMPTY_BODY: 400,
85
+ PAYLOAD_TOO_LARGE: 413,
86
+ NOT_FOUND: 404,
87
+ TASK_NOT_FOUND: 404,
88
+ TASK_NOT_CANCELLABLE: 409,
89
+ TASK_OWNED_BY_ANOTHER_INSTANCE: 409,
90
+ TASK_INTERRUPTED: 503,
91
+ TOO_MANY_REQUESTS: 429,
92
+ HOOK_ERROR: 500,
93
+ INTERNAL_ERROR: 500,
94
+ ENVELOPE_INVALID_MAGIC: 400,
95
+ ENVELOPE_VERSION_MISMATCH: 400,
96
+ SCHEMA_INVALID_TYPE: 400,
97
+ SCHEMA_DEPTH_EXCEEDED: 400,
98
+ SCHEMA_INVALID_VECTOR_DIMENSION: 400,
99
+ SCHEMA_INVALID_GEOPOINT: 400,
100
+ DOC_VALIDATION_FAILED: 400,
101
+ DOC_MISSING_REQUIRED_FIELD: 400,
102
+ SEARCH_INVALID_FIELD: 400,
103
+ VECTOR_DIMENSION_MISMATCH: 400,
104
+ SEARCH_INVALID_FILTER: 400,
105
+ SEARCH_INVALID_MODE: 400,
106
+ SEARCH_INVALID_CURSOR: 400,
107
+ SEARCH_RESULT_WINDOW_EXCEEDED: 400,
108
+ LANGUAGE_NOT_SUPPORTED: 400,
109
+ CONFIG_INVALID: 400,
110
+ EMBEDDING_DIMENSION_MISMATCH: 400,
111
+ EMBEDDING_NO_SOURCE: 400,
112
+ EMBEDDING_CONFIG_INVALID: 400,
113
+ INDEX_NOT_FOUND: 404,
114
+ DOC_NOT_FOUND: 404,
115
+ INDEX_ALREADY_EXISTS: 409,
116
+ INDEX_ORPHANED: 409,
117
+ INDEX_REOPEN_CAPACITY_EXHAUSTED: 503,
118
+ DOC_ALREADY_EXISTS: 409,
119
+ PARTITION_REBALANCING_BACKPRESSURE: 409,
120
+ PARTITION_CAPACITY_EXCEEDED: 409,
121
+ WORKER_BUSY: 429,
122
+ PARTITION_CORRUPTED: 503,
123
+ WORKER_CRASHED: 503,
124
+ WORKER_TIMEOUT: 503,
125
+ PERSISTENCE_SAVE_FAILED: 503,
126
+ PERSISTENCE_LOAD_FAILED: 503,
127
+ PERSISTENCE_DELETE_FAILED: 503,
128
+ PERSISTENCE_CRC_MISMATCH: 503,
129
+ PERSISTENCE_WAL_CORRUPT: 503,
130
+ PERSISTENCE_FSYNC_FAILED: 503,
131
+ EMBEDDING_FAILED: 503,
132
+ QUERY_ROUTING_FAILED: 503,
133
+ QUERY_PARTIAL_FAILURE: 503,
134
+ QUERY_NODE_TIMEOUT: 503,
135
+ QUERY_NO_ACTIVE_REPLICA: 503,
136
+ CLUSTER_OPERATION_UNSUPPORTED: 501,
137
+ REPLICATION_ENTRY_INVALID: 503,
138
+ REPLICATION_INSYNC_REMOVAL_FAILED: 503,
139
+ REPLICATION_ROLLBACK_FAILED: 503,
140
+ REPLICATION_LOG_FULL: 503,
141
+ REPLICATION_ENTRY_CORRUPT: 503,
142
+ REPLICATION_SNAPSHOT_CORRUPT: 503,
143
+ REPLICATION_TERM_MISMATCH: 503,
144
+ REPLICATION_SYNC_FAILED: 503,
145
+ PARTITION_NOT_PRIMARY: 503,
146
+ PARTITION_UNASSIGNED: 503,
147
+ INSUFFICIENT_REPLICAS: 503,
148
+ TRANSPORT_DEPENDENCY_MISSING: 500,
149
+ ALLOCATION_INVALID_CONFIG: 400,
150
+ ALLOCATION_NO_DATA_NODES: 503,
151
+ ALLOCATION_FAILED: 503,
152
+ NODE_ALREADY_JOINED: 409,
153
+ NODE_NOT_JOINED: 503,
154
+ NODE_NOT_READY: 503,
155
+ NODE_NOT_CONTROLLER: 503,
156
+ NODE_BOOTSTRAP_FAILED: 503,
157
+ COORDINATOR_DEPENDENCY_MISSING: 500,
158
+ SNAPSHOT_SYNC_UNAUTHORIZED: 403,
159
+ SNAPSHOT_SYNC_REQUEST_INVALID: 400,
160
+ SNAPSHOT_SYNC_HEADER_INVALID: 400,
161
+ SNAPSHOT_SYNC_FRAME_INVALID: 400,
162
+ SNAPSHOT_SYNC_INDEX_NOT_FOUND: 404,
163
+ SNAPSHOT_SYNC_TOO_LARGE: 413,
164
+ SNAPSHOT_SYNC_CHUNK_SIZE_EXCEEDED: 413,
165
+ SNAPSHOT_SYNC_ABORTED: 409,
166
+ SNAPSHOT_SYNC_CAPACITY_EXHAUSTED: 503,
167
+ SNAPSHOT_SYNC_SNAPSHOT_FAILED: 503,
168
+ SNAPSHOT_SYNC_DECODE_FAILED: 503,
169
+ SNAPSHOT_SYNC_HEADER_MISMATCH: 503,
170
+ SNAPSHOT_SYNC_CHUNK_OUT_OF_ORDER: 503,
171
+ SNAPSHOT_SYNC_CHUNK_OVERFLOW: 503,
172
+ SNAPSHOT_SYNC_CHUNK_MISSING: 503,
173
+ SNAPSHOT_SYNC_END_MISSING: 503,
174
+ SNAPSHOT_SYNC_CHECKSUM_MISMATCH: 503,
175
+ SNAPSHOT_SYNC_PRIMARY_ERROR: 503,
176
+ SNAPSHOT_SYNC_NO_TARGETS: 503,
177
+ SNAPSHOT_SYNC_TRANSPORT_FAILED: 503,
178
+ SNAPSHOT_SYNC_SCHEMA_UNAVAILABLE: 503,
179
+ SNAPSHOT_SYNC_RESTORE_FAILED: 503,
180
+ SNAPSHOT_SYNC_RESTORE_CLEANUP_FAILED: 503,
181
+ SNAPSHOT_SYNC_TIMEOUT: 503,
182
+ SNAPSHOT_SYNC_ALLOCATION_UNAVAILABLE: 503,
183
+ SNAPSHOT_SYNC_NOT_ASSIGNED: 503,
184
+ CONTROLLER_METADATA_INVALID: 500
185
+ };
186
+ function httpStatusForNarsilError(code) {
187
+ return STATUS_BY_CODE[code] ?? 500;
188
+ }
189
+ function serializeNarsilError(err) {
190
+ const hasDetails = err.details && Object.keys(err.details).length > 0;
191
+ return hasDetails ? { code: err.code, message: err.message, details: err.details } : { code: err.code, message: err.message };
192
+ }
193
+ function toHttpError(err) {
194
+ if (err instanceof NarsilError) {
195
+ return { status: httpStatusForNarsilError(err.code), body: serializeNarsilError(err) };
196
+ }
197
+ return {
198
+ status: 500,
199
+ body: { code: ServerErrorCodes.INTERNAL_ERROR, message: "An unexpected error occurred" }
200
+ };
201
+ }
202
+
203
+ // src/json-encoding.ts
204
+ function toPlainNumbers(key, value) {
205
+ if (ArrayBuffer.isView(value) && !(value instanceof DataView)) {
206
+ return Array.from(value);
207
+ }
208
+ return value;
209
+ }
210
+ function encodeJson(value) {
211
+ return JSON.stringify(value, toPlainNumbers);
212
+ }
213
+
214
+ // src/server/constants.ts
215
+ var BYTES_PER_MB = 1024 * 1024;
216
+ var DEFAULT_MAX_BODY_BYTES = 16 * BYTES_PER_MB;
217
+ var DEFAULT_MAX_IMPORT_BYTES = 100 * BYTES_PER_MB;
218
+ var DEFAULT_MAX_LINE_BYTES = 4 * BYTES_PER_MB;
219
+ var DEFAULT_IMPORT_BATCH_SIZE = 1e3;
220
+ var DEFAULT_MAX_CONCURRENT_REQUESTS = 0;
221
+ var DEFAULT_MAX_RESULT_WINDOW = 1e4;
222
+ var DEFAULT_MAX_FETCH_DOCUMENTS = 1e4;
223
+ var DEFAULT_MAX_IMPORT_ERRORS = 100;
224
+ var DEFAULT_MAX_TASK_PAGE_SIZE = 1e3;
225
+ var DEFAULT_MAX_CONCURRENT_TASKS = 4;
226
+ var RESPONSE_STREAM_THRESHOLD_BYTES = 64 * 1024;
227
+ var MAX_INDEX_TOUCH_INTERVAL_MS = 1e3;
228
+ var INDEX_TOUCH_INTERVALS_PER_IDLE_TIMEOUT = 4;
229
+ var DEFAULT_MAX_RETAINED_TASKS = 1e3;
230
+ var DEFAULT_TASK_PAGE_SIZE = 20;
231
+ var TASK_PROGRESS_WRITE_INTERVAL_MS = 250;
232
+ var RUNNING_TASK_TTL_MS = 24 * 60 * 60 * 1e3;
233
+ var TERMINAL_TASK_TTL_MS = 60 * 60 * 1e3;
234
+
235
+ // src/server/response.ts
236
+ var STATUS_TEXT = {
237
+ 200: "200 OK",
238
+ 201: "201 Created",
239
+ 202: "202 Accepted",
240
+ 204: "204 No Content",
241
+ 400: "400 Bad Request",
242
+ 401: "401 Unauthorized",
243
+ 403: "403 Forbidden",
244
+ 404: "404 Not Found",
245
+ 405: "405 Method Not Allowed",
246
+ 409: "409 Conflict",
247
+ 413: "413 Payload Too Large",
248
+ 415: "415 Unsupported Media Type",
249
+ 429: "429 Too Many Requests",
250
+ 500: "500 Internal Server Error",
251
+ 503: "503 Service Unavailable"
252
+ };
253
+ function statusLine(status) {
254
+ return STATUS_TEXT[status] ?? `${status}`;
255
+ }
256
+ function streamBody(res, abort, body, status, headers) {
257
+ if (abort.aborted) return;
258
+ const totalSize = body.byteLength;
259
+ res.cork(() => {
260
+ res.writeStatus(statusLine(status));
261
+ for (const [key, value] of headers) res.writeHeader(key, value);
262
+ const startOffset = res.getWriteOffset();
263
+ const [, done] = res.tryEnd(body, totalSize);
264
+ if (done) return;
265
+ res.onWritable((offset) => {
266
+ if (abort.aborted) return true;
267
+ const [retryOk] = res.tryEnd(body.subarray(offset - startOffset), totalSize);
268
+ return retryOk;
269
+ });
270
+ });
271
+ }
272
+ function sendJson(res, data, status = 200, abort) {
273
+ const payload = encodeJson(data);
274
+ if (abort && payload !== void 0 && Buffer.byteLength(payload) >= RESPONSE_STREAM_THRESHOLD_BYTES) {
275
+ streamBody(res, abort, Buffer.from(payload, "utf8"), status, [["Content-Type", "application/json"]]);
276
+ return;
277
+ }
278
+ res.cork(() => {
279
+ res.writeStatus(statusLine(status)).writeHeader("Content-Type", "application/json").end(payload);
280
+ });
281
+ }
282
+ function sendError(res, status, code, message, details) {
283
+ const body = { error: details ? { code, message, details } : { code, message } };
284
+ const payload = encodeJson(body);
285
+ res.cork(() => {
286
+ res.writeStatus(statusLine(status)).writeHeader("Content-Type", "application/json").end(payload);
287
+ });
288
+ }
289
+ function sendBinary(res, data, abort, status = 200) {
290
+ streamBody(res, abort, data, status, [["Content-Type", "application/octet-stream"]]);
291
+ }
292
+ function sendRelayed(res, abort, status, headers, body) {
293
+ if (abort.aborted) return;
294
+ if (body.byteLength >= RESPONSE_STREAM_THRESHOLD_BYTES) {
295
+ streamBody(res, abort, body, status, headers);
296
+ return;
297
+ }
298
+ res.cork(() => {
299
+ res.writeStatus(statusLine(status));
300
+ for (const [key, value] of headers) res.writeHeader(key, value);
301
+ res.end(body);
302
+ });
303
+ }
304
+
305
+ // src/server/handler-utils.ts
306
+ function parseJson(ctx) {
307
+ const raw = ctx.rawBody;
308
+ if (!raw || raw.length === 0) {
309
+ sendError(ctx.res, 400, ServerErrorCodes.EMPTY_BODY, "Request body is empty");
310
+ return void 0;
311
+ }
312
+ try {
313
+ return JSON.parse(raw.toString("utf8"));
314
+ } catch {
315
+ sendError(ctx.res, 400, ServerErrorCodes.INVALID_JSON, "Request body is not valid JSON");
316
+ return void 0;
317
+ }
318
+ }
319
+ function parseJsonOptional(ctx) {
320
+ const raw = ctx.rawBody;
321
+ if (!raw || raw.length === 0) return {};
322
+ try {
323
+ return JSON.parse(raw.toString("utf8"));
324
+ } catch {
325
+ sendError(ctx.res, 400, ServerErrorCodes.INVALID_JSON, "Request body is not valid JSON");
326
+ return void 0;
327
+ }
328
+ }
329
+ function respondError(ctx, err) {
330
+ if (ctx.abort.aborted) return;
331
+ const { status, body } = toHttpError(err);
332
+ sendError(ctx.res, status, body.code, body.message, body.details);
333
+ }
334
+ function respondJson(ctx, data, status = 200) {
335
+ if (ctx.abort.aborted) return;
336
+ sendJson(ctx.res, data, status, ctx.abort);
337
+ }
338
+ function badRequest(res, message, details) {
339
+ sendError(res, 400, ServerErrorCodes.INVALID_REQUEST, message, details);
340
+ }
341
+ function rejectInvalid(ctx, failure) {
342
+ if (ctx.abort.aborted) return;
343
+ badRequest(ctx.res, failure.message, failure.details);
344
+ }
345
+ function serializeBatchResult(result) {
346
+ return {
347
+ succeeded: result.succeeded,
348
+ failed: result.failed.map((f) => ({
349
+ docId: f.docId,
350
+ error: f.error instanceof NarsilError ? serializeNarsilError(f.error) : { code: "INTERNAL_ERROR", message: String(f.error) }
351
+ }))
352
+ };
353
+ }
354
+
355
+ // src/server/validation.ts
356
+ function isPlainObject(value) {
357
+ return typeof value === "object" && value !== null && !Array.isArray(value);
358
+ }
359
+ function overWindow(field, value, maxWindow) {
360
+ if (typeof value === "number" && value > maxWindow) {
361
+ return {
362
+ message: `Field "${field}" exceeds the maximum result window of ${maxWindow}`,
363
+ details: { value, limit: maxWindow }
364
+ };
365
+ }
366
+ return null;
367
+ }
368
+ function validateQuery(params, maxWindow) {
369
+ if (params.group && typeof params.group === "object" && "reduce" in params.group) {
370
+ return {
371
+ message: 'Custom group reducers are not available over HTTP; use "group.fields" and "group.maxPerGroup" only'
372
+ };
373
+ }
374
+ const rowBound = overWindow("limit", params.limit, maxWindow) ?? overWindow("offset", params.offset, maxWindow);
375
+ if (rowBound) return rowBound;
376
+ if (params.group && typeof params.group === "object") {
377
+ const grouped = overWindow("group.maxPerGroup", params.group.maxPerGroup, maxWindow) ?? overWindow("group.limit", params.group.limit, maxWindow);
378
+ if (grouped) return grouped;
379
+ }
380
+ if (params.facets && typeof params.facets === "object") {
381
+ for (const [field, facet] of Object.entries(params.facets)) {
382
+ if (facet && typeof facet === "object") {
383
+ const faceted = overWindow(`facets.${field}.limit`, facet.limit, maxWindow);
384
+ if (faceted) return faceted;
385
+ }
386
+ }
387
+ }
388
+ return null;
389
+ }
390
+ function validateSuggest(params) {
391
+ if (typeof params.prefix !== "string") {
392
+ return { message: 'Field "prefix" is required and must be a string' };
393
+ }
394
+ return null;
395
+ }
396
+ function validateDocumentBody(body) {
397
+ if (!isPlainObject(body.document)) {
398
+ return { message: 'Field "document" is required and must be an object' };
399
+ }
400
+ return null;
401
+ }
402
+ function validateMultiGet(body, maxFetch) {
403
+ if (!Array.isArray(body.docIds)) {
404
+ return { message: 'Field "docIds" is required and must be an array of strings' };
405
+ }
406
+ if (body.docIds.length > maxFetch) {
407
+ return {
408
+ message: `Field "docIds" exceeds the maximum of ${maxFetch} ids per request`,
409
+ details: { count: body.docIds.length, limit: maxFetch }
410
+ };
411
+ }
412
+ return null;
413
+ }
414
+ function validateList(params, maxFetch) {
415
+ if (params.cursor !== void 0 && typeof params.cursor !== "string") {
416
+ return { message: 'Field "cursor" must be a string' };
417
+ }
418
+ if (params.limit !== void 0 && typeof params.limit !== "number") {
419
+ return { message: 'Field "limit" must be a number' };
420
+ }
421
+ if (typeof params.limit === "number" && params.limit > maxFetch) {
422
+ return {
423
+ message: `Field "limit" exceeds the maximum of ${maxFetch} documents per request`,
424
+ details: { value: params.limit, limit: maxFetch }
425
+ };
426
+ }
427
+ return validateListSort(params.sort);
428
+ }
429
+ function validateListSort(sort) {
430
+ if (sort === void 0) return null;
431
+ if (typeof sort !== "object" || sort === null) {
432
+ return { message: 'Field "sort" must be an object keyed by field name, or a list of fields' };
433
+ }
434
+ const entries = Array.isArray(sort) ? sort.map(
435
+ (entry) => typeof entry === "object" && entry !== null ? [entry.field, entry.direction] : [entry, void 0]
436
+ ) : Object.entries(sort);
437
+ if (entries.length > MAX_SORT_FIELDS) {
438
+ return {
439
+ message: `Field "sort" exceeds the maximum of ${MAX_SORT_FIELDS} fields`,
440
+ details: { count: entries.length, limit: MAX_SORT_FIELDS }
441
+ };
442
+ }
443
+ for (const [field, direction] of entries) {
444
+ if (typeof field !== "string" || field.length === 0) {
445
+ return { message: 'Every entry of "sort" must name a field', details: { value: field } };
446
+ }
447
+ if (direction !== "asc" && direction !== "desc") {
448
+ return { message: `Field "sort.${field}" must be "asc" or "desc"`, details: { value: direction } };
449
+ }
450
+ }
451
+ return null;
452
+ }
453
+ function validateBatch(body) {
454
+ const action = body.action ?? "insert";
455
+ if (action === "insert") {
456
+ if (!Array.isArray(body.documents)) return { message: 'Batch insert requires a "documents" array' };
457
+ return null;
458
+ }
459
+ if (action === "update") {
460
+ if (!Array.isArray(body.updates)) return { message: 'Batch update requires an "updates" array' };
461
+ return null;
462
+ }
463
+ if (action === "delete") {
464
+ if (!Array.isArray(body.docIds)) return { message: 'Batch delete requires a "docIds" array' };
465
+ return null;
466
+ }
467
+ return { message: 'Field "action" must be one of "insert", "update", or "delete"' };
468
+ }
469
+ function validateCreateIndex(body) {
470
+ if (typeof body.name !== "string" || body.name.length === 0) {
471
+ return { message: 'Field "name" is required and must be a non-empty string' };
472
+ }
473
+ if (!isPlainObject(body.config)) {
474
+ return { message: 'Field "config" is required and must be an object' };
475
+ }
476
+ return null;
477
+ }
478
+ function validateRebalance(body) {
479
+ const target = body.targetPartitionCount;
480
+ if (typeof target !== "number" || !Number.isInteger(target) || target <= 0) {
481
+ return { message: 'Field "targetPartitionCount" is required and must be a positive integer' };
482
+ }
483
+ return null;
484
+ }
485
+ var TASK_TYPES = ["optimizeVectors", "rebalance", "restore", "import", "rebuildAnalysis"];
486
+ var TASK_STATUSES = ["queued", "running", "succeeded", "failed", "cancelled"];
487
+ function parseCountParam(raw, field, minimum) {
488
+ if (raw === null) return minimum;
489
+ const value = Number(raw);
490
+ if (!Number.isInteger(value) || value < minimum) {
491
+ return { message: `Query parameter "${field}" must be an integer of ${minimum} or more`, details: { value: raw } };
492
+ }
493
+ return value;
494
+ }
495
+ function parseEnumParam(raw, field, allowed) {
496
+ if (raw === null) return void 0;
497
+ const requested = raw.split(",").map((entry) => entry.trim());
498
+ const unknown = requested.filter((entry) => !allowed.includes(entry));
499
+ if (requested.length === 0 || unknown.length > 0) {
500
+ return {
501
+ message: `Query parameter "${field}" accepts only ${allowed.join(", ")}`,
502
+ details: { unknown }
503
+ };
504
+ }
505
+ return requested;
506
+ }
507
+ function isFailure(value) {
508
+ return isPlainObject(value) && typeof value.message === "string";
509
+ }
510
+ function parseTaskListQuery(params, maxPageSize) {
511
+ const type = parseEnumParam(params.get("type"), "type", TASK_TYPES);
512
+ if (isFailure(type)) return { failure: type };
513
+ const status = parseEnumParam(params.get("status"), "status", TASK_STATUSES);
514
+ if (isFailure(status)) return { failure: status };
515
+ const from = parseCountParam(params.get("from"), "from", 0);
516
+ if (isFailure(from)) return { failure: from };
517
+ const limit = parseCountParam(params.get("limit"), "limit", 1);
518
+ if (isFailure(limit)) return { failure: limit };
519
+ if (params.get("limit") !== null && limit > maxPageSize) {
520
+ return {
521
+ failure: {
522
+ message: `Query parameter "limit" exceeds the maximum page size of ${maxPageSize}`,
523
+ details: { value: limit, limit: maxPageSize }
524
+ }
525
+ };
526
+ }
527
+ const indexName = params.get("indexName");
528
+ return {
529
+ query: {
530
+ ...indexName === null ? {} : { indexName },
531
+ ...type === void 0 ? {} : { type },
532
+ ...status === void 0 ? {} : { status },
533
+ from,
534
+ ...params.get("limit") === null ? {} : { limit }
535
+ }
536
+ };
537
+ }
538
+
539
+ // src/server/handlers/document-reads.ts
540
+ function createDocumentReadHandlers(deps) {
541
+ const { engine, limits } = deps;
542
+ async function get(ctx) {
543
+ try {
544
+ const document = await engine.get(ctx.params[0], ctx.params[1]);
545
+ if (document === void 0) {
546
+ respondError(ctx, indexDocNotFound(ctx.params[1]));
547
+ return;
548
+ }
549
+ respondJson(ctx, { document });
550
+ } catch (err) {
551
+ respondError(ctx, err);
552
+ }
553
+ }
554
+ async function exists(ctx) {
555
+ try {
556
+ respondJson(ctx, { exists: await engine.has(ctx.params[0], ctx.params[1]) });
557
+ } catch (err) {
558
+ respondError(ctx, err);
559
+ }
560
+ }
561
+ async function count(ctx) {
562
+ try {
563
+ respondJson(ctx, { count: await engine.countDocuments(ctx.params[0]) });
564
+ } catch (err) {
565
+ respondError(ctx, err);
566
+ }
567
+ }
568
+ async function multiGet(ctx) {
569
+ const body = parseJson(ctx);
570
+ if (!body) return;
571
+ const failure = validateMultiGet(body, limits.maxFetchDocuments);
572
+ if (failure) {
573
+ rejectInvalid(ctx, failure);
574
+ return;
575
+ }
576
+ try {
577
+ const found = await engine.getMultiple(ctx.params[0], body.docIds);
578
+ const documents = {};
579
+ for (const [id, doc] of found) documents[id] = doc;
580
+ respondJson(ctx, { documents });
581
+ } catch (err) {
582
+ respondError(ctx, err);
583
+ }
584
+ }
585
+ async function list(ctx) {
586
+ const params = parseJson(ctx);
587
+ if (!params) return;
588
+ const failure = validateList(params, limits.maxFetchDocuments);
589
+ if (failure) {
590
+ rejectInvalid(ctx, failure);
591
+ return;
592
+ }
593
+ try {
594
+ respondJson(ctx, await engine.listDocuments(ctx.params[0], params));
595
+ } catch (err) {
596
+ respondError(ctx, err);
597
+ }
598
+ }
599
+ return { get, exists, count, multiGet, list };
600
+ }
601
+ function indexDocNotFound(docId) {
602
+ return new NarsilError(ErrorCodes.DOC_NOT_FOUND, `Document "${docId}" not found`, { docId });
603
+ }
604
+
605
+ // src/server/request.ts
606
+ function initAbortHandler(res) {
607
+ const listeners = [];
608
+ let aborted = false;
609
+ res.onAborted(() => {
610
+ aborted = true;
611
+ for (const fn of listeners) fn();
612
+ });
613
+ return {
614
+ get aborted() {
615
+ return aborted;
616
+ },
617
+ onAbort(fn) {
618
+ if (aborted) fn();
619
+ else listeners.push(fn);
620
+ }
621
+ };
622
+ }
623
+ function readBody(res, maxBytes, abort) {
624
+ return new Promise((resolve, reject) => {
625
+ if (abort.aborted) {
626
+ reject(new Error("aborted"));
627
+ return;
628
+ }
629
+ let done = false;
630
+ let total = 0;
631
+ const chunks = [];
632
+ abort.onAbort(() => {
633
+ if (done) return;
634
+ done = true;
635
+ reject(new Error("aborted"));
636
+ });
637
+ res.onData((chunk, isLast) => {
638
+ if (done || abort.aborted) return;
639
+ total += chunk.byteLength;
640
+ if (total > maxBytes) {
641
+ done = true;
642
+ if (!abort.aborted) {
643
+ sendError(res, 413, ServerErrorCodes.PAYLOAD_TOO_LARGE, "Request body exceeds the configured size limit");
644
+ }
645
+ reject(new Error("payload too large"));
646
+ return;
647
+ }
648
+ chunks.push(Buffer.from(new Uint8Array(chunk)));
649
+ if (isLast) {
650
+ done = true;
651
+ resolve(Buffer.concat(chunks));
652
+ }
653
+ });
654
+ });
655
+ }
656
+ function decodePathParameter(raw) {
657
+ if (!raw.includes("%")) return raw;
658
+ try {
659
+ return decodeURIComponent(raw);
660
+ } catch {
661
+ return null;
662
+ }
663
+ }
664
+ function isDenial(value) {
665
+ return typeof value === "object" && value !== null && "status" in value;
666
+ }
667
+ function authorizerFor(hook) {
668
+ return async (context) => {
669
+ try {
670
+ const result = await hook(context);
671
+ if (isDenial(result)) return { allowed: false, denial: result };
672
+ return { allowed: true };
673
+ } catch {
674
+ return { allowed: false, denial: null };
675
+ }
676
+ };
677
+ }
678
+ function sendDenial(res, authorization) {
679
+ if (authorization.allowed) return;
680
+ if (authorization.denial === null) {
681
+ sendError(res, 500, ServerErrorCodes.HOOK_ERROR, "The onRequest hook threw an error");
682
+ return;
683
+ }
684
+ sendError(res, authorization.denial.status, authorization.denial.code, authorization.denial.message);
685
+ }
686
+ function createRouteRunner(deps) {
687
+ const { authorize, gate, writeCors } = deps;
688
+ return (handler, opts) => {
689
+ const paramCount = opts.paramCount ?? 0;
690
+ const needsBody = opts.needsBody ?? false;
691
+ const useHook = authorize !== void 0 && !opts.skipHooks;
692
+ const gated = !opts.skipHooks;
693
+ return (res, req) => {
694
+ const params = [];
695
+ let malformed = null;
696
+ for (let i = 0; i < paramCount; i++) {
697
+ const decoded = decodePathParameter(req.getParameter(i) ?? "");
698
+ if (decoded === null) {
699
+ malformed = req.getParameter(i) ?? "";
700
+ break;
701
+ }
702
+ params.push(decoded);
703
+ }
704
+ const query = new URLSearchParams(req.getQuery() ?? "");
705
+ const contentType = req.getHeader("content-type");
706
+ let hookContext = null;
707
+ if (useHook) {
708
+ const headers = {};
709
+ req.forEach((key, value) => {
710
+ headers[key] = value;
711
+ });
712
+ hookContext = {
713
+ method: req.getMethod(),
714
+ path: req.getUrl(),
715
+ headers,
716
+ remoteAddress: Buffer.from(res.getRemoteAddressAsText()).toString()
717
+ };
718
+ }
719
+ if (writeCors) writeCors(res, req);
720
+ const abort = initAbortHandler(res);
721
+ if (malformed !== null) {
722
+ sendError(
723
+ res,
724
+ 400,
725
+ ServerErrorCodes.INVALID_REQUEST,
726
+ "A path segment holds a percent-escape the server cannot decode",
727
+ { segment: malformed }
728
+ );
729
+ return;
730
+ }
731
+ const bodyPromise = needsBody ? readBody(res, opts.maxBytes, abort) : Promise.resolve(null);
732
+ const hookPromise = hookContext !== null && authorize !== void 0 ? authorize(hookContext) : Promise.resolve({ allowed: true });
733
+ Promise.all([bodyPromise, hookPromise]).then(async ([rawBody, authorization]) => {
734
+ if (abort.aborted) return;
735
+ if (!authorization.allowed) {
736
+ sendDenial(res, authorization);
737
+ return;
738
+ }
739
+ if (gated && !gate.tryAcquire()) {
740
+ sendError(res, 429, ServerErrorCodes.TOO_MANY_REQUESTS, "The server is at capacity; retry shortly");
741
+ return;
742
+ }
743
+ try {
744
+ await handler({ res, params, query, contentType, rawBody, abort, hookContext });
745
+ } catch (err) {
746
+ if (!abort.aborted) {
747
+ sendError(res, 500, ServerErrorCodes.INTERNAL_ERROR, "An unexpected error occurred");
748
+ }
749
+ } finally {
750
+ if (gated) gate.release();
751
+ }
752
+ }).catch(() => {
753
+ });
754
+ };
755
+ };
756
+ }
757
+
758
+ // src/server/routes.ts
759
+ function serverRoutes(limits) {
760
+ const { maxBodyBytes, maxImportBytes } = limits;
761
+ const probe = { maxBytes: 0, skipHooks: true };
762
+ const bare = { maxBytes: 0 };
763
+ const named = { paramCount: 1, maxBytes: 0 };
764
+ const namedBody = { paramCount: 1, needsBody: true, maxBytes: maxBodyBytes };
765
+ const document = { paramCount: 2, maxBytes: 0 };
766
+ const documentBody = { paramCount: 2, needsBody: true, maxBytes: maxBodyBytes };
767
+ return [
768
+ { method: "get", path: "/livez", handler: "livez", options: probe },
769
+ { method: "get", path: "/readyz", handler: "readyz", options: probe },
770
+ { method: "get", path: "/health", handler: "livez", options: probe },
771
+ { method: "get", path: "/version", handler: "version", options: probe },
772
+ { method: "get", path: "/capabilities", handler: "capabilities", options: probe },
773
+ { method: "get", path: "/cluster", handler: "clusterTopology", options: bare },
774
+ { method: "get", path: "/stats/memory", handler: "memory", options: bare },
775
+ { method: "get", path: "/tasks", handler: "listTasks", options: bare },
776
+ { method: "get", path: "/tasks/:id", handler: "getTask", options: named },
777
+ { method: "post", path: "/tasks/:id/_cancel", handler: "cancelTask", options: named },
778
+ { method: "post", path: "/indexes", handler: "createIndex", options: { needsBody: true, maxBytes: maxBodyBytes } },
779
+ { method: "get", path: "/indexes", handler: "listIndexes", options: bare },
780
+ { method: "del", path: "/indexes/:name", handler: "dropIndex", options: named },
781
+ { method: "get", path: "/indexes/:name/stats", handler: "indexStats", options: named },
782
+ { method: "get", path: "/indexes/:name/partitions", handler: "indexPartitions", options: named },
783
+ { method: "get", path: "/indexes/:name/cluster", handler: "clusterAllocation", options: named },
784
+ { method: "post", path: "/indexes/:name/_clear", handler: "clearIndex", options: named },
785
+ { method: "post", path: "/indexes/:name/_open", handler: "openIndex", options: named },
786
+ { method: "post", path: "/indexes/:name/_close", handler: "closeIndex", options: named },
787
+ { method: "get", path: "/indexes/:name/count", handler: "count", options: named },
788
+ { method: "post", path: "/indexes/:name/documents", handler: "insert", options: namedBody },
789
+ { method: "post", path: "/indexes/:name/documents/_batch", handler: "batch", options: namedBody },
790
+ { method: "post", path: "/indexes/:name/documents/_list", handler: "list", options: namedBody },
791
+ { method: "post", path: "/indexes/:name/documents/_multi-get", handler: "multiGet", options: namedBody },
792
+ {
793
+ method: "post",
794
+ path: "/indexes/:name/documents/_import",
795
+ handler: "importNdjson",
796
+ options: { paramCount: 1, needsBody: true, maxBytes: maxImportBytes }
797
+ },
798
+ { method: "post", path: "/indexes/:name/_wait-for-writes", handler: "waitForWrites", options: named },
799
+ { method: "get", path: "/indexes/:name/documents/:id", handler: "get", options: document },
800
+ { method: "get", path: "/indexes/:name/documents/:id/_exists", handler: "exists", options: document },
801
+ { method: "put", path: "/indexes/:name/documents/:id", handler: "put", options: documentBody },
802
+ { method: "patch", path: "/indexes/:name/documents/:id", handler: "patch", options: documentBody },
803
+ { method: "del", path: "/indexes/:name/documents/:id", handler: "remove", options: document },
804
+ { method: "post", path: "/indexes/:name/search", handler: "search", options: namedBody },
805
+ { method: "post", path: "/indexes/:name/search/preflight", handler: "preflight", options: namedBody },
806
+ { method: "post", path: "/indexes/:name/suggest", handler: "suggest", options: namedBody },
807
+ { method: "post", path: "/indexes/:name/_checkpoint", handler: "checkpoint", options: named },
808
+ { method: "post", path: "/indexes/:name/_rebuild-analysis", handler: "rebuildAnalysis", options: named },
809
+ { method: "get", path: "/indexes/:name/snapshot", handler: "snapshot", options: named },
810
+ { method: "get", path: "/indexes/:name/vector-maintenance", handler: "vectorMaintenance", options: named },
811
+ { method: "post", path: "/indexes/:name/vectors/_compact", handler: "compact", options: namedBody },
812
+ { method: "post", path: "/indexes/:name/vectors/_optimize", handler: "optimize", options: namedBody },
813
+ { method: "post", path: "/indexes/:name/_rebalance", handler: "rebalance", options: namedBody },
814
+ { method: "post", path: "/indexes/:name/partition-config", handler: "partitionConfig", options: namedBody },
815
+ {
816
+ method: "post",
817
+ path: "/indexes/:name/restore",
818
+ handler: "restore",
819
+ options: { paramCount: 1, needsBody: true, maxBytes: maxImportBytes }
820
+ }
821
+ ];
822
+ }
823
+ function registerServerRoutes(app, run, handlers, limits, cors) {
824
+ if (cors) {
825
+ app.options("/*", (res, req) => {
826
+ const origin = req.getHeader("origin");
827
+ res.cork(() => {
828
+ res.writeStatus("204 No Content");
829
+ writeCorsOrigin(res, cors, origin);
830
+ res.writeHeader("Access-Control-Allow-Methods", cors.methods).writeHeader("Access-Control-Allow-Headers", cors.headers).writeHeader("Access-Control-Max-Age", "86400").endWithoutBody();
831
+ });
832
+ });
833
+ }
834
+ for (const route of serverRoutes(limits)) {
835
+ app[route.method](route.path, run(handlers[route.handler], route.options));
836
+ }
837
+ app.any("/*", (res) => {
838
+ sendError(res, 404, ServerErrorCodes.NOT_FOUND, "Route not found");
839
+ });
840
+ }
841
+
842
+ // src/server/runtime.ts
843
+ var cached = null;
844
+ async function loadUWebSockets() {
845
+ if (cached) return cached;
846
+ try {
847
+ const mod = await import('uWebSockets.js');
848
+ const resolved = typeof mod.App === "function" ? mod : mod.default;
849
+ if (!resolved || typeof resolved.App !== "function") {
850
+ throw new Error("uWebSockets.js did not expose an App factory");
851
+ }
852
+ cached = resolved;
853
+ return resolved;
854
+ } catch (err) {
855
+ throw new NarsilError(
856
+ ErrorCodes.CONFIG_INVALID,
857
+ "The `uWebSockets.js` package is required to run the Narsil HTTP server. Install it with `pnpm add github:uNetworking/uWebSockets.js#v20.58.0` (or the npm or yarn equivalent).",
858
+ { cause: err instanceof Error ? err.message : String(err) }
859
+ );
860
+ }
861
+ }
862
+
863
+ // src/server/request-threads/acceptor.ts
864
+ function acceptorOf(app) {
865
+ const candidate = app;
866
+ if (typeof candidate.addChildAppDescriptor !== "function") return null;
867
+ if (typeof candidate.removeChildAppDescriptor !== "function") return null;
868
+ return candidate;
869
+ }
870
+ function childAppOf(app) {
871
+ const candidate = app;
872
+ if (typeof candidate.getDescriptor !== "function" || typeof candidate.close !== "function") return null;
873
+ return candidate;
874
+ }
875
+
876
+ export { ASYNC_IMPORT_CAPABILITY, DEFAULT_IMPORT_BATCH_SIZE, DEFAULT_MAX_BODY_BYTES, DEFAULT_MAX_CONCURRENT_REQUESTS, DEFAULT_MAX_CONCURRENT_TASKS, DEFAULT_MAX_FETCH_DOCUMENTS, DEFAULT_MAX_IMPORT_BYTES, DEFAULT_MAX_IMPORT_ERRORS, DEFAULT_MAX_LINE_BYTES, DEFAULT_MAX_RESULT_WINDOW, DEFAULT_MAX_RETAINED_TASKS, DEFAULT_MAX_TASK_PAGE_SIZE, DEFAULT_TASK_PAGE_SIZE, INDEX_LIFECYCLE_CAPABILITY, INDEX_TOUCH_INTERVALS_PER_IDLE_TIMEOUT, MAIN_THREAD_GATE_SLOT, MAX_INDEX_TOUCH_INTERVAL_MS, REBUILD_ANALYSIS_CAPABILITY, RUNNING_TASK_TTL_MS, SERVER_CAPABILITIES, TASK_CANCEL_CAPABILITY, TASK_FILTER_CAPABILITY, TASK_PROGRESS_WRITE_INTERVAL_MS, TERMINAL_TASK_TTL_MS, acceptorOf, authorizerFor, badRequest, childAppOf, clearGateSlot, corsWriter, createDocumentReadHandlers, createRouteRunner, createSharedGateBuffer, createSharedRequestGate, gateSlotOfWorker, httpStatusForNarsilError, loadUWebSockets, parseJson, parseJsonOptional, parseTaskListQuery, registerServerRoutes, rejectInvalid, resolveCors, respondError, respondJson, sendBinary, sendError, sendRelayed, serializeBatchResult, serializeNarsilError, serverRoutes, validateBatch, validateCreateIndex, validateDocumentBody, validateQuery, validateRebalance, validateSuggest };