pool = new SseConnectionPool(); $this->broadcaster = new SseBroadcaster($this->pool); }); describe('Basic Broadcasting', function () { it('broadcasts raw SSE event to channel', function () { $channel = SseChannel::forUser('user123'); $stream = new MockSseStream(); $connection = new SseConnection( connectionId: ConnectionId::generate(), stream: $stream, channels: [$channel], userId: 'user123' ); $this->pool->add($connection); $event = new SseEvent( data: json_encode(['message' => 'test']), event: 'custom-event' ); $sent = $this->broadcaster->broadcast($event, $channel); expect($sent)->toBe(1); expect($stream->getSentEventsCount())->toBe(1); }); it('broadcasts to multiple channels at once', function () { $userChannel = SseChannel::forUser('user123'); $systemChannel = SseChannel::system(); $stream1 = new MockSseStream(); $connection1 = new SseConnection( connectionId: ConnectionId::generate(), stream: $stream1, channels: [$userChannel] ); $stream2 = new MockSseStream(); $connection2 = new SseConnection( connectionId: ConnectionId::generate(), stream: $stream2, channels: [$systemChannel] ); $this->pool->add($connection1); $this->pool->add($connection2); $event = new SseEvent(data: 'test', event: 'test'); $sent = $this->broadcaster->broadcastToMultipleChannels( $event, [$userChannel, $systemChannel] ); expect($sent)->toBe(2); }); it('broadcasts JSON data', function () { $channel = SseChannel::system(); $stream = new MockSseStream(); $connection = new SseConnection( connectionId: ConnectionId::generate(), stream: $stream, channels: [$channel] ); $this->pool->add($connection); $sent = $this->broadcaster->broadcastJson( ['foo' => 'bar', 'value' => 123], $channel, 'data-event' ); expect($sent)->toBe(1); $events = $stream->getSentEventsByType('data-event'); expect($events)->toHaveCount(1); $eventArray = array_values($events); // Re-index array $data = json_decode($eventArray[0]->data, true); expect($data['foo'])->toBe('bar'); expect($data['value'])->toBe(123); }); }); describe('Event Batching', function () { it('enables event batching with configuration', function () { $this->broadcaster->enableBatching(maxBatchSize: 5, maxBatchDelayMs: 50); $channel = SseChannel::system(); $stream = new MockSseStream(); $connection = new SseConnection( connectionId: ConnectionId::generate(), stream: $stream, channels: [$channel] ); $this->pool->add($connection); // Send 3 events (below batch size) for ($i = 0; $i < 3; $i++) { $event = new SseEvent(data: "event-{$i}", event: 'test'); $this->broadcaster->broadcast($event, $channel); } // Events should be batched, not sent yet expect($stream->getSentEventsCount())->toBe(0); // Flush manually $this->broadcaster->flushAll(); // Now should have 1 batch event expect($stream->getSentEventsCount())->toBe(1); $batchEvents = $stream->getSentEventsByType('batch'); expect($batchEvents)->toHaveCount(1); $eventArray = array_values($batchEvents); $batchData = json_decode($eventArray[0]->data, true); expect($batchData['type'])->toBe('batch'); expect($batchData['count'])->toBe(3); expect($batchData['events'])->toHaveCount(3); }); it('auto-flushes when batch size reached', function () { $this->broadcaster->enableBatching(maxBatchSize: 3, maxBatchDelayMs: 1000); $channel = SseChannel::system(); $stream = new MockSseStream(); $connection = new SseConnection( connectionId: ConnectionId::generate(), stream: $stream, channels: [$channel] ); $this->pool->add($connection); // Send exactly batch size for ($i = 0; $i < 3; $i++) { $event = new SseEvent(data: "event-{$i}", event: 'test'); $sent = $this->broadcaster->broadcast($event, $channel); if ($i < 2) { expect($sent)->toBe(0); // Not flushed yet } else { expect($sent)->toBe(1); // Auto-flushed on 3rd event } } expect($stream->getSentEventsCount())->toBe(1); }); it('disables batching and flushes pending events', function () { $this->broadcaster->enableBatching(maxBatchSize: 10, maxBatchDelayMs: 100); $channel = SseChannel::system(); $stream = new MockSseStream(); $connection = new SseConnection( connectionId: ConnectionId::generate(), stream: $stream, channels: [$channel] ); $this->pool->add($connection); // Add some events to batch for ($i = 0; $i < 3; $i++) { $event = new SseEvent(data: "event-{$i}", event: 'test'); $this->broadcaster->broadcast($event, $channel); } expect($stream->getSentEventsCount())->toBe(0); // Disable batching - should flush $this->broadcaster->disableBatching(); expect($stream->getSentEventsCount())->toBe(1); // New events should be sent immediately $event = new SseEvent(data: 'immediate', event: 'test'); $sent = $this->broadcaster->broadcast($event, $channel); expect($sent)->toBe(1); expect($stream->getSentEventsCount())->toBe(2); }); }); describe('Component Updates', function () { it('broadcasts component update', function () { $componentId = ComponentId::fromString('todo-list:instance-123'); $channel = SseChannel::forComponent($componentId); $stream = new MockSseStream(); $connection = new SseConnection( connectionId: ConnectionId::generate(), stream: $stream, channels: [$channel] ); $this->pool->add($connection); $sent = $this->broadcaster->broadcastComponentUpdate( $componentId, state: ['count' => 5], html: '
Updated HTML
', events: ['item-added'] ); expect($sent)->toBe(1); $events = $stream->getSentEventsByType('component-update'); expect($events)->toHaveCount(1); $eventArray = array_values($events); $data = json_decode($eventArray[0]->data, true); expect($data['componentId'])->toBe('todo-list:instance-123'); expect($data['state']['count'])->toBe(5); expect($data['html'])->toBe('
Updated HTML
'); expect($data['events'])->toBe(['item-added']); }); it('broadcasts component fragments', function () { $componentId = ComponentId::fromString('todo-list:instance-123'); $channel = SseChannel::forComponent($componentId); $stream = new MockSseStream(); $connection = new SseConnection( connectionId: ConnectionId::generate(), stream: $stream, channels: [$channel] ); $this->pool->add($connection); $sent = $this->broadcaster->broadcastComponentFragments( $componentId, fragments: [ 'header' => '

Updated

', 'footer' => '

New footer

' ], state: ['count' => 10], events: [] ); expect($sent)->toBe(1); $events = $stream->getSentEventsByType('component-fragments'); expect($events)->toHaveCount(1); $eventArray = array_values($events); $data = json_decode($eventArray[0]->data, true); expect($data['componentId'])->toBe('todo-list:instance-123'); expect($data['fragments']['header'])->toBe('

Updated

'); expect($data['fragments']['footer'])->toBe('

New footer

'); }); }); describe('Presence Broadcasting', function () { it('broadcasts presence update', function () { $channel = SseChannel::presence('room-123'); $stream = new MockSseStream(); $connection = new SseConnection( connectionId: ConnectionId::generate(), stream: $stream, channels: [$channel] ); $this->pool->add($connection); $sent = $this->broadcaster->broadcastPresence( room: 'room-123', userId: 'user456', status: 'online', metadata: ['name' => 'John Doe'] ); expect($sent)->toBe(1); $events = $stream->getSentEventsByType('presence'); expect($events)->toHaveCount(1); $eventArray = array_values($events); $data = json_decode($eventArray[0]->data, true); expect($data['userId'])->toBe('user456'); expect($data['status'])->toBe('online'); expect($data['metadata']['name'])->toBe('John Doe'); expect($data['timestamp'])->toBeInt(); }); }); describe('Notification Broadcasting', function () { it('broadcasts notification to user', function () { $channel = SseChannel::forUser('user123'); $stream = new MockSseStream(); $connection = new SseConnection( connectionId: ConnectionId::generate(), stream: $stream, channels: [$channel], userId: 'user123' ); $this->pool->add($connection); $sent = $this->broadcaster->broadcastNotification( userId: 'user123', title: 'New Message', message: 'You have a new message', type: 'info', data: ['messageId' => 789] ); expect($sent)->toBe(1); $events = $stream->getSentEventsByType('notification'); expect($events)->toHaveCount(1); $eventArray = array_values($events); $data = json_decode($eventArray[0]->data, true); expect($data['title'])->toBe('New Message'); expect($data['message'])->toBe('You have a new message'); expect($data['type'])->toBe('info'); expect($data['data']['messageId'])->toBe(789); }); }); describe('Progress Broadcasting', function () { it('broadcasts progress update', function () { $channel = SseChannel::forUser('user123'); $stream = new MockSseStream(); $connection = new SseConnection( connectionId: ConnectionId::generate(), stream: $stream, channels: [$channel], userId: 'user123' ); $this->pool->add($connection); $sent = $this->broadcaster->broadcastProgress( userId: 'user123', taskId: 'export-123', percent: 75, message: 'Exporting data...', data: ['rows_processed' => 7500] ); expect($sent)->toBe(1); $events = $stream->getSentEventsByType('progress'); expect($events)->toHaveCount(1); $eventArray = array_values($events); $data = json_decode($eventArray[0]->data, true); expect($data['taskId'])->toBe('export-123'); expect($data['percent'])->toBe(75); expect($data['message'])->toBe('Exporting data...'); expect($data['data']['rows_processed'])->toBe(7500); }); }); describe('System Announcements', function () { it('broadcasts system announcement to all clients', function () { $channel = SseChannel::system(); // Create multiple connections $streams = []; for ($i = 0; $i < 3; $i++) { $stream = new MockSseStream(); $streams[] = $stream; $connection = new SseConnection( connectionId: ConnectionId::generate(), stream: $stream, channels: [$channel] ); $this->pool->add($connection); } $sent = $this->broadcaster->broadcastSystemAnnouncement( message: 'System maintenance in 10 minutes', level: 'warning', data: ['scheduled_time' => '2024-01-15T10:00:00Z'] ); expect($sent)->toBe(3); foreach ($streams as $stream) { $events = $stream->getSentEventsByType('system-announcement'); expect($events)->toHaveCount(1); $eventArray = array_values($events); $data = json_decode($eventArray[0]->data, true); expect($data['message'])->toBe('System maintenance in 10 minutes'); expect($data['level'])->toBe('warning'); } }); }); describe('Statistics', function () { it('retrieves connection pool stats', function () { $stream = new MockSseStream(); $connection = new SseConnection( connectionId: ConnectionId::generate(), stream: $stream, channels: [SseChannel::system()], userId: 'user123' ); $this->pool->add($connection); $stats = $this->broadcaster->getStats(); expect($stats['total_connections'])->toBe(1); expect($stats['authenticated_connections'])->toBe(1); expect($stats['anonymous_connections'])->toBe(0); }); }); describe('Edge Cases', function () { it('handles broadcasting to channel with no connections', function () { $channel = SseChannel::forUser('nonexistent'); $event = new SseEvent(data: 'test', event: 'test'); $sent = $this->broadcaster->broadcast($event, $channel); expect($sent)->toBe(0); }); it('broadcasts to multiple users with different channels', function () { $user1Channel = SseChannel::forUser('user1'); $user2Channel = SseChannel::forUser('user2'); $stream1 = new MockSseStream(); $connection1 = new SseConnection( connectionId: ConnectionId::generate(), stream: $stream1, channels: [$user1Channel], userId: 'user1' ); $stream2 = new MockSseStream(); $connection2 = new SseConnection( connectionId: ConnectionId::generate(), stream: $stream2, channels: [$user2Channel], userId: 'user2' ); $this->pool->add($connection1); $this->pool->add($connection2); // Broadcast to user1 $sent1 = $this->broadcaster->broadcastNotification( 'user1', 'User 1 Notification', 'Message for user 1' ); expect($sent1)->toBe(1); expect($stream1->getSentEventsCount())->toBe(1); expect($stream2->getSentEventsCount())->toBe(0); // Broadcast to user2 $sent2 = $this->broadcaster->broadcastNotification( 'user2', 'User 2 Notification', 'Message for user 2' ); expect($sent2)->toBe(1); expect($stream1->getSentEventsCount())->toBe(1); // Still 1 expect($stream2->getSentEventsCount())->toBe(1); // Now 1 }); }); });