eddie-jekyll 0.4.1 → 0.4.2
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.
- checksums.yaml +4 -4
- data/assets/assets.list +46 -0
- data/assets/eddie-agent-worker.js +425 -320
- data/assets/eddie-boot.js +234 -0
- data/assets/eddie-dense-esm.js +624 -0
- data/assets/eddie-dense.js +632 -0
- data/assets/{eddie.wasm → eddie-dense.wasm} +0 -0
- data/assets/eddie-lite-esm.js +578 -0
- data/assets/{eddie-wasm.js → eddie-lite.js} +100 -59
- data/assets/eddie-lite.wasm +0 -0
- data/assets/eddie-sw-agent.js +846 -0
- data/assets/eddie-sw-dense.js +1873 -0
- data/assets/eddie-sw-gpu.js +1873 -0
- data/assets/eddie-sw-lite.js +1872 -0
- data/assets/eddie-transformers-sw.js +30926 -0
- data/assets/eddie-widget.js +1884 -458
- data/assets/eddie-worker.js +1205 -652
- data/scripts/install.sh +17 -9
- metadata +16 -5
data/assets/eddie-worker.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
2
|
// Generated by widget/build.sh from widget/src/worker.js and widget/src/lib/*.js; edit those instead.
|
|
3
3
|
"use strict";
|
|
4
|
+
const EDDIE_ASSET_VERSION = "c0fdc503e01a";
|
|
4
5
|
const EddieLib = {};
|
|
5
6
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
6
7
|
|
|
@@ -20,6 +21,17 @@ const EddieLib = {};
|
|
|
20
21
|
|
|
21
22
|
const HF_HOST = "https://huggingface.co";
|
|
22
23
|
|
|
24
|
+
/**
|
|
25
|
+
* `?v=` of the runtime assets (widget, workers, wasm, glue, service
|
|
26
|
+
* workers): a hash of the files that produced them, stamped into every
|
|
27
|
+
* bundle by widget/build.sh. Deliberately not the index's version, which
|
|
28
|
+
* changes on every content rebuild and so re-downloaded the whole engine
|
|
29
|
+
* on every deploy; see "Script tag" in widget/README.md. null outside a
|
|
30
|
+
* bundle (the Node tests), which just means no `?v=`.
|
|
31
|
+
*/
|
|
32
|
+
const ASSET_VERSION =
|
|
33
|
+
typeof EDDIE_ASSET_VERSION === "string" && EDDIE_ASSET_VERSION ? EDDIE_ASSET_VERSION : null;
|
|
34
|
+
|
|
23
35
|
/** Directory of an absolute URL, without query or hash, with a trailing slash. */
|
|
24
36
|
function baseUrlOf(href) {
|
|
25
37
|
const u = new URL(href);
|
|
@@ -87,6 +99,7 @@ const EddieLib = {};
|
|
|
87
99
|
|
|
88
100
|
return {
|
|
89
101
|
HF_HOST,
|
|
102
|
+
ASSET_VERSION,
|
|
90
103
|
baseUrlOf,
|
|
91
104
|
versionOf,
|
|
92
105
|
withVersion,
|
|
@@ -101,8 +114,9 @@ const EddieLib = {};
|
|
|
101
114
|
|
|
102
115
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
103
116
|
|
|
104
|
-
// Dense-lane selection
|
|
105
|
-
//
|
|
117
|
+
// Dense-lane selection and download sizes. Pure functions over the index
|
|
118
|
+
// manifest (see src/manifest.rs). The consent and notice copy lives in
|
|
119
|
+
// copy.js so the widget bundle does not carry the engine-side lane logic.
|
|
106
120
|
|
|
107
121
|
(function (factory) {
|
|
108
122
|
const api = factory();
|
|
@@ -170,6 +184,45 @@ const EddieLib = {};
|
|
|
170
184
|
return /\.(safetensors|bin|pt|gguf|onnx)$/i.test(String(file));
|
|
171
185
|
}
|
|
172
186
|
|
|
187
|
+
/** Site-relative directory the lane's model files live in (`eddie index --bundle-model`), or null for HuggingFace. */
|
|
188
|
+
function laneBaseUrl(lane) {
|
|
189
|
+
const b = lane && lane.runtime && lane.runtime.base_url;
|
|
190
|
+
return typeof b === "string" && b.trim() ? b.trim() : null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Where the lane's files are downloaded from: "site" (bundled next to the index) or "huggingface". */
|
|
194
|
+
function laneOrigin(lane) {
|
|
195
|
+
return laneBaseUrl(lane) ? "site" : "huggingface";
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Absolute URL of a site-bundled model file, resolved against the index URL (no version parameter yet). */
|
|
199
|
+
function siteModelUrl(lane, file, indexUrl) {
|
|
200
|
+
const base = laneBaseUrl(lane);
|
|
201
|
+
if (!base) return null;
|
|
202
|
+
return new URL(base.replace(/\/?$/, "/") + String(file).replace(/^\//, ""), indexUrl).href;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Name a lane's file is cached under (see urls.js cacheKey): a bundled f16
|
|
207
|
+
* copy and the HuggingFace original differ, so site files get their own
|
|
208
|
+
* prefix and never collide with a repo download of the same name.
|
|
209
|
+
*/
|
|
210
|
+
function laneFileName(lane, file) {
|
|
211
|
+
return laneBaseUrl(lane) ? "@site/" + file : String(file);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** The sidecar entry that holds `scope`'s section of `laneId`, or null when the section is in the core file. */
|
|
215
|
+
function sidecarFor(manifest, scope, laneId) {
|
|
216
|
+
const list = manifest && Array.isArray(manifest.sidecars) ? manifest.sidecars : [];
|
|
217
|
+
return list.find((s) => s.scope === scope && s.lane === laneId) || null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Bytes of the sidecar file(s) a lane's chunk vectors need, counted once per file. */
|
|
221
|
+
function laneSidecarBytes(manifest, laneId) {
|
|
222
|
+
const side = sidecarFor(manifest, "chunks", laneId);
|
|
223
|
+
return side && Number.isFinite(Number(side.bytes)) ? Number(side.bytes) : 0;
|
|
224
|
+
}
|
|
225
|
+
|
|
173
226
|
/**
|
|
174
227
|
* Why the WASM loader (init_dense_wasm) cannot run a wasm-candle lane, or
|
|
175
228
|
* null when it can: it accepts BERT-family lanes with config.json,
|
|
@@ -197,6 +250,11 @@ const EddieLib = {};
|
|
|
197
250
|
const lanes = (manifest && Array.isArray(manifest.dense)) ? manifest.dense : [];
|
|
198
251
|
const runtime = (opts && opts.denseRuntime) || "auto";
|
|
199
252
|
const hasWebGpu = !!(opts && opts.hasWebGpu);
|
|
253
|
+
// A pinned lane id (the visitor picked one in the settings panel, or the
|
|
254
|
+
// site set data-dense-lane): the choice narrows to that lane, and if it
|
|
255
|
+
// is not runnable here the result is no dense arm rather than a silently
|
|
256
|
+
// different model.
|
|
257
|
+
const pinned = (opts && opts.laneId) ? String(opts.laneId) : null;
|
|
200
258
|
const webgpu = lanes.filter(isWebGpuLane);
|
|
201
259
|
const skipped = [];
|
|
202
260
|
const wasm = lanes.filter(isWasmLane).filter((lane) => {
|
|
@@ -207,6 +265,9 @@ const EddieLib = {};
|
|
|
207
265
|
if (lanes.length === 0) {
|
|
208
266
|
return { candidates: [], skipped, reason: "index has no dense lane" };
|
|
209
267
|
}
|
|
268
|
+
if (runtime === "off") {
|
|
269
|
+
return { candidates: [], skipped, reason: "dense search is turned off" };
|
|
270
|
+
}
|
|
210
271
|
let candidates;
|
|
211
272
|
if (runtime === "wasm") {
|
|
212
273
|
candidates = wasm;
|
|
@@ -215,9 +276,11 @@ const EddieLib = {};
|
|
|
215
276
|
} else {
|
|
216
277
|
candidates = hasWebGpu ? webgpu.concat(wasm) : wasm;
|
|
217
278
|
}
|
|
279
|
+
if (pinned) candidates = candidates.filter((lane) => lane.id === pinned);
|
|
218
280
|
let reason = null;
|
|
219
281
|
if (candidates.length === 0) {
|
|
220
|
-
if (
|
|
282
|
+
if (pinned) reason = `lane ${pinned} cannot run here`;
|
|
283
|
+
else if (runtime === "wasm") reason = skipped.length ? "no wasm-candle lane the WASM loader can run" : "index has no wasm-candle lane";
|
|
221
284
|
else if (!hasWebGpu && webgpu.length && !wasm.length) reason = "no WebGPU adapter";
|
|
222
285
|
else if (runtime === "webgpu" && !hasWebGpu) reason = "no WebGPU adapter";
|
|
223
286
|
else if (runtime === "webgpu") reason = "index has no webgpu-onnx lane";
|
|
@@ -232,12 +295,70 @@ const EddieLib = {};
|
|
|
232
295
|
return runtime.dtype || "fp32";
|
|
233
296
|
}
|
|
234
297
|
|
|
235
|
-
/**
|
|
298
|
+
/** The exact byte count the manifest declares for a bundled model (`runtime.bytes`), or null. */
|
|
299
|
+
function laneDeclaredBytes(lane) {
|
|
300
|
+
const declared = lane && lane.runtime ? Number(lane.runtime.bytes) : NaN;
|
|
301
|
+
return Number.isFinite(declared) && declared > 0 ? declared : null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Bytes a lane downloads on first use, or null when unknown: the exact
|
|
306
|
+
* count the manifest carries for a bundled model (`runtime.bytes`, written
|
|
307
|
+
* by `eddie index --bundle-model`), else the table estimate for known
|
|
308
|
+
* HuggingFace repos. The table describes the f32 originals; a bundled f16
|
|
309
|
+
* copy without `runtime.bytes` should be measured instead (the engine
|
|
310
|
+
* HEADs the files), never estimated from the table.
|
|
311
|
+
*/
|
|
236
312
|
function laneDownloadBytes(lane) {
|
|
313
|
+
const declared = laneDeclaredBytes(lane);
|
|
314
|
+
if (declared != null) return declared;
|
|
237
315
|
const repo = (laneRepo(lane) || "").toLowerCase();
|
|
238
316
|
return Object.prototype.hasOwnProperty.call(DOWNLOAD_SIZES, repo) ? DOWNLOAD_SIZES[repo] : null;
|
|
239
317
|
}
|
|
240
318
|
|
|
319
|
+
return {
|
|
320
|
+
DEFAULT_WASM_FILES,
|
|
321
|
+
DOWNLOAD_SIZES,
|
|
322
|
+
laneKind,
|
|
323
|
+
isWasmLane,
|
|
324
|
+
isWebGpuLane,
|
|
325
|
+
laneRepo,
|
|
326
|
+
laneRevision,
|
|
327
|
+
laneFiles,
|
|
328
|
+
isWeightsFile,
|
|
329
|
+
laneBaseUrl,
|
|
330
|
+
laneOrigin,
|
|
331
|
+
siteModelUrl,
|
|
332
|
+
laneFileName,
|
|
333
|
+
sidecarFor,
|
|
334
|
+
laneSidecarBytes,
|
|
335
|
+
wasmLaneProblem,
|
|
336
|
+
chooseDenseLanes,
|
|
337
|
+
pickDtype,
|
|
338
|
+
laneDeclaredBytes,
|
|
339
|
+
laneDownloadBytes,
|
|
340
|
+
};
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
344
|
+
|
|
345
|
+
// User-facing copy shared by the widget and the search engine: byte
|
|
346
|
+
// formatting, the download consent card and the degraded-arm notices.
|
|
347
|
+
// Split out of lanes.js so the widget bundle carries only this, not the
|
|
348
|
+
// engine-side lane selection.
|
|
349
|
+
|
|
350
|
+
(function (factory) {
|
|
351
|
+
const api = factory();
|
|
352
|
+
if (typeof module === "object" && module && module.exports) {
|
|
353
|
+
module.exports = api;
|
|
354
|
+
} else if (typeof EddieLib === "object" && EddieLib) {
|
|
355
|
+
Object.assign(EddieLib, api);
|
|
356
|
+
} else {
|
|
357
|
+
globalThis.EddieLib = Object.assign(globalThis.EddieLib || {}, api);
|
|
358
|
+
}
|
|
359
|
+
})(function () {
|
|
360
|
+
"use strict";
|
|
361
|
+
|
|
241
362
|
function formatBytes(n) {
|
|
242
363
|
if (n == null || !Number.isFinite(n)) return "unknown size";
|
|
243
364
|
if (n >= 1e9) {
|
|
@@ -251,16 +372,21 @@ const EddieLib = {};
|
|
|
251
372
|
|
|
252
373
|
/**
|
|
253
374
|
* Consent card copy for a model download. `consentText` (site override) may
|
|
254
|
-
* contain `{size}` and `{
|
|
375
|
+
* contain `{size}`, `{model}` and `{origin}` placeholders.
|
|
376
|
+
* sizeBytes model download (null when unknown)
|
|
377
|
+
* origin "site" | "huggingface" (where the model is fetched from)
|
|
378
|
+
* sidecarBytes index vectors fetched from the site for this lane (0 when in the core index)
|
|
255
379
|
*/
|
|
256
380
|
function consentCopy(opts) {
|
|
257
381
|
const size = formatBytes(opts.sizeBytes);
|
|
258
382
|
const model = opts.model || "the search model";
|
|
383
|
+
const origin = opts.origin === "site" ? "this site" : "huggingface.co";
|
|
259
384
|
if (opts.consentText) {
|
|
260
|
-
return opts.consentText.replace(/\{size\}/g, size).replace(/\{model\}/g, model);
|
|
385
|
+
return opts.consentText.replace(/\{size\}/g, size).replace(/\{model\}/g, model).replace(/\{origin\}/g, origin);
|
|
261
386
|
}
|
|
262
|
-
const sized = opts.sizeBytes == null ?
|
|
263
|
-
|
|
387
|
+
const sized = opts.sizeBytes == null ? `a one-time download (size unknown) from ${origin}` : `a one-time ${size} download from ${origin}`;
|
|
388
|
+
const side = opts.sidecarBytes > 0 ? `, plus ${formatBytes(opts.sidecarBytes)} of index vectors from this site` : "";
|
|
389
|
+
let text = `Semantic search runs ${model} in your browser. That needs ${sized}${side}, kept in your browser's cache for next time.`;
|
|
264
390
|
if (opts.saveData) {
|
|
265
391
|
text = "Data saver is on. " + text;
|
|
266
392
|
}
|
|
@@ -271,8 +397,15 @@ const EddieLib = {};
|
|
|
271
397
|
* Drop degraded notes that describe the index design rather than a failure
|
|
272
398
|
* (an index without a dense lane or sparse arm is not "degraded").
|
|
273
399
|
*/
|
|
400
|
+
/**
|
|
401
|
+
* Drop the "degraded" entries that describe how the index or the visitor
|
|
402
|
+
* chose to run, not something going wrong: an index built without a lane,
|
|
403
|
+
* and a visitor who picked keyword-only search in the settings panel.
|
|
404
|
+
*/
|
|
274
405
|
function filterDesignDegraded(degraded) {
|
|
275
|
-
return (degraded || []).filter(
|
|
406
|
+
return (degraded || []).filter(
|
|
407
|
+
(d) => !/index has no (dense lane|sparse arm)|dense search is turned off/.test(d)
|
|
408
|
+
);
|
|
276
409
|
}
|
|
277
410
|
|
|
278
411
|
/** Human notice when a semantic arm is missing, or null when nothing to say. */
|
|
@@ -290,19 +423,6 @@ const EddieLib = {};
|
|
|
290
423
|
}
|
|
291
424
|
|
|
292
425
|
return {
|
|
293
|
-
DEFAULT_WASM_FILES,
|
|
294
|
-
DOWNLOAD_SIZES,
|
|
295
|
-
laneKind,
|
|
296
|
-
isWasmLane,
|
|
297
|
-
isWebGpuLane,
|
|
298
|
-
laneRepo,
|
|
299
|
-
laneRevision,
|
|
300
|
-
laneFiles,
|
|
301
|
-
isWeightsFile,
|
|
302
|
-
wasmLaneProblem,
|
|
303
|
-
chooseDenseLanes,
|
|
304
|
-
pickDtype,
|
|
305
|
-
laneDownloadBytes,
|
|
306
426
|
formatBytes,
|
|
307
427
|
consentCopy,
|
|
308
428
|
filterDesignDegraded,
|
|
@@ -349,8 +469,14 @@ const EddieLib = {};
|
|
|
349
469
|
}
|
|
350
470
|
|
|
351
471
|
async function readBody(response, onProgress) {
|
|
352
|
-
const
|
|
353
|
-
const
|
|
472
|
+
const headers = response.headers && response.headers.get ? response.headers : null;
|
|
473
|
+
const lenHeader = headers ? headers.get("Content-Length") : null;
|
|
474
|
+
// A compressed response (Content-Encoding: br/gzip) announces the wire
|
|
475
|
+
// size while the stream yields decoded bytes: the announced total is
|
|
476
|
+
// neither a buffer size nor a progress denominator then.
|
|
477
|
+
const encoding = headers ? String(headers.get("Content-Encoding") || "").trim().toLowerCase() : "";
|
|
478
|
+
const encoded = encoding !== "" && encoding !== "identity";
|
|
479
|
+
const total = !encoded && lenHeader && /^\d+$/.test(lenHeader) && Number(lenHeader) > 0 ? Number(lenHeader) : null;
|
|
354
480
|
if (onProgress) onProgress(0, total);
|
|
355
481
|
if (!response.body || typeof response.body.getReader !== "function") {
|
|
356
482
|
const buf = new Uint8Array(await response.arrayBuffer());
|
|
@@ -466,702 +592,1129 @@ const EddieLib = {};
|
|
|
466
592
|
|
|
467
593
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
468
594
|
|
|
469
|
-
// Eddie search
|
|
595
|
+
// Eddie search engine, host-independent.
|
|
596
|
+
//
|
|
597
|
+
// Everything the search worker does (manifest, lane choice, downloads with
|
|
598
|
+
// timeouts and retry, IndexedDB cache, sidecars, sparse tokenizer, the
|
|
599
|
+
// transformers.js WebGPU lane, the WASM calls and the consent handshake)
|
|
600
|
+
// lives here, behind an `env` the host supplies:
|
|
601
|
+
//
|
|
602
|
+
// createSearchEngine({
|
|
603
|
+
// post(message) broadcast sink: status and ready events
|
|
604
|
+
// loadWasm(baseUrl, variant)
|
|
605
|
+
// -> Promise of the wasm-bindgen API object;
|
|
606
|
+
// variant is "lite" (always first) or
|
|
607
|
+
// "dense" (only when a wasm-candle lane is
|
|
608
|
+
// about to run); a host without the dense
|
|
609
|
+
// module rejects and the lane degrades
|
|
610
|
+
// loadTransformers() -> Promise of the transformers.js module
|
|
611
|
+
// canRunWebGpuLane false when this host cannot run webgpu-onnx lanes
|
|
612
|
+
// navigator, indexedDB optional overrides (tests)
|
|
613
|
+
// })
|
|
614
|
+
//
|
|
615
|
+
// Lite first: the lite module (no model code, a quarter of the dense one
|
|
616
|
+
// after brotli) answers keyword and sparse queries at once. When a visitor
|
|
617
|
+
// accepts a CPU dense lane the dense module is loaded and the index, its
|
|
618
|
+
// attached sidecars and the sparse tokenizer are handed over to it; the
|
|
619
|
+
// bytes are kept only while that hand-over may still happen.
|
|
470
620
|
//
|
|
471
|
-
//
|
|
472
|
-
//
|
|
473
|
-
//
|
|
621
|
+
// `engine.handle(msg, reply)` dispatches one protocol message; `reply` is
|
|
622
|
+
// the sink for that message's own answers (search_result, cache_result,
|
|
623
|
+
// error with requestId ...). In a dedicated worker `post` and `reply` are
|
|
624
|
+
// both `self.postMessage`; in the service worker `reply` is the requesting
|
|
625
|
+
// page's port and `post` fans out to every connected page.
|
|
474
626
|
//
|
|
475
|
-
//
|
|
476
|
-
// init {indexUrl, baseUrl, version?, denseRuntime?, consent?}
|
|
477
|
-
// cache_check {requestId}
|
|
478
|
-
// search {requestId, query, topK?, mode?, evidence?, qa?}
|
|
479
|
-
// page {requestId, url}
|
|
480
|
-
// chunk {requestId, id}
|
|
481
|
-
// qa {requestId, query, k?}
|
|
482
|
-
// (worker -> main thread):
|
|
483
|
-
// status {state, ...} loading_wasm | loading_index | index_ready |
|
|
484
|
-
// consent_required | downloading_model |
|
|
485
|
-
// loading_model | error
|
|
486
|
-
// ready {lanes, lane, arms, degraded, manifest}
|
|
487
|
-
// cache_result {requestId, cached, lane, sizeBytes}
|
|
488
|
-
// search_result {requestId, results, arms, degraded, mode, qa?}
|
|
489
|
-
// page_result {requestId, page}
|
|
490
|
-
// chunk_result {requestId, chunk}
|
|
491
|
-
// qa_result {requestId, hits}
|
|
492
|
-
// error {requestId?, message, fatal?, unsupported?}
|
|
627
|
+
// The message shapes are documented in widget/README.md ("Worker protocol").
|
|
493
628
|
|
|
494
|
-
|
|
629
|
+
(function (factory) {
|
|
630
|
+
const api = factory();
|
|
631
|
+
if (typeof module === "object" && module && module.exports) {
|
|
632
|
+
module.exports = api;
|
|
633
|
+
} else if (typeof EddieLib === "object" && EddieLib) {
|
|
634
|
+
Object.assign(EddieLib, api);
|
|
635
|
+
} else {
|
|
636
|
+
globalThis.EddieLib = Object.assign(globalThis.EddieLib || {}, api);
|
|
637
|
+
}
|
|
638
|
+
})(function () {
|
|
639
|
+
"use strict";
|
|
495
640
|
|
|
496
|
-
const IDB_NAME = "eddie-models";
|
|
497
|
-
const IDB_VERSION = 2;
|
|
498
|
-
const IDB_FILES = "files";
|
|
499
|
-
const IDB_META = "meta";
|
|
500
|
-
const
|
|
501
|
-
const
|
|
641
|
+
const IDB_NAME = "eddie-models";
|
|
642
|
+
const IDB_VERSION = 2;
|
|
643
|
+
const IDB_FILES = "files";
|
|
644
|
+
const IDB_META = "meta";
|
|
645
|
+
const PROGRESS_INTERVAL_MS = 80;
|
|
646
|
+
const NOT_LOADED = "index not loaded yet";
|
|
502
647
|
|
|
503
|
-
|
|
648
|
+
/** What a wasm-bindgen API object can do; older glue without capabilities() is the full build. */
|
|
649
|
+
function wasmCapabilities(api) {
|
|
650
|
+
if (api && typeof api.capabilities === "function") {
|
|
651
|
+
try {
|
|
652
|
+
const c = JSON.parse(api.capabilities());
|
|
653
|
+
return { dense_wasm: !!c.dense_wasm, sparse: c.sparse !== false, version: c.version || null };
|
|
654
|
+
} catch (_) {
|
|
655
|
+
// fall through to feature detection
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return { dense_wasm: !!(api && typeof api.init_dense_wasm === "function"), sparse: true, version: null };
|
|
659
|
+
}
|
|
504
660
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
661
|
+
function createSearchEngine(env) {
|
|
662
|
+
const lib = typeof EddieLib === "object" && EddieLib ? EddieLib : env.lib;
|
|
663
|
+
const post = env.post;
|
|
664
|
+
const nav = env.navigator !== undefined ? env.navigator : globalThis.navigator;
|
|
665
|
+
const idb = env.indexedDB !== undefined ? env.indexedDB : globalThis.indexedDB;
|
|
666
|
+
const canRunWebGpuLane = env.canRunWebGpuLane !== false;
|
|
667
|
+
|
|
668
|
+
let wasm = null; // wasm-bindgen API object once loaded (lite, then dense after a hand-over)
|
|
669
|
+
let caps = null; // wasmCapabilities(wasm)
|
|
670
|
+
|
|
671
|
+
const state = {
|
|
672
|
+
phase: "idle", // idle | loading | awaiting_consent | awaiting_tier | ready | error | dead
|
|
673
|
+
initRunning: false,
|
|
674
|
+
rerun: false,
|
|
675
|
+
baseUrl: "",
|
|
676
|
+
version: null,
|
|
677
|
+
indexUrl: null,
|
|
678
|
+
loadedIndexUrl: null,
|
|
679
|
+
denseRuntime: "auto",
|
|
680
|
+
laneId: null, // visitor-pinned dense lane (settings panel), or null
|
|
681
|
+
consent: Object.create(null), // lane id -> true
|
|
682
|
+
wasmReady: false,
|
|
683
|
+
indexLoaded: false,
|
|
684
|
+
manifest: null,
|
|
685
|
+
// Bytes kept for the lite -> dense hand-over (dropped once it is done or impossible).
|
|
686
|
+
indexBytes: null,
|
|
687
|
+
sparseBytes: null,
|
|
688
|
+
sidecarBytes: Object.create(null), // file -> Uint8Array
|
|
689
|
+
attachedSidecars: [], // file names attached to the current module
|
|
690
|
+
candidates: [],
|
|
691
|
+
candidateReason: null,
|
|
692
|
+
hostSkipped: [], // webgpu-onnx lane ids this host cannot run
|
|
693
|
+
laneIndex: 0,
|
|
694
|
+
dense: null, // { lane, kind: "wasm" | "webgpu", embed? }
|
|
695
|
+
sparse: false,
|
|
696
|
+
degraded: [],
|
|
697
|
+
gpu: null, // { adapter, hasF16, maxBufferSize } | false
|
|
698
|
+
db: undefined, // IDBDatabase | null (unavailable)
|
|
699
|
+
siteSizes: Object.create(null), // lane id -> measured bundled download bytes
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
// -- dispatch ---------------------------------------------------------
|
|
703
|
+
|
|
704
|
+
function handle(msg, reply) {
|
|
705
|
+
const m = msg || {};
|
|
706
|
+
const out = reply || post;
|
|
707
|
+
switch (m.type) {
|
|
708
|
+
case "init":
|
|
709
|
+
return handleInit(m, out);
|
|
710
|
+
case "cache_check":
|
|
711
|
+
return handleCacheCheck(m, out);
|
|
712
|
+
case "cache_clear":
|
|
713
|
+
return handleCacheClear(m, out);
|
|
714
|
+
case "search":
|
|
715
|
+
return handleSearch(m, out);
|
|
716
|
+
case "page":
|
|
717
|
+
return handleLookup(m, out, "page_result", "page", () => wasm.page(String(m.url)));
|
|
718
|
+
case "chunk":
|
|
719
|
+
return handleLookup(m, out, "chunk_result", "chunk", () => wasm.chunk(Number(m.id)));
|
|
720
|
+
case "qa":
|
|
721
|
+
return handleQa(m, out);
|
|
722
|
+
default:
|
|
723
|
+
postError(out, m.requestId, `unknown message type ${String(m.type)}`);
|
|
724
|
+
return Promise.resolve();
|
|
725
|
+
}
|
|
726
|
+
}
|
|
526
727
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
break;
|
|
539
|
-
case "page":
|
|
540
|
-
handleLookup(msg, "page_result", "page", () => wasm_bindgen.page(String(msg.url)));
|
|
541
|
-
break;
|
|
542
|
-
case "chunk":
|
|
543
|
-
handleLookup(msg, "chunk_result", "chunk", () => wasm_bindgen.chunk(Number(msg.id)));
|
|
544
|
-
break;
|
|
545
|
-
case "qa":
|
|
546
|
-
handleQa(msg);
|
|
547
|
-
break;
|
|
548
|
-
default:
|
|
549
|
-
postError(msg.requestId, `unknown message type ${String(msg.type)}`);
|
|
550
|
-
}
|
|
551
|
-
};
|
|
728
|
+
/**
|
|
729
|
+
* The degraded list as reported to the host: design choices dropped
|
|
730
|
+
* (copy.js), and the dense arm's entries dropped entirely when the
|
|
731
|
+
* visitor turned the dense arm off -- they asked for keyword and sparse,
|
|
732
|
+
* so nothing is missing.
|
|
733
|
+
*/
|
|
734
|
+
function reportedDegraded(list) {
|
|
735
|
+
const filtered = lib.filterDesignDegraded(unique(list));
|
|
736
|
+
if (state.denseRuntime !== "off") return filtered;
|
|
737
|
+
return filtered.filter((d) => !String(d).startsWith("dense:"));
|
|
738
|
+
}
|
|
552
739
|
|
|
553
|
-
|
|
740
|
+
/** Snapshot for the service worker's `state` reply. */
|
|
741
|
+
function snapshot() {
|
|
742
|
+
return {
|
|
743
|
+
phase: state.phase,
|
|
744
|
+
indexUrl: state.loadedIndexUrl,
|
|
745
|
+
version: state.version,
|
|
746
|
+
indexLoaded: state.indexLoaded,
|
|
747
|
+
lane: state.dense ? state.dense.lane.id : null,
|
|
748
|
+
runtime: state.dense ? state.dense.kind : null,
|
|
749
|
+
arms: state.indexLoaded ? { dense: !!state.dense, sparse: state.sparse, bm25: true } : null,
|
|
750
|
+
degraded: reportedDegraded(state.degraded),
|
|
751
|
+
manifest: state.manifest ? manifestSummary() : null,
|
|
752
|
+
lanes: laneList(),
|
|
753
|
+
hostSkippedLanes: state.hostSkipped.slice(),
|
|
754
|
+
wasm: caps ? (caps.dense_wasm ? "dense" : "lite") : null,
|
|
755
|
+
};
|
|
756
|
+
}
|
|
554
757
|
|
|
555
|
-
|
|
556
|
-
if (state.phase === "dead") {
|
|
557
|
-
postError(null, "search engine crashed; reload the page to retry", true);
|
|
558
|
-
return;
|
|
559
|
-
}
|
|
560
|
-
if (msg.indexUrl) state.indexUrl = String(msg.indexUrl);
|
|
561
|
-
if (msg.baseUrl != null) state.baseUrl = String(msg.baseUrl);
|
|
562
|
-
if (msg.version != null) state.version = msg.version ? String(msg.version) : null;
|
|
563
|
-
if (msg.denseRuntime) state.denseRuntime = String(msg.denseRuntime);
|
|
564
|
-
if (msg.consent === true) {
|
|
565
|
-
const lane = currentCandidate();
|
|
566
|
-
if (lane) state.consent[lane.id] = true;
|
|
567
|
-
if (msg.consentLane) state.consent[String(msg.consentLane)] = true;
|
|
568
|
-
}
|
|
569
|
-
runInit();
|
|
570
|
-
}
|
|
758
|
+
// -- init ---------------------------------------------------------------
|
|
571
759
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
postReady();
|
|
592
|
-
evictStaleFiles();
|
|
593
|
-
} catch (err) {
|
|
594
|
-
state.phase = isFatal(err) ? "dead" : "error";
|
|
595
|
-
postStatus("error", {
|
|
596
|
-
message: describe(err),
|
|
597
|
-
fatal: isFatal(err),
|
|
598
|
-
unsupported: isUnsupported(err),
|
|
599
|
-
});
|
|
600
|
-
} finally {
|
|
601
|
-
state.initRunning = false;
|
|
602
|
-
if (state.rerun) {
|
|
603
|
-
state.rerun = false;
|
|
604
|
-
runInit();
|
|
760
|
+
function handleInit(msg, reply) {
|
|
761
|
+
if (state.phase === "dead") {
|
|
762
|
+
postError(reply, null, "search engine crashed; reload the page to retry", true);
|
|
763
|
+
return Promise.resolve();
|
|
764
|
+
}
|
|
765
|
+
if (msg.indexUrl) state.indexUrl = String(msg.indexUrl);
|
|
766
|
+
if (msg.baseUrl != null) state.baseUrl = String(msg.baseUrl);
|
|
767
|
+
if (msg.version != null) state.version = msg.version ? String(msg.version) : null;
|
|
768
|
+
const wasRuntime = state.denseRuntime;
|
|
769
|
+
const wasLane = state.laneId;
|
|
770
|
+
if (msg.denseRuntime) state.denseRuntime = String(msg.denseRuntime);
|
|
771
|
+
if (msg.laneId !== undefined) state.laneId = msg.laneId ? String(msg.laneId) : null;
|
|
772
|
+
if (state.denseRuntime !== wasRuntime || state.laneId !== wasLane) resetDenseSelection();
|
|
773
|
+
if (msg.consent === true) {
|
|
774
|
+
const lane = currentCandidate();
|
|
775
|
+
if (lane) state.consent[lane.id] = true;
|
|
776
|
+
if (msg.consentLane) state.consent[String(msg.consentLane)] = true;
|
|
777
|
+
}
|
|
778
|
+
return runInit();
|
|
605
779
|
}
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
780
|
|
|
609
|
-
async function
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
781
|
+
async function runInit() {
|
|
782
|
+
if (state.initRunning) {
|
|
783
|
+
state.rerun = true;
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
state.initRunning = true;
|
|
787
|
+
try {
|
|
788
|
+
state.phase = "loading";
|
|
789
|
+
await ensureWasm();
|
|
790
|
+
await ensureIndex();
|
|
791
|
+
await ensureDb();
|
|
792
|
+
await ensureGpu();
|
|
793
|
+
chooseLanes();
|
|
794
|
+
await ensureSparse();
|
|
795
|
+
const outcome = await ensureDense();
|
|
796
|
+
if (outcome === "consent") {
|
|
797
|
+
state.phase = "awaiting_consent";
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
if (outcome === "tier") {
|
|
801
|
+
state.phase = "awaiting_tier";
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
releaseHandoverBytes();
|
|
805
|
+
state.phase = "ready";
|
|
806
|
+
postReady();
|
|
807
|
+
evictStaleFiles();
|
|
808
|
+
} catch (err) {
|
|
809
|
+
state.phase = isFatal(err) ? "dead" : "error";
|
|
810
|
+
postStatus("error", {
|
|
811
|
+
message: describe(err),
|
|
812
|
+
fatal: isFatal(err),
|
|
813
|
+
unsupported: isUnsupported(err),
|
|
814
|
+
});
|
|
815
|
+
} finally {
|
|
816
|
+
state.initRunning = false;
|
|
817
|
+
if (state.rerun) {
|
|
818
|
+
state.rerun = false;
|
|
819
|
+
runInit();
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
626
823
|
|
|
627
|
-
async function
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
824
|
+
async function loadWasmVariant(variant) {
|
|
825
|
+
if (typeof WebAssembly !== "object") {
|
|
826
|
+
throw unsupported("This browser can't run WebAssembly.");
|
|
827
|
+
}
|
|
828
|
+
let api;
|
|
829
|
+
try {
|
|
830
|
+
api = await env.loadWasm(state.baseUrl, variant);
|
|
831
|
+
} catch (err) {
|
|
832
|
+
if (typeof WebAssembly === "object" && (err instanceof WebAssembly.CompileError || err instanceof WebAssembly.LinkError)) {
|
|
833
|
+
throw unsupported("This browser can't run the search engine (WebAssembly SIMD is required).");
|
|
834
|
+
}
|
|
835
|
+
throw err;
|
|
836
|
+
}
|
|
837
|
+
if (!api || typeof api.search !== "function") {
|
|
838
|
+
throw new Error(`loadWasm(${variant}) did not return the Eddie WASM API`);
|
|
839
|
+
}
|
|
840
|
+
return api;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
async function ensureWasm() {
|
|
844
|
+
if (state.wasmReady) return;
|
|
845
|
+
postStatus("loading_wasm", { variant: "lite" });
|
|
846
|
+
wasm = await loadWasmVariant("lite");
|
|
847
|
+
caps = wasmCapabilities(wasm);
|
|
848
|
+
state.wasmReady = true;
|
|
849
|
+
}
|
|
650
850
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
if (
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
851
|
+
/**
|
|
852
|
+
* Hand the loaded index over to the dense module (candle embedder) when
|
|
853
|
+
* the running one cannot embed. Everything the lite module was given is
|
|
854
|
+
* given again: index bytes, attached sidecars, a fetched sparse tokenizer.
|
|
855
|
+
*/
|
|
856
|
+
async function ensureDenseWasm() {
|
|
857
|
+
if (caps && caps.dense_wasm) return;
|
|
858
|
+
if (!state.indexBytes) throw new Error("dense hand-over: index bytes were released");
|
|
859
|
+
postStatus("loading_wasm", { variant: "dense" });
|
|
860
|
+
const dense = await loadWasmVariant("dense");
|
|
861
|
+
const dcaps = wasmCapabilities(dense);
|
|
862
|
+
if (!dcaps.dense_wasm) throw new Error("eddie-dense.wasm has no CPU embedder (capabilities.dense_wasm is false)");
|
|
863
|
+
dense.init_index(state.indexBytes);
|
|
864
|
+
for (const file of state.attachedSidecars) {
|
|
865
|
+
if (state.sidecarBytes[file]) dense.attach_sidecar(state.sidecarBytes[file]);
|
|
663
866
|
}
|
|
867
|
+
if (state.sparse && state.sparseBytes) dense.init_sparse_tokenizer(state.sparseBytes);
|
|
868
|
+
wasm = dense;
|
|
869
|
+
caps = dcaps;
|
|
664
870
|
}
|
|
665
|
-
} catch (err) {
|
|
666
|
-
console.warn("eddie: WebGPU adapter probe failed", err);
|
|
667
|
-
state.gpu = false;
|
|
668
|
-
}
|
|
669
|
-
const choice = lib.chooseDenseLanes(state.manifest, {
|
|
670
|
-
denseRuntime: state.denseRuntime,
|
|
671
|
-
hasWebGpu: !!state.gpu,
|
|
672
|
-
});
|
|
673
|
-
state.candidates = choice.candidates;
|
|
674
|
-
state.candidateReason = choice.reason;
|
|
675
|
-
for (const { lane, reason } of choice.skipped) {
|
|
676
|
-
console.warn(`eddie: dense lane ${lane.id} skipped: ${reason}`);
|
|
677
|
-
state.degraded.push(`dense: lane ${lane.id} skipped: ${reason}`);
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
871
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
try {
|
|
687
|
-
const repo = spec.tokenizer || spec.model;
|
|
688
|
-
const rev = spec.revision || "main";
|
|
689
|
-
let bytes = await getModelFile(repo, rev, "tokenizer.json", "sparse");
|
|
690
|
-
let ok = await lib.verifySha256(bytes, spec.vocab_hash);
|
|
691
|
-
if (!ok && bytes.fromCache) {
|
|
692
|
-
await idbDelete(lib.cacheKey(repo, rev, "tokenizer.json"));
|
|
693
|
-
bytes = await getModelFile(repo, rev, "tokenizer.json", "sparse", { noCache: true });
|
|
694
|
-
ok = await lib.verifySha256(bytes, spec.vocab_hash);
|
|
695
|
-
}
|
|
696
|
-
if (!ok) {
|
|
697
|
-
throw new Error("tokenizer.json SHA-256 does not match the index's vocab_hash");
|
|
698
|
-
}
|
|
699
|
-
wasm_bindgen.init_sparse_tokenizer(bytes);
|
|
700
|
-
state.sparse = true;
|
|
701
|
-
} catch (err) {
|
|
702
|
-
if (isFatal(err)) throw err;
|
|
703
|
-
console.warn("eddie: sparse arm unavailable", err);
|
|
704
|
-
state.degraded.push(`sparse: ${describe(err)}`);
|
|
705
|
-
}
|
|
706
|
-
}
|
|
872
|
+
/** A wasm-candle lane could still ask for the dense module later. */
|
|
873
|
+
function handoverPossible() {
|
|
874
|
+
if (!caps || caps.dense_wasm || state.dense) return false;
|
|
875
|
+
return state.candidates.slice(state.laneIndex).some((lane) => lib.isWasmLane(lane));
|
|
876
|
+
}
|
|
707
877
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
878
|
+
function releaseHandoverBytes() {
|
|
879
|
+
if (handoverPossible()) return;
|
|
880
|
+
state.indexBytes = null;
|
|
881
|
+
state.sparseBytes = null;
|
|
882
|
+
state.sidecarBytes = Object.create(null);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
async function ensureIndex() {
|
|
886
|
+
if (!state.indexUrl) throw new Error("init: indexUrl is required");
|
|
887
|
+
if (state.indexLoaded && state.loadedIndexUrl === state.indexUrl) return;
|
|
888
|
+
postStatus("loading_index", { progress: null });
|
|
889
|
+
let last = 0;
|
|
890
|
+
const bytes = await lib.fetchBytes(state.indexUrl, {
|
|
891
|
+
fetch: env.fetch,
|
|
892
|
+
timeoutMs: 180000,
|
|
893
|
+
onProgress: (loaded, total) => {
|
|
894
|
+
const now = Date.now();
|
|
895
|
+
if (total && now - last < PROGRESS_INTERVAL_MS && loaded !== total) return;
|
|
896
|
+
last = now;
|
|
897
|
+
postStatus("loading_index", { progress: total ? loaded / total : null, loaded, total });
|
|
898
|
+
},
|
|
899
|
+
});
|
|
900
|
+
state.manifest = JSON.parse(wasm.manifest(bytes));
|
|
901
|
+
wasm.init_index(bytes);
|
|
902
|
+
state.indexBytes = bytes;
|
|
903
|
+
state.indexLoaded = true;
|
|
904
|
+
state.loadedIndexUrl = state.indexUrl;
|
|
905
|
+
state.dense = null;
|
|
906
|
+
state.sparse = false;
|
|
907
|
+
state.sparseBytes = null;
|
|
908
|
+
state.sidecarBytes = Object.create(null);
|
|
909
|
+
state.attachedSidecars = [];
|
|
910
|
+
state.degraded = [];
|
|
911
|
+
state.hostSkipped = [];
|
|
912
|
+
state.laneIndex = 0;
|
|
913
|
+
state.gpu = null;
|
|
914
|
+
state.siteSizes = Object.create(null);
|
|
915
|
+
postStatus("index_ready", { manifest: manifestSummary(), lanes: laneList(), hostSkippedLanes: state.hostSkipped.slice() });
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
async function ensureGpu() {
|
|
919
|
+
if (state.gpu !== null) return;
|
|
920
|
+
state.gpu = false;
|
|
921
|
+
try {
|
|
922
|
+
if (state.denseRuntime !== "wasm" && nav && nav.gpu && typeof nav.gpu.requestAdapter === "function") {
|
|
923
|
+
const adapter = await nav.gpu.requestAdapter();
|
|
924
|
+
if (adapter) {
|
|
925
|
+
state.gpu = {
|
|
926
|
+
adapter,
|
|
927
|
+
hasF16: !!(adapter.features && adapter.features.has("shader-f16")),
|
|
928
|
+
maxBufferSize: adapter.limits ? adapter.limits.maxBufferSize : 0,
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
} catch (err) {
|
|
933
|
+
console.warn("eddie: WebGPU adapter probe failed", err);
|
|
934
|
+
state.gpu = false;
|
|
722
935
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* Pick the dense lanes to try, in order. Runs on every init, not just the
|
|
940
|
+
* first: the visitor can change `denseRuntime` or pin a lane from the
|
|
941
|
+
* settings panel at any time, and the answer has to change with it.
|
|
942
|
+
*/
|
|
943
|
+
function chooseLanes() {
|
|
944
|
+
const choice = lib.chooseDenseLanes(state.manifest, {
|
|
945
|
+
denseRuntime: state.denseRuntime,
|
|
946
|
+
laneId: state.laneId,
|
|
947
|
+
hasWebGpu: !!state.gpu && canRunWebGpuLane,
|
|
948
|
+
});
|
|
949
|
+
state.candidates = choice.candidates;
|
|
950
|
+
state.candidateReason = choice.reason;
|
|
951
|
+
if (!canRunWebGpuLane) {
|
|
952
|
+
state.hostSkipped = (state.manifest.dense || []).filter(lib.isWebGpuLane).map((l) => l.id);
|
|
953
|
+
}
|
|
954
|
+
for (const { lane, reason } of choice.skipped) {
|
|
955
|
+
console.warn(`eddie: dense lane ${lane.id} skipped: ${reason}`);
|
|
956
|
+
state.degraded.push(`dense: lane ${lane.id} skipped: ${reason}`);
|
|
727
957
|
}
|
|
728
|
-
return "ok";
|
|
729
|
-
} catch (err) {
|
|
730
|
-
if (isFatal(err)) throw err;
|
|
731
|
-
console.warn(`eddie: dense lane ${lane.id} failed`, err);
|
|
732
|
-
state.degraded.push(`dense: lane ${lane.id} failed: ${describe(err)}`);
|
|
733
958
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* Forget which dense lane is running so the next init picks again. Called
|
|
962
|
+
* when the visitor changes the lane or turns the dense arm off.
|
|
963
|
+
*/
|
|
964
|
+
function resetDenseSelection() {
|
|
965
|
+
state.dense = null;
|
|
966
|
+
state.laneIndex = 0;
|
|
967
|
+
state.degraded = state.degraded.filter((d) => !String(d).startsWith("dense:"));
|
|
968
|
+
// The hand-over bytes are dropped once no wasm-candle lane is in play
|
|
969
|
+
// (releaseHandoverBytes). A visitor who then picks the CPU lane needs
|
|
970
|
+
// them back: re-read the index, which is an immutable-cached fetch.
|
|
971
|
+
if (!state.indexBytes && !(caps && caps.dense_wasm)) {
|
|
972
|
+
state.indexLoaded = false;
|
|
973
|
+
}
|
|
738
974
|
}
|
|
739
|
-
}
|
|
740
|
-
return "none";
|
|
741
|
-
}
|
|
742
975
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
976
|
+
/**
|
|
977
|
+
* The sparse arm: nothing to fetch when the index embeds its vocabulary
|
|
978
|
+
* (`sparse_ready()` after init_index); otherwise tokenizer.json from the
|
|
979
|
+
* repo the manifest names, checked against `vocab_hash`.
|
|
980
|
+
*/
|
|
981
|
+
async function ensureSparse() {
|
|
982
|
+
if (state.sparse) return;
|
|
983
|
+
const spec = state.manifest && state.manifest.sparse;
|
|
984
|
+
if (!spec) return;
|
|
985
|
+
if (state.degraded.some((d) => d.startsWith("sparse:"))) return;
|
|
986
|
+
try {
|
|
987
|
+
if (typeof wasm.sparse_ready === "function" && wasm.sparse_ready()) {
|
|
988
|
+
state.sparse = true;
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
const repo = spec.tokenizer || spec.model;
|
|
992
|
+
const rev = spec.revision || "main";
|
|
993
|
+
let bytes = await getModelFile(repo, rev, "tokenizer.json", "sparse");
|
|
994
|
+
let ok = await lib.verifySha256(bytes, spec.vocab_hash, env.subtle);
|
|
995
|
+
if (!ok && bytes.fromCache) {
|
|
996
|
+
await idbDelete(lib.cacheKey(repo, rev, "tokenizer.json"));
|
|
997
|
+
bytes = await getModelFile(repo, rev, "tokenizer.json", "sparse", { noCache: true });
|
|
998
|
+
ok = await lib.verifySha256(bytes, spec.vocab_hash, env.subtle);
|
|
999
|
+
}
|
|
1000
|
+
if (!ok) {
|
|
1001
|
+
throw new Error("tokenizer.json SHA-256 does not match the index's vocab_hash");
|
|
1002
|
+
}
|
|
1003
|
+
wasm.init_sparse_tokenizer(bytes);
|
|
1004
|
+
state.sparse = true;
|
|
1005
|
+
state.sparseBytes = bytes;
|
|
1006
|
+
} catch (err) {
|
|
1007
|
+
if (isFatal(err)) throw err;
|
|
1008
|
+
console.warn("eddie: sparse arm unavailable", err);
|
|
1009
|
+
state.degraded.push(`sparse: ${describe(err)}`);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
761
1012
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
1013
|
+
/** Returns "ok" (a lane is loaded), "consent" (waiting for the visitor) or "none". */
|
|
1014
|
+
async function ensureDense() {
|
|
1015
|
+
if (state.dense) return "ok";
|
|
1016
|
+
for (; state.laneIndex < state.candidates.length; state.laneIndex++) {
|
|
1017
|
+
const lane = state.candidates[state.laneIndex];
|
|
1018
|
+
try {
|
|
1019
|
+
const cached = await laneCached(lane);
|
|
1020
|
+
if (!cached && !state.consent[lane.id]) {
|
|
1021
|
+
postStatus("consent_required", Object.assign(await consentDetails(lane), {
|
|
1022
|
+
lanes: laneList(),
|
|
1023
|
+
hostSkippedLanes: state.hostSkipped.slice(),
|
|
1024
|
+
}));
|
|
1025
|
+
return "consent";
|
|
1026
|
+
}
|
|
1027
|
+
await ensureSidecar(lane, "chunks");
|
|
1028
|
+
if (lib.isWasmLane(lane)) {
|
|
1029
|
+
await loadWasmLane(lane);
|
|
1030
|
+
} else {
|
|
1031
|
+
await loadWebGpuLane(lane);
|
|
1032
|
+
}
|
|
1033
|
+
return "ok";
|
|
1034
|
+
} catch (err) {
|
|
1035
|
+
if (isFatal(err)) throw err;
|
|
1036
|
+
if (err && err.eddieTier) {
|
|
1037
|
+
// This host lacks the runtime, another tier has it: let the
|
|
1038
|
+
// widget move the search there and init again.
|
|
1039
|
+
postStatus("tier_required", { tier: String(err.eddieTier), lane: laneSummary(lane), message: describe(err) });
|
|
1040
|
+
return "tier";
|
|
1041
|
+
}
|
|
1042
|
+
console.warn(`eddie: dense lane ${lane.id} failed`, err);
|
|
1043
|
+
state.degraded.push(`dense: lane ${lane.id} failed: ${describe(err)}`);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
if (state.candidates.length === 0 && state.candidateReason && (state.manifest.dense || []).length > 0) {
|
|
1047
|
+
if (!state.degraded.some((d) => d.startsWith("dense:"))) {
|
|
1048
|
+
state.degraded.push(`dense: ${state.candidateReason}`);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
return "none";
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/** What the consent card needs: the lane, the real download size and where it comes from. */
|
|
1055
|
+
async function consentDetails(lane) {
|
|
1056
|
+
return {
|
|
785
1057
|
lane: laneSummary(lane),
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
1058
|
+
sizeBytes: await laneDownloadBytes(lane),
|
|
1059
|
+
origin: lib.laneOrigin(lane),
|
|
1060
|
+
sidecarBytes: lib.laneSidecarBytes(state.manifest, lane.id),
|
|
1061
|
+
saveData: !!(nav && nav.connection && nav.connection.saveData),
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* Download size of a lane: the byte count the manifest declares
|
|
1067
|
+
* (`runtime.bytes` on a bundled lane) or the table estimate; for a
|
|
1068
|
+
* site-bundled wasm lane whose manifest predates `runtime.bytes`, the
|
|
1069
|
+
* Content-Length of its files (one HEAD per file, same origin), or null
|
|
1070
|
+
* when the server does not say.
|
|
1071
|
+
*/
|
|
1072
|
+
async function laneDownloadBytes(lane) {
|
|
1073
|
+
const declared = lib.laneDeclaredBytes(lane);
|
|
1074
|
+
if (declared != null) return declared;
|
|
1075
|
+
// The table estimates describe the HuggingFace originals; a bundled
|
|
1076
|
+
// f16 copy is half that, so measure it rather than estimate.
|
|
1077
|
+
if (lib.laneOrigin(lane) !== "site" || !lib.isWasmLane(lane)) return lib.laneDownloadBytes(lane);
|
|
1078
|
+
if (state.siteSizes[lane.id] !== undefined) return state.siteSizes[lane.id];
|
|
1079
|
+
const doFetch = env.fetch || globalThis.fetch;
|
|
1080
|
+
let total = 0;
|
|
1081
|
+
try {
|
|
1082
|
+
for (const file of lib.laneFiles(lane)) {
|
|
1083
|
+
const res = await doFetch(laneFileUrl(lane, file), { method: "HEAD" });
|
|
1084
|
+
const len = res && res.ok && res.headers && res.headers.get ? Number(res.headers.get("Content-Length")) : NaN;
|
|
1085
|
+
if (!Number.isFinite(len) || len <= 0) {
|
|
1086
|
+
total = null;
|
|
1087
|
+
break;
|
|
1088
|
+
}
|
|
1089
|
+
total += len;
|
|
1090
|
+
}
|
|
1091
|
+
} catch (_) {
|
|
1092
|
+
total = null;
|
|
1093
|
+
}
|
|
1094
|
+
state.siteSizes[lane.id] = total;
|
|
1095
|
+
return total;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
/** `manifest.sidecars` file for (`scope`, lane), fetched next to the index and attached once. */
|
|
1099
|
+
async function ensureSidecar(lane, scope) {
|
|
1100
|
+
const side = lib.sidecarFor(state.manifest, scope, lane.id);
|
|
1101
|
+
if (!side) return false;
|
|
1102
|
+
if (state.attachedSidecars.includes(side.file)) return true;
|
|
1103
|
+
const url = lib.withVersion(new URL(side.file, state.indexUrl).href, state.version);
|
|
1104
|
+
let last = 0;
|
|
1105
|
+
postStatus("loading_sidecar", { lane: laneSummary(lane), file: side.file, progress: null, loaded: 0, total: side.bytes || null });
|
|
1106
|
+
const bytes = await lib.fetchBytes(url, {
|
|
1107
|
+
fetch: env.fetch,
|
|
1108
|
+
timeoutMs: 180000,
|
|
1109
|
+
onProgress: (loaded, total) => {
|
|
1110
|
+
const now = Date.now();
|
|
1111
|
+
if (total && now - last < PROGRESS_INTERVAL_MS && loaded !== total) return;
|
|
1112
|
+
last = now;
|
|
1113
|
+
postStatus("loading_sidecar", { lane: laneSummary(lane), file: side.file, progress: total ? loaded / total : null, loaded, total });
|
|
1114
|
+
},
|
|
790
1115
|
});
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
postStatus("loading_model", { lane: laneSummary(lane) });
|
|
794
|
-
const pooling = runtime.pooling || "mean";
|
|
795
|
-
const embed = async (text) => {
|
|
796
|
-
const prefixed = (lane.query_prefix || "") + text;
|
|
797
|
-
const out = await extractor(prefixed, { pooling, normalize: lane.normalize !== false });
|
|
798
|
-
const data = out.data instanceof Float32Array ? out.data : Float32Array.from(out.data);
|
|
799
|
-
if (typeof out.dispose === "function") out.dispose();
|
|
800
|
-
if (data.length !== lane.dim) {
|
|
801
|
-
throw new Error(`lane ${lane.id}: model produced ${data.length}-d vectors but the index stores ${lane.dim}-d`);
|
|
802
|
-
}
|
|
803
|
-
for (let i = 0; i < data.length; i++) {
|
|
804
|
-
if (!Number.isFinite(data[i])) {
|
|
805
|
-
throw new Error(`lane ${lane.id}: model produced a non-finite value at dimension ${i}`);
|
|
1116
|
+
if (typeof wasm.attach_sidecar !== "function") {
|
|
1117
|
+
throw new Error("this WASM build cannot attach sidecars (rebuild the widget)");
|
|
806
1118
|
}
|
|
1119
|
+
wasm.attach_sidecar(bytes);
|
|
1120
|
+
state.attachedSidecars.push(side.file);
|
|
1121
|
+
if (handoverPossible()) state.sidecarBytes[side.file] = bytes;
|
|
1122
|
+
return true;
|
|
807
1123
|
}
|
|
808
|
-
return data;
|
|
809
|
-
};
|
|
810
|
-
await embed("warm up");
|
|
811
|
-
state.dense = { lane, kind: "webgpu", embed, dtype };
|
|
812
|
-
await idbMetaPut(webGpuMarker(lane, dtype), Date.now());
|
|
813
|
-
}
|
|
814
1124
|
|
|
815
|
-
function
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
runtime: state.dense ? state.dense.kind : null,
|
|
821
|
-
arms: { dense: !!state.dense, sparse: state.sparse, bm25: true },
|
|
822
|
-
degraded: lib.filterDesignDegraded(unique(state.degraded)),
|
|
823
|
-
manifest: manifestSummary(),
|
|
824
|
-
});
|
|
825
|
-
}
|
|
1125
|
+
function laneFileUrl(lane, file) {
|
|
1126
|
+
const site = lib.siteModelUrl(lane, file, state.indexUrl);
|
|
1127
|
+
if (site) return lib.withVersion(site, state.version);
|
|
1128
|
+
return lib.hfFileUrl(lib.laneRepo(lane), lib.laneRevision(lane), file);
|
|
1129
|
+
}
|
|
826
1130
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
1131
|
+
async function loadWasmLane(lane) {
|
|
1132
|
+
const repo = lib.laneRepo(lane);
|
|
1133
|
+
const rev = lib.laneRevision(lane);
|
|
1134
|
+
const files = lib.laneFiles(lane);
|
|
1135
|
+
const loaded = {};
|
|
1136
|
+
for (const file of files) {
|
|
1137
|
+
loaded[file] = await getModelFile(repo, rev, file, lane.id, { url: laneFileUrl(lane, file), name: lib.laneFileName(lane, file) });
|
|
1138
|
+
}
|
|
1139
|
+
await ensureDenseWasm();
|
|
1140
|
+
postStatus("loading_model", { lane: laneSummary(lane) });
|
|
1141
|
+
const config = loaded["config.json"];
|
|
1142
|
+
const tokenizer = loaded["tokenizer.json"];
|
|
1143
|
+
const weightsName = files.find((f) => lib.isWeightsFile(f));
|
|
1144
|
+
const weights = weightsName ? loaded[weightsName] : undefined;
|
|
1145
|
+
if (!config || !tokenizer || !weights) {
|
|
1146
|
+
throw new Error(`lane ${lane.id}: runtime.files must include config.json, tokenizer.json and a weights file`);
|
|
1147
|
+
}
|
|
1148
|
+
wasm.init_dense_wasm(lane.id, config, tokenizer, weights);
|
|
1149
|
+
state.dense = { lane, kind: "wasm" };
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
async function loadWebGpuLane(lane) {
|
|
1153
|
+
const runtime = lane.runtime;
|
|
1154
|
+
postStatus("loading_model", { lane: laneSummary(lane), file: "transformers.js" });
|
|
1155
|
+
const tf = await env.loadTransformers();
|
|
1156
|
+
if (state.db && tf.env) {
|
|
1157
|
+
// Keep the ONNX files in the same IndexedDB store as the wasm lanes: the
|
|
1158
|
+
// Cache API rejects multi-hundred-MB entries on some profiles, and one
|
|
1159
|
+
// store means one eviction policy (evictStaleFiles) and one consent check.
|
|
1160
|
+
tf.env.useCustomCache = true;
|
|
1161
|
+
tf.env.customCache = transformersCache();
|
|
1162
|
+
}
|
|
1163
|
+
if (tf.env) configureModelSource(tf.env, lane);
|
|
1164
|
+
if (env.configureTransformers) env.configureTransformers(tf);
|
|
1165
|
+
const dtype = lib.pickDtype(runtime, state.gpu && state.gpu.hasF16);
|
|
1166
|
+
let last = 0;
|
|
1167
|
+
const extractor = await tf.pipeline("feature-extraction", runtime.repo, {
|
|
1168
|
+
device: "webgpu",
|
|
1169
|
+
dtype,
|
|
1170
|
+
revision: lib.laneRevision(lane),
|
|
1171
|
+
progress_callback: (p) => {
|
|
1172
|
+
if (!p || p.status !== "progress") return;
|
|
1173
|
+
const now = Date.now();
|
|
1174
|
+
if (now - last < PROGRESS_INTERVAL_MS && p.progress < 100) return;
|
|
1175
|
+
last = now;
|
|
1176
|
+
postStatus("downloading_model", {
|
|
1177
|
+
lane: laneSummary(lane),
|
|
1178
|
+
file: p.file || p.name || "",
|
|
1179
|
+
progress: typeof p.progress === "number" ? p.progress / 100 : null,
|
|
1180
|
+
loaded: p.loaded,
|
|
1181
|
+
total: p.total,
|
|
1182
|
+
});
|
|
1183
|
+
},
|
|
1184
|
+
});
|
|
1185
|
+
postStatus("loading_model", { lane: laneSummary(lane) });
|
|
1186
|
+
const pooling = runtime.pooling || "mean";
|
|
1187
|
+
const embed = async (text) => {
|
|
1188
|
+
const prefixed = (lane.query_prefix || "") + text;
|
|
1189
|
+
const out = await extractor(prefixed, { pooling, normalize: lane.normalize !== false });
|
|
1190
|
+
const data = out.data instanceof Float32Array ? out.data : Float32Array.from(out.data);
|
|
1191
|
+
if (typeof out.dispose === "function") out.dispose();
|
|
1192
|
+
if (data.length !== lane.dim) {
|
|
1193
|
+
throw new Error(`lane ${lane.id}: model produced ${data.length}-d vectors but the index stores ${lane.dim}-d`);
|
|
1194
|
+
}
|
|
1195
|
+
for (let i = 0; i < data.length; i++) {
|
|
1196
|
+
if (!Number.isFinite(data[i])) {
|
|
1197
|
+
throw new Error(`lane ${lane.id}: model produced a non-finite value at dimension ${i}`);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
return data;
|
|
1201
|
+
};
|
|
1202
|
+
await embed("warm up");
|
|
1203
|
+
state.dense = { lane, kind: "webgpu", embed, dtype };
|
|
1204
|
+
await idbMetaPut(webGpuMarker(lane, dtype), Date.now());
|
|
1205
|
+
}
|
|
848
1206
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
1207
|
+
/**
|
|
1208
|
+
* Point transformers.js at the site-hosted copy of a webgpu-onnx repo
|
|
1209
|
+
* when the manifest gives one (`runtime.base_url`, relative to the index
|
|
1210
|
+
* URL; the directory mirrors the repo's file layout: config.json,
|
|
1211
|
+
* tokenizer.json, onnx/model_q4.onnx ...). transformers.js joins
|
|
1212
|
+
* remoteHost, remotePathTemplate and the file name with "/"; "." as the
|
|
1213
|
+
* template keeps the model id out of the path and the URL parser folds
|
|
1214
|
+
* the "./" away. Without a base_url the defaults (huggingface.co) are
|
|
1215
|
+
* restored, since these settings are global to the module.
|
|
1216
|
+
*/
|
|
1217
|
+
function configureModelSource(tfEnv, lane) {
|
|
1218
|
+
const base = lib.siteModelUrl(lane, "", state.indexUrl);
|
|
1219
|
+
if (base) {
|
|
1220
|
+
tfEnv.allowRemoteModels = true;
|
|
1221
|
+
tfEnv.remoteHost = base;
|
|
1222
|
+
tfEnv.remotePathTemplate = ".";
|
|
1223
|
+
} else {
|
|
1224
|
+
tfEnv.remoteHost = "https://huggingface.co/";
|
|
1225
|
+
tfEnv.remotePathTemplate = "{model}/resolve/{revision}/";
|
|
1226
|
+
}
|
|
856
1227
|
}
|
|
857
|
-
return true;
|
|
858
|
-
}
|
|
859
|
-
const dtype = lib.pickDtype(lane.runtime, state.gpu && state.gpu.hasF16);
|
|
860
|
-
return !!(await idbMetaGet(webGpuMarker(lane, dtype)));
|
|
861
|
-
}
|
|
862
1228
|
|
|
863
|
-
function
|
|
864
|
-
|
|
865
|
-
|
|
1229
|
+
function readyMessage() {
|
|
1230
|
+
return {
|
|
1231
|
+
type: "ready",
|
|
1232
|
+
lanes: laneList(),
|
|
1233
|
+
lane: state.dense ? state.dense.lane.id : null,
|
|
1234
|
+
runtime: state.dense ? state.dense.kind : null,
|
|
1235
|
+
arms: { dense: !!state.dense, sparse: state.sparse, bm25: true },
|
|
1236
|
+
degraded: reportedDegraded(state.degraded),
|
|
1237
|
+
manifest: manifestSummary(),
|
|
1238
|
+
hostSkippedLanes: state.hostSkipped.slice(),
|
|
1239
|
+
wasm: caps && caps.dense_wasm ? "dense" : "lite",
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
866
1242
|
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
* Query vector from the WebGPU lane, or nothing (the WASM lane embeds inside
|
|
871
|
-
* search()). An embedding failure degrades the dense arm for this query
|
|
872
|
-
* instead of failing the search; `note` carries the reason.
|
|
873
|
-
*/
|
|
874
|
-
async function queryVector(query, mode) {
|
|
875
|
-
if (!state.dense || state.dense.kind !== "webgpu") return { laneId: null, vec: null, note: null };
|
|
876
|
-
if (mode !== "hybrid" && mode !== "dense") return { laneId: null, vec: null, note: null };
|
|
877
|
-
try {
|
|
878
|
-
const vec = await state.dense.embed(query);
|
|
879
|
-
return { laneId: state.dense.lane.id, vec, note: null };
|
|
880
|
-
} catch (err) {
|
|
881
|
-
console.warn(`eddie: dense lane ${state.dense.lane.id} query embedding failed`, err);
|
|
882
|
-
return { laneId: null, vec: null, note: `dense: embedding failed for lane ${state.dense.lane.id}: ${describe(err)}` };
|
|
883
|
-
}
|
|
884
|
-
}
|
|
1243
|
+
function postReady() {
|
|
1244
|
+
post(readyMessage());
|
|
1245
|
+
}
|
|
885
1246
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
1247
|
+
// -- cache check --------------------------------------------------------
|
|
1248
|
+
|
|
1249
|
+
async function handleCacheCheck(msg, reply) {
|
|
1250
|
+
try {
|
|
1251
|
+
if (state.phase === "dead") throw fatal("search engine crashed; reload to retry");
|
|
1252
|
+
if (msg.indexUrl) state.indexUrl = String(msg.indexUrl);
|
|
1253
|
+
if (msg.baseUrl != null) state.baseUrl = String(msg.baseUrl);
|
|
1254
|
+
if (msg.version != null) state.version = msg.version ? String(msg.version) : null;
|
|
1255
|
+
if (msg.denseRuntime) state.denseRuntime = String(msg.denseRuntime);
|
|
1256
|
+
if (msg.laneId !== undefined) state.laneId = msg.laneId ? String(msg.laneId) : null;
|
|
1257
|
+
await ensureWasm();
|
|
1258
|
+
await ensureIndex();
|
|
1259
|
+
await ensureDb();
|
|
1260
|
+
await ensureGpu();
|
|
1261
|
+
chooseLanes();
|
|
1262
|
+
const lane = currentCandidate();
|
|
1263
|
+
const cached = lane ? await laneCached(lane) : true;
|
|
1264
|
+
reply({
|
|
1265
|
+
type: "cache_result",
|
|
1266
|
+
requestId: msg.requestId,
|
|
1267
|
+
cached,
|
|
1268
|
+
lanes: laneList(),
|
|
1269
|
+
lane: lane ? laneSummary(lane) : null,
|
|
1270
|
+
sizeBytes: lane ? await laneDownloadBytes(lane) : 0,
|
|
1271
|
+
origin: lane ? lib.laneOrigin(lane) : null,
|
|
1272
|
+
sidecarBytes: lane ? lib.laneSidecarBytes(state.manifest, lane.id) : 0,
|
|
1273
|
+
hostSkippedLanes: state.hostSkipped.slice(),
|
|
1274
|
+
phase: state.phase,
|
|
1275
|
+
});
|
|
1276
|
+
} catch (err) {
|
|
1277
|
+
if (isFatal(err)) state.phase = "dead";
|
|
1278
|
+
postError(reply, msg.requestId, describe(err), isFatal(err));
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
/**
|
|
1283
|
+
* Delete every cached model file. The engine keeps whatever it has
|
|
1284
|
+
* already loaded into memory for this session; the files come back on the
|
|
1285
|
+
* next visit that needs them.
|
|
1286
|
+
*/
|
|
1287
|
+
async function handleCacheClear(msg, reply) {
|
|
1288
|
+
let freedBytes = 0;
|
|
1289
|
+
try {
|
|
1290
|
+
await ensureDb();
|
|
1291
|
+
const keys = (await idbRequest(IDB_FILES, "readonly", (s) => s.getAllKeys())) || [];
|
|
1292
|
+
for (const key of keys) {
|
|
1293
|
+
const value = await idbGet(key);
|
|
1294
|
+
freedBytes += value ? (typeof value.size === "number" ? value.size : value.byteLength || 0) : 0;
|
|
1295
|
+
await idbDelete(key);
|
|
904
1296
|
}
|
|
1297
|
+
const metaKeys = (await idbRequest(IDB_META, "readonly", (s) => s.getAllKeys())) || [];
|
|
1298
|
+
for (const key of metaKeys) {
|
|
1299
|
+
await idbRequest(IDB_META, "readwrite", (s) => s.delete(key));
|
|
1300
|
+
}
|
|
1301
|
+
} catch (err) {
|
|
1302
|
+
console.warn("eddie: could not clear the model cache", err);
|
|
905
1303
|
}
|
|
1304
|
+
reply({ type: "cache_clear_result", requestId: msg.requestId, freedBytes });
|
|
906
1305
|
}
|
|
907
|
-
let qa = undefined;
|
|
908
|
-
if (msg.qa && Number(msg.qa) > 0 && hasSection("qa") && (vec || !laneId)) {
|
|
909
|
-
qa = JSON.parse(wasm_bindgen.qa_lookup(query, laneId, vec, Number(msg.qa)));
|
|
910
|
-
}
|
|
911
|
-
self.postMessage({
|
|
912
|
-
type: "search_result",
|
|
913
|
-
requestId,
|
|
914
|
-
results,
|
|
915
|
-
arms: res.arms,
|
|
916
|
-
degraded: lib.filterDesignDegraded(unique((res.degraded || []).concat(state.degraded))),
|
|
917
|
-
mode: res.mode,
|
|
918
|
-
lane: res.dense_lane || null,
|
|
919
|
-
qa,
|
|
920
|
-
});
|
|
921
|
-
} catch (err) {
|
|
922
|
-
failRequest(requestId, err);
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
1306
|
|
|
926
|
-
function
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
}
|
|
1307
|
+
async function laneCached(lane) {
|
|
1308
|
+
if (lib.isWasmLane(lane)) {
|
|
1309
|
+
if (!state.db) return false;
|
|
1310
|
+
const repo = lib.laneRepo(lane);
|
|
1311
|
+
const rev = lib.laneRevision(lane);
|
|
1312
|
+
for (const file of lib.laneFiles(lane)) {
|
|
1313
|
+
if (!(await idbHas(lib.cacheKey(repo, rev, lib.laneFileName(lane, file))))) return false;
|
|
1314
|
+
}
|
|
1315
|
+
return true;
|
|
1316
|
+
}
|
|
1317
|
+
const dtype = lib.pickDtype(lane.runtime, state.gpu && state.gpu.hasF16);
|
|
1318
|
+
return !!(await idbMetaGet(webGpuMarker(lane, dtype)));
|
|
1319
|
+
}
|
|
934
1320
|
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
const query = String(msg.query || "").trim();
|
|
939
|
-
const k = Number(msg.k) > 0 ? Number(msg.k) : 3;
|
|
940
|
-
if (!query || !hasSection("qa")) {
|
|
941
|
-
self.postMessage({ type: "qa_result", requestId: msg.requestId, hits: [] });
|
|
942
|
-
return;
|
|
943
|
-
}
|
|
944
|
-
const { laneId, vec } = await queryVector(query, "dense");
|
|
945
|
-
const hits = JSON.parse(wasm_bindgen.qa_lookup(query, laneId, vec, k));
|
|
946
|
-
self.postMessage({ type: "qa_result", requestId: msg.requestId, hits });
|
|
947
|
-
} catch (err) {
|
|
948
|
-
failRequest(msg.requestId, err);
|
|
949
|
-
}
|
|
950
|
-
}
|
|
1321
|
+
function webGpuMarker(lane, dtype) {
|
|
1322
|
+
return `webgpu:${lane.runtime.repo}@${lib.laneRevision(lane)}#${dtype}`;
|
|
1323
|
+
}
|
|
951
1324
|
|
|
952
|
-
|
|
953
|
-
if (isFatal(err)) {
|
|
954
|
-
// A trapped panic leaves the WASM instance unusable; never re-enter it.
|
|
955
|
-
state.phase = "dead";
|
|
956
|
-
postError(requestId, "search engine crashed: " + describe(err), true);
|
|
957
|
-
return;
|
|
958
|
-
}
|
|
959
|
-
postError(requestId, describe(err), false);
|
|
960
|
-
}
|
|
1325
|
+
// -- queries ------------------------------------------------------------
|
|
961
1326
|
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1327
|
+
/**
|
|
1328
|
+
* Query vector from the WebGPU lane, or nothing (the WASM lane embeds inside
|
|
1329
|
+
* search()). An embedding failure degrades the dense arm for this query
|
|
1330
|
+
* instead of failing the search; `note` carries the reason.
|
|
1331
|
+
*/
|
|
1332
|
+
async function queryVector(query, mode) {
|
|
1333
|
+
if (!state.dense || state.dense.kind !== "webgpu") return { laneId: null, vec: null, note: null };
|
|
1334
|
+
if (mode !== "hybrid" && mode !== "dense") return { laneId: null, vec: null, note: null };
|
|
1335
|
+
try {
|
|
1336
|
+
const vec = await state.dense.embed(query);
|
|
1337
|
+
return { laneId: state.dense.lane.id, vec, note: null };
|
|
1338
|
+
} catch (err) {
|
|
1339
|
+
console.warn(`eddie: dense lane ${state.dense.lane.id} query embedding failed`, err);
|
|
1340
|
+
return { laneId: null, vec: null, note: `dense: embedding failed for lane ${state.dense.lane.id}: ${describe(err)}` };
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
966
1343
|
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1344
|
+
/**
|
|
1345
|
+
* The qa-scope vectors of the active lane live in a sidecar the first QA
|
|
1346
|
+
* lookup fetches (never before: most searches never show the FAQ card).
|
|
1347
|
+
* A failed fetch leaves the lexical ranking in place.
|
|
1348
|
+
*/
|
|
1349
|
+
async function ensureQaVectors() {
|
|
1350
|
+
if (!state.dense || !hasSection("qa")) return;
|
|
1351
|
+
try {
|
|
1352
|
+
await ensureSidecar(state.dense.lane, "qa");
|
|
1353
|
+
} catch (err) {
|
|
1354
|
+
if (isFatal(err)) throw err;
|
|
1355
|
+
console.warn("eddie: qa sidecar unavailable; FAQ ranking is lexical", err);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
async function handleSearch(msg, reply) {
|
|
1360
|
+
const requestId = msg.requestId;
|
|
1361
|
+
try {
|
|
1362
|
+
requireIndex();
|
|
1363
|
+
const query = String(msg.query || "").trim();
|
|
1364
|
+
if (!query) throw new Error("search: empty query");
|
|
1365
|
+
const mode = msg.mode || "hybrid";
|
|
1366
|
+
const topK = Number(msg.topK) > 0 ? Number(msg.topK) : 8;
|
|
1367
|
+
const { laneId, vec, note } = await queryVector(query, mode);
|
|
1368
|
+
const res = JSON.parse(wasm.search(query, topK, mode, laneId, vec));
|
|
1369
|
+
if (note) res.degraded = (res.degraded || []).concat([note]);
|
|
1370
|
+
const results = res.results || [];
|
|
1371
|
+
if (msg.evidence) {
|
|
1372
|
+
for (const r of results) {
|
|
1373
|
+
try {
|
|
1374
|
+
r.text = JSON.parse(wasm.chunk(r.chunk)).text;
|
|
1375
|
+
} catch (_) {
|
|
1376
|
+
r.text = r.snippet;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
let qa = undefined;
|
|
1381
|
+
if (msg.qa && Number(msg.qa) > 0 && hasSection("qa") && (vec || !laneId)) {
|
|
1382
|
+
await ensureQaVectors();
|
|
1383
|
+
qa = JSON.parse(wasm.qa_lookup(query, laneId, vec, Number(msg.qa)));
|
|
1384
|
+
}
|
|
1385
|
+
reply({
|
|
1386
|
+
type: "search_result",
|
|
1387
|
+
requestId,
|
|
1388
|
+
results,
|
|
1389
|
+
arms: res.arms,
|
|
1390
|
+
degraded: reportedDegraded((res.degraded || []).concat(state.degraded)),
|
|
1391
|
+
mode: res.mode,
|
|
1392
|
+
lane: res.dense_lane || null,
|
|
1393
|
+
qa,
|
|
1394
|
+
});
|
|
1395
|
+
} catch (err) {
|
|
1396
|
+
failRequest(reply, requestId, err);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
function handleLookup(msg, reply, type, key, fn) {
|
|
1401
|
+
try {
|
|
1402
|
+
requireIndex();
|
|
1403
|
+
reply({ type, requestId: msg.requestId, [key]: JSON.parse(fn()) });
|
|
1404
|
+
} catch (err) {
|
|
1405
|
+
failRequest(reply, msg.requestId, err);
|
|
1406
|
+
}
|
|
1407
|
+
return Promise.resolve();
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
async function handleQa(msg, reply) {
|
|
1411
|
+
try {
|
|
1412
|
+
requireIndex();
|
|
1413
|
+
const query = String(msg.query || "").trim();
|
|
1414
|
+
const k = Number(msg.k) > 0 ? Number(msg.k) : 3;
|
|
1415
|
+
if (!query || !hasSection("qa")) {
|
|
1416
|
+
reply({ type: "qa_result", requestId: msg.requestId, hits: [] });
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
await ensureQaVectors();
|
|
1420
|
+
const { laneId, vec } = await queryVector(query, "dense");
|
|
1421
|
+
const hits = JSON.parse(wasm.qa_lookup(query, laneId, vec, k));
|
|
1422
|
+
reply({ type: "qa_result", requestId: msg.requestId, hits });
|
|
1423
|
+
} catch (err) {
|
|
1424
|
+
failRequest(reply, msg.requestId, err);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
function failRequest(reply, requestId, err) {
|
|
1429
|
+
if (isFatal(err)) {
|
|
1430
|
+
// A trapped panic leaves the WASM instance unusable; never re-enter it.
|
|
1431
|
+
state.phase = "dead";
|
|
1432
|
+
postError(reply, requestId, "search engine crashed: " + describe(err), true);
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
postError(reply, requestId, describe(err), false);
|
|
1436
|
+
}
|
|
970
1437
|
|
|
971
|
-
|
|
1438
|
+
function requireIndex() {
|
|
1439
|
+
if (state.phase === "dead") throw fatal("search engine crashed; reload to retry");
|
|
1440
|
+
if (!state.indexLoaded) throw new Error(NOT_LOADED);
|
|
1441
|
+
}
|
|
972
1442
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1443
|
+
function hasSection(name) {
|
|
1444
|
+
return !!(state.manifest && Array.isArray(state.manifest.sections) && state.manifest.sections.includes(name));
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// -- model files --------------------------------------------------------
|
|
1448
|
+
|
|
1449
|
+
/**
|
|
1450
|
+
* A model file from the IndexedDB cache or the network. `opts.url` and
|
|
1451
|
+
* `opts.name` override the HuggingFace URL and cache name (site-bundled
|
|
1452
|
+
* files); `opts.noCache` skips the cache read.
|
|
1453
|
+
*/
|
|
1454
|
+
async function getModelFile(repo, rev, file, laneId, opts) {
|
|
1455
|
+
const o = opts || {};
|
|
1456
|
+
const key = lib.cacheKey(repo, rev, o.name || file);
|
|
1457
|
+
if (!o.noCache) {
|
|
1458
|
+
const cached = await idbGet(key);
|
|
1459
|
+
if (cached) {
|
|
1460
|
+
const bytes = new Uint8Array(cached);
|
|
1461
|
+
bytes.fromCache = true;
|
|
1462
|
+
return bytes;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
const url = o.url || lib.hfFileUrl(repo, rev, file);
|
|
1466
|
+
let last = 0;
|
|
1467
|
+
postStatus("downloading_model", { lane: laneId, file, progress: null, loaded: 0, total: null });
|
|
1468
|
+
const bytes = await lib.fetchBytes(url, {
|
|
1469
|
+
fetch: env.fetch,
|
|
1470
|
+
timeoutMs: lib.timeoutForFile(file),
|
|
1471
|
+
retries: 1,
|
|
1472
|
+
backoffMs: 1500,
|
|
1473
|
+
onProgress: (loaded, total) => {
|
|
1474
|
+
const now = Date.now();
|
|
1475
|
+
if (now - last < PROGRESS_INTERVAL_MS && !(total && loaded === total)) return;
|
|
1476
|
+
last = now;
|
|
1477
|
+
postStatus("downloading_model", { lane: laneId, file, progress: total ? loaded / total : null, loaded, total });
|
|
1478
|
+
},
|
|
1479
|
+
});
|
|
1480
|
+
await idbPut(key, bytes.buffer);
|
|
980
1481
|
return bytes;
|
|
981
1482
|
}
|
|
982
|
-
}
|
|
983
|
-
const url = lib.hfFileUrl(repo, rev, file);
|
|
984
|
-
let last = 0;
|
|
985
|
-
postStatus("downloading_model", { lane: laneId, file, progress: null, loaded: 0, total: null });
|
|
986
|
-
const bytes = await lib.fetchBytes(url, {
|
|
987
|
-
timeoutMs: lib.timeoutForFile(file),
|
|
988
|
-
retries: 1,
|
|
989
|
-
backoffMs: 1500,
|
|
990
|
-
onProgress: (loaded, total) => {
|
|
991
|
-
const now = Date.now();
|
|
992
|
-
if (now - last < PROGRESS_INTERVAL_MS && !(total && loaded === total)) return;
|
|
993
|
-
last = now;
|
|
994
|
-
postStatus("downloading_model", { lane: laneId, file, progress: total ? loaded / total : null, loaded, total });
|
|
995
|
-
},
|
|
996
|
-
});
|
|
997
|
-
await idbPut(key, bytes.buffer);
|
|
998
|
-
return bytes;
|
|
999
|
-
}
|
|
1000
1483
|
|
|
1001
|
-
// -- IndexedDB (best effort: any failure only logs) --------------------
|
|
1002
|
-
|
|
1003
|
-
async function ensureDb() {
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1484
|
+
// -- IndexedDB (best effort: any failure only logs) --------------------
|
|
1485
|
+
|
|
1486
|
+
async function ensureDb() {
|
|
1487
|
+
if (state.db !== undefined) return;
|
|
1488
|
+
state.db = null;
|
|
1489
|
+
if (!idb) return;
|
|
1490
|
+
try {
|
|
1491
|
+
state.db = await new Promise((resolve, reject) => {
|
|
1492
|
+
const req = idb.open(IDB_NAME, IDB_VERSION);
|
|
1493
|
+
req.onupgradeneeded = () => {
|
|
1494
|
+
const db = req.result;
|
|
1495
|
+
if (!db.objectStoreNames.contains(IDB_FILES)) db.createObjectStore(IDB_FILES);
|
|
1496
|
+
if (!db.objectStoreNames.contains(IDB_META)) db.createObjectStore(IDB_META);
|
|
1497
|
+
};
|
|
1498
|
+
req.onsuccess = () => resolve(req.result);
|
|
1499
|
+
req.onerror = () => reject(req.error || new Error("indexedDB.open failed"));
|
|
1500
|
+
req.onblocked = () => reject(new Error("indexedDB.open blocked"));
|
|
1501
|
+
});
|
|
1502
|
+
} catch (err) {
|
|
1503
|
+
console.warn("eddie: model cache unavailable; files will be re-downloaded next visit", err);
|
|
1504
|
+
state.db = null;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
function idbRequest(store, mode, fn) {
|
|
1509
|
+
if (!state.db) return Promise.resolve(undefined);
|
|
1510
|
+
return new Promise((resolve) => {
|
|
1511
|
+
try {
|
|
1512
|
+
const tx = state.db.transaction(store, mode);
|
|
1513
|
+
const req = fn(tx.objectStore(store));
|
|
1514
|
+
req.onsuccess = () => resolve(req.result);
|
|
1515
|
+
req.onerror = () => {
|
|
1516
|
+
console.warn("eddie: model cache operation failed", req.error);
|
|
1517
|
+
resolve(undefined);
|
|
1518
|
+
};
|
|
1519
|
+
} catch (err) {
|
|
1520
|
+
console.warn("eddie: model cache operation failed", err);
|
|
1521
|
+
resolve(undefined);
|
|
1522
|
+
}
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
const idbGet = (key) => idbRequest(IDB_FILES, "readonly", (s) => s.get(key));
|
|
1527
|
+
const idbHas = async (key) => (await idbRequest(IDB_FILES, "readonly", (s) => s.getKey(key))) !== undefined;
|
|
1528
|
+
const idbPut = (key, value) => idbRequest(IDB_FILES, "readwrite", (s) => s.put(value, key));
|
|
1529
|
+
const idbDelete = (key) => idbRequest(IDB_FILES, "readwrite", (s) => s.delete(key));
|
|
1530
|
+
const idbMetaGet = (key) => idbRequest(IDB_META, "readonly", (s) => s.get(key));
|
|
1531
|
+
const idbMetaPut = (key, value) => idbRequest(IDB_META, "readwrite", (s) => s.put(value, key));
|
|
1532
|
+
|
|
1533
|
+
/**
|
|
1534
|
+
* transformers.js cache (`env.customCache`) over the model-file store.
|
|
1535
|
+
* transformers.js calls `match`/`put` with string keys: the HuggingFace URL
|
|
1536
|
+
* for remote files (mapped onto the repo@revision/file scheme) or a local
|
|
1537
|
+
* model path (never stored, so `match` misses and the URL key is tried next).
|
|
1538
|
+
*/
|
|
1539
|
+
function transformersCache() {
|
|
1540
|
+
const keyOf = (req) => {
|
|
1541
|
+
const url = typeof req === "string" ? req : req && req.url ? req.url : String(req);
|
|
1542
|
+
return lib.cacheKeyFromUrl(url) || "url:" + url;
|
|
1014
1543
|
};
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1544
|
+
return {
|
|
1545
|
+
async match(req) {
|
|
1546
|
+
const stored = await idbGet(keyOf(req));
|
|
1547
|
+
if (!stored) return undefined;
|
|
1548
|
+
const size = typeof stored.size === "number" ? stored.size : stored.byteLength;
|
|
1549
|
+
return new Response(stored, { headers: { "Content-Length": String(size) } });
|
|
1550
|
+
},
|
|
1551
|
+
async put(req, response) {
|
|
1552
|
+
const blob = await response.blob();
|
|
1553
|
+
await idbPut(keyOf(req), blob);
|
|
1554
|
+
},
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
/** Delete cached files that belong to models this index no longer references. */
|
|
1559
|
+
async function evictStaleFiles() {
|
|
1560
|
+
if (!state.db || !state.manifest) return;
|
|
1561
|
+
const keep = ["url:"];
|
|
1562
|
+
for (const lane of state.manifest.dense || []) {
|
|
1563
|
+
keep.push(`${lib.laneRepo(lane)}@${lib.laneRevision(lane)}/`);
|
|
1564
|
+
}
|
|
1565
|
+
if (state.manifest.sparse) {
|
|
1566
|
+
const s = state.manifest.sparse;
|
|
1567
|
+
keep.push(`${s.tokenizer || s.model}@${s.revision || "main"}/`);
|
|
1568
|
+
}
|
|
1569
|
+
const keys = await idbRequest(IDB_FILES, "readonly", (s) => s.getAllKeys());
|
|
1570
|
+
for (const key of keys || []) {
|
|
1571
|
+
if (typeof key === "string" && !keep.some((p) => key.startsWith(p))) {
|
|
1572
|
+
console.info("eddie: evicting cached model file", key);
|
|
1573
|
+
await idbDelete(key);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
// -- helpers ------------------------------------------------------------
|
|
1024
1579
|
|
|
1025
|
-
function
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1580
|
+
function currentCandidate() {
|
|
1581
|
+
return state.candidates[state.laneIndex] || null;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
function laneSummary(lane) {
|
|
1585
|
+
return {
|
|
1586
|
+
id: lane.id,
|
|
1587
|
+
model: lane.model,
|
|
1588
|
+
kind: lane.runtime ? lane.runtime.kind : null,
|
|
1589
|
+
repo: lib.laneRepo(lane),
|
|
1590
|
+
dim: lane.dim,
|
|
1591
|
+
origin: lib.laneOrigin(lane),
|
|
1035
1592
|
};
|
|
1036
|
-
} catch (err) {
|
|
1037
|
-
console.warn("eddie: model cache operation failed", err);
|
|
1038
|
-
resolve(undefined);
|
|
1039
1593
|
}
|
|
1040
|
-
});
|
|
1041
|
-
}
|
|
1042
1594
|
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
const idbDelete = (key) => idbRequest(IDB_FILES, "readwrite", (s) => s.delete(key));
|
|
1047
|
-
const idbMetaGet = (key) => idbRequest(IDB_META, "readonly", (s) => s.get(key));
|
|
1048
|
-
const idbMetaPut = (key, value) => idbRequest(IDB_META, "readwrite", (s) => s.put(value, key));
|
|
1049
|
-
|
|
1050
|
-
/**
|
|
1051
|
-
* transformers.js cache (`env.customCache`) over the model-file store.
|
|
1052
|
-
* transformers.js calls `match`/`put` with string keys: the HuggingFace URL
|
|
1053
|
-
* for remote files (mapped onto the repo@revision/file scheme) or a local
|
|
1054
|
-
* model path (never stored, so `match` misses and the URL key is tried next).
|
|
1055
|
-
*/
|
|
1056
|
-
function transformersCache() {
|
|
1057
|
-
const keyOf = (req) => {
|
|
1058
|
-
const url = typeof req === "string" ? req : req && req.url ? req.url : String(req);
|
|
1059
|
-
return lib.cacheKeyFromUrl(url) || "url:" + url;
|
|
1060
|
-
};
|
|
1061
|
-
return {
|
|
1062
|
-
async match(req) {
|
|
1063
|
-
const stored = await idbGet(keyOf(req));
|
|
1064
|
-
if (!stored) return undefined;
|
|
1065
|
-
const size = typeof stored.size === "number" ? stored.size : stored.byteLength;
|
|
1066
|
-
return new Response(stored, { headers: { "Content-Length": String(size) } });
|
|
1067
|
-
},
|
|
1068
|
-
async put(req, response) {
|
|
1069
|
-
const blob = await response.blob();
|
|
1070
|
-
await idbPut(keyOf(req), blob);
|
|
1071
|
-
},
|
|
1072
|
-
};
|
|
1073
|
-
}
|
|
1595
|
+
function laneList() {
|
|
1596
|
+
return (state.manifest && state.manifest.dense ? state.manifest.dense : []).map(laneSummary);
|
|
1597
|
+
}
|
|
1074
1598
|
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1599
|
+
function manifestSummary() {
|
|
1600
|
+
const m = state.manifest || {};
|
|
1601
|
+
return {
|
|
1602
|
+
format: m.format,
|
|
1603
|
+
eddie: m.eddie,
|
|
1604
|
+
chunks: m.chunks,
|
|
1605
|
+
pages: m.pages,
|
|
1606
|
+
sections: m.sections || [],
|
|
1607
|
+
sparse: !!m.sparse,
|
|
1608
|
+
sidecars: Array.isArray(m.sidecars) ? m.sidecars.length : 0,
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
function postStatus(stateName, extra) {
|
|
1613
|
+
post(Object.assign({ type: "status", state: stateName }, extra || {}));
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
function postError(reply, requestId, message, isFatalErr, isUnsupportedErr) {
|
|
1617
|
+
reply({ type: "error", requestId: requestId == null ? undefined : requestId, message, fatal: !!isFatalErr, unsupported: !!isUnsupportedErr });
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
return {
|
|
1621
|
+
handle,
|
|
1622
|
+
state: snapshot,
|
|
1623
|
+
readyMessage,
|
|
1624
|
+
get phase() {
|
|
1625
|
+
return state.phase;
|
|
1626
|
+
},
|
|
1627
|
+
};
|
|
1081
1628
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1629
|
+
|
|
1630
|
+
function unique(list) {
|
|
1631
|
+
return Array.from(new Set(list));
|
|
1085
1632
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
if (
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1633
|
+
|
|
1634
|
+
function describe(err) {
|
|
1635
|
+
if (err == null) return "unknown error";
|
|
1636
|
+
if (typeof err === "string") return err;
|
|
1637
|
+
if (err.message) return err.message;
|
|
1638
|
+
return String(err);
|
|
1092
1639
|
}
|
|
1093
|
-
}
|
|
1094
1640
|
|
|
1095
|
-
|
|
1641
|
+
function unsupported(message) {
|
|
1642
|
+
const e = new Error(message);
|
|
1643
|
+
e.eddieUnsupported = true;
|
|
1644
|
+
e.eddieFatal = true;
|
|
1645
|
+
return e;
|
|
1646
|
+
}
|
|
1096
1647
|
|
|
1097
|
-
function
|
|
1098
|
-
|
|
1099
|
-
|
|
1648
|
+
function fatal(message) {
|
|
1649
|
+
const e = new Error(message);
|
|
1650
|
+
e.eddieFatal = true;
|
|
1651
|
+
return e;
|
|
1652
|
+
}
|
|
1100
1653
|
|
|
1101
|
-
function
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
model: lane.model,
|
|
1105
|
-
kind: lane.runtime ? lane.runtime.kind : null,
|
|
1106
|
-
repo: lib.laneRepo(lane),
|
|
1107
|
-
dim: lane.dim,
|
|
1108
|
-
};
|
|
1109
|
-
}
|
|
1654
|
+
function isUnsupported(err) {
|
|
1655
|
+
return !!(err && err.eddieUnsupported);
|
|
1656
|
+
}
|
|
1110
1657
|
|
|
1111
|
-
function
|
|
1112
|
-
|
|
1113
|
-
|
|
1658
|
+
function isFatal(err) {
|
|
1659
|
+
if (!err) return false;
|
|
1660
|
+
if (err.eddieFatal) return true;
|
|
1661
|
+
return typeof WebAssembly === "object" && err instanceof WebAssembly.RuntimeError;
|
|
1662
|
+
}
|
|
1114
1663
|
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
eddie: m.eddie,
|
|
1120
|
-
chunks: m.chunks,
|
|
1121
|
-
pages: m.pages,
|
|
1122
|
-
sections: m.sections || [],
|
|
1123
|
-
sparse: !!m.sparse,
|
|
1124
|
-
};
|
|
1125
|
-
}
|
|
1664
|
+
/** True for the engine's "not initialised" replies (the client re-runs init). */
|
|
1665
|
+
function isNotLoadedMessage(message) {
|
|
1666
|
+
return typeof message === "string" && message.indexOf(NOT_LOADED) === 0;
|
|
1667
|
+
}
|
|
1126
1668
|
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
}
|
|
1669
|
+
return { createSearchEngine, wasmCapabilities, NOT_LOADED, isNotLoadedMessage };
|
|
1670
|
+
});
|
|
1130
1671
|
|
|
1131
|
-
|
|
1132
|
-
self.postMessage(Object.assign({ type: "status", state: stateName }, extra || {}));
|
|
1133
|
-
}
|
|
1672
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
1134
1673
|
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1674
|
+
// Eddie search worker (classic dedicated worker): the fallback host when the
|
|
1675
|
+
// service worker (eddie-sw.js) is unavailable, and the same protocol either
|
|
1676
|
+
// way. The engine itself is widget/src/lib/search-engine.js; this file only
|
|
1677
|
+
// binds it to a dedicated worker: the WASM glue comes in through
|
|
1678
|
+
// importScripts (no-modules builds: eddie-lite.js first, eddie-dense.js only
|
|
1679
|
+
// when a CPU dense lane is about to run) and transformers.js through a
|
|
1680
|
+
// dynamic import(). widget/build.sh concatenates widget/src/lib/*.js ahead
|
|
1681
|
+
// of this file, so the helpers are available as `EddieLib`.
|
|
1682
|
+
//
|
|
1683
|
+
// Protocol: see widget/README.md ("Worker protocol").
|
|
1138
1684
|
|
|
1139
|
-
|
|
1140
|
-
if (err == null) return "unknown error";
|
|
1141
|
-
if (typeof err === "string") return err;
|
|
1142
|
-
if (err.message) return err.message;
|
|
1143
|
-
return String(err);
|
|
1144
|
-
}
|
|
1685
|
+
"use strict";
|
|
1145
1686
|
|
|
1146
|
-
|
|
1147
|
-
const e = new Error(message);
|
|
1148
|
-
e.eddieUnsupported = true;
|
|
1149
|
-
e.eddieFatal = true;
|
|
1150
|
-
return e;
|
|
1151
|
-
}
|
|
1687
|
+
const TRANSFORMERS_URL = "https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.2.0";
|
|
1152
1688
|
|
|
1153
|
-
|
|
1154
|
-
const e = new Error(message);
|
|
1155
|
-
e.eddieFatal = true;
|
|
1156
|
-
return e;
|
|
1157
|
-
}
|
|
1689
|
+
const lib = EddieLib;
|
|
1158
1690
|
|
|
1159
|
-
|
|
1160
|
-
|
|
1691
|
+
const loaded = {}; // variant -> Promise of the wasm-bindgen API object
|
|
1692
|
+
|
|
1693
|
+
function loadVariant(baseUrl, variant) {
|
|
1694
|
+
if (variant !== "lite" && variant !== "dense") throw new Error(`unknown wasm variant ${String(variant)}`);
|
|
1695
|
+
if (!loaded[variant]) {
|
|
1696
|
+
loaded[variant] = (async () => {
|
|
1697
|
+
importScripts(lib.assetUrl(baseUrl, `eddie-${variant}.js`, lib.ASSET_VERSION));
|
|
1698
|
+
// eddie-dense.js is renamed by build.sh so both glues can share this
|
|
1699
|
+
// global scope (a second `let wasm_bindgen` would be a SyntaxError).
|
|
1700
|
+
const api = variant === "dense" ? wasm_bindgen_dense : wasm_bindgen;
|
|
1701
|
+
await api({ module_or_path: lib.assetUrl(baseUrl, `eddie-${variant}.wasm`, lib.ASSET_VERSION) });
|
|
1702
|
+
return api;
|
|
1703
|
+
})();
|
|
1704
|
+
loaded[variant].catch(() => {
|
|
1705
|
+
delete loaded[variant]; // a failed fetch may be retried
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
return loaded[variant];
|
|
1161
1709
|
}
|
|
1162
1710
|
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1711
|
+
const engine = lib.createSearchEngine({
|
|
1712
|
+
post: (message) => self.postMessage(message),
|
|
1713
|
+
loadWasm: (baseUrl, variant) => loadVariant(baseUrl, variant || "lite"),
|
|
1714
|
+
loadTransformers: () => import(TRANSFORMERS_URL),
|
|
1715
|
+
canRunWebGpuLane: true,
|
|
1716
|
+
});
|
|
1717
|
+
|
|
1718
|
+
self.onmessage = function (e) {
|
|
1719
|
+
engine.handle(e.data || {}, (message) => self.postMessage(message));
|
|
1720
|
+
};
|