@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/dist/service-host.js
CHANGED
|
@@ -1,185 +1,120 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RondevuSignaler } from './rondevu-signaler.js';
|
|
2
2
|
import { WebRTCContext } from './webrtc-context.js';
|
|
3
|
-
import {
|
|
4
|
-
import { NoOpSignaler } from './noop-signaler.js';
|
|
3
|
+
import { RTCDurableConnection } from './durable-connection.js';
|
|
5
4
|
import { EventBus } from './event-bus.js';
|
|
6
|
-
import { createBin } from './bin.js';
|
|
7
5
|
/**
|
|
8
|
-
* ServiceHost -
|
|
6
|
+
* ServiceHost - High-level wrapper for hosting a WebRTC service
|
|
9
7
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* Simplifies hosting by handling:
|
|
9
|
+
* - Offer/answer exchange
|
|
10
|
+
* - ICE candidate polling
|
|
11
|
+
* - Connection pool management
|
|
12
|
+
* - Automatic reconnection
|
|
12
13
|
*
|
|
13
14
|
* @example
|
|
14
15
|
* ```typescript
|
|
15
|
-
* const
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* const host = new ServiceHost({
|
|
17
|
+
* service: 'chat.app@1.0.0',
|
|
18
|
+
* rondevuService: myService,
|
|
19
|
+
* maxPeers: 5
|
|
18
20
|
* })
|
|
19
21
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* const host = new ServiceHost({
|
|
24
|
-
* service: 'chat.app@1.0.0',
|
|
25
|
-
* rondevuService,
|
|
26
|
-
* maxPeers: 5,
|
|
22
|
+
* host.events.on('connection', conn => {
|
|
23
|
+
* conn.events.on('message', msg => console.log('Received:', msg))
|
|
24
|
+
* conn.sendMessage('Hello!')
|
|
27
25
|
* })
|
|
28
26
|
*
|
|
29
27
|
* await host.start()
|
|
30
|
-
*
|
|
31
|
-
* host.events.on('connection', (conn) => {
|
|
32
|
-
* console.log('New connection:', conn.id)
|
|
33
|
-
* conn.events.on('message', (msg) => {
|
|
34
|
-
* console.log('Message:', msg)
|
|
35
|
-
* })
|
|
36
|
-
* })
|
|
37
28
|
* ```
|
|
38
29
|
*/
|
|
39
30
|
export class ServiceHost {
|
|
40
31
|
constructor(options) {
|
|
41
|
-
this.
|
|
42
|
-
this.
|
|
43
|
-
this.
|
|
32
|
+
this.options = options;
|
|
33
|
+
this.signaler = null;
|
|
34
|
+
this.connections = [];
|
|
35
|
+
this.running = false;
|
|
44
36
|
this.events = new EventBus();
|
|
45
|
-
this.
|
|
46
|
-
this.
|
|
47
|
-
this.maxPeers = options.maxPeers || 20;
|
|
48
|
-
this.ttl = options.ttl || 300000;
|
|
49
|
-
this.isPublic = options.isPublic !== false;
|
|
50
|
-
this.metadata = options.metadata;
|
|
37
|
+
this.webrtcContext = new WebRTCContext(options.rtcConfiguration);
|
|
38
|
+
this.maxPeers = options.maxPeers || 5;
|
|
51
39
|
}
|
|
52
40
|
/**
|
|
53
|
-
* Start hosting the service
|
|
41
|
+
* Start hosting the service
|
|
54
42
|
*/
|
|
55
43
|
async start() {
|
|
56
|
-
if (this.
|
|
57
|
-
throw new Error('ServiceHost already
|
|
44
|
+
if (this.running) {
|
|
45
|
+
throw new Error('ServiceHost already running');
|
|
58
46
|
}
|
|
59
|
-
this.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const offerPromises = [];
|
|
96
|
-
for (let i = 0; i < needed; i++) {
|
|
97
|
-
offerPromises.push(this.createOffer());
|
|
47
|
+
this.running = true;
|
|
48
|
+
// Create signaler
|
|
49
|
+
this.signaler = new RondevuSignaler(this.options.rondevuService, this.options.service);
|
|
50
|
+
// Create first connection (offerer)
|
|
51
|
+
const connection = new RTCDurableConnection({
|
|
52
|
+
context: this.webrtcContext,
|
|
53
|
+
signaler: this.signaler,
|
|
54
|
+
offer: null // null means we're the offerer
|
|
55
|
+
});
|
|
56
|
+
// Wait for connection to be ready
|
|
57
|
+
await connection.ready;
|
|
58
|
+
// Set up connection event listeners
|
|
59
|
+
connection.events.on('state-change', (state) => {
|
|
60
|
+
if (state === 'connected') {
|
|
61
|
+
this.connections.push(connection);
|
|
62
|
+
this.events.emit('connection', connection);
|
|
63
|
+
// Create next connection if under maxPeers
|
|
64
|
+
if (this.connections.length < this.maxPeers) {
|
|
65
|
+
this.createNextConnection().catch(err => {
|
|
66
|
+
console.error('Failed to create next connection:', err);
|
|
67
|
+
this.events.emit('error', err);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else if (state === 'disconnected') {
|
|
72
|
+
// Remove from connections list
|
|
73
|
+
const index = this.connections.indexOf(connection);
|
|
74
|
+
if (index > -1) {
|
|
75
|
+
this.connections.splice(index, 1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
// Publish service with the offer
|
|
80
|
+
const offer = connection.connection?.localDescription;
|
|
81
|
+
if (!offer?.sdp) {
|
|
82
|
+
throw new Error('Offer SDP is empty');
|
|
98
83
|
}
|
|
99
|
-
await
|
|
84
|
+
await this.signaler.setOffer(offer);
|
|
100
85
|
}
|
|
101
86
|
/**
|
|
102
|
-
* Create
|
|
87
|
+
* Create the next connection for incoming peers
|
|
103
88
|
*/
|
|
104
|
-
async
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const tempContext = new WebRTCContext(new NoOpSignaler());
|
|
108
|
-
// Create connection (offerer role)
|
|
109
|
-
const conn = new WebRTCRondevuConnection({
|
|
110
|
-
id: `${this.service}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
|
111
|
-
service: this.service,
|
|
112
|
-
offer: null,
|
|
113
|
-
context: tempContext,
|
|
114
|
-
});
|
|
115
|
-
// Wait for offer to be created
|
|
116
|
-
await conn.ready;
|
|
117
|
-
// Get offer SDP
|
|
118
|
-
if (!conn.connection?.localDescription?.sdp) {
|
|
119
|
-
throw new Error('Failed to create offer SDP');
|
|
120
|
-
}
|
|
121
|
-
const sdp = conn.connection.localDescription.sdp;
|
|
122
|
-
// Publish service offer
|
|
123
|
-
const service = await this.rondevuService.publishService({
|
|
124
|
-
serviceFqn: this.service,
|
|
125
|
-
sdp,
|
|
126
|
-
ttl: this.ttl,
|
|
127
|
-
isPublic: this.isPublic,
|
|
128
|
-
metadata: this.metadata,
|
|
129
|
-
});
|
|
130
|
-
// Replace with real signaler now that we have offerId
|
|
131
|
-
const realSignaler = new RondevuSignaler(this.rondevuService.getAPI(), service.offerId);
|
|
132
|
-
tempContext.signaler = realSignaler;
|
|
133
|
-
// Track connection
|
|
134
|
-
this.connections.set(conn.id, conn);
|
|
135
|
-
// Listen for state changes
|
|
136
|
-
const cleanup = conn.events.on('state-change', state => {
|
|
137
|
-
this.handleConnectionStateChange(conn, state);
|
|
138
|
-
});
|
|
139
|
-
this.bin(cleanup);
|
|
140
|
-
}
|
|
141
|
-
catch (error) {
|
|
142
|
-
this.events.emit('error', error);
|
|
89
|
+
async createNextConnection() {
|
|
90
|
+
if (!this.signaler || !this.running) {
|
|
91
|
+
return;
|
|
143
92
|
}
|
|
93
|
+
// For now, we'll use the same offer for all connections
|
|
94
|
+
// In a production scenario, you'd create multiple offers
|
|
95
|
+
// This is a limitation of the current service model
|
|
96
|
+
// which publishes one offer per service
|
|
144
97
|
}
|
|
145
98
|
/**
|
|
146
|
-
*
|
|
99
|
+
* Stop hosting the service
|
|
147
100
|
*/
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
this.fillOfferPool().catch(error => {
|
|
155
|
-
this.events.emit('error', error);
|
|
156
|
-
});
|
|
157
|
-
}
|
|
101
|
+
dispose() {
|
|
102
|
+
this.running = false;
|
|
103
|
+
// Cleanup signaler
|
|
104
|
+
if (this.signaler) {
|
|
105
|
+
this.signaler.dispose();
|
|
106
|
+
this.signaler = null;
|
|
158
107
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
this.events.emit('connection-closed', {
|
|
163
|
-
connectionId: conn.id,
|
|
164
|
-
reason: state,
|
|
165
|
-
});
|
|
166
|
-
if (this.isStarted) {
|
|
167
|
-
this.fillOfferPool().catch(error => {
|
|
168
|
-
this.events.emit('error', error);
|
|
169
|
-
});
|
|
170
|
-
}
|
|
108
|
+
// Disconnect all connections
|
|
109
|
+
for (const conn of this.connections) {
|
|
110
|
+
conn.disconnect();
|
|
171
111
|
}
|
|
112
|
+
this.connections = [];
|
|
172
113
|
}
|
|
173
114
|
/**
|
|
174
115
|
* Get all active connections
|
|
175
116
|
*/
|
|
176
117
|
getConnections() {
|
|
177
|
-
return
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* Get a specific connection by ID
|
|
181
|
-
*/
|
|
182
|
-
getConnection(connectionId) {
|
|
183
|
-
return this.connections.get(connectionId);
|
|
118
|
+
return [...this.connections];
|
|
184
119
|
}
|
|
185
120
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -14,8 +14,6 @@ export interface ConnectionEvents {
|
|
|
14
14
|
export declare const ConnectionStates: readonly ["connected", "disconnected", "connecting"];
|
|
15
15
|
export declare const isConnectionState: (state: string) => state is (typeof ConnectionStates)[number];
|
|
16
16
|
export interface ConnectionInterface {
|
|
17
|
-
id: string;
|
|
18
|
-
service: string;
|
|
19
17
|
state: (typeof ConnectionStates)[number];
|
|
20
18
|
lastActive: number;
|
|
21
19
|
expiresAt?: number;
|
|
@@ -24,7 +22,7 @@ export interface ConnectionInterface {
|
|
|
24
22
|
sendMessage(message: Message): Promise<boolean>;
|
|
25
23
|
}
|
|
26
24
|
export interface Signaler {
|
|
27
|
-
addIceCandidate(candidate: RTCIceCandidate): Promise<void
|
|
25
|
+
addIceCandidate(candidate: RTCIceCandidate): Promise<void>;
|
|
28
26
|
addListener(callback: (candidate: RTCIceCandidate) => void): Binnable;
|
|
29
27
|
addOfferListener(callback: (offer: RTCSessionDescriptionInit) => void): Binnable;
|
|
30
28
|
addAnswerListener(callback: (answer: RTCSessionDescriptionInit) => void): Binnable;
|
package/dist/types.js
CHANGED
package/dist/webrtc-context.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { Signaler } from './types';
|
|
2
1
|
export declare class WebRTCContext {
|
|
3
|
-
readonly
|
|
4
|
-
constructor(
|
|
2
|
+
private readonly config?;
|
|
3
|
+
constructor(config?: RTCConfiguration | undefined);
|
|
5
4
|
createPeerConnection(): RTCPeerConnection;
|
|
6
5
|
}
|
package/dist/webrtc-context.js
CHANGED
|
@@ -1,34 +1,35 @@
|
|
|
1
|
+
const DEFAULT_RTC_CONFIGURATION = {
|
|
2
|
+
iceServers: [
|
|
3
|
+
{
|
|
4
|
+
urls: 'stun:stun.relay.metered.ca:80',
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
urls: 'turn:standard.relay.metered.ca:80',
|
|
8
|
+
username: 'c53a9c971da5e6f3bc959d8d',
|
|
9
|
+
credential: 'QaccPqtPPaxyokXp',
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
urls: 'turn:standard.relay.metered.ca:80?transport=tcp',
|
|
13
|
+
username: 'c53a9c971da5e6f3bc959d8d',
|
|
14
|
+
credential: 'QaccPqtPPaxyokXp',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
urls: 'turn:standard.relay.metered.ca:443',
|
|
18
|
+
username: 'c53a9c971da5e6f3bc959d8d',
|
|
19
|
+
credential: 'QaccPqtPPaxyokXp',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
urls: 'turns:standard.relay.metered.ca:443?transport=tcp',
|
|
23
|
+
username: 'c53a9c971da5e6f3bc959d8d',
|
|
24
|
+
credential: 'QaccPqtPPaxyokXp',
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
};
|
|
1
28
|
export class WebRTCContext {
|
|
2
|
-
constructor(
|
|
3
|
-
this.
|
|
29
|
+
constructor(config) {
|
|
30
|
+
this.config = config;
|
|
4
31
|
}
|
|
5
32
|
createPeerConnection() {
|
|
6
|
-
return new RTCPeerConnection(
|
|
7
|
-
iceServers: [
|
|
8
|
-
{
|
|
9
|
-
urls: 'stun:stun.relay.metered.ca:80',
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
urls: 'turn:standard.relay.metered.ca:80',
|
|
13
|
-
username: 'c53a9c971da5e6f3bc959d8d',
|
|
14
|
-
credential: 'QaccPqtPPaxyokXp',
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
urls: 'turn:standard.relay.metered.ca:80?transport=tcp',
|
|
18
|
-
username: 'c53a9c971da5e6f3bc959d8d',
|
|
19
|
-
credential: 'QaccPqtPPaxyokXp',
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
urls: 'turn:standard.relay.metered.ca:443',
|
|
23
|
-
username: 'c53a9c971da5e6f3bc959d8d',
|
|
24
|
-
credential: 'QaccPqtPPaxyokXp',
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
urls: 'turns:standard.relay.metered.ca:443?transport=tcp',
|
|
28
|
-
username: 'c53a9c971da5e6f3bc959d8d',
|
|
29
|
-
credential: 'QaccPqtPPaxyokXp',
|
|
30
|
-
},
|
|
31
|
-
],
|
|
32
|
-
});
|
|
33
|
+
return new RTCPeerConnection(this.config || DEFAULT_RTC_CONFIGURATION);
|
|
33
34
|
}
|
|
34
35
|
}
|
package/package.json
CHANGED