diff --git a/src/Websocket/Controller.php b/src/Websocket/Controller.php index ffb86f8..9bdd8e3 100644 --- a/src/Websocket/Controller.php +++ b/src/Websocket/Controller.php @@ -15,7 +15,6 @@ use Illuminate\Support\Facades\Log; class Controller { protected bool $isMockConnection; - protected MockConnectionSocketPair|null $mockConnectionClone = null; final public function __construct( protected ConnectionInterface $connection, @@ -23,14 +22,7 @@ class Controller protected string $event, protected LocalChannelManager|RedisChannelManager $channelManager ) { - // Cache class check to avoid repeated get_class() calls (reflection is slow) - $connectionClass = get_class($connection); - $this->isMockConnection = $connectionClass === MockConnectionSocketPair::class; - - // Pre-clone MockConnection once if needed (reuse across method calls) - if ($this->isMockConnection) { - $this->mockConnectionClone = clone $connection; - } + $this->isMockConnection = $connection instanceof MockConnectionSocketPair; } /** @@ -167,11 +159,7 @@ class Controller // Pre-encode once (avoid repeated encoding) $encoded = json_encode($p); - if ($this->isMockConnection) { - $this->mockConnectionClone->send($encoded); - } else { - $this->connection->send($encoded); - } + $this->connection->send($encoded); return true; } @@ -198,11 +186,7 @@ class Controller // Pre-encode once (avoid repeated encoding) $encoded = json_encode($p); - if ($this->isMockConnection) { - $this->mockConnectionClone->send($encoded); - } else { - $this->connection->send($encoded); - } + $this->connection->send($encoded); return true; } @@ -240,14 +224,7 @@ class Controller Log::channel('websocket')->error('Send error: ' . @$p['data']['message'], $p); - // Pre-encode once (avoid repeated encoding) - $encoded = json_encode($p); - - if ($this->isMockConnection) { - $this->mockConnectionClone->send($encoded); - } else { - $this->connection->send($encoded); - } + $this->connection->send(json_encode($p)); return true; } @@ -272,30 +249,27 @@ class Controller 'channel' => $channel, ]; - if (!$this->isMockConnection) { - if (! $channel) { - $this->error('Channel not found'); - return; + if ($this->isMockConnection) { + $this->connection->broadcast($p, $channel, $including_self); + return; + } + + // Direct broadcast (non-forked context, e.g. testing) + if (! $channel) { + $this->error('Channel not found'); + return; + } + + $encoded = json_encode($p); + + foreach ($this->channel->getConnections() as $channel_conection) { + if ($channel_conection !== $this->connection) { + $channel_conection->send($encoded); } + } - // Pre-encode ONCE - massive improvement for 100+ connections - $encoded = json_encode($p); - - foreach ($this->channel->getConnections() as $channel_conection) { - if ($channel_conection !== $this->connection) { - $channel_conection->send($encoded); - } - - if ($including_self) { - $this->connection->send($encoded); - } - } - } else { - $this->mockConnectionClone->broadcast( - $p, - $channel, - $including_self - ); + if ($including_self) { + $this->connection->send($encoded); } } @@ -319,36 +293,28 @@ class Controller 'channel' => $channel, ]; - if (!$this->isMockConnection) { - // 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 = []; - - // 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, - $socketIds, - $channel - ); + if ($this->isMockConnection) { + $this->connection->whisper($p, $socketIds, $channel); + return; } + + // Direct whisper (non-forked context, e.g. testing) + $encoded = json_encode($p); + $socketIdLookup = array_flip($socketIds); + $sentTo = []; + + $this->channelManager->getLocalConnections()->then(function ($connections) use ($socketIdLookup, $encoded, &$sentTo) { + foreach ($connections as $connection) { + if (isset($sentTo[$connection->socketId])) { + continue; + } + + if (isset($socketIdLookup[$connection->socketId])) { + $connection->send($encoded); + $sentTo[$connection->socketId] = true; + } + } + }); } private static function send_error( diff --git a/src/Websocket/Handler.php b/src/Websocket/Handler.php index ae7d81c..34a81d5 100644 --- a/src/Websocket/Handler.php +++ b/src/Websocket/Handler.php @@ -11,14 +11,12 @@ use BlaxSoftware\LaravelWebSockets\Channels\PrivateChannel; use BlaxSoftware\LaravelWebSockets\Contracts\ChannelManager; use BlaxSoftware\LaravelWebSockets\Events\ConnectionClosed; use BlaxSoftware\LaravelWebSockets\Events\NewConnection; -use BlaxSoftware\LaravelWebSockets\Exceptions\WebSocketException; use BlaxSoftware\LaravelWebSockets\Ipc\SocketPairIpc; use BlaxSoftware\LaravelWebSockets\Websocket\MockConnectionSocketPair; use BlaxSoftware\LaravelWebSockets\Server\Exceptions\ConnectionsOverCapacity; use BlaxSoftware\LaravelWebSockets\Server\Exceptions\OriginNotAllowed; use BlaxSoftware\LaravelWebSockets\Server\Exceptions\UnknownAppKey; use BlaxSoftware\LaravelWebSockets\Server\Exceptions\WebSocketException as ExceptionsWebSocketException; -use BlaxSoftware\LaravelWebSockets\Server\Messages\PusherMessageFactory; use BlaxSoftware\LaravelWebSockets\Server\QueryParameters; use Exception; use Illuminate\Support\Facades\Auth; @@ -61,6 +59,11 @@ class Handler implements MessageComponentInterface */ private static ?bool $hotReload = null; + /** + * Whether debug mode is enabled (cached to avoid container resolution per message) + */ + private static ?bool $debug = null; + /** * Initialize a new handler. */ @@ -176,7 +179,7 @@ class Handler implements MessageComponentInterface $this->authenticateConnection($connection, $channel, $messageArray); // Only log in debug mode to reduce I/O - if (config('app.debug')) { + if (self::$debug ??= (bool) config('app.debug')) { Log::channel('websocket')->debug('[' . $connection->socketId . ']@' . $channel->getName() . ' | ' . $payload); } @@ -552,36 +555,35 @@ class Handler implements MessageComponentInterface return; } - // If it's already a string (JSON), try to parse for broadcast/whisper - if (is_string($data)) { - $bm = json_decode($data, true); - - if (isset($bm['broadcast']) && $bm['broadcast']) { - $this->broadcast( - $connection->app->id, - $bm['data'] ?? null, - $bm['event'] ?? null, - $bm['channel'] ?? null, - $bm['including_self'] ?? false, - $connection - ); - return; - } - - if (isset($bm['whisper']) && $bm['whisper']) { - $this->whisper( - $connection->app->id, - $bm['data'] ?? null, - $bm['event'] ?? null, - $bm['socket_ids'] ?? [], - $bm['channel'] ?? null, - ); - return; - } - - // Regular response - $connection->send($data); + // Prefix-based routing: B: = broadcast, W: = whisper, else regular response + // Avoids JSON decode overhead for regular responses (most common path) + if (str_starts_with($data, 'B:')) { + $bm = json_decode(substr($data, 2), true); + $this->broadcast( + $connection->app->id, + $bm['data'] ?? null, + $bm['event'] ?? null, + $bm['channel'] ?? null, + $bm['including_self'] ?? false, + $connection + ); + return; } + + if (str_starts_with($data, 'W:')) { + $bm = json_decode(substr($data, 2), true); + $this->whisper( + $connection->app->id, + $bm['data'] ?? null, + $bm['event'] ?? null, + $bm['socket_ids'] ?? [], + $bm['channel'] ?? null, + ); + return; + } + + // Regular response - send directly without JSON decode + $connection->send($data); } protected function handleMessageError(\Throwable $e): void @@ -902,11 +904,22 @@ class Handler implements MessageComponentInterface PrivateChannel|Channel|PresenceChannel|null $channel, $message = [] ) { + // Fast path: auth already resolved for this connection (skips cache read + DB query + cache write) + if (isset($connection->authLoaded)) { + if ($connection->user) { + Auth::login($connection->user); + } + $this->scheduleLogout(); + return; + } + $this->loadCachedAuth($connection, $channel); $this->ensureUserIsSet($connection, $channel); $this->updateAuthState($connection); $this->cacheAuthenticatedUser($connection); $this->scheduleLogout(); + + $connection->authLoaded = true; } protected function loadCachedAuth(ConnectionInterface $connection, $channel): void @@ -958,7 +971,6 @@ class Handler implements MessageComponentInterface /** @var \App\Models\User */ $user = Auth::user(); - $user->refresh(); $socketId = $connection->socketId; @@ -1055,23 +1067,23 @@ class Handler implements MessageComponentInterface bool $including_self = false, $connection = null ): void { - $channel = $this->channelManager->findOrCreate($appId, $channel); - $p = [ - 'event' => ($event ?? $event), + // Pre-encode once for all connections + $encoded = json_encode([ + 'event' => $event, 'data' => $payload, 'channel' => $channel->getName(), - ]; + ]); foreach ($channel->getConnections() as $channel_conection) { if ($channel_conection->socketId !== $connection->socketId) { - $channel_conection->send(json_encode($p)); + $channel_conection->send($encoded); } + } - if ($including_self) { - $connection->send(json_encode($p)); - } + if ($including_self) { + $connection->send($encoded); } } diff --git a/src/Websocket/MockConnectionSocketPair.php b/src/Websocket/MockConnectionSocketPair.php index a6f01d6..8e59c4d 100644 --- a/src/Websocket/MockConnectionSocketPair.php +++ b/src/Websocket/MockConnectionSocketPair.php @@ -44,11 +44,12 @@ class MockConnectionSocketPair implements ConnectionInterface bool $including_self = false, ): self { $data ??= []; - $data['broadcast'] = true; $data['channel'] ??= $channel; $data['including_self'] = $including_self; - return $this->send(json_encode($data)); + // B: prefix for instant routing in parent (avoids JSON decode for regular responses) + $this->ipc->sendToParent('B:' . json_encode($data)); + return $this; } /** @@ -61,11 +62,12 @@ class MockConnectionSocketPair implements ConnectionInterface ?string $channel = null, ): self { $data ??= []; - $data['whisper'] = true; $data['channel'] ??= $channel; $data['socket_ids'] = $socketIds; - return $this->send(json_encode($data)); + // W: prefix for instant routing in parent (avoids JSON decode for regular responses) + $this->ipc->sendToParent('W:' . json_encode($data)); + return $this; } public function close(): void