@ -10,31 +10,87 @@ export class CacheManager {
runtimeCacheName : 'runtime'
} ;
/**
* Hard cached domains are always attempted to be cached, regardless of browser.
* For example, your internal origin, fonts, CDNs, etc.
*/
public hardCachedDomains : string [ ] ;
/**
* Soft cached domains will be cached normally except on Safari.
* This is useful for domains where Safari’ s handling of cached CORS responses is problematic.
*/
public softCachedDomains : string [ ] ;
constructor ( losslessServiceWorkerRefArg : ServiceWorker ) {
this . losslessServiceWorkerRef = losslessServiceWorkerRefArg ;
// Default hard cached domains.
this . hardCachedDomains = [
this . losslessServiceWorkerRef . serviceWindowRef . location . origin ,
'https://unpkg.com' ,
'https://fonts.googleapis.com' ,
'https://fonts.gstatic.com'
] ;
// Default soft cached domains.
this . softCachedDomains = [
'https://assetbroker.'
] ;
this . _setupCache ( ) ;
}
/**
* Returns true if the given URL is in one of the hard cached domains.
*/
private isHardCached ( url : string ) : boolean {
return this . hardCachedDomains . some ( domain = > url . includes ( domain ) ) ;
}
/**
* Returns true if the given URL is in one of the soft cached domains.
*/
private isSoftCached ( url : string ) : boolean {
return this . softCachedDomains . some ( domain = > url . includes ( domain ) ) ;
}
/**
* Returns true if the given URL is cacheable (i.e. belongs to either hard or soft domains).
*/
private isCacheable ( url : string ) : boolean {
return this . isHardCached ( url ) || this . isSoftCached ( url ) ;
}
/**
* Sets up the fetch event listener and caching logic.
*/
private _setupCache = ( ) = > {
const createMatchRequest = ( requestArg : Request ) = > {
// lets create a matchRequest
const createMatchRequest = ( requestArg : Request ) : Request = > {
let matchRequest : Request ;
// For internal requests, use the original request.
if ( requestArg . url . startsWith ( this . losslessServiceWorkerRef . serviceWindowRef . location . origin ) ) {
// internal request
matchRequest = requestArg ;
} else {
// For external requests, create a new Request with appropriate CORS settings.
// For soft cached domains, we use 'no-cors' to keep the response opaque.
const isSoft = this . isSoftCached ( requestArg . url ) ;
const mode : RequestMode = isSoft ? 'no-cors' : 'cors' ;
matchRequest = new Request ( requestArg . url , {
. . . requestArg . clone ( ) ,
mode : 'cors'
method : requestArg.method ,
headers : requestArg.headers ,
mode ,
credentials : 'same-origin' ,
redirect : 'follow'
} ) ;
}
return matchRequest ;
} ;
/**
* c reates a 500 response
* C reates a 500 error response.
*/
const create500Response = async ( requestArg : Request , responseArg : Response ) = > {
const create500Response = async ( requestArg : Request , responseArg : Response ) : Promise < Response > = > {
return new Response (
`
<html>
@ -50,64 +106,60 @@ export class CacheManager {
</style>
</head>
<body>
<div class="note" >
<strong>serviceworker running, but status 500</strong><br >
</div >
serviceworker is unable to fetch this request <br>
Here is some info about the request/response pair: <br>
<br>
requestUrl: ${ requestArg . url } <br>
responseType: ${ responseArg . type } <br>
responseBody: ${ await responseArg . clone ( ) . text ( ) } <br>
<div class="note">
<strong>Service worker running, but status 500</strong><br >
</div >
Service worker is unable to fetch this request.<br >
Request URL: ${ requestArg . url } <br>
Response Type: ${ responseArg . type } <br>
Response Body: ${ await responseArg . clone ( ) . text ( ) } <br>
</body>
</html>
` ,
{
headers : {
"Content-Type" : "text/html"
} ,
headers : { "Content-Type" : "text/html" } ,
status : 500
}
) ;
} ;
// A list of local resources we always want to be cached.
this . losslessServiceWorkerRef . serviceWindowRef . addEventListener ( 'fetch' , async ( fetchEventArg : any ) = > {
// Lets block scopes we don't want to be passing through the serviceworker
const originalRequest : Request = fetchEventArg . request ;
const parsedUrl = new URL ( originalRequest . url ) ;
// Exclude specific hosts or paths from being handled by the service worker.
if (
parsedUrl . hostname . includes ( 'paddle.com' )
|| parsedUrl . hostname . includes ( 'paypal.com' )
|| parsedUrl . hostname . includes ( 'reception.lossless.one' )
|| parsedUrl . pathname . startsWith ( '/socket.io' )
|| originalRequest . url . startsWith ( 'https://umami.' )
parsedUrl . hostname . includes ( 'paddle.com' ) ||
parsedUrl . hostname . includes ( 'paypal.com' ) ||
parsedUrl . hostname . includes ( 'reception.lossless.one' ) ||
parsedUrl . pathname . startsWith ( '/socket.io' ) ||
originalRequest . url . startsWith ( 'https://umami.' )
) {
logger . log ( 'note' , ` s erviceworker not active for ${ parsedUrl . toString ( ) } ` ) ;
logger . log ( 'note' , ` S ervice worker not active for ${ parsedUrl . toString ( ) } ` ) ;
return ;
}
// lets continue for the rest
// Create a deferred response.
const done = plugins . smartpromise . defer < Response > ( ) ;
fetchEventArg . respondWith ( done . promise ) ;
if (
( originalRequest . method === 'GET' &&
( originalRequest . url . startsWith ( this . losslessServiceWorkerRef . serviceWindowRef . location . origin ) &&
! originalRequest . url . includes ( '/api/' ) &&
! originalRequ est . url . includ es ( 'smartserve/reloadcheck' ) ) ) ||
originalRequest . url . includes ( 'https://assetbroker.' ) ||
originalRequest . url . includes ( 'https://unpkg.com' ) ||
originalRequest . url . includes ( 'https://fonts.googleapis.com' ) ||
originalRequest . url . includes ( 'https://fonts.gstatic.com' )
) {
// lets see if things need to be updated
// not waiting here
this . losslessServiceWorkerRef . updateManager . checkUpdate ( this ) ;
// this code block is executed for local requests
// Determine if the request should be cached.
if ( originalRequest . method === 'GET' && this . isCacheable ( originalRequest . url ) ) {
// Check if running on Safari.
const userAgent = ( self . navigator && self . navigator . userAgent ) || "" ;
const isSafari = /Safari/ . t est( userAgent ) && ! /Chrome/ . t est ( userAgent ) ;
// For soft cached domains on Safari, bypass caching.
if ( this . isSoftCached ( originalRequest . url ) && isSafari ) {
logger . log ( 'info' , ` Safari detected – bypass caching for soft cached domain: ${ originalRequest . url } ` ) ;
const networkResponse = await fetch ( originalRequest ) . catch ( async err = > {
return await create500Response ( originalRequest , new Response ( err . message ) ) ;
} ) ;
done . resolve ( networkResponse ) ;
return ;
}
// For other cases, try serving from cache.
const matchRequest = createMatchRequest ( originalRequest ) ;
const cachedResponse = await caches . match ( matchRequest ) ;
if ( cachedResponse ) {
@ -115,111 +167,109 @@ export class CacheManager {
done . resolve ( cachedResponse ) ;
return ;
}
// in case there is no cached response
logger . log ( 'info' , ` NOTYETCACHED: trying to cache ${ matchRequest . url } ` ) ;
logger . log ( 'info' , ` NOTYETCACHED: fetching and caching ${ matchRequest . url } ` ) ;
const newResponse : Response = await fetch ( matchRequest ) . catch ( async err = > {
return await create500Response ( matchRequest , new Response ( err . message ) ) ;
} ) ;
// fill cache
// Put a copy of the response in the runtime cache.
// Do not cache responses that are not successful or are opaque (when not expected).
if ( newResponse . status > 299 || newResponse . type === 'opaque' ) {
logger . log (
'error' ,
` NOTCACHED: can' t cache response for ${ matchRequest . url } due to status ${
newResponse . status
} and type ${ newResponse . type } `
` NOTCACHED: canno t cache response for ${ matchRequest . url } (status: ${ newResponse . status } , type: ${ newResponse . type } ) `
) ;
done . resolve ( await create500Response ( matchRequest , newResponse ) ) ;
return ;
} else {
// Open the runtime cache.
const cache = await caches . open ( this . usedCacheNames . runtimeCacheName ) ;
const responseToPutTo Cache = newResponse . clone ( ) ;
const responseToCache = newResponse . clone ( ) ;
// Build new headers preserving all except caching-related headers.
const headers = new Headers ( ) ;
responseToPutTo Cache . headers . forEach ( ( value , key ) = > {
if (
value !== 'Cache-Control'
&& value !== 'cache-control'
&& value !== 'Expires'
&& value !== 'expires'
&& value !== 'Pragma'
&& value !== 'pragma'
) {
responseToCache . headers . forEach ( ( value , key ) = > {
if ( ! [ 'Cache-Control' , 'cache-control' , 'Expires' , 'expires' , 'Pragma' , 'pragma' ] . includes ( key ) ) {
headers . set ( key , value ) ;
}
} ) ;
// Prevent browser caching while allowing service worker caching
// Ensure necessary CORS headers are present.
if ( ! headers . has ( 'Access-Control-Allow-Origin' ) ) {
headers . set ( 'Access-Control-Allow-Origin' , '*' ) ;
}
if ( ! headers . has ( 'Access-Control-Allow-Methods' ) ) {
headers . set ( 'Access-Control-Allow-Methods' , 'GET, POST, PUT, DELETE, OPTIONS' ) ;
}
if ( ! headers . has ( 'Access-Control-Allow-Headers' ) ) {
headers . set ( 'Access-Control-Allow-Headers' , 'Content-Type' ) ;
}
// Set caching headers to prevent browser caching.
headers . set ( 'Cache-Control' , 'no-store, no-cache, must-revalidate, proxy-revalidate' ) ;
headers . set ( 'Pragma' , 'no-cache' ) ;
headers . set ( 'Expires' , '0' ) ;
headers . set ( 'Surrogate-Control' , 'no-store' ) ;
await cache . put ( matchRequest , new Response ( responseToPutToCache . body , {
. . . responseToPutToCache ,
headers
} ) ) ;
logger . log (
'ok' ,
` NOWCACHED: cached response for ${ matchRequest . url } for subsequent requests! `
) ;
// Read the response body as a blob (helps prevent issues with locked streams on Safari).
let newCachedResponse : Response ;
try {
const bodyBlob = await responseToCache . b lob ( ) ;
newCachedResponse = new Response ( bodyBlob , {
status : responseToCache.status ,
statusText : responseToCache.statusText ,
headers
} ) ;
} catch ( err ) {
newCachedResponse = newResponse ;
}
await cache . put ( matchRequest , newCachedResponse ) ;
logger . log ( 'ok' , ` NOWCACHED: response for ${ matchRequest . url } cached successfully ` ) ;
done . resolve ( newResponse ) ;
}
} else {
// this code block is executed for remote requests
logger . log (
'ok' ,
` NOTCACHED: not caching any responses for ${
originalRequest . url
} . Fetching from origin now... `
) ;
done . resolve (
await fetch ( originalRequest ) . catch ( async err = > {
return await create500Response ( originalRequest , new Response ( err . message ) ) ;
} )
) ;
// For non-cacheable requests, fetch directly from the network.
logger . log ( 'ok' , ` NOTCACHED: fetching ${ originalRequest . url } from origin (non-cacheable) ` ) ;
const networkResponse = await fetch ( originalRequest ) . catch ( async err = > {
return await create500Response ( originalRequest , new Response ( err . message ) ) ;
} ) ;
done . resolve ( networkResponse ) ;
}
} ) ;
}
} ;
/**
* update caches
* @param reasonArg
* Cleans all caches.
* Should only be run when a new service worker is activated.
* @param reasonArg A reason for the cache cleanup.
*/
/**
* cleans all caches
* should only be run when running a new service worker
* @param reasonArg
*/
public cleanCaches = async ( reasonArg = 'no reason given' ) = > {
public cleanCaches = async ( reasonArg = 'no reason given' ) : Promise < void > = > {
logger . log ( 'info' , ` MAJOR CACHEEVENT: cleaning caches now! Reason: ${ reasonArg } ` ) ;
const cacheNames = await caches . keys ( ) ;
const deletePromises = cacheNames . map ( cacheToDelete = > {
const deletePromise = caches . delete ( cacheToDelete ) ;
deletePromise . then ( ( ) = > {
logger . log ( 'ok' , ` Deleted cache ${ cacheToDelete } ` ) ;
} ) ;
return deletePromise ;
const deletePromise = caches . delete ( cacheToDelete ) ;
deletePromise . then ( ( ) = > {
logger . log ( 'ok' , ` Deleted cache ${ cacheToDelete } ` ) ;
} ) ;
return deletePromise ;
} ) ;
await Promise . all ( deletePromises ) ;
}
} ;
/**
* r evalidate cache
* R evalidates the runtime cache.
*/
public async revalidateCache() {
public async revalidateCache( ) : Promise < void > {
const runtimeCache = await caches . open ( this . usedCacheNames . runtimeCacheName ) ;
const cacheKeys = await runtimeCache . keys ( ) ;
for ( const requestArg of cacheKeys ) {
const cachedResponse = runtimeCache . match ( requestArg ) ;
// lets get a new response for comparison
const clonedRequest = requestArg . clone ( ) ;
const response = await plugins . smartpromise . timeoutWrap ( fetch ( clonedRequest ) , 5000 ) ; // Increased timeout for better mobile compatibility
const response = await plugins . smartpromise . timeoutWrap ( fetch ( clonedRequest ) , 5000 ) ;
if ( response && response . status >= 200 && response . status < 300 ) {
await runtimeCache . delete ( requestArg ) ;
await runtimeCache . put ( requestArg , response ) ;
}
}
}
}
}