> ## Documentation Index
> Fetch the complete documentation index at: https://docs.doman.id/llms.txt
> Use this file to discover all available pages before exploring further.

# SSE Implementation for Notification

This is an excellent use case for Server-Sent Events (SSE). It's much more efficient than polling, as it keeps a single, long-lived HTTP connection open for the server to push updates to the client.

Here is a complete step-by-step implementation for your Laravel 10 and Vue 2 application.

### The Flow

1. A user visits a page with the notification component.
2. The Vue component opens an SSE connection to a specific Laravel route (e.g., `/notifications/stream`).
3. The Laravel controller keeps this connection open.
4. When a new `Notification` is created in your MongoDB collection, the `NotificationObserver` fires.
5. The observer's `created` method will write the new notification data to the cache with a user-specific key.
6. The `SseController`, in its long-running loop, detects the new cache entry, sends it down the stream to the browser, and then deletes the cache entry.
7. Vue's `EventSource` listener receives the data and updates the UI in real-time.
8. The "Refresh" button will call a separate, standard API endpoint to fetch all notifications, just like your old polling mechanism.

***

### Step 1: Backend - The Model and Observer

First, let's set up the model and the observer that will react to new database entries.

#### 1a. Notification Model

I'll assume you are using the `jenssegers/laravel-mongodb` package. Your model might look something like this.

**`app/Models/Notification.php`**

```php theme={null}
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Jenssegers\Mongodb\Eloquent\Model;

class Notification extends Model
{
    use HasFactory;

    // Use 'mongodb' connection if you have it configured in config/database.php
    protected $connection = 'mongodb';

    // The collection associated with the model.
    protected $collection = 'notifications';

    protected $fillable = [
        'user_id', // The user who should receive the notification
        'message',
        'read_at',
        'link',
    ];

    protected $casts = [
        'read_at' => 'datetime',
        'user_id' => 'integer', // Cast if your user IDs are integers
    ];

    /**
     * Get the user that owns the notification.
     */
    public function user()
    {
        // Assuming your User model is a standard Eloquent model
        return $this->belongsTo(User::class);
    }
}
```

#### 1b. Create the Observer

This is the core of the trigger mechanism.

Generate the observer using Artisan:

```bash theme={null}
php artisan make:observer NotificationObserver --model=Notification
```

Now, let's implement the `created` method. It will write the new notification to the cache. The cache acts as a fast, temporary message queue between the web request that created the notification and the SSE process.

**`app/Observers/NotificationObserver.php`**

```php theme={null}
<?php

namespace App\Observers;

use App\Models\Notification;
use Illuminate\Support\Facades\Cache;

class NotificationObserver
{
    /**
     * Handle the Notification "created" event.
     *
     * We will cache the new notification with a unique key for the user.
     * The SSE controller will be watching for this cache key.
     */
    public function created(Notification $notification): void
    {
        // We use a user-specific key.
        // The 'pull' method in the controller will retrieve and delete this.
        // We add a unique suffix to handle multiple notifications created in quick succession.
        $key = 'user.' . $notification->user_id . '.new_notification.' . uniqid();

        // Cache for 10 minutes, just in case the SSE process is down.
        Cache::put($key, $notification->toJson(), 600);
    }
}
```

*Note:* We use `uniqid()` to ensure that if two notifications are created for the same user before the SSE loop runs, both are stored in the cache under different keys.

#### 1c. Register the Observer

You must register the observer in a service provider. The best place is `EventServiceProvider`.

**`app/Providers/EventServiceProvider.php`**

```php theme={null}
<?php

namespace App\Providers;

use App\Models\Notification;
use App\Observers\NotificationObserver;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    // ...

    /**
     * Register any events for your application.
     */
    public function boot(): void
    {
        Notification::observe(NotificationObserver::class);
    }

    // ...
}
```

### Step 2: Backend - The SSE Controller and Route

This controller will handle the persistent connection.

#### 2a. Create the Controller

```bash theme={null}
php artisan make:controller SseController
```

#### 2b. Implement the SSE Logic

