@vielzeug/scout 2.3.0 → 3.0.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
@@ -1,25 +1,16 @@
1
1
  # @vielzeug/scout
2
2
 
3
- Fast fuzzy-search. Builds a trigram inverted index at construction O(candidates) per query instead of O(corpus × field_length).
3
+ > Trigram fuzzy-search index with highlighting and reactive layer
4
4
 
5
- ## Features
6
-
7
- - **Trigram index** — fast candidate lookup; overlap-coefficient scoring
8
- - **Multi-field weighted ranking** — per-field weights, custom stringifiers
9
- - **Match highlighting** — character-range offsets for UI rendering
10
- - **Reactive layer** — `createSearch()` wraps any index in `ripple` signals with debounce
11
- - **Framework adapters** — `toSearchMatcher()` for sourcerer, `toFilterPredicate()` for filter pipelines
12
- - **Corpus reconciliation** — `setItems()` reconciles reference-based additions, removals, reindexes, and order in one notification
13
- - **Unsegmented-script helper** — `segmentWords()` pre-splits CJK/Thai text into words via `Intl.Segmenter`
14
- - **Event subscription** — `search.tap()` observes `query`/`isSearching`/`results`/`dispose` transitions
15
-
16
- ## Install
5
+ ## Installation
17
6
 
18
7
  ```sh
19
8
  pnpm add @vielzeug/scout
9
+ npm install @vielzeug/scout
10
+ yarn add @vielzeug/scout
20
11
  ```
21
12
 
22
- ## Quick start
13
+ ## Quick Start
23
14
 
24
15
  ```ts
25
16
  import { createIndex } from '@vielzeug/scout';
@@ -39,39 +30,14 @@ const index = createIndex(users, {
39
30
  console.log(index.search('ada')[0]?.item.name); // Ada Lovelace
40
31
  ```
41
32
 
42
- ## Reactive search
43
-
44
- ```ts
45
- import { createIndex, createSearch } from '@vielzeug/scout';
46
- import { effect } from '@vielzeug/ripple';
47
-
48
- const users = [{ name: 'Ada Lovelace' }, { name: 'Grace Hopper' }];
49
- const index = createIndex(users, { fields: ['name'] });
50
- const search = createSearch(index, { debounce: 150 });
51
-
52
- effect(() => {
53
- console.log(search.results.value);
54
- });
55
-
56
- search.query.value = 'alice';
57
- ```
58
-
59
- ## Sourcerer integration
60
-
61
- ```ts
62
- import { createIndex, toSearchMatcher } from '@vielzeug/scout';
63
- import { createLocalSource } from '@vielzeug/sourcerer';
64
-
65
- const users = [{ email: 'ada@example.com', name: 'Ada Lovelace' }];
66
- const index = createIndex(users, { fields: ['name', 'email'] });
67
- const source = createLocalSource(users, { match: toSearchMatcher(index) });
68
- ```
33
+ ## Documentation
69
34
 
