Skip to content

Commit f736ce8

Browse files
Port integration tests for buffer clearing rules from ably-js
Port four test cases from ably-js commit 9b2224d that verify the updated buffered object operations clearing behaviour: 1. bufferedObjectOperationsAreDiscardedOnAttached — starts a sync sequence, injects an operation, receives ATTACHED, injects another operation, completes sync, verifies only the post-ATTACHED operation was applied 2. bufferedObjectOperationsAreDiscardedWhenAlreadySyncingChannel- ReceivesAttached — already SYNCING, injects an operation, receives another ATTACHED, injects another operation, completes sync, verifies only the post-ATTACHED operation was applied 3. bufferedObjectOperationsAreNotDiscardedOnNewObjectSyncSequence — starts a sync, injects an operation, starts a new sync with a different sequence ID, injects another operation, completes the second sync, verifies BOTH operations were applied (buffer was NOT cleared by the new sequence) 4. operationsAreBufferedDuringResyncWithoutPrecedingAttached — completes a sync, then receives OBJECT_SYNC without a preceding ATTACHED, verifies operations are buffered during the resync and applied upon completion Also adds a testsOnly_bufferedObjectOperationsCount accessor to support direct assertions on buffer state in these tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9243c3f commit f736ce8

File tree

1 file changed

+225
-0
lines changed

1 file changed

