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