@delali/narsil 0.2.4-next.49 → 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 (43) hide show
  1. package/README.md +1 -1
  2. package/dist/{options-Dtk99qUs.d.ts → capabilities-JrVQOwi4.d.ts} +55 -47
  3. package/dist/chunk-AV4IJKD2.mjs +35 -0
  4. package/dist/{chunk-M6JGMKA5.mjs → chunk-BEIK3EOZ.mjs} +503 -2273
  5. package/dist/chunk-CMVG2UEF.mjs +2038 -0
  6. package/dist/chunk-CV5H7XL7.mjs +876 -0
  7. package/dist/{chunk-KGRYQVZ5.mjs → chunk-MHE7YWAU.mjs} +2 -1
  8. package/dist/{chunk-56PJBIJ6.mjs → chunk-ND34I3SV.mjs} +427 -1455
  9. package/dist/chunk-PIWKZPP7.mjs +64 -0
  10. package/dist/{chunk-HGJC5L65.mjs → chunk-PMBHH44R.mjs} +121 -2
  11. package/dist/chunk-RP23PWMV.mjs +11 -0
  12. package/dist/{chunk-RDGXYRF4.mjs → chunk-WYJXSLER.mjs} +8 -19
  13. package/dist/{chunk-JXHUHFJ5.mjs → chunk-X6YZO4MQ.mjs} +2 -11
  14. package/dist/{chunk-OWCFK42Q.mjs → chunk-XE6XR2OE.mjs} +3 -10
  15. package/dist/chunk-ZHUQKZBE.mjs +1292 -0
  16. package/dist/{client-DX7xPcNA.d.ts → client-CBOVHdjp.d.ts} +31 -7
  17. package/dist/client.d.ts +7 -7
  18. package/dist/client.mjs +26 -10
  19. package/dist/{config-B41W23Us.d.ts → config-Czh2YNDi.d.ts} +2 -2
  20. package/dist/{config-Co-zAXqO.d.ts → config-OON7IJ_B.d.ts} +26 -5
  21. package/dist/direct-executor-OREXU72C.mjs +12 -0
  22. package/dist/distribution/coordinator/etcd.d.ts +2 -2
  23. package/dist/distribution/coordinator/in-memory.d.ts +2 -2
  24. package/dist/distribution.d.ts +8 -8
  25. package/dist/distribution.mjs +28 -11
  26. package/dist/{engine-CABZg25p.d.ts → engine-9PtFY50q.d.ts} +114 -62
  27. package/dist/index.browser.mjs +3255 -2516
  28. package/dist/index.d.ts +7 -7
  29. package/dist/index.mjs +11 -6
  30. package/dist/{search-FjWhZN3f.d.ts → memory-Bzd5xcEW.d.ts} +88 -79
  31. package/dist/persistence/durability/checkpoint-worker.d.ts +1 -1
  32. package/dist/persistence/durability/checkpoint-worker.mjs +11 -6
  33. package/dist/react.d.ts +4 -4
  34. package/dist/{schema-B7juWg8Q.d.ts → schema-BqRx9t4E.d.ts} +22 -3
  35. package/dist/server.d.ts +47 -42
  36. package/dist/server.mjs +321 -817
  37. package/dist/thread-PZLEO6JL.mjs +310 -0
  38. package/dist/{types-DsSSjY4d.d.ts → types-DXPxaXPO.d.ts} +1 -1
  39. package/dist/vector/hnsw-build-worker.mjs +2 -1
  40. package/dist/vector/search-worker.mjs +3 -121
  41. package/dist/workers/entry.mjs +47 -3
  42. package/package.json +5 -1
  43. package/dist/direct-executor-HRTDWMVB.mjs +0 -9
package/dist/server.mjs CHANGED
@@ -1,494 +1,14 @@
1
- import { validateIndexName } from './chunk-KGRYQVZ5.mjs';
2
- import './chunk-OWCFK42Q.mjs';
3
- import { MAX_SORT_FIELDS } from './chunk-CBLS46IE.mjs';
4
- import { ServerErrorCodes, NarsilError, ErrorCodes } from './chunk-P4ELXVJQ.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, 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';
6
+ import { detectRuntime } from './chunk-AV4IJKD2.mjs';
7
+ import './chunk-CBLS46IE.mjs';
8
+ import { NarsilError, ServerErrorCodes, ErrorCodes } from './chunk-P4ELXVJQ.mjs';
5
9
  export { ServerErrorCodes } from './chunk-P4ELXVJQ.mjs';
6
10
  import { randomUUID } from 'crypto';
