Skip to content

Conversation

@andrepimenta
Copy link
Member

@andrepimenta andrepimenta commented Nov 19, 2025

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:

// Service accepts platform-specific adapter (like ErrorReportingService)
const service = new StorageService({
  messenger,
  storage: filesystemStorageAdapter, // Mobile provides this
});

Example controller usage:

// 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

Checklist

  • 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)
  • 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
  • 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)
  • 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

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.

Written by Cursor Bugbot for commit 6bf384a. This will update automatically on new commits. Configure here.

Add @metamask/storage-service package to enable controllers to store large,
infrequently-accessed data outside of Redux state.

**Problem**:
- 10.79 MB Engine state with 92% (10.18 MB) in just 2 controllers
- Slow app startup parsing large state
- High memory usage from rarely-accessed data

**Solution**:
Platform-agnostic StorageService with:
- Messenger integration (setItem, getItem, removeItem actions)
- Event system (itemSet, itemRemoved events)
- Storage adapters (FilesystemStorage, IndexedDB, InMemoryAdapter)
- Namespace isolation (storage:{namespace}:{key})

**Impact**:
- 92% state reduction potential (10.79 MB → 0.85 MB)
- Lazy loading - data loaded only when needed
- Event-driven - controllers react without coupling

**Implementation**:
- 100% test coverage (44 tests)
- Platform-agnostic design
- InMemoryAdapter for tests (zero config)
- Follows ErrorReportingService pattern

**Targets**:
- SnapController: 6.09 MB sourceCode
- TokenListController: 4.09 MB cache
- getAllKeys and clear now take namespace parameter
- Adapters handle filtering and clearing (allows platform-specific optimization)
- Removed internal key registry from core (simpler code)
- Renamed clearNamespace to clear (consistent with other methods)
- Use STORAGE_KEY_PREFIX constant ('storageService:') instead of hardcoded 'storage:'
- InMemoryAdapter implements filtering and clearing logic
- Mobile adapter can now be optimized per-platform

Benefits:
- Simpler core service (no registry maintenance)
- Platform-specific optimizations possible (IndexedDB can use IDBKeyRange)
- Clear adapter responsibilities (filtering, prefix handling)
- Consistent API (all methods take namespace first)

Tests: 100% coverage maintained, all tests passing
Move key building responsibility from core to adapters:
- StorageAdapter methods now take (namespace, key) parameters
- Adapters build full key format internally
- Core no longer has #buildKey() method
- Each platform can choose optimal key format

Benefits:
- Simpler core (no key format knowledge)
- Platform flexibility (IndexedDB could use different format)
- Adapter owns all key logic
- InMemoryAdapter builds: storageService:namespace:key
- Mobile adapter builds: storageService:namespace:key

Interface change (breaking for adapters):
- getItem(key) → getItem(namespace, key)
- setItem(key, value) → setItem(namespace, key, value)
- removeItem(key) → removeItem(namespace, key)

All tests updated and passing (100% coverage).
… adapters

- Remove StoredDataWrapper from exports (each adapter defines its own)
- Remove STORAGE_KEY_PREFIX from StorageService (adapters handle key building)
- Update StorageAdapter interface: getItem returns unknown (adapter handles parsing)
- Adapters now fully responsible for:
  - Building full storage keys (e.g., storageService:namespace:key)
  - Wrapping data with metadata (timestamp)
  - Serialization/deserialization
- Update tests to match new adapter delegation pattern
- 100% test coverage maintained
…rics

- Update StorageAdapter interface (adapters handle key building & serialization)
- Update adapter examples with full implementation
- Rename clearNamespace to clear
- Update key prefix from storage: to storageService:
- Update metrics to precise values (5.95 MB, 3.99 MB, 9.94 MB, 166 KB, 61 bytes)
@andrepimenta andrepimenta marked this pull request as ready for review November 25, 2025 18:07
@andrepimenta
Copy link
Member Author

