Skip to content

fix(extension): persist and serialize per-tab runtime state#8

Open
llmxtfai wants to merge 2 commits intoonllm-dev:mainfrom
llmxtfai:fix/mv3-tab-state-persistence-race
Open

fix(extension): persist and serialize per-tab runtime state#8
llmxtfai wants to merge 2 commits intoonllm-dev:mainfrom
llmxtfai:fix/mv3-tab-state-persistence-race

Conversation

@llmxtfai
Copy link

Problem

This Tab can flip back to Off after hard reload (Ctrl+Shift+R) because MV3 background worker state was in-memory only and gets reset on worker restart.

Fix

  • Persist per-tab runtime state (enabled, annotateMode) to chrome.storage.session
  • Hydrate state manager from session storage on access
  • Convert state manager methods to async and await in background message handlers
  • Serialize session-storage writes with a queue to prevent stale snapshot overwrites during rapid updates

Tests

Added packages/extension/src/background/state.test.ts:

  • restores enabled state after simulated worker restart
  • persists runtime updates to session storage
  • regression for out-of-order write race (ensures latest state survives)

Validation

  • pnpm --filter @onui/extension test (pass: 7 files, 20 tests)
  • pnpm --filter @onui/extension typecheck (pass)

Steve Olson added 2 commits February 28, 2026 12:52
Persist per-tab runtime state in chrome.storage.session so hard reloads and MV3 worker restarts do not reset This Tab toggle. Convert background state accessors to async and await them in message handlers. Serialize storage writes with a queue to prevent stale snapshot overwrites, and add regression tests for restart restore and write-order safety.
Add shared log filter that suppresses [onUI...] console.log output in production builds while keeping warnings/errors. Initialize filter in content, background, and popup entrypoints. Keep logs opt-in via localStorage key onui_debug_logs=1 or true.
Copy link
Collaborator

@iota31 iota31 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — PR #8

Nice fix for a real MV3 pain point. The serialized write queue and hydration-on-access patterns are well thought out. A few issues to address before merging:


Bug: chrome used directly instead of webext abstraction

The codebase has a cross-browser shim at @/shared/webext (webext resolves browser or chrome at runtime). The existing state.ts and all other extension code uses it. This PR bypasses it entirely:

  • state.ts constructorchrome.tabs.onRemoved.addListener(...) should be webext.tabs.onRemoved.addListener(...)
  • getStorageArea() — accesses chrome.storage directly

This will break Firefox compatibility (Firefox uses browser.storage). Recommend using webext.storage.session (with a type assertion for session if needed, since the webext proxy will forward correctly).


Bug: localStorage is unavailable in MV3 service workers

In logging.ts, isDebugLoggingEnabled() checks localStorage.getItem(...). MV3 service workers have no localStorage — the try/catch prevents a crash, but the opt-in debug flag can never work in the background context. This is effectively dead code for the primary use case.

Consider using chrome.storage.session or chrome.storage.local for the debug flag, or just accept that the background context always suppresses logs (and document that the localStorage flag only works in content/popup).


Nit: persistStates always reads latest in-memory state, not call-time state

This is actually correct behavior (latest state wins), but the doc comment / PR description says "prevent stale snapshot overwrites." Worth a brief inline comment clarifying why reading inside the .then() is intentional — the queue serializes I/O order, not snapshot capture. Future readers might try to "fix" this by capturing state at call time, which would actually introduce the bug it claims to prevent.


Nit: Second commit feels like a separate PR

The log suppression (suppressOnUiDebugLogs) is unrelated to the state persistence fix. Bundling it here makes the PR harder to review and revert independently. Consider splitting it out.


Minor: console.log monkey-patching scope

suppressOnUiDebugLogs only patches console.log, not console.info or console.debug. If any [onUI prefixed messages use those, they'll leak through. Also, the filter matches any string starting with [onUI — this is fine today but could inadvertently suppress future diagnostic messages. Consider matching the full prefix pattern (e.g., [onUI]) more precisely.


Tests look solid

The deferred-write test that controls when storage.set resolves is a particularly nice way to verify serialization. Good coverage of the restart-restore flow.


Summary: The core persistence logic is sound. The main blocker is the webext abstraction bypass — that should be fixed before merge. The logging commit would ideally be a separate PR.

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