Work with many Hypercores using Corestore
Manage and co-replicate many named cores from one Corestore.
An append-only log like Hypercore is powerful on its own, but it's most useful as a building-block for constructing larger data structures, such as databases or filesystems. Building these data structures often requires many cores, each with different responsibilities. For example, Hyperdrive uses one core to store file metadata and another to store file contents.
Corestore is a Hypercore factory that makes it easier to manage large collections of named Hypercores. This guide demonstrates a pattern often in use: co-replicating many cores using Corestore, where several 'internal cores' are linked to from a primary core. Only the primary core is announced on the swarm — the keys for the others are recorded inside that core.
In Replicate and persist with Hypercore, only a single Hypercore instance was replicated. But in this guide, you'll replicate a single Corestore instance, which will internally manage the replication of a collection of Hypercores. You'll achieve this with two Pear Terminal Applications: multicore-writer-app and multicore-reader-app.
Use one Corestore instance per application. Multiple Corestores over the same storage cause file-locking errors and duplicate core storage. A single Corestore:
- Reduces open file handles.
- Reduces storage by deduping Hypercore storage.
- Requires only one replication stream per peer connection.
- Simplifies referring to Hypercores by name.
If named cores collide across components, namespace them (store.namespace('a')) — retrieving cores by key is unaffected by namespacing.
Create the multicore writer app
Create the multicore writer app directory and add dependencies
Create the multicore-writer-app project with these commands:
mkdir multicore-writer-app
cd multicore-writer-app
npm init -y
npm pkg set type="module"
npm install bare-process corestore hyperswarm b4aThis will install the following dependencies:
bare-process: A module for working with processes.corestore: A module for working with Corestore.hyperswarm: A module for working with Hyperswarm.b4a: A module for working with buffers.
Add the multicore writer app logic
Create the multicore-writer-app/index.js file with the following content:
The multicore-writer-app uses a Corestore instance to create three Hypercores, which are then replicated with other peers using Hyperswarm:
- One Corestore and one Hyperswarm back the whole app (L7–L8).
core1bootstraps the system — its first block records the keys ofcore2andcore3(L22–L26), so a reader only needscore1's key to discover the rest. Writing the bootstrap list before announcing on the swarm ensurescore1.get(0)resolves as soon as a reader connects. - The three named cores are created up front (L13–L15); names map to local key pairs and are never sent to readers.
- Only
core1's discovery key is announced on the swarm (L32) —core2andcore3ride along becausestore.replicate(conn)replicates every loaded core over a single stream (L36). - The main core key is logged so it can be passed to the
multicore-reader-app(L28). - Terminal input is routed by length: short messages append to
core2, long ones tocore3(L39–L46).
import Hyperswarm from 'hyperswarm'
import Corestore from 'corestore'
import b4a from 'b4a'
import process from 'bare-process'
const store = new Corestore('./multicore-writer-storage')
const swarm = new Hyperswarm()
process.once('SIGINT', () => swarm.destroy().then(() => process.exit(0)))
// A name is a purely-local, and maps to a key pair. It's not visible to readers.
// Since a name always corresponds to a key pair, these are all writable
const core1 = store.get({ name: 'core-1', valueEncoding: 'json' })
const core2 = store.get({ name: 'core-2' })
const core3 = store.get({ name: 'core-3' })
await Promise.all([core1.ready(), core2.ready(), core3.ready()])
// Since Corestore does not exchange keys, they need to be exchanged elsewhere.
// Here, we'll record the other keys in the first block of core1. Do this
// *before* announcing on the swarm so that as soon as a reader connects,
// `core1.get(0)` resolves and the bootstrap key list is available.
if (core1.length === 0) {
await core1.append({
otherKeys: [core2, core3].map((core) => b4a.toString(core.key, 'hex'))
})
}
console.log('main core key:', b4a.toString(core1.key, 'hex'))
// Here we'll only join the swarm with the core1's discovery key
// We don't need to announce core2 and core3, because they'll be replicated with core1
swarm.join(core1.discoveryKey)
// Corestore replication internally manages to replicate every loaded core
// Corestore *does not* exchange keys (read capabilities) during replication.
swarm.on('connection', (conn) => store.replicate(conn))
// Record all short messages in core2, and all long ones in core3
process.stdin.on('data', (data) => {
if (data.length < 5) {
console.log('appending short data to core2')
core2.append(data)
} else {
console.log('appending long data to core3')
core3.append(data)
}
})Create the multicore reader app
The multicore-reader-app connects to the previous peer with Hyperswarm and replicates the local Corestore instance to receive the data from it. This requires the copied key to be supplied as an argument when executing the file, which will then be used to create a core with the same public key as the other peer (i.e., the same discovery key for both the reader and writer peers).
Create the multicore reader app directory and add dependencies
Create the multicore-reader-app project with these commands:
mkdir multicore-reader-app
cd multicore-reader-app
npm init -y
npm pkg set type="module"
npm install corestore hyperswarm b4a bare-processThis will install the following dependencies:
bare-process: A module for working with processes.corestore: A module for working with Corestore.hyperswarm: A module for working with Hyperswarm.b4a: A module for working with buffers.
Add the multicore reader app logic
Create the multicore-reader-app/index.js file with the following content. The reader takes the writer's main core key as a command-line argument (L6–L8) and opens its own Corestore (L10–L11). On each connection it replicates the whole store (L17), then gets core1 from the supplied key and joins the swarm on its discovery key (L20–L25). After core.update(), an empty core means the writer was never reached (L28–L32). It reads the bootstrap key list from the first block (L35) and, for every key, gets the corresponding core and logs each new block as it is appended (L36–L46):
import process from 'bare-process'
import Corestore from 'corestore'
import Hyperswarm from 'hyperswarm'
import b4a from 'b4a'
if (!Bare.argv[2]) throw new Error('provide a key')
const key = b4a.from(Bare.argv[2], 'hex')
const store = new Corestore('./multicore-reader-storage')
await store.ready()
const swarm = new Hyperswarm()
process.once('SIGINT', () => swarm.destroy().then(() => process.exit(0)))
// replication of corestore instance on every connection
swarm.on('connection', (conn) => store.replicate(conn))
// creation/getting of a hypercore instance using the key passed
const core = store.get({ key, valueEncoding: 'json' })
// wait till all the properties of the hypercore instance are initialized
await core.ready()
swarm.join(core.discoveryKey)
await swarm.flush()
// update the meta-data of the hypercore instance
await core.update()
if (core.length === 0) {
throw new Error('Could not connect to the writer peer')
}
// getting cores using the keys stored in the first block of main core
const { otherKeys } = await core.get(0)
for (const key of otherKeys) {
const core = store.get({ key: b4a.from(key, 'hex') })
// on every append to the hypercore,
// download the latest block of the core and log it to the console
core.on('append', () => {
const seq = core.length - 1
core.get(seq).then(block => {
console.log(`Block ${seq} in Core ${key}: ${block}`)
})
})
}Run the writer and reader
Run the multicore writer app
In one terminal, run multicore-writer-app with bare.
bare multicore-writer-appThe multicore-writer-app will output the main core key.
Run the multicore reader app
In another terminal, open the multicore-reader-app and pass it the key:
bare multicore-reader-app <SUPPLY THE KEY HERE>As inputs are made to the terminal running the writer application, outputs should be shown in the terminal running the reader application.
See also
- Share append-only databases with Hyperbee — key/value store on top of Hypercore.
- Create a full peer-to-peer filesystem with Hyperdrive — filesystem on top of two Hypercores.
- Host multiple rooms in one chat app — this co-replication pattern applied in a full app, where one Corestore backs many rooms.
- Corestore reference — full API for the store factory, namespacing, and replication surface used here.
- Hypercore reference — full API for the individual append-only log each Corestore session manages.