2021-04-06 17:18:37 +00:00
/ *
* LibResilient Service Worker .
* /
// initialize the LibResilientConfig array
//
// this also sets some sane defaults,
2022-01-22 16:56:24 +00:00
// which then can be modified via config.json
2021-04-06 17:18:37 +00:00
if ( typeof self . LibResilientConfig !== 'object' || self . LibResilientConfig === null ) {
self . LibResilientConfig = {
2023-11-10 21:08:03 +00:00
2021-04-06 17:18:37 +00:00
// how long do we wait before we decide that a plugin is unresponsive,
// and move on?
defaultPluginTimeout : 10000 ,
2024-02-23 17:58:45 +00:00
// how long should LibResilient wait before displaying the "still loading" screen
// to the user if the request mode is "navigate"?
//
// NOTICE: the still-loading screen is only used if this setting is > 0
// NOTICE: *and* there is a stashing plugin (normally, `cache`) configured and enabled in the config
// NOTICE: this is done to avoid loops -- otherwise, user would find themselves in a (manual, but still) loop
stillLoadingTimeout : 5000 ,
2023-11-10 21:08:03 +00:00
2021-04-06 17:18:37 +00:00
// plugins settings namespace
//
// this defines which plugins get loaded,
// and the order in which they are deployed to try to retrieve content
// assumption: plugin path = ./plugins/<plugin-name>.js
//
// this relies on JavaScript preserving the insertion order for properties
// https://stackoverflow.com/a/5525820
2021-09-15 20:06:04 +00:00
plugins : [ {
name : 'fetch'
} , {
name : 'cache'
}
] ,
2023-11-10 21:08:03 +00:00
2021-04-06 17:18:37 +00:00
// which components should be logged?
// this is an array of strings, components not listed here
// will have their debug output disabled
//
// by default, the service worker and all enabled plugins
// (so, all components that are used)
loggedComponents : [
'service-worker' ,
'fetch' ,
2021-09-19 10:04:19 +00:00
'cache'
2023-10-07 04:58:48 +00:00
] ,
2023-11-10 21:08:03 +00:00
2023-10-07 04:58:48 +00:00
// should we normalize query params?
//
// this usually makes sense: a request to example.com/?a=a&b=b is
// exactly equivalent to example.com/?b=b&a=a
//
// but in case a given website does something weird with query params...
// ..normalization can be disabled here
2023-11-10 21:08:03 +00:00
normalizeQueryParams : true ,
// do we want to use content-based MIME type detection using an external library?
//
// some plugins (for example, based on IPFS), receive content without a content type,
// because the transport simply does not support it. so we need to find a way to
// figure out the content type on our own -- that's done by the guessMimeType() function
//
// it can do this based on the extension of the file being requested,
// but that is a limited, imperfect approach.
//
// it can also load an external library (currently that's `file-type`), and guess
// the content type based on it. this approach is more exact, works for paths that
// do not have an "extension", and works for many more MIME types than the alternative.
//
// however, it is also slower (content needs to be read, inspected, and compared to a lot
// of signatures), and relies on an external library that needs to be distributed along
// with LibResilient.
//
// so, since it is not needed in case of most plugins, it is disabled by default.
useMimeSniffingLibrary : false
2021-04-06 17:18:37 +00:00
}
}
/ * *
* internal logging facility
*
* component - name of the component being logged about
* if the component is not in the LibResilientConfig . loggedComponents array ,
* message will not be displayed
* items - the rest of arguments will be passed to console . debug ( )
* /
self . log = function ( component , ... items ) {
2022-01-24 13:55:40 +00:00
if ( ( 'LibResilientConfig' in self ) && ( 'loggedComponents' in self . LibResilientConfig ) && ( self . LibResilientConfig . loggedComponents != undefined ) ) {
if ( self . LibResilientConfig . loggedComponents . indexOf ( component ) >= 0 ) {
console . debug ( ` LibResilient [COMMIT_UNKNOWN, ${ component } ] :: ` , ... items )
}
2021-04-06 17:18:37 +00:00
}
}
2023-11-10 21:08:03 +00:00
// Map() of file extensions to MIME types for the guessing game below
// this is by no means complete, and focuses mainly on formats that
// are important on the Web
let ext _to _mime = new Map ( [
[ 'htm' , 'text/html' ] ,
[ 'html' , 'text/html' ] ,
[ 'css' , 'text/css' ] ,
[ 'js' , 'text/javascript' ] ,
[ 'json' , 'application/json' ] ,
[ 'svg' , 'image/svg+xml' ] ,
[ 'ico' , 'image/x-icon' ] ,
[ 'gif' , 'image/gif' ] ,
[ 'png' , 'image/png' ] ,
[ 'jpg' , 'image/jpeg' ] ,
[ 'jpeg' , 'image/jpeg' ] ,
[ 'jpe' , 'image/jpeg' ] ,
[ 'jfif' , 'image/jpeg' ] ,
[ 'pjpeg' , 'image/jpeg' ] ,
[ 'pjp' , 'image/jpeg' ] ,
[ 'webp' , 'image/webp' ] ,
[ 'avi' , 'video/avi' ] ,
[ 'mp4' , 'video/mp4' ] ,
[ 'mp2' , 'video/mpeg' ] ,
[ 'mp3' , 'audio/mpeg' ] ,
[ 'mpa' , 'video/mpeg' ] ,
[ 'pdf' , 'application/pdf' ] ,
[ 'txt' , 'text/plain' ] ,
[ 'ics' , 'text/calendar' ] ,
[ 'jsonld' , 'application/ld+json' ] ,
[ 'mjs' , 'text/javascript' ] ,
[ 'oga' , 'audio/ogg' ] ,
[ 'ogv' , 'video/ogg' ] ,
[ 'ogx' , 'application/ogg' ] ,
[ 'opus' , 'audio/opus' ] ,
[ 'otf' , 'font/otf' ] ,
[ 'ts' , 'video/mp2t' ] ,
[ 'ttf' , 'font/ttf' ] ,
[ 'weba' , 'audio/webm' ] ,
[ 'webm' , 'video/webm' ] ,
[ 'webp' , 'image/webp' ] ,
[ 'woff' , 'font/woff' ] ,
[ 'woff2' , 'font/woff2' ] ,
[ 'xhtml' , 'application/xhtml+xml' ] ,
[ 'xml' , 'application/xml' ]
] )
// preparing the variable for the MIME detection module
// in case we want to use it
let detectMimeFromBuffer = null
/ * *
* guess the MIME type , based on content and path extension
*
* important : according to RFC 7231 we should not set Content - Type if we ' re not sure !
* https : //www.rfc-editor.org/rfc/rfc7231#section-3.1.1.5
*
* @ param ext - the extension of the path content was fetched as
* @ param content - the content itself
* @ returns string containing the MIME type , or empty string if guessing failed
* /
self . guessMimeType = async function ( ext , content ) {
// if we have file-type library loaded, that means that useMimeSniffingLibrary config field is set to true
// and that we were able to load file-type.js
//
// in other words, we want to use it, we can use it -- so use it!
if ( detectMimeFromBuffer !== null ) {
let ft = undefined
try {
ft = await detectMimeFromBuffer ( content )
} catch ( e ) {
self . log ( 'service-worker' , "+-- error while trying to guess MIME type based on content:" , e ) ;
}
// did we actually get anything?
if ( ( ft !== undefined ) && ( typeof ft === "object" ) && ( "mime" in ft ) ) {
// yup!
self . log ( 'service-worker' , "+-- guessed MIME type based on content: " + ft . mime ) ;
return ft . mime ;
} else {
self . log ( 'service-worker' , "+-- unable to guess MIME type based on content." )
}
}
// an empty string is in our case equivalent to not setting the Content-Type
// as `new Blob()` with no `type` option set ends up having type set to an empty string
if ( ext _to _mime . has ( ext ) ) {
self . log ( 'service-worker' , "+-- guessed MIME type based on extension: " + ext _to _mime . get ( ext ) ) ;
return ext _to _mime . get ( ext )
}
// if we're unable to guess the MIME type, we need to return an empty string
self . log ( 'service-worker' , " +-- unable to guess the MIME type" ) ;
return "" ;
}
2021-04-06 17:18:37 +00:00
2022-01-24 13:55:40 +00:00
/ * *
2022-02-02 01:14:31 +00:00
* verifying a config data object
*
* we are * NOT * checking for fields that are unknown / unexpected
* as resilience is more important than conrrectness here :
* we do want the config to load if at all it can be loaded ,
* an extra field or two is not a problem here
2022-01-24 13:55:40 +00:00
*
* cdata - config data to verify
* /
2022-02-02 01:14:31 +00:00
let verifyConfigData = ( cdata ) => {
2024-02-11 23:27:56 +00:00
// cdata needs to be an object
if ( typeof cdata !== "object" || cdata === null ) {
self . log ( 'service-worker' , 'fetched config does not contain a valid JSON object' )
return false ;
}
2022-01-24 13:55:40 +00:00
// basic check for the plugins field
if ( ! ( "plugins" in cdata ) || ! Array . isArray ( cdata . plugins ) ) {
self . log ( 'service-worker' , 'fetched config does not contain a valid "plugins" field' )
return false ;
}
// basic check for the loggedComponents
if ( ! ( "loggedComponents" in cdata ) || ! Array . isArray ( cdata . loggedComponents ) ) {
self . log ( 'service-worker' , 'fetched config does not contain a valid "loggedComponents" field' )
return false ;
}
2022-01-26 00:26:16 +00:00
// defaultPluginTimeout is optional
2022-01-25 00:28:41 +00:00
if ( "defaultPluginTimeout" in cdata ) {
if ( ! Number . isInteger ( cdata . defaultPluginTimeout ) ) {
self . log ( 'service-worker' , 'fetched config contains invalid "defaultPluginTimeout" data (integer expected)' )
return false ;
}
}
2024-02-23 17:58:45 +00:00
// stillLoadingTimeout is optional
if ( "stillLoadingTimeout" in cdata ) {
if ( ! Number . isInteger ( cdata . stillLoadingTimeout ) ) {
self . log ( 'service-worker' , 'fetched config contains invalid "stillLoadingTimeout" data (integer expected)' )
return false ;
}
}
2023-10-07 17:08:05 +00:00
// normalizeQueryParams is optional
if ( "normalizeQueryParams" in cdata ) {
2023-11-10 21:08:03 +00:00
if ( cdata . normalizeQueryParams !== true && cdata . normalizeQueryParams !== false ) {
2023-10-07 17:08:05 +00:00
self . log ( 'service-worker' , 'fetched config contains invalid "normalizeQueryParams" data (boolean expected)' )
return false ;
}
}
2023-11-10 21:08:03 +00:00
// useMimeSniffingLibrary is optional
if ( "useMimeSniffingLibrary" in cdata ) {
if ( cdata . useMimeSniffingLibrary !== true && cdata . useMimeSniffingLibrary !== false ) {
self . log ( 'service-worker' , 'fetched config contains invalid "useMimeSniffingLibrary" data (boolean expected)' )
return false ;
}
}
2022-01-24 13:55:40 +00:00
// we're good
return true ;
}
2022-02-02 01:28:13 +00:00
/ * *
* cache the ` config.json ` response , wherever from we got it
*
* configURL - url of the config file
* cresponse - response we ' re caching
* /
2024-02-18 00:43:42 +00:00
let cacheConfigJSON = async ( configURL , cresponse , use _source ) => {
2022-02-02 01:28:13 +00:00
try {
2024-02-18 00:43:42 +00:00
var cache = await caches . open ( use _source )
2022-02-02 01:28:13 +00:00
await cache . put ( configURL , cresponse )
2024-02-18 00:43:42 +00:00
self . log ( 'service-worker' , ` config cached in cache: ${ use _source } . ` )
2022-02-02 01:28:13 +00:00
} catch ( e ) {
2024-02-18 00:43:42 +00:00
self . log ( 'service-worker' , ` failed to cache config in cache ${ use _source } : ${ e } ` )
2022-02-02 01:28:13 +00:00
}
}
2022-02-06 23:06:43 +00:00
/ * *
* get config JSON and verify it ' s valid
*
* cresponse - the Response object to work with
* /
let getConfigJSON = async ( cresponse ) => {
if ( cresponse == undefined ) {
self . log ( 'service-worker' , 'config.json response is undefined' )
return false ;
}
if ( cresponse . status != 200 ) {
self . log ( 'service-worker' , ` config.json response status is not 200: ${ cdata . status } ${ cdata . statusText } ) ` )
return false ;
}
2022-02-07 03:11:35 +00:00
// cloning the response before applying json()
// so that we can cache the response later
2022-02-06 23:06:43 +00:00
var cdata = await cresponse . clone ( ) . json ( )
if ( verifyConfigData ( cdata ) ) {
return cdata ;
}
return false ;
}
2024-01-29 23:22:15 +00:00
/ * *
* execute on the configuration
*
* load plugin modules , making constructors available
* cycle through the plugin config instantiating plugins and their dependencies
* /
2024-02-01 01:11:14 +00:00
let executeConfig = ( config ) => {
2024-01-29 23:22:15 +00:00
2024-02-01 01:11:14 +00:00
// working on a copy of the plugins config so that config.plugins remains unmodified
// in case we need it later (for example, when re-loading the config)
let pluginsConfig = [ ... config . plugins ]
2024-02-11 23:27:56 +00:00
// this is the stash for plugins that need dependencies instantiated first
let dependentPlugins = new Array ( )
2024-01-29 23:22:15 +00:00
2024-02-23 17:58:45 +00:00
// do we have any stashing plugins enabled?
// this is important for the still-loading screen
let stashingEnabled = false
2024-02-11 23:27:56 +00:00
// only now load the plugins (config.json could have changed the defaults)
while ( pluginsConfig . length > 0 ) {
2024-01-29 23:22:15 +00:00
2024-02-11 23:27:56 +00:00
// get the first plugin config from the array
let pluginConfig = pluginsConfig . shift ( )
self . log ( 'service-worker' , ` handling plugin type: ${ pluginConfig . name } ` )
2024-01-29 23:22:15 +00:00
2024-02-11 23:27:56 +00:00
// load the relevant plugin script (if not yet loaded)
if ( ! LibResilientPluginConstructors . has ( pluginConfig . name ) ) {
self . log ( 'service-worker' , ` ${ pluginConfig . name } : loading plugin's source ` )
self . importScripts ( ` ./plugins/ ${ pluginConfig . name } /index.js ` )
}
// do we have any dependencies we should handle first?
if ( typeof pluginConfig . uses !== "undefined" ) {
self . log ( 'service-worker' , ` ${ pluginConfig . name } : ${ pluginConfig . uses . length } dependencies found ` )
2024-01-29 23:22:15 +00:00
2024-02-11 23:27:56 +00:00
// move the dependency plugin configs to LibResilientConfig to be worked on next
for ( let i = ( pluginConfig . uses . length ) ; i -- ; i >= 0 ) {
self . log ( 'service-worker' , ` ${ pluginConfig . name } : dependency found: ${ pluginConfig . uses [ i ] . name } ` )
// put the plugin config in front of the plugin configs array
pluginsConfig . unshift ( pluginConfig . uses [ i ] )
// set each dependency plugin config to false so that we can keep track
// as we fill those gaps later with instantiated dependency plugins
pluginConfig . uses [ i ] = false
2024-02-01 01:11:14 +00:00
}
2024-01-29 23:22:15 +00:00
2024-02-11 23:27:56 +00:00
// stash the plugin config until we have all the dependencies handled
self . log ( 'service-worker' , ` ${ pluginConfig . name } : not instantiating until dependencies are ready ` )
dependentPlugins . push ( pluginConfig )
// move on to the next plugin config, which at this point will be
// the first of dependencies for the plugin whose config got stashed
continue ;
}
do {
// if the plugin is not enabled, no instantiation for it nor for its dependencies
// if the pluginConfig does not have an "enabled" field, it should be assumed to be "true"
if ( ( "enabled" in pluginConfig ) && ( pluginConfig . enabled != true ) ) {
self . log ( 'service-worker' , ` skipping ${ pluginConfig . name } instantiation: plugin not enabled (dependencies will also not be instantiated) ` )
pluginConfig = dependentPlugins . pop ( )
if ( pluginConfig !== undefined ) {
let didx = pluginConfig . uses . indexOf ( false )
pluginConfig . uses . splice ( didx , 1 )
2024-01-29 23:22:15 +00:00
}
continue ;
}
2024-02-11 23:27:56 +00:00
// instantiate the plugin
let plugin = LibResilientPluginConstructors . get ( pluginConfig . name ) ( self , pluginConfig )
self . log ( 'service-worker' , ` ${ pluginConfig . name } : instantiated ` )
2024-01-29 23:22:15 +00:00
2024-02-23 17:58:45 +00:00
// is this a stashing plugin?
// we need at least one stashing plugin to be able to use the still-loading screen
stashingEnabled = stashingEnabled || ( ( "stash" in plugin ) && ( typeof plugin . stash === "function" ) )
2024-02-11 23:27:56 +00:00
// do we have a stashed plugin that requires dependencies?
if ( dependentPlugins . length === 0 ) {
// no we don't; so, this plugin goes directly to the plugin list
self . LibResilientPlugins . push ( plugin )
// we're done here
2024-02-23 17:58:45 +00:00
self . log ( 'service-worker' , ` ${ pluginConfig . name } : no dependent plugins left, pushing directly to LibResilientPlugins ` )
2024-02-11 23:27:56 +00:00
break ;
2024-01-29 23:22:15 +00:00
}
2024-02-11 23:27:56 +00:00
// at this point clearly there is at least one element in dependentPlugins
// so we can safely assume that the freshly instantiated plugin is a dependency
//
// in that case let's find the first empty spot for a dependency
let didx = dependentPlugins [ dependentPlugins . length - 1 ] . uses . indexOf ( false )
// assign the freshly instantiated plugin as that dependency
dependentPlugins [ dependentPlugins . length - 1 ] . uses [ didx ] = plugin
self . log ( 'service-worker' , ` ${ pluginConfig . name } : assigning as dependency (# ${ didx } ) to ${ dependentPlugins [ dependentPlugins . length - 1 ] . name } ` )
// was this the last one?
if ( didx >= dependentPlugins [ dependentPlugins . length - 1 ] . uses . length - 1 ) {
// yup, last one!
self . log ( 'service-worker' , ` ${ pluginConfig . name } : this was the last dependency of ${ dependentPlugins [ dependentPlugins . length - 1 ] . name } ` )
// we can now proceed to instantiate the last element of dependentPlugins
pluginConfig = dependentPlugins . pop ( )
continue
2024-02-01 01:11:14 +00:00
}
2024-02-11 23:27:56 +00:00
// it is not the last one, so there should be more dependency plugins to instantiate first
// before we can instantiate the last of element of dependentPlugins
// but that requires the full treatment, including checing the `uses` field for their configs
self . log ( 'service-worker' , ` ${ pluginConfig . name } : not yet the last dependency of ${ dependentPlugins [ dependentPlugins . length - 1 ] . name } ` )
pluginConfig = false
2024-01-29 23:22:15 +00:00
2024-02-11 23:27:56 +00:00
// if pluginConfig is not false, rinse-repeat the plugin instantiation steps
// since we are dealing with the last element of dependentPlugins
} while ( ( pluginConfig !== false ) && ( pluginConfig !== undefined ) )
2024-01-29 23:22:15 +00:00
2024-01-31 15:41:58 +00:00
}
2024-02-23 17:58:45 +00:00
// do we want to use MIME type guessing based on content?
2024-02-11 23:27:56 +00:00
// dealing with this at the very end so that we know we can safely set detectMimeFromBuffer
// and not need to re-set it back in case anything fails
if ( config . useMimeSniffingLibrary === true ) {
// we do not want to hit a NetworkError and end up using the default config
// much better to end up not using the fancy MIME type detection in such a case
try {
// we do! load the external lib
self . importScripts ( ` ./lib/file-type.js ` )
} catch ( e ) {
self . log ( 'service-worker' , ` error when fetching external MIME sniffing library: ${ e . message } ` )
}
if ( typeof fileType !== 'undefined' && "fileTypeFromBuffer" in fileType ) {
detectMimeFromBuffer = fileType . fileTypeFromBuffer
self . log ( 'service-worker' , 'loaded external MIME sniffing library' )
} else {
self . log ( 'service-worker' , 'failed to load external MIME sniffing library!' )
}
}
2024-02-23 17:58:45 +00:00
// finally -- if we do not have *any* stashing plugins enabled,
// we need to disable the still-loading screen
if ( ! stashingEnabled ) {
config . stillLoadingTimeout = 0
self . log ( 'service-worker' , 'still-loading screen disabled, as there are no stashing plugins enabled' )
}
2024-02-11 23:27:56 +00:00
// we're good!
return true ;
2024-01-29 23:22:15 +00:00
}
2024-01-31 21:46:29 +00:00
// flag signifying if the SW has been initialized already
2022-01-26 00:26:16 +00:00
var initDone = false
2022-01-24 13:55:40 +00:00
2021-04-06 17:18:37 +00:00
// load the plugins
2022-01-22 16:56:24 +00:00
let initServiceWorker = async ( ) => {
2022-01-26 00:26:16 +00:00
// if init has already been done, skip!
if ( initDone ) {
self . log ( 'service-worker' , 'skipping service-worker init, already done' )
return false ;
}
// everything in a try-catch block
// so that we get an informative message if there's an error
2021-09-19 10:04:19 +00:00
try {
2022-01-22 16:56:24 +00:00
2022-02-07 03:11:35 +00:00
// we'll need this later
2024-02-01 01:11:14 +00:00
let cresponse = null
let config = null
2022-02-07 03:11:35 +00:00
2022-01-22 16:56:24 +00:00
// self.registration.scope contains the scope this service worker is registered for
// so it makes sense to pull config from `config.json` file directly under that location
2024-02-01 01:11:14 +00:00
// TODO: this should probably be configurable somehow
let configURL = self . registration . scope + "config.json"
// clean version of LibResilientPlugins
// NOTICE: this assumes LibResilientPlugins is not ever used *before* this runs
// NOTICE: this assumption seems to hold currently, but noting for clarity
self . LibResilientPlugins = new Array ( )
// create the LibResilientPluginConstructors map
// the global... hack is here so that we can run tests; not the most elegant
// TODO: find a better way
self . LibResilientPluginConstructors = self . LibResilientPluginConstructors || new Map ( )
// point backup of LibResilientPluginConstructors, in case we need to roll back
// this is used during cleanup in executeConfig()
// TODO: handle in a more elegant way
let lrpcBackup = new Map ( self . LibResilientPluginConstructors )
2024-02-18 00:43:42 +00:00
// config sources to try
let config _sources = [ 'v1' , 'v1:verified' , 'fetch' ]
2024-02-01 01:11:14 +00:00
// keep track
let config _executed = false
2024-02-18 00:43:42 +00:00
let use _source = false
2024-02-01 01:11:14 +00:00
do {
2022-02-06 23:06:43 +00:00
2024-02-01 01:11:14 +00:00
// init config data var
let cdata = false
2022-02-06 23:06:43 +00:00
2024-02-01 01:11:14 +00:00
// where are we getting the config.json from this time?
2024-02-18 00:43:42 +00:00
// we eitehr get a string (name of a cache, or "fetch" for simple fetch()),
// or undefined (signifying need to use the defaults)
use _source = config _sources . shift ( )
2022-02-06 23:06:43 +00:00
2024-02-01 01:11:14 +00:00
try {
2022-02-06 23:06:43 +00:00
2024-02-18 00:43:42 +00:00
// are we using any kind of source, or fall back to defaults?
if ( typeof use _source === 'string' ) {
2024-02-01 01:11:14 +00:00
2024-02-18 00:43:42 +00:00
// some kind of source! cache?
if ( ( use _source === 'v1' ) || ( use _source === 'v1:verified' ) ) {
self . log ( 'service-worker' , ` retrieving config.json from cache: ${ use _source } . ` )
cresponse = await caches . match ( configURL , { cacheName : use _source } )
// bail early if we got nothing
if ( cresponse === undefined ) {
self . log ( 'service-worker' , ` config.json not found in cache: ${ use _source } . ` )
continue
}
// regular fetch?
// (we don't have any plugin transports at this point, obviously...)
} else if ( use _source === "fetch" ) {
self . log ( 'service-worker' , ` retrieving config.json using fetch(). ` )
cresponse = await fetch ( configURL )
// that should not happen!
} else {
throw new Error ( ` unknown config.json source: ${ use _source } ; this should never happen! ` )
}
// extract the retrieved JSON and verify it
cdata = await getConfigJSON ( cresponse )
// do we have anything to work with?
if ( cdata === false ) {
// cached config.json was invalid; no biggie, try another cache, or fetch()
if ( ( use _source === 'v1' ) || ( use _source === 'v1:verified' ) ) {
self . log ( 'service-worker' , ` cached config.json is not valid; cache: ${ use _source } ` )
// if that was a fetch() config, we need to fall-back to defaults!
} else {
self . log ( 'service-worker' , ` fetched config.json is not valid; using defaults ` )
}
// no valid config means we need to go around again
2024-02-01 01:11:14 +00:00
continue
2024-02-18 00:43:42 +00:00
// we good!
} else {
self . log ( 'service-worker' , ` valid-looking config.json retrieved. ` )
2024-02-01 01:11:14 +00:00
}
2024-02-18 00:43:42 +00:00
// anything else just means "use defaults"
2024-02-01 01:11:14 +00:00
} else {
2024-02-18 00:43:42 +00:00
self . log ( 'service-worker' , ` retrieving config.json failed completely, using built-in defaults. ` )
// defaults means an empty object here,
// we're merging with actual defaults later on
cdata = { }
2024-02-01 01:11:14 +00:00
}
2022-02-06 23:06:43 +00:00
2024-02-01 01:11:14 +00:00
// exception? no bueno!
} catch ( e ) {
self . log ( 'service-worker' , ` exception when trying to retrieve config.json: ${ e . message } ` )
2024-02-18 00:43:42 +00:00
continue
2022-01-24 13:55:40 +00:00
}
2024-02-01 01:11:14 +00:00
2024-02-18 00:43:42 +00:00
// merge configs — either with the retrieved JSON,
// or with an empty object if using defaults
2024-02-01 01:11:14 +00:00
config = { ... self . LibResilientConfig , ... cdata }
// try executing the config
2024-02-11 23:27:56 +00:00
// we want to catch any and all possible errors here
try {
config _executed = executeConfig ( config )
// exception? no bueno
} catch ( e ) {
// inform
self . log ( 'service-worker' , ` error while executing config: ${ e . message } ` )
// cleanup after a failed config execution
self . LibResilientPluginConstructors = new Map ( lrpcBackup )
self . LibResilientPlugins = new Array ( )
// we are not good
config _executed = false ;
}
2022-02-06 23:06:43 +00:00
2024-02-01 01:11:14 +00:00
// if we're using the defaults, and yet loading of the config failed
// something is massively wrong
2024-02-18 00:43:42 +00:00
if ( ( use _source === undefined ) && ( config _executed === false ) ) {
2024-02-01 01:11:14 +00:00
// this really should never happen
throw new Error ( 'Failed to load the default config; this should never happen!' )
}
2021-09-18 22:52:54 +00:00
2024-02-01 01:11:14 +00:00
// NOTICE: endless loop alert?
// NOTICE: this is not an endless loop because cresponse can only become false if we're using the default config
// NOTICE: and that single case is handled directly above
} while ( ! config _executed )
2021-09-15 20:06:04 +00:00
2024-02-01 01:11:14 +00:00
// we're good
self . LibResilientConfig = config
self . log ( 'service-worker' , 'config loaded.' )
2021-09-15 20:06:04 +00:00
2024-02-01 23:46:10 +00:00
// we're good, let's cache the config as verified if we need to
// that is, if it comes from the "v1" cache...
2024-02-18 00:43:42 +00:00
if ( use _source === "v1" ) {
2024-02-01 23:46:10 +00:00
self . log ( 'service-worker' , ` successfully loaded config.json; caching in cache: v1:verified ` )
2024-02-11 23:27:56 +00:00
await cacheConfigJSON ( configURL , cresponse , 'v1:verified' )
// we used the v1:verified cache; we should cache config.json into the v1 cache
// as that will speed things up a bit next time we need to load the service worker
2024-02-18 00:43:42 +00:00
} else if ( use _source === "v1:verified" ) {
2024-02-11 23:27:56 +00:00
self . log ( 'service-worker' , ` successfully loaded config.json; caching in cache: v1 ` )
await cacheConfigJSON ( configURL , cresponse , 'v1' )
// or, was fetch()-ed and valid (no caching if we're going with defaults, obviously)
2024-02-18 00:43:42 +00:00
} else if ( use _source === "fetch" ) {
self . log ( 'service-worker' , ` successfully loaded config.json; caching in caches: v1, v1:verified ` )
2024-02-01 23:46:10 +00:00
// we want to cache to both, so that:
// 1. we get the extra bit of performance from using the v1 cache that is checked first
// 2. but we get the verified config already in the v1:verified cache for later
2024-02-11 23:27:56 +00:00
await cacheConfigJSON ( configURL , await cresponse . clone ( ) , 'v1' )
await cacheConfigJSON ( configURL , cresponse , 'v1:verified' )
2024-01-31 21:46:29 +00:00
}
2021-09-18 22:52:54 +00:00
2022-01-22 16:56:24 +00:00
// inform
2022-02-07 03:11:35 +00:00
self . log ( 'service-worker' , ` service worker initialized. \n strategy in use: ${ self . LibResilientPlugins . map ( p => p . name ) . join ( ', ' ) } ` )
2022-01-26 00:26:16 +00:00
initDone = true ;
2022-02-07 03:11:35 +00:00
// regardless how we got the config file, if it's older than 24h...
2024-02-01 01:11:14 +00:00
if ( ( cresponse !== false ) && ( new Date ( ) ) - Date . parse ( cresponse . headers . get ( 'date' ) ) > 86400000 ) {
2022-02-07 03:11:35 +00:00
// try to get it asynchronously through the plugins, and cache it
self . log ( 'service-worker' , ` config.json stale, fetching through plugins ` )
getResourceThroughLibResilient ( configURL , { } , 'libresilient-internal' , false , false )
. then ( async cresponse => {
// extract JSON and verify it
var cdata = await getConfigJSON ( cresponse )
// did that work?
2024-02-01 01:11:14 +00:00
if ( cdata === false ) {
// we got a false in cdata, that means it probably is invalid (or the fetch failed)
self . log ( 'service-worker' , 'config.json loaded through transport other than fetch seems invalid, ignoring' )
return false
// otherwise, we good for more in-depth testing!
} else {
2022-02-08 02:44:01 +00:00
// if we got the new config.json via a method *other* than plain old fetch(),
// we will not be able to use importScripts() to load any pugins that have not been loaded already
2024-01-28 22:56:25 +00:00
//
// NOTICE: this *only* checks if we have all the necessary plugin constructors already available
// which signifies that relevant code has been successfully loaded; but there are other failure modes!
2022-02-08 02:44:01 +00:00
if ( cresponse . headers . get ( 'x-libresilient-method' ) != 'fetch' ) {
2024-01-31 21:46:29 +00:00
// go through the plugins in the new config and check if we already have their constructors
// i.e. if the plugin scripts have already been loaded
let currentPlugin = cdata . plugins . shift ( )
do {
// plugin constructor not available, meaning: we'd have to importScripts() it
// but we can't since this was not retrieved via fetch(), so we cannot assume
// that the main domain of the website is up and available
//
// if we cache this newly retrieved config.json, next time the service worker gets restarted
// we will end up with an error while trying to run importScripts() for this plugin
// which in turn would lead to the service worker being unregistered
//
// if the main domain is not available, this would mean the website stops working
// even though we *were* able to retrieve the new config.json via plugins!
// so, ignoring this new config.json.
if ( ! LibResilientPluginConstructors . has ( currentPlugin . name ) ) {
self . log (
'service-worker' ,
` warning: config.json loaded through transport other than fetch, but specifies not previously loaded plugin: " ${ currentPlugin . name } " \n ignoring the whole config.json. ` )
return false ;
}
// push any dependencies into the array, at the very front
// thus gradually flattening the config
if ( "uses" in currentPlugin ) {
cdata . plugins . unshift ( ... currentPlugin . uses )
}
// get the next plugin to check
currentPlugin = cdata . plugins . shift ( )
} while ( ( currentPlugin !== false ) && ( currentPlugin !== undefined ) )
2022-02-08 02:44:01 +00:00
}
2024-02-01 01:11:14 +00:00
self . log ( 'service-worker' , ` valid config.json successfully retrieved through plugins; caching. ` )
// cache it, asynchronously, in the temporary cache
// as the config has not been "execute-tested" yet
2024-02-01 23:46:10 +00:00
cacheConfigJSON ( configURL , cresponse , "v1" )
2022-02-07 03:11:35 +00:00
}
} )
}
2022-01-22 16:56:24 +00:00
} catch ( e ) {
// we only get a cryptic "Error while registering a service worker"
// unless we explicitly print the errors out in the console
console . error ( e )
2024-01-28 22:56:25 +00:00
// we got an error while initializing the service worker!
2022-02-08 02:44:01 +00:00
// better play it safe!
self . registration . unregister ( )
2024-02-05 23:37:10 +00:00
return false
2021-04-06 17:18:37 +00:00
}
2022-01-26 00:26:16 +00:00
return true ;
2021-04-06 17:18:37 +00:00
}
/ * *
* fetch counter per clientId
*
* we need to keep track of active fetches per clientId
* so that we can inform a given clientId when we ' re completely done
* /
2021-09-08 19:34:12 +00:00
self . activeFetches = new Map ( ) ;
2021-04-06 17:18:37 +00:00
/ * *
* decrement fetches counter
* and inform the correct clientId if all is finished done
* /
let decrementActiveFetches = ( clientId ) => {
// decrement the fetch counter for the client
2021-09-08 19:34:12 +00:00
self . activeFetches . set ( clientId , self . activeFetches . get ( clientId ) - 1 )
self . log ( 'service-worker' , '+-- activeFetches[' + clientId + ']:' , self . activeFetches . get ( clientId ) )
if ( self . activeFetches . get ( clientId ) === 0 ) {
2021-04-06 17:18:37 +00:00
self . log ( 'service-worker' , 'All fetches done!' )
// inform the client
// client has to be smart enough to know if that is just temporary
// (and new fetches will fire in a moment, because a CSS file just
// got fetched) or not
2024-02-23 17:58:45 +00:00
postMessage ( clientId , {
allFetched : true
} ) . then ( ( ) => {
self . log ( 'service-worker' , 'all-fetched message queued.' )
2021-04-06 17:18:37 +00:00
} )
}
}
/ *
* returns a Promise that either resolves or rejects after a set timeout
* optionally with a specific error message
*
* time - the timeout ( in ms )
* timeout _resolves - whether the Promise should resolve ( ) or reject ( ) when hitting the timeout ( default : false ( reject ) )
* error _message - optional error message to use when rejecting ( default : false ( no error message ) )
2023-09-26 20:32:54 +00:00
*
* returns an array containing :
* - timeout - related Promise as element 0
* - timeoutID as element 1
2021-04-06 17:18:37 +00:00
* /
let promiseTimeout = ( time , timeout _resolves = false , error _message = false ) => {
2023-09-26 20:32:54 +00:00
let timeout _id = null
let timeout _promise = new Promise ( ( resolve , reject ) => {
timeout _id = setTimeout ( ( ) => {
if ( timeout _resolves ) {
resolve ( time ) ;
} else {
if ( error _message ) {
reject ( new Error ( error _message ) )
} else {
reject ( time )
}
}
} , time ) ;
2021-04-06 17:18:37 +00:00
} ) ;
2023-09-26 20:32:54 +00:00
// we need both the promise and the timeout ID
// so that we can clearTimeout() if/when needed
return [ timeout _promise , timeout _id ]
2021-04-06 17:18:37 +00:00
} ;
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = * \
2024-02-23 17:58:45 +00:00
| * === LibResilientClient === * |
\ * === === === === === === === === === === === === === === === === === === === === === === === === = * /
/ * *
* Libresilient client class
*
* handles communication with a client
*
* TODO : track active fetches as part of this class ?
* TODO : https : //gitlab.com/rysiekpl/libresilient/-/issues/83
* /
let LibResilientClient = class {
async postMessage ( message ) {
// log
self . log ( 'service-worker' , 'postMessage():' , JSON . stringify ( message ) )
// add our message to the message queue
this . messageQueue . push ( message )
// try to get the client from Client API based on clientId
if ( ! this . client ) {
this . client = await self
. clients
. get ( this . clientId )
}
// now, we might still not have a valid client here
if ( ! this . client ) {
// store it for later, when we do get a valid client
self . log ( 'service-worker' , ` postMessage(): no valid client for id: ${ this . clientId } , added message to the queue ` )
// we have a valid client, it seems!
} else {
// we want all messages to be delivered, and in order they were added
// our message is at the end and will get handled in due course
let msg = false
while ( msg = this . messageQueue . shift ( ) ) {
try {
this . client . postMessage ( msg ) ;
} catch ( err ) {
// if we fail for whatever reason, bail from the loop
self . log ( 'service-worker' , ` postMessage(): client seems valid, but postMessage failed; message left in the queue \n - Error message: ${ err } ` )
this . messageQueue . unshift ( msg )
break
}
}
}
}
constructor ( clientId ) {
2024-02-28 02:58:28 +00:00
self . log ( 'service-worker' , ` new client: ${ clientId } ` )
2024-02-23 17:58:45 +00:00
// we often get the clientId long before
// we are able to get a valid client out of it
//
// so we need to keep both
this . clientId = clientId
this . client = null ;
// queued messages for when we have a client available
this . messageQueue = [ ]
}
}
// map of all known clients
let LibResilientClients = new Map ( )
/ * *
* getting a client based on clientId and sending a message
* ( or queueing it for later if we cannot get a valid client )
* /
let postMessage = async ( clientId , message ) => {
// do we already have a LibResilientClient instance for that client id?
let client = LibResilientClients . get ( clientId )
// if not, create it
if ( client === undefined ) {
client = new LibResilientClient ( clientId )
LibResilientClients . set ( clientId , client )
}
// send (or queue) the message
await client . postMessage ( message )
}
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = * \
| * === LibResilientResourceInfo === * |
2021-04-06 17:18:37 +00:00
\ * === === === === === === === === === === === === === === === === === === === === === === === === = * /
/ * *
* LibResilient resource info class
*
* keeps the values as long as the service worker is running ,
* and communicates all changes to relevant clients
*
* clients are responsible for saving and keeping the values across
* service worker restarts , if that ' s required
* /
let LibResilientResourceInfo = class {
/ * *
* constructor
* needed to set the URL and clientId
* /
constructor ( url , clientId ) {
2021-08-25 19:43:21 +00:00
// actual values of the fields
// only used internally, and stored into the Indexed DB
this . values = {
2024-02-23 17:58:45 +00:00
url : '' , // read only after initialization
clientId : null , // the client on whose behalf that request is being processed
lastError : null , // error from the previous plugin (for state:running) or the last emitted error (for state:failed or state:success)
method : null , // name of the current plugin (in case of state:running) or last plugin (for state:failed or state:success)
state : null , // can be "failed", "success", "running"
2021-08-25 19:43:21 +00:00
serviceWorker : 'COMMIT_UNKNOWN' // this will be replaced by commit sha in CI/CD; read-only
}
2024-02-23 17:58:45 +00:00
// queued messages for when we have a client available
this . messageQueue = [ ]
2021-04-06 17:18:37 +00:00
// set it
this . values . url = url
this . values . clientId = clientId
// we might not have a non-empty clientId if it's a cross-origin fetch
2023-09-26 20:32:54 +00:00
2021-04-06 17:18:37 +00:00
if ( clientId ) {
2024-02-23 17:58:45 +00:00
postMessage (
clientId ,
{ ... this . values }
)
2021-04-06 17:18:37 +00:00
}
}
/ * *
* update this . values and immediately postMessage ( ) to the relevant client
*
* data - an object with items to set in this . values
* /
update ( data ) {
// debug
var msg = 'Updated LibResilientResourceInfo for: ' + this . values . url
// was there a change? if not, no need to postMessage
var changed = false
// update the properties that are read-write
Object
. keys ( data )
. filter ( ( k ) => {
2024-02-23 17:58:45 +00:00
return [ 'lastError' , 'method' , 'state' ] . includes ( k )
2021-04-06 17:18:37 +00:00
} )
. forEach ( ( k ) => {
msg += '\n+-- ' + k + ': ' + data [ k ]
if ( this . values [ k ] !== data [ k ] ) {
msg += ' (changed!)'
changed = true
}
this . values [ k ] = data [ k ]
} )
self . log ( 'service-worker' , msg )
// send the message to the client
2024-02-23 17:58:45 +00:00
if ( changed ) {
postMessage (
this . values . clientId ,
{ ... this . values }
) ;
2021-04-06 17:18:37 +00:00
}
}
/ * *
2024-02-23 17:58:45 +00:00
* lastError property
2021-04-06 17:18:37 +00:00
* /
2024-02-23 17:58:45 +00:00
get lastError ( ) {
return this . values . lastError
2021-04-06 17:18:37 +00:00
}
/ * *
* method property
* /
get method ( ) {
return this . values . method
}
/ * *
* state property
* /
get state ( ) {
return this . values . state
}
/ * *
* serviceWorker property ( read - only )
* /
get serviceWorker ( ) {
return this . values . serviceWorker
}
/ * *
* url property ( read - only )
* /
get url ( ) {
return this . values . url
}
/ * *
* clientId property ( read - only )
* /
get clientId ( ) {
return this . values . clientId
}
}
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = * \
| * === Main Brain of LibResilient === * |
\ * === === === === === === === === === === === === === === === === === === === === === === === === = * /
2021-11-08 20:51:27 +00:00
/ * *
* generate Request ( ) - compatible init object from an existing Request
*
* req - the request to work off of
* /
let initFromRequest = ( req ) => {
return {
method : req . method ,
2021-11-17 13:57:47 +00:00
// TODO: ref. https://gitlab.com/rysiekpl/libresilient/-/issues/23
2021-11-16 23:46:13 +00:00
//headers: req.headers, TODO: commented out: https://stackoverflow.com/questions/32500073/request-header-field-access-control-allow-headers-is-not-allowed-by-itself-in-pr
//mode: req.mode, TODO: commented out because mode: navigate is haram in service worker, it seems
//credentials: req.credentials, TODO: commented out because credentials: "include" is haram if the Access-Control-Allow-Origin header is '*'
2021-11-08 20:51:27 +00:00
cache : req . cache ,
redirect : req . redirect ,
referrer : req . referrer ,
integrity : req . integrity
}
}
2021-04-06 17:18:37 +00:00
/ * *
* run a plugin ' s fetch ( ) method
* while handling all the auxiliary stuff like saving info in reqInfo
*
* plugin - the plugin to use
* url - string containing the URL to fetch
2021-11-08 20:51:27 +00:00
* init - Request ( ) initialization parameters
2021-04-06 17:18:37 +00:00
* reqInfo - instance of LibResilientResourceInfo
* /
2021-11-08 20:51:27 +00:00
let libresilientFetch = ( plugin , url , init , reqInfo ) => {
2021-04-06 17:18:37 +00:00
// status of the plugin
reqInfo . update ( {
2022-02-08 02:44:01 +00:00
method : ( plugin && "name" in plugin ) ? plugin . name : "unknown" ,
2021-04-06 17:18:37 +00:00
state : "running"
} )
// log stuff
self . log ( 'service-worker' , "LibResilient Service Worker handling URL:" , url ,
2021-11-08 20:51:27 +00:00
'\n+-- init:' , Object . getOwnPropertyNames ( init ) . map ( p => ` \n - ${ p } : ${ init [ p ] } ` ) . join ( '' ) ,
'\n+-- using method(s):' , plugin . name
)
2021-04-06 17:18:37 +00:00
2024-02-23 17:58:45 +00:00
// starting the fetch...
// if it errors out immediately, at least we don't have to deal
// with a dangling promise timeout, set up below
let fetch _promise = plugin . fetch ( url , init )
2023-09-26 20:32:54 +00:00
let timeout _promise , timeout _id
[ timeout _promise , timeout _id ] = promiseTimeout (
self . LibResilientConfig . defaultPluginTimeout ,
false ,
` LibResilient request using ${ plugin . name } timed out after ${ self . LibResilientConfig . defaultPluginTimeout } ms. `
)
// making sure there are no dangling promises etc
//
2024-02-23 17:58:45 +00:00
// this has to happen asynchronously
fetch _promise
2023-09-26 20:32:54 +00:00
// make sure the timeout is cancelled as soon as the promise resolves
// we do not want any dangling promises/timeouts after all!
2024-02-23 17:58:45 +00:00
. finally ( ( ) => {
2023-09-26 20:32:54 +00:00
clearTimeout ( timeout _id )
} )
// no-op to make sure we don't end up with dangling rejected premises
. catch ( ( e ) => { } )
2024-02-23 17:58:45 +00:00
// race the plugin(s) vs. the timeout
return Promise
. race ( [
fetch _promise ,
timeout _promise
] ) ;
2021-04-06 17:18:37 +00:00
}
/ * *
* calling a libresilient plugin function on the first plugin that implements it
*
* call - method name to call
* args - arguments that will be passed to it
* /
let callOnLibResilientPlugin = ( call , args ) => {
// find the first plugin implementing the method
2023-09-26 20:32:54 +00:00
for ( let i = 0 ; i < self . LibResilientPlugins . length ; i ++ ) {
2021-08-25 19:43:21 +00:00
if ( typeof self . LibResilientPlugins [ i ] [ call ] === 'function' ) {
self . log ( 'service-worker' , 'Calling plugin ' + self . LibResilientPlugins [ i ] . name + '.' + call + '()' )
2021-04-06 17:18:37 +00:00
// call it
2021-09-01 21:01:01 +00:00
// TODO: check if args is an Array?
2021-08-25 19:43:21 +00:00
return self . LibResilientPlugins [ i ] [ call ] . apply ( null , args )
2021-04-06 17:18:37 +00:00
}
}
}
/ * *
* Cycles through all the plugins , in the order they got registered ,
* and returns a Promise resolving to a Response in case any of the plugins
* was able to get the resource
*
2022-01-24 13:55:40 +00:00
* url - the url we want to fetch
* init - the init data for responses
2021-04-06 17:18:37 +00:00
* clientId - string containing the clientId of the requesting client
* useStashed - use stashed resources ; if false , only pull resources from live sources
* doStash - stash resources once fetched successfully ; if false , do not stash pulled resources automagically
* stashedResponse - TBD
* /
2022-01-24 13:55:40 +00:00
let getResourceThroughLibResilient = ( url , init , clientId , useStashed = true , doStash = true , stashedResponse = null ) => {
2021-11-08 20:51:27 +00:00
2021-04-06 17:18:37 +00:00
// set-up reqInfo for the fetch event
var reqInfo = new LibResilientResourceInfo ( url , clientId )
// fetch counter
2021-09-08 19:34:12 +00:00
self . activeFetches . set ( clientId , self . activeFetches . get ( clientId ) + 1 )
2021-04-06 17:18:37 +00:00
// filter out stash plugins if need be
2021-08-25 19:43:21 +00:00
var LibResilientPluginsRun = self . LibResilientPlugins . filter ( ( plugin ) => {
2021-09-18 22:52:54 +00:00
return ( useStashed || typeof plugin . stash !== 'function' )
2021-04-06 17:18:37 +00:00
} )
/ * *
* this uses Array . reduce ( ) to chain the LibResilientPlugins [ ] - generated Promises
* using the Promise the first registered plugin as the default value
*
* see : https : //css-tricks.com/why-using-reduce-to-sequentially-resolve-promises-works/
*
* this also means that LibResilientPlugins [ 0 ] . fetch ( ) below will run first
* ( counter - intutively ! )
*
* we are slice ( ) - ing it so that the first plugin is only run once ; it is
* run in the initialValue parameter below already
*
* ref :
* https : //developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce
* /
return LibResilientPluginsRun
. slice ( 1 )
. reduce (
2021-11-13 13:14:17 +00:00
( prevPromise , currentPlugin ) => {
return prevPromise . catch ( ( error ) => {
self . log ( 'service-worker' , "LibResilient plugin error for:" , url ,
'\n+-- method : ' + reqInfo . method ,
'\n+-- error : ' + error . toString ( ) )
// save info in reqInfo -- status of the previous method
reqInfo . update ( {
2024-02-23 17:58:45 +00:00
lastError : error . toString ( )
2021-11-13 13:14:17 +00:00
} )
return libresilientFetch ( currentPlugin , url , init , reqInfo )
2021-04-06 17:18:37 +00:00
} )
2021-11-13 13:14:17 +00:00
} ,
// this libresilientFetch() will run first
// all other promises generated by LibResilientPlugins[] will be chained on it
// using the catch() in reduce() above
// skipping this very first plugin by way of slice(1)
libresilientFetch ( LibResilientPluginsRun [ 0 ] , url , init , reqInfo )
)
. then ( ( response ) => {
// we got a successful response
decrementActiveFetches ( clientId )
2021-04-06 17:18:37 +00:00
2021-11-13 13:14:17 +00:00
// record the success
2024-02-23 17:58:45 +00:00
reqInfo . update ( {
lastError : null ,
state : "success"
} )
2021-04-06 17:18:37 +00:00
2021-11-13 13:14:17 +00:00
// get the plugin that was used to fetch content
2023-09-26 20:32:54 +00:00
let plugin = self . LibResilientPlugins . find ( p => p . name === reqInfo . method )
2021-11-13 13:14:17 +00:00
// if it's a stashing plugin...
if ( typeof plugin . stash === 'function' ) {
// we obviously do not want to stash
self . log ( 'service-worker' , 'Not stashing, since resource is already retrieved by a stashing plugin:' , url ) ;
// since we got the data from a stashing plugin,
// let's run the rest of plugins in the background to check if we can get a fresher resource
// and stash it in cache for later use
self . log ( 'service-worker' , 'starting background no-stashed fetch for:' , url ) ;
// event.waitUntil?
// https://stackoverflow.com/questions/37902441/what-does-event-waituntil-do-in-service-worker-and-why-is-it-needed/37906330#37906330
2024-02-23 17:58:45 +00:00
try {
getResourceThroughLibResilient ( url , init , clientId , false , true , response . clone ( ) )
. catch ( ( e ) => {
self . log ( 'service-worker' , 'background no-stashed fetch failed for:' , url ) ;
} )
} catch ( e ) {
self . log ( 'service-worker' , 'background no-stashed fetch failed for:' , url , ` \n +-- error: ${ e } ` ) ;
}
2021-11-13 13:14:17 +00:00
// return the response so that stuff can keep happening
return response
// otherwise, let's see if we want to stash
// and if we already had a stashed version that differs
} else {
// do we have a stashed version that differs?
if ( stashedResponse && stashedResponse . headers ) {
// this is where we check if the response from whatever plugin we got it from
// is newer than what we've stashed
self . log ( 'service-worker' , 'checking freshness of stashed version of:' , url ,
'\n+-- stashed from :' , stashedResponse . headers . get ( 'X-LibResilient-Method' ) ,
'\n+-- fetched using :' , response . headers . get ( 'X-LibResilient-Method' ) ,
'\n+-- stashed X-LibResilient-ETag :' , stashedResponse . headers . get ( 'X-LibResilient-ETag' ) ,
'\n+-- fetched X-LibResilient-ETag :' , response . headers . get ( 'X-LibResilient-ETag' ) )
// if the method does not match, or if it matches but the ETag doesn't
// we have a different response
// which means *probably* fresher content
if ( ( stashedResponse . headers . get ( 'X-LibResilient-Method' ) !== response . headers . get ( 'X-LibResilient-Method' ) )
|| ( stashedResponse . headers . get ( 'X-LibResilient-ETag' ) !== response . headers . get ( 'X-LibResilient-ETag' ) ) ) {
// inform!
self . log ( 'service-worker' , 'fetched version method or ETag differs from stashed for:' , url )
2024-02-23 17:58:45 +00:00
postMessage ( reqInfo . clientId , {
url : url ,
fetchedDiffers : true
2021-04-06 17:18:37 +00:00
} )
2021-11-13 13:14:17 +00:00
// TODO: this should probably modify doStash?
}
}
// do we want to stash?
if ( doStash ) {
// find the first stashing plugin
2023-09-26 20:32:54 +00:00
for ( let i = 0 ; i < self . LibResilientPlugins . length ; i ++ ) {
2021-11-13 13:14:17 +00:00
if ( typeof self . LibResilientPlugins [ i ] . stash === 'function' ) {
// ok, now we're in business
var hdrs = '\n+-- headers:'
response . headers . forEach ( ( v , k ) => {
hdrs += ` \n +-- ${ k } : ${ v } `
2021-04-06 17:18:37 +00:00
} )
2021-11-13 13:14:17 +00:00
self . log (
'service-worker' ,
` stashing a successful fetch of: ${ url } ` ,
` \n +-- fetched using : ${ response . headers . get ( 'X-LibResilient-Method' ) } ` ,
` \n +-- stashing using : ${ self . LibResilientPlugins [ i ] . name } ` ,
hdrs
)
// working on clone()'ed response so that the original one is not touched
// TODO: should a failed stashing break the flow here? probably not!
return self . LibResilientPlugins [ i ] . stash ( response . clone ( ) , url )
. then ( ( res ) => {
// original response will be needed further down
return response
} )
}
2021-04-06 17:18:37 +00:00
}
}
}
2021-11-13 13:14:17 +00:00
// if we're here it means we went through the whole list of plugins
// and found not a single stashing plugin
// or we don't want to stash the resources in the first place
// that's fine, but let's make sure the response goes forth
return response
} )
// a final catch... in case all plugins fail
. catch ( ( err ) => {
2024-02-23 17:58:45 +00:00
self . log ( 'service-worker' , "LibResilient failed completely: " , err ,
2021-11-13 13:14:17 +00:00
'\n+-- URL : ' + url )
// cleanup
reqInfo . update ( {
2024-02-23 17:58:45 +00:00
state : "failed" ,
lastError : err . toString ( )
2021-11-13 13:14:17 +00:00
} )
decrementActiveFetches ( clientId )
// rethrow
throw err
2021-04-06 17:18:37 +00:00
} )
}
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = * \
| * === Setting up the event handlers === * |
\ * === === === === === === === === === === === === === === === === === === === === === === === === = * /
2022-01-22 16:56:24 +00:00
self . addEventListener ( 'install' , async ( event ) => {
2024-02-05 23:37:10 +00:00
let init _promise = initServiceWorker ( )
await event . waitUntil ( init _promise )
if ( await init _promise === true ) {
2024-02-23 17:58:45 +00:00
self . skipWaiting ( )
2024-02-05 23:37:10 +00:00
// "COMMIT_UNKNOWN" will be replaced with commit ID
2024-02-23 17:58:45 +00:00
self . log ( 'service-worker' , "installed LibResilient Service Worker (commit: COMMIT_UNKNOWN)." ) ;
2024-02-05 23:37:10 +00:00
}
2021-04-06 17:18:37 +00:00
} ) ;
2022-01-26 00:26:16 +00:00
self . addEventListener ( 'activate' , async event => {
2024-02-23 17:58:45 +00:00
await event . waitUntil ( self . clients . claim ( ) )
self . log ( 'service-worker' , "activated LibResilient Service Worker (commit: COMMIT_UNKNOWN)." ) ;
2021-04-06 17:18:37 +00:00
} ) ;
2022-01-26 00:26:16 +00:00
self . addEventListener ( 'fetch' , async event => {
return void event . respondWith ( async function ( ) {
2022-02-01 03:10:28 +00:00
// initialize the SW; this is necessary as SW can be stopped at any time
// and restarted when an event gets triggered -- `fetch` is just such an event.
//
// `install` and `activate` events only handle the initial installation of the SW
// this means that we might end up here without initServiceWorker() ever being run
// and so in a situation where plugins have never been actually set-up!
//
// the good news is that the config.json should have been cached already
2022-01-26 00:26:16 +00:00
await initServiceWorker ( )
// if event.resultingClientId is available, we need to use this
// otherwise event.clientId is what we want
// ref. https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent/resultingClientId
var clientId = ( event . clientId !== null ) ? event . clientId : 'unknown-client'
if ( event . resultingClientId ) {
clientId = event . resultingClientId
// yeah, we seem to have to send the client their clientId
// because there is no way to get that client-side
// and we need that for sane messaging later
//
// so let's also send the plugin list, why not
//
// *sigh* JS is great *sigh*
2024-02-23 17:58:45 +00:00
postMessage ( clientId , {
clientId : clientId ,
plugins : self . LibResilientPlugins . map ( ( p ) => { return p . name } ) ,
serviceWorker : 'COMMIT_UNKNOWN'
} )
2022-01-26 00:26:16 +00:00
}
// counter!
if ( typeof self . activeFetches . get ( clientId ) !== "number" ) {
self . activeFetches . set ( clientId , 0 )
}
// info
self . log ( 'service-worker' , "Fetching!" ,
"\n+-- url :" , event . request . url ,
2024-02-23 17:58:45 +00:00
"\n+-- mode :" , event . request . mode ,
2022-01-26 00:26:16 +00:00
"\n+-- clientId :" , event . clientId ,
"\n+-- resultingClientId:" , event . resultingClientId ,
"\n +-- activeFetches[" + clientId + "]:" , self . activeFetches . get ( clientId )
)
// External requests go through a regular fetch()
if ( ! event . request . url . startsWith ( self . location . origin ) ) {
2023-09-26 20:32:54 +00:00
self . log ( 'service-worker' , 'External request, using standard fetch(); current origin: ' + self . location . origin )
2022-01-26 02:01:55 +00:00
return fetch ( event . request ) ;
2022-01-26 00:26:16 +00:00
}
2021-04-06 17:18:37 +00:00
2022-01-26 00:26:16 +00:00
// Non-GET requests go through a regular fetch()
if ( event . request . method !== 'GET' ) {
2023-09-26 20:32:54 +00:00
self . log ( 'service-worker' , 'Non-GET request, using standard fetch()' )
2022-01-26 02:01:55 +00:00
return fetch ( event . request ) ;
2022-01-26 00:26:16 +00:00
}
// clean the URL, removing any fragment identifier
var url = event . request . url . replace ( /#.+$/ , '' ) ;
2023-10-07 04:58:48 +00:00
// normalize query params, if we want that
if ( self . LibResilientConfig . normalizeQueryParams ) {
self . log ( 'service-worker' , 'normalizing query params' )
url = url . split ( '?' )
if ( url . length > 1 ) {
url [ 1 ] = url [ 1 ] . split ( '&' ) . sort ( ) . join ( '&' )
}
url = url . join ( '?' )
}
2022-01-26 00:26:16 +00:00
// get the init object from Request
var init = initFromRequest ( event . request )
2021-04-06 17:18:37 +00:00
2022-01-26 00:26:16 +00:00
// GET requests to our own domain that are *not* #libresilient-info requests
// get handled by plugins in case of an error
2024-02-23 17:58:45 +00:00
let lrPromise = getResourceThroughLibResilient ( url , init , clientId )
// is the stillLoadingScreen enabled, and are we navigating, or just fetching some resource?
if ( ( self . LibResilientConfig . stillLoadingTimeout > 0 ) && ( event . request . mode === 'navigate' ) ) {
self . log ( 'service-worker' , ` handling a navigate request; still-loading timeout: ${ self . LibResilientConfig . stillLoadingTimeout } . ` )
let slPromise , slTimeoutId
[ slPromise , slTimeoutId ] = promiseTimeout ( self . LibResilientConfig . stillLoadingTimeout , true )
// make sure to clear the timeout related to slPromise
// in case we manage to get the content through the plugins
lrPromise
. then ( ( ) => {
self . log ( 'service-worker' , ` content retrieved; still-loading timeout cleared. ` )
clearTimeout ( slTimeoutId )
} )
// return a Promise that races the "still loading" screen promise against the LibResilient plugins
return Promise . race ( [
// regular fetch-through-plugins
lrPromise ,
// the "still loading screen"
//
// this will delay a specified time, and ten return a Response
// with very basic HTML informing the user that the page is still loading,
// a Refresh header set, and a link for the user to reload the screen manually
slPromise
. then ( ( ) => {
// inform
self . log ( 'service-worker' , 'handling a navigate request is taking too long, showing the still-loading screen' )
// we need to create a new Response object
// with all the headers added explicitly,
// since response.headers is immutable
var responseInit = {
status : 202 ,
statusText : "Accepted" ,
headers : { } ,
url : url
} ;
responseInit . headers [ 'Content-Type' ] = "text/html"
// refresh: we want a minimum of 1s; stillLoadingTimeout is in ms!
//responseInit.headers['Refresh'] = Math.ceil( self.LibResilientConfig.stillLoadingTimeout / 1000 )
//responseInit.headers['ETag'] = ???
//responseInit.headers['X-LibResilient-ETag'] = ???
responseInit . headers [ 'X-LibResilient-Method' ] = "still-loading"
// TODO: make this configurable via config.json
// TODO: https://gitlab.com/rysiekpl/libresilient/-/issues/82
let stillLoadingHTML = ` <!DOCTYPE html><html><head><title>Still loading...</title></head><body>
< style >
body {
margin : auto ;
width : 30 em ;
margin : 2 em auto ;
background : # dddddd ;
color : black ;
font - family : sans - serif ;
}
h1 {
height : 1 em ;
margin - bottom :
0.2 em
}
h1 > span {
animation : throbber 2 s infinite 0 s linear ;
font - size : 70 % ;
position : relative ;
top : 0.2 em ;
}
# throbber1 {
animation - delay : 0 s ;
}
# throbber2 {
animation - delay : 0.5 s ;
}
# throbber3 {
animation - delay : 1 s ;
}
@ keyframes throbber {
0 % { opacity : 1.0 ; }
50 % { opacity : 0.1 ; }
100 % { opacity : 1.0 ; }
}
# working {
color : gray ;
font - family : monospace ;
font - weight : bold ;
display : flex ;
flex - wrap : nowrap ;
justify - content : left ;
margin - top : 0 em ;
}
a {
color : # 2 d5589
}
< / s t y l e >
< h1 id = "header" > Still loading < span id = "throbber1" > & # x2022 ; < / s p a n > < s p a n i d = " t h r o b b e r 2 " > & # x 2 0 2 2 ; < / s p a n > < s p a n i d = " t h r o b b e r 3 " > & # x 2 0 2 2 ; < / s p a n > < / h 1 >
< p id = "working" > attempts : & nbsp ; < span id = "status" > 1 < / s p a n > < / p >
< p id = "text" > The content is still being loaded , thank you for your patience . < br / > < br / > This page will auto - reload in a few seconds . If it does not , please < a href = "./" > click here < / a > . < / p >
< script >
let attempts = 0 ;
let header = document . getElementById ( 'header' )
let text = document . getElementById ( 'text' )
let status = document . getElementById ( 'status' )
navigator . serviceWorker . addEventListener ( 'message' , event => {
if ( event . data . url === window . location . href ) {
if ( event . data . state === 'success' ) {
header . innerHTML = "Loaded, redirecting!"
text . innerHTML = "The content has loaded, you are being redirected."
window . location . reload ( )
} else if ( event . data . state === 'failed' ) {
header . innerHTML = "Loading failed."
text . innerHTML = "We're sorry, we were unable to load this page."
}
if ( ( 'lastError' in event . data ) && ( typeof event . data . lastError === 'string' ) ) {
attempts += 1 ;
status . innerHTML = attempts ;
}
}
} )
< / s c r i p t > < / b o d y > < / h t m l > `
let blob = new Blob (
[ stillLoadingHTML ] ,
{ type : "text/html" }
)
return new Response (
blob ,
responseInit
)
} )
] )
// nope, just fetching a resource
} else {
if ( event . request . mode === 'navigate' ) {
self . log ( 'service-worker' , ` handling a navigate request, but still-loading screen is disabled. ` )
} else {
self . log ( 'service-worker' , 'handling a regular request; still-loading screen will not be used.' )
}
// no need for the whole "still loading screen" flow
return lrPromise ;
}
2022-01-26 00:26:16 +00:00
} ( ) )
2021-04-06 17:18:37 +00:00
} ) ;
/ * *
* assumptions to be considered :
* every message contains clientId ( so that we know where to respond if / w h e n w e n e e d t o )
* /
2022-02-01 03:10:28 +00:00
self . addEventListener ( 'message' , async ( event ) => {
// initialize the SW; this is necessary as SW can be stopped at any time
// and restarted when an event gets triggered -- `message` is just such an event.
//
// `install` and `activate` events only handle the initial installation of the SW
// this means that we might end up here without initServiceWorker() ever being run
// and so in a situation where plugins have never been actually set-up!
//
// the good news is that the config.json should have been cached already
await initServiceWorker ( )
2021-04-06 17:18:37 +00:00
// inform
var msg = 'Message received!'
Object . keys ( event . data ) . forEach ( ( k ) => {
msg += '\n+-- key: ' + k + " :: val: " + event . data [ k ]
} )
self . log ( 'service-worker' , msg ) ;
/ *
* supporting stash ( ) , unstash ( ) , and publish ( ) only
* /
if ( event . data . stash || event . data . unstash || event . data . publish ) {
if ( event . data . stash ) {
callOnLibResilientPlugin ( 'stash' , event . data . stash )
}
if ( event . data . unstash ) {
callOnLibResilientPlugin ( 'unstash' , event . data . unstash )
}
if ( event . data . publish ) {
callOnLibResilientPlugin ( 'publish' , event . data . publish )
}
}
} ) ;