Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
278eaa9
feat: Add StorageService for offloading large controller data
andrepimenta Nov 19, 2025
e844fdd
refactor(storage-service): Move getAllKeys/clear logic to adapters
andrepimenta Nov 19, 2025
19c7d6f
refactor: Adapters now build storage keys (not core)
andrepimenta Nov 25, 2025
5fedbc0
refactor(storage-service): delegate key building and serialization to…
andrepimenta Nov 25, 2025
5af0861
docs(storage-service): update CHANGELOG for initial release
andrepimenta Nov 25, 2025
de5388c
docs(storage-service): fix CHANGELOG format
andrepimenta Nov 25, 2025
e493d3e
docs(storage-service): update README with current API and precise met…
andrepimenta Nov 25, 2025
81100c2
build: add storage-service to tsconfig.build.json
andrepimenta Nov 26, 2025
f5c5aec
Merge branch 'main' into storage-service
andrepimenta Nov 26, 2025
04939f6
docs(storage-service): add JSDoc guidance for large value storage
andrepimenta Nov 26, 2025
3fe4314
Update packages/storage-service/src/StorageService.ts
andrepimenta Nov 26, 2025
84adfd5
Update packages/storage-service/src/StorageService.ts
andrepimenta Nov 26, 2025
7ac8bfc
refactor(storage-service): remove itemRemoved events, keep only itemSet
andrepimenta Nov 27, 2025
24b150e
Merge branch 'storage-service' of https://github.com/MetaMask/core in…
andrepimenta Nov 27, 2025
65efd41
chore(storage-service): add dual MIT+Apache2 license
andrepimenta Nov 27, 2025
60efad1
refactor(storage-service): use unknown instead of generic types
andrepimenta Nov 27, 2025
2222e57
refactor(storage-service): use generate-method-action-types pattern
andrepimenta Nov 27, 2025
ce68a05
Update packages/storage-service/package.json
andrepimenta Nov 27, 2025
afde837
docs(storage-service): simplify README to focus on usage
andrepimenta Nov 27, 2025
ae501a3
Merge branch 'storage-service' of https://github.com/MetaMask/core in…
andrepimenta Nov 27, 2025
81c9cf8
docs(storage-service): simplify CHANGELOG for initial release
andrepimenta Nov 27, 2025
625eee6
fix(storage-service): change event payload order to [key, value]
andrepimenta Nov 27, 2025
bda7243
Merge branch 'main' into storage-service
andrepimenta Nov 27, 2025
d3a7e4e
Fix prettier
andrepimenta Nov 27, 2025
389e50a
Merge branch 'storage-service' of https://github.com/MetaMask/core in…
andrepimenta Nov 27, 2025
3aeafac
Fix keywords
andrepimenta Nov 27, 2025
c46e2e2
Update packages/storage-service/package.json
andrepimenta Nov 27, 2025
1b48670
test(storage-service): add tests for itemSet event
andrepimenta Nov 27, 2025
8050f20
Merge branch 'storage-service' of https://github.com/MetaMask/core in…
andrepimenta Nov 27, 2025
9a65054
refactor(storage-service): use Json type instead of unknown
andrepimenta Nov 27, 2025
d6fe459
chore(storage-service): add CODEOWNERS and teams.json entries
andrepimenta Nov 27, 2025
5b4faeb
Update packages/storage-service/src/StorageService.ts
andrepimenta Nov 28, 2025
363533a
Update packages/storage-service/src/InMemoryStorageAdapter.ts
andrepimenta Nov 28, 2025
f5ccb18
Update packages/storage-service/src/InMemoryStorageAdapter.ts
andrepimenta Nov 28, 2025
b5c5d79
feat(storage-service): add StorageGetResult type for getItem responses
andrepimenta Nov 28, 2025
8557995
Merge branch 'storage-service' of https://github.com/MetaMask/core in…
andrepimenta Nov 28, 2025
7ccd865
Update packages/storage-service/CHANGELOG.md
andrepimenta Nov 28, 2025
a139816
fix(storage-service): fix prettier formatting in test
andrepimenta Nov 28, 2025
5598df9
Merge branch 'storage-service' of https://github.com/MetaMask/core in…
andrepimenta Nov 28, 2025
bbd8e39
Merge branch 'main' into storage-service
andrepimenta Nov 28, 2025
cf634fb
Prettier fix
andrepimenta Nov 28, 2025
6bf384a
Fix return types on StorageServiceGetItemAction
andrepimenta Nov 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
refactor(storage-service): use Json type instead of unknown
- 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
  • Loading branch information
