@thyn-ai/fuse-mojo-core 0.1.7
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/NOTICE +16 -0
- package/index.cjs +147 -0
- package/index.d.ts +87 -0
- package/index.mjs +7 -0
- package/package.json +48 -0
- package/src/engine.cjs +314 -0
- package/src/native.cjs +374 -0
- package/src/options.cjs +242 -0
- package/vendor/fuse.basic.cjs +1342 -0
package/src/native.cjs
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* koffi loader for the fusemojo native kernel, with an ABI-version handshake.
|
|
5
|
+
*
|
|
6
|
+
* Resolution order:
|
|
7
|
+
*
|
|
8
|
+
* 1. `$FUSE_MOJO_NATIVE_LIB` (explicit path override, for development; the
|
|
9
|
+
* pthread shim is expected next to it)
|
|
10
|
+
* 2. the platform package (`@thyn-ai/fuse-mojo-<platform>-<arch>`) installed as an
|
|
11
|
+
* optional dependency of `@thyn-ai/fuse-mojo-core`
|
|
12
|
+
* 3. the repository development build output `kernels/fuse/build/`
|
|
13
|
+
*
|
|
14
|
+
* The pthread shim (`libfusemojoshim`) is loaded from the same directory as
|
|
15
|
+
* the kernel; without it, queries run sequentially on the calling thread
|
|
16
|
+
* (same results, less speed).
|
|
17
|
+
*
|
|
18
|
+
* If the kernel cannot be found, fails to load, or reports an ABI version
|
|
19
|
+
* this package does not understand, `NativeUnavailable` is thrown and the
|
|
20
|
+
* caller falls back to the vendored Fuse.js implementation. Set
|
|
21
|
+
* `FUSE_MOJO_DISABLE_NATIVE=1` to force that fallback (used by the
|
|
22
|
+
* differential test suite and on platforms without a prebuilt library).
|
|
23
|
+
* `FUSE_MOJO_THREADS=N` caps the native thread count (1 = sequential).
|
|
24
|
+
*
|
|
25
|
+
* Stable C ABI (v1)::
|
|
26
|
+
*
|
|
27
|
+
* int32_t fusemojo_abi_version(void)
|
|
28
|
+
* void* fusemojo_index_create(const uint16_t* chars, const int32_t* offsets,
|
|
29
|
+
* int32_t n_texts, int32_t location, double distance,
|
|
30
|
+
* double threshold, int32_t min_match_char_length,
|
|
31
|
+
* int32_t find_all_matches, int32_t ignore_location,
|
|
32
|
+
* int32_t compute_matches, int32_t include_matches)
|
|
33
|
+
* void* fusemojo_search_begin(void* handle, const uint16_t* pattern,
|
|
34
|
+
* int32_t pattern_len, int32_t location_offset,
|
|
35
|
+
* int32_t exact_check, double* out_scores,
|
|
36
|
+
* int32_t* out_is_match, int32_t* out_idx_offsets,
|
|
37
|
+
* int32_t n_jobs)
|
|
38
|
+
* int32_t fusemojo_search_range(void* ctx, int32_t job_id, int32_t start, int32_t end)
|
|
39
|
+
* int32_t fusemojo_search_end(void* ctx)
|
|
40
|
+
* void fusemojo_copy_indices(void* handle, int32_t* out_pairs)
|
|
41
|
+
* void fusemojo_index_destroy(void* handle)
|
|
42
|
+
* int32_t fusemojo_search_parallel(void* ctx, int32_t n_texts, int32_t max_threads) [shim]
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
const fs = require('node:fs')
|
|
46
|
+
const os = require('node:os')
|
|
47
|
+
const path = require('node:path')
|
|
48
|
+
|
|
49
|
+
// Must equal ABI_VERSION in kernels/fuse/src/fusemojo.mojo. A mismatch means
|
|
50
|
+
// the installed package and the resolved shared library disagree; fall back.
|
|
51
|
+
const ABI_VERSION = 1
|
|
52
|
+
|
|
53
|
+
const ENV_LIB = 'FUSE_MOJO_NATIVE_LIB'
|
|
54
|
+
const ENV_DISABLE = 'FUSE_MOJO_DISABLE_NATIVE'
|
|
55
|
+
const ENV_THREADS = 'FUSE_MOJO_THREADS'
|
|
56
|
+
const ENV_MIN_CHUNK = 'FUSE_MOJO_MIN_CHUNK'
|
|
57
|
+
|
|
58
|
+
class NativeUnavailable extends Error {
|
|
59
|
+
constructor(message) {
|
|
60
|
+
super(message)
|
|
61
|
+
this.name = 'NativeUnavailable'
|
|
62
|
+
this.code = 'FUSE_MOJO_NATIVE_UNAVAILABLE'
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function libBasename() {
|
|
67
|
+
if (process.platform === 'darwin') return 'libfusemojo.dylib'
|
|
68
|
+
if (process.platform === 'linux') return 'libfusemojo.so'
|
|
69
|
+
if (process.platform === 'win32') return 'fusemojo.dll' // no Mojo toolchain builds this today
|
|
70
|
+
return 'libfusemojo.so'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function shimBasename() {
|
|
74
|
+
if (process.platform === 'darwin') return 'libfusemojoshim.dylib'
|
|
75
|
+
if (process.platform === 'linux') return 'libfusemojoshim.so'
|
|
76
|
+
if (process.platform === 'win32') return 'fusemojoshim.dll'
|
|
77
|
+
return 'libfusemojoshim.so'
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function candidatePaths() {
|
|
81
|
+
/** [sourceLabel, absolutePath] candidates, in resolver order. */
|
|
82
|
+
const out = []
|
|
83
|
+
const override = process.env[ENV_LIB]
|
|
84
|
+
if (override) {
|
|
85
|
+
out.push([`env ${ENV_LIB}`, override])
|
|
86
|
+
}
|
|
87
|
+
// Platform package, resolved relative to this package so npm's nested or
|
|
88
|
+
// hoisted layouts both work.
|
|
89
|
+
const platformPkg = `@thyn-ai/fuse-mojo-${process.platform}-${process.arch}`
|
|
90
|
+
try {
|
|
91
|
+
const pkgJson = require.resolve(`${platformPkg}/package.json`, { paths: [__dirname] })
|
|
92
|
+
out.push([`platform package ${platformPkg}`, path.join(path.dirname(pkgJson), 'lib', libBasename())])
|
|
93
|
+
} catch {
|
|
94
|
+
// optional dependency not installed (unsupported platform or pruned)
|
|
95
|
+
}
|
|
96
|
+
out.push([
|
|
97
|
+
'repo-dev build output',
|
|
98
|
+
path.resolve(__dirname, '..', '..', '..', '..', '..', 'kernels', 'fuse', 'build', libBasename()),
|
|
99
|
+
])
|
|
100
|
+
return out
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function bindAbi(lib) {
|
|
104
|
+
// koffi parses C prototypes; TypedArrays passed to pointer parameters are
|
|
105
|
+
// pinned for the duration of the call.
|
|
106
|
+
return {
|
|
107
|
+
abi_version: lib.func('int32_t fusemojo_abi_version(void)'),
|
|
108
|
+
index_create: lib.func(`void* fusemojo_index_create(
|
|
109
|
+
const uint16_t* chars, const int32_t* offsets, int32_t n_texts,
|
|
110
|
+
int32_t location, double distance, double threshold,
|
|
111
|
+
int32_t min_match_char_length, int32_t find_all_matches,
|
|
112
|
+
int32_t ignore_location, int32_t compute_matches, int32_t include_matches)`),
|
|
113
|
+
search_begin: lib.func(`void* fusemojo_search_begin(
|
|
114
|
+
void* handle, const uint16_t* pattern, int32_t pattern_len,
|
|
115
|
+
int32_t location_offset, int32_t exact_check,
|
|
116
|
+
double* out_scores, int32_t* out_is_match, int32_t* out_idx_offsets,
|
|
117
|
+
int32_t n_jobs)`),
|
|
118
|
+
search_range: lib.func(
|
|
119
|
+
'int32_t fusemojo_search_range(void* ctx, int32_t job_id, int32_t start, int32_t end)'
|
|
120
|
+
),
|
|
121
|
+
search_end: lib.func('int32_t fusemojo_search_end(void* ctx)'),
|
|
122
|
+
copy_indices: lib.func('void fusemojo_copy_indices(void* handle, int32_t* out_pairs)'),
|
|
123
|
+
index_destroy: lib.func('void fusemojo_index_destroy(void* handle)'),
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let _koffi = null
|
|
128
|
+
let _lib = null
|
|
129
|
+
let _shim = null // null = not tried; false = unavailable; function = parallel runner
|
|
130
|
+
let _libSource = null
|
|
131
|
+
let _loadError = null
|
|
132
|
+
|
|
133
|
+
function loadKoffi() {
|
|
134
|
+
if (_koffi) return _koffi
|
|
135
|
+
try {
|
|
136
|
+
_koffi = require('koffi')
|
|
137
|
+
} catch (err) {
|
|
138
|
+
throw new NativeUnavailable(`koffi is not available: ${err.message}`)
|
|
139
|
+
}
|
|
140
|
+
return _koffi
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function threadCap() {
|
|
144
|
+
const raw = process.env[ENV_THREADS]
|
|
145
|
+
if (raw !== undefined) {
|
|
146
|
+
const n = Number.parseInt(raw, 10)
|
|
147
|
+
if (Number.isInteger(n) && n > 0) return n
|
|
148
|
+
}
|
|
149
|
+
const cpus = os.cpus().length || 1
|
|
150
|
+
return Math.min(cpus, 64)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function minChunk() {
|
|
154
|
+
const raw = process.env[ENV_MIN_CHUNK]
|
|
155
|
+
if (raw !== undefined) {
|
|
156
|
+
const n = Number.parseInt(raw, 10)
|
|
157
|
+
if (Number.isInteger(n) && n > 0) return n
|
|
158
|
+
}
|
|
159
|
+
return 2048
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function load() {
|
|
163
|
+
/** Resolve, dlopen, and ABI-handshake the native kernel. Never caches failure. */
|
|
164
|
+
if (process.env[ENV_DISABLE] === '1') {
|
|
165
|
+
throw new NativeUnavailable(`native kernel disabled by ${ENV_DISABLE}=1`)
|
|
166
|
+
}
|
|
167
|
+
if (_lib !== null) {
|
|
168
|
+
return _lib
|
|
169
|
+
}
|
|
170
|
+
const koffi = loadKoffi()
|
|
171
|
+
const errors = []
|
|
172
|
+
for (const [label, candidate] of candidatePaths()) {
|
|
173
|
+
let resolved
|
|
174
|
+
try {
|
|
175
|
+
if (!candidate || !fs.existsSync(candidate)) {
|
|
176
|
+
continue
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
resolved = koffi.load(candidate)
|
|
180
|
+
} catch (err) {
|
|
181
|
+
errors.push(`${label} (${candidate}): ${err.message}`)
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
let abi
|
|
185
|
+
try {
|
|
186
|
+
const bound = bindAbi(resolved)
|
|
187
|
+
abi = bound.abi_version()
|
|
188
|
+
if (abi !== ABI_VERSION) {
|
|
189
|
+
errors.push(`${label} (${candidate}): native ABI v${abi} != wrapper ABI v${ABI_VERSION}`)
|
|
190
|
+
continue
|
|
191
|
+
}
|
|
192
|
+
// The pthread shim is optional; without it queries run sequentially.
|
|
193
|
+
_shim = false
|
|
194
|
+
const shimPath = path.join(path.dirname(candidate), shimBasename())
|
|
195
|
+
try {
|
|
196
|
+
if (fs.existsSync(shimPath)) {
|
|
197
|
+
const shimLib = koffi.load(shimPath)
|
|
198
|
+
_shim = shimLib.func(
|
|
199
|
+
'int32_t fusemojo_search_parallel(void* ctx, int32_t n_texts, int32_t max_threads, int32_t min_chunk)'
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
} catch {
|
|
203
|
+
_shim = false
|
|
204
|
+
}
|
|
205
|
+
_lib = bound
|
|
206
|
+
_libSource = `${label} (${candidate})${_shim ? ' +pthread shim' : ''}`
|
|
207
|
+
_loadError = null
|
|
208
|
+
return bound
|
|
209
|
+
} catch (err) {
|
|
210
|
+
errors.push(`${label} (${candidate}): ABI not recognized: ${err.message}`)
|
|
211
|
+
continue
|
|
212
|
+
}
|
|
213
|
+
} catch (err) {
|
|
214
|
+
errors.push(`${label}: ${err.message}`)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
_loadError = errors.join('; ') || 'no native kernel found on any resolver path'
|
|
218
|
+
throw new NativeUnavailable(_loadError)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function nativeAvailable() {
|
|
222
|
+
/** True if the native kernel can score right now. Never throws. */
|
|
223
|
+
try {
|
|
224
|
+
load()
|
|
225
|
+
return true
|
|
226
|
+
} catch {
|
|
227
|
+
return false
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function backendInfo() {
|
|
232
|
+
/** Diagnostics for the active backend. Never throws. */
|
|
233
|
+
const info = {
|
|
234
|
+
native_available: false,
|
|
235
|
+
native_source: null,
|
|
236
|
+
abi_version_expected: ABI_VERSION,
|
|
237
|
+
abi_version_native: null,
|
|
238
|
+
disabled_by_env: process.env[ENV_DISABLE] === '1',
|
|
239
|
+
threads: 0,
|
|
240
|
+
platform: process.platform,
|
|
241
|
+
arch: process.arch,
|
|
242
|
+
error: null,
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
const lib = load()
|
|
246
|
+
info.native_available = true
|
|
247
|
+
info.native_source = _libSource
|
|
248
|
+
info.abi_version_native = lib.abi_version()
|
|
249
|
+
info.threads = threadCap()
|
|
250
|
+
} catch (err) {
|
|
251
|
+
info.error = err.message
|
|
252
|
+
}
|
|
253
|
+
return info
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const registry = new FinalizationRegistry((handle) => {
|
|
257
|
+
try {
|
|
258
|
+
if (_lib) _lib.index_destroy(handle)
|
|
259
|
+
} catch {
|
|
260
|
+
// best-effort cleanup during GC; never throw
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
class NativeIndex {
|
|
265
|
+
/** Owned handle to a native text-collection index. */
|
|
266
|
+
constructor(chars, offsets, options) {
|
|
267
|
+
const lib = load() // throws NativeUnavailable
|
|
268
|
+
const nTexts = offsets.length - 1
|
|
269
|
+
const handle = lib.index_create(
|
|
270
|
+
chars,
|
|
271
|
+
offsets,
|
|
272
|
+
nTexts,
|
|
273
|
+
options.location,
|
|
274
|
+
options.distance,
|
|
275
|
+
options.threshold,
|
|
276
|
+
options.minMatchCharLength,
|
|
277
|
+
options.findAllMatches ? 1 : 0,
|
|
278
|
+
options.ignoreLocation ? 1 : 0,
|
|
279
|
+
options.computeMatches ? 1 : 0,
|
|
280
|
+
options.includeMatches ? 1 : 0
|
|
281
|
+
)
|
|
282
|
+
if (handle === null || handle === undefined) {
|
|
283
|
+
throw new NativeUnavailable(
|
|
284
|
+
'native kernel rejected the index (invalid sizes); falling back to the vendored Fuse.js'
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
// The kernel copies every buffer; the JS arrays may be GC'd.
|
|
288
|
+
this._lib = lib
|
|
289
|
+
this._handle = handle
|
|
290
|
+
this.nTexts = nTexts
|
|
291
|
+
this._threads = threadCap()
|
|
292
|
+
this._minChunk = minChunk()
|
|
293
|
+
// Reusable per-query buffers (single-threaded per instance).
|
|
294
|
+
this._scores = new Float64Array(nTexts)
|
|
295
|
+
this._isMatch = new Int32Array(nTexts)
|
|
296
|
+
this._idxOffsets = new Int32Array(nTexts + 1)
|
|
297
|
+
registry.register(this, handle, this)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Score one pattern chunk (Uint16Array of length 1..32) against every text.
|
|
302
|
+
* Returns { totalPairs, scores, isMatch, idxOffsets } views over the
|
|
303
|
+
* instance's shared buffers (valid until the next searchChunk call).
|
|
304
|
+
*/
|
|
305
|
+
searchChunk(patternU16, locationOffset, exactCheck) {
|
|
306
|
+
if (this._handle === null) {
|
|
307
|
+
throw new NativeUnavailable('native index is closed')
|
|
308
|
+
}
|
|
309
|
+
const ctx = this._lib.search_begin(
|
|
310
|
+
this._handle,
|
|
311
|
+
patternU16,
|
|
312
|
+
patternU16.length,
|
|
313
|
+
locationOffset,
|
|
314
|
+
exactCheck ? 1 : 0,
|
|
315
|
+
this._scores,
|
|
316
|
+
this._isMatch,
|
|
317
|
+
this._idxOffsets,
|
|
318
|
+
this._threads
|
|
319
|
+
)
|
|
320
|
+
if (ctx === null || ctx === undefined) {
|
|
321
|
+
throw new NativeUnavailable('native kernel rejected the pattern chunk')
|
|
322
|
+
}
|
|
323
|
+
let rc
|
|
324
|
+
if (_shim) {
|
|
325
|
+
rc = _shim(ctx, this.nTexts, this._threads, this._minChunk)
|
|
326
|
+
} else {
|
|
327
|
+
rc = this._lib.search_range(ctx, 0, 0, this.nTexts)
|
|
328
|
+
}
|
|
329
|
+
if (rc !== 0) {
|
|
330
|
+
// Drain the context so buffers/stash stay consistent, then fail.
|
|
331
|
+
this._lib.search_end(ctx)
|
|
332
|
+
throw new NativeUnavailable(`native scoring failed with status ${rc}`)
|
|
333
|
+
}
|
|
334
|
+
const totalPairs = this._lib.search_end(ctx)
|
|
335
|
+
if (totalPairs < 0) {
|
|
336
|
+
throw new NativeUnavailable(`native scoring finalize failed with status ${totalPairs}`)
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
totalPairs,
|
|
340
|
+
scores: this._scores,
|
|
341
|
+
isMatch: this._isMatch,
|
|
342
|
+
idxOffsets: this._idxOffsets,
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
copyIndices(totalPairs) {
|
|
347
|
+
const out = new Int32Array(totalPairs * 2)
|
|
348
|
+
if (totalPairs > 0) {
|
|
349
|
+
this._lib.copy_indices(this._handle, out)
|
|
350
|
+
}
|
|
351
|
+
return out
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
destroy() {
|
|
355
|
+
const handle = this._handle
|
|
356
|
+
this._handle = null
|
|
357
|
+
if (handle) {
|
|
358
|
+
registry.unregister(this)
|
|
359
|
+
this._lib.index_destroy(handle)
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
module.exports = {
|
|
365
|
+
NativeIndex,
|
|
366
|
+
NativeUnavailable,
|
|
367
|
+
nativeAvailable,
|
|
368
|
+
backendInfo,
|
|
369
|
+
ABI_VERSION,
|
|
370
|
+
ENV_LIB,
|
|
371
|
+
ENV_DISABLE,
|
|
372
|
+
ENV_THREADS,
|
|
373
|
+
ENV_MIN_CHUNK,
|
|
374
|
+
}
|
package/src/options.cjs
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Option handling for @thyn-ai/fuse-mojo-core: defaults, validation, key store,
|
|
5
|
+
* property-path getter, and the field-length norm.
|
|
6
|
+
*
|
|
7
|
+
* These functions replicate the *observable* behavior of the corresponding
|
|
8
|
+
* Fuse.js v7 configuration code paths (default values, key weight
|
|
9
|
+
* normalization, dotted-path value extraction, `1/sqrt(tokens)` field norm
|
|
10
|
+
* rounded to 3 decimals) so that results are identical on both backends.
|
|
11
|
+
* Written clean-room against the published documentation and observed
|
|
12
|
+
* behavior of the reference package.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const SPACE_RUN = /[^ ]+/g
|
|
16
|
+
|
|
17
|
+
/** Default options, identical to Fuse.js v7. */
|
|
18
|
+
const DEFAULT_CONFIG = {
|
|
19
|
+
isCaseSensitive: false,
|
|
20
|
+
includeMatches: false,
|
|
21
|
+
findAllMatches: false,
|
|
22
|
+
minMatchCharLength: 1,
|
|
23
|
+
includeScore: false,
|
|
24
|
+
keys: [],
|
|
25
|
+
shouldSort: true,
|
|
26
|
+
sortFn: defaultSortFn,
|
|
27
|
+
location: 0,
|
|
28
|
+
threshold: 0.6,
|
|
29
|
+
distance: 100,
|
|
30
|
+
useExtendedSearch: false,
|
|
31
|
+
getFn: fuseGet,
|
|
32
|
+
ignoreLocation: false,
|
|
33
|
+
ignoreFieldNorm: false,
|
|
34
|
+
fieldNormWeight: 1,
|
|
35
|
+
ignoreDiacritics: false,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function defaultSortFn(a, b) {
|
|
39
|
+
return a.score === b.score ? (a.idx < b.idx ? -1 : 1) : a.score < b.score ? -1 : 1
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const SUPPORTED_OPTIONS = [
|
|
43
|
+
'keys',
|
|
44
|
+
'threshold',
|
|
45
|
+
'location',
|
|
46
|
+
'distance',
|
|
47
|
+
'minMatchCharLength',
|
|
48
|
+
'includeScore',
|
|
49
|
+
'includeMatches',
|
|
50
|
+
'shouldSort',
|
|
51
|
+
'ignoreLocation',
|
|
52
|
+
'isCaseSensitive',
|
|
53
|
+
'findAllMatches',
|
|
54
|
+
'ignoreFieldNorm',
|
|
55
|
+
'fieldNormWeight',
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
class UnsupportedOptionError extends Error {
|
|
59
|
+
constructor(what) {
|
|
60
|
+
super(
|
|
61
|
+
`fuse-mojo: unsupported option or input: ${what}. ` +
|
|
62
|
+
`Supported options are: ${SUPPORTED_OPTIONS.join(', ')} ` +
|
|
63
|
+
'(plus the `limit` search parameter). Extended search ' +
|
|
64
|
+
'(`useExtendedSearch`, logical `$and`/`$or` queries), ' +
|
|
65
|
+
'`ignoreDiacritics`, custom `getFn`, custom `sortFn`, and external ' +
|
|
66
|
+
'indices (`Fuse.createIndex`/`parseIndex`) are not supported.'
|
|
67
|
+
)
|
|
68
|
+
this.name = 'UnsupportedOptionError'
|
|
69
|
+
this.code = 'FUSE_MOJO_UNSUPPORTED_OPTION'
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function validateOptions(options) {
|
|
74
|
+
if (options.useExtendedSearch) {
|
|
75
|
+
throw new UnsupportedOptionError('useExtendedSearch')
|
|
76
|
+
}
|
|
77
|
+
if (options.ignoreDiacritics) {
|
|
78
|
+
throw new UnsupportedOptionError('ignoreDiacritics')
|
|
79
|
+
}
|
|
80
|
+
if (options.getFn !== undefined && options.getFn !== fuseGet) {
|
|
81
|
+
throw new UnsupportedOptionError('custom getFn')
|
|
82
|
+
}
|
|
83
|
+
if (options.sortFn !== undefined && options.sortFn !== defaultSortFn) {
|
|
84
|
+
throw new UnsupportedOptionError('custom sortFn')
|
|
85
|
+
}
|
|
86
|
+
if (!Array.isArray(options.keys)) {
|
|
87
|
+
throw new UnsupportedOptionError('keys must be an array of strings or {name, weight} objects')
|
|
88
|
+
}
|
|
89
|
+
if (!Number.isFinite(options.threshold)) {
|
|
90
|
+
throw new UnsupportedOptionError('non-finite threshold')
|
|
91
|
+
}
|
|
92
|
+
if (typeof options.distance !== 'number' || Number.isNaN(options.distance)) {
|
|
93
|
+
throw new UnsupportedOptionError('non-numeric distance')
|
|
94
|
+
}
|
|
95
|
+
if (!Number.isInteger(options.location) || Math.abs(options.location) > 0x7fffffff) {
|
|
96
|
+
throw new UnsupportedOptionError('non-integer or out-of-range location')
|
|
97
|
+
}
|
|
98
|
+
if (
|
|
99
|
+
!Number.isInteger(options.minMatchCharLength) ||
|
|
100
|
+
options.minMatchCharLength > 0x7fffffff
|
|
101
|
+
) {
|
|
102
|
+
throw new UnsupportedOptionError('non-integer or out-of-range minMatchCharLength')
|
|
103
|
+
}
|
|
104
|
+
for (const key of options.keys) {
|
|
105
|
+
if (typeof key === 'object' && key !== null && !Array.isArray(key) && key.getFn) {
|
|
106
|
+
throw new UnsupportedOptionError('key getFn')
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Key store for standard (non-logical) search. Weights are the RAW weights
|
|
113
|
+
* from the key specifications: the reference only normalizes weights (to sum
|
|
114
|
+
* to 1) inside the KeyStore used by logical $and/$or queries; the regular
|
|
115
|
+
* object-list search path reads the un-normalized weights from the index's
|
|
116
|
+
* key entries. Logical queries are unsupported, so no normalization happens
|
|
117
|
+
* here either.
|
|
118
|
+
*/
|
|
119
|
+
function createKeyStore(keys) {
|
|
120
|
+
const store = []
|
|
121
|
+
for (const key of keys) {
|
|
122
|
+
let path
|
|
123
|
+
let id
|
|
124
|
+
let src
|
|
125
|
+
let weight = 1
|
|
126
|
+
if (typeof key === 'string' || Array.isArray(key)) {
|
|
127
|
+
src = key
|
|
128
|
+
path = Array.isArray(key) ? key : key.split('.')
|
|
129
|
+
id = Array.isArray(key) ? key.join('.') : key
|
|
130
|
+
} else if (typeof key === 'object' && key !== null) {
|
|
131
|
+
if (!Object.prototype.hasOwnProperty.call(key, 'name')) {
|
|
132
|
+
throw new Error('Missing "name" property in key object')
|
|
133
|
+
}
|
|
134
|
+
const name = key.name
|
|
135
|
+
src = name
|
|
136
|
+
if (Object.prototype.hasOwnProperty.call(key, 'weight')) {
|
|
137
|
+
weight = key.weight
|
|
138
|
+
if (weight <= 0) {
|
|
139
|
+
throw new Error(`Invalid "weight" property in key object: ${name}`)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
path = typeof name === 'string' ? name.split('.') : name
|
|
143
|
+
id = Array.isArray(name) ? name.join('.') : name
|
|
144
|
+
} else {
|
|
145
|
+
throw new UnsupportedOptionError(`key of type ${typeof key}`)
|
|
146
|
+
}
|
|
147
|
+
store.push({ path, id, src, weight })
|
|
148
|
+
}
|
|
149
|
+
return store
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const INFINITY = 1 / 0
|
|
153
|
+
|
|
154
|
+
function baseToString(value) {
|
|
155
|
+
if (typeof value === 'string') {
|
|
156
|
+
return value
|
|
157
|
+
}
|
|
158
|
+
const result = value + ''
|
|
159
|
+
return result === '0' && 1 / value === -INFINITY ? '-0' : result
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function toString(value) {
|
|
163
|
+
return value == null ? '' : baseToString(value)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Dotted-path property getter with array descent. Behavioral replica of the
|
|
168
|
+
* reference default getFn: string/number/boolean leaves are stringified,
|
|
169
|
+
* arrays are flattened depth-first in ascending index order, and the result
|
|
170
|
+
* is the single value, or the list of values when any array was traversed.
|
|
171
|
+
*/
|
|
172
|
+
function fuseGet(obj, path) {
|
|
173
|
+
const list = []
|
|
174
|
+
let arr = false
|
|
175
|
+
|
|
176
|
+
const deepGet = (obj, path, index) => {
|
|
177
|
+
if (obj === undefined || obj === null) {
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
if (!path[index]) {
|
|
181
|
+
// No path left (or an empty path segment): the object itself.
|
|
182
|
+
list.push(obj)
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
const key = path[index]
|
|
186
|
+
const value = obj[key]
|
|
187
|
+
if (value === undefined || value === null) {
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
if (
|
|
191
|
+
index === path.length - 1 &&
|
|
192
|
+
(typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean')
|
|
193
|
+
) {
|
|
194
|
+
list.push(toString(value))
|
|
195
|
+
} else if (Array.isArray(value)) {
|
|
196
|
+
arr = true
|
|
197
|
+
for (let i = 0, len = value.length; i < len; i += 1) {
|
|
198
|
+
deepGet(value[i], path, index + 1)
|
|
199
|
+
}
|
|
200
|
+
} else if (path.length) {
|
|
201
|
+
deepGet(value, path, index + 1)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
deepGet(obj, typeof path === 'string' ? path.split('.') : path, 0)
|
|
206
|
+
return arr ? list : list[0]
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function isBlank(value) {
|
|
210
|
+
return !value.trim().length
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Field-length norm: 1 / numTokens^(0.5 * weight), rounded to 3 decimals,
|
|
215
|
+
* cached by token count — identical to the reference norm generator.
|
|
216
|
+
*/
|
|
217
|
+
function createNormGetter(weight, mantissa = 3) {
|
|
218
|
+
const cache = new Map()
|
|
219
|
+
const m = Math.pow(10, mantissa)
|
|
220
|
+
return (value) => {
|
|
221
|
+
const numTokens = value.match(SPACE_RUN).length
|
|
222
|
+
if (cache.has(numTokens)) {
|
|
223
|
+
return cache.get(numTokens)
|
|
224
|
+
}
|
|
225
|
+
const norm = 1 / Math.pow(numTokens, 0.5 * weight)
|
|
226
|
+
const n = parseFloat(Math.round(norm * m) / m)
|
|
227
|
+
cache.set(numTokens, n)
|
|
228
|
+
return n
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
module.exports = {
|
|
233
|
+
DEFAULT_CONFIG,
|
|
234
|
+
SUPPORTED_OPTIONS,
|
|
235
|
+
UnsupportedOptionError,
|
|
236
|
+
validateOptions,
|
|
237
|
+
createKeyStore,
|
|
238
|
+
fuseGet,
|
|
239
|
+
isBlank,
|
|
240
|
+
createNormGetter,
|
|
241
|
+
defaultSortFn,
|
|
242
|
+
}
|