@kaltura-sdk/avatars 1.21.1 → 1.21.3-hotfix.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/index.esm.js CHANGED
@@ -6170,12 +6170,24 @@ const clientConfigurationSchema = z$1.object({
6170
6170
  isWebSearchEnabled: z$1.boolean(),
6171
6171
  isScreenShareEnabled: z$1.boolean(),
6172
6172
  isCameraAnalysisEnabled: z$1.boolean(),
6173
- youtubeUrl: z$1.string(),
6174
- audioMode: z$1.boolean(),
6175
- phoneMode: z$1.boolean(),
6176
6173
  initialHtml: z$1.string(),
6177
6174
  visualPhotos: z$1.array(z$1.unknown()),
6178
- visualVideos: z$1.array(z$1.unknown())
6175
+ visualVideos: z$1.array(z$1.unknown()),
6176
+ youtubeUrl: z$1.optional(z$1.string()),
6177
+ audioMode: z$1.optional(z$1.boolean()),
6178
+ phoneMode: z$1.optional(z$1.boolean()),
6179
+ forwardLoopMode: z$1.optional(z$1.boolean()),
6180
+ imaginativeAiMode: z$1.optional(z$1.boolean()),
6181
+ interruptionsEnabled: z$1.optional(z$1.boolean()),
6182
+ pauseConversationEnabled: z$1.optional(z$1.boolean()),
6183
+ showTranscription: z$1.optional(z$1.boolean()),
6184
+ shouldAggregateCurrentTurn: z$1.optional(z$1.boolean()),
6185
+ agentPersonaName: z$1.optional(z$1.nullable(z$1.string())),
6186
+ userName: z$1.optional(z$1.nullable(z$1.string())),
6187
+ asr: z$1.optional(z$1.object({
6188
+ provider: z$1.string(),
6189
+ language: z$1.string()
6190
+ }))
6179
6191
  })
6180
6192
  });
6181
6193
  const joinCompleteSchema = z$1.object({});
@@ -23829,6 +23841,11 @@ const avatarConnectionMachine = setup({
23829
23841
  });
23830
23842
 
23831
23843
  const ACTIVE_SESSION_STORE_ID = 'agenticExperienceAvatar';
23844
+ const PROBE_STORE_ID = 'agenticExperienceAvatarProbe';
23845
+ const HEARTBEAT_INTERVAL_MS = 2000;
23846
+ const HEARTBEAT_STALE_AFTER_MS = 6000;
23847
+ // A live owner answers within one postMessage round trip; this only has to outlast a busy main thread.
23848
+ const PROBE_TIMEOUT_MS = 1000;
23832
23849
  function createId() {
23833
23850
  return typeof crypto !== 'undefined' && crypto.randomUUID ? crypto.randomUUID() : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
23834
23851
  }
@@ -23837,18 +23854,29 @@ function createId() {
23837
23854
  * just overwriting the shared record: the previous owner sees an id that is
23838
23855
  * no longer its own and ends its own call (a close callback could not cross
23839
23856
  * frames).
23857
+ *
23858
+ * `pagehide` can't be trusted to release (crashed/frozen tabs, an iframe the
23859
+ * host removes while the avatar is mid-call), so the owner also refreshes
23860
+ * `heartbeatAt` and everyone else ignores a record that stopped refreshing.
23861
+ * `verifyConflictOwner()` shortcuts that wait: a removed iframe can't answer a ping.
23840
23862
  */
