diff --git a/examples/chat-in-the-browser/.babelrc b/examples/chat-in-the-browser/.babelrc new file mode 100644 index 0000000000..620e785799 --- /dev/null +++ b/examples/chat-in-the-browser/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["@babel/preset-env"], + "plugins": ["syntax-async-functions","transform-regenerator"] +} \ No newline at end of file diff --git a/examples/chat-in-the-browser/README.md b/examples/chat-in-the-browser/README.md new file mode 100644 index 0000000000..e49e073464 --- /dev/null +++ b/examples/chat-in-the-browser/README.md @@ -0,0 +1,63 @@ +# libp2p chat in the browser + +This example leverages the [Parcel.js bundler](https://parceljs.org/) to compile and serve the libp2p chat in the browser. Parcel uses [Babel](https://babeljs.io/) to handle transpilation of the code. You can use other bundlers such as Webpack or Browserify, but we will not be covering them here. + +Some interesting discussion and background on this example can be found in [it's pull request](https://github.com/libp2p/js-libp2p/pull/616). + +## Setup + +In order to run the example, first install the dependencies from same directory as this README: + +``` +cd ./examples/chat-in-the-browser +npm install +``` + +## Signaling Server + +This example uses the `libp2p-webrtc-star` module, which enables libp2p browser nodes to establish direct connections to one another via a central signaling server. For this example, we are using the signaling server that ships with `libp2p-webrtc-star`. + +You can start the server by running `npm run server`. This will start a signaling server locally on port `9090`. If you'd like to run a signaling server outside of this example, you can see instructions on how to do so in the [`libp2p-webrtc-star` README](https://github.com/libp2p/js-libp2p-webrtc-star). + +When you run the server, you should see output that looks something like this: + +```log +$ npm run server + +> libp2p-in-browser@1.0.0 server +> star-signal + +Listening on: http://0.0.0.0:9090 +``` + +## Running the examples + +Once you have started the signaling server, you can run the Parcel server. + +``` +npm start +``` + +The output should look something like this: + +```log +$ npm start + +> chat-in-the-browser@1.0.0 start +> parcel index.html + +Server running at http://localhost:1234 +✨ Built in 1000ms. +``` + +This will compile the code and start a server listening on port [http://localhost:1234](http://localhost:1234). Now open your browser to `http://localhost:1234`. You should see a log of your node's Peer ID, the discovered peers from the Bootstrap module, and connections to those peers as they are created. + +Now, if you open a second browser tab to `http://localhost:1234`, you should discover your node from the previous tab. This is due to the fact that the `libp2p-webrtc-star` transport also acts as a Peer Discovery interface. Your node will be notified of any peer that connects to the same signaling server you are connected to. Once libp2p discovers this new peer, it will attempt to establish a direct WebRTC connection. + +**Note**: In the example we assign libp2p to `window.libp2p`, in case you would like to play around with the API directly in the browser. You can of course make changes to `index.js` and Parcel will automatically rebuild and reload the browser tabs. + +**Note**: During startup, the example raises exceptions "protocol selection failed". This is normal. The reason is that the example tries to chat with the bootstrap servers, but they don't support the chat protocol. + +## TypeScript + +This example also shows how to use Libp2p using TypeScript. Libp2p itself currently does not support TypeScript, but this example includes a few minimal type declarations so that it can be used, even with strict compilation. diff --git a/examples/chat-in-the-browser/index.html b/examples/chat-in-the-browser/index.html new file mode 100644 index 0000000000..77950c03ba --- /dev/null +++ b/examples/chat-in-the-browser/index.html @@ -0,0 +1,27 @@ + + + + + + js-libp2p parcel.js browser example + + + +
+

Starting...

+
+ + + + +

+    
+ +
+

Log

+

