@ -10,76 +10,24 @@ 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 ) : Request = > {
// Create a matchRequest based on whether the request is internal or external.
let matchRequest : Request ;
// For internal requests, use the original request.
if ( requestArg . url . startsWith ( this . losslessServiceWorkerRef . serviceWindowRef . location . origin ) ) {
// Internal request; use the original.
matchRequest = requestArg ;
} else {
// For e xternal 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' ;
// E xternal request; create a new Request with appropriate CORS settings.
matchRequest = new Request ( requestArg . url , {
method : requestArg.method ,
headers : requestArg.headers ,
mode ,
mode : 'cors' ,
credentials : 'same-origin' ,
redirect : 'follow'
} ) ;
@ -107,27 +55,31 @@ export class CacheManager {
</head>
<body>
<div class="note">
<strong>S ervice worker running, but status 500</strong><br>
<strong>s erviceworker running, but status 500</strong><br>
</div>
S ervice 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>
s erviceworker 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>
</body>
</html>
` ,
{
headers : { "Content-Type" : "text/html" } ,
headers : {
"Content-Type" : "text/html"
} ,
status : 500
}
) ;
} ;
// Listen for fetch events.
this . losslessServiceWorkerRef . serviceWindowRef . addEventListener ( 'fetch' , async ( fetchEventArg : any ) = > {
// Block specific scopes.
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' ) ||
@ -135,7 +87,7 @@ export class CacheManager {
parsedUrl . pathname . startsWith ( '/socket.io' ) ||
originalRequest . url . startsWith ( 'https://umami.' )
) {
logger . log ( 'note' , ` S ervice worker not active for ${ parsedUrl . toString ( ) } ` ) ;
logger . log ( 'note' , ` s erviceworker not active for ${ parsedUrl . toString ( ) } ` ) ;
return ;
}
@ -143,23 +95,20 @@ export class CacheManager {
const done = plugins . smartpromise . defer < Response > ( ) ;
fetchEventArg . respondWith ( done . promise ) ;
// 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 ) ;
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' )
) {
// Check for updates asynchronously.
this . losslessServiceWorkerRef . updateManager . checkUpdate ( this ) ;
// 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.
// Try to serve from cache .
const matchRequest = createMatchRequest ( originalRequest ) ;
const cachedResponse = await caches . match ( matchRequest ) ;
if ( cachedResponse ) {
@ -168,33 +117,41 @@ export class CacheManager {
return ;
}
logger . log ( 'info' , ` NOTYETCACHED: fetching and caching ${ matchRequest . url } ` ) ;
// In case there is no cached response, fetch from the network.
logger . log ( 'info' , ` NOTYETCACHED: trying to cache ${ matchRequest . url } ` ) ;
const newResponse : Response = await fetch ( matchRequest ) . catch ( async err = > {
return await create500Response ( matchRequest , new Response ( err . message ) ) ;
} ) ;
// Do not cac he responses that are not successful or are opaque (when not expected) .
// If t he response status is an error or the response is opaque, do not cache it .
if ( newResponse . status > 299 || newResponse . type === 'opaque' ) {
logger . log (
'error' ,
` NOTCACHED: can not cache response for ${ matchRequest . url } ( status: ${ newResponse . status } , type: ${ newResponse . type } ) `
` NOTCACHED: not caching response for ${ matchRequest . url } due to status ${ newResponse . status } and type ${ newResponse . type } `
) ;
done . resolve ( await create500Response ( matchRequest , newResponse ) ) ;
// Simply return the network response without caching.
done . resolve ( newResponse ) ;
return ;
} else {
// Open the runtime cach e.
// Cache the respons e.
const cache = await caches . open ( this . usedCacheNames . runtimeCacheName ) ;
const responseToCache = newResponse . clone ( ) ;
// Build new headers preserving all except caching-related headers.
const responseToPutTo Cache = newResponse . clone ( ) ;
const headers = new Headers ( ) ;
responseToCache . headers . forEach ( ( value , key ) = > {
if ( ! [ 'Cache-Control' , 'cache-control' , 'Expires' , 'expires' , 'Pragma' , 'pragma' ] . includes ( key ) ) {
responseToPutTo Cache . headers . forEach ( ( value , key ) = > {
// Preserve all headers except caching-related ones.
if ( ! [
'Cache-Control' ,
'cache-control' ,
'Expires' ,
'expires' ,
'Pragma' ,
'pragma'
] . includes ( key ) ) {
headers . set ( key , value ) ;
}
} ) ;
// Ensure necessary CORS headers are present.
// Ensure CORS headers are present.
if ( ! headers . has ( 'Access-Control-Allow-Origin' ) ) {
headers . set ( 'Access-Control-Allow-Origin' , '*' ) ;
}
@ -204,46 +161,39 @@ export class CacheManager {
if ( ! headers . has ( 'Access-Control-Allow-Headers' ) ) {
headers . set ( 'Access-Control-Allow-Headers' , 'Content-Type' ) ;
}
// Set caching headers to prevent brows er caching.
// Prevent browser caching while allowing service work er 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' ) ;
// Read the response body as a blob (helps prevent issues with locked streams on Safari).
let newCachedResponse : Respons e;
try {
const bodyBlob = await responseToCache . blob ( ) ;
newC achedR esponse = 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 ` ) ;
await cache . put ( matchRequest , new Response ( responseToPutToCache . body , {
. . . responseToPutToCach e ,
headers
} ) ) ;
logger . log ( 'ok' , ` NOWCACHED: c ached r esponse for ${ matchRequest . url } for subsequent requests! ` ) ;
done . resolve ( newResponse ) ;
}
} else {
// 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 ) ;
// For remote requests not intended for caching , fetch directly from the origin .
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 ) ) ;
} )
) ;
}
} ) ;
} ;
}
/**
* Cleans all caches.
* Should only be run when a new service worker is activated.
* @param reasonArg A reason for the cache cleanup.
*/
public cleanCaches = async ( reasonArg = 'no reason given' ) : Promise < void > = > {
public cleanCaches = async ( reasonArg = 'no reason given' ) = > {
logger . log ( 'info' , ` MAJOR CACHEEVENT: cleaning caches now! Reason: ${ reasonArg } ` ) ;
const cacheNames = await caches . keys ( ) ;
@ -255,17 +205,18 @@ export class CacheManager {
return deletePromise ;
} ) ;
await Promise . all ( deletePromises ) ;
} ;
}
/**
* Revalidates the runtime cache.
*/
public async revalidateCache( ) : Promise < void > {
public async revalidateCache() {
const runtimeCache = await caches . open ( this . usedCacheNames . runtimeCacheName ) ;
const cacheKeys = await runtimeCache . keys ( ) ;
for ( const requestArg of cacheKeys ) {
// Fetch a new response for comparison.
const clonedRequest = requestArg . clone ( ) ;
const response = await plugins . smartpromise . timeoutWrap ( fetch ( clonedRequest ) , 5000 ) ;
const response = await plugins . smartpromise . timeoutWrap ( fetch ( clonedRequest ) , 5000 ) ; // Increased timeout for better mobile compatibility
if ( response && response . status >= 200 && response . status < 300 ) {
await runtimeCache . delete ( requestArg ) ;
await runtimeCache . put ( requestArg , response ) ;