Skip to content

feat: add expiresUnixAt deadline option to RateLimiterQueue.removeTokens (#212) - #365

Merged
animir merged 3 commits into
animir:masterfrom
Ruby-Leung:queue-removetokens-expiry
Jun 8, 2026
Merged

feat: add expiresUnixAt deadline option to RateLimiterQueue.removeTokens (#212)#365
animir merged 3 commits into
animir:masterfrom
Ruby-Leung:queue-removetokens-expiry

Conversation

@Ruby-Leung

Copy link
Copy Markdown
Contributor

Closes #212.

Adds an optional deadline to RateLimiterQueue.removeTokens so a queued request can give up instead of waiting forever.

API

const queue = new RateLimiterQueue(limiter)
// reject this request if it is still queued at the given Unix time (seconds)
await queue.removeTokens(1, 'limiter', Math.floor(Date.now() / 1000) + 5)

removeTokens(tokens, key = 'limiter', expiresUnixAt = 0)expiresUnixAt is 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 by key.

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 whose expiresUnixAt has passed (now >= expiresUnixAt) is rejected with a RateLimiterQueueError and removed from the queue (I used RateLimiterQueueError to 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:

  • a queued request past its expiresUnixAt is rejected with RateLimiterQueueError;
  • a queued request with a future expiresUnixAt still 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.

…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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 expiresUnixAt parameter to RateLimiterQueue.removeTokens and propagate it through the internal queue implementation.
  • Sweep the FIFO queue during _processFIFO to reject and remove expired queued items with RateLimiterQueueError.
  • 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.

Comment thread lib/RateLimiterQueue.js
Comment thread test/RateLimiterQueue.test.js
@animir

animir commented Jun 5, 2026

Copy link
Copy Markdown
Owner

@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.
It isn't an issue when queue is small.
But when queue grows larger, the filtering may take more than expected.

Is it possible to add a boolean flag for every queue? If there is at least one item added with expiresUnixAt > 0 then filter queue. Otherwise skip the filtering. This way most of current projects will be automagically opt-outed. What do you think?

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.
@Ruby-Leung

Copy link
Copy Markdown
Contributor Author

Implemented your suggestion — thanks again. Each internal queue now carries a _hasExpiringRequests flag that is armed (true) the moment a request with expiresUnixAt > 0 is enqueued; _processFIFO runs the expiry sweep only when it's set, so projects that never pass expiresUnixAt keep the original O(1) cost and are opted out automatically.

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 shift()ed out of the queue during its in-flight consume(), and a second _processFIFO could sweep the temporarily-empty queue and clear the flag; the rate-limit retry path then unshift()es the request back without re-arming it, leaving it stranded past its deadline forever. So I reverted to a strict set-once flag (exactly your suggestion). I also aligned the sweep's per-item predicate to expiresUnixAt > 0 to match the arming condition. Added a regression test that locks the invariant; all RateLimiterQueue tests pass.

@animir
animir merged commit e58ee32 into animir:master Jun 8, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

limiter.removeTokens of RateLimiterQueue with maximum wait time

3 participants