ttp-agent-sdk 2.0.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/GETTING_STARTED.md +429 -0
- package/README.md +303 -0
- package/dist/agent-widget.js +3 -0
- package/dist/agent-widget.js.LICENSE.txt +21 -0
- package/dist/agent-widget.js.map +1 -0
- package/dist/examples/react-example.html +455 -0
- package/dist/examples/react-example.jsx +307 -0
- package/dist/examples/test.html +235 -0
- package/dist/examples/vanilla-example.html +464 -0
- package/dist/index.html +224 -0
- package/examples/react-example.html +455 -0
- package/examples/react-example.jsx +307 -0
- package/examples/test.html +235 -0
- package/examples/vanilla-example.html +464 -0
- package/package.json +63 -0
- package/src/core/AudioPlayer.js +185 -0
- package/src/core/AudioRecorder.js +128 -0
- package/src/core/ConnectionManager.js +86 -0
- package/src/core/EventEmitter.js +53 -0
- package/src/core/VoiceSDK.js +390 -0
- package/src/core/WebSocketManager.js +218 -0
- package/src/core/WebSocketManagerV2.js +211 -0
- package/src/core/WebSocketSingleton.js +171 -0
- package/src/index.js +64 -0
- package/src/legacy/AgentSDK.js +462 -0
- package/src/react/VoiceButton.jsx +163 -0
- package/src/vanilla/VoiceButton.js +190 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocketManager - Handles WebSocket connection and message routing
|
|
3
|
+
*/
|
|
4
|
+
import EventEmitter from './EventEmitter.js';
|
|
5
|
+
import connectionManager from './ConnectionManager.js';
|
|
6
|
+
|
|
7
|
+
export default class WebSocketManager extends EventEmitter {
|
|
8
|
+
constructor(config) {
|
|
9
|
+
super();
|
|
10
|
+
this.config = config;
|
|
11
|
+
this.ws = null;
|
|
12
|
+
this.isConnected = false;
|
|
13
|
+
this.reconnectAttempts = 0;
|
|
14
|
+
this.maxReconnectAttempts = config.autoReconnect !== false ? 3 : 0; // Disable auto-reconnect if explicitly set to false
|
|
15
|
+
this.isReconnecting = false;
|
|
16
|
+
this.isConnecting = false; // Track if we're currently trying to connect
|
|
17
|
+
this.connectionId = null; // Unique ID for this connection attempt
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Connect to WebSocket
|
|
22
|
+
*/
|
|
23
|
+
async connect() {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
try {
|
|
26
|
+
// Prevent multiple connections
|
|
27
|
+
if (this.ws && (this.ws.readyState === WebSocket.CONNECTING || this.ws.readyState === WebSocket.OPEN)) {
|
|
28
|
+
resolve();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Prevent connection if already reconnecting
|
|
33
|
+
if (this.isReconnecting) {
|
|
34
|
+
resolve();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Prevent connection if already connecting
|
|
39
|
+
if (this.isConnecting) {
|
|
40
|
+
resolve();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check if connection is allowed by global manager
|
|
45
|
+
if (!connectionManager.isConnectionAllowed(this.config.websocketUrl)) {
|
|
46
|
+
console.log(`🔌 WebSocketManager: Connection blocked by global manager for ${this.config.websocketUrl}`);
|
|
47
|
+
resolve();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.isConnecting = true;
|
|
52
|
+
this.connectionId = Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
|
53
|
+
|
|
54
|
+
// Register with global connection manager
|
|
55
|
+
if (!connectionManager.registerConnection(this.config.websocketUrl, this.connectionId)) {
|
|
56
|
+
console.log(`🔌 WebSocketManager: Connection registration failed for ${this.connectionId}`);
|
|
57
|
+
this.isConnecting = false;
|
|
58
|
+
resolve();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log(`🔌 WebSocketManager: Starting connection attempt ${this.connectionId}`);
|
|
63
|
+
this.ws = new WebSocket(this.config.websocketUrl);
|
|
64
|
+
|
|
65
|
+
this.ws.onopen = () => {
|
|
66
|
+
console.log(`🔌 WebSocketManager: Connection successful ${this.connectionId}`);
|
|
67
|
+
this.isConnected = true;
|
|
68
|
+
this.reconnectAttempts = 0;
|
|
69
|
+
this.isReconnecting = false;
|
|
70
|
+
this.isConnecting = false;
|
|
71
|
+
this.emit('connected');
|
|
72
|
+
resolve();
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
this.ws.onmessage = (event) => {
|
|
76
|
+
this.handleMessage(event);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
this.ws.onclose = (event) => {
|
|
80
|
+
console.log(`🔌 WebSocketManager: Connection closed ${this.connectionId} (Code: ${event.code})`);
|
|
81
|
+
this.isConnected = false;
|
|
82
|
+
this.isConnecting = false;
|
|
83
|
+
this.emit('disconnected', event);
|
|
84
|
+
|
|
85
|
+
// Attempt reconnection if not intentional and not already reconnecting
|
|
86
|
+
if (event.code !== 1000 && this.reconnectAttempts < this.maxReconnectAttempts && !this.isReconnecting) {
|
|
87
|
+
this.isReconnecting = true;
|
|
88
|
+
this.reconnectAttempts++;
|
|
89
|
+
console.log(`🔌 WebSocketManager: Attempting reconnection ${this.reconnectAttempts}/${this.maxReconnectAttempts}`);
|
|
90
|
+
setTimeout(() => {
|
|
91
|
+
this.isReconnecting = false;
|
|
92
|
+
this.connect().catch(() => {
|
|
93
|
+
// Ignore reconnection errors to prevent infinite loops
|
|
94
|
+
});
|
|
95
|
+
}, 1000 * this.reconnectAttempts);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
this.ws.onerror = (error) => {
|
|
100
|
+
console.log(`🔌 WebSocketManager: Connection error ${this.connectionId}`, error);
|
|
101
|
+
this.isConnecting = false;
|
|
102
|
+
this.emit('error', error);
|
|
103
|
+
reject(error);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.log(`🔌 WebSocketManager: Connection failed ${this.connectionId}`, error);
|
|
108
|
+
this.isConnecting = false;
|
|
109
|
+
reject(error);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Disconnect from WebSocket
|
|
116
|
+
*/
|
|
117
|
+
disconnect() {
|
|
118
|
+
// Stop any reconnection attempts
|
|
119
|
+
this.isReconnecting = false;
|
|
120
|
+
this.reconnectAttempts = this.maxReconnectAttempts; // Prevent reconnection
|
|
121
|
+
|
|
122
|
+
// Unregister from global connection manager
|
|
123
|
+
if (this.connectionId) {
|
|
124
|
+
connectionManager.unregisterConnection(this.config.websocketUrl, this.connectionId);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
128
|
+
this.ws.close(1000, 'Intentional disconnect');
|
|
129
|
+
}
|
|
130
|
+
this.ws = null;
|
|
131
|
+
this.isConnected = false;
|
|
132
|
+
this.isConnecting = false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Reset reconnection attempts (useful for manual reconnection)
|
|
137
|
+
*/
|
|
138
|
+
resetReconnectionAttempts() {
|
|
139
|
+
this.reconnectAttempts = 0;
|
|
140
|
+
this.isReconnecting = false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Clear all global connections (useful for testing)
|
|
145
|
+
*/
|
|
146
|
+
static clearAllConnections() {
|
|
147
|
+
connectionManager.clearAll();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Send JSON message
|
|
152
|
+
*/
|
|
153
|
+
sendMessage(message) {
|
|
154
|
+
if (!this.isConnected || !this.ws) {
|
|
155
|
+
throw new Error('WebSocket not connected');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
this.ws.send(JSON.stringify(message));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Send binary data
|
|
163
|
+
*/
|
|
164
|
+
sendBinary(data) {
|
|
165
|
+
if (!this.isConnected || !this.ws) {
|
|
166
|
+
throw new Error('WebSocket not connected');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this.ws.send(data);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Handle incoming WebSocket messages
|
|
174
|
+
*/
|
|
175
|
+
handleMessage(event) {
|
|
176
|
+
// Check if it's binary data first
|
|
177
|
+
if (event.data instanceof ArrayBuffer) {
|
|
178
|
+
this.emit('binaryAudio', event.data);
|
|
179
|
+
return;
|
|
180
|
+
} else if (event.data instanceof Blob) {
|
|
181
|
+
event.data.arrayBuffer().then(arrayBuffer => {
|
|
182
|
+
this.emit('binaryAudio', arrayBuffer);
|
|
183
|
+
}).catch(err => {
|
|
184
|
+
this.emit('error', err);
|
|
185
|
+
});
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Handle JSON messages
|
|
190
|
+
try {
|
|
191
|
+
const message = JSON.parse(event.data);
|
|
192
|
+
|
|
193
|
+
// Handle barge-in related messages
|
|
194
|
+
if (message.t === 'barge_in_ack' || message.t === 'stop_sending') {
|
|
195
|
+
this.emit('bargeIn', message);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Handle stop playing message
|
|
199
|
+
if (message.t === 'stop_playing') {
|
|
200
|
+
this.emit('stopPlaying', message);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
this.emit('message', message);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
this.emit('error', error);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get connection status
|
|
211
|
+
*/
|
|
212
|
+
getStatus() {
|
|
213
|
+
return {
|
|
214
|
+
isConnected: this.isConnected,
|
|
215
|
+
readyState: this.ws ? this.ws.readyState : WebSocket.CLOSED
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocketManagerV2 - Uses singleton pattern to prevent multiple connections
|
|
3
|
+
*/
|
|
4
|
+
import EventEmitter from './EventEmitter.js';
|
|
5
|
+
import webSocketSingleton from './WebSocketSingleton.js';
|
|
6
|
+
|
|
7
|
+
export default class WebSocketManagerV2 extends EventEmitter {
|
|
8
|
+
constructor(config) {
|
|
9
|
+
super();
|
|
10
|
+
this.config = config;
|
|
11
|
+
this.ws = null;
|
|
12
|
+
this.isConnected = false;
|
|
13
|
+
this.connectionId = null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Connect to WebSocket using singleton
|
|
18
|
+
*/
|
|
19
|
+
async connect() {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
try {
|
|
22
|
+
this.connectionId = Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
|
23
|
+
console.log(`🔌 WebSocketManagerV2: Requesting connection ${this.connectionId} for ${this.config.websocketUrl}`);
|
|
24
|
+
|
|
25
|
+
// Get connection from singleton
|
|
26
|
+
webSocketSingleton.getConnection(this.config.websocketUrl, this.config)
|
|
27
|
+
.then((connection) => {
|
|
28
|
+
this.ws = connection;
|
|
29
|
+
console.log(`🔌 WebSocketManagerV2: Got connection ${this.connectionId}`);
|
|
30
|
+
|
|
31
|
+
// Set up event listeners
|
|
32
|
+
this.setupEventListeners();
|
|
33
|
+
|
|
34
|
+
// If already connected, resolve immediately
|
|
35
|
+
if (connection.readyState === WebSocket.OPEN) {
|
|
36
|
+
this.isConnected = true;
|
|
37
|
+
this.emit('connected');
|
|
38
|
+
resolve();
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
.catch((error) => {
|
|
42
|
+
console.error(`🔌 WebSocketManagerV2: Connection failed ${this.connectionId}`, error);
|
|
43
|
+
reject(error);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error(`🔌 WebSocketManagerV2: Connection error ${this.connectionId}`, error);
|
|
48
|
+
reject(error);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Set up event listeners
|
|
55
|
+
*/
|
|
56
|
+
setupEventListeners() {
|
|
57
|
+
if (!this.ws) return;
|
|
58
|
+
|
|
59
|
+
// Use singleton's event forwarding
|
|
60
|
+
const handleOpen = (event, url) => {
|
|
61
|
+
if (url === this.config.websocketUrl) {
|
|
62
|
+
console.log(`🔌 WebSocketManagerV2: Connection opened ${this.connectionId}`);
|
|
63
|
+
this.isConnected = true;
|
|
64
|
+
this.emit('connected');
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const handleClose = (event, url) => {
|
|
69
|
+
if (url === this.config.websocketUrl) {
|
|
70
|
+
console.log(`🔌 WebSocketManagerV2: Connection closed ${this.connectionId} (Code: ${event.code})`);
|
|
71
|
+
this.isConnected = false;
|
|
72
|
+
this.emit('disconnected', event);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const handleError = (event, url) => {
|
|
77
|
+
if (url === this.config.websocketUrl) {
|
|
78
|
+
console.log(`🔌 WebSocketManagerV2: Connection error ${this.connectionId}`, event);
|
|
79
|
+
this.emit('error', event);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const handleMessage = (event, url) => {
|
|
84
|
+
if (url === this.config.websocketUrl) {
|
|
85
|
+
this.handleMessage(event);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Add event listeners
|
|
90
|
+
webSocketSingleton.on('open', handleOpen);
|
|
91
|
+
webSocketSingleton.on('close', handleClose);
|
|
92
|
+
webSocketSingleton.on('error', handleError);
|
|
93
|
+
webSocketSingleton.on('message', handleMessage);
|
|
94
|
+
|
|
95
|
+
// Store handlers for cleanup
|
|
96
|
+
this.eventHandlers = {
|
|
97
|
+
open: handleOpen,
|
|
98
|
+
close: handleClose,
|
|
99
|
+
error: handleError,
|
|
100
|
+
message: handleMessage
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Disconnect from WebSocket
|
|
106
|
+
*/
|
|
107
|
+
disconnect() {
|
|
108
|
+
console.log(`🔌 WebSocketManagerV2: Disconnecting ${this.connectionId}`);
|
|
109
|
+
|
|
110
|
+
// Remove event listeners
|
|
111
|
+
if (this.eventHandlers) {
|
|
112
|
+
webSocketSingleton.off('open', this.eventHandlers.open);
|
|
113
|
+
webSocketSingleton.off('close', this.eventHandlers.close);
|
|
114
|
+
webSocketSingleton.off('error', this.eventHandlers.error);
|
|
115
|
+
webSocketSingleton.off('message', this.eventHandlers.message);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Release connection from singleton
|
|
119
|
+
if (this.config.websocketUrl) {
|
|
120
|
+
console.log(`🔌 WebSocketManagerV2: Releasing connection ${this.connectionId} from singleton`);
|
|
121
|
+
webSocketSingleton.releaseConnection(this.config.websocketUrl);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
this.ws = null;
|
|
125
|
+
this.isConnected = false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Send JSON message
|
|
130
|
+
*/
|
|
131
|
+
sendMessage(message) {
|
|
132
|
+
if (!this.isConnected || !this.ws) {
|
|
133
|
+
throw new Error('WebSocket not connected');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this.ws.send(JSON.stringify(message));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Send binary data
|
|
141
|
+
*/
|
|
142
|
+
sendBinary(data) {
|
|
143
|
+
if (!this.isConnected || !this.ws) {
|
|
144
|
+
throw new Error('WebSocket not connected');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.ws.send(data);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Handle incoming messages
|
|
152
|
+
*/
|
|
153
|
+
handleMessage(event) {
|
|
154
|
+
// Check if it's binary data
|
|
155
|
+
if (event.data instanceof ArrayBuffer) {
|
|
156
|
+
this.emit('binaryAudio', event.data);
|
|
157
|
+
return;
|
|
158
|
+
} else if (event.data instanceof Blob) {
|
|
159
|
+
event.data.arrayBuffer().then(arrayBuffer => {
|
|
160
|
+
this.emit('binaryAudio', arrayBuffer);
|
|
161
|
+
}).catch(err => {
|
|
162
|
+
console.error('🔌 WebSocketManagerV2: Error converting Blob to ArrayBuffer:', err);
|
|
163
|
+
});
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Handle JSON messages
|
|
168
|
+
try {
|
|
169
|
+
const message = JSON.parse(event.data);
|
|
170
|
+
|
|
171
|
+
// Handle barge-in related messages
|
|
172
|
+
if (message.t === 'barge_in_ack' || message.t === 'stop_sending') {
|
|
173
|
+
this.emit('bargeIn', message);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Handle stop playing message
|
|
177
|
+
if (message.t === 'stop_playing') {
|
|
178
|
+
this.emit('stopPlaying', message);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
this.emit('message', message);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
this.emit('error', error);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get connection status
|
|
189
|
+
*/
|
|
190
|
+
getStatus() {
|
|
191
|
+
return {
|
|
192
|
+
isConnected: this.isConnected,
|
|
193
|
+
readyState: this.ws ? this.ws.readyState : null,
|
|
194
|
+
connectionId: this.connectionId
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get singleton status (for debugging)
|
|
200
|
+
*/
|
|
201
|
+
static getSingletonStatus() {
|
|
202
|
+
return webSocketSingleton.getAllConnections();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Clear all singleton connections (for testing)
|
|
207
|
+
*/
|
|
208
|
+
static clearAllConnections() {
|
|
209
|
+
webSocketSingleton.clearAll();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocketSingleton - Ensures only one WebSocket connection per URL exists
|
|
3
|
+
*/
|
|
4
|
+
import EventEmitter from './EventEmitter.js';
|
|
5
|
+
|
|
6
|
+
class WebSocketSingleton extends EventEmitter {
|
|
7
|
+
constructor() {
|
|
8
|
+
super();
|
|
9
|
+
this.connections = new Map(); // Map of URL -> WebSocket instance
|
|
10
|
+
this.connectionCounts = new Map(); // Map of URL -> number of subscribers
|
|
11
|
+
this.creatingConnections = new Set(); // Set of URLs currently being created
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get or create a WebSocket connection
|
|
16
|
+
*/
|
|
17
|
+
async getConnection(url, config = {}) {
|
|
18
|
+
// If connection already exists, just return it
|
|
19
|
+
if (this.connections.has(url)) {
|
|
20
|
+
const existingConnection = this.connections.get(url);
|
|
21
|
+
this.connectionCounts.set(url, (this.connectionCounts.get(url) || 0) + 1);
|
|
22
|
+
console.log(`🔌 WebSocketSingleton: Reusing existing connection for ${url} (${this.connectionCounts.get(url)} subscribers)`);
|
|
23
|
+
return existingConnection;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check if we're already in the process of creating a connection
|
|
27
|
+
if (this.creatingConnections && this.creatingConnections.has(url)) {
|
|
28
|
+
console.log(`🔌 WebSocketSingleton: Connection already being created for ${url}, waiting...`);
|
|
29
|
+
// Wait for the existing creation to complete
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
const checkConnection = () => {
|
|
32
|
+
if (this.connections.has(url)) {
|
|
33
|
+
const existingConnection = this.connections.get(url);
|
|
34
|
+
this.connectionCounts.set(url, (this.connectionCounts.get(url) || 0) + 1);
|
|
35
|
+
console.log(`🔌 WebSocketSingleton: Got existing connection after wait for ${url} (${this.connectionCounts.get(url)} subscribers)`);
|
|
36
|
+
resolve(existingConnection);
|
|
37
|
+
} else {
|
|
38
|
+
setTimeout(checkConnection, 50);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
checkConnection();
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Create new connection
|
|
46
|
+
console.log(`🔌 WebSocketSingleton: Creating new connection for ${url}`);
|
|
47
|
+
this.creatingConnections.add(url);
|
|
48
|
+
const connection = new WebSocket(url);
|
|
49
|
+
this.connections.set(url, connection);
|
|
50
|
+
this.connectionCounts.set(url, 1);
|
|
51
|
+
|
|
52
|
+
// Set up event forwarding
|
|
53
|
+
connection.addEventListener('open', (event) => {
|
|
54
|
+
console.log(`🔌 WebSocketSingleton: Connection opened for ${url}`);
|
|
55
|
+
this.creatingConnections.delete(url);
|
|
56
|
+
this.emit('open', event, url);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
connection.addEventListener('close', (event) => {
|
|
60
|
+
console.log(`🔌 WebSocketSingleton: Connection closed for ${url} (Code: ${event.code})`);
|
|
61
|
+
this.creatingConnections.delete(url);
|
|
62
|
+
this.connections.delete(url);
|
|
63
|
+
this.connectionCounts.delete(url);
|
|
64
|
+
this.emit('close', event, url);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
connection.addEventListener('error', (event) => {
|
|
68
|
+
console.log(`🔌 WebSocketSingleton: Connection error for ${url}`, event);
|
|
69
|
+
this.creatingConnections.delete(url);
|
|
70
|
+
this.emit('error', event, url);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
connection.addEventListener('message', (event) => {
|
|
74
|
+
this.emit('message', event, url);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return connection;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Release a connection (decrement subscriber count)
|
|
82
|
+
*/
|
|
83
|
+
releaseConnection(url) {
|
|
84
|
+
if (!this.connections.has(url)) {
|
|
85
|
+
console.log(`🔌 WebSocketSingleton: Attempted to release non-existent connection for ${url}`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const currentCount = this.connectionCounts.get(url) || 0;
|
|
90
|
+
const newCount = Math.max(0, currentCount - 1);
|
|
91
|
+
this.connectionCounts.set(url, newCount);
|
|
92
|
+
|
|
93
|
+
console.log(`🔌 WebSocketSingleton: Released connection for ${url} (${newCount} subscribers remaining)`);
|
|
94
|
+
|
|
95
|
+
// If no more subscribers, close the connection
|
|
96
|
+
if (newCount === 0) {
|
|
97
|
+
const connection = this.connections.get(url);
|
|
98
|
+
if (connection && connection.readyState === WebSocket.OPEN) {
|
|
99
|
+
console.log(`🔌 WebSocketSingleton: Closing connection for ${url} (no more subscribers)`);
|
|
100
|
+
connection.close(1000, 'No more subscribers');
|
|
101
|
+
}
|
|
102
|
+
this.connections.delete(url);
|
|
103
|
+
this.connectionCounts.delete(url);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Force close a connection
|
|
109
|
+
*/
|
|
110
|
+
forceClose(url) {
|
|
111
|
+
if (this.connections.has(url)) {
|
|
112
|
+
const connection = this.connections.get(url);
|
|
113
|
+
if (connection && connection.readyState === WebSocket.OPEN) {
|
|
114
|
+
console.log(`🔌 WebSocketSingleton: Force closing connection for ${url}`);
|
|
115
|
+
connection.close(1000, 'Force close');
|
|
116
|
+
}
|
|
117
|
+
this.connections.delete(url);
|
|
118
|
+
this.connectionCounts.delete(url);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get connection status
|
|
124
|
+
*/
|
|
125
|
+
getConnectionStatus(url) {
|
|
126
|
+
if (!this.connections.has(url)) {
|
|
127
|
+
return { exists: false, readyState: null, subscribers: 0 };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const connection = this.connections.get(url);
|
|
131
|
+
return {
|
|
132
|
+
exists: true,
|
|
133
|
+
readyState: connection.readyState,
|
|
134
|
+
subscribers: this.connectionCounts.get(url) || 0
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get all active connections
|
|
140
|
+
*/
|
|
141
|
+
getAllConnections() {
|
|
142
|
+
const result = {};
|
|
143
|
+
for (const [url, connection] of this.connections.entries()) {
|
|
144
|
+
result[url] = {
|
|
145
|
+
readyState: connection.readyState,
|
|
146
|
+
subscribers: this.connectionCounts.get(url) || 0
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Clear all connections (for testing)
|
|
154
|
+
*/
|
|
155
|
+
clearAll() {
|
|
156
|
+
console.log(`🔌 WebSocketSingleton: Clearing all connections`);
|
|
157
|
+
for (const [url, connection] of this.connections.entries()) {
|
|
158
|
+
if (connection && connection.readyState === WebSocket.OPEN) {
|
|
159
|
+
connection.close(1000, 'Clear all');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
this.connections.clear();
|
|
163
|
+
this.connectionCounts.clear();
|
|
164
|
+
this.creatingConnections.clear();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Global singleton instance
|
|
169
|
+
const webSocketSingleton = new WebSocketSingleton();
|
|
170
|
+
|
|
171
|
+
export default webSocketSingleton;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TTP Agent SDK - Main Entry Point
|
|
3
|
+
*
|
|
4
|
+
* A comprehensive SDK for voice interaction with AI agents.
|
|
5
|
+
* Provides real-time audio recording, WebSocket communication, and audio playback.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - 🎤 Real-time Audio Recording with AudioWorklet
|
|
9
|
+
* - 🔄 WebSocket Communication with authentication
|
|
10
|
+
* - 🔊 Audio Playback with queue management
|
|
11
|
+
* - ⚛️ React Components
|
|
12
|
+
* - 🌐 Vanilla JavaScript Components
|
|
13
|
+
* - 🎯 Event-driven architecture
|
|
14
|
+
* - 🔒 Multiple authentication methods
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Core SDK
|
|
18
|
+
import VoiceSDK from './core/VoiceSDK.js';
|
|
19
|
+
import WebSocketManager from './core/WebSocketManager.js';
|
|
20
|
+
import WebSocketManagerV2 from './core/WebSocketManagerV2.js';
|
|
21
|
+
import AudioRecorder from './core/AudioRecorder.js';
|
|
22
|
+
import AudioPlayer from './core/AudioPlayer.js';
|
|
23
|
+
import EventEmitter from './core/EventEmitter.js';
|
|
24
|
+
|
|
25
|
+
// React components
|
|
26
|
+
import VoiceButton from './react/VoiceButton.jsx';
|
|
27
|
+
|
|
28
|
+
// Vanilla JavaScript components
|
|
29
|
+
import VanillaVoiceButton from './vanilla/VoiceButton.js';
|
|
30
|
+
|
|
31
|
+
// Legacy AgentSDK (for backward compatibility)
|
|
32
|
+
import { AgentSDK, AgentWidget } from './legacy/AgentSDK.js';
|
|
33
|
+
|
|
34
|
+
// Version
|
|
35
|
+
export const VERSION = '2.0.0';
|
|
36
|
+
|
|
37
|
+
// Named exports
|
|
38
|
+
export {
|
|
39
|
+
VoiceSDK,
|
|
40
|
+
WebSocketManager,
|
|
41
|
+
WebSocketManagerV2,
|
|
42
|
+
AudioRecorder,
|
|
43
|
+
AudioPlayer,
|
|
44
|
+
EventEmitter,
|
|
45
|
+
VoiceButton,
|
|
46
|
+
VanillaVoiceButton,
|
|
47
|
+
AgentSDK,
|
|
48
|
+
AgentWidget
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Default export for convenience
|
|
52
|
+
export default {
|
|
53
|
+
VoiceSDK,
|
|
54
|
+
WebSocketManager,
|
|
55
|
+
WebSocketManagerV2,
|
|
56
|
+
AudioRecorder,
|
|
57
|
+
AudioPlayer,
|
|
58
|
+
EventEmitter,
|
|
59
|
+
VoiceButton,
|
|
60
|
+
VanillaVoiceButton,
|
|
61
|
+
AgentSDK,
|
|
62
|
+
AgentWidget,
|
|
63
|
+
VERSION
|
|
64
|
+
};
|