@nlxai/core 1.1.8-alpha.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,729 @@
1
+ import fetch from 'isomorphic-fetch';
2
+ import { equals, adjust } from 'ramda';
3
+ import ReconnectingWebSocket from 'reconnecting-websocket';
4
+ import { v4 } from 'uuid';
5
+
6
+ var name = "@nlxai/core";
7
+ var version$1 = "1.1.8-alpha.0";
8
+ var description = "Low-level SDK for building NLX experiences";
9
+ var type = "module";
10
+ var main = "lib/index.cjs";
11
+ var module = "lib/index.esm.js";
12
+ var browser = "lib/index.umd.js";
13
+ var types = "lib/index.d.ts";
14
+ var exports = {
15
+ ".": {
16
+ types: "./lib/index.d.ts",
17
+ "import": "./lib/index.esm.js",
18
+ require: "./lib/index.cjs"
19
+ }
20
+ };
21
+ var scripts = {
22
+ build: "rm -rf lib && rollup -c --configPlugin typescript --configImportAttributesKey with",
23
+ docs: "rm -rf docs/ && typedoc && concat-md --decrease-title-levels --dir-name-as-title docs/ > docs/index.md",
24
+ "lint:check": "eslint src/ --ext .ts,.tsx,.js,.jsx --max-warnings 0",
25
+ lint: "eslint src/ --ext .ts,.tsx,.js,.jsx --fix",
26
+ prepublish: "npm run build",
27
+ "preview-docs": "npm run docs && comrak --unsafe --gfm -o docs/index.html docs/index.md && open docs/index.html",
28
+ "publish-docs": "npm run docs && mv docs/index.md ../website/src/content/headless-api-reference.md",
29
+ test: "typedoc --emit none",
30
+ tsc: "tsc"
31
+ };
32
+ var author = "Peter Szerzo <peter@nlx.ai>";
33
+ var license = "MIT";
34
+ var devDependencies = {
35
+ "@types/isomorphic-fetch": "^0.0.36",
36
+ "@types/node": "^22.10.1",
37
+ "@types/ramda": "0.29.1",
38
+ "@types/uuid": "^9.0.7",
39
+ "concat-md": "^0.5.1",
40
+ "eslint-config-nlx": "*",
41
+ prettier: "^3.1.0",
42
+ "rollup-config-nlx": "*",
43
+ typedoc: "^0.25.13",
44
+ "typedoc-plugin-markdown": "^3.17.1",
45
+ typescript: "^5.5.4"
46
+ };
47
+ var dependencies = {
48
+ "isomorphic-fetch": "^3.0.0",
49
+ ramda: "^0.29.1",
50
+ "reconnecting-websocket": "^4.4.0",
51
+ uuid: "^9.0.1"
52
+ };
53
+ var publishConfig = {
54
+ access: "public"
55
+ };
56
+ var gitHead = "8f4341c889c73a3b681dcdf25420ecec285c1ef6";
57
+ var packageJson = {
58
+ name: name,
59
+ version: version$1,
60
+ description: description,
61
+ type: type,
62
+ main: main,
63
+ module: module,
64
+ browser: browser,
65
+ types: types,
66
+ exports: exports,
67
+ scripts: scripts,
68
+ author: author,
69
+ license: license,
70
+ devDependencies: devDependencies,
71
+ dependencies: dependencies,
72
+ publishConfig: publishConfig,
73
+ gitHead: gitHead
74
+ };
75
+
76
+ /**
77
+ * Package version
78
+ */
79
+ const version = packageJson.version;
80
+ // use a custom Console to indicate we really want to log to the console and it's not incidental. `console.log` causes an eslint error
81
+ const Console = console;
82
+ /**
83
+ * Response type
84
+ */
85
+ var ResponseType;
86
+ (function (ResponseType) {
87
+ /**
88
+ * Response from the application
89
+ */
90
+ ResponseType["Application"] = "bot";
91
+ /**
92
+ * Response from the user
93
+ */
94
+ ResponseType["User"] = "user";
95
+ /**
96
+ * Generic failure (cannot be attributed to the application)
97
+ */
98
+ ResponseType["Failure"] = "failure";
99
+ })(ResponseType || (ResponseType = {}));
100
+ const welcomeIntent = "NLX.Welcome";
101
+ const defaultFailureMessage = "We encountered an issue. Please try again soon.";
102
+ const normalizeSlots = (slotsRecordOrArray) => {
103
+ if (Array.isArray(slotsRecordOrArray)) {
104
+ return slotsRecordOrArray;
105
+ }
106
+ return Object.entries(slotsRecordOrArray).map(([key, value]) => ({
107
+ slotId: key,
108
+ value,
109
+ }));
110
+ };
111
+ const normalizeStructuredRequest = (structured) => ({
112
+ ...structured,
113
+ intentId: structured.flowId ?? structured.intentId,
114
+ slots: structured.slots != null
115
+ ? normalizeSlots(structured.slots)
116
+ : structured.slots,
117
+ });
118
+ const fromInternal = (internalState) => internalState.responses;
119
+ const safeJsonParse = (val) => {
120
+ try {
121
+ const json = JSON.parse(val);
122
+ return json;
123
+ }
124
+ catch (_err) {
125
+ return null;
126
+ }
127
+ };
128
+ /**
129
+ * Helper method to decide when a new {@link Config} requires creating a new {@link ConversationHandler} or whether the old `Config`'s
130
+ * `ConversationHandler` can be used.
131
+ *
132
+ * The order of configs doesn't matter.
133
+ * @param config1 -
134
+ * @param config2 -
135
+ * @returns true if `createConversation` should be called again
136
+ */
137
+ const shouldReinitialize = (config1, config2) => {
138
+ return !equals(config1, config2);
139
+ };
140
+ const getBaseDomain = (url) => url.match(/(bots\.dev\.studio\.nlx\.ai|bots\.studio\.nlx\.ai|apps\.nlx\.ai|dev\.apps\.nlx\.ai)/g)?.[0] ?? "apps.nlx.ai";
141
+ /**
142
+ * When a HTTP URL is provided, deduce the websocket URL. Otherwise, return the argument.
143
+ * @param applicationUrl - the websocket URL
144
+ * @returns httpUrl - the HTTP URL
145
+ */
146
+ const normalizeToWebsocket = (applicationUrl) => {
147
+ if (isWebsocketUrl(applicationUrl)) {
148
+ return applicationUrl;
149
+ }
150
+ const base = getBaseDomain(applicationUrl);
151
+ const url = new URL(applicationUrl);
152
+ const pathChunks = url.pathname.split("/");
153
+ const deploymentKey = pathChunks[2];
154
+ const channelKey = pathChunks[3];
155
+ return `wss://us-east-1-ws.${base}?deploymentKey=${deploymentKey}&channelKey=${channelKey}`;
156
+ };
157
+ /**
158
+ * When a websocket URL is provided, deduce the HTTP URL. Otherwise, return the argument.
159
+ * @param applicationUrl - the websocket URL
160
+ * @returns httpUrl - the HTTP URL
161
+ */
162
+ const normalizeToHttp = (applicationUrl) => {
163
+ if (!isWebsocketUrl(applicationUrl)) {
164
+ return applicationUrl;
165
+ }
166
+ const base = getBaseDomain(applicationUrl);
167
+ const url = new URL(applicationUrl);
168
+ const params = new URLSearchParams(url.search);
169
+ const channelKey = params.get("channelKey");
170
+ const deploymentKey = params.get("deploymentKey");
171
+ return `https://${base}/c/${deploymentKey}/${channelKey}`;
172
+ };
173
+ const isWebsocketUrl = (url) => {
174
+ return url.indexOf("wss://") === 0;
175
+ };
176
+ /**
177
+ * Check whether a configuration is value.
178
+ * @param config - Chat configuration
179
+ * @returns isValid - Whether the configuration is valid
180
+ */
181
+ const isConfigValid = (config) => {
182
+ const applicationUrl = config.applicationUrl ?? "";
183
+ return applicationUrl.length > 0;
184
+ };
185
+ /**
186
+ * Call this to create a conversation handler.
187
+ * @param config -
188
+ * @returns The {@link ConversationHandler} is a bundle of functions to interact with the conversation.
189
+ */
190
+ function createConversation(config) {
191
+ let socket;
192
+ let socketMessageQueue = [];
193
+ let socketMessageQueueCheckInterval = null;
194
+ let voicePlusSocket;
195
+ let voicePlusSocketMessageQueue = [];
196
+ let voicePlusSocketMessageQueueCheckInterval = null;
197
+ const applicationUrl = config.applicationUrl ?? "";
198
+ // Check if the application URL has a language code appended to it
199
+ if (/[-|_][a-z]{2,}[-|_][A-Z]{2,}$/.test(applicationUrl)) {
200
+ Console.warn("Since v1.0.0, the language code is no longer added at the end of the application URL. Please remove the modifier (e.g. '-en-US') from the URL, and specify it in the `languageCode` parameter instead.");
201
+ }
202
+ const eventListeners = { voicePlusCommand: [] };
203
+ const initialConversationId = config.conversationId ?? v4();
204
+ let state = {
205
+ responses: config.responses ?? [],
206
+ languageCode: config.languageCode,
207
+ userId: config.userId,
208
+ conversationId: initialConversationId,
209
+ };
210
+ const fullApplicationHttpUrl = () => `${normalizeToHttp(applicationUrl)}${config.experimental?.completeApplicationUrl === true
211
+ ? ""
212
+ : `-${state.languageCode}`}`;
213
+ const setState = (change,
214
+ // Optionally send the response that causes the current state change, to be sent to subscribers
215
+ newResponse) => {
216
+ state = {
217
+ ...state,
218
+ ...change,
219
+ };
220
+ subscribers.forEach((subscriber) => {
221
+ subscriber(fromInternal(state), newResponse);
222
+ });
223
+ };
224
+ const failureHandler = () => {
225
+ const newResponse = {
226
+ type: ResponseType.Failure,
227
+ receivedAt: new Date().getTime(),
228
+ payload: {
229
+ text: config.failureMessage ?? defaultFailureMessage,
230
+ },
231
+ };
232
+ setState({
233
+ responses: [...state.responses, newResponse],
234
+ }, newResponse);
235
+ };
236
+ const messageResponseHandler = (response) => {
237
+ if (response?.messages.length > 0) {
238
+ const newResponse = {
239
+ type: ResponseType.Application,
240
+ receivedAt: new Date().getTime(),
241
+ payload: {
242
+ ...response,
243
+ messages: response.messages.map((message) => ({
244
+ nodeId: message.nodeId,
245
+ messageId: message.messageId,
246
+ text: message.text,
247
+ choices: message.choices ?? [],
248
+ })),
249
+ },
250
+ };
251
+ setState({
252
+ responses: [...state.responses, newResponse],
253
+ }, newResponse);
254
+ if (response.metadata.hasPendingDataRequest) {
255
+ appendStructuredUserResponse({ poll: true });
256
+ setTimeout(() => {
257
+ void sendToApplication({
258
+ request: {
259
+ structured: {
260
+ poll: true,
261
+ },
262
+ },
263
+ });
264
+ }, 1500);
265
+ }
266
+ }
267
+ else {
268
+ Console.warn("Invalid message structure, expected object with field 'messages'.");
269
+ failureHandler();
270
+ }
271
+ };
272
+ let requestOverride;
273
+ const sendVoicePlusMessage = (message) => {
274
+ if (voicePlusSocket?.readyState === 1) {
275
+ voicePlusSocket.send(JSON.stringify(message));
276
+ }
277
+ else {
278
+ voicePlusSocketMessageQueue = [...voicePlusSocketMessageQueue, message];
279
+ }
280
+ };
281
+ const sendToApplication = async (body) => {
282
+ if (requestOverride != null) {
283
+ requestOverride(body, (payload) => {
284
+ const newResponse = {
285
+ type: ResponseType.Application,
286
+ receivedAt: new Date().getTime(),
287
+ payload,
288
+ };
289
+ setState({
290
+ responses: [...state.responses, newResponse],
291
+ }, newResponse);
292
+ });
293
+ return;
294
+ }
295
+ const bodyWithContext = {
296
+ userId: state.userId,
297
+ conversationId: state.conversationId,
298
+ ...body,
299
+ languageCode: state.languageCode,
300
+ channelType: config.experimental?.channelType,
301
+ environment: config.environment,
302
+ };
303
+ if (isWebsocketUrl(applicationUrl)) {
304
+ if (socket?.readyState === 1) {
305
+ socket.send(JSON.stringify(bodyWithContext));
306
+ }
307
+ else {
308
+ socketMessageQueue = [...socketMessageQueue, bodyWithContext];
309
+ }
310
+ }
311
+ else {
312
+ try {
313
+ const res = await fetch(fullApplicationHttpUrl(), {
314
+ method: "POST",
315
+ headers: {
316
+ ...(config.headers ?? {}),
317
+ Accept: "application/json",
318
+ "Content-Type": "application/json",
319
+ "nlx-sdk-version": packageJson.version,
320
+ },
321
+ body: JSON.stringify(bodyWithContext),
322
+ });
323
+ if (res.status >= 400) {
324
+ throw new Error(`Responded with ${res.status}`);
325
+ }
326
+ const json = await res.json();
327
+ messageResponseHandler(json);
328
+ }
329
+ catch (err) {
330
+ Console.warn(err);
331
+ failureHandler();
332
+ }
333
+ }
334
+ };
335
+ let subscribers = [];
336
+ const checkSocketQueue = async () => {
337
+ if (socket?.readyState === 1 && socketMessageQueue[0] != null) {
338
+ await sendToApplication(socketMessageQueue[0]);
339
+ socketMessageQueue = socketMessageQueue.slice(1);
340
+ }
341
+ };
342
+ const checkVoicePlusSocketQueue = () => {
343
+ if (voicePlusSocket?.readyState === 1 &&
344
+ voicePlusSocketMessageQueue[0] != null) {
345
+ sendVoicePlusMessage(voicePlusSocketMessageQueue[0]);
346
+ voicePlusSocketMessageQueue = voicePlusSocketMessageQueue.slice(1);
347
+ }
348
+ };
349
+ const setupWebsocket = () => {
350
+ // If the socket is already set up, tear it down first
351
+ teardownWebsocket();
352
+ const url = new URL(applicationUrl);
353
+ if (config.experimental?.completeApplicationUrl !== true) {
354
+ url.searchParams.set("languageCode", state.languageCode);
355
+ url.searchParams.set("channelKey", `${url.searchParams.get("channelKey") ?? ""}-${state.languageCode}`);
356
+ }
357
+ url.searchParams.set("conversationId", state.conversationId);
358
+ socket = new ReconnectingWebSocket(url.href);
359
+ socketMessageQueueCheckInterval = setInterval(() => {
360
+ void checkSocketQueue();
361
+ }, 500);
362
+ socket.onmessage = function (e) {
363
+ if (typeof e?.data === "string") {
364
+ messageResponseHandler(safeJsonParse(e.data));
365
+ }
366
+ };
367
+ url.searchParams.set("voice-plus", "true");
368
+ voicePlusSocket = new ReconnectingWebSocket(url.href);
369
+ voicePlusSocketMessageQueueCheckInterval = setInterval(() => {
370
+ checkVoicePlusSocketQueue();
371
+ }, 500);
372
+ voicePlusSocket.onmessage = (e) => {
373
+ if (typeof e?.data === "string") {
374
+ const command = safeJsonParse(e.data);
375
+ if (command != null) {
376
+ eventListeners.voicePlusCommand.forEach((listener) => {
377
+ listener(command);
378
+ });
379
+ }
380
+ }
381
+ };
382
+ };
383
+ const setupCommandWebsocket = () => {
384
+ // If the socket is already set up, tear it down first
385
+ teardownCommandWebsocket();
386
+ if (config.bidirectional !== true) {
387
+ return;
388
+ }
389
+ const url = new URL(normalizeToWebsocket(applicationUrl));
390
+ if (config.experimental?.completeApplicationUrl !== true) {
391
+ url.searchParams.set("languageCode", state.languageCode);
392
+ url.searchParams.set("channelKey", `${url.searchParams.get("channelKey") ?? ""}-${state.languageCode}`);
393
+ }
394
+ url.searchParams.set("conversationId", state.conversationId);
395
+ url.searchParams.set("type", "voice-plus");
396
+ const apiKey = config.headers["nlx-api-key"];
397
+ if (!isWebsocketUrl(applicationUrl) && apiKey != null) {
398
+ url.searchParams.set("apiKey", apiKey);
399
+ }
400
+ voicePlusSocket = new ReconnectingWebSocket(url.href);
401
+ voicePlusSocketMessageQueueCheckInterval = setInterval(() => {
402
+ checkVoicePlusSocketQueue();
403
+ }, 500);
404
+ voicePlusSocket.onmessage = (e) => {
405
+ if (typeof e?.data === "string") {
406
+ const command = safeJsonParse(e.data);
407
+ if (command != null) {
408
+ eventListeners.voicePlusCommand.forEach((listener) => {
409
+ listener(command);
410
+ });
411
+ }
412
+ }
413
+ };
414
+ };
415
+ const teardownWebsocket = () => {
416
+ if (socketMessageQueueCheckInterval != null) {
417
+ clearInterval(socketMessageQueueCheckInterval);
418
+ }
419
+ if (socket != null) {
420
+ socket.onmessage = null;
421
+ socket.close();
422
+ socket = undefined;
423
+ }
424
+ };
425
+ const teardownCommandWebsocket = () => {
426
+ if (voicePlusSocketMessageQueueCheckInterval != null) {
427
+ clearInterval(voicePlusSocketMessageQueueCheckInterval);
428
+ }
429
+ if (voicePlusSocket != null) {
430
+ voicePlusSocket.onmessage = null;
431
+ voicePlusSocket.close();
432
+ voicePlusSocket = undefined;
433
+ }
434
+ };
435
+ if (isWebsocketUrl(applicationUrl)) {
436
+ setupWebsocket();
437
+ }
438
+ setupCommandWebsocket();
439
+ const appendStructuredUserResponse = (structured, context) => {
440
+ const newResponse = {
441
+ type: ResponseType.User,
442
+ receivedAt: new Date().getTime(),
443
+ payload: {
444
+ type: "structured",
445
+ ...normalizeStructuredRequest(structured),
446
+ context,
447
+ },
448
+ };
449
+ setState({
450
+ responses: [...state.responses, newResponse],
451
+ }, newResponse);
452
+ };
453
+ const sendFlow = (intentId, context) => {
454
+ appendStructuredUserResponse({ intentId }, context);
455
+ void sendToApplication({
456
+ context,
457
+ request: {
458
+ structured: {
459
+ intentId,
460
+ },
461
+ },
462
+ });
463
+ };
464
+ const sendText = (text, context) => {
465
+ const newResponse = {
466
+ type: ResponseType.User,
467
+ receivedAt: new Date().getTime(),
468
+ payload: {
469
+ type: "text",
470
+ text,
471
+ context,
472
+ },
473
+ };
474
+ setState({
475
+ responses: [...state.responses, newResponse],
476
+ }, newResponse);
477
+ void sendToApplication({
478
+ context,
479
+ request: {
480
+ unstructured: {
481
+ text,
482
+ },
483
+ },
484
+ });
485
+ };
486
+ const sendChoice = (choiceId, context, metadata) => {
487
+ let newResponses = [...state.responses];
488
+ const choiceResponse = {
489
+ type: ResponseType.User,
490
+ receivedAt: new Date().getTime(),
491
+ payload: {
492
+ type: "choice",
493
+ choiceId,
494
+ },
495
+ };
496
+ const responseIndex = metadata?.responseIndex ?? -1;
497
+ const messageIndex = metadata?.messageIndex ?? -1;
498
+ if (responseIndex > -1 && messageIndex > -1) {
499
+ newResponses = adjust(responseIndex, (response) => response.type === ResponseType.Application
500
+ ? {
501
+ ...response,
502
+ payload: {
503
+ ...response.payload,
504
+ messages: adjust(messageIndex, (message) => ({ ...message, selectedChoiceId: choiceId }), response.payload.messages),
505
+ },
506
+ }
507
+ : response, newResponses);
508
+ }
509
+ newResponses = [...newResponses, choiceResponse];
510
+ setState({
511
+ responses: newResponses,
512
+ }, choiceResponse);
513
+ void sendToApplication({
514
+ context,
515
+ request: {
516
+ structured: {
517
+ nodeId: metadata?.nodeId,
518
+ intentId: metadata?.intentId,
519
+ choiceId,
520
+ },
521
+ },
522
+ });
523
+ };
524
+ const unsubscribe = (subscriber) => {
525
+ subscribers = subscribers.filter((fn) => fn !== subscriber);
526
+ };
527
+ const subscribe = (subscriber) => {
528
+ subscribers = [...subscribers, subscriber];
529
+ subscriber(fromInternal(state));
530
+ return () => {
531
+ unsubscribe(subscriber);
532
+ };
533
+ };
534
+ return {
535
+ sendText,
536
+ sendContext: async (context) => {
537
+ const res = await fetch(`${fullApplicationHttpUrl()}/context`, {
538
+ method: "POST",
539
+ headers: {
540
+ ...(config.headers ?? {}),
541
+ Accept: "application/json",
542
+ "Content-Type": "application/json",
543
+ "nlx-conversation-id": state.conversationId,
544
+ "nlx-sdk-version": packageJson.version,
545
+ },
546
+ body: JSON.stringify({
547
+ languageCode: state.languageCode,
548
+ conversationId: state.conversationId,
549
+ userId: state.userId,
550
+ context,
551
+ }),
552
+ });
553
+ if (res.status >= 400) {
554
+ throw new Error(`Responded with ${res.status}`);
555
+ }
556
+ },
557
+ sendStructured: (structured, context) => {
558
+ appendStructuredUserResponse(structured, context);
559
+ void sendToApplication({
560
+ context,
561
+ request: {
562
+ structured: normalizeStructuredRequest(structured),
563
+ },
564
+ });
565
+ },
566
+ sendSlots: (slots, context) => {
567
+ appendStructuredUserResponse({ slots }, context);
568
+ void sendToApplication({
569
+ context,
570
+ request: {
571
+ structured: {
572
+ slots: normalizeSlots(slots),
573
+ },
574
+ },
575
+ });
576
+ },
577
+ sendFlow,
578
+ sendIntent: (intentId, context) => {
579
+ Console.warn("Calling `sendIntent` is deprecated and will be removed in a future version of the SDK. Use `sendFlow` instead.");
580
+ sendFlow(intentId, context);
581
+ },
582
+ sendWelcomeFlow: (context) => {
583
+ sendFlow(welcomeIntent, context);
584
+ },
585
+ sendWelcomeIntent: (context) => {
586
+ Console.warn("Calling `sendWelcomeIntent` is deprecated and will be removed in a future version of the SDK. Use `sendWelcomeFlow` instead.");
587
+ sendFlow(welcomeIntent, context);
588
+ },
589
+ sendChoice,
590
+ currentConversationId: () => {
591
+ return state.conversationId;
592
+ },
593
+ setLanguageCode: (languageCode) => {
594
+ if (languageCode === state.languageCode) {
595
+ Console.warn("Attempted to set language code to the one already active.");
596
+ return;
597
+ }
598
+ if (isWebsocketUrl(applicationUrl)) {
599
+ setupWebsocket();
600
+ }
601
+ setupCommandWebsocket();
602
+ setState({ languageCode });
603
+ },
604
+ currentLanguageCode: () => {
605
+ return state.languageCode;
606
+ },
607
+ getVoiceCredentials: async (context) => {
608
+ const url = normalizeToHttp(applicationUrl);
609
+ const res = await fetch(`${url}-${state.languageCode}/requestToken`, {
610
+ method: "POST",
611
+ headers: {
612
+ ...(config.headers ?? {}),
613
+ Accept: "application/json",
614
+ "Content-Type": "application/json",
615
+ "nlx-conversation-id": state.conversationId,
616
+ "nlx-sdk-version": packageJson.version,
617
+ },
618
+ body: JSON.stringify({
619
+ languageCode: state.languageCode,
620
+ conversationId: state.conversationId,
621
+ userId: state.userId,
622
+ requestToken: true,
623
+ context,
624
+ }),
625
+ });
626
+ if (res.status >= 400) {
627
+ throw new Error(`Responded with ${res.status}`);
628
+ }
629
+ const data = await res.json();
630
+ if (data?.url == null) {
631
+ throw new Error("Invalid response");
632
+ }
633
+ return data;
634
+ },
635
+ subscribe,
636
+ unsubscribe,
637
+ unsubscribeAll: () => {
638
+ subscribers = [];
639
+ },
640
+ reset: (options) => {
641
+ setState({
642
+ conversationId: v4(),
643
+ responses: options?.clearResponses === true ? [] : state.responses,
644
+ });
645
+ if (isWebsocketUrl(applicationUrl)) {
646
+ setupWebsocket();
647
+ }
648
+ setupCommandWebsocket();
649
+ },
650
+ destroy: () => {
651
+ subscribers = [];
652
+ if (isWebsocketUrl(applicationUrl)) {
653
+ teardownWebsocket();
654
+ }
655
+ teardownCommandWebsocket();
656
+ },
657
+ setRequestOverride: (val) => {
658
+ requestOverride = val;
659
+ },
660
+ addEventListener: (event, listener) => {
661
+ eventListeners[event] = [...eventListeners[event], listener];
662
+ },
663
+ removeEventListener: (event, listener) => {
664
+ eventListeners[event] = eventListeners[event].filter((l) => l !== listener);
665
+ },
666
+ sendVoicePlusContext: (context) => {
667
+ sendVoicePlusMessage({ context });
668
+ },
669
+ };
670
+ }
671
+ /**
672
+ * Get current expiration timestamp from the current list of reponses
673
+ * @param responses - the current list of user and application responses (first argument in the subscribe callback)
674
+ * @returns an expiration timestamp in Unix Epoch (`new Date().getTime()`), or `null` if this is not known (typically occurs if the application has not responded yet)
675
+ */
676
+ const getCurrentExpirationTimestamp = (responses) => {
677
+ let expirationTimestamp = null;
678
+ responses.forEach((response) => {
679
+ if (response.type === ResponseType.Application &&
680
+ response.payload.expirationTimestamp != null) {
681
+ expirationTimestamp = response.payload.expirationTimestamp;
682
+ }
683
+ });
684
+ return expirationTimestamp;
685
+ };
686
+ /**
687
+ * This package is intentionally designed with a subscription-based API as opposed to a promise-based one where each message corresponds to a single application response, available asynchronously.
688
+ *
689
+ * If you need a promise-based wrapper, you can use the `promisify` helper available in the package:
690
+ * @example
691
+ * ```typescript
692
+ * import { createConversation, promisify } from "@nlxai/core";
693
+ *
694
+ * const convo = createConversation(config);
695
+ *
696
+ * const sendTextWrapped = promisify(convo.sendText, convo);
697
+ *
698
+ * sendTextWrapped("Hello").then((response) => {
699
+ * console.log(response);
700
+ * });
701
+ * ```
702
+ * @typeParam T - the type of the function's params, e.g. for `sendText` it's `text: string, context?: Context`
703
+ * @param fn - the function to wrap (e.g. `convo.sendText`, `convo.sendChoice`, etc.)
704
+ * @param convo - the `ConversationHandler` (from {@link createConversation})
705
+ * @param timeout - the timeout in milliseconds
706
+ * @returns A promise-wrapped version of the function. The function, when called, returns a promise that resolves to the Conversation's next response.
707
+ */
708
+ function promisify(fn, convo, timeout = 10000) {
709
+ return async (payload) => {
710
+ return await new Promise((resolve, reject) => {
711
+ const timeoutId = setTimeout(() => {
712
+ reject(new Error("The request timed out."));
713
+ convo.unsubscribe(subscription);
714
+ }, timeout);
715
+ const subscription = (_responses, newResponse) => {
716
+ if (newResponse?.type === ResponseType.Application ||
717
+ newResponse?.type === ResponseType.Failure) {
718
+ clearTimeout(timeoutId);
719
+ convo.unsubscribe(subscription);
720
+ resolve(newResponse);
721
+ }
722
+ };
723
+ convo.subscribe(subscription);
724
+ fn(payload);
725
+ });
726
+ };
727
+ }
728
+
729
+ export { ResponseType, createConversation, getCurrentExpirationTimestamp, isConfigValid, promisify, shouldReinitialize, version };