Conversation
c2404be to
7ff3a8a
Compare
|
I plan on reviewing this but first I want to better understand the problems you see this controller solving and how you anticipate it being used. In the explanation for this PR, you say that as new controllers with data subscriptions are added, clients must be updated to inform those controllers to start when the UI is active and stop when the UI is not. You say that this isn't scalable because this code is scattered throughout the clients. Furthermore, in the technical proposal you created earlier, you seem to say that the UI should not be responsible for managing data subscriptions, the controller layer should. This is further implied in this PR when suggesting that with the existence of an ApplicationStateController, other controllers can now be aware of when the UI is open and automatically start and stop data subscriptions. First, I don't think it is bad that there is some code in the UI which controls when a data subscription is started or stopped. After all, not every screen needs every piece of data we use throughout MetaMask, so it seems perfectly fine to me to place that control at the React component level (i.e., "when this component mounts, start polling or open a WebSocket; when this components unmounts, stop"). My sense is that the problem we're trying to solve here is not how to handle what happens if a user switches away from a screen, but how to handle when the app (whether that's the mobile app, or a tab in a browser) is backgrounded or foregrounded. Here, the user is still on a screen — so no component is unmounted — but the user is not actively using the app. And in this case, it seems to me that if the current screen is actively polling for data or has a WebSocket open, then that data subscription needs to be paused, so that when the user returns, it can be resumed. In other words, alongside this controller I wonder if we ought to update the pattern that we've been using so that polling controllers, WebSocket services, and other places have both a I think reframing this controller — and the examples presented in places like the README — in this way would make more sense to me, but what do you think? |
packages/application-state-controller/src/ApplicationStateController.ts
Outdated
Show resolved
Hide resolved
Currently, when you look at the core codebase, you don't have a complete picture of how/when some code is executed. This is extremely hard to understand the whole workflow when you don't know that some logic lives in the UI. To me, it's the same utility as when we switch accounts: we have a state and events that we can listen to in any Controller, with no UI code/hooks needed. Thanks to this App State Event, we could consider removing all polling logic from the extension and mobile, which represents quite some code to clean, but ultimately will make platform code lighter. |
|
.@Kriys94 and I spoke earlier today about this. For completeness here is more or less a summary of what I said: Implications on polling controllersTo be clear, I don't think this controller is not needed. I can see the value in encapsulating the concept of whether the user is currently using MetaMask so that we can access that information in an agnostic fashion. However, there are caveats around how this controller ought to be used that I think should be highlighted or at least addressed somewhere. Namely, when it comes to polling controllers, I don't know that we would want to suggest that engineers can update their controllers to start polling when MetaMask is active and stop polling when it's inactive, e.g.: this.#messenger.subscribe(
'ApplicationStateController:stateChange',
(isClientOpen) => {
if (isClientOpen) {
this.#start();
} else {
this.#stop();
}
},
(state) => state.isClientOpen
);This pattern implies that we would be moving polling management to a more global location: as soon as MetaMask opens, no matter what screen the user is on, we start subscribing to updates for all kinds of data — even for ones that the current screen doesn't need. I don't feel like this is a good strategy long-term. We've gotten in trouble in the past about making network requests before users complete onboarding and I feel like we would run into that again if we went that direction. That's why I suggested continuing to have the UI drive which polls are active, but then introducing the idea of "pausing" and "resuming", for instance: this.#messenger.subscribe(
'ApplicationStateController:stateChange',
(isClientOpen) => {
if (isClientOpen) {
this.#resume();
} else {
this.#pause();
}
},
(state) => state.isClientOpen
);If we are providing examples for how to use this controller in this PR, then I think we should suggest that engineers follow these practices. NamingThe other thing I mentioned in the call is around naming:
I think the name I also wonder if we should rename the state property? I am fine with the word "open". On extension, it could reasonably mean that the popup is open, or the full-screen view is open (regardless of whether the tab is active), or the sidepanel is open. And on mobile, it would mean that the app is foregrounded. But, "client" seems like a synonym for "application" to me. So maybe we should rename the state property either |
packages/application-state-controller/src/ApplicationStateController.test.ts
Outdated
Show resolved
Hide resolved
packages/client-state-controller/src/ClientStateController.test.ts
Outdated
Show resolved
Hide resolved
packages/client-state-controller/src/ClientStateController.test.ts
Outdated
Show resolved
Hide resolved
packages/client-state-controller/src/ClientStateController.test.ts
Outdated
Show resolved
Hide resolved
packages/client-state-controller/src/ClientStateController.test.ts
Outdated
Show resolved
Hide resolved
876c111 to
3620a98
Compare
|
@mcmire For the naming, I called it ClientStateController. WDYT? |
257484c to
c3fd48e
Compare
7dff417 to
4f0fa7d
Compare
4f0fa7d to
d8ef56c
Compare
| /packages/eip-5792-middleware @MetaMask/wallet-integrations | ||
|
|
||
| ## Core Platform Team | ||
| /packages/ui-state-controller @MetaMask/core-platform |
There was a problem hiding this comment.
What are your thoughts on putting this controller under the ownership of Extension Platform and Mobile Platform? Our team already own a lot of things we shouldn't, and it doesn't fit with the goals of our team, it seems like something that's more feature-oriented. This controller also feels like something these teams need to be in the loop on, as the logic for tracking the state of the UI is dependent on the platform.
| /packages/ui-state-controller @MetaMask/core-platform | |
| /packages/ui-state-controller @MetaMask/extension-platform @MetaMask/mobile-platform |
d8ef56c to
14e4e02
Compare
Explanation
Current state: Application lifecycle management (knowing when the UI is open or closed) is currently scattered across platform-specific code (Extension and Mobile). When the UI state changes, MetamaskController must manually notify each controller/service that cares about this state via imperative calls.
Problem: This approach doesn't scale well. Every time a new controller needs to respond to client state changes (stop polling, disconnect WebSockets, pause subscriptions), the platform code has to be modified to add another manual call. Platform code becomes tightly coupled to controller-specific behavior.
Solution: This PR introduces
@metamask/ui-state-controller, a new shared controller that centralizes application lifecycle state. The pattern follows an inversion of control approach:UiStateController:setUiOpenUiStateController:stateChangeand manage themselvesThis enables polling controllers to stop when the client closes, WebSocket connections to disconnect, and real-time subscriptions to pause—all without modifying platform code, with the logic encapsulated in core.
State properties:
isUiOpen: boolean— Whether the client (UI) is currently open (not persisted, always starts asfalse)Messenger API:
UiStateController:setUiOpen— Called by platform codeUiStateController:stateChange— Subscribed to by consumer controllersReferences
Checklist