diff options
Diffstat (limited to 'protocols/Telegram/tdlib/td/example/web/tdweb/src')
4 files changed, 0 insertions, 1897 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 deleted file mode 100644 index 2f159b9424..0000000000 --- a/protocols/Telegram/tdlib/td/example/web/tdweb/src/index.js +++ /dev/null @@ -1,680 +0,0 @@ -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 deleted file mode 100644 index 95baed0318..0000000000 --- a/protocols/Telegram/tdlib/td/example/web/tdweb/src/logger.js +++ /dev/null @@ -1,47 +0,0 @@ -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 deleted file mode 100644 index 50447d65b3..0000000000 --- a/protocols/Telegram/tdlib/td/example/web/tdweb/src/wasm-utils.js +++ /dev/null @@ -1,136 +0,0 @@ -// 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 deleted file mode 100644 index dff1845126..0000000000 --- a/protocols/Telegram/tdlib/td/example/web/tdweb/src/worker.js +++ /dev/null @@ -1,1034 +0,0 @@ -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); - } -}; |