204 lines
No EOL
7.1 KiB
JavaScript
204 lines
No EOL
7.1 KiB
JavaScript
/**
|
|
* Fuse.js v7.5.0 - Lightweight fuzzy-search (http://fusejs.io)
|
|
*
|
|
* Copyright (c) 2026 Kiro Risk (http://kiro.me)
|
|
* All Rights Reserved. Apache Software License 2.0
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*/
|
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
|
|
//#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 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();
|
|
}
|
|
};
|
|
|
|
//#endregion
|
|
exports.FuseWorker = FuseWorker; |