Overview
SubWallet Extension’s storage layer provides a wrapper around Chrome’s chrome.storage.local API with error handling, prefixing, and type safety. All persistent data is stored using this system.
Storage Interface
The storage system is built on Chrome’s storage API:
chrome.storage.local.get(keys, callback);
chrome.storage.local.set(items, callback);
chrome.storage.local.remove(keys, callback);
Storage Constants
Common storage keys are defined as constants:
// Language settings
export const LANGUAGE = 'current-language';
export const DEFAULT_LANGUAGE = 'en';
// Currency settings
export const CURRENCY = 'current-currency';
// User preferences
export const REMIND_EXPORT_ACCOUNT = 'remind_export_account';
// Session tracking
export const LATEST_SESSION = 'general.latest-session';
// Migration flags
export const UPGRADE_DUPLICATE_ACCOUNT_NAME = 'general.upgrade-duplicate-account-name';
Storage Data Interface
export interface StorageDataInterface {
key: string;
value: unknown;
}
Serializable value to store
BaseStore Implementation
The BaseStore class provides the foundational storage operations.
Constructor
class BaseStore<T> {
constructor(prefix: string | null);
}
Storage key prefix for namespacing. Pass null for no prefix.
Example:
const store = new BaseStore('myapp');
// Keys will be prefixed: "myapp:key-name"
Storage Operations
get
Retrieve a single value from storage.
get(key: string, update: (value: T) => void): void
Storage key without prefix
update
(value: T) => void
required
Callback invoked with the retrieved value
Example:
store.get('user-settings', (settings) => {
console.log('Settings:', settings);
});
Implementation:
public get(_key: string, update: (value: T) => void): void {
const key = `${this.#prefix}${_key}`;
chrome.storage.local.get([key], (result: StoreValue): void => {
// Check for errors
const error = chrome.runtime.lastError;
if (error) {
console.error(`BaseStore.get:: runtime.lastError:`, error);
}
update(result[key] as T);
});
}
set
Store a value in Chrome storage.
set(key: string, value: T, update?: () => void): void
Storage key without prefix
Value to store (must be JSON-serializable)
Optional callback invoked after storage completes
Example:
store.set('user-settings', {
theme: 'dark',
language: 'en'
}, () => {
console.log('Settings saved!');
});
Implementation:
public set(_key: string, value: T, update?: () => void): void {
const key = `${this.#prefix}${_key}`;
chrome.storage.local.set({ [key]: value }, (): void => {
const error = chrome.runtime.lastError;
if (error) {
console.error(`BaseStore.set:: runtime.lastError:`, error);
}
update && update();
});
}
remove
Delete a value from storage.
remove(key: string, update?: () => void): void
Optional callback after removal
Example:
store.remove('user-settings', () => {
console.log('Settings removed!');
});
Implementation:
public remove(_key: string, update?: () => void): void {
const key = `${this.#prefix}${_key}`;
chrome.storage.local.remove(key, (): void => {
const error = chrome.runtime.lastError;
if (error) {
console.error(`BaseStore.remove:: runtime.lastError:`, error);
}
update && update();
});
}
all
Iterate over all prefixed storage entries.
all(update: (key: string, value: T) => void): void
update
(key: string, value: T) => void
required
Callback invoked for each key-value pair
Example:
store.all((key, value) => {
console.log(`${key}:`, value);
});
Implementation:
public all(update: (key: string, value: T) => void): void {
this.allMap((map): void => {
Object.entries(map).forEach(([key, value]): void => {
update(key, value);
});
});
}
allMap
Retrieve all prefixed entries as a single object.
allMap(update: (value: Record<string, T>) => void): void
update
(value: Record<string, T>) => void
required
Callback receiving all entries as an object
Example:
store.allMap((allData) => {
console.log('All entries:', allData);
});
Implementation:
public allMap(update: (value: Record<string, T>) => void): void {
chrome.storage.local.get(null, (result: StoreValue): void => {
const error = chrome.runtime.lastError;
if (error) {
console.error(`BaseStore.all:: runtime.lastError:`, error);
}
const entries = Object.entries(result);
const map: Record<string, T> = {};
for (let i = 0; i < entries.length; i++) {
const [key, value] = entries[i];
// Only include keys with our prefix
if (key.startsWith(this.#prefix)) {
map[key.replace(this.#prefix, '')] = value as T;
}
}
update(map);
});
}
getPrefix
Get the storage prefix for this store.
Returns: The prefix string
Example:
const prefix = store.getPrefix();
console.log('Prefix:', prefix); // "subwallet-accounts:"
Storage Prefixes
SubWallet uses prefixes to namespace different types of data:
import { EXTENSION_PREFIX } from '@subwallet/extension-base/defaults';
// EXTENSION_PREFIX = 'subwallet-'
// Example prefixed keys:
const accountsPrefix = `${EXTENSION_PREFIX}accounts`; // "subwallet-accounts"
const settingsPrefix = `${EXTENSION_PREFIX}settings`; // "subwallet-settings"
const currentAccountPrefix = `${EXTENSION_PREFIX}current_account`; // "subwallet-current_account"
Error Handling
All storage operations check for Chrome runtime errors:
const lastError = (type: string): void => {
const error = chrome.runtime.lastError;
if (error) {
console.error(`BaseStore.${type}:: runtime.lastError:`, error);
}
};
Common errors:
- QUOTA_BYTES_PER_ITEM exceeded: Item size exceeds 8KB (for sync storage)
- QUOTA_BYTES exceeded: Total storage exceeds 5MB (local) or 100KB (sync)
- Extension context invalidated: Extension was reloaded/disabled
Storage Types
Chrome provides multiple storage areas:
chrome.storage.local
Used by SubWallet for most data.
- Size limit: 5MB (can request unlimited)
- Synced: No
- Use case: All extension data
chrome.storage.sync
Not currently used by SubWallet.
- Size limit: 100KB total, 8KB per item
- Synced: Yes, across Chrome instances
- Use case: User preferences (if implemented)
chrome.storage.session
Not currently used by SubWallet.
- Size limit: 10MB
- Persisted: Only during browser session
- Use case: Temporary data
Best Practices
1. Use Prefixes
Always use prefixes to avoid key collisions:
// Good
const store = new BaseStore('myfeature');
// Bad - could conflict with other stores
const store = new BaseStore(null);
2. Handle Undefined Values
Storage may return undefined for missing keys:
store.get('key', (value) => {
if (value === undefined) {
console.log('Key not found, using default');
value = getDefaultValue();
}
});
3. Serialize Complex Objects
Chrome storage only supports JSON-serializable values:
// Good
store.set('data', {
timestamp: Date.now(),
values: [1, 2, 3]
});
// Bad - functions, symbols, etc. won't be stored
store.set('data', {
callback: () => console.log('test'),
symbol: Symbol('test')
});
4. Batch Operations
Use allMap for reading multiple items efficiently:
// Good - single storage read
store.allMap((allData) => {
processMultipleItems(allData);
});
// Bad - multiple storage reads
store.get('item1', (val1) => {
store.get('item2', (val2) => {
store.get('item3', (val3) => {
// callback hell + inefficient
});
});
});
5. Monitor Storage Quota
chrome.storage.local.getBytesInUse(null, (bytesInUse) => {
const quotaBytes = chrome.storage.local.QUOTA_BYTES;
const percentUsed = (bytesInUse / quotaBytes) * 100;
console.log(`Storage: ${percentUsed.toFixed(2)}% used`);
if (percentUsed > 80) {
console.warn('Storage almost full!');
}
});
Storage Migration
When changing storage schema:
function migrateStorage() {
store.get('old-key', (oldValue) => {
if (oldValue !== undefined) {
// Transform old data to new format
const newValue = transformData(oldValue);
// Save in new format
store.set('new-key', newValue, () => {
// Remove old key
store.remove('old-key');
});
}
});
}
Debugging Storage
- Open Chrome DevTools
- Go to Application tab
- Expand Storage → Extension
- Select your extension
- View all stored key-value pairs
Clear Storage
// Clear all extension storage
chrome.storage.local.clear(() => {
console.log('Storage cleared');
});
// Clear specific store
store.allMap((allData) => {
Object.keys(allData).forEach(key => {
store.remove(key);
});
});
- Minimize storage writes: Batch updates when possible
- Use appropriate prefixes: Helps filter data efficiently
- Cache frequently accessed data: Don’t read from storage on every access
- Clean up old data: Remove obsolete entries periodically
// Cache example
let cachedSettings = null;
function getSettings(callback) {
if (cachedSettings) {
callback(cachedSettings);
} else {
store.get('settings', (settings) => {
cachedSettings = settings;
callback(settings);
});
}
}