@delali/narsil 0.3.1-next.69 → 0.3.1-next.71

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 (46) hide show
  1. package/dist/{capabilities-B1wz2Zl0.d.ts → capabilities-Dt-7B4Wz.d.ts} +2 -2
  2. package/dist/{chunk-DD5X23E6.mjs → chunk-2STVUTNA.mjs} +1 -1
  3. package/dist/chunk-3JGWULDF.mjs +256 -0
  4. package/dist/{chunk-6TGZG2TV.mjs → chunk-653BNMAF.mjs} +190 -58
  5. package/dist/{chunk-YA52AC2V.mjs → chunk-FVLQ63B7.mjs} +203 -9
  6. package/dist/{chunk-5JLSTECC.mjs → chunk-GIVAGD3Q.mjs} +1 -1
  7. package/dist/{chunk-AV4IJKD2.mjs → chunk-HWQLTLNG.mjs} +18 -1
  8. package/dist/{chunk-6IT3MH5M.mjs → chunk-IOR2K26O.mjs} +178 -238
  9. package/dist/{chunk-4VU5TWCR.mjs → chunk-LMO5TJUS.mjs} +40 -35
  10. package/dist/{chunk-BEWOA4VV.mjs → chunk-M76V3OGJ.mjs} +508 -401
  11. package/dist/{chunk-YMZCMH5M.mjs → chunk-OI42GWLR.mjs} +17 -1
  12. package/dist/{chunk-2BDG2IWP.mjs → chunk-P4DSQ6M4.mjs} +16 -6
  13. package/dist/{chunk-THYRHHO2.mjs → chunk-YJDQ7NLA.mjs} +1 -1
  14. package/dist/{client-xVrOCYTW.d.ts → client-COek5GXK.d.ts} +3 -3
  15. package/dist/client.d.ts +8 -8
  16. package/dist/{config-CEeSV_Xo.d.ts → config-Dfanyqqt.d.ts} +2 -2
  17. package/dist/{config-CSvCVtB7.d.ts → config-rBpbgTw8.d.ts} +2 -2
  18. package/dist/direct-executor-GYJEKNV5.mjs +12 -0
  19. package/dist/distribution/coordinator/etcd.d.ts +2 -2
  20. package/dist/distribution/coordinator/etcd.mjs +1 -1
  21. package/dist/distribution/coordinator/in-memory.d.ts +2 -2
  22. package/dist/distribution/coordinator/in-memory.mjs +1 -1
  23. package/dist/distribution.d.ts +8 -8
  24. package/dist/distribution.mjs +109 -69
  25. package/dist/{engine-77ErLy8P.d.ts → engine-igk0aMpQ.d.ts} +109 -60
  26. package/dist/index.browser.mjs +1167 -749
  27. package/dist/index.d.ts +7 -7
  28. package/dist/index.mjs +10 -10
  29. package/dist/invalidation/filesystem.mjs +1 -1
  30. package/dist/{memory-CKF-9WLP.d.ts → memory-C7UMCMH3.d.ts} +35 -17
  31. package/dist/persistence/durability/checkpoint-worker.d.ts +2 -1
  32. package/dist/persistence/durability/checkpoint-worker.mjs +10 -10
  33. package/dist/react.d.ts +277 -182
  34. package/dist/react.mjs +189 -45
  35. package/dist/{schema-d5Afu03A.d.ts → schema-Bgzr4g7b.d.ts} +62 -10
  36. package/dist/server.d.ts +10 -10
  37. package/dist/server.mjs +11 -7
  38. package/dist/{tasks-BEiNxDRs.d.ts → tasks-BZt_zes6.d.ts} +3 -1
  39. package/dist/{thread-FLP45EUD.mjs → thread-JEVAYETR.mjs} +6 -6
  40. package/dist/{types-DAK7mnUZ.d.ts → types-gOUqhs2F.d.ts} +1 -1
  41. package/dist/vector/search-worker.d.ts +1 -1
  42. package/dist/vector/search-worker.mjs +2 -2
  43. package/dist/workers/entry.mjs +3 -3
  44. package/package.json +7 -7
  45. package/dist/chunk-VTPLKNWI.mjs +0 -135
  46. package/dist/direct-executor-3ZYXSGFX.mjs +0 -12
package/dist/react.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { createContext, useState, useEffect, createElement, useRef, useEffectEvent, useCallback, useMemo, useContext, useSyncExternalStore } from 'react';
1
+ import { createContext, useState, createElement, useRef, useEffectEvent, useEffect, useCallback, useMemo, useContext, useSyncExternalStore } from 'react';
2
2
 
3
3
  // src/errors.ts
4
4
  var NARSIL_ERROR_MARKER = /* @__PURE__ */ Symbol.for("@delali/narsil.NarsilError");
@@ -147,8 +147,53 @@ function isNarsilError(value) {
147
147
  return typeof value === "object" && value !== null && NARSIL_ERROR_MARKER in value;
148
148
  }
149
149
 
