Skip to content
This repository was archived by the owner on Feb 12, 2024. It is now read-only.

Commit b84467e

Browse files
author
Alan Shaw
committed
feat: EXPERIMENTAL ipfsx API - boot procedure and add API method
This PR allows ipfsx to be used by calling `IPFS.create(options)` with `{ EXPERIMENTAL: { ipfsx: true } }` options. It adds a single API method `add` that returns an iterator that yields objects of the form `{ cid, path, size }`. The iterator is decorated with a `first` and `last` function so users can conveniently `await` on the first or last item to be yielded as per the [proposal here](https://github.com/ipfs-shipyard/ipfsx/blob/master/API.md#add). In order to boot up a new ipfsx node I refactored the boot procedure to enable the following: 1. **Remove the big stateful blob "`self`" - components are passed just the dependencies they need to operate.** Right now it is opaque as to which components require which parts of an IPFS node without inspecting the entirety of the component's code. This change makes it easier to look at a component and know what aspects of the IPFS stack it uses and consequently allows us to understand which APIs should be available at which points of the node's lifecycle. It makes the code easier to understand, more maintainable and easier to mock dependencies for unit tests. 1. **Restrict APIs to appropriate lifecycle stage(s).** This PR introduces an `ApiManager` that allows us to update the API that is exposed at any given point. It allows us to (for example) disallow `ipfs.add` before the node is initialized or access `libp2p` before the node is started. The lifecycle methods `init`, `start` and `stop` each define which API methods are available after they have run avoiding having to put boilerplate in every method to check if it can be called when the node is in a particular state. See #1438 1. **Safer and more flexible API usage.** The `ApiManager` allows us to temporarily change APIs to stop `init` from being called again while it is already running and has the facility to rollback to the previous API state if an operation fails. It also enables piggybacking so we don't attempt 2 or more concurrent start/stop calls at once. See #1061 #2257 1. **Enable config changes at runtime.** Having an API that can be updated during a node's lifecycle will enable this feature in the future. **FEEDBACK REQUIRED**: The changes I've made here are a little...racy. They have a bunch of benefits, as I've outlined above but the `ApiManager` is implemented as a `Proxy`, allowing us to swap out the underlying API at will. How do y'all feel about that? Is there a better way or got a suggestion? resolves #1438 resolves #1061 resolves #2257 refs #2509 refs #1670 License: MIT Signed-off-by: Alan Shaw <[email protected]>
1 parent 79db03b commit b84467e

19 files changed

+917
-234
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"main": "src/core/index.js",
1717
"browser": {
1818
"./src/core/components/init-assets.js": false,
19+
"./src/core/runtime/init-assets-nodejs.js": "./src/core/runtime/init-assets-browser.js",
1920
"./src/core/runtime/add-from-fs-nodejs.js": "./src/core/runtime/add-from-fs-browser.js",
2021
"./src/core/runtime/config-nodejs.js": "./src/core/runtime/config-browser.js",
2122
"./src/core/runtime/dns-nodejs.js": "./src/core/runtime/dns-browser.js",

src/core/api-manager.js

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
module.exports = class ApiManager {
2+
constructor () {
3+
this._api = {}
4+
this._onUndef = () => undefined
5+
this.api = new Proxy({}, {
6+
get (target, prop) {
7+
return target[prop] === undefined
8+
? this._onUndef(prop)
9+
: target[prop]
10+
}
11+
})
12+
}
13+
14+
update (nextApi, onUndef) {
15+
const prevApi = this._api
16+
const prevUndef = this._onUndef
17+
this._api = nextApi
18+
if (onUndef) this._onUndef = onUndef
19+
return { cancel: () => this.update(prevApi, prevUndef), api: this.api }
20+
}
21+
}
+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
'use strict'
2+
3+
const importer = require('ipfs-unixfs-importer')
4+
const normaliseAddInput = require('ipfs-utils/src/files/normalise-input')
5+
const { parseChunkerString } = require('./utils')
6+
const pipe = require('it-pipe')
7+
const { withFirstAndLast } = require('../../utils')
8+
9+
module.exports = ({ ipld, dag, gcLock, preload, pin, constructorOptions }) => {
10+
return withFirstAndLast(async function * add (source, options) {
11+
options = options || {}
12+
13+
const opts = {
14+
shardSplitThreshold: constructorOptions.EXPERIMENTAL.sharding ? 1000 : Infinity,
15+
...options,
16+
strategy: 'balanced',
17+
...parseChunkerString(options.chunker)
18+
}
19+
20+
// CID v0 is for multihashes encoded with sha2-256
21+
if (opts.hashAlg && opts.cidVersion !== 1) {
22+
opts.cidVersion = 1
23+
}
24+
25+
if (opts.trickle) {
26+
opts.strategy = 'trickle'
27+
}
28+
29+
delete opts.trickle
30+
31+
if (opts.progress) {
32+
let total = 0
33+
const prog = opts.progress
34+
35+
opts.progress = (bytes) => {
36+
total += bytes
37+
prog(total)
38+
}
39+
}
40+
41+
const iterator = pipe(
42+
normaliseAddInput(source),
43+
source => importer(source, ipld, opts),
44+
transformFile(dag, opts),
45+
preloadFile(preload, opts),
46+
pinFile(pin, opts)
47+
)
48+
49+
const releaseLock = await gcLock.readLock()
50+
51+
try {
52+
yield * iterator
53+
} finally {
54+
releaseLock()
55+
}
56+
})
57+
}
58+
59+
function transformFile (dag, opts) {
60+
return async function * (source) {
61+
for await (const { cid, path, unixfs } of source) {
62+
if (opts.onlyHash) {
63+
yield {
64+
cid,
65+
path: path || cid.toString(),
66+
size: unixfs.fileSize()
67+
}
68+
69+
continue
70+
}
71+
72+
const node = await dag.get(cid, { ...opts, preload: false })
73+
74+
yield {
75+
cid,
76+
path: path || cid.toString(),
77+
size: Buffer.isBuffer(node) ? node.length : node.size
78+
}
79+
}
80+
}
81+
}
82+
83+
function preloadFile (preload, opts) {
84+
return async function * (source) {
85+
for await (const file of source) {
86+
const isRootFile = !file.path || opts.wrapWithDirectory
87+
? file.path === ''
88+
: !file.path.includes('/')
89+
90+
const shouldPreload = isRootFile && !opts.onlyHash && opts.preload !== false
91+
92+
if (shouldPreload) {
93+
preload(file.hash)
94+
}
95+
96+
yield file
97+
}
98+
}
99+
}
100+
101+
function pinFile (pin, opts) {
102+
return async function * (source) {
103+
for await (const file of source) {
104+
// Pin a file if it is the root dir of a recursive add or the single file
105+
// of a direct add.
106+
const pin = 'pin' in opts ? opts.pin : true
107+
const isRootDir = !file.path.includes('/')
108+
const shouldPin = pin && isRootDir && !opts.onlyHash
109+
110+
if (shouldPin) {
111+
// Note: addAsyncIterator() has already taken a GC lock, so tell
112+
// pin.add() not to take a (second) GC lock
113+
await pin.add(file.hash, {
114+
preload: false,
115+
lock: false
116+
})
117+
}
118+
119+
yield file
120+
}
121+
}
122+
}
+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
'use strict'
2+
3+
/**
4+
* Parses chunker string into options used by DAGBuilder in ipfs-unixfs-engine
5+
*
6+
*
7+
* @param {String} chunker Chunker algorithm supported formats:
8+
* "size-{size}"
9+
* "rabin"
10+
* "rabin-{avg}"
11+
* "rabin-{min}-{avg}-{max}"
12+
*
13+
* @return {Object} Chunker options for DAGBuilder
14+
*/
15+
const parseChunkerString = (chunker) => {
16+
if (!chunker) {
17+
return {
18+
chunker: 'fixed'
19+
}
20+
} else if (chunker.startsWith('size-')) {
21+
const sizeStr = chunker.split('-')[1]
22+
const size = parseInt(sizeStr)
23+
if (isNaN(size)) {
24+
throw new Error('Chunker parameter size must be an integer')
25+
}
26+
return {
27+
chunker: 'fixed',
28+
chunkerOptions: {
29+
maxChunkSize: size
30+
}
31+
}
32+
} else if (chunker.startsWith('rabin')) {
33+
return {
34+
chunker: 'rabin',
35+
chunkerOptions: parseRabinString(chunker)
36+
}
37+
} else {
38+
throw new Error(`Unrecognized chunker option: ${chunker}`)
39+
}
40+
}
41+
42+
/**
43+
* Parses rabin chunker string
44+
*
45+
* @param {String} chunker Chunker algorithm supported formats:
46+
* "rabin"
47+
* "rabin-{avg}"
48+
* "rabin-{min}-{avg}-{max}"
49+
*
50+
* @return {Object} rabin chunker options
51+
*/
52+
const parseRabinString = (chunker) => {
53+
const options = {}
54+
const parts = chunker.split('-')
55+
switch (parts.length) {
56+
case 1:
57+
options.avgChunkSize = 262144
58+
break
59+
case 2:
60+
options.avgChunkSize = parseChunkSize(parts[1], 'avg')
61+
break
62+
case 4:
63+
options.minChunkSize = parseChunkSize(parts[1], 'min')
64+
options.avgChunkSize = parseChunkSize(parts[2], 'avg')
65+
options.maxChunkSize = parseChunkSize(parts[3], 'max')
66+
break
67+
default:
68+
throw new Error('Incorrect chunker format (expected "rabin" "rabin-[avg]" or "rabin-[min]-[avg]-[max]"')
69+
}
70+
71+
return options
72+
}
73+
74+
const parseChunkSize = (str, name) => {
75+
const size = parseInt(str)
76+
if (isNaN(size)) {
77+
throw new Error(`Chunker parameter ${name} must be an integer`)
78+
}
79+
80+
return size
81+
}
82+
83+
module.exports = {
84+
parseChunkSize,
85+
parseRabinString,
86+
parseChunkerString
87+
}

src/core/components-ipfsx/index.js

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use strict'
2+
3+
module.exports = {
4+
add: require('./add'),
5+
init: require('./init'),
6+
start: require('./start'),
7+
stop: require('./stop'),
8+
legacy: {
9+
config: require('../components/config'),
10+
dag: require('../components/dag'),
11+
libp2p: require('../components/libp2p'),
12+
object: require('../components/object'),
13+
pin: require('../components/pin')
14+
}
15+
}

0 commit comments

Comments
 (0)