From 9cae72bd4d543b51b767ce8a9991d87de0b2ebd7 Mon Sep 17 00:00:00 2001 From: sergioperezcheco Date: Sat, 10 Oct 2026 09:45:06 +0800 Subject: [PATCH] fix(workers): ignore nullish document additions Assisted-by: Hermes AI agents Signed-off-by: sergioperezcheco --- dist/fuse-worker.cjs | 7 ++++++ dist/fuse-worker.mjs | 7 ++++++ src/workers/FuseWorker.ts | 6 ++++++ test/fuse-worker.test.js | 45 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+) diff --git a/dist/fuse-worker.cjs b/dist/fuse-worker.cjs index 05f6f32f6..455bf7543 100644 --- a/dist/fuse-worker.cjs +++ b/dist/fuse-worker.cjs @@ -12,6 +12,12 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 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/helpers/typeGuards.ts +function isDefined(value) { + return value !== void 0 && value !== null; +} + //#endregion //#region src/workers/FuseWorker.ts const DEFAULT_MAX_WORKERS = 8; @@ -159,6 +165,7 @@ var FuseWorker = class FuseWorker { return merged; } async add(doc) { + if (!isDefined(doc)) return; await this._ensureInit(); const shards = this._shards; const shard = shards[this._addCursor % shards.length]; diff --git a/dist/fuse-worker.mjs b/dist/fuse-worker.mjs index 1a6ddc4ec..31e51968e 100644 --- a/dist/fuse-worker.mjs +++ b/dist/fuse-worker.mjs @@ -10,6 +10,12 @@ 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/helpers/typeGuards.ts +function isDefined(value) { + return value !== void 0 && value !== null; +} + //#endregion //#region src/workers/FuseWorker.ts const DEFAULT_MAX_WORKERS = 8; @@ -156,6 +162,7 @@ var FuseWorker = class FuseWorker { return merged; } async add(doc) { + if (!isDefined(doc)) return; await this._ensureInit(); const shards = this._shards; const shard = shards[this._addCursor % shards.length]; diff --git a/src/workers/FuseWorker.ts b/src/workers/FuseWorker.ts index 428327eb7..43532eac6 100644 --- a/src/workers/FuseWorker.ts +++ b/src/workers/FuseWorker.ts @@ -1,6 +1,7 @@ /// import * as ErrorMsg from '../core/errorMessages' +import { isDefined } from '../helpers/typeGuards' import type { IFuseOptions, FuseOptionKey, @@ -278,6 +279,11 @@ export default class FuseWorker { } async add(doc: T): Promise { + // Match Fuse.add's no-op before advancing either global or shard indices. + if (!isDefined(doc)) { + return + } + await this._ensureInit() const shards = this._shards! diff --git a/test/fuse-worker.test.js b/test/fuse-worker.test.js index 7d1ddbe6e..09bdbbfff 100644 --- a/test/fuse-worker.test.js +++ b/test/fuse-worker.test.js @@ -124,6 +124,51 @@ describe('FuseWorker sharding', () => { fw.terminate() }) + test.each([null, undefined])( + 'add(%s) before initialization does not spawn workers', + async (doc) => { + MockWorker.instances = [] + const fw = new FuseWorker(['alpha'], {}, { numWorkers: 2 }) + await fw.add(doc) + expect(MockWorker.instances).toHaveLength(0) + expect(await fw.search('')).toEqual([{ item: 'alpha', refIndex: 0 }]) + fw.terminate() + } + ) + + test.each([1, 3])( + 'nullish additions preserve single-thread results with %s workers', + async (numWorkers) => { + const docs = ['alpha', 'beta', 'gamma'] + const options = { includeMatches: true, includeScore: true, threshold: 0 } + const fw = new FuseWorker(docs, options, { numWorkers }) + const fuse = new Fuse(docs.slice(), options) + await fw.search('alpha') + for (const doc of [null, undefined, 'delta', null, 'epsilon']) { + await fw.add(doc) + fuse.add(doc) + } + for (const query of ['', 'delta', 'epsilon', 'alpha']) { + expect(await fw.search(query)).toEqual(fuse.search(query)) + } + fw.terminate() + } + ) + + test('falsy but defined additions are not skipped', async () => { + const docs = [{ name: 'alpha' }] + const options = { keys: ['name'], threshold: 0 } + const fw = new FuseWorker(docs, options, { numWorkers: 3 }) + const fuse = new Fuse(docs.slice(), options) + for (const doc of [null, '', 0, false, { name: 'beta' }]) { + await fw.add(doc) + fuse.add(doc) + } + expect(await fw.search('')).toEqual(fuse.search('')) + expect(await fw.search('beta')).toEqual(fuse.search('beta')) + fw.terminate() + }) + test('interleaved search/add/setCollection keeps refIndex consistent', async () => { MockWorker.instances = [] const initial = Books.slice(0, 4)