70
- ## Highlighting
35
+ - [Overview](https://vielzeug.dev/scout/)
36
+ - [Usage Guide](https://vielzeug.dev/scout/usage)
37
+ - [API Reference](https://vielzeug.dev/scout/api)
38
+ - [Examples](https://vielzeug.dev/scout/examples)
39
+ - [Migration Guide](https://vielzeug.dev/scout/migration)
71
40
 
72
- ```ts
73
- import { highlight } from '@vielzeug/scout';
41
+ ## License
74
42
 
75
- const parts = highlight('Hello World', [[0, 5]]);
76
- // [{ text: 'Hello', highlighted: true }, { text: ' World', highlighted: false }]
77
- ```
43
+ MIT © [Helmuth Saatkamp](https://github.com/helmuthdu) part of the [Vielzeug](https://github.com/helmuthdu/vielzeug) monorepo.
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, ScoutEvent, ScoutIndexOptions, SearchConstraints, SearchResult, SearchState, } from './types';
9
+ export type { CreateSearchOptions, FieldDef, FieldMatch, HighlightPart, ScoutEvent, ScoutIndexOptions, SearchConstraints, SearchResult, SearchSnapshot, SearchState, SearchSubscribeOptions, } 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,UAAU,EACV,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,cAAc,EACd,WAAW,EACX,sBAAsB,GACvB,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,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;
1
+ const e=require("./errors.cjs"),t=require("./scout-index.cjs");var n=200;function r(e){for(let t of[...e])try{t()}catch(e){queueMicrotask(()=>{throw e})}}function i(e,t){if(e.size!==0)for(let n of[...e])try{n(t)}catch{}}function a(e,t,n,r,i){if(r||n?.signal?.aborted)return()=>{};e.add(t);let a=()=>{e.delete(t),n?.signal?.removeEventListener(`abort`,a)};return n?.signal?.addEventListener(`abort`,a,{once:!0,signal:i}),a}function o(t,o={}){let{debounce:s=n,limit:c,minQueryLength:l,threshold:u}=o;if(!Number.isSafeInteger(s)||s<0)throw new e.ScoutConfigurationError(`debounce must be a finite non-negative integer.`);let d=e=>t.search(e,{limit:c,minQueryLength:l,threshold:u}),f=(e,t,n)=>Object.freeze({isSearching:t,query:e,results:Object.freeze(n)}),p=new Set,m=new Set,h=new AbortController,g=f(``,!1,d(``)),_=``,v=!1,y,b=e=>{g=e,r(p),i(m,{snapshot:e,type:`state-change`})},x=e=>{_=e,b(f(e,!1,d(e)))},S=t.onMutate(()=>{b(f(g.query,g.isSearching,d(_)))}),C=t=>{if(v)throw new e.ScoutDisposedError(`SearchState.${t}() called after dispose()`)},w=()=>{v||(v=!0,y!==void 0&&clearTimeout(y),y=void 0,S(),i(m,{type:`dispose`}),p.clear(),m.clear(),h.abort())};return{clear(){C(`clear`),(g.query!==``||g.isSearching)&&(y!==void 0&&clearTimeout(y),y=void 0,x(``))},get disposalSignal(){return h.signal},dispose:w,get disposed(){return v},getSnapshot(){return g},setQuery(e){if(C(`setQuery`),e!==g.query){if(y!==void 0&&clearTimeout(y),y=void 0,e===_){b(f(e,!1,g.results));return}if(s===0){x(e);return}b(f(e,!0,g.results)),y=setTimeout(()=>{y=void 0,x(e)},s)}},subscribe(e,t){return a(p,e,t,v,h.signal)},tap(e,t){return a(m,e,t,v,h.signal)},[Symbol.dispose]:w}}function s(e,n){let r=t.createIndex(e,n),i=o(r,n);return{clear:i.clear,get disposalSignal(){return i.disposalSignal},dispose:i.dispose,get disposed(){return i.disposed},getSnapshot:i.getSnapshot,index:r,setQuery:i.setQuery,subscribe:i.subscribe,tap:i.tap,[Symbol.dispose]:i[Symbol.dispose]}}exports.createReactiveSearch=s,exports.createSearch=o;
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, 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
+ {"version":3,"file":"reactive.cjs","names":[],"sources":["../src/reactive.ts"],"sourcesContent":["import { ScoutConfigurationError, ScoutDisposedError } from './errors';\nimport { createIndex, type ScoutIndex } from './scout-index';\nimport type {\n CreateSearchOptions,\n ScoutEvent,\n ScoutIndexOptions,\n SearchResult,\n SearchSnapshot,\n SearchState,\n SearchSubscribeOptions,\n} from './types';\n\nexport type ReactiveSearch<T> = SearchState<T> & { readonly index: ScoutIndex<T> };\n\nconst DEFAULT_DEBOUNCE = 200;\n\nfunction notify(listeners: Set<() => void>): void {\n for (const listener of [...listeners]) {\n try {\n listener();\n } catch (error) {\n queueMicrotask(() => {\n throw error;\n });\n }\n }\n}\n\nfunction emit<T>(tappers: Set<(event: ScoutEvent<T>) => void>, event: ScoutEvent<T>): void {\n if (tappers.size === 0) return;\n\n for (const tapper of [...tappers]) {\n try {\n tapper(event);\n } catch {}\n }\n}\n\nfunction subscribe<T>(\n listeners: Set<T>,\n listener: T,\n options: SearchSubscribeOptions | undefined,\n disposed: boolean,\n lifetimeSignal: AbortSignal,\n): () => void {\n if (disposed || options?.signal?.aborted) return () => {};\n\n listeners.add(listener);\n const unsubscribe = (): void => {\n listeners.delete(listener);\n options?.signal?.removeEventListener('abort', unsubscribe);\n };\n options?.signal?.addEventListener('abort', unsubscribe, { once: true, signal: lifetimeSignal });\n return unsubscribe;\n}\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.isSafeInteger(debounceMs) || debounceMs < 0) {\n throw new ScoutConfigurationError('debounce must be a finite non-negative integer.');\n }\n\n const search = (query: string) => index.search(query, { limit, minQueryLength, threshold });\n const createSnapshot = (\n query: string,\n isSearching: boolean,\n results: ReadonlyArray<SearchResult<T>>,\n ): SearchSnapshot<T> => Object.freeze({ isSearching, query, results: Object.freeze(results) });\n const listeners = new Set<() => void>();\n const tappers = new Set<(event: ScoutEvent<T>) => void>();\n const controller = new AbortController();\n let snapshot = createSnapshot('', false, search(''));\n let committedQuery = '';\n let disposed = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n const publish = (next: SearchSnapshot<T>): void => {\n snapshot = next;\n notify(listeners);\n emit(tappers, { snapshot: next, type: 'state-change' });\n };\n const commit = (query: string): void => {\n committedQuery = query;\n publish(createSnapshot(query, false, search(query)));\n };\n const unsubscribeIndex = index.onMutate(() => {\n publish(createSnapshot(snapshot.query, snapshot.isSearching, search(committedQuery)));\n });\n const assertActive = (method: string): void => {\n if (disposed) throw new ScoutDisposedError(`SearchState.${method}() called after dispose()`);\n };\n const dispose = (): void => {\n if (disposed) return;\n disposed = true;\n if (timer !== undefined) clearTimeout(timer);\n timer = undefined;\n unsubscribeIndex();\n emit(tappers, { type: 'dispose' });\n listeners.clear();\n tappers.clear();\n controller.abort();\n };\n\n return {\n clear() {\n assertActive('clear');\n if (snapshot.query === '' && !snapshot.isSearching) return;\n if (timer !== undefined) clearTimeout(timer);\n timer = undefined;\n commit('');\n },\n get disposalSignal() {\n return controller.signal;\n },\n dispose,\n get disposed() {\n return disposed;\n },\n getSnapshot() {\n return snapshot;\n },\n setQuery(query) {\n assertActive('setQuery');\n if (query === snapshot.query) return;\n if (timer !== undefined) clearTimeout(timer);\n timer = undefined;\n\n if (query === committedQuery) {\n publish(createSnapshot(query, false, snapshot.results));\n return;\n }\n\n if (debounceMs === 0) {\n commit(query);\n return;\n }\n\n publish(createSnapshot(query, true, snapshot.results));\n timer = setTimeout(() => {\n timer = undefined;\n commit(query);\n }, debounceMs);\n },\n subscribe(listener, subscribeOptions) {\n return subscribe(listeners, listener, subscribeOptions, disposed, controller.signal);\n },\n tap(handler, tapOptions) {\n return subscribe(tappers, handler, tapOptions, disposed, controller.signal);\n },\n [Symbol.dispose]: dispose,\n };\n}\n\nexport function createReactiveSearch<T>(\n items: T[],\n options: ScoutIndexOptions<T> & Pick<CreateSearchOptions, 'debounce'>,\n): ReactiveSearch<T> {\n const index = createIndex(items, options);\n const state = createSearch(index, options);\n\n return {\n clear: state.clear,\n get disposalSignal() {\n return state.disposalSignal;\n },\n dispose: state.dispose,\n get disposed() {\n return state.disposed;\n },\n getSnapshot: state.getSnapshot,\n index,\n setQuery: state.setQuery,\n subscribe: state.subscribe,\n tap: state.tap,\n [Symbol.dispose]: state[Symbol.dispose],\n };\n}\n"],"mappings":"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"}
@@ -1,74 +1,8 @@
1
1
  import { type ScoutIndex } from './scout-index';
2
2
  import type { CreateSearchOptions, ScoutIndexOptions, SearchState } from './types';
3
- /**
4
- * Combined index + reactive search state returned by `createReactiveSearch()`.
5
- * Exposes the underlying `ScoutIndex` for incremental mutations (`add`, `remove`, `reindex`).
6
- */
7
3
  export type ReactiveSearch<T> = SearchState<T> & {
8
4
  readonly index: ScoutIndex<T>;
9
5
  };
10
- /**
11
- * Creates a reactive search state backed by a `ScoutIndex`.
12
- *
13
- * - Set `state.query.value` to trigger a (debounced) search.
14
- * - Read `state.results.value` inside an `effect` or `computed` to consume results reactively.
15
- * - `state.isSearching.value` is `true` while debouncing, `false` otherwise.
16
- * - Call `state.dispose()` (or `using state = createSearch(...)`) to release subscriptions.
17
- *
18
- * @example
19
- * ```ts
20
- * const index = createIndex(users, { fields: ['name', 'email'] });
21
- * const search = createSearch(index, { debounce: 150 });
22
- *
23
- * effect(() => {
24
- * const results = search.results.value;
25
- * renderList(results.map(r => r.item));
26
- * });
27
- *
28
- * // Wire to an input
29
- * input.addEventListener('input', e => {
30
- * search.query.value = e.currentTarget.value;
31
- * });
32
- *
33
- * // Clean up
34
- * search.dispose();
35
- * ```
36
- *
37
- * `results` also updates when the index is mutated directly via `index.add()` / `.remove()`
38
- * / `.reindex()` / `.setItems()` (not just when `query` changes), by subscribing to `index.onMutate()`.
39
- *
40
- * @param index - A `ScoutIndex` built with `createIndex()`.
41
- * @param options.debounce - Milliseconds to wait before committing query changes. Default: `200`.
42
- * @param options.limit - Override the index-level result limit.
43
- * @param options.minQueryLength - Override the index-level minimum query length.
44
- * @param options.threshold - Override the index-level score threshold.
45
- */
46
6
  export declare function createSearch<T>(index: ScoutIndex<T>, options?: CreateSearchOptions): SearchState<T>;
47
- /**
48
- * Creates a `ScoutIndex` and a reactive search state in one call — the shorthand
49
- * for the common pattern of `createIndex` + `createSearch`.
50
- *
51
- * The returned `ReactiveSearch` exposes the underlying index via `.index` for
52
- * incremental mutations (`add`, `remove`, `reindex`, `setItems`) after construction.
53
- *
54
- * @example
55
- * ```ts
56
- * const search = createReactiveSearch(users, {
57
- * fields: [{ field: 'name', weight: 2 }, 'email'],
58
- * debounce: 150,
59
- * });
60
- *
61
- * effect(() => renderList(search.results.value.map(r => r.item)));
62
- *
63
- * // Wire to an input
64
- * input.addEventListener('input', e => { search.query.value = e.currentTarget.value; });
65
- *
66
- * // Add a new item at runtime
67
- * search.index.add(newUser);
68
- * ```
69
- *
70
- * @param items - Initial corpus to index.
71
- * @param options - Index options (`fields`, `limit`, `minQueryLength`, `threshold`) plus optional `debounce`.
72
- */
73
7
  export declare function createReactiveSearch<T>(items: T[], options: ScoutIndexOptions<T> & Pick<CreateSearchOptions, 'debounce'>): ReactiveSearch<T>;
74
8
  //# sourceMappingURL=reactive.d.ts.map
@@ -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,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"}
1
+ {"version":3,"file":"reactive.d.ts","sourceRoot":"","sources":["../src/reactive.ts"],"names":[],"mappings":"AACA,OAAO,EAAe,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,KAAK,EACV,mBAAmB,EAEnB,iBAAiB,EAGjB,WAAW,EAEZ,MAAM,SAAS,CAAC;AAEjB,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,GAAG;IAAE,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,CAAA;CAAE,CAAC;AA4CnF,wBAAgB,YAAY,CAAC,CAAC,EAAE,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE,OAAO,GAAE,mBAAwB,GAAG,WAAW,CAAC,CAAC,CAAC,CAgGvG;AAED,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,CAoBnB"}
package/dist/reactive.js CHANGED
@@ -1,95 +1,113 @@
1
1
  import { ScoutConfigurationError as e, ScoutDisposedError as t } from "./errors.js";
2
2
  import { createIndex as n } from "./scout-index.js";
3
- import { batch as r, computed as i, signal as a } from "@vielzeug/ripple";
4
3
  //#region src/reactive.ts
5
- var o = 200;
4
+ var r = 200;
5
+ function i(e) {
6
+ for (let t of [...e]) try {
7
+ t();
8
+ } catch (e) {
9
+ queueMicrotask(() => {
10
+ throw e;
11
+ });
12
+ }
13
+ }
14
+ function a(e, t) {
15
+ if (e.size !== 0) for (let n of [...e]) try {
16
+ n(t);
17
+ } catch {}
18
+ }
19
+ function o(e, t, n, r, i) {
20
+ if (r || n?.signal?.aborted) return () => {};
21
+ e.add(t);
22
+ let a = () => {
23
+ e.delete(t), n?.signal?.removeEventListener("abort", a);
24
+ };
25
+ return n?.signal?.addEventListener("abort", a, {
26
+ once: !0,
27
+ signal: i
28
+ }), a;
29
+ }
6
30
  function s(n, s = {}) {
7
- let { debounce: c = o, limit: l, minQueryLength: u, threshold: d } = s;
8
- if (!Number.isFinite(c) || !Number.isInteger(c) || c < 0) throw new e("debounce must be a finite non-negative integer.");
9
- let f = a("", { name: "scout:query" }), p = a("", { name: "scout:committedQuery" }), m = a(0, { name: "scout:indexVersion" }), h = n.onMutate(() => {
10
- m.value++;
11
- }), g = i(() => f.value !== p.value, { name: "scout:isSearching" }), _ = i(() => (m.value, n.search(p.value, {
31
+ let { debounce: c = r, limit: l, minQueryLength: u, threshold: d } = s;
32
+ if (!Number.isSafeInteger(c) || c < 0) throw new e("debounce must be a finite non-negative integer.");
33
+ let f = (e) => n.search(e, {
12
34
  limit: l,
13
35
  minQueryLength: u,
14
36
  threshold: d
15
- })), { name: "scout:results" }), v = null;
16
- function y() {
17
- v !== null && (clearTimeout(v), v = null);
18
- }
19
- let b = f.subscribe(() => {
20
- let e = f.peek();
21
- if (y(), e !== p.peek()) {
22
- if (c === 0) {
23
- p.value = e;
24
- return;
25
- }
26
- v = setTimeout(() => {
27
- p.value = e, v = null;
28
- }, c);
29
- }
30
- });
31
- function x() {
32
- if (S) throw new t("SearchState.clear() called after dispose()");
33
- y(), r(() => {
34
- f.value = "", p.value = "";
37
+ }), p = (e, t, n) => Object.freeze({
38
+ isSearching: t,
39
+ query: e,
40
+ results: Object.freeze(n)
41
+ }), m = /* @__PURE__ */ new Set(), h = /* @__PURE__ */ new Set(), g = new AbortController(), _ = p("", !1, f("")), v = "", y = !1, b, x = (e) => {
42
+ _ = e, i(m), a(h, {
43
+ snapshot: e,
44
+ type: "state-change"
35
45
  });
36
- }
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);
66
- }
46
+ }, S = (e) => {
47
+ v = e, x(p(e, !1, f(e)));
48
+ }, C = n.onMutate(() => {
49
+ x(p(_.query, _.isSearching, f(v)));
50
+ }), w = (e) => {
51
+ if (y) throw new t(`SearchState.${e}() called after dispose()`);
52
+ }, T = () => {
53
+ y || (y = !0, b !== void 0 && clearTimeout(b), b = void 0, C(), a(h, { type: "dispose" }), m.clear(), h.clear(), g.abort());
54
+ };
67
55
  return {
68
- clear: x,
56
+ clear() {
57
+ w("clear"), (_.query !== "" || _.isSearching) && (b !== void 0 && clearTimeout(b), b = void 0, S(""));
58
+ },
69
59
  get disposalSignal() {
70
- return C.signal;
60
+ return g.signal;
71
61
  },
72
- dispose: k,
62
+ dispose: T,
73
63
  get disposed() {
74
- return S;
64
+ return y;
65
+ },
66
+ getSnapshot() {
67
+ return _;
68
+ },
69
+ setQuery(e) {
70
+ if (w("setQuery"), e !== _.query) {
71
+ if (b !== void 0 && clearTimeout(b), b = void 0, e === v) {
72
+ x(p(e, !1, _.results));
73
+ return;
74
+ }
75
+ if (c === 0) {
76
+ S(e);
77
+ return;
78
+ }
79
+ x(p(e, !0, _.results)), b = setTimeout(() => {
80
+ b = void 0, S(e);
81
+ }, c);
82
+ }
83
+ },
84
+ subscribe(e, t) {
85
+ return o(m, e, t, y, g.signal);
86
+ },
87
+ tap(e, t) {
88
+ return o(h, e, t, y, g.signal);
75
89
  },
76
- isSearching: g,
77
- query: f,
78
- results: _,
79
- tap: A,
80
- [Symbol.dispose]() {
81
- k();
82
- }
90
+ [Symbol.dispose]: T
83
91
  };
84
92
  }
85
93
  function c(e, t) {
86
- let r = n(e, {
87
- fields: t.fields,
88
- limit: t.limit,
89
- minQueryLength: t.minQueryLength,
90
- threshold: t.threshold
91
- }), i = s(r, { debounce: t.debounce });
92
- return Object.assign(Object.create(i), { index: r });
94
+ let r = n(e, t), i = s(r, t);
95
+ return {
96
+ clear: i.clear,
97
+ get disposalSignal() {
98
+ return i.disposalSignal;
99
+ },
100
+ dispose: i.dispose,
101
+ get disposed() {
102
+ return i.disposed;
103
+ },
104
+ getSnapshot: i.getSnapshot,
105
+ index: r,
106
+ setQuery: i.setQuery,
107
+ subscribe: i.subscribe,
108
+ tap: i.tap,
109
+ [Symbol.dispose]: i[Symbol.dispose]
110
+ };
93
111
  }
94
112
  //#endregion
95
113
  export { c as createReactiveSearch, s as createSearch };
@@ -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, 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
+ {"version":3,"file":"reactive.js","names":[],"sources":["../src/reactive.ts"],"sourcesContent":["import { ScoutConfigurationError, ScoutDisposedError } from './errors';\nimport { createIndex, type ScoutIndex } from './scout-index';\nimport type {\n CreateSearchOptions,\n ScoutEvent,\n ScoutIndexOptions,\n SearchResult,\n SearchSnapshot,\n SearchState,\n SearchSubscribeOptions,\n} from './types';\n\nexport type ReactiveSearch<T> = SearchState<T> & { readonly index: ScoutIndex<T> };\n\nconst DEFAULT_DEBOUNCE = 200;\n\nfunction notify(listeners: Set<() => void>): void {\n for (const listener of [...listeners]) {\n try {\n listener();\n } catch (error) {\n queueMicrotask(() => {\n throw error;\n });\n }\n }\n}\n\nfunction emit<T>(tappers: Set<(event: ScoutEvent<T>) => void>, event: ScoutEvent<T>): void {\n if (tappers.size === 0) return;\n\n for (const tapper of [...tappers]) {\n try {\n tapper(event);\n } catch {}\n }\n}\n\nfunction subscribe<T>(\n listeners: Set<T>,\n listener: T,\n options: SearchSubscribeOptions | undefined,\n disposed: boolean,\n lifetimeSignal: AbortSignal,\n): () => void {\n if (disposed || options?.signal?.aborted) return () => {};\n\n listeners.add(listener);\n const unsubscribe = (): void => {\n listeners.delete(listener);\n options?.signal?.removeEventListener('abort', unsubscribe);\n };\n options?.signal?.addEventListener('abort', unsubscribe, { once: true, signal: lifetimeSignal });\n return unsubscribe;\n}\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.isSafeInteger(debounceMs) || debounceMs < 0) {\n throw new ScoutConfigurationError('debounce must be a finite non-negative integer.');\n }\n\n const search = (query: string) => index.search(query, { limit, minQueryLength, threshold });\n const createSnapshot = (\n query: string,\n isSearching: boolean,\n results: ReadonlyArray<SearchResult<T>>,\n ): SearchSnapshot<T> => Object.freeze({ isSearching, query, results: Object.freeze(results) });\n const listeners = new Set<() => void>();\n const tappers = new Set<(event: ScoutEvent<T>) => void>();\n const controller = new AbortController();\n let snapshot = createSnapshot('', false, search(''));\n let committedQuery = '';\n let disposed = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n const publish = (next: SearchSnapshot<T>): void => {\n snapshot = next;\n notify(listeners);\n emit(tappers, { snapshot: next, type: 'state-change' });\n };\n const commit = (query: string): void => {\n committedQuery = query;\n publish(createSnapshot(query, false, search(query)));\n };\n const unsubscribeIndex = index.onMutate(() => {\n publish(createSnapshot(snapshot.query, snapshot.isSearching, search(committedQuery)));\n });\n const assertActive = (method: string): void => {\n if (disposed) throw new ScoutDisposedError(`SearchState.${method}() called after dispose()`);\n };\n const dispose = (): void => {\n if (disposed) return;\n disposed = true;\n if (timer !== undefined) clearTimeout(timer);\n timer = undefined;\n unsubscribeIndex();\n emit(tappers, { type: 'dispose' });\n listeners.clear();\n tappers.clear();\n controller.abort();\n };\n\n return {\n clear() {\n assertActive('clear');\n if (snapshot.query === '' && !snapshot.isSearching) return;\n if (timer !== undefined) clearTimeout(timer);\n timer = undefined;\n commit('');\n },\n get disposalSignal() {\n return controller.signal;\n },\n dispose,\n get disposed() {\n return disposed;\n },\n getSnapshot() {\n return snapshot;\n },\n setQuery(query) {\n assertActive('setQuery');\n if (query === snapshot.query) return;\n if (timer !== undefined) clearTimeout(timer);\n timer = undefined;\n\n if (query === committedQuery) {\n publish(createSnapshot(query, false, snapshot.results));\n return;\n }\n\n if (debounceMs === 0) {\n commit(query);\n return;\n }\n\n publish(createSnapshot(query, true, snapshot.results));\n timer = setTimeout(() => {\n timer = undefined;\n commit(query);\n }, debounceMs);\n },\n subscribe(listener, subscribeOptions) {\n return subscribe(listeners, listener, subscribeOptions, disposed, controller.signal);\n },\n tap(handler, tapOptions) {\n return subscribe(tappers, handler, tapOptions, disposed, controller.signal);\n },\n [Symbol.dispose]: dispose,\n };\n}\n\nexport function createReactiveSearch<T>(\n items: T[],\n options: ScoutIndexOptions<T> & Pick<CreateSearchOptions, 'debounce'>,\n): ReactiveSearch<T> {\n const index = createIndex(items, options);\n const state = createSearch(index, options);\n\n return {\n clear: state.clear,\n get disposalSignal() {\n return state.disposalSignal;\n },\n dispose: state.dispose,\n get disposed() {\n return state.disposed;\n },\n getSnapshot: state.getSnapshot,\n index,\n setQuery: state.setQuery,\n subscribe: state.subscribe,\n tap: state.tap,\n [Symbol.dispose]: state[Symbol.dispose],\n };\n}\n"],"mappings":"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"}