From f7e08a337bb50c6f3c7df1329d63496f6a6ffbd1 Mon Sep 17 00:00:00 2001 From: "Fabian @ Blax Software" Date: Mon, 2 Feb 2026 11:24:54 +0100 Subject: [PATCH] BF whisper, broadcast --- src/Websocket/Controller.php | 30 ++++++++++++------- src/Websocket/Handler.php | 27 ++++++++++++----- src/Websocket/MockConnectionSocketPair.php | 34 ++++++++++++++++++++++ 3 files changed, 72 insertions(+), 19 deletions(-) diff --git a/src/Websocket/Controller.php b/src/Websocket/Controller.php index fea5088..5ec87dc 100644 --- a/src/Websocket/Controller.php +++ b/src/Websocket/Controller.php @@ -15,7 +15,7 @@ use Illuminate\Support\Facades\Log; class Controller { protected bool $isMockConnection; - protected ?MockConnection $mockConnectionClone = null; + protected MockConnection|MockConnectionSocketPair|null $mockConnectionClone = null; final public function __construct( protected ConnectionInterface $connection, @@ -24,7 +24,9 @@ class Controller protected LocalChannelManager|RedisChannelManager $channelManager ) { // Cache class check to avoid repeated get_class() calls (reflection is slow) - $this->isMockConnection = get_class($connection) === MockConnection::class; + $connectionClass = get_class($connection); + $this->isMockConnection = $connectionClass === MockConnection::class + || $connectionClass === MockConnectionSocketPair::class; // Pre-clone MockConnection once if needed (reuse across method calls) if ($this->isMockConnection) { @@ -319,22 +321,28 @@ class Controller ]; if (!$this->isMockConnection) { - if (! $channel) { - $this->error('Channel not found'); - return; - } - // Pre-encode ONCE for all matching sockets $encoded = json_encode($p); // Use array_flip for O(1) lookup instead of O(n) in_array $socketIdLookup = array_flip($socketIds); + $sentTo = []; - foreach ($this->channel->getConnections() as $channel_conection) { - if (isset($socketIdLookup[$channel_conection->socketId])) { - $channel_conection->send($encoded); + // Search ALL connections across ALL channels to find target socket IDs + // This is necessary because whisper targets specific sockets regardless of channel + $this->channelManager->getLocalConnections()->then(function ($connections) use ($socketIdLookup, $encoded, &$sentTo) { + foreach ($connections as $connection) { + // Skip if already sent to this socket (can appear in multiple channels) + if (isset($sentTo[$connection->socketId])) { + continue; + } + + if (isset($socketIdLookup[$connection->socketId])) { + $connection->send($encoded); + $sentTo[$connection->socketId] = true; + } } - } + }); } else { $this->mockConnectionClone->whisper( $p, diff --git a/src/Websocket/Handler.php b/src/Websocket/Handler.php index df3e1bf..7e04706 100644 --- a/src/Websocket/Handler.php +++ b/src/Websocket/Handler.php @@ -1210,21 +1210,32 @@ class Handler implements MessageComponentInterface mixed $payload, ?string $event = null, array $socketIds = [], - ?string $channel = null + ?string $channelName = null ): void { - $channel = $this->channelManager->findOrCreate($appId, $channel); - $p = [ 'event' => ($event ?? $event), 'data' => $payload, - 'channel' => $channel->getName(), + 'channel' => $channelName, ]; $socketIdLookup = array_flip($socketIds); - foreach ($channel->getConnections() as $channel_conection) { - if (isset($socketIdLookup[$channel_conection->socketId])) { - $channel_conection->send(json_encode($p)); + $encoded = json_encode($p); + $sentTo = []; + + // Search ALL connections across ALL channels to find target socket IDs + // This is necessary because whisper targets specific sockets regardless of channel + $this->channelManager->getLocalConnections()->then(function ($connections) use ($socketIdLookup, $encoded, &$sentTo) { + foreach ($connections as $connection) { + // Skip if already sent to this socket (can appear in multiple channels) + if (isset($sentTo[$connection->socketId])) { + continue; + } + + if (isset($socketIdLookup[$connection->socketId])) { + $connection->send($encoded); + $sentTo[$connection->socketId] = true; + } } - } + }); } } diff --git a/src/Websocket/MockConnectionSocketPair.php b/src/Websocket/MockConnectionSocketPair.php index db7630f..a6f01d6 100644 --- a/src/Websocket/MockConnectionSocketPair.php +++ b/src/Websocket/MockConnectionSocketPair.php @@ -34,6 +34,40 @@ class MockConnectionSocketPair implements ConnectionInterface return $this; } + /** + * Broadcast a message to all connections in a channel. + * Serializes the data for the parent process to handle. + */ + public function broadcast( + $data, + ?string $channel = null, + bool $including_self = false, + ): self { + $data ??= []; + $data['broadcast'] = true; + $data['channel'] ??= $channel; + $data['including_self'] = $including_self; + + return $this->send(json_encode($data)); + } + + /** + * Whisper a message to specific socket IDs. + * Serializes the data for the parent process to handle. + */ + public function whisper( + $data, + array $socketIds, + ?string $channel = null, + ): self { + $data ??= []; + $data['whisper'] = true; + $data['channel'] ??= $channel; + $data['socket_ids'] = $socketIds; + + return $this->send(json_encode($data)); + } + public function close(): void { // No-op for mock