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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +2 -2
  2. package/dist/adapters/indexeddb.mjs +15 -10
  3. package/dist/{options-BC68VPoE.d.ts → capabilities-JrVQOwi4.d.ts} +55 -47
  4. package/dist/{chunk-BFIH7OV5.mjs → chunk-2GWVQWGA.mjs} +3 -1
  5. package/dist/chunk-AV4IJKD2.mjs +35 -0
  6. package/dist/{chunk-EV45L35L.mjs → chunk-BEIK3EOZ.mjs} +845 -2407
  7. package/dist/chunk-CBLS46IE.mjs +10 -0
  8. package/dist/chunk-CMVG2UEF.mjs +2038 -0
  9. package/dist/chunk-CV5H7XL7.mjs +876 -0
  10. package/dist/{chunk-JQUL2T6I.mjs → chunk-GCM3JFVA.mjs} +1 -2
  11. package/dist/{chunk-XWICBGYT.mjs → chunk-MHE7YWAU.mjs} +3 -5
  12. package/dist/{chunk-UZTT2BKQ.mjs → chunk-ND34I3SV.mjs} +1202 -1881
  13. package/dist/{chunk-PM52SMBT.mjs → chunk-OOSIDHA3.mjs} +2 -1
  14. package/dist/chunk-ORBLD3EL.mjs +6 -0
  15. package/dist/chunk-PIWKZPP7.mjs +64 -0
  16. package/dist/{chunk-4COAUBLJ.mjs → chunk-PMBHH44R.mjs} +124 -8
  17. package/dist/chunk-RP23PWMV.mjs +11 -0
  18. package/dist/chunk-UNEEFTEU.mjs +7 -0
  19. package/dist/{chunk-WTONLDU3.mjs → chunk-WYJXSLER.mjs} +9 -19
  20. package/dist/{chunk-DFZDV5FV.mjs → chunk-X6YZO4MQ.mjs} +19 -15
  21. package/dist/chunk-XE6XR2OE.mjs +23 -0
  22. package/dist/chunk-YEO6G2NG.mjs +6 -0
  23. package/dist/chunk-ZHUQKZBE.mjs +1292 -0
  24. package/dist/{client-C5BkUqB8.d.ts → client-CBOVHdjp.d.ts} +31 -7
  25. package/dist/client.d.ts +7 -7
  26. package/dist/client.mjs +250 -232
  27. package/dist/{config-B-ZI-ft5.d.ts → config-Czh2YNDi.d.ts} +2 -2
  28. package/dist/{config-BZRExglL.d.ts → config-OON7IJ_B.d.ts} +50 -13
  29. package/dist/direct-executor-OREXU72C.mjs +12 -0
  30. package/dist/distribution/coordinator/etcd.d.ts +2 -2
  31. package/dist/distribution/coordinator/etcd.mjs +2 -3
  32. package/dist/distribution/coordinator/in-memory.d.ts +2 -2
  33. package/dist/distribution/coordinator/in-memory.mjs +2 -2
  34. package/dist/distribution/transport/grpc.mjs +7 -9
  35. package/dist/distribution/transport/in-memory.mjs +2 -1
  36. package/dist/distribution/transport/tcp.mjs +3 -2
  37. package/dist/distribution.d.ts +8 -8
  38. package/dist/distribution.mjs +142 -101
  39. package/dist/embeddings/openai.mjs +5 -3
  40. package/dist/{engine-Cjr1uBLa.d.ts → engine-9PtFY50q.d.ts} +125 -69
  41. package/dist/index.browser.mjs +4689 -3330
  42. package/dist/index.d.ts +7 -7
  43. package/dist/index.mjs +14 -8
  44. package/dist/invalidation/broadcast-channel.mjs +2 -1
  45. package/dist/invalidation/filesystem.mjs +3 -4
  46. package/dist/{search-DDHjfXhJ.d.ts → memory-Bzd5xcEW.d.ts} +89 -63
  47. package/dist/persistence/durability/checkpoint-worker.d.ts +1 -1
  48. package/dist/persistence/durability/checkpoint-worker.mjs +14 -8
  49. package/dist/react.d.ts +4 -4
  50. package/dist/react.mjs +7 -5
  51. package/dist/{schema-B7juWg8Q.d.ts → schema-BqRx9t4E.d.ts} +22 -3
  52. package/dist/server.d.ts +47 -42
  53. package/dist/server.mjs +338 -821
  54. package/dist/thread-PZLEO6JL.mjs +310 -0
  55. package/dist/{types-DsSSjY4d.d.ts → types-DXPxaXPO.d.ts} +1 -1
  56. package/dist/vector/hnsw-build-worker.mjs +3 -4
  57. package/dist/vector/search-worker.mjs +4 -123
  58. package/dist/workers/entry.mjs +47 -3
  59. package/package.json +5 -1
  60. package/dist/direct-executor-EOTFGXTO.mjs +0 -8
