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.
@@ -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
+ };