tdlib-native
Version:
🚀 Telegram TDLib native nodejs wrapper
302 lines (301 loc) • 7.44 kB
JavaScript
import { $AsyncApi, $SyncApi, typename } from "./generated/types.mjs";
import { deserialize, serialize } from "./json.mjs";
import { promiseWithResolvers } from "./shared/async.mjs";
import { EventBus } from "./event-bus.mjs";
import debug from "debug";
import { TDLibOptions } from "./options.mjs";
import { assert } from "./assert.mjs";
const debugJson = debug("tdlib-native:json");
const tag = "@extra";
class TDError extends Error {
[typename] = "error";
code;
method;
parameters;
name = "TDError";
/**
* Creates an instance of TDError.
* @param {string} message
* @param {*} [options={}]
* @memberof TDError
*/
constructor(message, { code = Number.NaN, method = "unknown method", parameters = {} } = {}) {
super(message);
this.code = code;
this.method = method;
this.parameters = parameters;
}
/**
*
*
* @returns {object}
* @memberof TDError
*/
toJSON() {
return {
// eslint-disable-next-line security/detect-object-injection
[typename]: this[typename],
name: this.name,
message: this.message,
code: this.code,
method: this.method,
parameters: this.parameters
};
}
}
class Client {
_client;
_requests = /* @__PURE__ */ new Map();
_updates = new EventBus();
_adapter;
_state = "PAUSED";
/**
* Creates an instance of Client.
* @param {TDLib} adapter
* @memberof Client
*/
constructor(adapter) {
this._adapter = adapter;
this._client = adapter.create(300);
Object.seal(this);
}
api = new $AsyncApi(this);
syncApi = new $SyncApi(this);
_tdlibOptions = new TDLibOptions(this.api);
/**
*
*
* @template {keyof $AsyncApi} T
* @param {T} method
* @param {object} parameters
* @returns {object} {Promise<ReturnType<$AsyncApi[T]>>}
* @throws {TDError} - {@link TDError}
* @memberof Client
*/
async invoke(method, parameters) {
const extra = Math.random();
const data = promiseWithResolvers();
assignTemporary(parameters, { [typename]: method, [tag]: extra }, (merged) => {
const value = serialize(merged);
debugJson("Sent %s", value);
this._adapter.send(this._client, value);
});
this._requests.set(extra, data);
try {
return await data.promise;
} catch (error2) {
if (typeof error2 === "object" && error2 && typename in error2) {
const value = error2;
throw new TDError(value.message, { code: value.code, method, parameters });
}
throw error2;
}
}
/**
* Disables logging of TDLib instance
*
* @static
* @param {TDLib} lib
* @memberof Client
* @returns {void}
*/
static disableLogs(lib) {
Client.execute(lib, "setLogVerbosityLevel", { new_verbosity_level: 0 });
}
/**
*
*
* @template {keyof $SyncApi} T
* @param {TDLib|Client} executor
* @param {T} method
* @param {object} parameters
* @returns {object} {Promise<ReturnType<$SyncMethodsDict[T]>>}
* @throws {TDError} - {@link TDError}
* @memberof Client
*/
static execute(executor, method, parameters) {
let adapter;
let client;
if (executor instanceof Client) {
adapter = executor._adapter;
client = executor._client;
} else if (executor._isTDLib) {
adapter = executor;
} else {
throw new TDError("Invalid executor passed", {
code: -100,
method,
parameters
});
}
const extra = Math.random();
const value = assignTemporary(
parameters,
{ [typename]: method, [tag]: extra },
(merged) => {
const value2 = serialize(merged);
debugJson("Sent sync %s", value2);
return adapter.execute(client ?? null, value2);
}
);
debugJson("Received sync %s", value);
assert(value, new TDError("Method returned null", { method, parameters }));
let data;
try {
data = deserialize(value);
} catch {
throw new TDError("Method returned invalid json", { method, parameters });
}
assert(
typeof data === "object" && data && typename in data,
new TDError("Returned not an object", { method, parameters })
);
if (data[typename] === "error") {
const error2 = data;
throw new TDError(error2.message, { code: error2.code, method, parameters });
}
if (tag in data && data[tag]) {
delete data[tag];
}
return data;
}
/**
*
*
* @template {keyof $SyncApi} T
* @param {T} method
* @param {object} parameters
* @returns {object} {Promise<ReturnType<$SyncApi[T]>>}
* @throws {TDError} - {@link TDError}
* @memberof Client
*/
execute(method, parameters) {
return Client.execute(this, method, parameters);
}
_running;
/**
*
*
* @private
* @returns {Promise<void>}
* @memberof Client
*/
async _thread() {
while (this._state === "RUNNING") {
let value;
try {
value = await this._adapter.receive(this._client);
} catch (error2) {
if (error2 instanceof Error && error2.message.includes("destroyed")) {
break;
}
throw error2;
}
debugJson("Received %s", value);
if (!value) {
continue;
}
let data;
try {
data = deserialize(value);
} catch {
continue;
}
if (typeof data !== "object" || !data) {
continue;
}
const extra = data == null ? void 0 : data[tag];
if (extra) {
const async = this._requests.get(extra);
delete data[tag];
if (data[typename] === "error") {
async == null ? void 0 : async.reject(data);
} else {
async == null ? void 0 : async.resolve(data);
}
this._requests.delete(extra);
continue;
}
if (data[typename].startsWith("update")) {
this._updates.emit(data);
}
}
}
/**
*
* @see https://core.telegram.org/tdlib/options
* @readonly
* @memberof Client
*/
get tdlibOptions() {
return this._tdlibOptions;
}
/**
*
*
* @readonly
* @type {Observable<Update>}
* @memberof Client
*/
get updates() {
return this._updates;
}
/**
*
*
* @returns {Promise<void>}
* @memberof Client
*/
async start() {
assert(
this._state === "PAUSED" && !this._running,
"Cannot start: This client is running or destroyed"
);
this._state = "RUNNING";
this._running = this._thread().finally(() => {
this._running = void 0;
});
}
/**
*
*
* @returns {this}
* @memberof Client
*/
async pause() {
assert(
this._state === "RUNNING",
"Cannot pause: This client is paused or destroyed"
);
this._state = "PAUSED";
await this._running;
}
/**
*
*
* @returns {void}
* @memberof Client
*/
async destroy() {
if (this._state === "STOPPED") return;
this._state = "STOPPED";
this._updates.complete();
for (const async of this._requests.values()) {
async.reject(new TDError("Client is destroyed", { code: -1 }));
}
this._requests.clear();
await this._adapter.destroy(this._client);
}
}
function assignTemporary(object, data, callback) {
const merged = Object.assign(object, data);
const result = callback(merged);
for (const key of Object.keys(data)) {
delete object[key];
}
return result;
}
export {
Client,
TDError
};