Skip to content

Commit a26b15b

Browse files
Lazy load open cv (#6236)
Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
1 parent 4e4918b commit a26b15b

3 files changed

Lines changed: 142 additions & 70 deletions

File tree

frontend/index.html

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,5 @@
1919
<noscript>You need to enable JavaScript to run this app.</noscript>
2020
<div id="root"></div>
2121
<script type="module" src="src/index.tsx"></script>
22-
<!-- jscanify and OpenCV for mobile scanner - loaded after React for non-blocking page load -->
23-
<script src="/vendor/jscanify/opencv.js" async></script>
24-
<script src="/vendor/jscanify/jscanify.js" async></script>
2522
</body>
2623
</html>

frontend/src/core/pages/MobileScannerPage.tsx

Lines changed: 27 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,7 @@ import PhotoCameraRoundedIcon from "@mui/icons-material/PhotoCameraRounded";
2020
import UploadRoundedIcon from "@mui/icons-material/UploadRounded";
2121
import AddPhotoAlternateRoundedIcon from "@mui/icons-material/AddPhotoAlternateRounded";
2222
import CheckCircleRoundedIcon from "@mui/icons-material/CheckCircleRounded";
23-
24-
// jscanify is loaded via script tag in index.html as a global
25-
declare global {
26-
interface Window {
27-
jscanify: any;
28-
cv: any;
29-
}
30-
}
23+
import { loadJscanify } from "@app/utils/loadJscanify";
3124

3225
/**
3326
* MobileScannerPage
@@ -136,69 +129,36 @@ export default function MobileScannerPage() {
136129
validateSession();
137130
}, [sessionId, t]);
138131

139-
// Initialize jscanify scanner and wait for OpenCV (loaded via script tags in index.html)
140132
useEffect(() => {
141-
let retryCount = 0;
142-
const MAX_RETRIES = 50; // 5 seconds max wait
143-
144-
const initScanner = () => {
145-
// Check if both OpenCV and jscanify are loaded
146-
if (!(window as any).cv || !(window as any).cv.Mat) {
147-
retryCount++;
148-
if (retryCount < MAX_RETRIES) {
149-
if (retryCount % 10 === 1) {
150-
setLoadingStatus(
151-
`Loading OpenCV... (${retryCount}/${MAX_RETRIES})`,
152-
);
153-
console.log(
154-
`[${retryCount}/${MAX_RETRIES}] Waiting for OpenCV to load...`,
155-
);
156-
}
157-
setTimeout(initScanner, 100);
158-
} else {
159-
const error =
160-
"OpenCV failed to load after 5 seconds. Check that /vendor/jscanify/opencv.js is accessible.";
161-
setLoadingStatus("OpenCV load failed ✗");
162-
console.error(error);
163-
}
164-
return;
165-
}
166-
167-
if (!window.jscanify) {
168-
retryCount++;
169-
if (retryCount < MAX_RETRIES) {
170-
if (retryCount % 10 === 1) {
171-
setLoadingStatus(
172-
`Loading jscanify... (${retryCount}/${MAX_RETRIES})`,
173-
);
174-
console.log(
175-
`[${retryCount}/${MAX_RETRIES}] Waiting for jscanify to load...`,
176-
);
177-
}
178-
setTimeout(initScanner, 100);
179-
} else {
180-
const error =
181-
"jscanify failed to load after 5 seconds. Check that /vendor/jscanify/jscanify.js is accessible.";
182-
setLoadingStatus("jscanify load failed ✗");
183-
console.error(error);
133+
let cancelled = false;
134+
135+
loadJscanify({
136+
onStatus: (status) => {
137+
if (!cancelled) setLoadingStatus(status);
138+
},
139+
})
140+
.then(() => {
141+
if (cancelled) return;
142+
try {
143+
scannerRef.current = new window.jscanify!();
144+
setOpenCvReady(true);
145+
console.log("✓ jscanify initialized with OpenCV");
146+
} catch (err) {
147+
setLoadingStatus("jscanify init failed ✗");
148+
console.error("Failed to initialize jscanify:", err);
184149
}
185-
return;
186-
}
150+
})
151+
.catch((err) => {
152+
if (cancelled) return;
153+
setLoadingStatus(
154+
`Scanner library failed to load ✗: ${(err as Error).message}`,
155+
);
156+
console.error("Failed to load jscanify:", err);
157+
});
187158

188-
try {
189-
scannerRef.current = new window.jscanify();
190-
setOpenCvReady(true);
191-
// Don't set status here - let camera/detection effects control status from now on
192-
console.log("✓ jscanify initialized with OpenCV");
193-
} catch (err) {
194-
setLoadingStatus("jscanify init failed ✗");
195-
console.error("Failed to initialize jscanify:", err);
196-
}
159+
return () => {
160+
cancelled = true;
197161
};
198-
199-
// Start initialization
200-
setLoadingStatus("Loading OpenCV...");
201-
initScanner();
202162
}, []);
203163

204164
// Initialize camera
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
declare global {
2+
interface Window {
3+
cv?: any;
4+
jscanify?: any;
5+
}
6+
}
7+
8+
const OPENCV_SRC = "/vendor/jscanify/opencv.js";
9+
const JSCANIFY_SRC = "/vendor/jscanify/jscanify.js";
10+
11+
let loadPromise: Promise<void> | null = null;
12+
13+
function injectScript(src: string): Promise<void> {
14+
return new Promise((resolve, reject) => {
15+
// data-loaded: load event fired. data-loading: in flight.
16+
// Neither: foreign tag (e.g. HMR-preserved); trust the downstream
17+
// global check rather than wait on a load event that may already
18+
// have dispatched.
19+
const existing = document.querySelector<HTMLScriptElement>(
20+
`script[src="${src}"]`,
21+
);
22+
if (existing) {
23+
if (existing.dataset.loaded === "true") {
24+
resolve();
25+
return;
26+
}
27+
if (existing.dataset.loading === "true") {
28+
existing.addEventListener("load", () => resolve(), { once: true });
29+
existing.addEventListener(
30+
"error",
31+
() => reject(new Error(`Failed to load ${src}`)),
32+
{ once: true },
33+
);
34+
return;
35+
}
36+
resolve();
37+
return;
38+
}
39+
40+
const script = document.createElement("script");
41+
script.src = src;
42+
script.async = true;
43+
script.dataset.loading = "true";
44+
script.onload = () => {
45+
script.dataset.loaded = "true";
46+
delete script.dataset.loading;
47+
resolve();
48+
};
49+
script.onerror = () => {
50+
delete script.dataset.loading;
51+
reject(new Error(`Failed to load ${src}`));
52+
};
53+
document.head.appendChild(script);
54+
});
55+
}
56+
57+
function waitForGlobal(
58+
check: () => boolean,
59+
name: string,
60+
timeoutMs: number,
61+
onProgress?: (elapsedMs: number) => void,
62+
): Promise<void> {
63+
return new Promise((resolve, reject) => {
64+
const start = Date.now();
65+
const tick = () => {
66+
if (check()) {
67+
resolve();
68+
return;
69+
}
70+
const elapsed = Date.now() - start;
71+
if (elapsed > timeoutMs) {
72+
reject(new Error(`Timed out waiting for ${name}`));
73+
return;
74+
}
75+
onProgress?.(elapsed);
76+
setTimeout(tick, 100);
77+
};
78+
tick();
79+
});
80+
}
81+
82+
export interface LoadJscanifyOptions {
83+
onStatus?: (status: string) => void;
84+
}
85+
86+
export function loadJscanify(options: LoadJscanifyOptions = {}): Promise<void> {
87+
const { onStatus } = options;
88+
89+
if (loadPromise) return loadPromise;
90+
91+
loadPromise = (async () => {
92+
onStatus?.("Loading OpenCV...");
93+
await injectScript(OPENCV_SRC);
94+
// OpenCV's script load event fires before the WASM runtime is ready.
95+
await waitForGlobal(
96+
() => !!window.cv && !!window.cv.Mat,
97+
"OpenCV runtime (cv.Mat)",
98+
15000,
99+
(elapsed) => {
100+
if (elapsed > 2000) onStatus?.("Initializing OpenCV runtime...");
101+
},
102+
);
103+
104+
onStatus?.("Loading jscanify...");
105+
await injectScript(JSCANIFY_SRC);
106+
await waitForGlobal(() => !!window.jscanify, "jscanify global", 5000);
107+
108+
onStatus?.("Scanner ready");
109+
})().catch((err) => {
110+
loadPromise = null;
111+
throw err;
112+
});
113+
114+
return loadPromise;
115+
}

0 commit comments

Comments
 (0)