Skip to content
This repository was archived by the owner on Mar 10, 2020. It is now read-only.

Commit 69a56cb

Browse files
author
Alan Shaw
authored
docs: browser pubsub example (#1086)
License: MIT Signed-off-by: Alan Shaw <alan.shaw@protocol.ai>
1 parent 3515070 commit 69a56cb

File tree

8 files changed

+327
-25
lines changed

8 files changed

+327
-25
lines changed

README.md

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ const ipfs = ipfsClient({
203203
- [`ipfs.lsPullStream(ipfsPath)`](https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/FILES.md#lspullstream)
204204
- [`ipfs.lsReadableStream(ipfsPath)`](https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/FILES.md#lsreadablestream)
205205
- [MFS (mutable file system) specific](https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/FILES.md#mutable-file-system)
206-
206+
207207
_Explore the Mutable File System through interactive coding challenges in our [ProtoSchool tutorial](https://proto.school/#/mutable-file-system/)._
208208
- [`ipfs.files.cp([from, to], [callback])`](https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/FILES.md#filescp)
209209
- [`ipfs.files.flush([path], [callback])`](https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/FILES.md#filesflush)
@@ -233,7 +233,7 @@ const ipfs = ipfsClient({
233233
#### Graph
234234

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

342-
#### Pubsub Caveat
343-
344-
**Currently, the [PubSub API only works in Node.js environment](https://github.com/ipfs/js-ipfs-http-client/issues/518)**
345-
346-
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.
347-
348-
More info: https://github.com/ipfs/js-ipfs-http-client/issues/518
349-
350-
This means:
351-
- You can use pubsub from js-ipfs-http-client in Node.js
352-
- You can use pubsub from js-ipfs-http-client in Electron
353-
(when js-ipfs-http-client is ran in the main process of Electron)
354-
- You can't use pubsub from js-ipfs-http-client in the browser
355-
- You can't use pubsub from js-ipfs-http-client in Electron's
356-
renderer process
357-
- You can use pubsub from js-ipfs in the browsers
358-
- You can use pubsub from js-ipfs in Node.js
359-
- You can use pubsub from js-ipfs in Electron
360-
(in both the main process and the renderer process)
361-
- See https://github.com/ipfs/js-ipfs for details on
362-
pubsub in js-ipfs
363-
364342
#### Instance utils
365343

366344
- `ipfs.getEndpointConfig()`

examples/browser-pubsub/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
bundle.js

examples/browser-pubsub/README.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Pubsub in the browser
2+
3+
> Use pubsub in the browser!
4+
5+
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:
6+
7+
```
8+
+-----------+ +-----------+
9+
| +-------------------> |
10+
| js-ipfs | pubsub | go-ipfs |
11+
| <-------------------+ |
12+
+-----^-----+ +-----^-----+
13+
| |
14+
| HTTP API | HTTP API
15+
| |
16+
+-------------------+ +-------------------+
17+
+-------------------+ +-------------------+
18+
| | | |
19+
| | | |
20+
| Browser 1 | | Browser 2 |
21+
| | | |
22+
| | | |
23+
| | | |
24+
+-------------------+ +-------------------+
25+
```
26+
27+
## 1. Get started
28+
29+
With Node.js and git installed, clone the repo and install the project dependencies:
30+
31+
```sh
32+
git clone https://github.com/ipfs/js-ipfs-http-client.git
33+
cd js-ipfs-http-client
34+
npm install # Installs ipfs-http-client dependencies
35+
cd examples/browser-pubsub
36+
npm install # Installs browser-pubsub app dependencies
37+
```
38+
39+
Start the example application:
40+
41+
```sh
42+
npm start
43+
```
44+
45+
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:
46+
47+
```sh
48+
Starting up http-server, serving ./
49+
Available on:
50+
http://127.0.0.1:8888
51+
```
52+
53+
## 2. Start two IPFS nodes
54+
55+
To demonstrate pubsub we need two nodes running so pubsub messages can be passed between them.
56+
57+
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.
58+
59+
### Install and start the JS IPFS node
60+
61+
```sh
62+
npm install -g ipfs
63+
jsipfs init
64+
# Configure CORS to allow ipfs-http-client to access this IPFS node
65+
jsipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["http://127.0.0.1:8888"]'
66+
# Start the IPFS node, enabling pubsub
67+
jsipfs daemon --enable-pubsub-experiment
68+
```
69+
70+
### Install and start the Go IPFS node
71+
72+
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.
73+
74+
After installation:
75+
76+
```sh
77+
ipfs init
78+
# Configure CORS to allow ipfs-http-client to access this IPFS node
79+
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["http://127.0.0.1:8888"]'
80+
# Start the IPFS node, enabling pubsub
81+
ipfs daemon --enable-pubsub-experiment
82+
```
83+
84+
## 3. Open two browsers and connect to each node
85+
86+
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.
87+
88+
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.
89+
90+
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.
91+
92+
> 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.
93+
94+
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).

examples/browser-pubsub/index.html

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<title>Pubsub in the browser</title>
5+
<link rel="stylesheet" href="https://unpkg.com/tachyons@4.10.0/css/tachyons.min.css"/>
6+
<link rel="stylesheet" href="https://unpkg.com/ipfs-css@0.12.0/ipfs.css">
7+
</head>
8+
<body class="sans-serif">
9+
<header class="pv3 ph2 ph3-l bg-navy cf mb4">
10+
<a href="https://ipfs.io/" title="ipfs.io">
11+
<img src="https://ipfs.io/images/ipfs-logo.svg" class="v-mid" style="height:50px">
12+
</a>
13+
<h1 class="aqua fw2 montserrat dib ma0 pv2 ph1 v-mid fr f3 lh-copy">Pubsub</h1>
14+
</header>
15+
<div class="ph3 mb3">
16+
<div class="fw2 tracked ttu f6 teal-muted mb2">API Addr</div>
17+
<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" />
18+
<button id="node-connect" class="dib ph3 pv2 input-reset ba b--black-20 border-box">Connect</button>
19+
</div>
20+
<div class="ph3 mb3">
21+
<div class="fw2 tracked ttu f6 teal-muted mb2">Connect to peer</div>
22+
<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" />
23+
<button id="peer-connect" class="dib ph3 pv2 input-reset ba b--black-20 border-box">Connect</button>
24+
</div>
25+
<div class="ph3 mb3">
26+
<div class="fw2 tracked ttu f6 teal-muted mb2">Subscribe to pubsub topic</div>
27+
<input id="topic" class="dib w-50 ph1 pv2 monospace input-reset ba b--black-20 border-box" placeholder="e.g. books" />
28+
<button id="subscribe" class="dib ph3 pv2 input-reset ba b--black-20 border-box">Subscribe</button>
29+
</div>
30+
<div class="ph3 mb3">
31+
<div class="fw2 tracked ttu f6 teal-muted mb2">Send pubsub message</div>
32+
<input id="message" class="dib w-50 ph1 pv2 monospace input-reset ba b--black-20 border-box" />
33+
<button id="send" class="dib ph3 pv2 input-reset ba b--black-20 border-box">Send</button>
34+
</div>
35+
<div class="ph3 mb3">
36+
<div class="fw2 tracked ttu f6 teal-muted mb2">Console</div>
37+
<div id="console" class="f7 db w-100 ph1 pv2 monospace input-reset ba b--black-20 border-box overflow-scroll" style="height: 300px">
38+
</div>
39+
</div>
40+
<script src="bundle.js"></script>
41+
</body>
42+
</html>

examples/browser-pubsub/index.js

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
'use strict'
2+
3+
const IpfsHttpClient = require('ipfs-http-client')
4+
const { sleep, Logger, onEnterPress, catchAndLog } = require('./util')
5+
6+
async function main () {
7+
const apiUrlInput = document.getElementById('api-url')
8+
const nodeConnectBtn = document.getElementById('node-connect')
9+
10+
const peerAddrInput = document.getElementById('peer-addr')
11+
const peerConnectBtn = document.getElementById('peer-connect')
12+
13+
const topicInput = document.getElementById('topic')
14+
const subscribeBtn = document.getElementById('subscribe')
15+
16+
const messageInput = document.getElementById('message')
17+
const sendBtn = document.getElementById('send')
18+
19+
let log = Logger(document.getElementById('console'))
20+
let ipfs
21+
let topic
22+
let peerId
23+
24+
async function reset () {
25+
if (ipfs && topic) {
26+
log(`Unsubscribing from topic ${topic}`)
27+
await ipfs.pubsub.unsubscribe(topic)
28+
}
29+
log = Logger(document.getElementById('console'))
30+
topicInput.value = ''
31+
topic = null
32+
peerId = null
33+
ipfs = null
34+
}
35+
36+
async function nodeConnect (url) {
37+
await reset()
38+
log(`Connecting to ${url}`)
39+
ipfs = IpfsHttpClient(url)
40+
const { id, agentVersion } = await ipfs.id()
41+
peerId = id
42+
log(`<span class="green">Success!</span>`)
43+
log(`Version ${agentVersion}`)
44+
log(`Peer ID ${id}`)
45+
}
46+
47+
async function peerConnect (addr) {
48+
if (!addr) throw new Error('Missing peer multiaddr')
49+
if (!ipfs) throw new Error('Connect to a node first')
50+
log(`Connecting to peer ${addr}`)
51+
await ipfs.swarm.connect(addr)
52+
log(`<span class="green">Success!</span>`)
53+
log('Listing swarm peers...')
54+
await sleep()
55+
const peers = await ipfs.swarm.peers()
56+
peers.forEach(peer => {
57+
const fullAddr = `${peer.addr}/ipfs/${peer.peer.toB58String()}`
58+
log(`<span class="${addr.endsWith(peer.peer.toB58String()) ? 'teal' : ''}">${fullAddr}</span>`)
59+
})
60+
log(`(${peers.length} peers total)`)
61+
}
62+
63+
async function subscribe (nextTopic) {
64+
if (!nextTopic) throw new Error('Missing topic name')
65+
if (!ipfs) throw new Error('Connect to a node first')
66+
67+
const lastTopic = topic
68+
69+
if (topic) {
70+
topic = null
71+
log(`Unsubscribing from topic ${lastTopic}`)
72+
await ipfs.pubsub.unsubscribe(lastTopic)
73+
}
74+
75+
log(`Subscribing to ${nextTopic}...`)
76+
77+
await ipfs.pubsub.subscribe(nextTopic, msg => {
78+
const from = msg.from
79+
const seqno = msg.seqno.toString('hex')
80+
if (from === peerId) return log(`Ignoring message ${seqno} from self`)
81+
log(`Message ${seqno} from ${from}:`)
82+
try {
83+
log(JSON.stringify(msg.data.toString(), null, 2))
84+
} catch (_) {
85+
log(msg.data.toString('hex'))
86+
}
87+
}, {
88+
onError: (err, fatal) => {
89+
if (fatal) {
90+
console.error(err)
91+
log(`<span class="red">${err.message}</span>`)
92+
topic = null
93+
log('Resubscribing in 5s...')
94+
setTimeout(catchAndLog(() => subscribe(nextTopic), log), 5000)
95+
} else {
96+
console.warn(err)
97+
}
98+
}
99+
})
100+
101+
topic = nextTopic
102+
log(`<span class="green">Success!</span>`)
103+
}
104+
105+
async function send (msg) {
106+
if (!msg) throw new Error('Missing message')
107+
if (!topic) throw new Error('Subscribe to a topic first')
108+
if (!ipfs) throw new Error('Connect to a node first')
109+
110+
log(`Sending message to ${topic}...`)
111+
await ipfs.pubsub.publish(topic, msg)
112+
log(`<span class="green">Success!</span>`)
113+
}
114+
115+
const onNodeConnectClick = catchAndLog(() => nodeConnect(apiUrlInput.value), log)
116+
apiUrlInput.addEventListener('keydown', onEnterPress(onNodeConnectClick))
117+
nodeConnectBtn.addEventListener('click', onNodeConnectClick)
118+
119+
const onPeerConnectClick = catchAndLog(() => peerConnect(peerAddrInput.value), log)
120+
peerAddrInput.addEventListener('keydown', onEnterPress(onPeerConnectClick))
121+
peerConnectBtn.addEventListener('click', onPeerConnectClick)
122+
123+
const onSubscribeClick = catchAndLog(() => subscribe(topicInput.value), log)
124+
topicInput.addEventListener('keydown', onEnterPress(onSubscribeClick))
125+
subscribeBtn.addEventListener('click', onSubscribeClick)
126+
127+
const onSendClick = catchAndLog(async () => {
128+
await send(messageInput.value)
129+
messageInput.value = ''
130+
}, log)
131+
messageInput.addEventListener('keydown', onEnterPress(onSendClick))
132+
sendBtn.addEventListener('click', onSendClick)
133+
}
134+
135+
main()

examples/browser-pubsub/package.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "browser-pubsub-example",
3+
"version": "0.0.0",
4+
"description": "An example demonstrating pubsub in the browser",
5+
"private": true,
6+
"main": "index.js",
7+
"scripts": {
8+
"start": "npm run build && npm run serve",
9+
"build": "browserify index.js > bundle.js",
10+
"serve": "http-server -a 127.0.0.1 -p 8888",
11+
"test": "echo \"Error: no test specified\" && exit 1"
12+
},
13+
"author": "Alan Shaw",
14+
"license": "MIT",
15+
"dependencies": {
16+
"browserify": "^16.5.0",
17+
"http-server": "^0.11.1",
18+
"ipfs-http-client": "../../"
19+
}
20+
}

examples/browser-pubsub/util.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
exports.sleep = (ms = 1000) => new Promise(resolve => setTimeout(resolve, ms))
2+
3+
exports.Logger = outEl => {
4+
outEl.innerHTML = ''
5+
return message => {
6+
const container = document.createElement('div')
7+
container.innerHTML = message
8+
outEl.appendChild(container)
9+
outEl.scrollTop = outEl.scrollHeight
10+
}
11+
}
12+
13+
exports.onEnterPress = fn => {
14+
return e => {
15+
if (event.which == 13 || event.keyCode == 13) {
16+
e.preventDefault()
17+
fn()
18+
}
19+
}
20+
}
21+
22+
exports.catchAndLog = (fn, log) => {
23+
return async (...args) => {
24+
try {
25+
await fn(...args)
26+
} catch (err) {
27+
console.error(err)
28+
log(`<span class="red">${err.message}</span>`)
29+
}
30+
}
31+
}

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
"browser": {
1818
"glob": false,
1919
"fs": false,
20-
"stream": "readable-stream"
20+
"stream": "readable-stream",
21+
"ky-universal": "ky/umd"
2122
},
2223
"repository": "github:ipfs/js-ipfs-http-client",
2324
"scripts": {

0 commit comments

Comments
 (0)