关于javascript:RxJS v5中的速率限制和计数限制事件,但也允许传递

Rate-limiting and count-limiting events in RxJS v5, but also allowing pass-through

我有一堆事件要发送给服务。 但是请求受到速率限制,每个请求都有计数限制:

  • 每秒1个请求:bufferTime(1000)
  • 每个请求100个事件项:bufferCount(100)

问题是,我不确定如何以有意义的方式将它们组合在一起。

允许通过

使这一点进一步复杂化的是,如果我们没有达到任何限制,我需要确保事件立即通过。

例如,我不希望它在忙碌的时间内只有一个事件时才真正等待100个事件项才能通过。

旧版API

我还发现RxJS v4中存在一个bufferWithTimeOrCount,尽管我不确定即使拥有它也将如何使用它。

测试场

这是我为您测试的JSBin解决方案:

任何帮助将不胜感激。


bufferTime()运算符采用三个参数,这些参数结合了bufferTimebufferCount的功能。请参阅http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-bufferTime。

使用.bufferTime(1000, null, 3),您可以每1000ms或达到3个项目时创建一个缓冲区。但是,这意味着不能保证每个缓冲区之间的延迟为1000ms。

因此,您可以使用类似这样的简单易用的东西(最多1000ms仅缓冲3个项目):

1
2
3
4
5
6
7
click$
  .scan((a, b) => a + 1, 0)
  .bufferTime(1000, null, 3)
  .filter(buffer => buffer.length > 0)
  .concatMap(buffer => Rx.Observable.of(buffer).delay(1000))
  .timestamp()
  .subscribe(console.log);

观看现场演示:http://jsbin.com/libazer/7/edit?js,控制台,输出

与您可能想要的唯一区别是,第一次发射可能会延迟1000毫秒以上。这是因为bufferTime()delay(1000)运算符都会延迟以确保始终存在至少1000ms的间隙。


我希望这对您有用。

操作员

1
2
3
4
5
events$
  .windowCount(10)
  .mergeMap(m => m.bufferTime(100))
  .concatMap(val => Rx.Observable.of(val).delay(100))
  .filter(f => f.length > 0)

文件

  • .windowCount(number):[Rx Doc]
  • .bufferTime(number):[Rx Doc]

演示版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// test case
const mock = [8, 0, 2, 3, 30, 5, 6, 2, 2, 0, 0, 0, 1]

const tInterval = 100
const tCount = 10

Rx.Observable.interval(tInterval)
  .take(mock.length)
  .mergeMap(mm => Rx.Observable.range(0, mock[mm]))
 
  // start
  .windowCount(tCount)
  .mergeMap(m => m.bufferTime(tInterval))
  .concatMap(val => Rx.Observable.of(val).delay(tInterval))
  .filter(f => f.length > 0)
  // end

  .subscribe({
    next: (n) => console.log('Next: ', n),
    error: (e) => console.log('Error: ', e),
    complete: (c) => console.log('Completed'),
  })
1
<script src="https://unpkg.com/rxjs/bundles/Rx.min.js">

更新

经过更多测试。我发现上述答案在极端情况下存在一些问题。我认为它们是由.window().concat()引起的,然后在doc#concatMap中发现了警告。

Warning: if source values arrive endlessly and faster than their corresponding inner Observables can complete, it will result in memory issues as inner Observables amass in an unbounded buffer waiting for their turn to be subscribed to.

但是,我认为限制请求速率的正确方法可能是限制请求的周期时间。在您的情况下,仅限制每10毫秒只有1个请求。控制请求更简单,也可能更有效。

操作员

1
2
3
4
5
6
7
8
9
const tInterval = 100
const tCount = 10
const tCircle = tInterval / tCount

const rxTimer = Rx.Observable.timer(tCircle).ignoreElements()

events$
  .concatMap(m => Rx.Observable.of(m).merge(rxTimer)) // more accurate than `.delay()`
  // .concatMap(m => Rx.Observable.of(m).delay(tCircle))

要么

1
2
events$
  .zip(Rx.Observable.interval(tCircle), (x,y) => x)


我已修改此问题的答案,以支持您在待处理请求中添加数量有限的值(即事件)的用例。

其中的评论应说明其工作原理。

因为您需要记录在限速期间内发出的请求,所以我认为不可能使用bufferTimebufferCount运算符来执行您想要的操作-scan是以便可以在可观察范围内保持该状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
function rateLimit(source, period, valuesPerRequest, requestsPerPeriod = 1) {

  return source
    .scan((requests, value) => {

      const now = Date.now();
      const since = now - period;

      // Keep a record of all requests made within the last period. If the
      // number of requests made is below the limit, the value can be
      // included in an immediate request. Otherwise, it will need to be
      // included in a delayed request.

      requests = requests.filter((request) => request.until > since);
      if (requests.length >= requestsPerPeriod) {

        const leastRecentRequest = requests[0];
        const mostRecentRequest = requests[requests.length - 1];

        // If there is a request that has not yet been made, append the
        // value to that request if the number of values in that request's
        // is below the limit. Otherwise, another delayed request will be
        // required.

        if (
          (mostRecentRequest.until > now) &&
          (mostRecentRequest.values.length < valuesPerRequest)
        ) {

          mostRecentRequest.values.push(value);

        } else {

          // until is the time until which the value should be delayed.

          const until = leastRecentRequest.until + (
            period * Math.floor(requests.length / requestsPerPeriod)
          );

          // concatMap is used below to guarantee the values are emitted
          // in the same order in which they are received, so the delays
          // are cumulative. That means the actual delay is the difference
          // between the until times.

          requests.push({
            delay: (mostRecentRequest.until < now) ?
              (until - now) :
              (until - mostRecentRequest.until),
            until,
            values: [value]
          });
        }

      } else {

        requests.push({
          delay: 0,
          until: now,
          values: [value]
        });
      }
      return requests;

    }, [])

    // Emit only the most recent request.

    .map((requests) => requests[requests.length - 1])

    // If multiple values are added to the request, it will be emitted
    // mulitple times. Use distinctUntilChanged so that concatMap receives
    // the request only once.

    .distinctUntilChanged()
    .concatMap((request) => {

      const observable = Rx.Observable.of(request.values);
      return request.delay ? observable.delay(request.delay) : observable;
    });
}

const start = Date.now();
rateLimit(
  Rx.Observable.range(1, 250),
  1000,
  100,
  1
).subscribe((values) => console.log(
  `Request with ${values.length} value(s) at T+${Date.now() - start}`
));
1
.as-console-wrapper { max-height: 100% !important; top: 0; }
1
<script src="https://unpkg.com/rxjs@5/bundles/Rx.min.js">