@sayna-ai/node-sdk 0.0.18 → 0.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/index.cjs +244 -57
- package/dist/index.cjs.map +3 -3
- package/dist/index.js +244 -57
- package/dist/index.js.map +3 -3
- package/dist/sayna-client.d.ts +40 -2
- package/dist/sayna-client.d.ts.map +1 -1
- package/dist/types.d.ts +38 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -58,9 +58,11 @@ class SaynaServerError extends SaynaError {
|
|
|
58
58
|
|
|
59
59
|
// src/sayna-client.ts
|
|
60
60
|
var isBun = typeof process !== "undefined" && typeof process.versions?.bun === "string";
|
|
61
|
-
var
|
|
61
|
+
var isNode = typeof process !== "undefined" && typeof process.versions?.node === "string" && !isBun;
|
|
62
62
|
var WS;
|
|
63
|
-
if (
|
|
63
|
+
if (isNode) {
|
|
64
|
+
WS = __require("ws");
|
|
65
|
+
} else if (typeof globalThis.WebSocket !== "undefined") {
|
|
64
66
|
WS = globalThis.WebSocket;
|
|
65
67
|
} else {
|
|
66
68
|
WS = __require("ws");
|
|
@@ -86,7 +88,9 @@ class SaynaClient {
|
|
|
86
88
|
ttsCallback;
|
|
87
89
|
errorCallback;
|
|
88
90
|
messageCallback;
|
|
91
|
+
participantConnectedCallback;
|
|
89
92
|
participantDisconnectedCallback;
|
|
93
|
+
trackSubscribedCallback;
|
|
90
94
|
ttsPlaybackCompleteCallback;
|
|
91
95
|
sipTransferErrorCallback;
|
|
92
96
|
readyPromiseResolve;
|
|
@@ -115,7 +119,7 @@ class SaynaClient {
|
|
|
115
119
|
if (this.isConnected) {
|
|
116
120
|
return;
|
|
117
121
|
}
|
|
118
|
-
const wsUrl = this.
|
|
122
|
+
const wsUrl = this.getWebSocketUrl();
|
|
119
123
|
return new Promise((resolve, reject) => {
|
|
120
124
|
this.readyPromiseResolve = resolve;
|
|
121
125
|
this.readyPromiseReject = reject;
|
|
@@ -157,7 +161,7 @@ class SaynaClient {
|
|
|
157
161
|
const wasReady = this.isReady;
|
|
158
162
|
this.cleanup();
|
|
159
163
|
if (!wasReady && this.readyPromiseReject) {
|
|
160
|
-
const reason = event.reason.length > 0 ? event.reason : "none";
|
|
164
|
+
const reason = event.reason && event.reason.length > 0 ? event.reason : "none";
|
|
161
165
|
this.readyPromiseReject(new SaynaConnectionError(`WebSocket closed before ready (code: ${event.code}, reason: ${reason})`));
|
|
162
166
|
}
|
|
163
167
|
};
|
|
@@ -170,27 +174,30 @@ class SaynaClient {
|
|
|
170
174
|
try {
|
|
171
175
|
if (event.data instanceof Blob || event.data instanceof ArrayBuffer) {
|
|
172
176
|
const buffer = event.data instanceof Blob ? await event.data.arrayBuffer() : event.data;
|
|
173
|
-
|
|
174
|
-
await this.ttsCallback(buffer);
|
|
175
|
-
}
|
|
177
|
+
await this.invokeCallback(this.ttsCallback, buffer, "TTS audio");
|
|
176
178
|
} else {
|
|
177
179
|
if (typeof event.data !== "string") {
|
|
178
|
-
|
|
180
|
+
this.logProtocolWarning("Ignoring websocket message with unsupported non-binary payload type", event.data);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
let data;
|
|
184
|
+
try {
|
|
185
|
+
data = JSON.parse(event.data);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
this.logProtocolWarning(`Ignoring invalid websocket JSON: ${this.describeError(error)}`, event.data);
|
|
188
|
+
return;
|
|
179
189
|
}
|
|
180
|
-
const data = JSON.parse(event.data);
|
|
181
190
|
await this.handleJsonMessage(data);
|
|
182
191
|
}
|
|
183
192
|
} catch (error) {
|
|
184
|
-
|
|
185
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
186
|
-
await this.errorCallback({
|
|
187
|
-
type: "error",
|
|
188
|
-
message: `Failed to process message: ${errorMessage}`
|
|
189
|
-
});
|
|
190
|
-
}
|
|
193
|
+
await this.reportHandlerError("WebSocket message", error);
|
|
191
194
|
}
|
|
192
195
|
}
|
|
193
|
-
async handleJsonMessage(
|
|
196
|
+
async handleJsonMessage(raw) {
|
|
197
|
+
const data = this.parseIncomingMessage(raw);
|
|
198
|
+
if (!data) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
194
201
|
const messageType = data.type;
|
|
195
202
|
try {
|
|
196
203
|
switch (messageType) {
|
|
@@ -208,72 +215,246 @@ class SaynaClient {
|
|
|
208
215
|
break;
|
|
209
216
|
}
|
|
210
217
|
case "stt_result": {
|
|
211
|
-
|
|
212
|
-
if (this.sttCallback) {
|
|
213
|
-
await this.sttCallback(sttResult);
|
|
214
|
-
}
|
|
218
|
+
await this.invokeCallback(this.sttCallback, data, "STT result");
|
|
215
219
|
break;
|
|
216
220
|
}
|
|
217
221
|
case "error": {
|
|
218
|
-
|
|
219
|
-
if (this.errorCallback) {
|
|
220
|
-
await this.errorCallback(errorMsg);
|
|
221
|
-
}
|
|
222
|
+
await this.invokeCallback(this.errorCallback, data, "error");
|
|
222
223
|
if (this.readyPromiseReject && !this.isReady) {
|
|
223
|
-
this.readyPromiseReject(new SaynaServerError(
|
|
224
|
+
this.readyPromiseReject(new SaynaServerError(data.message));
|
|
224
225
|
}
|
|
225
226
|
break;
|
|
226
227
|
}
|
|
227
228
|
case "sip_transfer_error": {
|
|
228
|
-
const sipTransferError = data;
|
|
229
229
|
if (this.sipTransferErrorCallback) {
|
|
230
|
-
await this.sipTransferErrorCallback
|
|
230
|
+
await this.invokeCallback(this.sipTransferErrorCallback, data, "SIP transfer error");
|
|
231
231
|
} else if (this.errorCallback) {
|
|
232
232
|
await this.errorCallback({
|
|
233
233
|
type: "error",
|
|
234
|
-
message:
|
|
234
|
+
message: data.message
|
|
235
235
|
});
|
|
236
236
|
}
|
|
237
237
|
break;
|
|
238
238
|
}
|
|
239
239
|
case "message": {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
240
|
+
await this.invokeCallback(this.messageCallback, data.message, "message");
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
case "participant_connected": {
|
|
244
|
+
await this.invokeCallback(this.participantConnectedCallback, data.participant, "participant connected");
|
|
244
245
|
break;
|
|
245
246
|
}
|
|
246
247
|
case "participant_disconnected": {
|
|
247
|
-
|
|
248
|
-
if (this.participantDisconnectedCallback) {
|
|
249
|
-
await this.participantDisconnectedCallback(participantMsg.participant);
|
|
250
|
-
}
|
|
248
|
+
await this.invokeCallback(this.participantDisconnectedCallback, data.participant, "participant disconnected");
|
|
251
249
|
break;
|
|
252
250
|
}
|
|
253
|
-
case "
|
|
254
|
-
|
|
255
|
-
if (this.ttsPlaybackCompleteCallback) {
|
|
256
|
-
await this.ttsPlaybackCompleteCallback(ttsPlaybackCompleteMsg.timestamp);
|
|
257
|
-
}
|
|
251
|
+
case "track_subscribed": {
|
|
252
|
+
await this.invokeCallback(this.trackSubscribedCallback, data.track, "track subscribed");
|
|
258
253
|
break;
|
|
259
254
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if (this.errorCallback) {
|
|
264
|
-
await this.errorCallback({ type: "error", message: errorMessage });
|
|
265
|
-
} else {
|
|
266
|
-
console.warn(errorMessage);
|
|
267
|
-
}
|
|
255
|
+
case "tts_playback_complete": {
|
|
256
|
+
await this.invokeCallback(this.ttsPlaybackCompleteCallback, data.timestamp, "TTS playback complete");
|
|
257
|
+
break;
|
|
268
258
|
}
|
|
269
259
|
}
|
|
270
260
|
} catch (error) {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
261
|
+
await this.reportHandlerError(`"${messageType}"`, error);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
getWebSocketUrl() {
|
|
265
|
+
const wsUrl = this.url.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://");
|
|
266
|
+
const parsedUrl = new URL(wsUrl);
|
|
267
|
+
if (!parsedUrl.pathname.endsWith("/ws")) {
|
|
268
|
+
parsedUrl.pathname = parsedUrl.pathname.endsWith("/") ? `${parsedUrl.pathname}ws` : `${parsedUrl.pathname}/ws`;
|
|
269
|
+
}
|
|
270
|
+
return parsedUrl.toString();
|
|
271
|
+
}
|
|
272
|
+
parseIncomingMessage(raw) {
|
|
273
|
+
if (!this.isJsonObject(raw)) {
|
|
274
|
+
this.logProtocolWarning("Ignoring websocket payload because it is not a JSON object", raw);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const messageType = raw.type;
|
|
278
|
+
if (typeof messageType !== "string") {
|
|
279
|
+
this.logProtocolWarning('Ignoring websocket payload without a string "type" field', raw);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
switch (messageType) {
|
|
284
|
+
case "ready":
|
|
285
|
+
return {
|
|
286
|
+
type: "ready",
|
|
287
|
+
stream_id: this.getOptionalString(raw, "stream_id"),
|
|
288
|
+
livekit_room_name: this.getOptionalString(raw, "livekit_room_name"),
|
|
289
|
+
livekit_url: this.getOptionalString(raw, "livekit_url"),
|
|
290
|
+
sayna_participant_identity: this.getOptionalString(raw, "sayna_participant_identity"),
|
|
291
|
+
sayna_participant_name: this.getOptionalString(raw, "sayna_participant_name")
|
|
292
|
+
};
|
|
293
|
+
case "stt_result":
|
|
294
|
+
return {
|
|
295
|
+
type: "stt_result",
|
|
296
|
+
transcript: this.getRequiredString(raw, "transcript"),
|
|
297
|
+
is_final: this.getRequiredBoolean(raw, "is_final"),
|
|
298
|
+
is_speech_final: this.getRequiredBoolean(raw, "is_speech_final"),
|
|
299
|
+
confidence: this.getRequiredNumber(raw, "confidence")
|
|
300
|
+
};
|
|
301
|
+
case "error":
|
|
302
|
+
return {
|
|
303
|
+
type: "error",
|
|
304
|
+
message: this.getRequiredString(raw, "message")
|
|
305
|
+
};
|
|
306
|
+
case "sip_transfer_error":
|
|
307
|
+
return {
|
|
308
|
+
type: "sip_transfer_error",
|
|
309
|
+
message: this.getRequiredString(raw, "message")
|
|
310
|
+
};
|
|
311
|
+
case "message":
|
|
312
|
+
return {
|
|
313
|
+
type: "message",
|
|
314
|
+
message: this.parseSaynaMessage(raw, "message")
|
|
315
|
+
};
|
|
316
|
+
case "participant_connected":
|
|
317
|
+
return {
|
|
318
|
+
type: "participant_connected",
|
|
319
|
+
participant: this.parseParticipant(raw, "participant")
|
|
320
|
+
};
|
|
321
|
+
case "participant_disconnected":
|
|
322
|
+
return {
|
|
323
|
+
type: "participant_disconnected",
|
|
324
|
+
participant: this.parseParticipant(raw, "participant")
|
|
325
|
+
};
|
|
326
|
+
case "track_subscribed":
|
|
327
|
+
return {
|
|
328
|
+
type: "track_subscribed",
|
|
329
|
+
track: this.parseTrack(raw, "track")
|
|
330
|
+
};
|
|
331
|
+
case "tts_playback_complete":
|
|
332
|
+
return {
|
|
333
|
+
type: "tts_playback_complete",
|
|
334
|
+
timestamp: this.getRequiredNumber(raw, "timestamp")
|
|
335
|
+
};
|
|
336
|
+
default:
|
|
337
|
+
this.logProtocolWarning(`Ignoring unknown websocket message type "${messageType}"`, raw);
|
|
338
|
+
return;
|
|
276
339
|
}
|
|
340
|
+
} catch (error) {
|
|
341
|
+
this.logProtocolWarning(`Ignoring malformed websocket message "${messageType}": ${this.describeError(error)}`, raw);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
parseSaynaMessage(container, fieldName) {
|
|
346
|
+
const message = this.getRequiredObject(container, fieldName);
|
|
347
|
+
return {
|
|
348
|
+
message: this.getOptionalString(message, "message"),
|
|
349
|
+
data: this.getOptionalString(message, "data"),
|
|
350
|
+
identity: this.getRequiredString(message, "identity"),
|
|
351
|
+
topic: this.getRequiredString(message, "topic"),
|
|
352
|
+
room: this.getRequiredString(message, "room"),
|
|
353
|
+
timestamp: this.getRequiredNumber(message, "timestamp")
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
parseParticipant(container, fieldName) {
|
|
357
|
+
const participant = this.getRequiredObject(container, fieldName);
|
|
358
|
+
return {
|
|
359
|
+
identity: this.getRequiredString(participant, "identity"),
|
|
360
|
+
name: this.getOptionalString(participant, "name"),
|
|
361
|
+
room: this.getRequiredString(participant, "room"),
|
|
362
|
+
timestamp: this.getRequiredNumber(participant, "timestamp")
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
parseTrack(container, fieldName) {
|
|
366
|
+
const track = this.getRequiredObject(container, fieldName);
|
|
367
|
+
const trackKind = this.getRequiredString(track, "track_kind");
|
|
368
|
+
if (trackKind !== "audio" && trackKind !== "video") {
|
|
369
|
+
throw new Error('Field "track_kind" must be either "audio" or "video"');
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
identity: this.getRequiredString(track, "identity"),
|
|
373
|
+
name: this.getOptionalString(track, "name"),
|
|
374
|
+
track_kind: trackKind,
|
|
375
|
+
track_sid: this.getRequiredString(track, "track_sid"),
|
|
376
|
+
room: this.getRequiredString(track, "room"),
|
|
377
|
+
timestamp: this.getRequiredNumber(track, "timestamp")
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
getRequiredObject(container, fieldName) {
|
|
381
|
+
const value = container[fieldName];
|
|
382
|
+
if (!this.isJsonObject(value)) {
|
|
383
|
+
throw new Error(`Field "${fieldName}" must be an object`);
|
|
384
|
+
}
|
|
385
|
+
return value;
|
|
386
|
+
}
|
|
387
|
+
getRequiredString(container, fieldName) {
|
|
388
|
+
const value = container[fieldName];
|
|
389
|
+
if (typeof value !== "string") {
|
|
390
|
+
throw new Error(`Field "${fieldName}" must be a string`);
|
|
391
|
+
}
|
|
392
|
+
return value;
|
|
393
|
+
}
|
|
394
|
+
getOptionalString(container, fieldName) {
|
|
395
|
+
const value = container[fieldName];
|
|
396
|
+
if (typeof value === "undefined" || value === null) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (typeof value !== "string") {
|
|
400
|
+
throw new Error(`Field "${fieldName}" must be a string when present`);
|
|
401
|
+
}
|
|
402
|
+
return value;
|
|
403
|
+
}
|
|
404
|
+
getRequiredBoolean(container, fieldName) {
|
|
405
|
+
const value = container[fieldName];
|
|
406
|
+
if (typeof value !== "boolean") {
|
|
407
|
+
throw new Error(`Field "${fieldName}" must be a boolean`);
|
|
408
|
+
}
|
|
409
|
+
return value;
|
|
410
|
+
}
|
|
411
|
+
getRequiredNumber(container, fieldName) {
|
|
412
|
+
const value = container[fieldName];
|
|
413
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
414
|
+
throw new Error(`Field "${fieldName}" must be a finite number`);
|
|
415
|
+
}
|
|
416
|
+
return value;
|
|
417
|
+
}
|
|
418
|
+
isJsonObject(value) {
|
|
419
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
420
|
+
}
|
|
421
|
+
logProtocolWarning(message, payload) {
|
|
422
|
+
console.warn(`${message}; payload=${this.safeStringify(payload)}`);
|
|
423
|
+
}
|
|
424
|
+
safeStringify(payload) {
|
|
425
|
+
try {
|
|
426
|
+
return JSON.stringify(payload);
|
|
427
|
+
} catch {
|
|
428
|
+
return String(payload);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
describeError(error) {
|
|
432
|
+
return error instanceof Error ? error.message : String(error);
|
|
433
|
+
}
|
|
434
|
+
async invokeCallback(callback, payload, label) {
|
|
435
|
+
if (!callback) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
try {
|
|
439
|
+
await callback(payload);
|
|
440
|
+
} catch (error) {
|
|
441
|
+
await this.reportHandlerError(`${label} callback`, error);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
async reportHandlerError(label, error) {
|
|
445
|
+
const message = `${label} failed: ${this.describeError(error)}`;
|
|
446
|
+
if (!this.errorCallback) {
|
|
447
|
+
console.error(message);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
try {
|
|
451
|
+
await this.errorCallback({
|
|
452
|
+
type: "error",
|
|
453
|
+
message
|
|
454
|
+
});
|
|
455
|
+
} catch (callbackError) {
|
|
456
|
+
console.error(message);
|
|
457
|
+
console.error(`error callback failed: ${this.describeError(callbackError)}`);
|
|
277
458
|
}
|
|
278
459
|
}
|
|
279
460
|
createWebSocket(url) {
|
|
@@ -281,7 +462,7 @@ class SaynaClient {
|
|
|
281
462
|
return new WS(url);
|
|
282
463
|
}
|
|
283
464
|
const headers = { Authorization: `Bearer ${this.apiKey}` };
|
|
284
|
-
if (
|
|
465
|
+
if (isNode) {
|
|
285
466
|
return new WS(url, undefined, { headers });
|
|
286
467
|
}
|
|
287
468
|
if (isBun) {
|
|
@@ -393,9 +574,15 @@ class SaynaClient {
|
|
|
393
574
|
registerOnMessage(callback) {
|
|
394
575
|
this.messageCallback = callback;
|
|
395
576
|
}
|
|
577
|
+
registerOnParticipantConnected(callback) {
|
|
578
|
+
this.participantConnectedCallback = callback;
|
|
579
|
+
}
|
|
396
580
|
registerOnParticipantDisconnected(callback) {
|
|
397
581
|
this.participantDisconnectedCallback = callback;
|
|
398
582
|
}
|
|
583
|
+
registerOnTrackSubscribed(callback) {
|
|
584
|
+
this.trackSubscribedCallback = callback;
|
|
585
|
+
}
|
|
399
586
|
registerOnTtsPlaybackComplete(callback) {
|
|
400
587
|
this.ttsPlaybackCompleteCallback = callback;
|
|
401
588
|
}
|
|
@@ -848,4 +1035,4 @@ export {
|
|
|
848
1035
|
SaynaClient
|
|
849
1036
|
};
|
|
850
1037
|
|
|
851
|
-
//# debugId=
|
|
1038
|
+
//# debugId=855B93EB4E2A921264756E2164756E21
|