Skip to content

Commit

Permalink
Update logic to handle access point SW terminations
Browse files Browse the repository at this point in the history
  • Loading branch information
Gozala committed Dec 29, 2018
1 parent 1983fb6 commit 5e3549f
Show file tree
Hide file tree
Showing 8 changed files with 299 additions and 118 deletions.
10 changes: 9 additions & 1 deletion demo/index.html
@@ -1,5 +1,13 @@
<html>
<head>
<script type="module" src="https://lunet.link/companion/api.js"></script>
<meta charset="utf-8" />
<title>P2P Site</title>
<script
type="module"
async
defer
src="https://lunet.link/companion/embed.js"
></script>
</head>
<body></body>
</html>
2 changes: 2 additions & 0 deletions demo/lunet.link.companion.service.js
@@ -1 +1,3 @@
// SW forbids registering SW from other origins there for we just import
// one from access point site.
importScripts("https://lunet.link/companion/service.js")
3 changes: 2 additions & 1 deletion src/remote/static/companion/bridge.html
@@ -1,7 +1,8 @@
<html>
<head>
<meta charset="utf-8" />
<script type="module" async defer src="./companion.js"></script>
<script type="module" async defer src="./bridge.js"></script>
<title>P2P Access Point Bridge</title>
</head>
<body></body>
</html>
Expand Up @@ -9,12 +9,13 @@ class Companion {
}
async onMessage({ data, ports, origin, source }) {
console.log("connection request", { data, origin, ports })
// TODO: Handle a case where lunet.link has not being visite and no
// sw is registered yet.
await navigator.serviceWorker.ready
navigator.serviceWorker.controller.postMessage(
{ info: data, origin },
ports
)
void fetch(new URL("../keep-alive!", location))
}
handleEvent(event) {
switch (event.type) {
Expand Down
@@ -1,15 +1,19 @@
export const client = async () => {
export const embed = async () => {
try {
const baseURL = new URL("https://lunet.link/companion/")
setStatusMessage(
"⚙️ Setting application to serve you even without interent."
)
const registration = await navigator.serviceWorker.register(
"./lunet.link.companion.service.js",
{
scope: new URL(location).pathname
}
)

let registration = null
if (!navigator.serviceWorker.controller) {
registration = await navigator.serviceWorker.register(
"./lunet.link.companion.service.js?_=3",
{
scope: new URL(location).pathname
}
)
}
// try {
// await registration.update()
// } catch (error) {
Expand Down Expand Up @@ -51,4 +55,4 @@ const setStatusMessage = message => {
document.body.textContent = message
}

client()
embed()
203 changes: 163 additions & 40 deletions src/remote/static/companion/service.js
@@ -1,62 +1,119 @@
// @flow strict

self.addEventListener("install", function(event) {
console.log("ServiceWorker was installed", event)
const baseURI = new URL("https://lunet.link/")

// Companion service is used p2p sites / applications. Site uses embedded
// `iframe` with `companion/bridge.html` to connect this SW with an
// "access point" SW allowing site / app to load all of the data from the p2p
// network.

self.skipWaiting()
self.addEventListener("install", function(event) {
console.log(`Companion service worker is installed for ${self.origin}`)
// We cache all the companion assets because occasionally we will need to
// reconnect to the "access point" SW and if we won't be able to do so
// while offline (as access point doesn't serve across origins).
event.waitUntil(initCache())
})

self.addEventListener("activate", function(event) {
console.log("ServiceWorker was activated", event)
console.log(`Companion service worker is activated for ${self.origin}`)

event.waitUntil(self.clients.claim())
})

self.addEventListener("fetch", function(event) {
console.log("fetch from service worker", event)
console.log(`Companion at ${self.origin} got fetch request`, event)
const { request } = event
event.respondWith(handleRequest(request))
event.respondWith(matchRoute(request))
})

self.addEventListener("message", function({ data, ports, source }) {
const [port] = ports
const { origin, info } = data
console.log("connected", {
origin,
port,
info
})

accessPoint = new AccessPoint(port)

source.postMessage("ready")
connection = Connection.new(port, source)
console.log(
`Companion at ${self.origin} was connected an access point`,
connection
)
})

let accessPoint = null
let connection = null

/*::
type EncodedResponse = {
type:"response";
id:number;
buffer:ArrayBuffer;
url:string;
status:number;
statusText:string;
destination:string;
headers:{[key: string]: string};
integrity: string;
method: string;
mode: string;
redirect: boolean;
referrer: string;
referrerPolicy: string;
}
type Alive = {
type:"alive"
}
class AccessPoint {
type Message =
| Alive
| EncodedResponse
*/
class Connection {
/*::
id:number
time:number
port:MessagePort
pendingRequests: {[number]:(MessageEvent) => void}
pendingRequests: {[number]:(EncodedResponse) => void}
*/
static new(port /*:MessagePort*/, source /*:WindowProxy*/) {
const self = new this(port)
source.postMessage("ready")
return self
}
isAlive() {
return Date.now() - this.time < 200
}
constructor(port /*:MessagePort*/) {
this.time = Date.now()
this.id = 0
this.port = port
port.start()
this.port.onmessage = message => this.onMessage(message)
this.port.onmessage = (message /*:Object*/) => this.receive(message.data)
this.pendingRequests = {}
}
onMessage({ data }) {
console.log(data)
const pendingRequest = this.pendingRequests[data.id]
receive(message /*:Message*/) {
switch (message.type) {
case "alive": {
return this.alive()
}
case "response": {
return this.respond(message)
}
}
}
alive() {
this.time = Date.now()
}
respond(encodedResponse /*:EncodedResponse*/) {
const pendingRequest = this.pendingRequests[encodedResponse.id]
if (pendingRequest) {
pendingRequest(data)
pendingRequest(encodedResponse)
} else {
console.warn(`Unable to find request for id ${data.id}`, data)
console.warn(
`Unable to find request for id ${encodedResponse.id}`,
encodedResponse
)
}
}
receive(id /*:number*/) /*:Promise<MessageEvent>*/ {
return new Promise((resolve /*:MessageEvent => void*/) => {
wait(id /*:number*/) /*:Promise<EncodedResponse>*/ {
return new Promise((resolve /*:EncodedResponse => void*/) => {
this.pendingRequests[id] = resolve
})
}
Expand All @@ -81,7 +138,7 @@ class AccessPoint {
[buffer]
)

const response /*:any*/ = await this.receive(id)
const response = await this.wait(id)
return new Response(response.buffer, {
status: response.status,
statusText: response.statusText,
Expand All @@ -90,19 +147,85 @@ class AccessPoint {
}
}

const handleRequest = async request => {
if (accessPoint) {
return await accessPoint.request(request)
} else if (new URL(request.url).origin === self.origin) {
return new Response(
`<script type="module" src="https://lunet.link/companion/api.js"></script>`,
{
headers: {
"Content-Type": "text/html"
}
}
)
const matchRoute = async request => {
const url = new URL(request.url)
// If matches companion route serve it from cache
if (url.origin === baseURI.origin) {
return companionRoute(request)
}
// If connected to access point use it to fetch underlying content.
else if (connection && connection.isAlive()) {
return await connection.request(request)
}
// If not connected to access point then load a page that would
// establish connection.
// TODO: We should probably use redirects here instead.
else if (url.origin === self.origin) {
return await connectRoute(request)
}
// Otherwise it is fetch to some other foreign origin in which case we
// just serve it through fetch.
else {
return foreignFetch(request.url)
}
}

const foreignFetch = url => fetch(url, { mode: "no-cors" })

const companionRoute = async request => {
const cache = await caches.open("companion")
const response = await cache.match(request)
if (response) {
return response
} else {
return fetch(request.url)
return notFound(request)
}
}

const connectRoute = async request => {
return new Response(
`<html>
<head>
<meta charset="utf-8" />
<title>P2P Site</title>
<script
type="module"
async
defer
src="https://lunet.link/companion/embed.js"
></script>
</head>
<body></body>
</html>
`,
{
status: 200,
headers: {
"Content-Type": "text/html"
}
}
)
}

// Non existing documents under `companion` route.
const notFound = async request => {
return new Response("<h1>Page Not Found</h1>", {
status: 404,
headers: {
"content-type": "text/html"
}
})
}

const initCache = async () => {
console.log(`Init companion cache for ${self.origin}`)
const cache = await caches.open("companion")
const urls = [
new URL("./companion/bridge.html", baseURI),
new URL("./companion/bridge.js", baseURI),
new URL("./companion/embed.js", baseURI),
new URL("./companion/service.js", baseURI)
]
console.log(`Companion "${self.origin}" is caching`, urls)
return cache.addAll(urls)
}
32 changes: 17 additions & 15 deletions src/remote/static/main.js
Expand Up @@ -9,32 +9,34 @@ export const main = async () => {
setStatusMessage(
"⚙️ Setting things up, to serve you even without interent."
)
// Register "access point" service worker that will serve all the p2p sites
// through `MessagePort` instances.
const serviceURL = new URL("https://lunet.link/service.js", location.href)
// Uses the scope of the page it's served from.
const registration = await navigator.serviceWorker.register(serviceURL, {
scope: new URL(location).pathname
})
setStatusMessage("🎉 All set! Will be there for you any time")
// TODO: Work out a SW upgrade rollout strategy. For the proove of concept
// we just call update here to ease development.

await navigator.serviceWorker.ready
await registration.update()
activate()

await activate()
} catch (error) {
setStatusMessage(`☹️ Ooops, Something went wrong`)
console.error(error)
setStatusMessage(`☹️ Ooops, Something went wrong ${error}`)
}
}

const activate = async () => {
try {
const request = await fetch(location.href)
const content = await request.text()
const parser = new DOMParser()
const { documentElement } = parser.parseFromString(content, "text/html")
document.documentElement.replaceWith(documentElement)
} catch (error) {
setStatusMessage(
"😕 Ooops, Something went wrong. Have you tried reloading it yet ?"
)
console.error(error)
}
// Once SW is ready we load "control panel" UI by fetching it from SW.
const request = await fetch(location.href)
const content = await request.text()
// Then we parse it as HTML and replacing current DOM tree with new one.
const parser = new DOMParser()
const { documentElement } = parser.parseFromString(content, "text/html")
document.documentElement.replaceWith(documentElement)
}

main()

0 comments on commit 5e3549f

Please sign in to comment.