Bluesky app fork with some witchin' additions ๐Ÿ’ซ

[๐Ÿด] Global event mgmt (#3897)

* Add global event bus for messages logs

* Add rev to state

* Better handle error

* Clean up polling, add backgrounding

* Add trailConvo method

* Extend polling until we're ready for this

authored by

Eric Bailey and committed by
GitHub
87cb4c10 0625a914

+686 -41
+22 -19
src/App.native.tsx
··· 16 16 17 17 import {Provider as StatsigProvider} from '#/lib/statsig/statsig' 18 18 import {logger} from '#/logger' 19 + import {MessagesEventBusProvider} from '#/state/messages/events' 19 20 import {init as initPersistedState} from '#/state/persisted' 20 21 import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs' 21 22 import {Provider as ModerationOptsProvider} from '#/state/preferences/moderation-opts' ··· 95 96 // Resets the entire tree below when it changes: 96 97 key={currentAccount?.did}> 97 98 <QueryProvider currentDid={currentAccount?.did}> 98 - <PushNotificationsListener> 99 - <StatsigProvider> 100 - {/* LabelDefsProvider MUST come before ModerationOptsProvider */} 101 - <LabelDefsProvider> 102 - <ModerationOptsProvider> 103 - <LoggedOutViewProvider> 104 - <SelectedFeedProvider> 105 - <UnreadNotifsProvider> 106 - <GestureHandlerRootView style={s.h100pct}> 107 - <TestCtrls /> 108 - <Shell /> 109 - </GestureHandlerRootView> 110 - </UnreadNotifsProvider> 111 - </SelectedFeedProvider> 112 - </LoggedOutViewProvider> 113 - </ModerationOptsProvider> 114 - </LabelDefsProvider> 115 - </StatsigProvider> 116 - </PushNotificationsListener> 99 + <MessagesEventBusProvider> 100 + <PushNotificationsListener> 101 + <StatsigProvider> 102 + {/* LabelDefsProvider MUST come before ModerationOptsProvider */} 103 + <LabelDefsProvider> 104 + <ModerationOptsProvider> 105 + <LoggedOutViewProvider> 106 + <SelectedFeedProvider> 107 + <UnreadNotifsProvider> 108 + <GestureHandlerRootView style={s.h100pct}> 109 + <TestCtrls /> 110 + <Shell /> 111 + </GestureHandlerRootView> 112 + </UnreadNotifsProvider> 113 + </SelectedFeedProvider> 114 + </LoggedOutViewProvider> 115 + </ModerationOptsProvider> 116 + </LabelDefsProvider> 117 + </StatsigProvider> 118 + </PushNotificationsListener> 119 + </MessagesEventBusProvider> 117 120 </QueryProvider> 118 121 </React.Fragment> 119 122 </RootSiblingParent>
+20 -22
src/App.web.tsx
··· 9 9 10 10 import {Provider as StatsigProvider} from '#/lib/statsig/statsig' 11 11 import {logger} from '#/logger' 12 + import {MessagesEventBusProvider} from '#/state/messages/events' 12 13 import {init as initPersistedState} from '#/state/persisted' 13 14 import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs' 14 15 import {Provider as ModerationOptsProvider} from '#/state/preferences/moderation-opts' ··· 83 84 // Resets the entire tree below when it changes: 84 85 key={currentAccount?.did}> 85 86 <QueryProvider currentDid={currentAccount?.did}> 86 - <StatsigProvider> 87 - {/* LabelDefsProvider MUST come before ModerationOptsProvider */} 88 - <LabelDefsProvider> 89 - <ModerationOptsProvider> 90 - <LoggedOutViewProvider> 91 - <SelectedFeedProvider> 92 - <UnreadNotifsProvider> 93 - <SafeAreaProvider> 94 - <Shell /> 95 - </SafeAreaProvider> 96 - </UnreadNotifsProvider> 97 - </SelectedFeedProvider> 98 - </LoggedOutViewProvider> 99 - </ModerationOptsProvider> 100 - </LabelDefsProvider> 101 - </StatsigProvider> 87 + <MessagesEventBusProvider> 88 + <StatsigProvider> 89 + {/* LabelDefsProvider MUST come before ModerationOptsProvider */} 90 + <LabelDefsProvider> 91 + <ModerationOptsProvider> 92 + <LoggedOutViewProvider> 93 + <SelectedFeedProvider> 94 + <UnreadNotifsProvider> 95 + <SafeAreaProvider> 96 + <Shell /> 97 + </SafeAreaProvider> 98 + </UnreadNotifsProvider> 99 + </SelectedFeedProvider> 100 + </LoggedOutViewProvider> 101 + </ModerationOptsProvider> 102 + </LabelDefsProvider> 103 + </StatsigProvider> 104 + </MessagesEventBusProvider> 102 105 </QueryProvider> 103 106 </React.Fragment> 104 107 <ToastContainer /> ··· 112 115 const [isReady, setReady] = useState(false) 113 116 114 117 React.useEffect(() => { 115 - initPersistedState().then(() => { 116 - setReady(true) 117 - 118 - const preloadElement = document.getElementById('preload') 119 - preloadElement?.remove() 120 - }) 118 + initPersistedState().then(() => setReady(true)) 121 119 }, []) 122 120 123 121 if (!isReady) {
+466
src/state/messages/events/agent.ts
··· 1 + import {BskyAgent, ChatBskyConvoGetLog} from '@atproto-labs/api' 2 + import EventEmitter from 'eventemitter3' 3 + import {nanoid} from 'nanoid/non-secure' 4 + 5 + import {logger} from '#/logger' 6 + import { 7 + MessagesEventBusDispatch, 8 + MessagesEventBusDispatchEvent, 9 + MessagesEventBusError, 10 + MessagesEventBusErrorCode, 11 + MessagesEventBusParams, 12 + MessagesEventBusState, 13 + MessagesEventBusStatus, 14 + } from '#/state/messages/events/types' 15 + 16 + const LOGGER_CONTEXT = 'MessagesEventBus' 17 + 18 + const ACTIVE_POLL_INTERVAL = 60e3 19 + const BACKGROUND_POLL_INTERVAL = 60e3 20 + 21 + export class MessagesEventBus { 22 + private id: string 23 + 24 + private agent: BskyAgent 25 + private __tempFromUserDid: string 26 + private emitter = new EventEmitter() 27 + 28 + private status: MessagesEventBusStatus = MessagesEventBusStatus.Uninitialized 29 + private pollInterval = ACTIVE_POLL_INTERVAL 30 + private error: MessagesEventBusError | undefined 31 + private latestRev: string | undefined = undefined 32 + 33 + snapshot: MessagesEventBusState | undefined 34 + 35 + constructor(params: MessagesEventBusParams) { 36 + this.id = nanoid(3) 37 + this.agent = params.agent 38 + this.__tempFromUserDid = params.__tempFromUserDid 39 + 40 + this.subscribe = this.subscribe.bind(this) 41 + this.getSnapshot = this.getSnapshot.bind(this) 42 + this.init = this.init.bind(this) 43 + this.suspend = this.suspend.bind(this) 44 + this.resume = this.resume.bind(this) 45 + this.setPollInterval = this.setPollInterval.bind(this) 46 + this.trail = this.trail.bind(this) 47 + this.trailConvo = this.trailConvo.bind(this) 48 + } 49 + 50 + private commit() { 51 + this.snapshot = undefined 52 + this.subscribers.forEach(subscriber => subscriber()) 53 + } 54 + 55 + private subscribers: (() => void)[] = [] 56 + 57 + subscribe(subscriber: () => void) { 58 + if (this.subscribers.length === 0) this.init() 59 + 60 + this.subscribers.push(subscriber) 61 + 62 + return () => { 63 + this.subscribers = this.subscribers.filter(s => s !== subscriber) 64 + if (this.subscribers.length === 0) this.suspend() 65 + } 66 + } 67 + 68 + getSnapshot(): MessagesEventBusState { 69 + if (!this.snapshot) this.snapshot = this.generateSnapshot() 70 + // logger.debug(`${LOGGER_CONTEXT}: snapshotted`, {}, logger.DebugContext.convo) 71 + return this.snapshot 72 + } 73 + 74 + private generateSnapshot(): MessagesEventBusState { 75 + switch (this.status) { 76 + case MessagesEventBusStatus.Initializing: { 77 + return { 78 + status: MessagesEventBusStatus.Initializing, 79 + rev: undefined, 80 + error: undefined, 81 + setPollInterval: this.setPollInterval, 82 + trail: this.trail, 83 + trailConvo: this.trailConvo, 84 + } 85 + } 86 + case MessagesEventBusStatus.Ready: { 87 + return { 88 + status: this.status, 89 + rev: this.latestRev!, 90 + error: undefined, 91 + setPollInterval: this.setPollInterval, 92 + trail: this.trail, 93 + trailConvo: this.trailConvo, 94 + } 95 + } 96 + case MessagesEventBusStatus.Suspended: { 97 + return { 98 + status: this.status, 99 + rev: this.latestRev, 100 + error: undefined, 101 + setPollInterval: this.setPollInterval, 102 + trail: this.trail, 103 + trailConvo: this.trailConvo, 104 + } 105 + } 106 + case MessagesEventBusStatus.Error: { 107 + return { 108 + status: MessagesEventBusStatus.Error, 109 + rev: this.latestRev, 110 + error: this.error || { 111 + code: MessagesEventBusErrorCode.Unknown, 112 + retry: () => { 113 + this.init() 114 + }, 115 + }, 116 + setPollInterval: this.setPollInterval, 117 + trail: this.trail, 118 + trailConvo: this.trailConvo, 119 + } 120 + } 121 + default: { 122 + return { 123 + status: MessagesEventBusStatus.Uninitialized, 124 + rev: undefined, 125 + error: undefined, 126 + setPollInterval: this.setPollInterval, 127 + trail: this.trail, 128 + trailConvo: this.trailConvo, 129 + } 130 + } 131 + } 132 + } 133 + 134 + dispatch(action: MessagesEventBusDispatch) { 135 + const prevStatus = this.status 136 + 137 + switch (this.status) { 138 + case MessagesEventBusStatus.Uninitialized: { 139 + switch (action.event) { 140 + case MessagesEventBusDispatchEvent.Init: { 141 + this.status = MessagesEventBusStatus.Initializing 142 + this.setup() 143 + break 144 + } 145 + } 146 + break 147 + } 148 + case MessagesEventBusStatus.Initializing: { 149 + switch (action.event) { 150 + case MessagesEventBusDispatchEvent.Ready: { 151 + this.status = MessagesEventBusStatus.Ready 152 + this.setPollInterval(ACTIVE_POLL_INTERVAL) 153 + break 154 + } 155 + case MessagesEventBusDispatchEvent.Background: { 156 + this.status = MessagesEventBusStatus.Backgrounded 157 + this.setPollInterval(BACKGROUND_POLL_INTERVAL) 158 + break 159 + } 160 + case MessagesEventBusDispatchEvent.Suspend: { 161 + this.status = MessagesEventBusStatus.Suspended 162 + break 163 + } 164 + case MessagesEventBusDispatchEvent.Error: { 165 + this.status = MessagesEventBusStatus.Error 166 + this.error = action.payload 167 + break 168 + } 169 + } 170 + break 171 + } 172 + case MessagesEventBusStatus.Ready: { 173 + switch (action.event) { 174 + case MessagesEventBusDispatchEvent.Background: { 175 + this.status = MessagesEventBusStatus.Backgrounded 176 + this.setPollInterval(BACKGROUND_POLL_INTERVAL) 177 + break 178 + } 179 + case MessagesEventBusDispatchEvent.Suspend: { 180 + this.status = MessagesEventBusStatus.Suspended 181 + this.stopPoll() 182 + break 183 + } 184 + case MessagesEventBusDispatchEvent.Error: { 185 + this.status = MessagesEventBusStatus.Error 186 + this.error = action.payload 187 + this.stopPoll() 188 + break 189 + } 190 + } 191 + break 192 + } 193 + case MessagesEventBusStatus.Backgrounded: { 194 + switch (action.event) { 195 + case MessagesEventBusDispatchEvent.Resume: { 196 + this.status = MessagesEventBusStatus.Ready 197 + this.setPollInterval(ACTIVE_POLL_INTERVAL) 198 + break 199 + } 200 + case MessagesEventBusDispatchEvent.Suspend: { 201 + this.status = MessagesEventBusStatus.Suspended 202 + this.stopPoll() 203 + break 204 + } 205 + case MessagesEventBusDispatchEvent.Error: { 206 + this.status = MessagesEventBusStatus.Error 207 + this.error = action.payload 208 + this.stopPoll() 209 + break 210 + } 211 + } 212 + break 213 + } 214 + case MessagesEventBusStatus.Suspended: { 215 + switch (action.event) { 216 + case MessagesEventBusDispatchEvent.Resume: { 217 + this.status = MessagesEventBusStatus.Ready 218 + this.setPollInterval(ACTIVE_POLL_INTERVAL) 219 + break 220 + } 221 + case MessagesEventBusDispatchEvent.Background: { 222 + this.status = MessagesEventBusStatus.Backgrounded 223 + this.setPollInterval(BACKGROUND_POLL_INTERVAL) 224 + break 225 + } 226 + case MessagesEventBusDispatchEvent.Error: { 227 + this.status = MessagesEventBusStatus.Error 228 + this.error = action.payload 229 + this.stopPoll() 230 + break 231 + } 232 + } 233 + break 234 + } 235 + case MessagesEventBusStatus.Error: { 236 + switch (action.event) { 237 + case MessagesEventBusDispatchEvent.Resume: 238 + case MessagesEventBusDispatchEvent.Init: { 239 + this.status = MessagesEventBusStatus.Initializing 240 + this.error = undefined 241 + this.latestRev = undefined 242 + this.setup() 243 + break 244 + } 245 + } 246 + break 247 + } 248 + default: 249 + break 250 + } 251 + 252 + logger.debug( 253 + `${LOGGER_CONTEXT}: dispatch '${action.event}'`, 254 + { 255 + id: this.id, 256 + prev: prevStatus, 257 + next: this.status, 258 + }, 259 + logger.DebugContext.convo, 260 + ) 261 + 262 + this.commit() 263 + } 264 + 265 + private async setup() { 266 + logger.debug(`${LOGGER_CONTEXT}: setup`, {}, logger.DebugContext.convo) 267 + 268 + try { 269 + await this.initializeLatestRev() 270 + this.dispatch({event: MessagesEventBusDispatchEvent.Ready}) 271 + } catch (e: any) { 272 + logger.error(e, { 273 + context: `${LOGGER_CONTEXT}: setup failed`, 274 + }) 275 + 276 + this.dispatch({ 277 + event: MessagesEventBusDispatchEvent.Error, 278 + payload: { 279 + exception: e, 280 + code: MessagesEventBusErrorCode.InitFailed, 281 + retry: () => { 282 + this.init() 283 + }, 284 + }, 285 + }) 286 + } 287 + } 288 + 289 + init() { 290 + logger.debug(`${LOGGER_CONTEXT}: init`, {}, logger.DebugContext.convo) 291 + this.dispatch({event: MessagesEventBusDispatchEvent.Init}) 292 + } 293 + 294 + background() { 295 + logger.debug(`${LOGGER_CONTEXT}: background`, {}, logger.DebugContext.convo) 296 + this.dispatch({event: MessagesEventBusDispatchEvent.Background}) 297 + } 298 + 299 + suspend() { 300 + logger.debug(`${LOGGER_CONTEXT}: suspend`, {}, logger.DebugContext.convo) 301 + this.dispatch({event: MessagesEventBusDispatchEvent.Suspend}) 302 + } 303 + 304 + resume() { 305 + logger.debug(`${LOGGER_CONTEXT}: resume`, {}, logger.DebugContext.convo) 306 + this.dispatch({event: MessagesEventBusDispatchEvent.Resume}) 307 + } 308 + 309 + setPollInterval(interval: number) { 310 + this.pollInterval = interval 311 + this.resetPoll() 312 + } 313 + 314 + trail(handler: (events: ChatBskyConvoGetLog.OutputSchema['logs']) => void) { 315 + this.emitter.on('events', handler) 316 + return () => { 317 + this.emitter.off('events', handler) 318 + } 319 + } 320 + 321 + trailConvo( 322 + convoId: string, 323 + handler: (events: ChatBskyConvoGetLog.OutputSchema['logs']) => void, 324 + ) { 325 + const handle = (events: ChatBskyConvoGetLog.OutputSchema['logs']) => { 326 + const convoEvents = events.filter(ev => { 327 + if (typeof ev.convoId === 'string' && ev.convoId === convoId) { 328 + return ev.convoId === convoId 329 + } 330 + return false 331 + }) 332 + 333 + if (convoEvents.length > 0) { 334 + handler(convoEvents) 335 + } 336 + } 337 + 338 + this.emitter.on('events', handle) 339 + return () => { 340 + this.emitter.off('events', handle) 341 + } 342 + } 343 + 344 + private async initializeLatestRev() { 345 + logger.debug( 346 + `${LOGGER_CONTEXT}: initialize latest rev`, 347 + {}, 348 + logger.DebugContext.convo, 349 + ) 350 + 351 + const response = await this.agent.api.chat.bsky.convo.listConvos( 352 + { 353 + limit: 1, 354 + }, 355 + { 356 + headers: { 357 + Authorization: this.__tempFromUserDid, 358 + }, 359 + }, 360 + ) 361 + 362 + const {convos} = response.data 363 + 364 + for (const convo of convos) { 365 + if (convo.rev > (this.latestRev = this.latestRev || convo.rev)) { 366 + this.latestRev = convo.rev 367 + } 368 + } 369 + } 370 + 371 + /* 372 + * Polling 373 + */ 374 + 375 + private isPolling = false 376 + private pollIntervalRef: NodeJS.Timeout | undefined 377 + 378 + private resetPoll() { 379 + this.stopPoll() 380 + this.startPoll() 381 + } 382 + 383 + private startPoll() { 384 + if (!this.isPolling) this.poll() 385 + 386 + this.pollIntervalRef = setInterval(() => { 387 + if (this.isPolling) return 388 + this.poll() 389 + }, this.pollInterval) 390 + } 391 + 392 + private stopPoll() { 393 + if (this.pollIntervalRef) clearInterval(this.pollIntervalRef) 394 + } 395 + 396 + private async poll() { 397 + if (this.isPolling) return 398 + 399 + this.isPolling = true 400 + 401 + logger.debug(`${LOGGER_CONTEXT}: poll`, {}, logger.DebugContext.convo) 402 + 403 + try { 404 + const response = await this.agent.api.chat.bsky.convo.getLog( 405 + { 406 + cursor: this.latestRev, 407 + }, 408 + { 409 + headers: { 410 + Authorization: this.__tempFromUserDid, 411 + }, 412 + }, 413 + ) 414 + 415 + const {logs: events} = response.data 416 + 417 + let needsEmit = false 418 + let batch: ChatBskyConvoGetLog.OutputSchema['logs'] = [] 419 + 420 + for (const ev of events) { 421 + /* 422 + * If there's a rev, we should handle it. If there's not a rev, we don't 423 + * know what it is. 424 + */ 425 + if (typeof ev.rev === 'string') { 426 + /* 427 + * We only care about new events 428 + */ 429 + if (ev.rev > (this.latestRev = this.latestRev || ev.rev)) { 430 + /* 431 + * Update rev regardless of if it's a ev type we care about or not 432 + */ 433 + this.latestRev = ev.rev 434 + needsEmit = true 435 + batch.push(ev) 436 + } 437 + } 438 + } 439 + 440 + if (needsEmit) { 441 + try { 442 + this.emitter.emit('events', batch) 443 + } catch (e: any) { 444 + logger.error(e, { 445 + context: `${LOGGER_CONTEXT}: process latest events`, 446 + }) 447 + } 448 + } 449 + } catch (e: any) { 450 + logger.error(e, {context: `${LOGGER_CONTEXT}: poll events failed`}) 451 + 452 + this.dispatch({ 453 + event: MessagesEventBusDispatchEvent.Error, 454 + payload: { 455 + exception: e, 456 + code: MessagesEventBusErrorCode.PollFailed, 457 + retry: () => { 458 + this.init() 459 + }, 460 + }, 461 + }) 462 + } finally { 463 + this.isPolling = false 464 + } 465 + } 466 + }
+67
src/state/messages/events/index.tsx
··· 1 + import React from 'react' 2 + import {AppState} from 'react-native' 3 + import {BskyAgent} from '@atproto-labs/api' 4 + 5 + import {isWeb} from '#/platform/detection' 6 + import {MessagesEventBus} from '#/state/messages/events/agent' 7 + import {MessagesEventBusState} from '#/state/messages/events/types' 8 + import {useAgent} from '#/state/session' 9 + import {useDmServiceUrlStorage} from '#/screens/Messages/Temp/useDmServiceUrlStorage' 10 + import {IS_DEV} from '#/env' 11 + 12 + const MessagesEventBusContext = 13 + React.createContext<MessagesEventBusState | null>(null) 14 + 15 + export function useMessagesEventBus() { 16 + const ctx = React.useContext(MessagesEventBusContext) 17 + if (!ctx) { 18 + throw new Error('useChat must be used within a ChatProvider') 19 + } 20 + return ctx 21 + } 22 + 23 + export function MessagesEventBusProvider({ 24 + children, 25 + }: { 26 + children: React.ReactNode 27 + }) { 28 + const {serviceUrl} = useDmServiceUrlStorage() 29 + const {getAgent} = useAgent() 30 + const [bus] = React.useState( 31 + () => 32 + new MessagesEventBus({ 33 + agent: new BskyAgent({ 34 + service: serviceUrl, 35 + }), 36 + __tempFromUserDid: getAgent().session?.did!, 37 + }), 38 + ) 39 + const service = React.useSyncExternalStore(bus.subscribe, bus.getSnapshot) 40 + 41 + if (isWeb && IS_DEV) { 42 + // @ts-ignore 43 + window.messagesEventBus = service 44 + } 45 + 46 + React.useEffect(() => { 47 + const handleAppStateChange = (nextAppState: string) => { 48 + if (nextAppState === 'active') { 49 + bus.resume() 50 + } else { 51 + bus.background() 52 + } 53 + } 54 + 55 + const sub = AppState.addEventListener('change', handleAppStateChange) 56 + 57 + return () => { 58 + sub.remove() 59 + } 60 + }, [bus]) 61 + 62 + return ( 63 + <MessagesEventBusContext.Provider value={service}> 64 + {children} 65 + </MessagesEventBusContext.Provider> 66 + ) 67 + }
+111
src/state/messages/events/types.ts
··· 1 + import {BskyAgent, ChatBskyConvoGetLog} from '@atproto-labs/api' 2 + 3 + export type MessagesEventBusParams = { 4 + agent: BskyAgent 5 + __tempFromUserDid: string 6 + } 7 + 8 + export enum MessagesEventBusStatus { 9 + Uninitialized = 'uninitialized', 10 + Initializing = 'initializing', 11 + Ready = 'ready', 12 + Error = 'error', 13 + Backgrounded = 'backgrounded', 14 + Suspended = 'suspended', 15 + } 16 + 17 + export enum MessagesEventBusDispatchEvent { 18 + Init = 'init', 19 + Ready = 'ready', 20 + Error = 'error', 21 + Background = 'background', 22 + Suspend = 'suspend', 23 + Resume = 'resume', 24 + } 25 + 26 + export enum MessagesEventBusErrorCode { 27 + Unknown = 'unknown', 28 + InitFailed = 'initFailed', 29 + PollFailed = 'pollFailed', 30 + } 31 + 32 + export type MessagesEventBusError = { 33 + code: MessagesEventBusErrorCode 34 + exception?: Error 35 + retry: () => void 36 + } 37 + 38 + export type MessagesEventBusDispatch = 39 + | { 40 + event: MessagesEventBusDispatchEvent.Init 41 + } 42 + | { 43 + event: MessagesEventBusDispatchEvent.Ready 44 + } 45 + | { 46 + event: MessagesEventBusDispatchEvent.Background 47 + } 48 + | { 49 + event: MessagesEventBusDispatchEvent.Suspend 50 + } 51 + | { 52 + event: MessagesEventBusDispatchEvent.Resume 53 + } 54 + | { 55 + event: MessagesEventBusDispatchEvent.Error 56 + payload: MessagesEventBusError 57 + } 58 + 59 + export type TrailHandler = ( 60 + events: ChatBskyConvoGetLog.OutputSchema['logs'], 61 + ) => void 62 + 63 + export type MessagesEventBusState = 64 + | { 65 + status: MessagesEventBusStatus.Uninitialized 66 + rev: undefined 67 + error: undefined 68 + setPollInterval: (interval: number) => void 69 + trail: (handler: TrailHandler) => () => void 70 + trailConvo: (convoId: string, handler: TrailHandler) => () => void 71 + } 72 + | { 73 + status: MessagesEventBusStatus.Initializing 74 + rev: undefined 75 + error: undefined 76 + setPollInterval: (interval: number) => void 77 + trail: (handler: TrailHandler) => () => void 78 + trailConvo: (convoId: string, handler: TrailHandler) => () => void 79 + } 80 + | { 81 + status: MessagesEventBusStatus.Ready 82 + rev: string 83 + error: undefined 84 + setPollInterval: (interval: number) => void 85 + trail: (handler: TrailHandler) => () => void 86 + trailConvo: (convoId: string, handler: TrailHandler) => () => void 87 + } 88 + | { 89 + status: MessagesEventBusStatus.Backgrounded 90 + rev: string | undefined 91 + error: undefined 92 + setPollInterval: (interval: number) => void 93 + trail: (handler: TrailHandler) => () => void 94 + trailConvo: (convoId: string, handler: TrailHandler) => () => void 95 + } 96 + | { 97 + status: MessagesEventBusStatus.Suspended 98 + rev: string | undefined 99 + error: undefined 100 + setPollInterval: (interval: number) => void 101 + trail: (handler: TrailHandler) => () => void 102 + trailConvo: (convoId: string, handler: TrailHandler) => () => void 103 + } 104 + | { 105 + status: MessagesEventBusStatus.Error 106 + rev: string | undefined 107 + error: MessagesEventBusError 108 + setPollInterval: (interval: number) => void 109 + trail: (handler: TrailHandler) => () => void 110 + trailConvo: (convoId: string, handler: TrailHandler) => () => void 111 + }