@embrace-io/web-sdk
Version:
610 lines (609 loc) • 24.6 kB
JavaScript
import { clampNumber } from "../../utils/clampNumber.js";
import { generateUUID } from "../../utils/generateUUID.js";
import { getIncrementedCount } from "../../utils/getIncrementedCount.js";
import { KEY_EMB_COLD_START, KEY_EMB_IS_FINAL_SESSION_PART, KEY_EMB_PAGE_LOAD, KEY_EMB_SDK_STARTUP_DURATION, KEY_EMB_SESSION_PART_END_REASON, KEY_EMB_SESSION_PART_ID, KEY_EMB_SESSION_PART_NUMBER, KEY_EMB_SESSION_PART_START_REASON, KEY_EMB_STATE, KEY_EMB_TYPE, KEY_EMB_USER_SESSION_FOREGROUND_INACTIVITY_TIMEOUT_SECONDS, KEY_EMB_USER_SESSION_ID, KEY_EMB_USER_SESSION_INACTIVITY_TIMEOUT_SECONDS, KEY_EMB_USER_SESSION_MAX_DURATION_SECONDS, KEY_EMB_USER_SESSION_NUMBER, KEY_EMB_USER_SESSION_PART_INDEX, KEY_EMB_USER_SESSION_PREVIOUS_ID, KEY_EMB_USER_SESSION_START_TS, KEY_EMB_USER_SESSION_TERMINATION_REASON, KEY_PREFIX_EMB_PROPERTIES } from "../../constants/attributes.js";
import { getVisibilityState } from "../../utils/getVisibilityState.js";
import { throttle } from "../../utils/throttle.js";
import { EmbraceExtendedSpan } from "../EmbraceTraceManager/EmbraceExtendedSpan.js";
import { DEFAULT_ACTIVITY_EVENTS, DEFAULT_ACTIVITY_THROTTLE_MS, DEFAULT_USER_SESSION_FOREGROUND_INACTIVITY_TIMEOUT_SECONDS, DEFAULT_USER_SESSION_INACTIVITY_TIMEOUT_SECONDS, DEFAULT_USER_SESSION_MAX_DURATION_SECONDS, EMBRACE_LAST_END_USER_SESSION_TS_KEY, EMBRACE_SESSION_PART_NUMBER_KEY, EMBRACE_USER_SESSION_NUMBER_KEY, EMBRACE_USER_SESSION_STATE_KEY, FINAL_SESSION_PART_END_REASONS, MAX_USER_SESSION_FOREGROUND_INACTIVITY_TIMEOUT_SECONDS, MAX_USER_SESSION_INACTIVITY_TIMEOUT_SECONDS, MAX_USER_SESSION_MAX_DURATION_SECONDS, MIN_USER_SESSION_MAX_DURATION_SECONDS, SESSION_PART_SPAN_NAME } from "./constants.js";
import { addActivityListeners, isTabEngaged, removeActivityListeners } from "./utils/activity.js";
import { createUserSessionState, isUserSessionExpired, readPermanentProperties, readUserSessionState, storePermanentProperties } from "./utils/state.js";
import { diag, trace } from "@opentelemetry/api";
import { ATTR_SESSION_ID, ATTR_SESSION_PREVIOUS_ID } from "@opentelemetry/semantic-conventions/incubating";
//#region src/managers/EmbraceUserSessionManager/EmbraceUserSessionManager.ts
/**
* Parts are engagement-gated (visible AND focused), so only one part can
* be active at a time. Engagement exclusivity lets us treat storage as
* a plain shared row read on engagement and written on changes.
*/
var EmbraceUserSessionManager = class {
_state = null;
_previousUserSessionId = null;
_maxDurationTimeout = null;
_permanentProperties = {};
_hasStoredState = false;
_activeSessionPartId = null;
_currentSessionPartNumber = null;
_sessionPartSpan = null;
_activeSessionPartCounts = null;
_coldStart = true;
_nextSessionPartCounts = {};
_sdkStartupDuration = 0;
_sessionPartStartedListeners = [];
_sessionPartEndedListeners = [];
_tracer;
_diag;
_perf;
_storage;
_visibilityDoc;
_limitManager;
_dynamicConfigManager;
_target;
_activityEvents;
_onActivityThrottled;
_sessionPartInactivityTimer = null;
constructor({ dynamicConfigManager, diag: diagParam, perf, visibilityDoc, storage, limitManager, target = window, activityThrottleMs = DEFAULT_ACTIVITY_THROTTLE_MS, activityEvents = DEFAULT_ACTIVITY_EVENTS }) {
this._diag = diagParam ?? diag.createComponentLogger({ namespace: "EmbraceUserSessionManager" });
this._perf = perf;
this._visibilityDoc = visibilityDoc;
this._storage = storage;
this._limitManager = limitManager;
this._dynamicConfigManager = dynamicConfigManager;
this._permanentProperties = readPermanentProperties(this._storage, this._diag);
this._tracer = trace.getTracer("embrace-web-sdk-session-parts");
this._target = target;
this._activityEvents = activityEvents;
this._onActivityThrottled = throttle(this._onActivity, activityThrottleMs);
}
/**
* Reads the user-session durations from remote config, clamps each to its
* allowed range, and enforces inactivity <= max duration. Called once per
* user-session creation; the resolved values are frozen into that session's
* state.
*/
_resolveUserSessionDurations() {
const config = this._dynamicConfigManager.getConfig();
const userSessionMaxDurationSeconds = clampNumber({
diag: this._diag,
value: config.userSessionMaxDurationSeconds,
defaultValue: DEFAULT_USER_SESSION_MAX_DURATION_SECONDS,
min: MIN_USER_SESSION_MAX_DURATION_SECONDS,
max: MAX_USER_SESSION_MAX_DURATION_SECONDS
});
const userSessionInactivityTimeoutSeconds = clampNumber({
diag: this._diag,
value: config.userSessionInactivityTimeoutSeconds,
defaultValue: DEFAULT_USER_SESSION_INACTIVITY_TIMEOUT_SECONDS,
min: 30,
max: MAX_USER_SESSION_INACTIVITY_TIMEOUT_SECONDS
});
const userSessionForegroundInactivityTimeoutSeconds = clampNumber({
diag: this._diag,
value: config.userSessionForegroundInactivityTimeoutSeconds,
defaultValue: DEFAULT_USER_SESSION_FOREGROUND_INACTIVITY_TIMEOUT_SECONDS,
min: 30,
max: MAX_USER_SESSION_FOREGROUND_INACTIVITY_TIMEOUT_SECONDS
});
const inactivityWithinMax = userSessionInactivityTimeoutSeconds <= userSessionMaxDurationSeconds;
const foregroundWithinMax = userSessionForegroundInactivityTimeoutSeconds <= userSessionMaxDurationSeconds;
if (!inactivityWithinMax) this._diag.warn(`userSessionInactivityTimeoutSeconds (${userSessionInactivityTimeoutSeconds.toString()}s) exceeds userSessionMaxDurationSeconds (${userSessionMaxDurationSeconds.toString()}s); falling back to default inactivity timeout (${DEFAULT_USER_SESSION_INACTIVITY_TIMEOUT_SECONDS.toString()}s).`);
if (!foregroundWithinMax) this._diag.warn(`userSessionForegroundInactivityTimeoutSeconds (${userSessionForegroundInactivityTimeoutSeconds.toString()}s) exceeds userSessionMaxDurationSeconds (${userSessionMaxDurationSeconds.toString()}s); falling back to default foreground inactivity timeout (${DEFAULT_USER_SESSION_FOREGROUND_INACTIVITY_TIMEOUT_SECONDS.toString()}s).`);
return {
userSessionMaxDurationSeconds,
userSessionInactivityTimeoutSeconds: inactivityWithinMax ? userSessionInactivityTimeoutSeconds : DEFAULT_USER_SESSION_INACTIVITY_TIMEOUT_SECONDS,
userSessionForegroundInactivityTimeoutSeconds: foregroundWithinMax ? userSessionForegroundInactivityTimeoutSeconds : DEFAULT_USER_SESSION_FOREGROUND_INACTIVITY_TIMEOUT_SECONDS
};
}
setTracerProvider(tracerProvider) {
this._tracer = tracerProvider.getTracer("embrace-web-sdk-session-parts");
addActivityListeners({
target: this._target,
visibilityDoc: this._visibilityDoc,
activityEvents: this._activityEvents,
onActivity: this._onActivityThrottled,
onVisibilityChange: this._onVisibilityChange,
onFocus: this._onFocus,
onBlur: this._onBlur
});
const { state, created } = this._loadOrCreateUserSessionState(this._perf.getNowMillis());
this._state = state;
if (created) this._storeState();
}
recordSDKStartupDuration(duration) {
this._sdkStartupDuration = Math.ceil(duration);
}
getUserSessionId() {
return this._state?.userSessionId ?? null;
}
getPreviousUserSessionId() {
return this._state?.previousUserSessionId ?? this._previousUserSessionId;
}
getUserSessionStartTime() {
return this._state?.userSessionStartTs ?? null;
}
getUserSessionAttributes() {
if (!this._state) return null;
const previousUserSessionId = this.getPreviousUserSessionId() ?? "";
return {
[KEY_EMB_USER_SESSION_ID]: this._state.userSessionId,
[ATTR_SESSION_ID]: this._state.userSessionId,
[KEY_EMB_USER_SESSION_PREVIOUS_ID]: previousUserSessionId,
[ATTR_SESSION_PREVIOUS_ID]: previousUserSessionId,
[KEY_EMB_USER_SESSION_NUMBER]: this._state.userSessionNumber,
[KEY_EMB_USER_SESSION_PART_INDEX]: this._state.userSessionPartIndex,
[KEY_EMB_SESSION_PART_NUMBER]: this._currentSessionPartNumber ?? 0,
[KEY_EMB_USER_SESSION_START_TS]: this._state.userSessionStartTs,
[KEY_EMB_USER_SESSION_MAX_DURATION_SECONDS]: this._state.userSessionMaxDurationSeconds,
[KEY_EMB_USER_SESSION_INACTIVITY_TIMEOUT_SECONDS]: this._state.userSessionInactivityTimeoutSeconds,
[KEY_EMB_USER_SESSION_FOREGROUND_INACTIVITY_TIMEOUT_SECONDS]: this._state.userSessionForegroundInactivityTimeoutSeconds
};
}
endUserSession() {
if (!this._state) {
this._diag.debug("Trying to end user session, but there is no active session. This is a no-op.");
return;
}
const now = this._perf.getNowMillis();
const lastEndRaw = this._storage.getItem(EMBRACE_LAST_END_USER_SESSION_TS_KEY);
const lastEndTs = lastEndRaw === null ? NaN : Number(lastEndRaw);
if (Number.isFinite(lastEndTs) && now - lastEndTs < 5e3) {
this._diag.warn("endUserSession called within cooldown period, ignoring.");
return;
}
this._rolloverUserSession("manual");
this._storage.setItem(EMBRACE_LAST_END_USER_SESSION_TS_KEY, String(now));
}
getSessionPartId() {
return this._activeSessionPartId;
}
getSessionPartSpan() {
return this._sessionPartSpan;
}
startSessionPartInternal({ reason, timestamp }) {
if (this._sessionPartSpan) {
this._diag.warn(`startSessionPartInternal called while a part is active (reason: ${reason}); ignoring`);
return;
}
if (getVisibilityState(this._visibilityDoc) === "background" || !this._visibilityDoc.hasFocus()) {
this._diag.debug(`skipping session part start (reason: ${reason}) because the tab is not engaged`);
return;
}
const activeSessionPartId = generateUUID();
const previouslyRecordedCounts = this._nextSessionPartCounts;
this._beginUserSessionForPartStart();
const attributes = {
...this.getUserSessionAttributes(),
[KEY_EMB_TYPE]: "ux.session_part",
[KEY_EMB_STATE]: "foreground",
[KEY_EMB_SESSION_PART_ID]: activeSessionPartId,
[KEY_EMB_SESSION_PART_START_REASON]: reason,
[KEY_EMB_COLD_START]: this._coldStart,
...previouslyRecordedCounts
};
this._activeSessionPartId = activeSessionPartId;
let span;
try {
span = new EmbraceExtendedSpan(this._tracer.startSpan(SESSION_PART_SPAN_NAME, {
attributes,
startTime: timestamp
}));
} catch (error) {
this._activeSessionPartId = null;
this._diag.error("Error starting session part span", error);
return;
}
this._activeSessionPartCounts = {};
this._nextSessionPartCounts = {};
this._sessionPartSpan = span;
this._startSessionPartInactivityTimer();
this._fireListeners(this._sessionPartStartedListeners, "started", { reason });
}
endSessionPartInternal({ reason, userSessionEndReason, timestamp }) {
if (!this._sessionPartSpan) {
this._diag.debug("trying to end a session part, but there is no session part in progress. This is a no-op.");
return;
}
this._clearSessionPartInactivityTimer();
const isFinalSessionPart = FINAL_SESSION_PART_END_REASONS.has(reason);
const now = this._perf.getNowMillis();
const partEndTs = timestamp ?? now;
const span = this._sessionPartSpan;
try {
const endAttrs = {
[KEY_EMB_SESSION_PART_END_REASON]: reason,
...this._activeSessionPartCounts,
...this._limitManager.getDiagnosticCounts(),
[KEY_EMB_SDK_STARTUP_DURATION]: this._sdkStartupDuration
};
if (this._coldStart) endAttrs[KEY_EMB_PAGE_LOAD] = this._visibilityDoc.readyState === "complete";
if (isFinalSessionPart) {
endAttrs[KEY_EMB_IS_FINAL_SESSION_PART] = 1;
if (userSessionEndReason) endAttrs[KEY_EMB_USER_SESSION_TERMINATION_REASON] = userSessionEndReason;
}
const propertyAttrs = {};
for (const [bareKey, value] of Object.entries(this.getSessionPartProperties())) propertyAttrs[KEY_PREFIX_EMB_PROPERTIES + bareKey] = value;
try {
span.setAttributes({
...propertyAttrs,
...endAttrs
});
} catch (error) {
this._diag.warn("Error setting end attributes on session part span; ending span without them", error);
}
} finally {
this._fireListeners(this._sessionPartEndedListeners, "ended", void 0);
try {
span.end(partEndTs);
} catch (error) {
this._diag.warn("Error ending session part span", error);
}
this._sessionPartSpan = null;
this._coldStart = false;
this._activeSessionPartId = null;
this._activeSessionPartCounts = null;
this._currentSessionPartNumber = null;
this._limitManager.reset();
}
if (isFinalSessionPart) {
this._previousUserSessionId = this._state?.userSessionId ?? null;
this._state = null;
this._storage.removeItem(EMBRACE_USER_SESSION_STATE_KEY);
this._clearMaxDurationTimer();
} else this._continueUserSessionAfterPartEnd(now);
}
addBreadcrumb(name) {
if (!this._sessionPartSpan) {
this._diag.debug("trying to add breadcrumb, but there is no session part in progress. This is a no-op.");
return;
}
const limitedBreadcrumb = this._limitManager.limitBreadcrumb(name);
if (limitedBreadcrumb === "dropped") return;
this._sessionPartSpan.addEvent("emb-breadcrumb", { message: limitedBreadcrumb.name }, this._perf.getNowMillis());
}
addProperty(propertyKey, value, options) {
const limitedSessionProperty = this._limitManager.limitUserSessionProperty(propertyKey, value);
if (limitedSessionProperty === "dropped") return;
const bareKey = limitedSessionProperty.key;
const bareValue = limitedSessionProperty.value;
if (options?.lifespan === "permanent") {
const candidate = {
...this._permanentProperties,
[bareKey]: bareValue
};
if (!storePermanentProperties(this._storage, candidate)) {
this._diag.warn("Storage unavailable; rejecting permanent property write.");
return;
}
this._permanentProperties = candidate;
this._removeFromUserSessionProperties(bareKey);
return;
}
const { state } = this._loadOrCreateUserSessionState(this._perf.getNowMillis());
this._state = {
...state,
userSessionProperties: {
...state.userSessionProperties,
[bareKey]: bareValue
}
};
if (!this._storeState()) {
this._state = state;
this._diag.warn("Storage unavailable; rejecting user-session property write.");
}
}
removeProperty(propertyKey) {
const bareKey = this._limitManager.truncateString("session_property_key", propertyKey);
this._removeFromPermanentProperties(bareKey);
this._removeFromUserSessionProperties(bareKey);
}
/**
* Bare-keyed view of properties for the active part. User-session-scoped
* entries shadow permanent ones, matching the cross-scope flip rule
* enforced in `addProperty`. Callers apply the wire-format prefix at
* stamp time.
*/
getSessionPartProperties() {
return {
...this._permanentProperties,
...this._state?.userSessionProperties ?? {}
};
}
incrSessionPartCountForKey(key) {
if (!this._sessionPartSpan || !this._activeSessionPartCounts) {
this._diag.debug("trying to increment a count for the active session part, but there is no session part in progress. This is a no-op.");
return;
}
this._activeSessionPartCounts[key] = (this._activeSessionPartCounts[key] || 0) + 1;
}
incrNextSessionPartCountForKey(key) {
this._nextSessionPartCounts[key] = (this._nextSessionPartCounts[key] || 0) + 1;
}
addSessionPartStartedListener(listener) {
return this._addListener(this._sessionPartStartedListeners, listener);
}
addSessionPartEndedListener(listener) {
return this._addListener(this._sessionPartEndedListeners, listener);
}
getSessionId() {
return this.getUserSessionId();
}
getPreviousSessionId() {
return null;
}
getSessionStartTime() {
return this.getUserSessionStartTime();
}
endSessionSpan() {
this.endUserSession();
}
startSessionSpan() {}
getSessionSpan() {
return this.getSessionPartSpan();
}
currentSessionAsReadableSpan() {
return null;
}
addSessionStartedListener(_listener) {
return () => {};
}
addSessionEndedListener(_listener) {
return () => {};
}
/**
* Returns the persisted user-session state, rotating to a fresh one if
* the persisted row is missing or expired. Arms the max-duration timer
* when needed. The caller is responsible for assigning the result to
* `_state` and for persisting (the helper does not write to storage).
*/
_loadOrCreateUserSessionState(now) {
let state = readUserSessionState(this._storage, this._diag);
if (!state && !this._hasStoredState && this._state) state = this._state;
let created = false;
if (!state || isUserSessionExpired(state, now)) {
if (state) this._previousUserSessionId = state.userSessionId;
const userSessionNumber = getIncrementedCount(this._storage, EMBRACE_USER_SESSION_NUMBER_KEY, this._diag);
if (!this._coldStart) this._dynamicConfigManager.refreshRemoteConfig();
const { userSessionMaxDurationSeconds, userSessionInactivityTimeoutSeconds, userSessionForegroundInactivityTimeoutSeconds } = this._resolveUserSessionDurations();
state = createUserSessionState({
now,
previousUserSessionId: this._previousUserSessionId,
userSessionMaxDurationSeconds,
userSessionInactivityTimeoutSeconds,
userSessionForegroundInactivityTimeoutSeconds,
userSessionNumber
});
created = true;
}
if (created || this._maxDurationTimeout === null) this._setupMaxDurationTimer(state, now);
return {
state,
created
};
}
/**
* Bumps the part counter and arms the max-duration timer. Relies on
* `_loadOrCreateUserSessionState` to provide a non-null, non-expired state,
* so `getUserSessionAttributes()` is guaranteed non-null after this.
*/
_beginUserSessionForPartStart() {
const now = this._perf.getNowMillis();
const { state } = this._loadOrCreateUserSessionState(now);
this._currentSessionPartNumber = getIncrementedCount(this._storage, EMBRACE_SESSION_PART_NUMBER_KEY, this._diag);
this._state = {
...state,
userSessionPartIndex: state.userSessionPartIndex + 1,
inactivityDeadlineTs: null
};
this._storeState();
}
/**
* Records the inactivity deadline on the user-session row so the next
* part start can detect lazy expiry. The max-duration timer stays valid
* because userSessionMaxEndTs doesn't change between parts.
*/
_continueUserSessionAfterPartEnd(now) {
if (!this._state) {
this._clearMaxDurationTimer();
return;
}
if (this._hasStoredState && this._storage.getItem("embrace_user_session_state") === null) {
this._previousUserSessionId = this._state.userSessionId;
this._state = null;
this._clearMaxDurationTimer();
return;
}
this._state = {
...this._state,
inactivityDeadlineTs: now + this._state.userSessionInactivityTimeoutSeconds * 1e3
};
this._storeState();
}
/**
* Ends the active part as final and starts a fresh part for the next
* user session. State clearing for the engaged path is handled by
* endSessionPartInternal's isFinal branch. When no part is active
* (tab disengaged), the same state-clearing is performed inline so
* the next engagement creates a fresh user session rather than
* resuming the just-ended one.
*/
_rolloverUserSession(userSessionEndReason) {
if (this._sessionPartSpan) {
const boundaryTimestamp = this._perf.getNowMillis();
this.endSessionPartInternal({
reason: "user_session_ended",
userSessionEndReason,
timestamp: boundaryTimestamp
});
this.startSessionPartInternal({
reason: "user_session_rollover",
timestamp: boundaryTimestamp
});
return;
}
this._previousUserSessionId = this._state?.userSessionId ?? null;
this._state = null;
this._storage.removeItem(EMBRACE_USER_SESSION_STATE_KEY);
this._clearMaxDurationTimer();
}
/**
* Ends the active part and starts a new one within the same user session.
* The user session is preserved while the part counters advance, so the
* caller's `endReason`/`startReason` must both be non-final. Both spans share
* one boundary timestamp so they tile without a gap; the caller may supply it
* (for example the timestamp of the triggering event), defaulting to now. A
* no-op when no part is active: there is nothing to roll over and the next
* engagement starts one.
*/
rolloverSessionPartInternal({ endReason, startReason, timestamp }) {
if (!this._sessionPartSpan) return;
const boundaryTimestamp = timestamp ?? this._perf.getNowMillis();
this.endSessionPartInternal({
reason: endReason,
timestamp: boundaryTimestamp
});
this.startSessionPartInternal({
reason: startReason,
timestamp: boundaryTimestamp
});
}
_setupMaxDurationTimer(state, now) {
this._clearMaxDurationTimer();
const remaining = state.userSessionMaxEndTs - now;
if (remaining <= 0) return;
this._maxDurationTimeout = setTimeout(() => {
this._maxDurationTimeout = null;
this._rolloverUserSession("max_duration_reached");
}, remaining);
}
/**
* @internal Would be private but exposed for tests to simulate a page reload
*/
_clearMaxDurationTimer() {
if (this._maxDurationTimeout !== null) {
clearTimeout(this._maxDurationTimeout);
this._maxDurationTimeout = null;
}
}
/**
* Detaches browser-activity listeners and clears the part-inactivity
* timer. Intended for SDK teardown and tests; in production the manager
* lives for the page's lifetime.
*/
_shutdown() {
removeActivityListeners({
target: this._target,
visibilityDoc: this._visibilityDoc,
activityEvents: this._activityEvents,
onActivity: this._onActivityThrottled,
onVisibilityChange: this._onVisibilityChange,
onFocus: this._onFocus,
onBlur: this._onBlur
});
this._clearSessionPartInactivityTimer();
this._clearMaxDurationTimer();
}
_onActivity = () => {
try {
if (!isTabEngaged(this._visibilityDoc)) return;
if (this._activeSessionPartId === null) {
this.startSessionPartInternal({ reason: "web_activity" });
return;
}
this._startSessionPartInactivityTimer();
} catch (e) {
this._diag.warn("Error handling activity event", e);
}
};
_onVisibilityChange = () => {
this._handleEngagementTransition("visibilitychange");
};
_onFocus = () => {
this._handleEngagementTransition("focus");
};
_onBlur = () => {
this._handleEngagementTransition("blur");
};
_handleEngagementTransition(source) {
try {
const engaged = isTabEngaged(this._visibilityDoc);
const active = this._activeSessionPartId !== null;
if (!engaged && active) {
this._diag.debug(`tab disengaged via ${source}; ending current part`);
this.endSessionPartInternal({ reason: "background" });
return;
}
if (engaged && !active) {
this._diag.debug(`tab engaged via ${source}; starting new part`);
this.startSessionPartInternal({ reason: "foreground" });
return;
}
if (engaged && active) this._startSessionPartInactivityTimer();
} catch (e) {
this._diag.warn("Error handling engagement change", e);
}
}
_onSessionPartInactivity = () => {
this._sessionPartInactivityTimer = null;
try {
if (this._activeSessionPartId === null) return;
this._diag.debug("inactivity timer fired; ending current part and user session");
this.endSessionPartInternal({
reason: "web_foreground_inactivity",
userSessionEndReason: "inactivity"
});
} catch (e) {
this._diag.warn("Error handling inactivity timer", e);
}
};
_startSessionPartInactivityTimer() {
this._clearSessionPartInactivityTimer();
if (!this._state) return;
this._sessionPartInactivityTimer = setTimeout(this._onSessionPartInactivity, this._state.userSessionForegroundInactivityTimeoutSeconds * 1e3);
}
_clearSessionPartInactivityTimer() {
if (this._sessionPartInactivityTimer !== null) {
clearTimeout(this._sessionPartInactivityTimer);
this._sessionPartInactivityTimer = null;
}
}
_addListener(list, listener) {
list.push(listener);
return () => {
const i = list.indexOf(listener);
if (i !== -1) list.splice(i, 1);
};
}
_fireListeners(list, kind, arg) {
for (const listener of list) try {
listener(arg);
} catch (error) {
this._diag.warn(`Error while executing session part ${kind} listener`, error);
}
}
_storeState() {
if (!this._state) return false;
const stored = this._storage.setItem(EMBRACE_USER_SESSION_STATE_KEY, JSON.stringify(this._state));
if (stored) this._hasStoredState = true;
return stored;
}
_removeFromPermanentProperties(bareKey) {
if (!(bareKey in this._permanentProperties)) return;
const pruned = { ...this._permanentProperties };
delete pruned[bareKey];
this._permanentProperties = pruned;
storePermanentProperties(this._storage, pruned);
}
_removeFromUserSessionProperties(bareKey) {
if (!this._state || !(bareKey in this._state.userSessionProperties)) return;
const pruned = { ...this._state.userSessionProperties };
delete pruned[bareKey];
this._state = {
...this._state,
userSessionProperties: pruned
};
this._storeState();
}
};
//#endregion
export { EmbraceUserSessionManager };
//# sourceMappingURL=EmbraceUserSessionManager.js.map