@kaltura-sdk/avatars 1.21.0 → 1.21.2
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 +162 -273
- package/package.json +3 -3
- package/src/index.d.ts +0 -1
- package/src/lib/avatar-client-manager/utils/active-session-coordinator.d.ts +26 -0
- package/src/lib/test-utils/create-frame-api.d.ts +0 -6
- package/src/lib/global-tiny-data-store/index.d.ts +0 -1
- package/src/lib/global-tiny-data-store/listen-to-global-tiny-data-store.d.ts +0 -16
package/index.esm.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { UnisphereLoggerFactory, TinyDataStore, TinyPubSub, performanceMonitor,
|
|
1
|
+
import { UnisphereLoggerFactory, TinyDataStore, TinyPubSub, performanceMonitor, getOrCreateGlobalTinyDataStore } from '@unisphere/core';
|
|
2
2
|
import { getPartnerIdFromKs } from '@unisphere/runtime-js';
|
|
3
3
|
import { DisconnectReasons, getAvatarDefaultCapabilities, resolveActiveContextType, AvatarConnectionStatus } from '@unisphere/genie-types';
|
|
4
4
|
import { z as z$1 } from 'zod/mini';
|
|
@@ -1920,18 +1920,6 @@ PERFORMANCE OF THIS SOFTWARE.
|
|
|
1920
1920
|
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
|
|
1921
1921
|
|
|
1922
1922
|
|
|
1923
|
-
function __rest(s, e) {
|
|
1924
|
-
var t = {};
|
|
1925
|
-
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
1926
|
-
t[p] = s[p];
|
|
1927
|
-
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
1928
|
-
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
1929
|
-
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
1930
|
-
t[p[i]] = s[p[i]];
|
|
1931
|
-
}
|
|
1932
|
-
return t;
|
|
1933
|
-
}
|
|
1934
|
-
|
|
1935
1923
|
function __awaiter(thisArg, _arguments, P, generator) {
|
|
1936
1924
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
1937
1925
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
@@ -23840,240 +23828,12 @@ const avatarConnectionMachine = setup({
|
|
|
23840
23828
|
}
|
|
23841
23829
|
});
|
|
23842
23830
|
|
|
23843
|
-
const NAMESPACE = 'unisphere-global-tiny-data-store';
|
|
23844
|
-
const REGISTRY_KEY = 'globalTinyDataStore';
|
|
23845
|
-
function getRegistry(logger) {
|
|
23846
|
-
return getOrCreateGlobalUnisphere(REGISTRY_KEY, logger, {
|
|
23847
|
-
windowId: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`,
|
|
23848
|
-
// null prototype: ids come off the wire, `__proto__`/`constructor` must be plain keys
|
|
23849
|
-
entries: Object.create(null),
|
|
23850
|
-
listening: false,
|
|
23851
|
-
lastTs: 0
|
|
23852
|
-
}).value;
|
|
23853
|
-
}
|
|
23854
|
-
// `length`/`frames` are readable cross-origin, so this reaches every frame
|
|
23855
|
-
// regardless of nesting depth or origin; we can't know which one holds an avatar.
|
|
23856
|
-
function collectWindows(root) {
|
|
23857
|
-
const result = [];
|
|
23858
|
-
const visit = w => {
|
|
23859
|
-
result.push(w);
|
|
23860
|
-
let count = 0;
|
|
23861
|
-
try {
|
|
23862
|
-
count = w.length;
|
|
23863
|
-
} catch (_a) {
|
|
23864
|
-
return;
|
|
23865
|
-
}
|
|
23866
|
-
for (let i = 0; i < count; i++) {
|
|
23867
|
-
try {
|
|
23868
|
-
visit(w.frames[i]);
|
|
23869
|
-
} catch (_b) {
|
|
23870
|
-
// frame navigated away or became inaccessible while enumerating
|
|
23871
|
-
}
|
|
23872
|
-
}
|
|
23873
|
-
};
|
|
23874
|
-
visit(root);
|
|
23875
|
-
return result;
|
|
23876
|
-
}
|
|
23877
|
-
// Cache frames seen by this window so a `pagehide` message dispatched by an iframe
|
|
23878
|
-
// just before removal is still recognized even after `contentWindow` detaches from the DOM.
|
|
23879
|
-
const knownFrames = new WeakSet();
|
|
23880
|
-
function getPageWindows(win) {
|
|
23881
|
-
var _a;
|
|
23882
|
-
const current = collectWindows((_a = win.top) !== null && _a !== void 0 ? _a : win);
|
|
23883
|
-
for (const w of current) {
|
|
23884
|
-
knownFrames.add(w);
|
|
23885
|
-
}
|
|
23886
|
-
return current;
|
|
23887
|
-
}
|
|
23888
|
-
function isKnownPageWindow(win, source) {
|
|
23889
|
-
if (getPageWindows(win).includes(source)) {
|
|
23890
|
-
return true;
|
|
23891
|
-
}
|
|
23892
|
-
return knownFrames.has(source);
|
|
23893
|
-
}
|
|
23894
|
-
function post(target, message) {
|
|
23895
|
-
try {
|
|
23896
|
-
// '*' because the host topology (and origins) are unknown. The payload is
|
|
23897
|
-
// non-sensitive and receivers only accept messages from this page's frames.
|
|
23898
|
-
target.postMessage(message, '*');
|
|
23899
|
-
} catch (_a) {
|
|
23900
|
-
// target is gone
|
|
23901
|
-
}
|
|
23902
|
-
}
|
|
23903
|
-
function broadcast(win, message) {
|
|
23904
|
-
for (const target of getPageWindows(win)) {
|
|
23905
|
-
if (target !== win) {
|
|
23906
|
-
post(target, message);
|
|
23907
|
-
}
|
|
23908
|
-
}
|
|
23909
|
-
}
|
|
23910
|
-
function compareRevisions(a, b) {
|
|
23911
|
-
if (a.ts !== b.ts) {
|
|
23912
|
-
return a.ts - b.ts;
|
|
23913
|
-
}
|
|
23914
|
-
return a.origin < b.origin ? -1 : a.origin > b.origin ? 1 : 0;
|
|
23915
|
-
}
|
|
23916
|
-
function isRevision(rev) {
|
|
23917
|
-
return !!rev && typeof rev.ts === 'number' && typeof rev.origin === 'string';
|
|
23918
|
-
}
|
|
23919
|
-
function handleMessage(win, registry, logger, event) {
|
|
23920
|
-
const data = event.data;
|
|
23921
|
-
if (!data || data.ns !== NAMESPACE || typeof data.id !== 'string') {
|
|
23922
|
-
return;
|
|
23923
|
-
}
|
|
23924
|
-
const source = event.source;
|
|
23925
|
-
if (!source || source === win || !isKnownPageWindow(win, source)) {
|
|
23926
|
-
logger.debug('ignored message from a window outside this page', {
|
|
23927
|
-
data: {
|
|
23928
|
-
id: data.id,
|
|
23929
|
-
kind: data.kind
|
|
23930
|
-
}
|
|
23931
|
-
});
|
|
23932
|
-
return;
|
|
23933
|
-
}
|
|
23934
|
-
const entry = registry.entries[data.id];
|
|
23935
|
-
if (data.kind === 'request-sync') {
|
|
23936
|
-
if (entry === null || entry === void 0 ? void 0 : entry.rev) {
|
|
23937
|
-
logger.debug('answering sync request', {
|
|
23938
|
-
data: {
|
|
23939
|
-
id: data.id
|
|
23940
|
-
}
|
|
23941
|
-
});
|
|
23942
|
-
post(source, {
|
|
23943
|
-
ns: NAMESPACE,
|
|
23944
|
-
from: registry.windowId,
|
|
23945
|
-
kind: 'update',
|
|
23946
|
-
id: data.id,
|
|
23947
|
-
value: entry.store.getData(),
|
|
23948
|
-
rev: entry.rev
|
|
23949
|
-
});
|
|
23950
|
-
}
|
|
23951
|
-
return;
|
|
23952
|
-
}
|
|
23953
|
-
if (data.kind !== 'update' || !isRevision(data.rev)) {
|
|
23954
|
-
return;
|
|
23955
|
-
}
|
|
23956
|
-
// Lamport-style: a write made after observing this one must sort after it,
|
|
23957
|
-
// even within the same millisecond.
|
|
23958
|
-
registry.lastTs = Math.max(registry.lastTs, data.rev.ts);
|
|
23959
|
-
if (!entry) {
|
|
23960
|
-
logger.debug('stored update for an id nobody here listens to yet', {
|
|
23961
|
-
data: {
|
|
23962
|
-
id: data.id,
|
|
23963
|
-
rev: data.rev
|
|
23964
|
-
}
|
|
23965
|
-
});
|
|
23966
|
-
registry.entries[data.id] = {
|
|
23967
|
-
store: new TinyDataStore({
|
|
23968
|
-
initialValue: data.value,
|
|
23969
|
-
compare: 'deep'
|
|
23970
|
-
}),
|
|
23971
|
-
rev: data.rev
|
|
23972
|
-
};
|
|
23973
|
-
return;
|
|
23974
|
-
}
|
|
23975
|
-
// Equal/older revisions are what stops echoes between frames.
|
|
23976
|
-
if (entry.rev && compareRevisions(data.rev, entry.rev) <= 0) {
|
|
23977
|
-
logger.debug('dropped stale or duplicate update', {
|
|
23978
|
-
data: {
|
|
23979
|
-
id: data.id,
|
|
23980
|
-
rev: data.rev
|
|
23981
|
-
}
|
|
23982
|
-
});
|
|
23983
|
-
return;
|
|
23984
|
-
}
|
|
23985
|
-
logger.debug('applied update from another frame', {
|
|
23986
|
-
data: {
|
|
23987
|
-
id: data.id,
|
|
23988
|
-
value: data.value,
|
|
23989
|
-
rev: data.rev
|
|
23990
|
-
}
|
|
23991
|
-
});
|
|
23992
|
-
entry.rev = data.rev;
|
|
23993
|
-
entry.store.setData(() => data.value);
|
|
23994
|
-
}
|
|
23995
|
-
// The listener is installed once per window by whichever bundle listens first,
|
|
23996
|
-
// so it keeps that bundle's logger.
|
|
23997
|
-
function ensureListener(win, registry, logger) {
|
|
23998
|
-
if (registry.listening) {
|
|
23999
|
-
return;
|
|
24000
|
-
}
|
|
24001
|
-
registry.listening = true;
|
|
24002
|
-
win.addEventListener('message', event => handleMessage(win, registry, logger, event));
|
|
24003
|
-
}
|
|
24004
|
-
/**
|
|
24005
|
-
* A `TinyDataStore` shared by id between every workspace in this window and,
|
|
24006
|
-
* via `postMessage`, every other frame on the page (parent, siblings, nested).
|
|
24007
|
-
* Last write wins (timestamp, then window id as tie-break).
|
|
24008
|
-
*/
|
|
24009
|
-
function listenToGlobalTinyDataStore({
|
|
24010
|
-
id,
|
|
24011
|
-
initialValue,
|
|
24012
|
-
onChanges,
|
|
24013
|
-
logger = avatarClientLoggerFactory.getOrCreateLogger('service', 'global-tiny-data-store')
|
|
24014
|
-
}) {
|
|
24015
|
-
const win = window;
|
|
24016
|
-
const registry = getRegistry(logger);
|
|
24017
|
-
ensureListener(win, registry, logger);
|
|
24018
|
-
let entry = registry.entries[id];
|
|
24019
|
-
const isNew = !entry;
|
|
24020
|
-
if (!entry) {
|
|
24021
|
-
entry = registry.entries[id] = {
|
|
24022
|
-
store: new TinyDataStore({
|
|
24023
|
-
initialValue,
|
|
24024
|
-
compare: 'deep'
|
|
24025
|
-
}),
|
|
24026
|
-
rev: null
|
|
24027
|
-
};
|
|
24028
|
-
}
|
|
24029
|
-
const store = entry.store;
|
|
24030
|
-
const unsubscribe = store.onChanges(onChanges, {
|
|
24031
|
-
replayLastValue: false
|
|
24032
|
-
});
|
|
24033
|
-
logger.debug('listening to global store', {
|
|
24034
|
-
data: {
|
|
24035
|
-
id,
|
|
24036
|
-
isNew
|
|
24037
|
-
}
|
|
24038
|
-
});
|
|
24039
|
-
if (isNew) {
|
|
24040
|
-
broadcast(win, {
|
|
24041
|
-
ns: NAMESPACE,
|
|
24042
|
-
from: registry.windowId,
|
|
24043
|
-
kind: 'request-sync',
|
|
24044
|
-
id
|
|
24045
|
-
});
|
|
24046
|
-
}
|
|
24047
|
-
return {
|
|
24048
|
-
unsubscribe,
|
|
24049
|
-
getData: () => store.getData(),
|
|
24050
|
-
setData: value => {
|
|
24051
|
-
const rev = {
|
|
24052
|
-
ts: registry.lastTs = Math.max(Date.now(), registry.lastTs + 1),
|
|
24053
|
-
origin: registry.windowId
|
|
24054
|
-
};
|
|
24055
|
-
logger.debug('updating global store', {
|
|
24056
|
-
data: {
|
|
24057
|
-
id,
|
|
24058
|
-
value,
|
|
24059
|
-
rev
|
|
24060
|
-
}
|
|
24061
|
-
});
|
|
24062
|
-
entry.rev = rev;
|
|
24063
|
-
store.setData(() => value);
|
|
24064
|
-
broadcast(win, {
|
|
24065
|
-
ns: NAMESPACE,
|
|
24066
|
-
from: registry.windowId,
|
|
24067
|
-
kind: 'update',
|
|
24068
|
-
id,
|
|
24069
|
-
value,
|
|
24070
|
-
rev
|
|
24071
|
-
});
|
|
24072
|
-
}
|
|
24073
|
-
};
|
|
24074
|
-
}
|
|
24075
|
-
|
|
24076
23831
|
const ACTIVE_SESSION_STORE_ID = 'agenticExperienceAvatar';
|
|
23832
|
+
const PROBE_STORE_ID = 'agenticExperienceAvatarProbe';
|
|
23833
|
+
const HEARTBEAT_INTERVAL_MS = 2000;
|
|
23834
|
+
const HEARTBEAT_STALE_AFTER_MS = 6000;
|
|
23835
|
+
// A live owner answers within one postMessage round trip; this only has to outlast a busy main thread.
|
|
23836
|
+
const PROBE_TIMEOUT_MS = 1000;
|
|
24077
23837
|
function createId() {
|
|
24078
23838
|
return typeof crypto !== 'undefined' && crypto.randomUUID ? crypto.randomUUID() : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
24079
23839
|
}
|
|
@@ -24082,32 +23842,44 @@ function createId() {
|
|
|
24082
23842
|
* just overwriting the shared record: the previous owner sees an id that is
|
|
24083
23843
|
* no longer its own and ends its own call (a close callback could not cross
|
|
24084
23844
|
* frames).
|
|
23845
|
+
*
|
|
23846
|
+
* `pagehide` can't be trusted to release (crashed/frozen tabs, an iframe the
|
|
23847
|
+
* host removes while the avatar is mid-call), so the owner also refreshes
|
|
23848
|
+
* `heartbeatAt` and everyone else ignores a record that stopped refreshing.
|
|
23849
|
+
* `verifyConflictOwner()` shortcuts that wait: a removed iframe can't answer a ping.
|
|
24085
23850
|
*/
|
|
24086
23851
|
class ActiveSessionCoordinator {
|
|
24087
23852
|
constructor(_options) {
|
|
24088
23853
|
this._options = _options;
|
|
24089
23854
|
this.id = createId();
|
|
24090
23855
|
this._owns = false;
|
|
23856
|
+
// Heartbeats change every record, so only `id`/`preventDisconnections` count as a change.
|
|
24091
23857
|
this._conflict = new TinyDataStore({
|
|
24092
23858
|
initialValue: null,
|
|
24093
|
-
compare:
|
|
23859
|
+
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)
|
|
24094
23860
|
});
|
|
23861
|
+
// Owners that failed a probe, keyed to the heartbeat they were last seen with; a newer heartbeat revives them.
|
|
23862
|
+
this._abandoned = new Map();
|
|
23863
|
+
this._pendingProbe = null;
|
|
24095
23864
|
this._win = window;
|
|
24096
23865
|
this._onPageHide = () => this.release();
|
|
24097
|
-
|
|
24098
|
-
|
|
24099
|
-
initialValue: null,
|
|
24100
|
-
onChanges: value => this._handle(value),
|
|
24101
|
-
logger: _options.logger
|
|
24102
|
-
}),
|
|
24103
|
-
{
|
|
24104
|
-
unsubscribe
|
|
24105
|
-
} = _a,
|
|
24106
|
-
global = __rest(_a, ["unsubscribe"]);
|
|
23866
|
+
this._heartbeatTimer = null;
|
|
23867
|
+
this._expiryTimer = null;
|
|
24107
23868
|
this._logger = _options.logger;
|
|
24108
|
-
this._global =
|
|
24109
|
-
|
|
24110
|
-
|
|
23869
|
+
this._global = getOrCreateGlobalTinyDataStore({
|
|
23870
|
+
id: ACTIVE_SESSION_STORE_ID,
|
|
23871
|
+
initialValue: null,
|
|
23872
|
+
logger: _options.logger
|
|
23873
|
+
});
|
|
23874
|
+
this._unsubscribe = this._global.onChanges(value => this._handle(value));
|
|
23875
|
+
this._probe = getOrCreateGlobalTinyDataStore({
|
|
23876
|
+
id: PROBE_STORE_ID,
|
|
23877
|
+
initialValue: null,
|
|
23878
|
+
logger: _options.logger
|
|
23879
|
+
});
|
|
23880
|
+
this._unsubscribeProbe = this._probe.onChanges(probe => this._handleProbe(probe), {
|
|
23881
|
+
replayLastValue: false
|
|
23882
|
+
});
|
|
24111
23883
|
// A closed iframe must not leave the page locked.
|
|
24112
23884
|
this._win.addEventListener('pagehide', this._onPageHide);
|
|
24113
23885
|
}
|
|
@@ -24127,46 +23899,148 @@ class ActiveSessionCoordinator {
|
|
|
24127
23899
|
preventDisconnections
|
|
24128
23900
|
}
|
|
24129
23901
|
});
|
|
24130
|
-
this.
|
|
24131
|
-
|
|
24132
|
-
preventDisconnections
|
|
24133
|
-
});
|
|
23902
|
+
this._publish(preventDisconnections);
|
|
23903
|
+
this._startHeartbeat();
|
|
24134
23904
|
}
|
|
24135
23905
|
/** Republishes the flag only while this avatar owns the call; never claims an idle one */
|
|
24136
23906
|
updateFlag(preventDisconnections) {
|
|
24137
23907
|
var _a, _b;
|
|
24138
23908
|
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) {
|
|
24139
|
-
this.
|
|
24140
|
-
|
|
24141
|
-
|
|
24142
|
-
|
|
23909
|
+
this._publish(preventDisconnections);
|
|
23910
|
+
}
|
|
23911
|
+
}
|
|
23912
|
+
/**
|
|
23913
|
+
* Pings the avatar that owns the call. No answer within `PROBE_TIMEOUT_MS`
|
|
23914
|
+
* (removed iframe, crashed tab) marks it abandoned so `conflict` clears
|
|
23915
|
+
* instead of waiting for its heartbeat to go stale.
|
|
23916
|
+
*/
|
|
23917
|
+
verifyConflictOwner() {
|
|
23918
|
+
const record = this._global.getData();
|
|
23919
|
+
if (!record || record.id === this.id || this._pendingProbe) {
|
|
23920
|
+
return;
|
|
24143
23921
|
}
|
|
23922
|
+
const nonce = createId();
|
|
23923
|
+
const timer = setTimeout(() => {
|
|
23924
|
+
var _a;
|
|
23925
|
+
this._pendingProbe = null;
|
|
23926
|
+
const current = this._global.getData();
|
|
23927
|
+
// A newer heartbeat in the meantime proves the owner is alive.
|
|
23928
|
+
if ((current === null || current === void 0 ? void 0 : current.id) === record.id && current.heartbeatAt === record.heartbeatAt) {
|
|
23929
|
+
(_a = this._logger) === null || _a === void 0 ? void 0 : _a.log('active avatar session owner did not answer, treating it as abandoned', {
|
|
23930
|
+
data: {
|
|
23931
|
+
id: record.id
|
|
23932
|
+
}
|
|
23933
|
+
});
|
|
23934
|
+
this._abandoned.set(record.id, record.heartbeatAt);
|
|
23935
|
+
this._handle(current);
|
|
23936
|
+
}
|
|
23937
|
+
}, PROBE_TIMEOUT_MS);
|
|
23938
|
+
this._pendingProbe = {
|
|
23939
|
+
nonce,
|
|
23940
|
+
timer
|
|
23941
|
+
};
|
|
23942
|
+
this._probe.setData(() => ({
|
|
23943
|
+
nonce,
|
|
23944
|
+
ownerId: record.id,
|
|
23945
|
+
reply: false
|
|
23946
|
+
}));
|
|
24144
23947
|
}
|
|
24145
23948
|
/** Clears the record only if it is still ours, so a taken-over avatar can't wipe the new owner */
|
|
24146
23949
|
release() {
|
|
24147
23950
|
var _a, _b;
|
|
24148
23951
|
this._owns = false;
|
|
23952
|
+
this._stopHeartbeat();
|
|
24149
23953
|
if (((_a = this._global.getData()) === null || _a === void 0 ? void 0 : _a.id) === this.id) {
|
|
24150
23954
|
(_b = this._logger) === null || _b === void 0 ? void 0 : _b.log('releasing the active avatar session', {
|
|
24151
23955
|
data: {
|
|
24152
23956
|
id: this.id
|
|
24153
23957
|
}
|
|
24154
23958
|
});
|
|
24155
|
-
this._global.setData(null);
|
|
23959
|
+
this._global.setData(() => null);
|
|
24156
23960
|
}
|
|
24157
23961
|
}
|
|
24158
23962
|
dispose() {
|
|
24159
23963
|
this.release();
|
|
24160
23964
|
this._unsubscribe();
|
|
23965
|
+
this._unsubscribeProbe();
|
|
23966
|
+
this._clearExpiryTimer();
|
|
23967
|
+
this._clearPendingProbe();
|
|
24161
23968
|
this._win.removeEventListener('pagehide', this._onPageHide);
|
|
24162
23969
|
}
|
|
24163
|
-
|
|
23970
|
+
_publish(preventDisconnections) {
|
|
23971
|
+
this._global.setData(() => ({
|
|
23972
|
+
id: this.id,
|
|
23973
|
+
preventDisconnections,
|
|
23974
|
+
heartbeatAt: Date.now()
|
|
23975
|
+
}));
|
|
23976
|
+
}
|
|
23977
|
+
_startHeartbeat() {
|
|
23978
|
+
if (this._heartbeatTimer) {
|
|
23979
|
+
return;
|
|
23980
|
+
}
|
|
23981
|
+
this._heartbeatTimer = setInterval(() => {
|
|
23982
|
+
const current = this._global.getData();
|
|
23983
|
+
if (this._owns && (current === null || current === void 0 ? void 0 : current.id) === this.id) {
|
|
23984
|
+
this._publish(current.preventDisconnections);
|
|
23985
|
+
}
|
|
23986
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
23987
|
+
}
|
|
23988
|
+
_stopHeartbeat() {
|
|
23989
|
+
if (this._heartbeatTimer) {
|
|
23990
|
+
clearInterval(this._heartbeatTimer);
|
|
23991
|
+
this._heartbeatTimer = null;
|
|
23992
|
+
}
|
|
23993
|
+
}
|
|
23994
|
+
_clearPendingProbe() {
|
|
23995
|
+
if (this._pendingProbe) {
|
|
23996
|
+
clearTimeout(this._pendingProbe.timer);
|
|
23997
|
+
this._pendingProbe = null;
|
|
23998
|
+
}
|
|
23999
|
+
}
|
|
24000
|
+
_handleProbe(probe) {
|
|
24164
24001
|
var _a;
|
|
24165
|
-
|
|
24002
|
+
if (!probe) {
|
|
24003
|
+
return;
|
|
24004
|
+
}
|
|
24005
|
+
if (!probe.reply && probe.ownerId === this.id && this._owns) {
|
|
24006
|
+
this._probe.setData(() => Object.assign(Object.assign({}, probe), {
|
|
24007
|
+
reply: true
|
|
24008
|
+
}));
|
|
24009
|
+
} else if (probe.reply && probe.nonce === ((_a = this._pendingProbe) === null || _a === void 0 ? void 0 : _a.nonce)) {
|
|
24010
|
+
this._clearPendingProbe();
|
|
24011
|
+
}
|
|
24012
|
+
}
|
|
24013
|
+
_clearExpiryTimer() {
|
|
24014
|
+
if (this._expiryTimer) {
|
|
24015
|
+
clearTimeout(this._expiryTimer);
|
|
24016
|
+
this._expiryTimer = null;
|
|
24017
|
+
}
|
|
24018
|
+
}
|
|
24019
|
+
_handle(value) {
|
|
24020
|
+
var _a, _b, _c;
|
|
24021
|
+
this._clearExpiryTimer();
|
|
24022
|
+
// A record without `heartbeatAt` (older bundle in another frame) never expires.
|
|
24023
|
+
const staleIn = value && typeof value.heartbeatAt === 'number' ? value.heartbeatAt + HEARTBEAT_STALE_AFTER_MS - Date.now() : Infinity;
|
|
24024
|
+
const isAbandoned = !!value && value.heartbeatAt !== undefined && value.heartbeatAt <= ((_a = this._abandoned.get(value.id)) !== null && _a !== void 0 ? _a : -Infinity);
|
|
24025
|
+
const isStale = !!value && (staleIn < 0 || isAbandoned);
|
|
24026
|
+
if (isStale && value.id !== this.id) {
|
|
24027
|
+
(_b = this._logger) === null || _b === void 0 ? void 0 : _b.log('ignoring an abandoned active avatar session', {
|
|
24028
|
+
data: {
|
|
24029
|
+
id: value.id,
|
|
24030
|
+
heartbeatAt: value.heartbeatAt
|
|
24031
|
+
}
|
|
24032
|
+
});
|
|
24033
|
+
}
|
|
24034
|
+
const other = value && value.id !== this.id && !isStale ? value : null;
|
|
24166
24035
|
this._conflict.setData(() => other);
|
|
24036
|
+
if (other && Number.isFinite(staleIn)) {
|
|
24037
|
+
// Nothing is written when the owner dies, so re-evaluate once its record would go stale.
|
|
24038
|
+
this._expiryTimer = setTimeout(() => this._handle(this._global.getData()), staleIn + 1);
|
|
24039
|
+
}
|
|
24167
24040
|
if (other && this._owns) {
|
|
24168
24041
|
this._owns = false;
|
|
24169
|
-
|
|
24042
|
+
this._stopHeartbeat();
|
|
24043
|
+
(_c = this._logger) === null || _c === void 0 ? void 0 : _c.log('active avatar session taken over by another avatar', {
|
|
24170
24044
|
data: {
|
|
24171
24045
|
id: this.id,
|
|
24172
24046
|
by: other.id
|
|
@@ -24489,6 +24363,19 @@ class AvatarClientManager extends ActorManager {
|
|
|
24489
24363
|
preventDisconnections: (_b = (_a = this._config.runtimeSettings.multipleAvatarBehavior) === null || _a === void 0 ? void 0 : _a.preventDisconnections) !== null && _b !== void 0 ? _b : false
|
|
24490
24364
|
};
|
|
24491
24365
|
});
|
|
24366
|
+
// The call that held our connect is gone (ended, abandoned): the user already asked to start, so just start.
|
|
24367
|
+
this._activeSession.conflict.onChanges(conflict => {
|
|
24368
|
+
if (conflict || !this._isTakeoverPending.getData()) {
|
|
24369
|
+
return;
|
|
24370
|
+
}
|
|
24371
|
+
const options = this._pendingTakeoverOptions;
|
|
24372
|
+
this._pendingTakeoverOptions = undefined;
|
|
24373
|
+
this._isTakeoverPending.setData(() => false);
|
|
24374
|
+
this._logger.log('Resuming the held connect: the other avatar call is gone');
|
|
24375
|
+
this.connect(options);
|
|
24376
|
+
}, {
|
|
24377
|
+
replayLastValue: false
|
|
24378
|
+
});
|
|
24492
24379
|
this._status.onChanges(status => {
|
|
24493
24380
|
if (status === AvatarConnectionStatus.NotConnected || status === AvatarConnectionStatus.Disconnected || status === AvatarConnectionStatus.Error) {
|
|
24494
24381
|
this._activeSession.release();
|
|
@@ -24711,6 +24598,8 @@ class AvatarClientManager extends ActorManager {
|
|
|
24711
24598
|
});
|
|
24712
24599
|
this._pendingTakeoverOptions = options;
|
|
24713
24600
|
this._isTakeoverPending.setData(() => true);
|
|
24601
|
+
// An owner that is already gone (removed iframe, crashed tab) can't answer, so don't make the user wait out its heartbeat.
|
|
24602
|
+
this._activeSession.verifyConflictOwner();
|
|
24714
24603
|
return;
|
|
24715
24604
|
}
|
|
24716
24605
|
this._activeSession.claim(this._multipleAvatarBehavior.getData().preventDisconnections);
|
|
@@ -25086,4 +24975,4 @@ class AvatarClientManager extends ActorManager {
|
|
|
25086
24975
|
}
|
|
25087
24976
|
}
|
|
25088
24977
|
|
|
25089
|
-
export { ApplicationFeatures, AvatarClientManager, AvatarConnectionMachineEventTypes, AvatarConnectionMachineId, AvatarNotificationCode, ButtonNames, PageNames, agentRawTextClientToolDeltaSchema, agentRawTextDeltaSchema, agentRawTextSchema, agentTurnToTalkSchema, askPermissionsSchema, avatarClientLoggerFactory, avatarConnectionMachine, avatarRTCClientManager, clientConfigurationSchema, conversationEndedSchema, conversationTimeWarningSchema, createTinyPubsubEventsHandler, createTinyPubsubEventsSource, generatingSpeechSchema, getAvatarDisconnectNotificationVariant, getQueryParam, initializeLogsCollectionIfNeeded, isMappedAvatarDisconnectReason, isMobileDevice, joinCompleteSchema,
|
|
24978
|
+
export { ApplicationFeatures, AvatarClientManager, AvatarConnectionMachineEventTypes, AvatarConnectionMachineId, AvatarNotificationCode, ButtonNames, PageNames, agentRawTextClientToolDeltaSchema, agentRawTextDeltaSchema, agentRawTextSchema, agentTurnToTalkSchema, askPermissionsSchema, avatarClientLoggerFactory, avatarConnectionMachine, avatarRTCClientManager, clientConfigurationSchema, conversationEndedSchema, conversationTimeWarningSchema, createTinyPubsubEventsHandler, createTinyPubsubEventsSource, generatingSpeechSchema, getAvatarDisconnectNotificationVariant, getQueryParam, initializeLogsCollectionIfNeeded, isMappedAvatarDisconnectReason, isMobileDevice, joinCompleteSchema, noAgentSchema, onServerConnectedSchema, pauseExpiryWarningSchema, resetWebsocketClientTeardownState, showAgentSchema, showStartSpeechSchema, stvFinishedTalkingSchema, stvNewSessionSchema, stvSpeechChunkSchema, stvStartedTalkingSchema, timingTracker, unsupportedClientSchema, websocketClientManager, websocketDebugInfo, websocketMessage, websocketNonRecoveredReconnect, websocketRecoveryState, websocketSystemMessage };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaltura-sdk/avatars",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.2",
|
|
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.
|
|
16
|
-
"@unisphere/genie-types": "^1.47.
|
|
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",
|
package/src/index.d.ts
CHANGED
|
@@ -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
|
}
|
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
import type { ActiveSessionCoordinator } from '../avatar-client-manager/utils/active-session-coordinator';
|
|
2
|
-
import type { listenToGlobalTinyDataStore } from '../global-tiny-data-store';
|
|
3
2
|
type CoordinatorOptions = ConstructorParameters<typeof ActiveSessionCoordinator>[0];
|
|
4
3
|
export declare function getRealTopWindow(): Window;
|
|
5
4
|
export declare function addFrame(parent: Window): Window;
|
|
6
5
|
export declare function installBrowserLikePostMessage(windows: Window[]): void;
|
|
7
6
|
export declare function createFrameApi(win: Window): Promise<{
|
|
8
|
-
listen: (options: Parameters<typeof listenToGlobalTinyDataStore>[0]) => {
|
|
9
|
-
unsubscribe: () => void;
|
|
10
|
-
getData: () => unknown;
|
|
11
|
-
setData: (value: unknown) => void;
|
|
12
|
-
};
|
|
13
7
|
createCoordinator: (options: CoordinatorOptions) => ActiveSessionCoordinator;
|
|
14
8
|
cleanup: () => void;
|
|
15
9
|
}>;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './listen-to-global-tiny-data-store';
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { UnisphereLoggerType } from '@unisphere/core';
|
|
2
|
-
/**
|
|
3
|
-
* A `TinyDataStore` shared by id between every workspace in this window and,
|
|
4
|
-
* via `postMessage`, every other frame on the page (parent, siblings, nested).
|
|
5
|
-
* Last write wins (timestamp, then window id as tie-break).
|
|
6
|
-
*/
|
|
7
|
-
export declare function listenToGlobalTinyDataStore<T>({ id, initialValue, onChanges, logger, }: {
|
|
8
|
-
id: string;
|
|
9
|
-
initialValue: T;
|
|
10
|
-
onChanges: (value: T) => void;
|
|
11
|
-
logger?: UnisphereLoggerType;
|
|
12
|
-
}): {
|
|
13
|
-
unsubscribe: () => void;
|
|
14
|
-
getData: () => T;
|
|
15
|
-
setData: (value: T) => void;
|
|
16
|
-
};
|