@helia/delegated-routing-v1-http-api-client 5.1.1 → 5.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/client.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { NotFoundError, contentRoutingSymbol, peerRoutingSymbol, setMaxListeners } from '@libp2p/interface'
2
- import { logger } from '@libp2p/logger'
3
2
  import { peerIdFromString } from '@libp2p/peer-id'
4
3
  import { multiaddr } from '@multiformats/multiaddr'
5
4
  import { anySignal } from 'any-signal'
@@ -11,14 +10,12 @@ import defer from 'p-defer'
11
10
  import PQueue from 'p-queue'
12
11
  import { BadResponseError, InvalidRequestError } from './errors.js'
13
12
  import { DelegatedRoutingV1HttpApiClientContentRouting, DelegatedRoutingV1HttpApiClientPeerRouting } from './routings.js'
14
- import type { DelegatedRoutingV1HttpApiClient, DelegatedRoutingV1HttpApiClientInit, GetProvidersOptions, GetPeersOptions, GetIPNSOptions, PeerRecord } from './index.js'
15
- import type { ContentRouting, PeerRouting, AbortOptions, PeerId } from '@libp2p/interface'
13
+ import type { DelegatedRoutingV1HttpApiClient as DelegatedRoutingV1HttpApiClientInterface, DelegatedRoutingV1HttpApiClientInit, GetProvidersOptions, GetPeersOptions, GetIPNSOptions, PeerRecord, DelegatedRoutingV1HttpApiClientComponents } from './index.js'
14
+ import type { ContentRouting, PeerRouting, AbortOptions, PeerId, Logger } from '@libp2p/interface'
16
15
  import type { Multiaddr } from '@multiformats/multiaddr'
17
16
  import type { IPNSRecord } from 'ipns'
18
17
  import type { CID } from 'multiformats'
19
18
 
20
- const log = logger('delegated-routing-v1-http-api-client')
21
-
22
19
  const defaultValues = {
23
20
  concurrentRequests: 4,
24
21
  timeout: 30e3,
@@ -26,11 +23,11 @@ const defaultValues = {
26
23
  cacheName: 'delegated-routing-v1-cache'
27
24
  }
28
25
 
29
- export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV1HttpApiClient {
26
+ export class DelegatedRoutingV1HttpApiClient implements DelegatedRoutingV1HttpApiClientInterface {
27
+ public readonly url: URL
30
28
  private started: boolean
31
29
  private readonly httpQueue: PQueue
32
30
  private readonly shutDownController: AbortController
33
- private readonly clientUrl: URL
34
31
  private readonly timeout: number
35
32
  private readonly contentRouting: ContentRouting
36
33
  private readonly peerRouting: PeerRouting
@@ -40,10 +37,13 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
40
37
  private readonly cacheName: string
41
38
  private cache?: Cache
42
39
  private readonly cacheTTL: number
40
+ private log: Logger
41
+
43
42
  /**
44
43
  * Create a new DelegatedContentRouting instance
45
44
  */
46
- constructor (url: string | URL, init: DelegatedRoutingV1HttpApiClientInit = {}) {
45
+ constructor (components: DelegatedRoutingV1HttpApiClientComponents, init: DelegatedRoutingV1HttpApiClientInit & { url: string | URL }) {
46
+ this.log = components.logger.forComponent('delegated-routing-v1-http-api-client')
47
47
  this.started = false
48
48
  this.shutDownController = new AbortController()
49
49
  setMaxListeners(Infinity, this.shutDownController.signal)
@@ -51,7 +51,7 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
51
51
  concurrency: init.concurrentRequests ?? defaultValues.concurrentRequests
52
52
  })
53
53
  this.inFlightRequests = new Map() // Tracks in-flight requests to avoid duplicate requests
54
- this.clientUrl = url instanceof URL ? url : new URL(url)
54
+ this.url = init.url instanceof URL ? init.url : new URL(init.url)
55
55
  this.timeout = init.timeout ?? defaultValues.timeout
56
56
  this.filterAddrs = init.filterAddrs
57
57
  this.filterProtocols = init.filterProtocols
@@ -85,7 +85,7 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
85
85
  this.cache = await globalThis.caches?.open(this.cacheName)
86
86
 
87
87
  if (this.cache != null) {
88
- log('cache enabled with ttl %d', this.cacheTTL)
88
+ this.log('cache enabled with ttl %d', this.cacheTTL)
89
89
  }
90
90
  }
91
91
  }
@@ -101,7 +101,7 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
101
101
  }
