eddie-jekyll 0.2.0 → 0.2.4

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.
@@ -0,0 +1,226 @@
1
+ // SPDX-License-Identifier: GPL-3.0-only
2
+
3
+ // Eddie Web Worker
4
+ //
5
+ // Loads WASM module, downloads and caches the ML model in IndexedDB,
6
+ // and handles search queries from the main thread.
7
+
8
+ "use strict";
9
+
10
+ // -- Configuration --
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";
15
+
16
+ // -- State --
17
+ let baseUrl = "";
18
+ let initialized = false;
19
+
20
+ // -- Message handler --
21
+ self.onmessage = async function (e) {
22
+ const msg = e.data;
23
+
24
+ if (msg.type === "init") {
25
+ try {
26
+ await initialize(msg.indexUrl, msg.baseUrl);
27
+ } catch (err) {
28
+ postStatus("error", { error: err.message || String(err) });
29
+ }
30
+ } else if (msg.type === "search") {
31
+ try {
32
+ if (!initialized) {
33
+ throw new Error("Engine not initialized");
34
+ }
35
+ const mode = msg.mode || "hybrid";
36
+ const topK = msg.topK || 5;
37
+ if (typeof wasm_bindgen.search_with_answer !== "function") {
38
+ throw new Error("WASM does not expose search_with_answer");
39
+ }
40
+ const bundle = wasm_bindgen.search_with_answer(
41
+ msg.query,
42
+ topK,
43
+ msg.answerTopK || 5,
44
+ mode,
45
+ !!msg.answerMode,
46
+ msg.qaSubject || ""
47
+ );
48
+ self.postMessage({
49
+ type: "search_result",
50
+ requestId: msg.requestId,
51
+ results: bundle.results || [],
52
+ answer: bundle.answer || null,
53
+ });
54
+ } catch (err) {
55
+ // Keep UX alive on occasional wasm/runtime faults by degrading to keyword lane.
56
+ try {
57
+ if (typeof wasm_bindgen.search_with_answer !== "function") {
58
+ throw err;
59
+ }
60
+ const fallback = wasm_bindgen.search_with_answer(
61
+ msg.query,
62
+ msg.topK || 5,
63
+ msg.answerTopK || 5,
64
+ "keyword",
65
+ false,
66
+ msg.qaSubject || ""
67
+ );
68
+ self.postMessage({
69
+ type: "search_result",
70
+ requestId: msg.requestId,
71
+ results: fallback.results || [],
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
+ });
82
+ }
83
+ }
84
+ }
85
+ };
86
+
87
+ async function initialize(indexUrl, workerBaseUrl) {
88
+ baseUrl = workerBaseUrl || "";
89
+
90
+ // 1. Load WASM glue + instantiate
91
+ postStatus("loading_wasm");
92
+ const wasmGlueUrl = resolveUrl("eddie-wasm.js");
93
+ importScripts(wasmGlueUrl);
94
+ const wasmBinaryUrl = resolveUrl("eddie.wasm");
95
+ await wasm_bindgen(wasmBinaryUrl);
96
+
97
+ // 2. Fetch index
98
+ postStatus("loading_index");
99
+ const indexResponse = await fetch(indexUrl);
100
+ if (!indexResponse.ok) {
101
+ throw new Error(`Failed to fetch index: ${indexResponse.status}`);
102
+ }
103
+ const indexBytes = new Uint8Array(await indexResponse.arrayBuffer());
104
+
105
+ // 3. Parse model ID from index bytes (supports raw .bin and compressed .ed)
106
+ const modelId = wasm_bindgen.extract_model_id(indexBytes);
107
+
108
+ // 4. Fetch model files (with IndexedDB cache)
109
+ postStatus("checking_cache");
110
+ const db = await openModelDB();
111
+
112
+ const config = await getCachedOrFetch(
113
+ db,
114
+ modelId,
115
+ "config.json",
116
+ (loaded, total) => postStatus("downloading_model", { progress: loaded / total, file: "config.json" })
117
+ );
118
+ const tokenizer = await getCachedOrFetch(
119
+ db,
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);
153
+ });
154
+ }
155
+
156
+ function idbGet(db, key) {
157
+ return new Promise((resolve, reject) => {
158
+ const tx = db.transaction(IDB_STORE, "readonly");
159
+ const req = tx.objectStore(IDB_STORE).get(key);
160
+ req.onsuccess = () => resolve(req.result);
161
+ req.onerror = () => reject(req.error);
162
+ });
163
+ }
164
+
165
+ function idbPut(db, key, value) {
166
+ return new Promise((resolve, reject) => {
167
+ const tx = db.transaction(IDB_STORE, "readwrite");
168
+ const req = tx.objectStore(IDB_STORE).put(value, key);
169
+ req.onsuccess = () => resolve();
170
+ req.onerror = () => reject(req.error);
171
+ });
172
+ }
173
+
174
+ async function getCachedOrFetch(db, modelId, filename, onProgress) {
175
+ const key = `${modelId}/${filename}`;
176
+ const cached = await idbGet(db, key);
177
+ if (cached) {
178
+ return cached;
179
+ }
180
+
181
+ const url = `${HF_CDN}/${modelId}/resolve/main/${filename}`;
182
+ const response = await fetch(url);
183
+ if (!response.ok) {
184
+ throw new Error(`Failed to download ${filename}: ${response.status}`);
185
+ }
186
+
187
+ const contentLength = parseInt(response.headers.get("Content-Length") || "0", 10);
188
+ const reader = response.body.getReader();
189
+ const chunks = [];
190
+ let loaded = 0;
191
+
192
+ while (true) {
193
+ const { done, value } = await reader.read();
194
+ if (done) break;
195
+ chunks.push(value);
196
+ loaded += value.length;
197
+ if (contentLength > 0) {
198
+ onProgress(loaded, contentLength);
199
+ }
200
+ }
201
+
202
+ // Concatenate chunks into single ArrayBuffer
203
+ const total = chunks.reduce((sum, c) => sum + c.length, 0);
204
+ const result = new Uint8Array(total);
205
+ let offset = 0;
206
+ for (const chunk of chunks) {
207
+ result.set(chunk, offset);
208
+ offset += chunk.length;
209
+ }
210
+
211
+ const buffer = result.buffer;
212
+ await idbPut(db, key, buffer);
213
+ return buffer;
214
+ }
215
+
216
+ // -- Utilities --
217
+ function resolveUrl(filename) {
218
+ if (baseUrl) {
219
+ return baseUrl.replace(/\/$/, "") + "/" + filename;
220
+ }
221
+ return filename;
222
+ }
223
+
224
+ function postStatus(state, extra) {
225
+ self.postMessage({ type: "status", state, ...extra });
226
+ }
data/assets/eddie.wasm ADDED
Binary file
@@ -11,6 +11,12 @@ end
11
11
 
