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