,
+ send(client: TdjsonClient, request: string): void,
/** td_set_log_fatal_error_callback is deprecated in TDLib v1.8.0 */
setLogFatalErrorCallback(fn: null | ((errorMessage: string) => void)): void,
setLogMessageCallback(
maxVerbosityLevel: number,
callback: null | ((verbosityLevel: number, message: string) => void)
- ): void
+ ): void,
+ tdn: TdjsonNew
|}
-export type TdjsonCompat = {|
- // Compatibility with tdl-tdlib-addon
- +getName?: () => void,
+export type TdjsonTdlTdlibAddon = {|
+ getName(): void,
create(): TdjsonClient,
destroy(client: TdjsonClient): void,
- execute(client: null | TdjsonClient, query: {...}): {...} | null,
+ execute(client: null | TdjsonClient, request: {...}): {...} | null,
receive(client: TdjsonClient, timeout: number): Promise<{...} | null>,
- send(client: TdjsonClient, query: {...}): void,
- /** td_set_log_fatal_error_callback is deprecated in TDLib v1.8.0 */
+ send(client: TdjsonClient, request: {...}): void,
+ setLogFatalErrorCallback(fn: null | ((errorMessage: string) => void)): void,
+|}
+
+type TdjsonCompat = {|
+ /** `true` if runs in compatibility with the tdl-tdlib-addon package */
+ compat?: boolean,
+ create(): TdjsonClient,
+ destroy(client: TdjsonClient): void,
+ execute(client: null | TdjsonClient, request: {...}): {...} | null,
+ receive(client: TdjsonClient, timeout: number): Promise<{...} | null>,
+ send(client: TdjsonClient, request: {...}): void,
setLogFatalErrorCallback(fn: null | ((errorMessage: string) => void)): void,
- +setLogMessageCallback?: (
+ setLogMessageCallback(
maxVerbosityLevel: number,
callback: null | ((verbosityLevel: number, message: string) => void)
- ) => void
+ ): void,
+ tdn: TdjsonNew
|}
+// Compatibility with tdl-tdlib-addon
+function tdjsonCompat (td: TdjsonTdlTdlibAddon | Tdjson): TdjsonCompat {
+ function unvailable (name: string) {
+ throw new Error(`${name} is not available in tdl-tdlib-addon`)
+ }
+ const foundTdlTdlibAddon = (td: any).getName?.() === 'addon'
+ if (!foundTdlTdlibAddon) {
+ const tdjson: Tdjson = (td: any)
+ return {
+ ...tdjson,
+ execute (client, request) {
+ const response = tdjson.execute(client, JSON.stringify(request))
+ if (response == null) return null
+ return JSON.parse(response)
+ },
+ send (client, request) {
+ tdjson.send(client, JSON.stringify(request))
+ },
+ async receive (client, timeout) {
+ const response = await tdjson.receive(client, timeout)
+ if (response == null) return null
+ return JSON.parse(response)
+ }
+ }
+ }
+ const tdjsonOld: TdjsonTdlTdlibAddon = (td: any)
+ return {
+ compat: true,
+ create: tdjsonOld.create.bind(tdjsonOld),
+ destroy: tdjsonOld.destroy.bind(tdjsonOld),
+ execute: tdjsonOld.execute.bind(tdjsonOld),
+ receive: tdjsonOld.receive.bind(tdjsonOld),
+ send: tdjsonOld.send.bind(tdjsonOld),
+ setLogFatalErrorCallback: tdjsonOld.setLogFatalErrorCallback.bind(tdjsonOld),
+ setLogMessageCallback: () => unvailable('setLogMessageCallback'),
+ tdn: {
+ init: () => unvailable('tdn.init'),
+ createClientId: () => unvailable('tdn.createClientId'),
+ send: () => unvailable('tdn.send'),
+ receive: () => unvailable('tdn.receive'),
+ execute: () => unvailable('tdn.execute')
+ }
+ }
+}
+
export type TDLibParameters = $Rest
export type LoginUser = {|
@@ -211,8 +276,13 @@ function invariant (cond: boolean, msg: string = 'Invariant violation') {
if (!cond) throw new Error(msg)
}
+export type ManagingOptions = {
+ useTdn: boolean,
+ onClose: () => void
+}
+
const TDLIB_1_8_6 = new Version('1.8.6')
-const TDLIB_DEFAULT = new Version('1.8.12')
+const TDLIB_DEFAULT = new Version('1.8.19')
const TDL_MAGIC = '6c47e6b71ea'
@@ -242,7 +312,7 @@ export class Client {
+_emitter: EventEmitter = new EventEmitter();
+_pending: Map = new Map();
_requestId: number = 0
- _client: TdjsonClient | null
+ _client: TdjsonClient | null = null
_initialized: boolean = false
_paused: boolean = false
_connectionState: Td$ConnectionState = { _: 'connectionStateConnecting' }
@@ -250,10 +320,22 @@ export class Client {
_loginDetails: ?StrictLoginDetails
_loginDefer: TdlDeferred = new TdlDeferred()
_version: Version = TDLIB_DEFAULT
-
- constructor (tdlibInstance: TdjsonCompat, options: ClientOptions = {}) {
+ _tdn: boolean = false
+ _onClose: (() => void) = (() => {})
+ _clientId: number = -1
+
+ constructor (
+ tdlibInstance: TdjsonTdlTdlibAddon | Tdjson,
+ options: ClientOptions = {},
+ managing?: ManagingOptions
+ ) {
this._options = (mergeDeepRight(defaultOptions, options): StrictClientOptions)
- this._tdlib = tdlibInstance
+ this._tdlib = tdjsonCompat(tdlibInstance)
+
+ if (managing && managing.useTdn) {
+ this._tdn = true
+ this._onClose = managing.onClose
+ }
// Backward compatibility
if (this._options.useDefaultVerbosityLevel)
@@ -272,35 +354,44 @@ export class Client {
// Backward compatibility
if (this._options.verbosityLevel != null && this._options.verbosityLevel !== 'default') {
debug('Executing setLogVerbosityLevel', this._options.verbosityLevel)
- this._tdlib.execute(null, {
- '@type': 'setLogVerbosityLevel',
- new_verbosity_level: this._options.verbosityLevel
+ this.execute({
+ _: 'setLogVerbosityLevel',
+ new_verbosity_level: parseInt(this._options.verbosityLevel)
})
- } else if (this._tdlib.getName?.() === 'addon') {
+ } else if (this._tdlib.compat) {
debug('Executing setLogVerbosityLevel (tdl-tdlib-addon found)', this._options.verbosityLevel)
- this._tdlib.execute(null, {
- '@type': 'setLogVerbosityLevel',
+ this.execute({
+ _: 'setLogVerbosityLevel',
new_verbosity_level: 2
})
}
- this._client = this._tdlib.create()
-
- if (!this._client)
- throw new Error('Failed to create a TDLib client')
-
if (this._options.bare)
this._initialized = true
- // Note: To allow defining listeners before the first update, we must ensure
- // that emit is not executed in the current tick. process.nextTick or
- // queueMicrotask are redundant here because of await in the _loop function.
- this._loop()
- .catch(e => this._catchError(new TdlError(e)))
+ if (!this._tdn) {
+ this._client = this._tdlib.create()
+
+ if (!this._client) throw new Error('Failed to create a TDLib client')
+
+ // Note: To allow defining listeners before the first update, we must
+ // ensure that emit is not executed in the current tick. process.nextTick
+ // or queueMicrotask are redundant here because of await in the _loop
+ // function.
+ this._loop().catch(e => this._catchError(new TdlError(e)))
+ } else {
+ this._clientId = this._tdlib.tdn.createClientId()
+ // The new tdjson interface requires to send a dummy request first
+ this._sendTdl({ _: 'getOption', name: 'version' })
+ }
+ }
+
+ getClientId (): number {
+ return this._clientId
}
/** @deprecated */
- static create (tdlibInstance: TdjsonCompat, options: ClientOptions = {}): Client {
+ static create (tdlibInstance: TdjsonTdlTdlibAddon, options: ClientOptions = {}): Client {
return new Client(tdlibInstance, options)
}
@@ -383,15 +474,12 @@ export class Client {
/** @deprecated */
connect: () => Promise = () => Promise.resolve()
-
/** @deprecated */
connectAndLogin: (fn?: () => LoginDetails) => Promise = (fn = emptyDetails) => {
return this.login(fn)
}
-
/** @deprecated */
getBackendName: () => string = () => 'addon'
-
/** @deprecated */
pause: () => void = () => {
if (!this._paused) {
@@ -401,7 +489,6 @@ export class Client {
debug('pause (no-op)')
}
}
-
/** @deprecated */
resume: () => void = () => {
if (this._paused) {
@@ -462,6 +549,12 @@ export class Client {
destroy: () => void = () => {
debug('destroy')
+ if (this._tdn) {
+ this._onClose()
+ this._clientId = -1
+ this.emit('destroy')
+ return
+ }
if (this._client === null) return
this._tdlib.destroy(this._client)
this._client = null
@@ -473,7 +566,7 @@ export class Client {
close: () => Promise = () => {
debug('close')
return new Promise(resolve => {
- if (this._client === null) return resolve()
+ if (this._client === null && this._clientId === -1) return resolve()
// TODO: call this.resume() here?
// If the client is paused, we can't receive authorizationStateClosed
// and destroy won't be called
@@ -495,6 +588,13 @@ export class Client {
execute: Execute = request => {
debugReq('execute', request)
+ if (this._tdn) {
+ const tdRequest = deepRenameKey('_', '@type', request)
+ // the client can be null, it's fine
+ const tdResponse = this._tdlib.tdn.execute(JSON.stringify(tdRequest))
+ if (tdResponse == null) return null
+ return deepRenameKey('@type', '_', JSON.parse(tdResponse))
+ }
const tdRequest = deepRenameKey('_', '@type', request)
// the client can be null, it's fine
const tdResponse = this._tdlib.execute(this._client, tdRequest)
@@ -505,23 +605,21 @@ export class Client {
_send (request: { +_: string, +[k: any]: any }): void {
debugReq('send', request)
const tdRequest = deepRenameKey('_', '@type', request)
- if (this._client === null)
+ if (this._client === null && this._clientId === -1)
throw new Error('A closed client cannot be reused, create a new Client')
- this._tdlib.send(this._client, tdRequest)
+ if (this._tdn)
+ this._tdlib.tdn.send(this._clientId, JSON.stringify(tdRequest))
+ else
+ this._tdlib.send(this._client, tdRequest)
}
_sendTdl (request: { +_: string, +[k: any]: any }): void {
this._send({ ...request, '@extra': TDL_MAGIC })
}
- async _receive (timeout: number = this._options.receiveTimeout): Promise {
- if (this._client === null) return null
- const tdResponse = await this._tdlib.receive(this._client, timeout)
- if (tdResponse == null) return null
- return deepRenameKey('@type', '_', tdResponse)
- }
-
+ // Used with the old tdjson interface only
async _loop (): Promise {
+ const timeout = this._options.receiveTimeout
while (true) {
if (this._paused) {
debug('receive loop: waiting for resume')
@@ -534,21 +632,30 @@ export class Client {
break
}
- const response = await this._receive()
+ const res = await this._tdlib.receive(this._client, timeout)
- if (!response) {
+ if (res == null) {
debug('receive loop: response is empty')
continue
}
try {
- this._handleReceive(response)
+ this._handleReceive(deepRenameKey('@type', '_', res))
} catch (e) {
this._catchError(new TdlError(e))
}
}
}
+ // Can be called by the client manager in case the new interface is used
+ handleReceive (res: any): void {
+ try {
+ this._handleReceive(deepRenameKey('@type', '_', res))
+ } catch (e) {
+ this._catchError(new TdlError(e))
+ }
+ }
+
// This function can be called with any TDLib object
_handleReceive (res: any): void {
this.emit('response', res) // TODO: rename or remove this event
diff --git a/packages/tdl/src/index.js b/packages/tdl/src/index.js
index 183bc0a..9e47ebb 100644
--- a/packages/tdl/src/index.js
+++ b/packages/tdl/src/index.js
@@ -26,20 +26,28 @@ const defaultLibraryFile = (() => {
export type TDLibConfiguration = {
tdjson?: string,
libdir?: string,
- verbosityLevel?: number | 'default'
+ verbosityLevel?: number | 'default',
+ receiveTimeout?: number,
+ useNewTdjsonInterface?: boolean
}
// TODO: Use Required from new Flow versions
type StrictTDLibConfiguration = {
tdjson: string,
libdir: string,
- verbosityLevel: number | 'default'
+ verbosityLevel: number | 'default',
+ receiveTimeout: number,
+ useNewTdjsonInterface: boolean
}
+const defaultReceiveTimeout = 10
+
const cfg: StrictTDLibConfiguration = {
tdjson: defaultLibraryFile,
libdir: '',
- verbosityLevel: 2
+ verbosityLevel: 2,
+ receiveTimeout: defaultReceiveTimeout,
+ useNewTdjsonInterface: false
}
export function configure (opts: TDLibConfiguration = {}): void {
@@ -48,6 +56,8 @@ export function configure (opts: TDLibConfiguration = {}): void {
if (opts.tdjson != null) cfg.tdjson = opts.tdjson
if (opts.libdir != null) cfg.libdir = opts.libdir
if (opts.verbosityLevel != null) cfg.verbosityLevel = opts.verbosityLevel
+ if (opts.receiveTimeout != null) cfg.receiveTimeout = opts.receiveTimeout
+ if (opts.useNewTdjsonInterface != null) cfg.useNewTdjsonInterface = opts.useNewTdjsonInterface
}
export function init (): void {
@@ -57,10 +67,12 @@ export function init (): void {
tdjsonAddon = loadAddon(lib)
if (cfg.verbosityLevel !== 'default') {
debug('Executing setLogVerbosityLevel', cfg.verbosityLevel)
- tdjsonAddon.execute(null, {
+ const request = JSON.stringify({
'@type': 'setLogVerbosityLevel',
new_verbosity_level: cfg.verbosityLevel
})
+ if (cfg.useNewTdjsonInterface) tdjsonAddon.tdn.execute(request)
+ else tdjsonAddon.execute(null, request)
}
}
@@ -70,29 +82,84 @@ export const execute: Execute = function execute (request: any) {
if (!tdjsonAddon) throw Error('TDLib is uninitialized')
}
debug('execute', request)
- const tdRequest = deepRenameKey('_', '@type', request)
- const tdResponse = tdjsonAddon.execute(null, tdRequest)
- if (tdResponse == null) return null
- return deepRenameKey('@type', '_', tdResponse)
+ request = JSON.stringify(deepRenameKey('_', '@type', request))
+ const response = cfg.useNewTdjsonInterface
+ ? tdjsonAddon.tdn.execute(request)
+ : tdjsonAddon.execute(null, request)
+ if (response == null) return null
+ return deepRenameKey('@type', '_', JSON.parse(response))
}
-export function createClient (opts: any): Client {
+export function setLogMessageCallback (
+ maxVerbosityLevel: number,
+ callback: null | ((verbosityLevel: number, message: string) => void)
+): void {
if (!tdjsonAddon) {
init()
if (!tdjsonAddon) throw Error('TDLib is uninitialized')
}
- return new Client(tdjsonAddon, opts)
+ tdjsonAddon.setLogMessageCallback(maxVerbosityLevel, callback)
}
-export function setLogMessageCallback (
- maxVerbosityLevel: number,
- callback: null | (verbosityLevel: number, message: string) => void
-): void {
+const clientMap: Map = new Map()
+let tdnInitialized = false
+let runningReceiveLoop = false
+
+// Loop for the new tdjson interface
+async function receiveLoop () {
+ debug('Starting receive loop')
+ runningReceiveLoop = true
+ try {
+ while (true) {
+ if (clientMap.size < 1) {
+ debug('Stopping receive loop')
+ break
+ }
+ // $FlowIgnore[incompatible-use]
+ const responseString = await tdjsonAddon.tdn.receive()
+ if (responseString == null) {
+ debug('Receive loop: got empty response')
+ continue
+ }
+ const res = JSON.parse(responseString)
+ const clientId = res['@client_id']
+ const client = clientId != null ? clientMap.get(clientId) : undefined
+ if (client == null) {
+ debug(`Cannot find client_id ${clientId}`)
+ continue
+ }
+ delete res['@client_id'] // Note that delete is somewhat slow
+ client.handleReceive(res)
+ }
+ } finally {
+ runningReceiveLoop = false
+ }
+}
+
+export function createClient (opts: any): Client {
if (!tdjsonAddon) {
init()
if (!tdjsonAddon) throw Error('TDLib is uninitialized')
}
- tdjsonAddon.setLogMessageCallback(maxVerbosityLevel, callback)
+ if (cfg.useNewTdjsonInterface) {
+ if (!tdnInitialized) {
+ tdjsonAddon.tdn.init(cfg.receiveTimeout)
+ tdnInitialized = true
+ }
+ const onClose = () => {
+ debug(`Deleting client_id ${clientId}`)
+ clientMap.delete(clientId)
+ }
+ const client = new Client(tdjsonAddon, opts, { useTdn: true, onClose })
+ const clientId = client.getClientId()
+ clientMap.set(clientId, client)
+ if (!runningReceiveLoop)
+ receiveLoop()
+ return client
+ }
+ if (cfg.receiveTimeout !== defaultReceiveTimeout)
+ return new Client(tdjsonAddon, { ...opts, receiveTimeout: cfg.receiveTimeout })
+ return new Client(tdjsonAddon, opts)
}
// TODO: We could possibly export an unsafe/unstable getRawTdjson() : Tdjson
diff --git a/tests/integration/tdlib.test.js b/tests/integration/tdlib.test.js
index 4451682..a376bb3 100644
--- a/tests/integration/tdlib.test.js
+++ b/tests/integration/tdlib.test.js
@@ -28,6 +28,7 @@ let testName/*: string */
let createClient/*: () => tdl.Client */
const testingTdlTdlibAddon = process.env.TEST_TDL_TDLIB_ADDON === '1'
+const testingNewTdjson = process.env.TEST_NEW_TDJSON === '1'
if (testingTdlTdlibAddon) {
testName = 'tdl with tdl-tdlib-addon (backward compatibility)'
createClient = function () {
@@ -35,6 +36,19 @@ if (testingTdlTdlibAddon) {
if (libdir) tdjson = path.join(projectRoot, defaultLibraryFile)
return new tdl.Client(new TDLib(tdjson), { bare: true })
}
+} else if (testingNewTdjson) {
+ const ver = tdjson && tdjson.match(/td-1\.(\d)\.0/)
+ if (ver && ver[1] && Number(ver[1]) < 7) {
+ console.log('TEST_NEW_TDJSON is disabled for TDLib < 1.7.0.')
+ process.exit(0)
+ }
+ testName = 'tdl with useNewTdjsonInterface'
+ createClient = function () {
+ if (libdir) tdl.configure({ libdir })
+ else tdl.configure({ tdjson })
+ tdl.configure({ useNewTdjsonInterface: true })
+ return tdl.createClient({ bare: true })
+ }
} else {
testName = 'tdl'
createClient = function () {
@@ -50,7 +64,7 @@ describe(testName, () => {
client.on('error', e => console.error('error', e))
client.on('update', u => {
- console.log('update', u)
+ // console.log('update', u)
updates.push(u)
})
|