Skip to content

Commit

Permalink
feat: fix stack guard issues with Safari (#743)
Browse files Browse the repository at this point in the history
Safari does not support stack guards as it does not preserve stack
traces across async/await calls. This PR removes the reliance on stack
guards when they're not supported, but this means that concurrent access
can cause random logouts in those browsers.
  • Loading branch information
hf authored Jul 21, 2023
1 parent 293662c commit c614101
Show file tree
Hide file tree
Showing 2 changed files with 45 additions and 6 deletions.
14 changes: 12 additions & 2 deletions src/GoTrueClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
supportsLocalStorage,
stackGuard,
isInStackGuard,
stackGuardsSupported,
} from './lib/helpers'
import localStorageAdapter from './lib/local-storage'
import { polyfillGlobalThis } from './lib/polyfills'
Expand Down Expand Up @@ -806,6 +807,15 @@ export default class GoTrueClient {
this._debug('#_acquireLock', 'begin', acquireTimeout)

try {
if (!(await stackGuardsSupported())) {
this._debug(
'#_acquireLock',
'Stack guards not supported, so exclusive locking is not performed as it can lead to deadlocks if the lock is attempted to be recursively acquired (as the recursion cannot be detected).'
)

return await fn()
}

if (isInStackGuard('_acquireLock')) {
this._debug('#_acquireLock', 'recursive call')
return await fn()
Expand Down Expand Up @@ -908,13 +918,13 @@ export default class GoTrueClient {
> {
this._debug('#__loadSession()', 'begin')

if (this.logDebugMessages && !isInStackGuard('_useSession')) {
if (this.logDebugMessages && !isInStackGuard('_useSession') && (await stackGuardsSupported())) {
throw new Error('Please use #_useSession()')
}

// make sure we've read the session from the url if there is one
// save to just await, as long we make sure _initialize() never throws
if (!isInStackGuard('_initialize')) {
if (!isInStackGuard('_initialize') && (await stackGuardsSupported())) {
// only wait when not called from within #_initialize() since it's
// waiting for itself. one such pathway is #_initialize() ->
// #_handleVisibilityChange() -> #_onVisbilityChanged() ->
Expand Down
37 changes: 33 additions & 4 deletions src/lib/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,8 @@ export function decodeJWTPayload(token: string) {
/**
* Creates a promise that resolves to null after some time.
*/
export function sleep(time: number): Promise<null> {
return new Promise((accept) => {
export async function sleep(time: number): Promise<null> {
return await new Promise((accept) => {
setTimeout(() => accept(null), time)
})
}
Expand Down Expand Up @@ -309,6 +309,8 @@ const STACK_ENTRY_REGEX = /__stack_guard__([a-zA-Z0-9_-]+)__/
let STACK_GUARD_CHECKED = false
let STACK_GUARD_CHECK_FN: () => Promise<void> // eslint-disable-line prefer-const

let STACK_GUARDS_SUPPORTED = false

/**
* Checks if the current caller of the function is in a {@link
* #stackGuard} of the provided `name`. Works by looking through
Expand Down Expand Up @@ -370,9 +372,30 @@ export async function stackGuard<R>(name: string, fn: () => Promise<R>): Promise
[guardName]: async () => await fn(),
}

// Safari does not log the name of a dynamically named function unless you
// explicitly set the displayName
Object.assign(guardFunc[guardName], { displayName: guardName })

return await guardFunc[guardName]()
}

/**
* Returns if the JavaScript engine supports stack guards. If it doesn't
* certain features that depend on detecting recursive calls should be disabled
* to prevent deadlocks.
*/
export async function stackGuardsSupported(): Promise<boolean> {
if (STACK_GUARD_CHECKED) {
return STACK_GUARDS_SUPPORTED
}

await STACK_GUARD_CHECK_FN()

return STACK_GUARDS_SUPPORTED
}

let STACK_GUARD_WARNING_LOGGED = false

// In certain cases, if this file is transpiled using an ES2015 target, or is
// running in a JS engine that does not support async/await stack traces, this
// function will log a single warning message.
Expand All @@ -381,11 +404,17 @@ STACK_GUARD_CHECK_FN = async () => {
STACK_GUARD_CHECKED = true

await stackGuard('ENV_CHECK', async () => {
// sleeping for the next tick as Safari loses track of the async/await
// trace beyond this point
await sleep(0)

const result = isInStackGuard('ENV_CHECK')
STACK_GUARDS_SUPPORTED = result

if (!result) {
if (!result && !STACK_GUARD_WARNING_LOGGED) {
STACK_GUARD_WARNING_LOGGED = true
console.warn(
'@supabase/gotrue-js: Stack guards not supported in this environment. Generally not an issue but may point to a very conservative transpilation environment (use ES2017 or above) that implements async/await with generators, or this is a JavaScript engine that does not support async/await stack traces.'
'@supabase/gotrue-js: Stack guards not supported in this environment. Generally not an issue but may point to a very conservative transpilation environment (use ES2017 or above) that implements async/await with generators, or this is a JavaScript engine that does not support async/await stack traces. Safari is known to not support stack guards.'
)
}

Expand Down

0 comments on commit c614101

Please sign in to comment.