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

Commit

Permalink
feat: share IPFS node between browser tabs (#3081)
Browse files Browse the repository at this point in the history
This pull request adds 3 (sub)packages:

1. `ipfs-message-port-client` - Provides an API to an IPFS node over the [message channel][MessageChannel].
2. `ipfs-message-port-server` - Provides an IPFS node over [message channel][MessageChannel].
3. `ipfs-message-port-protocol` - Shared code between client / server mostly related to wire protocol encoding / decoding.

Fixes #3022

[MessageChannel]:https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel

Co-authored-by: Marcin Rataj <lidel@lidel.org>
Co-authored-by: Alex Potsides <alex@achingbrain.net>
  • Loading branch information
3 people committed Jul 27, 2020
1 parent 09735ca commit 1b8b1b8
Show file tree
Hide file tree
Showing 61 changed files with 5,330 additions and 11 deletions.
38 changes: 38 additions & 0 deletions examples/browser-sharing-node-across-tabs/README.md
@@ -0,0 +1,38 @@
# Sharing js-ipfs node across browsing contexts (tabs) using [SharedWorker][]

> In this example, you will find a boilerplate you can use to set up a js-ipfs
> node in the [SharedWorker] and use it from multiple tabs.
## Before you start

First clone this repo, install dependencies in the project root and build the project.

```bash
git clone https://github.com/ipfs/js-ipfs.git
cd js-ipfs/examples/browser-sharing-node-across-tabs
npm install
```

## Running the example

Run the following command within this folder:

```bash
npm start
```

Now open your browser at `http://localhost:3000`

You should see the following:

![Screen Shot](./Screen Shot.png)


### Run tests

```bash
npm test
```


[SharedWorker]:https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions examples/browser-sharing-node-across-tabs/index.html
@@ -0,0 +1,12 @@
<html>

<head>
<title>Sample App</title>
</head>

<body>
<div id='root'></div>
<script src="/static/bundle.js"></script>
</body>

</html>
36 changes: 36 additions & 0 deletions examples/browser-sharing-node-across-tabs/package.json
@@ -0,0 +1,36 @@
{
"name": "expample-browser-sharing-node-across-tabs",
"description": "Sharing IPFS node across browsing contexts",
"version": "1.0.0",
"private": true,
"scripts": {
"clean": "rm -rf ./dist",
"build": "webpack",
"start": "node server.js",
"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": "^4.43.0",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.11.0",
"worker-plugin": "4.0.3"
},
"dependencies": {
"ipfs": "^0.47.0",
"ipfs-message-port-client": "^0.0.1",
"ipfs-message-port-server": "^0.0.1"
},
"browserslist": [
">1%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}
18 changes: 18 additions & 0 deletions examples/browser-sharing-node-across-tabs/server.js
@@ -0,0 +1,18 @@
'use strict'

const webpack = require('webpack')
const WebpackDevServer = require('webpack-dev-server')
const config = require('./webpack.config')

const wds = new WebpackDevServer(webpack(config), {
hot: true,
historyApiFallback: true
})

wds.listen(3000, 'localhost', (err) => {
if (err) {
throw err
}

console.log('Listening at localhost:3000')
})
45 changes: 45 additions & 0 deletions examples/browser-sharing-node-across-tabs/src/main.js
@@ -0,0 +1,45 @@
'use strict'

import IPFSClient from "ipfs-message-port-client"


const main = async () => {
// connect / spawn shared ipfs worker & create a client.
const worker = new SharedWorker('./worker.js', { type: 'module' })
const ipfs = IPFSClient.from(worker.port)

const path = location.hash.slice(1)
if (path.startsWith('/ipfs/')) {
await viewer(ipfs, path)
} else {
await uploader(ipfs)
}
}

const uploader = async (ipfs) => {
document.body.outerHTML += '<div>Adding "hello world!" to shared IPFS node</div>'
const entry = await ipfs.add(ipfs, new Blob(['hello world!'], { type: "text/plain" }))
const path = `/ipfs/${entry.cid}/`
document.body.outerHTML += `<div class="ipfs-add">File was added:
<a target="_blank" href="${new URL(`#${path}`, location)}">${path}</a>
</div>`
}

const viewer = async (ipfs, path) => {
document.body.outerHTML += `<div class="loading">Loading ${path}</div>`
try {
const chunks = []
for await (const chunk of await ipfs.cat(path)) {
chunks.push(chunk)
}
const blob = new Blob(chunks)
const url = URL.createObjectURL(blob)
document.body.outerHTML +=
`<iframe id="content" sandbox src=${url} style="background:white;top:0;left:0;border:0;width:100%;height:100%;position:absolute;z-index:2;"></iframe>`

} catch(error) {
document.body.outerHTML += `<div class="error">${error}</div>`
}
}

onload = main
65 changes: 65 additions & 0 deletions examples/browser-sharing-node-across-tabs/src/worker.js
@@ -0,0 +1,65 @@
'use strict'

import IPFS from 'ipfs'
import { Server, IPFSService } from 'ipfs-message-port-server'

const main = async () => {
// start listening to all the incoming connections (browsing contexts that
// which run new SharedWorker...)
// Note: It is important to start listening before we do any await to ensure
// that connections aren't missed while awaiting.
const connections = listen(self, 'connect')

// Start an IPFS node & create server that will expose it's API to all clients
// over message channel.
const ipfs = await IPFS.create()
const service = new IPFSService(ipfs)
const server = new Server(service)

// connect every queued and future connection to the server.
for await (const event of connections) {
const port = event.ports[0]
if (port) {
server.connect(port)
}
}
}

/**
* Creates an AsyncIterable<Event> for all the events on the given `target` for
* the given event `type`. It is like `target.addEventListener(type, listener, options)`
* but instead of passing listener you get `AsyncIterable<Event>` instead.
* @param {EventTarget} target
* @param {string} type
* @param {AddEventListenerOptions} options
*/
const listen = function (target, type, options) {
const events = []
let resume
let ready = new Promise(resolve => (resume = resolve))

const write = event => {
events.push(event)
resume()
}
const read = async () => {
await ready
ready = new Promise(resolve => (resume = resolve))
return events.splice(0)
}

const reader = async function * () {
try {
while (true) {
yield * await read()
}
} finally {
target.removeEventListener(type, write, options)
}
}

target.addEventListener(type, write, options)
return reader()
}

main()
33 changes: 33 additions & 0 deletions examples/browser-sharing-node-across-tabs/test.js
@@ -0,0 +1,33 @@
'use strict'

const pkg = require('./package.json')

module.exports = {
[pkg.name]: (browser) => {
browser
.url(process.env.IPFS_EXAMPLE_TEST_URL)
.waitForElementVisible('.ipfs-add')

browser.expect.element('.ipfs-add a').text.to.contain('/ipfs/')
browser.click('.ipfs-add a')

browser.windowHandle(({ value }) => {
browser.windowHandles(({ value: handles }) => {
const [handle] = handles.filter(handle => handle != value)
browser.switchWindow(handle)
})
})

browser.waitForElementVisible('.loading')
browser.expect.element('.loading').text.to.contain('Loading /ipfs/')

browser.waitForElementVisible('#content').pause(5000)
browser.element('css selector', '#content', frame => {
browser.frame({ ELEMENT: frame.value.ELEMENT }, () => {
browser.waitForElementPresent('body')
browser.expect.element('body').text.to.contain('hello world!')
browser.end()
})
})
}
}
44 changes: 44 additions & 0 deletions examples/browser-sharing-node-across-tabs/webpack.config.js
@@ -0,0 +1,44 @@
'use strict'

var path = require('path')
var webpack = require('webpack')
const WorkerPlugin = require('worker-plugin')

module.exports = {
devtool: 'source-map',
entry: [
'webpack-dev-server/client?http://localhost:3000',
'webpack/hot/only-dev-server',
'./src/main'
],
output: {
path: path.join(__dirname, 'dist'),
filename: 'static/bundle.js'
},
plugins: [
new WorkerPlugin({
sharedWorker: true,
globalObject: 'self'
}),
new webpack.HotModuleReplacementPlugin()
],
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
},
node: {
fs: 'empty',
net: 'empty',
tls: 'empty'
}
}
2 changes: 1 addition & 1 deletion examples/traverse-ipld-graphs/package.json
Expand Up @@ -15,7 +15,7 @@
"dependencies": {
"cids": "^0.8.3",
"ipfs": "^0.48.0",
"ipld-block": "^0.9.1",
"ipld-block": "^0.9.2",
"ipld-dag-pb": "^0.19.0",
"multihashing-async": "^1.0.0"
}
Expand Down
4 changes: 2 additions & 2 deletions packages/interface-ipfs-core/package.json
Expand Up @@ -42,8 +42,8 @@
"ipfs-unixfs": "^1.0.3",
"ipfs-unixfs-importer": "^2.0.2",
"ipfs-utils": "^2.2.2",
"ipld-block": "^0.9.1",
"ipld-dag-cbor": "^0.15.2",
"ipld-block": "^0.9.2",
"ipld-dag-cbor": "^0.15.3",
"ipld-dag-pb": "^0.19.0",
"is-ipfs": "^1.0.3",
"iso-random-stream": "^1.1.1",
Expand Down
5 changes: 3 additions & 2 deletions packages/interface-ipfs-core/src/object/links.js
Expand Up @@ -59,8 +59,9 @@ module.exports = (common, options) => {
const node1bCid = await ipfs.object.put(node1b)

const links = await ipfs.object.links(node1bCid)
expect(links).to.be.an('array').that.has.property('length', 1)
expect(node1b.Links).to.be.deep.equal(links)

expect(links).to.have.lengthOf(1)
expect(node1b.Links).to.deep.equal(links)
})

it('should get links by base58 encoded multihash', async () => {
Expand Down
6 changes: 3 additions & 3 deletions packages/ipfs-http-client/package.json
Expand Up @@ -49,8 +49,8 @@
"form-data": "^3.0.0",
"ipfs-core-utils": "^0.3.0",
"ipfs-utils": "^2.2.2",
"ipld-block": "^0.9.1",
"ipld-dag-cbor": "^0.15.2",
"ipld-block": "^0.9.2",
"ipld-dag-cbor": "^0.15.3",
"ipld-dag-pb": "^0.19.0",
"ipld-raw": "^5.0.0",
"iso-url": "^0.4.7",
Expand All @@ -67,7 +67,7 @@
"nanoid": "^3.0.2",
"node-fetch": "^2.6.0",
"parse-duration": "^0.4.4",
"stream-to-it": "^0.2.0"
"stream-to-it": "^0.2.1"
},
"devDependencies": {
"aegir": "^23.0.0",
Expand Down

0 comments on commit 1b8b1b8

Please sign in to comment.