-
Notifications
You must be signed in to change notification settings - Fork 0
/
checkForJsBundleUpdateSaga.tsx
149 lines (130 loc) · 4.26 KB
/
checkForJsBundleUpdateSaga.tsx
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
141
142
143
144
145
146
147
148
149
import moment from 'moment-timezone'
import { SagaIterator } from 'redux-saga'
import { delay, call } from 'redux-saga/effects'
import { alert } from '@interface-technologies/iti-react'
const defaultDelayDuration = moment.duration(4, 'minutes')
const forceRefreshAfterAlertCount = 3
/** @internal */
export function getIndexHtml(): Promise<string | undefined> {
return (
fetch('/')
.then((response) => {
if (!response.ok) return undefined
return response.text()
})
// If fetch throws a TypeError for some weird reason, also return undefined
.catch(() => undefined)
)
}
/** @internal */
export function reload(): void {
window.location.reload()
}
function getBundleSrcFromDocument(
doc: Document,
bundleSrcPattern: RegExp
): string | undefined {
const scripts = Array.from(doc.getElementsByTagName('script'))
for (const script of scripts) {
const parts = script.src?.split('/')
if (parts.length === 0) continue
const src = parts[parts.length - 1]
if (bundleSrcPattern.test(src)) return src
}
return undefined
}
export interface CheckForJsBundleUpdateSagaOptions {
delayDuration?: moment.Duration
onError(e: unknown): void
bundleSrcPattern?: RegExp
}
/**
* Periodically fetches `/` (`index.html`) to check if a new JavaScript bundle has been
* released.
*
* If the bundle has been updated, the user is prompted to refresh the page.
* After 3 alerts are shown, the page is forcibly refreshed.
*
* Don't enable this in development!
*
* Your `index.html` must contain a script tag like:
*
* ```html
* <script src="app.23e590a23b49.js"></script>
* ```
*
* or
*
* ```html
* <script src="dist/app.js?v=Gq0JHtehvL9fMpV"></script>
* ```
*
* `checkForJsBundleUpdateSaga` will compare the `src` attribute of the script tag.
*
* An example of using `checkForJsBundleUpdateSaga` from your TypeScript code:
*
* ```
* export function* myCheckForJsBundleUpdateSaga(): SagaIterator<void> {
* if (process.env.NODE_ENV === 'development') return
*
* function onError(e: unknown): void {
* console.error(e)
* Bugsnag.notify(e)
* }
*
* yield call(checkForJsBundleUpdateSaga, { onError })
* }
* ```
*/
export function* checkForJsBundleUpdateSaga({
delayDuration = defaultDelayDuration,
onError,
bundleSrcPattern = /app\.\S+\.js/,
}: CheckForJsBundleUpdateSagaOptions): SagaIterator<void> {
const bundleSrc = getBundleSrcFromDocument(document, bundleSrcPattern)
if (!bundleSrc) {
onError(new Error('Could not get bundle src from current document.'))
return
}
yield delay(delayDuration.asMilliseconds())
let alertShownCount = 0
for (;;) {
try {
const indexHtml = (yield call(getIndexHtml)) as string | undefined
if (indexHtml) {
const retrievedDocument = new DOMParser().parseFromString(
indexHtml,
'text/html'
)
const retrievedBundleSrc = getBundleSrcFromDocument(
retrievedDocument,
bundleSrcPattern
)
if (!retrievedBundleSrc) {
onError('Could not get bundle src from retrieved document.')
return
}
if (bundleSrc !== retrievedBundleSrc) {
const content = (
<div>
<p>Please save your work and refresh the page.</p>
<p className="mb-0">
You may encounter errors if you do not refresh the page.
</p>
</div>
)
if (alertShownCount >= forceRefreshAfterAlertCount) {
window.onbeforeunload = null
yield call(reload)
return
}
yield call(alert, content, { title: 'Website Update Available!' })
alertShownCount += 1
}
}
} catch (e) {
onError(e)
}
yield delay(delayDuration.asMilliseconds())
}
}