Gracefully handle network errors and recovery

This commit is contained in:
Eric Bailey
2024-05-16 12:18:14 -05:00
parent b3c76a7d7f
commit 3334b2f201
4 changed files with 97 additions and 39 deletions
@@ -0,0 +1,54 @@
import React from 'react'
import {View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {ConvoItem} from '#/state/messages/convo/types'
import {atoms as a, useTheme} from '#/alf'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography'
export function MessageListFirehoseError({
item,
}: {
item: ConvoItem & {type: 'firehose-error'}
}) {
const t = useTheme()
const {_} = useLingui()
return (
<View style={[a.py_lg, a.align_center]}>
<View
style={[
a.flex_row,
a.align_center,
a.justify_between,
a.gap_sm,
{maxWidth: 400},
]}>
<CircleInfo
size="sm"
fill={t.palette.negative_400}
style={[{top: 3}]}
/>
<Text style={[a.leading_snug, a.flex_1, t.atoms.text_contrast_medium]}>
{_(msg`This chat was disconnected`)} &middot;{' '}
{item.retry && (
<InlineLinkText
to="#"
label={_(msg`Press to attempt reconnection`)}
onPress={e => {
e.preventDefault()
item.retry?.()
return false
}}>
{_(msg`Reconnect`)}
</InlineLinkText>
)}
</Text>
</View>
</View>
)
}
@@ -15,6 +15,7 @@ import {isWeb} from 'platform/detection'
import {List} from 'view/com/util/List'
import {MessageInput} from '#/screens/Messages/Conversation/MessageInput'
import {MessageListError} from '#/screens/Messages/Conversation/MessageListError'
import {MessageListFirehoseError} from '#/screens/Messages/Conversation/MessageListFirehoseError'
import {atoms as a} from '#/alf'
import {MessageItem} from '#/components/dms/MessageItem'
import {Loader} from '#/components/Loader'
@@ -41,6 +42,8 @@ function renderItem({item}: {item: ConvoItem}) {
return <Text>Deleted message</Text>
} else if (item.type === 'error') {
return <MessageListError item={item} />
} else if (item.type === 'firehose-error') {
return <MessageListFirehoseError item={item} />
}
return null
+31 -38
View File
@@ -21,6 +21,7 @@ import {
import {
ConvoDispatch,
ConvoDispatchEvent,
ConvoError,
ConvoErrorCode,
ConvoEvent,
ConvoItem,
@@ -55,13 +56,7 @@ export class Convo {
private senderUserDid: string
private status: ConvoStatus = ConvoStatus.Uninitialized
private error:
| {
code: ConvoErrorCode
exception?: Error
retry: () => void
}
| undefined
private error: ConvoError | undefined
private oldestRev: string | undefined | null = undefined
private isFetchingHistory = false
private latestRev: string | undefined = undefined
@@ -79,7 +74,6 @@ export class Convo {
{id: string; message: ChatBskyConvoSendMessage.InputSchema['message']}
> = new Map()
private deletedMessages: Set<string> = new Set()
private footerItems: Map<string, ConvoItem> = new Map()
private headerItems: Map<string, ConvoItem> = new Map()
private isProcessingPendingMessages = false
@@ -180,7 +174,7 @@ export class Convo {
status: ConvoStatus.Error,
items: [],
convo: undefined,
error: this.error,
error: this.error!,
sender: undefined,
recipients: undefined,
isFetchingHistory: false,
@@ -288,7 +282,7 @@ export class Convo {
if (this.convo) {
this.status = ConvoStatus.Ready
this.refreshConvo()
this.attemptCleanup()
this.maybeRecoverFromNetworkError()
} else {
this.status = ConvoStatus.Initializing
this.setup()
@@ -386,7 +380,6 @@ export class Convo {
this.newMessages = new Map()
this.pendingMessages = new Map()
this.deletedMessages = new Set()
this.footerItems = new Map()
this.headerItems = new Map()
this.pendingMessageFailure = null
@@ -394,10 +387,14 @@ export class Convo {
this.dispatch({event: ConvoDispatchEvent.Init})
}
private attemptCleanup() {
// override what's here in case remote convo state has changed
this.pendingMessageFailure = 'recoverable'
this.batchRetryPendingMessages()
maybeRecoverFromNetworkError() {
if (this.firehoseError) {
this.firehoseError.retry()
this.firehoseError = undefined
this.commit()
} else {
this.batchRetryPendingMessages()
}
}
private async setup() {
@@ -643,22 +640,16 @@ export class Convo {
)
}
private firehoseError: MessagesEventBusError | undefined
onFirehoseConnect() {
this.footerItems.delete(ConvoItemError.FirehoseFailed)
this.firehoseError = undefined
this.batchRetryPendingMessages()
this.commit()
}
onFirehoseError(error?: MessagesEventBusError) {
this.footerItems.set(ConvoItemError.FirehoseFailed, {
type: 'error',
key: ConvoItemError.FirehoseFailed,
code: ConvoItemError.FirehoseFailed,
retry: () => {
this.footerItems.delete(ConvoItemError.FirehoseFailed)
this.commit()
error?.retry()
},
})
this.firehoseError = error
this.commit()
}
@@ -852,18 +843,14 @@ export class Convo {
}
async batchRetryPendingMessages() {
if (this.pendingMessageFailure === 'unrecoverable') return
// reset if present, and recoverable
if (this.pendingMessageFailure === 'recoverable') {
this.pendingMessageFailure = null
this.commit()
}
if (this.pendingMessageFailure === null) return
const messageArray = Array.from(this.pendingMessages.values())
if (messageArray.length === 0) return
this.pendingMessageFailure = null
this.commit()
logger.debug(
`Convo: batch retrying ${this.pendingMessages.size} pending messages`,
{},
@@ -1012,15 +999,21 @@ export class Convo {
retry:
this.pendingMessageFailure === 'recoverable'
? () => {
this.batchRetryPendingMessages()
this.maybeRecoverFromNetworkError()
}
: undefined,
})
})
this.footerItems.forEach(item => {
items.push(item)
})
if (this.firehoseError) {
items.push({
type: 'firehose-error',
key: 'firehose-error',
retry: () => {
this.firehoseError?.retry()
},
})
}
return items
.filter(item => {
+9 -1
View File
@@ -123,6 +123,14 @@ export type ConvoItem =
*/
retry?: () => void
}
| {
type: 'firehose-error'
key: string
/**
* If present, error is recoverable.
*/
retry?: () => void
}
type DeleteMessage = (messageId: string) => Promise<void>
type SendMessage = (
@@ -194,7 +202,7 @@ export type ConvoStateError = {
status: ConvoStatus.Error
items: []
convo: undefined
error: any
error: ConvoError
sender: undefined
recipients: undefined
isFetchingHistory: false