MetaMask / MetaMask/metamask-extension
[Epic] State Sync Redesign: Per-Controller Pipeline
- Dominant language
- TypeScript
- Stars
- 13.2k
- Forks
- 5.6k
- Avg merge
- 2d 5h
- Merged PRs (30d)
- 451
Description
Redesign the extension's background-to-UI state pipeline from monolithic broadcast to per-controller delivery.
- Phase A1 restructures the background — replacing 4 legacy V1-era broadcast layers with `ControllerRegistry`, per-controller messenger subscriptions, and microtask-batched transport instead of 200ms debounce — while preserving the `metamask` Redux slice via a flattening adapter.
- Phase A2 extends to UI-side per-controller `useSyncExternalStore` subscriptions, removing the slice entirely.
Decision not yet made — A1 is independently valuable.
## Key Benefits
_A1 (background cleanup):_
- **4 legacy V1-era broadcast layers removed** (`ComposableObservableStore` x2, `PatchStore`, `publicConfigStore`) — completes BaseController V2 migration on background and transport side for controller `stateChange` routing. `MetamaskController` keeps its `EventEmitter` base as the event channel for events emitted during early app load before controllers and their messengers are guaranteed to be initialized.
- **Simplified controller addition**: 6 manual steps to 2
- **3 bug classes eliminated**: unbounded UI starvation (debounce with no `maxWait`), listener leaks (per-controller subscriptions without cleanup accounting), untyped event subscriptions (silent mismatches)
- **Controller-level attribution**: keyed patches identify which controller changed, improving state-related bug investigation
- **Deterministic timing**: microtask + `requestAnimationFrame` replaces arbitrary debounce, making timing bugs reproducible
- **Port disconnect detection**: surfaces service worker termination as visible error instead of silent dead port
- **UI latency 200ms to 17ms**: microtask batching replaces background debounce
- **Remove `@metamask/obs-store` dependency** (~8KB bundle reduction)
- Zero UI breakage — selectors, tests, Sentry, state logs all unchanged
_A2 (UI migration, builds on A1):_
- **Scoped concurrent rendering**: Per-controller `useSyncExternalStore` scopes React 18 concurrent features (`useDeferredValue`, `useTransition`) to per-controller granularity — deferred or transitioned re-renders evaluate only the affected controller's selectors, not all ~635. See [#6657](https://github.com/MetaMask/MetaMask-planning/issues/6657).
- **Remove `metamask` Redux slice** and `forceUpdateMetamaskState` (~109 RPC round-trips)
- **8 of 10 circular dependency cycles eliminated**; remaining cycle becomes easier to resolve
- **Flat-merge namespace collisions eliminated**: keyed state all the way to the UI — no more `Object.assign` merge at the transport boundary
- **UI latency 17ms to sub-frame** (React 18 automatic batching)
## ADRs
- [Extension: State Sync Redesign (decisions#124)](https://github.com/MetaMask/decisions/pull/124)
- [Mobile: State Sync Redesign (decisions#125)](https://github.com/MetaMask/decisions/pull/125)
- [Core: State subscription service (decisions#129)](https://github.com/MetaMask/decisions/pull/129) — defines `ControllerStateProxy`, `StateSubscriptionService`, `@metamask/state-subscription-service` package
## Phases
| Phase | Epics | Scope |
| ----- | ----- | ----- |
| **A1** (background cleanup) | Epic 1 + Epic 2 | Transport optimization, legacy layer removal, `MetamaskController` decomposition. UI unchanged — flattening adapter preserves `metamask` Redux slice. |
| **A2** (UI migration) | Epic 3 | Per-controller `useSyncExternalStore` subscriptions, selector migration, `metamask` slice removal. Depends on A1. |
## Epics
- [ ] **Epic 1: Transport optimization** (A1) — #40059 (6 issues)
- [ ] **Epic 2: Background cleanup** (A1) — #40061 (7 issues)
- [ ] **Epic 3: UI-controller subscriptions** (A2) — #40060 (14 issues)
### Spike validation
[PR #41758](https://github.com/MetaMask/metamask-extension/pull/41758) validates the broadcast layer removal. On the spike branch, four V1-era layers are deleted (`ComposableObservableStore` x2, `PatchStore`, `publicConfigStore`) and `background.js` is migrated to TypeScript. `MetamaskController` continues to extend `EventEmitter` as the event channel for events emitted during early app load before controllers and their messengers are guaranteed to be initialized; controller `stateChange` routing moves off it onto the `Messenger`. This demonstrates that Epic 2 (Background cleanup) overlaps significantly with the [MetamaskController decomposition epic (#41735)](https://github.com/MetaMask/metamask-extension/issues/41735) — the same branch achieves both.
## Dependency Graph
```
Epic 1 Epic 2
Transport optimization Background cleanup
│ │
└────────┬───────────┘
│
A1 complete
│
▼
Epic 3
UI-controller
subscriptions
│
▼
Issue 2.7
Remove @metamask/obs-store
(needs both epics complete)
```
Epics 1 and 2 overlap within A1 — some Epic 2 issues can begin in parallel with Epic 1, but Epic 2 completion depends on Epic 1's transport changes. Epic 3 (A2) starts after A1 is complete.
## Prior art
[#29600 Unflatten `metamask` Redux slice](https://github.com/MetaMask/metamask-extension/issues/29600) (on hold). Phase 1 merged (controller-keyed hydration). Phase 2 stalled due to downstream footprint (~20k+ LoC test mocks, 6 PRs per controller). These epics supersede #29600.
## Related
- [UI Messenger ADR (decisions#117)](https://github.com/MetaMask/decisions/pull/117), [(decisions#126)](https://github.com/MetaMask/decisions/pull/126) — complementary write-side counterpart
- [React 18 migration (MetaMask-planning#6919)](https://github.com/MetaMask/MetaMask-planning/issues/6919) — complementary: React 18 makes concurrent rendering available; state sync redesign makes it effective for controller state by scoping re-renders to per-controller granularity. See [#6657](https://github.com/MetaMask/MetaMask-planning/issues/6657).
- [State sync reliability (MetaMask-planning#6780)](https://github.com/MetaMask/MetaMask-planning/issues/6780) — subsumed by this redesign
Contributor guide
Research direction
Start with the linked Extension State Sync Redesign ADR and the A1 dependency graph, then review Epics 1 and 2 and spike PR #41758. Treat A1 as the independently valuable boundary; completion should match its agreed transport and background-cleanup scope while preserving the metamask Redux slice, with A2 handled only after A1.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, typescript
- Domain
- backend, frontend, full-stack
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 15/100