UNPKG

@bitrix24/b24jssdk

Version:

Bitrix24 REST API JavaScript SDK

218 lines (215 loc) • 7.74 kB
/** * @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/ */ 'use strict'; 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; } } } exports.AuthKeepAlive = AuthKeepAlive; exports.FRAME_PULSE_MAX_MS = FRAME_PULSE_MAX_MS; exports.FRAME_PULSE_MIN_MS = FRAME_PULSE_MIN_MS; exports.FRAME_REFRESH_MARGIN_MS = FRAME_REFRESH_MARGIN_MS; exports.frameTokenDelayMs = frameTokenDelayMs; exports.frameTokenDue = frameTokenDue; exports.resolveKeepAuthFreshParams = resolveKeepAuthFreshParams; //# sourceMappingURL=auth-keep-alive.cjs.map