diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml
index 239c6f69c..dc3438c25 100644
--- a/.github/workflows/nodejs.yml
+++ b/.github/workflows/nodejs.yml
@@ -26,5 +26,9 @@ jobs:
with:
node-version: ${{ matrix.node }}
- run: npm install --prefer-offline --no-audit --no-fund
+ # The build tool (tsdown) requires Node >= 22.18. The published library and
+ # its tests still run on Node 20: that leg tests the committed dist rather
+ # than rebuilding. Node 22/24 rebuild from source before testing.
- run: npm run build
+ if: matrix.node != '20'
- run: npm run test
diff --git a/.nvmrc b/.nvmrc
index 209e3ef4b..2bd5a0a98 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-20
+22
diff --git a/DEVELOPERS.md b/DEVELOPERS.md
index 24367ff87..2577421b2 100644
--- a/DEVELOPERS.md
+++ b/DEVELOPERS.md
@@ -7,6 +7,8 @@
## Setup
+Building and releasing require **Node >= 22.18** (the bundler, [tsdown](https://tsdown.dev), needs it); `nvm use` picks up the pinned `.nvmrc`. The published library runs on far older Node, and the CI test matrix still includes Node 20.
+
```shell
npm install
```
@@ -19,8 +21,7 @@ npm install
| `npm test` | Run the test suite (vitest) |
| `npm run typecheck` | Type-check the source without emitting |
| `npm run lint` | Lint source and tests (eslint) |
-| `npm run dev` | Watch mode for ESM dev build |
-| `npm run dev:cjs` | Watch mode for CJS dev build |
+| `npm run dev` | Watch mode (rebuilds all dist targets via tsdown) |
| `npm run bench` | Build, then run the core search + index regression benchmarks |
| `npm run bench:search` | Search benchmark (object/string fuzzy, scaling, remove) |
| `npm run bench:index` | Index creation benchmark across dataset sizes |
@@ -30,7 +31,7 @@ npm install
## Project Structure
-Source is TypeScript. Types are emitted from source via `rollup-plugin-dts`.
+Source is TypeScript. The build, including `.d.ts` emit, runs through [tsdown](https://tsdown.dev) (rolldown engine); see `tsdown.config.ts`.
```
src/
@@ -42,7 +43,8 @@ src/
entry.ts — Entry point with static methods and type exports
test/ — Tests and fixtures
bench/ — Benchmarks (search, index creation, extended, tokens, workers)
-scripts/ — Rollup build configs
+tsdown.config.ts — Build config (all dist targets + types)
+scripts/ — Release + docs helpers (bump-docs, deploy-docs, release)
dist/ — Built output (CJS, ESM, .d.ts)
```
diff --git a/dist/fuse-worker.cjs b/dist/fuse-worker.cjs
index aa42966f1..499f80c99 100644
--- a/dist/fuse-worker.cjs
+++ b/dist/fuse-worker.cjs
@@ -6,252 +6,199 @@
*
* http://www.apache.org/licenses/LICENSE-2.0
*/
+Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
-'use strict';
-
-var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
-const FUSE_WORKER_UNSUPPORTED_FN_OPTION = option => `FuseWorker does not support function-valued option '${option}': ` + `functions cannot be transferred to Web Workers via postMessage. ` + `Remove this option or fall back to Fuse.`;
-const FUSE_WORKER_TOKEN_SEARCH_UNSUPPORTED = `FuseWorker does not support useTokenSearch: token search depends on ` + `corpus-level statistics (df, fieldCount) that are computed per shard, ` + `so per-shard scores would diverge from single-thread Fuse. Use Fuse on ` + `the main thread for token search.`;
-
-///
+//#region src/core/errorMessages.ts
+const FUSE_WORKER_UNSUPPORTED_FN_OPTION = (option) => `FuseWorker does not support function-valued option '${option}': functions cannot be transferred to Web Workers via postMessage. Remove this option or fall back to Fuse.`;
+const FUSE_WORKER_TOKEN_SEARCH_UNSUPPORTED = "FuseWorker does not support useTokenSearch: token search depends on corpus-level statistics (df, fieldCount) that are computed per shard, so per-shard scores would diverge from single-thread Fuse. Use Fuse on the main thread for token search.";
+//#endregion
+//#region src/workers/FuseWorker.ts
const DEFAULT_MAX_WORKERS = 8;
function getDefaultWorkerCount() {
- const hw = typeof navigator !== 'undefined' ? navigator.hardwareConcurrency || 4 : 4;
- return Math.min(hw, DEFAULT_MAX_WORKERS);
+ const hw = typeof navigator !== "undefined" ? navigator.hardwareConcurrency || 4 : 4;
+ return Math.min(hw, DEFAULT_MAX_WORKERS);
}
-class FuseWorker {
- _shards = null;
- _addCursor = 0;
- _initPromise = null;
- _pending = new Map();
- _nextId = 0;
- constructor(docs, options, workerOptions) {
- this._docs = docs.slice();
- this._options = options || {};
- this._workerOptions = workerOptions || {};
- // Reject function-valued options eagerly. Without this check, postMessage
- // throws DataCloneError on first search() rather than at construction.
- FuseWorker._assertNoFunctionOptions(this._options);
- // Token search needs global corpus statistics, but each shard would build
- // its own — scores would diverge from single-thread Fuse. Refuse upfront
- // rather than silently returning ordering that doesn't match.
- if (this._options.useTokenSearch) {
- throw new Error(FUSE_WORKER_TOKEN_SEARCH_UNSUPPORTED);
- }
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore -- import.meta.url is resolved by Rollup at build time
- this._workerUrl = this._workerOptions.workerUrl || new URL('./fuse.worker.mjs', (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('fuse-worker.cjs', document.baseURI).href)));
- }
- static _assertNoFunctionOptions(options) {
- if (typeof options.sortFn === 'function') {
- throw new Error(FUSE_WORKER_UNSUPPORTED_FN_OPTION('sortFn'));
- }
- if (typeof options.getFn === 'function') {
- throw new Error(FUSE_WORKER_UNSUPPORTED_FN_OPTION('getFn'));
- }
- if (typeof options.tokenize === 'function') {
- throw new Error(FUSE_WORKER_UNSUPPORTED_FN_OPTION('tokenize'));
- }
- const keys = options.keys;
- if (Array.isArray(keys)) {
- for (let i = 0, len = keys.length; i < len; i += 1) {
- const key = keys[i];
- if (key && typeof key === 'object' && !Array.isArray(key)) {
- if (typeof key.getFn === 'function') {
- const name = key.name;
- const label = Array.isArray(name) ? name.join('.') : name ?? String(i);
- throw new Error(FUSE_WORKER_UNSUPPORTED_FN_OPTION(`keys[${label}].getFn`));
- }
- }
- }
- }
- }
- _getNumWorkers() {
- return this._workerOptions.numWorkers || getDefaultWorkerCount();
- }
- _ensureInit() {
- if (this._initPromise) return this._initPromise;
- this._initPromise = this._init();
- return this._initPromise;
- }
- _spawnWorker() {
- const worker = new Worker(this._workerUrl, {
- type: 'module'
- });
- worker.onmessage = e => {
- const {
- id,
- result,
- error
- } = e.data;
- const handler = this._pending.get(id);
- if (!handler) return;
- this._pending.delete(id);
- if (error) {
- handler.reject(new Error(error));
- } else {
- handler.resolve(result);
- }
- };
- worker.onerror = e => {
- for (const [, handler] of this._pending) {
- handler.reject(new Error(e.message));
- }
- };
- return worker;
- }
- _workerInitOptions() {
- // Force includeScore so the main thread can do a global (score, idx)
- // tie-break across shards. Score is stripped from the final result if the
- // caller didn't ask for it.
- return {
- ...this._options,
- includeScore: true
- };
- }
- async _init() {
- const numWorkers = this._getNumWorkers();
- const chunkSize = Math.ceil(this._docs.length / numWorkers);
- this._shards = [];
- this._addCursor = 0;
- const workerInitOptions = this._workerInitOptions();
- const initPromises = [];
- for (let i = 0; i < numWorkers; i++) {
- const start = i * chunkSize;
- const end = Math.min(start + chunkSize, this._docs.length);
- const chunk = this._docs.slice(start, end);
- const globalIndices = [];
- for (let j = start; j < end; j += 1) globalIndices.push(j);
- const shard = {
- worker: this._spawnWorker(),
- globalIndices
- };
- this._shards.push(shard);
- initPromises.push(this._call(shard.worker, 'init', [chunk, workerInitOptions]));
- }
- await Promise.all(initPromises);
- }
- _call(worker, method, args) {
- const id = this._nextId++;
- return new Promise((resolve, reject) => {
- this._pending.set(id, {
- resolve,
- reject
- });
- worker.postMessage({
- id,
- method,
- args
- });
- });
- }
- async search(query, options) {
- await this._ensureInit();
- const shards = this._shards;
- const results = await Promise.all(shards.map(s => this._call(s.worker, 'search', [query, options])));
-
- // Merge results from all shards, rewriting refIndex from shard-local to global
- const merged = [];
- for (let i = 0, len = results.length; i < len; i += 1) {
- const {
- globalIndices
- } = shards[i];
- for (const r of results[i]) {
- merged.push({
- ...r,
- refIndex: globalIndices[r.refIndex]
- });
- }
- }
- const shouldSort = this._options.shouldSort !== false;
- if (shouldSort) {
- // Mirror Fuse's default sortFn: (score, idx) tie-break, but on the
- // GLOBAL refIndex so equal-score results across shards collapse into a
- // deterministic order matching single-thread Fuse.
- merged.sort((a, b) => {
- const sa = a.score ?? 0;
- const sb = b.score ?? 0;
- if (sa === sb) {
- return a.refIndex < b.refIndex ? -1 : 1;
- }
- return sa < sb ? -1 : 1;
- });
- } else {
- // Restore global collection order. Round-robin add() and
- // shard-concatenation order otherwise leak through.
- merged.sort((a, b) => a.refIndex - b.refIndex);
- }
-
- // Workers always include score so the merge above can tie-break; strip it
- // here if the caller didn't ask for it. Rebuild the object instead of
- // `delete`-ing — `delete` deopts the shape and roughly doubles the
- // post-sort cost on large result sets.
- if (!this._options.includeScore) {
- for (let i = 0, len = merged.length; i < len; i += 1) {
- const r = merged[i];
- merged[i] = r.matches !== undefined ? {
- item: r.item,
- refIndex: r.refIndex,
- matches: r.matches
- } : {
- item: r.item,
- refIndex: r.refIndex
- };
- }
- }
- const limit = options?.limit;
- if (limit && limit > 0) {
- return merged.slice(0, limit);
- }
- return merged;
- }
- async add(doc) {
- await this._ensureInit();
- const shards = this._shards;
- const shard = shards[this._addCursor % shards.length];
- this._addCursor += 1;
- const globalIdx = this._docs.length;
- this._docs.push(doc);
- shard.globalIndices.push(globalIdx);
- await this._call(shard.worker, 'add', [doc]);
- }
- async setCollection(docs) {
- this._docs = docs.slice();
- if (!this._shards) {
- this._initPromise = null;
- return;
- }
- const shards = this._shards;
- const chunkSize = Math.ceil(this._docs.length / shards.length);
- this._addCursor = 0;
- const tasks = [];
- for (let i = 0, len = shards.length; i < len; i += 1) {
- const start = i * chunkSize;
- const end = Math.min(start + chunkSize, this._docs.length);
- const chunk = this._docs.slice(start, end);
- const globalIndices = [];
- for (let j = start; j < end; j += 1) globalIndices.push(j);
- shards[i].globalIndices = globalIndices;
- tasks.push(this._call(shards[i].worker, 'setCollection', [chunk]));
- }
- await Promise.all(tasks);
- }
- terminate() {
- if (this._shards) {
- for (const {
- worker
- } of this._shards) {
- worker.terminate();
- }
- this._shards = null;
- }
- this._initPromise = null;
- for (const [, handler] of this._pending) {
- handler.reject(new Error('FuseWorker terminated'));
- }
- this._pending.clear();
- }
+const browserWorkerBase = typeof document !== "undefined" ? document.currentScript?.src || new URL("fuse-worker.cjs", document.baseURI).href : void 0;
+function resolveDefaultWorkerUrl() {
+ if (browserWorkerBase !== void 0) return new URL("./fuse.worker.mjs", browserWorkerBase);
+ try {
+ return new URL("./fuse.worker.mjs", require("url").pathToFileURL(__filename).href);
+ } catch {
+ if (browserWorkerBase !== void 0) return new URL("./fuse.worker.mjs", browserWorkerBase);
+ throw new Error("FuseWorker: unable to resolve the worker script URL automatically; pass workerOptions.workerUrl explicitly.");
+ }
}
+var FuseWorker = class FuseWorker {
+ constructor(docs, options, workerOptions) {
+ this._shards = null;
+ this._addCursor = 0;
+ this._initPromise = null;
+ this._pending = /* @__PURE__ */ new Map();
+ this._nextId = 0;
+ this._docs = docs.slice();
+ this._options = options || {};
+ this._workerOptions = workerOptions || {};
+ FuseWorker._assertNoFunctionOptions(this._options);
+ if (this._options.useTokenSearch) throw new Error(FUSE_WORKER_TOKEN_SEARCH_UNSUPPORTED);
+ this._workerUrl = this._workerOptions.workerUrl || resolveDefaultWorkerUrl();
+ }
+ static _assertNoFunctionOptions(options) {
+ if (typeof options.sortFn === "function") throw new Error(FUSE_WORKER_UNSUPPORTED_FN_OPTION("sortFn"));
+ if (typeof options.getFn === "function") throw new Error(FUSE_WORKER_UNSUPPORTED_FN_OPTION("getFn"));
+ if (typeof options.tokenize === "function") throw new Error(FUSE_WORKER_UNSUPPORTED_FN_OPTION("tokenize"));
+ const keys = options.keys;
+ if (Array.isArray(keys)) for (let i = 0, len = keys.length; i < len; i += 1) {
+ const key = keys[i];
+ if (key && typeof key === "object" && !Array.isArray(key)) {
+ if (typeof key.getFn === "function") {
+ const name = key.name;
+ const label = Array.isArray(name) ? name.join(".") : name ?? String(i);
+ throw new Error(FUSE_WORKER_UNSUPPORTED_FN_OPTION(`keys[${label}].getFn`));
+ }
+ }
+ }
+ }
+ _getNumWorkers() {
+ return this._workerOptions.numWorkers || getDefaultWorkerCount();
+ }
+ _ensureInit() {
+ if (this._initPromise) return this._initPromise;
+ this._initPromise = this._init();
+ return this._initPromise;
+ }
+ _spawnWorker() {
+ const worker = new Worker(this._workerUrl, { type: "module" });
+ worker.onmessage = (e) => {
+ const { id, result, error } = e.data;
+ const handler = this._pending.get(id);
+ if (!handler) return;
+ this._pending.delete(id);
+ if (error) handler.reject(new Error(error));
+ else handler.resolve(result);
+ };
+ worker.onerror = (e) => {
+ for (const [, handler] of this._pending) handler.reject(new Error(e.message));
+ };
+ return worker;
+ }
+ _workerInitOptions() {
+ return {
+ ...this._options,
+ includeScore: true
+ };
+ }
+ async _init() {
+ const numWorkers = this._getNumWorkers();
+ const chunkSize = Math.ceil(this._docs.length / numWorkers);
+ this._shards = [];
+ this._addCursor = 0;
+ const workerInitOptions = this._workerInitOptions();
+ const initPromises = [];
+ for (let i = 0; i < numWorkers; i++) {
+ const start = i * chunkSize;
+ const end = Math.min(start + chunkSize, this._docs.length);
+ const chunk = this._docs.slice(start, end);
+ const globalIndices = [];
+ for (let j = start; j < end; j += 1) globalIndices.push(j);
+ const shard = {
+ worker: this._spawnWorker(),
+ globalIndices
+ };
+ this._shards.push(shard);
+ initPromises.push(this._call(shard.worker, "init", [chunk, workerInitOptions]));
+ }
+ await Promise.all(initPromises);
+ }
+ _call(worker, method, args) {
+ const id = this._nextId++;
+ return new Promise((resolve, reject) => {
+ this._pending.set(id, {
+ resolve,
+ reject
+ });
+ worker.postMessage({
+ id,
+ method,
+ args
+ });
+ });
+ }
+ async search(query, options) {
+ await this._ensureInit();
+ const shards = this._shards;
+ const results = await Promise.all(shards.map((s) => this._call(s.worker, "search", [query, options])));
+ const merged = [];
+ for (let i = 0, len = results.length; i < len; i += 1) {
+ const { globalIndices } = shards[i];
+ for (const r of results[i]) merged.push({
+ ...r,
+ refIndex: globalIndices[r.refIndex]
+ });
+ }
+ if (this._options.shouldSort !== false) merged.sort((a, b) => {
+ const sa = a.score ?? 0;
+ const sb = b.score ?? 0;
+ if (sa === sb) return a.refIndex < b.refIndex ? -1 : 1;
+ return sa < sb ? -1 : 1;
+ });
+ else merged.sort((a, b) => a.refIndex - b.refIndex);
+ if (!this._options.includeScore) for (let i = 0, len = merged.length; i < len; i += 1) {
+ const r = merged[i];
+ merged[i] = r.matches !== void 0 ? {
+ item: r.item,
+ refIndex: r.refIndex,
+ matches: r.matches
+ } : {
+ item: r.item,
+ refIndex: r.refIndex
+ };
+ }
+ const limit = options?.limit;
+ if (limit && limit > 0) return merged.slice(0, limit);
+ return merged;
+ }
+ async add(doc) {
+ await this._ensureInit();
+ const shards = this._shards;
+ const shard = shards[this._addCursor % shards.length];
+ this._addCursor += 1;
+ const globalIdx = this._docs.length;
+ this._docs.push(doc);
+ shard.globalIndices.push(globalIdx);
+ await this._call(shard.worker, "add", [doc]);
+ }
+ async setCollection(docs) {
+ this._docs = docs.slice();
+ if (!this._shards) {
+ this._initPromise = null;
+ return;
+ }
+ const shards = this._shards;
+ const chunkSize = Math.ceil(this._docs.length / shards.length);
+ this._addCursor = 0;
+ const tasks = [];
+ for (let i = 0, len = shards.length; i < len; i += 1) {
+ const start = i * chunkSize;
+ const end = Math.min(start + chunkSize, this._docs.length);
+ const chunk = this._docs.slice(start, end);
+ const globalIndices = [];
+ for (let j = start; j < end; j += 1) globalIndices.push(j);
+ shards[i].globalIndices = globalIndices;
+ tasks.push(this._call(shards[i].worker, "setCollection", [chunk]));
+ }
+ await Promise.all(tasks);
+ }
+ terminate() {
+ if (this._shards) {
+ for (const { worker } of this._shards) worker.terminate();
+ this._shards = null;
+ }
+ this._initPromise = null;
+ for (const [, handler] of this._pending) handler.reject(/* @__PURE__ */ new Error("FuseWorker terminated"));
+ this._pending.clear();
+ }
+};
-exports.FuseWorker = FuseWorker;
+//#endregion
+exports.FuseWorker = FuseWorker;
\ No newline at end of file
diff --git a/dist/fuse-worker.d.ts b/dist/fuse-worker.d.ts
index 2b53c6aba..26ac37534 100644
--- a/dist/fuse-worker.d.ts
+++ b/dist/fuse-worker.d.ts
@@ -1,18 +1,18 @@
-// Type definitions for Fuse.js v7.4.1
-// TypeScript v6.0.3
+
+//#region src/types.d.ts
type RangeTuple = [number, number];
interface FuseOptionKeyObject {
- name: string | string[];
- weight?: number;
- getFn?: (obj: T) => ReadonlyArray | string | null | undefined;
+ name: string | string[];
+ weight?: number;
+ getFn?: (obj: T) => ReadonlyArray | string | null | undefined;
}
type FuseOptionKey = FuseOptionKeyObject | string | string[];
interface KeyObject {
- path: string[];
- id: string;
- weight: number;
- src: string | string[];
- getFn?: ((obj: any) => ReadonlyArray | string | null | undefined) | null;
+ path: string[];
+ id: string;
+ weight: number;
+ src: string | string[];
+ getFn?: ((obj: any) => ReadonlyArray | string | null | undefined) | null;
}
type FuseGetFunction = (obj: T, path: string | string[]) => ReadonlyArray | string;
/**
@@ -23,140 +23,140 @@ type FuseGetFunction = (obj: T, path: string | string[]) => ReadonlyArray string[];
type FuseSortFunctionItem = {
- [key: string]: {
- $: string;
- } | {
- $: string;
- idx: number;
- }[];
+ [key: string]: {
+ $: string;
+ } | {
+ $: string;
+ idx: number;
+ }[];
};
type FuseSortFunctionMatch = {
- score: number;
- key: KeyObject;
- value: string;
- indices: ReadonlyArray[];
+ score: number;
+ key: KeyObject;
+ value: string;
+ indices: ReadonlyArray[];
};
type FuseSortFunctionMatchList = FuseSortFunctionMatch & {
- idx: number;
+ idx: number;
};
type FuseSortFunctionArg = {
- idx: number;
- item: FuseSortFunctionItem;
- score: number;
- matches?: (FuseSortFunctionMatch | FuseSortFunctionMatchList)[];
+ idx: number;
+ item: FuseSortFunctionItem;
+ score: number;
+ matches?: (FuseSortFunctionMatch | FuseSortFunctionMatchList)[];
};
type FuseSortFunction = (a: FuseSortFunctionArg, b: FuseSortFunctionArg) => number;
interface IFuseOptions {
- /** Indicates whether comparisons should be case sensitive. */
- isCaseSensitive?: boolean;
- /** Indicates whether comparisons should ignore diacritics (accents). */
- ignoreDiacritics?: boolean;
- /** Determines how close the match must be to the fuzzy location. */
- distance?: number;
- /** When true, the matching function will continue to the end of a search pattern even if a perfect match has already been located in the string. */
- findAllMatches?: boolean;
- /** The function to use to retrieve an object's value at the provided path. */
- getFn?: FuseGetFunction;
- /** When `true`, search will ignore `location` and `distance`. */
- ignoreLocation?: boolean;
- /** When `true`, the calculation for the relevance score will ignore the field-length norm. */
- ignoreFieldNorm?: boolean;
- /** Determines how much the field-length norm affects scoring. */
- fieldNormWeight?: number;
- /** Whether the matches should be included in the result set. */
- includeMatches?: boolean;
- /** Whether the score should be included in the result set. */
- includeScore?: boolean;
- /** List of keys that will be searched. */
- keys?: Array>;
- /** Determines approximately where in the text is the pattern expected to be found. */
- location?: number;
- /** Only the matches whose length exceeds this value will be returned. */
- minMatchCharLength?: number;
- /** Whether to sort the result list, by score. */
- shouldSort?: boolean;
- /** The function to use to sort all the results. */
- sortFn?: FuseSortFunction;
- /** At what point does the match algorithm give up. */
- threshold?: number;
- /** When `true`, it enables the use of unix-like search commands. */
- useExtendedSearch?: boolean;
- /** When `true`, enables token search with TF-IDF scoring. */
- useTokenSearch?: boolean;
- /**
- * Tokenizer used by `useTokenSearch`, applied identically at index-build
- * and query time. Accepts either a global `RegExp` or a function returning
- * `string[]`. Defaults to `/[\p{L}\p{M}\p{N}_]+/gu`, which handles CJK,
- * Cyrillic, Greek, Arabic, Hebrew, Devanagari, etc. out of the box. Use a
- * function form (e.g. wrapping `Intl.Segmenter`) for word-segmentation in
- * scripts without whitespace boundaries.
- */
- tokenize?: RegExp | FuseTokenizeFunction;
- /**
- * How the words of a multi-word query combine, for `useTokenSearch` only.
- * `'any'` (default) returns a record if it matches any query word (OR);
- * `'all'` returns it only when every query word matches somewhere in the
- * record — any field or array element (AND). Use `'all'` for filtering,
- * where adding a word should narrow the results. Has no effect unless
- * `useTokenSearch` is `true`.
- */
- tokenMatch?: 'all' | 'any';
+ /** Indicates whether comparisons should be case sensitive. */
+ isCaseSensitive?: boolean;
+ /** Indicates whether comparisons should ignore diacritics (accents). */
+ ignoreDiacritics?: boolean;
+ /** Determines how close the match must be to the fuzzy location. */
+ distance?: number;
+ /** When true, the matching function will continue to the end of a search pattern even if a perfect match has already been located in the string. */
+ findAllMatches?: boolean;
+ /** The function to use to retrieve an object's value at the provided path. */
+ getFn?: FuseGetFunction;
+ /** When `true`, search will ignore `location` and `distance`. */
+ ignoreLocation?: boolean;
+ /** When `true`, the calculation for the relevance score will ignore the field-length norm. */
+ ignoreFieldNorm?: boolean;
+ /** Determines how much the field-length norm affects scoring. */
+ fieldNormWeight?: number;
+ /** Whether the matches should be included in the result set. */
+ includeMatches?: boolean;
+ /** Whether the score should be included in the result set. */
+ includeScore?: boolean;
+ /** List of keys that will be searched. */
+ keys?: Array>;
+ /** Determines approximately where in the text is the pattern expected to be found. */
+ location?: number;
+ /** Only the matches whose length exceeds this value will be returned. */
+ minMatchCharLength?: number;
+ /** Whether to sort the result list, by score. */
+ shouldSort?: boolean;
+ /** The function to use to sort all the results. */
+ sortFn?: FuseSortFunction;
+ /** At what point does the match algorithm give up. */
+ threshold?: number;
+ /** When `true`, it enables the use of unix-like search commands. */
+ useExtendedSearch?: boolean;
+ /** When `true`, enables token search with TF-IDF scoring. */
+ useTokenSearch?: boolean;
+ /**
+ * Tokenizer used by `useTokenSearch`, applied identically at index-build
+ * and query time. Accepts either a global `RegExp` or a function returning
+ * `string[]`. Defaults to `/[\p{L}\p{M}\p{N}_]+/gu`, which handles CJK,
+ * Cyrillic, Greek, Arabic, Hebrew, Devanagari, etc. out of the box. Use a
+ * function form (e.g. wrapping `Intl.Segmenter`) for word-segmentation in
+ * scripts without whitespace boundaries.
+ */
+ tokenize?: RegExp | FuseTokenizeFunction;
+ /**
+ * How the words of a multi-word query combine, for `useTokenSearch` only.
+ * `'any'` (default) returns a record if it matches any query word (OR);
+ * `'all'` returns it only when every query word matches somewhere in the
+ * record — any field or array element (AND). Use `'all'` for filtering,
+ * where adding a word should narrow the results. Has no effect unless
+ * `useTokenSearch` is `true`.
+ */
+ tokenMatch?: 'all' | 'any';
}
interface FuseSearchOptions {
- limit?: number;
+ limit?: number;
}
interface FuseResultMatch {
- indices: ReadonlyArray;
- key?: string;
- refIndex?: number;
- value?: string;
+ indices: ReadonlyArray;
+ key?: string;
+ refIndex?: number;
+ value?: string;
}
interface FuseResult {
- item: T;
- refIndex: number;
- score?: number;
- matches?: ReadonlyArray;
+ item: T;
+ refIndex: number;
+ score?: number;
+ matches?: ReadonlyArray;
}
type Expression = string | {
- [key: string]: string;
+ [key: string]: string;
} | {
- $path: ReadonlyArray;
- $val: string;
+ $path: ReadonlyArray;
+ $val: string;
} | {
- $and?: Expression[];
+ $and?: Expression[];
} | {
- $or?: Expression[];
+ $or?: Expression[];
};
-
+//#endregion
+//#region src/workers/FuseWorker.d.ts
interface FuseWorkerOptions {
- /** Number of parallel workers. Defaults to navigator.hardwareConcurrency (max 8). */
- numWorkers?: number;
- /** Custom URL to the worker script. If not provided, resolves automatically via import.meta.url. */
- workerUrl?: string | URL;
+ /** Number of parallel workers. Defaults to navigator.hardwareConcurrency (max 8). */
+ numWorkers?: number;
+ /** Custom URL to the worker script. If not provided, resolves automatically via import.meta.url. */
+ workerUrl?: string | URL;
}
declare class FuseWorker {
- private _options;
- private _workerOptions;
- private _docs;
- private _shards;
- private _addCursor;
- private _initPromise;
- private _pending;
- private _nextId;
- private _workerUrl;
- constructor(docs: ReadonlyArray, options?: IFuseOptions, workerOptions?: FuseWorkerOptions);
- private static _assertNoFunctionOptions;
- private _getNumWorkers;
- private _ensureInit;
- private _spawnWorker;
- private _workerInitOptions;
- private _init;
- private _call;
- search(query: string | Expression, options?: FuseSearchOptions): Promise[]>;
- add(doc: T): Promise;
- setCollection(docs: ReadonlyArray): Promise;
- terminate(): void;
+ private _options;
+ private _workerOptions;
+ private _docs;
+ private _shards;
+ private _addCursor;
+ private _initPromise;
+ private _pending;
+ private _nextId;
+ private _workerUrl;
+ constructor(docs: ReadonlyArray, options?: IFuseOptions, workerOptions?: FuseWorkerOptions);
+ private static _assertNoFunctionOptions;
+ private _getNumWorkers;
+ private _ensureInit;
+ private _spawnWorker;
+ private _workerInitOptions;
+ private _init;
+ private _call;
+ search(query: string | Expression, options?: FuseSearchOptions): Promise[]>;
+ add(doc: T): Promise;
+ setCollection(docs: ReadonlyArray): Promise;
+ terminate(): void;
}
-
-export { FuseWorker };
-export type { FuseWorkerOptions };
+//#endregion
+export { FuseWorker, type FuseWorkerOptions };
\ No newline at end of file
diff --git a/dist/fuse-worker.mjs b/dist/fuse-worker.mjs
index 5be4cc041..5b541152a 100644
--- a/dist/fuse-worker.mjs
+++ b/dist/fuse-worker.mjs
@@ -6,249 +6,196 @@
*
* http://www.apache.org/licenses/LICENSE-2.0
*/
+//#region src/core/errorMessages.ts
+const FUSE_WORKER_UNSUPPORTED_FN_OPTION = (option) => `FuseWorker does not support function-valued option '${option}': functions cannot be transferred to Web Workers via postMessage. Remove this option or fall back to Fuse.`;
+const FUSE_WORKER_TOKEN_SEARCH_UNSUPPORTED = "FuseWorker does not support useTokenSearch: token search depends on corpus-level statistics (df, fieldCount) that are computed per shard, so per-shard scores would diverge from single-thread Fuse. Use Fuse on the main thread for token search.";
-const FUSE_WORKER_UNSUPPORTED_FN_OPTION = option => `FuseWorker does not support function-valued option '${option}': ` + `functions cannot be transferred to Web Workers via postMessage. ` + `Remove this option or fall back to Fuse.`;
-const FUSE_WORKER_TOKEN_SEARCH_UNSUPPORTED = `FuseWorker does not support useTokenSearch: token search depends on ` + `corpus-level statistics (df, fieldCount) that are computed per shard, ` + `so per-shard scores would diverge from single-thread Fuse. Use Fuse on ` + `the main thread for token search.`;
-
-///
-
+//#endregion
+//#region src/workers/FuseWorker.ts
const DEFAULT_MAX_WORKERS = 8;
function getDefaultWorkerCount() {
- const hw = typeof navigator !== 'undefined' ? navigator.hardwareConcurrency || 4 : 4;
- return Math.min(hw, DEFAULT_MAX_WORKERS);
+ const hw = typeof navigator !== "undefined" ? navigator.hardwareConcurrency || 4 : 4;
+ return Math.min(hw, DEFAULT_MAX_WORKERS);
}
-class FuseWorker {
- _shards = null;
- _addCursor = 0;
- _initPromise = null;
- _pending = new Map();
- _nextId = 0;
- constructor(docs, options, workerOptions) {
- this._docs = docs.slice();
- this._options = options || {};
- this._workerOptions = workerOptions || {};
- // Reject function-valued options eagerly. Without this check, postMessage
- // throws DataCloneError on first search() rather than at construction.
- FuseWorker._assertNoFunctionOptions(this._options);
- // Token search needs global corpus statistics, but each shard would build
- // its own — scores would diverge from single-thread Fuse. Refuse upfront
- // rather than silently returning ordering that doesn't match.
- if (this._options.useTokenSearch) {
- throw new Error(FUSE_WORKER_TOKEN_SEARCH_UNSUPPORTED);
- }
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore -- import.meta.url is resolved by Rollup at build time
- this._workerUrl = this._workerOptions.workerUrl || new URL('./fuse.worker.mjs', import.meta.url);
- }
- static _assertNoFunctionOptions(options) {
- if (typeof options.sortFn === 'function') {
- throw new Error(FUSE_WORKER_UNSUPPORTED_FN_OPTION('sortFn'));
- }
- if (typeof options.getFn === 'function') {
- throw new Error(FUSE_WORKER_UNSUPPORTED_FN_OPTION('getFn'));
- }
- if (typeof options.tokenize === 'function') {
- throw new Error(FUSE_WORKER_UNSUPPORTED_FN_OPTION('tokenize'));
- }
- const keys = options.keys;
- if (Array.isArray(keys)) {
- for (let i = 0, len = keys.length; i < len; i += 1) {
- const key = keys[i];
- if (key && typeof key === 'object' && !Array.isArray(key)) {
- if (typeof key.getFn === 'function') {
- const name = key.name;
- const label = Array.isArray(name) ? name.join('.') : name ?? String(i);
- throw new Error(FUSE_WORKER_UNSUPPORTED_FN_OPTION(`keys[${label}].getFn`));
- }
- }
- }
- }
- }
- _getNumWorkers() {
- return this._workerOptions.numWorkers || getDefaultWorkerCount();
- }
- _ensureInit() {
- if (this._initPromise) return this._initPromise;
- this._initPromise = this._init();
- return this._initPromise;
- }
- _spawnWorker() {
- const worker = new Worker(this._workerUrl, {
- type: 'module'
- });
- worker.onmessage = e => {
- const {
- id,
- result,
- error
- } = e.data;
- const handler = this._pending.get(id);
- if (!handler) return;
- this._pending.delete(id);
- if (error) {
- handler.reject(new Error(error));
- } else {
- handler.resolve(result);
- }
- };
- worker.onerror = e => {
- for (const [, handler] of this._pending) {
- handler.reject(new Error(e.message));
- }
- };
- return worker;
- }
- _workerInitOptions() {
- // Force includeScore so the main thread can do a global (score, idx)
- // tie-break across shards. Score is stripped from the final result if the
- // caller didn't ask for it.
- return {
- ...this._options,
- includeScore: true
- };
- }
- async _init() {
- const numWorkers = this._getNumWorkers();
- const chunkSize = Math.ceil(this._docs.length / numWorkers);
- this._shards = [];
- this._addCursor = 0;
- const workerInitOptions = this._workerInitOptions();
- const initPromises = [];
- for (let i = 0; i < numWorkers; i++) {
- const start = i * chunkSize;
- const end = Math.min(start + chunkSize, this._docs.length);
- const chunk = this._docs.slice(start, end);
- const globalIndices = [];
- for (let j = start; j < end; j += 1) globalIndices.push(j);
- const shard = {
- worker: this._spawnWorker(),
- globalIndices
- };
- this._shards.push(shard);
- initPromises.push(this._call(shard.worker, 'init', [chunk, workerInitOptions]));
- }
- await Promise.all(initPromises);
- }
- _call(worker, method, args) {
- const id = this._nextId++;
- return new Promise((resolve, reject) => {
- this._pending.set(id, {
- resolve,
- reject
- });
- worker.postMessage({
- id,
- method,
- args
- });
- });
- }
- async search(query, options) {
- await this._ensureInit();
- const shards = this._shards;
- const results = await Promise.all(shards.map(s => this._call(s.worker, 'search', [query, options])));
-
- // Merge results from all shards, rewriting refIndex from shard-local to global
- const merged = [];
- for (let i = 0, len = results.length; i < len; i += 1) {
- const {
- globalIndices
- } = shards[i];
- for (const r of results[i]) {
- merged.push({
- ...r,
- refIndex: globalIndices[r.refIndex]
- });
- }
- }
- const shouldSort = this._options.shouldSort !== false;
- if (shouldSort) {
- // Mirror Fuse's default sortFn: (score, idx) tie-break, but on the
- // GLOBAL refIndex so equal-score results across shards collapse into a
- // deterministic order matching single-thread Fuse.
- merged.sort((a, b) => {
- const sa = a.score ?? 0;
- const sb = b.score ?? 0;
- if (sa === sb) {
- return a.refIndex < b.refIndex ? -1 : 1;
- }
- return sa < sb ? -1 : 1;
- });
- } else {
- // Restore global collection order. Round-robin add() and
- // shard-concatenation order otherwise leak through.
- merged.sort((a, b) => a.refIndex - b.refIndex);
- }
-
- // Workers always include score so the merge above can tie-break; strip it
- // here if the caller didn't ask for it. Rebuild the object instead of
- // `delete`-ing — `delete` deopts the shape and roughly doubles the
- // post-sort cost on large result sets.
- if (!this._options.includeScore) {
- for (let i = 0, len = merged.length; i < len; i += 1) {
- const r = merged[i];
- merged[i] = r.matches !== undefined ? {
- item: r.item,
- refIndex: r.refIndex,
- matches: r.matches
- } : {
- item: r.item,
- refIndex: r.refIndex
- };
- }
- }
- const limit = options?.limit;
- if (limit && limit > 0) {
- return merged.slice(0, limit);
- }
- return merged;
- }
- async add(doc) {
- await this._ensureInit();
- const shards = this._shards;
- const shard = shards[this._addCursor % shards.length];
- this._addCursor += 1;
- const globalIdx = this._docs.length;
- this._docs.push(doc);
- shard.globalIndices.push(globalIdx);
- await this._call(shard.worker, 'add', [doc]);
- }
- async setCollection(docs) {
- this._docs = docs.slice();
- if (!this._shards) {
- this._initPromise = null;
- return;
- }
- const shards = this._shards;
- const chunkSize = Math.ceil(this._docs.length / shards.length);
- this._addCursor = 0;
- const tasks = [];
- for (let i = 0, len = shards.length; i < len; i += 1) {
- const start = i * chunkSize;
- const end = Math.min(start + chunkSize, this._docs.length);
- const chunk = this._docs.slice(start, end);
- const globalIndices = [];
- for (let j = start; j < end; j += 1) globalIndices.push(j);
- shards[i].globalIndices = globalIndices;
- tasks.push(this._call(shards[i].worker, 'setCollection', [chunk]));
- }
- await Promise.all(tasks);
- }
- terminate() {
- if (this._shards) {
- for (const {
- worker
- } of this._shards) {
- worker.terminate();
- }
- this._shards = null;
- }
- this._initPromise = null;
- for (const [, handler] of this._pending) {
- handler.reject(new Error('FuseWorker terminated'));
- }
- this._pending.clear();
- }
+const browserWorkerBase = typeof document !== "undefined" ? document.currentScript?.src || new URL("fuse-worker.cjs", document.baseURI).href : void 0;
+function resolveDefaultWorkerUrl() {
+ try {
+ return new URL("./fuse.worker.mjs", import.meta.url);
+ } catch {
+ if (browserWorkerBase !== void 0) return new URL("./fuse.worker.mjs", browserWorkerBase);
+ throw new Error("FuseWorker: unable to resolve the worker script URL automatically; pass workerOptions.workerUrl explicitly.");
+ }
}
+var FuseWorker = class FuseWorker {
+ constructor(docs, options, workerOptions) {
+ this._shards = null;
+ this._addCursor = 0;
+ this._initPromise = null;
+ this._pending = /* @__PURE__ */ new Map();
+ this._nextId = 0;
+ this._docs = docs.slice();
+ this._options = options || {};
+ this._workerOptions = workerOptions || {};
+ FuseWorker._assertNoFunctionOptions(this._options);
+ if (this._options.useTokenSearch) throw new Error(FUSE_WORKER_TOKEN_SEARCH_UNSUPPORTED);
+ this._workerUrl = this._workerOptions.workerUrl || resolveDefaultWorkerUrl();
+ }
+ static _assertNoFunctionOptions(options) {
+ if (typeof options.sortFn === "function") throw new Error(FUSE_WORKER_UNSUPPORTED_FN_OPTION("sortFn"));
+ if (typeof options.getFn === "function") throw new Error(FUSE_WORKER_UNSUPPORTED_FN_OPTION("getFn"));
+ if (typeof options.tokenize === "function") throw new Error(FUSE_WORKER_UNSUPPORTED_FN_OPTION("tokenize"));
+ const keys = options.keys;
+ if (Array.isArray(keys)) for (let i = 0, len = keys.length; i < len; i += 1) {
+ const key = keys[i];
+ if (key && typeof key === "object" && !Array.isArray(key)) {
+ if (typeof key.getFn === "function") {
+ const name = key.name;
+ const label = Array.isArray(name) ? name.join(".") : name ?? String(i);
+ throw new Error(FUSE_WORKER_UNSUPPORTED_FN_OPTION(`keys[${label}].getFn`));
+ }
+ }
+ }
+ }
+ _getNumWorkers() {
+ return this._workerOptions.numWorkers || getDefaultWorkerCount();
+ }
+ _ensureInit() {
+ if (this._initPromise) return this._initPromise;
+ this._initPromise = this._init();
+ return this._initPromise;
+ }
+ _spawnWorker() {
+ const worker = new Worker(this._workerUrl, { type: "module" });
+ worker.onmessage = (e) => {
+ const { id, result, error } = e.data;
+ const handler = this._pending.get(id);
+ if (!handler) return;
+ this._pending.delete(id);
+ if (error) handler.reject(new Error(error));
+ else handler.resolve(result);
+ };
+ worker.onerror = (e) => {
+ for (const [, handler] of this._pending) handler.reject(new Error(e.message));
+ };
+ return worker;
+ }
+ _workerInitOptions() {
+ return {
+ ...this._options,
+ includeScore: true
+ };
+ }
+ async _init() {
+ const numWorkers = this._getNumWorkers();
+ const chunkSize = Math.ceil(this._docs.length / numWorkers);
+ this._shards = [];
+ this._addCursor = 0;
+ const workerInitOptions = this._workerInitOptions();
+ const initPromises = [];
+ for (let i = 0; i < numWorkers; i++) {
+ const start = i * chunkSize;
+ const end = Math.min(start + chunkSize, this._docs.length);
+ const chunk = this._docs.slice(start, end);
+ const globalIndices = [];
+ for (let j = start; j < end; j += 1) globalIndices.push(j);
+ const shard = {
+ worker: this._spawnWorker(),
+ globalIndices
+ };
+ this._shards.push(shard);
+ initPromises.push(this._call(shard.worker, "init", [chunk, workerInitOptions]));
+ }
+ await Promise.all(initPromises);
+ }
+ _call(worker, method, args) {
+ const id = this._nextId++;
+ return new Promise((resolve, reject) => {
+ this._pending.set(id, {
+ resolve,
+ reject
+ });
+ worker.postMessage({
+ id,
+ method,
+ args
+ });
+ });
+ }
+ async search(query, options) {
+ await this._ensureInit();
+ const shards = this._shards;
+ const results = await Promise.all(shards.map((s) => this._call(s.worker, "search", [query, options])));
+ const merged = [];
+ for (let i = 0, len = results.length; i < len; i += 1) {
+ const { globalIndices } = shards[i];
+ for (const r of results[i]) merged.push({
+ ...r,
+ refIndex: globalIndices[r.refIndex]
+ });
+ }
+ if (this._options.shouldSort !== false) merged.sort((a, b) => {
+ const sa = a.score ?? 0;
+ const sb = b.score ?? 0;
+ if (sa === sb) return a.refIndex < b.refIndex ? -1 : 1;
+ return sa < sb ? -1 : 1;
+ });
+ else merged.sort((a, b) => a.refIndex - b.refIndex);
+ if (!this._options.includeScore) for (let i = 0, len = merged.length; i < len; i += 1) {
+ const r = merged[i];
+ merged[i] = r.matches !== void 0 ? {
+ item: r.item,
+ refIndex: r.refIndex,
+ matches: r.matches
+ } : {
+ item: r.item,
+ refIndex: r.refIndex
+ };
+ }
+ const limit = options?.limit;
+ if (limit && limit > 0) return merged.slice(0, limit);
+ return merged;
+ }
+ async add(doc) {
+ await this._ensureInit();
+ const shards = this._shards;
+ const shard = shards[this._addCursor % shards.length];
+ this._addCursor += 1;
+ const globalIdx = this._docs.length;
+ this._docs.push(doc);
+ shard.globalIndices.push(globalIdx);
+ await this._call(shard.worker, "add", [doc]);
+ }
+ async setCollection(docs) {
+ this._docs = docs.slice();
+ if (!this._shards) {
+ this._initPromise = null;
+ return;
+ }
+ const shards = this._shards;
+ const chunkSize = Math.ceil(this._docs.length / shards.length);
+ this._addCursor = 0;
+ const tasks = [];
+ for (let i = 0, len = shards.length; i < len; i += 1) {
+ const start = i * chunkSize;
+ const end = Math.min(start + chunkSize, this._docs.length);
+ const chunk = this._docs.slice(start, end);
+ const globalIndices = [];
+ for (let j = start; j < end; j += 1) globalIndices.push(j);
+ shards[i].globalIndices = globalIndices;
+ tasks.push(this._call(shards[i].worker, "setCollection", [chunk]));
+ }
+ await Promise.all(tasks);
+ }
+ terminate() {
+ if (this._shards) {
+ for (const { worker } of this._shards) worker.terminate();
+ this._shards = null;
+ }
+ this._initPromise = null;
+ for (const [, handler] of this._pending) handler.reject(/* @__PURE__ */ new Error("FuseWorker terminated"));
+ this._pending.clear();
+ }
+};
-export { FuseWorker };
+//#endregion
+export { FuseWorker };
\ No newline at end of file
diff --git a/dist/fuse.basic.cjs b/dist/fuse.basic.cjs
index 895f0d995..5bcdfc96c 100644
--- a/dist/fuse.basic.cjs
+++ b/dist/fuse.basic.cjs
@@ -7,1677 +7,1178 @@
* http://www.apache.org/licenses/LICENSE-2.0
*/
-'use strict';
-
+//#region src/helpers/typeGuards.ts
function isArray(value) {
- return !Array.isArray ? getTag(value) === '[object Array]' : Array.isArray(value);
+ return !Array.isArray ? getTag(value) === "[object Array]" : Array.isArray(value);
}
function baseToString(value) {
- // Exit early for strings to avoid a performance hit in some environments.
- if (typeof value == 'string') {
- return value;
- }
- if (typeof value === 'bigint') {
- return value.toString();
- }
- const result = value + '';
- return result == '0' && 1 / value == -Infinity ? '-0' : result;
+ if (typeof value == "string") return value;
+ if (typeof value === "bigint") return value.toString();
+ const result = value + "";
+ return result == "0" && 1 / value == -Infinity ? "-0" : result;
}
function toString(value) {
- return value == null ? '' : baseToString(value);
+ return value == null ? "" : baseToString(value);
}
function isString(value) {
- return typeof value === 'string';
+ return typeof value === "string";
}
function isNumber(value) {
- return typeof value === 'number';
+ return typeof value === "number";
}
-
-// Adapted from: https://github.com/lodash/lodash/blob/master/isBoolean.js
function isBoolean(value) {
- return value === true || value === false || isObjectLike(value) && getTag(value) == '[object Boolean]';
+ return value === true || value === false || isObjectLike(value) && getTag(value) == "[object Boolean]";
}
function isObject(value) {
- return typeof value === 'object';
+ return typeof value === "object";
}
-
-// Checks if `value` is object-like.
function isObjectLike(value) {
- return isObject(value) && value !== null;
+ return isObject(value) && value !== null;
}
function isDefined(value) {
- return value !== undefined && value !== null;
+ return value !== void 0 && value !== null;
}
function isBlank(value) {
- return !value.trim().length;
+ return !value.trim().length;
}
-
-// Gets the `toStringTag` of `value`.
-// Adapted from: https://github.com/lodash/lodash/blob/master/.internal/getTag.js
function getTag(value) {
- return value == null ? value === undefined ? '[object Undefined]' : '[object Null]' : Object.prototype.toString.call(value);
+ return value == null ? value === void 0 ? "[object Undefined]" : "[object Null]" : Object.prototype.toString.call(value);
}
-const EXTENDED_SEARCH_UNAVAILABLE = 'Extended search is not available';
-const LOGICAL_SEARCH_UNAVAILABLE = 'Logical search is not available';
-const TOKEN_SEARCH_UNAVAILABLE = 'Token search is not available';
+//#endregion
+//#region src/core/errorMessages.ts
+const EXTENDED_SEARCH_UNAVAILABLE = "Extended search is not available";
+const LOGICAL_SEARCH_UNAVAILABLE = "Logical search is not available";
+const TOKEN_SEARCH_UNAVAILABLE = "Token search is not available";
const INCORRECT_INDEX_TYPE = "Incorrect 'index' type";
-const INVALID_DOC_INDEX = 'Invalid doc index: must be a non-negative integer within the bounds of the docs array';
-const LOGICAL_SEARCH_INVALID_QUERY_FOR_KEY = key => `Invalid value for key ${key}`;
-const PATTERN_LENGTH_TOO_LARGE = max => `Pattern length exceeds max of ${max}.`;
-const MISSING_KEY_PROPERTY = name => `Missing ${name} property in key`;
-const INVALID_KEY_WEIGHT_VALUE = key => `Property 'weight' in key '${key}' must be a positive integer`;
-const FUSE_MATCH_TOKEN_SEARCH_UNSUPPORTED = `Fuse.match does not support useTokenSearch: token search requires ` + `corpus-level statistics (df, fieldCount) that a one-off string ` + `comparison does not have. Use new Fuse(...).search(...) instead.`;
-
+const INVALID_DOC_INDEX = "Invalid doc index: must be a non-negative integer within the bounds of the docs array";
+const LOGICAL_SEARCH_INVALID_QUERY_FOR_KEY = (key) => `Invalid value for key ${key}`;
+const PATTERN_LENGTH_TOO_LARGE = (max) => `Pattern length exceeds max of ${max}.`;
+const MISSING_KEY_PROPERTY = (name) => `Missing ${name} property in key`;
+const INVALID_KEY_WEIGHT_VALUE = (key) => `Property 'weight' in key '${key}' must be a positive integer`;
+const FUSE_MATCH_TOKEN_SEARCH_UNSUPPORTED = "Fuse.match does not support useTokenSearch: token search requires corpus-level statistics (df, fieldCount) that a one-off string comparison does not have. Use new Fuse(...).search(...) instead.";
+
+//#endregion
+//#region src/tools/KeyStore.ts
const hasOwn = Object.prototype.hasOwnProperty;
-class KeyStore {
- constructor(keys) {
- this._keys = [];
- this._keyMap = {};
- let totalWeight = 0;
- keys.forEach(key => {
- const obj = createKey(key);
- this._keys.push(obj);
- this._keyMap[obj.id] = obj;
- totalWeight += obj.weight;
- });
-
- // Normalize weights so that their sum is equal to 1
- this._keys.forEach(key => {
- key.weight /= totalWeight;
- });
- }
- get(keyId) {
- return this._keyMap[keyId];
- }
- keys() {
- return this._keys;
- }
- toJSON() {
- return JSON.stringify(this._keys);
- }
-}
+var KeyStore = class {
+ constructor(keys) {
+ this._keys = [];
+ this._keyMap = {};
+ let totalWeight = 0;
+ keys.forEach((key) => {
+ const obj = createKey(key);
+ this._keys.push(obj);
+ this._keyMap[obj.id] = obj;
+ totalWeight += obj.weight;
+ });
+ this._keys.forEach((key) => {
+ key.weight /= totalWeight;
+ });
+ }
+ get(keyId) {
+ return this._keyMap[keyId];
+ }
+ keys() {
+ return this._keys;
+ }
+ toJSON() {
+ return JSON.stringify(this._keys);
+ }
+};
function createKey(key) {
- let path = null;
- let id = null;
- let src = null;
- let weight = 1;
- let getFn = null;
- if (isString(key) || isArray(key)) {
- src = key;
- path = createKeyPath(key);
- id = createKeyId(key);
- } else {
- if (!hasOwn.call(key, 'name')) {
- throw new Error(MISSING_KEY_PROPERTY('name'));
- }
- const name = key.name;
- src = name;
- if (hasOwn.call(key, 'weight') && key.weight !== undefined) {
- weight = key.weight;
- if (weight <= 0) {
- throw new Error(INVALID_KEY_WEIGHT_VALUE(createKeyId(name)));
- }
- }
- path = createKeyPath(name);
- id = createKeyId(name);
- getFn = key.getFn ?? null;
- }
- return {
- path: path,
- id: id,
- weight,
- src: src,
- getFn
- };
+ let path = null;
+ let id = null;
+ let src = null;
+ let weight = 1;
+ let getFn = null;
+ if (isString(key) || isArray(key)) {
+ src = key;
+ path = createKeyPath(key);
+ id = createKeyId(key);
+ } else {
+ if (!hasOwn.call(key, "name")) throw new Error(MISSING_KEY_PROPERTY("name"));
+ const name = key.name;
+ src = name;
+ if (hasOwn.call(key, "weight") && key.weight !== void 0) {
+ weight = key.weight;
+ if (weight <= 0) throw new Error(INVALID_KEY_WEIGHT_VALUE(createKeyId(name)));
+ }
+ path = createKeyPath(name);
+ id = createKeyId(name);
+ getFn = key.getFn ?? null;
+ }
+ return {
+ path,
+ id,
+ weight,
+ src,
+ getFn
+ };
}
function createKeyPath(key) {
- return isArray(key) ? key : key.split('.');
+ return isArray(key) ? key : key.split(".");
}
function createKeyId(key) {
- return isArray(key) ? key.join('.') : key;
+ return isArray(key) ? key.join(".") : key;
}
+//#endregion
+//#region src/helpers/get.ts
function get(obj, path) {
- const list = [];
- let arr = false;
- const deepGet = (obj, path, index, arrayIndex) => {
- if (!isDefined(obj)) {
- return;
- }
- if (!path[index]) {
- // If there's no path left, we've arrived at the object we care about.
- list.push(arrayIndex !== undefined ? {
- v: obj,
- i: arrayIndex
- } : obj);
- } else {
- const key = path[index];
- const value = obj[key];
- if (!isDefined(value)) {
- return;
- }
-
- // If we're at the last value in the path, and if it's a string/number/bool,
- // add it to the list
- if (index === path.length - 1 && (isString(value) || isNumber(value) || isBoolean(value) || typeof value === 'bigint')) {
- list.push(arrayIndex !== undefined ? {
- v: toString(value),
- i: arrayIndex
- } : toString(value));
- } else if (isArray(value)) {
- arr = true;
- // Search each item in the array.
- for (let i = 0, len = value.length; i < len; i += 1) {
- deepGet(value[i], path, index + 1, i);
- }
- } else if (path.length) {
- // An object. Recurse further.
- deepGet(value, path, index + 1, arrayIndex);
- }
- }
- };
-
- // Backwards compatibility (since path used to be a string)
- deepGet(obj, isString(path) ? path.split('.') : path, 0);
- return arr ? list : list[0];
+ const list = [];
+ let arr = false;
+ const deepGet = (obj, path, index, arrayIndex) => {
+ if (!isDefined(obj)) return;
+ if (!path[index]) list.push(arrayIndex !== void 0 ? {
+ v: obj,
+ i: arrayIndex
+ } : obj);
+ else {
+ const value = obj[path[index]];
+ if (!isDefined(value)) return;
+ if (index === path.length - 1 && (isString(value) || isNumber(value) || isBoolean(value) || typeof value === "bigint")) list.push(arrayIndex !== void 0 ? {
+ v: toString(value),
+ i: arrayIndex
+ } : toString(value));
+ else if (isArray(value)) {
+ arr = true;
+ for (let i = 0, len = value.length; i < len; i += 1) deepGet(value[i], path, index + 1, i);
+ } else if (path.length) deepGet(value, path, index + 1, arrayIndex);
+ }
+ };
+ deepGet(obj, isString(path) ? path.split(".") : path, 0);
+ return arr ? list : list[0];
}
+//#endregion
+//#region src/core/config.ts
const MatchOptions = {
- includeMatches: false,
- findAllMatches: false,
- minMatchCharLength: 1
+ includeMatches: false,
+ findAllMatches: false,
+ minMatchCharLength: 1
};
const BasicOptions = {
- isCaseSensitive: false,
- ignoreDiacritics: false,
- includeScore: false,
- keys: [],
- shouldSort: true,
- sortFn: (a, b) => a.score === b.score ? a.idx < b.idx ? -1 : 1 : a.score < b.score ? -1 : 1
+ isCaseSensitive: false,
+ ignoreDiacritics: false,
+ includeScore: false,
+ keys: [],
+ shouldSort: true,
+ sortFn: (a, b) => a.score === b.score ? a.idx < b.idx ? -1 : 1 : a.score < b.score ? -1 : 1
};
const FuzzyOptions = {
- location: 0,
- threshold: 0.6,
- distance: 100
+ location: 0,
+ threshold: .6,
+ distance: 100
};
const AdvancedOptions = {
- useExtendedSearch: false,
- useTokenSearch: false,
- tokenize: undefined,
- tokenMatch: 'any',
- getFn: get,
- ignoreLocation: false,
- ignoreFieldNorm: false,
- fieldNormWeight: 1
+ useExtendedSearch: false,
+ useTokenSearch: false,
+ tokenize: void 0,
+ tokenMatch: "any",
+ getFn: get,
+ ignoreLocation: false,
+ ignoreFieldNorm: false,
+ fieldNormWeight: 1
};
const Config = Object.freeze({
- ...BasicOptions,
- ...MatchOptions,
- ...FuzzyOptions,
- ...AdvancedOptions
+ ...BasicOptions,
+ ...MatchOptions,
+ ...FuzzyOptions,
+ ...AdvancedOptions
});
-// Field-length norm: the shorter the field, the higher the weight.
-// Set to 3 decimals to reduce index size.
+//#endregion
+//#region src/tools/fieldNorm.ts
function norm(weight = 1, mantissa = 3) {
- const cache = new Map();
- const m = Math.pow(10, mantissa);
- return {
- get(value) {
- // Count words by counting space transitions — avoids allocating a regex match array
- let numTokens = 1;
- let inSpace = false;
- for (let i = 0; i < value.length; i++) {
- if (value.charCodeAt(i) === 32) {
- if (!inSpace) {
- numTokens++;
- inSpace = true;
- }
- } else {
- inSpace = false;
- }
- }
- if (cache.has(numTokens)) {
- return cache.get(numTokens);
- }
-
- // Default function is 1/sqrt(x), weight makes that variable
- const n = Math.round(m / Math.pow(numTokens, 0.5 * weight)) / m;
- cache.set(numTokens, n);
- return n;
- },
- clear() {
- cache.clear();
- }
- };
+ const cache = /* @__PURE__ */ new Map();
+ const m = Math.pow(10, mantissa);
+ return {
+ get(value) {
+ let numTokens = 1;
+ let inSpace = false;
+ for (let i = 0; i < value.length; i++) if (value.charCodeAt(i) === 32) {
+ if (!inSpace) {
+ numTokens++;
+ inSpace = true;
+ }
+ } else inSpace = false;
+ if (cache.has(numTokens)) return cache.get(numTokens);
+ const n = Math.round(m / Math.pow(numTokens, .5 * weight)) / m;
+ cache.set(numTokens, n);
+ return n;
+ },
+ clear() {
+ cache.clear();
+ }
+ };
}
-class FuseIndex {
- constructor({
- getFn = Config.getFn,
- fieldNormWeight = Config.fieldNormWeight
- } = {}) {
- this.norm = norm(fieldNormWeight, 3);
- this.getFn = getFn;
- this.isCreated = false;
- this.docs = [];
- this.keys = [];
- this._keysMap = {};
- this.setIndexRecords();
- }
- setSources(docs = []) {
- this.docs = docs;
- }
- setIndexRecords(records = []) {
- this.records = records;
- }
- setKeys(keys = []) {
- this.keys = keys;
- this._keysMap = {};
- keys.forEach((key, idx) => {
- this._keysMap[key.id] = idx;
- });
- }
- create() {
- if (this.isCreated || !this.docs.length) {
- return;
- }
- this.isCreated = true;
- const len = this.docs.length;
- this.records = new Array(len);
- let recordCount = 0;
-
- // List is Array
- if (isString(this.docs[0])) {
- for (let i = 0; i < len; i++) {
- const record = this._createStringRecord(this.docs[i], i);
- if (record) {
- this.records[recordCount++] = record;
- }
- }
- } else {
- // List is Array