@ -10,127 +10,76 @@ export class CacheManager {
runtimeCacheName : 'runtime'
} ;
/**
* Hard cached domains are always attempted to be cached.
* For example, your internal origin, fonts, CDNs, etc.
*/
public hardCachedDomains : string [ ] ;
/**
* Soft cached domains will be cached normally on non‑ Safari browsers,
* but on Safari caching is bypassed to avoid CORS issues.
*/
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 matches a hard cached domain.
*/
private isHardCached ( url : string ) : boolean {
return this . hardCachedDomains . some ( domain = > url . includes ( domain ) ) ;
}
/**
* Returns true if the given URL matches a soft cached domain.
*/
private isSoftCached ( url : string ) : boolean {
return this . softCachedDomains . some ( domain = > url . includes ( domain ) ) ;
}
/**
* Returns true if the given URL should be cached (hard or soft).
*/
private isCacheable ( url : string ) : boolean {
return this . isHardCached ( url ) || this . isSoftCached ( url ) ;
}
/**
* Creates a new Request with appropriate CORS settings.
*/
private createMatchRequest ( requestArg : Request ) : Request {
let matchRequest : Request ;
// For internal requests, use the original request.
if ( requestArg . url . startsWith ( this . losslessServiceWorkerRef . serviceWindowRef . location . origin ) ) {
matchRequest = requestArg ;
} else {
// For external requests, choose the request mode based on whether the URL is soft-cached.
const isSoft = this . isSoftCached ( requestArg . url ) ;
// For soft cached domains we use 'no-cors' to avoid CORS errors (at the expense of an opaque response).
const mode : RequestMode = isSoft ? 'no-cors' : 'cors' ;
matchRequest = new Request ( requestArg . url , {
method : requestArg.method ,
headers : requestArg.headers ,
mode ,
credentials : 'same-origin' ,
redirect : 'follow'
} ) ;
}
return matchRequest ;
}
/**
* Creates a 500 error response.
*/
private async create500Response ( requestArg : Request , responseArg : Response ) : Promise < Response > {
return new Response (
`
<html>
<head>
<style>
.note {
padding: 10px;
color: #fff;
background: #000;
border-bottom: 1px solid #e4002b;
margin-bottom: 20px;
}
</style>
</head>
<body>
<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" } ,
status : 500
}
) ;
}
/**
* 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 ;
if ( requestArg . url . startsWith ( this . losslessServiceWorkerRef . serviceWindowRef . location . origin ) ) {
// Internal request; use the original.
matchRequest = requestArg ;
} else {
// External request; create a new Request with appropriate CORS settings.
matchRequest = new Request ( requestArg . url , {
method : requestArg.method ,
headers : requestArg.headers ,
mode : 'cors' ,
credentials : 'same-origin' ,
redirect : 'follow'
} ) ;
}
return matchRequest ;
} ;
/**
* Creates a 500 error response.
*/
const create500Response = async ( requestArg : Request , responseArg : Response ) : Promise < Response > = > {
return new Response (
`
<html>
<head>
<style>
.note {
padding: 10px;
color: #fff;
background: #000;
border-bottom: 1px solid #e4002b;
margin-bottom: 20px;
}
</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>
</body>
</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' ) ||
@ -138,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 ;
}
@ -146,24 +95,21 @@ export class CacheManager {
const done = plugins . smartpromise . defer < Response > ( ) ;
fetchEventArg . respondWith ( done . promise ) ;
// Only handle GET requests for caching.
if ( originalRequest . method === 'GET' && this . isCacheable ( originalRequest . url ) ) {
// Determine if the browser is 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 entirely.
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 this . create500Response ( originalRequest , new Response ( err . message ) ) ;
} ) ;
done . resolve ( networkResponse ) ;
return ;
}
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 ) ;
// Try to serve from cache.
const matchRequest = this . createMatchRequest ( originalRequest ) ;
const matchRequest = createMatchRequest ( originalRequest ) ;
const cachedResponse = await caches . match ( matchRequest ) ;
if ( cachedResponse ) {
logger . log ( 'ok' , ` CACHED: found cached response for ${ matchRequest . url } ` ) ;
@ -171,96 +117,83 @@ 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 this . create500Response ( matchRequest , new Response ( err . message ) ) ;
return await create500Response ( matchRequest , new Response ( err . message ) ) ;
} ) ;
// If the response status indicate s an error, don' t cache.
if ( newResponse . status > 299 ) {
logger . log ( 'error' , ` NOTCACHED: response for ${ matchRequest . url } has status ${ newResponse . status } ` ) ;
done . resolve ( await this . create500Response ( matchRequest , newResponse ) ) ;
// If the response status is an error or the response is opaque , do no t cache it .
if ( newResponse . status > 299 || newResponse . type === 'opaque' ) {
logger . log (
'error' ,
` NOTCACHED: not caching response for ${ matchRequest . url } due to status ${ newResponse . status } and type ${ newResponse . type } `
) ;
// Simply return the network response without caching.
done . resolve ( newResponse ) ;
return ;
}
// Handle opaque responses separately.
if ( newResponse . type === 'opaque' ) {
// For soft-cached domains we expect opaque responses (from no-cors fetches).
if ( this . isSoftC ached ( matchRequest . url ) ) {
const cache = await caches . open ( this . usedCacheNames . runtimeCacheName ) ;
// Cache the opaque response as-is.
await cache . put ( matchRequest , newResponse . clone ( ) ) ;
logger . log ( 'ok' , ` NOWCACHED: cached opaque response for ${ matchRequest . url } ` ) ;
done . resolve ( newResponse ) ;
return ;
} else {
// If an opaque response comes from a non-soft domain, treat it as an error.
logger . log ( 'error' , ` NOTCACHED: opaque response received for non-soft domain ${ matchRequ est . url } ` ) ;
done . resolve ( await this . create500Respon se( matchRequest , newResponse ) ) ;
return ;
}
}
// For non-opaque responses, adjust headers and cache.
const cache = await caches . open ( this . usedCacheNames . runtimeCacheName ) ;
const responseToCache = newResponse . clone ( ) ;
const headers = new Headers ( ) ;
responseToCache . 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.
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' ) ;
}
// Prevent browser caching while allowing service worker 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 so we can create a new Response with modified headers.
let newCachedResponse : Response ;
try {
const bodyBlob = await responseToCache . blob ( ) ;
newCachedResponse = new Response ( bodyBlob , {
status : responseToCache.status ,
statusText : responseToCache.statusText ,
headers
} else {
// Cache the response.
const cache = await caches . open ( this . usedCacheNames . runtimeCacheName ) ;
const responseToPutToCache = newResponse . clone ( ) ;
const headers = new Headers ( ) ;
responseToPutToCache . headers . forE ach( ( value , key ) = > {
// Preserve all headers except caching-related ones.
if ( ! [
'Cache-Control' ,
'cache-control' ,
'Expires' ,
'expires' ,
'Pragma' ,
'pragma'
] . includ es( key ) ) {
headers . set ( key , value ) ;
}
} ) ;
} catch ( err ) {
newCachedResponse = newResponse ;
}
await cache . put ( matchRequest , newCachedResponse ) ;
logg er. log ( 'ok' , ` NOWCACHED: cached response for ${ matchRequest . url } ` ) ;
done . resolve ( newResponse ) ;
// Ensure CORS headers are present.
if ( ! head ers . 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' ) ;
}
// Prevent browser caching while allowing service worker 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! ` ) ;
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 this . 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 be run when a new service worker is activated.
* @param reasonArg A reason for the cache cleanup.
* Should only be run when a new service worker is activated.
*/
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 ( ) ;
@ -272,21 +205,22 @@ 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 ) ;
}
}
}
}
}