Skip to content

Commit 2b5ca47

Browse files
authored
fix: restore standalone ipld instance (#289)
This PR partially reverts #287 because `/api/v0/dag` in go-ipfs do not implement latest IPLD features yet, and ipfs-http-client delegates path traversal and resolution to go-ipfs, so codecs in JS are never used. By reverting to standalone js-ipld we decouple IPLD explorer from what is available in go-ipfs – we simply fetch raw block and do all decoding the old way, in JS. We will revisit this in the future, after IPLD Prime work lands in go-ipfs, but for now this is the only way to fix IPLD Explorer to work against go-ipfs backend.
1 parent d7ea32b commit 2b5ca47

File tree

8 files changed

+1467
-379
lines changed

8 files changed

+1467
-379
lines changed

package-lock.json

Lines changed: 1341 additions & 314 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,28 +18,33 @@
1818
"start": "cross-env NODE_ENV=production babel src -d dist --copy-files --ignore '**/*.test.js' --watch",
1919
"storybook": "start-storybook -p 9009 --static-dir public",
2020
"storybook:build": "build-storybook -c .storybook --static-dir public --output-dir build",
21-
"test": "react-scripts test"
21+
"test": "react-scripts test --env=jsdom"
2222
},
2323
"dependencies": {
24-
"@babel/cli": "^7.13.10",
24+
"@babel/cli": "^7.13.14",
2525
"@loadable/component": "^5.14.1",
2626
"@tableflip/react-inspector": "^2.3.0",
2727
"cids": "^1.1.6",
2828
"cytoscape": "^3.18.1",
2929
"cytoscape-dagre": "^2.3.2",
3030
"filesize": "^6.1.0",
3131
"ipfs-unixfs": "^4.0.1",
32+
"ipld": "0.29.0",
33+
"ipld-dag-cbor": "0.18.0",
34+
"ipld-ethereum": "6.0.0",
35+
"ipld-git": "0.6.4",
36+
"ipld-raw": "7.0.0",
3237
"milliseconds": "^1.0.3",
3338
"multibase": "^4.0.4",
3439
"multicodec": "^3.0.1",
3540
"multihashes": "^4.0.2",
3641
"react-joyride": "^2.3.0"
3742
},
3843
"devDependencies": {
39-
"@babel/core": "^7.13.10",
44+
"@babel/core": "^7.13.15",
4045
"@babel/plugin-proposal-class-properties": "^7.13.0",
41-
"@babel/preset-env": "^7.13.12",
42-
"@babel/preset-react": "^7.12.13",
46+
"@babel/preset-env": "^7.13.15",
47+
"@babel/preset-react": "^7.13.13",
4348
"@storybook/addon-a11y": "^5.3.21",
4449
"@storybook/addon-actions": "^5.3.21",
4550
"@storybook/addon-knobs": "^5.3.21",
@@ -64,7 +69,7 @@
6469
"i18next-localstorage-backend": "3.1.2",
6570
"intl-messageformat": "^9.6.4",
6671
"ipfs-css": "^1.3.0",
67-
"ipld-dag-pb": "^0.22.1",
72+
"ipld-dag-pb": "0.22.2",
6873
"react": "^16.14.0",
6974
"react-dom": "^16.14.0",
7075
"react-helmet": "^5.2.1",

src/bundles/explore.js

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import parseIpldPath from '../lib/parse-ipld-path'
55

66
// Find all the nodes and path boundaries traversed along a given path
77
const makeBundle = () => {
8+
// Lazy load ipld because it is a large dependency
9+
let IpldResolver = null
10+
let ipldFormats = null
11+
812
const bundle = createAsyncResourceBundle({
913
name: 'explore',
1014
actionBaseType: 'EXPLORE',
@@ -16,15 +20,22 @@ const makeBundle = () => {
1620
if (!pathParts) return null
1721
const { cidOrFqdn, rest } = pathParts
1822
try {
23+
if (!IpldResolver) {
24+
const { ipld, formats } = await getIpld()
25+
26+
IpldResolver = ipld
27+
ipldFormats = formats
28+
}
29+
const ipld = makeIpld(IpldResolver, ipldFormats, getIpfs)
30+
// TODO: handle ipns, which would give us a fqdn in the cid position.
1931
const cid = new Cid(cidOrFqdn)
20-
const ipfs = await getIpfs()
2132
const {
2233
targetNode,
2334
canonicalPath,
2435
localPath,
2536
nodes,
2637
pathBoundaries
27-
} = await resolveIpldPath(ipfs, cid, rest)
38+
} = await resolveIpldPath(ipld, cid, rest)
2839

2940
return {
3041
path,
@@ -101,4 +112,31 @@ function ensureLeadingSlash (str) {
101112
return `/${str}`
102113
}
103114

115+
function makeIpld (IpldResolver, ipldFormats, getIpfs) {
116+
return new IpldResolver({
117+
blockService: getIpfs().block,
118+
formats: ipldFormats
119+
})
120+
}
121+
122+
async function getIpld () {
123+
const ipldDeps = await Promise.all([
124+
import(/* webpackChunkName: "ipld" */ 'ipld'),
125+
import(/* webpackChunkName: "ipld" */ 'ipld-dag-cbor'),
126+
import(/* webpackChunkName: "ipld" */ 'ipld-dag-pb'),
127+
import(/* webpackChunkName: "ipld" */ 'ipld-git'),
128+
import(/* webpackChunkName: "ipld" */ 'ipld-raw'),
129+
import(/* webpackChunkName: "ipld" */ 'ipld-ethereum')
130+
])
131+
132+
// CommonJs exports object is .default when imported ESM style
133+
const [ipld, ...formats] = ipldDeps.map(mod => mod.default)
134+
135+
// ipldEthereum is an Object, each key points to a ipld format impl
136+
const ipldEthereum = formats.pop()
137+
formats.push(...Object.values(ipldEthereum))
138+
139+
return { ipld, formats }
140+
}
141+
104142
export default makeBundle

src/components/StartExploringPage.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,13 @@ const StartExploringPage = ({ t, embed, runTour = false, joyrideCallback }) => (
3636
<ExploreSuggestion name='Project Apollo Archives' cid='QmSnuWmxptJZdLJpKRarxBMS2Ju2oANVrgbr2xWbie9b2D' type='dag-pb' />
3737
</li>
3838
<li>
39-
<ExploreSuggestion name='IGIS git repo' cid='baf4bcfg4ep767tjp5lxyanx5urpjjgx5q2volvy' type='git-raw' />
39+
<ExploreSuggestion name='IGIS Git Repo' cid='baf4bcfg4ep767tjp5lxyanx5urpjjgx5q2volvy' type='git-raw' />
4040
</li>
4141
<li>
4242
<ExploreSuggestion name='An Ethereum Block' cid='bagiacgzah24drzou2jlkixpblbgbg6nxfrasoklzttzoht5hixhxz3rlncyq' type='eth-block' />
4343
</li>
4444
<li>
45-
<ExploreSuggestion name='XKCD' cid='QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm' type='dag-pb' />
45+
<ExploreSuggestion name='XKCD Archives' cid='QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm' type='dag-pb' />
4646
</li>
4747
</ul>
4848
</div>
@@ -63,10 +63,10 @@ const StartExploringPage = ({ t, embed, runTour = false, joyrideCallback }) => (
6363

6464
/* TODO: add dag-cbor and raw block examples
6565
<li>
66-
<ExploreSuggestion name='DAG-CBOR Block' cid='bafkreibm6jg3ux5qumhcn2b3flc3tyu6dmlb4xa7u5bf44yegnrjhc4yeq' type='dag-cbor' />
66+
<ExploreSuggestion name='DAG-CBOR Block' cid='bafyreicnokmhmrnlp2wjhyk2haep4tqxiptwfrp2rrs7rzq7uk766chqvq' type='dag-cbor' />
6767
</li>
6868
<li>
69-
<ExploreSuggestion name='Plain text as raw bytes' cid='bafkreibm6jg3ux5qumhcn2b3flc3tyu6dmlb4xa7u5bf44yegnrjhc4yeq' type='raw' />
69+
<ExploreSuggestion name='Raw Block for "hello"' cid='bafkreibm6jg3ux5qumhcn2b3flc3tyu6dmlb4xa7u5bf44yegnrjhc4yeq' type='raw' />
7070
</li>
7171
*/
7272

src/components/object-info/ObjectInfo.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ const objectInspectorTheme = {
1717

1818
// TODO: Use https://github.com/multiformats/multicodec/blob/master/table.csv to get full name.
1919
const nodeStyles = {
20-
'dag-cbor': { shortName: 'CBOR', name: 'CBOR', color: '#28CA9F' },
21-
'dag-pb': { shortName: 'PB', name: 'Protobuf', color: '#244e66' },
20+
'dag-cbor': { shortName: 'CBOR', name: 'dag-cbor', color: '#28CA9F' },
21+
'dag-pb': { shortName: 'PB', name: 'dag-pb', color: '#244e66' },
2222
'git-raw': { shortName: 'GIT', name: 'Git', color: '#378085' },
2323
'raw': { shortName: 'RAW', name: 'Raw Block', color: '#f14e32' }, // eslint-disable-line quote-props
2424
'eth-block': { shortName: 'ETH', name: 'Ethereum Block', color: '#383838' },
@@ -71,7 +71,7 @@ const ObjectInfo = ({ t, tReady, className, type, cid, localPath, size, data, li
7171
{nameForNode(type)}
7272
</span>
7373
{format === 'unixfs' ? (
74-
<a className='dn di-ns link charcoal ml2' href='https://docs.ipfs.io/concepts/file-systems/#unix-file-system-unixfs'>UnixFS</a>
74+
<a className='dn di-ns link charcoal ml2' href='https://docs.ipfs.io/concepts/file-systems/#unix-file-system-unixfs' target='_external'>UnixFS</a>
7575
) : null}
7676
{format === 'unixfs' && data.type && ['directory', 'file'].some(x => x === data.type) ? (
7777
<a className='link avenir ml2 pa2 fw5 f6 blue' href={`${gatewayUrl}/ipfs/${cid}`} target='_external'>

src/lib/cid.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@ export function getCodecOrNull (value) {
1717

1818
export function toCidStrOrNull (value) {
1919
const cid = toCidOrNull(value)
20-
return cid ? cid.toBaseEncodedString() : null
20+
return cid ? cid.toString() : null
2121
}

src/lib/resolve-ipld-path.js

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ import Cid from 'cids'
3535
* @param {Object[]} pathBoundaries accumulated path boundary info
3636
* @returns {{targetNode: Object, canonicalPath: String, localPath: String, nodes: Object[], pathBoundaries: Object[]}} resolved path info
3737
*/
38-
export default async function resolveIpldPath (ipfs, sourceCid, path, nodes = [], pathBoundaries = []) {
39-
const { value, remainderPath } = await ipldGetNodeAndRemainder(ipfs, sourceCid, path)
38+
export default async function resolveIpldPath (ipld, sourceCid, path, nodes = [], pathBoundaries = []) {
39+
const { value, remainderPath } = await ipldGetNodeAndRemainder(ipld, sourceCid, path)
4040
const sourceCidStr = sourceCid.toString()
4141

4242
const node = normaliseDagNode(value, sourceCidStr)
@@ -47,17 +47,28 @@ export default async function resolveIpldPath (ipfs, sourceCid, path, nodes = []
4747
if (link) {
4848
pathBoundaries.push(link)
4949
// Go again, using the link.target as the sourceCid, and the remainderPath as the path.
50-
return resolveIpldPath(ipfs, new Cid(link.target), remainderPath, nodes, pathBoundaries)
50+
return resolveIpldPath(ipld, new Cid(link.target), remainderPath, nodes, pathBoundaries)
5151
}
5252
// we made it to the containing node. Hand back the info
5353
const canonicalPath = path ? `${sourceCidStr}${path}` : sourceCidStr
5454
const targetNode = node
5555
return { targetNode, canonicalPath, localPath: path, nodes, pathBoundaries }
5656
}
5757

58-
export async function ipldGetNodeAndRemainder (ipfs, sourceCid, path) {
59-
const { value, remainderPath } = await ipfs.dag.get(sourceCid, path, { localResolve: true })
60-
return { value, remainderPath }
58+
export async function ipldGetNodeAndRemainder (ipld, sourceCid, path) {
59+
// TODO: find out why ipfs.dag.get with localResolve never resolves.
60+
// const {value, remainderPath} = await getIpfs().dag.get(sourceCid, path, {localResolve: true})
61+
62+
// TODO: use ipfs.dag.get when it gets ipld super powers
63+
// SEE: https://github.com/ipfs/js-ipfs-api/pull/755
64+
// const {value} = await getIpfs().dag.get(sourceCid)
65+
66+
// TODO: handle indexing into dag-pb links without using Links prefix as per go-ipfs dag.get does.
67+
// Current js-ipld-dag-pb resolver will throw with a path not available error if Links prefix is missing.
68+
return {
69+
value: await ipld.get(sourceCid),
70+
remainderPath: (await ipld.resolve(sourceCid, path || '/').first()).remainderPath
71+
}
6172
}
6273

6374
/**

src/lib/resolve-ipld-path.test.js

Lines changed: 50 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,37 @@ import { DAGNode } from 'ipld-dag-pb'
44
import resolveIpldPath, { findLinkPath } from './resolve-ipld-path'
55

66
it('resolves all nodes traversed along a path', async () => {
7-
const ipfsMock = {
8-
dag: { get: jest.fn() }
7+
const ipldMock = {
8+
get: jest.fn(),
9+
resolve: jest.fn()
910
}
1011
const cid = 'zdpuAs8sJjcmsPUfB1bUViftCZ8usnvs2cXrPH6MDyT4zrvSs'
1112
const path = '/a/b/a'
1213
const linkCid = 'zdpuAyzU5ahAKr5YV24J5TqrDX8PhzHLMkxx69oVzkBDWHnjq'
1314
const dagGetRes1 = {
14-
value: {
15-
a: {
16-
b: new CID(linkCid)
17-
}
18-
},
19-
remainderPath: '/a'
15+
a: {
16+
b: new CID(linkCid)
17+
}
2018
}
2119
const dagGetRes2 = {
22-
value: {
23-
a: 'hello world'
24-
},
25-
remainderPath: '/a'
20+
first: () => Promise.resolve({ remainderPath: '/a' })
21+
}
22+
const dagGetRes3 = {
23+
a: 'hello world'
24+
}
25+
const dagGetRes4 = {
26+
first: () => Promise.resolve({ remainderPath: '/a' })
2627
}
2728

28-
ipfsMock.dag.get.mockReturnValueOnce(Promise.resolve(dagGetRes1))
29-
ipfsMock.dag.get.mockReturnValueOnce(Promise.resolve(dagGetRes2))
29+
ipldMock.get.mockReturnValueOnce(Promise.resolve(dagGetRes1))
30+
ipldMock.resolve.mockReturnValueOnce(dagGetRes2)
31+
ipldMock.get.mockReturnValueOnce(Promise.resolve(dagGetRes3))
32+
ipldMock.resolve.mockReturnValueOnce(dagGetRes4)
3033

31-
const res = await resolveIpldPath(ipfsMock, new CID(cid), path)
34+
const res = await resolveIpldPath(ipldMock, new CID(cid), path)
3235

33-
expect(ipfsMock.dag.get.mock.calls.length).toBe(2)
36+
expect(ipldMock.get.mock.calls.length).toBe(2)
37+
expect(ipldMock.resolve.mock.calls.length).toBe(2)
3438
expect(res.canonicalPath).toBe(`${linkCid}/a`)
3539
expect(res.nodes.length).toBe(2)
3640
expect(res.nodes[0].type).toBe('dag-cbor')
@@ -45,20 +49,19 @@ it('resolves all nodes traversed along a path', async () => {
4549
})
4650
})
4751

48-
it('resolves thru dag-cbor to dag-pb', async () => {
49-
const ipfsMock = {
50-
dag: {
51-
get: jest.fn()
52-
}
52+
it('resolves thru dag-cbor to dag-pb to dag-pb', async () => {
53+
const ipldMock = {
54+
get: jest.fn(),
55+
resolve: jest.fn()
5356
}
5457

5558
const cid = 'zdpuAs8sJjcmsPUfB1bUViftCZ8usnvs2cXrPH6MDyT4zrvSs'
5659
const path = '/a/b/pb1'
5760

58-
const dagNode3 = await createDagPbNode(new Uint8Array(Buffer.from('the second pb node')), [])
61+
const dagNode3 = await createDagPbNode('the second pb node', [])
5962
const dagNode3CID = 'QmRLacjo71FTzKFELa7Yf5YqMwdftKNDNFq7EiE13uohar'
6063

61-
const dagNode2 = await createDagPbNode(new Uint8Array(Buffer.from('the first pb node')), [{
64+
const dagNode2 = await createDagPbNode('the first pb node', [{
6265
name: 'pb1',
6366
cid: dagNode3CID,
6467
size: 101
@@ -71,28 +74,35 @@ it('resolves thru dag-cbor to dag-pb', async () => {
7174
}
7275
}
7376

74-
const dagGetRes1 = {
75-
value: dagNode1,
76-
remainderPath: 'pb1'
77-
}
77+
const dagGetRes1 = dagNode1
7878

7979
const dagGetRes2 = {
80-
value: dagNode2,
81-
remainderPath: ''
80+
first: () => Promise.resolve({ remainderPath: 'pb1' })
8281
}
8382

84-
const dagGetRes3 = {
85-
value: dagNode3,
86-
remainderPath: ''
83+
const dagGetRes3 = dagNode2
84+
85+
const dagGetRes4 = {
86+
first: () => Promise.resolve({ remainderPath: '' })
8787
}
8888

89-
ipfsMock.dag.get.mockReturnValueOnce(Promise.resolve(dagGetRes1))
90-
ipfsMock.dag.get.mockReturnValueOnce(Promise.resolve(dagGetRes2))
91-
ipfsMock.dag.get.mockReturnValueOnce(Promise.resolve(dagGetRes3))
89+
const dagGetRes5 = dagNode3
90+
91+
const dagGetRes6 = {
92+
first: () => Promise.resolve({ remainderPath: '' })
93+
}
9294

93-
const res = await resolveIpldPath(ipfsMock, new CID(cid), path)
95+
ipldMock.get.mockReturnValueOnce(Promise.resolve(dagGetRes1))
96+
ipldMock.resolve.mockReturnValueOnce(dagGetRes2)
97+
ipldMock.get.mockReturnValueOnce(Promise.resolve(dagGetRes3))
98+
ipldMock.resolve.mockReturnValueOnce(dagGetRes4)
99+
ipldMock.get.mockReturnValueOnce(Promise.resolve(dagGetRes5))
100+
ipldMock.resolve.mockReturnValueOnce(dagGetRes6)
94101

95-
expect(ipfsMock.dag.get.mock.calls.length).toBe(3)
102+
const res = await resolveIpldPath(ipldMock, new CID(cid), path)
103+
104+
expect(ipldMock.get.mock.calls.length).toBe(3)
105+
expect(ipldMock.resolve.mock.calls.length).toBe(3)
96106
expect(res.targetNode.cid).toEqual(dagNode3CID)
97107
expect(res.canonicalPath).toBe(dagNode3CID)
98108
expect(res.nodes.length).toBe(3)
@@ -121,13 +131,10 @@ it('resolves thru dag-cbor to dag-pb', async () => {
121131
})
122132

123133
function createDagPbNode (data, links) {
124-
const node = new DAGNode(data)
125-
126-
for (const link of links) {
127-
node.addLink(link)
134+
if (typeof data === 'string') {
135+
data = new Uint8Array(Buffer.from(data))
128136
}
129-
130-
return node
137+
return new DAGNode(data, links)
131138
}
132139

133140
it('finds the linkPath from a fullPath and a remainderPath', () => {

0 commit comments

Comments
 (0)