tasq/node_modules/@supabase/auth-js/dist/main/lib/locks.js

344 lines
15 KiB
JavaScript

"use strict";
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");
/**
* @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.
*
* Use the {@link #isAcquireTimeout} property instead of checking with `instanceof`.
*
* @example
* ```ts
* import { LockAcquireTimeoutError } from '@supabase/auth-js'
*
* class CustomLockError extends LockAcquireTimeoutError {
* constructor() {
* super('Lock timed out')
* }
* }
* ```
*/
class LockAcquireTimeoutError extends Error {
constructor(message) {
super(message);
this.isAcquireTimeout = true;
}
}
exports.LockAcquireTimeoutError = LockAcquireTimeoutError;
/**
* Error thrown when the browser Navigator Lock API fails to acquire a lock.
*
* @example
* ```ts
* import { NavigatorLockAcquireTimeoutError } from '@supabase/auth-js'
*
* throw new NavigatorLockAcquireTimeoutError('Lock timed out')
* ```
*/
class NavigatorLockAcquireTimeoutError extends LockAcquireTimeoutError {
}
exports.NavigatorLockAcquireTimeoutError = NavigatorLockAcquireTimeoutError;
/**
* Error thrown when the process-level lock helper cannot acquire a lock.
*
* @example
* ```ts
* import { ProcessLockAcquireTimeoutError } from '@supabase/auth-js'
*
* throw new ProcessLockAcquireTimeoutError('Lock timed out')
* ```
*/
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.
* @example
* ```ts
* await navigatorLock('sync-user', 1000, async () => {
* await refreshSession()
* })
* ```
*/
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);
}
if ((e === null || e === void 0 ? void 0 : 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.
* @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 && e.isAcquireTimeout) {
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 && e.isAcquireTimeout) {
// 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