Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enh: allow refresh times greater than max JS number, of ~24.8 days #891

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
121 changes: 90 additions & 31 deletions src/runtime/utils/refreshHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,74 +8,133 @@ export class DefaultRefreshHandler implements RefreshHandler {
/** Runtime config is mostly used for getting provider data */
runtimeConfig?: ModuleOptionsNormalized

/** Refetch interval */
refetchIntervalTimer?: ReturnType<typeof setInterval>

// TODO: find more Generic method to start a Timer for the Refresh Token
/** Refetch interval for local/refresh schema */
refreshTokenIntervalTimer?: ReturnType<typeof setInterval>

/** Because passing `this.visibilityHandler` to `document.addEventHandler` loses `this` context */
private boundVisibilityHandler: typeof this.visibilityHandler

/** Maximum value for setTimeout & setInterval in JavaScript (~24.85 days) */
private readonly MAX_JS_TIMEOUT: number = 2_147_483_647

/**
* Timers for different refresh types.
* Key represents name, value is timeout object.
*/
private refreshTimers: { [key: string]: ReturnType<typeof setTimeout> } = {}

/**
* Interval durations for executing refresh timers.
* Key represents timer name, value is interval duration (in milliseconds).
*/
private refreshIntervals: { [key: string]: number } = {}

constructor(
public config: DefaultRefreshHandlerConfig
) {
this.boundVisibilityHandler = this.visibilityHandler.bind(this)
}

/**
* Initializes the refresh handler, setting up timers and event listeners.
*/
init(): void {
this.runtimeConfig = useRuntimeConfig().public.auth
this.auth = useAuth()

// Set up visibility change listener
document.addEventListener('visibilitychange', this.boundVisibilityHandler, false)

const { enablePeriodically } = this.config
const defaultRefreshInterval: number = 5 * 60 * 1000 // 5 minutes, in ms

// Set up 'periodic' refresh timer
if (enablePeriodically !== false) {
const intervalTime = enablePeriodically === true ? 1000 : enablePeriodically
this.refetchIntervalTimer = setInterval(() => {
if (this.auth?.data.value) {
this.auth.refresh()
}
}, intervalTime)
this.refreshIntervals.periodic = enablePeriodically === true ? defaultRefreshInterval : (enablePeriodically ?? defaultRefreshInterval)
this.startRefreshTimer('periodic', this.refreshIntervals.periodic)
}

// Set up 'maxAge' refresh timer
const provider = this.runtimeConfig.provider
if (provider.type === 'local' && provider.refresh.isEnabled && provider.refresh.token?.maxAgeInSeconds) {
const intervalTime = provider.refresh.token.maxAgeInSeconds * 1000

this.refreshTokenIntervalTimer = setInterval(() => {
if (this.auth?.refreshToken.value) {
this.auth.refresh()
}
}, intervalTime)
this.refreshIntervals.maxAge = provider.refresh.token.maxAgeInSeconds * 1000
this.startRefreshTimer('maxAge', this.refreshIntervals.maxAge)
}
}

/**
* Cleans up timers and event listeners.
*/
destroy(): void {
// Clear visibility handler
// Clear visibility change listener
document.removeEventListener('visibilitychange', this.boundVisibilityHandler, false)

// Clear refetch interval
clearInterval(this.refetchIntervalTimer)

// Clear refetch interval
if (this.refreshTokenIntervalTimer) {
clearInterval(this.refreshTokenIntervalTimer)
}
// Clear refresh timers
this.clearAllTimers()

// Release state
this.auth = undefined
this.runtimeConfig = undefined
}

/**
* Handles visibility changes, refreshing the session when the browser tab/page becomes visible.
*/
visibilityHandler(): void {
// Listen for when the page is visible, if the user switches tabs
// and makes our tab visible again, re-fetch the session, but only if
// this feature is not disabled.
if (this.config?.enableOnWindowFocus && document.visibilityState === 'visible' && this.auth?.data.value) {
this.auth.refresh()
}
}

/**
* Starts or restarts a refresh timer, handling large durations by breaking them into smaller intervals.
* This method is used to periodically trigger the refresh.
*
* @param {'periodic' | 'maxAge'} timerName - Identifies which timer to start.
* @param {number} durationMs - The duration in milliseconds before the next refresh should occur.
*/
private startRefreshTimer(timerName: 'periodic' | 'maxAge', durationMs: number): void {
// Ensure the duration is positive; if not, exit early
if (durationMs <= 0) {
return
}

// Validate that the timerName is one of the allowed values
if (!['periodic', 'maxAge'].includes(timerName)) {
throw new Error(`Invalid timer name: ${timerName}`)
}

if (durationMs > this.MAX_JS_TIMEOUT) {
// If the duration exceeds JavaScript's maximum timeout value:
// Set a timeout for the maximum allowed duration, then recursively call
// this method with the remaining time when that timeout completes.
this.refreshTimers[timerName] = setTimeout(() => {
this.startRefreshTimer(timerName, durationMs - this.MAX_JS_TIMEOUT)
}, this.MAX_JS_TIMEOUT)
}
else {
// If the duration is within the allowed range:
// The refresh can be triggered and the timer can be reset.
this.refreshTimers[timerName] = setTimeout(() => {
// Determine which auth property to check based on the timer type
const needsSessOrToken: 'data' | 'refreshToken' = timerName === 'periodic' ? 'data' : 'refreshToken'

// Only refresh if the relevant auth data exists
if (this.auth?.[needsSessOrToken].value) {
this.auth.refresh()
}

// Restart timer with its original duration
this.startRefreshTimer(timerName, this.refreshIntervals[timerName] ?? 0)
}, durationMs)
}
}

/**
* Clears all active refresh timers.
*/
private clearAllTimers(): void {
Object.values(this.refreshTimers).forEach((timer) => {
if (timer) {
clearTimeout(timer)
}
})
}
}
Loading