@metamaskbot publish-preview

@github-actions
Copy link
Contributor

Preview builds have been published. See these instructions for more information about preview builds.

Expand for full list of packages and versions.
{
  "@metamask-previews/account-tree-controller": "3.0.0-preview-e493d3e8",
  "@metamask-previews/accounts-controller": "34.0.0-preview-e493d3e8",
  "@metamask-previews/address-book-controller": "7.0.0-preview-e493d3e8",
  "@metamask-previews/analytics-controller": "0.0.0-preview-e493d3e8",
  "@metamask-previews/announcement-controller": "8.0.0-preview-e493d3e8",
  "@metamask-previews/app-metadata-controller": "2.0.0-preview-e493d3e8",
  "@metamask-previews/approval-controller": "8.0.0-preview-e493d3e8",
  "@metamask-previews/assets-controllers": "88.0.0-preview-e493d3e8",
  "@metamask-previews/base-controller": "9.0.0-preview-e493d3e8",
  "@metamask-previews/bridge-controller": "60.1.0-preview-e493d3e8",
  "@metamask-previews/bridge-status-controller": "60.1.0-preview-e493d3e8",
  "@metamask-previews/build-utils": "3.0.4-preview-e493d3e8",
  "@metamask-previews/chain-agnostic-permission": "1.2.2-preview-e493d3e8",
  "@metamask-previews/claims-controller": "0.2.0-preview-e493d3e8",
  "@metamask-previews/composable-controller": "12.0.0-preview-e493d3e8",
  "@metamask-previews/controller-utils": "11.15.0-preview-e493d3e8",
  "@metamask-previews/core-backend": "4.1.0-preview-e493d3e8",
  "@metamask-previews/delegation-controller": "1.0.0-preview-e493d3e8",
  "@metamask-previews/earn-controller": "10.0.0-preview-e493d3e8",
  "@metamask-previews/eip-5792-middleware": "2.0.0-preview-e493d3e8",
  "@metamask-previews/eip-7702-internal-rpc-middleware": "0.1.0-preview-e493d3e8",
  "@metamask-previews/eip1193-permission-middleware": "1.0.2-preview-e493d3e8",
  "@metamask-previews/ens-controller": "18.0.0-preview-e493d3e8",
  "@metamask-previews/error-reporting-service": "3.0.0-preview-e493d3e8",
  "@metamask-previews/eth-block-tracker": "14.0.0-preview-e493d3e8",
  "@metamask-previews/eth-json-rpc-middleware": "21.0.0-preview-e493d3e8",
  "@metamask-previews/eth-json-rpc-provider": "5.0.1-preview-e493d3e8",
  "@metamask-previews/foundryup": "1.0.1-preview-e493d3e8",
  "@metamask-previews/gas-fee-controller": "25.0.0-preview-e493d3e8",
  "@metamask-previews/gator-permissions-controller": "0.4.0-preview-e493d3e8",
  "@metamask-previews/json-rpc-engine": "10.1.1-preview-e493d3e8",
  "@metamask-previews/json-rpc-middleware-stream": "8.0.8-preview-e493d3e8",
  "@metamask-previews/keyring-controller": "24.0.0-preview-e493d3e8",
  "@metamask-previews/logging-controller": "7.0.0-preview-e493d3e8",
  "@metamask-previews/message-manager": "14.0.0-preview-e493d3e8",
  "@metamask-previews/messenger": "0.3.0-preview-e493d3e8",
  "@metamask-previews/multichain-account-service": "3.0.0-preview-e493d3e8",
  "@metamask-previews/multichain-api-middleware": "1.2.4-preview-e493d3e8",
  "@metamask-previews/multichain-network-controller": "2.0.0-preview-e493d3e8",
  "@metamask-previews/multichain-transactions-controller": "6.0.0-preview-e493d3e8",
  "@metamask-previews/name-controller": "9.0.0-preview-e493d3e8",
  "@metamask-previews/network-controller": "25.0.0-preview-e493d3e8",
  "@metamask-previews/network-enablement-controller": "3.1.0-preview-e493d3e8",
  "@metamask-previews/notification-services-controller": "20.0.0-preview-e493d3e8",
  "@metamask-previews/permission-controller": "12.1.0-preview-e493d3e8",
  "@metamask-previews/permission-log-controller": "5.0.0-preview-e493d3e8",
  "@metamask-previews/phishing-controller": "15.0.1-preview-e493d3e8",
  "@metamask-previews/polling-controller": "15.0.0-preview-e493d3e8",
  "@metamask-previews/preferences-controller": "21.0.0-preview-e493d3e8",
  "@metamask-previews/profile-sync-controller": "26.0.0-preview-e493d3e8",
  "@metamask-previews/rate-limit-controller": "7.0.0-preview-e493d3e8",
  "@metamask-previews/remote-feature-flag-controller": "2.0.0-preview-e493d3e8",
  "@metamask-previews/sample-controllers": "3.0.0-preview-e493d3e8",
  "@metamask-previews/seedless-onboarding-controller": "6.1.0-preview-e493d3e8",
  "@metamask-previews/selected-network-controller": "25.0.0-preview-e493d3e8",
  "@metamask-previews/shield-controller": "2.1.0-preview-e493d3e8",
  "@metamask-previews/signature-controller": "36.0.0-preview-e493d3e8",
  "@metamask-previews/storage-service": "1.0.0-preview-e493d3e8",
  "@metamask-previews/subscription-controller": "4.2.2-preview-e493d3e8",
  "@metamask-previews/token-search-discovery-controller": "4.0.0-preview-e493d3e8",
  "@metamask-previews/transaction-controller": "61.3.0-preview-e493d3e8",
  "@metamask-previews/transaction-pay-controller": "7.0.0-preview-e493d3e8",
  "@metamask-previews/user-operation-controller": "40.0.0-preview-e493d3e8"
}

