@@ -129,6 +129,12 @@ export interface MembershipManagerState {
129
129
rateLimitRetries : Map < MembershipActionType , number > ;
130
130
/** Retry counter for other errors */
131
131
networkErrorRetries : Map < MembershipActionType , number > ;
132
+ /** The time at which we expect the server to send the delayed leave event. */
133
+ expectedServerDelayLeaveTs ?: number ;
134
+ /** This is used to track if the client expects the scheduled delayed leave event to have
135
+ * been sent because restarting failed during the available time.
136
+ * Once we resend the delayed event or successfully restarted it will get unset. */
137
+ probablyLeft : boolean ;
132
138
}
133
139
134
140
/**
@@ -343,6 +349,7 @@ export class MembershipManager
343
349
rateLimitRetries : new Map ( ) ,
344
350
networkErrorRetries : new Map ( ) ,
345
351
expireUpdateIterations : 1 ,
352
+ probablyLeft : false ,
346
353
} ;
347
354
}
348
355
// Membership Event static parameters:
@@ -466,6 +473,8 @@ export class MembershipManager
466
473
this . stateKey ,
467
474
)
468
475
. then ( ( response ) => {
476
+ this . state . expectedServerDelayLeaveTs = Date . now ( ) + this . delayedLeaveEventDelayMs ;
477
+ this . setAndEmitProbablyLeft ( false ) ;
469
478
// On success we reset retries and set delayId.
470
479
this . resetRateLimitCounter ( MembershipActionType . SendDelayedEvent ) ;
471
480
this . state . delayId = response . delay_id ;
@@ -545,27 +554,58 @@ export class MembershipManager
545
554
} ) ;
546
555
}
547
556
557
+ private setAndEmitProbablyLeft ( probablyLeft : boolean ) : void {
558
+ if ( this . state . probablyLeft === probablyLeft ) {
559
+ return ;
560
+ }
561
+ this . state . probablyLeft = probablyLeft ;
562
+ this . emit ( MembershipManagerEvent . ProbablyLeft , this . state . probablyLeft ) ;
563
+ }
564
+
548
565
private async restartDelayedEvent ( delayId : string ) : Promise < ActionUpdate > {
566
+ // Compute the duration until we expect the server to send the delayed leave event.
567
+ const durationUntilServerDelayedLeave = this . state . expectedServerDelayLeaveTs
568
+ ? this . state . expectedServerDelayLeaveTs - Date . now ( )
569
+ : undefined ;
549
570
const abortPromise = new Promise ( ( _ , reject ) => {
550
- setTimeout ( ( ) => {
551
- reject ( new AbortError ( "Restart delayed event timed out before the HS responded" ) ) ;
552
- } , this . delayedLeaveEventRestartLocalTimeoutMs ) ;
571
+ setTimeout (
572
+ ( ) => {
573
+ reject ( new AbortError ( "Restart delayed event timed out before the HS responded" ) ) ;
574
+ } ,
575
+ // We abort immediately at the time where we expect the server to send the delayed leave event.
576
+ // At this point we want the catch block to run and set the `probablyLeft` state.
577
+ //
578
+ // While we are already in probablyLeft state, we use the unaltered delayedLeaveEventRestartLocalTimeoutMs.
579
+ durationUntilServerDelayedLeave !== undefined && ! this . state . probablyLeft
580
+ ? Math . min ( this . delayedLeaveEventRestartLocalTimeoutMs , durationUntilServerDelayedLeave )
581
+ : this . delayedLeaveEventRestartLocalTimeoutMs ,
582
+ ) ;
553
583
} ) ;
554
584
555
585
// The obvious choice here would be to use the `IRequestOpts` to set the timeout. Since this call might be forwarded
556
- // to the widget driver this information would ge lost. That is why we mimic the AbortError using the race.
586
+ // to the widget driver this information would get lost. That is why we mimic the AbortError using the race.
557
587
return await Promise . race ( [
558
588
this . client . _unstable_updateDelayedEvent ( delayId , UpdateDelayedEventAction . Restart ) ,
559
589
abortPromise ,
560
590
] )
561
591
. then ( ( ) => {
592
+ // Whenever we successfully restart the delayed event we update the `state.expectedServerDelayLeaveTs`
593
+ // which stores the predicted timestamp at which the server will send the delayed leave event if there wont be any further
594
+ // successful restart requests.
595
+ this . state . expectedServerDelayLeaveTs = Date . now ( ) + this . delayedLeaveEventDelayMs ;
562
596
this . resetRateLimitCounter ( MembershipActionType . RestartDelayedEvent ) ;
597
+ this . setAndEmitProbablyLeft ( false ) ;
563
598
return createInsertActionUpdate (
564
599
MembershipActionType . RestartDelayedEvent ,
565
600
this . delayedLeaveEventRestartMs ,
566
601
) ;
567
602
} )
568
603
. catch ( ( e ) => {
604
+ if ( this . state . expectedServerDelayLeaveTs && this . state . expectedServerDelayLeaveTs <= Date . now ( ) ) {
605
+ // Once we reach this point it's likely that the server is sending the delayed leave event so we emit `probablyLeft = true`.
606
+ // It will emit `probablyLeft = false` once we notice about our leave through sync and successfully setup a new state event.
607
+ this . setAndEmitProbablyLeft ( true ) ;
608
+ }
569
609
const repeatActionType = MembershipActionType . RestartDelayedEvent ;
570
610
if ( this . isNotFoundError ( e ) ) {
571
611
this . state . delayId = undefined ;
@@ -620,6 +660,7 @@ export class MembershipManager
620
660
this . stateKey ,
621
661
)
622
662
. then ( ( ) => {
663
+ this . setAndEmitProbablyLeft ( false ) ;
623
664
this . state . startTime = Date . now ( ) ;
624
665
// The next update should already use twice the membershipEventExpiryTimeout
625
666
this . state . expireUpdateIterations = 1 ;
0 commit comments