Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revamp futures #209

Open
wants to merge 9 commits into
base: 1.20
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions common/src/main/java/org/figuramc/figura/avatar/Avatar.java
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,10 @@ public void runPing(int id, byte[] data) {
});
}

public void submit(Runnable r) {
events.offer(r);
}

public LuaValue loadScript(String name, String chunk) {
return scriptError || luaRuntime == null || !loaded ? null : luaRuntime.load(name, chunk);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ public FiguraInputStream openReadStream(@LuaNotNil String path) {
Path p = securityCheck(path);
File f = p.toFile();
FileInputStream fis = new FileInputStream(f);
return new FiguraInputStream(fis);
return new FiguraInputStream(fis, parent);
} catch (FileNotFoundException e) {
throw new LuaError(e);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,37 @@
package org.figuramc.figura.lua.api.data;

import org.figuramc.figura.avatar.Avatar;
import org.figuramc.figura.lua.LuaWhitelist;
import org.figuramc.figura.lua.docs.LuaMethodDoc;
import org.figuramc.figura.lua.docs.LuaMethodOverload;
import org.figuramc.figura.lua.docs.LuaTypeDoc;
import org.jetbrains.annotations.Nullable;
import org.luaj.vm2.LuaError;
import org.luaj.vm2.LuaFunction;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.Varargs;

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.function.Consumer;
import java.util.function.Function;

@LuaWhitelist
@LuaTypeDoc(value = "future", name = "Future")
public class FiguraFuture<T> {
private final Avatar avatar;
private boolean isDone;
private boolean hasError;
private LuaError errorObject;
private final Deque<Consumer<T>> onFinish = new ArrayDeque<>();
private final Deque<Consumer<LuaError>> onFinishError = new ArrayDeque<>();
private T value;

public void handle(T value, Throwable error) {
public FiguraFuture(Avatar avatar) {
this.avatar = avatar;
}

public void complete(T value, Throwable error) {
if (error != null) error(error);
else complete(value);
}
Expand All @@ -23,6 +40,57 @@ public void complete(T value) {
if (!isDone) {
this.value = value;
isDone = true;
for (var f: onFinish) {
f.accept(value);
}
onFinish.clear();
onFinishError.clear();
}
}

public AutoCloseable onFinish(Consumer<T> f) {
if (isDone && !hasError) {
f.accept(value);
return () -> {};
} else {
onFinish.add(f);
return () -> onFinish.remove(f);
}
}
public AutoCloseable onFinish(Consumer<T> f, Consumer<LuaError> g) {
final var t = onFinish(f);
final var h = onFinishError(g);
return () -> { t.close(); h.close(); };
}
@LuaWhitelist
@LuaMethodDoc("future.on_finish")
public LuaCloseable onFinish(LuaFunction f, @Nullable LuaFunction g) {
if (avatar == null) {
throw new LuaError("Future::onFinish unavailable for legal reasons");
} else {
final var mgr = avatar.luaRuntime.typeManager;
AutoCloseable ab = onFinish(value -> avatar.submit(() -> f.invoke(mgr.javaToLua(f))));
AutoCloseable ot = g != null ? onFinishError(g) : null;
return new LuaCloseable(() -> {
ab.close();
if (ot != null) ot.close();
});
}
}

@LuaWhitelist
@LuaMethodDoc("future.on_finish_error")
public LuaCloseable onFinishError(LuaFunction f) {
return new LuaCloseable(onFinishError(err -> avatar.submit(() -> f.invoke(err.getMessageObject()))));
}

public AutoCloseable onFinishError(Consumer<LuaError> f) {
if (hasError) {
f.accept(errorObject);
return () -> {};
} else {
onFinishError.add(f);
return () -> onFinishError.remove(f);
}
}

Expand All @@ -31,6 +99,11 @@ public void error(Throwable t) {
hasError = true;
isDone = true;
errorObject = t instanceof LuaError e ? e : new LuaError(t);
for (var f: onFinishError) {
f.accept(errorObject);
}
onFinish.clear();
onFinishError.clear();
}
}

Expand All @@ -45,6 +118,7 @@ public boolean isDone() {
return isDone;
}

@LuaWhitelist
@LuaMethodDoc(
value = "future.has_error",
overloads = @LuaMethodOverload(
Expand Down Expand Up @@ -86,8 +160,77 @@ public void throwError() {
if (errorObject != null) throw errorObject;
}

public <R> FiguraFuture<R> map(Function<T, R> mapper) {
final var fut = new FiguraFuture<R>(avatar);
onFinish(v -> fut.complete(mapper.apply(v)));
onFinishError(fut::error);
return fut;
}

@LuaWhitelist
@LuaMethodDoc("future.map")
public FiguraFuture<LuaValue> map(LuaFunction mapper) {
return map(wrapLua(mapper));
}

public <R> FiguraFuture<R> andThen(Function<T, FiguraFuture<R>> f) {
final var fut = new FiguraFuture<R>(avatar);
onFinishError(fut::error);
onFinish(v -> {
final var r = f.apply(v);
r.onFinish(fut::complete);
r.onFinishError(fut::error);
});
return fut;
}
@LuaWhitelist
@LuaMethodDoc("future.and_then")
public FiguraFuture<?> andThen(LuaFunction f) {
final var fut = new FiguraFuture<>(avatar);
onFinish(v -> {
final var res = f.invoke(avatar.luaRuntime.typeManager.javaToLua(v)).arg1();
if (res.isuserdata() && res.checkuserdata() instanceof FiguraFuture<?> fut2) {
fut2.onFinish(fut::complete);
fut2.onFinishError(fut::error);
}
});
onFinishError(fut::error);
return fut;
}

public <R> Function<R, LuaValue> wrapLua(LuaFunction f) {
return a -> f.invoke(avatar.luaRuntime.typeManager.javaToLua(a)).arg1();
}

public FiguraFuture<T> handle(Function<LuaError, T> handler) {
final var fut = new FiguraFuture<T>(avatar);
onFinish(fut::complete);
onFinishError(e -> {
try {
fut.complete(handler.apply(e));
} catch (Throwable t) {
fut.error(t);
}
});
return fut;
}

@LuaWhitelist
@LuaMethodDoc("future.handle")
public FiguraFuture<LuaValue> handle(LuaFunction handler) {
return map(((Function<T, Varargs>) avatar.luaRuntime.typeManager::javaToLua).andThen(Varargs::arg1)).handle(err -> handler.invoke(err.getMessageObject()).arg1());
}

@Override
public String toString() {
return "Future(isDone=%s)".formatted(isDone);
if (isDone) {
if (hasError) {
return "Future(error: " + errorObject.toString() + ")";
} else {
return "Future(value: " + value.toString() + ")";
}
} else {
return "Future(pending)";
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.figuramc.figura.lua.api.data;

import org.figuramc.figura.avatar.Avatar;
import org.figuramc.figura.lua.LuaWhitelist;
import org.figuramc.figura.lua.docs.LuaMethodDoc;
import org.figuramc.figura.lua.docs.LuaMethodOverload;
Expand All @@ -18,13 +19,16 @@
public class FiguraInputStream extends InputStream {
private final InputStream sourceStream;
private final boolean asyncOnly;
public FiguraInputStream(InputStream sourceStream) {
this(sourceStream, false);
private final Avatar avatar;

public FiguraInputStream(InputStream sourceStream, Avatar avatar) {
this(sourceStream, false, avatar);
}

public FiguraInputStream(InputStream sourceStream, boolean asyncOnly) {
public FiguraInputStream(InputStream sourceStream, boolean asyncOnly, Avatar avatar) {
this.sourceStream = sourceStream;
this.asyncOnly = asyncOnly;
this.avatar = avatar;
}

@Override
Expand All @@ -44,7 +48,7 @@ public int read() {
public FiguraFuture<LuaString> readAsync(Integer limit) {
final int finalLimit = limit != null ? limit : available();
// Future handle that will be returned
FiguraFuture<LuaString> future = new FiguraFuture<>();
FiguraFuture<LuaString> future = new FiguraFuture<>(avatar);
// Calling an async read that will be put in a future results
CompletableFuture.supplyAsync(() -> {
try {
Expand All @@ -59,7 +63,7 @@ public FiguraFuture<LuaString> readAsync(Integer limit) {
} catch (IOException e) {
throw new LuaError(e);
}
}).whenCompleteAsync(future::handle);
}).whenCompleteAsync(future::complete);
return future;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.figuramc.figura.lua.api.data;

public class LuaCloseable implements AutoCloseable {
final AutoCloseable inner;

public LuaCloseable(AutoCloseable inner) {
this.inner = inner;
}

@Override
public void close() throws Exception {
inner.close();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public FiguraInputStream get(@LuaNotNil String path) {
try {
if (parent.resources.containsKey(path)) {
ByteArrayInputStream bais = new ByteArrayInputStream(parent.resources.get(path));
return new FiguraInputStream(new GZIPInputStream(bais));
return new FiguraInputStream(new GZIPInputStream(bais), parent);
}
} catch (IOException e) {
throw new LuaError(e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -283,11 +283,11 @@ public FiguraFuture<HttpResponse> send() {
}
parent.parent.log(NetworkingAPI.LogSource.HTTP, Component.literal("Sent %s request to %s".formatted(method, uri)));
HttpRequest req = this.getRequest();
FiguraFuture<HttpResponse> future = new FiguraFuture<>();
FiguraFuture<HttpResponse> future = new FiguraFuture<>(parent.parent.owner);
var asyncResponse = parent.httpClient.sendAsync(req, java.net.http.HttpResponse.BodyHandlers.ofInputStream());
asyncResponse.whenCompleteAsync((response, t) -> {
if (t != null) future.error(t);
else future.complete(new HttpResponse(new FiguraInputStream(response.body()),
else future.complete(new HttpResponse(new FiguraInputStream(response.body(), parent.parent.owner),
response.statusCode(), response.headers().map()));
});
return future;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent;
import org.figuramc.figura.FiguraMod;
import org.figuramc.figura.lua.api.data.FiguraFuture;
import org.figuramc.figura.lua.docs.LuaMethodOverload;
import org.figuramc.figura.utils.ColorUtils;
import org.luaj.vm2.LuaError;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.*;
import org.figuramc.figura.avatar.Avatar;
import org.figuramc.figura.config.Configs;
import org.figuramc.figura.lua.LuaWhitelist;
import org.figuramc.figura.lua.docs.LuaFieldDoc;
import org.figuramc.figura.lua.docs.LuaMethodDoc;
import org.figuramc.figura.lua.docs.LuaTypeDoc;
import org.figuramc.figura.permissions.Permissions;
import org.luaj.vm2.lib.OneArgFunction;
import org.luaj.vm2.lib.ZeroArgFunction;

import java.io.*;
import java.net.MalformedURLException;
Expand All @@ -25,6 +27,7 @@
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.Locale;
import java.util.function.Consumer;

@LuaWhitelist
@LuaTypeDoc(
Expand Down Expand Up @@ -238,6 +241,36 @@ public String toString() {
}
}

@LuaWhitelist
@LuaMethodDoc("net.new_future")
public Varargs newFuture(LuaFunction k) {
final var fut = new FiguraFuture<LuaValue>(owner);
final var mgr = owner.luaRuntime.typeManager;
final var ret = mgr.javaToLua(fut).arg1();
final var onc = new OneArgFunction() {
@Override
public LuaValue call(LuaValue val) {
fut.complete(val);
return ret;
}
};
final var one = new OneArgFunction() {
@Override
public LuaValue call(LuaValue err) {
fut.error(new LuaError(err));
return ret;
}
};
if (k != null) {
try {
k.call(onc, one);
} catch (Throwable t) {
fut.error(t);
}
}
return LuaValue.varargsOf(new LuaValue[] {ret, onc, one});
}

@Override
public String toString() {
return "NetworkingAPI";
Expand Down
6 changes: 6 additions & 0 deletions common/src/main/resources/assets/figura/lang/en_us.json
Original file line number Diff line number Diff line change
Expand Up @@ -1808,6 +1808,7 @@
"figura.docs.net.socket": "Instance of SocketAPI",
"figura.docs.net.is_networking_allowed": "Checks if your avatar can use networking features. Always false if networking is OFF in settings",
"figura.docs.net.is_link_allowed": "Checks if specified link allowed for usage in networking api",
"figura.docs.net.new_future": "Creates a new future controlled by Lua code\nReturns 3 values: the created future, a function to complete the future, and a function to make the future error\nIf an (optional) function is supplied, it will be called with the latter two functions, like JavaScript\nAny error that this function throws will automatically cause the future to fail",
"figura.docs.http": "A global API that contains HTTP related features",
"figura.docs.http.request": "Creates request builder for specified URI",
"figura.docs.http_request_builder": "A builder for HTTP request",
Expand All @@ -1821,11 +1822,16 @@
"figura.docs.http_request_builder.get_headers": "Returns table with all headers set for this request",
"figura.docs.http_request_builder.send": "Sends this request and returns Future object that will contain response object once request is done",
"figura.docs.future": "Object that contains result of operation that cant be finished immediately",
"figura.docs.future.on_finish": "Runs the given function when the future is complete\nIf the future is already complete, the function is called immediately\nIf the future errors, the callback is silently discarded\nThe function can be disconnected early by calling :close() on the returned closeable",
"figura.docs.future.on_finish_error": "Runs the given function when the future fails with an error\nIf the future has already failed, the function is called immediately\nIf the future completes successfully, thd callback is silently discarded\nThe function can be disconnected early by calling :close() on the returned closeable",
"figura.docs.future.is_done": "Checks if future is done, either successfully or with error",
"figura.docs.future.has_error": "Checks if error occurred while this future execution",
"figura.docs.future.get_value": "Returns value of this future object if future was executed successfully",
"figura.docs.future.get_or_error": "Throws error if it occurred while execution of this future, returns value otherwise",
"figura.docs.future.throw_error": "Throws an error if it occurred while execution of this future.",
"figura.docs.future.map": "Returns a new future that applies the given function to the return value\nIf the original future errored, the new future will have the same error\nIf you want to have the returned future waited for (like Haskell >>=), use :andThen",
"figura.docs.future.and_then": "Returns a new future that applies the given function to the return value and then waits for the returned future\nIf the function does not return a future, this will act like :map\nAny error in either the first or second future will cause the resulting future to have that error\nIf you do not want to wait for a returned future, use :map",
"figura.docs.future.handle": "Handles errors thrown by the future\nAny successful values will be preserved\nThe given function will be called with any error the source future\nThe resulting future will error if both the source future and supplied function error\n",
"figura.docs.http_response": "Object that contains HTTP response",
"figura.docs.http_response.get_data": "Returns input stream with response data",
"figura.docs.http_response.get_response_code": "Returns response code",
Expand Down
Loading