@vielzeug/scout 1.1.1
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 +70 -0
- package/dist/_dev.cjs +2 -0
- package/dist/_dev.cjs.map +1 -0
- package/dist/_dev.d.ts +2 -0
- package/dist/_dev.d.ts.map +1 -0
- package/dist/_dev.js +9 -0
- package/dist/_dev.js.map +1 -0
- package/dist/adapters.cjs +2 -0
- package/dist/adapters.cjs.map +1 -0
- package/dist/adapters.d.ts +40 -0
- package/dist/adapters.d.ts.map +1 -0
- package/dist/adapters.js +12 -0
- package/dist/adapters.js.map +1 -0
- package/dist/devtools.cjs +2 -0
- package/dist/devtools.cjs.map +1 -0
- package/dist/devtools.d.ts +38 -0
- package/dist/devtools.d.ts.map +1 -0
- package/dist/devtools.js +21 -0
- package/dist/devtools.js.map +1 -0
- package/dist/errors.cjs +2 -0
- package/dist/errors.cjs.map +1 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +13 -0
- package/dist/errors.js.map +1 -0
- package/dist/highlight.cjs +2 -0
- package/dist/highlight.cjs.map +1 -0
- package/dist/highlight.d.ts +68 -0
- package/dist/highlight.d.ts.map +1 -0
- package/dist/highlight.js +45 -0
- package/dist/highlight.js.map +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/reactive.cjs +2 -0
- package/dist/reactive.cjs.map +1 -0
- package/dist/reactive.d.ts +74 -0
- package/dist/reactive.d.ts.map +1 -0
- package/dist/reactive.js +71 -0
- package/dist/reactive.js.map +1 -0
- package/dist/scout-index.cjs +2 -0
- package/dist/scout-index.cjs.map +1 -0
- package/dist/scout-index.d.ts +64 -0
- package/dist/scout-index.d.ts.map +1 -0
- package/dist/scout-index.js +162 -0
- package/dist/scout-index.js.map +1 -0
- package/dist/scout.cjs +2 -0
- package/dist/scout.cjs.map +1 -0
- package/dist/scout.iife.js +2 -0
- package/dist/scout.iife.js.map +1 -0
- package/dist/scout.js +2 -0
- package/dist/scout.js.map +1 -0
- package/dist/segment.cjs +2 -0
- package/dist/segment.cjs.map +1 -0
- package/dist/segment.d.ts +26 -0
- package/dist/segment.d.ts.map +1 -0
- package/dist/segment.js +13 -0
- package/dist/segment.js.map +1 -0
- package/dist/tokenize.cjs +2 -0
- package/dist/tokenize.cjs.map +1 -0
- package/dist/tokenize.d.ts +2 -0
- package/dist/tokenize.d.ts.map +1 -0
- package/dist/tokenize.js +12 -0
- package/dist/tokenize.js.map +1 -0
- package/dist/trigram.cjs +2 -0
- package/dist/trigram.cjs.map +1 -0
- package/dist/trigram.d.ts +2 -0
- package/dist/trigram.d.ts.map +1 -0
- package/dist/trigram.js +16 -0
- package/dist/trigram.js.map +1 -0
- package/dist/types.d.ts +130 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +48 -0
package/dist/reactive.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ScoutDisposedError as e } from "./errors.js";
|
|
2
|
+
import { createIndex as t } from "./scout-index.js";
|
|
3
|
+
import { batch as n, computed as r, signal as i } from "@vielzeug/ripple";
|
|
4
|
+
//#region src/reactive.ts
|
|
5
|
+
var a = 200;
|
|
6
|
+
function o(t, o = {}) {
|
|
7
|
+
let { debounce: s = a, limit: c, minQueryLength: l, threshold: u } = o, d = i("", { name: "scout:query" }), f = i("", { name: "scout:committedQuery" }), p = i(0, { name: "scout:indexVersion" }), m = t.onMutate(() => {
|
|
8
|
+
p.value++;
|
|
9
|
+
}), h = r(() => d.value !== f.value, { name: "scout:isSearching" }), g = r(() => (p.value, t.search(f.value, {
|
|
10
|
+
limit: c,
|
|
11
|
+
minQueryLength: l,
|
|
12
|
+
threshold: u
|
|
13
|
+
})), { name: "scout:results" }), _ = null;
|
|
14
|
+
function v() {
|
|
15
|
+
_ !== null && (clearTimeout(_), _ = null);
|
|
16
|
+
}
|
|
17
|
+
let y = d.subscribe(() => {
|
|
18
|
+
let e = d.peek();
|
|
19
|
+
if (v(), e !== f.peek()) {
|
|
20
|
+
if (s === 0) {
|
|
21
|
+
f.value = e;
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
_ = setTimeout(() => {
|
|
25
|
+
f.value = e, _ = null;
|
|
26
|
+
}, s);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
function b() {
|
|
30
|
+
if (x) throw new e("SearchState.clear() called after dispose()");
|
|
31
|
+
v(), n(() => {
|
|
32
|
+
d.value = "", f.value = "";
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
let x = !1, S = new AbortController();
|
|
36
|
+
function C() {
|
|
37
|
+
x = !0, S.abort(), v(), y.dispose(), m(), d.dispose(), f.dispose(), h.dispose(), g.dispose(), p.dispose();
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
clear: b,
|
|
41
|
+
get disposalSignal() {
|
|
42
|
+
return S.signal;
|
|
43
|
+
},
|
|
44
|
+
dispose: C,
|
|
45
|
+
get disposed() {
|
|
46
|
+
return x;
|
|
47
|
+
},
|
|
48
|
+
isSearching: h,
|
|
49
|
+
query: d,
|
|
50
|
+
results: g,
|
|
51
|
+
[Symbol.dispose]() {
|
|
52
|
+
C();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function s(e, n) {
|
|
57
|
+
let r = t(e, {
|
|
58
|
+
fields: n.fields,
|
|
59
|
+
limit: n.limit,
|
|
60
|
+
minQueryLength: n.minQueryLength,
|
|
61
|
+
threshold: n.threshold
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
...o(r, { debounce: n.debounce }),
|
|
65
|
+
index: r
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
69
|
+
export { s as createReactiveSearch, o as createSearch };
|
|
70
|
+
|
|
71
|
+
//# sourceMappingURL=reactive.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reactive.js","names":[],"sources":["../src/reactive.ts"],"sourcesContent":["import { batch, computed, signal } from '@vielzeug/ripple';\n\nimport type { CreateSearchOptions, ScoutIndexOptions, SearchResult, SearchState } from './types';\n\nimport { ScoutDisposedError } from './errors';\nimport { createIndex, type ScoutIndex } from './scout-index';\n\n/**\n * Combined index + reactive search state returned by `createReactiveSearch()`.\n * Exposes the underlying `ScoutIndex` for incremental mutations (`add`, `remove`, `reindex`).\n */\nexport type ReactiveSearch<T> = SearchState<T> & {\n readonly index: ScoutIndex<T>;\n};\n\nconst DEFAULT_DEBOUNCE = 200;\n\n/**\n * Creates a reactive search state backed by a `ScoutIndex`.\n *\n * - Set `state.query.value` to trigger a (debounced) search.\n * - Read `state.results.value` inside an `effect` or `computed` to consume results reactively.\n * - `state.isSearching.value` is `true` while debouncing, `false` otherwise.\n * - Call `state.dispose()` (or `using state = createSearch(...)`) to release subscriptions.\n *\n * @example\n * ```ts\n * const index = createIndex(users, { fields: ['name', 'email'] });\n * const search = createSearch(index, { debounce: 150 });\n *\n * effect(() => {\n * const results = search.results.value;\n * renderList(results.map(r => r.item));\n * });\n *\n * // Wire to an input\n * input.addEventListener('input', e => {\n * search.query.value = e.currentTarget.value;\n * });\n *\n * // Clean up\n * search.dispose();\n * ```\n *\n * `results` also updates when the index is mutated directly via `index.add()` / `.remove()`\n * / `.reindex()` (not just when `query` changes), by subscribing to `index.onMutate()`.\n *\n * @param index - A `ScoutIndex` built with `createIndex()`.\n * @param options.debounce - Milliseconds to wait before committing query changes. Default: `200`.\n * @param options.limit - Override the index-level result limit.\n * @param options.minQueryLength - Override the index-level minimum query length.\n * @param options.threshold - Override the index-level score threshold.\n */\nexport function createSearch<T>(index: ScoutIndex<T>, options: CreateSearchOptions = {}): SearchState<T> {\n const { debounce: debounceMs = DEFAULT_DEBOUNCE, limit, minQueryLength, threshold } = options;\n\n const query = signal<string>('', { name: 'scout:query' });\n const committedQuery = signal<string>('', { name: 'scout:committedQuery' });\n const indexVersion = signal(0, { name: 'scout:indexVersion' });\n\n const unsubscribeMutations = index.onMutate(() => {\n indexVersion.value++;\n });\n\n const isSearching = computed(() => query.value !== committedQuery.value, { name: 'scout:isSearching' });\n\n const results = computed<SearchResult<T>[]>(\n () => {\n // Reading .value establishes a dependency so index mutations trigger a recompute.\n void indexVersion.value;\n\n return index.search(committedQuery.value, { limit, minQueryLength, threshold });\n },\n { name: 'scout:results' },\n );\n\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n function cancelTimer(): void {\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n }\n\n const subscription = query.subscribe(() => {\n const q = query.peek();\n\n cancelTimer();\n\n if (q === committedQuery.peek()) return;\n\n if (debounceMs === 0) {\n committedQuery.value = q;\n\n return;\n }\n\n timer = setTimeout(() => {\n committedQuery.value = q;\n timer = null;\n }, debounceMs);\n });\n\n function clear(): void {\n if (isDisposed) throw new ScoutDisposedError('SearchState.clear() called after dispose()');\n\n cancelTimer();\n\n batch(() => {\n query.value = '';\n committedQuery.value = '';\n });\n }\n\n let isDisposed = false;\n const ac = new AbortController();\n\n function dispose(): void {\n isDisposed = true;\n ac.abort();\n cancelTimer();\n subscription.dispose();\n unsubscribeMutations();\n query.dispose();\n committedQuery.dispose();\n isSearching.dispose();\n results.dispose();\n indexVersion.dispose();\n }\n\n return {\n clear,\n get disposalSignal(): AbortSignal {\n return ac.signal;\n },\n dispose,\n get disposed(): boolean {\n return isDisposed;\n },\n isSearching,\n query,\n results,\n [Symbol.dispose](): void {\n dispose();\n },\n };\n}\n\n/**\n * Creates a `ScoutIndex` and a reactive search state in one call — the shorthand\n * for the common pattern of `createIndex` + `createSearch`.\n *\n * The returned `ReactiveSearch` exposes the underlying index via `.index` for\n * incremental mutations (`add`, `remove`, `reindex`) after construction.\n *\n * @example\n * ```ts\n * const search = createReactiveSearch(users, {\n * fields: [{ field: 'name', weight: 2 }, 'email'],\n * debounce: 150,\n * });\n *\n * effect(() => renderList(search.results.value.map(r => r.item)));\n *\n * // Wire to an input\n * input.addEventListener('input', e => { search.query.value = e.currentTarget.value; });\n *\n * // Add a new item at runtime\n * search.index.add(newUser);\n * ```\n *\n * @param items - Initial corpus to index.\n * @param options - Index options (`fields`, `limit`, `minQueryLength`, `threshold`) plus optional `debounce`.\n */\nexport function createReactiveSearch<T>(\n items: T[],\n options: ScoutIndexOptions<T> & Pick<CreateSearchOptions, 'debounce'>,\n): ReactiveSearch<T> {\n const index = createIndex(items, {\n fields: options.fields,\n limit: options.limit,\n minQueryLength: options.minQueryLength,\n threshold: options.threshold,\n });\n const state = createSearch(index, { debounce: options.debounce });\n\n return { ...state, index };\n}\n"],"mappings":";;;;AAeA,IAAM,IAAmB;AAsCzB,SAAgB,EAAgB,GAAsB,IAA+B,CAAC,GAAmB;CACvG,IAAM,EAAE,UAAU,IAAa,GAAkB,UAAO,mBAAgB,iBAAc,GAEhF,IAAQ,EAAe,IAAI,EAAE,MAAM,cAAc,CAAC,GAClD,IAAiB,EAAe,IAAI,EAAE,MAAM,uBAAuB,CAAC,GACpE,IAAe,EAAO,GAAG,EAAE,MAAM,qBAAqB,CAAC,GAEvD,IAAuB,EAAM,eAAe;EAChD,EAAa;CACf,CAAC,GAEK,IAAc,QAAe,EAAM,UAAU,EAAe,OAAO,EAAE,MAAM,oBAAoB,CAAC,GAEhG,IAAU,SAGZ,EAAkB,OAEX,EAAM,OAAO,EAAe,OAAO;EAAE;EAAO;EAAgB;CAAU,CAAC,IAEhF,EAAE,MAAM,gBAAgB,CAC1B,GAEI,IAA8C;CAElD,SAAS,IAAoB;EAC3B,AAAI,MAAU,SACZ,aAAa,CAAK,GAClB,IAAQ;CAEZ;CAEA,IAAM,IAAe,EAAM,gBAAgB;EACzC,IAAM,IAAI,EAAM,KAAK;EAErB,MAAY,GAER,MAAM,EAAe,KAAK,GAE9B;OAAI,MAAe,GAAG;IACpB,EAAe,QAAQ;IAEvB;GACF;GAEA,IAAQ,iBAAiB;IAEvB,AADA,EAAe,QAAQ,GACvB,IAAQ;GACV,GAAG,CAAU;EALb;CAMF,CAAC;CAED,SAAS,IAAc;EACrB,IAAI,GAAY,MAAM,IAAI,EAAmB,4CAA4C;EAIzF,AAFA,EAAY,GAEZ,QAAY;GAEV,AADA,EAAM,QAAQ,IACd,EAAe,QAAQ;EACzB,CAAC;CACH;CAEA,IAAI,IAAa,IACX,IAAK,IAAI,gBAAgB;CAE/B,SAAS,IAAgB;EAUvB,AATA,IAAa,IACb,EAAG,MAAM,GACT,EAAY,GACZ,EAAa,QAAQ,GACrB,EAAqB,GACrB,EAAM,QAAQ,GACd,EAAe,QAAQ,GACvB,EAAY,QAAQ,GACpB,EAAQ,QAAQ,GAChB,EAAa,QAAQ;CACvB;CAEA,OAAO;EACL;EACA,IAAI,iBAA8B;GAChC,OAAO,EAAG;EACZ;EACA;EACA,IAAI,WAAoB;GACtB,OAAO;EACT;EACA;EACA;EACA;EACA,CAAC,OAAO,WAAiB;GACvB,EAAQ;EACV;CACF;AACF;AA4BA,SAAgB,EACd,GACA,GACmB;CACnB,IAAM,IAAQ,EAAY,GAAO;EAC/B,QAAQ,EAAQ;EAChB,OAAO,EAAQ;EACf,gBAAgB,EAAQ;EACxB,WAAW,EAAQ;CACrB,CAAC;CAGD,OAAO;EAAE,GAFK,EAAa,GAAO,EAAE,UAAU,EAAQ,SAAS,CAEnD;EAAO;CAAM;AAC3B"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./errors.cjs"),t=require("./highlight.cjs"),n=require("./tokenize.cjs"),r=require("./trigram.cjs");function i(e){return e.map(e=>typeof e==`string`?{field:e,stringify:n.defaultStringify,weight:1}:{field:e.field,stringify:e.stringify??n.defaultStringify,weight:e.weight??1})}function a(a,o){if(o.fields.length===0)throw new e.ScoutIndexError(`createIndex: at least one field is required.`);let s=i(o.fields),c=s.reduce((e,t)=>Math.max(e,t.weight),1),l=o.threshold??.2,u=o.limit??50,d=o.minQueryLength??3,f=new Map,p=new Map,m=new Set;function h(){for(let e of m)e()}let g=null,_=null;function v(e){return e===g&&_!==null?_:(g=e,_=r.generateTrigrams(e),_)}function y(e,t){for(let n of t){let t=p.get(n);t||(t=new Set,p.set(n,t)),t.add(e)}}function b(e,t){for(let n of t){let t=p.get(n);t&&(t.delete(e),t.size===0&&p.delete(n))}}function x(e){let t=new Map,i=new Map;for(let{field:a,stringify:o}of s){let s=e[a],c=o(s),l=n.tokenize(c),u=l.length>=1?r.generateTrigrams(l):new Set;t.set(a,u),i.set(a,c),y(e,u)}f.set(e,{trigrams:t,values:i})}function S(e){let t=new Set;for(let[n,r]of f)for(let i of r.values.values())if(i.toLowerCase().includes(e)){t.add(n);break}return t}function C(e){let t=new Set;for(let n of e){let e=p.get(n);if(e)for(let n of e)t.add(n)}return t}function w(e,t,n){let i=0;for(let{field:a,weight:o}of s){let s;if(t===null)s=+!!(n.values.get(a)??``).toLowerCase().includes(e);else{let e=n.trigrams.get(a);if(!e||e.size===0)continue;s=r.diceSimilarity(t,e)}let l=o/c*s;l>i&&(i=l)}return i}function T(e,n){let r=[];for(let{field:i}of s){let a=n.get(i);if(!a)continue;let o=t.findMatchRanges(a,e);o.length>0&&r.push({field:i,ranges:o})}return r}for(let e of a)x(e);return{add(e){f.has(e)||(x(e),h())},get items(){return[...f.keys()]},onMutate(e){return m.add(e),()=>{m.delete(e)}},reindex(e){let t=f.get(e);if(!t)return;let i=!1;for(let{field:a,stringify:o}of s){let s=e[a],c=o(s);if(c===t.values.get(a))continue;i=!0;let l=t.trigrams.get(a);l&&b(e,l);let u=n.tokenize(c),d=u.length>=1?r.generateTrigrams(u):new Set;t.trigrams.set(a,d),t.values.set(a,c),y(e,d)}i&&h()},remove(e){let t=f.get(e);if(t){for(let n of t.trigrams.values())b(e,n);f.delete(e),h()}},search(e,t){let r=t?.threshold??l,i=Math.max(0,t?.limit??u),a=t?.minQueryLength??d;if(!e.trim())return[...f.keys()].slice(0,i).map(e=>({item:e,matches:[],score:1}));let o=n.tokenize(e);if(!o)return[];let s=o.length<a?null:v(o),c=s===null?S(o):C(s),p=[];for(let e of c){let t=f.get(e);if(!t)continue;let n=w(o,s,t);if(n>=r){let r=T(o,t.values);p.push({item:e,matches:r,score:n})}}return p.sort((e,t)=>t.score-e.score).slice(0,i)},get size(){return f.size}}}exports.createIndex=a;
|
|
2
|
+
//# sourceMappingURL=scout-index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scout-index.cjs","names":[],"sources":["../src/scout-index.ts"],"sourcesContent":["import type { FieldDef, FieldMatch, ScoutIndexOptions, SearchConstraints, SearchResult } from './types';\n\nimport { ScoutIndexError } from './errors';\nimport { findMatchRanges } from './highlight';\nimport { defaultStringify, tokenize } from './tokenize';\nimport { diceSimilarity, generateTrigrams } from './trigram';\n\ntype FieldConfig<T> = {\n field: keyof T & string;\n stringify: (v: unknown) => string;\n weight: number;\n};\n\ntype ItemRecord = {\n /** Per-field trigrams for scoring. */\n trigrams: Map<string, Set<string>>;\n /** Per-field original text for highlighting. */\n values: Map<string, string>;\n};\n\n/**\n * A stateful, indexed search corpus. Created via `createIndex()`.\n *\n * Supports incremental `add()`, `remove()`, and `reindex()` operations — each patches\n * the trigram index in O(field_length) without a full rebuild.\n */\nexport interface ScoutIndex<T> {\n /** All items currently in the index, in insertion order. */\n readonly items: readonly T[];\n /** Number of items currently in the index. */\n readonly size: number;\n /** Adds `item` to the index. No-op if the item is already indexed (by reference). */\n add(item: T): void;\n /**\n * Re-reads the item's current field values and rebuilds its index entry in-place,\n * only updating fields whose values have changed. Preserves insertion order.\n * No-op if the item is not in the index.\n */\n reindex(item: T): void;\n /**\n * Removes `item` from the index by reference equality.\n * No-op if the item is not in the index.\n */\n remove(item: T): void;\n /**\n * Searches the index for `query` and returns results sorted by score descending.\n *\n * An empty (or whitespace-only) `query` returns all indexed items with `score = 1`.\n * A `query` with no indexable content after normalization (e.g. punctuation-only) returns\n * no results. Results below `threshold` are excluded. At most `limit` results are returned.\n */\n search(query: string, options?: SearchConstraints): SearchResult<T>[];\n /**\n * Subscribes `listener` to be called after every `add()` / `remove()` / `reindex()` call\n * that actually changes the index (no-ops — e.g. removing an unindexed item — don't fire\n * it). Returns an unsubscribe function.\n *\n * Framework-agnostic extension point: `createSearch()` uses this internally to keep\n * reactive `results` in sync with index mutations. Most callers won't need this directly.\n */\n onMutate(listener: () => void): () => void;\n}\n\nfunction resolveFields<T>(defs: ReadonlyArray<FieldDef<T>>): FieldConfig<T>[] {\n return defs.map((def) => {\n if (typeof def === 'string') {\n return { field: def, stringify: defaultStringify, weight: 1 };\n }\n\n return {\n field: def.field,\n stringify: def.stringify ?? defaultStringify,\n weight: def.weight ?? 1,\n };\n });\n}\n\n/**\n * Builds a trigram inverted index over `items` for fast fuzzy search.\n *\n * Construction is O(corpus × field_length). Subsequent `search()` calls are\n * O(candidates) — far faster than per-query Levenshtein for large corpora.\n *\n * @example\n * ```ts\n * const index = createIndex(users, {\n * fields: [{ field: 'name', weight: 2 }, 'email'],\n * threshold: 0.3,\n * limit: 20,\n * });\n *\n * const results = index.search('alice');\n * ```\n *\n * @throws {ScoutIndexError} If `options.fields` is empty.\n */\nexport function createIndex<T>(items: T[], options: ScoutIndexOptions<T>): ScoutIndex<T> {\n if (options.fields.length === 0) {\n throw new ScoutIndexError('createIndex: at least one field is required.');\n }\n\n const fields = resolveFields(options.fields);\n const maxWeight = fields.reduce((max, f) => Math.max(max, f.weight), 1);\n const defaultThreshold = options.threshold ?? 0.2;\n const defaultLimit = options.limit ?? 50;\n const defaultMinQueryLength = options.minQueryLength ?? 3;\n\n /** item → per-item record, preserves insertion order for `items` getter */\n const itemData = new Map<T, ItemRecord>();\n /** trigram → set of items that contain it */\n const invertedIndex = new Map<string, Set<T>>();\n const mutationListeners = new Set<() => void>();\n\n function notifyMutation(): void {\n for (const listener of mutationListeners) listener();\n }\n\n /** Single-entry cache for the most recent normalized query's trigrams (F2). */\n let cachedNormalized: string | null = null;\n let cachedTrigrams: Set<string> | null = null;\n\n function getQueryTrigrams(normalized: string): Set<string> {\n if (normalized === cachedNormalized && cachedTrigrams !== null) return cachedTrigrams;\n\n cachedNormalized = normalized;\n cachedTrigrams = generateTrigrams(normalized);\n\n return cachedTrigrams;\n }\n\n function addFieldToIndex(item: T, fieldTrigrams: Set<string>): void {\n for (const trigram of fieldTrigrams) {\n let bucket = invertedIndex.get(trigram);\n\n if (!bucket) {\n bucket = new Set<T>();\n invertedIndex.set(trigram, bucket);\n }\n\n bucket.add(item);\n }\n }\n\n function removeFieldFromIndex(item: T, fieldTrigrams: Set<string>): void {\n for (const trigram of fieldTrigrams) {\n const bucket = invertedIndex.get(trigram);\n\n if (bucket) {\n bucket.delete(item);\n\n if (bucket.size === 0) invertedIndex.delete(trigram);\n }\n }\n }\n\n function addItem(item: T): void {\n const trigrams = new Map<string, Set<string>>();\n const values = new Map<string, string>();\n\n for (const { field, stringify } of fields) {\n const raw = item[field];\n const text = stringify(raw);\n const normalized = tokenize(text);\n const fieldTrigrams = normalized.length >= 1 ? generateTrigrams(normalized) : new Set<string>();\n\n trigrams.set(field, fieldTrigrams);\n values.set(field, text);\n addFieldToIndex(item, fieldTrigrams);\n }\n\n itemData.set(item, { trigrams, values });\n }\n\n /**\n * Performs a full linear scan over all items for short queries.\n * O(n × field_count) — acceptable for small corpora; consider raising\n * `minQueryLength` on large datasets to avoid triggering this path.\n */\n function containmentScan(query: string): Set<T> {\n const result = new Set<T>();\n\n for (const [item, record] of itemData) {\n for (const value of record.values.values()) {\n if (value.toLowerCase().includes(query)) {\n result.add(item);\n break;\n }\n }\n }\n\n return result;\n }\n\n function trigramCandidates(queryTrigrams: Set<string>): Set<T> {\n const candidates = new Set<T>();\n\n for (const trigram of queryTrigrams) {\n const items = invertedIndex.get(trigram);\n\n if (items) {\n for (const item of items) candidates.add(item);\n }\n }\n\n return candidates;\n }\n\n function scoreCandidate(normalized: string, queryTrigrams: Set<string> | null, record: ItemRecord): number {\n let bestScore = 0;\n\n for (const { field, weight } of fields) {\n let fieldScore: number;\n\n if (queryTrigrams === null) {\n const raw = record.values.get(field) ?? '';\n\n fieldScore = raw.toLowerCase().includes(normalized) ? 1.0 : 0;\n } else {\n const itemTrigrams = record.trigrams.get(field);\n\n if (!itemTrigrams || itemTrigrams.size === 0) continue;\n\n fieldScore = diceSimilarity(queryTrigrams, itemTrigrams);\n }\n\n const weighted = fieldScore * (weight / maxWeight);\n\n if (weighted > bestScore) bestScore = weighted;\n }\n\n return bestScore;\n }\n\n function computeMatches(query: string, values: Map<string, string>): FieldMatch<keyof T & string>[] {\n const matches: FieldMatch<keyof T & string>[] = [];\n\n for (const { field } of fields) {\n const text = values.get(field);\n\n if (!text) continue;\n\n const ranges = findMatchRanges(text, query);\n\n if (ranges.length > 0) matches.push({ field, ranges });\n }\n\n return matches;\n }\n\n for (const item of items) {\n addItem(item);\n }\n\n return {\n add(item: T): void {\n if (itemData.has(item)) return;\n\n addItem(item);\n notifyMutation();\n },\n\n get items(): readonly T[] {\n return [...itemData.keys()];\n },\n\n onMutate(listener: () => void): () => void {\n mutationListeners.add(listener);\n\n return () => {\n mutationListeners.delete(listener);\n };\n },\n\n reindex(item: T): void {\n const record = itemData.get(item);\n\n if (!record) return;\n\n let changed = false;\n\n for (const { field, stringify } of fields) {\n const raw = item[field];\n const newText = stringify(raw);\n const oldText = record.values.get(field);\n\n if (newText === oldText) continue;\n\n changed = true;\n\n const oldTrigrams = record.trigrams.get(field);\n\n if (oldTrigrams) removeFieldFromIndex(item, oldTrigrams);\n\n const normalized = tokenize(newText);\n const newTrigrams = normalized.length >= 1 ? generateTrigrams(normalized) : new Set<string>();\n\n record.trigrams.set(field, newTrigrams);\n record.values.set(field, newText);\n addFieldToIndex(item, newTrigrams);\n }\n\n if (changed) notifyMutation();\n },\n\n remove(item: T): void {\n const record = itemData.get(item);\n\n if (!record) return;\n\n for (const fieldTrigrams of record.trigrams.values()) {\n removeFieldFromIndex(item, fieldTrigrams);\n }\n\n itemData.delete(item);\n notifyMutation();\n },\n\n search(query: string, options?: SearchConstraints): SearchResult<T>[] {\n const threshold = options?.threshold ?? defaultThreshold;\n const limit = Math.max(0, options?.limit ?? defaultLimit);\n const minQueryLength = options?.minQueryLength ?? defaultMinQueryLength;\n\n if (!query.trim()) {\n return [...itemData.keys()].slice(0, limit).map((item) => ({ item, matches: [], score: 1 }));\n }\n\n const normalized = tokenize(query);\n\n // Query had no indexable content (e.g. punctuation-only) — no match, not \"match all\".\n if (!normalized) return [];\n\n const isShort = normalized.length < minQueryLength;\n const queryTrigrams = isShort ? null : getQueryTrigrams(normalized);\n const candidates = queryTrigrams === null ? containmentScan(normalized) : trigramCandidates(queryTrigrams);\n const results: SearchResult<T>[] = [];\n\n for (const item of candidates) {\n const record = itemData.get(item);\n\n if (!record) continue;\n\n const score = scoreCandidate(normalized, queryTrigrams, record);\n\n if (score >= threshold) {\n const matches = computeMatches(normalized, record.values);\n\n results.push({ item, matches, score });\n }\n }\n\n return results.sort((a, b) => b.score - a.score).slice(0, limit);\n },\n\n get size(): number {\n return itemData.size;\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { ScoutIndexOptions, SearchConstraints, SearchResult } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* A stateful, indexed search corpus. Created via `createIndex()`.
|
|
4
|
+
*
|
|
5
|
+
* Supports incremental `add()`, `remove()`, and `reindex()` operations — each patches
|
|
6
|
+
* the trigram index in O(field_length) without a full rebuild.
|
|
7
|
+
*/
|
|
8
|
+
export interface ScoutIndex<T> {
|
|
9
|
+
/** All items currently in the index, in insertion order. */
|
|
10
|
+
readonly items: readonly T[];
|
|
11
|
+
/** Number of items currently in the index. */
|
|
12
|
+
readonly size: number;
|
|
13
|
+
/** Adds `item` to the index. No-op if the item is already indexed (by reference). */
|
|
14
|
+
add(item: T): void;
|
|
15
|
+
/**
|
|
16
|
+
* Re-reads the item's current field values and rebuilds its index entry in-place,
|
|
17
|
+
* only updating fields whose values have changed. Preserves insertion order.
|
|
18
|
+
* No-op if the item is not in the index.
|
|
19
|
+
*/
|
|
20
|
+
reindex(item: T): void;
|
|
21
|
+
/**
|
|
22
|
+
* Removes `item` from the index by reference equality.
|
|
23
|
+
* No-op if the item is not in the index.
|
|
24
|
+
*/
|
|
25
|
+
remove(item: T): void;
|
|
26
|
+
/**
|
|
27
|
+
* Searches the index for `query` and returns results sorted by score descending.
|
|
28
|
+
*
|
|
29
|
+
* An empty (or whitespace-only) `query` returns all indexed items with `score = 1`.
|
|
30
|
+
* A `query` with no indexable content after normalization (e.g. punctuation-only) returns
|
|
31
|
+
* no results. Results below `threshold` are excluded. At most `limit` results are returned.
|
|
32
|
+
*/
|
|
33
|
+
search(query: string, options?: SearchConstraints): SearchResult<T>[];
|
|
34
|
+
/**
|
|
35
|
+
* Subscribes `listener` to be called after every `add()` / `remove()` / `reindex()` call
|
|
36
|
+
* that actually changes the index (no-ops — e.g. removing an unindexed item — don't fire
|
|
37
|
+
* it). Returns an unsubscribe function.
|
|
38
|
+
*
|
|
39
|
+
* Framework-agnostic extension point: `createSearch()` uses this internally to keep
|
|
40
|
+
* reactive `results` in sync with index mutations. Most callers won't need this directly.
|
|
41
|
+
*/
|
|
42
|
+
onMutate(listener: () => void): () => void;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Builds a trigram inverted index over `items` for fast fuzzy search.
|
|
46
|
+
*
|
|
47
|
+
* Construction is O(corpus × field_length). Subsequent `search()` calls are
|
|
48
|
+
* O(candidates) — far faster than per-query Levenshtein for large corpora.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* const index = createIndex(users, {
|
|
53
|
+
* fields: [{ field: 'name', weight: 2 }, 'email'],
|
|
54
|
+
* threshold: 0.3,
|
|
55
|
+
* limit: 20,
|
|
56
|
+
* });
|
|
57
|
+
*
|
|
58
|
+
* const results = index.search('alice');
|
|
59
|
+
* ```
|
|
60
|
+
*
|
|
61
|
+
* @throws {ScoutIndexError} If `options.fields` is empty.
|
|
62
|
+
*/
|
|
63
|
+
export declare function createIndex<T>(items: T[], options: ScoutIndexOptions<T>): ScoutIndex<T>;
|
|
64
|
+
//# sourceMappingURL=scout-index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scout-index.d.ts","sourceRoot":"","sources":["../src/scout-index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAwB,iBAAiB,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAoBxG;;;;;GAKG;AACH,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,4DAA4D;IAC5D,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,CAAC;IAC7B,8CAA8C;IAC9C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,qFAAqF;IACrF,GAAG,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;IACnB;;;;OAIG;IACH,OAAO,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;IACvB;;;OAGG;IACH,MAAM,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;IACtB;;;;;;OAMG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC;IACtE;;;;;;;OAOG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;CAC5C;AAgBD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAqQvF"}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { ScoutIndexError as e } from "./errors.js";
|
|
2
|
+
import { findMatchRanges as t } from "./highlight.js";
|
|
3
|
+
import { defaultStringify as n, tokenize as r } from "./tokenize.js";
|
|
4
|
+
import { diceSimilarity as i, generateTrigrams as a } from "./trigram.js";
|
|
5
|
+
//#region src/scout-index.ts
|
|
6
|
+
function o(e) {
|
|
7
|
+
return e.map((e) => typeof e == "string" ? {
|
|
8
|
+
field: e,
|
|
9
|
+
stringify: n,
|
|
10
|
+
weight: 1
|
|
11
|
+
} : {
|
|
12
|
+
field: e.field,
|
|
13
|
+
stringify: e.stringify ?? n,
|
|
14
|
+
weight: e.weight ?? 1
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function s(n, s) {
|
|
18
|
+
if (s.fields.length === 0) throw new e("createIndex: at least one field is required.");
|
|
19
|
+
let c = o(s.fields), l = c.reduce((e, t) => Math.max(e, t.weight), 1), u = s.threshold ?? .2, d = s.limit ?? 50, f = s.minQueryLength ?? 3, p = /* @__PURE__ */ new Map(), m = /* @__PURE__ */ new Map(), h = /* @__PURE__ */ new Set();
|
|
20
|
+
function g() {
|
|
21
|
+
for (let e of h) e();
|
|
22
|
+
}
|
|
23
|
+
let _ = null, v = null;
|
|
24
|
+
function y(e) {
|
|
25
|
+
return e === _ && v !== null ? v : (_ = e, v = a(e), v);
|
|
26
|
+
}
|
|
27
|
+
function b(e, t) {
|
|
28
|
+
for (let n of t) {
|
|
29
|
+
let t = m.get(n);
|
|
30
|
+
t || (t = /* @__PURE__ */ new Set(), m.set(n, t)), t.add(e);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function x(e, t) {
|
|
34
|
+
for (let n of t) {
|
|
35
|
+
let t = m.get(n);
|
|
36
|
+
t && (t.delete(e), t.size === 0 && m.delete(n));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function S(e) {
|
|
40
|
+
let t = /* @__PURE__ */ new Map(), n = /* @__PURE__ */ new Map();
|
|
41
|
+
for (let { field: i, stringify: o } of c) {
|
|
42
|
+
let s = e[i], c = o(s), l = r(c), u = l.length >= 1 ? a(l) : /* @__PURE__ */ new Set();
|
|
43
|
+
t.set(i, u), n.set(i, c), b(e, u);
|
|
44
|
+
}
|
|
45
|
+
p.set(e, {
|
|
46
|
+
trigrams: t,
|
|
47
|
+
values: n
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function C(e) {
|
|
51
|
+
let t = /* @__PURE__ */ new Set();
|
|
52
|
+
for (let [n, r] of p) for (let i of r.values.values()) if (i.toLowerCase().includes(e)) {
|
|
53
|
+
t.add(n);
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
return t;
|
|
57
|
+
}
|
|
58
|
+
function w(e) {
|
|
59
|
+
let t = /* @__PURE__ */ new Set();
|
|
60
|
+
for (let n of e) {
|
|
61
|
+
let e = m.get(n);
|
|
62
|
+
if (e) for (let n of e) t.add(n);
|
|
63
|
+
}
|
|
64
|
+
return t;
|
|
65
|
+
}
|
|
66
|
+
function T(e, t, n) {
|
|
67
|
+
let r = 0;
|
|
68
|
+
for (let { field: a, weight: o } of c) {
|
|
69
|
+
let s;
|
|
70
|
+
if (t === null) s = +!!(n.values.get(a) ?? "").toLowerCase().includes(e);
|
|
71
|
+
else {
|
|
72
|
+
let e = n.trigrams.get(a);
|
|
73
|
+
if (!e || e.size === 0) continue;
|
|
74
|
+
s = i(t, e);
|
|
75
|
+
}
|
|
76
|
+
let c = o / l * s;
|
|
77
|
+
c > r && (r = c);
|
|
78
|
+
}
|
|
79
|
+
return r;
|
|
80
|
+
}
|
|
81
|
+
function E(e, n) {
|
|
82
|
+
let r = [];
|
|
83
|
+
for (let { field: i } of c) {
|
|
84
|
+
let a = n.get(i);
|
|
85
|
+
if (!a) continue;
|
|
86
|
+
let o = t(a, e);
|
|
87
|
+
o.length > 0 && r.push({
|
|
88
|
+
field: i,
|
|
89
|
+
ranges: o
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return r;
|
|
93
|
+
}
|
|
94
|
+
for (let e of n) S(e);
|
|
95
|
+
return {
|
|
96
|
+
add(e) {
|
|
97
|
+
p.has(e) || (S(e), g());
|
|
98
|
+
},
|
|
99
|
+
get items() {
|
|
100
|
+
return [...p.keys()];
|
|
101
|
+
},
|
|
102
|
+
onMutate(e) {
|
|
103
|
+
return h.add(e), () => {
|
|
104
|
+
h.delete(e);
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
reindex(e) {
|
|
108
|
+
let t = p.get(e);
|
|
109
|
+
if (!t) return;
|
|
110
|
+
let n = !1;
|
|
111
|
+
for (let { field: i, stringify: o } of c) {
|
|
112
|
+
let s = e[i], c = o(s);
|
|
113
|
+
if (c === t.values.get(i)) continue;
|
|
114
|
+
n = !0;
|
|
115
|
+
let l = t.trigrams.get(i);
|
|
116
|
+
l && x(e, l);
|
|
117
|
+
let u = r(c), d = u.length >= 1 ? a(u) : /* @__PURE__ */ new Set();
|
|
118
|
+
t.trigrams.set(i, d), t.values.set(i, c), b(e, d);
|
|
119
|
+
}
|
|
120
|
+
n && g();
|
|
121
|
+
},
|
|
122
|
+
remove(e) {
|
|
123
|
+
let t = p.get(e);
|
|
124
|
+
if (t) {
|
|
125
|
+
for (let n of t.trigrams.values()) x(e, n);
|
|
126
|
+
p.delete(e), g();
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
search(e, t) {
|
|
130
|
+
let n = t?.threshold ?? u, i = Math.max(0, t?.limit ?? d), a = t?.minQueryLength ?? f;
|
|
131
|
+
if (!e.trim()) return [...p.keys()].slice(0, i).map((e) => ({
|
|
132
|
+
item: e,
|
|
133
|
+
matches: [],
|
|
134
|
+
score: 1
|
|
135
|
+
}));
|
|
136
|
+
let o = r(e);
|
|
137
|
+
if (!o) return [];
|
|
138
|
+
let s = o.length < a ? null : y(o), c = s === null ? C(o) : w(s), l = [];
|
|
139
|
+
for (let e of c) {
|
|
140
|
+
let t = p.get(e);
|
|
141
|
+
if (!t) continue;
|
|
142
|
+
let r = T(o, s, t);
|
|
143
|
+
if (r >= n) {
|
|
144
|
+
let n = E(o, t.values);
|
|
145
|
+
l.push({
|
|
146
|
+
item: e,
|
|
147
|
+
matches: n,
|
|
148
|
+
score: r
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return l.sort((e, t) => t.score - e.score).slice(0, i);
|
|
153
|
+
},
|
|
154
|
+
get size() {
|
|
155
|
+
return p.size;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
//#endregion
|
|
160
|
+
export { s as createIndex };
|
|
161
|
+
|
|
162
|
+
//# sourceMappingURL=scout-index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scout-index.js","names":[],"sources":["../src/scout-index.ts"],"sourcesContent":["import type { FieldDef, FieldMatch, ScoutIndexOptions, SearchConstraints, SearchResult } from './types';\n\nimport { ScoutIndexError } from './errors';\nimport { findMatchRanges } from './highlight';\nimport { defaultStringify, tokenize } from './tokenize';\nimport { diceSimilarity, generateTrigrams } from './trigram';\n\ntype FieldConfig<T> = {\n field: keyof T & string;\n stringify: (v: unknown) => string;\n weight: number;\n};\n\ntype ItemRecord = {\n /** Per-field trigrams for scoring. */\n trigrams: Map<string, Set<string>>;\n /** Per-field original text for highlighting. */\n values: Map<string, string>;\n};\n\n/**\n * A stateful, indexed search corpus. Created via `createIndex()`.\n *\n * Supports incremental `add()`, `remove()`, and `reindex()` operations — each patches\n * the trigram index in O(field_length) without a full rebuild.\n */\nexport interface ScoutIndex<T> {\n /** All items currently in the index, in insertion order. */\n readonly items: readonly T[];\n /** Number of items currently in the index. */\n readonly size: number;\n /** Adds `item` to the index. No-op if the item is already indexed (by reference). */\n add(item: T): void;\n /**\n * Re-reads the item's current field values and rebuilds its index entry in-place,\n * only updating fields whose values have changed. Preserves insertion order.\n * No-op if the item is not in the index.\n */\n reindex(item: T): void;\n /**\n * Removes `item` from the index by reference equality.\n * No-op if the item is not in the index.\n */\n remove(item: T): void;\n /**\n * Searches the index for `query` and returns results sorted by score descending.\n *\n * An empty (or whitespace-only) `query` returns all indexed items with `score = 1`.\n * A `query` with no indexable content after normalization (e.g. punctuation-only) returns\n * no results. Results below `threshold` are excluded. At most `limit` results are returned.\n */\n search(query: string, options?: SearchConstraints): SearchResult<T>[];\n /**\n * Subscribes `listener` to be called after every `add()` / `remove()` / `reindex()` call\n * that actually changes the index (no-ops — e.g. removing an unindexed item — don't fire\n * it). Returns an unsubscribe function.\n *\n * Framework-agnostic extension point: `createSearch()` uses this internally to keep\n * reactive `results` in sync with index mutations. Most callers won't need this directly.\n */\n onMutate(listener: () => void): () => void;\n}\n\nfunction resolveFields<T>(defs: ReadonlyArray<FieldDef<T>>): FieldConfig<T>[] {\n return defs.map((def) => {\n if (typeof def === 'string') {\n return { field: def, stringify: defaultStringify, weight: 1 };\n }\n\n return {\n field: def.field,\n stringify: def.stringify ?? defaultStringify,\n weight: def.weight ?? 1,\n };\n });\n}\n\n/**\n * Builds a trigram inverted index over `items` for fast fuzzy search.\n *\n * Construction is O(corpus × field_length). Subsequent `search()` calls are\n * O(candidates) — far faster than per-query Levenshtein for large corpora.\n *\n * @example\n * ```ts\n * const index = createIndex(users, {\n * fields: [{ field: 'name', weight: 2 }, 'email'],\n * threshold: 0.3,\n * limit: 20,\n * });\n *\n * const results = index.search('alice');\n * ```\n *\n * @throws {ScoutIndexError} If `options.fields` is empty.\n */\nexport function createIndex<T>(items: T[], options: ScoutIndexOptions<T>): ScoutIndex<T> {\n if (options.fields.length === 0) {\n throw new ScoutIndexError('createIndex: at least one field is required.');\n }\n\n const fields = resolveFields(options.fields);\n const maxWeight = fields.reduce((max, f) => Math.max(max, f.weight), 1);\n const defaultThreshold = options.threshold ?? 0.2;\n const defaultLimit = options.limit ?? 50;\n const defaultMinQueryLength = options.minQueryLength ?? 3;\n\n /** item → per-item record, preserves insertion order for `items` getter */\n const itemData = new Map<T, ItemRecord>();\n /** trigram → set of items that contain it */\n const invertedIndex = new Map<string, Set<T>>();\n const mutationListeners = new Set<() => void>();\n\n function notifyMutation(): void {\n for (const listener of mutationListeners) listener();\n }\n\n /** Single-entry cache for the most recent normalized query's trigrams (F2). */\n let cachedNormalized: string | null = null;\n let cachedTrigrams: Set<string> | null = null;\n\n function getQueryTrigrams(normalized: string): Set<string> {\n if (normalized === cachedNormalized && cachedTrigrams !== null) return cachedTrigrams;\n\n cachedNormalized = normalized;\n cachedTrigrams = generateTrigrams(normalized);\n\n return cachedTrigrams;\n }\n\n function addFieldToIndex(item: T, fieldTrigrams: Set<string>): void {\n for (const trigram of fieldTrigrams) {\n let bucket = invertedIndex.get(trigram);\n\n if (!bucket) {\n bucket = new Set<T>();\n invertedIndex.set(trigram, bucket);\n }\n\n bucket.add(item);\n }\n }\n\n function removeFieldFromIndex(item: T, fieldTrigrams: Set<string>): void {\n for (const trigram of fieldTrigrams) {\n const bucket = invertedIndex.get(trigram);\n\n if (bucket) {\n bucket.delete(item);\n\n if (bucket.size === 0) invertedIndex.delete(trigram);\n }\n }\n }\n\n function addItem(item: T): void {\n const trigrams = new Map<string, Set<string>>();\n const values = new Map<string, string>();\n\n for (const { field, stringify } of fields) {\n const raw = item[field];\n const text = stringify(raw);\n const normalized = tokenize(text);\n const fieldTrigrams = normalized.length >= 1 ? generateTrigrams(normalized) : new Set<string>();\n\n trigrams.set(field, fieldTrigrams);\n values.set(field, text);\n addFieldToIndex(item, fieldTrigrams);\n }\n\n itemData.set(item, { trigrams, values });\n }\n\n /**\n * Performs a full linear scan over all items for short queries.\n * O(n × field_count) — acceptable for small corpora; consider raising\n * `minQueryLength` on large datasets to avoid triggering this path.\n */\n function containmentScan(query: string): Set<T> {\n const result = new Set<T>();\n\n for (const [item, record] of itemData) {\n for (const value of record.values.values()) {\n if (value.toLowerCase().includes(query)) {\n result.add(item);\n break;\n }\n }\n }\n\n return result;\n }\n\n function trigramCandidates(queryTrigrams: Set<string>): Set<T> {\n const candidates = new Set<T>();\n\n for (const trigram of queryTrigrams) {\n const items = invertedIndex.get(trigram);\n\n if (items) {\n for (const item of items) candidates.add(item);\n }\n }\n\n return candidates;\n }\n\n function scoreCandidate(normalized: string, queryTrigrams: Set<string> | null, record: ItemRecord): number {\n let bestScore = 0;\n\n for (const { field, weight } of fields) {\n let fieldScore: number;\n\n if (queryTrigrams === null) {\n const raw = record.values.get(field) ?? '';\n\n fieldScore = raw.toLowerCase().includes(normalized) ? 1.0 : 0;\n } else {\n const itemTrigrams = record.trigrams.get(field);\n\n if (!itemTrigrams || itemTrigrams.size === 0) continue;\n\n fieldScore = diceSimilarity(queryTrigrams, itemTrigrams);\n }\n\n const weighted = fieldScore * (weight / maxWeight);\n\n if (weighted > bestScore) bestScore = weighted;\n }\n\n return bestScore;\n }\n\n function computeMatches(query: string, values: Map<string, string>): FieldMatch<keyof T & string>[] {\n const matches: FieldMatch<keyof T & string>[] = [];\n\n for (const { field } of fields) {\n const text = values.get(field);\n\n if (!text) continue;\n\n const ranges = findMatchRanges(text, query);\n\n if (ranges.length > 0) matches.push({ field, ranges });\n }\n\n return matches;\n }\n\n for (const item of items) {\n addItem(item);\n }\n\n return {\n add(item: T): void {\n if (itemData.has(item)) return;\n\n addItem(item);\n notifyMutation();\n },\n\n get items(): readonly T[] {\n return [...itemData.keys()];\n },\n\n onMutate(listener: () => void): () => void {\n mutationListeners.add(listener);\n\n return () => {\n mutationListeners.delete(listener);\n };\n },\n\n reindex(item: T): void {\n const record = itemData.get(item);\n\n if (!record) return;\n\n let changed = false;\n\n for (const { field, stringify } of fields) {\n const raw = item[field];\n const newText = stringify(raw);\n const oldText = record.values.get(field);\n\n if (newText === oldText) continue;\n\n changed = true;\n\n const oldTrigrams = record.trigrams.get(field);\n\n if (oldTrigrams) removeFieldFromIndex(item, oldTrigrams);\n\n const normalized = tokenize(newText);\n const newTrigrams = normalized.length >= 1 ? generateTrigrams(normalized) : new Set<string>();\n\n record.trigrams.set(field, newTrigrams);\n record.values.set(field, newText);\n addFieldToIndex(item, newTrigrams);\n }\n\n if (changed) notifyMutation();\n },\n\n remove(item: T): void {\n const record = itemData.get(item);\n\n if (!record) return;\n\n for (const fieldTrigrams of record.trigrams.values()) {\n removeFieldFromIndex(item, fieldTrigrams);\n }\n\n itemData.delete(item);\n notifyMutation();\n },\n\n search(query: string, options?: SearchConstraints): SearchResult<T>[] {\n const threshold = options?.threshold ?? defaultThreshold;\n const limit = Math.max(0, options?.limit ?? defaultLimit);\n const minQueryLength = options?.minQueryLength ?? defaultMinQueryLength;\n\n if (!query.trim()) {\n return [...itemData.keys()].slice(0, limit).map((item) => ({ item, matches: [], score: 1 }));\n }\n\n const normalized = tokenize(query);\n\n // Query had no indexable content (e.g. punctuation-only) — no match, not \"match all\".\n if (!normalized) return [];\n\n const isShort = normalized.length < minQueryLength;\n const queryTrigrams = isShort ? null : getQueryTrigrams(normalized);\n const candidates = queryTrigrams === null ? containmentScan(normalized) : trigramCandidates(queryTrigrams);\n const results: SearchResult<T>[] = [];\n\n for (const item of candidates) {\n const record = itemData.get(item);\n\n if (!record) continue;\n\n const score = scoreCandidate(normalized, queryTrigrams, record);\n\n if (score >= threshold) {\n const matches = computeMatches(normalized, record.values);\n\n results.push({ item, matches, score });\n }\n }\n\n return results.sort((a, b) => b.score - a.score).slice(0, limit);\n },\n\n get size(): number {\n return itemData.size;\n },\n };\n}\n"],"mappings":"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"}
|
package/dist/scout.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});function e(e,t){return(n,r)=>e.search(r,t).map(e=>e.item)}function t(e,t,n){let r=new Set(e.search(t,n).map(e=>e.item));return e=>r.has(e)}var n=class e extends Error{constructor(e,t){super(e,t),this.name=new.target.name,Object.setPrototypeOf(this,new.target.prototype)}static is(t){return t instanceof e}},r=class extends n{},i=class extends n{};function a(e,t){let n=e.toLowerCase(),r=t.split(` `).filter(Boolean),i=[];for(let e of r){let t=0;for(;t<n.length;){let r=n.indexOf(e,t);if(r===-1)break;i.push([r,r+e.length]),t=r+1}}i.sort((e,t)=>e[0]-t[0]);let a=[];for(let e of i){let t=a[a.length-1];t&&e[0]<=t[1]?t[1]=Math.max(t[1],e[1]):a.push([e[0],e[1]])}return a}function o(e,t){if(!e)return[];if(!t.length)return[{highlighted:!1,text:e}];let n=[],r=0;for(let[i,a]of t)i>r&&n.push({highlighted:!1,text:e.slice(r,i)}),a>i&&n.push({highlighted:!0,text:e.slice(i,a)}),r=a;return r<e.length&&n.push({highlighted:!1,text:e.slice(r)}),n}function s(e,t,n){return o(n,e.matches.find(e=>e.field===t)?.ranges??[])}var c=null,l=()=>c,u=class e extends Error{constructor(e,t){super(e,t),this.name=new.target.name,Object.setPrototypeOf(this,new.target.prototype)}static is(t){return t instanceof e}},d=class extends u{},f=class extends u{},p=0,m=()=>++p,h=()=>p,g={scheduling:{activeDirty:`a`,batchDepth:0,dirtyWithEffectSubsA:new Set,dirtyWithEffectSubsB:new Set,pendingSubscribers:new Set},scopeCleanups:null,tracking:null},_=null,v=()=>_===null?g:_.get(),y=()=>v().scheduling,b=()=>_!==null,x=()=>v().tracking,S=(e,t)=>{if(_!==null){let n=_.get();return _.run({...n,tracking:e},t)}let n=g;g={...g,tracking:e};try{return t()}finally{g=n}},C=()=>v().scopeCleanups,w=e=>{let t=x();t?.kind===`effect`?t.cleanups.push(e):C()?.push(e)},T=e=>{let t=x();if(t!==null){if(t.sourceObserver?.(e),t.kind===`computed`)t.depCollector.push({source:e,version:e.version});else if(t.kind===`effect`){let n=t.effect;e.addEffectSub(n),t.subscriptions.add(()=>e.removeEffectSub(n)),t.deps.set(e,e.version)}}},E=e=>e instanceof Error?e:Error(`Non-Error thrown: ${String(e)}`),D=e=>{let t=[];for(let n of e)try{n()}catch(e){t.push(E(e))}return t},ee=(e,t)=>{let n=D(e);if(n.length===1)throw n[0];if(n.length>0)throw AggregateError(n,t)},te=!globalThis.__RIPPLE_PROD__;function O(e){te&&console.warn(`[@vielzeug/ripple] ${e}`)}var k=!1,A=0,j=[],M=e=>e.activeDirty===`a`?e.dirtyWithEffectSubsA:e.dirtyWithEffectSubsB,ne=(e,t)=>{let n=++A;for(let n of t.effectSubs())e.pendingSubscribers.add(n);for(let e of t.computedSubs())j.push(e);try{for(;j.length>0;){let t=j.pop();if(t.lastPropEpoch_!==n&&(t.lastPropEpoch_=n,t.markDirty())){t.effectSubs().size>0&&M(e).add(t);for(let e of t.computedSubs())j.push(e)}}}finally{j.length=0}},re=e=>{for(;M(e).size>0;){let t=M(e);e.activeDirty=e.activeDirty===`a`?`b`:`a`,M(e).clear();for(let n of t)if(n.hasSubscribers()&&n.refreshIfDirty()){for(let t of n.effectSubs())e.pendingSubscribers.add(t);for(let t of n.computedSubs())t.markDirty()&&t.effectSubs().size>0&&M(e).add(t)}t.clear()}},N=e=>{let t=0;for(;e.pendingSubscribers.size>0||M(e).size>0;){if(++t>100)throw new f(`infinite flush loop (> 100 iterations)`);if(M(e).size>0&&re(e),e.pendingSubscribers.size===0)continue;let n=[...e.pendingSubscribers];e.pendingSubscribers.clear(),ee(n,`subscriber errors`)}},ie=globalThis.process?.versions!=null,ae=e=>{if(!e.hasSubscribers())return;!k&&ie&&!b()&&(k=!0,O(`Signal updated in a Node.js-like environment. The module-level flush queue is shared across concurrent requests — use per-request worker isolation or the @vielzeug/ripple/ssr sub-path for request-isolated scheduling.`));let t=y();ne(t,e),t.batchDepth===0&&N(t)},oe=e=>{let t=y();t.batchDepth++;let n;try{n=e()}catch(e){throw t.batchDepth--,t.batchDepth===0&&(t.pendingSubscribers.clear(),t.dirtyWithEffectSubsA.clear(),t.dirtyWithEffectSubsB.clear()),e}return t.batchDepth--,t.batchDepth===0&&N(t),n},P=class{fn_;disposed_=!1;constructor(e){this.fn_=e}get disposed(){return this.disposed_}dispose(){if(this.disposed_)return;this.disposed_=!0;let e=this.fn_;this.fn_=null,e()}[Symbol.dispose](){this.dispose()}},F=Symbol(`ripple.is-signal`),I=Symbol(`ripple.is-computed`),L=Symbol(`ripple.uninitialized`),R=new FinalizationRegistry(({key:e,map:t})=>{t.delete(e)}),z=class{version=0;name;[F]=!0;computedSubs_=new Map;effectSubs_=new Set;constructor(e){this.name=e}addComputedSub(e){let t=new WeakRef(e);this.computedSubs_.set(e,t),R.register(e,{key:e,map:this.computedSubs_},t)}removeComputedSub(e){let t=this.computedSubs_.get(e);t!==void 0&&(this.computedSubs_.delete(e),R.unregister(t))}addEffectSub(e){this.effectSubs_.add(e)}removeEffectSub(e){this.effectSubs_.delete(e)}clearSubscribers(){for(let e of this.computedSubs_.values())R.unregister(e);this.computedSubs_.clear(),this.effectSubs_.clear()}hasSubscribers(){if(this.effectSubs_.size>0)return!0;for(let e of this.computedSubs_.values())if(e.deref()!==void 0)return!0;return!1}*computedSubs(){for(let[e,t]of this.computedSubs_){let n=t.deref();n===void 0?(this.computedSubs_.delete(e),R.unregister(t)):yield n}}effectSubs(){return this.effectSubs_}},B=class extends z{[I]=!0;lastPropEpoch_=0},V=class extends B{value_;dirty_;computing_;disposed_;deps_;compute_;equals_;maxRevision_;constructor(e,t){let{equals:n,name:r}=t??{};super(r),this.value_=L,this.dirty_=!0,this.computing_=!1,this.disposed_=!1,this.deps_=[],this.maxRevision_=-1,this.compute_=e,this.equals_=n===void 0?Object.is:(e,t)=>n(e,t)}markDirty(){return this.disposed_||this.dirty_?!1:(this.dirty_=!0,!0)}refreshIfDirty(){if(!this.dirty_)return!1;if(h()<=this.maxRevision_)return this.dirty_=!1,!1;if(this.deps_.length>0){let e=!0;for(let t of this.deps_){let n=t.source;if(`refreshIfDirty`in n&&n.refreshIfDirty(),n.version!==t.version){e=!1;break}}if(e)return this.dirty_=!1,this.maxRevision_=h(),!1}return this.recompute()}runCompute(e){try{return S({computed:this,depCollector:e,kind:`computed`},this.compute_)}catch(e){throw E(e)}}recompute(){if(this.computing_)throw new d(`computed cycle detected${this.name?` "${this.name}"`:``}`);this.computing_=!0;try{let e=[];l()?.compute?.({name:this.name});let t=this.runCompute(e);return this.dirty_=!1,this.maxRevision_=h(),this.updateDeps(e),this.value_===L||!this.equals_(this.value_,t)?(this.value_=t,this.version++,!0):!1}finally{this.computing_=!1}}updateDeps(e){let t=this.deps_;if(t.length===e.length&&t.every((t,n)=>t.source===e[n].source)){for(let n=0;n<e.length;n++)t[n].version=e[n].version;return}let n=new Set(t.map(e=>e.source)),r=new Set(e.map(e=>e.source));for(let e of t)r.has(e.source)||e.source.removeComputedSub(this);for(let t of e)n.has(t.source)||t.source.addComputedSub(this);this.deps_=e}get value(){return this.disposed_?this.value_===L?void 0:this.value_:(this.refreshIfDirty(),T(this),this.value_)}peek(){return this.disposed_?this.value_===L?void 0:this.value_:(this.refreshIfDirty(),this.value_)}subscribe=e=>{if(this.disposed_){let e=new P(()=>{});return e.dispose(),e}return this.refreshIfDirty(),this.addEffectSub(e),new P(()=>{this.removeEffectSub(e)})};get disposed(){return this.disposed_}dispose(){if(!this.disposed_){this.disposed_=!0;for(let e of this.deps_)e.source.removeComputedSub(this);this.deps_=[],this.clearSubscribers(),l()?.dispose?.({kind:`computed`,name:this.name})}}[Symbol.dispose](){this.dispose()}},H=(e,t)=>{let n=new V(e,t);return w(()=>n.dispose()),n},U=class extends z{value_;equals_;disposed_;constructor(e,t,n){super(n),this.value_=e,this.equals_=t??Object.is,this.disposed_=!1}get value(){return this.disposed_||T(this),this.value_}set value(e){if(this.disposed_||this.equals_(this.value_,e))return;let t=this.value_;this.value_=e,this.version=m(),l()?.write?.({name:this.name,newValue:e,oldValue:t}),ae(this)}peek(){return this.value_}subscribe=e=>{if(this.disposed_){let e=new P(()=>{});return e.dispose(),e}return this.addEffectSub(e),new P(()=>{this.removeEffectSub(e)})};get disposed(){return this.disposed_}dispose(){this.disposed_||(this.disposed_=!0,this.clearSubscribers(),l()?.dispose?.({kind:`signal`,name:this.name}))}[Symbol.dispose](){this.dispose()}},W=(e,t)=>new U(e,t?.equals,t?.name),G=!globalThis.__SCOUT_PROD__;function K(e){G&&console.warn(`[@vielzeug/scout] ${e}`)}function q(e){return e.toLowerCase().replace(/[^\p{L}\p{N}\s']/gu,` `).trim().replace(/\s+/g,` `)}function J(e){return e==null?``:typeof e==`string`?e:typeof e==`number`||typeof e==`boolean`?String(e):Array.isArray(e)?(K("defaultStringify: received an array value — provide a custom `stringify` on the FieldDef to control array indexing.\nFalling back to joining string/number/boolean elements with a space."),e.filter(e=>typeof e==`string`||typeof e==`number`||typeof e==`boolean`).join(` `)):``}function Y(e){let t=new Set,n=` ${e} `;for(let e=0;e<n.length-2;e++)t.add(n.slice(e,e+3));return t}function se(e,t){if(e.size===0||t.size===0)return 0;let n=0;for(let r of e)t.has(r)&&n++;return 2*n/(e.size+t.size)}function ce(e){return e.map(e=>typeof e==`string`?{field:e,stringify:J,weight:1}:{field:e.field,stringify:e.stringify??J,weight:e.weight??1})}function X(e,t){if(t.fields.length===0)throw new i(`createIndex: at least one field is required.`);let n=ce(t.fields),r=n.reduce((e,t)=>Math.max(e,t.weight),1),o=t.threshold??.2,s=t.limit??50,c=t.minQueryLength??3,l=new Map,u=new Map,d=new Set;function f(){for(let e of d)e()}let p=null,m=null;function h(e){return e===p&&m!==null?m:(p=e,m=Y(e),m)}function g(e,t){for(let n of t){let t=u.get(n);t||(t=new Set,u.set(n,t)),t.add(e)}}function _(e,t){for(let n of t){let t=u.get(n);t&&(t.delete(e),t.size===0&&u.delete(n))}}function v(e){let t=new Map,r=new Map;for(let{field:i,stringify:a}of n){let n=e[i],o=a(n),s=q(o),c=s.length>=1?Y(s):new Set;t.set(i,c),r.set(i,o),g(e,c)}l.set(e,{trigrams:t,values:r})}function y(e){let t=new Set;for(let[n,r]of l)for(let i of r.values.values())if(i.toLowerCase().includes(e)){t.add(n);break}return t}function b(e){let t=new Set;for(let n of e){let e=u.get(n);if(e)for(let n of e)t.add(n)}return t}function x(e,t,i){let a=0;for(let{field:o,weight:s}of n){let n;if(t===null)n=+!!(i.values.get(o)??``).toLowerCase().includes(e);else{let e=i.trigrams.get(o);if(!e||e.size===0)continue;n=se(t,e)}let c=s/r*n;c>a&&(a=c)}return a}function S(e,t){let r=[];for(let{field:i}of n){let n=t.get(i);if(!n)continue;let o=a(n,e);o.length>0&&r.push({field:i,ranges:o})}return r}for(let t of e)v(t);return{add(e){l.has(e)||(v(e),f())},get items(){return[...l.keys()]},onMutate(e){return d.add(e),()=>{d.delete(e)}},reindex(e){let t=l.get(e);if(!t)return;let r=!1;for(let{field:i,stringify:a}of n){let n=e[i],o=a(n);if(o===t.values.get(i))continue;r=!0;let s=t.trigrams.get(i);s&&_(e,s);let c=q(o),l=c.length>=1?Y(c):new Set;t.trigrams.set(i,l),t.values.set(i,o),g(e,l)}r&&f()},remove(e){let t=l.get(e);if(t){for(let n of t.trigrams.values())_(e,n);l.delete(e),f()}},search(e,t){let n=t?.threshold??o,r=Math.max(0,t?.limit??s),i=t?.minQueryLength??c;if(!e.trim())return[...l.keys()].slice(0,r).map(e=>({item:e,matches:[],score:1}));let a=q(e);if(!a)return[];let u=a.length<i?null:h(a),d=u===null?y(a):b(u),f=[];for(let e of d){let t=l.get(e);if(!t)continue;let r=x(a,u,t);if(r>=n){let n=S(a,t.values);f.push({item:e,matches:n,score:r})}}return f.sort((e,t)=>t.score-e.score).slice(0,r)},get size(){return l.size}}}var le=200;function Z(e,t={}){let{debounce:n=le,limit:i,minQueryLength:a,threshold:o}=t,s=W(``,{name:`scout:query`}),c=W(``,{name:`scout:committedQuery`}),l=W(0,{name:`scout:indexVersion`}),u=e.onMutate(()=>{l.value++}),d=H(()=>s.value!==c.value,{name:`scout:isSearching`}),f=H(()=>(l.value,e.search(c.value,{limit:i,minQueryLength:a,threshold:o})),{name:`scout:results`}),p=null;function m(){p!==null&&(clearTimeout(p),p=null)}let h=s.subscribe(()=>{let e=s.peek();if(m(),e!==c.peek()){if(n===0){c.value=e;return}p=setTimeout(()=>{c.value=e,p=null},n)}});function g(){if(_)throw new r(`SearchState.clear() called after dispose()`);m(),oe(()=>{s.value=``,c.value=``})}let _=!1,v=new AbortController;function y(){_=!0,v.abort(),m(),h.dispose(),u(),s.dispose(),c.dispose(),d.dispose(),f.dispose(),l.dispose()}return{clear:g,get disposalSignal(){return v.signal},dispose:y,get disposed(){return _},isSearching:d,query:s,results:f,[Symbol.dispose](){y()}}}function ue(e,t){let n=X(e,{fields:t.fields,limit:t.limit,minQueryLength:t.minQueryLength,threshold:t.threshold});return{...Z(n,{debounce:t.debounce}),index:n}}function Q(e){let t=de();return t?[...t.segment(e)].filter(e=>e.isWordLike).map(e=>e.segment).join(` `):e}var $;function de(){return $===void 0&&($=typeof Intl>`u`||typeof Intl.Segmenter!=`function`?null:new Intl.Segmenter(void 0,{granularity:`word`})),$}exports.ScoutDisposedError=r,exports.ScoutError=n,exports.ScoutIndexError=i,exports.createIndex=X,exports.createReactiveSearch=ue,exports.createSearch=Z,exports.findMatchRanges=a,exports.highlight=o,exports.highlightField=s,exports.segmentWords=Q,exports.toFilterPredicate=t,exports.toSearchFn=e;
|
|
2
|
+
//# sourceMappingURL=scout.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scout.cjs","names":["e","t","n","e","t","n","r","i","a","o","e","t","n","r","i","a","o","s","c","l","u","d","f","p","m","e","t","n","r","e","t","o","s","u","d","t","e","i","n","r","e","t","n","r","t","e","l","c","i","o","t","n","a","s","e","r","a","n","t","e","r","i"],"sources":["../src/adapters.ts","../src/errors.ts","../src/highlight.ts","../../ripple/dist/devtools-hook.js","../../ripple/dist/errors.js","../../ripple/dist/tracking.js","../../ripple/dist/_error-utils.js","../../ripple/dist/_dev.js","../../ripple/dist/scheduling.js","../../ripple/dist/subscription.js","../../ripple/dist/symbols.js","../../ripple/dist/reactive-base.js","../../ripple/dist/computed.js","../../ripple/dist/signal.js","../src/_dev.ts","../src/tokenize.ts","../src/trigram.ts","../src/scout-index.ts","../src/reactive.ts","../src/segment.ts"],"sourcesContent":["import type { ScoutIndex } from './scout-index';\nimport type { SearchConstraints } from './types';\n\n/**\n * Wraps a `ScoutIndex` as a `searchFn` compatible with `sourcerer`'s `LocalSourceConfig`.\n *\n * The returned function ignores the `items` argument (the index is the source of truth)\n * and delegates entirely to `index.search()`, returning plain items in score order.\n *\n * @example\n * ```ts\n * const index = createIndex(users, { fields: ['name', 'email'] });\n *\n * // Pass directly to sourcerer's searchFn option\n * const source = createLocalSource(users, { searchFn: toSearchFn(index) });\n * ```\n */\nexport function toSearchFn<T>(\n index: ScoutIndex<T>,\n options?: SearchConstraints,\n): (items: readonly T[], query: string) => readonly T[] {\n return (_items, query) => index.search(query, options).map((r) => r.item);\n}\n\n/**\n * Returns a predicate that returns `true` for items matching `query` in the given index.\n *\n * The predicate is computed once at call time — call `toFilterPredicate` again if the\n * query or corpus changes.\n *\n * Compatible with `Array.filter`, `vault`'s `query.filter()`, or any predicate pipeline.\n *\n * @example\n * ```ts\n * const index = createIndex(products, { fields: ['title', 'sku'] });\n *\n * // Array filter\n * const results = products.filter(toFilterPredicate(index, 'widget'));\n *\n * // vault query builder\n * const rows = await db.query('products')\n * .filter(toFilterPredicate(index, searchTerm))\n * .toArray();\n * ```\n */\nexport function toFilterPredicate<T>(\n index: ScoutIndex<T>,\n query: string,\n options?: SearchConstraints,\n): (item: T) => boolean {\n const matchSet = new Set(index.search(query, options).map((r) => r.item));\n\n return (item) => matchSet.has(item);\n}\n","/** Base class for all scout errors. Use `instanceof ScoutError` to catch any scout-originated error. */\nexport class ScoutError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n\n static is(err: unknown): err is ScoutError {\n return err instanceof ScoutError;\n }\n}\n\n/** Thrown when a method is called on a disposed search state instance. */\nexport class ScoutDisposedError extends ScoutError {}\n\n/** Thrown when an index is built or queried with an invalid configuration (e.g. zero fields defined). */\nexport class ScoutIndexError extends ScoutError {}\n","import type { HighlightPart, SearchResult } from './types';\n\n/**\n * Finds character ranges within `text` where `query` words appear.\n * Ranges are sorted and overlapping ranges are merged.\n *\n * Useful when you need to apply match ranges to a different string than the\n * indexed field value (e.g. a truncated preview or a differently formatted display string).\n *\n * @param text - The string to search within.\n * @param query - The normalized (tokenized) query string. Words are split on spaces.\n * @returns Sorted, non-overlapping `[start, end]` character ranges.\n */\nexport function findMatchRanges(text: string, query: string): [number, number][] {\n const lower = text.toLowerCase();\n const words = query.split(' ').filter(Boolean);\n const ranges: [number, number][] = [];\n\n for (const word of words) {\n let pos = 0;\n\n while (pos < lower.length) {\n const idx = lower.indexOf(word, pos);\n\n if (idx === -1) break;\n\n ranges.push([idx, idx + word.length]);\n pos = idx + 1;\n }\n }\n\n ranges.sort((a, b) => a[0] - b[0]);\n\n const merged: [number, number][] = [];\n\n for (const range of ranges) {\n const last = merged[merged.length - 1];\n\n if (last && range[0] <= last[1]) {\n last[1] = Math.max(last[1], range[1]);\n } else {\n merged.push([range[0], range[1]]);\n }\n }\n\n return merged;\n}\n\n/**\n * Splits `text` into highlighted and unhighlighted fragments using match `ranges`.\n *\n * Ranges must be sorted and non-overlapping (as produced by `SearchResult.matches[n].ranges`).\n * Use the returned parts to render highlighted text in a UI component.\n *\n * **`part.text` is the original, unescaped field value** (e.g. a user's name, bio, or\n * product title) — this function does no HTML escaping. Render each part via safe DOM APIs\n * (`textContent`, a framework's text binding) and wrap `highlighted` parts in your own\n * element (e.g. `<mark>`); never concatenate `part.text` into an HTML string for\n * `innerHTML` — that reintroduces the XSS risk this structured return shape avoids.\n *\n * @example\n * ```ts\n * highlight('Hello World', [[0, 5]]);\n * // [{ text: 'Hello', highlighted: true }, { text: ' World', highlighted: false }]\n *\n * highlight('Hello World', [[0, 5], [6, 11]]);\n * // [\n * // { text: 'Hello', highlighted: true },\n * // { text: ' ', highlighted: false },\n * // { text: 'World', highlighted: true },\n * // ]\n * ```\n *\n * @param text - The original field value to split.\n * @param ranges - Sorted, non-overlapping `[start, end]` ranges from `FieldMatch.ranges`.\n * @returns An array of `HighlightPart` objects. Returns an empty array for an empty `text`.\n */\nexport function highlight(text: string, ranges: [number, number][]): HighlightPart[] {\n if (!text) return [];\n\n if (!ranges.length) return [{ highlighted: false, text }];\n\n const parts: HighlightPart[] = [];\n let cursor = 0;\n\n for (const [start, end] of ranges) {\n if (start > cursor) {\n parts.push({ highlighted: false, text: text.slice(cursor, start) });\n }\n\n if (end > start) {\n parts.push({ highlighted: true, text: text.slice(start, end) });\n }\n\n cursor = end;\n }\n\n if (cursor < text.length) {\n parts.push({ highlighted: false, text: text.slice(cursor) });\n }\n\n return parts;\n}\n\n/**\n * Finds the match ranges for `field` in `result` and splits `text` into\n * highlighted and unhighlighted fragments in one step.\n *\n * This is the ergonomic shorthand for the common pattern:\n * ```ts\n * const match = result.matches.find(m => m.field === 'name');\n * const parts = highlight(item.name, match?.ranges ?? []);\n * ```\n *\n * @example\n * ```ts\n * for (const result of index.search('alice')) {\n * const parts = highlightField(result, 'name', result.item.name);\n * console.log(parts.map(p => p.highlighted ? `[${p.text}]` : p.text).join(''));\n * }\n * ```\n *\n * @param result - A `SearchResult` from `ScoutIndex.search()`.\n * @param field - The field name to look up in `result.matches`.\n * @param text - The original field value string to split.\n * @returns An array of `HighlightPart` objects.\n */\nexport function highlightField<T>(result: SearchResult<T>, field: keyof T & string, text: string): HighlightPart[] {\n const match = result.matches.find((m) => m.field === field);\n\n return highlight(text, match?.ranges ?? []);\n}\n","//#region src/devtools-hook.ts\nvar e = null, t = () => e, n = (t) => {\n\te = t;\n};\n//#endregion\nexport { t as getDevToolsHook, n as setDevToolsHook };\n\n//# sourceMappingURL=devtools-hook.js.map","//#region src/errors.ts\nvar e = class e extends Error {\n\tconstructor(e, t) {\n\t\tsuper(e, t), this.name = new.target.name, Object.setPrototypeOf(this, new.target.prototype);\n\t}\n\tstatic is(t) {\n\t\treturn t instanceof e;\n\t}\n}, t = class extends e {}, n = class extends e {}, r = class extends e {}, i = class extends e {}, a = class extends e {}, o = class extends e {};\n//#endregion\nexport { t as RippleComputedCycleError, n as RippleDisposedScopeError, r as RippleEnvironmentError, e as RippleError, i as RippleInfiniteLoopError, a as RippleInvalidCleanupError, o as RippleInvalidStoreError };\n\n//# sourceMappingURL=errors.js.map","//#region src/tracking.ts\nvar e = 0, t = () => ++e, n = () => e, r = () => ({\n\tactiveDirty: \"a\",\n\tbatchDepth: 0,\n\tdirtyWithEffectSubsA: /* @__PURE__ */ new Set(),\n\tdirtyWithEffectSubsB: /* @__PURE__ */ new Set(),\n\tpendingSubscribers: /* @__PURE__ */ new Set()\n}), i = {\n\tscheduling: r(),\n\tscopeCleanups: null,\n\ttracking: null\n}, a = null, o = () => a === null ? i : a.get(), s = () => o().scheduling, c = () => a !== null, l = () => o().tracking, u = (e, t) => {\n\tif (a !== null) {\n\t\tlet n = a.get();\n\t\treturn a.run({\n\t\t\t...n,\n\t\t\ttracking: e\n\t\t}, t);\n\t}\n\tlet n = i;\n\ti = {\n\t\t...i,\n\t\ttracking: e\n\t};\n\ttry {\n\t\treturn t();\n\t} finally {\n\t\ti = n;\n\t}\n}, d = (e, t) => {\n\tif (a !== null) {\n\t\tlet n = a.get();\n\t\treturn a.run({\n\t\t\t...n,\n\t\t\tscopeCleanups: e\n\t\t}, t);\n\t}\n\tlet n = i;\n\ti = {\n\t\t...i,\n\t\tscopeCleanups: e\n\t};\n\ttry {\n\t\treturn t();\n\t} finally {\n\t\ti = n;\n\t}\n}, f = () => o().scopeCleanups, p = (e) => {\n\tlet t = l();\n\tt?.kind === \"effect\" ? t.cleanups.push(e) : f()?.push(e);\n}, m = (e) => {\n\tlet t = a;\n\treturn a = e, t;\n}, h = (e, t) => {\n\tlet n = l();\n\treturn n === null ? t() : u({\n\t\t...n,\n\t\tsourceObserver: e\n\t}, t);\n}, g = (e) => u(null, e), _ = (e) => {\n\tlet t = l();\n\tif (t !== null) {\n\t\tif (t.sourceObserver?.(e), t.kind === \"computed\") t.depCollector.push({\n\t\t\tsource: e,\n\t\t\tversion: e.version\n\t\t});\n\t\telse if (t.kind === \"effect\") {\n\t\t\tlet n = t.effect;\n\t\t\te.addEffectSub(n), t.subscriptions.add(() => e.removeEffectSub(n)), t.deps.set(e, e.version);\n\t\t}\n\t}\n};\n//#endregion\nexport { m as _installContextHook, p as autoRegisterDisposal, r as createSchedulingState, n as getRevision, s as getSchedulingState, f as getScopeCleanups, l as getTracking, c as hasContextHook, t as tickRevision, _ as trackSource, g as untrack, d as withScopeCleanups, h as withSourceObserver, u as withTracking };\n\n//# sourceMappingURL=tracking.js.map","//#region src/_error-utils.ts\nvar e = (e) => e instanceof Error ? e : /* @__PURE__ */ Error(`Non-Error thrown: ${String(e)}`), t = (t) => {\n\tlet n = [];\n\tfor (let r of t) try {\n\t\tr();\n\t} catch (t) {\n\t\tn.push(e(t));\n\t}\n\treturn n;\n}, n = (e, n) => {\n\tlet r = t(e);\n\tif (r.length === 1) throw r[0];\n\tif (r.length > 0) throw AggregateError(r, n);\n}, r = (t, n, r) => {\n\tthrow n.length === 0 ? t : AggregateError([e(t), ...n], r, { cause: e(t) });\n};\n//#endregion\nexport { t as collectErrors, e as ensureError, r as rethrowWith, n as runAll };\n\n//# sourceMappingURL=_error-utils.js.map","//#region src/_dev.ts\nvar e = !globalThis.__RIPPLE_PROD__;\nfunction t(t) {\n\te && console.warn(`[@vielzeug/ripple] ${t}`);\n}\n//#endregion\nexport { t as warn };\n\n//# sourceMappingURL=_dev.js.map","import { runAll as e } from \"./_error-utils.js\";\nimport { RippleInfiniteLoopError as t } from \"./errors.js\";\nimport { warn as n } from \"./_dev.js\";\nimport { getSchedulingState as r, hasContextHook as i } from \"./tracking.js\";\nvar a = !1, o = 0, s = [], c = (e) => e.activeDirty === \"a\" ? e.dirtyWithEffectSubsA : e.dirtyWithEffectSubsB, l = (e, t) => {\n\tlet n = ++o;\n\tfor (let n of t.effectSubs()) e.pendingSubscribers.add(n);\n\tfor (let e of t.computedSubs()) s.push(e);\n\ttry {\n\t\tfor (; s.length > 0;) {\n\t\t\tlet t = s.pop();\n\t\t\tif (t.lastPropEpoch_ !== n && (t.lastPropEpoch_ = n, t.markDirty())) {\n\t\t\t\tt.effectSubs().size > 0 && c(e).add(t);\n\t\t\t\tfor (let e of t.computedSubs()) s.push(e);\n\t\t\t}\n\t\t}\n\t} finally {\n\t\ts.length = 0;\n\t}\n}, u = (e) => {\n\tfor (; c(e).size > 0;) {\n\t\tlet t = c(e);\n\t\te.activeDirty = e.activeDirty === \"a\" ? \"b\" : \"a\", c(e).clear();\n\t\tfor (let n of t) if (n.hasSubscribers() && n.refreshIfDirty()) {\n\t\t\tfor (let t of n.effectSubs()) e.pendingSubscribers.add(t);\n\t\t\tfor (let t of n.computedSubs()) t.markDirty() && t.effectSubs().size > 0 && c(e).add(t);\n\t\t}\n\t\tt.clear();\n\t}\n}, d = (n) => {\n\tlet r = 0;\n\tfor (; n.pendingSubscribers.size > 0 || c(n).size > 0;) {\n\t\tif (++r > 100) throw new t(\"infinite flush loop (> 100 iterations)\");\n\t\tif (c(n).size > 0 && u(n), n.pendingSubscribers.size === 0) continue;\n\t\tlet i = [...n.pendingSubscribers];\n\t\tn.pendingSubscribers.clear(), e(i, \"subscriber errors\");\n\t}\n}, f = globalThis.process?.versions != null, p = (e) => {\n\tif (!e.hasSubscribers()) return;\n\t!a && f && !i() && (a = !0, n(\"Signal updated in a Node.js-like environment. The module-level flush queue is shared across concurrent requests — use per-request worker isolation or the @vielzeug/ripple/ssr sub-path for request-isolated scheduling.\"));\n\tlet t = r();\n\tl(t, e), t.batchDepth === 0 && d(t);\n}, m = (e) => {\n\tlet t = r();\n\tt.batchDepth++;\n\tlet n;\n\ttry {\n\t\tn = e();\n\t} catch (e) {\n\t\tthrow t.batchDepth--, t.batchDepth === 0 && (t.pendingSubscribers.clear(), t.dirtyWithEffectSubsA.clear(), t.dirtyWithEffectSubsB.clear()), e;\n\t}\n\treturn t.batchDepth--, t.batchDepth === 0 && d(t), n;\n};\n//#endregion\nexport { m as batch, p as notifyNodeChange };\n\n//# sourceMappingURL=scheduling.js.map","//#region src/subscription.ts\nvar e = class {\n\tfn_;\n\tdisposed_ = !1;\n\tconstructor(e) {\n\t\tthis.fn_ = e;\n\t}\n\tget disposed() {\n\t\treturn this.disposed_;\n\t}\n\tdispose() {\n\t\tif (this.disposed_) return;\n\t\tthis.disposed_ = !0;\n\t\tlet e = this.fn_;\n\t\tthis.fn_ = null, e();\n\t}\n\t[Symbol.dispose]() {\n\t\tthis.dispose();\n\t}\n}, t = class {\n\tawaitDone_;\n\tgetCurrentRun_;\n\tsyncStop_;\n\tasyncDisposePromise_ = null;\n\tconstructor(e, t, n) {\n\t\tthis.awaitDone_ = t, this.getCurrentRun_ = n, this.syncStop_ = e;\n\t}\n\tget disposed() {\n\t\treturn this.syncStop_.disposed;\n\t}\n\tdispose() {\n\t\tthis.syncStop_.dispose();\n\t}\n\trun() {\n\t\treturn this.getCurrentRun_() ?? Promise.resolve();\n\t}\n\t[Symbol.dispose]() {\n\t\tthis.dispose();\n\t}\n\t[Symbol.asyncDispose]() {\n\t\treturn this.asyncDisposePromise_ === null ? (this.dispose(), this.asyncDisposePromise_ = this.awaitDone_(), this.asyncDisposePromise_) : this.asyncDisposePromise_;\n\t}\n};\n//#endregion\nexport { t as AsyncSubscriptionImpl, e as SubscriptionImpl };\n\n//# sourceMappingURL=subscription.js.map","//#region src/symbols.ts\nvar e = Symbol(\"ripple.is-signal\"), t = Symbol(\"ripple.is-computed\"), n = Symbol(\"ripple.is-store\"), r = Symbol(\"ripple.uninitialized\");\n//#endregion\nexport { t as IS_COMPUTED, e as IS_SIGNAL, n as IS_STORE, r as UNINITIALIZED };\n\n//# sourceMappingURL=symbols.js.map","import { IS_COMPUTED as e, IS_SIGNAL as t } from \"./symbols.js\";\n//#region src/reactive-base.ts\nvar n = new FinalizationRegistry(({ key: e, map: t }) => {\n\tt.delete(e);\n}), r = class {\n\tversion = 0;\n\tname;\n\t[t] = !0;\n\tcomputedSubs_ = /* @__PURE__ */ new Map();\n\teffectSubs_ = /* @__PURE__ */ new Set();\n\tconstructor(e) {\n\t\tthis.name = e;\n\t}\n\taddComputedSub(e) {\n\t\tlet t = new WeakRef(e);\n\t\tthis.computedSubs_.set(e, t), n.register(e, {\n\t\t\tkey: e,\n\t\t\tmap: this.computedSubs_\n\t\t}, t);\n\t}\n\tremoveComputedSub(e) {\n\t\tlet t = this.computedSubs_.get(e);\n\t\tt !== void 0 && (this.computedSubs_.delete(e), n.unregister(t));\n\t}\n\taddEffectSub(e) {\n\t\tthis.effectSubs_.add(e);\n\t}\n\tremoveEffectSub(e) {\n\t\tthis.effectSubs_.delete(e);\n\t}\n\tclearSubscribers() {\n\t\tfor (let e of this.computedSubs_.values()) n.unregister(e);\n\t\tthis.computedSubs_.clear(), this.effectSubs_.clear();\n\t}\n\thasSubscribers() {\n\t\tif (this.effectSubs_.size > 0) return !0;\n\t\tfor (let e of this.computedSubs_.values()) if (e.deref() !== void 0) return !0;\n\t\treturn !1;\n\t}\n\t*computedSubs() {\n\t\tfor (let [e, t] of this.computedSubs_) {\n\t\t\tlet r = t.deref();\n\t\t\tr === void 0 ? (this.computedSubs_.delete(e), n.unregister(t)) : yield r;\n\t\t}\n\t}\n\teffectSubs() {\n\t\treturn this.effectSubs_;\n\t}\n}, i = class extends r {\n\t[e] = !0;\n\tlastPropEpoch_ = 0;\n};\n//#endregion\nexport { i as ComputedBase, r as ReactiveBase };\n\n//# sourceMappingURL=reactive-base.js.map","import { getDevToolsHook as e } from \"./devtools-hook.js\";\nimport { ensureError as t } from \"./_error-utils.js\";\nimport { RippleComputedCycleError as n } from \"./errors.js\";\nimport { autoRegisterDisposal as r, getRevision as i, trackSource as a, withTracking as o } from \"./tracking.js\";\nimport { SubscriptionImpl as s } from \"./subscription.js\";\nimport { UNINITIALIZED as c } from \"./symbols.js\";\nimport { ComputedBase as l } from \"./reactive-base.js\";\n//#region src/computed.ts\nvar u = class extends l {\n\tvalue_;\n\tdirty_;\n\tcomputing_;\n\tdisposed_;\n\tdeps_;\n\tcompute_;\n\tequals_;\n\tmaxRevision_;\n\tconstructor(e, t) {\n\t\tlet { equals: n, name: r } = t ?? {};\n\t\tsuper(r), this.value_ = c, this.dirty_ = !0, this.computing_ = !1, this.disposed_ = !1, this.deps_ = [], this.maxRevision_ = -1, this.compute_ = e, this.equals_ = n === void 0 ? Object.is : (e, t) => n(e, t);\n\t}\n\tmarkDirty() {\n\t\treturn this.disposed_ || this.dirty_ ? !1 : (this.dirty_ = !0, !0);\n\t}\n\trefreshIfDirty() {\n\t\tif (!this.dirty_) return !1;\n\t\tif (i() <= this.maxRevision_) return this.dirty_ = !1, !1;\n\t\tif (this.deps_.length > 0) {\n\t\t\tlet e = !0;\n\t\t\tfor (let t of this.deps_) {\n\t\t\t\tlet n = t.source;\n\t\t\t\tif (\"refreshIfDirty\" in n && n.refreshIfDirty(), n.version !== t.version) {\n\t\t\t\t\te = !1;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (e) return this.dirty_ = !1, this.maxRevision_ = i(), !1;\n\t\t}\n\t\treturn this.recompute();\n\t}\n\trunCompute(e) {\n\t\ttry {\n\t\t\treturn o({\n\t\t\t\tcomputed: this,\n\t\t\t\tdepCollector: e,\n\t\t\t\tkind: \"computed\"\n\t\t\t}, this.compute_);\n\t\t} catch (e) {\n\t\t\tthrow t(e);\n\t\t}\n\t}\n\trecompute() {\n\t\tif (this.computing_) throw new n(`computed cycle detected${this.name ? ` \"${this.name}\"` : \"\"}`);\n\t\tthis.computing_ = !0;\n\t\ttry {\n\t\t\tlet t = [];\n\t\t\te()?.compute?.({ name: this.name });\n\t\t\tlet n = this.runCompute(t);\n\t\t\treturn this.dirty_ = !1, this.maxRevision_ = i(), this.updateDeps(t), this.value_ === c || !this.equals_(this.value_, n) ? (this.value_ = n, this.version++, !0) : !1;\n\t\t} finally {\n\t\t\tthis.computing_ = !1;\n\t\t}\n\t}\n\tupdateDeps(e) {\n\t\tlet t = this.deps_;\n\t\tif (t.length === e.length && t.every((t, n) => t.source === e[n].source)) {\n\t\t\tfor (let n = 0; n < e.length; n++) t[n].version = e[n].version;\n\t\t\treturn;\n\t\t}\n\t\tlet n = new Set(t.map((e) => e.source)), r = new Set(e.map((e) => e.source));\n\t\tfor (let e of t) r.has(e.source) || e.source.removeComputedSub(this);\n\t\tfor (let t of e) n.has(t.source) || t.source.addComputedSub(this);\n\t\tthis.deps_ = e;\n\t}\n\tget value() {\n\t\treturn this.disposed_ ? this.value_ === c ? void 0 : this.value_ : (this.refreshIfDirty(), a(this), this.value_);\n\t}\n\tpeek() {\n\t\treturn this.disposed_ ? this.value_ === c ? void 0 : this.value_ : (this.refreshIfDirty(), this.value_);\n\t}\n\tsubscribe = (e) => {\n\t\tif (this.disposed_) {\n\t\t\tlet e = new s(() => {});\n\t\t\treturn e.dispose(), e;\n\t\t}\n\t\treturn this.refreshIfDirty(), this.addEffectSub(e), new s(() => {\n\t\t\tthis.removeEffectSub(e);\n\t\t});\n\t};\n\tget disposed() {\n\t\treturn this.disposed_;\n\t}\n\tdispose() {\n\t\tif (!this.disposed_) {\n\t\t\tthis.disposed_ = !0;\n\t\t\tfor (let e of this.deps_) e.source.removeComputedSub(this);\n\t\t\tthis.deps_ = [], this.clearSubscribers(), e()?.dispose?.({\n\t\t\t\tkind: \"computed\",\n\t\t\t\tname: this.name\n\t\t\t});\n\t\t}\n\t}\n\t[Symbol.dispose]() {\n\t\tthis.dispose();\n\t}\n}, d = (e, t) => {\n\tlet n = new u(e, t);\n\treturn r(() => n.dispose()), n;\n};\n//#endregion\nexport { u as ComputedImpl, d as computed };\n\n//# sourceMappingURL=computed.js.map","import { getDevToolsHook as e } from \"./devtools-hook.js\";\nimport { tickRevision as t, trackSource as n } from \"./tracking.js\";\nimport { notifyNodeChange as r } from \"./scheduling.js\";\nimport { SubscriptionImpl as i } from \"./subscription.js\";\nimport { ReactiveBase as a } from \"./reactive-base.js\";\n//#region src/signal.ts\nvar o = class extends a {\n\tvalue_;\n\tequals_;\n\tdisposed_;\n\tconstructor(e, t, n) {\n\t\tsuper(n), this.value_ = e, this.equals_ = t ?? Object.is, this.disposed_ = !1;\n\t}\n\tget value() {\n\t\treturn this.disposed_ || n(this), this.value_;\n\t}\n\tset value(n) {\n\t\tif (this.disposed_ || this.equals_(this.value_, n)) return;\n\t\tlet i = this.value_;\n\t\tthis.value_ = n, this.version = t(), e()?.write?.({\n\t\t\tname: this.name,\n\t\t\tnewValue: n,\n\t\t\toldValue: i\n\t\t}), r(this);\n\t}\n\tpeek() {\n\t\treturn this.value_;\n\t}\n\tsubscribe = (e) => {\n\t\tif (this.disposed_) {\n\t\t\tlet e = new i(() => {});\n\t\t\treturn e.dispose(), e;\n\t\t}\n\t\treturn this.addEffectSub(e), new i(() => {\n\t\t\tthis.removeEffectSub(e);\n\t\t});\n\t};\n\tget disposed() {\n\t\treturn this.disposed_;\n\t}\n\tdispose() {\n\t\tthis.disposed_ || (this.disposed_ = !0, this.clearSubscribers(), e()?.dispose?.({\n\t\t\tkind: \"signal\",\n\t\t\tname: this.name\n\t\t}));\n\t}\n\t[Symbol.dispose]() {\n\t\tthis.dispose();\n\t}\n}, s = (e, t) => new o(e, t?.equals, t?.name);\n//#endregion\nexport { o as SignalImpl, s as signal };\n\n//# sourceMappingURL=signal.js.map","const isDev = !(globalThis as { __SCOUT_PROD__?: boolean }).__SCOUT_PROD__;\n\n/** @internal */\nexport function warn(msg: string): void {\n if (isDev) console.warn(`[@vielzeug/scout] ${msg}`);\n}\n","import { warn } from './_dev';\n\n/**\n * Normalizes a string for indexing or querying.\n * Lowercases, replaces punctuation (except apostrophes) with spaces,\n * trims, and collapses runs of whitespace to a single space.\n * @internal\n */\nexport function tokenize(text: string): string {\n return text\n .toLowerCase()\n .replace(/[^\\p{L}\\p{N}\\s']/gu, ' ')\n .trim()\n .replace(/\\s+/g, ' ');\n}\n\n/**\n * Converts an unknown field value to an indexable string.\n * Returns an empty string for `null`, `undefined`, objects, or symbols.\n * @internal\n */\nexport function defaultStringify(value: unknown): string {\n if (value == null) return '';\n\n if (typeof value === 'string') return value;\n\n if (typeof value === 'number' || typeof value === 'boolean') return String(value);\n\n if (Array.isArray(value)) {\n warn(\n 'defaultStringify: received an array value — provide a custom `stringify` on the FieldDef to control array indexing.\\n' +\n 'Falling back to joining string/number/boolean elements with a space.',\n );\n\n return value.filter((v) => typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean').join(' ');\n }\n\n return '';\n}\n","/**\n * Generates padded trigrams from a pre-normalized (lowercased, trimmed) string.\n *\n * The string is padded with a single leading and trailing space so that prefix\n * and suffix characters form complete trigrams.\n *\n * @example\n * `\"hello\"` → `{ \" he\", \"hel\", \"ell\", \"llo\", \"lo \" }`\n *\n * @internal\n */\nexport function generateTrigrams(text: string): Set<string> {\n const trigrams = new Set<string>();\n const padded = ` ${text} `;\n\n for (let i = 0; i < padded.length - 2; i++) {\n trigrams.add(padded.slice(i, i + 3));\n }\n\n return trigrams;\n}\n\n/**\n * Computes the Sørensen–Dice coefficient between two trigram sets.\n * Returns `0` when either set is empty; `1` when sets are identical.\n *\n * Formula: `2 * |A ∩ B| / (|A| + |B|)`\n *\n * @internal\n */\nexport function diceSimilarity(a: Set<string>, b: Set<string>): number {\n if (a.size === 0 || b.size === 0) return 0;\n\n let shared = 0;\n\n for (const trigram of a) {\n if (b.has(trigram)) shared++;\n }\n\n return (2 * shared) / (a.size + b.size);\n}\n","import type { FieldDef, FieldMatch, ScoutIndexOptions, SearchConstraints, SearchResult } from './types';\n\nimport { ScoutIndexError } from './errors';\nimport { findMatchRanges } from './highlight';\nimport { defaultStringify, tokenize } from './tokenize';\nimport { diceSimilarity, generateTrigrams } from './trigram';\n\ntype FieldConfig<T> = {\n field: keyof T & string;\n stringify: (v: unknown) => string;\n weight: number;\n};\n\ntype ItemRecord = {\n /** Per-field trigrams for scoring. */\n trigrams: Map<string, Set<string>>;\n /** Per-field original text for highlighting. */\n values: Map<string, string>;\n};\n\n/**\n * A stateful, indexed search corpus. Created via `createIndex()`.\n *\n * Supports incremental `add()`, `remove()`, and `reindex()` operations — each patches\n * the trigram index in O(field_length) without a full rebuild.\n */\nexport interface ScoutIndex<T> {\n /** All items currently in the index, in insertion order. */\n readonly items: readonly T[];\n /** Number of items currently in the index. */\n readonly size: number;\n /** Adds `item` to the index. No-op if the item is already indexed (by reference). */\n add(item: T): void;\n /**\n * Re-reads the item's current field values and rebuilds its index entry in-place,\n * only updating fields whose values have changed. Preserves insertion order.\n * No-op if the item is not in the index.\n */\n reindex(item: T): void;\n /**\n * Removes `item` from the index by reference equality.\n * No-op if the item is not in the index.\n */\n remove(item: T): void;\n /**\n * Searches the index for `query` and returns results sorted by score descending.\n *\n * An empty (or whitespace-only) `query` returns all indexed items with `score = 1`.\n * A `query` with no indexable content after normalization (e.g. punctuation-only) returns\n * no results. Results below `threshold` are excluded. At most `limit` results are returned.\n */\n search(query: string, options?: SearchConstraints): SearchResult<T>[];\n /**\n * Subscribes `listener` to be called after every `add()` / `remove()` / `reindex()` call\n * that actually changes the index (no-ops — e.g. removing an unindexed item — don't fire\n * it). Returns an unsubscribe function.\n *\n * Framework-agnostic extension point: `createSearch()` uses this internally to keep\n * reactive `results` in sync with index mutations. Most callers won't need this directly.\n */\n onMutate(listener: () => void): () => void;\n}\n\nfunction resolveFields<T>(defs: ReadonlyArray<FieldDef<T>>): FieldConfig<T>[] {\n return defs.map((def) => {\n if (typeof def === 'string') {\n return { field: def, stringify: defaultStringify, weight: 1 };\n }\n\n return {\n field: def.field,\n stringify: def.stringify ?? defaultStringify,\n weight: def.weight ?? 1,\n };\n });\n}\n\n/**\n * Builds a trigram inverted index over `items` for fast fuzzy search.\n *\n * Construction is O(corpus × field_length). Subsequent `search()` calls are\n * O(candidates) — far faster than per-query Levenshtein for large corpora.\n *\n * @example\n * ```ts\n * const index = createIndex(users, {\n * fields: [{ field: 'name', weight: 2 }, 'email'],\n * threshold: 0.3,\n * limit: 20,\n * });\n *\n * const results = index.search('alice');\n * ```\n *\n * @throws {ScoutIndexError} If `options.fields` is empty.\n */\nexport function createIndex<T>(items: T[], options: ScoutIndexOptions<T>): ScoutIndex<T> {\n if (options.fields.length === 0) {\n throw new ScoutIndexError('createIndex: at least one field is required.');\n }\n\n const fields = resolveFields(options.fields);\n const maxWeight = fields.reduce((max, f) => Math.max(max, f.weight), 1);\n const defaultThreshold = options.threshold ?? 0.2;\n const defaultLimit = options.limit ?? 50;\n const defaultMinQueryLength = options.minQueryLength ?? 3;\n\n /** item → per-item record, preserves insertion order for `items` getter */\n const itemData = new Map<T, ItemRecord>();\n /** trigram → set of items that contain it */\n const invertedIndex = new Map<string, Set<T>>();\n const mutationListeners = new Set<() => void>();\n\n function notifyMutation(): void {\n for (const listener of mutationListeners) listener();\n }\n\n /** Single-entry cache for the most recent normalized query's trigrams (F2). */\n let cachedNormalized: string | null = null;\n let cachedTrigrams: Set<string> | null = null;\n\n function getQueryTrigrams(normalized: string): Set<string> {\n if (normalized === cachedNormalized && cachedTrigrams !== null) return cachedTrigrams;\n\n cachedNormalized = normalized;\n cachedTrigrams = generateTrigrams(normalized);\n\n return cachedTrigrams;\n }\n\n function addFieldToIndex(item: T, fieldTrigrams: Set<string>): void {\n for (const trigram of fieldTrigrams) {\n let bucket = invertedIndex.get(trigram);\n\n if (!bucket) {\n bucket = new Set<T>();\n invertedIndex.set(trigram, bucket);\n }\n\n bucket.add(item);\n }\n }\n\n function removeFieldFromIndex(item: T, fieldTrigrams: Set<string>): void {\n for (const trigram of fieldTrigrams) {\n const bucket = invertedIndex.get(trigram);\n\n if (bucket) {\n bucket.delete(item);\n\n if (bucket.size === 0) invertedIndex.delete(trigram);\n }\n }\n }\n\n function addItem(item: T): void {\n const trigrams = new Map<string, Set<string>>();\n const values = new Map<string, string>();\n\n for (const { field, stringify } of fields) {\n const raw = item[field];\n const text = stringify(raw);\n const normalized = tokenize(text);\n const fieldTrigrams = normalized.length >= 1 ? generateTrigrams(normalized) : new Set<string>();\n\n trigrams.set(field, fieldTrigrams);\n values.set(field, text);\n addFieldToIndex(item, fieldTrigrams);\n }\n\n itemData.set(item, { trigrams, values });\n }\n\n /**\n * Performs a full linear scan over all items for short queries.\n * O(n × field_count) — acceptable for small corpora; consider raising\n * `minQueryLength` on large datasets to avoid triggering this path.\n */\n function containmentScan(query: string): Set<T> {\n const result = new Set<T>();\n\n for (const [item, record] of itemData) {\n for (const value of record.values.values()) {\n if (value.toLowerCase().includes(query)) {\n result.add(item);\n break;\n }\n }\n }\n\n return result;\n }\n\n function trigramCandidates(queryTrigrams: Set<string>): Set<T> {\n const candidates = new Set<T>();\n\n for (const trigram of queryTrigrams) {\n const items = invertedIndex.get(trigram);\n\n if (items) {\n for (const item of items) candidates.add(item);\n }\n }\n\n return candidates;\n }\n\n function scoreCandidate(normalized: string, queryTrigrams: Set<string> | null, record: ItemRecord): number {\n let bestScore = 0;\n\n for (const { field, weight } of fields) {\n let fieldScore: number;\n\n if (queryTrigrams === null) {\n const raw = record.values.get(field) ?? '';\n\n fieldScore = raw.toLowerCase().includes(normalized) ? 1.0 : 0;\n } else {\n const itemTrigrams = record.trigrams.get(field);\n\n if (!itemTrigrams || itemTrigrams.size === 0) continue;\n\n fieldScore = diceSimilarity(queryTrigrams, itemTrigrams);\n }\n\n const weighted = fieldScore * (weight / maxWeight);\n\n if (weighted > bestScore) bestScore = weighted;\n }\n\n return bestScore;\n }\n\n function computeMatches(query: string, values: Map<string, string>): FieldMatch<keyof T & string>[] {\n const matches: FieldMatch<keyof T & string>[] = [];\n\n for (const { field } of fields) {\n const text = values.get(field);\n\n if (!text) continue;\n\n const ranges = findMatchRanges(text, query);\n\n if (ranges.length > 0) matches.push({ field, ranges });\n }\n\n return matches;\n }\n\n for (const item of items) {\n addItem(item);\n }\n\n return {\n add(item: T): void {\n if (itemData.has(item)) return;\n\n addItem(item);\n notifyMutation();\n },\n\n get items(): readonly T[] {\n return [...itemData.keys()];\n },\n\n onMutate(listener: () => void): () => void {\n mutationListeners.add(listener);\n\n return () => {\n mutationListeners.delete(listener);\n };\n },\n\n reindex(item: T): void {\n const record = itemData.get(item);\n\n if (!record) return;\n\n let changed = false;\n\n for (const { field, stringify } of fields) {\n const raw = item[field];\n const newText = stringify(raw);\n const oldText = record.values.get(field);\n\n if (newText === oldText) continue;\n\n changed = true;\n\n const oldTrigrams = record.trigrams.get(field);\n\n if (oldTrigrams) removeFieldFromIndex(item, oldTrigrams);\n\n const normalized = tokenize(newText);\n const newTrigrams = normalized.length >= 1 ? generateTrigrams(normalized) : new Set<string>();\n\n record.trigrams.set(field, newTrigrams);\n record.values.set(field, newText);\n addFieldToIndex(item, newTrigrams);\n }\n\n if (changed) notifyMutation();\n },\n\n remove(item: T): void {\n const record = itemData.get(item);\n\n if (!record) return;\n\n for (const fieldTrigrams of record.trigrams.values()) {\n removeFieldFromIndex(item, fieldTrigrams);\n }\n\n itemData.delete(item);\n notifyMutation();\n },\n\n search(query: string, options?: SearchConstraints): SearchResult<T>[] {\n const threshold = options?.threshold ?? defaultThreshold;\n const limit = Math.max(0, options?.limit ?? defaultLimit);\n const minQueryLength = options?.minQueryLength ?? defaultMinQueryLength;\n\n if (!query.trim()) {\n return [...itemData.keys()].slice(0, limit).map((item) => ({ item, matches: [], score: 1 }));\n }\n\n const normalized = tokenize(query);\n\n // Query had no indexable content (e.g. punctuation-only) — no match, not \"match all\".\n if (!normalized) return [];\n\n const isShort = normalized.length < minQueryLength;\n const queryTrigrams = isShort ? null : getQueryTrigrams(normalized);\n const candidates = queryTrigrams === null ? containmentScan(normalized) : trigramCandidates(queryTrigrams);\n const results: SearchResult<T>[] = [];\n\n for (const item of candidates) {\n const record = itemData.get(item);\n\n if (!record) continue;\n\n const score = scoreCandidate(normalized, queryTrigrams, record);\n\n if (score >= threshold) {\n const matches = computeMatches(normalized, record.values);\n\n results.push({ item, matches, score });\n }\n }\n\n return results.sort((a, b) => b.score - a.score).slice(0, limit);\n },\n\n get size(): number {\n return itemData.size;\n },\n };\n}\n","import { batch, computed, signal } from '@vielzeug/ripple';\n\nimport type { CreateSearchOptions, ScoutIndexOptions, SearchResult, SearchState } from './types';\n\nimport { ScoutDisposedError } from './errors';\nimport { createIndex, type ScoutIndex } from './scout-index';\n\n/**\n * Combined index + reactive search state returned by `createReactiveSearch()`.\n * Exposes the underlying `ScoutIndex` for incremental mutations (`add`, `remove`, `reindex`).\n */\nexport type ReactiveSearch<T> = SearchState<T> & {\n readonly index: ScoutIndex<T>;\n};\n\nconst DEFAULT_DEBOUNCE = 200;\n\n/**\n * Creates a reactive search state backed by a `ScoutIndex`.\n *\n * - Set `state.query.value` to trigger a (debounced) search.\n * - Read `state.results.value` inside an `effect` or `computed` to consume results reactively.\n * - `state.isSearching.value` is `true` while debouncing, `false` otherwise.\n * - Call `state.dispose()` (or `using state = createSearch(...)`) to release subscriptions.\n *\n * @example\n * ```ts\n * const index = createIndex(users, { fields: ['name', 'email'] });\n * const search = createSearch(index, { debounce: 150 });\n *\n * effect(() => {\n * const results = search.results.value;\n * renderList(results.map(r => r.item));\n * });\n *\n * // Wire to an input\n * input.addEventListener('input', e => {\n * search.query.value = e.currentTarget.value;\n * });\n *\n * // Clean up\n * search.dispose();\n * ```\n *\n * `results` also updates when the index is mutated directly via `index.add()` / `.remove()`\n * / `.reindex()` (not just when `query` changes), by subscribing to `index.onMutate()`.\n *\n * @param index - A `ScoutIndex` built with `createIndex()`.\n * @param options.debounce - Milliseconds to wait before committing query changes. Default: `200`.\n * @param options.limit - Override the index-level result limit.\n * @param options.minQueryLength - Override the index-level minimum query length.\n * @param options.threshold - Override the index-level score threshold.\n */\nexport function createSearch<T>(index: ScoutIndex<T>, options: CreateSearchOptions = {}): SearchState<T> {\n const { debounce: debounceMs = DEFAULT_DEBOUNCE, limit, minQueryLength, threshold } = options;\n\n const query = signal<string>('', { name: 'scout:query' });\n const committedQuery = signal<string>('', { name: 'scout:committedQuery' });\n const indexVersion = signal(0, { name: 'scout:indexVersion' });\n\n const unsubscribeMutations = index.onMutate(() => {\n indexVersion.value++;\n });\n\n const isSearching = computed(() => query.value !== committedQuery.value, { name: 'scout:isSearching' });\n\n const results = computed<SearchResult<T>[]>(\n () => {\n // Reading .value establishes a dependency so index mutations trigger a recompute.\n void indexVersion.value;\n\n return index.search(committedQuery.value, { limit, minQueryLength, threshold });\n },\n { name: 'scout:results' },\n );\n\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n function cancelTimer(): void {\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n }\n\n const subscription = query.subscribe(() => {\n const q = query.peek();\n\n cancelTimer();\n\n if (q === committedQuery.peek()) return;\n\n if (debounceMs === 0) {\n committedQuery.value = q;\n\n return;\n }\n\n timer = setTimeout(() => {\n committedQuery.value = q;\n timer = null;\n }, debounceMs);\n });\n\n function clear(): void {\n if (isDisposed) throw new ScoutDisposedError('SearchState.clear() called after dispose()');\n\n cancelTimer();\n\n batch(() => {\n query.value = '';\n committedQuery.value = '';\n });\n }\n\n let isDisposed = false;\n const ac = new AbortController();\n\n function dispose(): void {\n isDisposed = true;\n ac.abort();\n cancelTimer();\n subscription.dispose();\n unsubscribeMutations();\n query.dispose();\n committedQuery.dispose();\n isSearching.dispose();\n results.dispose();\n indexVersion.dispose();\n }\n\n return {\n clear,\n get disposalSignal(): AbortSignal {\n return ac.signal;\n },\n dispose,\n get disposed(): boolean {\n return isDisposed;\n },\n isSearching,\n query,\n results,\n [Symbol.dispose](): void {\n dispose();\n },\n };\n}\n\n/**\n * Creates a `ScoutIndex` and a reactive search state in one call — the shorthand\n * for the common pattern of `createIndex` + `createSearch`.\n *\n * The returned `ReactiveSearch` exposes the underlying index via `.index` for\n * incremental mutations (`add`, `remove`, `reindex`) after construction.\n *\n * @example\n * ```ts\n * const search = createReactiveSearch(users, {\n * fields: [{ field: 'name', weight: 2 }, 'email'],\n * debounce: 150,\n * });\n *\n * effect(() => renderList(search.results.value.map(r => r.item)));\n *\n * // Wire to an input\n * input.addEventListener('input', e => { search.query.value = e.currentTarget.value; });\n *\n * // Add a new item at runtime\n * search.index.add(newUser);\n * ```\n *\n * @param items - Initial corpus to index.\n * @param options - Index options (`fields`, `limit`, `minQueryLength`, `threshold`) plus optional `debounce`.\n */\nexport function createReactiveSearch<T>(\n items: T[],\n options: ScoutIndexOptions<T> & Pick<CreateSearchOptions, 'debounce'>,\n): ReactiveSearch<T> {\n const index = createIndex(items, {\n fields: options.fields,\n limit: options.limit,\n minQueryLength: options.minQueryLength,\n threshold: options.threshold,\n });\n const state = createSearch(index, { debounce: options.debounce });\n\n return { ...state, index };\n}\n","/**\n * Splits `text` into whitespace-joined word segments using the runtime's native\n * `Intl.Segmenter` — no dependency, no bundle cost beyond this file.\n *\n * `tokenize()`'s trigram-based scoring works on unsegmented scripts (Chinese, Japanese,\n * Thai, ...) without this — trigrams are generated per-character, not per-word — but\n * `findMatchRanges()` / highlighting and the multi-word query semantics documented on\n * `SearchConstraints` assume space-separated words. Corpora in those scripts benefit from\n * pre-segmenting via a custom `stringify` so word boundaries exist for those features too.\n *\n * Not applied inside `tokenize()` itself: benchmarked at ~15x slower than the plain regex\n * path for the common whitespace-delimited case, which would regress `createIndex()`'s\n * O(corpus × field_length) construction cost for every caller, not just those indexing\n * unsegmented scripts. Opt in per-field instead.\n *\n * Returns `text` unchanged where `Intl.Segmenter` isn't available in the current runtime.\n *\n * @example\n * ```ts\n * const index = createIndex(documents, {\n * fields: [{ field: 'title', stringify: (v) => segmentWords(String(v)) }],\n * });\n * ```\n */\nexport function segmentWords(text: string): string {\n const segmenter = getSegmenter();\n\n if (!segmenter) return text;\n\n return [...segmenter.segment(text)]\n .filter((segment) => segment.isWordLike)\n .map((segment) => segment.segment)\n .join(' ');\n}\n\n// Constructing an `Intl.Segmenter` costs roughly as much as a `segment()` call itself\n// (both are called from `stringify`, once per field per item during `createIndex()`) — cache\n// the single stateless instance instead of rebuilding it on every `segmentWords()` call.\nlet cachedSegmenter: Intl.Segmenter | null | undefined;\n\nfunction getSegmenter(): Intl.Segmenter | null {\n if (cachedSegmenter === undefined) {\n cachedSegmenter =\n typeof Intl === 'undefined' || typeof Intl.Segmenter !== 'function'\n ? null\n : new Intl.Segmenter(undefined, { granularity: 'word' });\n }\n\n return cachedSegmenter;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var Scout=(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});function t(e,t){return(n,r)=>e.search(r,t).map(e=>e.item)}function n(e,t,n){let r=new Set(e.search(t,n).map(e=>e.item));return e=>r.has(e)}var r=class e extends Error{constructor(e,t){super(e,t),this.name=new.target.name,Object.setPrototypeOf(this,new.target.prototype)}static is(t){return t instanceof e}},i=class extends r{},a=class extends r{};function o(e,t){let n=e.toLowerCase(),r=t.split(` `).filter(Boolean),i=[];for(let e of r){let t=0;for(;t<n.length;){let r=n.indexOf(e,t);if(r===-1)break;i.push([r,r+e.length]),t=r+1}}i.sort((e,t)=>e[0]-t[0]);let a=[];for(let e of i){let t=a[a.length-1];t&&e[0]<=t[1]?t[1]=Math.max(t[1],e[1]):a.push([e[0],e[1]])}return a}function s(e,t){if(!e)return[];if(!t.length)return[{highlighted:!1,text:e}];let n=[],r=0;for(let[i,a]of t)i>r&&n.push({highlighted:!1,text:e.slice(r,i)}),a>i&&n.push({highlighted:!0,text:e.slice(i,a)}),r=a;return r<e.length&&n.push({highlighted:!1,text:e.slice(r)}),n}function c(e,t,n){return s(n,e.matches.find(e=>e.field===t)?.ranges??[])}var l=null,u=()=>l,d=class e extends Error{constructor(e,t){super(e,t),this.name=new.target.name,Object.setPrototypeOf(this,new.target.prototype)}static is(t){return t instanceof e}},f=class extends d{},p=class extends d{},m=0,h=()=>++m,g=()=>m,_={scheduling:{activeDirty:`a`,batchDepth:0,dirtyWithEffectSubsA:new Set,dirtyWithEffectSubsB:new Set,pendingSubscribers:new Set},scopeCleanups:null,tracking:null},v=null,y=()=>v===null?_:v.get(),b=()=>y().scheduling,x=()=>v!==null,S=()=>y().tracking,C=(e,t)=>{if(v!==null){let n=v.get();return v.run({...n,tracking:e},t)}let n=_;_={..._,tracking:e};try{return t()}finally{_=n}},w=()=>y().scopeCleanups,T=e=>{let t=S();t?.kind===`effect`?t.cleanups.push(e):w()?.push(e)},E=e=>{let t=S();if(t!==null){if(t.sourceObserver?.(e),t.kind===`computed`)t.depCollector.push({source:e,version:e.version});else if(t.kind===`effect`){let n=t.effect;e.addEffectSub(n),t.subscriptions.add(()=>e.removeEffectSub(n)),t.deps.set(e,e.version)}}},D=e=>e instanceof Error?e:Error(`Non-Error thrown: ${String(e)}`),ee=e=>{let t=[];for(let n of e)try{n()}catch(e){t.push(D(e))}return t},te=(e,t)=>{let n=ee(e);if(n.length===1)throw n[0];if(n.length>0)throw AggregateError(n,t)},ne=!globalThis.__RIPPLE_PROD__;function re(e){ne&&console.warn(`[@vielzeug/ripple] ${e}`)}var O=!1,k=0,A=[],j=e=>e.activeDirty===`a`?e.dirtyWithEffectSubsA:e.dirtyWithEffectSubsB,M=(e,t)=>{let n=++k;for(let n of t.effectSubs())e.pendingSubscribers.add(n);for(let e of t.computedSubs())A.push(e);try{for(;A.length>0;){let t=A.pop();if(t.lastPropEpoch_!==n&&(t.lastPropEpoch_=n,t.markDirty())){t.effectSubs().size>0&&j(e).add(t);for(let e of t.computedSubs())A.push(e)}}}finally{A.length=0}},N=e=>{for(;j(e).size>0;){let t=j(e);e.activeDirty=e.activeDirty===`a`?`b`:`a`,j(e).clear();for(let n of t)if(n.hasSubscribers()&&n.refreshIfDirty()){for(let t of n.effectSubs())e.pendingSubscribers.add(t);for(let t of n.computedSubs())t.markDirty()&&t.effectSubs().size>0&&j(e).add(t)}t.clear()}},P=e=>{let t=0;for(;e.pendingSubscribers.size>0||j(e).size>0;){if(++t>100)throw new p(`infinite flush loop (> 100 iterations)`);if(j(e).size>0&&N(e),e.pendingSubscribers.size===0)continue;let n=[...e.pendingSubscribers];e.pendingSubscribers.clear(),te(n,`subscriber errors`)}},F=globalThis.process?.versions!=null,ie=e=>{if(!e.hasSubscribers())return;!O&&F&&!x()&&(O=!0,re(`Signal updated in a Node.js-like environment. The module-level flush queue is shared across concurrent requests — use per-request worker isolation or the @vielzeug/ripple/ssr sub-path for request-isolated scheduling.`));let t=b();M(t,e),t.batchDepth===0&&P(t)},ae=e=>{let t=b();t.batchDepth++;let n;try{n=e()}catch(e){throw t.batchDepth--,t.batchDepth===0&&(t.pendingSubscribers.clear(),t.dirtyWithEffectSubsA.clear(),t.dirtyWithEffectSubsB.clear()),e}return t.batchDepth--,t.batchDepth===0&&P(t),n},I=class{fn_;disposed_=!1;constructor(e){this.fn_=e}get disposed(){return this.disposed_}dispose(){if(this.disposed_)return;this.disposed_=!0;let e=this.fn_;this.fn_=null,e()}[Symbol.dispose](){this.dispose()}},L=Symbol(`ripple.is-signal`),R=Symbol(`ripple.is-computed`),z=Symbol(`ripple.uninitialized`),B=new FinalizationRegistry(({key:e,map:t})=>{t.delete(e)}),V=class{version=0;name;[L]=!0;computedSubs_=new Map;effectSubs_=new Set;constructor(e){this.name=e}addComputedSub(e){let t=new WeakRef(e);this.computedSubs_.set(e,t),B.register(e,{key:e,map:this.computedSubs_},t)}removeComputedSub(e){let t=this.computedSubs_.get(e);t!==void 0&&(this.computedSubs_.delete(e),B.unregister(t))}addEffectSub(e){this.effectSubs_.add(e)}removeEffectSub(e){this.effectSubs_.delete(e)}clearSubscribers(){for(let e of this.computedSubs_.values())B.unregister(e);this.computedSubs_.clear(),this.effectSubs_.clear()}hasSubscribers(){if(this.effectSubs_.size>0)return!0;for(let e of this.computedSubs_.values())if(e.deref()!==void 0)return!0;return!1}*computedSubs(){for(let[e,t]of this.computedSubs_){let n=t.deref();n===void 0?(this.computedSubs_.delete(e),B.unregister(t)):yield n}}effectSubs(){return this.effectSubs_}},H=class extends V{[R]=!0;lastPropEpoch_=0},U=class extends H{value_;dirty_;computing_;disposed_;deps_;compute_;equals_;maxRevision_;constructor(e,t){let{equals:n,name:r}=t??{};super(r),this.value_=z,this.dirty_=!0,this.computing_=!1,this.disposed_=!1,this.deps_=[],this.maxRevision_=-1,this.compute_=e,this.equals_=n===void 0?Object.is:(e,t)=>n(e,t)}markDirty(){return this.disposed_||this.dirty_?!1:(this.dirty_=!0,!0)}refreshIfDirty(){if(!this.dirty_)return!1;if(g()<=this.maxRevision_)return this.dirty_=!1,!1;if(this.deps_.length>0){let e=!0;for(let t of this.deps_){let n=t.source;if(`refreshIfDirty`in n&&n.refreshIfDirty(),n.version!==t.version){e=!1;break}}if(e)return this.dirty_=!1,this.maxRevision_=g(),!1}return this.recompute()}runCompute(e){try{return C({computed:this,depCollector:e,kind:`computed`},this.compute_)}catch(e){throw D(e)}}recompute(){if(this.computing_)throw new f(`computed cycle detected${this.name?` "${this.name}"`:``}`);this.computing_=!0;try{let e=[];u()?.compute?.({name:this.name});let t=this.runCompute(e);return this.dirty_=!1,this.maxRevision_=g(),this.updateDeps(e),this.value_===z||!this.equals_(this.value_,t)?(this.value_=t,this.version++,!0):!1}finally{this.computing_=!1}}updateDeps(e){let t=this.deps_;if(t.length===e.length&&t.every((t,n)=>t.source===e[n].source)){for(let n=0;n<e.length;n++)t[n].version=e[n].version;return}let n=new Set(t.map(e=>e.source)),r=new Set(e.map(e=>e.source));for(let e of t)r.has(e.source)||e.source.removeComputedSub(this);for(let t of e)n.has(t.source)||t.source.addComputedSub(this);this.deps_=e}get value(){return this.disposed_?this.value_===z?void 0:this.value_:(this.refreshIfDirty(),E(this),this.value_)}peek(){return this.disposed_?this.value_===z?void 0:this.value_:(this.refreshIfDirty(),this.value_)}subscribe=e=>{if(this.disposed_){let e=new I(()=>{});return e.dispose(),e}return this.refreshIfDirty(),this.addEffectSub(e),new I(()=>{this.removeEffectSub(e)})};get disposed(){return this.disposed_}dispose(){if(!this.disposed_){this.disposed_=!0;for(let e of this.deps_)e.source.removeComputedSub(this);this.deps_=[],this.clearSubscribers(),u()?.dispose?.({kind:`computed`,name:this.name})}}[Symbol.dispose](){this.dispose()}},W=(e,t)=>{let n=new U(e,t);return T(()=>n.dispose()),n},G=class extends V{value_;equals_;disposed_;constructor(e,t,n){super(n),this.value_=e,this.equals_=t??Object.is,this.disposed_=!1}get value(){return this.disposed_||E(this),this.value_}set value(e){if(this.disposed_||this.equals_(this.value_,e))return;let t=this.value_;this.value_=e,this.version=h(),u()?.write?.({name:this.name,newValue:e,oldValue:t}),ie(this)}peek(){return this.value_}subscribe=e=>{if(this.disposed_){let e=new I(()=>{});return e.dispose(),e}return this.addEffectSub(e),new I(()=>{this.removeEffectSub(e)})};get disposed(){return this.disposed_}dispose(){this.disposed_||(this.disposed_=!0,this.clearSubscribers(),u()?.dispose?.({kind:`signal`,name:this.name}))}[Symbol.dispose](){this.dispose()}},K=(e,t)=>new G(e,t?.equals,t?.name),q=!globalThis.__SCOUT_PROD__;function oe(e){q&&console.warn(`[@vielzeug/scout] ${e}`)}function J(e){return e.toLowerCase().replace(/[^\p{L}\p{N}\s']/gu,` `).trim().replace(/\s+/g,` `)}function Y(e){return e==null?``:typeof e==`string`?e:typeof e==`number`||typeof e==`boolean`?String(e):Array.isArray(e)?(oe("defaultStringify: received an array value — provide a custom `stringify` on the FieldDef to control array indexing.\nFalling back to joining string/number/boolean elements with a space."),e.filter(e=>typeof e==`string`||typeof e==`number`||typeof e==`boolean`).join(` `)):``}function X(e){let t=new Set,n=` ${e} `;for(let e=0;e<n.length-2;e++)t.add(n.slice(e,e+3));return t}function se(e,t){if(e.size===0||t.size===0)return 0;let n=0;for(let r of e)t.has(r)&&n++;return 2*n/(e.size+t.size)}function ce(e){return e.map(e=>typeof e==`string`?{field:e,stringify:Y,weight:1}:{field:e.field,stringify:e.stringify??Y,weight:e.weight??1})}function Z(e,t){if(t.fields.length===0)throw new a(`createIndex: at least one field is required.`);let n=ce(t.fields),r=n.reduce((e,t)=>Math.max(e,t.weight),1),i=t.threshold??.2,s=t.limit??50,c=t.minQueryLength??3,l=new Map,u=new Map,d=new Set;function f(){for(let e of d)e()}let p=null,m=null;function h(e){return e===p&&m!==null?m:(p=e,m=X(e),m)}function g(e,t){for(let n of t){let t=u.get(n);t||(t=new Set,u.set(n,t)),t.add(e)}}function _(e,t){for(let n of t){let t=u.get(n);t&&(t.delete(e),t.size===0&&u.delete(n))}}function v(e){let t=new Map,r=new Map;for(let{field:i,stringify:a}of n){let n=e[i],o=a(n),s=J(o),c=s.length>=1?X(s):new Set;t.set(i,c),r.set(i,o),g(e,c)}l.set(e,{trigrams:t,values:r})}function y(e){let t=new Set;for(let[n,r]of l)for(let i of r.values.values())if(i.toLowerCase().includes(e)){t.add(n);break}return t}function b(e){let t=new Set;for(let n of e){let e=u.get(n);if(e)for(let n of e)t.add(n)}return t}function x(e,t,i){let a=0;for(let{field:o,weight:s}of n){let n;if(t===null)n=+!!(i.values.get(o)??``).toLowerCase().includes(e);else{let e=i.trigrams.get(o);if(!e||e.size===0)continue;n=se(t,e)}let c=s/r*n;c>a&&(a=c)}return a}function S(e,t){let r=[];for(let{field:i}of n){let n=t.get(i);if(!n)continue;let a=o(n,e);a.length>0&&r.push({field:i,ranges:a})}return r}for(let t of e)v(t);return{add(e){l.has(e)||(v(e),f())},get items(){return[...l.keys()]},onMutate(e){return d.add(e),()=>{d.delete(e)}},reindex(e){let t=l.get(e);if(!t)return;let r=!1;for(let{field:i,stringify:a}of n){let n=e[i],o=a(n);if(o===t.values.get(i))continue;r=!0;let s=t.trigrams.get(i);s&&_(e,s);let c=J(o),l=c.length>=1?X(c):new Set;t.trigrams.set(i,l),t.values.set(i,o),g(e,l)}r&&f()},remove(e){let t=l.get(e);if(t){for(let n of t.trigrams.values())_(e,n);l.delete(e),f()}},search(e,t){let n=t?.threshold??i,r=Math.max(0,t?.limit??s),a=t?.minQueryLength??c;if(!e.trim())return[...l.keys()].slice(0,r).map(e=>({item:e,matches:[],score:1}));let o=J(e);if(!o)return[];let u=o.length<a?null:h(o),d=u===null?y(o):b(u),f=[];for(let e of d){let t=l.get(e);if(!t)continue;let r=x(o,u,t);if(r>=n){let n=S(o,t.values);f.push({item:e,matches:n,score:r})}}return f.sort((e,t)=>t.score-e.score).slice(0,r)},get size(){return l.size}}}var le=200;function Q(e,t={}){let{debounce:n=le,limit:r,minQueryLength:a,threshold:o}=t,s=K(``,{name:`scout:query`}),c=K(``,{name:`scout:committedQuery`}),l=K(0,{name:`scout:indexVersion`}),u=e.onMutate(()=>{l.value++}),d=W(()=>s.value!==c.value,{name:`scout:isSearching`}),f=W(()=>(l.value,e.search(c.value,{limit:r,minQueryLength:a,threshold:o})),{name:`scout:results`}),p=null;function m(){p!==null&&(clearTimeout(p),p=null)}let h=s.subscribe(()=>{let e=s.peek();if(m(),e!==c.peek()){if(n===0){c.value=e;return}p=setTimeout(()=>{c.value=e,p=null},n)}});function g(){if(_)throw new i(`SearchState.clear() called after dispose()`);m(),ae(()=>{s.value=``,c.value=``})}let _=!1,v=new AbortController;function y(){_=!0,v.abort(),m(),h.dispose(),u(),s.dispose(),c.dispose(),d.dispose(),f.dispose(),l.dispose()}return{clear:g,get disposalSignal(){return v.signal},dispose:y,get disposed(){return _},isSearching:d,query:s,results:f,[Symbol.dispose](){y()}}}function ue(e,t){let n=Z(e,{fields:t.fields,limit:t.limit,minQueryLength:t.minQueryLength,threshold:t.threshold});return{...Q(n,{debounce:t.debounce}),index:n}}function de(e){let t=fe();return t?[...t.segment(e)].filter(e=>e.isWordLike).map(e=>e.segment).join(` `):e}var $;function fe(){return $===void 0&&($=typeof Intl>`u`||typeof Intl.Segmenter!=`function`?null:new Intl.Segmenter(void 0,{granularity:`word`})),$}return e.ScoutDisposedError=i,e.ScoutError=r,e.ScoutIndexError=a,e.createIndex=Z,e.createReactiveSearch=ue,e.createSearch=Q,e.findMatchRanges=o,e.highlight=s,e.highlightField=c,e.segmentWords=de,e.toFilterPredicate=n,e.toSearchFn=t,e})({});
|
|
2
|
+
//# sourceMappingURL=scout.iife.js.map
|