"use strict";
/**
 * Lock primitives retained for backwards-compatible imports. The auth client
 * coordinates refreshes itself (deduping in-instance callers onto a shared
 * in-flight promise) and lets the GoTrue server resolve cross-instance races,
 * so it does not invoke any primitive from this module. The functions still
 * work for direct callers that need a navigator.locks-backed or in-process
 * exclusive lock of their own.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProcessLockAcquireTimeoutError = exports.NavigatorLockAcquireTimeoutError = exports.LockAcquireTimeoutError = exports.internals = void 0;
exports.navigatorLock = navigatorLock;
exports.processLock = processLock;
const helpers_1 = require("./helpers");
/**
 * @deprecated Debug flag for `navigatorLock` / `processLock`. The auth
 * client ignores both, so this has no client-side effect.
 * @experimental
 */
exports.internals = {
    /**
     * @experimental
     */
    debug: !!(globalThis &&
        (0, helpers_1.supportsLocalStorage)() &&
        globalThis.localStorage &&
        globalThis.localStorage.getItem('supabase.gotrue-js.locks.debug') === 'true'),
};
/**
 * An error thrown when a lock cannot be acquired after some amount of time.
 *
 * @deprecated The auth client doesn't acquire locks around auth operations,
 * so this error never originates from `supabase.auth.*` calls. Direct callers
 * of `navigatorLock` / `processLock` still receive it on acquire timeout.
 */
class LockAcquireTimeoutError extends Error {
    constructor(message) {
        super(message);
        this.isAcquireTimeout = true;
    }
}
exports.LockAcquireTimeoutError = LockAcquireTimeoutError;
/**
 * @deprecated The auth client doesn't call `navigator.locks`, so this error
 * never originates from `supabase.auth.*` calls. Direct callers of
 * `navigatorLock` still receive it on acquire timeout.
 */
class NavigatorLockAcquireTimeoutError extends LockAcquireTimeoutError {
}
exports.NavigatorLockAcquireTimeoutError = NavigatorLockAcquireTimeoutError;
/**
 * @deprecated The auth client doesn't run `processLock`, so this error
 * never originates from `supabase.auth.*` calls. Direct callers of
 * `processLock` still receive it on acquire timeout.
 */
class ProcessLockAcquireTimeoutError extends LockAcquireTimeoutError {
}
exports.ProcessLockAcquireTimeoutError = ProcessLockAcquireTimeoutError;
/**
 * Implements a global exclusive lock using the Navigator LockManager API. It
 * is available on all browsers released after 2022-03-15 with Safari being the
 * last one to release support. If the API is not available, this function will
 * throw. Make sure you check availablility before configuring {@link
 * GoTrueClient}.
 *
 * You can turn on debugging by setting the `supabase.gotrue-js.locks.debug`
 * local storage item to `true`.
 *
 * Internals:
 *
 * Since the LockManager API does not preserve stack traces for the async
 * function passed in the `request` method, a trick is used where acquiring the
 * lock releases a previously started promise to run the operation in the `fn`
 * function. The lock waits for that promise to finish (with or without error),
 * while the function will finally wait for the result anyway.
 *
 * @param name Name of the lock to be acquired.
 * @param acquireTimeout If negative, no timeout. If 0 an error is thrown if
 *                       the lock can't be acquired without waiting. If positive, the lock acquire
 *                       will time out after so many milliseconds. An error is
 *                       a timeout if it has `isAcquireTimeout` set to true.
 * @param fn The operation to run once the lock is acquired.
 *
 * @deprecated The auth client coordinates refreshes itself and the server
 * resolves concurrent refresh races, so passing `{ lock: navigatorLock }`
 * to it has no effect. You can safely drop the import from your client setup.
 */
