@sayna-ai/node-sdk 0.0.19 → 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 +238 -53
- package/dist/index.cjs.map +3 -3
- package/dist/index.js +238 -53
- package/dist/index.js.map +3 -3
- package/dist/sayna-client.d.ts +39 -1
- 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
|
@@ -88,7 +88,9 @@ class SaynaClient {
|
|
|
88
88
|
ttsCallback;
|
|
89
89
|
errorCallback;
|
|
90
90
|
messageCallback;
|
|
91
|
+
participantConnectedCallback;
|
|
91
92
|
participantDisconnectedCallback;
|
|
93
|
+
trackSubscribedCallback;
|
|
92
94
|
ttsPlaybackCompleteCallback;
|
|
93
95
|
sipTransferErrorCallback;
|
|
94
96
|
readyPromiseResolve;
|
|
@@ -117,7 +119,7 @@ class SaynaClient {
|
|
|
117
119
|
if (this.isConnected) {
|
|
118
120
|
return;
|
|
119
121
|
}
|
|
120
|
-
const wsUrl = this.
|
|
122
|
+
const wsUrl = this.getWebSocketUrl();
|
|
121
123
|
return new Promise((resolve, reject) => {
|
|
122
124
|
this.readyPromiseResolve = resolve;
|
|
123
125
|
this.readyPromiseReject = reject;
|
|
@@ -172,27 +174,30 @@ class SaynaClient {
|
|
|
172
174
|
try {
|
|
173
175
|
if (event.data instanceof Blob || event.data instanceof ArrayBuffer) {
|
|
174
176
|
const buffer = event.data instanceof Blob ? await event.data.arrayBuffer() : event.data;
|
|
175
|
-
|
|
176
|
-
await this.ttsCallback(buffer);
|
|
177
|
-
}
|
|
177
|
+
await this.invokeCallback(this.ttsCallback, buffer, "TTS audio");
|
|
178
178
|
} else {
|
|
179
179
|
if (typeof event.data !== "string") {
|
|
180
|
-
|
|
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;
|
|
181
189
|
}
|
|
182
|
-
const data = JSON.parse(event.data);
|
|
183
190
|
await this.handleJsonMessage(data);
|
|
184
191
|
}
|
|
185
192
|
} catch (error) {
|
|
186
|
-
|
|
187
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
188
|
-
await this.errorCallback({
|
|
189
|
-
type: "error",
|
|
190
|
-
message: `Failed to process message: ${errorMessage}`
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
+
await this.reportHandlerError("WebSocket message", error);
|
|
193
194
|
}
|
|
194
195
|
}
|
|
195
|
-
async handleJsonMessage(
|
|
196
|
+
async handleJsonMessage(raw) {
|
|
197
|
+
const data = this.parseIncomingMessage(raw);
|
|
198
|
+
if (!data) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
196
201
|
const messageType = data.type;
|
|
197
202
|
try {
|
|
198
203
|
switch (messageType) {
|
|
@@ -210,72 +215,246 @@ class SaynaClient {
|
|
|
210
215
|
break;
|
|
211
216
|
}
|
|
212
217
|
case "stt_result": {
|
|
213
|
-
|
|
214
|
-
if (this.sttCallback) {
|
|
215
|
-
await this.sttCallback(sttResult);
|
|
216
|
-
}
|
|
218
|
+
await this.invokeCallback(this.sttCallback, data, "STT result");
|
|
217
219
|
break;
|
|
218
220
|
}
|
|
219
221
|
case "error": {
|
|
220
|
-
|
|
221
|
-
if (this.errorCallback) {
|
|
222
|
-
await this.errorCallback(errorMsg);
|
|
223
|
-
}
|
|
222
|
+
await this.invokeCallback(this.errorCallback, data, "error");
|
|
224
223
|
if (this.readyPromiseReject && !this.isReady) {
|
|
225
|
-
this.readyPromiseReject(new SaynaServerError(
|
|
224
|
+
this.readyPromiseReject(new SaynaServerError(data.message));
|
|
226
225
|
}
|
|
227
226
|
break;
|
|
228
227
|
}
|
|
229
228
|
case "sip_transfer_error": {
|
|
230
|
-
const sipTransferError = data;
|
|
231
229
|
if (this.sipTransferErrorCallback) {
|
|
232
|
-
await this.sipTransferErrorCallback
|
|
230
|
+
await this.invokeCallback(this.sipTransferErrorCallback, data, "SIP transfer error");
|
|
233
231
|
} else if (this.errorCallback) {
|
|
234
232
|
await this.errorCallback({
|
|
235
233
|
type: "error",
|
|
236
|
-
message:
|
|
234
|
+
message: data.message
|
|
237
235
|
});
|
|
238
236
|
}
|
|
239
237
|
break;
|
|
240
238
|
}
|
|
241
239
|
case "message": {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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");
|
|
246
245
|
break;
|
|
247
246
|
}
|
|
248
247
|
case "participant_disconnected": {
|
|
249
|
-
|
|
250
|
-
if (this.participantDisconnectedCallback) {
|
|
251
|
-
await this.participantDisconnectedCallback(participantMsg.participant);
|
|
252
|
-
}
|
|
248
|
+
await this.invokeCallback(this.participantDisconnectedCallback, data.participant, "participant disconnected");
|
|
253
249
|
break;
|
|
254
250
|
}
|
|
255
|
-
case "
|
|
256
|
-
|
|
257
|
-
if (this.ttsPlaybackCompleteCallback) {
|
|
258
|
-
await this.ttsPlaybackCompleteCallback(ttsPlaybackCompleteMsg.timestamp);
|
|
259
|
-
}
|
|
251
|
+
case "track_subscribed": {
|
|
252
|
+
await this.invokeCallback(this.trackSubscribedCallback, data.track, "track subscribed");
|
|
260
253
|
break;
|
|
261
254
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if (this.errorCallback) {
|
|
266
|
-
await this.errorCallback({ type: "error", message: errorMessage });
|
|
267
|
-
} else {
|
|
268
|
-
console.warn(errorMessage);
|
|
269
|
-
}
|
|
255
|
+
case "tts_playback_complete": {
|
|
256
|
+
await this.invokeCallback(this.ttsPlaybackCompleteCallback, data.timestamp, "TTS playback complete");
|
|
257
|
+
break;
|
|
270
258
|
}
|
|
271
259
|
}
|
|
272
260
|
} catch (error) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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;
|
|
278
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)}`);
|
|
279
458
|
}
|
|
280
459
|
}
|
|
281
460
|
createWebSocket(url) {
|
|
@@ -395,9 +574,15 @@ class SaynaClient {
|
|
|
395
574
|
registerOnMessage(callback) {
|
|
396
575
|
this.messageCallback = callback;
|
|
397
576
|
}
|
|
577
|
+
registerOnParticipantConnected(callback) {
|
|
578
|
+
this.participantConnectedCallback = callback;
|
|
579
|
+
}
|
|
398
580
|
registerOnParticipantDisconnected(callback) {
|
|
399
581
|
this.participantDisconnectedCallback = callback;
|
|
400
582
|
}
|
|
583
|
+
registerOnTrackSubscribed(callback) {
|
|
584
|
+
this.trackSubscribedCallback = callback;
|
|
585
|
+
}
|
|
401
586
|
registerOnTtsPlaybackComplete(callback) {
|
|
402
587
|
this.ttsPlaybackCompleteCallback = callback;
|
|
403
588
|
}
|
|
@@ -850,4 +1035,4 @@ export {
|
|
|
850
1035
|
SaynaClient
|
|
851
1036
|
};
|
|
852
1037
|
|
|
853
|
-
//# debugId=
|
|
1038
|
+
//# debugId=855B93EB4E2A921264756E2164756E21
|