@bitrix24/b24jssdk
Version:
Bitrix24 REST API JavaScript SDK
210 lines (208 loc) • 7.53 kB
JavaScript
/**
* @package @bitrix24/b24jssdk
* @version 3.0.0
* @copyright (c) 2026 Bitrix24
* @license MIT
* @see https://github.com/bitrix24/b24jssdk
* @see https://bitrix24.github.io/b24jssdk/
*/
var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
const FRAME_REFRESH_MARGIN_MS = 5 * 6e4;
const FRAME_PULSE_MIN_MS = 3e4;
const FRAME_PULSE_MAX_MS = 10 * 6e4;
function resolveKeepAuthFreshParams(params) {
const pick = /* @__PURE__ */ __name((value, fallback) => {
return Number.isFinite(value) && value > 0 ? value : fallback;
}, "pick");
const marginMs = pick(params?.marginMs, FRAME_REFRESH_MARGIN_MS);
const minDelayMs = Math.max(FRAME_PULSE_MIN_MS, pick(params?.minDelayMs, FRAME_PULSE_MIN_MS));
const maxDelayMs = pick(params?.maxDelayMs, FRAME_PULSE_MAX_MS);
return {
marginMs,
minDelayMs,
// A caller that inverts the bounds gets the wider of the two as the
// ceiling rather than a `Math.min(max, Math.max(min, …))` that silently
// collapses to `min` for every input.
maxDelayMs: Math.max(minDelayMs, maxDelayMs)
};
}
__name(resolveKeepAuthFreshParams, "resolveKeepAuthFreshParams");
function frameTokenDue(expiresSec, nowMs, params) {
if (!Number.isFinite(expiresSec) || expiresSec <= 0) {
return true;
}
const { marginMs } = resolveKeepAuthFreshParams(params);
return expiresSec * 1e3 - nowMs <= marginMs;
}
__name(frameTokenDue, "frameTokenDue");
function frameTokenDelayMs(expiresSec, nowMs, params) {
const { marginMs, minDelayMs, maxDelayMs } = resolveKeepAuthFreshParams(params);
if (!Number.isFinite(expiresSec) || expiresSec <= 0) {
return minDelayMs;
}
const untilRefresh = expiresSec * 1e3 - nowMs - marginMs;
return Math.min(maxDelayMs, Math.max(minDelayMs, untilRefresh));
}
__name(frameTokenDelayMs, "frameTokenDelayMs");
class AuthKeepAlive {
static {
__name(this, "AuthKeepAlive");
}
#actions;
#params;
#getLogger;
#timer = null;
#isRunning = false;
// The run whose tick is currently in flight, or `null`. Run-scoped rather
// than a plain flag: a tick left over from an earlier run must not block a
// newer run's tick, or the pulse strands itself with nothing scheduled.
#tickingRunId = null;
#onVisibilityChange = null;
// Identifies the current start()..stop() run. A tick or timer from an earlier
// run must not schedule work for a later one, and a bare ticking flag cannot
// tell them apart because `stop()` cannot cancel a refresh already in flight.
#runId = 0;
// Consecutive ticks that did not end with a usable token. Drives the backoff.
#failures = 0;
// When the next check is due. Everything that wants to check early — the
// visibility handler above all — defers to this, so a tab switch cannot walk
// past the backoff and turn a refusing parent back into a 30-second retry.
#nextCheckAtMs = 0;
constructor(actions, getLogger, params) {
this.#actions = actions;
this.#getLogger = getLogger;
this.#params = resolveKeepAuthFreshParams(params);
}
/**
* Starts the pulse. Idempotent, and a no-op outside a browser (SSR): there is
* no parent window to postMessage to, and no `document` to watch.
*/
start() {
if (this.#isRunning || typeof window === "undefined") {
return;
}
this.#isRunning = true;
this.#failures = 0;
const runId = this.#runId;
if (typeof document !== "undefined") {
this.#onVisibilityChange = () => {
if (document.visibilityState !== "visible") {
return;
}
const currentRunId = this.#runId;
const waitMs = this.#nextCheckAtMs - Date.now();
if (waitMs > 0) {
this.#schedule(waitMs, currentRunId);
return;
}
void this.#tick(currentRunId);
};
document.addEventListener("visibilitychange", this.#onVisibilityChange);
}
if (this.#tickingRunId !== null) {
this.#schedule(this.#params.minDelayMs, runId);
return;
}
void this.#tick(runId);
}
/**
* Stops the pulse and detaches the listener. Idempotent; safe to call on a
* pulse that never started.
*/
stop() {
this.#isRunning = false;
this.#runId += 1;
this.#clearTimer();
if (this.#onVisibilityChange !== null && typeof document !== "undefined") {
document.removeEventListener("visibilitychange", this.#onVisibilityChange);
}
this.#onVisibilityChange = null;
}
/**
* True between `start()` and `stop()`. Internal: the class does not leave the
* package (only {@link KeepAuthFreshParams} is exported), so this is for the
* owning `B24Frame` and for tests.
*/
get isRunning() {
return this.#isRunning;
}
async #tick(runId) {
if (!this.#isRunning || this.#tickingRunId === runId) {
return;
}
this.#tickingRunId = runId;
let delay = this.#params.minDelayMs;
try {
const authData = this.#actions.getAuthData();
const expires = authData === false ? void 0 : authData.expires;
if (!frameTokenDue(expires, Date.now(), this.#params)) {
this.#failures = 0;
delay = frameTokenDelayMs(expires, Date.now(), this.#params);
} else {
const refreshed = await this.#actions.refreshAuth();
const freshExpires = refreshed ? refreshed.expires : void 0;
if (frameTokenDue(freshExpires, Date.now(), this.#params)) {
this.#failures += 1;
delay = this.#backoffDelayMs();
} else {
this.#failures = 0;
delay = frameTokenDelayMs(freshExpires, Date.now(), this.#params);
}
}
} catch (error) {
this.#failures += 1;
delay = this.#backoffDelayMs();
try {
this.#getLogger().warning("keepAuthFresh: refresh failed, will retry", {
error: error instanceof Error ? error.message : String(error),
failures: this.#failures,
retryInMs: delay
}).catch(() => {
});
} catch {
}
} finally {
if (this.#tickingRunId === runId) {
this.#tickingRunId = null;
}
this.#schedule(delay, runId);
}
}
/**
* Exponential backoff over consecutive failed (or useless) refreshes, from
* the floor up to the ceiling: 30s, 1m, 2m, 4m, 8m, then 10m forever.
*
* A parent window that is navigated away, blocked or refusing is the case
* `REFRESH_AUTH_TIMEOUT` exists for, and each attempt costs a message plus a
* 10-second reject timer. Retrying it at a flat 30 seconds for the life of
* the tab is the behaviour this exists to prevent.
*/
#backoffDelayMs() {
const steps = Math.min(Math.max(this.#failures - 1, 0), 10);
return Math.min(this.#params.maxDelayMs, this.#params.minDelayMs * 2 ** steps);
}
#schedule(delayMs, runId) {
if (!this.#isRunning || this.#runId !== runId) {
return;
}
this.#clearTimer();
this.#nextCheckAtMs = Date.now() + delayMs;
this.#timer = setTimeout(() => {
this.#timer = null;
void this.#tick(runId);
}, delayMs);
const timer = this.#timer;
if (typeof timer?.unref === "function") {
timer.unref();
}
}
#clearTimer() {
if (this.#timer !== null) {
clearTimeout(this.#timer);
this.#timer = null;
}
}
}
export { AuthKeepAlive, FRAME_PULSE_MAX_MS, FRAME_PULSE_MIN_MS, FRAME_REFRESH_MARGIN_MS, frameTokenDelayMs, frameTokenDue, resolveKeepAuthFreshParams };
//# sourceMappingURL=auth-keep-alive.mjs.map