diff --git a/app/actions/remote/entry/common.ts b/app/actions/remote/entry/common.ts index c8a394410..46ab1dc2c 100644 --- a/app/actions/remote/entry/common.ts +++ b/app/actions/remote/entry/common.ts @@ -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) { diff --git a/app/actions/websocket/index.ts b/app/actions/websocket/index.ts index 0d5cec0e8..3484c6c99 100644 --- a/app/actions/websocket/index.ts +++ b/app/actions/websocket/index.ts @@ -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); diff --git a/app/client/websocket/index.ts b/app/client/websocket/index.ts index 54ed13c05..3070b0684 100644 --- a/app/client/websocket/index.ts +++ b/app/client/websocket/index.ts @@ -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; } } diff --git a/app/managers/websocket_manager.ts b/app/managers/websocket_manager.ts index c86f5ddb9..b40f94db2 100644 --- a/app/managers/websocket_manager.ts +++ b/app/managers/websocket_manager.ts @@ -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} = {}; private clients: Record = {}; - private connectionTimerIDs: Record void>> = {}; + private connectionTimerIDs: Record = {}; private isBackgroundTimerRunning = false; private netConnected = false; private previousActiveState: boolean; @@ -35,6 +34,9 @@ class WebsocketManager { private backgroundIntervalId: number | undefined; private firstConnectionSynced: Record = {}; + 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 => { diff --git a/app/queries/servers/entry.ts b/app/queries/servers/entry.ts index d237035c6..ec5149b85 100644 --- a/app/queries/servers/entry.ts +++ b/app/queries/servers/entry.ts @@ -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; diff --git a/app/queries/servers/system.ts b/app/queries/servers/system.ts index a369343cc..ce13d8afc 100644 --- a/app/queries/servers/system.ts +++ b/app/queries/servers/system.ts @@ -311,7 +311,7 @@ export const observeRecentCustomStatus = (database: Database): Observable { +export const getLastFullSync = async (serverDatabase: Database) => { try { const websocketLastDisconnected = await serverDatabase.get(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: [{