@vielzeug/scout 2.2.2 → 2.3.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 CHANGED
@@ -11,7 +11,7 @@ Fast fuzzy-search. Builds a trigram inverted index at construction — O(candida
11
11
  - **Framework adapters** — `toSearchMatcher()` for sourcerer, `toFilterPredicate()` for filter pipelines
12
12
  - **Corpus reconciliation** — `setItems()` reconciles reference-based additions, removals, reindexes, and order in one notification
13
13
  - **Unsegmented-script helper** — `segmentWords()` pre-splits CJK/Thai text into words via `Intl.Segmenter`
14
- - **Devtools** — `@vielzeug/scout/devtools`'s `debugSearch()` logs query/results transitions
14
+ - **Event subscription** — `search.tap()` observes `query`/`isSearching`/`results`/`dispose` transitions
15
15
 
16
16
  ## Install
17
17
 
package/dist/index.d.ts CHANGED
@@ -6,5 +6,5 @@ export { createReactiveSearch, createSearch } from './reactive';
6
6
  export type { ScoutIndex } from './scout-index';
7
7
  export { createIndex } from './scout-index';
8
8
  export { segmentWords } from './segment';
9
- export type { CreateSearchOptions, FieldDef, FieldMatch, HighlightPart, ScoutIndexOptions, SearchConstraints, SearchResult, SearchState, } from './types';
9
+ export type { CreateSearchOptions, FieldDef, FieldMatch, HighlightPart, ScoutEvent, ScoutIndexOptions, SearchConstraints, SearchResult, SearchState, } from './types';
10
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACnF,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AACzE,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAChE,YAAY,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,YAAY,EACV,mBAAmB,EACnB,QAAQ,EACR,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,iBAAiB,EACjB,YAAY,EACZ,WAAW,GACZ,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACnF,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AACzE,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAChE,YAAY,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,YAAY,EACV,mBAAmB,EACnB,QAAQ,EACR,UAAU,EACV,aAAa,EACb,UAAU,EACV,iBAAiB,EACjB,iBAAiB,EACjB,YAAY,EACZ,WAAW,GACZ,MAAM,SAAS,CAAC"}
package/dist/reactive.cjs CHANGED
@@ -1,2 +1,2 @@
1
- const e=require("./errors.cjs"),t=require("./scout-index.cjs");let n=require("@vielzeug/ripple");var r=200;function i(t,i={}){let{debounce:a=r,limit:o,minQueryLength:s,threshold:c}=i;if(!Number.isFinite(a)||!Number.isInteger(a)||a<0)throw new e.ScoutConfigurationError(`debounce must be a finite non-negative integer.`);let l=(0,n.signal)(``,{name:`scout:query`}),u=(0,n.signal)(``,{name:`scout:committedQuery`}),d=(0,n.signal)(0,{name:`scout:indexVersion`}),f=t.onMutate(()=>{d.value++}),p=(0,n.computed)(()=>l.value!==u.value,{name:`scout:isSearching`}),m=(0,n.computed)(()=>(d.value,t.search(u.value,{limit:o,minQueryLength:s,threshold:c})),{name:`scout:results`}),h=null;function g(){h!==null&&(clearTimeout(h),h=null)}let _=l.subscribe(()=>{let e=l.peek();if(g(),e!==u.peek()){if(a===0){u.value=e;return}h=setTimeout(()=>{u.value=e,h=null},a)}});function v(){if(y)throw new e.ScoutDisposedError(`SearchState.clear() called after dispose()`);g(),(0,n.batch)(()=>{l.value=``,u.value=``})}let y=!1,b=new AbortController;function x(){y=!0,b.abort(),g(),_(),f()}return{clear:v,get disposalSignal(){return b.signal},dispose:x,get disposed(){return y},isSearching:p,query:l,results:m,[Symbol.dispose](){x()}}}function a(e,n){let r=t.createIndex(e,{fields:n.fields,limit:n.limit,minQueryLength:n.minQueryLength,threshold:n.threshold}),a=i(r,{debounce:n.debounce});return Object.assign(Object.create(a),{index:r})}exports.createReactiveSearch=a,exports.createSearch=i;
1
+ const e=require("./errors.cjs"),t=require("./scout-index.cjs");let n=require("@vielzeug/ripple");var r=200;function i(t,i={}){let{debounce:a=r,limit:o,minQueryLength:s,threshold:c}=i;if(!Number.isFinite(a)||!Number.isInteger(a)||a<0)throw new e.ScoutConfigurationError(`debounce must be a finite non-negative integer.`);let l=(0,n.signal)(``,{name:`scout:query`}),u=(0,n.signal)(``,{name:`scout:committedQuery`}),d=(0,n.signal)(0,{name:`scout:indexVersion`}),f=t.onMutate(()=>{d.value++}),p=(0,n.computed)(()=>l.value!==u.value,{name:`scout:isSearching`}),m=(0,n.computed)(()=>(d.value,t.search(u.value,{limit:o,minQueryLength:s,threshold:c})),{name:`scout:results`}),h=null;function g(){h!==null&&(clearTimeout(h),h=null)}let _=l.subscribe(()=>{let e=l.peek();if(g(),e!==u.peek()){if(a===0){u.value=e;return}h=setTimeout(()=>{u.value=e,h=null},a)}});function v(){if(y)throw new e.ScoutDisposedError(`SearchState.clear() called after dispose()`);g(),(0,n.batch)(()=>{l.value=``,u.value=``})}let y=!1,b=new AbortController,x=new Set;function S(e){if(x.size!==0)for(let t of x)try{t(e)}catch{}}let C=l.subscribe(()=>S({query:l.peek(),type:`query-change`})),w=p.subscribe(()=>S({isSearching:p.peek(),type:`searching-change`})),T=m.subscribe(()=>S({results:m.peek(),type:`results-change`}));function E(){y||(y=!0,S({type:`dispose`}),x.clear(),b.abort(),g(),_(),f(),C(),w(),T())}function D(e,t){if(y)return()=>{};if(x.add(e),t?.signal){if(t.signal.aborted)return x.delete(e),()=>{};let n=()=>x.delete(e);return t.signal.addEventListener(`abort`,n,{once:!0}),()=>{x.delete(e),t.signal?.removeEventListener(`abort`,n)}}return()=>x.delete(e)}return{clear:v,get disposalSignal(){return b.signal},dispose:E,get disposed(){return y},isSearching:p,query:l,results:m,tap:D,[Symbol.dispose](){E()}}}function a(e,n){let r=t.createIndex(e,{fields:n.fields,limit:n.limit,minQueryLength:n.minQueryLength,threshold:n.threshold}),a=i(r,{debounce:n.debounce});return Object.assign(Object.create(a),{index:r})}exports.createReactiveSearch=a,exports.createSearch=i;
2
2
  //# sourceMappingURL=reactive.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"reactive.cjs","names":[],"sources":["../src/reactive.ts"],"sourcesContent":["import { batch, computed, signal } from '@vielzeug/ripple';\nimport { ScoutConfigurationError, ScoutDisposedError } from './errors';\nimport { createIndex, type ScoutIndex } from './scout-index';\nimport type { CreateSearchOptions, ScoutIndexOptions, SearchResult, SearchState } from './types';\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":"iGAaA,IAAM,EAAmB,IAsCzB,SAAgB,EAAgB,EAAsB,EAA+B,CAAC,EAAmB,CACvG,GAAM,CAAE,SAAU,EAAa,EAAkB,QAAO,iBAAgB,aAAc,EAEtF,GAAI,CAAC,OAAO,SAAS,CAAU,GAAK,CAAC,OAAO,UAAU,CAAU,GAAK,EAAa,EAChF,MAAM,IAAI,EAAA,wBAAwB,iDAAiD,EAGrF,IAAM,GAAA,EAAQ,EAAA,OAAA,CAAe,GAAI,CAAE,KAAM,aAAc,CAAC,EAClD,GAAA,EAAiB,EAAA,OAAA,CAAe,GAAI,CAAE,KAAM,sBAAuB,CAAC,EACpE,GAAA,EAAe,EAAA,OAAA,CAAO,EAAG,CAAE,KAAM,oBAAqB,CAAC,EAEvD,EAAuB,EAAM,aAAe,CAChD,EAAa,OACf,CAAC,EAEK,GAAA,EAAc,EAAA,SAAA,KAAe,EAAM,QAAU,EAAe,MAAO,CAAE,KAAM,mBAAoB,CAAC,EAEhG,GAAA,EAAU,EAAA,SAAA,MAGZ,EAAkB,MAEX,EAAM,OAAO,EAAe,MAAO,CAAE,QAAO,iBAAgB,WAAU,CAAC,GAEhF,CAAE,KAAM,eAAgB,CAC1B,EAEI,EAA8C,KAElD,SAAS,GAAoB,CACvB,IAAU,OACZ,aAAa,CAAK,EAClB,EAAQ,KAEZ,CAEA,IAAM,EAAe,EAAM,cAAgB,CACzC,IAAM,EAAI,EAAM,KAAK,EAErB,KAAY,EAER,IAAM,EAAe,KAAK,EAE9B,IAAI,IAAe,EAAG,CACpB,EAAe,MAAQ,EAEvB,MACF,CAEA,EAAQ,eAAiB,CACvB,EAAe,MAAQ,EACvB,EAAQ,IACV,EAAG,CAAU,CALb,CAMF,CAAC,EAED,SAAS,GAAc,CACrB,GAAI,EAAY,MAAM,IAAI,EAAA,mBAAmB,4CAA4C,EAEzF,EAAY,GAEZ,EAAA,EAAA,MAAA,KAAY,CACV,EAAM,MAAQ,GACd,EAAe,MAAQ,EACzB,CAAC,CACH,CAEA,IAAI,EAAa,GACX,EAAK,IAAI,gBAEf,SAAS,GAAgB,CACvB,EAAa,GACb,EAAG,MAAM,EACT,EAAY,EACZ,EAAa,EACb,EAAqB,CACvB,CAEA,MAAO,CACL,QACA,IAAI,gBAA8B,CAChC,OAAO,EAAG,MACZ,EACA,UACA,IAAI,UAAoB,CACtB,OAAO,CACT,EACA,cACA,QACA,UACA,CAAC,OAAO,UAAiB,CACvB,EAAQ,CACV,CACF,CACF,CA4BA,SAAgB,EACd,EACA,EACmB,CACnB,IAAM,EAAQ,EAAA,YAAY,EAAO,CAC/B,OAAQ,EAAQ,OAChB,MAAO,EAAQ,MACf,eAAgB,EAAQ,eACxB,UAAW,EAAQ,SACrB,CAAC,EACK,EAAQ,EAAa,EAAO,CAAE,SAAU,EAAQ,QAAS,CAAC,EAEhE,OAAO,OAAO,OAAO,OAAO,OAAO,CAAK,EAAG,CAAE,OAAM,CAAC,CACtD"}
1
+ {"version":3,"file":"reactive.cjs","names":[],"sources":["../src/reactive.ts"],"sourcesContent":["import { batch, computed, signal } from '@vielzeug/ripple';\nimport { ScoutConfigurationError, ScoutDisposedError } from './errors';\nimport { createIndex, type ScoutIndex } from './scout-index';\nimport type { CreateSearchOptions, ScoutEvent, ScoutIndexOptions, SearchResult, SearchState } from './types';\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 const tappers = new Set<(event: ScoutEvent<T>) => void>();\n\n function emitTap(event: ScoutEvent<T>): void {\n if (tappers.size === 0) return;\n for (const tapper of tappers) {\n try {\n tapper(event);\n } catch {\n // Observability must not affect search behavior.\n }\n }\n }\n\n const tapQuerySub = query.subscribe(() => emitTap({ query: query.peek(), type: 'query-change' }));\n const tapSearchingSub = isSearching.subscribe(() =>\n emitTap({ isSearching: isSearching.peek(), type: 'searching-change' }),\n );\n const tapResultsSub = results.subscribe(() => emitTap({ results: results.peek(), type: 'results-change' }));\n\n function dispose(): void {\n if (isDisposed) return;\n isDisposed = true;\n emitTap({ type: 'dispose' });\n tappers.clear();\n ac.abort();\n cancelTimer();\n subscription();\n unsubscribeMutations();\n tapQuerySub();\n tapSearchingSub();\n tapResultsSub();\n }\n\n function tap(handler: (event: ScoutEvent<T>) => void, opts?: { signal?: AbortSignal }): () => void {\n if (isDisposed) return () => {};\n tappers.add(handler);\n\n if (opts?.signal) {\n if (opts.signal.aborted) {\n tappers.delete(handler);\n return () => {};\n }\n const onAbort = () => tappers.delete(handler);\n opts.signal.addEventListener('abort', onAbort, { once: true });\n return () => {\n tappers.delete(handler);\n opts.signal?.removeEventListener('abort', onAbort);\n };\n }\n\n return () => tappers.delete(handler);\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 tap,\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":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"reactive.d.ts","sourceRoot":"","sources":["../src/reactive.ts"],"names":[],"mappings":"AAEA,OAAO,EAAe,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,KAAK,EAAE,mBAAmB,EAAE,iBAAiB,EAAgB,WAAW,EAAE,MAAM,SAAS,CAAC;AAEjG;;;GAGG;AACH,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,GAAG;IAC/C,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;CAC/B,CAAC;AAIF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE,OAAO,GAAE,mBAAwB,GAAG,WAAW,CAAC,CAAC,CAAC,CA6FvG;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EACpC,KAAK,EAAE,CAAC,EAAE,EACV,OAAO,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,mBAAmB,EAAE,UAAU,CAAC,GACpE,cAAc,CAAC,CAAC,CAAC,CAUnB"}
1
+ {"version":3,"file":"reactive.d.ts","sourceRoot":"","sources":["../src/reactive.ts"],"names":[],"mappings":"AAEA,OAAO,EAAe,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,KAAK,EAAE,mBAAmB,EAAc,iBAAiB,EAAgB,WAAW,EAAE,MAAM,SAAS,CAAC;AAE7G;;;GAGG;AACH,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,GAAG;IAC/C,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;CAC/B,CAAC;AAIF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE,OAAO,GAAE,mBAAwB,GAAG,WAAW,CAAC,CAAC,CAAC,CA0IvG;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EACpC,KAAK,EAAE,CAAC,EAAE,EACV,OAAO,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,mBAAmB,EAAE,UAAU,CAAC,GACpE,cAAc,CAAC,CAAC,CAAC,CAUnB"}
package/dist/reactive.js CHANGED
@@ -34,24 +34,51 @@ function s(n, s = {}) {
34
34
  f.value = "", p.value = "";
35
35
  });
36
36
  }
