/** * edge-worker-client.js — Main-thread proxy for the edge-worker Web Worker. * * Spawns js/edge-worker.js as a module Worker and exposes: * classify(text) → Promise<{ * complexity: 'trivial'|'moderate'|'complex', * score: number, * reason: string, * localReply?: string // only when complexity === 'trivial' * }> * sentiment(text) → Promise<{ label:'POSITIVE'|'NEGATIVE'|'NEUTRAL', score }> * onReady(fn) → register a callback fired when models finish loading * isReady() → boolean — true once both models are warm * warmup() → start the worker early so first classify() is fast * * Complexity tiers (set by the worker): * trivial — greeting/small-talk; answered locally, 0 sats, no server call * moderate — simple question; show cost preview, route to server * complex — technical/creative/code; always priced, show cost preview * * If Web Workers are unavailable (SSR / old browser), all calls fall back * gracefully: classify → { complexity:'moderate', ... } so the app still works. */ let _worker = null; let _ready = false; let _readyCb = null; const _pending = new Map(); // id → { resolve, reject } let _nextId = 1; function _init() { if (_worker) return; try { // Use import.meta.url so Vite can resolve the worker URL correctly. // type:'module' is required for ESM imports inside the worker. _worker = new Worker(new URL('./edge-worker.js', import.meta.url), { type: 'module' }); _worker.addEventListener('message', ({ data }) => { // Lifecycle events have no id if (data?.type === 'ready') { _ready = true; if (_readyCb) { _readyCb(); _readyCb = null; } return; } if (data?.type === 'error') { console.warn('[edge-worker] worker boot error:', data.message); // Resolve all pending with fallback values for (const [, { resolve }] of _pending) resolve(_fallback(null)); _pending.clear(); return; } // Regular response: { id, result } const { id, result } = data ?? {}; const entry = _pending.get(id); if (entry) { _pending.delete(id); entry.resolve(result); } }); _worker.addEventListener('error', (err) => { console.warn('[edge-worker] worker error:', err.message); }); } catch (err) { console.warn('[edge-worker] Web Workers unavailable — using fallback routing:', err.message); _worker = null; } } function _fallback(type) { if (type === 'sentiment') return { label: 'NEUTRAL', score: 0.5 }; // classify fallback: moderate keeps the UI functional (shows estimate, routes to server) return { complexity: 'moderate', score: 0, reason: 'worker-unavailable' }; } function _send(type, text) { if (!_worker) return Promise.resolve(_fallback(type)); const id = _nextId++; return new Promise((resolve) => { _pending.set(id, { resolve, reject: resolve }); _worker.postMessage({ id, type, text }); }); } // ── Public API ──────────────────────────────────────────────────────────────── export function classify(text) { _init(); return _send('classify', text); } export function sentiment(text) { _init(); return _send('sentiment', text); } export function onReady(fn) { if (_ready) { fn(); return; } _readyCb = fn; } export function isReady() { return _ready; } /** * warmup() — start the worker (and model loading) early so classify/sentiment * calls on first user interaction don't stall waiting for models. */ export function warmup() { _init(); }