Changed the loader

This commit is contained in:
WolverinDEV 2018-12-30 01:38:13 +01:00
parent c97987828e
commit 15ab95c1f9
2 changed files with 475 additions and 350 deletions

View file

@ -1,54 +1,193 @@
namespace app { namespace loader {
export enum Type { type Task = {
UNDEFINED, name: string,
RELEASE, priority: number, /* tasks with the same priority will be executed in sync */
DEBUG function: () => Promise<void>
};
export enum Stage {
/*
loading loader required files (incl this)
*/
INITIALIZING,
/*
setting up the loading process
*/
SETUP,
/*
loading all javascript files
*/
JAVASCRIPT,
/*
loading all template files
*/
TEMPLATES,
/*
initializing static/global stuff
*/
JAVASCRIPT_INITIALIZING,
/*
finalizing load process
*/
FINALIZING,
/*
invoking main task
*/
LOADED
} }
let moduleInitialized: boolean; let current_stage: Stage = Stage.INITIALIZING;
let applicationLoaded: boolean; const tasks: {[key:number]:Task[]} = {};
export let type: Type = Type.UNDEFINED;
export let loadedListener: (() => any)[];
export const appLoaded = Date.now();
export function initialized() : boolean { export function register_task(stage: Stage, task: Task) {
return moduleInitialized && applicationLoaded; const task_array = tasks[stage] || (tasks[stage] = []);
task_array.push(task);
task_array.sort((a, b) => a.priority < b.priority ? 1 : 0);
} }
export function callbackApp(errorMessage?: string) { export async function execute() {
if(errorMessage) { while(current_stage <= Stage.LOADED) {
console.error("Could not load application!"); let current_tasks: Task[] = [];
while((tasks[current_stage] || []).length > 0) {
if(current_tasks.length == 0 || current_tasks[0].priority == tasks[current_stage][0].priority) {
current_tasks.push(tasks[current_stage].pop());
} else break;
}
const errors: {
error: any,
task: Task
}[] = [];
const promises: Promise<void>[] = [];
for(const task of current_tasks) {
try {
promises.push(task.function().catch(error => {
errors.push({
task: task,
error: error
});
return Promise.resolve();
}));
} catch(error) {
errors.push({
task: task,
error: error
});
}
}
await Promise.all([...promises]);
if(errors.length > 0) {
console.error("Failed to execute loader. The following tasks failed (%d):", errors.length);
for(const error of errors)
console.error(" - %s: %o", error.task.name, error.error);
throw "failed to process step " + Stage[current_stage];
}
if(current_tasks.length == 0) {
if(current_stage < Stage.LOADED)
console.debug("[loader] entering next state (%s)", Stage[current_stage + 1]);
current_stage += 1;
}
}
console.debug("[loader] finished loader.");
}
type Script = string | string[];
function script_name(path: string | string[]) {
if(Array.isArray(path)) {
let buffer = "";
let _or = " or ";
for(let entry of path)
buffer += _or + formatPath(entry);
return buffer.slice(_or.length);
} else return "<code>" + path + "</code>";
}
class SyntaxError {
source: any;
constructor(source: any) {
this.source = source;
}
}
export async function load_script(path: Script) : Promise<void> {
if(Array.isArray(path)) { //We have some fallback
return load_script(path[0]).catch(error => {
if(error instanceof SyntaxError)
return error.source;
if(path.length > 1)
return load_script(path.slice(1));
return error;
});
} else { } else {
applicationLoaded = true; return new Promise((resolve, reject) => {
testInitialisation(); const tag: HTMLScriptElement = document.createElement("script");
const error_handler = (event: ErrorEvent) => {
if(event.filename == tag.src) { //Our tag throw an uncaught error
//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();
}
};
window.addEventListener('error', error_handler as any);
tag.type = "application/javascript";
tag.async = true;
tag.defer = true;
tag.onerror = error => {
window.removeEventListener('error', error_handler as any);
console.error("ERROR: %o", error);
tag.remove();
reject(error);
};
tag.onload = () => {
window.removeEventListener('error', error_handler as any);
console.debug("Script %o loaded", path);
resolve();
};
document.getElementById("scripts").appendChild(tag);
tag.src = path;
});
} }
} }
export function initialize() { export async function load_scripts(paths: Script[]) : Promise<void> {
moduleInitialized = false; const promises: Promise<void>[] = [];
applicationLoaded = false; const errors: {
loadedListener = []; script: Script,
error: any
}[] = [];
Module['onRuntimeInitialized'] = function() { for(const script of paths)
console.log("Runtime init!"); promises.push(load_script(script).catch(error => {
moduleInitialized = true; errors.push({
testInitialisation(); script: script,
}; error: error
});
return Promise.resolve();
}));
Module['onAbort'] = message => { await Promise.all([...promises]);
Module['onAbort'] = undefined;
displayCriticalError("Could not load webassembly files!<br>Message: <code>" + message + "</code>");
};
Module['locateFile'] = file => { if(errors.length > 0) {
return "wasm/" + file; console.error("Failed to load the following scripts:");
}; for(const script of errors)
console.log(" - %o: %o", script.script, script.error);
displayCriticalError("Failed to load script " + script_name(errors[0].script) + " <br>" + "View the browser console for more information!");
throw "failed to load script " + script_name(errors[0].script);
} }
function testInitialisation() {
if(moduleInitialized && applicationLoaded)
for(let l of loadedListener)
l();
} }
} }
@ -67,7 +206,7 @@ const display_critical_load = message => {
}; };
const loader_impl_display_critical_error = message => { const loader_impl_display_critical_error = message => {
if(typeof(createErrorModal) !== 'undefined') { if(typeof(createErrorModal) !== 'undefined' && typeof((<any>window).ModalFunctions) !== 'undefined') {
createErrorModal("A critical error occurred while loading the page!", message, {closeable: false}).open(); createErrorModal("A critical error occurred while loading the page!", message, {closeable: false}).open();
} else { } else {
display_critical_load(message); display_critical_load(message);
@ -89,86 +228,59 @@ function displayCriticalError(message: string) {
loader_impl_display_critical_error(message); loader_impl_display_critical_error(message);
} }
/* all javascript loaders */
function load_scripts(paths: (string | string[])[]) : {path: string, promise: Promise<Boolean>}[] { const loader_javascript = {
let result = []; load_scripts: async () => {
for(let path of paths) /*
result.push({path: path, promise: load_script(path)}); if(window.require !== undefined) {
return result; console.log("Loading node specific things");
} const remote = require('electron').remote;
module.paths.push(remote.app.getAppPath() + "/node_modules");
function load_script(path: string | string[]) : Promise<Boolean> { module.paths.push(remote.app.getAppPath() + "/app");
if(Array.isArray(path)) { //Having fallbacks module.paths.push(remote.getGlobal("browser-root") + "js/");
return new Promise<Boolean>((resolve, reject) => { window.$ = require("assets/jquery.min.js");
load_script(path[0]).then(resolve).catch(error => { require("native/loader_adapter.js");
if(path.length >= 2) {
load_script(path.slice(1)).then(resolve).catch(() => reject("could not load file " + formatPath(path)));
} else {
reject("could not load file");
} }
}); */
});
} else {
return new Promise((resolve, reject) => {
const tag: HTMLScriptElement = document.createElement("script");
tag.type = "application/javascript";
tag.async = true;
tag.defer = true;
tag.onerror = error => {
console.log(error);
tag.remove();
reject(error);
};
tag.onload = () => {
console.debug("Script %o loaded", path);
resolve();
};
document.getElementById("scripts").appendChild(tag);
tag.src = path;
});
}
}
function formatPath(path: string | string[]) {
if(Array.isArray(path)) {
let buffer = "";
let _or = " or ";
for(let entry of path)
buffer += _or + formatPath(entry);
return buffer.slice(_or.length);
} else return "<code>" + path + "</code>";
}
function loadRelease() {
app.type = app.Type.RELEASE;
console.log("Load for release!");
awaitLoad(load_scripts([
//Load general API's
["wasm/TeaWeb-Identity.js"],
["js/client.min.js", "js/client.js"]
])).then(() => {
console.log("Loaded successfully all scripts!");
app.callbackApp();
}).catch((error) => {
console.error("Could not load " + error.path);
});
}
/** Only possible for developers! **/
function loadDebug() {
app.type = app.Type.DEBUG;
console.log("Load for debug!");
let custom_scripts: (string | string[])[] = [];
if(!window.require) { if(!window.require) {
console.log("Adding browser audio player"); await loader.load_script(["vendor/jquery/jquery.min.js"]);
custom_scripts.push(["js/audio/AudioPlayer.js"]); }
custom_scripts.push(["js/audio/WebCodec.js"]); await loader.load_script("vendor/jsrender/jsrender.min.js");
custom_scripts.push(["js/WebPPTListener.js"]); await loader.load_scripts([
["vendor/bbcode/xbbcode.js"],
["vendor/moment/moment.js"],
["https://webrtc.github.io/adapter/adapter-latest.js"]
]);
try {
await loader.load_script("js/proto.js");
//we're loading for debug
loader.register_task(loader.Stage.JAVASCRIPT, {
name: "scripts debug",
priority: 20,
function: loader_javascript.load_scripts_debug
});
} catch(error) {
loader.register_task(loader.Stage.JAVASCRIPT, {
name: "scripts release",
priority: 20,
function: loadRelease /* fixme */
});
}
},
load_scripts_debug: async () => {
/* test if we're loading as TeaClient or WebClient */
if(!window.require) {
loader.register_task(loader.Stage.JAVASCRIPT, {
name: "javascript web",
priority: 10,
function: loader_javascript.load_scripts_debug_web
})
} }
load_wait_scripts([ await loader.load_scripts([
["wasm/TeaWeb-Identity.js"], ["wasm/TeaWeb-Identity.js"],
//Load general API's //Load general API's
@ -238,58 +350,91 @@ function loadDebug() {
"js/client.js", "js/client.js",
"js/chat.js", "js/chat.js",
"js/PPTListener.js", "js/PPTListener.js"
...custom_scripts ]);
]).then(() => load_wait_scripts([
await loader.load_scripts([
"js/codec/CodecWrapperWorker.js", "js/codec/CodecWrapperWorker.js",
"js/profiles/identities/NameIdentity.js", //Depends on Identity "js/profiles/identities/NameIdentity.js", //Depends on Identity
"js/profiles/identities/TeaForumIdentity.js", //Depends on Identity "js/profiles/identities/TeaForumIdentity.js", //Depends on Identity
"js/profiles/identities/TeamSpeakIdentity.js", //Depends on Identity "js/profiles/identities/TeamSpeakIdentity.js", //Depends on Identity
])).then(() => load_wait_scripts([ ]);
"js/main.js"
])).then(() => {
console.log("Loaded successfully all scripts!");
app.callbackApp();
});
}
function awaitLoad(promises: {path: string, promise: Promise<Boolean>}[]) : Promise<void> { await loader.load_script("js/main.js");
return new Promise<void>((resolve, reject) => { },
let awaiting = promises.length; load_scripts_debug_web: async () => {
let success = true; await loader.load_scripts([
["js/audio/AudioPlayer.js"],
["js/audio/WebCodec.js"],
["js/WebPPTListener.js"]
]);
},
for(let entry of promises) { loadRelease: async () => {
entry.promise.then(() => { console.log("Load for release!");
awaiting--;
if(awaiting == 0) resolve(); await loader.load_scripts([
}).catch(error => { //Load general API's
success = false; ["wasm/TeaWeb-Identity.js"],
if(error instanceof TypeError) { ["js/client.min.js", "js/client.js"]
console.error(error); ]);
let name = (error as any).fileName + "@" + (error as any).lineNumber + ":" + (error as any).columnNumber; }
displayCriticalError("Failed to execute script <code>" + name + "</code>.<hr>If you believe that it isn't your mistake<br>then please contact an administrator!"); };
const loader_webassembly = {
test_webassembly: async () => {
if(typeof (WebAssembly) === "undefined" || typeof (WebAssembly.compile) === "undefined") {
console.log(navigator.browserSpecs);
if (navigator.browserSpecs.name == 'Safari') {
if (parseInt(navigator.browserSpecs.version) < 11) {
displayCriticalError("You require Safari 11 or higher to use the web client!<br>Safari " + navigator.browserSpecs.version + " does not support WebAssambly!");
return; return;
} else {
console.error("Failed to load script " + entry.path);
} }
displayCriticalError("Failed to load script " + formatPath(entry.path) + ".<hr>If you believe that it isn't your mistake<br>then please contact an administrator!"); }
else {
// Do something for all other browsers.
}
displayCriticalError("You require WebAssembly for TeaSpeak-Web!");
throw "Missing web assembly";
}
},
setup_awaiter: async () => {
Module['_initialized'] = false;
Module['_initialized_callback'] = undefined;
Module['onRuntimeInitialized'] = () => {
Module['_initialized'] = true;
if(Module['_initialized_callback'])
Module['_initialized_callback']();
};
Module['onAbort'] = message => {
Module['onAbort'] = undefined;
Module['_initialized'] = false;
displayCriticalError("Could not load webassembly files!<br>Message: <code>" + message + "</code>");
};
Module['locateFile'] = file => "wasm/" + file;
},
awaiter: () => new Promise<void>((resolve, reject) => {
if(!Module['onAbort']) /* an error has been already encountered */
reject();
else if(!Module['_initialized'])
Module['_initialized_callback'] = resolve;
else
resolve();
}) })
} };
});
}
function load_wait_scripts(paths: (string | string[])[]) : Promise<void> {
return awaitLoad(load_scripts(paths));
}
function loadTemplates() { async function load_templates() {
//Load the templates try {
$.ajax("templates.html", { const response = await $.ajax("templates.html", {
cache: false, //Change this when in release mode cache: false, //Change this when in release mode
}).then((element, status) => { });
let node = document.createElement("html"); let node = document.createElement("html");
node.innerHTML = element; node.innerHTML = response;
let tags: HTMLCollection; let tags: HTMLCollection;
if(node.getElementsByTagName("body").length > 0) if(node.getElementsByTagName("body").length > 0)
tags = node.getElementsByTagName("body")[0].children; tags = node.getElementsByTagName("body")[0].children;
@ -306,66 +451,17 @@ function loadTemplates() {
root.appendChild(tag); root.appendChild(tag);
} }
}).catch(error => { } catch(error) {
console.error("Could not load templates!"); console.dir(error);
console.log(error); displayCriticalError("Failed to find template tag!");
displayCriticalError("Could not load HTML templates!"); throw "template error";
}); }
} }
interface Window { interface Window {
$: JQuery; $: JQuery;
} }
//TODO release config!
function loadSide() {
if(window.require !== undefined) {
console.log("Loading node specific things");
const remote = require('electron').remote;
module.paths.push(remote.app.getAppPath() + "/node_modules");
module.paths.push(remote.app.getAppPath() + "/app");
module.paths.push(remote.getGlobal("browser-root") + "js/");
window.$ = require("assets/jquery.min.js");
require("native/loader_adapter.js");
}
if(typeof (WebAssembly) === "undefined" || typeof (WebAssembly.compile) === "undefined") {
console.log(navigator.browserSpecs);
if (navigator.browserSpecs.name == 'Safari') {
if (parseInt(navigator.browserSpecs.version) < 11) {
displayCriticalError("You require Safari 11 or higher to use the web client!<br>Safari " + navigator.browserSpecs.version + " does not support WebAssambly!");
return;
}
}
else {
// Do something for all other browsers.
}
displayCriticalError("You require WebAssembly for TeaSpeak-Web!");
return;
}
//Load the general scripts and required scripts
(window.require !== undefined ?
Promise.resolve() :
load_wait_scripts([
"vendor/jquery/jquery.min.js"
])
).then(() => load_wait_scripts([
"vendor/jsrender/jsrender.min.js"
])).then(() => load_wait_scripts([
["vendor/bbcode/xbbcode.js"],
["vendor/moment/moment.js"],
["https://webrtc.github.io/adapter/adapter-latest.js"]
])).then(() => {
//Load the teaweb scripts
load_script("js/proto.js").then(loadDebug).catch(loadRelease);
//Load the teaweb templates
loadTemplates();
}).catch(error => {
displayCriticalError("Failed to load scripts.<br>Lookup the console for more details.");
console.error(error);
});
}
//FUN: loader_ignore_age=0&loader_default_duration=1500&loader_default_age=5000 //FUN: loader_ignore_age=0&loader_default_duration=1500&loader_default_age=5000
let _fadeout_warned = false; let _fadeout_warned = false;
function fadeoutLoader(duration = undefined, minAge = undefined, ignoreAge = undefined) { function fadeoutLoader(duration = undefined, minAge = undefined, ignoreAge = undefined) {
@ -393,11 +489,13 @@ function fadeoutLoader(duration = undefined, minAge = undefined, ignoreAge = und
else ignoreAge = false; else ignoreAge = false;
} }
/*
let age = Date.now() - app.appLoaded; let age = Date.now() - app.appLoaded;
if(age < minAge && !ignoreAge) { if(age < minAge && !ignoreAge) {
setTimeout(() => fadeoutLoader(duration, 0, true), minAge - age); setTimeout(() => fadeoutLoader(duration, 0, true), minAge - age);
return; return;
} }
*/
$(".loader .bookshelf_wrapper").animate({top: 0, opacity: 0}, duration); $(".loader .bookshelf_wrapper").animate({top: 0, opacity: 0}, duration);
$(".loader .half").animate({width: 0}, duration, () => { $(".loader .half").animate({width: 0}, duration, () => {
@ -405,21 +503,9 @@ function fadeoutLoader(duration = undefined, minAge = undefined, ignoreAge = und
}); });
} }
/* safari remove "fix" */
if(Element.prototype.remove === undefined)
Object.defineProperty(Element.prototype, "remove", {
enumerable: false,
configurable: false,
writable: false,
value: function(){
this.parentElement.removeChild(this);
}
});
if(typeof Module === "undefined") if(typeof Module === "undefined")
this["Module"] = {}; this["Module"] = {};
app.initialize();
app.loadedListener.push(fadeoutLoader);
navigator.browserSpecs = (function(){ navigator.browserSpecs = (function(){
let ua = navigator.userAgent, tem, M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || []; let ua = navigator.userAgent, tem, M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
@ -438,9 +524,67 @@ navigator.browserSpecs = (function(){
})(); })();
console.log(navigator.browserSpecs); //Object { name: "Firefox", version: "42" } console.log(navigator.browserSpecs); //Object { name: "Firefox", version: "42" }
try {
loadSide(); /* register tasks */
} catch(error) { loader.register_task(loader.Stage.INITIALIZING, {
displayCriticalError("Failed to invoke main loader function."); name: "safari fix",
console.error(error); function: async () => {
} /* safari remove "fix" */
if(Element.prototype.remove === undefined)
Object.defineProperty(Element.prototype, "remove", {
enumerable: false,
configurable: false,
writable: false,
value: function(){
this.parentElement.removeChild(this);
}
});
},
priority: 50
});
loader.register_task(loader.Stage.INITIALIZING, {
name: "webassembly tester",
function: loader_webassembly.test_webassembly,
priority: 20
});
loader.register_task(loader.Stage.INITIALIZING, {
name: "webassembly setup",
function: loader_webassembly.setup_awaiter,
priority: 10
});
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: "javascript webassembly",
function: loader_webassembly.awaiter,
priority: 10
});
loader.register_task(loader.Stage.JAVASCRIPT, {
name: "javascript",
function: loader_javascript.load_scripts,
priority: 10
});
loader.register_task(loader.Stage.TEMPLATES, {
name: "templates",
function: load_templates,
priority: 10
});
loader.register_task(loader.Stage.LOADED, {
name: "loaded handler",
function: async () => {
fadeoutLoader();
},
priority: 10
});
loader.execute().then(() => {
console.log("app successfully loaded!");
}).catch(error => {
displayCriticalError("failed to load app!<br>Please lookup the browser console for more details");
console.error("Failed to load app!\nError: %o", error);
});