@andrepimenta
Copy link
Member Author

@metamaskbot publish-preview

@github-actions
Copy link
Contributor

Preview builds have been published. See these instructions for more information about preview builds.

Expand for full list of packages and versions.
{
  "@metamask-previews/account-tree-controller": "4.0.0-preview-f5c5aecd",
  "@metamask-previews/accounts-controller": "35.0.0-preview-f5c5aecd",
  "@metamask-previews/address-book-controller": "7.0.1-preview-f5c5aecd",
  "@metamask-previews/analytics-controller": "0.0.0-preview-f5c5aecd",
  "@metamask-previews/announcement-controller": "8.0.0-preview-f5c5aecd",
  "@metamask-previews/app-metadata-controller": "2.0.0-preview-f5c5aecd",
  "@metamask-previews/approval-controller": "8.0.0-preview-f5c5aecd",
  "@metamask-previews/assets-controllers": "91.0.0-preview-f5c5aecd",
  "@metamask-previews/base-controller": "9.0.0-preview-f5c5aecd",
  "@metamask-previews/bridge-controller": "63.1.0-preview-f5c5aecd",
  "@metamask-previews/bridge-status-controller": "63.0.0-preview-f5c5aecd",
  "@metamask-previews/build-utils": "3.0.4-preview-f5c5aecd",
  "@metamask-previews/chain-agnostic-permission": "1.2.2-preview-f5c5aecd",
  "@metamask-previews/claims-controller": "0.2.0-preview-f5c5aecd",
  "@metamask-previews/composable-controller": "12.0.0-preview-f5c5aecd",
  "@metamask-previews/controller-utils": "11.16.0-preview-f5c5aecd",
  "@metamask-previews/core-backend": "5.0.0-preview-f5c5aecd",
  "@metamask-previews/delegation-controller": "2.0.0-preview-f5c5aecd",
  "@metamask-previews/earn-controller": "11.0.0-preview-f5c5aecd",
  "@metamask-previews/eip-5792-middleware": "2.0.0-preview-f5c5aecd",
  "@metamask-previews/eip-7702-internal-rpc-middleware": "0.1.0-preview-f5c5aecd",
  "@metamask-previews/eip1193-permission-middleware": "1.0.2-preview-f5c5aecd",
  "@metamask-previews/ens-controller": "19.0.0-preview-f5c5aecd",
  "@metamask-previews/error-reporting-service": "3.0.0-preview-f5c5aecd",
  "@metamask-previews/eth-block-tracker": "15.0.0-preview-f5c5aecd",
  "@metamask-previews/eth-json-rpc-middleware": "22.0.0-preview-f5c5aecd",
  "@metamask-previews/eth-json-rpc-provider": "6.0.0-preview-f5c5aecd",
  "@metamask-previews/foundryup": "1.0.1-preview-f5c5aecd",
  "@metamask-previews/gas-fee-controller": "26.0.0-preview-f5c5aecd",
  "@metamask-previews/gator-permissions-controller": "0.6.0-preview-f5c5aecd",
  "@metamask-previews/json-rpc-engine": "10.2.0-preview-f5c5aecd",
  "@metamask-previews/json-rpc-middleware-stream": "8.0.8-preview-f5c5aecd",
  "@metamask-previews/keyring-controller": "25.0.0-preview-f5c5aecd",
  "@metamask-previews/logging-controller": "7.0.1-preview-f5c5aecd",
  "@metamask-previews/message-manager": "14.1.0-preview-f5c5aecd",
  "@metamask-previews/messenger": "0.3.0-preview-f5c5aecd",
  "@metamask-previews/multichain-account-service": "4.0.0-preview-f5c5aecd",
  "@metamask-previews/multichain-api-middleware": "1.2.4-preview-f5c5aecd",
  "@metamask-previews/multichain-network-controller": "3.0.0-preview-f5c5aecd",
  "@metamask-previews/multichain-transactions-controller": "7.0.0-preview-f5c5aecd",
  "@metamask-previews/name-controller": "9.0.0-preview-f5c5aecd",
  "@metamask-previews/network-controller": "26.0.0-preview-f5c5aecd",
  "@metamask-previews/network-enablement-controller": "4.0.0-preview-f5c5aecd",
  "@metamask-previews/notification-services-controller": "21.0.0-preview-f5c5aecd",
  "@metamask-previews/permission-controller": "12.1.1-preview-f5c5aecd",
  "@metamask-previews/permission-log-controller": "5.0.0-preview-f5c5aecd",
  "@metamask-previews/phishing-controller": "16.1.0-preview-f5c5aecd",
  "@metamask-previews/polling-controller": "16.0.0-preview-f5c5aecd",
  "@metamask-previews/preferences-controller": "22.0.0-preview-f5c5aecd",
  "@metamask-previews/profile-metrics-controller": "0.0.0-preview-f5c5aecd",
  "@metamask-previews/profile-sync-controller": "27.0.0-preview-f5c5aecd",
  "@metamask-previews/rate-limit-controller": "7.0.0-preview-f5c5aecd",
  "@metamask-previews/remote-feature-flag-controller": "2.0.1-preview-f5c5aecd",
  "@metamask-previews/sample-controllers": "4.0.0-preview-f5c5aecd",
  "@metamask-previews/seedless-onboarding-controller": "7.0.0-preview-f5c5aecd",
  "@metamask-previews/selected-network-controller": "26.0.0-preview-f5c5aecd",
  "@metamask-previews/shield-controller": "3.0.0-preview-f5c5aecd",
  "@metamask-previews/signature-controller": "37.0.0-preview-f5c5aecd",
  "@metamask-previews/storage-service": "1.0.0-preview-f5c5aecd",
  "@metamask-previews/subscription-controller": "5.0.0-preview-f5c5aecd",
  "@metamask-previews/token-search-discovery-controller": "4.0.0-preview-f5c5aecd",
  "@metamask-previews/transaction-controller": "62.3.0-preview-f5c5aecd",
  "@metamask-previews/transaction-pay-controller": "10.1.0-preview-f5c5aecd",
  "@metamask-previews/user-operation-controller": "41.0.0-preview-f5c5aecd"
}

