From c8eee7c6a76d6427387caeb44e120a030913fe64 Mon Sep 17 00:00:00 2001 From: Joe Dixon Date: Mon, 20 Nov 2023 16:24:23 +0000 Subject: [PATCH] fix presence channels --- src/Channels/Channel.php | 5 ++ src/Channels/PresenceChannel.php | 21 +++--- src/Contracts/ChannelConnectionManager.php | 8 +++ .../ArrayChannelConnectionManager.php | 8 +++ src/Servers/Reverb/ChannelConnection.php | 7 +- tests/Pest.php | 11 +-- tests/Unit/Channels/PresenceChannelTest.php | 72 +++++++++++-------- 7 files changed, 84 insertions(+), 48 deletions(-) diff --git a/src/Channels/Channel.php b/src/Channels/Channel.php index fc469d15..b71f7317 100644 --- a/src/Channels/Channel.php +++ b/src/Channels/Channel.php @@ -55,6 +55,11 @@ public function unsubscribe(Connection $connection): void $this->connections->remove($connection); } + public function subscribed(Connection $connection): bool + { + return $this->connections->find($connection); + } + /** * Send a message to all connections subscribed to the channel. */ diff --git a/src/Channels/PresenceChannel.php b/src/Channels/PresenceChannel.php index 6c686b57..dd7b6c79 100644 --- a/src/Channels/PresenceChannel.php +++ b/src/Channels/PresenceChannel.php @@ -2,9 +2,7 @@ namespace Laravel\Reverb\Channels; -use Illuminate\Support\Facades\App; use Laravel\Reverb\Application; -use Laravel\Reverb\Contracts\ChannelManager; use Laravel\Reverb\Contracts\Connection; class PresenceChannel extends PrivateChannel @@ -32,16 +30,18 @@ public function subscribe(Connection $connection, string $auth = null, string $d */ public function unsubscribe(Connection $connection): void { - $data = App::make(ChannelManager::class) - ->for($connection->app()) - ->data($this, $connection); + if (! $subscription = $this->connections->find($connection)) { + parent::unsubscribe($connection); - if (isset($data['user_id'])) { + return; + } + + if ($userId = $subscription->data('user_id')) { $this->broadcast( $connection->app(), [ 'event' => 'pusher_internal:member_removed', - 'data' => ['user_id' => $data['user_id']], + 'data' => ['user_id' => $userId], 'channel' => $this->name(), ], $connection @@ -56,14 +56,13 @@ public function unsubscribe(Connection $connection): void */ public function data(Application $app): array { - $connections = App::make(ChannelManager::class) - ->for($app) - ->connectionKeys($this); + $connections = collect($this->connections->all()) + ->map(fn ($connection) => $connection->data()); return [ 'presence' => [ 'count' => $connections->count(), - 'ids' => $connections->map(fn ($connection) => $connection['user_id'])->toArray(), + 'ids' => $connections->map(fn ($connection) => $connection['user_id'])->all(), 'hash' => $connections->keyBy('user_id')->map->user_info->toArray(), ], ]; diff --git a/src/Contracts/ChannelConnectionManager.php b/src/Contracts/ChannelConnectionManager.php index 1dea9371..8ea7baa5 100644 --- a/src/Contracts/ChannelConnectionManager.php +++ b/src/Contracts/ChannelConnectionManager.php @@ -2,6 +2,8 @@ namespace Laravel\Reverb\Contracts; +use Laravel\Reverb\Servers\Reverb\ChannelConnection; + interface ChannelConnectionManager { /** @@ -14,8 +16,14 @@ public function add(Connection $connection, array $data): void; */ public function remove(Connection $connection): void; + /** + * Find a connection by its identifier. + */ + public function find(Connection $connection): ?ChannelConnection; + /** * Get all the connections. + * * @return array */ public function all(): array; diff --git a/src/Managers/ArrayChannelConnectionManager.php b/src/Managers/ArrayChannelConnectionManager.php index f699962c..35d568f1 100644 --- a/src/Managers/ArrayChannelConnectionManager.php +++ b/src/Managers/ArrayChannelConnectionManager.php @@ -31,6 +31,14 @@ public function remove(Connection $connection): void unset($this->connections[$connection->identifier()]); } + /** + * Find a connection by its identifier. + */ + public function find(Connection $connection): ?ChannelConnection + { + return $this->connections[$connection->identifier()] ?? null; + } + /** * Get all the connections. */ diff --git a/src/Servers/Reverb/ChannelConnection.php b/src/Servers/Reverb/ChannelConnection.php index 1777fc77..fb1da5fb 100644 --- a/src/Servers/Reverb/ChannelConnection.php +++ b/src/Servers/Reverb/ChannelConnection.php @@ -2,6 +2,7 @@ namespace Laravel\Reverb\Servers\Reverb; +use Illuminate\Support\Arr; use Laravel\Reverb\Contracts\Connection; class ChannelConnection @@ -22,8 +23,12 @@ public function connection(): Connection /** * Get the connection data. */ - public function data(): array + public function data(string $key = null): mixed { + if ($key) { + return Arr::get($this->data, $key); + } + return $this->data; } diff --git a/tests/Pest.php b/tests/Pest.php index b8660b23..247afb23 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -22,13 +22,14 @@ * @param bool $serializable * @return array */ -function connections(int $count = 1, $serializable = false): array +function connections(int $count = 1, array $data = [], $serializable = false): array { - return Collection::make(range(1, $count))->map(function () use ($serializable) { + return Collection::make(range(1, $count))->map(function () use ($data, $serializable) { return new ChannelConnection( - $serializable - ? new SerializableConnection(Uuid::uuid4()) - : new Connection(Uuid::uuid4()) + $serializable + ? new SerializableConnection(Uuid::uuid4()) + : new Connection(Uuid::uuid4()), + $data ); })->all(); } diff --git a/tests/Unit/Channels/PresenceChannelTest.php b/tests/Unit/Channels/PresenceChannelTest.php index 23d5ee67..c481908b 100644 --- a/tests/Unit/Channels/PresenceChannelTest.php +++ b/tests/Unit/Channels/PresenceChannelTest.php @@ -4,6 +4,7 @@ use Laravel\Reverb\Contracts\ApplicationProvider; use Laravel\Reverb\Contracts\ChannelConnectionManager; use Laravel\Reverb\Exceptions\ConnectionUnauthorized; +use Laravel\Reverb\Servers\Reverb\ChannelConnection; use Laravel\Reverb\Tests\Connection; beforeEach(function () { @@ -22,7 +23,7 @@ ->andReturn([]); $channel->subscribe($this->connection, validAuth($this->connection, 'presence-test-channel')); -})->todo(); +}); it('can unsubscribe a connection from a channel', function () { $channel = new PresenceChannel('presence-test-channel'); @@ -32,21 +33,21 @@ ->with($this->connection); $channel->unsubscribe($this->connection); -})->todo(); +}); it('can broadcast to all connections of a channel', function () { $channel = new PresenceChannel('presence-test-channel'); $this->channelConnectionManager->shouldReceive('subscribe'); - $this->channelConnectionManager->shouldReceive('connections') + $this->channelConnectionManager->shouldReceive('all') ->once() ->andReturn($connections = connections(3)); $channel->broadcast(app(ApplicationProvider::class)->findByKey('pusher-key'), ['foo' => 'bar']); - $connections->each(fn ($connection) => $connection->assertSent(['foo' => 'bar'])); -})->todo(); + collect($connections)->each(fn ($connection) => $connection->assertSent(['foo' => 'bar'])); +}); it('fails to subscribe if the signature is invalid', function () { $channel = new PresenceChannel('presence-test-channel'); @@ -59,15 +60,12 @@ it('can return data stored on the connection', function () { $channel = new PresenceChannel('presence-test-channel'); - $connections = connections(2) - ->map(fn ($connection, $index) => [ - 'user_info' => [ - 'name' => 'Joe', - ], - 'user_id' => $index + 1, - ]); + $connections = [ + connections(data: ['user_info' => ['name' => 'Joe'], 'user_id' => 1])[0], + connections(data: ['user_info' => ['name' => 'Joe'], 'user_id' => 2])[0], + ]; - $this->channelConnectionManager->shouldReceive('connectionKeys') + $this->channelConnectionManager->shouldReceive('all') ->once() ->andReturn($connections); @@ -81,36 +79,36 @@ ], ], ]); -})->todo(); +}); it('sends notification of subscription', function () { $channel = new PresenceChannel('presence-test-channel'); - $this->channelConnectionManager->shouldReceive('subscribe') + $this->channelConnectionManager->shouldReceive('add') ->once() - ->with($channel, $this->connection, []); + ->with($this->connection, []); - $this->channelConnectionManager->shouldReceive('connections') + $this->channelConnectionManager->shouldReceive('all') ->andReturn($connections = connections(3)); $channel->subscribe($this->connection, validAuth($this->connection, 'presence-test-channel')); - $connections->each(fn ($connection) => $connection->assertSent([ + collect($connections)->each(fn ($connection) => $connection->assertSent([ 'event' => 'pusher_internal:member_added', 'data' => [], 'channel' => 'presence-test-channel', ])); -})->todo(); +}); it('sends notification of subscription with data', function () { $channel = new PresenceChannel('presence-test-channel'); $data = json_encode(['name' => 'Joe']); - $this->channelConnectionManager->shouldReceive('subscribe') + $this->channelConnectionManager->shouldReceive('add') ->once() - ->with($channel, $this->connection, ['name' => 'Joe']); + ->with($this->connection, ['name' => 'Joe']); - $this->channelConnectionManager->shouldReceive('connections') + $this->channelConnectionManager->shouldReceive('all') ->andReturn($connections = connections(3)); $channel->subscribe( @@ -123,30 +121,42 @@ $data ); - $connections->each(fn ($connection) => $connection->assertSent([ + collect($connections)->each(fn ($connection) => $connection->assertSent([ 'event' => 'pusher_internal:member_added', 'data' => ['name' => 'Joe'], 'channel' => 'presence-test-channel', ])); -})->todo(); +}); it('sends notification of an unsubscribe', function () { $channel = new PresenceChannel('presence-test-channel'); - $connection = $connection = connections(1)->first(); + $data = json_encode(['user_info' => ['name' => 'Joe'], 'user_id' => 1]); - $this->channelConnectionManager->shouldReceive('data') - ->andReturn(['user_info' => ['name' => 'Joe'], 'user_id' => 1]); + $channel->subscribe( + $this->connection, + validAuth( + $this->connection, + 'presence-test-channel', + $data + ), + $data + ); - $this->channelConnectionManager->shouldReceive('connections') + $this->channelConnectionManager->shouldReceive('find') + ->andReturn(new ChannelConnection($this->connection, ['user_info' => ['name' => 'Joe'], 'user_id' => 1])); + + $this->channelConnectionManager->shouldReceive('all') ->andReturn($connections = connections(3)); - $this->channelConnectionManager->shouldReceive('unsubscribe'); + $this->channelConnectionManager->shouldReceive('remove') + ->once() + ->with($this->connection); $channel->unsubscribe($this->connection); - $connections->each(fn ($connection) => $connection->assertSent([ + collect($connections)->each(fn ($connection) => $connection->assertSent([ 'event' => 'pusher_internal:member_removed', 'data' => ['user_id' => 1], 'channel' => 'presence-test-channel', ])); -})->todo(); +});