@@ -1,5 +1,5 @@
1
- import { V as VectorMaintenanceResult, M as MemoryStats, B as BatchResult, L as ListParams, g as ListResult, I as IndexInfo, f as IndexStats, P as PartitionStatsResult, Q as QueryParams, e as QueryResult, h as PreflightResult, i as SuggestParams, j as SuggestResult } from './search-DDHjfXhJ.js';
2
- import { P as PartitionConfig, A as AnyDocument, b as InsertOptions } from './schema-B7juWg8Q.js';
1
+ import { P as PartitionConfig, A as AnyDocument, b as InsertOptions, W as WriteOptions } from './schema-BqRx9t4E.js';
2
+ import { V as VectorMaintenanceResult, M as MemoryStats, B as BatchResult, L as ListParams, g as ListResult, I as IndexInfo, f as IndexStats, P as PartitionStatsResult, Q as QueryParams, e as QueryResult, h as PreflightResult, i as SuggestParams, j as SuggestResult } from './memory-Bzd5xcEW.js';
3
3
  import { e as TaskRecord, b as ImportResult, H as HttpIndexConfig, c as TaskListQuery, T as TaskListPage } from './tasks-Bn4R1NPN.js';
4
4
 
5
5
  /**
@@ -248,6 +248,8 @@ interface BulkOperations {
248
248
  * @param indexName - This names the index holding the documents.
249
249
  * @param updates - Each entry names the document to replace and holds its
250
250
  * replacement.
251
+ * @param writeOptions - These per-write settings reach the server, and apply
252
+ * to the whole batch.
251
253
  * @param options - This sets the signal, the deadline, and the headers for
252
254
  * this request.
253
255
  * @returns The result lists the ids replaced, and each failure with its
@@ -256,17 +258,19 @@ interface BulkOperations {
256
258
  updateBatch(indexName: string, updates: Array<{
257
259
  docId: string;
258
260
  document: AnyDocument;
259
- }>, options?: RequestOptions): Promise<BatchResult>;
261
+ }>, writeOptions?: WriteOptions, options?: RequestOptions): Promise<BatchResult>;
260
262
  /**
261
263
  * Removes many documents in one request and reports each one's outcome.
262
264
  *
263
265
  * @param indexName - This names the index holding the documents.
264
266
  * @param docIds - These name the documents to remove.
267
+ * @param writeOptions - These per-write settings reach the server, and apply
268
+ * to the whole batch.
265
269
  * @param options - This sets the signal, the deadline, and the headers for
266
270
  * this request.
267
271
  * @returns The result lists the ids removed, and each failure with its error.
268
272
  */
269
- removeBatch(indexName: string, docIds: string[], options?: RequestOptions): Promise<BatchResult>;
273
+ removeBatch(indexName: string, docIds: string[], writeOptions?: WriteOptions, options?: RequestOptions): Promise<BatchResult>;
270
274
  /**
271
275
  * Reads many documents by id in one request.
272
276
  *
@@ -409,12 +413,14 @@ interface DocumentOperations {
409
413
  * @param indexName - This names the index that receives the document.
410
414
  * @param docId - This is the id to write at.
411
415
  * @param document - Its fields must match the types the schema declares.
416
+ * @param writeOptions - These per-write settings reach the server, such as
417
+ * waiting for every worker copy to apply the write.
412
418
  * @param options - This sets the signal, the deadline, and the headers for
413
419
  * this request.
414
420
  * @returns The result names the id, and says whether the write created the
415
421
  * document or replaced one.
416
422
  */
417
- put(indexName: string, docId: string, document: AnyDocument, options?: RequestOptions): Promise<PutResult>;
423
+ put(indexName: string, docId: string, document: AnyDocument, writeOptions?: WriteOptions, options?: RequestOptions): Promise<PutResult>;
418
424
  /**
419
425
  * Replaces the stored document at an id.
420
426
  *
@@ -422,23 +428,41 @@ interface DocumentOperations {
422
428
  * @param docId - This names the document to replace.
423
429
  * @param document - This replacement goes through the same schema validation
424
430
  * an insert would.
431
+ * @param writeOptions - These per-write settings reach the server, such as
432
+ * waiting for every worker copy to apply the write.
425
433
  * @param options - This sets the signal, the deadline, and the headers for
426
434
  * this request.
427
435
  * @throws A `NarsilError` with `DOC_NOT_FOUND` when the index holds no such
428
436
  * document.
429
437
  */
430
- update(indexName: string, docId: string, document: AnyDocument, options?: RequestOptions): Promise<void>;
438
+ update(indexName: string, docId: string, document: AnyDocument, writeOptions?: WriteOptions, options?: RequestOptions): Promise<void>;
431
439
  /**
432
440
  * Removes one document.
433
441
  *
434
442
  * @param indexName - This names the index holding the document.
435
443
  * @param docId - This names the document to remove.
444
+ * @param writeOptions - These per-write settings reach the server, such as
445
+ * waiting for every worker copy to apply the removal.
436
446
  * @param options - This sets the signal, the deadline, and the headers for
437
447
  * this request.
438
448
  * @throws A `NarsilError` with `DOC_NOT_FOUND` when the index holds no such
439
449
  * document.
440
450
  */
