by zackaj
Laravel-debounce allows you to accumulate / debounce a job, notification or command to avoid spamming your users and your app's queue.
It also tracks and registers every request occurrence and gives you a nice report tracking with information like ip address and authenticated user per request.
This laravel package uses UniqueJobs (atomic locks) and caching to run only one instance of a task in a debounced interval of x seconds delay.
Everytime a new activity is recorded (occurrence), the execution is delayed by x seconds.
- Debounce Notifications, Jobs and Artisan Commands Basic usage & Advanced usage
 - Report Tracking
 - Bonus CLI Debounce
 
A debounced notification to bulk notify users about new uploaded files.
debounce_compressed.mp4
See Code
FileUploaded.php
<?php
namespace App\Notifications;
use App\Models\File;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
class FileUploaded extends Notification
{
    use Queueable;
    public function __construct(public File $file) {}
    public function via(object $notifiable): array
    {
        return ['database'];
    }
    public function toArray(object $notifiable): array
    {
        return [
            'files' => $this->file->user->files()
                ->where('created_at', '>=', $this->file->created_at)
                ->get(),
        ];
    }
}DemoController.php
<?php
namespace App\Http\Controllers;
use App\Models\File;
use App\Models\User;
use App\Notifications\FileUploaded;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Notification;
use Zackaj\LaravelDebounce\Facades\Debounce;
class DemoController extends Controller
{
    public function normalNotification(Request $request)
    {
        $user = $request->user();
        $file = File::factory()->create(['user_id' => $user->id]);
        $otherUsers = User::query()->whereNot('id', $user->id)->get();
        Notification::send($otherUsers, new FileUploaded($file));
        return back();
    }
    public function debounceNotification(Request $request)
    {
        $user = $request->user();
        $file = File::factory()->create(['user_id' => $user->id]);
        $otherUsers = User::query()->whereNot('id', $user->id)->get();
        Debounce::notification(
            notifiables: $otherUsers,
            notification:new FileUploaded($file),
            delay: 5,
            uniqueKey:$user->id,
        );
        return back();
    }
}- Laravel application (> 10.x)
 - Up and running cache system that supports atomic locks
 - Up and running queue worker
 
  composer require zackaj/laravel-debounceYou can debounce existing jobs, notifications and commands with zero setup.
Warning you can't access report tracking without extending the package's classes, see Advanced usage.
use Zackaj\LaravelDebounce\Facades\Debounce;
//job
Debounce::job(
    job:new Job(),//replace
    delay:5,//delay in seconds
    uniqueKey:auth()->user()->id,//debounce per Job class name + uniqueKey
    sync:false, //optional, job will be fired to the queue
);
//notification
Debounce::notification(
    notifiables: auth()->user(),
    notification: new Notification(),//replace
    delay: 5,
    uniqueKey: auth()->user()->id,
    sendNow: false,
);
//command
Debounce::command(
    command: new Command(),//replace
    delay: 5,
    uniqueKey: $request->ip(),
    parameters: ['name' => 'zackaj'],//see Artisan::call() signature
    toQueue: false,//optional, send command to the queue when executed
    outputBuffer: null,//optional, //see Artisan::call() signature
);In order to use:
your existing jobs, notifications and commands must extend:
use Zackaj\LaravelDebounce\DebounceJob;
use Zackaj\LaravelDebounce\DebounceNotification;
use Zackaj\LaravelDebounce\DebounceCommand;or just generate new ones using the available make commands.
- Notification
 
php artisan make:debounce-notification TestNotification- Job
 
php artisan make:debounce-job TestJob- Command
 
php artisan make:debounce-command TestCommandAlternatively, now you can debounce from the job, notification and command instances directly without using the Debounce facade used in Basic usage
(new Job())->debounce(...);
(new Notification())->debounce(...);
(new Command())->debounce(...);Laravel-debounce uses the cache to store every request occurrence, use getReport() method within your debounceables to access the report chain that has a collection of occurrences.
Every report will have one occurrence minimum.
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Zackaj\LaravelDebounce\DebounceJob;
class Jobless extends DebounceJob implements ShouldQueue
{
    use Dispatchable;
    public function handle(): void
    {
        $this->getReport()->occurrences;//collection of occurrences
        $this->getReport()->occurrences->count();
        $this->getReport()->occurrences->first()->happenedAt;
        $this->getReport()->occurrences->first()->ip;
        $this->getReport()->occurrences->first()->ips;
        $this->getReport()->occurrences->first()->requestHeaders;//HeaderBag
        $this->getReport()->occurrences->first()->user;//authenticated user | null
    }
}If you wish to run some code before and/or after firing the debounceables you can use the available hooks.
Important: after() hook could run before your debounceable is handled if it's sent to the queue when:
sendNow==falseand your notificationimplements ShouldQueuesync==falseand your jobimplements ShouldQueuetoQueue==true(command)
see: Basic usage
<?php
...
class Jobless extends DebounceJob implements ShouldQueue
{
...
    public function before(): void
    {
        //run before dispatching the job
    }
    public function after(): void
    {
        //run after dispatching the job
    }
}You get the $notifiables injected into the hooks.
<?php
...
class FileUploaded extends DebounceNotification
{
...
    public function before($notifiables): void
    {
        //run before sending the notification
    }
    public function after($notifiables): void
    {
        //run after sending the notification
    }
}Due to limitations, the hook methods must be static.
<?php
...
class Test extends DebounceCommand
{
...
    public static function before(): void
    {
        //run before executing the command
    }
    public static function after(): void
    {
        //run after executing the command
    }
}By default laravel-debounce debounces from the last occurrence happenedAt timestamp
public function getLastActivityTimestamp(): ?Carbon
{
    return $this->getReport()->occurrences->last()->happenedAt;
}You can override this method in your debounceables in order to debounce from a custom timestamp of your choice. If null is returned the debouncer will fallback to the default implementation above.
<?php
...
class Jobless extends DebounceJob implements ShouldQueue
{
...
    public function getLastActivityTimestamp(): ?Carbon
    {
        return Message::latest()->first()?->seen_at;
    }
}You get the $notifiables injected into the method.
<?php
...
class FileUploaded extends DebounceNotification
{
...
    public function getLastActivityTimestamp(mixed $notifiables): ?Carbon
    {
        return $this->file->user->files->latest()->first()?->created_at;
    }
}Due to limitations, the method must be static.
<?php
...
class Test extends DebounceCommand
{
...
    public static function getLastActivityTimestamp(): ?Carbon
    {
        return User::latest()->first()?->created_at;
    }
}For fun, you can actually debounce commands from the CLI using the debounce:command Artisan command.
php artisan debounce:command 5 uniqueKey app:testhere's the signature for the command:
php artisan debounce:command {delay} {uniqueKey} {signature*}
I recommend using Laravel telescope to see the debouncer live in the queues tab and to debug any failures.
- Unique lock gets stuck sometimes when jobs fail github issue, I made a fix to the laravel core framework about this give it a reaction: PR (merged)
- cause: this happens when deleted models are unserialized causing the job to fail without clearing the lock.
 - solution: don't use 
SerializesModelstrait on Notifications/Jobs. (old temporary solution, now the bug is fixed) 
 
Contributions, issues and suggestions are always welcome! See contributing.md for ways to get started.
