From e2af288a07b2e113dc940ac57438779b1bc631e8 Mon Sep 17 00:00:00 2001 From: Naneynonn Date: Sun, 25 May 2025 20:29:30 +0300 Subject: [PATCH 1/8] Added zlib compression and fixed bug 1003 on startup --- src/Gateway/Connection.php | 50 ++++++++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/src/Gateway/Connection.php b/src/Gateway/Connection.php index 147c395..e80b44d 100644 --- a/src/Gateway/Connection.php +++ b/src/Gateway/Connection.php @@ -14,7 +14,6 @@ use Ragnarok\Fenrir\Constants\WebsocketEvents; use Ragnarok\Fenrir\DataMapper; use Ragnarok\Fenrir\EventHandler; -use Ragnarok\Fenrir\Gateway\Events\Meta\MetaEvent; use Ragnarok\Fenrir\Gateway\Handlers\HeartbeatAcknowledgedEvent; use Ragnarok\Fenrir\Gateway\Handlers\IdentifyHelloEvent; use Ragnarok\Fenrir\Gateway\Handlers\IdentifyResumeEvent; @@ -41,22 +40,26 @@ class Connection implements ConnectionInterface public const DISCORD_VERSION = 10; public const DEFAULT_WEBSOCKET_URL = 'wss://gateway.discord.gg/'; - private const QUERY_DATA = ['v' => self::DISCORD_VERSION]; + private const QUERY_DATA = [ + 'v' => self::DISCORD_VERSION, + 'encoding' => 'json', + 'compress' => 'zlib-stream' + ]; private const HEARTBEAT_ACK_TIMEOUT = 2.5; private ?int $sequence = null; - private ?string $sessionId = null; private ?string $resumeUrl = null; public EventHandler $events; - private TimerInterface $heartbeatTimer; private TimerInterface $unacknowledgedHeartbeatTimer; - private ShardInterface $shard; + private string $buffer = ''; + private $inflate; + public function __construct( private LoopInterface $loop, private string $token, @@ -69,23 +72,50 @@ public function __construct( private Retrier $retrier = new Retrier(), ) { $this->events = new EventHandler($mapper); + $this->resetInflater(); $this->websocket->on(WebsocketEvents::MESSAGE, function (MessageInterface $message) { - $parsedMessage = json_decode((string) $message, depth: 1024); - if ($parsedMessage === null) { + $this->buffer .= $message->getPayload(); + + if (!str_ends_with($this->buffer, "\x00\x00\xff\xff")) { return; } - $payload = $this->mapper->map($parsedMessage, Payload::class); + $json = @inflate_add($this->inflate, $this->buffer); + $this->buffer = ''; - $this->raw->emit((string) $payload->op, [$this, $payload, $this->logger]); + if ($json === false) { + $this->logger->warning('ZLIB decompression error'); + return; + } + + $parsed = json_decode($json); + if ($parsed === null) { + $this->logger->warning('Failed to decode JSON payload'); + return; + } + + $payload = $this->mapper->map($parsed, Payload::class); + + $this->loop->futureTick(function () use ($payload) { + $this->raw->emit((string) $payload->op, [$this, $payload, $this->logger]); + }); }); - $this->websocket->on(WebsocketEvents::CLOSE, $this->handleClose(...)); + $this->websocket->on(WebsocketEvents::CLOSE, function (int $code, string $reason) { + $this->resetInflater(); + $this->handleClose($code, $reason); + }); $this->registerEvents(); } + private function resetInflater(): void + { + $this->inflate = inflate_init(ZLIB_ENCODING_DEFLATE); + $this->buffer = ''; + } + private function registerEvents(): void { $this->raw->register( From 51d7c6042982b2999e3d78ea0428a4248a6e0801 Mon Sep 17 00:00:00 2001 From: Naneynonn Date: Sun, 25 May 2025 20:54:16 +0300 Subject: [PATCH 2/8] remove @ --- src/Gateway/Connection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gateway/Connection.php b/src/Gateway/Connection.php index e80b44d..fa77aa8 100644 --- a/src/Gateway/Connection.php +++ b/src/Gateway/Connection.php @@ -81,7 +81,7 @@ public function __construct( return; } - $json = @inflate_add($this->inflate, $this->buffer); + $json = inflate_add($this->inflate, $this->buffer); $this->buffer = ''; if ($json === false) { From e3be687b158d9e06383d1f2f174f069488ff54fc Mon Sep 17 00:00:00 2001 From: Naneynonn Date: Sun, 25 May 2025 21:06:45 +0300 Subject: [PATCH 3/8] Fix connection URL expectations in tests after enabling zlib compression --- tests/Gateway/ConnectionTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/Gateway/ConnectionTest.php b/tests/Gateway/ConnectionTest.php index c2de949..559d24f 100644 --- a/tests/Gateway/ConnectionTest.php +++ b/tests/Gateway/ConnectionTest.php @@ -80,7 +80,7 @@ public function testConnect(): void $websocket->expects() ->open() - ->with('::ws url::?v=10') + ->with('::ws url::?' . http_build_query(Connection::QUERY_DATA)) ->andReturns(PromiseFake::get('::return::')) ->once(); @@ -556,7 +556,7 @@ public function testItReconnectsWhenWebsocketConnectionClosedWithCertainCodes(in $websocket->emit(WebsocketEvents::CLOSE, [$code, 'reason']); - $this->assertEquals([Connection::DEFAULT_WEBSOCKET_URL . '?v=' . Connection::DISCORD_VERSION], $websocket->openings); + $this->assertEquals([Connection::DEFAULT_WEBSOCKET_URL . '?' . http_build_query(Connection::QUERY_DATA)], $websocket->openings); } public static function reconnectCloseCodesProvider(): array @@ -606,7 +606,7 @@ public function testItResumesWhenWebsocketConnectionClosedWithCertainCodes(int $ $websocket->emit(WebsocketEvents::CLOSE, [$code, 'reason']); - $this->assertEquals(['::resume url::?v=' . Connection::DISCORD_VERSION], $websocket->openings); + $this->assertEquals(['::resume url::?' . http_build_query(Connection::QUERY_DATA)], $websocket->openings); } /** @@ -641,7 +641,7 @@ public function testItReconnectsIfMissingResumeUrl(int $code) $websocket->emit(WebsocketEvents::CLOSE, [$code, 'reason']); - $this->assertEquals([Connection::DEFAULT_WEBSOCKET_URL . '?v=' . Connection::DISCORD_VERSION], $websocket->openings); + $this->assertEquals([Connection::DEFAULT_WEBSOCKET_URL . '?' . http_build_query(Connection::QUERY_DATA)], $websocket->openings); } /** @@ -676,7 +676,7 @@ public function testItReconnectsIfMissingSessionId(int $code) $websocket->emit(WebsocketEvents::CLOSE, [$code, 'reason']); - $this->assertEquals([Connection::DEFAULT_WEBSOCKET_URL . '?v=' . Connection::DISCORD_VERSION], $websocket->openings); + $this->assertEquals([Connection::DEFAULT_WEBSOCKET_URL . '?' . http_build_query(Connection::QUERY_DATA)], $websocket->openings); } public static function resumeCloseCodesProvider(): array From 6af0efad95689e1bc6b2a56229b4f28d4569751c Mon Sep 17 00:00:00 2001 From: Naneynonn Date: Sun, 25 May 2025 21:15:12 +0300 Subject: [PATCH 4/8] Refactor tests to use expected Gateway query parameters explicitly --- tests/Gateway/ConnectionTest.php | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/tests/Gateway/ConnectionTest.php b/tests/Gateway/ConnectionTest.php index 559d24f..02bd5cc 100644 --- a/tests/Gateway/ConnectionTest.php +++ b/tests/Gateway/ConnectionTest.php @@ -35,6 +35,12 @@ class ConnectionTest extends MockeryTestCase { + private const EXPECTED_QUERY_PARAMS = [ + 'v' => 10, + 'encoding' => 'json', + 'compress' => 'zlib-stream' + ]; + public function testGetDefaultUrl(): void { $connection = new Connection( @@ -80,7 +86,7 @@ public function testConnect(): void $websocket->expects() ->open() - ->with('::ws url::?' . http_build_query(Connection::QUERY_DATA)) + ->with('::ws url::?' . http_build_query(self::EXPECTED_QUERY_PARAMS)) ->andReturns(PromiseFake::get('::return::')) ->once(); @@ -556,7 +562,7 @@ public function testItReconnectsWhenWebsocketConnectionClosedWithCertainCodes(in $websocket->emit(WebsocketEvents::CLOSE, [$code, 'reason']); - $this->assertEquals([Connection::DEFAULT_WEBSOCKET_URL . '?' . http_build_query(Connection::QUERY_DATA)], $websocket->openings); + $this->assertEquals([Connection::DEFAULT_WEBSOCKET_URL . '?' . http_build_query(self::EXPECTED_QUERY_PARAMS)], $websocket->openings); } public static function reconnectCloseCodesProvider(): array @@ -606,7 +612,7 @@ public function testItResumesWhenWebsocketConnectionClosedWithCertainCodes(int $ $websocket->emit(WebsocketEvents::CLOSE, [$code, 'reason']); - $this->assertEquals(['::resume url::?' . http_build_query(Connection::QUERY_DATA)], $websocket->openings); + $this->assertEquals(['::resume url::?' . http_build_query(self::EXPECTED_QUERY_PARAMS)], $websocket->openings); } /** @@ -641,7 +647,7 @@ public function testItReconnectsIfMissingResumeUrl(int $code) $websocket->emit(WebsocketEvents::CLOSE, [$code, 'reason']); - $this->assertEquals([Connection::DEFAULT_WEBSOCKET_URL . '?' . http_build_query(Connection::QUERY_DATA)], $websocket->openings); + $this->assertEquals([Connection::DEFAULT_WEBSOCKET_URL . '?' . http_build_query(self::EXPECTED_QUERY_PARAMS)], $websocket->openings); } /** @@ -676,7 +682,7 @@ public function testItReconnectsIfMissingSessionId(int $code) $websocket->emit(WebsocketEvents::CLOSE, [$code, 'reason']); - $this->assertEquals([Connection::DEFAULT_WEBSOCKET_URL . '?' . http_build_query(Connection::QUERY_DATA)], $websocket->openings); + $this->assertEquals([Connection::DEFAULT_WEBSOCKET_URL . '?' . http_build_query(self::EXPECTED_QUERY_PARAMS)], $websocket->openings); } public static function resumeCloseCodesProvider(): array From 9f8cd4a35f70426d42a235ad3ef197e8fcce6c96 Mon Sep 17 00:00:00 2001 From: Naneynonn Date: Sun, 25 May 2025 21:20:40 +0300 Subject: [PATCH 5/8] Fix test to mock getPayload instead of __toString on MessageInterface --- tests/Gateway/ConnectionTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Gateway/ConnectionTest.php b/tests/Gateway/ConnectionTest.php index 02bd5cc..c49d8d4 100644 --- a/tests/Gateway/ConnectionTest.php +++ b/tests/Gateway/ConnectionTest.php @@ -492,7 +492,7 @@ public function testItEmitsGatewayMessagesAsEvents(): void /** @var MessageInterface&MockInterface */ $message = Mockery::mock(MessageInterface::class); $message->expects() - ->__toString() + ->getPayload() ->andReturns('{"op": 1}') ->once(); From a4d28ed05b4d0205f11886395b0e4fd7b44c45ea Mon Sep 17 00:00:00 2001 From: Naneynonn Date: Sun, 25 May 2025 21:52:50 +0300 Subject: [PATCH 6/8] Fix test for gateway message event with zlib payload handling --- tests/Gateway/ConnectionTest.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/Gateway/ConnectionTest.php b/tests/Gateway/ConnectionTest.php index c49d8d4..171aaee 100644 --- a/tests/Gateway/ConnectionTest.php +++ b/tests/Gateway/ConnectionTest.php @@ -468,8 +468,16 @@ public function testItEmitsGatewayMessagesAsEvents(): void ->registerOnce() ->withAnyArgs(); + $loop = Mockery::mock(LoopInterface::class); + $loop->shouldReceive('futureTick') + ->once() + ->with(Mockery::on(function ($callback) { + $callback(); // вызываем вручную + return true; + })); + $connection = new Connection( - Mockery::mock(LoopInterface::class), + $loop, '::token::', new Bitwise(), new DataMapper(new NullLogger()), @@ -493,7 +501,7 @@ public function testItEmitsGatewayMessagesAsEvents(): void $message = Mockery::mock(MessageInterface::class); $message->expects() ->getPayload() - ->andReturns('{"op": 1}') + ->andReturns(zlib_encode('{"op":1}', ZLIB_ENCODING_DEFLATE) . "\x00\x00\xff\xff") ->once(); $websocket->emit(WebsocketEvents::MESSAGE, [$message]); From 8592f3ad3948083da957fd1e336560d841d443c2 Mon Sep 17 00:00:00 2001 From: Naneynonn Date: Sun, 25 May 2025 21:54:51 +0300 Subject: [PATCH 7/8] Deleting unnecessary comment --- tests/Gateway/ConnectionTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Gateway/ConnectionTest.php b/tests/Gateway/ConnectionTest.php index 171aaee..ef09ac5 100644 --- a/tests/Gateway/ConnectionTest.php +++ b/tests/Gateway/ConnectionTest.php @@ -472,7 +472,7 @@ public function testItEmitsGatewayMessagesAsEvents(): void $loop->shouldReceive('futureTick') ->once() ->with(Mockery::on(function ($callback) { - $callback(); // вызываем вручную + $callback(); return true; })); From 7f4cb80182edb09f75df1565210ac52202300e45 Mon Sep 17 00:00:00 2001 From: Naneynonn Date: Sun, 25 May 2025 22:58:33 +0300 Subject: [PATCH 8/8] Use InflateContext type for $inflate property --- src/Gateway/Connection.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Gateway/Connection.php b/src/Gateway/Connection.php index fa77aa8..52c1f5e 100644 --- a/src/Gateway/Connection.php +++ b/src/Gateway/Connection.php @@ -31,6 +31,7 @@ use React\EventLoop\LoopInterface; use React\EventLoop\TimerInterface; use React\Promise\PromiseInterface; +use InflateContext; /** * @SuppressWarnings(PHPMD.TooManyPublicMethods) @@ -58,7 +59,7 @@ class Connection implements ConnectionInterface private ShardInterface $shard; private string $buffer = ''; - private $inflate; + private InflateContext|false $inflate; public function __construct( private LoopInterface $loop,