MetaMask / MetaMask/metamask-extension

[Epic] State Sync Redesign: Per-Controller Pipeline

Open
#40,063 0 comments 0 reactions 0 assignees View on GitHub
INVALID-ISSUE-TEMPLATE team-extension-platform
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.