23841
23863
  class ActiveSessionCoordinator {
23842
23864
  constructor(_options) {
23843
23865
  this._options = _options;
23844
23866
  this.id = createId();
23845
23867
  this._owns = false;
23868
+ // Heartbeats change every record, so only `id`/`preventDisconnections` count as a change.
23846
23869
  this._conflict = new TinyDataStore({
23847
23870
  initialValue: null,
23848
- compare: 'deep'
23871
+ compare: (a, b) => (a === null || a === void 0 ? void 0 : a.id) === (b === null || b === void 0 ? void 0 : b.id) && (a === null || a === void 0 ? void 0 : a.preventDisconnections) === (b === null || b === void 0 ? void 0 : b.preventDisconnections)
23849
23872
  });
23873
+ // Owners that failed a probe, keyed to the heartbeat they were last seen with; a newer heartbeat revives them.
23874
+ this._abandoned = new Map();
23875
+ this._pendingProbe = null;
23850
23876
  this._win = window;
23851
23877
  this._onPageHide = () => this.release();
23878
+ this._heartbeatTimer = null;
23879
+ this._expiryTimer = null;
23852
23880
  this._logger = _options.logger;
23853
23881
  this._global = getOrCreateGlobalTinyDataStore({
23854
23882
  id: ACTIVE_SESSION_STORE_ID,
@@ -23856,6 +23884,14 @@ class ActiveSessionCoordinator {
23856
23884
  logger: _options.logger
23857
23885
  });
23858
23886
  this._unsubscribe = this._global.onChanges(value => this._handle(value));
23887
+ this._probe = getOrCreateGlobalTinyDataStore({
23888
+ id: PROBE_STORE_ID,
23889
+ initialValue: null,
23890
+ logger: _options.logger
23891
+ });
23892
+ this._unsubscribeProbe = this._probe.onChanges(probe => this._handleProbe(probe), {
23893
+ replayLastValue: false
23894
+ });
23859
23895
  // A closed iframe must not leave the page locked.
23860
23896
  this._win.addEventListener('pagehide', this._onPageHide);
23861
23897
  }
@@ -23875,25 +23911,57 @@ class ActiveSessionCoordinator {
23875
23911
  preventDisconnections
23876
23912
  }
23877
23913
  });
23878
- this._global.setData(() => ({
23879
- id: this.id,
23880
- preventDisconnections
23881
- }));
23914
+ this._publish(preventDisconnections);
23915
+ this._startHeartbeat();
23882
23916
  }
23883
23917
  /** Republishes the flag only while this avatar owns the call; never claims an idle one */
23884
23918
  updateFlag(preventDisconnections) {
23885
23919
  var _a, _b;
23886
23920
  if (this._owns && ((_a = this._global.getData()) === null || _a === void 0 ? void 0 : _a.id) === this.id && ((_b = this._global.getData()) === null || _b === void 0 ? void 0 : _b.preventDisconnections) !== preventDisconnections) {
23887
- this._global.setData(() => ({
23888
- id: this.id,
23889
- preventDisconnections
23890
- }));
23921
+ this._publish(preventDisconnections);
23891
23922
  }
23892
23923
  }
23924
+ /**
23925
+ * Pings the avatar that owns the call. No answer within `PROBE_TIMEOUT_MS`
23926
+ * (removed iframe, crashed tab) marks it abandoned so `conflict` clears
23927
+ * instead of waiting for its heartbeat to go stale.
23928
+ */
23929
+ verifyConflictOwner() {
23930
+ const record = this._global.getData();
23931
+ if (!record || record.id === this.id || this._pendingProbe) {
23932
+ return;
23933
+ }
23934
+ const nonce = createId();
23935
+ const timer = setTimeout(() => {
23936
+ var _a;
23937
+ this._pendingProbe = null;
23938
+ const current = this._global.getData();
23939
+ // A newer heartbeat in the meantime proves the owner is alive.
23940
+ if ((current === null || current === void 0 ? void 0 : current.id) === record.id && current.heartbeatAt === record.heartbeatAt) {
23941
+ (_a = this._logger) === null || _a === void 0 ? void 0 : _a.log('active avatar session owner did not answer, treating it as abandoned', {
23942
+ data: {
23943
+ id: record.id
23944
+ }
23945
+ });
23946
+ this._abandoned.set(record.id, record.heartbeatAt);
23947
+ this._handle(current);
23948
+ }
23949
+ }, PROBE_TIMEOUT_MS);
23950
+ this._pendingProbe = {
23951
+ nonce,
23952
+ timer
23953
+ };
23954
+ this._probe.setData(() => ({
23955
+ nonce,
23956
+ ownerId: record.id,
23957
+ reply: false
23958
+ }));
23959
+ }
23893
23960
  /** Clears the record only if it is still ours, so a taken-over avatar can't wipe the new owner */