- Add warnings that service is designed for large values (100KB+)
- Add examples of good vs bad usage patterns
- Discourage many small key-value pairs in favor of single large objects
andrepimenta and others added 6 commits November 26, 2025 22:42
Co-authored-by: Mark Stacey <markjstacey@gmail.com>
Co-authored-by: Mark Stacey <markjstacey@gmail.com>
- Remove itemRemoved event publishing from removeItem() and clear()
- Remove StorageServiceItemRemovedEvent type
- Update StorageServiceEvents to only include itemSet
- Simplify API: controllers calling remove/clear already know what they're doing
- Update README and ADR documentation
Add standard MetaMask dual license setup as requested by legal:
- LICENSE: Dual license pointer
- LICENSE.MIT: MIT License
- LICENSE.APACHE2: Apache License 2.0
Since there's no schema validation, using generics gives false type safety.
- setItem now accepts value: unknown
- getItem now returns Promise<unknown>
- Callers must validate/cast the returned data
- Updated JSDoc examples to show casting pattern
andrepimenta and others added 4 commits November 27, 2025 16:33
- Add MESSENGER_EXPOSED_METHODS constant
- Use registerMethodActionHandlers for bulk action registration
- Auto-generate action types via generate-method-action-types script
- Simplify exports (StorageServiceActions = StorageServiceMethodActions)
Co-authored-by: Mark Stacey <markjstacey@gmail.com>
- Remove marketing copy (Problem/Solution/Real-World Impact)
- Remove API section (use inline docs instead)
- Move 'When to Use' to top for visibility
- Simplify usage examples
- Keep StorageAdapter Interface for implementers
- Verify event publishes with [key, value] payload
- Verify event only fires for matching namespace
- Replace unknown with Json type from @metamask/utils for type safety
- Add @metamask/utils as dependency (required for public API types)
- Remove StoredDataWrapper - just stringify/parse values directly
- Update StorageAdapter interface, StorageService, and InMemoryStorageAdapter
- Add storage-service to .github/CODEOWNERS with extension-platform,
  mobile-platform, and core-platform as owners
