import { MULTIPLE_STYTCH_CLIENTS_DETECTED_WARNING } from './constants.mjs';
import { shouldTryRefresh } from './shouldTryRefresh.mjs';
import { logger } from './utils/logger.mjs';
import { UNRECOVERABLE_ERROR_TYPES } from './public/SDKErrors.mjs';

class SessionManagerRegistry {
    hasWarned = false;
    registry = new Map();
    register(key, sessionManager) {
        const otherManager = this.registry.get(key);
        // If there appears to be another registered session manager, issue a
        // warning and cancel its background refresh in favor the newer registration
        if (otherManager && otherManager !== sessionManager) {
            if (!this.hasWarned) {
                logger.warn(MULTIPLE_STYTCH_CLIENTS_DETECTED_WARNING);
                this.hasWarned = true;
            }
            otherManager.cancelBackgroundRefresh();
        }
        this.registry.set(key, sessionManager);
    }
    unregister(publicToken, sessionManager) {
        const otherManager = this.registry.get(publicToken);
        if (otherManager && otherManager === sessionManager) {
            this.registry.delete(publicToken);
        }
    }
}
class SessionManager {
    _subscriptionService;
    _headlessSessionClient;
    _publicToken;
    _options;
    // Three minutes
    static REFRESH_INTERVAL_MS = 1000 * 60 * 3;
    // When testing - it's often more useful to set to a shorter duration
    // private static REFRESH_INTERVAL_MS = 1000 * 3;
    timeout = null;
    /** In minutes */ lastAuthenticationSessionDuration;
    static registry = new SessionManagerRegistry();
    register() {
        SessionManager.registry.register(this._publicToken, this);
    }
    unregister() {
        SessionManager.registry.unregister(this._publicToken, this);
    }
    constructor(_subscriptionService, _headlessSessionClient, _publicToken, _options){
        this._subscriptionService = _subscriptionService;
        this._headlessSessionClient = _headlessSessionClient;
        this._publicToken = _publicToken;
        this._options = _options;
        this._subscriptionService.subscribeToState(this._onDataChange);
    }
    /**
   * The core logic of the session refresh recursive trampoline
   * - Refreshes the currently issued session
   * - Schedules a future refresh if successful
   */ performBackgroundRefresh() {
        this._reauthenticateWithBackoff().then(()=>{
            this.scheduleBackgroundRefresh();
        }).catch((error)=>{
            logger.warn('Session background refresh failed. Signalling to app that user is logged out.', {
                error
            });
            this._subscriptionService.destroySession();
        });
    }
    scheduleBackgroundRefresh() {
        /* Highlander rules - there can only ever be one */ this.cancelBackgroundRefresh();
        this.register();
        this.timeout = setTimeout(()=>{
            this.performBackgroundRefresh();
        }, SessionManager.REFRESH_INTERVAL_MS);
    }
    cancelBackgroundRefresh() {
        if (this.timeout !== null) {
            this.unregister();
            clearTimeout(this.timeout);
            this.timeout = null;
        }
    }
    /**
   * We need to listen to a few types of events:
   * - If the user logs in via invoking a .authenticate() call, we should start the background worker
   * - If the user steps up their authentication via another .authenticate call(), we should restart the background worker
   * - If the user logs out, we should terminate the worker
   * - We should ignore session changes that we ourselves caused - so if we already have a timeout, leave it be!
   */ _onDataChange = (state)=>{
        if (state != null && state.sessionDurationMinutes) {
            this.lastAuthenticationSessionDuration = state.sessionDurationMinutes;
        }
        if (shouldTryRefresh(state)) {
            this.scheduleBackgroundRefresh();
        } else {
            this.cancelBackgroundRefresh();
        }
    };
    // In cases where we cannot get a satisfactory request:
    // - Stytch is hard-down
    // - The user's network is disconnected for an extended period of time
    // we will continue to retry every 4 minutes ad infinum
    _reauthenticateWithBackoff = async ()=>{
        let count = 0;
        while(true){
            try {
                const options = {
                    session_duration_minutes: this._options.keepSessionAlive ? this.lastAuthenticationSessionDuration : undefined
                };
                return await this._headlessSessionClient.authenticate(options);
            } catch (err) {
                if (SessionManager.isUnrecoverableError(err)) {
                    return Promise.reject(err);
                }
                count++;
                await new Promise((done)=>setTimeout(done, SessionManager.timeoutForAttempt(count)));
            }
        }
    };
    // We start with a backoff of 2000ms and increase exponentially to ~4 minutes (+/- 175 ms for jitter)
    // A short backoff initially helps increase the chance that we refresh the session before the JWT expires
    static timeoutForAttempt(count) {
        count = Math.min(count, 7);
        const jitter = Math.floor(Math.random() * 350) - 175;
        const delayMS = 2000 * 2 ** count;
        return jitter + delayMS;
    }
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    static isUnrecoverableError(error) {
        return UNRECOVERABLE_ERROR_TYPES.includes(error.error_type);
    }
}

export { SessionManager };
//# sourceMappingURL=SessionManager.mjs.map