23894
23961
  release() {
23895
23962
  var _a, _b;
23896
23963
  this._owns = false;
23964
+ this._stopHeartbeat();
23897
23965
  if (((_a = this._global.getData()) === null || _a === void 0 ? void 0 : _a.id) === this.id) {
23898
23966
  (_b = this._logger) === null || _b === void 0 ? void 0 : _b.log('releasing the active avatar session', {
23899
23967
  data: {
@@ -23906,15 +23974,85 @@ class ActiveSessionCoordinator {
23906
23974
  dispose() {
23907
23975
  this.release();
23908
23976
  this._unsubscribe();
23977
+ this._unsubscribeProbe();
23978
+ this._clearExpiryTimer();
23979
+ this._clearPendingProbe();
23909
23980
  this._win.removeEventListener('pagehide', this._onPageHide);
23910
23981
  }
23911
- _handle(value) {
23982
+ _publish(preventDisconnections) {
23983
+ this._global.setData(() => ({
23984
+ id: this.id,
23985
+ preventDisconnections,
23986
+ heartbeatAt: Date.now()
23987
+ }));
23988
+ }
23989
+ _startHeartbeat() {
23990
+ if (this._heartbeatTimer) {
23991
+ return;
23992
+ }
23993
+ this._heartbeatTimer = setInterval(() => {
23994
+ const current = this._global.getData();
23995
+ if (this._owns && (current === null || current === void 0 ? void 0 : current.id) === this.id) {
23996
+ this._publish(current.preventDisconnections);
23997
+ }
23998
+ }, HEARTBEAT_INTERVAL_MS);
23999
+ }
24000
+ _stopHeartbeat() {
24001
+ if (this._heartbeatTimer) {
24002
+ clearInterval(this._heartbeatTimer);
24003
+ this._heartbeatTimer = null;
24004
+ }
24005
+ }
24006
+ _clearPendingProbe() {
24007
+ if (this._pendingProbe) {
24008
+ clearTimeout(this._pendingProbe.timer);
24009
+ this._pendingProbe = null;
24010
+ }
24011
+ }
24012
+ _handleProbe(probe) {
23912
24013
  var _a;
23913
- const other = value && value.id !== this.id ? value : null;
24014
+ if (!probe) {
24015
+ return;
24016
+ }
24017
+ if (!probe.reply && probe.ownerId === this.id && this._owns) {
24018
+ this._probe.setData(() => Object.assign(Object.assign({}, probe), {
24019
+ reply: true
24020
+ }));
24021
+ } else if (probe.reply && probe.nonce === ((_a = this._pendingProbe) === null || _a === void 0 ? void 0 : _a.nonce)) {
24022
+ this._clearPendingProbe();
24023
+ }
24024
+ }
24025
+ _clearExpiryTimer() {
24026
+ if (this._expiryTimer) {
24027
+ clearTimeout(this._expiryTimer);
24028
+ this._expiryTimer = null;
24029
+ }
24030
+ }
24031
+ _handle(value) {
24032
+ var _a, _b, _c;
24033
+ this._clearExpiryTimer();
24034
+ // A record without `heartbeatAt` (older bundle in another frame) never expires.
24035
+ const staleIn = value && typeof value.heartbeatAt === 'number' ? value.heartbeatAt + HEARTBEAT_STALE_AFTER_MS - Date.now() : Infinity;
24036
+ const isAbandoned = !!value && value.heartbeatAt !== undefined && value.heartbeatAt <= ((_a = this._abandoned.get(value.id)) !== null && _a !== void 0 ? _a : -Infinity);
24037
+ const isStale = !!value && (staleIn < 0 || isAbandoned);
24038
+ if (isStale && value.id !== this.id) {
24039
+ (_b = this._logger) === null || _b === void 0 ? void 0 : _b.log('ignoring an abandoned active avatar session', {
24040
+ data: {
24041
+ id: value.id,
24042
+ heartbeatAt: value.heartbeatAt
24043
+ }
24044
+ });
24045
+ }
24046
+ const other = value && value.id !== this.id && !isStale ? value : null;
23914
24047
  this._conflict.setData(() => other);
24048
+ if (other && Number.isFinite(staleIn)) {
24049
+ // Nothing is written when the owner dies, so re-evaluate once its record would go stale.
24050
+ this._expiryTimer = setTimeout(() => this._handle(this._global.getData()), staleIn + 1);
24051
+ }
23915
24052
  if (other && this._owns) {
23916
24053
  this._owns = false;
23917
- (_a = this._logger) === null || _a === void 0 ? void 0 : _a.log('active avatar session taken over by another avatar', {
24054
+ this._stopHeartbeat();
24055
+ (_c = this._logger) === null || _c === void 0 ? void 0 : _c.log('active avatar session taken over by another avatar', {
23918
24056
  data: {
23919
24057
  id: this.id,
23920
24058
  by: other.id
@@ -24237,6 +24375,19 @@ class AvatarClientManager extends ActorManager {
24237
24375
  preventDisconnections: (_b = (_a = this._config.runtimeSettings.multipleAvatarBehavior) === null || _a === void 0 ? void 0 : _a.preventDisconnections) !== null && _b !== void 0 ? _b : false
24238
24376
  };
24239
24377
  });
24378
+ // The call that held our connect is gone (ended, abandoned): the user already asked to start, so just start.
24379
+ this._activeSession.conflict.onChanges(conflict => {
24380
+ if (conflict || !this._isTakeoverPending.getData()) {
24381
+ return;
24382
+ }
24383
+ const options = this._pendingTakeoverOptions;
24384
+ this._pendingTakeoverOptions = undefined;
24385
+ this._isTakeoverPending.setData(() => false);
24386
+ this._logger.log('Resuming the held connect: the other avatar call is gone');
24387
+ this.connect(options);
24388
+ }, {
24389
+ replayLastValue: false
24390
+ });
24240
24391
  this._status.onChanges(status => {
24241
24392
  if (status === AvatarConnectionStatus.NotConnected || status === AvatarConnectionStatus.Disconnected || status === AvatarConnectionStatus.Error) {
24242
24393
  this._activeSession.release();
@@ -24459,6 +24610,8 @@ class AvatarClientManager extends ActorManager {
24459
24610
  });
24460
24611
  this._pendingTakeoverOptions = options;
24461
24612
  this._isTakeoverPending.setData(() => true);
24613
+ // An owner that is already gone (removed iframe, crashed tab) can't answer, so don't make the user wait out its heartbeat.
24614
+ this._activeSession.verifyConflictOwner();
24462
24615
  return;
24463
24616
  }
24464
24617
  this._activeSession.claim(this._multipleAvatarBehavior.getData().preventDisconnections);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaltura-sdk/avatars",
3
- "version": "1.21.1",
3
+ "version": "1.21.3-hotfix.0",
4
4
  "author": "kaltura",
5
5
  "license": "AGPL-3.0",
6
6
  "repository": {
@@ -12,8 +12,8 @@
12
12
  "@kaltura-sdk/rtc-avatar": "^1.33.4",
13
13
  "@kaltura-sdk/rtc-core": "^1.26.2",
14
14
  "@local/genie-shared-utils": "^1.0.0",
15
- "@unisphere/core": "^1.101.1",
16
- "@unisphere/genie-types": "^1.47.0",
15
+ "@unisphere/core": "^1.101.2",
16
+ "@unisphere/genie-types": "^1.47.1",
17
17
  "@unisphere/notifications-core": "^1.25.0",
18
18
  "@unisphere/runtime": "^1.98.0",
19
19
  "@unisphere/runtime-js": "^1.98.0",
@@ -14,12 +14,24 @@ export declare const clientConfigurationSchema: z.ZodMiniObject<{
14
14
  isWebSearchEnabled: z.ZodMiniBoolean<boolean>;
15
15
  isScreenShareEnabled: z.ZodMiniBoolean<boolean>;
16
16
  isCameraAnalysisEnabled: z.ZodMiniBoolean<boolean>;
17
- youtubeUrl: z.ZodMiniString<string>;
18
- audioMode: z.ZodMiniBoolean<boolean>;
19
- phoneMode: z.ZodMiniBoolean<boolean>;
20
17
  initialHtml: z.ZodMiniString<string>;
21
18
  visualPhotos: z.ZodMiniArray<z.ZodMiniUnknown>;
22
19
  visualVideos: z.ZodMiniArray<z.ZodMiniUnknown>;
20
+ youtubeUrl: z.ZodMiniOptional<z.ZodMiniString<string>>;
21
+ audioMode: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
22
+ phoneMode: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
23
+ forwardLoopMode: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
24
+ imaginativeAiMode: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
25
+ interruptionsEnabled: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
26
+ pauseConversationEnabled: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
27
+ showTranscription: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
28
+ shouldAggregateCurrentTurn: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
29
+ agentPersonaName: z.ZodMiniOptional<z.ZodMiniNullable<z.ZodMiniString<string>>>;
30
+ userName: z.ZodMiniOptional<z.ZodMiniNullable<z.ZodMiniString<string>>>;
31
+ asr: z.ZodMiniOptional<z.ZodMiniObject<{
32
+ provider: z.ZodMiniString<string>;
33
+ language: z.ZodMiniString<string>;
34
+ }, z.core.$strip>>;
23
35
  }, z.core.$strip>;
24
36
  }, z.core.$strip>;
25
37
  export declare const joinCompleteSchema: z.ZodMiniObject<{}, z.core.$strip>;
@@ -1,11 +1,19 @@
1
1
  import { UnisphereLoggerType } from '@unisphere/core';
2
2
  import type { ActiveAvatarSession } from '@unisphere/genie-types';
3
3
  export declare const ACTIVE_SESSION_STORE_ID = "agenticExperienceAvatar";
4
+ export declare const HEARTBEAT_INTERVAL_MS = 2000;
5
+ export declare const HEARTBEAT_STALE_AFTER_MS = 6000;
6
+ export declare const PROBE_TIMEOUT_MS = 1000;
4
7
  /**
5
8
  * Page-wide "who owns the call" record for one avatar manager. Takeover is
6
9
  * just overwriting the shared record: the previous owner sees an id that is
7
10
  * no longer its own and ends its own call (a close callback could not cross
8
11
  * frames).
12
+ *
13
+ * `pagehide` can't be trusted to release (crashed/frozen tabs, an iframe the
14
+ * host removes while the avatar is mid-call), so the owner also refreshes
15
+ * `heartbeatAt` and everyone else ignores a record that stopped refreshing.
16
+ * `verifyConflictOwner()` shortcuts that wait: a removed iframe can't answer a ping.
9
17
  */
10
18
  export declare class ActiveSessionCoordinator {
11
19
  private readonly _options;
@@ -13,10 +21,16 @@ export declare class ActiveSessionCoordinator {
13
21
  private _owns;
14
22
  private readonly _conflict;
15
23
  private readonly _global;
24
+ private readonly _probe;
16
25
  private readonly _unsubscribe;
26
+ private readonly _unsubscribeProbe;
27
+ private readonly _abandoned;
28
+ private _pendingProbe;
17
29
  private readonly _win;
18
30
  private readonly _onPageHide;
19
31
  private readonly _logger?;
32
+ private _heartbeatTimer;
33
+ private _expiryTimer;
20
34
  constructor(_options: {
21
35
  onTakenOver: () => void;
22
36
  logger?: UnisphereLoggerType;
@@ -26,8 +40,20 @@ export declare class ActiveSessionCoordinator {
26
40
  claim(preventDisconnections: boolean): void;
27
41
  /** Republishes the flag only while this avatar owns the call; never claims an idle one */
28
42
  updateFlag(preventDisconnections: boolean): void;
43
+ /**
44
+ * Pings the avatar that owns the call. No answer within `PROBE_TIMEOUT_MS`
45
+ * (removed iframe, crashed tab) marks it abandoned so `conflict` clears
46
+ * instead of waiting for its heartbeat to go stale.
47
+ */
48
+ verifyConflictOwner(): void;
29
49
  /** Clears the record only if it is still ours, so a taken-over avatar can't wipe the new owner */
30
50
  release(): void;
31
51
  dispose(): void;
52
+ private _publish;
53
+ private _startHeartbeat;
54
+ private _stopHeartbeat;
55
+ private _clearPendingProbe;
56
+ private _handleProbe;
57
+ private _clearExpiryTimer;
32
58
  private _handle;
33
59
  }