key-collection
Version:
Hyperdb-based collection of 32-byte keys
153 lines (124 loc) • 5 kB
JavaScript
const os = require('os')
const path = require('path')
const fsProm = require('fs/promises')
const goodbye = require('graceful-goodbye')
const IdEnc = require('hypercore-id-encoding')
const yaml = require('yaml')
const { command, flag, arg, description } = require('paparam')
const Corestore = require('corestore')
const Hyperswarm = require('hyperswarm')
const rrp = require('resolve-reject-promise')
const KeyCollection = require('.')
const DEFAULT_STORAGE = path.join(os.homedir(), '.key-collection-data', 'corestore')
const DEFAULT_MIN_PEERS = 2
const logger = console
const sync = command('sync',
description('Sync a key collection you manage with a local yaml file, then swarm it'),
arg('<location>', 'YAML file location to sync against'),
flag('--name|-n [name]', 'Namespace to use (defaults to the name of the yaml file, without the extension'),
flag('--storage|-s [path]', `storage path, defaults to ${DEFAULT_STORAGE}`),
async function ({ args, flags }) {
const location = path.resolve(args.location)
const name = flags.name || path.parse(location).name
const storage = path.resolve(flags.storage || DEFAULT_STORAGE)
let swarm = null // Created after we verified all the rest is ok, to avoid needless swarming
logger.info(`Using core with name '${name}' in storage at ${storage}`)
logger.info(`Parsing desired state from ${location}`)
const store = new Corestore(storage)
const core = store.get({ name })
const keyColl = new KeyCollection(core)
goodbye(async () => {
logger.info('\nShutting down...')
if (swarm) await swarm.destroy()
await store.close()
})
const desiredState = await parseYaml(location)
logger.info('Syncing...')
await keyColl.sync(desiredState)
logger.info('Successfully synced')
logger.info('\nCurrent state:')
for (const [key, { name }] of await keyColl.toMap()) {
logger.info(`${key} -> ${name}`)
}
swarm = new Hyperswarm()
swarm.on('connection', conn => {
logger.info('Swarm opened connection')
store.replicate(conn)
conn.on('close', () => { logger.info('Swarm closed connection') })
})
swarm.join(keyColl.discoveryKey, { client: true, server: true })
logger.info(`\nSwarming the database on public key: ${IdEnc.normalize(keyColl.key)} (ctrl-c to stop)`)
}
)
const list = command('list',
description('List all entries of a key collection'),
arg('<key>', 'Key of the collection to list'),
flag('--storage|-s [path]', `storage path, defaults to ${DEFAULT_STORAGE}`),
flag('--min-peers|-m [minPeers]', `Minimum peers to connect to before assuming the list is up to date. Defaults to ${DEFAULT_MIN_PEERS}`),
async function ({ args, flags }) {
const key = IdEnc.decode(args.key)
const storage = path.resolve(flags.storage || DEFAULT_STORAGE)
const minPeers = parseInt(flags.minPeers || DEFAULT_MIN_PEERS)
logger.info(`Using storage at ${storage}`)
const store = new Corestore(storage)
const core = store.get({ key })
const keyColl = new KeyCollection(core)
await keyColl.ready()
const swarm = new Hyperswarm()
const { resolve, reject, promise } = rrp()
const timeoutMs = 5000
const checkMs = 250
const errTimeout = setTimeout(
() => reject(new Error(`Could not connect to at least ${minPeers} peers in ${timeoutMs}ms. Is this collection properly seeded?`)),
timeoutMs
)
const checkInterval = setInterval(() => {
if (core.peers.length >= minPeers) resolve()
}, checkMs)
let shuttingDown = false
swarm.on('connection', conn => {
logger.info('Swarm opened connection')
store.replicate(conn)
conn.on('close', () => {
if (shuttingDown) return
logger.info('Swarm closed connection')
})
})
goodbye(async () => {
clearTimeout(errTimeout)
clearInterval(checkInterval)
shuttingDown = true
logger.info('\nShutting down...')
await swarm.destroy()
await store.close()
})
logger.info('Swarming to get latest state...')
swarm.join(keyColl.discoveryKey, { client: true, server: false })
try {
await promise
} catch (e) {
console.error(e.message)
goodbye.exit()
return
}
logger.info('Gossipping length updates...')
await new Promise(resolve => setTimeout(resolve, 1000))
logger.info(`\nCollection (at length ${core.length}):`)
for (const [key, { name }] of await keyColl.toMap()) {
logger.info(`${key} -> ${name}`)
}
goodbye.exit()
}
)
async function parseYaml (location) {
const content = await fsProm.readFile(location, { encoding: 'utf-8' })
const rawDesiredState = await yaml.parse(content)
const res = new Map()
for (const [key, value] of Object.entries(rawDesiredState)) {
res.set(KeyCollection.normKey(key), { name: value?.name || '' })
}
return res
}
const cmd = command('key-collection', sync, list)
cmd.parse()