@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/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
+ }
@@ -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
+ }