├── .gitignore ├── problems ├── 01 │ ├── client.js │ ├── package.json │ ├── readme.md │ └── server.js ├── 02 │ ├── package.json │ ├── readme.md │ └── swarm.js ├── 03 │ ├── .gitignore │ ├── corestore.js │ ├── package.json │ ├── peer.js │ ├── readme.md │ └── seed.js ├── 04a │ ├── index.js │ ├── package.json │ └── readme.md ├── 04b │ ├── index.js │ ├── package.json │ └── readme.md ├── 05 │ ├── hypernews.js │ ├── package.json │ └── readme.md ├── 06 │ ├── hypernews.js │ ├── package.json │ └── readme.md └── 07 │ ├── hypernews.js │ ├── package.json │ └── readme.md ├── readme.md └── solutions ├── 05 └── hypernews.js ├── 06 └── hypernews.js └── 07 ├── hypernews.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | .env.production 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | .parcel-cache 80 | 81 | # Next.js build output 82 | .next 83 | out 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and not Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | 110 | # Stores VSCode versions used for testing VSCode extensions 111 | .vscode-test 112 | 113 | # yarn v2 114 | .yarn/cache 115 | .yarn/unplugged 116 | .yarn/build-state.yml 117 | .yarn/install-state.gz 118 | .pnp.* 119 | 120 | links 121 | cores 122 | keys -------------------------------------------------------------------------------- /problems/01/client.js: -------------------------------------------------------------------------------- 1 | import DHT from '@hyperswarm/dht' 2 | 3 | const node = new DHT() 4 | 5 | const remotePublicKey = Buffer.from('cc240d6f68525f515816e4c09328eb37c6eea2e1ec9190910c93f536561ca447', 'hex') 6 | const encryptedSocket = node.connect(remotePublicKey) 7 | 8 | encryptedSocket.on('open', function () { 9 | console.log('Connected to server') 10 | }) 11 | 12 | encryptedSocket.on('data', function (data) { 13 | console.log('Remote said:', data.toString()) 14 | }) 15 | 16 | -------------------------------------------------------------------------------- /problems/01/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "dependencies": { 4 | "@hyperswarm/dht": "next" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /problems/01/readme.md: -------------------------------------------------------------------------------- 1 | # P2P Networking with Hyperswarm 2 | 3 | A massive part of P2P is just connecting computers with each other. Unlike a cloud based environment, connecting computers running at home is challenging. The vast majority of networks are locked down behind firewalls and NATs making running servers at home very non-trivial. 4 | 5 | Luckily we've been spending the last many years trying to solve these problems by making Hyperswarm, a fully distributed and trustless DHT, that helps computers at home penetrate through NATs to make direct connections to other computers. 6 | 7 | Instead of using hostnames and ports, Hyperswarm uses key addressed networking. This means that servers (and clients) are identified by a cryptographic keypair. 8 | 9 | Instead of doing `connect(port, hostname)` you do `connect(publicKey)` and instead of servers doing `listen(port)` we do `listen({ publicKey, secretKey })`. 10 | 11 | This is a super powerful technique as it decouples the location at which a server has to be running. Additionally it also means that ALL connections can be end to end encrypted at all time as their address, ie public key, is the information you need cryptographically to bootstrap a fully secure session. 12 | 13 | Hyperswarm implements this low level API in its DHT module. Each instance of the DHT gossips with a global trustless network to find other peers associated with a key pair. You can think of this as being conceptually similar to how routers gossip IPs to find each other as well. 14 | 15 | ## Exercise 1: Making servers and clients with the DHT 16 | 17 | Let's try it out. First install the latest version of the Hyperswarm DHT module. It is available through NPM under the next tag. 18 | 19 | ``` 20 | npm install @hyperswarm/dht@next 21 | ``` 22 | 23 | Make two files server.js and client.js and add a package.json with `{ "type": "module" }` so ESM loading and top-level await works. 24 | 25 | Then in server.js make a server: 26 | 27 | ```js 28 | import DHT from '@hyperswarm/dht' 29 | 30 | // Make a Hyperswarm DHT node that connects to the global network. 31 | const node = new DHT() 32 | 33 | const server = node.createServer(function (encryptedSocket) { 34 | // Called when a new connection arrives. 35 | console.log('New connection from', encryptedSocket.remotePublicKey.toString('hex')) 36 | encryptedSocket.write('Hello world!') 37 | encryptedSocket.end() 38 | }) 39 | 40 | const keyPair = DHT.keyPair() 41 | await server.listen(keyPair) 42 | 43 | // Server is now listening. 44 | console.log('Connect to:') 45 | console.log(keyPair.publicKey.toString('hex')) 46 | ``` 47 | 48 | This example server creates a new key pair to listen on each time it's run, but prints out the public key. Copy the key it prints out and make a client in client.js: 49 | 50 | ```js 51 | import DHT from '@hyperswarm/dht' 52 | 53 | const node = new DHT() 54 | 55 | const remotePublicKey = Buffer.from('hex-from-above', 'hex') 56 | const encryptedSocket = node.connect(remotePublicKey) 57 | 58 | encryptedSocket.on('open', function () { 59 | console.log('Connected to server') 60 | }) 61 | 62 | encryptedSocket.on('data', function (data) { 63 | console.log('Remote said:', data.toString()) 64 | }) 65 | ``` 66 | 67 | Now for the exercise. 68 | 69 | 1. Run `server.js` in one terminal and copy the public key it prints. 70 | 2. Modify `client.js` to use the key and run it in another terminal. 71 | 3. See that it prints the server response. 72 | 73 | ## Exercise 2: 74 | 75 | P2P networks on the same computer is not as fun as P2P networks on remote computers. 76 | 77 | 1. Try getting another participant in the workshop to run your client. 78 | 79 | Alternatively ssh into a server if you can and try or get a workshop host to run your client. 80 | 81 | ## Next 82 | 83 | When you are done continue to [Problem 2](../02) 84 | -------------------------------------------------------------------------------- /problems/01/server.js: -------------------------------------------------------------------------------- 1 | import DHT from '@hyperswarm/dht' 2 | 3 | // Make a Hyperswarm DHT node that connects to the global network. 4 | const node = new DHT() 5 | 6 | const server = node.createServer(function (encryptedSocket) { 7 | // Called when a new connection arrives. 8 | console.log('New connection from', encryptedSocket.remotePublicKey.toString('hex')) 9 | encryptedSocket.write('Hello world!') 10 | encryptedSocket.end() 11 | }) 12 | 13 | const keyPair = DHT.keyPair() 14 | await server.listen(keyPair) 15 | 16 | // Server is now listening. 17 | console.log('Connect to:') 18 | console.log(keyPair.publicKey.toString('hex')) 19 | 20 | -------------------------------------------------------------------------------- /problems/02/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "dependencies": { 4 | "hyperswarm": "next" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /problems/02/readme.md: -------------------------------------------------------------------------------- 1 | # Swarms, Swarms Everywhere! 2 | 3 | Okay, so in the previous exercise we got our feet a little wet with the basics of P2P networking. 4 | The `createServer` / `connect` apis are at the foundation of everything we do with P2P, but often when making applications we don't really care who is acting as a server and who is acting as a client. After all we are making P2P applications so both peers are usually both at once! Similarly we often want to group peers per application and make sure peers reconnect etc. 5 | 6 | To avoid having to have users do all that work themselves we usually use an abstraction on top of the Hyperswarm DHT API called ... a swarm. A swarm just represents a set of incoming and outgoing connections that are being maintained for you. 7 | 8 | # Exercise 1 9 | 10 | Let's try it out. 11 | 12 | First install the main swarm abstraction. It's available as the main hyperswarm package. Again the latest one is released under the next tag. 13 | 14 | ``` 15 | npm install hyperswarm@next 16 | ``` 17 | 18 | Then make a file called `swarm.js` and add the following: 19 | 20 | ```js 21 | import Hyperswarm from 'hyperswarm' 22 | import crypto from 'crypto' 23 | 24 | const swarm = new Hyperswarm() 25 | 26 | // Swarms abstract away servers and clients and just gives you connections 27 | swarm.on('connection', function (encryptedSocket) { 28 | console.log('New connection from', encryptedSocket.remotePublicKey.toString('hex')) 29 | 30 | encryptedSocket.write('Hello world!') 31 | 32 | encryptedSocket.on('data', function (data) { 33 | console.log('Remote peer said:', data.toString()) 34 | }) 35 | encryptedSocket.on('error', function (err) { 36 | console.log('Remote peer errored:', err) 37 | }) 38 | encryptedSocket.on('close', function () { 39 | console.log('Remote peer fully left') 40 | }) 41 | }) 42 | 43 | // Topics are just identifiers to find other peers under 44 | const topic = crypto.createHash('sha256').update('Insert a topic name here').digest() 45 | swarm.join(topic) 46 | ``` 47 | 48 | Now do the following: 49 | 50 | 1. Replace the topic name above with something unique for you (could be your name). 51 | 2. Try running two or more instances of the swarm, see that the connect together. 52 | 3. Make sure it prints hello world for each peer you add. 53 | 54 | # Exercise 2 55 | 56 | Let's make a tiny small functional program out of our swarm. Let's turn it into a simple chat service. 57 | 58 | If we change of body of the connection handler to do this: 59 | 60 | ```js 61 | process.stdin.pipe(encryptedSocket).pipe(process.stdout) 62 | ``` 63 | 64 | Then we are effectively piping each peer to stdout out and piping our stdin to all peers - a silly chat!. 65 | 66 | 1. Update the code with the above change. 67 | 2. Try running multiple instances and type something and hit enter and see your messages appear with other peers. 68 | 3. Like in the previous exercise try sharing your chat topic with other people in the workshop and do a simple 5 line, end to end encrypted cross internet chat. 69 | 70 | # Next 71 | 72 | When you are done continue to [Problem 3](../03) 73 | 74 | -------------------------------------------------------------------------------- /problems/02/swarm.js: -------------------------------------------------------------------------------- 1 | import Hyperswarm from 'hyperswarm' 2 | import crypto from 'crypto' 3 | 4 | const swarm = new Hyperswarm() 5 | 6 | // Swarms abstract away servers and clients and just gives you connections 7 | swarm.on('connection', function (encryptedSocket) { 8 | console.log('New connection from', encryptedSocket.remotePublicKey.toString('hex')) 9 | 10 | encryptedSocket.write('Hello world!') 11 | 12 | encryptedSocket.on('data', function (data) { 13 | console.log('Remote peer said:', data.toString()) 14 | }) 15 | encryptedSocket.on('error', function (err) { 16 | console.log('Remote peer errored:', err) 17 | }) 18 | encryptedSocket.on('close', function () { 19 | console.log('Remote peer fully left') 20 | }) 21 | }) 22 | 23 | // Topics are just identifiers to find other peers under 24 | const topic = crypto.createHash('sha256').update('Insert a topic name here').digest() 25 | swarm.join(topic) 26 | 27 | -------------------------------------------------------------------------------- /problems/03/.gitignore: -------------------------------------------------------------------------------- 1 | store 2 | peer-store 3 | seed-store 4 | -------------------------------------------------------------------------------- /problems/03/corestore.js: -------------------------------------------------------------------------------- 1 | import Corestore from 'corestore' 2 | 3 | const store = new Corestore('./store') 4 | 5 | // You can access cores from the store either by their public key or a local name 6 | const core = store.get({ name: 'my-first-core' }) 7 | 8 | await core.ready() 9 | 10 | console.log('Core public key:', core.key.toString('hex')) 11 | console.log('Core has', core.length, 'entries') 12 | 13 | await core.append(Buffer.from('a block')) 14 | -------------------------------------------------------------------------------- /problems/03/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "dependencies": { 4 | "corestore": "next", 5 | "hyperswarm": "next" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /problems/03/peer.js: -------------------------------------------------------------------------------- 1 | import Corestore from 'corestore' 2 | import Hyperswarm from 'hyperswarm' 3 | 4 | const store = new Corestore('./peer-store') 5 | const swarm = new Hyperswarm() 6 | 7 | // Setup corestore replication 8 | swarm.on('connection', (connection) => store.replicate(connection)) 9 | 10 | // Load a core by public key 11 | const core = store.get(Buffer.from('public-key-from-above', 'hex')) 12 | 13 | await core.ready() 14 | 15 | // Join the Hypercore discoveryKey (a hash of it's public key) 16 | swarm.join(core.discoveryKey) 17 | 18 | // Make sure we have all the connections 19 | await swarm.flush() 20 | 21 | // Make sure we have the latest length 22 | await core.update() 23 | 24 | // Print the length (should print 10000) 25 | console.log('Core length is:', core.length) 26 | -------------------------------------------------------------------------------- /problems/03/readme.md: -------------------------------------------------------------------------------- 1 | # What to Swarm? Cores! 2 | 3 | While P2P networking is an incredibly powerful concept by itself, it often lacks a companion. A fully authenticated and secure data structure you can share with multiple peers without having to trust any of them to not modify the data before giving it to other peers. 4 | 5 | That abstraction is called Hypercore, and we've covered it in many previous workshops. For this workshop in the later exercises we'll be building powerful multi-writer applications on top of it, and we'll be using the next version of Hypercore to power this, called Hypercore 10. 6 | 7 | Hypercore 10 is an append-only log, or basically a distributed array, that supports a series of important APIs 8 | 9 | * `await core.append(data)` - Insert a new block of data 10 | * `data = await core.get(index)` - Get a specific block of data 11 | * `await core.update()` - Make sure you have the latest version 12 | * `core.length` - How much data is in the core? 13 | * `await core.truncate(newLength)` - Reset the core to a specific length 14 | 15 | These APIs can be used to build a ton of powerful data structures on top, including key/value stores, video streaming and much more. 16 | 17 | One of the most powerful things that Hypercore provides is the ability to download just the specific parts of the core you need for your application. For example a video stream would only want to download the blocks of data needed to render the video to the user rather than to download a full 4K video first. 18 | 19 | Hypercore provides all this, but in a way where peers can relay blocks of data to other peers, without anyone having to trust anything other than the public key of the Hypercore itself. The caveat is that only a single person is allowed to update and modify the Hypercore itself, but with the above primitives we can build simple abstractions that solves that also, which we'll do in the next exercise after this. 20 | 21 | Hypercores are most easily managed using something called a Corestore, which is a small abstraction that creates and maintains as many Hypercores as you need. 22 | 23 | You can read more about Hypercore 10 in it's readme: 24 | 25 | https://github.com/hypercore-protocol/hypercore-next 26 | 27 | And more about Corestore in it's readme: 28 | 29 | https://github.com/hypercore-protocol/corestore-next 30 | 31 | ## Exercise 1 - Using Corestore to make Hypercores 32 | 33 | First install the latest version of Corestore from NPM. Again it's available under the `next` npm tag. The Hypercores this version of Corestore produce are also all the of the latest version (10). If you want to play around with that directly that's also available under the `next` tag on Hypercore. 34 | 35 | ```sh 36 | npm install corestore@next 37 | ``` 38 | 39 | Now make a file called `corestore.js` and insert the following 40 | 41 | ```js 42 | import Corestore from 'corestore' 43 | 44 | const store = new Corestore('./store') 45 | 46 | // You can access cores from the store either by their public key or a local name 47 | const core = store.get({ name: 'my-first-core' }) 48 | 49 | await core.ready() 50 | 51 | console.log('Core public key:', core.key.toString('hex')) 52 | console.log('Core has', core.length, 'entries') 53 | 54 | await core.append(Buffer.from('a block')) 55 | ``` 56 | 57 | 1. Try running the above code a couple of times and see that the length of the core increases 58 | 2. Use `await core.get(index)` to read out a block 59 | 3. Use `sameCore = store.get(Buffer.from('the core key'))` to load the Hypercore from public key. 60 | 61 | ## Exercise 2 - Replicating a Corestore 62 | 63 | Corestores and Hypercores can easily be replicated over Hyperswarm or any other stream based transport. 64 | 65 | Let's try doing that. First you can use this file `seed.js` to easily make a feed that has a decent amount of data in a Hypercore. 66 | 67 | ```js 68 | import Corestore from 'corestore' 69 | import Hyperswarm from 'hyperswarm' 70 | 71 | const store = new Corestore('./seed-store') 72 | const swarm = new Hyperswarm() 73 | 74 | // Setup corestore replication 75 | swarm.on('connection', (connection) => store.replicate(connection)) 76 | 77 | // Load a core by name 78 | const core = store.get({ name: 'seeding-core' }) 79 | 80 | // Make sure the length is loaded 81 | await core.ready() 82 | 83 | // Join the Hypercore discoveryKey (a hash of it's public key) 84 | swarm.join(core.discoveryKey) 85 | 86 | // Insert 10000 blocks 87 | while (core.length < 10000) { 88 | await core.append(Buffer.from('the next block of data. #' + core.length)) 89 | } 90 | 91 | console.log('Core public key is:', core.key.toString('hex')) 92 | ``` 93 | 94 | Then use this scaffolding for the peer as `peer.js` 95 | 96 | ```js 97 | import Corestore from 'corestore' 98 | import Hyperswarm from 'hyperswarm' 99 | 100 | const store = new Corestore('./peer-store') 101 | const swarm = new Hyperswarm() 102 | 103 | // Setup corestore replication 104 | swarm.on('connection', (connection) => store.replicate(connection)) 105 | 106 | // Load a core by public key 107 | const core = store.get(Buffer.from('public-key-from-above', 'hex')) 108 | 109 | await core.ready() 110 | 111 | // Join the Hypercore discoveryKey (a hash of it's public key) 112 | swarm.join(core.discoveryKey) 113 | 114 | // Make sure we have all the connections 115 | await swarm.flush() 116 | 117 | // Make sure we have the latest length 118 | await core.update() 119 | 120 | // Print the length (should print 10000) 121 | console.log('Core length is:', core.length) 122 | ``` 123 | 124 | 1. Run the seed and modify the peer to use the public key from the seed. 125 | 2. Check that it prints the same length. 126 | 3. Modify the peer to get block 1453 as well and print it out 127 | 4. (Optional) Like before, try getting another workshop participant to load your Hypercore 128 | 129 | # Next 130 | 131 | When you are done continue to [Problem 4a](../04a) 132 | -------------------------------------------------------------------------------- /problems/03/seed.js: -------------------------------------------------------------------------------- 1 | import Corestore from 'corestore' 2 | import Hyperswarm from 'hyperswarm' 3 | 4 | const store = new Corestore('./seed-store') 5 | const swarm = new Hyperswarm() 6 | 7 | // Setup corestore replication 8 | swarm.on('connection', (connection) => store.replicate(connection)) 9 | 10 | // Load a core by name 11 | const core = store.get({ name: 'seeding-core' }) 12 | 13 | // Make sure the length is loaded 14 | await core.ready() 15 | 16 | // Join the Hypercore discoveryKey (a hash of it's public key) 17 | swarm.join(core.discoveryKey) 18 | 19 | // Insert 10000 blocks 20 | while (core.length < 10000) { 21 | await core.append(Buffer.from('the next block of data. #' + core.length)) 22 | } 23 | 24 | console.log('Core public key is:', core.key.toString('hex')) 25 | -------------------------------------------------------------------------------- /problems/04a/index.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import ram from 'random-access-memory' 3 | import Corestore from 'corestore' 4 | import Autobase from 'autobase' 5 | 6 | // (1) Ordering Chat Messages 7 | console.log(chalk.green('\n(1) Ordering Chat Messages\n')) 8 | { 9 | // Create two chat users, each with their own Hypercores. 10 | const store = new Corestore(ram) 11 | const userA = store.get({ name: 'userA' }) 12 | const userB = store.get({ name: 'userB' }) 13 | 14 | // Make two Autobases with those two users as inputs. 15 | const baseA = new Autobase({ inputs: [userA, userB], localInput: userA }) 16 | const baseB = new Autobase({ inputs: [userA, userB], localInput: userB }) 17 | 18 | // Append chat messages and read them out again, using the default options. 19 | // This simulates two peers who are always completely up-to-date with each others messages. 20 | await baseA.append('A0: hello!') 21 | await baseB.append('B0: hi! good to hear from you') 22 | await baseA.append('A1: likewise. fun exercise huh?') 23 | await baseB.append('B1: yep. great time.') 24 | 25 | for await (const node of baseA.createCausalStream()) { 26 | console.log(node.value.toString()) 27 | } 28 | } 29 | 30 | // (2) Forks and Reordering 31 | console.log(chalk.green('\n(2) Forks and Reordering\n')) 32 | { 33 | // Create two chat users, each with their own Hypercores. 34 | const store = new Corestore(ram) 35 | const userA = store.get({ name: 'userA' }) 36 | const userB = store.get({ name: 'userB' }) 37 | 38 | // Make two Autobases with those two users as inputs. 39 | const baseA = new Autobase({ inputs: [userA, userB], localInput: userA }) 40 | const baseB = new Autobase({ inputs: [userA, userB], localInput: userB }) 41 | 42 | // Append chat messages and read them out again, manually specifying empty clocks. 43 | // This simulates two peers creating independent forks. 44 | await baseA.append('A0: hello! anybody home?', []) // An empty array as a second argument means "empty clock" 45 | await baseB.append('B0: hello! first one here.', []) 46 | await baseA.append('A1: hmmm. guess not.', []) 47 | await baseB.append('B1: anybody home?', []) 48 | 49 | 50 | console.log(chalk.blue('After A and B each wrote two independent messages:')) 51 | for await (const node of baseA.createCausalStream()) { 52 | console.log(node.value.toString()) 53 | } 54 | 55 | // Add 3 more independent messages to A. Does its fork move to the beginning or the end? 56 | for (let i = 0; i < 3; i++) { 57 | await baseA.append(`A${2 + i}: trying again...`, []) 58 | } 59 | 60 | console.log(chalk.blue('After A wrote 3 more independent messages:')) 61 | for await (const node of baseA.createCausalStream()) { 62 | console.log(node.value.toString()) 63 | } 64 | 65 | // Add 5 more independent messages to B. Does its fork move to the beginning or the end? 66 | for (let i = 0; i < 5; i++) { 67 | await baseB.append(`B${2 + i}: also trying again...`, []) 68 | } 69 | 70 | console.log(chalk.blue('After B wrote 5 more independent messages:')) 71 | for await (const node of baseA.createCausalStream()) { 72 | console.log(node.value.toString()) 73 | } 74 | } 75 | 76 | // (2) Locking Forks in Time 77 | console.log(chalk.green('\n(2) Locking Forks in Time\n')) 78 | { 79 | // Create two chat users, each with their own Hypercores. 80 | const store = new Corestore(ram) 81 | const userA = store.get({ name: 'userA' }) 82 | const userB = store.get({ name: 'userB' }) 83 | 84 | // Make two Autobases with those two users as inputs. 85 | const baseA = new Autobase({ inputs: [userA, userB], localInput: userA }) 86 | const baseB = new Autobase({ inputs: [userA, userB], localInput: userB }) 87 | 88 | // (2) Append chat messages and read them out again, manually specifying empty clocks. 89 | // This simulates two peers creating independent forks. 90 | await baseA.append('A0: hello! anybody home?', []) // An empty array as a second argument means "empty clock" 91 | await baseB.append('B0: hello! first one here.', []) 92 | await baseA.append('A1: hmmm. guess not.', []) 93 | await baseB.append('B1: anybody home?', []) 94 | 95 | 96 | console.log(chalk.blue('After A and B each wrote two independent messages:')) 97 | for await (const node of baseA.createCausalStream()) { 98 | console.log(node.value.toString()) 99 | } 100 | 101 | // Add 3 more independent messages to A. Does its fork move to the beginning or the end? 102 | for (let i = 0; i < 3; i++) { 103 | await baseA.append(`A${2 + i}: trying again...`, []) 104 | } 105 | 106 | console.log(chalk.blue('After A wrote 3 more independent messages:')) 107 | for await (const node of baseA.createCausalStream()) { 108 | console.log(node.value.toString()) 109 | } 110 | 111 | // Add 5 more independent messages to B. Does its fork move to the beginning or the end? 112 | for (let i = 0; i < 5; i++) { 113 | await baseB.append(`B${2 + i}: also trying again...`, []) 114 | } 115 | 116 | console.log(chalk.blue('After B wrote 5 more independent messages:')) 117 | for await (const node of baseA.createCausalStream()) { 118 | console.log(node.value.toString()) 119 | } 120 | 121 | // Resolve the two forks by having B record a message that causally links both forks. 122 | await baseB.append('B7: looks like we\'re both online!') 123 | 124 | console.log(chalk.blue('After B resolved the forks:')) 125 | for await (const node of baseA.createCausalStream()) { 126 | console.log(node.value.toString()) 127 | } 128 | 129 | // Making A and B fork once more 130 | await baseA.append('A5: oops. gone again', []) 131 | await baseB.append('B8: hello?', []) 132 | 133 | console.log(chalk.blue('After A and B forked again:')) 134 | for await (const node of baseA.createCausalStream()) { 135 | console.log(node.value.toString()) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /problems/04a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "dependencies": { 4 | "corestore": "next", 5 | "autobase": "latest", 6 | "chalk": "^4.1.2", 7 | "random-access-memory": "^3.1.2" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /problems/04a/readme.md: -------------------------------------------------------------------------------- 1 | # Autobase 1 - Causal Streams 2 | 3 | Now that you've had a crash course in creating and replicate Hypercores between peers using Hyperswarm, let's dive into our newest feature: multiwriter collaboration with Autobase. 4 | 5 | Autobase is a new module we've introduced alongside Hypercore v10 that allows you to "rebase together" many Hypercores, perhaps from many different people on different machines, into a single, linearized Hypercore (called a "linearized view", which we'll get to later). Importantly, Autobase ensures that anybody can recreate that linearized Hypercore locally -- the ordering is uniquely defined by the set of input Hypercores passed to Autobase -- and we'll get to why that's important in the next exercise. 6 | 7 | For now, let's go through what ordering means in the context of Autobase by building a simple chat system. 8 | 9 | ## Setup 10 | 11 | As with the previous exercises, first create a module with `type: module` and an `index.js` and make sure to install the following dependencies: 12 | ``` 13 | npm i corestore@next autobase random-access-memory chalk 14 | ``` 15 | 16 | ## Creating Autobases 17 | 18 | Say you have two users, each with their own Hypercores, who want to chat with each other by appending messages to their cores. To get set up, we'll create two Hypercores, and then create one Autobase for each user: 19 | ```js 20 | import Hypercore from 'hypercore' 21 | import Autobase from 'autobase' 22 | import ram from 'random-access-memory' 23 | 24 | // Create two chat users, each with their own Hypercores. 25 | // Here since we'll be rerunning the same code a lot, we'll use the ram storage 26 | 27 | const store = new Corestore(ram) 28 | const userA = store.get({ name: 'userA' }) 29 | const userB = store.get({ name: 'userB' }) 30 | 31 | // Make an Autobase with those two users as inputs. 32 | 33 | const baseA = new Autobase({ inputs: [userA, userB], localInput: userA }) 34 | const baseB = new Autobase({ inputs: [userA, userB], localInput: userB }) 35 | ``` 36 | 37 | The Autobase constructor above says "Create an Autobase using `userA` and `userB` as inputs, where my local input is `userA`". The local input will be what's appended to by default in the `append` operations below. 38 | 39 | ## Ordering Chat Messages 40 | 41 | Somehow, each message needs to indicate its context: the messages that the sender had previously seen when they wrote that message, we call this "causal information". Autobase handles this for you automatically. 42 | 43 | Let's have each user write a few chat messages and then read out the complete chat log. We can do this with the `append` and `createCausalStream` methods as follows: 44 | ```js 45 | await baseA.append('A0: hello!') 46 | await baseB.append('B0: hi! good to hear from you') 47 | await baseA.append('A1: likewise. fun exercise huh?') 48 | await baseB.append('B1: yep. great time.') 49 | 50 | // Let's print all messages in causal order 51 | for await (const node of baseA.createCausalStream()) { 52 | console.log(node.value.toString()) 53 | } 54 | ``` 55 | 56 | You should hopefully see that the messages appear in the "correct" order, but reversed. They're reversed because Autobase's causal stream walks backwards, starting at the "head" of each input Hypercore, and yielding messages in causal order. Causal order here means that the N+1th message returned by the causal stream will *never* be causally-dependent on the Nth message. 57 | 58 | Note how the causal stream returns "input nodes" which contain the chat message (in the `value` field) along with additional metadata that's used for ordering. Take a look at the `clock` field, for example. When you do an `append` with default options, Autobase will embed the "latest clock" in the message, meaning that because our Hypercores are local in this example, we're simulating two peers who are connected and completely up-to-date with each other. 59 | 60 | But in the real world, peers come and go, and connectivity can be spotty. Let's make the example more interesting by modifying the causal information that's recorded by `append`. 61 | 62 | ### Exercises 63 | 1. Try printing the `clock` on the causal stream nodes instead of the `value` to get a sense for how Autobase orders messages. 64 | 2. Print the output of `baseB.createCausalStream()`. Is there any difference? 65 | 66 | ## Forks and Reordering 67 | 68 | First, let's start over and re-create `baseA` and `baseB`, so we can start from a fresh state. 69 | 70 | What if the second user writes a new message before observing the first user's latest message? Now we're in a "forked state", where the latest messages for each user are causally independent. 71 | 72 | We can simulate this by forcing `append` to record an empty clock in the input node, which means "this message is not causally-dependent on any other message": 73 | ```js 74 | await baseA.append('A0: hello! anybody home?', []) // An empty array as a second argument means "empty clock" 75 | await baseB.append('B0: hello! first one here.', []) 76 | await baseA.append('A1: hmmm. guess not.', []) 77 | await baseB.append('B1: anybody home?', []) 78 | 79 | for await (const node of baseA.createCausalStream()) { 80 | console.log(node.value.toString()) 81 | } 82 | ``` 83 | 84 | Since we have two independent forks, you should see either A's fork or B's fork yielded first, then the other yielded second. Which one comes first? The causal stream will *always* yield shorter forks before longer ones. We'll show why this is important in the next section (indexing), but in this case both forks have the same length (2). When forks have the same length, the "winner" is decided deterministically by comparing Hypercore keys -- everyone will always see the same ordering. 85 | 86 | ### Exercises 87 | 1. Try appending 3 more messages on `baseA`, all with empty clocks (growing the fork). How does that change the ordering? 88 | 2. Now append 5 more messages to `baseB`, also with empty clocks. What now? 89 | 90 | ## Locking Forks in Time 91 | 92 | Let's say A and B have been on independent forks for a while now, then they finally reconnect and A writes a new chat message that causally links to both forks. Immediately after that, they disconnect again and start forking once more. What happens to the ordering? 93 | 94 | To simulate this, only one additional `append` is necessary. Extend the previous example with the following `append` containing the latest clock: 95 | ```js 96 | // note that this append links the clocks of the previous ones 97 | await baseB.append('B7: looks like we\'re both online!') 98 | ``` 99 | 100 | At this point, the causal stream ordering is completely "locked". Anybody who observes B7, and subsequently creates a causal stream, will see the exact same ordering for all messages before B7. 101 | 102 | To show what we mean, let's make A and B fork one more time: 103 | ```js 104 | await baseA.append('A5: oops. gone again', []) 105 | await baseB.append('B8: hello?', []) 106 | ``` 107 | 108 | The two new forks are at the "tip" of the causal stream, and everything behind B7 remains the same. This property will be extremely useful in the next section, where we show how causal streams can be used to generate indexes over Autobase inputs. 109 | 110 | ## Next Up: Linearized Views 111 | 112 | Now that you've seen how Autobase can generate a deterministic ordering over messages in many input Hypercores, we'll walk through how to make use of that ordering to generate shareable, Hypercore-based views over those inputs. 113 | 114 | Continue to [Problem 4b](../04b) when ready. 115 | -------------------------------------------------------------------------------- /problems/04b/index.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import ram from 'random-access-memory' 3 | import Corestore from 'corestore' 4 | import Autobase from 'autobase' 5 | 6 | // (1) The Simplest Possible Index 7 | console.log(chalk.green('\n(1) The Simplest Possible Index\n')) 8 | { 9 | // Create two chat users, each with their own Hypercores. 10 | const store = new Corestore(ram) 11 | const userA = store.get({ name: 'userA' }) 12 | const userB = store.get({ name: 'userB' }) 13 | 14 | // Make two Autobases with those two users as inputs. 15 | const baseA = new Autobase([userA, userB], { input: userA }) 16 | const baseB = new Autobase([userA, userB], { input: userB }) 17 | 18 | // Append chat messages and read them out again, using the default options. 19 | // This simulates two peers who are always completely up-to-date with each others messages. 20 | await baseA.append('A0: hello!') 21 | await baseB.append('B0: hi! good to hear from you') 22 | await baseA.append('A1: likewise. fun exercise huh?') 23 | await baseB.append('B1: yep. great time.') 24 | 25 | const viewCore = store.get({ name: 'view-core' }) 26 | const view = baseA.linearize(viewCore) 27 | await view.update() 28 | 29 | for (let i = 0; i < view.length; i++) { 30 | const node = await view.get(i) 31 | console.log(node.value.toString()) 32 | } 33 | 34 | // B writes another message 35 | await baseB.append('B2: ok nice chatting') 36 | 37 | // The index needs to be updated again in order to pull in the new changes. 38 | await view.update() 39 | 40 | console.log('\nStatus after the second update:', view.status) 41 | } 42 | 43 | // (2) The Simplest Index, but with Forks 44 | console.log(chalk.green('\n(2) The Simplest Index, but with Forks\n')) 45 | { 46 | // Create two chat users, each with their own Hypercores. 47 | const store = new Corestore(ram) 48 | const userA = store.get({ name: 'userA' }) 49 | const userB = store.get({ name: 'userB' }) 50 | const viewCore = store.get({ name: 'view-core' }) 51 | 52 | // Make two Autobases with those two users as inputs. 53 | const baseA = new Autobase([userA, userB], { input: userA }) 54 | const baseB = new Autobase([userA, userB], { input: userB }) 55 | 56 | // We can use either Autobase to create the index, so well just pick baseA 57 | const view = baseA.linearize(viewCore) 58 | 59 | // Append chat messages and read them out again, manually specifying empty clocks. 60 | // This simulates two peers creating independent forks. 61 | await baseA.append('A0: hello! anybody home?', []) // An empty array as a second argument means "empty clock" 62 | await baseB.append('B0: hello! first one here.', []) 63 | await baseA.append('A1: hmmm. guess not.', []) 64 | await baseB.append('B1: anybody home?', []) 65 | 66 | await view.update() 67 | console.log(chalk.blue('Index status after the first two independent messages:'), view.status) 68 | 69 | // Add 3 more independent messages to A. 70 | for (let i = 0; i < 3; i++) { 71 | await baseA.append(`A${2 + i}: trying again...`, []) 72 | } 73 | 74 | await view.update() 75 | console.log(chalk.blue('Index status after A appends 3 more messages:'), view.status) 76 | 77 | // Add 5 more independent messages to B. Does its fork move to the beginning or the end? 78 | for (let i = 0; i < 5; i++) { 79 | await baseB.append(`B${2 + i}: also trying again...`, []) 80 | } 81 | 82 | await view.update() 83 | console.log(chalk.blue('Index status after B appends 5 more messages:'), view.status) 84 | } 85 | 86 | // (3) A Mapping Indexer 87 | console.log(chalk.green('\n(3) A Mapping Indexer\n')) 88 | { 89 | // Create two chat users, each with their own Hypercores. 90 | const store = new Corestore(ram) 91 | const userA = store.get({ name: 'userA' }) 92 | const userB = store.get({ name: 'userB' }) 93 | 94 | // Make two Autobases with those two users as inputs. 95 | const baseA = new Autobase([userA, userB], { input: userA }) 96 | const baseB = new Autobase([userA, userB], { input: userB }) 97 | 98 | // Append chat messages and read them out again, using the default options. 99 | // This simulates two peers who are always completely up-to-date with each others messages. 100 | await baseA.append('A0: hello!') 101 | await baseB.append('B0: hi! good to hear from you') 102 | await baseA.append('A1: likewise. fun exercise huh?') 103 | await baseB.append('B1: yep. great time.') 104 | 105 | const viewCore = store.get({ name: 'view-core' }) 106 | const view = baseA.linearize(viewCore, { 107 | async apply (batch) { 108 | batch = batch.map(({ value }) => Buffer.from(value.toString().toUpperCase())) 109 | await view.append(batch) 110 | } 111 | }) 112 | await view.update() 113 | 114 | // All the indexed nodes will be uppercased now. 115 | for (let i = 0; i < view.length; i++) { 116 | const node = await view.get(i) 117 | console.log(node.value.toString()) 118 | } 119 | 120 | // Make another index that is stateful, and records the total message count alongside the message text. 121 | const secondViewCore = store.get({ name: 'second-view-core' }) 122 | const secondView = baseA.linearize(secondViewCore, { 123 | async apply (batch) { 124 | let count = 0 125 | 126 | // First, we need to get the latest count from the last node in the view. 127 | if (secondView.length > 0) { 128 | const lastNode = await secondView.get(secondView.length - 1) 129 | const lastRecord = JSON.parse(lastNode.value.toString()) 130 | count = lastRecord.count 131 | } 132 | 133 | // Next, we can record a stringified record that includes both the message and the count for every node in the batch. 134 | batch = batch.map(({ value }, idx) => { 135 | const record = JSON.stringify({ 136 | message: value.toString(), 137 | count: count + idx + 1 138 | }) 139 | return Buffer.from(record) 140 | }) 141 | 142 | // Finally, append it just like before. 143 | await secondView.append(batch) 144 | } 145 | }) 146 | 147 | 148 | // Pull all the changes into the new, stateful index. 149 | await secondView.update() 150 | 151 | console.log(chalk.blue('\nStateful indexing results:\n')) 152 | for (let i = 0; i < secondView.length; i++) { 153 | const node = await secondView.get(i) 154 | console.log(JSON.parse(node.value.toString())) 155 | } 156 | } 157 | 158 | // (4) Sharing Indexes with Others 159 | console.log(chalk.green('\n(1) Sharing Indexes with Others\n')) 160 | { 161 | // Create two chat users, each with their own Hypercores. 162 | const store = new Corestore(ram) 163 | const userA = store.get({ name: 'userA' }) 164 | const userB = store.get({ name: 'userB' }) 165 | 166 | // Make two Autobases with those two users as inputs. 167 | const baseA = new Autobase([userA, userB], { input: userA }) 168 | const baseB = new Autobase([userA, userB], { input: userB }) 169 | 170 | // Append chat messages and read them out again, using the default options. 171 | // This simulates two peers who are always completely up-to-date with each others messages. 172 | await baseA.append('A0: hello!') 173 | await baseB.append('B0: hi! good to hear from you') 174 | await baseA.append('A1: likewise. fun exercise huh?') 175 | await baseB.append('B1: yep. great time.') 176 | 177 | const viewCore = store.get({ name: 'view-core' }) 178 | const view = baseA.linearize(viewCore) 179 | await view.update() 180 | 181 | // Now we will simulate a "reader" who will use the index above as a remote index. 182 | // The reader will not be participating in the chat, but will be reading from the index. 183 | const baseC = new Autobase([userA, userB]) 184 | const readerView = baseC.linearize([viewCore], { 185 | autocommit: false // Ignore this for now 186 | }) 187 | 188 | // This will piggy-back off of the work `viewCore` has already done. 189 | await readerView.update() 190 | 191 | // Since the remote index is fully up-to-date, the reader should not have to do any work. 192 | console.log(chalk.blue('Reader update status (should be zeros):'), readerView.status, '\n') 193 | 194 | for (let i = 0; i < readerView.length; i++) { 195 | const node = await readerView.get(i) 196 | console.log(node.value.toString()) 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /problems/04b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "dependencies": { 4 | "corestore": "next", 5 | "autobase": "latest", 6 | "chalk": "^4.1.2", 7 | "random-access-memory": "^3.1.2" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /problems/04b/readme.md: -------------------------------------------------------------------------------- 1 | # Autobase 2 - Linearized Views 2 | 3 | In the previous exercise, we saw how Autobase can give you a "causal stream" of messages from N input hypercores, and that this causal stream defines a particular kind of deterministic ordering. Most importantly, we saw how the causal stream treats forks, and how the stream ordering grows stable over time as input nodes become "locked" at specific positions. 4 | 5 | But why does Autobase produce a causal stream with these properties? So we can persist the stream into a Hypercore and share it! Before Hypercore 10, it wouldn't have been possible to store any kind of causal stream in a Hypercore, because append-only logs can't be reordered. With Hypercore 10's new `truncate` method, we can shorten a Hypercore to a particular length, and then re-append new blocks. Truncation is still expensive, though, so we want to minimize both how often we truncate, and how large those truncations are -- hence the causal stream's very particular approach to ordering. 6 | 7 | Let's demonstrate what it looks like to persist an Autobase's causal stream into a Hypercore, then go into some of the cool applications this enables. 8 | 9 | For the following bits, we'll build off of the chat system from the previous exercise. 10 | 11 | ## Setup 12 | 13 | As with the previous exercises, first create a module with `type: module` and an `index.js` and make sure to install the following dependencies: 14 | ``` 15 | npm i corestore@next autobase random-access-memory chalk 16 | ``` 17 | 18 | If you like, you can just copy over the code you wrote for the previous exercise. 19 | 20 | ## (1) The Simplest Possible View 21 | 22 | For the first example, we'll start with the very first example from the previous exercise: two fully-connected peers exchanging chat messages. No forks. 23 | 24 | To persist the conversation into a Hypercore, we can use `const view = base.linearize(...)`. This will return a "view", which looks and feels just like a Hypercore. 25 | 26 | Views have an `update` function that can be used to tell the view to process any changes to the inputs that have happened since the last update. 27 | 28 | Try adding this chunk of code to the end of section (1) from the previous exercise: 29 | ```js 30 | const viewCore = store.get({ name: 'view-core' }) 31 | const view = baseA.linearize(viewCore) 32 | await view.update() 33 | 34 | // The block at index 0 is a header block, so we skip over that. 35 | for (let i = 0; i < view.length; i++) { 36 | const node = await view.get(i) 37 | console.log(node.value.toString()) 38 | } 39 | ``` 40 | 41 | ### Exercises 42 | 1. Have `baseB` append another message. What happens to `view` after this? Try seeing what `view.status` says -- it gives stats about what happened to the view during the most recent update. 43 | 44 | ## (2) The Simplest View, but with Forks 45 | 46 | Now we'll see how a reordering of the causal stream affects indexing. Let's revisit the second example from the previous exercise (the one where we create two independent forks). 47 | 48 | Copy the code from that section, but create a linearized index using the approach above. Every time either `baseA` or `baseB` appends new messages, update the index with `await index.update()` and then see how `index.status` changes. 49 | 50 | You'll notice that whenever there's a reordering, the `removed` field is > 0 -- this means that the view Hypercore has been truncated. 51 | 52 | You'll also notice that after A grows by 3, `removed` is 4 and `added` is 7. This is because A and B get reordered (causing the entire view to be truncated), and then A's 3 new messages are added on top. 53 | 54 | ### Exercises 55 | 1. Just try out the example and watch how `view.status` changes in response to causal stream reorderings. 56 | 57 | ## (3) A Mapping 58 | 59 | Even the simple view we made is useful for certain cases -- with chat, for example, often you just want to display a chat log without having to recompute the ordering unnecessarily. 60 | 61 | But `linearize` really shines when it's paired with an `apply` function, which lets you configure exactly what should be recorded in the index in response to a batch of input nodes. 62 | 63 | As an example, let's say we want to apply a map function to the chat messages to convert all the messages to uppercase. We'd do this by adding the following `apply` option to `linearize`: 64 | ```js 65 | const view = base.linearize(indexCore, { 66 | async apply (batch) { 67 | batch = batch.map(({ value }) => Buffer.from(value.toString().toUpperCase())) 68 | await view.append(batch) 69 | } 70 | }) 71 | ``` 72 | 73 | Note that you can modify the `view` directly with `append`, which is just like a normal Hypercore `append`, but you can only do this inside of the `apply` function! 74 | 75 | The second exercise for this section involves making a stateful view -- an `apply` function that records cumulative information about all the input nodes that have been processed so far. It's definitely trickier, but really highlights the value of these derived views. If you were to share this view with others, they could check the message count by downloading a single block, the latest one! 76 | 77 | We extend this concept a lot further in the next exercise -- the rabbit hole goes deeper. 78 | 79 | ### Exercises 80 | 1. Add that `apply` function to `linearize` and see how the output Hypercore changes as a result. 81 | 2. __HARD__: Make the map function stateful, such that it includes the total number of messages sent by either A or B in the blocks it records. You can call `view.get` from inside the `apply` function. 82 | * For this exercise, feel free to jump straight to the solution if you get stuck. 83 | 84 | ## (4) Sharing Views with Others 85 | 86 | In the previous sections we've seen how linearized views can be used in combination with the `apply` function to make shareable data structures that are generated on the fly. However for readers accessing our data structures it's a bad user experience to have to regenerate the complete view themselves - especially as the Autobase's input Hypercores grow longer and longer over time. We'd prefer a near instant experience like we are used to from most centralised systems, without needing to wait for a long pre-processing step. Luckily, Autobase's "remote views" solve this for us. 87 | 88 | By passing Hypercores representing other peers' views to `linearize`, Autobase will piggy-back on those remote views, only applying the minimal changes necessary to bring the remote view up-to-date, instead of re-processing every block from start to finish. Using these remote views massively improves the reader-side experience. Say the participants in our chat want to share the chat log with millions of readers -- with remote views, those readers will have very little (if any) view-generation work to do locally, and they can make use of Hypercore's other cool features, like bandwidth sharing and sparse syncing. 89 | 90 | ### Exercises 91 | Let's extend the very first example in this section with a second view that treats the first view as a remote one. 92 | 93 | `linearize` behaves differently depending on whether the Hypercores it's given are readable (meaning coming from remote peers) or writable (local Hypercores). If you give it an Array of readable Hypercores, those will be treated as remote views. If the Hypercores are writable, they will be treated as local views, and the complete causal stream will be rebased into them. 94 | 95 | Copy over your code for (1), and add another index as follows: 96 | ```js 97 | const baseC = new Autobase([userA, userB]) 98 | const readerView = baseC.linearize([viewCore], { 99 | autocommit: false // Ignore this for now 100 | }) 101 | 102 | // This will piggy-back off of the work `viewCore` has already done. 103 | await readerView.update() 104 | ``` 105 | 106 | The `autocommit` flag is only necessary because we are simulating a remote peer locally, so `viewCore` is writable. We explicitly tell `linearize` to treat `viewCore` as a remote index with that flag. 107 | 108 | Notice how the `status` shows that the update didn't add or remove any new nodes. This means that the reader has detected that `viewCore` is completely up-to-date, and so no additional processing is necessary. 109 | 110 | ## (5) Sparsely Downloading Views 111 | 112 | Views are just Hypercores, and so they share all of Hypercore's nice properties. One particular cool feature is the ability to "sparsely download" blocks on-demand. In the next exercise, we're going to use the approach from the previous exercise to build a Hyperbee, our Hypercore-based [B-Tree](https://en.wikipedia.org/wiki/B-tree) implemention -- with Hyperbee, you can store KV-pairs, and then perform range queries over keys, while only touching a subset of the blocks in the underlying Hypercore. 113 | 114 | Imagine you have an Autobase with many inputs, and there's an indexer that's been doing the heavy lifting, digesting the inputs into a Hyperbee view. If a reader adds that view as a remote view, then they can immediately start querying the Hyperbee, only downloading blocks as needed, without needing to do any additional work! 115 | 116 | The ability to share and sparsely sync views makes Autobase a lot more useful, because it allows readers to sidestep as much indexing/downloading as possible, so long as there are existing views available on the network. 117 | 118 | ## Up Next: A Complete Example 119 | 120 | Now all the pieces are in place to build a useful application on top of Autobase and Hyperswarm. 121 | 122 | Continue on to [Problem 5](../05) to start building your CLI-based Reddit clone! 123 | -------------------------------------------------------------------------------- /problems/05/hypernews.js: -------------------------------------------------------------------------------- 1 | import minimist from 'minimist' 2 | import Corestore from 'corestore' 3 | import Hyperswarm from 'hyperswarm' 4 | import Autobase from 'autobase' 5 | import Hyperbee from 'hyperbee' 6 | import crypto from 'crypto' 7 | import lexint from 'lexicographic-integer' 8 | import ram from 'random-access-memory' 9 | 10 | const args = minimist(process.argv, { 11 | alias: { 12 | writers: 'w', 13 | indexes: 'i', 14 | storage: 's', 15 | name: 'n' 16 | }, 17 | default: { 18 | swarm: true 19 | }, 20 | boolean: ['ram', 'swarm'] 21 | }) 22 | 23 | class Hypernews { 24 | constructor () { 25 | this.store = new Corestore(args.ram ? ram : (args.storage || 'hypernews')) 26 | this.swarm = null 27 | this.autobase = null 28 | this.bee = null 29 | this.name = null 30 | } 31 | 32 | async start () { 33 | const writer = this.store.get({ name: 'writer' }) 34 | const viewOutput = this.store.get({ name: 'view' }) 35 | 36 | await writer.ready() 37 | 38 | this.name = args.name || writer.key.slice(0, 8).toString('hex') 39 | this.autobase = new Autobase([writer], { outputs: viewOutput }) 40 | 41 | for (const w of [].concat(args.writers || [])) { 42 | await this.autobase.addInput(this.store.get(Buffer.from(w, 'hex'))) 43 | } 44 | 45 | for (const i of [].concat(args.indexes || [])) { 46 | await this.autobase.addDefaultOutput(this.store.get(Buffer.from(i, 'hex'))) 47 | } 48 | 49 | await this.autobase.ready() 50 | 51 | if (args.swarm) { 52 | const topic = Buffer.from(sha256(this.name), 'hex') 53 | this.swarm = new Hyperswarm() 54 | this.swarm.on('connection', (socket) => this.store.replicate(socket)) 55 | this.swarm.join(topic) 56 | await this.swarm.flush() 57 | process.once('SIGINT', () => this.swarm.destroy()) // for faster restarts 58 | } 59 | 60 | this.info() 61 | 62 | const self = this 63 | const view = this.autobase.linearize({ 64 | unwrap: true, 65 | async apply (batch) { 66 | const b = self.bee.batch({ update: false }) 67 | 68 | for (const { value } of batch) { 69 | const op = JSON.parse(value) 70 | 71 | if (op.type === 'post') { 72 | const hash = sha256(op.data) 73 | await b.put('posts!' + hash, { hash, votes: 0, data: op.data }) 74 | } 75 | 76 | } 77 | 78 | await b.flush() 79 | } 80 | }) 81 | 82 | this.bee = new Hyperbee(view, { 83 | extension: false, 84 | keyEncoding: 'utf-8', 85 | valueEncoding: 'json' 86 | }) 87 | } 88 | 89 | info () { 90 | console.log('Autobase setup. Pass this to run this same setup in another instance:') 91 | console.log() 92 | console.log('hrepl hypernews.js ' + 93 | '-n ' + this.name + ' ' + 94 | this.autobase.inputs.map(i => '-w ' + i.key.toString('hex')).join(' ') + ' ' + 95 | this.autobase.defaultOutputs.map(i => '-i ' + i.key.toString('hex')).join(' ') 96 | ) 97 | console.log() 98 | console.log('To use another storage directory use --storage ./another') 99 | console.log('To disable swarming add --no-swarm') 100 | console.log() 101 | } 102 | 103 | async * all () { 104 | for await (const data of this.bee.createReadStream({ gt: 'posts!', lt: 'posts!~' })) { 105 | yield data.value 106 | } 107 | } 108 | 109 | } 110 | 111 | export const hypernews = new Hypernews() 112 | 113 | await hypernews.start() 114 | 115 | function sha256 (inp) { 116 | return crypto.createHash('sha256').update(inp).digest('hex') 117 | } 118 | -------------------------------------------------------------------------------- /problems/05/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "dependencies": { 4 | "autobase": "latest", 5 | "corestore": "next", 6 | "hyperbee": "^1.6.3", 7 | "hyperswarm": "next", 8 | "lexicographic-integer": "^1.1.0", 9 | "minimist": "^1.2.5", 10 | "random-access-memory": "^3.1.2" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /problems/05/readme.md: -------------------------------------------------------------------------------- 1 | # Creating Hypernews Posts 2 | 3 | 4 | Now that we've understood Hyperswarm and Autobase we're going to combine 5 | Autobase and Hyperswarm together to create a distributed command-line application 6 | which will, over the next few exercises, allow for posting, upvoting, downvoting 7 | and viewing the top 10 highest voted posts. 8 | 9 | ## Introducing Hyperbee 10 | 11 | In addition to Hyperswarm and Autobase we'll also be using Hyperbee. 12 | 13 | Hyperbee provides a key-value store abstraction over the top of a Hypercore, 14 | since an Autobase view is a Hypercore we can pass it into a Hyperbee 15 | 16 | 17 | ## Utilizing `hrepl` 18 | 19 | A REPL (Read-Eval-Print-Loop) such as the one provided when running `node` without 20 | a file argument can be useful for experimentation and prototyping. The [`hrepl`](https://github.com/davidmarkclements/hrepl) tool 21 | provides a REPL with some advanced functionality: 22 | 23 | * It exposes the exports of a given file within the REPL 24 | * It provides an easy logging command for iterable objects, which will be useful 25 | 26 | Install `hrepl` with the following command: 27 | 28 | ```sh 29 | % npm install -g hrepl 30 | ``` 31 | 32 | ## Starting `hypernews.js` in `hrepl` 33 | 34 | This folder contains a `hypernews.js` file, which is the starting point for this exercise. 35 | 36 | Currently `hypernews.js` starts a swarm node and creates an autobase database. It exposes 37 | an `all` method `start` method and an `info` method. The `info` method is called immediately 38 | and outputs information that includes a command that can be given to someone else to create a peer 39 | connection. The `start` method is likewise called immediately and initializes the Autobase 40 | and Hyperswarm instances. 41 | 42 | The `all` method returns an iterator that supplies the posted entries stored in the autobase 43 | instance. Currently there are no entries, so it supplies no entries. 44 | 45 | To start `hypernews.js` in `hrepl` run the following command: 46 | 47 | ``` 48 | % hrepl hypernews.js 49 | ``` 50 | 51 | This should output something like the following: 52 | 53 | 54 | ``` 55 | Autobase setup. Pass this to run this same setup in another instance: 56 | 57 | hrepl hypernews.js -n ff184becdab95cb0 -w ff184becdab95cb0b6f45910a25070967d446689fbcf51f4e3df97baeea2d718 -i 863acd316fbf151d18746782d195fba341b0399be1ba1bea5d00a794897b8480 58 | 59 | To use another storage directory use --storage ./another 60 | To disable swarming add --no-swarm 61 | 62 | API: hypernews (object) 63 | > 64 | ``` 65 | 66 | We can interact with the `hypernews` object, for example, if we we're to execute the following in `hrepl`: 67 | 68 | ```sh 69 | > hypernews.info() 70 | ``` 71 | 72 | This will output the info again. 73 | 74 | An `hrepl` command starts with a dot and may take an argument after a space, the `.help` command 75 | will display all commands, the `.exit` command will exit the REPL and the `.log` command can 76 | perform iterative logging. For example, run the following in `hrepl`: 77 | 78 | ```sh 79 | > .log [1, 2, 3] 80 | ``` 81 | 82 | This should result in the following: 83 | 84 | ``` 85 | > .log [1,2,3] 86 | try { for await (const data of [1,2,3]) console.log(data) } catch { console.log([1,2,3]) } 87 | 1 88 | 2 89 | 3 90 | ``` 91 | 92 | The `.log` command is a shorthand for the command printed at the top of its output. 93 | It takes the input and then attempts to loop over it with a `for await` loop 94 | (this works with both iterables and async-iterables), failing that it just passes the input to 95 | `console.log`. 96 | 97 | This will become more useful when logging async-iterables, which is what the `all` method returns. 98 | 99 | We can try to log out all of the posts but there will be nothing to output: 100 | 101 | ```sh 102 | > .log hypernews.all() 103 | try { for await (const data of [1,2,3]) console.log(data) } catch { console.log([1,2,3]) } 104 | ``` 105 | 106 | There's nothing to output because there are no posts. There are no posts becuase there's currently 107 | no way to add posts. 108 | 109 | ## Excercise - Implement `hypernews.post()` 110 | 111 | Create a `post` method on the `Hypernews` class that appends a log to `autobase`. 112 | 113 | The `post` method should be an `async` method and take a single argument (`text`). 114 | 115 | The appended message should be a serialized JSON object with the following fields: 116 | 117 | * `type` - with value `'post'` 118 | * `hash` - a sha256 of the text (the `sha256` function is provided in `hypernews.js`) 119 | * `data` - the `text` argument passed to `post` 120 | 121 | A log can be appended within a async method of the `Hypernews` class with: 122 | 123 | ```js 124 | await this.autobase.append() 125 | ``` 126 | 127 | Once implemented, it should be callable within `hrepl`: 128 | 129 | 130 | ```sh 131 | % hrepl hypernews.js interactive 132 | Autobase setup. Pass this to run this same setup in another instance: 133 | 134 | hrepl hypernews.js -n 83502500e6312a30 -w 83502500e6312a3015730b4d7020af6428825af5d02a32cb0b8992d46991a475 -i 469c49cae805f778843206ebcce7e83b5eedd354b4759f74d1241336ece33582 135 | 136 | To use another storage directory use --storage ./another 137 | To disable swarming add --no-swarm 138 | 139 | API: hypernews (object) 140 | > await hypernews.post('hello world') 141 | > .log hypernews.all() 142 | try { for await (const data of hypernews.all()) console.log(data) } catch { console.log(hypernews.all()) } 143 | { 144 | hash: 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9', 145 | votes: 0, 146 | data: 'hello world' 147 | } 148 | ``` 149 | 150 | # Next 151 | 152 | When you are done continue to [Problem 6](../06) 153 | -------------------------------------------------------------------------------- /problems/06/hypernews.js: -------------------------------------------------------------------------------- 1 | import minimist from 'minimist' 2 | import Corestore from 'corestore' 3 | import Hyperswarm from 'hyperswarm' 4 | import Autobase from 'autobase' 5 | import Hyperbee from 'hyperbee' 6 | import crypto from 'crypto' 7 | import ram from 'random-access-memory' 8 | 9 | const args = minimist(process.argv, { 10 | alias: { 11 | writers: 'w', 12 | indexes: 'i', 13 | storage: 's', 14 | name: 'n' 15 | }, 16 | default: { 17 | swarm: true 18 | }, 19 | boolean: ['ram', 'swarm'] 20 | }) 21 | 22 | class Hypernews { 23 | constructor () { 24 | this.store = new Corestore(args.ram ? ram : (args.storage || 'hypernews')) 25 | this.swarm = null 26 | this.autobase = null 27 | this.bee = null 28 | this.name = null 29 | } 30 | 31 | async start () { 32 | const writer = this.store.get({ name: 'writer' }) 33 | const viewOutput = this.store.get({ name: 'view-output' }) 34 | 35 | await writer.ready() 36 | 37 | this.name = args.name || writer.key.slice(0, 8).toString('hex') 38 | this.autobase = new Autobase([writer], { outputs: viewOutput }) 39 | 40 | for (const w of [].concat(args.writers || [])) { 41 | await this.autobase.addInput(this.store.get(Buffer.from(w, 'hex'))) 42 | } 43 | 44 | for (const i of [].concat(args.indexes || [])) { 45 | await this.autobase.addDefaultOutput(this.store.get(Buffer.from(i, 'hex'))) 46 | } 47 | 48 | await this.autobase.ready() 49 | 50 | if (args.swarm) { 51 | const topic = Buffer.from(sha256(this.name), 'hex') 52 | this.swarm = new Hyperswarm() 53 | this.swarm.on('connection', (socket) => this.store.replicate(socket)) 54 | this.swarm.join(topic) 55 | await this.swarm.flush() 56 | process.once('SIGINT', () => this.swarm.destroy()) // for faster restarts 57 | } 58 | 59 | this.info() 60 | 61 | const self = this 62 | const view = this.autobase.linearize({ 63 | unwrap: true, 64 | async apply (batch) { 65 | const b = self.bee.batch({ update: false }) 66 | 67 | for (const { value } of batch) { 68 | const op = JSON.parse(value) 69 | 70 | if (op.type === 'post') { 71 | const hash = sha256(op.data) 72 | await b.put('posts!' + hash, { hash, votes: 0, data: op.data }) 73 | } 74 | } 75 | 76 | await b.flush() 77 | } 78 | }) 79 | 80 | this.bee = new Hyperbee(view, { 81 | extension: false, 82 | keyEncoding: 'utf-8', 83 | valueEncoding: 'json' 84 | }) 85 | } 86 | 87 | info () { 88 | console.log('Autobase setup. Pass this to run this same setup in another instance:') 89 | console.log() 90 | console.log('hrepl hypernews.js ' + 91 | '-n ' + this.name + ' ' + 92 | this.autobase.inputs.map(i => '-w ' + i.key.toString('hex')).join(' ') + ' ' + 93 | this.autobase.defaultOutputs.map(i => '-i ' + i.key.toString('hex')).join(' ') 94 | ) 95 | console.log() 96 | console.log('To use another storage directory use --storage ./another') 97 | console.log('To disable swarming add --no-swarm') 98 | console.log() 99 | } 100 | 101 | async * all () { 102 | for await (const data of this.bee.createReadStream({ gt: 'posts!', lt: 'posts!~' })) { 103 | yield data.value 104 | } 105 | } 106 | 107 | async post (text) { 108 | const hash = sha256(text) 109 | 110 | await this.autobase.append(JSON.stringify({ 111 | type: 'post', 112 | hash, 113 | data: text 114 | })) 115 | } 116 | 117 | } 118 | 119 | export const hypernews = new Hypernews() 120 | 121 | await hypernews.start() 122 | 123 | function sha256 (inp) { 124 | return crypto.createHash('sha256').update(inp).digest('hex') 125 | } 126 | -------------------------------------------------------------------------------- /problems/06/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "dependencies": { 4 | "autobase": "latest", 5 | "corestore": "next", 6 | "hyperbee": "^1.6.3", 7 | "hyperswarm": "next", 8 | "lexicographic-integer": "^1.1.0", 9 | "minimist": "^1.2.5", 10 | "random-access-memory": "^3.1.2" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /problems/06/readme.md: -------------------------------------------------------------------------------- 1 | # Creating Hypernews Voting 2 | 3 | In this exercise we'll create a voting system for our Hypernews posts. 4 | 5 | ## Persistent, indexed, materialized views 6 | 7 | Within the `start` method of the `Hypernews` class, an Autobase view is created by invoking 8 | `this.autobase.linearize()`. The options object passed to that function contains an 9 | `apply` function - see [hypernews.js#L65-80](hypernews.js#L65-80). 10 | 11 | The `autobase.linearize()` creates a materialized view over the input hypercores, 12 | `apply` configures how that view is persisted. 13 | 14 | The `apply` function isn't called when entries are writen to `autobase`, instead it's called on read 15 | (for instance, when the `hypernews.all()` function is called, it calls `bee.createReadStream`). Once 16 | an entry has been processed by apply, it will not be processed again (i.e. it's idempotent). 17 | 18 | 19 | Currently the `apply` function looks like this: 20 | 21 | ```js 22 | async apply (batch) { 23 | const b = self.bee.batch({ update: false }) 24 | 25 | for (const { value } of batch) { 26 | const op = JSON.parse(value) 27 | 28 | if (op.type === 'post') { 29 | const hash = sha256(op.data) 30 | await b.put('posts!' + hash, { hash, votes: 0, data: op.data }) 31 | } 32 | 33 | } 34 | 35 | await b.flush() 36 | } 37 | ``` 38 | 39 | The `view` itself is actually the storage core for the `Hyperbee` instance (the p2p key value store - `bee`). 40 | On top of that, the `apply` function writes what could be considered "views" back into 41 | the `bee`. The `b` constant here is a batching object of the `bee`, it allows for many ops in the `for of` loop 42 | to be sent at one time when `b.flush()` is called. The reason `{ update: false }` is passed to `self.bee.batch` 43 | when creating the `b` constant, is it stops the `apply` function from being called when the puts are written 44 | (otherwise you could end up in infinite recursion). 45 | 46 | 47 | ## Excercise - Implement voting 48 | 49 | The `apply` function passed to `autobase.linearize()` currently supports one type of operation: `'post'`. 50 | 51 | Extend the `apply` function to meet the following criteria: 52 | 53 | * Modify the `apply` function to support another type: `op.type === 'vote'`. 54 | * For vote ops, use `op.hash` to get a particular post entry from the bee: `await bee.get('posts! + op.hash, { update: false })` 55 | * If the result isn't found, bail out (`continue` from the loop) 56 | * Check `op.up` to see whether to upvote or downvote an item. 57 | * Increment or decrement the votes amount on a post accordingly 58 | * Add a put to the `b` batch instance for that post, passing the updated votes (and other values) to it: `await b.put('posts!' + op.hash, )` 59 | 60 | Once this has been completed we can add the `upvote` and `downvote` methods to the `Hypernews` class. 61 | 62 | They're very similar to the `post` method, except they take a `hash` (instead of `text`), the `type` is `'vote'` (which we look for in the `apply` function), and instead of a `data` property there's an `up` property: 63 | 64 | ```js 65 | async upvote (hash) { 66 | await this.autobase.append(JSON.stringify({ 67 | type: 'vote', 68 | hash, 69 | up: true 70 | })) 71 | } 72 | 73 | async downvote (hash) { 74 | await this.autobase.append(JSON.stringify({ 75 | type: 'vote', 76 | hash, 77 | up: false 78 | })) 79 | } 80 | ``` 81 | 82 | Once the methods are added to `Hypernews` we should be able to check everything is working using `hrepl`: 83 | 84 | ```sh 85 | % hrepl hypernews.js 86 | Autobase setup. Pass this to run this same setup in another instance: 87 | 88 | hrepl hypernews.js -n bc9560e19f971939 -w bc9560e19f971939f2c3b917fc1399e8aea948f118187d81b93d709be92ef722 -i ead863d693da7794840a7871a64cacbaa98dfaa7a02852355afbf23b48bd9cc9 89 | 90 | To use another storage directory use --storage ./another 91 | To disable swarming add --no-swarm 92 | 93 | API: hypernews (object) 94 | > await hypernews.post('hello world') 95 | > await hypernews.post('sup earth') 96 | > .log hypernews.all() 97 | try { for await (const data of hypernews.all()) console.log(data) } catch { console.log(hypernews.all()) } 98 | { 99 | hash: 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9', 100 | votes: 0, 101 | data: 'hello world' 102 | } 103 | { 104 | hash: 'ef72c47db2a417b486c04a1b823eec2f95f2e3d373395b3bbdf80cbaf0a8aed5', 105 | votes: 0, 106 | data: 'sup earth' 107 | } 108 | > await hypernews.upvote('ef72c47db2a417b486c04a1b823eec2f95f2e3d373395b3bbdf80cbaf0a8aed5') 109 | > .log hypernews.all() 110 | try { for await (const data of hypernews.all()) console.log(data) } catch { console.log(hypernews.all()) } 111 | { 112 | hash: 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9', 113 | votes: 0, 114 | data: 'hello world' 115 | } 116 | { 117 | hash: 'ef72c47db2a417b486c04a1b823eec2f95f2e3d373395b3bbdf80cbaf0a8aed5', 118 | votes: 1, 119 | data: 'sup earth' 120 | } 121 | ``` 122 | 123 | # Next 124 | 125 | When you are done continue to [Problem 7](../07) 126 | -------------------------------------------------------------------------------- /problems/07/hypernews.js: -------------------------------------------------------------------------------- 1 | import minimist from 'minimist' 2 | import Corestore from 'corestore' 3 | import Hyperswarm from 'hyperswarm' 4 | import Autobase from 'autobase' 5 | import Hyperbee from 'hyperbee' 6 | import crypto from 'crypto' 7 | import lexint from 'lexicographic-integer' 8 | import ram from 'random-access-memory' 9 | 10 | const args = minimist(process.argv, { 11 | alias: { 12 | writers: 'w', 13 | indexes: 'i', 14 | storage: 's', 15 | name: 'n' 16 | }, 17 | default: { 18 | swarm: true 19 | }, 20 | boolean: ['ram', 'swarm'] 21 | }) 22 | 23 | class Hypernews { 24 | constructor () { 25 | this.store = new Corestore(args.ram ? ram : (args.storage || 'hypernews')) 26 | this.swarm = null 27 | this.autobase = null 28 | this.bee = null 29 | this.name = null 30 | } 31 | 32 | async start () { 33 | const writer = this.store.get({ name: 'writer' }) 34 | const viewOutput = this.store.get({ name: 'view' }) 35 | 36 | await writer.ready() 37 | 38 | this.name = args.name || writer.key.slice(0, 8).toString('hex') 39 | this.autobase = new Autobase([writer], { outputs: viewOutput }) 40 | 41 | for (const w of [].concat(args.writers || [])) { 42 | await this.autobase.addInput(this.store.get(Buffer.from(w, 'hex'))) 43 | } 44 | 45 | for (const i of [].concat(args.indexes || [])) { 46 | await this.autobase.addDefaultOutput(this.store.get(Buffer.from(i, 'hex'))) 47 | } 48 | 49 | await this.autobase.ready() 50 | 51 | if (args.swarm) { 52 | const topic = Buffer.from(sha256(this.name), 'hex') 53 | this.swarm = new Hyperswarm() 54 | this.swarm.on('connection', (socket) => this.store.replicate(socket)) 55 | this.swarm.join(topic) 56 | await this.swarm.flush() 57 | process.once('SIGINT', () => this.swarm.destroy()) // for faster restarts 58 | } 59 | 60 | this.info() 61 | 62 | const self = this 63 | const view = this.autobase.linearize({ 64 | unwrap: true, 65 | async apply (batch) { 66 | const b = self.bee.batch({ update: false }) 67 | 68 | for (const { value } of batch) { 69 | const op = JSON.parse(value) 70 | 71 | if (op.type === 'post') { 72 | const hash = sha256(op.data) 73 | await b.put('posts!' + hash, { hash, votes: 0, data: op.data }) 74 | } 75 | 76 | if (op.type === 'vote') { 77 | const inc = op.up ? 1 : -1 78 | const p = await self.bee.get('posts!' + op.hash, { update: false }) 79 | 80 | if (!p) continue 81 | 82 | p.value.votes += inc 83 | await b.put('posts!' + op.hash, p.value) 84 | } 85 | } 86 | 87 | await b.flush() 88 | } 89 | }) 90 | 91 | this.bee = new Hyperbee(view, { 92 | extension: false, 93 | keyEncoding: 'utf-8', 94 | valueEncoding: 'json' 95 | }) 96 | } 97 | 98 | info () { 99 | console.log('Autobase setup. Pass this to run this same setup in another instance:') 100 | console.log() 101 | console.log('hrepl hypernews.js ' + 102 | '-n ' + this.name + ' ' + 103 | this.autobase.inputs.map(i => '-w ' + i.key.toString('hex')).join(' ') + ' ' + 104 | this.autobase.defaultOutputs.map(i => '-i ' + i.key.toString('hex')).join(' ') 105 | ) 106 | console.log() 107 | console.log('To use another storage directory use --storage ./another') 108 | console.log('To disable swarming add --no-swarm') 109 | console.log() 110 | } 111 | 112 | async * all () { 113 | for await (const data of this.bee.createReadStream({ gt: 'posts!', lt: 'posts!~' })) { 114 | yield data.value 115 | } 116 | } 117 | 118 | 119 | async post (text) { 120 | const hash = sha256(text) 121 | 122 | await this.autobase.append(JSON.stringify({ 123 | type: 'post', 124 | hash, 125 | data: text 126 | })) 127 | } 128 | 129 | async upvote (hash) { 130 | await this.autobase.append(JSON.stringify({ 131 | type: 'vote', 132 | hash, 133 | up: true 134 | })) 135 | } 136 | 137 | async downvote (hash) { 138 | await this.autobase.append(JSON.stringify({ 139 | type: 'vote', 140 | hash, 141 | up: false 142 | })) 143 | } 144 | } 145 | 146 | export const hypernews = new Hypernews() 147 | 148 | await hypernews.start() 149 | 150 | function sha256 (inp) { 151 | return crypto.createHash('sha256').update(inp).digest('hex') 152 | } 153 | -------------------------------------------------------------------------------- /problems/07/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "dependencies": { 4 | "autobase": "latest", 5 | "corestore": "next", 6 | "hyperbee": "^1.6.3", 7 | "hyperswarm": "next", 8 | "lexicographic-integer": "^1.1.0", 9 | "minimist": "^1.2.5", 10 | "random-access-memory": "^3.1.2" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /problems/07/readme.md: -------------------------------------------------------------------------------- 1 | # Creating Hypernews Top Posts 2 | 3 | In this exercise we'll finalize the Hypernews app with functionality that 4 | allows ordering of posts by votes. 5 | 6 | ## Lexicographical Ordering 7 | 8 | Lexicographic essentially means alphabetical, the key point is that numbers are treated 9 | as strings. So for instance, lexicographically `10` comes before `2`. 10 | 11 | All keys are currently prefixed with `post!`. The `all` function creates a read stream 12 | from the `bee` key-value store of all keys greater than `post!` and less than `post!~`. 13 | Since all hashes are hexadecimal, regardless of the hash size, it can only contain 14 | characters from 0 to F - the tilde (`~`) is lexiographically greater than F. So this 15 | results in a read stream that provides all keys prefixed with `post!`. 16 | 17 | A new key prefix can be introduced to create ("persistently materialize") a list of 18 | posts ordered by total votes. We'll call it `top!`. 19 | 20 | Combining this prefix with a lexicographical representation of the total count key, 21 | means keys will automatically be ordered. 22 | 23 | The [`lexicographic-integer` module](https://github.com/substack/lexicographic-integer) 24 | can be used to convert the vote counts into a hexadecimal representation that will be 25 | ordered per the highest amount. 26 | 27 | This is imported in `hypernews.js` as `lexint`. 28 | 29 | A `top!` key should be created like so: `'top!' + lexint.pack(, 'hex') + '!' + `. 30 | 31 | 32 | ## Excercise - Implement Top Posts 33 | 34 | The `apply` function passed to `autobase.linearize()` needs to be modified once more, 35 | and a new `top` function needs to be added to the `Hypernews` class. 36 | 37 | The `top` function should look like so: 38 | 39 | ```js 40 | async * top () { 41 | for await (const data of this.bee.createReadStream({ gt: 'top!', lt: 'top!~', reverse: true })) { 42 | const { value } = (await this.bee.get('posts!' + data.value)) 43 | yield value 44 | } 45 | } 46 | ``` 47 | 48 | This method will not work until the `apply` function has been modified, but it can help to 49 | create this method and then think about what needs to be added to the `apply` function to support it. 50 | 51 | Similar to the `all` function it is an async function generator that yields out entries by iterating over 52 | a read stream as it supplies all entries between `top!` and `top!~`. However this time it's in reverse, 53 | because the entries with the highest votes will have the highest lexicographical value. 54 | 55 | The resulting `data` object of each iteration of the read stream contains a `value` property which 56 | should be the hash of a post entry, so this can be concatenated to the `posts!` prefix in order 57 | to fetch that actual post entry from the `bee`. The resulting object has a `value` property which is 58 | destructured and yielded out. 59 | 60 | Now modify the `apply` function passed to `autobase.linearize()` to meet the following criteria: 61 | 62 | * In addition to a `posts!` put for ops with `type` of `'post'`, create another put to a key `'top!' + lexint.pack(0, 'hex') + '!' + hash`. The 0 is for zero votes, which is the initial value of vote. The value of this put should be the hash. 63 | * For ops with a `type` of `'vote'`, *before* updating the vote count remove any existing `top!` prefixed key: `await b.del('top!' + lexint.pack(, 'hex') + '!' + op.hash)` 64 | * After increasing the vote and adding a `post!` put, add one more `top!` put which uses `lexint.pack` to encode the new vote amount for that entry. 65 | 66 | Once the `top` function has been added and the criteria is met, it should be possible to use `hypernews.top()` in `hrepl`: 67 | 68 | ```sh 69 | % hrepl hypernews.js interactive 70 | Autobase setup. Pass this to run this same setup in another instance: 71 | 72 | hrepl hypernews.js -n 0ef649a8f74234d8 -w 0ef649a8f74234d8898043e5376a269d6f27d980ca86d8a00093d76f57341d18 -i 3388ba1d9a37a96fc8f2ab25725b73b168632769aac771ae4cf34f3ed0d18790 73 | 74 | To use another storage directory use --storage ./another 75 | To disable swarming add --no-swarm 76 | 77 | API: hypernews (object) 78 | > await hypernews.post('hello world') 79 | > await hypernews.post('sup earth') 80 | > .log hypernews.all() 81 | try { for await (const data of hypernews.all()) console.log(data) } catch { console.log(hypernews.all()) } 82 | { 83 | hash: 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9', 84 | votes: 0, 85 | data: 'hello world' 86 | } 87 | { 88 | hash: 'ef72c47db2a417b486c04a1b823eec2f95f2e3d373395b3bbdf80cbaf0a8aed5', 89 | votes: 0, 90 | data: 'sup earth' 91 | } 92 | > await hypernews.upvote('ef72c47db2a417b486c04a1b823eec2f95f2e3d373395b3bbdf80cbaf0a8aed5') 93 | > .log hypernews.top() 94 | try { for await (const data of hypernews.top()) console.log(data) } catch { console.log(hypernews.top()) } 95 | { 96 | hash: 'ef72c47db2a417b486c04a1b823eec2f95f2e3d373395b3bbdf80cbaf0a8aed5', 97 | votes: 1, 98 | data: 'sup earth' 99 | } 100 | { 101 | hash: 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9', 102 | votes: 0, 103 | data: 'hello world' 104 | } 105 | ``` 106 | 107 | That's it for the workshop! 108 | 109 | We encourage you to continue tinkering with the application here. Maybe add an index showing who upvoted and who downvoted? 110 | 111 | If you are interested in learning more and keeping up with the ecosystem, join our discord at https://chat.hypercore-protocol.org 112 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Building Collaborative P2P Applications with Autobase 2 | 3 | Welcome to our workshop on building collaborative, peer-to-peer applications! We're the team behind the [Hypercore Protocol](https://hypercore-protocol.org), a suite of Node.js modules for building data-intensive P2P applications. 4 | 5 | For those of you already familiar with Hypercore, we'll be revisiting some of the basics through a new lens, and introducing a number of major changes and improvements in our v10 release (which as of today, is released as an [alpha release](https://github.com/hypercore-protocol/hypercore-next)). If you've never heard of Hypercore, or are unfamiliar with P2P software, we hope this workshop gives you a few "aha" moments -- it's exciting to build software without servers! 6 | 7 | For this workshop we'll be focusing almost entirely on our newest feature: support for multiwriter collaboration. Gearing up for this major release involved some serious revamping of our core modules, so we'll be covering all those changes on the way to the final exercise: a collaborative, CLI-based, ultra-minimal-but-illustrative Reddit clone. 8 | 9 | When you are ready start with [Problem 1](https://github.com/hypercore-protocol/p2p-multiwriter-with-autobase/tree/main/problems/01) which gives an intro to Hyperswarm v3, our P2P networking stack. 10 | 11 | If you want to keep up the Hypercore ecosystem we suggest you to join our Discord at https://chat.hypercore-protocol.org 12 | -------------------------------------------------------------------------------- /solutions/05/hypernews.js: -------------------------------------------------------------------------------- 1 | import minimist from 'minimist' 2 | import Corestore from 'corestore' 3 | import Hyperswarm from 'hyperswarm' 4 | import Autobase from 'autobase' 5 | import Hyperbee from 'hyperbee' 6 | import crypto from 'crypto' 7 | import lexint from 'lexicographic-integer' 8 | import ram from 'random-access-memory' 9 | 10 | const args = minimist(process.argv, { 11 | alias: { 12 | writers: 'w', 13 | indexes: 'i', 14 | storage: 's', 15 | name: 'n' 16 | }, 17 | default: { 18 | swarm: true 19 | }, 20 | boolean: ['ram', 'swarm'] 21 | }) 22 | 23 | class Hypernews { 24 | constructor () { 25 | this.store = new Corestore(args.ram ? ram : (args.storage || 'hypernews')) 26 | this.swarm = null 27 | this.autobase = null 28 | this.bee = null 29 | this.name = null 30 | } 31 | 32 | async start () { 33 | const writer = this.store.get({ name: 'writer' }) 34 | const autobaseIndex = this.store.get({ name: 'index' }) 35 | 36 | await writer.ready() 37 | 38 | this.name = args.name || writer.key.slice(0, 8).toString('hex') 39 | this.autobase = new Autobase([writer], { indexes: autobaseIndex }) 40 | 41 | for (const w of [].concat(args.writers || [])) { 42 | await this.autobase.addInput(this.store.get(Buffer.from(w, 'hex'))) 43 | } 44 | 45 | for (const i of [].concat(args.indexes || [])) { 46 | await this.autobase.addDefaultIndex(this.store.get(Buffer.from(i, 'hex'))) 47 | } 48 | 49 | await this.autobase.ready() 50 | 51 | if (args.swarm) { 52 | const topic = Buffer.from(sha256(this.name), 'hex') 53 | this.swarm = new Hyperswarm() 54 | this.swarm.on('connection', (socket) => this.store.replicate(socket)) 55 | this.swarm.join(topic) 56 | await this.swarm.flush() 57 | process.once('SIGINT', () => this.swarm.destroy()) // for faster restarts 58 | } 59 | 60 | this.info() 61 | 62 | const self = this 63 | const view = this.autobase.linearize({ 64 | unwrap: true, 65 | async apply (batch) { 66 | const b = self.bee.batch({ update: false }) 67 | 68 | for (const { value } of batch) { 69 | const op = JSON.parse(value) 70 | 71 | if (op.type === 'post') { 72 | const hash = sha256(op.data) 73 | await b.put('posts!' + hash, { hash, votes: 0, data: op.data }) 74 | } 75 | 76 | } 77 | 78 | await b.flush() 79 | } 80 | }) 81 | 82 | this.bee = new Hyperbee(view, { 83 | extension: false, 84 | keyEncoding: 'utf-8', 85 | valueEncoding: 'json' 86 | }) 87 | } 88 | 89 | info () { 90 | console.log('Autobase setup. Pass this to run this same setup in another instance:') 91 | console.log() 92 | console.log('hrepl hypernews.js ' + 93 | '-n ' + this.name + ' ' + 94 | this.autobase.inputs.map(i => '-w ' + i.key.toString('hex')).join(' ') + ' ' + 95 | this.autobase.defaultOutputs.map(i => '-i ' + i.key.toString('hex')).join(' ') 96 | ) 97 | console.log() 98 | console.log('To use another storage directory use --storage ./another') 99 | console.log('To disable swarming add --no-swarm') 100 | console.log() 101 | } 102 | 103 | async * all () { 104 | for await (const data of this.bee.createReadStream({ gt: 'posts!', lt: 'posts!~' })) { 105 | yield data.value 106 | } 107 | } 108 | 109 | async post (text) { 110 | const hash = sha256(text) 111 | 112 | await this.autobase.append(JSON.stringify({ 113 | type: 'post', 114 | hash, 115 | data: text 116 | })) 117 | } 118 | } 119 | 120 | export const hypernews = new Hypernews() 121 | 122 | await hypernews.start() 123 | 124 | function sha256 (inp) { 125 | return crypto.createHash('sha256').update(inp).digest('hex') 126 | } 127 | -------------------------------------------------------------------------------- /solutions/06/hypernews.js: -------------------------------------------------------------------------------- 1 | import minimist from 'minimist' 2 | import Corestore from 'corestore' 3 | import Hyperswarm from 'hyperswarm' 4 | import Autobase from 'autobase' 5 | import Hyperbee from 'hyperbee' 6 | import crypto from 'crypto' 7 | import ram from 'random-access-memory' 8 | 9 | const args = minimist(process.argv, { 10 | alias: { 11 | writers: 'w', 12 | indexes: 'i', 13 | storage: 's', 14 | name: 'n' 15 | }, 16 | default: { 17 | swarm: true 18 | }, 19 | boolean: ['ram', 'swarm'] 20 | }) 21 | 22 | class Hypernews { 23 | constructor () { 24 | this.store = new Corestore(args.ram ? ram : (args.storage || 'hypernews')) 25 | this.swarm = null 26 | this.autobase = null 27 | this.bee = null 28 | this.name = null 29 | } 30 | 31 | async start () { 32 | const writer = this.store.get({ name: 'writer' }) 33 | const autobaseIndex = this.store.get({ name: 'index' }) 34 | 35 | await writer.ready() 36 | 37 | this.name = args.name || writer.key.slice(0, 8).toString('hex') 38 | this.autobase = new Autobase([writer], { indexes: autobaseIndex }) 39 | 40 | for (const w of [].concat(args.writers || [])) { 41 | await this.autobase.addInput(this.store.get(Buffer.from(w, 'hex'))) 42 | } 43 | 44 | for (const i of [].concat(args.indexes || [])) { 45 | await this.autobase.addDefaultIndex(this.store.get(Buffer.from(i, 'hex'))) 46 | } 47 | 48 | await this.autobase.ready() 49 | 50 | if (args.swarm) { 51 | const topic = Buffer.from(sha256(this.name), 'hex') 52 | this.swarm = new Hyperswarm() 53 | this.swarm.on('connection', (socket) => this.store.replicate(socket)) 54 | this.swarm.join(topic) 55 | await this.swarm.flush() 56 | process.once('SIGINT', () => this.swarm.destroy()) // for faster restarts 57 | } 58 | 59 | this.info() 60 | 61 | const self = this 62 | const view = this.autobase.linearize({ 63 | unwrap: true, 64 | async apply (batch) { 65 | const b = self.bee.batch({ update: false }) 66 | 67 | for (const { value } of batch) { 68 | const op = JSON.parse(value) 69 | 70 | if (op.type === 'post') { 71 | const hash = sha256(op.data) 72 | await b.put('posts!' + hash, { hash, votes: 0, data: op.data }) 73 | } 74 | 75 | if (op.type === 'vote') { 76 | const inc = op.up ? 1 : -1 77 | const p = await self.bee.get('posts!' + op.hash, { update: false }) 78 | 79 | if (!p) continue 80 | 81 | p.value.votes += inc 82 | await b.put('posts!' + op.hash, p.value) 83 | } 84 | } 85 | 86 | await b.flush() 87 | } 88 | }) 89 | 90 | this.bee = new Hyperbee(view, { 91 | extension: false, 92 | keyEncoding: 'utf-8', 93 | valueEncoding: 'json' 94 | }) 95 | } 96 | 97 | info () { 98 | console.log('Autobase setup. Pass this to run this same setup in another instance:') 99 | console.log() 100 | console.log('hrepl hypernews.js ' + 101 | '-n ' + this.name + ' ' + 102 | this.autobase.inputs.map(i => '-w ' + i.key.toString('hex')).join(' ') + ' ' + 103 | this.autobase.defaultOutputs.map(i => '-i ' + i.key.toString('hex')).join(' ') 104 | ) 105 | console.log() 106 | console.log('To use another storage directory use --storage ./another') 107 | console.log('To disable swarming add --no-swarm') 108 | console.log() 109 | } 110 | 111 | async * all () { 112 | for await (const data of this.bee.createReadStream({ gt: 'posts!', lt: 'posts!~' })) { 113 | yield data.value 114 | } 115 | } 116 | 117 | async post (text) { 118 | const hash = sha256(text) 119 | 120 | await this.autobase.append(JSON.stringify({ 121 | type: 'post', 122 | hash, 123 | data: text 124 | })) 125 | } 126 | 127 | async upvote (hash) { 128 | await this.autobase.append(JSON.stringify({ 129 | type: 'vote', 130 | hash, 131 | up: true 132 | })) 133 | } 134 | 135 | async downvote (hash) { 136 | await this.autobase.append(JSON.stringify({ 137 | type: 'vote', 138 | hash, 139 | up: false 140 | })) 141 | } 142 | } 143 | 144 | export const hypernews = new Hypernews() 145 | 146 | await hypernews.start() 147 | 148 | function sha256 (inp) { 149 | return crypto.createHash('sha256').update(inp).digest('hex') 150 | } 151 | -------------------------------------------------------------------------------- /solutions/07/hypernews.js: -------------------------------------------------------------------------------- 1 | import minimist from 'minimist' 2 | import Corestore from 'corestore' 3 | import Hyperswarm from 'hyperswarm' 4 | import Autobase from 'autobase' 5 | import Hyperbee from 'hyperbee' 6 | import crypto from 'crypto' 7 | import lexint from 'lexicographic-integer' 8 | import ram from 'random-access-memory' 9 | 10 | const args = minimist(process.argv, { 11 | alias: { 12 | inputs: 'i', 13 | outputs: 'o', 14 | storage: 's', 15 | name: 'n' 16 | }, 17 | default: { 18 | swarm: true 19 | }, 20 | boolean: ['ram', 'swarm'] 21 | }) 22 | 23 | class Hypernews { 24 | constructor () { 25 | this.store = new Corestore(args.ram ? ram : (args.storage || 'hypernews')) 26 | this.swarm = null 27 | this.autobase = null 28 | this.bee = null 29 | this.name = null 30 | } 31 | 32 | async start () { 33 | const writer = this.store.get({ name: 'writer' }) 34 | const viewOutput = this.store.get({ name: 'view-output' }) 35 | 36 | await writer.ready() 37 | 38 | this.name = args.name || writer.key.slice(0, 8).toString('hex') 39 | this.autobase = new Autobase({ 40 | inputs: [writer], 41 | localInput: writer, 42 | outputs: [viewOutput] 43 | }) 44 | 45 | for (const i of [].concat(args.inputs || [])) { 46 | await this.autobase.addInput(this.store.get(Buffer.from(i, 'hex'))) 47 | } 48 | 49 | for (const o of [].concat(args.outputs || [])) { 50 | await this.autobase.addOutput(this.store.get(Buffer.from(o, 'hex'))) 51 | } 52 | 53 | await this.autobase.ready() 54 | 55 | if (args.swarm) { 56 | const topic = Buffer.from(sha256(this.name), 'hex') 57 | this.swarm = new Hyperswarm() 58 | this.swarm.on('connection', (socket) => this.store.replicate(socket)) 59 | this.swarm.join(topic) 60 | await this.swarm.flush() 61 | process.once('SIGINT', () => this.swarm.destroy()) // for faster restarts 62 | } 63 | 64 | this.info() 65 | 66 | this.autobase.start({ 67 | unwrap: true, 68 | async apply (bee, batch) { 69 | const b = bee.batch({ update: false }) 70 | 71 | for (const { value } of batch) { 72 | const op = JSON.parse(value) 73 | 74 | if (op.type === 'post') { 75 | const hash = sha256(op.data) 76 | await b.put('posts!' + hash, { hash, votes: 0, data: op.data }) 77 | await b.put('top!' + lexint.pack(0, 'hex') + '!' + hash, hash) 78 | } 79 | 80 | if (op.type === 'vote') { 81 | const inc = op.up ? 1 : -1 82 | const p = await bee.get('posts!' + op.hash, { update: false }) 83 | 84 | if (!p) continue 85 | 86 | await b.del('top!' + lexint.pack(p.value.votes, 'hex') + '!' + op.hash) 87 | p.value.votes += inc 88 | await b.put('posts!' + op.hash, p.value) 89 | await b.put('top!' + lexint.pack(p.value.votes, 'hex') + '!' + op.hash, op.hash) 90 | } 91 | } 92 | 93 | await b.flush() 94 | }, 95 | view (core) { 96 | return new Hyperbee(core.unwrap(), { // .unwrap() might become redundant if https://github.com/holepunchto/autobase/pull/33 gets merged 97 | extension: false, 98 | keyEncoding: 'utf-8', 99 | valueEncoding: 'json' 100 | }) 101 | } 102 | }) 103 | 104 | this.bee = this.autobase.view 105 | } 106 | 107 | info () { 108 | let localInputHex = this.autobase.localInput.key.toString('hex') 109 | console.log('Autobase setup. Use this to run an additional instance with the current one as an input:') 110 | console.log() 111 | console.log('hrepl hypernews.js ' + 112 | '-n ' + this.name + ' ' + 113 | this.autobase.inputs.map(i => '-i ' + i.key.toString('hex')).join(' ') + ' ' + 114 | this.autobase.outputs.map(o => '-o ' + o.key.toString('hex')).join(' ') + 115 | ' --storage ./instanceN' 116 | ) 117 | console.log() 118 | console.log('To disable swarming add --no-swarm') 119 | console.log() 120 | console.log('Note: for the first instance to accept updates of the second instance, it needs to have the second instance as an input.') 121 | console.log('This can be achieved by starting it with the -i ' + localInputHex + ' option') 122 | console.log('or at runtime with: hypernews.autobase.addInput(hypernews.store.get(Buffer.from(\'' + localInputHex + '\', \'hex\')))') 123 | console.log() 124 | } 125 | 126 | 127 | async * all () { 128 | for await (const data of this.bee.createReadStream({ gt: 'posts!', lt: 'posts!~' })) { 129 | yield data.value 130 | } 131 | } 132 | 133 | async * top () { 134 | for await (const data of this.bee.createReadStream({ gt: 'top!', lt: 'top!~', reverse: true })) { 135 | const { value } = (await this.bee.get('posts!' + data.value)) 136 | yield value 137 | } 138 | } 139 | 140 | async post (text) { 141 | const hash = sha256(text) 142 | 143 | await this.autobase.append(JSON.stringify({ 144 | type: 'post', 145 | hash, 146 | data: text 147 | })) 148 | } 149 | 150 | async upvote (hash) { 151 | await this.autobase.append(JSON.stringify({ 152 | type: 'vote', 153 | hash, 154 | up: true 155 | })) 156 | } 157 | 158 | async downvote (hash) { 159 | await this.autobase.append(JSON.stringify({ 160 | type: 'vote', 161 | hash, 162 | up: false 163 | })) 164 | } 165 | } 166 | 167 | export const hypernews = new Hypernews() 168 | 169 | await hypernews.start() 170 | 171 | function sha256 (inp) { 172 | return crypto.createHash('sha256').update(inp).digest('hex') 173 | } 174 | -------------------------------------------------------------------------------- /solutions/07/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "dependencies": { 4 | "autobase": "github:holepunchto/autobase#1c3833d27e", 5 | "corestore": "^6.4.1", 6 | "hrepl": "^1.1.3", 7 | "hyperbee": "^2.3.0", 8 | "hyperswarm": "^4.3.6", 9 | "lexicographic-integer": "^1.1.0", 10 | "minimist": "^1.2.5", 11 | "random-access-memory": "^6.1.0" 12 | } 13 | } 14 | --------------------------------------------------------------------------------