150
+ // src/react/reader.ts
151
+ function plainNumbersIn(value) {
152
+ if (value === null || typeof value !== "object") return value;
153
+ if (ArrayBuffer.isView(value)) {
154
+ return value instanceof DataView ? value : Array.from(value);
155
+ }
156
+ if (Array.isArray(value)) {
157
+ let copy2 = null;
158
+ for (let at = 0; at < value.length; at++) {
159
+ const converted = plainNumbersIn(value[at]);
160
+ if (converted === value[at]) continue;
161
+ copy2 ??= value.slice();
162
+ copy2[at] = converted;
163
+ }
164
+ return copy2 ?? value;
165
+ }
166
+ let copy = null;
167
+ for (const [key, held] of Object.entries(value)) {
168
+ const converted = plainNumbersIn(held);
169
+ if (converted === held) continue;
170
+ copy ??= { ...value };
171
+ copy[key] = converted;
172
+ }
173
+ return copy ?? value;
174
+ }
175
+ function withPlainNumbers(answer) {
176
+ return answer.then((value) => plainNumbersIn(value));
177
+ }
178
+ function engineReader(engine) {
179
+ return {
180
+ query(indexName, params) {
181
+ return withPlainNumbers(engine.query(indexName, params));
182
+ },
183
+ preflight: (indexName, params) => engine.preflight(indexName, params),
184
+ suggest: (indexName, params) => engine.suggest(indexName, params),
185
+ get: (indexName, docId) => withPlainNumbers(engine.get(indexName, docId)),
186
+ listDocuments(indexName, params) {
187
+ return withPlainNumbers(engine.listDocuments(indexName, params));
188
+ },
189
+ listIndexes: async () => engine.listIndexes(),
190
+ getStats: async (indexName) => engine.getStats(indexName)
191
+ };
192
+ }
193
+
150
194
  // src/react/constants.ts
151
195
  var DEFAULT_KEEP_ALIVE_MS = 2e3;
196
+ var DEFAULT_REFRESH_AFTER_WRITE_MS = 200;
152
197
  var DEFAULT_TASK_POLL_INTERVAL_MS = 250;
153
198
  var ERROR_RETRY_INTERVAL_MS = 5e3;
154
199
  var MAX_QUERY_KEY_DEPTH = 32;
@@ -173,10 +218,13 @@ var IDLE_SNAPSHOT = Object.freeze({
173
218
  isLoading: false,
174
219
  isFetching: false
175
220
  });
