Improve websocket behavior (#7976)
* Improve websocket behavior * Revert uninteded changes * Address feedback
This commit is contained in:
parent
dddd09aa9b
commit
b694809da1
6 changed files with 162 additions and 189 deletions
|
|
@ -22,7 +22,7 @@ import {getDeviceToken} from '@queries/app/global';
|
|||
import {getChannelById, queryAllChannelsForTeam, queryChannelsById} from '@queries/servers/channel';
|
||||
import {prepareModels, truncateCrtRelatedTables} from '@queries/servers/entry';
|
||||
import {getHasCRTChanged} from '@queries/servers/preference';
|
||||
import {getConfig, getCurrentChannelId, getCurrentTeamId, getIsDataRetentionEnabled, getPushVerificationStatus, getWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {getConfig, getCurrentChannelId, getCurrentTeamId, getIsDataRetentionEnabled, getPushVerificationStatus, getLastFullSync, setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {deleteMyTeams, getAvailableTeamIds, getTeamChannelHistory, queryMyTeams, queryMyTeamsByIds, queryTeamsById} from '@queries/servers/team';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
import {isDMorGM, sortChannelsByDisplayName} from '@utils/channel';
|
||||
|
|
@ -124,7 +124,7 @@ const entryRest = async (serverUrl: string, teamId?: string, channelId?: string,
|
|||
}
|
||||
const {database} = operator;
|
||||
|
||||
const lastDisconnectedAt = since || await getWebSocketLastDisconnected(database);
|
||||
const lastDisconnectedAt = since || await getLastFullSync(database);
|
||||
|
||||
const fetchedData = await fetchAppEntryData(serverUrl, lastDisconnectedAt, teamId, channelId);
|
||||
if ('error' in fetchedData) {
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ import {
|
|||
} from '@calls/connection/websocket_event_handlers';
|
||||
import {isSupportedServerCalls} from '@calls/utils';
|
||||
import {Screens, WebsocketEvents} from '@constants';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import AppsManager from '@managers/apps_manager';
|
||||
import {getActiveServerUrl} from '@queries/app/servers';
|
||||
|
|
@ -51,8 +50,8 @@ import {
|
|||
getCurrentChannelId,
|
||||
getCurrentTeamId,
|
||||
getLicense,
|
||||
getWebSocketLastDisconnected,
|
||||
resetWebSocketLastDisconnected,
|
||||
getLastFullSync,
|
||||
setLastFullSync,
|
||||
} from '@queries/servers/system';
|
||||
import {getIsCRTEnabled} from '@queries/servers/thread';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
|
|
@ -124,22 +123,6 @@ export async function handleReconnect(serverUrl: string) {
|
|||
return doReconnect(serverUrl);
|
||||
}
|
||||
|
||||
export async function handleClose(serverUrl: string, lastDisconnect: number) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return;
|
||||
}
|
||||
await operator.handleSystem({
|
||||
systems: [
|
||||
{
|
||||
id: SYSTEM_IDENTIFIERS.WEBSOCKET,
|
||||
value: lastDisconnect.toString(10),
|
||||
},
|
||||
],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
}
|
||||
|
||||
async function doReconnect(serverUrl: string) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
|
|
@ -153,14 +136,14 @@ async function doReconnect(serverUrl: string) {
|
|||
|
||||
const {database} = operator;
|
||||
|
||||
const lastDisconnectedAt = await getWebSocketLastDisconnected(database);
|
||||
resetWebSocketLastDisconnected(operator);
|
||||
const lastFullSync = await getLastFullSync(database);
|
||||
const now = Date.now();
|
||||
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
const currentChannelId = await getCurrentChannelId(database);
|
||||
|
||||
setTeamLoading(serverUrl, true);
|
||||
const entryData = await entry(serverUrl, currentTeamId, currentChannelId, lastDisconnectedAt);
|
||||
const entryData = await entry(serverUrl, currentTeamId, currentChannelId, lastFullSync);
|
||||
if ('error' in entryData) {
|
||||
setTeamLoading(serverUrl, false);
|
||||
return entryData.error;
|
||||
|
|
@ -174,6 +157,8 @@ async function doReconnect(serverUrl: string) {
|
|||
await operator.batchRecords(models, 'doReconnect');
|
||||
}
|
||||
|
||||
await setLastFullSync(operator, now);
|
||||
|
||||
const tabletDevice = isTablet();
|
||||
const isActiveServer = (await getActiveServerUrl()) === serverUrl;
|
||||
if (isActiveServer && tabletDevice && initialChannelId === currentChannelId) {
|
||||
|
|
@ -194,7 +179,7 @@ async function doReconnect(serverUrl: string) {
|
|||
loadConfigAndCalls(serverUrl, currentUserId);
|
||||
}
|
||||
|
||||
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId);
|
||||
await deferredAppEntryActions(serverUrl, lastFullSync, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId);
|
||||
|
||||
openAllUnreadChannels(serverUrl);
|
||||
|
||||
|
|
|
|||
|
|
@ -15,56 +15,51 @@ const MAX_WEBSOCKET_FAILS = 7;
|
|||
const WEBSOCKET_TIMEOUT = toMilliseconds({seconds: 30});
|
||||
const MIN_WEBSOCKET_RETRY_TIME = toMilliseconds({seconds: 3});
|
||||
const MAX_WEBSOCKET_RETRY_TIME = toMilliseconds({minutes: 5});
|
||||
const DEFAULT_OPTIONS = {
|
||||
forceConnection: true,
|
||||
};
|
||||
|
||||
export default class WebSocketClient {
|
||||
private conn?: WebSocketClientInterface;
|
||||
private connectionTimeout: NodeJS.Timeout | undefined;
|
||||
private connectionId: string;
|
||||
private connectionId = '';
|
||||
private token: string;
|
||||
private stop = false;
|
||||
private url = '';
|
||||
private serverUrl: string;
|
||||
private connectFailCount = 0;
|
||||
|
||||
// The first time we connect to a server (on init or login)
|
||||
// we do the sync out of the websocket lifecycle.
|
||||
// This is used to avoid calling twice to the sync logic.
|
||||
private shouldSkipSync = false;
|
||||
|
||||
// responseSequence is the number to track a response sent
|
||||
// via the websocket. A response will always have the same sequence number
|
||||
// as the request.
|
||||
private responseSequence: number;
|
||||
private responseSequence = 1;
|
||||
|
||||
// serverSequence is the incrementing sequence number from the
|
||||
// server-sent event stream.
|
||||
private serverSequence: number;
|
||||
private connectFailCount: number;
|
||||
private serverSequence = 0;
|
||||
|
||||
// Callbacks
|
||||
private eventCallback?: Function;
|
||||
private firstConnectCallback?: () => void;
|
||||
private missedEventsCallback?: () => void;
|
||||
private reconnectCallback?: () => void;
|
||||
private reliableReconnectCallback?: () => void;
|
||||
private errorCallback?: Function;
|
||||
private closeCallback?: (connectFailCount: number, lastDisconnect: number) => void;
|
||||
private closeCallback?: (connectFailCount: number) => void;
|
||||
private connectingCallback?: () => void;
|
||||
private stop: boolean;
|
||||
private lastConnect: number;
|
||||
private lastDisconnect: number;
|
||||
private url = '';
|
||||
|
||||
private serverUrl: string;
|
||||
private hasReliablyReconnect = false;
|
||||
|
||||
constructor(serverUrl: string, token: string, lastDisconnect = 0) {
|
||||
this.connectionId = '';
|
||||
constructor(serverUrl: string, token: string) {
|
||||
this.token = token;
|
||||
this.responseSequence = 1;
|
||||
this.serverSequence = 0;
|
||||
this.connectFailCount = 0;
|
||||
this.stop = false;
|
||||
this.serverUrl = serverUrl;
|
||||
this.lastConnect = 0;
|
||||
this.lastDisconnect = lastDisconnect;
|
||||
}
|
||||
|
||||
public async initialize(opts = {}) {
|
||||
const defaults = {
|
||||
forceConnection: true,
|
||||
};
|
||||
|
||||
const {forceConnection} = Object.assign({}, defaults, opts);
|
||||
public async initialize(opts = {}, shouldSkipSync = false) {
|
||||
const {forceConnection} = Object.assign({}, DEFAULT_OPTIONS, opts);
|
||||
|
||||
if (forceConnection) {
|
||||
this.stop = false;
|
||||
|
|
@ -114,17 +109,19 @@ export default class WebSocketClient {
|
|||
|
||||
// Manually changing protocol since getOrCreateWebsocketClient does not accept http/s
|
||||
if (this.url.startsWith('https:')) {
|
||||
this.url = 'wss:' + this.url.substr('https:'.length);
|
||||
this.url = 'wss:' + this.url.substring('https:'.length);
|
||||
}
|
||||
|
||||
if (this.url.startsWith('http:')) {
|
||||
this.url = 'ws:' + this.url.substr('http:'.length);
|
||||
this.url = 'ws:' + this.url.substring('http:'.length);
|
||||
}
|
||||
|
||||
if (this.connectFailCount === 0) {
|
||||
logInfo('websocket connecting to ' + this.url);
|
||||
}
|
||||
|
||||
this.shouldSkipSync = shouldSkipSync;
|
||||
|
||||
try {
|
||||
const headers: ClientHeaders = {origin};
|
||||
if (Platform.OS === 'android') {
|
||||
|
|
@ -139,9 +136,7 @@ export default class WebSocketClient {
|
|||
// In case turning on/off Wi-fi on Samsung devices
|
||||
// the websocket will call onClose then onError then initialize again with readyState CLOSED, we need to open it again
|
||||
if (this.conn.readyState === WebSocketReadyState.CLOSED) {
|
||||
if (this.connectionTimeout) {
|
||||
clearTimeout(this.connectionTimeout);
|
||||
}
|
||||
clearTimeout(this.connectionTimeout);
|
||||
this.conn.open();
|
||||
}
|
||||
return;
|
||||
|
|
@ -152,7 +147,7 @@ export default class WebSocketClient {
|
|||
}
|
||||
|
||||
this.conn!.onOpen(() => {
|
||||
this.lastConnect = Date.now();
|
||||
clearTimeout(this.connectionTimeout);
|
||||
|
||||
// No need to reset sequence number here.
|
||||
if (!reliableWebSockets) {
|
||||
|
|
@ -165,34 +160,35 @@ export default class WebSocketClient {
|
|||
this.sendMessage('authentication_challenge', {token: this.token});
|
||||
}
|
||||
|
||||
if (this.connectFailCount > 0) {
|
||||
if (this.shouldSkipSync) {
|
||||
logInfo('websocket connected to', this.url);
|
||||
this.firstConnectCallback?.();
|
||||
} else {
|
||||
logInfo('websocket re-established connection to', this.url);
|
||||
if (!reliableWebSockets && this.reconnectCallback) {
|
||||
this.reconnectCallback();
|
||||
} else if (reliableWebSockets) {
|
||||
// If a sync is needed, it is handled when receiving the HELLO websocket message
|
||||
this.reliableReconnectCallback?.();
|
||||
if (this.serverSequence && this.missedEventsCallback) {
|
||||
this.missedEventsCallback();
|
||||
}
|
||||
this.hasReliablyReconnect = true;
|
||||
}
|
||||
} else if (this.firstConnectCallback) {
|
||||
logInfo('websocket connected to', this.url);
|
||||
this.firstConnectCallback();
|
||||
}
|
||||
|
||||
this.connectFailCount = 0;
|
||||
});
|
||||
|
||||
this.conn!.onClose(() => {
|
||||
const now = Date.now();
|
||||
if (this.lastDisconnect < this.lastConnect) {
|
||||
this.lastDisconnect = now;
|
||||
}
|
||||
|
||||
clearTimeout(this.connectionTimeout);
|
||||
this.conn = undefined;
|
||||
this.responseSequence = 1;
|
||||
this.hasReliablyReconnect = false;
|
||||
|
||||
// We skip the sync on first connect, since we are syncing along
|
||||
// the init logic. If the connection closes at any point after that,
|
||||
// we don't want to skip the sync. If we keep the same connection and
|
||||
// reliable websockets are enabled this won't trigger a new sync.
|
||||
this.shouldSkipSync = false;
|
||||
|
||||
if (this.connectFailCount === 0) {
|
||||
logInfo('websocket closed', this.url);
|
||||
|
|
@ -201,7 +197,7 @@ export default class WebSocketClient {
|
|||
this.connectFailCount++;
|
||||
|
||||
if (this.closeCallback) {
|
||||
this.closeCallback(this.connectFailCount, this.lastDisconnect);
|
||||
this.closeCallback(this.connectFailCount);
|
||||
}
|
||||
|
||||
if (this.stop) {
|
||||
|
|
@ -212,22 +208,13 @@ export default class WebSocketClient {
|
|||
|
||||
// If we've failed a bunch of connections then start backing off
|
||||
if (this.connectFailCount > MAX_WEBSOCKET_FAILS) {
|
||||
retryTime = MIN_WEBSOCKET_RETRY_TIME * this.connectFailCount;
|
||||
if (retryTime > MAX_WEBSOCKET_RETRY_TIME) {
|
||||
retryTime = MAX_WEBSOCKET_RETRY_TIME;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.connectionTimeout) {
|
||||
clearTimeout(this.connectionTimeout);
|
||||
retryTime = Math.min(MIN_WEBSOCKET_RETRY_TIME * this.connectFailCount, MAX_WEBSOCKET_RETRY_TIME);
|
||||
}
|
||||
|
||||
this.connectionTimeout = setTimeout(
|
||||
() => {
|
||||
if (this.stop) {
|
||||
if (this.connectionTimeout) {
|
||||
clearTimeout(this.connectionTimeout);
|
||||
}
|
||||
clearTimeout(this.connectionTimeout);
|
||||
return;
|
||||
}
|
||||
this.initialize(opts);
|
||||
|
|
@ -238,7 +225,6 @@ export default class WebSocketClient {
|
|||
|
||||
this.conn!.onError((evt: any) => {
|
||||
if (evt.url === this.url) {
|
||||
this.hasReliablyReconnect = false;
|
||||
if (this.connectFailCount <= 1) {
|
||||
logError('websocket error', this.url);
|
||||
logError('WEBSOCKET ERROR EVENT', evt);
|
||||
|
|
@ -263,7 +249,7 @@ export default class WebSocketClient {
|
|||
} else if (this.eventCallback) {
|
||||
if (reliableWebSockets) {
|
||||
// We check the hello packet, which is always the first packet in a stream.
|
||||
if (msg.event === WebsocketEvents.HELLO && this.reconnectCallback) {
|
||||
if (msg.event === WebsocketEvents.HELLO) {
|
||||
logInfo(this.url, 'got connection id ', msg.data.connection_id);
|
||||
|
||||
// If we already have a connectionId present, and server sends a different one,
|
||||
|
|
@ -271,14 +257,16 @@ export default class WebSocketClient {
|
|||
// If the server is not available the first time we try to connect, we won't have a connection id
|
||||
// but still we need to sync.
|
||||
// Then we do the sync calls, and reset sequence number to 0.
|
||||
if (
|
||||
(this.connectionId !== '' && this.connectionId !== msg.data.connection_id) ||
|
||||
(this.hasReliablyReconnect && this.connectionId === '')
|
||||
) {
|
||||
logInfo(this.url, 'long timeout, or server restart, or sequence number is not found, or first connect after failure.');
|
||||
this.reconnectCallback();
|
||||
if (this.connectionId !== msg.data.connection_id) {
|
||||
if (this.connectionId) {
|
||||
logInfo(this.url, 'got a new connection due to long timeout, or server restart, or sequence number is not found');
|
||||
} else {
|
||||
logInfo(this.url, 'got the expected new connection id');
|
||||
}
|
||||
if (!this.shouldSkipSync) {
|
||||
this.reconnectCallback?.();
|
||||
}
|
||||
this.serverSequence = 0;
|
||||
this.hasReliablyReconnect = false;
|
||||
}
|
||||
|
||||
// If it's a fresh connection, we have to set the connectionId regardless.
|
||||
|
|
@ -290,16 +278,13 @@ export default class WebSocketClient {
|
|||
// we just disconnect and reconnect.
|
||||
if (msg.seq !== this.serverSequence) {
|
||||
logInfo(this.url, 'missed websocket event, act_seq=' + msg.seq + ' exp_seq=' + this.serverSequence);
|
||||
|
||||
// We are not calling this.close() because we need to auto-restart.
|
||||
this.connectFailCount = 0;
|
||||
this.responseSequence = 1;
|
||||
this.conn?.close(); // Will auto-reconnect after MIN_WEBSOCKET_RETRY_TIME.
|
||||
this.connectionId = ''; // There was some problem with the sequence number, so we reset the connection
|
||||
this.close(false); // Will auto-reconnect after MIN_WEBSOCKET_RETRY_TIME.
|
||||
return;
|
||||
}
|
||||
} else if (msg.seq !== this.serverSequence && this.reconnectCallback) {
|
||||
} else if (msg.seq !== this.serverSequence) {
|
||||
logInfo(this.url, 'missed websocket event, act_seq=' + msg.seq + ' exp_seq=' + this.serverSequence);
|
||||
this.reconnectCallback();
|
||||
this.reconnectCallback?.();
|
||||
}
|
||||
|
||||
this.serverSequence = msg.seq + 1;
|
||||
|
|
@ -338,7 +323,7 @@ export default class WebSocketClient {
|
|||
this.errorCallback = callback;
|
||||
}
|
||||
|
||||
public setCloseCallback(callback: (connectFailCount: number, lastDisconnect: number) => void) {
|
||||
public setCloseCallback(callback: (connectFailCount: number) => void) {
|
||||
this.closeCallback = callback;
|
||||
}
|
||||
|
||||
|
|
@ -346,14 +331,12 @@ export default class WebSocketClient {
|
|||
this.stop = stop;
|
||||
this.connectFailCount = 0;
|
||||
this.responseSequence = 1;
|
||||
this.hasReliablyReconnect = false;
|
||||
|
||||
if (this.conn && this.conn.readyState === WebSocketReadyState.OPEN) {
|
||||
this.conn.close();
|
||||
}
|
||||
clearTimeout(this.connectionTimeout);
|
||||
this.conn?.close();
|
||||
}
|
||||
|
||||
public invalidate() {
|
||||
clearTimeout(this.connectionTimeout);
|
||||
this.conn?.invalidate();
|
||||
this.conn = undefined;
|
||||
}
|
||||
|
|
@ -381,6 +364,6 @@ export default class WebSocketClient {
|
|||
}
|
||||
|
||||
public isConnected(): boolean {
|
||||
return this.conn?.readyState === WebSocketReadyState.OPEN; //|| (!this.stop && this.connectFailCount <= 2);
|
||||
return this.conn?.readyState === WebSocketReadyState.OPEN;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import NetInfo, {type NetInfoState} from '@react-native-community/netinfo';
|
||||
import {debounce, type DebouncedFunc} from 'lodash';
|
||||
import {AppState, type AppStateStatus} from 'react-native';
|
||||
import NetInfo, {type NetInfoState, type NetInfoSubscription} from '@react-native-community/netinfo';
|
||||
import {AppState, type AppStateStatus, type NativeEventSubscription} from 'react-native';
|
||||
import BackgroundTimer from 'react-native-background-timer';
|
||||
import {BehaviorSubject} from 'rxjs';
|
||||
import {distinctUntilChanged} from 'rxjs/operators';
|
||||
|
||||
import {setCurrentUserStatus} from '@actions/local/user';
|
||||
import {fetchStatusByIds} from '@actions/remote/user';
|
||||
import {handleClose, handleEvent, handleFirstConnect, handleReconnect} from '@actions/websocket';
|
||||
import {handleEvent, handleFirstConnect, handleReconnect} from '@actions/websocket';
|
||||
import WebSocketClient from '@client/websocket';
|
||||
import {General} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
|
|
@ -27,7 +26,7 @@ class WebsocketManager {
|
|||
private connectedSubjects: {[serverUrl: string]: BehaviorSubject<WebsocketConnectedState>} = {};
|
||||
|
||||
private clients: Record<string, WebSocketClient> = {};
|
||||
private connectionTimerIDs: Record<string, DebouncedFunc<() => void>> = {};
|
||||
private connectionTimerIDs: Record<string, NodeJS.Timeout> = {};
|
||||
private isBackgroundTimerRunning = false;
|
||||
private netConnected = false;
|
||||
private previousActiveState: boolean;
|
||||
|
|
@ -35,6 +34,9 @@ class WebsocketManager {
|
|||
private backgroundIntervalId: number | undefined;
|
||||
private firstConnectionSynced: Record<string, boolean> = {};
|
||||
|
||||
private appStateSubscription: NativeEventSubscription | undefined;
|
||||
private netStateSubscription: NetInfoSubscription | undefined;
|
||||
|
||||
constructor() {
|
||||
this.previousActiveState = AppState.currentState === 'active';
|
||||
}
|
||||
|
|
@ -45,36 +47,41 @@ class WebsocketManager {
|
|||
({serverUrl, token}) => {
|
||||
try {
|
||||
DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
this.createClient(serverUrl, token, 0);
|
||||
this.createClient(serverUrl, token);
|
||||
} catch (error) {
|
||||
logError('WebsocketManager init error', error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
AppState.addEventListener('change', this.onAppStateChange);
|
||||
NetInfo.addEventListener(this.onNetStateChange);
|
||||
this.appStateSubscription?.remove();
|
||||
this.netStateSubscription?.();
|
||||
|
||||
this.appStateSubscription = AppState.addEventListener('change', this.onAppStateChange);
|
||||
this.netStateSubscription = NetInfo.addEventListener(this.onNetStateChange);
|
||||
};
|
||||
|
||||
public invalidateClient = (serverUrl: string) => {
|
||||
this.clients[serverUrl]?.close();
|
||||
this.clients[serverUrl]?.close(true);
|
||||
this.clients[serverUrl]?.invalidate();
|
||||
if (this.connectionTimerIDs[serverUrl]) {
|
||||
this.connectionTimerIDs[serverUrl].cancel();
|
||||
}
|
||||
clearTimeout(this.connectionTimerIDs[serverUrl]);
|
||||
delete this.clients[serverUrl];
|
||||
delete this.firstConnectionSynced[serverUrl];
|
||||
|
||||
// We don't remove the connected subject so any potential client invalidation
|
||||
// and subsequent creation of the client can still be observed by the component.
|
||||
// Being purist, this is a memory leak, since we never clean any server url,
|
||||
// but since this information lives in memory and we don't expect many servers
|
||||
// to be added and removed in one single session, this should be fine.
|
||||
this.getConnectedSubject(serverUrl).next('not_connected');
|
||||
delete this.connectedSubjects[serverUrl];
|
||||
};
|
||||
|
||||
public createClient = (serverUrl: string, bearerToken: string, storedLastDisconnect = 0) => {
|
||||
public createClient = (serverUrl: string, bearerToken: string) => {
|
||||
if (this.clients[serverUrl]) {
|
||||
this.invalidateClient(serverUrl);
|
||||
}
|
||||
|
||||
const client = new WebSocketClient(serverUrl, bearerToken, storedLastDisconnect);
|
||||
const client = new WebSocketClient(serverUrl, bearerToken);
|
||||
|
||||
client.setFirstConnectCallback(() => this.onFirstConnect(serverUrl));
|
||||
client.setEventCallback((evt: any) => handleEvent(serverUrl, evt));
|
||||
|
|
@ -82,7 +89,7 @@ class WebsocketManager {
|
|||
//client.setMissedEventsCallback(() => {}) Nothing to do on missedEvents callback
|
||||
client.setReconnectCallback(() => this.onReconnect(serverUrl));
|
||||
client.setReliableReconnectCallback(() => this.onReliableReconnect(serverUrl));
|
||||
client.setCloseCallback((connectFailCount: number, lastDisconnect: number) => this.onWebsocketClose(serverUrl, connectFailCount, lastDisconnect));
|
||||
client.setCloseCallback((connectFailCount: number) => this.onWebsocketClose(serverUrl, connectFailCount));
|
||||
|
||||
this.clients[serverUrl] = client;
|
||||
|
||||
|
|
@ -92,23 +99,21 @@ class WebsocketManager {
|
|||
public closeAll = () => {
|
||||
for (const url of Object.keys(this.clients)) {
|
||||
const client = this.clients[url];
|
||||
if (client.isConnected()) {
|
||||
client.close(true);
|
||||
this.getConnectedSubject(url).next('not_connected');
|
||||
}
|
||||
client.close(true);
|
||||
this.getConnectedSubject(url).next('not_connected');
|
||||
}
|
||||
};
|
||||
|
||||
public openAll = async () => {
|
||||
let queued = 0;
|
||||
for await (const clientUrl of Object.keys(this.clients)) {
|
||||
const activeServerUrl = await DatabaseManager.getActiveServerUrl();
|
||||
if (clientUrl === activeServerUrl) {
|
||||
this.initializeClient(clientUrl);
|
||||
} else {
|
||||
queued += 1;
|
||||
this.getConnectedSubject(clientUrl).next('connecting');
|
||||
const bounce = debounce(this.initializeClient.bind(this, clientUrl), WAIT_UNTIL_NEXT);
|
||||
this.connectionTimerIDs[clientUrl] = bounce;
|
||||
bounce();
|
||||
this.connectionTimerIDs[clientUrl] = setTimeout(() => this.initializeClient(clientUrl), WAIT_UNTIL_NEXT * queued);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -131,24 +136,24 @@ class WebsocketManager {
|
|||
return this.connectedSubjects[serverUrl];
|
||||
};
|
||||
|
||||
private cancelAllConnections = () => {
|
||||
for (const url in this.connectionTimerIDs) {
|
||||
if (this.connectionTimerIDs[url]) {
|
||||
this.connectionTimerIDs[url].cancel();
|
||||
delete this.connectionTimerIDs[url];
|
||||
}
|
||||
private cancelConnectTimers = () => {
|
||||
for (const [url, timer] of Object.entries(this.connectionTimerIDs)) {
|
||||
clearTimeout(timer);
|
||||
delete this.connectionTimerIDs[url];
|
||||
}
|
||||
};
|
||||
|
||||
public initializeClient = async (serverUrl: string) => {
|
||||
const client: WebSocketClient = this.clients[serverUrl];
|
||||
this.connectionTimerIDs[serverUrl]?.cancel();
|
||||
clearTimeout(this.connectionTimerIDs[serverUrl]);
|
||||
delete this.connectionTimerIDs[serverUrl];
|
||||
if (!client?.isConnected()) {
|
||||
client.initialize();
|
||||
if (!this.firstConnectionSynced[serverUrl]) {
|
||||
const hasSynced = this.firstConnectionSynced[serverUrl];
|
||||
client.initialize({}, !hasSynced);
|
||||
if (!hasSynced) {
|
||||
const error = await handleFirstConnect(serverUrl);
|
||||
if (error) {
|
||||
// This will try to reconnect and try to sync again
|
||||
client.close(false);
|
||||
}
|
||||
|
||||
|
|
@ -178,12 +183,10 @@ class WebsocketManager {
|
|||
this.getConnectedSubject(serverUrl).next('connected');
|
||||
};
|
||||
|
||||
private onWebsocketClose = async (serverUrl: string, connectFailCount: number, lastDisconnect: number) => {
|
||||
private onWebsocketClose = async (serverUrl: string, connectFailCount: number) => {
|
||||
this.getConnectedSubject(serverUrl).next('not_connected');
|
||||
if (connectFailCount <= 1) { // First fail
|
||||
await setCurrentUserStatus(serverUrl, General.OFFLINE);
|
||||
await handleClose(serverUrl, lastDisconnect);
|
||||
|
||||
this.stopPeriodicStatusUpdates(serverUrl);
|
||||
}
|
||||
};
|
||||
|
|
@ -212,65 +215,66 @@ class WebsocketManager {
|
|||
}
|
||||
|
||||
private stopPeriodicStatusUpdates(serverUrl: string) {
|
||||
const currentId = this.statusUpdatesIntervalIDs[serverUrl];
|
||||
if (currentId != null) {
|
||||
clearInterval(currentId);
|
||||
}
|
||||
|
||||
clearInterval(this.statusUpdatesIntervalIDs[serverUrl]);
|
||||
delete this.statusUpdatesIntervalIDs[serverUrl];
|
||||
}
|
||||
|
||||
private onAppStateChange = async (appState: AppStateStatus) => {
|
||||
const isActive = appState === 'active';
|
||||
if (isActive === this.previousActiveState) {
|
||||
return;
|
||||
}
|
||||
|
||||
private onAppStateChange = (appState: AppStateStatus) => {
|
||||
const isMain = isMainActivity();
|
||||
|
||||
this.cancelAllConnections();
|
||||
if (!isActive && !this.isBackgroundTimerRunning) {
|
||||
this.isBackgroundTimerRunning = true;
|
||||
this.cancelAllConnections();
|
||||
this.backgroundIntervalId = BackgroundTimer.setInterval(() => {
|
||||
this.closeAll();
|
||||
BackgroundTimer.clearInterval(this.backgroundIntervalId!);
|
||||
this.isBackgroundTimerRunning = false;
|
||||
}, WAIT_TO_CLOSE);
|
||||
|
||||
this.previousActiveState = isActive;
|
||||
if (!isMain) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isActive && this.netConnected && isMain) { // Reopen the websockets only if there is connection
|
||||
if (this.backgroundIntervalId) {
|
||||
BackgroundTimer.clearInterval(this.backgroundIntervalId);
|
||||
}
|
||||
this.isBackgroundTimerRunning = false;
|
||||
this.openAll();
|
||||
this.previousActiveState = isActive;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMain) {
|
||||
this.previousActiveState = isActive;
|
||||
}
|
||||
const isActive = appState === 'active';
|
||||
this.handleStateChange(this.netConnected, isActive);
|
||||
};
|
||||
|
||||
private onNetStateChange = async (netState: NetInfoState) => {
|
||||
private onNetStateChange = (netState: NetInfoState) => {
|
||||
const newState = Boolean(netState.isConnected);
|
||||
if (this.netConnected === newState) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.netConnected = newState;
|
||||
this.handleStateChange(newState, this.previousActiveState);
|
||||
};
|
||||
|
||||
if (this.netConnected && this.previousActiveState) { // Reopen the websockets only if the app is active
|
||||
this.openAll();
|
||||
private handleStateChange = (currentIsConnected: boolean, currentIsActive: boolean) => {
|
||||
if (currentIsActive === this.previousActiveState && currentIsConnected === this.netConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeAll();
|
||||
this.cancelConnectTimers();
|
||||
|
||||
const wentBackground = this.previousActiveState && !currentIsActive;
|
||||
|
||||
this.previousActiveState = currentIsActive;
|
||||
this.netConnected = currentIsConnected;
|
||||
|
||||
if (!currentIsConnected) {
|
||||
this.closeAll();
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentIsActive) {
|
||||
if (this.isBackgroundTimerRunning) {
|
||||
BackgroundTimer.clearInterval(this.backgroundIntervalId!);
|
||||
}
|
||||
this.isBackgroundTimerRunning = false;
|
||||
if (this.netConnected) {
|
||||
this.openAll();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (wentBackground && !this.isBackgroundTimerRunning) {
|
||||
this.isBackgroundTimerRunning = true;
|
||||
this.backgroundIntervalId = BackgroundTimer.setInterval(() => {
|
||||
this.closeAll();
|
||||
BackgroundTimer.clearInterval(this.backgroundIntervalId!);
|
||||
this.isBackgroundTimerRunning = false;
|
||||
}, WAIT_TO_CLOSE);
|
||||
}
|
||||
};
|
||||
|
||||
public getClient = (serverUrl: string): WebSocketClient | undefined => {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import DatabaseManager from '@database/manager';
|
|||
import {prepareCategoriesAndCategoriesChannels} from './categories';
|
||||
import {prepareDeleteChannel, prepareMyChannelsForTeam} from './channel';
|
||||
import {prepareMyPreferences} from './preference';
|
||||
import {resetWebSocketLastDisconnected} from './system';
|
||||
import {resetLastFullSync} from './system';
|
||||
import {prepareDeleteTeam, prepareMyTeams} from './team';
|
||||
import {prepareUsers} from './user';
|
||||
|
||||
|
|
@ -103,7 +103,7 @@ export async function truncateCrtRelatedTables(serverUrl: string): Promise<{erro
|
|||
],
|
||||
});
|
||||
});
|
||||
await resetWebSocketLastDisconnected(operator);
|
||||
await resetLastFullSync(operator);
|
||||
} catch (error) {
|
||||
if (__DEV__) {
|
||||
throw error;
|
||||
|
|
|
|||
|
|
@ -311,7 +311,7 @@ export const observeRecentCustomStatus = (database: Database): Observable<UserCu
|
|||
);
|
||||
};
|
||||
|
||||
export const getWebSocketLastDisconnected = async (serverDatabase: Database) => {
|
||||
export const getLastFullSync = async (serverDatabase: Database) => {
|
||||
try {
|
||||
const websocketLastDisconnected = await serverDatabase.get<SystemModel>(SYSTEM).find(SYSTEM_IDENTIFIERS.WEBSOCKET);
|
||||
return (parseInt(websocketLastDisconnected?.value || 0, 10) || 0);
|
||||
|
|
@ -320,16 +320,17 @@ export const getWebSocketLastDisconnected = async (serverDatabase: Database) =>
|
|||
}
|
||||
};
|
||||
|
||||
export const observeWebsocketLastDisconnected = (database: Database) => {
|
||||
return querySystemValue(database, SYSTEM_IDENTIFIERS.WEBSOCKET).observe().pipe(
|
||||
switchMap((result) => (result.length ? result[0].observe() : of$({value: '0'}))),
|
||||
switchMap((model) => of$(parseInt(model.value || 0, 10) || 0)),
|
||||
);
|
||||
export const setLastFullSync = async (operator: ServerDataOperator, value: number, prepareRecordsOnly = false) => {
|
||||
return operator.handleSystem({systems: [{
|
||||
id: SYSTEM_IDENTIFIERS.WEBSOCKET,
|
||||
value,
|
||||
}],
|
||||
prepareRecordsOnly});
|
||||
};
|
||||
|
||||
export const resetWebSocketLastDisconnected = async (operator: ServerDataOperator, prepareRecordsOnly = false) => {
|
||||
export const resetLastFullSync = async (operator: ServerDataOperator, prepareRecordsOnly = false) => {
|
||||
const {database} = operator;
|
||||
const lastDisconnectedAt = await getWebSocketLastDisconnected(database);
|
||||
const lastDisconnectedAt = await getLastFullSync(database);
|
||||
|
||||
if (lastDisconnectedAt) {
|
||||
return operator.handleSystem({systems: [{
|
||||
|
|
|
|||
Loading…
Reference in a new issue