Going Beyond Progressive Web Apps With Service Workers

A presentation at ThunderPlains in October 2019 in Oklahoma City, OK, USA by Trent Willis

Slide 1

Slide 1

BEYOND PROGRESSIVE WEB APPS WITH SERVICE WORKERS @TrentMWillis #ThunderPlains

Slide 2

Slide 2

Atwood’s Law

Slide 3

Slide 3

Atwood’s Law “Any application that can be written in JavaScript, will eventually be written in JavaScript.” blog.codinghorror.com/the-principle-of-least-power

Slide 4

Slide 4

Slide 5

Slide 5

Slide 6

Slide 6

Slide 7

Slide 7

Slide 8

Slide 8

PROGRESSIVE WEB APPS

Slide 9

Slide 9

BEYOND PROGRESSIVE WEB APPS WITH SERVICE WORKERS

Slide 10

Slide 10

@TrentMWillis

Slide 11

Slide 11

Love The Web. Love JavaScript. @TrentMWillis #ThunderPlains

Slide 12

Slide 12

JavaScript continues to evolve. @TrentMWillis #ThunderPlains

Slide 13

Slide 13

Web Hypertext Application Technology Working Group @TrentMWillis #ThunderPlains

Slide 14

Slide 14

WHATWG @TrentMWillis #ThunderPlains

Slide 15

Slide 15

WHATWG Streams @TrentMWillis #ThunderPlains

Slide 16

Slide 16

WHATWG Streams “If installed inside the fetch hook of a service worker, this would allow developers to transparently polyfill new image formats.” streams.spec.whatwg.org @TrentMWillis #ThunderPlains

Slide 17

Slide 17

Idea #1: Transparently Polyfill New File Formats @TrentMWillis #ThunderPlains

Slide 18

Slide 18

New Image Format @TrentMWillis #ThunderPlains

Slide 19

Slide 19

New Image Format <img src=“./my-image.nif”> *Can Display PNG/JPEG/GIF, Not NIF @TrentMWillis #ThunderPlains

Slide 20

Slide 20

New Image Format Proxy Service Worker <img src=“./my-image.nif”> *Can Display PNG/JPEG/GIF, Not NIF @TrentMWillis #ThunderPlains

Slide 21

Slide 21

New Image Format .nif Service Worker .gif <img src=“./my-image.nif”> *Can Display PNG/JPEG/GIF, Not NIF @TrentMWillis #ThunderPlains

Slide 22

Slide 22

All code will be available online @TrentMWillis #ThunderPlains

Slide 23

Slide 23

navigator.serviceWorker.register(‘./service-worker.js’); service-worker.js @TrentMWillis #ThunderPlains

Slide 24

Slide 24

Occurs for every request on the page // service-worker.js self.addEventListener(‘fetch’, (event) => {; }); Lots of details and methods… @TrentMWillis #ThunderPlains

Slide 25

Slide 25

// service-worker.js self.addEventListener(‘fetch’, (event) => {; event.respondWith(); }); @TrentMWillis Used to implement offline functionality #ThunderPlains

Slide 26

Slide 26

// service-worker.js self.addEventListener(‘fetch’, (event) => {; event.respondWith(validFileFromPolyfilledFile(event.request)); }); @TrentMWillis #ThunderPlains

Slide 27

Slide 27

// service-worker.js self.addEventListener(‘fetch’, (event) => {; if (isPolyfilledFile(event.request)) { event.respondWith(validFileFromPolyfilledFile(event.request)); validFileFromPolyfilledFile }; }); @TrentMWillis #ThunderPlains

Slide 28

Slide 28

const validFileFromPolyfilledFile = async (request) => { }; @TrentMWillis #ThunderPlains

Slide 29

Slide 29

const validFileFromPolyfilledFile = async (request) => { const originalResponse = await fetch(request); }; @TrentMWillis #ThunderPlains

Slide 30

Slide 30

