Skip to content

Commit 44e6a06

Browse files
author
Alan Shaw
authored
feat: abortable requests (#2027)
1 parent 1cd0421 commit 44e6a06

3 files changed

Lines changed: 183 additions & 35 deletions

File tree

packages/client/src/lib.js

Lines changed: 60 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const RATE_LIMIT_PERIOD = 10 * 1000
4242
* @typedef {import('./lib/interface.js').CarReader} CarReader
4343
* @typedef {import('ipfs-car/blockstore').Blockstore} BlockstoreI
4444
* @typedef {import('./lib/interface.js').RateLimiter} RateLimiter
45+
* @typedef {import('./lib/interface.js').RequestOptions} RequestOptions
4546
*/
4647

4748
/**
@@ -127,15 +128,16 @@ class NFTStorage {
127128
*
128129
* @param {Service} service
129130
* @param {Blob} blob
131+
* @param {RequestOptions} [options]
130132
* @returns {Promise<CIDString>}
131133
*/
132-
static async storeBlob(service, blob) {
134+
static async storeBlob(service, blob, options) {
133135
const blockstore = new Blockstore()
134136
let cidString
135137

136138
try {
137139
const { cid, car } = await NFTStorage.encodeBlob(blob, { blockstore })
138-
await NFTStorage.storeCar(service, car)
140+
await NFTStorage.storeCar(service, car, options)
139141
cidString = cid.toString()
140142
} finally {
141143
await blockstore.close()
@@ -155,7 +157,7 @@ class NFTStorage {
155157
static async storeCar(
156158
{ endpoint, token, rateLimiter = globalRateLimiter },
157159
car,
158-
{ onStoredChunk, maxRetries, decoders } = {}
160+
{ onStoredChunk, maxRetries, decoders, signal } = {}
159161
) {
160162
const url = new URL('upload/', endpoint)
161163
const headers = NFTStorage.auth(token)
@@ -176,11 +178,20 @@ class NFTStorage {
176178
const cid = await pRetry(
177179
async () => {
178180
await rateLimiter()
179-
const response = await fetch(url.toString(), {
180-
method: 'POST',
181-
headers,
182-
body: carFile,
183-
})
181+
/** @type {Response} */
182+
let response
183+
try {
184+
response = await fetch(url.toString(), {
185+
method: 'POST',
186+
headers,
187+
body: carFile,
188+
signal,
189+
})
190+
} catch (/** @type {any} */ err) {
191+
// TODO: remove me and test when client accepts custom fetch impl
192+
/* c8 ignore next 1 */
193+
throw signal && signal.aborted ? new AbortError(err) : err
194+
}
184195
/* c8 ignore next 3 */
185196
if (response.status === 429) {
186197
throw new Error('rate limited')
@@ -219,16 +230,17 @@ class NFTStorage {
219230
*
220231
* @param {Service} service
221232
* @param {FilesSource} filesSource
233+
* @param {RequestOptions} [options]
222234
* @returns {Promise<CIDString>}
223235
*/
224-
static async storeDirectory(service, filesSource) {
236+
static async storeDirectory(service, filesSource, options) {
225237
const blockstore = new Blockstore()
226238
let cidString
227239
try {
228240
const { cid, car } = await NFTStorage.encodeDirectory(filesSource, {
229241
blockstore,
230242
})
231-
await NFTStorage.storeCar(service, car)
243+
await NFTStorage.storeCar(service, car, options)
232244
cidString = cid.toString()
233245
} finally {
234246
await blockstore.close()
@@ -258,11 +270,12 @@ class NFTStorage {
258270
* @template {import('./lib/interface.js').TokenInput} T
259271
* @param {Service} service
260272
* @param {T} metadata
273+
* @param {RequestOptions} [options]
261274
* @returns {Promise<TokenType<T>>}
262275
*/
263-
static async store(service, metadata) {
276+
static async store(service, metadata, options) {
264277
const { token, car } = await NFTStorage.encodeNFT(metadata)
265-
await NFTStorage.storeCar(service, car)
278+
await NFTStorage.storeCar(service, car, options)
266279
return token
267280
}
268281

@@ -272,17 +285,20 @@ class NFTStorage {
272285
*
273286
* @param {Service} service
274287
* @param {string} cid
288+
* @param {RequestOptions} [options]
275289
* @returns {Promise<import('./lib/interface.js').StatusResult>}
276290
*/
277291
static async status(
278292
{ endpoint, token, rateLimiter = globalRateLimiter },
279-
cid
293+
cid,
294+
options
280295
) {
281296
const url = new URL(`${cid}/`, endpoint)
282297
await rateLimiter()
283298
const response = await fetch(url.toString(), {
284299
method: 'GET',
285300
headers: NFTStorage.auth(token),
301+
signal: options && options.signal,
286302
})
287303
/* c8 ignore next 3 */
288304
if (response.status === 429) {
@@ -308,12 +324,19 @@ class NFTStorage {
308324
*
309325
* @param {import('./lib/interface.js').PublicService} service
310326
* @param {string} cid
327+
* @param {RequestOptions} [options]
311328
* @returns {Promise<import('./lib/interface.js').CheckResult>}
312329
*/
313-
static async check({ endpoint, rateLimiter = globalRateLimiter }, cid) {
330+
static async check(
331+
{ endpoint, rateLimiter = globalRateLimiter },
332+
cid,
333+
options
334+
) {
314335
const url = new URL(`check/${cid}/`, endpoint)
315336
await rateLimiter()
316-
const response = await fetch(url.toString())
337+
const response = await fetch(url.toString(), {
338+
signal: options && options.signal,
339+
})
317340
/* c8 ignore next 3 */
318341
if (response.status === 429) {
319342
throw new Error('rate limited')
@@ -338,17 +361,20 @@ class NFTStorage {
338361
*
339362
* @param {Service} service
340363
* @param {string} cid
364+
* @param {RequestOptions} [options]
341365
* @returns {Promise<void>}
342366
*/
343367
static async delete(
344368
{ endpoint, token, rateLimiter = globalRateLimiter },
345-
cid
369+
cid,
370+
options
346371
) {
347372
const url = new URL(`${cid}/`, endpoint)
348373
await rateLimiter()
349374
const response = await fetch(url.toString(), {
350375
method: 'DELETE',
351376
headers: NFTStorage.auth(token),
377+
signal: options && options.signal,
352378
})
353379
/* c8 ignore next 3 */
354380
if (response.status === 429) {
@@ -498,9 +524,10 @@ class NFTStorage {
498524
* ```
499525
*
500526
* @param {Blob} blob
527+
* @param {RequestOptions} [options]
501528
*/
502-
storeBlob(blob) {
503-
return NFTStorage.storeBlob(this, blob)
529+
storeBlob(blob, options) {
530+
return NFTStorage.storeBlob(this, blob, options)
504531
}
505532

506533
/**
@@ -560,9 +587,10 @@ class NFTStorage {
560587
* instance as well, in which case directory structure will be retained.
561588
*
562589
* @param {FilesSource} files
590+
* @param {RequestOptions} [options]
563591
*/
564-
storeDirectory(files) {
565-
return NFTStorage.storeDirectory(this, files)
592+
storeDirectory(files, options) {
593+
return NFTStorage.storeDirectory(this, files, options)
566594
}
567595

568596
/**
@@ -575,9 +603,10 @@ class NFTStorage {
575603
* ```
576604
*
577605
* @param {string} cid
606+
* @param {RequestOptions} [options]
578607
*/
579-
status(cid) {
580-
return NFTStorage.status(this, cid)
608+
status(cid, options) {
609+
return NFTStorage.status(this, cid, options)
581610
}
582611

583612
/**
@@ -592,9 +621,10 @@ class NFTStorage {
592621
* ```
593622
*
594623
* @param {string} cid
624+
* @param {RequestOptions} [options]
595625
*/
596-
delete(cid) {
597-
return NFTStorage.delete(this, cid)
626+
delete(cid, options) {
627+
return NFTStorage.delete(this, cid, options)
598628
}
599629

600630
/**
@@ -607,9 +637,10 @@ class NFTStorage {
607637
* ```
608638
*
609639
* @param {string} cid
640+
* @param {RequestOptions} [options]
610641
*/
611-
check(cid) {
612-
return NFTStorage.check(this, cid)
642+
check(cid, options) {
643+
return NFTStorage.check(this, cid, options)
613644
}
614645

615646
/**
@@ -650,9 +681,10 @@ class NFTStorage {
650681
*
651682
* @template {import('./lib/interface.js').TokenInput} T
652683
* @param {T} token
684+
* @param {RequestOptions} [options]
653685
*/
654-
store(token) {
655-
return NFTStorage.store(this, token)
686+
store(token, options) {
687+
return NFTStorage.store(this, token, options)
656688
}
657689
}
658690

packages/client/src/lib/interface.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,11 @@ export interface API {
9494
* will be transformed into a URL that looks like
9595
* `ipfs://bafy...hash/image/blob`.
9696
*/
97-
store<T extends TokenInput>(service: Service, token: T): Promise<Token<T>>
97+
store<T extends TokenInput>(service: Service, token: T, options?: RequestOptions): Promise<Token<T>>
9898
/**
9999
* Stores a single file and returns it's CID.
100100
*/
101-
storeBlob(service: Service, content: Blob | File): Promise<CIDString>
101+
storeBlob(service: Service, content: Blob | File, options?: RequestOptions): Promise<CIDString>
102102
/**
103103
* Stores a CAR file and returns it's root CID.
104104
*/
@@ -112,25 +112,32 @@ export interface API {
112112
* be within the same directory, otherwise error is raised e.g. `foo/bar.png`,
113113
* `foo/bla/baz.json` is ok but `foo/bar.png`, `bla/baz.json` is not.
114114
*/
115-
storeDirectory(service: Service, files: FilesSource): Promise<CIDString>
115+
storeDirectory(service: Service, files: FilesSource, options?: RequestOptions): Promise<CIDString>
116116
/**
117117
* Returns current status of the stored NFT by its CID. Note the NFT must
118118
* have previously been stored by this account.
119119
*/
120-
status(service: Service, cid: string): Promise<StatusResult>
120+
status(service: Service, cid: string, options?: RequestOptions): Promise<StatusResult>
121121
/**
122122
* Removes stored content by its CID from this account. Please note that
123123
* even if content is removed from the service other nodes that have
124124
* replicated it might still continue providing it.
125125
*/
126-
delete(service: Service, cid: string): Promise<void>
126+
delete(service: Service, cid: string, options?: RequestOptions): Promise<void>
127127
/**
128128
* Check if a CID of an NFT is being stored by NFT.Storage.
129129
*/
130-
check(service: PublicService, cid: string): Promise<CheckResult>
130+
check(service: PublicService, cid: string, options?: RequestOptions): Promise<CheckResult>
131131
}
132132

133-
export interface CarStorerOptions {
133+
export interface RequestOptions {
134+
/**
135+
* A signal that can be used to abort the request.
136+
*/
137+
signal?: AbortSignal
138+
}
139+
140+
export interface CarStorerOptions extends RequestOptions {
134141
/**
135142
* Callback called after each chunk of data has been uploaded. By default,
136143
* data is split into chunks of around 10MB. It is passed the actual chunk

0 commit comments

Comments
 (0)