GramJs: Wake-up pings for faster reconnect after idle mode

This commit is contained in:
Alexander Zinchuk 2022-01-27 04:08:21 +01:00
parent a55fbdfcc4
commit 1c0388f6d1
6 changed files with 102 additions and 31 deletions

View File

@ -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',

View File

@ -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'))),
]);

View File

@ -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<boolean>}
*/
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...');

View File

@ -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,

View File

@ -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,
};

View File

@ -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`