const validFileFromPolyfilledFile = async (request) => { const originalResponse = await fetch(request); const originalData = originalResponse.blob(); // .text(), .json(), etc. }; @TrentMWillis #ThunderPlains

Slide 31

Slide 31

const validFileFromPolyfilledFile = async (request) => { const originalResponse = await fetch(request); const originalData = originalResponse.blob(); // .text(), .json(), etc. const modifiedData = polyfillTransform(originalData); }; @TrentMWillis #ThunderPlains

Slide 32

Slide 32

const validFileFromPolyfilledFile = async (request) => { const originalResponse = await fetch(request); const originalData = originalResponse.blob(); // .text(), .json(), etc. const modifiedData = polyfillTransform(originalData); const modifiedResponse = new Response(modifiedData); return modifiedResponse; }; @TrentMWillis #ThunderPlains

Slide 33

Slide 33

cryptogram-naive.glitch.me @TrentMWillis #ThunderPlains

Slide 34

Slide 34

Polyfill new extensions to the web platform @TrentMWillis #ThunderPlains

Slide 35

Slide 35

Like importing HTML into JavaScript modules @TrentMWillis #ThunderPlains

Slide 36

Slide 36

html-modules-polyfill.glitch.me @TrentMWillis #ThunderPlains

Slide 37

Slide 37

Idea #2: Compile Code At Runtime @TrentMWillis #ThunderPlains

Slide 38

Slide 38

Idea #2: Compile TypeScript At Runtime @TrentMWillis #ThunderPlains

Slide 39

Slide 39

<script src=”my-module.ts”></script>

@TrentMWillis #ThunderPlains

Slide 40

Slide 40

<script src=”my-module.ts” type=”module”></script>

@TrentMWillis #ThunderPlains

Slide 41

Slide 41

Cache API @TrentMWillis #ThunderPlains

Slide 42

Slide 42

Only compile changed files @TrentMWillis #ThunderPlains

Slide 43

Slide 43

const compileWithCache = async (response, compile) => { }; @TrentMWillis #ThunderPlains

Slide 44

Slide 44

const compileWithCache = async (response, compile) => { const cache = await caches.open(‘compile-cache’); }; @TrentMWillis #ThunderPlains

Slide 45

Slide 45

const compileWithCache = async (response, compile) => { const cache = await caches.open(‘compile-cache’); if (response.headers.get(‘status’) === ‘304’) { } }; @TrentMWillis #ThunderPlains

Slide 46

Slide 46

const compileWithCache = async (response, compile) => { const cache = await caches.open(‘compile-cache’); if (response.headers.get(‘status’) === ‘304’) { return cache.match(request.url); } }; @TrentMWillis #ThunderPlains

Slide 47

Slide 47

const compileWithCache = async (response, compile) => { const cache = await caches.open(‘compile-cache’); if (response.headers.get(‘status’) === ‘304’) { return cache.match(request.url); } const compiledResponse = compile(response); }; @TrentMWillis #ThunderPlains

Slide 48

Slide 48

const compileWithCache = async (response, compile) => { const cache = await caches.open(‘compile-cache’); if (response.headers.get(‘status’) === ‘304’) { return cache.match(request.url); } const compiledResponse = compile(response); cache.put(request.url, compiledResponse.clone()); }; @TrentMWillis #ThunderPlains

Slide 49

Slide 49

const compileWithCache = async (response, compile) => { const cache = await caches.open(‘compile-cache’); if (response.headers.get(‘status’) === ‘304’) { return cache.match(request.url); } const compiledResponse = compile(response); cache.put(request.url, compiledResponse.clone()); return compiledResponse; }; @TrentMWillis #ThunderPlains

Slide 50

Slide 50

typescript-in-browser.glitch.me @TrentMWillis #ThunderPlains

Slide 51

Slide 51

Idea #3: Process Data Off The Main Thread @TrentMWillis #ThunderPlains

Slide 52

Slide 52

API JS @TrentMWillis Process Here #ThunderPlains

Slide 53

Slide 53

API Service Worker Process Here UI @TrentMWillis #ThunderPlains

Slide 54

Slide 54

API Web Worker Service Worker Process Here UI @TrentMWillis #ThunderPlains

Slide 55

Slide 55

The page makes a request… @TrentMWillis #ThunderPlains

Slide 56

Slide 56

The request from the page An identifier of which page const transform = async (request, clientId) => { const [response, port] = await Promise.all([ fetch(request), getPortToWebWorkerFromClient(clientId) getPortToWebWorkerFromClient ]); }; @TrentMWillis #ThunderPlains

Slide 57

Slide 57

const getPortToWebWorkerFromClient = async (clientId) => { const client = await self.clients.get(clientId); }; @TrentMWillis #ThunderPlains

Slide 58

Slide 58

const getPortToWebWorkerFromClient = async (clientId) => { const client = await self.clients.get(clientId); return new Promise(resolve => { }); }; @TrentMWillis #ThunderPlains

Slide 59

Slide 59

const getPortToWebWorkerFromClient = async (clientId) => { const client = await self.clients.get(clientId); return new Promise(resolve => { client.postMessage({}); }); }; Ask the page how to communicate with the web worker @TrentMWillis #ThunderPlains

Slide 60

Slide 60

const webWorker = new Worker(‘web-worker.js’); navigator.serviceWorker.onmessage = () => { }; @TrentMWillis #ThunderPlains

Slide 61

Slide 61

const webWorker = new Worker(‘web-worker.js’); navigator.serviceWorker.onmessage = () => { const channel = new MessageChannel(); }; @TrentMWillis #ThunderPlains

Slide 62

Slide 62

const webWorker = new Worker(‘web-worker.js’); navigator.serviceWorker.onmessage = () => { const channel = new MessageChannel(); const serviceWorker = navigator.serviceWorker.controller; serviceWorker.postMessage({port: channel.port2}, [channel.port2]); }; @TrentMWillis #ThunderPlains

Slide 63

Slide 63

const webWorker = new Worker(‘web-worker.js’); navigator.serviceWorker.onmessage = () => { This will let the SW talk to the WW! const channel = new MessageChannel(); const serviceWorker = navigator.serviceWorker.controller; serviceWorker.postMessage({port: channel.port2}, [channel.port2]); webWorker.postMessage({port: channel.port1}, [channel.port1]); }; @TrentMWillis #ThunderPlains

Slide 64

Slide 64

const getPortToWebWorkerFromClient = async (clientId) => { const client = await self.clients.get(clientId); return new Promise(resolve => { client.postMessage({}); }); }; @TrentMWillis #ThunderPlains

Slide 65

Slide 65

const getPortToWebWorkerFromClient = async (clientId) => { const client = await self.clients.get(clientId); return new Promise(resolve => { self.onmessage = (msg) => { Now we know how to self.onmessage = null; talk to the Web Worker resolve(msg.data.port); }; client.postMessage({}); }); }; @TrentMWillis #ThunderPlains

Slide 66

Slide 66

const transform = async (request, clientId) => { const [response, port] = await Promise.all([ fetch(request), getPortToWebWorkerFromClient(clientId) ]); }; @TrentMWillis #ThunderPlains

Slide 67

Slide 67

const transform = async (request, clientId) => { const [response, port] = await Promise.all([ fetch(request), getPortToWebWorkerFromClient(clientId) ]); return new Promise(async (resolve) => { }); }; @TrentMWillis #ThunderPlains

Slide 68

Slide 68

const transform = async (request, clientId) => { const [response, port] = await Promise.all([ fetch(request), getPortToWebWorkerFromClient(clientId) ]); return new Promise(async (resolve) => { port.postMessage(await response.json()); }); Send the data to the Web Worker }; @TrentMWillis #ThunderPlains

Slide 69

Slide 69

const transform = async (request, clientId) => { const [response, port] = await Promise.all([ fetch(request), getPortToWebWorkerFromClient(clientId) ]); Get the result back return new Promise(async (resolve) => { from the Web Worker port.onmessage = (msg) => { resolve(new Response([${msg.data.toString()}])); }; port.postMessage(await response.json()); }); }; @TrentMWillis #ThunderPlains

Slide 70

Slide 70

service-worker-blocker.glitch.me @TrentMWillis #ThunderPlains

Slide 71

Slide 71

service-worker-worker.glitch.me @TrentMWillis #ThunderPlains

Slide 72

Slide 72

Idea #4: Process Data As It Streams @TrentMWillis #ThunderPlains

Slide 73

Slide 73

Original Data @TrentMWillis #ThunderPlains

Slide 74

Slide 74

Original Data @TrentMWillis Modified Data #ThunderPlains

Slide 75

Slide 75

BATCH Batch digra PROCESSING Encrypted Chunk Encrypted Chunk Decrypted Chunk @TrentMWillis Encrypted Chunk Decrypted Chunk Encrypted Chunk Decrypted Chunk Encrypted Chunk Decrypted Chunk Encrypted Chunk Decrypted Chunk Decrypted Chunk #ThunderPlains

Slide 76

Slide 76

Encrypted Chunk STREAM PROCESSING Encrypted Chunk Decrypted Chunk @TrentMWillis Encrypted Chunk Decrypted Chunk Encrypted Chunk Decrypted Chunk Encrypted Chunk Decrypted Chunk Encrypted Chunk Decrypted Chunk Decrypted Chunk #ThunderPlains

Slide 77

Slide 77

Data Chunk Data Chunk Modified Chunk @TrentMWillis Data Chunk Modified Chunk Data Chunk Modified Chunk Data Chunk Modified Chunk Data Chunk Modified Chunk Modified Chunk #ThunderPlains

Slide 78

Slide 78

Data Chunk Data Chunk Modified Chunk @TrentMWillis Data Chunk Modified Chunk Data Chunk Modified Chunk Data Chunk Modified Chunk Data Chunk Modified Chunk Modified Chunk #ThunderPlains

Slide 79

Slide 79

Data Chunk Data Chunk Modified Chunk @TrentMWillis Data Chunk Modified Chunk Data Chunk Modified Chunk Data Chunk Modified Chunk Data Chunk Modified Chunk Modified Chunk #ThunderPlains

Slide 80

Slide 80

Data Chunk Data Chunk Modified Chunk @TrentMWillis Data Chunk Modified Chunk Data Chunk Modified Chunk Data Chunk Modified Chunk Data Chunk Modified Chunk Modified Chunk #ThunderPlains

Slide 81

Slide 81

Original Data Data Chunk Data Chunk Modified Chunk @TrentMWillis Data Chunk Modified Chunk Modified Data Data Chunk Modified Chunk Data Chunk Modified Chunk Data Chunk Modified Chunk Modified Chunk #ThunderPlains

Slide 82

Slide 82

const validFileFromPolyfilledFile = async (request) => { const originalResponse = await fetch(request); const originalData = originalResponse.blob(); // .text(), .json(), etc. const modifiedData = polyfillTransform(originalData); const modifiedResponse = new Response(modifiedData); return modifiedResponse; }; @TrentMWillis #ThunderPlains

Slide 83

Slide 83

const validFileFromPolyfilledFile = async (request) => {; const originalResponse = await fetch(request); const transformStream = new TransformStream(new Transformer()); const transformedBody = originalResponse.body.pipeThrough( transformStream ); const transformedResponse = new Response(transformedBody); return transformedResponse; }; @TrentMWillis #ThunderPlains

Slide 84

Slide 84

const validFileFromPolyfilledFile = async (request) => {; const originalResponse = await fetch(request); const transformStream = new TransformStream(new Transformer()); const transformedBody = originalResponse.body.pipeThrough( transformStream ); const transformedResponse = new Response(transformedBody); return transformedResponse; }; @TrentMWillis #ThunderPlains

Slide 85

Slide 85

const validFileFromPolyfilledFile = async (request) => {; const originalResponse = await fetch(request); const transformStream = new TransformStream(new Transformer()); const transformedBody = originalResponse.body.pipeThrough( transformStream ); const transformedResponse = new Response(transformedBody); return transformedResponse; }; @TrentMWillis #ThunderPlains

Slide 86

Slide 86

const validFileFromPolyfilledFile = async (request) => {; const originalResponse = await fetch(request); const transformStream = new TransformStream(new Transformer()); const transformedBody = originalResponse.body.pipeThrough( transformStream ); const transformedResponse = new Response(transformedBody); return transformedResponse; }; @TrentMWillis #ThunderPlains

Slide 87

Slide 87

const validFileFromPolyfilledFile = async (request) => {; const originalResponse = await fetch(request); const transformStream = new TransformStream(new Transformer()); const transformedBody = originalResponse.body.pipeThrough( transformStream ); const transformedResponse = new Response(transformedBody); return transformedResponse; }; @TrentMWillis #ThunderPlains

Slide 88

Slide 88

const validFileFromPolyfilledFile = async (request) => {; const originalResponse = await fetch(request); const transformStream = new TransformStream(new Transformer()); Transformer const transformedBody = originalResponse.body.pipeThrough( transformStream ); const transformedResponse = new Response(transformedBody); return transformedResponse; }; @TrentMWillis #ThunderPlains

Slide 89

Slide 89

class Transformer {; async start() {}; async transform() {}; async flush() {}; }; @TrentMWillis #ThunderPlains

Slide 90

Slide 90

cryptogram-streaming.glitch.me @TrentMWillis #ThunderPlains

Slide 91

Slide 91

Idea #1: Transparently Polyfill New File Formats Idea #2: Compile Code At Runtime Idea #3: Process Data Off The Main Thread Idea #4: Process Data As It Streams @TrentMWillis #ThunderPlains

Slide 92

Slide 92

Service Workers, Streams, MessageChannels, Web Workers, Caches, Web Assembly… @TrentMWillis #ThunderPlains

Slide 93

Slide 93

Service Workers, Streams, MessageChannels, Web Workers, Caches, Web Assembly…these are all just tools to build things, so go build something! @TrentMWillis #ThunderPlains

Slide 94

Slide 94

EXPERIMENT WITH JAVASCRIPT AND HAVE FUN! *ALSO, USE GLITCH glitch.com/@trentmwillis/fun-with-service-workers @TrentMWillis #ThunderPlains