37
- let S = !1, C = new AbortController();
38
- function w() {
39
- S = !0, C.abort(), y(), b(), h();
37
+ let S = !1, C = new AbortController(), w = /* @__PURE__ */ new Set();
38
+ function T(e) {
39
+ if (w.size !== 0) for (let t of w) try {
40
+ t(e);
41
+ } catch {}
42
+ }
43
+ let E = f.subscribe(() => T({
44
+ query: f.peek(),
45
+ type: "query-change"
46
+ })), D = g.subscribe(() => T({
47
+ isSearching: g.peek(),
48
+ type: "searching-change"
49
+ })), O = _.subscribe(() => T({
50
+ results: _.peek(),
51
+ type: "results-change"
52
+ }));
53
+ function k() {
54
+ S || (S = !0, T({ type: "dispose" }), w.clear(), C.abort(), y(), b(), h(), E(), D(), O());
55
+ }
56
+ function A(e, t) {
57
+ if (S) return () => {};
58
+ if (w.add(e), t?.signal) {
59
+ if (t.signal.aborted) return w.delete(e), () => {};
60
+ let n = () => w.delete(e);
61
+ return t.signal.addEventListener("abort", n, { once: !0 }), () => {
62
+ w.delete(e), t.signal?.removeEventListener("abort", n);
63
+ };
64
+ }
65
+ return () => w.delete(e);
40
66
  }
41
67
  return {
42
68
  clear: x,
43
69
  get disposalSignal() {
44
70
  return C.signal;
45
71
  },
46
- dispose: w,
72
+ dispose: k,
47
73
  get disposed() {
48
74
  return S;
49
75
  },
50
76
  isSearching: g,
51
77
  query: f,
52
78
  results: _,
79
+ tap: A,
53
80
  [Symbol.dispose]() {
54
- w();
81
+ k();
55
82
  }
56
83
  };
57
84
  }