andrepimenta committed Nov 27, 2025
commit 9a65054c0e29b415a9abe53b8c2adc5d4aeae96f
3 changes: 2 additions & 1 deletion packages/storage-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@
"test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch"
},
"dependencies": {
"@metamask/messenger": "^0.3.0"
"@metamask/messenger": "^0.3.0",
"@metamask/utils": "^11.8.1"
},
"devDependencies": {
"@metamask/auto-changelog": "^3.4.4",
Expand Down
38 changes: 12 additions & 26 deletions packages/storage-service/src/InMemoryStorageAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,8 @@
import type { Json } from '@metamask/utils';

import type { StorageAdapter } from './types';
import { STORAGE_KEY_PREFIX } from './types';

/**
* Wrapper for stored data with metadata.
* Each adapter can define its own wrapper structure.
*/
type StoredDataWrapper<T = unknown> = {
/** Timestamp when data was stored (milliseconds since epoch). */
timestamp: number;
/** The actual data being stored. */
data: T;
};

/**
* In-memory storage adapter (default fallback).
* Implements the {@link StorageAdapter} interface using a Map.
Expand All @@ -30,8 +21,8 @@ type StoredDataWrapper<T = unknown> = {
* @example
* ```typescript
* const adapter = new InMemoryStorageAdapter();
* await adapter.setItem('key', 'value');
* const value = await adapter.getItem('key'); // Returns 'value'
* await adapter.setItem('SnapController', 'snap-id:sourceCode', 'const x = 1;');
* const value = await adapter.getItem('SnapController', 'snap-id:sourceCode'); // 'const x = 1;'
* // After restart: data is lost
* ```
*/
Expand All @@ -51,13 +42,13 @@ export class InMemoryStorageAdapter implements StorageAdapter {

/**
* Retrieve an item from in-memory storage.
* Deserializes and unwraps the stored data.
* Deserializes JSON data from storage.
*
* @param namespace - The controller namespace.
* @param key - The data key.
* @returns The unwrapped data, or null if not found.
* @returns The parsed JSON data, or null if not found.
*/
async getItem(namespace: string, key: string): Promise<unknown> {
async getItem(namespace: string, key: string): Promise<Json | null> {
const fullKey = `${STORAGE_KEY_PREFIX}${namespace}:${key}`;
const serialized = this.#storage.get(fullKey);

Expand All @@ -66,8 +57,7 @@ export class InMemoryStorageAdapter implements StorageAdapter {
}

try {
const wrapper: StoredDataWrapper = JSON.parse(serialized);
return wrapper.data;
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.

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.

Expand All @@ -78,19 +68,15 @@ export class InMemoryStorageAdapter implements StorageAdapter {

/**
* Store an item in in-memory storage.
* Wraps with metadata and serializes to string.
* Serializes JSON data to string.
*
* @param namespace - The controller namespace.
* @param key - The data key.
* @param value - The value to store (will be wrapped and serialized).
* @param value - The JSON value to store.
*/
async setItem(namespace: string, key: string, value: unknown): Promise<void> {
async setItem(namespace: string, key: string, value: Json): Promise<void> {
const fullKey = `${STORAGE_KEY_PREFIX}${namespace}:${key}`;
const wrapper: StoredDataWrapper = {
timestamp: Date.now(),
data: value,
};
this.#storage.set(fullKey, JSON.stringify(wrapper));
this.#storage.set(fullKey, JSON.stringify(value));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import type { StorageService } from './StorageService';

/**
* Store large data in storage.
* Store large JSON data in storage.
*
* ⚠️ **Designed for large values (100KB+), not many small ones.**
* Each storage operation has I/O overhead. For best performance,
Expand All @@ -26,22 +26,19 @@ import type { StorageService } from './StorageService';
*
* @param namespace - Controller namespace (e.g., 'SnapController').
* @param key - Storage key (e.g., 'npm:@metamask/example-snap:sourceCode').
* @param value - Data to store (should be 100KB+ for optimal use).
* @param value - JSON data to store (should be 100KB+ for optimal use).
*/
export type StorageServiceSetItemAction = {
type: `StorageService:setItem`;
handler: StorageService['setItem'];
};

/**
* Retrieve data from storage.
*
* Returns `unknown` since there's no schema validation.
* Callers should validate or cast the result to the expected type.
* Retrieve JSON data from storage.
*
* @param namespace - Controller namespace (e.g., 'SnapController').
* @param key - Storage key (e.g., 'npm:@metamask/example-snap:sourceCode').
* @returns Parsed data or null if not found. Type is `unknown` - caller must validate.
* @returns Parsed JSON data or null if not found.
*/
export type StorageServiceGetItemAction = {
type: `StorageService:getItem`;
Expand Down
17 changes: 8 additions & 9 deletions packages/storage-service/src/StorageService.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Json } from '@metamask/utils';

import { InMemoryStorageAdapter } from './InMemoryStorageAdapter';
import type {
StorageAdapter,
Expand Down Expand Up @@ -133,7 +135,7 @@ export class StorageService {
}

/**
* Store large data in storage.
* Store large JSON data in storage.
*
* ⚠️ **Designed for large values (100KB+), not many small ones.**
* Each storage operation has I/O overhead. For best performance,
Expand All @@ -153,9 +155,9 @@ export class StorageService {
*
* @param namespace - Controller namespace (e.g., 'SnapController').
* @param key - Storage key (e.g., 'npm:@metamask/example-snap:sourceCode').
* @param value - Data to store (should be 100KB+ for optimal use).
* @param value - JSON data to store (should be 100KB+ for optimal use).
*/
async setItem(namespace: string, key: string, value: unknown): Promise<void> {
async setItem(namespace: string, key: string, value: Json): Promise<void> {
// Adapter handles serialization and wrapping with metadata
await this.#storage.setItem(namespace, key, value);

Expand All @@ -170,16 +172,13 @@ export class StorageService {
}

/**
* Retrieve data from storage.
*
* Returns `unknown` since there's no schema validation.
* Callers should validate or cast the result to the expected type.
* Retrieve JSON data from storage.
*
* @param namespace - Controller namespace (e.g., 'SnapController').
* @param key - Storage key (e.g., 'npm:@metamask/example-snap:sourceCode').
* @returns Parsed data or null if not found. Type is `unknown` - caller must validate.
* @returns Parsed JSON data or null if not found.
*/
async getItem(namespace: string, key: string): Promise<unknown> {
async getItem(namespace: string, key: string): Promise<Json | null> {
// Adapter handles deserialization and unwrapping
return await this.#storage.getItem(namespace, key);
}
Expand Down
14 changes: 7 additions & 7 deletions packages/storage-service/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Messenger } from '@metamask/messenger';
import type { Json } from '@metamask/utils';

import type { StorageServiceMethodActions } from './StorageService-method-action-types';

Expand Down Expand Up @@ -30,12 +31,12 @@ export type StorageAdapter = {
*
* @param namespace - The controller namespace (e.g., 'SnapController').
* @param key - The data key (e.g., 'snap-id:sourceCode').
* @returns The value as a string, or null if not found.
* @returns The JSON value, or null if not found.
*/
getItem(namespace: string, key: string): Promise<unknown>;
getItem(namespace: string, key: string): Promise<Json | null>;

/**
* Store a large value in storage.
* Store a large JSON value in storage.
*
* ⚠️ **Store large values, not many small ones.**
* Each storage operation has I/O overhead. For best performance:
Expand All @@ -44,14 +45,13 @@ export type StorageAdapter = {
*
* Adapter is responsible for:
* - Building the full storage key
* - Wrapping value with metadata (timestamp, etc.)
* - Serializing to string (JSON.stringify)
*
* @param namespace - The controller namespace (e.g., 'SnapController').
* @param key - The data key (e.g., 'snap-id:sourceCode').
* @param value - The value to store (will be wrapped and serialized by adapter).
* @param value - The JSON value to store.
*/
setItem(namespace: string, key: string, value: unknown): Promise<void>;
setItem(namespace: string, key: string, value: Json): Promise<void>;

/**
* Remove an item from storage.
Expand Down Expand Up @@ -138,7 +138,7 @@ export type StorageServiceActions = StorageServiceMethodActions;
*/
export type StorageServiceItemSetEvent = {
type: `${typeof SERVICE_NAME}:itemSet:${string}`;
payload: [key: string, value: unknown];
payload: [key: string, value: Json];
};

/**
Expand Down
1 change: 1 addition & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4920,6 +4920,7 @@ __metadata:
dependencies:
"@metamask/auto-changelog": "npm:^3.4.4"
"@metamask/messenger": "npm:^0.3.0"
"@metamask/utils": "npm:^11.8.1"
"@ts-bridge/cli": "npm:^0.6.4"
"@types/jest": "npm:^27.4.1"
deepmerge: "npm:^4.2.2"
Expand Down
Loading