Add an emscripten example (tdweb)
GitOrigin-RevId: a05f88d5899de77fb15a0b9d476f09c7a388dc74
This commit is contained in:
parent
873e15efed
commit
df1c8d8297
21
example/emscripten/build-openssl.sh
Executable file
21
example/emscripten/build-openssl.sh
Executable file
@ -0,0 +1,21 @@
|
||||
#!/bin/sh
|
||||
|
||||
OPENSSL=OpenSSL_1_1_0j
|
||||
if [ ! -f $OPENSSL.tar.gz ]; then
|
||||
echo "Download openssl"
|
||||
wget https://github.com/openssl/openssl/archive/$OPENSSL.tar.gz
|
||||
fi
|
||||
rm -rf ./$OPENSSL
|
||||
tar xzf $OPENSSL.tar.gz || exit 1
|
||||
cd openssl-$OPENSSL
|
||||
|
||||
emconfigure ./Configure linux-generic32 no-shared
|
||||
sed -i bak 's/CROSS_COMPILE=.*/CROSS_COMPILE=/g' Makefile
|
||||
emmake make depend -s || exit 1
|
||||
emmake make -s || exit 1
|
||||
|
||||
rm -rf ../build/crypto || exit 1
|
||||
mkdir -p ../build/crypto/lib || exit 1
|
||||
cp libcrypto.a libssl.a ../build/crypto/lib/ || exit 1
|
||||
cp -r include ../build/crypto/ || exit 1
|
||||
cd ..
|
37
example/emscripten/build-tdlib.sh
Executable file
37
example/emscripten/build-tdlib.sh
Executable file
@ -0,0 +1,37 @@
|
||||
mkdir -p build/generate
|
||||
mkdir -p build/asmjs
|
||||
mkdir -p build/wasm
|
||||
|
||||
TD_ROOT=$(realpath ../../)
|
||||
#OPENSSL_OPTIONS="-DOPENSSL_ROOT=$TD_ROOT/third_party/crypto/emscripten"
|
||||
OPENSSL_ROOT=$(realpath ./build/crypto/)
|
||||
OPENSSL_CRYPTO_LIBRARY=$OPENSSL_ROOT/lib/libcrypto.a
|
||||
OPENSSL_SSL_LIBRARY=$OPENSSL_ROOT/lib/libssl.a
|
||||
|
||||
OPENSSL_OPTIONS="-DOPENSSL_FOUND=1 \
|
||||
-DOPENSSL_ROOT_DIR=\"$OPENSSL_ROOT\" \
|
||||
-DOPENSSL_INCLUDE_DIR=\"$OPENSSL_ROOT/include\" \
|
||||
-DOPENSSL_CRYPTO_LIBRARY=\"$OPENSSL_CRYPTO_LIBRARY\" \
|
||||
-DOPENSSL_SSL_LIBRARY=\"$OPENSSL_SSL_LIBRARY\" \
|
||||
-DOPENSSL_LIBRARIES=\"$OPENSSL_SSL_LIBRARY;$OPENSSL_CRYPTO_LIBRARY\" \
|
||||
-DOPENSSL_VERSION=\"1.1.0j\""
|
||||
|
||||
pushd .
|
||||
cd build/wasm
|
||||
eval emconfigure cmake $TD_ROOT -GNinja $OPENSSL_OPTIONS
|
||||
popd
|
||||
|
||||
pushd .
|
||||
cd build/asmjs
|
||||
eval emconfigure cmake $TD_ROOT -GNinja -DASMJS=1 $OPENSSL_OPTIONS
|
||||
popd
|
||||
|
||||
pushd .
|
||||
cd build/generate
|
||||
cmake $TD_ROOT -GNinja
|
||||
popd
|
||||
|
||||
cmake --build build/generate -j --target prepare_cross_compiling
|
||||
cmake --build build/wasm -j --target td_wasm
|
||||
cmake --build build/asmjs -j --target td_asmjs
|
||||
|
4
example/emscripten/build-tdweb.sh
Executable file
4
example/emscripten/build-tdweb.sh
Executable file
@ -0,0 +1,4 @@
|
||||
pushd .
|
||||
cd tdweb
|
||||
npm install
|
||||
npm run build
|
4
example/emscripten/copy-tdlib.sh
Executable file
4
example/emscripten/copy-tdlib.sh
Executable file
@ -0,0 +1,4 @@
|
||||
dest=tdweb/src/prebuilt/release/
|
||||
mkdir -p $dest
|
||||
cp build/wasm/td_wasm.{js,wasm} $dest
|
||||
cp build/asmjs/td_asmjs.js{,.mem} $dest
|
86
example/emscripten/tdweb/package.json
Normal file
86
example/emscripten/tdweb/package.json
Normal file
@ -0,0 +1,86 @@
|
||||
{
|
||||
"name": "@arseny30/tdweb",
|
||||
"version": "0.2.23",
|
||||
"description": "Javascript interface for TDLib (telegram library)",
|
||||
"main": "dist/tdweb.js",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"precommit": "lint-staged",
|
||||
"build": "node --max_old_space_size=8192 node_modules/.bin/webpack ",
|
||||
"start": "webpack-dev-server --open"
|
||||
},
|
||||
"keywords": [
|
||||
"telegram"
|
||||
],
|
||||
"author": "Arseny Smirnov",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-eslint": "^7.2.3",
|
||||
"babel-loader": "^7.1.5",
|
||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"clean-webpack-plugin": "^0.1.19",
|
||||
"eslint": "^4.19.1",
|
||||
"eslint-config-react-app": "^2.1.0",
|
||||
"eslint-loader": "^1.9.0",
|
||||
"eslint-plugin-flowtype": "^2.50.1",
|
||||
"eslint-plugin-import": "^2.14.0",
|
||||
"eslint-plugin-jsx-a11y": "^5.1.1",
|
||||
"eslint-plugin-react": "^7.11.1",
|
||||
"exports-loader": "^0.6.4",
|
||||
"file-loader": "^1.1.11",
|
||||
"html-webpack-plugin": "^2.30.1",
|
||||
"husky": "^0.14.3",
|
||||
"lint-staged": "^4.3.0",
|
||||
"prettier": "^1.14.2",
|
||||
"script-loader": "^0.7.2",
|
||||
"uglifyjs-webpack-plugin": "^1.3.0",
|
||||
"webpack": "^3.12.0",
|
||||
"webpack-dev-server": "^2.11.3",
|
||||
"worker-loader": "^1.1.1"
|
||||
},
|
||||
"lint-staged": {
|
||||
"webpack.config.json": [
|
||||
"prettier --single-quote --write",
|
||||
"git add"
|
||||
],
|
||||
"package.json": [
|
||||
"prettier --single-quote --write",
|
||||
"git add"
|
||||
],
|
||||
"src/*.{js,jsx,json,css}": [
|
||||
"prettier --single-quote --write",
|
||||
"git add"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-runtime": "^6.26.0",
|
||||
"detect-browser": "^2.5.1",
|
||||
"localforage": "^1.7.2",
|
||||
"uuid": "^3.3.2"
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
"env"
|
||||
],
|
||||
"plugins": [
|
||||
"syntax-dynamic-import",
|
||||
"transform-runtime"
|
||||
]
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "eslint-config-react-app",
|
||||
"env": {
|
||||
"worker": true,
|
||||
"node": true,
|
||||
"browser": true
|
||||
},
|
||||
"globals": {
|
||||
"WebAssembly": true
|
||||
}
|
||||
}
|
||||
}
|
212
example/emscripten/tdweb/src/index.js
Normal file
212
example/emscripten/tdweb/src/index.js
Normal file
@ -0,0 +1,212 @@
|
||||
import MyWorker from './worker.js';
|
||||
import './third_party/broadcastchannel.js';
|
||||
import uuid4 from 'uuid/v4';
|
||||
import log from './logger.js';
|
||||
|
||||
const sleep = ms => new Promise(res => setTimeout(res, ms));
|
||||
|
||||
class TdClient {
|
||||
constructor(options) {
|
||||
log.setVerbosity(options.jsVerbosity);
|
||||
this.worker = new MyWorker();
|
||||
var self = this;
|
||||
this.worker.onmessage = function(e) {
|
||||
let response = e.data;
|
||||
log.info(
|
||||
'receive from worker: ',
|
||||
JSON.parse(
|
||||
JSON.stringify(response, (key, value) => {
|
||||
if (key === 'arr') {
|
||||
return undefined;
|
||||
}
|
||||
return value;
|
||||
})
|
||||
)
|
||||
);
|
||||
if ('@extra' in response) {
|
||||
var query_id = response['@extra'].query_id;
|
||||
var [resolve, reject] = self.query_callbacks.get(query_id);
|
||||
self.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') {
|
||||
self.onInited();
|
||||
return;
|
||||
}
|
||||
if (
|
||||
response['@type'] === 'updateAuthorizationState' &&
|
||||
response.authorization_state['@type'] === 'authorizationStateClosed'
|
||||
) {
|
||||
self.onClosed();
|
||||
}
|
||||
self.onUpdate(response);
|
||||
}
|
||||
};
|
||||
this.query_id = 0;
|
||||
this.query_callbacks = new Map();
|
||||
this.worker.postMessage({ '@type': 'init', options: options });
|
||||
this.closeOtherClients(options);
|
||||
}
|
||||
|
||||
onBroadcastMessage(e) {
|
||||
var message = e.data;
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
postState() {
|
||||
let state = {
|
||||
id: this.uid,
|
||||
state: this.state,
|
||||
timestamp: this.timestamp,
|
||||
isBackground: this.isBackground
|
||||
};
|
||||
log.info('Post state: ', state);
|
||||
this.channel.postMessage(state);
|
||||
}
|
||||
|
||||
onWaitSetEmpty() {
|
||||
// nop
|
||||
}
|
||||
|
||||
onInited() {
|
||||
this.isInited = true;
|
||||
this.doSendStart();
|
||||
}
|
||||
sendStart() {
|
||||
this.wantSendStart = true;
|
||||
this.doSendStart();
|
||||
}
|
||||
|
||||
doSendStart() {
|
||||
if (!this.isInited || !this.wantSendStart || this.state !== 'start') {
|
||||
return;
|
||||
}
|
||||
this.wantSendStart = false;
|
||||
this.state = 'active';
|
||||
let query = { '@type': 'start' };
|
||||
log.info('send to worker: ', query);
|
||||
this.worker.postMessage(query);
|
||||
}
|
||||
|
||||
onClosed() {
|
||||
this.isClosing = true;
|
||||
this.worker.terminate();
|
||||
log.info('worker is terminated');
|
||||
this.state = 'closed';
|
||||
this.postState();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
let query = { '@type': 'close' };
|
||||
log.info('send to worker: ', query);
|
||||
this.worker.postMessage(query);
|
||||
|
||||
this.state = 'closing';
|
||||
this.postState();
|
||||
}
|
||||
|
||||
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');
|
||||
let prefix = options.prefix || 'tdlib';
|
||||
this.channel = new BroadcastChannel(prefix);
|
||||
|
||||
this.postState();
|
||||
|
||||
var self = this;
|
||||
this.channel.onmessage = message => {
|
||||
self.onBroadcastMessage(message);
|
||||
};
|
||||
|
||||
await sleep(300);
|
||||
if (this.waitSet.size !== 0) {
|
||||
await new Promise(resolve => {
|
||||
self.onWaitSetEmpty = resolve;
|
||||
});
|
||||
}
|
||||
this.sendStart();
|
||||
}
|
||||
|
||||
onUpdate(response) {
|
||||
log.info('ignore onUpdate');
|
||||
//nop
|
||||
}
|
||||
|
||||
send(query) {
|
||||
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'] === 'setJsVerbosity') {
|
||||
log.setVerbosity(query.verbosity);
|
||||
}
|
||||
|
||||
log.info('send to worker: ', query);
|
||||
this.worker.postMessage(query);
|
||||
return new Promise((resolve, reject) => {
|
||||
this.query_callbacks.set(this.query_id, [resolve, reject]);
|
||||
});
|
||||
}
|
||||
}
|
||||
export default TdClient;
|
47
example/emscripten/tdweb/src/logger.js
Normal file
47
example/emscripten/tdweb/src/logger.js
Normal file
@ -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, WARNINIG: 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;
|
83
example/emscripten/tdweb/src/third_party/broadcastchannel.js
vendored
Normal file
83
example/emscripten/tdweb/src/third_party/broadcastchannel.js
vendored
Normal file
@ -0,0 +1,83 @@
|
||||
// from https://gist.github.com/inexorabletash/52f437d1451d12145264
|
||||
(function(global) {
|
||||
var channels = [];
|
||||
|
||||
function BroadcastChannel(channel) {
|
||||
var $this = this;
|
||||
channel = String(channel);
|
||||
|
||||
var id = '$BroadcastChannel$' + channel + '$';
|
||||
|
||||
channels[id] = channels[id] || [];
|
||||
channels[id].push(this);
|
||||
|
||||
this._name = channel;
|
||||
this._id = id;
|
||||
this._closed = false;
|
||||
this._mc = new MessageChannel();
|
||||
this._mc.port1.start();
|
||||
this._mc.port2.start();
|
||||
|
||||
global.addEventListener('storage', function(e) {
|
||||
if (e.storageArea !== global.localStorage) return;
|
||||
if (e.newValue === null) return;
|
||||
if (e.key.substring(0, id.length) !== id) return;
|
||||
var data = JSON.parse(e.newValue);
|
||||
$this._mc.port2.postMessage(data);
|
||||
});
|
||||
}
|
||||
|
||||
BroadcastChannel.prototype = {
|
||||
// BroadcastChannel API
|
||||
get name() { return this._name; },
|
||||
postMessage: function(message) {
|
||||
var $this = this;
|
||||
if (this._closed) {
|
||||
var e = new Error();
|
||||
e.name = 'InvalidStateError';
|
||||
throw e;
|
||||
}
|
||||
var value = JSON.stringify(message);
|
||||
|
||||
// Broadcast to other contexts via storage events...
|
||||
var key = this._id + String(Date.now()) + '$' + String(Math.random());
|
||||
global.localStorage.setItem(key, value);
|
||||
setTimeout(function() { global.localStorage.removeItem(key); }, 500);
|
||||
|
||||
// Broadcast to current context via ports
|
||||
channels[this._id].forEach(function(bc) {
|
||||
if (bc === $this) return;
|
||||
bc._mc.port2.postMessage(JSON.parse(value));
|
||||
});
|
||||
},
|
||||
close: function() {
|
||||
if (this._closed) return;
|
||||
this._closed = true;
|
||||
this._mc.port1.close();
|
||||
this._mc.port2.close();
|
||||
|
||||
var index = channels[this._id].indexOf(this);
|
||||
channels[this._id].splice(index, 1);
|
||||
},
|
||||
|
||||
// EventTarget API
|
||||
get onmessage() { return this._mc.port1.onmessage; },
|
||||
set onmessage(value) { this._mc.port1.onmessage = value; },
|
||||
addEventListener: function(type, listener /*, useCapture*/) {
|
||||
return this._mc.port1.addEventListener.apply(this._mc.port1, arguments);
|
||||
},
|
||||
removeEventListener: function(type, listener /*, useCapture*/) {
|
||||
return this._mc.port1.removeEventListener.apply(this._mc.port1, arguments);
|
||||
},
|
||||
dispatchEvent: function(event) {
|
||||
return this._mc.port1.dispatchEvent.apply(this._mc.port1, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
if (global.BroadcastChannel) {
|
||||
console.log("already has native BroadcastChannel");
|
||||
} else {
|
||||
global.BroadcastChannel = BroadcastChannel;
|
||||
console.log("use polyfill for BroadcastChannel");
|
||||
}
|
||||
}(window.top));
|
121
example/emscripten/tdweb/src/wasm-utils.js
Normal file
121
example/emscripten/tdweb/src/wasm-utils.js
Normal file
@ -0,0 +1,121 @@
|
||||
// 1. +++ fetchAndInstantiate() +++ //
|
||||
|
||||
// This library function fetches the wasm module at 'url', instantiates it with
|
||||
// the given 'importObject', and returns the instantiated object instance
|
||||
|
||||
export function instantiateStreaming(url, importObject) {
|
||||
return WebAssembly.instantiateStreaming(fetch(url), importObject).then(
|
||||
results => results.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);
|
||||
}
|
||||
);
|
||||
}
|
543
example/emscripten/tdweb/src/worker.js
Normal file
543
example/emscripten/tdweb/src/worker.js
Normal file
@ -0,0 +1,543 @@
|
||||
import localforage from 'localforage';
|
||||
import log from './logger.js';
|
||||
import {
|
||||
instantiateCachedURL,
|
||||
/*fetchAndInstantiate,*/ instantiateStreaming
|
||||
} from './wasm-utils.js';
|
||||
|
||||
import td_wasm_release from './prebuilt/release/td_wasm.wasm';
|
||||
|
||||
// Uncomment for asmjs support
|
||||
//import td_asmjs_mem_release from './prebuilt/release/td_asmjs.js.mem';
|
||||
|
||||
import { detect } from 'detect-browser';
|
||||
const browser = detect();
|
||||
const tdlibVersion = 5;
|
||||
|
||||
async function loadTdLibWasm(useStreaming) {
|
||||
let Module = await import('./prebuilt/release/td_wasm.js');
|
||||
log.info('got td_wasm.js');
|
||||
let td_wasm = td_wasm_release;
|
||||
let TdModule = new Promise((resolve, reject) =>
|
||||
Module({
|
||||
onRuntimeInitialized: () => {
|
||||
log.info('runtime intialized');
|
||||
},
|
||||
instantiateWasm: (imports, successCallback) => {
|
||||
log.info('start instantiateWasm');
|
||||
let next = instance => {
|
||||
log.info('finish instantiateWasm');
|
||||
successCallback(instance);
|
||||
};
|
||||
if (useStreaming) {
|
||||
instantiateStreaming(td_wasm, imports).then(next);
|
||||
} else {
|
||||
instantiateCachedURL(tdlibVersion, td_wasm, imports).then(next);
|
||||
}
|
||||
return {};
|
||||
},
|
||||
ENVIROMENT: 'WORKER'
|
||||
}).then(m => {
|
||||
delete m.then;
|
||||
resolve(m);
|
||||
})
|
||||
);
|
||||
|
||||
return TdModule;
|
||||
}
|
||||
|
||||
// Uncomment for asmjs support
|
||||
//async function loadTdLibAsmjs() {
|
||||
//let Module = await import('./prebuilt/release/td_asmjs.js');
|
||||
//console.log('got td_wasm.js');
|
||||
//let fromFile = 'td_asmjs.js.mem';
|
||||
//let toFile = td_asmjs_mem_release;
|
||||
//let TdModule = new Promise((resolve, reject) =>
|
||||
//Module({
|
||||
//onRuntimeInitialized: () => {
|
||||
//console.log('runtime intialized');
|
||||
//},
|
||||
//locateFile: name => {
|
||||
//if (name === fromFile) {
|
||||
//return toFile;
|
||||
//}
|
||||
//return name;
|
||||
//},
|
||||
//ENVIROMENT: 'WORKER'
|
||||
//}).then(m => {
|
||||
//delete m.then;
|
||||
//resolve(m);
|
||||
//})
|
||||
//);
|
||||
|
||||
//return TdModule;
|
||||
//}
|
||||
|
||||
async function loadTdLib(mode) {
|
||||
// Uncomment for asmjs support
|
||||
//if (mode === 'asmjs') {
|
||||
//return loadTdLibAsmjs();
|
||||
//}
|
||||
return loadTdLibWasm(mode !== 'wasm');
|
||||
}
|
||||
|
||||
class OutboundFileSystem {
|
||||
constructor(root, FS) {
|
||||
this.root = root;
|
||||
this.nextFileId = 0;
|
||||
this.FS = FS;
|
||||
this.files = new Set();
|
||||
FS.mkdir(root);
|
||||
}
|
||||
blobToPath(blob, name) {
|
||||
var 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
|
||||
);
|
||||
let 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) {
|
||||
try {
|
||||
let ifs = new InboundFileSystem();
|
||||
ifs.root = root;
|
||||
ifs.FS = FS;
|
||||
FS.mkdir(root);
|
||||
|
||||
ifs.store = localforage.createInstance({
|
||||
name: dbName
|
||||
});
|
||||
let keys = await ifs.store.keys();
|
||||
|
||||
ifs.pids = new Set(keys);
|
||||
return ifs;
|
||||
} catch (e) {
|
||||
log.error('Failed to init Inbound FileSystem: ', e);
|
||||
}
|
||||
}
|
||||
|
||||
has(pid) {
|
||||
return this.pids.has(pid);
|
||||
}
|
||||
|
||||
forget(pid) {
|
||||
this.pids.delete(pid);
|
||||
}
|
||||
|
||||
async persist(pid, path) {
|
||||
var arr;
|
||||
try {
|
||||
arr = this.FS.readFile(path);
|
||||
await this.store.setItem(pid, new Blob([arr]));
|
||||
this.pids.add(pid);
|
||||
this.FS.unlink(path);
|
||||
} catch (e) {
|
||||
log.error('Failed persist ' + path + ' ', e);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
}
|
||||
|
||||
class DbFileSystem {
|
||||
static async create(root, FS, readOnly = false) {
|
||||
try {
|
||||
let dbfs = new DbFileSystem();
|
||||
dbfs.root = root;
|
||||
dbfs.FS = FS;
|
||||
dbfs.syncfs_total_time = 0;
|
||||
dbfs.readOnly = readOnly;
|
||||
FS.mkdir(root);
|
||||
FS.mount(FS.filesystems.IDBFS, {}, root);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
FS.syncfs(true, err => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
dbfs.syncfsInterval = setInterval(() => {
|
||||
dbfs.sync();
|
||||
}, 5000);
|
||||
return dbfs;
|
||||
} catch (e) {
|
||||
log.error('Failed to init DbFileSystem: ', e);
|
||||
}
|
||||
}
|
||||
async sync() {
|
||||
if (this.readOnly) {
|
||||
return;
|
||||
}
|
||||
let start = performance.now();
|
||||
await new Promise((resolve, reject) => {
|
||||
this.FS.syncfs(false, () => {
|
||||
let 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();
|
||||
});
|
||||
});
|
||||
}
|
||||
async close() {
|
||||
clearInterval(this.syncfsInterval);
|
||||
await this.sync();
|
||||
}
|
||||
}
|
||||
|
||||
class TdFileSystem {
|
||||
static async create(prefix, FS, readOnly = false) {
|
||||
try {
|
||||
let tdfs = new TdFileSystem();
|
||||
tdfs.prefix = prefix;
|
||||
tdfs.FS = FS;
|
||||
FS.mkdir(prefix);
|
||||
|
||||
//WORKERFS. Temporary stores Blobs for outbound files
|
||||
tdfs.outboundFileSystem = new OutboundFileSystem(
|
||||
prefix + '/outboundfs',
|
||||
FS
|
||||
);
|
||||
|
||||
//MEMFS. Store to IDB and delete files as soon as possible
|
||||
let inboundFileSystem = InboundFileSystem.create(
|
||||
prefix,
|
||||
prefix + '/inboundfs',
|
||||
FS
|
||||
);
|
||||
|
||||
//IDBFS. MEMFS which is flushed to IDB from time to time
|
||||
let dbFileSystem = DbFileSystem.create(prefix + '/dbfs', FS, readOnly);
|
||||
|
||||
tdfs.inboundFileSystem = await inboundFileSystem;
|
||||
tdfs.dbFileSystem = await dbFileSystem;
|
||||
return tdfs;
|
||||
} catch (e) {
|
||||
log.error('Failed to init TdFileSystem: ', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TdClient {
|
||||
constructor(callback) {
|
||||
log.info('Start worker');
|
||||
this.pendingQueries = [];
|
||||
this.isPending = true;
|
||||
this.callback = callback;
|
||||
this.wasInit = false;
|
||||
}
|
||||
|
||||
async init(options) {
|
||||
if (this.wasInit) {
|
||||
return;
|
||||
}
|
||||
log.setVerbosity(options.jsVerbosity);
|
||||
this.wasInit = true;
|
||||
|
||||
options = options || {};
|
||||
let mode = 'wasm';
|
||||
if (browser && (browser.name === 'chrome' || browser.name === 'safari')) {
|
||||
mode = 'asmjs';
|
||||
}
|
||||
mode = options.mode || mode;
|
||||
|
||||
this.TdModule = await loadTdLib(mode);
|
||||
log.info('got TdModule');
|
||||
this.td_functions = {
|
||||
td_create: this.TdModule.cwrap('td_create', 'number', []),
|
||||
td_destroy: this.TdModule.cwrap('td_destroy', null, ['number']),
|
||||
td_send: this.TdModule.cwrap('td_send', null, ['number', 'string']),
|
||||
td_execute: this.TdModule.cwrap('td_execute', 'string', [
|
||||
'number',
|
||||
'string'
|
||||
]),
|
||||
td_receive: this.TdModule.cwrap('td_receive', 'string', ['number']),
|
||||
td_set_verbosity: verbosity => {
|
||||
this.td_functions.td_execute(
|
||||
0,
|
||||
JSON.stringify({
|
||||
'@type': 'setLogVerbosityLevel',
|
||||
new_verbosity_level: verbosity
|
||||
})
|
||||
);
|
||||
},
|
||||
td_get_timeout: this.TdModule.cwrap('td_get_timeout', 'number', [])
|
||||
};
|
||||
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' });
|
||||
var self = this;
|
||||
await new Promise(resolve => {
|
||||
self.onStart = resolve;
|
||||
});
|
||||
this.isStarted = true;
|
||||
|
||||
log.info('may start now');
|
||||
if (this.isClosing) {
|
||||
return;
|
||||
}
|
||||
let prefix = options.prefix || 'tdlib';
|
||||
log.info('FS start init');
|
||||
this.tdfs = await TdFileSystem.create(
|
||||
'/' + prefix,
|
||||
this.FS,
|
||||
options.readOnly
|
||||
);
|
||||
log.info('FS inited');
|
||||
|
||||
// no async initialization after this point
|
||||
if (options.verbosity === undefined) {
|
||||
options.verbosity = 5;
|
||||
}
|
||||
this.td_functions.td_set_verbosity(options.verbosity);
|
||||
this.client = this.td_functions.td_create();
|
||||
|
||||
this.savingFiles = new Map();
|
||||
this.flushPendingQueries();
|
||||
|
||||
this.receive();
|
||||
//setInterval(()=>this.receive(), 100);
|
||||
}
|
||||
|
||||
prepareQueryRecursive(query) {
|
||||
if (query['@type'] === 'inputFileBlob') {
|
||||
return {
|
||||
'@type': 'inputFileLocal',
|
||||
path: this.tdfs.outboundFileSystem.blobToPath(query.blob, query.name)
|
||||
};
|
||||
}
|
||||
for (var key in query) {
|
||||
let field = query[key];
|
||||
if (field && typeof field === 'object') {
|
||||
query[key] = this.prepareQueryRecursive(field);
|
||||
}
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
prepareQuery(query) {
|
||||
if (query['@type'] === 'setTdlibParameters') {
|
||||
query.parameters.database_directory = this.tdfs.dbFileSystem.root;
|
||||
query.parameters.files_directory = this.tdfs.inboundFileSystem.root;
|
||||
}
|
||||
return this.prepareQueryRecursive(query);
|
||||
}
|
||||
|
||||
onStart() {
|
||||
//nop
|
||||
log.info('ignore on_start');
|
||||
}
|
||||
|
||||
send(query) {
|
||||
if (this.wasFatalError || this.isClosing) {
|
||||
return;
|
||||
}
|
||||
if (query['@type'] === 'init') {
|
||||
this.init(query.options);
|
||||
return;
|
||||
}
|
||||
if (query['@type'] === 'start') {
|
||||
log.info('on_start');
|
||||
this.onStart();
|
||||
return;
|
||||
}
|
||||
if (query['@type'] === 'setJsVerbosity') {
|
||||
log.setVerbosity(query.verbosity);
|
||||
return;
|
||||
}
|
||||
if (query['@type'] === 'setVerbosity') {
|
||||
this.td_functions.td_set_verbosity(query.verbosity);
|
||||
return;
|
||||
}
|
||||
if (this.isPending) {
|
||||
this.pendingQueries.push(query);
|
||||
return;
|
||||
}
|
||||
query = this.prepareQuery(query);
|
||||
this.td_functions.td_send(this.client, JSON.stringify(query));
|
||||
this.scheduleReceiveSoon();
|
||||
}
|
||||
|
||||
receive() {
|
||||
this.cancelReceive();
|
||||
if (this.wasFatalError) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
while (true) {
|
||||
let msg = this.td_functions.td_receive(this.client);
|
||||
if (!msg) {
|
||||
break;
|
||||
}
|
||||
let 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();
|
||||
let 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 asyncOnFatalError(error) {
|
||||
await this.tdfs.dbFileSystem.sync();
|
||||
this.callback({ '@type': 'updateFatalError', error: error });
|
||||
}
|
||||
|
||||
async saveFile(pid, file) {
|
||||
let isSaving = this.savingFiles.has(pid);
|
||||
this.savingFiles.set(pid, file);
|
||||
if (isSaving) {
|
||||
return;
|
||||
}
|
||||
let arr = await this.tdfs.inboundFileSystem.persist(pid, file.local.path);
|
||||
file = this.savingFiles.get(pid);
|
||||
file.idb_key = pid;
|
||||
if (arr) {
|
||||
file.arr = arr;
|
||||
}
|
||||
this.callback({ '@type': 'updateFile', file: file }, [arr.buffer]);
|
||||
delete file.arr;
|
||||
this.savingFiles.delete(pid);
|
||||
}
|
||||
|
||||
prepareFile(file) {
|
||||
let pid = 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) {
|
||||
this.saveFile(pid, file);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
prepareResponse(response) {
|
||||
if (response['@type'] === 'file') {
|
||||
return this.prepareFile(response);
|
||||
}
|
||||
for (var key in response) {
|
||||
let field = response[key];
|
||||
if (field && typeof field === 'object') {
|
||||
response[key] = this.prepareResponse(field);
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
flushPendingQueries() {
|
||||
this.isPending = false;
|
||||
for (let query of this.pendingQueries) {
|
||||
this.send(query);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var client = new TdClient((e, t = []) => postMessage(e, t));
|
||||
|
||||
onmessage = function(e) {
|
||||
try {
|
||||
client.send(e.data);
|
||||
} catch (error) {
|
||||
client.onFatalError(error);
|
||||
}
|
||||
};
|
79
example/emscripten/tdweb/webpack.config.js
Normal file
79
example/emscripten/tdweb/webpack.config.js
Normal file
@ -0,0 +1,79 @@
|
||||
const path = require('path');
|
||||
const CleanWebpackPlugin = require('clean-webpack-plugin');
|
||||
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
entry: ['./src/index.js'],
|
||||
output: {
|
||||
filename: 'tdweb.js',
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
library: 'tdweb',
|
||||
libraryTarget: 'umd',
|
||||
umdNamedDefine: true
|
||||
},
|
||||
devServer: {
|
||||
contentBase: './dist'
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin(),
|
||||
new CleanWebpackPlugin(['dist'], {})
|
||||
//, new UglifyJSPlugin()
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js|jsx)$/,
|
||||
exclude: /prebuilt/,
|
||||
enforce: 'pre',
|
||||
include: [path.resolve(__dirname, 'src')],
|
||||
use: [
|
||||
{
|
||||
loader: require.resolve('eslint-loader')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /worker\.(js|jsx)$/,
|
||||
include: [path.resolve(__dirname, 'src')],
|
||||
use: [
|
||||
{
|
||||
loader: require.resolve('worker-loader')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.(js|jsx)$/,
|
||||
exclude: /prebuilt/,
|
||||
include: [path.resolve(__dirname, 'src')],
|
||||
use: [
|
||||
{
|
||||
loader: require.resolve('babel-loader')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.(wasm|mem)$/,
|
||||
include: [path.resolve(__dirname, 'src')],
|
||||
use: [
|
||||
{
|
||||
loader: require.resolve('file-loader')
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
node: {
|
||||
dgram: 'empty',
|
||||
fs: 'empty',
|
||||
net: 'empty',
|
||||
tls: 'empty',
|
||||
crypto: 'empty',
|
||||
child_process: 'empty'
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
ws$: 'fs'
|
||||
}
|
||||
}
|
||||
};
|
@ -36,7 +36,8 @@ int main() {
|
||||
{"\"td/tl/tl_object_parse.h\"", "\"td/tl/tl_object_store.h\""}, {"\"td/utils/buffer.h\""});
|
||||
|
||||
generate_cpp<>("auto/td/mtproto", "mtproto_api", "Slice", "Slice",
|
||||
{"\"td/tl/tl_object_parse.h\"", "\"td/tl/tl_object_store.h\""}, {"\"td/utils/Slice.h\""});
|
||||
{"\"td/tl/tl_object_parse.h\"", "\"td/tl/tl_object_store.h\""},
|
||||
{"\"td/utils/Slice.h\"", "\"td/utils/UInt.h\""});
|
||||
|
||||
#ifdef TD_ENABLE_JNI
|
||||
generate_cpp<td::TD_TL_writer_jni_cpp, td::TD_TL_writer_jni_h>(
|
||||
|
Reference in New Issue
Block a user