- Add storage-service to teams.json with extension and mobile platform teams
@andrepimenta andrepimenta requested a review from a team as a code owner November 27, 2025 22:33
@andrepimenta
Copy link
Member Author

@metamaskbot publish-preview

@github-actions
Copy link
Contributor

Preview builds have been published. See these instructions for more information about preview builds.

Expand for full list of packages and versions.
{
  "@metamask-previews/account-tree-controller": "4.0.0-preview-d6fe4594",
  "@metamask-previews/accounts-controller": "35.0.0-preview-d6fe4594",
  "@metamask-previews/address-book-controller": "7.0.1-preview-d6fe4594",
  "@metamask-previews/analytics-controller": "0.0.0-preview-d6fe4594",
  "@metamask-previews/announcement-controller": "8.0.0-preview-d6fe4594",
  "@metamask-previews/app-metadata-controller": "2.0.0-preview-d6fe4594",
  "@metamask-previews/approval-controller": "8.0.0-preview-d6fe4594",
  "@metamask-previews/assets-controllers": "92.0.0-preview-d6fe4594",
  "@metamask-previews/base-controller": "9.0.0-preview-d6fe4594",
  "@metamask-previews/bridge-controller": "63.2.0-preview-d6fe4594",
  "@metamask-previews/bridge-status-controller": "63.1.0-preview-d6fe4594",
  "@metamask-previews/build-utils": "3.0.4-preview-d6fe4594",
  "@metamask-previews/chain-agnostic-permission": "1.2.2-preview-d6fe4594",
  "@metamask-previews/claims-controller": "0.2.0-preview-d6fe4594",
  "@metamask-previews/composable-controller": "12.0.0-preview-d6fe4594",
  "@metamask-previews/controller-utils": "11.16.0-preview-d6fe4594",
  "@metamask-previews/core-backend": "5.0.0-preview-d6fe4594",
  "@metamask-previews/delegation-controller": "2.0.0-preview-d6fe4594",
  "@metamask-previews/earn-controller": "11.0.0-preview-d6fe4594",
  "@metamask-previews/eip-5792-middleware": "2.0.0-preview-d6fe4594",
  "@metamask-previews/eip-7702-internal-rpc-middleware": "0.1.0-preview-d6fe4594",
  "@metamask-previews/eip1193-permission-middleware": "1.0.2-preview-d6fe4594",
  "@metamask-previews/ens-controller": "19.0.0-preview-d6fe4594",
  "@metamask-previews/error-reporting-service": "3.0.0-preview-d6fe4594",
  "@metamask-previews/eth-block-tracker": "15.0.0-preview-d6fe4594",
  "@metamask-previews/eth-json-rpc-middleware": "22.0.0-preview-d6fe4594",
  "@metamask-previews/eth-json-rpc-provider": "6.0.0-preview-d6fe4594",
  "@metamask-previews/foundryup": "1.0.1-preview-d6fe4594",
  "@metamask-previews/gas-fee-controller": "26.0.0-preview-d6fe4594",
  "@metamask-previews/gator-permissions-controller": "0.6.0-preview-d6fe4594",
  "@metamask-previews/json-rpc-engine": "10.2.0-preview-d6fe4594",
  "@metamask-previews/json-rpc-middleware-stream": "8.0.8-preview-d6fe4594",
  "@metamask-previews/keyring-controller": "25.0.0-preview-d6fe4594",
  "@metamask-previews/logging-controller": "7.0.1-preview-d6fe4594",
  "@metamask-previews/message-manager": "14.1.0-preview-d6fe4594",
  "@metamask-previews/messenger": "0.3.0-preview-d6fe4594",
  "@metamask-previews/multichain-account-service": "4.0.0-preview-d6fe4594",
  "@metamask-previews/multichain-api-middleware": "1.2.4-preview-d6fe4594",
  "@metamask-previews/multichain-network-controller": "3.0.0-preview-d6fe4594",
  "@metamask-previews/multichain-transactions-controller": "7.0.0-preview-d6fe4594",
  "@metamask-previews/name-controller": "9.0.0-preview-d6fe4594",
  "@metamask-previews/network-controller": "26.0.0-preview-d6fe4594",
  "@metamask-previews/network-enablement-controller": "4.0.0-preview-d6fe4594",
  "@metamask-previews/notification-services-controller": "21.0.0-preview-d6fe4594",
  "@metamask-previews/permission-controller": "12.1.1-preview-d6fe4594",
  "@metamask-previews/permission-log-controller": "5.0.0-preview-d6fe4594",
  "@metamask-previews/phishing-controller": "16.1.0-preview-d6fe4594",
  "@metamask-previews/polling-controller": "16.0.0-preview-d6fe4594",
  "@metamask-previews/preferences-controller": "22.0.0-preview-d6fe4594",
  "@metamask-previews/profile-metrics-controller": "0.0.0-preview-d6fe4594",
  "@metamask-previews/profile-sync-controller": "27.0.0-preview-d6fe4594",
  "@metamask-previews/rate-limit-controller": "7.0.0-preview-d6fe4594",
  "@metamask-previews/remote-feature-flag-controller": "2.0.1-preview-d6fe4594",
  "@metamask-previews/sample-controllers": "4.0.0-preview-d6fe4594",
  "@metamask-previews/seedless-onboarding-controller": "7.0.0-preview-d6fe4594",
  "@metamask-previews/selected-network-controller": "26.0.0-preview-d6fe4594",
  "@metamask-previews/shield-controller": "3.1.0-preview-d6fe4594",
  "@metamask-previews/signature-controller": "37.0.0-preview-d6fe4594",
  "@metamask-previews/storage-service": "0.0.0-preview-d6fe4594",
  "@metamask-previews/subscription-controller": "5.1.0-preview-d6fe4594",
  "@metamask-previews/token-search-discovery-controller": "4.0.0-preview-d6fe4594",
  "@metamask-previews/transaction-controller": "62.3.1-preview-d6fe4594",
  "@metamask-previews/transaction-pay-controller": "10.2.0-preview-d6fe4594",
  "@metamask-previews/user-operation-controller": "41.0.0-preview-d6fe4594"
}

