Skip to content
This repository has been archived by the owner on Feb 12, 2024. It is now read-only.

feat: implement service worker example #3374

Merged
merged 10 commits into from
Nov 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions examples/browser-service-worker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Using js-ipfs node in [SharedWorker][] from [ServiceWorker][]

> In this example, you will find boilerplate code you can use to set up an IPFS
> node in a [SharedWorker][] and use it from a [ServiceWorker][].

## General Overview

### `src/main.js`

Module is loaded in the main thread (DOM window) and is responsible for wiring
all the pieces together:

1. Activates a [SharedWorker][] that runs an IPFS node.
2. Registers a [ServiceWorker][] to serve IPFS content from.
3. Listens to [MessagePort][] requests from the [ServiceWorker][] and responds
back with a [MessagePort][] of the [SharedWorker][], enabling
it to interact with shaerd IPFS node.

### `src/worker.js`

Module is loaded in the [SharedWorker][]. It demonstrates how to setup the IPFS
node such that it can be used in other browsing contexts.

### `src/service.js`

Module is loaded in the [ServiceWorker][] and responds to all the requests from
the page. It recognizes four different request routes:

1. Routes `/ipfs/...`, `/ipns/...` are served html pages that:

- Contain a full page iframe that has an `src` derived from request path e.g.:

```
/ipfs/Qm...hash/file/name -> /view/Qm...hash/file/name
```
- `src/main.js` script loaded in it.

This way when request from `/view/Qm..hash/file/name` arrives [ServiceWorker][]
can obtain a [MessagePort][] for the [SharedWorker][] by requesting it from
the iframe container.

2. Routes `/view/ipfs/...` and are served corresponding content from IPFS. On
such request message is send to an iframe container (That is why `/ipfs/...`
and `/ipns/...` routes served `iframe` and `src/main.js`), through which
[MessagePort][] for the [SharedWorker][] is obtained and used to retrieve
content from the shared IPFS node and served back.

> There is a stub for `/view/ipns/...` route, which is left as an excercise
> for the reader to fill.

3. All other routes are served by fetchging it from the network.


## Before you start

First clone this repo, cd into the example directory and install the dependencies

```bash
git clone https://github.com/ipfs/js-ipfs.git
cd js-ipfs/examples/browser-service-worker
npm install
```

## Running the example

Run the following command within this folder:

```bash
npm start
```

Now open your browser at `http://localhost:3000`
Gozala marked this conversation as resolved.
Show resolved Hide resolved

You should see the following:

![Screen Shot](./index-view.png)

If you navigate to the following address `http://localhost:3000/ipfs/bafybeicqzoixu6ivztffjy4bktwxy6lxaxkvnavkya7kfgwyhx4bund2ga/` it should load a
page from ipfs and appear as:

![Screen Shot](./page-view.png)

### Run tests

```bash
npm test
```


[SharedWorker]:https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker
[ServiceWorker]:https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API
[MessagePort]:https://developer.mozilla.org/en-US/docs/Web/API/MessagePort
Binary file added examples/browser-service-worker/index-view.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
119 changes: 119 additions & 0 deletions examples/browser-service-worker/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<html>

<head>
<title>IPFS Viewer</title>
<style>
@media (prefers-color-scheme: dark) {

a,
body {
background: #000;
color: white;
}
}

@media (prefers-color-scheme: light) {

a,
body {
background: white;
color: #000;
}
}


body {
font-family: -apple-system, BlinkMacSystemFont,
'avenir next', avenir,
'helvetica neue', helvetica,
ubuntu,
roboto, noto,
'segoe ui', arial,
sans-serif;
}

a {
text-decoration: none;
}

a:hover {
text-decoration: underline;
}

.dt {
display: table;
}

.dtc {
display: table-cell;
}

.fw6 {
font-weight: 600;
}

.vh-100 {
height: 100vh;
}

.w-100 {
width: 100%;
}

.white {
color: #fff;
}

.bg-dark-pink {
background-color: #d5008f;
}

.ph3 {
padding-left: 1rem;
padding-right: 1rem;
}

.tc {
text-align: center;
}

.f6 {
font-size: .875rem;
}

.v-mid {
vertical-align: middle;
}

@media screen and (min-width: 30em) and (max-width: 60em) {
.f2-m {
font-size: 2.25rem;
}
}

@media screen and (min-width: 60em) {
.ph4-l {
padding-left: 2rem;
padding-right: 2rem;
}

.f-subheadline-l {
font-size: 5rem;
}
}
</style>
<script src="/main.js"></script>
</head>

