Skip to content

Commit 8e5acc4

Browse files
AndyTWFclaude
andcommitted
feat: add extras field to PresenceMessage (TP3i)
Add extras field to PresenceMessage type with support for both msgpack and JSON serialization/deserialization. Includes end-to-end test verifying extras round-trip through presence enter. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3298bcc commit 8e5acc4

2 files changed

Lines changed: 129 additions & 1 deletion

File tree

lib/src/main/java/io/ably/lib/types/PresenceMessage.java

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,16 @@ public enum Action {
7575
*/
7676
public Action action;
7777

78+
/**
79+
* A MessageExtras object of arbitrary key-value pairs that may contain metadata, and/or ancillary payloads.
80+
* Valid payloads include {@link DeltaExtras}, {@link JsonObject}.
81+
* <p>
82+
* Spec: TP3i
83+
*/
84+
public MessageExtras extras;
85+
86+
private static final String EXTRAS = "extras";
87+
7888
/**
7989
* Default constructor
8090
*/
@@ -123,16 +133,22 @@ public Object clone() {
123133
result.encoding = encoding;
124134
result.data = data;
125135
result.action = action;
136+
result.extras = extras;
126137
return result;
127138
}
128139

129140
void writeMsgpack(MessagePacker packer) throws IOException {
130141
int fieldCount = super.countFields();
131142
++fieldCount;
143+
if(extras != null) ++fieldCount;
132144
packer.packMapHeader(fieldCount);
133145
super.writeFields(packer);
134146
packer.packString("action");
135147
packer.packInt(action.getValue());
148+
if(extras != null) {
149+
packer.packString(EXTRAS);
150+
extras.write(packer);
151+
}
136152
}
137153

138154
PresenceMessage readMsgpack(MessageUnpacker unpacker) throws IOException {
@@ -145,6 +161,8 @@ PresenceMessage readMsgpack(MessageUnpacker unpacker) throws IOException {
145161
if(super.readField(unpacker, fieldName, fieldFormat)) { continue; }
146162
if(fieldName.equals("action")) {
147163
action = Action.findByValue(unpacker.unpackInt());
164+
} else if (fieldName.equals(EXTRAS)) {
165+
extras = MessageExtras.read(unpacker);
148166
} else {
149167
Log.v(TAG, "Unexpected field: " + fieldName);
150168
unpacker.skipValue();
@@ -260,6 +278,24 @@ public static PresenceMessage[] fromEncodedArray(String presenceMsgArray, Channe
260278
}
261279
}
262280

281+
@Override
282+
protected void read(final JsonObject map) throws MessageDecodeException {
283+
super.read(map);
284+
285+
final JsonElement extrasElement = map.get(EXTRAS);
286+
if (null != extrasElement) {
287+
if (!(extrasElement instanceof JsonObject)) {
288+
throw MessageDecodeException.fromDescription("PresenceMessage extras is of type \"" + extrasElement.getClass() + "\" when expected a JSON object.");
289+
}
290+
extras = MessageExtras.read((JsonObject) extrasElement);
291+
}
292+
293+
Integer actionValue = readInt(map, "action");
294+
if (actionValue != null) {
295+
action = Action.findByValue(actionValue);
296+
}
297+
}
298+
263299
public static class ActionSerializer implements JsonDeserializer<Action> {
264300
@Override
265301
public Action deserialize(JsonElement json, Type t, JsonDeserializationContext ctx)
@@ -268,13 +304,32 @@ public Action deserialize(JsonElement json, Type t, JsonDeserializationContext c
268304
}
269305
}
270306

271-
public static class Serializer implements JsonSerializer<PresenceMessage> {
307+
public static class Serializer implements JsonSerializer<PresenceMessage>, JsonDeserializer<PresenceMessage> {
272308
@Override
273309
public JsonElement serialize(PresenceMessage message, Type typeOfMessage, JsonSerializationContext ctx) {
274310
final JsonObject json = BaseMessage.toJsonObject(message);
275311
if(message.action != null) json.addProperty("action", message.action.getValue());
312+
if(message.extras != null) {
313+
json.add(EXTRAS, Serialisation.gson.toJsonTree(message.extras));
314+
}
276315
return json;
277316
}
317+
318+
@Override
319+
public PresenceMessage deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
320+
if (!(json instanceof JsonObject)) {
321+
throw new JsonParseException("Expected an object but got \"" + json.getClass() + "\".");
322+
}
323+
324+
final PresenceMessage message = new PresenceMessage();
325+
try {
326+
message.read((JsonObject) json);
327+
} catch (MessageDecodeException e) {
328+
Log.e(TAG, e.getMessage(), e);
329+
throw new JsonParseException("Failed to deserialize PresenceMessage from JSON.", e);
330+
}
331+
return message;
332+
}
278333
}
279334

280335
/**

lib/src/test/java/io/ably/lib/test/realtime/RealtimePresenceTest.java

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3625,6 +3625,79 @@ public void messages_from_encoded_json_array() throws AblyException {
36253625
}
36263626
}
36273627

3628+
/**
3629+
* Enter presence with extras field and verify it comes back on the other side
3630+
* Test TP3i
3631+
*/
3632+
@Test
3633+
public void presence_enter_with_extras() {
3634+
AblyRealtime clientAbly1 = null;
3635+
TestChannel testChannel = new TestChannel();
3636+
try {
3637+
/* subscribe for presence events in the anonymous connection */
3638+
PresenceWaiter presenceWaiter = new PresenceWaiter(testChannel.realtimeChannel);
3639+
/* set up a connection with specific clientId */
3640+
ClientOptions client1Opts = new ClientOptions() {{
3641+
tokenDetails = token1;
3642+
clientId = testClientId1;
3643+
}};
3644+
fillInOptions(client1Opts);
3645+
clientAbly1 = new AblyRealtime(client1Opts);
3646+
3647+
/* wait until connected */
3648+
(new ConnectionWaiter(clientAbly1.connection)).waitFor(ConnectionState.connected);
3649+
assertEquals("Verify connected state reached", clientAbly1.connection.state, ConnectionState.connected);
3650+
3651+
/* get channel and attach */
3652+
Channel client1Channel = clientAbly1.channels.get(testChannel.channelName);
3653+
client1Channel.attach();
3654+
(new ChannelWaiter(client1Channel)).waitFor(ChannelState.attached);
3655+
assertEquals("Verify attached state reached", client1Channel.state, ChannelState.attached);
3656+
3657+
/* create extras with headers.foo */
3658+
JsonObject extrasJson = new JsonObject();
3659+
JsonObject headers = new JsonObject();
3660+
headers.addProperty("foo", "bar");
3661+
extrasJson.add("headers", headers);
3662+
io.ably.lib.types.MessageExtras extras = new io.ably.lib.types.MessageExtras(extrasJson);
3663+
3664+
/* create presence message with extras */
3665+
String enterString = "Test data (presence_enter_with_extras)";
3666+
PresenceMessage presenceMsg = new PresenceMessage(PresenceMessage.Action.enter, null, enterString);
3667+
presenceMsg.extras = extras;
3668+
3669+
/* let client1 enter the channel with extras and wait for the entered event to be delivered */
3670+
CompletionWaiter enterComplete = new CompletionWaiter();
3671+
client1Channel.presence.updatePresence(presenceMsg, enterComplete);
3672+
presenceWaiter.waitFor(testClientId1, Action.enter);
3673+
PresenceMessage receivedMessage = presenceWaiter.contains(testClientId1, Action.enter);
3674+
assertNotNull("Verify presence message received", receivedMessage);
3675+
assertEquals("Verify data matches", enterString, receivedMessage.data);
3676+
3677+
/* verify extras field is present and correct */
3678+
assertNotNull("Verify extras is not null", receivedMessage.extras);
3679+
JsonObject receivedExtrasJson = receivedMessage.extras.asJsonObject();
3680+
assertNotNull("Verify extras JSON is not null", receivedExtrasJson);
3681+
assertTrue("Verify headers exists in extras", receivedExtrasJson.has("headers"));
3682+
JsonObject receivedHeaders = receivedExtrasJson.getAsJsonObject("headers");
3683+
assertNotNull("Verify headers object is not null", receivedHeaders);
3684+
assertTrue("Verify foo exists in headers", receivedHeaders.has("foo"));
3685+
assertEquals("Verify foo value matches", "bar", receivedHeaders.get("foo").getAsString());
3686+
3687+
/* verify enter callback called on completion */
3688+
enterComplete.waitFor();
3689+
assertTrue("Verify enter callback called on completion", enterComplete.success);
3690+
} catch(AblyException e) {
3691+
e.printStackTrace();
3692+
fail("Unexpected exception running test: " + e.getMessage());
3693+
} finally {
3694+
if(clientAbly1 != null)
3695+
clientAbly1.close();
3696+
if(testChannel != null)
3697+
testChannel.dispose();
3698+
}
3699+
}
3700+
36283701
static class MessagesData {
36293702
public PresenceMessage[] messages;
36303703
}

0 commit comments

Comments
 (0)