@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.
@@ -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 given key
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 { CodeError, start, stop } from '@libp2p/interface'
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
- async createSession (root: CID, options?: AbortOptions): Promise<Blockstore> {
174
- const releaseLock = await this.lock.readLock()
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
  }