From 6c2058f912e8d2abc9883fe1c9b105f0b381f85e Mon Sep 17 00:00:00 2001 From: Bradley Momberger Date: Tue, 16 Jun 2015 14:29:26 -0700 Subject: [PATCH 1/2] Basic service worker support (on-demand fixtures) with tests --- src/service-worker/service-worker-main.js | 192 +++++++++++++++- .../service-worker-main_test.js | 7 +- src/service-worker/service-worker.js | 171 ++++++++++++-- src/service-worker/service-worker_test.js | 216 ++++++++++++++++-- src/service-worker/test_frame.html | 31 +++ test.html | 1 + test/service-worker-main_test.js | 6 + 7 files changed, 569 insertions(+), 55 deletions(-) create mode 100644 src/service-worker/test_frame.html create mode 100644 test/service-worker-main_test.js diff --git a/src/service-worker/service-worker-main.js b/src/service-worker/service-worker-main.js index c9ad760d..3b01f7fe 100644 --- a/src/service-worker/service-worker-main.js +++ b/src/service-worker/service-worker-main.js @@ -1,15 +1,183 @@ -console.log("WORKER - LISTENING"); -addEventListener("message", function(ev){ - console.log("WORKER - got message") - - postMessage({ - type: "response", - requestId: ev.data.requestId, - response: {data: [{id: 1}, {id: 2}]} +var cache_version = 'v1'; + +self.addEventListener("install", function(ev) { + // ev.waitUntil( + // addCache( + // // registration.scope + "/todos", + // // new Response('{"data": [{"id": 1}, {"id": 2}]}') + // ).then(function() { + // console.log("WORKER - Installed"); + // }, function(e) { + // console.error("wORKER - Install failed!", e); + // }) + // ); +}); + +self.addEventListener("activate", function(ev) { + clients.matchAll({}).then(function(cs) { + cs.forEach(function(c) { + c.postMessage({ + type: "activated" + }); + }); + }); +}); + +function addCache(scope, response) { + return caches.open(cache_version).then(function(cache) { + return cache.put( + scope, + response + ); }); - - +} + +function removeCache(scope) { + return caches.open(cache_version).then(function(cache) { + return cache.delete( + scope + ); + }); +} + +self.addEventListener("message", function(ev){ + var deferred; + console.log("WORKER - got message", ev.data); + if(ev.data.request === "unregister") { + + deferred = self.registration.unregister(self.location.pathname); + clients.matchAll({}).then(function(cs) { + cs.forEach(function(c) { + deferred.then(function() { + c.postMessage({ response: "uninstalled" }); + }); + }); + }); + } + + else if(ev.data.request === "run") { + ev.ports.forEach(function(port) { + port.postMessage({ + type: "ready" + }); + }); + clients.matchAll({}).then(function(cs) { + console.log("sending ready to", cs.length, "clients:", cs); + cs.forEach(function(c) { + c.postMessage({ + type: "ready" + }); + }); + }); + } + + else if(ev.data.request === "fixturize") { + addCache(registration.scope + ev.data.url, new Response(ev.data.response)).then(function() { + ev.ports.forEach(function(port) { + port.postMessage({ + type: "fixturize:success" + }); + }); + }); + } + + else { + ev.ports.forEach(function(port) { + port.postMessage({ + type: "response", + requestId: ev.data.requestId, + response: {data: [{id: 1}, {id: 2}]} + }); + }); + clients.matchAll({}).then(function(cs) { + cs.forEach(function(c) { + c.postMessage({ + type: "response", + requestId: ev.data.requestId, + response: {data: [{id: 1}, {id: 2}]} + }); + }); + }); + } }); -postMessage({ - type: "ready" + +self.addEventListener('fetch', function(event) { + try { + if(event.request.method === "PUT") { + event.respondWith( + caches.match(event.request).then(function(response) { + return Promise.all([event.request.json(), response.json()]); + }, function(e) { + return fetch(event.request); + }).then(function(jsons) { + var key, newresponse; + var newobj = jsons[0]; + var oldobj = jsons[1]; + for(key in newobj) { + (oldobj.data || oldobj)[key] = newobj[key]; + } + newresponse = new Response(JSON.stringify(oldobj)); + return addCache(event.request.url, newresponse).then(function() { + // need another response to return because caching reads the body + return new Response(JSON.stringify(oldobj)); + }); + }) + ); + } else if(event.request.method === "POST") { + event.respondWith( + caches.match(event.request).then(function(response) { + return Promise.all([event.request.json(), response.json()]); + }, function(e) { + return fetch(event.request); + }).then(function(jsons) { + var newresponse; + var newobj = jsons[0]; + var oldlist = jsons[1]; + newobj.id = oldlist.data.length + 1; + oldlist.data.push(newobj); + newresponse = new Response(JSON.stringify(oldlist)); + return Promise.all([ + addCache(event.request.url, newresponse), + addCache(event.request.url + "/" + newobj.id, new Response(JSON.stringify(newobj))) + ]).then(function() { + return new Response(JSON.stringify(newobj)); + }); + }) + ); + } else if(event.request.method === "DELETE") { + event.respondWith( + caches.match(event.request.url.replace(/\/[^\/]*$/, "")).then(function(response) { + return Promise.all([event.request.json(), response.json()]); + }, function(e) { + return fetch(event.request); + }).then(function(jsons) { + var newresponse; + var newobj = jsons[0]; + var oldlist = jsons[1]; + var id = event.request.url.substr(event.request.url.lastIndexOf("/") + 1); + var i = oldlist.data.length - 1; + for(; i >= 0; i--) { + if(oldlist.data[i].id === +id) { + oldlist.data.splice(i, 1); + } + } + newresponse = new Response(JSON.stringify(oldlist)); + return Promise.all([ + addCache(event.request.url.replace(/\/[^\/]*$/, ""), newresponse), + removeCache(event.request.url, new Response(JSON.stringify(newobj))) + ]).then(function() { + return new Response(JSON.stringify(newobj)); + }); + }) + ); + } else if(event.request.method === "GET") { + event.respondWith( + caches.match(event.request).then(function(response) { + return response || fetch(event.request); + }) + ); + } + } catch(e) { + console.log(e); + } }); diff --git a/src/service-worker/service-worker-main_test.js b/src/service-worker/service-worker-main_test.js index 04e950cf..6cbd17f2 100644 --- a/src/service-worker/service-worker-main_test.js +++ b/src/service-worker/service-worker-main_test.js @@ -1 +1,6 @@ -var serviceWorkerMain = require("can-connect/service-worker/service-worker-main"); +var serviceWorkerMain; +if(typeof importScripts === 'function') { + serviceWorkerMain = importScripts(["./service-worker-main.js"]); +} else { + serviceWorkerMain = require("./service-worker-main.js"); +} diff --git a/src/service-worker/service-worker.js b/src/service-worker/service-worker.js index 1c4ab3a8..c94f0483 100644 --- a/src/service-worker/service-worker.js +++ b/src/service-worker/service-worker.js @@ -1,6 +1,8 @@ var connect = require("can-connect"); var getItems = require("can-connect/helpers/get-items"); +var can = require("can/util/util"); +can = require("can/util/string/string"); var canSet = require("can-set"); var helpers = require("can-connect/helpers/"); require("when/es6-shim/Promise"); @@ -9,42 +11,165 @@ require("when/es6-shim/Promise"); * @module can-connect/service-worker * @parent can-connect.modules */ -module.exports = connect.behavior("service-worker",function(base){ - - var worker = new Worker(this.workerURL); +var scope; + +module.exports = connect.behavior("service-worker",function(options){ var requestId = 0; var requestDeferreds = {}; - var isReady = helpers.deferred(); - - var makeRequest = function(data){ + var isReady = new helpers.deferred(); + + if(navigator.serviceWorker.controller) { + navigator.serviceWorker.ready.then(function(registration) { + if(!registration) console.error("ERROR no registration"); + scope = (registration || {}).scope; + isReady.resolve(navigator.serviceWorker.controller); + }); + } else { + navigator.serviceWorker.register( + options.workerURL, + { scope: (scope = options.scope || options.workerURL.replace(/(\/.*\/).*\??.*/, "$1")) } + ).then(function(manager) { + var chennel; + var worker = (manager.installing || manager.active); + //manager.unregister(worker); + + if(worker === manager.installing) { + worker.onstatechange = function(ev) { + var channel = new MessageChannel(); + console.log("MAIN - State changed", ev.target.state); + if(ev.target.state === "activated") { + isReady.resolve(worker); + } + }; + } else { + isReady.resolve(worker); + } + }).catch(function(e) { + console.error(e); + }); + //}); + } + + + var makeRequest = function(url, data){ var reqId = requestId++; var def = helpers.deferred(); requestDeferreds[reqId] = def; - + var channel = {}; + if('MessageChannel' in window) { + channel = new MessageChannel(); + } + + data = data || {}; + isReady.promise.then(function(){ - worker.postMessage({ - request: data, - requestId: reqId + var _url = scope + url; + + if(typeof data.method !== "string" || data.method.toUpperCase() === "GET") { + if(data.body && !can.isEmptyObject(data.body)) { + _url += "?" + can.param(data.body); + } + delete data.body; + } else { + url = new Request(can.extend({ url: url }, data)); + } + + fetch( + _url, data + ).then(function(result) { + return result.json().then(function(json) { + def.resolve(json); + }); + }).catch(function(result) { + def.reject(result); }); }); - + return def.promise; }; - worker.onmessage = function(ev){ - console.log("MAIN - got message", ev.data.type) - if(ev.data.type === "ready"){ - isReady.resolve(); - } else if(ev.data.type === "response") { - requestDeferreds[ev.data.requestId].resolve(ev.data.response); - } - - }; return { getListData: function(params){ - return makeRequest({ - params: params + var url = options.findAll ? + can.sub(options.findAll, params, true) || options.findAll : + ("/" + options.name); + return makeRequest( + url, + { + body: params, + method: "GET" + } + ); + }, + getInstanceData: function(params) { + var url = options.findOne ? + can.sub(options.findOne, params, true) || options.findOne : + ("/" + options.name + "/" + params.id); + return makeRequest( + url, + { + body: params, + method: "GET" + } + ); + }, + updateListData: function(params) { + var that = this; + return Promise.all(params.map(function(param) { + return that.updateInstanceData(param); + })); + }, + updateInstanceData: function(params) { + var url = options.update ? + can.sub(options.update, params) || options.update : + ("/" + options.name + "/" + params.id); + return makeRequest( + url, + { + body: JSON.stringify(params), + method: "PUT" + } + ); + }, + createInstanceData: function(params) { + var url = options.create ? + can.sub(options.create, params) || options.create : + ("/" + options.name); + return makeRequest( + url, + { + body: JSON.stringify(params), + method: "POST" + } + ); + }, + destroyInstanceData: function(params) { + var url = options.destroy ? + can.sub(options.destroy, params) || options.destroy : + ("/" + options.name + "/" + params.id); + return makeRequest( + url, + { + body: JSON.stringify(params), + method: "DELETE" + } + ); + }, + messageWorker: function(params) { + var channel = new MessageChannel(); + isReady.promise.then(function(worker) { + worker.postMessage(params, [channel.port2]); }); - } + return channel.port1; + }, + fixturize: function(params) { + return this.messageWorker({ + request: "fixturize", + url: params.url, + type: params.type || "GET", + response: params.response + }) + }, + ready: isReady.promise }; }); \ No newline at end of file diff --git a/src/service-worker/service-worker_test.js b/src/service-worker/service-worker_test.js index fcdda404..85c43f9e 100644 --- a/src/service-worker/service-worker_test.js +++ b/src/service-worker/service-worker_test.js @@ -2,36 +2,214 @@ var QUnit = require("steal-qunit"); var serviceWorkerCache = require("can-connect/service-worker/"); var connect = require("can-connect"); -var logErrorAndStart = function(e){ - debugger; - ok(false,"Error "+e); - start(); +var makeIframe = function(src){ + var iframe = document.createElement('iframe'); + window.removeMyself = function(){ + delete window.removeMyself; + document.body.removeChild(iframe); + }; + document.body.appendChild(iframe); + iframe.src = src; + return iframe; }; -var items = [{id: 1, foo:"bar"},{id: 2, foo:"bar"},{id: 3, foo:"bar"}]; -var aItems = [{id: 10, name: "A"},{id: 11, name: "A"},{id: 12, name: "A"}]; +var injectScript = function(frame, script) { + frame.addEventListener("load", function() { + var el = frame.contentWindow.document.createElement("script"); + el.innerText = script; + frame.contentWindow.document.body.appendChild(el); + }); +}; -QUnit.module("can-connect/service-worker",{ +var testcount = 0; +var timeout; +QUnit.module("can-connect/service-worker-cache",{ + timeout: null, setup: function(){ this.connection = connect([serviceWorkerCache],{ name: "todos", - workerURL: System.stealURL+"?main=src/service-worker/service-worker-main_test" + findAll: "/todos", + findOne: "/todos/{id}", + update: "/todos/{id}", + create: "/todos", + destroy: "/todos/{id}", + workerURL: System.stealURL.substr(0, System.stealURL.indexOf("can-connect")) + "can-connect/src/service-worker/service-worker-main_test.js", + scope: System.stealURL.substr(0, System.stealURL.indexOf("can-connect")) + "can-connect/src/service-worker/" }); + testcount ++; + }, + teardown: function() { + var that = this; + timeout && clearTimeout(timeout); + timeout = setTimeout(function() { + testcount--; + if(testcount < 1) { + navigator.serviceWorker.getRegistration().then(function(reg) { + if(reg) { + reg.unregister(location.pathname); + } + }); + //window.removeMyself(); + } + }, 1000); } }); -QUnit.test("updateListData", function(){ - var items = [{id: 1, foo:"bar"},{id: 2, foo:"bar"},{id: 3, foo:"bar"}]; - - var connection = this.connection; - +QUnit.test("getStubListData", function(){ + stop(); + this.connection.ready.then(function() { + var items = [{id: 1, foo:"bar"},{id: 2, foo:"bar"},{id: 3, foo:"bar"}]; + injectScript( + makeIframe("src/service-worker/test_frame.html"), + 'ready.then(function() {\ + connection.fixturize({\ + url: "/todos",\ + response: \'{"data": [{"id": 1}, {"id": 2}]}\'\ + }).onmessage = function() {\ + connection.getListData({})\ + .then(function(listData){\ + QUnit.deepEqual(listData, {data: [{id: 1}, {id: 2}]}, "got back data");\ + window.removeMyself();\ + QUnit.start();\ + }, function(e){ QUnit.ok(false, e.message); QUnit.start(); }); }; });' + ); + }); +}); + +QUnit.test("getStubInstanceData", function(){ + stop(); + this.connection.ready.then(function() { + var items = [{id: 1, foo:"bar"},{id: 2, foo:"bar"},{id: 3, foo:"bar"}]; + injectScript( + makeIframe("src/service-worker/test_frame.html"), + 'ready.then(function() {\ + connection.fixturize({\ + url: "/todos/1",\ + response: \'{"data": {"id": 1}}\'\ + }).onmessage = function() {\ + connection.getInstanceData({id: 1})\ + .then(function(listData){\ + QUnit.deepEqual(listData, {data: {id: 1}}, "got back data");\ + window.removeMyself();\ + QUnit.start();\ + }, function(e){ QUnit.ok(false, e.message); QUnit.start(); }); }; });' + ); + }); +}); + +QUnit.test("updateStubInstanceData", function(){ + stop(); + this.connection.ready.then(function() { + injectScript( + makeIframe("src/service-worker/test_frame.html"), + 'ready.then(function() {\ + connection.fixturize({\ + url: "/todos/1",\ + response: \'{"data": {"id": 1}}\'\ + }).onmessage = function() {\ + connection.updateInstanceData({id: 1, foo: "bar"})\ + .then(function() {\ + return connection.getInstanceData({id: 1});\ + })\ + .then(function(listData){\ + QUnit.deepEqual(listData, {data: {id: 1, foo: "bar"}}, "got back data");\ + window.removeMyself();\ + QUnit.start();\ + }, function(e){ QUnit.ok(false, e.message); QUnit.start(); }); }; });' + ); + }); +}); + +QUnit.test("updateStubListData", function(){ stop(); - connection.getListData({foo: "bar"}) - .then(function(listData){ - deepEqual(listData, {data: [{id: 1}, {id: 2}]}, "got back data"); - start(); - }, logErrorAndStart); - + this.connection.ready.then(function() { + injectScript( + makeIframe("src/service-worker/test_frame.html"), + 'ready.then(function() {\ + connection.fixturize({\ + url: "/todos/1",\ + response: \'{"data": {"id": 1}}\'\ + }).onmessage = function() {\ + connection.fixturize({\ + url: "/todos/2",\ + response: \'{"data": {"id": 2}}\'\ + }).onmessage = function() {\ + connection.updateListData([{id: 1, foo: "bar"}, {id: 2, foo: "baz"}])\ + .then(function() {\ + return connection.getInstanceData({id: 1});\ + })\ + .then(function(listData){\ + QUnit.deepEqual(listData, {data: {id: 1, foo: "bar"}}, "got back data");\ + return connection.getInstanceData({id: 2});\ + }).then(function(listData2) {\ + QUnit.deepEqual(listData2, {data: {id: 2, foo: "baz"}}, "got back data");\ + window.removeMyself();\ + QUnit.start();\ + }, function(e){ QUnit.ok(false, e.message); QUnit.start(); }); }; }; });' + ); + }); }); +QUnit.test("createStubInstanceData", function(){ + stop(); + this.connection.ready.then(function() { + injectScript( + makeIframe("src/service-worker/test_frame.html"), + 'ready.then(function() {\ + connection.fixturize({\ + url: "/todos",\ + response: \'{"data": []}\'\ + }).onmessage = function() {\ + connection.createInstanceData({foo: "bar"})\ + .then(function() {\ + return connection.getListData({});\ + })\ + .then(function(listData){\ + QUnit.deepEqual(listData, {data: [{id: 1, foo: "bar"}]}, "got back data");\ + window.removeMyself();\ + QUnit.start();\ + }, function(e){ QUnit.ok(false, e.message); QUnit.start(); }); }; });' + ); + }); +}); +QUnit.test("destroyStubInstanceData", function(){ + stop(); + this.connection.ready.then(function() { + injectScript( + makeIframe("src/service-worker/test_frame.html"), + 'ready.then(function() {\ + connection.fixturize({\ + url: "/todos",\ + response: \'{"data": [{"id": 1}]}\'\ + }).onmessage = function() {\ + connection.destroyInstanceData({id: 1, foo: "bar"})\ + .then(function() {\ + return connection.getListData({});\ + })\ + .then(function(listData){\ + QUnit.deepEqual(listData, {data: []}, "got back data");\ + QUnit.start();\ + window.removeMyself();\ + }, function(e){ QUnit.ok(false, e.message); QUnit.start(); clearTimeout(to); }); }; });' + ); + }); +}); + + +QUnit.test("register as client", function() { + stop(); + this.connection.ready.then(function() { + injectScript( + makeIframe("src/service-worker/test_frame.html"), + "ready.then(function() {\ + connection.messageWorker({ request: 'run' }).onmessage = function(d) { \ + QUnit.equal(d.type, 'message');\ + QUnit.equal(d.data.type, 'ready');\ + window.removeMyself();\ + QUnit.start(); \ + }; \ + }, function(e){ QUnit.ok(false, e.message); QUnit.start(); });" + ); + }); +}); diff --git a/src/service-worker/test_frame.html b/src/service-worker/test_frame.html new file mode 100644 index 00000000..244a6da1 --- /dev/null +++ b/src/service-worker/test_frame.html @@ -0,0 +1,31 @@ + + + + can-connect tests + + + + + + + + diff --git a/test.html b/test.html index 65410655..c1d74026 100644 --- a/test.html +++ b/test.html @@ -1,3 +1,4 @@ can-connect tests +
diff --git a/test/service-worker-main_test.js b/test/service-worker-main_test.js new file mode 100644 index 00000000..17592210 --- /dev/null +++ b/test/service-worker-main_test.js @@ -0,0 +1,6 @@ +var serviceWorkerMain; +if(typeof importScripts === 'function') { + serviceWorkerMain = importScripts(["../service-worker-main.js"]); +} else { + serviceWorkerMain = require("../service-worker-main.js"); +} From 6dfbc120e7940884a246f73dd36e524288216d78 Mon Sep 17 00:00:00 2001 From: Bradley Momberger Date: Tue, 16 Jun 2015 14:43:11 -0700 Subject: [PATCH 2/2] Removed superfluous http-equiv from test htmls; guarded test suite for only those platforms where SW is supported --- src/service-worker/service-worker_test.js | 4 ++++ src/service-worker/test_frame.html | 1 - test.html | 1 - 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/service-worker/service-worker_test.js b/src/service-worker/service-worker_test.js index 85c43f9e..6b2ae2ae 100644 --- a/src/service-worker/service-worker_test.js +++ b/src/service-worker/service-worker_test.js @@ -21,6 +21,8 @@ var injectScript = function(frame, script) { }); }; +if(typeof navigator.serviceWorker === "object") { + var testcount = 0; var timeout; QUnit.module("can-connect/service-worker-cache",{ @@ -213,3 +215,5 @@ QUnit.test("register as client", function() { ); }); }); + +} \ No newline at end of file diff --git a/src/service-worker/test_frame.html b/src/service-worker/test_frame.html index 244a6da1..2165c0bc 100644 --- a/src/service-worker/test_frame.html +++ b/src/service-worker/test_frame.html @@ -2,7 +2,6 @@ can-connect tests -