refactor: extracted dc config management to separate class

This commit is contained in:
teidesu 2022-11-03 15:26:40 +03:00 committed by Alina Sireneva
parent 6a2c5d90b7
commit 4848c4e62d
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
2 changed files with 117 additions and 80 deletions

View file

@ -35,6 +35,8 @@ import {
import { addPublicKey } from './utils/crypto/keys'
import { readStringSession, writeStringSession } from './utils/string-session'
import { ConfigManager } from './network/config-manager'
export interface BaseTelegramClientOptions {
/**
* API ID from my.telegram.org
@ -247,8 +249,9 @@ export class BaseTelegramClient extends EventEmitter {
protected _lastUpdateTime = 0
private _floodWaitedRequests: Record<string, number> = {}
protected _config?: tl.RawConfig
protected _cdnConfig?: tl.RawCdnConfig
protected _config = new ConfigManager(() =>
this.call({ _: 'help.getConfig' })
)
private _additionalConnections: SessionConnection[] = []
@ -499,6 +502,8 @@ export class BaseTelegramClient extends EventEmitter {
async close(): Promise<void> {
await this._onClose()
this._config.destroy()
this._cleanupPrimaryConnection(true)
// close additional connections
this._additionalConnections.forEach((conn) => conn.destroy())
@ -507,82 +512,6 @@ export class BaseTelegramClient extends EventEmitter {
await this.storage.destroy?.()
}
/**
* Utility function to find the DC by its ID.
*
* @param id Datacenter ID
* @param preferMedia Whether to prefer media-only DCs
* @param cdn Whether the needed DC is a CDN DC
*/
async getDcById(
id: number,
preferMedia = false,
cdn = false,
): Promise<tl.RawDcOption> {
if (!this._config) {
this._config = await this.call({ _: 'help.getConfig' })
}
if (cdn && !this._cdnConfig) {
this._cdnConfig = await this.call({ _: 'help.getCdnConfig' })
for (const key of this._cdnConfig.publicKeys) {
await addPublicKey(this._crypto, key.publicKey)
}
}
if (this._useIpv6) {
// first try to find ipv6 dc
let found
if (preferMedia) {
found = this._config.dcOptions.find(
(it) =>
it.id === id &&
it.mediaOnly &&
it.cdn === cdn &&
it.ipv6 &&
!it.tcpoOnly,
)
}
if (!found) {
found = this._config.dcOptions.find(
(it) =>
it.id === id &&
it.cdn === cdn &&
it.ipv6 &&
!it.tcpoOnly,
)
}
if (found) return found
}
let found
if (preferMedia) {
found = this._config.dcOptions.find(
(it) =>
it.id === id &&
it.mediaOnly &&
it.cdn === cdn &&
!it.tcpoOnly &&
!it.ipv6,
)
}
if (!found) {
found = this._config.dcOptions.find(
(it) =>
it.id === id && it.cdn === cdn && !it.tcpoOnly && !it.ipv6,
)
}
if (found) return found
throw new Error(`Could not find${cdn ? ' CDN' : ''} DC ${id}`)
}
/**
* Change primary DC and write that fact to the storage.
* Will immediately reconnect to another DC.
@ -591,7 +520,12 @@ export class BaseTelegramClient extends EventEmitter {
*/
async changeDc(newDc: tl.RawDcOption | number): Promise<void> {
if (typeof newDc === 'number') {
newDc = await this.getDcById(newDc)
const res = await this._config.findOption({
dcId: newDc,
allowIpv6: this._useIpv6,
})
if (!res) throw new Error('DC not found')
newDc = res
}
this._primaryDc = newDc
@ -764,7 +698,13 @@ export class BaseTelegramClient extends EventEmitter {
disableUpdates?: boolean
},
): Promise<SessionConnection> {
const dc = await this.getDcById(dcId, params?.media, params?.cdn)
const dc = await this._config.findOption({
dcId,
preferMedia: params?.media,
cdn: params?.cdn,
allowIpv6: this._useIpv6,
})
if (!dc) throw new Error('DC not found')
const connection = new SessionConnection(
{
dc,

View file

@ -0,0 +1,97 @@
import { tl } from '@mtcute/tl'
export class ConfigManager {
constructor(private _update: () => Promise<tl.RawConfig>) {}
private _destroyed = false
private _config?: tl.RawConfig
private _cdnConfig?: tl.RawCdnConfig
private _updateTimeout?: NodeJS.Timeout
private _updatingPromise?: Promise<void>
private _listeners: ((config: tl.RawConfig) => void)[] = []
get isStale(): boolean {
return !this._config || this._config.expires < Date.now() / 1000
}
update(): Promise<void> {
if (!this.isStale) return Promise.resolve()
if (this._updatingPromise) return this._updatingPromise
return (this._updatingPromise = this._update().then((config) => {
if (this._destroyed) return
this._config = config
if (this._updateTimeout) clearTimeout(this._updateTimeout)
this._updateTimeout = setTimeout(
() => this.update(),
(config.expires - Date.now() / 1000) * 1000
)
for (const cb of this._listeners) cb(config)
}))
}
onConfigUpdate(cb: (config: tl.RawConfig) => void): void {
this._listeners.push(cb)
}
offConfigUpdate(cb: (config: tl.RawConfig) => void): void {
const idx = this._listeners.indexOf(cb)
if (idx >= 0) this._listeners.splice(idx, 1)
}
getNow(): tl.RawConfig | undefined {
return this._config
}
async get(): Promise<tl.RawConfig> {
if (this.isStale) await this.update()
return this._config!
}
destroy(): void {
if (this._updateTimeout) clearTimeout(this._updateTimeout)
this._listeners.length = 0
this._destroyed = true
}
async findOption(params: {
dcId: number
allowIpv6?: boolean
preferIpv6?: boolean
preferMedia?: boolean
cdn?: boolean
}): Promise<tl.RawDcOption | undefined> {
if (this.isStale) await this.update()
const options = this._config!.dcOptions.filter((opt) => {
if (opt.id === params.dcId) return true
if (opt.ipv6 && !params.allowIpv6) return false
if (opt.cdn && !params.cdn) return false
if (opt.tcpoOnly) return false // unsupported
return true
})
if (params.preferMedia && params.preferIpv6) {
const r = options.find((opt) => opt.mediaOnly && opt.ipv6)
if (r) return r
}
if (params.preferMedia) {
const r = options.find((opt) => opt.mediaOnly)
if (r) return r
}
if (params.preferIpv6) {
const r = options.find((opt) => opt.ipv6)
if (r) return r
}
return options[0]
}
}