ulai-js-sdk 0.1.0 → 0.1.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/dist/events.js CHANGED
@@ -8,63 +8,170 @@ const eventemitter3_1 = __importDefault(require("eventemitter3"));
8
8
  class EventsClient extends eventemitter3_1.default {
9
9
  ws = null;
10
10
  url;
11
+ sessionId = null;
12
+ eventsToken = null;
13
+ lastEventId = null;
14
+ isClosedManually = false;
15
+ retryAttempts = 0;
16
+ maxRetries = 5;
17
+ reconnectTimer = null;
18
+ heartbeatTimer = null;
11
19
  constructor(cpBase) {
12
20
  super();
13
- const scheme = cpBase.startsWith('https') ? 'wss' : 'ws';
14
- const host = cpBase.replace(/^https?:\/\//, '').replace(/\/$/, '');
15
- this.url = `${scheme}://${host}/api/v1/events`;
21
+ this.url = this.buildUrl(cpBase);
22
+ }
23
+ buildUrl(cpBase) {
24
+ const cleanHost = cpBase
25
+ .replace(/^https?:\/\//, '')
26
+ .replace(/^wss?:\/\//, '')
27
+ .replace(/\/$/, '');
28
+ const isLocal = cleanHost.startsWith('localhost') ||
29
+ cleanHost.startsWith('127.0.0.1');
30
+ let scheme = 'wss';
31
+ if (isLocal) {
32
+ scheme = 'ws';
33
+ }
34
+ else if (typeof window !== 'undefined' && window.location.protocol === 'http:') {
35
+ scheme = 'ws';
36
+ }
37
+ else if (cpBase.startsWith('http://')) {
38
+ scheme = 'ws';
39
+ }
40
+ return `${scheme}://${cleanHost}/api/v1/events`;
16
41
  }
17
42
  connect(sessionId, eventsToken) {
18
- this.ws = new WebSocket(this.url);
19
- this.ws.onopen = () => {
20
- this.ws?.send(JSON.stringify({
21
- action: 'subscribe',
22
- scope: 'session',
23
- session_id: sessionId,
24
- token: eventsToken
25
- }));
26
- };
27
- this.ws.onmessage = (event) => {
28
- try {
29
- const msg = JSON.parse(event.data);
30
- if (msg.type === 'subscribed') {
31
- this.emit('connected', msg);
32
- }
33
- else if (msg.type === 'transcript_delta') {
34
- this.emit('transcript_delta', msg);
43
+ this.sessionId = sessionId;
44
+ this.eventsToken = eventsToken;
45
+ this.isClosedManually = false;
46
+ this.cleanupSocket();
47
+ try {
48
+ this.ws = new WebSocket(this.url);
49
+ this.ws.onopen = () => {
50
+ this.retryAttempts = 0;
51
+ this.sendSubscribe();
52
+ this.startHeartbeat();
53
+ };
54
+ this.ws.onmessage = (event) => {
55
+ try {
56
+ const msg = JSON.parse(event.data);
57
+ if (msg.id) {
58
+ this.lastEventId = msg.id;
59
+ }
60
+ if (msg.type === 'subscribed') {
61
+ this.emit('connected', msg);
62
+ }
63
+ else if (msg.type === 'transcript_delta') {
64
+ this.emit('transcript_delta', msg);
65
+ }
66
+ else if (msg.type === 'roster') {
67
+ this.emit('roster', msg);
68
+ }
69
+ else if (msg.type === 'session_terminated') {
70
+ this.emit('session_terminated', msg);
71
+ }
72
+ else if (msg.type === 'participant_joined') {
73
+ this.emit('participant_joined', msg);
74
+ }
75
+ else if (msg.type === 'participant_left') {
76
+ this.emit('participant_left', msg);
77
+ }
78
+ else if (msg.type === 'error') {
79
+ this.emit('error', msg);
80
+ }
35
81
  }
36
- else if (msg.type === 'roster') {
37
- this.emit('roster', msg);
82
+ catch (err) {
83
+ console.error('[Events] Failed to parse message', err);
38
84
  }
39
- else if (msg.type === 'session_terminated') {
40
- this.emit('session_terminated', msg);
85
+ };
86
+ this.ws.onclose = (event) => {
87
+ this.stopHeartbeat();
88
+ if (this.isClosedManually) {
89
+ this.emit('disconnected');
90
+ return;
41
91
  }
42
- else if (msg.type === 'participant_joined') {
43
- this.emit('participant_joined', msg);
92
+ if (this.retryAttempts < this.maxRetries) {
93
+ this.retryAttempts++;
94
+ const delay = Math.min(1000 * Math.pow(1.5, this.retryAttempts), 10000);
95
+ console.warn(`[Events] Event socket closed. Reconnecting in ${Math.round(delay)}ms (attempt ${this.retryAttempts}/${this.maxRetries})...`);
96
+ this.emit('reconnecting', { attempt: this.retryAttempts });
97
+ this.reconnectTimer = setTimeout(() => {
98
+ if (!this.isClosedManually && this.sessionId && this.eventsToken) {
99
+ this.connect(this.sessionId, this.eventsToken);
100
+ }
101
+ }, delay);
44
102
  }
45
- else if (msg.type === 'participant_left') {
46
- this.emit('participant_left', msg);
103
+ else {
104
+ console.error('[Events] Max reconnect attempts exceeded for events socket');
105
+ this.emit('disconnected', event);
47
106
  }
48
- else if (msg.type === 'error') {
49
- this.emit('error', msg);
107
+ };
108
+ this.ws.onerror = (error) => {
109
+ this.emit('error', error);
110
+ };
111
+ }
112
+ catch (err) {
113
+ this.emit('error', err);
114
+ }
115
+ }
116
+ sendSubscribe() {
117
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.sessionId || !this.eventsToken) {
118
+ return;
119
+ }
120
+ const payload = {
121
+ action: 'subscribe',
122
+ scope: 'session',
123
+ session_id: this.sessionId,
124
+ token: this.eventsToken,
125
+ };
126
+ if (this.lastEventId) {
127
+ payload.last_event_id = this.lastEventId;
128
+ }
129
+ try {
130
+ this.ws.send(JSON.stringify(payload));
131
+ }
132
+ catch (err) {
133
+ console.error('[Events] Failed to send subscription', err);
134
+ }
135
+ }
136
+ startHeartbeat() {
137
+ this.stopHeartbeat();
138
+ this.heartbeatTimer = setInterval(() => {
139
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
140
+ try {
141
+ this.ws.send(JSON.stringify({ action: 'ping' }));
50
142
  }
143
+ catch (_) { }
51
144
  }
52
- catch (err) {
53
- console.error('Failed to parse events message', err);
54
- }
55
- };
56
- this.ws.onclose = () => {
57
- this.emit('disconnected');
58
- };
59
- this.ws.onerror = (error) => {
60
- this.emit('error', error);
61
- };
145
+ }, 25000);
62
146
  }
63
- close() {
147
+ stopHeartbeat() {
148
+ if (this.heartbeatTimer) {
149
+ clearInterval(this.heartbeatTimer);
150
+ this.heartbeatTimer = null;
151
+ }
152
+ }
153
+ cleanupSocket() {
154
+ this.stopHeartbeat();
155
+ if (this.reconnectTimer) {
156
+ clearTimeout(this.reconnectTimer);
157
+ this.reconnectTimer = null;
158
+ }
64
159
  if (this.ws) {
65
- this.ws.close();
160
+ this.ws.onopen = null;
161
+ this.ws.onmessage = null;
162
+ this.ws.onclose = null;
163
+ this.ws.onerror = null;
164
+ try {
165
+ this.ws.close();
166
+ }
167
+ catch (_) { }
66
168
  this.ws = null;
67
169
  }
68
170
  }
171
+ close() {
172
+ this.isClosedManually = true;
173
+ this.cleanupSocket();
174
+ this.emit('disconnected');
175
+ }
69
176
  }
70
177
  exports.EventsClient = EventsClient;
package/dist/room.d.ts CHANGED
@@ -5,14 +5,42 @@ export declare class Room extends EventEmitter<RoomEvent> {
5
5
  private signaling;
6
6
  private eventsClient;
7
7
  private peerConnection;
8
+ private audioTransceiver;
8
9
  private localStream;
9
10
  private remoteStreams;
10
11
  roomId: string | null;
11
12
  participantId: string | null;
13
+ private readonly isPolite;
14
+ private makingOffer;
15
+ private candidateQueue;
16
+ private signalQueue;
12
17
  constructor(cpBase: string);
13
18
  connect(options: JoinRoomOptions): Promise<void>;
14
19
  private setupEventsClient;
15
20
  private setupSignaling;
16
- publishAudio(): Promise<void>;
21
+ private handleSignal;
22
+ private safeNegotiate;
23
+ private handleIncomingCandidate;
24
+ private drainCandidateQueue;
25
+ /**
26
+ * Publishes local microphone audio to the room.
27
+ */
28
+ publishAudio(): Promise<MediaStream>;
29
+ /**
30
+ * Sets microphone mute state.
31
+ */
32
+ setMuted(muted: boolean): void;
33
+ /**
34
+ * Returns whether the microphone is currently muted.
35
+ */
36
+ isMuted(): boolean;
37
+ /**
38
+ * Gets the active local media stream.
39
+ */
40
+ getLocalStream(): MediaStream | null;
41
+ /**
42
+ * Gets all subscribed remote audio streams.
43
+ */
44
+ getRemoteStreams(): MediaStream[];
17
45
  disconnect(): void;
18
46
  }
package/dist/room.js CHANGED
@@ -12,10 +12,18 @@ class Room extends eventemitter3_1.default {
12
12
  signaling = null;
13
13
  eventsClient = null;
14
14
  peerConnection = null;
15
+ audioTransceiver = null;
15
16
  localStream = null;
16
17
  remoteStreams = new Map();
17
18
  roomId = null;
18
19
  participantId = null;
20
+ // Perfect Negotiation State (Client is the polite peer)
21
+ isPolite = true;
22
+ makingOffer = false;
23
+ candidateQueue = [];
24
+ // Signaling messages are handled one at a time, in arrival order. Handled concurrently, a bridge
25
+ // offer could be processed in the middle of our own offer and dropped.
26
+ signalQueue = Promise.resolve();
19
27
  constructor(cpBase) {
20
28
  super();
21
29
  this.cpBase = cpBase;
@@ -23,43 +31,75 @@ class Room extends eventemitter3_1.default {
23
31
  async connect(options) {
24
32
  this.roomId = options.roomId;
25
33
  this.participantId = options.participantId || '';
26
- // 1. Setup Control Plane Events
34
+ // 1. Setup Control Plane Events (Transcripts, Roster, Session Lifecycle)
27
35
  if (options.eventsToken) {
28
36
  this.eventsClient = new events_1.EventsClient(this.cpBase);
29
37
  this.setupEventsClient();
30
38
  this.eventsClient.connect(this.roomId, options.eventsToken);
31
39
  }
32
- // 2. Setup WebRTC Signaling if nodeAddress and ticket are provided
40
+ // 2. Setup WebRTC Signaling & Media
33
41
  if (options.nodeAddress && options.ticket) {
34
42
  this.signaling = new signaling_1.SignalingClient(options.nodeAddress, options.ticket);
35
43
  this.setupSignaling();
36
- this.peerConnection = new RTCPeerConnection({
37
- iceServers: [
38
- { urls: 'stun:stun.l.google.com:19302' },
39
- { urls: ['turn:turn.ulai.in:3478', 'turns:turn.ulai.in:5349'], username: 'ulai', credential: 'xulaizrt' }
40
- ]
41
- });
44
+ const defaultIceServers = [
45
+ { urls: 'stun:stun.l.google.com:19302' },
46
+ ];
47
+ const iceServers = options.iceServers && options.iceServers.length > 0
48
+ ? options.iceServers
49
+ : defaultIceServers;
50
+ this.peerConnection = new RTCPeerConnection({ iceServers });
51
+ // Every local change that needs an offer (the initial transceiver, publishing the mic, an
52
+ // offer rolled back by a collision) fires this. Offers sent before signaling opens wait in
53
+ // its outbox.
54
+ this.peerConnection.onnegotiationneeded = () => {
55
+ void this.safeNegotiate();
56
+ };
57
+ // Pre-allocate audio transceiver as recvonly so the initial offer immediately
58
+ // negotiates audio codec capabilities (m=audio) and is ready for agent downlink.
59
+ try {
60
+ this.audioTransceiver = this.peerConnection.addTransceiver('audio', {
61
+ direction: 'recvonly',
62
+ });
63
+ }
64
+ catch (err) {
65
+ console.warn('[Room] Error adding initial audio transceiver:', err);
66
+ }
42
67
  this.peerConnection.onicecandidate = (event) => {
43
68
  if (event.candidate && this.signaling) {
44
- this.signaling.send({ type: 'candidate', candidate: JSON.stringify(event.candidate.toJSON()) });
69
+ this.signaling.send({
70
+ type: 'candidate',
71
+ candidate: JSON.stringify(event.candidate.toJSON()),
72
+ });
45
73
  }
46
74
  };
75
+ // Resilient ontrack handler: works with stream-grouped tracks and bare Pion SFU tracks
47
76
  this.peerConnection.ontrack = (event) => {
48
- const stream = event.streams[0];
49
- if (stream) {
50
- this.remoteStreams.set(stream.id, stream);
51
- this.emit('trackSubscribed', event.track, stream);
52
- }
77
+ const stream = event.streams && event.streams[0]
78
+ ? event.streams[0]
79
+ : new MediaStream([event.track]);
80
+ this.remoteStreams.set(stream.id, stream);
81
+ this.emit('trackSubscribed', event.track, stream);
82
+ event.track.onended = () => {
83
+ this.remoteStreams.delete(stream.id);
84
+ this.emit('trackUnsubscribed', event.track, stream);
85
+ };
53
86
  };
54
87
  this.peerConnection.onconnectionstatechange = () => {
55
- switch (this.peerConnection?.connectionState) {
88
+ const state = this.peerConnection?.connectionState;
89
+ switch (state) {
56
90
  case 'connected':
57
91
  this.emit('connected');
58
92
  break;
93
+ case 'connecting':
94
+ this.emit('reconnecting');
95
+ break;
59
96
  case 'disconnected':
60
97
  case 'failed':
61
98
  this.emit('disconnected');
62
99
  break;
100
+ case 'closed':
101
+ this.emit('disconnected');
102
+ break;
63
103
  }
64
104
  };
65
105
  this.signaling.connect();
@@ -81,65 +121,212 @@ class Room extends eventemitter3_1.default {
81
121
  setupSignaling() {
82
122
  if (!this.signaling)
83
123
  return;
84
- this.signaling.on('connected', async () => {
85
- // Create initial offer when signaling connects
86
- if (this.peerConnection) {
87
- const offer = await this.peerConnection.createOffer();
88
- await this.peerConnection.setLocalDescription(offer);
89
- this.signaling.send({ type: 'offer', sdp: this.peerConnection.localDescription?.sdp });
90
- }
124
+ // The initial offer comes from onnegotiationneeded and is flushed from the outbox on connect.
125
+ this.signaling.on('reconnected', async () => {
126
+ this.emit('reconnected');
127
+ await this.safeNegotiate();
91
128
  });
92
- this.signaling.on('message', async (msg) => {
93
- if (!this.peerConnection)
94
- return;
95
- try {
96
- if (msg.type === 'offer' || msg.type === 'restart-offer') {
97
- await this.peerConnection.setRemoteDescription(new RTCSessionDescription({ type: 'offer', sdp: msg.sdp }));
98
- const answer = await this.peerConnection.createAnswer();
99
- await this.peerConnection.setLocalDescription(answer);
100
- this.signaling.send({ type: 'answer', sdp: this.peerConnection.localDescription?.sdp });
129
+ this.signaling.on('message', (msg) => {
130
+ this.signalQueue = this.signalQueue.then(() => this.handleSignal(msg));
131
+ });
132
+ this.signaling.on('disconnected', () => {
133
+ this.emit('disconnected');
134
+ });
135
+ }
136
+ async handleSignal(msg) {
137
+ const pc = this.peerConnection;
138
+ if (!pc)
139
+ return;
140
+ try {
141
+ if (msg.type === 'offer' || msg.type === 'restart-offer') {
142
+ if (!msg.sdp)
143
+ return;
144
+ // Perfect negotiation: detect collision (SDP Glare)
145
+ const offerCollision = this.makingOffer || pc.signalingState !== 'stable';
146
+ if (offerCollision && !this.isPolite) {
147
+ console.warn('[Room] Impolite collision: ignoring incoming offer');
148
+ return;
101
149
  }
102
- else if (msg.type === 'answer') {
103
- await this.peerConnection.setRemoteDescription(new RTCSessionDescription({ type: 'answer', sdp: msg.sdp }));
150
+ if (offerCollision) {
151
+ console.log('[Room] Polite collision: rolling back local offer to accept server offer');
104
152
  }
105
- else if (msg.type === 'candidate' && msg.candidate) {
106
- await this.peerConnection.addIceCandidate(JSON.parse(msg.candidate));
153
+ // On a collision this implicitly rolls back our own offer, even one still being made. An
154
+ // explicit rollback throws if we're stable by the time it runs, which is what happens when
155
+ // the bridge sends its answer and then its own offer right away (someone already talking),
156
+ // and the bridge's offer was dropped. Whatever our offer carried is offered again via
157
+ // negotiationneeded once we're back to stable.
158
+ await pc.setRemoteDescription({ type: 'offer', sdp: msg.sdp });
159
+ await this.drainCandidateQueue();
160
+ await pc.setLocalDescription();
161
+ this.signaling?.send({
162
+ type: 'answer',
163
+ sdp: pc.localDescription?.sdp,
164
+ });
165
+ }
166
+ else if (msg.type === 'answer') {
167
+ if (!msg.sdp)
168
+ return;
169
+ // An answer to an offer we've since rolled back has nothing to apply to.
170
+ if (pc.signalingState !== 'have-local-offer') {
171
+ console.warn('[Room] Ignoring answer in signalingState', pc.signalingState);
172
+ return;
107
173
  }
174
+ await pc.setRemoteDescription({ type: 'answer', sdp: msg.sdp });
175
+ await this.drainCandidateQueue();
108
176
  }
109
- catch (err) {
110
- console.error('Error handling signaling message', err);
177
+ else if (msg.type === 'candidate' && msg.candidate) {
178
+ const candidateInit = JSON.parse(msg.candidate);
179
+ await this.handleIncomingCandidate(candidateInit);
111
180
  }
112
- });
113
- this.signaling.on('disconnected', () => {
114
- this.emit('disconnected');
115
- });
181
+ }
182
+ catch (err) {
183
+ console.error('[Room] Error handling signaling message:', err);
184
+ this.emit('error', err);
185
+ }
116
186
  }
187
+ async safeNegotiate() {
188
+ if (!this.peerConnection || this.makingOffer)
189
+ return;
190
+ // Mid-negotiation, setLocalDescription() would make an answer, not an offer. negotiationneeded
191
+ // fires again once the current exchange completes.
192
+ if (this.peerConnection.signalingState !== 'stable')
193
+ return;
194
+ try {
195
+ this.makingOffer = true;
196
+ // Without an argument the offer is created when it's applied, so it can't go stale against
197
+ // an offer from the bridge that arrives meanwhile.
198
+ await this.peerConnection.setLocalDescription();
199
+ if (this.peerConnection.pendingLocalDescription?.type !== 'offer')
200
+ return; // rolled back
201
+ this.signaling?.send({
202
+ type: 'offer',
203
+ sdp: this.peerConnection.localDescription?.sdp,
204
+ });
205
+ }
206
+ catch (err) {
207
+ console.error('[Room] safeNegotiate error:', err);
208
+ }
209
+ finally {
210
+ this.makingOffer = false;
211
+ }
212
+ }
213
+ async handleIncomingCandidate(candidate) {
214
+ if (!this.peerConnection?.remoteDescription || !this.peerConnection.remoteDescription.type) {
215
+ // Buffer candidates if remote description is not set yet
216
+ this.candidateQueue.push(candidate);
217
+ return;
218
+ }
219
+ try {
220
+ await this.peerConnection.addIceCandidate(candidate);
221
+ }
222
+ catch (err) {
223
+ console.warn('[Room] Failed to add ICE candidate:', err);
224
+ }
225
+ }
226
+ async drainCandidateQueue() {
227
+ while (this.candidateQueue.length > 0) {
228
+ const candidate = this.candidateQueue.shift();
229
+ if (candidate && this.peerConnection?.remoteDescription) {
230
+ try {
231
+ await this.peerConnection.addIceCandidate(candidate);
232
+ }
233
+ catch (err) {
234
+ console.warn('[Room] Error draining candidate:', err);
235
+ }
236
+ }
237
+ }
238
+ }
239
+ /**
240
+ * Publishes local microphone audio to the room.
241
+ */
117
242
  async publishAudio() {
118
243
  try {
119
- this.localStream = await navigator.mediaDevices.getUserMedia({ audio: true });
120
- if (this.peerConnection && this.localStream) {
121
- this.localStream.getTracks().forEach((track) => {
122
- this.peerConnection.addTrack(track, this.localStream);
123
- });
124
- // Renegotiate
125
- const offer = await this.peerConnection.createOffer();
126
- await this.peerConnection.setLocalDescription(offer);
127
- this.signaling?.send({ type: 'offer', sdp: this.peerConnection.localDescription?.sdp });
244
+ this.localStream = await navigator.mediaDevices.getUserMedia({
245
+ audio: {
246
+ echoCancellation: true,
247
+ noiseSuppression: true,
248
+ autoGainControl: true,
249
+ },
250
+ video: false,
251
+ });
252
+ const audioTrack = this.localStream.getAudioTracks()[0];
253
+ if (this.peerConnection && audioTrack) {
254
+ if (this.audioTransceiver) {
255
+ this.audioTransceiver.direction = 'sendrecv';
256
+ if (this.audioTransceiver.sender) {
257
+ await this.audioTransceiver.sender.replaceTrack(audioTrack);
258
+ }
259
+ }
260
+ else {
261
+ const transceiver = this.peerConnection
262
+ .getTransceivers()
263
+ .find((t) => t.receiver.track.kind === 'audio');
264
+ if (transceiver) {
265
+ this.audioTransceiver = transceiver;
266
+ this.audioTransceiver.direction = 'sendrecv';
267
+ await this.audioTransceiver.sender.replaceTrack(audioTrack);
268
+ }
269
+ else {
270
+ this.peerConnection.addTrack(audioTrack, this.localStream);
271
+ }
272
+ }
273
+ // The direction change (or addTrack) fires negotiationneeded, which sends the offer.
128
274
  }
275
+ return this.localStream;
129
276
  }
130
277
  catch (err) {
131
- console.error('Failed to publish audio:', err);
278
+ console.error('[Room] Failed to publish audio:', err);
132
279
  this.emit('error', err);
280
+ throw err;
133
281
  }
134
282
  }
283
+ /**
284
+ * Sets microphone mute state.
285
+ */
286
+ setMuted(muted) {
287
+ if (this.localStream) {
288
+ this.localStream.getAudioTracks().forEach((track) => {
289
+ track.enabled = !muted;
290
+ });
291
+ this.emit('muteChanged', muted);
292
+ }
293
+ }
294
+ /**
295
+ * Returns whether the microphone is currently muted.
296
+ */
297
+ isMuted() {
298
+ if (!this.localStream)
299
+ return true;
300
+ const tracks = this.localStream.getAudioTracks();
301
+ return tracks.length === 0 || !tracks[0].enabled;
302
+ }
303
+ /**
304
+ * Gets the active local media stream.
305
+ */
306
+ getLocalStream() {
307
+ return this.localStream;
308
+ }
309
+ /**
310
+ * Gets all subscribed remote audio streams.
311
+ */
312
+ getRemoteStreams() {
313
+ return Array.from(this.remoteStreams.values());
314
+ }
135
315
  disconnect() {
136
316
  if (this.localStream) {
137
- this.localStream.getTracks().forEach(track => track.stop());
317
+ this.localStream.getTracks().forEach((track) => track.stop());
318
+ this.localStream = null;
138
319
  }
139
320
  if (this.peerConnection) {
321
+ this.peerConnection.onicecandidate = null;
322
+ this.peerConnection.ontrack = null;
323
+ this.peerConnection.onconnectionstatechange = null;
140
324
  this.peerConnection.close();
141
325
  this.peerConnection = null;
142
326
  }
327
+ this.audioTransceiver = null;
328
+ this.candidateQueue = [];
329
+ this.remoteStreams.clear();
143
330
  if (this.signaling) {
144
331
  this.signaling.close();
145
332
  this.signaling = null;
@@ -3,8 +3,19 @@ import { SignalMessage } from './types';
3
3
  export declare class SignalingClient extends EventEmitter {
4
4
  private ws;
5
5
  private url;
6
+ private outbox;
7
+ private isClosedManually;
8
+ private retryAttempts;
9
+ private readonly maxRetries;
10
+ private reconnectTimer;
11
+ private heartbeatTimer;
6
12
  constructor(nodeAddress: string, ticket: string);
13
+ private buildUrl;
7
14
  connect(): void;
8
15
  send(message: SignalMessage): void;
16
+ private flushOutbox;
17
+ private startHeartbeat;
18
+ private stopHeartbeat;
19
+ private cleanupSocket;
9
20
  close(): void;
10
21
  }