UNPKG

nftstorage.link

Version:

Utilities for working with the NFT.Storage IPFS Edge Gateway

385 lines (343 loc) 10.8 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var throttledQueue = require('throttled-queue'); var pSettle = require('p-settle'); var pRetry = require('p-retry'); var fetch = require('@web-std/fetch'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var throttledQueue__default = /*#__PURE__*/_interopDefaultLegacy(throttledQueue); var pSettle__default = /*#__PURE__*/_interopDefaultLegacy(pSettle); var pRetry__default = /*#__PURE__*/_interopDefaultLegacy(pRetry); var fetch__default = /*#__PURE__*/_interopDefaultLegacy(fetch); /** * A client library for the https://api.nftstorage.link/ perma-cache service. It provides a convenient * interface for working with the [Raw HTTP API](https://nftstorage.link/#api-docs) * from a web browser or [Node.js](https://nodejs.org/) and comes bundled with * TS for out-of-the box type inference and better IntelliSense. * * @example * ```js * import { PermaCache } from 'nftstorage.link' * * const urls = [ * 'https://bafkreidyeivj7adnnac6ljvzj2e3rd5xdw3revw4da7mx2ckrstapoupoq.ipfs.nftstorage.link' * ] * * const cache = new PermaCache({ token: 'YOUR_NFT_STORAGE_TOKEN' }) * const permaCacheEntries = await cache.put(urls) * await cache.delete(urls) * ``` * @module */ const MAX_RETRIES = 5; // These match what is enforced server-side const RATE_LIMIT_REQUESTS = 100; const RATE_LIMIT_PERIOD = 60 * 1000; /** * @typedef { import('./lib/interface.js').RateLimiter } RateLimiter * @typedef { import('./lib/interface.js').Service } Service * @typedef { import('./lib/interface.js').PutOptions} PutOptions * @typedef { import('./lib/interface.js').DeleteOptions} DeleteOptions * @typedef { import('./lib/interface.js').ListOptions} ListOptions * @typedef { import('./lib/interface.js').CacheResult} CacheResult * @typedef { import('./lib/interface.js').CacheDeleteResult} CacheDeleteResult * @typedef { import('./lib/interface.js').CacheEntry} CacheEntry * @typedef { import('./lib/interface.js').AccountInfo} AccountInfo */ /** * @implements Service */ class PermaCache { /** * Constructs a client bound to the given `options.token` and * `options.endpoint`. * * @example * ```js * import { PermaCache } from 'nftstorage.link' * const cache = new PermaCache({ token: API_TOKEN }) * ``` * * @param {{token: string, endpoint?:URL, rateLimiter?: RateLimiter}} options */ constructor({ token, endpoint = new URL('https://api.nftstorage.link'), rateLimiter, }) { /** * Authorization token. * * @readonly */ this.token = token; /** * Service API endpoint `URL`. * @readonly */ this.endpoint = endpoint; /** * @readonly */ this.rateLimiter = rateLimiter || createRateLimiter(); } /** * @hidden * @param {string} token * @returns {Record<string, string>} */ static headers(token) { /* c8 ignore next 1 */ if (!token) throw new Error('missing token') return { Authorization: `Bearer ${token}`, 'X-Client': 'nftstorage.link/js', } } /** * @param {Service} service * @param {string[]} urls * @param {PutOptions} [options] * @returns {Promise<CacheResult[]>} */ static async put( { endpoint, token, rateLimiter = globalRateLimiter }, urls, { onPut, maxRetries } = {} ) { urls.forEach(validateUrl); const headers = PermaCache.headers(token); /** @type {import('p-settle').PromiseResult<CacheResult>[]} */ const cacheResults = await pSettle__default["default"]( urls.map(async (url) => { const apiUrl = new URL( `perma-cache/${encodeURIComponent(url)}`, endpoint ); return await pRetry__default["default"]( async () => { await rateLimiter(); const response = await fetch__default["default"](apiUrl.toString(), { method: 'POST', headers, }); /** @type {CacheResult} */ const result = await response.json(); if (!response.ok) { // @ts-ignore Only exists on Error const e = new Error(result.message); // do not retry if fatal errors - will not succeed if (response.status >= 400 && response.status < 500) { throw new pRetry.AbortError(e) } /* c8 ignore next 2 */ throw e } onPut && onPut(url); return result }, /* c8 ignore next 3 */ { retries: maxRetries == null ? MAX_RETRIES : maxRetries, } ) }) ); return cacheResults.map((r, i) => { // @ts-ignore reason and value might not exist, but one of them always exists return r.reason ? { url: urls[i], error: r.reason.message } : r.value }) } /** * @param {Service} service * @param {ListOptions} [options] * @returns {AsyncIterable<CacheEntry>} */ static async *list( { endpoint, token, rateLimiter = globalRateLimiter }, { sort = 'date', order = 'asc' } = {} ) { const headers = PermaCache.headers(token); let search = new URLSearchParams({ sort, order }); let nextPageUrl = new URL(`perma-cache?${search}`, endpoint); while (true) { await rateLimiter(); const response = await fetch__default["default"](nextPageUrl.toString(), { method: 'GET', headers, }); const result = await response.json(); if (!response.ok) { throw new Error(result.message) } for (const entry of result) { yield entry; } // Go over next links until not provided by API anymore const link = response.headers.get('link'); if (!link) { break } nextPageUrl = new URL( link.replace('<', '').replace('>; rel="next"', ''), endpoint ); } } /** * @param {Service} service * @param {string[]} urls * @param {DeleteOptions} [options] * @returns {Promise<CacheDeleteResult[]>} */ static async delete( { endpoint, token, rateLimiter = globalRateLimiter }, urls, { onDelete, maxRetries } = {} ) { urls.forEach(validateUrl); const headers = PermaCache.headers(token); /** @type {import('p-settle').PromiseResult<CacheDeleteResult>[]} */ const cacheDeleteResults = await pSettle__default["default"]( urls.map(async (url) => { const apiUrl = new URL( `perma-cache/${encodeURIComponent(url)}`, endpoint ); return await pRetry__default["default"]( async () => { await rateLimiter(); const response = await fetch__default["default"](apiUrl.toString(), { method: 'DELETE', headers, }); const result = await response.json(); if (!response.ok) { const e = new Error(result.message); // do not retry if fatal errors - will not succeed if (response.status >= 400 && response.status < 500) { throw new pRetry.AbortError(e) } /* c8 ignore next 2 */ throw e } onDelete && onDelete(url); return { url, } }, /* c8 ignore next 3 */ { retries: maxRetries == null ? MAX_RETRIES : maxRetries, } ) }) ); return cacheDeleteResults.map((r, i) => { // @ts-ignore reason and value might not exist, but one of them always exists return r.reason ? { url: urls[i], error: r.reason.message } : r.value }) } /** * @param {Service} service * @return {Promise<AccountInfo>} */ static async accountInfo({ endpoint, token, rateLimiter = globalRateLimiter, }) { const url = new URL('perma-cache/account', endpoint); const headers = PermaCache.headers(token); await rateLimiter(); const response = await fetch__default["default"](url.toString(), { method: 'GET', headers, }); const result = await response.json(); if (!response.ok) { throw new Error(result.message) } return result } // Just a sugar so you don't have to pass around endpoint and token around. /** * Perma cache URLS into nftstorage.link. * * Returns the corresponding Perma cache entries created. * * @param {string[]} urls * @param {PutOptions} [options] * @returns {Promise<CacheResult[]>} */ put(urls, options) { return PermaCache.put(this, urls, options) } /** * List all Perma cached URLs for this account. Use a `for await...of` loop to fetch them all. * @example * Fetch all the urls * ```js * const urls = [] * for await (const item of client.list()) { * urls.push(item) * } * ``` * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of * @param {ListOptions} [options] * @returns {AsyncIterable<CacheEntry>} */ list(options) { return PermaCache.list(this, options) } /** * @param {string[]} urls * @param {DeleteOptions} [options] * @returns {Promise<CacheDeleteResult[]>} */ delete(urls, options) { return PermaCache.delete(this, urls, options) } /** * Fetch info on PermaCache for the user. */ accountInfo() { return PermaCache.accountInfo(this) } } /** * Creates a rate limiter which limits at the same rate as is enforced * server-side, to allow the client to avoid exceeding the requests limit and * being blocked for 30 seconds. * @returns {RateLimiter} */ function createRateLimiter() { const throttle = throttledQueue__default["default"](RATE_LIMIT_REQUESTS, RATE_LIMIT_PERIOD); return () => throttle(() => {}) } /** * Rate limiter used by static API if no rate limiter is passed. Note that each * instance of the PermaCache class gets it's own limiter if none is passed. * This is because rate limits are enforced per API token. */ const globalRateLimiter = createRateLimiter(); /** * @param {string} urlString */ function validateUrl(urlString) { const url = new URL(urlString); if ( !url.hostname.includes('.ipfs.nftstorage.link') && !( url.hostname.includes('nftstorage.link') && url.pathname.startsWith('/ipfs') ) ) { throw new Error( `Invalid URL (not an nftstorage.link IPFS URL): ${urlString}` ) } } exports.PermaCache = PermaCache; exports.createRateLimiter = createRateLimiter; //# sourceMappingURL=perma-cache.cjs.map