mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-08 07:17:39 +00:00
refactor: replace React Query message cache with windowed message store
Introduces a new message-window-store module to manage message state with automatic windowing of visible and pending messages. This replaces manual React Query cache operations with a centralized, observable state system. close #39 Key changes: - New MessageWindowState tracks visible/pending messages with size limits - Automatic trimming of message windows (400 visible, 200 pending messages) - Pending message buffering when user scrolls away from bottom - Centralized status updates for optimistic messages - Thread component simplified with forwarded scroll and pending callbacks - Removes message count tracking from components This improves performance for chats with many messages and provides a cleaner separation of concerns between UI and state management.
This commit is contained in:
@@ -1,8 +1,12 @@
|
||||
import { useMutation, useQueryClient, type InfiniteData } from '@tanstack/react-query'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import type { ApiClient } from '@/api/client'
|
||||
import type { DecryptedMessage, MessagesResponse } from '@/types/api'
|
||||
import { makeClientSideId, upsertMessagesInCache } from '@/lib/messages'
|
||||
import { queryKeys } from '@/lib/query-keys'
|
||||
import type { DecryptedMessage } from '@/types/api'
|
||||
import { makeClientSideId } from '@/lib/messages'
|
||||
import {
|
||||
appendOptimisticMessage,
|
||||
getMessageWindowState,
|
||||
updateMessageStatus,
|
||||
} from '@/lib/message-window-store'
|
||||
import { usePlatform } from '@/hooks/usePlatform'
|
||||
|
||||
type SendMessageInput = {
|
||||
@@ -12,36 +16,16 @@ type SendMessageInput = {
|
||||
createdAt: number
|
||||
}
|
||||
|
||||
function updateMessageStatus(
|
||||
data: InfiniteData<MessagesResponse> | undefined,
|
||||
localId: string,
|
||||
status: DecryptedMessage['status'],
|
||||
): InfiniteData<MessagesResponse> | undefined {
|
||||
if (!data) return data
|
||||
|
||||
const pages = data.pages.map((page) => ({
|
||||
...page,
|
||||
messages: page.messages.map((message) =>
|
||||
message.localId === localId
|
||||
? { ...message, status }
|
||||
: message
|
||||
),
|
||||
}))
|
||||
|
||||
return {
|
||||
...data,
|
||||
pages,
|
||||
}
|
||||
}
|
||||
|
||||
function findMessageByLocalId(
|
||||
data: InfiniteData<MessagesResponse> | undefined,
|
||||
sessionId: string,
|
||||
localId: string,
|
||||
): DecryptedMessage | null {
|
||||
if (!data) return null
|
||||
for (const page of data.pages) {
|
||||
const match = page.messages.find((message) => message.localId === localId)
|
||||
if (match) return match
|
||||
const state = getMessageWindowState(sessionId)
|
||||
for (const message of state.messages) {
|
||||
if (message.localId === localId) return message
|
||||
}
|
||||
for (const message of state.pending) {
|
||||
if (message.localId === localId) return message
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -51,7 +35,6 @@ export function useSendMessage(api: ApiClient | null, sessionId: string | null):
|
||||
retryMessage: (localId: string) => void
|
||||
isSending: boolean
|
||||
} {
|
||||
const queryClient = useQueryClient()
|
||||
const { haptic } = usePlatform()
|
||||
|
||||
const mutation = useMutation({
|
||||
@@ -72,23 +55,14 @@ export function useSendMessage(api: ApiClient | null, sessionId: string | null):
|
||||
originalText: input.text,
|
||||
}
|
||||
|
||||
queryClient.setQueryData<InfiniteData<MessagesResponse>>(
|
||||
queryKeys.messages(input.sessionId),
|
||||
(data) => upsertMessagesInCache(data, [optimisticMessage]),
|
||||
)
|
||||
appendOptimisticMessage(input.sessionId, optimisticMessage)
|
||||
},
|
||||
onSuccess: (_, input) => {
|
||||
queryClient.setQueryData<InfiniteData<MessagesResponse>>(
|
||||
queryKeys.messages(input.sessionId),
|
||||
(data) => updateMessageStatus(data, input.localId, 'sent'),
|
||||
)
|
||||
updateMessageStatus(input.sessionId, input.localId, 'sent')
|
||||
haptic.notification('success')
|
||||
},
|
||||
onError: (_, input) => {
|
||||
queryClient.setQueryData<InfiniteData<MessagesResponse>>(
|
||||
queryKeys.messages(input.sessionId),
|
||||
(data) => updateMessageStatus(data, input.localId, 'failed'),
|
||||
)
|
||||
updateMessageStatus(input.sessionId, input.localId, 'failed')
|
||||
haptic.notification('error')
|
||||
},
|
||||
})
|
||||
@@ -109,14 +83,10 @@ export function useSendMessage(api: ApiClient | null, sessionId: string | null):
|
||||
if (!api || !sessionId) return
|
||||
if (mutation.isPending) return
|
||||
|
||||
const data = queryClient.getQueryData<InfiniteData<MessagesResponse>>(queryKeys.messages(sessionId))
|
||||
const message = findMessageByLocalId(data, localId)
|
||||
const message = findMessageByLocalId(sessionId, localId)
|
||||
if (!message?.originalText) return
|
||||
|
||||
queryClient.setQueryData<InfiniteData<MessagesResponse>>(
|
||||
queryKeys.messages(sessionId),
|
||||
(current) => updateMessageStatus(current, localId, 'sending'),
|
||||
)
|
||||
updateMessageStatus(sessionId, localId, 'sending')
|
||||
|
||||
mutation.mutate({
|
||||
sessionId,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { isPermissionModeAllowedForFlavor } from '@hapi/protocol'
|
||||
import type { ApiClient } from '@/api/client'
|
||||
import type { ModelMode, PermissionMode } from '@/types/api'
|
||||
import { queryKeys } from '@/lib/query-keys'
|
||||
import { clearMessageWindow } from '@/lib/message-window-store'
|
||||
|
||||
export function useSessionActions(
|
||||
api: ApiClient | null,
|
||||
@@ -100,7 +101,7 @@ export function useSessionActions(
|
||||
onSuccess: async () => {
|
||||
if (!sessionId) return
|
||||
queryClient.removeQueries({ queryKey: queryKeys.session(sessionId) })
|
||||
queryClient.removeQueries({ queryKey: queryKeys.messages(sessionId) })
|
||||
clearMessageWindow(sessionId)
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.sessions })
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user