diff --git a/lib/bootstrap.js b/lib/bootstrap.js
index cc7004ff..6954045b 100644
--- a/lib/bootstrap.js
+++ b/lib/bootstrap.js
@@ -24,6 +24,7 @@ var rename = require('gulp-rename');
var promptly = require('promisified-promptly');
var gitconfiglocal = require('gitconfiglocal');
var through2 = require('through2');
+var gutil = require('gulp-util');
function gitUrl(dir) {
return new Promise(function (resolve, reject) {
@@ -124,7 +125,9 @@ module.exports = function(config) {
})
.then(function(templateConfig) {
return new Promise(function(resolve, reject) {
- var stream = gulp.src([__dirname + '/../templates/**'])
+ var contents = __dirname + '/../templates/**';
+ var workerTemplate = __dirname + '/../templates/app/offline-worker.js';
+ var stream = gulp.src([contents, '!' + workerTemplate])
.pipe(rename(function (path) {
// NPM can't include a .gitignore file so we have to rename it.
if (path.basename === 'gitignore') {
diff --git a/lib/offline.js b/lib/offline.js
index 88732148..bb2187fe 100644
--- a/lib/offline.js
+++ b/lib/offline.js
@@ -20,11 +20,13 @@
var promisify = require('promisify-node');
var fs = require('fs');
+var conflict = require('gulp-conflict');
var path = require('path');
-var swPrecache = require('sw-precache');
-var gutil = require('gulp-util');
-var ghslug = promisify(require('github-slug'));
var glob = require('glob');
+var template = require('gulp-template');
+var crypto = require('crypto');
+var gulp = require('gulp');
+var ghslug = promisify(require('github-slug'));
module.exports = promisify(function(config, callback) {
var rootDir = config.rootDir || './';
@@ -40,7 +42,7 @@ module.exports = promisify(function(config, callback) {
var fileGlobs = config.fileGlobs || ['**/*'];
- // Remove the existing service worker, if any, so sw-precache doesn't include
+ // Remove the existing service worker, if any, so offliner doesn't include
// it in the list of files to cache.
try {
fs.unlinkSync(path.join(rootDir, 'offline-worker.js'));
@@ -59,29 +61,49 @@ module.exports = promisify(function(config, callback) {
return '';
}
}).then(function(cacheId) {
- var staticFileGlobs = fileGlobs.map(function(v) {
- return path.join(rootDir, v)
- });
+ var absoluteGlobs =
+ fileGlobs.map(function (v) { return path.join(rootDir, v); });
+ var files = flatGlobs(absoluteGlobs);
+ var filesAndHashes = getFilesAndHashes(files, rootDir);
+ var replacements = { resources: filesAndHashes, name: cacheId };
+
+ var stream = gulp.src([__dirname + '/../templates/app/offline-worker.js'])
+ .pipe(template(replacements))
+ .pipe(conflict(rootDir))
+ .pipe(gulp.dest(rootDir));
- staticFileGlobs.forEach(function(globPattern) {
- glob.sync(globPattern.replace(path.sep, '/')).forEach(function(file) {
- var stat = fs.statSync(file);
+ stream.on('finish', function () { callback(); });
+ stream.on('error', function (e) { callback(e); });
+ });
- if (stat.isFile() && stat.size > 2 * 1024 * 1024 && staticFileGlobs.indexOf(file) === -1) {
- gutil.log(gutil.colors.red.bold(file + ' is bigger than 2 MiB. Are you sure you want to cache it? To suppress this warning, explicitly include the file in the fileGlobs list.'));
- }
+ function flatGlobs(fileGlobs) {
+ return Object.keys(fileGlobs.reduce(function (matchings, fileGlob) {
+ fileGlob = fileGlob.replace(path.sep, '/');
+ glob.sync(fileGlob, { nodir: true }).forEach(function (m) {
+ matchings[m] = m;
});
- });
+ return matchings;
+ }, {}));
+ }
+
+ function getFilesAndHashes(files, stripPrefix) {
+ return files
+ .map(function (filepath) {
+ var data = fs.readFileSync(filepath);
+ var hash = getHash(data);
+ return {
+ filepath: filepath.replace(stripPrefix, function (match, offset) {
+ return offset === 0 ? '' : match;
+ }),
+ hash: hash
+ };
+ });
+ }
+
+ function getHash(data) {
+ var sha1 = crypto.createHash('sha1');
+ sha1.update(data);
+ return sha1.digest('hex');
+ }
- swPrecache.write(path.join(rootDir, 'offline-worker.js'), {
- staticFileGlobs: staticFileGlobs,
- stripPrefix: rootDir,
- verbose: true,
- logger: gutil.log,
- importScripts: config.importScripts || [],
- cacheId: cacheId,
- ignoreUrlParametersMatching: config.ignoreUrlParametersMatching || [/./],
- maximumFileSizeToCacheInBytes: Infinity,
- }, callback);
- });
});
diff --git a/package.json b/package.json
index 4269e1bd..7b648f81 100644
--- a/package.json
+++ b/package.json
@@ -51,7 +51,6 @@
"promisify-node": "^0.2.1",
"read-yaml": "^1.0.0",
"rimraf": "^2.4.3",
- "sw-precache": "^2.0.0",
"temp": "^0.8.3",
"through2": "^2.0.0",
"travis-ci": "^2.0.3",
@@ -60,7 +59,6 @@
},
"devDependencies": {
"chai": "^3.3.0",
- "glob": "^5.0.15",
"gulp-istanbul": "^0.10.1",
"gulp-mocha": "^2.1.3",
"mocha": "^2.3.3",
diff --git a/templates/app/index.html b/templates/app/index.html
index 179b1190..91874eb7 100644
--- a/templates/app/index.html
+++ b/templates/app/index.html
@@ -29,8 +29,6 @@
-
-
diff --git a/templates/app/offline-worker.js b/templates/app/offline-worker.js
new file mode 100644
index 00000000..1999528c
--- /dev/null
+++ b/templates/app/offline-worker.js
@@ -0,0 +1,19 @@
+importScripts('./scripts/offliner/offliner.js');
+importScripts('./scripts/offliner/middlewares.js');
+
+var offliner = new off.Offliner("<%= name %>");
+offliner.prefetch.use(off.fetchers.urls).resources([
+<% resources.forEach(function (resource) {
+%> '<%= resource.filepath %>', /* <%= resource.hash %> */
+<% }); %>
+]);
+
+offliner.fetch
+ .use(off.sources.cache)
+ .use(off.sources.network)
+ .orFail();
+
+offliner.update
+ .use(off.updaters.reinstall);
+
+offliner.standalone();
diff --git a/templates/app/scripts/offline-manager.js b/templates/app/scripts/offline-manager.js
index a0521435..fd65dcdf 100644
--- a/templates/app/scripts/offline-manager.js
+++ b/templates/app/scripts/offline-manager.js
@@ -1,29 +1,32 @@
-function updateFound() {
- var installingWorker = this.installing;
+(function (global) {
+ 'use strict';
- // Wait for the new service worker to be installed before prompting to update.
- installingWorker.addEventListener('statechange', function() {
- switch (installingWorker.state) {
- case 'installed':
- // Only show the prompt if there is currently a controller so it is not
- // shown on first load.
- if (navigator.serviceWorker.controller &&
- window.confirm('An updated version of this page is available, would you like to update?')) {
- window.location.reload();
- return;
- }
- break;
-
- case 'redundant':
- console.error('The installing service worker became redundant.');
- break;
- }
- });
-}
+ if ('serviceWorker' in navigator) {
+ var script = document.createElement('SCRIPT');
+ script.src = 'scripts/offliner/offliner-client.js';
+ script.dataset.worker = 'offline-worker.js';
+ script.onload = function () {
+ var off = global.off.restore();
+ var isActivationDelayed = false;
-if ('serviceWorker' in navigator) {
- navigator.serviceWorker.register('offline-worker.js').then(function(registration) {
- console.log('offline worker registered');
- registration.addEventListener('updatefound', updateFound);
- });
-}
+ off.on('activationPending', function () {
+ if (confirm('An updated version of this page is available, would you like to update?')) {
+ off.activate().then(function () { window.location.reload(); });
+ }
+ else if (!isActivationDelayed) {
+ global.addEventListener('beforeunload', function () {
+ off.activate();
+ });
+ isActivationDelayed = true;
+ }
+ });
+ off.install().then(function () {
+ console.log('offline worker registered');
+ });
+ };
+ document.addEventListener('DOMContentLoaded', function onBody() {
+ document.removeEventListener('DOMContentLoaded', onBody);
+ document.body.appendChild(script);
+ });
+ }
+}(this));
diff --git a/templates/app/scripts/offliner/middlewares.js b/templates/app/scripts/offliner/middlewares.js
new file mode 100644
index 00000000..56b2e4ad
--- /dev/null
+++ b/templates/app/scripts/offliner/middlewares.js
@@ -0,0 +1,46 @@
+
+self.off.sources.cache = function (request, activeCache) {
+ return activeCache.match(request).then(function (response) {
+ return response ? Promise.resolve(response) : Promise.reject();
+ });
+};
+self.off.sources.network = function (request) {
+ return fetch(request);
+};
+
+self.off.fetchers.urls = {
+
+ type: 'url',
+
+ normalize: function (resource) {
+ return { type: this.type, url: resource };
+ },
+
+ prefetch: function (resources, cache) {
+ return Promise.all(resources.map(function (resource) {
+ var bustedUrl = resource.url + '?__b=' + Date.now();
+ var request = new Request(bustedUrl, { mode: 'no-cors' });
+ return fetch(request).then(function (response) {
+ var url = new URL(request.url, location);
+ if (url.pathname.match(/\/index\.html?$/)) {
+ cache.put('/', response.clone());
+ }
+ cache.put(resource.url, response);
+ });
+ }));
+ }
+};
+
+self.off.updaters.reinstall = {
+ check: function () {
+ return Promise.resolve('v' + Date().toString());
+ },
+
+ isNewVersion: function () {
+ return this.flags.isCalledFromInstall;
+ },
+
+ evolve: function (previousCache, newCache, reinstall) {
+ return reinstall();
+ }
+};
diff --git a/templates/app/scripts/offliner/offliner-client.js b/templates/app/scripts/offliner/offliner-client.js
new file mode 100644
index 00000000..7aebb58e
--- /dev/null
+++ b/templates/app/scripts/offliner/offliner-client.js
@@ -0,0 +1,369 @@
+(function (exports) {
+ 'use strict';
+
+ var nextPromiseId = 1;
+
+ var originalOff = exports.off;
+
+ var root = (function () {
+ var root = new URL(
+ document.currentScript.dataset.root || '',
+ window.location.origin
+ ).href;
+ return root.endsWith('/') ? root : root + '/';
+ }());
+
+ var workerURL =
+ root + (document.currentScript.dataset.worker || 'offliner-worker.js');
+
+ /**
+ * The exported global `off` object contains methods for communicating with
+ * the offliner worker in charge.
+ *
+ * @class OfflinerClient
+ */
+ exports.off = {
+
+ /**
+ * Callbacks for the events.
+ *
+ * @property _eventListeners
+ * @type Object
+ * @private
+ */
+ _eventListeners: {},
+
+ /**
+ * Implementation callbacks for cross promises by its unique id.
+ *
+ * @property _xpromises
+ * @type Object
+ * @private
+ */
+ _xpromises: {},
+
+ /**
+ * Call `restore()` when you want the `off` name in the global scope for
+ * other purposes. The method will restore the previous contents to the
+ * global variable and return the `OfflinerClient`.
+ *
+ * @method restore
+ * @return {OfflinerClient} The current offliner client.
+ */
+ restore: function () {
+ exports.off = originalOff;
+ return this;
+ },
+
+ /**
+ * Register the offliner worker. The worker will be installed with
+ * root `/` [scope](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register#Syntax)
+ * unless you add the `data-root` attribute to the script tag.
+ *
+ * In the same way, the client will look for a script in the specified root
+ * called `offliner-worker.js`. If you want to change this behaviour, use
+ * the `data-worker` attribute.
+ *
+ * For instance, suppose your web application is running under:
+ * https://delapuente.github.com/offliner
+ *
+ * And you have your worker at:
+ * https://delapuente.github.com/offliner/worker.js
+ *
+ * Then the script tag should looks like:
+ * ```html
+ *
+ * ```
+ *
+ * @method install
+ * @return {Promise} A promise resolving if the installation success.
+ */
+ install: function () {
+ if (!('serviceWorker' in navigator)) {
+ return Promise.reject(new Error('serviceworkers-not-supported'));
+ }
+
+ return navigator.serviceWorker.register(workerURL, {
+ scope: root
+ }).then(function (registration) {
+ return this.connect().then(function () {
+ return registration;
+ });
+ }.bind(this));
+ },
+
+ /**
+ * Keeps the promise of connect.
+ *
+ * @property _connecting
+ * @type Promise
+ * @private
+ */
+ _connecting: null,
+
+ /**
+ * Connects the client with offliner allowing the client to control offliner
+ * and receive events.
+ *
+ * @method connect
+ * @return {Promise} A promise resolving once connection has been stablished
+ * with the worker and communication is possible.
+ */
+ connect: function () {
+ if (!this._connecting) { this._connecting = this._connect(); }
+ return this._connecting;
+ },
+
+ /**
+ * The actual implementation for {{#crossLink "connect:method"}}{{/crossLink}}
+ *
+ * @method _connect
+ * @return {Promise} A promise resolving once connection has been stablished
+ * with the worker and communication is possible.
+ * @private
+ */
+ _connect: function () {
+ if (!('serviceWorker' in navigator)) {
+ return Promise.reject(new Error('serviceworkers-not-supported'));
+ }
+
+ var installMessageHandlers = this._installMessageHandlers.bind(this);
+ var checkForActivationPending = this._checkForActivationPending.bind(this);
+ return new Promise(function (fulfill, reject) {
+ navigator.serviceWorker.getRegistration(root).then(function (registration) {
+ if (registration.active) {
+ installMessageHandlers();
+ checkForActivationPending();
+ return fulfill();
+ }
+
+ var installingWorker = registration.installing;
+ if (!installingWorker) {
+ return reject(new Error('impossible-to-connect'));
+ }
+
+ installingWorker.onstatechange = function () {
+ if (installingWorker.state === 'installed') {
+ installMessageHandlers();
+ checkForActivationPending();
+ fulfill();
+ }
+ };
+ });
+ });
+ },
+
+ /**
+ * Attaches a listener for a type of event.
+ *
+ * @method on
+ * @param type {String} The type of the event.
+ * @param handler {Callback} The callback receiving the event.
+ * @param willBeThis {Object} The context object `this` for the `handler`.
+ */
+ on: function (type, handler, willBeThis) {
+ if (!this._has(type, handler, willBeThis)) {
+ this._eventListeners[type] = this._eventListeners[type] || [];
+ this._eventListeners[type].push([handler, willBeThis]);
+ }
+ },
+
+ /**
+ * Request an update to offliner.
+ *
+ * @method update
+ * @return {Promise} If the update process is successful, the promise will
+ * resolve to a new version and an
+ * {{#crossLink "OfflinerClient/activationPending:event"}}{{/crossLink}}
+ * will be triggered. If the update is not needed, the promise will be
+ * rejected with `no-update-needed` reason.
+ */
+ update: function () {
+ return this._xpromise('update');
+ },
+
+ /**
+ * Performs the activation of the pending update. I.e. replaces the current
+ * cache with that updated in the update process. Normally, you want to
+ * reload the application when the activation ends successfuly.
+ *
+ * @method activate
+ * @return {Promise} A promise resolving into the activated version or
+ * rejected with `no-activation-pending` if there was not an activation.
+ */
+ activate: function () {
+ return this._xpromise('activate');
+ },
+
+ /**
+ * Run the listeners for some type of event.
+ *
+ * @method _runListeners
+ * @param type {String} The type of the events selecting the listeners to
+ * be run.
+ * @param evt {Object} The event contents.
+ * @private
+ */
+ _runListeners: function (type, evt) {
+ var listeners = this._eventListeners[type] || [];
+ listeners.forEach(function (listenerAndThis) {
+ var listener = listenerAndThis[0];
+ var willBeThis = listenerAndThis[1];
+ listener.call(willBeThis, evt);
+ });
+ },
+
+ /**
+ * Registers the listeners for enabling communication between the worker
+ * and the client code.
+ *
+ * @method _installMessageHandlers
+ * @private
+ */
+ _installMessageHandlers: function installMessageHandlers() {
+ var that = this;
+ if (!installMessageHandlers.done) {
+ if (typeof BroadcastChannel === 'function') {
+ var bc = new BroadcastChannel('offliner-channel');
+ bc.onmessage = onmessage;
+ }
+ else {
+ navigator.serviceWorker.addEventListener('message', onmessage);
+ }
+ installMessageHandlers.done = true;
+ }
+
+ function onmessage(e) {
+ var msg = e.data;
+ var type = msg ? msg.type : '';
+ var typeAndSubType = type.split(':');
+ if (typeAndSubType[0] === 'offliner') {
+ that._handleMessage(typeAndSubType[1], msg);
+ }
+ }
+ },
+
+ /**
+ * Make offliner to check for pending activations and dispatch
+ * {{#crossLink "OfflinerClient/activationPending:event"}}{{/crossLink}}
+ * if so.
+ *
+ * @method _checkForActivationPending
+ * @private
+ */
+ _checkForActivationPending: function () {
+ // TODO: should we add a prefix for offliner messages?
+ this._send({ type: 'checkForActivationPending' });
+ },
+
+ /**
+ * Discriminates between {{#crossLink "OfflinerClient/xpromise:event"}}{{/crossLink}}
+ * events which are treated in a special way and the rest of the events that
+ * simply trigger the default dispatching algorithm.
+ *
+ * @method _handleMessage
+ * @param offlinerType {String} The type of the message without the
+ * `offliner:` prefix.
+ * @param msg {Any} The event.
+ * @private
+ */
+ _handleMessage: function (offlinerType, msg) {
+ var sw = navigator.serviceWorker;
+ if (offlinerType === 'xpromise') {
+ this._resolveCrossPromise(msg);
+ }
+ else {
+ this._runListeners(offlinerType, msg);
+ }
+ },
+
+ /**
+ * @method _has
+ * @param type {String} The type for the listener registration.
+ * @param handler {Function} The listener.
+ * @param willBeThis {Object} The context object `this` which the function
+ * will be called with.
+ * @return `true` if the listener registration already exists.
+ * @private
+ */
+ _has: function (type, handler, willBeThis) {
+ var listeners = this._eventListeners[type] || [];
+ for (var i = 0, listenerAndThis; (listenerAndThis = listeners[i]); i++) {
+ if (listenerAndThis[0] === handler &&
+ listenerAndThis[1] === willBeThis) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Creates a cross promise registration. A _cross promise_ or xpromise
+ * is a special kind of promise that is generated in the client but whose
+ * implementation is in a worker.
+ *
+ * @method _xpromise
+ * @param order {String} The string for the implementation part to select
+ * the implementation to run.
+ * @return {Promise} A promise delegating its implementation in some code
+ * running in a worker.
+ * @private
+ */
+ _xpromise: function (order) {
+ return new Promise(function (accept, reject) {
+ var uniqueId = nextPromiseId++;
+ var msg = {
+ type: 'xpromise',
+ id: uniqueId,
+ order: order
+ };
+ this._xpromises[uniqueId] = [accept, rejectWithError];
+ this._send(msg);
+
+ function rejectWithError(errorKey) {
+ reject(new Error(errorKey)); // TODO: Add a OfflinerError type
+ }
+ }.bind(this));
+ },
+
+ /**
+ * Sends a message to the worker.
+ *
+ * @method _send
+ * @param msg {Any} The message to be sent.
+ * @private
+ */
+ _send: function (msg) {
+ navigator.serviceWorker.getRegistration(root)
+ .then(function (registration) {
+ if (!registration || !registration.active) {
+ // TODO: Wait for the service worker to be active and try to
+ // resend.
+ console.warn('Not service worker active right now.');
+ }
+ else {
+ return registration.active.postMessage(msg);
+ }
+ });
+ },
+
+ /**
+ * Resolves a cross promise based on information received by the
+ * implementation in the worker.
+ *
+ * @method _resolveCrossPromise
+ * @param msg {Object} An object with the proper data to resolve a xpromise.
+ * @private
+ */
+ _resolveCrossPromise: function (msg) {
+ var implementation = this._xpromises[msg.id];
+ if (implementation) {
+ implementation[msg.status === 'rejected' ? 1 : 0](msg.value);
+ }
+ else {
+ console.warn('Trying to resolve unexistent promise:', msg.id);
+ }
+ }
+ };
+
+}(this.exports || this));
diff --git a/templates/app/scripts/offliner/offliner.js b/templates/app/scripts/offliner/offliner.js
new file mode 100644
index 00000000..939a4ebc
--- /dev/null
+++ b/templates/app/scripts/offliner/offliner.js
@@ -0,0 +1,1090 @@
+(function (self) {
+ 'use strict';
+
+ ['log', 'warn', 'error'].forEach(function (method) {
+ self[method] = console[method].bind(console);
+ });
+
+ var DEFAULT_VERSION = '-offliner:v0';
+ var CONFIG_CACHE = '__offliner-config';
+
+ /**
+ * @class UpdateControl
+ * @private
+ */
+
+ /**
+ * Indicates if updates have been scheduled.
+ * @property scheduled
+ * @type boolean
+ */
+
+ /**
+ * Set to `true` when the update has run once.
+ * @property alreadyRunOnce
+ * @type boolean
+ */
+
+ /**
+ * Holds the reference to the timer for the next update.
+ * @property intervalId
+ * @type Number
+ */
+
+ /**
+ * Holds the reference to the promise representing the currently running
+ * update process.
+ * @property inProgressProcess
+ * @type Object
+ */
+
+ /**
+ * Creates a new Offliner instance.
+ * @param {String} - a unique name representing the offline handler. This
+ * allow you to instantiate several offliners for the same or different
+ * workers without causing collisions between the configuration and cache
+ * names.
+ *
+ * @class Offliner
+ */
+ function Offliner(uniquename) {
+ Object.defineProperty(this, '_uniquename', {
+ get: function () { return uniquename ? uniquename + ':' : ''; }
+ });
+
+ /**
+ * Prevent the worker to be installed twice.
+ *
+ * @property _isStarted
+ * @type boolean
+ * @default false
+ * @private
+ */
+ this._isStarted = false;
+
+ /**
+ * Mark the instance to be used as middleware.
+ *
+ * @property _isMiddleware
+ * @type boolean
+ * @default false
+ * @private
+ */
+ this._isMiddleware = false;
+
+ /**
+ * The middleware implementation for serviceworkerware.
+ *
+ * @property _middleware;
+ * @type Object
+ * @default null
+ * @private
+ */
+ this._middleware = null;
+
+ /**
+ * The global update control.
+ *
+ * @property _updateControl
+ * @type UpdateControl
+ * @readonly
+ * @private
+ */
+ Object.defineProperty(this, '_updateControl', { value: {
+ scheduled: false,
+ alreadyRunOnce: false,
+ intervalId: null,
+ inProgressProcess: null
+ }});
+
+ /**
+ * API to configure the fetching pipeline.
+ *
+ * @property fetch
+ * @type FetchConfig
+ * @readonly
+ */
+ Object.defineProperty(this, 'fetch', { value: new FetchConfig() });
+
+ /**
+ * API to configure the prefetch process.
+ *
+ * @type PrefetchConfig
+ * @property prefetch
+ * @readonly
+ */
+ Object.defineProperty(this, 'prefetch', { value: new PrefetchConfig() });
+
+ /**
+ * API to configure the update process.
+ *
+ * @type UpdateConfig
+ * @property update
+ * @readonly
+ */
+ Object.defineProperty(this, 'update', { value: new UpdateConfig() });
+ }
+
+ /**
+ * Installs the service worker in stand-alone mode.
+ * @method standalone
+ * @throws {Error} offliner throws when trying to install it in standalone
+ * mode if it was already used as middleware by calling
+ * {{#crossLink "Offliner/asMiddleware:method"}}{{/crossLink}}.
+ */
+ Offliner.prototype.standalone = function () {
+ if (this._isMiddleware) {
+ throw new Error('offliner has been already started as a middleware.');
+ }
+
+ if (this._isStarted) { return; }
+
+ self.addEventListener('install', function (e) {
+ e.waitUntil(
+ this._install()
+ .then(function () {
+ log('Offliner installed');
+ return typeof self.skipWaiting === 'function' ?
+ self.skipWaiting() : Promise.resolve();
+ })
+ );
+ }.bind(this));
+
+ self.addEventListener('activate', function (e) {
+ var ok = function () {
+ log('Offliner activated!');
+ return typeof self.clients.claim === 'function' ?
+ self.clients.claim() : Promise.resolve();
+ };
+ e.waitUntil(
+ this._activate().then(ok, ok)
+ );
+ }.bind(this));
+
+ self.addEventListener('fetch', function (e) {
+ if (e.request.method !== 'GET') {
+ e.respondWith(fetch(e.request));
+ }
+ else {
+ e.respondWith(this._fetch(e.request));
+ }
+ }.bind(this));
+
+ self.addEventListener('message', function (e) {
+ this._processMessage(e.data);
+ }.bind(this));
+
+ this._isStarted = true;
+ };
+
+ /**
+ * Returns an object to be used with [serviceworkerware](https://github.com/arcturus/serviceworkerware).
+ * Once the method is called once, the method will allways return the same
+ * object.
+ *
+ * @method asMiddleware
+ * @return {Object} A serviceworkerware middleware.
+ * @throws {Error} offliner will throw if you try to use it as middleware
+ * after calling {{#crossLink "Offliner/standalone:method"}}{{/crossLink}}.
+ */
+ Offliner.prototype.asMiddleware = function () {
+ if (this._isStarted) {
+ throw new Error('offliner has been already installed in standalone mode');
+ }
+
+ if (!this._middleware) {
+ this._middleware = {
+ onInstall: this._install.bind(this),
+ onActivate: this._activate.bind(this),
+ onFetch: function (request, response) {
+ if (response || request.method !== 'GET') {
+ return Promise.resolve(response);
+ }
+ this._fetch(request);
+ }.bind(this),
+ onMessage: function (e) { this._processMessage(e.data); }.bind(this)
+ };
+ }
+
+ this._isMiddleware = true;
+ return this._middleware;
+ };
+
+ Offliner.prototype._activate = function () {
+ return this.get('activation-pending')
+ .then(function (isActivationPending) {
+ if (isActivationPending) { this._sendActivationPending(); }
+ }.bind(this));
+ };
+
+ /**
+ * Process the different messages that can receive the worker.
+ *
+ * @method _processMessage
+ * @private
+ */
+ Offliner.prototype._processMessage = function (msg) {
+ switch (msg.type) {
+ case 'xpromise':
+ this._receiveCrossPromise(msg.id, msg.order);
+ break;
+ case 'checkForActivationPending':
+ this._checkForActivationPending();
+ break;
+ default:
+ warn('Message not recognized:', msg);
+ break;
+ }
+ };
+
+ /**
+ * Executes the promise implementation.
+ *
+ * @method _receiveCrossPromise
+ * @param id {String} The unique id for the cross promise.
+ * @param order {String} The order to be executed.
+ * @private
+ */
+ Offliner.prototype._receiveCrossPromise = function (id, order) {
+ switch (order) {
+ case 'update':
+ var fromInstall = false;
+ this._update().then(
+ this._resolve.bind(this, id),
+ this._reject.bind(this, id)
+ );
+ break;
+ case 'activate':
+ this._activateNextCache().then(
+ this._resolve.bind(this, id),
+ this._reject.bind(this, id)
+ );
+ break;
+ default:
+ warn('Cross Promise implementation not recognized:', order);
+ break;
+ }
+ };
+
+ /**
+ * Check if there is an activation pending. If so, offliner dispatches an
+ * activation pending request.
+ *
+ * @method _checkForActivationPending
+ * @private
+ */
+ Offliner.prototype._checkForActivationPending = function () {
+ this.get('activation-pending').then(function (isActivationPending) {
+ if (isActivationPending) {
+ this._sendActivationPending();
+ }
+ }.bind(this));
+ };
+
+ /**
+ * Resolves a cross promise.
+ *
+ * @method _resolve
+ * @param id {String} The unique id for the cross promise.
+ * @param value {Any} The value to resolve the promise with.
+ * @private
+ */
+ Offliner.prototype._resolve = function (id, value) {
+ this._resolvePromise(id, 'resolved', value);
+ };
+
+ /**
+ * Rejects a cross promise.
+ *
+ * @method _reject
+ * @param id {String} The unique id for the cross promise.
+ * @param reason {Any} The value to reject the promise with.
+ * @private
+ */
+ Offliner.prototype._reject = function (id, reason) {
+ this._resolvePromise(id, 'rejected', reason);
+ };
+
+ /**
+ * Broadcast a message to the clients informing the cross promise to be
+ * solved in which status and with which value.
+ *
+ * @method _resolvePromise
+ * @param id {String} The unique id for the cross promise.
+ * @param status {String} The status at which the promise will solve to.
+ * Can be `'rejected'` or `'solved'`.
+ * @param value {Any} The value for the cross promise.
+ * @private
+ */
+ Offliner.prototype._resolvePromise = function (id, status, value) {
+ this._broadcastMessage({
+ type: 'xpromise',
+ id: id,
+ status: status,
+ value: value
+ });
+ };
+
+ /**
+ * Gets a setting for the offliner handler.
+ *
+ * @method get
+ * @param {String} key The setting to be retrieved.
+ * @private
+ */
+ Offliner.prototype.get = function (key) {
+ var configURL = this._getConfigURL(key);
+ return caches.open(CONFIG_CACHE).then(function (cache) {
+ return cache.match(configURL).then(function (response) {
+ if (!response) { return Promise.resolve(null); }
+ else { return response.json(); }
+ });
+ });
+ };
+
+ /**
+ * Sets a setting for the offliner handler.
+ *
+ * @method set
+ * @param {String} key The setting.
+ * @param {any} value The value to be set.
+ * @private
+ */
+ Offliner.prototype.set = function (key, value) {
+ var configURL = this._getConfigURL(key);
+ var response = new Response(JSON.stringify(value));
+ return caches.open(CONFIG_CACHE).then(function (cache) {
+ return cache.put(configURL, response);
+ });
+ };
+
+ /**
+ * Return a fake URL scheme for a setting.
+ *
+ * @method _getConfigURL
+ * @param {String} key The setting.
+ * @return a fake URL scheme for the setting.
+ * @private
+ */
+ Offliner.prototype._getConfigURL = function (key) {
+ return 'http://config/' + this._uniquename + key;
+ };
+
+ /**
+ * Determine if the worker should prefetch or update after (re)installing the
+ * service worker.
+ *
+ * @method _install
+ * @private
+ */
+ Offliner.prototype._install = function () {
+ var fromInstall = true;
+ return this.get('current-version').then(function (currentVersion) {
+ var isUpdateEnabled = this.update.option('enabled');
+ if (currentVersion) {
+ return isUpdateEnabled ? this._update(fromInstall) : Promise.resolve();
+ }
+ return this._initialize().then(this._prefetch.bind(this));
+ }.bind(this), error);
+ };
+
+ /**
+ * Initializes the current version and active cache for the first time.
+ *
+ * @method _initialize
+ * @private
+ */
+ Offliner.prototype._initialize = function () {
+ return this._getCacheNameForVersion(DEFAULT_VERSION)
+ .then(this.set.bind(this, 'active-cache'))
+ .then(this.set.bind(this, 'current-version', DEFAULT_VERSION))
+ .then(this.set.bind(this, 'activation-pending', false));
+ };
+
+ /**
+ * Performs a generic update process. It consists into:
+ *
+ * 1. Check for a new version using a middleware.
+ * 2. Prepare the new version database.
+ * 3. Evolve the offline cache using the middleware.
+ * 4. Clean-up.
+ *
+ * @method _update
+ * @param {Boolean} fromInstall Indicates if the call comes from the
+ * {{#crossLink "Offliner/_install:method"}}{{/crossLink}} method.
+ * @return {Promise} A Promise resolving in the vertion to update or rejecting
+ * if there is no update needed (`reason = 'no-update-needed'`).
+ * @private
+ */
+ Offliner.prototype._update = function (fromInstall) {
+ // XXX: Only one update process is allowed at a time.
+ var that = this;
+ if (!this._updateControl.inProgressProcess) {
+ this._updateControl.inProgressProcess = this.get('current-version')
+ .then(function (currentVersion) {
+ this.update.flags = {
+ isCalledFromInstall: fromInstall,
+ isFirstUpdate: (currentVersion === DEFAULT_VERSION)
+ };
+ }.bind(this))
+ .then(this._getLatestVersion.bind(this))
+ .then(this._checkIfNewVersion.bind(this))
+ .then(updateCache);
+ }
+ return this._updateControl.inProgressProcess;
+
+ function updateCache(newVersion) {
+ if (newVersion) {
+ return that._getCacheNameForVersion(newVersion)
+ .then(caches.open.bind(caches))
+ .then(that._evolveCache.bind(that))
+ .then(that.set.bind(that, 'activation-pending', true))
+ .then(that._sendActivationPending.bind(that))
+ .then(function () {
+ endUpdateProcess(); // XXX: Notice this call before ending!
+ return Promise.resolve(newVersion);
+ });
+ }
+ endUpdateProcess(); // XXX: Notice this call before ending!
+ return Promise.reject('no-update-needed');
+ }
+
+ function endUpdateProcess() {
+ that._updateControl.alreadyRunOnce = true;
+ that._updateControl.inProgressProcess = null;
+ }
+ };
+
+ /**
+ * Broadcast a message to all clients to indicate there is an update
+ * activation ready.
+ *
+ * @method _sendActivationPending
+ * @private
+ */
+ Offliner.prototype._sendActivationPending = function () {
+ /**
+ * Event emitted on worker activation or under request to point out there
+ * is a new version activation pending.
+ *
+ * @event activationPending
+ * @for OfflinerClient
+ */
+ this._broadcastMessage({ type: 'activationPending' });
+ };
+
+ /**
+ * Broadcast a message to all clients to indicate the activation of the
+ * new version ended properly.
+ *
+ * @method _sendActivationDone
+ * @private
+ * @for Offliner
+ */
+ Offliner.prototype._sendActivationDone = function () {
+ this._broadcastMessage({ type: 'activationDone' });
+ };
+
+ /**
+ * Broadcast a message to all clients to indicate there was a failure while
+ * activating the update.
+ *
+ * @method _sendActivationFailed
+ * @private
+ */
+ Offliner.prototype._sendActivationFailed = function () {
+ this._broadcastMessage({ type: 'activationFailed' });
+ };
+
+ /**
+ * Broadcast a message in the clients. The method will add the `offliner:`
+ * prefix to the type of the events but this is stripped out automatically by
+ * the {{#crossLink "OfflinerClient/_installMessageHandlers:method"}}{{/crossLink}}
+ * client side.
+ *
+ * @method _broadcastMessage
+ * @param msg {Any} the message to be broadcasted.
+ * @private
+ */
+ Offliner.prototype._broadcastMessage = function (msg) {
+ msg.type = 'offliner:' + msg.type;
+ if (this._isMiddleware) {
+ this.asMiddleware().broadcastMessage(msg, 'offliner-channel');
+ }
+ else {
+ if (typeof BroadcastChannel === 'function') {
+ var channel = new BroadcastChannel('offliner-channel');
+ channel.postMessage(msg);
+ channel.close();
+ }
+ else {
+ clients.matchAll().then(function (controlled) {
+ controlled.forEach(function (client) {
+ client.postMessage(msg);
+ });
+ });
+ }
+ }
+ };
+
+ /**
+ * Return the CACHE name for a version given.
+ *
+ * @method _getCacheNameForVersion
+ * @param {String} version The version to calculate the name for.
+ * @return {Promise} A promise resolving with the name for the
+ * version.
+ * @private
+ */
+ Offliner.prototype._getCacheNameForVersion = function (version) {
+ return Promise.resolve(this._uniquename + 'cache-' + version);
+ };
+
+ /**
+ * Opens current active cache and starts prefetch.
+ *
+ * @method _prefetch
+ * @private
+ */
+ Offliner.prototype._prefetch = function () {
+ return this._openActiveCache().then(this._doPrefetch.bind(this));
+ };
+
+ /**
+ * Processes prefetch declared resources using the registered middlewares.
+ *
+ * @method _doPrefetch
+ * @param {Cache} cache The cache for the middlewares to populate.
+ * @private
+ */
+ Offliner.prototype._doPrefetch = function (cache) {
+ var allResources = this.prefetch.resources();
+ var fetchers = this.prefetch.fetchers();
+ var resourcesByType = groupResources(fetchers, allResources);
+ return fetchers.reduce(function (process, fetcher) {
+ return process.then(function () {
+ var resources = resourcesByType[fetcher.type];
+ return fetcher.prefetch(resources, cache);
+ });
+ }, Promise.resolve());
+
+ function groupResources(fetchers, resources) {
+ var resourceGatherers = fetchers.reduce(function (gatherers, fetcher) {
+ gatherers[fetcher.type] = [];
+ return gatherers;
+ }, {});
+ resources.forEach(function (resource) {
+ var resourcesByType = resourceGatherers[resource.type];
+ if (resourcesByType) { resourcesByType.push(resource); }
+ });
+ return resourceGatherers;
+ }
+ };
+
+ /**
+ * Obtains the latest version using the update middleware.
+ *
+ * @method _getLatestVersion
+ * @return {Promise} Tag representing the latest version. The tag will
+ * be used as suffix for the new cache.
+ * @private
+ */
+ Offliner.prototype._getLatestVersion = function () {
+ return this.update.check();
+ };
+
+ /**
+ * Determine if there is a new version based on the latest version and the
+ * current one by using the update middleware.
+ *
+ * @method _checkIfNewVersion
+ * @return {Promise} latestVersion The new version tag is returned
+ * if there is a new version or `null` otherwise.
+ * @private
+ */
+ Offliner.prototype._checkIfNewVersion = function (latestVersion) {
+ return this.get('current-version').then(function (currentVersion) {
+ var isNewVersion =
+ this.update.isNewVersion(currentVersion, latestVersion);
+
+ if (isNewVersion) {
+ log('New version ' + latestVersion + ' found!');
+ if (currentVersion) { log('Updating from version ' + currentVersion); }
+ else { log('First update'); }
+
+ return this.set('next-version', latestVersion)
+ .then(function () { return latestVersion; });
+ }
+ else {
+ log('No update needed');
+ }
+ return null;
+ }.bind(this));
+ };
+
+ /**
+ * Evolves the current cache to the new cache by using the update middleware.
+ *
+ * @method _evolveCache
+ * @param {Cache} newCache The new cache.
+ * @private
+ */
+ Offliner.prototype._evolveCache = function (newCache) {
+ return this._openActiveCache().then(function (currentCache) {
+ var reinstall = this._doPrefetch.bind(this, newCache);
+ return this.update.evolve(currentCache, newCache, reinstall);
+ }.bind(this));
+ };
+
+ /**
+ * Uses dynamic information to open the active CACHE.
+ *
+ * @method _openActiveCache
+ * @return {Promise} A promise resolving to the active cache.
+ * @private
+ */
+ Offliner.prototype._openActiveCache = function () {
+ return this.get('active-cache').then(caches.open.bind(caches));
+ };
+
+ /**
+ * Change the active cache to be the evolved cache if available. Once the
+ * active cache has been updated, the former one is lost.
+ *
+ * @method _activateNextCache
+ * @return {Promise} A Promise resolving in the new version or rejecting
+ * if there is no pending activation.
+ * @private
+ */
+ Offliner.prototype._activateNextCache = function () {
+ return this.get('activation-pending').then(function (isActivationPending) {
+ if (isActivationPending) {
+ return this._swapCaches()
+ .then(this._updateCurrentVersion.bind(this));
+ }
+ return Promise.reject('no-activation-pending');
+ }.bind(this));
+ };
+
+ /**
+ * Makes active cache to be the next-version cache populated during a past
+ * update process. After swapping, the previous cache is lost.
+ *
+ * @method _swapCaches
+ * @private
+ */
+ Offliner.prototype._swapCaches = function () {
+ var that = this;
+ return Promise.all([
+ getCurrentCache(),
+ getNextCache()
+ ]).then(swap);
+
+ function getCurrentCache() {
+ return that.get('active-cache');
+ }
+
+ function getNextCache() {
+ return that.get('next-version')
+ .then(that._getCacheNameForVersion.bind(that));
+ }
+
+ function swap(names) {
+ var currentCache = names[0],
+ nextCache = names[1];
+ return that.set('active-cache', nextCache)
+ .then(deleteOtherCaches([nextCache, CONFIG_CACHE]));
+ }
+
+ function deleteOtherCaches(exclude) {
+ return function () {
+ return caches.keys().then(function (cacheNames) {
+ return Promise.all(
+ cacheNames.filter(function (cacheName) {
+ return exclude.indexOf(cacheName) < 0;
+ })
+ .map(function (cacheName) {
+ return caches.delete(cacheName);
+ })
+ );
+ });
+ };
+ }
+ };
+
+ /**
+ * Updates the current version.
+ *
+ * @method _updateCurrentVersion
+ * @private
+ */
+ Offliner.prototype._updateCurrentVersion = function () {
+ var nextVersion = this.get('next-version');
+ return nextVersion
+ .then(this.set.bind(this, 'current-version'))
+ .then(this.set.bind(this, 'activation-pending', false))
+ .then(function () { return nextVersion; });
+ };
+
+ /**
+ * Use configured middlewares to perform the fetch process.
+ *
+ * @method _fetch
+ * @param {Request} request The request to be fetched.
+ * @private
+ */
+ Offliner.prototype._fetch = function (request) {
+ return new Promise(function (resolve, reject) {
+ this._openActiveCache().then(function (cache) {
+ var sources = this.fetch.pipeline();
+ trySources(sources);
+
+ function trySources(sources, from) {
+ from = from || 0;
+ var sourcesCount = sources.length;
+ if (from === sources.length) { reject(); }
+ else {
+ sources[from](request, cache).then(resolve, function () {
+ trySources(sources, from + 1);
+ });
+ }
+ }
+ }.bind(this));
+ }.bind(this));
+ };
+
+ /**
+ * A resource is an object with a type and other fields to be retrieved by
+ * the {{#crossLink "Fetcher"}}{{/crossLink}} with the same type.
+ * @class Resource
+ */
+
+ /**
+ * The type to associate the resource with an specific
+ * {{#crossLink "Fetcher"}}{{/crossLink}}.
+ *
+ * @property type
+ * @type String
+ * @readonly
+ */
+
+ /**
+ * A fetcher is an object for handling resouces during the prefetching
+ * prefetch process. A fetcher must include a `type` and normalize and
+ * prefetch implementations.
+ *
+ * @class Fetcher
+ * @private
+ */
+
+ /**
+ * While prefetching resources, each resource has a `type`. The resource
+ * is handled by the fetcher whose `type` match it.
+ *
+ * @property type
+ * @type String
+ * @readonly
+ */
+
+ /**
+ * Normalizes a resource not following the {{#crossLink "Resource"}}
+ * {{/crossLink}} convention.
+ *
+ * @method normalize
+ * @param {any} resource The denormalized resource.
+ */
+
+ /**
+ * Retrieve a set of resources.
+ *
+ * @method prefetch
+ * @param {Resource[]} resource The denormalized resource.
+ * @param {Cache} cache The cache to populate.
+ */
+
+ /**
+ * Prefetch process consists into recovering from the Web those
+ * resources configured in offliner. To do so, you call
+ * {{#crossLink "PrefetchConfig/use:method"}}{{/crossLink}}, then list the
+ * resources by calling {{#crossLink "PrefetchConfig/resources:method"}}
+ * {{/crossLink}}.
+ *
+ * @class PrefetchConfig
+ */
+ function PrefetchConfig() {
+ this._resourceFetchers = {};
+ this._resources = [];
+ }
+
+ /**
+ * Register a {{#crossLink "Fetcher"}}{{/crossLink}}. The fetcher will be used
+ * to retrieve the resources of the fetcher's type.
+ *
+ * @method use
+ * @param {Fetcher} fetcher The fetcher to be used for resources of fetcher's
+ * type.
+ * @chainable
+ */
+ PrefetchConfig.prototype.use = function (fetcher) {
+ this._resourceFetchers[fetcher.type] = fetcher;
+ this._activeFetcher = fetcher;
+ return this;
+ };
+
+ /**
+ * Add resources to the prefetch list of resources.
+ *
+ * @method resources
+ * @param {Resource|Resource[]} resources The list of resources to be added.
+ * Each resource in the list is normalized by the last registered fetcher so
+ * some fetchers allows a short syntax for its resources.
+ * @chainable
+ */
+ PrefetchConfig.prototype.resources = function (resources) {
+ if (arguments.length === 0) { return this._resources; }
+
+ if (!Array.isArray(resources)) { resources = [resources]; }
+ for (var i = 0, resource; (resource = resources[i]); i++) {
+ var normalized;
+ if (typeof resource !== 'object' || !resource || !resource.type) {
+ try {
+ normalized = this._activeFetcher.normalize(resource);
+ }
+ catch (e) {}
+ }
+ if (!normalized) {
+ warn(resource, 'can not be normalized by', this._activeFetcher.type);
+ }
+ else {
+ this._resources.push(normalized);
+ }
+ }
+ return this;
+ };
+
+ /**
+ * @method fetchers
+ * @return {Fetcher[]} the registered fetchers.
+ */
+ PrefetchConfig.prototype.fetchers = function () {
+ return Object.keys(this._resourceFetchers).map(function (type) {
+ return this._resourceFetchers[type];
+ }.bind(this));
+ };
+
+ /**
+ * An object implementing methods to check for new version and update the
+ * activate cache.
+ *
+ * @class UpdateImplementation
+ */
+
+ /**
+ * Checks for a new version.
+ *
+ * @method check
+ * @return {Promise} A promise resolving in the new version.
+ */
+
+ /**
+ * Determines if the checked new version is actually a new version.
+ *
+ * @method isNewVersion
+ * @param {String} currentVersion The current version.
+ * @param {String} latestVersion The version from
+ * {{#crossLink "UpdateImplementation/check:method"}}{{/crossLink}}.
+ * @return {Boolean}
+ */
+
+ /**
+ * Populate the updated cache.
+ *
+ * @method evolve
+ * @param {Cache} currentCache The current active cache. **Do not modify this
+ * cache!**
+ * @param {Cache} nextCache The cache to be populated.
+ * @param {Function} reinstall A function to trigger the prefetch process. Some
+ * update algorithms just want to prefetch again.
+ * @return {Promise} A promise resolving after finishing the update process.
+ * If you simply wants to simply reinstall, return the value from `reinstall`
+ * invocation.
+ */
+
+ /**
+ * Update consists into determine if there is a new version and
+ * then evolve the current cache to be up to date. To register an update
+ * algorithm you provide a {{#crossLink "UpdateImplementation"}}
+ * {{/crossLink}} instance by using {{#crossLink "UpdateConfig/use:method"}}
+ * {{/crossLink}}.
+ *
+ * @class UpdateConfig
+ */
+ function UpdateConfig() {
+ this._options = {};
+ }
+
+ /**
+ * Gets or set an option.
+ *
+ * @method option
+ * @param {String} optname The name of the option to be set or get.
+ * @param {any} [value] If provided, the value to be set for the passed option.
+ * @chainable
+ * @return {any} The value of the option when getting.
+ */
+ UpdateConfig.prototype.option = function (optname, value) {
+ if (arguments.length === 2) {
+ this._options[optname] = value;
+ return this;
+ }
+ if (arguments.length === 1) {
+ return this._options[optname];
+ }
+ };
+
+ /**
+ * Register the update implementation.
+ *
+ * @method use
+ * @param {UpdateImplementation} impl The update implementation to be used.
+ * @chainable
+ */
+ UpdateConfig.prototype.use = function (impl) {
+ this.option('enabled', true);
+ this._impl = impl;
+ return this;
+ };
+
+ /**
+ * Flags set at the beginning of the update process. They include:
+ *
+ * @property flags
+ * @type UpdateFlags
+ */
+ Object.defineProperty(UpdateConfig.prototype, 'flags', {
+ set: function (value) {
+ this._impl.flags = value;
+ },
+ get: function () {
+ return this._impl.flags;
+ }
+ });
+
+ /**
+ * Triggers the {{#crossLink "UpdateImplementation/check:method"}}
+ * {{/crossLink}} algorithm of the registered update implementation.
+ *
+ * @method check
+ */
+ UpdateConfig.prototype.check = function () {
+ return this._impl && this._impl.check();
+ };
+
+ /**
+ * Calls the {{#crossLink "UpdateImplementation/isNewVersion:method"}}
+ * {{/crossLink}} check of the registered update implementation.
+ *
+ * @method isNewVersion
+ */
+ UpdateConfig.prototype.isNewVersion =
+ function (currentVersion, latestVersion) {
+ return this._impl.isNewVersion(currentVersion, latestVersion);
+ };
+
+ /**
+ * Performs the {{#crossLink "UpdateImplementation/evolve:method"}}
+ * {{/crossLink}} process of the registered update implementation.
+ *
+ * @method evolve
+ */
+ UpdateConfig.prototype.evolve = function (currentCache, newCache, prefetch) {
+ return this._impl.evolve(currentCache, newCache, prefetch);
+ };
+
+ /**
+ * A source handler is a **function** that accepts a request and the
+ * active cache and return a Promise resolving into the proper Response. It's
+ * used with {{#crossLink "FetchConfig/use:method"}}{{/crossLink}} of
+ * {{#crossLink "FetchConfig"}}{{/crossLink}}.
+ *
+ * `sourceHandler(request, activeCache)`
+ *
+ * @class SourceHandler
+ */
+
+ /**
+ * The fetch process consists into pass the request along a list
+ * of source handlers. You call {{#crossLink "FetchConfig/use:method"}}
+ * {{/crossLink}} to add a new source handler to the pipeline.
+ *
+ * @class FetchConfig
+ */
+ function FetchConfig() {
+ this._pipeline = [];
+ }
+
+ /**
+ * Adds a new {{#crossLink "SourceHandler"}}{{/crossLink}} to the fetching
+ * pipeline.
+ *
+ * @method use
+ * @param {SourceHandler} source The handler to be added to the pipeline.
+ * @chainable
+ */
+ FetchConfig.prototype.use = function (source) {
+ this._pipeline.push(source);
+ return this;
+ };
+
+ /**
+ * Gets the current pipeline of sources.
+ *
+ * @method pipeline
+ * @return {SourceHandler[]} The current pipeline of source handlers.
+ */
+ FetchConfig.prototype.pipeline = function () {
+ return this._pipeline;
+ };
+
+ /**
+ * Adds an always failing source handler to the pipeline.
+ *
+ * @method orFail
+ */
+ FetchConfig.prototype.orFail = function () {
+ this.use(function () {
+ return Promise.reject(new Error('End of fetch pipeline!'));
+ });
+ };
+
+ /**
+ * The exported module for offliner.
+ * @module off
+ */
+ self.off = {};
+
+ self.off.Offliner = Offliner;
+
+ /**
+ * A collection of {{#crossLink "SourceHandler"}}{{/crossLink}}
+ * constructors to configure offliner.
+ * @submodule sources
+ */
+ self.off.sources = {};
+
+ /**
+ * A collection of {{#crossLink "Fetcher"}}{{/crossLink}} constructors to
+ * configure offliner.
+ * @submodule fetchers
+ */
+ self.off.fetchers = {};
+
+ /**
+ * A collection of {{#crossLink "UpdateImplementation"}}{{/crossLink}}
+ * constructors to configure offliner.
+ * @submodule updaters
+ */
+ self.off.updaters = {};
+
+}(typeof self === 'undefined' ? this : self));
+
diff --git a/test/testOffline.js b/test/testOffline.js
index 98acb602..52bdfd34 100644
--- a/test/testOffline.js
+++ b/test/testOffline.js
@@ -71,18 +71,6 @@ describe('Offline', function() {
});
});
- it('should use importScript in the service worker if the importScripts option is defined', function() {
- var dir = temp.mkdirSync('oghliner');
-
- return offline({
- rootDir: dir,
- importScripts: [ 'a-script.js', ],
- }).then(function() {
- var content = fs.readFileSync(path.join(dir, 'offline-worker.js'), 'utf8');
- assert.notEqual(content.indexOf('importScripts("a-script.js");'), -1);
- });
- });
-
it('should use the GitHub slug as the cache ID if it is available', function() {
var rootDir = temp.mkdirSync('oghliner');
var dir = path.join(rootDir, 'dist');
@@ -219,13 +207,7 @@ describe('Offline', function() {
process.chdir(rootDir);
- var checkWarnings = checkWrite([
- 'test_file_1.js is bigger than 2 MiB',
- 'test_file_2.js is bigger than 2 MiB',
- 'test_file_3.js is bigger than 2 MiB',
- ], [], 'Total precache size');
-
- var offlinePromise = offline({
+ return offline({
rootDir: dir,
}).then(function() {
var content = fs.readFileSync(path.join(dir, 'offline-worker.js'), 'utf8');
@@ -233,8 +215,6 @@ describe('Offline', function() {
assert.notEqual(content.indexOf('test_file_2.js'), -1);
assert.notEqual(content.indexOf('test_file_3.js'), -1);
});
-
- return Promise.all([ checkWarnings, offlinePromise ]);
});
it('should not cache excluded files', function() {
@@ -250,14 +230,7 @@ describe('Offline', function() {
process.chdir(rootDir);
- var checkWarnings = checkWrite([
- 'test_file_2.js is bigger than 2 MiB',
- 'test_file_3.js is bigger than 2 MiB',
- ], [
- 'test_file_1.js is bigger than 2 MiB',
- ], 'Total precache size');
-
- var offlinePromise = offline({
+ return offline({
rootDir: dir,
fileGlobs: [
'!(test_file_1.js)',
@@ -268,43 +241,5 @@ describe('Offline', function() {
assert.notEqual(content.indexOf('test_file_2.js'), -1);
assert.notEqual(content.indexOf('test_file_3.js'), -1);
});
-
- return Promise.all([ checkWarnings, offlinePromise ]);
- });
-
- it('should not warn about explicitly included files', function() {
- var rootDir = temp.mkdirSync('oghliner');
- var dir = path.join(rootDir, 'dist');
- fs.mkdirSync(dir);
-
- var content = new Buffer(4 * 1024 * 1024);
-
- fs.writeFileSync(path.join(dir, 'test_file_1.js'), content);
- fs.writeFileSync(path.join(dir, 'test_file_2.js'), content);
- fs.writeFileSync(path.join(dir, 'test_file_3.js'), content);
-
- process.chdir(rootDir);
-
- var checkWarnings = checkWrite([
- 'test_file_2.js is bigger than 2 MiB',
- 'test_file_3.js is bigger than 2 MiB',
- ], [
- 'test_file_1.js is bigger than 2 MiB',
- ], 'Total precache size');
-
- var offlinePromise = offline({
- rootDir: dir,
- fileGlobs: [
- '*',
- 'test_file_1.js',
- ],
- }).then(function() {
- var content = fs.readFileSync(path.join(dir, 'offline-worker.js'), 'utf8');
- assert.notEqual(content.indexOf('test_file_1.js'), -1);
- assert.notEqual(content.indexOf('test_file_2.js'), -1);
- assert.notEqual(content.indexOf('test_file_3.js'), -1);
- });
-
- return Promise.all([ checkWarnings, offlinePromise ]);
});
});