@vielzeug/scout 1.1.4 → 1.1.6
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/dist/scout-index.cjs +1 -1
- package/dist/scout-index.cjs.map +1 -1
- package/dist/scout-index.js +15 -15
- package/dist/scout-index.js.map +1 -1
- package/dist/scout.cjs +1 -1
- package/dist/scout.cjs.map +1 -1
- package/dist/scout.iife.js +1 -1
- package/dist/scout.iife.js.map +1 -1
- package/dist/scout.js +1 -1
- package/dist/scout.js.map +1 -1
- package/dist/trigram.cjs +1 -1
- package/dist/trigram.cjs.map +1 -1
- package/dist/trigram.js +2 -2
- package/dist/trigram.js.map +1 -1
- package/dist/types.d.ts +4 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/scout-index.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./errors.cjs"),t=require("./highlight.cjs"),n=require("./tokenize.cjs"),r=require("./trigram.cjs");function i(e){return e.map(e=>typeof e==`string`?{field:e,stringify:n.defaultStringify,weight:1}:{field:e.field,stringify:e.stringify??n.defaultStringify,weight:e.weight??1})}function a(a,o){if(o.fields.length===0)throw new e.ScoutIndexError(`createIndex: at least one field is required.`);let s=i(o.fields),c=s.reduce((e,t)=>Math.max(e,t.weight),1),l=o.threshold??.2,u=o.limit??50,d=o.minQueryLength??3,f=new Map,p=new Map,m=new Set;function h(){for(let e of m)e()}let g=null,_=null;function v(e){return e===g&&_!==null?_:(g=e,_=r.generateTrigrams(e),_)}function y(e,t){for(let n of t){let t=p.get(n);t||(t=new Set,p.set(n,t)),t.add(e)}}function b(e,t){for(let n of t){let t=p.get(n);t&&(t.delete(e),t.size===0&&p.delete(n))}}function x(e){let t=new Map,i=new Map;for(let{field:a,stringify:o}of s){let s=e[a],c=o(s),l=n.tokenize(c),u=l.length>=1?r.generateTrigrams(l):new Set;t.set(a,u),i.set(a,c),y(e,u)}f.set(e,{trigrams:t,values:i})}function S(e){let t=new Set;for(let[n,r]of f)for(let i of r.values.values())if(i.toLowerCase().includes(e)){t.add(n);break}return t}function C(e){let t=new Set;for(let n of e){let e=p.get(n);if(e)for(let n of e)t.add(n)}return t}function w(e,t,n){let i=0;for(let{field:a,weight:o}of s){let s;if(t===null)s=+!!(n.values.get(a)??``).toLowerCase().includes(e);else{let e=n.trigrams.get(a);if(!e||e.size===0)continue;s=r.
|
|
1
|
+
const e=require("./errors.cjs"),t=require("./highlight.cjs"),n=require("./tokenize.cjs"),r=require("./trigram.cjs");function i(e){return e.map(e=>typeof e==`string`?{field:e,stringify:n.defaultStringify,weight:1}:{field:e.field,stringify:e.stringify??n.defaultStringify,weight:e.weight??1})}function a(a,o){if(o.fields.length===0)throw new e.ScoutIndexError(`createIndex: at least one field is required.`);let s=i(o.fields),c=s.reduce((e,t)=>Math.max(e,t.weight),1),l=o.threshold??.2,u=o.limit??50,d=o.minQueryLength??3,f=new Map,p=new Map,m=new Set;function h(){for(let e of m)e()}let g=null,_=null;function v(e){return e===g&&_!==null?_:(g=e,_=r.generateTrigrams(e),_)}function y(e,t){for(let n of t){let t=p.get(n);t||(t=new Set,p.set(n,t)),t.add(e)}}function b(e,t){for(let n of t){let t=p.get(n);t&&(t.delete(e),t.size===0&&p.delete(n))}}function x(e){let t=new Map,i=new Map;for(let{field:a,stringify:o}of s){let s=e[a],c=o(s),l=n.tokenize(c),u=l.length>=1?r.generateTrigrams(l):new Set;t.set(a,u),i.set(a,c),y(e,u)}f.set(e,{trigrams:t,values:i})}function S(e){let t=new Set;for(let[n,r]of f)for(let i of r.values.values())if(i.toLowerCase().includes(e)){t.add(n);break}return t}function C(e){let t=new Set;for(let n of e){let e=p.get(n);if(e)for(let n of e)t.add(n)}return t}function w(e,t,n){let i=0;for(let{field:a,weight:o}of s){let s;if(t===null)s=+!!(n.values.get(a)??``).toLowerCase().includes(e);else{let e=n.trigrams.get(a);if(!e||e.size===0)continue;s=r.overlapSimilarity(t,e)}let l=o/c*s;l>i&&(i=l)}return i}function T(e,n){let r=[];for(let{field:i}of s){let a=n.get(i);if(!a)continue;let o=t.findMatchRanges(a,e);o.length>0&&r.push({field:i,ranges:o})}return r}for(let e of a)x(e);return{add(e){f.has(e)||(x(e),h())},get items(){return[...f.keys()]},onMutate(e){return m.add(e),()=>{m.delete(e)}},reindex(e){let t=f.get(e);if(!t)return;let i=!1;for(let{field:a,stringify:o}of s){let s=e[a],c=o(s);if(c===t.values.get(a))continue;i=!0;let l=t.trigrams.get(a);l&&b(e,l);let u=n.tokenize(c),d=u.length>=1?r.generateTrigrams(u):new Set;t.trigrams.set(a,d),t.values.set(a,c),y(e,d)}i&&h()},remove(e){let t=f.get(e);if(t){for(let n of t.trigrams.values())b(e,n);f.delete(e),h()}},search(e,t){let r=t?.threshold??l,i=Math.max(0,t?.limit??u),a=t?.minQueryLength??d;if(!e.trim())return[...f.keys()].slice(0,i).map(e=>({item:e,matches:[],score:1}));let o=n.tokenize(e);if(!o)return[];let s=o.length<a?null:v(o),c=s===null?S(o):C(s),p=[];for(let e of c){let t=f.get(e);if(!t)continue;let n=w(o,s,t);if(n>=r){let r=T(o,t.values);p.push({item:e,matches:r,score:n})}}return p.sort((e,t)=>t.score-e.score).slice(0,i)},get size(){return f.size}}}exports.createIndex=a;
|
|
2
2
|
//# sourceMappingURL=scout-index.cjs.map
|
package/dist/scout-index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scout-index.cjs","names":[],"sources":["../src/scout-index.ts"],"sourcesContent":["import type { FieldDef, FieldMatch, ScoutIndexOptions, SearchConstraints, SearchResult } from './types';\n\nimport { ScoutIndexError } from './errors';\nimport { findMatchRanges } from './highlight';\nimport { defaultStringify, tokenize } from './tokenize';\nimport { diceSimilarity, generateTrigrams } from './trigram';\n\ntype FieldConfig<T> = {\n field: keyof T & string;\n stringify: (v: unknown) => string;\n weight: number;\n};\n\ntype ItemRecord = {\n /** Per-field trigrams for scoring. */\n trigrams: Map<string, Set<string>>;\n /** Per-field original text for highlighting. */\n values: Map<string, string>;\n};\n\n/**\n * A stateful, indexed search corpus. Created via `createIndex()`.\n *\n * Supports incremental `add()`, `remove()`, and `reindex()` operations — each patches\n * the trigram index in O(field_length) without a full rebuild.\n */\nexport interface ScoutIndex<T> {\n /** All items currently in the index, in insertion order. */\n readonly items: readonly T[];\n /** Number of items currently in the index. */\n readonly size: number;\n /** Adds `item` to the index. No-op if the item is already indexed (by reference). */\n add(item: T): void;\n /**\n * Re-reads the item's current field values and rebuilds its index entry in-place,\n * only updating fields whose values have changed. Preserves insertion order.\n * No-op if the item is not in the index.\n */\n reindex(item: T): void;\n /**\n * Removes `item` from the index by reference equality.\n * No-op if the item is not in the index.\n */\n remove(item: T): void;\n /**\n * Searches the index for `query` and returns results sorted by score descending.\n *\n * An empty (or whitespace-only) `query` returns all indexed items with `score = 1`.\n * A `query` with no indexable content after normalization (e.g. punctuation-only) returns\n * no results. Results below `threshold` are excluded. At most `limit` results are returned.\n */\n search(query: string, options?: SearchConstraints): SearchResult<T>[];\n /**\n * Subscribes `listener` to be called after every `add()` / `remove()` / `reindex()` call\n * that actually changes the index (no-ops — e.g. removing an unindexed item — don't fire\n * it). Returns an unsubscribe function.\n *\n * Framework-agnostic extension point: `createSearch()` uses this internally to keep\n * reactive `results` in sync with index mutations. Most callers won't need this directly.\n */\n onMutate(listener: () => void): () => void;\n}\n\nfunction resolveFields<T>(defs: ReadonlyArray<FieldDef<T>>): FieldConfig<T>[] {\n return defs.map((def) => {\n if (typeof def === 'string') {\n return { field: def, stringify: defaultStringify, weight: 1 };\n }\n\n return {\n field: def.field,\n stringify: def.stringify ?? defaultStringify,\n weight: def.weight ?? 1,\n };\n });\n}\n\n/**\n * Builds a trigram inverted index over `items` for fast fuzzy search.\n *\n * Construction is O(corpus × field_length). Subsequent `search()` calls are\n * O(candidates) — far faster than per-query Levenshtein for large corpora.\n *\n * @example\n * ```ts\n * const index = createIndex(users, {\n * fields: [{ field: 'name', weight: 2 }, 'email'],\n * threshold: 0.3,\n * limit: 20,\n * });\n *\n * const results = index.search('alice');\n * ```\n *\n * @throws {ScoutIndexError} If `options.fields` is empty.\n */\nexport function createIndex<T>(items: T[], options: ScoutIndexOptions<T>): ScoutIndex<T> {\n if (options.fields.length === 0) {\n throw new ScoutIndexError('createIndex: at least one field is required.');\n }\n\n const fields = resolveFields(options.fields);\n const maxWeight = fields.reduce((max, f) => Math.max(max, f.weight), 1);\n const defaultThreshold = options.threshold ?? 0.2;\n const defaultLimit = options.limit ?? 50;\n const defaultMinQueryLength = options.minQueryLength ?? 3;\n\n /** item → per-item record, preserves insertion order for `items` getter */\n const itemData = new Map<T, ItemRecord>();\n /** trigram → set of items that contain it */\n const invertedIndex = new Map<string, Set<T>>();\n const mutationListeners = new Set<() => void>();\n\n function notifyMutation(): void {\n for (const listener of mutationListeners) listener();\n }\n\n /** Single-entry cache for the most recent normalized query's trigrams (F2). */\n let cachedNormalized: string | null = null;\n let cachedTrigrams: Set<string> | null = null;\n\n function getQueryTrigrams(normalized: string): Set<string> {\n if (normalized === cachedNormalized && cachedTrigrams !== null) return cachedTrigrams;\n\n cachedNormalized = normalized;\n cachedTrigrams = generateTrigrams(normalized);\n\n return cachedTrigrams;\n }\n\n function addFieldToIndex(item: T, fieldTrigrams: Set<string>): void {\n for (const trigram of fieldTrigrams) {\n let bucket = invertedIndex.get(trigram);\n\n if (!bucket) {\n bucket = new Set<T>();\n invertedIndex.set(trigram, bucket);\n }\n\n bucket.add(item);\n }\n }\n\n function removeFieldFromIndex(item: T, fieldTrigrams: Set<string>): void {\n for (const trigram of fieldTrigrams) {\n const bucket = invertedIndex.get(trigram);\n\n if (bucket) {\n bucket.delete(item);\n\n if (bucket.size === 0) invertedIndex.delete(trigram);\n }\n }\n }\n\n function addItem(item: T): void {\n const trigrams = new Map<string, Set<string>>();\n const values = new Map<string, string>();\n\n for (const { field, stringify } of fields) {\n const raw = item[field];\n const text = stringify(raw);\n const normalized = tokenize(text);\n const fieldTrigrams = normalized.length >= 1 ? generateTrigrams(normalized) : new Set<string>();\n\n trigrams.set(field, fieldTrigrams);\n values.set(field, text);\n addFieldToIndex(item, fieldTrigrams);\n }\n\n itemData.set(item, { trigrams, values });\n }\n\n /**\n * Performs a full linear scan over all items for short queries.\n * O(n × field_count) — acceptable for small corpora; consider raising\n * `minQueryLength` on large datasets to avoid triggering this path.\n */\n function containmentScan(query: string): Set<T> {\n const result = new Set<T>();\n\n for (const [item, record] of itemData) {\n for (const value of record.values.values()) {\n if (value.toLowerCase().includes(query)) {\n result.add(item);\n break;\n }\n }\n }\n\n return result;\n }\n\n function trigramCandidates(queryTrigrams: Set<string>): Set<T> {\n const candidates = new Set<T>();\n\n for (const trigram of queryTrigrams) {\n const items = invertedIndex.get(trigram);\n\n if (items) {\n for (const item of items) candidates.add(item);\n }\n }\n\n return candidates;\n }\n\n function scoreCandidate(normalized: string, queryTrigrams: Set<string> | null, record: ItemRecord): number {\n let bestScore = 0;\n\n for (const { field, weight } of fields) {\n let fieldScore: number;\n\n if (queryTrigrams === null) {\n const raw = record.values.get(field) ?? '';\n\n fieldScore = raw.toLowerCase().includes(normalized) ? 1.0 : 0;\n } else {\n const itemTrigrams = record.trigrams.get(field);\n\n if (!itemTrigrams || itemTrigrams.size === 0) continue;\n\n fieldScore = diceSimilarity(queryTrigrams, itemTrigrams);\n }\n\n const weighted = fieldScore * (weight / maxWeight);\n\n if (weighted > bestScore) bestScore = weighted;\n }\n\n return bestScore;\n }\n\n function computeMatches(query: string, values: Map<string, string>): FieldMatch<keyof T & string>[] {\n const matches: FieldMatch<keyof T & string>[] = [];\n\n for (const { field } of fields) {\n const text = values.get(field);\n\n if (!text) continue;\n\n const ranges = findMatchRanges(text, query);\n\n if (ranges.length > 0) matches.push({ field, ranges });\n }\n\n return matches;\n }\n\n for (const item of items) {\n addItem(item);\n }\n\n return {\n add(item: T): void {\n if (itemData.has(item)) return;\n\n addItem(item);\n notifyMutation();\n },\n\n get items(): readonly T[] {\n return [...itemData.keys()];\n },\n\n onMutate(listener: () => void): () => void {\n mutationListeners.add(listener);\n\n return () => {\n mutationListeners.delete(listener);\n };\n },\n\n reindex(item: T): void {\n const record = itemData.get(item);\n\n if (!record) return;\n\n let changed = false;\n\n for (const { field, stringify } of fields) {\n const raw = item[field];\n const newText = stringify(raw);\n const oldText = record.values.get(field);\n\n if (newText === oldText) continue;\n\n changed = true;\n\n const oldTrigrams = record.trigrams.get(field);\n\n if (oldTrigrams) removeFieldFromIndex(item, oldTrigrams);\n\n const normalized = tokenize(newText);\n const newTrigrams = normalized.length >= 1 ? generateTrigrams(normalized) : new Set<string>();\n\n record.trigrams.set(field, newTrigrams);\n record.values.set(field, newText);\n addFieldToIndex(item, newTrigrams);\n }\n\n if (changed) notifyMutation();\n },\n\n remove(item: T): void {\n const record = itemData.get(item);\n\n if (!record) return;\n\n for (const fieldTrigrams of record.trigrams.values()) {\n removeFieldFromIndex(item, fieldTrigrams);\n }\n\n itemData.delete(item);\n notifyMutation();\n },\n\n search(query: string, options?: SearchConstraints): SearchResult<T>[] {\n const threshold = options?.threshold ?? defaultThreshold;\n const limit = Math.max(0, options?.limit ?? defaultLimit);\n const minQueryLength = options?.minQueryLength ?? defaultMinQueryLength;\n\n if (!query.trim()) {\n return [...itemData.keys()].slice(0, limit).map((item) => ({ item, matches: [], score: 1 }));\n }\n\n const normalized = tokenize(query);\n\n // Query had no indexable content (e.g. punctuation-only) — no match, not \"match all\".\n if (!normalized) return [];\n\n const isShort = normalized.length < minQueryLength;\n const queryTrigrams = isShort ? null : getQueryTrigrams(normalized);\n const candidates = queryTrigrams === null ? containmentScan(normalized) : trigramCandidates(queryTrigrams);\n const results: SearchResult<T>[] = [];\n\n for (const item of candidates) {\n const record = itemData.get(item);\n\n if (!record) continue;\n\n const score = scoreCandidate(normalized, queryTrigrams, record);\n\n if (score >= threshold) {\n const matches = computeMatches(normalized, record.values);\n\n results.push({ item, matches, score });\n }\n }\n\n return results.sort((a, b) => b.score - a.score).slice(0, limit);\n },\n\n get size(): number {\n return itemData.size;\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"scout-index.cjs","names":[],"sources":["../src/scout-index.ts"],"sourcesContent":["import type { FieldDef, FieldMatch, ScoutIndexOptions, SearchConstraints, SearchResult } from './types';\n\nimport { ScoutIndexError } from './errors';\nimport { findMatchRanges } from './highlight';\nimport { defaultStringify, tokenize } from './tokenize';\nimport { generateTrigrams, overlapSimilarity } from './trigram';\n\ntype FieldConfig<T> = {\n field: keyof T & string;\n stringify: (v: unknown) => string;\n weight: number;\n};\n\ntype ItemRecord = {\n /** Per-field trigrams for scoring. */\n trigrams: Map<string, Set<string>>;\n /** Per-field original text for highlighting. */\n values: Map<string, string>;\n};\n\n/**\n * A stateful, indexed search corpus. Created via `createIndex()`.\n *\n * Supports incremental `add()`, `remove()`, and `reindex()` operations — each patches\n * the trigram index in O(field_length) without a full rebuild.\n */\nexport interface ScoutIndex<T> {\n /** All items currently in the index, in insertion order. */\n readonly items: readonly T[];\n /** Number of items currently in the index. */\n readonly size: number;\n /** Adds `item` to the index. No-op if the item is already indexed (by reference). */\n add(item: T): void;\n /**\n * Re-reads the item's current field values and rebuilds its index entry in-place,\n * only updating fields whose values have changed. Preserves insertion order.\n * No-op if the item is not in the index.\n */\n reindex(item: T): void;\n /**\n * Removes `item` from the index by reference equality.\n * No-op if the item is not in the index.\n */\n remove(item: T): void;\n /**\n * Searches the index for `query` and returns results sorted by score descending.\n *\n * An empty (or whitespace-only) `query` returns all indexed items with `score = 1`.\n * A `query` with no indexable content after normalization (e.g. punctuation-only) returns\n * no results. Results below `threshold` are excluded. At most `limit` results are returned.\n */\n search(query: string, options?: SearchConstraints): SearchResult<T>[];\n /**\n * Subscribes `listener` to be called after every `add()` / `remove()` / `reindex()` call\n * that actually changes the index (no-ops — e.g. removing an unindexed item — don't fire\n * it). Returns an unsubscribe function.\n *\n * Framework-agnostic extension point: `createSearch()` uses this internally to keep\n * reactive `results` in sync with index mutations. Most callers won't need this directly.\n */\n onMutate(listener: () => void): () => void;\n}\n\nfunction resolveFields<T>(defs: ReadonlyArray<FieldDef<T>>): FieldConfig<T>[] {\n return defs.map((def) => {\n if (typeof def === 'string') {\n return { field: def, stringify: defaultStringify, weight: 1 };\n }\n\n return {\n field: def.field,\n stringify: def.stringify ?? defaultStringify,\n weight: def.weight ?? 1,\n };\n });\n}\n\n/**\n * Builds a trigram inverted index over `items` for fast fuzzy search.\n *\n * Construction is O(corpus × field_length). Subsequent `search()` calls are\n * O(candidates) — far faster than per-query Levenshtein for large corpora.\n *\n * @example\n * ```ts\n * const index = createIndex(users, {\n * fields: [{ field: 'name', weight: 2 }, 'email'],\n * threshold: 0.3,\n * limit: 20,\n * });\n *\n * const results = index.search('alice');\n * ```\n *\n * @throws {ScoutIndexError} If `options.fields` is empty.\n */\nexport function createIndex<T>(items: T[], options: ScoutIndexOptions<T>): ScoutIndex<T> {\n if (options.fields.length === 0) {\n throw new ScoutIndexError('createIndex: at least one field is required.');\n }\n\n const fields = resolveFields(options.fields);\n const maxWeight = fields.reduce((max, f) => Math.max(max, f.weight), 1);\n const defaultThreshold = options.threshold ?? 0.2;\n const defaultLimit = options.limit ?? 50;\n const defaultMinQueryLength = options.minQueryLength ?? 3;\n\n /** item → per-item record, preserves insertion order for `items` getter */\n const itemData = new Map<T, ItemRecord>();\n /** trigram → set of items that contain it */\n const invertedIndex = new Map<string, Set<T>>();\n const mutationListeners = new Set<() => void>();\n\n function notifyMutation(): void {\n for (const listener of mutationListeners) listener();\n }\n\n /** Single-entry cache for the most recent normalized query's trigrams (F2). */\n let cachedNormalized: string | null = null;\n let cachedTrigrams: Set<string> | null = null;\n\n function getQueryTrigrams(normalized: string): Set<string> {\n if (normalized === cachedNormalized && cachedTrigrams !== null) return cachedTrigrams;\n\n cachedNormalized = normalized;\n cachedTrigrams = generateTrigrams(normalized);\n\n return cachedTrigrams;\n }\n\n function addFieldToIndex(item: T, fieldTrigrams: Set<string>): void {\n for (const trigram of fieldTrigrams) {\n let bucket = invertedIndex.get(trigram);\n\n if (!bucket) {\n bucket = new Set<T>();\n invertedIndex.set(trigram, bucket);\n }\n\n bucket.add(item);\n }\n }\n\n function removeFieldFromIndex(item: T, fieldTrigrams: Set<string>): void {\n for (const trigram of fieldTrigrams) {\n const bucket = invertedIndex.get(trigram);\n\n if (bucket) {\n bucket.delete(item);\n\n if (bucket.size === 0) invertedIndex.delete(trigram);\n }\n }\n }\n\n function addItem(item: T): void {\n const trigrams = new Map<string, Set<string>>();\n const values = new Map<string, string>();\n\n for (const { field, stringify } of fields) {\n const raw = item[field];\n const text = stringify(raw);\n const normalized = tokenize(text);\n const fieldTrigrams = normalized.length >= 1 ? generateTrigrams(normalized) : new Set<string>();\n\n trigrams.set(field, fieldTrigrams);\n values.set(field, text);\n addFieldToIndex(item, fieldTrigrams);\n }\n\n itemData.set(item, { trigrams, values });\n }\n\n /**\n * Performs a full linear scan over all items for short queries.\n * O(n × field_count) — acceptable for small corpora; consider raising\n * `minQueryLength` on large datasets to avoid triggering this path.\n */\n function containmentScan(query: string): Set<T> {\n const result = new Set<T>();\n\n for (const [item, record] of itemData) {\n for (const value of record.values.values()) {\n if (value.toLowerCase().includes(query)) {\n result.add(item);\n break;\n }\n }\n }\n\n return result;\n }\n\n function trigramCandidates(queryTrigrams: Set<string>): Set<T> {\n const candidates = new Set<T>();\n\n for (const trigram of queryTrigrams) {\n const items = invertedIndex.get(trigram);\n\n if (items) {\n for (const item of items) candidates.add(item);\n }\n }\n\n return candidates;\n }\n\n function scoreCandidate(normalized: string, queryTrigrams: Set<string> | null, record: ItemRecord): number {\n let bestScore = 0;\n\n for (const { field, weight } of fields) {\n let fieldScore: number;\n\n if (queryTrigrams === null) {\n const raw = record.values.get(field) ?? '';\n\n fieldScore = raw.toLowerCase().includes(normalized) ? 1.0 : 0;\n } else {\n const itemTrigrams = record.trigrams.get(field);\n\n if (!itemTrigrams || itemTrigrams.size === 0) continue;\n\n fieldScore = overlapSimilarity(queryTrigrams, itemTrigrams);\n }\n\n const weighted = fieldScore * (weight / maxWeight);\n\n if (weighted > bestScore) bestScore = weighted;\n }\n\n return bestScore;\n }\n\n function computeMatches(query: string, values: Map<string, string>): FieldMatch<keyof T & string>[] {\n const matches: FieldMatch<keyof T & string>[] = [];\n\n for (const { field } of fields) {\n const text = values.get(field);\n\n if (!text) continue;\n\n const ranges = findMatchRanges(text, query);\n\n if (ranges.length > 0) matches.push({ field, ranges });\n }\n\n return matches;\n }\n\n for (const item of items) {\n addItem(item);\n }\n\n return {\n add(item: T): void {\n if (itemData.has(item)) return;\n\n addItem(item);\n notifyMutation();\n },\n\n get items(): readonly T[] {\n return [...itemData.keys()];\n },\n\n onMutate(listener: () => void): () => void {\n mutationListeners.add(listener);\n\n return () => {\n mutationListeners.delete(listener);\n };\n },\n\n reindex(item: T): void {\n const record = itemData.get(item);\n\n if (!record) return;\n\n let changed = false;\n\n for (const { field, stringify } of fields) {\n const raw = item[field];\n const newText = stringify(raw);\n const oldText = record.values.get(field);\n\n if (newText === oldText) continue;\n\n changed = true;\n\n const oldTrigrams = record.trigrams.get(field);\n\n if (oldTrigrams) removeFieldFromIndex(item, oldTrigrams);\n\n const normalized = tokenize(newText);\n const newTrigrams = normalized.length >= 1 ? generateTrigrams(normalized) : new Set<string>();\n\n record.trigrams.set(field, newTrigrams);\n record.values.set(field, newText);\n addFieldToIndex(item, newTrigrams);\n }\n\n if (changed) notifyMutation();\n },\n\n remove(item: T): void {\n const record = itemData.get(item);\n\n if (!record) return;\n\n for (const fieldTrigrams of record.trigrams.values()) {\n removeFieldFromIndex(item, fieldTrigrams);\n }\n\n itemData.delete(item);\n notifyMutation();\n },\n\n search(query: string, options?: SearchConstraints): SearchResult<T>[] {\n const threshold = options?.threshold ?? defaultThreshold;\n const limit = Math.max(0, options?.limit ?? defaultLimit);\n const minQueryLength = options?.minQueryLength ?? defaultMinQueryLength;\n\n if (!query.trim()) {\n return [...itemData.keys()].slice(0, limit).map((item) => ({ item, matches: [], score: 1 }));\n }\n\n const normalized = tokenize(query);\n\n // Query had no indexable content (e.g. punctuation-only) — no match, not \"match all\".\n if (!normalized) return [];\n\n const isShort = normalized.length < minQueryLength;\n const queryTrigrams = isShort ? null : getQueryTrigrams(normalized);\n const candidates = queryTrigrams === null ? containmentScan(normalized) : trigramCandidates(queryTrigrams);\n const results: SearchResult<T>[] = [];\n\n for (const item of candidates) {\n const record = itemData.get(item);\n\n if (!record) continue;\n\n const score = scoreCandidate(normalized, queryTrigrams, record);\n\n if (score >= threshold) {\n const matches = computeMatches(normalized, record.values);\n\n results.push({ item, matches, score });\n }\n }\n\n return results.sort((a, b) => b.score - a.score).slice(0, limit);\n },\n\n get size(): number {\n return itemData.size;\n },\n };\n}\n"],"mappings":"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"}
|
package/dist/scout-index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ScoutIndexError as e } from "./errors.js";
|
|
2
2
|
import { findMatchRanges as t } from "./highlight.js";
|
|
3
3
|
import { defaultStringify as n, tokenize as r } from "./tokenize.js";
|
|
4
|
-
import {
|
|
4
|
+
import { generateTrigrams as i, overlapSimilarity as a } from "./trigram.js";
|
|
5
5
|
//#region src/scout-index.ts
|
|
6
6
|
function o(e) {
|
|
7
7
|
return e.map((e) => typeof e == "string" ? {
|
|
@@ -22,7 +22,7 @@ function s(n, s) {
|
|
|
22
22
|
}
|
|
23
23
|
let _ = null, v = null;
|
|
24
24
|
function y(e) {
|
|
25
|
-
return e === _ && v !== null ? v : (_ = e, v =
|
|
25
|
+
return e === _ && v !== null ? v : (_ = e, v = i(e), v);
|
|
26
26
|
}
|
|
27
27
|
function b(e, t) {
|
|
28
28
|
for (let n of t) {
|
|
@@ -38,9 +38,9 @@ function s(n, s) {
|
|
|
38
38
|
}
|
|
39
39
|
function S(e) {
|
|
40
40
|
let t = /* @__PURE__ */ new Map(), n = /* @__PURE__ */ new Map();
|
|
41
|
-
for (let { field:
|
|
42
|
-
let s = e[
|
|
43
|
-
t.set(
|
|
41
|
+
for (let { field: a, stringify: o } of c) {
|
|
42
|
+
let s = e[a], c = o(s), l = r(c), u = l.length >= 1 ? i(l) : /* @__PURE__ */ new Set();
|
|
43
|
+
t.set(a, u), n.set(a, c), b(e, u);
|
|
44
44
|
}
|
|
45
45
|
p.set(e, {
|
|
46
46
|
trigrams: t,
|
|
@@ -65,13 +65,13 @@ function s(n, s) {
|
|
|
65
65
|
}
|
|
66
66
|
function T(e, t, n) {
|
|
67
67
|
let r = 0;
|
|
68
|
-
for (let { field:
|
|
68
|
+
for (let { field: i, weight: o } of c) {
|
|
69
69
|
let s;
|
|
70
|
-
if (t === null) s = +!!(n.values.get(
|
|
70
|
+
if (t === null) s = +!!(n.values.get(i) ?? "").toLowerCase().includes(e);
|
|
71
71
|
else {
|
|
72
|
-
let e = n.trigrams.get(
|
|
72
|
+
let e = n.trigrams.get(i);
|
|
73
73
|
if (!e || e.size === 0) continue;
|
|
74
|
-
s =
|
|
74
|
+
s = a(t, e);
|
|
75
75
|
}
|
|
76
76
|
let c = o / l * s;
|
|
77
77
|
c > r && (r = c);
|
|
@@ -108,14 +108,14 @@ function s(n, s) {
|
|
|
108
108
|
let t = p.get(e);
|
|
109
109
|
if (!t) return;
|
|
110
110
|
let n = !1;
|
|
111
|
-
for (let { field:
|
|
112
|
-
let s = e[
|
|
113
|
-
if (c === t.values.get(
|
|
111
|
+
for (let { field: a, stringify: o } of c) {
|
|
112
|
+
let s = e[a], c = o(s);
|
|
113
|
+
if (c === t.values.get(a)) continue;
|
|
114
114
|
n = !0;
|
|
115
|
-
let l = t.trigrams.get(
|
|
115
|
+
let l = t.trigrams.get(a);
|
|
116
116
|
l && x(e, l);
|
|
117
|
-
let u = r(c), d = u.length >= 1 ?
|
|
118
|
-
t.trigrams.set(
|
|
117
|
+
let u = r(c), d = u.length >= 1 ? i(u) : /* @__PURE__ */ new Set();
|
|
118
|
+
t.trigrams.set(a, d), t.values.set(a, c), b(e, d);
|
|
119
119
|
}
|
|
120
120
|
n && g();
|
|
121
121
|
},
|
package/dist/scout-index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scout-index.js","names":[],"sources":["../src/scout-index.ts"],"sourcesContent":["import type { FieldDef, FieldMatch, ScoutIndexOptions, SearchConstraints, SearchResult } from './types';\n\nimport { ScoutIndexError } from './errors';\nimport { findMatchRanges } from './highlight';\nimport { defaultStringify, tokenize } from './tokenize';\nimport { diceSimilarity, generateTrigrams } from './trigram';\n\ntype FieldConfig<T> = {\n field: keyof T & string;\n stringify: (v: unknown) => string;\n weight: number;\n};\n\ntype ItemRecord = {\n /** Per-field trigrams for scoring. */\n trigrams: Map<string, Set<string>>;\n /** Per-field original text for highlighting. */\n values: Map<string, string>;\n};\n\n/**\n * A stateful, indexed search corpus. Created via `createIndex()`.\n *\n * Supports incremental `add()`, `remove()`, and `reindex()` operations — each patches\n * the trigram index in O(field_length) without a full rebuild.\n */\nexport interface ScoutIndex<T> {\n /** All items currently in the index, in insertion order. */\n readonly items: readonly T[];\n /** Number of items currently in the index. */\n readonly size: number;\n /** Adds `item` to the index. No-op if the item is already indexed (by reference). */\n add(item: T): void;\n /**\n * Re-reads the item's current field values and rebuilds its index entry in-place,\n * only updating fields whose values have changed. Preserves insertion order.\n * No-op if the item is not in the index.\n */\n reindex(item: T): void;\n /**\n * Removes `item` from the index by reference equality.\n * No-op if the item is not in the index.\n */\n remove(item: T): void;\n /**\n * Searches the index for `query` and returns results sorted by score descending.\n *\n * An empty (or whitespace-only) `query` returns all indexed items with `score = 1`.\n * A `query` with no indexable content after normalization (e.g. punctuation-only) returns\n * no results. Results below `threshold` are excluded. At most `limit` results are returned.\n */\n search(query: string, options?: SearchConstraints): SearchResult<T>[];\n /**\n * Subscribes `listener` to be called after every `add()` / `remove()` / `reindex()` call\n * that actually changes the index (no-ops — e.g. removing an unindexed item — don't fire\n * it). Returns an unsubscribe function.\n *\n * Framework-agnostic extension point: `createSearch()` uses this internally to keep\n * reactive `results` in sync with index mutations. Most callers won't need this directly.\n */\n onMutate(listener: () => void): () => void;\n}\n\nfunction resolveFields<T>(defs: ReadonlyArray<FieldDef<T>>): FieldConfig<T>[] {\n return defs.map((def) => {\n if (typeof def === 'string') {\n return { field: def, stringify: defaultStringify, weight: 1 };\n }\n\n return {\n field: def.field,\n stringify: def.stringify ?? defaultStringify,\n weight: def.weight ?? 1,\n };\n });\n}\n\n/**\n * Builds a trigram inverted index over `items` for fast fuzzy search.\n *\n * Construction is O(corpus × field_length). Subsequent `search()` calls are\n * O(candidates) — far faster than per-query Levenshtein for large corpora.\n *\n * @example\n * ```ts\n * const index = createIndex(users, {\n * fields: [{ field: 'name', weight: 2 }, 'email'],\n * threshold: 0.3,\n * limit: 20,\n * });\n *\n * const results = index.search('alice');\n * ```\n *\n * @throws {ScoutIndexError} If `options.fields` is empty.\n */\nexport function createIndex<T>(items: T[], options: ScoutIndexOptions<T>): ScoutIndex<T> {\n if (options.fields.length === 0) {\n throw new ScoutIndexError('createIndex: at least one field is required.');\n }\n\n const fields = resolveFields(options.fields);\n const maxWeight = fields.reduce((max, f) => Math.max(max, f.weight), 1);\n const defaultThreshold = options.threshold ?? 0.2;\n const defaultLimit = options.limit ?? 50;\n const defaultMinQueryLength = options.minQueryLength ?? 3;\n\n /** item → per-item record, preserves insertion order for `items` getter */\n const itemData = new Map<T, ItemRecord>();\n /** trigram → set of items that contain it */\n const invertedIndex = new Map<string, Set<T>>();\n const mutationListeners = new Set<() => void>();\n\n function notifyMutation(): void {\n for (const listener of mutationListeners) listener();\n }\n\n /** Single-entry cache for the most recent normalized query's trigrams (F2). */\n let cachedNormalized: string | null = null;\n let cachedTrigrams: Set<string> | null = null;\n\n function getQueryTrigrams(normalized: string): Set<string> {\n if (normalized === cachedNormalized && cachedTrigrams !== null) return cachedTrigrams;\n\n cachedNormalized = normalized;\n cachedTrigrams = generateTrigrams(normalized);\n\n return cachedTrigrams;\n }\n\n function addFieldToIndex(item: T, fieldTrigrams: Set<string>): void {\n for (const trigram of fieldTrigrams) {\n let bucket = invertedIndex.get(trigram);\n\n if (!bucket) {\n bucket = new Set<T>();\n invertedIndex.set(trigram, bucket);\n }\n\n bucket.add(item);\n }\n }\n\n function removeFieldFromIndex(item: T, fieldTrigrams: Set<string>): void {\n for (const trigram of fieldTrigrams) {\n const bucket = invertedIndex.get(trigram);\n\n if (bucket) {\n bucket.delete(item);\n\n if (bucket.size === 0) invertedIndex.delete(trigram);\n }\n }\n }\n\n function addItem(item: T): void {\n const trigrams = new Map<string, Set<string>>();\n const values = new Map<string, string>();\n\n for (const { field, stringify } of fields) {\n const raw = item[field];\n const text = stringify(raw);\n const normalized = tokenize(text);\n const fieldTrigrams = normalized.length >= 1 ? generateTrigrams(normalized) : new Set<string>();\n\n trigrams.set(field, fieldTrigrams);\n values.set(field, text);\n addFieldToIndex(item, fieldTrigrams);\n }\n\n itemData.set(item, { trigrams, values });\n }\n\n /**\n * Performs a full linear scan over all items for short queries.\n * O(n × field_count) — acceptable for small corpora; consider raising\n * `minQueryLength` on large datasets to avoid triggering this path.\n */\n function containmentScan(query: string): Set<T> {\n const result = new Set<T>();\n\n for (const [item, record] of itemData) {\n for (const value of record.values.values()) {\n if (value.toLowerCase().includes(query)) {\n result.add(item);\n break;\n }\n }\n }\n\n return result;\n }\n\n function trigramCandidates(queryTrigrams: Set<string>): Set<T> {\n const candidates = new Set<T>();\n\n for (const trigram of queryTrigrams) {\n const items = invertedIndex.get(trigram);\n\n if (items) {\n for (const item of items) candidates.add(item);\n }\n }\n\n return candidates;\n }\n\n function scoreCandidate(normalized: string, queryTrigrams: Set<string> | null, record: ItemRecord): number {\n let bestScore = 0;\n\n for (const { field, weight } of fields) {\n let fieldScore: number;\n\n if (queryTrigrams === null) {\n const raw = record.values.get(field) ?? '';\n\n fieldScore = raw.toLowerCase().includes(normalized) ? 1.0 : 0;\n } else {\n const itemTrigrams = record.trigrams.get(field);\n\n if (!itemTrigrams || itemTrigrams.size === 0) continue;\n\n fieldScore = diceSimilarity(queryTrigrams, itemTrigrams);\n }\n\n const weighted = fieldScore * (weight / maxWeight);\n\n if (weighted > bestScore) bestScore = weighted;\n }\n\n return bestScore;\n }\n\n function computeMatches(query: string, values: Map<string, string>): FieldMatch<keyof T & string>[] {\n const matches: FieldMatch<keyof T & string>[] = [];\n\n for (const { field } of fields) {\n const text = values.get(field);\n\n if (!text) continue;\n\n const ranges = findMatchRanges(text, query);\n\n if (ranges.length > 0) matches.push({ field, ranges });\n }\n\n return matches;\n }\n\n for (const item of items) {\n addItem(item);\n }\n\n return {\n add(item: T): void {\n if (itemData.has(item)) return;\n\n addItem(item);\n notifyMutation();\n },\n\n get items(): readonly T[] {\n return [...itemData.keys()];\n },\n\n onMutate(listener: () => void): () => void {\n mutationListeners.add(listener);\n\n return () => {\n mutationListeners.delete(listener);\n };\n },\n\n reindex(item: T): void {\n const record = itemData.get(item);\n\n if (!record) return;\n\n let changed = false;\n\n for (const { field, stringify } of fields) {\n const raw = item[field];\n const newText = stringify(raw);\n const oldText = record.values.get(field);\n\n if (newText === oldText) continue;\n\n changed = true;\n\n const oldTrigrams = record.trigrams.get(field);\n\n if (oldTrigrams) removeFieldFromIndex(item, oldTrigrams);\n\n const normalized = tokenize(newText);\n const newTrigrams = normalized.length >= 1 ? generateTrigrams(normalized) : new Set<string>();\n\n record.trigrams.set(field, newTrigrams);\n record.values.set(field, newText);\n addFieldToIndex(item, newTrigrams);\n }\n\n if (changed) notifyMutation();\n },\n\n remove(item: T): void {\n const record = itemData.get(item);\n\n if (!record) return;\n\n for (const fieldTrigrams of record.trigrams.values()) {\n removeFieldFromIndex(item, fieldTrigrams);\n }\n\n itemData.delete(item);\n notifyMutation();\n },\n\n search(query: string, options?: SearchConstraints): SearchResult<T>[] {\n const threshold = options?.threshold ?? defaultThreshold;\n const limit = Math.max(0, options?.limit ?? defaultLimit);\n const minQueryLength = options?.minQueryLength ?? defaultMinQueryLength;\n\n if (!query.trim()) {\n return [...itemData.keys()].slice(0, limit).map((item) => ({ item, matches: [], score: 1 }));\n }\n\n const normalized = tokenize(query);\n\n // Query had no indexable content (e.g. punctuation-only) — no match, not \"match all\".\n if (!normalized) return [];\n\n const isShort = normalized.length < minQueryLength;\n const queryTrigrams = isShort ? null : getQueryTrigrams(normalized);\n const candidates = queryTrigrams === null ? containmentScan(normalized) : trigramCandidates(queryTrigrams);\n const results: SearchResult<T>[] = [];\n\n for (const item of candidates) {\n const record = itemData.get(item);\n\n if (!record) continue;\n\n const score = scoreCandidate(normalized, queryTrigrams, record);\n\n if (score >= threshold) {\n const matches = computeMatches(normalized, record.values);\n\n results.push({ item, matches, score });\n }\n }\n\n return results.sort((a, b) => b.score - a.score).slice(0, limit);\n },\n\n get size(): number {\n return itemData.size;\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"scout-index.js","names":[],"sources":["../src/scout-index.ts"],"sourcesContent":["import type { FieldDef, FieldMatch, ScoutIndexOptions, SearchConstraints, SearchResult } from './types';\n\nimport { ScoutIndexError } from './errors';\nimport { findMatchRanges } from './highlight';\nimport { defaultStringify, tokenize } from './tokenize';\nimport { generateTrigrams, overlapSimilarity } from './trigram';\n\ntype FieldConfig<T> = {\n field: keyof T & string;\n stringify: (v: unknown) => string;\n weight: number;\n};\n\ntype ItemRecord = {\n /** Per-field trigrams for scoring. */\n trigrams: Map<string, Set<string>>;\n /** Per-field original text for highlighting. */\n values: Map<string, string>;\n};\n\n/**\n * A stateful, indexed search corpus. Created via `createIndex()`.\n *\n * Supports incremental `add()`, `remove()`, and `reindex()` operations — each patches\n * the trigram index in O(field_length) without a full rebuild.\n */\nexport interface ScoutIndex<T> {\n /** All items currently in the index, in insertion order. */\n readonly items: readonly T[];\n /** Number of items currently in the index. */\n readonly size: number;\n /** Adds `item` to the index. No-op if the item is already indexed (by reference). */\n add(item: T): void;\n /**\n * Re-reads the item's current field values and rebuilds its index entry in-place,\n * only updating fields whose values have changed. Preserves insertion order.\n * No-op if the item is not in the index.\n */\n reindex(item: T): void;\n /**\n * Removes `item` from the index by reference equality.\n * No-op if the item is not in the index.\n */\n remove(item: T): void;\n /**\n * Searches the index for `query` and returns results sorted by score descending.\n *\n * An empty (or whitespace-only) `query` returns all indexed items with `score = 1`.\n * A `query` with no indexable content after normalization (e.g. punctuation-only) returns\n * no results. Results below `threshold` are excluded. At most `limit` results are returned.\n */\n search(query: string, options?: SearchConstraints): SearchResult<T>[];\n /**\n * Subscribes `listener` to be called after every `add()` / `remove()` / `reindex()` call\n * that actually changes the index (no-ops — e.g. removing an unindexed item — don't fire\n * it). Returns an unsubscribe function.\n *\n * Framework-agnostic extension point: `createSearch()` uses this internally to keep\n * reactive `results` in sync with index mutations. Most callers won't need this directly.\n */\n onMutate(listener: () => void): () => void;\n}\n\nfunction resolveFields<T>(defs: ReadonlyArray<FieldDef<T>>): FieldConfig<T>[] {\n return defs.map((def) => {\n if (typeof def === 'string') {\n return { field: def, stringify: defaultStringify, weight: 1 };\n }\n\n return {\n field: def.field,\n stringify: def.stringify ?? defaultStringify,\n weight: def.weight ?? 1,\n };\n });\n}\n\n/**\n * Builds a trigram inverted index over `items` for fast fuzzy search.\n *\n * Construction is O(corpus × field_length). Subsequent `search()` calls are\n * O(candidates) — far faster than per-query Levenshtein for large corpora.\n *\n * @example\n * ```ts\n * const index = createIndex(users, {\n * fields: [{ field: 'name', weight: 2 }, 'email'],\n * threshold: 0.3,\n * limit: 20,\n * });\n *\n * const results = index.search('alice');\n * ```\n *\n * @throws {ScoutIndexError} If `options.fields` is empty.\n */\nexport function createIndex<T>(items: T[], options: ScoutIndexOptions<T>): ScoutIndex<T> {\n if (options.fields.length === 0) {\n throw new ScoutIndexError('createIndex: at least one field is required.');\n }\n\n const fields = resolveFields(options.fields);\n const maxWeight = fields.reduce((max, f) => Math.max(max, f.weight), 1);\n const defaultThreshold = options.threshold ?? 0.2;\n const defaultLimit = options.limit ?? 50;\n const defaultMinQueryLength = options.minQueryLength ?? 3;\n\n /** item → per-item record, preserves insertion order for `items` getter */\n const itemData = new Map<T, ItemRecord>();\n /** trigram → set of items that contain it */\n const invertedIndex = new Map<string, Set<T>>();\n const mutationListeners = new Set<() => void>();\n\n function notifyMutation(): void {\n for (const listener of mutationListeners) listener();\n }\n\n /** Single-entry cache for the most recent normalized query's trigrams (F2). */\n let cachedNormalized: string | null = null;\n let cachedTrigrams: Set<string> | null = null;\n\n function getQueryTrigrams(normalized: string): Set<string> {\n if (normalized === cachedNormalized && cachedTrigrams !== null) return cachedTrigrams;\n\n cachedNormalized = normalized;\n cachedTrigrams = generateTrigrams(normalized);\n\n return cachedTrigrams;\n }\n\n function addFieldToIndex(item: T, fieldTrigrams: Set<string>): void {\n for (const trigram of fieldTrigrams) {\n let bucket = invertedIndex.get(trigram);\n\n if (!bucket) {\n bucket = new Set<T>();\n invertedIndex.set(trigram, bucket);\n }\n\n bucket.add(item);\n }\n }\n\n function removeFieldFromIndex(item: T, fieldTrigrams: Set<string>): void {\n for (const trigram of fieldTrigrams) {\n const bucket = invertedIndex.get(trigram);\n\n if (bucket) {\n bucket.delete(item);\n\n if (bucket.size === 0) invertedIndex.delete(trigram);\n }\n }\n }\n\n function addItem(item: T): void {\n const trigrams = new Map<string, Set<string>>();\n const values = new Map<string, string>();\n\n for (const { field, stringify } of fields) {\n const raw = item[field];\n const text = stringify(raw);\n const normalized = tokenize(text);\n const fieldTrigrams = normalized.length >= 1 ? generateTrigrams(normalized) : new Set<string>();\n\n trigrams.set(field, fieldTrigrams);\n values.set(field, text);\n addFieldToIndex(item, fieldTrigrams);\n }\n\n itemData.set(item, { trigrams, values });\n }\n\n /**\n * Performs a full linear scan over all items for short queries.\n * O(n × field_count) — acceptable for small corpora; consider raising\n * `minQueryLength` on large datasets to avoid triggering this path.\n */\n function containmentScan(query: string): Set<T> {\n const result = new Set<T>();\n\n for (const [item, record] of itemData) {\n for (const value of record.values.values()) {\n if (value.toLowerCase().includes(query)) {\n result.add(item);\n break;\n }\n }\n }\n\n return result;\n }\n\n function trigramCandidates(queryTrigrams: Set<string>): Set<T> {\n const candidates = new Set<T>();\n\n for (const trigram of queryTrigrams) {\n const items = invertedIndex.get(trigram);\n\n if (items) {\n for (const item of items) candidates.add(item);\n }\n }\n\n return candidates;\n }\n\n function scoreCandidate(normalized: string, queryTrigrams: Set<string> | null, record: ItemRecord): number {\n let bestScore = 0;\n\n for (const { field, weight } of fields) {\n let fieldScore: number;\n\n if (queryTrigrams === null) {\n const raw = record.values.get(field) ?? '';\n\n fieldScore = raw.toLowerCase().includes(normalized) ? 1.0 : 0;\n } else {\n const itemTrigrams = record.trigrams.get(field);\n\n if (!itemTrigrams || itemTrigrams.size === 0) continue;\n\n fieldScore = overlapSimilarity(queryTrigrams, itemTrigrams);\n }\n\n const weighted = fieldScore * (weight / maxWeight);\n\n if (weighted > bestScore) bestScore = weighted;\n }\n\n return bestScore;\n }\n\n function computeMatches(query: string, values: Map<string, string>): FieldMatch<keyof T & string>[] {\n const matches: FieldMatch<keyof T & string>[] = [];\n\n for (const { field } of fields) {\n const text = values.get(field);\n\n if (!text) continue;\n\n const ranges = findMatchRanges(text, query);\n\n if (ranges.length > 0) matches.push({ field, ranges });\n }\n\n return matches;\n }\n\n for (const item of items) {\n addItem(item);\n }\n\n return {\n add(item: T): void {\n if (itemData.has(item)) return;\n\n addItem(item);\n notifyMutation();\n },\n\n get items(): readonly T[] {\n return [...itemData.keys()];\n },\n\n onMutate(listener: () => void): () => void {\n mutationListeners.add(listener);\n\n return () => {\n mutationListeners.delete(listener);\n };\n },\n\n reindex(item: T): void {\n const record = itemData.get(item);\n\n if (!record) return;\n\n let changed = false;\n\n for (const { field, stringify } of fields) {\n const raw = item[field];\n const newText = stringify(raw);\n const oldText = record.values.get(field);\n\n if (newText === oldText) continue;\n\n changed = true;\n\n const oldTrigrams = record.trigrams.get(field);\n\n if (oldTrigrams) removeFieldFromIndex(item, oldTrigrams);\n\n const normalized = tokenize(newText);\n const newTrigrams = normalized.length >= 1 ? generateTrigrams(normalized) : new Set<string>();\n\n record.trigrams.set(field, newTrigrams);\n record.values.set(field, newText);\n addFieldToIndex(item, newTrigrams);\n }\n\n if (changed) notifyMutation();\n },\n\n remove(item: T): void {\n const record = itemData.get(item);\n\n if (!record) return;\n\n for (const fieldTrigrams of record.trigrams.values()) {\n removeFieldFromIndex(item, fieldTrigrams);\n }\n\n itemData.delete(item);\n notifyMutation();\n },\n\n search(query: string, options?: SearchConstraints): SearchResult<T>[] {\n const threshold = options?.threshold ?? defaultThreshold;\n const limit = Math.max(0, options?.limit ?? defaultLimit);\n const minQueryLength = options?.minQueryLength ?? defaultMinQueryLength;\n\n if (!query.trim()) {\n return [...itemData.keys()].slice(0, limit).map((item) => ({ item, matches: [], score: 1 }));\n }\n\n const normalized = tokenize(query);\n\n // Query had no indexable content (e.g. punctuation-only) — no match, not \"match all\".\n if (!normalized) return [];\n\n const isShort = normalized.length < minQueryLength;\n const queryTrigrams = isShort ? null : getQueryTrigrams(normalized);\n const candidates = queryTrigrams === null ? containmentScan(normalized) : trigramCandidates(queryTrigrams);\n const results: SearchResult<T>[] = [];\n\n for (const item of candidates) {\n const record = itemData.get(item);\n\n if (!record) continue;\n\n const score = scoreCandidate(normalized, queryTrigrams, record);\n\n if (score >= threshold) {\n const matches = computeMatches(normalized, record.values);\n\n results.push({ item, matches, score });\n }\n }\n\n return results.sort((a, b) => b.score - a.score).slice(0, limit);\n },\n\n get size(): number {\n return itemData.size;\n },\n };\n}\n"],"mappings":"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"}
|
package/dist/scout.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});function e(e,t){return(n,r)=>e.search(r,t).map(e=>e.item)}function t(e,t,n){let r=new Set(e.search(t,n).map(e=>e.item));return e=>r.has(e)}var n=class e extends Error{constructor(e,t){super(e,t),this.name=new.target.name,Object.setPrototypeOf(this,new.target.prototype)}static is(t){return t instanceof e}},r=class extends n{},i=class extends n{};function a(e,t){let n=e.toLowerCase(),r=t.split(` `).filter(Boolean),i=[];for(let e of r){let t=0;for(;t<n.length;){let r=n.indexOf(e,t);if(r===-1)break;i.push([r,r+e.length]),t=r+1}}i.sort((e,t)=>e[0]-t[0]);let a=[];for(let e of i){let t=a[a.length-1];t&&e[0]<=t[1]?t[1]=Math.max(t[1],e[1]):a.push([e[0],e[1]])}return a}function o(e,t){if(!e)return[];if(!t.length)return[{highlighted:!1,text:e}];let n=[],r=0;for(let[i,a]of t)i>r&&n.push({highlighted:!1,text:e.slice(r,i)}),a>i&&n.push({highlighted:!0,text:e.slice(i,a)}),r=a;return r<e.length&&n.push({highlighted:!1,text:e.slice(r)}),n}function s(e,t,n){return o(n,e.matches.find(e=>e.field===t)?.ranges??[])}var c=null,l=()=>c,u=class e extends Error{constructor(e,t){super(e,t),this.name=new.target.name,Object.setPrototypeOf(this,new.target.prototype)}static is(t){return t instanceof e}},d=class extends u{},f=class extends u{},p=0,m=()=>++p,h=()=>p,g={scheduling:{activeDirty:`a`,batchDepth:0,dirtyWithEffectSubsA:new Set,dirtyWithEffectSubsB:new Set,pendingSubscribers:new Set},scopeCleanups:null,tracking:null},_=null,v=()=>_===null?g:_.get(),y=()=>v().scheduling,b=()=>_!==null,x=()=>v().tracking,S=(e,t)=>{if(_!==null){let n=_.get();return _.run({...n,tracking:e},t)}let n=g;g={...g,tracking:e};try{return t()}finally{g=n}},C=()=>v().scopeCleanups,w=e=>{let t=x();t?.kind===`effect`?t.cleanups.push(e):C()?.push(e)},T=e=>{let t=x();if(t!==null){if(t.sourceObserver?.(e),t.kind===`computed`)t.depCollector.push({source:e,version:e.version});else if(t.kind===`effect`){let n=t.effect;e.addEffectSub(n),t.subscriptions.add(()=>e.removeEffectSub(n)),t.deps.set(e,e.version)}}},E=e=>e instanceof Error?e:Error(`Non-Error thrown: ${String(e)}`),D=e=>{let t=[];for(let n of e)try{n()}catch(e){t.push(E(e))}return t},ee=(e,t)=>{let n=D(e);if(n.length===1)throw n[0];if(n.length>0)throw AggregateError(n,t)},te=!globalThis.__RIPPLE_PROD__;function O(e){te&&console.warn(`[@vielzeug/ripple] ${e}`)}var k=!1,A=0,j=[],M=e=>e.activeDirty===`a`?e.dirtyWithEffectSubsA:e.dirtyWithEffectSubsB,ne=(e,t)=>{let n=++A;for(let n of t.effectSubs())e.pendingSubscribers.add(n);for(let e of t.computedSubs())j.push(e);try{for(;j.length>0;){let t=j.pop();if(t.lastPropEpoch_!==n&&(t.lastPropEpoch_=n,t.markDirty())){t.effectSubs().size>0&&M(e).add(t);for(let e of t.computedSubs())j.push(e)}}}finally{j.length=0}},re=e=>{for(;M(e).size>0;){let t=M(e);e.activeDirty=e.activeDirty===`a`?`b`:`a`,M(e).clear();for(let n of t)if(n.hasSubscribers()&&n.refreshIfDirty()){for(let t of n.effectSubs())e.pendingSubscribers.add(t);for(let t of n.computedSubs())t.markDirty()&&t.effectSubs().size>0&&M(e).add(t)}t.clear()}},N=e=>{let t=0;for(;e.pendingSubscribers.size>0||M(e).size>0;){if(++t>100)throw new f(`infinite flush loop (> 100 iterations)`);if(M(e).size>0&&re(e),e.pendingSubscribers.size===0)continue;let n=[...e.pendingSubscribers];e.pendingSubscribers.clear(),ee(n,`subscriber errors`)}},ie=globalThis.process?.versions!=null,ae=e=>{if(!e.hasSubscribers())return;!k&&ie&&!b()&&(k=!0,O(`Signal updated in a Node.js-like environment. The module-level flush queue is shared across concurrent requests — use per-request worker isolation or the @vielzeug/ripple/ssr sub-path for request-isolated scheduling.`));let t=y();ne(t,e),t.batchDepth===0&&N(t)},oe=e=>{let t=y();t.batchDepth++;let n;try{n=e()}catch(e){throw t.batchDepth--,t.batchDepth===0&&(t.pendingSubscribers.clear(),t.dirtyWithEffectSubsA.clear(),t.dirtyWithEffectSubsB.clear()),e}return t.batchDepth--,t.batchDepth===0&&N(t),n},P=class{fn_;disposed_=!1;constructor(e){this.fn_=e}get disposed(){return this.disposed_}dispose(){if(this.disposed_)return;this.disposed_=!0;let e=this.fn_;this.fn_=null,e()}[Symbol.dispose](){this.dispose()}},F=Symbol(`ripple.is-signal`),I=Symbol(`ripple.is-computed`),L=Symbol(`ripple.uninitialized`),R=new FinalizationRegistry(({key:e,map:t})=>{t.delete(e)}),z=class{version=0;name;[F]=!0;computedSubs_=new Map;effectSubs_=new Set;constructor(e){this.name=e}addComputedSub(e){let t=new WeakRef(e);this.computedSubs_.set(e,t),R.register(e,{key:e,map:this.computedSubs_},t)}removeComputedSub(e){let t=this.computedSubs_.get(e);t!==void 0&&(this.computedSubs_.delete(e),R.unregister(t))}addEffectSub(e){this.effectSubs_.add(e)}removeEffectSub(e){this.effectSubs_.delete(e)}clearSubscribers(){for(let e of this.computedSubs_.values())R.unregister(e);this.computedSubs_.clear(),this.effectSubs_.clear()}hasSubscribers(){if(this.effectSubs_.size>0)return!0;for(let e of this.computedSubs_.values())if(e.deref()!==void 0)return!0;return!1}*computedSubs(){for(let[e,t]of this.computedSubs_){let n=t.deref();n===void 0?(this.computedSubs_.delete(e),R.unregister(t)):yield n}}effectSubs(){return this.effectSubs_}},B=class extends z{[I]=!0;lastPropEpoch_=0},V=class extends B{value_;dirty_;computing_;disposed_;deps_;compute_;equals_;maxRevision_;constructor(e,t){let{equals:n,name:r}=t??{};super(r),this.value_=L,this.dirty_=!0,this.computing_=!1,this.disposed_=!1,this.deps_=[],this.maxRevision_=-1,this.compute_=e,this.equals_=n===void 0?Object.is:(e,t)=>n(e,t)}markDirty(){return this.disposed_||this.dirty_?!1:(this.dirty_=!0,!0)}refreshIfDirty(){if(!this.dirty_)return!1;if(h()<=this.maxRevision_)return this.dirty_=!1,!1;if(this.deps_.length>0){let e=!0;for(let t of this.deps_){let n=t.source;if(`refreshIfDirty`in n&&n.refreshIfDirty(),n.version!==t.version){e=!1;break}}if(e)return this.dirty_=!1,this.maxRevision_=h(),!1}return this.recompute()}runCompute(e){try{return S({computed:this,depCollector:e,kind:`computed`},this.compute_)}catch(e){throw E(e)}}recompute(){if(this.computing_)throw new d(`computed cycle detected${this.name?` "${this.name}"`:``}`);this.computing_=!0;try{let e=[];l()?.compute?.({name:this.name});let t=this.runCompute(e);return this.dirty_=!1,this.maxRevision_=h(),this.updateDeps(e),this.value_===L||!this.equals_(this.value_,t)?(this.value_=t,this.version++,!0):!1}finally{this.computing_=!1}}updateDeps(e){let t=this.deps_;if(t.length===e.length&&t.every((t,n)=>t.source===e[n].source)){for(let n=0;n<e.length;n++)t[n].version=e[n].version;return}let n=new Set(t.map(e=>e.source)),r=new Set(e.map(e=>e.source));for(let e of t)r.has(e.source)||e.source.removeComputedSub(this);for(let t of e)n.has(t.source)||t.source.addComputedSub(this);this.deps_=e}get value(){return this.disposed_?this.value_===L?void 0:this.value_:(this.refreshIfDirty(),T(this),this.value_)}peek(){return this.disposed_?this.value_===L?void 0:this.value_:(this.refreshIfDirty(),this.value_)}subscribe=e=>{if(this.disposed_){let e=new P(()=>{});return e.dispose(),e}return this.refreshIfDirty(),this.addEffectSub(e),new P(()=>{this.removeEffectSub(e)})};get disposed(){return this.disposed_}dispose(){if(!this.disposed_){this.disposed_=!0;for(let e of this.deps_)e.source.removeComputedSub(this);this.deps_=[],this.clearSubscribers(),l()?.dispose?.({kind:`computed`,name:this.name})}}[Symbol.dispose](){this.dispose()}},H=(e,t)=>{let n=new V(e,t);return w(()=>n.dispose()),n},U=class extends z{value_;equals_;disposed_;constructor(e,t,n){super(n),this.value_=e,this.equals_=t??Object.is,this.disposed_=!1}get value(){return this.disposed_||T(this),this.value_}set value(e){if(this.disposed_||this.equals_(this.value_,e))return;let t=this.value_;this.value_=e,this.version=m(),l()?.write?.({name:this.name,newValue:e,oldValue:t}),ae(this)}peek(){return this.value_}subscribe=e=>{if(this.disposed_){let e=new P(()=>{});return e.dispose(),e}return this.addEffectSub(e),new P(()=>{this.removeEffectSub(e)})};get disposed(){return this.disposed_}dispose(){this.disposed_||(this.disposed_=!0,this.clearSubscribers(),l()?.dispose?.({kind:`signal`,name:this.name}))}[Symbol.dispose](){this.dispose()}},W=(e,t)=>new U(e,t?.equals,t?.name),G=!globalThis.__SCOUT_PROD__;function K(e){G&&console.warn(`[@vielzeug/scout] ${e}`)}function q(e){return e.toLowerCase().replace(/[^\p{L}\p{N}\s']/gu,` `).trim().replace(/\s+/g,` `)}function J(e){return e==null?``:typeof e==`string`?e:typeof e==`number`||typeof e==`boolean`?String(e):Array.isArray(e)?(K("defaultStringify: received an array value — provide a custom `stringify` on the FieldDef to control array indexing.\nFalling back to joining string/number/boolean elements with a space."),e.filter(e=>typeof e==`string`||typeof e==`number`||typeof e==`boolean`).join(` `)):``}function Y(e){let t=new Set,n=` ${e} `;for(let e=0;e<n.length-2;e++)t.add(n.slice(e,e+3));return t}function se(e,t){if(e.size===0||t.size===0)return 0;let n=0;for(let r of e)t.has(r)&&n++;return 2*n/(e.size+t.size)}function ce(e){return e.map(e=>typeof e==`string`?{field:e,stringify:J,weight:1}:{field:e.field,stringify:e.stringify??J,weight:e.weight??1})}function X(e,t){if(t.fields.length===0)throw new i(`createIndex: at least one field is required.`);let n=ce(t.fields),r=n.reduce((e,t)=>Math.max(e,t.weight),1),o=t.threshold??.2,s=t.limit??50,c=t.minQueryLength??3,l=new Map,u=new Map,d=new Set;function f(){for(let e of d)e()}let p=null,m=null;function h(e){return e===p&&m!==null?m:(p=e,m=Y(e),m)}function g(e,t){for(let n of t){let t=u.get(n);t||(t=new Set,u.set(n,t)),t.add(e)}}function _(e,t){for(let n of t){let t=u.get(n);t&&(t.delete(e),t.size===0&&u.delete(n))}}function v(e){let t=new Map,r=new Map;for(let{field:i,stringify:a}of n){let n=e[i],o=a(n),s=q(o),c=s.length>=1?Y(s):new Set;t.set(i,c),r.set(i,o),g(e,c)}l.set(e,{trigrams:t,values:r})}function y(e){let t=new Set;for(let[n,r]of l)for(let i of r.values.values())if(i.toLowerCase().includes(e)){t.add(n);break}return t}function b(e){let t=new Set;for(let n of e){let e=u.get(n);if(e)for(let n of e)t.add(n)}return t}function x(e,t,i){let a=0;for(let{field:o,weight:s}of n){let n;if(t===null)n=+!!(i.values.get(o)??``).toLowerCase().includes(e);else{let e=i.trigrams.get(o);if(!e||e.size===0)continue;n=se(t,e)}let c=s/r*n;c>a&&(a=c)}return a}function S(e,t){let r=[];for(let{field:i}of n){let n=t.get(i);if(!n)continue;let o=a(n,e);o.length>0&&r.push({field:i,ranges:o})}return r}for(let t of e)v(t);return{add(e){l.has(e)||(v(e),f())},get items(){return[...l.keys()]},onMutate(e){return d.add(e),()=>{d.delete(e)}},reindex(e){let t=l.get(e);if(!t)return;let r=!1;for(let{field:i,stringify:a}of n){let n=e[i],o=a(n);if(o===t.values.get(i))continue;r=!0;let s=t.trigrams.get(i);s&&_(e,s);let c=q(o),l=c.length>=1?Y(c):new Set;t.trigrams.set(i,l),t.values.set(i,o),g(e,l)}r&&f()},remove(e){let t=l.get(e);if(t){for(let n of t.trigrams.values())_(e,n);l.delete(e),f()}},search(e,t){let n=t?.threshold??o,r=Math.max(0,t?.limit??s),i=t?.minQueryLength??c;if(!e.trim())return[...l.keys()].slice(0,r).map(e=>({item:e,matches:[],score:1}));let a=q(e);if(!a)return[];let u=a.length<i?null:h(a),d=u===null?y(a):b(u),f=[];for(let e of d){let t=l.get(e);if(!t)continue;let r=x(a,u,t);if(r>=n){let n=S(a,t.values);f.push({item:e,matches:n,score:r})}}return f.sort((e,t)=>t.score-e.score).slice(0,r)},get size(){return l.size}}}var le=200;function Z(e,t={}){let{debounce:n=le,limit:i,minQueryLength:a,threshold:o}=t,s=W(``,{name:`scout:query`}),c=W(``,{name:`scout:committedQuery`}),l=W(0,{name:`scout:indexVersion`}),u=e.onMutate(()=>{l.value++}),d=H(()=>s.value!==c.value,{name:`scout:isSearching`}),f=H(()=>(l.value,e.search(c.value,{limit:i,minQueryLength:a,threshold:o})),{name:`scout:results`}),p=null;function m(){p!==null&&(clearTimeout(p),p=null)}let h=s.subscribe(()=>{let e=s.peek();if(m(),e!==c.peek()){if(n===0){c.value=e;return}p=setTimeout(()=>{c.value=e,p=null},n)}});function g(){if(_)throw new r(`SearchState.clear() called after dispose()`);m(),oe(()=>{s.value=``,c.value=``})}let _=!1,v=new AbortController;function y(){_=!0,v.abort(),m(),h.dispose(),u(),s.dispose(),c.dispose(),d.dispose(),f.dispose(),l.dispose()}return{clear:g,get disposalSignal(){return v.signal},dispose:y,get disposed(){return _},isSearching:d,query:s,results:f,[Symbol.dispose](){y()}}}function ue(e,t){let n=X(e,{fields:t.fields,limit:t.limit,minQueryLength:t.minQueryLength,threshold:t.threshold});return{...Z(n,{debounce:t.debounce}),index:n}}function Q(e){let t=de();return t?[...t.segment(e)].filter(e=>e.isWordLike).map(e=>e.segment).join(` `):e}var $;function de(){return $===void 0&&($=typeof Intl>`u`||typeof Intl.Segmenter!=`function`?null:new Intl.Segmenter(void 0,{granularity:`word`})),$}exports.ScoutDisposedError=r,exports.ScoutError=n,exports.ScoutIndexError=i,exports.createIndex=X,exports.createReactiveSearch=ue,exports.createSearch=Z,exports.findMatchRanges=a,exports.highlight=o,exports.highlightField=s,exports.segmentWords=Q,exports.toFilterPredicate=t,exports.toSearchFn=e;
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});function e(e,t){return(n,r)=>e.search(r,t).map(e=>e.item)}function t(e,t,n){let r=new Set(e.search(t,n).map(e=>e.item));return e=>r.has(e)}var n=class e extends Error{constructor(e,t){super(e,t),this.name=new.target.name,Object.setPrototypeOf(this,new.target.prototype)}static is(t){return t instanceof e}},r=class extends n{},i=class extends n{};function a(e,t){let n=e.toLowerCase(),r=t.split(` `).filter(Boolean),i=[];for(let e of r){let t=0;for(;t<n.length;){let r=n.indexOf(e,t);if(r===-1)break;i.push([r,r+e.length]),t=r+1}}i.sort((e,t)=>e[0]-t[0]);let a=[];for(let e of i){let t=a[a.length-1];t&&e[0]<=t[1]?t[1]=Math.max(t[1],e[1]):a.push([e[0],e[1]])}return a}function o(e,t){if(!e)return[];if(!t.length)return[{highlighted:!1,text:e}];let n=[],r=0;for(let[i,a]of t)i>r&&n.push({highlighted:!1,text:e.slice(r,i)}),a>i&&n.push({highlighted:!0,text:e.slice(i,a)}),r=a;return r<e.length&&n.push({highlighted:!1,text:e.slice(r)}),n}function s(e,t,n){return o(n,e.matches.find(e=>e.field===t)?.ranges??[])}var c=null,l=()=>c,u=class e extends Error{constructor(e,t){super(e,t),this.name=new.target.name,Object.setPrototypeOf(this,new.target.prototype)}static is(t){return t instanceof e}},d=class extends u{},f=class extends u{},p=0,m=()=>++p,h=()=>p,g={scheduling:{activeDirty:`a`,batchDepth:0,dirtyWithEffectSubsA:new Set,dirtyWithEffectSubsB:new Set,pendingSubscribers:new Set},scopeCleanups:null,tracking:null},_=null,v=()=>_===null?g:_.get(),y=()=>v().scheduling,b=()=>_!==null,x=()=>v().tracking,S=(e,t)=>{if(_!==null){let n=_.get();return _.run({...n,tracking:e},t)}let n=g;g={...g,tracking:e};try{return t()}finally{g=n}},C=()=>v().scopeCleanups,w=e=>{let t=x();t?.kind===`effect`?t.cleanups.push(e):C()?.push(e)},T=e=>{let t=x();if(t!==null){if(t.sourceObserver?.(e),t.kind===`computed`)t.depCollector.push({source:e,version:e.version});else if(t.kind===`effect`){let n=t.effect;e.addEffectSub(n),t.subscriptions.add(()=>e.removeEffectSub(n)),t.deps.set(e,e.version)}}},E=e=>e instanceof Error?e:Error(`Non-Error thrown: ${String(e)}`),D=e=>{let t=[];for(let n of e)try{n()}catch(e){t.push(E(e))}return t},ee=(e,t)=>{let n=D(e);if(n.length===1)throw n[0];if(n.length>0)throw AggregateError(n,t)},te=!globalThis.__RIPPLE_PROD__;function O(e){te&&console.warn(`[@vielzeug/ripple] ${e}`)}var k=!1,A=0,j=[],M=e=>e.activeDirty===`a`?e.dirtyWithEffectSubsA:e.dirtyWithEffectSubsB,ne=(e,t)=>{let n=++A;for(let n of t.effectSubs())e.pendingSubscribers.add(n);for(let e of t.computedSubs())j.push(e);try{for(;j.length>0;){let t=j.pop();if(t.lastPropEpoch_!==n&&(t.lastPropEpoch_=n,t.markDirty())){t.effectSubs().size>0&&M(e).add(t);for(let e of t.computedSubs())j.push(e)}}}finally{j.length=0}},re=e=>{for(;M(e).size>0;){let t=M(e);e.activeDirty=e.activeDirty===`a`?`b`:`a`,M(e).clear();for(let n of t)if(n.hasSubscribers()&&n.refreshIfDirty()){for(let t of n.effectSubs())e.pendingSubscribers.add(t);for(let t of n.computedSubs())t.markDirty()&&t.effectSubs().size>0&&M(e).add(t)}t.clear()}},N=e=>{let t=0;for(;e.pendingSubscribers.size>0||M(e).size>0;){if(++t>100)throw new f(`infinite flush loop (> 100 iterations)`);if(M(e).size>0&&re(e),e.pendingSubscribers.size===0)continue;let n=[...e.pendingSubscribers];e.pendingSubscribers.clear(),ee(n,`subscriber errors`)}},ie=globalThis.process?.versions!=null,ae=e=>{if(!e.hasSubscribers())return;!k&&ie&&!b()&&(k=!0,O(`Signal updated in a Node.js-like environment. The module-level flush queue is shared across concurrent requests — use per-request worker isolation or the @vielzeug/ripple/ssr sub-path for request-isolated scheduling.`));let t=y();ne(t,e),t.batchDepth===0&&N(t)},oe=e=>{let t=y();t.batchDepth++;let n;try{n=e()}catch(e){throw t.batchDepth--,t.batchDepth===0&&(t.pendingSubscribers.clear(),t.dirtyWithEffectSubsA.clear(),t.dirtyWithEffectSubsB.clear()),e}return t.batchDepth--,t.batchDepth===0&&N(t),n},P=class{fn_;disposed_=!1;constructor(e){this.fn_=e}get disposed(){return this.disposed_}dispose(){if(this.disposed_)return;this.disposed_=!0;let e=this.fn_;this.fn_=null,e()}[Symbol.dispose](){this.dispose()}},F=Symbol(`ripple.is-signal`),I=Symbol(`ripple.is-computed`),L=Symbol(`ripple.uninitialized`),R=new FinalizationRegistry(({key:e,map:t})=>{t.delete(e)}),z=class{version=0;name;[F]=!0;computedSubs_=new Map;effectSubs_=new Set;constructor(e){this.name=e}addComputedSub(e){let t=new WeakRef(e);this.computedSubs_.set(e,t),R.register(e,{key:e,map:this.computedSubs_},t)}removeComputedSub(e){let t=this.computedSubs_.get(e);t!==void 0&&(this.computedSubs_.delete(e),R.unregister(t))}addEffectSub(e){this.effectSubs_.add(e)}removeEffectSub(e){this.effectSubs_.delete(e)}clearSubscribers(){for(let e of this.computedSubs_.values())R.unregister(e);this.computedSubs_.clear(),this.effectSubs_.clear()}hasSubscribers(){if(this.effectSubs_.size>0)return!0;for(let e of this.computedSubs_.values())if(e.deref()!==void 0)return!0;return!1}*computedSubs(){for(let[e,t]of this.computedSubs_){let n=t.deref();n===void 0?(this.computedSubs_.delete(e),R.unregister(t)):yield n}}effectSubs(){return this.effectSubs_}},B=class extends z{[I]=!0;lastPropEpoch_=0},V=class extends B{value_;dirty_;computing_;disposed_;deps_;compute_;equals_;maxRevision_;constructor(e,t){let{equals:n,name:r}=t??{};super(r),this.value_=L,this.dirty_=!0,this.computing_=!1,this.disposed_=!1,this.deps_=[],this.maxRevision_=-1,this.compute_=e,this.equals_=n===void 0?Object.is:(e,t)=>n(e,t)}markDirty(){return this.disposed_||this.dirty_?!1:(this.dirty_=!0,!0)}refreshIfDirty(){if(!this.dirty_)return!1;if(h()<=this.maxRevision_)return this.dirty_=!1,!1;if(this.deps_.length>0){let e=!0;for(let t of this.deps_){let n=t.source;if(`refreshIfDirty`in n&&n.refreshIfDirty(),n.version!==t.version){e=!1;break}}if(e)return this.dirty_=!1,this.maxRevision_=h(),!1}return this.recompute()}runCompute(e){try{return S({computed:this,depCollector:e,kind:`computed`},this.compute_)}catch(e){throw E(e)}}recompute(){if(this.computing_)throw new d(`computed cycle detected${this.name?` "${this.name}"`:``}`);this.computing_=!0;try{let e=[];l()?.compute?.({name:this.name});let t=this.runCompute(e);return this.dirty_=!1,this.maxRevision_=h(),this.updateDeps(e),this.value_===L||!this.equals_(this.value_,t)?(this.value_=t,this.version++,!0):!1}finally{this.computing_=!1}}updateDeps(e){let t=this.deps_;if(t.length===e.length&&t.every((t,n)=>t.source===e[n].source)){for(let n=0;n<e.length;n++)t[n].version=e[n].version;return}let n=new Set(t.map(e=>e.source)),r=new Set(e.map(e=>e.source));for(let e of t)r.has(e.source)||e.source.removeComputedSub(this);for(let t of e)n.has(t.source)||t.source.addComputedSub(this);this.deps_=e}get value(){return this.disposed_?this.value_===L?void 0:this.value_:(this.refreshIfDirty(),T(this),this.value_)}peek(){return this.disposed_?this.value_===L?void 0:this.value_:(this.refreshIfDirty(),this.value_)}subscribe=e=>{if(this.disposed_){let e=new P(()=>{});return e.dispose(),e}return this.refreshIfDirty(),this.addEffectSub(e),new P(()=>{this.removeEffectSub(e)})};get disposed(){return this.disposed_}dispose(){if(!this.disposed_){this.disposed_=!0;for(let e of this.deps_)e.source.removeComputedSub(this);this.deps_=[],this.clearSubscribers(),l()?.dispose?.({kind:`computed`,name:this.name})}}[Symbol.dispose](){this.dispose()}},H=(e,t)=>{let n=new V(e,t);return w(()=>n.dispose()),n},U=class extends z{value_;equals_;disposed_;constructor(e,t,n){super(n),this.value_=e,this.equals_=t??Object.is,this.disposed_=!1}get value(){return this.disposed_||T(this),this.value_}set value(e){if(this.disposed_||this.equals_(this.value_,e))return;let t=this.value_;this.value_=e,this.version=m(),l()?.write?.({name:this.name,newValue:e,oldValue:t}),ae(this)}peek(){return this.value_}subscribe=e=>{if(this.disposed_){let e=new P(()=>{});return e.dispose(),e}return this.addEffectSub(e),new P(()=>{this.removeEffectSub(e)})};get disposed(){return this.disposed_}dispose(){this.disposed_||(this.disposed_=!0,this.clearSubscribers(),l()?.dispose?.({kind:`signal`,name:this.name}))}[Symbol.dispose](){this.dispose()}},W=(e,t)=>new U(e,t?.equals,t?.name),G=!globalThis.__SCOUT_PROD__;function K(e){G&&console.warn(`[@vielzeug/scout] ${e}`)}function q(e){return e.toLowerCase().replace(/[^\p{L}\p{N}\s']/gu,` `).trim().replace(/\s+/g,` `)}function J(e){return e==null?``:typeof e==`string`?e:typeof e==`number`||typeof e==`boolean`?String(e):Array.isArray(e)?(K("defaultStringify: received an array value — provide a custom `stringify` on the FieldDef to control array indexing.\nFalling back to joining string/number/boolean elements with a space."),e.filter(e=>typeof e==`string`||typeof e==`number`||typeof e==`boolean`).join(` `)):``}function Y(e){let t=new Set,n=` ${e} `;for(let e=0;e<n.length-2;e++)t.add(n.slice(e,e+3));return t}function se(e,t){if(e.size===0||t.size===0)return 0;let n=0;for(let r of e)t.has(r)&&n++;return n<2&&n<e.size?0:n/Math.min(e.size,t.size)}function ce(e){return e.map(e=>typeof e==`string`?{field:e,stringify:J,weight:1}:{field:e.field,stringify:e.stringify??J,weight:e.weight??1})}function X(e,t){if(t.fields.length===0)throw new i(`createIndex: at least one field is required.`);let n=ce(t.fields),r=n.reduce((e,t)=>Math.max(e,t.weight),1),o=t.threshold??.2,s=t.limit??50,c=t.minQueryLength??3,l=new Map,u=new Map,d=new Set;function f(){for(let e of d)e()}let p=null,m=null;function h(e){return e===p&&m!==null?m:(p=e,m=Y(e),m)}function g(e,t){for(let n of t){let t=u.get(n);t||(t=new Set,u.set(n,t)),t.add(e)}}function _(e,t){for(let n of t){let t=u.get(n);t&&(t.delete(e),t.size===0&&u.delete(n))}}function v(e){let t=new Map,r=new Map;for(let{field:i,stringify:a}of n){let n=e[i],o=a(n),s=q(o),c=s.length>=1?Y(s):new Set;t.set(i,c),r.set(i,o),g(e,c)}l.set(e,{trigrams:t,values:r})}function y(e){let t=new Set;for(let[n,r]of l)for(let i of r.values.values())if(i.toLowerCase().includes(e)){t.add(n);break}return t}function b(e){let t=new Set;for(let n of e){let e=u.get(n);if(e)for(let n of e)t.add(n)}return t}function x(e,t,i){let a=0;for(let{field:o,weight:s}of n){let n;if(t===null)n=+!!(i.values.get(o)??``).toLowerCase().includes(e);else{let e=i.trigrams.get(o);if(!e||e.size===0)continue;n=se(t,e)}let c=s/r*n;c>a&&(a=c)}return a}function S(e,t){let r=[];for(let{field:i}of n){let n=t.get(i);if(!n)continue;let o=a(n,e);o.length>0&&r.push({field:i,ranges:o})}return r}for(let t of e)v(t);return{add(e){l.has(e)||(v(e),f())},get items(){return[...l.keys()]},onMutate(e){return d.add(e),()=>{d.delete(e)}},reindex(e){let t=l.get(e);if(!t)return;let r=!1;for(let{field:i,stringify:a}of n){let n=e[i],o=a(n);if(o===t.values.get(i))continue;r=!0;let s=t.trigrams.get(i);s&&_(e,s);let c=q(o),l=c.length>=1?Y(c):new Set;t.trigrams.set(i,l),t.values.set(i,o),g(e,l)}r&&f()},remove(e){let t=l.get(e);if(t){for(let n of t.trigrams.values())_(e,n);l.delete(e),f()}},search(e,t){let n=t?.threshold??o,r=Math.max(0,t?.limit??s),i=t?.minQueryLength??c;if(!e.trim())return[...l.keys()].slice(0,r).map(e=>({item:e,matches:[],score:1}));let a=q(e);if(!a)return[];let u=a.length<i?null:h(a),d=u===null?y(a):b(u),f=[];for(let e of d){let t=l.get(e);if(!t)continue;let r=x(a,u,t);if(r>=n){let n=S(a,t.values);f.push({item:e,matches:n,score:r})}}return f.sort((e,t)=>t.score-e.score).slice(0,r)},get size(){return l.size}}}var le=200;function Z(e,t={}){let{debounce:n=le,limit:i,minQueryLength:a,threshold:o}=t,s=W(``,{name:`scout:query`}),c=W(``,{name:`scout:committedQuery`}),l=W(0,{name:`scout:indexVersion`}),u=e.onMutate(()=>{l.value++}),d=H(()=>s.value!==c.value,{name:`scout:isSearching`}),f=H(()=>(l.value,e.search(c.value,{limit:i,minQueryLength:a,threshold:o})),{name:`scout:results`}),p=null;function m(){p!==null&&(clearTimeout(p),p=null)}let h=s.subscribe(()=>{let e=s.peek();if(m(),e!==c.peek()){if(n===0){c.value=e;return}p=setTimeout(()=>{c.value=e,p=null},n)}});function g(){if(_)throw new r(`SearchState.clear() called after dispose()`);m(),oe(()=>{s.value=``,c.value=``})}let _=!1,v=new AbortController;function y(){_=!0,v.abort(),m(),h.dispose(),u(),s.dispose(),c.dispose(),d.dispose(),f.dispose(),l.dispose()}return{clear:g,get disposalSignal(){return v.signal},dispose:y,get disposed(){return _},isSearching:d,query:s,results:f,[Symbol.dispose](){y()}}}function ue(e,t){let n=X(e,{fields:t.fields,limit:t.limit,minQueryLength:t.minQueryLength,threshold:t.threshold});return{...Z(n,{debounce:t.debounce}),index:n}}function Q(e){let t=de();return t?[...t.segment(e)].filter(e=>e.isWordLike).map(e=>e.segment).join(` `):e}var $;function de(){return $===void 0&&($=typeof Intl>`u`||typeof Intl.Segmenter!=`function`?null:new Intl.Segmenter(void 0,{granularity:`word`})),$}exports.ScoutDisposedError=r,exports.ScoutError=n,exports.ScoutIndexError=i,exports.createIndex=X,exports.createReactiveSearch=ue,exports.createSearch=Z,exports.findMatchRanges=a,exports.highlight=o,exports.highlightField=s,exports.segmentWords=Q,exports.toFilterPredicate=t,exports.toSearchFn=e;
|
|
2
2
|
//# sourceMappingURL=scout.cjs.map
|