Skip to content

Conversation

@Cramsden
Copy link
Contributor

@Cramsden Cramsden commented Oct 31, 2025

📜 Tickets

Jira ticket

💡 Description

  • Isolate store state to the MainActor
  • Isolate all subscriptions including subscribe and unsubscribe to the MainActor
  • Isolate Subscription and SubscriptionWrapper to the MainActor
  • Isolate global store variable to the MainActor
  • Fix calls to dispatchLegacy that were not already isolated to the MainActor and move them to dispatch

🎥 Demos

Before After
Demo

📝 Checklist

  • I filled in the ticket numbers and a description of my work
  • I updated the PR name to follow our PR naming guidelines
  • I ensured unit tests pass and wrote tests for new code
  • If working on UI, I checked and implemented accessibility (Dynamic Text and VoiceOver)
  • If adding telemetry, I read the data stewardship requirements and will request a data review
  • If adding or modifying strings, I read the guidelines and will request a string review from l10n
  • If needed, I updated documentation and added comments to complex code

@mobiletest-ci-bot
Copy link

mobiletest-ci-bot commented Oct 31, 2025

Messages
📖 Project coverage: 38.59%

💪 Quality guardian

5 tests files modified. You're a champion of test coverage! 🚀

🥇 Perfect PR size

Smaller PRs are easier to review. Thanks for making life easy for reviewers! ✨

💬 Description craftsman

Great PR description! Reviewers salute you 🫡

❌ Per-file test coverage gate

The following changed file(s) are below 35.0% coverage:

File Coverage Required
BrowserKit/Sources/Redux/DispatchStore.swift 0.0% 35.0%
BrowserKit/Sources/Redux/Store.swift 0.0% 35.0%
BrowserKit/Sources/Redux/Subscription.swift 0.0% 35.0%
firefox-ios/Client/Frontend/Home/Homepage Rebuild/ContextMenu/ContextMenuState.swift 34.6% 35.0%
firefox-ios/Client/Frontend/PasswordGenerator/PasswordGeneratorViewController.swift 31.2% 35.0%

Client.app: Coverage: 37.06

File Coverage
AddressBarState.swift 95.46%
PasswordGeneratorViewController.swift 31.18% ⚠️
LoginsHelper.swift 0.0% ⚠️
NavigationToolbarContainerModel.swift 76.99%
NavigationBarState.swift 90.46%
ContextMenuState.swift 34.62% ⚠️
WallpaperSettingsViewModel.swift 72.96%
AppState.swift 96.1%
AddressBarPanGestureHandler.swift 27.24% ⚠️
NativeErrorPageViewController.swift 77.3%
AddressToolbarContainerModel.swift 87.3%

Generated by 🚫 Danger Swift against ce303de

Copy link
Collaborator

@ih-codes ih-codes left a comment

Choose a reason for hiding this comment

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

Seems reasonable to me at first glance. I honestly expected way more issues/changes, haha.

I commented a few spots where I think the State reducer files could avoid isolation if they used the passed-in state instead of direct store access. Seems odd we would bypass the reducer state input to access store directly anyway.


weak var coordinatorDelegate: ContextMenuCoordinator?

@MainActor
Copy link
Collaborator

Choose a reason for hiding this comment

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

ContextMenuState feels a bit weird that it contains all these actions for dispatching to the store, probably some responsibilities that should eventually be separated out. Unrelated to this work though.


// MARK: - Navigation Toolbar Actions
@MainActor
private static func navigationActions(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would it be better for us to instead pass in the current state and not need to access this on the main actor because of this line right below? Then I don't think we'd need any @MainActors in this file?

guard let toolbarState = store.state.screenState(ToolbarState.self, for: .toolbar, window: action.windowUUID)

Copy link
Collaborator

Choose a reason for hiding this comment

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

I looked closer at this, and the problem is that this reducer also needs the parent ToolbarState to calculate the next state, which feels a bit weird to me. But that's why it accesses the store. 🤔

What are everyone's thoughts on a reducer needing to access the global store to get a higher level state in order to make decisions? In other words, one reducer can't render the next state with just its previous state + an action. The reducer also needs to access the global state, so the next state is really a function of the previous state + global current state + an action.

CC @lmarceau @OrlaM

In this specific example, the reducer for NavigationBarState (a child property of ToolbarState) needs to access the ToolbarState through the global store to help decide whether the user can navigate forward/backward with the nav buttons, etc..

/// Stores your entire app state in the form of a single data structure.
/// This state can only be modified by dispatching Actions to the store.
/// Whenever the state of the store changes, the store will notify all store subscriber.
public final class Store<State: StateType & Sendable>: DefaultDispatchStore {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we just annotate the entire store @MainActor if we're doing so in AppState.swift anyway? 🤔

@mergify
Copy link
Contributor

mergify bot commented Nov 4, 2025

This pull request has conflicts when rebasing. Could you fix it @Cramsden? 🙏

}

@objc
private nonisolated func didTapReload() {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is marked nonisolated but it's called from a UIButton tap. In other places we did not mark those UI button @objc selector methods as nonisolated, and we call directly store.dispatch. It's assumed since the click is from a button and it's in the UI, then it's always the main thread.

We can totally change that, but I just want us to have consistency.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I personally would vote to always mark @objc methods nonisolated... I think there's less room for programmer error then, like if they decide to call such a method from a notification later as well or something. You'd have to check every caller to know if the usage is "safe" to assume main actor isolation. 👀

@ih-codes ih-codes force-pushed the cr/FXIOS-13944_isolate-new-state-to-main-actor branch from f958b6e to a0e36e3 Compare November 5, 2025 16:44
@ih-codes
Copy link
Collaborator

ih-codes commented Nov 5, 2025

I've rebased and fixed the conflicts for you!

firefox-ios/Client/Frontend/Browser/Toolbars/Redux/AddressBarState.swift
firefox-ios/Client/Frontend/NativeErrorPage/NativeErrorPageViewController.swift

I think I'll also try to address mine and @lmarceau's PR feedback on your branch if that's ok (just revert my commit(s) if you disagree with the changes). But I thought I'd help out to get this one over the finish line since you're out sick @Cramsden and it's a big one I'd like to see merged in soon.

@ih-codes
Copy link
Collaborator

ih-codes commented Nov 5, 2025

Ok, I made 2 small changes for some of the PR feedback in a commit. I resolved those conversations and left the rest open for you @Cramsden!

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.

5 participants