7 Commits
Author SHA1 Message Date
selimaj-dev b80ce6ea01 Notification handler 2026-02-23 06:28:12 +01:00
selimaj-dev b236de082a Fixed ping bug 2026-02-23 05:54:56 +01:00
selimaj-dev 1ba5cd8159 HOWWWW 2026-02-20 02:22:58 +01:00
selimaj-dev 34d86a23ab Fixed build artifacts 2026-02-20 02:06:05 +01:00
selimaj-dev 6b0992e208 v0.1.2
Gradle Build / build (push) Canceled after 47s
2026-02-20 01:42:30 +01:00
selimaj-dev 2b64b10646 Fixed types 2026-02-20 01:26:10 +01:00
selimaj-dev 5cb57127de Publishing with jitpack
Gradle Build / build (push) Canceled after 2m8s
2026-02-19 22:15:17 +01:00
10 changed files with 167 additions and 123 deletions
+34
View File
@@ -0,0 +1,34 @@
name: Gradle Build
on:
push:
tags:
- 'v*' # Run when a version tag like v0.1.0 is pushed
pull_request: # Optional: test PRs too
jobs:
build:
runs-on: ubuntu-latest
steps:
# 1. Checkout code
- uses: actions/checkout@v4
# 2. Set up JDK 17
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
# 3. Set up Gradle
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
# 4. Build project
- name: Build with Gradle
run: ./gradlew build
# 5. Run tests
- name: Run tests
run: ./gradlew test
-44
View File
@@ -1,44 +0,0 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will build a package using Gradle and then publish it to GitHub packages when a release is created
# For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#Publishing-using-gradle
name: Gradle Package
on:
release:
types: [created]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
server-id: github # Value of the distributionManagement/repository/id field of the pom.xml
settings-path: ${{ github.workspace }} # location for the settings.xml file
- name: Setup Gradle
uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0
- name: Build with Gradle
run: ./gradlew build
# The USERNAME and TOKEN need to correspond to the credentials environment variables used in
# the publishing section of your build.gradle
- name: Publish to GitHub Packages
run: ./gradlew publish
env:
USERNAME: ${{ github.actor }}
TOKEN: ${{ secrets.GITHUB_TOKEN }}
+2 -10
View File
@@ -26,22 +26,14 @@ It ensures compile-time guarantees for message structure, reduces runtime errors
```groovy ```groovy
repositories { repositories {
mavenCentral() maven { url 'https://jitpack.io' }
maven {
url = uri("https://maven.pkg.github.com/selimaj-dev/session-java")
credentials {
username = project.findProperty("gpr.user") ?: System.getenv("GITHUB_USERNAME")
password = project.findProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN")
}
}
} }
``` ```
#### Add the dependency #### Add the dependency
```groovy ```groovy
dependencies { dependencies {
implementation "dev.selimaj.session:session-java:0.1.0" implementation 'com.github.selimaj-dev:session-java:0.1.3'
} }
``` ```
+5 -18
View File
@@ -4,7 +4,7 @@ plugins {
} }
group = "dev.selimaj.session" group = "dev.selimaj.session"
version = "0.1.0" version = "0.1.3"
java { java {
toolchain { toolchain {
@@ -36,22 +36,9 @@ test {
} }
publishing { publishing {
publications { publications {
gpr(MavenPublication) { mavenJava(MavenPublication) {
from(components.java) from components.java
groupId = project.group
artifactId = "session-java"
version = project.version
} }
} }
repositories { }
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/selimaj-dev/session-java")
credentials {
username = project.findProperty("gpr.user") ?: System.getenv("USERNAME")
password = project.findProperty("gpr.key") ?: System.getenv("TOKEN")
}
}
}
}
+36 -5
View File
@@ -7,11 +7,13 @@ import dev.selimaj.session.types.SessionResult;
import dev.selimaj.session.types.Message; import dev.selimaj.session.types.Message;
import dev.selimaj.session.types.Method; import dev.selimaj.session.types.Method;
import dev.selimaj.session.types.MethodHandler; import dev.selimaj.session.types.MethodHandler;
import dev.selimaj.session.types.NotificationHandler;
import java.net.URI; import java.net.URI;
import java.net.http.HttpClient; import java.net.http.HttpClient;
import java.net.http.WebSocket; import java.net.http.WebSocket;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
public final class Session { public final class Session {
@@ -34,6 +36,18 @@ public final class Session {
return new Session(ws, listener); return new Session(ws, listener);
} }
public static Session connect(String uri, long timeout, TimeUnit unit) throws Exception {
SessionListener listener = new SessionListener();
WebSocket ws = HttpClient.newHttpClient()
.newWebSocketBuilder()
.buildAsync(URI.create(uri), listener)
.orTimeout(timeout, unit)
.join();
return new Session(ws, listener);
}
public void send(Message msg) throws Exception { public void send(Message msg) throws Exception {
listener.send(ws, msg); listener.send(ws, msg);
} }
@@ -50,16 +64,17 @@ public final class Session {
listener.notify(ws, method, data); listener.notify(ws, method, data);
} }
<Req extends JsonNode, Res extends JsonNode, Err extends JsonNode> CompletableFuture<Res> request( public <Req, Res, Err> CompletableFuture<Res> request(
Method<Req, Res, Err> method, Req req) throws Exception { Method<Req, Res, Err> method, Req req) throws Exception {
return listener.request(ws, method.getName(), req, method.getResClass()); return listener.request(ws, method.getName(), req, method.getResClass());
} }
<Req extends JsonNode, Res extends JsonNode, Err extends JsonNode> void onRequest(Method<Req, Res, Err> method, public <Req, Res, Err> void onRequest(Method<Req, Res, Err> method,
MethodHandler<Req> handler) { MethodHandler<Req, Res, Err> handler) {
MethodHandler<JsonNode> wrapper = (id, value) -> { MethodHandler<JsonNode, JsonNode, JsonNode> wrapper = (id, value) -> {
try { try {
return handler.handle(id, listener.mapper.treeToValue(value, method.getReqClass())); return handler.handle(id, listener.mapper.treeToValue(value, method.getReqClass()))
.intoJSON(listener.mapper);
} catch (Exception e) { } catch (Exception e) {
return SessionResult.error(TextNode.valueOf(e.getMessage())); return SessionResult.error(TextNode.valueOf(e.getMessage()));
} }
@@ -68,6 +83,22 @@ public final class Session {
this.listener.methods.put(method.getName(), wrapper); this.listener.methods.put(method.getName(), wrapper);
} }
public <Req, Res, Err> void onNotification(Method<Req, Res, Err> method,
NotificationHandler<Req, Res, Err> handler) {
NotificationHandler<JsonNode, JsonNode, JsonNode> wrapper = (id, value) -> {
return CompletableFuture.supplyAsync(() -> {
try {
return handler.handle(id, listener.mapper.treeToValue(value, method.getReqClass())).get()
.intoJSON(listener.mapper);
} catch (Exception e) {
return SessionResult.error(TextNode.valueOf(e.getMessage()));
}
});
};
this.listener.notificationHandlers.put(method.getName(), wrapper);
}
public void close() throws Exception { public void close() throws Exception {
ws.sendClose(0, "Session closed"); ws.sendClose(0, "Session closed");
} }
@@ -1,7 +1,9 @@
package dev.selimaj.session; package dev.selimaj.session;
import java.net.http.WebSocket; import java.net.http.WebSocket;
import java.nio.ByteBuffer;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage; import java.util.concurrent.CompletionStage;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@@ -11,9 +13,11 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import dev.selimaj.session.types.Message; import dev.selimaj.session.types.Message;
import dev.selimaj.session.types.MethodHandler; import dev.selimaj.session.types.MethodHandler;
import dev.selimaj.session.types.NotificationHandler;
public class SessionListener implements WebSocket.Listener { public class SessionListener implements WebSocket.Listener {
public final ConcurrentHashMap<String, MethodHandler<JsonNode>> methods = new ConcurrentHashMap<>(); final ConcurrentHashMap<String, MethodHandler<JsonNode, JsonNode, JsonNode>> methods = new ConcurrentHashMap<>();
final ConcurrentHashMap<String, NotificationHandler<JsonNode, JsonNode, JsonNode>> notificationHandlers = new ConcurrentHashMap<>();
final ObjectMapper mapper = new ObjectMapper(); final ObjectMapper mapper = new ObjectMapper();
private final ConcurrentHashMap<Integer, CompletableFuture<JsonNode>> pending = new ConcurrentHashMap<>(); private final ConcurrentHashMap<Integer, CompletableFuture<JsonNode>> pending = new ConcurrentHashMap<>();
@@ -31,20 +35,19 @@ public class SessionListener implements WebSocket.Listener {
} }
if (msg instanceof Message.Request r) { if (msg instanceof Message.Request r) {
MethodHandler<JsonNode> handler = methods.get(r.method()); MethodHandler<JsonNode, JsonNode, JsonNode> handler = methods.get(r.method());
if (handler != null) { if (handler != null) {
handler.handle(r.id(), r.data()) try {
.thenAccept(res -> { var res = handler.handle(r.id(), r.data());
try {
if (res.isError()) { if (res.isError()) {
respondError(ws, r.id(), res.value()); respondError(ws, r.id(), res.getJsonNode(mapper));
} else { } else {
respond(ws, r.id(), res.value()); respond(ws, r.id(), res.getJsonNode(mapper));
} }
} catch (Exception ignored) { } catch (Exception ignored) {
} }
});
} }
} else if (msg instanceof Message.Response r) { } else if (msg instanceof Message.Response r) {
CompletableFuture<JsonNode> fut = pending.remove(r.id()); CompletableFuture<JsonNode> fut = pending.remove(r.id());
@@ -57,6 +60,30 @@ public class SessionListener implements WebSocket.Listener {
new RuntimeException(r.error().toString())); new RuntimeException(r.error().toString()));
} }
ws.request(1);
return null;
}
@Override
public void onOpen(WebSocket ws) {
ws.request(1);
}
@Override
public CompletionStage<?> onPing(WebSocket ws, ByteBuffer message) {
ws.request(1);
return ws.sendPong(message);
}
@Override
public CompletionStage<?> onPong(WebSocket ws, ByteBuffer message) {
ws.request(1);
return null;
}
@Override
public CompletionStage<?> onClose(WebSocket ws, int statusCode, String reason) {
ws.request(1);
return null; return null;
} }
@@ -76,28 +103,36 @@ public class SessionListener implements WebSocket.Listener {
send(ws, new Message.Notification(method, mapper.valueToTree(data))); send(ws, new Message.Notification(method, mapper.valueToTree(data)));
} }
<Req, Res> CompletableFuture<Res> request( public <Req, Res> CompletableFuture<Res> request(
WebSocket ws, WebSocket ws,
String method, String method,
Req data, Req data,
Class<Res> resType) throws Exception { Class<Res> resType) {
int id = this.id.incrementAndGet(); int id = this.id.incrementAndGet();
CompletableFuture<JsonNode> fut = new CompletableFuture<>(); return CompletableFuture
pending.put(id, fut); .runAsync(() -> {
try {
send(ws, new Message.Request( send(ws, new Message.Request(
id, id,
method, method,
mapper.valueToTree(data))); mapper.valueToTree(data)));
} catch (Exception e) {
return fut.thenApply(json -> { throw new CompletionException(e);
try { }
return mapper.treeToValue(json, resType); })
} catch (Exception e) { .thenCompose(v -> {
throw new RuntimeException(e); CompletableFuture<JsonNode> fut = new CompletableFuture<>();
} pending.put(id, fut);
}); return fut;
})
.thenApply(json -> {
try {
return mapper.treeToValue(json, resType);
} catch (Exception e) {
throw new CompletionException(e);
}
});
} }
} }
@@ -1,8 +1,6 @@
package dev.selimaj.session.types; package dev.selimaj.session.types;
import com.fasterxml.jackson.databind.JsonNode; public class Method<Req, Res, Err> {
public class Method<Req extends JsonNode, Res extends JsonNode, Err extends JsonNode> {
private final String name; private final String name;
private final Class<Req> reqClass; private final Class<Req> reqClass;
private final Class<Res> resClass; private final Class<Res> resClass;
@@ -1,10 +1,6 @@
package dev.selimaj.session.types; package dev.selimaj.session.types;
import java.util.concurrent.CompletableFuture;
import com.fasterxml.jackson.databind.JsonNode;
@FunctionalInterface @FunctionalInterface
public interface MethodHandler<Req extends JsonNode> { public interface MethodHandler<Req, Res, Err> {
CompletableFuture<SessionResult> handle(int id, Req data); SessionResult<Res, Err> handle(int id, Req data);
} }
@@ -0,0 +1,8 @@
package dev.selimaj.session.types;
import java.util.concurrent.CompletableFuture;
@FunctionalInterface
public interface NotificationHandler<Req, Res, Err> {
CompletableFuture<SessionResult<Res, Err>> handle(int id, Req data);
}
@@ -1,15 +1,22 @@
package dev.selimaj.session.types; package dev.selimaj.session.types;
import java.util.concurrent.CompletableFuture;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
public record SessionResult(boolean isError, JsonNode value) { public record SessionResult<T, E>(boolean isError, Object value) {
public static CompletableFuture<SessionResult> ok(JsonNode value) { public static <T, E> SessionResult<T, E> ok(T value) {
return CompletableFuture.completedFuture(new SessionResult(false, value)); return new SessionResult<>(false, value);
} }
public static CompletableFuture<SessionResult> error(JsonNode value) { public static <T, E> SessionResult<T, E> error(E err) {
return CompletableFuture.completedFuture(new SessionResult(false, value)); return new SessionResult<>(false, err);
}
public SessionResult<JsonNode, JsonNode> intoJSON(ObjectMapper mapper) {
return new SessionResult<>(this.isError, mapper.valueToTree(this.value));
}
public JsonNode getJsonNode(ObjectMapper mapper) {
return mapper.valueToTree(this.value);
} }
} }