@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/dist/api.d.ts
CHANGED
|
@@ -25,20 +25,29 @@ export interface Offer {
|
|
|
25
25
|
expiresAt: number;
|
|
26
26
|
answererPeerId?: string;
|
|
27
27
|
}
|
|
28
|
+
export interface OfferRequest {
|
|
29
|
+
sdp: string;
|
|
30
|
+
}
|
|
28
31
|
export interface ServiceRequest {
|
|
29
32
|
username: string;
|
|
30
33
|
serviceFqn: string;
|
|
31
|
-
|
|
34
|
+
offers: OfferRequest[];
|
|
32
35
|
ttl?: number;
|
|
33
36
|
isPublic?: boolean;
|
|
34
37
|
metadata?: Record<string, any>;
|
|
35
38
|
signature: string;
|
|
36
39
|
message: string;
|
|
37
40
|
}
|
|
41
|
+
export interface ServiceOffer {
|
|
42
|
+
offerId: string;
|
|
43
|
+
sdp: string;
|
|
44
|
+
createdAt: number;
|
|
45
|
+
expiresAt: number;
|
|
46
|
+
}
|
|
38
47
|
export interface Service {
|
|
39
48
|
serviceId: string;
|
|
40
49
|
uuid: string;
|
|
41
|
-
|
|
50
|
+
offers: ServiceOffer[];
|
|
42
51
|
username: string;
|
|
43
52
|
serviceFqn: string;
|
|
44
53
|
isPublic: boolean;
|
|
@@ -57,6 +66,10 @@ export declare class RondevuAPI {
|
|
|
57
66
|
private baseUrl;
|
|
58
67
|
private credentials?;
|
|
59
68
|
constructor(baseUrl: string, credentials?: Credentials | undefined);
|
|
69
|
+
/**
|
|
70
|
+
* Set credentials for authentication
|
|
71
|
+
*/
|
|
72
|
+
setCredentials(credentials: Credentials): void;
|
|
60
73
|
/**
|
|
61
74
|
* Authentication header
|
|
62
75
|
*/
|
|
@@ -119,15 +132,11 @@ export declare class RondevuAPI {
|
|
|
119
132
|
sdp: string;
|
|
120
133
|
}>;
|
|
121
134
|
/**
|
|
122
|
-
* Search services by username
|
|
135
|
+
* Search services by username - lists all services for a username
|
|
123
136
|
*/
|
|
124
137
|
searchServicesByUsername(username: string): Promise<Service[]>;
|
|
125
138
|
/**
|
|
126
|
-
* Search services by FQN
|
|
127
|
-
*/
|
|
128
|
-
searchServicesByFqn(serviceFqn: string): Promise<Service[]>;
|
|
129
|
-
/**
|
|
130
|
-
* Search services by username AND FQN
|
|
139
|
+
* Search services by username AND FQN - returns full service details
|
|
131
140
|
*/
|
|
132
141
|
searchServices(username: string, serviceFqn: string): Promise<Service[]>;
|
|
133
142
|
/**
|
package/dist/api.js
CHANGED
|
@@ -28,6 +28,12 @@ export class RondevuAPI {
|
|
|
28
28
|
this.baseUrl = baseUrl;
|
|
29
29
|
this.credentials = credentials;
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Set credentials for authentication
|
|
33
|
+
*/
|
|
34
|
+
setCredentials(credentials) {
|
|
35
|
+
this.credentials = credentials;
|
|
36
|
+
}
|
|
31
37
|
/**
|
|
32
38
|
* Authentication header
|
|
33
39
|
*/
|
|
@@ -148,14 +154,16 @@ export class RondevuAPI {
|
|
|
148
154
|
const response = await fetch(`${this.baseUrl}/offers/${offerId}/answer`, {
|
|
149
155
|
headers: this.getAuthHeader(),
|
|
150
156
|
});
|
|
151
|
-
if (response.status === 404) {
|
|
152
|
-
return null; // No answer yet
|
|
153
|
-
}
|
|
154
157
|
if (!response.ok) {
|
|
158
|
+
// 404 means not yet answered
|
|
159
|
+
if (response.status === 404) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
155
162
|
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
156
163
|
throw new Error(`Failed to get answer: ${error.error || response.statusText}`);
|
|
157
164
|
}
|
|
158
|
-
|
|
165
|
+
const data = await response.json();
|
|
166
|
+
return { sdp: data.sdp };
|
|
159
167
|
}
|
|
160
168
|
/**
|
|
161
169
|
* Search offers by topic
|
|
@@ -199,7 +207,8 @@ export class RondevuAPI {
|
|
|
199
207
|
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
200
208
|
throw new Error(`Failed to get ICE candidates: ${error.error || response.statusText}`);
|
|
201
209
|
}
|
|
202
|
-
|
|
210
|
+
const data = await response.json();
|
|
211
|
+
return data.candidates || [];
|
|
203
212
|
}
|
|
204
213
|
// ============================================
|
|
205
214
|
// Services
|
|
@@ -208,7 +217,7 @@ export class RondevuAPI {
|
|
|
208
217
|
* Publish a service
|
|
209
218
|
*/
|
|
210
219
|
async publishService(service) {
|
|
211
|
-
const response = await fetch(`${this.baseUrl}/services`, {
|
|
220
|
+
const response = await fetch(`${this.baseUrl}/users/${encodeURIComponent(service.username)}/services`, {
|
|
212
221
|
method: 'POST',
|
|
213
222
|
headers: {
|
|
214
223
|
'Content-Type': 'application/json',
|
|
@@ -236,37 +245,31 @@ export class RondevuAPI {
|
|
|
236
245
|
return await response.json();
|
|
237
246
|
}
|
|
238
247
|
/**
|
|
239
|
-
* Search services by username
|
|
248
|
+
* Search services by username - lists all services for a username
|
|
240
249
|
*/
|
|
241
250
|
async searchServicesByUsername(username) {
|
|
242
|
-
const response = await fetch(`${this.baseUrl}/
|
|
251
|
+
const response = await fetch(`${this.baseUrl}/users/${encodeURIComponent(username)}/services`, { headers: this.getAuthHeader() });
|
|
243
252
|
if (!response.ok) {
|
|
244
253
|
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
245
254
|
throw new Error(`Failed to search services: ${error.error || response.statusText}`);
|
|
246
255
|
}
|
|
247
|
-
|
|
256
|
+
const data = await response.json();
|
|
257
|
+
return data.services || [];
|
|
248
258
|
}
|
|
249
259
|
/**
|
|
250
|
-
* Search services by FQN
|
|
251
|
-
*/
|
|
252
|
-
async searchServicesByFqn(serviceFqn) {
|
|
253
|
-
const response = await fetch(`${this.baseUrl}/services?serviceFqn=${encodeURIComponent(serviceFqn)}`, { headers: this.getAuthHeader() });
|
|
254
|
-
if (!response.ok) {
|
|
255
|
-
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
256
|
-
throw new Error(`Failed to search services: ${error.error || response.statusText}`);
|
|
257
|
-
}
|
|
258
|
-
return await response.json();
|
|
259
|
-
}
|
|
260
|
-
/**
|
|
261
|
-
* Search services by username AND FQN
|
|
260
|
+
* Search services by username AND FQN - returns full service details
|
|
262
261
|
*/
|
|
263
262
|
async searchServices(username, serviceFqn) {
|
|
264
|
-
const response = await fetch(`${this.baseUrl}/
|
|
263
|
+
const response = await fetch(`${this.baseUrl}/users/${encodeURIComponent(username)}/services/${encodeURIComponent(serviceFqn)}`, { headers: this.getAuthHeader() });
|
|
265
264
|
if (!response.ok) {
|
|
265
|
+
if (response.status === 404) {
|
|
266
|
+
return [];
|
|
267
|
+
}
|
|
266
268
|
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
267
269
|
throw new Error(`Failed to search services: ${error.error || response.statusText}`);
|
|
268
270
|
}
|
|
269
|
-
|
|
271
|
+
const service = await response.json();
|
|
272
|
+
return [service];
|
|
270
273
|
}
|
|
271
274
|
// ============================================
|
|
272
275
|
// Usernames
|
|
@@ -275,7 +278,7 @@ export class RondevuAPI {
|
|
|
275
278
|
* Check if username is available
|
|
276
279
|
*/
|
|
277
280
|
async checkUsername(username) {
|
|
278
|
-
const response = await fetch(`${this.baseUrl}/
|
|
281
|
+
const response = await fetch(`${this.baseUrl}/users/${encodeURIComponent(username)}`);
|
|
279
282
|
if (!response.ok) {
|
|
280
283
|
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
281
284
|
throw new Error(`Failed to check username: ${error.error || response.statusText}`);
|
|
@@ -286,7 +289,7 @@ export class RondevuAPI {
|
|
|
286
289
|
* Claim a username (requires Ed25519 signature)
|
|
287
290
|
*/
|
|
288
291
|
async claimUsername(username, publicKey, signature, message) {
|
|
289
|
-
const response = await fetch(`${this.baseUrl}/
|
|
292
|
+
const response = await fetch(`${this.baseUrl}/users/${encodeURIComponent(username)}`, {
|
|
290
293
|
method: 'POST',
|
|
291
294
|
headers: {
|
|
292
295
|
'Content-Type': 'application/json',
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { ConnectionEvents, ConnectionInterface, Message, QueueMessageOptions, Signaler } from './types.js';
|
|
2
|
+
import { EventBus } from './event-bus.js';
|
|
3
|
+
import { WebRTCContext } from './webrtc-context';
|
|
4
|
+
export type WebRTCRondevuConnectionOptions = {
|
|
5
|
+
offer?: RTCSessionDescriptionInit | null;
|
|
6
|
+
context: WebRTCContext;
|
|
7
|
+
signaler: Signaler;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* WebRTCRondevuConnection - WebRTC peer connection wrapper with Rondevu signaling
|
|
11
|
+
*
|
|
12
|
+
* Manages a WebRTC peer connection lifecycle including:
|
|
13
|
+
* - Automatic offer/answer creation based on role
|
|
14
|
+
* - ICE candidate exchange via Rondevu signaling server
|
|
15
|
+
* - Connection state management with type-safe events
|
|
16
|
+
* - Data channel creation and message handling
|
|
17
|
+
*
|
|
18
|
+
* The connection automatically determines its role (offerer or answerer) based on whether
|
|
19
|
+
* an offer is provided in the constructor. The offerer creates the data channel, while
|
|
20
|
+
* the answerer receives it via the 'datachannel' event.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* // Offerer side (creates offer)
|
|
25
|
+
* const connection = new WebRTCRondevuConnection(
|
|
26
|
+
* 'conn-123',
|
|
27
|
+
* 'peer-username',
|
|
28
|
+
* 'chat.service@1.0.0'
|
|
29
|
+
* );
|
|
30
|
+
*
|
|
31
|
+
* await connection.ready; // Wait for local offer
|
|
32
|
+
* const sdp = connection.connection.localDescription!.sdp!;
|
|
33
|
+
* // Send sdp to signaling server...
|
|
34
|
+
*
|
|
35
|
+
* // Answerer side (receives offer)
|
|
36
|
+
* const connection = new WebRTCRondevuConnection(
|
|
37
|
+
* 'conn-123',
|
|
38
|
+
* 'peer-username',
|
|
39
|
+
* 'chat.service@1.0.0',
|
|
40
|
+
* { type: 'offer', sdp: remoteOfferSdp }
|
|
41
|
+
* );
|
|
42
|
+
*
|
|
43
|
+
* await connection.ready; // Wait for local answer
|
|
44
|
+
* const answerSdp = connection.connection.localDescription!.sdp!;
|
|
45
|
+
* // Send answer to signaling server...
|
|
46
|
+
*
|
|
47
|
+
* // Both sides: Set up signaler and listen for state changes
|
|
48
|
+
* connection.setSignaler(signaler);
|
|
49
|
+
* connection.events.on('state-change', (state) => {
|
|
50
|
+
* console.log('Connection state:', state);
|
|
51
|
+
* });
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export declare class RTCDurableConnection implements ConnectionInterface {
|
|
55
|
+
private readonly side;
|
|
56
|
+
readonly expiresAt: number;
|
|
57
|
+
readonly lastActive: number;
|
|
58
|
+
readonly events: EventBus<ConnectionEvents>;
|
|
59
|
+
readonly ready: Promise<void>;
|
|
60
|
+
private iceBin;
|
|
61
|
+
private context;
|
|
62
|
+
private readonly signaler;
|
|
63
|
+
private _conn;
|
|
64
|
+
private _state;
|
|
65
|
+
private _dataChannel;
|
|
66
|
+
private messageQueue;
|
|
67
|
+
constructor({ context, offer, signaler }: WebRTCRondevuConnectionOptions);
|
|
68
|
+
/**
|
|
69
|
+
* Getter method for retrieving the current connection.
|
|
70
|
+
*
|
|
71
|
+
* @return {RTCPeerConnection|null} The current connection instance.
|
|
72
|
+
*/
|
|
73
|
+
get connection(): RTCPeerConnection | null;
|
|
74
|
+
/**
|
|
75
|
+
* Update connection state and emit state-change event
|
|
76
|
+
*/
|
|
77
|
+
private setState;
|
|
78
|
+
/**
|
|
79
|
+
* Start ICE candidate exchange when gathering begins
|
|
80
|
+
*/
|
|
81
|
+
private startIce;
|
|
82
|
+
/**
|
|
83
|
+
* Stop ICE candidate exchange when gathering completes
|
|
84
|
+
*/
|
|
85
|
+
private stopIce;
|
|
86
|
+
/**
|
|
87
|
+
* Disconnects the current connection and cleans up resources.
|
|
88
|
+
* Closes the active connection if it exists, resets the connection instance to null,
|
|
89
|
+
* stops the ICE process, and updates the state to 'disconnected'.
|
|
90
|
+
*
|
|
91
|
+
* @return {void} No return value.
|
|
92
|
+
*/
|
|
93
|
+
disconnect(): void;
|
|
94
|
+
/**
|
|
95
|
+
* Current connection state
|
|
96
|
+
*/
|
|
97
|
+
get state(): "connected" | "disconnected" | "connecting";
|
|
98
|
+
/**
|
|
99
|
+
* Setup data channel event listeners
|
|
100
|
+
*/
|
|
101
|
+
private setupDataChannelListeners;
|
|
102
|
+
/**
|
|
103
|
+
* Flush the message queue
|
|
104
|
+
*/
|
|
105
|
+
private flushQueue;
|
|
106
|
+
/**
|
|
107
|
+
* Queue a message for sending when connection is established
|
|
108
|
+
*
|
|
109
|
+
* @param message - Message to queue (string or ArrayBuffer)
|
|
110
|
+
* @param options - Queue options (e.g., expiration time)
|
|
111
|
+
*/
|
|
112
|
+
queueMessage(message: Message, options?: QueueMessageOptions): Promise<void>;
|
|
113
|
+
/**
|
|
114
|
+
* Send a message immediately
|
|
115
|
+
*
|
|
116
|
+
* @param message - Message to send (string or ArrayBuffer)
|
|
117
|
+
* @returns Promise resolving to true if sent successfully
|
|
118
|
+
*/
|
|
119
|
+
sendMessage(message: Message): Promise<boolean>;
|
|
120
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { isConnectionState, } from './types.js';
|
|
2
|
+
import { EventBus } from './event-bus.js';
|
|
3
|
+
import { createBin } from './bin.js';
|
|
4
|
+
/**
|
|
5
|
+
* WebRTCRondevuConnection - WebRTC peer connection wrapper with Rondevu signaling
|
|
6
|
+
*
|
|
7
|
+
* Manages a WebRTC peer connection lifecycle including:
|
|
8
|
+
* - Automatic offer/answer creation based on role
|
|
9
|
+
* - ICE candidate exchange via Rondevu signaling server
|
|
10
|
+
* - Connection state management with type-safe events
|
|
11
|
+
* - Data channel creation and message handling
|
|
12
|
+
*
|
|
13
|
+
* The connection automatically determines its role (offerer or answerer) based on whether
|
|
14
|
+
* an offer is provided in the constructor. The offerer creates the data channel, while
|
|
15
|
+
* the answerer receives it via the 'datachannel' event.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* // Offerer side (creates offer)
|
|
20
|
+
* const connection = new WebRTCRondevuConnection(
|
|
21
|
+
* 'conn-123',
|
|
22
|
+
* 'peer-username',
|
|
23
|
+
* 'chat.service@1.0.0'
|
|
24
|
+
* );
|
|
25
|
+
*
|
|
26
|
+
* await connection.ready; // Wait for local offer
|
|
27
|
+
* const sdp = connection.connection.localDescription!.sdp!;
|
|
28
|
+
* // Send sdp to signaling server...
|
|
29
|
+
*
|
|
30
|
+
* // Answerer side (receives offer)
|
|
31
|
+
* const connection = new WebRTCRondevuConnection(
|
|
32
|
+
* 'conn-123',
|
|
33
|
+
* 'peer-username',
|
|
34
|
+
* 'chat.service@1.0.0',
|
|
35
|
+
* { type: 'offer', sdp: remoteOfferSdp }
|
|
36
|
+
* );
|
|
37
|
+
*
|
|
38
|
+
* await connection.ready; // Wait for local answer
|
|
39
|
+
* const answerSdp = connection.connection.localDescription!.sdp!;
|
|
40
|
+
* // Send answer to signaling server...
|
|
41
|
+
*
|
|
42
|
+
* // Both sides: Set up signaler and listen for state changes
|
|
43
|
+
* connection.setSignaler(signaler);
|
|
44
|
+
* connection.events.on('state-change', (state) => {
|
|
45
|
+
* console.log('Connection state:', state);
|
|
46
|
+
* });
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export class RTCDurableConnection {
|
|
50
|
+
constructor({ context, offer, signaler }) {
|
|
51
|
+
this.expiresAt = 0;
|
|
52
|
+
this.lastActive = 0;
|
|
53
|
+
this.events = new EventBus();
|
|
54
|
+
this.iceBin = createBin();
|
|
55
|
+
this._conn = null;
|
|
56
|
+
this._state = 'disconnected';
|
|
57
|
+
this._dataChannel = null;
|
|
58
|
+
this.messageQueue = [];
|
|
59
|
+
this.context = context;
|
|
60
|
+
this.signaler = signaler;
|
|
61
|
+
this._conn = context.createPeerConnection();
|
|
62
|
+
this.side = offer ? 'answer' : 'offer';
|
|
63
|
+
// setup data channel
|
|
64
|
+
if (offer) {
|
|
65
|
+
this._conn.addEventListener('datachannel', e => {
|
|
66
|
+
this._dataChannel = e.channel;
|
|
67
|
+
this.setupDataChannelListeners(this._dataChannel);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
this._dataChannel = this._conn.createDataChannel('vu.ronde.protocol');
|
|
72
|
+
this.setupDataChannelListeners(this._dataChannel);
|
|
73
|
+
}
|
|
74
|
+
// setup description exchange
|
|
75
|
+
this.ready = offer
|
|
76
|
+
? this._conn
|
|
77
|
+
.setRemoteDescription(offer)
|
|
78
|
+
.then(() => this._conn?.createAnswer())
|
|
79
|
+
.then(async (answer) => {
|
|
80
|
+
if (!answer || !this._conn)
|
|
81
|
+
throw new Error('Connection disappeared');
|
|
82
|
+
await this._conn.setLocalDescription(answer);
|
|
83
|
+
return await signaler.setAnswer(answer);
|
|
84
|
+
})
|
|
85
|
+
: this._conn.createOffer().then(async (offer) => {
|
|
86
|
+
if (!this._conn)
|
|
87
|
+
throw new Error('Connection disappeared');
|
|
88
|
+
await this._conn.setLocalDescription(offer);
|
|
89
|
+
return await signaler.setOffer(offer);
|
|
90
|
+
});
|
|
91
|
+
// propagate connection state changes
|
|
92
|
+
this._conn.addEventListener('connectionstatechange', () => {
|
|
93
|
+
console.log(this.side, 'connection state changed: ', this._conn.connectionState);
|
|
94
|
+
const state = isConnectionState(this._conn.connectionState)
|
|
95
|
+
? this._conn.connectionState
|
|
96
|
+
: 'disconnected';
|
|
97
|
+
this.setState(state);
|
|
98
|
+
});
|
|
99
|
+
this._conn.addEventListener('iceconnectionstatechange', () => {
|
|
100
|
+
console.log(this.side, 'ice connection state changed: ', this._conn.iceConnectionState);
|
|
101
|
+
});
|
|
102
|
+
// start ICE candidate exchange when gathering begins
|
|
103
|
+
this._conn.addEventListener('icegatheringstatechange', () => {
|
|
104
|
+
if (this._conn.iceGatheringState === 'gathering') {
|
|
105
|
+
this.startIce();
|
|
106
|
+
}
|
|
107
|
+
else if (this._conn.iceGatheringState === 'complete') {
|
|
108
|
+
this.stopIce();
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Getter method for retrieving the current connection.
|
|
114
|
+
*
|
|
115
|
+
* @return {RTCPeerConnection|null} The current connection instance.
|
|
116
|
+
*/
|
|
117
|
+
get connection() {
|
|
118
|
+
return this._conn;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Update connection state and emit state-change event
|
|
122
|
+
*/
|
|
123
|
+
setState(state) {
|
|
124
|
+
this._state = state;
|
|
125
|
+
this.events.emit('state-change', state);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Start ICE candidate exchange when gathering begins
|
|
129
|
+
*/
|
|
130
|
+
startIce() {
|
|
131
|
+
const listener = ({ candidate }) => {
|
|
132
|
+
if (candidate)
|
|
133
|
+
this.signaler.addIceCandidate(candidate);
|
|
134
|
+
};
|
|
135
|
+
if (!this._conn)
|
|
136
|
+
throw new Error('Connection disappeared');
|
|
137
|
+
this._conn.addEventListener('icecandidate', listener);
|
|
138
|
+
this.iceBin(this.signaler.addListener((candidate) => this._conn?.addIceCandidate(candidate)), () => this._conn?.removeEventListener('icecandidate', listener));
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Stop ICE candidate exchange when gathering completes
|
|
142
|
+
*/
|
|
143
|
+
stopIce() {
|
|
144
|
+
this.iceBin.clean();
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Disconnects the current connection and cleans up resources.
|
|
148
|
+
* Closes the active connection if it exists, resets the connection instance to null,
|
|
149
|
+
* stops the ICE process, and updates the state to 'disconnected'.
|
|
150
|
+
*
|
|
151
|
+
* @return {void} No return value.
|
|
152
|
+
*/
|
|
153
|
+
disconnect() {
|
|
154
|
+
this._conn?.close();
|
|
155
|
+
this._conn = null;
|
|
156
|
+
this.stopIce();
|
|
157
|
+
this.setState('disconnected');
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Current connection state
|
|
161
|
+
*/
|
|
162
|
+
get state() {
|
|
163
|
+
return this._state;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Setup data channel event listeners
|
|
167
|
+
*/
|
|
168
|
+
setupDataChannelListeners(channel) {
|
|
169
|
+
channel.addEventListener('message', e => {
|
|
170
|
+
this.events.emit('message', e.data);
|
|
171
|
+
});
|
|
172
|
+
channel.addEventListener('open', () => {
|
|
173
|
+
// Channel opened - flush queued messages
|
|
174
|
+
this.flushQueue().catch(err => {
|
|
175
|
+
console.error('Failed to flush message queue:', err);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
channel.addEventListener('error', err => {
|
|
179
|
+
console.error('Data channel error:', err);
|
|
180
|
+
});
|
|
181
|
+
channel.addEventListener('close', () => {
|
|
182
|
+
console.log('Data channel closed');
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Flush the message queue
|
|
187
|
+
*/
|
|
188
|
+
async flushQueue() {
|
|
189
|
+
while (this.messageQueue.length > 0 && this._state === 'connected') {
|
|
190
|
+
const item = this.messageQueue.shift();
|
|
191
|
+
// Check expiration
|
|
192
|
+
if (item.options.expiresAt && Date.now() > item.options.expiresAt) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const success = await this.sendMessage(item.message);
|
|
196
|
+
if (!success) {
|
|
197
|
+
// Re-queue on failure
|
|
198
|
+
this.messageQueue.unshift(item);
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Queue a message for sending when connection is established
|
|
205
|
+
*
|
|
206
|
+
* @param message - Message to queue (string or ArrayBuffer)
|
|
207
|
+
* @param options - Queue options (e.g., expiration time)
|
|
208
|
+
*/
|
|
209
|
+
async queueMessage(message, options = {}) {
|
|
210
|
+
this.messageQueue.push({
|
|
211
|
+
message,
|
|
212
|
+
options,
|
|
213
|
+
timestamp: Date.now()
|
|
214
|
+
});
|
|
215
|
+
// Try immediate send if connected
|
|
216
|
+
if (this._state === 'connected') {
|
|
217
|
+
await this.flushQueue();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Send a message immediately
|
|
222
|
+
*
|
|
223
|
+
* @param message - Message to send (string or ArrayBuffer)
|
|
224
|
+
* @returns Promise resolving to true if sent successfully
|
|
225
|
+
*/
|
|
226
|
+
async sendMessage(message) {
|
|
227
|
+
if (this._state !== 'connected' || !this._dataChannel) {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
if (this._dataChannel.readyState !== 'open') {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
// TypeScript has trouble with the union type, so we cast to any
|
|
235
|
+
// Both string and ArrayBuffer are valid for RTCDataChannel.send()
|
|
236
|
+
this._dataChannel.send(message);
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
console.error('Send failed:', err);
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
export { EventBus } from './event-bus.js';
|
|
6
6
|
export { RondevuAPI } from './api.js';
|
|
7
7
|
export { RondevuService } from './rondevu-service.js';
|
|
8
|
-
export { RondevuSignaler } from './signaler.js';
|
|
8
|
+
export { RondevuSignaler } from './rondevu-signaler.js';
|
|
9
|
+
export { WebRTCContext } from './webrtc-context.js';
|
|
10
|
+
export { RTCDurableConnection } from './durable-connection';
|
|
9
11
|
export { ServiceHost } from './service-host.js';
|
|
10
12
|
export { ServiceClient } from './service-client.js';
|
|
11
|
-
export { WebRTCRondevuConnection } from './connection.js';
|
|
12
13
|
export { createBin } from './bin.js';
|
|
13
14
|
export type { ConnectionInterface, QueueMessageOptions, Message, ConnectionEvents, Signaler, } from './types.js';
|
|
14
15
|
export type { Credentials, Keypair, OfferRequest, Offer, ServiceRequest, Service, IceCandidate, } from './api.js';
|
|
@@ -16,3 +17,4 @@ export type { Binnable } from './bin.js';
|
|
|
16
17
|
export type { RondevuServiceOptions, PublishServiceOptions } from './rondevu-service.js';
|
|
17
18
|
export type { ServiceHostOptions, ServiceHostEvents } from './service-host.js';
|
|
18
19
|
export type { ServiceClientOptions, ServiceClientEvents } from './service-client.js';
|
|
20
|
+
export type { PollingConfig } from './rondevu-signaler.js';
|
package/dist/index.js
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
export { EventBus } from './event-bus.js';
|
|
6
6
|
export { RondevuAPI } from './api.js';
|
|
7
7
|
export { RondevuService } from './rondevu-service.js';
|
|
8
|
-
export { RondevuSignaler } from './signaler.js';
|
|
8
|
+
export { RondevuSignaler } from './rondevu-signaler.js';
|
|
9
|
+
export { WebRTCContext } from './webrtc-context.js';
|
|
10
|
+
export { RTCDurableConnection } from './durable-connection';
|
|
9
11
|
export { ServiceHost } from './service-host.js';
|
|
10
12
|
export { ServiceClient } from './service-client.js';
|
|
11
|
-
export { WebRTCRondevuConnection } from './connection.js';
|
|
12
13
|
export { createBin } from './bin.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { RondevuService } from './rondevu-service.js';
|
|
2
|
+
import { Keypair } from './api.js';
|
|
3
|
+
export interface QuickStartOptions {
|
|
4
|
+
apiUrl: string;
|
|
5
|
+
username?: string;
|
|
6
|
+
keypair?: Keypair;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Quick start helper for initializing Rondevu service
|
|
10
|
+
*
|
|
11
|
+
* Simplifies initialization by:
|
|
12
|
+
* - Auto-generating username if not provided
|
|
13
|
+
* - Auto-generating keypair if not provided
|
|
14
|
+
* - Registering with the server
|
|
15
|
+
* - Claiming the username
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* // Simple usage with auto-generated username
|
|
20
|
+
* const service = await quickStart({ apiUrl: 'https://api.ronde.vu' })
|
|
21
|
+
*
|
|
22
|
+
* // With custom username
|
|
23
|
+
* const service = await quickStart({
|
|
24
|
+
* apiUrl: 'https://api.ronde.vu',
|
|
25
|
+
* username: 'my-username'
|
|
26
|
+
* })
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare function quickStart(options: QuickStartOptions): Promise<RondevuService>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { RondevuService } from './rondevu-service.js';
|
|
2
|
+
/**
|
|
3
|
+
* Quick start helper for initializing Rondevu service
|
|
4
|
+
*
|
|
5
|
+
* Simplifies initialization by:
|
|
6
|
+
* - Auto-generating username if not provided
|
|
7
|
+
* - Auto-generating keypair if not provided
|
|
8
|
+
* - Registering with the server
|
|
9
|
+
* - Claiming the username
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* // Simple usage with auto-generated username
|
|
14
|
+
* const service = await quickStart({ apiUrl: 'https://api.ronde.vu' })
|
|
15
|
+
*
|
|
16
|
+
* // With custom username
|
|
17
|
+
* const service = await quickStart({
|
|
18
|
+
* apiUrl: 'https://api.ronde.vu',
|
|
19
|
+
* username: 'my-username'
|
|
20
|
+
* })
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export async function quickStart(options) {
|
|
24
|
+
// Generate username if not provided
|
|
25
|
+
const username = options.username || `user-${generateId()}`;
|
|
26
|
+
// Create service
|
|
27
|
+
const serviceOptions = {
|
|
28
|
+
apiUrl: options.apiUrl,
|
|
29
|
+
username,
|
|
30
|
+
keypair: options.keypair
|
|
31
|
+
};
|
|
32
|
+
const service = new RondevuService(serviceOptions);
|
|
33
|
+
// Initialize (generates keypair and registers)
|
|
34
|
+
await service.initialize();
|
|
35
|
+
// Claim username
|
|
36
|
+
await service.claimUsername();
|
|
37
|
+
return service;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Generate a random ID
|
|
41
|
+
*/
|
|
42
|
+
function generateId() {
|
|
43
|
+
return Math.random().toString(36).substring(2, 10);
|
|
44
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { RondevuService } from "./rondevu-service";
|
|
2
|
+
import { RTCDurableConnection } from "./durable-connection";
|
|
3
|
+
export declare class RondevuContext {
|
|
4
|
+
readonly rondevu: RondevuService;
|
|
5
|
+
readonly rtcConfig: RTCConfiguration;
|
|
6
|
+
private readonly context;
|
|
7
|
+
private readonly connections;
|
|
8
|
+
constructor(rondevu: RondevuService, rtcConfig: RTCConfiguration);
|
|
9
|
+
createConnection(service: string, host?: string, offer?: RTCSessionDescriptionInit): RTCDurableConnection;
|
|
10
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { WebRTCContext } from "./webrtc-context";
|
|
2
|
+
import { RTCDurableConnection } from "./durable-connection";
|
|
3
|
+
import { RondevuSignaler } from "./rondevu-signaler";
|
|
4
|
+
export class RondevuContext {
|
|
5
|
+
constructor(rondevu, rtcConfig) {
|
|
6
|
+
this.rondevu = rondevu;
|
|
7
|
+
this.rtcConfig = rtcConfig;
|
|
8
|
+
this.connections = [];
|
|
9
|
+
this.context = new WebRTCContext(rtcConfig);
|
|
10
|
+
}
|
|
11
|
+
createConnection(service, host, offer) {
|
|
12
|
+
const conn = new RTCDurableConnection({
|
|
13
|
+
context: this.context,
|
|
14
|
+
offer,
|
|
15
|
+
signaler: new RondevuSignaler(this.rondevu, service, host)
|
|
16
|
+
});
|
|
17
|
+
this.connections.push(conn);
|
|
18
|
+
return conn;
|
|
19
|
+
}
|
|
20
|
+
}
|