176
- function createResourceStore(keepAliveMs = DEFAULT_KEEP_ALIVE_MS) {
221
+ var ANY_INDEX = /* @__PURE__ */ Symbol("any index");
222
+ var NO_INDEX = /* @__PURE__ */ Symbol("no index");
223
+ function createResourceStore(keepAliveMs = DEFAULT_KEEP_ALIVE_MS, refreshAfterWriteMs = DEFAULT_REFRESH_AFTER_WRITE_MS) {
177
224
  const entries = /* @__PURE__ */ new Map();
178
225
  let holders = 0;
179
226
  let closing = null;
227
+ const staleRefreshes = /* @__PURE__ */ new Map();
180
228
  function publish(entry, next) {
181
229
  entry.snapshot = next;
182
230
  for (const listener of entry.listeners) listener();
@@ -185,6 +233,7 @@ function createResourceStore(keepAliveMs = DEFAULT_KEEP_ALIVE_MS) {
185
233
  if (entry.controller !== null) return;
186
234
  const controller = new AbortController();
187
235
  entry.controller = controller;
236
+ entry.stale = false;
188
237
  entry.version++;
189
238
  const version = entry.version;
190
239
  if (!entry.snapshot.isFetching) publish(entry, { ...entry.snapshot, isFetching: true });
@@ -209,9 +258,32 @@ function createResourceStore(keepAliveMs = DEFAULT_KEEP_ALIVE_MS) {
209
258
  );
210
259
  }
211
260
  function disposeAll() {
261
+ for (const timer of staleRefreshes.values()) clearTimeout(timer);
262
+ staleRefreshes.clear();
212
263
  for (const [key, entry] of [...entries]) drop(key, entry);
213
264
  entries.clear();
214
265
  }
266
+ function refreshStale(scope) {
267
+ staleRefreshes.delete(scope);
268
+ for (const [key, entry] of entries) {
269
+ if (entry.writeScope === scope && entry.stale && entry.listeners.size > 0) load(key, entry);
270
+ }
271
+ }
272
+ function scheduleRefresh(scope) {
273
+ const pending = staleRefreshes.get(scope);
274
+ if (pending !== void 0) clearTimeout(pending);
275
+ staleRefreshes.set(
276
+ scope,
277
+ setTimeout(() => refreshStale(scope), refreshAfterWriteMs)
278
+ );
279
+ }
280
+ function markStale(entry) {
281
+ entry.stale = true;
282
+ if (entry.controller === null) return;
283
+ entry.controller.abort();
284
+ entry.controller = null;
285
+ entry.version++;
286
+ }
215
287
  function drop(key, entry) {
216
288
  if (entries.get(key) === entry) entries.delete(key);
217
289
  if (entry.disposal !== null) clearTimeout(entry.disposal);
@@ -220,7 +292,7 @@ function createResourceStore(keepAliveMs = DEFAULT_KEEP_ALIVE_MS) {
220
292
  entry.controller = null;
221
293
  }
222
294
  return {
223
- subscribe(key, loader, onChange) {
295
+ subscribe(key, loader, onChange, writeScope) {
224
296
  let entry = entries.get(key);
225
297
  if (entry === void 0) {
226
298
  entry = {
@@ -229,7 +301,9 @@ function createResourceStore(keepAliveMs = DEFAULT_KEEP_ALIVE_MS) {
229
301
  loader,
230
302
  controller: null,
231
303
  version: 0,
232
- disposal: null
304
+ disposal: null,
305
+ writeScope,
306
+ stale: false
233
307
  };
234
308
  entries.set(key, entry);
235
309
  } else {
@@ -241,7 +315,7 @@ function createResourceStore(keepAliveMs = DEFAULT_KEEP_ALIVE_MS) {
241
315
  }
242
316
  const held = entry;
243
317
  held.listeners.add(onChange);
244
- const retryable = held.snapshot.isLoading || held.snapshot.error !== void 0;
318
+ const retryable = held.stale || held.snapshot.isLoading || held.snapshot.error !== void 0;
245
319
  if (retryable && held.controller === null) load(key, held);
246
320
  return () => {
247
321
  held.listeners.delete(onChange);
@@ -259,6 +333,15 @@ function createResourceStore(keepAliveMs = DEFAULT_KEEP_ALIVE_MS) {
259
333
  const entry = entries.get(key);
260
334
  if (entry !== void 0) load(key, entry);
261
335
  },
336
+ invalidate(indexName) {
337
+ const marked = /* @__PURE__ */ new Set();
338
+ for (const entry of entries.values()) {
339
+ if (entry.writeScope !== ANY_INDEX && entry.writeScope !== indexName) continue;
340
+ markStale(entry);
341
+ marked.add(entry.writeScope);
342
+ }
343
+ for (const scope of marked) scheduleRefresh(scope);
344
+ },
262
345
  retain() {
263
346
  holders++;
264
347
  if (closing !== null) {
@@ -283,30 +366,72 @@ function createResourceStore(keepAliveMs = DEFAULT_KEEP_ALIVE_MS) {
283
366
 
284
367
  // src/react/context.ts
285
368
  var NarsilContext = createContext(null);
286
- function build(client, keepAliveMs) {
287
- return { client, store: createResourceStore(keepAliveMs), keepAliveMs };
369
+ function buildContextValue(props) {
370
+ const { client, engine, keepAliveMs, refreshAfterWriteMs } = props;
371
+ const shared = { store: createResourceStore(keepAliveMs, refreshAfterWriteMs), keepAliveMs, refreshAfterWriteMs };
372
+ if (client !== void 0 && engine === void 0) return { ...shared, reader: client, client, engine: null };
373
+ if (engine !== void 0 && client === void 0) {
374
+ return { ...shared, reader: engineReader(engine), client: null, engine };
375
+ }
376
+ throw new NarsilError(
377
+ ErrorCodes.CONFIG_INVALID,
378
+ "A NarsilProvider requires exactly one of client and engine, but its props set both or neither"
379
+ );
380
+ }
381
+ function providerPropsChanged(value, props) {
382
+ return value.client !== (props.client ?? null) || value.engine !== (props.engine ?? null) || value.keepAliveMs !== props.keepAliveMs || value.refreshAfterWriteMs !== props.refreshAfterWriteMs;
383
+ }
384
+ function useStoreRetention(store) {
385
+ useEffect(() => store.retain(), [store]);
386
+ }
387
+ function useRefreshAfterWrites(engine, store) {
388
+ useEffect(() => {
389
+ if (engine === null) return;
390
+ const onWrite = (payload) => {
391
+ store.invalidate(payload.indexName);
392
+ };
393
+ engine.on("write", onWrite);
394
+ return () => {
395
+ engine.off("write", onWrite);
396
+ };
397
+ }, [engine, store]);
288
398
  }
289
399
  function NarsilProvider(props) {
290
- const { client, keepAliveMs, children } = props;
291
- const [value, setValue] = useState(() => build(client, keepAliveMs));
292
- if (value.client !== client || value.keepAliveMs !== keepAliveMs) {
293
- setValue(build(client, keepAliveMs));
400
+ const [value, setValue] = useState(() => buildContextValue(props));
401
+ if (providerPropsChanged(value, props)) {
402
+ setValue(buildContextValue(props));
294
403
  }
295
- useEffect(() => value.store.retain(), [value]);
296
- return createElement(NarsilContext.Provider, { value }, children);
404
+ useStoreRetention(value.store);
405
+ useRefreshAfterWrites(value.engine, value.store);
406
+ return createElement(NarsilContext.Provider, { value }, props.children);
297
407
  }
298
408
  function useNarsilContext() {
299
409
  const value = useContext(NarsilContext);
300
410
  if (value === null) {
301
411
  throw new NarsilError(
302
412
  ErrorCodes.CONFIG_INVALID,
303
- "A Narsil hook ran outside a NarsilProvider, so it found no client to send through"
413
+ "A Narsil hook was called outside a NarsilProvider, so no client or engine is available to it"
304
414
  );
305
415
  }
306
416
  return value;
307
417
  }
418
+ function useServerClient(hookName) {
419
+ const { client } = useNarsilContext();
420
+ if (client !== null) return client;
421
+ throw new NarsilError(
422
+ ErrorCodes.CONFIG_INVALID,
423
+ `${hookName} works only under a client of a server, because only a server handles tasks and imports, while the NarsilProvider above it holds an engine`,
424
+ { hook: hookName }
425
+ );
426
+ }
308
427
  function useNarsilClient() {
309
- return useNarsilContext().client;
428
+ const { client } = useNarsilContext();
429
+ if (client !== null) return client;
430
+ throw new NarsilError(
431
+ ErrorCodes.CONFIG_INVALID,
432
+ "useNarsilClient returns a client, while the NarsilProvider above it holds an engine, so import that engine directly",
433
+ { hook: "useNarsilClient" }
434
+ );
310
435
  }
311
436
 
312
437
  // src/react/key.ts
@@ -457,8 +582,8 @@ function useKey(parts) {
457
582
  cached.current = { parts, key };
458
583
  return key;
459
584
  }
460
- function useRead(parts, run, options) {
461
- const { client, store } = useNarsilContext();
585
+ function useRead(source, parts, run, options, writeScope) {
586
+ const { store } = useNarsilContext();
462
587
  const enabled = options?.enabled ?? true;
463
588
  const headers = options?.headers;
464
589
  const timeoutMs = options?.timeoutMs;
@@ -472,11 +597,11 @@ function useRead(parts, run, options) {
472
597
  if (!enabled) return NO_SUBSCRIPTION;
473
598
  const loader = (signal) => {
474
599
  const held = call.current;
475
- return held.run(client, { ...held.request, signal });
600
+ return held.run(source, { ...held.request, signal });
476
601
  };
477
- return store.subscribe(key, loader, onChange);
602
+ return store.subscribe(key, loader, onChange, writeScope);
478
603
  },
479
- [enabled, store, client, key]
604
+ [enabled, store, source, key, writeScope]
480
605
  );
481
606
  const readSnapshot = useCallback(
482
607
  () => enabled ? store.snapshot(key) : IDLE_SNAPSHOT,
@@ -508,21 +633,28 @@ function useRead(parts, run, options) {
508
633
  [data, snapshot, refresh]
509
634
  );
510
635
  }
636
+ function useProviderRead(parts, run, options, writeScope) {
637
+ const { reader } = useNarsilContext();
638
+ return useRead(reader, parts, run, options, writeScope);
639
+ }
511
640
 
512
641
  // src/react/documents.ts
513
642
  function useDocument(indexName, docId, options) {
514
643
  const id = docId ?? "";
515
644
  const enabled = (options?.enabled ?? true) && id.length > 0;
516
- return useRead(["get", indexName, id], (client, request) => client.get(indexName, id, request), {
517
- ...options,
518
- enabled
519
- });
645
+ return useProviderRead(
646
+ ["get", indexName, id],
647
+ (reader, request) => reader.get(indexName, id, request),
648
+ { ...options, enabled },
649
+ indexName
650
+ );
520
651
  }
521
652
  function useDocuments(indexName, params, options) {
522
- return useRead(
653
+ return useProviderRead(
523
654
  ["listDocuments", indexName, params],
524
- (client, request) => client.listDocuments(indexName, params, request),
525
- options
655
+ (reader, request) => reader.listDocuments(indexName, params, request),
656
+ options,
657
+ indexName
526
658
  );
527
659
  }
528
660
 
@@ -544,7 +676,7 @@ function isTerminalTask(record) {
544
676
  // src/react/import.ts
545
677
  var NOTHING = { task: void 0, error: void 0, starting: false };
546
678
  function useImport(indexName, options) {
547
- const client = useNarsilClient();
679
+ const client = useServerClient("useImport");
548
680
  const [progress, setProgress] = useState(NOTHING);
549
681
  const polling = useRef(null);
550
682
  const sending = useRef(null);
@@ -652,51 +784,63 @@ function useImport(indexName, options) {
652
784
 
653
785
  // src/react/indexes.ts
654
786
  function useIndexes(options) {
655
- return useRead(["listIndexes"], (client, request) => client.listIndexes(request), options);
787
+ return useProviderRead(["listIndexes"], (reader, request) => reader.listIndexes(request), options, ANY_INDEX);
656
788
  }
657
789
  function useStats(indexName, options) {
658
- return useRead(["getStats", indexName], (client, request) => client.getStats(indexName, request), options);
790
+ return useProviderRead(
791
+ ["getStats", indexName],
792
+ (reader, request) => reader.getStats(indexName, request),
793
+ options,
794
+ indexName
795
+ );
659
796
  }
660
797
 
661
798
  // src/react/search.ts
662
799
  function useQuery(indexName, params, options) {
663
- return useRead(
800
+ return useProviderRead(
664
801
  ["query", indexName, params],
665
- (client, request) => client.query(indexName, params, request),
666
- options
802
+ (reader, request) => reader.query(indexName, params, request),
803
+ options,
804
+ indexName
667
805
  );
668
806
  }
669
807
  function usePreflight(indexName, params, options) {
670
- return useRead(
808
+ return useProviderRead(
671
809
  ["preflight", indexName, params],
672
- (client, request) => client.preflight(indexName, params, request),
673
- options
810
+ (reader, request) => reader.preflight(indexName, params, request),
811
+ options,
812
+ indexName
674
813
  );
675
814
  }
676
815
  function useSuggest(indexName, params, options) {
677
- return useRead(
816
+ return useProviderRead(
678
817
  ["suggest", indexName, params],
679
- (client, request) => client.suggest(indexName, params, request),
680
- options
818
+ (reader, request) => reader.suggest(indexName, params, request),
819
+ options,
820
+ indexName
681
821
  );
682
822
  }
683
823
 
684
824
  // src/react/tasks.ts
685
825
  function useTask(taskId, options) {
826
+ const client = useServerClient("useTask");
686
827
  const id = taskId ?? "";
687
828
  const enabled = (options?.enabled ?? true) && id.length > 0;
688
- const state = useRead(["getTask", id], (client, request) => client.getTask(id, request), {
689
- ...options,
690
- enabled,
691
- refreshIntervalMs: 0
692
- });
829
+ const state = useRead(
830
+ client,
831
+ ["getTask", id],
832
+ (server, request) => server.getTask(id, request),
833
+ { ...options, enabled, refreshIntervalMs: 0 },
834
+ NO_INDEX
835
+ );
693
836
  const running = state.data !== null && !isTerminalTask(state.data);
694
837
  const interval = pollInterval(options?.pollIntervalMs ?? DEFAULT_TASK_POLL_INTERVAL_MS, state.error !== void 0);
695
838
  usePolling(state.refresh, interval, enabled && running);
696
839
  return state;
697
840
  }
698
841
  function useTasks(query, options) {
699
- return useRead(["listTasks", query], (client, request) => client.listTasks(query, request), options);
842
+ const client = useServerClient("useTasks");
843
+ return useRead(client, ["listTasks", query], (server, request) => server.listTasks(query, request), options, NO_INDEX);
700
844
  }
701
845
 
702
846
  export { ClientErrorCodes, ErrorCodes, NarsilError, NarsilProvider, ServerErrorCodes, isNarsilError, useDocument, useDocuments, useImport, useIndexes, useNarsilClient, usePreflight, useQuery, useStats, useSuggest, useTask, useTasks };
@@ -13,20 +13,63 @@ type AnyDocument = Record<string, unknown> & {
13
13
  id?: string;
14
14
  };
15
15
  /**
16
- * The type a schema field declares, which sets how the engine indexes it.
16
+ * Each of these options can follow the base type of a `string` or `string[]`
17
+ * field after a colon, as in `string:sortable:pattern`.
17
18
  *
18
- * `string` is analysed and searchable, `number`, `boolean`, and `enum` are
19
- * filterable and sortable, `geopoint` accepts a latitude and longitude pair,
20
- * and `vector[N]` holds an N-dimensional embedding. Each array form indexes
21
- * every element of the field.
19
+ * The engine can sort by a field whose type includes `sortable`. On a single
20
+ * `string` field with that option, the engine can also test a text range with
21
+ * `gt`, `gte`, `lt`, `lte`, or `between`. The engine raises
22
+ * `DOC_VALIDATION_FAILED` for a value longer than
23
+ * {@link IndexConfig.patternValueLimit} in a field whose type includes
24
+ * `pattern`. A later release will add pattern search and partial-word search,
25
+ * which will test the fields whose types include `pattern` and `partial`
26
+ * respectively.
22
27
  *
23
- * `string:sortable` is a `string` in every respect, and a sort may name it.
24
- * Ordering text costs far more memory per document than ordering a number, so
25
- * a sort naming a plain `string` field raises `SEARCH_INVALID_FIELD`.
28
+ * @public
29
+ */
30
+ type TextFieldOption = 'sortable' | 'pattern' | 'partial';
31
+ /**
32
+ * This union contains every string of options that can follow a base type,
33
+ * with each option after a colon. The options can come in any order, although
34
+ * each one can appear only once.
26
35
  *
27
36
  * @public
28
37
  */
29
- type FieldType = 'string' | 'string:sortable' | 'number' | 'boolean' | 'enum' | 'geopoint' | `vector[${number}]` | 'string[]' | 'number[]' | 'boolean[]' | 'enum[]';
38
+ type FieldOptionChain<Remaining extends string> = {
39
+ [Option in Remaining]: `:${Option}` | `:${Option}${FieldOptionChain<Exclude<Remaining, Option>>}`;
40
+ }[Remaining];
41
+ /**
42
+ * The engine indexes each field of a schema according to the field's type,
43
+ * which is one of these.
44
+ *
45
+ * The engine splits a `string` value into words, so it can find the document
46
+ * by any word in the value. It can filter and sort by a `number`, `boolean`,
47
+ * or `enum` field. A `geopoint` value is a latitude and longitude pair. A
48
+ * `vector[N]` value is an embedding with N dimensions. For each list form, the
49
+ * engine indexes every element of the list.
50
+ *
51
+ * The engine stores and compares a `verbatim` value exactly as written, as one
52
+ * piece of text. Because the engine keeps that value whole, it leaves the
53
+ * field out of keyword search, although it can test the whole value in a
54
+ * filter.
55
+ *
56
+ * Add the {@link TextFieldOption} options to a `string` or `string[]` type, or
57
+ * `sortable` alone to a `verbatim` or `verbatim[]` type, as in
58
+ * `string:sortable:pattern` or `verbatim[]:sortable`. The engine stores the
59
+ * options in the order `sortable`, `pattern`, `partial`, whatever their order
60
+ * in the type name. It raises `SCHEMA_INVALID_TYPE` for any other
61
+ * option, such as `number:sortable` or `verbatim:pattern`, and for an option
62
+ * written twice.
63
+ *
64
+ * To sort by a text field, the engine keeps up to the first 512 code points of
65
+ * every document's value in memory, against 8 bytes per document for a number
66
+ * field. For that reason, the engine sorts by a text field only where the
67
+ * field's type includes `sortable`, while it raises `SEARCH_INVALID_FIELD`
68
+ * for a sort on any other text field.
69
+ *
70
+ * @public
71
+ */
72
+ type FieldType = 'string' | `string${FieldOptionChain<TextFieldOption>}` | 'string[]' | `string[]${FieldOptionChain<TextFieldOption>}` | 'verbatim' | 'verbatim:sortable' | 'verbatim[]' | 'verbatim[]:sortable' | 'number' | 'boolean' | 'enum' | 'geopoint' | `vector[${number}]` | 'number[]' | 'boolean[]' | 'enum[]';
30
73
  /**
31
74
  * The field layout of an index, written as a plain object.
32
75
  *
@@ -173,6 +216,15 @@ interface IndexConfig {
173
216
  * insert throughput and one entry per changed spelling.
174
217
  */
175
218
  surfaceForms?: boolean;
219
+ /**
220
+ * The engine raises `DOC_VALIDATION_FAILED` for a document whose value in a
221
+ * `verbatim` field, or in a field whose type includes `pattern`, is longer
222
+ * than this many code points. It tests each element of a list on its own.
223
+ * Set a whole number from 1 to 65,536, or leave this unset for a limit of
224
+ * 8,192. For any other value, {@link Narsil.createIndex} throws
225
+ * `CONFIG_INVALID`.
226
+ */
227
+ patternValueLimit?: number;
176
228
  }
177
229
  /**
178
230
  * BM25 tuning for an index.
@@ -286,4 +338,4 @@ interface InsertOptions extends WriteOptions {
286
338
  skipClone?: boolean;
287
339
  }
288
340
 
289
- export type { AnyDocument as A, BM25Params as B, CustomTokenizer as C, EmbeddingFieldConfig as E, FieldType as F, IndexConfig as I, PartitionConfig as P, SchemaDefinition as S, VectorIndexConfig as V, WriteOptions as W, InsertOptions as a, StopWordOverride as b, ScoringMode as c, VectorQuantizationMode as d, VectorStorageMode as e };
341
+ export type { AnyDocument as A, BM25Params as B, CustomTokenizer as C, EmbeddingFieldConfig as E, FieldOptionChain as F, IndexConfig as I, PartitionConfig as P, SchemaDefinition as S, TextFieldOption as T, VectorIndexConfig as V, WriteOptions as W, InsertOptions as a, StopWordOverride as b, FieldType as c, ScoringMode as d, VectorQuantizationMode as e, VectorStorageMode as f };
package/dist/server.d.ts CHANGED
@@ -1,14 +1,14 @@
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, i as isNarsilError } from './memory-CKF-9WLP.js';
2
- import { b as ServerOptions, N as NarsilServer } from './capabilities-B1wz2Zl0.js';
3
- export { A as ASYNC_IMPORT_CAPABILITY, C as CorsOptions, E as ErrorEnvelope, I as INDEX_LIFECYCLE_CAPABILITY, O as OnRequestHook, R as REBUILD_ANALYSIS_CAPABILITY, c as RequestContext, d as RequestDenial, S as SERVER_CAPABILITIES, e as ServerLimits, T as TASK_CANCEL_CAPABILITY, a as TASK_FILTER_CAPABILITY } from './capabilities-B1wz2Zl0.js';
4
- import { a as Narsil } from './engine-77ErLy8P.js';
5
- import { d as TaskProgress, b as ImportResult, h as TaskStore, e as TaskRecord } from './tasks-BEiNxDRs.js';
6
- export { C as CapabilitiesResponse, a as CreateIndexEmbedding, i as CreateIndexRequest, H as HttpIndexConfig, I as ImportError, M as MemoryStatsResponse, T as TaskListPage, c as TaskListQuery, f as TaskStatus, g as TaskType } from './tasks-BEiNxDRs.js';
7
- import './schema-d5Afu03A.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, i as isNarsilError } from './memory-C7UMCMH3.js';
2
+ import { b as ServerOptions, N as NarsilServer } from './capabilities-Dt-7B4Wz.js';
3
+ export { A as ASYNC_IMPORT_CAPABILITY, C as CorsOptions, E as ErrorEnvelope, I as INDEX_LIFECYCLE_CAPABILITY, O as OnRequestHook, R as REBUILD_ANALYSIS_CAPABILITY, c as RequestContext, d as RequestDenial, S as SERVER_CAPABILITIES, e as ServerLimits, T as TASK_CANCEL_CAPABILITY, a as TASK_FILTER_CAPABILITY } from './capabilities-Dt-7B4Wz.js';
4
+ import { a as Narsil } from './engine-igk0aMpQ.js';
5
+ import { d as TaskProgress, b as ImportResult, h as TaskStore, e as TaskRecord } from './tasks-BZt_zes6.js';
6
+ export { C as CapabilitiesResponse, a as CreateIndexEmbedding, i as CreateIndexRequest, H as HttpIndexConfig, I as ImportError, M as MemoryStatsResponse, T as TaskListPage, c as TaskListQuery, f as TaskStatus, g as TaskType } from './tasks-BZt_zes6.js';
7
+ import './schema-Bgzr4g7b.js';
8
8
  import './adapters-79OIa4xm.js';
9
- import './config-CSvCVtB7.js';
10
- import './config-CEeSV_Xo.js';
11
- import './types-DAK7mnUZ.js';
9
+ import './config-rBpbgTw8.js';
10
+ import './config-Dfanyqqt.js';
11
+ import './types-gOUqhs2F.js';
12
12
  import './types-D5OwhtsQ.js';
13
13
 
14
14
  /**
package/dist/server.mjs CHANGED
@@ -1,11 +1,14 @@
1
- import { DEFAULT_MAX_RETAINED_TASKS, loadUWebSockets, resolveCors, authorizerFor, createSharedGateBuffer, gateSlotOfWorker, createRouteRunner, corsHeaderReader, 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, createVersionHandler, 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, IMPORT_FLUSHES_IN_FLIGHT, badRequest, SERVER_CAPABILITIES, toHttpError, MIN_UNBOUNDED_SERVER_LIMIT, MIN_COUNTED_SERVER_LIMIT } from './chunk-DD5X23E6.mjs';
2
- export { ASYNC_IMPORT_CAPABILITY, INDEX_LIFECYCLE_CAPABILITY, REBUILD_ANALYSIS_CAPABILITY, SERVER_CAPABILITIES, TASK_CANCEL_CAPABILITY, TASK_FILTER_CAPABILITY, httpStatusForNarsilError } from './chunk-DD5X23E6.mjs';
3
- import { validateIndexName } from './chunk-VTPLKNWI.mjs';
4
- import { engineCoreOf, providedDocId, resolveVectorText } from './chunk-YA52AC2V.mjs';
5
- import { detectRuntime } from './chunk-AV4IJKD2.mjs';
6
- import './chunk-5JLSTECC.mjs';
7
- import './chunk-6TGZG2TV.mjs';
1
+ import { DEFAULT_MAX_RETAINED_TASKS, loadUWebSockets, resolveCors, authorizerFor, createSharedGateBuffer, gateSlotOfWorker, createRouteRunner, corsHeaderReader, 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, createVersionHandler, 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, IMPORT_FLUSHES_IN_FLIGHT, badRequest, SERVER_CAPABILITIES, toHttpError, MIN_UNBOUNDED_SERVER_LIMIT, MIN_COUNTED_SERVER_LIMIT } from './chunk-2STVUTNA.mjs';
2
+ export { ASYNC_IMPORT_CAPABILITY, INDEX_LIFECYCLE_CAPABILITY, REBUILD_ANALYSIS_CAPABILITY, SERVER_CAPABILITIES, TASK_CANCEL_CAPABILITY, TASK_FILTER_CAPABILITY, httpStatusForNarsilError } from './chunk-2STVUTNA.mjs';
3
+ import { validateIndexName } from './chunk-3JGWULDF.mjs';
4
+ import { engineCoreOf, providedDocId, resolveVectorText } from './chunk-FVLQ63B7.mjs';
5
+ import { detectRuntime } from './chunk-HWQLTLNG.mjs';
6
+ import './chunk-GIVAGD3Q.mjs';
7
+ import './chunk-653BNMAF.mjs';
8
+ import './chunk-TGP5M5R7.mjs';
8
9
  import './chunk-YNVIUGF5.mjs';
10
+ import './chunk-U2BKYEC7.mjs';
11
+ import './chunk-OI42GWLR.mjs';
9
12
  import { startIdleHeapCleanup } from './chunk-Y3L5MDRV.mjs';
10
13
  import { NarsilError, ErrorCodes, ServerErrorCodes, isNarsilError } from './chunk-USKFU7J5.mjs';
11
14
  export { ClientErrorCodes, ErrorCodes, NarsilError, ServerErrorCodes, isNarsilError } from './chunk-USKFU7J5.mjs';
@@ -594,6 +597,7 @@ function mapHttpIndexConfig(http) {
594
597
  if (http.vectorPromotion !== void 0) config.vectorPromotion = http.vectorPromotion;
595
598
  if (http.strict !== void 0) config.strict = http.strict;
596
599
  if (http.required !== void 0) config.required = http.required;
600
+ if (http.patternValueLimit !== void 0) config.patternValueLimit = http.patternValueLimit;
597
601
  if (http.stopWords !== void 0) {
598
602
  if (!Array.isArray(http.stopWords) || http.stopWords.some((w) => typeof w !== "string")) {
599
603
  throw new NarsilError(ErrorCodes.CONFIG_INVALID, 'Field "stopWords" must be an array of strings');
@@ -1,4 +1,4 @@
1
- import { M as MemoryStats } from './memory-CKF-9WLP.js';
1
+ import { M as MemoryStats } from './memory-C7UMCMH3.js';
2
2
 
3
3
  /** Declarative index configuration accepted over HTTP. Function-valued engine
4
4
  * options (custom tokenizer, stopWords-as-function, group reducer, embedding
@@ -68,6 +68,8 @@ interface HttpIndexConfig {
68
68
  embedding?: CreateIndexEmbedding;
69
69
  /** The server rejects a document that omits any of these fields. */
70
70
  required?: string[];
71
+ /** The server returns a `DOC_VALIDATION_FAILED` error for a document whose value in a `verbatim` field, or in a field whose type includes `pattern`, is longer than this many code points. Set a whole number from 1 to 65,536, or leave this unset for a limit of 8,192. */
72
+ patternValueLimit?: number;
71
73
  }
72
74
  /**
73
75
  * One record an import refused, naming the line it came from and why the
@@ -1,11 +1,11 @@
1
- import { loadUWebSockets, childAppOf, createRouteRunner, corsHeaderReader, createSharedRequestGate, registerServerRoutes, sendRelayed, createDocumentReadHandlers, respondJson, SERVER_CAPABILITIES, versionReport, sendError, serverRoutes, parseJson, validateSuggest, rejectInvalid, respondError, validateQuery } from './chunk-DD5X23E6.mjs';
2
- import { executeListDocuments, executeSuggest, executePreflight, queryBindingOf, executeQuery } from './chunk-2BDG2IWP.mjs';
3
- import './chunk-5JLSTECC.mjs';
4
- import { resolveProjection, projectionKeepsField, setNestedValue } from './chunk-4VU5TWCR.mjs';
5
- import { extractVectorFieldsFromSchema } from './chunk-6TGZG2TV.mjs';
1
+ import { loadUWebSockets, childAppOf, createRouteRunner, corsHeaderReader, createSharedRequestGate, registerServerRoutes, sendRelayed, createDocumentReadHandlers, respondJson, SERVER_CAPABILITIES, versionReport, sendError, serverRoutes, parseJson, validateSuggest, rejectInvalid, respondError, validateQuery } from './chunk-2STVUTNA.mjs';
2
+ import { executeListDocuments, executeSuggest, executePreflight, queryBindingOf, executeQuery } from './chunk-P4DSQ6M4.mjs';
3
+ import './chunk-GIVAGD3Q.mjs';
4
+ import { resolveProjection, projectionKeepsField, setNestedValue } from './chunk-LMO5TJUS.mjs';
5
+ import { extractVectorFieldsFromSchema } from './chunk-653BNMAF.mjs';
6
6
  import './chunk-ZJ4CSAB2.mjs';
7
7
  import './chunk-YNVIUGF5.mjs';
8
- import './chunk-YMZCMH5M.mjs';
8
+ import './chunk-OI42GWLR.mjs';
9
9
  import { ServerErrorCodes, NarsilError, ErrorCodes } from './chunk-USKFU7J5.mjs';
10
10
 
11
11
  // src/server/request-threads/held-vectors.ts
@@ -1,4 +1,4 @@
1
- import { S as SchemaDefinition } from './schema-d5Afu03A.js';
1
+ import { S as SchemaDefinition } from './schema-Bgzr4g7b.js';
2
2
 
3
3
  /**
4
4
  * What a node does in a cluster. A node may hold several roles at once.
@@ -1,4 +1,4 @@
1
- import { d as VectorQuantizationMode } from '../schema-d5Afu03A.js';
1
+ import { e as VectorQuantizationMode } from '../schema-Bgzr4g7b.js';
2
2
  import { A as ArenaSimd } from '../simd-DW5AtJ6B.js';
3
3
  import '../adapters-79OIa4xm.js';
4
4
 
@@ -1,7 +1,7 @@
1
- import { openSharedVectorField, MAX_WORKER_COPIES, createVectorStore, osqBitsOf, createHNSWIndex, createOsqQuantizer, osqRecordBytes } from '../chunk-THYRHHO2.mjs';
1
+ import { openSharedVectorField, MAX_WORKER_COPIES, createVectorStore, osqBitsOf, createHNSWIndex, createOsqQuantizer, osqRecordBytes } from '../chunk-YJDQ7NLA.mjs';
2
2
  import '../chunk-ZJ4CSAB2.mjs';
3
3
  import '../chunk-YNVIUGF5.mjs';
4
- import '../chunk-YMZCMH5M.mjs';
4
+ import '../chunk-OI42GWLR.mjs';
5
5
  import '../chunk-USKFU7J5.mjs';
6
6
 
7
7
  // src/vector/worker-copy.ts
@@ -18,7 +18,7 @@ async function importBootstrapModule(moduleUrl) {
18
18
  await import(moduleUrl);
19
19
  }
20
20
  async function stopServingRequests() {
21
- const { closeRequestThread } = await import('../thread-FLP45EUD.mjs');
21
+ const { closeRequestThread } = await import('../thread-JEVAYETR.mjs');
22
22
  closeRequestThread();
23
23
  }
24
24
  function holdsCopies(executor) {
@@ -49,7 +49,7 @@ function createActionHandler(executor) {
49
49
  if (!holdsCopies(executor)) {
50
50
  throw new NarsilError(ErrorCodes.CONFIG_INVALID, "This executor holds no copies to answer requests from");
51
51
  }
52
- const starting = import('../thread-FLP45EUD.mjs').then(
52
+ const starting = import('../thread-JEVAYETR.mjs').then(
53
53
  ({ serveRequests }) => serveRequests(executor, action.settings)
54
54
  );
55
55
  serving.start = starting.then(
@@ -102,7 +102,7 @@ async function setup() {
102
102
  } catch {
103
103
  parentPort = null;
104
104
  }
105
- const { createDirectExecutor } = await import('../direct-executor-3ZYXSGFX.mjs');
105
+ const { createDirectExecutor } = await import('../direct-executor-GYJEKNV5.mjs');
106
106
  const handleAction = createActionHandler(createDirectExecutor({ threadSlot }));
107
107
  if (parentPort) {
108
108
  const port = parentPort;