We'll use Laravel's `StreamedResponse` to send data without closing the connection.

**`app/Http/Controllers/SseController.php`**

```php theme={null}
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Symfony\Component\HttpFoundation\StreamedResponse;

class SseController extends Controller
{
    public function stream()
    {
        $response = new StreamedResponse(function() {
            $user = Auth::user();

            while (true) {
                // 1. Check if the connection is aborted by the client
                if (connection_aborted()) {
                    break;
                }

                // 2. Check for new notification keys in the cache for this user
                $cachePrefix = 'user.' . $user->id . '.new_notification.';
                $keys = Cache::getStore()->getPrefix() . $cachePrefix . '*';

                // In Redis, you'd use Cache::keys('...'), but for file/db cache we scan
                // Note: For high-traffic sites, use Redis for this pattern.
                // For simplicity, this example will work with standard file/db cache.
                $notificationKeys = collect(\Illuminate\Support\Facades\DB::table('cache')
                    ->where('key', 'like', $keys.'%')
                    ->pluck('key')
                    ->map(fn($key) => str_replace(Cache::getStore()->getPrefix(), '', $key))
                );

                foreach ($notificationKeys as $key) {
                    // Use Cache::pull to get the item and delete it atomically
                    $notificationJson = Cache::pull($key);

                    if ($notificationJson) {
                        // 3. Format the data as an SSE message
                        echo "event: new-notification\n"; // Event name
                        echo "id: " . uniqid() . "\n";   // Event ID
                        echo "data: " . $notificationJson . "\n\n"; // The JSON data

                        // 4. Flush the output buffer to send the data to the client
                        ob_flush();
                        flush();
                    }
                }

                // 5. Sleep for a short duration to avoid high CPU usage
                sleep(2);
            }
        });

        // Set the headers for SSE
        $response->headers->set('Content-Type', 'text/event-stream');
        $response->headers->set('X-Accel-Buffering', 'no'); // Nginx-specific, important
        $response->headers->set('Cache-Control', 'no-cache');
        $response->headers->set('Connection', 'keep-alive');

        return $response;
    }
}
```

> **Important:** The cache scanning part is not very performant with the `file` or `database` cache drivers. For production, you should **strongly** consider using `redis`:
> `CACHE_DRIVER=redis` in your `.env` file. With Redis, you could use `Cache::keys($cachePrefix . '*')` which is much more efficient.

#### 2c. Define the Route

This route must be protected by authentication middleware.

**`routes/web.php`**

```php theme={null}
use App\Http\Controllers\SseController;
use App\Http\Controllers\NotificationController; // We'll create this next

// ... other routes

Route::middleware(['auth'])->group(function () {
    // The SSE endpoint
    Route::get('/notifications/stream', [SseController::class, 'stream'])->name('notifications.stream');

    // The manual-refresh endpoint (your old polling endpoint)
    Route::get('/notifications', [NotificationController::class, 'index'])->name('notifications.index');
});
```

### Step 3: Backend - Manual Refresh Endpoint

This is the standard endpoint that the "Refresh" button will use.

#### 3a. Create the Controller

```bash theme={null}
php artisan make:controller NotificationController
```

#### 3b. Implement the Index Method

This method simply fetches all notifications for the current user.

**`app/Http/Controllers/NotificationController.php`**

```php theme={null}
<?php

namespace App\Http\Controllers;

use App\Models\Notification;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class NotificationController extends Controller
{
    /**
     * Fetch all notifications for the authenticated user.
     * This is used for the initial load and manual refresh.
     */
    public function index()
    {
        $notifications = Notification::where('user_id', Auth::id())
            ->latest() // Order by most recent
            ->take(10)   // Limit the number
            ->get();

        return response()->json($notifications);
    }
}
```

### Step 4: Frontend - The Vue 2 Component

Now let's create the Vue component that consumes the SSE stream and provides the manual refresh button.

**`resources/js/components/NotificationBell.vue`**

