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

Commit 62d8ecb

Browse files
authored
feat: dht client (#3947)
* Enables `libp2p-kad-dht` in client mode * Updates types with new DHT events BREAKING CHANGE: The DHT API has been refactored to return async iterators of query events
1 parent c272bfb commit 62d8ecb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+1300
-668
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"description": "JavaScript implementation of the IPFS specification",
55
"scripts": {
66
"link": "lerna link",
7-
"reset": "lerna run clean && rimraf packages/*/node_modules node_modules",
7+
"reset": "lerna run clean && rimraf packages/*/node_modules node_modules package-lock.json packages/*/package-lock.json",
88
"test": "lerna run test",
99
"test:node": "lerna run test:node",
1010
"test:browser": "lerna run test:browser",

packages/interface-ipfs-core/package.json

+5-5
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,9 @@
7676
"ipfs-unixfs": "^6.0.3",
7777
"ipfs-unixfs-importer": "^9.0.3",
7878
"ipfs-utils": "^9.0.2",
79-
"ipns": "^0.15.0",
79+
"ipns": "^0.16.0",
8080
"is-ipfs": "^6.0.1",
81-
"iso-random-stream": "^2.0.0",
81+
"iso-random-stream": "^2.0.2",
8282
"it-all": "^1.0.4",
8383
"it-buffer-stream": "^2.0.0",
8484
"it-concat": "^2.0.0",
@@ -90,7 +90,7 @@
9090
"it-pushable": "^1.4.2",
9191
"it-tar": "^4.0.0",
9292
"it-to-buffer": "^2.0.0",
93-
"libp2p-crypto": "^0.19.7",
93+
"libp2p-crypto": "^0.21.0",
9494
"libp2p-websockets": "^0.16.2",
9595
"multiaddr": "^10.0.0",
9696
"multiformats": "^9.4.13",
@@ -99,9 +99,9 @@
9999
"p-map": "^4.0.0",
100100
"p-retry": "^4.5.0",
101101
"pako": "^1.0.2",
102-
"peer-id": "^0.15.1",
102+
"peer-id": "^0.16.0",
103103
"readable-stream": "^3.4.0",
104-
"sinon": "^11.1.1",
104+
"sinon": "^12.0.01",
105105
"uint8arrays": "^3.0.0"
106106
},
107107
"devDependencies": {

packages/interface-ipfs-core/src/dht/disabled.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { expect } from 'aegir/utils/chai.js'
44
import { getDescribe, getIt } from '../utils/mocha.js'
5-
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
5+
import all from 'it-all'
66

77
/**
88
* @typedef {import('ipfsd-ctl').Factory} Factory
@@ -42,8 +42,9 @@ export function testDisabled (factory, options) {
4242
after(() => factory.clean())
4343

4444
it('should error when DHT not available', async () => {
45-
await expect(nodeA.dht.get(uint8ArrayFromString('/ipns/Qme6KJdKcp85TYbLxuLV7oQzMiLremD7HMoXLZEmgo6Rnh')))
46-
.to.eventually.be.rejected()
45+
const events = await all(nodeA.dht.get('/ipns/12D3KooWQMSMXmsBvs5YDEQ6tXsaFv9tjuzmDmEvusaiQSFdrJdN'))
46+
47+
expect(events.filter(event => event.name === 'QUERY_ERROR')).to.not.be.empty()
4748
})
4849
})
4950
}

packages/interface-ipfs-core/src/dht/find-peer.js

+24-12
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
import { expect } from 'aegir/utils/chai.js'
44
import { getDescribe, getIt } from '../utils/mocha.js'
55
import testTimeout from '../utils/test-timeout.js'
6+
import drain from 'it-drain'
7+
import all from 'it-all'
8+
import { ensureReachable } from './utils.js'
69

710
/**
811
* @typedef {import('ipfsd-ctl').Factory} Factory
@@ -23,41 +26,50 @@ export function testFindPeer (factory, options) {
2326
let nodeA
2427
/** @type {import('ipfs-core-types').IPFS} */
2528
let nodeB
26-
/** @type {import('ipfs-core-types/src/root').IDResult} */
27-
let nodeBId
2829

2930
before(async () => {
3031
nodeA = (await factory.spawn()).api
3132
nodeB = (await factory.spawn()).api
32-
nodeBId = await nodeB.id()
3333

34-
await nodeA.swarm.connect(nodeBId.addresses[0])
34+
await ensureReachable(nodeA, nodeB)
3535
})
3636

3737
after(() => factory.clean())
3838

3939
it('should respect timeout option when finding a peer on the DHT', async () => {
4040
const nodeBId = await nodeB.id()
4141

42-
await testTimeout(() => nodeA.dht.findPeer(nodeBId.id, {
42+
await testTimeout(() => drain(nodeA.dht.findPeer(nodeBId.id, {
4343
timeout: 1
44-
}))
44+
})))
4545
})
4646

4747
it('should find other peers', async () => {
4848
const nodeBId = await nodeB.id()
49-
const res = await nodeA.dht.findPeer(nodeBId.id)
50-
const id = res.id.toString()
5149

50+
const results = await all(nodeA.dht.findPeer(nodeBId.id))
51+
const finalPeer = results.filter(event => event.name === 'FINAL_PEER').pop()
52+
53+
if (!finalPeer || finalPeer.name !== 'FINAL_PEER') {
54+
throw new Error('No finalPeer event received')
55+
}
56+
57+
const id = finalPeer.peer.id
5258
const nodeAddresses = nodeBId.addresses.map((addr) => addr.nodeAddress())
53-
const peerAddresses = res.addrs.map(ma => ma.nodeAddress())
59+
const peerAddresses = finalPeer.peer.multiaddrs.map(ma => ma.nodeAddress())
5460

55-
expect(id).to.be.eql(nodeBId.id)
61+
expect(id).to.equal(nodeBId.id)
5662
expect(peerAddresses).to.deep.include(nodeAddresses[0])
5763
})
5864

59-
it('should fail to find other peer if peer does not exist', () => {
60-
return expect(nodeA.dht.findPeer('Qmd7qZS4T7xXtsNFdRoK1trfMs5zU94EpokQ9WFtxdPxsZ')).to.eventually.be.rejected()
65+
it('should fail to find other peer if peer does not exist', async () => {
66+
const events = await all(nodeA.dht.findPeer('Qmd7qZS4T7xXtsNFdRoK1trfMs5zU94EpokQ9WFtxdPxsZ'))
67+
68+
// no finalPeer events found
69+
expect(events.filter(event => event.name === 'FINAL_PEER')).to.be.empty()
70+
71+
// queryError events found
72+
expect(events.filter(event => event.name === 'QUERY_ERROR')).to.not.be.empty()
6173
})
6274
})
6375
}

packages/interface-ipfs-core/src/dht/find-provs.js

+14-46
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { expect } from 'aegir/utils/chai.js'
44
import { getDescribe, getIt } from '../utils/mocha.js'
55
import all from 'it-all'
66
import drain from 'it-drain'
7-
import { fakeCid } from './utils.js'
87
import testTimeout from '../utils/test-timeout.js'
8+
import { ensureReachable } from './utils.js'
99

1010
/**
1111
* @typedef {import('ipfsd-ctl').Factory} Factory
@@ -20,34 +20,22 @@ export function testFindProvs (factory, options) {
2020
const it = getIt(options)
2121

2222
describe('.dht.findProvs', function () {
23-
this.timeout(20000)
23+
this.timeout(80 * 1000)
2424

2525
/** @type {import('ipfs-core-types').IPFS} */
2626
let nodeA
2727
/** @type {import('ipfs-core-types').IPFS} */
2828
let nodeB
2929
/** @type {import('ipfs-core-types').IPFS} */
3030
let nodeC
31-
/** @type {import('ipfs-core-types/src/root').IDResult} */
32-
let nodeAId
33-
/** @type {import('ipfs-core-types/src/root').IDResult} */
34-
let nodeBId
35-
/** @type {import('ipfs-core-types/src/root').IDResult} */
36-
let nodeCId
3731

3832
before(async () => {
3933
nodeA = (await factory.spawn()).api
4034
nodeB = (await factory.spawn()).api
4135
nodeC = (await factory.spawn()).api
4236

43-
nodeAId = await nodeA.id()
44-
nodeBId = await nodeB.id()
45-
nodeCId = await nodeC.id()
46-
47-
await Promise.all([
48-
nodeB.swarm.connect(nodeAId.addresses[0]),
49-
nodeC.swarm.connect(nodeBId.addresses[0])
50-
])
37+
await ensureReachable(nodeB, nodeA)
38+
await ensureReachable(nodeC, nodeB)
5139
})
5240

5341
after(() => factory.clean())
@@ -57,8 +45,6 @@ export function testFindProvs (factory, options) {
5745
*/
5846
let providedCid
5947
before('add providers for the same cid', async function () {
60-
this.timeout(10 * 1000)
61-
6248
const cids = await Promise.all([
6349
nodeB.object.new('unixfs-dir'),
6450
nodeC.object.new('unixfs-dir')
@@ -79,38 +65,20 @@ export function testFindProvs (factory, options) {
7965
})
8066

8167
it('should be able to find providers', async function () {
82-
// @ts-ignore this is mocha
83-
this.timeout(20 * 1000)
84-
85-
const provs = await all(nodeA.dht.findProvs(providedCid, { numProviders: 2 }))
86-
const providerIds = provs.map((p) => p.id.toString())
68+
/** @type {string[]} */
69+
const providerIds = []
8770

88-
expect(providerIds).to.have.members([
89-
nodeBId.id,
90-
nodeCId.id
91-
])
92-
})
93-
94-
it('should take options to override timeout config', async function () {
95-
const options = {
96-
timeout: 1
71+
for await (const event of nodeA.dht.findProvs(providedCid)) {
72+
if (event.name === 'PROVIDER') {
73+
providerIds.push(...event.providers.map(prov => prov.id))
74+
}
9775
}
9876

99-
const cidV0 = await fakeCid()
100-
const start = Date.now()
101-
let res
102-
103-
try {
104-
res = await all(nodeA.dht.findProvs(cidV0, options))
105-
} catch (/** @type {any} */ err) {
106-
// rejected by http client
107-
expect(err).to.have.property('name', 'TimeoutError')
108-
return
109-
}
77+
const nodeBId = await nodeB.id()
78+
const nodeCId = await nodeC.id()
11079

111-
// rejected by the server, errors don't work over http - https://github.com/ipfs/js-ipfs/issues/2519
112-
expect(res).to.be.an('array').with.lengthOf(0)
113-
expect(Date.now() - start).to.be.lessThan(100)
80+
expect(providerIds).to.include(nodeBId.id)
81+
expect(providerIds).to.include(nodeCId.id)
11482
})
11583
})
11684
}

packages/interface-ipfs-core/src/dht/get.js

+24-13
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
import { expect } from 'aegir/utils/chai.js'
44
import { getDescribe, getIt } from '../utils/mocha.js'
55
import testTimeout from '../utils/test-timeout.js'
6-
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
76
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
7+
import drain from 'it-drain'
8+
import all from 'it-all'
9+
import { ensureReachable } from './utils.js'
810

911
/**
1012
* @typedef {import('ipfsd-ctl').Factory} Factory
@@ -19,19 +21,18 @@ export function testGet (factory, options) {
1921
const it = getIt(options)
2022

2123
describe('.dht.get', function () {
24+
this.timeout(80 * 1000)
25+
2226
/** @type {import('ipfs-core-types').IPFS} */
2327
let nodeA
2428
/** @type {import('ipfs-core-types').IPFS} */
2529
let nodeB
26-
/** @type {import('ipfs-core-types/src/root').IDResult} */
27-
let nodeBId
2830

2931
before(async () => {
3032
nodeA = (await factory.spawn()).api
3133
nodeB = (await factory.spawn()).api
32-
nodeBId = await nodeB.id()
3334

34-
await nodeA.swarm.connect(nodeBId.addresses[0])
35+
await ensureReachable(nodeA, nodeB)
3536
})
3637

3738
after(() => factory.clean())
@@ -40,23 +41,33 @@ export function testGet (factory, options) {
4041
const data = await nodeA.add('should put a value to the DHT')
4142
const publish = await nodeA.name.publish(data.cid)
4243

43-
await testTimeout(() => nodeB.dht.get(uint8ArrayFromString(`/ipns/${publish.name}`), {
44+
await testTimeout(() => drain(nodeB.dht.get(`/ipns/${publish.name}`, {
4445
timeout: 1
45-
}))
46+
})))
4647
})
4748

48-
it('should error when getting a non-existent key from the DHT', () => {
49-
return expect(nodeA.dht.get(uint8ArrayFromString('non-existing'), { timeout: 100 }))
50-
.to.eventually.be.rejected
51-
.and.be.an.instanceOf(Error)
49+
it('should error when getting a non-existent key from the DHT', async () => {
50+
const key = '/ipns/k51qzi5uqu5dl0dbfddy2wb42nvbc6anyxnkrguy5l0h0bv9kaih6j6vqdskqk'
51+
const events = await all(nodeA.dht.get(key))
52+
53+
// no value events found
54+
expect(events.filter(event => event.name === 'VALUE')).to.be.empty()
55+
56+
// queryError events found
57+
expect(events.filter(event => event.name === 'QUERY_ERROR')).to.not.be.empty()
5258
})
5359

5460
it('should get a value after it was put on another node', async () => {
5561
const data = await nodeA.add('should put a value to the DHT')
5662
const publish = await nodeA.name.publish(data.cid)
57-
const record = await nodeA.dht.get(uint8ArrayFromString(`/ipns/${publish.name}`))
63+
const events = await all(nodeA.dht.get(`/ipns/${publish.name}`))
64+
const valueEvent = events.filter(event => event.name === 'VALUE').pop()
65+
66+
if (!valueEvent || valueEvent.name !== 'VALUE') {
67+
throw new Error('Value event not found')
68+
}
5869

59-
expect(uint8ArrayToString(record)).to.contain(data.cid.toString())
70+
expect(uint8ArrayToString(valueEvent.value)).to.contain(data.cid.toString())
6071
})
6172
})
6273
}

packages/interface-ipfs-core/src/dht/provide.js

+4-17
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { CID } from 'multiformats/cid'
55
import all from 'it-all'
66
import { expect } from 'aegir/utils/chai.js'
77
import { getDescribe, getIt } from '../utils/mocha.js'
8+
import { ensureReachable } from './utils.js'
89

910
/**
1011
* @typedef {import('ipfsd-ctl').Factory} Factory
@@ -27,8 +28,8 @@ export function testProvide (factory, options) {
2728
before(async () => {
2829
ipfs = (await factory.spawn()).api
2930
const nodeB = (await factory.spawn()).api
30-
const nodeBId = await nodeB.id()
31-
await ipfs.swarm.connect(nodeBId.addresses[0])
31+
32+
await ensureReachable(ipfs, nodeB)
3233
})
3334

3435
after(() => factory.clean())
@@ -48,28 +49,14 @@ export function testProvide (factory, options) {
4849
.that.include('not found locally')
4950
})
5051

51-
it('should allow multiple CIDs to be passed', async () => {
52-
const res = await all(ipfs.addAll([
53-
{ content: uint8ArrayFromString('t0') },
54-
{ content: uint8ArrayFromString('t1') }
55-
]))
56-
57-
await all(ipfs.dht.provide(res.map(f => f.cid)))
58-
})
59-
6052
it('should provide a CIDv1', async () => {
6153
const res = await ipfs.add(uint8ArrayFromString('test'), { cidVersion: 1 })
6254
await all(ipfs.dht.provide(res.cid))
6355
})
6456

65-
it('should error on non CID arg', () => {
57+
it('should error on non CID arg', async () => {
6658
// @ts-expect-error invalid arg
6759
return expect(all(ipfs.dht.provide({}))).to.eventually.be.rejected()
6860
})
69-
70-
it('should error on array containing non CID arg', () => {
71-
// @ts-expect-error invalid arg
72-
return expect(all(ipfs.dht.provide([{}]))).to.eventually.be.rejected()
73-
})
7461
})
7562
}

0 commit comments

Comments
 (0)