Skip to content

Commit 6a62d8e

Browse files
Swoftyclaude
andcommitted
refactor: replace string delimiter wire format with JSON envelope
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a632499 commit 6a62d8e

10 files changed

Lines changed: 139 additions & 55 deletions

File tree

commons/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ java {
1515
}
1616

1717
dependencies {
18+
implementation(libs.json)
1819
implementation(libs.snakeyaml)
1920
implementation(project(":packer"))
2021
implementation(libs.mongodb.bson)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package net.swofty.commons.redis;
2+
3+
import org.json.JSONObject;
4+
5+
public record RedisEnvelope(String id, String from, String payload) {
6+
public String serialize() {
7+
JSONObject json = new JSONObject();
8+
json.put("id", id);
9+
json.put("from", from);
10+
json.put("payload", payload);
11+
return json.toString();
12+
}
13+
14+
public static RedisEnvelope deserialize(String json) {
15+
JSONObject obj = new JSONObject(json);
16+
return new RedisEnvelope(
17+
obj.getString("id"),
18+
obj.getString("from"),
19+
obj.optString("payload", "")
20+
);
21+
}
22+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Service Communication Phase 2 - Design Spec
2+
3+
## Goal
4+
5+
Complete the typed service communication refactor: migrate Path C (proxy channels), replace fragile string delimiter wire format, annotate remaining polymorphic event hierarchies for Jackson, and add standardized error handling.
6+
7+
## 1. Path C Migration
8+
9+
Replace `ToProxyChannels`/`FromProxyChannels` enums + `ProxyChannelRequirements` + raw JSONObject with typed `ProtocolObject` subclasses using `JacksonSerializer`.
10+
11+
### To-Proxy Channels (11 channels, game server -> proxy)
12+
13+
Each becomes a `ProtocolObject<Request, Response>` in `commons/.../protocol/objects/proxy/to/`. The velocity.extension `@ChannelListener` handler gets typed request/response instead of JSONObject.
14+
15+
### From-Proxy Channels (8 channels, proxy -> game server)
16+
17+
Each becomes a `ProtocolObject<Request, Response>` in `commons/.../protocol/objects/proxy/from/`. The `ProxyToClient` interface is replaced by a typed equivalent.
18+
19+
### Deletion targets
20+
21+
- `ToProxyChannels` enum
22+
- `FromProxyChannels` enum
23+
- `ProxyChannelRequirements` abstract class
24+
- All 20 requirements classes in `commons/.../proxy/requirements/to/` and `from/`
25+
- `ProxyToClient` interface
26+
- All old `ProxyToClient` implementations (8 files)
27+
28+
## 2. Wire Format
29+
30+
Replace all `}=-=-={` and `}=-=---={` string delimiter framing with a JSON envelope.
31+
32+
### Envelope format
33+
34+
```json
35+
{"id":"<uuid>","from":"<sender-filter-id>","payload":"<serialized-content>"}
36+
```
37+
38+
### Files to change (6 core transport files)
39+
40+
- `proxy.api/.../redis/ServerOutboundMessage.java` - sends to proxy + receives from service
41+
- `proxy.api/.../ProxyAPI.java` - receives from proxy + receives from service
42+
- `velocity.extension/.../redis/RedisListener.java` - receives from server, sends response
43+
- `velocity.extension/.../redis/RedisMessage.java` - sends to server, receives response
44+
- `service.generic/.../ServiceInitializer.java` - receives from server, sends response
45+
- `service.generic/.../redis/ServiceToServerManager.java` - sends to server, receives response
46+
- `service.punishment/.../ProxyRedis.java` - sends to proxy
47+
48+
## 3. Remaining Hand-Written Serializers
49+
50+
Add Jackson polymorphism annotations to `PartyEvent` (30 subclasses) and `FriendEvent` (24 subclasses) hierarchies, then swap `SendPartyEventToServiceProtocolObject` and `SendFriendEventToServiceProtocolObject` to `JacksonSerializer`.
51+
52+
### Approach
53+
54+
- Add `@JsonTypeInfo(use = Id.NAME)` + `@JsonSubTypes({...})` to `PartyEvent` and `FriendEvent` base classes
55+
- Add `@JsonCreator` to each subclass constructor
56+
- Replace hand-written serializers with `JacksonSerializer` in the two ProtocolObjects
57+
58+
## 4. Standardized Error Envelope
59+
60+
Not adding a `Result<T>` wrapper -- it would require changing every single protocol response type and every call site. Instead, standardize on a convention: every Response record should have `boolean success` and `@Nullable String error` fields. This is already the pattern used by most protocols. Audit and fix the few that don't follow it.

proxy.api/src/main/java/net/swofty/proxyapi/ProxyAPI.java

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package net.swofty.proxyapi;
22

33
import net.swofty.commons.protocol.ServicePushProtocol;
4+
import net.swofty.commons.redis.RedisEnvelope;
45
import net.swofty.proxyapi.redis.ProxyToClient;
56
import net.swofty.proxyapi.redis.TypedServiceHandler;
67
import net.swofty.redisapi.api.ChannelRegistry;
@@ -22,10 +23,10 @@ public ProxyAPI(String URI, UUID serverUUID) {
2223

2324
public void registerFromProxyHandler(ProxyToClient handler) {
2425
RedisAPI.getInstance().registerChannel(handler.getChannel().getChannelName(), (event) -> {
25-
String[] split = event.message.split("}=-=-=\\{");
26-
UUID request = UUID.fromString(split[0].substring(split[0].indexOf(";") + 1));
27-
String rawMessage = split[1];
28-
JSONObject json = new JSONObject(rawMessage);
26+
String messageWithoutFilter = event.message.substring(event.message.indexOf(";") + 1);
27+
RedisEnvelope envelope = RedisEnvelope.deserialize(messageWithoutFilter);
28+
UUID request = UUID.fromString(envelope.id());
29+
JSONObject json = new JSONObject(envelope.payload());
2930

3031
JSONObject response = handler.onMessage(json);
3132

@@ -37,7 +38,7 @@ public void registerFromProxyHandler(ProxyToClient handler) {
3738
RedisAPI.getInstance().publishMessage(
3839
"proxy",
3940
ChannelRegistry.getFromName(handler.getChannel().getChannelName()),
40-
request + "}=-=-={" + response.toString());
41+
new RedisEnvelope(envelope.id(), serverUUID.toString(), response.toString()).serialize());
4142
});
4243
}
4344

@@ -46,10 +47,11 @@ public <T, R> void registerTypedServiceHandler(TypedServiceHandler<T, R> handler
4647
String channelName = "service_" + protocol.channel();
4748

4849
RedisAPI.getInstance().registerChannel(channelName, (event) -> {
49-
String[] split = event.message.split("}=-=-=\\{");
50-
String serviceId = split[0].substring(split[0].indexOf(";") + 1);
51-
UUID requestId = UUID.fromString(split[1]);
52-
String rawMessage = split[2];
50+
String messageWithoutFilter = event.message.substring(event.message.indexOf(";") + 1);
51+
RedisEnvelope envelope = RedisEnvelope.deserialize(messageWithoutFilter);
52+
String serviceId = envelope.from();
53+
UUID requestId = UUID.fromString(envelope.id());
54+
String rawMessage = envelope.payload();
5355

5456
Thread.startVirtualThread(() -> {
5557
T typedMessage = protocol.translateFromString(rawMessage);
@@ -60,15 +62,16 @@ public <T, R> void registerTypedServiceHandler(TypedServiceHandler<T, R> handler
6062
RedisAPI.getInstance().publishMessage(
6163
serviceId,
6264
ChannelRegistry.getFromName("service_response"),
63-
requestId + "}=-=-={" + serializedResponse);
65+
new RedisEnvelope(envelope.id(), serverUUID.toString(), serializedResponse).serialize());
6466
});
6567
});
6668

6769
RedisAPI.getInstance().registerChannel("service_broadcast_" + protocol.channel(), (event) -> {
68-
String[] split = event.message.split("}=-=-=\\{");
69-
String serviceId = split[0].substring(split[0].indexOf(";") + 1);
70-
UUID requestId = UUID.fromString(split[1]);
71-
String rawMessage = split[2];
70+
String messageWithoutFilter = event.message.substring(event.message.indexOf(";") + 1);
71+
RedisEnvelope envelope = RedisEnvelope.deserialize(messageWithoutFilter);
72+
String serviceId = envelope.from();
73+
UUID requestId = UUID.fromString(envelope.id());
74+
String rawMessage = envelope.payload();
7275

7376
Thread.startVirtualThread(() -> {
7477
T typedMessage = protocol.translateFromString(rawMessage);
@@ -81,7 +84,7 @@ public <T, R> void registerTypedServiceHandler(TypedServiceHandler<T, R> handler
8184
RedisAPI.getInstance().publishMessage(
8285
serviceId,
8386
ChannelRegistry.getFromName("service_broadcast_response"),
84-
requestId + "}=-=-={" + serverUUID.toString() + "}=-=-={" + serializedResponse);
87+
new RedisEnvelope(envelope.id(), serverUUID.toString(), serializedResponse).serialize());
8588
});
8689
});
8790
}

proxy.api/src/main/java/net/swofty/proxyapi/redis/ServerOutboundMessage.java

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import net.swofty.commons.impl.ServiceProxyRequest;
55
import net.swofty.commons.protocol.ProtocolObject;
66
import net.swofty.commons.proxy.ToProxyChannels;
7+
import net.swofty.commons.redis.RedisEnvelope;
78
import net.swofty.redisapi.api.ChannelRegistry;
89
import net.swofty.redisapi.api.RedisAPI;
910
import org.json.JSONObject;
@@ -24,10 +25,10 @@ public static void registerServerToProxy(ToProxyChannels channel) {
2425
RedisAPI.getInstance().registerChannel(channel.getChannelName(), (event) -> {
2526
String messageWithoutFilter = event.message.substring(event.message.indexOf(";") + 1);
2627

27-
String[] split = messageWithoutFilter.split("}=-=-=\\{");
28-
UUID uuid = UUID.fromString(split[0]);
28+
RedisEnvelope envelope = RedisEnvelope.deserialize(messageWithoutFilter);
29+
UUID uuid = UUID.fromString(envelope.id());
2930

30-
redisMessageListeners.get(uuid).accept(split[1]);
31+
redisMessageListeners.get(uuid).accept(envelope.payload());
3132
redisMessageListeners.remove(uuid);
3233
});
3334
}
@@ -43,7 +44,7 @@ public static void sendMessageToProxy(ToProxyChannels channel, JSONObject messag
4344

4445
RedisAPI.getInstance().publishMessage("proxy",
4546
ChannelRegistry.getFromName(channel.getChannelName()),
46-
message.toString() + "}=-=-={" + uuid + "}=-=-={" + filterID);
47+
new RedisEnvelope(uuid.toString(), filterID.toString(), message.toString()).serialize());
4748
}
4849

4950
public static void registerFromProtocolObject(ProtocolObject object) {
@@ -52,13 +53,10 @@ public static void registerFromProtocolObject(ProtocolObject object) {
5253

5354
RedisAPI.getInstance().registerChannel(object.channel(), (event) -> {
5455
String messageWithoutFilter = event.message.substring(event.message.indexOf(";") + 1);
55-
String[] split = messageWithoutFilter.split("}=-=---=\\{");
5656

57-
UUID uuid = UUID.fromString(split[0]);
58-
String message;
59-
if (split.length != 1) {
60-
message = split[1];
61-
} else message = "";
57+
RedisEnvelope envelope = RedisEnvelope.deserialize(messageWithoutFilter);
58+
UUID uuid = UUID.fromString(envelope.id());
59+
String message = envelope.payload();
6260

6361
try {
6462
redisMessageListeners.get(uuid).accept(message);

service.generic/src/main/java/net/swofty/service/generic/ServiceInitializer.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import lombok.RequiredArgsConstructor;
44
import net.swofty.commons.config.ConfigProvider;
55
import net.swofty.commons.impl.ServiceProxyRequest;
6+
import net.swofty.commons.redis.RedisEnvelope;
67
import net.swofty.commons.skyblock.item.attribute.ItemAttribute;
78
import net.swofty.commons.protocol.ProtocolObject;
89
import net.swofty.redisapi.api.ChannelRegistry;
@@ -52,7 +53,7 @@ public void init() {
5253

5354
RedisAPI.getInstance().publishMessage(request.getRequestServer(),
5455
ChannelRegistry.getFromName(request.getEndpoint()),
55-
request.getRequestId() + "}=-=---={" + response).join();
56+
new RedisEnvelope(request.getRequestId().toString(), service.getType().name(), response).serialize()).join();
5657
});
5758
});
5859
});

service.generic/src/main/java/net/swofty/service/generic/redis/ServiceToServerManager.java

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import net.swofty.commons.ServiceType;
44
import net.swofty.commons.protocol.ServicePushProtocol;
5+
import net.swofty.commons.redis.RedisEnvelope;
56
import net.swofty.commons.protocol.objects.data.GetPlayerDataPushProtocol;
67
import net.swofty.commons.protocol.objects.data.LockPlayerDataPushProtocol;
78
import net.swofty.commons.protocol.objects.data.UnlockPlayerDataPushProtocol;
@@ -36,25 +37,25 @@ public static void initialize(ServiceType serviceType) {
3637

3738
// Register response handler for server responses
3839
RedisAPI.getInstance().registerChannel("service_response", (event) -> {
39-
String[] split = event.message.split("}=-=-=\\{");
40-
UUID requestId = UUID.fromString(split[0].substring(split[0].indexOf(";") + 1));
41-
String response = split[1];
40+
String messageWithoutFilter = event.message.substring(event.message.indexOf(";") + 1);
41+
RedisEnvelope envelope = RedisEnvelope.deserialize(messageWithoutFilter);
42+
UUID requestId = UUID.fromString(envelope.id());
4243

4344
CompletableFuture<JSONObject> future = pendingRequests.remove(requestId);
4445
if (future != null) {
45-
future.complete(new JSONObject(response));
46+
future.complete(new JSONObject(envelope.payload()));
4647
}
4748
});
4849

4950
RedisAPI.getInstance().registerChannel("service_broadcast_response", (event) -> {
50-
String[] split = event.message.split("}=-=-=\\{");
51-
UUID requestId = UUID.fromString(split[0].substring(split[0].indexOf(";") + 1));
52-
UUID serverUUID = UUID.fromString(split[1]);
53-
String response = split[2];
51+
String messageWithoutFilter = event.message.substring(event.message.indexOf(";") + 1);
52+
RedisEnvelope envelope = RedisEnvelope.deserialize(messageWithoutFilter);
53+
UUID requestId = UUID.fromString(envelope.id());
54+
UUID serverUUID = UUID.fromString(envelope.from());
5455

5556
BroadcastRequest broadcastRequest = pendingBroadcastRequests.get(requestId);
5657
if (broadcastRequest != null) {
57-
broadcastRequest.addResponse(serverUUID, new JSONObject(response));
58+
broadcastRequest.addResponse(serverUUID, new JSONObject(envelope.payload()));
5859
}
5960
});
6061
}
@@ -90,12 +91,11 @@ public static <T, R> CompletableFuture<R> sendToServer(
9091

9192
String serialized = protocol.translateToString(message);
9293
String channelName = "service_" + protocol.channel();
93-
String messageContent = currentServiceType.name() + "}=-=-={" + requestId + "}=-=-={" + serialized;
9494

9595
RedisAPI.getInstance().publishMessage(
9696
serverUUID.toString(),
9797
ChannelRegistry.getFromName(channelName),
98-
messageContent
98+
new RedisEnvelope(requestId.toString(), currentServiceType.name(), serialized).serialize()
9999
);
100100

101101
return future;
@@ -115,13 +115,10 @@ public static <T, R> CompletableFuture<Map<UUID, R>> sendToAllServers(
115115

116116
String serialized = protocol.translateToString(message);
117117
String channelName = "service_broadcast_" + protocol.channel();
118-
String messageContent = currentServiceType.name()
119-
+ "}=-=-={" + requestId
120-
+ "}=-=-={" + serialized;
121118

122119
RedisAPI.getInstance().publishMessage("all",
123120
ChannelRegistry.getFromName(channelName),
124-
messageContent);
121+
new RedisEnvelope(requestId.toString(), currentServiceType.name(), serialized).serialize());
125122

126123
scheduler.schedule(() -> {
127124
BroadcastRequest req = pendingBroadcastRequests.remove(requestId);

service.punishment/src/main/java/net/swofty/service/punishment/ProxyRedis.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import redis.clients.jedis.JedisPoolConfig;
66

77
import net.swofty.commons.proxy.ToProxyChannels;
8+
import net.swofty.commons.redis.RedisEnvelope;
89
import org.json.JSONObject;
910

1011
import java.net.URI;
@@ -66,7 +67,7 @@ public static CompletableFuture<Void> publishMessage(String filterId, String cha
6667
public static void publishToProxy(ToProxyChannels channel, JSONObject message) {
6768
UUID uuid = UUID.randomUUID();
6869
publishMessage("proxy", channel.getChannelName(),
69-
message.toString() + "}=-=-={" + uuid + "}=-=-={" + uuid);
70+
new RedisEnvelope(uuid.toString(), uuid.toString(), message.toString()).serialize());
7071
}
7172

7273
public static boolean isInitialized() {

velocity.extension/src/main/java/net/swofty/velocity/redis/RedisListener.java

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package net.swofty.velocity.redis;
22

3+
import net.swofty.commons.redis.RedisEnvelope;
34
import net.swofty.redisapi.api.ChannelRegistry;
45
import net.swofty.redisapi.api.RedisAPI;
56
import org.json.JSONObject;
@@ -17,28 +18,26 @@ public RedisListener() {
1718
}
1819

1920
public void onMessage(String channel, String message) {
20-
String[] split = message.split("}=-=-=\\{");
21-
String rawMessage = split[0];
22-
UUID uuid = UUID.fromString(split[1]);
23-
UUID filterID = UUID.fromString(split[2]);
24-
25-
String messageWithoutFilter = rawMessage.substring(rawMessage.indexOf(";") + 1);
26-
JSONObject json = new JSONObject(messageWithoutFilter);
21+
String messageWithoutFilter = message.substring(message.indexOf(";") + 1);
22+
RedisEnvelope envelope = RedisEnvelope.deserialize(messageWithoutFilter);
23+
UUID uuid = UUID.fromString(envelope.id());
24+
UUID filterID = UUID.fromString(envelope.from());
25+
JSONObject json = new JSONObject(envelope.payload());
2726

2827
Thread.startVirtualThread(() -> {
2928
JSONObject response;
3029
try {
3130
response = receivedMessage(json, filterID);
3231
} catch (Exception e) {
33-
System.out.println("Error on channel " + channel + " with message " + messageWithoutFilter);
32+
System.out.println("Error on channel " + channel + " with message " + envelope.payload());
3433
Logger.error(e, "Error in Redis listener");
3534
return;
3635
}
3736

3837
RedisAPI.getInstance().publishMessage(
3938
filterID.toString(),
4039
ChannelRegistry.getFromName(channel),
41-
uuid + "}=-=-={" + response.toString());
40+
new RedisEnvelope(envelope.id(), "proxy", response.toString()).serialize());
4241
});
4342
}
4443

velocity.extension/src/main/java/net/swofty/velocity/redis/RedisMessage.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package net.swofty.velocity.redis;
22

33
import net.swofty.commons.proxy.FromProxyChannels;
4+
import net.swofty.commons.redis.RedisEnvelope;
45
import net.swofty.redisapi.api.ChannelRegistry;
56
import net.swofty.redisapi.api.RedisAPI;
67
import org.json.JSONObject;
@@ -24,16 +25,17 @@ public static CompletableFuture<JSONObject> sendMessageToServer(UUID server,
2425
RedisAPI.getInstance().publishMessage(
2526
server.toString(),
2627
ChannelRegistry.getFromName(channel.getChannelName()),
27-
requestID + "}=-=-={" + message.toString());
28+
new RedisEnvelope(requestID.toString(), "proxy", message.toString()).serialize());
2829

2930
return future;
3031
}
3132

3233
public static void registerProxyToServer(FromProxyChannels channel) {
3334
RedisAPI.getInstance().registerChannel(channel.getChannelName(), (event) -> {
34-
String[] split = event.message.split("}=-=-=\\{");
35-
UUID request = UUID.fromString(split[0].substring(split[0].indexOf(";") + 1));
36-
String rawMessage = split[1];
35+
String messageWithoutFilter = event.message.substring(event.message.indexOf(";") + 1);
36+
RedisEnvelope envelope = RedisEnvelope.deserialize(messageWithoutFilter);
37+
UUID request = UUID.fromString(envelope.id());
38+
String rawMessage = envelope.payload();
3739

3840
try {
3941
callbacks.get(request).complete(new JSONObject(rawMessage));

0 commit comments

Comments
 (0)