@@ -15,6 +15,7 @@ import { Controller } from "@hotwired/stimulus";
1515export default class extends Controller {
1616 static readonly MANIFEST_WAIT_POLL_INTERVAL_MS : number = 2000 ;
1717 static readonly MANIFEST_WAIT_MAX_ATTEMPTS : number = 30 ;
18+ static BACKGROUND_SYNC_INTERVAL_MS : number = 300000 ;
1819
1920 static values = {
2021 fetchUrl : String ,
@@ -80,10 +81,38 @@ export default class extends Controller {
8081 private itemHeight : number = 80 ;
8182 private isLoading : boolean = false ;
8283 private isUploading : boolean = false ;
84+ private isConnected : boolean = false ;
85+ private backgroundSyncTimeoutId : ReturnType < typeof setTimeout > | null = null ;
86+ private latestManifestRevision : string | null = null ;
87+ private isBackgroundSyncEnabled : boolean = false ;
88+ private readonly focusHandler = ( ) : void => {
89+ void this . checkForManifestUpdates ( ) ;
90+ } ;
91+ private readonly visibilityChangeHandler = ( ) : void => {
92+ if ( document . visibilityState === "visible" ) {
93+ void this . checkForManifestUpdates ( ) ;
94+ }
95+ } ;
8396
8497 connect ( ) : void {
85- this . fetchAssets ( ) ;
98+ this . isConnected = true ;
99+ void this . fetchAssets ( ) ;
86100 this . setupDropzone ( ) ;
101+ this . isBackgroundSyncEnabled = this . getBackgroundSyncIntervalMs ( ) > 0 ;
102+ if ( this . isBackgroundSyncEnabled ) {
103+ this . startBackgroundSync ( ) ;
104+ window . addEventListener ( "focus" , this . focusHandler ) ;
105+ document . addEventListener ( "visibilitychange" , this . visibilityChangeHandler ) ;
106+ }
107+ }
108+
109+ disconnect ( ) : void {
110+ this . isConnected = false ;
111+ this . stopBackgroundSync ( ) ;
112+ if ( this . isBackgroundSyncEnabled ) {
113+ window . removeEventListener ( "focus" , this . focusHandler ) ;
114+ document . removeEventListener ( "visibilitychange" , this . visibilityChangeHandler ) ;
115+ }
87116 }
88117
89118 /**
@@ -307,7 +336,7 @@ export default class extends Controller {
307336 setTimeout ( ( ) => this . showUploadStatus ( "none" ) , 5000 ) ;
308337 }
309338
310- private async fetchAssets ( ) : Promise < string [ ] | null > {
339+ private async fetchAssets ( ) : Promise < { urls : string [ ] ; revision : string } | null > {
311340 if ( this . isLoading || ! this . fetchUrlValue ) {
312341 return null ;
313342 }
@@ -324,9 +353,10 @@ export default class extends Controller {
324353 throw new Error ( `HTTP ${ response . status } ` ) ;
325354 }
326355
327- const data = ( await response . json ( ) ) as { urls ?: string [ ] } ;
328- const manifestUrls = data . urls ?? [ ] ;
329- this . urls = manifestUrls ;
356+ const data = ( await response . json ( ) ) as { urls ?: string [ ] ; revision ?: string } ;
357+ const manifestData = this . normalizeManifestData ( data ) ;
358+ this . urls = manifestData . urls ;
359+ this . latestManifestRevision = manifestData . revision ;
330360
331361 this . showLoading ( false ) ;
332362
@@ -339,7 +369,7 @@ export default class extends Controller {
339369 this . filter ( ) ;
340370 this . showEmpty ( this . filteredUrls . length === 0 ) ;
341371 }
342- return manifestUrls ;
372+ return manifestData ;
343373 } catch {
344374 this . showLoading ( false ) ;
345375 this . showEmpty ( true ) ;
@@ -362,11 +392,11 @@ export default class extends Controller {
362392 const maxAttempts = this . getManifestWaitMaxAttempts ( ) ;
363393
364394 for ( let attempt = 0 ; attempt < maxAttempts ; attempt ++ ) {
365- const manifestUrls = await this . fetchManifestUrlsSilently ( ) ;
366- if ( manifestUrls !== null ) {
367- const manifestUrlSet = new Set ( manifestUrls ) ;
395+ const manifestData = await this . fetchManifestUrlsSilently ( ) ;
396+ if ( manifestData !== null ) {
397+ const manifestUrlSet = new Set ( manifestData . urls ) ;
368398 const manifestFileNameSet = new Set (
369- manifestUrls . map ( ( url ) => this . extractFilename ( url ) ) . filter ( ( fileName ) => fileName !== "" ) ,
399+ manifestData . urls . map ( ( url ) => this . extractFilename ( url ) ) . filter ( ( fileName ) => fileName !== "" ) ,
370400 ) ;
371401 pendingUrls . forEach ( ( url ) => {
372402 const fileName = this . extractFilename ( url ) ;
@@ -390,7 +420,7 @@ export default class extends Controller {
390420 return false ;
391421 }
392422
393- private async fetchManifestUrlsSilently ( ) : Promise < string [ ] | null > {
423+ private async fetchManifestUrlsSilently ( ) : Promise < { urls : string [ ] ; revision : string } | null > {
394424 if ( ! this . fetchUrlValue ) {
395425 return null ;
396426 }
@@ -403,13 +433,79 @@ export default class extends Controller {
403433 return null ;
404434 }
405435
406- const data = ( await response . json ( ) ) as { urls ?: string [ ] } ;
407- return data . urls ?? [ ] ;
436+ const data = ( await response . json ( ) ) as { urls ?: string [ ] ; revision ?: string } ;
437+ return this . normalizeManifestData ( data ) ;
408438 } catch {
409439 return null ;
410440 }
411441 }
412442
443+ private startBackgroundSync ( ) : void {
444+ const intervalMs = this . getBackgroundSyncIntervalMs ( ) ;
445+ if ( intervalMs <= 0 ) {
446+ return ;
447+ }
448+
449+ this . stopBackgroundSync ( ) ;
450+ this . scheduleNextBackgroundSync ( intervalMs ) ;
451+ }
452+
453+ private stopBackgroundSync ( ) : void {
454+ if ( this . backgroundSyncTimeoutId !== null ) {
455+ clearTimeout ( this . backgroundSyncTimeoutId ) ;
456+ this . backgroundSyncTimeoutId = null ;
457+ }
458+ }
459+
460+ private scheduleNextBackgroundSync ( intervalMs : number ) : void {
461+ if ( ! this . isConnected ) {
462+ return ;
463+ }
464+ this . backgroundSyncTimeoutId = setTimeout ( ( ) => {
465+ void this . runBackgroundSyncTick ( ) ;
466+ } , intervalMs ) ;
467+ }
468+
469+ private async runBackgroundSyncTick ( ) : Promise < void > {
470+ await this . checkForManifestUpdates ( ) ;
471+ if ( this . isConnected ) {
472+ this . scheduleNextBackgroundSync ( this . getBackgroundSyncIntervalMs ( ) ) ;
473+ }
474+ }
475+
476+ private async checkForManifestUpdates ( ) : Promise < void > {
477+ if ( this . isUploading ) {
478+ return ;
479+ }
480+
481+ const currentRevision = this . latestManifestRevision ;
482+ const manifestData = await this . fetchManifestUrlsSilently ( ) ;
483+ if ( manifestData === null ) {
484+ return ;
485+ }
486+
487+ if ( currentRevision === null || manifestData . revision !== currentRevision ) {
488+ await this . fetchAssets ( ) ;
489+ }
490+ }
491+
492+ private normalizeManifestData ( data : { urls ?: string [ ] ; revision ?: string } ) : { urls : string [ ] ; revision : string } {
493+ const urls = data . urls ?? [ ] ;
494+ const revision = data . revision ?? this . computeManifestRevision ( urls ) ;
495+
496+ return { urls, revision } ;
497+ }
498+
499+ private computeManifestRevision ( urls : string [ ] ) : string {
500+ const normalized = [ ...urls ] . sort ( ( a , b ) => a . localeCompare ( b ) ) ;
501+ return normalized . join ( "|" ) ;
502+ }
503+
504+ private getBackgroundSyncIntervalMs ( ) : number {
505+ const ctor = this . constructor as typeof Controller & { BACKGROUND_SYNC_INTERVAL_MS ?: number } ;
506+ return ctor . BACKGROUND_SYNC_INTERVAL_MS ?? 30000 ;
507+ }
508+
413509 private getManifestWaitPollIntervalMs ( ) : number {
414510 const ctor = this . constructor as typeof Controller & { MANIFEST_WAIT_POLL_INTERVAL_MS ?: number } ;
415511 return ctor . MANIFEST_WAIT_POLL_INTERVAL_MS ?? 2000 ;
0 commit comments