Skip to content

Commit 2031401

Browse files
authored
Adding whisper feature (georgeboot#24)
1 parent 0e53b7f commit 2031401

File tree

4 files changed

+112
-8
lines changed

4 files changed

+112
-8
lines changed

js-src/Channel.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import {EventFormatter} from 'laravel-echo/src/util';
2-
import {Channel as BaseChannel} from 'laravel-echo/src/channel/channel';
3-
import {PresenceChannel} from "laravel-echo/src/channel";
4-
import {Websocket} from "./Websocket";
1+
import { EventFormatter } from 'laravel-echo/src/util';
2+
import { Channel as BaseChannel } from 'laravel-echo/src/channel/channel';
3+
import { PresenceChannel } from "laravel-echo/src/channel";
4+
import { Websocket } from "./Websocket";
55

66
/**
77
* This class represents a Pusher channel.
@@ -105,9 +105,12 @@ export class Channel extends BaseChannel implements PresenceChannel {
105105
}
106106

107107
whisper(event: string, data: object): Channel {
108+
let channel = this.name;
109+
let formattedEvent = "client-" + event;
108110
this.socket.send({
109-
event,
111+
"event": formattedEvent,
110112
data,
113+
channel,
111114
})
112115

113116
return this;

js-tests/Connector.test.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import WS from "jest-websocket-mock";
2-
import {Connector} from "../js-src/Connector";
3-
import {Channel} from "../js-src/Channel";
2+
import { Connector } from "../js-src/Connector";
3+
import { Channel } from "../js-src/Channel";
44

55
describe('Connector', () => {
66
let server: WS;
@@ -54,4 +54,33 @@ describe('Connector', () => {
5454
expect(handler1).toBeCalled();
5555
expect(handler2).not.toBeCalled();
5656
})
57+
58+
test('we can send a whisper event', async () => {
59+
const connector = new Connector({
60+
host: "ws://localhost:1234",
61+
})
62+
63+
await server.connected;
64+
65+
await expect(server).toReceiveMessage('{"event":"whoami"}');
66+
server.send('{"event":"whoami","data":{"socket_id":"test-socket-id"}}')
67+
68+
const channel = connector.channel('my-test-channel')
69+
70+
await expect(server).toReceiveMessage('{"event":"subscribe","data":{"channel":"my-test-channel"}}');
71+
72+
server.send('{"event":"subscription_succeeded","channel":"my-test-channel"}')
73+
74+
expect(channel).toBeInstanceOf(Channel)
75+
76+
const handler1 = jest.fn();
77+
const handler2 = jest.fn();
78+
79+
channel.on('client-whisper', handler1)
80+
81+
server.send('{"event":"client-whisper","data":"whisper","channel":"my-test-channel","data":{}}')
82+
83+
expect(handler1).toBeCalled();
84+
expect(handler2).not.toBeCalled();
85+
})
5786
});

src/Handler.php

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
namespace Georgeboot\LaravelEchoApiGateway;
44

5+
use Aws\ApiGatewayManagementApi\Exception\ApiGatewayManagementApiException;
56
use Bref\Context\Context;
67
use Bref\Event\ApiGateway\WebsocketEvent;
78
use Bref\Event\ApiGateway\WebsocketHandler;
89
use Bref\Event\Http\HttpResponse;
10+
use Illuminate\Support\Arr;
911
use Illuminate\Support\Str;
1012
use Throwable;
1113

@@ -69,9 +71,11 @@ protected function handleMessage(WebsocketEvent $event, Context $context): void
6971
$this->subscribe($event, $context);
7072
} elseif ($eventType === 'unsubscribe') {
7173
$this->unsubscribe($event, $context);
74+
} elseif (Str::startsWith($eventType, 'client-')) {
75+
$this->broadcastToChannel($event, $context);
7276
} else {
7377
$this->sendMessage($event, $context, [
74-
'event' => 'error',
78+
'event' => 'error'
7579
]);
7680
}
7781
}
@@ -134,8 +138,42 @@ protected function unsubscribe(WebsocketEvent $event, Context $context): void
134138
]);
135139
}
136140

141+
public function broadcastToChannel(WebsocketEvent $event, Context $context): void
142+
{
143+
$skipConnectionId = $event->getConnectionId();
144+
$eventBody = json_decode($event->getBody(), true);
145+
$channel = Arr::get($eventBody, 'channel');
146+
$event = Arr::get($eventBody, 'event');
147+
$payload = Arr::get($eventBody, 'data');
148+
if (is_object($payload) || is_array($payload)) {
149+
$payload = json_encode($payload);
150+
}
151+
$data = json_encode([
152+
'event'=>$event,
153+
'channel'=>$channel,
154+
'data'=>$payload,
155+
]) ?: '';
156+
$this->subscriptionRepository->getConnectionIdsForChannel($channel)
157+
->reject(fn ($connectionId) => $connectionId === $skipConnectionId)
158+
->each(fn (string $connectionId) => $this->sendMessageToConnection($connectionId, $data));
159+
}
160+
137161
public function sendMessage(WebsocketEvent $event, Context $context, array $data): void
138162
{
139163
$this->connectionRepository->sendMessage($event->getConnectionId(), json_encode($data, JSON_THROW_ON_ERROR));
140164
}
165+
166+
protected function sendMessageToConnection(string $connectionId, string $data): void
167+
{
168+
try {
169+
$this->connectionRepository->sendMessage($connectionId, $data);
170+
} catch (ApiGatewayManagementApiException $exception) {
171+
if ($exception->getAwsErrorCode() === 'GoneException') {
172+
$this->subscriptionRepository->clearConnection($connectionId);
173+
return;
174+
}
175+
176+
throw $exception;
177+
}
178+
}
141179
}

tests/HandlerTest.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,40 @@
7979
], $context);
8080
});
8181

82+
it('can broadcast a whisper', function () {
83+
app()->instance(SubscriptionRepository::class, Mockery::mock(SubscriptionRepository::class, function ($mock) {
84+
/** @var Mock $mock */
85+
$mock->shouldReceive('getConnectionIdsForChannel')->withArgs(function (string $channel): bool {
86+
return $channel === 'test-channel';
87+
})->once()
88+
->andReturn(collect(['connection-id-1', 'connection-id-2']));
89+
}));
90+
91+
app()->instance(ConnectionRepository::class, Mockery::mock(ConnectionRepository::class, function ($mock) {
92+
/** @var Mock $mock */
93+
$mock->shouldReceive('sendMessage')->withArgs(function (string $connectionId, string $data): bool {
94+
return $connectionId === 'connection-id-2' and $data === '{"event":"client-test","channel":"test-channel","data":"whisper"}';
95+
})->once();
96+
}));
97+
98+
/** @var Handler $handler */
99+
$handler = app(Handler::class);
100+
101+
$context = new Context('request-id-1', 50_000, 'function-arn', 'trace-id-1');
102+
103+
$handler->handle([
104+
'requestContext' => [
105+
'routeKey' => 'my-test-route-key',
106+
'eventType' => 'MESSAGE',
107+
'connectionId' => 'connection-id-1',
108+
'domainName' => 'test-domain',
109+
'apiId' => 'api-id-1',
110+
'stage' => 'stage-test',
111+
],
112+
'body' => json_encode(['event' => 'client-test', 'channel' => 'test-channel', 'data'=>'whisper']),
113+
], $context);
114+
});
115+
82116
it('handles dropped connections', function () {
83117
$mock = new MockHandler();
84118

0 commit comments

Comments
 (0)