├── .gitignore ├── LICENSE ├── README.md ├── core.js ├── dht.js ├── key.js ├── package.json └── swarm.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | yarn.lock -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Lucas Barrena 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # use-hyper 2 | 3 | React hooks for the hypercore-protocol stack 4 | 5 | ![](https://img.shields.io/npm/v/use-hyper.svg) ![](https://img.shields.io/npm/dt/use-hyper.svg) ![](https://img.shields.io/github/license/LuKks/use-hyper.svg) 6 | 7 | ``` 8 | npm i use-hyper 9 | ``` 10 | 11 | Warning: this is experimental, and API might unexpectedly change until v1. 12 | 13 | ## Usage 14 | 15 | Every hook requires the related library installed: 16 | 17 | - `useCore` depends on `hypercore`. 18 | - `useDHT` depends on `@hyperswarm/dht-relay`. 19 | - `useSwarm` depends on `hyperswarm`. 20 | 21 | If you import `useSwarm` then install this specific branch:\ 22 | `npm i holepunchto/hyperswarm#add-swarm-session` 23 | 24 | ```jsx 25 | import { useCore, useCoreWatch, useCoreEvent } from 'use-hyper/core' 26 | import { DHT, useDHT } from 'use-hyper/dht' 27 | import { Swarm, useSwarm, useReplicate } from 'use-hyper/swarm' 28 | import RAM from 'random-access-memory' 29 | 30 | const Child = () => { 31 | const { core } = useCore() // Gets core from context 32 | 33 | const { onwatch } = useCoreWatch() // Triggers re-render when core changes 34 | const { onwatch: onappend } = useCoreWatch(['append']) // Same as above 35 | 36 | useCoreEvent('append', () => console.log('on event', core.length)) 37 | 38 | useReplicate(core) 39 | 40 | const DHT = useDHT() // Gets DHT from the context 41 | const swarm = useSwarm() // Same, from context 42 | 43 | return ( 44 |
45 | ID {core.id}
46 | Length {core.length}
47 | Peers {core.peers.length} 48 |
49 | ) 50 | } 51 | 52 | const App = () => { 53 | return ( 54 | 55 | 56 | 57 | ) 58 | } 59 | 60 | export default () => { 61 | return ( 62 | 63 | 64 | 65 | 66 | 67 | ) 68 | } 69 | ``` 70 | 71 | Every state like `core`, `dht`, or `swarm` starts being `null` but then gets updated with the corresponding object. 72 | 73 | ## License 74 | 75 | MIT 76 | -------------------------------------------------------------------------------- /core.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useRef, useContext, createContext } from 'react' 2 | import Hypercore from 'hypercore' 3 | import safetyCatch from 'safety-catch' 4 | 5 | const CoreContext = createContext() 6 | 7 | const EVENTS = [ 8 | 'ready', 9 | 'append', 10 | 'close', 11 | 'peer-add', 12 | 'peer-remove', 13 | 'truncate' 14 | ] 15 | 16 | export const Core = ({ children, storage, publicKey, ...options }) => { 17 | const [core, setCore] = useState(null) 18 | const deps = Object.values(options) 19 | 20 | useEffect(() => { 21 | if (!storage) return 22 | 23 | const core = new Hypercore(storage, publicKey, options) 24 | const onready = () => setCore(core) 25 | core.once('ready', onready) 26 | 27 | return () => { 28 | core.off('ready', onready) 29 | core.close().catch(safetyCatch) 30 | } 31 | }, [storage, publicKey, ...deps]) 32 | 33 | if (!core) return null 34 | 35 | return React.createElement( 36 | CoreContext.Provider, 37 | { value: { core } }, 38 | children 39 | ) 40 | } 41 | 42 | export const useCore = () => { 43 | const context = useContext(CoreContext) 44 | 45 | if (context === undefined) { 46 | throw new Error('useCore must be used within a Core component') 47 | } 48 | 49 | return context 50 | } 51 | 52 | export const useCoreEvent = (event, cb) => { 53 | const { core } = useCore() 54 | const fn = useRef(cb) 55 | 56 | useEffect(() => { 57 | fn.current = cb 58 | }, [cb]) 59 | 60 | useEffect(() => { 61 | if (!core) return 62 | 63 | const listener = (a, b, c) => fn.current(a, b, c) 64 | core.on(event, listener) 65 | 66 | return () => core.off(event, listener) 67 | }, [core, event]) 68 | } 69 | 70 | export const useCoreWatch = (events = EVENTS) => { 71 | const { core } = useCore() 72 | const [onwatch, setUpdated] = useState(0) 73 | 74 | const length = useRef(0) 75 | const peers = useRef(0) 76 | 77 | useEffect(() => { 78 | length.current = 0 79 | peers.current = 0 80 | }, [core]) 81 | 82 | useEffect(() => { 83 | if (!core) return 84 | 85 | const onchange = () => { 86 | length.current = core.length 87 | peers.current = core.peers.length 88 | 89 | setUpdated(i => i + 1) 90 | } 91 | 92 | for (const event of events) core.on(event, onchange) 93 | 94 | // Try to trigger the initial change 95 | if (events.includes('ready') && core.opened) onchange() 96 | else if (events.includes('close') && core.closed) onchange() 97 | else if (events.includes('append') && length < core.length) onchange() 98 | else if (events.includes('peer-add') && peers < core.peers.length) onchange() 99 | else if (events.includes('peer-remove') && peers > core.peers.length) onchange() 100 | 101 | return () => { 102 | for (const event of events) core.off(event, onchange) 103 | } 104 | }, [core, ...events]) 105 | 106 | return { onwatch } 107 | } 108 | -------------------------------------------------------------------------------- /dht.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useContext, createContext } from 'react' 2 | import DHTRelay from '@hyperswarm/dht-relay' 3 | import Stream from '@hyperswarm/dht-relay/ws' 4 | import safetyCatch from 'safety-catch' 5 | import { primaryKey } from './key.js' 6 | 7 | // + add more relays 8 | // + should detect WebSocket errors, etc to retry a different relay 9 | 10 | const DHT_RELAY_ADDRESS = 'wss://dht1-relay.leet.ar:49443' 11 | 12 | const DHTContext = createContext() 13 | 14 | export const DHT = ({ children, url, keyPair, ...options }) => { 15 | const [dht, setDHT] = useState(null) 16 | 17 | useEffect(() => { 18 | const ws = new window.WebSocket(url || DHT_RELAY_ADDRESS) 19 | const stream = new Stream(true, ws) 20 | 21 | keyPair = keyPair || DHTRelay.keyPair(primaryKey) 22 | 23 | const relay = new DHTRelay(stream, { keyPair, ...options }) 24 | setDHT(relay) 25 | 26 | return () => { 27 | relay.destroy().catch(safetyCatch) 28 | } 29 | }, [keyPair]) 30 | 31 | return React.createElement( 32 | DHTContext.Provider, 33 | { 34 | value: { dht } 35 | }, 36 | children 37 | ) 38 | } 39 | 40 | export const useDHT = () => { 41 | const context = useContext(DHTContext) 42 | 43 | if (context === undefined) { 44 | throw new Error('useDHT must be used within a DHT component') 45 | } 46 | 47 | return context 48 | } 49 | -------------------------------------------------------------------------------- /key.js: -------------------------------------------------------------------------------- 1 | import b4a from 'b4a' 2 | import sodium from 'sodium-universal' 3 | 4 | let primaryKey = window.localStorage.getItem('primary-key') 5 | 6 | if (primaryKey === null) { 7 | primaryKey = b4a.toString(randomBytes(32), 'hex') 8 | window.localStorage.setItem('primary-key', primaryKey) 9 | } 10 | 11 | primaryKey = b4a.from(primaryKey, 'hex') 12 | 13 | export { primaryKey } 14 | 15 | function randomBytes (n) { 16 | const buf = b4a.allocUnsafe(n) 17 | sodium.randombytes_buf(buf) 18 | return buf 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-hyper", 3 | "version": "0.0.6", 4 | "description": "React hooks for the hypercore-protocol stack", 5 | "main": "", 6 | "scripts": { 7 | "test": "standard" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/LuKks/use-hyper.git" 12 | }, 13 | "author": "Lucas Barrena (LuKks)", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/LuKks/use-hyper/issues" 17 | }, 18 | "homepage": "https://github.com/LuKks/use-hyper", 19 | "devDependencies": { 20 | "standard": "^17.0.0" 21 | }, 22 | "dependencies": { 23 | "b4a": "^1.6.3", 24 | "safety-catch": "^1.0.2", 25 | "sodium-universal": "^4.0.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /swarm.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useContext, createContext } from 'react' 2 | import Hyperswarm from 'hyperswarm' 3 | import safetyCatch from 'safety-catch' 4 | import { useDHT } from './dht.js' 5 | 6 | const SwarmContext = createContext() 7 | 8 | export const Swarm = ({ children, ...options }) => { 9 | const { dht } = useDHT() 10 | const [swarm, setSwarm] = useState(null) 11 | 12 | useEffect(() => { 13 | if (!dht) return 14 | 15 | const swarm = new Hyperswarm({ 16 | keyPair: dht.defaultKeyPair, 17 | ...options, 18 | dht 19 | }) 20 | 21 | setSwarm(swarm) 22 | 23 | return () => { 24 | swarm.destroy().catch(safetyCatch) // Run on background 25 | } 26 | }, [dht]) 27 | 28 | return React.createElement( 29 | SwarmContext.Provider, 30 | { value: { swarm } }, 31 | children 32 | ) 33 | } 34 | 35 | export const useSwarm = () => { 36 | const context = useContext(SwarmContext) 37 | 38 | if (context === undefined) { 39 | throw new Error('useSwarm must be used within a Swarm component') 40 | } 41 | 42 | return context 43 | } 44 | 45 | export const useReplicate = (core, deps = []) => { 46 | const { swarm } = useSwarm() 47 | 48 | useEffect(() => { 49 | if (!swarm || !core || core.closed) return 50 | 51 | let cleanup = false 52 | let session = null 53 | 54 | const onsocket = socket => core.replicate(socket) 55 | const ready = core.ready().catch(safetyCatch) 56 | 57 | ready.then(() => { 58 | if (cleanup) return 59 | 60 | session = swarm.session({ keyPair: swarm.keyPair }) 61 | 62 | // + done could be outside of ready 63 | const done = core.findingPeers() 64 | session.on('connection', onsocket) 65 | session.join(core.discoveryKey, { server: false, client: true }) 66 | session.flush().then(done, done) 67 | }) 68 | 69 | return () => { 70 | cleanup = true 71 | 72 | if (!session) return 73 | 74 | session.destroy().catch(safetyCatch) // Run on background 75 | } 76 | }, [swarm, core, ...deps]) 77 | } 78 | --------------------------------------------------------------------------------