@exotel-npm-dev/exotel-ai-assist 1.0.4 → 1.0.6

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.
@@ -343,6 +343,7 @@ class BroadcastChannelTransport {
343
343
  this.pendingUrl = null;
344
344
  this.pendingAuthToken = null;
345
345
  this.lockReleaser = null;
346
+ this._destroyed = false;
346
347
  const channelName = `exotel-ai-assist:${sessionHash}`;
347
348
  const lockName = `exotel-ai-assist-leader:${sessionHash}`;
348
349
  this.channel = new BroadcastChannel(channelName);
@@ -378,20 +379,27 @@ class BroadcastChannelTransport {
378
379
  }, lock => {
379
380
  if (lock) {
380
381
  this.isLeader = true;
381
- if (this.pendingUrl && this.pendingAuthToken) {
382
- this._openSocket(this.pendingUrl, this.pendingAuthToken);
383
- this.pendingUrl = null;
384
- this.pendingAuthToken = null;
385
- }
386
382
  // Hold the lock for the lifetime of this transport instance.
387
383
  return new Promise(resolve => {
388
384
  this.lockReleaser = resolve;
385
+ // The lock can arrive asynchronously AFTER destroy() was called
386
+ // (e.g. React StrictMode runs effect cleanup before the microtask
387
+ // queue drains). Release the lock immediately so the next transport
388
+ // in line can acquire it, and skip opening any socket.
389
+ if (this._destroyed) {
390
+ resolve();
391
+ return;
392
+ }
393
+ if (this.pendingUrl && this.pendingAuthToken) {
394
+ this._openSocket(this.pendingUrl, this.pendingAuthToken);
395
+ this.pendingUrl = null;
396
+ this.pendingAuthToken = null;
397
+ }
389
398
  });
390
399
  }
391
400
  return Promise.resolve();
392
401
  }).catch(() => {
393
- // Navigator Locks not supported — every tab owns its own WebSocket.
394
- this.isLeader = true;
402
+ if (!this._destroyed) this.isLeader = true;
395
403
  });
396
404
  }
