@@ -8,7 +8,6 @@ import { strict as assert } from "assert";
88import { Side } from "@fluidframework/merge-tree/internal" ;
99import { useStrictPartialLengthChecks } from "@fluidframework/merge-tree/internal/test" ;
1010import { MockContainerRuntimeFactoryForReconnection } from "@fluidframework/test-runtime-utils/internal" ;
11-
1211import { IntervalStickiness } from "../intervals/index.js" ;
1312
1413import { Client , assertConsistent , assertSequenceIntervals } from "./intervalTestUtils.js" ;
@@ -592,4 +591,225 @@ describe("interval rebasing", () => {
592591
593592 assertSequenceIntervals ( clients [ 0 ] . sharedString , intervals , [ { start : 0 , end : 2 } ] ) ;
594593 } ) ;
594+
595+ it ( "emits deleteInterval event when interval slides off during rebasing" , async ( ) => {
596+ const { containerRuntimeFactory, clients } = setup3Clients ( ) ;
597+
598+ // Set up initial content - use a scenario that causes interval to slide off
599+ clients [ 0 ] . sharedString . insertText ( 0 , "012Z45" ) ;
600+ clients [ 2 ] . sharedString . insertText ( 0 , "X" ) ;
601+ containerRuntimeFactory . processAllMessages ( ) ;
602+ await assertConsistent ( clients ) ;
603+
604+ clients [ 1 ] . sharedString . insertText ( 0 , "01234567" ) ;
605+ clients [ 0 ] . containerRuntime . connected = false ;
606+ containerRuntimeFactory . processAllMessages ( ) ;
607+ await assertConsistent ( clients ) ;
608+
609+ clients [ 0 ] . sharedString . insertText ( 0 , "ABCDEFGHIJKLMN" ) ;
610+ const collection_0 = clients [ 0 ] . sharedString . getIntervalCollection ( "test" ) ;
611+ const interval = collection_0 . add ( { start : 20 , end : 20 } ) ;
612+ const intervalId = interval . getIntervalId ( ) ;
613+
614+ // Track deleteInterval events
615+ let deleteEventFired = false ;
616+ let deletedIntervalId : string | undefined ;
617+ // eslint-disable-next-line @typescript-eslint/no-shadow
618+ collection_0 . on ( "deleteInterval" , ( interval , local , op ) => {
619+ deleteEventFired = true ;
620+ deletedIntervalId = interval ?. getIntervalId ( ) ;
621+ } ) ;
622+
623+ // Cause the interval to slide off
624+ clients [ 2 ] . sharedString . removeRange ( 13 , 15 ) ;
625+ containerRuntimeFactory . processAllMessages ( ) ;
626+ await assertConsistent ( clients ) ;
627+
628+ // Reconnect - this triggers rebasing where the interval will slide off
629+ clients [ 0 ] . containerRuntime . connected = true ;
630+ containerRuntimeFactory . processAllMessages ( ) ;
631+ await assertConsistent ( clients ) ;
632+
633+ // Verify the deleteInterval event was emitted
634+ // If the interval slides off, it should have been deleted and event emitted
635+ const finalInterval = collection_0 . getIntervalById ( intervalId ) ;
636+ if ( ! finalInterval ) {
637+ // Interval slide off - verify event was emitted
638+ assert . equal (
639+ deleteEventFired ,
640+ true ,
641+ "deleteInterval event should be emitted when interval slides off during rebasing" ,
642+ ) ;
643+ assert . equal ( deletedIntervalId , intervalId , "deleted interval ID should match" ) ;
644+ }
645+ } ) ;
646+
647+ it ( "does not emit events with undefined interval for deleted intervals" , async ( ) => {
648+ const { containerRuntimeFactory, clients } = setup3Clients ( ) ;
649+
650+ // Set up initial content
651+ clients [ 0 ] . sharedString . insertText ( 0 , "ABCD" ) ;
652+ containerRuntimeFactory . processAllMessages ( ) ;
653+ await assertConsistent ( clients ) ;
654+
655+ // Add interval on client 0
656+ const collection_0 = clients [ 0 ] . sharedString . getIntervalCollection ( "test" ) ;
657+ const collection_1 = clients [ 1 ] . sharedString . getIntervalCollection ( "test" ) ;
658+ const interval_0 = collection_0 . add ( { start : 1 , end : 3 } ) ; // BC
659+ const intervalId = interval_0 . getIntervalId ( ) ;
660+
661+ containerRuntimeFactory . processAllMessages ( ) ;
662+ await assertConsistent ( clients ) ;
663+
664+ // Track all events to ensure interval is never undefined
665+ let eventWithUndefinedInterval = false ;
666+ const checkInterval = ( interval ) => {
667+ if ( interval === undefined ) {
668+ eventWithUndefinedInterval = true ;
669+ }
670+ } ;
671+
672+ collection_1 . on ( "changeInterval" , ( interval ) => checkInterval ( interval ) ) ;
673+ collection_1 . on ( "propertyChanged" , ( interval ) => checkInterval ( interval ) ) ;
674+ collection_1 . on ( "changed" , ( interval ) => checkInterval ( interval ) ) ;
675+
676+ // Client 0: Delete the interval locally
677+ collection_0 . removeIntervalById ( intervalId ) ;
678+
679+ // Client 1: Change properties of the same interval before seeing the delete
680+ clients [ 1 ] . containerRuntime . connected = false ;
681+ collection_1 . change ( intervalId , { props : { foo : "bar" } } ) ;
682+
683+ // Process delete op first
684+ containerRuntimeFactory . processOneMessage ( ) ;
685+
686+ // Reconnect client 1 and process the property change
687+ clients [ 1 ] . containerRuntime . connected = true ;
688+ containerRuntimeFactory . processAllMessages ( ) ;
689+ await assertConsistent ( clients ) ;
690+
691+ // Verify no events were emitted with undefined interval
692+ assert . equal (
693+ eventWithUndefinedInterval ,
694+ false ,
695+ "events should never be emitted with undefined interval" ,
696+ ) ;
697+
698+ // Verify interval is deleted on both clients
699+ assert . equal (
700+ collection_0 . getIntervalById ( intervalId ) ,
701+ undefined ,
702+ "interval should be deleted on client 0" ,
703+ ) ;
704+ assert . equal (
705+ collection_1 . getIntervalById ( intervalId ) ,
706+ undefined ,
707+ "interval should be deleted on client 1" ,
708+ ) ;
709+ } ) ;
710+
711+ it ( "does not emit changeInterval event with undefined for deleted interval with endpoint changes" , async ( ) => {
712+ const { containerRuntimeFactory, clients } = setup3Clients ( ) ;
713+
714+ clients [ 0 ] . sharedString . insertText ( 0 , "ABCDEFGH" ) ;
715+ containerRuntimeFactory . processAllMessages ( ) ;
716+ await assertConsistent ( clients ) ;
717+
718+ const collection_0 = clients [ 0 ] . sharedString . getIntervalCollection ( "test" ) ;
719+ const collection_1 = clients [ 1 ] . sharedString . getIntervalCollection ( "test" ) ;
720+ const interval_0 = collection_0 . add ( { start : 1 , end : 3 } ) ;
721+ const intervalId = interval_0 . getIntervalId ( ) ;
722+
723+ containerRuntimeFactory . processAllMessages ( ) ;
724+ await assertConsistent ( clients ) ;
725+
726+ // Track changeInterval events to ensure interval is never undefined
727+ let changeEventWithUndefinedInterval = false ;
728+ collection_1 . on ( "changeInterval" , ( interval , previousInterval , local , op ) => {
729+ if ( interval === undefined ) {
730+ changeEventWithUndefinedInterval = true ;
731+ }
732+ } ) ;
733+
734+ // Client 0: Delete the interval
735+ collection_0 . removeIntervalById ( intervalId ) ;
736+
737+ // Client 1: Change endpoints of the same interval before seeing the delete
738+ clients [ 1 ] . containerRuntime . connected = false ;
739+ collection_1 . change ( intervalId , { start : 2 , end : 4 } ) ;
740+
741+ // Process delete op first
742+ containerRuntimeFactory . processOneMessage ( ) ;
743+
744+ // Reconnect client 1 and process the endpoint change
745+ clients [ 1 ] . containerRuntime . connected = true ;
746+ containerRuntimeFactory . processAllMessages ( ) ;
747+ await assertConsistent ( clients ) ;
748+
749+ // Verify no changeInterval events were emitted with undefined interval
750+ assert . equal (
751+ changeEventWithUndefinedInterval ,
752+ false ,
753+ "changeInterval event should never be emitted with undefined interval" ,
754+ ) ;
755+
756+ // Verify interval is deleted on both clients
757+ assert . equal ( collection_0 . getIntervalById ( intervalId ) , undefined ) ;
758+ assert . equal ( collection_1 . getIntervalById ( intervalId ) , undefined ) ;
759+ } ) ;
760+
761+ it ( "does not emit events with undefined when interval deleted during concurrent add" , async ( ) => {
762+ const { containerRuntimeFactory, clients } = setup3Clients ( ) ;
763+
764+ clients [ 0 ] . sharedString . insertText ( 0 , "ABCD" ) ;
765+ containerRuntimeFactory . processAllMessages ( ) ;
766+ await assertConsistent ( clients ) ;
767+
768+ const collection_0 = clients [ 0 ] . sharedString . getIntervalCollection ( "test" ) ;
769+ const collection_1 = clients [ 1 ] . sharedString . getIntervalCollection ( "test" ) ;
770+
771+ // Client 0 adds an interval
772+ const interval_0 = collection_0 . add ( { start : 1 , end : 3 } ) ;
773+ const intervalId = interval_0 . getIntervalId ( ) ;
774+
775+ // Process client 0's add so both clients have the interval
776+ containerRuntimeFactory . processAllMessages ( ) ;
777+ await assertConsistent ( clients ) ;
778+
779+ // Client 1 disconnects and tries to modify the interval
780+ clients [ 1 ] . containerRuntime . connected = false ;
781+ collection_1 . change ( intervalId , { props : { modified : true } } ) ;
782+
783+ // Meanwhile, Client 0 deletes the interval
784+ collection_0 . removeIntervalById ( intervalId ) ;
785+ containerRuntimeFactory . processOneMessage ( ) ;
786+
787+ // Track events on client 1 to ensure interval is never undefined
788+ let eventWithUndefinedInterval = false ;
789+ const checkInterval = ( interval ) => {
790+ if ( interval === undefined ) {
791+ eventWithUndefinedInterval = true ;
792+ }
793+ } ;
794+
795+ collection_1 . on ( "deleteInterval" , ( interval ) => checkInterval ( interval ) ) ;
796+ collection_1 . on ( "propertyChanged" , ( interval ) => checkInterval ( interval ) ) ;
797+ collection_1 . on ( "changed" , ( interval ) => checkInterval ( interval ) ) ;
798+
799+ // Reconnect client 1 and process its change (which arrives after delete)
800+ clients [ 1 ] . containerRuntime . connected = true ;
801+ containerRuntimeFactory . processAllMessages ( ) ;
802+ await assertConsistent ( clients ) ;
803+
804+ // Verify no events were emitted with undefined interval
805+ assert . equal (
806+ eventWithUndefinedInterval ,
807+ false ,
808+ "no events should be emitted with undefined interval during concurrent modify/delete" ,
809+ ) ;
810+
811+ // Verify final state - interval should be deleted on both
812+ assert . equal ( collection_0 . getIntervalById ( intervalId ) , undefined ) ;
813+ assert . equal ( collection_1 . getIntervalById ( intervalId ) , undefined ) ;
814+ } ) ;
595815} ) ;
0 commit comments