Skip to content

Commit a6dfc88

Browse files
committed
feat(ClientController): init
1 parent 14e4e02 commit a6dfc88

23 files changed

Lines changed: 346 additions & 319 deletions

.github/CODEOWNERS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
/packages/eip-5792-middleware @MetaMask/wallet-integrations
7272

7373
## Core Platform Team
74-
/packages/ui-state-controller @MetaMask/core-platform
74+
/packages/client-controller @MetaMask/core-platform
7575
/packages/base-controller @MetaMask/core-platform
7676
/packages/build-utils @MetaMask/core-platform
7777
/packages/composable-controller @MetaMask/core-platform

.yarnrc.yml

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ compressionLevel: mixed
22

33
enableGlobalCache: false
44

5+
enableImmutableInstalls: true
6+
57
enableScripts: false
68

79
enableTelemetry: false
@@ -12,18 +14,14 @@ logFilters:
1214

1315
nodeLinker: node-modules
1416

15-
plugins:
16-
- path: .yarn/plugins/@yarnpkg/plugin-allow-scripts.cjs
17-
spec: "https://raw.githubusercontent.com/LavaMoat/LavaMoat/main/packages/yarn-plugin-allow-scripts/bundles/@yarnpkg/plugin-allow-scripts.js"
18-
19-
# Configure the NPM minimal age gate to 3 days, meaning packages must be at
20-
# least 3 days old to be installed.
21-
npmMinimalAgeGate: 4320 # 3 days (in minutes)
17+
npmMinimalAgeGate: 4320
2218

