migrate the stateful chat agents to the chat client
Convo and MessagesEventBus were the last two useAgent consumers holding a long-lived agent rather than reading one per render. Both now take a chat client, which carries the atproto-proxy header itself, so the per-call DM_SERVICE_HEADERS drop out under the double-set rule. Both providers gained an effect that pushes the current client into the instance. The instances are created once with useState and outlive the client they were built with: an account switch, a cross-tab token sync or an expiry rescue replaces the bundle and disposes the previous clients, so without the sync a convo would keep sending through a dead session. `handleSendMessageFailure` narrows on XrpcResponseError rather than XRPCError. The lex client throws the former, so the status branch (and the recoverable / unrecoverable split that drives the retry banner) had silently stopped firing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -70,7 +70,7 @@ import {Loader} from '#/components/Loader'
|
|||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {useAnalytics} from '#/analytics'
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_ANDROID, IS_NATIVE, IS_WEB} from '#/env'
|
import {IS_ANDROID, IS_NATIVE, IS_WEB} from '#/env'
|
||||||
import {app} from '#/lexicons'
|
import {app, type chat, type com} from '#/lexicons'
|
||||||
import * as bsky from '#/types/bsky'
|
import * as bsky from '#/types/bsky'
|
||||||
import {ChatStatusInfo} from './ChatStatusInfo'
|
import {ChatStatusInfo} from './ChatStatusInfo'
|
||||||
import {groupSystemMessages, type RenderItem} from './groupSystemMessages'
|
import {groupSystemMessages, type RenderItem} from './groupSystemMessages'
|
||||||
@@ -537,10 +537,7 @@ export function MessagesList({
|
|||||||
*/
|
*/
|
||||||
rt.detectFacetsWithoutResolution()
|
rt.detectFacetsWithoutResolution()
|
||||||
|
|
||||||
let embed:
|
let embed: chat.bsky.convo.defs.MessageInput['embed']
|
||||||
| $Typed<AppBskyEmbedRecord.Main>
|
|
||||||
| $Typed<ChatBskyEmbedJoinLink.Main>
|
|
||||||
| undefined
|
|
||||||
let embedView:
|
let embedView:
|
||||||
| $Typed<AppBskyEmbedRecord.View>
|
| $Typed<AppBskyEmbedRecord.View>
|
||||||
| $Typed<ChatBskyEmbedJoinLink.View>
|
| $Typed<ChatBskyEmbedJoinLink.View>
|
||||||
@@ -576,10 +573,15 @@ export function MessagesList({
|
|||||||
if (post) {
|
if (post) {
|
||||||
embed = {
|
embed = {
|
||||||
$type: 'app.bsky.embed.record',
|
$type: 'app.bsky.embed.record',
|
||||||
|
/*
|
||||||
|
* `getPost` still returns an `@atproto/api` view, whose `uri` and
|
||||||
|
* `cid` are plain strings rather than the branded syntax types
|
||||||
|
* the lexicon input declares.
|
||||||
|
*/
|
||||||
record: {
|
record: {
|
||||||
uri: post.uri,
|
uri: post.uri,
|
||||||
cid: post.cid,
|
cid: post.cid,
|
||||||
},
|
} as com.atproto.repo.strongRef.Main,
|
||||||
}
|
}
|
||||||
|
|
||||||
embedView = {
|
embedView = {
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
import {
|
import {
|
||||||
type $Typed,
|
type $Typed,
|
||||||
type AppBskyEmbedRecord,
|
type AppBskyEmbedRecord,
|
||||||
type AtpAgent,
|
|
||||||
type ChatBskyActorDefs,
|
type ChatBskyActorDefs,
|
||||||
ChatBskyConvoDefs,
|
ChatBskyConvoDefs,
|
||||||
type ChatBskyConvoGetLog,
|
type ChatBskyConvoGetLog,
|
||||||
type ChatBskyConvoSendMessage,
|
|
||||||
type ChatBskyEmbedJoinLink,
|
type ChatBskyEmbedJoinLink,
|
||||||
type ChatBskyGroupDefs,
|
type ChatBskyGroupDefs,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {XRPCError} from '@atproto/api'
|
import {type Client, XrpcResponseError} from '@atproto/lex'
|
||||||
import {EventEmitter} from 'eventemitter3'
|
import {EventEmitter} from 'eventemitter3'
|
||||||
import {nanoid} from 'nanoid/non-secure'
|
import {nanoid} from 'nanoid/non-secure'
|
||||||
|
|
||||||
import {networkRetry} from '#/lib/async/retry'
|
import {networkRetry} from '#/lib/async/retry'
|
||||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
|
||||||
import {
|
import {
|
||||||
isErrorMaybeAppPasswordPermissions,
|
isErrorMaybeAppPasswordPermissions,
|
||||||
isNetworkError,
|
isNetworkError,
|
||||||
@@ -52,6 +49,7 @@ import {
|
|||||||
parseConvoView,
|
parseConvoView,
|
||||||
} from '#/components/dms/util'
|
} from '#/components/dms/util'
|
||||||
import {IS_NATIVE} from '#/env'
|
import {IS_NATIVE} from '#/env'
|
||||||
|
import {chat} from '#/lexicons'
|
||||||
|
|
||||||
const logger = Logger.create(Logger.Context.ConversationAgent)
|
const logger = Logger.create(Logger.Context.ConversationAgent)
|
||||||
|
|
||||||
@@ -105,7 +103,7 @@ function toDeletedMessageView(
|
|||||||
export class Convo {
|
export class Convo {
|
||||||
private id: string
|
private id: string
|
||||||
|
|
||||||
private agent: AtpAgent
|
private chatClient: Client
|
||||||
private events: MessagesEventBus
|
private events: MessagesEventBus
|
||||||
private senderUserDid: string
|
private senderUserDid: string
|
||||||
|
|
||||||
@@ -131,7 +129,7 @@ export class Convo {
|
|||||||
string,
|
string,
|
||||||
{
|
{
|
||||||
id: string
|
id: string
|
||||||
message: ChatBskyConvoSendMessage.InputSchema['message']
|
message: chat.bsky.convo.defs.MessageInput
|
||||||
optimisticEmbedView?:
|
optimisticEmbedView?:
|
||||||
| $Typed<AppBskyEmbedRecord.View>
|
| $Typed<AppBskyEmbedRecord.View>
|
||||||
| $Typed<ChatBskyEmbedJoinLink.View>
|
| $Typed<ChatBskyEmbedJoinLink.View>
|
||||||
@@ -166,9 +164,9 @@ export class Convo {
|
|||||||
constructor(params: ConvoParams) {
|
constructor(params: ConvoParams) {
|
||||||
this.id = nanoid(3)
|
this.id = nanoid(3)
|
||||||
this.convoId = params.convoId
|
this.convoId = params.convoId
|
||||||
this.agent = params.agent
|
this.chatClient = params.chatClient
|
||||||
this.events = params.events
|
this.events = params.events
|
||||||
this.senderUserDid = params.agent.assertDid
|
this.senderUserDid = params.chatClient.assertDid
|
||||||
|
|
||||||
if (params.placeholderData) {
|
if (params.placeholderData) {
|
||||||
this.setupPlaceholderData(params.placeholderData)
|
this.setupPlaceholderData(params.placeholderData)
|
||||||
@@ -197,6 +195,15 @@ export class Convo {
|
|||||||
this.subscribers.forEach(subscriber => subscriber())
|
this.subscribers.forEach(subscriber => subscriber())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Point the convo at a new chat client, for when the session bundle is
|
||||||
|
* replaced underneath it. Conversation state is unaffected: the same account
|
||||||
|
* is being served over a fresh session.
|
||||||
|
*/
|
||||||
|
updateClient(chatClient: Client) {
|
||||||
|
this.chatClient = chatClient
|
||||||
|
}
|
||||||
|
|
||||||
private subscribers: (() => void)[] = []
|
private subscribers: (() => void)[] = []
|
||||||
|
|
||||||
subscribe(subscriber: () => void) {
|
subscribe(subscriber: () => void) {
|
||||||
@@ -720,13 +727,12 @@ export class Convo {
|
|||||||
this.pendingFetchConvo = (async () => {
|
this.pendingFetchConvo = (async () => {
|
||||||
try {
|
try {
|
||||||
const response = await networkRetry(2, () => {
|
const response = await networkRetry(2, () => {
|
||||||
return this.agent.chat.bsky.convo.getConvo(
|
return this.chatClient.call(chat.bsky.convo.getConvo, {
|
||||||
{convoId: this.convoId},
|
convoId: this.convoId,
|
||||||
{headers: DM_SERVICE_HEADERS},
|
})
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const convo = response.data.convo
|
const convo = response.convo
|
||||||
|
|
||||||
return {
|
return {
|
||||||
convo,
|
convo,
|
||||||
@@ -763,18 +769,15 @@ export class Convo {
|
|||||||
let cursor: string | undefined
|
let cursor: string | undefined
|
||||||
do {
|
do {
|
||||||
const result = await networkRetry(2, () => {
|
const result = await networkRetry(2, () => {
|
||||||
return this.agent.chat.bsky.convo.getConvoMembers(
|
return this.chatClient.call(chat.bsky.convo.getConvoMembers, {
|
||||||
{
|
convoId: this.convoId,
|
||||||
convoId: this.convoId,
|
limit: 50,
|
||||||
limit: 50,
|
cursor,
|
||||||
cursor,
|
})
|
||||||
},
|
|
||||||
{headers: DM_SERVICE_HEADERS},
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
cursor = result.data.cursor
|
cursor = result.cursor
|
||||||
|
|
||||||
for (const member of result.data.members) {
|
for (const member of result.members) {
|
||||||
this.relatedProfiles.set(member.did, member)
|
this.relatedProfiles.set(member.did, member)
|
||||||
}
|
}
|
||||||
} while (cursor)
|
} while (cursor)
|
||||||
@@ -808,16 +811,13 @@ export class Convo {
|
|||||||
|
|
||||||
const nextCursor = this.oldestRev // for TS
|
const nextCursor = this.oldestRev // for TS
|
||||||
const response = await networkRetry(2, () => {
|
const response = await networkRetry(2, () => {
|
||||||
return this.agent.chat.bsky.convo.getMessages(
|
return this.chatClient.call(chat.bsky.convo.getMessages, {
|
||||||
{
|
cursor: nextCursor,
|
||||||
cursor: nextCursor,
|
convoId: this.convoId,
|
||||||
convoId: this.convoId,
|
limit: IS_NATIVE ? 30 : 60,
|
||||||
limit: IS_NATIVE ? 30 : 60,
|
})
|
||||||
},
|
|
||||||
{headers: DM_SERVICE_HEADERS},
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
const {cursor, messages, relatedProfiles} = response.data
|
const {cursor, messages, relatedProfiles} = response
|
||||||
|
|
||||||
// Trust the cursor for pagination. We can't infer "no more pages" from a
|
// Trust the cursor for pagination. We can't infer "no more pages" from a
|
||||||
// short page: the server pages by raw rows but strips deleted messages
|
// short page: the server pages by raw rows but strips deleted messages
|
||||||
@@ -1031,7 +1031,7 @@ export class Convo {
|
|||||||
private pendingMessageFailure: 'recoverable' | 'unrecoverable' | null = null
|
private pendingMessageFailure: 'recoverable' | 'unrecoverable' | null = null
|
||||||
|
|
||||||
sendMessage(
|
sendMessage(
|
||||||
message: ChatBskyConvoSendMessage.InputSchema['message'],
|
message: chat.bsky.convo.defs.MessageInput,
|
||||||
optimisticEmbedView?:
|
optimisticEmbedView?:
|
||||||
| $Typed<AppBskyEmbedRecord.View>
|
| $Typed<AppBskyEmbedRecord.View>
|
||||||
| $Typed<ChatBskyEmbedJoinLink.View>,
|
| $Typed<ChatBskyEmbedJoinLink.View>,
|
||||||
@@ -1165,14 +1165,10 @@ export class Convo {
|
|||||||
|
|
||||||
const {id, message} = pendingMessage
|
const {id, message} = pendingMessage
|
||||||
|
|
||||||
const response = await this.agent.chat.bsky.convo.sendMessage(
|
const res = await this.chatClient.call(chat.bsky.convo.sendMessage, {
|
||||||
{
|
convoId: this.convoId,
|
||||||
convoId: this.convoId,
|
message,
|
||||||
message,
|
})
|
||||||
},
|
|
||||||
{encoding: 'application/json', headers: DM_SERVICE_HEADERS},
|
|
||||||
)
|
|
||||||
const res = response.data
|
|
||||||
|
|
||||||
// remove from queue
|
// remove from queue
|
||||||
this.pendingMessages.delete(id)
|
this.pendingMessages.delete(id)
|
||||||
@@ -1197,8 +1193,15 @@ export class Convo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleSendMessageFailure(e: Error | XRPCError) {
|
/*
|
||||||
if (e instanceof XRPCError) {
|
* The lex client throws `XrpcResponseError`, not `@atproto/api`'s
|
||||||
|
* `XRPCError`, so the status/message branch narrows on the lex class. Only
|
||||||
|
* a genuine server response carries a status: transport and internal lex
|
||||||
|
* failures are `XrpcInternalError`s and fall through to the generic arm,
|
||||||
|
* where `isNetworkError` keeps them out of the logs.
|
||||||
|
*/
|
||||||
|
private handleSendMessageFailure(e: Error | XrpcResponseError) {
|
||||||
|
if (e instanceof XrpcResponseError) {
|
||||||
if (NETWORK_FAILURE_STATUSES.includes(e.status)) {
|
if (NETWORK_FAILURE_STATUSES.includes(e.status)) {
|
||||||
this.pendingMessageFailure = 'recoverable'
|
this.pendingMessageFailure = 'recoverable'
|
||||||
} else {
|
} else {
|
||||||
@@ -1261,16 +1264,15 @@ export class Convo {
|
|||||||
)
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {data} = await this.agent.chat.bsky.convo.sendMessageBatch(
|
const {items} = await this.chatClient.call(
|
||||||
|
chat.bsky.convo.sendMessageBatch,
|
||||||
{
|
{
|
||||||
items: messageArray.map(({message}) => ({
|
items: messageArray.map(({message}) => ({
|
||||||
convoId: this.convoId,
|
convoId: this.convoId,
|
||||||
message,
|
message,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
{encoding: 'application/json', headers: DM_SERVICE_HEADERS},
|
|
||||||
)
|
)
|
||||||
const {items} = data
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Insert into `newMessages` as soon as we have a real ID. That way, when
|
* Insert into `newMessages` as soon as we have a real ID. That way, when
|
||||||
@@ -1304,13 +1306,10 @@ export class Convo {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await networkRetry(2, () => {
|
await networkRetry(2, () => {
|
||||||
return this.agent.chat.bsky.convo.deleteMessageForSelf(
|
return this.chatClient.call(chat.bsky.convo.deleteMessageForSelf, {
|
||||||
{
|
convoId: this.convoId,
|
||||||
convoId: this.convoId,
|
messageId,
|
||||||
messageId,
|
})
|
||||||
},
|
|
||||||
{encoding: 'application/json', headers: DM_SERVICE_HEADERS},
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const e = err as Error
|
const e = err as Error
|
||||||
@@ -1529,10 +1528,11 @@ export class Convo {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
logger.debug(`Adding reaction ${emoji} to message ${messageId}`)
|
logger.debug(`Adding reaction ${emoji} to message ${messageId}`)
|
||||||
const {data} = await this.agent.chat.bsky.convo.addReaction(
|
const data = await this.chatClient.call(chat.bsky.convo.addReaction, {
|
||||||
{messageId, value: emoji, convoId: this.convoId},
|
messageId,
|
||||||
{encoding: 'application/json', headers: DM_SERVICE_HEADERS},
|
value: emoji,
|
||||||
)
|
convoId: this.convoId,
|
||||||
|
})
|
||||||
if (ChatBskyConvoDefs.isMessageView(data.message)) {
|
if (ChatBskyConvoDefs.isMessageView(data.message)) {
|
||||||
if (this.pastMessages.has(messageId)) {
|
if (this.pastMessages.has(messageId)) {
|
||||||
this.pastMessages.set(messageId, data.message)
|
this.pastMessages.set(messageId, data.message)
|
||||||
@@ -1594,10 +1594,11 @@ export class Convo {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
logger.debug(`Removing reaction ${emoji} from message ${messageId}`)
|
logger.debug(`Removing reaction ${emoji} from message ${messageId}`)
|
||||||
await this.agent.chat.bsky.convo.removeReaction(
|
await this.chatClient.call(chat.bsky.convo.removeReaction, {
|
||||||
{messageId, value: emoji, convoId: this.convoId},
|
messageId,
|
||||||
{encoding: 'application/json', headers: DM_SERVICE_HEADERS},
|
value: emoji,
|
||||||
)
|
convoId: this.convoId,
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (restore) restore()
|
if (restore) restore()
|
||||||
throw error
|
throw error
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import {
|
|||||||
} from '#/state/queries/messages/conversation'
|
} from '#/state/queries/messages/conversation'
|
||||||
import {RQKEY_ROOT as ListConvosQueryKeyRoot} from '#/state/queries/messages/list-conversations'
|
import {RQKEY_ROOT as ListConvosQueryKeyRoot} from '#/state/queries/messages/list-conversations'
|
||||||
import {RQKEY as createProfileQueryKey} from '#/state/queries/profile'
|
import {RQKEY as createProfileQueryKey} from '#/state/queries/profile'
|
||||||
import {useAgent} from '#/state/session'
|
import {useChatClient} from '#/state/session'
|
||||||
import {type GroupConvoMember} from '#/components/dms/util'
|
import {type GroupConvoMember} from '#/components/dms/util'
|
||||||
|
|
||||||
export * from '#/state/messages/convo/util'
|
export * from '#/state/messages/convo/util'
|
||||||
@@ -80,7 +80,7 @@ export function ConvoProvider({
|
|||||||
convoId,
|
convoId,
|
||||||
}: Pick<ConvoParams, 'convoId'> & {children: React.ReactNode}) {
|
}: Pick<ConvoParams, 'convoId'> & {children: React.ReactNode}) {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const agent = useAgent()
|
const chatClient = useChatClient()
|
||||||
const events = useMessagesEventBus()
|
const events = useMessagesEventBus()
|
||||||
const [convo] = useState(() => {
|
const [convo] = useState(() => {
|
||||||
const placeholder = queryClient.getQueryData<ChatBskyConvoDefs.ConvoView>(
|
const placeholder = queryClient.getQueryData<ChatBskyConvoDefs.ConvoView>(
|
||||||
@@ -88,7 +88,7 @@ export function ConvoProvider({
|
|||||||
)
|
)
|
||||||
return new Convo({
|
return new Convo({
|
||||||
convoId,
|
convoId,
|
||||||
agent,
|
chatClient,
|
||||||
events,
|
events,
|
||||||
placeholderData: placeholder ? {convo: placeholder} : undefined,
|
placeholderData: placeholder ? {convo: placeholder} : undefined,
|
||||||
})
|
})
|
||||||
@@ -96,6 +96,16 @@ export function ConvoProvider({
|
|||||||
const service = useSyncExternalStore(convo.subscribe, convo.getSnapshot)
|
const service = useSyncExternalStore(convo.subscribe, convo.getSnapshot)
|
||||||
const {mutate: markAsRead} = useMarkAsReadMutation()
|
const {mutate: markAsRead} = useMarkAsReadMutation()
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The convo outlives the client it was constructed with: replacing the
|
||||||
|
* session bundle builds fresh clients over the new session and disposes the
|
||||||
|
* old ones, so a convo still holding the previous client would send through a
|
||||||
|
* dead session.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
convo.updateClient(chatClient)
|
||||||
|
}, [convo, chatClient])
|
||||||
|
|
||||||
const appState = useAppState()
|
const appState = useAppState()
|
||||||
const isActive = appState === 'active'
|
const isActive = appState === 'active'
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
import {
|
import {
|
||||||
type $Typed,
|
type $Typed,
|
||||||
type AppBskyEmbedRecord,
|
type AppBskyEmbedRecord,
|
||||||
type AtpAgent,
|
|
||||||
type ChatBskyActorDefs,
|
type ChatBskyActorDefs,
|
||||||
type ChatBskyConvoDefs,
|
type ChatBskyConvoDefs,
|
||||||
type ChatBskyConvoSendMessage,
|
|
||||||
type ChatBskyEmbedJoinLink,
|
type ChatBskyEmbedJoinLink,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
|
import {type Client} from '@atproto/lex'
|
||||||
|
|
||||||
import {type MessagesEventBus} from '#/state/messages/events/agent'
|
import {type MessagesEventBus} from '#/state/messages/events/agent'
|
||||||
import {type ConvoWithDetails} from '#/components/dms/util'
|
import {type ConvoWithDetails} from '#/components/dms/util'
|
||||||
|
import {type chat} from '#/lexicons'
|
||||||
|
|
||||||
export type ConvoParams = {
|
export type ConvoParams = {
|
||||||
convoId: string
|
convoId: string
|
||||||
agent: AtpAgent
|
/** The chat client, which proxies `chat.bsky.*` to the chat service. */
|
||||||
|
chatClient: Client
|
||||||
events: MessagesEventBus
|
events: MessagesEventBus
|
||||||
placeholderData?: {
|
placeholderData?: {
|
||||||
convo: ChatBskyConvoDefs.ConvoView
|
convo: ChatBskyConvoDefs.ConvoView
|
||||||
@@ -108,7 +109,7 @@ export type ConvoItem =
|
|||||||
|
|
||||||
type DeleteMessage = (messageId: string) => Promise<void>
|
type DeleteMessage = (messageId: string) => Promise<void>
|
||||||
type SendMessage = (
|
type SendMessage = (
|
||||||
message: ChatBskyConvoSendMessage.InputSchema['message'],
|
message: chat.bsky.convo.defs.MessageInput,
|
||||||
optimisticEmbedView:
|
optimisticEmbedView:
|
||||||
| $Typed<AppBskyEmbedRecord.View>
|
| $Typed<AppBskyEmbedRecord.View>
|
||||||
| $Typed<ChatBskyEmbedJoinLink.View>
|
| $Typed<ChatBskyEmbedJoinLink.View>
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import {type AtpAgent, type ChatBskyConvoGetLog} from '@atproto/api'
|
import {type Client} from '@atproto/lex'
|
||||||
import {EventEmitter} from 'eventemitter3'
|
import {EventEmitter} from 'eventemitter3'
|
||||||
import {nanoid} from 'nanoid/non-secure'
|
import {nanoid} from 'nanoid/non-secure'
|
||||||
|
|
||||||
import {networkRetry} from '#/lib/async/retry'
|
import {networkRetry} from '#/lib/async/retry'
|
||||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
|
||||||
import {
|
import {
|
||||||
isErrorMaybeAppPasswordPermissions,
|
isErrorMaybeAppPasswordPermissions,
|
||||||
isNetworkError,
|
isNetworkError,
|
||||||
@@ -21,13 +20,14 @@ import {
|
|||||||
type MessagesEventBusParams,
|
type MessagesEventBusParams,
|
||||||
MessagesEventBusStatus,
|
MessagesEventBusStatus,
|
||||||
} from '#/state/messages/events/types'
|
} from '#/state/messages/events/types'
|
||||||
|
import {chat} from '#/lexicons'
|
||||||
|
|
||||||
const logger = Logger.create(Logger.Context.DMsAgent)
|
const logger = Logger.create(Logger.Context.DMsAgent)
|
||||||
|
|
||||||
export class MessagesEventBus {
|
export class MessagesEventBus {
|
||||||
private id: string
|
private id: string
|
||||||
|
|
||||||
private agent: AtpAgent
|
private chatClient: Client
|
||||||
private emitter = new EventEmitter<{event: [MessagesEventBusEvent]}>()
|
private emitter = new EventEmitter<{event: [MessagesEventBusEvent]}>()
|
||||||
|
|
||||||
private status: MessagesEventBusStatus = MessagesEventBusStatus.Initializing
|
private status: MessagesEventBusStatus = MessagesEventBusStatus.Initializing
|
||||||
@@ -37,11 +37,20 @@ export class MessagesEventBus {
|
|||||||
|
|
||||||
constructor(params: MessagesEventBusParams) {
|
constructor(params: MessagesEventBusParams) {
|
||||||
this.id = nanoid(3)
|
this.id = nanoid(3)
|
||||||
this.agent = params.agent
|
this.chatClient = params.chatClient
|
||||||
|
|
||||||
this.init()
|
this.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Point the bus at a new chat client, for when the session bundle is
|
||||||
|
* replaced underneath it. The poll cursor and subscribers are unaffected: the
|
||||||
|
* same account is being served over a fresh session.
|
||||||
|
*/
|
||||||
|
updateClient(chatClient: Client) {
|
||||||
|
this.chatClient = chatClient
|
||||||
|
}
|
||||||
|
|
||||||
requestPollInterval(interval: number) {
|
requestPollInterval(interval: number) {
|
||||||
const id = nanoid()
|
const id = nanoid()
|
||||||
this.requestedPollIntervals.set(id, interval)
|
this.requestedPollIntervals.set(id, interval)
|
||||||
@@ -260,14 +269,11 @@ export class MessagesEventBus {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await networkRetry(2, () => {
|
const response = await networkRetry(2, () => {
|
||||||
return this.agent.chat.bsky.convo.getLog(
|
return this.chatClient.call(chat.bsky.convo.getLog, {})
|
||||||
{},
|
|
||||||
{headers: DM_SERVICE_HEADERS},
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
// throw new Error('UNCOMMENT TO TEST INIT FAILURE')
|
// throw new Error('UNCOMMENT TO TEST INIT FAILURE')
|
||||||
|
|
||||||
const {cursor} = response.data
|
const {cursor} = response
|
||||||
|
|
||||||
// should always be defined
|
// should always be defined
|
||||||
if (cursor) {
|
if (cursor) {
|
||||||
@@ -355,21 +361,18 @@ export class MessagesEventBus {
|
|||||||
// )
|
// )
|
||||||
|
|
||||||
let needsEmit = false
|
let needsEmit = false
|
||||||
let batch: ChatBskyConvoGetLog.OutputSchema['logs'] = []
|
let batch: chat.bsky.convo.getLog.$OutputBody['logs'] = []
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await networkRetry(2, () => {
|
const response = await networkRetry(2, () => {
|
||||||
return this.agent.chat.bsky.convo.getLog(
|
return this.chatClient.call(chat.bsky.convo.getLog, {
|
||||||
{
|
cursor: this.latestRev,
|
||||||
cursor: this.latestRev,
|
})
|
||||||
},
|
|
||||||
{headers: DM_SERVICE_HEADERS},
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// throw new Error('UNCOMMENT TO TEST POLL FAILURE')
|
// throw new Error('UNCOMMENT TO TEST POLL FAILURE')
|
||||||
|
|
||||||
const {logs: events} = response.data
|
const {logs: events} = response
|
||||||
|
|
||||||
for (const ev of events) {
|
for (const ev of events) {
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {createContext, useContext, useEffect, useState} from 'react'
|
|||||||
import {AppState} from 'react-native'
|
import {AppState} from 'react-native'
|
||||||
|
|
||||||
import {MessagesEventBus} from '#/state/messages/events/agent'
|
import {MessagesEventBus} from '#/state/messages/events/agent'
|
||||||
import {useAgent, useSession} from '#/state/session'
|
import {useChatClient, useSession} from '#/state/session'
|
||||||
|
|
||||||
const MessagesEventBusContext = createContext<MessagesEventBus | null>(null)
|
const MessagesEventBusContext = createContext<MessagesEventBus | null>(null)
|
||||||
MessagesEventBusContext.displayName = 'MessagesEventBusContext'
|
MessagesEventBusContext.displayName = 'MessagesEventBusContext'
|
||||||
@@ -42,14 +42,24 @@ export function MessagesEventBusProviderInner({
|
|||||||
}: {
|
}: {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
const agent = useAgent()
|
const chatClient = useChatClient()
|
||||||
const [bus] = useState(
|
const [bus] = useState(
|
||||||
() =>
|
() =>
|
||||||
new MessagesEventBus({
|
new MessagesEventBus({
|
||||||
agent,
|
chatClient,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The bus outlives the client it was constructed with: replacing the session
|
||||||
|
* bundle (account switch, cross-tab token sync, expiry rescue) builds fresh
|
||||||
|
* clients over the new session and disposes the old ones, so a bus still
|
||||||
|
* holding the previous client would poll through a dead session.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
bus.updateClient(chatClient)
|
||||||
|
}, [bus, chatClient])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
bus.resume()
|
bus.resume()
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import {type AtpAgent, type ChatBskyConvoGetLog} from '@atproto/api'
|
import {type Client} from '@atproto/lex'
|
||||||
|
|
||||||
|
import {type chat} from '#/lexicons'
|
||||||
|
|
||||||
export type MessagesEventBusParams = {
|
export type MessagesEventBusParams = {
|
||||||
agent: AtpAgent
|
/** The chat client, which proxies `chat.bsky.*` to the chat service. */
|
||||||
|
chatClient: Client
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum MessagesEventBusStatus {
|
export enum MessagesEventBusStatus {
|
||||||
@@ -64,5 +67,5 @@ export type MessagesEventBusEvent =
|
|||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'logs'
|
type: 'logs'
|
||||||
logs: ChatBskyConvoGetLog.OutputSchema['logs']
|
logs: chat.bsky.convo.getLog.$OutputBody['logs']
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user