Skip to content

Commit 17d439f

Browse files
committed
feat: implement protocol v4 support for enhanced message annotations and versioning
Introduce `MessageAnnotations` and `MessageVersion` classes for protocol v4. Replace `summary` with `annotations` in `Message`. Enhance version tracking with detailed metadata, ensuring compatibility with the new protocol. Update tests and protocol version to `4`.
1 parent 3f9d007 commit 17d439f

File tree

11 files changed

+486
-256
lines changed

11 files changed

+486
-256
lines changed

lib/src/main/java/io/ably/lib/realtime/ChannelBase.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@
2929
import io.ably.lib.types.DeltaExtras;
3030
import io.ably.lib.types.ErrorInfo;
3131
import io.ably.lib.types.Message;
32-
import io.ably.lib.types.MessageAction;
3332
import io.ably.lib.types.MessageDecodeException;
3433
import io.ably.lib.types.MessageSerializer;
34+
import io.ably.lib.types.MessageVersion;
3535
import io.ably.lib.types.PaginatedResult;
3636
import io.ably.lib.types.Param;
3737
import io.ably.lib.types.PresenceMessage;
@@ -901,10 +901,10 @@ private void onMessage(final ProtocolMessage protocolMessage) {
901901
if(msg.connectionId == null) msg.connectionId = protocolMessage.connectionId;
902902
if(msg.timestamp == 0) msg.timestamp = protocolMessage.timestamp;
903903
if(msg.id == null) msg.id = protocolMessage.id + ':' + i;
904-
// (TM2k)
905-
if(msg.serial == null && msg.version != null && msg.action == MessageAction.MESSAGE_CREATE) msg.serial = msg.version;
906-
// (TM2o)
907-
if(msg.createdAt == null && msg.action == MessageAction.MESSAGE_CREATE) msg.createdAt = msg.timestamp;
904+
// (TM2s1)
905+
if(msg.version == null) msg.version = new MessageVersion(msg.serial, msg.timestamp);
906+
if(msg.version.serial == null) msg.version.serial = msg.serial;
907+
if(msg.version.timestamp == 0) msg.version.timestamp = msg.timestamp;
908908

909909
try {
910910
if (msg.data != null) msg.decode(options, decodingContext);

lib/src/main/java/io/ably/lib/transport/Defaults.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public class Defaults {
1212
* spec: G4
1313
* </p>
1414
*/
15-
public static final String ABLY_PROTOCOL_VERSION = "2";
15+
public static final String ABLY_PROTOCOL_VERSION = "4";
1616

1717
public static final String ABLY_AGENT_VERSION = String.format("%s/%s", "ably-java", BuildConfig.VERSION);
1818

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

Lines changed: 26 additions & 179 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
import java.io.IOException;
44
import java.lang.reflect.Type;
55
import java.util.Collection;
6-
import java.util.HashMap;
7-
import java.util.Map;
86

97
import com.google.gson.JsonArray;
108
import com.google.gson.JsonDeserializer;
@@ -50,143 +48,41 @@ public class Message extends BaseMessage {
5048
public String connectionKey;
5149

5250
/**
53-
* (TM2k) serial string – an opaque string that uniquely identifies the message. If a message received from Ably
51+
* (TM2r) serial string – an opaque string that uniquely identifies the message. If a message received from Ably
5452
* (whether over realtime or REST, eg history) with an action of MESSAGE_CREATE does not contain a serial,
5553
* the SDK must set it equal to its version.
5654
*/
5755
public String serial;
5856

5957
/**
60-
* (TM2p) version string – an opaque string that uniquely identifies the message, and is different for different versions.
58+
* (TM2s) version object – enhanced version tracking for Protocol v4.
59+
* Contains detailed version metadata including serial and timestamp information.
6160
* (May not be populated depending on app & channel namespace settings)
6261
*/
63-
public String version;
62+
public MessageVersion version;
6463

6564
/**
6665
* (TM2j) action enum
6766
*/
6867
public MessageAction action;
6968

70-
/**
71-
* (TM2o) createdAt time in milliseconds since epoch. If a message received from Ably
72-
* (whether over realtime or REST, eg history) with an action of MESSAGE_CREATE does not contain a createdAt,
73-
* the SDK must set it equal to the TM2f timestamp.
74-
*/
75-
public Long createdAt;
76-
77-
/**
78-
* (TM2l) ref string – an opaque string that uniquely identifies some referenced message.
79-
*/
80-
public String refSerial;
81-
82-
/**
83-
* (TM2m) refType string – an opaque string that identifies the type of this reference.
84-
*/
85-
public String refType;
86-
87-
/**
88-
* (TM2n) operation object – data object that may contain the `optional` attributes.
89-
*/
90-
public Operation operation;
91-
9269
/**
9370
* (TM2q) A summary of all the annotations that have been made to the message, whose keys are the `type` fields
9471
* from any annotations that it includes. Will always be populated for a message with action {@code MESSAGE_SUMMARY},
9572
* and may be populated for any other type (in particular a message retrieved from
9673
* REST history will have its latest summary included).
9774
*/
98-
public Summary summary;
99-
100-
public static class Operation {
101-
public String clientId;
102-
public String description;
103-
public Map<String, String> metadata;
75+
public MessageAnnotations annotations;
10476

105-
void write(MessagePacker packer) throws IOException {
106-
int fieldCount = 0;
107-
if (clientId != null) fieldCount++;
108-
if (description != null) fieldCount++;
109-
if (metadata != null) fieldCount++;
11077

111-
packer.packMapHeader(fieldCount);
112-
113-
if (clientId != null) {
114-
packer.packString("clientId");
115-
packer.packString(clientId);
116-
}
117-
if (description != null) {
118-
packer.packString("description");
119-
packer.packString(description);
120-
}
121-
if (metadata != null) {
122-
packer.packString("metadata");
123-
packer.packMapHeader(metadata.size());
124-
for (Map.Entry<String, String> entry : metadata.entrySet()) {
125-
packer.packString(entry.getKey());
126-
packer.packString(entry.getValue());
127-
}
128-
}
129-
}
130-
131-
protected static Operation read(final MessageUnpacker unpacker) throws IOException {
132-
Operation operation = new Operation();
133-
int fieldCount = unpacker.unpackMapHeader();
134-
for (int i = 0; i < fieldCount; i++) {
135-
String fieldName = unpacker.unpackString().intern();
136-
switch (fieldName) {
137-
case "clientId":
138-
operation.clientId = unpacker.unpackString();
139-
break;
140-
case "description":
141-
operation.description = unpacker.unpackString();
142-
break;
143-
case "metadata":
144-
int mapSize = unpacker.unpackMapHeader();
145-
operation.metadata = new HashMap<>(mapSize);
146-
for (int j = 0; j < mapSize; j++) {
147-
String key = unpacker.unpackString();
148-
String value = unpacker.unpackString();
149-
operation.metadata.put(key, value);
150-
}
151-
break;
152-
default:
153-
unpacker.skipValue();
154-
break;
155-
}
156-
}
157-
return operation;
158-
}
159-
160-
protected static Operation read(final JsonObject jsonObject) throws MessageDecodeException {
161-
Operation operation = new Operation();
162-
if (jsonObject.has("clientId")) {
163-
operation.clientId = jsonObject.get("clientId").getAsString();
164-
}
165-
if (jsonObject.has("description")) {
166-
operation.description = jsonObject.get("description").getAsString();
167-
}
168-
if (jsonObject.has("metadata")) {
169-
JsonObject metadataObject = jsonObject.getAsJsonObject("metadata");
170-
operation.metadata = new HashMap<>();
171-
for (Map.Entry<String, JsonElement> entry : metadataObject.entrySet()) {
172-
operation.metadata.put(entry.getKey(), entry.getValue().getAsString());
173-
}
174-
}
175-
return operation;
176-
}
177-
}
17878

17979
private static final String NAME = "name";
18080
private static final String EXTRAS = "extras";
18181
private static final String CONNECTION_KEY = "connectionKey";
18282
private static final String SERIAL = "serial";
18383
private static final String VERSION = "version";
18484
private static final String ACTION = "action";
185-
private static final String CREATED_AT = "createdAt";
186-
private static final String REF_SERIAL = "refSerial";
187-
private static final String REF_TYPE = "refType";
188-
private static final String OPERATION = "operation";
189-
private static final String SUMMARY = "summary";
85+
private static final String ANNOTATIONS = "annotations";
19086

19187
/**
19288
* Default constructor
@@ -270,11 +166,7 @@ void writeMsgpack(MessagePacker packer) throws IOException {
270166
if(serial != null) ++fieldCount;
271167
if(version != null) ++fieldCount;
272168
if(action != null) ++fieldCount;
273-
if(createdAt != null) ++fieldCount;
274-
if(refSerial != null) ++fieldCount;
275-
if(refType != null) ++fieldCount;
276-
if(operation != null) ++fieldCount;
277-
if(summary != null) ++fieldCount;
169+
if(annotations != null) ++fieldCount;
278170

279171
packer.packMapHeader(fieldCount);
280172
super.writeFields(packer);
@@ -296,31 +188,15 @@ void writeMsgpack(MessagePacker packer) throws IOException {
296188
}
297189
if(version != null) {
298190
packer.packString(VERSION);
299-
packer.packString(version);
191+
version.writeMsgpack(packer);
300192
}
301193
if(action != null) {
302194
packer.packString(ACTION);
303195
packer.packInt(action.ordinal());
304196
}
305-
if(createdAt != null) {
306-
packer.packString(CREATED_AT);
307-
packer.packLong(createdAt);
308-
}
309-
if(refSerial != null) {
310-
packer.packString(REF_SERIAL);
311-
packer.packString(refSerial);
312-
}
313-
if(refType != null) {
314-
packer.packString(REF_TYPE);
315-
packer.packString(refType);
316-
}
317-
if(operation != null) {
318-
packer.packString(OPERATION);
319-
operation.write(packer);
320-
}
321-
if(summary != null) {
322-
packer.packString(SUMMARY);
323-
summary.write(packer);
197+
if(annotations != null) {
198+
packer.packString(ANNOTATIONS);
199+
annotations.writeMsgpack(packer);
324200
}
325201
}
326202

@@ -346,19 +222,11 @@ Message readMsgpack(MessageUnpacker unpacker) throws IOException {
346222
} else if (fieldName.equals(SERIAL)) {
347223
serial = unpacker.unpackString();
348224
} else if (fieldName.equals(VERSION)) {
349-
version = unpacker.unpackString();
225+
version = MessageVersion.fromMsgpack(unpacker);
350226
} else if (fieldName.equals(ACTION)) {
351227
action = MessageAction.tryFindByOrdinal(unpacker.unpackInt());
352-
} else if (fieldName.equals(CREATED_AT)) {
353-
createdAt = unpacker.unpackLong();
354-
} else if (fieldName.equals(REF_SERIAL)) {
355-
refSerial = unpacker.unpackString();
356-
} else if (fieldName.equals(REF_TYPE)) {
357-
refType = unpacker.unpackString();
358-
} else if (fieldName.equals(OPERATION)) {
359-
operation = Operation.read(unpacker);
360-
} else if (fieldName.equals(SUMMARY)) {
361-
summary = Summary.read(unpacker);
228+
} else if (fieldName.equals(ANNOTATIONS)) {
229+
annotations = MessageAnnotations.fromMsgpack(unpacker);
362230
}
363231
else {
364232
Log.v(TAG, "Unexpected field: " + fieldName);
@@ -519,27 +387,18 @@ protected void read(final JsonObject map) throws MessageDecodeException {
519387
connectionKey = readString(map, CONNECTION_KEY);
520388

521389
serial = readString(map, SERIAL);
522-
version = readString(map, VERSION);
390+
391+
// Handle version field - supports both legacy string format and new MessageVersion object format
392+
final JsonElement versionElement = map.get(VERSION);
393+
if (versionElement != null) {
394+
version = MessageVersion.read(versionElement);
395+
}
523396
Integer actionOrdinal = readInt(map, ACTION);
524397
action = actionOrdinal == null ? null : MessageAction.tryFindByOrdinal(actionOrdinal);
525-
createdAt = readLong(map, CREATED_AT);
526-
refSerial = readString(map, REF_SERIAL);
527-
refType = readString(map, REF_TYPE);
528-
529-
final JsonElement operationElement = map.get(OPERATION);
530-
if (null != operationElement) {
531-
if (!operationElement.isJsonObject()) {
532-
throw MessageDecodeException.fromDescription("Message operation is of type \"" + operationElement.getClass() + "\" when expected a JSON object.");
533-
}
534-
operation = Operation.read(operationElement.getAsJsonObject());
535-
}
536398

537-
final JsonElement summaryElement = map.get(SUMMARY);
538-
if (summaryElement != null) {
539-
if (!summaryElement.isJsonObject()) {
540-
throw MessageDecodeException.fromDescription("Message summary is of type \"" + summaryElement.getClass() + "\" when expected a JSON object.");
541-
}
542-
summary = Summary.read(summaryElement.getAsJsonObject());
399+
final JsonElement annotationsElement = map.get(ANNOTATIONS);
400+
if (annotationsElement != null) {
401+
annotations = MessageAnnotations.read(annotationsElement);
543402
}
544403
}
545404

@@ -560,25 +419,13 @@ public JsonElement serialize(Message message, Type typeOfMessage, JsonSerializat
560419
json.addProperty(SERIAL, message.serial);
561420
}
562421
if (message.version != null) {
563-
json.addProperty(VERSION, message.version);
422+
json.add(VERSION, message.version.toJsonTree());
564423
}
565424
if (message.action != null) {
566425
json.addProperty(ACTION, message.action.ordinal());
567426
}
568-
if (message.createdAt != null) {
569-
json.addProperty(CREATED_AT, message.createdAt);
570-
}
571-
if (message.refSerial != null) {
572-
json.addProperty(REF_SERIAL, message.refSerial);
573-
}
574-
if (message.refType != null) {
575-
json.addProperty(REF_TYPE, message.refType);
576-
}
577-
if (message.operation != null) {
578-
json.add(OPERATION, Serialisation.gson.toJsonTree(message.operation));
579-
}
580-
if (message.summary != null) {
581-
json.add(SUMMARY, message.summary.toJsonTree());
427+
if (message.annotations != null) {
428+
json.add(ANNOTATIONS, message.annotations.toJsonTree());
582429
}
583430
return json;
584431
}

0 commit comments

Comments
 (0)