Skip to content
This repository has been archived by the owner on Apr 14, 2023. It is now read-only.

fix(NA): Non forced close without connection terminate and also some connection's flow improvements #197

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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Changelog

### vNEXT
- Fix for non forced closes (now it wont send connection_terminate) [PR #197](https://github.com/apollographql/subscriptions-transport-ws/pull/197)
- A lot of connection's flow improvements (on connect, on disconnect and on reconnect) [PR #197](https://github.com/apollographql/subscriptions-transport-ws/pull/197)
- Require specific lodash/assign module instead of entire package, so memory impact is reduced [PR #196](https://github.com/apollographql/subscriptions-transport-ws/pull/196)

### 0.7.3
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,9 +216,9 @@ ReactDOM.render(
### `Constructor(url, options, connectionCallback)`
- `url: string` : url that the client will connect to, starts with `ws://` or `wss://`
- `options?: Object` : optional, object to modify default client behavior
* `timeout?: number` : how long the client should wait in ms for a keep-alive message from the server (default 30000 ms), this parameter is ignored if the server does not send keep-alive messages.
* `timeout?: number` : how long the client should wait in ms for a keep-alive message from the server (default 10000 ms), this parameter is ignored if the server does not send keep-alive messages. This will also be used to calculate the max connection time per connect/reconnect
* `lazy?: boolean` : use to set lazy mode - connects only when first subscription created, and delay the socket initialization
* `connectionParams?: Object | Function` : object that will be available as first argument of `onConnect` (in server side), if passed a function - it will call it and send the return value.
* `connectionParams?: Object | Function` : object that will be available as first argument of `onConnect` (in server side), if passed a function - it will call it and send the return value
* `reconnect?: boolean` : automatic reconnect in case of connection error
* `reconnectionAttempts?: number` : how much reconnect attempts
* `connectionCallback?: (error) => {}` : optional, callback that called after the first init message, with the error (if there is one)
Expand Down
110 changes: 90 additions & 20 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,14 @@ export class SubscriptionClient {
private connectionCallback: any;
private eventEmitter: EventEmitter;
private lazy: boolean;
private forceClose: boolean;
private closedByUser: boolean;
private wsImpl: any;
private wasKeepAliveReceived: boolean;
private checkConnectionTimeoutId: any;
private tryReconnectTimeoutId: any;
private checkConnectionIntervalId: any;
private maxConnectTimeoutId: any;
private middlewares: Middleware[];
private maxConnectTimeGenerator: any;

constructor(url: string, options?: ClientOptions, webSocketImpl?: any) {
const {
Expand Down Expand Up @@ -103,11 +106,12 @@ export class SubscriptionClient {
this.reconnecting = false;
this.reconnectionAttempts = reconnectionAttempts;
this.lazy = !!lazy;
this.forceClose = false;
this.closedByUser = false;
this.backoff = new Backoff({ jitter: 0.5 });
this.eventEmitter = new EventEmitter();
this.middlewares = [];
this.client = null;
this.maxConnectTimeGenerator = this.createMaxConnectTimeGenerator();

if (!this.lazy) {
this.connect();
Expand All @@ -122,12 +126,24 @@ export class SubscriptionClient {
return this.client.readyState;
}

public close(isForced = true) {
public close(isForced = true, closedByUser = true) {
if (this.client !== null) {
this.forceClose = isForced;
this.sendMessage(undefined, MessageTypes.GQL_CONNECTION_TERMINATE, null);
this.closedByUser = closedByUser;

if (isForced) {
this.clearCheckConnectionInterval();
this.clearMaxConnectTimeout();
this.clearTryReconnectTimeout();
this.sendMessage(undefined, MessageTypes.GQL_CONNECTION_TERMINATE, null);
}

this.client.close();
this.client = null;
this.eventEmitter.emit('disconnected');

if (!isForced) {
this.tryReconnect();
}
}
}

Expand Down Expand Up @@ -277,6 +293,38 @@ export class SubscriptionClient {
return this;
}

private createMaxConnectTimeGenerator() {
const minValue = 1000;
const maxValue = this.wsTimeout;

return new Backoff({
min: minValue,
max: maxValue,
factor: 1.2,
});
}

private clearCheckConnectionInterval() {
if (this.checkConnectionIntervalId) {
clearInterval(this.checkConnectionIntervalId);
this.checkConnectionIntervalId = null;
}
}

private clearMaxConnectTimeout() {
if (this.maxConnectTimeoutId) {
clearTimeout(this.maxConnectTimeoutId);
this.maxConnectTimeoutId = null;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reformat :)


private clearTryReconnectTimeout() {
if (this.tryReconnectTimeoutId) {
clearTimeout(this.tryReconnectTimeoutId);
this.tryReconnectTimeoutId = null;
}
}

private logWarningOnNonProductionEnv(warning: string) {
if (process && process.env && process.env.NODE_ENV !== 'production') {
console.warn(warning);
Expand Down Expand Up @@ -369,7 +417,7 @@ export class SubscriptionClient {
// send message, or queue it if connection is not open
private sendMessageRaw(message: Object) {
switch (this.status) {
case this.client.OPEN:
case this.wsImpl.OPEN:
let serializedMessage: string = JSON.stringify(message);
let parsedMessage: any;
try {
Expand All @@ -380,7 +428,7 @@ export class SubscriptionClient {

this.client.send(serializedMessage);
break;
case this.client.CONNECTING:
case this.wsImpl.CONNECTING:
this.unsentMessagesQueue.push(message);

break;
Expand All @@ -397,7 +445,7 @@ export class SubscriptionClient {
}

private tryReconnect() {
if (!this.reconnect || this.backoff.attempts > this.reconnectionAttempts) {
if (!this.reconnect || this.backoff.attempts >= this.reconnectionAttempts) {
return;
}

Expand All @@ -410,8 +458,10 @@ export class SubscriptionClient {
this.reconnecting = true;
}

this.clearTryReconnectTimeout();

const delay = this.backoff.duration();
setTimeout(() => {
this.tryReconnectTimeoutId = setTimeout(() => {
this.connect();
}, delay);
}
Expand All @@ -424,13 +474,35 @@ export class SubscriptionClient {
}

private checkConnection() {
this.wasKeepAliveReceived ? this.wasKeepAliveReceived = false : this.close(false);
if (this.wasKeepAliveReceived) {
this.wasKeepAliveReceived = false;
return;
}

if (!this.reconnecting) {
this.close(false, true);
}
}

private checkMaxConnectTimeout() {
this.clearMaxConnectTimeout();

// Max timeout trying to connect
this.maxConnectTimeoutId = setTimeout(() => {
if (this.status !== this.wsImpl.OPEN) {
this.close(false, true);
}
}, this.maxConnectTimeGenerator.duration());
}

private connect() {
this.client = new this.wsImpl(this.url, GRAPHQL_WS);

this.checkMaxConnectTimeout();

this.client.onopen = () => {
this.clearMaxConnectTimeout();
this.closedByUser = false;
this.eventEmitter.emit(this.reconnecting ? 'reconnecting' : 'connecting');

const payload: ConnectionParams = typeof this.connectionParams === 'function' ? this.connectionParams() : this.connectionParams;
Expand All @@ -441,12 +513,8 @@ export class SubscriptionClient {
};

this.client.onclose = () => {
this.eventEmitter.emit('disconnected');

if (this.forceClose) {
this.forceClose = false;
} else {
this.tryReconnect();
if ( !this.closedByUser ) {
this.close(false, false);
}
};

Expand Down Expand Up @@ -493,6 +561,7 @@ export class SubscriptionClient {
this.eventEmitter.emit(this.reconnecting ? 'reconnected' : 'connected');
this.reconnecting = false;
this.backoff.reset();
this.maxConnectTimeGenerator.reset();

if (this.connectionCallback) {
this.connectionCallback();
Expand Down Expand Up @@ -522,10 +591,11 @@ export class SubscriptionClient {
this.checkConnection();
}

if (this.checkConnectionTimeoutId) {
clearTimeout(this.checkConnectionTimeoutId);
if (this.checkConnectionIntervalId) {
clearInterval(this.checkConnectionIntervalId);
this.checkConnection();
}
this.checkConnectionTimeoutId = setTimeout(this.checkConnection.bind(this), this.wsTimeout);
this.checkConnectionIntervalId = setInterval(this.checkConnection.bind(this), this.wsTimeout);
break;

default:
Expand Down
2 changes: 1 addition & 1 deletion src/defaults.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const WS_TIMEOUT = 30000;
const WS_TIMEOUT = 10000;

export {
WS_TIMEOUT,
Expand Down
Loading