async function navigatorLock(name, acquireTimeout, fn) {
    if (exports.internals.debug) {
        console.log('@supabase/gotrue-js: navigatorLock: acquire lock', name, acquireTimeout);
    }
    const abortController = new globalThis.AbortController();
    let acquireTimeoutTimer;
    if (acquireTimeout > 0) {
        acquireTimeoutTimer = setTimeout(() => {
            abortController.abort();
            if (exports.internals.debug) {
                console.log('@supabase/gotrue-js: navigatorLock acquire timed out', name);
            }
        }, acquireTimeout);
    }
    // MDN article: https://developer.mozilla.org/en-US/docs/Web/API/LockManager/request
    // Wrapping navigator.locks.request() with a plain Promise is done as some
    // libraries like zone.js patch the Promise object to track the execution
    // context. However, it appears that most browsers use an internal promise
    // implementation when using the navigator.locks.request() API causing them
    // to lose context and emit confusing log messages or break certain features.
    // This wrapping is believed to help zone.js track the execution context
    // better.
    await Promise.resolve();
    try {
        return await globalThis.navigator.locks.request(name, acquireTimeout === 0
            ? {
                mode: 'exclusive',
                ifAvailable: true,
            }
            : {
                mode: 'exclusive',
                signal: abortController.signal,
            }, async (lock) => {
            if (lock) {
                // Lock acquired — cancel the acquire-timeout timer so it cannot fire
                // while fn() is running. Without this, a delayed timeout abort would
                // set signal.aborted = true even though we already hold the lock,
                // causing a subsequent steal to be misclassified as "our timeout
                // fired" and triggering a spurious steal-back cascade.
                clearTimeout(acquireTimeoutTimer);
                if (exports.internals.debug) {
                    console.log('@supabase/gotrue-js: navigatorLock: acquired', name, lock.name);
                }
                try {
                    return await fn();
                }
                finally {
                    if (exports.internals.debug) {
                        console.log('@supabase/gotrue-js: navigatorLock: released', name, lock.name);
                    }
                }
            }
            else {
                if (acquireTimeout === 0) {
                    if (exports.internals.debug) {
                        console.log('@supabase/gotrue-js: navigatorLock: not immediately available', name);
                    }
                    throw new NavigatorLockAcquireTimeoutError(`Acquiring an exclusive Navigator LockManager lock "${name}" immediately failed`);
                }
                else {
                    if (exports.internals.debug) {
                        try {
                            const result = await globalThis.navigator.locks.query();
                            console.log('@supabase/gotrue-js: Navigator LockManager state', JSON.stringify(result, null, '  '));
                        }
                        catch (e) {
                            console.warn('@supabase/gotrue-js: Error when querying Navigator LockManager state', e);
                        }
                    }
                    // Browser is not following the Navigator LockManager spec, it
                    // returned a null lock when we didn't use ifAvailable. So we can
                    // pretend the lock is acquired in the name of backward compatibility
                    // and user experience and just run the function.
                    console.warn('@supabase/gotrue-js: Navigator LockManager returned a null lock when using #request without ifAvailable set to true, it appears this browser is not following the LockManager spec https://developer.mozilla.org/en-US/docs/Web/API/LockManager/request');
                    clearTimeout(acquireTimeoutTimer);
                    return await fn();
                }
            }
        });
    }
    catch (e) {
        // Always clear the acquire timeout once the request settles, so it cannot
        // fire later and incorrectly abort/log after a rejection.
        if (acquireTimeout > 0) {
            clearTimeout(acquireTimeoutTimer);
        }
        // DOMException does not extend Error in Node.js, so use structural check
        if (e !== null &&
            typeof e === 'object' &&
            'name' in e &&
            e.name === 'AbortError' &&
            acquireTimeout > 0) {
            if (abortController.signal.aborted) {
                // OUR timeout fired — the lock is genuinely orphaned. Steal it.
                //
                // The lock acquisition was aborted because the timeout fired while the
                // request was still pending. This typically means another lock holder is
                // not releasing the lock, possibly due to React Strict Mode's
                // double-mount/unmount behavior or a component unmounting mid-operation,
                // leaving an orphaned lock.
                //
                // Recovery: use { steal: true } to forcefully acquire the lock. Per the
                // Web Locks API spec, this releases any currently held lock with the same
                // name and grants the request immediately, preempting any queued requests.
                // The previous holder's callback continues running to completion but no
                // longer holds the lock for exclusion purposes.
                //
                // See: https://github.com/supabase/supabase/issues/42505
                if (exports.internals.debug) {
                    console.log('@supabase/gotrue-js: navigatorLock: acquire timeout, recovering by stealing lock', name);
                }
                console.warn(`@supabase/gotrue-js: Lock "${name}" was not released within ${acquireTimeout}ms. ` +
                    'This may indicate an orphaned lock from a component unmount (e.g., React Strict Mode). ' +
                    'Forcefully acquiring the lock to recover.');
                return await Promise.resolve().then(() => globalThis.navigator.locks.request(name, {
                    mode: 'exclusive',
                    steal: true,
                }, async (lock) => {
                    if (lock) {
                        if (exports.internals.debug) {
                            console.log('@supabase/gotrue-js: navigatorLock: recovered (stolen)', name, lock.name);
                        }
                        try {
                            return await fn();
                        }
                        finally {
                            if (exports.internals.debug) {
                                console.log('@supabase/gotrue-js: navigatorLock: released (stolen)', name, lock.name);
                            }
                        }
                    }
                    else {
                        // This should not happen with steal: true, but handle gracefully.
                        console.warn('@supabase/gotrue-js: Navigator LockManager returned null lock even with steal: true');
                        return await fn();
                    }
                }));
            }
            else {
                // We HELD the lock but another request stole it from us.
                // Per the Web Locks spec, our fn() callback is still running as an
                // orphaned background task — do NOT steal back. Stealing back would
                // cause a cascade (A steals B, B steals A, ...) and run fn() a second
                // time concurrently, corrupting auth state.
                // Convert to a typed error so callers (e.g. _autoRefreshTokenTick)
                // can handle/filter it without it leaking to Sentry as a raw AbortError.
                if (exports.internals.debug) {
                    console.log('@supabase/gotrue-js: navigatorLock: lock was stolen by another request', name);
                }
                throw new NavigatorLockAcquireTimeoutError(`Lock "${name}" was released because another request stole it`);
            }
        }
        throw e;
    }
}
const PROCESS_LOCKS = {};
/**
 * Implements a global exclusive lock that works only in the current process.
 * Useful for environments like React Native or other non-browser
 * single-process (i.e. no concept of "tabs") environments.
 *
 * Use {@link navigatorLock} in browser environments.
 *
 * @param name Name of the lock to be acquired.
 * @param acquireTimeout If negative, no timeout. If 0 an error is thrown if
 *                       the lock can't be acquired without waiting. If positive, the lock acquire
 *                       will time out after so many milliseconds. An error is
 *                       a timeout if it has `isAcquireTimeout` set to true.
 * @param fn The operation to run once the lock is acquired.
 *
 * @deprecated The auth client coordinates refreshes itself and the server
 * resolves concurrent refresh races, so passing `{ lock: processLock }`
 * to it has no effect. You can safely drop the import from your client setup.
 *
 * @example
 * ```ts
 * await processLock('migrate', 5000, async () => {
 *   await runMigration()
 * })
 * ```
 */
