Skip to content

Conversation

@DarkGhostHunter
Copy link
Contributor

@DarkGhostHunter DarkGhostHunter commented Dec 31, 2025

What?

This introduces the attemptOnce to the Rate Limiter helper, which is just syntax sugar for attempt() with 1 retry while being atomic without the need of atomic locks. It uses the same syntax style that attempt(), with a 60 second default.

The idea is to tun a callback only once inside a given window of time, sharing it execution state across multiple app instances and checkable across the whole app. This also allows to clear the debounce data manually or on failure.

use Illuminate\Support\Facades\Cache;
use App\Alerts\SystemAlert;

// 1. Somewhat irresponsable but works
if (Cache::add('send-sms', true, 60)) {
    SystemAlert::send('Something happened');
}

// 2. Alternative using atomic locks
$lock = Cache::lock('send-sms', 60);

if ($lock->get()) {
    try {
        SystemAlert::send('Something happened');
    } catch (Throwable $throwable) {
    
        $lock->release();
        
        throw $throwable;
    } finally {
        // Do not release the lock on success so the debounce works.
    }
}}

// Much simpler and without atomic locks.
use Illuminate\Support\Facades\RateLimiter;

RateLimiter::attemptOnce('send-sms', fn () => SystemAlert::send('Something happened'));

When the debounced callback is executed, its original result will be returned, otherwise false is returned.

use Illuminate\Support\Facades\RateLimiter;

$result = RateLimiter::attemptOnce('computed', fn () => /* ... */));

if ($result === false) {
    $seconds = RateLimiter::availableIn('computed');
    
    return "The computation was already done, wait $seconds seconds."
}

return 'Compute done!'

Since it uses the Rate Limiter, the status of the key can be checked elsewhere.

use Illuminate\Support\Facades\RateLimiter;

public function form()
{
    return view('sms.send', [
        'remaining' => RateLimiter::availableIn('send-sms'),
    ]);
}

public function send()
{
    RateLimiter::attemptOnce('send-sms', function () {
        // ...
    });
    
    return to_route('sms.send');
}

Caveats

  1. Does not cache the result

This is made to avoid serializing a result, or making the developer mistakenly not returning anything (null of void. I assume the developer will also want the fresh result of the callback instead of stale data, otherwise he would use Cache::remember() or have more control with an Atomic Lock.

  1. Attempt is cleared on exception

If the callback fails, the attempt is cleared. If the developer requires to not do so, catching the exception inside the function is required, like using with rescue().

use App\Alerts\SystemAlert;
use App\Alerts\Exceptions\ThrottledException;
use Illuminate\Support\Facades\RateLimiter;

RateLimiter::attemptOnce('computed', function () {
    rescue(fn () => SystemAlert::send('Something happened'));
});

@taylorotwell
Copy link
Member

I think the idea is cool but not sure on the naming. To me debounce means something slightly different, and it definitely means something different in popular libraries like Lodash. Unfortunately I don't have any better suggestions atm.

@DarkGhostHunter
Copy link
Contributor Author

DarkGhostHunter commented Jan 1, 2026

I think the idea is cool but not sure on the naming. To me debounce means something slightly different, and it definitely means something different in popular libraries like Lodash. Unfortunately I don't have any better suggestions atm.

I have in mind "pace" and "once" and "oncePer". That's as far as my brain can go. BTW, Happy new year.

@Rizky92
Copy link
Contributor

Rizky92 commented Jan 2, 2026

I think of retain or hold for singular word. But the clearest would be attemptOnce.

@taylorotwell
Copy link
Member

Think I would prefer just releasing this as a single function package for now.

@DarkGhostHunter
Copy link
Contributor Author

There, I simplified the logic and use attemptOnce, which sounds better to me. Let's see what @taylorotwell thinks about it now.

@DarkGhostHunter
Copy link
Contributor Author

Ffs, browser didn't update the PR status. @taylorotwell care to reconsider?

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.

3 participants