+225
-0
lines changed

Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,231 @@ struct InternalDefaultRealtimeObjectsTests {
623623
let newRoot = realtimeObjects.testsOnly_objectsPool.root
624624
#expect(newRoot.testsOnly_data.isEmpty) // Should be zero-valued (empty)
625625
}
626+
627+
// MARK: - RTO4d Buffered Operations Tests
628+
629+
// @spec RTO4d
630+
@Test
631+
func bufferedObjectOperationsAreDiscardedOnAttached() {
632+
let internalQueue = TestFactories.createInternalQueue()
633+
let realtimeObjects = InternalDefaultRealtimeObjectsTests.createDefaultRealtimeObjects(internalQueue: internalQueue)
634+
let sequenceId = "seq1"
635+
636+
// Start a sync sequence with a cursor (so it doesn't complete immediately)
637+
internalQueue.ably_syncNoDeadlock {
638+
realtimeObjects.nosync_handleObjectSyncProtocolMessage(
639+
objectMessages: [
640+
TestFactories.mapObjectMessage(objectId: "map:sync@100"),
641+
],
642+
protocolMessageChannelSerial: "\(sequenceId):cursor1",
643+
)
644+
}
645+
646+
// Inject an OBJECT operation; it will be buffered per RTO8a
647+
internalQueue.ably_syncNoDeadlock {
648+
realtimeObjects.nosync_handleObjectProtocolMessage(objectMessages: [
649+
TestFactories.mapCreateOperationMessage(objectId: "map:pre-attached@200"),
650+
])
651+
}
652+
653+
#expect(realtimeObjects.testsOnly_bufferedObjectOperationsCount == 1)
654+
655+
// Receive ATTACHED with HAS_OBJECTS — buffer should be cleared per RTO4d
656+
internalQueue.ably_syncNoDeadlock {
657+
realtimeObjects.nosync_onChannelAttached(hasObjects: true)
658+
}
659+
660+
#expect(realtimeObjects.testsOnly_bufferedObjectOperationsCount == 0)
661+
662+
// Inject another OBJECT operation after ATTACHED
663+
internalQueue.ably_syncNoDeadlock {
664+
realtimeObjects.nosync_handleObjectProtocolMessage(objectMessages: [
665+
TestFactories.mapCreateOperationMessage(objectId: "map:post-attached@300"),
666+
])
667+
}
668+
669+
// Complete the sync sequence
670+
internalQueue.ably_syncNoDeadlock {
671+
realtimeObjects.nosync_handleObjectSyncProtocolMessage(
672+
objectMessages: [],
673+
protocolMessageChannelSerial: "\(sequenceId):",
674+
)
675+
}
676+
677+
// Only the post-ATTACHED operation should have been applied
678+
let pool = realtimeObjects.testsOnly_objectsPool
679+
#expect(pool.entries["map:pre-attached@200"] == nil)
680+
#expect(pool.entries["map:post-attached@300"] != nil)
681+
}
682+
683+
// @spec RTO4d
684+
@Test
685+
func bufferedObjectOperationsAreDiscardedWhenAlreadySyncingChannelReceivesAttached() {
686+
let internalQueue = TestFactories.createInternalQueue()
687+
let realtimeObjects = InternalDefaultRealtimeObjectsTests.createDefaultRealtimeObjects(internalQueue: internalQueue)
688+
let sequenceId = "seq1"
689+
690+
// Transition to SYNCING via a first ATTACHED
691+
internalQueue.ably_syncNoDeadlock {
692+
realtimeObjects.nosync_onChannelAttached(hasObjects: true)
693+
}
694+
695+
// Start a sync sequence
696+
internalQueue.ably_syncNoDeadlock {
697+
realtimeObjects.nosync_handleObjectSyncProtocolMessage(
698+
objectMessages: [
699+
TestFactories.mapObjectMessage(objectId: "map:sync@100"),
700+
],
701+
protocolMessageChannelSerial: "\(sequenceId):cursor1",
702+
)
703+
}
704+
705+
// Inject an OBJECT operation; it will be buffered per RTO8a
706+
internalQueue.ably_syncNoDeadlock {
707+
realtimeObjects.nosync_handleObjectProtocolMessage(objectMessages: [
708+
TestFactories.mapCreateOperationMessage(objectId: "map:pre-attached@200"),
709+
])
710+
}
711+
712+
#expect(realtimeObjects.testsOnly_bufferedObjectOperationsCount == 1)
713+
714+
// Receive another ATTACHED (e.g. due to RESUMED) — buffer should be cleared per RTO4d
715+
internalQueue.ably_syncNoDeadlock {
716+
realtimeObjects.nosync_onChannelAttached(hasObjects: true)
717+
}
718+
719+
#expect(realtimeObjects.testsOnly_bufferedObjectOperationsCount == 0)
720+
721+
// Inject another OBJECT operation after the second ATTACHED
722+
internalQueue.ably_syncNoDeadlock {
723+
realtimeObjects.nosync_handleObjectProtocolMessage(objectMessages: [
724+
TestFactories.mapCreateOperationMessage(objectId: "map:post-attached@300"),
725+
])
726+
}
727+
728+
// Complete the sync sequence
729+
internalQueue.ably_syncNoDeadlock {
730+
realtimeObjects.nosync_handleObjectSyncProtocolMessage(
731+
objectMessages: [],
732+
protocolMessageChannelSerial: "\(sequenceId):",
733+
)
734+
}
735+
736+
// Only the post-ATTACHED operation should have been applied
737+
let pool = realtimeObjects.testsOnly_objectsPool
738+
#expect(pool.entries["map:pre-attached@200"] == nil)
739+
#expect(pool.entries["map:post-attached@300"] != nil)
740+
}
741+
742+
// Verifies that RTO5a2b (replaced by RTO4d) no longer clears buffered operations on new OBJECT_SYNC sequence
743+
@Test
744+
func bufferedObjectOperationsAreNotDiscardedOnNewObjectSyncSequence() {
745+
let internalQueue = TestFactories.createInternalQueue()
746+
let realtimeObjects = InternalDefaultRealtimeObjectsTests.createDefaultRealtimeObjects(internalQueue: internalQueue)
747+
let firstSequenceId = "seq1"
748+
let secondSequenceId = "seq2"
749+
750+
// Start a first sync sequence
751+
internalQueue.ably_syncNoDeadlock {
752+
realtimeObjects.nosync_handleObjectSyncProtocolMessage(
753+
objectMessages: [
754+
TestFactories.mapObjectMessage(objectId: "map:sync1@100"),
755+
],
756+
protocolMessageChannelSerial: "\(firstSequenceId):cursor1",
757+
)
758+
}
759+
760+
// Inject an OBJECT operation; it will be buffered per RTO8a
761+
internalQueue.ably_syncNoDeadlock {
762+
realtimeObjects.nosync_handleObjectProtocolMessage(objectMessages: [
763+
TestFactories.mapCreateOperationMessage(objectId: "map:buffered1@200"),
764+
])
765+
}
766+
767+
#expect(realtimeObjects.testsOnly_bufferedObjectOperationsCount == 1)
768+
769+
// Start a new sync sequence with a different ID — buffer should NOT be cleared
770+
internalQueue.ably_syncNoDeadlock {
771+
realtimeObjects.nosync_handleObjectSyncProtocolMessage(
772+
objectMessages: [
773+
TestFactories.mapObjectMessage(objectId: "map:sync2@300"),
774+
],
775+
protocolMessageChannelSerial: "\(secondSequenceId):cursor1",
776+
)
777+
}
778+
779+
// Inject another OBJECT operation
780+
internalQueue.ably_syncNoDeadlock {
781+
realtimeObjects.nosync_handleObjectProtocolMessage(objectMessages: [
782+
TestFactories.mapCreateOperationMessage(objectId: "map:buffered2@400"),
783+
])
784+
}
785+
786+
#expect(realtimeObjects.testsOnly_bufferedObjectOperationsCount == 2)
787+
788+
// Complete the second sync sequence
789+
internalQueue.ably_syncNoDeadlock {
790+
realtimeObjects.nosync_handleObjectSyncProtocolMessage(
791+
objectMessages: [],
792+
protocolMessageChannelSerial: "\(secondSequenceId):",
793+
)
794+
}
795+
796+
// Both buffered operations should have been applied (buffer was NOT cleared by new sequence)
797+
let pool = realtimeObjects.testsOnly_objectsPool
798+
#expect(pool.entries["map:buffered1@200"] != nil)
799+
#expect(pool.entries["map:buffered2@400"] != nil)
800+
}
801+
802+
// Verifies that operations are buffered when OBJECT_SYNC is received after a completed sync without a preceding ATTACHED (RTO5e resync)
803+
@Test
804+
func operationsAreBufferedDuringResyncWithoutPrecedingAttached() {
805+
let internalQueue = TestFactories.createInternalQueue()
806+
let realtimeObjects = InternalDefaultRealtimeObjectsTests.createDefaultRealtimeObjects(internalQueue: internalQueue)
807+
let firstSequenceId = "seq1"
808+
let resyncSequenceId = "seq2"
809+
810+
// Complete an initial sync to reach SYNCED state
811+
internalQueue.ably_syncNoDeadlock {
812+
realtimeObjects.nosync_handleObjectSyncProtocolMessage(
813+
objectMessages: [
814+
TestFactories.mapObjectMessage(objectId: "map:initial@100"),
815+
],
816+
protocolMessageChannelSerial: nil, // Complete sync immediately
817+
)
818+
}
819+
820+
// Receive an OBJECT_SYNC without a preceding ATTACHED — triggers RTO5e resync
821+
internalQueue.ably_syncNoDeadlock {
822+
realtimeObjects.nosync_handleObjectSyncProtocolMessage(
823+
objectMessages: [
824+
TestFactories.mapObjectMessage(objectId: "map:resync@200"),
825+
],
826+
protocolMessageChannelSerial: "\(resyncSequenceId):cursor1",
827+
)
828+
}
829+
830+
// Inject an OBJECT operation; should be buffered since we're now SYNCING again
831+
internalQueue.ably_syncNoDeadlock {
832+
realtimeObjects.nosync_handleObjectProtocolMessage(objectMessages: [
833+
TestFactories.mapCreateOperationMessage(objectId: "map:buffered@300"),
834+
])
835+
}
836+
837+
#expect(realtimeObjects.testsOnly_bufferedObjectOperationsCount == 1)
838+
839+
// Complete the resync sequence
840+
internalQueue.ably_syncNoDeadlock {
841+
realtimeObjects.nosync_handleObjectSyncProtocolMessage(
842+
objectMessages: [],
843+
protocolMessageChannelSerial: "\(resyncSequenceId):",
844+
)
845+
}
846+
847+
// Buffered operation should have been applied upon completion
848+
let pool = realtimeObjects.testsOnly_objectsPool
849+
#expect(pool.entries["map:buffered@300"] != nil)
850+
}
626851
}
627852

628853
/// Tests for `InternalDefaultRealtimeObjects.getRoot`, covering RTO1 specification points

0 commit comments

Comments
 (0)