async function processLock(name, acquireTimeout, fn) {
    var _a;
    const previousOperation = (_a = PROCESS_LOCKS[name]) !== null && _a !== void 0 ? _a : Promise.resolve();
    // Wrap previousOperation to handle errors without using .catch()
    // This avoids Firefox content script security errors
    const previousOperationHandled = (async () => {
        try {
            await previousOperation;
            return null;
        }
        catch (e) {
            // ignore error of previous operation that we're waiting to finish
            return null;
        }
    })();
    const currentOperation = (async () => {
        let timeoutId = null;
        try {
            // Wait for either previous operation or timeout
            const timeoutPromise = acquireTimeout >= 0
                ? new Promise((_, reject) => {
                    timeoutId = setTimeout(() => {
                        console.warn(`@supabase/gotrue-js: Lock "${name}" acquisition timed out after ${acquireTimeout}ms. ` +
                            'This may be caused by another operation holding the lock. ' +
                            'Consider increasing lockAcquireTimeout or checking for stuck operations.');
                        reject(new ProcessLockAcquireTimeoutError(`Acquiring process lock with name "${name}" timed out`));
                    }, acquireTimeout);
                })
                : null;
            await Promise.race([previousOperationHandled, timeoutPromise].filter((x) => x));
            // If we reach here, previousOperationHandled won the race
            // Clear the timeout to prevent false warnings
            if (timeoutId !== null) {
                clearTimeout(timeoutId);
            }
        }
        catch (e) {
            // Clear the timeout on error path as well
            if (timeoutId !== null) {
                clearTimeout(timeoutId);
            }
            // Re-throw timeout errors, ignore others
            if (e instanceof LockAcquireTimeoutError) {
                throw e;
            }
            // Fall through to run fn() - previous operation finished with error
        }
        // Previous operations finished and we didn't get a race on the acquire
        // timeout, so the current operation can finally start
        return await fn();
    })();
    PROCESS_LOCKS[name] = (async () => {
        try {
            return await currentOperation;
        }
        catch (e) {
            if (e instanceof LockAcquireTimeoutError) {
                // if the current operation timed out, it doesn't mean that the previous
                // operation finished, so we need continue waiting for it to finish
                try {
                    await previousOperation;
                }
                catch (prevError) {
                    // Ignore previous operation errors
                }
                return null;
            }
            throw e;
        }
    })();
    // finally wait for the current operation to finish successfully, with an
    // error or with an acquire timeout error
    return await currentOperation;
}
//# sourceMappingURL=locks.js.map