1+ import { logger } from '@libp2p/logger'
2+ import PQueue from 'p-queue'
3+ import defer from 'p-defer'
4+ import errCode from 'err-code'
5+ import anySignal from 'any-signal'
6+ import type { AbortOptions } from '@libp2p/interfaces'
7+ import type { ContentRouting } from '@libp2p/interface-content-routing'
8+ import type { PeerInfo } from '@libp2p/interface-peer-info'
9+ import type { Startable } from '@libp2p/interfaces/startable'
10+ import type { CID } from 'multiformats/cid'
11+ import HTTP from 'ipfs-utils/src/http.js'
12+ import { peerIdFromString } from '@libp2p/peer-id'
13+ import { multiaddr } from '@multiformats/multiaddr'
14+
15+ const log = logger ( 'libp2p:delegated-content-routing' )
16+
17+ const DEFAULT_TIMEOUT = 30e3 // 30 second default
18+ const CONCURRENT_HTTP_REQUESTS = 4
19+
20+ export interface HTTPClientExtraOptions {
21+ headers ?: Record < string , string >
22+ searchParams ?: URLSearchParams
23+ }
24+
25+ /**
26+ * An implementation of content routing, using a delegated peer
27+ */
28+ class ReframeV1Routing implements ContentRouting , Startable {
29+ private readonly clientUrl : URL
30+
31+ private readonly httpQueue : PQueue
32+ private started : boolean
33+ private abortController : AbortController
34+
35+ /**
36+ * Create a new DelegatedContentRouting instance
37+ */
38+ constructor ( protocol : string , host : string , port : string ) {
39+ this . started = false
40+ this . abortController = new AbortController ( )
41+
42+ // limit concurrency to avoid request flood in web browser
43+ // https://github.com/libp2p/js-libp2p-delegated-content-routing/issues/12
44+ this . httpQueue = new PQueue ( {
45+ concurrency : CONCURRENT_HTTP_REQUESTS
46+ } )
47+
48+ log ( `enabled IPNI routing via ${ protocol } ://${ host } :${ port } ` )
49+ this . clientUrl = new URL ( `${ protocol } ://${ host } :${ port } ` )
50+ }
51+
52+ isStarted ( ) : boolean {
53+ return this . started
54+ }
55+
56+ start ( ) : void {
57+ this . started = true
58+ }
59+
60+ stop ( ) : void {
61+ this . httpQueue . clear ( )
62+ this . abortController . abort ( )
63+ this . abortController = new AbortController ( )
64+ this . started = false
65+ }
66+
67+ /**
68+ * Search the dht for providers of the given CID.
69+ *
70+ * - call `findProviders` on the delegated node.
71+ */
72+ async * findProviders ( key : CID , options : HTTPClientExtraOptions & AbortOptions = { } ) : AsyncIterable < PeerInfo > {
73+ log ( 'findProviders starts: %c' , key )
74+ options . signal = anySignal ( [ this . abortController . signal ] . concat ( ( options . signal != null ) ? [ options . signal ] : [ ] ) )
75+ setTimeout ( ( ) => {
76+ this . abortController . abort ( "findProviders timed out" )
77+ } , DEFAULT_TIMEOUT ) ;
78+
79+ const onStart = defer ( )
80+ const onFinish = defer ( )
81+
82+ void this . httpQueue . add ( async ( ) => {
83+ onStart . resolve ( )
84+ return await onFinish . promise
85+ } )
86+
87+ try {
88+ await onStart . promise
89+
90+ const resource = `${ this . clientUrl } routing/v1/providers/${ key . toString ( ) } `
91+ const getOptions = { headers : { "Accept" : "application/x-ndjson" } , signal : this . abortController . signal }
92+ const a = await HTTP . get ( resource , getOptions )
93+ const b = a . ndjson ( )
94+ for await ( const event of b ) {
95+ if ( event . Protocol != "transport-bitswap" || event . Schema != "bitswap" ) {
96+ continue
97+ }
98+
99+ console . log ( event )
100+ yield this . mapEvent ( event )
101+ }
102+ } catch ( err ) {
103+ log . error ( 'findProviders errored:' , err )
104+ throw err
105+ } finally {
106+ onFinish . resolve ( )
107+ log ( 'findProviders finished: %c' , key )
108+ }
109+ }
110+
111+ mapEvent ( event : any ) : any {
112+ const peer = peerIdFromString ( event . ID )
113+ let ma : any = [ ]
114+ for ( const strAddr of event . Addrs ) {
115+ const addr = multiaddr ( strAddr )
116+ ma . push ( addr )
117+ }
118+ const pi = {
119+ id : peer ,
120+ multiaddrs : ma
121+ }
122+ return pi
123+ }
124+
125+ /**
126+ * Does nothing, just part of implementing the interface
127+ */
128+ async provide ( key : CID , options : HTTPClientExtraOptions & AbortOptions = { } ) : Promise < void > {
129+ }
130+
131+ /**
132+ * Does nothing, just part of implementing the interface
133+ */
134+ async put ( key : Uint8Array , value : Uint8Array , options : HTTPClientExtraOptions & AbortOptions = { } ) : Promise < void > {
135+ }
136+
137+ /**
138+ * Does nothing, just part of implementing the interface
139+ */
140+ async get ( key : Uint8Array , options : HTTPClientExtraOptions & AbortOptions = { } ) : Promise < Uint8Array > {
141+ throw errCode ( new Error ( 'Not found' ) , 'ERR_NOT_FOUND' )
142+ }
143+ }
144+
145+ export function reframeV1Routing ( protocol :string , host : string , port : string ) : ( components ?: any ) => ContentRouting {
146+ return ( ) => new ReframeV1Routing ( protocol , host , port )
147+ }
0 commit comments