├── .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 |   
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 |
--------------------------------------------------------------------------------