Made the opus replay working again
parent
c2fa1badcb
commit
956f778c01
|
@ -8,7 +8,7 @@ set(CMAKE_C_COMPILER "emcc")
|
||||||
set(CMAKE_C_LINK_EXECUTABLE "emcc")
|
set(CMAKE_C_LINK_EXECUTABLE "emcc")
|
||||||
set(CMAKE_CXX_FLAGS "-O3 --llvm-lto 1 --memory-init-file 0 -s WASM=1 -s ASSERTIONS=1") # -s ALLOW_MEMORY_GROWTH=1 -O3
|
set(CMAKE_CXX_FLAGS "-O3 --llvm-lto 1 --memory-init-file 0 -s WASM=1 -s ASSERTIONS=1") # -s ALLOW_MEMORY_GROWTH=1 -O3
|
||||||
set(CMAKE_VERBOSE_MAKEFILE ON)
|
set(CMAKE_VERBOSE_MAKEFILE ON)
|
||||||
set(CMAKE_EXE_LINKER_FLAGS "-s EXTRA_EXPORTED_RUNTIME_METHODS='[\"ccall\", \"cwrap\", \"Pointer_stringify\"]' -s ENVIRONMENT='worker'") #
|
set(CMAKE_EXE_LINKER_FLAGS "-s EXPORTED_FUNCTIONS='[\"_malloc\", \"_free\"]' -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"ccall\", \"cwrap\"]' -s ENVIRONMENT='worker' --pre-js ${CMAKE_SOURCE_DIR}/init.js") #
|
||||||
#add_definitions(-D_GLIBCXX_USE_CXX11_ABI=0)
|
#add_definitions(-D_GLIBCXX_USE_CXX11_ABI=0)
|
||||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/generated/")
|
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/generated/")
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
for(const callback of Array.isArray(self.__init_em_module) ? self.__init_em_module : [])
|
||||||
|
callback(Module);
|
|
@ -23,6 +23,7 @@ extern "C" {
|
||||||
"Memory allocation has failed" //-7 (OPUS_ALLOC_FAIL)
|
"Memory allocation has failed" //-7 (OPUS_ALLOC_FAIL)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
EMSCRIPTEN_KEEPALIVE
|
||||||
inline const char* opus_error_message(int error) {
|
inline const char* opus_error_message(int error) {
|
||||||
error = abs(error);
|
error = abs(error);
|
||||||
if(error > 0 && error <= 7) return opus_errors[error - 1];
|
if(error > 0 && error <= 7) return opus_errors[error - 1];
|
||||||
|
@ -59,11 +60,13 @@ extern "C" {
|
||||||
INVOKE_OPUS(error, opus_encoder_ctl, handle->encoder, OPUS_SET_COMPLEXITY(1));
|
INVOKE_OPUS(error, opus_encoder_ctl, handle->encoder, OPUS_SET_COMPLEXITY(1));
|
||||||
//INVOKE_OPUS(error, opus_encoder_ctl, handle->encoder, OPUS_SET_BITRATE(4740));
|
//INVOKE_OPUS(error, opus_encoder_ctl, handle->encoder, OPUS_SET_BITRATE(4740));
|
||||||
|
|
||||||
|
/* //This method is obsolete!
|
||||||
EM_ASM(
|
EM_ASM(
|
||||||
printMessageToServerTab('Encoder initialized!');
|
printMessageToServerTab('Encoder initialized!');
|
||||||
printMessageToServerTab(' Comprexity: 1');
|
printMessageToServerTab(' Comprexity: 1');
|
||||||
printMessageToServerTab(' Bitrate: 4740');
|
printMessageToServerTab(' Bitrate: 4740');
|
||||||
);
|
);
|
||||||
|
*/
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -98,9 +101,11 @@ extern "C" {
|
||||||
auto result = opus_encode_float(handle->encoder, (float*) buffer, length / handle->channelCount, buffer, maxLength);
|
auto result = opus_encode_float(handle->encoder, (float*) buffer, length / handle->channelCount, buffer, maxLength);
|
||||||
if(result < 0) return result;
|
if(result < 0) return result;
|
||||||
auto end = currentMillies();
|
auto end = currentMillies();
|
||||||
|
/* //This message is obsolete
|
||||||
EM_ASM({
|
EM_ASM({
|
||||||
printMessageToServerTab("codec_opus_encode(...) tooks " + $0 + "ms to execute!");
|
printMessageToServerTab("codec_opus_encode(...) tooks " + $0 + "ms to execute!");
|
||||||
}, end - begin);
|
}, end - begin);
|
||||||
|
*/
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
192
file.ts
192
file.ts
|
@ -463,7 +463,7 @@ const WEB_APP_FILE_LIST = [
|
||||||
"path": "./",
|
"path": "./",
|
||||||
"local-path": "./shared/html/"
|
"local-path": "./shared/html/"
|
||||||
},
|
},
|
||||||
{ /* javascript loader for releases */
|
{ /* javascript files as manifest.json */
|
||||||
"type": "js",
|
"type": "js",
|
||||||
"search-pattern": /.*$/,
|
"search-pattern": /.*$/,
|
||||||
"build-target": "dev|rel",
|
"build-target": "dev|rel",
|
||||||
|
@ -471,7 +471,14 @@ const WEB_APP_FILE_LIST = [
|
||||||
"path": "js/",
|
"path": "js/",
|
||||||
"local-path": "./dist/"
|
"local-path": "./dist/"
|
||||||
},
|
},
|
||||||
|
{ /* loader javascript file */
|
||||||
|
"type": "js",
|
||||||
|
"search-pattern": /.*$/,
|
||||||
|
"build-target": "dev|rel",
|
||||||
|
|
||||||
|
"path": "js/",
|
||||||
|
"local-path": "./loader/dist/"
|
||||||
|
},
|
||||||
{ /* shared javascript files (WebRTC adapter) */
|
{ /* shared javascript files (WebRTC adapter) */
|
||||||
"type": "js",
|
"type": "js",
|
||||||
"search-pattern": /.*\.js$/,
|
"search-pattern": /.*\.js$/,
|
||||||
|
@ -661,41 +668,49 @@ namespace generator {
|
||||||
return result.digest("hex");
|
return result.digest("hex");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rreaddir = async p => {
|
||||||
|
const result = [];
|
||||||
|
try {
|
||||||
|
const files = await fs.readdir(p);
|
||||||
|
for(const file of files) {
|
||||||
|
const file_path = path.join(p, file);
|
||||||
|
|
||||||
|
const info = await fs.stat(file_path);
|
||||||
|
if(info.isDirectory()) {
|
||||||
|
result.push(...await rreaddir(file_path));
|
||||||
|
} else {
|
||||||
|
result.push(file_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(error) {
|
||||||
|
if(error.code === "ENOENT")
|
||||||
|
return [];
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
function file_matches_options(file: ProjectResource, options: SearchOptions) {
|
||||||
|
if(typeof file["web-only"] === "boolean" && file["web-only"] && options.target !== "web")
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if(typeof file["client-only"] === "boolean" && file["client-only"] && options.target !== "client")
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if(typeof file["serve-only"] === "boolean" && file["serve-only"] && !options.serving)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if(!file["build-target"].split("|").find(e => e === options.mode))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return !(Array.isArray(file["req-parm"]) && file["req-parm"].find(e => !options.parameter.find(p => p.toLowerCase() === e.toLowerCase())));
|
||||||
|
}
|
||||||
|
|
||||||
export async function search_files(files: ProjectResource[], options: SearchOptions) : Promise<Entry[]> {
|
export async function search_files(files: ProjectResource[], options: SearchOptions) : Promise<Entry[]> {
|
||||||
const result: Entry[] = [];
|
const result: Entry[] = [];
|
||||||
|
|
||||||
const rreaddir = async p => {
|
|
||||||
const result = [];
|
|
||||||
try {
|
|
||||||
const files = await fs.readdir(p);
|
|
||||||
for(const file of files) {
|
|
||||||
const file_path = path.join(p, file);
|
|
||||||
|
|
||||||
const info = await fs.stat(file_path);
|
|
||||||
if(info.isDirectory()) {
|
|
||||||
result.push(...await rreaddir(file_path));
|
|
||||||
} else {
|
|
||||||
result.push(file_path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch(error) {
|
|
||||||
if(error.code === "ENOENT")
|
|
||||||
return [];
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
for(const file of files) {
|
for(const file of files) {
|
||||||
if(typeof file["web-only"] === "boolean" && file["web-only"] && options.target !== "web")
|
if(!file_matches_options(file, options))
|
||||||
continue;
|
|
||||||
if(typeof file["client-only"] === "boolean" && file["client-only"] && options.target !== "client")
|
|
||||||
continue;
|
|
||||||
if(typeof file["serve-only"] === "boolean" && file["serve-only"] && !options.serving)
|
|
||||||
continue;
|
|
||||||
if(!file["build-target"].split("|").find(e => e === options.mode))
|
|
||||||
continue;
|
|
||||||
if(Array.isArray(file["req-parm"]) && file["req-parm"].find(e => !options.parameter.find(p => p.toLowerCase() === e.toLowerCase())))
|
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
const normal_local = path.normalize(path.join(options.source_path, file["local-path"]));
|
const normal_local = path.normalize(path.join(options.source_path, file["local-path"]));
|
||||||
|
@ -723,23 +738,52 @@ namespace generator {
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function search_http_file(files: ProjectResource[], target_file: string, options: SearchOptions) : Promise<string> {
|
||||||
|
for(const file of files) {
|
||||||
|
if(!file_matches_options(file, options))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if(file.path !== "./" && !target_file.startsWith("/" + file.path.replace(/\\/g, "/")))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const normal_local = path.normalize(path.join(options.source_path, file["local-path"]));
|
||||||
|
const files: string[] = await rreaddir(normal_local);
|
||||||
|
for(const f of files) {
|
||||||
|
const local_name = f.substr(normal_local.length);
|
||||||
|
if(!local_name.match(file["search-pattern"]) && !local_name.replace("\\\\", "/").match(file["search-pattern"]))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if(typeof(file["search-exclude"]) !== "undefined" && f.match(file["search-exclude"]))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if("/" + path.join(file.path, local_name).replace(/\\/g, "/") === target_file)
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace server {
|
namespace server {
|
||||||
|
import SearchOptions = generator.SearchOptions;
|
||||||
export type Options = {
|
export type Options = {
|
||||||
port: number;
|
port: number;
|
||||||
php: string;
|
php: string;
|
||||||
|
|
||||||
|
search_options: SearchOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
const exec: (command: string) => Promise<{ stdout: string, stderr: string }> = util.promisify(cp.exec);
|
const exec: (command: string) => Promise<{ stdout: string, stderr: string }> = util.promisify(cp.exec);
|
||||||
|
|
||||||
let files: (generator.Entry & { http_path: string; })[] = [];
|
let files: ProjectResource[] = [];
|
||||||
let server: http.Server;
|
let server: http.Server;
|
||||||
let php: string;
|
let php: string;
|
||||||
export async function launch(_files: generator.Entry[], options: Options) {
|
let options: Options;
|
||||||
//Don't use this check anymore, because we're searching within the PATH variable
|
export async function launch(_files: ProjectResource[], options_: Options) {
|
||||||
//if(!await fs.exists(options.php) || !(await fs.stat(options.php)).isFile())
|
options = options_;
|
||||||
// throw "invalid php interpreter (not found)";
|
files = _files;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const info = await exec(options.php + " --version");
|
const info = await exec(options.php + " --version");
|
||||||
|
@ -763,17 +807,6 @@ namespace server {
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
files = _files.map(e =>{
|
|
||||||
return {
|
|
||||||
type: e.type,
|
|
||||||
name: e.name,
|
|
||||||
hash: e.hash,
|
|
||||||
local_path: e.local_path,
|
|
||||||
target_path: e.target_path,
|
|
||||||
http_path: "/" + e.target_path.replace(/\\/g, "/")
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function shutdown() {
|
export async function shutdown() {
|
||||||
|
@ -817,8 +850,8 @@ namespace server {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function serve_file(pathname: string, query: any, response: http.ServerResponse) {
|
async function serve_file(pathname: string, query: any, response: http.ServerResponse) {
|
||||||
const file = files.find(e => e.http_path === pathname);
|
const file = await generator.search_http_file(files, pathname, options.search_options);
|
||||||
if(!file) {
|
if(!file) {
|
||||||
console.log("[SERVER] Client requested unknown file %s", pathname);
|
console.log("[SERVER] Client requested unknown file %s", pathname);
|
||||||
response.writeHead(404);
|
response.writeHead(404);
|
||||||
|
@ -827,13 +860,13 @@ namespace server {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let type = mt.lookup(path.extname(file.local_path)) || "text/html";
|
let type = mt.lookup(path.extname(file)) || "text/html";
|
||||||
console.log("[SERVER] Serving file %s (%s) (%s)", file.target_path, type, file.local_path);
|
console.log("[SERVER] Serving file %s", file, type);
|
||||||
if(path.extname(file.local_path) === ".php") {
|
if(path.extname(file) === ".php") {
|
||||||
serve_php(file.local_path, query, response);
|
serve_php(file, query, response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const fis = fs.createReadStream(file.local_path);
|
const fis = fs.createReadStream(file);
|
||||||
|
|
||||||
response.writeHead(200, "success", {
|
response.writeHead(200, "success", {
|
||||||
"Content-Type": type + "; charset=utf-8"
|
"Content-Type": type + "; charset=utf-8"
|
||||||
|
@ -846,23 +879,22 @@ namespace server {
|
||||||
fis.on("data", data => response.write(data));
|
fis.on("data", data => response.write(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
function handle_api_request(request: http.IncomingMessage, response: http.ServerResponse, url: url_utils.UrlWithParsedQuery) {
|
async function handle_api_request(request: http.IncomingMessage, response: http.ServerResponse, url: url_utils.UrlWithParsedQuery) {
|
||||||
if(url.query["type"] === "files") {
|
if(url.query["type"] === "files") {
|
||||||
response.writeHead(200, { "info-version": 1 });
|
response.writeHead(200, { "info-version": 1 });
|
||||||
response.write("type\thash\tpath\tname\n");
|
response.write("type\thash\tpath\tname\n");
|
||||||
for(const file of files)
|
for(const file of await generator.search_files(files, options.search_options))
|
||||||
if(file.http_path.endsWith(".php"))
|
if(file.name.endsWith(".php"))
|
||||||
response.write(file.type + "\t" + file.hash + "\t" + path.dirname(file.http_path) + "\t" + path.basename(file.http_path, ".php") + ".html" + "\n");
|
response.write(file.type + "\t" + file.hash + "\t" + path.dirname(file.target_path) + "\t" + path.basename(file.name, ".php") + ".html" + "\n");
|
||||||
else
|
else
|
||||||
response.write(file.type + "\t" + file.hash + "\t" + path.dirname(file.http_path) + "\t" + path.basename(file.http_path) + "\n");
|
response.write(file.type + "\t" + file.hash + "\t" + path.dirname(file.target_path) + "\t" + file.name + "\n");
|
||||||
response.end();
|
response.end();
|
||||||
return;
|
return;
|
||||||
} else if(url.query["type"] === "file") {
|
} else if(url.query["type"] === "file") {
|
||||||
let p = path.join(url.query["path"] as string, url.query["name"] as string).replace(/\\/g, "/");
|
let p = path.join(url.query["path"] as string, url.query["name"] as string).replace(/\\/g, "/");
|
||||||
if(p.endsWith(".html")) {
|
if(p.endsWith(".html")) {
|
||||||
const np = p.substr(0, p.length - 5) + ".php";
|
const np = await generator.search_http_file(files, p, options.search_options);
|
||||||
if(files.find(e => e.http_path == np) && !files.find(e => e.http_path == p))
|
if(np) p = np;
|
||||||
p = np;
|
|
||||||
}
|
}
|
||||||
serve_file(p, url.query, response);
|
serve_file(p, url.query, response);
|
||||||
return;
|
return;
|
||||||
|
@ -1041,17 +1073,16 @@ function php_exe() : string {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main_serve(target: "client" | "web", mode: "rel" | "dev", port: number) {
|
async function main_serve(target: "client" | "web", mode: "rel" | "dev", port: number) {
|
||||||
const files = await generator.search_files(target === "client" ? CLIENT_APP_FILE_LIST : WEB_APP_FILE_LIST, {
|
await server.launch(target === "client" ? CLIENT_APP_FILE_LIST : WEB_APP_FILE_LIST, {
|
||||||
source_path: __dirname,
|
|
||||||
parameter: [],
|
|
||||||
target: target,
|
|
||||||
mode: mode,
|
|
||||||
serving: true
|
|
||||||
});
|
|
||||||
|
|
||||||
await server.launch(files, {
|
|
||||||
port: port,
|
port: port,
|
||||||
php: php_exe(),
|
php: php_exe(),
|
||||||
|
search_options: {
|
||||||
|
source_path: __dirname,
|
||||||
|
parameter: [],
|
||||||
|
target: target,
|
||||||
|
mode: mode,
|
||||||
|
serving: true
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Server started on %d", port);
|
console.log("Server started on %d", port);
|
||||||
|
@ -1060,14 +1091,6 @@ async function main_serve(target: "client" | "web", mode: "rel" | "dev", port: n
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main_develop(node: boolean, target: "client" | "web", port: number, flags: string[]) {
|
async function main_develop(node: boolean, target: "client" | "web", port: number, flags: string[]) {
|
||||||
const files = await generator.search_files(target === "client" ? CLIENT_APP_FILE_LIST : WEB_APP_FILE_LIST, {
|
|
||||||
source_path: __dirname,
|
|
||||||
parameter: [],
|
|
||||||
target: target,
|
|
||||||
mode: "dev",
|
|
||||||
serving: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const tscwatcher = new watcher.TSCWatcher();
|
const tscwatcher = new watcher.TSCWatcher();
|
||||||
try {
|
try {
|
||||||
if(flags.indexOf("--no-tsc") == -1)
|
if(flags.indexOf("--no-tsc") == -1)
|
||||||
|
@ -1079,9 +1102,16 @@ async function main_develop(node: boolean, target: "client" | "web", port: numbe
|
||||||
await sasswatcher.start();
|
await sasswatcher.start();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await server.launch(files, {
|
await server.launch(target === "client" ? CLIENT_APP_FILE_LIST : WEB_APP_FILE_LIST, {
|
||||||
port: port,
|
port: port,
|
||||||
php: php_exe(),
|
php: php_exe(),
|
||||||
|
search_options: {
|
||||||
|
source_path: __dirname,
|
||||||
|
parameter: [],
|
||||||
|
target: target,
|
||||||
|
mode: "dev",
|
||||||
|
serving: true
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
console.error("Failed to start server: %o", error instanceof Error ? error.message : error);
|
console.error("Failed to start server: %o", error instanceof Error ? error.message : error);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import * as loader from "./loader";
|
import * as loader from "./loader/loader";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
@ -8,6 +8,11 @@ declare global {
|
||||||
|
|
||||||
const node_require: typeof require = window.require;
|
const node_require: typeof require = window.require;
|
||||||
|
|
||||||
|
function cache_tag() {
|
||||||
|
const ui = ui_version();
|
||||||
|
return "?_ts=" + (!!ui && ui !== "unknown" ? ui : Date.now());
|
||||||
|
}
|
||||||
|
|
||||||
let _ui_version;
|
let _ui_version;
|
||||||
export function ui_version() {
|
export function ui_version() {
|
||||||
if(typeof(_ui_version) !== "string") {
|
if(typeof(_ui_version) !== "string") {
|
||||||
|
@ -22,6 +27,15 @@ export function ui_version() {
|
||||||
return _ui_version;
|
return _ui_version;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Manifest {
|
||||||
|
version: number;
|
||||||
|
|
||||||
|
chunks: {[key: string]: {
|
||||||
|
hash: string,
|
||||||
|
file: string
|
||||||
|
}[]};
|
||||||
|
}
|
||||||
|
|
||||||
/* all javascript loaders */
|
/* all javascript loaders */
|
||||||
const loader_javascript = {
|
const loader_javascript = {
|
||||||
detect_type: async () => {
|
detect_type: async () => {
|
||||||
|
@ -72,7 +86,7 @@ const loader_javascript = {
|
||||||
},
|
},
|
||||||
load_scripts: async () => {
|
load_scripts: async () => {
|
||||||
if(!window.require) {
|
if(!window.require) {
|
||||||
await loader.load_script(["vendor/jquery/jquery.min.js"]);
|
await loader.scripts.load(["vendor/jquery/jquery.min.js"], { cache_tag: cache_tag() });
|
||||||
} else {
|
} else {
|
||||||
/*
|
/*
|
||||||
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
|
@ -84,45 +98,41 @@ const loader_javascript = {
|
||||||
});
|
});
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
await loader.load_script(["vendor/DOMPurify/purify.min.js"]);
|
await loader.scripts.load(["vendor/DOMPurify/purify.min.js"], { cache_tag: cache_tag() });
|
||||||
|
|
||||||
await loader.load_script("vendor/jsrender/jsrender.min.js");
|
await loader.scripts.load("vendor/jsrender/jsrender.min.js", { cache_tag: cache_tag() });
|
||||||
await loader.load_scripts([
|
await loader.scripts.load_multiple([
|
||||||
["vendor/xbbcode/src/parser.js"],
|
["vendor/xbbcode/src/parser.js"],
|
||||||
["vendor/moment/moment.js"],
|
["vendor/moment/moment.js"],
|
||||||
["vendor/twemoji/twemoji.min.js", ""], /* empty string means not required */
|
["vendor/twemoji/twemoji.min.js", ""], /* empty string means not required */
|
||||||
["vendor/highlight/highlight.pack.js", ""], /* empty string means not required */
|
["vendor/highlight/highlight.pack.js", ""], /* empty string means not required */
|
||||||
["vendor/remarkable/remarkable.min.js", ""], /* empty string means not required */
|
["vendor/remarkable/remarkable.min.js", ""], /* empty string means not required */
|
||||||
["adapter/adapter-latest.js", "https://webrtc.github.io/adapter/adapter-latest.js"]
|
["adapter/adapter-latest.js", "https://webrtc.github.io/adapter/adapter-latest.js"]
|
||||||
]);
|
], {
|
||||||
await loader.load_scripts([
|
cache_tag: cache_tag(),
|
||||||
["vendor/emoji-picker/src/jquery.lsxemojipicker.js"]
|
max_parallel_requests: -1
|
||||||
]);
|
});
|
||||||
|
|
||||||
if(!loader.version().debug_mode) {
|
await loader.scripts.load("vendor/emoji-picker/src/jquery.lsxemojipicker.js", { cache_tag: cache_tag() });
|
||||||
loader.register_task(loader.Stage.JAVASCRIPT, {
|
|
||||||
name: "scripts release",
|
let manifest: Manifest;
|
||||||
priority: 20,
|
try {
|
||||||
function: loader_javascript.load_release
|
const response = await fetch("js/manifest.json");
|
||||||
});
|
if(!response.ok) throw response.status + " " + response.statusText;
|
||||||
} else {
|
|
||||||
loader.register_task(loader.Stage.JAVASCRIPT, {
|
manifest = await response.json();
|
||||||
name: "scripts debug",
|
} catch(error) {
|
||||||
priority: 20,
|
console.error("Failed to load javascript manifest: %o", error);
|
||||||
function: loader_javascript.load_scripts_debug
|
loader.critical_error("Failed to load manifest.json", error);
|
||||||
});
|
throw "failed to load manifest.json";
|
||||||
}
|
}
|
||||||
},
|
if(manifest.version !== 1)
|
||||||
load_scripts_debug: async () => {
|
throw "invalid manifest version";
|
||||||
await loader.load_scripts(["js/shared-app.js"])
|
|
||||||
},
|
|
||||||
load_release: async () => {
|
|
||||||
console.log("Load for release!");
|
|
||||||
|
|
||||||
await loader.load_scripts([
|
await loader.scripts.load_multiple(manifest.chunks["shared-app"].map(e => "js/" + e.file), {
|
||||||
//Load general API's
|
cache_tag: undefined,
|
||||||
["js/client.min.js", "js/client.js"]
|
max_parallel_requests: -1
|
||||||
]);
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -149,15 +159,20 @@ const loader_webassembly = {
|
||||||
|
|
||||||
const loader_style = {
|
const loader_style = {
|
||||||
load_style: async () => {
|
load_style: async () => {
|
||||||
await loader.load_styles([
|
const options = {
|
||||||
|
cache_tag: cache_tag(),
|
||||||
|
max_parallel_requests: -1
|
||||||
|
};
|
||||||
|
|
||||||
|
await loader.style.load_multiple([
|
||||||
"vendor/xbbcode/src/xbbcode.css"
|
"vendor/xbbcode/src/xbbcode.css"
|
||||||
]);
|
], options);
|
||||||
await loader.load_styles([
|
await loader.style.load_multiple([
|
||||||
"vendor/emoji-picker/src/jquery.lsxemojipicker.css"
|
"vendor/emoji-picker/src/jquery.lsxemojipicker.css"
|
||||||
]);
|
], options);
|
||||||
await loader.load_styles([
|
await loader.style.load_multiple([
|
||||||
["vendor/highlight/styles/darcula.css", ""], /* empty string means not required */
|
["vendor/highlight/styles/darcula.css", ""], /* empty string means not required */
|
||||||
]);
|
], options);
|
||||||
|
|
||||||
if(loader.version().debug_mode) {
|
if(loader.version().debug_mode) {
|
||||||
await loader_style.load_style_debug();
|
await loader_style.load_style_debug();
|
||||||
|
@ -167,7 +182,7 @@ const loader_style = {
|
||||||
},
|
},
|
||||||
|
|
||||||
load_style_debug: async () => {
|
load_style_debug: async () => {
|
||||||
await loader.load_styles([
|
await loader.style.load_multiple([
|
||||||
"css/static/main.css",
|
"css/static/main.css",
|
||||||
"css/static/main-layout.css",
|
"css/static/main-layout.css",
|
||||||
"css/static/helptag.css",
|
"css/static/helptag.css",
|
||||||
|
@ -218,14 +233,20 @@ const loader_style = {
|
||||||
"css/static/htmltags.css",
|
"css/static/htmltags.css",
|
||||||
"css/static/hostbanner.css",
|
"css/static/hostbanner.css",
|
||||||
"css/static/menu-bar.css"
|
"css/static/menu-bar.css"
|
||||||
]);
|
], {
|
||||||
|
cache_tag: cache_tag(),
|
||||||
|
max_parallel_requests: -1
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
load_style_release: async () => {
|
load_style_release: async () => {
|
||||||
await loader.load_styles([
|
await loader.style.load_multiple([
|
||||||
"css/static/base.css",
|
"css/static/base.css",
|
||||||
"css/static/main.css",
|
"css/static/main.css",
|
||||||
]);
|
], {
|
||||||
|
cache_tag: cache_tag(),
|
||||||
|
max_parallel_requests: -1
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -313,11 +334,14 @@ loader.register_task(loader.Stage.STYLE, {
|
||||||
loader.register_task(loader.Stage.TEMPLATES, {
|
loader.register_task(loader.Stage.TEMPLATES, {
|
||||||
name: "templates",
|
name: "templates",
|
||||||
function: async () => {
|
function: async () => {
|
||||||
await loader.load_templates([
|
await loader.templates.load_multiple([
|
||||||
"templates.html",
|
"templates.html",
|
||||||
"templates/modal/musicmanage.html",
|
"templates/modal/musicmanage.html",
|
||||||
"templates/modal/newcomer.html",
|
"templates/modal/newcomer.html",
|
||||||
]);
|
], {
|
||||||
|
cache_tag: cache_tag(),
|
||||||
|
max_parallel_requests: -1
|
||||||
|
});
|
||||||
},
|
},
|
||||||
priority: 10
|
priority: 10
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import * as loader from "./loader";
|
import * as loader from "./loader/loader";
|
||||||
|
|
||||||
let is_debug = false;
|
let is_debug = false;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as loader from "./app";
|
import * as loader from "./app";
|
||||||
import * as loader_base from "./loader";
|
import * as loader_base from "./loader/loader";
|
||||||
|
|
||||||
export = loader_base;
|
export = loader_base;
|
||||||
loader.run();
|
loader.run();
|
|
@ -1,5 +1,8 @@
|
||||||
import {AppVersion} from "../exports/loader";
|
import {AppVersion} from "tc-loader";
|
||||||
import {type} from "os";
|
import {LoadSyntaxError, script_name} from "./utils";
|
||||||
|
import * as script_loader from "./script_loader";
|
||||||
|
import * as style_loader from "./style_loader";
|
||||||
|
import * as template_loader from "./template_loader";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
@ -205,10 +208,6 @@ export async function execute() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* cleanup */
|
|
||||||
{
|
|
||||||
_script_promises = {};
|
|
||||||
}
|
|
||||||
if(config.verbose) console.debug("[loader] finished loader. (Total time: %dms)", Date.now() - load_begin);
|
if(config.verbose) console.debug("[loader] finished loader. (Total time: %dms)", Date.now() - load_begin);
|
||||||
}
|
}
|
||||||
export function execute_managed() {
|
export function execute_managed() {
|
||||||
|
@ -233,347 +232,32 @@ export function execute_managed() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DependSource = {
|
let _fadeout_warned;
|
||||||
url: string;
|
export function hide_overlay() {
|
||||||
depends: string[];
|
if(typeof($) === "undefined") {
|
||||||
}
|
if(!_fadeout_warned)
|
||||||
export type SourcePath = string | DependSource | string[];
|
console.warn("Could not fadeout loader screen. Missing jquery functions.");
|
||||||
|
_fadeout_warned = true;
|
||||||
function script_name(path: SourcePath, html: boolean) {
|
return;
|
||||||
if(Array.isArray(path)) {
|
|
||||||
let buffer = "";
|
|
||||||
let _or = " or ";
|
|
||||||
for(let entry of path)
|
|
||||||
buffer += _or + script_name(entry, html);
|
|
||||||
return buffer.slice(_or.length);
|
|
||||||
} else if(typeof(path) === "string")
|
|
||||||
return html ? "<code>" + path + "</code>" : path;
|
|
||||||
else
|
|
||||||
return html ? "<code>" + path.url + "</code>" : path.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
class SyntaxError {
|
|
||||||
source: any;
|
|
||||||
|
|
||||||
constructor(source: any) {
|
|
||||||
this.source = source;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let _script_promises: {[key: string]: Promise<void>} = {};
|
|
||||||
export async function load_script(path: SourcePath) : Promise<void> {
|
|
||||||
if(Array.isArray(path)) { //We have some fallback
|
|
||||||
return load_script(path[0]).catch(error => {
|
|
||||||
console.log(typeof error + " - " + (error instanceof SyntaxError));
|
|
||||||
if(error instanceof SyntaxError)
|
|
||||||
return Promise.reject(error);
|
|
||||||
|
|
||||||
if(path.length > 1)
|
|
||||||
return load_script(path.slice(1));
|
|
||||||
|
|
||||||
return Promise.reject(error);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const source = typeof(path) === "string" ? {url: path, depends: []} : path;
|
|
||||||
if(source.url.length == 0) return Promise.resolve();
|
|
||||||
|
|
||||||
return _script_promises[source.url] = (async () => {
|
|
||||||
/* await depends */
|
|
||||||
for(const depend of source.depends) {
|
|
||||||
if(!_script_promises[depend])
|
|
||||||
throw "Missing dependency " + depend;
|
|
||||||
await _script_promises[depend];
|
|
||||||
}
|
|
||||||
|
|
||||||
const tag: HTMLScriptElement = document.createElement("script");
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
let error = false;
|
|
||||||
const error_handler = (event: ErrorEvent) => {
|
|
||||||
if(event.filename == tag.src && event.message.indexOf("Illegal constructor") == -1) { //Our tag throw an uncaught error
|
|
||||||
if(config.verbose) console.log("msg: %o, url: %o, line: %o, col: %o, error: %o", event.message, event.filename, event.lineno, event.colno, event.error);
|
|
||||||
window.removeEventListener('error', error_handler as any);
|
|
||||||
|
|
||||||
reject(new SyntaxError(event.error));
|
|
||||||
event.preventDefault();
|
|
||||||
error = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener('error', error_handler as any);
|
|
||||||
|
|
||||||
const cleanup = () => {
|
|
||||||
tag.onerror = undefined;
|
|
||||||
tag.onload = undefined;
|
|
||||||
|
|
||||||
clearTimeout(timeout_handle);
|
|
||||||
window.removeEventListener('error', error_handler as any);
|
|
||||||
};
|
|
||||||
const timeout_handle = setTimeout(() => {
|
|
||||||
cleanup();
|
|
||||||
reject("timeout");
|
|
||||||
}, 5000);
|
|
||||||
tag.type = "application/javascript";
|
|
||||||
tag.async = true;
|
|
||||||
tag.defer = true;
|
|
||||||
tag.onerror = error => {
|
|
||||||
cleanup();
|
|
||||||
tag.remove();
|
|
||||||
reject(error);
|
|
||||||
};
|
|
||||||
tag.onload = () => {
|
|
||||||
cleanup();
|
|
||||||
|
|
||||||
if(config.verbose) console.debug("Script %o loaded", path);
|
|
||||||
setTimeout(resolve, 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
document.getElementById("scripts").appendChild(tag);
|
|
||||||
|
|
||||||
tag.src = source.url + (cache_tag || "");
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function load_scripts(paths: SourcePath[]) : Promise<void> {
|
|
||||||
const promises: Promise<void>[] = [];
|
|
||||||
const errors: {
|
|
||||||
script: SourcePath,
|
|
||||||
error: any
|
|
||||||
}[] = [];
|
|
||||||
|
|
||||||
for(const script of paths)
|
|
||||||
promises.push(load_script(script).catch(error => {
|
|
||||||
errors.push({
|
|
||||||
script: script,
|
|
||||||
error: error
|
|
||||||
});
|
|
||||||
return Promise.resolve();
|
|
||||||
}));
|
|
||||||
|
|
||||||
await Promise.all([...promises]);
|
|
||||||
|
|
||||||
if(errors.length > 0) {
|
|
||||||
if(config.error) {
|
|
||||||
console.error("Failed to load the following scripts:");
|
|
||||||
for(const script of errors) {
|
|
||||||
const sname = script_name(script.script, false);
|
|
||||||
if(script.error instanceof SyntaxError) {
|
|
||||||
const source = script.error.source as Error;
|
|
||||||
if(source.name === "TypeError") {
|
|
||||||
let prefix = "";
|
|
||||||
while(prefix.length < sname.length + 7) prefix += " ";
|
|
||||||
console.log(" - %s: %s:\n%s", sname, source.message, source.stack.split("\n").map(e => prefix + e.trim()).slice(1).join("\n"));
|
|
||||||
} else {
|
|
||||||
console.log(" - %s: %o", sname, source);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(" - %s: %o", sname, script.error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
critical_error("Failed to load script " + script_name(errors[0].script, true) + " <br>" + "View the browser console for more information!");
|
|
||||||
throw "failed to load script " + script_name(errors[0].script, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function load_style(path: SourcePath) : Promise<void> {
|
|
||||||
if(Array.isArray(path)) { //We have some fallback
|
|
||||||
return load_style(path[0]).catch(error => {
|
|
||||||
if(error instanceof SyntaxError)
|
|
||||||
return Promise.reject(error.source);
|
|
||||||
|
|
||||||
if(path.length > 1)
|
|
||||||
return load_script(path.slice(1));
|
|
||||||
|
|
||||||
return Promise.reject(error);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if(!path) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
|
||||||
const tag: HTMLLinkElement = document.createElement("link");
|
|
||||||
|
|
||||||
let error = false;
|
|
||||||
const error_handler = (event: ErrorEvent) => {
|
|
||||||
if(config.verbose) console.log("msg: %o, url: %o, line: %o, col: %o, error: %o", event.message, event.filename, event.lineno, event.colno, event.error);
|
|
||||||
if(event.filename == tag.href) { //FIXME!
|
|
||||||
window.removeEventListener('error', error_handler as any);
|
|
||||||
|
|
||||||
reject(new SyntaxError(event.error));
|
|
||||||
event.preventDefault();
|
|
||||||
error = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener('error', error_handler as any);
|
|
||||||
|
|
||||||
tag.type = "text/css";
|
|
||||||
tag.rel = "stylesheet";
|
|
||||||
|
|
||||||
const cleanup = () => {
|
|
||||||
tag.onerror = undefined;
|
|
||||||
tag.onload = undefined;
|
|
||||||
|
|
||||||
clearTimeout(timeout_handle);
|
|
||||||
window.removeEventListener('error', error_handler as any);
|
|
||||||
};
|
|
||||||
|
|
||||||
const timeout_handle = setTimeout(() => {
|
|
||||||
cleanup();
|
|
||||||
reject("timeout");
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
tag.onerror = error => {
|
|
||||||
cleanup();
|
|
||||||
tag.remove();
|
|
||||||
if(config.error)
|
|
||||||
console.error("File load error for file %s: %o", path, error);
|
|
||||||
reject("failed to load file " + path);
|
|
||||||
};
|
|
||||||
tag.onload = () => {
|
|
||||||
cleanup();
|
|
||||||
{
|
|
||||||
const css: CSSStyleSheet = tag.sheet as CSSStyleSheet;
|
|
||||||
const rules = css.cssRules;
|
|
||||||
const rules_remove: number[] = [];
|
|
||||||
const rules_add: string[] = [];
|
|
||||||
|
|
||||||
for(let index = 0; index < rules.length; index++) {
|
|
||||||
const rule = rules.item(index);
|
|
||||||
let rule_text = rule.cssText;
|
|
||||||
|
|
||||||
if(rule.cssText.indexOf("%%base_path%%") != -1) {
|
|
||||||
rules_remove.push(index);
|
|
||||||
rules_add.push(rule_text.replace("%%base_path%%", document.location.origin + document.location.pathname));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for(const index of rules_remove.sort((a, b) => b > a ? 1 : 0)) {
|
|
||||||
if(css.removeRule)
|
|
||||||
css.removeRule(index);
|
|
||||||
else
|
|
||||||
css.deleteRule(index);
|
|
||||||
}
|
|
||||||
for(const rule of rules_add)
|
|
||||||
css.insertRule(rule, rules_remove[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(config.verbose) console.debug("Style sheet %o loaded", path);
|
|
||||||
setTimeout(resolve, 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
document.getElementById("style").appendChild(tag);
|
|
||||||
tag.href = path + (cache_tag || "");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function load_styles(paths: SourcePath[]) : Promise<void> {
|
|
||||||
const promises: Promise<void>[] = [];
|
|
||||||
const errors: {
|
|
||||||
sheet: SourcePath,
|
|
||||||
error: any
|
|
||||||
}[] = [];
|
|
||||||
|
|
||||||
for(const sheet of paths)
|
|
||||||
promises.push(load_style(sheet).catch(error => {
|
|
||||||
errors.push({
|
|
||||||
sheet: sheet,
|
|
||||||
error: error
|
|
||||||
});
|
|
||||||
return Promise.resolve();
|
|
||||||
}));
|
|
||||||
|
|
||||||
await Promise.all([...promises]);
|
|
||||||
|
|
||||||
if(errors.length > 0) {
|
|
||||||
if(config.error) {
|
|
||||||
console.error("Failed to load the following style sheet:");
|
|
||||||
for(const sheet of errors)
|
|
||||||
console.log(" - %o: %o", sheet.sheet, sheet.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
critical_error("Failed to load style sheet " + script_name(errors[0].sheet, true) + " <br>" + "View the browser console for more information!");
|
|
||||||
throw "failed to load style sheet " + script_name(errors[0].sheet, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function load_template(path: SourcePath) : Promise<void> {
|
|
||||||
try {
|
|
||||||
const response = await $.ajax(path + (cache_tag || ""));
|
|
||||||
|
|
||||||
let node = document.createElement("html");
|
|
||||||
node.innerHTML = response;
|
|
||||||
let tags: HTMLCollection;
|
|
||||||
if(node.getElementsByTagName("body").length > 0)
|
|
||||||
tags = node.getElementsByTagName("body")[0].children;
|
|
||||||
else
|
|
||||||
tags = node.children;
|
|
||||||
|
|
||||||
let root = document.getElementById("templates");
|
|
||||||
if(!root) {
|
|
||||||
critical_error("Failed to find template tag!");
|
|
||||||
throw "Failed to find template tag";
|
|
||||||
}
|
|
||||||
while(tags.length > 0){
|
|
||||||
let tag = tags.item(0);
|
|
||||||
root.appendChild(tag);
|
|
||||||
|
|
||||||
}
|
|
||||||
} catch(error) {
|
|
||||||
let msg;
|
|
||||||
if('responseText' in error)
|
|
||||||
msg = error.responseText;
|
|
||||||
else if(error instanceof Error)
|
|
||||||
msg = error.message;
|
|
||||||
|
|
||||||
critical_error("failed to load template " + script_name(path, true), msg);
|
|
||||||
throw "template error";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function load_templates(paths: SourcePath[]) : Promise<void> {
|
|
||||||
const promises: Promise<void>[] = [];
|
|
||||||
const errors: {
|
|
||||||
template: SourcePath,
|
|
||||||
error: any
|
|
||||||
}[] = [];
|
|
||||||
|
|
||||||
for(const template of paths)
|
|
||||||
promises.push(load_template(template).catch(error => {
|
|
||||||
errors.push({
|
|
||||||
template: template,
|
|
||||||
error: error
|
|
||||||
});
|
|
||||||
return Promise.resolve();
|
|
||||||
}));
|
|
||||||
|
|
||||||
await Promise.all([...promises]);
|
|
||||||
|
|
||||||
if(errors.length > 0) {
|
|
||||||
if (config.error) {
|
|
||||||
console.error("Failed to load the following templates:");
|
|
||||||
for (const sheet of errors)
|
|
||||||
console.log(" - %s: %o", script_name(sheet.template, false), sheet.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
critical_error("Failed to load template " + script_name(errors[0].template, true) + " <br>" + "View the browser console for more information!");
|
|
||||||
throw "failed to load template " + script_name(errors[0].template, false);
|
|
||||||
}
|
}
|
||||||
|
const animation_duration = 750;
|
||||||
|
|
||||||
|
$(".loader .bookshelf_wrapper").animate({top: 0, opacity: 0}, animation_duration);
|
||||||
|
$(".loader .half").animate({width: 0}, animation_duration, () => {
|
||||||
|
$(".loader").detach();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* versions management */
|
||||||
let version_: AppVersion;
|
let version_: AppVersion;
|
||||||
export function version() : AppVersion { return version_; }
|
export function version() : AppVersion { return version_; }
|
||||||
export function set_version(version: AppVersion) { version_ = version; }
|
export function set_version(version: AppVersion) { version_ = version; }
|
||||||
|
|
||||||
export type ErrorHandler = (message: string, detail: string) => void;
|
|
||||||
|
|
||||||
|
/* critical error handler */
|
||||||
|
export type ErrorHandler = (message: string, detail: string) => void;
|
||||||
let _callback_critical_error: ErrorHandler;
|
let _callback_critical_error: ErrorHandler;
|
||||||
let _callback_critical_called: boolean = false;
|
let _callback_critical_called: boolean = false;
|
||||||
|
|
||||||
export function critical_error(message: string, detail?: string) {
|
export function critical_error(message: string, detail?: string) {
|
||||||
if(_callback_critical_called) {
|
if(_callback_critical_called) {
|
||||||
console.warn("[CRITICAL] %s", message);
|
console.warn("[CRITICAL] %s", message);
|
||||||
|
@ -603,29 +287,24 @@ export function critical_error(message: string, detail?: string) {
|
||||||
|
|
||||||
tag.classList.add("shown");
|
tag.classList.add("shown");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function critical_error_handler(handler?: ErrorHandler, override?: boolean) : ErrorHandler {
|
export function critical_error_handler(handler?: ErrorHandler, override?: boolean) : ErrorHandler {
|
||||||
if((typeof(handler) === "object" && handler !== _callback_critical_error) || override)
|
if((typeof(handler) === "object" && handler !== _callback_critical_error) || override)
|
||||||
_callback_critical_error = handler;
|
_callback_critical_error = handler;
|
||||||
return _callback_critical_error;
|
return _callback_critical_error;
|
||||||
}
|
}
|
||||||
|
|
||||||
let _fadeout_warned;
|
/* loaders */
|
||||||
export function hide_overlay() {
|
export type DependSource = {
|
||||||
if(typeof($) === "undefined") {
|
url: string;
|
||||||
if(!_fadeout_warned)
|
depends: string[];
|
||||||
console.warn("Could not fadeout loader screen. Missing jquery functions.");
|
|
||||||
_fadeout_warned = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const animation_duration = 750;
|
|
||||||
|
|
||||||
$(".loader .bookshelf_wrapper").animate({top: 0, opacity: 0}, animation_duration);
|
|
||||||
$(".loader .half").animate({width: 0}, animation_duration, () => {
|
|
||||||
$(".loader").detach();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
export type SourcePath = string | DependSource | string[];
|
||||||
|
|
||||||
|
export const scripts = script_loader;
|
||||||
|
export const style = style_loader;
|
||||||
|
export const templates = template_loader;
|
||||||
|
|
||||||
|
/* Hello World message */
|
||||||
{
|
{
|
||||||
|
|
||||||
const hello_world = () => {
|
const hello_world = () => {
|
|
@ -0,0 +1,121 @@
|
||||||
|
import {config, critical_error, SourcePath} from "./loader";
|
||||||
|
import {load_parallel, LoadSyntaxError, ParallelOptions, script_name} from "./utils";
|
||||||
|
|
||||||
|
let _script_promises: {[key: string]: Promise<void>} = {};
|
||||||
|
|
||||||
|
function load_script_url(url: string) : Promise<void> {
|
||||||
|
if(typeof _script_promises[url] === "object")
|
||||||
|
return _script_promises[url];
|
||||||
|
|
||||||
|
return (_script_promises[url] = new Promise((resolve, reject) => {
|
||||||
|
const script_tag: HTMLScriptElement = document.createElement("script");
|
||||||
|
|
||||||
|
let error = false;
|
||||||
|
const error_handler = (event: ErrorEvent) => {
|
||||||
|
if(event.filename == script_tag.src && event.message.indexOf("Illegal constructor") == -1) { //Our tag throw an uncaught error
|
||||||
|
if(config.verbose) console.log("msg: %o, url: %o, line: %o, col: %o, error: %o", event.message, event.filename, event.lineno, event.colno, event.error);
|
||||||
|
window.removeEventListener('error', error_handler as any);
|
||||||
|
|
||||||
|
reject(new LoadSyntaxError(event.error));
|
||||||
|
event.preventDefault();
|
||||||
|
error = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('error', error_handler as any);
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
script_tag.onerror = undefined;
|
||||||
|
script_tag.onload = undefined;
|
||||||
|
|
||||||
|
clearTimeout(timeout_handle);
|
||||||
|
window.removeEventListener('error', error_handler as any);
|
||||||
|
};
|
||||||
|
const timeout_handle = setTimeout(() => {
|
||||||
|
cleanup();
|
||||||
|
reject("timeout");
|
||||||
|
}, 5000);
|
||||||
|
script_tag.type = "application/javascript";
|
||||||
|
script_tag.async = true;
|
||||||
|
script_tag.defer = true;
|
||||||
|
script_tag.onerror = error => {
|
||||||
|
cleanup();
|
||||||
|
script_tag.remove();
|
||||||
|
reject(error);
|
||||||
|
};
|
||||||
|
script_tag.onload = () => {
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
if(config.verbose) console.debug("Script %o loaded", url);
|
||||||
|
setTimeout(resolve, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById("scripts").appendChild(script_tag);
|
||||||
|
|
||||||
|
script_tag.src = url;
|
||||||
|
})).then(result => {
|
||||||
|
/* cleanup memory */
|
||||||
|
_script_promises[url] = Promise.resolve(); /* this promise does not holds the whole script tag and other memory */
|
||||||
|
return _script_promises[url];
|
||||||
|
}).catch(error => {
|
||||||
|
/* cleanup memory */
|
||||||
|
_script_promises[url] = Promise.reject(error); /* this promise does not holds the whole script tag and other memory */
|
||||||
|
return _script_promises[url];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Options {
|
||||||
|
cache_tag?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function load(path: SourcePath, options: Options) : Promise<void> {
|
||||||
|
if(Array.isArray(path)) { //We have fallback scripts
|
||||||
|
return load(path[0], options).catch(error => {
|
||||||
|
if(error instanceof LoadSyntaxError)
|
||||||
|
return Promise.reject(error);
|
||||||
|
|
||||||
|
if(path.length > 1)
|
||||||
|
return load(path.slice(1), options);
|
||||||
|
|
||||||
|
return Promise.reject(error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const source = typeof(path) === "string" ? {url: path, depends: []} : path;
|
||||||
|
if(source.url.length == 0) return Promise.resolve();
|
||||||
|
|
||||||
|
/* await depends */
|
||||||
|
for(const depend of source.depends) {
|
||||||
|
if(!_script_promises[depend])
|
||||||
|
throw "Missing dependency " + depend;
|
||||||
|
await _script_promises[depend];
|
||||||
|
}
|
||||||
|
await load_script_url(source.url + (options.cache_tag || ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type MultipleOptions = Options | ParallelOptions;
|
||||||
|
export async function load_multiple(paths: SourcePath[], options: MultipleOptions) : Promise<void> {
|
||||||
|
const result = await load_parallel<SourcePath>(paths, e => load(e, options), e => script_name(e, false), options);
|
||||||
|
if(result.failed.length > 0) {
|
||||||
|
if(config.error) {
|
||||||
|
console.error("Failed to load the following scripts:");
|
||||||
|
for(const script of result.failed) {
|
||||||
|
const sname = script_name(script.request, false);
|
||||||
|
if(script.error instanceof LoadSyntaxError) {
|
||||||
|
const source = script.error.source as Error;
|
||||||
|
if(source.name === "TypeError") {
|
||||||
|
let prefix = "";
|
||||||
|
while(prefix.length < sname.length + 7) prefix += " ";
|
||||||
|
console.log(" - %s: %s:\n%s", sname, source.message, source.stack.split("\n").map(e => prefix + e.trim()).slice(1).join("\n"));
|
||||||
|
} else {
|
||||||
|
console.log(" - %s: %o", sname, source);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(" - %s: %o", sname, script.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
critical_error("Failed to load script " + script_name(result.failed[0].request, true) + " <br>" + "View the browser console for more information!");
|
||||||
|
throw "failed to load script " + script_name(result.failed[0].request, false);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,142 @@
|
||||||
|
import {config, critical_error, SourcePath} from "./loader";
|
||||||
|
import {load_parallel, LoadSyntaxError, ParallelOptions, script_name} from "./utils";
|
||||||
|
|
||||||
|
let _style_promises: {[key: string]: Promise<void>} = {};
|
||||||
|
|
||||||
|
function load_style_url(url: string) : Promise<void> {
|
||||||
|
if(typeof _style_promises[url] === "object")
|
||||||
|
return _style_promises[url];
|
||||||
|
|
||||||
|
return (_style_promises[url] = new Promise((resolve, reject) => {
|
||||||
|
const tag: HTMLLinkElement = document.createElement("link");
|
||||||
|
|
||||||
|
let error = false;
|
||||||
|
const error_handler = (event: ErrorEvent) => {
|
||||||
|
if(config.verbose) console.log("msg: %o, url: %o, line: %o, col: %o, error: %o", event.message, event.filename, event.lineno, event.colno, event.error);
|
||||||
|
if(event.filename == tag.href) { //FIXME!
|
||||||
|
window.removeEventListener('error', error_handler as any);
|
||||||
|
|
||||||
|
reject(new SyntaxError(event.error));
|
||||||
|
event.preventDefault();
|
||||||
|
error = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('error', error_handler as any);
|
||||||
|
|
||||||
|
tag.type = "text/css";
|
||||||
|
tag.rel = "stylesheet";
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
tag.onerror = undefined;
|
||||||
|
tag.onload = undefined;
|
||||||
|
|
||||||
|
clearTimeout(timeout_handle);
|
||||||
|
window.removeEventListener('error', error_handler as any);
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeout_handle = setTimeout(() => {
|
||||||
|
cleanup();
|
||||||
|
reject("timeout");
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
tag.onerror = error => {
|
||||||
|
cleanup();
|
||||||
|
tag.remove();
|
||||||
|
if(config.error)
|
||||||
|
console.error("File load error for file %s: %o", url, error);
|
||||||
|
reject("failed to load file " + url);
|
||||||
|
};
|
||||||
|
tag.onload = () => {
|
||||||
|
cleanup();
|
||||||
|
{
|
||||||
|
const css: CSSStyleSheet = tag.sheet as CSSStyleSheet;
|
||||||
|
const rules = css.cssRules;
|
||||||
|
const rules_remove: number[] = [];
|
||||||
|
const rules_add: string[] = [];
|
||||||
|
|
||||||
|
for(let index = 0; index < rules.length; index++) {
|
||||||
|
const rule = rules.item(index);
|
||||||
|
let rule_text = rule.cssText;
|
||||||
|
|
||||||
|
if(rule.cssText.indexOf("%%base_path%%") != -1) {
|
||||||
|
rules_remove.push(index);
|
||||||
|
rules_add.push(rule_text.replace("%%base_path%%", document.location.origin + document.location.pathname));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for(const index of rules_remove.sort((a, b) => b > a ? 1 : 0)) {
|
||||||
|
if(css.removeRule)
|
||||||
|
css.removeRule(index);
|
||||||
|
else
|
||||||
|
css.deleteRule(index);
|
||||||
|
}
|
||||||
|
for(const rule of rules_add)
|
||||||
|
css.insertRule(rule, rules_remove[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(config.verbose) console.debug("Style sheet %o loaded", url);
|
||||||
|
setTimeout(resolve, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById("style").appendChild(tag);
|
||||||
|
tag.href = url;
|
||||||
|
})).then(result => {
|
||||||
|
/* cleanup memory */
|
||||||
|
_style_promises[url] = Promise.resolve(); /* this promise does not holds the whole script tag and other memory */
|
||||||
|
return _style_promises[url];
|
||||||
|
}).catch(error => {
|
||||||
|
/* cleanup memory */
|
||||||
|
_style_promises[url] = Promise.reject(error); /* this promise does not holds the whole script tag and other memory */
|
||||||
|
return _style_promises[url];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Options {
|
||||||
|
cache_tag?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function load(path: SourcePath, options: Options) : Promise<void> {
|
||||||
|
if(Array.isArray(path)) { //We have fallback scripts
|
||||||
|
return load(path[0], options).catch(error => {
|
||||||
|
if(error instanceof LoadSyntaxError)
|
||||||
|
return Promise.reject(error);
|
||||||
|
|
||||||
|
if(path.length > 1)
|
||||||
|
return load(path.slice(1), options);
|
||||||
|
|
||||||
|
return Promise.reject(error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const source = typeof(path) === "string" ? {url: path, depends: []} : path;
|
||||||
|
if(source.url.length == 0) return Promise.resolve();
|
||||||
|
|
||||||
|
/* await depends */
|
||||||
|
for(const depend of source.depends) {
|
||||||
|
if(!_style_promises[depend])
|
||||||
|
throw "Missing dependency " + depend;
|
||||||
|
await _style_promises[depend];
|
||||||
|
}
|
||||||
|
await load_style_url(source.url + (options.cache_tag || ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MultipleOptions = Options | ParallelOptions;
|
||||||
|
export async function load_multiple(paths: SourcePath[], options: MultipleOptions) : Promise<void> {
|
||||||
|
const result = await load_parallel<SourcePath>(paths, e => load(e, options), e => script_name(e, false), options);
|
||||||
|
if(result.failed.length > 0) {
|
||||||
|
if(config.error) {
|
||||||
|
console.error("Failed to load the following style sheets:");
|
||||||
|
for(const style of result.failed) {
|
||||||
|
const sname = script_name(style.request, false);
|
||||||
|
if(style.error instanceof LoadSyntaxError) {
|
||||||
|
console.log(" - %s: %o", sname, style.error.source);
|
||||||
|
} else {
|
||||||
|
console.log(" - %s: %o", sname, style.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
critical_error("Failed to load style " + script_name(result.failed[0].request, true) + " <br>" + "View the browser console for more information!");
|
||||||
|
throw "failed to load style " + script_name(result.failed[0].request, false);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
import {config, critical_error, SourcePath} from "./loader";
|
||||||
|
import {load_parallel, LoadSyntaxError, ParallelOptions, script_name} from "./utils";
|
||||||
|
|
||||||
|
let _template_promises: {[key: string]: Promise<void>} = {};
|
||||||
|
|
||||||
|
function load_template_url(url: string) : Promise<void> {
|
||||||
|
if(typeof _template_promises[url] === "object")
|
||||||
|
return _template_promises[url];
|
||||||
|
|
||||||
|
return (_template_promises[url] = (async () => {
|
||||||
|
const response = await $.ajax(url);
|
||||||
|
|
||||||
|
let node = document.createElement("html");
|
||||||
|
node.innerHTML = response;
|
||||||
|
let tags: HTMLCollection;
|
||||||
|
if(node.getElementsByTagName("body").length > 0)
|
||||||
|
tags = node.getElementsByTagName("body")[0].children;
|
||||||
|
else
|
||||||
|
tags = node.children;
|
||||||
|
|
||||||
|
let root = document.getElementById("templates");
|
||||||
|
if(!root) {
|
||||||
|
critical_error("Failed to find template tag!");
|
||||||
|
throw "Failed to find template tag";
|
||||||
|
}
|
||||||
|
while(tags.length > 0){
|
||||||
|
let tag = tags.item(0);
|
||||||
|
root.appendChild(tag);
|
||||||
|
|
||||||
|
}
|
||||||
|
})()).then(result => {
|
||||||
|
/* cleanup memory */
|
||||||
|
_template_promises[url] = Promise.resolve(); /* this promise does not holds the whole script tag and other memory */
|
||||||
|
return _template_promises[url];
|
||||||
|
}).catch(error => {
|
||||||
|
/* cleanup memory */
|
||||||
|
_template_promises[url] = Promise.reject(error); /* this promise does not holds the whole script tag and other memory */
|
||||||
|
return _template_promises[url];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Options {
|
||||||
|
cache_tag?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function load(path: SourcePath, options: Options) : Promise<void> {
|
||||||
|
if(Array.isArray(path)) { //We have fallback scripts
|
||||||
|
return load(path[0], options).catch(error => {
|
||||||
|
if(error instanceof LoadSyntaxError)
|
||||||
|
return Promise.reject(error);
|
||||||
|
|
||||||
|
if(path.length > 1)
|
||||||
|
return load(path.slice(1), options);
|
||||||
|
|
||||||
|
return Promise.reject(error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const source = typeof(path) === "string" ? {url: path, depends: []} : path;
|
||||||
|
if(source.url.length == 0) return Promise.resolve();
|
||||||
|
|
||||||
|
/* await depends */
|
||||||
|
for(const depend of source.depends) {
|
||||||
|
if(!_template_promises[depend])
|
||||||
|
throw "Missing dependency " + depend;
|
||||||
|
await _template_promises[depend];
|
||||||
|
}
|
||||||
|
await load_template_url(source.url + (options.cache_tag || ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MultipleOptions = Options | ParallelOptions;
|
||||||
|
export async function load_multiple(paths: SourcePath[], options: MultipleOptions) : Promise<void> {
|
||||||
|
const result = await load_parallel<SourcePath>(paths, e => load(e, options), e => script_name(e, false), options);
|
||||||
|
if(result.failed.length > 0) {
|
||||||
|
if(config.error) {
|
||||||
|
console.error("Failed to load the following template files:");
|
||||||
|
for(const style of result.failed) {
|
||||||
|
const sname = script_name(style.request, false);
|
||||||
|
if(style.error instanceof LoadSyntaxError) {
|
||||||
|
console.log(" - %s: %o", sname, style.error.source);
|
||||||
|
} else {
|
||||||
|
console.log(" - %s: %o", sname, style.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
critical_error("Failed to load template file " + script_name(result.failed[0].request, true) + " <br>" + "View the browser console for more information!");
|
||||||
|
throw "failed to load template file " + script_name(result.failed[0].request, false);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
import {SourcePath} from "./loader";
|
||||||
|
import {Options} from "./script_loader";
|
||||||
|
|
||||||
|
export class LoadSyntaxError {
|
||||||
|
readonly source: any;
|
||||||
|
constructor(source: any) {
|
||||||
|
this.source = source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function script_name(path: SourcePath, html: boolean) {
|
||||||
|
if(Array.isArray(path)) {
|
||||||
|
let buffer = "";
|
||||||
|
let _or = " or ";
|
||||||
|
for(let entry of path)
|
||||||
|
buffer += _or + script_name(entry, html);
|
||||||
|
return buffer.slice(_or.length);
|
||||||
|
} else if(typeof(path) === "string")
|
||||||
|
return html ? "<code>" + path + "</code>" : path;
|
||||||
|
else
|
||||||
|
return html ? "<code>" + path.url + "</code>" : path.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ParallelOptions extends Options {
|
||||||
|
max_parallel_requests?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ParallelResult<T> {
|
||||||
|
succeeded: T[];
|
||||||
|
failed: {
|
||||||
|
request: T,
|
||||||
|
error: T
|
||||||
|
}[],
|
||||||
|
|
||||||
|
skipped: T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function load_parallel<T>(requests: T[], executor: (_: T) => Promise<void>, stringify: (_: T) => string, options: ParallelOptions) : Promise<ParallelResult<T>> {
|
||||||
|
const result: ParallelResult<T> = { failed: [], succeeded: [], skipped: [] };
|
||||||
|
const pending_requests = requests.slice(0).reverse(); /* we're only able to pop from the back */
|
||||||
|
const current_requests = {};
|
||||||
|
|
||||||
|
while (pending_requests.length > 0) {
|
||||||
|
while(typeof options.max_parallel_requests !== "number" || options.max_parallel_requests <= 0 || Object.keys(current_requests).length < options.max_parallel_requests) {
|
||||||
|
const script = pending_requests.pop();
|
||||||
|
const name = stringify(script);
|
||||||
|
|
||||||
|
current_requests[name] = executor(script).catch(e => result.failed.push({ request: script, error: e })).then(() => {
|
||||||
|
delete current_requests[name];
|
||||||
|
});
|
||||||
|
if(pending_requests.length == 0) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Wait 'till a new "slot" for downloading is free.
|
||||||
|
* This should also not throw because any errors will be caught before.
|
||||||
|
*/
|
||||||
|
await Promise.race(Object.keys(current_requests).map(e => current_requests[e]));
|
||||||
|
if(result.failed.length > 0)
|
||||||
|
break; /* finish loading the other requests and than show the error */
|
||||||
|
}
|
||||||
|
await Promise.all(Object.keys(current_requests).map(e => current_requests[e]));
|
||||||
|
result.skipped.push(...pending_requests);
|
||||||
|
return result;
|
||||||
|
}
|
|
@ -73,12 +73,6 @@ export type DependSource = {
|
||||||
depends: string[];
|
depends: string[];
|
||||||
}
|
}
|
||||||
export type SourcePath = string | DependSource | string[];
|
export type SourcePath = string | DependSource | string[];
|
||||||
export function load_script(path: SourcePath) : Promise<void>;
|
|
||||||
export function load_scripts(paths: SourcePath[]) : Promise<void>;
|
|
||||||
export function load_style(path: SourcePath) : Promise<void>;
|
|
||||||
export function load_styles(paths: SourcePath[]) : Promise<void>;
|
|
||||||
export function load_template(path: SourcePath) : Promise<void>;
|
|
||||||
export function load_templates(paths: SourcePath[]) : Promise<void>;
|
|
||||||
export type ErrorHandler = (message: string, detail: string) => void;
|
export type ErrorHandler = (message: string, detail: string) => void;
|
||||||
export function critical_error(message: string, detail?: string);
|
export function critical_error(message: string, detail?: string);
|
||||||
export function critical_error_handler(handler?: ErrorHandler, override?: boolean);
|
export function critical_error_handler(handler?: ErrorHandler, override?: boolean);
|
||||||
|
|
|
@ -54,9 +54,9 @@ module.exports = {
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
filename: 'loader.js',
|
filename: 'loader.js',
|
||||||
path: path.resolve(__dirname, '../dist'),
|
path: path.resolve(__dirname, 'dist'),
|
||||||
library: "loader",
|
library: "loader",
|
||||||
//libraryTarget: "umd" //"var" | "assign" | "this" | "window" | "self" | "global" | "commonjs" | "commonjs2" | "commonjs-module" | "amd" | "amd-require" | "umd" | "umd2" | "jsonp" | "system"
|
libraryTarget: "window" //"var" | "assign" | "this" | "window" | "self" | "global" | "commonjs" | "commonjs2" | "commonjs-module" | "amd" | "amd-require" | "umd" | "umd2" | "jsonp" | "system"
|
||||||
},
|
},
|
||||||
optimization: { }
|
optimization: { }
|
||||||
};
|
};
|
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
|
@ -34,11 +34,17 @@
|
||||||
"@types/react-dom": "^16.9.5",
|
"@types/react-dom": "^16.9.5",
|
||||||
"@types/sha256": "^0.2.0",
|
"@types/sha256": "^0.2.0",
|
||||||
"@types/websocket": "0.0.40",
|
"@types/websocket": "0.0.40",
|
||||||
|
"chunk-manifest-webpack-plugin": "^1.1.2",
|
||||||
"clean-css": "^4.2.1",
|
"clean-css": "^4.2.1",
|
||||||
|
"clean-webpack-plugin": "^3.0.0",
|
||||||
"css-loader": "^3.4.2",
|
"css-loader": "^3.4.2",
|
||||||
"csso-cli": "^2.0.2",
|
"csso-cli": "^2.0.2",
|
||||||
|
"exports-loader": "^0.7.0",
|
||||||
|
"file-loader": "^6.0.0",
|
||||||
"fs-extra": "latest",
|
"fs-extra": "latest",
|
||||||
"gulp": "^4.0.2",
|
"gulp": "^4.0.2",
|
||||||
|
"html-loader": "^1.0.0",
|
||||||
|
"html-webpack-plugin": "^4.0.3",
|
||||||
"mime-types": "^2.1.24",
|
"mime-types": "^2.1.24",
|
||||||
"mini-css-extract-plugin": "^0.9.0",
|
"mini-css-extract-plugin": "^0.9.0",
|
||||||
"mkdirp": "^0.5.1",
|
"mkdirp": "^0.5.1",
|
||||||
|
@ -53,7 +59,10 @@
|
||||||
"typescript": "3.6.5",
|
"typescript": "3.6.5",
|
||||||
"wat2wasm": "^1.0.2",
|
"wat2wasm": "^1.0.2",
|
||||||
"webpack": "^4.42.1",
|
"webpack": "^4.42.1",
|
||||||
"webpack-cli": "^3.3.11"
|
"webpack-bundle-analyzer": "^3.6.1",
|
||||||
|
"webpack-cli": "^3.3.11",
|
||||||
|
"webpack-manifest-plugin": "^2.2.0",
|
||||||
|
"worker-plugin": "^4.0.2"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import {settings, Settings} from "tc-shared/settings";
|
import {settings, Settings} from "tc-shared/settings";
|
||||||
import * as loader from "tc-loader";
|
import * as loader from "tc-loader";
|
||||||
|
import * as log from "tc-shared/log";
|
||||||
import {LogCategory} from "tc-shared/log";
|
import {LogCategory} from "tc-shared/log";
|
||||||
import * as bipc from "tc-shared/BrowserIPC";
|
import * as bipc from "tc-shared/BrowserIPC";
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"paths": {
|
"paths": {
|
||||||
"*": ["shared/declarations/*"],
|
"*": ["shared/declarations/*"],
|
||||||
"tc-shared/*": ["shared/js/*"],
|
"tc-shared/*": ["shared/js/*"],
|
||||||
|
"tc-backend/web/*": ["web/js/*"], /* specific web part */
|
||||||
"tc-backend/*": ["shared/backend.d/*"],
|
"tc-backend/*": ["shared/backend.d/*"],
|
||||||
"tc-loader": ["loader/exports/loader.d.ts"]
|
"tc-loader": ["loader/exports/loader.d.ts"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import {BasicCodec} from "./BasicCodec";
|
import {BasicCodec} from "./BasicCodec";
|
||||||
|
|
||||||
export class CodecWrapperRaw extends BasicCodec {
|
export class CodecRaw extends BasicCodec {
|
||||||
converterRaw: any;
|
converterRaw: any;
|
||||||
converter: Uint8Array;
|
converter: Uint8Array;
|
||||||
bufferSize: number = 4096 * 4;
|
bufferSize: number = 4096 * 4;
|
|
@ -1,135 +1,37 @@
|
||||||
import {BasicCodec} from "./BasicCodec";
|
import {BasicCodec} from "./BasicCodec";
|
||||||
import {CodecType} from "./Codec";
|
import {CodecType} from "./Codec";
|
||||||
import {LogCategory} from "tc-shared/log";
|
|
||||||
import * as log from "tc-shared/log";
|
import * as log from "tc-shared/log";
|
||||||
import {settings} from "tc-shared/settings";
|
import {LogCategory} from "tc-shared/log";
|
||||||
|
|
||||||
|
interface ExecuteResult {
|
||||||
|
result?: any;
|
||||||
|
error?: string;
|
||||||
|
|
||||||
|
success: boolean;
|
||||||
|
|
||||||
|
timings: {
|
||||||
|
upstream: number;
|
||||||
|
downstream: number;
|
||||||
|
handle: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class CodecWrapperWorker extends BasicCodec {
|
export class CodecWrapperWorker extends BasicCodec {
|
||||||
private _worker: Worker;
|
private _worker: Worker;
|
||||||
private _workerListener: {token: string, resolve: (data: any) => void}[] = [];
|
|
||||||
private _workerCallbackToken = "callback_token";
|
|
||||||
private _workerTokeIndex: number = 0;
|
|
||||||
type: CodecType;
|
|
||||||
|
|
||||||
private _initialized: boolean = false;
|
private _initialized: boolean = false;
|
||||||
private _workerCallbackResolve: () => any;
|
private _initialize_promise: Promise<Boolean>;
|
||||||
private _workerCallbackReject: ($: any) => any;
|
|
||||||
|
|
||||||
private _initializePromise: Promise<Boolean>;
|
private _token_index: number = 0;
|
||||||
name(): string {
|
readonly type: CodecType;
|
||||||
return "Worker for " + CodecType[this.type] + " Channels " + this.channelCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
initialise() : Promise<Boolean> {
|
private pending_executes: {[key: string]: {
|
||||||
if(this._initializePromise) return this._initializePromise;
|
timeout?: any;
|
||||||
return this._initializePromise = this.spawnWorker().then(() => new Promise<Boolean>((resolve, reject) => {
|
|
||||||
const token = this.generateToken();
|
|
||||||
this.sendWorkerMessage({
|
|
||||||
command: "initialise",
|
|
||||||
type: this.type,
|
|
||||||
channelCount: this.channelCount,
|
|
||||||
token: token
|
|
||||||
});
|
|
||||||
|
|
||||||
this._workerListener.push({
|
timestamp_send: number,
|
||||||
token: token,
|
|
||||||
resolve: data => {
|
|
||||||
this._initialized = data["success"] == true;
|
|
||||||
if(data["success"] == true)
|
|
||||||
resolve();
|
|
||||||
else
|
|
||||||
reject(data.message);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
initialized() : boolean {
|
resolve: (_: ExecuteResult) => void;
|
||||||
return this._initialized;
|
reject: (_: any) => void;
|
||||||
}
|
}} = {};
|
||||||
|
|
||||||
deinitialise() {
|
|
||||||
this.sendWorkerMessage({
|
|
||||||
command: "deinitialise"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
decode(data: Uint8Array): Promise<AudioBuffer> {
|
|
||||||
let token = this.generateToken();
|
|
||||||
let result = new Promise<AudioBuffer>((resolve, reject) => {
|
|
||||||
this._workerListener.push(
|
|
||||||
{
|
|
||||||
token: token,
|
|
||||||
resolve: (data) => {
|
|
||||||
if(data.success) {
|
|
||||||
let array = new Float32Array(data.dataLength);
|
|
||||||
for(let index = 0; index < array.length; index++)
|
|
||||||
array[index] = data.data[index];
|
|
||||||
|
|
||||||
let audioBuf = this._audioContext.createBuffer(this.channelCount, array.length / this.channelCount, this._codecSampleRate);
|
|
||||||
for (let channel = 0; channel < this.channelCount; channel++) {
|
|
||||||
for (let offset = 0; offset < audioBuf.length; offset++) {
|
|
||||||
audioBuf.getChannelData(channel)[offset] = array[channel + offset * this.channelCount];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resolve(audioBuf);
|
|
||||||
} else {
|
|
||||||
reject(data.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
this.sendWorkerMessage({
|
|
||||||
command: "decodeSamples",
|
|
||||||
token: token,
|
|
||||||
data: data,
|
|
||||||
dataLength: data.length
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
encode(data: AudioBuffer) : Promise<Uint8Array> {
|
|
||||||
let token = this.generateToken();
|
|
||||||
let result = new Promise<Uint8Array>((resolve, reject) => {
|
|
||||||
this._workerListener.push(
|
|
||||||
{
|
|
||||||
token: token,
|
|
||||||
resolve: (data) => {
|
|
||||||
if(data.success) {
|
|
||||||
let array = new Uint8Array(data.dataLength);
|
|
||||||
for(let index = 0; index < array.length; index++)
|
|
||||||
array[index] = data.data[index];
|
|
||||||
resolve(array);
|
|
||||||
} else {
|
|
||||||
reject(data.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
let buffer = new Float32Array(this.channelCount * data.length);
|
|
||||||
for (let offset = 0; offset < data.length; offset++) {
|
|
||||||
for (let channel = 0; channel < this.channelCount; channel++)
|
|
||||||
buffer[offset * this.channelCount + channel] = data.getChannelData(channel)[offset];
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sendWorkerMessage({
|
|
||||||
command: "encodeSamples",
|
|
||||||
token: token,
|
|
||||||
data: buffer,
|
|
||||||
dataLength: buffer.length
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
reset() : boolean {
|
|
||||||
this.sendWorkerMessage({
|
|
||||||
command: "reset"
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(type: CodecType) {
|
constructor(type: CodecType) {
|
||||||
super(48000);
|
super(48000);
|
||||||
|
@ -146,35 +48,92 @@ export class CodecWrapperWorker extends BasicCodec {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateToken() {
|
name(): string {
|
||||||
return this._workerTokeIndex++ + "_token";
|
return "Worker for " + CodecType[this.type] + " Channels " + this.channelCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendWorkerMessage(message: any, transfare?: any[]) {
|
async initialise() : Promise<Boolean> {
|
||||||
message["timestamp"] = Date.now();
|
if(this._initialized) return;
|
||||||
this._worker.postMessage(message, transfare as any);
|
|
||||||
|
this._initialize_promise = this.spawn_worker().then(() => this.execute("initialise", {
|
||||||
|
type: this.type,
|
||||||
|
channelCount: this.channelCount,
|
||||||
|
})).then(result => {
|
||||||
|
if(result.success)
|
||||||
|
return Promise.resolve(true);
|
||||||
|
|
||||||
|
log.error(LogCategory.VOICE, tr("Failed to initialize codec %s: %s"), CodecType[this.type], result.error);
|
||||||
|
return Promise.reject(result.error);
|
||||||
|
});
|
||||||
|
|
||||||
|
this._initialized = true;
|
||||||
|
await this._initialize_promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
private onWorkerMessage(message: any) {
|
initialized() : boolean {
|
||||||
|
return this._initialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
deinitialise() {
|
||||||
|
this.execute("deinitialise", {});
|
||||||
|
this._initialized = false;
|
||||||
|
this._initialize_promise = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async decode(data: Uint8Array): Promise<AudioBuffer> {
|
||||||
|
const result = await this.execute("decodeSamples", { data: data, length: data.length });
|
||||||
|
if(result.timings.downstream > 5 || result.timings.upstream > 5 || result.timings.handle > 5)
|
||||||
|
log.warn(LogCategory.VOICE, tr("Worker message stock time: {downstream: %dms, handle: %dms, upstream: %dms}"), result.timings.downstream, result.timings.handle, result.timings.upstream);
|
||||||
|
|
||||||
|
if(!result.success) throw result.error || tr("unknown decode error");
|
||||||
|
|
||||||
|
let array = new Float32Array(result.result.length);
|
||||||
|
for(let index = 0; index < array.length; index++)
|
||||||
|
array[index] = result.result.data[index];
|
||||||
|
|
||||||
|
let audioBuf = this._audioContext.createBuffer(this.channelCount, array.length / this.channelCount, this._codecSampleRate);
|
||||||
|
for (let channel = 0; channel < this.channelCount; channel++) {
|
||||||
|
for (let offset = 0; offset < audioBuf.length; offset++) {
|
||||||
|
audioBuf.getChannelData(channel)[offset] = array[channel + offset * this.channelCount];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return audioBuf;
|
||||||
|
}
|
||||||
|
|
||||||
|
async encode(data: AudioBuffer) : Promise<Uint8Array> {
|
||||||
|
let buffer = new Float32Array(this.channelCount * data.length);
|
||||||
|
for (let offset = 0; offset < data.length; offset++) {
|
||||||
|
for (let channel = 0; channel < this.channelCount; channel++)
|
||||||
|
buffer[offset * this.channelCount + channel] = data.getChannelData(channel)[offset];
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.execute("encodeSamples", { data: buffer, length: buffer.length });
|
||||||
|
if(result.timings.downstream > 5 || result.timings.upstream > 5)
|
||||||
|
log.warn(LogCategory.VOICE, tr("Worker message stock time: {downstream: %dms, handle: %dms, upstream: %dms}"), result.timings.downstream, result.timings.handle, result.timings.upstream);
|
||||||
|
if(!result.success) throw result.error || tr("unknown encode error");
|
||||||
|
|
||||||
|
let array = new Uint8Array(result.result.length);
|
||||||
|
for(let index = 0; index < array.length; index++)
|
||||||
|
array[index] = result.result.data[index];
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() : boolean {
|
||||||
|
//TODO: Await result!
|
||||||
|
this.execute("reset", {});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handle_worker_message(message: any) {
|
||||||
if(!message["token"]) {
|
if(!message["token"]) {
|
||||||
log.error(LogCategory.VOICE, tr("Invalid worker token!"));
|
log.error(LogCategory.VOICE, tr("Invalid worker token!"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(message["token"] == this._workerCallbackToken) {
|
if(message["token"] === "notify") {
|
||||||
if(message["type"] == "loaded") {
|
/* currently not really used */
|
||||||
log.info(LogCategory.VOICE, tr("[Codec] Got worker init response: Success: %o Message: %o"), message["success"], message["message"]);
|
if(message["type"] == "chatmessage_server") {
|
||||||
if(message["success"]) {
|
|
||||||
if(this._workerCallbackResolve)
|
|
||||||
this._workerCallbackResolve();
|
|
||||||
} else {
|
|
||||||
if(this._workerCallbackReject)
|
|
||||||
this._workerCallbackReject(message["message"]);
|
|
||||||
}
|
|
||||||
this._workerCallbackReject = undefined;
|
|
||||||
this._workerCallbackResolve = undefined;
|
|
||||||
return;
|
|
||||||
} else if(message["type"] == "chatmessage_server") {
|
|
||||||
//FIXME?
|
//FIXME?
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -182,29 +141,69 @@ export class CodecWrapperWorker extends BasicCodec {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* lets warn on general packets. Control packets are allowed to "stuck" a bit longer */
|
const request = this.pending_executes[message["token"]];
|
||||||
if(Date.now() - message["timestamp"] > 5)
|
if(typeof request !== "object") {
|
||||||
log.warn(LogCategory.VOICE, tr("Worker message stock time: %d"), Date.now() - message["timestamp"]);
|
log.error(LogCategory.VOICE, tr("Received worker execute result for unknown token (%s)"), message["token"]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
delete this.pending_executes[message["token"]];
|
||||||
|
|
||||||
for(let entry of this._workerListener) {
|
const result: ExecuteResult = {
|
||||||
if(entry.token == message["token"]) {
|
success: message["success"],
|
||||||
entry.resolve(message);
|
error: message["error"],
|
||||||
this._workerListener.remove(entry);
|
result: message["result"],
|
||||||
return;
|
timings: {
|
||||||
|
downstream: message["timestamp_received"] - request.timestamp_send,
|
||||||
|
handle: message["timestamp_send"] - message["timestamp_received"],
|
||||||
|
upstream: Date.now() - message["timestamp_send"]
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
clearTimeout(request.timeout);
|
||||||
|
request.resolve(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handle_worker_error(error: any) {
|
||||||
|
log.error(LogCategory.VOICE, tr("Received error from codec worker. Closing worker."));
|
||||||
|
for(const token of Object.keys(this.pending_executes)) {
|
||||||
|
this.pending_executes[token].reject(error);
|
||||||
|
delete this.pending_executes[token];
|
||||||
}
|
}
|
||||||
|
|
||||||
log.error(LogCategory.VOICE, tr("Could not find worker token entry! (%o)"), message["token"]);
|
this._worker = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private spawnWorker() : Promise<Boolean> {
|
private execute(command: string, data: any, timeout?: number) : Promise<ExecuteResult> {
|
||||||
return new Promise<Boolean>((resolve, reject) => {
|
return new Promise<any>((resolve, reject) => {
|
||||||
this._workerCallbackReject = reject;
|
if(!this._worker) {
|
||||||
this._workerCallbackResolve = resolve;
|
reject(tr("worker does not exists"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this._worker = new Worker(settings.static("worker_directory", "js/workers/") + "WorkerCodec.js");
|
const token = this._token_index++ + "_token";
|
||||||
this._worker.onmessage = event => this.onWorkerMessage(event.data);
|
|
||||||
this._worker.onerror = (error: ErrorEvent) => reject("Failed to load worker (" + error.message + ")"); //TODO tr
|
const payload = {
|
||||||
|
token: token,
|
||||||
|
command: command,
|
||||||
|
data: data,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.pending_executes[token] = {
|
||||||
|
timeout: typeof timeout === "number" ? setTimeout(() => reject(tr("timeout for command ") + command), timeout) : undefined,
|
||||||
|
resolve: resolve,
|
||||||
|
reject: reject,
|
||||||
|
timestamp_send: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
this._worker.postMessage(payload);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async spawn_worker() : Promise<void> {
|
||||||
|
this._worker = new Worker("tc-backend/web/workers/codec", { type: "module" });
|
||||||
|
this._worker.onmessage = event => this.handle_worker_message(event.data);
|
||||||
|
this._worker.onerror = event => this.handle_worker_error(event.error);
|
||||||
|
|
||||||
|
const result = await this.execute("global-initialize", {}, 15000);
|
||||||
|
if(!result.success) throw result.error;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,7 +1,8 @@
|
||||||
const prefix = "[CodecWorker] ";
|
import {CodecType} from "tc-backend/web/codec/Codec";
|
||||||
const workerCallbackToken = "callback_token";
|
|
||||||
|
|
||||||
interface CodecWorker {
|
const prefix = "[CodecWorker] ";
|
||||||
|
|
||||||
|
export interface CodecWorker {
|
||||||
name();
|
name();
|
||||||
initialise?() : string;
|
initialise?() : string;
|
||||||
deinitialise();
|
deinitialise();
|
||||||
|
@ -11,78 +12,17 @@ interface CodecWorker {
|
||||||
reset();
|
reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
let codecInstance: CodecWorker;
|
let supported_types = {};
|
||||||
|
export function register_codec(type: CodecType, allocator: (options?: any) => Promise<CodecWorker>) {
|
||||||
|
supported_types[type] = allocator;
|
||||||
|
}
|
||||||
|
|
||||||
onmessage = function(e: MessageEvent) {
|
let initialize_callback: () => Promise<true | string>;
|
||||||
let data = e.data;
|
export function set_initialize_callback(callback: () => Promise<true | string>) {
|
||||||
|
initialize_callback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
let res: any = {};
|
export let codecInstance: CodecWorker;
|
||||||
res.token = data.token;
|
|
||||||
res.success = false;
|
|
||||||
|
|
||||||
//console.log(prefix + " Got from main: %o", data);
|
|
||||||
switch (data.command) {
|
|
||||||
case "initialise":
|
|
||||||
let error;
|
|
||||||
console.log(prefix + "Got initialize for type " + CodecType[data.type as CodecType]);
|
|
||||||
switch (data.type as CodecType) {
|
|
||||||
case CodecType.OPUS_MUSIC:
|
|
||||||
codecInstance = new OpusWorker(2, OpusType.AUDIO);
|
|
||||||
break;
|
|
||||||
case CodecType.OPUS_VOICE:
|
|
||||||
codecInstance = new OpusWorker(1, OpusType.VOIP);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
error = "Could not find worker type!";
|
|
||||||
console.error("Could not resolve opus type!");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
error = error || codecInstance.initialise();
|
|
||||||
if(error)
|
|
||||||
res["message"] = error;
|
|
||||||
else
|
|
||||||
res["success"] = true;
|
|
||||||
break;
|
|
||||||
case "encodeSamples":
|
|
||||||
let encodeArray = new Float32Array(data.dataLength);
|
|
||||||
for(let index = 0; index < encodeArray.length; index++)
|
|
||||||
encodeArray[index] = data.data[index];
|
|
||||||
|
|
||||||
let encodeResult = codecInstance.encode(encodeArray);
|
|
||||||
|
|
||||||
if(typeof encodeResult === "string") {
|
|
||||||
res.message = encodeResult;
|
|
||||||
} else {
|
|
||||||
res.success = true;
|
|
||||||
res.data = encodeResult;
|
|
||||||
res.dataLength = encodeResult.length;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "decodeSamples":
|
|
||||||
let decodeArray = new Uint8Array(data.dataLength);
|
|
||||||
for(let index = 0; index < decodeArray.length; index++)
|
|
||||||
decodeArray[index] = data.data[index];
|
|
||||||
|
|
||||||
let decodeResult = codecInstance.decode(decodeArray);
|
|
||||||
|
|
||||||
if(typeof decodeResult === "string") {
|
|
||||||
res.message = decodeResult;
|
|
||||||
} else {
|
|
||||||
res.success = true;
|
|
||||||
res.data = decodeResult;
|
|
||||||
res.dataLength = decodeResult.length;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "reset":
|
|
||||||
codecInstance.reset();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.error(prefix + "Unknown type " + data.command);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(res.token && res.token.length > 0) sendMessage(res, e.origin);
|
|
||||||
};
|
|
||||||
|
|
||||||
function printMessageToServerTab(message: string) {
|
function printMessageToServerTab(message: string) {
|
||||||
/*
|
/*
|
||||||
|
@ -95,7 +35,105 @@ function printMessageToServerTab(message: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
declare function postMessage(message: any): void;
|
declare function postMessage(message: any): void;
|
||||||
function sendMessage(message: any, origin?: string){
|
function sendMessage(message: any, origin?: string) {
|
||||||
message["timestamp"] = Date.now();
|
message["timestamp"] = Date.now();
|
||||||
postMessage(message);
|
postMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let globally_initialized = false;
|
||||||
|
let global_initialize_result;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param command
|
||||||
|
* @param data
|
||||||
|
* @return string on error or object on success
|
||||||
|
*/
|
||||||
|
async function handle_message(command: string, data: any) : Promise<string | object> {
|
||||||
|
switch (command) {
|
||||||
|
case "global-initialize":
|
||||||
|
const init_result = globally_initialized ? global_initialize_result : await initialize_callback();
|
||||||
|
globally_initialized = true;
|
||||||
|
|
||||||
|
if(typeof init_result === "string")
|
||||||
|
return init_result;
|
||||||
|
|
||||||
|
return {};
|
||||||
|
case "initialise":
|
||||||
|
console.log(prefix + "Got initialize for type " + CodecType[data.type as CodecType]);
|
||||||
|
if(!supported_types[data.type])
|
||||||
|
return "type unsupported";
|
||||||
|
|
||||||
|
try {
|
||||||
|
codecInstance = await supported_types[data.type](data.options);
|
||||||
|
} catch(ex) {
|
||||||
|
console.error(prefix + "Failed to allocate codec: %o", ex);
|
||||||
|
return typeof ex === "string" ? ex : "failed to allocate codec";
|
||||||
|
}
|
||||||
|
|
||||||
|
const error = codecInstance.initialise();
|
||||||
|
if(error) return error;
|
||||||
|
|
||||||
|
return {};
|
||||||
|
case "encodeSamples":
|
||||||
|
let encodeArray = new Float32Array(data.length);
|
||||||
|
for(let index = 0; index < encodeArray.length; index++)
|
||||||
|
encodeArray[index] = data.data[index];
|
||||||
|
|
||||||
|
let encodeResult = codecInstance.encode(encodeArray);
|
||||||
|
if(typeof encodeResult === "string")
|
||||||
|
return encodeResult;
|
||||||
|
else
|
||||||
|
return { data: encodeResult, length: encodeResult.length };
|
||||||
|
case "decodeSamples":
|
||||||
|
let decodeArray = new Uint8Array(data.length);
|
||||||
|
for(let index = 0; index < decodeArray.length; index++)
|
||||||
|
decodeArray[index] = data.data[index];
|
||||||
|
|
||||||
|
let decodeResult = codecInstance.decode(decodeArray);
|
||||||
|
if(typeof decodeResult === "string")
|
||||||
|
return decodeResult;
|
||||||
|
else
|
||||||
|
return { data: decodeResult, length: decodeResult.length };
|
||||||
|
case "reset":
|
||||||
|
codecInstance.reset();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return "unknown command";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const handle_message_event = (e: MessageEvent) => {
|
||||||
|
const token = e.data.token;
|
||||||
|
const received = Date.now();
|
||||||
|
|
||||||
|
const send_result = result => {
|
||||||
|
const data = {};
|
||||||
|
if(typeof result === "object") {
|
||||||
|
data["result"] = result;
|
||||||
|
data["success"] = true;
|
||||||
|
} else if(typeof result === "string") {
|
||||||
|
data["error"] = result;
|
||||||
|
data["success"] = false;
|
||||||
|
} else {
|
||||||
|
data["error"] = "invalid result";
|
||||||
|
data["success"] = false;
|
||||||
|
}
|
||||||
|
data["token"] = token;
|
||||||
|
data["timestamp_received"] = received;
|
||||||
|
data["timestamp_send"] = Date.now();
|
||||||
|
|
||||||
|
sendMessage(data, e.origin);
|
||||||
|
};
|
||||||
|
handle_message(e.data.command, e.data.data).then(res => {
|
||||||
|
if(token) {
|
||||||
|
send_result(res);
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.warn("An error has been thrown while handing command %s: %o", e.data.command, error);
|
||||||
|
if(token) {
|
||||||
|
send_result(typeof error === "string" ? error : "unexpected exception has been thrown");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
addEventListener("message", handle_message_event);
|
|
@ -1,78 +1,82 @@
|
||||||
/// <reference path="CodecWorker.ts" />
|
import * as cworker from "./CodecWorker";
|
||||||
|
import {CodecType} from "tc-backend/web/codec/Codec";
|
||||||
|
import {CodecWorker} from "./CodecWorker";
|
||||||
|
|
||||||
|
const prefix = "OpusWorker";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
__init_em_module: ((Module: any) => void)[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.__init_em_module = self.__init_em_module || [];
|
||||||
|
|
||||||
const WASM_ERROR_MESSAGES = [
|
const WASM_ERROR_MESSAGES = [
|
||||||
'no native wasm support detected'
|
'no native wasm support detected'
|
||||||
];
|
];
|
||||||
|
|
||||||
this["Module"] = this["Module"] || ({} as any); /* its required to cast {} to any!*/
|
let Module;
|
||||||
|
self.__init_em_module.push(m => Module = m);
|
||||||
|
const runtime_initialize_promise = new Promise((resolve, reject) => {
|
||||||
|
self.__init_em_module.push(Module => {
|
||||||
|
const cleanup = () => {
|
||||||
|
Module['onRuntimeInitialized'] = undefined;
|
||||||
|
Module['onAbort'] = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
let initialized = false;
|
Module['onRuntimeInitialized'] = () => {
|
||||||
Module['onRuntimeInitialized'] = function() {
|
cleanup();
|
||||||
initialized = true;
|
resolve();
|
||||||
console.log(prefix + "Initialized!");
|
};
|
||||||
|
|
||||||
sendMessage({
|
Module['onAbort'] = error => {
|
||||||
token: workerCallbackToken,
|
cleanup();
|
||||||
type: "loaded",
|
|
||||||
success: true
|
let message;
|
||||||
})
|
if(error instanceof DOMException)
|
||||||
};
|
message = "DOMException (" + error.name + "): " + error.code + " => " + error.message;
|
||||||
|
else {
|
||||||
|
abort_message = error;
|
||||||
|
message = error;
|
||||||
|
if(error.indexOf("no binaryen method succeeded") != -1) {
|
||||||
|
for(const error of WASM_ERROR_MESSAGES) {
|
||||||
|
if(last_error_message.indexOf(error) != -1) {
|
||||||
|
message = "no native wasm support detected, but its required";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reject(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
let abort_message: string = undefined;
|
let abort_message: string = undefined;
|
||||||
let last_error_message: string;
|
let last_error_message: string;
|
||||||
|
self.__init_em_module.push(Module => {
|
||||||
|
Module['print'] = function() {
|
||||||
|
if(arguments.length == 1 && arguments[0] == abort_message)
|
||||||
|
return; /* we don't need to reprint the abort message! */
|
||||||
|
|
||||||
Module['print'] = function() {
|
console.log("Print: ", ...arguments);
|
||||||
if(arguments.length == 1 && arguments[0] == abort_message)
|
};
|
||||||
return; /* we don't need to reprint the abort message! */
|
|
||||||
console.log(...arguments);
|
|
||||||
};
|
|
||||||
|
|
||||||
Module['printErr'] = function() {
|
Module['printErr'] = function() {
|
||||||
if(arguments.length == 1 && arguments[0] == abort_message)
|
if(arguments.length == 1 && arguments[0] == abort_message)
|
||||||
return; /* we don't need to reprint the abort message! */
|
return; /* we don't need to reprint the abort message! */
|
||||||
|
|
||||||
last_error_message = arguments[0];
|
last_error_message = arguments[0];
|
||||||
for(const suppress of WASM_ERROR_MESSAGES)
|
for(const suppress of WASM_ERROR_MESSAGES)
|
||||||
if((arguments[0] as string).indexOf(suppress) != -1)
|
if((arguments[0] as string).indexOf(suppress) != -1)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
console.error(...arguments);
|
console.error("Error: ",...arguments);
|
||||||
};
|
};
|
||||||
|
|
||||||
Module['onAbort'] = (message: string | DOMException) => {
|
|
||||||
/* no native wasm support detected */
|
|
||||||
Module['onAbort'] = undefined;
|
|
||||||
|
|
||||||
if(message instanceof DOMException)
|
|
||||||
message = "DOMException (" + message.name + "): " + message.code + " => " + message.message;
|
|
||||||
else {
|
|
||||||
abort_message = message;
|
|
||||||
if(message.indexOf("no binaryen method succeeded") != -1)
|
|
||||||
for(const error of WASM_ERROR_MESSAGES)
|
|
||||||
if(last_error_message.indexOf(error) != -1) {
|
|
||||||
message = "no native wasm support detected, but its required";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sendMessage({
|
|
||||||
token: workerCallbackToken,
|
|
||||||
type: "loaded",
|
|
||||||
success: false,
|
|
||||||
message: message
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log("Node init!");
|
|
||||||
Module['locateFile'] = file => "../../wasm/" + file;
|
Module['locateFile'] = file => "../../wasm/" + file;
|
||||||
importScripts("../../wasm/TeaWeb-Worker-Codec-Opus.js");
|
});
|
||||||
} catch (e) {
|
|
||||||
if(typeof(Module['onAbort']) === "function") {
|
|
||||||
console.log(e);
|
|
||||||
Module['onAbort']("Failed to load native scripts");
|
|
||||||
} /* else the error had been already handled because its a WASM error */
|
|
||||||
}
|
|
||||||
|
|
||||||
enum OpusType {
|
enum OpusType {
|
||||||
VOIP = 2048,
|
VOIP = 2048,
|
||||||
|
@ -89,6 +93,7 @@ class OpusWorker implements CodecWorker {
|
||||||
private fn_decode: any;
|
private fn_decode: any;
|
||||||
private fn_encode: any;
|
private fn_encode: any;
|
||||||
private fn_reset: any;
|
private fn_reset: any;
|
||||||
|
private fn_error_message: any;
|
||||||
|
|
||||||
private bufferSize = 4096 * 2;
|
private bufferSize = 4096 * 2;
|
||||||
private encodeBufferRaw: any;
|
private encodeBufferRaw: any;
|
||||||
|
@ -106,11 +111,12 @@ class OpusWorker implements CodecWorker {
|
||||||
}
|
}
|
||||||
|
|
||||||
initialise?() : string {
|
initialise?() : string {
|
||||||
this.fn_newHandle = cwrap("codec_opus_createNativeHandle", "number", ["number", "number"]);
|
this.fn_newHandle = Module.cwrap("codec_opus_createNativeHandle", "number", ["number", "number"]);
|
||||||
this.fn_decode = cwrap("codec_opus_decode", "number", ["number", "number", "number", "number"]);
|
this.fn_decode = Module.cwrap("codec_opus_decode", "number", ["number", "number", "number", "number"]);
|
||||||
/* codec_opus_decode(handle, buffer, length, maxlength) */
|
/* codec_opus_decode(handle, buffer, length, maxlength) */
|
||||||
this.fn_encode = cwrap("codec_opus_encode", "number", ["number", "number", "number", "number"]);
|
this.fn_encode = Module.cwrap("codec_opus_encode", "number", ["number", "number", "number", "number"]);
|
||||||
this.fn_reset = cwrap("codec_opus_reset", "number", ["number"]);
|
this.fn_reset = Module.cwrap("codec_opus_reset", "number", ["number"]);
|
||||||
|
this.fn_error_message = Module.cwrap("opus_error_message", "string", ["number"]);
|
||||||
|
|
||||||
this.nativeHandle = this.fn_newHandle(this.channelCount, this.type);
|
this.nativeHandle = this.fn_newHandle(this.channelCount, this.type);
|
||||||
|
|
||||||
|
@ -127,12 +133,8 @@ class OpusWorker implements CodecWorker {
|
||||||
decode(data: Uint8Array): Float32Array | string {
|
decode(data: Uint8Array): Float32Array | string {
|
||||||
if (data.byteLength > this.decodeBuffer.byteLength) return "Data to long!";
|
if (data.byteLength > this.decodeBuffer.byteLength) return "Data to long!";
|
||||||
this.decodeBuffer.set(data);
|
this.decodeBuffer.set(data);
|
||||||
//console.log("decode(" + data.length + ")");
|
|
||||||
//console.log(data);
|
|
||||||
let result = this.fn_decode(this.nativeHandle, this.decodeBuffer.byteOffset, data.byteLength, this.decodeBuffer.byteLength);
|
let result = this.fn_decode(this.nativeHandle, this.decodeBuffer.byteOffset, data.byteLength, this.decodeBuffer.byteLength);
|
||||||
if (result < 0) {
|
if (result < 0) return this.fn_error_message(result);
|
||||||
return "invalid result on decode (" + result + ")";
|
|
||||||
}
|
|
||||||
return Module.HEAPF32.slice(this.decodeBuffer.byteOffset / 4, (this.decodeBuffer.byteOffset / 4) + (result * this.channelCount));
|
return Module.HEAPF32.slice(this.decodeBuffer.byteOffset / 4, (this.decodeBuffer.byteOffset / 4) + (result * this.channelCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,9 +142,7 @@ class OpusWorker implements CodecWorker {
|
||||||
this.encodeBuffer.set(data);
|
this.encodeBuffer.set(data);
|
||||||
|
|
||||||
let result = this.fn_encode(this.nativeHandle, this.encodeBuffer.byteOffset, data.length, this.encodeBuffer.byteLength);
|
let result = this.fn_encode(this.nativeHandle, this.encodeBuffer.byteOffset, data.length, this.encodeBuffer.byteLength);
|
||||||
if (result < 0) {
|
if (result < 0) return this.fn_error_message(result);
|
||||||
return "invalid result on encode (" + result + ")";
|
|
||||||
}
|
|
||||||
let buf = Module.HEAP8.slice(this.encodeBuffer.byteOffset, this.encodeBuffer.byteOffset + result);
|
let buf = Module.HEAP8.slice(this.encodeBuffer.byteOffset, this.encodeBuffer.byteOffset + result);
|
||||||
return Uint8Array.from(buf);
|
return Uint8Array.from(buf);
|
||||||
}
|
}
|
||||||
|
@ -152,3 +152,24 @@ class OpusWorker implements CodecWorker {
|
||||||
this.fn_reset(this.nativeHandle);
|
this.fn_reset(this.nativeHandle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
cworker.register_codec(CodecType.OPUS_MUSIC, async () => new OpusWorker(2, OpusType.AUDIO));
|
||||||
|
cworker.register_codec(CodecType.OPUS_VOICE, async () => new OpusWorker(1, OpusType.VOIP));
|
||||||
|
|
||||||
|
cworker.set_initialize_callback(async () => {
|
||||||
|
try {
|
||||||
|
require("tc-generated/codec/opus");
|
||||||
|
} catch (e) {
|
||||||
|
if(Module) {
|
||||||
|
if(typeof(Module['onAbort']) === "function") {
|
||||||
|
Module['onAbort']("Failed to load native scripts");
|
||||||
|
} /* else the error had been already handled because its a WASM error */
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!Module)
|
||||||
|
throw "Missing module handle";
|
||||||
|
|
||||||
|
await runtime_initialize_promise;
|
||||||
|
return true;
|
||||||
|
});
|
|
@ -0,0 +1 @@
|
||||||
|
require("./OpusCodec");
|
|
@ -1,16 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"module": "none",
|
|
||||||
"target": "es6",
|
|
||||||
"sourceMap": true,
|
|
||||||
"outFile": "WorkerCodec.js"
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"../../types/"
|
|
||||||
],
|
|
||||||
"files": [
|
|
||||||
"codec/CodecWorker.ts",
|
|
||||||
"codec/OpusCodec.ts",
|
|
||||||
"../codec/Codec.ts"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,6 +1,10 @@
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const webpack = require("webpack");
|
||||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||||
const CircularDependencyPlugin = require('circular-dependency-plugin')
|
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||||
|
const ManifestGenerator = require("./webpack/ManifestPlugin");
|
||||||
|
const WorkerPlugin = require('worker-plugin');
|
||||||
|
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||||
|
|
||||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -11,10 +15,16 @@ module.exports = {
|
||||||
devtool: 'inline-source-map',
|
devtool: 'inline-source-map',
|
||||||
mode: "development",
|
mode: "development",
|
||||||
plugins: [
|
plugins: [
|
||||||
|
new CleanWebpackPlugin(),
|
||||||
new MiniCssExtractPlugin({
|
new MiniCssExtractPlugin({
|
||||||
filename: isDevelopment ? '[name].css' : '[name].[hash].css',
|
filename: isDevelopment ? '[name].css' : '[name].[hash].css',
|
||||||
chunkFilename: isDevelopment ? '[id].css' : '[id].[hash].css'
|
chunkFilename: isDevelopment ? '[id].css' : '[id].[hash].css'
|
||||||
}),
|
}),
|
||||||
|
new ManifestGenerator({
|
||||||
|
file: path.join(__dirname, "dist/manifest.json")
|
||||||
|
}),
|
||||||
|
new WorkerPlugin(),
|
||||||
|
//new BundleAnalyzerPlugin()
|
||||||
/*
|
/*
|
||||||
new CircularDependencyPlugin({
|
new CircularDependencyPlugin({
|
||||||
//exclude: /a\.js|node_modules/,
|
//exclude: /a\.js|node_modules/,
|
||||||
|
@ -23,6 +33,12 @@ module.exports = {
|
||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
})
|
})
|
||||||
*/
|
*/
|
||||||
|
/*
|
||||||
|
new webpack.optimize.AggressiveSplittingPlugin({
|
||||||
|
minSize: 1024 * 128,
|
||||||
|
maxSize: 1024 * 1024
|
||||||
|
})
|
||||||
|
*/
|
||||||
],
|
],
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
|
@ -58,49 +74,28 @@ module.exports = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['.tsx', '.ts', '.js', ".scss"],
|
extensions: ['.tsx', '.ts', '.js', ".scss"],
|
||||||
alias: {
|
alias: {
|
||||||
"tc-shared": path.resolve(__dirname, "shared/js"),
|
"tc-shared": path.resolve(__dirname, "shared/js"),
|
||||||
"tc-backend": path.resolve(__dirname, "web/js")
|
"tc-backend/web": path.resolve(__dirname, "web/js"),
|
||||||
|
"tc-backend": path.resolve(__dirname, "web/js"),
|
||||||
|
"tc-generated/codec/opus": path.resolve(__dirname, "asm/generated/TeaWeb-Worker-Codec-Opus.js"),
|
||||||
//"tc-backend": path.resolve(__dirname, "shared/backend.d"),
|
//"tc-backend": path.resolve(__dirname, "shared/backend.d"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
externals: {
|
externals: {
|
||||||
"tc-loader": "umd loader"
|
"tc-loader": "window loader"
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
filename: 'shared-app.js',
|
filename: '[contenthash].js',
|
||||||
path: path.resolve(__dirname, 'dist'),
|
path: path.resolve(__dirname, 'dist'),
|
||||||
libraryTarget: "umd",
|
publicPath: "js/"
|
||||||
library: "shared"
|
|
||||||
},
|
},
|
||||||
optimization: {
|
optimization: {
|
||||||
/*
|
splitChunks: { }
|
||||||
splitChunks: {
|
|
||||||
chunks: 'async',
|
|
||||||
minSize: 1,
|
|
||||||
maxSize: 500000,
|
|
||||||
minChunks: 1,
|
|
||||||
maxAsyncRequests: 6,
|
|
||||||
maxInitialRequests: 4,
|
|
||||||
automaticNameDelimiter: '~',
|
|
||||||
automaticNameMaxLength: 30,
|
|
||||||
cacheGroups: {
|
|
||||||
defaultVendors: {
|
|
||||||
test: /[\\/]node_modules[\\/]/,
|
|
||||||
priority: -10
|
|
||||||
},
|
|
||||||
default: {
|
|
||||||
minChunks: 2,
|
|
||||||
priority: -20,
|
|
||||||
reuseExistingChunk: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
};
|
};
|
|
@ -0,0 +1,49 @@
|
||||||
|
import * as webpack from "webpack";
|
||||||
|
import * as fs from "fs";
|
||||||
|
|
||||||
|
interface Options {
|
||||||
|
file?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ManifestGenerator {
|
||||||
|
private manifest_content;
|
||||||
|
|
||||||
|
readonly options: Options;
|
||||||
|
constructor(options: Options) {
|
||||||
|
this.options = options || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(compiler: webpack.Compiler) {
|
||||||
|
compiler.hooks.afterCompile.tap(this.constructor.name, compilation => {
|
||||||
|
const chunks_data = {};
|
||||||
|
for(const chunk_group of compilation.chunkGroups) {
|
||||||
|
console.log(chunk_group.options.name);
|
||||||
|
const js_files = [];
|
||||||
|
for(const chunk of chunk_group.chunks) {
|
||||||
|
if(chunk.files.length !== 1) throw "expected only one file per chunk";
|
||||||
|
|
||||||
|
const file = chunk.files[0];
|
||||||
|
console.log("Chunk: %s - %s - %s", chunk.id, chunk.hash, file);
|
||||||
|
//console.log(chunk);
|
||||||
|
//console.log(" - %s - %o", chunk.id, chunk);
|
||||||
|
js_files.push({
|
||||||
|
hash: chunk.hash,
|
||||||
|
file: file
|
||||||
|
})
|
||||||
|
}
|
||||||
|
chunks_data[chunk_group.options.name] = js_files;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.manifest_content = {
|
||||||
|
version: 1,
|
||||||
|
chunks: chunks_data
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
compiler.hooks.done.tap(this.constructor.name, () => {
|
||||||
|
fs.writeFileSync(this.options.file || "manifest.json", JSON.stringify(this.manifest_content));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export = ManifestGenerator;
|
Loading…
Reference in New Issue