@@ -1 +1 @@
1
- {"version":3,"file":"reactive.js","names":[],"sources":["../src/reactive.ts"],"sourcesContent":["import { batch, computed, signal } from '@vielzeug/ripple';\nimport { ScoutConfigurationError, ScoutDisposedError } from './errors';\nimport { createIndex, type ScoutIndex } from './scout-index';\nimport type { CreateSearchOptions, ScoutIndexOptions, SearchResult, SearchState } from './types';\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":";;;;AAaA,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"}
1
+ {"version":3,"file":"reactive.js","names":[],"sources":["../src/reactive.ts"],"sourcesContent":["import { batch, computed, signal } from '@vielzeug/ripple';\nimport { ScoutConfigurationError, ScoutDisposedError } from './errors';\nimport { createIndex, type ScoutIndex } from './scout-index';\nimport type { CreateSearchOptions, ScoutEvent, ScoutIndexOptions, SearchResult, SearchState } from './types';\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 const tappers = new Set<(event: ScoutEvent<T>) => void>();\n\n function emitTap(event: ScoutEvent<T>): void {\n if (tappers.size === 0) return;\n for (const tapper of tappers) {\n try {\n tapper(event);\n } catch {\n // Observability must not affect search behavior.\n }\n }\n }\n\n const tapQuerySub = query.subscribe(() => emitTap({ query: query.peek(), type: 'query-change' }));\n const tapSearchingSub = isSearching.subscribe(() =>\n emitTap({ isSearching: isSearching.peek(), type: 'searching-change' }),\n );\n const tapResultsSub = results.subscribe(() => emitTap({ results: results.peek(), type: 'results-change' }));\n\n function dispose(): void {\n if (isDisposed) return;\n isDisposed = true;\n emitTap({ type: 'dispose' });\n tappers.clear();\n ac.abort();\n cancelTimer();\n subscription();\n unsubscribeMutations();\n tapQuerySub();\n tapSearchingSub();\n tapResultsSub();\n }\n\n function tap(handler: (event: ScoutEvent<T>) => void, opts?: { signal?: AbortSignal }): () => void {\n if (isDisposed) return () => {};\n tappers.add(handler);\n\n if (opts?.signal) {\n if (opts.signal.aborted) {\n tappers.delete(handler);\n return () => {};\n }\n const onAbort = () => tappers.delete(handler);\n opts.signal.addEventListener('abort', onAbort, { once: true });\n return () => {\n tappers.delete(handler);\n opts.signal?.removeEventListener('abort', onAbort);\n };\n }\n\n return () => tappers.delete(handler);\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 tap,\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":"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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=-1,i=new Set;return(a,o)=>((o!==n||e.revision!==r)&&(n=o,r=e.revision,i=new Set(e.search(o,t).map(e=>e.item))),i.has(a))}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 extends Error{constructor(e,t){super(e,t),this.name=new.target.name,Object.setPrototypeOf(this,new.target.prototype)}},r=class extends n{},i=class extends n{};function a(e){return e.toLowerCase().replace(/[^\p{L}\p{N}\s']/gu,` `).trim().replace(/\s+/g,` `)}function o(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 s(e,t){let n=e.toLowerCase(),r=a(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 o=[];for(let e of i){let t=o[o.length-1];t&&e[0]<=t[1]?t[1]=Math.max(t[1],e[1]):o.push([e[0],e[1]])}return o}function c(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 l(e,t,n){return c(n,e.matches.find(e=>e.field===t)?.ranges??[])}var u=class extends Error{constructor(e,t){super(e,t),this.name=new.target.name,Object.setPrototypeOf(this,new.target.prototype)}},d=class extends u{},f=class extends u{},p=class extends u{},m=class extends u{},h=Symbol.for(`@vielzeug/ripple/reactive`),g=100,_=Symbol(`ripple.unset`),v=class{[h]=!0;dependents=new Set;name;runtime;constructor(e,t){this.runtime=e,this.name=t}subscribe(e){this.runtime.assertActive(),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()}},y=class extends v{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.runtime.assertActive(),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}update(e){this.value=e(this.current)}},b=class extends v{dependencies=new Set;computing=!1;disposed=!1;dirty=!0;current=_;derive;equals;constructor(e,t,n){super(e,n?.name),this.derive=t,this.equals=n?.equals??Object.is}get value(){return this.runtime.track(this),this.refresh(),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 d(`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===_||!this.equals(this.current,e);return this.current=e,this.dirty=!1,t}finally{this.computing=!1}}},x=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 p(`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()}},S=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 x(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})}}},C=class{activeEffectScope;activeObserver;activeScope;isDisposed=!1;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 x(this,`runtime`),this.activeScope=this.rootScope}get disposed(){return this.isDisposed}signal=(e,t)=>(this.assertActive(),new y(this,e,t));computed=(e,t)=>{this.assertActive();let n=new b(this,e,t);return(this.activeEffectScope??this.activeScope).owned.add(n),n};effect=(e,t)=>{this.assertActive();let n=new S(this,e,t);return(this.activeEffectScope??this.activeScope).owned.add(n),n.run(),n};createScope=e=>{this.assertActive();let t=new x(this,e);return this.activeScope.owned.add(t),t};batch=e=>(this.assertActive(),this.propagate(e));untrack=e=>(this.assertActive(),this.withObserver(void 0,e));dispose(){this.isDisposed||(this.isDisposed=!0,this.rootScope.dispose())}assertActive(){if(this.isDisposed)throw new f(`Cannot use a disposed Ripple runtime.`)}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,e.dependencies.size===0)}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>g)throw new m(`infinite reactive flush (>${g} 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}}},w=e=>(t,n,r)=>{let i=e.signal({status:`pending`},{name:r?.name}),a=e.signal(0),o,s=!1,c=()=>{a.value,o?.abort();let e=i.peek(),r=e.status===`success`?e.value:`previous`in e?e.previous:void 0,c;try{c=t()}catch(e){i.value=r===void 0?{error:e,status:`error`}:{error:e,previous:r,status:`error`};return}let l=new AbortController;o=l,i.value=r===void 0?{status:`pending`}:{previous:r,status:`pending`};let u;try{u=Promise.resolve(n(c,{signal:l.signal}))}catch(e){u=Promise.reject(e)}u.then(e=>{!s&&!l.signal.aborted&&(i.value={status:`success`,value:e})},e=>{!s&&!l.signal.aborted&&(i.value=r===void 0?{error:e,status:`error`}:{error:e,previous:r,status:`error`})})},l=e.effect(()=>(c(),()=>o?.abort()),{name:r?.name});l.disposalSignal.addEventListener(`abort`,()=>{s=!0},{once:!0});let u={get disposalSignal(){return l.disposalSignal},dispose:()=>l.dispose(),get disposed(){return s},get name(){return i.name},peek:()=>i.peek(),reload:()=>{s||(a.value=a.peek()+1)},subscribe:e=>i.subscribe(e),[Symbol.dispose](){this.dispose()},get value(){return i.value}};return Object.defineProperty(u,h,{value:!0}),u},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),r?.once&&(c=!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 C(e),n=w(t);return{batch:t.batch,computed:t.computed,createScope:t.createScope,dispose:()=>t.dispose(),get disposed(){return t.disposed},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.untrack,E.watch,E.resource;function A(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 j(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 M(e,t,n){if(!Number.isFinite(e)||!Number.isInteger(e)||e<n)throw new i(`${t} must be a finite integer greater than or equal to ${n}.`);return e}function N(e,t,n,r=1/0){if(!Number.isFinite(e)||e<n||e>r)throw new i(`${t} must be a finite number between ${n} and ${r}.`);return e}function P(e){return e.map(e=>typeof e==`string`?{field:e,stringify:o,weight:1}:{field:e.field,stringify:e.stringify??o,weight:N(e.weight??1,`weight for field "${e.field}"`,Number.MIN_VALUE)})}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=N(t.threshold??.2,`threshold`,0,1),c=M(t.limit??50,`limit`,0),l=M(t.minQueryLength??3,`minQueryLength`,1),u=new Map,d=new Map,f=new Set,p=0;function m(){p++;for(let e of f)e()}let h=null,g=null;function _(e){return e===h&&g!==null?g:(h=e,g=A(e),g)}function v(e,t){for(let n of t){let t=d.get(n);t||(t=new Set,d.set(n,t)),t.add(e)}}function y(e,t){for(let n of t){let t=d.get(n);t&&(t.delete(e),t.size===0&&d.delete(n))}}function b(e){let t=new Map,r=new Map;for(let{field:i,stringify:o}of n){let n=e[i],s=o(n),c=a(s),l=c.length>=1?A(c):new Set;t.set(i,l),r.set(i,s),v(e,l)}u.set(e,{trigrams:t,values:r})}function x(e){let t=new Set;for(let[n,r]of u)for(let i of r.values.values())if(i.toLowerCase().includes(e)){t.add(n);break}return t}function S(e){let t=new Set;for(let n of e){let e=d.get(n);if(e)for(let n of e)t.add(n)}return t}function C(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=j(t,e)}let c=s/r*n;c>a&&(a=c)}return a}function w(e,t){let r=[];for(let{field:i}of n){let n=t.get(i);if(!n)continue;let a=s(n,e);a.length>0&&r.push({field:i,ranges:a})}return r}function T(e){let t=u.get(e);if(!t)return!1;let r=!1;for(let{field:i,stringify:o}of n){let n=o(e[i]);if(n===t.values.get(i))continue;r=!0;let s=t.trigrams.get(i);s&&y(e,s);let c=a(n),l=c.length>=1?A(c):new Set;t.trigrams.set(i,l),t.values.set(i,n),v(e,l)}return r}function E(e){let t=u.get(e);if(!t)return!1;for(let n of t.trigrams.values())y(e,n);return u.delete(e),!0}for(let t of e)u.has(t)||b(t);return{add(e){u.has(e)||(b(e),m())},get items(){return[...u.keys()]},onMutate(e){return f.add(e),()=>{f.delete(e)}},reindex(e){T(e)&&m()},remove(e){E(e)&&m()},get revision(){return p},search(e,t){let n=N(t?.threshold??o,`threshold`,0,1),r=M(t?.limit??c,`limit`,0),i=M(t?.minQueryLength??l,`minQueryLength`,1);if(!e.trim())return[...u.keys()].slice(0,r).map(e=>({item:e,matches:[],score:1}));let s=a(e);if(!s)return[];let d=s.length<i?null:_(s),f=d===null?x(s):S(d),p=[];for(let e of f){let t=u.get(e);if(!t)continue;let r=C(s,d,t);if(r>=n){let n=w(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[...u.keys()])t.has(e)||(E(e),r=!0);for(let e of n)u.has(e)?T(e)&&(r=!0):(b(e),r=!0);let i=[...u.keys()];if(i.length!==n.length||i.some((e,t)=>e!==n[t])){let e=new Map(n.map(e=>[e,u.get(e)]));u.clear();for(let[t,n]of e)u.set(t,n);r=!0}r&&m()},get size(){return u.size}}}var I=200;function L(e,t={}){let{debounce:n=I,limit:a,minQueryLength:o,threshold:s}=t;if(!Number.isFinite(n)||!Number.isInteger(n)||n<0)throw new i(`debounce must be a finite non-negative integer.`);let c=D(``,{name:`scout:query`}),l=D(``,{name:`scout:committedQuery`}),u=D(0,{name:`scout:indexVersion`}),d=e.onMutate(()=>{u.value++}),f=O(()=>c.value!==l.value,{name:`scout:isSearching`}),p=O(()=>(u.value,e.search(l.value,{limit:a,minQueryLength:o,threshold:s})),{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 r(`SearchState.clear() called after dispose()`);h(),k(()=>{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 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.ScoutConfigurationError=i,exports.ScoutDisposedError=r,exports.ScoutError=n,exports.createIndex=F,exports.createReactiveSearch=R,exports.createSearch=L,exports.findMatchRanges=s,exports.highlight=c,exports.highlightField=l,exports.segmentWords=z,exports.toFilterPredicate=t,exports.toSearchMatcher=e;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});function e(e,t){let n,r=-1,i=new Set;return(a,o)=>((o!==n||e.revision!==r)&&(n=o,r=e.revision,i=new Set(e.search(o,t).map(e=>e.item))),i.has(a))}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 extends Error{constructor(e,t){super(e,t),this.name=new.target.name,Object.setPrototypeOf(this,new.target.prototype)}},r=class extends n{},i=class extends n{};function a(e){return e.toLowerCase().replace(/[^\p{L}\p{N}\s']/gu,` `).trim().replace(/\s+/g,` `)}function o(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 s(e,t){let n=e.toLowerCase(),r=a(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 o=[];for(let e of i){let t=o[o.length-1];t&&e[0]<=t[1]?t[1]=Math.max(t[1],e[1]):o.push([e[0],e[1]])}return o}function c(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 l(e,t,n){return c(n,e.matches.find(e=>e.field===t)?.ranges??[])}var u=class extends Error{constructor(e,t){super(e,t),this.name=new.target.name,Object.setPrototypeOf(this,new.target.prototype)}},d=class extends u{},f=class extends u{},p=class extends u{},m=class extends u{},h=Symbol.for(`@vielzeug/ripple/reactive`),g=100,_=Symbol(`ripple.unset`),v=class{[h]=!0;dependents=new Set;name;runtime;constructor(e,t){this.runtime=e,this.name=t}subscribe(e){this.runtime.assertActive(),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()}},y=class extends v{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.runtime.assertActive(),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}update(e){this.value=e(this.current)}},b=class extends v{dependencies=new Set;computing=!1;disposed=!1;dirty=!0;current=_;derive;equals;constructor(e,t,n){super(e,n?.name),this.derive=t,this.equals=n?.equals??Object.is}get value(){return this.runtime.track(this),this.refresh(),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 d(`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===_||!this.equals(this.current,e);return this.current=e,this.dirty=!1,t}finally{this.computing=!1}}},x=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 p(`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()}},S=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 x(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})}}},C=class{activeEffectScope;activeObserver;activeScope;isDisposed=!1;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 x(this,`runtime`),this.activeScope=this.rootScope}get disposed(){return this.isDisposed}signal=(e,t)=>(this.assertActive(),new y(this,e,t));computed=(e,t)=>{this.assertActive();let n=new b(this,e,t);return(this.activeEffectScope??this.activeScope).owned.add(n),n};effect=(e,t)=>{this.assertActive();let n=new S(this,e,t);return(this.activeEffectScope??this.activeScope).owned.add(n),n.run(),n};createScope=e=>{this.assertActive();let t=new x(this,e);return this.activeScope.owned.add(t),t};batch=e=>(this.assertActive(),this.propagate(e));untrack=e=>(this.assertActive(),this.withObserver(void 0,e));dispose(){this.isDisposed||(this.isDisposed=!0,this.rootScope.dispose())}assertActive(){if(this.isDisposed)throw new f(`Cannot use a disposed Ripple runtime.`)}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,e.dependencies.size===0)}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>g)throw new m(`infinite reactive flush (>${g} 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}}},w=e=>(t,n,r)=>{let i=e.signal({status:`pending`},{name:r?.name}),a=e.signal(0),o,s=!1,c=()=>{a.value,o?.abort();let e=i.peek(),r=e.status===`success`?e.value:`previous`in e?e.previous:void 0,c;try{c=t()}catch(e){i.value=r===void 0?{error:e,status:`error`}:{error:e,previous:r,status:`error`};return}let l=new AbortController;o=l,i.value=r===void 0?{status:`pending`}:{previous:r,status:`pending`};let u;try{u=Promise.resolve(n(c,{signal:l.signal}))}catch(e){u=Promise.reject(e)}u.then(e=>{!s&&!l.signal.aborted&&(i.value={status:`success`,value:e})},e=>{!s&&!l.signal.aborted&&(i.value=r===void 0?{error:e,status:`error`}:{error:e,previous:r,status:`error`})})},l=e.effect(()=>(c(),()=>o?.abort()),{name:r?.name});l.disposalSignal.addEventListener(`abort`,()=>{s=!0},{once:!0});let u={get disposalSignal(){return l.disposalSignal},dispose:()=>l.dispose(),get disposed(){return s},get name(){return i.name},peek:()=>i.peek(),reload:()=>{s||(a.value=a.peek()+1)},subscribe:e=>i.subscribe(e),[Symbol.dispose](){this.dispose()},get value(){return i.value}};return Object.defineProperty(u,h,{value:!0}),u},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),r?.once&&(c=!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 C(e),n=w(t);return{batch:t.batch,computed:t.computed,createScope:t.createScope,dispose:()=>t.dispose(),get disposed(){return t.disposed},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.untrack,E.watch,E.resource;function A(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 j(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 M(e,t,n){if(!Number.isFinite(e)||!Number.isInteger(e)||e<n)throw new i(`${t} must be a finite integer greater than or equal to ${n}.`);return e}function N(e,t,n,r=1/0){if(!Number.isFinite(e)||e<n||e>r)throw new i(`${t} must be a finite number between ${n} and ${r}.`);return e}function P(e){return e.map(e=>typeof e==`string`?{field:e,stringify:o,weight:1}:{field:e.field,stringify:e.stringify??o,weight:N(e.weight??1,`weight for field "${e.field}"`,Number.MIN_VALUE)})}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=N(t.threshold??.2,`threshold`,0,1),c=M(t.limit??50,`limit`,0),l=M(t.minQueryLength??3,`minQueryLength`,1),u=new Map,d=new Map,f=new Set,p=0;function m(){p++;for(let e of f)e()}let h=null,g=null;function _(e){return e===h&&g!==null?g:(h=e,g=A(e),g)}function v(e,t){for(let n of t){let t=d.get(n);t||(t=new Set,d.set(n,t)),t.add(e)}}function y(e,t){for(let n of t){let t=d.get(n);t&&(t.delete(e),t.size===0&&d.delete(n))}}function b(e){let t=new Map,r=new Map;for(let{field:i,stringify:o}of n){let n=e[i],s=o(n),c=a(s),l=c.length>=1?A(c):new Set;t.set(i,l),r.set(i,s),v(e,l)}u.set(e,{trigrams:t,values:r})}function x(e){let t=new Set;for(let[n,r]of u)for(let i of r.values.values())if(i.toLowerCase().includes(e)){t.add(n);break}return t}function S(e){let t=new Set;for(let n of e){let e=d.get(n);if(e)for(let n of e)t.add(n)}return t}function C(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=j(t,e)}let c=s/r*n;c>a&&(a=c)}return a}function w(e,t){let r=[];for(let{field:i}of n){let n=t.get(i);if(!n)continue;let a=s(n,e);a.length>0&&r.push({field:i,ranges:a})}return r}function T(e){let t=u.get(e);if(!t)return!1;let r=!1;for(let{field:i,stringify:o}of n){let n=o(e[i]);if(n===t.values.get(i))continue;r=!0;let s=t.trigrams.get(i);s&&y(e,s);let c=a(n),l=c.length>=1?A(c):new Set;t.trigrams.set(i,l),t.values.set(i,n),v(e,l)}return r}function E(e){let t=u.get(e);if(!t)return!1;for(let n of t.trigrams.values())y(e,n);return u.delete(e),!0}for(let t of e)u.has(t)||b(t);return{add(e){u.has(e)||(b(e),m())},get items(){return[...u.keys()]},onMutate(e){return f.add(e),()=>{f.delete(e)}},reindex(e){T(e)&&m()},remove(e){E(e)&&m()},get revision(){return p},search(e,t){let n=N(t?.threshold??o,`threshold`,0,1),r=M(t?.limit??c,`limit`,0),i=M(t?.minQueryLength??l,`minQueryLength`,1);if(!e.trim())return[...u.keys()].slice(0,r).map(e=>({item:e,matches:[],score:1}));let s=a(e);if(!s)return[];let d=s.length<i?null:_(s),f=d===null?x(s):S(d),p=[];for(let e of f){let t=u.get(e);if(!t)continue;let r=C(s,d,t);if(r>=n){let n=w(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[...u.keys()])t.has(e)||(E(e),r=!0);for(let e of n)u.has(e)?T(e)&&(r=!0):(b(e),r=!0);let i=[...u.keys()];if(i.length!==n.length||i.some((e,t)=>e!==n[t])){let e=new Map(n.map(e=>[e,u.get(e)]));u.clear();for(let[t,n]of e)u.set(t,n);r=!0}r&&m()},get size(){return u.size}}}var I=200;function L(e,t={}){let{debounce:n=I,limit:a,minQueryLength:o,threshold:s}=t;if(!Number.isFinite(n)||!Number.isInteger(n)||n<0)throw new i(`debounce must be a finite non-negative integer.`);let c=D(``,{name:`scout:query`}),l=D(``,{name:`scout:committedQuery`}),u=D(0,{name:`scout:indexVersion`}),d=e.onMutate(()=>{u.value++}),f=O(()=>c.value!==l.value,{name:`scout:isSearching`}),p=O(()=>(u.value,e.search(l.value,{limit:a,minQueryLength:o,threshold:s})),{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 r(`SearchState.clear() called after dispose()`);h(),k(()=>{c.value=``,l.value=``})}let v=!1,y=new AbortController,b=new Set;function x(e){if(b.size!==0)for(let t of b)try{t(e)}catch{}}let S=c.subscribe(()=>x({query:c.peek(),type:`query-change`})),C=f.subscribe(()=>x({isSearching:f.peek(),type:`searching-change`})),w=p.subscribe(()=>x({results:p.peek(),type:`results-change`}));function T(){v||(v=!0,x({type:`dispose`}),b.clear(),y.abort(),h(),g(),d(),S(),C(),w())}function E(e,t){if(v)return()=>{};if(b.add(e),t?.signal){if(t.signal.aborted)return b.delete(e),()=>{};let n=()=>b.delete(e);return t.signal.addEventListener(`abort`,n,{once:!0}),()=>{b.delete(e),t.signal?.removeEventListener(`abort`,n)}}return()=>b.delete(e)}return{clear:_,get disposalSignal(){return y.signal},dispose:T,get disposed(){return v},isSearching:f,query:c,results:p,tap:E,[Symbol.dispose](){T()}}}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.ScoutConfigurationError=i,exports.ScoutDisposedError=r,exports.ScoutError=n,exports.createIndex=F,exports.createReactiveSearch=R,exports.createSearch=L,exports.findMatchRanges=s,exports.highlight=c,exports.highlightField=l,exports.segmentWords=z,exports.toFilterPredicate=t,exports.toSearchMatcher=e;
2
2
  //# sourceMappingURL=scout.cjs.map