@sitnikov/connection-broker
v0.1.1
Published
Generic connection broker for routing messages between clients
Readme
@sitnikov/connection-broker
Generic connection broker that enables ANY two parties to communicate when they can't reach each other directly.
Overview
The connection broker is a completely reusable, domain-agnostic WebSocket server that routes messages between clients. It knows nothing about browsers, MCP, or any specific use case - it just manages connections, channels, and message routing.
Key Concepts
- Connections: Clients connect to the broker and register with a "role" (arbitrary string)
- Roles: Used to discover connections (e.g., list all connections with role "browser-tab")
- Channels: Ephemeral pseudo-connections between two clients
- Messages: Opaque payloads routed between channels
Protocol Flow
- Client connects → declares a role → gets assigned a UUID
- Any client can list all connections by role (returns UUIDs only)
- Any client can open a channel (pseudo-connection) to another client by UUID
- Both ends of channel can send messages to each other (with confirmation)
- Either end can close the channel
ID Types
- Connection IDs: UUID (persistent per client)
- Channel IDs: UUID (persistent per channel)
- Message IDs: Auto-increment integers (unique per message, used for request/response tracking)
Installation
npm install @sitnikov/connection-brokerUsage
Starting the Broker Server
connection-broker --port 3004Or programmatically:
import { ConnectionBroker } from '@sitnikov/connection-broker'
const broker = new ConnectionBroker(3004)
// Broker is now running on port 3004Using the Client SDK
import { BrokerClient } from '@sitnikov/connection-broker/client'
// Connect to broker with a role
const client = new BrokerClient('ws://localhost:3004')
await client.connect('my-role')
console.log('My ID:', client.getMyId())
// Optional: Pass a logger for debugging
import { Logger } from './Logger'
const logger = new Logger('/path/to/log.txt')
const clientWithLogger = new BrokerClient('ws://localhost:3004', logger)
await clientWithLogger.connect('my-role')
// Discover connections by role
const ids = await client.listByRole('other-role')
console.log('Found connections:', ids)
// Open a channel to a specific connection
const channel = await client.openChannel(targetId)
// Send messages (returns promise that resolves when broker confirms delivery)
await channel.send({ type: 'greeting', message: 'Hello!' })
// Listen for messages
channel.onMessage = (payload) => {
console.log('Received:', payload)
channel.send({ type: 'response', message: 'Got it!' })
}
// Listen for channel close
channel.onClosed = () => {
console.log('Channel closed')
}
// Close the channel
channel.close()
// Listen for incoming channels
client.onIncomingChannel = (channel) => {
console.log('Incoming channel from:', channel.getPeerId())
channel.onMessage = (payload) => {
console.log('Received:', payload)
}
}
// Disconnect from broker
client.disconnect()Example Use Cases
Browser Automation
- Browser tabs connect with role
"browser-tab" - MCP server connects with role
"mcp-server" - MCP server lists tabs, opens channels, sends commands
- Tabs execute commands and send back results
Chat Application
- Users connect with role
"user" - Server connects with role
"chat-server" - Server can list all users and route messages between them
Remote Procedure Calls
- Service A connects with role
"service-a" - Service B connects with role
"service-b" - Services can discover each other and exchange RPC requests/responses
Protocol Reference
All messages include:
id: Auto-increment integer (unique message ID)replyTo: Integer (optional, references request message ID for responses)
Client → Broker Messages
Register
{
"type": "register",
"id": 1,
"role": "my-role"
}Response (success):
{
"type": "registered",
"id": 2,
"replyTo": 1,
"connectionId": "connection-uuid"
}Response (failure):
{
"type": "register_failed",
"id": 2,
"replyTo": 1,
"error": "Error message"
}List by Role
{
"type": "list_by_role",
"id": 3,
"role": "other-role"
}Response (success):
{
"type": "connections",
"id": 4,
"replyTo": 3,
"ids": ["uuid-1", "uuid-2", "uuid-3"]
}Response (failure):
{
"type": "list_by_role_failed",
"id": 4,
"replyTo": 3,
"error": "Error message"
}Open Channel
{
"type": "open",
"id": 5,
"targetId": "connection-uuid"
}Response to opener (success):
{
"type": "channel_opened",
"id": 6,
"replyTo": 5,
"channelId": "channel-uuid",
"targetId": "connection-uuid"
}Notification to target (unsolicited):
{
"type": "incoming_channel",
"id": 7,
"from": "connection-uuid",
"channelId": "channel-uuid"
}Response (failure):
{
"type": "channel_open_failed",
"id": 6,
"replyTo": 5,
"targetId": "connection-uuid",
"error": "Target connection not found"
}Send Message on Channel
{
"type": "message",
"id": 8,
"channelId": "channel-uuid",
"payload": { "any": "data" }
}Notification to recipient (unsolicited):
{
"type": "channel_message",
"id": 9,
"channelId": "channel-uuid",
"payload": { "any": "data" }
}Response to sender (success):
{
"type": "channel_message_sent",
"id": 10,
"replyTo": 8,
"channelId": "channel-uuid"
}Response (failure):
{
"type": "channel_message_failed",
"id": 10,
"replyTo": 8,
"channelId": "channel-uuid",
"error": "Channel not found"
}Close Channel
{
"type": "close",
"id": 11,
"channelId": "channel-uuid"
}Response to closer (success):
{
"type": "channel_closed",
"id": 12,
"replyTo": 11,
"channelId": "channel-uuid"
}Notification to other party (unsolicited):
{
"type": "channel_closed_notification",
"id": 13,
"channelId": "channel-uuid"
}Response (failure):
{
"type": "channel_close_failed",
"id": 12,
"replyTo": 11,
"channelId": "channel-uuid",
"error": "Channel not found"
}Message Flow Patterns
Request-Response (expects reply):
register→registeredorregister_failedlist_by_role→connectionsorlist_by_role_failedopen→channel_openedorchannel_open_failedmessage→channel_message_sentorchannel_message_failedclose→channel_closedorchannel_close_failed
Unsolicited Notifications (no replyTo):
incoming_channel- Notifies target about new channelchannel_message- Notifies recipient about messagechannel_closed_notification- Notifies other party about closure
Architecture
The broker is designed with these principles:
- Role-agnostic: Roles are just strings, no validation or special handling
- Payload-agnostic: Messages are opaque, broker doesn't inspect them
- Stateless channels: No persistent state beyond active connections and channels
- Simple ID system:
- Connection IDs & Channel IDs: UUIDs (persistent identifiers)
- Message IDs: Auto-increment integers (for request/response tracking)
- Generic routing: Routes messages between parties without understanding content
- Request-Response Pattern: All operations (register, list, open, send, close) get explicit success or failure responses
- Unsolicited Notifications: Events like incoming channels, messages, and closures are pushed to clients without replyTo
License
MIT