102
102
 
103
103
  async * getProviders (cid: CID, options: GetProvidersOptions = {}): AsyncGenerator<PeerRecord> {
104
- log('getProviders starts: %c', cid)
104
+ this.log('getProviders starts: %c', cid)
105
105
 
106
106
  const timeoutSignal = AbortSignal.timeout(this.timeout)
107
107
  const signal = anySignal([this.shutDownController.signal, timeoutSignal, options.signal])
@@ -118,10 +118,15 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
118
118
  await onStart.promise
119
119
 
120
120
  // https://specs.ipfs.tech/routing/http-routing-v1/
121
- const url = new URL(`${this.clientUrl}routing/v1/providers/${cid}`)
121
+ const url = new URL(`${this.url}routing/v1/providers/${cid}`)
122
122
 
123
123
  this.#addFilterParams(url, options.filterAddrs, options.filterProtocols)
124
- const getOptions = { headers: { Accept: 'application/x-ndjson' }, signal }
124
+ const getOptions = {
125
+ headers: {
126
+ accept: 'application/x-ndjson, application/json'
127
+ },
128
+ signal
129
+ }
125
130
  const res = await this.#makeRequest(url.toString(), getOptions)
126
131
 
127
132
  if (!res.ok) {
@@ -142,15 +147,22 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
142
147
  throw new BadResponseError(`Unexpected status code: ${res.status}`)
143
148
  }
144
149
 
145
- if (res.body == null) {
146
- throw new BadResponseError('Routing response had no body')
147
- }
148
-
149
150
  const contentType = res.headers.get('Content-Type')
151
+
150
152
  if (contentType == null) {
151
153
  throw new BadResponseError('No Content-Type header received')
152
154
  }
153
155
 
156
+ if (res.body == null) {
157
+ if (contentType !== 'application/x-ndjson') {
158
+ throw new BadResponseError('Routing response had no body')
159
+ }
160
+
161
+ // cached ndjson responses have no body property if the gateway returned
162
+ // no results
163
+ return
164
+ }
165
+
154
166
  if (contentType.startsWith('application/json')) {
155
167
  const body = await res.json()
156
168
  // Handle null/undefined Providers from servers (both old and new may return empty arrays)
@@ -175,12 +187,12 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
175
187
  } finally {
176
188
  signal.clear()
177
189
  onFinish.resolve()
178
- log('getProviders finished: %c', cid)
190
+ this.log('getProviders finished: %c', cid)
179
191
  }
180
192
  }
181
193
 
182
194
  async * getPeers (peerId: PeerId, options: GetPeersOptions = {}): AsyncGenerator<PeerRecord> {
183
- log('getPeers starts: %c', peerId)
195
+ this.log('getPeers starts: %c', peerId)
184
196
 
185
197
  const timeoutSignal = AbortSignal.timeout(this.timeout)
186
198
  const signal = anySignal([this.shutDownController.signal, timeoutSignal, options.signal])
@@ -197,7 +209,7 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
197
209
  await onStart.promise
198
210
 
199
211
  // https://specs.ipfs.tech/routing/http-routing-v1/
200
- const url = new URL(`${this.clientUrl}routing/v1/peers/${peerId.toCID().toString()}`)
212
+ const url = new URL(`${this.url}routing/v1/peers/${peerId.toCID().toString()}`)
201
213
  this.#addFilterParams(url, options.filterAddrs, options.filterProtocols)
202
214
 
203
215
  const getOptions = { headers: { Accept: 'application/x-ndjson' }, signal }
@@ -241,16 +253,16 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
241
253
  }
