UNPKG

dat-sdk

Version:
349 lines (284 loc) 8.98 kB
const path = require('path') // This is a dirty hack for browserify to work. 😅 if (!path.posix) path.posix = path const SwarmNetworker = require('@corestore/networker') const RAA = require('random-access-application') const DatEncoding = require('dat-encoding') const crypto = require('hypercore-crypto') const RAM = require('random-access-memory') const HypercoreProtocol = require('hypercore-protocol') const datDNS = require('dat-dns') const makeHyperdrive = require('hyperdrive') const Corestore = require('corestore') const makeHypercore = require('hypercore') const makeHypercorePromise = require('@geut/hypercore-promise') const makeHyperdrivePromise = require('@geut/hyperdrive-promise') const DEFAULT_SWARM_OPTS = { extensions: [], preferredPort: 42666 } const DEFAULT_DRIVE_OPTS = { sparse: true, persist: true } const DEFAULT_CORE_OPTS = { sparse: true, persist: true } const DEFAULT_DNS_OPTS = {} const DEFAULT_CORESTORE_OPTS = { sparse: true } const DEFAULT_APPLICATION_NAME = 'dat-sdk' const CLOSE_FN = Symbol('close') const HANDLE_COUNT = Symbol('closeCount') module.exports = SDK // TODO: Set up Promise API based on Beaker https://github.com/beakerbrowser/beaker/blob/blue-hyperdrive10/app/bg/web-apis/fg/hyperdrive.js async function SDK ({ storage, corestore, applicationName = DEFAULT_APPLICATION_NAME, persist = true, swarmOpts, driveOpts, coreOpts, dnsOpts, corestoreOpts } = {} ) { // Derive storage if it isn't provided // Don't derive if corestore was provided if (!storage && !corestore) { if (persist !== false) { storage = RAA(applicationName) } else { // Nothing should be persisted. 🤷 storage = RAM } } if (!corestore) { corestore = new Corestore( storage, Object.assign({}, DEFAULT_CORESTORE_OPTS, corestoreOpts) ) } // Track list of hyperdrives const drives = new Map() const cores = new Map() await corestore.ready() // I think this is used to create a persisted identity? // Needs to be created before the swarm so that it can be passed in const noiseSeed = await deriveSecret(applicationName, 'replication-keypair') const keyPair = HypercoreProtocol.keyPair(noiseSeed) const swarm = new SwarmNetworker(corestore, Object.assign({ keyPair }, DEFAULT_SWARM_OPTS, swarmOpts)) const dns = datDNS(Object.assign({}, DEFAULT_DNS_OPTS, dnsOpts)) return { Hyperdrive, Hypercore, resolveName, getIdentity, deriveSecret, registerExtension, close, get keyPair () { return keyPair }, _storage: storage, _corestore: corestore, _swarm: swarm, _dns: dns } async function deriveSecret (namespace, name) { return corestore.inner._deriveSecret(namespace, name) } function getIdentity () { console.warn('getIdentity is being deprecated and will be removed in version 3.x.x, please use sdk.keyPair instead') return keyPair } function close (cb) { for (const drive of drives.values()) { drive.close() } for (const core of cores.values()) { core.close() } swarm.close().then(cb, cb) } function resolveName (url, cb) { return dns.resolveName(url, cb) } function registerExtension (name, handlers) { return swarm.registerExtension(name, handlers) } function Hyperdrive (nameOrKey, opts) { if (!nameOrKey) throw new Error('Must give a name or key in the constructor') opts = Object.assign({}, DEFAULT_DRIVE_OPTS, driveOpts, opts) let key = null try { key = DatEncoding.decode(nameOrKey) // Normalize keys to be hex strings of the key instead of dat URLs nameOrKey = key.toString('hex') } catch (e) { // Probably isn't a `dat://` URL, so it must be a name } if (drives.has(nameOrKey)) { const existing = drives.get(nameOrKey) existing[HANDLE_COUNT]++ return existing } opts.namespace = nameOrKey const { persist } = opts let driveStorage = corestore if (!persist) { driveStorage = RAM } else if (opts.storage) { driveStorage = opts.storage(key) } else { driveStorage = corestore } const drive = makeHyperdrive(driveStorage, key, opts) const wrappedDrive = makeHyperdrivePromise(drive) drive[HANDLE_COUNT] = 0 drive[CLOSE_FN] = drive.close drive.close = function (fd, cb) { if (fd && cb) return this[CLOSE_FN](fd, cb) const hasHandles = wrappedDrive[HANDLE_COUNT]-- if (hasHandles > 0) setTimeout(fd, 0) else setTimeout(() => this[CLOSE_FN](fd, cb), 0) } if (driveStorage !== corestore) { drive.ready(() => { for (const core of drive.corestore.store.list().values()) { trackMemoryCore(core) } drive.corestore.store.on('feed', (core) => { trackMemoryCore(core) }) }) } drives.set(nameOrKey, wrappedDrive) if (!key) { drive.ready(() => { const key = drive.key const stringKey = key.toString('hex') drives.set(stringKey, wrappedDrive) }) } drive.ready(() => { const { discoveryKey = drive.discoveryKey, lookup = true, announce = true } = opts // Don't advertise if we're not looking up or announcing if (!lookup && !announce) return swarm.configure(discoveryKey, { lookup, announce }) }) drive.once('close', () => { const key = drive.key const stringKey = key.toString('hex') drives.delete(stringKey) drives.delete(nameOrKey) const { discoveryKey = drive.discoveryKey } = opts swarm.configure(discoveryKey, { announce: false, lookup: false }) }) return wrappedDrive } function Hypercore (nameOrKey, opts) { if (!nameOrKey) throw new Error('Must give a name or key in the constructor') opts = Object.assign({}, DEFAULT_CORE_OPTS, driveOpts, opts) let key = null try { key = DatEncoding.decode(nameOrKey) // Normalize keys to be hex strings of the key instead of dat URLs nameOrKey = key.toString('hex') } catch (e) { // Probably isn't a `dat://` URL, so it must be a name } if (cores.has(nameOrKey)) { const existing = cores.get(nameOrKey) existing[HANDLE_COUNT]++ return existing } const { persist } = opts let coreStorage = null if (!persist) { coreStorage = RAM } else if (opts.storage) { coreStorage = opts.storage(key) } let core = null // If sotrage was passed in the opts, use it. Else use the corestore if (coreStorage) { // We only want to generate keys if we have a custom storage // Else the corestore does fancy key storage for us if (!key) { const { publicKey, secretKey } = crypto.keyPair() key = publicKey opts.secretKey = secretKey } core = makeHypercore(coreStorage, key, opts) trackMemoryCore(core) } else { if (key) { // If a dat key was provided, get it from the corestore core = corestore.get({ ...opts, key }) } else { // If no dat key was provided, but a name was given, use it as a namespace core = corestore.namespace(nameOrKey).default(opts) } } // Wrap with promises const wrappedCore = makeHypercorePromise(core) core[HANDLE_COUNT] = 0 core.close = function (cb) { const hasHandles = wrappedCore[HANDLE_COUNT]-- if (hasHandles <= 0) { setTimeout(() => { core._close(cb || function noop () {}) }, 0) } else if (cb) setTimeout(cb, 0) } cores.set(nameOrKey, wrappedCore) if (!key) { core.ready(() => { const key = core.key const stringKey = key.toString('hex') cores.set(stringKey, wrappedCore) }) } core.ready(() => { const { discoveryKey = core.discoveryKey, lookup = true, announce = true } = opts // Don't advertise if we're not looking up or announcing if (!lookup && !announce) return swarm.configure(discoveryKey, { announce, lookup }) }) core.once('close', () => { const { discoveryKey = core.discoveryKey } = opts const key = core.key const stringKey = key.toString('hex') swarm.configure(discoveryKey, { announce: false, lookup: false }) cores.delete(stringKey) cores.delete(nameOrKey) }) return wrappedCore } function trackMemoryCore (core) { core.ready(() => { cacheCore(core) corestore.inner._injectIntoReplicationStreams(core) corestore.inner.emit('feed', core) }) core.once('close', () => { uncacheCore(core) }) } function cacheCore (core) { corestore.inner.cache.set(core.discoveryKey.toString('hex'), core) } function uncacheCore (core) { corestore.inner.cache.delete(core.discoveryKey.toString('hex')) } }