Skip to content

Conversation

@AlexRubik
Copy link
Contributor

@AlexRubik AlexRubik commented Dec 31, 2025

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-30319

📔 Objective

Problem: When switching to an account with phishing detection access, the browser extension UI freezes. The phishing cache update runs synchronously, blocking the service worker thread and preventing the popup from responding.

Solution: Decouple the cache update from the critical UI path by moving it to a background subscription and deferring triggers to the next event loop tick.

Changes

phishing-data.service.ts

  • Remove startWith(undefined) — prevents auto-triggering on subscription
  • Add MIN_UPDATE_INTERVAL (5 min) — skips updates if cache is fresh
  • Add _updateInProgress flag with filter() — prevents concurrent updates
  • Add finalize() — reliably resets flag on success/error/completion
  • Replace share() with shareReplay() — prevents duplicate subscriptions from triggering multiple fetches
  • Add triggerUpdateIfNeeded() method — explicit trigger for consumers

phishing-detection.service.ts

  • Move update$ subscription outside the merge() — runs independently, doesn't block UI event handlers
  • Add delay(0) trigger — defers triggerUpdateIfNeeded() to the next event loop tick, allowing initialize() to complete first
  • Add proper cleanup for the new subscription

phishing-detection.service.spec.ts

  • Add update$: EMPTY mock to fix test compatibility

Why This Works

The service worker is single-threaded. Previously, the cache update ran in the same flow as UI event setup.

Now:

  1. initialize() completes immediately (in clients/apps/browser/src/dirt/phishing-detection/services/phishing-detection.service.ts)
  2. Cache update runs in the background on the next tick
  3. Popup can open and respond while update happens

⏰ Reminders before review

  • Contributor guidelines followed
  • All formatters and local linters executed and passed
  • Written new unit and / or integration tests where applicable
  • Protected functional changes with optionality (feature flags)
  • Used internationalization (i18n) for all UI strings
  • CI builds passed
  • Communicated to DevOps any deployment requirements
  • Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team

🦮 Reviewer guidelines

  • 👍 (:+1:) or similar for great changes
  • 📝 (:memo:) or ℹ️ (:information_source:) for notes or general info
  • ❓ (:question:) for questions
  • 🤔 (:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
  • 🎨 (:art:) for suggestions / improvements
  • ❌ (:x:) or ⚠️ (:warning:) for more significant problems or concerns needing attention
  • 🌱 (:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt
  • ⛏ (:pick:) for minor or nitpick changes

Remove automatic cache update triggering that caused UI freezes when
switching to accounts with phishing detection access.

Root cause: The update$ observable used startWith(undefined) which
triggered an immediate cache refresh whenever a new subscription was
created. On account switch, phishingDetectionSettingsService.on$ emits
true, creating a new subscription and triggering a full ~800K entry
fetch that blocks the UI thread.

Fix:
- Remove startWith(undefined) to prevent auto-triggering on subscription
- Add MIN_UPDATE_INTERVAL (5 min) constant for cache freshness checks
- Add _updateInProgress flag to prevent concurrent updates
- Add filter() to skip updates when one is already in progress
- Add cache freshness check (skip if updated within 5 minutes)
- Add finalize() to reliably reset _updateInProgress flag (per ADR)
- Replace share() with shareReplay() to prevent duplicate work
- Add triggerUpdateIfNeeded() public method for explicit update requests

The scheduled 24-hour update interval is unaffected - it still calls
_triggerUpdate$.next() via the task scheduler.
Update PhishingDetectionService to explicitly trigger cache updates
when phishing detection becomes active for an account, using a
non-blocking pattern.

Changes:
- Add call to phishingDataService.triggerUpdateIfNeeded() when on$ emits true
- Use of(null).pipe(delay(0)) to defer update to next event loop tick
- This prevents the update from blocking the account switch UI flow

The delay(0) pattern is preferred over setTimeout per codebase conventions
(RxJS over native JS). The subscription auto-completes since of() emits
once and completes, so no manual cleanup is needed.

Combined with the previous commit's safeguards (cache freshness check,
concurrent update prevention), this ensures:
1. Account switch completes immediately (non-blocking trigger)
2. Cache updates only run when actually needed (< 5 min freshness)
3. Concurrent updates are prevented (_updateInProgress flag)

Fixes: PM-30319
Move phishingDataService.update$ to a separate subscription outside the
merge() stream to prevent blocking the service worker during critical
initialization and account switch flows.

Background:
The service worker is single-threaded. When the phishing cache update
runs, it downloads a 25MB file and parses 800K entries using .split(),
which is CPU-intensive synchronous work. During this parsing, the
service worker cannot respond to popup requests, causing the extension
UI to appear frozen when the user clicks the extension icon.

Previously, update$ was included in the merge() alongside UI event
handlers (onTabUpdated$, onContinueCommand$, onCancelCommand$). When
on$ emitted true (user has phishing access), the merge subscription
was created as part of the same synchronous flow, coupling the heavy
cache work with the UI event setup.

Changes:
- Create separate updateSub subscription at initialization
- Remove update$ from merge() - now only contains UI event streams
- Keep delay(0) trigger for triggerUpdateIfNeeded()

How delay(0) works:
JavaScript's event loop must complete all synchronous code before
processing async callbacks. delay(0) schedules the trigger for the
next event loop tick, meaning:
1. initialize() completes and returns
2. Service worker is 'free' to handle other tasks
3. Next tick: triggerUpdateIfNeeded() fires
4. Cache update runs in background

The cache parsing will still block the thread when it eventually runs,
but this is now decoupled from the critical initialization path. The
window where blocking can affect user interaction is minimized.

PM-30319
@github-actions
Copy link
Contributor

Logo
Checkmarx One – Scan Summary & Details158130b4-046c-416f-b896-a09bb7777346

Great job! No new security vulnerabilities introduced in this pull request

@codecov
Copy link

codecov bot commented Dec 31, 2025

Codecov Report

❌ Patch coverage is 17.64706% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 42.25%. Comparing base (11b5342) to head (559da8a).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...ishing-detection/services/phishing-data.service.ts 16.66% 10 Missing ⚠️
...g-detection/services/phishing-detection.service.ts 20.00% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #18157      +/-   ##
==========================================
- Coverage   42.26%   42.25%   -0.02%     
==========================================
  Files        3599     3599              
  Lines      104516   104532      +16     
  Branches    15776    15778       +2     
==========================================
- Hits        44171    44167       -4     
- Misses      58465    58485      +20     
  Partials     1880     1880              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@AlexRubik AlexRubik self-assigned this Dec 31, 2025
@AlexRubik AlexRubik marked this pull request as ready for review December 31, 2025 02:54
@AlexRubik AlexRubik requested a review from a team as a code owner December 31, 2025 02:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants