diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index 825103e..9ed59b5 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -16,10 +16,10 @@ jobs:
steps:
- uses: actions/checkout@v3
- - name: Set up JDK 11
+ - name: Set up JDK 21
uses: actions/setup-java@v3
with:
- java-version: '11'
+ java-version: '21'
distribution: 'temurin'
cache: maven
- name: Build with Maven
diff --git a/pom.xml b/pom.xml
index 3fa1dff..9f902c4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -9,8 +9,8 @@
1.1.3
- 1.8
- 1.8
+ 21
+ 21
diff --git a/src/main/java/net/swofty/redisapi/api/RedisAPI.java b/src/main/java/net/swofty/redisapi/api/RedisAPI.java
index 8e797eb..9082c3f 100644
--- a/src/main/java/net/swofty/redisapi/api/RedisAPI.java
+++ b/src/main/java/net/swofty/redisapi/api/RedisAPI.java
@@ -3,6 +3,7 @@
import lombok.AccessLevel;
import lombok.NonNull;
import lombok.experimental.FieldDefaults;
+import net.swofty.redisapi.api.requests.DataStreamListener;
import net.swofty.redisapi.events.EventRegistry;
import net.swofty.redisapi.events.RedisMessagingReceiveEvent;
import net.swofty.redisapi.events.RedisMessagingReceiveInterface;
@@ -141,6 +142,13 @@ public static RedisAPI generateInstance(@NonNull String uri, String password) {
* Starts listeners for the Redis Pub/Sub channels
*/
public void startListeners() {
+ try {
+ registerChannel("internal-data-request", DataStreamListener.class);
+ } catch (ChannelAlreadyRegisteredException ignored) {
+ System.out.println("[WARNING]: The internal data request channel has already been registered. This will cause issues if you are using the DataRequest API along with the Redis API." +
+ "\n Channel Name: internal-data-request");
+ }
+
new Thread(() -> {
try (Jedis jedis = getPool().getResource()) {
EventRegistry.pubSub = new JedisPubSub() {
diff --git a/src/main/java/net/swofty/redisapi/api/requests/DataRequest.java b/src/main/java/net/swofty/redisapi/api/requests/DataRequest.java
new file mode 100644
index 0000000..0225260
--- /dev/null
+++ b/src/main/java/net/swofty/redisapi/api/requests/DataRequest.java
@@ -0,0 +1,62 @@
+package net.swofty.redisapi.api.requests;
+
+import net.swofty.redisapi.api.ChannelRegistry;
+import net.swofty.redisapi.api.RedisAPI;
+import net.swofty.redisapi.util.RedisParsableMessage;
+import org.json.JSONObject;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+
+public class DataRequest {
+ public static final Map RECEIVED_DATA = new HashMap<>();
+
+ private final String id;
+ private final String filter;
+ private final String key;
+ private final JSONObject data;
+
+ /**
+ * Create a new data request to get a specific object of data from a specific filter ID.
+ * @param filterID The filter ID where you want to receive data from, can be "all" for all listeners.
+ * @param key The data identifier key.
+ */
+ public DataRequest(String filterID, String key, JSONObject data) {
+ this.id = UUID.randomUUID().toString();
+ this.filter = filterID;
+ this.key = key;
+ this.data = data;
+ }
+
+ public CompletableFuture await() {
+ return CompletableFuture.supplyAsync(() -> {
+ long start = System.currentTimeMillis();
+ JSONObject request = new JSONObject();
+ request.put("id", id);
+ request.put("key", key);
+ request.put("data", data);
+ request.put("sender", "proxy");
+ request.put("stream", StreamType.REQUEST.name());
+
+ RedisAPI.getInstance().publishMessage(filter, ChannelRegistry.getFromName("internal-data-request"), RedisParsableMessage.from(request).formatForSend());
+
+ int timeout = 0;
+ while (!RECEIVED_DATA.containsKey(id)) {
+ try { Thread.sleep(1); timeout++; } catch (InterruptedException ignored) { }
+ if (timeout >= 100) break;
+ }
+
+ JSONObject response = RECEIVED_DATA.get(id);
+ RECEIVED_DATA.remove(id);
+ long latency = (System.currentTimeMillis() - start);
+ return new DataResponse(response, latency);
+ });
+ }
+
+ public enum StreamType {
+ REQUEST,
+ RESPONSE
+ }
+}
diff --git a/src/main/java/net/swofty/redisapi/api/requests/DataRequestResponder.java b/src/main/java/net/swofty/redisapi/api/requests/DataRequestResponder.java
new file mode 100644
index 0000000..936059b
--- /dev/null
+++ b/src/main/java/net/swofty/redisapi/api/requests/DataRequestResponder.java
@@ -0,0 +1,42 @@
+package net.swofty.redisapi.api.requests;
+
+import org.json.JSONObject;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+public class DataRequestResponder {
+ public static final Map RESPONDERS = new HashMap<>();
+
+ private final Function callback;
+
+ protected DataRequestResponder(Function callback) {
+ this.callback = callback;
+ }
+
+ public JSONObject respond(JSONObject request) {
+ return this.callback.apply(request);
+ }
+
+ /**
+ * Creates a new Data Request Responder. Must be registered before {@link net.swofty.redisapi.api.RedisAPI#startListeners()} in order to work properly.
+ * @param key The key to respond to.
+ * @param callback Callback, has a JSONObject parameter request, and returns a JSONObject response, request and response both can be empty.
+ * @return The created DataRequestResponder, not entirely useful, but still there.
+ */
+ public static DataRequestResponder create(String key, Function callback) {
+ DataRequestResponder responder = new DataRequestResponder(callback);
+ RESPONDERS.put(key, responder);
+ return responder;
+ }
+
+ /**
+ * Get a DataRequestResponder by key.
+ * @param key The key to get the DataRequestResponder by.
+ * @return The DataRequestResponder, or null if it doesn't exist.
+ */
+ public static DataRequestResponder get(String key) {
+ return RESPONDERS.get(key);
+ }
+}
diff --git a/src/main/java/net/swofty/redisapi/api/requests/DataResponse.java b/src/main/java/net/swofty/redisapi/api/requests/DataResponse.java
new file mode 100644
index 0000000..dc4c88b
--- /dev/null
+++ b/src/main/java/net/swofty/redisapi/api/requests/DataResponse.java
@@ -0,0 +1,11 @@
+package net.swofty.redisapi.api.requests;
+
+import org.json.JSONObject;
+
+/**
+ * The response to a DataRequest.
+ * @param data The data object, will be null if the request has timed out.
+ * @param latency The latency of the request, normal range is between 1-10ms, unless the server is under heavy load or the Redis server is running on a different machine.
+ */
+public record DataResponse(JSONObject data, long latency) {
+}
diff --git a/src/main/java/net/swofty/redisapi/api/requests/DataStreamListener.java b/src/main/java/net/swofty/redisapi/api/requests/DataStreamListener.java
new file mode 100644
index 0000000..dd82a4c
--- /dev/null
+++ b/src/main/java/net/swofty/redisapi/api/requests/DataStreamListener.java
@@ -0,0 +1,38 @@
+package net.swofty.redisapi.api.requests;
+
+import net.swofty.redisapi.api.ChannelRegistry;
+import net.swofty.redisapi.api.RedisAPI;
+import net.swofty.redisapi.events.RedisMessagingReceiveInterface;
+import net.swofty.redisapi.util.RedisParsableMessage;
+import org.json.JSONObject;
+
+public class DataStreamListener implements RedisMessagingReceiveInterface {
+ @Override
+ public void onMessage(String channel, String message) {
+ RedisParsableMessage msg = RedisParsableMessage.parse(message);
+ DataRequest.StreamType type = DataRequest.StreamType.valueOf(msg.get("stream", "NONE"));
+ String key = msg.get("key", "NONE");
+ String id = msg.get("id", "NONE");
+ String sender = msg.get("sender", "NONE");
+ JSONObject data = msg.getJson().getJSONObject("data");
+
+ switch (type) {
+ case REQUEST -> {
+ DataRequestResponder responder = DataRequestResponder.get(key);
+ if (responder == null) return;
+
+ JSONObject response = responder.respond(data);
+ JSONObject responseJson = new JSONObject();
+ responseJson.put("id", id);
+ responseJson.put("sender", "internal");
+ responseJson.put("stream", DataRequest.StreamType.RESPONSE.name());
+ responseJson.put("key", key);
+ responseJson.put("data", response);
+
+ RedisAPI.getInstance().publishMessage(sender, ChannelRegistry.getFromName("internal-data-request"),
+ RedisParsableMessage.from(responseJson).formatForSend());
+ }
+ case RESPONSE -> DataRequest.RECEIVED_DATA.put(id, data);
+ }
+ }
+}
diff --git a/src/main/java/net/swofty/redisapi/util/RedisParsableMessage.java b/src/main/java/net/swofty/redisapi/util/RedisParsableMessage.java
new file mode 100644
index 0000000..c698f29
--- /dev/null
+++ b/src/main/java/net/swofty/redisapi/util/RedisParsableMessage.java
@@ -0,0 +1,104 @@
+package net.swofty.redisapi.util;
+
+import lombok.Getter;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * This is a utility class, used for sending JSONObjects over Redis instead of working with raw Strings.
+ */
+@Getter
+public class RedisParsableMessage {
+ private final JSONObject json;
+
+ protected RedisParsableMessage(JSONObject json) {
+ this.json = json;
+ }
+
+ /**
+ * Builds a new RedisParsableMessage from a JSONObject.
+ * @param fields The fields to build the JSONObject from.
+ * @return The built RedisParsableMessage.
+ */
+ public static RedisParsableMessage from( Map fields) {
+ return from(new JSONObject(fields));
+ }
+
+ /**
+ * Builds a new RedisParsableMessage from a JSONObject.
+ * @param obj The JSONObject to build the RedisParsableMessage from.
+ * @return The built RedisParsableMessage.
+ */
+ public static RedisParsableMessage from(JSONObject obj) {
+ return new RedisParsableMessage(obj);
+ }
+
+ /**
+ * Parse a RedisParsableMessage from a raw String.
+ * @param raw The raw String to parse.
+ * @return The parsed RedisParsableMessage.
+ * @throws IllegalArgumentException if the raw String is not a valid JSONObject.
+ */
+ public static RedisParsableMessage parse(String raw) {
+ String toParse = raw;
+ if (raw.contains(";")) {
+ String[] split = raw.split(";");
+ toParse = split[1];
+ }
+ return new RedisParsableMessage(new JSONObject(toParse));
+ }
+
+ /**
+ * Formats the JSONObject into a String to send over Redis, this is the same as {@link #json#toString()}.
+ * @return The formatted String.
+ */
+ public String formatForSend() {
+ return json.toString();
+ }
+
+ @Override
+ public String toString() {
+ return formatForSend();
+ }
+
+ /**
+ * Get an object from the JSONObject.
+ * @param key The key to get the object from.
+ * @param defaultValue The default value to return if the key is not found.
+ * @return The object.
+ * @param The type of the object.
+ */
+ public T get(String key, T defaultValue) {
+ return json.has(key) ? (T) json.get(key) : defaultValue;
+ }
+
+ /*
+ * Beyond here are some utility methods for getting data from the JSONObject.
+ */
+
+ /**
+ * Get a UUID from the JSONObject.
+ * @param key The key to get the UUID from.
+ * @return The UUID.
+ */
+ public UUID getUUID(String key) {
+ return UUID.fromString(get(key, ""));
+ }
+
+ public JSONArray getJsonArray(String key) {
+ return json.has(key) ? json.getJSONArray(key) : new JSONArray();
+ }
+
+ public List getStringList(String key) {
+ return json.has(key) ? json.getJSONArray(key).toList().stream().map(String::valueOf).toList() : new ArrayList<>();
+ }
+
+ public boolean getBoolean(String key) {
+ return json.has(key) && json.getBoolean(key);
+ }
+}