Documentation Index
Fetch the complete documentation index at: https://mintlify.com/Koniverse/SubWallet-Extension/llms.txt
Use this file to discover all available pages before exploring further.
What are Stores?
Stores are data persistence layers in SubWallet Extension that handle saving and retrieving data from Chrome’s local storage. They provide a clean abstraction over the browser storage API and enable reactive data patterns through RxJS subjects.
Stores are essential for:
- Persisting user data across browser sessions
- Reactive data flow through observable patterns
- Type-safe storage with TypeScript interfaces
- Namespaced data to avoid key conflicts
Store Architecture
SubWallet implements two types of stores:
BaseStore
The foundation class that provides basic CRUD operations with Chrome storage:
export default abstract class BaseStore <T> {
#prefix: string;
constructor (prefix: string | null) {
this.#prefix = prefix ? `${prefix}:` : '';
}
public get (_key: string, update: (value: T) => void): void
public set (_key: string, value: T, update?: () => void): void
public remove (_key: string, update?: () => void): void
public all (update: (key: string, value: T) => void): void
public allMap (update: (value: Record<string, T>) => void): void
}
Key Features:
- Prefix-based namespacing to avoid storage key collisions
- Callback-based async operations
- Automatic error handling via
chrome.runtime.lastError
SubscribableStore
Extends BaseStore with reactive capabilities using RxJS:
export default abstract class SubscribableStore<T> extends BaseStore<T> {
private readonly subject: Subject<T> = new Subject<T>();
public getSubject (): Subject<T> {
return this.subject;
}
public override set (_key: string, value: T, update?: () => void): void {
super.set(_key, value, () => {
this.subject.next(value);
update && update();
});
}
public asyncGet = async (key: string): Promise<T> => {
return new Promise((resolve) => {
this.get(key, resolve);
});
};
}
Key Features:
- RxJS Subject emits on every data change
- Promise-based
asyncGet for modern async/await syntax
- Automatic notification to all subscribers when data changes
Creating a New Store
Step 1: Define Your Store Class
Create a new file in packages/extension-base/src/stores/:
// CurrentAccountStore.ts
import { EXTENSION_PREFIX } from '@subwallet/extension-base/defaults';
import SubscribableStore from '@subwallet/extension-base/stores/SubscribableStore';
import { CurrentAccountInfo } from '@subwallet/extension-base/types';
export default class CurrentAccountStore extends SubscribableStore<CurrentAccountInfo> {
constructor () {
super(EXTENSION_PREFIX ? `${EXTENSION_PREFIX}current_account` : null);
}
}
Important:
- Use
SubscribableStore if you need reactive updates
- Use
BaseStore for simple key-value storage
- Always prefix with
EXTENSION_PREFIX for namespace isolation
- Use descriptive, unique keys for your store
Step 2: Define the Data Type
Ensure your data type is defined in the appropriate types file:
// background/KoniTypes.ts
export interface CurrentAccountInfo {
address: string;
currentAccountType: AccountType;
proxyId?: string;
}
Step 3: Integrate into KoniState
Add your store to the state management class:
// koni/background/handlers/State.ts
export default class KoniState extends State {
private readonly currentAccountStore = new CurrentAccountStore();
private currentAccountReady = false;
// Setter method
public setCurrentAccount (accountInfo: CurrentAccountInfo, callback?: () => void): void {
this.currentAccountStore.set('CurrentAccount', accountInfo, () => {
this.eventService.emit('account.updateCurrent', accountInfo);
callback && callback();
});
}
// Getter method
public getCurrentAccount (update: (value: CurrentAccountInfo) => void): void {
if (!this.currentAccountReady) {
this.currentAccountStore.get('CurrentAccount', (value) => {
this.currentAccountReady = true;
update(value);
});
} else {
this.currentAccountStore.get('CurrentAccount', update);
}
}
// Subscribe method for reactive updates
public subscribeCurrentAccount () {
return this.currentAccountStore.getSubject();
}
}
Step 4: Export Your Store
Add your store to the index file:
// stores/index.ts
export { default as CurrentAccountStore } from './CurrentAccountStore';
Real-World Example: SettingsStore
Here’s how the SettingsStore is implemented:
import { RequestSettingsType } from '@subwallet/extension-base/background/KoniTypes';
import { EXTENSION_PREFIX } from '@subwallet/extension-base/defaults';
import SubscribableStore from '@subwallet/extension-base/stores/SubscribableStore';
export default class SettingsStore extends SubscribableStore<RequestSettingsType> {
constructor () {
super(EXTENSION_PREFIX ? `${EXTENSION_PREFIX}settings` : null);
}
}
Usage in KoniState:
private readonly settingsStore = new SettingsStore();
public getSettings (update: (value: RequestSettingsType) => void): void {
this.settingsStore.get('Settings', update);
}
public setSettings (settings: RequestSettingsType, callback?: () => void): void {
this.settingsStore.set('Settings', settings, callback);
}
public subscribeSettings () {
return this.settingsStore.getSubject();
}
Best Practices
1. Choose the Right Base Class
- Use
SubscribableStore when:
- UI needs real-time updates
- Multiple components depend on the data
- Data changes frequently
- Use
BaseStore when:
- Data is read once at startup
- Simple configuration storage
- No need for reactive updates
2. Storage Keys
// Good: Descriptive and unique
super(EXTENSION_PREFIX ? `${EXTENSION_PREFIX}current_account` : null);
// Bad: Too generic, potential conflicts
super(EXTENSION_PREFIX ? `${EXTENSION_PREFIX}data` : null);
3. Type Safety
Always define proper TypeScript interfaces:
// Good
export interface PriceJson {
currency: CurrencyType;
priceMap: Record<string, number>;
ready: boolean;
}
class PriceStore extends SubscribableStore<PriceJson> {}
// Bad
class PriceStore extends SubscribableStore<any> {}
4. Ready State Pattern
Implement ready flags to prevent redundant storage reads:
private priceStoreReady = false;
public getPrice (update: (value: PriceJson) => void): void {
if (!this.priceStoreReady) {
this.priceStore.get('Price', (value) => {
this.priceStoreReady = true;
update(value);
});
} else {
this.priceStore.get('Price', update);
}
}
5. Event Emission
Emit events when data changes to trigger dependent systems:
public setPrice (priceData: PriceJson, callback?: () => void): void {
this.priceStore.set('Price', priceData, () => {
this.eventService.emit('price.updated', priceData);
callback && callback();
});
}
6. Error Handling
BaseStore automatically logs errors, but add application-level handling:
public async loadSettings (): Promise<RequestSettingsType> {
try {
return await this.settingsStore.asyncGet('Settings');
} catch (error) {
console.error('Failed to load settings:', error);
return DEFAULT_SETTINGS; // Fallback
}
}
Common Patterns
Pattern 1: Cache with Store Backup
private cachedData: PriceJson | null = null;
public async getPrice (): Promise<PriceJson> {
if (this.cachedData) {
return this.cachedData;
}
this.cachedData = await this.priceStore.asyncGet('Price');
return this.cachedData;
}
Pattern 2: Subscribe with Initial Value
public subscribePrice (callback: (value: PriceJson) => void) {
// Send current value immediately
this.getPrice(callback);
// Subscribe to future changes
return this.priceStore.getSubject().subscribe(callback);
}
Pattern 3: Batch Updates
public async updateMultipleSettings (updates: Partial<RequestSettingsType>) {
const current = await this.settingsStore.asyncGet('Settings');
const merged = { ...current, ...updates };
return new Promise<void>((resolve) => {
this.settingsStore.set('Settings', merged, resolve);
});
}
Debugging Stores
View Storage Contents
In Chrome DevTools:
- Open Developer Tools > Application > Local Storage
- Look for keys prefixed with your
EXTENSION_PREFIX
- Inspect values (stored as JSON)
Subscribe to Changes
// In console or debugging code
this.priceStore.getSubject().subscribe((value) => {
console.log('Price updated:', value);
});
Common Issues
Issue: Data not persisting
// Wrong: Callback not executed
this.store.set('key', value); // No callback
// Right: Wait for persistence
this.store.set('key', value, () => console.log('Saved'));
Issue: Subject not emitting
// Wrong: Using BaseStore instead of SubscribableStore
class MyStore extends BaseStore<T> {}
// Right: Use SubscribableStore for reactive updates
class MyStore extends SubscribableStore<T> {}
Store Lifecycle
- Services - Use stores for service data persistence
- APIs - Store API responses for caching
- Cron Jobs - Periodically update store data