Fix: Handsontable Numeric Cell Type Registration Race Condition¶
Note: This is a temporary planning document. Delete after the fix in #2537 is merged.
Problem¶
The timepoint spreadsheet in Data Extraction crashes with:
Sentry: SYRF-WEB-372 -- 83 occurrences, 13 users, ongoing since December 2025.
Root Cause¶
A race condition in TimepointSpreadsheetComponent between async Handsontable cell type registration and synchronous table initialisation.
Sequence of Events (Current -- Broken)¶
- Angular creates
TimepointSpreadsheetComponent ngOnChanges()fires first (Angular lifecycle order), settingthis.columnswithtype: 'numeric'ngOnInit()fires, callingawait ensureHotBasicsRegistered()-- this yields to the microtask queueHotTableComponent.ngAfterViewInitfires synchronously, callingthis.hotInstance.init()which resolves cell type strings- Crash --
NumericCellTypehasn't been registered yet because the async import hasn't resolved
Key Files¶
| File | Role |
|---|---|
src/services/web/src/app/shared/form-controls/timepoint-spreadsheet/timepoint-spreadsheet.component.ts |
Component that declares columns with type: 'numeric' |
src/services/web/src/app/shared/form-controls/timepoint-spreadsheet/timepoint-spreadsheet.component.html |
Template containing <hot-table> |
src/services/web/src/app/shared/utils/handsontable-loader.ts |
Lazy registration utility for NumericCellType and CopyPaste plugin |
Why It's Timing-Dependent¶
The bug depends on whether the dynamic import('handsontable/cellTypes') resolves before HotTableComponent.ngAfterViewInit runs. On fast machines with cached modules, the import may resolve quickly enough. On slower machines or first loads, the race is lost and the crash occurs.
Fix Options¶
Option A: Defer <hot-table> rendering (Recommended)¶
Add a ready flag to TimepointSpreadsheetComponent. Await ensureHotBasicsRegistered() in ngOnInit(), then set ready = true. Wrap the <hot-table> in the template with @if (ready).
Pros:
- Simplest change (3 files, ~10 lines)
- Preserves lazy-loading intent -- Handsontable stays out of the initial bundle
- Only affects the component that needs it
- Brief loading state is imperceptible to users
Cons:
- Momentary flash before table appears (milliseconds, unlikely to be visible)
Option B: Register cell types eagerly at app startup¶
Call registerCellType(NumericCellType) in app.config.ts or an APP_INITIALIZER.
Pros:
- Guarantees registration before any component loads
Cons:
- Adds Handsontable to the initial bundle (~200KB+), increasing load time for ALL users
- Defeats the purpose of the lazy loader utility
- Overkill for a single component
Option C: Route resolver on Data Extraction route¶
Add a resolver that awaits ensureHotBasicsRegistered() on the review route.
Pros:
- Registration guaranteed before the component tree mounts
Cons:
- Couples Handsontable registration to routing config
- Would need to be added to every route that might use the spreadsheet
- More complex than necessary
Chosen Approach: Option A¶
Implementation Plan¶
timepoint-spreadsheet.component.ts:- Add
ready = falseproperty - In
ngOnInit(), awaitensureHotBasicsRegistered(), then setthis.ready = true -
Trigger change detection after setting
ready(component may use OnPush or need a manual cycle) -
timepoint-spreadsheet.component.html: -
Wrap the
<hot-table>element with@if (ready) { ... } -
Tests:
- Add/update unit test verifying the table doesn't render before registration completes
-
Ensure existing tests still pass
-
Manual verification:
- Navigate to a Data Extraction review page with timepoint data
- Confirm spreadsheet renders without error
- Check Sentry for absence of new SYRF-WEB-372 events after deployment
Sequence of Events (After Fix)¶
- Angular creates
TimepointSpreadsheetComponent ngOnChanges()fires, settingthis.columnswithtype: 'numeric'ngOnInit()fires, callingawait ensureHotBasicsRegistered()-- yields to microtask queue- Template renders with
@if (ready)--readyisfalse, so<hot-table>is not mounted ensureHotBasicsRegistered()resolves --NumericCellTypeis now registeredreadyis set totrue, Angular re-renders,<hot-table>mountsHotTableComponent.ngAfterViewInitfires,getCellType('numeric')succeeds
Acceptance Criteria¶
-
NumericCellTypeis guaranteed registered beforeHotTableComponent.ngAfterViewInitruns - No regression in Data Extraction timepoint input
- Unit test covers the loading guard
- Sentry issue SYRF-WEB-372 stops receiving new events after deployment