+    
+ + + + diff --git a/examples/chat-in-the-browser/index.ts b/examples/chat-in-the-browser/index.ts new file mode 100644 index 0000000000..155a2c5fe5 --- /dev/null +++ b/examples/chat-in-the-browser/index.ts @@ -0,0 +1,148 @@ +import 'babel-polyfill' +import Libp2p from 'libp2p' +import Websockets from 'libp2p-websockets' +import WebRTCStar from 'libp2p-webrtc-star' +import Secio from 'libp2p-secio' +import Mplex from 'libp2p-mplex' +import Boostrap from 'libp2p-bootstrap' +import pipe from 'it-pipe' +import PeerInfo from 'peer-info' +import { consume } from 'streaming-iterables' +import { ProtocolHandler } from './types/libp2p' +import multiaddr from 'multiaddr' + +declare global { + interface Window { + libp2p: Libp2p + send: (event: KeyboardEvent | MouseEvent) => void + } +} + +document.addEventListener('DOMContentLoaded', async () => { + // Create our libp2p node + const libp2p = await Libp2p.create({ + modules: { + transport: [Websockets, WebRTCStar], + connEncryption: [Secio], + streamMuxer: [Mplex], + peerDiscovery: [Boostrap] + }, + config: { + peerDiscovery: { + bootstrap: { + enabled: true, + list: [ + '/dns4/ams-1.bootstrap.libp2p.io/tcp/443/wss/p2p/QmSoLer265NRgSp2LA3dPaeykiS1J6DifTC88f5uVQKNAd', + '/dns4/lon-1.bootstrap.libp2p.io/tcp/443/wss/p2p/QmSoLMeWqB7YGVLJN3pNLQpmmEk35v6wYtsMGLzSr5QBU3', + '/dns4/sfo-3.bootstrap.libp2p.io/tcp/443/wss/p2p/QmSoLPppuBtQSGwKDZT2M73ULpjvfd3aZ6ha4oFGL1KrGM', + '/dns4/sgp-1.bootstrap.libp2p.io/tcp/443/wss/p2p/QmSoLSafTMBsPKadTEgaXctDQVcqN88CNLHXMkTNwMKPnu', + '/dns4/nyc-1.bootstrap.libp2p.io/tcp/443/wss/p2p/QmSoLueR4xBeUbY9WZ9xGUUxunbKWcrNFTDAadQJmocnWm', + '/dns4/nyc-2.bootstrap.libp2p.io/tcp/443/wss/p2p/QmSoLV4Bbm51jM9C4gDYZQ9Cy3U6aXMJDAbzgu2fzaDs64' + ] + } + } + } + }) + + // Our protocol identifier + const protocol = '/chat' + + // UI elements + const status = document.getElementById('status')! + const chat = document.getElementById('chat')! + const output = document.getElementById('output')! + const txtSend = document.getElementById('txt_send')! as HTMLInputElement + const btnSend = document.getElementById('btn_send')! as HTMLButtonElement + + chat.textContent = '' + output.textContent = '' + + function addChatLine (txt: string) { + const now = new Date().toLocaleTimeString() + chat.textContent += `[${now}] ${txt}\n` + } + + function log (txt: string) { + console.info(txt) + output.textContent += `${txt.trim()}\n` + } + + // Add the signaling server address, along with our PeerId to our multiaddrs list + // libp2p will automatically attempt to dial to the signaling server so that it can + // receive inbound connections from other peers + const webrtcAddr = '/ip4/0.0.0.0/tcp/9090/wss/p2p-webrtc-star' + libp2p.peerInfo.multiaddrs.add(multiaddr(webrtcAddr)) + + // Listen for new peers + libp2p.on('peer:discovery', (peerInfo: PeerInfo) => { + log(`Found peer ${peerInfo.id.toB58String()}`) + }) + + // Listen for new connections to peers + let remotePeer: PeerInfo | null + + libp2p.on('peer:connect', (peerInfo: PeerInfo) => { + log(`Connected to ${peerInfo.id.toB58String()}`) + libp2p.dialProtocol(peerInfo, [protocol]).then(() => { + log('dialed a stream') + // Dial was successful, meaning that the other end can speak our + // protocol. Capture the peerInfo so that we can send messages later on. + remotePeer = peerInfo + btnSend.disabled = false + }) + }) + + // Listen for peers disconnecting + libp2p.on('peer:disconnect', (peerInfo: PeerInfo) => { + log(`Disconnected from ${peerInfo.id.toB58String()}`) + }) + + await libp2p.start() + status.innerText = 'libp2p started!' + log(`libp2p id is ${libp2p.peerInfo.id.toB58String()}`) + + const handleChat: ProtocolHandler = async ({ connection, stream }) => { + log(`handle chat from ${connection?.remotePeer.toB58String()}`) + const handledStream = stream + pipe(handledStream, async function (source: AsyncGenerator) { + for await (const msg of source) { + log(`Received message: ${msg}`) + addChatLine( + `${connection?.remotePeer.toB58String().substr(0, 5)}: ${msg}` + ) + } + // Causes `consume` in `sendMessage` to close the stream, as a sort + // of ACK: + pipe([], handledStream) + }) + } + + // Tell libp2p how to handle our protocol + await libp2p.handle([protocol], handleChat) + + function send (event: KeyboardEvent | MouseEvent) { + const k = event as KeyboardEvent + if (k && k.keyCode !== 13) return // ignore key events other than + + if (remotePeer) { + const value = txtSend.value + txtSend.value = '' + sendMessage(remotePeer, value) + addChatLine(`me: ${value}`) + } + } + + async function sendMessage (peerInfo: PeerInfo, message: string) { + try { + const { stream } = await libp2p.dialProtocol(peerInfo, [protocol]) + await pipe([message], stream, consume) + } catch (err) { + log('Send failed; please check console for details.') + console.error('Could not send the message', err) + } + } + + // Export libp2p and send to the window so you can play with the API + window.libp2p = libp2p + window.send = send +}) diff --git a/examples/chat-in-the-browser/package.json b/examples/chat-in-the-browser/package.json new file mode 100644 index 0000000000..4b1964eebf --- /dev/null +++ b/examples/chat-in-the-browser/package.json @@ -0,0 +1,38 @@ +{ + "name": "chat-in-the-browser", + "version": "1.0.0", + "description": "A libp2p-based chat running in the browser", + "main": "index.js", + "browserslist": [ + "last 2 Chrome versions" + ], + "scripts": { + "type-check": "tsc --noEmit", + "type-check:watch": "tsc --noEmit --watch", + "test": "echo \"Error: no test specified\" && exit 1", + "start": "parcel index.html", + "server": "star-signal" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@babel/preset-env": "^7.8.3", + "libp2p": "../../", + "libp2p-bootstrap": "^0.10.3", + "libp2p-mplex": "^0.9.3", + "libp2p-secio": "^0.12.2", + "libp2p-webrtc-star": "^0.17.3", + "libp2p-websockets": "^0.13.2" + }, + "devDependencies": { + "@babel/cli": "^7.8.3", + "@babel/core": "^7.8.3", + "@types/node": "^13.13.4", + "babel-plugin-syntax-async-functions": "^6.13.0", + "babel-plugin-transform-regenerator": "^6.26.0", + "babel-polyfill": "^6.26.0", + "parcel-bundler": "^1.12.4", + "typescript": "^3.8.3" + } +} diff --git a/examples/chat-in-the-browser/tsconfig.json b/examples/chat-in-the-browser/tsconfig.json new file mode 100644 index 0000000000..d5747a143a --- /dev/null +++ b/examples/chat-in-the-browser/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "strict": true, + "target": "esnext", + "module": "commonjs", + "allowSyntheticDefaultImports": true, + "baseUrl": "./", + "paths": { + "*": [ + "types/*" + ] + } + }, + "exclude": [ + "node_modules", + ] +} diff --git a/examples/chat-in-the-browser/types/libp2p-bootstrap/index.d.ts b/examples/chat-in-the-browser/types/libp2p-bootstrap/index.d.ts new file mode 100644 index 0000000000..e8faf6e9e4 --- /dev/null +++ b/examples/chat-in-the-browser/types/libp2p-bootstrap/index.d.ts @@ -0,0 +1,9 @@ +/* + * This is a minimal type declaration file for the chat-in-the-browser example. + * It is incomplete, but you can use it as a basis for your own TypeScript + * projects. + */ + +export = libp2p_bootstrap + +declare class libp2p_bootstrap {} diff --git a/examples/chat-in-the-browser/types/libp2p-mplex/index.d.ts b/examples/chat-in-the-browser/types/libp2p-mplex/index.d.ts new file mode 100644 index 0000000000..ccd86d2cf8 --- /dev/null +++ b/examples/chat-in-the-browser/types/libp2p-mplex/index.d.ts @@ -0,0 +1,9 @@ +/* + * This is a minimal type declaration file for the chat-in-the-browser example. + * It is incomplete, but you can use it as a basis for your own TypeScript + * projects. + */ + +export = libp2p_mplex + +declare class libp2p_mplex {} diff --git a/examples/chat-in-the-browser/types/libp2p-secio/index.d.ts b/examples/chat-in-the-browser/types/libp2p-secio/index.d.ts new file mode 100644 index 0000000000..8f11f91cbe --- /dev/null +++ b/examples/chat-in-the-browser/types/libp2p-secio/index.d.ts @@ -0,0 +1,19 @@ +/* + * This is a minimal type declaration file for the chat-in-the-browser example. + * It is incomplete, but you can use it as a basis for your own TypeScript + * projects. + */ + +export const protocol: string + +export function secureInbound ( + localPeer: any, + duplex: any, + remotePeer: any +): any + +export function secureOutbound ( + localPeer: any, + duplex: any, + remotePeer: any +): any diff --git a/examples/chat-in-the-browser/types/libp2p-webrtc-star/index.d.ts b/examples/chat-in-the-browser/types/libp2p-webrtc-star/index.d.ts new file mode 100644 index 0000000000..5acae13061 --- /dev/null +++ b/examples/chat-in-the-browser/types/libp2p-webrtc-star/index.d.ts @@ -0,0 +1,11 @@ +/* + * This is a minimal type declaration file for the chat-in-the-browser example. + * It is incomplete, but you can use it as a basis for your own TypeScript + * projects. + */ + +export = libp2p_webrtc_star + +declare function libp2p_webrtc_star (...args: any[]): any + +declare namespace libp2p_webrtc_star {} diff --git a/examples/chat-in-the-browser/types/libp2p-websockets/index.d.ts b/examples/chat-in-the-browser/types/libp2p-websockets/index.d.ts new file mode 100644 index 0000000000..e04203f37d --- /dev/null +++ b/examples/chat-in-the-browser/types/libp2p-websockets/index.d.ts @@ -0,0 +1,11 @@ +/* + * This is a minimal type declaration file for the chat-in-the-browser example. + * It is incomplete, but you can use it as a basis for your own TypeScript + * projects. + */ + +export = libp2p_websockets + +declare function libp2p_websockets (...args: any[]): any + +declare namespace libp2p_websockets {} diff --git a/examples/chat-in-the-browser/types/libp2p/index.d.ts b/examples/chat-in-the-browser/types/libp2p/index.d.ts new file mode 100644 index 0000000000..ba3b828ccc --- /dev/null +++ b/examples/chat-in-the-browser/types/libp2p/index.d.ts @@ -0,0 +1,39 @@ +/* + * This is a minimal type declaration file for the chat-in-the-browser example. + * It is incomplete, but you can use it as a basis for your own TypeScript + * projects. + */ + +import PeerInfo from 'peer-info' +import PeerId from 'peer-id' + +export = Libp2p + +declare class Libp2p { + peerInfo: PeerInfo + + static create (options: any): Promise + start (): Promise + handle ( + protocol: string | string[], + handler: Libp2p.ProtocolHandler + ): Promise + dialProtocol (remote: PeerInfo, protocols: string[]): Promise<{ stream: any }> + on (event: Libp2p.Event, handler: Libp2p.PeerInfoHandler): void +} + +declare namespace Libp2p { + type Event = 'peer:connect' | 'peer:disconnect' | 'peer:discovery' + type PeerInfoHandler = (peerInfo: PeerInfo) => void + type Stream = { + source: AsyncGenerator + sink: (source: any) => void + } + type ProtocolHandler = (result: { + connection?: { + remotePeer: PeerId + } + protocol?: string + stream: Stream + }) => void +}