@frak-labs/frame-connector
Version:
Generic, type-safe RPC communication layer for bidirectional postMessage communication
235 lines (171 loc) • 6.26 kB
Markdown
Modern, type-safe RPC communication layer for cross-window postMessage communication.
This package provides a robust RPC system for communication between the Frak Wallet and SDK clients. It maintains 100% backward compatibility with the existing message format while providing a modern API with promises and async iterators.
- **Type-safe**: Full TypeScript support with schema-based typing
- **Backward compatible**: Uses the existing `{ id, topic, data }` message format
- **Modern API**: Promises for one-shot requests, async iterators for streams
- **Functional**: No classes, just pure functions
- **Secure**: Origin validation and error handling built-in
- **Framework-agnostic**: Works with any transport mechanism
Messages maintain the exact same format for backward compatibility:
```typescript
{
id: string // Unique request identifier
topic: string // Method name (e.g., 'frak_sendInteraction')
data: unknown // Request parameters or response data
}
```
Each RPC method in the schema is annotated with a `ResponseType`:
- `"promise"`: One-shot request that resolves once (e.g., `frak_sendInteraction`)
- `"stream"`: Streaming request that can emit multiple values (e.g., `frak_listenToWalletStatus`)
## Usage
### Client-Side (SDK)
```typescript
import { createRpcClient } from '@frak-labs/frame-connector'
// Create the client
const client = createRpcClient({
transport: window,
targetOrigin: 'https://wallet.frak.id'
})
// Connect before making requests
await client.connect()
// One-shot request (promise-based)
const result = await client.request(
'frak_sendInteraction',
productId,
interaction,
signature
)
// Streaming request (async iterator)
for await (const status of client.stream('frak_listenToWalletStatus')) {
console.log('Wallet status:', status)
if (status.key === 'connected') {
console.log('Connected to wallet:', status.wallet)
}
}
// Cleanup when done
client.cleanup()
```
```typescript
import { createRpcListener } from '@frak-labs/frame-connector'
// Create the listener
const listener = createRpcListener({
transport: window,
allowedOrigins: ['https://example.com', 'https://app.example.com']
})
// Register a promise handler (one-shot)
listener.handle('frak_sendInteraction', async (params, context) => {
const [productId, interaction, signature] = params
// Process the interaction
const hash = await processInteraction(productId, interaction, signature)
return {
status: 'success',
hash
}
})
// Register a stream handler (multiple emissions)
listener.handleStream('frak_listenToWalletStatus', (params, emit, context) => {
// Emit initial status
emit({ key: 'connecting' })
// Set up listener for wallet changes
const unsubscribe = walletState.subscribe((state) => {
if (state.connected) {
emit({ key: 'connected', wallet: state.address })
} else {
emit({ key: 'not-connected' })
}
})
// Return cleanup function (optional)
return () => unsubscribe()
})
// Cleanup when done
listener.cleanup()
```
Creates an RPC client for SDK-side communication.
**Config:**
- `transport: RpcTransport` - The transport mechanism (e.g., `window`)
- `targetOrigin: string` - Target origin for postMessage
- `handshake?: HandshakeConfig` - Optional handshake configuration
**Returns:**
- `connect(): Promise<void>` - Establish connection
- `request(method, ...params): Promise<T>` - Make one-shot request
- `stream(method, ...params): AsyncIterableIterator<T>` - Make streaming request
- `getState(): ConnectionState` - Get current connection state
- `cleanup(): void` - Clean up resources
### Listener
#### `createRpcListener(config: RpcListenerConfig): RpcListener`
Creates an RPC listener for Wallet-side communication.
**Config:**
- `transport: RpcTransport` - The transport mechanism (e.g., `window`)
- `allowedOrigins: string | string[]` - Allowed origins for security
**Returns:**
- `handle(method, handler): void` - Register promise handler
- `handleStream(method, handler): void` - Register stream handler
- `unregister(method): void` - Unregister a handler
- `cleanup(): void` - Clean up resources
## Type Safety
The package provides full type safety through the RPC schema:
```typescript
// Method names are type-checked
client.request('frak_sendInteraction', ...) // ✓ Valid
client.request('invalid_method', ...) // ✗ Type error
// Parameters are type-checked
client.request('frak_sendInteraction', productId, interaction) // ✓ Valid
client.request('frak_sendInteraction', 'wrong-params') // ✗ Type error
// Return types are inferred
const result = await client.request('frak_sendInteraction', ...)
// result is typed as SendInteractionReturnType
```
Errors are thrown as `FrakRpcError` with standard error codes:
```typescript
import { RpcErrorCodes } from '@frak-labs/frame-connector'
try {
const result = await client.request('frak_sendInteraction', ...)
} catch (error) {
if (error.code === RpcErrorCodes.userRejected) {
console.log('User rejected the request')
} else if (error.code === RpcErrorCodes.invalidParams) {
console.error('Invalid parameters:', error.message)
}
}
```
This package is part of the Frak Wallet monorepo and uses Bun as the package manager.
```bash
bun install
bun run typecheck
bun run format
```
For existing code using the old callback-based API, migration to the new API is straightforward:
```typescript
// Old SDK client
await transport.listenerRequest(
{ method: 'frak_listenToWalletStatus' },
(status) => {
console.log('Status:', status)
}
)
```
```typescript
// New RPC client
for await (const status of client.stream('frak_listenToWalletStatus')) {
console.log('Status:', status)
}
```
The underlying message format remains unchanged, ensuring 100% backward compatibility.