@noego/wire
v0.0.1
Published
Relay client for multi-device messaging
Downloads
58
Readme
@noego/wire
Relay client for multi-device messaging via the Beacon protocol. Handles pairing, SSE streaming, reconnection, dedup, and heartbeat monitoring.
Zero runtime dependencies — uses built-in fetch and manual SSE parsing.
Quick Start
import { WireClient } from '@noego/wire';
// Register a new device (static — before you have a connection)
const registration = await WireClient.register('http://localhost:3080', {
deviceName: 'My Desktop',
deviceType: 'desktop',
});
// → { userId, deviceId, authToken, pairingCode, expiresAt }
// Claim a pairing code from another device
const claim = await WireClient.claim('http://localhost:3080', {
pairingCode: 'ABC123',
deviceName: 'My Phone',
});
// → { userId, deviceId, authToken, desktopDeviceId, desktopDeviceName }
// Create a connected client
const wire = new WireClient({
beaconUrl: 'http://localhost:3080',
deviceId: registration.deviceId,
authToken: registration.authToken,
});
await wire.connect();
// Send messages
await wire.send({
target: { kind: 'device', deviceId: 'dev_abc123' },
type: 'chat.send-message',
payload: { content: 'Hello' },
});
// Receive messages
wire.messages$.subscribe((msg) => {
console.log(msg.type, msg.payload);
});
// Or use the event-based API
wire.on('message', (msg) => { /* ... */ });
// Refresh the session without losing registered handlers
await wire.reconnect();
// Cleanup
wire.close();API
Static Methods
| Method | Purpose |
|--------|---------|
| WireClient.register(beaconUrl, opts) | Register a new device, get credentials + pairing code |
| WireClient.claim(beaconUrl, opts) | Claim a pairing code, get credentials for paired device |
Instance Methods
| Method | Purpose |
|--------|---------|
| connect() | Create session + open SSE stream |
| reconnect() | Refresh the session/stream on the same instance while preserving handlers |
| send(input) | Send a message, get acknowledgement |
| request(input, timeoutMs?) | Send + wait for correlated response |
| on(event, handler) | Subscribe to events — returns unsubscribe function |
| close() | Close connection, cancel pending requests |
Properties
| Property | Type | Purpose |
|----------|------|---------|
| isConnected | boolean | Connection state |
| currentSessionId | string \| null | Active session ID |
| messages$ | { subscribe(handler): { unsubscribe() } } | Observable-compatible message stream |
Events
| Event | Payload | When |
|-------|---------|------|
| open | — | SSE connection established |
| message | RelayMessage | Message received |
| close | — | Connection closed |
| error | Error | Connection error |
| reconnecting | — | Attempting reconnect |
Protocol Features
- Reconnection: Exponential backoff (500ms → 5s) on connection loss
- Heartbeat: Monitors server keepalive, reconnects on timeout (2.5× heartbeat interval)
- Dedup: Client-side by
messageId— monotonic, persisted cursor - Stream ordering: Validates
streamSeqis strictly increasing, reconnects on violation - Request/response: Correlation via
requestIdwith configurable timeout
Reconnect Behavior
Event handlers registered with on() and messages$ live on the WireClient instance.
- Reusing the same instance preserves the listener registry.
- Creating a new
WireClient(...)creates a new, empty listener registry. - For routine transport recovery, call
wire.reconnect(). - If you intentionally called
wire.close(), you can callwire.connect()again on the same instance.
Wrapper services should keep a single long-lived WireClient and reconnect that instance instead of replacing it:
class RealtimeSocket {
private readonly wire = new WireClient(config);
async connect(): Promise<void> {
await this.wire.connect();
}
async recoverTransport(): Promise<void> {
await this.wire.reconnect();
}
}Testing
npm testIntegration tests boot a real @noego/beacon server in-process — two wires exchange messages over real HTTP + SSE.