23-
# Override the minimal age gate, allowing certain packages to be installed
24-
# regardless of their publish age.
2519
npmPreapprovedPackages:
2620
- "@metamask/*"
2721
- "@metamask-previews/*"
2822
- "@lavamoat/*"
2923
- "@ts-bridge/*"
24+
25+
plugins:
26+
- path: .yarn/plugins/@yarnpkg/plugin-allow-scripts.cjs
27+
spec: "https://raw.githubusercontent.com/LavaMoat/LavaMoat/main/packages/yarn-plugin-allow-scripts/bundles/@yarnpkg/plugin-allow-scripts.js"
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [Unreleased]
9+
10+
### Added
11+
12+
- Initial release of `@metamask/client-controller`
13+
- `ClientController` for managing client (UI) open/closed state
14+
- `ClientController:setUiOpen` messenger action for platform code to call
15+
- `ClientController:stateChange` event for controllers to subscribe to lifecycle changes
16+
- `isUiOpen` state property (not persisted - always starts as `false`)
17+
- `clientControllerSelectors.selectIsUiOpen` selector for derived state access
18+
- Full TypeScript support with exported types
19+
20+
[Unreleased]: https://github.com/MetaMask/core/
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
# `@metamask/client-controller`
2+
3+
Client-level state for MetaMask (e.g. whether a UI window is open). Provides a centralized way for controllers to respond to application lifecycle changes.
4+
5+
## Installation
6+
7+
```bash
8+
yarn add @metamask/client-controller
9+
```
10+
11+
or
12+
13+
```bash
14+
npm install @metamask/client-controller
15+
```
16+
17+
## Usage
18+
19+
### Basic Setup
20+
21+
```typescript
22+
import { Messenger } from '@metamask/messenger';
23+
import {
24+
ClientController,
25+
ClientControllerActions,
26+
ClientControllerEvents,
27+
} from '@metamask/client-controller';
28+
29+
const rootMessenger = new Messenger<
30+
'Root',
31+
ClientControllerActions,
32+
ClientControllerEvents
33+
>({ namespace: 'Root' });
34+
35+
const controllerMessenger = new Messenger({
36+
namespace: 'ClientController',
37+
parent: rootMessenger,
38+
});
39+
40+
const clientController = new ClientController({
41+
messenger: controllerMessenger,
42+
});
43+
```
44+
45+
### Platform Integration
46+
47+
Platform code calls `ClientController:setUiOpen` when the UI is opened or
48+
closed:
49+
50+
```text
51+
onUiOpened() {
52+
controllerMessenger.call('ClientController:setUiOpen', true);
53+
}
54+
55+
onUiClosed() {
56+
controllerMessenger.call('ClientController:setUiOpen', false);
57+
}
58+
```
59+
60+
### Consumer controller and using with other lifecycle state (e.g. Keyring unlock/lock)
61+
62+
Use `ClientController:stateChange` only for behavior that **must** run when the
63+
UI is open or closed (e.g., pausing/resuming a critical background task). **Use
64+
the selector** when subscribing so the handler receives a single derived value
65+
(e.g. `isUiOpen`), and **prefer pause/resume** over stop/start for polling.
66+
67+
UI open/close alone is usually not enough to decide when to start or stop work.
68+
Combine `ClientController:stateChange` with other lifecycle events, such as
69+
**KeyringController:unlock** / **KeyringController:lock** (or any controller that
70+
expresses "ready for background work"). Only start subscriptions, polling, or
71+
network requests when **both** the UI is open and the keyring (or equivalent) is
72+
unlocked; stop or pause when the UI closes **or** the keyring locks.
73+
74+
#### Important: Usage guidelines and warnings
75+
76+
**Do not subscribe to updates for all kinds of data as soon as the client
77+
opens.** When MetaMask opens, the current screen may not need every type of
78+
data. Starting subscriptions, polling, or network requests for everything when
79+
`isUiOpen` becomes true can lead to unnecessary network traffic and battery
80+
use, requests before onboarding is complete (a recurring source of issues), and
81+
poor performance as more features are added.
82+
83+
**Use this controller responsibly:**
84+
85+
- Start only the subscriptions, polling, or requests that are **needed for the
86+
current screen or flow**
87+
- Do **not** start network-dependent or heavy behavior solely because
88+
`ClientController:stateChange` reported `isUiOpen: true`
89+
- Consider **deferring** non-critical updates until the user has completed
90+
onboarding or reached a screen that needs that data
91+
- Prefer starting and stopping per feature or per screen (e.g., when a
92+
component mounts that needs the data) rather than globally when the client
93+
opens
94+
- **Combine with Keyring unlock/lock:** Only start work when it is appropriate
95+
for both UI open state and wallet state (e.g. client open **and** keyring
96+
unlocked)
97+
- **Prefer pause/resume over stop/start for polling** so you can resume without
98+
full re-initialization. Use the selector when subscribing (see example
99+
below).
100+
101+
```typescript
102+
import { clientControllerSelectors } from '@metamask/client-controller';
103+
104+
class SomeDataController extends BaseController {
105+
#uiOpen = false;
106+
#keyringUnlocked = false;
107+
108+
constructor({ messenger }) {
109+
super({ messenger, ... });
110+
111+
messenger.subscribe(
112+
'ClientController:stateChange',
113+
(isUiOpen) => {
114+
this.#uiOpen = isUiOpen;
115+
this.updateActive();
116+
},
117+
clientControllerSelectors.selectIsUiOpen,
118+
);
119+
120+
messenger.subscribe('KeyringController:unlock', () => {
121+
this.#keyringUnlocked = true;
122+
this.updateActive();
123+
});
124+
125+
messenger.subscribe('KeyringController:lock', () => {
126+
this.#keyringUnlocked = false;
127+
this.updateActive();
128+
});
129+
}
130+
131+
updateActive() {
132+
const shouldRun = this.#uiOpen && this.#keyringUnlocked;
133+
if (shouldRun) {
134+
this.resume();
135+
} else {
136+
this.pause();
137+
}
138+
}
139+
}
140+
```
141+
142+
Note: `stateChange` emits `[state, patches]`; the selector receives the full
143+
payload and returns the value passed to the handler (here, `isUiOpen`).
144+
145+
## API Reference
146+
147+
### State
148+
149+
| Property | Type | Description |
150+
| ---------- | --------- | ------------------------------------------ |
151+
| `isUiOpen` | `boolean` | Whether the client (UI) is currently open. |
152+
153+
State is not persisted. It always starts as `false`.
154+
155+
### Actions
156+
157+
| Action | Parameters | Description |
158+
| ---------------------------- | --------------- | ---------------------------- |
159+
| `ClientController:getState` | none | Returns current state. |
160+
| `ClientController:setUiOpen` | `open: boolean` | Sets whether the UI is open. |
161+
162+
### Events
163+
164+
| Event | Payload | Description |
165+
| ------------------------------ | ------------------ | ---------------------------- |
166+
| `ClientController:stateChange` | `[state, patches]` | Standard state change event. |
167+
168+
### Selectors
169+
170+
```typescript
171+
import { clientControllerSelectors } from '@metamask/client-controller';
172+
173+
const state = messenger.call('ClientController:getState');
174+
const isOpen = clientControllerSelectors.selectIsUiOpen(state);
175+
```
176+
177+
## Contributing
178+
179+
This package is part of a monorepo. Instructions for contributing can be found
180+
in the [monorepo README](https://github.com/MetaMask/core#readme).
File renamed without changes.
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
{
2-
"name": "@metamask/ui-state-controller",
2+
"name": "@metamask/client-controller",
33
"version": "0.0.0",
4-
"description": "Tracks and manages the lifecycle state of MetaMask as an application",
4+
"description": "Client-level state for MetaMask (e.g. whether a UI window is open)",
55
"keywords": [
66
"MetaMask",
77
"Ethereum"
88
],
9-
"homepage": "https://github.com/MetaMask/core/tree/main/packages/ui-state-controller#readme",
9+
"homepage": "https://github.com/MetaMask/core/tree/main/packages/client-controller#readme",
1010
"bugs": {
1111
"url": "https://github.com/MetaMask/core/issues"
1212
},
@@ -38,8 +38,8 @@
3838
"build": "ts-bridge --project tsconfig.build.json --verbose --clean --no-references",
3939
"build:all": "ts-bridge --project tsconfig.build.json --verbose --clean",
4040
"build:docs": "typedoc",
41-
"changelog:update": "../../scripts/update-changelog.sh @metamask/ui-state-controller",
42-
"changelog:validate": "../../scripts/validate-changelog.sh @metamask/ui-state-controller",
41+
"changelog:update": "../../scripts/update-changelog.sh @metamask/client-controller",
42+
"changelog:validate": "../../scripts/validate-changelog.sh @metamask/client-controller",
4343
"publish:preview": "yarn npm publish --tag preview",
4444
"since-latest-release": "../../scripts/since-latest-release.sh",
4545
"test": "NODE_OPTIONS=--experimental-vm-modules jest --reporters=jest-silent-reporter",

packages/ui-state-controller/src/UiStateController-method-action-types.ts renamed to packages/client-controller/src/ClientController-method-action-types.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Do not edit manually.
44
*/
55

6-
import type { UiStateController } from './UiStateController';
6+
import type { ClientController } from './ClientController';
77

88
/**
99
* Updates state with whether the MetaMask UI is open.
@@ -14,12 +14,12 @@ import type { UiStateController } from './UiStateController';
1414
*
1515
* @param open - Whether the MetaMask UI is open.
1616
*/
17-
export type UiStateControllerSetUiOpenAction = {
18-
type: `UiStateController:setUiOpen`;
19-
handler: UiStateController['setUiOpen'];
17+
export type ClientControllerSetUiOpenAction = {
18+
type: `ClientController:setUiOpen`;
19+
handler: ClientController['setUiOpen'];
2020
};
2121

2222
/**
23-
* Union of all UiStateController action types.
23+
* Union of all ClientController action types.
2424
*/
25-
export type UiStateControllerMethodActions = UiStateControllerSetUiOpenAction;
25+
export type ClientControllerMethodActions = ClientControllerSetUiOpenAction;

0 commit comments

Comments
 (0)