@vielzeug/scout 2.0.1 → 2.1.0
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 +11 -4
- package/dist/_index-state.cjs +2 -0
- package/dist/_index-state.cjs.map +1 -0
- package/dist/_index-state.d.ts +4 -0
- package/dist/_index-state.d.ts.map +1 -0
- package/dist/_index-state.js +12 -0
- package/dist/_index-state.js.map +1 -0
- package/dist/adapters.cjs +1 -1
- package/dist/adapters.cjs.map +1 -1
- package/dist/adapters.d.ts.map +1 -1
- package/dist/adapters.js +9 -5
- package/dist/adapters.js.map +1 -1
- package/dist/errors.cjs +1 -1
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.d.ts +2 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/highlight.cjs +1 -1
- package/dist/highlight.cjs.map +1 -1
- package/dist/highlight.d.ts +1 -1
- package/dist/highlight.d.ts.map +1 -1
- package/dist/highlight.js +18 -17
- package/dist/highlight.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/reactive.cjs +1 -1
- package/dist/reactive.cjs.map +1 -1
- package/dist/reactive.d.ts +2 -2
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +47 -45
- package/dist/reactive.js.map +1 -1
- package/dist/scout-index.cjs +1 -1
- package/dist/scout-index.cjs.map +1 -1
- package/dist/scout-index.d.ts +11 -4
- package/dist/scout-index.d.ts.map +1 -1
- package/dist/scout-index.js +104 -75
- package/dist/scout-index.js.map +1 -1
- package/dist/scout.cjs +1 -1
- package/dist/scout.cjs.map +1 -1
- package/dist/scout.iife.js +1 -1
- package/dist/scout.iife.js.map +1 -1
- package/dist/scout.js +1 -1
- package/dist/scout.js.map +1 -1
- package/dist/types.d.ts +10 -8
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/reactive.js.map
CHANGED
|
@@ -1 +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();\n unsubscribeMutations();\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 Object.assign(Object.create(state), { index }) as ReactiveSearch<T>;\n}\n"],"mappings":";;;;AAeA,IAAM,IAAmB;AAsCzB,SAAgB,EAAgB,GAAsB,IAA+B,CAAC,GAAmB;CACvG,IAAM,EAAE,UAAU,IAAa,GAAkB,UAAO,mBAAgB,iBAAc,
|
|
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 { ScoutConfigurationError, 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()` / `.setItems()` (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 if (!Number.isFinite(debounceMs) || !Number.isInteger(debounceMs) || debounceMs < 0) {\n throw new ScoutConfigurationError('debounce must be a finite non-negative integer.');\n }\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();\n unsubscribeMutations();\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`, `setItems`) 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 Object.assign(Object.create(state), { index }) as ReactiveSearch<T>;\n}\n"],"mappings":";;;;AAeA,IAAM,IAAmB;AAsCzB,SAAgB,EAAgB,GAAsB,IAA+B,CAAC,GAAmB;CACvG,IAAM,EAAE,UAAU,IAAa,GAAkB,UAAO,mBAAgB,iBAAc;CAEtF,IAAI,CAAC,OAAO,SAAS,CAAU,KAAK,CAAC,OAAO,UAAU,CAAU,KAAK,IAAa,GAChF,MAAM,IAAI,EAAwB,iDAAiD;CAGrF,IAAM,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;EAKvB,AAJA,IAAa,IACb,EAAG,MAAM,GACT,EAAY,GACZ,EAAa,GACb,EAAqB;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,GACK,IAAQ,EAAa,GAAO,EAAE,UAAU,EAAQ,SAAS,CAAC;CAEhE,OAAO,OAAO,OAAO,OAAO,OAAO,CAAK,GAAG,EAAE,SAAM,CAAC;AACtD"}
|
package/dist/scout-index.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./
|
|
1
|
+
const e=require("./_index-state.cjs"),t=require("./errors.cjs"),n=require("./tokenize.cjs"),r=require("./highlight.cjs"),i=require("./trigram.cjs");function a(e,n,r){if(!Number.isFinite(e)||!Number.isInteger(e)||e<r)throw new t.ScoutConfigurationError(`${n} must be a finite integer greater than or equal to ${r}.`);return e}function o(e,n,r,i=1/0){if(!Number.isFinite(e)||e<r||e>i)throw new t.ScoutConfigurationError(`${n} must be a finite number between ${r} and ${i}.`);return e}function s(e){return e.map(e=>typeof e==`string`?{field:e,stringify:n.defaultStringify,weight:1}:{field:e.field,stringify:e.stringify??n.defaultStringify,weight:o(e.weight??1,`weight for field "${e.field}"`,Number.MIN_VALUE)})}function c(c,l){if(l.fields.length===0)throw new t.ScoutConfigurationError(`createIndex: at least one field is required.`);let u=s(l.fields),d=u.reduce((e,t)=>Math.max(e,t.weight),1),f=o(l.threshold??.2,`threshold`,0,1),p=a(l.limit??50,`limit`,0),m=a(l.minQueryLength??3,`minQueryLength`,1),h=new Map,g=new Map,_=new Set,v=0;function y(){v++;for(let e of _)e()}let b=null,x=null;function S(e){return e===b&&x!==null?x:(b=e,x=i.generateTrigrams(e),x)}function C(e,t){for(let n of t){let t=g.get(n);t||(t=new Set,g.set(n,t)),t.add(e)}}function w(e,t){for(let n of t){let t=g.get(n);t&&(t.delete(e),t.size===0&&g.delete(n))}}function T(e){let t=new Map,r=new Map;for(let{field:a,stringify:o}of u){let s=e[a],c=o(s),l=n.tokenize(c),u=l.length>=1?i.generateTrigrams(l):new Set;t.set(a,u),r.set(a,c),C(e,u)}h.set(e,{trigrams:t,values:r})}function E(e){let t=new Set;for(let[n,r]of h)for(let i of r.values.values())if(i.toLowerCase().includes(e)){t.add(n);break}return t}function D(e){let t=new Set;for(let n of e){let e=g.get(n);if(e)for(let n of e)t.add(n)}return t}function O(e,t,n){let r=0;for(let{field:a,weight:o}of u){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=i.overlapSimilarity(t,e)}let c=o/d*s;c>r&&(r=c)}return r}function k(e,t){let n=[];for(let{field:i}of u){let a=t.get(i);if(!a)continue;let o=r.findMatchRanges(a,e);o.length>0&&n.push({field:i,ranges:o})}return n}function A(e){let t=h.get(e);if(!t)return!1;let r=!1;for(let{field:a,stringify:o}of u){let s=o(e[a]);if(s===t.values.get(a))continue;r=!0;let c=t.trigrams.get(a);c&&w(e,c);let l=n.tokenize(s),u=l.length>=1?i.generateTrigrams(l):new Set;t.trigrams.set(a,u),t.values.set(a,s),C(e,u)}return r}function j(e){let t=h.get(e);if(!t)return!1;for(let n of t.trigrams.values())w(e,n);return h.delete(e),!0}for(let e of c)h.has(e)||T(e);let M={add(e){h.has(e)||(T(e),y())},get items(){return[...h.keys()]},onMutate(e){return _.add(e),()=>{_.delete(e)}},reindex(e){A(e)&&y()},remove(e){j(e)&&y()},search(e,t){let r=o(t?.threshold??f,`threshold`,0,1),i=a(t?.limit??p,`limit`,0),s=a(t?.minQueryLength??m,`minQueryLength`,1);if(!e.trim())return[...h.keys()].slice(0,i).map(e=>({item:e,matches:[],score:1}));let c=n.tokenize(e);if(!c)return[];let l=c.length<s?null:S(c),u=l===null?E(c):D(l),d=[];for(let e of u){let t=h.get(e);if(!t)continue;let n=O(c,l,t);if(n>=r){let r=k(c,t.values);d.push({item:e,matches:r,score:n})}}return d.sort((e,t)=>t.score-e.score).slice(0,i)},setItems(e){let t=new Set(e),n=[...t],r=!1;for(let e of[...h.keys()])t.has(e)||(j(e),r=!0);for(let e of n)h.has(e)?A(e)&&(r=!0):(T(e),r=!0);let i=[...h.keys()];if(i.length!==n.length||i.some((e,t)=>e!==n[t])){let e=new Map(n.map(e=>[e,h.get(e)]));h.clear();for(let[t,n]of e)h.set(t,n);r=!0}r&&y()},get size(){return h.size}};return e.registerIndexRevision(M,()=>v),M}exports.createIndex=c;
|
|
2
2
|
//# sourceMappingURL=scout-index.cjs.map
|
package/dist/scout-index.cjs.map
CHANGED
|
@@ -1 +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 { generateTrigrams, overlapSimilarity } 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 = overlapSimilarity(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":"oHA+DA,SAAS,EAAiB,EAAoD,CAC5E,OAAO,EAAK,IAAK,GACX,OAAO,GAAQ,SACV,CAAE,MAAO,EAAK,UAAW,EAAA,iBAAkB,OAAQ,CAAE,EAGvD,CACL,MAAO,EAAI,MACX,UAAW,EAAI,WAAa,EAAA,iBAC5B,OAAQ,EAAI,QAAU,CACxB,CACD,CACH,CAqBA,SAAgB,EAAe,EAAY,EAA8C,CACvF,GAAI,EAAQ,OAAO,SAAW,EAC5B,MAAM,IAAI,EAAA,gBAAgB,8CAA8C,EAG1E,IAAM,EAAS,EAAc,EAAQ,MAAM,EACrC,EAAY,EAAO,QAAQ,EAAK,IAAM,KAAK,IAAI,EAAK,EAAE,MAAM,EAAG,CAAC,EAChE,EAAmB,EAAQ,WAAa,GACxC,EAAe,EAAQ,OAAS,GAChC,EAAwB,EAAQ,gBAAkB,EAGlD,EAAW,IAAI,IAEf,EAAgB,IAAI,IACpB,EAAoB,IAAI,IAE9B,SAAS,GAAuB,CAC9B,IAAK,IAAM,KAAY,EAAmB,EAAS,CACrD,CAGA,IAAI,EAAkC,KAClC,EAAqC,KAEzC,SAAS,EAAiB,EAAiC,CAMzD,OALI,IAAe,GAAoB,IAAmB,KAAa,GAEvE,EAAmB,EACnB,EAAiB,EAAA,iBAAiB,CAAU,EAErC,EACT,CAEA,SAAS,EAAgB,EAAS,EAAkC,CAClE,IAAK,IAAM,KAAW,EAAe,CACnC,IAAI,EAAS,EAAc,IAAI,CAAO,EAEjC,IACH,EAAS,IAAI,IACb,EAAc,IAAI,EAAS,CAAM,GAGnC,EAAO,IAAI,CAAI,CACjB,CACF,CAEA,SAAS,EAAqB,EAAS,EAAkC,CACvE,IAAK,IAAM,KAAW,EAAe,CACnC,IAAM,EAAS,EAAc,IAAI,CAAO,EAEpC,IACF,EAAO,OAAO,CAAI,EAEd,EAAO,OAAS,GAAG,EAAc,OAAO,CAAO,EAEvD,CACF,CAEA,SAAS,EAAQ,EAAe,CAC9B,IAAM,EAAW,IAAI,IACf,EAAS,IAAI,IAEnB,IAAK,GAAM,CAAE,QAAO,eAAe,EAAQ,CACzC,IAAM,EAAM,EAAK,GACX,EAAO,EAAU,CAAG,EACpB,EAAa,EAAA,SAAS,CAAI,EAC1B,EAAgB,EAAW,QAAU,EAAI,EAAA,iBAAiB,CAAU,EAAI,IAAI,IAElF,EAAS,IAAI,EAAO,CAAa,EACjC,EAAO,IAAI,EAAO,CAAI,EACtB,EAAgB,EAAM,CAAa,CACrC,CAEA,EAAS,IAAI,EAAM,CAAE,WAAU,QAAO,CAAC,CACzC,CAOA,SAAS,EAAgB,EAAuB,CAC9C,IAAM,EAAS,IAAI,IAEnB,IAAK,GAAM,CAAC,EAAM,KAAW,EAC3B,IAAK,IAAM,KAAS,EAAO,OAAO,OAAO,EACvC,GAAI,EAAM,YAAY,CAAC,CAAC,SAAS,CAAK,EAAG,CACvC,EAAO,IAAI,CAAI,EACf,KACF,CAIJ,OAAO,CACT,CAEA,SAAS,EAAkB,EAAoC,CAC7D,IAAM,EAAa,IAAI,IAEvB,IAAK,IAAM,KAAW,EAAe,CACnC,IAAM,EAAQ,EAAc,IAAI,CAAO,EAEvC,GAAI,EACF,IAAK,IAAM,KAAQ,EAAO,EAAW,IAAI,CAAI,CAEjD,CAEA,OAAO,CACT,CAEA,SAAS,EAAe,EAAoB,EAAmC,EAA4B,CACzG,IAAI,EAAY,EAEhB,IAAK,GAAM,CAAE,QAAO,YAAY,EAAQ,CACtC,IAAI,EAEJ,GAAI,IAAkB,KAGpB,EAAA,IAFY,EAAO,OAAO,IAAI,CAAK,GAAK,GAAA,CAEvB,YAAY,CAAC,CAAC,SAAS,CAAU,MAC7C,CACL,IAAM,EAAe,EAAO,SAAS,IAAI,CAAK,EAE9C,GAAI,CAAC,GAAgB,EAAa,OAAS,EAAG,SAE9C,EAAa,EAAA,kBAAkB,EAAe,CAAY,CAC5D,CAEA,IAAM,EAAyB,EAAS,EAAvB,EAEb,EAAW,IAAW,EAAY,EACxC,CAEA,OAAO,CACT,CAEA,SAAS,EAAe,EAAe,EAA6D,CAClG,IAAM,EAA0C,CAAC,EAEjD,IAAK,GAAM,CAAE,WAAW,EAAQ,CAC9B,IAAM,EAAO,EAAO,IAAI,CAAK,EAE7B,GAAI,CAAC,EAAM,SAEX,IAAM,EAAS,EAAA,gBAAgB,EAAM,CAAK,EAEtC,EAAO,OAAS,GAAG,EAAQ,KAAK,CAAE,QAAO,QAAO,CAAC,CACvD,CAEA,OAAO,CACT,CAEA,IAAK,IAAM,KAAQ,EACjB,EAAQ,CAAI,EAGd,MAAO,CACL,IAAI,EAAe,CACb,EAAS,IAAI,CAAI,IAErB,EAAQ,CAAI,EACZ,EAAe,EACjB,EAEA,IAAI,OAAsB,CACxB,MAAO,CAAC,GAAG,EAAS,KAAK,CAAC,CAC5B,EAEA,SAAS,EAAkC,CAGzC,OAFA,EAAkB,IAAI,CAAQ,MAEjB,CACX,EAAkB,OAAO,CAAQ,CACnC,CACF,EAEA,QAAQ,EAAe,CACrB,IAAM,EAAS,EAAS,IAAI,CAAI,EAEhC,GAAI,CAAC,EAAQ,OAEb,IAAI,EAAU,GAEd,IAAK,GAAM,CAAE,QAAO,eAAe,EAAQ,CACzC,IAAM,EAAM,EAAK,GACX,EAAU,EAAU,CAAG,EAG7B,GAAI,IAFY,EAAO,OAAO,IAAI,CAElB,EAAS,SAEzB,EAAU,GAEV,IAAM,EAAc,EAAO,SAAS,IAAI,CAAK,EAEzC,GAAa,EAAqB,EAAM,CAAW,EAEvD,IAAM,EAAa,EAAA,SAAS,CAAO,EAC7B,EAAc,EAAW,QAAU,EAAI,EAAA,iBAAiB,CAAU,EAAI,IAAI,IAEhF,EAAO,SAAS,IAAI,EAAO,CAAW,EACtC,EAAO,OAAO,IAAI,EAAO,CAAO,EAChC,EAAgB,EAAM,CAAW,CACnC,CAEI,GAAS,EAAe,CAC9B,EAEA,OAAO,EAAe,CACpB,IAAM,EAAS,EAAS,IAAI,CAAI,EAE3B,KAEL,KAAK,IAAM,KAAiB,EAAO,SAAS,OAAO,EACjD,EAAqB,EAAM,CAAa,EAG1C,EAAS,OAAO,CAAI,EACpB,EAAe,CAJ2B,CAK5C,EAEA,OAAO,EAAe,EAAgD,CACpE,IAAM,EAAY,GAAS,WAAa,EAClC,EAAQ,KAAK,IAAI,EAAG,GAAS,OAAS,CAAY,EAClD,EAAiB,GAAS,gBAAkB,EAElD,GAAI,CAAC,EAAM,KAAK,EACd,MAAO,CAAC,GAAG,EAAS,KAAK,CAAC,CAAC,CAAC,MAAM,EAAG,CAAK,CAAC,CAAC,IAAK,IAAU,CAAE,OAAM,QAAS,CAAC,EAAG,MAAO,CAAE,EAAE,EAG7F,IAAM,EAAa,EAAA,SAAS,CAAK,EAGjC,GAAI,CAAC,EAAY,MAAO,CAAC,EAGzB,IAAM,EADU,EAAW,OAAS,EACJ,KAAO,EAAiB,CAAU,EAC5D,EAAa,IAAkB,KAAO,EAAgB,CAAU,EAAI,EAAkB,CAAa,EACnG,EAA6B,CAAC,EAEpC,IAAK,IAAM,KAAQ,EAAY,CAC7B,IAAM,EAAS,EAAS,IAAI,CAAI,EAEhC,GAAI,CAAC,EAAQ,SAEb,IAAM,EAAQ,EAAe,EAAY,EAAe,CAAM,EAE9D,GAAI,GAAS,EAAW,CACtB,IAAM,EAAU,EAAe,EAAY,EAAO,MAAM,EAExD,EAAQ,KAAK,CAAE,OAAM,UAAS,OAAM,CAAC,CACvC,CACF,CAEA,OAAO,EAAQ,MAAM,EAAG,IAAM,EAAE,MAAQ,EAAE,KAAK,CAAC,CAAC,MAAM,EAAG,CAAK,CACjE,EAEA,IAAI,MAAe,CACjB,OAAO,EAAS,IAClB,CACF,CACF"}
|
|
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 { registerIndexRevision } from './_index-state';\nimport { ScoutConfigurationError } from './errors';\nimport { findMatchRanges } from './highlight';\nimport { defaultStringify, tokenize } from './tokenize';\nimport { generateTrigrams, overlapSimilarity } 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 * Reconciles the index to `items` by reference identity. Retained items are reindexed,\n * new items are added, missing items are removed, and one mutation notification fires\n * when indexed corpus or field values change. Duplicate references collapse to one item.\n */\n setItems(items: readonly 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 changed `add()` / `remove()` / `reindex()`\n * / `setItems()` operation. No-ops — e.g. removing an unindexed item or reconciling an\n * unchanged corpus — do not fire it. Each changed `setItems()` reconciliation fires once.\n * 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 requireFiniteInteger(value: number, name: string, minimum: number): number {\n if (!Number.isFinite(value) || !Number.isInteger(value) || value < minimum) {\n throw new ScoutConfigurationError(`${name} must be a finite integer greater than or equal to ${minimum}.`);\n }\n\n return value;\n}\n\nfunction requireFiniteNumber(value: number, name: string, minimum: number, maximum = Number.POSITIVE_INFINITY): number {\n if (!Number.isFinite(value) || value < minimum || value > maximum) {\n throw new ScoutConfigurationError(`${name} must be a finite number between ${minimum} and ${maximum}.`);\n }\n\n return value;\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: requireFiniteNumber(def.weight ?? 1, `weight for field \"${def.field}\"`, Number.MIN_VALUE),\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 {ScoutConfigurationError} If options use an invalid field or numeric configuration.\n */\nexport function createIndex<T>(items: T[], options: ScoutIndexOptions<T>): ScoutIndex<T> {\n if (options.fields.length === 0) {\n throw new ScoutConfigurationError('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 = requireFiniteNumber(options.threshold ?? 0.2, 'threshold', 0, 1);\n const defaultLimit = requireFiniteInteger(options.limit ?? 50, 'limit', 0);\n const defaultMinQueryLength = requireFiniteInteger(options.minQueryLength ?? 3, 'minQueryLength', 1);\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 let revision = 0;\n\n function notifyMutation(): void {\n revision++;\n\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 = overlapSimilarity(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 function reindexItem(item: T): boolean {\n const record = itemData.get(item);\n\n if (!record) return false;\n\n let changed = false;\n\n for (const { field, stringify } of fields) {\n const newText = stringify(item[field]);\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 return changed;\n }\n\n function removeItem(item: T): boolean {\n const record = itemData.get(item);\n\n if (!record) return false;\n\n for (const fieldTrigrams of record.trigrams.values()) {\n removeFieldFromIndex(item, fieldTrigrams);\n }\n\n itemData.delete(item);\n\n return true;\n }\n\n for (const item of items) {\n if (!itemData.has(item)) addItem(item);\n }\n\n const index: ScoutIndex<T> = {\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 if (reindexItem(item)) notifyMutation();\n },\n\n remove(item: T): void {\n if (removeItem(item)) notifyMutation();\n },\n\n search(query: string, options?: SearchConstraints): SearchResult<T>[] {\n const threshold = requireFiniteNumber(options?.threshold ?? defaultThreshold, 'threshold', 0, 1);\n const limit = requireFiniteInteger(options?.limit ?? defaultLimit, 'limit', 0);\n const minQueryLength = requireFiniteInteger(\n options?.minQueryLength ?? defaultMinQueryLength,\n 'minQueryLength',\n 1,\n );\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 setItems(items: readonly T[]): void {\n const incoming = new Set(items);\n const next = [...incoming];\n let changed = false;\n\n for (const item of [...itemData.keys()]) {\n if (incoming.has(item)) continue;\n\n removeItem(item);\n changed = true;\n }\n\n for (const item of next) {\n if (!itemData.has(item)) {\n addItem(item);\n changed = true;\n } else if (reindexItem(item)) {\n changed = true;\n }\n }\n\n const current = [...itemData.keys()];\n const orderChanged = current.length !== next.length || current.some((item, index) => item !== next[index]);\n\n if (orderChanged) {\n const records = new Map(next.map((item) => [item, itemData.get(item)!]));\n\n itemData.clear();\n\n for (const [item, record] of records) itemData.set(item, record);\n\n changed = true;\n }\n\n if (changed) notifyMutation();\n },\n\n get size(): number {\n return itemData.size;\n },\n };\n\n registerIndexRevision(index, () => revision);\n\n return index;\n}\n"],"mappings":"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"}
|
package/dist/scout-index.d.ts
CHANGED
|
@@ -23,6 +23,12 @@ export interface ScoutIndex<T> {
|
|
|
23
23
|
* No-op if the item is not in the index.
|
|
24
24
|
*/
|
|
25
25
|
remove(item: T): void;
|
|
26
|
+
/**
|
|
27
|
+
* Reconciles the index to `items` by reference identity. Retained items are reindexed,
|
|
28
|
+
* new items are added, missing items are removed, and one mutation notification fires
|
|
29
|
+
* when indexed corpus or field values change. Duplicate references collapse to one item.
|
|
30
|
+
*/
|
|
31
|
+
setItems(items: readonly T[]): void;
|
|
26
32
|
/**
|
|
27
33
|
* Searches the index for `query` and returns results sorted by score descending.
|
|
28
34
|
*
|
|
@@ -32,9 +38,10 @@ export interface ScoutIndex<T> {
|
|
|
32
38
|
*/
|
|
33
39
|
search(query: string, options?: SearchConstraints): SearchResult<T>[];
|
|
34
40
|
/**
|
|
35
|
-
* Subscribes `listener` to be called after every `add()` / `remove()` / `reindex()`
|
|
36
|
-
*
|
|
37
|
-
* it
|
|
41
|
+
* Subscribes `listener` to be called after every changed `add()` / `remove()` / `reindex()`
|
|
42
|
+
* / `setItems()` operation. No-ops — e.g. removing an unindexed item or reconciling an
|
|
43
|
+
* unchanged corpus — do not fire it. Each changed `setItems()` reconciliation fires once.
|
|
44
|
+
* Returns an unsubscribe function.
|
|
38
45
|
*
|
|
39
46
|
* Framework-agnostic extension point: `createSearch()` uses this internally to keep
|
|
40
47
|
* reactive `results` in sync with index mutations. Most callers won't need this directly.
|
|
@@ -58,7 +65,7 @@ export interface ScoutIndex<T> {
|
|
|
58
65
|
* const results = index.search('alice');
|
|
59
66
|
* ```
|
|
60
67
|
*
|
|
61
|
-
* @throws {
|
|
68
|
+
* @throws {ScoutConfigurationError} If options use an invalid field or numeric configuration.
|
|
62
69
|
*/
|
|
63
70
|
export declare function createIndex<T>(items: T[], options: ScoutIndexOptions<T>): ScoutIndex<T>;
|
|
64
71
|
//# sourceMappingURL=scout-index.d.ts.map
|
|
@@ -1 +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;
|
|
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;AAqBxG;;;;;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;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,GAAG,IAAI,CAAC;IACpC;;;;;;OAMG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC;IACtE;;;;;;;;OAQG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;CAC5C;AAgCD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CA6TvF"}
|
package/dist/scout-index.js
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { registerIndexRevision as e } from "./_index-state.js";
|
|
2
|
+
import { ScoutConfigurationError as t } from "./errors.js";
|
|
3
3
|
import { defaultStringify as n, tokenize as r } from "./tokenize.js";
|
|
4
|
-
import {
|
|
4
|
+
import { findMatchRanges as i } from "./highlight.js";
|
|
5
|
+
import { generateTrigrams as a, overlapSimilarity as o } from "./trigram.js";
|
|
5
6
|
//#region src/scout-index.ts
|
|
6
|
-
function
|
|
7
|
+
function s(e, n, r) {
|
|
8
|
+
if (!Number.isFinite(e) || !Number.isInteger(e) || e < r) throw new t(`${n} must be a finite integer greater than or equal to ${r}.`);
|
|
9
|
+
return e;
|
|
10
|
+
}
|
|
11
|
+
function c(e, n, r, i = Infinity) {
|
|
12
|
+
if (!Number.isFinite(e) || e < r || e > i) throw new t(`${n} must be a finite number between ${r} and ${i}.`);
|
|
13
|
+
return e;
|
|
14
|
+
}
|
|
15
|
+
function l(e) {
|
|
7
16
|
return e.map((e) => typeof e == "string" ? {
|
|
8
17
|
field: e,
|
|
9
18
|
stringify: n,
|
|
@@ -11,152 +20,172 @@ function o(e) {
|
|
|
11
20
|
} : {
|
|
12
21
|
field: e.field,
|
|
13
22
|
stringify: e.stringify ?? n,
|
|
14
|
-
weight: e.weight ?? 1
|
|
23
|
+
weight: c(e.weight ?? 1, `weight for field "${e.field}"`, Number.MIN_VALUE)
|
|
15
24
|
});
|
|
16
25
|
}
|
|
17
|
-
function
|
|
18
|
-
if (
|
|
19
|
-
let
|
|
20
|
-
function
|
|
21
|
-
|
|
26
|
+
function u(n, u) {
|
|
27
|
+
if (u.fields.length === 0) throw new t("createIndex: at least one field is required.");
|
|
28
|
+
let d = l(u.fields), f = d.reduce((e, t) => Math.max(e, t.weight), 1), p = c(u.threshold ?? .2, "threshold", 0, 1), m = s(u.limit ?? 50, "limit", 0), h = s(u.minQueryLength ?? 3, "minQueryLength", 1), g = /* @__PURE__ */ new Map(), _ = /* @__PURE__ */ new Map(), v = /* @__PURE__ */ new Set(), y = 0;
|
|
29
|
+
function b() {
|
|
30
|
+
y++;
|
|
31
|
+
for (let e of v) e();
|
|
22
32
|
}
|
|
23
|
-
let
|
|
24
|
-
function
|
|
25
|
-
return e ===
|
|
33
|
+
let x = null, S = null;
|
|
34
|
+
function C(e) {
|
|
35
|
+
return e === x && S !== null ? S : (x = e, S = a(e), S);
|
|
26
36
|
}
|
|
27
|
-
function
|
|
37
|
+
function w(e, t) {
|
|
28
38
|
for (let n of t) {
|
|
29
|
-
let t =
|
|
30
|
-
t || (t = /* @__PURE__ */ new Set(),
|
|
39
|
+
let t = _.get(n);
|
|
40
|
+
t || (t = /* @__PURE__ */ new Set(), _.set(n, t)), t.add(e);
|
|
31
41
|
}
|
|
32
42
|
}
|
|
33
|
-
function
|
|
43
|
+
function T(e, t) {
|
|
34
44
|
for (let n of t) {
|
|
35
|
-
let t =
|
|
36
|
-
t && (t.delete(e), t.size === 0 &&
|
|
45
|
+
let t = _.get(n);
|
|
46
|
+
t && (t.delete(e), t.size === 0 && _.delete(n));
|
|
37
47
|
}
|
|
38
48
|
}
|
|
39
|
-
function
|
|
49
|
+
function E(e) {
|
|
40
50
|
let t = /* @__PURE__ */ new Map(), n = /* @__PURE__ */ new Map();
|
|
41
|
-
for (let { field:
|
|
42
|
-
let s = e[
|
|
43
|
-
t.set(
|
|
51
|
+
for (let { field: i, stringify: o } of d) {
|
|
52
|
+
let s = e[i], c = o(s), l = r(c), u = l.length >= 1 ? a(l) : /* @__PURE__ */ new Set();
|
|
53
|
+
t.set(i, u), n.set(i, c), w(e, u);
|
|
44
54
|
}
|
|
45
|
-
|
|
55
|
+
g.set(e, {
|
|
46
56
|
trigrams: t,
|
|
47
57
|
values: n
|
|
48
58
|
});
|
|
49
59
|
}
|
|
50
|
-
function
|
|
60
|
+
function D(e) {
|
|
51
61
|
let t = /* @__PURE__ */ new Set();
|
|
52
|
-
for (let [n, r] of
|
|
62
|
+
for (let [n, r] of g) for (let i of r.values.values()) if (i.toLowerCase().includes(e)) {
|
|
53
63
|
t.add(n);
|
|
54
64
|
break;
|
|
55
65
|
}
|
|
56
66
|
return t;
|
|
57
67
|
}
|
|
58
|
-
function
|
|
68
|
+
function O(e) {
|
|
59
69
|
let t = /* @__PURE__ */ new Set();
|
|
60
70
|
for (let n of e) {
|
|
61
|
-
let e =
|
|
71
|
+
let e = _.get(n);
|
|
62
72
|
if (e) for (let n of e) t.add(n);
|
|
63
73
|
}
|
|
64
74
|
return t;
|
|
65
75
|
}
|
|
66
|
-
function
|
|
76
|
+
function k(e, t, n) {
|
|
67
77
|
let r = 0;
|
|
68
|
-
for (let { field: i, weight:
|
|
78
|
+
for (let { field: i, weight: a } of d) {
|
|
69
79
|
let s;
|
|
70
80
|
if (t === null) s = +!!(n.values.get(i) ?? "").toLowerCase().includes(e);
|
|
71
81
|
else {
|
|
72
82
|
let e = n.trigrams.get(i);
|
|
73
83
|
if (!e || e.size === 0) continue;
|
|
74
|
-
s =
|
|
84
|
+
s = o(t, e);
|
|
75
85
|
}
|
|
76
|
-
let c =
|
|
86
|
+
let c = a / f * s;
|
|
77
87
|
c > r && (r = c);
|
|
78
88
|
}
|
|
79
89
|
return r;
|
|
80
90
|
}
|
|
81
|
-
function
|
|
82
|
-
let
|
|
83
|
-
for (let { field:
|
|
84
|
-
let a =
|
|
91
|
+
function A(e, t) {
|
|
92
|
+
let n = [];
|
|
93
|
+
for (let { field: r } of d) {
|
|
94
|
+
let a = t.get(r);
|
|
85
95
|
if (!a) continue;
|
|
86
|
-
let o =
|
|
87
|
-
o.length > 0 &&
|
|
88
|
-
field:
|
|
96
|
+
let o = i(a, e);
|
|
97
|
+
o.length > 0 && n.push({
|
|
98
|
+
field: r,
|
|
89
99
|
ranges: o
|
|
90
100
|
});
|
|
91
101
|
}
|
|
92
|
-
return
|
|
102
|
+
return n;
|
|
103
|
+
}
|
|
104
|
+
function j(e) {
|
|
105
|
+
let t = g.get(e);
|
|
106
|
+
if (!t) return !1;
|
|
107
|
+
let n = !1;
|
|
108
|
+
for (let { field: i, stringify: o } of d) {
|
|
109
|
+
let s = o(e[i]);
|
|
110
|
+
if (s === t.values.get(i)) continue;
|
|
111
|
+
n = !0;
|
|
112
|
+
let c = t.trigrams.get(i);
|
|
113
|
+
c && T(e, c);
|
|
114
|
+
let l = r(s), u = l.length >= 1 ? a(l) : /* @__PURE__ */ new Set();
|
|
115
|
+
t.trigrams.set(i, u), t.values.set(i, s), w(e, u);
|
|
116
|
+
}
|
|
117
|
+
return n;
|
|
118
|
+
}
|
|
119
|
+
function M(e) {
|
|
120
|
+
let t = g.get(e);
|
|
121
|
+
if (!t) return !1;
|
|
122
|
+
for (let n of t.trigrams.values()) T(e, n);
|
|
123
|
+
return g.delete(e), !0;
|
|
93
124
|
}
|
|
94
|
-
for (let e of n)
|
|
95
|
-
|
|
125
|
+
for (let e of n) g.has(e) || E(e);
|
|
126
|
+
let N = {
|
|
96
127
|
add(e) {
|
|
97
|
-
|
|
128
|
+
g.has(e) || (E(e), b());
|
|
98
129
|
},
|
|
99
130
|
get items() {
|
|
100
|
-
return [...
|
|
131
|
+
return [...g.keys()];
|
|
101
132
|
},
|
|
102
133
|
onMutate(e) {
|
|
103
|
-
return
|
|
104
|
-
|
|
134
|
+
return v.add(e), () => {
|
|
135
|
+
v.delete(e);
|
|
105
136
|
};
|
|
106
137
|
},
|
|
107
138
|
reindex(e) {
|
|
108
|
-
|
|
109
|
-
if (!t) return;
|
|
110
|
-
let n = !1;
|
|
111
|
-
for (let { field: a, stringify: o } of c) {
|
|
112
|
-
let s = e[a], c = o(s);
|
|
113
|
-
if (c === t.values.get(a)) continue;
|
|
114
|
-
n = !0;
|
|
115
|
-
let l = t.trigrams.get(a);
|
|
116
|
-
l && x(e, l);
|
|
117
|
-
let u = r(c), d = u.length >= 1 ? i(u) : /* @__PURE__ */ new Set();
|
|
118
|
-
t.trigrams.set(a, d), t.values.set(a, c), b(e, d);
|
|
119
|
-
}
|
|
120
|
-
n && g();
|
|
139
|
+
j(e) && b();
|
|
121
140
|
},
|
|
122
141
|
remove(e) {
|
|
123
|
-
|
|
124
|
-
if (t) {
|
|
125
|
-
for (let n of t.trigrams.values()) x(e, n);
|
|
126
|
-
p.delete(e), g();
|
|
127
|
-
}
|
|
142
|
+
M(e) && b();
|
|
128
143
|
},
|
|
129
144
|
search(e, t) {
|
|
130
|
-
let n = t?.threshold ??
|
|
131
|
-
if (!e.trim()) return [...
|
|
145
|
+
let n = c(t?.threshold ?? p, "threshold", 0, 1), i = s(t?.limit ?? m, "limit", 0), a = s(t?.minQueryLength ?? h, "minQueryLength", 1);
|
|
146
|
+
if (!e.trim()) return [...g.keys()].slice(0, i).map((e) => ({
|
|
132
147
|
item: e,
|
|
133
148
|
matches: [],
|
|
134
149
|
score: 1
|
|
135
150
|
}));
|
|
136
151
|
let o = r(e);
|
|
137
152
|
if (!o) return [];
|
|
138
|
-
let
|
|
139
|
-
for (let e of
|
|
140
|
-
let t =
|
|
153
|
+
let l = o.length < a ? null : C(o), u = l === null ? D(o) : O(l), d = [];
|
|
154
|
+
for (let e of u) {
|
|
155
|
+
let t = g.get(e);
|
|
141
156
|
if (!t) continue;
|
|
142
|
-
let r =
|
|
157
|
+
let r = k(o, l, t);
|
|
143
158
|
if (r >= n) {
|
|
144
|
-
let n =
|
|
145
|
-
|
|
159
|
+
let n = A(o, t.values);
|
|
160
|
+
d.push({
|
|
146
161
|
item: e,
|
|
147
162
|
matches: n,
|
|
148
163
|
score: r
|
|
149
164
|
});
|
|
150
165
|
}
|
|
151
166
|
}
|
|
152
|
-
return
|
|
167
|
+
return d.sort((e, t) => t.score - e.score).slice(0, i);
|
|
168
|
+
},
|
|
169
|
+
setItems(e) {
|
|
170
|
+
let t = new Set(e), n = [...t], r = !1;
|
|
171
|
+
for (let e of [...g.keys()]) t.has(e) || (M(e), r = !0);
|
|
172
|
+
for (let e of n) g.has(e) ? j(e) && (r = !0) : (E(e), r = !0);
|
|
173
|
+
let i = [...g.keys()];
|
|
174
|
+
if (i.length !== n.length || i.some((e, t) => e !== n[t])) {
|
|
175
|
+
let e = new Map(n.map((e) => [e, g.get(e)]));
|
|
176
|
+
g.clear();
|
|
177
|
+
for (let [t, n] of e) g.set(t, n);
|
|
178
|
+
r = !0;
|
|
179
|
+
}
|
|
180
|
+
r && b();
|
|
153
181
|
},
|
|
154
182
|
get size() {
|
|
155
|
-
return
|
|
183
|
+
return g.size;
|
|
156
184
|
}
|
|
157
185
|
};
|
|
186
|
+
return e(N, () => y), N;
|
|
158
187
|
}
|
|
159
188
|
//#endregion
|
|
160
|
-
export {
|
|
189
|
+
export { u as createIndex };
|
|
161
190
|
|
|
162
191
|
//# sourceMappingURL=scout-index.js.map
|
package/dist/scout-index.js.map
CHANGED
|
@@ -1 +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 { generateTrigrams, overlapSimilarity } 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 = overlapSimilarity(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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"}
|
|
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 { registerIndexRevision } from './_index-state';\nimport { ScoutConfigurationError } from './errors';\nimport { findMatchRanges } from './highlight';\nimport { defaultStringify, tokenize } from './tokenize';\nimport { generateTrigrams, overlapSimilarity } 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 * Reconciles the index to `items` by reference identity. Retained items are reindexed,\n * new items are added, missing items are removed, and one mutation notification fires\n * when indexed corpus or field values change. Duplicate references collapse to one item.\n */\n setItems(items: readonly 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 changed `add()` / `remove()` / `reindex()`\n * / `setItems()` operation. No-ops — e.g. removing an unindexed item or reconciling an\n * unchanged corpus — do not fire it. Each changed `setItems()` reconciliation fires once.\n * 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 requireFiniteInteger(value: number, name: string, minimum: number): number {\n if (!Number.isFinite(value) || !Number.isInteger(value) || value < minimum) {\n throw new ScoutConfigurationError(`${name} must be a finite integer greater than or equal to ${minimum}.`);\n }\n\n return value;\n}\n\nfunction requireFiniteNumber(value: number, name: string, minimum: number, maximum = Number.POSITIVE_INFINITY): number {\n if (!Number.isFinite(value) || value < minimum || value > maximum) {\n throw new ScoutConfigurationError(`${name} must be a finite number between ${minimum} and ${maximum}.`);\n }\n\n return value;\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: requireFiniteNumber(def.weight ?? 1, `weight for field \"${def.field}\"`, Number.MIN_VALUE),\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 {ScoutConfigurationError} If options use an invalid field or numeric configuration.\n */\nexport function createIndex<T>(items: T[], options: ScoutIndexOptions<T>): ScoutIndex<T> {\n if (options.fields.length === 0) {\n throw new ScoutConfigurationError('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 = requireFiniteNumber(options.threshold ?? 0.2, 'threshold', 0, 1);\n const defaultLimit = requireFiniteInteger(options.limit ?? 50, 'limit', 0);\n const defaultMinQueryLength = requireFiniteInteger(options.minQueryLength ?? 3, 'minQueryLength', 1);\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 let revision = 0;\n\n function notifyMutation(): void {\n revision++;\n\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 = overlapSimilarity(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 function reindexItem(item: T): boolean {\n const record = itemData.get(item);\n\n if (!record) return false;\n\n let changed = false;\n\n for (const { field, stringify } of fields) {\n const newText = stringify(item[field]);\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 return changed;\n }\n\n function removeItem(item: T): boolean {\n const record = itemData.get(item);\n\n if (!record) return false;\n\n for (const fieldTrigrams of record.trigrams.values()) {\n removeFieldFromIndex(item, fieldTrigrams);\n }\n\n itemData.delete(item);\n\n return true;\n }\n\n for (const item of items) {\n if (!itemData.has(item)) addItem(item);\n }\n\n const index: ScoutIndex<T> = {\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 if (reindexItem(item)) notifyMutation();\n },\n\n remove(item: T): void {\n if (removeItem(item)) notifyMutation();\n },\n\n search(query: string, options?: SearchConstraints): SearchResult<T>[] {\n const threshold = requireFiniteNumber(options?.threshold ?? defaultThreshold, 'threshold', 0, 1);\n const limit = requireFiniteInteger(options?.limit ?? defaultLimit, 'limit', 0);\n const minQueryLength = requireFiniteInteger(\n options?.minQueryLength ?? defaultMinQueryLength,\n 'minQueryLength',\n 1,\n );\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 setItems(items: readonly T[]): void {\n const incoming = new Set(items);\n const next = [...incoming];\n let changed = false;\n\n for (const item of [...itemData.keys()]) {\n if (incoming.has(item)) continue;\n\n removeItem(item);\n changed = true;\n }\n\n for (const item of next) {\n if (!itemData.has(item)) {\n addItem(item);\n changed = true;\n } else if (reindexItem(item)) {\n changed = true;\n }\n }\n\n const current = [...itemData.keys()];\n const orderChanged = current.length !== next.length || current.some((item, index) => item !== next[index]);\n\n if (orderChanged) {\n const records = new Map(next.map((item) => [item, itemData.get(item)!]));\n\n itemData.clear();\n\n for (const [item, record] of records) itemData.set(item, record);\n\n changed = true;\n }\n\n if (changed) notifyMutation();\n },\n\n get size(): number {\n return itemData.size;\n },\n };\n\n registerIndexRevision(index, () => revision);\n\n return index;\n}\n"],"mappings":"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"}
|
package/dist/scout.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});function e(e,t){let n,r=new Set;return(i,a)=>(a!==n&&(n=a,r=new Set(e.search(a,t).map(e=>e.item))),r.has(i))}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=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}},l=class extends c{},u=class extends c{},d=class extends c{},f=Symbol(`ripple.reactive`),p=Symbol(`ripple.signal`),m=Symbol(`ripple.computed`),h=100,g=Symbol(`ripple.unset`),_=class{[f]=!0;dependents=new Set;name;runtime;constructor(e,t){this.runtime=e,this.name=t}subscribe(e){this.peek();let t={dependencies:new Set,onDependencyChanged:()=>this.runtime.enqueueListener(e)};return this.dependents.add(t),()=>this.dependents.delete(t)}notify(){for(let e of[...this.dependents])e.onDependencyChanged()}},v=class extends _{[p]=!0;current;equals;constructor(e,t,n){super(e,n?.name),this.current=t,this.equals=n?.equals??Object.is}get value(){return this.runtime.track(this),this.current}set value(e){if(this.equals(this.current,e))return;let t=this.current;this.current=e,this.runtime.emit({kind:`write`,name:this.name,next:e,previous:t}),this.runtime.propagate(()=>this.notify())}peek(){return this.current}},y=class extends _{[m]=!0;dependencies=new Set;computing=!1;disposed=!1;dirty=!0;current=g;derive;equals;constructor(e,t,n){super(e,n?.name),this.derive=t,this.equals=n?.equals??Object.is}get value(){return this.refresh(),this.runtime.track(this),this.current}peek(){return this.refresh(),this.current}onDependencyChanged(){this.disposed||(this.dirty||=!0,this.dependents.size>0&&this.refresh()&&this.notify())}dispose(){this.disposed||(this.disposed=!0,this.runtime.clearDependencies(this),this.dependents.clear())}refresh(){if(!this.dirty||this.disposed)return!1;if(this.computing)throw new l(`computed cycle detected${this.name===void 0?``:` "${this.name}"`}`);this.computing=!0,this.runtime.emit({kind:`compute`,name:this.name});try{let e=this.runtime.collect(this,this.derive),t=this.current===g||!this.equals(this.current,e);return this.current=e,this.dirty=!1,t}finally{this.computing=!1}}},b=class{disposalController=new AbortController;owned=new Set;name;isDisposed=!1;runtime;constructor(e,t){this.runtime=e,this.name=t}get disposed(){return this.isDisposed}get disposalSignal(){return this.disposalController.signal}run(e){if(this.isDisposed)throw new u(`Cannot run a disposed scope.`);return this.runtime.withScope(this,e)}dispose(){if(!this.isDisposed){this.isDisposed=!0;for(let e of[...this.owned].reverse())e.dispose();this.owned.clear(),this.disposalController.abort(),this.runtime.emit({kind:`dispose`,name:this.name,node:`scope`})}}[Symbol.dispose](){this.dispose()}},x=class{dependencies=new Set;disposalController=new AbortController;cleanup;isDisposed=!1;owner;scheduled=!1;callback;options;runtime;constructor(e,t,n){this.runtime=e,this.callback=t,this.options=n}get disposed(){return this.isDisposed}get disposalSignal(){return this.disposalController.signal}onDependencyChanged(){if(!this.isDisposed){if(this.options?.scheduler===`microtask`){if(this.scheduled)return;this.scheduled=!0,queueMicrotask(()=>{this.scheduled=!1,this.isDisposed||this.runtime.enqueue(this)});return}this.runtime.enqueue(this)}}run(){if(this.isDisposed)return;this.owner?.dispose(),this.owner=void 0,this.runCleanup(),this.runtime.emit({kind:`effect`,name:this.options?.name});let e=new b(this.runtime);try{let t=this.runtime.withEffectScope(e,()=>this.runtime.collectEffect(this,this.callback));this.owner=e,this.cleanup=typeof t==`function`?t:void 0}catch(t){e.dispose(),this.runtime.report(t,{kind:`effect`,name:this.options?.name})}}dispose(){this.isDisposed||(this.isDisposed=!0,this.owner?.dispose(),this.owner=void 0,this.runtime.clearDependencies(this),this.runCleanup(),this.disposalController.abort(),this.runtime.emit({kind:`dispose`,name:this.options?.name,node:`effect`}))}[Symbol.dispose](){this.dispose()}runCleanup(){let e=this.cleanup;if(this.cleanup=void 0,e!==void 0)try{e()}catch(e){this.runtime.report(e,{kind:`cleanup`,name:this.options?.name})}}},S=class{activeEffectScope;activeObserver;activeScope;flushDepth=0;flushing=!1;pending=new Set;listeners=new Set;rootScope;observer;onError;constructor(e){this.observer=e?.observer,this.onError=e?.onError??(e=>{queueMicrotask(()=>{throw e})}),this.rootScope=new b(this,`runtime`),this.activeScope=this.rootScope}signal=(e,t)=>new v(this,e,t);computed=(e,t)=>{let n=new y(this,e,t);return(this.activeEffectScope??this.activeScope).owned.add(n),n};effect=(e,t)=>{let n=new x(this,e,t);return(this.activeEffectScope??this.activeScope).owned.add(n),n.run(),n};createScope=e=>{let t=new b(this,e);return this.activeScope.owned.add(t),t};batch=e=>this.propagate(e);untrack=e=>this.withObserver(void 0,e);dispose(){this.rootScope.dispose()}track(e){let t=this.activeObserver;t?.collecting!==void 0&&t.collecting.add(e)}clearDependencies(e){for(let t of e.dependencies)t.dependents.delete(e);e.dependencies.clear()}collect(e,t){return this.collectWith(e,t,!1)}collectEffect(e,t){return this.collectWith(e,t,!0)}withEffectScope(e,t){let n=this.activeEffectScope;this.activeEffectScope=e;try{return t()}finally{this.activeEffectScope=n}}withObserver(e,t){let n=this.activeObserver;this.activeObserver=e;try{return t()}finally{this.activeObserver=n}}withScope(e,t){let n=this.activeEffectScope,r=this.activeScope;this.activeEffectScope=void 0,this.activeScope=e;try{return t()}finally{this.activeEffectScope=n,this.activeScope=r}}enqueue(e){this.pending.add(e),this.flushDepth===0&&this.flush()}enqueueListener(e){this.listeners.add(e),this.flushDepth===0&&this.flush()}propagate(e){this.flushDepth++;try{return e()}finally{this.flushDepth--,this.flushDepth===0&&this.flush()}}emit(e){try{this.observer?.(e)}catch(t){this.report(t,{kind:`observer`,name:e.name})}}report(e,t){try{this.onError(e,t)}catch(e){queueMicrotask(()=>{throw e})}}collectWith(e,t,n){let r=this.activeObserver,i=new Set;e.collecting=i,this.activeObserver=e;try{let n=t();return this.commitDependencies(e,i),n}catch(t){throw n&&this.commitDependencies(e,i),t}finally{e.collecting=void 0,this.activeObserver=r}}commitDependencies(e,t){for(let n of e.dependencies)t.has(n)||n.dependents.delete(e);for(let n of t)e.dependencies.has(n)||n.dependents.add(e);e.dependencies.clear();for(let n of t)e.dependencies.add(n)}flush(){if(this.flushing)return;this.flushing=!0;let e=0;try{for(;this.pending.size>0||this.listeners.size>0;){if(++e>h)throw new d(`infinite reactive flush (>${h} iterations)`);let t=[...this.pending],n=[...this.listeners];this.pending.clear(),this.listeners.clear();for(let e of t)e.run();for(let e of n)try{e()}catch(e){this.report(e,{kind:`listener`})}}}finally{this.flushing=!1}}},C=e=>(t,n,r)=>{let i=e.signal({status:`pending`},{name:r?.name}),a=e.signal(0),o=new AbortController,s,c=!1,l=()=>{a.value,s?.abort();let e=i.peek(),r=e.status===`success`?e.value:`previous`in e?e.previous:void 0,o;try{o=t()}catch(e){i.value=r===void 0?{error:e,status:`error`}:{error:e,previous:r,status:`error`};return}let l=new AbortController;s=l,i.value=r===void 0?{status:`pending`}:{previous:r,status:`pending`};let u;try{u=Promise.resolve(n(o,{signal:l.signal}))}catch(e){u=Promise.reject(e)}u.then(e=>{!c&&!l.signal.aborted&&(i.value={status:`success`,value:e})},e=>{!c&&!l.signal.aborted&&(i.value=r===void 0?{error:e,status:`error`}:{error:e,previous:r,status:`error`})})},u=e.effect(()=>(l(),()=>s?.abort()),{name:r?.name});return u.disposalSignal.addEventListener(`abort`,()=>{c=!0,o.abort()},{once:!0}),{get disposalSignal(){return o.signal},dispose:()=>u.dispose(),get disposed(){return c},get name(){return i.name},peek:()=>i.peek(),reload:()=>{c||(a.value=a.peek()+1)},subscribe:e=>i.subscribe(e),[Symbol.dispose](){this.dispose()},get value(){return i.value}}},w=e=>(t,n)=>{let r=e.signal(t,{name:n?.name});return{get name(){return r.name},peek:()=>r.peek(),set:e=>{r.value=e},subscribe:e=>r.subscribe(e),update:e=>{r.value=e(r.peek())},get value(){return r.value}}},T=e=>(t,n,r)=>{let i=typeof t==`function`?t:()=>t.value,a=r?.equals??Object.is,o=!0,s,c=!1,l=e.effect(()=>{let e=i();if(o){o=!1,s=e,r?.immediate&&n(e,void 0),c=r?.once===!0&&r.immediate===!0;return}if(a(s,e))return;let t=s;s=e,n(e,t),r?.once&&l.dispose()},{name:r?.name});return c&&l.dispose(),l},E=(e=>{let t=new S(e),n=C(t),r=w(t);return{batch:t.batch,computed:t.computed,createScope:t.createScope,createStore:r,dispose:()=>t.dispose(),effect:t.effect,resource:n,signal:t.signal,untrack:t.untrack,watch:T(t)}})(),D=E.signal,O=E.computed;E.effect;var k=E.batch;E.createScope,E.createStore,E.resource,E.untrack,E.watch;function A(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)?e.filter(e=>typeof e==`string`||typeof e==`number`||typeof e==`boolean`).join(` `):``}function M(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 N(e,t){if(e.size===0||t.size===0)return 0;let n=0;for(let r of e)t.has(r)&&n++;return n<2&&n<e.size?0:n/Math.min(e.size,t.size)}function P(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 F(e,t){if(t.fields.length===0)throw new i(`createIndex: at least one field is required.`);let n=P(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=M(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=A(o),c=s.length>=1?M(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=N(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=A(o),l=c.length>=1?M(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=A(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 I=200;function L(e,t={}){let{debounce:n=I,limit:i,minQueryLength:a,threshold:o}=t,s=D(``,{name:`scout:query`}),c=D(``,{name:`scout:committedQuery`}),l=D(0,{name:`scout:indexVersion`}),u=e.onMutate(()=>{l.value++}),d=O(()=>s.value!==c.value,{name:`scout:isSearching`}),f=O(()=>(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(),k(()=>{s.value=``,c.value=``})}let _=!1,v=new AbortController;function y(){_=!0,v.abort(),m(),h(),u()}return{clear:g,get disposalSignal(){return v.signal},dispose:y,get disposed(){return _},isSearching:d,query:s,results:f,[Symbol.dispose](){y()}}}function R(e,t){let n=F(e,{fields:t.fields,limit:t.limit,minQueryLength:t.minQueryLength,threshold:t.threshold}),r=L(n,{debounce:t.debounce});return Object.assign(Object.create(r),{index:n})}function z(e){let t=V();return t?[...t.segment(e)].filter(e=>e.isWordLike).map(e=>e.segment).join(` `):e}var B;function V(){return B===void 0&&(B=typeof Intl>`u`||typeof Intl.Segmenter!=`function`?null:new Intl.Segmenter(void 0,{granularity:`word`})),B}exports.ScoutDisposedError=r,exports.ScoutError=n,exports.ScoutIndexError=i,exports.createIndex=F,exports.createReactiveSearch=R,exports.createSearch=L,exports.findMatchRanges=a,exports.highlight=o,exports.highlightField=s,exports.segmentWords=z,exports.toFilterPredicate=t,exports.toSearchMatcher=e;
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=new WeakMap;function t(t){return e.get(t)?.()??0}function n(t,n){e.set(t,n)}function r(e,n){let r,i=-1,a=new Set;return(o,s)=>{let c=t(e);return(s!==r||c!==i)&&(r=s,i=c,a=new Set(e.search(s,n).map(e=>e.item))),a.has(o)}}function i(e,t,n){let r=new Set(e.search(t,n).map(e=>e.item));return e=>r.has(e)}var a=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}},o=class extends a{},s=class extends a{};function c(e){return e.toLowerCase().replace(/[^\p{L}\p{N}\s']/gu,` `).trim().replace(/\s+/g,` `)}function l(e){return e==null?``:typeof e==`string`?e:typeof e==`number`||typeof e==`boolean`?String(e):Array.isArray(e)?e.filter(e=>typeof e==`string`||typeof e==`number`||typeof e==`boolean`).join(` `):``}function u(e,t){let n=e.toLowerCase(),r=c(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 d(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 f(e,t,n){return d(n,e.matches.find(e=>e.field===t)?.ranges??[])}var p=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}},m=class extends p{},h=class extends p{},g=class extends p{},_=Symbol(`ripple.reactive`),v=Symbol(`ripple.signal`),y=Symbol(`ripple.computed`),b=100,x=Symbol(`ripple.unset`),S=class{[_]=!0;dependents=new Set;name;runtime;constructor(e,t){this.runtime=e,this.name=t}subscribe(e){this.peek();let t={dependencies:new Set,onDependencyChanged:()=>this.runtime.enqueueListener(e)};return this.dependents.add(t),()=>this.dependents.delete(t)}notify(){for(let e of[...this.dependents])e.onDependencyChanged()}},C=class extends S{[v]=!0;current;equals;constructor(e,t,n){super(e,n?.name),this.current=t,this.equals=n?.equals??Object.is}get value(){return this.runtime.track(this),this.current}set value(e){if(this.equals(this.current,e))return;let t=this.current;this.current=e,this.runtime.emit({kind:`write`,name:this.name,next:e,previous:t}),this.runtime.propagate(()=>this.notify())}peek(){return this.current}},w=class extends S{[y]=!0;dependencies=new Set;computing=!1;disposed=!1;dirty=!0;current=x;derive;equals;constructor(e,t,n){super(e,n?.name),this.derive=t,this.equals=n?.equals??Object.is}get value(){return this.refresh(),this.runtime.track(this),this.current}peek(){return this.refresh(),this.current}onDependencyChanged(){this.disposed||(this.dirty||=!0,this.dependents.size>0&&this.refresh()&&this.notify())}dispose(){this.disposed||(this.disposed=!0,this.runtime.clearDependencies(this),this.dependents.clear())}refresh(){if(!this.dirty||this.disposed)return!1;if(this.computing)throw new m(`computed cycle detected${this.name===void 0?``:` "${this.name}"`}`);this.computing=!0,this.runtime.emit({kind:`compute`,name:this.name});try{let e=this.runtime.collect(this,this.derive),t=this.current===x||!this.equals(this.current,e);return this.current=e,this.dirty=!1,t}finally{this.computing=!1}}},T=class{disposalController=new AbortController;owned=new Set;name;isDisposed=!1;runtime;constructor(e,t){this.runtime=e,this.name=t}get disposed(){return this.isDisposed}get disposalSignal(){return this.disposalController.signal}run(e){if(this.isDisposed)throw new h(`Cannot run a disposed scope.`);return this.runtime.withScope(this,e)}dispose(){if(!this.isDisposed){this.isDisposed=!0;for(let e of[...this.owned].reverse())e.dispose();this.owned.clear(),this.disposalController.abort(),this.runtime.emit({kind:`dispose`,name:this.name,node:`scope`})}}[Symbol.dispose](){this.dispose()}},E=class{dependencies=new Set;disposalController=new AbortController;cleanup;isDisposed=!1;owner;scheduled=!1;callback;options;runtime;constructor(e,t,n){this.runtime=e,this.callback=t,this.options=n}get disposed(){return this.isDisposed}get disposalSignal(){return this.disposalController.signal}onDependencyChanged(){if(!this.isDisposed){if(this.options?.scheduler===`microtask`){if(this.scheduled)return;this.scheduled=!0,queueMicrotask(()=>{this.scheduled=!1,this.isDisposed||this.runtime.enqueue(this)});return}this.runtime.enqueue(this)}}run(){if(this.isDisposed)return;this.owner?.dispose(),this.owner=void 0,this.runCleanup(),this.runtime.emit({kind:`effect`,name:this.options?.name});let e=new T(this.runtime);try{let t=this.runtime.withEffectScope(e,()=>this.runtime.collectEffect(this,this.callback));this.owner=e,this.cleanup=typeof t==`function`?t:void 0}catch(t){e.dispose(),this.runtime.report(t,{kind:`effect`,name:this.options?.name})}}dispose(){this.isDisposed||(this.isDisposed=!0,this.owner?.dispose(),this.owner=void 0,this.runtime.clearDependencies(this),this.runCleanup(),this.disposalController.abort(),this.runtime.emit({kind:`dispose`,name:this.options?.name,node:`effect`}))}[Symbol.dispose](){this.dispose()}runCleanup(){let e=this.cleanup;if(this.cleanup=void 0,e!==void 0)try{e()}catch(e){this.runtime.report(e,{kind:`cleanup`,name:this.options?.name})}}},D=class{activeEffectScope;activeObserver;activeScope;flushDepth=0;flushing=!1;pending=new Set;listeners=new Set;rootScope;observer;onError;constructor(e){this.observer=e?.observer,this.onError=e?.onError??(e=>{queueMicrotask(()=>{throw e})}),this.rootScope=new T(this,`runtime`),this.activeScope=this.rootScope}signal=(e,t)=>new C(this,e,t);computed=(e,t)=>{let n=new w(this,e,t);return(this.activeEffectScope??this.activeScope).owned.add(n),n};effect=(e,t)=>{let n=new E(this,e,t);return(this.activeEffectScope??this.activeScope).owned.add(n),n.run(),n};createScope=e=>{let t=new T(this,e);return this.activeScope.owned.add(t),t};batch=e=>this.propagate(e);untrack=e=>this.withObserver(void 0,e);dispose(){this.rootScope.dispose()}track(e){let t=this.activeObserver;t?.collecting!==void 0&&t.collecting.add(e)}clearDependencies(e){for(let t of e.dependencies)t.dependents.delete(e);e.dependencies.clear()}collect(e,t){return this.collectWith(e,t,!1)}collectEffect(e,t){return this.collectWith(e,t,!0)}withEffectScope(e,t){let n=this.activeEffectScope;this.activeEffectScope=e;try{return t()}finally{this.activeEffectScope=n}}withObserver(e,t){let n=this.activeObserver;this.activeObserver=e;try{return t()}finally{this.activeObserver=n}}withScope(e,t){let n=this.activeEffectScope,r=this.activeScope;this.activeEffectScope=void 0,this.activeScope=e;try{return t()}finally{this.activeEffectScope=n,this.activeScope=r}}enqueue(e){this.pending.add(e),this.flushDepth===0&&this.flush()}enqueueListener(e){this.listeners.add(e),this.flushDepth===0&&this.flush()}propagate(e){this.flushDepth++;try{return e()}finally{this.flushDepth--,this.flushDepth===0&&this.flush()}}emit(e){try{this.observer?.(e)}catch(t){this.report(t,{kind:`observer`,name:e.name})}}report(e,t){try{this.onError(e,t)}catch(e){queueMicrotask(()=>{throw e})}}collectWith(e,t,n){let r=this.activeObserver,i=new Set;e.collecting=i,this.activeObserver=e;try{let n=t();return this.commitDependencies(e,i),n}catch(t){throw n&&this.commitDependencies(e,i),t}finally{e.collecting=void 0,this.activeObserver=r}}commitDependencies(e,t){for(let n of e.dependencies)t.has(n)||n.dependents.delete(e);for(let n of t)e.dependencies.has(n)||n.dependents.add(e);e.dependencies.clear();for(let n of t)e.dependencies.add(n)}flush(){if(this.flushing)return;this.flushing=!0;let e=0;try{for(;this.pending.size>0||this.listeners.size>0;){if(++e>b)throw new g(`infinite reactive flush (>${b} iterations)`);let t=[...this.pending],n=[...this.listeners];this.pending.clear(),this.listeners.clear();for(let e of t)e.run();for(let e of n)try{e()}catch(e){this.report(e,{kind:`listener`})}}}finally{this.flushing=!1}}},O=e=>(t,n,r)=>{let i=e.signal({status:`pending`},{name:r?.name}),a=e.signal(0),o=new AbortController,s,c=!1,l=()=>{a.value,s?.abort();let e=i.peek(),r=e.status===`success`?e.value:`previous`in e?e.previous:void 0,o;try{o=t()}catch(e){i.value=r===void 0?{error:e,status:`error`}:{error:e,previous:r,status:`error`};return}let l=new AbortController;s=l,i.value=r===void 0?{status:`pending`}:{previous:r,status:`pending`};let u;try{u=Promise.resolve(n(o,{signal:l.signal}))}catch(e){u=Promise.reject(e)}u.then(e=>{!c&&!l.signal.aborted&&(i.value={status:`success`,value:e})},e=>{!c&&!l.signal.aborted&&(i.value=r===void 0?{error:e,status:`error`}:{error:e,previous:r,status:`error`})})},u=e.effect(()=>(l(),()=>s?.abort()),{name:r?.name});return u.disposalSignal.addEventListener(`abort`,()=>{c=!0,o.abort()},{once:!0}),{get disposalSignal(){return o.signal},dispose:()=>u.dispose(),get disposed(){return c},get name(){return i.name},peek:()=>i.peek(),reload:()=>{c||(a.value=a.peek()+1)},subscribe:e=>i.subscribe(e),[Symbol.dispose](){this.dispose()},get value(){return i.value}}},k=e=>(t,n)=>{let r=e.signal(t,{name:n?.name});return{get name(){return r.name},peek:()=>r.peek(),set:e=>{r.value=e},subscribe:e=>r.subscribe(e),update:e=>{r.value=e(r.peek())},get value(){return r.value}}},A=e=>(t,n,r)=>{let i=typeof t==`function`?t:()=>t.value,a=r?.equals??Object.is,o=!0,s,c=!1,l=e.effect(()=>{let e=i();if(o){o=!1,s=e,r?.immediate&&n(e,void 0),c=r?.once===!0&&r.immediate===!0;return}if(a(s,e))return;let t=s;s=e,n(e,t),r?.once&&l.dispose()},{name:r?.name});return c&&l.dispose(),l},j=(e=>{let t=new D(e),n=O(t),r=k(t);return{batch:t.batch,computed:t.computed,createScope:t.createScope,createStore:r,dispose:()=>t.dispose(),effect:t.effect,resource:n,signal:t.signal,untrack:t.untrack,watch:A(t)}})(),M=j.signal,N=j.computed;j.effect;var P=j.batch;j.createScope,j.createStore,j.resource,j.untrack,j.watch;function F(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 I(e,t){if(e.size===0||t.size===0)return 0;let n=0;for(let r of e)t.has(r)&&n++;return n<2&&n<e.size?0:n/Math.min(e.size,t.size)}function L(e,t,n){if(!Number.isFinite(e)||!Number.isInteger(e)||e<n)throw new s(`${t} must be a finite integer greater than or equal to ${n}.`);return e}function R(e,t,n,r=1/0){if(!Number.isFinite(e)||e<n||e>r)throw new s(`${t} must be a finite number between ${n} and ${r}.`);return e}function z(e){return e.map(e=>typeof e==`string`?{field:e,stringify:l,weight:1}:{field:e.field,stringify:e.stringify??l,weight:R(e.weight??1,`weight for field "${e.field}"`,Number.MIN_VALUE)})}function B(e,t){if(t.fields.length===0)throw new s(`createIndex: at least one field is required.`);let r=z(t.fields),i=r.reduce((e,t)=>Math.max(e,t.weight),1),a=R(t.threshold??.2,`threshold`,0,1),o=L(t.limit??50,`limit`,0),l=L(t.minQueryLength??3,`minQueryLength`,1),d=new Map,f=new Map,p=new Set,m=0;function h(){m++;for(let e of p)e()}let g=null,_=null;function v(e){return e===g&&_!==null?_:(g=e,_=F(e),_)}function y(e,t){for(let n of t){let t=f.get(n);t||(t=new Set,f.set(n,t)),t.add(e)}}function b(e,t){for(let n of t){let t=f.get(n);t&&(t.delete(e),t.size===0&&f.delete(n))}}function x(e){let t=new Map,n=new Map;for(let{field:i,stringify:a}of r){let r=e[i],o=a(r),s=c(o),l=s.length>=1?F(s):new Set;t.set(i,l),n.set(i,o),y(e,l)}d.set(e,{trigrams:t,values:n})}function S(e){let t=new Set;for(let[n,r]of d)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=f.get(n);if(e)for(let n of e)t.add(n)}return t}function w(e,t,n){let a=0;for(let{field:o,weight:s}of r){let r;if(t===null)r=+!!(n.values.get(o)??``).toLowerCase().includes(e);else{let e=n.trigrams.get(o);if(!e||e.size===0)continue;r=I(t,e)}let c=s/i*r;c>a&&(a=c)}return a}function T(e,t){let n=[];for(let{field:i}of r){let r=t.get(i);if(!r)continue;let a=u(r,e);a.length>0&&n.push({field:i,ranges:a})}return n}function E(e){let t=d.get(e);if(!t)return!1;let n=!1;for(let{field:i,stringify:a}of r){let r=a(e[i]);if(r===t.values.get(i))continue;n=!0;let o=t.trigrams.get(i);o&&b(e,o);let s=c(r),l=s.length>=1?F(s):new Set;t.trigrams.set(i,l),t.values.set(i,r),y(e,l)}return n}function D(e){let t=d.get(e);if(!t)return!1;for(let n of t.trigrams.values())b(e,n);return d.delete(e),!0}for(let t of e)d.has(t)||x(t);let O={add(e){d.has(e)||(x(e),h())},get items(){return[...d.keys()]},onMutate(e){return p.add(e),()=>{p.delete(e)}},reindex(e){E(e)&&h()},remove(e){D(e)&&h()},search(e,t){let n=R(t?.threshold??a,`threshold`,0,1),r=L(t?.limit??o,`limit`,0),i=L(t?.minQueryLength??l,`minQueryLength`,1);if(!e.trim())return[...d.keys()].slice(0,r).map(e=>({item:e,matches:[],score:1}));let s=c(e);if(!s)return[];let u=s.length<i?null:v(s),f=u===null?S(s):C(u),p=[];for(let e of f){let t=d.get(e);if(!t)continue;let r=w(s,u,t);if(r>=n){let n=T(s,t.values);p.push({item:e,matches:n,score:r})}}return p.sort((e,t)=>t.score-e.score).slice(0,r)},setItems(e){let t=new Set(e),n=[...t],r=!1;for(let e of[...d.keys()])t.has(e)||(D(e),r=!0);for(let e of n)d.has(e)?E(e)&&(r=!0):(x(e),r=!0);let i=[...d.keys()];if(i.length!==n.length||i.some((e,t)=>e!==n[t])){let e=new Map(n.map(e=>[e,d.get(e)]));d.clear();for(let[t,n]of e)d.set(t,n);r=!0}r&&h()},get size(){return d.size}};return n(O,()=>m),O}var V=200;function H(e,t={}){let{debounce:n=V,limit:r,minQueryLength:i,threshold:a}=t;if(!Number.isFinite(n)||!Number.isInteger(n)||n<0)throw new s(`debounce must be a finite non-negative integer.`);let c=M(``,{name:`scout:query`}),l=M(``,{name:`scout:committedQuery`}),u=M(0,{name:`scout:indexVersion`}),d=e.onMutate(()=>{u.value++}),f=N(()=>c.value!==l.value,{name:`scout:isSearching`}),p=N(()=>(u.value,e.search(l.value,{limit:r,minQueryLength:i,threshold:a})),{name:`scout:results`}),m=null;function h(){m!==null&&(clearTimeout(m),m=null)}let g=c.subscribe(()=>{let e=c.peek();if(h(),e!==l.peek()){if(n===0){l.value=e;return}m=setTimeout(()=>{l.value=e,m=null},n)}});function _(){if(v)throw new o(`SearchState.clear() called after dispose()`);h(),P(()=>{c.value=``,l.value=``})}let v=!1,y=new AbortController;function b(){v=!0,y.abort(),h(),g(),d()}return{clear:_,get disposalSignal(){return y.signal},dispose:b,get disposed(){return v},isSearching:f,query:c,results:p,[Symbol.dispose](){b()}}}function U(e,t){let n=B(e,{fields:t.fields,limit:t.limit,minQueryLength:t.minQueryLength,threshold:t.threshold}),r=H(n,{debounce:t.debounce});return Object.assign(Object.create(r),{index:n})}function W(e){let t=K();return t?[...t.segment(e)].filter(e=>e.isWordLike).map(e=>e.segment).join(` `):e}var G;function K(){return G===void 0&&(G=typeof Intl>`u`||typeof Intl.Segmenter!=`function`?null:new Intl.Segmenter(void 0,{granularity:`word`})),G}exports.ScoutConfigurationError=s,exports.ScoutDisposedError=o,exports.ScoutError=a,exports.createIndex=B,exports.createReactiveSearch=U,exports.createSearch=H,exports.findMatchRanges=u,exports.highlight=d,exports.highlightField=f,exports.segmentWords=W,exports.toFilterPredicate=i,exports.toSearchMatcher=r;
|
|
2
2
|
//# sourceMappingURL=scout.cjs.map
|