Persistent limits. Effortlessly automated.
One line. No boilerplate. No setup. The limit package gives you instant, persistent control over cooldowns and rate limits โ across sessions, isolates, and app restarts. Define once, automate forever.
Table of Contents
- โฒ Cooldown โ automatically manage cooldown periods (e.g. daily rewards, retry delays)
- ๐ Rate Limiter โ control rates using a token bucket (e.g. 1000 actions per 15 minutes)
๐ฅ Why Use limit
?
Working with cooldowns and rate limits usually means:
- Manual
DateTime
comparisons - Writing timers or expiration logic
- Saving timestamps or counters
- Handling null, casting, and cleanup
limit removes all that: you just define, call, and trust it.
- โ Lets you define, control, and forget โ the system handles everything in the background
- โ One-line setup, no manual storage or timers
- โ Persisted across app restarts and isolates
- โ Async-safe and cache-friendly
- โ Works great for daily rewards, retry delays, API limits, chat quotas, and more
๐ Choosing the Right Limiter
Each limiter is tailored for a specific pattern of time-based control.
Goal | Use |
---|---|
"Only once every X time" | Cooldown |
"Allow N actions per Y minutes" | RateLimiter |
"Only once every 24 hours"
โ Fixed cooldown timer from last activation
โ Great for claim buttons, retry delays, or cooldown locks
"Allow 100 actions per 15 minutes (rolling refill)"
โ Token bucket algorithm
โ Replenishes tokens over time (not per action)
โ Great for APIs, messaging, or hard quota control
โฒ Cooldown
Persistent Cooldown Service
โคด๏ธ Back -> Table of Contents
Cooldown
is a plug-and-play utility service for managing cooldown windows (e.g. daily rewards, button lockouts, retry delays) that persist across sessions and isolates โ no timers, no manual bookkeeping, no re-implementation every time.
It handles:
- Cooldown timing (
DateTime.now()
+ duration) - Persistent storage (with caching and async-safety)
- Activation tracking and expiration logic
- Usage statistics (activation count, expiry progress, etc.)
๐ง How to Use
isCooldownActive()
โ Returnstrue
if the cooldown is still activeisExpired()
โ Returnstrue
if the cooldown has expired or was never startedactivateCooldown()
โ Starts the cooldown using the configured durationtryActivate()
โ Starts cooldown only if it's not active โ returns whether it was triggeredreset()
โ Clears the cooldown timer, but keeps the activation countcompleteReset()
โ Fully resets both the cooldown and its usage countertimeRemaining()
โ Returns remaining time as aDuration
secondsRemaining()
โ Same as above, in secondspercentRemaining()
โ Progress indicator between0.0
and1.0
getLastActivationTime()
โ ReturnsDateTime?
of last activationgetEndTime()
โ Returns when the cooldown will endwhenExpires()
โ Returns aFuture
that completes when the cooldown endsgetActivationCount()
โ Returns the total number of activationsremoveAll()
โ Deletes all stored values (for testing/debugging)anyStateExists()
โ Returnstrue
if any cooldown data exists in storage
โ Define a Cooldown
final cooldown = Cooldown('daily_reward', duration: Duration(hours: 24));
This creates a persistent cooldown that lasts 24 hours. It uses the prefix 'daily_reward'
to store:
- Last activation timestamp
- Activation count
๐ Check If Cooldown Is Active
if (await cooldown.isCooldownActive()) {
print('Wait before trying again!');
}
โฑ Activate the Cooldown
await cooldown.activateCooldown();
This sets the cooldown to now and begins the countdown. The activation count is automatically incremented.
โก Try Activating Only If Expired
if (await cooldown.tryActivate()) {
print('Action allowed and cooldown started');
} else {
print('Still cooling down...');
}
Use this for one-line cooldown triggers (e.g. claiming a daily gift or retrying a network call).
๐งผ Reset or Fully Clear Cooldown
await cooldown.reset(); // Clears only the time
await cooldown.completeReset(); // Clears time and resets usage counter
๐ Check Time Remaining
final remaining = await cooldown.timeRemaining();
print('Still ${remaining.inMinutes} minutes left');
You can also use:
await cooldown.secondsRemaining(); // int
await cooldown.percentRemaining(); // double between 0.0โ1.0
๐ View Timing Info
final lastUsed = await cooldown.getLastActivationTime();
final endsAt = await cooldown.getEndTime();
โณ Wait for Expiry (e.g. for auto-retry)
await cooldown.whenExpires(); // Completes only when cooldown is over
๐ Get Activation Count
final count = await cooldown.getActivationCount();
print('Used $count times');
๐งช Test Utilities
await cooldown.removeAll(); // Clears all stored cooldown state
final exists = await cooldown.anyStateExists(); // Returns true if anything is stored
You can create as many cooldowns as you need โ each with a unique prefix. All state is persisted, isolate-safe, and instantly reusable.
โก Optional useCache
Parameter
Each limiter accepts a useCache
flag:
final cooldown = Cooldown(
'name_key',
duration: Duration(minutes: 5),
useCache: true // false by default
);
-
useCache: false
(default):- Fully isolate-safe
- Reads directly from storage every time
- Best when multiple isolates might read/write the same data
-
useCache: true
:- Uses memory caching for faster access
- Not isolate-safe โ may lead to stale or out-of-sync data across isolates
- Best when used in single-isolate environments (most apps)
โ ๏ธ Warning: Enabling
useCache
disables isolate safety. Use only when you're sure no other isolate accesses the same key.
๐ RateLimiter
Token Bucket Rate Limiter
โคด๏ธ Back -> Table of Contents
RateLimiter
is a high-performance, plug-and-play utility that implements a token bucket algorithm to enforce rate limits โ like โ100 actions per 15 minutesโ โ across sessions, isolates, and app restarts.
It handles:
- Token-based rate limiting
- Automatic time-based token refill
- Persistent state using
prf
types (PrfIso<double>
,PrfIso<DateTime>
) - Async-safe, isolate-compatible behavior
Perfect for chat limits, API quotas, retry windows, or any action frequency cap โ all stored locally.
๐ง How to Use
tryConsume()
โ Tries to use 1 token; returnstrue
if allowed, orfalse
if rate-limitedisLimitedNow()
โ Returnstrue
if no tokens are currently availableisReady()
โ Returnstrue
if at least one token is availablegetAvailableTokens()
โ Returns the current number of usable tokens (calculated live)timeUntilNextToken()
โ Returns aDuration
until at least one token will be availablenextAllowedTime()
โ Returns the exactDateTime
when a token will be availablereset()
โ Resets to full token count and updates last refill to nowremoveAll()
โ Deletes all limiter state (for testing/debugging)anyStateExists()
โ Returnstrue
if limiter data exists in storagerunIfAllowed(action)
โ Runs a callback if allowed, otherwise returnsnull
debugStats()
โ Returns detailed internal stats for logging and debugging
The limiter uses fractional tokens internally to maintain precise refill rates, even across app restarts. No timers or background services required โ it just works.
โ
RateLimiter
Basic Setup
Create a limiter with a key, a maximum number of actions, and a refill duration:
final limiter = RateLimiter(
'chat_send',
maxTokens: 100,
refillDuration: Duration(minutes: 15),
);
This example allows up to 100 actions per 15 minutes. The token count is automatically replenished over time โ even after app restarts.
๐ Check & Consume
To attempt an action:
final canSend = await limiter.tryConsume();
if (canSend) {
// Allowed โ proceed with the action
} else {
// Blocked โ too many actions, rate limit hit
}
Returns true
if a token was available and consumed, or false
if the limit was exceeded.
๐งฎ Get Available Tokens
To check how many tokens are usable at the moment:
final tokens = await limiter.getAvailableTokens();
print('Tokens left: ${tokens.toStringAsFixed(2)}');
Useful for debugging, showing rate limit progress, or enabling/disabling UI actions.
โณ Time Until Next Token
To wait or show feedback until the next token becomes available:
final waitTime = await limiter.timeUntilNextToken();
print('Try again in: ${waitTime.inSeconds}s');
You can also get the actual time point:
final nextTime = await limiter.nextAllowedTime();
๐ Reset the Limiter
To fully refill the bucket and reset the refill clock:
await limiter.reset();
Use this after manual overrides, feature unlocks, or privileged user actions.
๐งผ Clear All Stored State
To wipe all saved token/refill data (for debugging or tests):
await limiter.removeAll();
To check if the limiter has any stored state:
final exists = await limiter.anyStateExists();
โก Optional useCache
Parameter
Each limiter accepts a useCache
flag:
final limiter = RateLimiter(
'key',
maxTokens: 10,
refillDuration: Duration(minutes: 5),
useCache: true // false by default
);
-
useCache: false
(default):- Fully isolate-safe
- Reads directly from storage every time
- Best when multiple isolates might read/write the same data
-
useCache: true
:- Uses memory caching for faster access
- Not isolate-safe โ may lead to stale or out-of-sync data across isolates
- Best when used in single-isolate environments (most apps)
โ ๏ธ Warning: Enabling
useCache
disables isolate safety. Use only when you're sure no other isolate accesses the same key.
๐ License MIT ยฉ Jozz
Libraries
- limit
limit
Package Barrel File- services/cooldown
- services/rate_limiter