Handle network connectivity changes in websocket (#30788)
This commit introduces listeners for network changes that will: - Test the websocket if we get an offline event to check if we have disconnected. - Re-connect the websocket immediately if we get an online event, and we are disconnected. Additionally when a ping fails, we now immediately call the onclose() callback instead of waiting for the system to trigger it when the broken websocket closes. This allows us to re-connect much more quickly (since we don't have to wait for the broken websocket to get cleaned up by the system).
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
190b4e7f03
Коммит
67ab69606a
@@ -110,7 +110,6 @@ exports[`PostBodyAdditionalContent with a normal link Should render the plugin c
|
|||||||
"conn": null,
|
"conn": null,
|
||||||
"connectFailCount": 0,
|
"connectFailCount": 0,
|
||||||
"connectionId": "",
|
"connectionId": "",
|
||||||
"connectionUrl": null,
|
|
||||||
"errorCallback": null,
|
"errorCallback": null,
|
||||||
"errorListeners": Set {},
|
"errorListeners": Set {},
|
||||||
"eventCallback": null,
|
"eventCallback": null,
|
||||||
@@ -119,6 +118,8 @@ exports[`PostBodyAdditionalContent with a normal link Should render the plugin c
|
|||||||
"messageListeners": Set {},
|
"messageListeners": Set {},
|
||||||
"missedEventCallback": null,
|
"missedEventCallback": null,
|
||||||
"missedMessageListeners": Set {},
|
"missedMessageListeners": Set {},
|
||||||
|
"offlineHandler": null,
|
||||||
|
"onlineHandler": null,
|
||||||
"pingInterval": null,
|
"pingInterval": null,
|
||||||
"postedAck": false,
|
"postedAck": false,
|
||||||
"reconnectCallback": null,
|
"reconnectCallback": null,
|
||||||
@@ -128,6 +129,7 @@ exports[`PostBodyAdditionalContent with a normal link Should render the plugin c
|
|||||||
"responseSequence": 1,
|
"responseSequence": 1,
|
||||||
"serverHostname": "",
|
"serverHostname": "",
|
||||||
"serverSequence": 0,
|
"serverSequence": 0,
|
||||||
|
"waitingForPong": false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -168,7 +170,6 @@ exports[`PostBodyAdditionalContent with a normal link Should render the plugin c
|
|||||||
"conn": null,
|
"conn": null,
|
||||||
"connectFailCount": 0,
|
"connectFailCount": 0,
|
||||||
"connectionId": "",
|
"connectionId": "",
|
||||||
"connectionUrl": null,
|
|
||||||
"errorCallback": null,
|
"errorCallback": null,
|
||||||
"errorListeners": Set {},
|
"errorListeners": Set {},
|
||||||
"eventCallback": null,
|
"eventCallback": null,
|
||||||
@@ -177,6 +178,8 @@ exports[`PostBodyAdditionalContent with a normal link Should render the plugin c
|
|||||||
"messageListeners": Set {},
|
"messageListeners": Set {},
|
||||||
"missedEventCallback": null,
|
"missedEventCallback": null,
|
||||||
"missedMessageListeners": Set {},
|
"missedMessageListeners": Set {},
|
||||||
|
"offlineHandler": null,
|
||||||
|
"onlineHandler": null,
|
||||||
"pingInterval": null,
|
"pingInterval": null,
|
||||||
"postedAck": false,
|
"postedAck": false,
|
||||||
"reconnectCallback": null,
|
"reconnectCallback": null,
|
||||||
@@ -186,6 +189,7 @@ exports[`PostBodyAdditionalContent with a normal link Should render the plugin c
|
|||||||
"responseSequence": 1,
|
"responseSequence": 1,
|
||||||
"serverHostname": "",
|
"serverHostname": "",
|
||||||
"serverSequence": 0,
|
"serverSequence": 0,
|
||||||
|
"waitingForPong": false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,13 +3,52 @@
|
|||||||
|
|
||||||
import WebSocketClient from './websocket';
|
import WebSocketClient from './websocket';
|
||||||
|
|
||||||
// Define some WebSocket globals that aren't defined in node
|
// Define some browser globals that aren't defined in node
|
||||||
if (typeof WebSocket === 'undefined') {
|
if (typeof WebSocket === 'undefined') {
|
||||||
(global as any).WebSocket = {
|
(global as any).WebSocket = {
|
||||||
CONNECTING: 0, OPEN: 1, CLOSING: 2, CLOSED: 3,
|
CONNECTING: 0, OPEN: 1, CLOSING: 2, CLOSED: 3,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mock window and navigator if they're not defined
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
const eventHandlers: {[key: string]: Array<(event: Event) => void>} = {};
|
||||||
|
|
||||||
|
// Create a mock window object with working event handlers
|
||||||
|
(global as any).window = {
|
||||||
|
addEventListener: jest.fn((event: string, handler: (event: Event) => void) => {
|
||||||
|
if (!eventHandlers[event]) {
|
||||||
|
eventHandlers[event] = [];
|
||||||
|
}
|
||||||
|
eventHandlers[event].push(handler);
|
||||||
|
}),
|
||||||
|
removeEventListener: jest.fn((event: string, handler: (event: Event) => void) => {
|
||||||
|
if (eventHandlers[event]) {
|
||||||
|
const index = eventHandlers[event].indexOf(handler);
|
||||||
|
if (index !== -1) {
|
||||||
|
eventHandlers[event].splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
dispatchEvent: jest.fn((event: Event) => {
|
||||||
|
const handlers = eventHandlers[event.type] || [];
|
||||||
|
handlers.forEach((handler) => handler(event));
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock Event class if it's not defined
|
||||||
|
if (typeof Event === 'undefined') {
|
||||||
|
(global as any).Event = class MockEvent {
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
constructor(type: string) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
class MockWebSocket {
|
class MockWebSocket {
|
||||||
readonly binaryType: BinaryType = 'blob';
|
readonly binaryType: BinaryType = 'blob';
|
||||||
readonly bufferedAmount: number = 0;
|
readonly bufferedAmount: number = 0;
|
||||||
@@ -286,7 +325,7 @@ describe('websocketclient', () => {
|
|||||||
},
|
},
|
||||||
minWebSocketRetryTime: 1,
|
minWebSocketRetryTime: 1,
|
||||||
reconnectJitterRange: 1,
|
reconnectJitterRange: 1,
|
||||||
clientPingInterval: 1,
|
clientPingInterval: 10,
|
||||||
});
|
});
|
||||||
|
|
||||||
let numPings = 0;
|
let numPings = 0;
|
||||||
@@ -321,14 +360,16 @@ describe('websocketclient', () => {
|
|||||||
if (mockWebSocket.onclose) {
|
if (mockWebSocket.onclose) {
|
||||||
mockWebSocket.onclose();
|
mockWebSocket.onclose();
|
||||||
}
|
}
|
||||||
if ((mockWebSocket.close as jest.Mock).mock.calls.length > 2) {
|
if (jest.mocked(mockWebSocket.close).mock.calls.length === 3) {
|
||||||
|
setTimeout(() => {
|
||||||
client.close();
|
client.close();
|
||||||
|
}, 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
client.initialize('mock.url');
|
client.initialize('mock.url');
|
||||||
|
|
||||||
jest.advanceTimersByTime(30);
|
jest.advanceTimersByTime(100);
|
||||||
|
|
||||||
client.close();
|
client.close();
|
||||||
|
|
||||||
@@ -418,4 +459,160 @@ describe('websocketclient', () => {
|
|||||||
|
|
||||||
jest.useRealTimers();
|
jest.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should add network event listener on initialize', () => {
|
||||||
|
// Mock window.addEventListener
|
||||||
|
const originalAddEventListener = window.addEventListener;
|
||||||
|
const originalRemoveEventListener = window.removeEventListener;
|
||||||
|
|
||||||
|
const addEventListenerMock = jest.fn();
|
||||||
|
const removeEventListenerMock = jest.fn();
|
||||||
|
|
||||||
|
window.addEventListener = addEventListenerMock;
|
||||||
|
window.removeEventListener = removeEventListenerMock;
|
||||||
|
|
||||||
|
// Create client
|
||||||
|
const mockWebSocket = new MockWebSocket();
|
||||||
|
const client = new WebSocketClient({
|
||||||
|
newWebSocketFn: (url: string) => {
|
||||||
|
mockWebSocket.url = url;
|
||||||
|
return mockWebSocket;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// No listeners should be added on construction
|
||||||
|
expect(addEventListenerMock).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Initialize should add listeners
|
||||||
|
client.initialize('mock.url');
|
||||||
|
|
||||||
|
// Verify event listeners are added on initialize
|
||||||
|
expect(addEventListenerMock).toHaveBeenCalledWith('online', expect.any(Function));
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
client.close();
|
||||||
|
|
||||||
|
// Verify event listeners are removed
|
||||||
|
expect(removeEventListenerMock).toHaveBeenCalledWith('online', expect.any(Function));
|
||||||
|
|
||||||
|
// Restore mocks
|
||||||
|
window.addEventListener = originalAddEventListener;
|
||||||
|
window.removeEventListener = originalRemoveEventListener;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should reconnect when network comes online', () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
var connected = true;
|
||||||
|
const mockWebSocket = new MockWebSocket();
|
||||||
|
const newWebSocketFn = jest.fn((url: string) => {
|
||||||
|
mockWebSocket.url = url;
|
||||||
|
|
||||||
|
// selectively simulate the network being down
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!connected && mockWebSocket.onclose) {
|
||||||
|
mockWebSocket.close();
|
||||||
|
}
|
||||||
|
}, 1);
|
||||||
|
|
||||||
|
return mockWebSocket;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use a small minWebSocketRetryTime to speed up the test
|
||||||
|
const client = new WebSocketClient({
|
||||||
|
newWebSocketFn,
|
||||||
|
minWebSocketRetryTime: 100,
|
||||||
|
maxWebSocketRetryTime: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize the client
|
||||||
|
client.initialize('mock.url');
|
||||||
|
mockWebSocket.open();
|
||||||
|
expect(newWebSocketFn).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// Simulate network going offline
|
||||||
|
mockWebSocket.close();
|
||||||
|
connected = false;
|
||||||
|
|
||||||
|
// Connection should be closed
|
||||||
|
expect(mockWebSocket.readyState).toBe(WebSocket.CLOSED);
|
||||||
|
|
||||||
|
// Wait a very long time to max out retry timeout
|
||||||
|
jest.advanceTimersByTime(10000);
|
||||||
|
|
||||||
|
// Reset the mock to track the next retry
|
||||||
|
// which should be quicker than the max
|
||||||
|
newWebSocketFn.mockClear();
|
||||||
|
|
||||||
|
// Simulate network coming back online
|
||||||
|
connected = true;
|
||||||
|
const onlineEvent = new Event('online');
|
||||||
|
window.dispatchEvent(onlineEvent);
|
||||||
|
|
||||||
|
// Should not reconnect immediately (should wait for the timeout)
|
||||||
|
expect(newWebSocketFn).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Advance timers to trigger the reconnect
|
||||||
|
jest.advanceTimersByTime(110);
|
||||||
|
|
||||||
|
// Should have reconnected after the timeout
|
||||||
|
expect(newWebSocketFn).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
client.close();
|
||||||
|
|
||||||
|
// Reset timers
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should send ping when network goes offline', () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
const mockWebSocket = new MockWebSocket();
|
||||||
|
const client = new WebSocketClient({
|
||||||
|
newWebSocketFn: (url: string) => {
|
||||||
|
mockWebSocket.url = url;
|
||||||
|
setTimeout(() => {
|
||||||
|
if (mockWebSocket.onopen) {
|
||||||
|
mockWebSocket.open();
|
||||||
|
}
|
||||||
|
}, 1);
|
||||||
|
return mockWebSocket;
|
||||||
|
},
|
||||||
|
clientPingInterval: 300,
|
||||||
|
});
|
||||||
|
|
||||||
|
let numPings = 0;
|
||||||
|
mockWebSocket.send = (evt) => {
|
||||||
|
const msg = JSON.parse(evt);
|
||||||
|
if (msg.action !== 'ping') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
numPings++;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openSpy = jest.spyOn(mockWebSocket, 'open');
|
||||||
|
const closeSpy = jest.spyOn(mockWebSocket, 'close');
|
||||||
|
|
||||||
|
client.initialize('mock.url');
|
||||||
|
jest.advanceTimersByTime(10);
|
||||||
|
|
||||||
|
expect(mockWebSocket.readyState).toBe(WebSocket.OPEN);
|
||||||
|
expect(openSpy).toBeCalledTimes(1);
|
||||||
|
expect(closeSpy).toBeCalledTimes(0);
|
||||||
|
expect(numPings).toBe(1);
|
||||||
|
|
||||||
|
// Simulate network going offline
|
||||||
|
const offlineEvent = new Event('offline');
|
||||||
|
window.dispatchEvent(offlineEvent);
|
||||||
|
jest.advanceTimersByTime(10);
|
||||||
|
|
||||||
|
client.close();
|
||||||
|
|
||||||
|
expect(openSpy).toBeCalledTimes(1);
|
||||||
|
expect(closeSpy).toBeCalledTimes(1);
|
||||||
|
expect(numPings).toBe(2);
|
||||||
|
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ export default class WebSocketClient {
|
|||||||
private config: WebSocketClientConfig;
|
private config: WebSocketClientConfig;
|
||||||
|
|
||||||
private conn: WebSocket | null;
|
private conn: WebSocket | null;
|
||||||
private connectionUrl: string | null;
|
|
||||||
|
|
||||||
// responseSequence is the number to track a response sent
|
// responseSequence is the number to track a response sent
|
||||||
// via the websocket. A response will always have the same sequence number
|
// via the websocket. A response will always have the same sequence number
|
||||||
@@ -89,12 +88,17 @@ export default class WebSocketClient {
|
|||||||
private postedAck: boolean;
|
private postedAck: boolean;
|
||||||
|
|
||||||
private pingInterval: ReturnType<typeof setInterval> | null;
|
private pingInterval: ReturnType<typeof setInterval> | null;
|
||||||
|
private waitingForPong: boolean;
|
||||||
|
|
||||||
|
// reconnectTimeout is used for automatic reconnect after socket close
|
||||||
private reconnectTimeout: ReturnType<typeof setTimeout> | null;
|
private reconnectTimeout: ReturnType<typeof setTimeout> | null;
|
||||||
|
|
||||||
|
// Network event handlers
|
||||||
|
private onlineHandler: (() => void) | null = null;
|
||||||
|
private offlineHandler: (() => void) | null = null;
|
||||||
|
|
||||||
constructor(config?: Partial<WebSocketClientConfig>) {
|
constructor(config?: Partial<WebSocketClientConfig>) {
|
||||||
this.conn = null;
|
this.conn = null;
|
||||||
this.connectionUrl = null;
|
|
||||||
this.responseSequence = 1;
|
this.responseSequence = 1;
|
||||||
this.serverSequence = 0;
|
this.serverSequence = 0;
|
||||||
this.connectFailCount = 0;
|
this.connectFailCount = 0;
|
||||||
@@ -105,12 +109,13 @@ export default class WebSocketClient {
|
|||||||
this.reconnectTimeout = null;
|
this.reconnectTimeout = null;
|
||||||
this.config = {...defaultWebSocketClientConfig, ...config};
|
this.config = {...defaultWebSocketClientConfig, ...config};
|
||||||
this.pingInterval = null;
|
this.pingInterval = null;
|
||||||
|
this.waitingForPong = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// on connect, only send auth cookie and blank state.
|
// on connect, only send auth cookie and blank state.
|
||||||
// on hello, get the connectionID and store it.
|
// on hello, get the connectionID and store it.
|
||||||
// on reconnect, send cookie, connectionID, sequence number.
|
// on reconnect, send cookie, connectionID, sequence number.
|
||||||
initialize(connectionUrl = this.connectionUrl, token?: string, postedAck?: boolean) {
|
initialize(connectionUrl: string, token?: string, postedAck?: boolean) {
|
||||||
if (this.conn) {
|
if (this.conn) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -135,6 +140,61 @@ export default class WebSocketClient {
|
|||||||
this.postedAck = postedAck;
|
this.postedAck = postedAck;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup network event listener
|
||||||
|
// Remove existing listeners if any
|
||||||
|
if (this.onlineHandler) {
|
||||||
|
window.removeEventListener('online', this.onlineHandler);
|
||||||
|
}
|
||||||
|
if (this.offlineHandler) {
|
||||||
|
window.removeEventListener('offline', this.offlineHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onlineHandler = () => {
|
||||||
|
// If we're already connected, don't need to do anything
|
||||||
|
if (this.conn && this.conn.readyState === WebSocket.OPEN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('network online event received, scheduling reconnect'); // eslint-disable-line no-console
|
||||||
|
|
||||||
|
// Set a timer to reconnect after a delay to avoid rapid connection attempts
|
||||||
|
this.clearReconnectTimeout();
|
||||||
|
this.reconnectTimeout = setTimeout(
|
||||||
|
() => {
|
||||||
|
this.reconnectTimeout = null;
|
||||||
|
this.initialize(connectionUrl, token, this.postedAck);
|
||||||
|
},
|
||||||
|
this.config.minWebSocketRetryTime,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.offlineHandler = () => {
|
||||||
|
// If we've detected a full disconnection, don't need to do anything more
|
||||||
|
if (this.conn && this.conn.readyState !== WebSocket.OPEN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('network offline event received, checking connection'); // eslint-disable-line no-console
|
||||||
|
|
||||||
|
// If we haven't detected a full disconnection,
|
||||||
|
// send a ping immediately to test the socket
|
||||||
|
//
|
||||||
|
// NOTE: There is a potential race condition here with the regular ping interval.
|
||||||
|
// If we send this ping close to when the interval check occurs (e.g., at 29.5s of the 30s interval),
|
||||||
|
// the server might not have enough time to respond before the interval executes.
|
||||||
|
// When the interval runs, it will see we're still waiting for a pong and close the connection,
|
||||||
|
// even though the network might be fine and the server just needs a bit more time to respond.
|
||||||
|
// This race condition is rare and the impact is just an unnecessary reconnect,
|
||||||
|
// so we accept this limitation to keep the implementation simple.
|
||||||
|
this.waitingForPong = true;
|
||||||
|
this.ping(() => {
|
||||||
|
this.waitingForPong = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('online', this.onlineHandler);
|
||||||
|
window.addEventListener('offline', this.offlineHandler);
|
||||||
|
|
||||||
// Add connection id, and last_sequence_number to the query param.
|
// Add connection id, and last_sequence_number to the query param.
|
||||||
// We cannot use a cookie because it will bleed across tabs.
|
// We cannot use a cookie because it will bleed across tabs.
|
||||||
// We cannot also send it as part of the auth_challenge, because the session cookie is already sent with the request.
|
// We cannot also send it as part of the auth_challenge, because the session cookie is already sent with the request.
|
||||||
@@ -144,57 +204,8 @@ export default class WebSocketClient {
|
|||||||
} else {
|
} else {
|
||||||
this.conn = new WebSocket(websocketUrl);
|
this.conn = new WebSocket(websocketUrl);
|
||||||
}
|
}
|
||||||
this.connectionUrl = connectionUrl;
|
|
||||||
|
|
||||||
this.conn.onopen = () => {
|
const onclose = () => {
|
||||||
if (token) {
|
|
||||||
this.sendMessage('authentication_challenge', {token});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.connectFailCount > 0) {
|
|
||||||
console.log('websocket re-established connection'); //eslint-disable-line no-console
|
|
||||||
|
|
||||||
this.reconnectCallback?.();
|
|
||||||
this.reconnectListeners.forEach((listener) => listener());
|
|
||||||
} else if (this.firstConnectCallback || this.firstConnectListeners.size > 0) {
|
|
||||||
this.firstConnectCallback?.();
|
|
||||||
this.firstConnectListeners.forEach((listener) => listener());
|
|
||||||
}
|
|
||||||
|
|
||||||
this.stopPingInterval();
|
|
||||||
|
|
||||||
// Send a ping immediately to test the socket
|
|
||||||
var waitingForPong = true;
|
|
||||||
this.ping(() => {
|
|
||||||
waitingForPong = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// And every 30 seconds after, checking to ensure
|
|
||||||
// we're getting responses from the server
|
|
||||||
this.pingInterval = setInterval(
|
|
||||||
() => {
|
|
||||||
if (!waitingForPong) {
|
|
||||||
waitingForPong = true;
|
|
||||||
this.ping(() => {
|
|
||||||
waitingForPong = false;
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('ping received no response within time limit: re-establishing websocket'); //eslint-disable-line no-console
|
|
||||||
|
|
||||||
// We are not calling this.close() because we need to auto-restart.
|
|
||||||
this.connectFailCount = 0;
|
|
||||||
this.responseSequence = 1;
|
|
||||||
this.stopPingInterval();
|
|
||||||
this.conn?.close(); // Will auto-reconnect after configured retry time
|
|
||||||
},
|
|
||||||
this.config.clientPingInterval);
|
|
||||||
|
|
||||||
this.connectFailCount = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.conn.onclose = () => {
|
|
||||||
this.conn = null;
|
this.conn = null;
|
||||||
this.responseSequence = 1;
|
this.responseSequence = 1;
|
||||||
|
|
||||||
@@ -231,11 +242,73 @@ export default class WebSocketClient {
|
|||||||
this.reconnectTimeout = setTimeout(
|
this.reconnectTimeout = setTimeout(
|
||||||
() => {
|
() => {
|
||||||
this.reconnectTimeout = null;
|
this.reconnectTimeout = null;
|
||||||
this.initialize(this.connectionUrl, token, this.postedAck);
|
this.initialize(connectionUrl, token, this.postedAck);
|
||||||
},
|
},
|
||||||
retryTime,
|
retryTime,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
this.conn.onclose = onclose;
|
||||||
|
|
||||||
|
this.conn.onopen = () => {
|
||||||
|
if (token) {
|
||||||
|
this.sendMessage('authentication_challenge', {token});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.connectFailCount > 0) {
|
||||||
|
console.log('websocket re-established connection'); //eslint-disable-line no-console
|
||||||
|
|
||||||
|
this.reconnectCallback?.();
|
||||||
|
this.reconnectListeners.forEach((listener) => listener());
|
||||||
|
} else if (this.firstConnectCallback || this.firstConnectListeners.size > 0) {
|
||||||
|
this.firstConnectCallback?.();
|
||||||
|
this.firstConnectListeners.forEach((listener) => listener());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stopPingInterval();
|
||||||
|
|
||||||
|
// Send a ping immediately to test the socket
|
||||||
|
this.waitingForPong = true;
|
||||||
|
this.ping(() => {
|
||||||
|
this.waitingForPong = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// And every 30 seconds after, checking to ensure
|
||||||
|
// we're getting responses from the server
|
||||||
|
this.pingInterval = setInterval(
|
||||||
|
() => {
|
||||||
|
if (!this.waitingForPong) {
|
||||||
|
this.waitingForPong = true;
|
||||||
|
this.ping(() => {
|
||||||
|
this.waitingForPong = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stopPingInterval();
|
||||||
|
|
||||||
|
// If we aren't connected, we should already be trying to
|
||||||
|
// re-connect the websocket. So there's nothing more to do here.
|
||||||
|
if (!this.conn || this.conn.readyState !== WebSocket.OPEN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('ping received no response within time limit: re-establishing websocket'); //eslint-disable-line no-console
|
||||||
|
|
||||||
|
// Calling conn.close() will trigger the onclose callback,
|
||||||
|
// but sometimes with a significant delay. So instead, we
|
||||||
|
// call the onclose callback ourselves immediately. We also
|
||||||
|
// unset the callback on the old connection to ensure it
|
||||||
|
// is only called once.
|
||||||
|
this.connectFailCount = 0;
|
||||||
|
this.responseSequence = 1;
|
||||||
|
this.conn.onclose = () => {};
|
||||||
|
this.conn.close();
|
||||||
|
onclose();
|
||||||
|
},
|
||||||
|
this.config.clientPingInterval);
|
||||||
|
|
||||||
|
this.connectFailCount = 0;
|
||||||
|
};
|
||||||
|
|
||||||
this.conn.onerror = (evt) => {
|
this.conn.onerror = (evt) => {
|
||||||
if (this.connectFailCount <= 1) {
|
if (this.connectFailCount <= 1) {
|
||||||
@@ -433,17 +506,31 @@ export default class WebSocketClient {
|
|||||||
close() {
|
close() {
|
||||||
this.connectFailCount = 0;
|
this.connectFailCount = 0;
|
||||||
this.responseSequence = 1;
|
this.responseSequence = 1;
|
||||||
if (this.reconnectTimeout) {
|
this.clearReconnectTimeout();
|
||||||
clearTimeout(this.reconnectTimeout);
|
|
||||||
this.reconnectTimeout = null;
|
|
||||||
}
|
|
||||||
this.stopPingInterval();
|
this.stopPingInterval();
|
||||||
|
|
||||||
if (this.conn && this.conn.readyState === WebSocket.OPEN) {
|
if (this.conn && this.conn.readyState === WebSocket.OPEN) {
|
||||||
this.conn.onclose = () => {};
|
this.conn.onclose = () => {};
|
||||||
this.conn.close();
|
this.conn.close();
|
||||||
this.conn = null;
|
this.conn = null;
|
||||||
console.log('websocket closed'); //eslint-disable-line no-console
|
console.log('websocket closed'); //eslint-disable-line no-console
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.onlineHandler) {
|
||||||
|
window.removeEventListener('online', this.onlineHandler);
|
||||||
|
this.onlineHandler = null;
|
||||||
|
}
|
||||||
|
if (this.offlineHandler) {
|
||||||
|
window.removeEventListener('offline', this.offlineHandler);
|
||||||
|
this.offlineHandler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearReconnectTimeout() {
|
||||||
|
if (this.reconnectTimeout) {
|
||||||
|
clearTimeout(this.reconnectTimeout);
|
||||||
|
this.reconnectTimeout = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stopPingInterval() {
|
stopPingInterval() {
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user