diff --git a/platform/javascript/js/engine/config.js b/platform/javascript/js/engine/config.js index bba20dc3606..8c2636c7aba 100644 --- a/platform/javascript/js/engine/config.js +++ b/platform/javascript/js/engine/config.js @@ -227,10 +227,10 @@ const InternalConfig = function (initConfig) { // eslint-disable-line no-unused- /** * @ignore * @param {string} loadPath - * @param {Promise} loadPromise + * @param {Response} response */ - Config.prototype.getModuleConfig = function (loadPath, loadPromise) { - let loader = loadPromise; + Config.prototype.getModuleConfig = function (loadPath, response) { + let r = response; return { 'print': this.onPrint, 'printErr': this.onPrintError, @@ -238,12 +238,17 @@ const InternalConfig = function (initConfig) { // eslint-disable-line no-unused- 'noExitRuntime': true, 'dynamicLibraries': [`${loadPath}.side.wasm`], 'instantiateWasm': function (imports, onSuccess) { - loader.then(function (xhr) { - WebAssembly.instantiate(xhr.response, imports).then(function (result) { - onSuccess(result['instance'], result['module']); + function done(result) { + onSuccess(result['instance'], result['module']); + } + if (typeof (WebAssembly.instantiateStreaming) !== 'undefined') { + WebAssembly.instantiateStreaming(Promise.resolve(r), imports).then(done); + } else { + r.arrayBuffer().then(function (buffer) { + WebAssembly.instantiate(buffer, imports).then(done); }); - }); - loader = null; + } + r = null; return {}; }, 'locateFile': function (path) { diff --git a/platform/javascript/js/engine/engine.externs.js b/platform/javascript/js/engine/engine.externs.js index 1a94dd15ec6..35a66a93aef 100644 --- a/platform/javascript/js/engine/engine.externs.js +++ b/platform/javascript/js/engine/engine.externs.js @@ -1,3 +1,4 @@ var Godot; var WebAssembly = {}; WebAssembly.instantiate = function(buffer, imports) {}; +WebAssembly.instantiateStreaming = function(response, imports) {}; diff --git a/platform/javascript/js/engine/engine.js b/platform/javascript/js/engine/engine.js index c51955ed3dc..e2d27c4c023 100644 --- a/platform/javascript/js/engine/engine.js +++ b/platform/javascript/js/engine/engine.js @@ -42,7 +42,7 @@ const Engine = (function () { Engine.load = function (basePath) { if (loadPromise == null) { loadPath = basePath; - loadPromise = preloader.loadPromise(`${loadPath}.wasm`); + loadPromise = preloader.loadPromise(`${loadPath}.wasm`, true); requestAnimationFrame(preloader.animateProgress); } return loadPromise; @@ -98,21 +98,25 @@ const Engine = (function () { } Engine.load(basePath); } - preloader.setProgressFunc(this.config.onProgress); - let config = this.config.getModuleConfig(loadPath, loadPromise); const me = this; - initPromise = new Promise(function (resolve, reject) { - Godot(config).then(function (module) { - module['initFS'](me.config.persistentPaths).then(function (fs_err) { - me.rtenv = module; - if (me.config.unloadAfterInit) { - Engine.unload(); - } - resolve(); - config = null; + function doInit(promise) { + return promise.then(function (response) { + return Godot(me.config.getModuleConfig(loadPath, response.clone())); + }).then(function (module) { + const paths = me.config.persistentPaths; + return module['initFS'](paths).then(function (err) { + return Promise.resolve(module); }); + }).then(function (module) { + me.rtenv = module; + if (me.config.unloadAfterInit) { + Engine.unload(); + } + return Promise.resolve(); }); - }); + } + preloader.setProgressFunc(this.config.onProgress); + initPromise = doInit(loadPromise); return initPromise; }, diff --git a/platform/javascript/js/engine/preloader.js b/platform/javascript/js/engine/preloader.js index ec34fb93f2a..45bdcf26d4f 100644 --- a/platform/javascript/js/engine/preloader.js +++ b/platform/javascript/js/engine/preloader.js @@ -1,54 +1,79 @@ const Preloader = /** @constructor */ function () { // eslint-disable-line no-unused-vars - const loadXHR = function (resolve, reject, file, tracker, attempts) { - const xhr = new XMLHttpRequest(); + function getTrackedResponse(response, load_status) { + let clen = 0; + let compressed = false; + response.headers.forEach(function (value, header) { + const h = header.toLowerCase().trim(); + // We can't accurately compute compressed stream length. + if (h === 'content-encoding') { + compressed = true; + } else if (h === 'content-length') { + const length = parseInt(value, 10); + if (!Number.isNaN(length) && length > 0) { + clen = length; + } + } + }); + if (!compressed && clen) { + load_status.total = clen; + } + function onloadprogress(reader, controller) { + return reader.read().then(function (result) { + if (load_status.done) { + return Promise.resolve(); + } + if (result.value) { + controller.enqueue(result.value); + load_status.loaded += result.value.length; + } + if (!result.done) { + return onloadprogress(reader, controller); + } + load_status.done = true; + return Promise.resolve(); + }); + } + const reader = response.body.getReader(); + return new Response(new ReadableStream({ + start: function (controller) { + onloadprogress(reader, controller).then(function () { + controller.close(); + }); + }, + }), { headers: response.headers }); + } + + function loadFetch(file, tracker, raw) { tracker[file] = { total: 0, loaded: 0, - final: false, + done: false, }; - xhr.onerror = function () { + return fetch(file).then(function (response) { + if (!response.ok) { + return Promise.reject(new Error(`Failed loading file '${file}'`)); + } + const tr = getTrackedResponse(response, tracker[file]); + if (raw) { + return Promise.resolve(tr); + } + return tr.arrayBuffer(); + }); + } + + function retry(func, attempts = 1) { + function onerror(err) { if (attempts <= 1) { - reject(new Error(`Failed loading file '${file}'`)); - } else { + return Promise.reject(err); + } + return new Promise(function (resolve, reject) { setTimeout(function () { - loadXHR(resolve, reject, file, tracker, attempts - 1); + retry(func, attempts - 1).then(resolve).catch(reject); }, 1000); - } - }; - xhr.onabort = function () { - tracker[file].final = true; - reject(new Error(`Loading file '${file}' was aborted.`)); - }; - xhr.onloadstart = function (ev) { - tracker[file].total = ev.total; - tracker[file].loaded = ev.loaded; - }; - xhr.onprogress = function (ev) { - tracker[file].loaded = ev.loaded; - tracker[file].total = ev.total; - }; - xhr.onload = function () { - if (xhr.status >= 400) { - if (xhr.status < 500 || attempts <= 1) { - reject(new Error(`Failed loading file '${file}': ${xhr.statusText}`)); - xhr.abort(); - } else { - setTimeout(function () { - loadXHR(resolve, reject, file, tracker, attempts - 1); - }, 1000); - } - } else { - tracker[file].final = true; - resolve(xhr); - } - }; - // Make request. - xhr.open('GET', file); - if (!file.endsWith('.js')) { - xhr.responseType = 'arraybuffer'; + }); } - xhr.send(); - }; + return func().catch(onerror); + } const DOWNLOAD_ATTEMPTS_MAX = 4; const loadingFiles = {}; @@ -63,7 +88,7 @@ const Preloader = /** @constructor */ function () { // eslint-disable-line no-un Object.keys(loadingFiles).forEach(function (file) { const stat = loadingFiles[file]; - if (!stat.final) { + if (!stat.done) { progressIsFinal = false; } if (!totalIsValid || stat.total === 0) { @@ -92,10 +117,8 @@ const Preloader = /** @constructor */ function () { // eslint-disable-line no-un progressFunc = callback; }; - this.loadPromise = function (file) { - return new Promise(function (resolve, reject) { - loadXHR(resolve, reject, file, loadingFiles, DOWNLOAD_ATTEMPTS_MAX); - }); + this.loadPromise = function (file, raw = false) { + return retry(loadFetch.bind(null, file, loadingFiles, raw), DOWNLOAD_ATTEMPTS_MAX); }; this.preloadedFiles = []; @@ -103,10 +126,10 @@ const Preloader = /** @constructor */ function () { // eslint-disable-line no-un let buffer = null; if (typeof pathOrBuffer === 'string') { const me = this; - return this.loadPromise(pathOrBuffer).then(function (xhr) { + return this.loadPromise(pathOrBuffer).then(function (buf) { me.preloadedFiles.push({ path: destPath || pathOrBuffer, - buffer: xhr.response, + buffer: buf, }); return Promise.resolve(); });