Skip to content

Commit

Permalink
docs: browser pubsub example (ipfs#1086)
Browse files Browse the repository at this point in the history
License: MIT
Signed-off-by: Alan Shaw <alan.shaw@protocol.ai>
  • Loading branch information
alanshaw committed Aug 29, 2019
1 parent 3515070 commit 69a56cb
Show file tree
Hide file tree
Showing 8 changed files with 327 additions and 25 deletions.
26 changes: 2 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ const ipfs = ipfsClient({
- [`ipfs.lsPullStream(ipfsPath)`](https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/FILES.md#lspullstream)
- [`ipfs.lsReadableStream(ipfsPath)`](https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/FILES.md#lsreadablestream)
- [MFS (mutable file system) specific](https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/FILES.md#mutable-file-system)

_Explore the Mutable File System through interactive coding challenges in our [ProtoSchool tutorial](https://proto.school/#/mutable-file-system/)._
- [`ipfs.files.cp([from, to], [callback])`](https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/FILES.md#filescp)
- [`ipfs.files.flush([path], [callback])`](https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/FILES.md#filesflush)
Expand Down Expand Up @@ -233,7 +233,7 @@ const ipfs = ipfsClient({
#### Graph

- [dag](https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/DAG.md)

_Explore the DAG API through interactive coding challenges in our [ProtoSchool tutorial](https://proto.school/#/basics)._
- [`ipfs.dag.get(cid, [path], [options], [callback])`](https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/DAG.md#dagget)
- [`ipfs.dag.put(dagNode, [options], [callback])`](https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/DAG.md#dagput)
Expand Down Expand Up @@ -339,28 +339,6 @@ const ipfs = ipfsClient({
- [`ipfs.key.rename(oldName, newName, [callback])`](https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/KEY.md#keyrename)
- [`ipfs.key.rm(name, [callback])`](https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/KEY.md#keyrm)

#### Pubsub Caveat

**Currently, the [PubSub API only works in Node.js environment](https://github.com/ipfs/js-ipfs-http-client/issues/518)**

We currently don't support pubsub when run in the browser, and we test it with separate set of tests to make sure if it's being used in the browser, pubsub errors.

More info: https://github.com/ipfs/js-ipfs-http-client/issues/518

This means:
- You can use pubsub from js-ipfs-http-client in Node.js
- You can use pubsub from js-ipfs-http-client in Electron
(when js-ipfs-http-client is ran in the main process of Electron)
- You can't use pubsub from js-ipfs-http-client in the browser
- You can't use pubsub from js-ipfs-http-client in Electron's
renderer process
- You can use pubsub from js-ipfs in the browsers
- You can use pubsub from js-ipfs in Node.js
- You can use pubsub from js-ipfs in Electron
(in both the main process and the renderer process)
- See https://github.com/ipfs/js-ipfs for details on
pubsub in js-ipfs

#### Instance utils

- `ipfs.getEndpointConfig()`
Expand Down
1 change: 1 addition & 0 deletions examples/browser-pubsub/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bundle.js
94 changes: 94 additions & 0 deletions examples/browser-pubsub/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Pubsub in the browser

> Use pubsub in the browser!
This example is a demo web application that allows you to connect to an IPFS node, subscribe to a pubsub topic and send/receive messages. We'll start two IPFS nodes and two browsers and use the `ipfs-http-client` to instruct each node to listen to a pubsub topic and send/receive pubsub messages to/from each other. We're aiming for something like this:

```
+-----------+ +-----------+
| +-------------------> |
| js-ipfs | pubsub | go-ipfs |
| <-------------------+ |
+-----^-----+ +-----^-----+
| |
| HTTP API | HTTP API
| |
+-------------------+ +-------------------+
+-------------------+ +-------------------+
| | | |
| | | |
| Browser 1 | | Browser 2 |
| | | |
| | | |
| | | |
+-------------------+ +-------------------+
```

## 1. Get started

With Node.js and git installed, clone the repo and install the project dependencies:

```sh
git clone https://github.com/ipfs/js-ipfs-http-client.git
cd js-ipfs-http-client
npm install # Installs ipfs-http-client dependencies
cd examples/browser-pubsub
npm install # Installs browser-pubsub app dependencies
```

Start the example application:

```sh
npm start
```

You should see something similar to the following in your terminal and the web app should now be available if you navigate to http://127.0.0.1:8888 using your browser:

```sh
Starting up http-server, serving ./
Available on:
http://127.0.0.1:8888
```

## 2. Start two IPFS nodes

To demonstrate pubsub we need two nodes running so pubsub messages can be passed between them.

Right now the easiest way to do this is to install and start a `js-ipfs` and `go-ipfs` node. There are other ways to do this, see [this document on running multiple nodes](https://github.com/ipfs/js-ipfs/tree/master/examples/running-multiple-nodes) for details.

### Install and start the JS IPFS node

```sh
npm install -g ipfs
jsipfs init
# Configure CORS to allow ipfs-http-client to access this IPFS node
jsipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["http://127.0.0.1:8888"]'
# Start the IPFS node, enabling pubsub
jsipfs daemon --enable-pubsub-experiment
```

### Install and start the Go IPFS node

Head over to https://dist.ipfs.io/#go-ipfs and hit the "Download go-ipfs" button. Extract the archive and read the instructions to install.

After installation:

```sh
ipfs init
# Configure CORS to allow ipfs-http-client to access this IPFS node
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["http://127.0.0.1:8888"]'
# Start the IPFS node, enabling pubsub
ipfs daemon --enable-pubsub-experiment
```

## 3. Open two browsers and connect to each node

Now, open up **two** browser windows. This could be two tabs in the same browser or two completely different browsers, it doesn't matter. Navigate to http://127.0.0.1:8888 in both.

In the "API ADDR" field enter `/ip4/127.0.0.1/tcp/5001` in one browser and `/ip4/127.0.0.1/tcp/5002` in the other and hit the "Connect" button.

This connects each browser to an IPFS node and now from the comfort of our browser we can instruct each node to listen to a pubsub topic and send/receive pubsub messages to each other.

> N.B. Since our two IPFS nodes are running on the same network they should have already found each other by MDNS. So you probably won't need to use the "CONNECT TO PEER" field. If you find your pubsub messages aren't getting through, check the output from your `jsipfs daemon` command and find the first address listed in "Swarm listening on" - it'll look like `/ip4/127.0.0.1/tcp/4002/ipfs/Qm...`. Paste this address into the "CONNECT TO PEER" field for the browser that is connected to your go-ipfs node and hit connect.
Finally, use the "SUBSCRIBE TO PUBSUB TOPIC" and "SEND MESSAGE" fields to do some pubsub-ing, you should see messages sent from one browser appear in the log of the other (provided they're both subscribed to the same topic).
42 changes: 42 additions & 0 deletions examples/browser-pubsub/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!doctype html>
<html>
<head>
<title>Pubsub in the browser</title>
<link rel="stylesheet" href="https://unpkg.com/tachyons@4.10.0/css/tachyons.min.css"/>
<link rel="stylesheet" href="https://unpkg.com/ipfs-css@0.12.0/ipfs.css">
</head>
<body class="sans-serif">
<header class="pv3 ph2 ph3-l bg-navy cf mb4">
<a href="https://ipfs.io/" title="ipfs.io">
<img src="https://ipfs.io/images/ipfs-logo.svg" class="v-mid" style="height:50px">
</a>
<h1 class="aqua fw2 montserrat dib ma0 pv2 ph1 v-mid fr f3 lh-copy">Pubsub</h1>
</header>
<div class="ph3 mb3">
<div class="fw2 tracked ttu f6 teal-muted mb2">API Addr</div>
<input id="api-url" value="/ip4/127.0.0.1/tcp/5001" class="dib w-50 ph1 pv2 monospace input-reset ba b--black-20 border-box" placeholder="e.g. /ip4/127.0.0.1/tcp/5001" />
<button id="node-connect" class="dib ph3 pv2 input-reset ba b--black-20 border-box">Connect</button>
</div>
<div class="ph3 mb3">
<div class="fw2 tracked ttu f6 teal-muted mb2">Connect to peer</div>
<input id="peer-addr" class="dib w-50 ph1 pv2 monospace input-reset ba b--black-20 border-box" placeholder="e.g. /ip4/127.0.0.1/tcp/4002/ipfs/QmPeerId" />
<button id="peer-connect" class="dib ph3 pv2 input-reset ba b--black-20 border-box">Connect</button>
</div>
<div class="ph3 mb3">
<div class="fw2 tracked ttu f6 teal-muted mb2">Subscribe to pubsub topic</div>
<input id="topic" class="dib w-50 ph1 pv2 monospace input-reset ba b--black-20 border-box" placeholder="e.g. books" />
<button id="subscribe" class="dib ph3 pv2 input-reset ba b--black-20 border-box">Subscribe</button>
</div>
<div class="ph3 mb3">
<div class="fw2 tracked ttu f6 teal-muted mb2">Send pubsub message</div>
<input id="message" class="dib w-50 ph1 pv2 monospace input-reset ba b--black-20 border-box" />
<button id="send" class="dib ph3 pv2 input-reset ba b--black-20 border-box">Send</button>
</div>
<div class="ph3 mb3">
<div class="fw2 tracked ttu f6 teal-muted mb2">Console</div>
<div id="console" class="f7 db w-100 ph1 pv2 monospace input-reset ba b--black-20 border-box overflow-scroll" style="height: 300px">
</div>
</div>
<script src="bundle.js"></script>
</body>
</html>
135 changes: 135 additions & 0 deletions examples/browser-pubsub/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
'use strict'

const IpfsHttpClient = require('ipfs-http-client')
const { sleep, Logger, onEnterPress, catchAndLog } = require('./util')

async function main () {
const apiUrlInput = document.getElementById('api-url')
const nodeConnectBtn = document.getElementById('node-connect')

const peerAddrInput = document.getElementById('peer-addr')
const peerConnectBtn = document.getElementById('peer-connect')

const topicInput = document.getElementById('topic')
const subscribeBtn = document.getElementById('subscribe')

const messageInput = document.getElementById('message')
const sendBtn = document.getElementById('send')

let log = Logger(document.getElementById('console'))
let ipfs
let topic
let peerId

async function reset () {
if (ipfs && topic) {
log(`Unsubscribing from topic ${topic}`)
await ipfs.pubsub.unsubscribe(topic)
}
log = Logger(document.getElementById('console'))
topicInput.value = ''
topic = null
peerId = null
ipfs = null
}

async function nodeConnect (url) {
await reset()
log(`Connecting to ${url}`)
ipfs = IpfsHttpClient(url)
const { id, agentVersion } = await ipfs.id()
peerId = id
log(`<span class="green">Success!</span>`)
log(`Version ${agentVersion}`)
log(`Peer ID ${id}`)
}

async function peerConnect (addr) {
if (!addr) throw new Error('Missing peer multiaddr')
if (!ipfs) throw new Error('Connect to a node first')
log(`Connecting to peer ${addr}`)
await ipfs.swarm.connect(addr)
log(`<span class="green">Success!</span>`)
log('Listing swarm peers...')
await sleep()
const peers = await ipfs.swarm.peers()
peers.forEach(peer => {
const fullAddr = `${peer.addr}/ipfs/${peer.peer.toB58String()}`
log(`<span class="${addr.endsWith(peer.peer.toB58String()) ? 'teal' : ''}">${fullAddr}</span>`)
})
log(`(${peers.length} peers total)`)
}

async function subscribe (nextTopic) {
if (!nextTopic) throw new Error('Missing topic name')
if (!ipfs) throw new Error('Connect to a node first')

const lastTopic = topic

if (topic) {
topic = null
log(`Unsubscribing from topic ${lastTopic}`)
await ipfs.pubsub.unsubscribe(lastTopic)
}

log(`Subscribing to ${nextTopic}...`)

await ipfs.pubsub.subscribe(nextTopic, msg => {
const from = msg.from
const seqno = msg.seqno.toString('hex')
if (from === peerId) return log(`Ignoring message ${seqno} from self`)
log(`Message ${seqno} from ${from}:`)
try {
log(JSON.stringify(msg.data.toString(), null, 2))
} catch (_) {
log(msg.data.toString('hex'))
}
}, {
onError: (err, fatal) => {
if (fatal) {
console.error(err)
log(`<span class="red">${err.message}</span>`)
topic = null
log('Resubscribing in 5s...')
setTimeout(catchAndLog(() => subscribe(nextTopic), log), 5000)
} else {
console.warn(err)
}
}
})

topic = nextTopic
log(`<span class="green">Success!</span>`)
}

async function send (msg) {
if (!msg) throw new Error('Missing message')
if (!topic) throw new Error('Subscribe to a topic first')
if (!ipfs) throw new Error('Connect to a node first')

log(`Sending message to ${topic}...`)
await ipfs.pubsub.publish(topic, msg)
log(`<span class="green">Success!</span>`)
}

const onNodeConnectClick = catchAndLog(() => nodeConnect(apiUrlInput.value), log)
apiUrlInput.addEventListener('keydown', onEnterPress(onNodeConnectClick))
nodeConnectBtn.addEventListener('click', onNodeConnectClick)

const onPeerConnectClick = catchAndLog(() => peerConnect(peerAddrInput.value), log)
peerAddrInput.addEventListener('keydown', onEnterPress(onPeerConnectClick))
peerConnectBtn.addEventListener('click', onPeerConnectClick)

const onSubscribeClick = catchAndLog(() => subscribe(topicInput.value), log)
topicInput.addEventListener('keydown', onEnterPress(onSubscribeClick))
subscribeBtn.addEventListener('click', onSubscribeClick)

const onSendClick = catchAndLog(async () => {
await send(messageInput.value)
messageInput.value = ''
}, log)
messageInput.addEventListener('keydown', onEnterPress(onSendClick))
sendBtn.addEventListener('click', onSendClick)
}

main()
20 changes: 20 additions & 0 deletions examples/browser-pubsub/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "browser-pubsub-example",
"version": "0.0.0",
"description": "An example demonstrating pubsub in the browser",
"private": true,
"main": "index.js",
"scripts": {
"start": "npm run build && npm run serve",
"build": "browserify index.js > bundle.js",
"serve": "http-server -a 127.0.0.1 -p 8888",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Alan Shaw",
"license": "MIT",
"dependencies": {
"browserify": "^16.5.0",
"http-server": "^0.11.1",
"ipfs-http-client": "../../"
}
}
31 changes: 31 additions & 0 deletions examples/browser-pubsub/util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
exports.sleep = (ms = 1000) => new Promise(resolve => setTimeout(resolve, ms))

exports.Logger = outEl => {
outEl.innerHTML = ''
return message => {
const container = document.createElement('div')
container.innerHTML = message
outEl.appendChild(container)
outEl.scrollTop = outEl.scrollHeight
}
}

exports.onEnterPress = fn => {
return e => {
if (event.which == 13 || event.keyCode == 13) {
e.preventDefault()
fn()
}
}
}

exports.catchAndLog = (fn, log) => {
return async (...args) => {
try {
await fn(...args)
} catch (err) {
console.error(err)
log(`<span class="red">${err.message}</span>`)
}
}
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"browser": {
"glob": false,
"fs": false,
"stream": "readable-stream"
"stream": "readable-stream",
"ky-universal": "ky/umd"
},
"repository": "github:ipfs/js-ipfs-http-client",
"scripts": {
Expand Down

0 comments on commit 69a56cb

Please sign in to comment.