mirror of
https://github.com/fluencelabs/js-libp2p
synced 2025-04-25 10:32:14 +00:00
feat: support delegated value store in content-routing module
This commit is contained in:
parent
a335fda852
commit
f9e5db5b2a
@ -190,6 +190,23 @@ If you want to know more about libp2p pubsub, you should read the following cont
|
|||||||
- https://docs.libp2p.io/concepts/publish-subscribe
|
- https://docs.libp2p.io/concepts/publish-subscribe
|
||||||
- https://github.com/libp2p/specs/tree/master/pubsub
|
- https://github.com/libp2p/specs/tree/master/pubsub
|
||||||
|
|
||||||
|
### Value Storage
|
||||||
|
|
||||||
|
Some libp2p components are able to provide Key/Value storage capabilities that can be used by other libp2p components. A Value Storage module implements the [ValueStore interface](https://github.com/libp2p/js-libp2p-interfaces/blob/master/packages/interfaces/src/value-store/types.d.ts), which provides `put`
|
||||||
|
and `get` methods for storing arbitrary binary data.
|
||||||
|
|
||||||
|
Some available peer routing modules are:
|
||||||
|
|
||||||
|
- [js-libp2p-kad-dht](https://github.com/libp2p/js-libp2p-kad-dht)
|
||||||
|
- [js-libp2p-delegated-content-routing](https://github.com/libp2p/js-libp2p-delegated-content-routing)
|
||||||
|
- via `DelgatedValueStore`
|
||||||
|
|
||||||
|
The current [DHT](#dht) implementation implements the `ValueStore` interface, and if the DHT is enabled
|
||||||
|
there is no need to separately enable the value storage capability.
|
||||||
|
|
||||||
|
Other implementations of value storage may be enabled by including a `ValueStore` implementation in
|
||||||
|
the `modules.valueStorage` configuration as shown below.
|
||||||
|
|
||||||
## Customizing libp2p
|
## Customizing libp2p
|
||||||
|
|
||||||
When [creating a libp2p node](./API.md#create), the modules needed should be specified as follows:
|
When [creating a libp2p node](./API.md#create), the modules needed should be specified as follows:
|
||||||
@ -202,6 +219,7 @@ const modules = {
|
|||||||
contentRouting: [],
|
contentRouting: [],
|
||||||
peerRouting: [],
|
peerRouting: [],
|
||||||
peerDiscovery: [],
|
peerDiscovery: [],
|
||||||
|
valueStorage: [],
|
||||||
dht: dhtImplementation,
|
dht: dhtImplementation,
|
||||||
pubsub: pubsubImplementation
|
pubsub: pubsubImplementation
|
||||||
}
|
}
|
||||||
@ -394,6 +412,7 @@ const { NOISE } = require('libp2p-noise')
|
|||||||
const ipfsHttpClient = require('ipfs-http-client')
|
const ipfsHttpClient = require('ipfs-http-client')
|
||||||
const DelegatedPeerRouter = require('libp2p-delegated-peer-routing')
|
const DelegatedPeerRouter = require('libp2p-delegated-peer-routing')
|
||||||
const DelegatedContentRouter = require('libp2p-delegated-content-routing')
|
const DelegatedContentRouter = require('libp2p-delegated-content-routing')
|
||||||
|
const DelegatedValueStore = require('libp2p-delegated-content-routing/value-store')
|
||||||
const PeerId = require('peer-id')
|
const PeerId = require('peer-id')
|
||||||
|
|
||||||
// create a peerId
|
// create a peerId
|
||||||
@ -411,6 +430,12 @@ const delegatedContentRouting = new DelegatedContentRouter(peerId, ipfsHttpClien
|
|||||||
port: 443
|
port: 443
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const delegatedValueStore = new DelegatedValueStore(ipfsHttpClient.create({
|
||||||
|
host: 'node0.delegate.ipfs.io', // In production you should setup your own delegates
|
||||||
|
protocol: 'https',
|
||||||
|
port: 443
|
||||||
|
}))
|
||||||
|
|
||||||
const node = await Libp2p.create({
|
const node = await Libp2p.create({
|
||||||
modules: {
|
modules: {
|
||||||
transport: [TCP],
|
transport: [TCP],
|
||||||
@ -418,6 +443,7 @@ const node = await Libp2p.create({
|
|||||||
connEncryption: [NOISE],
|
connEncryption: [NOISE],
|
||||||
contentRouting: [delegatedContentRouting],
|
contentRouting: [delegatedContentRouting],
|
||||||
peerRouting: [delegatedPeerRouting],
|
peerRouting: [delegatedPeerRouting],
|
||||||
|
valueStorage: [delegatedValueStore],
|
||||||
},
|
},
|
||||||
peerId,
|
peerId,
|
||||||
peerRouting: { // Peer routing configuration
|
peerRouting: { // Peer routing configuration
|
||||||
|
@ -78,10 +78,10 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"abortable-iterator": "^3.0.0",
|
|
||||||
"@motrix/nat-api": "^0.3.1",
|
"@motrix/nat-api": "^0.3.1",
|
||||||
"@vascosantos/moving-average": "^1.1.0",
|
"@vascosantos/moving-average": "^1.1.0",
|
||||||
"abort-controller": "^3.0.0",
|
"abort-controller": "^3.0.0",
|
||||||
|
"abortable-iterator": "^3.0.0",
|
||||||
"aggregate-error": "^3.1.0",
|
"aggregate-error": "^3.1.0",
|
||||||
"any-signal": "^2.1.1",
|
"any-signal": "^2.1.1",
|
||||||
"bignumber.js": "^9.0.1",
|
"bignumber.js": "^9.0.1",
|
||||||
@ -104,7 +104,6 @@
|
|||||||
"it-pipe": "^1.1.0",
|
"it-pipe": "^1.1.0",
|
||||||
"it-take": "^1.0.0",
|
"it-take": "^1.0.0",
|
||||||
"libp2p-crypto": "^0.19.4",
|
"libp2p-crypto": "^0.19.4",
|
||||||
"libp2p-interfaces": "^1.0.0",
|
|
||||||
"libp2p-utils": "^0.4.0",
|
"libp2p-utils": "^0.4.0",
|
||||||
"mafmt": "^10.0.0",
|
"mafmt": "^10.0.0",
|
||||||
"merge-options": "^3.0.4",
|
"merge-options": "^3.0.4",
|
||||||
@ -149,7 +148,7 @@
|
|||||||
"it-pushable": "^1.4.0",
|
"it-pushable": "^1.4.0",
|
||||||
"libp2p": ".",
|
"libp2p": ".",
|
||||||
"libp2p-bootstrap": "^0.13.0",
|
"libp2p-bootstrap": "^0.13.0",
|
||||||
"libp2p-delegated-content-routing": "^0.11.0",
|
"libp2p-delegated-content-routing": "git+ssh://git@github.com/libp2p/js-libp2p-delegated-content-routing#362cd00988e717f6fc49b0a1f2fa7bbaabbfcc53",
|
||||||
"libp2p-delegated-peer-routing": "^0.10.0",
|
"libp2p-delegated-peer-routing": "^0.10.0",
|
||||||
"libp2p-floodsub": "^0.27.0",
|
"libp2p-floodsub": "^0.27.0",
|
||||||
"libp2p-gossipsub": "^0.11.0",
|
"libp2p-gossipsub": "^0.11.0",
|
||||||
|
@ -6,7 +6,8 @@ const {
|
|||||||
storeAddresses,
|
storeAddresses,
|
||||||
uniquePeers,
|
uniquePeers,
|
||||||
requirePeers,
|
requirePeers,
|
||||||
maybeLimitSource
|
maybeLimitSource,
|
||||||
|
raceToSuccess
|
||||||
} = require('./utils')
|
} = require('./utils')
|
||||||
|
|
||||||
const merge = require('it-merge')
|
const merge = require('it-merge')
|
||||||
@ -17,12 +18,8 @@ const { pipe } = require('it-pipe')
|
|||||||
* @typedef {import('multiaddr').Multiaddr} Multiaddr
|
* @typedef {import('multiaddr').Multiaddr} Multiaddr
|
||||||
* @typedef {import('multiformats/cid').CID} CID
|
* @typedef {import('multiformats/cid').CID} CID
|
||||||
* @typedef {import('libp2p-interfaces/src/content-routing/types').ContentRouting} ContentRoutingModule
|
* @typedef {import('libp2p-interfaces/src/content-routing/types').ContentRouting} ContentRoutingModule
|
||||||
*/
|
* @typedef {import('libp2p-interfaces/src/value-store/types').ValueStore} ValueStoreModule
|
||||||
|
* @typedef {import('libp2p-interfaces/src/value-store/types').GetValueResult} GetData
|
||||||
/**
|
|
||||||
* @typedef {Object} GetData
|
|
||||||
* @property {PeerId} from
|
|
||||||
* @property {Uint8Array} val
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class ContentRouting {
|
class ContentRouting {
|
||||||
@ -34,11 +31,16 @@ class ContentRouting {
|
|||||||
this.libp2p = libp2p
|
this.libp2p = libp2p
|
||||||
/** @type {ContentRoutingModule[]} */
|
/** @type {ContentRoutingModule[]} */
|
||||||
this.routers = libp2p._modules.contentRouting || []
|
this.routers = libp2p._modules.contentRouting || []
|
||||||
|
/** @type {ValueStoreModule[]} */
|
||||||
|
this.valueStores = libp2p._modules.valueStorage || []
|
||||||
this.dht = libp2p._dht
|
this.dht = libp2p._dht
|
||||||
|
|
||||||
// If we have the dht, add it to the available content routers
|
// If we have the dht, add it to the available content routers and value stores
|
||||||
if (this.dht && libp2p._config.dht.enabled) {
|
if (this.dht && libp2p._config.dht.enabled) {
|
||||||
this.routers.push(this.dht)
|
this.routers.push(this.dht)
|
||||||
|
if (!this.valueStores.includes(this.dht)) {
|
||||||
|
this.valueStores.push(this.dht)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,7 +85,7 @@ class ContentRouting {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store the given key/value pair in the DHT.
|
* Store the given key/value pair in the DHT and/or configured ValueStore.
|
||||||
*
|
*
|
||||||
* @param {Uint8Array} key
|
* @param {Uint8Array} key
|
||||||
* @param {Uint8Array} value
|
* @param {Uint8Array} value
|
||||||
@ -91,12 +93,25 @@ class ContentRouting {
|
|||||||
* @param {number} [options.minPeers] - minimum number of peers required to successfully put
|
* @param {number} [options.minPeers] - minimum number of peers required to successfully put
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
put (key, value, options) {
|
async put (key, value, options) {
|
||||||
if (!this.libp2p.isStarted() || !this.dht.isStarted) {
|
if (!this.libp2p.isStarted()) {
|
||||||
|
throw errCode(new Error(messages.NOT_STARTED_YET), codes.ERR_NODE_NOT_STARTED)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.libp2p._config.dht.enabled && !this.dht.isStarted) {
|
||||||
throw errCode(new Error(messages.NOT_STARTED_YET), codes.DHT_NOT_STARTED)
|
throw errCode(new Error(messages.NOT_STARTED_YET), codes.DHT_NOT_STARTED)
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.dht.put(key, value, options)
|
if (this.valueStores.length === 0) {
|
||||||
|
throw errCode(new Error(messages.VALUE_STORE_REQUIRED), codes.ERR_VALUE_STORE_UNAVAILABLE)
|
||||||
|
}
|
||||||
|
|
||||||
|
const promises = []
|
||||||
|
for (const store of this.valueStores) {
|
||||||
|
promises.push(store.put(key, value, options))
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -109,11 +124,24 @@ class ContentRouting {
|
|||||||
* @returns {Promise<GetData>}
|
* @returns {Promise<GetData>}
|
||||||
*/
|
*/
|
||||||
get (key, options) {
|
get (key, options) {
|
||||||
if (!this.libp2p.isStarted() || !this.dht.isStarted) {
|
if (!this.libp2p.isStarted()) {
|
||||||
|
throw errCode(new Error(messages.NOT_STARTED_YET), codes.ERR_NODE_NOT_STARTED)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.libp2p._config.dht.enabled && !this.dht.isStarted) {
|
||||||
throw errCode(new Error(messages.NOT_STARTED_YET), codes.DHT_NOT_STARTED)
|
throw errCode(new Error(messages.NOT_STARTED_YET), codes.DHT_NOT_STARTED)
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.dht.get(key, options)
|
if (this.valueStores.length === 0) {
|
||||||
|
throw errCode(new Error(messages.VALUE_STORE_REQUIRED), codes.ERR_VALUE_STORE_UNAVAILABLE)
|
||||||
|
}
|
||||||
|
|
||||||
|
const promises = []
|
||||||
|
for (const store of this.valueStores) {
|
||||||
|
promises.push(store.get(key, options))
|
||||||
|
}
|
||||||
|
|
||||||
|
return raceToSuccess(promises)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -81,9 +81,39 @@ function maybeLimitSource (source, max) {
|
|||||||
return source
|
return source
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like Promise.race, but only fails if all input promises fail.
|
||||||
|
*
|
||||||
|
* Returns a promise that will resolve with the value of the first promise
|
||||||
|
* to resolve, but will only fail if all promises fail.
|
||||||
|
*
|
||||||
|
* @template {any} T
|
||||||
|
* @param {Promise<T>[]} promises - an array of promises.
|
||||||
|
* @returns {Promise<T>} the resolved value of the first promise that succeeded, or an Error if _all_ promises fail.
|
||||||
|
*/
|
||||||
|
function raceToSuccess (promises) {
|
||||||
|
const combineErrors = (/** @type Error[] */ errors) => {
|
||||||
|
if (errors.length === 1) {
|
||||||
|
return errors[0]
|
||||||
|
}
|
||||||
|
return new Error(`${errors.length} operations failed: ` + errors.map(e => e.message).join(', '))
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(promises.map(p => {
|
||||||
|
return p.then(
|
||||||
|
val => Promise.reject(val),
|
||||||
|
err => Promise.resolve(err)
|
||||||
|
)
|
||||||
|
})).then(
|
||||||
|
errors => Promise.reject(combineErrors(errors)),
|
||||||
|
val => Promise.resolve(val)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
storeAddresses,
|
storeAddresses,
|
||||||
uniquePeers,
|
uniquePeers,
|
||||||
requirePeers,
|
requirePeers,
|
||||||
maybeLimitSource
|
maybeLimitSource,
|
||||||
|
raceToSuccess
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,8 @@
|
|||||||
exports.messages = {
|
exports.messages = {
|
||||||
NOT_STARTED_YET: 'The libp2p node is not started yet',
|
NOT_STARTED_YET: 'The libp2p node is not started yet',
|
||||||
DHT_DISABLED: 'DHT is not available',
|
DHT_DISABLED: 'DHT is not available',
|
||||||
CONN_ENCRYPTION_REQUIRED: 'At least one connection encryption module is required'
|
CONN_ENCRYPTION_REQUIRED: 'At least one connection encryption module is required',
|
||||||
|
VALUE_STORE_REQUIRED: 'At least one value storage module is required for this operation if the DHT is not enabled.'
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.codes = {
|
exports.codes = {
|
||||||
@ -34,5 +35,6 @@ exports.codes = {
|
|||||||
ERR_TRANSPORT_DIAL_FAILED: 'ERR_TRANSPORT_DIAL_FAILED',
|
ERR_TRANSPORT_DIAL_FAILED: 'ERR_TRANSPORT_DIAL_FAILED',
|
||||||
ERR_UNSUPPORTED_PROTOCOL: 'ERR_UNSUPPORTED_PROTOCOL',
|
ERR_UNSUPPORTED_PROTOCOL: 'ERR_UNSUPPORTED_PROTOCOL',
|
||||||
ERR_INVALID_MULTIADDR: 'ERR_INVALID_MULTIADDR',
|
ERR_INVALID_MULTIADDR: 'ERR_INVALID_MULTIADDR',
|
||||||
ERR_SIGNATURE_NOT_VALID: 'ERR_SIGNATURE_NOT_VALID'
|
ERR_SIGNATURE_NOT_VALID: 'ERR_SIGNATURE_NOT_VALID',
|
||||||
|
ERR_VALUE_STORE_UNAVAILABLE: 'ERR_VALUE_STORE_UNAVAILABLE'
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,7 @@ const { updateSelfPeerRecord } = require('./record/utils')
|
|||||||
* @typedef {import('libp2p-interfaces/src/transport/types').TransportFactory<any, any>} TransportFactory
|
* @typedef {import('libp2p-interfaces/src/transport/types').TransportFactory<any, any>} TransportFactory
|
||||||
* @typedef {import('libp2p-interfaces/src/stream-muxer/types').MuxerFactory} MuxerFactory
|
* @typedef {import('libp2p-interfaces/src/stream-muxer/types').MuxerFactory} MuxerFactory
|
||||||
* @typedef {import('libp2p-interfaces/src/content-routing/types').ContentRouting} ContentRoutingModule
|
* @typedef {import('libp2p-interfaces/src/content-routing/types').ContentRouting} ContentRoutingModule
|
||||||
|
* @typedef {import('libp2p-interfaces/src/value-store/types').ValueStore} ValueStoreModule
|
||||||
* @typedef {import('libp2p-interfaces/src/peer-discovery/types').PeerDiscoveryFactory} PeerDiscoveryFactory
|
* @typedef {import('libp2p-interfaces/src/peer-discovery/types').PeerDiscoveryFactory} PeerDiscoveryFactory
|
||||||
* @typedef {import('libp2p-interfaces/src/peer-routing/types').PeerRouting} PeerRoutingModule
|
* @typedef {import('libp2p-interfaces/src/peer-routing/types').PeerRouting} PeerRoutingModule
|
||||||
* @typedef {import('libp2p-interfaces/src/crypto/types').Crypto} Crypto
|
* @typedef {import('libp2p-interfaces/src/crypto/types').Crypto} Crypto
|
||||||
@ -102,6 +103,7 @@ const { updateSelfPeerRecord } = require('./record/utils')
|
|||||||
* @property {PeerDiscoveryFactory[]} [peerDiscovery]
|
* @property {PeerDiscoveryFactory[]} [peerDiscovery]
|
||||||
* @property {PeerRoutingModule[]} [peerRouting]
|
* @property {PeerRoutingModule[]} [peerRouting]
|
||||||
* @property {ContentRoutingModule[]} [contentRouting]
|
* @property {ContentRoutingModule[]} [contentRouting]
|
||||||
|
* @property {ValueStoreModule[]} [valueStorage]
|
||||||
* @property {Object} [dht]
|
* @property {Object} [dht]
|
||||||
* @property {{new(...args: any[]): Pubsub}} [pubsub]
|
* @property {{new(...args: any[]): Pubsub}} [pubsub]
|
||||||
* @property {Protector} [connProtector]
|
* @property {Protector} [connProtector]
|
||||||
|
@ -11,12 +11,14 @@ const mergeOptions = require('merge-options')
|
|||||||
const { CID } = require('multiformats/cid')
|
const { CID } = require('multiformats/cid')
|
||||||
const ipfsHttpClient = require('ipfs-http-client')
|
const ipfsHttpClient = require('ipfs-http-client')
|
||||||
const DelegatedContentRouter = require('libp2p-delegated-content-routing')
|
const DelegatedContentRouter = require('libp2p-delegated-content-routing')
|
||||||
|
const DelegatedValueStore = require('libp2p-delegated-content-routing/src/value-store')
|
||||||
const { Multiaddr } = require('multiaddr')
|
const { Multiaddr } = require('multiaddr')
|
||||||
const drain = require('it-drain')
|
const drain = require('it-drain')
|
||||||
const all = require('it-all')
|
const all = require('it-all')
|
||||||
|
|
||||||
const peerUtils = require('../utils/creators/peer')
|
const peerUtils = require('../utils/creators/peer')
|
||||||
const { baseOptions, routingOptions } = require('./utils')
|
const { baseOptions, routingOptions } = require('./utils')
|
||||||
|
const uint8arrays = require('uint8arrays')
|
||||||
|
|
||||||
describe('content-routing', () => {
|
describe('content-routing', () => {
|
||||||
describe('no routers', () => {
|
describe('no routers', () => {
|
||||||
@ -96,25 +98,58 @@ describe('content-routing', () => {
|
|||||||
|
|
||||||
return deferred.promise
|
return deferred.promise
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should put a key/value pair to the DHT', async () => {
|
||||||
|
const deferred = pDefer()
|
||||||
|
|
||||||
|
sinon.stub(nodes[0]._dht, 'put').callsFake(async () => {
|
||||||
|
deferred.resolve()
|
||||||
|
})
|
||||||
|
|
||||||
|
const key = new TextEncoder().encode('/foo/bar')
|
||||||
|
const val = new TextEncoder().encode('hello-world')
|
||||||
|
await nodes[0].contentRouting.put(key, val)
|
||||||
|
|
||||||
|
return deferred.promise
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should get a value by key from the DHT', async () => {
|
||||||
|
const deferred = pDefer()
|
||||||
|
sinon.stub(nodes[0]._dht, 'get').callsFake(async () => {
|
||||||
|
const val = new TextEncoder().encode('hello-world')
|
||||||
|
deferred.resolve(val)
|
||||||
|
return { from: nodes[0].id, val }
|
||||||
|
})
|
||||||
|
const key = new TextEncoder().encode('/foo/bar')
|
||||||
|
const res = await nodes[0].contentRouting.get(key)
|
||||||
|
expect(res.from).to.equal(nodes[0].id)
|
||||||
|
return deferred.promise
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('via delegate router', () => {
|
describe('via delegate router', () => {
|
||||||
let node
|
let node
|
||||||
let delegate
|
let delegate
|
||||||
|
let valueStore
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const [peerId] = await peerUtils.createPeerId({ fixture: true })
|
const [peerId] = await peerUtils.createPeerId({ fixture: true })
|
||||||
|
const [delegateId] = await peerUtils.createPeerId({ fixture: true })
|
||||||
|
|
||||||
delegate = new DelegatedContentRouter(peerId, ipfsHttpClient.create({
|
const ipfsClient = ipfsHttpClient.create({
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
protocol: 'http',
|
protocol: 'http',
|
||||||
port: 60197
|
port: 60197
|
||||||
}))
|
})
|
||||||
|
|
||||||
|
delegate = new DelegatedContentRouter(peerId, ipfsClient)
|
||||||
|
valueStore = new DelegatedValueStore(delegateId, ipfsClient)
|
||||||
|
|
||||||
;[node] = await peerUtils.createPeer({
|
;[node] = await peerUtils.createPeer({
|
||||||
config: mergeOptions(baseOptions, {
|
config: mergeOptions(baseOptions, {
|
||||||
modules: {
|
modules: {
|
||||||
contentRouting: [delegate]
|
contentRouting: [delegate],
|
||||||
|
valueStorage: [valueStore]
|
||||||
},
|
},
|
||||||
config: {
|
config: {
|
||||||
dht: {
|
dht: {
|
||||||
@ -244,25 +279,67 @@ describe('content-routing', () => {
|
|||||||
|
|
||||||
expect(mockApi.isDone()).to.equal(true)
|
expect(mockApi.isDone()).to.equal(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should put a key/value pair using the delegated node', async () => {
|
||||||
|
const mockApi = nock('http://0.0.0.0:60197')
|
||||||
|
.post('/api/v0/dht/put')
|
||||||
|
.query(true)
|
||||||
|
.reply(200, '', [
|
||||||
|
'Content-Type', 'application/json',
|
||||||
|
'X-Chunked-Output', '1'
|
||||||
|
])
|
||||||
|
|
||||||
|
const key = new TextEncoder().encode('/foo/bar')
|
||||||
|
const val = new TextEncoder().encode('a-value')
|
||||||
|
await node.contentRouting.put(key, val)
|
||||||
|
|
||||||
|
expect(mockApi.isDone()).to.equal(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should get a value by key using the delegated node', async () => {
|
||||||
|
const val = new TextEncoder().encode('hello-world')
|
||||||
|
const valueBase64 = uint8arrays.toString(val, 'base64pad')
|
||||||
|
const mockApi = nock('http://0.0.0.0:60197')
|
||||||
|
.post('/api/v0/dht/get')
|
||||||
|
.query(true)
|
||||||
|
.reply(200, `{"Extra":"${valueBase64}","Type":5}`, [
|
||||||
|
'Content-Type', 'application/json',
|
||||||
|
'X-Chunked-Output', '1'
|
||||||
|
])
|
||||||
|
|
||||||
|
const key = new TextEncoder().encode('/foo/bar')
|
||||||
|
await node.contentRouting.get(key)
|
||||||
|
|
||||||
|
expect(mockApi.isDone()).to.equal(true)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('via dht and delegate routers', () => {
|
describe('via dht and delegate routers', () => {
|
||||||
let node
|
let node
|
||||||
|
let nodeId
|
||||||
let delegate
|
let delegate
|
||||||
|
let delegateId
|
||||||
|
let valueStore
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const [peerId] = await peerUtils.createPeerId({ fixture: true })
|
const [peerId] = await peerUtils.createPeerId({ fixture: true })
|
||||||
|
const [delegatePeerId] = await peerUtils.createPeerId({ fixture: true })
|
||||||
|
nodeId = peerId
|
||||||
|
delegateId = delegatePeerId
|
||||||
|
|
||||||
delegate = new DelegatedContentRouter(peerId, ipfsHttpClient.create({
|
const ipfsClient = ipfsHttpClient.create({
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
protocol: 'http',
|
protocol: 'http',
|
||||||
port: 60197
|
port: 60197
|
||||||
}))
|
})
|
||||||
|
delegate = new DelegatedContentRouter(peerId, ipfsClient)
|
||||||
|
valueStore = new DelegatedValueStore(delegateId, ipfsClient)
|
||||||
|
|
||||||
;[node] = await peerUtils.createPeer({
|
;[node] = await peerUtils.createPeer({
|
||||||
config: mergeOptions(routingOptions, {
|
config: mergeOptions(routingOptions, {
|
||||||
modules: {
|
modules: {
|
||||||
contentRouting: [delegate]
|
contentRouting: [delegate],
|
||||||
|
valueStorage: [valueStore]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -442,5 +519,112 @@ describe('content-routing', () => {
|
|||||||
expect(providers).to.have.length.above(0)
|
expect(providers).to.have.length.above(0)
|
||||||
expect(providers).to.eql(results)
|
expect(providers).to.eql(results)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should put values to the DHT and delegated node', async () => {
|
||||||
|
const deferredDHT = pDefer()
|
||||||
|
sinon.stub(node._dht, 'put').callsFake(async () => {
|
||||||
|
deferredDHT.resolve()
|
||||||
|
})
|
||||||
|
|
||||||
|
const mockApi = nock('http://0.0.0.0:60197')
|
||||||
|
.post('/api/v0/dht/put')
|
||||||
|
.query(true)
|
||||||
|
.reply(200, '', [
|
||||||
|
'Content-Type', 'application/json',
|
||||||
|
'X-Chunked-Output', '1'
|
||||||
|
])
|
||||||
|
|
||||||
|
const key = new TextEncoder().encode('/foo/bar')
|
||||||
|
const val = new TextEncoder().encode('hello-world')
|
||||||
|
await node.contentRouting.put(key, val)
|
||||||
|
|
||||||
|
expect(mockApi.isDone()).to.equal(true)
|
||||||
|
return deferredDHT.promise
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should try to get values by key from both DHT and delegated node', async () => {
|
||||||
|
const deferred = pDefer()
|
||||||
|
sinon.stub(node._dht, 'get').callsFake(async () => {
|
||||||
|
// small delay to allow delegate call to go through before dht promise resolves
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 10))
|
||||||
|
const val = new TextEncoder().encode('hello-world')
|
||||||
|
deferred.resolve(val)
|
||||||
|
const from = nodeId
|
||||||
|
return { from, val }
|
||||||
|
})
|
||||||
|
|
||||||
|
const val = new TextEncoder().encode('hello-world')
|
||||||
|
const valueBase64 = uint8arrays.toString(val, 'base64pad')
|
||||||
|
const mockApi = nock('http://0.0.0.0:60197')
|
||||||
|
.post('/api/v0/dht/get')
|
||||||
|
.query(true)
|
||||||
|
.reply(200, `{"Extra":"${valueBase64}","Type":5}`, [
|
||||||
|
'Content-Type', 'application/json',
|
||||||
|
'X-Chunked-Output', '1'
|
||||||
|
])
|
||||||
|
|
||||||
|
const key = new TextEncoder().encode('/foo/bar')
|
||||||
|
await node.contentRouting.get(key)
|
||||||
|
|
||||||
|
expect(mockApi.isDone()).to.equal(true)
|
||||||
|
return deferred.promise
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return a value for a key from the delegate node if the DHT fails', async () => {
|
||||||
|
const deferred = pDefer()
|
||||||
|
sinon.stub(node._dht, 'get').callsFake(async () => {
|
||||||
|
deferred.resolve()
|
||||||
|
throw new Error('bang!')
|
||||||
|
})
|
||||||
|
|
||||||
|
const val = new TextEncoder().encode('hello-world')
|
||||||
|
const valueBase64 = uint8arrays.toString(val, 'base64pad')
|
||||||
|
const mockApi = nock('http://0.0.0.0:60197')
|
||||||
|
.post('/api/v0/dht/get')
|
||||||
|
.query(true)
|
||||||
|
.reply(200, `{"Extra":"${valueBase64}","Type":5}`, [
|
||||||
|
'Content-Type', 'application/json',
|
||||||
|
'X-Chunked-Output', '1'
|
||||||
|
])
|
||||||
|
|
||||||
|
const key = new TextEncoder().encode('/foo/bar')
|
||||||
|
const res = await node.contentRouting.get(key)
|
||||||
|
const returnedValue = new TextDecoder().decode(res.val)
|
||||||
|
|
||||||
|
expect(mockApi.isDone()).to.equal(true)
|
||||||
|
expect(res.from).to.equal(delegateId)
|
||||||
|
expect(returnedValue).to.equal('hello-world')
|
||||||
|
return deferred.promise
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return a value for key from the DHT if the delegate node fails', async () => {
|
||||||
|
const deferred = pDefer()
|
||||||
|
sinon.stub(node._dht, 'get').callsFake(async () => {
|
||||||
|
// small delay to allow delegate call to go through before dht promise resolves
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 10))
|
||||||
|
const val = new TextEncoder().encode('hello-world')
|
||||||
|
deferred.resolve(val)
|
||||||
|
const from = nodeId
|
||||||
|
return { from, val }
|
||||||
|
})
|
||||||
|
|
||||||
|
const mockApi = nock('http://0.0.0.0:60197')
|
||||||
|
.post('/api/v0/dht/get')
|
||||||
|
.query(true)
|
||||||
|
.reply(503, 'No soup for you!', [
|
||||||
|
'Content-Type', 'application/json',
|
||||||
|
'X-Chunked-Output', '1'
|
||||||
|
])
|
||||||
|
|
||||||
|
const key = new TextEncoder().encode('/foo/bar')
|
||||||
|
const res = await node.contentRouting.get(key)
|
||||||
|
const valueString = new TextDecoder().decode(res.val)
|
||||||
|
|
||||||
|
expect(mockApi.isDone()).to.equal(true)
|
||||||
|
expect(res.from).to.deep.equal(nodeId)
|
||||||
|
expect(valueString).to.equal('hello-world')
|
||||||
|
|
||||||
|
return deferred.promise
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user