Overview
The messaging system enables communication between the extension’s UI layer and background service. It uses Chrome’s runtime messaging API with a port-based connection that supports both one-time requests and long-lived subscriptions.
Core Architecture
Message Types
Messages are strongly typed using TypeScript discriminated unions:
export type MessageTypes = keyof RequestSignatures;
export type MessageTypesWithSubscriptions = keyof SubscriptionMessageTypes;
export type MessageTypesWithNoSubscriptions = Exclude<MessageTypes, keyof SubscriptionMessageTypes>;
export type MessageTypesWithNullRequest = NullKeys<RequestTypes>;
Message Structure
Unique identifier generated for each message
The message type identifier (e.g., 'pri(accounts.create.suriV2)')
request
RequestTypes[TMessageType]
required
The request payload specific to the message type
origin
'page' | 'extension' | string
required
Origin of the message sender
Core Functions
sendMessage
Send a message to the background service and receive a response.
export function sendMessage<TMessageType extends MessageTypes>(
message: TMessageType,
request?: RequestTypes[TMessageType],
subscriber?: (data: unknown) => void
): Promise<ResponseTypes[TMessageType]>
The message type identifier
request
RequestTypes[TMessageType]
Request payload (optional for null-request messages)
Callback function for subscription-based messages
Returns: Promise<ResponseTypes[TMessageType]>
Example: Simple Request
import { sendMessage } from '@subwallet/extension-koni-ui/messaging/base';
// Ping the background service
const response = await sendMessage('pri(ping)', null);
Example: Request with Payload
// Create account with seed phrase
const result = await sendMessage('pri(accounts.create.suriV2)', {
name: 'My Account',
password: 'secure-password',
suri: 'seed phrase words...',
types: ['sr25519']
});
Example: Subscription
// Subscribe to notifications
await sendMessage(
'pri(notifications.subscribe)',
null,
(notifications) => {
console.log('Received notifications:', notifications);
}
);
subscribeMessage
Create a subscription that starts immediately and returns an unsubscribe function.
export function subscribeMessage<TMessageType extends MessageTypesWithSubscriptions>(
message: TMessageType,
request: RequestTypes[TMessageType],
callback: (data: ResponseTypes[TMessageType]) => void,
subscriber: (data: SubscriptionMessageTypes[TMessageType]) => void
): {
promise: Promise<ResponseTypes[TMessageType]>,
unsub: () => void
}
The subscription message type
request
RequestTypes[TMessageType]
required
Request payload
Called once when subscription is established
Called for each subscription update
Returns: Object with promise and unsub function
Example
import { subscribeMessage } from '@subwallet/extension-koni-ui/messaging/base';
const { promise, unsub } = subscribeMessage(
'pri(transaction.history.subscribe)',
{ address: '0x...', chain: 'polkadot' },
(initial) => console.log('Initial data:', initial),
(update) => console.log('Update:', update)
);
// Later, unsubscribe
unsub();
lazySubscribeMessage
Create a subscription that can be started manually.
export function lazySubscribeMessage<TMessageType extends MessageTypesWithSubscriptions>(
message: TMessageType,
request: RequestTypes[TMessageType],
callback: (data: ResponseTypes[TMessageType]) => void,
subscriber: (data: SubscriptionMessageTypes[TMessageType]) => void
): {
promise: Promise<ResponseTypes[TMessageType]>,
start: () => void,
unsub: () => void
}
Returns: Object with promise, start, and unsub functions
Example
const { promise, start, unsub } = lazySubscribeMessage(
'pri(price.subscribeCurrentTokenPrice)',
'bitcoin-price-id',
(initial) => console.log('Initial price:', initial),
(update) => console.log('Price update:', update)
);
// Start subscription when needed
start();
// Stop when done
unsub();
Message Categories
Account Messages
// Create account from seed/private key
await sendMessage('pri(accounts.create.suriV2)', request);
// Export account as JSON
await sendMessage('pri(accounts.export.json)', { address, password });
// Save current account
await sendMessage('pri(accounts.saveCurrentProxy)', { address });
Settings Messages
// Subscribe to settings changes
await sendMessage('pri(settings.subscribe)', data, callback);
// Save theme preference
await sendMessage('pri(settings.saveTheme)', 'dark');
// Toggle balance visibility
await sendMessage('pri(settings.changeBalancesVisibility)', null);
Transaction Messages
// Get transaction details
await sendMessage('pri(transactions.getOne)', { hash, chain });
// Subscribe to transaction history
await sendMessage(
'pri(transaction.history.subscribe)',
{ address, chain },
callback
);
Price & Chart Messages
// Subscribe to current token price
await sendMessage('pri(price.subscribeCurrentTokenPrice)', priceId, callback);
// Get historical price data
await sendMessage('pri(price.getHistory)', { priceId, timeframe });
// Check if chart is available
await sendMessage('pri(price.checkCoinGeckoPriceSupport)', priceId);
Port Connection
The messaging system maintains a persistent connection to the background service:
const PORT_EXTENSION = 'subwallet-extension';
let port: chrome.runtime.Port;
port = chrome.runtime.connect({ name: PORT_EXTENSION });
Auto-Reconnection
The port automatically reconnects if the connection is lost:
port.onDisconnect.addListener(() => {
const err = chrome.runtime.lastError;
if (err) {
console.warn(`${err.message}, Reconnecting to the port.`);
setTimeout(onConnectPort, 1000);
}
});
Message Handler
Internal handler structure for managing pending requests:
interface Handler {
resolve: (data: any) => void;
reject: (error: Error) => void;
subscriber?: (data: any) => void;
}
type Handlers = Record<string, Handler>;
Handlers are stored by message ID and automatically cleaned up after one-time requests.
Error Handling
port.onMessage.addListener((data) => {
const handler = handlers[data.id];
if (!handler) {
console.error(`Unknown response: ${JSON.stringify(data)}`);
return;
}
if (data.error) {
handler.reject(new Error(data.error));
} else {
handler.resolve(data.response);
}
});
Best Practices
-
Use typed messages: Always use the predefined message type constants to ensure type safety
-
Handle errors: Wrap
sendMessage calls in try-catch blocks
try {
const result = await sendMessage('pri(accounts.create.suriV2)', request);
} catch (error) {
console.error('Account creation failed:', error);
}
- Cleanup subscriptions: Always call
unsub() when subscriptions are no longer needed
useEffect(() => {
const { unsub } = subscribeMessage(...);
return () => unsub();
}, []);
- Batch related operations: Combine related requests to reduce message overhead
Common Patterns
Request-Response Pattern
// One-time request
const data = await sendMessage('pri(someMessage)', request);
Subscribe-Update Pattern
// Long-lived subscription
const { unsub } = subscribeMessage(
'pri(someMessage.subscribe)',
request,
(initial) => setState(initial),
(update) => setState(update)
);
Lazy Initialization Pattern
// Create subscription but start later
const { start, unsub } = lazySubscribeMessage(...);
// Start when component mounts
useEffect(() => {
start();
return () => unsub();
}, []);