@xtr-dev/rondevu-client 0.10.0 → 0.10.2
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 +326 -533
- package/dist/api.d.ts +6 -6
- 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 +4 -0
- package/dist/rondevu-service.js +9 -3
- package/dist/rondevu-signaler.d.ts +110 -0
- package/dist/rondevu-signaler.js +361 -0
- package/dist/service-client.d.ts +31 -46
- package/dist/service-client.js +95 -122
- package/dist/service-host.d.ts +28 -62
- package/dist/service-host.js +81 -146
- package/dist/types.d.ts +1 -3
- package/dist/types.js +5 -1
- package/dist/webrtc-context.d.ts +2 -3
- package/dist/webrtc-context.js +30 -29
- 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,14 @@ TypeScript/JavaScript client for Rondevu, providing durable WebRTC connections t
|
|
|
15
15
|
|
|
16
16
|
## Features
|
|
17
17
|
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
22
|
-
- **Username Claiming**:
|
|
23
|
-
- **Service Publishing**: Package-style naming (
|
|
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
|
+
- **Automatic Reconnection**: Built-in retry logic with exponential backoff
|
|
21
|
+
- **Message Queuing**: Messages sent while disconnected are queued and flushed on reconnect
|
|
22
|
+
- **Cryptographic Username Claiming**: Secure ownership with Ed25519 signatures
|
|
23
|
+
- **Service Publishing**: Package-style naming (chat.app@1.0.0)
|
|
24
24
|
- **TypeScript**: Full type safety and autocomplete
|
|
25
|
-
- **Configurable**:
|
|
25
|
+
- **Configurable Polling**: Exponential backoff with jitter to reduce server load
|
|
26
26
|
|
|
27
27
|
## Install
|
|
28
28
|
|
|
@@ -32,588 +32,396 @@ npm install @xtr-dev/rondevu-client
|
|
|
32
32
|
|
|
33
33
|
## Quick Start
|
|
34
34
|
|
|
35
|
-
###
|
|
35
|
+
### Hosting a Service (Alice)
|
|
36
36
|
|
|
37
37
|
```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
|
-
|
|
38
|
+
import { RondevuService, ServiceHost } from '@xtr-dev/rondevu-client'
|
|
39
|
+
|
|
40
|
+
// Step 1: Create and initialize service
|
|
41
|
+
const service = new RondevuService({
|
|
42
|
+
apiUrl: 'https://api.ronde.vu',
|
|
43
|
+
username: 'alice'
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
await service.initialize() // Generates keypair
|
|
47
|
+
await service.claimUsername() // Claims username with signature
|
|
48
|
+
|
|
49
|
+
// Step 2: Create ServiceHost
|
|
50
|
+
const host = new ServiceHost({
|
|
51
|
+
service: 'chat.app@1.0.0',
|
|
52
|
+
rondevuService: service,
|
|
53
|
+
maxPeers: 5, // Accept up to 5 connections
|
|
54
|
+
ttl: 300000 // 5 minutes
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// Step 3: Listen for incoming connections
|
|
58
|
+
host.events.on('connection', (connection) => {
|
|
59
|
+
console.log('✅ New connection!')
|
|
60
|
+
|
|
61
|
+
connection.events.on('message', (msg) => {
|
|
62
|
+
console.log('📨 Received:', msg)
|
|
63
|
+
connection.sendMessage('Hello from Alice!')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
connection.events.on('state-change', (state) => {
|
|
67
|
+
console.log('Connection state:', state)
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
host.events.on('error', (error) => {
|
|
72
|
+
console.error('Host error:', error)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// Step 4: Start hosting
|
|
76
|
+
await host.start()
|
|
77
|
+
console.log('Service is now live! Others can connect to @alice')
|
|
78
|
+
|
|
79
|
+
// Later: stop hosting
|
|
80
|
+
host.dispose()
|
|
78
81
|
```
|
|
79
82
|
|
|
80
83
|
### Connecting to a Service (Bob)
|
|
81
84
|
|
|
82
85
|
```typescript
|
|
83
|
-
import {
|
|
84
|
-
|
|
85
|
-
//
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
86
|
+
import { RondevuService, ServiceClient } from '@xtr-dev/rondevu-client'
|
|
87
|
+
|
|
88
|
+
// Step 1: Create and initialize service
|
|
89
|
+
const service = new RondevuService({
|
|
90
|
+
apiUrl: 'https://api.ronde.vu',
|
|
91
|
+
username: 'bob'
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
await service.initialize()
|
|
95
|
+
await service.claimUsername()
|
|
96
|
+
|
|
97
|
+
// Step 2: Create ServiceClient
|
|
98
|
+
const client = new ServiceClient({
|
|
99
|
+
username: 'alice', // Connect to Alice
|
|
100
|
+
serviceFqn: 'chat.app@1.0.0',
|
|
101
|
+
rondevuService: service,
|
|
102
|
+
autoReconnect: true,
|
|
103
|
+
maxReconnectAttempts: 5
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
// Step 3: Listen for connection events
|
|
107
|
+
client.events.on('connected', (connection) => {
|
|
108
|
+
console.log('✅ Connected to Alice!')
|
|
109
|
+
|
|
110
|
+
connection.events.on('message', (msg) => {
|
|
111
|
+
console.log('📨 Received:', msg)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// Send a message
|
|
115
|
+
connection.sendMessage('Hello from Bob!')
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
client.events.on('disconnected', () => {
|
|
119
|
+
console.log('🔌 Disconnected')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
client.events.on('reconnecting', ({ attempt, maxAttempts }) => {
|
|
123
|
+
console.log(`🔄 Reconnecting (${attempt}/${maxAttempts})...`)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
client.events.on('error', (error) => {
|
|
127
|
+
console.error('❌ Error:', error)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
// Step 4: Connect
|
|
131
|
+
await client.connect()
|
|
132
|
+
|
|
133
|
+
// Later: disconnect
|
|
134
|
+
client.dispose()
|
|
135
|
+
```
|
|
93
136
|
|
|
94
|
-
|
|
95
|
-
const channel = connection.createChannel('main');
|
|
137
|
+
## Core Concepts
|
|
96
138
|
|
|
97
|
-
|
|
98
|
-
console.log('📥 Received:', data);
|
|
99
|
-
});
|
|
139
|
+
### RondevuService
|
|
100
140
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
141
|
+
Handles authentication and username management:
|
|
142
|
+
- Generates Ed25519 keypair for signing
|
|
143
|
+
- Claims usernames with cryptographic proof
|
|
144
|
+
- Provides API client for signaling server
|
|
105
145
|
|
|
106
|
-
|
|
107
|
-
connection.on('connected', () => {
|
|
108
|
-
console.log('🎉 Connected to Alice');
|
|
109
|
-
});
|
|
146
|
+
### ServiceHost
|
|
110
147
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
148
|
+
High-level wrapper for hosting a WebRTC service:
|
|
149
|
+
- Automatically creates and publishes offers
|
|
150
|
+
- Handles incoming connections
|
|
151
|
+
- Manages ICE candidate exchange
|
|
152
|
+
- Supports multiple simultaneous peers
|
|
114
153
|
|
|
115
|
-
|
|
116
|
-
console.log('🔌 Disconnected');
|
|
117
|
-
});
|
|
154
|
+
### ServiceClient
|
|
118
155
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
156
|
+
High-level wrapper for connecting to services:
|
|
157
|
+
- Discovers services by username
|
|
158
|
+
- Handles offer/answer exchange automatically
|
|
159
|
+
- Built-in auto-reconnection with exponential backoff
|
|
160
|
+
- Event-driven API
|
|
122
161
|
|
|
123
|
-
|
|
124
|
-
await connection.connect();
|
|
162
|
+
### RTCDurableConnection
|
|
125
163
|
|
|
126
|
-
|
|
127
|
-
|
|
164
|
+
Low-level connection wrapper (used internally):
|
|
165
|
+
- Manages WebRTC PeerConnection lifecycle
|
|
166
|
+
- Handles ICE candidate polling
|
|
167
|
+
- Provides message queue for reliability
|
|
168
|
+
- State management and events
|
|
128
169
|
|
|
129
|
-
|
|
130
|
-
await connection.close();
|
|
131
|
-
```
|
|
170
|
+
## API Reference
|
|
132
171
|
|
|
133
|
-
|
|
172
|
+
### RondevuService
|
|
134
173
|
|
|
135
|
-
|
|
174
|
+
```typescript
|
|
175
|
+
const service = new RondevuService({
|
|
176
|
+
apiUrl: string, // Signaling server URL
|
|
177
|
+
username: string, // Your username
|
|
178
|
+
keypair?: Keypair // Optional: reuse existing keypair
|
|
179
|
+
})
|
|
136
180
|
|
|
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
|
|
181
|
+
// Initialize service (generates keypair if not provided)
|
|
182
|
+
await service.initialize(): Promise<void>
|
|
142
183
|
|
|
143
|
-
|
|
184
|
+
// Claim username with cryptographic signature
|
|
185
|
+
await service.claimUsername(): Promise<void>
|
|
144
186
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
- Flushes queue on reconnection
|
|
148
|
-
- Configurable queue size and message age limits
|
|
149
|
-
- RTCDataChannel-compatible API with event emitters
|
|
187
|
+
// Check if username is claimed
|
|
188
|
+
service.isUsernameClaimed(): boolean
|
|
150
189
|
|
|
151
|
-
|
|
190
|
+
// Get current username
|
|
191
|
+
service.getUsername(): string
|
|
152
192
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
- Creates DurableConnection for each incoming peer
|
|
156
|
-
- Manages connection pool for multiple simultaneous connections
|
|
193
|
+
// Get keypair
|
|
194
|
+
service.getKeypair(): Keypair
|
|
157
195
|
|
|
158
|
-
|
|
196
|
+
// Get API client
|
|
197
|
+
service.getAPI(): RondevuAPI
|
|
198
|
+
```
|
|
159
199
|
|
|
160
|
-
###
|
|
200
|
+
### ServiceHost
|
|
161
201
|
|
|
162
202
|
```typescript
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
203
|
+
const host = new ServiceHost({
|
|
204
|
+
service: string, // Service FQN (e.g., 'chat.app@1.0.0')
|
|
205
|
+
rondevuService: RondevuService,
|
|
206
|
+
maxPeers?: number, // Default: 5
|
|
207
|
+
ttl?: number, // Default: 300000 (5 minutes)
|
|
208
|
+
isPublic?: boolean, // Default: true
|
|
209
|
+
rtcConfiguration?: RTCConfiguration
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
// Start hosting
|
|
213
|
+
await host.start(): Promise<void>
|
|
214
|
+
|
|
215
|
+
// Stop hosting and cleanup
|
|
216
|
+
host.dispose(): void
|
|
217
|
+
|
|
218
|
+
// Get all active connections
|
|
219
|
+
host.getConnections(): RTCDurableConnection[]
|
|
220
|
+
|
|
221
|
+
// Events
|
|
222
|
+
host.events.on('connection', (conn: RTCDurableConnection) => {})
|
|
223
|
+
host.events.on('error', (error: Error) => {})
|
|
181
224
|
```
|
|
182
225
|
|
|
183
|
-
###
|
|
226
|
+
### ServiceClient
|
|
184
227
|
|
|
185
228
|
```typescript
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
229
|
+
const client = new ServiceClient({
|
|
230
|
+
username: string, // Host username to connect to
|
|
231
|
+
serviceFqn: string, // Service FQN (e.g., 'chat.app@1.0.0')
|
|
232
|
+
rondevuService: RondevuService,
|
|
233
|
+
autoReconnect?: boolean, // Default: true
|
|
234
|
+
maxReconnectAttempts?: number, // Default: 5
|
|
235
|
+
rtcConfiguration?: RTCConfiguration
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
// Connect to service
|
|
239
|
+
await client.connect(): Promise<RTCDurableConnection>
|
|
240
|
+
|
|
241
|
+
// Disconnect and cleanup
|
|
242
|
+
client.dispose(): void
|
|
243
|
+
|
|
244
|
+
// Get current connection
|
|
245
|
+
client.getConnection(): RTCDurableConnection | null
|
|
246
|
+
|
|
247
|
+
// Events
|
|
248
|
+
client.events.on('connected', (conn: RTCDurableConnection) => {})
|
|
249
|
+
client.events.on('disconnected', () => {})
|
|
250
|
+
client.events.on('reconnecting', (info: { attempt: number, maxAttempts: number }) => {})
|
|
251
|
+
client.events.on('error', (error: Error) => {})
|
|
200
252
|
```
|
|
201
253
|
|
|
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
|
|
254
|
+
### RTCDurableConnection
|
|
210
255
|
|
|
211
256
|
```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
|
-
```
|
|
257
|
+
// Connection state
|
|
258
|
+
connection.state: 'connected' | 'connecting' | 'disconnected'
|
|
271
259
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
service.on('published', (serviceId, uuid) => {
|
|
275
|
-
console.log(`Service published: ${uuid}`);
|
|
276
|
-
});
|
|
260
|
+
// Send message (returns true if sent, false if queued)
|
|
261
|
+
await connection.sendMessage(message: string): Promise<boolean>
|
|
277
262
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
});
|
|
263
|
+
// Queue message for sending when connected
|
|
264
|
+
await connection.queueMessage(message: string, options?: QueueMessageOptions): Promise<void>
|
|
281
265
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
});
|
|
266
|
+
// Disconnect
|
|
267
|
+
connection.disconnect(): void
|
|
285
268
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
})
|
|
269
|
+
// Events
|
|
270
|
+
connection.events.on('message', (msg: string) => {})
|
|
271
|
+
connection.events.on('state-change', (state: ConnectionStates) => {})
|
|
272
|
+
```
|
|
289
273
|
|
|
290
|
-
|
|
291
|
-
console.error(`Service error (${context}):`, error);
|
|
292
|
-
});
|
|
274
|
+
## Configuration
|
|
293
275
|
|
|
294
|
-
|
|
295
|
-
console.log('Service stopped');
|
|
296
|
-
});
|
|
297
|
-
```
|
|
276
|
+
### Polling Configuration
|
|
298
277
|
|
|
299
|
-
|
|
278
|
+
The signaling uses configurable polling with exponential backoff:
|
|
300
279
|
|
|
301
280
|
```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();
|
|
281
|
+
// Default polling config
|
|
282
|
+
{
|
|
283
|
+
initialInterval: 500, // Start at 500ms
|
|
284
|
+
maxInterval: 5000, // Max 5 seconds
|
|
285
|
+
backoffMultiplier: 1.5, // Increase by 1.5x each time
|
|
286
|
+
maxRetries: 50, // Max 50 attempts
|
|
287
|
+
jitter: true // Add random 0-100ms to prevent thundering herd
|
|
288
|
+
}
|
|
349
289
|
```
|
|
350
290
|
|
|
351
|
-
|
|
352
|
-
```typescript
|
|
353
|
-
connection.on('state', (newState, previousState) => {
|
|
354
|
-
console.log(`State: ${previousState} → ${newState}`);
|
|
355
|
-
});
|
|
291
|
+
This is handled automatically - no configuration needed.
|
|
356
292
|
|
|
357
|
-
|
|
358
|
-
console.log('Connected');
|
|
359
|
-
});
|
|
293
|
+
### WebRTC Configuration
|
|
360
294
|
|
|
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
|
|
295
|
+
Provide custom STUN/TURN servers:
|
|
379
296
|
|
|
380
297
|
```typescript
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
298
|
+
const host = new ServiceHost({
|
|
299
|
+
service: 'chat.app@1.0.0',
|
|
300
|
+
rondevuService: service,
|
|
301
|
+
rtcConfiguration: {
|
|
302
|
+
iceServers: [
|
|
303
|
+
{ urls: 'stun:stun.l.google.com:19302' },
|
|
304
|
+
{
|
|
305
|
+
urls: 'turn:turn.example.com:3478',
|
|
306
|
+
username: 'user',
|
|
307
|
+
credential: 'pass'
|
|
308
|
+
}
|
|
309
|
+
]
|
|
310
|
+
}
|
|
311
|
+
})
|
|
312
|
+
```
|
|
385
313
|
|
|
386
|
-
|
|
387
|
-
channel.send('Hello!');
|
|
388
|
-
channel.send(new ArrayBuffer(1024));
|
|
389
|
-
channel.send(new Blob(['data']));
|
|
314
|
+
## Username Rules
|
|
390
315
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
316
|
+
- **Format**: Lowercase alphanumeric + dash (`a-z`, `0-9`, `-`)
|
|
317
|
+
- **Length**: 3-32 characters
|
|
318
|
+
- **Pattern**: `^[a-z0-9][a-z0-9-]*[a-z0-9]$`
|
|
319
|
+
- **Validity**: 365 days from claim/last use
|
|
320
|
+
- **Ownership**: Secured by Ed25519 public key signature
|
|
394
321
|
|
|
395
|
-
|
|
396
|
-
const buffered = channel.bufferedAmount;
|
|
322
|
+
## Examples
|
|
397
323
|
|
|
398
|
-
|
|
399
|
-
channel.bufferedAmountLowThreshold = 16 * 1024; // 16KB
|
|
324
|
+
### Chat Application
|
|
400
325
|
|
|
401
|
-
|
|
402
|
-
const queueSize = channel.getQueueSize();
|
|
326
|
+
See [demo/demo.js](./demo/demo.js) for a complete working example.
|
|
403
327
|
|
|
404
|
-
|
|
405
|
-
channel.close();
|
|
406
|
-
```
|
|
328
|
+
### Persistent Keypair
|
|
407
329
|
|
|
408
|
-
**Channel Events:**
|
|
409
330
|
```typescript
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
331
|
+
// Save keypair to localStorage
|
|
332
|
+
const service = new RondevuService({
|
|
333
|
+
apiUrl: 'https://api.ronde.vu',
|
|
334
|
+
username: 'alice'
|
|
335
|
+
})
|
|
413
336
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
});
|
|
337
|
+
await service.initialize()
|
|
338
|
+
await service.claimUsername()
|
|
417
339
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
340
|
+
// Save for later
|
|
341
|
+
localStorage.setItem('rondevu-keypair', JSON.stringify(service.getKeypair()))
|
|
342
|
+
localStorage.setItem('rondevu-username', service.getUsername())
|
|
421
343
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
344
|
+
// Load on next session
|
|
345
|
+
const savedKeypair = JSON.parse(localStorage.getItem('rondevu-keypair'))
|
|
346
|
+
const savedUsername = localStorage.getItem('rondevu-username')
|
|
425
347
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
348
|
+
const service2 = new RondevuService({
|
|
349
|
+
apiUrl: 'https://api.ronde.vu',
|
|
350
|
+
username: savedUsername,
|
|
351
|
+
keypair: savedKeypair
|
|
352
|
+
})
|
|
429
353
|
|
|
430
|
-
|
|
431
|
-
console.warn(`Queue overflow: ${droppedCount} messages dropped`);
|
|
432
|
-
});
|
|
354
|
+
await service2.initialize() // Reuses keypair
|
|
433
355
|
```
|
|
434
356
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
### Connection Configuration
|
|
357
|
+
### Message Queue Example
|
|
438
358
|
|
|
439
359
|
```typescript
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
360
|
+
// Messages are automatically queued if not connected yet
|
|
361
|
+
client.events.on('connected', (connection) => {
|
|
362
|
+
// Send immediately
|
|
363
|
+
connection.sendMessage('Hello!')
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
// Or queue for later
|
|
367
|
+
await client.connect()
|
|
368
|
+
const conn = client.getConnection()
|
|
369
|
+
await conn.queueMessage('This will be sent when connected', {
|
|
370
|
+
expiresAt: Date.now() + 60000 // Expire after 1 minute
|
|
371
|
+
})
|
|
450
372
|
```
|
|
451
373
|
|
|
452
|
-
|
|
374
|
+
## Migration from v0.9.x
|
|
453
375
|
|
|
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
|
-
```
|
|
376
|
+
v0.11.0 introduces high-level wrappers and RESTful API changes:
|
|
467
377
|
|
|
468
|
-
|
|
378
|
+
**API Changes:**
|
|
379
|
+
- Server endpoints restructured (`/usernames/*` → `/users/*`)
|
|
380
|
+
- Added `ServiceHost` and `ServiceClient` wrappers
|
|
381
|
+
- Message queue fully implemented
|
|
382
|
+
- Configurable polling with exponential backoff
|
|
383
|
+
- Removed deprecated `cleanup()` methods (use `dispose()`)
|
|
469
384
|
|
|
470
|
-
|
|
385
|
+
**Migration Guide:**
|
|
471
386
|
|
|
472
387
|
```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!');
|
|
388
|
+
// Before (v0.9.x) - Manual WebRTC setup
|
|
389
|
+
const signaler = new RondevuSignaler(service, 'chat@1.0.0')
|
|
390
|
+
const context = new WebRTCContext()
|
|
391
|
+
const pc = context.createPeerConnection()
|
|
392
|
+
// ... 50+ lines of boilerplate
|
|
393
|
+
|
|
394
|
+
// After (v0.11.0) - ServiceHost wrapper
|
|
395
|
+
const host = new ServiceHost({
|
|
396
|
+
service: 'chat@1.0.0',
|
|
397
|
+
rondevuService: service
|
|
398
|
+
})
|
|
399
|
+
await host.start()
|
|
400
|
+
// Done!
|
|
516
401
|
```
|
|
517
402
|
|
|
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
|
|
403
|
+
## Platform Support
|
|
575
404
|
|
|
576
405
|
### Modern Browsers
|
|
577
406
|
Works out of the box - no additional setup needed.
|
|
578
407
|
|
|
579
408
|
### Node.js 18+
|
|
580
|
-
Native fetch is available, but
|
|
409
|
+
Native fetch is available, but WebRTC requires polyfills:
|
|
581
410
|
|
|
582
411
|
```bash
|
|
583
412
|
npm install wrtc
|
|
584
413
|
```
|
|
585
414
|
|
|
586
415
|
```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
|
-
});
|
|
416
|
+
import { WebRTCContext } from '@xtr-dev/rondevu-client'
|
|
417
|
+
import { RTCPeerConnection, RTCSessionDescription, RTCIceCandidate } from 'wrtc'
|
|
418
|
+
|
|
419
|
+
// Configure WebRTC context
|
|
420
|
+
const context = new WebRTCContext({
|
|
421
|
+
RTCPeerConnection,
|
|
422
|
+
RTCSessionDescription,
|
|
423
|
+
RTCIceCandidate
|
|
424
|
+
} as any)
|
|
617
425
|
```
|
|
618
426
|
|
|
619
427
|
## TypeScript
|
|
@@ -622,38 +430,23 @@ All types are exported:
|
|
|
622
430
|
|
|
623
431
|
```typescript
|
|
624
432
|
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';
|
|
433
|
+
RondevuServiceOptions,
|
|
434
|
+
ServiceHostOptions,
|
|
435
|
+
ServiceHostEvents,
|
|
436
|
+
ServiceClientOptions,
|
|
437
|
+
ServiceClientEvents,
|
|
438
|
+
ConnectionInterface,
|
|
439
|
+
ConnectionEvents,
|
|
440
|
+
ConnectionStates,
|
|
441
|
+
Message,
|
|
442
|
+
QueueMessageOptions,
|
|
443
|
+
Signaler,
|
|
444
|
+
PollingConfig,
|
|
445
|
+
Credentials,
|
|
446
|
+
Keypair
|
|
447
|
+
} from '@xtr-dev/rondevu-client'
|
|
646
448
|
```
|
|
647
449
|
|
|
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
450
|
## License
|
|
658
451
|
|
|
659
452
|
MIT
|