<body>
<article class="vh-100 dt w-100">
<div class="dtc v-mid tc ph3 ph4-l">
<h1 class="f6 f2-m f-subheadline-l fw6 tc">Load content by adding IPFS path to the URL ⤴</h2>
<p>Something like <a
href="/ipfs/bafybeicqzoixu6ivztffjy4bktwxy6lxaxkvnavkya7kfgwyhx4bund2ga/">/ipfs/bafybeicqzoixu6ivztffjy4bktwxy6lxaxkvnavkya7kfgwyhx4bund2ga/</a>
</p>
</div>
</center>
</body>

</html>
37 changes: 37 additions & 0 deletions examples/browser-service-worker/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "example-browser-service-worker",
"description": "IPFS with service worker",
"version": "1.0.0",
"private": true,
"scripts": {
"clean": "rm -rf ./dist",
"build": "webpack",
"start": "webpack-dev-server",
"test": "test-ipfs-example"
},
"license": "MIT",
"keywords": [],
"devDependencies": {
"@babel/core": "^7.2.2",
"@babel/preset-env": "^7.3.1",
"babel-loader": "^8.0.5",
"copy-webpack-plugin": "^5.0.4",
"test-ipfs-example": "^2.0.3",
"webpack": "5.4.0",
"webpack-cli": "4.1.0",
"webpack-dev-server": "3.11.0"
},
"dependencies": {
"ipfs": "^0.51.0",
"ipfs-message-port-client": "^0.3.0",
"ipfs-message-port-protocol": "^0.3.0",
"ipfs-message-port-server": "^0.3.0",
"process": "0.11.10"
},
"browserslist": [
">1%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}
Binary file added examples/browser-service-worker/page-view.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
87 changes: 87 additions & 0 deletions examples/browser-service-worker/src/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
'use strict'

// This is an entry point to our program.
const main = async () => {
// We start a shared worker where IPFS node is loaded.
const worker = createIPFSWorker()
// @ts-ignore - Store worker in the window so that it's available in console.
window.worker = createIPFSWorker()

// Service workers do not have access to the `SharedWorker` API
// (see https://github.com/w3c/ServiceWorker/issues/678)
// To overcome that limitation the page will listen for the service worker message
// and provide it with a message port to the shared worker, which will enable
// it to use our (shared) IPFS node.
navigator.serviceWorker.onmessage = onServiceWorkerMessage

// @ts-ignore - register expects string but weback requires this URL hack.
await navigator.serviceWorker.register(new URL('./service.js', import.meta.url), { scope: '/' })

await navigator.serviceWorker.ready

// This is just for testing, lets us know when SW is ready.
const meta = document.createElement("meta")
meta.name = "sw-ready"
document.head.appendChild(meta)

// URLs like `localhost:3000/ipfs/Qmf412jQZiuVUtdgnB36FXFX7xg5V6KEbSJ4dpQuhkLyfD`
// are loaded from service worker. However it could be that such a URL is loaded
// before the service worker was registered in which case our server just loads a blank
// page (that doesn't have data-viewer attribute). If that is the case we load
// the actual IPFS content after the SW is ready.
if (document.documentElement.dataset.viewer == null) {
load(location.pathname)
}
}

/**
* @param {string} path
*/
const load = async (path) => {
const [,protocol] = path.split('/')
switch (protocol) {
case 'ipfs':
case 'ipns': {
document.body.innerHTML = `<iframe id="viewer" style="width:100%;height:100%;position:fixed;top:0;left:0;border:none;" src="/view${path}"></iframe>`
}
}
}

/**
* Handles ipfs message port request from service worker and
* responds to it with it.
*
* @param {MessageEvent} event
*/
const onServiceWorkerMessage = (event) => {
/** @type {null|ServiceWorker} */
const serviceWorker = (event.source)
if (serviceWorker == null) return
switch (event.data.method) {
case 'ipfs-message-port': {
// Receives request from service worker, creates a new shared worker and
// responds back with the message port.
// Note: MessagePort can be transferred only once which is why we need to
// create a SharedWorker each time. However a ServiceWorker is only created
// once (in main function) all other creations just create port to it.
const worker = createIPFSWorker()
return serviceWorker.postMessage({
method: 'ipfs-message-port',
id: event.data.id,
port: worker.port
}, [worker.port])
}
}
}

/**
* Creates a shared worker instance that exposes JS-IPFS node over MessagePort.
* @returns {SharedWorker}
*/
const createIPFSWorker = () => new SharedWorker(
// @ts-ignore - Constructor takes string but webpack needs URL
new URL('./worker.js', import.meta.url),
'IPFS'
)

main()
Loading