Selaa lähdekoodia

Merge pull request #46728 from Faless/js/4.x_fetch_world

[HTML5] Replace XMLHttpRequest(s) with Fetch.
Rémi Verschelde 4 vuotta sitten
vanhempi
commit
a4b5edf468

+ 1 - 1
platform/javascript/SCsub

@@ -17,7 +17,7 @@ sys_env.AddJSLibraries(
     [
         "js/libs/library_godot_audio.js",
         "js/libs/library_godot_display.js",
-        "js/libs/library_godot_http_request.js",
+        "js/libs/library_godot_fetch.js",
         "js/libs/library_godot_os.js",
         "js/libs/library_godot_runtime.js",
     ]

+ 30 - 16
platform/javascript/export/export.cpp

@@ -242,7 +242,7 @@ class EditorExportPlatformJavaScript : public EditorExportPlatform {
 		return name;
 	}
 
-	void _fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug, int p_flags, const Vector<SharedObject> p_shared_objects);
+	void _fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug, int p_flags, const Vector<SharedObject> p_shared_objects, const Dictionary &p_file_sizes);
 
 	static void _server_thread_poll(void *data);
 
@@ -281,7 +281,7 @@ public:
 	~EditorExportPlatformJavaScript();
 };
 
-void EditorExportPlatformJavaScript::_fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug, int p_flags, const Vector<SharedObject> p_shared_objects) {
+void EditorExportPlatformJavaScript::_fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug, int p_flags, const Vector<SharedObject> p_shared_objects, const Dictionary &p_file_sizes) {
 	String str_template = String::utf8(reinterpret_cast<const char *>(p_html.ptr()), p_html.size());
 	String str_export;
 	Vector<String> lines = str_template.split("\n");
@@ -300,6 +300,7 @@ void EditorExportPlatformJavaScript::_fix_html(Vector<uint8_t> &p_html, const Re
 	config["gdnativeLibs"] = libs;
 	config["executable"] = p_name;
 	config["args"] = args;
+	config["fileSizes"] = p_file_sizes;
 	const String str_config = JSON::print(config);
 
 	for (int i = 0; i < lines.size(); i++) {
@@ -472,6 +473,8 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese
 		return ERR_FILE_CORRUPT;
 	}
 
+	Vector<uint8_t> html;
+	Dictionary file_sizes;
 	do {
 		//get filename
 		unz_file_info info;
@@ -480,6 +483,16 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese
 
 		String file = fname;
 
+		// HTML is handled later
+		if (file == "godot.html") {
+			if (custom_html.is_empty()) {
+				html.resize(info.uncompressed_size);
+				unzOpenCurrentFile(pkg);
+				unzReadCurrentFile(pkg, html.ptrw(), html.size());
+				unzCloseCurrentFile(pkg);
+			}
+			continue;
+		}
 		Vector<uint8_t> data;
 		data.resize(info.uncompressed_size);
 
@@ -490,14 +503,7 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese
 
 		//write
 
-		if (file == "godot.html") {
-			if (!custom_html.is_empty()) {
-				continue;
-			}
-			_fix_html(data, p_preset, p_path.get_file().get_basename(), p_debug, p_flags, shared_objects);
-			file = p_path.get_file();
-
-		} else if (file == "godot.js") {
+		if (file == "godot.js") {
 			file = p_path.get_file().get_basename() + ".js";
 
 		} else if (file == "godot.worker.js") {
@@ -511,6 +517,7 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese
 
 		} else if (file == "godot.wasm") {
 			file = p_path.get_file().get_basename() + ".wasm";
+			file_sizes[file.get_file()] = (uint64_t)info.uncompressed_size;
 		}
 
 		String dst = p_path.get_base_dir().plus_file(file);
@@ -532,19 +539,26 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese
 			EditorNode::get_singleton()->show_warning(TTR("Could not read custom HTML shell:") + "\n" + custom_html);
 			return ERR_FILE_CANT_READ;
 		}
-		Vector<uint8_t> buf;
-		buf.resize(f->get_len());
-		f->get_buffer(buf.ptrw(), buf.size());
+		html.resize(f->get_len());
+		f->get_buffer(html.ptrw(), html.size());
 		memdelete(f);
-		_fix_html(buf, p_preset, p_path.get_file().get_basename(), p_debug, p_flags, shared_objects);
-
+	}
+	{
+		FileAccess *f = FileAccess::open(pck_path, FileAccess::READ);
+		if (f) {
+			file_sizes[pck_path.get_file()] = (uint64_t)f->get_len();
+			memdelete(f);
+			f = NULL;
+		}
+		_fix_html(html, p_preset, p_path.get_file().get_basename(), p_debug, p_flags, shared_objects, file_sizes);
 		f = FileAccess::open(p_path, FileAccess::WRITE);
 		if (!f) {
 			EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + p_path);
 			return ERR_FILE_CANT_WRITE;
 		}
-		f->store_buffer(buf.ptr(), buf.size());
+		f->store_buffer(html.ptr(), html.size());
 		memdelete(f);
+		html.resize(0);
 	}
 
 	Ref<Image> splash;

+ 5 - 8
platform/javascript/http_client.h.inc

@@ -30,24 +30,21 @@
 
 // HTTPClient's additional private members in the javascript platform
 
-Error prepare_request(Method p_method, const String &p_url, const Vector<String> &p_headers);
+Error make_request(Method p_method, const String &p_url, const Vector<String> &p_headers, const uint8_t *p_body, int p_body_len);
+static void _parse_headers(int p_len, const char **p_headers, void *p_ref);
 
-int xhr_id;
+int js_id = 0;
 int read_limit = 4096;
-int response_read_offset = 0;
 Status status = STATUS_DISCONNECTED;
 
 String host;
 int port = -1;
 bool use_tls = false;
-String username;
-String password;
 
 int polled_response_code = 0;
-String polled_response_header;
-PackedByteArray polled_response;
+Vector<String> response_headers;
+Vector<uint8_t> response_buffer;
 
 #ifdef DEBUG_ENABLED
-bool has_polled = false;
 uint64_t last_polling_frame = 0;
 #endif

+ 104 - 79
platform/javascript/http_client_javascript.cpp

@@ -30,7 +30,38 @@
 
 #include "core/io/http_client.h"
 
-#include "http_request.h"
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "stddef.h"
+
+typedef enum {
+	GODOT_JS_FETCH_STATE_REQUESTING = 0,
+	GODOT_JS_FETCH_STATE_BODY = 1,
+	GODOT_JS_FETCH_STATE_DONE = 2,
+	GODOT_JS_FETCH_STATE_ERROR = -1,
+} godot_js_fetch_state_t;
+
+extern int godot_js_fetch_create(const char *p_method, const char *p_url, const char **p_headers, int p_headers_len, const uint8_t *p_body, int p_body_len);
+extern int godot_js_fetch_read_headers(int p_id, void (*parse_callback)(int p_size, const char **p_headers, void *p_ref), void *p_ref);
+extern int godot_js_fetch_read_chunk(int p_id, uint8_t *p_buf, int p_buf_size);
+extern void godot_js_fetch_free(int p_id);
+extern godot_js_fetch_state_t godot_js_fetch_state_get(int p_id);
+extern int godot_js_fetch_body_length_get(int p_id);
+extern int godot_js_fetch_http_status_get(int p_id);
+extern int godot_js_fetch_is_chunked(int p_id);
+
+#ifdef __cplusplus
+}
+#endif
+
+void HTTPClient::_parse_headers(int p_len, const char **p_headers, void *p_ref) {
+	HTTPClient *client = static_cast<HTTPClient *>(p_ref);
+	for (int i = 0; i < p_len; i++) {
+		client->response_headers.push_back(String::utf8(p_headers[i]));
+	}
+}
 
 Error HTTPClient::connect_to_host(const String &p_host, int p_port, bool p_ssl, bool p_verify_host) {
 	close();
@@ -74,7 +105,7 @@ Ref<StreamPeer> HTTPClient::get_connection() const {
 	ERR_FAIL_V_MSG(REF(), "Accessing an HTTPClient's StreamPeer is not supported for the HTML5 platform.");
 }
 
-Error HTTPClient::prepare_request(Method p_method, const String &p_url, const Vector<String> &p_headers) {
+Error HTTPClient::make_request(Method p_method, const String &p_url, const Vector<String> &p_headers, const uint8_t *p_body, int p_body_len) {
 	ERR_FAIL_INDEX_V(p_method, METHOD_MAX, ERR_INVALID_PARAMETER);
 	ERR_FAIL_COND_V_MSG(p_method == METHOD_TRACE || p_method == METHOD_CONNECT, ERR_UNAVAILABLE, "HTTP methods TRACE and CONNECT are not supported for the HTML5 platform.");
 	ERR_FAIL_COND_V(status != STATUS_CONNECTED, ERR_INVALID_PARAMETER);
@@ -83,46 +114,33 @@ Error HTTPClient::prepare_request(Method p_method, const String &p_url, const Ve
 	ERR_FAIL_COND_V(!p_url.begins_with("/"), ERR_INVALID_PARAMETER);
 
 	String url = (use_tls ? "https://" : "http://") + host + ":" + itos(port) + p_url;
-	godot_xhr_reset(xhr_id);
-	godot_xhr_open(xhr_id, _methods[p_method], url.utf8().get_data(),
-			username.is_empty() ? nullptr : username.utf8().get_data(),
-			password.is_empty() ? nullptr : password.utf8().get_data());
-
+	Vector<CharString> keeper;
+	Vector<const char *> c_strings;
 	for (int i = 0; i < p_headers.size(); i++) {
-		int header_separator = p_headers[i].find(": ");
-		ERR_FAIL_COND_V(header_separator < 0, ERR_INVALID_PARAMETER);
-		godot_xhr_set_request_header(xhr_id,
-				p_headers[i].left(header_separator).utf8().get_data(),
-				p_headers[i].right(header_separator + 2).utf8().get_data());
+		keeper.push_back(p_headers[i].utf8());
+		c_strings.push_back(keeper[i].get_data());
+	}
+	if (js_id) {
+		godot_js_fetch_free(js_id);
 	}
-	response_read_offset = 0;
+	js_id = godot_js_fetch_create(_methods[p_method], url.utf8().get_data(), c_strings.ptrw(), c_strings.size(), p_body, p_body_len);
 	status = STATUS_REQUESTING;
 	return OK;
 }
 
 Error HTTPClient::request_raw(Method p_method, const String &p_url, const Vector<String> &p_headers, const Vector<uint8_t> &p_body) {
-	Error err = prepare_request(p_method, p_url, p_headers);
-	if (err != OK)
-		return err;
 	if (p_body.is_empty()) {
-		godot_xhr_send(xhr_id, nullptr, 0);
-	} else {
-		godot_xhr_send(xhr_id, p_body.ptr(), p_body.size());
+		return make_request(p_method, p_url, p_headers, nullptr, 0);
 	}
-	return OK;
+	return make_request(p_method, p_url, p_headers, p_body.ptr(), p_body.size());
 }
 
 Error HTTPClient::request(Method p_method, const String &p_url, const Vector<String> &p_headers, const String &p_body) {
-	Error err = prepare_request(p_method, p_url, p_headers);
-	if (err != OK)
-		return err;
 	if (p_body.is_empty()) {
-		godot_xhr_send(xhr_id, nullptr, 0);
-	} else {
-		const CharString cs = p_body.utf8();
-		godot_xhr_send(xhr_id, cs.get_data(), cs.length());
+		return make_request(p_method, p_url, p_headers, nullptr, 0);
 	}
-	return OK;
+	const CharString cs = p_body.utf8();
+	return make_request(p_method, p_url, p_headers, (const uint8_t *)cs.get_data(), cs.size() - 1);
 }
 
 void HTTPClient::close() {
@@ -130,10 +148,13 @@ void HTTPClient::close() {
 	port = -1;
 	use_tls = false;
 	status = STATUS_DISCONNECTED;
-	polled_response.resize(0);
 	polled_response_code = 0;
-	polled_response_header = String();
-	godot_xhr_reset(xhr_id);
+	response_headers.resize(0);
+	response_buffer.resize(0);
+	if (js_id) {
+		godot_js_fetch_free(js_id);
+		js_id = 0;
+	}
 }
 
 HTTPClient::Status HTTPClient::get_status() const {
@@ -141,12 +162,11 @@ HTTPClient::Status HTTPClient::get_status() const {
 }
 
 bool HTTPClient::has_response() const {
-	return !polled_response_header.is_empty();
+	return response_headers.size() > 0;
 }
 
 bool HTTPClient::is_response_chunked() const {
-	// TODO evaluate using moz-chunked-arraybuffer, fetch & ReadableStream
-	return false;
+	return godot_js_fetch_is_chunked(js_id);
 }
 
 int HTTPClient::get_response_code() const {
@@ -154,36 +174,42 @@ int HTTPClient::get_response_code() const {
 }
 
 Error HTTPClient::get_response_headers(List<String> *r_response) {
-	if (polled_response_header.is_empty())
+	if (!response_headers.size()) {
 		return ERR_INVALID_PARAMETER;
-
-	Vector<String> header_lines = polled_response_header.split("\r\n", false);
-	for (int i = 0; i < header_lines.size(); ++i) {
-		r_response->push_back(header_lines[i]);
 	}
-	polled_response_header = String();
+	for (int i = 0; i < response_headers.size(); i++) {
+		r_response->push_back(response_headers[i]);
+	}
+	response_headers.clear();
 	return OK;
 }
 
 int HTTPClient::get_response_body_length() const {
-	return polled_response.size();
+	return godot_js_fetch_body_length_get(js_id);
 }
 
 PackedByteArray HTTPClient::read_response_body_chunk() {
 	ERR_FAIL_COND_V(status != STATUS_BODY, PackedByteArray());
 
-	int to_read = MIN(read_limit, polled_response.size() - response_read_offset);
-	PackedByteArray chunk;
-	chunk.resize(to_read);
-	memcpy(chunk.ptrw(), polled_response.ptr() + response_read_offset, to_read);
-	response_read_offset += to_read;
-
-	if (response_read_offset == polled_response.size()) {
-		status = STATUS_CONNECTED;
-		polled_response.resize(0);
-		godot_xhr_reset(xhr_id);
+	if (response_buffer.size() != read_limit) {
+		response_buffer.resize(read_limit);
+	}
+	int read = godot_js_fetch_read_chunk(js_id, response_buffer.ptrw(), read_limit);
+
+	// Check if the stream is over.
+	godot_js_fetch_state_t state = godot_js_fetch_state_get(js_id);
+	if (state == GODOT_JS_FETCH_STATE_DONE) {
+		status = STATUS_DISCONNECTED;
+	} else if (state != GODOT_JS_FETCH_STATE_BODY) {
+		status = STATUS_CONNECTION_ERROR;
 	}
 
+	PackedByteArray chunk;
+	if (!read) {
+		return chunk;
+	}
+	chunk.resize(read);
+	copymem(chunk.ptrw(), response_buffer.ptr(), read);
 	return chunk;
 }
 
@@ -217,48 +243,48 @@ Error HTTPClient::poll() {
 			return OK;
 
 		case STATUS_CONNECTED:
-		case STATUS_BODY:
 			return OK;
 
+		case STATUS_BODY: {
+			godot_js_fetch_state_t state = godot_js_fetch_state_get(js_id);
+			if (state == GODOT_JS_FETCH_STATE_DONE) {
+				status = STATUS_DISCONNECTED;
+			} else if (state != GODOT_JS_FETCH_STATE_BODY) {
+				status = STATUS_CONNECTION_ERROR;
+				return ERR_CONNECTION_ERROR;
+			}
+			return OK;
+		}
+
 		case STATUS_CONNECTION_ERROR:
 			return ERR_CONNECTION_ERROR;
 
 		case STATUS_REQUESTING: {
 #ifdef DEBUG_ENABLED
-			if (!has_polled) {
-				has_polled = true;
-			} else {
-				// forcing synchronous requests is not possible on the web
-				if (last_polling_frame == Engine::get_singleton()->get_process_frames()) {
-					WARN_PRINT("HTTPClient polled multiple times in one frame, "
-							   "but request cannot progress more than once per "
-							   "frame on the HTML5 platform.");
-				}
+			// forcing synchronous requests is not possible on the web
+			if (last_polling_frame == Engine::get_singleton()->get_process_frames()) {
+				WARN_PRINT("HTTPClient polled multiple times in one frame, "
+						   "but request cannot progress more than once per "
+						   "frame on the HTML5 platform.");
 			}
 			last_polling_frame = Engine::get_singleton()->get_process_frames();
 #endif
 
-			polled_response_code = godot_xhr_get_status(xhr_id);
-			if (godot_xhr_get_ready_state(xhr_id) != XHR_READY_STATE_DONE) {
+			polled_response_code = godot_js_fetch_http_status_get(js_id);
+			godot_js_fetch_state_t js_state = godot_js_fetch_state_get(js_id);
+			if (js_state == GODOT_JS_FETCH_STATE_REQUESTING) {
 				return OK;
-			} else if (!polled_response_code) {
+			} else if (js_state == GODOT_JS_FETCH_STATE_ERROR) {
+				// Fetch is in error state.
+				status = STATUS_CONNECTION_ERROR;
+				return ERR_CONNECTION_ERROR;
+			}
+			if (godot_js_fetch_read_headers(js_id, &_parse_headers, this)) {
+				// Failed to parse headers.
 				status = STATUS_CONNECTION_ERROR;
 				return ERR_CONNECTION_ERROR;
 			}
-
 			status = STATUS_BODY;
-
-			PackedByteArray bytes;
-			int len = godot_xhr_get_response_headers_length(xhr_id);
-			bytes.resize(len + 1);
-
-			godot_xhr_get_response_headers(xhr_id, reinterpret_cast<char *>(bytes.ptrw()), len);
-			bytes.ptrw()[len] = 0;
-
-			polled_response_header = String::utf8(reinterpret_cast<const char *>(bytes.ptr()));
-
-			polled_response.resize(godot_xhr_get_response_length(xhr_id));
-			godot_xhr_get_response(xhr_id, polled_response.ptrw(), polled_response.size());
 			break;
 		}
 
@@ -269,9 +295,8 @@ Error HTTPClient::poll() {
 }
 
 HTTPClient::HTTPClient() {
-	xhr_id = godot_xhr_new();
 }
 
 HTTPClient::~HTTPClient() {
-	godot_xhr_free(xhr_id);
+	close();
 }

+ 0 - 73
platform/javascript/http_request.h

@@ -1,73 +0,0 @@
-/*************************************************************************/
-/*  http_request.h                                                       */
-/*************************************************************************/
-/*                       This file is part of:                           */
-/*                           GODOT ENGINE                                */
-/*                      https://godotengine.org                          */
-/*************************************************************************/
-/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur.                 */
-/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md).   */
-/*                                                                       */
-/* Permission is hereby granted, free of charge, to any person obtaining */
-/* a copy of this software and associated documentation files (the       */
-/* "Software"), to deal in the Software without restriction, including   */
-/* without limitation the rights to use, copy, modify, merge, publish,   */
-/* distribute, sublicense, and/or sell copies of the Software, and to    */
-/* permit persons to whom the Software is furnished to do so, subject to */
-/* the following conditions:                                             */
-/*                                                                       */
-/* The above copyright notice and this permission notice shall be        */
-/* included in all copies or substantial portions of the Software.       */
-/*                                                                       */
-/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
-/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
-/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
-/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
-/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
-/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
-/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
-/*************************************************************************/
-
-#ifndef HTTP_REQUEST_H
-#define HTTP_REQUEST_H
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-#include "stddef.h"
-
-typedef enum {
-	XHR_READY_STATE_UNSENT = 0,
-	XHR_READY_STATE_OPENED = 1,
-	XHR_READY_STATE_HEADERS_RECEIVED = 2,
-	XHR_READY_STATE_LOADING = 3,
-	XHR_READY_STATE_DONE = 4,
-} godot_xhr_ready_state_t;
-
-extern int godot_xhr_new();
-extern void godot_xhr_reset(int p_xhr_id);
-extern void godot_xhr_free(int p_xhr_id);
-
-extern int godot_xhr_open(int p_xhr_id, const char *p_method, const char *p_url, const char *p_user = nullptr, const char *p_password = nullptr);
-
-extern void godot_xhr_set_request_header(int p_xhr_id, const char *p_header, const char *p_value);
-
-extern void godot_xhr_send(int p_xhr_id, const void *p_data, int p_len);
-extern void godot_xhr_abort(int p_xhr_id);
-
-/* this is an HTTPClient::ResponseCode, not ::Status */
-extern int godot_xhr_get_status(int p_xhr_id);
-extern godot_xhr_ready_state_t godot_xhr_get_ready_state(int p_xhr_id);
-
-extern int godot_xhr_get_response_headers_length(int p_xhr_id);
-extern void godot_xhr_get_response_headers(int p_xhr_id, char *r_dst, int p_len);
-
-extern int godot_xhr_get_response_length(int p_xhr_id);
-extern void godot_xhr_get_response(int p_xhr_id, void *r_dst, int p_len);
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif /* HTTP_REQUEST_H */

+ 19 - 8
platform/javascript/js/engine/config.js

@@ -100,6 +100,11 @@ const InternalConfig = function (initConfig) { // eslint-disable-line no-unused-
 		 * @type {Array.<string>}
 		 */
 		gdnativeLibs: [],
+		/**
+		 * @ignore
+		 * @type {Array.<string>}
+		 */
+		fileSizes: [],
 		/**
 		 * A callback function for handling Godot's ``OS.execute`` calls.
 		 *
@@ -219,6 +224,7 @@ const InternalConfig = function (initConfig) { // eslint-disable-line no-unused-
 		this.canvasResizePolicy = parse('canvasResizePolicy', this.canvasResizePolicy);
 		this.persistentPaths = parse('persistentPaths', this.persistentPaths);
 		this.gdnativeLibs = parse('gdnativeLibs', this.gdnativeLibs);
+		this.fileSizes = parse('fileSizes', this.fileSizes);
 		this.args = parse('args', this.args);
 		this.onExecute = parse('onExecute', this.onExecute);
 		this.onExit = parse('onExit', this.onExit);
@@ -227,10 +233,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 +244,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) {

+ 1 - 0
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) {};

+ 21 - 16
platform/javascript/js/engine/engine.js

@@ -35,14 +35,15 @@ const Engine = (function () {
 	 * Load the engine from the specified base path.
 	 *
 	 * @param {string} basePath Base path of the engine to load.
+	 * @param {number=} [size=0] The file size if known.
 	 * @returns {Promise} A Promise that resolves once the engine is loaded.
 	 *
 	 * @function Engine.load
 	 */
-	Engine.load = function (basePath) {
+	Engine.load = function (basePath, size) {
 		if (loadPromise == null) {
 			loadPath = basePath;
-			loadPromise = preloader.loadPromise(`${loadPath}.wasm`);
+			loadPromise = preloader.loadPromise(`${loadPath}.wasm`, size, true);
 			requestAnimationFrame(preloader.animateProgress);
 		}
 		return loadPromise;
@@ -96,23 +97,27 @@ const Engine = (function () {
 						initPromise = Promise.reject(new Error('A base path must be provided when calling `init` and the engine is not loaded.'));
 						return initPromise;
 					}
-					Engine.load(basePath);
+					Engine.load(basePath, this.config.fileSizes[`${basePath}.wasm`]);
 				}
-				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;
 			},
 
@@ -133,7 +138,7 @@ const Engine = (function () {
 			 * @returns {Promise} A Promise that resolves once the file is loaded.
 			 */
 			preloadFile: function (file, path) {
-				return preloader.preload(file, path);
+				return preloader.preload(file, path, this.config.fileSizes[file]);
 			},
 
 			/**

+ 74 - 51
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, fileSize, raw) {
 		tracker[file] = {
-			total: 0,
+			total: fileSize || 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,21 +117,19 @@ 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, fileSize, raw = false) {
+		return retry(loadFetch.bind(null, file, loadingFiles, fileSize, raw), DOWNLOAD_ATTEMPTS_MAX);
 	};
 
 	this.preloadedFiles = [];
-	this.preload = function (pathOrBuffer, destPath) {
+	this.preload = function (pathOrBuffer, destPath, fileSize) {
 		let buffer = null;
 		if (typeof pathOrBuffer === 'string') {
 			const me = this;
-			return this.loadPromise(pathOrBuffer).then(function (xhr) {
+			return this.loadPromise(pathOrBuffer, fileSize).then(function (buf) {
 				me.preloadedFiles.push({
 					path: destPath || pathOrBuffer,
-					buffer: xhr.response,
+					buffer: buf,
 				});
 				return Promise.resolve();
 			});

+ 2 - 2
platform/javascript/js/libs/library_godot_display.js

@@ -671,7 +671,7 @@ const GodotDisplay = {
 			document.head.appendChild(link);
 		}
 		const old_icon = GodotDisplay.window_icon;
-		const png = new Blob([GodotRuntime.heapCopy(HEAPU8, p_ptr, p_len)], { type: 'image/png' });
+		const png = new Blob([GodotRuntime.heapSlice(HEAPU8, p_ptr, p_len)], { type: 'image/png' });
 		GodotDisplay.window_icon = URL.createObjectURL(png);
 		link.href = GodotDisplay.window_icon;
 		if (old_icon) {
@@ -711,7 +711,7 @@ const GodotDisplay = {
 		const shape = GodotRuntime.parseString(p_shape);
 		const old_shape = GodotDisplayCursor.cursors[shape];
 		if (p_len > 0) {
-			const png = new Blob([GodotRuntime.heapCopy(HEAPU8, p_ptr, p_len)], { type: 'image/png' });
+			const png = new Blob([GodotRuntime.heapSlice(HEAPU8, p_ptr, p_len)], { type: 'image/png' });
 			const url = URL.createObjectURL(png);
 			GodotDisplayCursor.cursors[shape] = {
 				url: url,

+ 258 - 0
platform/javascript/js/libs/library_godot_fetch.js

@@ -0,0 +1,258 @@
+/*************************************************************************/
+/*  library_godot_fetch.js                                               */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+const GodotFetch = {
+	$GodotFetch__deps: ['$GodotRuntime'],
+	$GodotFetch: {
+
+		onread: function (id, result) {
+			const obj = IDHandler.get(id);
+			if (!obj) {
+				return;
+			}
+			if (result.value) {
+				obj.chunks.push(result.value);
+			}
+			obj.reading = false;
+			obj.done = result.done;
+		},
+
+		onresponse: function (id, response) {
+			const obj = IDHandler.get(id);
+			if (!obj) {
+				return;
+			}
+			let size = -1;
+			let compressed = false;
+			let chunked = false;
+			response.headers.forEach(function (value, header) {
+				const v = value.toLowerCase().trim();
+				const h = header.toLowerCase().trim();
+				if (h === 'content-encoding') {
+					compressed = true;
+					size = -1;
+				} else if (h === 'content-length') {
+					const len = Number.parseInt(value, 10);
+					if (!Number.isNaN(len) && !compressed) {
+						size = len;
+					}
+				} else if (h === 'transfer-encoding' && v === 'chunked') {
+					chunked = true;
+				}
+			});
+			obj.bodySize = size;
+			obj.status = response.status;
+			obj.response = response;
+			obj.reader = response.body.getReader();
+			obj.chunked = chunked;
+		},
+
+		onerror: function (id, err) {
+			GodotRuntime.error(err);
+			const obj = IDHandler.get(id);
+			if (!obj) {
+				return;
+			}
+			obj.error = err;
+		},
+
+		create: function (method, url, headers, body) {
+			const obj = {
+				request: null,
+				response: null,
+				reader: null,
+				error: null,
+				done: false,
+				reading: false,
+				status: 0,
+				chunks: [],
+				bodySize: -1,
+			};
+			const id = IDHandler.add(obj);
+			const init = {
+				method: method,
+				headers: headers,
+				body: body,
+			};
+			obj.request = fetch(url, init);
+			obj.request.then(GodotFetch.onresponse.bind(null, id)).catch(GodotFetch.onerror.bind(null, id));
+			return id;
+		},
+
+		free: function (id) {
+			const obj = IDHandler.get(id);
+			if (!obj) {
+				return;
+			}
+			IDHandler.remove(id);
+			if (!obj.request) {
+				return;
+			}
+			// Try to abort
+			obj.request.then(function (response) {
+				response.abort();
+			}).catch(function (e) { /* nothing to do */ });
+		},
+
+		read: function (id) {
+			const obj = IDHandler.get(id);
+			if (!obj) {
+				return;
+			}
+			if (obj.reader && !obj.reading) {
+				if (obj.done) {
+					obj.reader = null;
+					return;
+				}
+				obj.reading = true;
+				obj.reader.read().then(GodotFetch.onread.bind(null, id)).catch(GodotFetch.onerror.bind(null, id));
+			}
+		},
+	},
+
+	godot_js_fetch_create__sig: 'iii',
+	godot_js_fetch_create: function (p_method, p_url, p_headers, p_headers_size, p_body, p_body_size) {
+		const method = GodotRuntime.parseString(p_method);
+		const url = GodotRuntime.parseString(p_url);
+		const headers = GodotRuntime.parseStringArray(p_headers, p_headers_size);
+		const body = p_body_size ? GodotRuntime.heapSlice(HEAP8, p_body, p_body_size) : null;
+		return GodotFetch.create(method, url, headers.map(function (hv) {
+			const idx = hv.indexOf(':');
+			if (idx <= 0) {
+				return [];
+			}
+			return [
+				hv.slice(0, idx).trim(),
+				hv.slice(idx + 1).trim(),
+			];
+		}).filter(function (v) {
+			return v.length === 2;
+		}), body);
+	},
+
+	godot_js_fetch_state_get__sig: 'ii',
+	godot_js_fetch_state_get: function (p_id) {
+		const obj = IDHandler.get(p_id);
+		if (!obj) {
+			return -1;
+		}
+		if (obj.error) {
+			return -1;
+		}
+		if (!obj.response) {
+			return 0;
+		}
+		if (obj.reader) {
+			return 1;
+		}
+		if (obj.done) {
+			return 2;
+		}
+		return -1;
+	},
+
+	godot_js_fetch_http_status_get__sig: 'ii',
+	godot_js_fetch_http_status_get: function (p_id) {
+		const obj = IDHandler.get(p_id);
+		if (!obj || !obj.response) {
+			return 0;
+		}
+		return obj.status;
+	},
+
+	godot_js_fetch_read_headers__sig: 'iii',
+	godot_js_fetch_read_headers: function (p_id, p_parse_cb, p_ref) {
+		const obj = IDHandler.get(p_id);
+		if (!obj || !obj.response) {
+			return 1;
+		}
+		const cb = GodotRuntime.get_func(p_parse_cb);
+		const arr = [];
+		obj.response.headers.forEach(function (v, h) {
+			arr.push(`${h}:${v}`);
+		});
+		const c_ptr = GodotRuntime.allocStringArray(arr);
+		cb(arr.length, c_ptr, p_ref);
+		GodotRuntime.freeStringArray(c_ptr, arr.length);
+		return 0;
+	},
+
+	godot_js_fetch_read_chunk__sig: 'ii',
+	godot_js_fetch_read_chunk: function (p_id, p_buf, p_buf_size) {
+		const obj = IDHandler.get(p_id);
+		if (!obj || !obj.response) {
+			return 0;
+		}
+		let to_read = p_buf_size;
+		const chunks = obj.chunks;
+		while (to_read && chunks.length) {
+			const chunk = obj.chunks[0];
+			if (chunk.length > to_read) {
+				GodotRuntime.heapCopy(HEAP8, chunk.slice(0, to_read), p_buf);
+				chunks[0] = chunk.slice(to_read);
+				to_read = 0;
+			} else {
+				GodotRuntime.heapCopy(HEAP8, chunk, p_buf);
+				to_read -= chunk.length;
+				chunks.pop();
+			}
+		}
+		if (!chunks.length) {
+			GodotFetch.read(p_id);
+		}
+		return p_buf_size - to_read;
+	},
+
+	godot_js_fetch_body_length_get__sig: 'ii',
+	godot_js_fetch_body_length_get: function (p_id) {
+		const obj = IDHandler.get(p_id);
+		if (!obj || !obj.response) {
+			return -1;
+		}
+		return obj.bodySize;
+	},
+
+	godot_js_fetch_is_chunked__sig: 'ii',
+	godot_js_fetch_is_chunked: function (p_id) {
+		const obj = IDHandler.get(p_id);
+		if (!obj || !obj.response) {
+			return -1;
+		}
+		return obj.chunked ? 1 : 0;
+	},
+
+	godot_js_fetch_free__sig: 'vi',
+	godot_js_fetch_free: function (id) {
+		GodotFetch.free(id);
+	},
+};
+
+autoAddDeps(GodotFetch, '$GodotFetch');
+mergeInto(LibraryManager.library, GodotFetch);

+ 0 - 142
platform/javascript/js/libs/library_godot_http_request.js

@@ -1,142 +0,0 @@
-/*************************************************************************/
-/*  http_request.js                                                      */
-/*************************************************************************/
-/*                       This file is part of:                           */
-/*                           GODOT ENGINE                                */
-/*                      https://godotengine.org                          */
-/*************************************************************************/
-/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur.                 */
-/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md).   */
-/*                                                                       */
-/* Permission is hereby granted, free of charge, to any person obtaining */
-/* a copy of this software and associated documentation files (the       */
-/* "Software"), to deal in the Software without restriction, including   */
-/* without limitation the rights to use, copy, modify, merge, publish,   */
-/* distribute, sublicense, and/or sell copies of the Software, and to    */
-/* permit persons to whom the Software is furnished to do so, subject to */
-/* the following conditions:                                             */
-/*                                                                       */
-/* The above copyright notice and this permission notice shall be        */
-/* included in all copies or substantial portions of the Software.       */
-/*                                                                       */
-/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
-/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
-/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
-/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
-/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
-/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
-/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
-/*************************************************************************/
-
-const GodotHTTPRequest = {
-	$GodotHTTPRequest__deps: ['$GodotRuntime'],
-	$GodotHTTPRequest: {
-		requests: [],
-
-		getUnusedRequestId: function () {
-			const idMax = GodotHTTPRequest.requests.length;
-			for (let potentialId = 0; potentialId < idMax; ++potentialId) {
-				if (GodotHTTPRequest.requests[potentialId] instanceof XMLHttpRequest) {
-					continue;
-				}
-				return potentialId;
-			}
-			GodotHTTPRequest.requests.push(null);
-			return idMax;
-		},
-
-		setupRequest: function (xhr) {
-			xhr.responseType = 'arraybuffer';
-		},
-	},
-
-	godot_xhr_new__sig: 'i',
-	godot_xhr_new: function () {
-		const newId = GodotHTTPRequest.getUnusedRequestId();
-		GodotHTTPRequest.requests[newId] = new XMLHttpRequest();
-		GodotHTTPRequest.setupRequest(GodotHTTPRequest.requests[newId]);
-		return newId;
-	},
-
-	godot_xhr_reset__sig: 'vi',
-	godot_xhr_reset: function (xhrId) {
-		GodotHTTPRequest.requests[xhrId] = new XMLHttpRequest();
-		GodotHTTPRequest.setupRequest(GodotHTTPRequest.requests[xhrId]);
-	},
-
-	godot_xhr_free__sig: 'vi',
-	godot_xhr_free: function (xhrId) {
-		GodotHTTPRequest.requests[xhrId].abort();
-		GodotHTTPRequest.requests[xhrId] = null;
-	},
-
-	godot_xhr_open__sig: 'viiiii',
-	godot_xhr_open: function (xhrId, method, url, p_user, p_password) {
-		const user = p_user > 0 ? GodotRuntime.parseString(p_user) : null;
-		const password = p_password > 0 ? GodotRuntime.parseString(p_password) : null;
-		GodotHTTPRequest.requests[xhrId].open(GodotRuntime.parseString(method), GodotRuntime.parseString(url), true, user, password);
-	},
-
-	godot_xhr_set_request_header__sig: 'viii',
-	godot_xhr_set_request_header: function (xhrId, header, value) {
-		GodotHTTPRequest.requests[xhrId].setRequestHeader(GodotRuntime.parseString(header), GodotRuntime.parseString(value));
-	},
-
-	godot_xhr_send__sig: 'viii',
-	godot_xhr_send: function (xhrId, p_ptr, p_len) {
-		let data = null;
-		if (p_ptr && p_len) {
-			data = GodotRuntime.heapCopy(HEAP8, p_ptr, p_len);
-		}
-		GodotHTTPRequest.requests[xhrId].send(data);
-	},
-
-	godot_xhr_abort__sig: 'vi',
-	godot_xhr_abort: function (xhrId) {
-		GodotHTTPRequest.requests[xhrId].abort();
-	},
-
-	godot_xhr_get_status__sig: 'ii',
-	godot_xhr_get_status: function (xhrId) {
-		return GodotHTTPRequest.requests[xhrId].status;
-	},
-
-	godot_xhr_get_ready_state__sig: 'ii',
-	godot_xhr_get_ready_state: function (xhrId) {
-		return GodotHTTPRequest.requests[xhrId].readyState;
-	},
-
-	godot_xhr_get_response_headers_length__sig: 'ii',
-	godot_xhr_get_response_headers_length: function (xhrId) {
-		const headers = GodotHTTPRequest.requests[xhrId].getAllResponseHeaders();
-		return headers === null ? 0 : GodotRuntime.strlen(headers);
-	},
-
-	godot_xhr_get_response_headers__sig: 'viii',
-	godot_xhr_get_response_headers: function (xhrId, dst, len) {
-		const str = GodotHTTPRequest.requests[xhrId].getAllResponseHeaders();
-		if (str === null) {
-			return;
-		}
-		GodotRuntime.stringToHeap(str, dst, len);
-	},
-
-	godot_xhr_get_response_length__sig: 'ii',
-	godot_xhr_get_response_length: function (xhrId) {
-		const body = GodotHTTPRequest.requests[xhrId].response;
-		return body === null ? 0 : body.byteLength;
-	},
-
-	godot_xhr_get_response__sig: 'viii',
-	godot_xhr_get_response: function (xhrId, dst, len) {
-		let buf = GodotHTTPRequest.requests[xhrId].response;
-		if (buf === null) {
-			return;
-		}
-		buf = new Uint8Array(buf).subarray(0, len);
-		HEAPU8.set(buf, dst);
-	},
-};
-
-autoAddDeps(GodotHTTPRequest, '$GodotHTTPRequest');
-mergeInto(LibraryManager.library, GodotHTTPRequest);

+ 15 - 1
platform/javascript/js/libs/library_godot_runtime.js

@@ -72,11 +72,16 @@ const GodotRuntime = {
 			return p_heap.subarray(p_ptr / bytes, p_ptr / bytes + p_len);
 		},
 
-		heapCopy: function (p_heap, p_ptr, p_len) {
+		heapSlice: function (p_heap, p_ptr, p_len) {
 			const bytes = p_heap.BYTES_PER_ELEMENT;
 			return p_heap.slice(p_ptr / bytes, p_ptr / bytes + p_len);
 		},
 
+		heapCopy: function (p_dst, p_src, p_ptr) {
+			const bytes = p_src.BYTES_PER_ELEMENT;
+			return p_dst.set(p_src, p_ptr / bytes);
+		},
+
 		/*
 		 * Strings
 		 */
@@ -84,6 +89,15 @@ const GodotRuntime = {
 			return UTF8ToString(p_ptr); // eslint-disable-line no-undef
 		},
 
+		parseStringArray: function (p_ptr, p_size) {
+			const strings = [];
+			const ptrs = GodotRuntime.heapSub(HEAP32, p_ptr, p_size); // TODO wasm64
+			ptrs.forEach(function (ptr) {
+				strings.push(GodotRuntime.parseString(ptr));
+			});
+			return strings;
+		},
+
 		strlen: function (p_str) {
 			return lengthBytesUTF8(p_str); // eslint-disable-line no-undef
 		},