feat: add expiresUnixAt deadline option to RateLimiterQueue.removeTokens (#212) - #365
Conversation
…ens (animir#212) Adds an optional expiresUnixAt parameter (absolute Unix timestamp in seconds, default 0 = never) to removeTokens. Queued requests that pass their deadline are rejected with a RateLimiterQueueError during FIFO processing, keeping the timeout internal to the queue as suggested in animir#212 (no external Promise.race that could leak tokens). Implements the maintainer's sketch, with the expiry comparison oriented so an item is rejected once now >= expiresUnixAt. Adds tests for both the expired-rejection and still-valid paths.
There was a problem hiding this comment.
Pull request overview
This PR extends RateLimiterQueue.removeTokens with an optional absolute Unix-seconds deadline (expiresUnixAt) so queued requests can be rejected if they remain queued past a caller-specified cutoff time, matching the use case described in #212.
Changes:
- Add a third
expiresUnixAtparameter toRateLimiterQueue.removeTokensand propagate it through the internal queue implementation. - Sweep the FIFO queue during
_processFIFOto reject and remove expired queued items withRateLimiterQueueError. - Add mocha tests and update TypeScript typings to cover/document the new parameter.
Reviewed changes
Copilot reviewed 2 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
lib/RateLimiterQueue.js |
Adds expiresUnixAt plumbing and FIFO-time sweep to reject expired queued requests. |
test/RateLimiterQueue.test.js |
Adds test coverage for expiration rejection vs. future-deadline fulfillment. |
types.d.ts |
Updates RateLimiterQueue.removeTokens signature and documents the new deadline parameter. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@Ruby-Leung Hi. While changes look good, I'd like to discuss potentially increased processing time. Many users of RateLimiterQueue will get code that would filter the whole queue on every item processing. Is it possible to add a boolean flag for every queue? If there is at least one item added with |
Only filter the FIFO queue for expired requests when at least one request with expiresUnixAt > 0 is currently queued. A per-queue flag (_hasExpiringRequests) is raised when such a request is enqueued and recomputed during the sweep, so it falls back to the fast path once none remain. Projects that never pass expiresUnixAt keep the original O(1) processing cost per item.
The prior commit cleared _hasExpiringRequests from a snapshot of _queue during the sweep. With an async underlying limiter a deadline-bearing request can be momentarily shift()ed out of _queue (in-flight consume) while a second _processFIFO sweeps the temporarily empty queue and clears the flag; the rate-limit retry path then unshift()es the request back without re-arming the flag, so the sweep stays disabled and the request is never expired past its deadline. Make the flag set-once: arm it when a deadline-bearing request is enqueued and never clear it from the sweep. Also align the per-item expiry predicate with the arming condition (expiresUnixAt > 0) so a non-positive deadline is treated consistently regardless of queue contents. Add a regression test that locks the invariant.
|
Implemented your suggestion — thanks again. Each internal queue now carries a Full disclosure on the two commits: I first also cleared the flag once the queue drained of deadline requests (to restore the fast path), but testing showed that's unsafe — under an async limiter a deadline request is momentarily |
Closes #212.
Adds an optional deadline to
RateLimiterQueue.removeTokensso a queued request can give up instead of waiting forever.API
removeTokens(tokens, key = 'limiter', expiresUnixAt = 0)—expiresUnixAtis an absolute Unix timestamp in seconds;0(the default) keeps the current "never expires" behaviour, so this is fully backwards compatible. I added it as the third parameter because the second slot is now taken bykey.Implementation
Following the approach you outlined in #212, the timeout is handled internally inside the FIFO cycle (no external
Promise.race, so the queue can't be left inconsistent). During_processFIFO, any queued item whoseexpiresUnixAthas passed (now >= expiresUnixAt) is rejected with aRateLimiterQueueErrorand removed from the queue (I usedRateLimiterQueueErrorto match the existing queue-full rejection).One thing worth flagging: because the sweep runs inside
_processFIFO, an expired request is rejected at the next processing tick at-or-after its deadline, not exactly at the deadline — matching the "built into the normal cycle" design from the issue. Happy to add an eager timer if you'd prefer prompt rejection.Tests
Two cases added to
test/RateLimiterQueue.test.js:expiresUnixAtis rejected withRateLimiterQueueError;expiresUnixAtstill resolves normally.npx mocha test/RateLimiterQueue.test.js→ 18 passing.Happy to adjust the parameter name/shape, the error type, or eager-vs-lazy expiry to your preference.