diff --git a/src/api/gramjs/methods/client.ts b/src/api/gramjs/methods/client.ts index 42d512a9..81505167 100644 --- a/src/api/gramjs/methods/client.ts +++ b/src/api/gramjs/methods/client.ts @@ -93,7 +93,9 @@ export async function init(_onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs) initialMethod: platform === 'iOS' || platform === 'Android' ? 'phoneNumber' : 'qrCode', }); } catch (err) { - // TODO Investigate which request causes this exception + // eslint-disable-next-line no-console + console.error(err); + if (err.message !== 'Disconnect') { onUpdate({ '@type': 'updateConnectionState', diff --git a/src/lib/gramjs/client/TelegramClient.js b/src/lib/gramjs/client/TelegramClient.js index 8378d943..59b1b186 100644 --- a/src/lib/gramjs/client/TelegramClient.js +++ b/src/lib/gramjs/client/TelegramClient.js @@ -11,8 +11,11 @@ const { constructors, requests, } = require('../tl'); -const MTProtoSender = require('../network/MTProtoSender'); -const { ConnectionTCPObfuscated } = require('../network/connection/TCPObfuscated'); +const { + ConnectionTCPObfuscated, + MTProtoSender, + UpdateConnectionState, +} = require('../network'); const { authFlow, checkAuthorization, @@ -33,6 +36,14 @@ const PING_INTERVAL = 3000; // 3 sec const PING_TIMEOUT = 5000; // 5 sec const PING_FAIL_ATTEMPTS = 3; const PING_FAIL_INTERVAL = 100; // ms + +// An unusually long interval is a sign of returning from background mode... +const PING_INTERVAL_TO_WAKE_UP = 5000; // 5 sec +// ... so we send a quick "wake-up" ping to confirm than connection was dropped ASAP +const PING_WAKE_UP_TIMEOUT = 3000; // 3 sec +// We also send a warning to the user even a bit more quickly +const PING_WAKE_UP_WARNING_TIMEOUT = 1000; // 1 sec + const PING_DISCONNECT_DELAY = 60000; // 1 min // All types @@ -215,23 +226,51 @@ class TelegramClient { } async _updateLoop() { + let lastPongAt; + while (!this._destroyed) { await Helpers.sleep(PING_INTERVAL); if (this._sender.isReconnecting || this._isSwitchingDc) { + lastPongAt = undefined; continue; } try { - await attempts(() => { - return timeout(this._sender.send(new requests.PingDelayDisconnect({ + const ping = () => { + return this._sender.send(new requests.PingDelayDisconnect({ pingId: Helpers.getRandomInt(Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER), disconnectDelay: PING_DISCONNECT_DELAY, - })), PING_TIMEOUT); - }, PING_FAIL_ATTEMPTS, PING_FAIL_INTERVAL); + })); + }; + + const pingAt = Date.now(); + const lastInterval = lastPongAt ? pingAt - lastPongAt : undefined; + + if (!lastInterval || lastInterval < PING_INTERVAL_TO_WAKE_UP) { + await attempts(() => timeout(ping, PING_TIMEOUT), PING_FAIL_ATTEMPTS, PING_FAIL_INTERVAL); + } else { + let wakeUpWarningTimeout = setTimeout(() => { + this._handleUpdate(new UpdateConnectionState(UpdateConnectionState.disconnected)); + wakeUpWarningTimeout = undefined; + }, PING_WAKE_UP_WARNING_TIMEOUT); + + await timeout(ping, PING_WAKE_UP_TIMEOUT); + + if (wakeUpWarningTimeout) { + clearTimeout(wakeUpWarningTimeout); + wakeUpWarningTimeout = undefined; + } + + this._handleUpdate(new UpdateConnectionState(UpdateConnectionState.connected)); + } + + lastPongAt = Date.now(); } catch (err) { // eslint-disable-next-line no-console console.warn(err); + lastPongAt = undefined; + if (this._sender.isReconnecting || this._isSwitchingDc) { continue; } @@ -251,6 +290,8 @@ class TelegramClient { } catch (e) { // we don't care about errors here } + + lastPongAt = undefined; } } await this.disconnect(); @@ -1052,9 +1093,9 @@ class TelegramClient { } } -function timeout(promise, ms) { +function timeout(cb, ms) { return Promise.race([ - promise, + cb(), Helpers.sleep(ms) .then(() => Promise.reject(new Error('TIMEOUT'))), ]); diff --git a/src/lib/gramjs/network/MTProtoSender.js b/src/lib/gramjs/network/MTProtoSender.js index e1275c04..3c8e2132 100644 --- a/src/lib/gramjs/network/MTProtoSender.js +++ b/src/lib/gramjs/network/MTProtoSender.js @@ -21,7 +21,7 @@ const BinaryReader = require('../extensions/BinaryReader'); const { UpdateConnectionState, UpdateServerTimeOffset, -} = require('./index'); +} = require('./updates'); const { BadMessageError } = require('../errors/Common'); const { BadServerSalt, @@ -169,6 +169,8 @@ class MTProtoSender { * @returns {Promise} */ async connect(connection, force) { + this.userDisconnected = false; + if (this._user_connected && !force) { this._log.info('User is already connected!'); return false; @@ -315,13 +317,15 @@ class MTProtoSender { async _disconnect() { this._send_queue.rejectAll(); + if (this._updateCallback) { + this._updateCallback(new UpdateConnectionState(UpdateConnectionState.disconnected)); + } + if (this._connection === undefined) { this._log.info('Not disconnecting (already have no connection)'); return; } - if (this._updateCallback) { - this._updateCallback(new UpdateConnectionState(UpdateConnectionState.disconnected)); - } + this._log.info('Disconnecting from %s...'.replace('%s', this._connection.toString())); this._user_connected = false; this._log.debug('Closing current connection...'); diff --git a/src/lib/gramjs/network/index.js b/src/lib/gramjs/network/index.js index 95134034..9b710d32 100644 --- a/src/lib/gramjs/network/index.js +++ b/src/lib/gramjs/network/index.js @@ -1,24 +1,6 @@ const MTProtoPlainSender = require('./MTProtoPlainSender'); const MTProtoSender = require('./MTProtoSender'); -class UpdateConnectionState { - static disconnected = -1; - - static connected = 1; - - static broken = 0; - - constructor(state) { - this.state = state; - } -} - -class UpdateServerTimeOffset { - constructor(timeOffset) { - this.timeOffset = timeOffset; - } -} - const { Connection, ConnectionTCPFull, @@ -26,6 +8,11 @@ const { ConnectionTCPObfuscated, } = require('./connection'); +const { + UpdateConnectionState, + UpdateServerTimeOffset, +} = require('./updates'); + module.exports = { Connection, ConnectionTCPFull, diff --git a/src/lib/gramjs/network/updates.js b/src/lib/gramjs/network/updates.js new file mode 100644 index 00000000..0c42e720 --- /dev/null +++ b/src/lib/gramjs/network/updates.js @@ -0,0 +1,23 @@ +class UpdateConnectionState { + static disconnected = -1; + + static connected = 1; + + static broken = 0; + + constructor(state, origin) { + this.state = state; + this.origin = origin; + } +} + +class UpdateServerTimeOffset { + constructor(timeOffset) { + this.timeOffset = timeOffset; + } +} + +module.exports = { + UpdateConnectionState, + UpdateServerTimeOffset, +}; diff --git a/src/modules/actions/api/sync.ts b/src/modules/actions/api/sync.ts index f7781b9a..370da89b 100644 --- a/src/modules/actions/api/sync.ts +++ b/src/modules/actions/api/sync.ts @@ -47,14 +47,28 @@ addReducer('afterSync', () => { void afterSync(); }); +const RELEASE_STATUS_TIMEOUT = 15000; // 10 sec; + +let releaseStatusTimeout: number | undefined; + async function sync(afterSyncCallback: () => void) { if (DEBUG) { // eslint-disable-next-line no-console console.log('>>> START SYNC'); } + if (releaseStatusTimeout) { + clearTimeout(releaseStatusTimeout); + } + setGlobal({ ...getGlobal(), isSyncing: true }); + // Workaround for `isSyncing = true` sometimes getting stuck for some reason + releaseStatusTimeout = window.setTimeout(() => { + setGlobal({ ...getGlobal(), isSyncing: false }); + releaseStatusTimeout = undefined; + }, RELEASE_STATUS_TIMEOUT); + await callApi('fetchCurrentUser'); // This fetches only active chats and clears archived chats, which will be fetched in `afterSync`