github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/chat/sourceofflinable.go (about) 1 package chat 2 3 import ( 4 "sync" 5 "time" 6 7 "github.com/keybase/client/go/chat/globals" 8 "github.com/keybase/client/go/protocol/keybase1" 9 10 "github.com/keybase/client/go/chat/types" 11 "github.com/keybase/client/go/chat/utils" 12 "golang.org/x/net/context" 13 ) 14 15 // sourceOfflinable implements the chat/types.Offlinable interface. 16 // It is meant to be embedded in inbox and conversation sources. 17 // It's main purpose is that IsOffline() will wait for 4s to see if any 18 // in progress connections succeed before returning. 19 type sourceOfflinable struct { 20 globals.Contextified 21 utils.DebugLabeler 22 offline, delayed bool 23 connected chan bool 24 sync.Mutex 25 } 26 27 var _ types.Offlinable = (*sourceOfflinable)(nil) 28 29 func newSourceOfflinable(g *globals.Context, labeler utils.DebugLabeler) *sourceOfflinable { 30 return &sourceOfflinable{ 31 Contextified: globals.NewContextified(g), 32 DebugLabeler: labeler, 33 connected: makeConnectedChan(), 34 } 35 } 36 37 func (s *sourceOfflinable) Connected(ctx context.Context) { 38 defer s.Trace(ctx, nil, "Connected")() 39 s.Lock() 40 defer s.Unlock() 41 s.Debug(ctx, "connected: offline to false") 42 s.offline = false 43 s.connected <- true 44 } 45 46 func (s *sourceOfflinable) Disconnected(ctx context.Context) { 47 defer s.Trace(ctx, nil, "Disconnected")() 48 s.Lock() 49 defer s.Unlock() 50 if s.offline { 51 s.Debug(ctx, "already disconnected, ignoring disconnected callback") 52 return 53 } 54 s.Debug(ctx, "disconnected: offline to true") 55 s.offline = true 56 s.delayed = false 57 close(s.connected) 58 s.connected = makeConnectedChan() 59 } 60 61 func (s *sourceOfflinable) IsOffline(ctx context.Context) bool { 62 s.Lock() 63 offline := s.offline 64 connected := s.connected 65 delayed := s.delayed 66 s.Unlock() 67 68 if offline { 69 if delayed { 70 s.Debug(ctx, "IsOffline: offline, but skipping delay since we already did it") 71 return offline 72 } 73 if s.G().MobileAppState.State() != keybase1.MobileAppState_FOREGROUND { 74 s.Debug(ctx, "IsOffline: offline, but not waiting for anything since not in foreground") 75 return offline 76 } 77 timeoutCh := time.After(5 * time.Second) 78 for { 79 select { 80 case <-connected: 81 s.Debug(ctx, "IsOffline: waited and got %v", s.offline) 82 s.Lock() 83 if s.offline { 84 connected = s.connected 85 s.Unlock() 86 s.Debug(ctx, "IsOffline: since we got word of being offline, we will keep waiting") 87 continue 88 } 89 defer s.Unlock() 90 return s.offline 91 case <-ctx.Done(): 92 s.Lock() 93 defer s.Unlock() 94 s.Debug(ctx, "IsOffline: aborted: %s state: %v", ctx.Err(), s.offline) 95 return s.offline 96 case <-timeoutCh: 97 s.Lock() 98 defer s.Unlock() 99 select { 100 case <-ctx.Done(): 101 s.Debug(ctx, "IsOffline: timed out, but context canceled so not setting delayed: state: %v", 102 s.offline) 103 return s.offline 104 default: 105 } 106 s.delayed = true 107 s.Debug(ctx, "IsOffline: timed out, setting delay wait: state: %v", s.offline) 108 return s.offline 109 } 110 } 111 } 112 return offline 113 } 114 115 // makeConnectedChan creates a buffered channel for Connected to signal that 116 // a connection happened. The buffer size is 10 just to be extra-safe that 117 // a send on the channel won't block during its lifetime (a buffer size of 118 // 1 should be all that is required). 119 func makeConnectedChan() chan bool { 120 return make(chan bool, 10) 121 122 }