Skip to content

Commit b26ce0f

Browse files
committed
[ECO-5426] refactor: extract sync tracking logic into ObjectsSyncTracker class
- Move parseSyncChannelSerial logic to companion object for better performance - Add comprehensive unit tests covering edge cases and sync state detection - Update ObjectsManager to use ObjectsSyncTracker for cleaner separation of concerns - Removed outdated IMPLEMENTATION_SUMMARY.md
1 parent 47b3c86 commit b26ce0f

5 files changed

Lines changed: 126 additions & 174 deletions

File tree

live-objects/IMPLEMENTATION_SUMMARY.md

Lines changed: 0 additions & 141 deletions
This file was deleted.

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

Lines changed: 5 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ internal class ObjectsManager(private val liveObjects: DefaultLiveObjects) {
2222
*/
2323
private val bufferedObjectOperations = mutableListOf<ObjectMessage>() // RTO7a
2424

25-
2625
/**
2726
* Handles object messages (non-sync messages).
2827
*
@@ -49,42 +48,20 @@ internal class ObjectsManager(private val liveObjects: DefaultLiveObjects) {
4948
* @spec RTO5 - Parses sync channel serial and manages sync sequences
5049
*/
5150
internal fun handleObjectSyncMessages(objectMessages: List<ObjectMessage>, syncChannelSerial: String?) {
52-
val (syncId, syncCursor) = parseSyncChannelSerial(syncChannelSerial) // RTO5a
53-
val newSyncSequence = currentSyncId != syncId
54-
if (newSyncSequence) {
51+
val syncTracker = ObjectsSyncTracker(syncChannelSerial)
52+
if (syncTracker.hasSyncStarted(currentSyncId)) {
5553
// RTO5a2 - new sync sequence started
56-
startNewSync(syncId)
54+
startNewSync(syncTracker.syncId)
5755
}
5856

5957
// RTO5a3 - continue current sync sequence
6058
applyObjectSyncMessages(objectMessages) // RTO5b
6159

6260
// RTO5a4 - if this is the last (or only) message in a sequence of sync updates, end the sync
63-
if (syncChannelSerial.isNullOrEmpty() || syncCursor.isNullOrEmpty()) {
61+
if (syncTracker.hasSyncEnded()) {
6462
// defer the state change event until the next tick if this was a new sync sequence
6563
// to allow any event listeners to process the start of the new sequence event that was emitted earlier during this event loop.
66-
endSync(newSyncSequence)
67-
}
68-
}
69-
70-
/**
71-
* Parses sync channel serial to extract syncId and syncCursor.
72-
*
73-
* @spec RTO5 - Sync channel serial parsing logic
74-
*/
75-
private fun parseSyncChannelSerial(syncChannelSerial: String?): Pair<String?, String?> {
76-
if (syncChannelSerial.isNullOrEmpty()) {
77-
return Pair(null, null)
78-
}
79-
80-
// RTO5a1 - syncChannelSerial is a two-part identifier: <sequence id>:<cursor value>
81-
val match = Regex("^([\\w-]+):(.*)$").find(syncChannelSerial)
82-
return if (match != null) {
83-
val syncId = match.groupValues[1]
84-
val syncCursor = match.groupValues[2]
85-
Pair(syncId, syncCursor)
86-
} else {
87-
Pair(null, null)
64+
endSync(true)
8865
}
8966
}
9067

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package io.ably.lib.objects
2+
3+
/**
4+
* @spec RTO5 - SyncTracker class for tracking objects sync status
5+
*/
6+
internal class ObjectsSyncTracker(syncChannelSerial: String?) {
7+
internal val syncId: String?
8+
private val syncCursor: String?
9+
private val hasEnded: Boolean
10+
11+
init {
12+
val parsed = parseSyncChannelSerial(syncChannelSerial)
13+
syncId = parsed.first
14+
syncCursor = parsed.second
15+
hasEnded = syncChannelSerial.isNullOrEmpty() || syncCursor.isNullOrEmpty()
16+
}
17+
18+
/**
19+
* Checks if a new sync sequence has started.
20+
*
21+
* @param prevSyncId The previously stored sync ID
22+
* @return true if a new sync sequence has started, false otherwise
23+
*/
24+
internal fun hasSyncStarted(prevSyncId: String?): Boolean {
25+
return prevSyncId != syncId
26+
}
27+
28+
/**
29+
* Checks if the current sync sequence has ended.
30+
*
31+
* @return true if the sync sequence has ended, false otherwise
32+
*/
33+
internal fun hasSyncEnded(): Boolean {
34+
return hasEnded
35+
}
36+
37+
companion object {
38+
/**
39+
* Parses sync channel serial to extract syncId and syncCursor.
40+
*
41+
* @param syncChannelSerial The sync channel serial to parse
42+
* @return Pair of syncId and syncCursor, both null if parsing fails
43+
*/
44+
private fun parseSyncChannelSerial(syncChannelSerial: String?): Pair<String?, String?> {
45+
if (syncChannelSerial.isNullOrEmpty()) {
46+
return Pair(null, null)
47+
}
48+
49+
// RTO5a1 - syncChannelSerial is a two-part identifier: <sequence id>:<cursor value>
50+
val match = Regex("^([\\w-]+):(.*)$").find(syncChannelSerial)
51+
return if (match != null) {
52+
val syncId = match.groupValues[1]
53+
val syncCursor = match.groupValues[2]
54+
Pair(syncId, syncCursor)
55+
} else {
56+
Pair(null, null)
57+
}
58+
}
59+
}
60+
}

live-objects/src/main/kotlin/io/ably/lib/objects/type/livecounter/DefaultLiveCounter.kt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,8 @@ internal class DefaultLiveCounter(
4545
TODO("Not yet implemented")
4646
}
4747

48-
/**
49-
* @spec RTLC5 - Returns the current counter value
50-
*/
5148
override fun value(): Long {
52-
// RTLC5a, RTLC5b - Configuration validation would be done here
53-
return data // RTLC5c
49+
TODO("Not yet implemented")
5450
}
5551

5652
override fun applyObjectState(objectState: ObjectState): Map<String, Long> {
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package io.ably.lib.objects.unit
2+
3+
import io.ably.lib.objects.ObjectsSyncTracker
4+
import org.junit.Test
5+
import org.junit.Assert.*
6+
7+
class ObjectsSyncTrackerTest {
8+
9+
@Test
10+
fun `should parse valid sync channel serial with syncId and cursor`() {
11+
val syncTracker = ObjectsSyncTracker("sync-123:cursor-456")
12+
13+
assertEquals("sync-123", syncTracker.syncId)
14+
assertFalse(syncTracker.hasSyncStarted("sync-123"))
15+
16+
assertTrue(syncTracker.hasSyncStarted(null))
17+
assertTrue(syncTracker.hasSyncStarted("sync-124"))
18+
19+
assertFalse(syncTracker.hasSyncEnded())
20+
}
21+
22+
@Test
23+
fun `should handle null sync channel serial`() {
24+
val syncTracker = ObjectsSyncTracker(null)
25+
26+
assertNull(syncTracker.syncId)
27+
assertTrue(syncTracker.hasSyncEnded())
28+
}
29+
30+
@Test
31+
fun `should handle empty sync channel serial`() {
32+
val syncTracker = ObjectsSyncTracker("")
33+
34+
assertNull(syncTracker.syncId)
35+
assertTrue(syncTracker.hasSyncEnded())
36+
}
37+
38+
@Test
39+
fun `should handle sync channel serial with special characters`() {
40+
val syncTracker = ObjectsSyncTracker("sync_123-456:cursor_789-012")
41+
42+
assertEquals("sync_123-456", syncTracker.syncId)
43+
assertFalse(syncTracker.hasSyncEnded())
44+
}
45+
46+
@Test
47+
fun `should detect sync sequence ended when syncChannelSerial is null`() {
48+
val syncTracker = ObjectsSyncTracker(null)
49+
50+
assertTrue(syncTracker.hasSyncEnded())
51+
}
52+
53+
@Test
54+
fun `should detect sync sequence ended when sync cursor is empty`() {
55+
val syncTracker = ObjectsSyncTracker("sync-123:")
56+
assertTrue(syncTracker.hasSyncStarted(null))
57+
assertTrue(syncTracker.hasSyncStarted(""))
58+
assertTrue(syncTracker.hasSyncEnded())
59+
}
60+
}

0 commit comments

Comments
 (0)