242
254
  }
243
255
  } catch (err) {
244
- log.error('getPeers errored:', err)
256
+ this.log.error('getPeers errored - %e', err)
245
257
  } finally {
246
258
  signal.clear()
247
259
  onFinish.resolve()
248
- log('getPeers finished: %c', peerId)
260
+ this.log('getPeers finished: %c', peerId)
249
261
  }
250
262
  }
251
263
 
252
264
  async getIPNS (libp2pKey: CID<unknown, 0x72, 0x00 | 0x12, 1>, options: GetIPNSOptions = {}): Promise<IPNSRecord> {
253
- log('getIPNS starts: %s', libp2pKey)
265
+ this.log('getIPNS starts: %s', libp2pKey)
254
266
 
255
267
  const timeoutSignal = AbortSignal.timeout(this.timeout)
256
268
  const signal = anySignal([this.shutDownController.signal, timeoutSignal, options.signal])
@@ -264,7 +276,7 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
264
276
  })
265
277
 
266
278
  // https://specs.ipfs.tech/routing/http-routing-v1/
267
- const resource = `${this.clientUrl}routing/v1/ipns/${libp2pKey}`
279
+ const resource = `${this.url}routing/v1/ipns/${libp2pKey}`
268
280
 
269
281
  try {
270
282
  await onStart.promise
@@ -272,7 +284,7 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
272
284
  const getOptions = { headers: { Accept: 'application/vnd.ipfs.ipns-record' }, signal }
273
285
  const res = await this.#makeRequest(resource, getOptions)
274
286
 
275
- log('getIPNS GET %s %d', resource, res.status)
287
+ this.log('getIPNS GET %s %d', resource, res.status)
276
288
 
277
289
  // Per IPIP-0513: Handle 404 as "no record found" for backward compatibility
278
290
  // IPNS is different - we still throw NotFoundError for 404 (backward compat)
@@ -311,18 +323,18 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
311
323
 
312
324
  return unmarshalIPNSRecord(body)
313
325
  } catch (err: any) {
314
- log.error('getIPNS GET %s error:', resource, err)
326
+ this.log.error('getIPNS GET %s error - %e', resource, err)
315
327
 
316
328
  throw err
317
329
  } finally {
318
330
  signal.clear()
319
331
  onFinish.resolve()
320
- log('getIPNS finished: %s', libp2pKey)
332
+ this.log('getIPNS finished: %s', libp2pKey)
321
333
  }
322
334
  }
323
335
 
324
336
  async putIPNS (libp2pKey: CID<unknown, 0x72, 0x00 | 0x12, 1>, record: IPNSRecord, options: AbortOptions = {}): Promise<void> {
325
- log('putIPNS starts: %c', libp2pKey)
337
+ this.log('putIPNS starts: %c', libp2pKey)
326
338
 
327
339
  const timeoutSignal = AbortSignal.timeout(this.timeout)
328
340
  const signal = anySignal([this.shutDownController.signal, timeoutSignal, options.signal])
@@ -336,7 +348,7 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
336
348
  })
337
349
 
338
350
  // https://specs.ipfs.tech/routing/http-routing-v1/
339
- const resource = `${this.clientUrl}routing/v1/ipns/${libp2pKey}`
351
+ const resource = `${this.url}routing/v1/ipns/${libp2pKey}`
340
352
 
