Skip to content

Commit b0374ad

Browse files
committed
[ECO-5380] Updated LiveObjectsAdaopter interface
1. Added and implemented method to calculate maxMessageSizeLimit 2. Added extension method ensureMessageSizeWithinLimit to validate object msg sizes 3. Added unit test to validate objectMessageSizeWithinLimit
1 parent 507ed08 commit b0374ad

11 files changed

Lines changed: 96 additions & 39 deletions

File tree

lib/src/main/java/io/ably/lib/objects/Adapter.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,9 @@ public void send(@NotNull ProtocolMessage msg, @NotNull CompletionListener liste
2929
// Always queue LiveObjects messages to ensure reliable state synchronization and proper acknowledgment
3030
ably.connection.connectionManager.send(msg, true, listener);
3131
}
32+
33+
@Override
34+
public long maxMessageSizeLimit() {
35+
return ably.connection.connectionManager.maxMessageSize;
36+
}
3237
}

lib/src/main/java/io/ably/lib/objects/LiveObjectsAdapter.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,13 @@ public interface LiveObjectsAdapter {
2323
* @param channelSerial the serial to set for the channel
2424
*/
2525
void setChannelSerial(@NotNull String channelName, @NotNull String channelSerial);
26+
27+
/**
28+
* Retrieves the maximum message size allowed for the messages.
29+
* This method returns the maximum size in bytes that a message can have.
30+
*
31+
* @return the maximum message size limit in bytes.
32+
*/
33+
long maxMessageSizeLimit();
2634
}
2735

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1286,6 +1286,7 @@ private synchronized void onConnected(ProtocolMessage message) {
12861286
connection.key = connectionDetails.connectionKey; //RTN16d
12871287
maxIdleInterval = connectionDetails.maxIdleInterval;
12881288
connectionStateTtl = connectionDetails.connectionStateTtl;
1289+
maxMessageSize = connectionDetails.maxMessageSize;
12891290

12901291
/* set the clientId resolved from token, if any */
12911292
String clientId = connectionDetails.clientId;
@@ -1981,6 +1982,7 @@ private boolean isFatalError(ErrorInfo err) {
19811982
private long lastActivity;
19821983
private CMConnectivityListener connectivityListener;
19831984
private long connectionStateTtl = Defaults.connectionStateTtl;
1985+
public long maxMessageSize = Defaults.maxMessageSize;
19841986
long maxIdleInterval = Defaults.maxIdleInterval;
19851987
private int disconnectedRetryAttempt = 0;
19861988

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ public class Defaults {
5252
public static long fallbackRetryTimeout = 10*60*1000L;
5353
/* CD2h (but no default in the spec) */
5454
public static long maxIdleInterval = 20000L;
55+
// 64kB, as per CD2c
56+
public static long maxMessageSize = 65536L;
5557
/* DF1a */
5658
public static long connectionStateTtl = 120000L;
5759

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

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -543,29 +543,6 @@ protected void read(final JsonObject map) throws MessageDecodeException {
543543
}
544544
}
545545

546-
/**
547-
* Calculates the size of the message.
548-
* Spec: TM6
549-
*/
550-
protected int size() {
551-
// Spec: TM6a - Sum of sizes of name, data, clientId, and extras
552-
int nameSize = name != null ? name.length() : 0; // Spec: TM6a
553-
int dataSize = 0;
554-
555-
if (data != null) {
556-
if (data instanceof byte[]) {
557-
dataSize = ((byte[]) data).length; // Spec: TM6c
558-
} else {
559-
dataSize = Serialisation.gson.toJson(data).length(); // Spec: TM6b
560-
}
561-
}
562-
563-
int clientIdSize = clientId != null ? clientId.length() : 0; // Spec: TM6f
564-
int extrasSize = extras != null ? Serialisation.gson.toJson(extras).length() : 0; // Spec: TM6d
565-
566-
return nameSize + dataSize + clientIdSize + extrasSize;
567-
}
568-
569546
public static class Serializer implements JsonSerializer<Message>, JsonDeserializer<Message> {
570547
@Override
571548
public JsonElement serialize(Message message, Type typeOfMessage, JsonSerializationContext ctx) {

live-objects/src/main/kotlin/io/ably/lib/objects/ErrorCodes.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package io.ably.lib.objects
33
internal enum class ErrorCode(public val code: Int) {
44
BadRequest(40_000),
55
InternalError(50_000),
6+
MaxMessageSizeExceeded(40_009),
67
}
78

89
internal enum class HttpStatusCode(public val code: Int) {

live-objects/src/main/kotlin/io/ably/lib/objects/Helpers.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@ internal suspend fun LiveObjectsAdapter.sendAsync(message: ProtocolMessage) = su
2323
}
2424
}
2525

26+
internal fun LiveObjectsAdapter.ensureMessageSizeWithinLimit(objectMessages: Array<ObjectMessage>) {
27+
val maximumAllowedSize = maxMessageSizeLimit()
28+
val objectsTotalMessageSize = objectMessages.sumOf { it.size() }
29+
if (objectsTotalMessageSize > maximumAllowedSize) {
30+
throw ablyException("ObjectMessage size $objectsTotalMessageSize exceeds maximum allowed size of $maximumAllowedSize bytes",
31+
ErrorCode.MaxMessageSizeExceeded)
32+
}
33+
}
34+
2635
internal enum class ProtocolMessageFormat(private val value: String) {
2736
Msgpack("msgpack"),
2837
Json("json");

live-objects/src/main/kotlin/io/ably/lib/objects/ObjectMessage.kt

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package io.ably.lib.objects
22

3-
import com.google.gson.Gson
4-
53
/**
64
* An enum class representing the different actions that can be performed on an object.
75
* Spec: OOP2
@@ -339,11 +337,11 @@ internal data class ObjectMessage(
339337
* Calculates the size of an ObjectMessage in bytes.
340338
* Spec: OM3
341339
*/
342-
internal fun ObjectMessage.size(): Int {
340+
internal fun ObjectMessage.size(): Long {
343341
val clientIdSize = clientId?.length ?: 0 // Spec: OM3f
344342
val operationSize = operation?.size() ?: 0 // Spec: OM3b, OOP4
345343
val objectStateSize = objectState?.size() ?: 0 // Spec: OM3c, OST3
346-
val extrasSize = extras?.let { Gson().toJson(it).length } ?: 0 // Spec: OM3d
344+
val extrasSize = extras?.let { gson.toJson(it).length } ?: 0 // Spec: OM3d
347345

348346
return clientIdSize + operationSize + objectStateSize + extrasSize
349347
}
@@ -352,7 +350,7 @@ internal fun ObjectMessage.size(): Int {
352350
* Calculates the size of an ObjectOperation in bytes.
353351
* Spec: OOP4
354352
*/
355-
private fun ObjectOperation.size(): Int {
353+
private fun ObjectOperation.size(): Long {
356354
val mapOpSize = mapOp?.size() ?: 0 // Spec: OOP4b, OMO3
357355
val counterOpSize = counterOp?.size() ?: 0 // Spec: OOP4c, OCO3
358356
val mapSize = map?.size() ?: 0 // Spec: OOP4d, OMP4
@@ -365,7 +363,7 @@ private fun ObjectOperation.size(): Int {
365363
* Calculates the size of an ObjectState in bytes.
366364
* Spec: OST3
367365
*/
368-
private fun ObjectState.size(): Int {
366+
private fun ObjectState.size(): Long {
369367
val mapSize = map?.size() ?: 0 // Spec: OST3b, OMP4
370368
val counterSize = counter?.size() ?: 0 // Spec: OST3c, OCN3
371369
val createOpSize = createOp?.size() ?: 0 // Spec: OST3d, OOP4
@@ -374,10 +372,10 @@ private fun ObjectState.size(): Int {
374372
}
375373

376374
/**
377-
* Calculates the size of an ObjectMap in bytes.
375+
* Calculates the size of an ObjectMapOp in bytes.
378376
* Spec: OMO3
379377
*/
380-
private fun ObjectMapOp.size(): Int {
378+
private fun ObjectMapOp.size(): Long {
381379
val keySize = key.length // Spec: OMO3d - Size of the key
382380
val dataSize = data?.size() ?: 0 // Spec: OMO3b - Size of the data, calculated per "OD3"
383381
return keySize + dataSize
@@ -387,7 +385,7 @@ private fun ObjectMapOp.size(): Int {
387385
* Calculates the size of a CounterOp in bytes.
388386
* Spec: OCO3
389387
*/
390-
private fun ObjectCounterOp.size(): Int {
388+
private fun ObjectCounterOp.size(): Long {
391389
// Size is 8 if amount is a number, 0 if amount is null or omitted
392390
return if (amount != null) 8 else 0 // Spec: OCO3a, OCO3b
393391
}
@@ -396,7 +394,7 @@ private fun ObjectCounterOp.size(): Int {
396394
* Calculates the size of an ObjectMap in bytes.
397395
* Spec: OMP4
398396
*/
399-
private fun ObjectMap.size(): Int {
397+
private fun ObjectMap.size(): Long {
400398
// Calculate the size of all map entries in the map property
401399
val entriesSize = entries?.entries?.sumOf {
402400
it.key.length + it.value.size() // // Spec: OMP4a1, OMP4a2
@@ -409,7 +407,7 @@ private fun ObjectMap.size(): Int {
409407
* Calculates the size of an ObjectCounter in bytes.
410408
* Spec: OCN3
411409
*/
412-
private fun ObjectCounter.size(): Int {
410+
private fun ObjectCounter.size(): Long {
413411
// Size is 8 if count is a number, 0 if count is null or omitted
414412
return if (count != null) 8 else 0
415413
}
@@ -418,7 +416,7 @@ private fun ObjectCounter.size(): Int {
418416
* Calculates the size of a MapEntry in bytes.
419417
* Spec: OME3
420418
*/
421-
private fun ObjectMapEntry.size(): Int {
419+
private fun ObjectMapEntry.size(): Long {
422420
// The size is equal to the size of the data property, calculated per "OD3"
423421
return data?.size() ?: 0
424422
}
@@ -427,20 +425,20 @@ private fun ObjectMapEntry.size(): Int {
427425
* Calculates the size of an ObjectData in bytes.
428426
* Spec: OD3
429427
*/
430-
private fun ObjectData.size(): Int {
428+
private fun ObjectData.size(): Long {
431429
return value?.size() ?: 0 // Spec: OD3f
432430
}
433431

434432
/**
435433
* Calculates the size of an ObjectValue in bytes.
436434
* Spec: OD3*
437435
*/
438-
private fun ObjectValue.size(): Int {
436+
private fun ObjectValue.size(): Long {
439437
return when (value) {
440438
is Boolean -> 1 // Spec: OD3b
441-
is Binary -> value.size() // Spec: OD3c
439+
is Binary -> value.size().toLong() // Spec: OD3c
442440
is Number -> 8 // Spec: OD3d
443-
is String -> value.length // Spec: OD3e
441+
is String -> value.byteSize.toLong() // Spec: OD3e
444442
else -> 0 // Spec: OD3f
445443
}
446444
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package io.ably.lib.objects
2+
3+
import com.google.gson.Gson
4+
import com.google.gson.GsonBuilder
5+
6+
internal val gson: Gson = createGsonSerializer()
7+
8+
private fun createGsonSerializer(): Gson {
9+
return GsonBuilder().create() // Do not call serializeNulls() to omit null values
10+
}

live-objects/src/main/kotlin/io/ably/lib/objects/Utils.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,10 @@ private fun createAblyException(
3333
internal fun clientError(errorMessage: String) = ablyException(errorMessage, ErrorCode.BadRequest, HttpStatusCode.BadRequest)
3434

3535
internal fun serverError(errorMessage: String) = ablyException(errorMessage, ErrorCode.InternalError, HttpStatusCode.InternalServerError)
36+
37+
/**
38+
* Calculates the byte size of a string.
39+
* For non-ASCII, the byte size can be 2–4x the character count. For ASCII, there is no difference.
40+
*/
41+
internal val String.byteSize: Int
42+
get() = this.toByteArray(Charsets.UTF_8).size

0 commit comments

Comments
 (0)