currently refactoring a lot

This commit is contained in:
kento2 2026-07-04 01:03:46 +02:00
parent 701a5ff620
commit 7fe7409a15
49 changed files with 856 additions and 767 deletions

View file

@ -21,6 +21,9 @@ dependencies {
// https://mvnrepository.com/artifact/org.mongodb/bson
implementation("org.mongodb:bson:5.8.0")
api("org.mongodb:bson:5.8.0")
// http://forge.kentoj.de/scrow/-/packages/maven/de.kentoj.scrow:kencommandapi-core
api("de.kentoj.scrow:kencommandapi-core:0.21")
}
publishing {

View file

@ -0,0 +1,57 @@
package de.kentoj.scrowlib.convention;
import de.kentoj.kencommandapi.api.platform.MessageStyle;
import lombok.Getter;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import java.util.List;
public class ScrowMessageStyle implements MessageStyle {
@Getter
private final Component prefix;
@Getter
private final String prefixText;
@Getter
private final TextColor prefixColor;
public ScrowMessageStyle(String prefixText, TextColor prefixColor) {
this.prefixText = prefixText;
this.prefixColor = prefixColor;
this.prefix = Component.text(prefixText)
.color(prefixColor)
.decorate(TextDecoration.BOLD);
}
@Override
public Component ok(Component msg) {
return prefix.append(Component.text("").color(NamedTextColor.GRAY))
.append(msg.colorIfAbsent(NamedTextColor.GRAY));
}
public Component list(Component header, List<? extends Component> list) {
var res = ok(header);
for (Component component : list) {
res = res.appendNewline()
.append(Component.text(" - ").color(NamedTextColor.GRAY))
.append(component.colorIfAbsent(NamedTextColor.GRAY));
}
return res;
}
@Override
public Component err(Component msg) {
return ok(msg.color(NamedTextColor.RED));
}
@Override
public Component exception(Component msg) {
return ok(Component.text("ERROR: ")
.color(NamedTextColor.DARK_RED)
.append(msg)
.decorate(TextDecoration.BOLD));
}
}

View file

@ -0,0 +1,46 @@
package de.kentoj.scrowlib.messaging;
import com.leakyabstractions.result.api.Result;
import io.nats.client.Connection;
import io.nats.client.Dispatcher;
import lombok.extern.slf4j.Slf4j;
import org.bson.Document;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Slf4j
public class MessageBroker {
private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
private final Connection nats;
protected final Dispatcher dispatcher;
public MessageBroker(Connection nats) {
this.nats = nats;
this.dispatcher = nats.createDispatcher();
}
public void publish(String subject, Result<Document, String> result) {
nats.publish(subject, ResultRepository.toBytes(result));
}
public void handle(String subject, RequestHandler handler) {
dispatcher.subscribe(subject, msg -> {
if (msg.getReplyTo() == null) {
log.warn("request in {} has replyTo unset; ignoring", subject);
return;
}
handler.handle(DocumentRepository.fromMessage(msg))
.thenAccept(result ->
executor.submit(() -> publish(msg.getReplyTo(), result))
);
});
}
public Result<Document, String> request(String subject, Document document) {
var response = nats.request(subject, DocumentRepository.toBytes(document)).join();
return ResultRepository.fromDocument(DocumentRepository.fromMessage(response));
}
}

View file

@ -1,57 +0,0 @@
package de.kentoj.scrowlib.messaging;
import de.kentoj.scrowlib.messaging.respose.SafeResult;
import io.nats.client.Connection;
import io.nats.client.Dispatcher;
import io.nats.client.Message;
import org.bson.Document;
import org.jetbrains.annotations.NotNull;
import reactor.core.Disposable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.concurrent.atomic.AtomicBoolean;
public class NatsRepository {
private final Connection nats;
private final Dispatcher dispatcher;
public NatsRepository(Connection nats) {
this.nats = nats;
this.dispatcher = nats.createDispatcher();
}
public Mono<@NotNull Void> publishSafeResult(String subject, SafeResult safeResult) {
return this.publish(subject, safeResult.toDocument());
}
public Mono<@NotNull Void> publish(String subject, Document document) {
return publishRaw(subject, DocumentRepository.toBytes(document));
}
public Mono<@NotNull Void> publishRaw(String subject, byte[] data) {
return Mono.fromRunnable(() -> nats.publish(subject, data));
}
public Flux<@NotNull SubscriptionContext> subscribe(String subject) {
return Flux.create(sink -> {
dispatcher.subscribe(subject, msg -> {
try {
sink.next(new SubscriptionContext(msg));
} catch (Throwable e) {
sink.error(e);
}
});
});
}
public Mono<@NotNull Message> requestRaw(String subject, Document document) {
return Mono.fromFuture(nats.request(subject, DocumentRepository.toBytes(document)));
}
public Mono<@NotNull Message> requestRaw(String subject, byte[] data) {
return Mono.fromFuture(nats.request(subject, data));
}
}

View file

@ -0,0 +1,16 @@
package de.kentoj.scrowlib.messaging;
import com.leakyabstractions.result.api.Result;
import org.bson.Document;
import java.util.concurrent.CompletionStage;
@FunctionalInterface
public interface RequestHandler {
/**
* Processes a request and replies
*
* @return result to send back to the requesting client.
*/
CompletionStage<Result<Document, String>> handle(Document request);
}

View file

@ -0,0 +1,26 @@
package de.kentoj.scrowlib.messaging;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.bson.Document;
@NoArgsConstructor(access = AccessLevel.NONE)
public class ResultRepository {
public static byte[] toBytes(Result<Document, String> result) {
var doc = result.hasSuccess()
? new Document("data", result.getSuccess().orElseThrow())
: new Document("error", result.getFailure().orElseThrow());
return DocumentRepository.toBytes(doc);
}
public static Result<Document, String> fromDocument(Document doc) {
var error = doc.getString("error");
return error != null
? Results.failure(error)
: Results.success(doc.get("data", Document.class));
}
}

View file

@ -1,17 +0,0 @@
package de.kentoj.scrowlib.messaging;
import io.nats.client.Message;
import lombok.AllArgsConstructor;
import lombok.Value;
import org.bson.Document;
@Value
@AllArgsConstructor
public class SubscriptionContext {
Message msg;
Document document;
public SubscriptionContext(Message msg) {
this(msg, DocumentRepository.fromMessage(msg));
}
}

View file

@ -1,34 +0,0 @@
package de.kentoj.scrowlib.messaging.respose;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.bson.Document;
@Getter
@AllArgsConstructor(access = AccessLevel.PACKAGE)
public final class SafeErrorResult extends SafeResult {
private final String type;
private final String message;
public SafeErrorResult(Throwable throwable) {
this.type = throwable.getClass().getCanonicalName();
this.message = throwable.getMessage();
}
@Override
public Document unwrap() {
throw this.toException();
}
public RuntimeException toException() {
return new RuntimeException(message);
}
@Override
public Document toDocument() {
return new Document("error", new Document()
.append("type", this.type)
.append("message", this.getMessage()));
}
}

View file

@ -1,71 +0,0 @@
package de.kentoj.scrowlib.messaging.respose;
import de.kentoj.scrowlib.messaging.DocumentRepository;
import io.nats.client.Message;
import org.bson.Document;
import org.jetbrains.annotations.NotNull;
import reactor.core.publisher.Mono;
public abstract sealed class SafeResult permits SafeErrorResult, SafeSuccessResult {
/**
* @return the SafeResult as a Document for transit
*/
public abstract Document toDocument();
/**
* @return the data stored in the SafeResult
* @throws RuntimeException if the SafeResult represents an error
*/
public abstract Document unwrap();
public boolean isError() {
return this instanceof SafeErrorResult;
}
public boolean isSuccess() {
return this instanceof SafeSuccessResult;
}
public SafeErrorResult asError() {
return (SafeErrorResult) this;
}
public SafeSuccessResult asSuccess() {
return (SafeSuccessResult) this;
}
public static Document unwrap(Message message) {
return fromMessage(message).unwrap();
}
public static SafeResult wrap(Document document) {
return new SafeSuccessResult(document);
}
public static SafeResult wrap(Throwable throwable) {
return new SafeErrorResult(throwable);
}
public static SafeResult fromMessage(Message message) {
return fromDocument(DocumentRepository.fromMessage(message));
}
public static SafeResult fromDocument(Document document) {
if (document.containsKey("error")) {
var doc = document.get("error", Document.class);
return new SafeErrorResult(
doc.getString("type"),
doc.getString("message")
);
} else if (document.containsKey("data")) {
return new SafeSuccessResult(document.get("data", Document.class));
}
throw new IllegalArgumentException("document does not conform to SafeDocument format");
}
public static Mono<@NotNull SafeResult> mono(Mono<@NotNull Document> mono) {
return mono.map(SafeResult::wrap)
.onErrorResume(err -> Mono.just(SafeResult.wrap(err)));
}
}

View file

@ -1,22 +0,0 @@
package de.kentoj.scrowlib.messaging.respose;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.bson.Document;
@Getter
@AllArgsConstructor(access = AccessLevel.PACKAGE)
public final class SafeSuccessResult extends SafeResult {
private final Document data;
@Override
public Document unwrap() {
return this.getData();
}
@Override
public Document toDocument() {
return new Document("data", this.getData());
}
}

View file

@ -0,0 +1,23 @@
package de.kentoj.scrowlib.servermanager;
import com.leakyabstractions.result.api.Result;
import org.jspecify.annotations.Nullable;
import java.util.List;
public interface InstanceManager {
/**
* @return Mono with handle of deployed server
*/
Result<ServerInstance, String> deployInstance(String template);
Result<Void, String> destroyInstance(String handle);
/**
* @return Flux with handles of running servers
*/
Result<List<ServerInstance>, String> getInstances();
Result<@Nullable ServerInstance, String> getInstance(String handle);
}

View file

@ -0,0 +1,45 @@
package de.kentoj.scrowlib.servermanager;
import com.leakyabstractions.result.api.Result;
import de.kentoj.scrowlib.messaging.MessageBroker;
import io.nats.client.Connection;
import org.bson.Document;
import org.jspecify.annotations.Nullable;
import java.util.List;
public class InstanceManagerImpl implements InstanceManager {
private final MessageBroker messageBroker;
public InstanceManagerImpl(Connection natsCon) {
messageBroker = new MessageBroker(natsCon);
}
@Override
public Result<ServerInstance, String> deployInstance(String template) {
var result = messageBroker.request("instance.deploy", new Document("template", template));
return result.mapSuccess(ServerInstance::fromDocument);
}
@Override
public Result<Void, String> destroyInstance(String handle) {
var result = messageBroker.request("instance.destroy", new Document("handle", handle));
return result.mapSuccess(__ -> null);
}
@Override
public Result<List<ServerInstance>, String> getInstances() {
var result = messageBroker.request("instance.list", new Document());
return result.mapSuccess(doc -> doc.getList("instances", Document.class)
.stream()
.map(ServerInstance::fromDocument)
.toList());
}
@Override
public Result<@Nullable ServerInstance, String> getInstance(String handle) {
var result = messageBroker.request("instance.get", new Document("handle", handle));
return result.mapSuccess(ServerInstance::fromDocument);
}
}

View file

@ -2,25 +2,27 @@ package de.kentoj.scrowlib.servermanager;
import com.google.common.base.Preconditions;
import lombok.Value;
import org.bson.Document;
@Value
public class ServerInstance {
String templateName;
String handle;
String template;
int port;
String state;
public ServerInstance(String templateName, String handle, int port, String state) {
Preconditions.checkNotNull(templateName);
public ServerInstance(String handle, String template, int port) {
Preconditions.checkNotNull(template);
Preconditions.checkNotNull(handle);
Preconditions.checkNotNull(state);
this.templateName = templateName;
this.template = template;
this.handle = handle;
this.port = port;
this.state = state;
}
public ServerInstance(String templateName, String handle, int port) {
this(templateName, handle, port, "STARTING");
static ServerInstance fromDocument(Document document) {
return new ServerInstance(
document.getString("handle"),
document.getString("template"),
document.getInteger("port")
);
}
}

View file

@ -1,31 +0,0 @@
package de.kentoj.scrowlib.servermanager;
import org.jetbrains.annotations.NotNull;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.UUID;
public interface ServerManager {
/**
* @return Mono with handle of deployed server
*/
Mono<@NotNull ServerInstance> deployServer(String template);
/**
* Attempts a clean shutdown, if that doesn't succeed, kills the server.
*/
Mono<@NotNull Void> destroyServer(String handle);
/**
* @return Flux with handles of running servers
*/
Flux<@NotNull ServerInstance> getRunningServers();
Mono<@NotNull ServerInstance> getInfo(String handle);
Mono<@NotNull Void> setState(String handle, String state);
Mono<@NotNull Void> sendPlayer(UUID playerId, String serverHandle);
}

View file

@ -1,88 +0,0 @@
package de.kentoj.scrowlib.servermanager;
import com.google.common.base.Preconditions;
import de.kentoj.scrowlib.messaging.NatsRepository;
import de.kentoj.scrowlib.messaging.respose.SafeResult;
import io.nats.client.Connection;
import org.bson.Document;
import org.jetbrains.annotations.NotNull;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.UUID;
public class ServerManagerImpl implements ServerManager {
private final NatsRepository natsRepo;
public ServerManagerImpl(Connection nats) {
this.natsRepo = new NatsRepository(nats);
}
@Override
public Mono<@NotNull ServerInstance> deployServer(String template) {
return natsRepo.requestRaw("crown.deploy-server", new Document("template", template))
.map(SafeResult::unwrap)
.map(doc -> new ServerInstance(
template,
doc.getString("handle"),
doc.getInteger("port")
));
}
@Override
public Flux<@NotNull ServerInstance> getRunningServers() {
return natsRepo.requestRaw("crown.list-servers", new Document())
.map(SafeResult::unwrap)
.map(doc ->
doc.getList("instances", Document.class)
)
.flatMapIterable(l -> l)
.map(doc -> new ServerInstance(
doc.getString("templateName"),
doc.getString("handle"),
doc.getInteger("port"),
doc.getString("state")
));
}
@Override
public Mono<@NotNull Void> destroyServer(String handle) {
return natsRepo.requestRaw("crown.destroy-server", new Document("handle", handle))
.map(SafeResult::unwrap)
.then();
}
@Override
public Mono<@NotNull ServerInstance> getInfo(String handle) {
return natsRepo.requestRaw("crown.get-info", new Document("handle", handle))
.map(SafeResult::unwrap)
.map(doc -> new ServerInstance(
doc.getString("templateName"),
handle,
doc.getInteger("port"),
doc.getString("state")
));
}
@Override
public Mono<@NotNull Void> setState(String handle, String state) {
Preconditions.checkArgument(!state.contains(" "), "state may not contain spaces");
var payload = new Document()
.append("handle", handle)
.append("state", state);
return natsRepo.requestRaw("crown.set-state", payload)
.map(SafeResult::unwrap)
.then();
}
@Override
public Mono<@NotNull Void> sendPlayer(UUID playerId, String serverHandle) {
var payload = new Document()
.append("playerId", playerId.toString())
.append("handle", serverHandle);
return natsRepo.requestRaw("proxy.send-player", payload)
.map(SafeResult::unwrap)
.then();
}
}

View file

@ -0,0 +1,13 @@
package de.kentoj.scrowlib.utils;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.NONE)
public class EnvUtils {
public static String envOrThrow(String name) {
var env = System.getenv(name);
if (env == null) throw new IllegalStateException("environment variable " + name + " not set");
return env;
}
}