@xtr-dev/rondevu-client 0.20.1 → 0.21.3
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 +83 -385
- package/dist/api/batcher.d.ts +60 -38
- package/dist/api/batcher.js +121 -77
- package/dist/api/client.d.ts +104 -61
- package/dist/api/client.js +273 -185
- package/dist/connections/answerer.d.ts +15 -6
- package/dist/connections/answerer.js +56 -19
- package/dist/connections/base.d.ts +6 -4
- package/dist/connections/base.js +26 -16
- package/dist/connections/config.d.ts +30 -0
- package/dist/connections/config.js +20 -0
- package/dist/connections/events.d.ts +6 -6
- package/dist/connections/offerer.d.ts +37 -8
- package/dist/connections/offerer.js +92 -24
- package/dist/core/ice-config.d.ts +35 -0
- package/dist/core/ice-config.js +111 -0
- package/dist/core/index.d.ts +18 -18
- package/dist/core/index.js +18 -13
- package/dist/core/offer-pool.d.ts +30 -11
- package/dist/core/offer-pool.js +90 -76
- package/dist/core/peer.d.ts +158 -0
- package/dist/core/peer.js +254 -0
- package/dist/core/polling-manager.d.ts +71 -0
- package/dist/core/polling-manager.js +122 -0
- package/dist/core/rondevu-errors.d.ts +59 -0
- package/dist/core/rondevu-errors.js +75 -0
- package/dist/core/rondevu-types.d.ts +125 -0
- package/dist/core/rondevu-types.js +6 -0
- package/dist/core/rondevu.d.ts +106 -209
- package/dist/core/rondevu.js +222 -349
- package/dist/crypto/adapter.d.ts +25 -9
- package/dist/crypto/node.d.ts +27 -5
- package/dist/crypto/node.js +96 -25
- package/dist/crypto/web.d.ts +26 -4
- package/dist/crypto/web.js +102 -25
- package/dist/utils/message-buffer.js +4 -4
- package/dist/webrtc/adapter.d.ts +22 -0
- package/dist/webrtc/adapter.js +5 -0
- package/dist/webrtc/browser.d.ts +12 -0
- package/dist/webrtc/browser.js +15 -0
- package/dist/webrtc/node.d.ts +32 -0
- package/dist/webrtc/node.js +32 -0
- package/package.json +17 -6
package/README.md
CHANGED
|
@@ -2,37 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@xtr-dev/rondevu-client)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**WebRTC signaling client with durable connections**
|
|
6
6
|
|
|
7
|
-
TypeScript
|
|
8
|
-
|
|
9
|
-
**Related repositories:**
|
|
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))
|
|
11
|
-
- [@xtr-dev/rondevu-server](https://github.com/xtr-dev/rondevu-server) - HTTP signaling server ([npm](https://www.npmjs.com/package/@xtr-dev/rondevu-server), [live](https://api.ronde.vu))
|
|
12
|
-
- [@xtr-dev/rondevu-demo](https://github.com/xtr-dev/rondevu-demo) - Interactive demo ([live](https://ronde.vu))
|
|
13
|
-
|
|
14
|
-
---
|
|
7
|
+
TypeScript client for [Rondevu](https://github.com/xtr-dev/rondevu-server), providing WebRTC signaling with automatic reconnection, message buffering, and tags-based discovery.
|
|
15
8
|
|
|
16
9
|
## Features
|
|
17
10
|
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
- **📊 Connection State Machine**: Explicit lifecycle tracking with native RTC events
|
|
23
|
-
- **🎯 Rich Event System**: 20+ events for monitoring connection health including `connection:rotated`
|
|
24
|
-
- **⚡ Improved Reliability**: ICE polling lifecycle management, proper cleanup, rotation fallback
|
|
25
|
-
- **🏗️ Internal Refactoring**: Cleaner codebase with OfferPool extraction and consolidated ICE polling
|
|
26
|
-
|
|
27
|
-
### Core Features
|
|
28
|
-
- **Username Claiming**: Secure ownership with Ed25519 signatures
|
|
29
|
-
- **Anonymous Users**: Auto-generated anonymous usernames for quick testing
|
|
30
|
-
- **Service Publishing**: Publish services with multiple offers for connection pooling
|
|
31
|
-
- **Service Discovery**: Direct lookup, random discovery, or paginated search
|
|
32
|
-
- **Efficient Batch Polling**: Single endpoint for answers and ICE candidates
|
|
33
|
-
- **Semantic Version Matching**: Compatible version resolution (chat:1.0.0 matches any 1.x.x)
|
|
34
|
-
- **TypeScript**: Full type safety and autocomplete
|
|
35
|
-
- **Keypair Management**: Generate or reuse Ed25519 keypairs
|
|
11
|
+
- **Simple Peer API**: Connect with `rondevu.peer({ tags, username })`
|
|
12
|
+
- **Tags-Based Discovery**: Find peers using tags (e.g., `["chat", "video"]`)
|
|
13
|
+
- **Automatic Reconnection**: Built-in exponential backoff
|
|
14
|
+
- **Message Buffering**: Queues messages during disconnections
|
|
36
15
|
|
|
37
16
|
## Installation
|
|
38
17
|
|
|
@@ -42,412 +21,131 @@ npm install @xtr-dev/rondevu-client
|
|
|
42
21
|
|
|
43
22
|
## Quick Start
|
|
44
23
|
|
|
45
|
-
### Publishing a Service (Offerer)
|
|
46
|
-
|
|
47
24
|
```typescript
|
|
48
25
|
import { Rondevu } from '@xtr-dev/rondevu-client'
|
|
49
26
|
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
iceServers: 'ipv4-turn' // Preset: 'ipv4-turn', 'hostname-turns', 'google-stun', 'relay-only'
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
// 2. Publish service with automatic offer management
|
|
58
|
-
await rondevu.publishService({
|
|
59
|
-
service: 'chat:1.0.0',
|
|
60
|
-
maxOffers: 5, // Maintain up to 5 concurrent offers
|
|
61
|
-
connectionConfig: {
|
|
62
|
-
reconnectEnabled: true, // Auto-reconnect on failures
|
|
63
|
-
bufferEnabled: true, // Buffer messages during disconnections
|
|
64
|
-
connectionTimeout: 30000 // 30 second timeout
|
|
65
|
-
}
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
// 3. Start accepting connections
|
|
69
|
-
await rondevu.startFilling()
|
|
70
|
-
|
|
71
|
-
// 4. Handle incoming connections
|
|
72
|
-
rondevu.on('connection:opened', (offerId, connection) => {
|
|
73
|
-
console.log('New connection:', offerId)
|
|
74
|
-
|
|
75
|
-
// Listen for messages
|
|
76
|
-
connection.on('message', (data) => {
|
|
77
|
-
console.log('Received:', data)
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
// Monitor connection state
|
|
81
|
-
connection.on('connected', () => {
|
|
82
|
-
console.log('Fully connected!')
|
|
83
|
-
connection.send('Hello from Alice!')
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
connection.on('disconnected', () => {
|
|
87
|
-
console.log('Connection lost, will auto-reconnect')
|
|
88
|
-
})
|
|
89
|
-
})
|
|
90
|
-
```
|
|
27
|
+
// ============================================
|
|
28
|
+
// ALICE: Host and wait for connections
|
|
29
|
+
// ============================================
|
|
30
|
+
const alice = await Rondevu.connect({ username: 'alice' })
|
|
91
31
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
// 1. Connect to Rondevu
|
|
98
|
-
const rondevu = await Rondevu.connect({
|
|
99
|
-
apiUrl: 'https://api.ronde.vu',
|
|
100
|
-
username: 'bob',
|
|
101
|
-
iceServers: 'ipv4-turn'
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
// 2. Connect to service - returns AnswererConnection
|
|
105
|
-
const connection = await rondevu.connectToService({
|
|
106
|
-
serviceFqn: 'chat:1.0.0@alice',
|
|
107
|
-
connectionConfig: {
|
|
108
|
-
reconnectEnabled: true,
|
|
109
|
-
bufferEnabled: true,
|
|
110
|
-
maxReconnectAttempts: 5
|
|
111
|
-
}
|
|
32
|
+
alice.on('connection:opened', (offerId, connection) => {
|
|
33
|
+
console.log('Connected to', connection.peerUsername)
|
|
34
|
+
connection.on('message', (data) => console.log('Received:', data))
|
|
35
|
+
connection.send('Hello!')
|
|
112
36
|
})
|
|
113
37
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
console.log('Connected to alice!')
|
|
117
|
-
connection.send('Hello from Bob!')
|
|
118
|
-
})
|
|
38
|
+
const offer = await alice.offer({ tags: ['chat'], maxOffers: 5 })
|
|
39
|
+
// Later: offer.cancel() to stop accepting connections
|
|
119
40
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
41
|
+
// ============================================
|
|
42
|
+
// BOB: Connect to Alice
|
|
43
|
+
// ============================================
|
|
44
|
+
const bob = await Rondevu.connect()
|
|
123
45
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
46
|
+
const peer = await bob.peer({
|
|
47
|
+
username: 'alice',
|
|
48
|
+
tags: ['chat']
|
|
127
49
|
})
|
|
128
50
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
connection.on('failed', (error) => {
|
|
134
|
-
console.error('Connection failed:', error)
|
|
135
|
-
})
|
|
51
|
+
peer.on('open', () => peer.send('Hello Alice!'))
|
|
52
|
+
peer.on('message', (data) => console.log('Received:', data))
|
|
136
53
|
```
|
|
137
54
|
|
|
138
|
-
##
|
|
55
|
+
## API Reference
|
|
139
56
|
|
|
140
57
|
### Rondevu.connect()
|
|
141
58
|
|
|
142
59
|
```typescript
|
|
143
60
|
const rondevu = await Rondevu.connect({
|
|
144
|
-
apiUrl
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
iceServers?: IceServerPreset | RTCIceServer[], //
|
|
148
|
-
debug?: boolean
|
|
61
|
+
apiUrl?: string, // Default: 'https://api.ronde.vu'
|
|
62
|
+
credential?: Credential, // Reuse existing credential
|
|
63
|
+
username?: string, // Claim username (4-32 chars)
|
|
64
|
+
iceServers?: IceServerPreset | RTCIceServer[], // Default: 'rondevu'
|
|
65
|
+
debug?: boolean
|
|
149
66
|
})
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
### Service Publishing
|
|
153
67
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
service: string, // e.g., 'chat:1.0.0' (username auto-appended)
|
|
157
|
-
maxOffers: number, // Maximum concurrent offers to maintain
|
|
158
|
-
offerFactory?: OfferFactory, // Optional: custom offer creation
|
|
159
|
-
ttl?: number, // Optional: offer lifetime in ms (default: 300000)
|
|
160
|
-
connectionConfig?: Partial<ConnectionConfig> // Optional: durability settings
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
await rondevu.startFilling() // Start accepting connections
|
|
164
|
-
rondevu.stopFilling() // Stop and close all connections
|
|
68
|
+
rondevu.getName() // Get username
|
|
69
|
+
rondevu.getCredential() // Get credential for reuse
|
|
165
70
|
```
|
|
166
71
|
|
|
167
|
-
|
|
72
|
+
**ICE Presets**: `'rondevu'` (default), `'rondevu-relay'`, `'google-stun'`, `'public-stun'`
|
|
168
73
|
|
|
169
|
-
|
|
74
|
+
### rondevu.peer()
|
|
170
75
|
|
|
171
76
|
```typescript
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
### Connection Configuration
|
|
192
|
-
|
|
193
|
-
```typescript
|
|
194
|
-
interface ConnectionConfig {
|
|
195
|
-
// Timeouts
|
|
196
|
-
connectionTimeout: number // Default: 30000ms (30s)
|
|
197
|
-
iceGatheringTimeout: number // Default: 10000ms (10s)
|
|
198
|
-
|
|
199
|
-
// Reconnection
|
|
200
|
-
reconnectEnabled: boolean // Default: true
|
|
201
|
-
maxReconnectAttempts: number // Default: 5 (0 = infinite)
|
|
202
|
-
reconnectBackoffBase: number // Default: 1000ms
|
|
203
|
-
reconnectBackoffMax: number // Default: 30000ms (30s)
|
|
204
|
-
|
|
205
|
-
// Message buffering
|
|
206
|
-
bufferEnabled: boolean // Default: true
|
|
207
|
-
maxBufferSize: number // Default: 100 messages
|
|
208
|
-
maxBufferAge: number // Default: 60000ms (1 min)
|
|
209
|
-
|
|
210
|
-
// Debug
|
|
211
|
-
debug: boolean // Default: false
|
|
212
|
-
}
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
### Connection Events
|
|
216
|
-
|
|
217
|
-
```typescript
|
|
218
|
-
// Lifecycle events
|
|
219
|
-
connection.on('connecting', () => {})
|
|
220
|
-
connection.on('connected', () => {})
|
|
221
|
-
connection.on('disconnected', (reason) => {})
|
|
222
|
-
connection.on('failed', (error) => {})
|
|
223
|
-
connection.on('closed', (reason) => {})
|
|
224
|
-
|
|
225
|
-
// Reconnection events
|
|
226
|
-
connection.on('reconnecting', (attempt) => {})
|
|
227
|
-
connection.on('reconnect:success', () => {})
|
|
228
|
-
connection.on('reconnect:failed', (error) => {})
|
|
229
|
-
connection.on('reconnect:exhausted', (attempts) => {})
|
|
230
|
-
|
|
231
|
-
// Message events
|
|
232
|
-
connection.on('message', (data) => {})
|
|
233
|
-
connection.on('message:buffered', (data) => {})
|
|
234
|
-
connection.on('message:replayed', (message) => {})
|
|
235
|
-
|
|
236
|
-
// ICE events
|
|
237
|
-
connection.on('ice:connection:state', (state) => {})
|
|
238
|
-
connection.on('ice:polling:started', () => {})
|
|
239
|
-
connection.on('ice:polling:stopped', () => {})
|
|
77
|
+
const peer = await rondevu.peer({
|
|
78
|
+
tags: string[],
|
|
79
|
+
username?: string,
|
|
80
|
+
rtcConfig?: RTCConfiguration
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// Events
|
|
84
|
+
peer.on('open', () => {})
|
|
85
|
+
peer.on('close', (reason) => {})
|
|
86
|
+
peer.on('message', (data) => {})
|
|
87
|
+
peer.on('error', (error) => {})
|
|
88
|
+
peer.on('reconnecting', (attempt, max) => {})
|
|
89
|
+
|
|
90
|
+
// Properties & Methods
|
|
91
|
+
peer.state // 'connecting' | 'connected' | 'reconnecting' | ...
|
|
92
|
+
peer.peerUsername
|
|
93
|
+
peer.send(data)
|
|
94
|
+
peer.close()
|
|
240
95
|
```
|
|
241
96
|
|
|
242
|
-
###
|
|
243
|
-
|
|
244
|
-
```typescript
|
|
245
|
-
// Unified discovery API
|
|
246
|
-
const service = await rondevu.findService(
|
|
247
|
-
'chat:1.0.0@alice', // Direct lookup (with username)
|
|
248
|
-
{ mode: 'direct' }
|
|
249
|
-
)
|
|
250
|
-
|
|
251
|
-
const service = await rondevu.findService(
|
|
252
|
-
'chat:1.0.0', // Random discovery (without username)
|
|
253
|
-
{ mode: 'random' }
|
|
254
|
-
)
|
|
255
|
-
|
|
256
|
-
const result = await rondevu.findService(
|
|
257
|
-
'chat:1.0.0',
|
|
258
|
-
{
|
|
259
|
-
mode: 'paginated',
|
|
260
|
-
limit: 20,
|
|
261
|
-
offset: 0
|
|
262
|
-
}
|
|
263
|
-
)
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
## Migration Guide
|
|
267
|
-
|
|
268
|
-
**Upgrading from v0.18.10 or earlier?** See [MIGRATION.md](./MIGRATION.md) for detailed upgrade instructions.
|
|
269
|
-
|
|
270
|
-
### Quick Migration Summary
|
|
271
|
-
|
|
272
|
-
**Before (v0.18.7/v0.18.10):**
|
|
273
|
-
```typescript
|
|
274
|
-
const context = await rondevu.connectToService({
|
|
275
|
-
serviceFqn: 'chat:1.0.0@alice',
|
|
276
|
-
onConnection: ({ dc }) => {
|
|
277
|
-
dc.addEventListener('message', (e) => console.log(e.data))
|
|
278
|
-
dc.send('Hello')
|
|
279
|
-
}
|
|
280
|
-
})
|
|
281
|
-
```
|
|
97
|
+
### rondevu.offer()
|
|
282
98
|
|
|
283
|
-
**After (v0.18.9/v0.18.11):**
|
|
284
99
|
```typescript
|
|
285
|
-
const
|
|
286
|
-
|
|
100
|
+
const offer = await rondevu.offer({
|
|
101
|
+
tags: string[],
|
|
102
|
+
maxOffers: number,
|
|
103
|
+
ttl?: number, // Offer lifetime in ms (default: 300000)
|
|
104
|
+
autoStart?: boolean // Auto-start filling (default: true)
|
|
287
105
|
})
|
|
288
106
|
|
|
289
|
-
|
|
290
|
-
connection.send('Hello') // Use connection.send()
|
|
291
|
-
})
|
|
107
|
+
offer.cancel() // Stop accepting connections
|
|
292
108
|
|
|
293
|
-
|
|
294
|
-
|
|
109
|
+
rondevu.on('connection:opened', (offerId, connection) => {
|
|
110
|
+
connection.on('message', (data) => {})
|
|
111
|
+
connection.send('Hello!')
|
|
295
112
|
})
|
|
296
113
|
```
|
|
297
114
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
### Custom Offer Factory
|
|
115
|
+
### rondevu.discover()
|
|
301
116
|
|
|
302
117
|
```typescript
|
|
303
|
-
await rondevu.
|
|
304
|
-
|
|
305
|
-
maxOffers: 3,
|
|
306
|
-
offerFactory: async (pc) => {
|
|
307
|
-
// Customize data channel settings
|
|
308
|
-
const dc = pc.createDataChannel('files', {
|
|
309
|
-
ordered: true,
|
|
310
|
-
maxRetransmits: 10
|
|
311
|
-
})
|
|
312
|
-
|
|
313
|
-
// Add custom listeners
|
|
314
|
-
dc.addEventListener('open', () => {
|
|
315
|
-
console.log('Transfer channel ready')
|
|
316
|
-
})
|
|
317
|
-
|
|
318
|
-
const offer = await pc.createOffer()
|
|
319
|
-
await pc.setLocalDescription(offer)
|
|
320
|
-
return { dc, offer }
|
|
321
|
-
}
|
|
322
|
-
})
|
|
118
|
+
const result = await rondevu.discover(['chat'], { limit: 20 })
|
|
119
|
+
result.offers.forEach(o => console.log(o.username, o.tags))
|
|
323
120
|
```
|
|
324
121
|
|
|
325
|
-
|
|
122
|
+
## Credentials
|
|
326
123
|
|
|
327
124
|
```typescript
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
//
|
|
331
|
-
const pc = connection.getPeerConnection()
|
|
332
|
-
const dc = connection.getDataChannel()
|
|
333
|
-
|
|
334
|
-
// Note: Using raw DataChannel bypasses buffering/reconnection features
|
|
335
|
-
if (dc) {
|
|
336
|
-
dc.addEventListener('message', (e) => {
|
|
337
|
-
console.log('Raw message:', e.data)
|
|
338
|
-
})
|
|
339
|
-
}
|
|
340
|
-
```
|
|
125
|
+
// Auto-generated username
|
|
126
|
+
const rondevu = await Rondevu.connect()
|
|
127
|
+
// rondevu.getName() === 'friendly-panda-a1b2c3'
|
|
341
128
|
|
|
342
|
-
|
|
129
|
+
// Claimed username
|
|
130
|
+
const rondevu = await Rondevu.connect({ username: 'alice' })
|
|
343
131
|
|
|
344
|
-
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
connectionConfig: {
|
|
348
|
-
reconnectEnabled: false, // Disable auto-reconnect
|
|
349
|
-
bufferEnabled: false, // Disable message buffering
|
|
350
|
-
}
|
|
351
|
-
})
|
|
352
|
-
```
|
|
132
|
+
// Save and restore credentials
|
|
133
|
+
const credential = rondevu.getCredential()
|
|
134
|
+
localStorage.setItem('cred', JSON.stringify(credential))
|
|
353
135
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
136
|
+
const saved = JSON.parse(localStorage.getItem('cred'))
|
|
137
|
+
const rondevu = await Rondevu.connect({ credential: saved })
|
|
138
|
+
```
|
|
357
139
|
|
|
358
|
-
|
|
359
|
-
- Detailed API reference for all methods
|
|
360
|
-
- Type definitions and interfaces
|
|
361
|
-
- Platform support (Browser & Node.js)
|
|
362
|
-
- Advanced usage patterns
|
|
363
|
-
- Username rules and service FQN format
|
|
140
|
+
## Tag Validation
|
|
364
141
|
|
|
365
|
-
|
|
142
|
+
Tags: 1-64 chars, lowercase alphanumeric with dots/dashes.
|
|
366
143
|
|
|
367
|
-
|
|
144
|
+
Valid: `chat`, `video-call`, `com.example.service`
|
|
368
145
|
|
|
369
|
-
|
|
370
|
-
rondevu.on('connection:opened', (offerId, connection) => {
|
|
371
|
-
console.log(`Connection ${offerId} opened`)
|
|
372
|
-
|
|
373
|
-
// Listen for offer rotation
|
|
374
|
-
rondevu.on('connection:rotated', (oldOfferId, newOfferId, conn) => {
|
|
375
|
-
if (conn === connection) {
|
|
376
|
-
console.log(`Connection rotated: ${oldOfferId} → ${newOfferId}`)
|
|
377
|
-
// Same connection object! Event listeners still work
|
|
378
|
-
// Message buffer preserved
|
|
379
|
-
}
|
|
380
|
-
})
|
|
381
|
-
|
|
382
|
-
connection.on('message', (data) => {
|
|
383
|
-
console.log('Received:', data)
|
|
384
|
-
// This listener continues working even after rotation
|
|
385
|
-
})
|
|
386
|
-
|
|
387
|
-
connection.on('failed', () => {
|
|
388
|
-
console.log('Connection failed, will auto-rotate to new offer')
|
|
389
|
-
})
|
|
390
|
-
})
|
|
391
|
-
```
|
|
146
|
+
## Links
|
|
392
147
|
|
|
393
|
-
|
|
394
|
-
- ✅ Same connection object remains usable through disconnections
|
|
395
|
-
- ✅ Message buffer preserved during temporary disconnections
|
|
396
|
-
- ✅ Event listeners don't need to be re-registered
|
|
397
|
-
- ✅ Seamless reconnection experience for offerer side
|
|
398
|
-
|
|
399
|
-
## Examples
|
|
400
|
-
|
|
401
|
-
- [React Demo](https://github.com/xtr-dev/rondevu-demo) - Full browser UI ([live](https://ronde.vu))
|
|
402
|
-
|
|
403
|
-
## Changelog
|
|
404
|
-
|
|
405
|
-
### v0.20.0 (Latest)
|
|
406
|
-
- **Connection Persistence** - OffererConnection objects now persist across disconnections
|
|
407
|
-
- **Offer Rotation** - When connection fails, same object is rebound to new offer
|
|
408
|
-
- **Message Buffering** - Now works seamlessly on offerer side through rotations
|
|
409
|
-
- **New Event**: `connection:rotated` emitted when offer is rotated
|
|
410
|
-
- **Internal**: Added `OffererConnection.rebindToOffer()` method
|
|
411
|
-
- **Internal**: Modified OfferPool failure handler to rotate offers instead of destroying connections
|
|
412
|
-
- **Internal**: Added rotation lock to prevent concurrent rotations
|
|
413
|
-
- **Internal**: Added max rotation attempts limit (default: 5)
|
|
414
|
-
- 100% backward compatible - no breaking changes
|
|
415
|
-
|
|
416
|
-
### v0.19.0
|
|
417
|
-
- **Internal Refactoring** - Improved codebase maintainability (no API changes)
|
|
418
|
-
- Extract OfferPool class for offer lifecycle management
|
|
419
|
-
- Consolidate ICE polling logic (remove ~86 lines of duplicate code)
|
|
420
|
-
- Add AsyncLock utility for race-free concurrent operations
|
|
421
|
-
- Disable reconnection for offerer connections (offers are ephemeral)
|
|
422
|
-
- 100% backward compatible - upgrade without code changes
|
|
423
|
-
|
|
424
|
-
### v0.18.11
|
|
425
|
-
- Restore EventEmitter-based durable connections (same as v0.18.9)
|
|
426
|
-
- Durable WebRTC connections with state machine
|
|
427
|
-
- Automatic reconnection with exponential backoff
|
|
428
|
-
- Message buffering during disconnections
|
|
429
|
-
- ICE polling lifecycle management
|
|
430
|
-
- **Breaking:** `connectToService()` returns `AnswererConnection` instead of `ConnectionContext`
|
|
431
|
-
- See [MIGRATION.md](./MIGRATION.md) for upgrade guide
|
|
432
|
-
|
|
433
|
-
### v0.18.10
|
|
434
|
-
- Temporary revert to callback-based API (reverted in v0.18.11)
|
|
435
|
-
|
|
436
|
-
### v0.18.9
|
|
437
|
-
- Add durable WebRTC connections with state machine
|
|
438
|
-
- Implement automatic reconnection with exponential backoff
|
|
439
|
-
- Add message buffering during disconnections
|
|
440
|
-
- Fix ICE polling lifecycle (stops when connected)
|
|
441
|
-
- Add fillOffers() semaphore to prevent exceeding maxOffers
|
|
442
|
-
- **Breaking:** `connectToService()` returns `AnswererConnection` instead of `ConnectionContext`
|
|
443
|
-
- **Breaking:** `connection:opened` event signature changed
|
|
444
|
-
- See [MIGRATION.md](./MIGRATION.md) for upgrade guide
|
|
445
|
-
|
|
446
|
-
### v0.18.8
|
|
447
|
-
- Initial durable connections implementation
|
|
448
|
-
|
|
449
|
-
### v0.18.3
|
|
450
|
-
- Fix EventEmitter cross-platform compatibility
|
|
148
|
+
- [Live Demo](https://ronde.vu) | [Server](https://github.com/xtr-dev/rondevu-server) | [API](https://api.ronde.vu)
|
|
451
149
|
|
|
452
150
|
## License
|
|
453
151
|
|
package/dist/api/batcher.d.ts
CHANGED
|
@@ -1,61 +1,83 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* RPC Batcher
|
|
2
|
+
* RPC Request Batcher with throttling
|
|
3
|
+
*
|
|
4
|
+
* Collects RPC requests over a short time window and sends them efficiently.
|
|
5
|
+
*
|
|
6
|
+
* Due to server authentication design (signature covers method+params),
|
|
7
|
+
* authenticated requests are sent individually while unauthenticated
|
|
8
|
+
* requests can be truly batched together.
|
|
3
9
|
*/
|
|
10
|
+
export interface RpcRequest {
|
|
11
|
+
method: string;
|
|
12
|
+
params?: any;
|
|
13
|
+
}
|
|
14
|
+
export interface RpcResponse {
|
|
15
|
+
success: boolean;
|
|
16
|
+
result?: any;
|
|
17
|
+
error?: string;
|
|
18
|
+
errorCode?: string;
|
|
19
|
+
}
|
|
4
20
|
export interface BatcherOptions {
|
|
5
|
-
/**
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
*/
|
|
21
|
+
/** Delay in ms before flushing queued requests (default: 10) */
|
|
22
|
+
delay?: number;
|
|
23
|
+
/** Maximum batch size for unauthenticated requests (default: 50) */
|
|
9
24
|
maxBatchSize?: number;
|
|
10
|
-
/**
|
|
11
|
-
* Maximum time to wait before sending a batch (ms)
|
|
12
|
-
* Default: 50ms
|
|
13
|
-
*/
|
|
14
|
-
maxWaitTime?: number;
|
|
15
|
-
/**
|
|
16
|
-
* Minimum time between batches (ms)
|
|
17
|
-
* Default: 10ms
|
|
18
|
-
*/
|
|
19
|
-
throttleInterval?: number;
|
|
20
25
|
}
|
|
21
26
|
/**
|
|
22
|
-
*
|
|
27
|
+
* RpcBatcher - Batches RPC requests with throttling
|
|
23
28
|
*
|
|
24
29
|
* @example
|
|
25
30
|
* ```typescript
|
|
26
|
-
* const batcher = new RpcBatcher(
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* )
|
|
31
|
+
* const batcher = new RpcBatcher('https://api.example.com', {
|
|
32
|
+
* delay: 10,
|
|
33
|
+
* maxBatchSize: 50
|
|
34
|
+
* })
|
|
30
35
|
*
|
|
31
|
-
* //
|
|
32
|
-
* const result1 = await
|
|
33
|
-
*
|
|
34
|
-
*
|
|
36
|
+
* // Requests made within the delay window are batched
|
|
37
|
+
* const [result1, result2] = await Promise.all([
|
|
38
|
+
* batcher.add({ method: 'getOffer', params: {...} }, null),
|
|
39
|
+
* batcher.add({ method: 'getOffer', params: {...} }, null)
|
|
40
|
+
* ])
|
|
35
41
|
* ```
|
|
36
42
|
*/
|
|
37
43
|
export declare class RpcBatcher {
|
|
44
|
+
private readonly baseUrl;
|
|
38
45
|
private queue;
|
|
39
|
-
private
|
|
40
|
-
private
|
|
41
|
-
private
|
|
42
|
-
|
|
43
|
-
constructor(sendBatch: (requests: any[]) => Promise<any[]>, options?: BatcherOptions);
|
|
46
|
+
private flushTimer;
|
|
47
|
+
private readonly delay;
|
|
48
|
+
private readonly maxBatchSize;
|
|
49
|
+
constructor(baseUrl: string, options?: BatcherOptions);
|
|
44
50
|
/**
|
|
45
|
-
* Add
|
|
46
|
-
*
|
|
51
|
+
* Add a request to the batch queue
|
|
52
|
+
* @param request - The RPC request
|
|
53
|
+
* @param authHeaders - Auth headers for authenticated requests, null for unauthenticated
|
|
54
|
+
* @returns Promise that resolves with the request result
|
|
47
55
|
*/
|
|
48
|
-
add(request:
|
|
56
|
+
add(request: RpcRequest, authHeaders: Record<string, string> | null): Promise<any>;
|
|
49
57
|
/**
|
|
50
|
-
*
|
|
58
|
+
* Schedule a flush after the delay
|
|
51
59
|
*/
|
|
52
|
-
|
|
60
|
+
private scheduleFlush;
|
|
53
61
|
/**
|
|
54
|
-
*
|
|
62
|
+
* Flush all queued requests
|
|
55
63
|
*/
|
|
56
|
-
|
|
64
|
+
private flush;
|
|
65
|
+
/**
|
|
66
|
+
* Process unauthenticated requests in batches
|
|
67
|
+
*/
|
|
68
|
+
private processUnauthenticatedBatches;
|
|
69
|
+
/**
|
|
70
|
+
* Process authenticated requests individually
|
|
71
|
+
* Each authenticated request needs its own HTTP call because
|
|
72
|
+
* the signature covers the specific method+params
|
|
73
|
+
*/
|
|
74
|
+
private processAuthenticatedRequests;
|
|
75
|
+
/**
|
|
76
|
+
* Send a batch of requests
|
|
77
|
+
*/
|
|
78
|
+
private sendBatch;
|
|
57
79
|
/**
|
|
58
|
-
*
|
|
80
|
+
* Flush immediately (useful for cleanup/testing)
|
|
59
81
|
*/
|
|
60
|
-
|
|
82
|
+
flushNow(): Promise<void>;
|
|
61
83
|
}
|