@xtr-dev/rondevu-client 0.10.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +332 -533
- package/dist/api.d.ts +17 -8
- package/dist/api.js +28 -25
- package/dist/durable-connection.d.ts +120 -0
- package/dist/durable-connection.js +244 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -2
- package/dist/quick-start.d.ts +29 -0
- package/dist/quick-start.js +44 -0
- package/dist/rondevu-context.d.ts +10 -0
- package/dist/rondevu-context.js +20 -0
- package/dist/rondevu-service.d.ts +8 -2
- package/dist/rondevu-service.js +12 -6
- package/dist/rondevu-signaler.d.ts +110 -0
- package/dist/rondevu-signaler.js +372 -0
- package/dist/service-client.d.ts +30 -47
- package/dist/service-client.js +95 -123
- package/dist/service-host.d.ts +28 -64
- package/dist/service-host.js +81 -147
- package/dist/types.d.ts +1 -3
- package/dist/types.js +5 -1
- package/dist/webrtc-context.d.ts +1 -3
- package/dist/webrtc-context.js +1 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@xtr-dev/rondevu-client)
|
|
4
4
|
|
|
5
|
-
🌐 **WebRTC
|
|
5
|
+
🌐 **Simple, high-level WebRTC peer-to-peer connections**
|
|
6
6
|
|
|
7
|
-
TypeScript/JavaScript client for Rondevu, providing
|
|
7
|
+
TypeScript/JavaScript client for Rondevu, providing easy-to-use WebRTC connections with automatic signaling, username-based discovery, and built-in reconnection support.
|
|
8
8
|
|
|
9
9
|
**Related repositories:**
|
|
10
10
|
- [@xtr-dev/rondevu-client](https://github.com/xtr-dev/rondevu-client) - TypeScript client library ([npm](https://www.npmjs.com/package/@xtr-dev/rondevu-client))
|
|
@@ -15,14 +15,16 @@ TypeScript/JavaScript client for Rondevu, providing durable WebRTC connections t
|
|
|
15
15
|
|
|
16
16
|
## Features
|
|
17
17
|
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
18
|
+
- **High-Level Wrappers**: ServiceHost and ServiceClient eliminate WebRTC boilerplate
|
|
19
|
+
- **Username-Based Discovery**: Connect to peers by username, not complex offer/answer exchange
|
|
20
|
+
- **Semver-Compatible Matching**: Requesting chat@1.0.0 matches any compatible 1.x.x version
|
|
21
|
+
- **Privacy-First Design**: Services are hidden by default - no enumeration possible
|
|
22
|
+
- **Automatic Reconnection**: Built-in retry logic with exponential backoff
|
|
23
|
+
- **Message Queuing**: Messages sent while disconnected are queued and flushed on reconnect
|
|
24
|
+
- **Cryptographic Username Claiming**: Secure ownership with Ed25519 signatures
|
|
25
|
+
- **Service Publishing**: Package-style naming (chat.app@1.0.0) with multiple simultaneous offers
|
|
24
26
|
- **TypeScript**: Full type safety and autocomplete
|
|
25
|
-
- **Configurable**:
|
|
27
|
+
- **Configurable Polling**: Exponential backoff with jitter to reduce server load
|
|
26
28
|
|
|
27
29
|
## Install
|
|
28
30
|
|
|
@@ -32,588 +34,400 @@ npm install @xtr-dev/rondevu-client
|
|
|
32
34
|
|
|
33
35
|
## Quick Start
|
|
34
36
|
|
|
35
|
-
###
|
|
37
|
+
### Hosting a Service (Alice)
|
|
36
38
|
|
|
37
39
|
```typescript
|
|
38
|
-
import {
|
|
39
|
-
|
|
40
|
-
//
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
40
|
+
import { RondevuService, ServiceHost } from '@xtr-dev/rondevu-client'
|
|
41
|
+
|
|
42
|
+
// Step 1: Create and initialize service
|
|
43
|
+
const service = new RondevuService({
|
|
44
|
+
apiUrl: 'https://api.ronde.vu',
|
|
45
|
+
username: 'alice'
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
await service.initialize() // Generates keypair
|
|
49
|
+
await service.claimUsername() // Claims username with signature
|
|
50
|
+
|
|
51
|
+
// Step 2: Create ServiceHost
|
|
52
|
+
const host = new ServiceHost({
|
|
53
|
+
service: 'chat.app@1.0.0',
|
|
54
|
+
rondevuService: service,
|
|
55
|
+
maxPeers: 5, // Accept up to 5 connections
|
|
56
|
+
ttl: 300000 // 5 minutes
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// Step 3: Listen for incoming connections
|
|
60
|
+
host.events.on('connection', (connection) => {
|
|
61
|
+
console.log('✅ New connection!')
|
|
62
|
+
|
|
63
|
+
connection.events.on('message', (msg) => {
|
|
64
|
+
console.log('📨 Received:', msg)
|
|
65
|
+
connection.sendMessage('Hello from Alice!')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
connection.events.on('state-change', (state) => {
|
|
69
|
+
console.log('Connection state:', state)
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
host.events.on('error', (error) => {
|
|
74
|
+
console.error('Host error:', error)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
// Step 4: Start hosting
|
|
78
|
+
await host.start()
|
|
79
|
+
console.log('Service is now live! Others can connect to @alice')
|
|
80
|
+
|
|
81
|
+
// Later: stop hosting
|
|
82
|
+
host.dispose()
|
|
78
83
|
```
|
|
79
84
|
|
|
80
85
|
### Connecting to a Service (Bob)
|
|
81
86
|
|
|
82
87
|
```typescript
|
|
83
|
-
import {
|
|
84
|
-
|
|
85
|
-
//
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
88
|
+
import { RondevuService, ServiceClient } from '@xtr-dev/rondevu-client'
|
|
89
|
+
|
|
90
|
+
// Step 1: Create and initialize service
|
|
91
|
+
const service = new RondevuService({
|
|
92
|
+
apiUrl: 'https://api.ronde.vu',
|
|
93
|
+
username: 'bob'
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
await service.initialize()
|
|
97
|
+
await service.claimUsername()
|
|
98
|
+
|
|
99
|
+
// Step 2: Create ServiceClient
|
|
100
|
+
const client = new ServiceClient({
|
|
101
|
+
username: 'alice', // Connect to Alice
|
|
102
|
+
serviceFqn: 'chat.app@1.0.0',
|
|
103
|
+
rondevuService: service,
|
|
104
|
+
autoReconnect: true,
|
|
105
|
+
maxReconnectAttempts: 5
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// Step 3: Listen for connection events
|
|
109
|
+
client.events.on('connected', (connection) => {
|
|
110
|
+
console.log('✅ Connected to Alice!')
|
|
111
|
+
|
|
112
|
+
connection.events.on('message', (msg) => {
|
|
113
|
+
console.log('📨 Received:', msg)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
// Send a message
|
|
117
|
+
connection.sendMessage('Hello from Bob!')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
client.events.on('disconnected', () => {
|
|
121
|
+
console.log('🔌 Disconnected')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
client.events.on('reconnecting', ({ attempt, maxAttempts }) => {
|
|
125
|
+
console.log(`🔄 Reconnecting (${attempt}/${maxAttempts})...`)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
client.events.on('error', (error) => {
|
|
129
|
+
console.error('❌ Error:', error)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// Step 4: Connect
|
|
133
|
+
await client.connect()
|
|
134
|
+
|
|
135
|
+
// Later: disconnect
|
|
136
|
+
client.dispose()
|
|
137
|
+
```
|
|
93
138
|
|
|
94
|
-
|
|
95
|
-
const channel = connection.createChannel('main');
|
|
139
|
+
## Core Concepts
|
|
96
140
|
|
|
97
|
-
|
|
98
|
-
console.log('📥 Received:', data);
|
|
99
|
-
});
|
|
141
|
+
### RondevuService
|
|
100
142
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
143
|
+
Handles authentication and username management:
|
|
144
|
+
- Generates Ed25519 keypair for signing
|
|
145
|
+
- Claims usernames with cryptographic proof
|
|
146
|
+
- Provides API client for signaling server
|
|
105
147
|
|
|
106
|
-
|
|
107
|
-
connection.on('connected', () => {
|
|
108
|
-
console.log('🎉 Connected to Alice');
|
|
109
|
-
});
|
|
148
|
+
### ServiceHost
|
|
110
149
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
150
|
+
High-level wrapper for hosting a WebRTC service:
|
|
151
|
+
- Automatically creates and publishes offers
|
|
152
|
+
- Handles incoming connections
|
|
153
|
+
- Manages ICE candidate exchange
|
|
154
|
+
- Supports multiple simultaneous peers
|
|
114
155
|
|
|
115
|
-
|
|
116
|
-
console.log('🔌 Disconnected');
|
|
117
|
-
});
|
|
156
|
+
### ServiceClient
|
|
118
157
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
158
|
+
High-level wrapper for connecting to services:
|
|
159
|
+
- Discovers services by username
|
|
160
|
+
- Handles offer/answer exchange automatically
|
|
161
|
+
- Built-in auto-reconnection with exponential backoff
|
|
162
|
+
- Event-driven API
|
|
122
163
|
|
|
123
|
-
|
|
124
|
-
await connection.connect();
|
|
164
|
+
### RTCDurableConnection
|
|
125
165
|
|
|
126
|
-
|
|
127
|
-
|
|
166
|
+
Low-level connection wrapper (used internally):
|
|
167
|
+
- Manages WebRTC PeerConnection lifecycle
|
|
168
|
+
- Handles ICE candidate polling
|
|
169
|
+
- Provides message queue for reliability
|
|
170
|
+
- State management and events
|
|
128
171
|
|
|
129
|
-
|
|
130
|
-
await connection.close();
|
|
131
|
-
```
|
|
172
|
+
## API Reference
|
|
132
173
|
|
|
133
|
-
|
|
174
|
+
### RondevuService
|
|
134
175
|
|
|
135
|
-
|
|
176
|
+
```typescript
|
|
177
|
+
const service = new RondevuService({
|
|
178
|
+
apiUrl: string, // Signaling server URL
|
|
179
|
+
username: string, // Your username
|
|
180
|
+
keypair?: Keypair // Optional: reuse existing keypair
|
|
181
|
+
})
|
|
136
182
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
- Exponential backoff with jitter (1s → 2s → 4s → 8s → ... max 30s)
|
|
140
|
-
- Configurable max retry attempts (default: 10)
|
|
141
|
-
- Manages multiple DurableChannel instances
|
|
183
|
+
// Initialize service (generates keypair if not provided)
|
|
184
|
+
await service.initialize(): Promise<void>
|
|
142
185
|
|
|
143
|
-
|
|
186
|
+
// Claim username with cryptographic signature
|
|
187
|
+
await service.claimUsername(): Promise<void>
|
|
144
188
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
- Flushes queue on reconnection
|
|
148
|
-
- Configurable queue size and message age limits
|
|
149
|
-
- RTCDataChannel-compatible API with event emitters
|
|
189
|
+
// Check if username is claimed
|
|
190
|
+
service.isUsernameClaimed(): boolean
|
|
150
191
|
|
|
151
|
-
|
|
192
|
+
// Get current username
|
|
193
|
+
service.getUsername(): string
|
|
152
194
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
- Creates DurableConnection for each incoming peer
|
|
156
|
-
- Manages connection pool for multiple simultaneous connections
|
|
195
|
+
// Get keypair
|
|
196
|
+
service.getKeypair(): Keypair
|
|
157
197
|
|
|
158
|
-
|
|
198
|
+
// Get API client
|
|
199
|
+
service.getAPI(): RondevuAPI
|
|
200
|
+
```
|
|
159
201
|
|
|
160
|
-
###
|
|
202
|
+
### ServiceHost
|
|
161
203
|
|
|
162
204
|
```typescript
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
205
|
+
const host = new ServiceHost({
|
|
206
|
+
service: string, // Service FQN (e.g., 'chat.app@1.0.0')
|
|
207
|
+
rondevuService: RondevuService,
|
|
208
|
+
maxPeers?: number, // Default: 5
|
|
209
|
+
ttl?: number, // Default: 300000 (5 minutes)
|
|
210
|
+
isPublic?: boolean, // Default: true
|
|
211
|
+
rtcConfiguration?: RTCConfiguration
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
// Start hosting
|
|
215
|
+
await host.start(): Promise<void>
|
|
216
|
+
|
|
217
|
+
// Stop hosting and cleanup
|
|
218
|
+
host.dispose(): void
|
|
219
|
+
|
|
220
|
+
// Get all active connections
|
|
221
|
+
host.getConnections(): RTCDurableConnection[]
|
|
222
|
+
|
|
223
|
+
// Events
|
|
224
|
+
host.events.on('connection', (conn: RTCDurableConnection) => {})
|
|
225
|
+
host.events.on('error', (error: Error) => {})
|
|
181
226
|
```
|
|
182
227
|
|
|
183
|
-
###
|
|
228
|
+
### ServiceClient
|
|
184
229
|
|
|
185
230
|
```typescript
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
231
|
+
const client = new ServiceClient({
|
|
232
|
+
username: string, // Host username to connect to
|
|
233
|
+
serviceFqn: string, // Service FQN (e.g., 'chat.app@1.0.0')
|
|
234
|
+
rondevuService: RondevuService,
|
|
235
|
+
autoReconnect?: boolean, // Default: true
|
|
236
|
+
maxReconnectAttempts?: number, // Default: 5
|
|
237
|
+
rtcConfiguration?: RTCConfiguration
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
// Connect to service
|
|
241
|
+
await client.connect(): Promise<RTCDurableConnection>
|
|
242
|
+
|
|
243
|
+
// Disconnect and cleanup
|
|
244
|
+
client.dispose(): void
|
|
245
|
+
|
|
246
|
+
// Get current connection
|
|
247
|
+
client.getConnection(): RTCDurableConnection | null
|
|
248
|
+
|
|
249
|
+
// Events
|
|
250
|
+
client.events.on('connected', (conn: RTCDurableConnection) => {})
|
|
251
|
+
client.events.on('disconnected', () => {})
|
|
252
|
+
client.events.on('reconnecting', (info: { attempt: number, maxAttempts: number }) => {})
|
|
253
|
+
client.events.on('error', (error: Error) => {})
|
|
200
254
|
```
|
|
201
255
|
|
|
202
|
-
|
|
203
|
-
- Format: Lowercase alphanumeric + dash (`a-z`, `0-9`, `-`)
|
|
204
|
-
- Length: 3-32 characters
|
|
205
|
-
- Pattern: `^[a-z0-9][a-z0-9-]*[a-z0-9]$`
|
|
206
|
-
- Validity: 365 days from claim/last use
|
|
207
|
-
- Ownership: Secured by Ed25519 public key
|
|
208
|
-
|
|
209
|
-
### Durable Service API
|
|
256
|
+
### RTCDurableConnection
|
|
210
257
|
|
|
211
258
|
```typescript
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
username: 'alice',
|
|
215
|
-
privateKey: keypair.privateKey,
|
|
216
|
-
serviceFqn: 'chat@1.0.0',
|
|
217
|
-
|
|
218
|
-
// Service options
|
|
219
|
-
isPublic: true, // optional, default: false
|
|
220
|
-
metadata: { version: '1.0' }, // optional
|
|
221
|
-
ttl: 300000, // optional, default: 5 minutes
|
|
222
|
-
ttlRefreshMargin: 0.2, // optional, refresh at 80% of TTL
|
|
223
|
-
|
|
224
|
-
// Connection pooling
|
|
225
|
-
poolSize: 10, // optional, default: 1
|
|
226
|
-
pollingInterval: 2000, // optional, default: 2000ms
|
|
227
|
-
|
|
228
|
-
// Connection options (applied to incoming connections)
|
|
229
|
-
maxReconnectAttempts: 10, // optional, default: 10
|
|
230
|
-
reconnectBackoffBase: 1000, // optional, default: 1000ms
|
|
231
|
-
reconnectBackoffMax: 30000, // optional, default: 30000ms
|
|
232
|
-
reconnectJitter: 0.2, // optional, default: 0.2 (±20%)
|
|
233
|
-
connectionTimeout: 30000, // optional, default: 30000ms
|
|
234
|
-
|
|
235
|
-
// Message queuing
|
|
236
|
-
maxQueueSize: 1000, // optional, default: 1000
|
|
237
|
-
maxMessageAge: 60000, // optional, default: 60000ms (1 minute)
|
|
238
|
-
|
|
239
|
-
// WebRTC configuration
|
|
240
|
-
rtcConfig: {
|
|
241
|
-
iceServers: [
|
|
242
|
-
{ urls: 'stun:stun.l.google.com:19302' }
|
|
243
|
-
]
|
|
244
|
-
},
|
|
245
|
-
|
|
246
|
-
// Connection handler
|
|
247
|
-
handler: (channel, connectionId) => {
|
|
248
|
-
// Handle incoming connection
|
|
249
|
-
channel.on('message', (data) => {
|
|
250
|
-
console.log('Received:', data);
|
|
251
|
-
channel.send(`Echo: ${data}`);
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
// Start the service
|
|
257
|
-
const info = await service.start();
|
|
258
|
-
// { serviceId: '...', uuid: '...', expiresAt: 1234567890 }
|
|
259
|
-
|
|
260
|
-
// Get active connections
|
|
261
|
-
const connections = service.getActiveConnections();
|
|
262
|
-
// ['conn-123', 'conn-456']
|
|
263
|
-
|
|
264
|
-
// Get service info
|
|
265
|
-
const serviceInfo = service.getServiceInfo();
|
|
266
|
-
// { serviceId: '...', uuid: '...', expiresAt: 1234567890 } | null
|
|
267
|
-
|
|
268
|
-
// Stop the service
|
|
269
|
-
await service.stop();
|
|
270
|
-
```
|
|
259
|
+
// Connection state
|
|
260
|
+
connection.state: 'connected' | 'connecting' | 'disconnected'
|
|
271
261
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
service.on('published', (serviceId, uuid) => {
|
|
275
|
-
console.log(`Service published: ${uuid}`);
|
|
276
|
-
});
|
|
262
|
+
// Send message (returns true if sent, false if queued)
|
|
263
|
+
await connection.sendMessage(message: string): Promise<boolean>
|
|
277
264
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
});
|
|
265
|
+
// Queue message for sending when connected
|
|
266
|
+
await connection.queueMessage(message: string, options?: QueueMessageOptions): Promise<void>
|
|
281
267
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
});
|
|
268
|
+
// Disconnect
|
|
269
|
+
connection.disconnect(): void
|
|
285
270
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
})
|
|
271
|
+
// Events
|
|
272
|
+
connection.events.on('message', (msg: string) => {})
|
|
273
|
+
connection.events.on('state-change', (state: ConnectionStates) => {})
|
|
274
|
+
```
|
|
289
275
|
|
|
290
|
-
|
|
291
|
-
console.error(`Service error (${context}):`, error);
|
|
292
|
-
});
|
|
276
|
+
## Configuration
|
|
293
277
|
|
|
294
|
-
|
|
295
|
-
console.log('Service stopped');
|
|
296
|
-
});
|
|
297
|
-
```
|
|
278
|
+
### Polling Configuration
|
|
298
279
|
|
|
299
|
-
|
|
280
|
+
The signaling uses configurable polling with exponential backoff:
|
|
300
281
|
|
|
301
282
|
```typescript
|
|
302
|
-
//
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
// Message queuing
|
|
312
|
-
maxQueueSize: 1000, // optional, default: 1000
|
|
313
|
-
maxMessageAge: 60000, // optional, default: 60000ms
|
|
314
|
-
|
|
315
|
-
// WebRTC configuration
|
|
316
|
-
rtcConfig: {
|
|
317
|
-
iceServers: [
|
|
318
|
-
{ urls: 'stun:stun.l.google.com:19302' }
|
|
319
|
-
]
|
|
320
|
-
}
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
// Connect by UUID
|
|
324
|
-
const connection2 = await client.connectByUuid('service-uuid-here', {
|
|
325
|
-
maxReconnectAttempts: 5
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
// Create channels before connecting
|
|
329
|
-
const channel = connection.createChannel('main');
|
|
330
|
-
const fileChannel = connection.createChannel('files', {
|
|
331
|
-
ordered: false,
|
|
332
|
-
maxRetransmits: 3
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
// Get existing channel
|
|
336
|
-
const existingChannel = connection.getChannel('main');
|
|
337
|
-
|
|
338
|
-
// Check connection state
|
|
339
|
-
const state = connection.getState();
|
|
340
|
-
// 'connecting' | 'connected' | 'reconnecting' | 'disconnected' | 'failed' | 'closed'
|
|
341
|
-
|
|
342
|
-
const isConnected = connection.isConnected();
|
|
343
|
-
|
|
344
|
-
// Connect
|
|
345
|
-
await connection.connect();
|
|
346
|
-
|
|
347
|
-
// Close connection
|
|
348
|
-
await connection.close();
|
|
283
|
+
// Default polling config
|
|
284
|
+
{
|
|
285
|
+
initialInterval: 500, // Start at 500ms
|
|
286
|
+
maxInterval: 5000, // Max 5 seconds
|
|
287
|
+
backoffMultiplier: 1.5, // Increase by 1.5x each time
|
|
288
|
+
maxRetries: 50, // Max 50 attempts
|
|
289
|
+
jitter: true // Add random 0-100ms to prevent thundering herd
|
|
290
|
+
}
|
|
349
291
|
```
|
|
350
292
|
|
|
351
|
-
|
|
352
|
-
```typescript
|
|
353
|
-
connection.on('state', (newState, previousState) => {
|
|
354
|
-
console.log(`State: ${previousState} → ${newState}`);
|
|
355
|
-
});
|
|
293
|
+
This is handled automatically - no configuration needed.
|
|
356
294
|
|
|
357
|
-
|
|
358
|
-
console.log('Connected');
|
|
359
|
-
});
|
|
295
|
+
### WebRTC Configuration
|
|
360
296
|
|
|
361
|
-
|
|
362
|
-
console.log(`Reconnecting (${attempt}/${maxAttempts}) in ${delay}ms`);
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
connection.on('disconnected', () => {
|
|
366
|
-
console.log('Disconnected');
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
connection.on('failed', (error, permanent) => {
|
|
370
|
-
console.error('Connection failed:', error, 'Permanent:', permanent);
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
connection.on('closed', () => {
|
|
374
|
-
console.log('Connection closed');
|
|
375
|
-
});
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
### Durable Channel API
|
|
297
|
+
Provide custom STUN/TURN servers:
|
|
379
298
|
|
|
380
299
|
```typescript
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
300
|
+
const host = new ServiceHost({
|
|
301
|
+
service: 'chat.app@1.0.0',
|
|
302
|
+
rondevuService: service,
|
|
303
|
+
rtcConfiguration: {
|
|
304
|
+
iceServers: [
|
|
305
|
+
{ urls: 'stun:stun.l.google.com:19302' },
|
|
306
|
+
{
|
|
307
|
+
urls: 'turn:turn.example.com:3478',
|
|
308
|
+
username: 'user',
|
|
309
|
+
credential: 'pass'
|
|
310
|
+
}
|
|
311
|
+
]
|
|
312
|
+
}
|
|
313
|
+
})
|
|
314
|
+
```
|
|
385
315
|
|
|
386
|
-
|
|
387
|
-
channel.send('Hello!');
|
|
388
|
-
channel.send(new ArrayBuffer(1024));
|
|
389
|
-
channel.send(new Blob(['data']));
|
|
316
|
+
## Username Rules
|
|
390
317
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
318
|
+
- **Format**: Lowercase alphanumeric + dash (`a-z`, `0-9`, `-`)
|
|
319
|
+
- **Length**: 3-32 characters
|
|
320
|
+
- **Pattern**: `^[a-z0-9][a-z0-9-]*[a-z0-9]$`
|
|
321
|
+
- **Validity**: 365 days from claim/last use
|
|
322
|
+
- **Ownership**: Secured by Ed25519 public key signature
|
|
394
323
|
|
|
395
|
-
|
|
396
|
-
const buffered = channel.bufferedAmount;
|
|
324
|
+
## Examples
|
|
397
325
|
|
|
398
|
-
|
|
399
|
-
channel.bufferedAmountLowThreshold = 16 * 1024; // 16KB
|
|
326
|
+
### Chat Application
|
|
400
327
|
|
|
401
|
-
|
|
402
|
-
const queueSize = channel.getQueueSize();
|
|
328
|
+
See [demo/demo.js](./demo/demo.js) for a complete working example.
|
|
403
329
|
|
|
404
|
-
|
|
405
|
-
channel.close();
|
|
406
|
-
```
|
|
330
|
+
### Persistent Keypair
|
|
407
331
|
|
|
408
|
-
**Channel Events:**
|
|
409
332
|
```typescript
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
333
|
+
// Save keypair to localStorage
|
|
334
|
+
const service = new RondevuService({
|
|
335
|
+
apiUrl: 'https://api.ronde.vu',
|
|
336
|
+
username: 'alice'
|
|
337
|
+
})
|
|
413
338
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
});
|
|
339
|
+
await service.initialize()
|
|
340
|
+
await service.claimUsername()
|
|
417
341
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
342
|
+
// Save for later
|
|
343
|
+
localStorage.setItem('rondevu-keypair', JSON.stringify(service.getKeypair()))
|
|
344
|
+
localStorage.setItem('rondevu-username', service.getUsername())
|
|
421
345
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
346
|
+
// Load on next session
|
|
347
|
+
const savedKeypair = JSON.parse(localStorage.getItem('rondevu-keypair'))
|
|
348
|
+
const savedUsername = localStorage.getItem('rondevu-username')
|
|
425
349
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
350
|
+
const service2 = new RondevuService({
|
|
351
|
+
apiUrl: 'https://api.ronde.vu',
|
|
352
|
+
username: savedUsername,
|
|
353
|
+
keypair: savedKeypair
|
|
354
|
+
})
|
|
429
355
|
|
|
430
|
-
|
|
431
|
-
console.warn(`Queue overflow: ${droppedCount} messages dropped`);
|
|
432
|
-
});
|
|
356
|
+
await service2.initialize() // Reuses keypair
|
|
433
357
|
```
|
|
434
358
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
### Connection Configuration
|
|
359
|
+
### Message Queue Example
|
|
438
360
|
|
|
439
361
|
```typescript
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
362
|
+
// Messages are automatically queued if not connected yet
|
|
363
|
+
client.events.on('connected', (connection) => {
|
|
364
|
+
// Send immediately
|
|
365
|
+
connection.sendMessage('Hello!')
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
// Or queue for later
|
|
369
|
+
await client.connect()
|
|
370
|
+
const conn = client.getConnection()
|
|
371
|
+
await conn.queueMessage('This will be sent when connected', {
|
|
372
|
+
expiresAt: Date.now() + 60000 // Expire after 1 minute
|
|
373
|
+
})
|
|
450
374
|
```
|
|
451
375
|
|
|
452
|
-
|
|
376
|
+
## Migration from v0.9.x
|
|
453
377
|
|
|
454
|
-
|
|
455
|
-
interface DurableServiceConfig extends DurableConnectionConfig {
|
|
456
|
-
username: string;
|
|
457
|
-
privateKey: string;
|
|
458
|
-
serviceFqn: string;
|
|
459
|
-
isPublic?: boolean; // default: false
|
|
460
|
-
metadata?: Record<string, any>;
|
|
461
|
-
ttl?: number; // default: 300000 (5 minutes)
|
|
462
|
-
ttlRefreshMargin?: number; // default: 0.2 (refresh at 80%)
|
|
463
|
-
poolSize?: number; // default: 1
|
|
464
|
-
pollingInterval?: number; // default: 2000 (2 seconds)
|
|
465
|
-
}
|
|
466
|
-
```
|
|
378
|
+
v0.11.0+ introduces high-level wrappers, RESTful API changes, and semver-compatible discovery:
|
|
467
379
|
|
|
468
|
-
|
|
380
|
+
**API Changes:**
|
|
381
|
+
- Server endpoints restructured (`/usernames/*` → `/users/*`)
|
|
382
|
+
- Added `ServiceHost` and `ServiceClient` wrappers
|
|
383
|
+
- Message queue fully implemented
|
|
384
|
+
- Configurable polling with exponential backoff
|
|
385
|
+
- Removed deprecated `cleanup()` methods (use `dispose()`)
|
|
386
|
+
- **v0.11.0+**: Services use `offers` array instead of single `sdp`
|
|
387
|
+
- **v0.11.0+**: Semver-compatible service discovery (chat@1.0.0 matches 1.x.x)
|
|
388
|
+
- **v0.11.0+**: All services are hidden - no listing endpoint
|
|
389
|
+
- **v0.11.0+**: Services support multiple simultaneous offers for connection pooling
|
|
469
390
|
|
|
470
|
-
|
|
391
|
+
**Migration Guide:**
|
|
471
392
|
|
|
472
393
|
```typescript
|
|
473
|
-
//
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
poolSize: 50, // Handle 50 concurrent users
|
|
487
|
-
handler: (channel, connectionId) => {
|
|
488
|
-
console.log(`User ${connectionId} joined`);
|
|
489
|
-
|
|
490
|
-
channel.on('message', (data) => {
|
|
491
|
-
console.log(`[${connectionId}]: ${data}`);
|
|
492
|
-
// Broadcast to all users (implement your broadcast logic)
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
channel.on('close', () => {
|
|
496
|
-
console.log(`User ${connectionId} left`);
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
await service.start();
|
|
502
|
-
|
|
503
|
-
// Client
|
|
504
|
-
const client2 = new Rondevu();
|
|
505
|
-
await client2.register();
|
|
506
|
-
|
|
507
|
-
const connection = await client2.connect('alice', 'chat@1.0.0');
|
|
508
|
-
const channel = connection.createChannel('chat');
|
|
509
|
-
|
|
510
|
-
channel.on('message', (data) => {
|
|
511
|
-
console.log('Message:', data);
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
await connection.connect();
|
|
515
|
-
channel.send('Hello everyone!');
|
|
394
|
+
// Before (v0.9.x) - Manual WebRTC setup
|
|
395
|
+
const signaler = new RondevuSignaler(service, 'chat@1.0.0')
|
|
396
|
+
const context = new WebRTCContext()
|
|
397
|
+
const pc = context.createPeerConnection()
|
|
398
|
+
// ... 50+ lines of boilerplate
|
|
399
|
+
|
|
400
|
+
// After (v0.11.0) - ServiceHost wrapper
|
|
401
|
+
const host = new ServiceHost({
|
|
402
|
+
service: 'chat@1.0.0',
|
|
403
|
+
rondevuService: service
|
|
404
|
+
})
|
|
405
|
+
await host.start()
|
|
406
|
+
// Done!
|
|
516
407
|
```
|
|
517
408
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
```typescript
|
|
521
|
-
// Server
|
|
522
|
-
const service = await client.exposeService({
|
|
523
|
-
username: 'alice',
|
|
524
|
-
privateKey: keypair.privateKey,
|
|
525
|
-
serviceFqn: 'files@1.0.0',
|
|
526
|
-
handler: (channel, connectionId) => {
|
|
527
|
-
channel.on('message', async (data) => {
|
|
528
|
-
const request = JSON.parse(data);
|
|
529
|
-
|
|
530
|
-
if (request.action === 'download') {
|
|
531
|
-
const file = await fs.readFile(request.path);
|
|
532
|
-
const chunkSize = 16 * 1024; // 16KB chunks
|
|
533
|
-
|
|
534
|
-
for (let i = 0; i < file.byteLength; i += chunkSize) {
|
|
535
|
-
const chunk = file.slice(i, i + chunkSize);
|
|
536
|
-
channel.send(chunk);
|
|
537
|
-
|
|
538
|
-
// Wait for buffer to drain if needed
|
|
539
|
-
while (channel.bufferedAmount > 16 * 1024 * 1024) { // 16MB
|
|
540
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
channel.send(JSON.stringify({ done: true }));
|
|
545
|
-
}
|
|
546
|
-
});
|
|
547
|
-
}
|
|
548
|
-
});
|
|
549
|
-
|
|
550
|
-
await service.start();
|
|
551
|
-
|
|
552
|
-
// Client
|
|
553
|
-
const connection = await client.connect('alice', 'files@1.0.0');
|
|
554
|
-
const channel = connection.createChannel('files');
|
|
555
|
-
|
|
556
|
-
const chunks = [];
|
|
557
|
-
channel.on('message', (data) => {
|
|
558
|
-
if (typeof data === 'string') {
|
|
559
|
-
const msg = JSON.parse(data);
|
|
560
|
-
if (msg.done) {
|
|
561
|
-
const blob = new Blob(chunks);
|
|
562
|
-
console.log('Download complete:', blob.size, 'bytes');
|
|
563
|
-
}
|
|
564
|
-
} else {
|
|
565
|
-
chunks.push(data);
|
|
566
|
-
console.log('Progress:', chunks.length * 16 * 1024, 'bytes');
|
|
567
|
-
}
|
|
568
|
-
});
|
|
569
|
-
|
|
570
|
-
await connection.connect();
|
|
571
|
-
channel.send(JSON.stringify({ action: 'download', path: '/file.zip' }));
|
|
572
|
-
```
|
|
573
|
-
|
|
574
|
-
## Platform-Specific Setup
|
|
409
|
+
## Platform Support
|
|
575
410
|
|
|
576
411
|
### Modern Browsers
|
|
577
412
|
Works out of the box - no additional setup needed.
|
|
578
413
|
|
|
579
414
|
### Node.js 18+
|
|
580
|
-
Native fetch is available, but
|
|
415
|
+
Native fetch is available, but WebRTC requires polyfills:
|
|
581
416
|
|
|
582
417
|
```bash
|
|
583
418
|
npm install wrtc
|
|
584
419
|
```
|
|
585
420
|
|
|
586
421
|
```typescript
|
|
587
|
-
import {
|
|
588
|
-
import { RTCPeerConnection, RTCSessionDescription, RTCIceCandidate } from 'wrtc'
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
})
|
|
596
|
-
```
|
|
597
|
-
|
|
598
|
-
### Node.js < 18
|
|
599
|
-
Install both fetch and WebRTC polyfills:
|
|
600
|
-
|
|
601
|
-
```bash
|
|
602
|
-
npm install node-fetch wrtc
|
|
603
|
-
```
|
|
604
|
-
|
|
605
|
-
```typescript
|
|
606
|
-
import { Rondevu } from '@xtr-dev/rondevu-client';
|
|
607
|
-
import fetch from 'node-fetch';
|
|
608
|
-
import { RTCPeerConnection, RTCSessionDescription, RTCIceCandidate } from 'wrtc';
|
|
609
|
-
|
|
610
|
-
const client = new Rondevu({
|
|
611
|
-
baseUrl: 'https://api.ronde.vu',
|
|
612
|
-
fetch: fetch as any,
|
|
613
|
-
RTCPeerConnection,
|
|
614
|
-
RTCSessionDescription,
|
|
615
|
-
RTCIceCandidate
|
|
616
|
-
});
|
|
422
|
+
import { WebRTCContext } from '@xtr-dev/rondevu-client'
|
|
423
|
+
import { RTCPeerConnection, RTCSessionDescription, RTCIceCandidate } from 'wrtc'
|
|
424
|
+
|
|
425
|
+
// Configure WebRTC context
|
|
426
|
+
const context = new WebRTCContext({
|
|
427
|
+
RTCPeerConnection,
|
|
428
|
+
RTCSessionDescription,
|
|
429
|
+
RTCIceCandidate
|
|
430
|
+
} as any)
|
|
617
431
|
```
|
|
618
432
|
|
|
619
433
|
## TypeScript
|
|
@@ -622,38 +436,23 @@ All types are exported:
|
|
|
622
436
|
|
|
623
437
|
```typescript
|
|
624
438
|
import type {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
DurableConnectionEvents,
|
|
641
|
-
DurableChannelEvents,
|
|
642
|
-
DurableServiceEvents,
|
|
643
|
-
ConnectionInfo,
|
|
644
|
-
ServiceInfo
|
|
645
|
-
} from '@xtr-dev/rondevu-client';
|
|
439
|
+
RondevuServiceOptions,
|
|
440
|
+
ServiceHostOptions,
|
|
441
|
+
ServiceHostEvents,
|
|
442
|
+
ServiceClientOptions,
|
|
443
|
+
ServiceClientEvents,
|
|
444
|
+
ConnectionInterface,
|
|
445
|
+
ConnectionEvents,
|
|
446
|
+
ConnectionStates,
|
|
447
|
+
Message,
|
|
448
|
+
QueueMessageOptions,
|
|
449
|
+
Signaler,
|
|
450
|
+
PollingConfig,
|
|
451
|
+
Credentials,
|
|
452
|
+
Keypair
|
|
453
|
+
} from '@xtr-dev/rondevu-client'
|
|
646
454
|
```
|
|
647
455
|
|
|
648
|
-
## Migration from v0.8.x
|
|
649
|
-
|
|
650
|
-
v0.9.0 is a **breaking change** that replaces the low-level APIs with high-level durable connections. See [MIGRATION.md](./MIGRATION.md) for detailed migration guide.
|
|
651
|
-
|
|
652
|
-
**Key Changes:**
|
|
653
|
-
- ❌ Removed: `client.services.*`, `client.discovery.*`, `client.createPeer()` (low-level APIs)
|
|
654
|
-
- ✅ Added: `client.exposeService()`, `client.connect()`, `client.connectByUuid()` (durable APIs)
|
|
655
|
-
- ✅ Changed: Focus on durable connections with automatic reconnection and message queuing
|
|
656
|
-
|
|
657
456
|
## License
|
|
658
457
|
|
|
659
458
|
MIT
|