@@ -13,6 +13,22 @@ export class AppComponent implements OnInit, OnDestroy {
1313 loginPromptVisible = false ;
1414 isLoggedIn = false ;
1515 private sessionInvalidSub : Subscription | null = null ;
16+ private swVersionSub : Subscription | null = null ;
17+ private swUnrecoverableSub : Subscription | null = null ;
18+ private updateCheckIntervalId : any = null ;
19+ private updateCheckHandler : ( ( ) => void ) | null = null ;
20+ private visibilityChangeHandler : ( ( ) => void ) | null = null ;
21+ private userActivityHandler : ( ( ) => void ) | null = null ;
22+ private pendingReload = false ;
23+ private reloadStartAt : number | null = null ;
24+ private reloadTimerId : any = null ;
25+ private lastUserActivityAt = Date . now ( ) ;
26+ private readonly appStartAt = Date . now ( ) ;
27+
28+ private readonly updateCheckIntervalMs = 5 * 60_000 ; // 5 minutes
29+ private readonly idleBeforeReloadMs = 30_000 ; // 30s of inactivity
30+ private readonly maxReloadDelayMs = 5 * 60_000 ; // force refresh within 5 minutes
31+ private readonly startupImmediateReloadWindowMs = 10_000 ; // reload immediately if update is found within first 10s after load
1632
1733 constructor (
1834 private swUpdate : SwUpdate ,
@@ -44,6 +60,33 @@ export class AppComponent implements OnInit, OnDestroy {
4460 if ( this . sessionInvalidSub ) {
4561 this . sessionInvalidSub . unsubscribe ( ) ;
4662 }
63+ if ( this . swVersionSub ) {
64+ this . swVersionSub . unsubscribe ( ) ;
65+ }
66+ if ( this . swUnrecoverableSub ) {
67+ this . swUnrecoverableSub . unsubscribe ( ) ;
68+ }
69+ if ( this . updateCheckIntervalId ) {
70+ clearInterval ( this . updateCheckIntervalId ) ;
71+ this . updateCheckIntervalId = null ;
72+ }
73+ if ( this . reloadTimerId ) {
74+ clearTimeout ( this . reloadTimerId ) ;
75+ this . reloadTimerId = null ;
76+ }
77+ if ( this . updateCheckHandler ) {
78+ window . removeEventListener ( 'focus' , this . updateCheckHandler ) ;
79+ window . removeEventListener ( 'online' , this . updateCheckHandler ) ;
80+ }
81+ if ( this . visibilityChangeHandler ) {
82+ document . removeEventListener ( 'visibilitychange' , this . visibilityChangeHandler ) ;
83+ }
84+ if ( this . userActivityHandler ) {
85+ window . removeEventListener ( 'pointerdown' , this . userActivityHandler , true ) ;
86+ window . removeEventListener ( 'keydown' , this . userActivityHandler , true ) ;
87+ window . removeEventListener ( 'touchstart' , this . userActivityHandler , true ) ;
88+ window . removeEventListener ( 'wheel' , this . userActivityHandler , true ) ;
89+ }
4790 }
4891
4992 private async validateSessionOnStartup ( ) : Promise < void > {
@@ -60,20 +103,105 @@ export class AppComponent implements OnInit, OnDestroy {
60103
61104 checkForUpdates ( ) {
62105 if ( this . swUpdate . isEnabled ) {
63- this . swUpdate . checkForUpdate ( ) . then ( ( ) => {
64- console . log ( "Checked for updates" ) ;
65- } ) ;
106+ this . updateCheckHandler = ( ) => {
107+ if ( typeof navigator !== 'undefined' && navigator && 'onLine' in navigator && ! navigator . onLine ) return ;
108+ this . swUpdate
109+ . checkForUpdate ( )
110+ . then ( found => {
111+ if ( found ) {
112+ console . log ( 'New version detected' ) ;
113+ this . activateUpdateAndReload ( ) ;
114+ }
115+ } )
116+ . catch ( err => {
117+ console . warn ( 'Service worker update check failed' , err ) ;
118+ } ) ;
119+ } ;
120+
121+ this . visibilityChangeHandler = ( ) => {
122+ if ( document . visibilityState === 'visible' ) {
123+ this . updateCheckHandler ?.( ) ;
124+ } else if ( this . pendingReload ) {
125+ this . tryReloadForUpdate ( ) ;
126+ }
127+ } ;
66128
67- this . swUpdate . versionUpdates . subscribe ( event => {
129+ this . userActivityHandler = ( ) => {
130+ this . lastUserActivityAt = Date . now ( ) ;
131+ } ;
132+
133+ window . addEventListener ( 'focus' , this . updateCheckHandler ) ;
134+ window . addEventListener ( 'online' , this . updateCheckHandler ) ;
135+ document . addEventListener ( 'visibilitychange' , this . visibilityChangeHandler ) ;
136+ window . addEventListener ( 'pointerdown' , this . userActivityHandler , true ) ;
137+ window . addEventListener ( 'keydown' , this . userActivityHandler , true ) ;
138+ window . addEventListener ( 'touchstart' , this . userActivityHandler , true ) ;
139+ window . addEventListener ( 'wheel' , this . userActivityHandler , true ) ;
140+
141+ this . swVersionSub = this . swUpdate . versionUpdates . subscribe ( event => {
68142 if ( event . type === 'VERSION_READY' ) {
69- if ( confirm ( "New version available. Load New Version?" ) ) {
70- window . location . reload ( ) ;
71- }
143+ this . activateUpdateAndReload ( ) ;
144+ } else if ( event . type === 'VERSION_INSTALLATION_FAILED' ) {
145+ console . warn ( 'Service worker update installation failed' , event . error ) ;
72146 }
73147 } ) ;
148+
149+ this . swUnrecoverableSub = this . swUpdate . unrecoverable . subscribe ( event => {
150+ console . warn ( 'Service worker unrecoverable state, reloading' , event . reason ) ;
151+ window . location . reload ( ) ;
152+ } ) ;
153+
154+ this . updateCheckHandler ( ) ;
155+ this . updateCheckIntervalId = setInterval ( this . updateCheckHandler , this . updateCheckIntervalMs ) ;
74156 }
75157 }
76158
159+ private activateUpdateAndReload ( ) {
160+ if ( this . pendingReload ) return ;
161+ this . pendingReload = true ;
162+ this . reloadStartAt = Date . now ( ) ;
163+
164+ this . swUpdate
165+ . activateUpdate ( )
166+ . then ( activated => {
167+ if ( activated ) console . log ( 'Activated new version' ) ;
168+ this . tryReloadForUpdate ( ) ;
169+ } )
170+ . catch ( err => {
171+ console . warn ( 'Service worker activateUpdate failed' , err ) ;
172+ this . tryReloadForUpdate ( ) ;
173+ } ) ;
174+ }
175+
176+ private tryReloadForUpdate ( ) {
177+ if ( ! this . pendingReload ) return ;
178+
179+ const now = Date . now ( ) ;
180+ const waitedMs = this . reloadStartAt ? now - this . reloadStartAt : 0 ;
181+ const idleMs = now - this . lastUserActivityAt ;
182+ const detectedAt = this . reloadStartAt ?? now ;
183+ const detectedSinceStartMs = detectedAt - this . appStartAt ;
184+ const shouldReloadImmediatelyOnStartup = detectedSinceStartMs <= this . startupImmediateReloadWindowMs ;
185+
186+ const shouldReload =
187+ shouldReloadImmediatelyOnStartup ||
188+ document . visibilityState === 'hidden' ||
189+ ! document . hasFocus ( ) ||
190+ idleMs >= this . idleBeforeReloadMs ||
191+ waitedMs >= this . maxReloadDelayMs ;
192+
193+ if ( shouldReload ) {
194+ window . location . reload ( ) ;
195+ return ;
196+ }
197+
198+ if ( this . reloadTimerId ) return ;
199+ this . reloadTimerId = setTimeout ( ( ) => {
200+ this . reloadTimerId = null ;
201+ this . tryReloadForUpdate ( ) ;
202+ } , 5_000 ) ;
203+ }
204+
77205 onLoginModalVisibleChange ( visible : boolean ) {
78206 this . loginPromptVisible = visible ;
79207 }
0 commit comments