@xtr-dev/rondevu-client 0.9.2 → 0.10.1
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/dist/api.d.ts +147 -0
- package/dist/api.js +307 -0
- package/dist/bin.d.ts +35 -0
- package/dist/bin.js +35 -0
- package/dist/connection-manager.d.ts +104 -0
- package/dist/connection-manager.js +324 -0
- package/dist/connection.d.ts +112 -0
- package/dist/connection.js +194 -0
- package/dist/event-bus.d.ts +52 -0
- package/dist/event-bus.js +84 -0
- package/dist/index.d.ts +15 -11
- package/dist/index.js +9 -11
- package/dist/noop-signaler.d.ts +14 -0
- package/dist/noop-signaler.js +27 -0
- package/dist/rondevu-service.d.ts +81 -0
- package/dist/rondevu-service.js +131 -0
- package/dist/service-client.d.ts +94 -0
- package/dist/service-client.js +186 -0
- package/dist/service-host.d.ts +103 -0
- package/dist/service-host.js +186 -0
- package/dist/signaler.d.ts +25 -0
- package/dist/signaler.js +89 -0
- package/dist/types.d.ts +33 -0
- package/dist/types.js +2 -0
- package/dist/webrtc-context.d.ts +7 -0
- package/dist/webrtc-context.js +36 -0
- package/package.json +16 -2
- package/dist/auth.d.ts +0 -20
- package/dist/auth.js +0 -41
- package/dist/durable/channel.d.ts +0 -115
- package/dist/durable/channel.js +0 -301
- package/dist/durable/connection.d.ts +0 -125
- package/dist/durable/connection.js +0 -370
- package/dist/durable/reconnection.d.ts +0 -90
- package/dist/durable/reconnection.js +0 -127
- package/dist/durable/service.d.ts +0 -103
- package/dist/durable/service.js +0 -264
- package/dist/durable/types.d.ts +0 -149
- package/dist/durable/types.js +0 -28
- package/dist/event-emitter.d.ts +0 -54
- package/dist/event-emitter.js +0 -102
- package/dist/offer-pool.d.ts +0 -86
- package/dist/offer-pool.js +0 -145
- package/dist/offers.d.ts +0 -101
- package/dist/offers.js +0 -202
- package/dist/peer/answering-state.d.ts +0 -11
- package/dist/peer/answering-state.js +0 -39
- package/dist/peer/closed-state.d.ts +0 -8
- package/dist/peer/closed-state.js +0 -10
- package/dist/peer/connected-state.d.ts +0 -8
- package/dist/peer/connected-state.js +0 -11
- package/dist/peer/creating-offer-state.d.ts +0 -12
- package/dist/peer/creating-offer-state.js +0 -45
- package/dist/peer/exchanging-ice-state.d.ts +0 -17
- package/dist/peer/exchanging-ice-state.js +0 -64
- package/dist/peer/failed-state.d.ts +0 -10
- package/dist/peer/failed-state.js +0 -16
- package/dist/peer/idle-state.d.ts +0 -7
- package/dist/peer/idle-state.js +0 -14
- package/dist/peer/index.d.ts +0 -71
- package/dist/peer/index.js +0 -176
- package/dist/peer/state.d.ts +0 -23
- package/dist/peer/state.js +0 -63
- package/dist/peer/types.d.ts +0 -43
- package/dist/peer/types.js +0 -1
- package/dist/peer/waiting-for-answer-state.d.ts +0 -17
- package/dist/peer/waiting-for-answer-state.js +0 -60
- package/dist/rondevu.d.ts +0 -184
- package/dist/rondevu.js +0 -171
- package/dist/service-pool.d.ts +0 -123
- package/dist/service-pool.js +0 -488
- package/dist/usernames.d.ts +0 -79
- package/dist/usernames.js +0 -153
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-safe EventBus with event name to payload type mapping
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* EventBus - Type-safe event emitter with inferred event data types
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* interface MyEvents {
|
|
9
|
+
* 'user:connected': { userId: string; timestamp: number };
|
|
10
|
+
* 'user:disconnected': { userId: string };
|
|
11
|
+
* 'message:received': string;
|
|
12
|
+
* }
|
|
13
|
+
*
|
|
14
|
+
* const bus = new EventBus<MyEvents>();
|
|
15
|
+
*
|
|
16
|
+
* // TypeScript knows data is { userId: string; timestamp: number }
|
|
17
|
+
* bus.on('user:connected', (data) => {
|
|
18
|
+
* console.log(data.userId, data.timestamp);
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* // TypeScript knows data is string
|
|
22
|
+
* bus.on('message:received', (data) => {
|
|
23
|
+
* console.log(data.toUpperCase());
|
|
24
|
+
* });
|
|
25
|
+
*/
|
|
26
|
+
export class EventBus {
|
|
27
|
+
constructor() {
|
|
28
|
+
this.handlers = new Map();
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Subscribe to an event
|
|
32
|
+
* Returns a cleanup function to unsubscribe
|
|
33
|
+
*/
|
|
34
|
+
on(event, handler) {
|
|
35
|
+
if (!this.handlers.has(event)) {
|
|
36
|
+
this.handlers.set(event, new Set());
|
|
37
|
+
}
|
|
38
|
+
this.handlers.get(event).add(handler);
|
|
39
|
+
// Return cleanup function
|
|
40
|
+
return () => this.off(event, handler);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Subscribe to an event once (auto-unsubscribe after first call)
|
|
44
|
+
*/
|
|
45
|
+
once(event, handler) {
|
|
46
|
+
const wrappedHandler = (data) => {
|
|
47
|
+
handler(data);
|
|
48
|
+
this.off(event, wrappedHandler);
|
|
49
|
+
};
|
|
50
|
+
this.on(event, wrappedHandler);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Unsubscribe from an event
|
|
54
|
+
*/
|
|
55
|
+
off(event, handler) {
|
|
56
|
+
const eventHandlers = this.handlers.get(event);
|
|
57
|
+
if (eventHandlers) {
|
|
58
|
+
eventHandlers.delete(handler);
|
|
59
|
+
if (eventHandlers.size === 0) {
|
|
60
|
+
this.handlers.delete(event);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Emit an event with data
|
|
66
|
+
*/
|
|
67
|
+
emit(event, data) {
|
|
68
|
+
const eventHandlers = this.handlers.get(event);
|
|
69
|
+
if (eventHandlers) {
|
|
70
|
+
eventHandlers.forEach(handler => handler(data));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Remove all handlers for a specific event, or all handlers if no event specified
|
|
75
|
+
*/
|
|
76
|
+
clear(event) {
|
|
77
|
+
if (event !== undefined) {
|
|
78
|
+
this.handlers.delete(event);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
this.handlers.clear();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @xtr-dev/rondevu-client
|
|
3
|
-
* WebRTC peer signaling
|
|
3
|
+
* WebRTC peer signaling client
|
|
4
4
|
*/
|
|
5
|
-
export {
|
|
6
|
-
export
|
|
7
|
-
export {
|
|
8
|
-
export
|
|
9
|
-
export {
|
|
10
|
-
export
|
|
11
|
-
export {
|
|
12
|
-
export {
|
|
13
|
-
export {
|
|
14
|
-
export type {
|
|
5
|
+
export { EventBus } from './event-bus.js';
|
|
6
|
+
export { RondevuAPI } from './api.js';
|
|
7
|
+
export { RondevuService } from './rondevu-service.js';
|
|
8
|
+
export { RondevuSignaler } from './signaler.js';
|
|
9
|
+
export { ServiceHost } from './service-host.js';
|
|
10
|
+
export { ServiceClient } from './service-client.js';
|
|
11
|
+
export { WebRTCRondevuConnection } from './connection.js';
|
|
12
|
+
export { createBin } from './bin.js';
|
|
13
|
+
export type { ConnectionInterface, QueueMessageOptions, Message, ConnectionEvents, Signaler, } from './types.js';
|
|
14
|
+
export type { Credentials, Keypair, OfferRequest, Offer, ServiceRequest, Service, IceCandidate, } from './api.js';
|
|
15
|
+
export type { Binnable } from './bin.js';
|
|
16
|
+
export type { RondevuServiceOptions, PublishServiceOptions } from './rondevu-service.js';
|
|
17
|
+
export type { ServiceHostOptions, ServiceHostEvents } from './service-host.js';
|
|
18
|
+
export type { ServiceClientOptions, ServiceClientEvents } from './service-client.js';
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @xtr-dev/rondevu-client
|
|
3
|
-
* WebRTC peer signaling
|
|
3
|
+
* WebRTC peer signaling client
|
|
4
4
|
*/
|
|
5
|
-
|
|
6
|
-
export {
|
|
7
|
-
|
|
8
|
-
export {
|
|
9
|
-
|
|
10
|
-
export {
|
|
11
|
-
|
|
12
|
-
export {
|
|
13
|
-
export { DurableChannel } from './durable/channel.js';
|
|
14
|
-
export { DurableService } from './durable/service.js';
|
|
5
|
+
export { EventBus } from './event-bus.js';
|
|
6
|
+
export { RondevuAPI } from './api.js';
|
|
7
|
+
export { RondevuService } from './rondevu-service.js';
|
|
8
|
+
export { RondevuSignaler } from './signaler.js';
|
|
9
|
+
export { ServiceHost } from './service-host.js';
|
|
10
|
+
export { ServiceClient } from './service-client.js';
|
|
11
|
+
export { WebRTCRondevuConnection } from './connection.js';
|
|
12
|
+
export { createBin } from './bin.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Signaler } from './types.js';
|
|
2
|
+
import { Binnable } from './bin.js';
|
|
3
|
+
/**
|
|
4
|
+
* NoOpSignaler - A signaler that does nothing
|
|
5
|
+
* Used as a placeholder during connection setup before the real signaler is available
|
|
6
|
+
*/
|
|
7
|
+
export declare class NoOpSignaler implements Signaler {
|
|
8
|
+
addIceCandidate(_candidate: RTCIceCandidate): void;
|
|
9
|
+
addListener(_callback: (candidate: RTCIceCandidate) => void): Binnable;
|
|
10
|
+
addOfferListener(_callback: (offer: RTCSessionDescriptionInit) => void): Binnable;
|
|
11
|
+
addAnswerListener(_callback: (answer: RTCSessionDescriptionInit) => void): Binnable;
|
|
12
|
+
setOffer(_offer: RTCSessionDescriptionInit): Promise<void>;
|
|
13
|
+
setAnswer(_answer: RTCSessionDescriptionInit): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NoOpSignaler - A signaler that does nothing
|
|
3
|
+
* Used as a placeholder during connection setup before the real signaler is available
|
|
4
|
+
*/
|
|
5
|
+
export class NoOpSignaler {
|
|
6
|
+
addIceCandidate(_candidate) {
|
|
7
|
+
// No-op
|
|
8
|
+
}
|
|
9
|
+
addListener(_callback) {
|
|
10
|
+
// Return no-op cleanup function
|
|
11
|
+
return () => { };
|
|
12
|
+
}
|
|
13
|
+
addOfferListener(_callback) {
|
|
14
|
+
// Return no-op cleanup function
|
|
15
|
+
return () => { };
|
|
16
|
+
}
|
|
17
|
+
addAnswerListener(_callback) {
|
|
18
|
+
// Return no-op cleanup function
|
|
19
|
+
return () => { };
|
|
20
|
+
}
|
|
21
|
+
async setOffer(_offer) {
|
|
22
|
+
// No-op
|
|
23
|
+
}
|
|
24
|
+
async setAnswer(_answer) {
|
|
25
|
+
// No-op
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { RondevuAPI, Credentials, Keypair, Service } from './api.js';
|
|
2
|
+
export interface RondevuServiceOptions {
|
|
3
|
+
apiUrl: string;
|
|
4
|
+
username: string;
|
|
5
|
+
keypair?: Keypair;
|
|
6
|
+
credentials?: Credentials;
|
|
7
|
+
}
|
|
8
|
+
export interface PublishServiceOptions {
|
|
9
|
+
serviceFqn: string;
|
|
10
|
+
sdp: string;
|
|
11
|
+
ttl?: number;
|
|
12
|
+
isPublic?: boolean;
|
|
13
|
+
metadata?: Record<string, any>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* RondevuService - High-level service management with automatic signature handling
|
|
17
|
+
*
|
|
18
|
+
* Provides a simplified API for:
|
|
19
|
+
* - Username claiming with Ed25519 signatures
|
|
20
|
+
* - Service publishing with automatic signature generation
|
|
21
|
+
* - Keypair management
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* // Initialize service (generates keypair automatically)
|
|
26
|
+
* const service = new RondevuService({
|
|
27
|
+
* apiUrl: 'https://signal.example.com',
|
|
28
|
+
* username: 'myusername',
|
|
29
|
+
* })
|
|
30
|
+
*
|
|
31
|
+
* await service.initialize()
|
|
32
|
+
*
|
|
33
|
+
* // Claim username (one time)
|
|
34
|
+
* await service.claimUsername()
|
|
35
|
+
*
|
|
36
|
+
* // Publish a service
|
|
37
|
+
* const publishedService = await service.publishService({
|
|
38
|
+
* serviceFqn: 'chat.app@1.0.0',
|
|
39
|
+
* sdp: offerSdp,
|
|
40
|
+
* ttl: 300000,
|
|
41
|
+
* isPublic: true,
|
|
42
|
+
* })
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export declare class RondevuService {
|
|
46
|
+
private readonly api;
|
|
47
|
+
private readonly username;
|
|
48
|
+
private keypair;
|
|
49
|
+
private usernameClaimed;
|
|
50
|
+
constructor(options: RondevuServiceOptions);
|
|
51
|
+
/**
|
|
52
|
+
* Initialize the service - generates keypair if not provided
|
|
53
|
+
* Call this before using other methods
|
|
54
|
+
*/
|
|
55
|
+
initialize(): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Claim the username with Ed25519 signature
|
|
58
|
+
* Should be called once before publishing services
|
|
59
|
+
*/
|
|
60
|
+
claimUsername(): Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Publish a service with automatic signature generation
|
|
63
|
+
*/
|
|
64
|
+
publishService(options: PublishServiceOptions): Promise<Service>;
|
|
65
|
+
/**
|
|
66
|
+
* Get the current keypair (for backup/storage)
|
|
67
|
+
*/
|
|
68
|
+
getKeypair(): Keypair | null;
|
|
69
|
+
/**
|
|
70
|
+
* Get the public key
|
|
71
|
+
*/
|
|
72
|
+
getPublicKey(): string | null;
|
|
73
|
+
/**
|
|
74
|
+
* Check if username has been claimed
|
|
75
|
+
*/
|
|
76
|
+
isUsernameClaimed(): boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Access to underlying API for advanced operations
|
|
79
|
+
*/
|
|
80
|
+
getAPI(): RondevuAPI;
|
|
81
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { RondevuAPI } from './api.js';
|
|
2
|
+
/**
|
|
3
|
+
* RondevuService - High-level service management with automatic signature handling
|
|
4
|
+
*
|
|
5
|
+
* Provides a simplified API for:
|
|
6
|
+
* - Username claiming with Ed25519 signatures
|
|
7
|
+
* - Service publishing with automatic signature generation
|
|
8
|
+
* - Keypair management
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* // Initialize service (generates keypair automatically)
|
|
13
|
+
* const service = new RondevuService({
|
|
14
|
+
* apiUrl: 'https://signal.example.com',
|
|
15
|
+
* username: 'myusername',
|
|
16
|
+
* })
|
|
17
|
+
*
|
|
18
|
+
* await service.initialize()
|
|
19
|
+
*
|
|
20
|
+
* // Claim username (one time)
|
|
21
|
+
* await service.claimUsername()
|
|
22
|
+
*
|
|
23
|
+
* // Publish a service
|
|
24
|
+
* const publishedService = await service.publishService({
|
|
25
|
+
* serviceFqn: 'chat.app@1.0.0',
|
|
26
|
+
* sdp: offerSdp,
|
|
27
|
+
* ttl: 300000,
|
|
28
|
+
* isPublic: true,
|
|
29
|
+
* })
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export class RondevuService {
|
|
33
|
+
constructor(options) {
|
|
34
|
+
this.keypair = null;
|
|
35
|
+
this.usernameClaimed = false;
|
|
36
|
+
this.username = options.username;
|
|
37
|
+
this.keypair = options.keypair || null;
|
|
38
|
+
this.api = new RondevuAPI(options.apiUrl, options.credentials);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Initialize the service - generates keypair if not provided
|
|
42
|
+
* Call this before using other methods
|
|
43
|
+
*/
|
|
44
|
+
async initialize() {
|
|
45
|
+
if (!this.keypair) {
|
|
46
|
+
this.keypair = await RondevuAPI.generateKeypair();
|
|
47
|
+
}
|
|
48
|
+
// Register with API if no credentials provided
|
|
49
|
+
if (!this.api['credentials']) {
|
|
50
|
+
const credentials = await this.api.register();
|
|
51
|
+
this.api.credentials = credentials;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Claim the username with Ed25519 signature
|
|
56
|
+
* Should be called once before publishing services
|
|
57
|
+
*/
|
|
58
|
+
async claimUsername() {
|
|
59
|
+
if (!this.keypair) {
|
|
60
|
+
throw new Error('Service not initialized. Call initialize() first.');
|
|
61
|
+
}
|
|
62
|
+
// Check if username is already claimed
|
|
63
|
+
const check = await this.api.checkUsername(this.username);
|
|
64
|
+
if (!check.available) {
|
|
65
|
+
// Verify it's claimed by us
|
|
66
|
+
if (check.owner === this.keypair.publicKey) {
|
|
67
|
+
this.usernameClaimed = true;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
throw new Error(`Username "${this.username}" is already claimed by another user`);
|
|
71
|
+
}
|
|
72
|
+
// Generate signature for username claim
|
|
73
|
+
const message = `claim-username-${this.username}-${Date.now()}`;
|
|
74
|
+
const signature = await RondevuAPI.signMessage(message, this.keypair.privateKey);
|
|
75
|
+
// Claim the username
|
|
76
|
+
await this.api.claimUsername(this.username, this.keypair.publicKey, signature, message);
|
|
77
|
+
this.usernameClaimed = true;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Publish a service with automatic signature generation
|
|
81
|
+
*/
|
|
82
|
+
async publishService(options) {
|
|
83
|
+
if (!this.keypair) {
|
|
84
|
+
throw new Error('Service not initialized. Call initialize() first.');
|
|
85
|
+
}
|
|
86
|
+
if (!this.usernameClaimed) {
|
|
87
|
+
throw new Error('Username not claimed. Call claimUsername() first or the server will reject the service.');
|
|
88
|
+
}
|
|
89
|
+
const { serviceFqn, sdp, ttl, isPublic, metadata } = options;
|
|
90
|
+
// Generate signature for service publication
|
|
91
|
+
const message = `publish-${this.username}-${serviceFqn}-${Date.now()}`;
|
|
92
|
+
const signature = await RondevuAPI.signMessage(message, this.keypair.privateKey);
|
|
93
|
+
// Create service request
|
|
94
|
+
const serviceRequest = {
|
|
95
|
+
username: this.username,
|
|
96
|
+
serviceFqn,
|
|
97
|
+
sdp,
|
|
98
|
+
signature,
|
|
99
|
+
message,
|
|
100
|
+
ttl,
|
|
101
|
+
isPublic,
|
|
102
|
+
metadata,
|
|
103
|
+
};
|
|
104
|
+
// Publish to server
|
|
105
|
+
return await this.api.publishService(serviceRequest);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Get the current keypair (for backup/storage)
|
|
109
|
+
*/
|
|
110
|
+
getKeypair() {
|
|
111
|
+
return this.keypair;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Get the public key
|
|
115
|
+
*/
|
|
116
|
+
getPublicKey() {
|
|
117
|
+
return this.keypair?.publicKey || null;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Check if username has been claimed
|
|
121
|
+
*/
|
|
122
|
+
isUsernameClaimed() {
|
|
123
|
+
return this.usernameClaimed;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Access to underlying API for advanced operations
|
|
127
|
+
*/
|
|
128
|
+
getAPI() {
|
|
129
|
+
return this.api;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { WebRTCRondevuConnection } from './connection.js';
|
|
2
|
+
import { RondevuService } from './rondevu-service.js';
|
|
3
|
+
import { EventBus } from './event-bus.js';
|
|
4
|
+
import { ConnectionInterface } from './types.js';
|
|
5
|
+
export interface ServiceClientOptions {
|
|
6
|
+
username: string;
|
|
7
|
+
serviceFqn: string;
|
|
8
|
+
rondevuService: RondevuService;
|
|
9
|
+
autoReconnect?: boolean;
|
|
10
|
+
reconnectDelay?: number;
|
|
11
|
+
maxReconnectAttempts?: number;
|
|
12
|
+
rtcConfiguration?: RTCConfiguration;
|
|
13
|
+
}
|
|
14
|
+
export interface ServiceClientEvents {
|
|
15
|
+
connected: ConnectionInterface;
|
|
16
|
+
disconnected: {
|
|
17
|
+
reason: string;
|
|
18
|
+
};
|
|
19
|
+
reconnecting: {
|
|
20
|
+
attempt: number;
|
|
21
|
+
maxAttempts: number;
|
|
22
|
+
};
|
|
23
|
+
error: Error;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* ServiceClient - Connects to a hosted service
|
|
27
|
+
*
|
|
28
|
+
* Searches for available service offers and establishes a WebRTC connection.
|
|
29
|
+
* Optionally supports automatic reconnection on failure.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* const rondevuService = new RondevuService({
|
|
34
|
+
* apiUrl: 'https://signal.example.com',
|
|
35
|
+
* username: 'client-user',
|
|
36
|
+
* })
|
|
37
|
+
*
|
|
38
|
+
* await rondevuService.initialize()
|
|
39
|
+
*
|
|
40
|
+
* const client = new ServiceClient({
|
|
41
|
+
* username: 'host-user',
|
|
42
|
+
* serviceFqn: 'chat.app@1.0.0',
|
|
43
|
+
* rondevuService,
|
|
44
|
+
* autoReconnect: true,
|
|
45
|
+
* })
|
|
46
|
+
*
|
|
47
|
+
* await client.connect()
|
|
48
|
+
*
|
|
49
|
+
* client.events.on('connected', (conn) => {
|
|
50
|
+
* console.log('Connected to service')
|
|
51
|
+
* conn.sendMessage('Hello!')
|
|
52
|
+
* })
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export declare class ServiceClient {
|
|
56
|
+
private readonly username;
|
|
57
|
+
private readonly serviceFqn;
|
|
58
|
+
private readonly rondevuService;
|
|
59
|
+
private readonly autoReconnect;
|
|
60
|
+
private readonly reconnectDelay;
|
|
61
|
+
private readonly maxReconnectAttempts;
|
|
62
|
+
private readonly rtcConfiguration?;
|
|
63
|
+
private connection;
|
|
64
|
+
private reconnectAttempts;
|
|
65
|
+
private reconnectTimeout;
|
|
66
|
+
private readonly bin;
|
|
67
|
+
private isConnecting;
|
|
68
|
+
readonly events: EventBus<ServiceClientEvents>;
|
|
69
|
+
constructor(options: ServiceClientOptions);
|
|
70
|
+
/**
|
|
71
|
+
* Connect to the service
|
|
72
|
+
*/
|
|
73
|
+
connect(): Promise<WebRTCRondevuConnection>;
|
|
74
|
+
/**
|
|
75
|
+
* Disconnect from the service
|
|
76
|
+
*/
|
|
77
|
+
disconnect(): void;
|
|
78
|
+
/**
|
|
79
|
+
* Get the current connection
|
|
80
|
+
*/
|
|
81
|
+
getConnection(): WebRTCRondevuConnection | null;
|
|
82
|
+
/**
|
|
83
|
+
* Check if currently connected
|
|
84
|
+
*/
|
|
85
|
+
isConnected(): boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Handle connection state changes
|
|
88
|
+
*/
|
|
89
|
+
private handleConnectionStateChange;
|
|
90
|
+
/**
|
|
91
|
+
* Schedule a reconnection attempt
|
|
92
|
+
*/
|
|
93
|
+
private scheduleReconnect;
|
|
94
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { WebRTCRondevuConnection } from './connection.js';
|
|
2
|
+
import { WebRTCContext } from './webrtc-context.js';
|
|
3
|
+
import { RondevuSignaler } from './signaler.js';
|
|
4
|
+
import { EventBus } from './event-bus.js';
|
|
5
|
+
import { createBin } from './bin.js';
|
|
6
|
+
/**
|
|
7
|
+
* ServiceClient - Connects to a hosted service
|
|
8
|
+
*
|
|
9
|
+
* Searches for available service offers and establishes a WebRTC connection.
|
|
10
|
+
* Optionally supports automatic reconnection on failure.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const rondevuService = new RondevuService({
|
|
15
|
+
* apiUrl: 'https://signal.example.com',
|
|
16
|
+
* username: 'client-user',
|
|
17
|
+
* })
|
|
18
|
+
*
|
|
19
|
+
* await rondevuService.initialize()
|
|
20
|
+
*
|
|
21
|
+
* const client = new ServiceClient({
|
|
22
|
+
* username: 'host-user',
|
|
23
|
+
* serviceFqn: 'chat.app@1.0.0',
|
|
24
|
+
* rondevuService,
|
|
25
|
+
* autoReconnect: true,
|
|
26
|
+
* })
|
|
27
|
+
*
|
|
28
|
+
* await client.connect()
|
|
29
|
+
*
|
|
30
|
+
* client.events.on('connected', (conn) => {
|
|
31
|
+
* console.log('Connected to service')
|
|
32
|
+
* conn.sendMessage('Hello!')
|
|
33
|
+
* })
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export class ServiceClient {
|
|
37
|
+
constructor(options) {
|
|
38
|
+
this.connection = null;
|
|
39
|
+
this.reconnectAttempts = 0;
|
|
40
|
+
this.reconnectTimeout = null;
|
|
41
|
+
this.bin = createBin();
|
|
42
|
+
this.isConnecting = false;
|
|
43
|
+
this.events = new EventBus();
|
|
44
|
+
this.username = options.username;
|
|
45
|
+
this.serviceFqn = options.serviceFqn;
|
|
46
|
+
this.rondevuService = options.rondevuService;
|
|
47
|
+
this.autoReconnect = options.autoReconnect !== false;
|
|
48
|
+
this.reconnectDelay = options.reconnectDelay || 2000;
|
|
49
|
+
this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
|
|
50
|
+
this.rtcConfiguration = options.rtcConfiguration;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Connect to the service
|
|
54
|
+
*/
|
|
55
|
+
async connect() {
|
|
56
|
+
if (this.isConnecting) {
|
|
57
|
+
throw new Error('Already connecting');
|
|
58
|
+
}
|
|
59
|
+
if (this.connection && this.connection.state === 'connected') {
|
|
60
|
+
return this.connection;
|
|
61
|
+
}
|
|
62
|
+
this.isConnecting = true;
|
|
63
|
+
try {
|
|
64
|
+
// Search for available services
|
|
65
|
+
const services = await this.rondevuService
|
|
66
|
+
.getAPI()
|
|
67
|
+
.searchServices(this.username, this.serviceFqn);
|
|
68
|
+
if (services.length === 0) {
|
|
69
|
+
throw new Error(`No services found for ${this.username}/${this.serviceFqn}`);
|
|
70
|
+
}
|
|
71
|
+
// Get the first available service
|
|
72
|
+
const service = services[0];
|
|
73
|
+
// Get service details including SDP
|
|
74
|
+
const serviceDetails = await this.rondevuService.getAPI().getService(service.uuid);
|
|
75
|
+
// Create WebRTC context with signaler for this offer
|
|
76
|
+
const signaler = new RondevuSignaler(this.rondevuService.getAPI(), serviceDetails.offerId);
|
|
77
|
+
const context = new WebRTCContext(signaler, this.rtcConfiguration);
|
|
78
|
+
// Create connection (answerer role)
|
|
79
|
+
const conn = new WebRTCRondevuConnection({
|
|
80
|
+
id: `client-${this.serviceFqn}-${Date.now()}`,
|
|
81
|
+
service: this.serviceFqn,
|
|
82
|
+
offer: {
|
|
83
|
+
type: 'offer',
|
|
84
|
+
sdp: serviceDetails.sdp,
|
|
85
|
+
},
|
|
86
|
+
context,
|
|
87
|
+
});
|
|
88
|
+
// Wait for answer to be created
|
|
89
|
+
await conn.ready;
|
|
90
|
+
// Get answer SDP
|
|
91
|
+
if (!conn.connection?.localDescription?.sdp) {
|
|
92
|
+
throw new Error('Failed to create answer SDP');
|
|
93
|
+
}
|
|
94
|
+
const answerSdp = conn.connection.localDescription.sdp;
|
|
95
|
+
// Send answer to server
|
|
96
|
+
await this.rondevuService.getAPI().answerOffer(serviceDetails.offerId, answerSdp);
|
|
97
|
+
// Track connection
|
|
98
|
+
this.connection = conn;
|
|
99
|
+
this.reconnectAttempts = 0;
|
|
100
|
+
// Listen for state changes
|
|
101
|
+
const cleanup = conn.events.on('state-change', state => {
|
|
102
|
+
this.handleConnectionStateChange(state);
|
|
103
|
+
});
|
|
104
|
+
this.bin(cleanup);
|
|
105
|
+
this.isConnecting = false;
|
|
106
|
+
// Emit connected event when actually connected
|
|
107
|
+
if (conn.state === 'connected') {
|
|
108
|
+
this.events.emit('connected', conn);
|
|
109
|
+
}
|
|
110
|
+
return conn;
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
this.isConnecting = false;
|
|
114
|
+
this.events.emit('error', error);
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Disconnect from the service
|
|
120
|
+
*/
|
|
121
|
+
disconnect() {
|
|
122
|
+
if (this.reconnectTimeout) {
|
|
123
|
+
clearTimeout(this.reconnectTimeout);
|
|
124
|
+
this.reconnectTimeout = null;
|
|
125
|
+
}
|
|
126
|
+
if (this.connection) {
|
|
127
|
+
this.connection.disconnect();
|
|
128
|
+
this.connection = null;
|
|
129
|
+
}
|
|
130
|
+
this.bin.clean();
|
|
131
|
+
this.reconnectAttempts = 0;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Get the current connection
|
|
135
|
+
*/
|
|
136
|
+
getConnection() {
|
|
137
|
+
return this.connection;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Check if currently connected
|
|
141
|
+
*/
|
|
142
|
+
isConnected() {
|
|
143
|
+
return this.connection?.state === 'connected';
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Handle connection state changes
|
|
147
|
+
*/
|
|
148
|
+
handleConnectionStateChange(state) {
|
|
149
|
+
if (state === 'connected') {
|
|
150
|
+
this.events.emit('connected', this.connection);
|
|
151
|
+
this.reconnectAttempts = 0;
|
|
152
|
+
}
|
|
153
|
+
else if (state === 'disconnected') {
|
|
154
|
+
this.events.emit('disconnected', { reason: 'Connection closed' });
|
|
155
|
+
// Attempt reconnection if enabled
|
|
156
|
+
if (this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
157
|
+
this.scheduleReconnect();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Schedule a reconnection attempt
|
|
163
|
+
*/
|
|
164
|
+
scheduleReconnect() {
|
|
165
|
+
if (this.reconnectTimeout) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
this.reconnectAttempts++;
|
|
169
|
+
this.events.emit('reconnecting', {
|
|
170
|
+
attempt: this.reconnectAttempts,
|
|
171
|
+
maxAttempts: this.maxReconnectAttempts,
|
|
172
|
+
});
|
|
173
|
+
// Exponential backoff
|
|
174
|
+
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
|
|
175
|
+
this.reconnectTimeout = setTimeout(() => {
|
|
176
|
+
this.reconnectTimeout = null;
|
|
177
|
+
this.connect().catch(error => {
|
|
178
|
+
this.events.emit('error', error);
|
|
179
|
+
// Schedule next attempt if we haven't exceeded max attempts
|
|
180
|
+
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
181
|
+
this.scheduleReconnect();
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}, delay);
|
|
185
|
+
}
|
|
186
|
+
}
|