eddie-jekyll 0.2.4 → 0.4.0
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/eddie-agent-worker.js +536 -0
- data/assets/eddie-wasm.js +205 -124
- data/assets/eddie-widget.js +1832 -581
- data/assets/eddie-worker.js +1123 -182
- data/assets/eddie.wasm +0 -0
- data/lib/eddie/jekyll.rb +1 -1
- data/scripts/install.sh +2 -1
- metadata +7 -6
data/assets/eddie-worker.js
CHANGED
|
@@ -1,226 +1,1167 @@
|
|
|
1
1
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
|
+
// Generated by widget/build.sh from widget/src/worker.js and widget/src/lib/*.js; edit those instead.
|
|
3
|
+
"use strict";
|
|
4
|
+
const EddieLib = {};
|
|
5
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
6
|
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// Loads WASM module, downloads and caches the ML model in IndexedDB,
|
|
6
|
-
// and handles search queries from the main thread.
|
|
7
|
+
// URL, version and cache-key helpers shared by the widget and the worker.
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
(function (factory) {
|
|
10
|
+
const api = factory();
|
|
11
|
+
if (typeof module === "object" && module && module.exports) {
|
|
12
|
+
module.exports = api;
|
|
13
|
+
} else if (typeof EddieLib === "object" && EddieLib) {
|
|
14
|
+
Object.assign(EddieLib, api);
|
|
15
|
+
} else {
|
|
16
|
+
globalThis.EddieLib = Object.assign(globalThis.EddieLib || {}, api);
|
|
17
|
+
}
|
|
18
|
+
})(function () {
|
|
19
|
+
"use strict";
|
|
9
20
|
|
|
10
|
-
|
|
11
|
-
const HF_CDN = "https://huggingface.co";
|
|
12
|
-
const MODEL_FILES = ["config.json", "tokenizer.json", "model.safetensors"];
|
|
13
|
-
const IDB_NAME = "eddie-models";
|
|
14
|
-
const IDB_STORE = "files";
|
|
21
|
+
const HF_HOST = "https://huggingface.co";
|
|
15
22
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
/** Directory of an absolute URL, without query or hash, with a trailing slash. */
|
|
24
|
+
function baseUrlOf(href) {
|
|
25
|
+
const u = new URL(href);
|
|
26
|
+
u.search = "";
|
|
27
|
+
u.hash = "";
|
|
28
|
+
const s = u.href;
|
|
29
|
+
return s.substring(0, s.lastIndexOf("/") + 1);
|
|
30
|
+
}
|
|
19
31
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
32
|
+
/** The `v` query parameter of a URL (absolute or relative), or null. */
|
|
33
|
+
function versionOf(href) {
|
|
34
|
+
if (!href) return null;
|
|
35
|
+
const q = href.indexOf("?");
|
|
36
|
+
if (q < 0) return null;
|
|
37
|
+
const params = new URLSearchParams(href.substring(q + 1).split("#")[0]);
|
|
38
|
+
const v = params.get("v");
|
|
39
|
+
return v ? v : null;
|
|
40
|
+
}
|
|
23
41
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
42
|
+
/** Append `?v=<version>` unless the URL already carries a `v` parameter. */
|
|
43
|
+
function withVersion(url, version) {
|
|
44
|
+
if (!version || versionOf(url)) return url;
|
|
45
|
+
const hashAt = url.indexOf("#");
|
|
46
|
+
const hash = hashAt >= 0 ? url.substring(hashAt) : "";
|
|
47
|
+
const body = hashAt >= 0 ? url.substring(0, hashAt) : url;
|
|
48
|
+
const sep = body.includes("?") ? (body.endsWith("?") || body.endsWith("&") ? "" : "&") : "?";
|
|
49
|
+
return body + sep + "v=" + encodeURIComponent(version) + hash;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Runtime asset next to the widget script, version-busted when a version is known. */
|
|
53
|
+
function assetUrl(baseUrl, name, version) {
|
|
54
|
+
const base = baseUrl ? baseUrl.replace(/\/?$/, "/") : "";
|
|
55
|
+
return withVersion(base + name, version);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** `https://huggingface.co/<repo>/resolve/<revision>/<file>` */
|
|
59
|
+
function hfFileUrl(repo, revision, file) {
|
|
60
|
+
const rev = encodeURIComponent(revision || "main");
|
|
61
|
+
const path = String(file).split("/").map(encodeURIComponent).join("/");
|
|
62
|
+
return `${HF_HOST}/${repo}/resolve/${rev}/${path}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** IndexedDB key for a cached model file. */
|
|
66
|
+
function cacheKey(repo, revision, file) {
|
|
67
|
+
return `${repo}@${revision || "main"}/${file}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const HF_RESOLVE_RE = /^https:\/\/huggingface\.co\/([^/]+\/[^/]+)\/resolve\/([^/]+)\/([^?#]+)/;
|
|
71
|
+
|
|
72
|
+
/** The `cacheKey` of an `hfFileUrl`-shaped URL, or null for any other URL. */
|
|
73
|
+
function cacheKeyFromUrl(url) {
|
|
74
|
+
const m = HF_RESOLVE_RE.exec(String(url || ""));
|
|
75
|
+
if (!m) return null;
|
|
76
|
+
return cacheKey(m[1], decodeURIComponent(m[2]), m[3].split("/").map(decodeURIComponent).join("/"));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isWeightsFile(file) {
|
|
80
|
+
return /\.(safetensors|onnx|onnx_data|bin|data|pt|gguf)$/i.test(String(file));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Per-file fetch timeout: 10 minutes for weights, 60 seconds for everything else. */
|
|
84
|
+
function timeoutForFile(file) {
|
|
85
|
+
return isWeightsFile(file) ? 600000 : 60000;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
HF_HOST,
|
|
90
|
+
baseUrlOf,
|
|
91
|
+
versionOf,
|
|
92
|
+
withVersion,
|
|
93
|
+
assetUrl,
|
|
94
|
+
hfFileUrl,
|
|
95
|
+
cacheKey,
|
|
96
|
+
cacheKeyFromUrl,
|
|
97
|
+
isWeightsFile,
|
|
98
|
+
timeoutForFile,
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
103
|
+
|
|
104
|
+
// Dense-lane selection, download sizes, consent copy and degraded-arm notices.
|
|
105
|
+
// Pure functions over the index manifest (see src/manifest.rs).
|
|
106
|
+
|
|
107
|
+
(function (factory) {
|
|
108
|
+
const api = factory();
|
|
109
|
+
if (typeof module === "object" && module && module.exports) {
|
|
110
|
+
module.exports = api;
|
|
111
|
+
} else if (typeof EddieLib === "object" && EddieLib) {
|
|
112
|
+
Object.assign(EddieLib, api);
|
|
113
|
+
} else {
|
|
114
|
+
globalThis.EddieLib = Object.assign(globalThis.EddieLib || {}, api);
|
|
115
|
+
}
|
|
116
|
+
})(function () {
|
|
117
|
+
"use strict";
|
|
118
|
+
|
|
119
|
+
const DEFAULT_WASM_FILES = ["config.json", "tokenizer.json", "model.safetensors"];
|
|
120
|
+
|
|
121
|
+
// Approximate download sizes (bytes) keyed by lower-cased HuggingFace repo.
|
|
122
|
+
const DOWNLOAD_SIZES = {
|
|
123
|
+
"sentence-transformers/multi-qa-minilm-l6-cos-v1": 91e6,
|
|
124
|
+
"sentence-transformers/all-minilm-l6-v2": 91e6,
|
|
125
|
+
"sentence-transformers/all-minilm-l12-v2": 134e6,
|
|
126
|
+
"sentence-transformers/paraphrase-multilingual-minilm-l12-v2": 471e6,
|
|
127
|
+
"baai/bge-small-en-v1.5": 134e6,
|
|
128
|
+
"baai/bge-base-en-v1.5": 438e6,
|
|
129
|
+
"onnx-community/qwen3-embedding-0.6b-onnx": 900e6,
|
|
130
|
+
"onnx-community/harrier-oss-v1-0.6b-onnx": 400e6,
|
|
131
|
+
"xenova/bge-m3": 570e6,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
function laneKind(lane) {
|
|
135
|
+
return lane && lane.runtime ? lane.runtime.kind : null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isWasmLane(lane) {
|
|
139
|
+
return laneKind(lane) === "wasm-candle";
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isWebGpuLane(lane) {
|
|
143
|
+
return laneKind(lane) === "webgpu-onnx";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Repo the runtime downloads from. */
|
|
147
|
+
function laneRepo(lane) {
|
|
148
|
+
if (isWebGpuLane(lane)) return lane.runtime.repo;
|
|
149
|
+
return lane.model;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* HuggingFace revision for the runtime's downloads. `lane.revision` pins
|
|
154
|
+
* `lane.model` (the repo the indexer used); a webgpu-onnx lane downloads
|
|
155
|
+
* from a different repo (`runtime.repo`) that the manifest does not pin.
|
|
156
|
+
*/
|
|
157
|
+
function laneRevision(lane) {
|
|
158
|
+
if (isWebGpuLane(lane)) return "main";
|
|
159
|
+
return lane.revision || "main";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Files a wasm-candle lane needs (config, tokenizer, weights). */
|
|
163
|
+
function laneFiles(lane) {
|
|
164
|
+
if (!isWasmLane(lane)) return [];
|
|
165
|
+
const files = Array.isArray(lane.runtime.files) ? lane.runtime.files : [];
|
|
166
|
+
return files.length ? files.slice() : DEFAULT_WASM_FILES.slice();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function isWeightsFile(file) {
|
|
170
|
+
return /\.(safetensors|bin|pt|gguf|onnx)$/i.test(String(file));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Why the WASM loader (init_dense_wasm) cannot run a wasm-candle lane, or
|
|
175
|
+
* null when it can: it accepts BERT-family lanes with config.json,
|
|
176
|
+
* tokenizer.json and exactly one unsharded model.safetensors.
|
|
177
|
+
*/
|
|
178
|
+
function wasmLaneProblem(lane) {
|
|
179
|
+
if (!isWasmLane(lane)) return "not a wasm-candle lane";
|
|
180
|
+
if (lane.family && lane.family !== "bert") return `family ${lane.family} is not supported in WASM`;
|
|
181
|
+
const files = laneFiles(lane);
|
|
182
|
+
for (const need of ["config.json", "tokenizer.json"]) {
|
|
183
|
+
if (!files.includes(need)) return `runtime.files lacks ${need}`;
|
|
29
184
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
185
|
+
const weights = files.filter(isWeightsFile);
|
|
186
|
+
if (weights.length === 1 && weights[0] === "model.safetensors") return null;
|
|
187
|
+
if (weights.length === 0) return "runtime.files lists no weights file";
|
|
188
|
+
return `WASM needs a single model.safetensors, not ${weights.join(", ")}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Ordered dense-lane candidates for this browser, plus the lanes skipped
|
|
193
|
+
* because this runtime cannot load them (`skipped: [{lane, reason}]`).
|
|
194
|
+
* `denseRuntime`: "auto" | "wasm" | "webgpu"; `hasWebGpu`: an adapter was obtained.
|
|
195
|
+
*/
|
|
196
|
+
function chooseDenseLanes(manifest, opts) {
|
|
197
|
+
const lanes = (manifest && Array.isArray(manifest.dense)) ? manifest.dense : [];
|
|
198
|
+
const runtime = (opts && opts.denseRuntime) || "auto";
|
|
199
|
+
const hasWebGpu = !!(opts && opts.hasWebGpu);
|
|
200
|
+
const webgpu = lanes.filter(isWebGpuLane);
|
|
201
|
+
const skipped = [];
|
|
202
|
+
const wasm = lanes.filter(isWasmLane).filter((lane) => {
|
|
203
|
+
const problem = wasmLaneProblem(lane);
|
|
204
|
+
if (problem) skipped.push({ lane, reason: problem });
|
|
205
|
+
return !problem;
|
|
206
|
+
});
|
|
207
|
+
if (lanes.length === 0) {
|
|
208
|
+
return { candidates: [], skipped, reason: "index has no dense lane" };
|
|
209
|
+
}
|
|
210
|
+
let candidates;
|
|
211
|
+
if (runtime === "wasm") {
|
|
212
|
+
candidates = wasm;
|
|
213
|
+
} else if (runtime === "webgpu") {
|
|
214
|
+
candidates = hasWebGpu ? webgpu : [];
|
|
215
|
+
} else {
|
|
216
|
+
candidates = hasWebGpu ? webgpu.concat(wasm) : wasm;
|
|
217
|
+
}
|
|
218
|
+
let reason = null;
|
|
219
|
+
if (candidates.length === 0) {
|
|
220
|
+
if (runtime === "wasm") reason = skipped.length ? "no wasm-candle lane the WASM loader can run" : "index has no wasm-candle lane";
|
|
221
|
+
else if (!hasWebGpu && webgpu.length && !wasm.length) reason = "no WebGPU adapter";
|
|
222
|
+
else if (runtime === "webgpu" && !hasWebGpu) reason = "no WebGPU adapter";
|
|
223
|
+
else if (runtime === "webgpu") reason = "index has no webgpu-onnx lane";
|
|
224
|
+
else reason = "no runnable dense lane";
|
|
225
|
+
}
|
|
226
|
+
return { candidates, skipped, reason };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** transformers.js dtype for a webgpu-onnx lane on this adapter. */
|
|
230
|
+
function pickDtype(runtime, hasF16) {
|
|
231
|
+
if (hasF16 && runtime.dtype_f16) return runtime.dtype_f16;
|
|
232
|
+
return runtime.dtype || "fp32";
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Approximate bytes a lane downloads on first use, or null when unknown. */
|
|
236
|
+
function laneDownloadBytes(lane) {
|
|
237
|
+
const repo = (laneRepo(lane) || "").toLowerCase();
|
|
238
|
+
return Object.prototype.hasOwnProperty.call(DOWNLOAD_SIZES, repo) ? DOWNLOAD_SIZES[repo] : null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function formatBytes(n) {
|
|
242
|
+
if (n == null || !Number.isFinite(n)) return "unknown size";
|
|
243
|
+
if (n >= 1e9) {
|
|
244
|
+
const g = n / 1e9;
|
|
245
|
+
return (g >= 10 ? Math.round(g) : Math.round(g * 10) / 10) + " GB";
|
|
246
|
+
}
|
|
247
|
+
if (n >= 1e6) return Math.round(n / 1e6) + " MB";
|
|
248
|
+
if (n >= 1e3) return Math.round(n / 1e3) + " KB";
|
|
249
|
+
return n + " B";
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Consent card copy for a model download. `consentText` (site override) may
|
|
254
|
+
* contain `{size}` and `{model}` placeholders.
|
|
255
|
+
*/
|
|
256
|
+
function consentCopy(opts) {
|
|
257
|
+
const size = formatBytes(opts.sizeBytes);
|
|
258
|
+
const model = opts.model || "the search model";
|
|
259
|
+
if (opts.consentText) {
|
|
260
|
+
return opts.consentText.replace(/\{size\}/g, size).replace(/\{model\}/g, model);
|
|
261
|
+
}
|
|
262
|
+
const sized = opts.sizeBytes == null ? "a one-time download (size unknown)" : `a one-time ${size} download`;
|
|
263
|
+
let text = `Semantic search runs a language model in your browser. That needs ${sized} of ${model}, kept in your browser's cache for next time.`;
|
|
264
|
+
if (opts.saveData) {
|
|
265
|
+
text = "Data saver is on. " + text;
|
|
266
|
+
}
|
|
267
|
+
return text;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Drop degraded notes that describe the index design rather than a failure
|
|
272
|
+
* (an index without a dense lane or sparse arm is not "degraded").
|
|
273
|
+
*/
|
|
274
|
+
function filterDesignDegraded(degraded) {
|
|
275
|
+
return (degraded || []).filter((d) => !/index has no (dense lane|sparse arm)/.test(d));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Human notice when a semantic arm is missing, or null when nothing to say. */
|
|
279
|
+
function degradedNotice(arms, degraded) {
|
|
280
|
+
const named = filterDesignDegraded(degraded).filter((d) => /^(dense|sparse)\b/.test(d));
|
|
281
|
+
if (named.length === 0) return null;
|
|
282
|
+
const a = arms || {};
|
|
283
|
+
if (!a.dense && !a.sparse) {
|
|
284
|
+
return "Keyword-only results: the semantic model isn't available.";
|
|
285
|
+
}
|
|
286
|
+
if (!a.dense) {
|
|
287
|
+
return "Keyword and sparse results only: the dense model isn't available.";
|
|
288
|
+
}
|
|
289
|
+
return "Results without the learned-sparse arm.";
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
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
|
+
formatBytes,
|
|
307
|
+
consentCopy,
|
|
308
|
+
filterDesignDegraded,
|
|
309
|
+
degradedNotice,
|
|
310
|
+
};
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
314
|
+
|
|
315
|
+
// Streaming download with progress, per-attempt timeout and one retry, plus
|
|
316
|
+
// SHA-256 verification. `fetch`, `sleep` and `subtle` are injectable for tests.
|
|
317
|
+
|
|
318
|
+
(function (factory) {
|
|
319
|
+
const api = factory();
|
|
320
|
+
if (typeof module === "object" && module && module.exports) {
|
|
321
|
+
module.exports = api;
|
|
322
|
+
} else if (typeof EddieLib === "object" && EddieLib) {
|
|
323
|
+
Object.assign(EddieLib, api);
|
|
324
|
+
} else {
|
|
325
|
+
globalThis.EddieLib = Object.assign(globalThis.EddieLib || {}, api);
|
|
326
|
+
}
|
|
327
|
+
})(function () {
|
|
328
|
+
"use strict";
|
|
329
|
+
|
|
330
|
+
class HttpError extends Error {
|
|
331
|
+
constructor(url, status) {
|
|
332
|
+
super(`HTTP ${status} fetching ${url}`);
|
|
333
|
+
this.name = "HttpError";
|
|
334
|
+
this.status = status;
|
|
335
|
+
this.url = url;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function isRetryableStatus(status) {
|
|
340
|
+
return status === 429 || status === 408 || status >= 500;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function isNetworkError(err) {
|
|
344
|
+
if (!err) return false;
|
|
345
|
+
if (err.name === "AbortError" || err.name === "TimeoutError") return true;
|
|
346
|
+
if (err instanceof HttpError) return isRetryableStatus(err.status);
|
|
347
|
+
// fetch() rejects with a TypeError on network failures (ERR_NETWORK_CHANGED etc.)
|
|
348
|
+
return err.name === "TypeError";
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function readBody(response, onProgress) {
|
|
352
|
+
const lenHeader = response.headers && response.headers.get ? response.headers.get("Content-Length") : null;
|
|
353
|
+
const total = lenHeader && /^\d+$/.test(lenHeader) && Number(lenHeader) > 0 ? Number(lenHeader) : null;
|
|
354
|
+
if (onProgress) onProgress(0, total);
|
|
355
|
+
if (!response.body || typeof response.body.getReader !== "function") {
|
|
356
|
+
const buf = new Uint8Array(await response.arrayBuffer());
|
|
357
|
+
if (onProgress) onProgress(buf.length, total == null ? buf.length : total);
|
|
358
|
+
return buf;
|
|
359
|
+
}
|
|
360
|
+
const reader = response.body.getReader();
|
|
361
|
+
let loaded = 0;
|
|
362
|
+
if (total != null) {
|
|
363
|
+
const out = new Uint8Array(total);
|
|
364
|
+
for (;;) {
|
|
365
|
+
const { done, value } = await reader.read();
|
|
366
|
+
if (done) break;
|
|
367
|
+
if (loaded + value.length > total) {
|
|
368
|
+
throw new Error("response longer than Content-Length");
|
|
369
|
+
}
|
|
370
|
+
out.set(value, loaded);
|
|
371
|
+
loaded += value.length;
|
|
372
|
+
if (onProgress) onProgress(loaded, total);
|
|
34
373
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (typeof wasm_bindgen.search_with_answer !== "function") {
|
|
38
|
-
throw new Error("WASM does not expose search_with_answer");
|
|
374
|
+
if (loaded !== total) {
|
|
375
|
+
throw new TypeError(`incomplete response: ${loaded} of ${total} bytes`);
|
|
39
376
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
377
|
+
return out;
|
|
378
|
+
}
|
|
379
|
+
const chunks = [];
|
|
380
|
+
for (;;) {
|
|
381
|
+
const { done, value } = await reader.read();
|
|
382
|
+
if (done) break;
|
|
383
|
+
chunks.push(value);
|
|
384
|
+
loaded += value.length;
|
|
385
|
+
if (onProgress) onProgress(loaded, null);
|
|
386
|
+
}
|
|
387
|
+
const out = new Uint8Array(loaded);
|
|
388
|
+
let offset = 0;
|
|
389
|
+
for (const c of chunks) {
|
|
390
|
+
out.set(c, offset);
|
|
391
|
+
offset += c.length;
|
|
392
|
+
}
|
|
393
|
+
return out;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Download `url` as a Uint8Array.
|
|
398
|
+
* opts: { fetch, timeoutMs (per attempt), retries (default 1), backoffMs (default 1500),
|
|
399
|
+
* onProgress(loaded, total|null), sleep, signal }
|
|
400
|
+
* Retries once on network errors, timeouts, 408/429/5xx. 4xx is final.
|
|
401
|
+
*/
|
|
402
|
+
async function fetchBytes(url, opts) {
|
|
403
|
+
const o = opts || {};
|
|
404
|
+
const doFetch = o.fetch || globalThis.fetch;
|
|
405
|
+
const sleep = o.sleep || ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
406
|
+
const retries = o.retries == null ? 1 : o.retries;
|
|
407
|
+
const backoffMs = o.backoffMs == null ? 1500 : o.backoffMs;
|
|
408
|
+
let lastErr = null;
|
|
409
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
410
|
+
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
411
|
+
let timer = null;
|
|
412
|
+
if (controller && o.timeoutMs) {
|
|
413
|
+
timer = setTimeout(() => controller.abort(), o.timeoutMs);
|
|
414
|
+
}
|
|
415
|
+
const onOuterAbort = () => controller && controller.abort();
|
|
416
|
+
if (o.signal && controller) o.signal.addEventListener("abort", onOuterAbort);
|
|
56
417
|
try {
|
|
57
|
-
|
|
58
|
-
|
|
418
|
+
const response = await doFetch(url, { signal: controller ? controller.signal : undefined, cache: "default" });
|
|
419
|
+
if (!response.ok) {
|
|
420
|
+
throw new HttpError(url, response.status);
|
|
59
421
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
answer: null,
|
|
73
|
-
degraded: true,
|
|
74
|
-
laneError: err.message || String(err),
|
|
75
|
-
});
|
|
76
|
-
} catch (fallbackErr) {
|
|
77
|
-
self.postMessage({
|
|
78
|
-
type: "error",
|
|
79
|
-
requestId: msg.requestId,
|
|
80
|
-
error: fallbackErr.message || fallbackErr.toString(),
|
|
81
|
-
});
|
|
422
|
+
return await readBody(response, o.onProgress);
|
|
423
|
+
} catch (err) {
|
|
424
|
+
lastErr = err;
|
|
425
|
+
if (o.signal && o.signal.aborted) throw err;
|
|
426
|
+
if (attempt < retries && isNetworkError(err)) {
|
|
427
|
+
await sleep(backoffMs * (attempt + 1));
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
throw err;
|
|
431
|
+
} finally {
|
|
432
|
+
if (timer) clearTimeout(timer);
|
|
433
|
+
if (o.signal && controller) o.signal.removeEventListener("abort", onOuterAbort);
|
|
82
434
|
}
|
|
83
435
|
}
|
|
436
|
+
throw lastErr;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function toHex(buffer) {
|
|
440
|
+
const bytes = new Uint8Array(buffer);
|
|
441
|
+
let s = "";
|
|
442
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
443
|
+
s += bytes[i].toString(16).padStart(2, "0");
|
|
444
|
+
}
|
|
445
|
+
return s;
|
|
84
446
|
}
|
|
447
|
+
|
|
448
|
+
async function sha256Hex(bytes, subtle) {
|
|
449
|
+
const s = subtle || (globalThis.crypto && globalThis.crypto.subtle);
|
|
450
|
+
if (!s) throw new Error("crypto.subtle unavailable");
|
|
451
|
+
const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
452
|
+
return toHex(await s.digest("SHA-256", view));
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** True when the SHA-256 of `bytes` equals `expectedHex` (case-insensitive, optional "sha256:" prefix). */
|
|
456
|
+
async function verifySha256(bytes, expectedHex, subtle) {
|
|
457
|
+
if (!expectedHex) return false;
|
|
458
|
+
const want = String(expectedHex).replace(/^sha256:/i, "").trim().toLowerCase();
|
|
459
|
+
if (!/^[0-9a-f]{64}$/.test(want)) return false;
|
|
460
|
+
const got = await sha256Hex(bytes, subtle);
|
|
461
|
+
return got === want;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return { HttpError, isNetworkError, isRetryableStatus, fetchBytes, sha256Hex, verifySha256, toHex };
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
468
|
+
|
|
469
|
+
// Eddie search worker (classic worker).
|
|
470
|
+
//
|
|
471
|
+
// widget/build.sh concatenates widget/src/lib/*.js ahead of this file, so the
|
|
472
|
+
// pure helpers are available as `EddieLib`. The WASM glue is loaded with
|
|
473
|
+
// importScripts; transformers.js (WebGPU dense lane) with a dynamic import().
|
|
474
|
+
//
|
|
475
|
+
// Protocol (main thread -> worker):
|
|
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?}
|
|
493
|
+
|
|
494
|
+
"use strict";
|
|
495
|
+
|
|
496
|
+
const IDB_NAME = "eddie-models";
|
|
497
|
+
const IDB_VERSION = 2;
|
|
498
|
+
const IDB_FILES = "files";
|
|
499
|
+
const IDB_META = "meta";
|
|
500
|
+
const TRANSFORMERS_URL = "https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.2.0";
|
|
501
|
+
const PROGRESS_INTERVAL_MS = 80;
|
|
502
|
+
|
|
503
|
+
const lib = EddieLib;
|
|
504
|
+
|
|
505
|
+
const state = {
|
|
506
|
+
phase: "idle", // idle | loading | awaiting_consent | ready | error | dead
|
|
507
|
+
initRunning: false,
|
|
508
|
+
rerun: false,
|
|
509
|
+
baseUrl: "",
|
|
510
|
+
version: null,
|
|
511
|
+
indexUrl: null,
|
|
512
|
+
denseRuntime: "auto",
|
|
513
|
+
consent: Object.create(null), // lane id -> true
|
|
514
|
+
wasmReady: false,
|
|
515
|
+
indexLoaded: false,
|
|
516
|
+
manifest: null,
|
|
517
|
+
candidates: [],
|
|
518
|
+
candidateReason: null,
|
|
519
|
+
laneIndex: 0,
|
|
520
|
+
dense: null, // { lane, kind: "wasm" | "webgpu", embed? }
|
|
521
|
+
sparse: false,
|
|
522
|
+
degraded: [],
|
|
523
|
+
gpu: null, // { adapter, hasF16, maxBufferSize } | false
|
|
524
|
+
db: undefined, // IDBDatabase | null (unavailable)
|
|
85
525
|
};
|
|
86
526
|
|
|
87
|
-
|
|
88
|
-
|
|
527
|
+
self.onmessage = function (e) {
|
|
528
|
+
const msg = e.data || {};
|
|
529
|
+
switch (msg.type) {
|
|
530
|
+
case "init":
|
|
531
|
+
handleInit(msg);
|
|
532
|
+
break;
|
|
533
|
+
case "cache_check":
|
|
534
|
+
handleCacheCheck(msg);
|
|
535
|
+
break;
|
|
536
|
+
case "search":
|
|
537
|
+
handleSearch(msg);
|
|
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
|
+
};
|
|
552
|
+
|
|
553
|
+
// -- init ---------------------------------------------------------------
|
|
554
|
+
|
|
555
|
+
function handleInit(msg) {
|
|
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
|
+
}
|
|
89
571
|
|
|
90
|
-
|
|
572
|
+
async function runInit() {
|
|
573
|
+
if (state.initRunning) {
|
|
574
|
+
state.rerun = true;
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
state.initRunning = true;
|
|
578
|
+
try {
|
|
579
|
+
state.phase = "loading";
|
|
580
|
+
await ensureWasm();
|
|
581
|
+
await ensureIndex();
|
|
582
|
+
await ensureDb();
|
|
583
|
+
await ensureGpu();
|
|
584
|
+
await ensureSparse();
|
|
585
|
+
const outcome = await ensureDense();
|
|
586
|
+
if (outcome === "consent") {
|
|
587
|
+
state.phase = "awaiting_consent";
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
state.phase = "ready";
|
|
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();
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
async function ensureWasm() {
|
|
610
|
+
if (state.wasmReady) return;
|
|
91
611
|
postStatus("loading_wasm");
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
modelId,
|
|
121
|
-
"tokenizer.json",
|
|
122
|
-
(loaded, total) => postStatus("downloading_model", { progress: loaded / total, file: "tokenizer.json" })
|
|
123
|
-
);
|
|
124
|
-
const weights = await getCachedOrFetch(
|
|
125
|
-
db,
|
|
126
|
-
modelId,
|
|
127
|
-
"model.safetensors",
|
|
128
|
-
(loaded, total) => postStatus("downloading_model", { progress: loaded / total, file: "model.safetensors" })
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
// 5. Initialize WASM engine
|
|
132
|
-
postStatus("initializing");
|
|
133
|
-
wasm_bindgen.init_engine(
|
|
134
|
-
new Uint8Array(config),
|
|
135
|
-
new Uint8Array(tokenizer),
|
|
136
|
-
new Uint8Array(weights),
|
|
137
|
-
indexBytes
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
initialized = true;
|
|
141
|
-
postStatus("ready");
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// -- IndexedDB helpers --
|
|
145
|
-
function openModelDB() {
|
|
146
|
-
return new Promise((resolve, reject) => {
|
|
147
|
-
const req = indexedDB.open(IDB_NAME, 1);
|
|
148
|
-
req.onupgradeneeded = () => {
|
|
149
|
-
req.result.createObjectStore(IDB_STORE);
|
|
150
|
-
};
|
|
151
|
-
req.onsuccess = () => resolve(req.result);
|
|
152
|
-
req.onerror = () => reject(req.error);
|
|
612
|
+
if (typeof WebAssembly !== "object") {
|
|
613
|
+
throw unsupported("This browser can't run WebAssembly.");
|
|
614
|
+
}
|
|
615
|
+
try {
|
|
616
|
+
importScripts(lib.assetUrl(state.baseUrl, "eddie-wasm.js", state.version));
|
|
617
|
+
await wasm_bindgen({ module_or_path: lib.assetUrl(state.baseUrl, "eddie.wasm", state.version) });
|
|
618
|
+
} catch (err) {
|
|
619
|
+
if (err instanceof WebAssembly.CompileError || err instanceof WebAssembly.LinkError) {
|
|
620
|
+
throw unsupported("This browser can't run the search engine (WebAssembly SIMD is required).");
|
|
621
|
+
}
|
|
622
|
+
throw err;
|
|
623
|
+
}
|
|
624
|
+
state.wasmReady = true;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
async function ensureIndex() {
|
|
628
|
+
if (state.indexLoaded) return;
|
|
629
|
+
if (!state.indexUrl) throw new Error("init: indexUrl is required");
|
|
630
|
+
postStatus("loading_index", { progress: null });
|
|
631
|
+
let last = 0;
|
|
632
|
+
const bytes = await lib.fetchBytes(state.indexUrl, {
|
|
633
|
+
timeoutMs: 180000,
|
|
634
|
+
onProgress: (loaded, total) => {
|
|
635
|
+
const now = Date.now();
|
|
636
|
+
if (total && now - last < PROGRESS_INTERVAL_MS && loaded !== total) return;
|
|
637
|
+
last = now;
|
|
638
|
+
postStatus("loading_index", { progress: total ? loaded / total : null, loaded, total });
|
|
639
|
+
},
|
|
153
640
|
});
|
|
641
|
+
state.manifest = JSON.parse(wasm_bindgen.manifest(bytes));
|
|
642
|
+
wasm_bindgen.init_index(bytes);
|
|
643
|
+
state.indexLoaded = true;
|
|
644
|
+
state.dense = null;
|
|
645
|
+
state.sparse = false;
|
|
646
|
+
state.degraded = [];
|
|
647
|
+
state.laneIndex = 0;
|
|
648
|
+
postStatus("index_ready", { manifest: manifestSummary(), lanes: laneList() });
|
|
154
649
|
}
|
|
155
650
|
|
|
156
|
-
function
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
651
|
+
async function ensureGpu() {
|
|
652
|
+
if (state.gpu !== null) return;
|
|
653
|
+
state.gpu = false;
|
|
654
|
+
try {
|
|
655
|
+
if (state.denseRuntime !== "wasm" && self.navigator && navigator.gpu && typeof navigator.gpu.requestAdapter === "function") {
|
|
656
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
657
|
+
if (adapter) {
|
|
658
|
+
state.gpu = {
|
|
659
|
+
adapter,
|
|
660
|
+
hasF16: !!(adapter.features && adapter.features.has("shader-f16")),
|
|
661
|
+
maxBufferSize: adapter.limits ? adapter.limits.maxBufferSize : 0,
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
}
|
|
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,
|
|
162
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
|
+
}
|
|
163
679
|
}
|
|
164
680
|
|
|
165
|
-
function
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
681
|
+
async function ensureSparse() {
|
|
682
|
+
if (state.sparse) return;
|
|
683
|
+
const spec = state.manifest && state.manifest.sparse;
|
|
684
|
+
if (!spec) return;
|
|
685
|
+
if (state.degraded.some((d) => d.startsWith("sparse:"))) return;
|
|
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
|
+
}
|
|
707
|
+
|
|
708
|
+
/** Returns "ok" (a lane is loaded), "consent" (waiting for the visitor) or "none". */
|
|
709
|
+
async function ensureDense() {
|
|
710
|
+
if (state.dense) return "ok";
|
|
711
|
+
for (; state.laneIndex < state.candidates.length; state.laneIndex++) {
|
|
712
|
+
const lane = state.candidates[state.laneIndex];
|
|
713
|
+
try {
|
|
714
|
+
const cached = await laneCached(lane);
|
|
715
|
+
if (!cached && !state.consent[lane.id]) {
|
|
716
|
+
postStatus("consent_required", {
|
|
717
|
+
lane: laneSummary(lane),
|
|
718
|
+
sizeBytes: lib.laneDownloadBytes(lane),
|
|
719
|
+
saveData: !!(self.navigator && navigator.connection && navigator.connection.saveData),
|
|
720
|
+
});
|
|
721
|
+
return "consent";
|
|
722
|
+
}
|
|
723
|
+
if (lib.isWasmLane(lane)) {
|
|
724
|
+
await loadWasmLane(lane);
|
|
725
|
+
} else {
|
|
726
|
+
await loadWebGpuLane(lane);
|
|
727
|
+
}
|
|
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
|
+
}
|
|
734
|
+
}
|
|
735
|
+
if (state.candidates.length === 0 && state.candidateReason && (state.manifest.dense || []).length > 0) {
|
|
736
|
+
if (!state.degraded.some((d) => d.startsWith("dense:"))) {
|
|
737
|
+
state.degraded.push(`dense: ${state.candidateReason}`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return "none";
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
async function loadWasmLane(lane) {
|
|
744
|
+
const repo = lib.laneRepo(lane);
|
|
745
|
+
const rev = lib.laneRevision(lane);
|
|
746
|
+
const files = lib.laneFiles(lane);
|
|
747
|
+
const loaded = {};
|
|
748
|
+
for (const file of files) {
|
|
749
|
+
loaded[file] = await getModelFile(repo, rev, file, lane.id);
|
|
750
|
+
}
|
|
751
|
+
postStatus("loading_model", { lane: laneSummary(lane) });
|
|
752
|
+
const config = loaded["config.json"];
|
|
753
|
+
const tokenizer = loaded["tokenizer.json"];
|
|
754
|
+
const weights = files.map((f) => loaded[f]).find((b, i) => lib.isWeightsFile(files[i]));
|
|
755
|
+
if (!config || !tokenizer || !weights) {
|
|
756
|
+
throw new Error(`lane ${lane.id}: runtime.files must include config.json, tokenizer.json and a weights file`);
|
|
757
|
+
}
|
|
758
|
+
wasm_bindgen.init_dense_wasm(lane.id, config, tokenizer, weights);
|
|
759
|
+
state.dense = { lane, kind: "wasm" };
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
async function loadWebGpuLane(lane) {
|
|
763
|
+
const runtime = lane.runtime;
|
|
764
|
+
postStatus("loading_model", { lane: laneSummary(lane), file: "transformers.js" });
|
|
765
|
+
const tf = await import(TRANSFORMERS_URL);
|
|
766
|
+
if (state.db && tf.env) {
|
|
767
|
+
// Keep the ONNX files in the same IndexedDB store as the wasm lanes: the
|
|
768
|
+
// Cache API rejects multi-hundred-MB entries on some profiles, and one
|
|
769
|
+
// store means one eviction policy (evictStaleFiles) and one consent check.
|
|
770
|
+
tf.env.useCustomCache = true;
|
|
771
|
+
tf.env.customCache = transformersCache();
|
|
772
|
+
}
|
|
773
|
+
const dtype = lib.pickDtype(runtime, state.gpu && state.gpu.hasF16);
|
|
774
|
+
let last = 0;
|
|
775
|
+
const extractor = await tf.pipeline("feature-extraction", runtime.repo, {
|
|
776
|
+
device: "webgpu",
|
|
777
|
+
dtype,
|
|
778
|
+
revision: lib.laneRevision(lane),
|
|
779
|
+
progress_callback: (p) => {
|
|
780
|
+
if (!p || p.status !== "progress") return;
|
|
781
|
+
const now = Date.now();
|
|
782
|
+
if (now - last < PROGRESS_INTERVAL_MS && p.progress < 100) return;
|
|
783
|
+
last = now;
|
|
784
|
+
postStatus("downloading_model", {
|
|
785
|
+
lane: laneSummary(lane),
|
|
786
|
+
file: p.file || p.name || "",
|
|
787
|
+
progress: typeof p.progress === "number" ? p.progress / 100 : null,
|
|
788
|
+
loaded: p.loaded,
|
|
789
|
+
total: p.total,
|
|
790
|
+
});
|
|
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}`);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
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
|
+
|
|
815
|
+
function postReady() {
|
|
816
|
+
self.postMessage({
|
|
817
|
+
type: "ready",
|
|
818
|
+
lanes: laneList(),
|
|
819
|
+
lane: state.dense ? state.dense.lane.id : null,
|
|
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(),
|
|
171
824
|
});
|
|
172
825
|
}
|
|
173
826
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
827
|
+
// -- cache check --------------------------------------------------------
|
|
828
|
+
|
|
829
|
+
async function handleCacheCheck(msg) {
|
|
830
|
+
try {
|
|
831
|
+
await ensureWasm();
|
|
832
|
+
await ensureIndex();
|
|
833
|
+
await ensureDb();
|
|
834
|
+
await ensureGpu();
|
|
835
|
+
const lane = currentCandidate();
|
|
836
|
+
const cached = lane ? await laneCached(lane) : true;
|
|
837
|
+
self.postMessage({
|
|
838
|
+
type: "cache_result",
|
|
839
|
+
requestId: msg.requestId,
|
|
840
|
+
cached,
|
|
841
|
+
lane: lane ? laneSummary(lane) : null,
|
|
842
|
+
sizeBytes: lane ? lib.laneDownloadBytes(lane) : 0,
|
|
843
|
+
});
|
|
844
|
+
} catch (err) {
|
|
845
|
+
postError(msg.requestId, describe(err), isFatal(err));
|
|
179
846
|
}
|
|
847
|
+
}
|
|
180
848
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
849
|
+
async function laneCached(lane) {
|
|
850
|
+
if (lib.isWasmLane(lane)) {
|
|
851
|
+
if (!state.db) return false;
|
|
852
|
+
const repo = lib.laneRepo(lane);
|
|
853
|
+
const rev = lib.laneRevision(lane);
|
|
854
|
+
for (const file of lib.laneFiles(lane)) {
|
|
855
|
+
if (!(await idbHas(lib.cacheKey(repo, rev, file)))) return false;
|
|
856
|
+
}
|
|
857
|
+
return true;
|
|
185
858
|
}
|
|
859
|
+
const dtype = lib.pickDtype(lane.runtime, state.gpu && state.gpu.hasF16);
|
|
860
|
+
return !!(await idbMetaGet(webGpuMarker(lane, dtype)));
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function webGpuMarker(lane, dtype) {
|
|
864
|
+
return `webgpu:${lane.runtime.repo}@${lib.laneRevision(lane)}#${dtype}`;
|
|
865
|
+
}
|
|
186
866
|
|
|
187
|
-
|
|
188
|
-
const reader = response.body.getReader();
|
|
189
|
-
const chunks = [];
|
|
190
|
-
let loaded = 0;
|
|
867
|
+
// -- queries ------------------------------------------------------------
|
|
191
868
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
+
}
|
|
885
|
+
|
|
886
|
+
async function handleSearch(msg) {
|
|
887
|
+
const requestId = msg.requestId;
|
|
888
|
+
try {
|
|
889
|
+
requireIndex();
|
|
890
|
+
const query = String(msg.query || "").trim();
|
|
891
|
+
if (!query) throw new Error("search: empty query");
|
|
892
|
+
const mode = msg.mode || "hybrid";
|
|
893
|
+
const topK = Number(msg.topK) > 0 ? Number(msg.topK) : 8;
|
|
894
|
+
const { laneId, vec, note } = await queryVector(query, mode);
|
|
895
|
+
const res = JSON.parse(wasm_bindgen.search(query, topK, mode, laneId, vec));
|
|
896
|
+
if (note) res.degraded = (res.degraded || []).concat([note]);
|
|
897
|
+
const results = res.results || [];
|
|
898
|
+
if (msg.evidence) {
|
|
899
|
+
for (const r of results) {
|
|
900
|
+
try {
|
|
901
|
+
r.text = JSON.parse(wasm_bindgen.chunk(r.chunk)).text;
|
|
902
|
+
} catch (_) {
|
|
903
|
+
r.text = r.snippet;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
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)));
|
|
199
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
|
+
|
|
926
|
+
function handleLookup(msg, type, key, fn) {
|
|
927
|
+
try {
|
|
928
|
+
requireIndex();
|
|
929
|
+
self.postMessage({ type, requestId: msg.requestId, [key]: JSON.parse(fn()) });
|
|
930
|
+
} catch (err) {
|
|
931
|
+
failRequest(msg.requestId, err);
|
|
200
932
|
}
|
|
933
|
+
}
|
|
201
934
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
935
|
+
async function handleQa(msg) {
|
|
936
|
+
try {
|
|
937
|
+
requireIndex();
|
|
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);
|
|
209
949
|
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function failRequest(requestId, err) {
|
|
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
|
+
}
|
|
961
|
+
|
|
962
|
+
function requireIndex() {
|
|
963
|
+
if (state.phase === "dead") throw fatal("search engine crashed; reload to retry");
|
|
964
|
+
if (!state.indexLoaded) throw new Error("index not loaded yet");
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function hasSection(name) {
|
|
968
|
+
return !!(state.manifest && Array.isArray(state.manifest.sections) && state.manifest.sections.includes(name));
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// -- model files --------------------------------------------------------
|
|
972
|
+
|
|
973
|
+
async function getModelFile(repo, rev, file, laneId, opts) {
|
|
974
|
+
const key = lib.cacheKey(repo, rev, file);
|
|
975
|
+
if (!(opts && opts.noCache)) {
|
|
976
|
+
const cached = await idbGet(key);
|
|
977
|
+
if (cached) {
|
|
978
|
+
const bytes = new Uint8Array(cached);
|
|
979
|
+
bytes.fromCache = true;
|
|
980
|
+
return bytes;
|
|
981
|
+
}
|
|
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
|
+
|
|
1001
|
+
// -- IndexedDB (best effort: any failure only logs) --------------------
|
|
1002
|
+
|
|
1003
|
+
async function ensureDb() {
|
|
1004
|
+
if (state.db !== undefined) return;
|
|
1005
|
+
state.db = null;
|
|
1006
|
+
if (typeof indexedDB === "undefined") return;
|
|
1007
|
+
try {
|
|
1008
|
+
state.db = await new Promise((resolve, reject) => {
|
|
1009
|
+
const req = indexedDB.open(IDB_NAME, IDB_VERSION);
|
|
1010
|
+
req.onupgradeneeded = () => {
|
|
1011
|
+
const db = req.result;
|
|
1012
|
+
if (!db.objectStoreNames.contains(IDB_FILES)) db.createObjectStore(IDB_FILES);
|
|
1013
|
+
if (!db.objectStoreNames.contains(IDB_META)) db.createObjectStore(IDB_META);
|
|
1014
|
+
};
|
|
1015
|
+
req.onsuccess = () => resolve(req.result);
|
|
1016
|
+
req.onerror = () => reject(req.error || new Error("indexedDB.open failed"));
|
|
1017
|
+
req.onblocked = () => reject(new Error("indexedDB.open blocked"));
|
|
1018
|
+
});
|
|
1019
|
+
} catch (err) {
|
|
1020
|
+
console.warn("eddie: model cache unavailable; files will be re-downloaded next visit", err);
|
|
1021
|
+
state.db = null;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function idbRequest(store, mode, fn) {
|
|
1026
|
+
if (!state.db) return Promise.resolve(undefined);
|
|
1027
|
+
return new Promise((resolve) => {
|
|
1028
|
+
try {
|
|
1029
|
+
const tx = state.db.transaction(store, mode);
|
|
1030
|
+
const req = fn(tx.objectStore(store));
|
|
1031
|
+
req.onsuccess = () => resolve(req.result);
|
|
1032
|
+
req.onerror = () => {
|
|
1033
|
+
console.warn("eddie: model cache operation failed", req.error);
|
|
1034
|
+
resolve(undefined);
|
|
1035
|
+
};
|
|
1036
|
+
} catch (err) {
|
|
1037
|
+
console.warn("eddie: model cache operation failed", err);
|
|
1038
|
+
resolve(undefined);
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
210
1042
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
1043
|
+
const idbGet = (key) => idbRequest(IDB_FILES, "readonly", (s) => s.get(key));
|
|
1044
|
+
const idbHas = async (key) => (await idbRequest(IDB_FILES, "readonly", (s) => s.getKey(key))) !== undefined;
|
|
1045
|
+
const idbPut = (key, value) => idbRequest(IDB_FILES, "readwrite", (s) => s.put(value, key));
|
|
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
|
+
};
|
|
214
1073
|
}
|
|
215
1074
|
|
|
216
|
-
|
|
217
|
-
function
|
|
218
|
-
if (
|
|
219
|
-
|
|
1075
|
+
/** Delete cached files that belong to models this index no longer references. */
|
|
1076
|
+
async function evictStaleFiles() {
|
|
1077
|
+
if (!state.db || !state.manifest) return;
|
|
1078
|
+
const keep = ["url:"];
|
|
1079
|
+
for (const lane of state.manifest.dense || []) {
|
|
1080
|
+
keep.push(`${lib.laneRepo(lane)}@${lib.laneRevision(lane)}/`);
|
|
1081
|
+
}
|
|
1082
|
+
if (state.manifest.sparse) {
|
|
1083
|
+
const s = state.manifest.sparse;
|
|
1084
|
+
keep.push(`${s.tokenizer || s.model}@${s.revision || "main"}/`);
|
|
220
1085
|
}
|
|
221
|
-
|
|
1086
|
+
const keys = await idbRequest(IDB_FILES, "readonly", (s) => s.getAllKeys());
|
|
1087
|
+
for (const key of keys || []) {
|
|
1088
|
+
if (typeof key === "string" && !keep.some((p) => key.startsWith(p))) {
|
|
1089
|
+
console.info("eddie: evicting cached model file", key);
|
|
1090
|
+
await idbDelete(key);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// -- helpers ------------------------------------------------------------
|
|
1096
|
+
|
|
1097
|
+
function currentCandidate() {
|
|
1098
|
+
return state.candidates[state.laneIndex] || null;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function laneSummary(lane) {
|
|
1102
|
+
return {
|
|
1103
|
+
id: lane.id,
|
|
1104
|
+
model: lane.model,
|
|
1105
|
+
kind: lane.runtime ? lane.runtime.kind : null,
|
|
1106
|
+
repo: lib.laneRepo(lane),
|
|
1107
|
+
dim: lane.dim,
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
function laneList() {
|
|
1112
|
+
return (state.manifest && state.manifest.dense ? state.manifest.dense : []).map(laneSummary);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
function manifestSummary() {
|
|
1116
|
+
const m = state.manifest || {};
|
|
1117
|
+
return {
|
|
1118
|
+
format: m.format,
|
|
1119
|
+
eddie: m.eddie,
|
|
1120
|
+
chunks: m.chunks,
|
|
1121
|
+
pages: m.pages,
|
|
1122
|
+
sections: m.sections || [],
|
|
1123
|
+
sparse: !!m.sparse,
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function unique(list) {
|
|
1128
|
+
return Array.from(new Set(list));
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
function postStatus(stateName, extra) {
|
|
1132
|
+
self.postMessage(Object.assign({ type: "status", state: stateName }, extra || {}));
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function postError(requestId, message, isFatalErr, isUnsupported) {
|
|
1136
|
+
self.postMessage({ type: "error", requestId: requestId == null ? undefined : requestId, message, fatal: !!isFatalErr, unsupported: !!isUnsupported });
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function describe(err) {
|
|
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
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function unsupported(message) {
|
|
1147
|
+
const e = new Error(message);
|
|
1148
|
+
e.eddieUnsupported = true;
|
|
1149
|
+
e.eddieFatal = true;
|
|
1150
|
+
return e;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
function fatal(message) {
|
|
1154
|
+
const e = new Error(message);
|
|
1155
|
+
e.eddieFatal = true;
|
|
1156
|
+
return e;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
function isUnsupported(err) {
|
|
1160
|
+
return !!(err && err.eddieUnsupported);
|
|
222
1161
|
}
|
|
223
1162
|
|
|
224
|
-
function
|
|
225
|
-
|
|
1163
|
+
function isFatal(err) {
|
|
1164
|
+
if (!err) return false;
|
|
1165
|
+
if (err.eddieFatal) return true;
|
|
1166
|
+
return typeof WebAssembly === "object" && err instanceof WebAssembly.RuntimeError;
|
|
226
1167
|
}
|