diff --git a/packages/core/src/base-client.ts b/packages/core/src/base-client.ts index 90ae5e7f..a0081939 100644 --- a/packages/core/src/base-client.ts +++ b/packages/core/src/base-client.ts @@ -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 = {} - 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 { 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 { - 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 { 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 { - 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, diff --git a/packages/core/src/network/config-manager.ts b/packages/core/src/network/config-manager.ts new file mode 100644 index 00000000..54f5381f --- /dev/null +++ b/packages/core/src/network/config-manager.ts @@ -0,0 +1,97 @@ +import { tl } from '@mtcute/tl' + +export class ConfigManager { + constructor(private _update: () => Promise) {} + + private _destroyed = false + private _config?: tl.RawConfig + private _cdnConfig?: tl.RawCdnConfig + + private _updateTimeout?: NodeJS.Timeout + private _updatingPromise?: Promise + + private _listeners: ((config: tl.RawConfig) => void)[] = [] + + get isStale(): boolean { + return !this._config || this._config.expires < Date.now() / 1000 + } + + update(): Promise { + 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 { + 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 { + 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] + } +}