341
353
  try {
342
354
  await onStart.promise
@@ -346,19 +358,19 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
346
358
  const getOptions = { method: 'PUT', headers: { 'Content-Type': 'application/vnd.ipfs.ipns-record' }, body, signal }
347
359
  const res = await this.#makeRequest(resource, getOptions)
348
360
 
349
- log('putIPNS PUT %s %d', resource, res.status)
361
+ this.log('putIPNS PUT %s %d', resource, res.status)
350
362
 
351
363
  if (res.status !== 200) {
352
364
  throw new BadResponseError('PUT ipns response had status other than 200')
353
365
  }
354
366
  } catch (err: any) {
355
- log.error('putIPNS PUT %s error:', resource, err.stack)
367
+ this.log.error('putIPNS PUT %s error - %e', resource, err.stack)
356
368
 
357
369
  throw err
358
370
  } finally {
359
371
  signal.clear()
360
372
  onFinish.resolve()
361
- log('putIPNS finished: %c', libp2pKey)
373
+ this.log('putIPNS finished: %c', libp2pKey)
362
374
  }
363
375
  }
364
376
 
@@ -384,7 +396,7 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
384
396
  Protocols: protocols
385
397
  }
386
398
  } catch (err) {
387
- log.error('could not conform record to peer schema', err)
399
+ this.log.error('could not conform record to peer schema - %e', err)
388
400
  }
389
401
  }
390
402
 
@@ -416,16 +428,22 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
416
428
  // Only try to use cache for GET requests
417
429
  if (requestMethod === 'GET') {
418
430
  const cachedResponse = await this.cache?.match(url)
431
+
419
432
  if (cachedResponse != null) {
420
433
  // Check if the cached response has expired
421
434
  const expires = parseInt(cachedResponse.headers.get('x-cache-expires') ?? '0', 10)
422
435
  if (expires > Date.now()) {
423
- log('returning cached response for %s', key)
436
+ this.log('returning cached response for %s', key)
437
+ this.logResponse(cachedResponse)
438
+
424
439
  return cachedResponse
425
440
  } else {
441
+ this.log('evicting cached response for %s', key)
426
442
  // Remove expired response from cache
427
443
  await this.cache?.delete(url)
428
444
  }
445
+ } else if (this.cache != null) {
446
+ this.log('cache miss for %s', key)
429
447
  }
430
448
  }
431
449
 
@@ -433,12 +451,18 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
433
451
  const existingRequest = this.inFlightRequests.get(key)
434
452
  if (existingRequest != null) {
435
453
  const response = await existingRequest
436
- log('deduplicating outgoing request for %s', key)
454
+ this.log('deduplicating outgoing request for %s', key)
437
455
  return response.clone()
438
456
  }
439
457
 
458
+ this.log('outgoing request:')
459
+ this.logRequest(url, options)
460
+
440
461
  // Create new request and track it
