Skip to content

Handling websocket disconnects #31

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ advised to only use this package for non-critical / non-production projects.
In order to use this package, your project needs to meet the following criteria:

- PHP 7.4 or 8.x
- Laravel 6, 7 or 8
- Laravel 6 to 11
- Uses either [bref](https://bref.sh) or [Laravel Vapor](https://vapor.laravel.com) to deploy to AWS
- Has a working queue
- Uses Laravel Mix or any other tool to bundle your assets
Expand Down Expand Up @@ -120,6 +120,7 @@ provider:

environment:
# Add these variables
# Please note : in Laravel 11, this setting is now BROADCAST_CONNECTION
BROADCAST_DRIVER: laravel-echo-api-gateway
LARAVEL_ECHO_API_GATEWAY_DYNAMODB_TABLE: !Ref ConnectionsTable
LARAVEL_ECHO_API_GATEWAY_API_ID: !Ref WebsocketsApi
Expand Down Expand Up @@ -201,4 +202,19 @@ window.Echo = new Echo({
});
```

You can also enable console output by passing a `debug: true` otpion to your window.Echo intializer :
```js
import Echo from 'laravel-echo';
import {broadcaster} from 'laravel-echo-api-gateway';

window.Echo = new Echo({
broadcaster,
// replace the placeholders
host: 'wss://{api-ip}.execute-api.{region}.amazonaws.com/{stage}',
debug: true
});
```



Lastly, you have to generate your assets by running Laravel Mix. After this step, you should be up and running.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"php": "^8.0|^8.1|^8.2",
"ext-json": "*",
"aws/aws-sdk-php": "^3.308",
"bref/bref": "^1.1",
"bref/bref": "^1.1|^2.0",
"guzzlehttp/guzzle": "^6.3|^7.0",
"laravel/framework": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0"
},
Expand Down
74 changes: 50 additions & 24 deletions js-src/Websocket.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import {Channel} from "./Channel";
import {AxiosResponse} from "axios";
import { AxiosResponse } from "axios";
import axios from 'axios';
import { Channel } from "./Channel";

export type Options = { authEndpoint: string, host: string, bearerToken: string, auth: any, debug: boolean };

export type Options = { authEndpoint: string, host: string, bearerToken: string, auth: any };
export type MessageBody = { event: string, channel?: string, data: object };

export class Websocket {
Expand All @@ -19,14 +21,20 @@ export class Websocket {

private socketId: string;

private closing = false;

private pingInterval: NodeJS.Timeout;

constructor(options: Options) {
this.options = options;
private connect(host: string): void {
this.options.debug && console.log('Connecting');

this.websocket = new WebSocket(options.host)
this.websocket = new WebSocket(host)

this.websocket.onopen = () => {
this.send({
event: 'whoami',
})

while (this.buffer.length) {
const message = this.buffer[0]

Expand All @@ -44,7 +52,7 @@ export class Websocket {
}

if (message.channel) {
console.log(`Received event ${message.event} on channel ${message.channel}`)
this.options.debug && console.log(`Received event ${message.event} on channel ${message.channel}`)

if (this.listeners[message.channel] && this.listeners[message.channel][message.event]) {
this.listeners[message.channel][message.event](message.data)
Expand All @@ -56,12 +64,24 @@ export class Websocket {
if (this.internalListeners[message.event]) {
this.internalListeners[message.event](message.data)
}

}


this.websocket.onclose = () => {
if (this.socketId && !this.closing || !this.socketId) {
this.options.debug && console.info('Connection lost, reconnecting...');
setTimeout(() => {
this.socketId = undefined
this.connect(host)
}, 1000);
}
};

this.on('whoami', ({ socket_id: socketId }) => {
this.socketId = socketId

console.log(`just set socketId to ${socketId}`)
this.options.debug && console.log(`just set socketId to ${socketId}`)

while (this.channelBacklog.length) {
const channel = this.channelBacklog[0]
Expand All @@ -72,27 +92,32 @@ export class Websocket {
}
})

this.send({
event: 'whoami',
})

// send ping every 60 seconds to keep connection alive
this.pingInterval = setInterval(() => {
console.log('Sending ping')

this.send({
event: 'ping',
})
if (this.websocket.readyState === this.websocket.OPEN) {
this.options.debug && console.log('Sending ping')
this.send({
event: 'ping',
})
}
}, 60 * 1000)

}

constructor(options: Options) {
this.options = options;

this.connect(this.options.host);

return this
}

protected parseMessage(body: string): MessageBody {
try {
return JSON.parse(body)
} catch (error) {
console.error(error)
this.options.debug && console.error(error)

return undefined
}
Expand All @@ -115,7 +140,8 @@ export class Websocket {
this.buffer.push(message)
}

close (): void {
close(): void {
this.closing = true
this.internalListeners = {}

clearInterval(this.pingInterval)
Expand All @@ -134,7 +160,7 @@ export class Websocket {

private actuallySubscribe(channel: Channel): void {
if (channel.name.startsWith('private-') || channel.name.startsWith('presence-')) {
console.log(`Sending auth request for channel ${channel.name}`)
this.options.debug && console.log(`Sending auth request for channel ${channel.name}`)

if (this.options.bearerToken) {
this.options.auth.headers['Authorization'] = 'Bearer ' + this.options.bearerToken;
Expand All @@ -146,21 +172,21 @@ export class Websocket {
}, {
headers: this.options.auth.headers || {}
}).then((response: AxiosResponse) => {
console.log(`Subscribing to channels ${channel.name}`)
this.options.debug && console.log(`Subscribing to channels ${channel.name}`)

this.send({
event: 'subscribe',
data: {
channel: channel.name,
... response.data
...response.data
},
})
}).catch((error) => {
console.log(`Auth request for channel ${channel.name} failed`)
console.error(error)
this.options.debug && console.log(`Auth request for channel ${channel.name} failed`)
this.options.debug && console.error(error)
})
} else {
console.log(`Subscribing to channels ${channel.name}`)
this.options.debug && console.log(`Subscribing to channels ${channel.name}`)

this.send({
event: 'subscribe',
Expand Down
31 changes: 27 additions & 4 deletions js-tests/Connector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import WS from "jest-websocket-mock";
import { Connector } from "../js-src/Connector";
import { Channel } from "../js-src/Channel";

const mockedHost = 'ws://localhost:1234';

describe('Connector', () => {
let server: WS;

beforeEach(() => {
server = new WS("ws://localhost:1234");
jest.useRealTimers();
server = new WS(mockedHost);
});

afterEach(() => {
Expand All @@ -15,7 +18,7 @@ describe('Connector', () => {

test('socket id is correctly set', async () => {
const connector = new Connector({
host: "ws://localhost:1234",
host: mockedHost,
})

await server.connected;
Expand All @@ -26,9 +29,29 @@ describe('Connector', () => {
expect(connector.socketId()).toBe('test-socket-id')
})

test('we reconnect to the server on error', async () => {
const connector = new Connector({
host: mockedHost,
})

await server.connected;
await expect(server).toReceiveMessage('{"event":"whoami"}');
server.send('{"event":"whoami","data":{"socket_id":"test-socket-id"}}')

server.close();
await server.closed;
server.server.stop(() => (server = new WS(mockedHost)));

await server.connected;
await expect(server).toReceiveMessage('{"event":"whoami"}');
server.send('{"event":"whoami","data":{"socket_id":"test-socket-id2"}}')

expect(connector.socketId()).toBe('test-socket-id2')
})

test('we can subscribe to a channel and listen to events', async () => {
const connector = new Connector({
host: "ws://localhost:1234",
host: mockedHost,
})

await server.connected;
Expand Down Expand Up @@ -57,7 +80,7 @@ describe('Connector', () => {

test('we can send a whisper event', async () => {
const connector = new Connector({
host: "ws://localhost:1234",
host: mockedHost,
})

await server.connected;
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
"jest": "^24.9.0",
"jest-websocket-mock": "^2.2.0",
"laravel-echo": "^1.10.0",
"mock-socket": "^9.0.3",
"rollup": "^2.10.2",
"rollup-plugin-typescript2": "^0.27.1",
"standard-version": "^8.0.1",
Expand Down
Loading
Loading