@helia/delegated-routing-v1-http-api-client 0.0.0
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/LICENSE +4 -0
- package/README.md +50 -0
- package/dist/index.min.js +47 -0
- package/dist/src/client.d.ts +25 -0
- package/dist/src/client.d.ts.map +1 -0
- package/dist/src/client.js +231 -0
- package/dist/src/client.js.map +1 -0
- package/dist/src/index.d.ts +70 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +26 -0
- package/dist/src/index.js.map +1 -0
- package/package.json +151 -0
- package/src/client.ts +266 -0
- package/src/index.ts +82 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { CodeError } from '@libp2p/interface/errors'
|
|
2
|
+
import { logger } from '@libp2p/logger'
|
|
3
|
+
import { peerIdFromString } from '@libp2p/peer-id'
|
|
4
|
+
import { multiaddr } from '@multiformats/multiaddr'
|
|
5
|
+
import { anySignal } from 'any-signal'
|
|
6
|
+
import toIt from 'browser-readablestream-to-it'
|
|
7
|
+
import { unmarshal, type IPNSRecord, marshal, peerIdToRoutingKey } from 'ipns'
|
|
8
|
+
import { ipnsValidator } from 'ipns/validator'
|
|
9
|
+
// @ts-expect-error no types
|
|
10
|
+
import ndjson from 'iterable-ndjson'
|
|
11
|
+
import defer from 'p-defer'
|
|
12
|
+
import PQueue from 'p-queue'
|
|
13
|
+
import type { DelegatedRoutingV1HttpApiClient, DelegatedRoutingV1HttpApiClientInit, PeerRecord } from './index.js'
|
|
14
|
+
import type { AbortOptions } from '@libp2p/interface'
|
|
15
|
+
import type { PeerId } from '@libp2p/interface/peer-id'
|
|
16
|
+
import type { CID } from 'multiformats'
|
|
17
|
+
|
|
18
|
+
const log = logger('routing-v1-http-api-client')
|
|
19
|
+
|
|
20
|
+
const defaultValues = {
|
|
21
|
+
concurrentRequests: 4,
|
|
22
|
+
timeout: 30e3
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV1HttpApiClient {
|
|
26
|
+
private started: boolean
|
|
27
|
+
private readonly httpQueue: PQueue
|
|
28
|
+
private readonly shutDownController: AbortController
|
|
29
|
+
private readonly clientUrl: URL
|
|
30
|
+
private readonly timeout: number
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create a new DelegatedContentRouting instance
|
|
34
|
+
*/
|
|
35
|
+
constructor (url: string | URL, init: DelegatedRoutingV1HttpApiClientInit = {}) {
|
|
36
|
+
this.started = false
|
|
37
|
+
this.shutDownController = new AbortController()
|
|
38
|
+
this.httpQueue = new PQueue({
|
|
39
|
+
concurrency: init.concurrentRequests ?? defaultValues.concurrentRequests
|
|
40
|
+
})
|
|
41
|
+
this.clientUrl = url instanceof URL ? url : new URL(url)
|
|
42
|
+
this.timeout = init.timeout ?? defaultValues.timeout
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
isStarted (): boolean {
|
|
46
|
+
return this.started
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
start (): void {
|
|
50
|
+
this.started = true
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
stop (): void {
|
|
54
|
+
this.httpQueue.clear()
|
|
55
|
+
this.shutDownController.abort()
|
|
56
|
+
this.started = false
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async * getProviders (cid: CID, options: AbortOptions = {}): AsyncGenerator<PeerRecord, any, unknown> {
|
|
60
|
+
log('getProviders starts: %c', cid)
|
|
61
|
+
|
|
62
|
+
const signal = anySignal([this.shutDownController.signal, options.signal, AbortSignal.timeout(this.timeout)])
|
|
63
|
+
const onStart = defer()
|
|
64
|
+
const onFinish = defer()
|
|
65
|
+
|
|
66
|
+
void this.httpQueue.add(async () => {
|
|
67
|
+
onStart.resolve()
|
|
68
|
+
return onFinish.promise
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
await onStart.promise
|
|
73
|
+
|
|
74
|
+
// https://specs.ipfs.tech/routing/http-routing-v1/
|
|
75
|
+
const resource = `${this.clientUrl}routing/v1/providers/${cid.toString()}`
|
|
76
|
+
const getOptions = { headers: { Accept: 'application/x-ndjson' }, signal }
|
|
77
|
+
const res = await fetch(resource, getOptions)
|
|
78
|
+
|
|
79
|
+
if (res.body == null) {
|
|
80
|
+
throw new CodeError('Routing response had no body', 'ERR_BAD_RESPONSE')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const contentType = res.headers.get('Content-Type')
|
|
84
|
+
if (contentType === 'application/json') {
|
|
85
|
+
const body = await res.json()
|
|
86
|
+
|
|
87
|
+
for (const provider of body.Providers) {
|
|
88
|
+
const record = this.#handleProviderRecords(provider)
|
|
89
|
+
if (record != null) {
|
|
90
|
+
yield record
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
for await (const provider of ndjson(toIt(res.body))) {
|
|
95
|
+
const record = this.#handleProviderRecords(provider)
|
|
96
|
+
if (record != null) {
|
|
97
|
+
yield record
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} catch (err) {
|
|
102
|
+
log.error('getProviders errored:', err)
|
|
103
|
+
} finally {
|
|
104
|
+
signal.clear()
|
|
105
|
+
onFinish.resolve()
|
|
106
|
+
log('getProviders finished: %c', cid)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async * getPeerInfo (peerId: PeerId, options: AbortOptions | undefined = {}): AsyncGenerator<PeerRecord, any, unknown> {
|
|
111
|
+
log('getPeers starts: %c', peerId)
|
|
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/${peerId.toCID().toString()}`
|
|
127
|
+
const getOptions = { headers: { Accept: 'application/x-ndjson' }, signal }
|
|
128
|
+
const res = await fetch(resource, getOptions)
|
|
129
|
+
|
|
130
|
+
if (res.body == null) {
|
|
131
|
+
throw new CodeError('Routing response had no body', 'ERR_BAD_RESPONSE')
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const contentType = res.headers.get('Content-Type')
|
|
135
|
+
if (contentType === 'application/json') {
|
|
136
|
+
const body = await res.json()
|
|
137
|
+
|
|
138
|
+
for (const peer of body.Peers) {
|
|
139
|
+
const record = this.#handlePeerRecords(peerId, peer)
|
|
140
|
+
if (record != null) {
|
|
141
|
+
yield record
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
for await (const peer of ndjson(toIt(res.body))) {
|
|
146
|
+
const record = this.#handlePeerRecords(peerId, peer)
|
|
147
|
+
if (record != null) {
|
|
148
|
+
yield record
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} catch (err) {
|
|
153
|
+
log.error('getPeers errored:', err)
|
|
154
|
+
} finally {
|
|
155
|
+
signal.clear()
|
|
156
|
+
onFinish.resolve()
|
|
157
|
+
log('getPeers finished: %c', peerId)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async getIPNS (peerId: PeerId, options: AbortOptions = {}): Promise<IPNSRecord> {
|
|
162
|
+
log('getIPNS starts: %c', peerId)
|
|
163
|
+
|
|
164
|
+
const signal = anySignal([this.shutDownController.signal, options.signal, AbortSignal.timeout(this.timeout)])
|
|
165
|
+
const onStart = defer()
|
|
166
|
+
const onFinish = defer()
|
|
167
|
+
|
|
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/${peerId.toCID().toString()}`
|
|
178
|
+
const getOptions = { headers: { Accept: 'application/vnd.ipfs.ipns-record' }, signal }
|
|
179
|
+
const res = await fetch(resource, getOptions)
|
|
180
|
+
|
|
181
|
+
if (res.body == null) {
|
|
182
|
+
throw new CodeError('GET ipns response had no body', 'ERR_BAD_RESPONSE')
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const body = new Uint8Array(await res.arrayBuffer())
|
|
186
|
+
await ipnsValidator(peerIdToRoutingKey(peerId), body)
|
|
187
|
+
return unmarshal(body)
|
|
188
|
+
} finally {
|
|
189
|
+
signal.clear()
|
|
190
|
+
onFinish.resolve()
|
|
191
|
+
log('getIPNS finished: %c', peerId)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async putIPNS (peerId: PeerId, record: IPNSRecord, options: AbortOptions = {}): Promise<void> {
|
|
196
|
+
log('getIPNS starts: %c', peerId)
|
|
197
|
+
|
|
198
|
+
const signal = anySignal([this.shutDownController.signal, options.signal, AbortSignal.timeout(this.timeout)])
|
|
199
|
+
const onStart = defer()
|
|
200
|
+
const onFinish = defer()
|
|
201
|
+
|
|
202
|
+
void this.httpQueue.add(async () => {
|
|
203
|
+
onStart.resolve()
|
|
204
|
+
return onFinish.promise
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
await onStart.promise
|
|
209
|
+
|
|
210
|
+
const body = marshal(record)
|
|
211
|
+
|
|
212
|
+
// https://specs.ipfs.tech/routing/http-routing-v1/
|
|
213
|
+
const resource = `${this.clientUrl}routing/v1/ipns/${peerId.toCID().toString()}`
|
|
214
|
+
const getOptions = { method: 'PUT', headers: { 'Content-Type': 'application/vnd.ipfs.ipns-record' }, body, signal }
|
|
215
|
+
const res = await fetch(resource, getOptions)
|
|
216
|
+
if (res.status !== 200) {
|
|
217
|
+
throw new CodeError('PUT ipns response had status other than 200', 'ERR_BAD_RESPONSE')
|
|
218
|
+
}
|
|
219
|
+
} finally {
|
|
220
|
+
signal.clear()
|
|
221
|
+
onFinish.resolve()
|
|
222
|
+
log('getIPNS finished: %c', peerId)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
#handleProviderRecords (record: any): PeerRecord | undefined {
|
|
227
|
+
if (record.Schema === 'peer') {
|
|
228
|
+
// Peer schema can have additional, user-defined, fields.
|
|
229
|
+
record.ID = peerIdFromString(record.ID)
|
|
230
|
+
record.Addrs = record.Addrs.map(multiaddr)
|
|
231
|
+
return record
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (record.Schema === 'bitswap') {
|
|
235
|
+
// Bitswap schema is deprecated, was incorrectly used when server had no
|
|
236
|
+
// information about actual protocols, so we convert it to peer result
|
|
237
|
+
// without protocol information
|
|
238
|
+
return {
|
|
239
|
+
Schema: 'peer',
|
|
240
|
+
ID: peerIdFromString(record.ID),
|
|
241
|
+
Addrs: record.Addrs.map(multiaddr),
|
|
242
|
+
Protocols: record.Protocol != null ? [record.Protocol] : []
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (record.ID != null && Array.isArray(record.Addrs)) {
|
|
247
|
+
return {
|
|
248
|
+
Schema: 'peer',
|
|
249
|
+
ID: peerIdFromString(record.ID),
|
|
250
|
+
Addrs: record.Addrs.map(multiaddr),
|
|
251
|
+
Protocols: Array.isArray(record.Protocols) ? record.Protocols : []
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
#handlePeerRecords (peerId: PeerId, record: any): PeerRecord | undefined {
|
|
257
|
+
if (record.Schema === 'peer') {
|
|
258
|
+
// Peer schema can have additional, user-defined, fields.
|
|
259
|
+
record.ID = peerIdFromString(record.ID)
|
|
260
|
+
record.Addrs = record.Addrs.map(multiaddr)
|
|
261
|
+
if (peerId.equals(record.ID)) {
|
|
262
|
+
return record
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
*
|
|
4
|
+
* Create a client to use with a Routing V1 HTTP API server.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
*
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { createRoutingV1HttpApiClient } from '@helia/routing-v1-http-api-client'
|
|
10
|
+
* import { CID } from 'multiformats/cid'
|
|
11
|
+
*
|
|
12
|
+
* const client = createRoutingV1HttpApiClient(new URL('https://example.org'))
|
|
13
|
+
*
|
|
14
|
+
* for await (const prov of getProviders(CID.parse('QmFoo'))) {
|
|
15
|
+
* // ...
|
|
16
|
+
* }
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { DefaultDelegatedRoutingV1HttpApiClient } from './client.js'
|
|
21
|
+
import type { AbortOptions } from '@libp2p/interface'
|
|
22
|
+
import type { PeerId } from '@libp2p/interface/peer-id'
|
|
23
|
+
import type { Multiaddr } from '@multiformats/multiaddr'
|
|
24
|
+
import type { IPNSRecord } from 'ipns'
|
|
25
|
+
import type { CID } from 'multiformats/cid'
|
|
26
|
+
|
|
27
|
+
export interface PeerRecord {
|
|
28
|
+
Schema: 'peer'
|
|
29
|
+
ID: PeerId
|
|
30
|
+
Addrs?: Multiaddr[]
|
|
31
|
+
Protocols?: string[]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface DelegatedRoutingV1HttpApiClientInit {
|
|
35
|
+
/**
|
|
36
|
+
* A concurrency limit to avoid request flood in web browser (default: 4)
|
|
37
|
+
*
|
|
38
|
+
* @see https://github.com/libp2p/js-libp2p-delegated-content-routing/issues/12
|
|
39
|
+
*/
|
|
40
|
+
concurrentRequests?: number
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* How long a request is allowed to take in ms (default: 30 seconds)
|
|
44
|
+
*/
|
|
45
|
+
timeout?: number
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface DelegatedRoutingV1HttpApiClient {
|
|
49
|
+
/**
|
|
50
|
+
* Returns an async generator of PeerInfos that can provide the content
|
|
51
|
+
* for the passed CID
|
|
52
|
+
*/
|
|
53
|
+
getProviders(cid: CID, options?: AbortOptions): AsyncGenerator<PeerRecord>
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Returns an async generator of PeerInfos for the provided PeerId
|
|
57
|
+
*/
|
|
58
|
+
getPeerInfo(peerId: PeerId, options?: AbortOptions): AsyncGenerator<PeerRecord>
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Returns a promise of a IPNSRecord for the given PeerId
|
|
62
|
+
*/
|
|
63
|
+
getIPNS(peerId: PeerId, options?: AbortOptions): Promise<IPNSRecord>
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Publishes the given IPNSRecorded for the provided PeerId
|
|
67
|
+
*/
|
|
68
|
+
putIPNS(peerId: PeerId, record: IPNSRecord, options?: AbortOptions): Promise<void>
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Shut down any currently running HTTP requests and clear up any resources
|
|
72
|
+
* that are in use
|
|
73
|
+
*/
|
|
74
|
+
stop(): void
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Create and return a client to use with a Routing V1 HTTP API server
|
|
79
|
+
*/
|
|
80
|
+
export function createDelegatedRoutingV1HttpApiClient (url: URL, init: DelegatedRoutingV1HttpApiClientInit = {}): DelegatedRoutingV1HttpApiClient {
|
|
81
|
+
return new DefaultDelegatedRoutingV1HttpApiClient(url, init)
|
|
82
|
+
}
|