```vue theme={null}
<template>
  <div class="notification-container">
    <button @click="toggleDropdown" class="bell-button">
      <i class="fa fa-bell"></i> <!-- Assuming you use FontAwesome -->
      <span v-if="notificationCount > 0" class="notification-badge">{{ notificationCount }}</span>
    </button>
    <div v-if="isOpen" class="notification-dropdown">
      <div class="dropdown-header">
        <span>Notifications</span>
        <button @click.prevent="refreshNotifications" class="refresh-button">Refresh</button>
      </div>
      <ul>
        <li v-if="notifications.length === 0" class="no-notifications">
          No new notifications
        </li>
        <li v-for="notification in notifications" :key="notification._id">
          {{ notification.message }}
        </li>
      </ul>
    </div>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  name: 'NotificationBell',
  data() {
    return {
      notifications: [],
      isOpen: false,
      eventSource: null,
    };
  },
  computed: {
    notificationCount() {
      // You might want to filter for unread notifications here
      return this.notifications.length;
    }
  },
  methods: {
    toggleDropdown() {
      this.isOpen = !this.isOpen;
    },

    /**
     * Manual refresh method using Axios (your old polling logic).
     */
    refreshNotifications() {
      console.log('Manually fetching notifications...');
      axios.get('/notifications')
        .then(response => {
          this.notifications = response.data;
        })
        .catch(error => {
          console.error("Error fetching notifications:", error);
        });
    },

    /**
     * Sets up the Server-Sent Events connection.
     */
    connectToSse() {
      // Make sure we don't have an existing connection
      if (this.eventSource) {
        this.eventSource.close();
      }

      console.log('Connecting to SSE stream...');
      this.eventSource = new EventSource('/notifications/stream');

      this.eventSource.onopen = () => {
        console.log('SSE Connection opened.');
      };

      // Listen for our custom 'new-notification' event
      this.eventSource.addEventListener('new-notification', (event) => {
        console.log('New notification received via SSE:', event.data);
        const newNotification = JSON.parse(event.data);

        // Add the new notification to the top of the list
        this.notifications.unshift(newNotification);
      });

      this.eventSource.onerror = (error) => {
        console.error('SSE Error:', error);
        // The browser will automatically try to reconnect, but you might want
        // to close it after several failed attempts.
        this.eventSource.close();
      };
    }
  },
  mounted() {
    // 1. Fetch initial notifications on component mount
    this.refreshNotifications();

    // 2. Connect to the SSE stream for real-time updates
    this.connectToSse();
  },
  beforeDestroy() {
    // VERY IMPORTANT: Close the connection when the component is destroyed
    // to prevent memory leaks and orphaned server processes.
    if (this.eventSource) {
      console.log('Closing SSE connection.');
      this.eventSource.close();
    }
  }
};
</script>

<style scoped>
/* Add some basic styling for the component */
.notification-container {
  position: relative;
  display: inline-block;
}
.bell-button {
  position: relative;
  background: none;
  border: none;
  font-size: 24px;
  cursor: pointer;
}
.notification-badge {
  position: absolute;
  top: -5px;
  right: -5px;
  background-color: red;
  color: white;
  border-radius: 50%;
  padding: 2px 6px;
  font-size: 12px;
}
.notification-dropdown {
  position: absolute;
  right: 0;
  top: 100%;
  width: 300px;
  background: white;
  border: 1px solid #ccc;
  box-shadow: 0 2px 10px rgba(0,0,0,0.1);
  z-index: 1000;
}
.dropdown-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 8px 12px;
  border-bottom: 1px solid #eee;
}
.refresh-button {
  font-size: 12px;
}
ul {
  list-style: none;
  padding: 0;
  margin: 0;
  max-height: 400px;
  overflow-y: auto;
}
li {
  padding: 10px 15px;
  border-bottom: 1px solid #eee;
}
li:last-child {
  border-bottom: none;
}
.no-notifications {
  color: #888;
  text-align: center;
}
</style>
```

You can now use the `<notification-bell></notification-bell>` component anywhere in your Vue application. You've successfully replaced polling with a real-time SSE push system while retaining the ability to manually refresh.
