diff --git a/package.json b/package.json index a174deadcd..951354bd75 100644 --- a/package.json +++ b/package.json @@ -148,6 +148,7 @@ "multihashes": "~0.4.14", "multihashing-async": "~0.6.0", "node-fetch": "^2.3.0", + "p-queue": "^6.1.0", "peer-book": "~0.9.0", "peer-id": "~0.12.3", "peer-info": "~0.15.0", diff --git a/src/core/runtime/dns-browser.js b/src/core/runtime/dns-browser.js index 74d0a14cdb..7b2725e491 100644 --- a/src/core/runtime/dns-browser.js +++ b/src/core/runtime/dns-browser.js @@ -1,33 +1,57 @@ -/* global self */ +/* eslint-env browser */ 'use strict' +const TLRU = require('../../utils/tlru') +const { default: PQueue } = require('p-queue') + +// Avoid sending multiple queries for the same hostname by caching results +const cache = new TLRU(1000) +// TODO: /api/v0/dns does not return TTL yet: https://github.com/ipfs/go-ipfs/issues/5884 +// However we know browsers themselves cache DNS records for at least 1 minute, +// which acts a provisional default ttl: https://stackoverflow.com/a/36917902/11518426 +const ttl = 60 * 1000 + +// browsers limit concurrent connections per host, +// we don't want preload calls to exhaust the limit (~6) +const _httpQueue = new PQueue({ concurrency: 4 }) + +function unpackResponse (domain, response, callback) { + if (response.Path) { + return callback(null, response.Path) + } else { + const err = new Error(response.Message) + return callback(err) + } +} + module.exports = (domain, opts, callback) => { if (typeof opts === 'function') { callback = opts opts = {} } - opts = opts || {} - domain = encodeURIComponent(domain) - let url = `https://ipfs.io/api/v0/dns?arg=${domain}` + if (cache.has(domain)) { + const response = cache.get(domain) + return unpackResponse(domain, response, callback) + } + + let url = `https://ipfs.io/api/v0/dns?arg=${domain}` Object.keys(opts).forEach(prop => { url += `&${encodeURIComponent(prop)}=${encodeURIComponent(opts[prop])}` }) - self.fetch(url, { mode: 'cors' }) + _httpQueue.add(() => fetch(url, { mode: 'cors' }) .then((response) => { return response.json() }) .then((response) => { - if (response.Path) { - return callback(null, response.Path) - } else { - return callback(new Error(response.Message)) - } + cache.set(domain, response, ttl) + return unpackResponse(domain, response, callback) }) .catch((error) => { callback(error) }) + ) } diff --git a/src/core/runtime/preload-browser.js b/src/core/runtime/preload-browser.js index 81407f483f..d24c7f3eff 100644 --- a/src/core/runtime/preload-browser.js +++ b/src/core/runtime/preload-browser.js @@ -1,18 +1,23 @@ /* eslint-env browser */ 'use strict' +const { default: PQueue } = require('p-queue') const debug = require('debug') const log = debug('ipfs:preload') log.error = debug('ipfs:preload:error') +// browsers limit concurrent connections per host, +// we don't want preload calls to exhaust the limit (~6) +const _httpQueue = new PQueue({ concurrency: 4 }) + module.exports = function preload (url, callback) { log(url) const controller = new AbortController() const signal = controller.signal - fetch(url, { signal }) + _httpQueue.add(() => fetch(url, { signal }) .then(res => { if (!res.ok) { log.error('failed to preload', url, res.status, res.statusText) @@ -22,6 +27,7 @@ module.exports = function preload (url, callback) { }) .then(() => callback()) .catch(callback) + ).catch(callback) return { cancel: () => controller.abort()