-
-
Notifications
You must be signed in to change notification settings - Fork 177
/
Copy pathrefreshHandler.ts
140 lines (118 loc) · 4.98 KB
/
refreshHandler.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import type { DefaultRefreshHandlerConfig, ModuleOptionsNormalized, RefreshHandler } from '../types'
import { useAuth, useRuntimeConfig } from '#imports'
export class DefaultRefreshHandler implements RefreshHandler {
/** Result of `useAuth` composable, mostly used for session data/refreshing */
auth?: ReturnType<typeof useAuth>
/** Runtime config is mostly used for getting provider data */
runtimeConfig?: ModuleOptionsNormalized
/** 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) {
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) {
this.refreshIntervals.maxAge = provider.refresh.token.maxAgeInSeconds * 1000
this.startRefreshTimer('maxAge', this.refreshIntervals.maxAge)
}
}
/**
* Cleans up timers and event listeners.
*/
destroy(): void {
// Clear visibility change listener
document.removeEventListener('visibilitychange', this.boundVisibilityHandler, false)
// 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 {
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)
}
})
}
}