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

Configure Feed

Select the types of activity you want to include in your feed.

[๐Ÿด] Refactor event bus (#3919)

* Refactor to singleton class outside react

* Fix retry, remove debug logs

authored by

Eric Bailey and committed by
GitHub
ce2eddca 0c6bf276

+151 -257
+127 -194
src/state/messages/events/agent.ts
··· 3 3 import {nanoid} from 'nanoid/non-secure' 4 4 5 5 import {logger} from '#/logger' 6 + import {DEFAULT_POLL_INTERVAL} from '#/state/messages/events/const' 6 7 import { 7 8 MessagesEventBusDispatch, 8 9 MessagesEventBusDispatchEvent, 9 10 MessagesEventBusError, 10 11 MessagesEventBusErrorCode, 12 + MessagesEventBusEvents, 11 13 MessagesEventBusParams, 12 - MessagesEventBusState, 13 14 MessagesEventBusStatus, 14 15 } from '#/state/messages/events/types' 15 16 16 17 const LOGGER_CONTEXT = 'MessagesEventBus' 17 18 18 - const DEFAULT_POLL_INTERVAL = 60e3 19 - 20 19 export class MessagesEventBus { 21 20 private id: string 22 21 23 22 private agent: BskyAgent 24 23 private __tempFromUserDid: string 25 - private emitter = new EventEmitter() 24 + private emitter = new EventEmitter<MessagesEventBusEvents>() 26 25 27 - private status: MessagesEventBusStatus = MessagesEventBusStatus.Uninitialized 26 + private status: MessagesEventBusStatus = MessagesEventBusStatus.Initializing 28 27 private error: MessagesEventBusError | undefined 29 28 private latestRev: string | undefined = undefined 30 29 private pollInterval = DEFAULT_POLL_INTERVAL 31 30 private requestedPollIntervals: Map<string, number> = new Map() 32 - 33 - snapshot: MessagesEventBusState | undefined 34 31 35 32 constructor(params: MessagesEventBusParams) { 36 33 this.id = nanoid(3) 37 34 this.agent = params.agent 38 35 this.__tempFromUserDid = params.__tempFromUserDid 39 36 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.requestPollInterval = this.requestPollInterval.bind(this) 46 - this.trail = this.trail.bind(this) 47 - this.trailConvo = this.trailConvo.bind(this) 37 + this.init() 48 38 } 49 39 50 - private commit() { 51 - this.snapshot = undefined 52 - this.subscribers.forEach(subscriber => subscriber()) 40 + requestPollInterval(interval: number) { 41 + const id = nanoid() 42 + this.requestedPollIntervals.set(id, interval) 43 + this.dispatch({ 44 + event: MessagesEventBusDispatchEvent.UpdatePoll, 45 + }) 46 + return () => { 47 + this.requestedPollIntervals.delete(id) 48 + this.dispatch({ 49 + event: MessagesEventBusDispatchEvent.UpdatePoll, 50 + }) 51 + } 53 52 } 54 53 55 - private subscribers: (() => void)[] = [] 54 + trail(handler: (events: ChatBskyConvoGetLog.OutputSchema['logs']) => void) { 55 + this.emitter.on('events', handler) 56 + return () => { 57 + this.emitter.off('events', handler) 58 + } 59 + } 56 60 57 - subscribe(subscriber: () => void) { 58 - if (this.subscribers.length === 0) this.init() 61 + trailConvo( 62 + convoId: string, 63 + handler: (events: ChatBskyConvoGetLog.OutputSchema['logs']) => void, 64 + ) { 65 + const handle = (events: ChatBskyConvoGetLog.OutputSchema['logs']) => { 66 + const convoEvents = events.filter(ev => { 67 + if (typeof ev.convoId === 'string' && ev.convoId === convoId) { 68 + return ev.convoId === convoId 69 + } 70 + return false 71 + }) 59 72 60 - this.subscribers.push(subscriber) 73 + if (convoEvents.length > 0) { 74 + handler(convoEvents) 75 + } 76 + } 61 77 78 + this.emitter.on('events', handle) 62 79 return () => { 63 - this.subscribers = this.subscribers.filter(s => s !== subscriber) 64 - if (this.subscribers.length === 0) this.suspend() 80 + this.emitter.off('events', handle) 65 81 } 66 82 } 67 83 68 - getSnapshot(): MessagesEventBusState { 69 - if (!this.snapshot) this.snapshot = this.generateSnapshot() 70 - // logger.debug(`${LOGGER_CONTEXT}: snapshotted`, {}, logger.DebugContext.convo) 71 - return this.snapshot 84 + getLatestRev() { 85 + return this.latestRev 86 + } 87 + 88 + onConnect(handler: () => void) { 89 + this.emitter.on('connect', handler) 90 + 91 + if ( 92 + this.status === MessagesEventBusStatus.Ready || 93 + this.status === MessagesEventBusStatus.Backgrounded || 94 + this.status === MessagesEventBusStatus.Suspended 95 + ) { 96 + handler() 97 + } 98 + 99 + return () => { 100 + this.emitter.off('connect', handler) 101 + } 72 102 } 73 103 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 - requestPollInterval: this.requestPollInterval, 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 - requestPollInterval: this.requestPollInterval, 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 - requestPollInterval: this.requestPollInterval, 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 - requestPollInterval: this.requestPollInterval, 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 - requestPollInterval: this.requestPollInterval, 127 - trail: this.trail, 128 - trailConvo: this.trailConvo, 129 - } 130 - } 104 + onError(handler: (payload?: MessagesEventBusError) => void) { 105 + this.emitter.on('error', handler) 106 + 107 + if (this.status === MessagesEventBusStatus.Error) { 108 + handler(this.error) 109 + } 110 + 111 + return () => { 112 + this.emitter.off('error', handler) 131 113 } 132 114 } 133 115 134 - dispatch(action: MessagesEventBusDispatch) { 116 + background() { 117 + logger.debug(`${LOGGER_CONTEXT}: background`, {}, logger.DebugContext.convo) 118 + this.dispatch({event: MessagesEventBusDispatchEvent.Background}) 119 + } 120 + 121 + suspend() { 122 + logger.debug(`${LOGGER_CONTEXT}: suspend`, {}, logger.DebugContext.convo) 123 + this.dispatch({event: MessagesEventBusDispatchEvent.Suspend}) 124 + } 125 + 126 + resume() { 127 + logger.debug(`${LOGGER_CONTEXT}: resume`, {}, logger.DebugContext.convo) 128 + this.dispatch({event: MessagesEventBusDispatchEvent.Resume}) 129 + } 130 + 131 + private dispatch(action: MessagesEventBusDispatch) { 135 132 const prevStatus = this.status 136 133 137 134 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 135 case MessagesEventBusStatus.Initializing: { 149 136 switch (action.event) { 150 137 case MessagesEventBusDispatchEvent.Ready: { 151 138 this.status = MessagesEventBusStatus.Ready 152 139 this.resetPoll() 140 + this.emitter.emit('connect') 153 141 break 154 142 } 155 143 case MessagesEventBusDispatchEvent.Background: { 156 144 this.status = MessagesEventBusStatus.Backgrounded 157 145 this.resetPoll() 146 + this.emitter.emit('connect') 158 147 break 159 148 } 160 149 case MessagesEventBusDispatchEvent.Suspend: { ··· 164 153 case MessagesEventBusDispatchEvent.Error: { 165 154 this.status = MessagesEventBusStatus.Error 166 155 this.error = action.payload 156 + this.emitter.emit('error', action.payload) 167 157 break 168 158 } 169 159 } ··· 185 175 this.status = MessagesEventBusStatus.Error 186 176 this.error = action.payload 187 177 this.stopPoll() 178 + this.emitter.emit('error', action.payload) 179 + break 180 + } 181 + case MessagesEventBusDispatchEvent.UpdatePoll: { 182 + this.resetPoll() 188 183 break 189 184 } 190 185 } ··· 206 201 this.status = MessagesEventBusStatus.Error 207 202 this.error = action.payload 208 203 this.stopPoll() 204 + this.emitter.emit('error', action.payload) 205 + break 206 + } 207 + case MessagesEventBusDispatchEvent.UpdatePoll: { 208 + this.resetPoll() 209 209 break 210 210 } 211 211 } ··· 227 227 this.status = MessagesEventBusStatus.Error 228 228 this.error = action.payload 229 229 this.stopPoll() 230 + this.emitter.emit('error', action.payload) 230 231 break 231 232 } 232 233 } ··· 234 235 } 235 236 case MessagesEventBusStatus.Error: { 236 237 switch (action.event) { 237 - case MessagesEventBusDispatchEvent.Resume: 238 - case MessagesEventBusDispatchEvent.Init: { 238 + case MessagesEventBusDispatchEvent.Resume: { 239 + // basically reset 239 240 this.status = MessagesEventBusStatus.Initializing 240 241 this.error = undefined 241 242 this.latestRev = undefined 242 - this.setup() 243 + this.init() 243 244 break 244 245 } 245 246 } ··· 258 259 }, 259 260 logger.DebugContext.convo, 260 261 ) 261 - 262 - this.commit() 263 262 } 264 263 265 - private async setup() { 266 - logger.debug(`${LOGGER_CONTEXT}: setup`, {}, logger.DebugContext.convo) 264 + private async init() { 265 + logger.debug(`${LOGGER_CONTEXT}: init`, {}, logger.DebugContext.convo) 267 266 268 267 try { 269 - await this.initializeLatestRev() 268 + const response = await this.agent.api.chat.bsky.convo.listConvos( 269 + { 270 + limit: 1, 271 + }, 272 + { 273 + headers: { 274 + Authorization: this.__tempFromUserDid, 275 + }, 276 + }, 277 + ) 278 + // throw new Error('UNCOMMENT TO TEST INIT FAILURE') 279 + 280 + const {convos} = response.data 281 + 282 + for (const convo of convos) { 283 + if (convo.rev > (this.latestRev = this.latestRev || convo.rev)) { 284 + this.latestRev = convo.rev 285 + } 286 + } 287 + 270 288 this.dispatch({event: MessagesEventBusDispatchEvent.Ready}) 271 289 } catch (e: any) { 272 290 logger.error(e, { 273 - context: `${LOGGER_CONTEXT}: setup failed`, 291 + context: `${LOGGER_CONTEXT}: init failed`, 274 292 }) 275 293 276 294 this.dispatch({ ··· 279 297 exception: e, 280 298 code: MessagesEventBusErrorCode.InitFailed, 281 299 retry: () => { 282 - this.init() 300 + this.dispatch({event: MessagesEventBusDispatchEvent.Resume}) 283 301 }, 284 302 }, 285 303 }) 286 304 } 287 305 } 288 306 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 - requestPollInterval(interval: number) { 310 - const id = nanoid() 311 - this.requestedPollIntervals.set(id, interval) 312 - this.resetPoll() 313 - return () => { 314 - this.requestedPollIntervals.delete(id) 315 - this.resetPoll() 316 - } 317 - } 318 - 319 - trail(handler: (events: ChatBskyConvoGetLog.OutputSchema['logs']) => void) { 320 - this.emitter.on('events', handler) 321 - return () => { 322 - this.emitter.off('events', handler) 323 - } 324 - } 325 - 326 - trailConvo( 327 - convoId: string, 328 - handler: (events: ChatBskyConvoGetLog.OutputSchema['logs']) => void, 329 - ) { 330 - const handle = (events: ChatBskyConvoGetLog.OutputSchema['logs']) => { 331 - const convoEvents = events.filter(ev => { 332 - if (typeof ev.convoId === 'string' && ev.convoId === convoId) { 333 - return ev.convoId === convoId 334 - } 335 - return false 336 - }) 337 - 338 - if (convoEvents.length > 0) { 339 - handler(convoEvents) 340 - } 341 - } 342 - 343 - this.emitter.on('events', handle) 344 - return () => { 345 - this.emitter.off('events', handle) 346 - } 347 - } 348 - 349 - private async initializeLatestRev() { 350 - logger.debug( 351 - `${LOGGER_CONTEXT}: initialize latest rev`, 352 - {}, 353 - logger.DebugContext.convo, 354 - ) 355 - 356 - const response = await this.agent.api.chat.bsky.convo.listConvos( 357 - { 358 - limit: 1, 359 - }, 360 - { 361 - headers: { 362 - Authorization: this.__tempFromUserDid, 363 - }, 364 - }, 365 - ) 366 - 367 - const {convos} = response.data 368 - 369 - for (const convo of convos) { 370 - if (convo.rev > (this.latestRev = this.latestRev || convo.rev)) { 371 - this.latestRev = convo.rev 372 - } 373 - } 374 - } 375 - 376 307 /* 377 308 * Polling 378 309 */ ··· 430 361 }, 431 362 ) 432 363 364 + // throw new Error('UNCOMMENT TO TEST POLL FAILURE') 365 + 433 366 const {logs: events} = response.data 434 367 435 368 let needsEmit = false ··· 473 406 exception: e, 474 407 code: MessagesEventBusErrorCode.PollFailed, 475 408 retry: () => { 476 - this.init() 409 + this.dispatch({event: MessagesEventBusDispatchEvent.Resume}) 477 410 }, 478 411 }, 479 412 })
+1
src/state/messages/events/const.ts
··· 1 + export const DEFAULT_POLL_INTERVAL = 20e3
+10 -9
src/state/messages/events/index.tsx
··· 5 5 import {useGate} from '#/lib/statsig/statsig' 6 6 import {isWeb} from '#/platform/detection' 7 7 import {MessagesEventBus} from '#/state/messages/events/agent' 8 - import {MessagesEventBusState} from '#/state/messages/events/types' 9 8 import {useAgent} from '#/state/session' 10 9 import {useDmServiceUrlStorage} from '#/screens/Messages/Temp/useDmServiceUrlStorage' 11 10 import {IS_DEV} from '#/env' 12 11 13 - const MessagesEventBusContext = 14 - React.createContext<MessagesEventBusState | null>(null) 12 + const MessagesEventBusContext = React.createContext<MessagesEventBus | null>( 13 + null, 14 + ) 15 15 16 16 export function useMessagesEventBus() { 17 17 const ctx = React.useContext(MessagesEventBusContext) ··· 37 37 __tempFromUserDid: getAgent().session?.did!, 38 38 }), 39 39 ) 40 - const service = React.useSyncExternalStore(bus.subscribe, bus.getSnapshot) 41 40 42 - if (isWeb && IS_DEV) { 43 - // @ts-ignore 44 - window.messagesEventBus = service 45 - } 41 + React.useEffect(() => { 42 + if (isWeb && IS_DEV) { 43 + // @ts-ignore 44 + window.bus = bus 45 + } 46 + }, [bus]) 46 47 47 48 React.useEffect(() => { 48 49 const handleAppStateChange = (nextAppState: string) => { ··· 61 62 }, [bus]) 62 63 63 64 return ( 64 - <MessagesEventBusContext.Provider value={service}> 65 + <MessagesEventBusContext.Provider value={bus}> 65 66 {children} 66 67 </MessagesEventBusContext.Provider> 67 68 )
+13 -54
src/state/messages/events/types.ts
··· 6 6 } 7 7 8 8 export enum MessagesEventBusStatus { 9 - Uninitialized = 'uninitialized', 10 9 Initializing = 'initializing', 11 10 Ready = 'ready', 12 11 Error = 'error', ··· 15 14 } 16 15 17 16 export enum MessagesEventBusDispatchEvent { 18 - Init = 'init', 19 17 Ready = 'ready', 20 18 Error = 'error', 21 19 Background = 'background', 22 20 Suspend = 'suspend', 23 21 Resume = 'resume', 22 + UpdatePoll = 'updatePoll', 24 23 } 25 24 26 25 export enum MessagesEventBusErrorCode { ··· 37 36 38 37 export type MessagesEventBusDispatch = 39 38 | { 40 - event: MessagesEventBusDispatchEvent.Init 41 - } 42 - | { 43 39 event: MessagesEventBusDispatchEvent.Ready 44 40 } 45 41 | { ··· 54 50 | { 55 51 event: MessagesEventBusDispatchEvent.Error 56 52 payload: MessagesEventBusError 53 + } 54 + | { 55 + event: MessagesEventBusDispatchEvent.UpdatePoll 57 56 } 58 57 59 58 export type TrailHandler = ( ··· 61 60 ) => void 62 61 63 62 export type RequestPollIntervalHandler = (interval: number) => () => void 63 + export type OnConnectHandler = (handler: () => void) => () => void 64 + export type OnDisconnectHandler = ( 65 + handler: (error?: MessagesEventBusError) => void, 66 + ) => () => void 64 67 65 - export type MessagesEventBusState = 66 - | { 67 - status: MessagesEventBusStatus.Uninitialized 68 - rev: undefined 69 - error: undefined 70 - requestPollInterval: RequestPollIntervalHandler 71 - trail: (handler: TrailHandler) => () => void 72 - trailConvo: (convoId: string, handler: TrailHandler) => () => void 73 - } 74 - | { 75 - status: MessagesEventBusStatus.Initializing 76 - rev: undefined 77 - error: undefined 78 - requestPollInterval: RequestPollIntervalHandler 79 - trail: (handler: TrailHandler) => () => void 80 - trailConvo: (convoId: string, handler: TrailHandler) => () => void 81 - } 82 - | { 83 - status: MessagesEventBusStatus.Ready 84 - rev: string 85 - error: undefined 86 - requestPollInterval: RequestPollIntervalHandler 87 - trail: (handler: TrailHandler) => () => void 88 - trailConvo: (convoId: string, handler: TrailHandler) => () => void 89 - } 90 - | { 91 - status: MessagesEventBusStatus.Backgrounded 92 - rev: string | undefined 93 - error: undefined 94 - requestPollInterval: RequestPollIntervalHandler 95 - trail: (handler: TrailHandler) => () => void 96 - trailConvo: (convoId: string, handler: TrailHandler) => () => void 97 - } 98 - | { 99 - status: MessagesEventBusStatus.Suspended 100 - rev: string | undefined 101 - error: undefined 102 - requestPollInterval: RequestPollIntervalHandler 103 - trail: (handler: TrailHandler) => () => void 104 - trailConvo: (convoId: string, handler: TrailHandler) => () => void 105 - } 106 - | { 107 - status: MessagesEventBusStatus.Error 108 - rev: string | undefined 109 - error: MessagesEventBusError 110 - requestPollInterval: RequestPollIntervalHandler 111 - trail: (handler: TrailHandler) => () => void 112 - trailConvo: (convoId: string, handler: TrailHandler) => () => void 113 - } 68 + export type MessagesEventBusEvents = { 69 + events: [ChatBskyConvoGetLog.OutputSchema['logs']] 70 + connect: undefined 71 + error: [MessagesEventBusError] | undefined 72 + }