mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2026-10-26 17:24:27 +00:00
Update web emulator to use fetch for HTTP requests
This will allow us to support streaming in the future. TeaVM doesn't have bindings for this right now, we should upstream this.
This commit is contained in:
7 files changed
+226
-37
No files matched your search
@@ -72,7 +72,7 @@ public class Builder {
|
||||
|
||||
// Then finally start the compiler!
|
||||
run(output, remapper, TeaVMTargetType.JAVASCRIPT, TeaVMOptimizationLevel.ADVANCED, minify);
|
||||
run(output, remapper, TeaVMTargetType.WEBASSEMBLY_GC, TeaVMOptimizationLevel.SIMPLE, minify);
|
||||
run(output, remapper, TeaVMTargetType.WEBASSEMBLY_GC, TeaVMOptimizationLevel.ADVANCED, minify);
|
||||
|
||||
try (var runtime = Objects.requireNonNull(Builder.class.getClassLoader()).getResourceAsStream("org/teavm/backend/wasm/wasm-gc-module-runtime.js")) {
|
||||
if (runtime == null) throw new IllegalStateException("Cannot find WASM runtime");
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
package cc.tweaked.web.http;
|
||||
|
||||
import cc.tweaked.web.Main;
|
||||
import com.google.common.base.Splitter;
|
||||
import cc.tweaked.web.js.Console;
|
||||
import cc.tweaked.web.js.abort.AbortController;
|
||||
import cc.tweaked.web.js.fetch.Headers;
|
||||
import cc.tweaked.web.js.fetch.RequestInit;
|
||||
import cc.tweaked.web.js.fetch.Response;
|
||||
import dan200.computercraft.core.Logging;
|
||||
import dan200.computercraft.core.apis.IAPIEnvironment;
|
||||
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
|
||||
@@ -20,8 +24,10 @@ import io.netty.handler.codec.http.HttpMethod;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.teavm.jso.JSObject;
|
||||
import org.teavm.jso.ajax.XMLHttpRequest;
|
||||
import org.teavm.jso.typedarrays.ArrayBuffer;
|
||||
import org.teavm.jso.core.JSError;
|
||||
import org.teavm.jso.core.JSPromise;
|
||||
import org.teavm.jso.typedarrays.Int8Array;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
@@ -74,10 +80,10 @@ final class HttpRequest extends Resource<HttpRequest, HttpRequest.State> {
|
||||
}
|
||||
}
|
||||
|
||||
private record Fetching(XMLHttpRequest request) implements State {
|
||||
private record Fetching(AbortController abort) implements State {
|
||||
@Override
|
||||
public void close() {
|
||||
request.abort();
|
||||
abort.abort();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,18 +94,31 @@ final class HttpRequest extends Resource<HttpRequest, HttpRequest.State> {
|
||||
|
||||
private @Nullable State doRequest(HttpUri uri, HttpMethod method) {
|
||||
try {
|
||||
var request = new XMLHttpRequest();
|
||||
request.setOnReadyStateChange(() -> onResponseStateChange(request));
|
||||
request.setResponseType("arraybuffer");
|
||||
var address = uri.getUri().toASCIIString();
|
||||
request.open(method.toString(), Main.CORS_PROXY.isEmpty() ? address : Main.CORS_PROXY.replace("{}", address));
|
||||
var abort = new AbortController();
|
||||
|
||||
var request = RequestInit.create();
|
||||
request.setMethod(method.name());
|
||||
if (postBuffer != null) request.setBody(Int8Array.fromJavaBuffer(postBuffer));
|
||||
request.setSignal(abort.getSignal());
|
||||
|
||||
var requestHeaders = new Headers();
|
||||
for (var iterator = headers.iteratorAsString(); iterator.hasNext(); ) {
|
||||
var header = iterator.next();
|
||||
request.setRequestHeader(header.getKey(), header.getValue());
|
||||
requestHeaders.append(header.getKey(), header.getValue());
|
||||
}
|
||||
request.setRequestHeader("X-CC-Redirect", followRedirects ? "true" : "false");
|
||||
request.send(postBuffer == null ? null : Int8Array.fromJavaBuffer(postBuffer));
|
||||
return new Fetching(request);
|
||||
requestHeaders.append("X-CC-Redirect", Boolean.toString(followRedirects));
|
||||
|
||||
var address = uri.getUri().toASCIIString();
|
||||
Response.fetch(Main.CORS_PROXY.isEmpty() ? address : Main.CORS_PROXY.replace("{}", address), request)
|
||||
.then(this::handleResponse)
|
||||
.catchError(e -> {
|
||||
if (e instanceof JSError err) Console.error(err);
|
||||
LOG.error(Logging.HTTP_ERROR, "Error in HTTP request", e);
|
||||
close(Fetching.class, () -> queueFailure("Could not connect"));
|
||||
return null;
|
||||
});
|
||||
|
||||
return new Fetching(abort);
|
||||
} catch (Exception e) {
|
||||
LOG.error(Logging.HTTP_ERROR, "Error in HTTP request", e);
|
||||
queueFailure("Could not connect");
|
||||
@@ -107,39 +126,39 @@ final class HttpRequest extends Resource<HttpRequest, HttpRequest.State> {
|
||||
}
|
||||
}
|
||||
|
||||
private void onResponseStateChange(XMLHttpRequest request) {
|
||||
if (request.getReadyState() != XMLHttpRequest.DONE) return;
|
||||
if (request.getStatus() == 0) {
|
||||
private @Nullable JSPromise<? extends @Nullable JSObject> handleResponse(Response response) {
|
||||
if (response.getStatus() == 0) {
|
||||
close(Fetching.class, () -> queueFailure("Could not connect"));
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
var buffer = (ArrayBuffer) request.getResponse();
|
||||
SeekableByteChannel contents = new ArrayByteChannel(new Int8Array(buffer).copyToJavaArray());
|
||||
var reader = new ReadHandle(contents, binary);
|
||||
|
||||
Map<String, String> responseHeaders = new HashMap<>();
|
||||
for (var header : Splitter.on("\r\n").split(request.getAllResponseHeaders())) {
|
||||
var index = header.indexOf(':');
|
||||
if (index < 0) continue;
|
||||
|
||||
response.getHeaders().forEach((v, k, h) -> {
|
||||
// Normalise the header (so "content-type" becomes "Content-Type")
|
||||
var upcase = true;
|
||||
var headerBuilder = new StringBuilder(index);
|
||||
for (var i = 0; i < index; i++) {
|
||||
var c = header.charAt(i);
|
||||
var headerBuilder = new StringBuilder(k.length());
|
||||
for (var i = 0; i < k.length(); i++) {
|
||||
var c = k.charAt(i);
|
||||
headerBuilder.append(upcase ? Character.toUpperCase(c) : c);
|
||||
upcase = c == '-';
|
||||
}
|
||||
responseHeaders.put(headerBuilder.toString(), header.substring(index + 1).trim());
|
||||
}
|
||||
var stream = new HttpResponseHandle(reader, request.getStatus(), request.getStatusText(), responseHeaders);
|
||||
responseHeaders.put(headerBuilder.toString(), v);
|
||||
});
|
||||
|
||||
if (request.getStatus() >= 200 && request.getStatus() < 400) {
|
||||
close(Fetching.class, () -> environment.queueEvent(SUCCESS_EVENT, address, stream));
|
||||
} else {
|
||||
close(Fetching.class, () -> environment.queueEvent(FAILURE_EVENT, address, request.getStatusText(), stream));
|
||||
}
|
||||
return response.bytes().then(body -> {
|
||||
SeekableByteChannel contents = new ArrayByteChannel(new Int8Array(body).copyToJavaArray());
|
||||
var reader = new ReadHandle(contents, binary);
|
||||
|
||||
var stream = new HttpResponseHandle(reader, response.getStatus(), response.getStatusText(), responseHeaders);
|
||||
|
||||
if (response.getStatus() >= 200 && response.getStatus() < 400) {
|
||||
close(Fetching.class, () -> environment.queueEvent(SUCCESS_EVENT, address, stream));
|
||||
} else {
|
||||
close(Fetching.class, () -> environment.queueEvent(FAILURE_EVENT, address, response.getStatusText(), stream));
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private void queueFailure(String message) {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// SPDX-FileCopyrightText: 2026 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package cc.tweaked.web.js.abort;
|
||||
|
||||
import org.teavm.jso.JSClass;
|
||||
import org.teavm.jso.JSObject;
|
||||
import org.teavm.jso.JSProperty;
|
||||
|
||||
/**
|
||||
* The {@link AbortController} interface represents a controller object that allows you to abort one or more Web
|
||||
* requests as and when desired.
|
||||
*
|
||||
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/AbortController">MDN Reference</a>
|
||||
*/
|
||||
@JSClass
|
||||
public class AbortController implements JSObject {
|
||||
public AbortController() {
|
||||
}
|
||||
|
||||
@JSProperty
|
||||
public native AbortSignal getSignal();
|
||||
|
||||
public native void abort();
|
||||
|
||||
public native void abort(String reason);
|
||||
|
||||
public native void abort(JSObject reason);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// SPDX-FileCopyrightText: 2026 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package cc.tweaked.web.js.abort;
|
||||
|
||||
import org.teavm.jso.JSObject;
|
||||
import org.teavm.jso.JSProperty;
|
||||
import org.teavm.jso.dom.events.EventTarget;
|
||||
|
||||
/**
|
||||
* The {@link AbortSignal} interface represents a signal object that allows you to communicate with an asynchronous
|
||||
* operation (such as a fetch request) and abort it if required via an {@link AbortController} object.
|
||||
*
|
||||
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal">MDN Reference</a>
|
||||
*/
|
||||
public interface AbortSignal extends EventTarget {
|
||||
@JSProperty
|
||||
boolean getAborted();
|
||||
|
||||
@JSProperty
|
||||
JSObject getReason();
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// SPDX-FileCopyrightText: 2026 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package cc.tweaked.web.js.fetch;
|
||||
|
||||
import org.teavm.jso.JSClass;
|
||||
import org.teavm.jso.JSFunctor;
|
||||
import org.teavm.jso.JSObject;
|
||||
|
||||
/**
|
||||
* The {@link Headers} interface of the Fetch API allows you to perform various actions on HTTP request and response
|
||||
* headers. These actions include retrieving, setting, adding to, and removing headers from the list of the request's
|
||||
* headers.
|
||||
*
|
||||
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Headers">MDN Reference</a>
|
||||
*/
|
||||
@JSClass
|
||||
public class Headers implements JSObject {
|
||||
public native void append(String key, String value);
|
||||
|
||||
public native void forEach(ForEachConsumer consumer);
|
||||
|
||||
@JSFunctor
|
||||
@FunctionalInterface
|
||||
public interface ForEachConsumer extends JSObject {
|
||||
void accept(String value, String key, Headers object);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// SPDX-FileCopyrightText: 2026 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package cc.tweaked.web.js.fetch;
|
||||
|
||||
import cc.tweaked.web.js.abort.AbortSignal;
|
||||
import org.teavm.jso.JSClass;
|
||||
import org.teavm.jso.JSObject;
|
||||
import org.teavm.jso.JSProperty;
|
||||
import org.teavm.jso.core.JSMapLike;
|
||||
import org.teavm.jso.core.JSObjects;
|
||||
import org.teavm.jso.typedarrays.ArrayBuffer;
|
||||
import org.teavm.jso.typedarrays.TypedArray;
|
||||
|
||||
/**
|
||||
* The properties that can be passed to {@link Response#fetch(String, RequestInit)}.
|
||||
*
|
||||
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/RequestInit">MDN Reference</a>
|
||||
*/
|
||||
@JSClass(transparent = true)
|
||||
public abstract class RequestInit implements JSObject {
|
||||
private RequestInit() {
|
||||
}
|
||||
|
||||
public static RequestInit create() {
|
||||
return JSObjects.create();
|
||||
}
|
||||
|
||||
@JSProperty
|
||||
public native void setMethod(String method);
|
||||
|
||||
@JSProperty
|
||||
public native void setBody(String type);
|
||||
|
||||
@JSProperty
|
||||
public native void setBody(ArrayBuffer body);
|
||||
|
||||
@JSProperty
|
||||
public native void setBody(TypedArray body);
|
||||
|
||||
@JSProperty
|
||||
public native void setHeaders(Headers headers);
|
||||
|
||||
@JSProperty
|
||||
public native void setHeaders(JSMapLike<String> headers);
|
||||
|
||||
@JSProperty
|
||||
public native void setSignal(AbortSignal signal);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// SPDX-FileCopyrightText: 2026 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package cc.tweaked.web.js.fetch;
|
||||
|
||||
import org.teavm.jso.JSBody;
|
||||
import org.teavm.jso.JSObject;
|
||||
import org.teavm.jso.JSProperty;
|
||||
import org.teavm.jso.core.JSPromise;
|
||||
import org.teavm.jso.typedarrays.Uint8Array;
|
||||
|
||||
/**
|
||||
* The {@link Response} interface of the Fetch API represents the response to a request.
|
||||
*
|
||||
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Response">MDN Reference</a>
|
||||
*/
|
||||
public abstract class Response implements JSObject {
|
||||
private Response() {
|
||||
}
|
||||
|
||||
@JSBody(params = "url", script = "return fetch(url)")
|
||||
public static native JSPromise<Response> fetch(String url);
|
||||
|
||||
@JSBody(params = { "url", "options" }, script = "return fetch(url, options)")
|
||||
public static native JSPromise<Response> fetch(String url, RequestInit options);
|
||||
|
||||
@JSProperty
|
||||
public native int getStatus();
|
||||
|
||||
@JSProperty
|
||||
public native String getStatusText();
|
||||
|
||||
@JSProperty
|
||||
public native Headers getHeaders();
|
||||
|
||||
public native JSPromise<Uint8Array> bytes();
|
||||
}
|
||||
Reference in new issue
Block a user