Skip to main content

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

id
string
required
Unique identifier generated for each message
message
MessageType
required
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]>
message
MessageType
required
The message type identifier
request
RequestTypes[TMessageType]
Request payload (optional for null-request messages)
subscriber
function
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
}
message
MessageType
required
The subscription message type
request
RequestTypes[TMessageType]
required
Request payload
callback
function
required
Called once when subscription is established
subscriber
function
required
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

  1. Use typed messages: Always use the predefined message type constants to ensure type safety
  2. 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);
}
  1. Cleanup subscriptions: Always call unsub() when subscriptions are no longer needed
useEffect(() => {
  const { unsub } = subscribeMessage(...);
  return () => unsub();
}, []);
  1. 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();
}, []);

Build docs developers (and LLMs) love