summaryrefslogtreecommitdiff
path: root/protocols/Telegram/tdlib/td/example/web/tdweb/src
diff options
context:
space:
mode:
authorGeorge Hazan <ghazan@miranda.im>2022-11-30 17:48:47 +0300
committerGeorge Hazan <ghazan@miranda.im>2022-11-30 17:48:47 +0300
commit0ece30dc7c0e34b4c5911969b8fa99c33c6d023c (patch)
tree671325d3fec09b999411e4e3ab84ef8259261818 /protocols/Telegram/tdlib/td/example/web/tdweb/src
parent46c53ffc6809c67e4607e99951a2846c382b63b2 (diff)
Telegram: update for TDLIB
Diffstat (limited to 'protocols/Telegram/tdlib/td/example/web/tdweb/src')
-rw-r--r--protocols/Telegram/tdlib/td/example/web/tdweb/src/index.js680
-rw-r--r--protocols/Telegram/tdlib/td/example/web/tdweb/src/logger.js47
-rw-r--r--protocols/Telegram/tdlib/td/example/web/tdweb/src/wasm-utils.js136
-rw-r--r--protocols/Telegram/tdlib/td/example/web/tdweb/src/worker.js1034
4 files changed, 1897 insertions, 0 deletions
diff --git a/protocols/Telegram/tdlib/td/example/web/tdweb/src/index.js b/protocols/Telegram/tdlib/td/example/web/tdweb/src/index.js
new file mode 100644
index 0000000000..2f159b9424
--- /dev/null
+++ b/protocols/Telegram/tdlib/td/example/web/tdweb/src/index.js
@@ -0,0 +1,680 @@
+import MyWorker from './worker.js';
+//import localforage from 'localforage';
+import BroadcastChannel from 'broadcast-channel';
+import uuid4 from 'uuid/v4';
+import log from './logger.js';
+
+const sleep = ms => new Promise(res => setTimeout(res, ms));
+
+/**
+ * TDLib in a browser
+ *
+ * TDLib can be compiled to WebAssembly or asm.js using Emscripten compiler and used in a browser from JavaScript.
+ * This is a convenient wrapper for TDLib in a browser which controls TDLib instance creation, handles interaction
+ * with TDLib and manages a filesystem for persistent TDLib data.
+ * TDLib instance is created in a Web Worker to run it in a separate thread.
+ * TdClient just sends queries to the Web Worker and receives updates and results from it.
+ * <br>
+ * <br>
+ * Differences from the TDLib JSON API:<br>
+ * 1. Added the update <code>updateFatalError error:string = Update;</code> which is sent whenever TDLib encounters a fatal error.<br>
+ * 2. Added the method <code>setJsLogVerbosityLevel new_verbosity_level:string = Ok;</code>, which allows to change the verbosity level of tdweb logging.<br>
+ * 3. Added the possibility to use blobs as input files via the constructor <code>inputFileBlob data:<JavaScript blob> = InputFile;</code>.<br>
+ * 4. The class <code>filePart</code> contains data as a JavaScript blob instead of a base64-encoded string.<br>
+ * 5. The methods <code>getStorageStatistics</code>, <code>getStorageStatisticsFast</code>, <code>optimizeStorage</code>, <code>addProxy</code> and <code>getFileDownloadedPrefixSize</code> are not supported.<br>
+ * <br>
+ */
+class TdClient {
+ /**
+ * @callback TdClient~updateCallback
+ * @param {Object} update The update.
+ */
+
+ /**
+ * Create TdClient.
+ * @param {Object} options - Options for TDLib instance creation.
+ * @param {TdClient~updateCallback} options.onUpdate - Callback for all incoming updates.
+ * @param {string} [options.instanceName=tdlib] - Name of the TDLib instance. Currently only one instance of TdClient with a given name is allowed. All but one instances with the same name will be automatically closed. Usually, the newest non-background instance is kept alive. Files will be stored in an IndexedDb table with the same name.
+ * @param {boolean} [options.isBackground=false] - Pass true if the instance is opened from the background.
+ * @param {string} [options.jsLogVerbosityLevel=info] - The initial verbosity level of the JavaScript part of the code (one of 'error', 'warning', 'info', 'log', 'debug').
+ * @param {number} [options.logVerbosityLevel=2] - The initial verbosity level for the TDLib internal logging (0-1023).
+ * @param {boolean} [options.useDatabase=true] - Pass false to use TDLib without database and secret chats. It will significantly improve loading time, but some functionality will be unavailable.
+ * @param {boolean} [options.readOnly=false] - For debug only. Pass true to open TDLib database in read-only mode
+ * @param {string} [options.mode=auto] - For debug only. The type of the TDLib build to use. 'asmjs' for asm.js and 'wasm' for WebAssembly. If mode == 'auto' WebAbassembly will be used if supported by browser, asm.js otherwise.
+ */
+ constructor(options) {
+ log.setVerbosity(options.jsLogVerbosityLevel);
+ this.worker = new MyWorker();
+ this.worker.onmessage = e => {
+ this.onResponse(e.data);
+ };
+ this.query_id = 0;
+ this.query_callbacks = new Map();
+ if ('onUpdate' in options) {
+ this.onUpdate = options.onUpdate;
+ delete options.onUpdate;
+ }
+ options.instanceName = options.instanceName || 'tdlib';
+ this.fileManager = new FileManager(options.instanceName, this);
+ this.worker.postMessage({ '@type': 'init', options: options });
+ this.closeOtherClients(options);
+ }
+
+ /**
+ * Send a query to TDLib.
+ *
+ * If the query contains the field '@extra', the same field will be added into the result.
+ *
+ * @param {Object} query - The query for TDLib. See the [td_api.tl]{@link https://github.com/tdlib/td/blob/master/td/generate/scheme/td_api.tl} scheme or
+ * the automatically generated [HTML documentation]{@link https://core.telegram.org/tdlib/docs/td__api_8h.html}
+ * for a list of all available TDLib [methods]{@link https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1_function.html} and
+ * [classes]{@link https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1_object.html}.
+ * @returns {Promise} Promise object represents the result of the query.
+ */
+ send(query) {
+ return this.doSend(query, true);
+ }
+
+ /** @private */
+ sendInternal(query) {
+ return this.doSend(query, false);
+ }
+ /** @private */
+ doSend(query, isExternal) {
+ this.query_id++;
+ if (query['@extra']) {
+ query['@extra'] = {
+ '@old_extra': JSON.parse(JSON.stringify(query['@extra'])),
+ query_id: this.query_id
+ };
+ } else {
+ query['@extra'] = {
+ query_id: this.query_id
+ };
+ }
+ if (query['@type'] === 'setJsLogVerbosityLevel') {
+ log.setVerbosity(query.new_verbosity_level);
+ }
+
+ log.debug('send to worker: ', query);
+ const res = new Promise((resolve, reject) => {
+ this.query_callbacks.set(this.query_id, [resolve, reject]);
+ });
+ if (isExternal) {
+ this.externalPostMessage(query);
+ } else {
+ this.worker.postMessage(query);
+ }
+ return res;
+ }
+
+ /** @private */
+ externalPostMessage(query) {
+ const unsupportedMethods = [
+ 'getStorageStatistics',
+ 'getStorageStatisticsFast',
+ 'optimizeStorage',
+ 'addProxy',
+ 'init',
+ 'start'
+ ];
+ if (unsupportedMethods.includes(query['@type'])) {
+ this.onResponse({
+ '@type': 'error',
+ '@extra': query['@extra'],
+ code: 400,
+ message: "Method '" + query['@type'] + "' is not supported"
+ });
+ return;
+ }
+ if (query['@type'] === 'readFile' || query['@type'] === 'readFilePart') {
+ this.readFile(query);
+ return;
+ }
+ if (query['@type'] === 'deleteFile') {
+ this.deleteFile(query);
+ return;
+ }
+ this.worker.postMessage(query);
+ }
+
+ /** @private */
+ async readFile(query) {
+ const response = await this.fileManager.readFile(query);
+ this.onResponse(response);
+ }
+
+ /** @private */
+ async deleteFile(query) {
+ const response = this.fileManager.deleteFile(query);
+ try {
+ if (response.idb_key) {
+ await this.sendInternal({
+ '@type': 'deleteIdbKey',
+ idb_key: response.idb_key
+ });
+ delete response.idb_key;
+ }
+ await this.sendInternal({
+ '@type': 'deleteFile',
+ file_id: query.file_id
+ });
+ } catch (e) {}
+ this.onResponse(response);
+ }
+
+ /** @private */
+ onResponse(response) {
+ log.debug(
+ 'receive from worker: ',
+ JSON.parse(
+ JSON.stringify(response, (key, value) => {
+ if (key === 'arr' || key === 'data') {
+ return undefined;
+ }
+ return value;
+ })
+ )
+ );
+
+ // for FileManager
+ response = this.prepareResponse(response);
+
+ if ('@extra' in response) {
+ const query_id = response['@extra'].query_id;
+ const [resolve, reject] = this.query_callbacks.get(query_id);
+ this.query_callbacks.delete(query_id);
+ if ('@old_extra' in response['@extra']) {
+ response['@extra'] = response['@extra']['@old_extra'];
+ }
+ if (resolve) {
+ if (response['@type'] === 'error') {
+ reject(response);
+ } else {
+ resolve(response);
+ }
+ }
+ } else {
+ if (response['@type'] === 'inited') {
+ this.onInited();
+ return;
+ }
+ if (response['@type'] === 'fsInited') {
+ this.onFsInited();
+ return;
+ }
+ if (
+ response['@type'] === 'updateAuthorizationState' &&
+ response.authorization_state['@type'] === 'authorizationStateClosed'
+ ) {
+ this.onClosed();
+ }
+ this.onUpdate(response);
+ }
+ }
+
+ /** @private */
+ prepareFile(file) {
+ return this.fileManager.registerFile(file);
+ }
+
+ /** @private */
+ prepareResponse(response) {
+ if (response['@type'] === 'file') {
+ if (false && Math.random() < 0.1) {
+ (async () => {
+ log.warn('DELETE FILE', response.id);
+ try {
+ await this.send({ '@type': 'deleteFile', file_id: response.id });
+ } catch (e) {}
+ })();
+ }
+ return this.prepareFile(response);
+ }
+ for (const key in response) {
+ const field = response[key];
+ if (
+ field &&
+ typeof field === 'object' &&
+ key !== 'data' &&
+ key !== 'arr'
+ ) {
+ response[key] = this.prepareResponse(field);
+ }
+ }
+ return response;
+ }
+
+ /** @private */
+ onBroadcastMessage(e) {
+ //const message = e.data;
+ const message = e;
+ if (message.uid === this.uid) {
+ log.info('ignore self broadcast message: ', message);
+ return;
+ }
+ log.info('got broadcast message: ', message);
+ if (message.isBackground && !this.isBackground) {
+ // continue
+ } else if (
+ (!message.isBackground && this.isBackground) ||
+ message.timestamp > this.timestamp
+ ) {
+ this.close();
+ return;
+ }
+ if (message.state === 'closed') {
+ this.waitSet.delete(message.uid);
+ if (this.waitSet.size === 0) {
+ log.info('onWaitSetEmpty');
+ this.onWaitSetEmpty();
+ this.onWaitSetEmpty = () => {};
+ }
+ } else {
+ this.waitSet.add(message.uid);
+ if (message.state !== 'closing') {
+ this.postState();
+ }
+ }
+ }
+
+ /** @private */
+ postState() {
+ const state = {
+ uid: this.uid,
+ state: this.state,
+ timestamp: this.timestamp,
+ isBackground: this.isBackground
+ };
+ log.info('Post state: ', state);
+ this.channel.postMessage(state);
+ }
+
+ /** @private */
+ onWaitSetEmpty() {
+ // nop
+ }
+
+ /** @private */
+ onFsInited() {
+ this.fileManager.init();
+ }
+
+ /** @private */
+ onInited() {
+ this.isInited = true;
+ this.doSendStart();
+ }
+
+ /** @private */
+ sendStart() {
+ this.wantSendStart = true;
+ this.doSendStart();
+ }
+
+ /** @private */
+ doSendStart() {
+ if (!this.isInited || !this.wantSendStart || this.state !== 'start') {
+ return;
+ }
+ this.wantSendStart = false;
+ this.state = 'active';
+ const query = { '@type': 'start' };
+ log.info('send to worker: ', query);
+ this.worker.postMessage(query);
+ }
+
+ /** @private */
+ onClosed() {
+ this.isClosing = true;
+ this.worker.terminate();
+ log.info('worker is terminated');
+ this.state = 'closed';
+ this.postState();
+ }
+
+ /** @private */
+ close() {
+ if (this.isClosing) {
+ return;
+ }
+ this.isClosing = true;
+
+ log.info('close state: ', this.state);
+
+ if (this.state === 'start') {
+ this.onClosed();
+ this.onUpdate({
+ '@type': 'updateAuthorizationState',
+ authorization_state: {
+ '@type': 'authorizationStateClosed'
+ }
+ });
+ return;
+ }
+
+ const query = { '@type': 'close' };
+ log.info('send to worker: ', query);
+ this.worker.postMessage(query);
+
+ this.state = 'closing';
+ this.postState();
+ }
+
+ /** @private */
+ async closeOtherClients(options) {
+ this.uid = uuid4();
+ this.state = 'start';
+ this.isBackground = !!options.isBackground;
+ this.timestamp = Date.now();
+ this.waitSet = new Set();
+
+ log.info('close other clients');
+ this.channel = new BroadcastChannel(options.instanceName, {
+ webWorkerSupport: false
+ });
+
+ this.postState();
+
+ this.channel.onmessage = message => {
+ this.onBroadcastMessage(message);
+ };
+
+ await sleep(300);
+ if (this.waitSet.size !== 0) {
+ await new Promise(resolve => {
+ this.onWaitSetEmpty = resolve;
+ });
+ }
+ this.sendStart();
+ }
+
+ /** @private */
+ onUpdate(update) {
+ log.info('ignore onUpdate');
+ //nop
+ }
+}
+
+/** @private */
+class ListNode {
+ constructor(value) {
+ this.value = value;
+ this.clear();
+ }
+
+ erase() {
+ this.prev.connect(this.next);
+ this.clear();
+ }
+ clear() {
+ this.prev = this;
+ this.next = this;
+ }
+
+ connect(other) {
+ this.next = other;
+ other.prev = this;
+ }
+
+ onUsed(other) {
+ other.usedAt = Date.now();
+ other.erase();
+ other.connect(this.next);
+ log.debug('LRU: used file_id: ', other.value);
+ this.connect(other);
+ }
+
+ getLru() {
+ if (this === this.next) {
+ throw new Error('popLru from empty list');
+ }
+ return this.prev;
+ }
+}
+
+/** @private */
+class FileManager {
+ constructor(instanceName, client) {
+ this.instanceName = instanceName;
+ this.cache = new Map();
+ this.pending = [];
+ this.transaction_id = 0;
+ this.totalSize = 0;
+ this.lru = new ListNode(-1);
+ this.client = client;
+ }
+
+ init() {
+ this.idb = new Promise((resolve, reject) => {
+ const request = indexedDB.open(this.instanceName);
+ request.onsuccess = () => resolve(request.result);
+ request.onerror = () => reject(request.error);
+ });
+ //this.store = localforage.createInstance({
+ //name: instanceName
+ //});
+ this.isInited = true;
+ }
+
+ unload(info) {
+ if (info.arr) {
+ log.debug(
+ 'LRU: delete file_id: ',
+ info.node.value,
+ ' with arr.length: ',
+ info.arr.length
+ );
+ this.totalSize -= info.arr.length;
+ delete info.arr;
+ }
+ if (info.node) {
+ info.node.erase();
+ delete info.node;
+ }
+ }
+
+ registerFile(file) {
+ if (file.idb_key || file.arr) {
+ file.local.is_downloading_completed = true;
+ } else {
+ file.local.is_downloading_completed = false;
+ }
+ let info = {};
+ const cached_info = this.cache.get(file.id);
+ if (cached_info) {
+ info = cached_info;
+ } else {
+ this.cache.set(file.id, info);
+ }
+ if (file.idb_key) {
+ info.idb_key = file.idb_key;
+ delete file.idb_key;
+ } else {
+ delete info.idb_key;
+ }
+ if (file.arr) {
+ const now = Date.now();
+ while (this.totalSize > 100000000) {
+ const node = this.lru.getLru();
+ // immunity for 60 seconds
+ if (node.usedAt + 60 * 1000 > now) {
+ break;
+ }
+ const lru_info = this.cache.get(node.value);
+ this.unload(lru_info);
+ }
+
+ if (info.arr) {
+ log.warn('Got file.arr at least twice for the same file');
+ this.totalSize -= info.arr.length;
+ }
+ info.arr = file.arr;
+ delete file.arr;
+ this.totalSize += info.arr.length;
+ if (!info.node) {
+ log.debug(
+ 'LRU: create file_id: ',
+ file.id,
+ ' with arr.length: ',
+ info.arr.length
+ );
+ info.node = new ListNode(file.id);
+ }
+ this.lru.onUsed(info.node);
+ log.info('Total file.arr size: ', this.totalSize);
+ }
+ info.file = file;
+ return file;
+ }
+
+ async flushLoad() {
+ const pending = this.pending;
+ this.pending = [];
+ const idb = await this.idb;
+ const transaction_id = this.transaction_id++;
+ const read = idb
+ .transaction(['keyvaluepairs'], 'readonly')
+ .objectStore('keyvaluepairs');
+ log.debug('Load group of files from idb', pending.length);
+ for (const query of pending) {
+ const request = read.get(query.key);
+ request.onsuccess = event => {
+ const blob = event.target.result;
+ if (blob) {
+ if (blob.size === 0) {
+ log.error('Got empty blob from db ', query.key);
+ }
+ query.resolve({ data: blob, transaction_id: transaction_id });
+ } else {
+ query.reject();
+ }
+ };
+ request.onerror = () => query.reject(request.error);
+ }
+ }
+
+ load(key, resolve, reject) {
+ if (this.pending.length === 0) {
+ setTimeout(() => {
+ this.flushLoad();
+ }, 1);
+ }
+ this.pending.push({ key: key, resolve: resolve, reject: reject });
+ }
+
+ async doLoadFull(info) {
+ if (info.arr) {
+ return { data: new Blob([info.arr]), transaction_id: -1 };
+ }
+ if (info.idb_key) {
+ const idb_key = info.idb_key;
+ //return this.store.getItem(idb_key);
+ return await new Promise((resolve, reject) => {
+ this.load(idb_key, resolve, reject);
+ });
+ }
+ throw new Error('File is not loaded');
+ }
+ async doLoad(info, offset, size) {
+ if (!info.arr && !info.idb_key && info.file.local.path) {
+ try {
+ const count = await this.client.sendInternal({
+ '@type': 'getFileDownloadedPrefixSize',
+ file_id: info.file.id,
+ offset: offset
+ });
+ //log.error(count, size);
+ if (!size) {
+ size = count.count;
+ } else if (size > count.count) {
+ throw new Error('File not loaded yet');
+ }
+ const res = await this.client.sendInternal({
+ '@type': 'readFilePart',
+ path: info.file.local.path,
+ offset: offset,
+ count: size
+ });
+ res.data = new Blob([res.data]);
+ res.transaction_id = -2;
+ //log.error(res);
+ return res;
+ } catch (e) {
+ log.info('readFilePart failed', info, offset, size, e);
+ }
+ }
+
+ const res = await this.doLoadFull(info);
+
+ // return slice(size, offset + size)
+ const data_size = res.data.size;
+ if (!size) {
+ size = data_size;
+ }
+ if (offset > data_size) {
+ offset = data_size;
+ }
+ res.data = res.data.slice(offset, offset + size);
+ return res;
+ }
+
+ doDelete(info) {
+ this.unload(info);
+ return info.idb_key;
+ }
+
+ async readFile(query) {
+ try {
+ if (!this.isInited) {
+ throw new Error('FileManager is not inited');
+ }
+ const info = this.cache.get(query.file_id);
+ if (!info) {
+ throw new Error('File is not loaded');
+ }
+ if (info.node) {
+ this.lru.onUsed(info.node);
+ }
+ query.offset = query.offset || 0;
+ query.size = query.count || query.size || 0;
+ const response = await this.doLoad(info, query.offset, query.size);
+ return {
+ '@type': 'filePart',
+ '@extra': query['@extra'],
+ data: response.data,
+ transaction_id: response.transaction_id
+ };
+ } catch (e) {
+ return {
+ '@type': 'error',
+ '@extra': query['@extra'],
+ code: 400,
+ message: e
+ };
+ }
+ }
+
+ deleteFile(query) {
+ const res = {
+ '@type': 'ok',
+ '@extra': query['@extra']
+ };
+ try {
+ if (!this.isInited) {
+ throw new Error('FileManager is not inited');
+ }
+ const info = this.cache.get(query.file_id);
+ if (!info) {
+ throw new Error('File is not loaded');
+ }
+ const idb_key = this.doDelete(info);
+ if (idb_key) {
+ res.idb_key = idb_key;
+ }
+ } catch (e) {}
+ return res;
+ }
+}
+
+export default TdClient;
diff --git a/protocols/Telegram/tdlib/td/example/web/tdweb/src/logger.js b/protocols/Telegram/tdlib/td/example/web/tdweb/src/logger.js
new file mode 100644
index 0000000000..95baed0318
--- /dev/null
+++ b/protocols/Telegram/tdlib/td/example/web/tdweb/src/logger.js
@@ -0,0 +1,47 @@
+class Logger {
+ constructor() {
+ this.setVerbosity('WARNING');
+ }
+ debug(...str) {
+ if (this.checkVerbosity(4)) {
+ console.log(...str);
+ }
+ }
+ log(...str) {
+ if (this.checkVerbosity(4)) {
+ console.log(...str);
+ }
+ }
+ info(...str) {
+ if (this.checkVerbosity(3)) {
+ console.info(...str);
+ }
+ }
+ warn(...str) {
+ if (this.checkVerbosity(2)) {
+ console.warn(...str);
+ }
+ }
+ error(...str) {
+ if (this.checkVerbosity(1)) {
+ console.error(...str);
+ }
+ }
+ setVerbosity(level, default_level = 'info') {
+ if (level === undefined) {
+ level = default_level;
+ }
+ if (typeof level === 'string') {
+ level =
+ { ERROR: 1, WARNING: 2, INFO: 3, LOG: 4, DEBUG: 4 }[
+ level.toUpperCase()
+ ] || 2;
+ }
+ this.level = level;
+ }
+ checkVerbosity(level) {
+ return this.level >= level;
+ }
+}
+let log = new Logger();
+export default log;
diff --git a/protocols/Telegram/tdlib/td/example/web/tdweb/src/wasm-utils.js b/protocols/Telegram/tdlib/td/example/web/tdweb/src/wasm-utils.js
new file mode 100644
index 0000000000..50447d65b3
--- /dev/null
+++ b/protocols/Telegram/tdlib/td/example/web/tdweb/src/wasm-utils.js
@@ -0,0 +1,136 @@
+// 1. +++ fetchAndInstantiate() +++ //
+
+// This library function fetches the wasm module at 'url', instantiates it with
+// the given 'importObject', and returns the instantiated object instance
+
+export async function instantiateStreaming(url, importObject) {
+ let result = await WebAssembly.instantiateStreaming(fetch(url), importObject);
+ return result.instance;
+}
+export function fetchAndInstantiate(url, importObject) {
+ return fetch(url)
+ .then(response => response.arrayBuffer())
+ .then(bytes => WebAssembly.instantiate(bytes, importObject))
+ .then(results => results.instance);
+}
+
+// 2. +++ instantiateCachedURL() +++ //
+
+// This library function fetches the wasm Module at 'url', instantiates it with
+// the given 'importObject', and returns a Promise resolving to the finished
+// wasm Instance. Additionally, the function attempts to cache the compiled wasm
+// Module in IndexedDB using 'url' as the key. The entire site's wasm cache (not
+// just the given URL) is versioned by dbVersion and any change in dbVersion on
+// any call to instantiateCachedURL() will conservatively clear out the entire
+// cache to avoid stale modules.
+export function instantiateCachedURL(dbVersion, url, importObject) {
+ const dbName = 'wasm-cache';
+ const storeName = 'wasm-cache';
+
+ // This helper function Promise-ifies the operation of opening an IndexedDB
+ // database and clearing out the cache when the version changes.
+ function openDatabase() {
+ return new Promise((resolve, reject) => {
+ var request = indexedDB.open(dbName, dbVersion);
+ request.onerror = reject.bind(null, 'Error opening wasm cache database');
+ request.onsuccess = () => {
+ resolve(request.result);
+ };
+ request.onupgradeneeded = event => {
+ var db = request.result;
+ if (db.objectStoreNames.contains(storeName)) {
+ console.log(`Clearing out version ${event.oldVersion} wasm cache`);
+ db.deleteObjectStore(storeName);
+ }
+ console.log(`Creating version ${event.newVersion} wasm cache`);
+ db.createObjectStore(storeName);
+ };
+ });
+ }
+
+ // This helper function Promise-ifies the operation of looking up 'url' in the
+ // given IDBDatabase.
+ function lookupInDatabase(db) {
+ return new Promise((resolve, reject) => {
+ var store = db.transaction([storeName]).objectStore(storeName);
+ var request = store.get(url);
+ request.onerror = reject.bind(null, `Error getting wasm module ${url}`);
+ request.onsuccess = event => {
+ if (request.result) resolve(request.result);
+ else reject(`Module ${url} was not found in wasm cache`);
+ };
+ });
+ }
+
+ // This helper function fires off an async operation to store the given wasm
+ // Module in the given IDBDatabase.
+ function storeInDatabase(db, module) {
+ var store = db.transaction([storeName], 'readwrite').objectStore(storeName);
+ var request = store.put(module, url);
+ request.onerror = err => {
+ console.log(`Failed to store in wasm cache: ${err}`);
+ };
+ request.onsuccess = err => {
+ console.log(`Successfully stored ${url} in wasm cache`);
+ };
+ }
+
+ // This helper function fetches 'url', compiles it into a Module,
+ // instantiates the Module with the given import object.
+ function fetchAndInstantiate() {
+ return fetch(url)
+ .then(response => response.arrayBuffer())
+ .then(buffer => WebAssembly.instantiate(buffer, importObject));
+ }
+
+ // With all the Promise helper functions defined, we can now express the core
+ // logic of an IndexedDB cache lookup. We start by trying to open a database.
+ return openDatabase().then(
+ db => {
+ // Now see if we already have a compiled Module with key 'url' in 'db':
+ return lookupInDatabase(db).then(
+ module => {
+ // We do! Instantiate it with the given import object.
+ console.log(`Found ${url} in wasm cache`);
+ return WebAssembly.instantiate(module, importObject);
+ },
+ errMsg => {
+ // Nope! Compile from scratch and then store the compiled Module in 'db'
+ // with key 'url' for next time.
+ console.log(errMsg);
+ return fetchAndInstantiate().then(results => {
+ try {
+ storeInDatabase(db, results.module);
+ } catch (e) {
+ console.log('Failed to store module into db');
+ }
+ return results.instance;
+ });
+ }
+ );
+ },
+ errMsg => {
+ // If opening the database failed (due to permissions or quota), fall back
+ // to simply fetching and compiling the module and don't try to store the
+ // results.
+ console.log(errMsg);
+ return fetchAndInstantiate().then(results => results.instance);
+ }
+ );
+}
+
+export async function instantiateAny(version, url, importObject) {
+ console.log("instantiate");
+ try {
+ return await instantiateStreaming(url, importObject);
+ } catch (e) {
+ console.log("instantiateStreaming failed", e);
+ }
+ try {
+ return await instantiateCachedURL(version, url, importObject);
+ } catch (e) {
+ console.log("instantiateCachedURL failed", e);
+ }
+ throw new Error("can't instantiate wasm");
+}
+
diff --git a/protocols/Telegram/tdlib/td/example/web/tdweb/src/worker.js b/protocols/Telegram/tdlib/td/example/web/tdweb/src/worker.js
new file mode 100644
index 0000000000..dff1845126
--- /dev/null
+++ b/protocols/Telegram/tdlib/td/example/web/tdweb/src/worker.js
@@ -0,0 +1,1034 @@
+import localforage from 'localforage';
+import log from './logger.js';
+import { instantiateAny } from './wasm-utils.js';
+
+import td_wasm_release from './prebuilt/release/td_wasm.wasm';
+import td_asmjs_mem_release from './prebuilt/release/td_asmjs.js.mem';
+
+const tdlibVersion = 6;
+const localForageDrivers = [
+ localforage.INDEXEDDB,
+ localforage.LOCALSTORAGE,
+ 'memoryDriver'
+];
+
+async function initLocalForage() {
+ // Implement the driver here.
+ const memoryDriver = {
+ _driver: 'memoryDriver',
+ _initStorage: function(options) {
+ const dbInfo = {};
+ if (options) {
+ for (const i in options) {
+ dbInfo[i] = options[i];
+ }
+ }
+ this._dbInfo = dbInfo;
+ this._map = new Map();
+ },
+ clear: async function() {
+ this._map.clear();
+ },
+ getItem: async function(key) {
+ const value = this._map.get(key);
+ console.log('getItem', this._map, key, value);
+ return value;
+ },
+ iterate: async function(iteratorCallback) {
+ log.error('iterate is not supported');
+ },
+ key: async function(n) {
+ log.error('key n is not supported');
+ },
+ keys: async function() {
+ return this._map.keys();
+ },
+ length: async function() {
+ return this._map.size();
+ },
+ removeItem: async function(key) {
+ this._map.delete(key);
+ },
+ setItem: async function(key, value) {
+ const originalValue = this._map.get(key);
+ console.log('setItem', this._map, key, value);
+ this._map.set(key, value);
+ return originalValue;
+ }
+ };
+
+ // Add the driver to localForage.
+ localforage.defineDriver(memoryDriver);
+}
+
+async function loadTdlibWasm(onFS, wasmUrl) {
+ console.log('loadTdlibWasm');
+ const td_module = await import('./prebuilt/release/td_wasm.js');
+ const createTdwebModule = td_module.default;
+ log.info('got td_wasm.js', td_module, createTdwebModule);
+ let td_wasm = td_wasm_release;
+ if (wasmUrl) {
+ td_wasm = wasmUrl;
+ }
+ let module = createTdwebModule({
+ onRuntimeInitialized: () => {
+ log.info('runtime intialized');
+ onFS(module.FS);
+ },
+ instantiateWasm: (imports, successCallback) => {
+ log.info('start instantiateWasm', td_wasm, imports);
+ const next = instance => {
+ log.info('finish instantiateWasm');
+ successCallback(instance);
+ };
+ instantiateAny(tdlibVersion, td_wasm, imports).then(next);
+ return {};
+ },
+ ENVIROMENT: 'WORKER'
+ });
+ log.info('Wait module');
+ module = await module;
+ log.info('Got module', module);
+ //onFS(module.FS);
+ return module;
+}
+
+async function loadTdlibAsmjs(onFS) {
+ console.log('loadTdlibAsmjs');
+ const createTdwebModule = (await import('./prebuilt/release/td_asmjs.js'))
+ .default;
+ console.log('got td_asm.js', createTdwebModule);
+ const fromFile = 'td_asmjs.js.mem';
+ const toFile = td_asmjs_mem_release;
+ let module = createTdwebModule({
+ onRuntimeInitialized: () => {
+ console.log('runtime intialized');
+ onFS(module.FS);
+ },
+ locateFile: name => {
+ if (name === fromFile) {
+ return toFile;
+ }
+ return name;
+ },
+ ENVIROMENT: 'WORKER'
+ });
+ log.info('Wait module');
+ module = await module;
+ log.info('Got module', module);
+ //onFS(module.FS);
+ return module;
+}
+
+async function loadTdlib(mode, onFS, wasmUrl) {
+ const wasmSupported = (() => {
+ try {
+ if (
+ typeof WebAssembly === 'object' &&
+ typeof WebAssembly.instantiate === 'function'
+ ) {
+ const module = new WebAssembly.Module(
+ Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00)
+ );
+ if (module instanceof WebAssembly.Module)
+ return (
+ new WebAssembly.Instance(module) instanceof WebAssembly.Instance
+ );
+ }
+ } catch (e) {}
+ return false;
+ })();
+ if (!wasmSupported) {
+ if (mode === 'wasm') {
+ log.error('WebAssembly is not supported, trying to use it anyway');
+ } else {
+ log.warn('WebAssembly is not supported, trying to use asm.js');
+ mode = 'asmjs';
+ }
+ }
+
+ if (mode === 'asmjs') {
+ return loadTdlibAsmjs(onFS);
+ }
+ return loadTdlibWasm(onFS, wasmUrl);
+}
+
+class OutboundFileSystem {
+ constructor(root, FS) {
+ this.root = root;
+ this.nextFileId = 0;
+ this.FS = FS;
+ this.files = new Set();
+ FS.mkdir(root);
+ }
+ blobToPath(blob, name) {
+ const dir = this.root + '/' + this.nextFileId;
+ if (!name) {
+ name = 'blob';
+ }
+ this.nextFileId++;
+ this.FS.mkdir(dir);
+ this.FS.mount(
+ this.FS.filesystems.WORKERFS,
+ {
+ blobs: [{ name: name, data: blob }]
+ },
+ dir
+ );
+ const path = dir + '/' + name;
+ this.files.add(path);
+ return path;
+ }
+
+ forgetPath(path) {
+ if (this.files.has(path)) {
+ this.FS.unmount(path);
+ this.files.delete(path);
+ }
+ }
+}
+
+class InboundFileSystem {
+ static async create(dbName, root, FS_promise) {
+ const start = performance.now();
+ try {
+ const ifs = new InboundFileSystem();
+ ifs.pending = [];
+ ifs.pendingHasTimeout = false;
+ ifs.persistCount = 0;
+ ifs.persistSize = 0;
+ ifs.pendingI = 0;
+ ifs.inPersist = false;
+ ifs.totalCount = 0;
+
+ ifs.root = root;
+
+ //ifs.store = localforage.createInstance({
+ //name: dbName,
+ //driver: localForageDrivers
+ //});
+ log.debug('IDB name: ' + dbName);
+ ifs.idb = new Promise((resolve, reject) => {
+ const request = indexedDB.open(dbName);
+ request.onsuccess = () => resolve(request.result);
+ request.onerror = () => reject(request.error);
+ request.onupgradeneeded = () => {
+ request.result.createObjectStore('keyvaluepairs');
+ };
+ });
+
+ ifs.load_pids();
+
+ const FS = await FS_promise;
+ await ifs.idb;
+ ifs.FS = FS;
+ ifs.FS.mkdir(root);
+ const create_time = (performance.now() - start) / 1000;
+ log.debug('InboundFileSystem::create ' + create_time);
+ return ifs;
+ } catch (e) {
+ log.error('Failed to init Inbound FileSystem: ', e);
+ }
+ }
+
+ async load_pids() {
+ const keys_start = performance.now();
+ log.debug('InboundFileSystem::create::keys start');
+ //const keys = await this.store.keys();
+
+ let idb = await this.idb;
+ let read = idb
+ .transaction(['keyvaluepairs'], 'readonly')
+ .objectStore('keyvaluepairs');
+ const keys = await new Promise((resolve, reject) => {
+ const request = read.getAllKeys();
+ request.onsuccess = () => resolve(request.result);
+ request.onerror = () => reject(request.error);
+ });
+
+ const keys_time = (performance.now() - keys_start) / 1000;
+ log.debug(
+ 'InboundFileSystem::create::keys ' + keys_time + ' ' + keys.length
+ );
+ this.pids = new Set(keys);
+ }
+
+ has(pid) {
+ if (!this.pids) {
+ return true;
+ }
+
+ return this.pids.has(pid);
+ }
+
+ forget(pid) {
+ if (this.pids) {
+ this.pids.delete(pid);
+ }
+ }
+
+ async doPersist(pid, path, arr, resolve, reject, write) {
+ this.persistCount++;
+ let size = arr.length;
+ this.persistSize += size;
+ try {
+ //log.debug('persist.do start', pid, path, arr.length);
+ //await this.store.setItem(pid, new Blob([arr]));
+ await new Promise((resolve, reject) => {
+ const request = write.put(new Blob([arr]), pid);
+ request.onsuccess = () => resolve(request.result);
+ request.onerror = () => reject(request.error);
+ });
+ if (this.pids) {
+ this.pids.add(pid);
+ }
+ this.FS.unlink(path);
+ } catch (e) {
+ log.error('Failed persist ' + path + ' ', e);
+ }
+ //log.debug('persist.do finish', pid, path, arr.length);
+ this.persistCount--;
+ this.persistSize -= size;
+ resolve();
+
+ this.tryFinishPersist();
+ }
+
+ async flushPersist() {
+ if (this.inPersist) {
+ return;
+ }
+ log.debug('persist.flush');
+ this.inPersist = true;
+ let idb = await this.idb;
+ this.writeBegin = performance.now();
+ let write = idb
+ .transaction(['keyvaluepairs'], 'readwrite')
+ .objectStore('keyvaluepairs');
+ while (
+ this.pendingI < this.pending.length &&
+ this.persistCount < 20 &&
+ this.persistSize < 50 << 20
+ ) {
+ var q = this.pending[this.pendingI];
+ this.pending[this.pendingI] = null;
+ // TODO: add to transaction
+ this.doPersist(q.pid, q.path, q.arr, q.resolve, q.reject, write);
+ this.pendingI++;
+ this.totalCount++;
+ }
+ log.debug(
+ 'persist.flush transaction cnt=' +
+ this.persistCount +
+ ', size=' +
+ this.persistSize
+ );
+ this.inPersist = false;
+ this.tryFinishPersist();
+ }
+
+ async tryFinishPersist() {
+ if (this.inPersist) {
+ return;
+ }
+ if (this.persistCount !== 0) {
+ return;
+ }
+ log.debug('persist.finish ' + (performance.now() - this.writeBegin) / 1000);
+ if (this.pendingI === this.pending.length) {
+ this.pending = [];
+ this.pendingHasTimeout = false;
+ this.pendingI = 0;
+ log.debug('persist.finish done');
+ return;
+ }
+ log.debug('persist.finish continue');
+ this.flushPersist();
+ }
+
+ async persist(pid, path, arr) {
+ if (!this.pendingHasTimeout) {
+ this.pendingHasTimeout = true;
+ log.debug('persist set timeout');
+ setTimeout(() => {
+ this.flushPersist();
+ }, 1);
+ }
+ await new Promise((resolve, reject) => {
+ this.pending.push({
+ pid: pid,
+ path: path,
+ arr: arr,
+ resolve: resolve,
+ reject: reject
+ });
+ });
+ }
+
+ async unlink(pid) {
+ log.debug('Unlink ' + pid);
+ try {
+ this.forget(pid);
+ //await this.store.removeItem(pid);
+ let idb = await this.idb;
+ await new Promise((resolve, reject) => {
+ let write = idb
+ .transaction(['keyvaluepairs'], 'readwrite')
+ .objectStore('keyvaluepairs');
+ const request = write.delete(pid);
+ request.onsuccess = () => resolve(request.result);
+ request.onerror = () => reject(request.error);
+ });
+ } catch (e) {
+ log.error('Failed unlink ' + pid + ' ', e);
+ }
+ }
+}
+
+class DbFileSystem {
+ static async create(root, FS_promise, readOnly = false) {
+ const start = performance.now();
+ try {
+ const dbfs = new DbFileSystem();
+ dbfs.root = root;
+ const FS = await FS_promise;
+ dbfs.FS = FS;
+ dbfs.syncfs_total_time = 0;
+ dbfs.readOnly = readOnly;
+ dbfs.syncActive = 0;
+ FS.mkdir(root);
+ FS.mount(FS.filesystems.IDBFS, {}, root);
+
+ await new Promise((resolve, reject) => {
+ FS.syncfs(true, err => {
+ resolve();
+ });
+ });
+
+ const rmrf = path => {
+ log.debug('rmrf ', path);
+ let info;
+ try {
+ info = FS.lookupPath(path);
+ } catch (e) {
+ return;
+ }
+ log.debug('rmrf ', path, info);
+ if (info.node.isFolder) {
+ for (const key in info.node.contents) {
+ rmrf(info.path + '/' + info.node.contents[key].name);
+ }
+ log.debug('rmdir ', path);
+ FS.rmdir(path);
+ } else {
+ log.debug('unlink ', path);
+ FS.unlink(path);
+ }
+ };
+ //const dirs = ['thumbnails', 'profile_photos', 'secret', 'stickers', 'temp', 'wallpapers', 'secret_thumbnails', 'passport'];
+ const dirs = [];
+ const root_dir = FS.lookupPath(root);
+ for (const key in root_dir.node.contents) {
+ const value = root_dir.node.contents[key];
+ log.debug('node ', key, value);
+ if (!value.isFolder) {
+ continue;
+ }
+ dirs.push(root_dir.path + '/' + value.name);
+ }
+ for (const i in dirs) {
+ const dir = dirs[i];
+ rmrf(dir);
+ //FS.mkdir(dir);
+ //FS.mount(FS.filesystems.MEMFS, {}, dir);
+ }
+ dbfs.syncfsInterval = setInterval(() => {
+ dbfs.sync();
+ }, 5000);
+ const create_time = (performance.now() - start) / 1000;
+ log.debug('DbFileSystem::create ' + create_time);
+ return dbfs;
+ } catch (e) {
+ log.error('Failed to init DbFileSystem: ', e);
+ }
+ }
+ async sync(force) {
+ if (this.readOnly) {
+ return;
+ }
+ if (this.syncActive > 0 && !force) {
+ log.debug('SYNC: skip');
+ return;
+ }
+ this.syncActive++;
+ const start = performance.now();
+ await new Promise((resolve, reject) => {
+ this.FS.syncfs(false, () => {
+ const syncfs_time = (performance.now() - start) / 1000;
+ this.syncfs_total_time += syncfs_time;
+ log.debug('SYNC: ' + syncfs_time);
+ log.debug('SYNC total: ' + this.syncfs_total_time);
+ resolve();
+ });
+ });
+ this.syncActive--;
+ }
+ async close() {
+ clearInterval(this.syncfsInterval);
+ await this.sync(true);
+ }
+ async destroy() {
+ clearInterval(this.syncfsInterval);
+ if (this.readOnly) {
+ return;
+ }
+ this.FS.unmount(this.root);
+ const req = indexedDB.deleteDatabase(this.root);
+ await new Promise((resolve, reject) => {
+ req.onsuccess = function(e) {
+ log.info('SUCCESS');
+ resolve(e.result);
+ };
+ req.onerror = function(e) {
+ log.info('ONERROR');
+ reject(e.error);
+ };
+ req.onblocked = function(e) {
+ log.info('ONBLOCKED');
+ reject('blocked');
+ };
+ });
+ }
+}
+
+class TdFileSystem {
+ static async init_fs(prefix, FS_promise) {
+ const FS = await FS_promise;
+ FS.mkdir(prefix);
+ return FS;
+ }
+ static async create(instanceName, FS_promise, readOnly = false) {
+ try {
+ const tdfs = new TdFileSystem();
+ const prefix = '/' + instanceName;
+ tdfs.prefix = prefix;
+ FS_promise = TdFileSystem.init_fs(prefix, FS_promise);
+
+ //MEMFS. Store to IDB and delete files as soon as possible
+ const inboundFileSystem = InboundFileSystem.create(
+ instanceName,
+ prefix + '/inboundfs',
+ FS_promise
+ );
+
+ //IDBFS. MEMFS which is flushed to IDB from time to time
+ const dbFileSystem = DbFileSystem.create(
+ prefix + '/dbfs',
+ FS_promise,
+ readOnly
+ );
+
+ const FS = await FS_promise;
+ tdfs.FS = FS;
+
+ //WORKERFS. Temporary stores Blobs for outbound files
+ tdfs.outboundFileSystem = new OutboundFileSystem(
+ prefix + '/outboundfs',
+ tdfs.FS
+ );
+
+ tdfs.inboundFileSystem = await inboundFileSystem;
+ tdfs.dbFileSystem = await dbFileSystem;
+ return tdfs;
+ } catch (e) {
+ log.error('Failed to init TdFileSystem: ', e);
+ }
+ }
+ async destroy() {
+ await this.dbFileSystem.destroy();
+ }
+}
+
+class TdClient {
+ constructor(callback) {
+ log.info('Start worker');
+ this.pendingQueries = [];
+ this.isPending = true;
+ this.callback = callback;
+ this.wasInit = false;
+ }
+
+ async testLocalForage() {
+ await initLocalForage();
+ const DRIVERS = [
+ localforage.INDEXEDDB,
+ 'memoryDriver',
+ localforage.LOCALSTORAGE,
+ localforage.WEBSQL,
+ localForageDrivers
+ ];
+ for (const driverName of DRIVERS) {
+ console.log('Test ', driverName);
+ try {
+ await localforage.setDriver(driverName);
+ console.log('A');
+ await localforage.setItem('hello', 'world');
+ console.log('B');
+ const x = await localforage.getItem('hello');
+ console.log('got ', x);
+ await localforage.clear();
+ console.log('C');
+ } catch (error) {
+ console.log('Error', error);
+ }
+ }
+ }
+
+ async init(options) {
+ if (this.wasInit) {
+ return;
+ }
+ //await this.testLocalForage();
+ log.setVerbosity(options.jsLogVerbosityLevel);
+ this.wasInit = true;
+
+ options = options || {};
+ const mode = options.mode || 'wasm';
+
+ const FS_promise = new Promise(resolve => {
+ this.onFS = resolve;
+ });
+
+ const tdfs_promise = TdFileSystem.create(
+ options.instanceName,
+ FS_promise,
+ options.readOnly
+ );
+
+ this.useDatabase = true;
+ if ('useDatabase' in options) {
+ this.useDatabase = options.useDatabase;
+ }
+
+ log.info('load TdModule');
+ this.TdModule = await loadTdlib(mode, this.onFS, options.wasmUrl);
+ log.info('got TdModule');
+ this.td_functions = {
+ td_create: this.TdModule.cwrap(
+ 'td_emscripten_create_client_id',
+ 'number',
+ []
+ ),
+ td_send: this.TdModule.cwrap('td_emscripten_send', null, [
+ 'number',
+ 'string'
+ ]),
+ td_execute: this.TdModule.cwrap('td_emscripten_execute', 'string', [
+ 'string'
+ ]),
+ td_receive: this.TdModule.cwrap('td_emscripten_receive', 'string', []),
+ td_set_verbosity: verbosity => {
+ this.td_functions.td_execute(
+ JSON.stringify({
+ '@type': 'setLogVerbosityLevel',
+ new_verbosity_level: verbosity
+ })
+ );
+ },
+ td_get_timeout: this.TdModule.cwrap(
+ 'td_emscripten_get_timeout',
+ 'number',
+ []
+ )
+ };
+ //this.onFS(this.TdModule.FS);
+ this.FS = this.TdModule.FS;
+ this.TdModule['websocket']['on']('error', error => {
+ this.scheduleReceiveSoon();
+ });
+ this.TdModule['websocket']['on']('open', fd => {
+ this.scheduleReceiveSoon();
+ });
+ this.TdModule['websocket']['on']('listen', fd => {
+ this.scheduleReceiveSoon();
+ });
+ this.TdModule['websocket']['on']('connection', fd => {
+ this.scheduleReceiveSoon();
+ });
+ this.TdModule['websocket']['on']('message', fd => {
+ this.scheduleReceiveSoon();
+ });
+ this.TdModule['websocket']['on']('close', fd => {
+ this.scheduleReceiveSoon();
+ });
+
+ // wait till it is allowed to start
+ this.callback({ '@type': 'inited' });
+ await new Promise(resolve => {
+ this.onStart = resolve;
+ });
+ this.isStarted = true;
+
+ log.info('may start now');
+ if (this.isClosing) {
+ return;
+ }
+ log.info('FS start init');
+ this.tdfs = await tdfs_promise;
+ log.info('FS inited');
+ this.callback({ '@type': 'fsInited' });
+
+ // no async initialization after this point
+ if (options.logVerbosityLevel === undefined) {
+ options.logVerbosityLevel = 2;
+ }
+ this.td_functions.td_set_verbosity(options.logVerbosityLevel);
+ this.client_id = this.td_functions.td_create();
+
+ this.savingFiles = new Map();
+ this.send({
+ '@type': 'setOption',
+ name: 'store_all_files_in_files_directory',
+ value: {
+ '@type': 'optionValueBoolean',
+ value: true
+ }
+ });
+ this.send({
+ '@type': 'setOption',
+ name: 'language_pack_database_path',
+ value: {
+ '@type': 'optionValueString',
+ value: this.tdfs.dbFileSystem.root + '/language'
+ }
+ });
+ this.send({
+ '@type': 'setOption',
+ name: 'ignore_background_updates',
+ value: {
+ '@type': 'optionValueBoolean',
+ value: !this.useDatabase
+ }
+ });
+
+ this.flushPendingQueries();
+
+ this.receive();
+ }
+
+ prepareQueryRecursive(query) {
+ if (query['@type'] === 'inputFileBlob') {
+ return {
+ '@type': 'inputFileLocal',
+ path: this.tdfs.outboundFileSystem.blobToPath(query.data, query.name)
+ };
+ }
+ for (const key in query) {
+ const field = query[key];
+ if (field && typeof field === 'object') {
+ query[key] = this.prepareQueryRecursive(field);
+ }
+ }
+ return query;
+ }
+
+ prepareQuery(query) {
+ if (query['@type'] === 'setTdlibParameters') {
+ query.database_directory = this.tdfs.dbFileSystem.root;
+ query.files_directory = this.tdfs.inboundFileSystem.root;
+
+ const useDb = this.useDatabase;
+ query.use_file_database = useDb;
+ query.use_chat_info_database = useDb;
+ query.use_message_database = useDb;
+ query.use_secret_chats = useDb;
+ }
+ if (query['@type'] === 'getLanguagePackString') {
+ query.language_pack_database_path =
+ this.tdfs.dbFileSystem.root + '/language';
+ }
+ return this.prepareQueryRecursive(query);
+ }
+
+ onStart() {
+ //nop
+ log.info('ignore on_start');
+ }
+
+ deleteIdbKey(query) {
+ try {
+ } catch (e) {
+ this.callback({
+ '@type': 'error',
+ '@extra': query['@extra'],
+ code: 400,
+ message: e
+ });
+ return;
+ }
+ this.callback({
+ '@type': 'ok',
+ '@extra': query['@extra']
+ });
+ }
+
+ readFilePart(query) {
+ let res;
+ try {
+ //const file_size = this.FS.stat(query.path).size;
+ const stream = this.FS.open(query.path, 'r');
+ const buf = new Uint8Array(query.count);
+ this.FS.read(stream, buf, 0, query.count, query.offset);
+ this.FS.close(stream);
+ res = buf;
+ } catch (e) {
+ this.callback({
+ '@type': 'error',
+ '@extra': query['@extra'],
+ code: 400,
+ message: e.toString()
+ });
+ return;
+ }
+ this.callback(
+ {
+ '@type': 'filePart',
+ '@extra': query['@extra'],
+ data: res
+ },
+ [res.buffer]
+ );
+ }
+
+ send(query) {
+ if (this.isClosing) {
+ return;
+ }
+ if (this.wasFatalError) {
+ if (query['@type'] === 'destroy') {
+ this.destroy({ '@type': 'ok', '@extra': query['@extra'] });
+ }
+ return;
+ }
+ if (query['@type'] === 'init') {
+ this.init(query.options);
+ return;
+ }
+ if (query['@type'] === 'start') {
+ log.info('on_start');
+ this.onStart();
+ return;
+ }
+ if (query['@type'] === 'setJsLogVerbosityLevel') {
+ log.setVerbosity(query.new_verbosity_level);
+ return;
+ }
+ if (this.isPending) {
+ this.pendingQueries.push(query);
+ return;
+ }
+ if (
+ query['@type'] === 'setLogVerbosityLevel' ||
+ query['@type'] === 'getLogVerbosityLevel' ||
+ query['@type'] === 'setLogTagVerbosityLevel' ||
+ query['@type'] === 'getLogTagVerbosityLevel' ||
+ query['@type'] === 'getLogTags'
+ ) {
+ this.execute(query);
+ return;
+ }
+ if (query['@type'] === 'readFilePart') {
+ this.readFilePart(query);
+ return;
+ }
+ if (query['@type'] === 'deleteIdbKey') {
+ this.deleteIdbKey(query);
+ return;
+ }
+ query = this.prepareQuery(query);
+ this.td_functions.td_send(this.client_id, JSON.stringify(query));
+ this.scheduleReceiveSoon();
+ }
+
+ execute(query) {
+ try {
+ const res = this.td_functions.td_execute(JSON.stringify(query));
+ const response = JSON.parse(res);
+ this.callback(response);
+ } catch (error) {
+ this.onFatalError(error);
+ }
+ }
+ receive() {
+ this.cancelReceive();
+ if (this.wasFatalError) {
+ return;
+ }
+ try {
+ while (true) {
+ const msg = this.td_functions.td_receive();
+ if (!msg) {
+ break;
+ }
+ const response = this.prepareResponse(JSON.parse(msg));
+ if (
+ response['@type'] === 'updateAuthorizationState' &&
+ response.authorization_state['@type'] === 'authorizationStateClosed'
+ ) {
+ this.close(response);
+ break;
+ }
+ this.callback(response);
+ }
+
+ this.scheduleReceive();
+ } catch (error) {
+ this.onFatalError(error);
+ }
+ }
+
+ cancelReceive() {
+ if (this.receiveTimeout) {
+ clearTimeout(this.receiveTimeout);
+ delete this.receiveTimeout;
+ }
+ delete this.receiveSoon;
+ }
+ scheduleReceiveSoon() {
+ if (this.receiveSoon) {
+ return;
+ }
+ this.cancelReceive();
+ this.receiveSoon = true;
+ this.scheduleReceiveIn(0.001);
+ }
+ scheduleReceive() {
+ if (this.receiveSoon) {
+ return;
+ }
+ this.cancelReceive();
+ const timeout = this.td_functions.td_get_timeout();
+ this.scheduleReceiveIn(timeout);
+ }
+ scheduleReceiveIn(timeout) {
+ //return;
+ log.debug('Scheduler receive in ' + timeout + 's');
+ this.receiveTimeout = setTimeout(() => this.receive(), timeout * 1000);
+ }
+
+ onFatalError(error) {
+ this.wasFatalError = true;
+ this.asyncOnFatalError(error);
+ }
+
+ async close(last_update) {
+ // close db and cancell all timers
+ this.isClosing = true;
+ if (this.isStarted) {
+ log.debug('close worker: start');
+ await this.tdfs.dbFileSystem.close();
+ this.cancelReceive();
+ log.debug('close worker: finish');
+ }
+ this.callback(last_update);
+ }
+
+ async destroy(result) {
+ try {
+ log.info('destroy tdfs ...');
+ await this.tdfs.destroy();
+ log.info('destroy tdfs ok');
+ } catch (e) {
+ log.error('Failed destroy', e);
+ }
+ this.callback(result);
+ this.callback({
+ '@type': 'updateAuthorizationState',
+ authorization_state: {
+ '@type': 'authorizationStateClosed'
+ }
+ });
+ }
+
+ async asyncOnFatalError(error) {
+ await this.tdfs.dbFileSystem.sync();
+ this.callback({ '@type': 'updateFatalError', error: error });
+ }
+
+ saveFile(pid, file) {
+ const isSaving = this.savingFiles.has(pid);
+ this.savingFiles.set(pid, file);
+ if (isSaving) {
+ return file;
+ }
+ try {
+ const arr = this.FS.readFile(file.local.path);
+ if (arr) {
+ file = Object.assign({}, file);
+ file.arr = arr;
+ this.doSaveFile(pid, file, arr);
+ }
+ } catch (e) {
+ log.error('Failed to readFile: ', e);
+ }
+ return file;
+ }
+
+ async doSaveFile(pid, file, arr) {
+ await this.tdfs.inboundFileSystem.persist(pid, file.local.path, arr);
+ file = this.savingFiles.get(pid);
+ file.idb_key = pid;
+ this.callback({ '@type': 'updateFile', file: file });
+
+ this.savingFiles.delete(pid);
+ }
+
+ prepareFile(file) {
+ const pid = file.remote.unique_id ? file.remote.unique_id : file.remote.id;
+ if (!pid) {
+ return file;
+ }
+
+ if (file.local.is_downloading_active) {
+ this.tdfs.inboundFileSystem.forget(pid);
+ } else if (this.tdfs.inboundFileSystem.has(pid)) {
+ file.idb_key = pid;
+ return file;
+ }
+
+ if (file.local.is_downloading_completed) {
+ file = this.saveFile(pid, file);
+ }
+ return file;
+ }
+
+ prepareResponse(response) {
+ if (response['@type'] === 'file') {
+ return this.prepareFile(response);
+ }
+ for (const key in response) {
+ const field = response[key];
+ if (field && typeof field === 'object') {
+ response[key] = this.prepareResponse(field);
+ }
+ }
+ return response;
+ }
+
+ flushPendingQueries() {
+ this.isPending = false;
+ for (const query of this.pendingQueries) {
+ this.send(query);
+ }
+ }
+}
+
+const client = new TdClient((e, t = []) => postMessage(e, t));
+
+onmessage = function(e) {
+ try {
+ client.send(e.data);
+ } catch (error) {
+ client.onFatalError(error);
+ }
+};