pipwave-ekyc-uikit 3.4.0 → 3.5.0-beta.1

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,184 @@
1
+ # Branch review: `hotfix-teojunjie-sc-356160-fix-face-model-load-deadlock`
2
+
3
+ Compared against `origin/master`. The branch is 2 commits ahead, 0 behind.
4
+
5
+ - `1cb8851` [sc-356160] Add timeout and retry to face model load
6
+ - `09fb8c1` [sc-356160] Add Retry dialog when face model load fails
7
+
8
+ ```
9
+ src/plugins/face-camera.ts | 16 ++
10
+ src/plugins/face-detection.ts | 80 ++++--
11
+ src/plugins/test/face-detection.load.test.ts | 221 +++++++++++++++++ (new)
12
+ src/plugins/{ => test}/card-detection.inference.test.ts
13
+ src/plugins/{ => test}/card-detection.test.ts
14
+ src/plugins/{ => test}/detection-failure.test.ts
15
+ src/plugins/{ => test}/face-detection.test.ts
16
+ src/translations/en.ts | 1 +
17
+ 8 files changed, 308 insertions(+), 30 deletions(-)
18
+ ```
19
+
20
+ ## The bug being fixed
21
+
22
+ The selfie screen downloads face-api model files before it can start. The old code in
23
+ `src/plugins/face-detection.ts` did this:
24
+
25
+ 1. Set `loadedModelState` to `"LOADING"`.
26
+ 2. Start the download.
27
+ 3. Wait in a loop: `while (loadedModelState === "LOADING") { sleep 500ms }`.
28
+
29
+ If a download stalled but never actually failed, step 1's `"LOADING"` value was never changed. So the loop
30
+ in step 3 kept waiting forever, and the user was stuck looking at the "Initializing Camera..." spinner with
31
+ no error and no way forward.
32
+
33
+ ## What the branch changes
34
+
35
+ ### 1. `src/plugins/face-detection.ts` — bound the download and retry it
36
+
37
+ - **One shared download.** A new module-level `loadPromise` holds the single download in progress. The
38
+ background pre-warm (from `src/uisdk.ts`) and the selfie screen now wait on the same promise instead of
39
+ each watching the `loadedModelState` variable. It is set back to `null` when the download finishes, so a
40
+ failed download can be tried again later.
41
+ - **A time limit and retries.** The new `_attemptLoadModel()` wraps the four loader calls in
42
+ `withTimeout(..., 10000, "face model load")` and retries up to 2 more times, waiting 500ms then 1s between
43
+ tries. If all 3 attempts fail, it sets `loadedModelState` back to `"NOT_LOADED"` and throws.
44
+ - **`init()` handles the failure.** It now wraps the download in try/catch. On failure it hides the spinner,
45
+ calls a new `onModelLoadFailed?.()` callback, and returns without starting detection. The old endless
46
+ `while` loop is gone.
47
+ - **Reuses existing helpers.** `withTimeout` and `computeBackoffMs` already existed in
48
+ `src/plugins/detection-failure.ts` (written for the card model loader), so nothing was duplicated. It
49
+ skips the third helper, `classifyFailure`, and the comment explains why: face-api's loaders only download
50
+ files, so every failure here is worth retrying.
51
+
52
+ ### 2. `src/plugins/face-camera.ts` — show a Retry dialog
53
+
54
+ `init()` now sets `faceDetectionSdk.onModelLoadFailed` to a new `showModelLoadFailedDialog()` method. That
55
+ method opens a `BaseDialog` with the message `$t("face_camera_unavailable")` and a `$t("btn_retry")` button,
56
+ plus a Cancel button.
57
+
58
+ Pressing Retry calls `faceDetectionSdk.init()` again. That works because the state was already reset to
59
+ `"NOT_LOADED"`, so it starts a genuinely new download instead of waiting on the dead one. Cancel just closes
60
+ the dialog and leaves the back button in the header as the way out.
61
+
62
+ ### 3. `src/translations/en.ts` — one new string
63
+
64
+ `face_camera_unavailable: "Couldn't start the camera. Please check your connection and retry."`
65
+
66
+ `en.ts` is the only translation file in this repo, so there are no other languages left missing the string.
67
+
68
+ ### 4. Tests moved and added
69
+
70
+ Four existing test files moved from `src/plugins/` into `src/plugins/test/`. I checked the diff: the only
71
+ content change is the import paths getting one level deeper (`./card-detection` became `../card-detection`,
72
+ `../classes/MainPage` became `../../classes/MainPage`). No config change was needed because
73
+ `vitest.config.ts` already looks for `src/**/*.test.ts`.
74
+
75
+ `face-detection.load.test.ts` is new and adds 9 tests:
76
+
77
+ - a normal successful load
78
+ - a load that fails once then succeeds on retry
79
+ - a stalled load that times out, fails, and resets the state cleanly
80
+ - a fresh retry working after an earlier failure
81
+ - two callers at once sharing a single download (not downloading twice)
82
+ - the failure reaching the second caller too, so it isn't left hanging
83
+ - a download that finishes *after* its attempt already timed out, which must be ignored
84
+ - `init()` hiding the spinner when the load fails
85
+ - `init()` calling `onModelLoadFailed` so the dialog can appear
86
+
87
+ ## Checks I ran
88
+
89
+ | Check | Result |
90
+ | --- | --- |
91
+ | `npx vitest run` | 5 files, **76 tests passed** |
92
+ | `npx tsc --noEmit` | no errors |
93
+ | `npm run build` | builds fine; only the bundle-size warnings that were already there |
94
+
95
+ One thing to watch: the tests need Node 18 or newer. On Node 16 (the shell default here) vitest crashes at
96
+ startup with a `node:util` / `styleText` SyntaxError before running a single test. Worth checking that CI and
97
+ other developers aren't on Node 16.
98
+
99
+ ## Problems found
100
+
101
+ ### 1. The 10-second limit is probably too short (most important)
102
+
103
+ The four loaders download about 7 MB in total, from `public/face-models`:
104
+
105
+ | file | size |
106
+ | --- | --- |
107
+ | `tiny_face_detector_model-shard1` | 193 KB |
108
+ | `face_landmark_68_model-shard1` | 357 KB |
109
+ | `face_expression_model-shard1` | 329 KB |
110
+ | `face_recognition_model-shard1` + `shard2` | 6.44 MB |
111
+ | **total** | **~7.0 MB** |
112
+
113
+ To finish 7 MB in 10 seconds you need about 700 KB/s, which is roughly 5.6 Mbps held steady. On a weak
114
+ mobile connection the download is fine, just slow, but the new code gives up on it. It then retries twice,
115
+ also giving up, and shows "Couldn't start the camera" after about 31.5 seconds (three 10s attempts plus
116
+ 500ms and 1s of waiting).
117
+
118
+ So we've fixed a rare freeze but made slow connections fail where they used to eventually work.
119
+
120
+ Two smaller side effects of the same setting:
121
+
122
+ - `withTimeout` doesn't cancel the download it gave up on. Sometimes that helps, because the first download
123
+ may finish and fill the browser cache in time for retry 2 or 3 to reuse it. But that depends on the cache
124
+ headers on `assets.pipwave.com` and on lucky timing, so we can't rely on it.
125
+ - Because nothing is cancelled, up to 3 attempts x 4 loaders can be downloading at the same time, all
126
+ fighting over the same slow connection.
127
+
128
+ What I'd suggest, best option first:
129
+
130
+ 1. Raise the limit per attempt to 30-45 seconds. The point is to catch a download that has stopped moving,
131
+ not one that is merely slow, so the limit doesn't need to be tight.
132
+ 2. Or detect a stall instead of a total time (give up only when no bytes have arrived for a while).
133
+ 3. Or pass an `AbortController` so a given-up attempt stops using bandwidth. face-api's loaders may not
134
+ accept one, in which case option 1 matters more.
135
+
136
+ Worth noting this mostly affects the slow-connection minority: the pre-warm in `src/uisdk.ts:83` starts
137
+ downloading early in the eKYC flow, so most users are done well before they reach the selfie screen.
138
+
139
+ ### 2. The 90-second idle timer can fire while the Retry dialog is open
140
+
141
+ `FaceCamera.init()` starts a 90-second idle timer (`src/plugins/face-camera.ts:233-245`) before the model
142
+ download begins. One failed download cycle uses up to 31.5 seconds of that.
143
+
144
+ So if the user reads the dialog and taps Retry late, the idle timer can go off in the middle of the retry.
145
+ The timer calls `stopVideoStream()`, which kills the camera tracks, while the retrying `init()` carries on
146
+ and calls `startDetection()` on a camera that no longer works. The result is a stuck camera with no spinner
147
+ and no error message.
148
+
149
+ Simple fix: reset the idle timer inside the Retry callback (or clear it when the load fails and restart it
150
+ on Retry).
151
+
152
+ ### 3. Two dialogs could stack up (minor)
153
+
154
+ `init()` doesn't check whether it is already running before starting the download. The
155
+ `!this.iFrameRef && !this.isStartingDetection` check happens after the `await`, not before it. If the video
156
+ element fires `loadedmetadata` twice (for example the camera stream is re-acquired, or the device rotates),
157
+ both calls share the same failing download and both call `onModelLoadFailed`, so two dialogs appear on top
158
+ of each other. A simple "already showed the dialog" flag would prevent it.
159
+
160
+ ### 4. Cosmetic
161
+
162
+ - `src/plugins/face-detection.ts:745` has trailing whitespace on a new blank line inside
163
+ `_attemptLoadModel()`. Two similar cases were already in the file. There's no eslint config in this repo,
164
+ so nothing flags it.
165
+ - The Cancel button text is hardcoded as `"Cancel"` in `src/components/baseDialog.ts:73` instead of using
166
+ `$t(...)`. This isn't new, and it doesn't matter while English is the only locale, but this new dialog is
167
+ one more place using it.
168
+
169
+ ## Things I checked that are correct
170
+
171
+ - **No way back into the old freeze.** There is never a moment where `loadedModelState` is `"LOADING"` while
172
+ `loadPromise` is `null`. An async function runs up to its first `await` immediately, so `"LOADING"` is set
173
+ before the promise is handed to anyone, and on failure `"NOT_LOADED"` is set before the throw. So a caller
174
+ arriving at any point either joins the live download or starts a new one.
175
+ - **No unhandled promise rejections.** The caller that starts the download always awaits it, and the
176
+ pre-warm at `src/uisdk.ts:83` has a `.catch(...)`.
177
+ - **Retry really does start over.** The state is reset and `loadPromise` cleared before the dialog appears,
178
+ so Retry downloads again rather than waiting on the failed promise. Test 4 covers this.
179
+ - **Retry shows a spinner.** `FACEDETECTIONSDK.init()` calls `createFullLoading` itself, so pressing Retry
180
+ gives visible feedback instead of looking like nothing happened.
181
+ - **The `finally` block in `FaceCamera.init()`** still hides the spinner based on
182
+ `getLoadedModelState() !== "LOADING"`. This branch doesn't change that behaviour.
183
+ - **`dist/pw-bundle.js` doesn't include these changes.** That's expected: this repo only commits `dist/` when
184
+ the version is bumped for a release.