如何使用主机提供的速率限制来限制我的 JS API 获取请求?

问题描述 投票:0回答:4

这是迄今为止我的代码:

const allRows = [];

async function fileToLines(file) {
  return new Promise((resolve, reject) => {
    reader = new FileReader();
    reader.onload = function(e) {
      parsedLines = e.target.result.split(/\r|\n|\r\n/);
      resolve(parsedLines);
    };
    reader.readAsText(file);
  });
}

document
.getElementById('fileInput')
.addEventListener('change', async function(e) {
  var file = e.target.files[0];

  if (file != undefined) {
    fileToLines(file).then( async id => {
      console.log(id)
      console.log(parsedLines)
      console.log(typeof id);

      var idInt = id.map(Number);
      var idFiltered = id.filter(function(v){return v!==''});

      console.log(idFiltered)

      for(let id of idFiltered) {
        const row = await getRelease(id);
        allRows.push(row);
      }
      download();
    });
  }
});

function getRelease(idFiltered) {
  return fetch(`https://api.***.com/releases/${idFiltered}`, {
    headers: {
    'User-Agent': '***/0.1',
    },
  })
  .then(response => response.json())
  .then(data => {
    if (data.message === 'Release not found.') {
      return { error: `Release with ID ${idFiltered} does not exist` };
    } else {
      const id = data.id;
      const delimiter = document.getElementById("delimiter").value || "|";
      const artists = data.artists ? data.artists.map(artist => artist.name) : [];
      const barcode = data.identifiers.filter(id => id.type === 'Barcode')
      .map(barcode => barcode.value);
      var formattedBarcode = barcode.join(delimiter);
      const country = data.country || 'Unknown';
      const genres = data.genres || [];
      const formattedGenres = genres.join(delimiter);
      const labels = data.labels ? data.labels.map(label => label.name) : [];
      const formattedLabels = labels.join(delimiter);
      const catno = data.labels ? data.labels.map(catno => catno.catno) : [];
      const formattedCatNo = catno.join(delimiter);
      const styles = data.styles || [];
      const formattedStyles = styles.join(delimiter);
      const tracklist = data.tracklist ? data.tracklist
      .map(track => track.title) : [];
      const formattedTracklist = tracklist.join(delimiter);
      const year = data.year || 'Unknown';
      const format = data.formats ? data.formats.map(format => format.name) : [];
      const qty = data.formats ? data.formats.map(format => format.qty) : [];
      const descriptions = data.formats ? data.formats
      .map(descriptions => descriptions.descriptions) : [];
      const preformattedDescriptions = descriptions.toString()
      .replace('"','""').replace(/,/g, ', ');
      const formattedDescriptions = '"' + preformattedDescriptions + '"';

      return [idFiltered,
        artists,
        format,
        qty,
        formattedDescriptions,
        formattedLabels,
        formattedCatNo,
        country,
        year,
        formattedGenres,
        formattedStyles,
        formattedBarcode,
        formattedTracklist
      ];
    }
  });
}

function download() {
  const ROW_NAMES = [
    "release_id",
    "artist",
    "format",
    "qty",
    "format descriptions",
    "label",
    "catno",
    "country",
    "year",
    "genres",
    "styles",
    "barcode",
    "tracklist"
  ];
  var csvContent = "data:text/csv;charset=utf-8,"
  + ROW_NAMES + "\n" + allRows.map(e => e.join(",")).join("\n");

  console.log(csvContent);

  var encodedUri = encodeURI(csvContent);
  var link = document.createElement("a");
  link.setAttribute("href", encodedUri);
  link.setAttribute("download", "my_data.csv");
  document.body.appendChild(link); // Required for FF
  link.click();
}

2.5 年前,当我试图解决这个问题时(!),有人告诉我最简单的方法“就是维护一系列承诺来跟踪请求”,就像这样......

  const timer = ms => new Promise(resolve => setTimeout(resolve, ms));

  let requests = Promise.resolve();

  function getRelease(id) {
   const apiCall = requests.then(() =>
    fetch(`https://api.***.com/releases/${id}`, {
      headers: {
        'User-Agent': '***/0.1',
      }
    })   
   );

   // add to chain / queue 
   requests = apiCall.then(response => 
    +response.headers.get("X-***-Ratelimit-Remaining") <= 1 && timer(60 * 1000)
   );

   return apiCall
     .then(response => response.json())
     .then(parseReleaseData);
  }

建议此代码的人评论...

现在,一个请求将被完成,如果达到速率限制,则会等待一分钟。

如果出现速率限制错误,您可能需要重试。您还可以添加多个承诺队列以实现更高的吞吐量。

我之前尝试时,似乎设置了 60 秒的延迟才能拨打任何电话?我想我想再次尝试这个方法,但我不知道如何编码。就像,我不确定

