dsw
Version:
Dynamic Service Worker, offline Progressive Web Apps much easier
376 lines (336 loc) • 15.9 kB
JavaScript
// TODO: should pre-cache or cache in the first load, some of the page's already sources (like css, js or images), or tell the user it supports offline usage, only in the next reload
var isInSWScope = false;
var isInTest = typeof global.it === 'function';
import getBestMatchingRX from './best-matching-rx.js';
import cacheManager from './cache-manager.js';
import goFetch from './go-fetch.js';
import strategies from './strategies.js';
const DSW = {};
const REQUEST_TIME_LIMIT = 5000;
// this try/catch is used simply to figure out the current scope
try {
let SWScope = ServiceWorkerGlobalScope;
if(self instanceof ServiceWorkerGlobalScope){
isInSWScope = true;
}
}catch(e){ /* nothing...just had to find out the scope */ }
if (isInSWScope) {
const DSWManager = {
rules: {},
addRule (sts, rule, rx) {
this.rules[sts] = this.rules[sts] || [];
let newRule = {
name: rule.name,
rx,
strategy: rule.strategy || 'offline-first',
action: rule['apply']
};
this.rules[sts].push(newRule);
// if there is a rule for cache
if (newRule.action.cache) {
// we will register it in the cacheManager
cacheManager.register(newRule);
}
return newRule;
},
treatBadPage (response, pathName, event) {
let result;
(DSWManager.rules[
response && response.status? response.status : 404
] || [])
.some((cur, idx)=>{
let matching = pathName.match(cur.rx);
if (matching) {
if (cur.action.fetch) {
// not found requisitions should
// fetch a different resource
console.info('Found fallback rule for ', pathName, '\nLooking for its result');
result = cacheManager.get(cur,
new Request(cur.action.fetch),
event,
matching);
return true; // stopping the loop
}
}
});
if (!result) {
console.info('No rules for failed request: ', pathName, '\nWill output the failure');
}
return result || response;
},
setup (dswConfig={}) {
// let's prepare both cacheManager and strategies with the
// current referencies
cacheManager.setup(DSWManager, PWASettings, goFetch);
strategies.setup(DSWManager, cacheManager, goFetch);
return new Promise((resolve, reject)=>{
// we will prepare and store the rules here, so it becomes
// easier to deal with, latelly on each requisition
let preCache = PWASettings.appShell || [],
dbs = [];
Object.keys(dswConfig.dswRules).forEach(heuristic=>{
let ruleName = heuristic;
heuristic = dswConfig.dswRules[heuristic];
heuristic.name = ruleName;
heuristic.action = heuristic.action || heuristic['apply'];
let appl = heuristic.action,
extensions,
status,
path;
// in case "match" is an array
// we will treat it as an "OR"
if (Array.isArray(heuristic.match)) {
extensions = [];
path = [];
heuristic.match.map(cur=>{
if (cur.extension) {
extensions.push(cur.extension);
}
if (cur.path) {
path.push(cur.path);
}
});
extensions = extensions.join('|');
if (extensions.length) {
extensions+= '|';
}
path = (path.join('|') || '') + '|';
} else {
// "match" may be an object, then we simply use it
path = (heuristic.match.path || '' );// aqui + '([.+]?)';
extensions = heuristic.match.extension,
status = heuristic.match.status;
}
// preparing extentions to be added to the regexp
let ending = '([\/\&\?]|$)';
if (Array.isArray(extensions)){
extensions = '([.+]?)(' + extensions.join(ending+'|') + ending + ')';
} else if (typeof extensions == 'string'){
extensions = '([.+]?)(' + extensions + ending + ')';
} else {
extensions = '';
}
// and now we "build" the regular expression itself!
let rx = new RegExp(path + (extensions? '((\\.)(('+ extensions +')([\\?\&\/].+)?))': ''), 'i');
// if it fetches something, and this something is not dynamic
// also, if it will redirect to some static url
let noVars = /\$[0-9]+/;
if ( (appl.fetch && !appl.fetch.match(noVars))
||
(appl.redirect && !appl.redirect.match(noVars))) {
preCache.push({
url: appl.fetch || appl.redirect,
rule: heuristic
});
}
// in case the rule uses an indexedDB
appl.indexedDB = appl.indexedDB || appl.idb || appl.IDB || undefined;
if (appl.indexedDB) {
dbs.push(appl.indexedDB);
}
// preparing status to store the heuristic
status = Array.isArray(status)? status : [status || '*'];
// storing the new, shorter, optimized structure of the
// rules for all the status that it should be applied to
status.forEach(sts=>{
if (sts == 200) {
sts = '*';
}
let addedRule = this.addRule(sts, heuristic, rx);
});
});
// adding the dsw itself to cache
this.addRule('*', {
name: 'serviceWorker',
match: { path: /^\/dsw.js(\?=dsw-manager)?$/ },
'apply': { cache: { } }
}, location.href);
// addinf the root path to be also cached by default
let rootMatchingRX = /^(\/|\/index(\.[0-1a-z]+)?)$/;
this.addRule('*', {
name: 'rootDir',
match: { path: rootMatchingRX },
'apply': { cache: { } }
}, rootMatchingRX);
preCache.unshift('/');
// if we've got urls to pre-store, let's cache them!
// also, if there is any database to be created, this is the time
if(preCache.length || dbs.length){
// we fetch them now, and store it in cache
return Promise.all(
preCache.map(function(cur) {
return cacheManager
.add(cur.url||cur, null, null, cur.rule);
}).concat(dbs.map(function(cur) {
return cacheManager.createDB(cur);
})
)).then(resolve);
}else{
resolve();
}
});
},
getRulesBeforeFetching () {
// returns all the rules for * or 200
return this.rules['*'] || false;
},
createRequest (request, event, matching) {
return goFetch(null, request.url || request, event, matching);
},
createRedirect (request, event, matching) {
return goFetch(null, request.url || request, event, matching);
},
startListening () {
// and from now on, we listen for any request and treat it
self.addEventListener('fetch', event=>{
const url = new URL(event.request.url);
const pathName = url.pathname;
// in case we want to enforce https
if (PWASettings.enforceSSL) {
if (url.protocol != 'https:' && url.hostname != 'localhost') {
return event.respondWith(Response.redirect(
event.request.url.replace('http:', 'https:'), 302));
}
}
let i = 0,
l = (DSWManager.rules['*'] || []).length;
for (; i<l; i++) {
let rule = DSWManager.rules['*'][i];
let matching = pathName.match(rule.rx);
if (matching) {
// if there is a rule that matches the url
return event.respondWith(
strategies[rule.strategy](
rule,
event.request,
event,
matching
)
);
}
}
// if no rule is applied, we will request it
// this is the function to deal with the resolt of this request
let defaultTreatment = function (response) {
if (response && response.status == 200) {
return response;
} else {
return DSWManager.treatBadPage(response, pathName, event);
}
};
// once no rule matched, we simply respond the event with a fetch
return event.respondWith(
fetch(goFetch(null, event.request))
// but we will still treat the rules that use the status
.then(defaultTreatment)
.catch(defaultTreatment)
);
});
}
};
self.addEventListener('activate', function(event) {
event.waitUntil(_=>{
let promises = [];
if (PWASettings.applyImmediately) {
promises.push(self.clients.claim());
}
promises.push(cacheManager.deleteUnusedCaches(PWASettings.keepUnusedCaches));
return Promise.all(promises);
});
});
self.addEventListener('install', function(event) {
// undoing some bad named properties :/
PWASettings.dswRules = PWASettings.rules || PWASettings.dswRules || {};
PWASettings.dswVersion = PWASettings.version || PWASettings.dswVersion || '1';
if (PWASettings.applyImmediately) {
event.waitUntil(self.skipWaiting().then(_=>{
return DSWManager.setup(PWASettings);
}));
}else{
event.waitUntil(DSWManager.setup(PWASettings));
}
});
self.addEventListener('message', function(event) {
// TODO: add support to message event
});
self.addEventListener('sync', function(event) {
// TODO: add support to sync event
//debugger;
});
DSWManager.startListening();
}else{
DSW.setup = config => {
return new Promise((resolve, reject)=>{
// opening on a page scope...let's install the worker
if(navigator.serviceWorker){
if (!navigator.serviceWorker.controller) {
// we will use the same script, already loaded, for our service worker
var src = document.querySelector('script[src$="dsw.js"]').getAttribute('src');
navigator.serviceWorker
.register(src)
.then(SW=>{
console.info('[ SW ] :: registered');
if (config && config.sync) {
if ('SyncManager' in window) {
navigator.serviceWorker.ready.then(function(reg) {
return reg.sync.register('myFirstSync');
})
.then(_=>{
resolve({
status: true,
sync: true,
sw: true
});
})
.catch(function(err) {
reject({
status: false,
sync: false,
sw: true,
message: 'Registered Service worker, but was unable to activate sync',
error: err
});
});
} else {
reject({
status: false,
sync: false,
sw: true,
message: 'Registered Service worker, but was unable to activate sync',
error: null
});
}
} else {
resolve({
status: true,
sync: false,
sw: true
});
}
})
.catch(err=>{
reject({
status: false,
sync: false,
sw: false,
message: 'Failed registering service worker',
error: err
});
});
}
}else{
reject({
status: false,
sync: false,
sw: false,
message: 'Service Worker not supported',
error: null
});
}
});
};
if (typeof window !== 'undefined') {
window.DSW = DSW;
}
}
export default DSW;