You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: Add StorageService for offloading large controller data (#7192)
# Add StorageService for Large Controller Data
## Explanation
### What is the current state and why does it need to change?
**Current state**: MetaMask Mobile Engine state is 10.79 MB, with 92%
(9.94 MB) concentrated in just 2 controllers:
- **SnapController**: 5.95 MB of snap source code (55% of total state)
- **TokenListController**: 3.99 MB of token metadata cache (37% of
state)
This data is **rarely accessed** after initial load but stays in Redux
state, causing:
- Slow app startup (parsing 10.79 MB on every launch)
- High memory usage (all data loaded even if not needed)
- Slow persist operations (up to 6.26 MB written per controller change)
**Why change**: Controllers need a way to store large,
infrequently-accessed data outside of Redux state while maintaining
platform portability and testability.
### What is the solution and how does it work?
**New package**: `@metamask/storage-service`
A platform-agnostic service that allows controllers to offload large
data from state to persistent storage via messenger actions.
**How it works**:
1. Controllers call `StorageService:setItem` via messenger to store
large data
2. StorageService saves to platform-specific storage (FilesystemStorage
on mobile, IndexedDB on extension)
3. StorageService publishes events
(`StorageService:itemSet:{namespace}`) so other controllers can react
4. Controllers call `StorageService:getItem` to load data lazily (only
when needed)
**Storage adapter pattern**:
```typescript
// Service accepts platform-specific adapter (like ErrorReportingService)
const service = new StorageService({
messenger,
storage: filesystemStorageAdapter, // Mobile provides this
});
```
**Example controller usage**:
```typescript
// Store data (out of state)
await this.messenger.call(
'StorageService:setItem',
'MyController',
'dataKey',
largeData,
);
// Load on demand (lazy loading)
const data = await this.messenger.call(
'StorageService:getItem',
'MyController',
'dataKey',
);
// Subscribe to changes (optional)
this.messenger.subscribe(
'StorageService:itemSet:MyController',
(key, value) => {
// React to storage changes
},
);
```
### Why this architecture?
**Platform-agnostic**: Service defines `StorageAdapter` interface;
clients provide implementation (mobile: FilesystemStorage, extension:
IndexedDB, tests: in-memory)
**Messenger-integrated**: Controllers access storage via messenger
actions, no direct dependencies
**Event-driven**: Controllers can subscribe to storage changes without
coupling
**Testable**: InMemoryAdapter provides zero-config testing (no mocking
needed)
**Proven pattern**: Follows ErrorReportingService design (accepts
platform-specific function)
### Expected impact?
**With both controllers optimized**:
- State: 10.79 MB → 0.85 MB (**92% reduction**)
- App startup: 92% faster state parsing
- Memory: 9.94 MB freed
- Disk I/O: Up to 9.94 MB less per persist
**This PR adds infrastructure** - Controllers can now use
StorageService. Separate PRs will integrate with SnapController and
TokenListController.
## References
- **ADR**:
[0017-storage-service-large-data.md](https://github.com/MetaMask/decisions/blob/core/0017-storage-service-large-data/decisions/core/0017-storage-service-large-data.md)
- **Mobile PR**: MetaMask/metamask-mobile#22943
## Checklist
- [x] I've updated the test suite for new or updated code as appropriate
- 100% test coverage (44 tests)
- Tests for all storage operations, namespace isolation, events, error
handling
- Real-world usage test (simulates 6 MB snap source code)
- [x] I've updated documentation (JSDoc, Markdown, etc.) for new or
updated code as appropriate
- Complete README with examples
- JSDoc for all public APIs
- Architecture documentation in ADR
- [x] I've communicated my changes to consumers by updating changelogs
for packages I've changed, highlighting breaking changes as necessary
- CHANGELOG.md created for initial release
- No breaking changes (new package)
- [x] I've prepared draft pull requests for clients and consumer
packages to resolve any breaking changes
- N/A - No breaking changes
- Consumer PRs will be created after this is merged and released
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> Introduces `@metamask/storage-service` with messenger-based APIs and
an in-memory adapter to offload large controller data, plus repo wiring
and ownership updates.
>
> - **New Package**: `packages/storage-service`
> - `StorageService`: Messenger-exposed methods `setItem`, `getItem`,
`removeItem`, `getAllKeys`, `clear`; publishes
`StorageService:itemSet:{namespace}` events.
> - `InMemoryStorageAdapter`: Default non-persistent adapter
implementing `StorageAdapter` with key prefix `storageService:`.
> - Types/exports: `StorageAdapter`, `StorageGetResult`, messenger
types, action types file, constants `SERVICE_NAME`,
`STORAGE_KEY_PREFIX`.
> - **Tests & Docs**:
> - Comprehensive unit tests for service and adapter (namespace
isolation, events, errors); Jest config with 100% coverage thresholds.
> - `README.md` with usage/examples; `CHANGELOG.md` initialized;
dual-license files.
> - **Repo Wiring**:
> - Add package to `tsconfig.build.json`, `yarn.lock`, exports/index,
and TypeDoc config.
> - Update `.github/CODEOWNERS` and `teams.json` to include
`storage-service` ownership.
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
6bf384a. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Mark Stacey <[email protected]>
0 commit comments