441
462
  const requestPromise = fetch(url, options).then(async response => {
463
+ this.log('incoming response:')
464
+ this.logResponse(response)
465
+
442
466
  // Only cache successful GET requests
443
467
  if (this.cache != null && response.ok && requestMethod === 'GET') {
444
468
  const expires = Date.now() + this.cacheTTL
@@ -464,4 +488,25 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
464
488
  const response = await requestPromise
465
489
  return response
466
490
  }
491
+
492
+ toString (): string {
493
+ return `DefaultDelegatedRoutingV1HttpApiClient(${this.url})`
494
+ }
495
+
496
+ private logRequest (url: string, init: RequestInit): void {
497
+ const headers = new Headers(init.headers)
498
+ this.log('%s %s HTTP/1.1', init.method ?? 'GET', url)
499
+
500
+ for (const [key, value] of headers.entries()) {
501
+ this.log('%s: %s', key, value)
502
+ }
503
+ }
504
+
505
+ private logResponse (response: Response): void {
506
+ this.log('HTTP/1.1 %d %s', response.status, response.statusText)
507
+
508
+ for (const [key, value] of response.headers.entries()) {
509
+ this.log('%s: %s', key, value)
510
+ }
511
+ }
467
512
  }
package/src/index.ts CHANGED
@@ -83,8 +83,9 @@
83
83
  * ```
84
84
  */
85
85
 
86
- import { DefaultDelegatedRoutingV1HttpApiClient } from './client.js'
87
- import type { AbortOptions, PeerId } from '@libp2p/interface'
86
+ import { defaultLogger } from '@libp2p/logger'
87
+ import { DelegatedRoutingV1HttpApiClient as DelegatedRoutingV1HttpApiClientClass } from './client.js'
88
+ import type { AbortOptions, ComponentLogger, PeerId } from '@libp2p/interface'
88
89
  import type { Multiaddr } from '@multiformats/multiaddr'
89
90
  import type { IPNSRecord } from 'ipns'
90
91
  import type { CID } from 'multiformats/cid'
@@ -155,6 +156,10 @@ export interface DelegatedRoutingV1HttpApiClientInit extends FilterOptions {
155
156
  cacheName?: string
156
157
  }
157
158
 
159
+ export interface DelegatedRoutingV1HttpApiClientComponents {
160
+ logger: ComponentLogger
161
+ }
162
+
158
163
  export interface GetIPNSOptions extends AbortOptions {
159
164
  /**
160
165
  * By default incoming IPNS records are validated, pass false here to skip
@@ -169,6 +174,11 @@ export type GetProvidersOptions = FilterOptions & AbortOptions
169
174
  export type GetPeersOptions = FilterOptions & AbortOptions
170
175
 
171
176
  export interface DelegatedRoutingV1HttpApiClient {
177
+ /**
178
+ * The URL that requests are sent to
179
+ */
180
+ url: URL
181
+
172
182
  /**
173
183
  * Returns an async generator of {@link PeerRecord}s that can provide the
174
184
  * content for the passed {@link CID}
@@ -206,7 +216,23 @@ export interface DelegatedRoutingV1HttpApiClient {
206
216
 
207
217
  /**
208
218
  * Create and return a client to use with a Routing V1 HTTP API server
219
+ *
220
+ * @deprecated use `delegatedRoutingV1HttpApiClient` instead - this function will be removed in a future release
221
+ */
222
+ export function createDelegatedRoutingV1HttpApiClient (url: URL | string, init: Omit<DelegatedRoutingV1HttpApiClientInit, 'url'> = {}): DelegatedRoutingV1HttpApiClient {
223
+ return new DelegatedRoutingV1HttpApiClientClass({
224
+ logger: defaultLogger()
225
+ }, {
226
+ ...init,
227
+ url: new URL(url)
228
+ })
229
+ }
230
+
231
+ /**
232
+ * Create and return a client to use with a Routing V1 HTTP API server
233
+ *
234
+ * TODO: add `url` to `DelegatedRoutingV1HttpApiClientInit` interface and release as breaking change
209
235
  */
210
- export function createDelegatedRoutingV1HttpApiClient (url: URL | string, init: DelegatedRoutingV1HttpApiClientInit = {}): DelegatedRoutingV1HttpApiClient {
211
- return new DefaultDelegatedRoutingV1HttpApiClient(new URL(url), init)
236
+ export function delegatedRoutingV1HttpApiClient (init: DelegatedRoutingV1HttpApiClientInit & { url: string | URL }): (components: DelegatedRoutingV1HttpApiClientComponents) => DelegatedRoutingV1HttpApiClient {
237
+ return (components) => new DelegatedRoutingV1HttpApiClientClass(components, init)
212
238
  }
package/src/routings.ts CHANGED
@@ -86,6 +86,10 @@ export class DelegatedRoutingV1HttpApiClientContentRouting implements ContentRou
86
86
  throw err
87
87
  }
88
88
  }
89
+
90
+ toString (): string {
91
+ return `DelegatedRoutingV1HttpApiClientContentRouting(${this.client.url})`
92
+ }
89
93
  }
90
94
 
91
95
  /**
@@ -114,4 +118,8 @@ export class DelegatedRoutingV1HttpApiClientPeerRouting implements PeerRouting {
114
118
  async * getClosestPeers (key: Uint8Array, options: AbortOptions = {}): AsyncIterable<PeerInfo> {
115
119
  // noop
116
120
  }
121
+
122
+ toString (): string {
123
+ return `DelegatedRoutingV1HttpApiClientPeerRouting(${this.client.url})`
124
+ }
117
125
  }