smokesigns
v0.2.2
Published
WebRTC and dweb connectivity library for establishing peer-to-peer connections via Autonomi network
Maintainers
Readme
smokesigns
WebRTC and dweb connectivity library for establishing peer-to-peer connections via Autonomi network or handshake servers.
Features
- WebRTC peer-to-peer connections with automatic offer/answer exchange
- Multiple signaling backends:
- DwebConnector: Uses Autonomi network's public scratchpad
- HandshakeserverConnector: Uses a simple handshake server (e.g., handshake.autonomi.space)
- Ordered, reliable data channels
- TypeScript support
- Automatic ICE candidate gathering with timeout
- STUN-only configuration (no TURN servers required)
- Presence heartbeat with last-seen detection (minimal polling, automatic reconnection)
Why "smokesigns"?
Just like smoke signals in the past allowed people to communicate over long distances without relying on external infrastructure or service providers, smokesigns enables direct peer-to-peer communication without depending on centralized servers. It's just you, your communication partner, and your "fires" (devices)! 🔥
The library provides the digital equivalent of smoke signals - simple, direct, and decentralized communication.
Installation
npm install smokesignsUsage
Using HandshakeserverConnector
The HandshakeserverConnector uses a simple REST API server for signaling. Both peers need to agree on two 96-character hex addresses - one for each direction of communication.
import { Link, HandshakeserverConnector } from 'smokesigns';
// Peer A configuration
const peerA = new Link({
readWriteInterface: new HandshakeserverConnector({
serverUrl: 'https://handshake.autonomi.space',
readAddress: '2222...2222', // 96-char hex address to read from
writeAddress: '1111...1111' // 96-char hex address to write to
}),
priority: true // Peer A creates the offer
});
// Peer B configuration (inverse addresses)
const peerB = new Link({
readWriteInterface: new HandshakeserverConnector({
serverUrl: 'https://handshake.autonomi.space',
readAddress: '1111...1111', // Read what peer A writes
writeAddress: '2222...2222' // Write where peer A reads
}),
priority: false // Peer B waits for offer and creates answer
});
// Connect both peers
await peerA.connect();
await peerB.connect();Using DwebConnector
The DwebConnector uses the Autonomi network's public scratchpad for signaling.
import { Link, DwebConnector } from 'smokesigns';
const link = new Link({
readWriteInterface: new DwebConnector({
backendUrl: 'https://api.example.com',
writeTuple: ['myapp', 'myobject'],
readScratchpadAddress: 'scratchpad-address-here'
}),
priority: true
});
await link.connect();Custom Read/Write Interface
You can implement your own signaling mechanism by implementing the ReadWriteInterface:
import type { ReadWriteInterface } from 'smokesigns';
class CustomConnector implements ReadWriteInterface {
async write(data: any): Promise<void> {
// Implement your write logic
}
async read(): Promise<any> {
// Implement your read logic
// Return null if no data available
}
}WebRTC Configuration
The library uses a STUN-only configuration optimized for direct peer-to-peer connections:
- Multiple STUN servers for reliability
- Ordered data channels for guaranteed message delivery
- ICE gathering timeout of 3 seconds for cloud environments
- No TURN servers (direct connections only)
Overview
smokesigns provides three main components:
Link- A WebRTC connection object that handles offer/answer exchangeDwebConnector- A connector using the dweb framework via Autonomi for handshake establishmentHandshakeserverConnector- A connector using a simple handshake server for signaling
Core Objects
Link
A connection object with WebRTC functionality that manages offer/answer exchange through a read/write interface.
Properties:
connected: boolean- Whether the link is currently connecteddisconnected: boolean- Inverse of connectedconnect(): Promise<void>- Start the connection processdisconnect(): void- Disconnect the linklastSeen: number | null- Unix timestamp of when the partner was last active (updated every minute)onPresenceUpdate?: (ts: number) => void- Callback fired wheneverlastSeenchanges
Constructor Options:
readWriteInterface: ReadWriteInterface- Object withread()andwrite(data)methodspriority: boolean- If true, creates WebRTC offer; if false, waits for offer and creates answer
Behavior:
Timing behaviour:
- A connection attempt is only started immediately if you call
connect()within the first 10 seconds after the start of a full minute (hh:mm:00–hh:mm:09). - Danach werden automatische Versuche immer exakt zur nächsten vollen Minute ausgeführt.
- A connection attempt is only started immediately if you call
Role specific behaviour:
- Priority = true: Creates WebRTC offer, writes it via the interface, then polls for answer
- Priority = false: Polls for incoming offer (max 30 s old), then creates and writes answer
DwebConnector
A connector that uses the dweb framework via Autonomi to establish handshakes through public scratchpads.
Constructor Options:
backendUrl: string- Backend URL for dweb API calls (empty string for relative URLs)writeTuple: [string, string]-[appname, objectname]for writing to public scratchpadreadScratchpadAddress: string- Address of the public scratchpad to read from
Methods:
write(data: any): Promise<void>- Write data to public scratchpadread(): Promise<any>- Read data from public scratchpad (returns null if no data)
Usage Examples
Basic WebRTC Connection with Dweb
import { Link, DwebConnector } from 'smokesigns';
// Create dweb connector
const dwebConnector = new DwebConnector({
backendUrl: 'https://your-dweb-backend.com', // or '' for relative URLs
writeTuple: ['my-app', 'handshake-channel'],
readScratchpadAddress: 'peer-scratchpad-address'
});
// Create WebRTC link (as offer creator)
const link = new Link({
readWriteInterface: dwebConnector,
priority: true
});
// Connect
try {
await link.connect();
console.log('Connected!', link.connected);
} catch (error) {
console.error('Connection failed:', error);
}
// Later disconnect
link.disconnect();Custom ReadWriteInterface
You can implement your own ReadWriteInterface for different transport mechanisms:
import { Link } from 'smokesigns';
import type { ReadWriteInterface } from 'smokesigns';
class LocalStorageInterface implements ReadWriteInterface {
constructor(private key: string) {}
async write(data: any): Promise<void> {
localStorage.setItem(this.key, JSON.stringify(data));
}
async read(): Promise<any> {
const stored = localStorage.getItem(this.key);
return stored ? JSON.parse(stored) : null;
}
}
const link = new Link({
readWriteInterface: new LocalStorageInterface('webrtc-handshake'),
priority: false // Wait for offer, create answer
});Two-Way Setup
// Peer A (creates offer)
const peerA = new Link({
readWriteInterface: new DwebConnector({
backendUrl: '',
writeTuple: ['chat-app', 'peer-a-channel'],
readScratchpadAddress: 'peer-b-scratchpad-address'
}),
priority: true
});
// Peer B (creates answer)
const peerB = new Link({
readWriteInterface: new DwebConnector({
backendUrl: '',
writeTuple: ['chat-app', 'peer-b-channel'],
readScratchpadAddress: 'peer-a-scratchpad-address'
}),
priority: false
});
// Connect both simultaneously
await Promise.all([peerA.connect(), peerB.connect()]);Standalone Dweb Usage
import { DwebConnector } from 'smokesigns';
const connector = new DwebConnector({
backendUrl: 'https://autonomi-backend.example.com',
writeTuple: ['my-app', 'data-exchange'],
readScratchpadAddress: 'target-scratchpad-address'
});
// Write data
await connector.write({
message: 'Hello from smokesigns!',
timestamp: Date.now()
});
// Read data
const data = await connector.read();
if (data) {
console.log('Received:', data);
}ReadWriteInterface
The ReadWriteInterface is the core abstraction that allows different transport mechanisms:
interface ReadWriteInterface {
write(data: any): Promise<void> | void;
read(): Promise<any> | any;
}This allows Link to work with various backends:
- Dweb/Autonomi scratchpads (via
DwebConnector) - Local storage
- WebSocket servers
- HTTP APIs
- Any custom transport mechanism
Development
# Install dependencies
npm install
# Run tests
npm test
# Build library
npm run build
# Development mode
npm run devLicense
MIT
Connection Flow
graph TD
start["Link.connect() called"] --> attempt["Immediate connection attempt"]
attempt --> priority{priority flag}
priority -->|true| offerFlow["createOfferFlow()"]
priority -->|false| waitFlow["waitForOfferFlow()"]
%% Offer side
offerFlow --> writeOffer["Write Offer via ReadWriteInterface"]
writeOffer --> pollAns["Poll for Answer (max 30 s)"]
pollAns --> ansReceived{Answer received?}
ansReceived -->|yes| establish1["Establish WebRTC"]
ansReceived -->|no| retry1["Wait until next full minute"]
%% Answer side
waitFlow --> pollOffer["Poll for Offer (max 30 s)"]
pollOffer --> offerReceived{Recent offer?}
offerReceived -->|yes| genAnswer["Generate Answer & write"]
genAnswer --> establish2["Establish WebRTC"]
offerReceived -->|no| retry2["Wait until next full minute"]
%% Retry logic
retry1 --> attempt
retry2 --> attempt
%% Reconnection after disconnect
establish1 --> monitor["Monitor connection state"]
establish2 --> monitor
monitor --> disconnected{Disconnected?}
disconnected -->|yes| retry3["Wait until next full minute"]
retry3 --> attemptRunning the test suite
Unit tests
Run all unit tests:
npm testEnd-to-end browser test
The E2E test spins up two real Chrome instances via Puppeteer and establishes a full WebRTC connection through the public handshake server.
Headless (default):
npm run test:e2eVisible browser with DevTools (handy for debugging):
npm run test:e2e:uiOr manually:
HEADLESS=false npm run test:e2eWhen run in UI mode the script automatically opens DevTools and slows actions down a bit (slowMo=100ms) so you can watch the handshake in real time. It also keeps the windows open for 5 seconds after the test finishes.
