Skip to content

Commit d8ceb0b

Browse files
stbrodyachingbrain
andauthoredJan 24, 2022
feat: add fetch protocol (#1036)
Adds three methods to implement the `/libp2p/fetch/0.0.1` protocol: * `libp2p.fetch(peerId, key) => Promise<Uint8Array>` * `libp2p.fetchService.registerLookupFunction(prefix, lookupFunction)` * `libp2p.fetchService.unRegisterLookupFunction(prefix, [lookupFunction])` Co-authored-by: achingbrain <alex@achingbrain.net>
1 parent 00e4959 commit d8ceb0b

File tree

11 files changed

+932
-2
lines changed

11 files changed

+932
-2
lines changed
 

‎doc/API.md

+69
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
* [`handle`](#handle)
1313
* [`unhandle`](#unhandle)
1414
* [`ping`](#ping)
15+
* [`fetch`](#fetch)
16+
* [`fetchService.registerLookupFunction`](#fetchserviceregisterlookupfunction)
17+
* [`fetchService.unRegisterLookupFunction`](#fetchserviceunregisterlookupfunction)
1518
* [`multiaddrs`](#multiaddrs)
1619
* [`addressManager.getListenAddrs`](#addressmanagergetlistenaddrs)
1720
* [`addressManager.getAnnounceAddrs`](#addressmanagergetannounceaddrs)
@@ -455,6 +458,72 @@ Pings a given peer and get the operation's latency.
455458
const latency = await libp2p.ping(otherPeerId)
456459
```
457460

461+
## fetch
462+
463+
Fetch a value from a remote node
464+
465+
`libp2p.fetch(peer, key)`
466+
467+
#### Parameters
468+
469+
| Name | Type | Description |
470+
|------|------|-------------|
471+
| peer | [`PeerId`][peer-id]\|[`Multiaddr`][multiaddr]\|`string` | peer to ping |
472+
| key | `string` | A key that corresponds to a value on the remote node |
473+
474+
#### Returns
475+
476+
| Type | Description |
477+
|------|-------------|
478+
| `Promise<Uint8Array | null>` | The value for the key or null if it cannot be found |
479+
480+
#### Example
481+
482+
```js
483+
// ...
484+
const value = await libp2p.fetch(otherPeerId, '/some/key')
485+
```
486+
487+
## fetchService.registerLookupFunction
488+
489+
Register a function to look up values requested by remote nodes
490+
491+
`libp2p.fetchService.registerLookupFunction(prefix, lookup)`
492+
493+
#### Parameters
494+
495+
| Name | Type | Description |
496+
|------|------|-------------|
497+
| prefix | `string` | All queries below this prefix will be passed to the lookup function |
498+
| lookup | `(key: string) => Promise<Uint8Array | null>` | A function that takes a key and returns a Uint8Array or null |
499+
500+
#### Example
501+
502+
```js
503+
// ...
504+
const value = await libp2p.fetchService.registerLookupFunction('/prefix', (key) => { ... })
505+
```
506+
507+
## fetchService.unregisterLookupFunction
508+
509+
Removes the passed lookup function or any function registered for the passed prefix
510+
511+
`libp2p.fetchService.unregisterLookupFunction(prefix, lookup)`
512+
513+
#### Parameters
514+
515+
| Name | Type | Description |
516+
|------|------|-------------|
517+
| prefix | `string` | All queries below this prefix will be passed to the lookup function |
518+
| lookup | `(key: string) => Promise<Uint8Array | null>` | Optional: A function that takes a key and returns a Uint8Array or null |
519+
520+
#### Example
521+
522+
```js
523+
// ...
524+
libp2p.fetchService.unregisterLookupFunction('/prefix')
525+
```
526+
458527
## multiaddrs
459528

460529
Gets the multiaddrs the libp2p node announces to the network. This computes the advertising multiaddrs

‎package.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,17 @@
2020
"scripts": {
2121
"lint": "aegir lint",
2222
"build": "aegir build",
23-
"build:proto": "npm run build:proto:circuit && npm run build:proto:identify && npm run build:proto:plaintext && npm run build:proto:address-book && npm run build:proto:proto-book && npm run build:proto:peer && npm run build:proto:peer-record && npm run build:proto:envelope",
23+
"build:proto": "npm run build:proto:circuit && npm run build:proto:fetch && npm run build:proto:identify && npm run build:proto:plaintext && npm run build:proto:address-book && npm run build:proto:proto-book && npm run build:proto:peer && npm run build:proto:peer-record && npm run build:proto:envelope",
2424
"build:proto:circuit": "pbjs -t static-module -w commonjs -r libp2p-circuit --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/circuit/protocol/index.js ./src/circuit/protocol/index.proto",
25+
"build:proto:fetch": "pbjs -t static-module -w commonjs -r libp2p-fetch --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/fetch/proto.js ./src/fetch/proto.proto",
2526
"build:proto:identify": "pbjs -t static-module -w commonjs -r libp2p-identify --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/identify/message.js ./src/identify/message.proto",
2627
"build:proto:plaintext": "pbjs -t static-module -w commonjs -r libp2p-plaintext --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/insecure/proto.js ./src/insecure/proto.proto",
2728
"build:proto:peer": "pbjs -t static-module -w commonjs -r libp2p-peer --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/peer-store/pb/peer.js ./src/peer-store/pb/peer.proto",
2829
"build:proto:peer-record": "pbjs -t static-module -w commonjs -r libp2p-peer-record --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/record/peer-record/peer-record.js ./src/record/peer-record/peer-record.proto",
2930
"build:proto:envelope": "pbjs -t static-module -w commonjs -r libp2p-envelope --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/record/envelope/envelope.js ./src/record/envelope/envelope.proto",
30-
"build:proto-types": "npm run build:proto-types:circuit && npm run build:proto-types:identify && npm run build:proto-types:plaintext && npm run build:proto-types:address-book && npm run build:proto-types:proto-book && npm run build:proto-types:peer && npm run build:proto-types:peer-record && npm run build:proto-types:envelope",
31+
"build:proto-types": "npm run build:proto-types:circuit && npm run build:proto-types:fetch && npm run build:proto-types:identify && npm run build:proto-types:plaintext && npm run build:proto-types:address-book && npm run build:proto-types:proto-book && npm run build:proto-types:peer && npm run build:proto-types:peer-record && npm run build:proto-types:envelope",
3132
"build:proto-types:circuit": "pbts -o src/circuit/protocol/index.d.ts src/circuit/protocol/index.js",
33+
"build:proto-types:fetch": "pbts -o src/fetch/proto.d.ts src/fetch/proto.js",
3234
"build:proto-types:identify": "pbts -o src/identify/message.d.ts src/identify/message.js",
3335
"build:proto-types:plaintext": "pbts -o src/insecure/proto.d.ts src/insecure/proto.js",
3436
"build:proto-types:peer": "pbts -o src/peer-store/pb/peer.d.ts src/peer-store/pb/peer.js",

‎src/fetch/README.md

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
libp2p-fetch JavaScript Implementation
2+
=====================================
3+
4+
> Libp2p fetch protocol JavaScript implementation
5+
6+
## Overview
7+
8+
An implementation of the Fetch protocol as described here: https://github.com/libp2p/specs/tree/master/fetch
9+
10+
The fetch protocol is a simple protocol for requesting a value corresponding to a key from a peer.
11+
12+
## Usage
13+
14+
```javascript
15+
const Libp2p = require('libp2p')
16+
17+
/**
18+
* Given a key (as a string) returns a value (as a Uint8Array), or null if the key isn't found.
19+
* All keys must be prefixed my the same prefix, which will be used to find the appropriate key
20+
* lookup function.
21+
* @param key - a string
22+
* @returns value - a Uint8Array value that corresponds to the given key, or null if the key doesn't
23+
* have a corresponding value.
24+
*/
25+
async function my_subsystem_key_lookup(key) {
26+
// app specific callback to lookup key-value pairs.
27+
}
28+
29+
// Enable this peer to respond to fetch requests for keys that begin with '/my_subsystem_key_prefix/'
30+
const libp2p = Libp2p.create(...)
31+
libp2p.fetchService.registerLookupFunction('/my_subsystem_key_prefix/', my_subsystem_key_lookup)
32+
33+
const key = '/my_subsystem_key_prefix/{...}'
34+
const peerDst = PeerId.parse('Qmfoo...') // or Multiaddr instance
35+
const value = await libp2p.fetch(peerDst, key)
36+
```

‎src/fetch/constants.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
'use strict'
2+
3+
module.exports = {
4+
// https://github.com/libp2p/specs/tree/master/fetch#wire-protocol
5+
PROTOCOL: '/libp2p/fetch/0.0.1'
6+
}

‎src/fetch/index.js

+159
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
'use strict'
2+
3+
const debug = require('debug')
4+
const log = Object.assign(debug('libp2p:fetch'), {
5+
error: debug('libp2p:fetch:err')
6+
})
7+
const errCode = require('err-code')
8+
const { codes } = require('../errors')
9+
const lp = require('it-length-prefixed')
10+
const { FetchRequest, FetchResponse } = require('./proto')
11+
// @ts-ignore it-handshake does not export types
12+
const handshake = require('it-handshake')
13+
const { PROTOCOL } = require('./constants')
14+
15+
/**
16+
* @typedef {import('../')} Libp2p
17+
* @typedef {import('multiaddr').Multiaddr} Multiaddr
18+
* @typedef {import('peer-id')} PeerId
19+
* @typedef {import('libp2p-interfaces/src/stream-muxer/types').MuxedStream} MuxedStream
20+
* @typedef {(key: string) => Promise<Uint8Array | null>} LookupFunction
21+
*/
22+
23+
/**
24+
* A simple libp2p protocol for requesting a value corresponding to a key from a peer.
25+
* Developers can register one or more lookup function for retrieving the value corresponding to
26+
* a given key. Each lookup function must act on a distinct part of the overall key space, defined
27+
* by a fixed prefix that all keys that should be routed to that lookup function will start with.
28+
*/
29+
class FetchProtocol {
30+
/**
31+
* @param {Libp2p} libp2p
32+
*/
33+
constructor (libp2p) {
34+
this._lookupFunctions = new Map() // Maps key prefix to value lookup function
35+
this._libp2p = libp2p
36+
this.handleMessage = this.handleMessage.bind(this)
37+
}
38+
39+
/**
40+
* Sends a request to fetch the value associated with the given key from the given peer.
41+
*
42+
* @param {PeerId|Multiaddr} peer
43+
* @param {string} key
44+
* @returns {Promise<Uint8Array | null>}
45+
*/
46+
async fetch (peer, key) {
47+
// @ts-ignore multiaddr might not have toB58String
48+
log('dialing %s to %s', this._protocol, peer.toB58String ? peer.toB58String() : peer)
49+
50+
const connection = await this._libp2p.dial(peer)
51+
const { stream } = await connection.newStream(FetchProtocol.PROTOCOL)
52+
const shake = handshake(stream)
53+
54+
// send message
55+
const request = new FetchRequest({ identifier: key })
56+
shake.write(lp.encode.single(FetchRequest.encode(request).finish()))
57+
58+
// read response
59+
const response = FetchResponse.decode((await lp.decode.fromReader(shake.reader).next()).value.slice())
60+
switch (response.status) {
61+
case (FetchResponse.StatusCode.OK): {
62+
return response.data
63+
}
64+
case (FetchResponse.StatusCode.NOT_FOUND): {
65+
return null
66+
}
67+
case (FetchResponse.StatusCode.ERROR): {
68+
const errmsg = (new TextDecoder()).decode(response.data)
69+
throw errCode(new Error('Error in fetch protocol response: ' + errmsg), codes.ERR_INVALID_PARAMETERS)
70+
}
71+
default: {
72+
throw errCode(new Error('Unknown response status'), codes.ERR_INVALID_MESSAGE)
73+
}
74+
}
75+
}
76+
77+
/**
78+
* Invoked when a fetch request is received. Reads the request message off the given stream and
79+
* responds based on looking up the key in the request via the lookup callback that corresponds
80+
* to the key's prefix.
81+
*
82+
* @param {object} options
83+
* @param {MuxedStream} options.stream
84+
* @param {string} options.protocol
85+
*/
86+
async handleMessage (options) {
87+
const { stream } = options
88+
const shake = handshake(stream)
89+
const request = FetchRequest.decode((await lp.decode.fromReader(shake.reader).next()).value.slice())
90+
91+
let response
92+
const lookup = this._getLookupFunction(request.identifier)
93+
if (lookup) {
94+
const data = await lookup(request.identifier)
95+
if (data) {
96+
response = new FetchResponse({ status: FetchResponse.StatusCode.OK, data })
97+
} else {
98+
response = new FetchResponse({ status: FetchResponse.StatusCode.NOT_FOUND })
99+
}
100+
} else {
101+
const errmsg = (new TextEncoder()).encode('No lookup function registered for key: ' + request.identifier)
102+
response = new FetchResponse({ status: FetchResponse.StatusCode.ERROR, data: errmsg })
103+
}
104+
105+
shake.write(lp.encode.single(FetchResponse.encode(response).finish()))
106+
}
107+
108+
/**
109+
* Given a key, finds the appropriate function for looking up its corresponding value, based on
110+
* the key's prefix.
111+
*
112+
* @param {string} key
113+
*/
114+
_getLookupFunction (key) {
115+
for (const prefix of this._lookupFunctions.keys()) {
116+
if (key.startsWith(prefix)) {
117+
return this._lookupFunctions.get(prefix)
118+
}
119+
}
120+
return null
121+
}
122+
123+
/**
124+
* Registers a new lookup callback that can map keys to values, for a given set of keys that
125+
* share the same prefix.
126+
*
127+
* @param {string} prefix
128+
* @param {LookupFunction} lookup
129+
*/
130+
registerLookupFunction (prefix, lookup) {
131+
if (this._lookupFunctions.has(prefix)) {
132+
throw errCode(new Error("Fetch protocol handler for key prefix '" + prefix + "' already registered"), codes.ERR_KEY_ALREADY_EXISTS)
133+
}
134+
this._lookupFunctions.set(prefix, lookup)
135+
}
136+
137+
/**
138+
* Registers a new lookup callback that can map keys to values, for a given set of keys that
139+
* share the same prefix.
140+
*
141+
* @param {string} prefix
142+
* @param {LookupFunction} [lookup]
143+
*/
144+
unregisterLookupFunction (prefix, lookup) {
145+
if (lookup != null) {
146+
const existingLookup = this._lookupFunctions.get(prefix)
147+
148+
if (existingLookup !== lookup) {
149+
return
150+
}
151+
}
152+
153+
this._lookupFunctions.delete(prefix)
154+
}
155+
}
156+
157+
FetchProtocol.PROTOCOL = PROTOCOL
158+
159+
exports = module.exports = FetchProtocol

‎src/fetch/proto.d.ts

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import * as $protobuf from "protobufjs";
2+
/** Properties of a FetchRequest. */
3+
export interface IFetchRequest {
4+
5+
/** FetchRequest identifier */
6+
identifier?: (string|null);
7+
}
8+
9+
/** Represents a FetchRequest. */
10+
export class FetchRequest implements IFetchRequest {
11+
12+
/**
13+
* Constructs a new FetchRequest.
14+
* @param [p] Properties to set
15+
*/
16+
constructor(p?: IFetchRequest);
17+
18+
/** FetchRequest identifier. */
19+
public identifier: string;
20+
21+
/**
22+
* Encodes the specified FetchRequest message. Does not implicitly {@link FetchRequest.verify|verify} messages.
23+
* @param m FetchRequest message or plain object to encode
24+
* @param [w] Writer to encode to
25+
* @returns Writer
26+
*/
27+
public static encode(m: IFetchRequest, w?: $protobuf.Writer): $protobuf.Writer;
28+
29+
/**
30+
* Decodes a FetchRequest message from the specified reader or buffer.
31+
* @param r Reader or buffer to decode from
32+
* @param [l] Message length if known beforehand
33+
* @returns FetchRequest
34+
* @throws {Error} If the payload is not a reader or valid buffer
35+
* @throws {$protobuf.util.ProtocolError} If required fields are missing
36+
*/
37+
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): FetchRequest;
38+
39+
/**
40+
* Creates a FetchRequest message from a plain object. Also converts values to their respective internal types.
41+
* @param d Plain object
42+
* @returns FetchRequest
43+
*/
44+
public static fromObject(d: { [k: string]: any }): FetchRequest;
45+
46+
/**
47+
* Creates a plain object from a FetchRequest message. Also converts values to other types if specified.
48+
* @param m FetchRequest
49+
* @param [o] Conversion options
50+
* @returns Plain object
51+
*/
52+
public static toObject(m: FetchRequest, o?: $protobuf.IConversionOptions): { [k: string]: any };
53+
54+
/**
55+
* Converts this FetchRequest to JSON.
56+
* @returns JSON object
57+
*/
58+
public toJSON(): { [k: string]: any };
59+
}
60+
61+
/** Properties of a FetchResponse. */
62+
export interface IFetchResponse {
63+
64+
/** FetchResponse status */
65+
status?: (FetchResponse.StatusCode|null);
66+
67+
/** FetchResponse data */
68+
data?: (Uint8Array|null);
69+
}
70+
71+
/** Represents a FetchResponse. */
72+
export class FetchResponse implements IFetchResponse {
73+
74+
/**
75+
* Constructs a new FetchResponse.
76+
* @param [p] Properties to set
77+
*/
78+
constructor(p?: IFetchResponse);
79+
80+
/** FetchResponse status. */
81+
public status: FetchResponse.StatusCode;
82+
83+
/** FetchResponse data. */
84+
public data: Uint8Array;
85+
86+
/**
87+
* Encodes the specified FetchResponse message. Does not implicitly {@link FetchResponse.verify|verify} messages.
88+
* @param m FetchResponse message or plain object to encode
89+
* @param [w] Writer to encode to
90+
* @returns Writer
91+
*/
92+
public static encode(m: IFetchResponse, w?: $protobuf.Writer): $protobuf.Writer;
93+
94+
/**
95+
* Decodes a FetchResponse message from the specified reader or buffer.
96+
* @param r Reader or buffer to decode from
97+
* @param [l] Message length if known beforehand
98+
* @returns FetchResponse
99+
* @throws {Error} If the payload is not a reader or valid buffer
100+
* @throws {$protobuf.util.ProtocolError} If required fields are missing
101+
*/
102+
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): FetchResponse;
103+
104+
/**
105+
* Creates a FetchResponse message from a plain object. Also converts values to their respective internal types.
106+
* @param d Plain object
107+
* @returns FetchResponse
108+
*/
109+
public static fromObject(d: { [k: string]: any }): FetchResponse;
110+
111+
/**
112+
* Creates a plain object from a FetchResponse message. Also converts values to other types if specified.
113+
* @param m FetchResponse
114+
* @param [o] Conversion options
115+
* @returns Plain object
116+
*/
117+
public static toObject(m: FetchResponse, o?: $protobuf.IConversionOptions): { [k: string]: any };
118+
119+
/**
120+
* Converts this FetchResponse to JSON.
121+
* @returns JSON object
122+
*/
123+
public toJSON(): { [k: string]: any };
124+
}
125+
126+
export namespace FetchResponse {
127+
128+
/** StatusCode enum. */
129+
enum StatusCode {
130+
OK = 0,
131+
NOT_FOUND = 1,
132+
ERROR = 2
133+
}
134+
}

‎src/fetch/proto.js

+333
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
/*eslint-disable*/
2+
"use strict";
3+
4+
var $protobuf = require("protobufjs/minimal");
5+
6+
// Common aliases
7+
var $Reader = $protobuf.Reader, $Writer = $protobuf.Writer, $util = $protobuf.util;
8+
9+
// Exported root namespace
10+
var $root = $protobuf.roots["libp2p-fetch"] || ($protobuf.roots["libp2p-fetch"] = {});
11+
12+
$root.FetchRequest = (function() {
13+
14+
/**
15+
* Properties of a FetchRequest.
16+
* @exports IFetchRequest
17+
* @interface IFetchRequest
18+
* @property {string|null} [identifier] FetchRequest identifier
19+
*/
20+
21+
/**
22+
* Constructs a new FetchRequest.
23+
* @exports FetchRequest
24+
* @classdesc Represents a FetchRequest.
25+
* @implements IFetchRequest
26+
* @constructor
27+
* @param {IFetchRequest=} [p] Properties to set
28+
*/
29+
function FetchRequest(p) {
30+
if (p)
31+
for (var ks = Object.keys(p), i = 0; i < ks.length; ++i)
32+
if (p[ks[i]] != null)
33+
this[ks[i]] = p[ks[i]];
34+
}
35+
36+
/**
37+
* FetchRequest identifier.
38+
* @member {string} identifier
39+
* @memberof FetchRequest
40+
* @instance
41+
*/
42+
FetchRequest.prototype.identifier = "";
43+
44+
/**
45+
* Encodes the specified FetchRequest message. Does not implicitly {@link FetchRequest.verify|verify} messages.
46+
* @function encode
47+
* @memberof FetchRequest
48+
* @static
49+
* @param {IFetchRequest} m FetchRequest message or plain object to encode
50+
* @param {$protobuf.Writer} [w] Writer to encode to
51+
* @returns {$protobuf.Writer} Writer
52+
*/
53+
FetchRequest.encode = function encode(m, w) {
54+
if (!w)
55+
w = $Writer.create();
56+
if (m.identifier != null && Object.hasOwnProperty.call(m, "identifier"))
57+
w.uint32(10).string(m.identifier);
58+
return w;
59+
};
60+
61+
/**
62+
* Decodes a FetchRequest message from the specified reader or buffer.
63+
* @function decode
64+
* @memberof FetchRequest
65+
* @static
66+
* @param {$protobuf.Reader|Uint8Array} r Reader or buffer to decode from
67+
* @param {number} [l] Message length if known beforehand
68+
* @returns {FetchRequest} FetchRequest
69+
* @throws {Error} If the payload is not a reader or valid buffer
70+
* @throws {$protobuf.util.ProtocolError} If required fields are missing
71+
*/
72+
FetchRequest.decode = function decode(r, l) {
73+
if (!(r instanceof $Reader))
74+
r = $Reader.create(r);
75+
var c = l === undefined ? r.len : r.pos + l, m = new $root.FetchRequest();
76+
while (r.pos < c) {
77+
var t = r.uint32();
78+
switch (t >>> 3) {
79+
case 1:
80+
m.identifier = r.string();
81+
break;
82+
default:
83+
r.skipType(t & 7);
84+
break;
85+
}
86+
}
87+
return m;
88+
};
89+
90+
/**
91+
* Creates a FetchRequest message from a plain object. Also converts values to their respective internal types.
92+
* @function fromObject
93+
* @memberof FetchRequest
94+
* @static
95+
* @param {Object.<string,*>} d Plain object
96+
* @returns {FetchRequest} FetchRequest
97+
*/
98+
FetchRequest.fromObject = function fromObject(d) {
99+
if (d instanceof $root.FetchRequest)
100+
return d;
101+
var m = new $root.FetchRequest();
102+
if (d.identifier != null) {
103+
m.identifier = String(d.identifier);
104+
}
105+
return m;
106+
};
107+
108+
/**
109+
* Creates a plain object from a FetchRequest message. Also converts values to other types if specified.
110+
* @function toObject
111+
* @memberof FetchRequest
112+
* @static
113+
* @param {FetchRequest} m FetchRequest
114+
* @param {$protobuf.IConversionOptions} [o] Conversion options
115+
* @returns {Object.<string,*>} Plain object
116+
*/
117+
FetchRequest.toObject = function toObject(m, o) {
118+
if (!o)
119+
o = {};
120+
var d = {};
121+
if (o.defaults) {
122+
d.identifier = "";
123+
}
124+
if (m.identifier != null && m.hasOwnProperty("identifier")) {
125+
d.identifier = m.identifier;
126+
}
127+
return d;
128+
};
129+
130+
/**
131+
* Converts this FetchRequest to JSON.
132+
* @function toJSON
133+
* @memberof FetchRequest
134+
* @instance
135+
* @returns {Object.<string,*>} JSON object
136+
*/
137+
FetchRequest.prototype.toJSON = function toJSON() {
138+
return this.constructor.toObject(this, $protobuf.util.toJSONOptions);
139+
};
140+
141+
return FetchRequest;
142+
})();
143+
144+
$root.FetchResponse = (function() {
145+
146+
/**
147+
* Properties of a FetchResponse.
148+
* @exports IFetchResponse
149+
* @interface IFetchResponse
150+
* @property {FetchResponse.StatusCode|null} [status] FetchResponse status
151+
* @property {Uint8Array|null} [data] FetchResponse data
152+
*/
153+
154+
/**
155+
* Constructs a new FetchResponse.
156+
* @exports FetchResponse
157+
* @classdesc Represents a FetchResponse.
158+
* @implements IFetchResponse
159+
* @constructor
160+
* @param {IFetchResponse=} [p] Properties to set
161+
*/
162+
function FetchResponse(p) {
163+
if (p)
164+
for (var ks = Object.keys(p), i = 0; i < ks.length; ++i)
165+
if (p[ks[i]] != null)
166+
this[ks[i]] = p[ks[i]];
167+
}
168+
169+
/**
170+
* FetchResponse status.
171+
* @member {FetchResponse.StatusCode} status
172+
* @memberof FetchResponse
173+
* @instance
174+
*/
175+
FetchResponse.prototype.status = 0;
176+
177+
/**
178+
* FetchResponse data.
179+
* @member {Uint8Array} data
180+
* @memberof FetchResponse
181+
* @instance
182+
*/
183+
FetchResponse.prototype.data = $util.newBuffer([]);
184+
185+
/**
186+
* Encodes the specified FetchResponse message. Does not implicitly {@link FetchResponse.verify|verify} messages.
187+
* @function encode
188+
* @memberof FetchResponse
189+
* @static
190+
* @param {IFetchResponse} m FetchResponse message or plain object to encode
191+
* @param {$protobuf.Writer} [w] Writer to encode to
192+
* @returns {$protobuf.Writer} Writer
193+
*/
194+
FetchResponse.encode = function encode(m, w) {
195+
if (!w)
196+
w = $Writer.create();
197+
if (m.status != null && Object.hasOwnProperty.call(m, "status"))
198+
w.uint32(8).int32(m.status);
199+
if (m.data != null && Object.hasOwnProperty.call(m, "data"))
200+
w.uint32(18).bytes(m.data);
201+
return w;
202+
};
203+
204+
/**
205+
* Decodes a FetchResponse message from the specified reader or buffer.
206+
* @function decode
207+
* @memberof FetchResponse
208+
* @static
209+
* @param {$protobuf.Reader|Uint8Array} r Reader or buffer to decode from
210+
* @param {number} [l] Message length if known beforehand
211+
* @returns {FetchResponse} FetchResponse
212+
* @throws {Error} If the payload is not a reader or valid buffer
213+
* @throws {$protobuf.util.ProtocolError} If required fields are missing
214+
*/
215+
FetchResponse.decode = function decode(r, l) {
216+
if (!(r instanceof $Reader))
217+
r = $Reader.create(r);
218+
var c = l === undefined ? r.len : r.pos + l, m = new $root.FetchResponse();
219+
while (r.pos < c) {
220+
var t = r.uint32();
221+
switch (t >>> 3) {
222+
case 1:
223+
m.status = r.int32();
224+
break;
225+
case 2:
226+
m.data = r.bytes();
227+
break;
228+
default:
229+
r.skipType(t & 7);
230+
break;
231+
}
232+
}
233+
return m;
234+
};
235+
236+
/**
237+
* Creates a FetchResponse message from a plain object. Also converts values to their respective internal types.
238+
* @function fromObject
239+
* @memberof FetchResponse
240+
* @static
241+
* @param {Object.<string,*>} d Plain object
242+
* @returns {FetchResponse} FetchResponse
243+
*/
244+
FetchResponse.fromObject = function fromObject(d) {
245+
if (d instanceof $root.FetchResponse)
246+
return d;
247+
var m = new $root.FetchResponse();
248+
switch (d.status) {
249+
case "OK":
250+
case 0:
251+
m.status = 0;
252+
break;
253+
case "NOT_FOUND":
254+
case 1:
255+
m.status = 1;
256+
break;
257+
case "ERROR":
258+
case 2:
259+
m.status = 2;
260+
break;
261+
}
262+
if (d.data != null) {
263+
if (typeof d.data === "string")
264+
$util.base64.decode(d.data, m.data = $util.newBuffer($util.base64.length(d.data)), 0);
265+
else if (d.data.length)
266+
m.data = d.data;
267+
}
268+
return m;
269+
};
270+
271+
/**
272+
* Creates a plain object from a FetchResponse message. Also converts values to other types if specified.
273+
* @function toObject
274+
* @memberof FetchResponse
275+
* @static
276+
* @param {FetchResponse} m FetchResponse
277+
* @param {$protobuf.IConversionOptions} [o] Conversion options
278+
* @returns {Object.<string,*>} Plain object
279+
*/
280+
FetchResponse.toObject = function toObject(m, o) {
281+
if (!o)
282+
o = {};
283+
var d = {};
284+
if (o.defaults) {
285+
d.status = o.enums === String ? "OK" : 0;
286+
if (o.bytes === String)
287+
d.data = "";
288+
else {
289+
d.data = [];
290+
if (o.bytes !== Array)
291+
d.data = $util.newBuffer(d.data);
292+
}
293+
}
294+
if (m.status != null && m.hasOwnProperty("status")) {
295+
d.status = o.enums === String ? $root.FetchResponse.StatusCode[m.status] : m.status;
296+
}
297+
if (m.data != null && m.hasOwnProperty("data")) {
298+
d.data = o.bytes === String ? $util.base64.encode(m.data, 0, m.data.length) : o.bytes === Array ? Array.prototype.slice.call(m.data) : m.data;
299+
}
300+
return d;
301+
};
302+
303+
/**
304+
* Converts this FetchResponse to JSON.
305+
* @function toJSON
306+
* @memberof FetchResponse
307+
* @instance
308+
* @returns {Object.<string,*>} JSON object
309+
*/
310+
FetchResponse.prototype.toJSON = function toJSON() {
311+
return this.constructor.toObject(this, $protobuf.util.toJSONOptions);
312+
};
313+
314+
/**
315+
* StatusCode enum.
316+
* @name FetchResponse.StatusCode
317+
* @enum {number}
318+
* @property {number} OK=0 OK value
319+
* @property {number} NOT_FOUND=1 NOT_FOUND value
320+
* @property {number} ERROR=2 ERROR value
321+
*/
322+
FetchResponse.StatusCode = (function() {
323+
var valuesById = {}, values = Object.create(valuesById);
324+
values[valuesById[0] = "OK"] = 0;
325+
values[valuesById[1] = "NOT_FOUND"] = 1;
326+
values[valuesById[2] = "ERROR"] = 2;
327+
return values;
328+
})();
329+
330+
return FetchResponse;
331+
})();
332+
333+
module.exports = $root;

‎src/fetch/proto.proto

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
syntax = "proto3";
2+
3+
message FetchRequest {
4+
string identifier = 1;
5+
}
6+
7+
message FetchResponse {
8+
StatusCode status = 1;
9+
enum StatusCode {
10+
OK = 0;
11+
NOT_FOUND = 1;
12+
ERROR = 2;
13+
}
14+
bytes data = 2;
15+
}

‎src/index.js

+20
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const PubsubAdapter = require('./pubsub-adapter')
3131
const Registrar = require('./registrar')
3232
const ping = require('./ping')
3333
const IdentifyService = require('./identify')
34+
const FetchService = require('./fetch')
3435
const NatManager = require('./nat-manager')
3536
const { updateSelfPeerRecord } = require('./record/utils')
3637

@@ -323,6 +324,8 @@ class Libp2p extends EventEmitter {
323324
ping.mount(this)
324325

325326
this._onDiscoveryPeer = this._onDiscoveryPeer.bind(this)
327+
328+
this.fetchService = new FetchService(this)
326329
}
327330

328331
/**
@@ -356,6 +359,10 @@ class Libp2p extends EventEmitter {
356359
await this.handle(Object.values(IdentifyService.getProtocolStr(this)), this.identifyService.handleMessage)
357360
}
358361

362+
if (this.fetchService) {
363+
await this.handle(FetchService.PROTOCOL, this.fetchService.handleMessage)
364+
}
365+
359366
try {
360367
await this._onStarting()
361368
await this._onDidStart()
@@ -407,6 +414,8 @@ class Libp2p extends EventEmitter {
407414
await this.natManager.stop()
408415
await this.transportManager.close()
409416

417+
this.unhandle(FetchService.PROTOCOL)
418+
410419
ping.unmount(this)
411420
this.dialer.destroy()
412421
} catch (/** @type {any} */ err) {
@@ -559,6 +568,17 @@ class Libp2p extends EventEmitter {
559568
)
560569
}
561570

571+
/**
572+
* Sends a request to fetch the value associated with the given key from the given peer.
573+
*
574+
* @param {PeerId|Multiaddr} peer
575+
* @param {string} key
576+
* @returns {Promise<Uint8Array | null>}
577+
*/
578+
fetch (peer, key) {
579+
return this.fetchService.fetch(peer, key)
580+
}
581+
562582
/**
563583
* Pings the given peer in order to obtain the operation latency.
564584
*

‎test/fetch/fetch.node.js

+155
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
'use strict'
2+
/* eslint-env mocha */
3+
4+
const { expect } = require('aegir/utils/chai')
5+
const Libp2p = require('../../src')
6+
const TCP = require('libp2p-tcp')
7+
const Mplex = require('libp2p-mplex')
8+
const { NOISE } = require('@chainsafe/libp2p-noise')
9+
const MDNS = require('libp2p-mdns')
10+
const { createPeerId } = require('../utils/creators/peer')
11+
const { codes } = require('../../src/errors')
12+
const { Multiaddr } = require('multiaddr')
13+
14+
async function createLibp2pNode (peerId) {
15+
return await Libp2p.create({
16+
peerId,
17+
addresses: {
18+
listen: ['/ip4/0.0.0.0/tcp/0']
19+
},
20+
modules: {
21+
transport: [TCP],
22+
streamMuxer: [Mplex],
23+
connEncryption: [NOISE],
24+
peerDiscovery: [MDNS]
25+
}
26+
})
27+
}
28+
29+
describe('Fetch', () => {
30+
/** @type {Libp2p} */
31+
let sender
32+
/** @type {Libp2p} */
33+
let receiver
34+
const PREFIX_A = '/moduleA/'
35+
const PREFIX_B = '/moduleB/'
36+
const DATA_A = { foobar: 'hello world' }
37+
const DATA_B = { foobar: 'goodnight moon' }
38+
39+
const generateLookupFunction = function (prefix, data) {
40+
return async function (key) {
41+
key = key.slice(prefix.length) // strip prefix from key
42+
const val = data[key]
43+
if (val) {
44+
return (new TextEncoder()).encode(val)
45+
}
46+
return null
47+
}
48+
}
49+
50+
beforeEach(async () => {
51+
const [peerIdA, peerIdB] = await createPeerId({ number: 2 })
52+
sender = await createLibp2pNode(peerIdA)
53+
receiver = await createLibp2pNode(peerIdB)
54+
55+
await sender.start()
56+
await receiver.start()
57+
58+
await Promise.all([
59+
...sender.multiaddrs.map(addr => receiver.dial(addr.encapsulate(new Multiaddr(`/p2p/${sender.peerId}`)))),
60+
...receiver.multiaddrs.map(addr => sender.dial(addr.encapsulate(new Multiaddr(`/p2p/${receiver.peerId}`))))
61+
])
62+
})
63+
64+
afterEach(async () => {
65+
receiver.fetchService.unregisterLookupFunction(PREFIX_A)
66+
receiver.fetchService.unregisterLookupFunction(PREFIX_B)
67+
68+
await sender.stop()
69+
await receiver.stop()
70+
})
71+
72+
it('fetch key that exists in receivers datastore', async () => {
73+
receiver.fetchService.registerLookupFunction(PREFIX_A, generateLookupFunction(PREFIX_A, DATA_A))
74+
75+
const rawData = await sender.fetch(receiver.peerId, '/moduleA/foobar')
76+
const value = (new TextDecoder()).decode(rawData)
77+
expect(value).to.equal('hello world')
78+
})
79+
80+
it('Different lookups for different prefixes', async () => {
81+
receiver.fetchService.registerLookupFunction(PREFIX_A, generateLookupFunction(PREFIX_A, DATA_A))
82+
receiver.fetchService.registerLookupFunction(PREFIX_B, generateLookupFunction(PREFIX_B, DATA_B))
83+
84+
const rawDataA = await sender.fetch(receiver.peerId, '/moduleA/foobar')
85+
const valueA = (new TextDecoder()).decode(rawDataA)
86+
expect(valueA).to.equal('hello world')
87+
88+
// Different lookup functions can be registered on different prefixes, and have different
89+
// values for the same key underneath the different prefix.
90+
const rawDataB = await sender.fetch(receiver.peerId, '/moduleB/foobar')
91+
const valueB = (new TextDecoder()).decode(rawDataB)
92+
expect(valueB).to.equal('goodnight moon')
93+
})
94+
95+
it('fetch key that does not exist in receivers datastore', async () => {
96+
receiver.fetchService.registerLookupFunction(PREFIX_A, generateLookupFunction(PREFIX_A, DATA_A))
97+
const result = await sender.fetch(receiver.peerId, '/moduleA/garbage')
98+
99+
expect(result).to.equal(null)
100+
})
101+
102+
it('fetch key with unknown prefix throws error', async () => {
103+
receiver.fetchService.registerLookupFunction(PREFIX_A, generateLookupFunction(PREFIX_A, DATA_A))
104+
105+
await expect(sender.fetch(receiver.peerId, '/moduleUNKNOWN/foobar'))
106+
.to.eventually.be.rejected.with.property('code', codes.ERR_INVALID_PARAMETERS)
107+
})
108+
109+
it('registering multiple handlers for same prefix errors', async () => {
110+
receiver.fetchService.registerLookupFunction(PREFIX_A, generateLookupFunction(PREFIX_A, DATA_A))
111+
112+
expect(() => receiver.fetchService.registerLookupFunction(PREFIX_A, generateLookupFunction(PREFIX_A, DATA_B)))
113+
.to.throw().with.property('code', codes.ERR_KEY_ALREADY_EXISTS)
114+
})
115+
116+
it('can unregister handler', async () => {
117+
const lookupFunction = generateLookupFunction(PREFIX_A, DATA_A)
118+
receiver.fetchService.registerLookupFunction(PREFIX_A, lookupFunction)
119+
const rawDataA = await sender.fetch(receiver.peerId, '/moduleA/foobar')
120+
const valueA = (new TextDecoder()).decode(rawDataA)
121+
expect(valueA).to.equal('hello world')
122+
123+
receiver.fetchService.unregisterLookupFunction(PREFIX_A, lookupFunction)
124+
125+
await expect(sender.fetch(receiver.peerId, '/moduleA/foobar'))
126+
.to.eventually.be.rejectedWith(/No lookup function registered for key/)
127+
})
128+
129+
it('can unregister all handlers', async () => {
130+
const lookupFunction = generateLookupFunction(PREFIX_A, DATA_A)
131+
receiver.fetchService.registerLookupFunction(PREFIX_A, lookupFunction)
132+
const rawDataA = await sender.fetch(receiver.peerId, '/moduleA/foobar')
133+
const valueA = (new TextDecoder()).decode(rawDataA)
134+
expect(valueA).to.equal('hello world')
135+
136+
receiver.fetchService.unregisterLookupFunction(PREFIX_A)
137+
138+
await expect(sender.fetch(receiver.peerId, '/moduleA/foobar'))
139+
.to.eventually.be.rejectedWith(/No lookup function registered for key/)
140+
})
141+
142+
it('does not unregister wrong handlers', async () => {
143+
const lookupFunction = generateLookupFunction(PREFIX_A, DATA_A)
144+
receiver.fetchService.registerLookupFunction(PREFIX_A, lookupFunction)
145+
const rawDataA = await sender.fetch(receiver.peerId, '/moduleA/foobar')
146+
const valueA = (new TextDecoder()).decode(rawDataA)
147+
expect(valueA).to.equal('hello world')
148+
149+
receiver.fetchService.unregisterLookupFunction(PREFIX_A, () => {})
150+
151+
const rawDataB = await sender.fetch(receiver.peerId, '/moduleA/foobar')
152+
const valueB = (new TextDecoder()).decode(rawDataB)
153+
expect(valueB).to.equal('hello world')
154+
})
155+
})

‎tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
],
99
"exclude": [
1010
"src/circuit/protocol/index.js", // exclude generated file
11+
"src/fetch/proto.js", // exclude generated file
1112
"src/identify/message.js", // exclude generated file
1213
"src/insecure/proto.js", // exclude generated file
1314
"src/peer-store/pb/peer.js", // exclude generated file

0 commit comments

Comments
 (0)
Please sign in to comment.