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 = ''; }