diff --git a/.gitignore b/.gitignore
index c56b5fa5..9a1b7daa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,5 @@ content/framed.html
content/turtlebits.js
content/welcome.css
content/worker.js
+content/worker-stats.html
+content/worker-stats.js
diff --git a/Gruntfile.js b/Gruntfile.js
index b6c4d7d2..bea0e386 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -107,7 +107,10 @@ module.exports = function(grunt) {
flatten: true,
src: [
'content/src/editor.html',
- 'content/src/framed.html'
+ 'content/src/framed.html',
+ 'content/src/worker.js',
+ 'content/src/worker-stats.js',
+ 'content/src/worker-stats.html'
],
dest: 'content'
} ]
diff --git a/content/src/debug.js b/content/src/debug.js
index 78ae3d3d..c07f13f0 100644
--- a/content/src/debug.js
+++ b/content/src/debug.js
@@ -104,7 +104,8 @@ var debug = window.ide = {
},
setEditorText: function(text) {
view.changePaneEditorText(view.paneid('left'), text);
- }
+ },
+ serviceWorker: false // '/worker.js'
};
//////////////////////////////////////////////////////////////////////
diff --git a/content/src/editor.html b/content/src/editor.html
index ed9879b4..b620b43b 100644
--- a/content/src/editor.html
+++ b/content/src/editor.html
@@ -10,9 +10,9 @@
-/editor.css">
-/lib/tooltipster/css/tooltipster.css">
-/lib/droplet.css">
+
+
+
diff --git a/content/src/storage.js b/content/src/storage.js
index c2716a2d..bc251a1c 100644
--- a/content/src/storage.js
+++ b/content/src/storage.js
@@ -5,6 +5,16 @@
define(['jquery', 'see', 'filetype'],
function($, see, filetype) {
+var sw = null;
+function installServiceWorker() {
+ navigator.serviceWorker.register('/worker.js').then(function(reg) {
+ console.log('happy', reg);
+ }, function(err) {
+ console.log('sad', err);
+ });
+}
+installServiceWorker();
+
eval(see.scope('storage'));
function hasBackup(filename) {
try {
diff --git a/content/src/view.js b/content/src/view.js
index 48fec458..d530ac3e 100644
--- a/content/src/view.js
+++ b/content/src/view.js
@@ -1090,7 +1090,6 @@ function setPaneRunHtml(
p.html('');
var iframe = $('
').appendTo(p);
// Destroy and create new iframe.
- iframe.attr('src', 'about:blank');
var framewin = iframe[0].contentWindow;
var framedoc = framewin.document;
framedoc.open();
diff --git a/content/src/worker-stats.html b/content/src/worker-stats.html
new file mode 100644
index 00000000..a755de2f
--- /dev/null
+++ b/content/src/worker-stats.html
@@ -0,0 +1,100 @@
+
+
+
+
ServiceWorker stats for ${worker_scope}
+
+
+
+
+
ServiceWorker stats for PencilCode
+
Cache hits: Loading...
+
Cache misses: Loading...
+
Cache errors: Loading...
+
+
Recent activity
+
+
+
Main cache keys
+
+
+
User cache keys
+
+
+
+
diff --git a/content/src/worker-stats.js b/content/src/worker-stats.js
new file mode 100644
index 00000000..9a733162
--- /dev/null
+++ b/content/src/worker-stats.js
@@ -0,0 +1,106 @@
+// Some stats for this service worker session.
+
+var CACHE_HIT = 'hit';
+var CACHE_MISS = 'miss';
+var CACHE_ERROR = 'error';
+var CACHE_UPDATED = 'updated';
+var CACHE_FALLBACK = 'fallback';
+var CACHE_SKIPPED = 'skip';
+
+(function() {
+ var cache_hits = 0;
+ var cache_misses = 0;
+ var cache_errors = 0;
+ var cache_activity = {};
+ var max_cache_activity = 30;
+ var result_handler = {
+ hit: function() { ++cache_hits; },
+ miss: function() { ++cache_misses; },
+ error: function() { ++cache_errors; }
+ };
+
+ var stats_page = '/worker-stats.html';
+
+ function addResult(url, result) {
+ if (!(url in cache_activity))
+ cache_activity[url] = [];
+ cache_activity[url].push(result);
+ if (result in result_handler)
+ (result_handler[result])();
+ }
+
+ function handlePageRequest(request, path) {
+ if (path === '/killcache') {
+ return killCache();
+ }
+ if (path === '/get') {
+ return getStats();
+ }
+ return getStatsPage();
+ }
+
+ function getStats() {
+ var main_cache_entries = null;
+ var user_cache_entries = null;
+
+ return self.caches.open(main_cache_name)
+ .then(function(cache) {
+ return cache.keys();
+ })
+ .then(function(keys) {
+ main_cache_entries = keys.map(function(request) {
+ return request.url;
+ });
+ })
+ .then(function() {
+ return self.caches.open(user_cache_name);
+ })
+ .then(function(cache) {
+ return cache.keys();
+ })
+ .then(function(keys) {
+ user_cache_entries = keys.map(function(request) {
+ return request.url;
+ });
+ })
+ .then(function() {
+ var data = {
+ worker_scope: self.scope,
+ cache_hits: cache_hits,
+ cache_misses: cache_misses,
+ cache_errors: cache_errors,
+ main_cache_keys: main_cache_entries,
+ user_cache_keys: user_cache_entries,
+ recent_requests: Object.keys(cache_activity).map(function(key) {
+ return [key].concat(cache_activity[key]);
+ })
+ };
+ var blob = new Blob([JSON.stringify(data)], { type: 'text/javascript' });
+ return new Response(blob);
+ });
+ }
+
+ function killCache() {
+ return Promise.all([
+ self.caches.delete(main_cache_name),
+ self.caches.delete(user_cache_name)
+ ])
+ .then(function() {
+ return getStatsPage();
+ })
+ .then(function() {
+ return new Response('OK');
+ });
+ }
+
+ function getStatsPage() {
+ return getCachedResponse(new Request(stats_page));
+ }
+
+ self.stats = {
+ addResult: addResult,
+ handlePageRequest: handlePageRequest,
+ prefetch: getStatsPage,
+ };
+})();
+
diff --git a/content/src/worker.js b/content/src/worker.js
new file mode 100644
index 00000000..67f174c4
--- /dev/null
+++ b/content/src/worker.js
@@ -0,0 +1,234 @@
+importScripts('worker-stats.js');
+
+// TODO: Cache names should be versioned.
+var main_cache_name = 'main';
+var user_cache_name = 'user';
+
+function getCachedResponse(request) {
+ return self.caches.open(main_cache_name)
+ .then(function(cache) {
+ return cache.match(request)
+ .then(function(response) {
+ if (response === undefined) {
+ stats.addResult(request.url, CACHE_MISS);
+ return fetch(request)
+ .then(function(response) {
+ cache.put(request, response.clone());
+ return response;
+ });
+ } else {
+ stats.addResult(request.url, CACHE_HIT);
+ fetch(request)
+ .then(function(response) {
+ return response && cache.put(request, response);
+ });
+ return response;
+ }
+ });
+ });
+}
+
+function appendPath(base, name) {
+ if (base === '') {
+ return name;
+ }
+ return base + '/' + name;
+}
+
+function rawCacheEntryName(filename) {
+ return '/raw/' + filename;
+}
+
+function prefetchUserFiles(path, list) {
+ var p = Promise.all(list.map(function(entry) {
+ var entry_path = appendPath(path, entry.name);
+ if (entry.mode === 'drwx') {
+ return prefetchUserDirectory(entry_path);
+ } else {
+ return fetch('/load?file=' + entry_path)
+ .then(function(response) {
+ return self.caches.open(user_cache_name)
+ .then(function(cache) {
+ console.log("Adding user file: " + entry_path);
+ return cache.put(rawCacheEntryName(entry_path), response);
+ });
+ });
+ }
+ }));
+ return p;
+}
+
+function prefetchUserDirectory(path) {
+ path = path || '';
+ // path must be a directory.
+ var p = fetch('/load?file=' + path)
+ .then(function(response) {
+ return self.caches.open(user_cache_name)
+ .then(function(cache) {
+ console.log("Adding user directory : " + path);
+ return cache.put(rawCacheEntryName(path), response.clone());
+ })
+ .then(function() {
+ // response should be json.
+ return response.json();
+ });
+ })
+ .then(function(data) {
+ var list = data.list;
+ return prefetchUserFiles(path, list);
+ });
+ return p;
+}
+
+function getSearchParams(search) {
+ var params = {};
+ if (search.length === 0 || search[0] !== '?') {
+ return {};
+ }
+ search.substr(1).split('&').forEach(function(pair) {
+ var kv = pair.split('=');
+ if (kv.length === 2) {
+ params[kv[0]] = decodeURIComponent(kv[1]);
+ }
+ });
+ return params;
+}
+
+function handleLoadRequest(request, pathname) {
+ var url = new URL(request.url);
+ var params = getSearchParams(url.search);
+ var filename = params.file || (pathname.length > 1 && pathname.substr(1)) || '';
+
+ console.log('Load for ' + filename + ' with URL ' + request.url);
+
+ return fetch(request)
+ .then(function(response) {
+ var response_clone = response.clone();
+ return self.caches.open(user_cache_name)
+ .then(function(cache) {
+ stats.addResult(request.url, CACHE_UPDATED);
+ console.log('Updating cached entry: ' + filename);
+ return cache.put(rawCacheEntryName(filename), response.clone());
+ })
+ .then(function() {
+ return response;
+ });
+ })
+ .catch(function(e) {
+ return self.caches.open(user_cache_name)
+ .then(function(cache) {
+ stats.addResult(request.url, CACHE_FALLBACK);
+ console.log('Serving from cache: ' + request.url);
+ return cache.match(rawCacheEntryName(filename));
+ })
+ .then(function(result) {
+ if (result) {
+ stats.addResult(request.url, CACHE_HIT);
+ return result;
+ } else {
+ stats.addResult(request.url, CACHE_MISS);
+ throw new NetworkError();
+ }
+ });
+ });
+}
+
+function handleSaveRequest(request, pathname) {
+ var url = new URL(request.url);
+ var params = getSearchParams(url.search);
+ var data = params.data;
+ var meta = params.meta;
+ var mode = params.mode;
+ var sourcefile = params.sourcefile;
+ var conditional = params.conditional;
+ var key = params.key;
+ var sourcekey = params.sourcekey;
+
+ console.log("Search params: " + params);
+
+ return fetch(request);
+}
+
+function serveEditPage(request, filename) {
+ return self.caches.open(main_cache_name)
+ .then(function(cache) {
+ return cache.match('/editor.html');
+ })
+ .then(function(response) {
+ if (response) {
+ // Replace some of the text.
+ stats.addResult(request.url, CACHE_HIT);
+ console.log('serving editor.html');
+ return response.text().then(function(text) {
+ var sub_text = text.replace('', filename);
+ var b = new Blob([sub_text], {'type': 'text/html'});
+ return new Response(b);
+ });
+ } else {
+ stats.addResult(request.url, CACHE_MISS);
+ // Just go back to the origin server if possible.
+ return fetch(request);
+ }
+ });
+}
+
+function onInstall(event) {
+ console.log('onInstall');
+ // TODO: Should validate exisiting cache entries here.
+ // Kick off an update, but don't wait for it.
+ prefetchUserDirectory();
+ var p = Promise.all([
+ stats.prefetch(),
+ getCachedResponse(new Request('/editor.html'))
+ ]);
+ event.waitUntil(p);
+ // TODO: Should also fetch on Sync.
+}
+
+function onActivate(event) {
+ console.log('onActivate');
+}
+
+var blacklisted_prefixes = [
+ '/log', '/raw'
+];
+
+var page_handlers = [
+ // Note that it's important to begin and end the prefix string with /.
+ { prefix: '/stats/', handler: stats.handlePageRequest },
+ { prefix: '/edit/', handler: serveEditPage },
+ { prefix: '/load/', handler: handleLoadRequest },
+ { prefix: '/save/', handler: handleSaveRequest }
+];
+
+function onFetch(event) {
+ var url = new URL(event.request.url);
+ var scopeurl = new URL(self.scope);
+ if (url.host === url.host) {
+ if (page_handlers.some(function(h) {
+ if (url.pathname.indexOf(h.prefix) === 0) {
+ var remaining_path = url.pathname.substr(h.prefix.length - 1);
+ event.respondWith(h.handler(event.request, remaining_path));
+ return true;
+ }
+ return false;
+ })) {
+ return;
+ }
+ if (blacklisted_prefixes.some(function(prefix) {
+ return url.pathname.indexOf(prefix) == 0;
+ })) {
+ stats.addResult(event.request.url, CACHE_SKIPPED);
+ return;
+ }
+ }
+ if (url.host === 'www.google-analytics.com') {
+ stats.addResult(event.request.url, CACHE_SKIPPED);
+ return;
+ }
+ event.respondWith(getCachedResponse(event.request));
+}
+
+self.addEventListener('install', onInstall);
+self.addEventListener('activate', onActivate);
+self.addEventListener('fetch', onFetch);
diff --git a/content/welcome.html b/content/welcome.html
index b05a301f..a2474dce 100644
--- a/content/welcome.html
+++ b/content/welcome.html
@@ -378,4 +378,13 @@
Resources
ga('send', 'pageview');
});
+
diff --git a/server/load.js b/server/load.js
index 9fee422e..00efed17 100644
--- a/server/load.js
+++ b/server/load.js
@@ -7,16 +7,9 @@ var filemeta = require('./filemeta');
exports.handleLoad = function(req, res, app, format) {
var filename = req.param('file', utils.filenameFromUri(req));
- var callback = req.param('callback', null);
- var tail = req.param('tail', null);
var user = res.locals.owner;
var origfilename = filename;
- tail = parseInt(tail);
- if (Number.isNaN(tail)) {
- tail = null;
- }
-
if (filename == null) {
filename = '';
}