whalesong
Version:
Distributed container registry built on hypercores and IPFS
176 lines (147 loc) • 5.13 kB
JavaScript
import fs from 'fs/promises'
import path from 'path'
import { createRequire } from 'module'
import IPFSCtl from 'ipfsd-ctl'
import Debug from 'debug'
import Hyperbee from 'hyperbee'
import { Client as HyperspaceClient, Server as HyperspaceServer } from 'hyperspace'
import { baseDir } from './utils.js'
const debug = Debug('whalesong:clients')
const clients = new Map()
export async function setup () {
// TODO: init hyperbees and ipfs in parallell for speedup.
await setupIpfsClient()
await setupHyperspaceServer()
await setupHyperspaceClient()
}
export async function shutdown () {
debug('shutting down ipfsd')
await clients.get('ipfsd')?.stop()
debug('shutting down hyperspace corestore')
await clients.get('corestore')?.close()
debug('shutting down hyperspace client')
await clients.get('hyperspace-client')?.close()
debug('shutting down hyperspace server')
await clients.get('hyperspace-server')?.stop()
debug('shutdown finished')
}
async function setupIpfsClient () {
const require = createRequire(import.meta.url)
const ipfsd = await IPFSCtl.createController({
ipfsHttpModule: require('ipfs-http-client'),
ipfsBin: require('go-ipfs').path(),
disposable: false,
forceKillTimeout: 10000,
ipfsOptions: {
repo: path.join(await baseDir(), 'ipfs'),
config: {
Addresses: {
Gateway: '' // disable gateway since it is quite common for people to have stuff listening on port 8080.
}
}
}
})
debug(`ipfsd initialized?: ${ipfsd.initialized}`)
if (!ipfsd.initialized) {
await ipfsd.init()
debug(`initialized ipfsd: ${ipfsd.initialized}`)
}
try {
debug(`ipfsd started?: ${ipfsd.started}`)
if (!ipfsd.started) {
await ipfsd.start()
debug(`started ipfsd: ${ipfsd.started}`)
}
const id = await ipfsd.api.id()
debug('IPFS daemon initialized, it has id %j', id)
} catch (e) {
// On an unclean shutdown, IPFS may refuse to start.
// Removing the api file in the ipfs repo seems to solve it, heh.
// (alternatively, you can run "IPFS_PATH=~/.whalesong/ipfs ipfs id" which solves it)
if (e.message.includes('ECONNREFUSED')) {
// try to remove api file and then try starting again. If that fails too we just give up.
const apiFilePath = path.join(ipfsd.path, 'api')
debug('removing ipfs api file path since we couldnt start on first try. deleting %s', apiFilePath)
await fs.unlink(apiFilePath)
await ipfsd.start()
debug(`started ipfsd: ${ipfsd.started}`)
const id = await ipfsd.api.id()
debug('IPFS daemon initialized, it has id %j', id)
} else {
throw e
}
}
clients.set('ipfsd', ipfsd)
// TODO: add sigint/sigterm handler here to gracefully shut down?
}
async function setupHyperspaceServer () {
const server = new HyperspaceServer({
storage: path.join(await baseDir(), 'hyperspace'),
host: 'whalesong-hyperspace'
})
await server.ready()
server.on('client-open', () => {
console.log('A HyperspaceClient has connected')
})
server.on('client-close', () => {
console.log('A HyperspaceClient has disconnected')
})
clients.set('hyperspace-server', server)
}
async function setupHyperspaceClient () {
const client = new HyperspaceClient({
host: 'whalesong-hyperspace'
})
await client.ready()
debug('hyperspace client is now ready')
clients.set('hyperspace-client', client)
clients.set('corestore', client.corestore('whalesong'))
}
async function setupHyperbeeClient (feedOrPubKey, shouldReplicate) {
const corestore = clients.get('corestore')
let core
if (feedOrPubKey === 'settings') {
core = corestore.default()
} else if (feedOrPubKey === null) {
core = corestore.get() // creates a new hypercore
} else if (feedOrPubKey.match(/^[0-9a-fA-F]{64}$/)) {
core = corestore.get(feedOrPubKey)
} else {
debug(`invalid feed or pubkey given, got: ${feedOrPubKey}`)
throw new Error(`invalid feed or pubkey given, got: ${feedOrPubKey}`)
}
core.on('peer-add', (peer) => {
debug('Started replicating feed %s with new peer: %s/%s', feedOrPubKey, peer.remoteAddress, peer.type)
})
core.on('peer-remove', (peer) => {
debug('Stopped replicating feed %s with peer: %s/%s', feedOrPubKey, peer.remoteAddress, peer.type)
})
const bee = new Hyperbee(core, {
keyEncoding: 'utf-8',
valueEncoding: 'json'
})
// start replicating core.
if (shouldReplicate) {
await clients.get('hyperspace-client').replicate(bee.feed)
debug('Started replicating feed')
}
await bee.ready()
const key = bee.feed.key.toString('hex')
debug('Created hyperbee client with feed %s. feed writable: %s', key, bee.feed.writable)
return {
key,
bee
}
}
export function getIpfsClient () {
return clients.get('ipfsd').api
}
export async function getHyperbee (pubKey) {
return setupHyperbeeClient(pubKey, true)
}
export async function getSettingsHyperbee () {
return setupHyperbeeClient('settings', false)
}
export async function getNewHyperbee () {
return setupHyperbeeClient(null, true)
}