12
12
  root = Pathname.new(__dir__).join("..").expand_path
13
13
  script = root.join("scripts", "install.sh")
14
- asset_root = ARGV[1] || "/repo/dist"
14
+ env = {
15
+ "EDDIE_PACKAGE_ROOT" => root.to_s
16
+ }
15
17
 
16
- exec("bash", script.to_s, File.expand_path(site_dir), File.expand_path(asset_root))
18
+ if ARGV[1]
19
+ exec(env, "bash", script.to_s, File.expand_path(site_dir), File.expand_path(ARGV[1]))
20
+ else
21
+ exec(env, "bash", script.to_s, File.expand_path(site_dir))
22
+ end
data/lib/eddie/jekyll.rb CHANGED
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Eddie
4
4
  module Jekyll
5
- VERSION = "0.2.0"
5
+ VERSION = "0.2.4"
6
6
  end
7
7
  end
data/scripts/install.sh CHANGED
@@ -2,7 +2,19 @@
2
2
  set -euo pipefail
3
3
 
4
4
  SITE_DIR="${1:?usage: install.sh <jekyll-site-dir>}"
5
- ASSET_ROOT="${2:-/repo/dist}"
5
+ ASSET_ROOT="${2:-}"
6
+ PACKAGE_ROOT="${EDDIE_PACKAGE_ROOT:-}"
7
+ ASSETS=(eddie-widget.js eddie-worker.js eddie-wasm.js eddie.wasm)
8
+
9
+ if [[ -z "$ASSET_ROOT" && -n "$PACKAGE_ROOT" ]]; then
10
+ ASSET_ROOT="$PACKAGE_ROOT/assets"
11
+ fi
12
+
13
+ if [[ -z "$ASSET_ROOT" ]]; then
14
+ echo "No asset root provided and no packaged assets found." >&2
15
+ echo "Pass an explicit asset-root or set EDDIE_PACKAGE_ROOT." >&2
16
+ exit 1
17
+ fi
6
18
 
7
19
  require_asset() {
8
20
  local asset_name="$1"
@@ -13,7 +25,7 @@ require_asset() {
13
25
  fi
14
26
  }
15
27
 
16
- for asset in eddie-widget.js eddie-worker.js eddie-wasm.js eddie.wasm; do
28
+ for asset in "${ASSETS[@]}"; do
17
29
  require_asset "$asset"
18
30
  done
19
31
 
metadata CHANGED
@@ -1,14 +1,34 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eddie-jekyll
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jason Grey
8
8
  bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies: []
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: jt55401-eddie-cli
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.2'
19
+ - - ">="
20
+ - !ruby/object:Gem::Version
21
+ version: 0.2.4
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - "~>"
27
+ - !ruby/object:Gem::Version
28
+ version: '0.2'
29
+ - - ">="
30
+ - !ruby/object:Gem::Version
31
+ version: 0.2.4
12
32
  description: Provides a CLI helper that runs Eddie's Jekyll installer script.
13
33
  executables:
14
34
  - eddie-jekyll-install
@@ -17,6 +37,10 @@ extra_rdoc_files: []
17
37
  files:
18
38
  - LICENSE.txt
19
39
  - README.md
40
+ - assets/eddie-wasm.js
41
+ - assets/eddie-widget.js
42
+ - assets/eddie-worker.js
43
+ - assets/eddie.wasm
20
44
  - exe/eddie-jekyll-install
21
45
  - lib/eddie/jekyll.rb
22
46
  - scripts/install.sh