397
405
  _openSocket(url, authToken) {
@@ -401,6 +409,7 @@ class BroadcastChannelTransport {
401
409
  this.socket = new WebSocket(url);
402
410
  this.socket.onopen = () => {
403
411
  var _a;
412
+ if (this._destroyed) return;
404
413
  this.socketConnected = true;
405
414
  this.socket.send(JSON.stringify({
406
415
  type: "auth",
@@ -409,36 +418,39 @@ class BroadcastChannelTransport {
409
418
  const msg = {
410
419
  type: "CONNECTED"
411
420
  };
412
- (_a = this.handler) === null || _a === void 0 ? void 0 : _a.call(this, msg);
413
421
  this.channel.postMessage(msg);
422
+ (_a = this.handler) === null || _a === void 0 ? void 0 : _a.call(this, msg);
414
423
  };
415
424
  this.socket.onmessage = ev => {
416
425
  var _a;
426
+ if (this._destroyed) return;
417
427
  const msg = {
418
428
  type: "MESSAGE",
419
429
  payload: ev.data
420
430
  };
421
- (_a = this.handler) === null || _a === void 0 ? void 0 : _a.call(this, msg);
422
431
  this.channel.postMessage(msg);
432
+ (_a = this.handler) === null || _a === void 0 ? void 0 : _a.call(this, msg);
423
433
  };
424
434
  this.socket.onerror = () => {
425
435
  var _a;
436
+ if (this._destroyed) return;
426
437
  const msg = {
427
438
  type: "ERROR",
428
439
  message: "WebSocket error"
429
440
  };
430
- (_a = this.handler) === null || _a === void 0 ? void 0 : _a.call(this, msg);
431
441
  this.channel.postMessage(msg);
442
+ (_a = this.handler) === null || _a === void 0 ? void 0 : _a.call(this, msg);
432
443
  };
433
444
  this.socket.onclose = ev => {
434
445
  var _a;
446
+ if (this._destroyed) return;
435
447
  this.socketConnected = false;
436
448
  const msg = {
437
449
  type: "DISCONNECTED",
438
450
  code: ev.code
439
451
  };
440
- (_a = this.handler) === null || _a === void 0 ? void 0 : _a.call(this, msg);
441
452
  this.channel.postMessage(msg);
453
+ (_a = this.handler) === null || _a === void 0 ? void 0 : _a.call(this, msg);
442
454
  this.socket = null;
443
455
  };
444
456
  }
@@ -467,6 +479,7 @@ class BroadcastChannelTransport {
467
479
  }
468
480
  destroy() {
469
481
  var _a;
482
+ this._destroyed = true;
470
483
  this.disconnect();
471
484
  this.channel.close();
472
485
  (_a = this.lockReleaser) === null || _a === void 0 ? void 0 : _a.call(this);
package/dist/index.js CHANGED
@@ -19761,6 +19761,7 @@ class BroadcastChannelTransport {
19761
19761
  this.pendingUrl = null;
19762
19762
  this.pendingAuthToken = null;
19763
19763
  this.lockReleaser = null;
19764
+ this._destroyed = false;
19764
19765
  const channelName = `exotel-ai-assist:${sessionHash}`;
19765
19766
  const lockName = `exotel-ai-assist-leader:${sessionHash}`;
19766
19767
  this.channel = new BroadcastChannel(channelName);
@@ -19796,20 +19797,27 @@ class BroadcastChannelTransport {
19796
19797
  }, lock => {
19797
19798
  if (lock) {
19798
19799
  this.isLeader = true;
19799
- if (this.pendingUrl && this.pendingAuthToken) {
19800
- this._openSocket(this.pendingUrl, this.pendingAuthToken);
19801
- this.pendingUrl = null;
19802
- this.pendingAuthToken = null;
19803
- }
19804
19800
  // Hold the lock for the lifetime of this transport instance.
19805
19801
  return new Promise(resolve => {
19806
19802
  this.lockReleaser = resolve;
19803
+ // The lock can arrive asynchronously AFTER destroy() was called
19804
+ // (e.g. React StrictMode runs effect cleanup before the microtask
19805
+ // queue drains). Release the lock immediately so the next transport
19806
+ // in line can acquire it, and skip opening any socket.
19807
+ if (this._destroyed) {
19808
+ resolve();
19809
+ return;
19810
+ }
19811
+ if (this.pendingUrl && this.pendingAuthToken) {
19812
+ this._openSocket(this.pendingUrl, this.pendingAuthToken);
19813
+ this.pendingUrl = null;
19814
+ this.pendingAuthToken = null;
19815
+ }
19807
19816
  });
19808
19817
  }
19809
19818
  return Promise.resolve();
19810
19819
  }).catch(() => {
19811
- // Navigator Locks not supported — every tab owns its own WebSocket.
19812
- this.isLeader = true;
19820
+ if (!this._destroyed) this.isLeader = true;
19813
19821
  });
19814
19822
  }
19815
19823
  _openSocket(url, authToken) {
@@ -19819,6 +19827,7 @@ class BroadcastChannelTransport {
19819
19827
  this.socket = new WebSocket(url);
19820
19828
  this.socket.onopen = () => {
19821
19829
  var _a;
19830
+ if (this._destroyed) return;
19822
19831
  this.socketConnected = true;
19823
19832
  this.socket.send(JSON.stringify({
19824
19833
  type: "auth",
@@ -19827,36 +19836,39 @@ class BroadcastChannelTransport {
19827
19836
  const msg = {
19828
19837
  type: "CONNECTED"
19829
19838
  };
19830
- (_a = this.handler) === null || _a === void 0 ? void 0 : _a.call(this, msg);
19831
19839
  this.channel.postMessage(msg);
19840
+ (_a = this.handler) === null || _a === void 0 ? void 0 : _a.call(this, msg);
19832
19841
  };
19833
19842
  this.socket.onmessage = ev => {
19834
19843
  var _a;
19844
+ if (this._destroyed) return;
19835
19845
  const msg = {
19836
19846
  type: "MESSAGE",
19837
19847
  payload: ev.data
19838
19848
  };
19839
- (_a = this.handler) === null || _a === void 0 ? void 0 : _a.call(this, msg);
19840
19849
  this.channel.postMessage(msg);
19850
+ (_a = this.handler) === null || _a === void 0 ? void 0 : _a.call(this, msg);
19841
19851
  };
19842
19852
  this.socket.onerror = () => {
19843
19853
  var _a;
19854
+ if (this._destroyed) return;
19844
19855
  const msg = {
19845
19856
  type: "ERROR",
19846
19857
  message: "WebSocket error"
19847
19858
  };
19848
- (_a = this.handler) === null || _a === void 0 ? void 0 : _a.call(this, msg);
19849
19859
  this.channel.postMessage(msg);
19860
+ (_a = this.handler) === null || _a === void 0 ? void 0 : _a.call(this, msg);
19850
19861
  };
19851
19862
  this.socket.onclose = ev => {
19852
19863
  var _a;
19864
+ if (this._destroyed) return;
19853
19865
  this.socketConnected = false;
19854
19866
  const msg = {
19855
19867
  type: "DISCONNECTED",
19856
19868
  code: ev.code
19857
19869
  };
19858
- (_a = this.handler) === null || _a === void 0 ? void 0 : _a.call(this, msg);
19859
19870
  this.channel.postMessage(msg);
19871
+ (_a = this.handler) === null || _a === void 0 ? void 0 : _a.call(this, msg);
19860
19872
  this.socket = null;
19861
19873
  };
19862
19874
  }
@@ -19885,6 +19897,7 @@ class BroadcastChannelTransport {
19885
19897
  }
19886
19898
  destroy() {
19887
19899
  var _a;
19900
+ this._destroyed = true;
19888
19901
  this.disconnect();
19889
19902
  this.channel.close();
19890
19903
  (_a = this.lockReleaser) === null || _a === void 0 ? void 0 : _a.call(this);
@@ -21189,7 +21202,6 @@ ExotelAIAssistController.RECONNECT_BASE_MS = 3000;
21189
21202
 
21190
21203
  function useExotelAIAssist(params) {
21191
21204
  const controllerRef = reactExports.useRef(null);
21192
- const paramsRef = reactExports.useRef(params);
21193
21205
  const [status, setStatus] = reactExports.useState("idle");
21194
21206
  const [suggestions, setSuggestions] = reactExports.useState([]);
21195
21207
  const [transcripts, setTranscripts] = reactExports.useState([]);
@@ -21198,10 +21210,14 @@ function useExotelAIAssist(params) {
21198
21210
  const [lastError, setLastError] = reactExports.useState(null);
21199
21211
  const paramHash = reactExports.useMemo(() => Utils.hash(params), [params]);
21200
21212
  reactExports.useEffect(() => {
21201
- paramsRef.current = params;
21202
- });
21203
- reactExports.useEffect(() => {
21204
- const ctrl = new ExotelAIAssistController(paramsRef.current);
21213
+ // Clear all stale state from the previous session before connecting.
21214
+ setStatus("idle");
21215
+ setSuggestions([]);
21216
+ setTranscripts([]);
21217
+ setSentiment(null);
21218
+ setBotConfig(null);
21219
+ setLastError(null);
21220
+ const ctrl = new ExotelAIAssistController(params);
21205
21221
  controllerRef.current = ctrl;
21206
21222
  ctrl.on("statusChange", setStatus);
21207
21223
  ctrl.on("botConfig", setBotConfig);
@@ -21219,22 +21235,6 @@ function useExotelAIAssist(params) {
21219
21235
  controllerRef.current = null;
21220
21236
  };
21221
21237
  // eslint-disable-next-line react-hooks/exhaustive-deps
21222
- }, []);
21223
- const prevParamHashRef = reactExports.useRef(paramHash);
21224
- reactExports.useEffect(() => {
21225
- if (!controllerRef.current) return;
21226
- const prevParamHash = prevParamHashRef.current;
21227
- const paramHashChanged = paramHash !== prevParamHash;
21228
- if (paramHashChanged) {
21229
- prevParamHashRef.current = paramHash;
21230
- setSuggestions([]);
21231
- setTranscripts([]);
21232
- setSentiment(null);
21233
- setBotConfig(null);
21234
- setLastError(null);
21235
- }
21236
- controllerRef.current.setParams(params);
21237
- // eslint-disable-next-line react-hooks/exhaustive-deps
21238
21238
  }, [paramHash]);
21239
21239
  const connect = reactExports.useCallback(() => {
21240
21240
  var _a;
@@ -21771,7 +21771,8 @@ function TranscriptTab({
21771
21771
  alignItems: "flex-start",
21772
21772
  gap: "12px",
21773
21773
  padding: "10px",
21774
- borderBottom: "1px solid #e5e7eb"
21774
+ borderBottom: "1px solid #e5e7eb",
21775
+ textAlign: "left"
21775
21776
  },
21776
21777
  children: [jsxRuntimeExports.jsx("span", {
21777
21778
  style: {
@@ -5047,6 +5047,7 @@ class BroadcastChannelTransport {
5047
5047
  this.pendingUrl = null;
5048
5048
  this.pendingAuthToken = null;
5049
5049
  this.lockReleaser = null;
5050
+ this._destroyed = false;
5050
5051
  const channelName = `exotel-ai-assist:${sessionHash}`;
5051
5052
  const lockName = `exotel-ai-assist-leader:${sessionHash}`;
5052
5053
  this.channel = new BroadcastChannel(channelName);
@@ -5082,20 +5083,27 @@ class BroadcastChannelTransport {
5082
5083
  }, lock => {
5083
5084
  if (lock) {
5084
5085
  this.isLeader = true;
5085
- if (this.pendingUrl && this.pendingAuthToken) {
5086
- this._openSocket(this.pendingUrl, this.pendingAuthToken);
5087
- this.pendingUrl = null;
5088
- this.pendingAuthToken = null;
5089
- }
5090
5086
  // Hold the lock for the lifetime of this transport instance.
5091
5087
  return new Promise(resolve => {
5092
5088
  this.lockReleaser = resolve;
5089
+ // The lock can arrive asynchronously AFTER destroy() was called
5090
+ // (e.g. React StrictMode runs effect cleanup before the microtask
5091
+ // queue drains). Release the lock immediately so the next transport
5092
+ // in line can acquire it, and skip opening any socket.
5093
+ if (this._destroyed) {
5094
+ resolve();
5095
+ return;
5096
+ }
5097
+ if (this.pendingUrl && this.pendingAuthToken) {
5098
+ this._openSocket(this.pendingUrl, this.pendingAuthToken);
5099
+ this.pendingUrl = null;
5100
+ this.pendingAuthToken = null;
5101
+ }
5093
5102
  });
5094
5103
  }
5095
5104
  return Promise.resolve();
5096
5105
  }).catch(() => {
5097
- // Navigator Locks not supported — every tab owns its own WebSocket.
5098
- this.isLeader = true;
5106
+ if (!this._destroyed) this.isLeader = true;
5099
5107
  });
5100
5108
  }
5101
5109
  _openSocket(url, authToken) {
@@ -5105,6 +5113,7 @@ class BroadcastChannelTransport {
5105
5113
  this.socket = new WebSocket(url);
5106
5114
  this.socket.onopen = () => {
5107
5115
  var _a;
5116
+ if (this._destroyed) return;
5108
5117
  this.socketConnected = true;
5109
5118
  this.socket.send(JSON.stringify({
5110
5119
  type: "auth",
@@ -5113,36 +5122,39 @@ class BroadcastChannelTransport {
5113
5122
  const msg = {
5114
5123
  type: "CONNECTED"
5115
5124
  };
5116
- (_a = this.handler) === null || _a === void 0 ? void 0 : _a.call(this, msg);
5117
5125
  this.channel.postMessage(msg);
5126
+ (_a = this.handler) === null || _a === void 0 ? void 0 : _a.call(this, msg);
5118
5127
  };
5119
5128
  this.socket.onmessage = ev => {
5120
5129
  var _a;
5130
+ if (this._destroyed) return;
5121
5131
  const msg = {
5122
5132
  type: "MESSAGE",
5123
5133
  payload: ev.data
5124
5134
  };
5125
- (_a = this.handler) === null || _a === void 0 ? void 0 : _a.call(this, msg);
5126
5135
  this.channel.postMessage(msg);
5136
+ (_a = this.handler) === null || _a === void 0 ? void 0 : _a.call(this, msg);
5127
5137
  };
5128
5138
  this.socket.onerror = () => {
5129
5139
  var _a;
5140
+ if (this._destroyed) return;
5130
5141
  const msg = {
5131
5142
  type: "ERROR",
5132
5143
  message: "WebSocket error"
5133
5144
  };
5134
- (_a = this.handler) === null || _a === void 0 ? void 0 : _a.call(this, msg);
5135
5145
  this.channel.postMessage(msg);
5146
+ (_a = this.handler) === null || _a === void 0 ? void 0 : _a.call(this, msg);
5136
5147
  };
5137
5148
  this.socket.onclose = ev => {
5138
5149
  var _a;
5150
+ if (this._destroyed) return;
5139
5151
  this.socketConnected = false;
5140
5152
  const msg = {
5141
5153
  type: "DISCONNECTED",
5142
5154
  code: ev.code
5143
5155
  };
5144
- (_a = this.handler) === null || _a === void 0 ? void 0 : _a.call(this, msg);
5145
5156
  this.channel.postMessage(msg);
5157
+ (_a = this.handler) === null || _a === void 0 ? void 0 : _a.call(this, msg);
5146
5158
  this.socket = null;
5147
5159
  };
5148
5160
  }
@@ -5171,6 +5183,7 @@ class BroadcastChannelTransport {
5171
5183
  }
5172
5184
  destroy() {
5173
5185
  var _a;
5186
+ this._destroyed = true;
5174
5187
  this.disconnect();
5175
5188
  this.channel.close();
5176
5189
  (_a = this.lockReleaser) === null || _a === void 0 ? void 0 : _a.call(this);
@@ -6475,7 +6488,6 @@ ExotelAIAssistController.RECONNECT_BASE_MS = 3000;
6475
6488
 
6476
6489
  function useExotelAIAssist(params) {
6477
6490
  const controllerRef = React.useRef(null);
6478
- const paramsRef = React.useRef(params);
6479
6491
  const [status, setStatus] = React.useState("idle");
6480
6492
  const [suggestions, setSuggestions] = React.useState([]);
6481
6493
  const [transcripts, setTranscripts] = React.useState([]);
@@ -6484,10 +6496,14 @@ function useExotelAIAssist(params) {
6484
6496
  const [lastError, setLastError] = React.useState(null);
6485
6497
  const paramHash = React.useMemo(() => Utils.hash(params), [params]);
6486
6498
  React.useEffect(() => {
6487
- paramsRef.current = params;
6488
- });
6489
- React.useEffect(() => {
6490
- const ctrl = new ExotelAIAssistController(paramsRef.current);
6499
+ // Clear all stale state from the previous session before connecting.
6500
+ setStatus("idle");
6501
+ setSuggestions([]);
6502
+ setTranscripts([]);
6503
+ setSentiment(null);
6504
+ setBotConfig(null);
6505
+ setLastError(null);
6506
+ const ctrl = new ExotelAIAssistController(params);
6491
6507
  controllerRef.current = ctrl;
6492
6508
  ctrl.on("statusChange", setStatus);
6493
6509
  ctrl.on("botConfig", setBotConfig);
@@ -6505,22 +6521,6 @@ function useExotelAIAssist(params) {
6505
6521
  controllerRef.current = null;
6506
6522
  };
6507
6523
  // eslint-disable-next-line react-hooks/exhaustive-deps
6508
- }, []);
6509
- const prevParamHashRef = React.useRef(paramHash);
6510
- React.useEffect(() => {
6511
- if (!controllerRef.current) return;
6512
- const prevParamHash = prevParamHashRef.current;
6513
- const paramHashChanged = paramHash !== prevParamHash;
6514
- if (paramHashChanged) {
6515
- prevParamHashRef.current = paramHash;
6516
- setSuggestions([]);
6517
- setTranscripts([]);
6518
- setSentiment(null);
6519
- setBotConfig(null);
6520
- setLastError(null);
6521
- }
6522
- controllerRef.current.setParams(params);
6523
- // eslint-disable-next-line react-hooks/exhaustive-deps
6524
6524
  }, [paramHash]);
6525
6525
  const connect = React.useCallback(() => {
6526
6526
  var _a;
@@ -7057,7 +7057,8 @@ function TranscriptTab({
7057
7057
  alignItems: "flex-start",
7058
7058
  gap: "12px",
7059
7059
  padding: "10px",
7060
- borderBottom: "1px solid #e5e7eb"
7060
+ borderBottom: "1px solid #e5e7eb",
7061
+ textAlign: "left"
7061
7062
  },
7062
7063
  children: [jsxRuntime.jsx("span", {
7063
7064
  style: {
@@ -16,6 +16,7 @@ export declare class BroadcastChannelTransport implements ITransport {
16
16
  private pendingUrl;
17
17
  private pendingAuthToken;
18
18
  private lockReleaser;
19
+ private _destroyed;
19
20
  /**
20
21
  * @param sessionHash - A hash of all connection-relevant params (see
21
22
  * Utils.hashParams). Tabs with identical params share one WebSocket;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exotel-npm-dev/exotel-ai-assist",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "AI-powered real-time suggestions, transcript, and sentiment for agent calls",
5
5
  "license": "Apache-2.0",
6
6
  "main": "dist/index.js",