7
-
8
- // src/server/capabilities.ts
9
- var ASYNC_IMPORT_CAPABILITY = "documents.import.async";
10
- var TASK_CANCEL_CAPABILITY = "tasks.cancel";
11
- var TASK_FILTER_CAPABILITY = "tasks.filter";
12
- var REBUILD_ANALYSIS_CAPABILITY = "indexes.rebuildAnalysis";
13
- var INDEX_LIFECYCLE_CAPABILITY = "indexes.lifecycle";
14
- var SERVER_CAPABILITIES = [
15
- ASYNC_IMPORT_CAPABILITY,
16
- TASK_CANCEL_CAPABILITY,
17
- TASK_FILTER_CAPABILITY,
18
- REBUILD_ANALYSIS_CAPABILITY,
19
- INDEX_LIFECYCLE_CAPABILITY
20
- ];
21
-
22
- // src/server/constants.ts
23
- var BYTES_PER_MB = 1024 * 1024;
24
- var DEFAULT_MAX_BODY_BYTES = 16 * BYTES_PER_MB;
25
- var DEFAULT_MAX_IMPORT_BYTES = 100 * BYTES_PER_MB;
26
- var DEFAULT_MAX_LINE_BYTES = 4 * BYTES_PER_MB;
27
- var DEFAULT_IMPORT_BATCH_SIZE = 1e3;
28
- var DEFAULT_MAX_CONCURRENT_REQUESTS = 0;
29
- var DEFAULT_MAX_RESULT_WINDOW = 1e4;
30
- var DEFAULT_MAX_FETCH_DOCUMENTS = 1e4;
31
- var DEFAULT_MAX_IMPORT_ERRORS = 100;
32
- var DEFAULT_MAX_TASK_PAGE_SIZE = 1e3;
33
- var DEFAULT_MAX_CONCURRENT_TASKS = 4;
34
- var RESPONSE_STREAM_THRESHOLD_BYTES = 64 * 1024;
35
- var DEFAULT_MAX_RETAINED_TASKS = 1e3;
36
- var DEFAULT_TASK_PAGE_SIZE = 20;
37
- var TASK_PROGRESS_WRITE_INTERVAL_MS = 250;
38
- var RUNNING_TASK_TTL_MS = 24 * 60 * 60 * 1e3;
39
- var TERMINAL_TASK_TTL_MS = 60 * 60 * 1e3;
40
-
41
- // src/server/cors.ts
42
- function resolveCors(cors) {
43
- if (!cors) return null;
44
- if (cors === true) {
45
- return { origin: "*", methods: "GET, POST, PUT, PATCH, DELETE, OPTIONS", headers: "Content-Type, Authorization" };
46
- }
47
- return {
48
- origin: cors.origin ?? "*",
49
- methods: cors.methods?.join(", ") ?? "GET, POST, PUT, PATCH, DELETE, OPTIONS",
50
- headers: cors.headers?.join(", ") ?? "Content-Type, Authorization"
51
- };
52
- }
53
- function matchOrigin(cors, requestOrigin) {
54
- if (cors.origin === "*") return "*";
55
- if (typeof cors.origin === "string") return cors.origin;
56
- return cors.origin.includes(requestOrigin) ? requestOrigin : null;
57
- }
58
- function writeCorsOrigin(res, cors, requestOrigin) {
59
- const allowed = matchOrigin(cors, requestOrigin);
60
- if (!allowed) return;
61
- res.writeHeader("Access-Control-Allow-Origin", allowed);
62
- if (allowed !== "*") res.writeHeader("Vary", "Origin");
63
- }
64
- function corsWriter(cors) {
65
- return (res, req) => writeCorsOrigin(res, cors, req.getHeader("origin"));
66
- }
67
-
68
- // src/server/errors.ts
69
- var STATUS_BY_CODE = {
70
- INVALID_REQUEST: 400,
71
- INVALID_JSON: 400,
72
- EMPTY_BODY: 400,
73
- PAYLOAD_TOO_LARGE: 413,
74
- NOT_FOUND: 404,
75
- TASK_NOT_FOUND: 404,
76
- TASK_NOT_CANCELLABLE: 409,
77
- TASK_OWNED_BY_ANOTHER_INSTANCE: 409,
78
- TASK_INTERRUPTED: 503,
79
- TOO_MANY_REQUESTS: 429,
80
- HOOK_ERROR: 500,
81
- INTERNAL_ERROR: 500,
82
- ENVELOPE_INVALID_MAGIC: 400,
83
- ENVELOPE_VERSION_MISMATCH: 400,
84
- SCHEMA_INVALID_TYPE: 400,
85
- SCHEMA_DEPTH_EXCEEDED: 400,
86
- SCHEMA_INVALID_VECTOR_DIMENSION: 400,
87
- SCHEMA_INVALID_GEOPOINT: 400,
88
- DOC_VALIDATION_FAILED: 400,
89
- DOC_MISSING_REQUIRED_FIELD: 400,
90
- SEARCH_INVALID_FIELD: 400,
91
- VECTOR_DIMENSION_MISMATCH: 400,
92
- SEARCH_INVALID_FILTER: 400,
93
- SEARCH_INVALID_MODE: 400,
94
- SEARCH_INVALID_CURSOR: 400,
95
- SEARCH_RESULT_WINDOW_EXCEEDED: 400,
96
- LANGUAGE_NOT_SUPPORTED: 400,
97
- CONFIG_INVALID: 400,
98
- EMBEDDING_DIMENSION_MISMATCH: 400,
99
- EMBEDDING_NO_SOURCE: 400,
100
- EMBEDDING_CONFIG_INVALID: 400,
101
- INDEX_NOT_FOUND: 404,
102
- DOC_NOT_FOUND: 404,
103
- INDEX_ALREADY_EXISTS: 409,
104
- INDEX_ORPHANED: 409,
105
- INDEX_REOPEN_CAPACITY_EXHAUSTED: 503,
106
- DOC_ALREADY_EXISTS: 409,
107
- PARTITION_REBALANCING_BACKPRESSURE: 409,
108
- PARTITION_CAPACITY_EXCEEDED: 409,
109
- WORKER_BUSY: 429,
110
- PARTITION_CORRUPTED: 503,
111
- WORKER_CRASHED: 503,
112
- WORKER_TIMEOUT: 503,
113
- PERSISTENCE_SAVE_FAILED: 503,
114
- PERSISTENCE_LOAD_FAILED: 503,
115
- PERSISTENCE_DELETE_FAILED: 503,
116
- PERSISTENCE_CRC_MISMATCH: 503,
117
- PERSISTENCE_WAL_CORRUPT: 503,
118
- PERSISTENCE_FSYNC_FAILED: 503,
119
- EMBEDDING_FAILED: 503,
120
- QUERY_ROUTING_FAILED: 503,
121
- QUERY_PARTIAL_FAILURE: 503,
122
- QUERY_NODE_TIMEOUT: 503,
123
- QUERY_NO_ACTIVE_REPLICA: 503,
124
- CLUSTER_OPERATION_UNSUPPORTED: 501,
125
- REPLICATION_ENTRY_INVALID: 503,
126
- REPLICATION_INSYNC_REMOVAL_FAILED: 503,
127
- REPLICATION_ROLLBACK_FAILED: 503,
128
- REPLICATION_LOG_FULL: 503,
129
- REPLICATION_ENTRY_CORRUPT: 503,
130
- REPLICATION_SNAPSHOT_CORRUPT: 503,
131
- REPLICATION_TERM_MISMATCH: 503,
132
- REPLICATION_SYNC_FAILED: 503,
133
- PARTITION_NOT_PRIMARY: 503,
134
- PARTITION_UNASSIGNED: 503,
135
- INSUFFICIENT_REPLICAS: 503,
136
- TRANSPORT_DEPENDENCY_MISSING: 500,
137
- ALLOCATION_INVALID_CONFIG: 400,
138
- ALLOCATION_NO_DATA_NODES: 503,
139
- ALLOCATION_FAILED: 503,
140
- NODE_ALREADY_JOINED: 409,
141
- NODE_NOT_JOINED: 503,
142
- NODE_NOT_READY: 503,
143
- NODE_NOT_CONTROLLER: 503,
144
- NODE_BOOTSTRAP_FAILED: 503,
145
- COORDINATOR_DEPENDENCY_MISSING: 500,
146
- SNAPSHOT_SYNC_UNAUTHORIZED: 403,
147
- SNAPSHOT_SYNC_REQUEST_INVALID: 400,
148
- SNAPSHOT_SYNC_HEADER_INVALID: 400,
149
- SNAPSHOT_SYNC_FRAME_INVALID: 400,
150
- SNAPSHOT_SYNC_INDEX_NOT_FOUND: 404,
151
- SNAPSHOT_SYNC_TOO_LARGE: 413,
152
- SNAPSHOT_SYNC_CHUNK_SIZE_EXCEEDED: 413,
153
- SNAPSHOT_SYNC_ABORTED: 409,
154
- SNAPSHOT_SYNC_CAPACITY_EXHAUSTED: 503,
155
- SNAPSHOT_SYNC_SNAPSHOT_FAILED: 503,
156
- SNAPSHOT_SYNC_DECODE_FAILED: 503,
157
- SNAPSHOT_SYNC_HEADER_MISMATCH: 503,
158
- SNAPSHOT_SYNC_CHUNK_OUT_OF_ORDER: 503,
159
- SNAPSHOT_SYNC_CHUNK_OVERFLOW: 503,
160
- SNAPSHOT_SYNC_CHUNK_MISSING: 503,
161
- SNAPSHOT_SYNC_END_MISSING: 503,
162
- SNAPSHOT_SYNC_CHECKSUM_MISMATCH: 503,
163
- SNAPSHOT_SYNC_PRIMARY_ERROR: 503,
164
- SNAPSHOT_SYNC_NO_TARGETS: 503,
165
- SNAPSHOT_SYNC_TRANSPORT_FAILED: 503,
166
- SNAPSHOT_SYNC_SCHEMA_UNAVAILABLE: 503,
167
- SNAPSHOT_SYNC_RESTORE_FAILED: 503,
168
- SNAPSHOT_SYNC_RESTORE_CLEANUP_FAILED: 503,
169
- SNAPSHOT_SYNC_TIMEOUT: 503,
170
- SNAPSHOT_SYNC_ALLOCATION_UNAVAILABLE: 503,
171
- SNAPSHOT_SYNC_NOT_ASSIGNED: 503,
172
- CONTROLLER_METADATA_INVALID: 500
173
- };
174
- function httpStatusForNarsilError(code) {
175
- return STATUS_BY_CODE[code] ?? 500;
176
- }
177
- function serializeNarsilError(err) {
178
- const hasDetails = err.details && Object.keys(err.details).length > 0;
179
- return hasDetails ? { code: err.code, message: err.message, details: err.details } : { code: err.code, message: err.message };
180
- }
181
- function toHttpError(err) {
182
- if (err instanceof NarsilError) {
183
- return { status: httpStatusForNarsilError(err.code), body: serializeNarsilError(err) };
184
- }
185
- return {
186
- status: 500,
187
- body: { code: ServerErrorCodes.INTERNAL_ERROR, message: "An unexpected error occurred" }
188
- };
189
- }
190
-
191
- // src/json-encoding.ts
192
- function toPlainNumbers(key, value) {
193
- if (ArrayBuffer.isView(value) && !(value instanceof DataView)) {
194
- return Array.from(value);
195
- }
196
- return value;
197
- }
198
- function encodeJson(value) {
199
- return JSON.stringify(value, toPlainNumbers);
200
- }
201
-
202
- // src/server/response.ts
203
- var STATUS_TEXT = {
204
- 200: "200 OK",
205
- 201: "201 Created",
206
- 202: "202 Accepted",
207
- 204: "204 No Content",
208
- 400: "400 Bad Request",
209
- 401: "401 Unauthorized",
210
- 403: "403 Forbidden",
211
- 404: "404 Not Found",
212
- 405: "405 Method Not Allowed",
213
- 409: "409 Conflict",
214
- 413: "413 Payload Too Large",
215
- 415: "415 Unsupported Media Type",
216
- 429: "429 Too Many Requests",
217
- 500: "500 Internal Server Error",
218
- 503: "503 Service Unavailable"
219
- };
220
- function statusLine(status) {
221
- return STATUS_TEXT[status] ?? `${status}`;
222
- }
223
- function streamBody(res, abort, body, status, contentType) {
224
- if (abort.aborted) return;
225
- const totalSize = body.byteLength;
226
- res.cork(() => {
227
- res.writeStatus(statusLine(status)).writeHeader("Content-Type", contentType);
228
- const startOffset = res.getWriteOffset();
229
- const [, done] = res.tryEnd(body, totalSize);
230
- if (done) return;
231
- res.onWritable((offset) => {
232
- if (abort.aborted) return true;
233
- const [retryOk] = res.tryEnd(body.subarray(offset - startOffset), totalSize);
234
- return retryOk;
235
- });
236
- });
237
- }
238
- function sendJson(res, data, status = 200, abort) {
239
- const payload = encodeJson(data);
240
- if (abort && payload !== void 0 && Buffer.byteLength(payload) >= RESPONSE_STREAM_THRESHOLD_BYTES) {
241
- streamBody(res, abort, Buffer.from(payload, "utf8"), status, "application/json");
242
- return;
243
- }
244
- res.cork(() => {
245
- res.writeStatus(statusLine(status)).writeHeader("Content-Type", "application/json").end(payload);
246
- });
247
- }
248
- function sendError(res, status, code, message, details) {
249
- const body = { error: details ? { code, message, details } : { code, message } };
250
- const payload = encodeJson(body);
251
- res.cork(() => {
252
- res.writeStatus(statusLine(status)).writeHeader("Content-Type", "application/json").end(payload);
253
- });
254
- }
255
- function sendBinary(res, data, abort, status = 200) {
256
- streamBody(res, abort, data, status, "application/octet-stream");
257
- }
258
-
259
- // src/server/handler-utils.ts
260
- function parseJson(ctx) {
261
- const raw = ctx.rawBody;
262
- if (!raw || raw.length === 0) {
263
- sendError(ctx.res, 400, ServerErrorCodes.EMPTY_BODY, "Request body is empty");
264
- return void 0;
265
- }
266
- try {
267
- return JSON.parse(raw.toString("utf8"));
268
- } catch {
269
- sendError(ctx.res, 400, ServerErrorCodes.INVALID_JSON, "Request body is not valid JSON");
270
- return void 0;
271
- }
272
- }
273
- function parseJsonOptional(ctx) {
274
- const raw = ctx.rawBody;
275
- if (!raw || raw.length === 0) return {};
276
- try {
277
- return JSON.parse(raw.toString("utf8"));
278
- } catch {
279
- sendError(ctx.res, 400, ServerErrorCodes.INVALID_JSON, "Request body is not valid JSON");
280
- return void 0;
281
- }
282
- }
283
- function respondError(ctx, err) {
284
- if (ctx.abort.aborted) return;
285
- const { status, body } = toHttpError(err);
286
- sendError(ctx.res, status, body.code, body.message, body.details);
287
- }
288
- function respondJson(ctx, data, status = 200) {
289
- if (ctx.abort.aborted) return;
290
- sendJson(ctx.res, data, status, ctx.abort);
291
- }
292
- function badRequest(res, message, details) {
293
- sendError(res, 400, ServerErrorCodes.INVALID_REQUEST, message, details);
294
- }
295
- function rejectInvalid(ctx, failure) {
296
- if (ctx.abort.aborted) return;
297
- badRequest(ctx.res, failure.message, failure.details);
298
- }
299
- function serializeBatchResult(result) {
300
- return {
301
- succeeded: result.succeeded,
302
- failed: result.failed.map((f) => ({
303
- docId: f.docId,
304
- error: f.error instanceof NarsilError ? serializeNarsilError(f.error) : { code: "INTERNAL_ERROR", message: String(f.error) }
305
- }))
306
- };
307
- }
308
-
309
- // src/server/validation.ts
310
- function isPlainObject(value) {
311
- return typeof value === "object" && value !== null && !Array.isArray(value);
312
- }
313
- function overWindow(field, value, maxWindow) {
314
- if (typeof value === "number" && value > maxWindow) {
315
- return {
316
- message: `Field "${field}" exceeds the maximum result window of ${maxWindow}`,
317
- details: { value, limit: maxWindow }
318
- };
319
- }
320
- return null;
321
- }
322
- function validateQuery(params, maxWindow) {
323
- if (params.group && typeof params.group === "object" && "reduce" in params.group) {
324
- return {
325
- message: 'Custom group reducers are not available over HTTP; use "group.fields" and "group.maxPerGroup" only'
326
- };
327
- }
328
- const rowBound = overWindow("limit", params.limit, maxWindow) ?? overWindow("offset", params.offset, maxWindow);
329
- if (rowBound) return rowBound;
330
- if (params.group && typeof params.group === "object") {
331
- const grouped = overWindow("group.maxPerGroup", params.group.maxPerGroup, maxWindow) ?? overWindow("group.limit", params.group.limit, maxWindow);
332
- if (grouped) return grouped;
333
- }
334
- if (params.facets && typeof params.facets === "object") {
335
- for (const [field, facet] of Object.entries(params.facets)) {
336
- if (facet && typeof facet === "object") {
337
- const faceted = overWindow(`facets.${field}.limit`, facet.limit, maxWindow);
338
- if (faceted) return faceted;
339
- }
340
- }
341
- }
342
- return null;
343
- }
344
- function validateSuggest(params) {
345
- if (typeof params.prefix !== "string") {
346
- return { message: 'Field "prefix" is required and must be a string' };
347
- }
348
- return null;
349
- }
350
- function validateDocumentBody(body) {
351
- if (!isPlainObject(body.document)) {
352
- return { message: 'Field "document" is required and must be an object' };
353
- }
354
- return null;
355
- }
356
- function validateMultiGet(body, maxFetch) {
357
- if (!Array.isArray(body.docIds)) {
358
- return { message: 'Field "docIds" is required and must be an array of strings' };
359
- }
360
- if (body.docIds.length > maxFetch) {
361
- return {
362
- message: `Field "docIds" exceeds the maximum of ${maxFetch} ids per request`,
363
- details: { count: body.docIds.length, limit: maxFetch }
364
- };
365
- }
366
- return null;
367
- }
368
- function validateList(params, maxFetch) {
369
- if (params.cursor !== void 0 && typeof params.cursor !== "string") {
370
- return { message: 'Field "cursor" must be a string' };
371
- }
372
- if (params.limit !== void 0 && typeof params.limit !== "number") {
373
- return { message: 'Field "limit" must be a number' };
374
- }
375
- if (typeof params.limit === "number" && params.limit > maxFetch) {
376
- return {
377
- message: `Field "limit" exceeds the maximum of ${maxFetch} documents per request`,
378
- details: { value: params.limit, limit: maxFetch }
379
- };
380
- }
381
- return validateListSort(params.sort);
382
- }
383
- function validateListSort(sort) {
384
- if (sort === void 0) return null;
385
- if (typeof sort !== "object" || sort === null) {
386
- return { message: 'Field "sort" must be an object keyed by field name, or a list of fields' };
387
- }
388
- const entries = Array.isArray(sort) ? sort.map(
389
- (entry) => typeof entry === "object" && entry !== null ? [entry.field, entry.direction] : [entry, void 0]
390
- ) : Object.entries(sort);
391
- if (entries.length > MAX_SORT_FIELDS) {
392
- return {
393
- message: `Field "sort" exceeds the maximum of ${MAX_SORT_FIELDS} fields`,
394
- details: { count: entries.length, limit: MAX_SORT_FIELDS }
395
- };
396
- }
397
- for (const [field, direction] of entries) {
398
- if (typeof field !== "string" || field.length === 0) {
399
- return { message: 'Every entry of "sort" must name a field', details: { value: field } };
400
- }
401
- if (direction !== "asc" && direction !== "desc") {
402
- return { message: `Field "sort.${field}" must be "asc" or "desc"`, details: { value: direction } };
403
- }
404
- }
405
- return null;
406
- }
407
- function validateBatch(body) {
408
- const action = body.action ?? "insert";
409
- if (action === "insert") {
410
- if (!Array.isArray(body.documents)) return { message: 'Batch insert requires a "documents" array' };
411
- return null;
412
- }
413
- if (action === "update") {
414
- if (!Array.isArray(body.updates)) return { message: 'Batch update requires an "updates" array' };
415
- return null;
416
- }
417
- if (action === "delete") {
418
- if (!Array.isArray(body.docIds)) return { message: 'Batch delete requires a "docIds" array' };
419
- return null;
420
- }
421
- return { message: 'Field "action" must be one of "insert", "update", or "delete"' };
422
- }
423
- function validateCreateIndex(body) {
424
- if (typeof body.name !== "string" || body.name.length === 0) {
425
- return { message: 'Field "name" is required and must be a non-empty string' };
426
- }
427
- if (!isPlainObject(body.config)) {
428
- return { message: 'Field "config" is required and must be an object' };
429
- }
430
- return null;
431
- }
432
- function validateRebalance(body) {
433
- const target = body.targetPartitionCount;
434
- if (typeof target !== "number" || !Number.isInteger(target) || target <= 0) {
435
- return { message: 'Field "targetPartitionCount" is required and must be a positive integer' };
436
- }
437
- return null;
438
- }
439
- var TASK_TYPES = ["optimizeVectors", "rebalance", "restore", "import", "rebuildAnalysis"];
440
- var TASK_STATUSES = ["queued", "running", "succeeded", "failed", "cancelled"];
441
- function parseCountParam(raw, field, minimum) {
442
- if (raw === null) return minimum;
443
- const value = Number(raw);
444
- if (!Number.isInteger(value) || value < minimum) {
445
- return { message: `Query parameter "${field}" must be an integer of ${minimum} or more`, details: { value: raw } };
446
- }
447
- return value;
448
- }
449
- function parseEnumParam(raw, field, allowed) {
450
- if (raw === null) return void 0;
451
- const requested = raw.split(",").map((entry) => entry.trim());
452
- const unknown = requested.filter((entry) => !allowed.includes(entry));
453
- if (requested.length === 0 || unknown.length > 0) {
454
- return {
455
- message: `Query parameter "${field}" accepts only ${allowed.join(", ")}`,
456
- details: { unknown }
457
- };
458
- }
459
- return requested;
460
- }
461
- function isFailure(value) {
462
- return isPlainObject(value) && typeof value.message === "string";
463
- }
464
- function parseTaskListQuery(params, maxPageSize) {
465
- const type = parseEnumParam(params.get("type"), "type", TASK_TYPES);
466
- if (isFailure(type)) return { failure: type };
467
- const status = parseEnumParam(params.get("status"), "status", TASK_STATUSES);
468
- if (isFailure(status)) return { failure: status };
469
- const from = parseCountParam(params.get("from"), "from", 0);
470
- if (isFailure(from)) return { failure: from };
471
- const limit = parseCountParam(params.get("limit"), "limit", 1);
472
- if (isFailure(limit)) return { failure: limit };
473
- if (params.get("limit") !== null && limit > maxPageSize) {
474
- return {
475
- failure: {
476
- message: `Query parameter "limit" exceeds the maximum page size of ${maxPageSize}`,
477
- details: { value: limit, limit: maxPageSize }
478
- }
479
- };
480
- }
481
- const indexName = params.get("indexName");
482
- return {
483
- query: {
484
- ...indexName === null ? {} : { indexName },
485
- ...type === void 0 ? {} : { type },
486
- ...status === void 0 ? {} : { status },
487
- from,
488
- ...params.get("limit") === null ? {} : { limit }
489
- }
490
- };
491
- }
11
+ import { MessageChannel } from 'worker_threads';
492
12
 