441
- remove(indexName: string, docId: string, options?: RequestOptions): Promise<void>;
451
+ remove(indexName: string, docId: string, writeOptions?: WriteOptions, options?: RequestOptions): Promise<void>;
452
+ /**
453
+ * Resolves once every worker copy of an index on the server has applied
454
+ * every write that returned before the call.
455
+ *
456
+ * A write returns once the server's main copy holds it, and the copies
457
+ * apply it afterwards, so a query can come back without a write that
458
+ * returned before it. Call this after a run of writes, or pass `wait: true`
459
+ * on one write, before a query that has to see them.
460
+ *
461
+ * @param indexName - This names the index whose copies have to catch up.
462
+ * @param options - This sets the signal, the deadline, and the headers for
463
+ * this request.
464
+ */
465
+ waitForWrites(indexName: string, options?: RequestOptions): Promise<void>;
442
466
  /**
443
467
  * Counts the documents in an index.
444
468
  *
package/dist/client.d.ts CHANGED
@@ -1,10 +1,10 @@
1
- export { C as ClientErrorCode, a as ClientErrorCodes, E as ErrorCode, b as ErrorCodes, N as NarsilError, c as NarsilErrorCode, S as ServerErrorCode, d as ServerErrorCodes } from './search-DDHjfXhJ.js';
2
- export { A as ASYNC_IMPORT_CAPABILITY, E as ErrorEnvelope, I as INDEX_LIFECYCLE_CAPABILITY, R as REBUILD_ANALYSIS_CAPABILITY, S as SERVER_CAPABILITIES, T as TASK_CANCEL_CAPABILITY, a as TASK_FILTER_CAPABILITY } from './options-BC68VPoE.js';
1
+ export { C as ClientErrorCode, a as ClientErrorCodes, E as ErrorCode, b as ErrorCodes, N as NarsilError, c as NarsilErrorCode, S as ServerErrorCode, d as ServerErrorCodes } from './memory-Bzd5xcEW.js';
2
+ export { A as ASYNC_IMPORT_CAPABILITY, E as ErrorEnvelope, I as INDEX_LIFECYCLE_CAPABILITY, R as REBUILD_ANALYSIS_CAPABILITY, S as SERVER_CAPABILITIES, T as TASK_CANCEL_CAPABILITY, a as TASK_FILTER_CAPABILITY } from './capabilities-JrVQOwi4.js';
3
3
  export { C as CapabilitiesResponse, a as CreateIndexEmbedding, H as HttpIndexConfig, I as ImportError, b as ImportResult, T as TaskListPage, c as TaskListQuery, d as TaskProgress, e as TaskRecord, f as TaskStatus, g as TaskType } from './tasks-Bn4R1NPN.js';
4
- export { A as AdminOperations, B as BulkOperations, D as DocumentOperations, F as FetchFunction, I as ImportSource, a as IndexOperations, N as NarsilClient, b as NarsilClientOptions, P as PutResult, R as RequestOptions, S as SearchOperations, c as ServerOperations, d as ServerVersion, T as TaskOperations, W as WaitForTaskOptions, e as createNarsilClient } from './client-C5BkUqB8.js';
5
- import './schema-B7juWg8Q.js';
4
+ export { A as AdminOperations, B as BulkOperations, D as DocumentOperations, F as FetchFunction, I as ImportSource, a as IndexOperations, N as NarsilClient, b as NarsilClientOptions, P as PutResult, R as RequestOptions, S as SearchOperations, c as ServerOperations, d as ServerVersion, T as TaskOperations, W as WaitForTaskOptions, e as createNarsilClient } from './client-CBOVHdjp.js';
5
+ import './schema-BqRx9t4E.js';
6
6
  import './adapters-79OIa4xm.js';
7
- import './config-B-ZI-ft5.js';
8
- import './config-BZRExglL.js';
9
- import './types-DsSSjY4d.js';
7
+ import './config-Czh2YNDi.js';
8
+ import './config-OON7IJ_B.js';
9
+ import './types-DXPxaXPO.js';
10
10
  import './types-D5OwhtsQ.js';
package/dist/client.mjs CHANGED
@@ -166,220 +166,10 @@ function encodeJson(value) {
166
166
  return JSON.stringify(value, toPlainNumbers);
167
167
  }
168
168
 
169
- // src/client/http.ts
170
- var DEFAULT_TIMEOUT_MS = 3e4;
169
+ // src/client/constants.ts
170
+ var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
171
171
  var NO_TIMEOUT = 0;
172
- function isRecord(value) {
173
- return typeof value === "object" && value !== null && !Array.isArray(value);
174
- }
175
- function unrefTimer(timer) {
176
- const unrefable = timer;
177
- unrefable.unref?.();
178
- }
179
- function startDeadline(timeoutMs, caller) {
180
- const controller = new AbortController();
181
- let expired = false;
182
- const abortWithCaller = () => controller.abort();
183
- if (caller?.aborted) controller.abort();
184
- else caller?.addEventListener("abort", abortWithCaller);
185
- let timer;
186
- if (timeoutMs > 0 && !controller.signal.aborted) {
187
- timer = setTimeout(() => {
188
- expired = true;
189
- controller.abort();
190
- }, timeoutMs);
191
- unrefTimer(timer);
192
- }
193
- return {
194
- signal: controller.signal,
195
- expired: () => expired,
196
- release: () => {
197
- if (timer !== void 0) clearTimeout(timer);
198
- caller?.removeEventListener("abort", abortWithCaller);
199
- }
200
- };
201
- }
202
- var ResponseTooLargeError = class extends Error {
203
- constructor(limitBytes) {
204
- super(`The answer passed the ${limitBytes} byte ceiling this client reads`);
205
- this.limitBytes = limitBytes;
206
- }
207
- };
208
- async function readStreamCapped(stream, limitBytes) {
209
- const reader = stream.getReader();
210
- const chunks = [];
211
- let total = 0;
212
- try {
213
- for (; ; ) {
214
- const { done, value } = await reader.read();
215
- if (done) break;
216
- total += value.byteLength;
217
- if (total > limitBytes) throw new ResponseTooLargeError(limitBytes);
218
- chunks.push(value);
219
- }
220
- } finally {
221
- reader.releaseLock();
222
- if (total > limitBytes) await stream.cancel().catch(() => void 0);
223
- }
224
- const body = new Uint8Array(total);
225
- let offset = 0;
226
- for (const chunk of chunks) {
227
- body.set(chunk, offset);
228
- offset += chunk.byteLength;
229
- }
230
- return body;
231
- }
232
- async function readAnswer(response, limitBytes, wantsBytes) {
233
- const stream = response.body;
234
- if (limitBytes > 0 && stream !== null && stream !== void 0) {
235
- const body = await readStreamCapped(stream, limitBytes);
236
- return wantsBytes ? { text: "", bytes: body } : { text: new TextDecoder().decode(body), bytes: null };
237
- }
238
- if (wantsBytes) {
239
- const bytes = new Uint8Array(await response.arrayBuffer());
240
- if (limitBytes > 0 && bytes.byteLength > limitBytes) throw new ResponseTooLargeError(limitBytes);
241
- return { text: "", bytes };
242
- }
243
- const text = await response.text();
244
- if (limitBytes > 0 && text.length > limitBytes) throw new ResponseTooLargeError(limitBytes);
245
- return { text, bytes: null };
246
- }
247
- function invalidResponse(message, details) {
248
- return new NarsilError(ClientErrorCodes.CLIENT_INVALID_RESPONSE, message, details);
249
- }
250
- function errorFromBody(status, payload, url) {
251
- const envelope = isRecord(payload) && isRecord(payload.error) ? payload.error : void 0;
252
- if (envelope === void 0 || typeof envelope.code !== "string" || typeof envelope.message !== "string") {
253
- return invalidResponse(`The server answered HTTP ${status} without a Narsil error envelope`, { url, status });
254
- }
255
- const details = isRecord(envelope.details) ? envelope.details : {};
256
- return new NarsilError(envelope.code, envelope.message, { ...details, status });
257
- }
258
- function isAbsoluteUrl(value) {
259
- try {
260
- new URL(value);
261
- return true;
262
- } catch {
263
- return false;
264
- }
265
- }
266
- function normaliseBase(url) {
267
- const trimmed = url.trim();
268
- if (trimmed.length === 0) {
269
- throw new NarsilError(ErrorCodes.CONFIG_INVALID, 'The client needs a server address, and "url" is empty');
270
- }
271
- if (!trimmed.startsWith("/") && !isAbsoluteUrl(trimmed)) {
272
- throw new NarsilError(
273
- ErrorCodes.CONFIG_INVALID,
274
- `The client cannot read "${trimmed}" as a server address; pass an absolute URL or a path starting with "/"`,
275
- { url: trimmed }
276
- );
277
- }
278
- return trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
279
- }
280
- function withQuery(path, query) {
281
- if (query === void 0) return path;
282
- const params = new URLSearchParams();
283
- for (const [key, value] of Object.entries(query)) {
284
- if (value !== void 0) params.set(key, value);
285
- }
286
- const encoded = params.toString();
287
- return encoded.length === 0 ? path : `${path}?${encoded}`;
288
- }
289
- function createTransport(options) {
290
- const base = normaliseBase(options.url);
291
- const send = options.fetch ?? ((input, init) => fetch(input, init));
292
- const baseHeaders = { ...options.headers };
293
- if (options.apiKey !== void 0 && options.apiKey.length > 0) {
294
- baseHeaders.authorization = `Bearer ${options.apiKey}`;
295
- }
296
- function resolveTimeout(spec) {
297
- const perCall = spec.options?.timeoutMs;
298
- if (perCall !== void 0) return perCall;
299
- if (options.timeoutMs !== void 0) return options.timeoutMs;
300
- return spec.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
301
- }
302
- function resolveResponseCeiling(spec) {
303
- const perCall = spec.options?.maxResponseBytes;
304
- if (perCall !== void 0) return perCall;
305
- return options.maxResponseBytes ?? 0;
306
- }
307
- async function exchange(spec) {
308
- const url = `${base}${withQuery(spec.path, spec.query)}`;
309
- const headers = { ...baseHeaders, ...spec.options?.headers };
310
- if (spec.contentType !== void 0) headers["content-type"] = spec.contentType;
311
- const timeoutMs = resolveTimeout(spec);
312
- const deadline = startDeadline(timeoutMs, spec.options?.signal);
313
- try {
314
- const init = { method: spec.method, headers, signal: deadline.signal };
315
- if (spec.body !== void 0) init.body = spec.body;
316
- const response = await send(url, init);
317
- const wantsBytes = spec.binaryAnswer === true && response.ok;
318
- const answer = await readAnswer(response, resolveResponseCeiling(spec), wantsBytes);
319
- return { status: response.status, ...answer };
320
- } catch (err) {
321
- if (err instanceof ResponseTooLargeError) {
322
- throw invalidResponse(`The server at ${url} answered more than this client reads: ${err.message}`, {
323
- url,
324
- maxResponseBytes: err.limitBytes
325
- });
326
- }
327
- if (deadline.expired()) {
328
- throw new NarsilError(
329
- ClientErrorCodes.CLIENT_REQUEST_TIMEOUT,
330
- `The server at ${url} did not answer within ${timeoutMs} ms`,
331
- { url, timeoutMs }
332
- );
333
- }
334
- if (spec.options?.signal?.aborted === true) {
335
- throw new NarsilError(ClientErrorCodes.CLIENT_REQUEST_ABORTED, `The caller cancelled the request to ${url}`, {
336
- url
337
- });
338
- }
339
- throw new NarsilError(
340
- ClientErrorCodes.CLIENT_CONNECTION_FAILED,
341
- `The client could not reach ${url}: ${describeError(err)}`,
342
- { url }
343
- );
344
- } finally {
345
- deadline.release();
346
- }
347
- }
348
- function parse(status, text, path) {
349
- if (text.length === 0) return void 0;
350
- try {
351
- return JSON.parse(text);
352
- } catch {
353
- throw invalidResponse(`The server answered HTTP ${status} with a body that is not JSON`, { url: path, status });
354
- }
355
- }
356
- async function json(spec) {
357
- const { status, text } = await exchange(spec);
358
- const payload = parse(status, text, spec.path);
359
- if (status >= 200 && status < 300) return payload;
360
- throw errorFromBody(status, payload, spec.path);
361
- }
362
- return {
363
- json,
364
- async jsonOrNull(spec, absentCode) {
365
- try {
366
- return await json(spec);
367
- } catch (err) {
368
- if (err instanceof NarsilError && err.code === absentCode) return null;
369
- throw err;
370
- }
371
- },
372
- async binary(spec) {
373
- const { status, text, bytes } = await exchange(spec);
374
- if (bytes !== null) return bytes;
375
- throw errorFromBody(status, parse(status, text, spec.path), spec.path);
376
- },
377
- async probe(spec) {
378
- const { status } = await exchange(spec);
379
- return status;
380
- }
381
- };
382
- }
172
+ var DEFAULT_TASK_POLL_INTERVAL_MS = 250;
383
173
 
384
174
  // src/client/paths.ts
385
175
  function indexPath(indexName) {
@@ -393,7 +183,7 @@ function taskPath(taskId) {
393
183
  }
394
184
 
395
185
  // src/client/response-shape.ts
396
- function isRecord2(value) {
186
+ function isRecord(value) {
397
187
  return typeof value === "object" && value !== null && !Array.isArray(value);
398
188
  }
399
189
  function malformed(path, expected) {
@@ -402,27 +192,27 @@ function malformed(path, expected) {
402
192
  });
403
193
  }
404
194
  function readArray(payload, key, path) {
405
- if (!isRecord2(payload) || !Array.isArray(payload[key])) throw malformed(path, `"${key}" array`);
195
+ if (!isRecord(payload) || !Array.isArray(payload[key])) throw malformed(path, `"${key}" array`);
406
196
  return payload[key];
407
197
  }
408
198
  function readNumber(payload, key, path) {
409
- if (!isRecord2(payload) || typeof payload[key] !== "number") throw malformed(path, `"${key}" number`);
199
+ if (!isRecord(payload) || typeof payload[key] !== "number") throw malformed(path, `"${key}" number`);
410
200
  return payload[key];
411
201
  }
412
202
  function readBoolean(payload, key, path) {
413
- if (!isRecord2(payload) || typeof payload[key] !== "boolean") throw malformed(path, `"${key}" flag`);
203
+ if (!isRecord(payload) || typeof payload[key] !== "boolean") throw malformed(path, `"${key}" flag`);
414
204
  return payload[key];
415
205
  }
416
206
  function readString(payload, key, path) {
417
- if (!isRecord2(payload) || typeof payload[key] !== "string") throw malformed(path, `"${key}" string`);
207
+ if (!isRecord(payload) || typeof payload[key] !== "string") throw malformed(path, `"${key}" string`);
418
208
  return payload[key];
419
209
  }
420
210
  function readObject(payload, key, path) {
421
- if (!isRecord2(payload) || !isRecord2(payload[key])) throw malformed(path, `"${key}" object`);
211
+ if (!isRecord(payload) || !isRecord(payload[key])) throw malformed(path, `"${key}" object`);
422
212
  return payload[key];
423
213
  }
424
214
  function readBody(payload, path) {
425
- if (!isRecord2(payload)) throw malformed(path, "JSON object");
215
+ if (!isRecord(payload)) throw malformed(path, "JSON object");
426
216
  return payload;
427
217
  }
428
218
 
@@ -544,11 +334,19 @@ function createBulkOperations(transport) {
544
334
  options
545
335
  );
546
336
  },
547
- updateBatch(indexName, updates, options) {
548
- return batch(indexName, { action: "update", updates }, options);
337
+ updateBatch(indexName, updates, writeOptions, options) {
338
+ return batch(
339
+ indexName,
340
+ { action: "update", updates, ...writeOptions === void 0 ? {} : { options: writeOptions } },
341
+ options
342
+ );
549
343
  },
550
- removeBatch(indexName, docIds, options) {
551
- return batch(indexName, { action: "delete", docIds }, options);
344
+ removeBatch(indexName, docIds, writeOptions, options) {
345
+ return batch(
346
+ indexName,
347
+ { action: "delete", docIds, ...writeOptions === void 0 ? {} : { options: writeOptions } },
348
+ options
349
+ );
552
350
  },
553
351
  async getMultiple(indexName, docIds, options) {
554
352
  const path = `${indexPath(indexName)}/documents/_multi-get`;
@@ -612,28 +410,36 @@ function createDocumentOperations(transport) {
612
410
  const path = `${documentPath(indexName, docId)}/_exists`;
613
411
  return readBoolean(await transport.json({ method: "GET", path, options }), "exists", path);
614
412
  },
615
- async put(indexName, docId, document, options) {
413
+ async put(indexName, docId, document, writeOptions, options) {
616
414
  const path = documentPath(indexName, docId);
617
415
  const payload = await transport.json({
618
416
  method: "PUT",
619
417
  path,
620
- body: encodeJson({ document }),
418
+ body: encodeJson({ document, ...writeOptions === void 0 ? {} : { options: writeOptions } }),
621
419
  contentType: "application/json",
622
420
  options
623
421
  });
624
422
  return { id: readString(payload, "id", path), created: readBoolean(payload, "created", path) };
625
423
  },
626
- async update(indexName, docId, document, options) {
424
+ async update(indexName, docId, document, writeOptions, options) {
627
425
  await transport.json({
628
426
  method: "PATCH",
629
427
  path: documentPath(indexName, docId),
630
- body: encodeJson({ document }),
428
+ body: encodeJson({ document, ...writeOptions === void 0 ? {} : { options: writeOptions } }),
631
429
  contentType: "application/json",
632
430
  options
633
431
  });
634
432
  },
635
- async remove(indexName, docId, options) {
636
- await transport.json({ method: "DELETE", path: documentPath(indexName, docId), options });
433
+ async remove(indexName, docId, writeOptions, options) {
434
+ await transport.json({
435
+ method: "DELETE",
436
+ path: documentPath(indexName, docId),
437
+ ...writeOptions?.wait === true ? { query: { wait: "true" } } : {},
438
+ options
439
+ });
440
+ },
441
+ async waitForWrites(indexName, options) {
442
+ await transport.json({ method: "POST", path: `${indexPath(indexName)}/_wait-for-writes`, options });
637
443
  },
638
444
  async countDocuments(indexName, options) {
639
445
  const path = `${indexPath(indexName)}/count`;
@@ -642,6 +448,219 @@ function createDocumentOperations(transport) {
642
448
  };
643
449
  }
644
450
 
451
+ // src/client/http.ts
452
+ function isRecord2(value) {
453
+ return typeof value === "object" && value !== null && !Array.isArray(value);
454
+ }
455
+ function unrefTimer(timer) {
456
+ const unrefable = timer;
457
+ unrefable.unref?.();
458
+ }
459
+ function startDeadline(timeoutMs, caller) {
460
+ const controller = new AbortController();
461
+ let expired = false;
462
+ const abortWithCaller = () => controller.abort();
463
+ if (caller?.aborted) controller.abort();
464
+ else caller?.addEventListener("abort", abortWithCaller);
465
+ let timer;
466
+ if (timeoutMs > 0 && !controller.signal.aborted) {
467
+ timer = setTimeout(() => {
468
+ expired = true;
469
+ controller.abort();
470
+ }, timeoutMs);
471
+ unrefTimer(timer);
472
+ }
473
+ return {
474
+ signal: controller.signal,
475
+ expired: () => expired,
476
+ release: () => {
477
+ if (timer !== void 0) clearTimeout(timer);
478
+ caller?.removeEventListener("abort", abortWithCaller);
479
+ }
480
+ };
481
+ }
482
+ var ResponseTooLargeError = class extends Error {
483
+ constructor(limitBytes) {
484
+ super(`The answer passed the ${limitBytes} byte ceiling this client reads`);
485
+ this.limitBytes = limitBytes;
486
+ }
487
+ };
488
+ async function readStreamCapped(stream, limitBytes) {
489
+ const reader = stream.getReader();
490
+ const chunks = [];
491
+ let total = 0;
492
+ try {
493
+ for (; ; ) {
494
+ const { done, value } = await reader.read();
495
+ if (done) break;
496
+ total += value.byteLength;
497
+ if (total > limitBytes) throw new ResponseTooLargeError(limitBytes);
498
+ chunks.push(value);
499
+ }
500
+ } finally {
501
+ reader.releaseLock();
502
+ if (total > limitBytes) await stream.cancel().catch(() => void 0);
503
+ }
504
+ const body = new Uint8Array(total);
505
+ let offset = 0;
506
+ for (const chunk of chunks) {
507
+ body.set(chunk, offset);
508
+ offset += chunk.byteLength;
509
+ }
510
+ return body;
511
+ }
512
+ async function readAnswer(response, limitBytes, wantsBytes) {
513
+ const stream = response.body;
514
+ if (limitBytes > 0 && stream !== null && stream !== void 0) {
515
+ const body = await readStreamCapped(stream, limitBytes);
516
+ return wantsBytes ? { text: "", bytes: body } : { text: new TextDecoder().decode(body), bytes: null };
517
+ }
518
+ if (wantsBytes) {
519
+ const bytes = new Uint8Array(await response.arrayBuffer());
520
+ if (limitBytes > 0 && bytes.byteLength > limitBytes) throw new ResponseTooLargeError(limitBytes);
521
+ return { text: "", bytes };
522
+ }
523
+ const text = await response.text();
524
+ if (limitBytes > 0 && text.length > limitBytes) throw new ResponseTooLargeError(limitBytes);
525
+ return { text, bytes: null };
526
+ }
527
+ function invalidResponse(message, details) {
528
+ return new NarsilError(ClientErrorCodes.CLIENT_INVALID_RESPONSE, message, details);
529
+ }
530
+ function errorFromBody(status, payload, url) {
531
+ const envelope = isRecord2(payload) && isRecord2(payload.error) ? payload.error : void 0;
532
+ if (envelope === void 0 || typeof envelope.code !== "string" || typeof envelope.message !== "string") {
533
+ return invalidResponse(`The server answered HTTP ${status} without a Narsil error envelope`, { url, status });
534
+ }
535
+ const details = isRecord2(envelope.details) ? envelope.details : {};
536
+ return new NarsilError(envelope.code, envelope.message, { ...details, status });
537
+ }
538
+ function isAbsoluteUrl(value) {
539
+ try {
540
+ new URL(value);
541
+ return true;
542
+ } catch {
543
+ return false;
544
+ }
545
+ }
546
+ function normaliseBase(url) {
547
+ const trimmed = url.trim();
548
+ if (trimmed.length === 0) {
549
+ throw new NarsilError(ErrorCodes.CONFIG_INVALID, 'The client needs a server address, and "url" is empty');
550
+ }
551
+ if (!trimmed.startsWith("/") && !isAbsoluteUrl(trimmed)) {
552
+ throw new NarsilError(
553
+ ErrorCodes.CONFIG_INVALID,
554
+ `The client cannot read "${trimmed}" as a server address; pass an absolute URL or a path starting with "/"`,
555
+ { url: trimmed }
556
+ );
557
+ }
558
+ return trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
559
+ }
560
+ function withQuery(path, query) {
561
+ if (query === void 0) return path;
562
+ const params = new URLSearchParams();
563
+ for (const [key, value] of Object.entries(query)) {
564
+ if (value !== void 0) params.set(key, value);
565
+ }
566
+ const encoded = params.toString();
567
+ return encoded.length === 0 ? path : `${path}?${encoded}`;
568
+ }
569
+ function createTransport(options) {
570
+ const base = normaliseBase(options.url);
571
+ const send = options.fetch ?? ((input, init) => fetch(input, init));
572
+ const baseHeaders = { ...options.headers };
573
+ if (options.apiKey !== void 0 && options.apiKey.length > 0) {
574
+ baseHeaders.authorization = `Bearer ${options.apiKey}`;
575
+ }
576
+ function resolveTimeout(spec) {
577
+ const perCall = spec.options?.timeoutMs;
578
+ if (perCall !== void 0) return perCall;
579
+ if (options.timeoutMs !== void 0) return options.timeoutMs;
580
+ return spec.defaultTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
581
+ }
582
+ function resolveResponseCeiling(spec) {
583
+ const perCall = spec.options?.maxResponseBytes;
584
+ if (perCall !== void 0) return perCall;
585
+ return options.maxResponseBytes ?? 0;
586
+ }
587
+ async function exchange(spec) {
588
+ const url = `${base}${withQuery(spec.path, spec.query)}`;
589
+ const headers = { ...baseHeaders, ...spec.options?.headers };
590
+ if (spec.contentType !== void 0) headers["content-type"] = spec.contentType;
591
+ const timeoutMs = resolveTimeout(spec);
592
+ const deadline = startDeadline(timeoutMs, spec.options?.signal);
593
+ try {
594
+ const init = { method: spec.method, headers, signal: deadline.signal };
595
+ if (spec.body !== void 0) init.body = spec.body;
596
+ const response = await send(url, init);
597
+ const wantsBytes = spec.binaryAnswer === true && response.ok;
598
+ const answer = await readAnswer(response, resolveResponseCeiling(spec), wantsBytes);
599
+ return { status: response.status, ...answer };
600
+ } catch (err) {
601
+ if (err instanceof ResponseTooLargeError) {
602
+ throw invalidResponse(`The server at ${url} answered more than this client reads: ${err.message}`, {
603
+ url,
604
+ maxResponseBytes: err.limitBytes
605
+ });
606
+ }
607
+ if (deadline.expired()) {
608
+ throw new NarsilError(
609
+ ClientErrorCodes.CLIENT_REQUEST_TIMEOUT,
610
+ `The server at ${url} did not answer within ${timeoutMs} ms`,
611
+ { url, timeoutMs }
612
+ );
613
+ }
614
+ if (spec.options?.signal?.aborted === true) {
615
+ throw new NarsilError(ClientErrorCodes.CLIENT_REQUEST_ABORTED, `The caller cancelled the request to ${url}`, {
616
+ url
617
+ });
618
+ }
619
+ throw new NarsilError(
620
+ ClientErrorCodes.CLIENT_CONNECTION_FAILED,
621
+ `The client could not reach ${url}: ${describeError(err)}`,
622
+ { url }
623
+ );
624
+ } finally {
625
+ deadline.release();
626
+ }
627
+ }
628
+ function parse(status, text, path) {
629
+ if (text.length === 0) return void 0;
630
+ try {
631
+ return JSON.parse(text);
632
+ } catch {
633
+ throw invalidResponse(`The server answered HTTP ${status} with a body that is not JSON`, { url: path, status });
634
+ }
635
+ }
636
+ async function json(spec) {
637
+ const { status, text } = await exchange(spec);
638
+ const payload = parse(status, text, spec.path);
639
+ if (status >= 200 && status < 300) return payload;
640
+ throw errorFromBody(status, payload, spec.path);
641
+ }
642
+ return {
643
+ json,
644
+ async jsonOrNull(spec, absentCode) {
645
+ try {
646
+ return await json(spec);
647
+ } catch (err) {
648
+ if (err instanceof NarsilError && err.code === absentCode) return null;
649
+ throw err;
650
+ }
651
+ },
652
+ async binary(spec) {
653
+ const { status, text, bytes } = await exchange(spec);
654
+ if (bytes !== null) return bytes;
655
+ throw errorFromBody(status, parse(status, text, spec.path), spec.path);
656
+ },
657
+ async probe(spec) {
658
+ const { status } = await exchange(spec);
659
+ return status;
660
+ }
661
+ };
662
+ }
663
+
645
664
  // src/client/indexes.ts
646
665
  function createIndexOperations(transport) {
647
666
  return {
@@ -746,7 +765,6 @@ function createServerOperations(transport) {
746
765
  }
747
766
 
748
767
  // src/client/tasks.ts
749
- var DEFAULT_POLL_INTERVAL_MS = 250;
750
768
  function isTerminal(record) {
751
769
  return record.status === "succeeded" || record.status === "failed" || record.status === "cancelled";
752
770
  }
@@ -794,7 +812,7 @@ function createTaskOperations(transport) {
794
812
  return readBody(await transport.json({ method: "POST", path, options }), path);
795
813
  },
796
814
  async waitForTask(taskId, options) {
797
- const interval = options?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
815
+ const interval = options?.pollIntervalMs ?? DEFAULT_TASK_POLL_INTERVAL_MS;
798
816
  const waitTimeoutMs = options?.waitTimeoutMs ?? 0;
799
817
  const startedAt = Date.now();
800
818
  let reported = null;