Gracefully handle network errors and recovery
This commit is contained in:
@@ -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`)} ·{' '}
|
||||||
|
{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 {List} from 'view/com/util/List'
|
||||||
import {MessageInput} from '#/screens/Messages/Conversation/MessageInput'
|
import {MessageInput} from '#/screens/Messages/Conversation/MessageInput'
|
||||||
import {MessageListError} from '#/screens/Messages/Conversation/MessageListError'
|
import {MessageListError} from '#/screens/Messages/Conversation/MessageListError'
|
||||||
|
import {MessageListFirehoseError} from '#/screens/Messages/Conversation/MessageListFirehoseError'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
import {MessageItem} from '#/components/dms/MessageItem'
|
import {MessageItem} from '#/components/dms/MessageItem'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
@@ -41,6 +42,8 @@ function renderItem({item}: {item: ConvoItem}) {
|
|||||||
return <Text>Deleted message</Text>
|
return <Text>Deleted message</Text>
|
||||||
} else if (item.type === 'error') {
|
} else if (item.type === 'error') {
|
||||||
return <MessageListError item={item} />
|
return <MessageListError item={item} />
|
||||||
|
} else if (item.type === 'firehose-error') {
|
||||||
|
return <MessageListFirehoseError item={item} />
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
ConvoDispatch,
|
ConvoDispatch,
|
||||||
ConvoDispatchEvent,
|
ConvoDispatchEvent,
|
||||||
|
ConvoError,
|
||||||
ConvoErrorCode,
|
ConvoErrorCode,
|
||||||
ConvoEvent,
|
ConvoEvent,
|
||||||
ConvoItem,
|
ConvoItem,
|
||||||
@@ -55,13 +56,7 @@ export class Convo {
|
|||||||
private senderUserDid: string
|
private senderUserDid: string
|
||||||
|
|
||||||
private status: ConvoStatus = ConvoStatus.Uninitialized
|
private status: ConvoStatus = ConvoStatus.Uninitialized
|
||||||
private error:
|
private error: ConvoError | undefined
|
||||||
| {
|
|
||||||
code: ConvoErrorCode
|
|
||||||
exception?: Error
|
|
||||||
retry: () => void
|
|
||||||
}
|
|
||||||
| undefined
|
|
||||||
private oldestRev: string | undefined | null = undefined
|
private oldestRev: string | undefined | null = undefined
|
||||||
private isFetchingHistory = false
|
private isFetchingHistory = false
|
||||||
private latestRev: string | undefined = undefined
|
private latestRev: string | undefined = undefined
|
||||||
@@ -79,7 +74,6 @@ export class Convo {
|
|||||||
{id: string; message: ChatBskyConvoSendMessage.InputSchema['message']}
|
{id: string; message: ChatBskyConvoSendMessage.InputSchema['message']}
|
||||||
> = new Map()
|
> = new Map()
|
||||||
private deletedMessages: Set<string> = new Set()
|
private deletedMessages: Set<string> = new Set()
|
||||||
private footerItems: Map<string, ConvoItem> = new Map()
|
|
||||||
private headerItems: Map<string, ConvoItem> = new Map()
|
private headerItems: Map<string, ConvoItem> = new Map()
|
||||||
|
|
||||||
private isProcessingPendingMessages = false
|
private isProcessingPendingMessages = false
|
||||||
@@ -180,7 +174,7 @@ export class Convo {
|
|||||||
status: ConvoStatus.Error,
|
status: ConvoStatus.Error,
|
||||||
items: [],
|
items: [],
|
||||||
convo: undefined,
|
convo: undefined,
|
||||||
error: this.error,
|
error: this.error!,
|
||||||
sender: undefined,
|
sender: undefined,
|
||||||
recipients: undefined,
|
recipients: undefined,
|
||||||
isFetchingHistory: false,
|
isFetchingHistory: false,
|
||||||
@@ -288,7 +282,7 @@ export class Convo {
|
|||||||
if (this.convo) {
|
if (this.convo) {
|
||||||
this.status = ConvoStatus.Ready
|
this.status = ConvoStatus.Ready
|
||||||
this.refreshConvo()
|
this.refreshConvo()
|
||||||
this.attemptCleanup()
|
this.maybeRecoverFromNetworkError()
|
||||||
} else {
|
} else {
|
||||||
this.status = ConvoStatus.Initializing
|
this.status = ConvoStatus.Initializing
|
||||||
this.setup()
|
this.setup()
|
||||||
@@ -386,7 +380,6 @@ export class Convo {
|
|||||||
this.newMessages = new Map()
|
this.newMessages = new Map()
|
||||||
this.pendingMessages = new Map()
|
this.pendingMessages = new Map()
|
||||||
this.deletedMessages = new Set()
|
this.deletedMessages = new Set()
|
||||||
this.footerItems = new Map()
|
|
||||||
this.headerItems = new Map()
|
this.headerItems = new Map()
|
||||||
|
|
||||||
this.pendingMessageFailure = null
|
this.pendingMessageFailure = null
|
||||||
@@ -394,10 +387,14 @@ export class Convo {
|
|||||||
this.dispatch({event: ConvoDispatchEvent.Init})
|
this.dispatch({event: ConvoDispatchEvent.Init})
|
||||||
}
|
}
|
||||||
|
|
||||||
private attemptCleanup() {
|
maybeRecoverFromNetworkError() {
|
||||||
// override what's here in case remote convo state has changed
|
if (this.firehoseError) {
|
||||||
this.pendingMessageFailure = 'recoverable'
|
this.firehoseError.retry()
|
||||||
this.batchRetryPendingMessages()
|
this.firehoseError = undefined
|
||||||
|
this.commit()
|
||||||
|
} else {
|
||||||
|
this.batchRetryPendingMessages()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async setup() {
|
private async setup() {
|
||||||
@@ -643,22 +640,16 @@ export class Convo {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private firehoseError: MessagesEventBusError | undefined
|
||||||
|
|
||||||
onFirehoseConnect() {
|
onFirehoseConnect() {
|
||||||
this.footerItems.delete(ConvoItemError.FirehoseFailed)
|
this.firehoseError = undefined
|
||||||
|
this.batchRetryPendingMessages()
|
||||||
this.commit()
|
this.commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
onFirehoseError(error?: MessagesEventBusError) {
|
onFirehoseError(error?: MessagesEventBusError) {
|
||||||
this.footerItems.set(ConvoItemError.FirehoseFailed, {
|
this.firehoseError = error
|
||||||
type: 'error',
|
|
||||||
key: ConvoItemError.FirehoseFailed,
|
|
||||||
code: ConvoItemError.FirehoseFailed,
|
|
||||||
retry: () => {
|
|
||||||
this.footerItems.delete(ConvoItemError.FirehoseFailed)
|
|
||||||
this.commit()
|
|
||||||
error?.retry()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
this.commit()
|
this.commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -852,18 +843,14 @@ export class Convo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async batchRetryPendingMessages() {
|
async batchRetryPendingMessages() {
|
||||||
if (this.pendingMessageFailure === 'unrecoverable') return
|
if (this.pendingMessageFailure === null) return
|
||||||
|
|
||||||
// reset if present, and recoverable
|
|
||||||
if (this.pendingMessageFailure === 'recoverable') {
|
|
||||||
this.pendingMessageFailure = null
|
|
||||||
this.commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
const messageArray = Array.from(this.pendingMessages.values())
|
const messageArray = Array.from(this.pendingMessages.values())
|
||||||
|
|
||||||
if (messageArray.length === 0) return
|
if (messageArray.length === 0) return
|
||||||
|
|
||||||
|
this.pendingMessageFailure = null
|
||||||
|
this.commit()
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Convo: batch retrying ${this.pendingMessages.size} pending messages`,
|
`Convo: batch retrying ${this.pendingMessages.size} pending messages`,
|
||||||
{},
|
{},
|
||||||
@@ -1012,15 +999,21 @@ export class Convo {
|
|||||||
retry:
|
retry:
|
||||||
this.pendingMessageFailure === 'recoverable'
|
this.pendingMessageFailure === 'recoverable'
|
||||||
? () => {
|
? () => {
|
||||||
this.batchRetryPendingMessages()
|
this.maybeRecoverFromNetworkError()
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
this.footerItems.forEach(item => {
|
if (this.firehoseError) {
|
||||||
items.push(item)
|
items.push({
|
||||||
})
|
type: 'firehose-error',
|
||||||
|
key: 'firehose-error',
|
||||||
|
retry: () => {
|
||||||
|
this.firehoseError?.retry()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return items
|
return items
|
||||||
.filter(item => {
|
.filter(item => {
|
||||||
|
|||||||
@@ -123,6 +123,14 @@ export type ConvoItem =
|
|||||||
*/
|
*/
|
||||||
retry?: () => void
|
retry?: () => void
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'firehose-error'
|
||||||
|
key: string
|
||||||
|
/**
|
||||||
|
* If present, error is recoverable.
|
||||||
|
*/
|
||||||
|
retry?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
type DeleteMessage = (messageId: string) => Promise<void>
|
type DeleteMessage = (messageId: string) => Promise<void>
|
||||||
type SendMessage = (
|
type SendMessage = (
|
||||||
@@ -194,7 +202,7 @@ export type ConvoStateError = {
|
|||||||
status: ConvoStatus.Error
|
status: ConvoStatus.Error
|
||||||
items: []
|
items: []
|
||||||
convo: undefined
|
convo: undefined
|
||||||
error: any
|
error: ConvoError
|
||||||
sender: undefined
|
sender: undefined
|
||||||
recipients: undefined
|
recipients: undefined
|
||||||
isFetchingHistory: false
|
isFetchingHistory: false
|
||||||
|
|||||||
Reference in New Issue
Block a user