UNPKG

dsw

Version:

Dynamic Service Worker, offline Progressive Web Apps much easier

1,143 lines (1,040 loc) 48 kB
var isInSWScope = false; var isInTest = typeof global.it === 'function'; var preCache; var failedAppShellFiles = []; var ROOT_SW_SCOPE = null; import logger from './logger.js'; import getBestMatchingRX from './best-matching-rx.js'; import cacheManager from './cache-manager.js'; import goFetch from './go-fetch.js'; import strategies from './strategies.js'; import utils from './utils.js'; const DSW = { version: '#@!THE_DSW_VERSION_INFO!@#', build: '#@!THE_DSW_BUILD_TIMESTAMP!@#', ready: null }; const REQUEST_TIME_LIMIT = 5000; const REGISTRATION_TIMEOUT = 12000; const DEFAULT_NOTIF_DURATION = 6000; const currentlyMocking = {}; var installationFailure = err=>{ let errMessage = `Failed storing appshell.\n ${failedAppShellFiles.join(',')} failed loading.\n`; return errMessage; }; function getSWRoot () { if (ROOT_SW_SCOPE) { return ROOT_SW_SCOPE; } ROOT_SW_SCOPE = (new URL(location.href)) .pathname .replace(/\/[^\/]+$/, '/'); return ROOT_SW_SCOPE; } // These will be used in both ServiceWorker and Client scopes DSW.isOffline = DSW.offline = _=>{ return !navigator.onLine; }; DSW.isOnline = DSW.online = _=>{ return navigator.onLine; }; // 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 = { requestId: 0, tracking: {}, trackMoved: {}, 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); } else { // if it is supposed NOT to cache if (newRule.action.cache === false) { newRule.strategy = 'online-first'; } } return newRule; }, treatBadPage (response, pathName, event) { let result; DSWManager.traceStep( event.request, 'Request failed', { status: response.status, statusText: response.statusText, url: response.url, type: response.type }); (DSWManager.rules[ response && response.status? response.status : 404 ] || []) .some((cur, idx)=>{ let matching = pathName.match(cur.rx); if (matching) { if (cur.action.redirect && !cur.action.fetch) { cur.action.fetch = cur.action.redirect; } if (cur.action.fetch) { DSWManager.traceStep( event.request, 'Found fallback rule', cur, false, { url: event.request.url, id: event.request.requestId, steps: event.request.traceSteps }); // not found requests should // fetch a different resource let req = new Request(cur.action.fetch); req.requestId = event.request.requestId; req.traceSteps = event.request.traceSteps; // applyMatch result = cacheManager.get(cur, req, event, matching); return true; // stopping the loop } } }); if (!result) { DSWManager.traceStep(event.request, 'No fallback found. Request failed'); } return result || response; }, // SW Scope's setup setup (dswConfig={}) { DSWManager.rootSWScope = getSWRoot(); // let's prepare both cacheManager and strategies with the // current referencies utils.setup(DSWManager, PWASettings, DSW); cacheManager.setup(DSWManager, PWASettings, goFetch); strategies.setup(DSWManager, cacheManager, goFetch); return DSW.ready = new Promise((resolve, reject)=>{ // we will prepare and store the rules here, so it becomes // easier to deal with, latelly on each requisition preCache = PWASettings.appShell || []; let dbs = []; Object.keys(dswConfig.rules || dswConfig.dswRules).forEach(heuristic=>{ let ruleName = heuristic; heuristic = (dswConfig.rules || 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 (!heuristic.match.length && !Object.keys(heuristic.match).length) { // if there is nothing to match...we do nothing return; } if (!Object.keys(heuristic.match).length) { // if there is nothing to apply, we do nothing with it, either return; } 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 || '' ); 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)) || (appl.cache && appl.cache.from)) { preCache.push({ url: (appl.cache && appl.cache.from) ? appl.cache.from : (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', strategy: 'fastest', match: { path: /^\/dsw.js(\?=dsw-manager)?$/ }, 'apply': { cache: { } } }, location.href); // adding the root path to be also cached by default let rootMatchingRX = /^(\/|\/index(\.[0-1a-z]+)?)$/; this.addRule('*', { name: 'rootDir', strategy: 'fastest', match: { path: getSWRoot() }, 'apply': { cache: { } } }, rootMatchingRX); preCache.unshift(getSWRoot()); // 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) { let url = cur.url||cur; return cacheManager .add(url, null, null, cur.rule) .catch(err=>{ // in case it is a Response if (err && err.status == 404) { failedAppShellFiles.push(err.url); throw new Error(installationFailure()); } }); }).concat(dbs.map(function(cur) { return cacheManager.createDB(cur).catch(err=>{ throw new Error('Failed preparing IndexedDB\n', err.message || err); }); }) )) .then(_=>{ resolve(); }) .catch(err=>{ let errMessage = 'Could not register the service worker.' + '\n' + (err.url || err.message) + '\n'; logger.error(errMessage, err); reject(errMessage); }); }else{ resolve(); } }); }, getRulesBeforeFetching () { // returns all the rules for * or 200 return this.rules['*'] || false; }, createRequest (request, event, matching) { return goFetch(null, request, event, matching); }, createRedirect (request, event, matching) { return goFetch(null, request, event, matching); }, traceStep (request, step, data, fill=false, moved=false) { // we may also receive a list of requests if (Array.isArray(request)) { request.forEach(req=>{ DSWManager.traceStep(req, step, data, fill, moved); }); return; } // if there are no tracking listeners, this request will not be tracked if (DSWManager.tracking) { let id = request.requestId; request.traceSteps = request.traceSteps || []; data = data || {}; if (fill) { data.url = request.url; data.type = request.type; data.method = request.method; data.redirect = request.redirect; data.referrer = request.referrer; } let reqTime = ((performance.now() - request.timeArriving) / 1000); request.traceSteps.push({ step, data, timing: reqTime.toFixed(4) + 's' // timing from the begining of the request }); } // but if it has moved, we then track it if (moved) { DSWManager.trackMoved[moved.url] = moved; } }, respondItWith (event, response) { // respond With This // first of all...we respond the event event.respondWith(new Promise((resolve, reject)=>{ if (typeof response.then == 'function') { response.then(result=>{ if (typeof result.clone != 'function') { return resolve(result); } let response = result.clone(); // then, if it has been tracked, let's tell the listeners if (DSWManager.tracking && response.status != 302) { response.text().then(result=>{ // if the result is a string (text, html, etc) // we will preview only a small part of it if ((result[0] || '').charCodeAt(0) < 128) { result = result.substring(0, 180) + (result.length > 180? '...': ''); } DSWManager.traceStep( event.request, 'Responded', { response: { status: response.status, statusText: response.statusText, type: response.type, method: response.method, url: response.url, }, preview: result }, true); DSWManager.sendTraceData(event); }); } resolve(result); }); } else { resolve(response); } })); }, sendTraceData (event) { let tracker; let traceBack = (port, key)=>{ // sending the trace information back to client let traceData = { id: event.request.requestId, src: event.request.traceSteps[0].data.url, method: event.request.traceSteps[0].data.method, steps: event.request.traceSteps }; // if it has been redirected if (traceData.src != event.request.url) { traceData.redirectedTo = event.request.url; } port.postMessage(traceData); }; // here we will send a message to each listener in the client(s) // with the trace information for(tracker in DSWManager.tracking) { if (event.request.url.match(tracker)) { DSWManager.tracking[tracker].ports.forEach(traceBack); break; } } // let's clear the garbage left from the request if (event.request.traceSteps && event.request.traceSteps.length) { delete DSWManager.trackMoved[event.request.traceSteps[0].data.url]; } }, broadcast (message) { return clients.matchAll().then(result=>{ result.forEach(cur=>{ cur.postMessage(message); }); }); }, startListening () { // and from now on, we listen for any request and treat it self.addEventListener('fetch', event=>{ DSWManager.requestId = 1 + (DSWManager.requestId || 0); // let's deal with tracking information for the current request if (DSWManager.trackMoved[event.request.url]) { let movedInfo = DSWManager.trackMoved[event.request.url]; event.request.requestId = movedInfo.id; event.request.traceSteps = movedInfo.steps; event.request.originalSrc = movedInfo.url; if (movedInfo.rule && movedInfo.rule.action && movedInfo.rule.action.cache && movedInfo.rule.action.cache.from) { // it has moved, but is supposed to be cached from somewhere else // because it uses variables that are not supposed to be cached event.request.cachedFrom = movedInfo.rule.action.cache.from; } delete DSWManager.trackMoved[event.request.url]; } else { // it is a brand new request event.request.requestId = DSWManager.requestId; event.request.timeArriving = performance.now(); DSWManager.traceStep(event.request, 'Arrived in Service Worker', {}, true); } // these are the request's url structured data that we need const url = new URL(event.request.url); const sameOrigin = url.origin == location.origin; const pathName = url.pathname; // Service Workers can only deal with GET requests if (event.request.method != 'GET') { DSWManager.traceStep(event.request, `Ignoring ${event.request.method} request` , {}); DSWManager.sendTraceData(event); return; } // in case there are no rules (happens when chrome crashes, for example) if (!Object.keys(DSWManager.rules).length) { return DSWManager.setup(PWASettings).then(_=>fetch(event)); } // in case we want to enforce https if (PWASettings.enforceSSL) { if (url.protocol != 'https:' && url.hostname != 'localhost') { DSWManager.traceStep(event.request, 'Redirected from http to https'); return DSWManager.respondItWith(event, Response.redirect( event.request.url.replace('http:', 'https:'), 302)); } } // get the best fiting rx for the path, to find the rule that // matches the most let matchingRule; if (!sameOrigin) { matchingRule = getBestMatchingRX(url.origin + url.pathname, DSWManager.rules['*']); } else { matchingRule = getBestMatchingRX(pathName, DSWManager.rules['*']); } if (matchingRule) { // if there is a rule that matches the url // we add a trace step for it DSWManager.traceStep( event.request, `Best matching rule found: "${matchingRule.rule.name}"`, { rule: matchingRule.rule, url: event.request.url }); // and then respond the request with the promise for the content return DSWManager.respondItWith( event, // we apply the right strategy for the matching rule strategies[matchingRule.rule.strategy]( matchingRule.rule, event.request, event, matchingRule.matching ) ); } else { // this means there were no rules to apply if (!sameOrigin) { // if it is not sameOrigin and there is no rule for it DSWManager.traceStep( event.request, 'Ignoring request because it is not from same origin and there are no rules for it', {} ); DSWManager.sendTraceData(event); return; } } // if no rule is applied, we will request it // this is the function to deal with the result of this request let defaultTreatment = function (response) { if (response && (response.status == 200 || response.type == 'opaque' || response.type == 'opaqueredirect')) { return response; } else { return DSWManager.treatBadPage(response, pathName, event); } }; // once no rule matched, we simply respond the event with a fetch return DSWManager.respondItWith( event, fetch(goFetch(null, event.request)) // but we will still treat the rules that use the status .then(defaultTreatment) .catch(defaultTreatment) ); }); } }; let DSWStatus = false; let DSWStatusError = null; 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).then(_=>{ DSWManager.broadcast({ DSWStatus, error: DSWStatusError || null }); }).catch(err=>{ DSWManager.broadcast({ DSWStatus, error: err || DSWStatusError }); }); })()); }); 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) { return event.waitUntil( DSWManager.setup(PWASettings) .then(_=>{ DSWStatus = true; self.skipWaiting(); }) .catch(err=>{ DSWStatus = false; DSWStatusError = err; self.skipWaiting(); }) ); }else{ return event.waitUntil(DSWManager.setup(PWASettings)); } }); self.addEventListener('message', function(event) { const ports = event.ports; if (event.data.trackPath) { let tp = event.data.trackPath; DSWManager.tracking[tp] = { rx: new RegExp(tp, 'i'), ports: ports }; return; } if (event.data.clearEverythingUp) { cacheManager.clear() .then(result=>{ ports.forEach(port=>{ port.postMessage({ cacheCleaned: true }); }); }); return; } if (event.data.enableMocking) { let mockId = event.data.enableMocking.mockId; let matching = event.data.enableMocking.match; let finalMockId = mockId + matching; // we will mock only for some clients (this way you can have two tabs with different approaches) currentlyMocking[event.source.id] = currentlyMocking[event.source.id] || {}; let client = currentlyMocking[event.source.id]; // this client will mock the rules in mockId client[finalMockId] = client[finalMockId] || []; currentlyMocking[finalMockId].push(); // TODO: add mock support return; } }); self.addEventListener('push', function(event) { // let's trigger the event DSWManager.broadcast({ event: 'pushnotification', data: event.data }); if (PWASettings.notification && PWASettings.notification.dataSrc) { // if there is a dataSrc defined, we fetch it return event.waitUntil(fetch(PWASettings.notification.dataSrc).then(response=>{ if (response.status == 200) { // then to use it as the structure for the notification return response.json().then(data=>{ let notifData = {}; if (PWASettings.notification.dataPath) { notifData = data[PWASettings.notification.dataPath]; } else { notifData = data; } let notif = self.registration.showNotification(notifData.title, { 'body': notifData.body || notifData.content || notifData.message, 'icon': notifData.icon || notifData.image, 'tag': notifData.tag || null }); }); } else { throw new Error(`Fetching ${PWASettings.notification.dataSrc} returned a ${response.status} status.`); } }).catch(err=>{ logger.warn('Received a push, but Failed retrieving the notification data.', err); })); } else if (PWASettings.notification.title) { // you can also specify the message data let n = PWASettings.notification; let notif = self.registration.showNotification( n.title, { 'body': n.body || n.content || n.message, 'icon': n.icon || n.image, 'tag': n.tag || null }); } }); // When user clicks/touches the notification, we shall close it and open // or focus the web page self.addEventListener('notificationclick', function(event) { let tag = event.notification.tag, targetUrl, eventData = { tag, title: event.notification.title, body: event.notification.body, icon: event.notification.icon, badge: event.notification.badge, lang: event.notification.lang, timestamp: event.notification.timestamp }; event.notification.close(); // the targetUul is the used to know if DSW should open a new window, // focus a window or simply trigger the event if (PWASettings.notification && PWASettings.notification.target !== void(0)) { targetUrl = PWASettings.notification.target; } else { targetUrl = location.toString(); } event.waitUntil( // let's look for all windows(or frames) that are using our sw clients.matchAll({ type: 'window' }).then(function(windowClients) { let p; // and let's see if any of these is already our page for (let i = 0; i < windowClients.length; i++) { let client = windowClients[i]; // if it is, we simply focus it if ((client.url == targetUrl || (new URL(client.url)).pathname == targetUrl) && 'focus' in client) { p= client.focus(); break; } } // if it is not opened, we open it if (!p && targetUrl && clients.openWindow) { p= clients.openWindow(targetUrl); } else { // if not, we simply resolve it p = Promise.resolve(); } // now we execute the promise (either a openWindow or focus) p.then(_=>{ // and then trigger the event DSWManager.broadcast({ event: 'notificationclicked', data: eventData, }); }); }) ); }); self.addEventListener('sync', function(event) { // TODO: add support to sync event as browsers evolve and support the feature //debugger; }); DSWManager.startListening(); }else{ DSW.status = { version: PWASettings.version || PWASettings.dswVersion, registered: false, sync: false, appShell: false, notification: false }; let pendingResolve, pendingReject, registeredServiceWorker, installationTimeOut; const eventManager = (()=>{ const events = {}; return { addEventListener (eventName, listener) { events[eventName] = events[eventName] || []; events[eventName].push(listener); }, trigger (eventName, data={}) { let listener; try { if (events[eventName]) { for (listener of events[eventName]) { if (typeof listener == 'function') { listener(data); } } } listener = 'on' + eventName; if (typeof DSW[listener] == 'function') { DSW[listener](data); } }catch(e){ if (listener && listener.name) { listener = listener.name; } else { listener = listener || 'annonymous'; } logger.error(`Failed trigerring event ${eventName} on listener ${listener}` , e.message, e); } } }; })(); // let's store some events, so it can be autocompleted in devTools DSW.addEventListener = eventManager.addEventListener; DSW.onpushnotification = function () { /* use this to know when a notification arrived */ }; DSW.onnotificationclicked = function () { /* use this to know when the user has clicked in a notification */ }; DSW.onenabled = function () { /* use this to know when DSW is enabled and running */ }; DSW.onregistered = function () { /* use this to know when DSW has been registered */ }; DSW.onunregistered = function () { /* use this to know when DSW has been unregistered */ }; DSW.onnotificationsenabled = function () { /* use this to know when user has enabled notifications */ }; navigator.serviceWorker.addEventListener('message', event=>{ // if it is waiting for the installation confirmation if (pendingResolve && event.data.DSWStatus !== void(0)) { // and if the message is about a successful installation if (registeredServiceWorker) { // this means all the appShell have been downloaded if (event.data.DSWStatus) { DSW.status.appShell = true; eventManager.trigger('activated', DSW.status); pendingResolve(DSW.status); } else { // if it failed, let's unregister it, to avoid false positives DSW.status.appShell = false; DSW.status.error = event.data.error; pendingReject(DSW.status); registeredServiceWorker.unregister(); } } pendingResolve = false; pendingReject = false; } eventManager.trigger(event.data.event, event.data.data); // yeah, I know ¬¬ }); DSW.trace = function (match, options, callback) { if (!callback && typeof options == 'function') { callback = options; options = {}; } var messageChannel = new MessageChannel(); messageChannel.port1.onmessage = function(event) { callback(event.data); }; navigator.serviceWorker .controller .postMessage({ trackPath: match }, [messageChannel.port2]); }; DSW.enableMocking = function (mockId, match='.*') { var messageChannel = new MessageChannel(); navigator.serviceWorker .controller .postMessage({ enableMocking: { mockId, match } }, [messageChannel.port2]); }; DSW.disableMocking = function (mockId, match='.*') { var messageChannel = new MessageChannel(); navigator.serviceWorker .controller .postMessage({ disableMocking: { mockId, match } }, [messageChannel.port2]); }; DSW.sendMessage = (message, waitForAnswer=false)=>{ // This method sends a message to the service worker. // Useful for specific tokens and internal use and trace return new Promise((resolve, reject)=>{ var messageChannel = new MessageChannel(); // in case the user expects an answer from the SW after sending // this message... if (waitForAnswer) { // we will wait for it, and then resolve or reject only when // the SW has answered messageChannel.port1.onmessage = function(event) { if (event.data.error) { reject(event.data.error); } else { resolve(event.data); } }; } else { // otherwise, we simply resolve it, after 10ms (just to use another flow) setTimeout(resolve, 10); } navigator.serviceWorker .controller .postMessage(message, [messageChannel.port2]); }); }; DSW.onNetworkStatusChange = callback=>{ let cb = function () { callback(navigator.onLine); }; window.addEventListener('online', cb); window.addEventListener('offline', cb); // in case we are already offline, we will trigger now, the callback // this way, fevelopers will know right away that their app has loaded // offline if(!navigator.onLine) { cb(); } }; // this means all the appShell dependencies have been downloaded and // the sw has been successfuly installed and registered DSW.isAppShellDone = DSW.isActivated = _=>{ return DSW.status.registered && DSW.status.appShell; }; DSW.isRegistered = _=>{ return DSW.status.registered; }; // this method will register the SW for push notifications // but is not really connected to web notifications (the popup message) DSW.enableNotifications = _=>{ return new Promise((resolve, reject)=>{ if (navigator.onLine) { navigator.serviceWorker.ready.then(function(reg) { let req = reg.pushManager.subscribe({ userVisibleOnly: true }); return req.then(function(sub) { DSW.status.notification = sub.endpoint; eventManager.trigger('notificationsenabled', DSW.status); logger.info('Registered to notification server'); resolve(sub); }).catch(reason=>{ reject(reason || 'Not allowed by user'); }); }); } else { reject('Must be connected to enable notifications'); } }); }; DSW.notify = (title='Untitled', options={})=>{ return new Promise((resolve, reject)=>{ DSW.enableNotifications().then(_=>{ const opts = { body: options.body || '', icon: options.icon || false, tag: options.tag || null }; let n = new Notification(title, opts); if (options.duration) { setTimeout(_=>{ n.close(); }, options.duration * 1000); } resolve(n); }).catch(reason=>{ reject(reason); }); }); }; DSW.unregister = _=>{ return new Promise((resolve, reject)=>{ if (DSW.status) { // if it is not registered or has already been unregistered // we simply resolve the promise if (!DSW.status.registered) { resolve(DSW.status); } } DSW.ready.then(result=>{ cacheManager.clear() // firstly, we clear the caches .then(result=>{ if (result) { DSW.status.appShell = false; localStorage.setItem('DSW-STATUS', JSON.stringify(DSW.status)); // now we try and unregister the ServiceWorker registeredServiceWorker.unregister() .then(success=>{ if (success) { DSW.status.registered = false; DSW.status.sync = false; DSW.status.notification = false; DSW.status.ready = false; localStorage.setItem('DSW-STATUS', JSON.stringify(DSW.status)); resolve(DSW.status); eventManager.trigger('unregistered', DSW.status); } else { reject('Could not unregister service worker'); } }); // TODO: clear indexedDB too // indexedDBManager.delete(); } else { reject('Could not clean up the caches'); } }); }); }); }; // client's setup DSW.setup = (config={}) => { // in case DSW.setup has already been called if (DSW.ready) { return DSW.ready; } return new Promise((resolve, reject)=>{ // this promise rejects in case of errors, and only resolved in case // the service worker has just been registered. DSW.ready = new Promise((resolve, reject)=>{ let appShellPromise = new Promise((resolve, reject)=>{ pendingResolve = function(){ clearTimeout(installationTimeOut); resolve(DSW.status); }; }); pendingReject = function(reason){ clearTimeout(installationTimeOut); reject(reason || 'Installation timeout'); }; // opening on a page scope...let's install the worker if (navigator.serviceWorker) { if (!navigator.serviceWorker.controller) { // rejects the registration after some time, if not resolved by then installationTimeOut = setTimeout(_=>{ reject('Registration timed out'); }, config.timeout || REGISTRATION_TIMEOUT); let documentBodyPromise = new Promise(resolve=>{ if (document.readyState === 'complete') { resolve(document); } else { document.addEventListener('DOMContentLoaded', function() { resolve(document); }); } }); // we will use the same script, already loaded, for our service worker var src = document.querySelector('script[src$="dsw.js"]').getAttribute('src'); Promise.all([ documentBodyPromise, navigator.serviceWorker.register(src) ]).then(SW=>{ registeredServiceWorker = SW; DSW.status.registered = true; navigator.serviceWorker.ready.then(function(reg) { DSW.status.ready = true; eventManager.trigger('registered', DSW.status); Promise.all([ appShellPromise, // enabling notification if they were set to "auto" new Promise((resolve, reject)=>{ if (PWASettings.notification && PWASettings.notification.auto) { return DSW.enableNotifications(); } else { resolve(); } }), new Promise((resolve, reject)=>{ // setting up sync if (config && config.sync) { if ('SyncManager' in window) { navigator.serviceWorker.ready.then(function(reg) { return reg.sync.register('syncr'); }) .then(_=>{ DSW.status.sync = true; resolve(); }); } else { DSW.status.sync= 'Failed enabling sync'; resolve(); } } else { resolve(); } }) ]).then(_=>{ localStorage.setItem('DSW-STATUS', JSON.stringify(DSW.status)); eventManager.trigger('enabled', DSW.status); logger.info('Service Worker was registered', DSW.status); resolve(DSW.status); }); }); }) .catch(err=>{ reject({ status: false, sync: false, sw: false, message: 'Failed registering service worker with the message:\n ' + (err.message), error: err }); }); } else { // service worker was already registered and is active // setting up traceable requests if (config && config.trace) { navigator.serviceWorker.ready.then(function(reg) { registeredServiceWorker = reg; let match; for(match in config.trace){ DSW.trace(match, config.trace[match]); } }); } else { navigator.serviceWorker.ready.then(function(reg) { registeredServiceWorker = reg; }); } // on refreshes, we update the variable to be used in the API DSW.status = JSON.parse(localStorage.getItem('DSW-STATUS')) || DSW.status; resolve(DSW.status); } } else { DSW.status.fail = 'Service worker not supported'; } }); // if it is not activated, we return the "ready" promise if (!DSW.isActivated()) { return DSW.ready .then(result=>{ resolve(result); }) .catch(reason=>{ reject(reason); }); } }); }; if (typeof window !== 'undefined') { window.DSW = DSW; } } export default DSW;