diff --git a/docs/core-api/BITSWAP.md b/docs/core-api/BITSWAP.md index 786f19ace4..f8f6d3c02a 100644 --- a/docs/core-api/BITSWAP.md +++ b/docs/core-api/BITSWAP.md @@ -159,7 +159,7 @@ The returned object contains the following keys: - `provideBufLen` is an integer. - `wantlist` (array of [CID][cid]s) -- `peers` (array of peer IDs as Strings) +- `peers` (array of peer IDs represented by CIDs) - `blocksReceived` is a [BigNumber Int][1] - `dataReceived` is a [BigNumber Int][1] - `blocksSent` is a [BigNumber Int][1] @@ -194,4 +194,4 @@ A great source of [examples][] can be found in the tests for this API. [examples]: https://github.com/ipfs/js-ipfs/blob/master/packages/interface-ipfs-core/src/bitswap [cid]: https://www.npmjs.com/package/cids [peerid]: https://www.npmjs.com/package/peer-id -[AbortSignal]: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal \ No newline at end of file +[AbortSignal]: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal diff --git a/examples/browser-ipns-publish/package.json b/examples/browser-ipns-publish/package.json index 988c6d2184..fbdc1f0029 100644 --- a/examples/browser-ipns-publish/package.json +++ b/examples/browser-ipns-publish/package.json @@ -27,7 +27,7 @@ "devDependencies": { "delay": "^4.4.0", "execa": "^4.0.3", - "ipfsd-ctl": "^7.0.2", + "ipfsd-ctl": "^7.1.1", "go-ipfs": "^0.7.0", "parcel-bundler": "^1.12.4", "path": "^0.12.7", diff --git a/examples/explore-ethereum-blockchain/package.json b/examples/explore-ethereum-blockchain/package.json index 28ae6c4a39..097783c2f0 100644 --- a/examples/explore-ethereum-blockchain/package.json +++ b/examples/explore-ethereum-blockchain/package.json @@ -12,7 +12,7 @@ "devDependencies": { "ipfs": "^0.52.1", "ipfs-http-client": "^48.1.1", - "ipfsd-ctl": "^7.0.2", + "ipfsd-ctl": "^7.1.1", "ipld-ethereum": "^5.0.1", "test-ipfs-example": "^2.0.3" } diff --git a/examples/http-client-browser-pubsub/package.json b/examples/http-client-browser-pubsub/package.json index 0b04ed1f63..36b9c8b1fd 100644 --- a/examples/http-client-browser-pubsub/package.json +++ b/examples/http-client-browser-pubsub/package.json @@ -21,7 +21,7 @@ "execa": "^4.0.3", "go-ipfs": "^0.7.0", "ipfs": "^0.52.1", - "ipfsd-ctl": "^7.0.2", + "ipfsd-ctl": "^7.1.1", "parcel-bundler": "^1.12.4", "test-ipfs-example": "^2.0.3" } diff --git a/examples/http-client-bundle-webpack/package.json b/examples/http-client-bundle-webpack/package.json index 5401879b70..6d9b00b922 100644 --- a/examples/http-client-bundle-webpack/package.json +++ b/examples/http-client-bundle-webpack/package.json @@ -25,7 +25,7 @@ "copy-webpack-plugin": "^5.0.4", "execa": "^4.0.3", "ipfs": "^0.52.1", - "ipfsd-ctl": "^7.0.2", + "ipfsd-ctl": "^7.1.1", "react-hot-loader": "^4.12.21", "rimraf": "^3.0.2", "test-ipfs-example": "^2.0.3", diff --git a/examples/http-client-name-api/package.json b/examples/http-client-name-api/package.json index 28587a19f2..90e0c1ab0c 100644 --- a/examples/http-client-name-api/package.json +++ b/examples/http-client-name-api/package.json @@ -18,7 +18,7 @@ "devDependencies": { "execa": "^4.0.3", "go-ipfs": "^0.7.0", - "ipfsd-ctl": "^7.0.2", + "ipfsd-ctl": "^7.1.1", "parcel-bundler": "^1.12.4", "rimraf": "^3.0.2", "test-ipfs-example": "^2.0.3" diff --git a/packages/interface-ipfs-core/src/add.js b/packages/interface-ipfs-core/src/add.js index f6e2c6a82a..19625a8331 100644 --- a/packages/interface-ipfs-core/src/add.js +++ b/packages/interface-ipfs-core/src/add.js @@ -55,7 +55,7 @@ module.exports = (common, options) => { after(() => common.clean()) - it('should respect timeout option when adding files', () => { + it('should respect timeout option when adding a file', () => { return testTimeout(() => ipfs.add('Hello', { timeout: 1 })) diff --git a/packages/ipfs-cli/src/commands/init.js b/packages/ipfs-cli/src/commands/init.js index 2a5f9fd0c1..ff29400fde 100644 --- a/packages/ipfs-cli/src/commands/init.js +++ b/packages/ipfs-cli/src/commands/init.js @@ -27,7 +27,8 @@ module.exports = { type: 'number', alias: 'b', default: '2048', - describe: 'Number of bits to use in the generated RSA private key (defaults to 2048)' + describe: 'Number of bits to use in the generated RSA private key (defaults to 2048)', + coerce: Number }) .option('empty-repo', { alias: 'e', @@ -69,22 +70,20 @@ module.exports = { const IPFS = require('ipfs-core') const Repo = require('ipfs-repo') - const node = await IPFS.create({ - repo: new Repo(repoPath), - init: false, - start: false, - config - }) - try { - await node.init({ - algorithm: argv.algorithm, - bits: argv.bits, - privateKey: argv.privateKey, - emptyRepo: argv.emptyRepo, - profiles: argv.profile, - pass: argv.pass, - log: print + await IPFS.create({ + repo: new Repo(repoPath), + init: { + algorithm: argv.algorithm, + bits: argv.bits, + privateKey: argv.privateKey, + emptyRepo: argv.emptyRepo, + profiles: argv.profile, + pass: argv.pass + }, + start: false, + // @ts-ignore - Expects more than {} + config }) } catch (err) { if (err.code === 'EACCES') { diff --git a/packages/ipfs-core/package.json b/packages/ipfs-core/package.json index e725457173..a4269322a5 100644 --- a/packages/ipfs-core/package.json +++ b/packages/ipfs-core/package.json @@ -110,7 +110,6 @@ "multicodec": "^2.0.1", "multihashing-async": "^2.0.1", "native-abort-controller": "~0.0.3", - "p-defer": "^3.0.0", "p-queue": "^6.6.1", "parse-duration": "^0.4.4", "peer-id": "^0.14.1", @@ -122,7 +121,7 @@ "delay": "^4.4.0", "go-ipfs": "^0.7.0", "interface-ipfs-core": "^0.142.2", - "ipfsd-ctl": "^7.0.2", + "ipfsd-ctl": "^7.1.1", "ipld-git": "^0.6.1", "iso-url": "^1.0.0", "nanoid": "^3.1.12", diff --git a/packages/ipfs-core/src/api-manager.js b/packages/ipfs-core/src/api-manager.js deleted file mode 100644 index 03a33b925d..0000000000 --- a/packages/ipfs-core/src/api-manager.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict' - -module.exports = class ApiManager { - /** - * @callback UndefFn - * @param {PropertyKey} prop - */ - - /** - * @template API - * @typedef {{ cancel: () => any, api: API }} Updated - */ - - constructor () { - this._api = {} - /** - * @type {UndefFn} - * @returns {any} - */ - this._onUndef = () => undefined - this.api = new Proxy(this._api, { - get: (_, prop) => { - if (prop === 'then') return undefined // Not a promise! - return this._api[prop] === undefined ? this._onUndef(prop) : this._api[prop] - } - }) - } - - /** - * @template A - * @param {A} nextApi - * @param {UndefFn} [onUndef] - * @returns {Updated} - */ - update (nextApi, onUndef) { - const prevApi = { ...this._api } - const prevUndef = this._onUndef - Object.keys(this._api).forEach(k => { delete this._api[k] }) - const api = Object.assign(this._api, nextApi) - if (onUndef) this._onUndef = onUndef - return { cancel: () => this.update(prevApi, prevUndef), api } - } -} diff --git a/packages/ipfs-core/src/components/add-all/index.js b/packages/ipfs-core/src/components/add-all/index.js index 0c1c24435a..eb5077c824 100644 --- a/packages/ipfs-core/src/components/add-all/index.js +++ b/packages/ipfs-core/src/components/add-all/index.js @@ -13,10 +13,10 @@ const mergeOptions = require('merge-options').bind({ ignoreUndefined: true }) * @param {import('..').GCLock} config.gcLock * @param {import('..').Preload} config.preload * @param {import('..').Pin} config.pin - * @param {import('../init').ConstructorOptions} config.options + * @param {ShardingOptions} [config.options] */ -module.exports = ({ block, gcLock, preload, pin, options: constructorOptions }) => { - const isShardingEnabled = constructorOptions.EXPERIMENTAL && constructorOptions.EXPERIMENTAL.sharding +module.exports = ({ block, gcLock, preload, pin, options }) => { + const isShardingEnabled = options && options.sharding /** * Import multiple files and data into IPFS. * @@ -205,4 +205,7 @@ function pinFile (pin, opts) { * @typedef {import('../../utils').MTime} MTime * @typedef {import('../../utils').AbortOptions} AbortOptions * @typedef {import('..').CID} CID + * + * @typedef {Object} ShardingOptions + * @property {boolean} [sharding] */ diff --git a/packages/ipfs-core/src/components/bitswap/index.js b/packages/ipfs-core/src/components/bitswap/index.js new file mode 100644 index 0000000000..8209007a75 --- /dev/null +++ b/packages/ipfs-core/src/components/bitswap/index.js @@ -0,0 +1,27 @@ +'use strict' + +const createWantlist = require('./wantlist') +const createWantlistForPeer = require('./wantlist-for-peer') +const createUnwant = require('./unwant') +const createStat = require('./stat') + +class BitswapAPI { + /** + * @param {Object} config + * @param {NetworkService} config.network + */ + constructor ({ network }) { + this.wantlist = createWantlist({ network }) + this.wantlistForPeer = createWantlistForPeer({ network }) + this.unwant = createUnwant({ network }) + this.stat = createStat({ network }) + } +} +module.exports = BitswapAPI + +/** + * @typedef {import('..').NetworkService} NetworkService + * @typedef {import('..').PeerId} PeerId + * @typedef {import('..').CID} CID + * @typedef {import('..').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/bitswap/stat.js b/packages/ipfs-core/src/components/bitswap/stat.js index 6197426058..197855ded0 100644 --- a/packages/ipfs-core/src/components/bitswap/stat.js +++ b/packages/ipfs-core/src/components/bitswap/stat.js @@ -6,16 +6,13 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') /** * @param {Object} config - * @param {import('..').IPFSBitSwap} config.bitswap + * @param {import('.').NetworkService} config.network */ -module.exports = ({ bitswap }) => { +module.exports = ({ network }) => { /** * Show diagnostic information on the bitswap agent. * Note: `bitswap.stat` and `stats.bitswap` can be used interchangeably. * - * @param {import('../../utils').AbortOptions} [_options] - * @returns {Promise} - * * @example * ```js * const stats = await ipfs.bitswap.stat() @@ -35,8 +32,11 @@ module.exports = ({ bitswap }) => { * // dupDataReceived: 0 * // } * ``` + * @param {import('.').AbortOptions} [options] + * @returns {Promise} */ - async function stat (_options) { // eslint-disable-line require-await + async function stat (options) { + const { bitswap } = await network.use(options) const snapshot = bitswap.stat().snapshot return { @@ -59,13 +59,11 @@ module.exports = ({ bitswap }) => { * @typedef {object} BitswapStats - An object that contains information about the bitswap agent * @property {number} provideBufLen - an integer * @property {CID[]} wantlist - * @property {string[]} peers - array of peer IDs as Strings + * @property {CID[]} peers - array of peer IDs * @property {Big} blocksReceived * @property {Big} dataReceived * @property {Big} blocksSent * @property {Big} dataSent * @property {Big} dupBlksReceived * @property {Big} dupDataReceived - * - * @typedef {import('..').CID} CID */ diff --git a/packages/ipfs-core/src/components/bitswap/unwant.js b/packages/ipfs-core/src/components/bitswap/unwant.js index 26af33e3c3..bcfc308c36 100644 --- a/packages/ipfs-core/src/components/bitswap/unwant.js +++ b/packages/ipfs-core/src/components/bitswap/unwant.js @@ -6,15 +6,12 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') /** * @param {Object} config - * @param {import('..').IPFSBitSwap} config.bitswap + * @param {import('.').NetworkService} config.network */ -module.exports = ({ bitswap }) => { +module.exports = ({ network }) => { /** * Removes one or more CIDs from the wantlist * - * @param {CID | CID[]} cids - The CIDs to remove from the wantlist - * @param {AbortOptions} [options] - * @returns {Promise} - A promise that resolves once the request is complete * @example * ```JavaScript * let list = await ipfs.bitswap.wantlist() @@ -27,8 +24,14 @@ module.exports = ({ bitswap }) => { * console.log(list) * // [] * ``` + * + * @param {CID | CID[]} cids - The CIDs to remove from the wantlist + * @param {AbortOptions} [options] + * @returns {Promise} - A promise that resolves once the request is complete */ - async function unwant (cids, options) { // eslint-disable-line require-await + async function unwant (cids, options) { + const { bitswap } = await network.use(options) + if (!Array.isArray(cids)) { cids = [cids] } @@ -46,6 +49,5 @@ module.exports = ({ bitswap }) => { } /** - * @typedef {import('..').CID} CID - * @typedef {import('../../utils').AbortOptions} AbortOptions + * @typedef {import('.').AbortOptions} AbortOptions */ diff --git a/packages/ipfs-core/src/components/bitswap/wantlist-for-peer.js b/packages/ipfs-core/src/components/bitswap/wantlist-for-peer.js index dff9d534e1..c81920c85c 100644 --- a/packages/ipfs-core/src/components/bitswap/wantlist-for-peer.js +++ b/packages/ipfs-core/src/components/bitswap/wantlist-for-peer.js @@ -5,24 +5,26 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') /** * @param {Object} config - * @param {import('..').IPFSBitSwap} config.bitswap + * @param {import('.').NetworkService} config.network */ -module.exports = ({ bitswap }) => { +module.exports = ({ network }) => { /** * Returns the wantlist for a connected peer * - * @param {PeerId | CID | string | Uint8Array} peerId - A peer ID to return the wantlist for\ - * @param {AbortOptions} [options] - * @returns {Promise} - An array of CIDs currently in the wantlist - * * @example * ```js * const list = await ipfs.bitswap.wantlistForPeer(peerId) * console.log(list) * // [ CID('QmHash') ] * ``` + * + * @param {PeerId | CID | string | Uint8Array} peerId - A peer ID to return the wantlist for\ + * @param {AbortOptions} [options] + * @returns {Promise} - An array of CIDs currently in the wantlist + * */ - async function wantlistForPeer (peerId, options = {}) { // eslint-disable-line require-await + async function wantlistForPeer (peerId, options = {}) { + const { bitswap } = await network.use(options) const list = bitswap.wantlistForPeer(PeerId.createFromCID(peerId), options) return Array.from(list).map(e => e[1].cid) @@ -32,9 +34,9 @@ module.exports = ({ bitswap }) => { } /** - * @typedef {import('../../utils').AbortOptions} AbortOptions - * @typedef {import('..').CID} CID - * @typedef {import('..').PeerId} PeerId + * @typedef {import('.').AbortOptions} AbortOptions + * @typedef {import('.').CID} CID + * @typedef {import('.').PeerId} PeerId */ /** diff --git a/packages/ipfs-core/src/components/bitswap/wantlist.js b/packages/ipfs-core/src/components/bitswap/wantlist.js index 0d43f816b5..9f2e0d0298 100644 --- a/packages/ipfs-core/src/components/bitswap/wantlist.js +++ b/packages/ipfs-core/src/components/bitswap/wantlist.js @@ -4,22 +4,24 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') /** * @param {Object} config - * @param {import('..').IPFSBitSwap} config.bitswap + * @param {import('.').NetworkService} config.network */ -module.exports = ({ bitswap }) => { +module.exports = ({ network }) => { /** * Returns the wantlist for your node * - * @param {AbortOptions} [options] - * @returns {Promise} - An array of CIDs currently in the wantlist. * @example * ```js * const list = await ipfs.bitswap.wantlist() * console.log(list) * // [ CID('QmHash') ] * ``` + * + * @param {AbortOptions} [options] + * @returns {Promise} - An array of CIDs currently in the wantlist. */ - async function wantlist (options = {}) { // eslint-disable-line require-await + async function wantlist (options = {}) { + const { bitswap } = await network.use(options) const list = bitswap.getWantlist(options) return Array.from(list).map(e => e[1].cid) @@ -29,6 +31,6 @@ module.exports = ({ bitswap }) => { } /** - * @typedef {import('../../utils').AbortOptions} AbortOptions - * @typedef {import('..').CID} CID + * @typedef {import('.').AbortOptions} AbortOptions + * @typedef {import('.').CID} CID */ diff --git a/packages/ipfs-core/src/components/block/get.js b/packages/ipfs-core/src/components/block/get.js index d4c8825af2..42ea8b22ff 100644 --- a/packages/ipfs-core/src/components/block/get.js +++ b/packages/ipfs-core/src/components/block/get.js @@ -5,8 +5,8 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') /** * @param {Object} config - * @param {import('..').IPFSBlockService} config.blockService - * @param {import('..').Preload} config.preload + * @param {import('.').BlockService} config.blockService + * @param {import('.').Preload} config.preload */ module.exports = ({ blockService, preload }) => { /** @@ -39,7 +39,7 @@ module.exports = ({ blockService, preload }) => { * @typedef {Object} GetOptions * @property {boolean} [preload=true] * - * @typedef {import('../../utils').AbortOptions} AbortOptions - * @typedef {import('..').CID} CID - * @typedef {import('..').IPLDBlock} IPLDBlock + * @typedef {import('.').AbortOptions} AbortOptions + * @typedef {import('.').CID} CID + * @typedef {import('.').IPLDBlock} IPLDBlock */ diff --git a/packages/ipfs-core/src/components/block/index.js b/packages/ipfs-core/src/components/block/index.js new file mode 100644 index 0000000000..252a49d115 --- /dev/null +++ b/packages/ipfs-core/src/components/block/index.js @@ -0,0 +1,36 @@ +'use strict' + +const createGet = require('./get') +const createPut = require('./put') +const createRm = require('./rm') +const createStat = require('./stat') + +class BlockAPI { + /** + * @param {Object} config + * @param {Preload} config.preload + * @param {BlockService} config.blockService + * @param {GCLock} config.gcLock + * @param {Pin} config.pin + * @param {PinManager} config.pinManager + */ + constructor ({ blockService, preload, gcLock, pinManager, pin }) { + this.get = createGet({ blockService, preload }) + this.put = createPut({ blockService, preload, gcLock, pin }) + this.rm = createRm({ blockService, gcLock, pinManager }) + this.stat = createStat({ blockService, preload }) + } +} + +module.exports = BlockAPI + +/** + * @typedef {import('..').Preload} Preload + * @typedef {import('..').BlockService} BlockService + * @typedef {import('..').GCLock} GCLock + * @typedef {import('..').Pin} Pin + * @typedef {import('..').PinManager} PinManager + * @typedef {import('..').AbortOptions} AbortOptions + * @typedef {import('..').CID} CID + * @typedef {import('..').IPLDBlock} IPLDBlock + */ diff --git a/packages/ipfs-core/src/components/block/put.js b/packages/ipfs-core/src/components/block/put.js index 5e281e4700..9d3aab45c1 100644 --- a/packages/ipfs-core/src/components/block/put.js +++ b/packages/ipfs-core/src/components/block/put.js @@ -8,10 +8,10 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') /** * @param {Object} config - * @param {import('..').IPFSBlockService} config.blockService - * @param {import('..').Pin} config.pin - * @param {import('..').GCLock} config.gcLock - * @param {import('..').Preload} config.preload + * @param {import('.').BlockService} config.blockService + * @param {import('.').Pin} config.pin + * @param {import('.').GCLock} config.gcLock + * @param {import('.').Preload} config.preload */ module.exports = ({ blockService, pin, gcLock, preload }) => { /** @@ -21,9 +21,6 @@ module.exports = ({ blockService, pin, gcLock, preload }) => { * don't need to pass options, as the block instance will carry the CID * value as a property. * - * @param {Uint8Array | IPLDBlock} block - The block or data to store - * @param {PutOptions & AbortOptions} [options] - **Note:** If you pass a `Block` instance as the block parameter, you don't need to pass options, as the block instance will carry the CID value as a property. - * @returns {Promise} - A Block type object, containing both the data and the hash of the block * @example * ```js * // Defaults @@ -53,6 +50,10 @@ module.exports = ({ blockService, pin, gcLock, preload }) => { * // Logs: * // the CID of the object * ``` + * + * @param {IPLDBlock|Uint8Array} block - The block or data to store + * @param {PutOptions & AbortOptions} [options] - **Note:** If you pass a `Block` instance as the block parameter, you don't need to pass options, as the block instance will carry the CID value as a property. + * @returns {Promise} - A Block type object, containing both the data and the hash of the block */ async function put (block, options = {}) { if (Array.isArray(block)) { @@ -60,8 +61,11 @@ module.exports = ({ blockService, pin, gcLock, preload }) => { } if (!Block.isBlock(block)) { + /** @type {Uint8Array} */ + const bytes = (block) if (options.cid && isIPFS.cid(options.cid)) { - block = new Block(block, CID.isCID(options.cid) ? options.cid : new CID(options.cid)) + const cid = CID.isCID(options.cid) ? options.cid : new CID(options.cid) + block = new Block(bytes, cid) } else { const mhtype = options.mhtype || 'sha2-256' const format = options.format || 'dag-pb' @@ -81,7 +85,7 @@ module.exports = ({ blockService, pin, gcLock, preload }) => { const multihash = await multihashing(block, mhtype) const cid = new CID(cidVersion, format, multihash) - block = new Block(block, cid) + block = new Block(bytes, cid) } } @@ -122,8 +126,7 @@ module.exports = ({ blockService, pin, gcLock, preload }) => { * @property {boolean} [pin=false] - If true, pin added blocks recursively (default: `false`) * @property {boolean} [preload] * - * @typedef {import('../../utils').AbortOptions} AbortOptions - * @typedef {import('..').CID} CID - * @typedef {import('..').IPLDBlock} IPLDBlock + * @typedef {import('.').AbortOptions} AbortOptions + * @typedef {import('.').IPLDBlock} IPLDBlock * @typedef {0|1} CIDVersion */ diff --git a/packages/ipfs-core/src/components/block/rm.js b/packages/ipfs-core/src/components/block/rm.js index 8d2826f1c1..4f05a387f5 100644 --- a/packages/ipfs-core/src/components/block/rm.js +++ b/packages/ipfs-core/src/components/block/rm.js @@ -12,9 +12,9 @@ const BLOCK_RM_CONCURRENCY = 8 /** * @param {Object} config - * @param {import('..').IPFSBlockService} config.blockService - * @param {import('../pin/pin-manager')} config.pinManager - * @param {import('..').GCLock} config.gcLock + * @param {import('.').BlockService} config.blockService + * @param {import('.').PinManager} config.pinManager + * @param {import('.').GCLock} config.gcLock */ module.exports = ({ blockService, gcLock, pinManager }) => { /** @@ -65,6 +65,7 @@ module.exports = ({ blockService, gcLock, pinManager }) => { } // remove has check when https://github.com/ipfs/js-ipfs-block-service/pull/88 is merged + // @ts-ignore - this accesses some internals const has = await blockService._repo.blocks.has(cid) if (!has) { @@ -96,7 +97,7 @@ module.exports = ({ blockService, gcLock, pinManager }) => { * @property {boolean} [force=false] - Ignores nonexistent blocks * @property {boolean} [quiet=false] - Write minimal output * - * @typedef {import('../../utils').AbortOptions} AbortOptions + * @typedef {import('.').AbortOptions} AbortOptions * * @typedef {RmSucceess|RmFailure} RmResult * Note: If an error is present for a given object, the block with diff --git a/packages/ipfs-core/src/components/block/stat.js b/packages/ipfs-core/src/components/block/stat.js index 138cf1a6d7..080530fd7b 100644 --- a/packages/ipfs-core/src/components/block/stat.js +++ b/packages/ipfs-core/src/components/block/stat.js @@ -5,9 +5,10 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') /** * @param {Object} config - * @param {import('..').IPFSBlockService} config.blockService - * @param {import('..').Preload} config.preload + * @param {import('.').BlockService} config.blockService + * @param {import('.').Preload} config.preload */ + module.exports = ({ blockService, preload }) => { /** /** @@ -50,7 +51,6 @@ module.exports = ({ blockService, preload }) => { * @typedef {Object} StatOptions * @property {boolean} [preload] * - * @typedef {import('../../utils').AbortOptions} AbortOptions - * - * @typedef {import('..').CID} CID + * @typedef {import('.').AbortOptions} AbortOptions + * @typedef {import('.').CID} CID */ diff --git a/packages/ipfs-core/src/components/bootstrap/add.js b/packages/ipfs-core/src/components/bootstrap/add.js index 6b4dd623ff..d7b44421a9 100644 --- a/packages/ipfs-core/src/components/bootstrap/add.js +++ b/packages/ipfs-core/src/components/bootstrap/add.js @@ -4,7 +4,8 @@ const { isValidMultiaddr } = require('./utils') const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') /** - * @param {import('..').IPFSRepo} repo + * @param {Object} config + * @param {import('.').Repo} config.repo */ module.exports = ({ repo }) => { /** @@ -30,11 +31,13 @@ module.exports = ({ repo }) => { const config = await repo.config.getAll(options) + // @ts-ignore - May not have `Bootstrap` if (config.Bootstrap.indexOf(multiaddr.toString()) === -1) { + // @ts-ignore - May not have `Bootstrap` config.Bootstrap.push(multiaddr.toString()) } - await repo.config.set(config) + await repo.config.replace(config) return { Peers: [multiaddr] @@ -46,7 +49,7 @@ module.exports = ({ repo }) => { /** * @typedef {import('./utils').Peers} Peers - * @typedef {import('../../utils').AbortOptions} AbortOptions - * @typedef {import('..').CID} CID - * @typedef {import('..').Multiaddr} Multiaddr + * @typedef {import('.').AbortOptions} AbortOptions + * @typedef {import('.').CID} CID + * @typedef {import('.').Multiaddr} Multiaddr */ diff --git a/packages/ipfs-core/src/components/bootstrap/clear.js b/packages/ipfs-core/src/components/bootstrap/clear.js index 1220ef8af7..a755451a81 100644 --- a/packages/ipfs-core/src/components/bootstrap/clear.js +++ b/packages/ipfs-core/src/components/bootstrap/clear.js @@ -5,7 +5,7 @@ const Multiaddr = require('multiaddr') /** * @param {Object} config - * @param {import('..').IPFSRepo} config.repo + * @param {import('.').Repo} config.repo */ module.exports = ({ repo }) => { /** @@ -26,7 +26,7 @@ module.exports = ({ repo }) => { const removed = config.Bootstrap || [] config.Bootstrap = [] - await repo.config.set(config) + await repo.config.replace(config) return { Peers: removed.map(ma => new Multiaddr(ma)) } } @@ -35,8 +35,6 @@ module.exports = ({ repo }) => { } /** - * @typedef {import('../../utils').AbortOptions} AbortOptions + * @typedef {import('.').AbortOptions} AbortOptions * @typedef {import('./utils').Peers} Peers - * @typedef {import('..').CID} CID - * @typedef {import('..').Multiaddr} Multiaddr */ diff --git a/packages/ipfs-core/src/components/bootstrap/index.js b/packages/ipfs-core/src/components/bootstrap/index.js new file mode 100644 index 0000000000..3cb7c9dafa --- /dev/null +++ b/packages/ipfs-core/src/components/bootstrap/index.js @@ -0,0 +1,28 @@ +'use strict' + +const createAdd = require('./add') +const createClear = require('./clear') +const createList = require('./list') +const createReset = require('./reset') +const createRm = require('./rm') +class BootstrapAPI { + /** + * @param {Object} config + * @param {Repo} config.repo + */ + constructor ({ repo }) { + this.add = createAdd({ repo }) + this.list = createList({ repo }) + this.rm = createRm({ repo }) + this.clear = createClear({ repo }) + this.reset = createReset({ repo }) + } +} +module.exports = BootstrapAPI + +/** + * @typedef {import('..').Repo} Repo + * @typedef {import('..').AbortOptions} AbortOptions + * @typedef {import('..').CID} CID + * @typedef {import('..').Multiaddr} Multiaddr + */ diff --git a/packages/ipfs-core/src/components/bootstrap/list.js b/packages/ipfs-core/src/components/bootstrap/list.js index 5c445e4056..87785d3df6 100644 --- a/packages/ipfs-core/src/components/bootstrap/list.js +++ b/packages/ipfs-core/src/components/bootstrap/list.js @@ -4,7 +4,8 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') const Multiaddr = require('multiaddr') /** - * @param {import('..').IPFSRepo} repo + * @param {Object} config + * @param {import('.').Repo} config.repo */ module.exports = ({ repo }) => { /** @@ -21,7 +22,8 @@ module.exports = ({ repo }) => { * ``` */ async function list (options) { - const peers = await repo.config.get('Bootstrap', options) + /** @type {string[]|null} */ + const peers = (await repo.config.get('Bootstrap', options)) return { Peers: (peers || []).map(ma => new Multiaddr(ma)) } } @@ -29,8 +31,7 @@ module.exports = ({ repo }) => { } /** - * @typedef {import('../../utils').AbortOptions} AbortOptions * @typedef {import('./utils').Peers} Peers - * @typedef {import('..').CID} CID - * @typedef {import('..').Multiaddr} Multiaddr + * @typedef {import('.').AbortOptions} AbortOptions + * @typedef {import('.').CID} CID */ diff --git a/packages/ipfs-core/src/components/bootstrap/reset.js b/packages/ipfs-core/src/components/bootstrap/reset.js index 897c68a8eb..9fdf3bc743 100644 --- a/packages/ipfs-core/src/components/bootstrap/reset.js +++ b/packages/ipfs-core/src/components/bootstrap/reset.js @@ -5,7 +5,8 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') const Multiaddr = require('multiaddr') /** - * @param {import('..').IPFSRepo} repo + * @param {Object} config + * @param {import('.').Repo} config.repo */ module.exports = ({ repo }) => { /** @@ -25,7 +26,7 @@ module.exports = ({ repo }) => { const config = await repo.config.getAll(options) config.Bootstrap = defaultConfig().Bootstrap - await repo.config.set(config) + await repo.config.replace(config) return { Peers: defaultConfig().Bootstrap.map(ma => new Multiaddr(ma)) @@ -36,8 +37,6 @@ module.exports = ({ repo }) => { } /** - * @typedef {import('../../utils').AbortOptions} AbortOptions + * @typedef {import('.').AbortOptions} AbortOptions * @typedef {import('./utils').Peers} Peers - * @typedef {import('..').CID} CID - * @typedef {import('..').Multiaddr} Multiaddr */ diff --git a/packages/ipfs-core/src/components/bootstrap/rm.js b/packages/ipfs-core/src/components/bootstrap/rm.js index f96a2537f4..1e10b1b87b 100644 --- a/packages/ipfs-core/src/components/bootstrap/rm.js +++ b/packages/ipfs-core/src/components/bootstrap/rm.js @@ -4,7 +4,8 @@ const { isValidMultiaddr } = require('./utils') const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') /** - * @param {import('..').IPFSRepo} repo + * @param {Object} config + * @param {import('.').Repo} config.repo */ module.exports = ({ repo }) => { /** @@ -29,7 +30,7 @@ module.exports = ({ repo }) => { const config = await repo.config.getAll(options) config.Bootstrap = (config.Bootstrap || []).filter(ma => ma.toString() !== multiaddr.toString()) - await repo.config.set(config) + await repo.config.replace(config) return { Peers: [multiaddr] } } @@ -38,8 +39,7 @@ module.exports = ({ repo }) => { } /** - * @typedef {import('../../utils').AbortOptions} AbortOptions + * @typedef {import('.').AbortOptions} AbortOptions + * @typedef {import('.').Multiaddr} Multiaddr * @typedef {import('./utils').Peers} Peers - * @typedef {import('..').CID} CID - * @typedef {import('..').Multiaddr} Multiaddr */ diff --git a/packages/ipfs-core/src/components/config.js b/packages/ipfs-core/src/components/config.js index d3319ab055..ee45ce5c39 100644 --- a/packages/ipfs-core/src/components/config.js +++ b/packages/ipfs-core/src/components/config.js @@ -6,27 +6,58 @@ const log = require('debug')('ipfs:core:config') /** * @param {Object} config - * @param {import('.').IPFSRepo} config.repo - * @returns {Config} + * @param {import('.').Repo} config.repo */ module.exports = ({ repo }) => { return { - getAll: withTimeoutOption(repo.config.getAll), - get: withTimeoutOption((key, options) => { - if (!key) { - return Promise.reject(new Error('key argument is required')) - } - - return repo.config.get(key, options) - }), - set: withTimeoutOption(repo.config.set), - replace: withTimeoutOption(repo.config.replace), + getAll: withTimeoutOption(getAll), + get: withTimeoutOption(get), + set: withTimeoutOption(set), + replace: withTimeoutOption(replace), profiles: { apply: withTimeoutOption(applyProfile), list: withTimeoutOption(listProfiles) } } + /** + * @param {AbortOptions} [options] + */ + async function getAll (options = {}) { // eslint-disable-line require-await + return repo.config.getAll(options) + } + + /** + * + * @param {string} key + * @param {AbortOptions} [options] + */ + async function get (key, options) { // eslint-disable-line require-await + if (!key) { + return Promise.reject(new Error('key argument is required')) + } + + return repo.config.get(key, options) + } + + /** + * + * @param {string} key + * @param {ToJSON} value + * @param {AbortOptions} [options] + */ + async function set (key, value, options) { // eslint-disable-line require-await + return repo.config.set(key, value, options) + } + + /** + * @param {IPFSConfig} value + * @param {AbortOptions} [options] + */ + async function replace (value, options) { // eslint-disable-line require-await + return repo.config.replace(value, options) + } + /** * @param {string} profileName * @param {*} options @@ -51,6 +82,7 @@ module.exports = ({ repo }) => { } // Scrub private key from output + // @ts-ignore `oldCfg.Identity` maybe undefined delete oldCfg.Identity.PrivKey delete newCfg.Identity.PrivKey @@ -190,10 +222,10 @@ module.exports.profiles = profiles * Returns the currently being used config. If the daemon is off, it returns * the stored config. * - * @param {string} [key] - The key of the value that should be fetched from the + * @param {string} key - The key of the value that should be fetched from the * config file. If no key is passed, then the whole config will be returned. * @param {AbortOptions} [options] - * @returns {Promise} - An object containing the configuration of the IPFS node + * @returns {Promise} - An object containing the configuration of the IPFS node * @example * const config = await ipfs.config.get('Addresses.Swarm') * console.log(config) @@ -216,7 +248,7 @@ module.exports.profiles = profiles * an effect. * * @param {string} key - The key of the value that should be added or replaced. - * @param {JSON} value - The value to be set. + * @param {ToJSON} value - The value to be set. * @param {AbortOptions} [options] * @returns {Promise} - Promise succeeds if config change succeeded, * otherwise fails with error. @@ -231,7 +263,7 @@ module.exports.profiles = profiles * i.e: if a config.replace changes the multiaddrs of the Swarm, Swarm will * have to be restarted manually for the changes to take an effect. * - * @param {Partial} value - A new configuration. + * @param {IPFSConfig} value - A new configuration. * @param {AbortOptions} [options] * @returns {Promise} * @example @@ -262,15 +294,12 @@ module.exports.profiles = profiles * @callback ApplyProfile * List available config profiles * @param {string} name - * @param {ApplyOptions} [options] + * @param {ApplyOptions & AbortOptions} [options] * @returns {Promise<{original: IPFSConfig, updated: IPFSConfig}>} * - * @typedef {Object} ApplyOptionsExt + * @typedef {Object} ApplyOptions * @property {boolean} [dryRun=false] - If true does not apply the profile - * @typedef {AbortOptions & ApplyOptionsExt} ApplyOptions - * * - * @typedef {import('../utils').AbortOptions} AbortOptions * * @typedef {Object} IPFSConfig * @property {AddressConfig} Addresses @@ -282,6 +311,7 @@ module.exports.profiles = profiles * @property {KeychainConfig} [Keychain] * @property {PubsubConfig} [Pubsub] * @property {SwarmConfig} [Swarm] + * @property {RoutingConfig} [Routing] * * @typedef {Object} AddressConfig * Contains information about various listener addresses to be used by this node. @@ -471,4 +501,10 @@ module.exports.profiles = profiles * exceeded, will trigger a connection GC operation. * * {{LowWater?:number, HighWater?:number}} ConnMgr + * + * @typedef {Object} RoutingConfig + * @property {string} [Type] + * + * @typedef {import('../interface/basic').ToJSON} ToJSON + * @typedef {import('.').AbortOptions} AbortOptions */ diff --git a/packages/ipfs-core/src/components/dag/get.js b/packages/ipfs-core/src/components/dag/get.js index 057ae4b4ae..7fb86bfca8 100644 --- a/packages/ipfs-core/src/components/dag/get.js +++ b/packages/ipfs-core/src/components/dag/get.js @@ -14,12 +14,8 @@ module.exports = ({ ipld, preload }) => { /** * Retrieve an IPLD format node * - * @param {CID} ipfsPath - A DAG node that follows one of the supported IPLD formats - * @param {GetOptions & AbortOptions} [options] - An optional configration - * @returns {Promise} * @example * ```js - * ```JavaScript * // example obj * const obj = { * a: 1, @@ -58,6 +54,10 @@ module.exports = ({ ipld, preload }) => { * // Logs: * // 6 * ``` + * + * @param {CID|string} ipfsPath - A DAG node that follows one of the supported IPLD formats + * @param {GetOptions & AbortOptions} [options] - An optional configration + * @returns {Promise} */ const get = async function get (ipfsPath, options = {}) { const { @@ -74,11 +74,11 @@ module.exports = ({ ipld, preload }) => { } if (options.path) { - const result = options.localResolve - /** @type {DagEntry} - first will return undefined if empty */ - ? (await first(ipld.resolve(cid, options.path))) - /** @type {DagEntry} - last will return undefined if empty */ - : (await last(ipld.resolve(cid, options.path))) + const entry = options.localResolve + ? await first(ipld.resolve(cid, options.path)) + : await last(ipld.resolve(cid, options.path)) + /** @type {DagEntry} - first and last will return undefined when empty */ + const result = (entry) return result } @@ -102,6 +102,6 @@ module.exports = ({ ipld, preload }) => { * @property {Object} value * @property {string} remainderPath * - * @typedef {import('..').CID} CID - * @typedef {import('../../utils').AbortOptions} AbortOptions + * @typedef {import('.').CID} CID + * @typedef {import('.').AbortOptions} AbortOptions */ diff --git a/packages/ipfs-core/src/components/dag/index.js b/packages/ipfs-core/src/components/dag/index.js new file mode 100644 index 0000000000..ccb49b2986 --- /dev/null +++ b/packages/ipfs-core/src/components/dag/index.js @@ -0,0 +1,69 @@ +'use strict' + +const createGet = require('./get') +const createResolve = require('./resolve') +const createTree = require('./tree') +const createPut = require('./put') + +class Reader { + /** + * @param {ReaderConfig} config + */ + constructor (config) { + this.get = createGet(config) + this.resolve = createResolve(config) + this.tree = createTree(config) + } +} + +class DagAPI { + /** + * @param {Object} config + * @param {IPLD} config.ipld + * @param {Preload} config.preload + * @param {Pin} config.pin + * @param {GCLock} config.gcLock + * @param {DagReader} config.dagReader + */ + constructor ({ ipld, pin, preload, gcLock, dagReader }) { + const { get, resolve, tree } = dagReader + const put = createPut({ ipld, preload, pin, gcLock }) + + this.get = get + this.resolve = resolve + this.tree = tree + this.put = put + } + + /** + * Creates a reader part of the DAG API. This allows other APIs that require + * reader parts of the DAG API to be instantiated before components required + * by writer end are. + * + * @param {ReaderConfig} config + * @returns {DagReader} + */ + static reader (config) { + return new Reader(config) + } +} + +module.exports = DagAPI + +/** + * @typedef {Object} DagReader + * @property {ReturnType} get + * @property {ReturnType} resolve + * @property {ReturnType} tree + * + * @typedef {Object} ReaderConfig + * @property {IPLD} ipld + * @property {Preload} preload + * + * @typedef {import('..').IPLD} IPLD + * @typedef {import('..').Preload} Preload + * @typedef {import('..').Pin} Pin + * @typedef {import('..').GCLock} GCLock + * @typedef {import('..').CID} CID + * @typedef {import('..').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/dag/put.js b/packages/ipfs-core/src/components/dag/put.js index 54d45b8959..687d11ccc8 100644 --- a/packages/ipfs-core/src/components/dag/put.js +++ b/packages/ipfs-core/src/components/dag/put.js @@ -11,10 +11,10 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') /** * @param {Object} config - * @param {import('..').IPLD} config.ipld - * @param {import("..").Pin} config.pin - * @param {import("..").GCLock} config.gcLock - * @param {import("..").Preload} config.preload + * @param {import('.').IPLD} config.ipld + * @param {import('.').Pin} config.pin + * @param {import('.').Preload} config.preload + * @param {import('.').GCLock} config.gcLock */ module.exports = ({ ipld, pin, gcLock, preload }) => { /** @@ -32,49 +32,15 @@ module.exports = ({ ipld, pin, gcLock, preload }) => { * // zBwWX9ecx5F4X54WAjmFLErnBT6ByfNxStr5ovowTL7AhaUR98RWvXPS1V3HqV1qs3r5Ec5ocv7eCdbqYQREXNUfYNuKG * ``` */ - // eslint-disable-next-line complexity async function put (dagNode, options = {}) { - if (options.cid && (options.format || options.hashAlg)) { - throw new Error('Can\'t put dag node. Please provide either `cid` OR `format` and `hashAlg` options.') - } else if (((options.format && !options.hashAlg) || (!options.format && options.hashAlg))) { - throw new Error('Can\'t put dag node. Please provide `format` AND `hashAlg` options.') - } - - const optionDefaults = { - format: multicodec.DAG_CBOR, - hashAlg: multicodec.SHA2_256 - } - - // The IPLD expects the format and hashAlg as constants - if (options.format && typeof options.format === 'string') { - options.format = nameToCodec(options.format) - } - if (options.hashAlg && typeof options.hashAlg === 'string') { - options.hashAlg = nameToCodec(options.hashAlg) - } - - options = options.cid ? options : Object.assign({}, optionDefaults, options) - - // js-ipld defaults to verion 1 CIDs. Hence set version 0 explicitly for - // dag-pb nodes - if (options.version === undefined) { - if (options.format === multicodec.DAG_PB && options.hashAlg === multicodec.SHA2_256) { - options.version = 0 - } else { - options.version = 1 - } - } - - let release + const { cidVersion, format, hashAlg } = readEncodingOptions(options) - if (options.pin) { - release = await gcLock.readLock() - } + const release = options.pin ? await gcLock.readLock() : null try { - const cid = await ipld.put(dagNode, options.format, { - hashAlg: options.hashAlg, - cidVersion: options.version, + const cid = await ipld.put(dagNode, format, { + hashAlg, + cidVersion, signal: options.signal }) @@ -100,15 +66,93 @@ module.exports = ({ ipld, pin, gcLock, preload }) => { } /** - * @typedef {Object} PutOptions + * + * @param {PutOptions} options + */ +const readEncodingOptions = (options) => { + if (options.cid && (options.format || options.hashAlg)) { + throw new Error('Can\'t put dag node. Please provide either `cid` OR `format` and `hashAlg` options.') + } else if (((options.format && !options.hashAlg) || (!options.format && options.hashAlg))) { + throw new Error('Can\'t put dag node. Please provide `format` AND `hashAlg` options.') + } + + const { hashAlg, format } = options.cid != null + ? { format: options.cid.code, hashAlg: undefined } + : encodingCodes({ ...defaultCIDOptions, ...options }) + const cidVersion = readVersion({ ...options, format, hashAlg }) + + return { + cidVersion, + format, + hashAlg + } +} + +/** + * + * @param {Object} options + * @param {number|string} options.format + * @param {number|string} [options.hashAlg] + */ +const encodingCodes = ({ format, hashAlg }) => ({ + format: typeof format === 'string' ? nameToCodec(format) : format, + hashAlg: typeof hashAlg === 'string' ? nameToCodec(hashAlg) : hashAlg +}) + +/** + * Figures out what version of CID should be used given the options. + * + * @param {Object} options + * @param {0|1} [options.version] + * @param {CID} [options.cid] + * @param {number} [options.format] + * @param {number} [options.hashAlg] + */ +const readVersion = ({ version, cid, format, hashAlg }) => { + // If version is passed just use that. + if (typeof version === 'number') { + return version + // If cid is provided use version field from it. + } else if (cid) { + return cid.version + // If it's dag-pb nodes use version 0 + } else if (format === multicodec.DAG_PB && hashAlg === multicodec.SHA2_256) { + return 0 + } else { + // Otherwise use version 1 + return 1 + } +} + +/** @type {WithCIDOptions} */ +const defaultCIDOptions = { + format: multicodec.DAG_CBOR, + hashAlg: multicodec.SHA2_256 +} + +/** + * @typedef {PutWith & OtherPutOptions} PutOptions + * @typedef {WithCID | WithCIDOptions} PutWith + * + * + * @typedef {Object} WithCID * @property {CID} [cid] - * @property {string|number} [format] - * @property {string|number} [hashAlg] + * // Note: We still stil need to reserve these fields otherwise it implies + * // that those fields can still be there and have very different types. + * @property {undefined} [format] + * @property {undefined} [hashAlg] + * @property {undefined} [version] + * + * @typedef {Object} WithCIDOptions + * @property {undefined} [cid] + * @property {string|number} format + * @property {string|number} hashAlg + * @property {0|1} [version] * + * @typedef {Object} OtherPutOptions * @property {boolean} [pin=false] - * @property {number} [version] * @property {boolean} [preload=false] * - * @typedef {import('..').CID} CID - * @typedef {import('../../utils').AbortOptions} AbortOptions + * @typedef {import('.').CID} CID + * @typedef {import('.').AbortOptions} AbortOptions */ diff --git a/packages/ipfs-core/src/components/dag/resolve.js b/packages/ipfs-core/src/components/dag/resolve.js index 51931d5d89..574b62665d 100644 --- a/packages/ipfs-core/src/components/dag/resolve.js +++ b/packages/ipfs-core/src/components/dag/resolve.js @@ -6,8 +6,8 @@ const toCidAndPath = require('ipfs-core-utils/src/to-cid-and-path') /** * @param {Object} config - * @param {import('..').IPLD} config.ipld - * @param {import('..').Preload} config.preload + * @param {import('.').IPLD} config.ipld + * @param {import('.').Preload} config.preload */ module.exports = ({ ipld, preload }) => { /** @@ -103,6 +103,5 @@ module.exports = ({ ipld, preload }) => { * @property {string} remainderPath - The path to the end of the IPFS path * inside the node referenced by the CID * - * @typedef {import('..').CID} CID - * @typedef {import('../../utils').AbortOptions} AbortOptions + * @typedef {import('.').AbortOptions} AbortOptions */ diff --git a/packages/ipfs-core/src/components/dag/tree.js b/packages/ipfs-core/src/components/dag/tree.js index e335d02b16..f18c980afa 100644 --- a/packages/ipfs-core/src/components/dag/tree.js +++ b/packages/ipfs-core/src/components/dag/tree.js @@ -5,8 +5,8 @@ const toCidAndPath = require('ipfs-core-utils/src/to-cid-and-path') /** * @param {Object} config - * @param {import('..').IPLD} config.ipld - * @param {import("..").Preload} config.preload + * @param {import('.').IPLD} config.ipld + * @param {import('.').Preload} config.preload */ module.exports = ({ ipld, preload }) => { /** @@ -77,6 +77,6 @@ module.exports = ({ ipld, preload }) => { * @property {string} remainderPath - The path to the end of the IPFS path * inside the node referenced by the CID * - * @typedef {import('..').CID} CID - * @typedef {import('../../utils').AbortOptions} AbortOptions + * @typedef {import('.').CID} CID + * @typedef {import('.').AbortOptions} AbortOptions */ diff --git a/packages/ipfs-core/src/components/dht.js b/packages/ipfs-core/src/components/dht.js index 15c4caf6d0..f13ba3b8b7 100644 --- a/packages/ipfs-core/src/components/dht.js +++ b/packages/ipfs-core/src/components/dht.js @@ -3,9 +3,16 @@ const PeerId = require('peer-id') const CID = require('cids') const errCode = require('err-code') +const { NotEnabledError } = require('../errors') +const get = require('dlv') const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') -module.exports = ({ libp2p, repo }) => { +/** + * @param {Object} config + * @param {import('.').NetworkService} config.network + * @param {import('.').Repo} config.repo + */ +module.exports = ({ network, repo }) => { const { get, put, findProvs, findPeer, provide, query } = { /** * Given a key, query the DHT for its best value. @@ -14,7 +21,8 @@ module.exports = ({ libp2p, repo }) => { * @param {AbortOptions} [options] - The key associated with the value to find * @returns {Promise} */ - async get (key, options = {}) { // eslint-disable-line require-await + async get (key, options = {}) { + const { libp2p } = await use(network, options) return libp2p._dht.get(normalizeCID(key), options) }, @@ -30,8 +38,9 @@ module.exports = ({ libp2p, repo }) => { * @param {AbortOptions} [options] * @returns {AsyncIterable} */ - put (key, value, options) { - return libp2p._dht.put(normalizeCID(key), value) + async * put (key, value, options) { + const { libp2p } = await use(network, options) + yield * libp2p._dht.put(normalizeCID(key), value) }, /** @@ -50,6 +59,7 @@ module.exports = ({ libp2p, repo }) => { * ``` */ async * findProvs (cid, options = {}) { + const { libp2p } = await use(network, options) if (options.numProviders) { options.maxNumProviders = options.numProviders } @@ -83,7 +93,8 @@ module.exports = ({ libp2p, repo }) => { * // '/ip4/147.75.94.115/tcp/4001' * ``` */ - async findPeer (peerId, options) { // eslint-disable-line require-await + async findPeer (peerId, options) { + const { libp2p } = await use(network, options) if (typeof peerId === 'string') { peerId = PeerId.createFromCID(peerId) } @@ -104,6 +115,7 @@ module.exports = ({ libp2p, repo }) => { * @returns {AsyncIterable} */ async * provide (cids, options = {}) { + const { libp2p } = await use(network, options) cids = Array.isArray(cids) ? cids : [cids] for (var i in cids) { @@ -142,6 +154,7 @@ module.exports = ({ libp2p, repo }) => { * @returns {AsyncIterable<{ id: CID, addrs: Multiaddr[] }>} */ async * query (peerId, options) { + const { libp2p } = await use(network, options) if (typeof peerId === 'string') { peerId = PeerId.createFromCID(peerId) } @@ -191,6 +204,18 @@ const parseCID = cid => { const normalizeCID = cid => cid instanceof Uint8Array ? cid : parseCID(cid) +/** + * @param {import('.').NetworkService} network + * @param {AbortOptions} [options] + */ +const use = async (network, options) => { + const net = await network.use(options) + if (get(net.libp2p, '_config.dht.enabled', false)) { + return net + } else { + throw new NotEnabledError('dht not enabled') + } +} /** * @typedef {Object} QueryEvent * @property {PeerId} id diff --git a/packages/ipfs-core/src/components/files/index.js b/packages/ipfs-core/src/components/files/index.js index 88260daadc..bb1478e7b9 100644 --- a/packages/ipfs-core/src/components/files/index.js +++ b/packages/ipfs-core/src/components/files/index.js @@ -95,10 +95,10 @@ function createMfs (options) { * @param {Object} context * @param {import('..').IPLD} context.ipld * @param {import('..').Block} context.block - * @param {import('..').IPFSBlockService} context.blockService - * @param {import('..').IPFSRepo} context.repo + * @param {import('..').BlockService} context.blockService + * @param {import('..').Repo} context.repo * @param {import('..').Preload} context.preload - * @param {import('../init').ConstructorOptions} context.options + * @param {import('..').Options} context.options * @returns {MFS} */ module.exports = ({ ipld, block, blockService, repo, preload, options: constructorOptions }) => { diff --git a/packages/ipfs-core/src/components/gc-lock.js b/packages/ipfs-core/src/components/gc-lock.js new file mode 100644 index 0000000000..9eed02de1c --- /dev/null +++ b/packages/ipfs-core/src/components/gc-lock.js @@ -0,0 +1,24 @@ +'use strict' + +const mortice = require('mortice') + +/** + * @param {Object} config + * @param {string} config.path + * @param {boolean} [config.repoOwner] + * @returns {GCLock} + */ +module.exports = ({ path, repoOwner }) => + mortice(path, { + singleProcess: repoOwner !== false + }) + +/** + * @typedef {RWLock} GCLock + * + * @typedef {Object} RWLock + * @property {() => Promise} readLock + * @property {() => Promise} writeLock + * + * @typedef {() => void} Lock + */ diff --git a/packages/ipfs-core/src/components/id.js b/packages/ipfs-core/src/components/id.js index f319bf3e03..547a67bb1c 100644 --- a/packages/ipfs-core/src/components/id.js +++ b/packages/ipfs-core/src/components/id.js @@ -7,15 +7,15 @@ const uint8ArrayToString = require('uint8arrays/to-string') /** * @param {Object} config - * @param {import('peer-id')} config.peerId - * @param {import('libp2p')} [config.libp2p] + * @param {import('.').PeerId} config.peerId + * @param {import('.').NetworkService} config.network */ -module.exports = ({ peerId, libp2p }) => { +module.exports = ({ peerId, network }) => { /** * Returns the identity of the Peer * * @param {import('../utils').AbortOptions} [_options] - * @returns {Promise} + * @returns {Promise} * @example * ```js * const identity = await ipfs.id() @@ -27,7 +27,10 @@ module.exports = ({ peerId, libp2p }) => { let addresses = [] let protocols = [] - if (libp2p) { + const net = network.try() + + if (net) { + const { libp2p } = net // only available while the node is running addresses = libp2p.transportManager.getAddrs() protocols = Array.from(libp2p.upgrader.protocols.keys()) @@ -59,7 +62,7 @@ module.exports = ({ peerId, libp2p }) => { } /** - * @typedef {object} PeerId + * @typedef {object} ID * The Peer identity * @property {string} id - the Peer ID * @property {string} publicKey - the public key of the peer as a base64 encoded string diff --git a/packages/ipfs-core/src/components/index.js b/packages/ipfs-core/src/components/index.js index cc68c7cb2e..f2d8b06759 100644 --- a/packages/ipfs-core/src/components/index.js +++ b/packages/ipfs-core/src/components/index.js @@ -1,322 +1,386 @@ 'use strict' -/** - * @typedef {ReturnType} Add - */ -exports.add = require('./add') - -/** - * @typedef {ReturnType} AddAll - */ - -exports.addAll = require('./add-all') - -/** - * @typedef {Object} Block - * @property {ReturnType} get - * @property {ReturnType} put - * @property {ReturnType} rm - * @property {ReturnType} stat - */ -exports.block = { - get: require('./block/get'), - put: require('./block/put'), - rm: require('./block/rm'), - stat: require('./block/stat') +const { mergeOptions } = require('../utils') +const { isTest } = require('ipfs-utils/src/env') +const log = require('debug')('ipfs') + +const { DAGNode } = require('ipld-dag-pb') +const UnixFs = require('ipfs-unixfs') +const multicodec = require('multicodec') +const initAssets = require('../runtime/init-assets-nodejs') +const { AlreadyInitializedError } = require('../errors') + +const createStartAPI = require('./start') +const createStopAPI = require('./stop') +const createDNSAPI = require('./dns') +const createIsOnlineAPI = require('./is-online') +const createResolveAPI = require('./resolve') +const PinAPI = require('./pin') +const IPNSAPI = require('./ipns') +const NameAPI = require('./name') +const createRefsAPI = require('./refs') +const createRefsLocalAPI = require('./refs/local') +const BitswapAPI = require('./bitswap') +const BootstrapAPI = require('./bootstrap') +const BlockAPI = require('./block') +const RootAPI = require('./root') +const createVersionAPI = require('./version') +const createIDAPI = require('./id') +const createConfigAPI = require('./config') +const DagAPI = require('./dag') +const PinManagerAPI = require('./pin/pin-manager') +const createPreloadAPI = require('../preload') +const createMfsPreloadAPI = require('../mfs-preload') +const createFilesAPI = require('./files') +const KeyAPI = require('./key') +const ObjectAPI = require('./object') +const RepoAPI = require('./repo') +const StatsAPI = require('./stats') +const IPFSBlockService = require('ipfs-block-service') +const createIPLD = require('./ipld') +const Storage = require('./storage') +const Network = require('./network') +const Service = require('../utils/service') +const SwarmAPI = require('./swarm') +const createGCLockAPI = require('./gc-lock') +const createPingAPI = require('./ping') +const createDHTAPI = require('./dht') +const createPubSubAPI = require('./pubsub') + +class IPFS { + /** + * @param {Object} config + * @param {Print} config.print + * @param {StorageAPI} config.storage + * @param {Options} config.options + */ + constructor ({ print, storage, options }) { + const { peerId, repo, keychain } = storage + const network = Service.create(Network) + + const preload = createPreloadAPI(options.preload) + + /** @type {BlockService} */ + const blockService = new IPFSBlockService(storage.repo) + const ipld = createIPLD({ blockService, print, options: options.ipld }) + + const gcLock = createGCLockAPI({ + path: repo.path, + repoOwner: options.repoOwner + }) + const dns = createDNSAPI() + const isOnline = createIsOnlineAPI({ network }) + const ipns = new IPNSAPI(options) + const dagReader = DagAPI.reader({ ipld, preload }) + + const name = new NameAPI({ + dns, + ipns, + dagReader, + peerId, + isOnline, + keychain, + options + }) + const resolve = createResolveAPI({ ipld, name }) + const pinManager = new PinManagerAPI({ repo, dagReader }) + const pin = new PinAPI({ gcLock, pinManager, dagReader }) + const block = new BlockAPI({ blockService, preload, gcLock, pinManager, pin }) + const dag = new DagAPI({ ipld, preload, gcLock, pin, dagReader }) + const refs = Object.assign(createRefsAPI({ ipld, resolve, preload }), { + local: createRefsLocalAPI({ repo: storage.repo }) + }) + const { add, addAll, cat, get, ls } = new RootAPI({ + gcLock, + preload, + pin, + block, + ipld, + options: options.EXPERIMENTAL + }) + + const files = createFilesAPI({ + ipld, + block, + blockService, + repo, + preload, + options + }) + + const mfsPreload = createMfsPreloadAPI({ + files, + preload, + options: options.preload + }) + + this.preload = preload + this.name = name + this.ipld = ipld + this.ipns = ipns + this.pin = pin + this.resolve = resolve + this.block = block + this.refs = refs + + this.start = createStartAPI({ + network, + peerId, + repo, + blockService, + preload, + ipns, + mfsPreload, + print, + keychain, + options + }) + + this.stop = createStopAPI({ + network, + preload, + mfsPreload, + blockService, + ipns, + repo + }) + + this.dht = createDHTAPI({ network, repo }) + this.pubsub = createPubSubAPI({ network, config: options.config }) + this.dns = dns + this.isOnline = isOnline + this.id = createIDAPI({ network, peerId }) + this.version = createVersionAPI({ repo }) + this.bitswap = new BitswapAPI({ network }) + this.bootstrap = new BootstrapAPI({ repo }) + this.config = createConfigAPI({ repo }) + this.ping = createPingAPI({ network }) + + this.add = add + this.addAll = addAll + this.cat = cat + this.get = get + this.ls = ls + + this.dag = dag + this.files = files + this.key = new KeyAPI({ keychain }) + this.object = new ObjectAPI({ ipld, preload, gcLock, dag }) + this.repo = new RepoAPI({ gcLock, pin, repo, refs }) + this.stats = new StatsAPI({ repo, network }) + this.swarm = new SwarmAPI({ network }) + + // For the backwards compatibility + Object.defineProperty(this, 'libp2p', { + get () { + const net = network.try() + return net ? net.libp2p : undefined + } + }) + } + + /** + * `IPFS.create` will do the initialization. Keep this around for backwards + * compatibility. + * + * @deprecated + */ + async init () { // eslint-disable-line require-await + throw new AlreadyInitializedError() + } + + /** + * @param {Options} options + */ + static async create (options = {}) { + options = mergeOptions(getDefaultOptions(), options) + + // eslint-disable-next-line no-console + const print = options.silent ? log : console.log + + const init = { + ...mergeOptions(initOptions(options), options), + print + } + + const storage = await Storage.start(init) + const config = await storage.repo.config.getAll() + + const ipfs = new IPFS({ + storage, + print, + options: { ...options, config } + }) + + await ipfs.preload.start() + + ipfs.ipns.startOffline(storage) + if (storage.isNew && !init.emptyRepo) { + // add empty unixfs dir object (go-ipfs assumes this exists) + const cid = await addEmptyDir(ipfs) + + log('adding default assets') + await initAssets({ addAll: ipfs.addAll, print }) + + log('initializing IPNS keyspace') + await ipfs.ipns.initializeKeyspace(storage.peerId.privKey, cid.toString()) + } + + if (options.start !== false) { + await ipfs.start() + } + + return ipfs + } } +module.exports = IPFS /** - * @typedef {Object} BitSwap - * @property {ReturnType} stat - * @property {ReturnType} unwant - * @property {ReturnType} wantlist + * @param {Options} options + * @returns {InitOptions} */ -exports.bitswap = { - stat: require('./bitswap/stat'), - unwant: require('./bitswap/unwant'), - wantlist: require('./bitswap/wantlist'), - wantlistForPeer: require('./bitswap/wantlist-for-peer') -} +const initOptions = ({ init }) => + typeof init === 'object' ? init : {} /** - * @typedef {Object} Bootstrap - * @property {ReturnType} add - * @property {ReturnType} list - * @property {ReturnType} rm + * @param {IPFS} ipfs */ -exports.bootstrap = { - add: require('./bootstrap/add'), - clear: require('./bootstrap/clear'), - list: require('./bootstrap/list'), - reset: require('./bootstrap/reset'), - rm: require('./bootstrap/rm') +const addEmptyDir = async (ipfs) => { + const node = new DAGNode(new UnixFs('directory').marshal()) + const cid = await ipfs.dag.put(node, { + version: 0, + format: multicodec.DAG_PB, + hashAlg: multicodec.SHA2_256, + preload: false + }) + + await ipfs.pin.add(cid) + + return cid } /** - * @typedef {ReturnType} Cat + * @returns {Options} */ -exports.cat = require('./cat') +const getDefaultOptions = () => ({ + start: true, + EXPERIMENTAL: {}, + preload: { + enabled: !isTest, // preload by default, unless in test env + addresses: [ + '/dns4/node0.preload.ipfs.io/https', + '/dns4/node1.preload.ipfs.io/https', + '/dns4/node2.preload.ipfs.io/https', + '/dns4/node3.preload.ipfs.io/https' + ] + } +}) /** - * @typedef {ReturnType} Config - */ -exports.config = require('./config') - -/** - * @typedef {Object} DAG - * @property {ReturnType} get - * @property {ReturnType} put - * @property {ReturnType} resolve - * @property {ReturnType} tree - */ -exports.dag = { - get: require('./dag/get'), - put: require('./dag/put'), - resolve: require('./dag/resolve'), - tree: require('./dag/tree') -} - -/** @typedef {ReturnType} DHT */ -exports.dht = require('./dht') - -/** @typedef {ReturnType} DNS */ -exports.dns = require('./dns') - -/** @typedef {ReturnType} Files */ -exports.files = require('./files') - -/** @typedef {ReturnType} Get */ -exports.get = require('./get') - -/** @typedef {ReturnType} ID */ -exports.id = require('./id') - -/** @typedef {ReturnType} Init */ -exports.init = require('./init') - -/** @typedef {ReturnType} IsOnline */ -exports.isOnline = require('./is-online') - -/** - * @typedef {Object} Key - * @property {ReturnType} export - * @property {ReturnType} gen - * @property {ReturnType} import - * @property {ReturnType} info - * @property {ReturnType} list - * @property {ReturnType} rename - * @property {ReturnType} rm - */ - -exports.key = { - export: require('./key/export'), - gen: require('./key/gen'), - import: require('./key/import'), - info: require('./key/info'), - list: require('./key/list'), - rename: require('./key/rename'), - rm: require('./key/rm') -} - -/** @typedef {ReturnType} LibP2P */ -exports.libp2p = require('./libp2p') - -/** @typedef {ReturnType} LS */ -exports.ls = require('./ls') - -/** - * @typedef {Object} Name - * @property {ReturnType} publish - * @property {ReturnType} resolve - * @property {NamePubSub} pubsub + * @typedef {StorageOptions & IPFSOptions} Options * - * @typedef {Object} NamePubSub - * @property {ReturnType} cancel - * @property {ReturnType} state - * @property {ReturnType} subs - */ - -exports.name = { - publish: require('./name/publish'), - pubsub: { - cancel: require('./name/pubsub/cancel'), - state: require('./name/pubsub/state'), - subs: require('./name/pubsub/subs') - }, - resolve: require('./name/resolve') -} - -/** - * @typedef {Object} ObjectAPI - * @property {ReturnType} data - * @property {ReturnType} get - * @property {ReturnType} links - * @property {ReturnType} new - * @property {ReturnType} put - * @property {ReturnType} stat - * @property {ObjectPath} patch + * @typedef {Object} IPFSOptions + * Options argument can be used to specify advanced configuration. + * @property {InitOptions} [init] - Initialization options + * the IPFS node. + * Note that *initializing* a repo is different from creating an instance of + * [`ipfs.Repo`](https://github.com/ipfs/js-ipfs-repo). The IPFS constructor + * sets many special properties when initializing a repo, so you should usually + * not try and call `repoInstance.init()` yourself. + * @property {boolean} [start=true] - If `false`, do not automatically + * start the IPFS node. Instead, you’ll need to manually call + * [`node.start()`](https://github.com/ipfs/js-ipfs/blob/master/packages/ipfs/docs/MODULE.md#nodestart) + * yourself. + * @property {string} [pass=null] - A passphrase to encrypt/decrypt your keys. + * @property {boolean} [silent=false] - Prevents all logging output from the + * IPFS node. (Default: `false`) + * @property {RelayOptions} [relay={ enabled: true, hop: { enabled: false, active: false } }] + * - Configure circuit relay (see the [circuit relay tutorial] + * (https://github.com/ipfs/js-ipfs/tree/master/examples/circuit-relaying) + * to learn more). + * @property {boolean} [offline=false] - Run ipfs node offline. The node does + * not connect to the rest of the network but provides a local API. + * @property {PreloadOptions} [preload] - Configure remote preload nodes. + * The remote will preload content added on this node, and also attempt to + * preload objects requested by this node. + * @property {ExperimentalOptions} [EXPERIMENTAL] - Enable and configure + * experimental features. + * @property {IPFSConfig} [config] - Modify the default IPFS node config. This + * object will be *merged* with the default config; it will not replace it. + * (Default: [`config-nodejs.js`](https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs/src/core/runtime/config-nodejs.js) + * in Node.js, [`config-browser.js`](https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs/src/core/runtime/config-browser.js) + * in browsers) + * @property {IPLDOptions} [ipld] - Modify the default IPLD config. This object + * will be *merged* with the default config; it will not replace it. Check IPLD + * [docs](https://github.com/ipld/js-ipld#ipld-constructor) for more information + * on the available options. (Default: [`ipld.js`] + * (https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs/src/core/runtime/ipld-nodejs.js) in Node.js, [`ipld-browser.js`](https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs/src/core/runtime/ipld-browser.js) + * (https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs/src/core/runtime/ipld.js) + * in browsers) + * @property {LibP2POptions|Function} [libp2p] - The libp2p option allows you to build + * your libp2p node by configuration, or via a bundle function. If you are + * looking to just modify the below options, using the object format is the + * quickest way to get the default features of libp2p. If you need to create a + * more customized libp2p node, such as with custom transports or peer/content + * routers that need some of the ipfs data on startup, a custom bundle is a + * great way to achieve this. + * - You can see the bundle in action in the [custom libp2p example](https://github.com/ipfs/js-ipfs/tree/master/examples/custom-libp2p). + * - Please see [libp2p/docs/CONFIGURATION.md](https://github.com/libp2p/js-libp2p/blob/master/doc/CONFIGURATION.md) + * for the list of options libp2p supports. + * - Default: [`libp2p-nodejs.js`](../src/core/runtime/libp2p-nodejs.js) + * in Node.js, [`libp2p-browser.js`](../src/core/runtime/libp2p-browser.js) in + * browsers. + * @property {boolean} [repoOwner] * - * @typedef {Object} ObjectPath - * @property {ReturnType} addLink - * @property {ReturnType} rmLink - * @property {ReturnType} appendData - * @property {ReturnType} setData - */ -exports.object = { - data: require('./object/data'), - get: require('./object/get'), - links: require('./object/links'), - new: require('./object/new'), - patch: { - addLink: require('./object/patch/add-link'), - appendData: require('./object/patch/append-data'), - rmLink: require('./object/patch/rm-link'), - setData: require('./object/patch/set-data') - }, - put: require('./object/put'), - stat: require('./object/stat') -} - -/** - * @typedef Pin - * @property {ReturnType} add - * @property {ReturnType} addAll - * @property {ReturnType} ls - * @property {ReturnType} rm - */ -exports.pin = { - add: require('./pin/add'), - addAll: require('./pin/add-all'), - ls: require('./pin/ls'), - rm: require('./pin/rm'), - rmAll: require('./pin/rm-all') -} - -/** - * @typedef {ReturnType} Ping - */ -exports.ping = require('./ping') - -/** - * @typedef {ReturnType} PubSub - */ -exports.pubsub = require('./pubsub') - -/** - * @typedef {ReturnType} Refs - * @typedef {ReturnType} LocalRefs - * @typedef {Refs & {local:LocalRefs}} RefsWithLocal - */ -exports.refs = Object.assign(require('./refs'), { local: require('./refs/local') }) - -/** - * @typedef {Object} Repo - * @property {ReturnType} gc - * @property {ReturnType} stat - * @property {ReturnType} version - */ -exports.repo = { - gc: require('./repo/gc'), - stat: require('./repo/stat'), - version: require('./repo/version') -} - -/** @typedef {ReturnType} Resolve */ -exports.resolve = require('./resolve') - -/** @typedef {ReturnType} Start */ -exports.start = require('./start') - -/** - * @typedef {Object} Stats - * @property {ReturnType} bw - */ -exports.stats = { - bw: require('./stats/bw') -} - -/** @typedef {ReturnType} Stop */ -exports.stop = require('./stop') - -/** - * @typedef {Object} Swarm - * @property {ReturnType} addrs - * @property {ReturnType} connect - * @property {ReturnType} disconnect - * @property {ReturnType} localAddrs - * @property {ReturnType} peers - */ -exports.swarm = { - addrs: require('./swarm/addrs'), - connect: require('./swarm/connect'), - disconnect: require('./swarm/disconnect'), - localAddrs: require('./swarm/local-addrs'), - peers: require('./swarm/peers') -} - -/** - * @typedef {ReturnType} Version - */ -exports.version = require('./version') - -/** - * @typedef {ReturnType} Preload - * @typedef {RWLock} GCLock + * @typedef {object} ExperimentalOptions + * @property {boolean} [ipnsPubsub] - Enable pub-sub on IPNS. (Default: `false`) + * @property {boolean} [sharding] - Enable directory sharding. Directories that have many child objects will be represented by multiple DAG nodes instead of just one. It can improve lookup performance when a directory has several thousand files or more. (Default: `false`) * - * @typedef {Object} RWLock - * @property {() => Promise} readLock - * @property {() => Promise} writeLock * - * @typedef {() => void} Lock + * @typedef {import('./storage').StorageOptions} StorageOptions + * @typedef {import('../preload').Options} PreloadOptions + * @typedef {import('./ipld').Options} IPLDOptions + * @typedef {import('./libp2p').Options} LibP2POptions * - * // External library types - * @typedef {import('cids')} CID - * @typedef {import('peer-id')} PeerId - * @typedef {import('multiaddr')} Multiaddr + * @typedef {object} RelayOptions + * @property {boolean} [enabled] - Enable circuit relay dialer and listener. (Default: `true`) + * @property {object} [hop] + * @property {boolean} [hop.enabled] - Make this node a relay (other nodes can connect *through* it). (Default: `false`) + * @property {boolean} [hop.active] - Make this an *active* relay node. Active relay nodes will attempt to dial a destin * - * // Justs pretending these things are typed & hopefully in the future they - * // wil be. - * @typedef {import('ipld')} IPLD - * @typedef {import('ipld').Config} IPLDConfig - * @typedef {import('ipld-block')} IPLDBlock - * @typedef {import('ipfs-repo')} IPFSRepo - * @typedef {import('ipfs-block-service')} IPFSBlockService - * @typedef {import('ipfs-bitswap')} IPFSBitSwap - * @typedef {import('libp2p')} LibP2PService - * @typedef {import('libp2p').Config} LibP2PConfig - */ - -/** - * @typedef {Object} IPFSAPI - * @property {Add} add - * @property {BitSwap} bitswap - * @property {Block} block - * @property {Bootstrap} bootstrap - * @property {Cat} cat - * @property {Config} config - * @property {DAG} dag - * @property {DHT} dht - * @property {DNS} dns - * @property {Files} files - * @property {Get} get - * @property {ID} id - * @property {IsOnline} isOnline - * @property {Key} key - * @property {LibP2P} libp2p - * @property {LS} ls - * @property {Name} name - * @property {ObjectAPI} object - * @property {Pin} pin - * @property {Ping} ping - * @property {PubSub} pubsub - * @property {Refs} refs - * @property {Repo} repo - * @property {Resolve} resolve - * @property {Stats} stats - * @property {Swarm} swarm - * @property {Version} version + * @typedef {import('./storage').InitOptions} InitOptions * - * @property {Init} init - * @property {Start} start - * @property {Stop} stop + * @typedef {import('./storage')} StorageAPI + * + * @typedef {import('./network').Options} NetworkOptions + * @typedef {Service} NetworkService + * @typedef {import('./storage').Repo} Repo + * @typedef {(...args:any[]) => void} Print + * @typedef {import('./storage').Keychain} Keychain + * @typedef {import('./config').IPFSConfig} IPFSConfig + * + * @typedef {import('peer-id')} PeerId + * @typedef {import('./libp2p').LibP2P} LibP2P + * @typedef {import('./pin/pin-manager')} PinManager + * @typedef {import('../interface/block-service').BlockService} BlockService + * @typedef {import('../interface/bitswap').Bitswap} BitSwap + * @typedef {import('./ipld').IPLD} IPLD + * @typedef {import('./gc-lock').GCLock} GCLock + * @typedef {import('../preload').Preload} Preload + * @typedef {import('../mfs-preload').MFSPreload} MFSPreload + * @typedef {import('./ipns')} IPNS + * @typedef {import('./pin')} Pin + * @typedef {import('./block')} Block + * @typedef {import('./dag').DagReader} DagReader + * @typedef {import('./dag')} Dag + * @typedef {ReturnType} Files + * @typedef {ReturnType} IsOnline + * @typedef {ReturnType} Resolve + * @typedef {ReturnType} Refs + * @typedef {ReturnType} DNS + * @typedef {import('./name')} Name + * @typedef {import('../utils').AbortOptions} AbortOptions + * @typedef {import('cids')} CID + * @typedef {import('multiaddr')} Multiaddr + * @typedef {import('./ipld').Block} IPLDBlock */ diff --git a/packages/ipfs-core/src/components/init.js b/packages/ipfs-core/src/components/init.js deleted file mode 100644 index 30bdc1ad77..0000000000 --- a/packages/ipfs-core/src/components/init.js +++ /dev/null @@ -1,562 +0,0 @@ -'use strict' - -const log = require('debug')('ipfs:components:init') -const PeerId = require('peer-id') -const uint8ArrayFromString = require('uint8arrays/from-string') -const uint8ArrayToString = require('uint8arrays/to-string') -const mergeOptions = require('merge-options') -const getDefaultConfig = require('../runtime/config-nodejs.js') -const createRepo = require('../runtime/repo-nodejs') -const mortice = require('mortice') -const { DAGNode } = require('ipld-dag-pb') -const UnixFs = require('ipfs-unixfs') -const multicodec = require('multicodec') -const { - AlreadyInitializingError, - AlreadyInitializedError, - NotStartedError, - NotEnabledError -} = require('../errors') -const BlockService = require('ipfs-block-service') - -/** - * @typedef {import('.').IPLD} IPLD - */ -const Ipld = require('ipld') -const getDefaultIpldOptions = require('../runtime/ipld') - -const createPreloader = require('../preload') -const { ERR_REPO_NOT_INITIALIZED } = require('ipfs-repo').errors -const IPNS = require('../ipns') -const OfflineDatastore = require('../ipns/routing/offline-datastore') -const initAssets = require('../runtime/init-assets-nodejs') -const PinManager = require('./pin/pin-manager') -const Components = require('./') - -/** - * @param {Object} config - * @param {import('../api-manager')} config.apiManager - * @param {(...args:any[]) => void} config.print - * @param {ConstructorOptions} config.options - */ -module.exports = ({ - apiManager, - print, - options: constructorOptions -}) => -/** - * @param {Object} options - */ - async function init (options = {}) { - const { cancel } = apiManager.update({ init: () => { throw new AlreadyInitializingError() } }) - - try { - if (typeof constructorOptions.init === 'object') { - options = mergeOptions(constructorOptions.init, options) - } - - options.pass = options.pass || constructorOptions.pass - - if (constructorOptions.config) { - options.config = mergeOptions(options.config, constructorOptions.config) - } - - options.repo = options.repo || constructorOptions.repo - options.repoAutoMigrate = options.repoAutoMigrate || constructorOptions.repoAutoMigrate - - const repo = typeof options.repo === 'string' || options.repo == null - ? createRepo({ path: options.repo, autoMigrate: options.repoAutoMigrate, silent: constructorOptions.silent }) - : options.repo - - let isInitialized = true - - if (repo.closed) { - try { - await repo.open() - } catch (err) { - if (err.code === ERR_REPO_NOT_INITIALIZED) { - isInitialized = false - } else { - throw err - } - } - } - - if (!isInitialized && options.allowNew === false) { - throw new NotEnabledError('new repo initialization is not enabled') - } - - const { peerId, keychain } = isInitialized - ? await initExistingRepo(repo, options) - : await initNewRepo(repo, { ...options, print }) - - log('peer created') - - const blockService = new BlockService(repo) - const ipld = new Ipld(getDefaultIpldOptions(blockService, constructorOptions.ipld, log)) - - const preload = createPreloader(constructorOptions.preload) - await preload.start() - - // Make sure GC lock is specific to repo, for tests where there are - // multiple instances of IPFS - const gcLock = mortice(repo.path, { singleProcess: constructorOptions.repoOwner !== false }) - const dag = { - get: Components.dag.get({ ipld, preload }), - resolve: Components.dag.resolve({ ipld, preload }), - tree: Components.dag.tree({ ipld, preload }), - // FIXME: resolve this circular dependency - get put () { - const put = Components.dag.put({ ipld, pin, gcLock, preload }) - Object.defineProperty(this, 'put', { value: put }) - return put - } - } - - const object = { - data: Components.object.data({ ipld, preload }), - get: Components.object.get({ ipld, preload }), - links: Components.object.links({ dag }), - new: Components.object.new({ ipld, preload }), - patch: { - addLink: Components.object.patch.addLink({ ipld, gcLock, preload }), - appendData: Components.object.patch.appendData({ ipld, gcLock, preload }), - rmLink: Components.object.patch.rmLink({ ipld, gcLock, preload }), - setData: Components.object.patch.setData({ ipld, gcLock, preload }) - }, - put: Components.object.put({ ipld, gcLock, preload }), - stat: Components.object.stat({ ipld, preload }) - } - - const pinManager = new PinManager(repo, dag) - const pinAddAll = Components.pin.addAll({ pinManager, gcLock, dag }) - const pinRmAll = Components.pin.rmAll({ pinManager, gcLock, dag }) - - const pin = { - add: Components.pin.add({ addAll: pinAddAll }), - addAll: pinAddAll, - ls: Components.pin.ls({ pinManager, dag }), - rm: Components.pin.rm({ rmAll: pinRmAll }), - rmAll: pinRmAll - } - - const block = { - get: Components.block.get({ blockService, preload }), - put: Components.block.put({ blockService, pin, gcLock, preload }), - rm: Components.block.rm({ blockService, gcLock, pinManager }), - stat: Components.block.stat({ blockService, preload }) - } - - const addAll = Components.addAll({ block, preload, pin, gcLock, options: constructorOptions }) - - if (!isInitialized && !options.emptyRepo) { - // add empty unixfs dir object (go-ipfs assumes this exists) - const emptyDirCid = await addEmptyDir({ dag, pin }) - - log('adding default assets') - await initAssets({ addAll, print }) - - log('initializing IPNS keyspace') - // Setup the offline routing for IPNS. - // This is primarily used for offline ipns modifications, such as the initializeKeyspace feature. - const offlineDatastore = new OfflineDatastore(repo) - const ipns = new IPNS(offlineDatastore, repo.datastore, peerId, keychain, { pass: options.pass }) - await ipns.initializeKeyspace(peerId.privKey, emptyDirCid.toString()) - } - - const api = createApi({ - add: Components.add({ addAll }), - addAll, - apiManager, - constructorOptions, - block, - blockService, - dag, - gcLock, - initOptions: options, - ipld, - keychain, - object, - peerId, - pin, - pinManager, - preload, - print, - repo - }) - - return apiManager.update(api, () => { throw new NotStartedError() }).api - } catch (err) { - cancel() - throw err - } - } - -/** - * @param {IPFSRepo} repo - * @param {Object} options - * @param {PrivateKey} options.privateKey - * @param {boolean} [options.emptyRepo] - * @param {number} [options.bits=2048] - Number of bits to use in the generated key - * @param {string[]} options.profiles - * @param {IPFSConfig} options.config - * @param {string} [options.pass] - * @param {(...args:any[]) => void} options.print - * @param {KeyType} [options.algorithm='RSA'] - */ -async function initNewRepo (repo, { privateKey, emptyRepo, algorithm, bits, profiles, config, pass, print }) { - emptyRepo = emptyRepo || false - bits = bits == null ? 2048 : Number(bits) - - // @ts-ignore https://github.com/schnittstabil/merge-options/pull/26 - config = mergeOptions(applyProfiles(profiles, getDefaultConfig()), config) - - // Verify repo does not exist yet - const exists = await repo.exists() - log('repo exists?', exists) - - if (exists === true) { - throw new Error('repo already exists') - } - - const peerId = await createPeerId({ privateKey, algorithm, bits, print }) - - log('identity generated') - - config.Identity = { - PeerID: peerId.toB58String(), - PrivKey: uint8ArrayToString(peerId.privKey.bytes, 'base64pad') - } - - log('peer identity: %s', config.Identity.PeerID) - - await repo.init(config) - await repo.open() - - log('repo opened') - - // Create libp2p for Keychain creation - const libp2p = Components.libp2p({ - peerId, - repo, - config, - keychainConfig: { - pass - } - }) - - if (libp2p.keychain && libp2p.keychain.opts) { - await libp2p.loadKeychain() - - await repo.config.set('Keychain', { - dek: libp2p.keychain.opts.dek - }) - } - - return { peerId, keychain: libp2p.keychain } -} - -/** - * @param {IPFSRepo} repo - * @param {Object} options - * @param {IPFSConfig} [options.config] - * @param {string[]} [options.profiles] - * @param {string} [options.pass] - */ -async function initExistingRepo (repo, { config: newConfig, profiles, pass }) { - let config = await repo.config.getAll() - - if (newConfig || profiles) { - if (profiles) { - config = applyProfiles(profiles, config) - } - if (newConfig) { - // @ts-ignore https://github.com/schnittstabil/merge-options/pull/26 - config = mergeOptions(config, newConfig) - } - await repo.config.set(config) - } - - const peerId = await PeerId.createFromPrivKey(config.Identity.PrivKey) - - const libp2p = Components.libp2p({ - peerId, - repo, - config, - keychainConfig: { - pass, - ...config.Keychain - } - }) - - libp2p.keychain && await libp2p.loadKeychain() - - return { peerId, keychain: libp2p.keychain } -} - -/** - * @param {Object} options - * @param {KeyType} [options.algorithm='RSA'] - * @param {PrivateKey} options.privateKey - * @param {number} options.bits - * @param {(...args:any[]) => void} options.print - */ -function createPeerId ({ privateKey, algorithm = 'RSA', bits, print }) { - if (privateKey) { - log('using user-supplied private-key') - return typeof privateKey === 'object' - ? privateKey - : PeerId.createFromPrivKey(uint8ArrayFromString(privateKey, 'base64pad')) - } else { - // Generate peer identity keypair + transform to desired format + add to config. - print('generating %s-bit (rsa only) %s keypair...', bits, algorithm) - // @ts-ignore - expects "Ed25519" | "RSA" | "secp256k1" instoad of string - return PeerId.create({ keyType: algorithm, bits }) - } -} - -async function addEmptyDir ({ dag, pin }) { - const node = new DAGNode(new UnixFs('directory').marshal()) - const cid = await dag.put(node, { - version: 0, - format: multicodec.DAG_PB, - hashAlg: multicodec.SHA2_256, - preload: false - }) - await pin.add(cid) - - return cid -} - -// Apply profiles (e.g. ['server', 'lowpower']) to config -function applyProfiles (profiles, config) { - return (profiles || []).reduce((config, name) => { - const profile = require('./config').profiles[name] - if (!profile) { - throw new Error(`Could not find profile with name '${name}'`) - } - log('applying profile %s', name) - return profile.transform(config) - }, config) -} - -function createApi ({ - add, - addAll, - apiManager, - constructorOptions, - block, - blockService, - dag, - gcLock, - initOptions, - ipld, - keychain, - object, - peerId, - pin, - pinManager, - preload, - print, - repo -}) { - const notStarted = async () => { // eslint-disable-line require-await - throw new NotStartedError() - } - - const resolve = Components.resolve({ ipld }) - const refs = Object.assign(Components.refs({ ipld, resolve, preload }), { - local: Components.refs.local({ repo }) - }) - - const api = { - add, - addAll, - bitswap: { - stat: notStarted, - unwant: notStarted, - wantlist: notStarted, - wantlistForPeer: notStarted - }, - bootstrap: { - add: Components.bootstrap.add({ repo }), - list: Components.bootstrap.list({ repo }), - rm: Components.bootstrap.rm({ repo }) - }, - block, - cat: Components.cat({ ipld, preload }), - config: Components.config({ repo }), - dag, - dns: Components.dns(), - files: Components.files({ ipld, block, blockService, repo, preload, options: constructorOptions }), - get: Components.get({ ipld, preload }), - id: Components.id({ peerId }), - init: async () => { throw new AlreadyInitializedError() }, // eslint-disable-line require-await - isOnline: Components.isOnline({}), - key: { - export: Components.key.export({ keychain }), - gen: Components.key.gen({ keychain }), - import: Components.key.import({ keychain }), - info: Components.key.info({ keychain }), - list: Components.key.list({ keychain }), - rename: Components.key.rename({ keychain }), - rm: Components.key.rm({ keychain }) - }, - ls: Components.ls({ ipld, preload }), - object, - pin, - refs, - repo: { - gc: Components.repo.gc({ gcLock, pin, refs, repo }), - stat: Components.repo.stat({ repo }), - version: Components.repo.version({ repo }) - }, - resolve, - start: Components.start({ - apiManager, - options: constructorOptions, - blockService, - gcLock, - initOptions, - ipld, - keychain, - peerId, - pinManager, - preload, - print, - repo - }), - stats: { - bitswap: notStarted, - bw: notStarted, - repo: Components.repo.stat({ repo }) - }, - stop: () => {}, - swarm: { - addrs: notStarted, - connect: notStarted, - disconnect: notStarted, - localAddrs: Components.swarm.localAddrs({ multiaddrs: [] }), - peers: notStarted - }, - version: Components.version({ repo }) - } - - return api -} - -/** - * @template {boolean | InitOptions} Init - * @template {boolean} Start - * - * @typedef {Object} ConstructorOptions - * Options argument can be used to specify advanced configuration. - * @property {RepoOption} [repo='~/.jsipfs'] - * @property {boolean} [repoAutoMigrate=true] - `js-ipfs` comes bundled with a - * tool that automatically migrates your IPFS repository when a new version is - * available. - * @property {Init} [init=true] - Perform repo initialization steps when creating - * the IPFS node. - * Note that *initializing* a repo is different from creating an instance of - * [`ipfs.Repo`](https://github.com/ipfs/js-ipfs-repo). The IPFS constructor - * sets many special properties when initializing a repo, so you should usually - * not try and call `repoInstance.init()` yourself. - * @property {Start} [start=true] - If `false`, do not automatically - * start the IPFS node. Instead, you’ll need to manually call - * [`node.start()`](https://github.com/ipfs/js-ipfs/blob/master/packages/ipfs/docs/MODULE.md#nodestart) - * yourself. - * @property {string} [pass=null] - A passphrase to encrypt/decrypt your keys. - * @property {boolean} [silent=false] - Prevents all logging output from the - * IPFS node. (Default: `false`) - * @property {RelayOptions} [relay={ enabled: true, hop: { enabled: false, active: false } }] - * - Configure circuit relay (see the [circuit relay tutorial] - * (https://github.com/ipfs/js-ipfs/tree/master/examples/circuit-relaying) - * to learn more). - * @property {boolean} [offline=false] - Run ipfs node offline. The node does - * not connect to the rest of the network but provides a local API. - * @property {PreloadOptions} [preload] - Configure remote preload nodes. - * The remote will preload content added on this node, and also attempt to - * preload objects requested by this node. - * @property {ExperimentalOptions} [EXPERIMENTAL] - Enable and configure - * experimental features. - * @property {object} [config] - Modify the default IPFS node config. This - * object will be *merged* with the default config; it will not replace it. - * (Default: [`config-nodejs.js`](https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs/src/core/runtime/config-nodejs.js) - * in Node.js, [`config-browser.js`](https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs/src/core/runtime/config-browser.js) - * in browsers) - * @property {import('.').IPLDConfig} [ipld] - Modify the default IPLD config. This object - * will be *merged* with the default config; it will not replace it. Check IPLD - * [docs](https://github.com/ipld/js-ipld#ipld-constructor) for more information - * on the available options. (Default: [`ipld.js`] - * (https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs/src/core/runtime/ipld.js) - * in browsers) - * @property {object|Function} [libp2p] - The libp2p option allows you to build - * your libp2p node by configuration, or via a bundle function. If you are - * looking to just modify the below options, using the object format is the - * quickest way to get the default features of libp2p. If you need to create a - * more customized libp2p node, such as with custom transports or peer/content - * routers that need some of the ipfs data on startup, a custom bundle is a - * great way to achieve this. - * - You can see the bundle in action in the [custom libp2p example](https://github.com/ipfs/js-ipfs/tree/master/examples/custom-libp2p). - * - Please see [libp2p/docs/CONFIGURATION.md](https://github.com/libp2p/js-libp2p/blob/master/doc/CONFIGURATION.md) - * for the list of options libp2p supports. - * - Default: [`libp2p-nodejs.js`](../src/core/runtime/libp2p-nodejs.js) - * in Node.js, [`libp2p-browser.js`](../src/core/runtime/libp2p-browser.js) in - * browsers. - * - * @property {boolean} [repoOwner] - */ - -/** - * @typedef {IPFSRepo|string} RepoOption - * The file path at which to store the IPFS node’s data. Alternatively, you - * can set up a customized storage system by providing an `ipfs.Repo` instance. - * - * @example - * ```js - * // Store data outside your user directory - * const node = await IPFS.create({ repo: '/var/ipfs/data' }) - * ``` - * - * @typedef {object} RelayOptions - * @property {boolean} [enabled] - Enable circuit relay dialer and listener. (Default: `true`) - * @property {object} [hop] - * @property {boolean} [hop.enabled] - Make this node a relay (other nodes can connect *through* it). (Default: `false`) - * @property {boolean} [hop.active] - Make this an *active* relay node. Active relay nodes will attempt to dial a destination peer even if that peer is not yet connected to the relay. (Default: `false`) - * - * @typedef {object} PreloadOptions - * @property {boolean} [enabled] - Enable content preloading (Default: `true`) - * @property {number} [interval] - * @property {string[]} [addresses] - Multiaddr API addresses of nodes that should preload content. - * **NOTE:** nodes specified here should also be added to your node's bootstrap address list at `config.Boostrap`. - * - * @typedef {object} ExperimentalOptions - * @property {boolean} [ipnsPubsub] - Enable pub-sub on IPNS. (Default: `false`) - * @property {boolean} [sharding] - Enable directory sharding. Directories that have many child objects will be represented by multiple DAG nodes instead of just one. It can improve lookup performance when a directory has several thousand files or more. (Default: `false`) - * - * @typedef {Object} InitOptions - * @property {boolean} [emptyRepo=false] - Whether to remove built-in assets, - * like the instructional tour and empty mutable file system, from the repo. - * @property {number} [bits=2048] - Number of bits to use in the generated key - * pair (rsa only). - * @property {PrivateKey} [privateKey] - A pre-generated private key to use. - * **NOTE: This overrides `bits`.** - * @property {string} [pass] - A passphrase to encrypt keys. You should - * generally use the top-level `pass` option instead of the `init.pass` - * option (this one will take its value from the top-level option if not set). - * @property {string[]} [profiles] - Apply profile settings to config. - * @property {boolean} [allowNew=true] - Set to `false` to disallow - * initialization if the repo does not already exist. - * @property {IPFSConfig} [config] - * - * @typedef {import('./config').IPFSConfig} IPFSConfig - * @typedef {import('.').IPFSRepo} IPFSRepo - * - * @typedef {'RSA' | 'ed25519' | 'secp256k1'} KeyType - * - * @typedef {string|PeerId} PrivateKey - * Can be either a base64 string or a [PeerId](https://github.com/libp2p/js-peer-id) - * instance. - * - * @typedef {import('libp2p').Keychain} Keychain - */ diff --git a/packages/ipfs-core/src/components/ipld.js b/packages/ipfs-core/src/components/ipld.js new file mode 100644 index 0000000000..a8b465bc08 --- /dev/null +++ b/packages/ipfs-core/src/components/ipld.js @@ -0,0 +1,23 @@ +'use strict' + +const getDefaultIpldOptions = require('../runtime/ipld') +const Ipld = require('ipld') + +/** + * @param {Object} config + * @param {BlockService} config.blockService + * @param {Print} config.print + * @param {Options} [config.options] + * @returns {IPLD} + */ +const createIPLD = ({ blockService, print, options }) => + new Ipld(getDefaultIpldOptions(blockService, options, print)) +module.exports = createIPLD + +/** + * @typedef {import('../interface/ipld').IPLD} IPLD + * @typedef {import('../interface/ipld').Options} Options + * @typedef {import('../interface/block-service').BlockService} BlockService + * @typedef {import('../interface/basic').Block} Block + * @typedef {import('.').Print} Print + */ diff --git a/packages/ipfs-core/src/components/ipns.js b/packages/ipfs-core/src/components/ipns.js new file mode 100644 index 0000000000..ca305ea3a9 --- /dev/null +++ b/packages/ipfs-core/src/components/ipns.js @@ -0,0 +1,112 @@ +'use strict' + +const IPNS = require('../ipns') +const routingConfig = require('../ipns/routing/config') +const OfflineDatastore = require('../ipns/routing/offline-datastore') +const { NotInitializedError, AlreadyInitializedError } = require('../errors') +const log = require('debug')('ipfs:components:ipns') + +class IPNSAPI { + /** + * @param {Object} options + * @param {string} [options.pass] + * @param {boolean} [options.offline] + * @param {LibP2POptions} [options.libp2p] + * @param {ExperimentalOptions} [options.EXPERIMENTAL] + */ + constructor (options = {}) { + this.options = options + this.offline = null + this.online = null + } + + getIPNS () { + const ipns = this.online || this.offline + if (ipns) { + return ipns + } else { + throw new NotInitializedError() + } + } + + get routing () { + return this.getIPNS().routing + } + + /** + * Activates IPNS subsystem in an ofline mode. If it was started once already + * it will throw an exception. + * + * This is primarily used for offline ipns modifications, such as the + * initializeKeyspace feature. + * + * @param {Object} config + * @param {import('.').Repo} config.repo + * @param {import('.').PeerId} config.peerId + * @param {import('.').Keychain} config.keychain + */ + startOffline ({ repo, peerId, keychain }) { + if (this.offline != null) { + throw new AlreadyInitializedError() + } + + log('initializing IPNS keyspace') + + const routing = new OfflineDatastore(repo) + const ipns = new IPNS(routing, repo.datastore, peerId, keychain, this.options) + + this.offline = ipns + } + + /** + * @param {Object} config + * @param {import('.').LibP2P} config.libp2p + * @param {import('.').Repo} config.repo + * @param {import('.').PeerId} config.peerId + * @param {import('.').Keychain} config.keychain + */ + async startOnline ({ libp2p, repo, peerId, keychain }) { + if (this.online != null) { + throw new AlreadyInitializedError() + } + const routing = routingConfig({ libp2p, repo, peerId, options: this.options }) + + const ipns = new IPNS(routing, repo.datastore, peerId, keychain, this.options) + await ipns.republisher.start() + this.online = ipns + } + + async stop () { + const ipns = this.online + if (ipns) { + await ipns.republisher.stop() + this.online = null + } + } + + publish (privKey, value, lifetime) { + return this.getIPNS().publish(privKey, value, lifetime) + } + + resolve (name, options) { + return this.getIPNS().resolve(name, options) + } + + initializeKeyspace (privKey, value) { + return this.getIPNS().initializeKeyspace(privKey, value) + } +} +module.exports = IPNSAPI + +/** + * @typedef {Object} ExperimentalOptions + * @property {boolean} [ipnsPubsub] + * + * @typedef {Object} LibP2POptions + * @property {DHTConfig} [config] + * + * @typedef {Object} DHTConfig + * @property {boolean} [enabled] + * + * @typedef {import('../ipns')} IPNS + */ diff --git a/packages/ipfs-core/src/components/is-online.js b/packages/ipfs-core/src/components/is-online.js index 450b62e153..1e9593a0f8 100644 --- a/packages/ipfs-core/src/components/is-online.js +++ b/packages/ipfs-core/src/components/is-online.js @@ -2,7 +2,13 @@ /** * @param {Object} config - * @param {import('libp2p')} [config.libp2p] + * @param {import('.').NetworkService} config.network */ -module.exports = ({ libp2p }) => () => - Boolean(libp2p && libp2p.isStarted()) +module.exports = ({ network }) => + /** + * @returns {boolean} + */ + () => { + const net = network.try() + return net != null && net.libp2p.isStarted() + } diff --git a/packages/ipfs-core/src/components/key/export.js b/packages/ipfs-core/src/components/key/export.js index e6766bfaa4..cbf432ed1c 100644 --- a/packages/ipfs-core/src/components/key/export.js +++ b/packages/ipfs-core/src/components/key/export.js @@ -2,6 +2,32 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') +/** + * @param {Object} config + * @param {import('.').Keychain} config.keychain + */ module.exports = ({ keychain }) => { - return withTimeoutOption((name, password, options = {}) => keychain.exportKey(name, password, options)) + /** + * Remove a key + * + * @example + * ```js + * const pem = await ipfs.key.export('self', 'password') + * + * console.log(pem) + * // -----BEGIN ENCRYPTED PRIVATE KEY----- + * // MIIFDTA/BgkqhkiG9w0BBQ0wMjAaBgkqhkiG9w0BBQwwDQQIpdO40RVyBwACAWQw + * // ... + * // YA== + * // -----END ENCRYPTED PRIVATE KEY----- + * ``` + * @param {string} name - The name of the key to export + * @param {string} password - Password to set on the PEM output + * @param {import('.').AbortOptions} options + * @returns {Promise} - The string representation of the key + */ + const exportKey = (name, password, options) => + keychain.exportKey(name, password, options) + + return withTimeoutOption(exportKey) } diff --git a/packages/ipfs-core/src/components/key/gen.js b/packages/ipfs-core/src/components/key/gen.js index 76a270339e..491a938657 100644 --- a/packages/ipfs-core/src/components/key/gen.js +++ b/packages/ipfs-core/src/components/key/gen.js @@ -2,8 +2,42 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') +/** + * @param {Object} config + * @param {import('.').Keychain} config.keychain + */ module.exports = ({ keychain }) => { - return withTimeoutOption((name, options = {}) => { + /** + * Generate a new key + * + * @example + * ```js + * const key = await ipfs.key.gen('my-key', { + * type: 'rsa', + * size: 2048 + * }) + * + * console.log(key) + * // { id: 'QmYWqAFvLWb2G5A69JGXui2JJXzaHXiUEmQkQgor6kNNcJ', + * // name: 'my-key' } + * ``` + * + * @param {string} name - The name to give the key + * @param {GenOptions & AbortOptions} options + * @returns {Promise} + */ + const gen = (name, options = {}) => { return keychain.createKey(name, options.type || 'rsa', options.size || 2048) - }) + } + + return withTimeoutOption(gen) } + +/** + * @typedef {Object} GenOptions + * @property {import('libp2p-crypto').KeyType} [type='RSA'] - The key type + * @property {number} [size=2048] - The key size in bits + * + * @typedef {import('.').Key} Key + * @typedef {import('.').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/key/import.js b/packages/ipfs-core/src/components/key/import.js index 815e9063ec..047ae4d5ce 100644 --- a/packages/ipfs-core/src/components/key/import.js +++ b/packages/ipfs-core/src/components/key/import.js @@ -2,6 +2,31 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') +/** + * @param {Object} config + * @param {import('.').Keychain} config.keychain + */ module.exports = ({ keychain }) => { - return withTimeoutOption((name, pem, password, options) => keychain.importKey(name, pem, password, options)) + /** + * Remove a key + * + * @example + * ```js + * const key = await ipfs.key.import('clone', pem, 'password') + * + * console.log(key) + * // { id: 'QmQRiays958UM7norGRQUG3tmrLq8pJdmJarwYSk2eLthQ', + * // name: 'clone' } + * ``` + * @param {string} name - The name of the key to import + * @param {string} pem - The PEM encoded key + * @param {string} password - The password that protects the PEM key + * @param {import('.').AbortOptions} options + * @returns {Promise} - An object that describes the new key + */ + const importKey = (name, pem, password, options) => { + return keychain.importKey(name, pem, password, options) + } + + return withTimeoutOption(importKey) } diff --git a/packages/ipfs-core/src/components/key/index.js b/packages/ipfs-core/src/components/key/index.js new file mode 100644 index 0000000000..4869addc74 --- /dev/null +++ b/packages/ipfs-core/src/components/key/index.js @@ -0,0 +1,35 @@ +'use strict' + +const createExport = require('./export') +const createGen = require('./gen') +const createImport = require('./import') +const createInfo = require('./info') +const createList = require('./list') +const createRename = require('./rename') +const createRm = require('./rm') + +class KeyAPI { + /** + * @param {Object} config + * @param {Keychain} config.keychain + */ + constructor ({ keychain }) { + this.gen = createGen({ keychain }) + this.list = createList({ keychain }) + this.rm = createRm({ keychain }) + this.rename = createRename({ keychain }) + this.export = createExport({ keychain }) + this.import = createImport({ keychain }) + this.info = createInfo({ keychain }) + } +} +module.exports = KeyAPI + +/** + * @typedef {import('..').Keychain} Keychain + * @typedef {import('..').AbortOptions} AbortOptions + * + * @typedef {Object} Key + * @property {string} name + * @property {string} id + */ diff --git a/packages/ipfs-core/src/components/key/info.js b/packages/ipfs-core/src/components/key/info.js index 6000424535..491eed66b6 100644 --- a/packages/ipfs-core/src/components/key/info.js +++ b/packages/ipfs-core/src/components/key/info.js @@ -2,6 +2,22 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') +/** + * @param {Object} config + * @param {import('.').Keychain} config.keychain + */ module.exports = ({ keychain }) => { - return withTimeoutOption((name, options) => keychain.findKeyByName(name, options)) + /** + * @param {string} name + * @param {AbortOptions} [options] + * @returns {Promise} + */ + const info = (name, options = {}) => keychain.findKeyByName(name, options) + + return withTimeoutOption(info) } + +/** + * @typedef {import('.').Key} Key + * @typedef {import('.').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/key/list.js b/packages/ipfs-core/src/components/key/list.js index 62b7b13513..379b388547 100644 --- a/packages/ipfs-core/src/components/key/list.js +++ b/packages/ipfs-core/src/components/key/list.js @@ -2,6 +2,39 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') +/** + * @param {Object} config + * @param {import('.').Keychain} config.keychain + */ module.exports = ({ keychain }) => { - return withTimeoutOption((options) => keychain.listKeys(options)) + /** + * List all the keys + * + * @example + * ```js + * const keys = await ipfs.key.list() + * + * console.log(keys) + * // [ + * // { id: 'QmTe4tuceM2sAmuZiFsJ9tmAopA8au71NabBDdpPYDjxAb', + * // name: 'self' }, + * // { id: 'QmWETF5QvzGnP7jKq5sPDiRjSM2fzwzNsna4wSBEzRzK6W', + * // name: 'my-key' } + * // ] + * ``` + * + * @param {AbortOptions} [options] + * @returns {Promise} + */ + const list = (options = {}) => keychain.listKeys(options) + + return withTimeoutOption(list) } + +/** + * @typedef {Object} KeyEntry + * @property {string} name - The name of the key + * @property {string} hash - The hash of the key + * + * @typedef {import('.').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/key/rename.js b/packages/ipfs-core/src/components/key/rename.js index be6e44ce4d..26b49c807d 100644 --- a/packages/ipfs-core/src/components/key/rename.js +++ b/packages/ipfs-core/src/components/key/rename.js @@ -2,8 +2,30 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') +/** + * @param {Object} config + * @param {import('.').Keychain} config.keychain + */ module.exports = ({ keychain }) => { - return withTimeoutOption(async (oldName, newName, options) => { + /** + * Rename a key + * + * @example + * ```js + * const key = await ipfs.key.rename('my-key', 'my-new-key') + * + * console.log(key) + * // { id: 'Qmd4xC46Um6s24MradViGLFtMitvrR4SVexKUgPgFjMNzg', + * // was: 'my-key', + * // now: 'my-new-key', + * // overwrite: false } + * ``` + * @param {string} oldName - The current key name + * @param {string} newName - The desired key name + * @param {AbortOptions} [options] + * @returns {Promise} + */ + const rename = async (oldName, newName, options = {}) => { const key = await keychain.renameKey(oldName, newName, options) return { was: oldName, @@ -11,5 +33,17 @@ module.exports = ({ keychain }) => { id: key.id, overwrite: false } - }) + } + + return withTimeoutOption(rename) } + +/** + * @typedef {Object} RenamedKey + * @property {string} was - The name of the key + * @property {string} now - The hash of the key + * @property {string} id + * @property {boolean} overwrite + * + * @typedef {import('.').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/key/rm.js b/packages/ipfs-core/src/components/key/rm.js index 41b2b16d4b..2004a6553b 100644 --- a/packages/ipfs-core/src/components/key/rm.js +++ b/packages/ipfs-core/src/components/key/rm.js @@ -2,6 +2,34 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') +/** + * @param {Object} config + * @param {import('.').Keychain} config.keychain + */ module.exports = ({ keychain }) => { - return withTimeoutOption((name, options) => keychain.removeKey(name, options)) + /** + * Remove a key + * + * @example + * ```js + * const key = await ipfs.key.rm('my-key') + * + * console.log(key) + * // { id: 'QmWETF5QvzGnP7jKq5sPDiRjSM2fzwzNsna4wSBEzRzK6W', + * // name: 'my-key' } + * ``` + * + * @param {string} name - The name of the key to remove + * @param {import('.').AbortOptions} options + * @returns {Promise} - An object that describes the removed key + */ + const rm = (name, options) => keychain.removeKey(name, options) + + return withTimeoutOption(rm) } + +/** + * @typedef {Object} RemovedKey + * @property {string} name - The name of the key + * @property {string} id - The hash of the key + */ diff --git a/packages/ipfs-core/src/components/libp2p.js b/packages/ipfs-core/src/components/libp2p.js index 5086e40549..c985b5632f 100644 --- a/packages/ipfs-core/src/components/libp2p.js +++ b/packages/ipfs-core/src/components/libp2p.js @@ -7,13 +7,13 @@ const PubsubRouters = require('../runtime/libp2p-pubsub-routers-nodejs') /** * @param {Object} config - * @param {import('.').IPFSRepo} config.repo - * @param {Object} [config.options] - * @param {import('.').PeerId} [config.peerId] - * @param {string[]} [config.multiaddrs] - * @param {{pass?:string}} [config.keychainConfig] - * @param {import('.').LibP2PConfig} [config.config] - * @returns {import('.').LibP2PService} + * @param {Repo} config.repo + * @param {IPFSOptions|undefined} config.options + * @param {PeerId} config.peerId + * @param {Multiaddr[]|undefined} config.multiaddrs + * @param {KeychainConfig|undefined} config.keychainConfig + * @param {Partial|undefined} config.config + * @returns {LibP2P} */ module.exports = ({ options = {}, @@ -45,6 +45,17 @@ module.exports = ({ return new Libp2p(libp2pOptions) } +/** + * @param {Object} input + * @param {IPFSOptions} input.options + * @param {Partial} input.config + * @param {Repo['datastore']} input.datastore + * @param {Repo['keys']} input.keys + * @param {KeychainConfig} input.keychainConfig + * @param {PeerId} input.peerId + * @param {Multiaddr[]} input.multiaddrs + * @returns {Options} + */ function getLibp2pOptions ({ options, config, datastore, keys, keychainConfig, peerId, multiaddrs }) { const getPubsubRouter = () => { const router = get(config, 'Pubsub.Router') || 'gossipsub' @@ -143,3 +154,16 @@ function getLibp2pOptions ({ options, config, datastore, keys, keychainConfig, p return libp2pConfig } + +/** + * @typedef {Object} KeychainConfig + * @property {string} [pass] + * + * @typedef {import('.').Repo} Repo + * @typedef {import('.').Multiaddr} Multiaddr + * @typedef {import('.').PeerId} PeerId + * @typedef {import('.').Options} IPFSOptions + * @typedef {import('libp2p')} LibP2P + * @typedef {import('libp2p').Options} Options + * @typedef {import('.').IPFSConfig} IPFSConfig + */ diff --git a/packages/ipfs-core/src/components/ls.js b/packages/ipfs-core/src/components/ls.js index 81256669dc..ae34e1aee9 100644 --- a/packages/ipfs-core/src/components/ls.js +++ b/packages/ipfs-core/src/components/ls.js @@ -7,8 +7,8 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') /** * @param {Object} config - * @param {import('.').IPLD} config.ipld - * @param {import('.').Preload} config.preload + * @param {import('./root').IPLD} config.ipld + * @param {import('./root').Preload} config.preload */ module.exports = function ({ ipld, preload }) { /** diff --git a/packages/ipfs-core/src/components/name/index.js b/packages/ipfs-core/src/components/name/index.js new file mode 100644 index 0000000000..a8bb3b7faf --- /dev/null +++ b/packages/ipfs-core/src/components/name/index.js @@ -0,0 +1,41 @@ +'use strict' + +const createPublishAPI = require('./publish') +const createResolveAPI = require('./resolve') +const PubSubAPI = require('./pubsub') +class NameAPI { + /** + * @param {Object} config + * @param {IPNS} config.ipns + * @param {PeerId} config.peerId + * @param {Options} config.options + * @param {DagReader} config.dagReader + * @param {IsOnline} config.isOnline + * @param {Keychain} config.keychain + * @param {DNS} config.dns + */ + constructor ({ dns, ipns, dagReader, peerId, isOnline, keychain, options }) { + this.publish = createPublishAPI({ ipns, dagReader, peerId, isOnline, keychain }) + this.resolve = createResolveAPI({ dns, ipns, peerId, isOnline, options }) + this.pubsub = new PubSubAPI({ ipns, options: options.EXPERIMENTAL }) + } +} +module.exports = NameAPI + +/** + * @typedef {ResolveOptions & ExperimentalOptions} Options + * + * @typedef {Object} ExperimentalOptions + * @property {PubSubOptions} [EXPERIMENTAL] + * + * @typedef {import('./pubsub').Options} PubSubOptions + * @typedef {import('./resolve').ResolveOptions} ResolveOptions + * + * @typedef {import('..').IPNS} IPNS + * @typedef {import('..').PeerId} PeerId + * @typedef {import('..').DagReader} DagReader + * @typedef {import('..').Keychain} Keychain + * @typedef {import('..').IsOnline} IsOnline + * @typedef {import('..').DNS} DNS + * @typedef {import('..').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/name/publish.js b/packages/ipfs-core/src/components/name/publish.js index bc87fb879d..bade8a1eec 100644 --- a/packages/ipfs-core/src/components/name/publish.js +++ b/packages/ipfs-core/src/components/name/publish.js @@ -17,13 +17,13 @@ const { resolvePath } = require('./utils') * IPNS - Inter-Planetary Naming System * * @param {Object} config - * @param {import('../../ipns')} config.ipns - * @param {import('../index').DAG} config.dag - * @param {import('peer-id')} config.peerId - * @param {import('../index').IsOnline} config.isOnline - * @param {import('../init').Keychain} config.keychain + * @param {import('.').IPNS} config.ipns + * @param {import('.').DagReader} config.dagReader + * @param {import('.').PeerId} config.peerId + * @param {import('.').IsOnline} config.isOnline + * @param {import('.').Keychain} config.keychain */ -module.exports = ({ ipns, dag, peerId, isOnline, keychain }) => { +module.exports = ({ ipns, dagReader, peerId, isOnline, keychain }) => { const lookupKey = async keyName => { if (keyName === 'self') { return peerId.privKey @@ -94,7 +94,7 @@ module.exports = ({ ipns, dag, peerId, isOnline, keychain }) => { const results = await Promise.all([ // verify if the path exists, if not, an error will stop the execution lookupKey(key), - resolve ? resolvePath({ ipns, dag }, value) : Promise.resolve() + resolve ? resolvePath({ ipns, dagReader }, value) : Promise.resolve() ]) // Start publishing process @@ -124,5 +124,5 @@ module.exports = ({ ipns, dag, peerId, isOnline, keychain }) => { * @property {string} name * @property {string} value * - * @typedef {import('../../utils').AbortOptions} AbortOptions + * @typedef {import('.').AbortOptions} AbortOptions */ diff --git a/packages/ipfs-core/src/components/name/pubsub/cancel.js b/packages/ipfs-core/src/components/name/pubsub/cancel.js index f734efd2a1..5d9508c52d 100644 --- a/packages/ipfs-core/src/components/name/pubsub/cancel.js +++ b/packages/ipfs-core/src/components/name/pubsub/cancel.js @@ -5,15 +5,15 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') /** * @param {Object} config - * @param {import('../../../ipns')} config.ipns - * @param {import('../../init').ConstructorOptions} config.options + * @param {import('.').IPNS} config.ipns + * @param {import('.').Options} [config.options] */ -module.exports = ({ ipns, options: constructorOptions }) => { +module.exports = ({ ipns, options: routingOptions }) => { /** * Cancel a name subscription. * * @param {string} name - The name of the subscription to cancel. - * @param {AbortOptions} [options] + * @param {import('.').AbortOptions} [options] * @returns {Promise<{ canceled: boolean }>} * @example * ```js @@ -24,13 +24,9 @@ module.exports = ({ ipns, options: constructorOptions }) => { * ``` */ async function cancel (name, options) { // eslint-disable-line require-await - const pubsub = getPubsubRouting(ipns, constructorOptions) + const pubsub = getPubsubRouting(ipns, routingOptions) return pubsub.cancel(name, options) } return withTimeoutOption(cancel) } - -/** - * @typedef {import('../../../utils').AbortOptions} AbortOptions - */ diff --git a/packages/ipfs-core/src/components/name/pubsub/index.js b/packages/ipfs-core/src/components/name/pubsub/index.js new file mode 100644 index 0000000000..1ea174e082 --- /dev/null +++ b/packages/ipfs-core/src/components/name/pubsub/index.js @@ -0,0 +1,25 @@ +'use strict' + +const createCancelAPI = require('./cancel') +const createStateAPI = require('./state') +const createSubsAPI = require('./subs') + +class PubSubAPI { + /** + * @param {Object} config + * @param {IPNS} config.ipns + * @param {Options} [config.options] + */ + constructor ({ ipns, options }) { + this.cancel = createCancelAPI({ ipns, options }) + this.state = createStateAPI({ ipns, options }) + this.subs = createSubsAPI({ ipns, options }) + } +} +module.exports = PubSubAPI + +/** + * @typedef {import('..').IPNS} IPNS + * @typedef {import('./utils').PubSubRoutingOptions} Options + * @typedef {import('..').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/name/pubsub/state.js b/packages/ipfs-core/src/components/name/pubsub/state.js index a704de8a49..4794d4abc4 100644 --- a/packages/ipfs-core/src/components/name/pubsub/state.js +++ b/packages/ipfs-core/src/components/name/pubsub/state.js @@ -3,11 +3,16 @@ const { getPubsubRouting } = require('./utils') const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') -module.exports = ({ ipns, options: constructorOptions }) => { +/** + * @param {Object} config + * @param {import('.').IPNS} config.ipns + * @param {import('.').Options} [config.options] + */ +module.exports = ({ ipns, options: routingOptions }) => { /** * Query the state of IPNS pubsub. * - * @param {AbortOptions} [_options] + * @param {import('.').AbortOptions} [_options] * @returns {Promise<{ enabled: boolean }>} * ```js * const result = await ipfs.name.pubsub.state() @@ -17,7 +22,7 @@ module.exports = ({ ipns, options: constructorOptions }) => { */ async function state (_options) { // eslint-disable-line require-await try { - return { enabled: Boolean(getPubsubRouting(ipns, constructorOptions)) } + return { enabled: Boolean(getPubsubRouting(ipns, routingOptions)) } } catch (err) { return { enabled: false } } @@ -25,7 +30,3 @@ module.exports = ({ ipns, options: constructorOptions }) => { return withTimeoutOption(state) } - -/** - * @typedef {import('../../../utils').AbortOptions} AbortOptions - */ diff --git a/packages/ipfs-core/src/components/name/pubsub/subs.js b/packages/ipfs-core/src/components/name/pubsub/subs.js index a68b5eb05e..3c16d72c2e 100644 --- a/packages/ipfs-core/src/components/name/pubsub/subs.js +++ b/packages/ipfs-core/src/components/name/pubsub/subs.js @@ -3,11 +3,16 @@ const { getPubsubRouting } = require('./utils') const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') -module.exports = ({ ipns, options: constructorOptions }) => { +/** + * @param {Object} config + * @param {import('.').IPNS} config.ipns + * @param {import('.').Options} [config.options] + */ +module.exports = ({ ipns, options: routingOptions }) => { /** * Show current name subscriptions. * - * @param {AbortOptions} [options] + * @param {import('.').AbortOptions} [options] * @returns {Promise} * @example * ```js @@ -17,13 +22,9 @@ module.exports = ({ ipns, options: constructorOptions }) => { * ``` */ async function subs (options) { // eslint-disable-line require-await - const pubsub = getPubsubRouting(ipns, constructorOptions) + const pubsub = getPubsubRouting(ipns, routingOptions) return pubsub.getSubscriptions(options) } return withTimeoutOption(subs) } - -/** - * @typedef {import('../../../utils').AbortOptions} AbortOptions - */ diff --git a/packages/ipfs-core/src/components/name/pubsub/utils.js b/packages/ipfs-core/src/components/name/pubsub/utils.js index ee53a96f9c..43e7e69ae9 100644 --- a/packages/ipfs-core/src/components/name/pubsub/utils.js +++ b/packages/ipfs-core/src/components/name/pubsub/utils.js @@ -3,9 +3,14 @@ const IpnsPubsubDatastore = require('../../../ipns/routing/pubsub-datastore') const errcode = require('err-code') -// Get pubsub from IPNS routing +/** + * Get pubsub from IPNS routing + * + * @param {import('.').IPNS} ipns + * @param {PubSubRoutingOptions} [options] + */ exports.getPubsubRouting = (ipns, options) => { - if (!ipns || !(options.EXPERIMENTAL && options.EXPERIMENTAL.ipnsPubsub)) { + if (!ipns || !(options && options.ipnsPubsub)) { throw errcode(new Error('IPNS pubsub subsystem is not enabled'), 'ERR_IPNS_PUBSUB_NOT_ENABLED') } @@ -23,3 +28,8 @@ exports.getPubsubRouting = (ipns, options) => { return pubsub } + +/** + * @typedef {Object} PubSubRoutingOptions + * @property {boolean} [ipnsPubsub] - Enable pub-sub on IPNS. (Default: `false`) + */ diff --git a/packages/ipfs-core/src/components/name/resolve.js b/packages/ipfs-core/src/components/name/resolve.js index 0589a28dfa..0f747a4a7c 100644 --- a/packages/ipfs-core/src/components/name/resolve.js +++ b/packages/ipfs-core/src/components/name/resolve.js @@ -2,7 +2,7 @@ const debug = require('debug') const errcode = require('err-code') -const mergeOptions = require('merge-options') +const { mergeOptions } = require('../../utils') const CID = require('cids') const isDomain = require('is-domain-name') @@ -28,18 +28,18 @@ const appendRemainder = (result, remainder) => * IPNS - Inter-Planetary Naming System * * @param {Object} config - * @param {import('../index').DNS} config.dns - * @param {import('../../ipns')} config.ipns - * @param {import('peer-id')} config.peerId - * @param {import('../index').IsOnline} config.isOnline - * @param {{offline?:boolean}} config.options + * @param {import('.').DNS} config.dns + * @param {import('.').IPNS} config.ipns + * @param {import('.').PeerId} config.peerId + * @param {import('.').IsOnline} config.isOnline + * @param {ResolveOptions} config.options */ -module.exports = ({ dns, ipns, peerId, isOnline, options: constructorOptions }) => { +module.exports = ({ dns, ipns, peerId, isOnline, options: { offline } }) => { /** * Given a key, query the DHT for its best value. * * @param {string} name - ipns name to resolve. Defaults to your node's peerID. - * @param {ResolveOptions} [options] + * @param {Options & AbortOptions} [options] * @returns {AsyncIterable} * @example * ```js @@ -58,8 +58,6 @@ module.exports = ({ dns, ipns, peerId, isOnline, options: constructorOptions }) recursive: true }, options) - const { offline } = constructorOptions - // TODO: params related logic should be in the core implementation if (offline && options && options.nocache) { throw errcode(new Error('cannot specify both offline and nocache'), 'ERR_NOCACHE_AND_OFFLINE') @@ -104,11 +102,12 @@ module.exports = ({ dns, ipns, peerId, isOnline, options: constructorOptions }) /** * IPFS resolve options. * - * @typedef {ResolveSettings & AbortOptions} ResolveOptions - * - * @typedef {Object} ResolveSettings + * @typedef {Object} Options * @property {boolean} [options.nocache=false] - do not use cached entries. * @property {boolean} [options.recursive=true] - resolve until the result is not an IPNS name. * - * @typedef {import('../../utils').AbortOptions} AbortOptions + * @typedef {Object} ResolveOptions + * @property {boolean} [offline] + * + * @typedef {import('.').AbortOptions} AbortOptions */ diff --git a/packages/ipfs-core/src/components/name/utils.js b/packages/ipfs-core/src/components/name/utils.js index acfb307fbd..b2b5970f78 100644 --- a/packages/ipfs-core/src/components/name/utils.js +++ b/packages/ipfs-core/src/components/name/utils.js @@ -2,14 +2,26 @@ const isIPFS = require('is-ipfs') -// resolves the given path by parsing out protocol-specific entries -// (e.g. /ipns/) and then going through the /ipfs/ entries and returning the final node -exports.resolvePath = ({ ipns, dag }, name) => { +/** + * resolves the given path by parsing out protocol-specific entries + * (e.g. /ipns/) and then going through the /ipfs/ entries and returning the final node + * + * @param {Object} context + * @param {IPNS} context.ipns + * @param {DagReader} context.dagReader + * @param {string} name + */ +exports.resolvePath = ({ ipns, dagReader }, name) => { // ipns path if (isIPFS.ipnsPath(name)) { return ipns.resolve(name) } // ipfs path - return dag.get(name.substring('/ipfs/'.length)) + return dagReader.get(name.substring('/ipfs/'.length)) } + +/** + * @typedef {import('.').DagReader} DagReader + * @typedef {import('.').IPNS} IPNS + */ diff --git a/packages/ipfs-core/src/components/network.js b/packages/ipfs-core/src/components/network.js new file mode 100644 index 0000000000..8da890a3d5 --- /dev/null +++ b/packages/ipfs-core/src/components/network.js @@ -0,0 +1,123 @@ +'use strict' + +const IPFSBitswap = require('ipfs-bitswap') +const createLibP2P = require('./libp2p') +const Multiaddr = require('multiaddr') +const errCode = require('err-code') + +class Network { + /** + * @param {PeerId} peerId + * @param {LibP2P} libp2p + * @param {BitSwap} bitswap + */ + constructor (peerId, libp2p, bitswap) { + this.peerId = peerId + this.libp2p = libp2p + this.bitswap = bitswap + } + + /** + * @param {Options} options + */ + static async start ({ peerId, repo, print, options }) { + // Need to ensure that repo is open as it could have been closed between + // `init` and `start`. + if (repo.closed) { + await repo.open() + } + + const config = await repo.config.getAll() + + const libp2p = createLibP2P({ + options, + repo, + peerId, + multiaddrs: readAddrs(peerId, config), + config, + keychainConfig: undefined + }) + + if (libp2p.keychain) { + await libp2p.loadKeychain() + } + + await libp2p.start() + + for (const ma of libp2p.transportManager.getAddrs()) { + print(`Swarm listening on ${ma}/p2p/${peerId.toB58String()}`) + } + + const bitswap = new IPFSBitswap(libp2p, repo.blocks, { statsEnabled: true }) + await bitswap.start() + + return new Network(peerId, libp2p, bitswap) + } + + /** + * @param {Network} network + */ + static async stop (network) { + await Promise.all([ + network.bitswap.stop(), + network.libp2p.stop() + ]) + } +} +module.exports = Network + +/** + * + * @param {PeerId} peerId + * @param {IPFSConfig} config + * @returns {Multiaddr[]} + */ +const readAddrs = (peerId, config) => { + const peerIdStr = peerId.toB58String() + /** @type {Multiaddr[]} */ + const addrs = [] + const swarm = (config.Addresses && config.Addresses.Swarm) || [] + for (const addr of swarm) { + let ma = Multiaddr(addr) + + // Temporary error for users migrating using websocket-star multiaddrs for listenning on libp2p + // websocket-star support was removed from ipfs and libp2p + if (ma.protoCodes().includes(WEBSOCKET_STAR_PROTO_CODE)) { + throw errCode(new Error('websocket-star swarm addresses are not supported. See https://github.com/ipfs/js-ipfs/issues/2779'), 'ERR_WEBSOCKET_STAR_SWARM_ADDR_NOT_SUPPORTED') + } + + // multiaddrs that go via a signalling server or other intermediary (e.g. stardust, + // webrtc-star) can have the intermediary's peer ID in the address, so append our + // peer ID to the end of it + const maId = ma.getPeerId() + if (maId && maId !== peerIdStr) { + ma = ma.encapsulate(`/p2p/${peerIdStr}`) + } + + addrs.push(ma) + } + + return addrs +} + +const WEBSOCKET_STAR_PROTO_CODE = 479 +/** + * @typedef {Object} Online + * @property {LibP2P} libp2p + * @property {BitSwap} bitswap + * + * @typedef {Object} Options + * @property {PeerId} options.peerId + * @property {Repo} options.repo + * @property {Print} options.print + * @property {IPFSOptions} options.options + * + * @typedef {import('.').IPFSConfig} IPFSConfig + * @typedef {import('.').Options} IPFSOptions + * @typedef {import('.').Repo} Repo + * @typedef {import('.').Print} Print + * @typedef {import('.').LibP2P} LibP2P + * @typedef {import('../interface/bitswap').Bitswap} BitSwap + * @typedef {import('.').PeerId} PeerId + * @typedef {import('.').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/object/data.js b/packages/ipfs-core/src/components/object/data.js index 8e3bdfb84e..3245cc6f14 100644 --- a/packages/ipfs-core/src/components/object/data.js +++ b/packages/ipfs-core/src/components/object/data.js @@ -2,10 +2,29 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') +/** + * @param {Object} config + * @param {import('.').IPLD} config.ipld + * @param {import('.').Preload} config.preload + */ module.exports = ({ ipld, preload }) => { const get = require('./get')({ ipld, preload }) - return withTimeoutOption(async function data (multihash, options) { + + /** + * @param {import('.').CID} multihash + * @param {GetOptions & AbortOptions} options + * @returns {Promise} + */ + async function data (multihash, options) { const node = await get(multihash, options) return node.Data - }) + } + + return withTimeoutOption(data) } + +/** + * @typedef {import('cids')} CID + * @typedef {import('./get').GetOptions} GetOptions + * @typedef {import('.').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/object/get.js b/packages/ipfs-core/src/components/object/get.js index 669898b77e..51a0b975f1 100644 --- a/packages/ipfs-core/src/components/object/get.js +++ b/packages/ipfs-core/src/components/object/get.js @@ -5,6 +5,11 @@ const errCode = require('err-code') const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') const uint8ArrayFromString = require('uint8arrays/from-string') +/** + * @param {string|Uint8Array|CID} multihash + * @param {string} [enc] + * @returns {string|Uint8Array} + */ function normalizeMultihash (multihash, enc) { if (typeof multihash === 'string') { if (enc === 'base58' || !enc) { @@ -19,8 +24,18 @@ function normalizeMultihash (multihash, enc) { throw new Error('unsupported multihash') } +/** + * @param {Object} config + * @param {import('.').IPLD} config.ipld + * @param {import('.').Preload} config.preload + */ module.exports = ({ ipld, preload }) => { - return withTimeoutOption(async function get (multihash, options = {}) { // eslint-disable-line require-await + /** + * + * @param {CID} multihash + * @param {GetOptions & AbortOptions} [options] + */ + async function get (multihash, options = {}) { // eslint-disable-line require-await let mh, cid try { @@ -44,5 +59,16 @@ module.exports = ({ ipld, preload }) => { } return ipld.get(cid, { signal: options.signal }) - }) + } + + return withTimeoutOption(get) } + +/** + * @typedef {Object} GetOptions + * @property {boolean} [preload] + * @property {number} [cidVersion] + * @property {string} [enc] + * + * @typedef {import('.').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/object/index.js b/packages/ipfs-core/src/components/object/index.js new file mode 100644 index 0000000000..210ab36f3d --- /dev/null +++ b/packages/ipfs-core/src/components/object/index.js @@ -0,0 +1,39 @@ +'use strict' + +const createData = require('./data') +const createGet = require('./get') +const createLinks = require('./links') +const createNew = require('./new') +const createPut = require('./put') +const createStat = require('./stat') +const ObjectPatchAPI = require('./patch') + +class ObjectAPI { + /** + * @param {Object} config + * @param {IPLD} config.ipld + * @param {Preload} config.preload + * @param {GCLock} config.gcLock + * @param {Dag} config.dag + */ + constructor ({ ipld, preload, dag, gcLock }) { + this.data = createData({ ipld, preload }) + this.get = createGet({ ipld, preload }) + this.links = createLinks({ dag }) + this.new = createNew({ ipld, preload }) + this.put = createPut({ ipld, preload, gcLock }) + this.stat = createStat({ ipld, preload }) + this.patch = new ObjectPatchAPI({ ipld, preload, gcLock }) + } +} + +module.exports = ObjectAPI + +/** + * @typedef {import('..').IPLD} IPLD + * @typedef {import('..').Preload} Preload + * @typedef {import('..').GCLock} GCLock + * @typedef {import('..').Dag} Dag + * @typedef {import('..').CID} CID + * @typedef {import('..').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/object/links.js b/packages/ipfs-core/src/components/object/links.js index d6a7b8d67c..d0b3739945 100644 --- a/packages/ipfs-core/src/components/object/links.js +++ b/packages/ipfs-core/src/components/object/links.js @@ -5,6 +5,11 @@ const DAGLink = dagPB.DAGLink const CID = require('cids') const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') +/** + * @param {any} node + * @param {DAGLink[]} [links] + * @returns {DAGLink[]} + */ function findLinks (node, links = []) { for (const key in node) { const val = node[key] @@ -35,8 +40,17 @@ function findLinks (node, links = []) { return links } +/** + * @param {Object} config + * @param {import('.').Dag} config.dag + */ module.exports = ({ dag }) => { - return withTimeoutOption(async function links (multihash, options = {}) { + /** + * @param {CID} multihash + * @param {import('.').AbortOptions} options + * @returns {Promise} + */ + async function links (multihash, options = {}) { const cid = new CID(multihash) const result = await dag.get(cid, options) @@ -53,5 +67,7 @@ module.exports = ({ dag }) => { } throw new Error(`Cannot resolve links from codec ${cid.codec}`) - }) + } + + return withTimeoutOption(links) } diff --git a/packages/ipfs-core/src/components/object/new.js b/packages/ipfs-core/src/components/object/new.js index 315ae9efcb..00d9199278 100644 --- a/packages/ipfs-core/src/components/object/new.js +++ b/packages/ipfs-core/src/components/object/new.js @@ -6,8 +6,18 @@ const multicodec = require('multicodec') const Unixfs = require('ipfs-unixfs') const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') +/** + * @param {Object} config + * @param {import('.').IPLD} config.ipld + * @param {import('.').Preload} config.preload + */ module.exports = ({ ipld, preload }) => { - return withTimeoutOption(async function _new (options = {}) { + /** + * + * @param {NewOptions & AbortOptions} options + * @returns {Promise} + */ + async function _new (options = {}) { let data if (options.template) { @@ -33,5 +43,19 @@ module.exports = ({ ipld, preload }) => { } return cid - }) + } + + return withTimeoutOption(_new) } + +/** + * @typedef {Object} NewOptions + * @property {string} [template] + * @property {boolean} [recursive] + * @property {boolean} [nocache] + * @property {boolean} [preload] + * @property {string} [enc] + * + * @typedef {import('.').CID} CID + * @typedef {import('.').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/object/patch/add-link.js b/packages/ipfs-core/src/components/object/patch/add-link.js index 123a92a39d..f46b5e53f4 100644 --- a/packages/ipfs-core/src/components/object/patch/add-link.js +++ b/packages/ipfs-core/src/components/object/patch/add-link.js @@ -2,13 +2,21 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') +/** + * @param {Object} config + * @param {import('.').IPLD} config.ipld + * @param {import('.').Preload} config.preload + * @param {import('.').GCLock} config.gcLock + */ module.exports = ({ ipld, gcLock, preload }) => { const get = require('../get')({ ipld, preload }) const put = require('../put')({ ipld, gcLock, preload }) - return withTimeoutOption(async function addLink (multihash, link, options) { + async function addLink (multihash, link, options) { const node = await get(multihash, options) node.addLink(link) return put(node, options) - }) + } + + return withTimeoutOption(addLink) } diff --git a/packages/ipfs-core/src/components/object/patch/append-data.js b/packages/ipfs-core/src/components/object/patch/append-data.js index 3ddd9d700e..b2d6b07c0d 100644 --- a/packages/ipfs-core/src/components/object/patch/append-data.js +++ b/packages/ipfs-core/src/components/object/patch/append-data.js @@ -4,13 +4,20 @@ const { DAGNode } = require('ipld-dag-pb') const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') const uint8ArrayConcat = require('uint8arrays/concat') +/** + * @param {Object} config + * @param {import('.').IPLD} config.ipld + * @param {import('.').Preload} config.preload + * @param {import('.').GCLock} config.gcLock + */ module.exports = ({ ipld, gcLock, preload }) => { const get = require('../get')({ ipld, preload }) const put = require('../put')({ ipld, gcLock, preload }) - - return withTimeoutOption(async function appendData (multihash, data, options) { + async function appendData (multihash, data, options) { const node = await get(multihash, options) const newData = uint8ArrayConcat([node.Data, data]) return put(new DAGNode(newData, node.Links), options) - }) + } + + return withTimeoutOption(appendData) } diff --git a/packages/ipfs-core/src/components/object/patch/index.js b/packages/ipfs-core/src/components/object/patch/index.js new file mode 100644 index 0000000000..5166c6b33e --- /dev/null +++ b/packages/ipfs-core/src/components/object/patch/index.js @@ -0,0 +1,30 @@ +'use strict' + +const createAddLink = require('./add-link') +const createAppendData = require('./append-data') +const createRmLink = require('./rm-link') +const createSetData = require('./set-data') + +class ObjectPatchAPI { + /** + * @param {Object} config + * @param {IPLD} config.ipld + * @param {Preload} config.preload + * @param {GCLock} config.gcLock + */ + constructor ({ ipld, preload, gcLock }) { + this.addLink = createAddLink({ ipld, preload, gcLock }) + this.appendData = createAppendData({ ipld, preload, gcLock }) + this.rmLink = createRmLink({ ipld, preload, gcLock }) + this.setData = createSetData({ ipld, preload, gcLock }) + } +} +module.exports = ObjectPatchAPI + +/** + * @typedef {import('..').IPLD} IPLD + * @typedef {import('..').Preload} Preload + * @typedef {import('..').GCLock} GCLock + * @typedef {import('..').CID} CID + * @typedef {import('..').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/object/patch/rm-link.js b/packages/ipfs-core/src/components/object/patch/rm-link.js index ed24d8d168..3edf78ac4b 100644 --- a/packages/ipfs-core/src/components/object/patch/rm-link.js +++ b/packages/ipfs-core/src/components/object/patch/rm-link.js @@ -2,13 +2,21 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') +/** + * @param {Object} config + * @param {import('.').IPLD} config.ipld + * @param {import('.').Preload} config.preload + * @param {import('.').GCLock} config.gcLock + */ module.exports = ({ ipld, gcLock, preload }) => { const get = require('../get')({ ipld, preload }) const put = require('../put')({ ipld, gcLock, preload }) - return withTimeoutOption(async function rmLink (multihash, linkRef, options) { + async function rmLink (multihash, linkRef, options) { const node = await get(multihash, options) node.rmLink(linkRef.Name || linkRef.name) return put(node, options) - }) + } + + return withTimeoutOption(rmLink) } diff --git a/packages/ipfs-core/src/components/object/patch/set-data.js b/packages/ipfs-core/src/components/object/patch/set-data.js index e556a8ea3c..1777ed2a59 100644 --- a/packages/ipfs-core/src/components/object/patch/set-data.js +++ b/packages/ipfs-core/src/components/object/patch/set-data.js @@ -3,12 +3,20 @@ const { DAGNode } = require('ipld-dag-pb') const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') +/** + * @param {Object} config + * @param {import('.').IPLD} config.ipld + * @param {import('.').Preload} config.preload + * @param {import('.').GCLock} config.gcLock + */ module.exports = ({ ipld, gcLock, preload }) => { const get = require('../get')({ ipld, preload }) const put = require('../put')({ ipld, gcLock, preload }) - return withTimeoutOption(async function setData (multihash, data, options) { + async function setData (multihash, data, options) { const node = await get(multihash, options) return put(new DAGNode(data, node.Links), options) - }) + } + + return withTimeoutOption(setData) } diff --git a/packages/ipfs-core/src/components/object/put.js b/packages/ipfs-core/src/components/object/put.js index 65cf70fcb9..6afbfbde2c 100644 --- a/packages/ipfs-core/src/components/object/put.js +++ b/packages/ipfs-core/src/components/object/put.js @@ -46,8 +46,20 @@ function parseProtoBuffer (buf) { return dagPB.util.deserialize(buf) } +/** + * @param {Object} config + * @param {import('.').IPLD} config.ipld + * @param {import('.').Preload} config.preload + * @param {import('.').GCLock} config.gcLock + */ module.exports = ({ ipld, gcLock, preload }) => { - return withTimeoutOption(async function put (obj, options = {}) { + /** + * + * @param {Uint8Array|DAGNode|{ Data: any, links: DAGLink[]}} obj + * @param {PutOptions & AbortOptions} options + * @returns {Promise} + */ + async function put (obj, options = {}) { const encoding = options.enc let node @@ -82,5 +94,16 @@ module.exports = ({ ipld, gcLock, preload }) => { } finally { release() } - }) + } + + return withTimeoutOption(put) } + +/** + * @typedef {Object} PutOptions + * @property {boolean} [preload] + * @property {string} [enc] + * + * @typedef {import('.').CID} CID + * @typedef {import('.').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/object/stat.js b/packages/ipfs-core/src/components/object/stat.js index 699c9b4d3e..666967c0be 100644 --- a/packages/ipfs-core/src/components/object/stat.js +++ b/packages/ipfs-core/src/components/object/stat.js @@ -3,9 +3,22 @@ const dagPB = require('ipld-dag-pb') const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') +/** + * @param {Object} config + * @param {import('.').IPLD} config.ipld + * @param {import('.').Preload} config.preload + */ module.exports = ({ ipld, preload }) => { const get = require('./get')({ ipld, preload }) - return withTimeoutOption(async function stat (multihash, options = {}) { + + /** + * Returns stats about an Object + * + * @param {CID} multihash + * @param {StatOptions & AbortOptions} options + * @returns {Promise} + */ + async function stat (multihash, options = {}) { const node = await get(multihash, options) const serialized = dagPB.util.serialize(node) const cid = await dagPB.util.cid(serialized, { @@ -23,5 +36,20 @@ module.exports = ({ ipld, preload }) => { DataSize: node.Data.length, CumulativeSize: blockSize + linkLength } - }) + } + + return withTimeoutOption(stat) } +/** + * @typedef {Object} Stat + * @property {string} Hash + * @property {number} NumLinks + * @property {number} BlockSize + * @property {number} LinksSize + * @property {number} DataSize + * @property {number} CumulativeSize + * + * @typedef {import('./get').GetOptions} StatOptions + * @typedef {import('.').CID} CID + * @typedef {import('.').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/pin/add-all.js b/packages/ipfs-core/src/components/pin/add-all.js index 09892c24e6..cf6bdf6e69 100644 --- a/packages/ipfs-core/src/components/pin/add-all.js +++ b/packages/ipfs-core/src/components/pin/add-all.js @@ -10,13 +10,12 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') const normaliseInput = require('ipfs-core-utils/src/pins/normalise-input') /** - * * @param {Object} config - * @param {import('..').GCLock} config.gcLock - * @param {import('..').DAG} config.dag - * @param {import('./pin-manager')} config.pinManager + * @param {import('.').GCLock} config.gcLock + * @param {import('.').DagReader} config.dagReader + * @param {import('.').PinManager} config.pinManager */ -module.exports = ({ pinManager, gcLock, dag }) => { +module.exports = ({ pinManager, gcLock, dagReader }) => { /** * Adds multiple IPFS objects to the pinset and also stores it to the IPFS * repo. pinset is the set of hashes currently pinned (not gc'able) @@ -40,7 +39,7 @@ module.exports = ({ pinManager, gcLock, dag }) => { */ const pinAdd = async function * () { for await (const { path, recursive, metadata } of normaliseInput(source)) { - const cid = await resolvePath(dag, path) + const cid = await resolvePath(dagReader, path) // verify that each hash can be pinned const { reason } = await pinManager.isPinnedWithType(cid, [PinTypes.recursive, PinTypes.direct]) @@ -90,9 +89,9 @@ module.exports = ({ pinManager, gcLock, dag }) => { * @typedef {Object} AddSettings * @property {boolean} [lock] * - * @typedef {import('../../utils').AbortOptions} AbortOptions + * @typedef {import('.').AbortOptions} AbortOptions * - * @typedef {import('..').CID} CID + * @typedef {import('.').CID} CID */ /** diff --git a/packages/ipfs-core/src/components/pin/index.js b/packages/ipfs-core/src/components/pin/index.js new file mode 100644 index 0000000000..2e3ee7c66b --- /dev/null +++ b/packages/ipfs-core/src/components/pin/index.js @@ -0,0 +1,35 @@ +'use strict' + +const createAdd = require('./add') +const createAddAll = require('./add-all') +const createLs = require('./ls') +const createRm = require('./rm') +const createRmAll = require('./rm-all') + +class PinAPI { + /** + * @param {Object} config + * @param {GCLock} config.gcLock + * @param {DagReader} config.dagReader + * @param {PinManager} config.pinManager + */ + constructor ({ gcLock, dagReader, pinManager }) { + const addAll = createAddAll({ gcLock, dagReader, pinManager }) + this.addAll = addAll + this.add = createAdd({ addAll }) + const rmAll = createRmAll({ gcLock, dagReader, pinManager }) + this.rmAll = rmAll + this.rm = createRm({ rmAll }) + this.ls = createLs({ dagReader, pinManager }) + } +} +module.exports = PinAPI + +/** + * @typedef {import('..').Repo} Repo + * @typedef {import('..').GCLock} GCLock + * @typedef {import('..').DagReader} DagReader + * @typedef {import('..').PinManager} PinManager + * @typedef {import('..').AbortOptions} AbortOptions + * @typedef {import('..').CID} CID + */ diff --git a/packages/ipfs-core/src/components/pin/ls.js b/packages/ipfs-core/src/components/pin/ls.js index 4764e32d94..3fb486288a 100644 --- a/packages/ipfs-core/src/components/pin/ls.js +++ b/packages/ipfs-core/src/components/pin/ls.js @@ -22,10 +22,10 @@ function toPin (type, cid, metadata) { /** * @param {Object} config - * @param {import('./pin-manager')} config.pinManager - * @param {import('../index').DAG} config.dag + * @param {import('.').PinManager} config.pinManager + * @param {import('.').DagReader} config.dagReader */ -module.exports = ({ pinManager, dag }) => { +module.exports = ({ pinManager, dagReader }) => { /** * List all the objects pinned to local storage * @@ -74,7 +74,7 @@ module.exports = ({ pinManager, dag }) => { let matched = false for await (const { path } of normaliseInput(options.paths)) { - const cid = await resolvePath(dag, path) + const cid = await resolvePath(dagReader, path) const { reason, pinned, parent, metadata } = await pinManager.isPinnedWithType(cid, type) if (!pinned) { @@ -138,6 +138,6 @@ module.exports = ({ pinManager, dag }) => { * @typedef {import('./pin-manager').PinType} PinType * @typedef {import('./pin-manager').PinQueryType} PinQueryType * - * @typedef {import('../../utils').AbortOptions} AbortOptions - * @typedef {import('..').CID} CID + * @typedef {import('.').AbortOptions} AbortOptions + * @typedef {import('.').CID} CID */ diff --git a/packages/ipfs-core/src/components/pin/pin-manager.js b/packages/ipfs-core/src/components/pin/pin-manager.js index 9c99ee5d15..e284831ec2 100644 --- a/packages/ipfs-core/src/components/pin/pin-manager.js +++ b/packages/ipfs-core/src/components/pin/pin-manager.js @@ -49,14 +49,25 @@ const PinTypes = { } class PinManager { - constructor (repo, dag) { + /** + * @param {Object} config + * @param {import('.').Repo} config.repo + * @param {import('.').DagReader} config.dagReader + */ + constructor ({ repo, dagReader }) { this.repo = repo - this.dag = dag + this.dag = dagReader this.log = debug('ipfs:pin') this.directPins = new Set() this.recursivePins = new Set() } + /** + * @private + * @param {CID} cid + * @param {Object} options + * @param {boolean} [options.preload] + */ async * _walkDag (cid, { preload = false }) { const { value: node } = await this.dag.get(cid, { preload }) @@ -73,6 +84,11 @@ class PinManager { } } + /** + * @param {CID} cid + * @param {PinOptions & AbortOptions} [options] + * @returns {Promise} + */ async pinDirectly (cid, options = {}) { await this.dag.get(cid, options) @@ -95,10 +111,21 @@ class PinManager { return this.repo.pins.put(cidToKey(cid), cbor.encode(pin)) } - async unpin (cid) { // eslint-disable-line require-await + /** + * @param {CID} cid + * @param {AbortOptions} [options] + * @returns {Promise} + */ + // eslint-disable-next-line require-await + async unpin (cid, options) { return this.repo.pins.delete(cidToKey(cid)) } + /** + * @param {CID} cid + * @param {PreloadOptions & PinOptions & AbortOptions} [options] + * @returns {Promise} + */ async pinRecursively (cid, options = {}) { await this.fetchCompleteDag(cid, options) @@ -121,7 +148,11 @@ class PinManager { await this.repo.pins.put(cidToKey(cid), cbor.encode(pin)) } - async * directKeys () { + /** + * @param {AbortOptions} [options] + * @returns {AsyncIterable<{ cid: CID, metadata: any }>} + */ + async * directKeys (options) { for await (const entry of this.repo.pins.query({ filters: [(entry) => { const pin = cbor.decode(entry.value) @@ -141,7 +172,11 @@ class PinManager { } } - async * recursiveKeys () { + /** + * @param {AbortOptions} [options] + * @returns {AsyncIterable<{ cid: CID, metadata: any }>} + */ + async * recursiveKeys (options) { for await (const entry of this.repo.pins.query({ filters: [(entry) => { const pin = cbor.decode(entry.value) @@ -184,7 +219,12 @@ class PinManager { } } - async isPinnedWithType (cid, types) { + /** + * @param {CID} cid + * @param {PinQueryType|PinQueryType[]} types + * @param {AbortOptions} [options] + */ + async isPinnedWithType (cid, types, options) { if (!Array.isArray(types)) { types = [types] } @@ -256,18 +296,38 @@ class PinManager { } } + /** + * @param {CID} cid + * @param {PreloadOptions & AbortOptions} options + */ async fetchCompleteDag (cid, options) { await all(this._walkDag(cid, { preload: options.preload })) } - // Throws an error if the pin type is invalid + /** + * Throws an error if the pin type is invalid + * + * @param {any} type + * @returns {type is PinType} + */ static checkPinType (type) { if (typeof type !== 'string' || !Object.keys(PinTypes).includes(type)) { throw invalidPinTypeErr(type) } + return true } } PinManager.PinTypes = PinTypes module.exports = PinManager + +/** + * @typedef {Object} PinOptions + * @property {any} [metadata] + * + * @typedef {Object} PreloadOptions + * @property {boolean} [preload] + * + * @typedef {import('.').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/pin/rm-all.js b/packages/ipfs-core/src/components/pin/rm-all.js index e02d2e7bfb..2157acd407 100644 --- a/packages/ipfs-core/src/components/pin/rm-all.js +++ b/packages/ipfs-core/src/components/pin/rm-all.js @@ -7,11 +7,11 @@ const { PinTypes } = require('./pin-manager') /** * @param {Object} config - * @param {import('./pin-manager')} config.pinManager - * @param {import('..').GCLock} config.gcLock - * @param {import('..').DAG} config.dag + * @param {import('.').PinManager} config.pinManager + * @param {import('.').GCLock} config.gcLock + * @param {import('.').DagReader} config.dagReader */ -module.exports = ({ pinManager, gcLock, dag }) => { +module.exports = ({ pinManager, gcLock, dagReader }) => { /** * Unpin one or more blocks from your repo * @@ -36,7 +36,7 @@ module.exports = ({ pinManager, gcLock, dag }) => { try { // verify that each hash can be unpinned for await (const { path, recursive } of normaliseInput(source)) { - const cid = await resolvePath(dag, path) + const cid = await resolvePath(dagReader, path) const { pinned, reason } = await pinManager.isPinnedWithType(cid, PinTypes.all) if (!pinned) { @@ -73,7 +73,7 @@ module.exports = ({ pinManager, gcLock, dag }) => { } /** - * @typedef {import('..').CID} CID - * @typedef {import('../../utils').AbortOptions} AbortOptions + * @typedef {import('.').CID} CID + * @typedef {import('.').AbortOptions} AbortOptions * @typedef {import('./add-all').Source} Source */ diff --git a/packages/ipfs-core/src/components/ping.js b/packages/ipfs-core/src/components/ping.js index a3285be652..077986d66d 100644 --- a/packages/ipfs-core/src/components/ping.js +++ b/packages/ipfs-core/src/components/ping.js @@ -7,9 +7,9 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') /** * @param {Object} config - * @param {import('libp2p')} config.libp2p + * @param {import('.').NetworkService} config.network */ -module.exports = ({ libp2p }) => { +module.exports = ({ network }) => { /** * Send echo request packets to IPFS hosts. * @@ -28,6 +28,7 @@ module.exports = ({ libp2p }) => { * ``` */ async function * ping (peerId, options = {}) { + const { libp2p } = await network.use() options.count = options.count || 10 if (!PeerId.isPeerId(peerId)) { @@ -92,5 +93,5 @@ module.exports = ({ libp2p }) => { * @typedef {Object} PingSettings * @property {number} [count=10] - The number of ping messages to send * - * @typedef {import('../utils').AbortOptions} AbortOptions + * @typedef {import('.').AbortOptions} AbortOptions */ diff --git a/packages/ipfs-core/src/components/pubsub.js b/packages/ipfs-core/src/components/pubsub.js index 876594d5aa..db671706f0 100644 --- a/packages/ipfs-core/src/components/pubsub.js +++ b/packages/ipfs-core/src/components/pubsub.js @@ -2,18 +2,149 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') const errCode = require('err-code') +const { NotEnabledError } = require('../errors') +const get = require('dlv') + +/** + * @param {Object} config + * @param {import('.').NetworkService} config.network + * @param {import('.').IPFSConfig} [config.config] + */ +module.exports = ({ network, config }) => { + const isEnabled = get(config, 'Pubsub.Enabled', true) -module.exports = ({ libp2p }) => { return { - subscribe: withTimeoutOption((...args) => libp2p.pubsub.subscribe(...args)), - unsubscribe: withTimeoutOption((...args) => libp2p.pubsub.unsubscribe(...args)), - publish: withTimeoutOption(async (topic, data, _options) => { - if (!data) { - throw errCode(new Error('argument "data" is required'), 'ERR_ARG_REQUIRED') - } - await libp2p.pubsub.publish(topic, data) - }), - ls: withTimeoutOption((...args) => libp2p.pubsub.getTopics(...args)), - peers: withTimeoutOption((...args) => libp2p.pubsub.getSubscribers(...args)) + subscribe: isEnabled ? withTimeoutOption(subscribe) : notEnabled, + unsubscribe: isEnabled ? withTimeoutOption(unsubscribe) : notEnabled, + publish: isEnabled ? withTimeoutOption(publish) : notEnabled, + ls: isEnabled ? withTimeoutOption(ls) : notEnabled, + peers: isEnabled ? withTimeoutOption(peers) : notEnabled + } + + /** + * Subscribe to a pubsub topic. + * + * @example + * ```js + * const topic = 'fruit-of-the-day' + * const receiveMsg = (msg) => console.log(msg.data.toString()) + * + * await ipfs.pubsub.subscribe(topic, receiveMsg) + * console.log(`subscribed to ${topic}`) + * ``` + * + * @param {string} topic - The topic name + * @param {(message:Message) => void} handler - Event handler which will be + * called with a message object everytime one is received. + * @param {AbortOptions} [options] + * @returns {Promise} + */ + async function subscribe (topic, handler, options) { + const { libp2p } = await network.use(options) + return libp2p.pubsub.subscribe(topic, handler, options) + } + + /** + * Unsubscribes from a pubsub topic. + * + * @example + * ```js + * const topic = 'fruit-of-the-day' + * const receiveMsg = (msg) => console.log(msg.toString()) + * + * await ipfs.pubsub.subscribe(topic, receiveMsg) + * console.log(`subscribed to ${topic}`) + * + * await ipfs.pubsub.unsubscribe(topic, receiveMsg) + * console.log(`unsubscribed from ${topic}`) + * + * // Or removing all listeners: + * + * const topic = 'fruit-of-the-day' + * const receiveMsg = (msg) => console.log(msg.toString()) + * await ipfs.pubsub.subscribe(topic, receiveMsg); + * // Will unsubscribe ALL handlers for the given topic + * await ipfs.pubsub.unsubscribe(topic); + * ``` + * + * @param {string} topic - The topic to unsubscribe from + * @param {(message:Message) => void} [handler] - The handler to remove. If + * not provided unsubscribes al handlers for the topic. + * @param {AbortOptions} [options] + * @returns {Promise} + */ + async function unsubscribe (topic, handler, options) { + const { libp2p } = await network.use(options) + libp2p.pubsub.unsubscribe(topic, handler, options) + } + + /** + * Publish a data message to a pubsub topic. + * + * @example + * ```js + * const topic = 'fruit-of-the-day' + * const msg = new TextEncoder().encode('banana') + * + * await ipfs.pubsub.publish(topic, msg) + * // msg was broadcasted + * console.log(`published to ${topic}`) + * ``` + * + * @param {string} topic + * @param {Uint8Array} data + * @param {AbortOptions} options + * @returns {Promise} + */ + async function publish (topic, data, options) { + const { libp2p } = await network.use(options) + if (!data) { + throw errCode(new Error('argument "data" is required'), 'ERR_ARG_REQUIRED') + } + await libp2p.pubsub.publish(topic, data) + } + /** + * Returns the list of subscriptions the peer is subscribed to. + * + * @param {AbortOptions} [options] + * @returns {Promise} + */ + async function ls (options) { + const { libp2p } = await network.use(options) + return libp2p.pubsub.getTopics(options) + } + + /** + * Returns the peers that are subscribed to one topic. + * + * @example + * ```js + * const topic = 'fruit-of-the-day' + * + * const peerIds = await ipfs.pubsub.peers(topic) + * console.log(peerIds) + * ``` + * + * @param {string} topic + * @param {AbortOptions} [options] + * @returns {Promise} - An array of peer IDs subscribed to the topic + */ + async function peers (topic, options) { + const { libp2p } = await network.use(options) + return libp2p.pubsub.getSubscribers(topic, options) } } + +const notEnabled = async () => { // eslint-disable-line require-await + throw new NotEnabledError('pubsub not enabled') +} + +/** + * @typedef {Object} Message + * @property {string} from + * @property {Uint8Array} seqno + * @property {Uint8Array} data + * @property {string[]} topicIDs + * + * @typedef {import('.').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/refs/index.js b/packages/ipfs-core/src/components/refs/index.js index 15aee1a3d0..438532a74d 100644 --- a/packages/ipfs-core/src/components/refs/index.js +++ b/packages/ipfs-core/src/components/refs/index.js @@ -13,8 +13,21 @@ const Format = { edges: ' -> ' } +/** + * @param {Object} config + * @param {import('..').IPLD} config.ipld + * @param {import('..').Resolve} config.resolve + * @param {import('..').Preload} config.preload + */ module.exports = function ({ ipld, resolve, preload }) { - return withTimeoutOption(async function * refs (ipfsPath, options = {}) { // eslint-disable-line require-await + /** + * Get links (references) from an object + * + * @param {CID|string} ipfsPath - The object to search for references + * @param {RefsOptions & AbortOptions} [options] + * @returns {AsyncIterable} + */ + async function * refs (ipfsPath, options = {}) { if (options.maxDepth === 0) { return } @@ -29,13 +42,17 @@ module.exports = function ({ ipld, resolve, preload }) { options.maxDepth = options.recursive ? Infinity : 1 } + /** @type {(string|CID)[]} */ const rawPaths = Array.isArray(ipfsPath) ? ipfsPath : [ipfsPath] + const paths = rawPaths.map(p => getFullPath(preload, p, options)) for (const path of paths) { yield * refsStream(resolve, ipld, path, options) } - }) + } + + return withTimeoutOption(refs) } module.exports.Format = Format @@ -160,3 +177,17 @@ function getNodeLinks (node, path = '') { } return links } + +/** + * @typedef {Object} RefsOptions + * @property {boolean} [recursive=false] - Recursively list references of child nodes + * @property {boolean} [unique=false] - Omit duplicate references from output + * @property {string} [format=''] - Output edges with given format. Available tokens: ``, ``, `` + * @property {boolean} [edges=false] - output references in edge format: `" -> "` + * @property {number} [maxDepth=1] - only for recursive refs, limits fetch and listing to the given depth + * + * @typedef {{ref:string, err?:null}|{ref?:undefined, err:Error}} RefResult + * + * @typedef {import('..').AbortOptions} AbortOptions + * @typedef {import('..').Repo} Repo + */ diff --git a/packages/ipfs-core/src/components/refs/local.js b/packages/ipfs-core/src/components/refs/local.js index 92411408a1..0615b05a2d 100644 --- a/packages/ipfs-core/src/components/refs/local.js +++ b/packages/ipfs-core/src/components/refs/local.js @@ -2,10 +2,21 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') +/** + * @param {Object} config + * @param {import('.').Repo} config.repo + */ module.exports = function ({ repo }) { - return withTimeoutOption(async function * refsLocal (options = {}) { + /** + * @param {import('.').AbortOptions} [options] + * @returns {AsyncIterable<{ref: string}>} + */ + async function * refsLocal (options = {}) { + // @ts-ignore - TS is not aware of keysOnly for await (const cid of repo.blocks.query({ keysOnly: true, signal: options.signal })) { yield { ref: cid.toString() } } - }) + } + + return withTimeoutOption(refsLocal) } diff --git a/packages/ipfs-core/src/components/repo/gc.js b/packages/ipfs-core/src/components/repo/gc.js index 5a8bee05c8..327f156bea 100644 --- a/packages/ipfs-core/src/components/repo/gc.js +++ b/packages/ipfs-core/src/components/repo/gc.js @@ -16,10 +16,10 @@ const BLOCK_RM_CONCURRENCY = 256 * Perform mark and sweep garbage collection * * @param {Object} config - * @param {import('..').GCLock} config.gcLock - * @param {import('..').Pin} config.pin - * @param {import('..').Refs} config.refs - * @param {import('..').IPFSRepo} config.repo + * @param {import('.').GCLock} config.gcLock + * @param {import('.').Pin} config.pin + * @param {import('.').Refs} config.refs + * @param {import('.').Repo} config.repo */ module.exports = ({ gcLock, pin, refs, repo }) => { /** @@ -36,6 +36,7 @@ module.exports = ({ gcLock, pin, refs, repo }) => { // Mark all blocks that are being used const markedSet = await createMarkedSet({ pin, refs, repo }) // Get all blocks keys from the blockstore + // @ts-ignore - TS is not aware of keysOnly overload const blockKeys = repo.blocks.query({ keysOnly: true }) // Delete blocks that are not being used diff --git a/packages/ipfs-core/src/components/repo/index.js b/packages/ipfs-core/src/components/repo/index.js new file mode 100644 index 0000000000..82e4e401d5 --- /dev/null +++ b/packages/ipfs-core/src/components/repo/index.js @@ -0,0 +1,29 @@ +'use strict' + +const createGC = require('./gc') +const createStat = require('./stat') +const createVersion = require('./version') + +class RepoAPI { + /** + * @param {Object} config + * @param {GCLock} config.gcLock + * @param {Pin} config.pin + * @param {Repo} config.repo + * @param {Refs} config.refs + */ + constructor ({ gcLock, pin, repo, refs }) { + this.gc = createGC({ gcLock, pin, refs, repo }) + this.stat = createStat({ repo }) + this.version = createVersion({ repo }) + } +} +module.exports = RepoAPI + +/** + * @typedef {import('..').GCLock} GCLock + * @typedef {import('..').Pin} Pin + * @typedef {import('..').Repo} Repo + * @typedef {import('..').Refs} Refs + * @typedef {import('..').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/repo/stat.js b/packages/ipfs-core/src/components/repo/stat.js index 3cf268254a..612ec451fc 100644 --- a/packages/ipfs-core/src/components/repo/stat.js +++ b/packages/ipfs-core/src/components/repo/stat.js @@ -2,8 +2,15 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') +/** + * @param {Object} config + * @param {import('.').Repo} config.repo + */ module.exports = ({ repo }) => { - return withTimeoutOption(async function stat (options) { + /** + * @param {import('.').AbortOptions} [options] + */ + async function stat (options) { const stats = await repo.stat(options) return { @@ -13,5 +20,7 @@ module.exports = ({ repo }) => { version: stats.version.toString(), storageMax: stats.storageMax } - }) + } + + return withTimeoutOption(stat) } diff --git a/packages/ipfs-core/src/components/repo/version.js b/packages/ipfs-core/src/components/repo/version.js index 3d73e89045..25c6202595 100644 --- a/packages/ipfs-core/src/components/repo/version.js +++ b/packages/ipfs-core/src/components/repo/version.js @@ -3,15 +3,21 @@ const { repoVersion } = require('ipfs-repo') const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') +/** + * @param {Object} config + * @param {import('.').Repo} config.repo + */ module.exports = ({ repo }) => { /** * If the repo has been initialized, report the current version. * Otherwise report the version that would be initialized. * - * @returns {number} + * @param {import('.').AbortOptions} options + * @returns {Promise} */ - return withTimeoutOption(async function version (options) { + async function version (options) { try { + // @ts-ignore - not a public API await repo._checkInitialized(options) } catch (err) { // TODO: (dryajov) This is really hacky, there must be a better way @@ -30,5 +36,7 @@ module.exports = ({ repo }) => { } return repo.version.get(options) - }) + } + + return withTimeoutOption(version) } diff --git a/packages/ipfs-core/src/components/resolve.js b/packages/ipfs-core/src/components/resolve.js index 8e9672d6ab..3336e4d7cf 100644 --- a/packages/ipfs-core/src/components/resolve.js +++ b/packages/ipfs-core/src/components/resolve.js @@ -7,8 +7,8 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') /** * @param {Object} config - * @param {import('.').IPLD} config.ipld - An instance of IPLD - * @param {import('.').Name} [config.name] - An IPFS core interface name API + * @param {import('.').IPLD} config.ipld + * @param {import('.').Name} config.name - An IPFS core interface name API */ module.exports = ({ ipld, name }) => { /** @@ -102,5 +102,5 @@ module.exports = ({ ipld, name }) => { * @property {boolean} [recursive=true] - Resolve until result is an IPFS name. * @property {string} [cidBase='base58btc'] - Multibase codec name the CID in the resolved path will be encoded with. * - * @typedef {import('../utils').AbortOptions} AbortOptions + * @typedef {import('.').AbortOptions} AbortOptions */ diff --git a/packages/ipfs-core/src/components/root.js b/packages/ipfs-core/src/components/root.js new file mode 100644 index 0000000000..bb89644d4f --- /dev/null +++ b/packages/ipfs-core/src/components/root.js @@ -0,0 +1,45 @@ +'use strict' + +const createAddAPI = require('./add') +const createAddAllAPI = require('./add-all') +const createCatAPI = require('./cat') +const createGetAPI = require('./get') +const createLsAPI = require('./ls') + +class RootAPI { + /** + * @param {Object} config + * @param {Block} config.block + * @param {Pin} config.pin + * @param {GCLock} config.gcLock + * @param {Preload} config.preload + * @param {IPLD} config.ipld + * @param {ShardingOptions} [config.options] + */ + constructor ({ preload, gcLock, pin, block, ipld, options }) { + const addAll = createAddAllAPI({ + preload, + gcLock, + block, + pin, + options + }) + + this.addAll = addAll + this.add = createAddAPI({ addAll }) + this.cat = createCatAPI({ ipld, preload }) + this.get = createGetAPI({ ipld, preload }) + this.ls = createLsAPI({ ipld, preload }) + } +} + +module.exports = RootAPI + +/** + * @typedef {import('.').Block} Block + * @typedef {import('.').Pin} Pin + * @typedef {import('.').GCLock} GCLock + * @typedef {import('.').IPLD} IPLD + * @typedef {import('.').Preload} Preload + * @typedef {import('./add-all').ShardingOptions} ShardingOptions + */ diff --git a/packages/ipfs-core/src/components/start.js b/packages/ipfs-core/src/components/start.js index d421707ef4..a9d512f377 100644 --- a/packages/ipfs-core/src/components/start.js +++ b/packages/ipfs-core/src/components/start.js @@ -1,404 +1,37 @@ 'use strict' -const log = require('debug')('ipfs:components:start') -const Bitswap = require('ipfs-bitswap') -const multiaddr = require('multiaddr') -const get = require('dlv') -const defer = require('p-defer') -const errCode = require('err-code') -const IPNS = require('../ipns') -const routingConfig = require('../ipns/routing/config') -const { AlreadyInitializedError, NotEnabledError } = require('../errors') -const Components = require('./') -const createMfsPreload = require('../mfs-preload') -const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') - -const WEBSOCKET_STAR_PROTO_CODE = 479 +const Service = require('../utils/service') /** * @param {Object} config - * @param {APIManager} config.apiManager - * @param {StartOptions} config.options - * @param {IPFSBlockService} config.blockService - * @param {GCLock} config.gcLock - * @param {InitOptions} config.initOptions - * @param {IPLD} config.ipld - * @param {Keychain} config.keychain - * @param {PeerId} config.peerId - * @param {PinManager} config.pinManager - * @param {Preload} config.preload - * @param {Print} config.print - * @param {IPFSRepo} config.repo + * @param {import('.').NetworkService} config.network + * @param {import('.').PeerId} config.peerId + * @param {import('.').Repo} config.repo + * @param {import('.').BlockService} config.blockService + * @param {import('.').Print} config.print + * @param {import('.').Preload} config.preload + * @param {import('.').MFSPreload} config.mfsPreload + * @param {import('.').IPNS} config.ipns + * @param {import('.').Keychain} config.keychain + * @param {import('.').Options} config.options */ -module.exports = ({ - apiManager, - options: constructorOptions, - blockService, - gcLock, - initOptions, - ipld, - keychain, - peerId, - pinManager, - preload, - print, - repo -}) => { - async function start () { - const startPromise = defer() - startPromise.promise.catch((err) => log(err)) - - const { cancel } = apiManager.update({ start: () => startPromise.promise }) - - try { - // The repo may be closed if previously stopped - if (repo.closed) { - await repo.open() - } - - const config = await repo.config.getAll() - const addrs = [] - - if (config.Addresses && config.Addresses.Swarm) { - config.Addresses.Swarm.forEach(addr => { - let ma = multiaddr(addr) - - // Temporary error for users migrating using websocket-star multiaddrs for listenning on libp2p - // websocket-star support was removed from ipfs and libp2p - if (ma.protoCodes().includes(WEBSOCKET_STAR_PROTO_CODE)) { - throw errCode(new Error('websocket-star swarm addresses are not supported. See https://github.com/ipfs/js-ipfs/issues/2779'), 'ERR_WEBSOCKET_STAR_SWARM_ADDR_NOT_SUPPORTED') - } - - // multiaddrs that go via a signalling server or other intermediary (e.g. stardust, - // webrtc-star) can have the intermediary's peer ID in the address, so append our - // peer ID to the end of it - const maId = ma.getPeerId() - if (maId && maId !== peerId.toB58String()) { - ma = ma.encapsulate(`/p2p/${peerId.toB58String()}`) - } - - addrs.push(ma) - }) - } - - const libp2p = Components.libp2p({ - options: constructorOptions, - repo, - peerId: peerId, - multiaddrs: addrs, - config - }) - - libp2p.keychain && await libp2p.loadKeychain() - - await libp2p.start() - - libp2p.transportManager.getAddrs().forEach(ma => print(`Swarm listening on ${ma}/p2p/${peerId.toB58String()}`)) - - const ipnsRouting = routingConfig({ libp2p, repo, peerId, options: constructorOptions }) - const ipns = new IPNS(ipnsRouting, repo.datastore, peerId, keychain, { pass: initOptions.pass }) - const bitswap = new Bitswap(libp2p, repo.blocks, { statsEnabled: true }) - - await bitswap.start() - - blockService.setExchange(bitswap) - - const dag = { - get: Components.dag.get({ ipld, preload }), - resolve: Components.dag.resolve({ ipld, preload }), - tree: Components.dag.tree({ ipld, preload }), - // FIXME: resolve this circular dependency - get put () { - const put = Components.dag.put({ ipld, pin, gcLock, preload }) - Object.defineProperty(this, 'put', { value: put }) - return put - } - } - - const pinAddAll = Components.pin.addAll({ pinManager, gcLock, dag }) - const pinRmAll = Components.pin.rmAll({ pinManager, gcLock, dag }) - - const pin = { - add: Components.pin.add({ addAll: pinAddAll }), - addAll: pinAddAll, - ls: Components.pin.ls({ pinManager, dag }), - rm: Components.pin.rm({ rmAll: pinRmAll }), - rmAll: pinRmAll - } - - const block = { - get: Components.block.get({ blockService, preload }), - put: Components.block.put({ blockService, pin, gcLock, preload }), - rm: Components.block.rm({ blockService, gcLock, pinManager }), - stat: Components.block.stat({ blockService, preload }) - } - - const files = Components.files({ ipld, block, blockService, repo, preload, options: constructorOptions }) - const mfsPreload = createMfsPreload({ files, preload, options: constructorOptions.preload }) - - await Promise.all([ - ipns.republisher.start(), - preload.start(), - mfsPreload.start() - ]) - - const api = createApi({ - apiManager, - bitswap, - block, - blockService, - config, - constructorOptions, - dag, - files, - gcLock, - initOptions, - ipld, - ipns, - keychain, - libp2p, - mfsPreload, - peerId, - pin, - preload, - print, - repo - }) - - const { api: startedApi } = apiManager.update(api, () => undefined) - startPromise.resolve(startedApi) - return startedApi - } catch (err) { - cancel() - startPromise.reject(err) - throw err - } - } - return withTimeoutOption(start) -} - -/** - * @param {CreateAPIConfig} config - */ -function createApi ({ - apiManager, - bitswap, - block, - blockService, - config, - constructorOptions, - dag, - files, - gcLock, - initOptions, - ipld, - ipns, - keychain, - libp2p, - mfsPreload, - peerId, - pin, - preload, - print, - repo -}) { - const object = { - data: Components.object.data({ ipld, preload }), - get: Components.object.get({ ipld, preload }), - links: Components.object.links({ dag }), - new: Components.object.new({ ipld, preload }), - patch: { - addLink: Components.object.patch.addLink({ ipld, gcLock, preload }), - appendData: Components.object.patch.appendData({ ipld, gcLock, preload }), - rmLink: Components.object.patch.rmLink({ ipld, gcLock, preload }), - setData: Components.object.patch.setData({ ipld, gcLock, preload }) - }, - put: Components.object.put({ ipld, gcLock, preload }), - stat: Components.object.stat({ ipld, preload }) - } - - const addAll = Components.addAll({ block, preload, pin, gcLock, options: constructorOptions }) - const isOnline = Components.isOnline({ libp2p }) - - const dhtNotEnabled = async () => { // eslint-disable-line require-await - throw new NotEnabledError('dht not enabled') - } - - const dhtNotEnabledIterator = async function * () { // eslint-disable-line require-await,require-yield - throw new NotEnabledError('dht not enabled') - } - - const dht = get(libp2p, '_config.dht.enabled', false) ? Components.dht({ libp2p, repo }) : { - get: dhtNotEnabled, - put: dhtNotEnabled, - findProvs: dhtNotEnabledIterator, - findPeer: dhtNotEnabled, - provide: dhtNotEnabledIterator, - query: dhtNotEnabledIterator - } - - const dns = Components.dns() - const name = { - pubsub: { - cancel: Components.name.pubsub.cancel({ ipns, options: constructorOptions }), - state: Components.name.pubsub.state({ ipns, options: constructorOptions }), - subs: Components.name.pubsub.subs({ ipns, options: constructorOptions }) - }, - publish: Components.name.publish({ ipns, dag, peerId, isOnline, keychain }), - resolve: Components.name.resolve({ dns, ipns, peerId, isOnline, options: constructorOptions }) - } - const resolve = Components.resolve({ name, ipld }) - const refs = Object.assign( - Components.refs({ ipld, resolve, preload }), - { local: Components.refs.local({ repo }) } - ) - - const pubsubNotEnabled = async () => { // eslint-disable-line require-await - throw new NotEnabledError('pubsub not enabled') - } - - const pubsub = get(constructorOptions, 'config.Pubsub.Enabled', get(config, 'Pubsub.Enabled', true)) - ? Components.pubsub({ libp2p }) - : { - subscribe: pubsubNotEnabled, - unsubscribe: pubsubNotEnabled, - publish: pubsubNotEnabled, - ls: pubsubNotEnabled, - peers: pubsubNotEnabled - } - - const api = { - add: Components.add({ addAll }), - addAll, - bitswap: { - stat: Components.bitswap.stat({ bitswap }), - unwant: Components.bitswap.unwant({ bitswap }), - wantlist: Components.bitswap.wantlist({ bitswap }), - wantlistForPeer: Components.bitswap.wantlistForPeer({ bitswap }) - }, - block, - bootstrap: { - add: Components.bootstrap.add({ repo }), - clear: Components.bootstrap.clear({ repo }), - list: Components.bootstrap.list({ repo }), - reset: Components.bootstrap.reset({ repo }), - rm: Components.bootstrap.rm({ repo }) - }, - cat: Components.cat({ ipld, preload }), - config: Components.config({ repo }), - dag, - dht, - dns, - files, - get: Components.get({ ipld, preload }), - id: Components.id({ peerId, libp2p }), - init: async () => { throw new AlreadyInitializedError() }, // eslint-disable-line require-await - isOnline, - ipld, - key: { - export: Components.key.export({ keychain }), - gen: Components.key.gen({ keychain }), - import: Components.key.import({ keychain }), - info: Components.key.info({ keychain }), - list: Components.key.list({ keychain }), - rename: Components.key.rename({ keychain }), - rm: Components.key.rm({ keychain }) - }, - libp2p, - ls: Components.ls({ ipld, preload }), - name, - object, - pin, - ping: Components.ping({ libp2p }), - pubsub, - refs, - repo: { - gc: Components.repo.gc({ gcLock, pin, refs, repo }), - stat: Components.repo.stat({ repo }), - version: Components.repo.version({ repo }) - }, - resolve, - start: () => apiManager.api, - stats: { - bitswap: Components.bitswap.stat({ bitswap }), - bw: libp2p.metrics - ? Components.stats.bw({ libp2p }) - : async () => { // eslint-disable-line require-await - throw new NotEnabledError('libp2p metrics not enabled') - }, - repo: Components.repo.stat({ repo }) - }, - stop: Components.stop({ - apiManager, - bitswap, - options: constructorOptions, - blockService, - gcLock, - initOptions, - ipld, - ipns, - keychain, - libp2p, - mfsPreload, +module.exports = ({ network, preload, peerId, keychain, repo, ipns, blockService, mfsPreload, print, options }) => { + const start = async () => { + const { bitswap, libp2p } = await Service.start(network, { peerId, - preload, + repo, print, - repo - }), - swarm: { - addrs: Components.swarm.addrs({ libp2p }), - connect: Components.swarm.connect({ libp2p }), - disconnect: Components.swarm.disconnect({ libp2p }), - localAddrs: Components.swarm.localAddrs({ multiaddrs: libp2p.multiaddrs }), - peers: Components.swarm.peers({ libp2p }) - }, - version: Components.version({ repo }) + options + }) + + blockService.setExchange(bitswap) + + await Promise.all([ + ipns.startOnline({ keychain, libp2p, peerId, repo }), + preload.start(), + mfsPreload.start() + ]) } - return api + return start } - -/** - * @typedef {Object} CreateAPIConfig - * @property {APIManager} apiManager - * @property {Bitswap} [bitswap] - * @property {Block} block - * @property {IPFSBlockService} blockService - * @property {Config} config - * @property {StartOptions} constructorOptions - * @property {DAG} dag - * @property {Files} [files] - * @property {GCLock} gcLock - * @property {InitOptions} initOptions - * @property {IPLD} ipld - * @property {import('../ipns')} ipns - * @property {Keychain} keychain - * @property {LibP2P} libp2p - * @property {MFSPreload} mfsPreload - * @property {PeerId} peerId - * @property {Pin} pin - * @property {Preload} preload - * @property {Print} print - * @property {IPFSRepo} repo - * - * @typedef {(...args:any[]) => void} Print - * - * @typedef {import('./init').InitOptions} InitOptions - * @typedef {import('./init').ConstructorOptions} StartOptions - * @typedef {import('./init').Keychain} Keychain - * @typedef {import('../api-manager')} APIManager - * @typedef {import('./pin/pin-manager')} PinManager - * @typedef {import('../mfs-preload').MFSPreload} MFSPreload - * @typedef {import('.').IPFSBlockService} IPFSBlockService - * @typedef {import('.').GCLock} GCLock - * @typedef {import('.')} IPLD - * @typedef {import('.').PeerId} PeerId - * @typedef {import('.').Preload} Preload - * @typedef {import('.').IPFSRepo} IPFSRepo - * @typedef {import('.').LibP2P} LibP2P - * @typedef {import('.').Pin} Pin - * @typedef {import('.').Files} Files - * @typedef {import('.').DAG} DAG - * @typedef {import('.').Config} Config - * @typedef {import('.').Block} Block - */ diff --git a/packages/ipfs-core/src/components/stats/bw.js b/packages/ipfs-core/src/components/stats/bw.js index cb5b13fe7a..5634bf029c 100644 --- a/packages/ipfs-core/src/components/stats/bw.js +++ b/packages/ipfs-core/src/components/stats/bw.js @@ -5,6 +5,11 @@ const parseDuration = require('parse-duration').default const errCode = require('err-code') const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') +/** + * @param {LibP2P} libp2p + * @param {BWOptions} opts + * @returns {BandwidthInfo} + */ function getBandwidthStats (libp2p, opts) { let stats @@ -35,17 +40,30 @@ function getBandwidthStats (libp2p, opts) { } } -module.exports = ({ libp2p }) => { - return withTimeoutOption(async function * (options = {}) { +/** + * @param {Object} config + * @param {import('.').NetworkService} config.network + */ +module.exports = ({ network }) => { + /** + * Get IPFS bandwidth information + * + * @param {BWOptions & AbortOptions} options + * @returns {AsyncIterable} + */ + const bw = async function * (options = {}) { + const { libp2p } = await network.use(options) + if (!options.poll) { yield getBandwidthStats(libp2p, options) return } - let interval = options.interval || 1000 + const interval = options.interval || 1000 + let ms = -1 try { - interval = typeof interval === 'string' ? parseDuration(interval) : interval - if (!interval || interval < 0) throw new Error('invalid duration') + ms = typeof interval === 'string' ? parseDuration(interval) || -1 : interval + if (!ms || ms < 0) throw new Error('invalid duration') } catch (err) { throw errCode(err, 'ERR_INVALID_POLL_INTERVAL') } @@ -55,10 +73,31 @@ module.exports = ({ libp2p }) => { while (true) { yield getBandwidthStats(libp2p, options) // eslint-disable-next-line no-loop-func - await new Promise(resolve => { timeoutId = setTimeout(resolve, interval) }) + await new Promise(resolve => { timeoutId = setTimeout(resolve, ms) }) } } finally { clearTimeout(timeoutId) } - }) + } + + return withTimeoutOption(bw) } + +/** + * @typedef {Object} BWOptions + * @property {PeerId|CID|string} [peer] - Specifies a peer to print bandwidth for + * @property {string} [proto] - Specifies a protocol to print bandwidth for + * @property {boolean} [poll] - Is used to yield bandwidth info at an interval + * @property {number|string} [interval=1000] - The time interval to wait between updating output, if `poll` is `true`. + * + * @typedef {Object} BandwidthInfo + * @property {Big} totalIn + * @property {Big} totalOut + * @property {Big} rateIn + * @property {Big} rateOut + * + * @typedef {import('.').LibP2P} LibP2P + * @typedef {import('.').PeerId} PeerId + * @typedef {import('.').CID} CID + * @typedef {import('.').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/stats/index.js b/packages/ipfs-core/src/components/stats/index.js new file mode 100644 index 0000000000..6760c21853 --- /dev/null +++ b/packages/ipfs-core/src/components/stats/index.js @@ -0,0 +1,29 @@ +'use strict' + +const createBW = require('./bw') +const createRepo = require('../repo/stat') +const createBitswap = require('../bitswap/stat') + +class StatsAPI { + /** + * @param {Object} config + * @param {Repo} config.repo + * @param {NetworkService} config.network + */ + constructor ({ repo, network }) { + this.repo = createRepo({ repo }) + this.bw = createBW({ network }) + this.bitswap = createBitswap({ network }) + } +} + +module.exports = StatsAPI + +/** + * @typedef {import('..').Repo} Repo + * @typedef {import('..').PeerId} PeerId + * @typedef {import('..').LibP2P} LibP2P + * @typedef {import('..').CID} CID + * @typedef {import('..').NetworkService} NetworkService + * @typedef {import('..').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/stop.js b/packages/ipfs-core/src/components/stop.js index 10d03a8809..5ff0412014 100644 --- a/packages/ipfs-core/src/components/stop.js +++ b/packages/ipfs-core/src/components/stop.js @@ -1,232 +1,27 @@ 'use strict' -const defer = require('p-defer') -const { NotStartedError, AlreadyInitializedError } = require('../errors') -const Components = require('./') -const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') +const Service = require('../utils/service') -module.exports = ({ - apiManager, - options: constructorOptions, - bitswap, - blockService, - gcLock, - initOptions, - ipld, - ipns, - keychain, - libp2p, - mfsPreload, - peerId, - pinManager = {}, - preload, - print, - repo -}) => { - /** - * Stops the IPFS node and in case of talking with an IPFS Daemon, it stops - * the process. - * - * @param {AbortOptions} _options - * @returns {Promise} - * @example - * ```js - * await ipfs.stop() - * ``` - */ - async function stop (_options) { - const stopPromise = defer() - const { cancel } = apiManager.update({ stop: () => stopPromise.promise }) - - try { - blockService.unsetExchange() - bitswap.stop() - preload.stop() - - await Promise.all([ - ipns.republisher.stop(), - mfsPreload.stop(), - libp2p.stop(), - repo.close() - ]) - - const api = createApi({ - apiManager, - constructorOptions, - blockService, - gcLock, - initOptions, - ipld, - keychain, - peerId, - pinManager, - preload, - print, - repo - }) - - apiManager.update(api, () => { throw new NotStartedError() }) - } catch (err) { - cancel() - stopPromise.reject(err) - throw err - } - - stopPromise.resolve() - } - - return withTimeoutOption(stop) -} - -function createApi ({ - apiManager, - constructorOptions, - blockService, - gcLock, - initOptions, - ipld, - keychain, - peerId, - pinManager, - preload, - print, - repo -}) { - const dag = { - get: Components.dag.get({ ipld, preload }), - resolve: Components.dag.resolve({ ipld, preload }), - tree: Components.dag.tree({ ipld, preload }), - // FIXME: resolve this circular dependency - get put () { - const put = Components.dag.put({ ipld, pin, gcLock, preload }) - Object.defineProperty(this, 'put', { value: put }) - return put - } - } - const object = { - data: Components.object.data({ ipld, preload }), - get: Components.object.get({ ipld, preload }), - links: Components.object.links({ dag }), - new: Components.object.new({ ipld, preload }), - patch: { - addLink: Components.object.patch.addLink({ ipld, gcLock, preload }), - appendData: Components.object.patch.appendData({ ipld, gcLock, preload }), - rmLink: Components.object.patch.rmLink({ ipld, gcLock, preload }), - setData: Components.object.patch.setData({ ipld, gcLock, preload }) - }, - put: Components.object.put({ ipld, gcLock, preload }), - stat: Components.object.stat({ ipld, preload }) - } - - const pinAddAll = Components.pin.addAll({ pinManager, gcLock, dag }) - const pinRmAll = Components.pin.rmAll({ pinManager, gcLock, dag }) - - const pin = { - add: Components.pin.add({ addAll: pinAddAll }), - addAll: pinAddAll, - ls: Components.pin.ls({ pinManager, dag }), - rm: Components.pin.rm({ rmAll: pinRmAll }), - rmAll: pinRmAll - } - - const block = { - get: Components.block.get({ blockService, preload }), - put: Components.block.put({ blockService, pin, gcLock, preload }), - rm: Components.block.rm({ blockService, gcLock, pinManager }), - stat: Components.block.stat({ blockService, preload }) - } - - const addAll = Components.addAll({ block, preload, pin, gcLock, options: constructorOptions }) - const resolve = Components.resolve({ ipld }) - const refs = Object.assign( - Components.refs({ ipld, resolve, preload }), - { local: Components.refs.local({ repo }) } - ) - - const notStarted = async () => { // eslint-disable-line require-await - throw new NotStartedError() - } - - const api = { - add: Components.add({ addAll }), - addAll, - bitswap: { - stat: notStarted, - unwant: notStarted, - wantlist: notStarted, - wantlistForPeer: notStarted - }, - block, - bootstrap: { - add: Components.bootstrap.add({ repo }), - clear: Components.bootstrap.clear({ repo }), - list: Components.bootstrap.list({ repo }), - reset: Components.bootstrap.reset({ repo }), - rm: Components.bootstrap.rm({ repo }) - }, - cat: Components.cat({ ipld, preload }), - config: Components.config({ repo }), - dag, - dns: Components.dns(), - files: Components.files({ ipld, block, blockService, repo, preload, options: constructorOptions }), - get: Components.get({ ipld, preload }), - id: Components.id({ peerId }), - init: async () => { // eslint-disable-line require-await - throw new AlreadyInitializedError() - }, - isOnline: Components.isOnline({}), - key: { - export: Components.key.export({ keychain }), - gen: Components.key.gen({ keychain }), - import: Components.key.import({ keychain }), - info: Components.key.info({ keychain }), - list: Components.key.list({ keychain }), - rename: Components.key.rename({ keychain }), - rm: Components.key.rm({ keychain }) - }, - ls: Components.ls({ ipld, preload }), - object, - pin, - refs, - repo: { - gc: Components.repo.gc({ gcLock, pin, refs, repo }), - stat: Components.repo.stat({ repo }), - version: Components.repo.version({ repo }) - }, - resolve, - start: Components.start({ - apiManager, - options: constructorOptions, - blockService, - gcLock, - initOptions, - ipld, - keychain, - peerId, - pinManager, - preload, - print, - repo - }), - stats: { - bitswap: notStarted, - bw: notStarted, - repo: Components.repo.stat({ repo }) - }, - stop: () => {}, - swarm: { - addrs: notStarted, - connect: notStarted, - disconnect: notStarted, - localAddrs: Components.swarm.localAddrs({ multiaddrs: [] }), - peers: notStarted - }, - version: Components.version({ repo }) +/** + * @param {Object} config + * @param {import('.').NetworkService} config.network + * @param {import('.').Preload} config.preload + * @param {import('.').BlockService} config.blockService + * @param {import('.').IPNS} config.ipns + * @param {import('.').Repo} config.repo + * @param {import('.').MFSPreload} config.mfsPreload + */ +module.exports = ({ network, preload, blockService, ipns, repo, mfsPreload }) => { + const stop = async () => { + blockService.unsetExchange() + await Promise.all([ + preload.stop(), + ipns.stop(), + mfsPreload.stop(), + Service.stop(network), + repo.close() + ]) } - return api + return stop } - -/** - * @typedef {import('../utils').AbortOptions} AbortOptions - */ diff --git a/packages/ipfs-core/src/components/storage.js b/packages/ipfs-core/src/components/storage.js new file mode 100644 index 0000000000..4152d3bd24 --- /dev/null +++ b/packages/ipfs-core/src/components/storage.js @@ -0,0 +1,301 @@ +'use strict' + +const log = require('debug')('ipfs:components:peer:storage') +const createRepo = require('../runtime/repo-nodejs') +const getDefaultConfig = require('../runtime/config-nodejs') +const { ERR_REPO_NOT_INITIALIZED } = require('ipfs-repo').errors +const uint8ArrayFromString = require('uint8arrays/from-string') +const uint8ArrayToString = require('uint8arrays/to-string') +const PeerId = require('peer-id') +const { mergeOptions } = require('../utils') +const configService = require('./config') +const { NotEnabledError } = require('../errors') +const createLibP2P = require('./libp2p') + +class Storage { + /** + * @private + * @param {PeerId} peerId + * @param {Keychain} keychain + * @param {Repo} repo + * @param {Print} print + * @param {boolean} isNew + */ + constructor (peerId, keychain, repo, print, isNew) { + this.print = print + this.peerId = peerId + this.keychain = keychain + this.repo = repo + this.print = print + this.isNew = isNew + } + + /** + * + * @param {Options} options + */ + static async start (options) { + const { repoAutoMigrate: autoMigrate, repo: inputRepo, print, silent } = options + + const repo = (typeof inputRepo === 'string' || inputRepo == null) + ? createRepo({ path: inputRepo, autoMigrate, silent }) + : inputRepo + + const { peerId, keychain, isNew } = await loadRepo(repo, options) + + return new Storage(peerId, keychain, repo, print, isNew) + } +} +module.exports = Storage + +/** + * + * @param {Repo} repo + * @param {RepoOptions & InitOptions} options + * @returns {Promise<{peerId: PeerId, keychain:Keychain, isNew:boolean }>} + */ +const loadRepo = async (repo, options) => { + const openError = await openRepo(repo) + if (openError == null) { + // If opened successfully configure repo + return { ...await configureRepo(repo, options), isNew: false } + } else if (openError.code === ERR_REPO_NOT_INITIALIZED) { + if (options.allowNew === false) { + throw new NotEnabledError('Initialization of new repos disabled by config, pass `config.init.isNew: true` to enable it') + } else { + // If failed to open, because repo isn't initilaized and initalizing a + // new repo allowed, init repo: + return { ...await initRepo(repo, options), isNew: true } + } + } else { + throw openError + } +} + +/** + * Attempts to open given repo unless it is already open and returns result + * containing repo or an error if failed. + * + * @param {Repo} repo + * @returns {Promise<(Error & { code: number }) | null>} + */ +const openRepo = async (repo) => { + // If repo is closed attempt to open it. + if (repo.closed) { + try { + await repo.open() + return null + } catch (error) { + return error + } + } else { + return null + } +} + +/** + * @param {Repo} repo + * @param {RepoOptions & InitOptions} options + * @returns {Promise<{peerId: PeerId, keychain:Keychain}>} + */ +const initRepo = async (repo, options) => { + // 1. Verify that repo does not exist yet (if it does and we could not + // open it we give up) + const exists = await repo.exists() + log('repo exists?', exists) + + if (exists === true) { + throw new Error('repo already exists') + } + + // 2. Restore `peerId` from a given `.privateKey` or init new using + // provide options. + const peerId = options.privateKey + ? await decodePeerId(options.privateKey) + : await initPeerId(options) + + const identity = peerIdToIdentity(peerId) + + log('peer identity: %s', identity.PeerID) + + // 3. Init new repo with provided `.config` and restored / initalized + // peerd identity. + const config = { + ...mergeOptions(applyProfiles(getDefaultConfig(), options.profiles), options.config), + Identity: identity + } + await repo.init(config) + + // 4. Open initalized repo. + await repo.open() + + log('repo opened') + + // Create libp2p for Keychain creation + const libp2p = createLibP2P({ + options: undefined, + multiaddrs: undefined, + peerId, + repo, + config, + keychainConfig: { + pass: options.pass + } + }) + + if (libp2p.keychain && libp2p.keychain.opts) { + await libp2p.loadKeychain() + + await repo.config.set('Keychain', { + dek: libp2p.keychain.opts.dek + }) + } + + return { peerId, keychain: libp2p.keychain } +} + +/** + * Takes `peerId` either represented as a string serialized string or + * an instance and returns a `PeerId` instance. + * + * @param {PeerId|string} peerId + * @returns {Promise|PeerId} + */ +const decodePeerId = (peerId) => { + log('using user-supplied private-key') + return typeof peerId === 'object' + ? peerId + : PeerId.createFromPrivKey(uint8ArrayFromString(peerId, 'base64pad')) +} + +/** + * Initializes new PeerId by generting an underlying keypair. + * + * @param {Object} options + * @param {KeyType} [options.algorithm='RSA'] + * @param {number} [options.bits=2048] + * @param {Print} options.print + * @returns {Promise} + */ +const initPeerId = ({ print, algorithm = 'RSA', bits = 2048 }) => { + // Generate peer identity keypair + transform to desired format + add to config. + print('generating %s-bit (rsa only) %s keypair...', bits, algorithm) + return PeerId.create({ keyType: algorithm, bits }) +} + +/** + * @param {PeerId} peerId + */ +const peerIdToIdentity = (peerId) => ({ + PeerID: peerId.toB58String(), + /** @type {string} */ + PrivKey: uint8ArrayToString(peerId.privKey.bytes, 'base64pad') +}) + +/** + * Applies passed `profiles` and a `config` to an open repo. + * + * @param {Repo} repo + * @param {ConfigureOptions} options + * @returns {Promise<{peerId: PeerId, keychain:Keychain}>} + */ +const configureRepo = async (repo, { config, profiles, pass }) => { + const original = await repo.config.getAll() + const changed = mergeConfigs(applyProfiles(original, profiles), config) + + if (original !== changed) { + await repo.config.replace(changed) + } + + // @ts-ignore - Identity may not be present + const peerId = await PeerId.createFromPrivKey(changed.Identity.PrivKey) + const libp2p = createLibP2P({ + options: undefined, + multiaddrs: undefined, + peerId, + repo, + config: changed, + keychainConfig: { + pass, + ...changed.Keychain + } + }) + + if (libp2p.keychain) { + await libp2p.loadKeychain() + } + + return { peerId, keychain: libp2p.keychain } +} + +/** + * @param {IPFSConfig} config + * @param {Partial} [changes] + */ +const mergeConfigs = (config, changes) => + changes ? mergeOptions(config, changes) : config + +/** + * Apply profiles (e.g. ['server', 'lowpower']) to config + * + * @param {IPFSConfig} config + * @param {string[]} [profiles] + */ +const applyProfiles = (config, profiles) => { + return (profiles || []).reduce((config, name) => { + const profile = configService.profiles[name] + if (!profile) { + throw new Error(`Could not find profile with name '${name}'`) + } + log('applying profile %s', name) + return profile.transform(config) + }, config) +} + +/** + * @typedef {StorageOptions & RepoOptions & InitOptions} Options + * + * @typedef {Object} StorageOptions + * @property {Repo|string} [repo='~/.jsipfs'] - The file path at which to store the + * IPFS node’s data. Alternatively, you can set up a customized storage system + * by providing an Repo implementation. (In browser default is 'ipfs'). + * @property {boolean} [repoAutoMigrate=true] - js-ipfs comes bundled with a tool + * that automatically migrates your IPFS repository when a new version is + * available. + * @property {boolean} [repoOwner] + * @property {IPLDOptions} [ipld] + * + * + * @typedef {Object} RepoOptions + * @property {Print} print + * @property {IPFSConfig} [config] + * @property {boolean} [silent] + * + * @typedef {Object} ConfigureOptions + * @property {IPFSConfig} [options.config] + * @property {string[]} [options.profiles] + * @property {string} [options.pass] + * + * @typedef {Object} InitOptions - On Frist run js-ipfs will initalize a repo + * which can be customized through this settings. + * @property {boolean} [emptyRepo=false] - Whether to remove built-in assets, + * like the instructional tour and empty mutable file system, from the repo. + * @property {KeyType} [algorithm='RSA'] - The type of key to use. + * @property {number} [bits=2048] - Number of bits to use in the generated key + * pair (rsa only). + * @property {PeerId|string} [privateKey] - A pre-generated private key to use. + * **NOTE: This overrides `bits`.** + * @property {string} [pass] - A passphrase to encrypt keys. You should + * generally use the top-level `pass` option instead of the `init.pass` + * option (this one will take its value from the top-level option if not set). + * @property {string[]} [profiles] - Apply profile settings to config. + * @property {boolean} [allowNew=true] - Set to `false` to disallow + * initialization if the repo does not already exist. + * + * @typedef {import('.').IPLDOptions} IPLDOptions + * @typedef {import('.').Print} Print + * @typedef {import('.').IPFSConfig} IPFSConfig + * @typedef {import('../interface/repo').Repo} Repo + * @typedef {import('libp2p-crypto').KeyType} KeyType + * @typedef {import('libp2p').LibP2PKeychain} Keychain + */ diff --git a/packages/ipfs-core/src/components/swarm/addrs.js b/packages/ipfs-core/src/components/swarm/addrs.js index 42301f414c..03000ed58a 100644 --- a/packages/ipfs-core/src/components/swarm/addrs.js +++ b/packages/ipfs-core/src/components/swarm/addrs.js @@ -2,9 +2,20 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') -module.exports = ({ libp2p }) => { - return withTimeoutOption(async function addrs (options) { // eslint-disable-line require-await +/** + * @param {Object} config + * @param {import('.').NetworkService} config.network + */ +module.exports = ({ network }) => { + /** + * List of known addresses of each peer connected. + * + * @param {import('../../utils').AbortOptions} options + * @returns {Promise} + */ + async function addrs (options) { // eslint-disable-line require-await const peers = [] + const { libp2p } = await network.use(options) for (const [peerId, peer] of libp2p.peerStore.peers.entries(options)) { peers.push({ id: peerId, @@ -12,5 +23,15 @@ module.exports = ({ libp2p }) => { }) } return peers - }) + } + + return withTimeoutOption(addrs) } + +/** + * @typedef {Object} PeerInfo + * @property {string} id + * @property {Multiaddr[]} addrs + * + * @typedef {import('.').Multiaddr} Multiaddr + */ diff --git a/packages/ipfs-core/src/components/swarm/connect.js b/packages/ipfs-core/src/components/swarm/connect.js index 8f287505e1..306aba2487 100644 --- a/packages/ipfs-core/src/components/swarm/connect.js +++ b/packages/ipfs-core/src/components/swarm/connect.js @@ -2,8 +2,22 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') -module.exports = ({ libp2p }) => { - return withTimeoutOption(function connect (addr, options) { +/** + * @param {Object} config + * @param {import('.').NetworkService} config.network + */ +module.exports = ({ network }) => { + /** + * Open a connection to a given address. + * + * @param {import('.').Multiaddr} addr + * @param {import('.').AbortOptions} [options] + * @returns {Promise} + */ + async function connect (addr, options) { + const { libp2p } = await network.use(options) return libp2p.dial(addr, options) - }) + } + + return withTimeoutOption(connect) } diff --git a/packages/ipfs-core/src/components/swarm/disconnect.js b/packages/ipfs-core/src/components/swarm/disconnect.js index 9ef3966606..7bfdbbb839 100644 --- a/packages/ipfs-core/src/components/swarm/disconnect.js +++ b/packages/ipfs-core/src/components/swarm/disconnect.js @@ -2,8 +2,22 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') -module.exports = ({ libp2p }) => { - return withTimeoutOption(function disconnect (addr, options) { +/** + * @param {Object} config + * @param {import('.').NetworkService} config.network + */ +module.exports = ({ network }) => { + /** + * Close a connection on a given address. + * + * @param {import('.').Multiaddr} addr + * @param {import('.').AbortOptions} [options] + * @returns {Promise} + */ + async function disconnect (addr, options) { + const { libp2p } = await network.use(options) return libp2p.hangUp(addr, options) - }) + } + + return withTimeoutOption(disconnect) } diff --git a/packages/ipfs-core/src/components/swarm/index.js b/packages/ipfs-core/src/components/swarm/index.js new file mode 100644 index 0000000000..29af23f83b --- /dev/null +++ b/packages/ipfs-core/src/components/swarm/index.js @@ -0,0 +1,29 @@ +'use strict' + +const createAddrsAPI = require('./addrs') +const createConnectAPI = require('./connect') +const createDisconnectAPI = require('./disconnect') +const createLocalAddrsAPI = require('./local-addrs') +const createPeersAPI = require('./peers') + +class SwarmAPI { + /** + * @param {Object} config + * @param {NetworkService} config.network + */ + constructor ({ network }) { + this.addrs = createAddrsAPI({ network }) + this.connect = createConnectAPI({ network }) + this.disconnect = createDisconnectAPI({ network }) + this.localAddrs = createLocalAddrsAPI({ network }) + this.peers = createPeersAPI({ network }) + } +} + +module.exports = SwarmAPI + +/** + * @typedef {import('..').NetworkService} NetworkService + * @typedef {import('..').Multiaddr} Multiaddr + * @typedef {import('..').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/swarm/local-addrs.js b/packages/ipfs-core/src/components/swarm/local-addrs.js index 51912f8259..e7321aa175 100644 --- a/packages/ipfs-core/src/components/swarm/local-addrs.js +++ b/packages/ipfs-core/src/components/swarm/local-addrs.js @@ -2,8 +2,25 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') -module.exports = ({ multiaddrs }) => { - return withTimeoutOption(async function localAddrs () { // eslint-disable-line require-await - return multiaddrs - }) +/** + * @param {Object} config + * @param {import('.').NetworkService} config.network + */ +module.exports = ({ network }) => { + /** + * Local addresses this node is listening on. + * + * @param {import('.').AbortOptions} [options] + * @returns {Promise} + */ + async function localAddrs (options) { + const { libp2p } = await network.use(options) + return libp2p.multiaddrs + } + + return withTimeoutOption(localAddrs) } + +/** + * @typedef {import('.').Multiaddr} Multiaddr + */ diff --git a/packages/ipfs-core/src/components/swarm/peers.js b/packages/ipfs-core/src/components/swarm/peers.js index a3e1199e95..0c91011217 100644 --- a/packages/ipfs-core/src/components/swarm/peers.js +++ b/packages/ipfs-core/src/components/swarm/peers.js @@ -2,8 +2,19 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') -module.exports = ({ libp2p }) => { - return withTimeoutOption(async function peers (options = {}) { // eslint-disable-line require-await +/** + * @param {Object} config + * @param {import('.').NetworkService} config.network + */ +module.exports = ({ network }) => { + /** + * Local addresses this node is listening on. + * + * @param {PeersOptions & AbortOptions} [options] + * @returns {Promise} + */ + async function peers (options = {}) { + const { libp2p } = await network.use(options) const verbose = options.v || options.verbose const peers = [] @@ -28,5 +39,26 @@ module.exports = ({ libp2p }) => { } return peers - }) + } + + return withTimeoutOption(peers) } + +/** + * @typedef {Object} PeerConnection + * @property {Multiaddr} addr + * @property {string} peer + * @property {string} [latency] + * @property {string} [muxer] + * @property {number} [direction] + * + * @typedef {Object} PeersOptions + * @property {boolean} [direction=false] + * @property {boolean} [streams=false] + * @property {boolean} [verbose=false] + * @property {boolean} [v=false] + * @property {boolean} [latency=false] + * + * @typedef {import('.').Multiaddr} Multiaddr + * @typedef {import('.').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/version.js b/packages/ipfs-core/src/components/version.js index f9024bb657..ba9b006bd0 100644 --- a/packages/ipfs-core/src/components/version.js +++ b/packages/ipfs-core/src/components/version.js @@ -6,11 +6,15 @@ const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') // gitHead is defined in published versions const meta = { gitHead: '', ...pkg } +/** + * @param {Object} config + * @param {import('.').Repo} config.repo + */ module.exports = ({ repo }) => { /** * Returns the implementation version * - * @param {import('../utils').AbortOptions} [options] + * @param {import('.').AbortOptions} [options] * @returns {Promise} * @example * ```js @@ -40,7 +44,7 @@ module.exports = ({ repo }) => { * supported by this node * * @property {string} version - * @property {string} repo + * @property {number} repo * @property {string} [commit] * @property {string} [interface-ipfs-core] * @property {string} [ipfs-http-client] diff --git a/packages/ipfs-core/src/errors.js b/packages/ipfs-core/src/errors.js index e15dec7260..95f1e3c317 100644 --- a/packages/ipfs-core/src/errors.js +++ b/packages/ipfs-core/src/errors.js @@ -44,6 +44,28 @@ class NotStartedError extends Error { NotStartedError.code = 'ERR_NOT_STARTED' exports.NotStartedError = NotStartedError +class AlreadyStartingError extends Error { + constructor (message = 'cannot start, already startin') { + super(message) + this.name = 'AlreadyStartingError' + this.code = AlreadyStartingError.code + } +} + +AlreadyStartingError.code = 'ERR_ALREADY_STARTING' +exports.AlreadyStartingError = AlreadyStartingError + +class AlreadyStartedError extends Error { + constructor (message = 'cannot start, already started') { + super(message) + this.name = 'AlreadyStartedError' + this.code = AlreadyStartedError.code + } +} + +AlreadyStartedError.code = 'ERR_ALREADY_STARTED' +exports.AlreadyStartedError = AlreadyStartedError + class NotEnabledError extends Error { constructor (message = 'not enabled') { super(message) diff --git a/packages/ipfs-core/src/index.js b/packages/ipfs-core/src/index.js index e718345207..bef8d923e3 100644 --- a/packages/ipfs-core/src/index.js +++ b/packages/ipfs-core/src/index.js @@ -1,10 +1,5 @@ 'use strict' -const log = require('debug')('ipfs') - -/** @type {typeof Object.assign} */ -const mergeOptions = require('merge-options') -const { isTest } = require('ipfs-utils/src/env') const globSource = require('ipfs-utils/src/files/glob-source') const urlSource = require('ipfs-utils/src/files/url-source') const PeerId = require('peer-id') @@ -16,70 +11,10 @@ const multicodec = require('multicodec') const multihashing = require('multihashing-async') const multihash = multihashing.multihash const CID = require('cids') -const { NotInitializedError } = require('./errors') -const Components = require('./components') -const ApiManager = require('./api-manager') - -const getDefaultOptions = () => ({ - init: true, - start: true, - EXPERIMENTAL: {}, - preload: { - enabled: !isTest, // preload by default, unless in test env - addresses: [ - '/dns4/node0.preload.ipfs.io/https', - '/dns4/node1.preload.ipfs.io/https', - '/dns4/node2.preload.ipfs.io/https', - '/dns4/node3.preload.ipfs.io/https' - ] - } -}) - -/** - * Creates and returns a ready to use instance of an IPFS node. - * - * @template {boolean | InitOptions} Init - * @template {boolean} Start - * @param {CreateOptions} [options] - */ -async function create (options = {}) { - options = mergeOptions(getDefaultOptions(), options) - - // eslint-disable-next-line no-console - const print = options.silent ? log : console.log - - const apiManager = new ApiManager() - - const { api } = apiManager.update({ - init: Components.init({ apiManager, print, options }), - dns: Components.dns(), - isOnline: Components.isOnline({ libp2p: undefined }) - }, async () => { throw new NotInitializedError() }) // eslint-disable-line require-await - - const initializedApi = options.init && await api.init() - const startedApi = options.start && initializedApi && await initializedApi.start() - - /** - * create returns object that has different API set based on `options.init` - * and `options.start` values. If we just return `startedApi || initializedApi || api` - * TS will infer return type to be ` typeof startedAPI || typeof initializedApi || typeof api` - * which user would in practice act like `api` with all the extra APIs as optionals. - * - * Type trickery below attempts to affect inference by explicitly telling - * what the return type is and when. - * - * @typedef {typeof api} API - * @typedef {NonNullable} InitializedAPI - * @typedef {NonNullable} StartedAPI - * @type {If, API>} - */ - // @ts-ignore - const ipfs = startedApi || initializedApi || api - return ipfs -} +const IPFS = require('./components') module.exports = { - create, + create: IPFS.create, crypto, isIPFS, CID, @@ -92,100 +27,3 @@ module.exports = { globSource, urlSource } - -/** - * @template {boolean | InitOptions} Init - * @template {boolean} Start - * - * @typedef {Object} CreateOptions - * Options argument can be used to specify advanced configuration. - * @property {RepoOption} [repo='~/.jsipfs'] - * @property {boolean} [repoAutoMigrate=true] - `js-ipfs` comes bundled with a - * tool that automatically migrates your IPFS repository when a new version is - * available. - * @property {Init} [init=true] - Perform repo initialization steps when creating - * the IPFS node. - * Note that *initializing* a repo is different from creating an instance of - * [`ipfs.Repo`](https://github.com/ipfs/js-ipfs-repo). The IPFS constructor - * sets many special properties when initializing a repo, so you should usually - * not try and call `repoInstance.init()` yourself. - * @property {Start} [start=true] - If `false`, do not automatically - * start the IPFS node. Instead, you’ll need to manually call - * [`node.start()`](https://github.com/ipfs/js-ipfs/blob/master/packages/ipfs/docs/MODULE.md#nodestart) - * yourself. - * @property {string} [pass=null] - A passphrase to encrypt/decrypt your keys. - * @property {boolean} [silent=false] - Prevents all logging output from the - * IPFS node. (Default: `false`) - * @property {RelayOptions} [relay={ enabled: true, hop: { enabled: false, active: false } }] - * - Configure circuit relay (see the [circuit relay tutorial] - * (https://github.com/ipfs/js-ipfs/tree/master/examples/circuit-relaying) - * to learn more). - * @property {boolean} [offline=false] - Run ipfs node offline. The node does - * not connect to the rest of the network but provides a local API. - * @property {PreloadOptions} [preload] - Configure remote preload nodes. - * The remote will preload content added on this node, and also attempt to - * preload objects requested by this node. - * @property {ExperimentalOptions} [EXPERIMENTAL] - Enable and configure - * experimental features. - * @property {object} [config] - Modify the default IPFS node config. This - * object will be *merged* with the default config; it will not replace it. - * (Default: [`config-nodejs.js`](https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs/src/core/runtime/config-nodejs.js) - * in Node.js, [`config-browser.js`](https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs/src/core/runtime/config-browser.js) - * in browsers) - * @property {import('./components').IPLDConfig} [ipld] - Modify the default IPLD config. This object - * will be *merged* with the default config; it will not replace it. Check IPLD - * [docs](https://github.com/ipld/js-ipld#ipld-constructor) for more information - * on the available options. (Default: [`ipld.js`] - * (https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs/src/core/runtime/ipld.js) - * in browsers) - * @property {object|Function} [libp2p] - The libp2p option allows you to build - * your libp2p node by configuration, or via a bundle function. If you are - * looking to just modify the below options, using the object format is the - * quickest way to get the default features of libp2p. If you need to create a - * more customized libp2p node, such as with custom transports or peer/content - * routers that need some of the ipfs data on startup, a custom bundle is a - * great way to achieve this. - * - You can see the bundle in action in the [custom libp2p example](https://github.com/ipfs/js-ipfs/tree/master/examples/custom-libp2p). - * - Please see [libp2p/docs/CONFIGURATION.md](https://github.com/libp2p/js-libp2p/blob/master/doc/CONFIGURATION.md) - * for the list of options libp2p supports. - * - Default: [`libp2p-nodejs.js`](../src/core/runtime/libp2p-nodejs.js) - * in Node.js, [`libp2p-browser.js`](../src/core/runtime/libp2p-browser.js) in - * browsers. - */ - -/** - * @typedef {IPFSRepo|string} RepoOption - * The file path at which to store the IPFS node’s data. Alternatively, you - * can set up a customized storage system by providing an `ipfs.Repo` instance. - * - * @example - * ```js - * // Store data outside your user directory - * const node = await IPFS.create({ repo: '/var/ipfs/data' }) - * ``` - * @typedef {import('./components/init').InitOptions} InitOptions - * - * @typedef {object} RelayOptions - * @property {boolean} [enabled] - Enable circuit relay dialer and listener. (Default: `true`) - * @property {object} [hop] - * @property {boolean} [hop.enabled] - Make this node a relay (other nodes can connect *through* it). (Default: `false`) - * @property {boolean} [hop.active] - Make this an *active* relay node. Active relay nodes will attempt to dial a destination peer even if that peer is not yet connected to the relay. (Default: `false`) - * - * @typedef {object} PreloadOptions - * @property {boolean} [enabled] - Enable content preloading (Default: `true`) - * @property {string[]} [addresses] - Multiaddr API addresses of nodes that should preload content. - * **NOTE:** nodes specified here should also be added to your node's bootstrap address list at `config.Boostrap`. - * - * @typedef {object} ExperimentalOptions - * @property {boolean} [ipnsPubsub] - Enable pub-sub on IPNS. (Default: `false`) - * @property {boolean} [sharding] - Enable directory sharding. Directories that have many child objects will be represented by multiple DAG nodes instead of just one. It can improve lookup performance when a directory has several thousand files or more. (Default: `false`) - * - * @typedef {import('./components').IPFSRepo} IPFSRepo - */ - -/** - * Utility type to write type level conditionals - * - * @template Conditon, Then, Else - * @typedef {NonNullable extends false ? Else : Then } If - */ diff --git a/packages/ipfs-core/src/interface/basic.ts b/packages/ipfs-core/src/interface/basic.ts new file mode 100644 index 0000000000..41dea96426 --- /dev/null +++ b/packages/ipfs-core/src/interface/basic.ts @@ -0,0 +1,28 @@ +import CID from 'cids' +import PeerId from 'peer-id' +import BigInteger from 'bignumber.js' +export type Await = + | T + | Promise + +export type AwaitIterable = + | Iterable + | AsyncIterable + +export interface AbortOptions { + signal?: AbortSignal +} +export type ToJSON = + | null + | string + | number + | boolean + | ToJSON[] + | { toJSON?: () => ToJSON } & {[key:string]: ToJSON} + +export interface Block { + cid: CID + data: Uint8Array +} + +export type { CID, PeerId, BigInteger } diff --git a/packages/ipfs-core/src/interface/bitswap.ts b/packages/ipfs-core/src/interface/bitswap.ts new file mode 100644 index 0000000000..456a232adc --- /dev/null +++ b/packages/ipfs-core/src/interface/bitswap.ts @@ -0,0 +1,88 @@ +import { PeerId, CID, Block, Await, BigInteger, AbortOptions } from './basic' +import { MovingAverage } from './moving-avarage' +import { StoreReader, StoreExporter, StoreImporter } from './store' + +export interface Bitswap extends + StoreReader, + StoreExporter, + StoreImporter +{ + + readonly peerId: PeerId + + enableStats(): void + disableStats(): void + + wantlistForPeer(peerId: PeerId, options?:AbortOptions): Map + ledgerForPeer(peerId: PeerId): Ledger + + put(block: Block, options?: AbortOptions): Await + + unwant(cids: Iterable, options?: AbortOptions): void + cancelWants(cids: Iterable): void + getWantlist(options?: AbortOptions): Iterable<[string, WantListEntry]> + peers(): PeerId[] + stat(): Stats + start(): void + stop(): void +} + +export interface Ledger { + sentBytes(n:number):void + receivedBytes(n:number):void + + wants(cid: CID, priority: number, wantType: WantType):void + cancelWant(cid: CID): void + wantlistContains(cid:CID): WantListEntry|void + + debtRatio():number +} + +export interface WantListEntry { + readonly cid: CID + priority: number + inc(): void + dec(): void + hasRefs(): boolean + equals(other: WantListEntry): boolean +} + +export type WantList = { + entries: Entry[] + full?: boolean +} + +export type Entry = { + block: Uint8Array + priority: number + cancel: boolean + wantType?: WantType + sendDontHave?: boolean +} + +export type BlockPresence = { + cid: Uint8Array + type: BlockPresenceType +} + +export type Have = 0 +export type DontHave = 1 +export type BlockPresenceType = Have | DontHave + +export type WantBlock = 0 +export type HaveBlock = 1 +export type WantType = WantBlock | HaveBlock + +export type BlockData = { + prefix: Uint8Array + data: Uint8Array +} + +export interface Stats { + enable(): void + disable(): void + stop(): void + readonly snapshot: Record + readonly movingAverages: Record> + push(counter: number, inc: number): void +} diff --git a/packages/ipfs-core/src/interface/block-service.ts b/packages/ipfs-core/src/interface/block-service.ts new file mode 100644 index 0000000000..aa206edc4d --- /dev/null +++ b/packages/ipfs-core/src/interface/block-service.ts @@ -0,0 +1,20 @@ +import { Block, CID, Await, AbortOptions } from './basic' +import { StoreReader, StoreImporter, StoreExporter, StoreEraser } from './store' +import { Bitswap } from './bitswap' + +export interface BlockService extends + StoreReader, + StoreExporter, + StoreImporter, + StoreEraser +{ + setExchange(bitswap: Bitswap): void + + unsetExchange(): void + hasExchange(): boolean + + /** + * Put a block to the underlying datastore. + */ + put(block: Block, options?:AbortOptions): Await +} diff --git a/packages/ipfs-core/src/interface/datastore.ts b/packages/ipfs-core/src/interface/datastore.ts new file mode 100644 index 0000000000..418eadccf5 --- /dev/null +++ b/packages/ipfs-core/src/interface/datastore.ts @@ -0,0 +1,185 @@ +import { KeyValueStore, StoreBatch, StoreSelector, Resource } from './store' +export interface DataStore extends + KeyValueStore, + StoreSelector, + StoreBatch, + Resource +{ +} + +export interface Key { + /** + * Returns the "name" of this key (field of last namespace). + * + * @example + * ```js + * key.toString() + * // '/Comedy/MontyPython/Actor:JohnCleese' + * key.name() + * // 'JohnCleese' + * ``` + */ + name(): string + + /** + * Returns the "type" of this key (value of last namespace). + * + * @example + * ```js + * key.toString() + * '/Comedy/MontyPython/Actor:JohnCleese' + * key.type() + * // 'Actor' + * ``` + */ + type(): string + + /** + * Returns the `namespaces` making up this `Key`. + */ + namespaces(): string[] + + /** + * Returns the "base" namespace of this key. + * + * @example + * ```js + * key.toString() + * // '/Comedy/MontyPython/Actor:JohnCleese' + * key.baseNamespace() + * // 'Actor:JohnCleese' + */ + baseNamespace(): string + + /** + * Returns an "instance" of this type key (appends value to namespace). + * + * @example + * ```js + * key.toString() + * // '/Comedy/MontyPython/Actor' + * key.instance('JohnClesse').toString() + * // '/Comedy/MontyPython/Actor:JohnCleese' + * ``` + */ + instance(): Key + + /** + * Returns the "path" of this key (parent + type). + * + * @example + * ```js + * key.toString() + * '/Comedy/MontyPython/Actor:JohnCleese' + * key.path().toString() + * // '/Comedy/MontyPython/Actor' + * ``` + */ + path(): Key + + /** + * Returns the `parent` Key of this Key. + * + * @example + * ```js + * key.toString() + * "/Comedy/MontyPython/Actor:JohnCleese" + * key.parent().toString() + * // "/Comedy/MontyPython" + * ``` + */ + parent(): Key + + /** + * Returns the `child` Key of this Key. + * + * @example + * ```js + * key.toString() + * '/Comedy/MontyPython' + * child.toString() + * // 'Actor:JohnCleese' + * key.child(child).toString() + * '/Comedy/MontyPython/Actor:JohnCleese' + * ``` + */ + child(key: Key): Key + + /** + * Check if the given key is sorted lower than this. + */ + less(key: Key): boolean + + /** + * Returns whether this key is a prefix of `other` + * + * @example + * ```js + * comedy.toString() + * '/Comedy' + * monty.toString() + * '/Comedy/MontyPython' + * comedy.isAncestorOf(monty) + * // true + * ``` + */ + isAncestorOf(other: Key): boolean + + /** + * Returns whether this key is a contains `other` as prefix. + * ```js + * comedy.toString() + * '/Comedy' + * monty.toString() + * '/Comedy/MontyPython' + * monty.isDecendantOf(comedy) + * // true + * ``` + */ + isDecendantOf(other: Key): boolean + + /** + * Returns wether this key has only one namespace. + */ + isTopLevel(): boolean + + /** + * Returns the key with all parts in reversed order. + * + * @example + * ```js + * key.toString() + * // '/Comedy/MontyPython/Actor:JohnCleese' + * key.reverse().toString() + * // /Actor:JohnCleese/MontyPython/Comedy + * new Key('/Comedy/MontyPython/Actor:JohnCleese').reverse() + * ``` + */ + reverse(): Key + + /** + * Concats one or more Keys into one new Key. + */ + concat(...keys: Key[]): Key + + /** + * Returns the array representation of this key. + * + * @example + * ```js + * key.toString() + * // '/Comedy/MontyPython/Actor:JohnCleese' + * key.list() + * // ['Comedy', 'MontyPythong', 'Actor:JohnCleese'] + * ``` + */ + list(): string[] + toString(): string +} + +export type Value = Uint8Array + +export interface Entry { + key: Key, + value: Value +} diff --git a/packages/ipfs-core/src/interface/format.ts b/packages/ipfs-core/src/interface/format.ts new file mode 100644 index 0000000000..4c0da87482 --- /dev/null +++ b/packages/ipfs-core/src/interface/format.ts @@ -0,0 +1,41 @@ + +import { Await, CID } from './basic' + +export interface Format { + util: Util + resolver: Resolver + + defaultHashArg: string | number + codec: string | number +} + +export interface Util { + /** + * Serialize an IPLD Node into a binary blob. + */ + serialize(node:T):Uint8Array + /** + * Deserialize a binary blob into an IPLD Node. + */ + deserialize(bytes: Uint8Array): T + + /** + * Calculate the CID of the binary blob. + */ + cid(bytes:Uint8Array, options?:CIDOptions): Await +} + +export interface CIDOptions { + cidVersion?: number + hashAlg?: number | string +} + +export interface Resolver { + resolve(bytes: Uint8Array, path: string): ResolveResult + tree(byte: Uint8Array): string[] +} + +export interface ResolveResult { + value: T + remainderPath: string +} diff --git a/packages/ipfs-core/src/interface/ipld.ts b/packages/ipfs-core/src/interface/ipld.ts new file mode 100644 index 0000000000..c168e38f61 --- /dev/null +++ b/packages/ipfs-core/src/interface/ipld.ts @@ -0,0 +1,43 @@ +import { BlockService } from './block-service' +import { Await, CID, AwaitIterable, AbortOptions } from './basic' +import { StoreReader, StoreExporter, StoreEraser } from './store' +import { ResolveResult, Format } from './format' + +export interface IPLD extends + StoreReader, + StoreExporter, + StoreEraser + +{ + put(value:T, format:FormatCode, options?:PutOptions & AbortOptions):Await + putMany(values: AwaitIterable, format: FormatCode, options?:PutOptions):AwaitIterable + + resolve(cid: CID, path: string, options?: AbortOptions): AwaitIterable> + tree(cid:CID, path?:string, options?:TreeOptions & AbortOptions):AwaitIterable + + addFormat(format:Format):IPLD + removeFormat(format:Format):IPLD + + defaultOptions: Options +} + +export type FormatCode = number +export type HashAlg = number + +export interface Options { + blockService?: BlockService + formats?: Record + + loadFormat?: (code:number|string) => Promise> +} + +export interface PutOptions { + hashAlg?: HashAlg, + cidVersion?: 0|1, + onlyHash?: boolean, + +} + +export interface TreeOptions { + recursive?: boolean +} diff --git a/packages/ipfs-core/src/interface/moving-avarage.ts b/packages/ipfs-core/src/interface/moving-avarage.ts new file mode 100644 index 0000000000..99045ae3f8 --- /dev/null +++ b/packages/ipfs-core/src/interface/moving-avarage.ts @@ -0,0 +1,9 @@ +export interface MovingAverage { + variance(): number + movingAverage(): number + + deviation(): number + forecast(): number + + push(time: number, value:number):void +} diff --git a/packages/ipfs-core/src/interface/repo.ts b/packages/ipfs-core/src/interface/repo.ts new file mode 100644 index 0000000000..efe003516a --- /dev/null +++ b/packages/ipfs-core/src/interface/repo.ts @@ -0,0 +1,104 @@ +import { CID, Block, ToJSON, Await, AbortOptions } from './basic' +import { DataStore, Key } from './datastore' +import { + ValueStore, StoreReader, Resource, StoreLookup, + StoreImporter, StoreExporter, StoreEraser, StoreSelector, + KeyValueStore +} from './store' + +export interface Repo extends Resource { + readonly path: string + closed: boolean + + /** + * Initializes necessary structures inside the repo + */ + init(config:Partial):Await + + /** + * Tells whether this repo exists or not. + */ + exists():Await + + /** + * Tells whether the repo has been initialized. + */ + isInitialized():Await + + /** + * Gets the repo status. + */ + stat(options?:AbortOptions):Await + + root: KeyValueStore + + blocks: BlockStore + datastore: DataStore + + pins: PinStore + config: ConfigStore + keys: KeyStore + + version: ValueStore + apiAddr: ValueStore +} + +export interface RepoStatus { + numObjects: number + repoPath: string + repoSize: number + version: number + storageMax: number +} + +interface BlockStore extends + StoreImporter, + StoreReader, + StoreLookup, + StoreExporter, + StoreEraser, + StoreSelector +{ + put(block: Block): Await +} + +export interface ConfigStore extends + StoreReader +{ + /** + * Set a config `value`, where `value` can be anything that is serializable + * to JSON. + */ + set(key: string, value: ToJSON, options?:AbortOptions): Await + + /** + * Set the whole `config` which can be a any value that is serializable to + * JSON. + * + * @param config + */ + replace(config: Config, options?: AbortOptions): Await + + /** + * Get the entire config value. + */ + getAll(options?:AbortOptions): Await + + /** + * Whether the config sub-repo exists. + */ + exists(): Await +} + +export interface PinStore extends + KeyValueStore, + Object +{ +} + +export interface KeyStore extends + KeyValueStore, + Object +{ + +} diff --git a/packages/ipfs-core/src/interface/store.ts b/packages/ipfs-core/src/interface/store.ts new file mode 100644 index 0000000000..332c573062 --- /dev/null +++ b/packages/ipfs-core/src/interface/store.ts @@ -0,0 +1,124 @@ + +import { Await, AwaitIterable, AbortOptions } from './basic' + +export interface ValueStore { + get(options?:AbortOptions): Await + set(value: T): Await +} + +export interface KeyValueStore extends + StoreReader, + StoreExporter, + StoreSelector, + StoreLookup, + StoreWriter, + StoreImporter, + StoreEraser { +} + +// Interface Datastore + +export interface StoreReader { + /** + * The key retrieve the value for. + */ + get(key: Key, options?: AbortOptions): Await +} + +export interface StoreLookup { + /** + * Check for the existence of a given key + */ + has(key: Key, options?: AbortOptions): Await +} + +export interface StoreExporter { + /** + * Retrieve a stream of values stored under the given keys. + */ + getMany(keys: AwaitIterable, options?: AbortOptions): AwaitIterable +} + +export interface StoreSelector { + /** + * Search the store for some values. + */ + query(query: Query, options?: AbortOptions): AwaitIterable +} + +export interface StoreWriter { + /** + * Store a value with the given key. + */ + put(key: Key, value: Value, options?: AbortOptions): Await +} + +export interface StoreImporter { + /** + * Store many key-value pairs. + */ + putMany(entries: AwaitIterable, options?: AbortOptions): AwaitIterable +} + +export interface StoreEraser { + /** + * Delete the content stored under the given key. + */ + delete(key: Key, options?: AbortOptions): Await + /** + * Delete the content stored under the given keys. + */ + deleteMany(keys: AwaitIterable, options?: AbortOptions): AwaitIterable + +} +export interface StoreBatch { + batch(): Batch +} + +export interface Batch { + put(key: Key, value: Value): void + delete(key: Key): void + + commit(options?: AbortOptions): Await +} + +export interface Resource { + /** + * Opens the datastore, this is only needed if the store was closed before, + * otherwise this is taken care of by the constructor. + */ + open(): Await + /** + * Close the datastore, this should always be called to ensure resources + * are cleaned up. + */ + close(): Await +} + +export interface Query { + /** + * Only return values where the key starts with this prefix + */ + prefix?: string + /** + * Filter the results according to the these functions + */ + filters?: Array<(resut: Entry) => boolean> + /** + * Order the results according to these functions + */ + orders?: Array<(results: Entry[]) => Entry[]> + /** + * Only return this many records + */ + limit?: number + /** + * An options object, all properties are optional + */ + options?: Options + /** + * A way to signal that the caller is no longer interested in the outcome of + * this operation + */ + signal?: AbortSignal +} diff --git a/packages/ipfs-core/src/mfs-preload.js b/packages/ipfs-core/src/mfs-preload.js index aabffee3bb..e800eff9f4 100644 --- a/packages/ipfs-core/src/mfs-preload.js +++ b/packages/ipfs-core/src/mfs-preload.js @@ -8,9 +8,9 @@ const log = Object.assign(debug('ipfs:mfs-preload'), { /** * @param {Object} config - * @param {import('./components/index').Preload} config.preload - * @param {import('./components/index').Files} config.files - * @param {import('./components/init').PreloadOptions} [config.options] + * @param {import('./components').Preload} config.preload + * @param {import('./components').Files} config.files + * @param {Options} [config.options] */ module.exports = ({ preload, files, options = {} }) => { options.interval = options.interval || 30 * 1000 @@ -61,4 +61,8 @@ module.exports = ({ preload, files, options = {} }) => { /** * @typedef {ReturnType} MFSPreload + * @typedef {PreloadOptions & MFSPreloadOptions} Options + * @typedef {Object} MFSPreloadOptions + * @property {number} [interval] + * @typedef {import('./components').PreloadOptions} PreloadOptions */ diff --git a/packages/ipfs-core/src/preload.js b/packages/ipfs-core/src/preload.js index 53425347d3..6c74a7d508 100644 --- a/packages/ipfs-core/src/preload.js +++ b/packages/ipfs-core/src/preload.js @@ -16,10 +16,7 @@ const log = Object.assign( ) /** - * @param {Object} [options] - * @param {boolean} [options.enabled = false] - Whether to preload anything - * @param {string[]} [options.addresses = []] - Which preload servers to use - * @param {number} [options.cache = 1000] - How many CIDs to cache + * @param {Options & AbortOptions} [options] */ const createPreloader = (options = {}) => { options.enabled = Boolean(options.enabled) @@ -111,3 +108,15 @@ const createPreloader = (options = {}) => { } module.exports = createPreloader + +/** + * @typedef {ReturnType} Preload + * + * @typedef {object} Options + * @property {boolean} [enabled = false] - Whether to preload anything + * @property {number} [cache = 1000] - How many CIDs to cache + * @property {string[]} [addresses = []] - Which preload servers to use. + * **NOTE:** nodes specified here should also be added to your node's bootstrap address list at `config.Boostrap`. + * + * @typedef {import('./components').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/runtime/config-nodejs.js b/packages/ipfs-core/src/runtime/config-nodejs.js index 2c6e53b96d..345ff72276 100644 --- a/packages/ipfs-core/src/runtime/config-nodejs.js +++ b/packages/ipfs-core/src/runtime/config-nodejs.js @@ -38,7 +38,8 @@ module.exports = () => ({ '/dns4/node3.preload.ipfs.io/tcp/443/wss/p2p/QmY7JB6MQXhxHvq7dBDh4HpbH29v4yE9JRadAVpndvzySN' ], Pubsub: { - Router: 'gossipsub', + /** @type {'gossipsub'} */ + Router: ('gossipsub'), Enabled: true }, Swarm: { diff --git a/packages/ipfs-core/src/runtime/repo-nodejs.js b/packages/ipfs-core/src/runtime/repo-nodejs.js index e8ad5ee54c..a5f3ec99d8 100644 --- a/packages/ipfs-core/src/runtime/repo-nodejs.js +++ b/packages/ipfs-core/src/runtime/repo-nodejs.js @@ -9,6 +9,7 @@ const path = require('path') * @param {string} [options.path] * @param {boolean} [options.silent] * @param {boolean} [options.autoMigrate] + * @returns {Repo} */ module.exports = (options = {}) => { const repoPath = options.path || path.join(os.homedir(), '.jsipfs') @@ -29,3 +30,8 @@ module.exports = (options = {}) => { onMigrationProgress: options.silent ? null : onMigrationProgress }) } + +/** + * @typedef {import('../interface/repo').Repo} Repo + * @typedef {import('../components/config').IPFSConfig} IPFSConfig + */ diff --git a/packages/ipfs-core/src/utils.js b/packages/ipfs-core/src/utils.js index e2f7bce6d9..6846006bf6 100644 --- a/packages/ipfs-core/src/utils.js +++ b/packages/ipfs-core/src/utils.js @@ -6,6 +6,11 @@ const CID = require('cids') const Key = require('interface-datastore').Key const errCode = require('err-code') const toCidAndPath = require('ipfs-core-utils/src/to-cid-and-path') +const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option') +/** @type {typeof Object.assign} */ +const mergeOptions = require('merge-options') + +exports.mergeOptions = mergeOptions const ERR_BAD_PATH = 'ERR_BAD_PATH' @@ -63,7 +68,7 @@ const normalizeCidPath = (path) => { * - /ipfs//link/to/pluto * - multihash Buffer * - * @param {import('./components').DAG} dag - The IPFS dag api + * @param {import('./components').DagReader} dag * @param {CID | string} ipfsPath - A CID or IPFS path * @param {Object} [options] - Optional options passed directly to dag.resolve * @returns {Promise} @@ -213,7 +218,19 @@ const mapFile = (file, options = {}) => { * @template {Record} ExtraOptions */ +const withTimeout = withTimeoutOption( + /** + * @template T + * @param {Promise|T} promise + * @param {AbortOptions} [_options] + * @returns {Promise} + */ + async (promise, _options) => await promise +) + exports.normalizePath = normalizePath exports.normalizeCidPath = normalizeCidPath exports.resolvePath = resolvePath exports.mapFile = mapFile +exports.withTimeoutOption = withTimeoutOption +exports.withTimeout = withTimeout diff --git a/packages/ipfs-core/src/utils/service.js b/packages/ipfs-core/src/utils/service.js new file mode 100644 index 0000000000..fda5020b84 --- /dev/null +++ b/packages/ipfs-core/src/utils/service.js @@ -0,0 +1,239 @@ +'use strict' + +const { NotStartedError, AlreadyStartingError, AlreadyStartedError } = require('../errors') +const { withTimeout } = require('../utils') + +/** + * @template Options, T + * + * Allows you to create a handle to service that can be started or + * stopped. It enables defining components that need to use service + * functionality before service is started. + * + */ +class Service { + /** + * Takes `activation` function that takes `options` and (async) returns + * an implementation. + * + * @template {(options:any) => Await} T + * + * @param {Object} config + * @param {T} config.start + * @param {(state:State) => Await} [config.stop] + * @returns {Service[0], State>} + */ + static create ({ start, stop }) { + return new Service(start, stop) + } + + /** + * Starts the service (by running actiavtion function). Will (async) throw + * unless service is stopped. + * + * @template Options, T + * @param {Service} service + * @param {Options} options + * @returns {Promise} + */ + static async start (service, options) { + const { state, activate } = service + switch (state.status) { + // If service is in 'stopped' state we activate and transition to + // to 'pending' state. Once activation is complete transition state to + // 'started' state. + // Note: This is the only code that does state transitions from + // - stopped + // - started + // Which ensures no race conditions can occur. + case 'stopped': { + try { + const promise = activate(options) + service.state = { status: 'starting', ready: promise } + // Note: MUST await after state transition above otherwise race + // condition may occur. + const result = await promise + service.state = { status: 'started', value: result } + return result + // If failed to start, transiton from 'starting' to 'stopped' + // state. + } catch (error) { + service.state = { status: 'stopped' } + throw error + } + } + case 'starting': { + throw new AlreadyStartingError() + } + case 'started': { + throw new AlreadyStartedError() + } + // If service is stopping we just wait for that to complete + // and try again. + case 'stopping': { + await state.ready + return await Service.start(service, options) + } + default: { + return Service.panic(service) + } + } + } + + /** + * Stops the service by executing deactivation. If service is stopped + * or is stopping this is noop. If service is starting up when called + * it will await for start to complete and then retry stop afterwards. + * This may (async) throw if `deactivate` does. + * + * @template T + * @param {Service} service + * @returns {Promise} + */ + static async stop (service) { + const { state, deactivate } = service + switch (state.status) { + // If stopped there's nothing to do. + case 'stopped': { + break + } + // If service is starting we await for it to complete + // and try again. That way + case 'starting': { + // We do not want to error stop if start failed. + try { await state.ready } catch (_) {} + return await Service.stop(service) + } + // if service is stopping we just await for it to complete. + case 'stopping': { + return await state.ready + } + case 'started': { + if (deactivate) { + await deactivate(state.value) + } + service.state = { status: 'stopped' } + break + } + default: { + Service.panic(state) + } + } + } + + /** + * @template T + * @param {Service} service + * @returns {T|null} + */ + static try ({ state }) { + switch (state.status) { + case 'started': + return state.value + default: + return null + } + } + + /** + * Unwraps state and returns underlying value. If state is in idle state it + * will throw an error. If state is pending it will wait and return the + * result or throw on failure. If state is ready returns result. + * + * @template T + * @param {Service} service + * @param {AbortOptions} [options] + * @returns {Promise} + */ + static async use ({ state }, options) { + switch (state.status) { + case 'started': + return state.value + case 'starting': + return await withTimeout(state.ready, options) + default: + throw new NotStartedError() + } + } + + // eslint-disable-next-line jsdoc/require-returns-check + /** + * @private + * @param {Service} service + * @returns {never} + */ + static panic ({ state }) { + const status = JSON.stringify({ status: state.status }) + throw RangeError(`Service in invalid state ${status}, should never happen if you see this please report a bug`) + } + + /** + * Takes `activation` function that takes `options` and (async) returns + * an implementation. + * + * @private + * @param {(options:Options) => Await} activate + * @param {(state:T) => Await} [deactivate] + */ + constructor (activate, deactivate) { + this.activate = activate + this.deactivate = deactivate + + /** + * A state machine for this service. + * + * @private + * @type {ServiceState} + */ + this.state = { status: 'stopped' } + } + + /** + * Allows you to asynchronously obtain service implementation. If service + * is starting it will await for completion. If service is stopped or stopping + * this will (async) throw exception. This allows components that need to use + * this service convenient API to do it. + * + * @param {AbortOptions} [options] - Abort options. + * @returns {Promise} + */ + async use (options) { + return await Service.use(this, options) + } + + /** + * @returns {T|null} + */ + try () { + return Service.try(this) + } +} +module.exports = Service + +/** + * @template T + * @typedef {import('../interface/basic').Await} Await + */ +/** + * @template {(options:any) => any} T + * @typedef {Parameters[0]} Options + */ +/** + * @template {(options:any) => any} T + * @typedef {ReturnType extends ? Promise ? U : ReturnType} State + */ +/** + * Represents service state which can be not started in which case + * it is instanceof `Error`. Pending in which case it's promise or + * ready in which case it is the value itself. + * + * @template T + * @typedef {{ status: 'stopped' } + * | { status: 'starting', ready: Await } + * | { status: 'started', value: T } + * | { status: 'stopping', ready: Await } + * } ServiceState + */ +/** + * @typedef {import('../utils').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/test/create-node.spec.js b/packages/ipfs-core/test/create-node.spec.js index e666f040cd..c7c8abacc8 100644 --- a/packages/ipfs-core/test/create-node.spec.js +++ b/packages/ipfs-core/test/create-node.spec.js @@ -87,9 +87,8 @@ describe('create node', function () { expect(ipfs.isOnline()).to.be.false() }) - it('should create but not initialize and not start', async () => { + it('should create but not start', async () => { const ipfs = await IPFS.create({ - init: false, start: false, repo: tempRepo, config: { Addresses: { Swarm: [] } } diff --git a/packages/ipfs-core/test/init.spec.js b/packages/ipfs-core/test/init.spec.js index f43733adad..273594a7fb 100644 --- a/packages/ipfs-core/test/init.spec.js +++ b/packages/ipfs-core/test/init.spec.js @@ -21,20 +21,22 @@ describe('init', function () { let repo let cleanup - beforeEach(async () => { + const init = async (options) => { const res = await createNode({ - init: false, + init: options, start: false }) + ipfs = res.ipfs repo = res.repo cleanup = res.cleanup - }) + return ipfs + } afterEach(() => cleanup()) it('should init successfully', async () => { - await ipfs.init({ bits: 512 }) + await init({ bits: 512 }) const res = await repo.exists() expect(res).to.equal(true) @@ -46,7 +48,7 @@ describe('init', function () { }) it('should init successfully with a keychain pass', async () => { - await ipfs.init({ bits: 512, pass: nanoid() }) + await init({ bits: 512, pass: nanoid() }) const res = await repo.exists() expect(res).to.equal(true) @@ -60,7 +62,7 @@ describe('init', function () { }) it('should init with a key algorithm (ed25519)', async () => { - await ipfs.init({ algorithm: 'ed25519' }) + await init({ algorithm: 'ed25519' }) const config = await repo.config.getAll() const peerId = await PeerId.createFromPrivKey(config.Identity.PrivKey) @@ -68,7 +70,7 @@ describe('init', function () { }) it('should init with a key algorithm (secp256k1)', async () => { - await ipfs.init({ algorithm: 'secp256k1' }) + await init({ algorithm: 'secp256k1' }) const config = await repo.config.getAll() const peerId = await PeerId.createFromPrivKey(config.Identity.PrivKey) @@ -78,35 +80,35 @@ describe('init', function () { it('should set # of bits in key', async function () { this.timeout(120 * 1000) - await ipfs.init({ bits: 1024 }) + await init({ bits: 1024 }) const config = await repo.config.getAll() expect(config.Identity.PrivKey.length).is.above(256) }) it('should allow a pregenerated key to be used', async () => { - await ipfs.init({ privateKey }) + await init({ privateKey }) const config = await repo.config.getAll() expect(config.Identity.PeerID).is.equal('QmRsooYQasV5f5r834NSpdUtmejdQcpxXkK6qsozZWEihC') }) it('should allow a pregenerated ed25519 key to be used', async () => { - await ipfs.init({ privateKey: edPrivateKey }) + await init({ privateKey: edPrivateKey }) const config = await repo.config.getAll() expect(config.Identity.PeerID).is.equal('12D3KooWRm8J3iL796zPFi2EtGGtUJn58AG67gcqzMFHZnnsTzqD') }) it('should allow a pregenerated secp256k1 key to be used', async () => { - await ipfs.init({ privateKey: secpPrivateKey }) + await init({ privateKey: secpPrivateKey }) const config = await repo.config.getAll() expect(config.Identity.PeerID).is.equal('16Uiu2HAm5qw8UyXP2RLxQUx5KvtSN8DsTKz8quRGqGNC3SYiaB8E') }) it('should write init docs', async () => { - await ipfs.init({ bits: 512 }) + await init({ bits: 512 }) const multihash = 'QmPZ9gcCEpqKTo6aq61g2nXGUhM4iCL3ewB6LDXZCtioEB' const node = await ipfs.object.get(multihash, { enc: 'base58' }) @@ -114,7 +116,7 @@ describe('init', function () { }) it('should allow init with an empty repo', async () => { - await ipfs.init({ bits: 512, emptyRepo: true }) + await init({ bits: 512, emptyRepo: true }) // Should not have default assets const multihash = uint8ArrayFromString('12205e7c3ce237f936c76faf625e90f7751a9f5eeb048f59873303c215e9cce87599', 'base16') @@ -122,14 +124,14 @@ describe('init', function () { }) it('should apply one profile', async () => { - await ipfs.init({ bits: 512, profiles: ['test'] }) + await init({ bits: 512, profiles: ['test'] }) const config = await repo.config.getAll() expect(config.Bootstrap).to.be.empty() }) it('should apply multiple profiles', async () => { - await ipfs.init({ bits: 512, profiles: ['test', 'local-discovery'] }) + await init({ bits: 512, profiles: ['test', 'local-discovery'] }) const config = await repo.config.getAll() expect(config.Bootstrap).to.be.empty() diff --git a/packages/ipfs-daemon/src/index.js b/packages/ipfs-daemon/src/index.js index 79f01e70fa..46b623f39c 100644 --- a/packages/ipfs-daemon/src/index.js +++ b/packages/ipfs-daemon/src/index.js @@ -42,7 +42,7 @@ class Daemon { : this._options.repo // start the daemon - const ipfsOpts = Object.assign({}, { init: true, start: true, libp2p: getLibp2p }, this._options, { repo }) + const ipfsOpts = Object.assign({}, { start: true, libp2p: getLibp2p }, this._options, { repo }) const ipfs = this._ipfs = await IPFS.create(ipfsOpts) // start HTTP servers (if API or Gateway is enabled in options) diff --git a/packages/ipfs-http-client/package.json b/packages/ipfs-http-client/package.json index 0a13b70b0b..315abda5e0 100644 --- a/packages/ipfs-http-client/package.json +++ b/packages/ipfs-http-client/package.json @@ -80,7 +80,7 @@ "aegir": "^29.2.2", "go-ipfs": "^0.7.0", "ipfs-core": "^0.3.0", - "ipfsd-ctl": "^7.0.2", + "ipfsd-ctl": "^7.1.1", "it-all": "^1.0.4", "it-concat": "^1.0.1", "nock": "^13.0.2", diff --git a/packages/ipfs-http-client/src/block/put.js b/packages/ipfs-http-client/src/block/put.js index 669ea710d0..bf30422c74 100644 --- a/packages/ipfs-http-client/src/block/put.js +++ b/packages/ipfs-http-client/src/block/put.js @@ -23,6 +23,8 @@ module.exports = configure(api => { mhlen: length, version: data.cid.version } + // @ts-ignore - data is typed as block so TS complains about + // Uint8Array assignment. data = data.data } else if (options.cid) { const cid = new CID(options.cid) @@ -64,7 +66,7 @@ module.exports = configure(api => { throw err } - return new Block(data, new CID(res.Key)) + return new Block(/** @type {Uint8Array} */(data), new CID(res.Key)) } return put diff --git a/packages/ipfs-http-client/src/dag/put.js b/packages/ipfs-http-client/src/dag/put.js index b1d0e8ae01..1f58b32278 100644 --- a/packages/ipfs-http-client/src/dag/put.js +++ b/packages/ipfs-http-client/src/dag/put.js @@ -23,21 +23,24 @@ module.exports = configure((api, opts) => { throw new Error('Failed to put DAG node. Provide `format` AND `hashAlg` options') } + let encodingOptions if (options.cid) { const cid = new CID(options.cid) - options = { + encodingOptions = { ...options, format: multicodec.getName(cid.code), hashAlg: multihash.decode(cid.multihash).name } delete options.cid + } else { + encodingOptions = options } const settings = { format: 'dag-cbor', hashAlg: 'sha2-256', inputEnc: 'raw', - ...options + ...encodingOptions } const format = await load(settings.format) diff --git a/packages/ipfs/package.json b/packages/ipfs/package.json index 2f0da089a3..d20167faf8 100644 --- a/packages/ipfs/package.json +++ b/packages/ipfs/package.json @@ -53,7 +53,7 @@ "ipfs-http-client": "^48.1.2", "ipfs-interop": "^3.0.0", "ipfs-utils": "^5.0.0", - "ipfsd-ctl": "^7.0.2", + "ipfsd-ctl": "^7.1.1", "iso-url": "^1.0.0", "libp2p-webrtc-star": "^0.20.1", "merge-options": "^2.0.0",