@helia/utils 0.1.0-9c8a2c0 → 0.1.0-9ea934e
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/dist/index.min.js +6 -1
- package/dist/src/abstract-session.d.ts +53 -0
- package/dist/src/abstract-session.d.ts.map +1 -0
- package/dist/src/abstract-session.js +205 -0
- package/dist/src/abstract-session.js.map +1 -0
- package/dist/src/bloom-filter.d.ts +33 -0
- package/dist/src/bloom-filter.d.ts.map +1 -0
- package/dist/src/bloom-filter.js +113 -0
- package/dist/src/bloom-filter.js.map +1 -0
- package/dist/src/index.d.ts +19 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/routing.d.ts +4 -1
- package/dist/src/routing.d.ts.map +1 -1
- package/dist/src/routing.js +47 -2
- package/dist/src/routing.js.map +1 -1
- package/dist/src/storage.d.ts +2 -2
- package/dist/src/storage.d.ts.map +1 -1
- package/dist/src/storage.js +12 -13
- package/dist/src/storage.js.map +1 -1
- package/dist/src/utils/networked-storage.d.ts +28 -23
- package/dist/src/utils/networked-storage.d.ts.map +1 -1
- package/dist/src/utils/networked-storage.js +180 -32
- package/dist/src/utils/networked-storage.js.map +1 -1
- package/package.json +9 -2
- package/src/abstract-session.ts +287 -0
- package/src/bloom-filter.ts +141 -0
- package/src/index.ts +23 -1
- package/src/routing.ts +55 -1
- package/src/storage.ts +13 -16
- package/src/utils/networked-storage.ts +214 -47
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { DEFAULT_SESSION_MIN_PROVIDERS, DEFAULT_SESSION_MAX_PROVIDERS } from '@helia/interface'
|
|
2
|
+
import { CodeError, TypedEventEmitter, setMaxListeners } from '@libp2p/interface'
|
|
3
|
+
import { Queue } from '@libp2p/utils/queue'
|
|
4
|
+
import { base64 } from 'multiformats/bases/base64'
|
|
5
|
+
import pDefer from 'p-defer'
|
|
6
|
+
import { BloomFilter } from './bloom-filter.js'
|
|
7
|
+
import type { BlockBroker, BlockRetrievalOptions, CreateSessionOptions } from '@helia/interface'
|
|
8
|
+
import type { AbortOptions, ComponentLogger, Logger } from '@libp2p/interface'
|
|
9
|
+
import type { CID } from 'multiformats/cid'
|
|
10
|
+
import type { DeferredPromise } from 'p-defer'
|
|
11
|
+
import type { ProgressEvent } from 'progress-events'
|
|
12
|
+
|
|
13
|
+
export interface AbstractSessionComponents {
|
|
14
|
+
logger: ComponentLogger
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AbstractCreateSessionOptions extends CreateSessionOptions {
|
|
18
|
+
name: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface BlockstoreSessionEvents<Provider> {
|
|
22
|
+
provider: CustomEvent<Provider>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export abstract class AbstractSession<Provider, RetrieveBlockProgressEvents extends ProgressEvent> extends TypedEventEmitter<BlockstoreSessionEvents<Provider>> implements BlockBroker<RetrieveBlockProgressEvents> {
|
|
26
|
+
private intialPeerSearchComplete?: Promise<void>
|
|
27
|
+
private readonly requests: Map<string, Promise<Uint8Array>>
|
|
28
|
+
private readonly name: string
|
|
29
|
+
protected log: Logger
|
|
30
|
+
protected logger: ComponentLogger
|
|
31
|
+
private readonly minProviders: number
|
|
32
|
+
private readonly maxProviders: number
|
|
33
|
+
public readonly providers: Provider[]
|
|
34
|
+
private readonly evictionFilter: BloomFilter
|
|
35
|
+
|
|
36
|
+
constructor (components: AbstractSessionComponents, init: AbstractCreateSessionOptions) {
|
|
37
|
+
super()
|
|
38
|
+
|
|
39
|
+
setMaxListeners(Infinity, this)
|
|
40
|
+
this.name = init.name
|
|
41
|
+
this.logger = components.logger
|
|
42
|
+
this.log = components.logger.forComponent(this.name)
|
|
43
|
+
this.requests = new Map()
|
|
44
|
+
this.minProviders = init.minProviders ?? DEFAULT_SESSION_MIN_PROVIDERS
|
|
45
|
+
this.maxProviders = init.maxProviders ?? DEFAULT_SESSION_MAX_PROVIDERS
|
|
46
|
+
this.providers = []
|
|
47
|
+
this.evictionFilter = BloomFilter.create(this.maxProviders)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async retrieve (cid: CID, options: BlockRetrievalOptions<RetrieveBlockProgressEvents> = {}): Promise<Uint8Array> {
|
|
51
|
+
// see if we are already requesting this CID in this session
|
|
52
|
+
const cidStr = base64.encode(cid.multihash.bytes)
|
|
53
|
+
const existingJob = this.requests.get(cidStr)
|
|
54
|
+
|
|
55
|
+
if (existingJob != null) {
|
|
56
|
+
this.log('join existing request for %c', cid)
|
|
57
|
+
return existingJob
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const deferred: DeferredPromise<Uint8Array> = pDefer()
|
|
61
|
+
this.requests.set(cidStr, deferred.promise)
|
|
62
|
+
|
|
63
|
+
if (this.providers.length === 0) {
|
|
64
|
+
let first = false
|
|
65
|
+
|
|
66
|
+
if (this.intialPeerSearchComplete == null) {
|
|
67
|
+
first = true
|
|
68
|
+
this.log = this.logger.forComponent(`${this.name}:${cid}`)
|
|
69
|
+
this.intialPeerSearchComplete = this.findProviders(cid, this.minProviders, options)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
await this.intialPeerSearchComplete
|
|
73
|
+
|
|
74
|
+
if (first) {
|
|
75
|
+
this.log('found initial session peers for %c', cid)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let foundBlock = false
|
|
80
|
+
|
|
81
|
+
// this queue manages outgoing requests - as new peers are added to the
|
|
82
|
+
// session they will be added to the queue so we can request the current
|
|
83
|
+
// block from multiple peers as they are discovered
|
|
84
|
+
const queue = new Queue<Uint8Array, { provider: Provider, priority?: number }>({
|
|
85
|
+
concurrency: this.maxProviders
|
|
86
|
+
})
|
|
87
|
+
queue.addEventListener('error', () => {})
|
|
88
|
+
queue.addEventListener('failure', (evt) => {
|
|
89
|
+
this.log.error('error querying provider %o, evicting from session', evt.detail.job.options.provider, evt.detail.error)
|
|
90
|
+
this.evict(evt.detail.job.options.provider)
|
|
91
|
+
})
|
|
92
|
+
queue.addEventListener('success', (evt) => {
|
|
93
|
+
// peer has sent block, return it to the caller
|
|
94
|
+
foundBlock = true
|
|
95
|
+
deferred.resolve(evt.detail.result)
|
|
96
|
+
})
|
|
97
|
+
queue.addEventListener('idle', () => {
|
|
98
|
+
if (foundBlock || options.signal?.aborted === true) {
|
|
99
|
+
// we either found the block or the user gave up
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// find more session peers and retry
|
|
104
|
+
Promise.resolve()
|
|
105
|
+
.then(async () => {
|
|
106
|
+
this.log('no session peers had block for for %c, finding new providers', cid)
|
|
107
|
+
|
|
108
|
+
// evict this.minProviders random providers to make room for more
|
|
109
|
+
for (let i = 0; i < this.minProviders; i++) {
|
|
110
|
+
if (this.providers.length === 0) {
|
|
111
|
+
break
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const provider = this.providers[Math.floor(Math.random() * this.providers.length)]
|
|
115
|
+
this.evict(provider)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// find new providers for the CID
|
|
119
|
+
await this.findProviders(cid, this.minProviders, options)
|
|
120
|
+
|
|
121
|
+
// keep trying until the abort signal fires
|
|
122
|
+
this.log('found new providers re-retrieving %c', cid)
|
|
123
|
+
this.requests.delete(cidStr)
|
|
124
|
+
deferred.resolve(await this.retrieve(cid, options))
|
|
125
|
+
})
|
|
126
|
+
.catch(err => {
|
|
127
|
+
this.log.error('could not find new providers for %c', cid, err)
|
|
128
|
+
deferred.reject(err)
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
const peerAddedToSessionListener = (event: CustomEvent<Provider>): void => {
|
|
133
|
+
queue.add(async () => {
|
|
134
|
+
return this.queryProvider(cid, event.detail, options)
|
|
135
|
+
}, {
|
|
136
|
+
provider: event.detail
|
|
137
|
+
})
|
|
138
|
+
.catch(err => {
|
|
139
|
+
if (options.signal?.aborted === true) {
|
|
140
|
+
// skip logging error if signal was aborted because abort can happen
|
|
141
|
+
// on success (e.g. another session found the block)
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
this.log.error('error retrieving session block for %c', cid, err)
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// add new session peers to query as they are discovered
|
|
150
|
+
this.addEventListener('provider', peerAddedToSessionListener)
|
|
151
|
+
|
|
152
|
+
// query each session peer directly
|
|
153
|
+
Promise.all([...this.providers].map(async (provider) => {
|
|
154
|
+
return queue.add(async () => {
|
|
155
|
+
return this.queryProvider(cid, provider, options)
|
|
156
|
+
}, {
|
|
157
|
+
provider
|
|
158
|
+
})
|
|
159
|
+
}))
|
|
160
|
+
.catch(err => {
|
|
161
|
+
if (options.signal?.aborted === true) {
|
|
162
|
+
// skip logging error if signal was aborted because abort can happen
|
|
163
|
+
// on success (e.g. another session found the block)
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
this.log.error('error retrieving session block for %c', cid, err)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
return await deferred.promise
|
|
172
|
+
} finally {
|
|
173
|
+
this.removeEventListener('provider', peerAddedToSessionListener)
|
|
174
|
+
queue.clear()
|
|
175
|
+
this.requests.delete(cidStr)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
evict (provider: Provider): void {
|
|
180
|
+
this.evictionFilter.add(this.toEvictionKey(provider))
|
|
181
|
+
const index = this.providers.findIndex(prov => this.equals(prov, provider))
|
|
182
|
+
|
|
183
|
+
if (index === -1) {
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.providers.splice(index, 1)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
isEvicted (provider: Provider): boolean {
|
|
191
|
+
return this.providers.some(prov => this.equals(prov, provider))
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
hasProvider (provider: Provider): boolean {
|
|
195
|
+
// dedupe existing gateways
|
|
196
|
+
if (this.providers.find(prov => this.equals(prov, provider)) != null) {
|
|
197
|
+
return true
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// dedupe failed session peers
|
|
201
|
+
if (this.isEvicted(provider)) {
|
|
202
|
+
return true
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return false
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private async findProviders (cid: CID, count: number, options: AbortOptions): Promise<void> {
|
|
209
|
+
const deferred: DeferredPromise<void> = pDefer()
|
|
210
|
+
let found = 0
|
|
211
|
+
|
|
212
|
+
// run async to resolve the deferred promise when `count` providers are
|
|
213
|
+
// found but continue util this.providers reaches this.maxProviders
|
|
214
|
+
void Promise.resolve()
|
|
215
|
+
.then(async () => {
|
|
216
|
+
this.log('finding %d-%d new provider(s) for %c', count, this.maxProviders, cid)
|
|
217
|
+
|
|
218
|
+
for await (const provider of this.findNewProviders(cid, options)) {
|
|
219
|
+
if (found === this.maxProviders || options.signal?.aborted === true) {
|
|
220
|
+
break
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (this.hasProvider(provider)) {
|
|
224
|
+
continue
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
this.log('found %d/%d new providers', found, this.maxProviders)
|
|
228
|
+
this.providers.push(provider)
|
|
229
|
+
|
|
230
|
+
// let the new peer join current queries
|
|
231
|
+
this.safeDispatchEvent('provider', {
|
|
232
|
+
detail: provider
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
found++
|
|
236
|
+
|
|
237
|
+
if (found === count) {
|
|
238
|
+
this.log('session is ready')
|
|
239
|
+
deferred.resolve()
|
|
240
|
+
// continue finding peers until we reach this.maxProviders
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (this.providers.length === this.maxProviders) {
|
|
244
|
+
this.log('found max session peers', found)
|
|
245
|
+
break
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
this.log('found %d/%d new session peers', found, this.maxProviders)
|
|
250
|
+
|
|
251
|
+
if (found < count) {
|
|
252
|
+
throw new CodeError(`Found ${found} of ${count} ${this.name} providers for ${cid}`, 'ERR_INSUFFICIENT_PROVIDERS_FOUND')
|
|
253
|
+
}
|
|
254
|
+
})
|
|
255
|
+
.catch(err => {
|
|
256
|
+
this.log.error('error searching routing for potential session peers for %c', cid, err.errors ?? err)
|
|
257
|
+
deferred.reject(err)
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
return deferred.promise
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* This method should search for new providers and yield them.
|
|
265
|
+
*/
|
|
266
|
+
abstract findNewProviders (cid: CID, options: AbortOptions): AsyncGenerator<Provider>
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* The subclass should contact the provider and request the block from it.
|
|
270
|
+
*
|
|
271
|
+
* If the provider cannot provide the block an error should be thrown.
|
|
272
|
+
*
|
|
273
|
+
* The provider will then be excluded from ongoing queries.
|
|
274
|
+
*/
|
|
275
|
+
abstract queryProvider (cid: CID, provider: Provider, options: AbortOptions): Promise<Uint8Array>
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Turn a provider into a concise Uint8Array representation for use in a Bloom
|
|
279
|
+
* filter
|
|
280
|
+
*/
|
|
281
|
+
abstract toEvictionKey (provider: Provider): Uint8Array | string
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Return `true` if we consider one provider to be the same as another
|
|
285
|
+
*/
|
|
286
|
+
abstract equals (providerA: Provider, providerB: Provider): boolean
|
|
287
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// ported from xxbloom - https://github.com/ceejbot/xxbloom/blob/master/LICENSE
|
|
2
|
+
import { randomBytes } from '@libp2p/crypto'
|
|
3
|
+
import mur from 'murmurhash3js-revisited'
|
|
4
|
+
import { Uint8ArrayList } from 'uint8arraylist'
|
|
5
|
+
import { alloc } from 'uint8arrays/alloc'
|
|
6
|
+
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
|
|
7
|
+
|
|
8
|
+
const LN2_SQUARED = Math.LN2 * Math.LN2
|
|
9
|
+
|
|
10
|
+
export interface BloomFilterOptions {
|
|
11
|
+
seeds?: number[]
|
|
12
|
+
hashes?: number
|
|
13
|
+
bits?: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class BloomFilter {
|
|
17
|
+
/**
|
|
18
|
+
* Create a `BloomFilter` with the smallest `bits` and `hashes` value for the
|
|
19
|
+
* specified item count and error rate.
|
|
20
|
+
*/
|
|
21
|
+
static create (itemcount: number, errorRate: number = 0.005): BloomFilter {
|
|
22
|
+
const opts = optimize(itemcount, errorRate)
|
|
23
|
+
return new BloomFilter(opts)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public readonly seeds: number[]
|
|
27
|
+
public readonly bits: number
|
|
28
|
+
public buffer: Uint8Array
|
|
29
|
+
|
|
30
|
+
constructor (options: BloomFilterOptions = {}) {
|
|
31
|
+
if (options.seeds != null) {
|
|
32
|
+
this.seeds = options.seeds
|
|
33
|
+
} else {
|
|
34
|
+
this.seeds = generateSeeds(options.hashes ?? 8)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.bits = options.bits ?? 1024
|
|
38
|
+
this.buffer = alloc(Math.ceil(this.bits / 8))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Add an item to the filter
|
|
43
|
+
*/
|
|
44
|
+
add (item: Uint8Array | string): void {
|
|
45
|
+
if (typeof item === 'string') {
|
|
46
|
+
item = uint8ArrayFromString(item)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (let i = 0; i < this.seeds.length; i++) {
|
|
50
|
+
const hash = mur.x86.hash32(item, this.seeds[i])
|
|
51
|
+
const bit = hash % this.bits
|
|
52
|
+
|
|
53
|
+
this.setbit(bit)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Test if the filter has an item. If it returns false it definitely does not
|
|
59
|
+
* have the item. If it returns true, it probably has the item but there's
|
|
60
|
+
* an `errorRate` chance it doesn't.
|
|
61
|
+
*/
|
|
62
|
+
has (item: Uint8Array | string): boolean {
|
|
63
|
+
if (typeof item === 'string') {
|
|
64
|
+
item = uint8ArrayFromString(item)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (let i = 0; i < this.seeds.length; i++) {
|
|
68
|
+
const hash = mur.x86.hash32(item, this.seeds[i])
|
|
69
|
+
const bit = hash % this.bits
|
|
70
|
+
|
|
71
|
+
const isSet = this.getbit(bit)
|
|
72
|
+
|
|
73
|
+
if (!isSet) {
|
|
74
|
+
return false
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return true
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Reset the filter
|
|
83
|
+
*/
|
|
84
|
+
clear (): void {
|
|
85
|
+
this.buffer.fill(0)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
setbit (bit: number): void {
|
|
89
|
+
let pos = 0
|
|
90
|
+
let shift = bit
|
|
91
|
+
while (shift > 7) {
|
|
92
|
+
pos++
|
|
93
|
+
shift -= 8
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let bitfield = this.buffer[pos]
|
|
97
|
+
bitfield |= (0x1 << shift)
|
|
98
|
+
this.buffer[pos] = bitfield
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
getbit (bit: number): boolean {
|
|
102
|
+
let pos = 0
|
|
103
|
+
let shift = bit
|
|
104
|
+
while (shift > 7) {
|
|
105
|
+
pos++
|
|
106
|
+
shift -= 8
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const bitfield = this.buffer[pos]
|
|
110
|
+
return (bitfield & (0x1 << shift)) !== 0
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function optimize (itemcount: number, errorRate: number = 0.005): { bits: number, hashes: number } {
|
|
115
|
+
const bits = Math.round(-1 * itemcount * Math.log(errorRate) / LN2_SQUARED)
|
|
116
|
+
const hashes = Math.round((bits / itemcount) * Math.LN2)
|
|
117
|
+
|
|
118
|
+
return { bits, hashes }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function generateSeeds (count: number): number[] {
|
|
122
|
+
let buf: Uint8ArrayList
|
|
123
|
+
let j: number
|
|
124
|
+
const seeds = []
|
|
125
|
+
|
|
126
|
+
for (let i = 0; i < count; i++) {
|
|
127
|
+
buf = new Uint8ArrayList(randomBytes(4))
|
|
128
|
+
seeds[i] = buf.getUint32(0, true)
|
|
129
|
+
|
|
130
|
+
// Make sure we don't end up with two identical seeds,
|
|
131
|
+
// which is unlikely but possible.
|
|
132
|
+
for (j = 0; j < i; j++) {
|
|
133
|
+
if (seeds[i] === seeds[j]) {
|
|
134
|
+
i--
|
|
135
|
+
break
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return seeds
|
|
141
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -39,6 +39,9 @@ import type { Datastore } from 'interface-datastore'
|
|
|
39
39
|
import type { CID } from 'multiformats/cid'
|
|
40
40
|
import type { MultihashHasher } from 'multiformats/hashes/interface'
|
|
41
41
|
|
|
42
|
+
export { AbstractSession, type AbstractCreateSessionOptions } from './abstract-session.js'
|
|
43
|
+
export { BloomFilter } from './bloom-filter.js'
|
|
44
|
+
|
|
42
45
|
/**
|
|
43
46
|
* Options used to create a Helia node.
|
|
44
47
|
*/
|
|
@@ -101,6 +104,24 @@ export interface HeliaInit {
|
|
|
101
104
|
*/
|
|
102
105
|
routers?: Array<Partial<Routing>>
|
|
103
106
|
|
|
107
|
+
/**
|
|
108
|
+
* During provider lookups, peers can be returned from routing implementations
|
|
109
|
+
* with no multiaddrs.
|
|
110
|
+
*
|
|
111
|
+
* This can happen when they've been retrieved from network peers that only
|
|
112
|
+
* store multiaddrs for a limited amount of time.
|
|
113
|
+
*
|
|
114
|
+
* When this happens the peer's info has to be looked up with a further query.
|
|
115
|
+
*
|
|
116
|
+
* To not have this query block the yielding of other providers returned with
|
|
117
|
+
* multiaddrs, a separate queue is used to perform this lookup.
|
|
118
|
+
*
|
|
119
|
+
* This config value controls the concurrency of that queue.
|
|
120
|
+
*
|
|
121
|
+
* @default 5
|
|
122
|
+
*/
|
|
123
|
+
providerLookupConcurrency?: number
|
|
124
|
+
|
|
104
125
|
/**
|
|
105
126
|
* Components used by subclasses
|
|
106
127
|
*/
|
|
@@ -171,7 +192,8 @@ export class Helia implements HeliaInterface {
|
|
|
171
192
|
}
|
|
172
193
|
|
|
173
194
|
return routers
|
|
174
|
-
})
|
|
195
|
+
}),
|
|
196
|
+
providerLookupConcurrency: init.providerLookupConcurrency
|
|
175
197
|
})
|
|
176
198
|
|
|
177
199
|
const networkedStorage = new NetworkedStorage(components)
|
package/src/routing.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { CodeError, start, stop } from '@libp2p/interface'
|
|
2
|
+
import { PeerQueue } from '@libp2p/utils/peer-queue'
|
|
2
3
|
import merge from 'it-merge'
|
|
3
4
|
import type { Routing as RoutingInterface, Provider, RoutingOptions } from '@helia/interface'
|
|
4
5
|
import type { AbortOptions, ComponentLogger, Logger, PeerId, PeerInfo, Startable } from '@libp2p/interface'
|
|
5
6
|
import type { CID } from 'multiformats/cid'
|
|
6
7
|
|
|
8
|
+
const DEFAULT_PROVIDER_LOOKUP_CONCURRENCY = 5
|
|
9
|
+
|
|
7
10
|
export interface RoutingInit {
|
|
8
11
|
routers: Array<Partial<RoutingInterface>>
|
|
12
|
+
providerLookupConcurrency?: number
|
|
9
13
|
}
|
|
10
14
|
|
|
11
15
|
export interface RoutingComponents {
|
|
@@ -15,10 +19,12 @@ export interface RoutingComponents {
|
|
|
15
19
|
export class Routing implements RoutingInterface, Startable {
|
|
16
20
|
private readonly log: Logger
|
|
17
21
|
private readonly routers: Array<Partial<RoutingInterface>>
|
|
22
|
+
private readonly providerLookupConcurrency: number
|
|
18
23
|
|
|
19
24
|
constructor (components: RoutingComponents, init: RoutingInit) {
|
|
20
25
|
this.log = components.logger.forComponent('helia:routing')
|
|
21
26
|
this.routers = init.routers ?? []
|
|
27
|
+
this.providerLookupConcurrency = init.providerLookupConcurrency ?? DEFAULT_PROVIDER_LOOKUP_CONCURRENCY
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
async start (): Promise<void> {
|
|
@@ -30,14 +36,25 @@ export class Routing implements RoutingInterface, Startable {
|
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
/**
|
|
33
|
-
* Iterates over all content routers in parallel to find providers of the
|
|
39
|
+
* Iterates over all content routers in parallel to find providers of the
|
|
40
|
+
* given key
|
|
34
41
|
*/
|
|
35
42
|
async * findProviders (key: CID, options: RoutingOptions = {}): AsyncIterable<Provider> {
|
|
36
43
|
if (this.routers.length === 0) {
|
|
37
44
|
throw new CodeError('No content routers available', 'ERR_NO_ROUTERS_AVAILABLE')
|
|
38
45
|
}
|
|
39
46
|
|
|
47
|
+
// provider multiaddrs are only cached for a limited time, so they can come
|
|
48
|
+
// back as an empty array - when this happens we have to do a FIND_PEER
|
|
49
|
+
// query to get updated addresses, but we shouldn't block on this so use a
|
|
50
|
+
// separate bounded queue to perform this lookup
|
|
51
|
+
const queue = new PeerQueue<Provider | null>({
|
|
52
|
+
concurrency: this.providerLookupConcurrency
|
|
53
|
+
})
|
|
54
|
+
queue.addEventListener('error', () => {})
|
|
55
|
+
|
|
40
56
|
for await (const peer of merge(
|
|
57
|
+
queue.toGenerator(),
|
|
41
58
|
...supports(this.routers, 'findProviders')
|
|
42
59
|
.map(router => router.findProviders(key, options))
|
|
43
60
|
)) {
|
|
@@ -47,6 +64,43 @@ export class Routing implements RoutingInterface, Startable {
|
|
|
47
64
|
continue
|
|
48
65
|
}
|
|
49
66
|
|
|
67
|
+
peer.multiaddrs = peer.multiaddrs.map(ma => {
|
|
68
|
+
if (ma.getPeerId() != null) {
|
|
69
|
+
return ma
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return ma.encapsulate(`/p2p/${peer.id}`)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// have to refresh peer info for this peer to get updated multiaddrs
|
|
76
|
+
if (peer.multiaddrs.length === 0) {
|
|
77
|
+
// already looking this peer up
|
|
78
|
+
if (queue.find(peer.id) != null) {
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
queue.add(async () => {
|
|
83
|
+
try {
|
|
84
|
+
const provider = await this.findPeer(peer.id, options)
|
|
85
|
+
|
|
86
|
+
if (provider.multiaddrs.length === 0) {
|
|
87
|
+
return null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return provider
|
|
91
|
+
} catch (err) {
|
|
92
|
+
this.log.error('could not load multiaddrs for peer', peer.id, err)
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
95
|
+
}, {
|
|
96
|
+
peerId: peer.id,
|
|
97
|
+
signal: options.signal
|
|
98
|
+
})
|
|
99
|
+
.catch(err => {
|
|
100
|
+
this.log.error('could not load multiaddrs for peer', peer.id, err)
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
50
104
|
yield peer
|
|
51
105
|
}
|
|
52
106
|
}
|
package/src/storage.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { start, stop } from '@libp2p/interface'
|
|
2
2
|
import createMortice from 'mortice'
|
|
3
|
-
import type { Blocks, Pair, DeleteManyBlocksProgressEvents, DeleteBlockProgressEvents, GetBlockProgressEvents, GetManyBlocksProgressEvents, PutManyBlocksProgressEvents, PutBlockProgressEvents, GetAllBlocksProgressEvents, GetOfflineOptions } from '@helia/interface/blocks'
|
|
3
|
+
import type { Blocks, Pair, DeleteManyBlocksProgressEvents, DeleteBlockProgressEvents, GetBlockProgressEvents, GetManyBlocksProgressEvents, PutManyBlocksProgressEvents, PutBlockProgressEvents, GetAllBlocksProgressEvents, GetOfflineOptions, SessionBlockstore } from '@helia/interface/blocks'
|
|
4
4
|
import type { Pins } from '@helia/interface/pins'
|
|
5
5
|
import type { AbortOptions, Startable } from '@libp2p/interface'
|
|
6
6
|
import type { Blockstore } from 'interface-blockstore'
|
|
@@ -62,6 +62,7 @@ export class BlockStorage implements Blocks, Startable {
|
|
|
62
62
|
* Put a block to the underlying datastore
|
|
63
63
|
*/
|
|
64
64
|
async put (cid: CID, block: Uint8Array, options: AbortOptions & ProgressOptions<PutBlockProgressEvents> = {}): Promise<CID> {
|
|
65
|
+
options?.signal?.throwIfAborted()
|
|
65
66
|
const releaseLock = await this.lock.readLock()
|
|
66
67
|
|
|
67
68
|
try {
|
|
@@ -75,6 +76,7 @@ export class BlockStorage implements Blocks, Startable {
|
|
|
75
76
|
* Put a multiple blocks to the underlying datastore
|
|
76
77
|
*/
|
|
77
78
|
async * putMany (blocks: AwaitIterable<{ cid: CID, block: Uint8Array }>, options: AbortOptions & ProgressOptions<PutManyBlocksProgressEvents> = {}): AsyncIterable<CID> {
|
|
79
|
+
options?.signal?.throwIfAborted()
|
|
78
80
|
const releaseLock = await this.lock.readLock()
|
|
79
81
|
|
|
80
82
|
try {
|
|
@@ -88,6 +90,7 @@ export class BlockStorage implements Blocks, Startable {
|
|
|
88
90
|
* Get a block by cid
|
|
89
91
|
*/
|
|
90
92
|
async get (cid: CID, options: GetOfflineOptions & AbortOptions & ProgressOptions<GetBlockProgressEvents> = {}): Promise<Uint8Array> {
|
|
93
|
+
options?.signal?.throwIfAborted()
|
|
91
94
|
const releaseLock = await this.lock.readLock()
|
|
92
95
|
|
|
93
96
|
try {
|
|
@@ -101,6 +104,7 @@ export class BlockStorage implements Blocks, Startable {
|
|
|
101
104
|
* Get multiple blocks back from an (async) iterable of cids
|
|
102
105
|
*/
|
|
103
106
|
async * getMany (cids: AwaitIterable<CID>, options: GetOfflineOptions & AbortOptions & ProgressOptions<GetManyBlocksProgressEvents> = {}): AsyncIterable<Pair> {
|
|
107
|
+
options?.signal?.throwIfAborted()
|
|
104
108
|
const releaseLock = await this.lock.readLock()
|
|
105
109
|
|
|
106
110
|
try {
|
|
@@ -114,6 +118,7 @@ export class BlockStorage implements Blocks, Startable {
|
|
|
114
118
|
* Delete a block from the blockstore
|
|
115
119
|
*/
|
|
116
120
|
async delete (cid: CID, options: AbortOptions & ProgressOptions<DeleteBlockProgressEvents> = {}): Promise<void> {
|
|
121
|
+
options?.signal?.throwIfAborted()
|
|
117
122
|
const releaseLock = await this.lock.writeLock()
|
|
118
123
|
|
|
119
124
|
try {
|
|
@@ -131,6 +136,7 @@ export class BlockStorage implements Blocks, Startable {
|
|
|
131
136
|
* Delete multiple blocks from the blockstore
|
|
132
137
|
*/
|
|
133
138
|
async * deleteMany (cids: AwaitIterable<CID>, options: AbortOptions & ProgressOptions<DeleteManyBlocksProgressEvents> = {}): AsyncIterable<CID> {
|
|
139
|
+
options?.signal?.throwIfAborted()
|
|
134
140
|
const releaseLock = await this.lock.writeLock()
|
|
135
141
|
|
|
136
142
|
try {
|
|
@@ -151,6 +157,7 @@ export class BlockStorage implements Blocks, Startable {
|
|
|
151
157
|
}
|
|
152
158
|
|
|
153
159
|
async has (cid: CID, options: AbortOptions = {}): Promise<boolean> {
|
|
160
|
+
options?.signal?.throwIfAborted()
|
|
154
161
|
const releaseLock = await this.lock.readLock()
|
|
155
162
|
|
|
156
163
|
try {
|
|
@@ -161,6 +168,7 @@ export class BlockStorage implements Blocks, Startable {
|
|
|
161
168
|
}
|
|
162
169
|
|
|
163
170
|
async * getAll (options: AbortOptions & ProgressOptions<GetAllBlocksProgressEvents> = {}): AsyncIterable<Pair> {
|
|
171
|
+
options?.signal?.throwIfAborted()
|
|
164
172
|
const releaseLock = await this.lock.readLock()
|
|
165
173
|
|
|
166
174
|
try {
|
|
@@ -170,19 +178,8 @@ export class BlockStorage implements Blocks, Startable {
|
|
|
170
178
|
}
|
|
171
179
|
}
|
|
172
180
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
try {
|
|
177
|
-
const blocks = await this.child.createSession(root, options)
|
|
178
|
-
|
|
179
|
-
if (blocks == null) {
|
|
180
|
-
throw new CodeError('Sessions not supported', 'ERR_UNSUPPORTED')
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return blocks
|
|
184
|
-
} finally {
|
|
185
|
-
releaseLock()
|
|
186
|
-
}
|
|
181
|
+
createSession (root: CID, options?: AbortOptions): SessionBlockstore {
|
|
182
|
+
options?.signal?.throwIfAborted()
|
|
183
|
+
return this.child.createSession(root, options)
|
|
187
184
|
}
|
|
188
185
|
}
|