493
13
  // src/server/handlers/admin.ts
494
14
  function createAdminHandlers(deps) {
@@ -746,7 +266,8 @@ function createClusterHandlers(deps) {
746
266
 
747
267
  // src/server/handlers/documents.ts
748
268
  function createDocumentHandlers(deps) {
749
- const { engine, limits } = deps;
269
+ const { engine } = deps;
270
+ const reads = createDocumentReadHandlers(deps);
750
271
  async function insert(ctx) {
751
272
  const body = parseJson(ctx);
752
273
  if (!body) return;
@@ -762,25 +283,6 @@ function createDocumentHandlers(deps) {
762
283
  respondError(ctx, err);
763
284
  }
764
285
  }
765
- async function get(ctx) {
766
- try {
767
- const document = await engine.get(ctx.params[0], ctx.params[1]);
768
- if (document === void 0) {
769
- respondError(ctx, indexDocNotFound(ctx.params[1]));
770
- return;
771
- }
772
- respondJson(ctx, { document });
773
- } catch (err) {
774
- respondError(ctx, err);
775
- }
776
- }
777
- async function exists(ctx) {
778
- try {
779
- respondJson(ctx, { exists: await engine.has(ctx.params[0], ctx.params[1]) });
780
- } catch (err) {
781
- respondError(ctx, err);
782
- }
783
- }
784
286
  async function put(ctx) {
785
287
  const body = parseJson(ctx);
786
288
  if (!body) return;
@@ -792,10 +294,10 @@ function createDocumentHandlers(deps) {
792
294
  const [name, id] = ctx.params;
793
295
  try {
794
296
  if (await engine.has(name, id)) {
795
- await engine.update(name, id, body.document);
297
+ await engine.update(name, id, body.document, body.options);
796
298
  respondJson(ctx, { id, created: false });
797
299
  } else {
798
- const created = await engine.insert(name, body.document, id);
300
+ const created = await engine.insert(name, body.document, id, body.options);
799
301
  respondJson(ctx, { id: created, created: true }, 201);
800
302
  }
801
303
  } catch (err) {
@@ -811,7 +313,7 @@ function createDocumentHandlers(deps) {
811
313
  return;
812
314
  }
813
315
  try {
814
- await engine.update(ctx.params[0], ctx.params[1], body.document);
316
+ await engine.update(ctx.params[0], ctx.params[1], body.document, body.options);
815
317
  respondJson(ctx, { id: ctx.params[1] });
816
318
  } catch (err) {
817
319
  respondError(ctx, err);
@@ -819,46 +321,16 @@ function createDocumentHandlers(deps) {
819
321
  }
820
322
  async function remove(ctx) {
821
323
  try {
822
- await engine.remove(ctx.params[0], ctx.params[1]);
324
+ await engine.remove(ctx.params[0], ctx.params[1], { wait: ctx.query.get("wait") === "true" });
823
325
  respondJson(ctx, { id: ctx.params[1], removed: true });
824
326
  } catch (err) {
825
327
  respondError(ctx, err);
826
328
  }
827
329
  }
828
- async function count(ctx) {
829
- try {
830
- respondJson(ctx, { count: await engine.countDocuments(ctx.params[0]) });
831
- } catch (err) {
832
- respondError(ctx, err);
833
- }
834
- }
835
- async function multiGet(ctx) {
836
- const body = parseJson(ctx);
837
- if (!body) return;
838
- const failure = validateMultiGet(body, limits.maxFetchDocuments);
839
- if (failure) {
840
- rejectInvalid(ctx, failure);
841
- return;
842
- }
330
+ async function waitForWrites(ctx) {
843
331
  try {
844
- const found = await engine.getMultiple(ctx.params[0], body.docIds);
845
- const documents = {};
846
- for (const [id, doc] of found) documents[id] = doc;
847
- respondJson(ctx, { documents });
848
- } catch (err) {
849
- respondError(ctx, err);
850
- }
851
- }
852
- async function list(ctx) {
853
- const params = parseJson(ctx);
854
- if (!params) return;
855
- const failure = validateList(params, limits.maxFetchDocuments);
856
- if (failure) {
857
- rejectInvalid(ctx, failure);
858
- return;
859
- }
860
- try {
861
- respondJson(ctx, await engine.listDocuments(ctx.params[0], params));
332
+ await engine.waitForWrites(ctx.params[0]);
333
+ respondJson(ctx, { ok: true });
862
334
  } catch (err) {
863
335
  respondError(ctx, err);
864
336
  }
@@ -874,9 +346,12 @@ function createDocumentHandlers(deps) {
874
346
  const action = body.action ?? "insert";
875
347
  try {
876
348
  if (action === "update") {
877
- respondJson(ctx, serializeBatchResult(await engine.updateBatch(ctx.params[0], body.updates ?? [])));
349
+ respondJson(
350
+ ctx,
351
+ serializeBatchResult(await engine.updateBatch(ctx.params[0], body.updates ?? [], body.options))
352
+ );
878
353
  } else if (action === "delete") {
879
- respondJson(ctx, serializeBatchResult(await engine.removeBatch(ctx.params[0], body.docIds ?? [])));
354
+ respondJson(ctx, serializeBatchResult(await engine.removeBatch(ctx.params[0], body.docIds ?? [], body.options)));
880
355
  } else {
881
356
  respondJson(
882
357
  ctx,
@@ -887,10 +362,7 @@ function createDocumentHandlers(deps) {
887
362
  respondError(ctx, err);
888
363
  }
889
364
  }
890
- return { insert, get, exists, put, patch, remove, count, list, multiGet, batch };
891
- }
892
- function indexDocNotFound(docId) {
893
- return new NarsilError(ErrorCodes.DOC_NOT_FOUND, `Document "${docId}" not found`, { docId });
365
+ return { ...reads, insert, put, patch, remove, batch, waitForWrites };
894
366
  }
895
367
 
896
368
  // src/server/handlers/health.ts
@@ -955,7 +427,7 @@ function* iterateNdjson(buffer, maxLineBytes) {
955
427
  function yieldToEventLoop() {
956
428
  return new Promise((resolve) => setImmediate(resolve));
957
429
  }
958
- function isPlainObject2(value) {
430
+ function isPlainObject(value) {
959
431
  return typeof value === "object" && value !== null && !Array.isArray(value);
960
432
  }
961
433
  async function runImport(engine, options) {
@@ -993,7 +465,7 @@ async function runImport(engine, options) {
993
465
  recordFailure({ line: line.lineNumber, code: ServerErrorCodes.INVALID_JSON, message: "Line is not valid JSON" });
994
466
  continue;
995
467
  }
996
- if (!isPlainObject2(parsed)) {
468
+ if (!isPlainObject(parsed)) {
997
469
  recordFailure({
998
470
  line: line.lineNumber,
999
471
  code: ServerErrorCodes.INVALID_REQUEST,
@@ -1245,185 +717,237 @@ function createVersionHandler(deps) {
1245
717
  return { report };
1246
718
  }
1247
719
 
1248
- // src/server/request.ts
1249
- function initAbortHandler(res) {
1250
- const listeners = [];
1251
- let aborted = false;
1252
- res.onAborted(() => {
1253
- aborted = true;
1254
- for (const fn of listeners) fn();
720
+ // src/server/request-threads/captured-response.ts
721
+ var DEFAULT_STATUS = 200;
722
+ var NO_RESPONSE_STATUS = 500;
723
+ function statusOf(line) {
724
+ const parsed = Number.parseInt(line, 10);
725
+ return Number.isFinite(parsed) ? parsed : DEFAULT_STATUS;
726
+ }
727
+ function bodyBytes(body) {
728
+ if (body === void 0) return new Uint8Array(0);
729
+ if (typeof body === "string") return Buffer.from(body, "utf8");
730
+ return body;
731
+ }
732
+ function createCapturingResponse(remoteAddress) {
733
+ let status = DEFAULT_STATUS;
734
+ const headers = [];
735
+ let finished = false;
736
+ let resolveDone = () => void 0;
737
+ const done = new Promise((resolve) => {
738
+ resolveDone = resolve;
1255
739
  });
1256
- return {
1257
- get aborted() {
1258
- return aborted;
740
+ function finish(body) {
741
+ if (finished) return;
742
+ finished = true;
743
+ resolveDone({ status, headers, body });
744
+ }
745
+ const sink = {
746
+ cork(callback) {
747
+ callback();
748
+ return sink;
749
+ },
750
+ writeStatus(line) {
751
+ if (!finished) status = statusOf(line);
752
+ return sink;
753
+ },
754
+ writeHeader(key, value) {
755
+ if (!finished) headers.push([key, value]);
756
+ return sink;
1259
757
  },
1260
- onAbort(fn) {
1261
- if (aborted) fn();
1262
- else listeners.push(fn);
758
+ end(body) {
759
+ finish(bodyBytes(body));
760
+ return sink;
761
+ },
762
+ endWithoutBody() {
763
+ finish(new Uint8Array(0));
764
+ return sink;
765
+ },
766
+ tryEnd(body) {
767
+ finish(body);
768
+ return [true, true];
769
+ },
770
+ getWriteOffset: () => 0,
771
+ onWritable: () => sink,
772
+ onAborted: () => sink,
773
+ onData: () => sink,
774
+ getRemoteAddressAsText: () => new TextEncoder().encode(remoteAddress).buffer
775
+ };
776
+ return {
777
+ sink,
778
+ done,
779
+ finishUnanswered() {
780
+ if (finished) return;
781
+ status = NO_RESPONSE_STATUS;
782
+ finish(new Uint8Array(0));
1263
783
  }
1264
784
  };
1265
785
  }
1266
- function readBody(res, maxBytes, abort) {
1267
- return new Promise((resolve, reject) => {
1268
- if (abort.aborted) {
1269
- reject(new Error("aborted"));
1270
- return;
1271
- }
1272
- let done = false;
1273
- let total = 0;
1274
- const chunks = [];
1275
- abort.onAbort(() => {
1276
- if (done) return;
1277
- done = true;
1278
- reject(new Error("aborted"));
1279
- });
1280
- res.onData((chunk, isLast) => {
1281
- if (done || abort.aborted) return;
1282
- total += chunk.byteLength;
1283
- if (total > maxBytes) {
1284
- done = true;
1285
- if (!abort.aborted) {
1286
- sendError(res, 413, ServerErrorCodes.PAYLOAD_TOO_LARGE, "Request body exceeds the configured size limit");
1287
- }
1288
- reject(new Error("payload too large"));
1289
- return;
1290
- }
1291
- chunks.push(Buffer.from(new Uint8Array(chunk)));
1292
- if (isLast) {
1293
- done = true;
1294
- resolve(Buffer.concat(chunks));
1295
- }
1296
- });
1297
- });
786
+ function toRelayedResponse(requestId, captured) {
787
+ return { type: "response", requestId, status: captured.status, headers: captured.headers, body: captured.body };
1298
788
  }
1299
- var ConcurrencyGate = class {
1300
- constructor(max) {
1301
- this.max = max;
1302
- }
1303
- inFlight = 0;
1304
- tryAcquire() {
1305
- if (this.max <= 0) return true;
1306
- if (this.inFlight >= this.max) return false;
1307
- this.inFlight++;
1308
- return true;
1309
- }
1310
- release() {
1311
- if (this.max <= 0) return;
1312
- if (this.inFlight > 0) this.inFlight--;
1313
- }
1314
- };
1315
- function decodePathParameter(raw) {
1316
- if (!raw.includes("%")) return raw;
789
+
790
+ // src/server/request-threads/host.ts
791
+ var NO_ABORT = { aborted: false, onAbort: () => void 0 };
792
+ function isRelayRequest(message) {
793
+ if (typeof message !== "object" || message === null) return false;
794
+ const { type, requestId } = message;
795
+ if (type === "touch") return true;
796
+ return typeof requestId === "number" && (type === "request" || type === "authorize" || type === "embed");
797
+ }
798
+ async function answerRequest(handlers, request) {
799
+ const captured = createCapturingResponse(request.hookContext?.remoteAddress ?? "");
800
+ const ctx = {
801
+ res: captured.sink,
802
+ params: request.params,
803
+ query: new URLSearchParams(request.query),
804
+ contentType: request.contentType,
805
+ rawBody: request.rawBody === null ? null : Buffer.from(request.rawBody),
806
+ abort: NO_ABORT,
807
+ hookContext: request.hookContext
808
+ };
1317
809
  try {
1318
- return decodeURIComponent(raw);
810
+ await handlers[request.route](ctx);
1319
811
  } catch {
1320
- return null;
812
+ sendError(ctx.res, 500, ServerErrorCodes.INTERNAL_ERROR, "An unexpected error occurred");
1321
813
  }
814
+ captured.finishUnanswered();
815
+ return toRelayedResponse(request.requestId, await captured.done);
1322
816
  }
1323
- function isDenial(value) {
1324
- return typeof value === "object" && value !== null && "status" in value;
817
+ async function answerAuthorize(authorize, request) {
818
+ if (authorize === void 0) {
819
+ return { type: "authorized", requestId: request.requestId, denial: null, hookError: false };
820
+ }
821
+ const authorization = await authorize(request.context);
822
+ if (authorization.allowed) {
823
+ return { type: "authorized", requestId: request.requestId, denial: null, hookError: false };
824
+ }
825
+ return {
826
+ type: "authorized",
827
+ requestId: request.requestId,
828
+ denial: authorization.denial,
829
+ hookError: authorization.denial === null
830
+ };
1325
831
  }
1326
- async function runHook(res, ctx, hook) {
832
+ async function answerEmbed(core, request) {
1327
833
  try {
1328
- const result = await hook(ctx);
1329
- if (isDenial(result)) {
1330
- sendError(res, result.status, result.code, result.message);
1331
- return false;
834
+ const entry = core.requireIndex(request.indexName);
835
+ const resolved = await resolveVectorText(
836
+ { vector: request.vector },
837
+ entry.embeddingAdapter,
838
+ core.abortController.signal,
839
+ entry.embeddingAdapterName
840
+ );
841
+ const value = resolved.vector?.value;
842
+ return {
843
+ type: "embedded",
844
+ requestId: request.requestId,
845
+ value: value === void 0 ? null : Array.from(value),
846
+ error: null
847
+ };
848
+ } catch (err) {
849
+ const error = err instanceof NarsilError ? serializeNarsilError(err) : { code: "INTERNAL_ERROR", message: err instanceof Error ? err.message : String(err) };
850
+ return { type: "embedded", requestId: request.requestId, value: null, error };
851
+ }
852
+ }
853
+ function touchIntervalFor(copyIdleTimeoutMs) {
854
+ return Math.min(MAX_INDEX_TOUCH_INTERVAL_MS, Math.floor(copyIdleTimeoutMs / INDEX_TOUCH_INTERVALS_PER_IDLE_TIMEOUT));
855
+ }
856
+ async function startRequestThreads(options) {
857
+ const found = acceptorOf(options.app);
858
+ if (found === null) return null;
859
+ const acceptor = found;
860
+ const { core, handlers, authorize } = options;
861
+ const searchHooks = core.pluginRegistry.hasHooks("beforeSearch") || core.pluginRegistry.hasHooks("afterSearch");
862
+ const touchIntervalMs = touchIntervalFor(core.orchestrator.copyIdleTimeoutMs());
863
+ const threads = /* @__PURE__ */ new Map();
864
+ let stopped = false;
865
+ function relayFrom(port) {
866
+ port.on("message", (message) => {
867
+ if (!isRelayRequest(message)) return;
868
+ if (message.type === "touch") {
869
+ core.orchestrator.noteAccess(message.indexName);
870
+ return;
871
+ }
872
+ const reply = message.type === "request" ? answerRequest(handlers, message) : message.type === "authorize" ? answerAuthorize(authorize, message) : answerEmbed(core, message);
873
+ void reply.then((answer) => port.postMessage(answer));
874
+ });
875
+ }
876
+ async function stopThread(workerId, thread) {
877
+ try {
878
+ acceptor.removeChildAppDescriptor(thread.descriptor);
879
+ } catch {
1332
880
  }
1333
- return true;
1334
- } catch {
1335
- sendError(res, 500, ServerErrorCodes.HOOK_ERROR, "The onRequest hook threw an error");
1336
- return false;
881
+ thread.port.close();
882
+ clearGateSlot(options.gate, gateSlotOfWorker(workerId));
883
+ await thread.executor.execute({ type: "stopServing", requestId: `stop-serving-${workerId}` }).catch(() => void 0);
1337
884
  }
1338
- }
1339
- function createRouteRunner(deps) {
1340
- const gate = new ConcurrencyGate(deps.maxConcurrentRequests);
1341
- const { onRequest, writeCors } = deps;
1342
- return (handler, opts) => {
1343
- const paramCount = opts.paramCount ?? 0;
1344
- const needsBody = opts.needsBody ?? false;
1345
- const useHook = Boolean(onRequest) && !opts.skipHooks;
1346
- const gated = !opts.skipHooks;
1347
- return (res, req) => {
1348
- const params = [];
1349
- let malformed = null;
1350
- for (let i = 0; i < paramCount; i++) {
1351
- const decoded = decodePathParameter(req.getParameter(i) ?? "");
1352
- if (decoded === null) {
1353
- malformed = req.getParameter(i) ?? "";
1354
- break;
1355
- }
1356
- params.push(decoded);
1357
- }
1358
- const query = new URLSearchParams(req.getQuery() ?? "");
1359
- const contentType = req.getHeader("content-type");
1360
- let hookCtx = null;
1361
- if (useHook) {
1362
- const headers = {};
1363
- req.forEach((key, value) => {
1364
- headers[key] = value;
1365
- });
1366
- hookCtx = {
1367
- method: req.getMethod(),
1368
- path: req.getUrl(),
1369
- headers,
1370
- remoteAddress: Buffer.from(res.getRemoteAddressAsText()).toString()
1371
- };
1372
- }
1373
- if (writeCors) writeCors(res, req);
1374
- const abort = initAbortHandler(res);
1375
- if (malformed !== null) {
1376
- sendError(
1377
- res,
1378
- 400,
1379
- ServerErrorCodes.INVALID_REQUEST,
1380
- "A path segment holds a percent-escape the server cannot decode",
1381
- { segment: malformed }
885
+ const listener = {
886
+ async onWorkerReady(workerId, executor) {
887
+ if (stopped) return;
888
+ const channel = new MessageChannel();
889
+ relayFrom(channel.port2);
890
+ let result;
891
+ try {
892
+ result = await executor.execute(
893
+ {
894
+ type: "serveRequests",
895
+ requestId: `serve-${workerId}`,
896
+ settings: {
897
+ port: channel.port1,
898
+ gate: options.gate,
899
+ gateSlot: gateSlotOfWorker(workerId),
900
+ maxConcurrentRequests: options.limits.maxConcurrentRequests,
901
+ touchIntervalMs,
902
+ cors: options.cors,
903
+ limits: options.limits,
904
+ build: options.build,
905
+ authorizes: authorize !== void 0,
906
+ searchHooks
907
+ }
908
+ },
909
+ [channel.port1]
1382
910
  );
911
+ } catch (err) {
912
+ channel.port2.close();
913
+ console.warn(`Worker ${workerId} could not start taking requests:`, err);
1383
914
  return;
1384
915
  }
1385
- const bodyPromise = needsBody ? readBody(res, opts.maxBytes, abort) : Promise.resolve(null);
1386
- const hookPromise = hookCtx && onRequest ? runHook(res, hookCtx, onRequest) : Promise.resolve(true);
1387
- Promise.all([bodyPromise, hookPromise]).then(async ([rawBody, allowed]) => {
1388
- if (abort.aborted || !allowed) return;
1389
- if (gated && !gate.tryAcquire()) {
1390
- sendError(res, 429, ServerErrorCodes.TOO_MANY_REQUESTS, "The server is at capacity; retry shortly");
1391
- return;
1392
- }
1393
- try {
1394
- await handler({ res, params, query, contentType, rawBody, abort });
1395
- } catch (err) {
1396
- if (!abort.aborted) {
1397
- sendError(res, 500, ServerErrorCodes.INTERNAL_ERROR, "An unexpected error occurred");
1398
- }
1399
- } finally {
1400
- if (gated) gate.release();
1401
- }
1402
- }).catch(() => {
1403
- });
1404
- };
916
+ const thread = { descriptor: result.descriptor, port: channel.port2, executor };
917
+ if (stopped) {
918
+ await stopThread(workerId, thread);
919
+ return;
920
+ }
921
+ acceptor.addChildAppDescriptor(result.descriptor);
922
+ threads.set(workerId, thread);
923
+ },
924
+ onWorkerGone(workerId) {
925
+ const thread = threads.get(workerId);
926
+ if (thread === void 0) return;
927
+ threads.delete(workerId);
928
+ try {
929
+ acceptor.removeChildAppDescriptor(thread.descriptor);
930
+ } catch {
931
+ }
932
+ thread.port.close();
933
+ clearGateSlot(options.gate, gateSlotOfWorker(workerId));
934
+ }
1405
935
  };
1406
- }
1407
-
1408
- // src/server/runtime.ts
1409
- var cached = null;
1410
- async function loadUWebSockets() {
1411
- if (cached) return cached;
1412
- try {
1413
- const mod = await import('uWebSockets.js');
1414
- const resolved = typeof mod.App === "function" ? mod : mod.default;
1415
- if (!resolved || typeof resolved.App !== "function") {
1416
- throw new Error("uWebSockets.js did not expose an App factory");
936
+ await core.orchestrator.serveRequestsOnWorkers(listener);
937
+ return {
938
+ get threadCount() {
939
+ return threads.size;
940
+ },
941
+ async stop() {
942
+ stopped = true;
943
+ core.orchestrator.stopRequestThreads();
944
+ const stopping = [...threads.entries()].map(([workerId, thread]) => {
945
+ threads.delete(workerId);
946
+ return stopThread(workerId, thread);
947
+ });
948
+ await Promise.all(stopping);
1417
949
  }
1418
- cached = resolved;
1419
- return resolved;
1420
- } catch (err) {
1421
- throw new NarsilError(
1422
- ErrorCodes.CONFIG_INVALID,
1423
- "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).",
1424
- { cause: err instanceof Error ? err.message : String(err) }
1425
- );
1426
- }
950
+ };
1427
951
  }
1428
952
 
1429
953
  // src/server/task-store.ts
@@ -1672,21 +1196,79 @@ function assertSecureBinding(host, options) {
1672
1196
  { host }
1673
1197
  );
1674
1198
  }
1199
+ function mainThreadHandlers(deps) {
1200
+ const idx = createIndexHandlers(deps);
1201
+ const doc = createDocumentHandlers(deps);
1202
+ const search = createSearchHandlers(deps);
1203
+ const admin = createAdminHandlers(deps);
1204
+ const health = createHealthHandlers(deps);
1205
+ const cluster = createClusterHandlers(deps);
1206
+ const version = createVersionHandler(deps);
1207
+ const capabilities = createCapabilitiesHandler();
1208
+ return {
1209
+ livez: health.livez,
1210
+ readyz: health.readyz,
1211
+ version: version.report,
1212
+ capabilities: capabilities.report,
1213
+ clusterTopology: cluster.topology,
1214
+ clusterAllocation: cluster.allocation,
1215
+ memory: admin.memory,
1216
+ listTasks: admin.listTasks,
1217
+ getTask: admin.getTask,
1218
+ cancelTask: admin.cancelTask,
1219
+ createIndex: idx.create,
1220
+ listIndexes: idx.list,
1221
+ dropIndex: idx.drop,
1222
+ indexStats: idx.stats,
1223
+ indexPartitions: idx.partitions,
1224
+ clearIndex: idx.clear,
1225
+ openIndex: idx.open,
1226
+ closeIndex: idx.close,
1227
+ insert: doc.insert,
1228
+ batch: doc.batch,
1229
+ importNdjson: createImportHandler(deps),
1230
+ put: doc.put,
1231
+ patch: doc.patch,
1232
+ remove: doc.remove,
1233
+ waitForWrites: doc.waitForWrites,
1234
+ search: search.search,
1235
+ preflight: search.preflight,
1236
+ suggest: search.suggest,
1237
+ get: doc.get,
1238
+ exists: doc.exists,
1239
+ count: doc.count,
1240
+ list: doc.list,
1241
+ multiGet: doc.multiGet,
1242
+ checkpoint: admin.checkpoint,
1243
+ rebuildAnalysis: admin.rebuildAnalysis,
1244
+ snapshot: admin.snapshot,
1245
+ vectorMaintenance: admin.vectorMaintenance,
1246
+ compact: admin.compact,
1247
+ optimize: admin.optimize,
1248
+ rebalance: admin.rebalance,
1249
+ partitionConfig: admin.partitionConfig,
1250
+ restore: admin.restore
1251
+ };
1252
+ }
1675
1253
  var NarsilHttpServer = class {
1676
1254
  host;
1677
1255
  port;
1678
1256
  options;
1257
+ engine;
1679
1258
  deps;
1680
1259
  uws = null;
1681
1260
  listenSocket = null;
1682
1261
  ready = false;
1262
+ threads = null;
1683
1263
  constructor(engine, options = {}) {
1684
1264
  this.host = options.host ?? "127.0.0.1";
1685
1265
  this.port = options.port ?? 9876;
1686
1266
  this.options = options;
1267
+ this.engine = engine;
1687
1268
  for (const [name, adapter] of Object.entries(options.embeddingAdapters ?? {})) {
1688
1269
  engine.registerEmbeddingAdapter(name, adapter);
1689
1270
  }
1271
+ engineCoreOf(engine)?.orchestrator.shareMainThread();
1690
1272
  const taskStore = options.taskStore ?? new InMemoryTaskStore();
1691
1273
  const instanceId = options.instanceId ?? randomUUID();
1692
1274
  const limits = resolveLimits(options.limits);
@@ -1704,7 +1286,7 @@ var NarsilHttpServer = class {
1704
1286
  const uws = await loadUWebSockets();
1705
1287
  this.uws = uws;
1706
1288
  const app = uws.App();
1707
- this.registerRoutes(app);
1289
+ await this.registerRoutes(app);
1708
1290
  await new Promise((resolve, reject) => {
1709
1291
  app.listen(this.host, this.port, (socket) => {
1710
1292
  if (socket) {
@@ -1724,121 +1306,43 @@ var NarsilHttpServer = class {
1724
1306
  this.uws.us_listen_socket_close(this.listenSocket);
1725
1307
  this.listenSocket = null;
1726
1308
  }
1309
+ if (this.threads !== null) {
1310
+ const threads = this.threads;
1311
+ this.threads = null;
1312
+ await threads.stop();
1313
+ }
1727
1314
  }
1728
1315
  get listeningPort() {
1729
1316
  if (!this.listenSocket || !this.uws) return -1;
1730
1317
  return this.uws.us_socket_local_port(this.listenSocket);
1731
1318
  }
1732
- registerRoutes(app) {
1319
+ get requestThreadCount() {
1320
+ return this.threads?.threadCount ?? 0;
1321
+ }
1322
+ async registerRoutes(app) {
1733
1323
  const cors = resolveCors(this.options.cors);
1324
+ const authorize = this.options.onRequest ? authorizerFor(this.options.onRequest) : void 0;
1325
+ const core = engineCoreOf(this.engine);
1326
+ const threadCount = core === void 0 ? 0 : core.orchestrator.requestThreadCount();
1327
+ const gate = createSharedGateBuffer(gateSlotOfWorker(threadCount));
1328
+ const { limits, build } = this.deps;
1329
+ const handlers = mainThreadHandlers(this.deps);
1734
1330
  const run = createRouteRunner({
1735
- onRequest: this.options.onRequest,
1736
- maxConcurrentRequests: this.deps.limits.maxConcurrentRequests,
1331
+ authorize,
1332
+ gate: createSharedRequestGate(gate, limits.maxConcurrentRequests, MAIN_THREAD_GATE_SLOT),
1737
1333
  writeCors: cors ? corsWriter(cors) : void 0
1738
1334
  });
1739
- const { maxBodyBytes, maxImportBytes } = this.deps.limits;
1740
- const idx = createIndexHandlers(this.deps);
1741
- const doc = createDocumentHandlers(this.deps);
1742
- const search = createSearchHandlers(this.deps);
1743
- const admin = createAdminHandlers(this.deps);
1744
- const health = createHealthHandlers(this.deps);
1745
- const cluster = createClusterHandlers(this.deps);
1746
- const version = createVersionHandler(this.deps);
1747
- const capabilities = createCapabilitiesHandler();
1748
- const importNdjson = createImportHandler(this.deps);
1749
- const json = (handler, opts) => run(handler, opts);
1750
- if (cors) {
1751
- app.options("/*", (res, req) => {
1752
- const origin = req.getHeader("origin");
1753
- res.cork(() => {
1754
- res.writeStatus("204 No Content");
1755
- writeCorsOrigin(res, cors, origin);
1756
- res.writeHeader("Access-Control-Allow-Methods", cors.methods).writeHeader("Access-Control-Allow-Headers", cors.headers).writeHeader("Access-Control-Max-Age", "86400").endWithoutBody();
1757
- });
1758
- });
1335
+ registerServerRoutes(app, run, handlers, limits, cors);
1336
+ if (core === void 0 || !detectRuntime().supportsWorkerThreads) return;
1337
+ try {
1338
+ this.threads = await startRequestThreads({ core, app, handlers, authorize, gate, limits, build, cors });
1339
+ } catch (err) {
1340
+ console.warn("The server could not start request threads, so the main thread answers every request:", err);
1759
1341
  }
1760
- app.get("/livez", json(health.livez, { maxBytes: 0, skipHooks: true }));
1761
- app.get("/readyz", json(health.readyz, { maxBytes: 0, skipHooks: true }));
1762
- app.get("/health", json(health.livez, { maxBytes: 0, skipHooks: true }));
1763
- app.get("/version", json(version.report, { maxBytes: 0, skipHooks: true }));
1764
- app.get("/capabilities", json(capabilities.report, { maxBytes: 0, skipHooks: true }));
1765
- app.get("/cluster", json(cluster.topology, { maxBytes: 0 }));
1766
- app.get("/stats/memory", json(admin.memory, { maxBytes: 0 }));
1767
- app.get("/tasks", json(admin.listTasks, { maxBytes: 0 }));
1768
- app.get("/tasks/:id", json(admin.getTask, { paramCount: 1, maxBytes: 0 }));
1769
- app.post("/tasks/:id/_cancel", json(admin.cancelTask, { paramCount: 1, maxBytes: 0 }));
1770
- app.post("/indexes", json(idx.create, { needsBody: true, maxBytes: maxBodyBytes }));
1771
- app.get("/indexes", json(idx.list, { maxBytes: 0 }));
1772
- app.del("/indexes/:name", json(idx.drop, { paramCount: 1, maxBytes: 0 }));
1773
- app.get("/indexes/:name/stats", json(idx.stats, { paramCount: 1, maxBytes: 0 }));
1774
- app.get("/indexes/:name/partitions", json(idx.partitions, { paramCount: 1, maxBytes: 0 }));
1775
- app.get("/indexes/:name/cluster", json(cluster.allocation, { paramCount: 1, maxBytes: 0 }));
1776
- app.post("/indexes/:name/_clear", json(idx.clear, { paramCount: 1, maxBytes: 0 }));
1777
- app.post("/indexes/:name/_open", json(idx.open, { paramCount: 1, maxBytes: 0 }));
1778
- app.post("/indexes/:name/_close", json(idx.close, { paramCount: 1, maxBytes: 0 }));
1779
- app.get("/indexes/:name/count", json(doc.count, { paramCount: 1, maxBytes: 0 }));
1780
- app.post("/indexes/:name/documents", json(doc.insert, { paramCount: 1, needsBody: true, maxBytes: maxBodyBytes }));
1781
- app.post(
1782
- "/indexes/:name/documents/_batch",
1783
- json(doc.batch, { paramCount: 1, needsBody: true, maxBytes: maxBodyBytes })
1784
- );
1785
- app.post(
1786
- "/indexes/:name/documents/_list",
1787
- json(doc.list, { paramCount: 1, needsBody: true, maxBytes: maxBodyBytes })
1788
- );
1789
- app.post(
1790
- "/indexes/:name/documents/_multi-get",
1791
- json(doc.multiGet, { paramCount: 1, needsBody: true, maxBytes: maxBodyBytes })
1792
- );
1793
- app.post(
1794
- "/indexes/:name/documents/_import",
1795
- json(importNdjson, { paramCount: 1, needsBody: true, maxBytes: maxImportBytes })
1796
- );
1797
- app.get("/indexes/:name/documents/:id", json(doc.get, { paramCount: 2, maxBytes: 0 }));
1798
- app.get("/indexes/:name/documents/:id/_exists", json(doc.exists, { paramCount: 2, maxBytes: 0 }));
1799
- app.put("/indexes/:name/documents/:id", json(doc.put, { paramCount: 2, needsBody: true, maxBytes: maxBodyBytes }));
1800
- app.patch(
1801
- "/indexes/:name/documents/:id",
1802
- json(doc.patch, { paramCount: 2, needsBody: true, maxBytes: maxBodyBytes })
1803
- );
1804
- app.del("/indexes/:name/documents/:id", json(doc.remove, { paramCount: 2, maxBytes: 0 }));
1805
- app.post("/indexes/:name/search", json(search.search, { paramCount: 1, needsBody: true, maxBytes: maxBodyBytes }));
1806
- app.post(
1807
- "/indexes/:name/search/preflight",
1808
- json(search.preflight, { paramCount: 1, needsBody: true, maxBytes: maxBodyBytes })
1809
- );
1810
- app.post("/indexes/:name/suggest", json(search.suggest, { paramCount: 1, needsBody: true, maxBytes: maxBodyBytes }));
1811
- app.post("/indexes/:name/_checkpoint", json(admin.checkpoint, { paramCount: 1, maxBytes: 0 }));
1812
- app.post("/indexes/:name/_rebuild-analysis", json(admin.rebuildAnalysis, { paramCount: 1, maxBytes: 0 }));
1813
- app.get("/indexes/:name/snapshot", json(admin.snapshot, { paramCount: 1, maxBytes: 0 }));
1814
- app.get("/indexes/:name/vector-maintenance", json(admin.vectorMaintenance, { paramCount: 1, maxBytes: 0 }));
1815
- app.post(
1816
- "/indexes/:name/vectors/_compact",
1817
- json(admin.compact, { paramCount: 1, needsBody: true, maxBytes: maxBodyBytes })
1818
- );
1819
- app.post(
1820
- "/indexes/:name/vectors/_optimize",
1821
- json(admin.optimize, { paramCount: 1, needsBody: true, maxBytes: maxBodyBytes })
1822
- );
1823
- app.post(
1824
- "/indexes/:name/_rebalance",
1825
- json(admin.rebalance, { paramCount: 1, needsBody: true, maxBytes: maxBodyBytes })
1826
- );
1827
- app.post(
1828
- "/indexes/:name/partition-config",
1829
- json(admin.partitionConfig, { paramCount: 1, needsBody: true, maxBytes: maxBodyBytes })
1830
- );
1831
- app.post(
1832
- "/indexes/:name/restore",
1833
- json(admin.restore, { paramCount: 1, needsBody: true, maxBytes: maxImportBytes })
1834
- );
1835
- app.any("/*", (res) => {
1836
- sendError(res, 404, ServerErrorCodes.NOT_FOUND, "Route not found");
1837
- });
1838
1342
  }
1839
1343
  };
1840
1344
  function createServer(engine, options) {
1841
1345
  return new NarsilHttpServer(engine, options);
1842
1346
  }
1843
1347
 
1844
- export { ASYNC_IMPORT_CAPABILITY, INDEX_LIFECYCLE_CAPABILITY, InMemoryTaskStore, REBUILD_ANALYSIS_CAPABILITY, SERVER_CAPABILITIES, TASK_CANCEL_CAPABILITY, TASK_FILTER_CAPABILITY, createServer, httpStatusForNarsilError };
1348
+ export { InMemoryTaskStore, createServer };