const apiCall = requests.then(() =>
如何适合我当前的代码。我可以看到建议的代码实际上返回“apiCall”,而我的方法设置为返回所有单独的数据字段,所以我不确定如何继续。从主机获取
Ratelimit
并根据需要设置超时似乎是一个好方法,但我只是不确定从哪里开始。请问有什么帮助吗?

编辑:我一直在尝试这样做,但仍然不起作用:

const timer = ms => new Promise(resolve => setTimeout(resolve, ms));

const createThrottler = (rateLimit) => {
  let requestTimestamp = 0;
  return (requestHandler) => {
    return async (...params) => {
      const currentTimestamp = Math.floor(Date.now() / 1000);
      if (currentTimestamp < requestTimestamp + rateLimit) {
        await timer(rateLimit - (currentTimestamp - requestTimestamp))
      }
      requestTimestamp = Math.floor(Date.now() / 1000);
      return await requestHandler(...params);
    }
  }
}

const throttle = createThrottler(2500);

const throttleFetch = throttle(fetch);

Edit2:我想知道是否存在问题,我将此行注释掉了:

const rateLimit = Math.floor((60 / response.headers.get("X-Discogs-Ratelimit-Remaining")) * 1000);

所以我尝试取消评论,但现在我明白了

未捕获的引用错误:

response
未定义

Edit3:我得到了一个让

createThrottler()
功能正常工作的建议:-

const rateLimit = 2500;

const timer = ms => new Promise(resolve => setTimeout(resolve, ms));

const createThrottler = (rateLimit) => {
  let requestTimestamp = 0;
  return (requestHandler) => {
    return async (...params) => {
      const currentTimestamp = Number(Date.now());
      if (currentTimestamp < requestTimestamp + rateLimit) {
        const timeOut = rateLimit - (currentTimestamp - requestTimestamp);
        requestTimestamp = Number(Date.now()) + timeOut;
        await timer(timeOut)
      }
      requestTimestamp = Number(Date.now());
      return await requestHandler(...params);
    }
  }
}

我不能说我自己就能解决这个问题,但我们就是这样。所以现在我正在尝试弄清楚如何以及在哪里编码

const rateLimit = Math.floor((60 / response.headers.get("X-Discogs-Ratelimit-Remaining")) * 1000);

没有得到

未捕获(承诺中)ReferenceError:响应未定义

javascript json csv
4个回答
2
投票

你看过debounce吗?

您可以将速率限制为在任何定义的时间段内服务的 1 个呼叫。将此视为量化。另一种方法是在较长的时间范围内对呼叫进行计数,然后无限期地或在定义的时间内阻止进一步的呼叫 - 这取决于您的首选用例。

通常,速率限制更多地与安全有关,第一个选项(在规定的时间内服务 1 个呼叫)是合适的。如果您为 Web API 执行此操作,您可能希望拒绝“太早”的请求,并为请求者提供某种类型的反馈以及适当的 HTTP 状态代码。

此处讨论如何实现所有不同的选项:https://thoughtspile.github.io/2018/07/07/rate-limit-promises/

编辑: 回应下面的OP评论并审查代码......我认为你想太多了。

FWIW 我大部分时间都使用 debounce(相当于你的“节流阀”),它的字面意思是沿着 debounce(functionReference,timeoutInMilliseconds) 的方式使用。

代码看起来像这样

function debounce(func, waitFor) {
    let timeout;
    return (...args) => new Promise(resolve => {
        if (timeout) {
            clearTimeout(timeout);
        }
        timeout = setTimeout(() => resolve(func(...args)), waitFor);
    });
}

将你的

throttle(fetch)
更改为我的
debounce(fetch,2500)
,应该就足够了。您不需要在那一行进行赋值操作,只需调用它,或者编写另一个名为
debouncedFetch
的函数来封装它,然后从您需要的任何地方调用它即可。


1
投票

我正在尝试使用与 OP 相同的 API 端点来解决非常相似的问题。 我的解决方案获取一系列发行 ID 的完整发行信息。 我怀疑我的解决方案是否接近最佳,但它工作正常。

const recordIDs: number[] = JSON.parse(req.body.records)
const endpoint = `${discogsAPIURL}releases/`
const user = req.user! as IUser
const records: ReleaseFull[] = []
const throttlePoint = 6 // X-Discogs-Ratelimit-Remaining to begin throttling
let requestsMade = 0
let limitRemaining = 60
let wait = 0

while (requestsMade < recordIDs.length) {
  if (wait) await new Promise((resolve) => setTimeout(resolve, wait))
  const url = endpoint + recordIDs[requestsMade].toString()
  const response = await authorisedDiscogsRequest(url, user)

  if (response.status === 200) {
    requestsMade++
    const retrievedRecord = (await response.json()) as ReleaseFull
    records.push(retrievedRecord)
    res.write("data: " + `${requestsMade / recordIDs.length}\n\n`)
    limitRemaining = parseInt(
      response.headers.get("X-Discogs-Ratelimit-Remaining") || "0"
    )
    wait =
      limitRemaining < throttlePoint
        ? (throttlePoint - limitRemaining) * 1000
        : 0
  } else if (response.status === 429) {
    wait = wait + 10000
  } else if (response.status === 404) {
    res.write("data: " + `Error: A release was not found by Discogs.\n\n`)
    res.end()
  } else {
    res.write("data: " + `Error: Unexpected error.\n\n`)
    res.end()
  }
}

0
投票

尝试承诺速率限制

来自他们的文档:

var throttle = require('promise-ratelimit')(2000); /* rateInMilliseconds */

var startTime = Date.now();

for (var i = 0; i < 10; i++) {
    throttle().then(function() { console.log(Date.now() - startTime); });
}

0
投票

最好使用支持插件的 fetch 包装器,这里我们使用 xior.js,示例:

import xior from 'xior';
import throttlePlugin from 'xior/plugins/throttle';

const http = xior.create();

http.plugins.use(
  throttlePlugin({
    onThrottle(config) {
      console.log(`Throttle requests ${config.method} ${config.url}`);
    },
  })
);

xior.js 中有更多有用的插件。检查 -> https://github.com/suhaotian/xior

© www.soinside.com 2019 - 2024. All rights reserved.