Skip to content

Commit fdd1fc2

Browse files
committed
refactor: client with new routing v1
1 parent 7dac713 commit fdd1fc2

File tree

7 files changed

+198
-37
lines changed

7 files changed

+198
-37
lines changed

packages/client/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
[![codecov](https://img.shields.io/codecov/c/github/ipfs/helia-routing-v1-http-api.svg?style=flat-square)](https://codecov.io/gh/ipfs/helia-routing-v1-http-api)
1212
[![CI](https://img.shields.io/github/actions/workflow/status/ipfs/helia-routing-v1-http-api/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/ipfs/helia-routing-v1-http-api/actions/workflows/js-test-and-release.yml?query=branch%3Amain)
1313

14-
> A Routing V1 HTTP API client
14+
> A [Delegated Routing V1](https://specs.ipfs.tech/routing/http-routing-v1/) HTTP API client
1515
1616
## Table of contents <!-- omit in toc -->
1717

packages/client/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,9 @@
136136
"@multiformats/multiaddr": "^12.1.3",
137137
"any-signal": "^4.1.1",
138138
"browser-readablestream-to-it": "^2.0.3",
139+
"ipns": "^7.0.1",
139140
"it-all": "^3.0.2",
141+
"it-to-buffer": "^4.0.3",
140142
"iterable-ndjson": "^1.1.0",
141143
"multiformats": "^12.1.1",
142144
"p-defer": "^4.0.0",

packages/client/src/client.ts

+153-28
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,19 @@ import { peerIdFromString } from '@libp2p/peer-id'
44
import { multiaddr } from '@multiformats/multiaddr'
55
import { anySignal } from 'any-signal'
66
import toIt from 'browser-readablestream-to-it'
7+
import { unmarshal, type IPNSRecord, marshal } from 'ipns'
8+
import toBuffer from 'it-to-buffer'
79
// @ts-expect-error no types
810
import ndjson from 'iterable-ndjson'
911
import defer from 'p-defer'
1012
import PQueue from 'p-queue'
11-
import type { RoutingV1HttpApiClient, RoutingV1HttpApiClientInit } from './index.js'
13+
import type { RoutingV1HttpApiClient, RoutingV1HttpApiClientInit, Record } from './index.js'
1214
import type { AbortOptions } from '@libp2p/interface'
13-
import type { PeerInfo } from '@libp2p/interface/peer-info'
14-
import type { Multiaddr } from '@multiformats/multiaddr'
15+
import type { PeerId } from '@libp2p/interface/peer-id'
1516
import type { CID } from 'multiformats'
1617

1718
const log = logger('routing-v1-http-api-client')
1819

19-
interface RoutingV1HttpApiGetProvidersResponse {
20-
Protocol: string
21-
Schema: string
22-
ID: string
23-
Addrs: Multiaddr[]
24-
}
25-
2620
const defaultValues = {
2721
concurrentRequests: 4,
2822
timeout: 30e3
@@ -62,7 +56,7 @@ export class DefaultRoutingV1HttpApiClient implements RoutingV1HttpApiClient {
6256
this.started = false
6357
}
6458

65-
async * getProviders (cid: CID, options: AbortOptions | undefined = {}): AsyncGenerator<PeerInfo, any, unknown> {
59+
async * getProviders (cid: CID, options: AbortOptions | undefined = {}): AsyncGenerator<Record, any, unknown> {
6660
log('findProviders starts: %c', cid)
6761

6862
const signal = anySignal([this.shutDownController.signal, options.signal, AbortSignal.timeout(this.timeout)])
@@ -77,7 +71,7 @@ export class DefaultRoutingV1HttpApiClient implements RoutingV1HttpApiClient {
7771
try {
7872
await onStart.promise
7973

80-
// https://github.com/ipfs/specs/blob/main/routing/ROUTING_V1_HTTP.md#api
74+
// https://specs.ipfs.tech/routing/http-routing-v1/
8175
const resource = `${this.clientUrl}routing/v1/providers/${cid.toString()}`
8276
const getOptions = { headers: { Accept: 'application/x-ndjson' }, signal }
8377
const a = await fetch(resource, getOptions)
@@ -86,12 +80,23 @@ export class DefaultRoutingV1HttpApiClient implements RoutingV1HttpApiClient {
8680
throw new CodeError('Routing response had no body', 'ERR_BAD_RESPONSE')
8781
}
8882

89-
for await (const event of ndjson(toIt(a.body))) {
90-
if (event.Protocol !== 'transport-bitswap') {
91-
continue
92-
}
83+
const contentType = a.headers.get('Content-Type')
84+
if (contentType === 'application/json') {
85+
const body = await a.json()
9386

94-
yield this.#mapProvider(event)
87+
for (const provider of body.Providers) {
88+
const record = this.#handleRecord(provider)
89+
if (record !== null) {
90+
yield record
91+
}
92+
}
93+
} else {
94+
for await (const provider of ndjson(toIt(a.body))) {
95+
const record = this.#handleRecord(provider)
96+
if (record !== null) {
97+
yield record
98+
}
99+
}
95100
}
96101
} catch (err) {
97102
log.error('findProviders errored:', err)
@@ -102,21 +107,141 @@ export class DefaultRoutingV1HttpApiClient implements RoutingV1HttpApiClient {
102107
}
103108
}
104109

105-
#mapProvider (event: RoutingV1HttpApiGetProvidersResponse): PeerInfo {
106-
const peer = peerIdFromString(event.ID)
107-
const ma: Multiaddr[] = []
110+
async * getPeers (pid: PeerId, options: AbortOptions | undefined = {}): AsyncGenerator<Record, any, unknown> {
111+
log('findPeers starts: %c', pid)
112+
113+
const signal = anySignal([this.shutDownController.signal, options.signal, AbortSignal.timeout(this.timeout)])
114+
const onStart = defer()
115+
const onFinish = defer()
116+
117+
void this.httpQueue.add(async () => {
118+
onStart.resolve()
119+
return onFinish.promise
120+
})
121+
122+
try {
123+
await onStart.promise
124+
125+
// https://specs.ipfs.tech/routing/http-routing-v1/
126+
const resource = `${this.clientUrl}routing/v1/peers/${pid.toCID().toString()}`
127+
const getOptions = { headers: { Accept: 'application/x-ndjson' }, signal }
128+
const a = await fetch(resource, getOptions)
129+
130+
if (a.body == null) {
131+
throw new CodeError('Routing response had no body', 'ERR_BAD_RESPONSE')
132+
}
133+
134+
const contentType = a.headers.get('Content-Type')
135+
if (contentType === 'application/json') {
136+
const body = await a.json()
108137

109-
for (const strAddr of event.Addrs) {
110-
const addr = multiaddr(strAddr)
111-
ma.push(addr)
138+
for (const peer of body.Peers) {
139+
const record = this.#handleRecord(peer)
140+
if (record !== null) {
141+
yield record
142+
}
143+
}
144+
} else {
145+
for await (const peer of ndjson(toIt(a.body))) {
146+
const record = this.#handleRecord(peer)
147+
if (record !== null) {
148+
yield record
149+
}
150+
}
151+
}
152+
} catch (err) {
153+
log.error('findPeers errored:', err)
154+
} finally {
155+
signal.clear()
156+
onFinish.resolve()
157+
log('findPeers finished: %c', pid)
112158
}
159+
}
160+
161+
async getIPNS (pid: PeerId, options: AbortOptions | undefined = {}): Promise<IPNSRecord> {
162+
log('getIPNS starts: %c', pid)
163+
164+
const signal = anySignal([this.shutDownController.signal, options.signal, AbortSignal.timeout(this.timeout)])
165+
const onStart = defer()
166+
const onFinish = defer()
113167

114-
const pi = {
115-
id: peer,
116-
multiaddrs: ma,
117-
protocols: []
168+
void this.httpQueue.add(async () => {
169+
onStart.resolve()
170+
return onFinish.promise
171+
})
172+
173+
try {
174+
await onStart.promise
175+
176+
// https://specs.ipfs.tech/routing/http-routing-v1/
177+
const resource = `${this.clientUrl}routing/v1/ipns/${pid.toCID().toString()}`
178+
const getOptions = { headers: { Accept: 'application/vnd.ipfs.ipns-record' }, signal }
179+
const a = await fetch(resource, getOptions)
180+
181+
if (a.body == null) {
182+
throw new CodeError('GET ipns response had no body', 'ERR_BAD_RESPONSE')
183+
}
184+
185+
const body = await toBuffer(toIt(a.body))
186+
return unmarshal(body)
187+
} finally {
188+
signal.clear()
189+
onFinish.resolve()
190+
log('getIPNS finished: %c', pid)
191+
}
192+
}
193+
194+
async putIPNS (pid: PeerId, record: IPNSRecord, options: AbortOptions | undefined = {}): Promise<void> {
195+
log('getIPNS starts: %c', pid)
196+
197+
const signal = anySignal([this.shutDownController.signal, options.signal, AbortSignal.timeout(this.timeout)])
198+
const onStart = defer()
199+
const onFinish = defer()
200+
201+
void this.httpQueue.add(async () => {
202+
onStart.resolve()
203+
return onFinish.promise
204+
})
205+
206+
try {
207+
await onStart.promise
208+
209+
const body = marshal(record)
210+
211+
// https://specs.ipfs.tech/routing/http-routing-v1/
212+
const resource = `${this.clientUrl}routing/v1/ipns/${pid.toCID().toString()}`
213+
const getOptions = { method: 'PUT', headers: { 'Content-Type': 'application/vnd.ipfs.ipns-record' }, body, signal }
214+
const res = await fetch(resource, getOptions)
215+
if (res.status !== 200) {
216+
throw new CodeError('PUT ipns response had status other than 200', 'ERR_BAD_RESPONSE')
217+
}
218+
} finally {
219+
signal.clear()
220+
onFinish.resolve()
221+
log('getIPNS finished: %c', pid)
222+
}
223+
}
224+
225+
#handleRecord (record: any): Record | null {
226+
if (record.Schema === 'peer') {
227+
// Peer schema can have additional, user-defined, fields.
228+
record.ID = peerIdFromString(record.ID)
229+
record.Addrs = record.Addrs.map(multiaddr)
230+
return record
231+
} else if (record.Schema === 'bitswap') {
232+
// Bitswap schema cannot have additional fields.
233+
return {
234+
Schema: record.Schema,
235+
Protocol: record.Protocol,
236+
ID: peerIdFromString(record.ID),
237+
Addrs: record.Addrs.map(multiaddr)
238+
}
239+
} else if (record.Schema !== '') {
240+
// TODO: in Go, we send unknown schemas as an UnknownRecord. I feel like
241+
// doing this here will make it harder. Is there a way in TypeScript
242+
// to do something like if schema === 'bitswap' then it is a BitswapRecord?
118243
}
119244

120-
return pi
245+
return null
121246
}
122247
}

packages/client/src/index.ts

+36-2
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,28 @@
1919

2020
import { DefaultRoutingV1HttpApiClient } from './client.js'
2121
import type { AbortOptions } from '@libp2p/interface'
22-
import type { PeerInfo } from '@libp2p/interface/peer-info'
22+
import type { PeerId } from '@libp2p/interface/peer-id'
23+
import type { Multiaddr } from '@multiformats/multiaddr'
24+
import type { IPNSRecord } from 'ipns'
2325
import type { CID } from 'multiformats/cid'
2426

27+
export interface PeerRecord {
28+
Schema: string
29+
ID: PeerId
30+
Addrs: Multiaddr[]
31+
Protocols: string[]
32+
}
33+
34+
// Deprecated: please use PeerRecord instead.
35+
export interface BitswapRecord {
36+
Schema: string
37+
Protocol: string
38+
ID: PeerId
39+
Addrs: Multiaddr[]
40+
}
41+
42+
export type Record = PeerRecord | BitswapRecord
43+
2544
export interface RoutingV1HttpApiClientInit {
2645
/**
2746
* A concurrency limit to avoid request flood in web browser (default: 4)
@@ -41,7 +60,22 @@ export interface RoutingV1HttpApiClient {
4160
* Returns an async generator of PeerInfos that can provide the content
4261
* for the passed CID
4362
*/
44-
getProviders(cid: CID, options?: AbortOptions): AsyncGenerator<PeerInfo>
63+
getProviders(cid: CID, options?: AbortOptions): AsyncGenerator<Record>
64+
65+
/**
66+
* Returns an async generator of PeerInfos for the provided PeerId
67+
*/
68+
getPeers(pid: PeerId, options?: AbortOptions): AsyncGenerator<Record>
69+
70+
/**
71+
* Returns a promise of a IPNSRecord for the given PeerId
72+
*/
73+
getIPNS(pid: PeerId, options?: AbortOptions): Promise<IPNSRecord>
74+
75+
/**
76+
* Publishes the given IPNSRecorded for the provided PeerId
77+
*/
78+
putIPNS(pid: PeerId, record: IPNSRecord, options?: AbortOptions): Promise<void>
4579

4680
/**
4781
* Shut down any currently running HTTP requests and clear up any resources

packages/client/test/index.spec.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ describe('routing-v1-http-api-client', () => {
3333
ID: (await createEd25519PeerId()).toString(),
3434
Addrs: ['/ip4/41.41.41.41/tcp/1234']
3535
}, {
36-
Protocol: 'transport-bitswap',
37-
Schema: 'bitswap',
36+
Protocols: ['transport-bitswap'],
37+
Schema: 'peer',
3838
Metadata: 'gBI=',
3939
ID: (await createEd25519PeerId()).toString(),
4040
Addrs: ['/ip4/42.42.42.42/tcp/1234']
@@ -50,8 +50,8 @@ describe('routing-v1-http-api-client', () => {
5050

5151
const provs = await all(client.getProviders(cid))
5252
expect(provs.map(prov => ({
53-
id: prov.id.toString(),
54-
addrs: prov.multiaddrs.map(ma => ma.toString())
53+
id: prov.ID.toString(),
54+
addrs: prov.Addrs.map(ma => ma.toString())
5555
}))).to.deep.equal(providers.map(prov => ({
5656
id: prov.ID,
5757
addrs: prov.Addrs

packages/interop/test/index.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ describe('routing-v1-http-api interop', () => {
6464

6565
for await (const prov of client.getProviders(cid)) {
6666
// should be a node in this test network
67-
if (network.map(node => node.libp2p.peerId.toString()).includes(prov.id.toString())) {
67+
if (network.map(node => node.libp2p.peerId.toString()).includes(prov.ID.toString())) {
6868
foundProvider = true
6969
break
7070
}

packages/server/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
[![codecov](https://img.shields.io/codecov/c/github/ipfs/helia-routing-v1-http-api.svg?style=flat-square)](https://codecov.io/gh/ipfs/helia-routing-v1-http-api)
1212
[![CI](https://img.shields.io/github/actions/workflow/status/ipfs/helia-routing-v1-http-api/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/ipfs/helia-routing-v1-http-api/actions/workflows/js-test-and-release.yml?query=branch%3Amain)
1313

14-
> A Routing V1 HTTP API server powered by Helia
14+
> A [Delegated Routing V1](https://specs.ipfs.tech/routing/http-routing-v1/) HTTP API server powered by Helia
1515
1616
## Table of contents <!-- omit in toc -->
1717

0 commit comments

Comments
 (0)