View file

@ -120,6 +120,7 @@ async function initialize() {
const main = $("#tmpl_main").renderTag(); const main = $("#tmpl_main").renderTag();
$("body").append(main); $("body").append(main);
} catch(error) { } catch(error) {
console.error(error);
display_load_error(tr("Failed to setup main page!")); display_load_error(tr("Failed to setup main page!"));
return; return;
} }
@ -159,39 +160,14 @@ function main() {
//Modals.spawnSettingsModal(); //Modals.spawnSettingsModal();
//Modals.createChannelModal(undefined); //Modals.createChannelModal(undefined);
/* if(settings.static("connect_default") && settings.static("connect_address", "")) {
//FIXME const profile_uuid = settings.static("connect_profile") as string;
if(settings.static("default_connect_url")) { const profile = profiles.find_profile(profile_uuid) || profiles.default_profile();
switch (settings.static("default_connect_type")) { const address = settings.static("connect_address", "");
case "teaforo": const username = settings.static("connect_username", "Another TeaSpeak user");
if(forumIdentity && forumIdentity.valid())
globalClient.startConnection(settings.static("default_connect_url"), forumIdentity);
else
Modals.spawnConnectModal({
url: settings.static<string>("default_connect_url"),
enforce: true
}, { identity: IdentitifyType.TEAFORO, enforce: true});
break;
case "teamspeak": globalClient.startConnection(address, profile, username);
let connectIdentity = TSIdentityHelper.loadIdentity(settings.global("connect_identity_teamspeak_identity", ""));
if(!connectIdentity || !connectIdentity.valid())
Modals.spawnConnectModal({
url: settings.static<string>("default_connect_url"),
enforce: true
}, { identity: IdentitifyType.TEAMSPEAK, enforce: true});
else
globalClient.startConnection(settings.static("default_connect_url"), connectIdentity);
break;
default:
Modals.spawnConnectModal({
url: settings.static<string>("default_connect_url"),
enforce: true
});
} }
}
/* /*
let tag = $("#tmpl_music_frame").renderTag({ let tag = $("#tmpl_music_frame").renderTag({
//thumbnail: "img/loading_image.svg" //thumbnail: "img/loading_image.svg"
@ -220,7 +196,9 @@ function main() {
}); });
} }
app.loadedListener.push(async () => { loader.register_task(loader.Stage.LOADED, {
name: "async main invoke",
function: async () => {
try { try {
await initialize(); await initialize();
main(); main();
@ -237,4 +215,7 @@ app.loadedListener.push(async () => {
ex = ex.name + ": " + ex.message; ex = ex.name + ": " + ex.message;
displayCriticalError("Failed to invoke main function:<br>" + ex); displayCriticalError("Failed to invoke main function:<br>" + ex);
} }
},
priority: 10
}); });