pipwave-ekyc-uikit 3.3.2-beta.2 → 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.
- package/branch-review-sc-356160.md +184 -0
- package/dist/pw-bundle.js +2 -2
- package/package.json +2 -2
|
@@ -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.
|