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_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_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)
|
||||
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)
|
||||
};
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
inline const char* opus_error_message(int error) {
|
||||
error = abs(error);
|
||||
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_BITRATE(4740));
|
||||
|
||||
/* //This method is obsolete!
|
||||
EM_ASM(
|
||||
printMessageToServerTab('Encoder initialized!');
|
||||
printMessageToServerTab(' Comprexity: 1');
|
||||
printMessageToServerTab(' Bitrate: 4740');
|
||||
);
|
||||
*/
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -98,9 +101,11 @@ extern "C" {
|
|||
auto result = opus_encode_float(handle->encoder, (float*) buffer, length / handle->channelCount, buffer, maxLength);
|
||||
if(result < 0) return result;
|
||||
auto end = currentMillies();
|
||||
/* //This message is obsolete
|
||||
EM_ASM({
|
||||
printMessageToServerTab("codec_opus_encode(...) tooks " + $0 + "ms to execute!");
|
||||
}, end - begin);
|
||||
printMessageToServerTab("codec_opus_encode(...) tooks " + $0 + "ms to execute!");
|
||||
}, end - begin);
|
||||
*/
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
192
file.ts
192
file.ts
|
@ -463,7 +463,7 @@ const WEB_APP_FILE_LIST = [
|
|||
"path": "./",
|
||||
"local-path": "./shared/html/"
|
||||
},
|
||||
{ /* javascript loader for releases */
|
||||
{ /* javascript files as manifest.json */
|
||||
"type": "js",
|
||||
"search-pattern": /.*$/,
|
||||
"build-target": "dev|rel",
|
||||
|
@ -471,7 +471,14 @@ const WEB_APP_FILE_LIST = [
|
|||
"path": "js/",
|
||||
"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) */
|
||||
"type": "js",
|
||||
"search-pattern": /.*\.js$/,
|
||||
|
@ -661,41 +668,49 @@ namespace generator {
|
|||
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[]> {
|
||||
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) {
|
||||
if(typeof file["web-only"] === "boolean" && file["web-only"] && options.target !== "web")
|
||||
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())))
|
||||
if(!file_matches_options(file, options))
|
||||
continue;
|
||||
|
||||
const normal_local = path.normalize(path.join(options.source_path, file["local-path"]));
|
||||
|
@ -723,23 +738,52 @@ namespace generator {
|
|||
|
||||
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 {
|
||||
import SearchOptions = generator.SearchOptions;
|
||||
export type Options = {
|
||||
port: number;
|
||||
php: string;
|
||||
|
||||
search_options: SearchOptions;
|
||||
}
|
||||
|
||||
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 php: string;
|
||||
export async function launch(_files: generator.Entry[], options: Options) {
|
||||
//Don't use this check anymore, because we're searching within the PATH variable
|
||||
//if(!await fs.exists(options.php) || !(await fs.stat(options.php)).isFile())
|
||||
// throw "invalid php interpreter (not found)";
|
||||
let options: Options;
|
||||
export async function launch(_files: ProjectResource[], options_: Options) {
|
||||
options = options_;
|
||||
files = _files;
|
||||
|
||||
try {
|
||||
const info = await exec(options.php + " --version");
|
||||
|
@ -763,17 +807,6 @@ namespace server {
|
|||
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() {
|
||||
|
@ -817,8 +850,8 @@ namespace server {
|
|||
});
|
||||
}
|
||||
|
||||
function serve_file(pathname: string, query: any, response: http.ServerResponse) {
|
||||
const file = files.find(e => e.http_path === pathname);
|
||||
async function serve_file(pathname: string, query: any, response: http.ServerResponse) {
|
||||
const file = await generator.search_http_file(files, pathname, options.search_options);
|
||||
if(!file) {
|
||||
console.log("[SERVER] Client requested unknown file %s", pathname);
|
||||
response.writeHead(404);
|
||||
|
@ -827,13 +860,13 @@ namespace server {
|
|||
return;
|
||||
}
|
||||
|
||||
let type = mt.lookup(path.extname(file.local_path)) || "text/html";
|
||||
console.log("[SERVER] Serving file %s (%s) (%s)", file.target_path, type, file.local_path);
|
||||
if(path.extname(file.local_path) === ".php") {
|
||||
serve_php(file.local_path, query, response);
|
||||
let type = mt.lookup(path.extname(file)) || "text/html";
|
||||
console.log("[SERVER] Serving file %s", file, type);
|
||||
if(path.extname(file) === ".php") {
|
||||
serve_php(file, query, response);
|
||||
return;
|
||||
}
|
||||
const fis = fs.createReadStream(file.local_path);
|
||||
const fis = fs.createReadStream(file);
|
||||
|
||||
response.writeHead(200, "success", {
|
||||
"Content-Type": type + "; charset=utf-8"
|
||||
|
@ -846,23 +879,22 @@ namespace server {
|
|||
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") {
|
||||
response.writeHead(200, { "info-version": 1 });
|
||||
response.write("type\thash\tpath\tname\n");
|
||||
for(const file of files)
|
||||
if(file.http_path.endsWith(".php"))
|
||||
response.write(file.type + "\t" + file.hash + "\t" + path.dirname(file.http_path) + "\t" + path.basename(file.http_path, ".php") + ".html" + "\n");
|
||||
for(const file of await generator.search_files(files, options.search_options))
|
||||
if(file.name.endsWith(".php"))
|
||||
response.write(file.type + "\t" + file.hash + "\t" + path.dirname(file.target_path) + "\t" + path.basename(file.name, ".php") + ".html" + "\n");
|
||||
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();
|
||||
return;
|
||||
} else if(url.query["type"] === "file") {
|
||||
let p = path.join(url.query["path"] as string, url.query["name"] as string).replace(/\\/g, "/");
|
||||
if(p.endsWith(".html")) {
|
||||
const np = p.substr(0, p.length - 5) + ".php";
|
||||
if(files.find(e => e.http_path == np) && !files.find(e => e.http_path == p))
|
||||
p = np;
|
||||
const np = await generator.search_http_file(files, p, options.search_options);
|
||||
if(np) p = np;
|
||||
}
|
||||
serve_file(p, url.query, response);
|
||||
return;
|
||||
|
@ -1041,17 +1073,16 @@ function php_exe() : string {
|
|||
}
|
||||
|
||||
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, {
|
||||
source_path: __dirname,
|
||||
parameter: [],
|
||||
target: target,
|
||||
mode: mode,
|
||||
serving: true
|
||||
});
|
||||
|
||||
await server.launch(files, {
|
||||
await server.launch(target === "client" ? CLIENT_APP_FILE_LIST : WEB_APP_FILE_LIST, {
|
||||
port: port,
|
||||
php: php_exe(),
|
||||
search_options: {
|
||||
source_path: __dirname,
|
||||
parameter: [],
|
||||
target: target,
|
||||
mode: mode,
|
||||
serving: true
|
||||
}
|
||||
});
|
||||
|
||||
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[]) {
|
||||
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();
|
||||
try {
|
||||
if(flags.indexOf("--no-tsc") == -1)
|
||||
|
@ -1079,9 +1102,16 @@ async function main_develop(node: boolean, target: "client" | "web", port: numbe
|
|||
await sasswatcher.start();
|
||||
|
||||
try {
|
||||
await server.launch(files, {
|
||||
await server.launch(target === "client" ? CLIENT_APP_FILE_LIST : WEB_APP_FILE_LIST, {
|
||||
port: port,
|
||||
php: php_exe(),
|
||||
search_options: {
|
||||
source_path: __dirname,
|
||||
parameter: [],
|
||||
target: target,
|
||||
mode: "dev",
|
||||
serving: true
|
||||
}
|
||||
});
|
||||
} catch(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 {
|
||||
interface Window {
|
||||
|
@ -8,6 +8,11 @@ declare global {
|
|||
|
||||
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;
|
||||
export function ui_version() {
|
||||
if(typeof(_ui_version) !== "string") {
|
||||
|
@ -22,6 +27,15 @@ export function ui_version() {
|
|||
return _ui_version;
|
||||
}
|
||||
|
||||
interface Manifest {
|
||||
version: number;
|
||||
|
||||
chunks: {[key: string]: {
|
||||
hash: string,
|
||||
file: string
|
||||
}[]};
|
||||
}
|
||||
|
||||
/* all javascript loaders */
|
||||
const loader_javascript = {
|
||||
detect_type: async () => {
|
||||
|
@ -72,7 +86,7 @@ const loader_javascript = {
|
|||
},
|
||||
load_scripts: async () => {
|
||||
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 {
|
||||
/*
|
||||
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.load_scripts([
|
||||
await loader.scripts.load("vendor/jsrender/jsrender.min.js", { cache_tag: cache_tag() });
|
||||
await loader.scripts.load_multiple([
|
||||
["vendor/xbbcode/src/parser.js"],
|
||||
["vendor/moment/moment.js"],
|
||||
["vendor/twemoji/twemoji.min.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 */
|
||||
["adapter/adapter-latest.js", "https://webrtc.github.io/adapter/adapter-latest.js"]
|
||||
]);
|
||||
await loader.load_scripts([
|
||||
["vendor/emoji-picker/src/jquery.lsxemojipicker.js"]
|
||||
]);
|
||||
], {
|
||||
cache_tag: cache_tag(),
|
||||
max_parallel_requests: -1
|
||||
});
|
||||
|
||||
if(!loader.version().debug_mode) {
|
||||
loader.register_task(loader.Stage.JAVASCRIPT, {
|
||||
name: "scripts release",
|
||||
priority: 20,
|
||||
function: loader_javascript.load_release
|
||||
});
|
||||
} else {
|
||||
loader.register_task(loader.Stage.JAVASCRIPT, {
|
||||
name: "scripts debug",
|
||||
priority: 20,
|
||||
function: loader_javascript.load_scripts_debug
|
||||
});
|
||||
await loader.scripts.load("vendor/emoji-picker/src/jquery.lsxemojipicker.js", { cache_tag: cache_tag() });
|
||||
|
||||
let manifest: Manifest;
|
||||
try {
|
||||
const response = await fetch("js/manifest.json");
|
||||
if(!response.ok) throw response.status + " " + response.statusText;
|
||||
|
||||
manifest = await response.json();
|
||||
} catch(error) {
|
||||
console.error("Failed to load javascript manifest: %o", error);
|
||||
loader.critical_error("Failed to load manifest.json", error);
|
||||
throw "failed to load manifest.json";
|
||||
}
|
||||
},
|
||||
load_scripts_debug: async () => {
|
||||
await loader.load_scripts(["js/shared-app.js"])
|
||||
},
|
||||
load_release: async () => {
|
||||
console.log("Load for release!");
|
||||
if(manifest.version !== 1)
|
||||
throw "invalid manifest version";
|
||||
|
||||
await loader.load_scripts([
|
||||
//Load general API's
|
||||
["js/client.min.js", "js/client.js"]
|
||||
]);
|
||||
await loader.scripts.load_multiple(manifest.chunks["shared-app"].map(e => "js/" + e.file), {
|
||||
cache_tag: undefined,
|
||||
max_parallel_requests: -1
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -149,15 +159,20 @@ const loader_webassembly = {
|
|||
|
||||
const loader_style = {
|
||||
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"
|
||||
]);
|
||||
await loader.load_styles([
|
||||
], options);
|
||||
await loader.style.load_multiple([
|
||||
"vendor/emoji-picker/src/jquery.lsxemojipicker.css"
|
||||
]);
|
||||
await loader.load_styles([
|
||||
], options);
|
||||
await loader.style.load_multiple([
|
||||
["vendor/highlight/styles/darcula.css", ""], /* empty string means not required */
|
||||
]);
|
||||
], options);
|
||||
|
||||
if(loader.version().debug_mode) {
|
||||
await loader_style.load_style_debug();
|
||||
|
@ -167,7 +182,7 @@ const loader_style = {
|
|||
},
|
||||
|
||||
load_style_debug: async () => {
|
||||
await loader.load_styles([
|
||||
await loader.style.load_multiple([
|
||||
"css/static/main.css",
|
||||
"css/static/main-layout.css",
|
||||
"css/static/helptag.css",
|
||||
|
@ -218,14 +233,20 @@ const loader_style = {
|
|||
"css/static/htmltags.css",
|
||||
"css/static/hostbanner.css",
|
||||
"css/static/menu-bar.css"
|
||||
]);
|
||||
], {
|
||||
cache_tag: cache_tag(),
|
||||
max_parallel_requests: -1
|
||||
});
|
||||
},
|
||||
|
||||
load_style_release: async () => {
|
||||
await loader.load_styles([
|
||||
await loader.style.load_multiple([
|
||||
"css/static/base.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, {
|
||||
name: "templates",
|
||||
function: async () => {
|
||||
await loader.load_templates([
|
||||
await loader.templates.load_multiple([
|
||||
"templates.html",
|
||||
"templates/modal/musicmanage.html",
|
||||
"templates/modal/newcomer.html",
|
||||
]);
|
||||
], {
|
||||
cache_tag: cache_tag(),
|
||||
max_parallel_requests: -1
|
||||
});
|
||||
},
|
||||
priority: 10
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import * as loader from "./loader";
|
||||
import * as loader from "./loader/loader";
|
||||
|
||||
let is_debug = false;
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as loader from "./app";
|
||||
import * as loader_base from "./loader";
|
||||
import * as loader_base from "./loader/loader";
|
||||
|
||||
export = loader_base;
|
||||
loader.run();
|
|
@ -1,5 +1,8 @@
|
|||
import {AppVersion} from "../exports/loader";
|
||||
import {type} from "os";
|
||||
import {AppVersion} from "tc-loader";
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
export function execute_managed() {
|
||||
|
@ -233,347 +232,32 @@ export function execute_managed() {
|
|||
});
|
||||
}
|
||||
|
||||
export type DependSource = {
|
||||
url: string;
|
||||
depends: string[];
|
||||
}
|
||||
export type SourcePath = string | DependSource | string[];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
let _fadeout_warned;
|
||||
export function hide_overlay() {
|
||||
if(typeof($) === "undefined") {
|
||||
if(!_fadeout_warned)
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
/* versions management */
|
||||
let version_: AppVersion;
|
||||
export function version() : AppVersion { return 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_called: boolean = false;
|
||||
|
||||
export function critical_error(message: string, detail?: string) {
|
||||
if(_callback_critical_called) {
|
||||
console.warn("[CRITICAL] %s", message);
|
||||
|
@ -603,29 +287,24 @@ export function critical_error(message: string, detail?: string) {
|
|||
|
||||
tag.classList.add("shown");
|
||||
}
|
||||
|
||||
export function critical_error_handler(handler?: ErrorHandler, override?: boolean) : ErrorHandler {
|
||||
if((typeof(handler) === "object" && handler !== _callback_critical_error) || override)
|
||||
_callback_critical_error = handler;
|
||||
return _callback_critical_error;
|
||||
}
|
||||
|
||||
let _fadeout_warned;
|
||||
export function hide_overlay() {
|
||||
if(typeof($) === "undefined") {
|
||||
if(!_fadeout_warned)
|
||||
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();
|
||||
});
|
||||
/* loaders */
|
||||
export type DependSource = {
|
||||
url: string;
|
||||
depends: string[];
|
||||
}
|
||||
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 = () => {
|
|
@ -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[];
|
||||
}
|
||||
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 function critical_error(message: string, detail?: string);
|
||||
export function critical_error_handler(handler?: ErrorHandler, override?: boolean);
|
||||
|
|
|
@ -54,9 +54,9 @@ module.exports = {
|
|||
},
|
||||
output: {
|
||||
filename: 'loader.js',
|
||||
path: path.resolve(__dirname, '../dist'),
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
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: { }
|
||||
};
|
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/sha256": "^0.2.0",
|
||||
"@types/websocket": "0.0.40",
|
||||
"chunk-manifest-webpack-plugin": "^1.1.2",
|
||||
"clean-css": "^4.2.1",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"css-loader": "^3.4.2",
|
||||
"csso-cli": "^2.0.2",
|
||||
"exports-loader": "^0.7.0",
|
||||
"file-loader": "^6.0.0",
|
||||
"fs-extra": "latest",
|
||||
"gulp": "^4.0.2",
|
||||
"html-loader": "^1.0.0",
|
||||
"html-webpack-plugin": "^4.0.3",
|
||||
"mime-types": "^2.1.24",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
|
@ -53,7 +59,10 @@
|
|||
"typescript": "3.6.5",
|
||||
"wat2wasm": "^1.0.2",
|
||||
"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": {
|
||||
"type": "git",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {settings, Settings} from "tc-shared/settings";
|
||||
import * as loader from "tc-loader";
|
||||
import * as log from "tc-shared/log";
|
||||
import {LogCategory} from "tc-shared/log";
|
||||
import * as bipc from "tc-shared/BrowserIPC";
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
"paths": {
|
||||
"*": ["shared/declarations/*"],
|
||||
"tc-shared/*": ["shared/js/*"],
|
||||
"tc-backend/web/*": ["web/js/*"], /* specific web part */
|
||||
"tc-backend/*": ["shared/backend.d/*"],
|
||||
"tc-loader": ["loader/exports/loader.d.ts"]
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {BasicCodec} from "./BasicCodec";
|
||||
|
||||
export class CodecWrapperRaw extends BasicCodec {
|
||||
export class CodecRaw extends BasicCodec {
|
||||
converterRaw: any;
|
||||
converter: Uint8Array;
|
||||
bufferSize: number = 4096 * 4;
|
|
@ -1,135 +1,37 @@
|
|||
import {BasicCodec} from "./BasicCodec";
|
||||
import {CodecType} from "./Codec";
|
||||
import {LogCategory} 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 {
|
||||
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 _workerCallbackResolve: () => any;
|
||||
private _workerCallbackReject: ($: any) => any;
|
||||
private _initialize_promise: Promise<Boolean>;
|
||||
|
||||
private _initializePromise: Promise<Boolean>;
|
||||
name(): string {
|
||||
return "Worker for " + CodecType[this.type] + " Channels " + this.channelCount;
|
||||
}
|
||||
private _token_index: number = 0;
|
||||
readonly type: CodecType;
|
||||
|
||||
initialise() : Promise<Boolean> {
|
||||
if(this._initializePromise) return this._initializePromise;
|
||||
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
|
||||
});
|
||||
private pending_executes: {[key: string]: {
|
||||
timeout?: any;
|
||||
|
||||
this._workerListener.push({
|
||||
token: token,
|
||||
resolve: data => {
|
||||
this._initialized = data["success"] == true;
|
||||
if(data["success"] == true)
|
||||
resolve();
|
||||
else
|
||||
reject(data.message);
|
||||
}
|
||||
})
|
||||
}));
|
||||
}
|
||||
timestamp_send: number,
|
||||
|
||||
initialized() : boolean {
|
||||
return this._initialized;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
resolve: (_: ExecuteResult) => void;
|
||||
reject: (_: any) => void;
|
||||
}} = {};
|
||||
|
||||
constructor(type: CodecType) {
|
||||
super(48000);
|
||||
|
@ -146,35 +48,92 @@ export class CodecWrapperWorker extends BasicCodec {
|
|||
}
|
||||
}
|
||||
|
||||
private generateToken() {
|
||||
return this._workerTokeIndex++ + "_token";
|
||||
name(): string {
|
||||
return "Worker for " + CodecType[this.type] + " Channels " + this.channelCount;
|
||||
}
|
||||
|
||||
private sendWorkerMessage(message: any, transfare?: any[]) {
|
||||
message["timestamp"] = Date.now();
|
||||
this._worker.postMessage(message, transfare as any);
|
||||
async initialise() : Promise<Boolean> {
|
||||
if(this._initialized) return;
|
||||
|
||||
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"]) {
|
||||
log.error(LogCategory.VOICE, tr("Invalid worker token!"));
|
||||
return;
|
||||
}
|
||||
|
||||
if(message["token"] == this._workerCallbackToken) {
|
||||
if(message["type"] == "loaded") {
|
||||
log.info(LogCategory.VOICE, tr("[Codec] Got worker init response: Success: %o Message: %o"), message["success"], message["message"]);
|
||||
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") {
|
||||
if(message["token"] === "notify") {
|
||||
/* currently not really used */
|
||||
if(message["type"] == "chatmessage_server") {
|
||||
//FIXME?
|
||||
return;
|
||||
}
|
||||
|
@ -182,29 +141,69 @@ export class CodecWrapperWorker extends BasicCodec {
|
|||
return;
|
||||
}
|
||||
|
||||
/* lets warn on general packets. Control packets are allowed to "stuck" a bit longer */
|
||||
if(Date.now() - message["timestamp"] > 5)
|
||||
log.warn(LogCategory.VOICE, tr("Worker message stock time: %d"), Date.now() - message["timestamp"]);
|
||||
const request = this.pending_executes[message["token"]];
|
||||
if(typeof request !== "object") {
|
||||
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) {
|
||||
if(entry.token == message["token"]) {
|
||||
entry.resolve(message);
|
||||
this._workerListener.remove(entry);
|
||||
return;
|
||||
const result: ExecuteResult = {
|
||||
success: message["success"],
|
||||
error: message["error"],
|
||||
result: message["result"],
|
||||
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> {
|
||||
return new Promise<Boolean>((resolve, reject) => {
|
||||
this._workerCallbackReject = reject;
|
||||
this._workerCallbackResolve = resolve;
|
||||
private execute(command: string, data: any, timeout?: number) : Promise<ExecuteResult> {
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
if(!this._worker) {
|
||||
reject(tr("worker does not exists"));
|
||||
return;
|
||||
}
|
||||
|
||||
this._worker = new Worker(settings.static("worker_directory", "js/workers/") + "WorkerCodec.js");
|
||||
this._worker.onmessage = event => this.onWorkerMessage(event.data);
|
||||
this._worker.onerror = (error: ErrorEvent) => reject("Failed to load worker (" + error.message + ")"); //TODO tr
|
||||
const token = this._token_index++ + "_token";
|
||||
|
||||
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] ";
|
||||
const workerCallbackToken = "callback_token";
|
||||
import {CodecType} from "tc-backend/web/codec/Codec";
|
||||
|
||||
interface CodecWorker {
|
||||
const prefix = "[CodecWorker] ";
|
||||
|
||||
export interface CodecWorker {
|
||||
name();
|
||||
initialise?() : string;
|
||||
deinitialise();
|
||||
|
@ -11,78 +12,17 @@ interface CodecWorker {
|
|||
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 data = e.data;
|
||||
let initialize_callback: () => Promise<true | string>;
|
||||
export function set_initialize_callback(callback: () => Promise<true | string>) {
|
||||
initialize_callback = callback;
|
||||
}
|
||||
|
||||
let res: any = {};
|
||||
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);
|
||||
};
|
||||
export let codecInstance: CodecWorker;
|
||||
|
||||
function printMessageToServerTab(message: string) {
|
||||
/*
|
||||
|
@ -95,7 +35,105 @@ function printMessageToServerTab(message: string) {
|
|||
}
|
||||
|
||||
declare function postMessage(message: any): void;
|
||||
function sendMessage(message: any, origin?: string){
|
||||
function sendMessage(message: any, origin?: string) {
|
||||
message["timestamp"] = Date.now();
|
||||
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 = [
|
||||
'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'] = function() {
|
||||
initialized = true;
|
||||
console.log(prefix + "Initialized!");
|
||||
Module['onRuntimeInitialized'] = () => {
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
|
||||
sendMessage({
|
||||
token: workerCallbackToken,
|
||||
type: "loaded",
|
||||
success: true
|
||||
})
|
||||
};
|
||||
Module['onAbort'] = error => {
|
||||
cleanup();
|
||||
|
||||
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 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() {
|
||||
if(arguments.length == 1 && arguments[0] == abort_message)
|
||||
return; /* we don't need to reprint the abort message! */
|
||||
console.log(...arguments);
|
||||
};
|
||||
console.log("Print: ", ...arguments);
|
||||
};
|
||||
|
||||
Module['printErr'] = function() {
|
||||
if(arguments.length == 1 && arguments[0] == abort_message)
|
||||
return; /* we don't need to reprint the abort message! */
|
||||
Module['printErr'] = function() {
|
||||
if(arguments.length == 1 && arguments[0] == abort_message)
|
||||
return; /* we don't need to reprint the abort message! */
|
||||
|
||||
last_error_message = arguments[0];
|
||||
for(const suppress of WASM_ERROR_MESSAGES)
|
||||
if((arguments[0] as string).indexOf(suppress) != -1)
|
||||
return;
|
||||
last_error_message = arguments[0];
|
||||
for(const suppress of WASM_ERROR_MESSAGES)
|
||||
if((arguments[0] as string).indexOf(suppress) != -1)
|
||||
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;
|
||||
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 {
|
||||
VOIP = 2048,
|
||||
|
@ -89,6 +93,7 @@ class OpusWorker implements CodecWorker {
|
|||
private fn_decode: any;
|
||||
private fn_encode: any;
|
||||
private fn_reset: any;
|
||||
private fn_error_message: any;
|
||||
|
||||
private bufferSize = 4096 * 2;
|
||||
private encodeBufferRaw: any;
|
||||
|
@ -106,11 +111,12 @@ class OpusWorker implements CodecWorker {
|
|||
}
|
||||
|
||||
initialise?() : string {
|
||||
this.fn_newHandle = cwrap("codec_opus_createNativeHandle", "number", ["number", "number"]);
|
||||
this.fn_decode = cwrap("codec_opus_decode", "number", ["number", "number", "number", "number"]);
|
||||
this.fn_newHandle = Module.cwrap("codec_opus_createNativeHandle", "number", ["number", "number"]);
|
||||
this.fn_decode = Module.cwrap("codec_opus_decode", "number", ["number", "number", "number", "number"]);
|
||||
/* codec_opus_decode(handle, buffer, length, maxlength) */
|
||||
this.fn_encode = cwrap("codec_opus_encode", "number", ["number", "number", "number", "number"]);
|
||||
this.fn_reset = cwrap("codec_opus_reset", "number", ["number"]);
|
||||
this.fn_encode = Module.cwrap("codec_opus_encode", "number", ["number", "number", "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);
|
||||
|
||||
|
@ -127,12 +133,8 @@ class OpusWorker implements CodecWorker {
|
|||
decode(data: Uint8Array): Float32Array | string {
|
||||
if (data.byteLength > this.decodeBuffer.byteLength) return "Data to long!";
|
||||
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);
|
||||
if (result < 0) {
|
||||
return "invalid result on decode (" + result + ")";
|
||||
}
|
||||
if (result < 0) return this.fn_error_message(result);
|
||||
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);
|
||||
|
||||
let result = this.fn_encode(this.nativeHandle, this.encodeBuffer.byteOffset, data.length, this.encodeBuffer.byteLength);
|
||||
if (result < 0) {
|
||||
return "invalid result on encode (" + result + ")";
|
||||
}
|
||||
if (result < 0) return this.fn_error_message(result);
|
||||
let buf = Module.HEAP8.slice(this.encodeBuffer.byteOffset, this.encodeBuffer.byteOffset + result);
|
||||
return Uint8Array.from(buf);
|
||||
}
|
||||
|
@ -151,4 +151,25 @@ class OpusWorker implements CodecWorker {
|
|||
console.log(prefix + " Reseting opus codec!");
|
||||
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 webpack = require("webpack");
|
||||
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';
|
||||
module.exports = {
|
||||
|
@ -11,10 +15,16 @@ module.exports = {
|
|||
devtool: 'inline-source-map',
|
||||
mode: "development",
|
||||
plugins: [
|
||||
new CleanWebpackPlugin(),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: isDevelopment ? '[name].css' : '[name].[hash].css',
|
||||
chunkFilename: isDevelopment ? '[id].css' : '[id].[hash].css'
|
||||
}),
|
||||
new ManifestGenerator({
|
||||
file: path.join(__dirname, "dist/manifest.json")
|
||||
}),
|
||||
new WorkerPlugin(),
|
||||
//new BundleAnalyzerPlugin()
|
||||
/*
|
||||
new CircularDependencyPlugin({
|
||||
//exclude: /a\.js|node_modules/,
|
||||
|
@ -23,6 +33,12 @@ module.exports = {
|
|||
cwd: process.cwd(),
|
||||
})
|
||||
*/
|
||||
/*
|
||||
new webpack.optimize.AggressiveSplittingPlugin({
|
||||
minSize: 1024 * 128,
|
||||
maxSize: 1024 * 1024
|
||||
})
|
||||
*/
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
|
@ -58,49 +74,28 @@ module.exports = {
|
|||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.tsx', '.ts', '.js', ".scss"],
|
||||
alias: {
|
||||
"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"),
|
||||
},
|
||||
},
|
||||
externals: {
|
||||
"tc-loader": "umd loader"
|
||||
"tc-loader": "window loader"
|
||||
},
|
||||
output: {
|
||||
filename: 'shared-app.js',
|
||||
filename: '[contenthash].js',
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
libraryTarget: "umd",
|
||||
library: "shared"
|
||||
publicPath: "js/"
|
||||
},
|
||||
optimization: {
|
||||
/*
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
splitChunks: { }
|
||||
}
|
||||
};
|
|
@ -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