v3: hardened opencv.js loader — fetch with download progress, robust runtime detection, 45s failover, retryable

This commit is contained in:
2026-08-14 21:53:36 +10:00
parent 290976a6cc
commit 77e0845a71
+57 -23
View File
@@ -26,32 +26,66 @@ export function isCvReady() { return !!(_cv && _cv.Mat); }
export function getCv() { return _cv; } export function getCv() { return _cv; }
export function _injectCv(m) { _cv = m; } // test hook (node) export function _injectCv(m) { _cv = m; } // test hook (node)
export function loadOpenCV(url = '/vendor/opencv.js') { export function loadOpenCV(url = '/vendor/opencv.js', onProgress = null) {
if (isCvReady()) return Promise.resolve(_cv); if (isCvReady()) return Promise.resolve(_cv);
if (_loading) return _loading; if (_loading) return _loading;
_loading = new Promise((resolve, reject) => { _loading = (async () => {
const s = document.createElement('script'); // 1) download with progress (13MB — visitors on exhibit wifi deserve a %)
s.src = url; const resp = await fetch(url);
s.async = true; if (!resp.ok) throw new Error(`opencv.js HTTP ${resp.status}`);
s.onerror = () => reject(new Error('opencv.js failed to load')); const total = +resp.headers.get('content-length') || 0;
s.onload = () => { let blob;
if (resp.body && resp.body.getReader) {
const reader = resp.body.getReader();
const chunks = [];
let got = 0;
for (;;) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value); got += value.length;
if (onProgress) onProgress(total ? Math.round(got / total * 100) : Math.round(got / 1e6) + 'MB');
}
blob = new Blob(chunks, { type: 'text/javascript' });
} else {
blob = await resp.blob();
}
// 2) evaluate
if (onProgress) onProgress('init');
const src = URL.createObjectURL(blob);
try {
await new Promise((res, rej) => {
const s = document.createElement('script');
s.src = src;
s.onload = res;
s.onerror = () => rej(new Error('opencv.js eval failed'));
document.head.appendChild(s);
});
} finally {
setTimeout(() => URL.revokeObjectURL(src), 5000);
}
// 3) resolve the runtime — window.cv may be the module, a thenable that
// resolves to it, or get swapped mid-init depending on the emscripten
// build. Poll fresh every tick; hard 45s ceiling so a hang becomes a
// visible failover instead of an eternal "loading".
const t0 = performance.now();
let thenAttached = false;
for (;;) {
const mod = window.cv; const mod = window.cv;
const finish = (m) => { _cv = m; resolve(m); }; if (mod && mod.Mat) { _cv = mod; return mod; }
if (!mod) return reject(new Error('opencv.js loaded but window.cv missing')); if (mod && typeof mod.then === 'function' && !thenAttached) {
if (mod.Mat) return finish(mod); // already initialized thenAttached = true;
if (typeof mod.then === 'function') return mod.then(finish, reject); // promise build mod.then((m) => { if (m && m.Mat) { window.cv = m; } }, () => {});
mod.onRuntimeInitialized = () => finish(mod); // classic emscripten }
// safety: poll in case onRuntimeInitialized was already consumed if (mod && !mod.Mat && typeof mod.then !== 'function' && !mod.__nbxHook) {
const t0 = performance.now(); mod.__nbxHook = true;
(function poll() { const prev = mod.onRuntimeInitialized;
if (_cv) return; mod.onRuntimeInitialized = () => { if (typeof prev === 'function') prev(); };
if (mod.Mat) return finish(mod); }
if (performance.now() - t0 > 30000) return reject(new Error('opencv.js init timeout')); if (performance.now() - t0 > 45000) throw new Error('opencv.js init timeout');
setTimeout(poll, 100); await new Promise(r => setTimeout(r, 100));
})(); }
}; })();
document.head.appendChild(s); _loading.catch(() => { _loading = null; }); // allow retry after failure
});
return _loading; return _loading;
} }