Skip to content

Refactor JsMessageProcessor Factory#272

Open
davertay-j wants to merge 6 commits into
mainfrom
dt/refactor_processor_di
Open

Refactor JsMessageProcessor Factory#272
davertay-j wants to merge 6 commits into
mainfrom
dt/refactor_processor_di

Conversation

@davertay-j

@davertay-j davertay-j commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Refactor, primarily to relocate the TopazScriptHandler added in #266 as AppMessage, and secondarily to fix the issue with the life-cycle race condition.

Closes #269


Note

Medium Risk
Changes the WebView JS bridge composition and navigation teardown ordering; mistakes could affect BLE context lifetime, keyboard overlay state, or user-agent switching across tabs.

Overview
Refactors how native JS message handlers are wired: app-wide builders (Bluetooth, logger) stay in TopazMain / AppModel, while per-tab handlers (topaz / AppMessage, virtual keyboard) are merged when each WebPageModel is built. That replaces the separate TopazScriptHandler with an AppMessage module and routes setUserAgentMode through AppMessageProcessorWebPageAppMessageHostWebPageModel.

Virtual keyboard state is no longer global (VirtualKeyboardModel.shared removed); each tab gets its own model injected into VirtualKeyboard, and detach resets overlaysContent on that instance—addressing cross-webview teardown races without resetting another tab’s keyboard in Coordinator.

Shared JsMessage plumbing adds DispatchingJsMessageProcessor, generic action decoding, JsMessageLog, and common decode errors; VirtualKeyboard adopts that pattern. On cross-origin navigation, processor detach is awaited before attaching a new JS context (detachProcessorsAndWait), so didDetach finishes before the next page’s handlers run.

Reviewed by Cursor Bugbot for commit 9b018bc. Configure here.

davertay-j and others added 5 commits June 15, 2026 14:18
The global processor factory could only capture app-wide scope, so the
AppMessageProcessor was made a shared singleton with its page pushed in
via a setter. Each page load overwrote that callback (last-writer-wins),
so a setUserAgentMode message from one tab could mutate another tab's
web view.

Make the factory per-tab and composed at two sites along the module
graph: TopazMain supplies app-domain builders (Bluetooth, logging,
keyboard), and AppModel merges in page-coupled builders that capture the
freshly-created page. AppMessageProcessor now receives an AppMessageHost
at construction (bridged to WebPageModel by WebPageAppMessageHost in the
App module) and is built fresh per page, so the shared mutable callback
and the race are both gone. Builders still construct per JS context, so
Bluetooth's reset-on-cross-origin behavior is unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
VirtualKeyboardModel was a shared singleton, so detaching an old web
view's handler could clobber the newly-attached web view's keyboard
state. The reset-on-default was wedged into Coordinator.attachNewHandler
as a workaround for that cross-instance race.

Now that AppModel can build page-coupled processors, give each page its
own VirtualKeyboardModel and add a page-domain VirtualKeyboard builder
alongside AppMessage. The processor's didDetach reset is re-enabled (safe
per-page) and the Coordinator workaround is removed. enableDebugLogging
is threaded through AppModel so both page-coupled processors keep their
debug logging.

Co-authored-by: Cursor <cursoragent@cursor.com>

Cleanup a bit
@davertay-j davertay-j added the minor Changes that should bump the MINOR version number label Jun 15, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 9b018bc. Configure here.

await self.detachOldHandlerAndWait(from: webView)
self.contextId = newContextId
self.attachNewHandler(to: webView)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cross-origin handler Task races

High Severity

Cross-origin JS handler teardown and re-attach run inside an unstructured Task, so work continues after didInitiateNavigation returns. Overlapping cross-origin navigations can finish out of order and restore a stale contextId with mismatched handlers, the page can load before handlers are registered, and a pending task can call attachNewHandler after deinitialize has started tearing the web view down.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9b018bc. Configure here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is a subtle edge case that we haven't seen manifest. The proper fix requires moving the context swap to occur at a different navigation point so it can be awaited. Will address this in a follow up PR.

@davertay-j davertay-j requested a review from twyatt June 15, 2026 21:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

minor Changes that should bump the MINOR version number

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor JsProcessor loading

2 participants