const serialized = this.#storage.get(fullKey);

if (!serialized) {
return null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. This could be ambiguous for the caller, there would be no way to differentiate data stored as null versus non-existent data.

Maybe we could return the data wrapped in an object, e.g. { data: ___ }, so that the null return can more clearly mean that it's not present?

Or we could use undefined as the empty return value, but that seems less than idea given that we'll be calling this across process boundaries on extension, and that would end up coerced to null.

Copy link
Member

@Gudahtt Gudahtt Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or I guess we could go Result type with this, e.g. { result } or { error } or {} (empty), to capture the error case as well. If you wanted this to "not crash by default", but in a way that still allows callers to handle the error.

return JSON.parse(serialized) as Json;
} catch (error) {
// istanbul ignore next - defensive error handling for corrupted data
console.error(`Failed to parse stored data for ${fullKey}:`, error);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems a bit dangerous. The caller would have no way to differentiate empty data from data that we failed to retrieve. The caller might then proceed when it's unsafe to do so, or overwrite the data, or something like that.

Given that we don't know precisely why the data is being stored/retrieved, or what the consequences of this failure might be, it would be safer to not catch the error and let the caller deal with it. We can highlight that it can throw with a TSDoc @throws directive in the doc comment for getItem (on both the service and the storage adapter) so that it's more obvious to the caller that they should expect failure some of the time.

try {
return JSON.parse(serialized) as Json;
} catch (error) {
// istanbul ignore next - defensive error handling for corrupted data
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This is a legitimate test case, we should be able to test this (even if we do remove the test block). Storage corruption can happen.

andrepimenta and others added 3 commits November 28, 2025 17:22
Co-authored-by: Mark Stacey <markjstacey@gmail.com>
Co-authored-by: Mark Stacey <markjstacey@gmail.com>
Co-authored-by: Mark Stacey <markjstacey@gmail.com>
Distinguish between found, not found, and error conditions:
- { result: Json } - data found and retrieved
- {} (empty object) - key doesn't exist
- { error: Error } - error occurred during retrieval

This allows consumers to distinguish stored null from missing keys.

Also adds test for corrupted data error handling.
Gudahtt
Gudahtt previously approved these changes Nov 28, 2025
Copy link
Member

@Gudahtt Gudahtt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!

return { result };
} catch (error) {
console.error(`Failed to parse stored data for ${fullKey}:`, error);
return { error: error as Error };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normally I don't like to cast if we can avoid it, and we can avoid it here by validating the error type.

But... it is pretty easy to tell in this case that the only possible error here is SyntaxError. So.... this seems pretty unobjectionable.

I might propose an improvement here later, but I can do it as a separate PR

Co-authored-by: Mark Stacey <markjstacey@gmail.com>
Gudahtt
Gudahtt previously approved these changes Nov 28, 2025
@andrepimenta andrepimenta added this pull request to the merge queue Nov 28, 2025
Merged via the queue into main with commit c8fc0e1 Nov 28, 2025
281 checks passed
@andrepimenta andrepimenta deleted the storage-service branch November 28, 2025 21:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants