From a725e75d5a9c34e4245fc987425faefcec4c0f63 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Fri, 2 Oct 2026 14:33:23 +0200 Subject: [PATCH] refactor: simplify optimistic updates --- .../__tests__/offline-support/optimistic-update.tsx | 8 ++++---- package/src/components/Channel/Channel.tsx | 4 ++-- .../hooks/__tests__/useMessageOperations.test.tsx | 4 ++-- .../components/Message/hooks/useMessageOperations.ts | 10 +++++----- package/src/store/types.ts | 6 +++--- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/package/src/__tests__/offline-support/optimistic-update.tsx b/package/src/__tests__/offline-support/optimistic-update.tsx index a001ff643c..6fa7d20837 100644 --- a/package/src/__tests__/offline-support/optimistic-update.tsx +++ b/package/src/__tests__/offline-support/optimistic-update.tsx @@ -52,7 +52,7 @@ const Channel = ChannelRaw as unknown as React.ComponentType< >; // Tests reach into internal / private StreamChat + LLC Channel APIs (sync manager, legacy -// `wsConnection`, `_deleteMessage`, `_sendReaction`, `_sendMessage`). Helpers narrow at the +// `wsConnection`, and the HTTP methods a replay calls). Helpers narrow at the // call sites without sprinkling `any` everywhere. type TestPendingTask = { id: number; type: string; payload: unknown }; type TestSyncManager = { @@ -1219,8 +1219,8 @@ export const OptimisticUpdates = () => { expect(pendingTasksRows.length).toBe(2); }); - const deleteMessageSpy = jest.spyOn(chatClient, '_deleteMessage').mockImplementation(); - const sendReactionSpy = jest.spyOn(channel, '_sendReaction').mockImplementation(); + const deleteMessageSpy = jest.spyOn(chatClient, 'deleteMessage').mockImplementation(); + const sendReactionSpy = jest.spyOn(chatClient, 'sendReaction').mockImplementation(); act(() => dispatchConnectionChangedEvent(chatClient, true)); @@ -1272,7 +1272,7 @@ export const OptimisticUpdates = () => { expect(pendingTasksRows.length).toBe(1); }); - const sendMessageSpy = jest.spyOn(channel, '_sendMessage').mockImplementation(); + const sendMessageSpy = jest.spyOn(channel, 'sendMessage').mockImplementation(); act(() => dispatchConnectionChangedEvent(chatClient, true)); diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index d7a6316e3a..06531c5096 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -686,7 +686,7 @@ const ChannelWithContext = (props: PropsWithChildren) = // instantly, then it awaits any attachment uploads still in flight and POSTs — through a // `sendMessageRequest` registered via `client.config.set(...)`, if any. It throws on failure, which the MessageInput send flow // catches to surface a notification. - await (threadInstance ?? channel).sendMessageWithLocalUpdate({ + await (threadInstance ?? channel).messageOperations.send({ localMessage, message: messageToSend, options, @@ -710,7 +710,7 @@ const ChannelWithContext = (props: PropsWithChildren) = const target = threadInstance?.messagePaginator.getItem(localMessage.id) ? threadInstance : channel; - await target.updateMessageWithLocalUpdate({ localMessage, options }); + await target.messageOperations.update({ localMessage, options }); }, ); diff --git a/package/src/components/Message/hooks/__tests__/useMessageOperations.test.tsx b/package/src/components/Message/hooks/__tests__/useMessageOperations.test.tsx index 4d6eed82c5..93c48fe572 100644 --- a/package/src/components/Message/hooks/__tests__/useMessageOperations.test.tsx +++ b/package/src/components/Message/hooks/__tests__/useMessageOperations.test.tsx @@ -43,7 +43,7 @@ describe('useMessageOperations.deleteMessage', () => { }) as unknown as LocalMessage; it('removes a failed send locally, without a server delete', async () => { - const serverDelete = jest.spyOn(channel, 'deleteMessageWithLocalUpdate').mockResolvedValue(); + const serverDelete = jest.spyOn(channel.messageOperations, 'delete').mockResolvedValue(); const removeItem = jest.spyOn(channel.messagePaginator, 'removeItem'); const { result } = render(); @@ -54,7 +54,7 @@ describe('useMessageOperations.deleteMessage', () => { }); it('deletes a message whose edit failed on the server, since the server has it', async () => { - const serverDelete = jest.spyOn(channel, 'deleteMessageWithLocalUpdate').mockResolvedValue(); + const serverDelete = jest.spyOn(channel.messageOperations, 'delete').mockResolvedValue(); const { result } = render(); await act(() => diff --git a/package/src/components/Message/hooks/useMessageOperations.ts b/package/src/components/Message/hooks/useMessageOperations.ts index 11a83febfc..ac4ca966c6 100644 --- a/package/src/components/Message/hooks/useMessageOperations.ts +++ b/package/src/components/Message/hooks/useMessageOperations.ts @@ -22,7 +22,7 @@ export type MessageOperations = { /** * Message operations (delete/remove/retry/react/update) resolved from the channel and — when a * thread is open — the thread instance. These are thin wrappers over the stream-chat message - * operations engine (`*WithLocalUpdate`, `messagePaginator.ingestItem/removeItem`), which owns the + * operations engine (`messageOperations.*`, `messagePaginator.ingestItem/removeItem`), which owns the * optimistic lifecycle, offline-DB persistence and paginator ingest. Previously these lived on the * MessagesContext; they now resolve per-message (mirroring stream-chat-react, which has no shared * action context) so the operations stay co-located with the LLC state they drive. @@ -68,7 +68,7 @@ export const useMessageOperations = (): MessageOperations => { const retrySendMessage: MessageOperations['retrySendMessage'] = useStableCallback( async (localMessage) => { - await (threadInstance ?? channel).retrySendMessageWithLocalUpdate({ localMessage }); + await (threadInstance ?? channel).messageOperations.retry({ localMessage }); }, ); @@ -85,7 +85,7 @@ export const useMessageOperations = (): MessageOperations => { // channel message's reaction into the thread paginator while a (channel-wide) thread is open. const target = threadInstance?.messagePaginator.getItem(messageId) ? threadInstance : channel; - await target.addReactionWithLocalUpdate({ + await target.messageOperations.addReaction({ messageId, options: { enforce_unique: enforceUniqueReaction }, reaction: { type }, @@ -101,7 +101,7 @@ export const useMessageOperations = (): MessageOperations => { const target = threadInstance?.messagePaginator.getItem(messageId) ? threadInstance : channel; - await target.deleteReactionWithLocalUpdate({ messageId, type }); + await target.messageOperations.deleteReaction({ messageId, type }); }, ); @@ -137,7 +137,7 @@ export const useMessageOperations = (): MessageOperations => { const target = threadInstance?.messagePaginator.getItem(message.id) ? threadInstance : channel; - await target.deleteMessageWithLocalUpdate({ + await target.messageOperations.delete({ localMessage: message, options, }); diff --git a/package/src/store/types.ts b/package/src/store/types.ts index a3d676c374..fc998f9141 100644 --- a/package/src/store/types.ts +++ b/package/src/store/types.ts @@ -1,4 +1,4 @@ -import type { Channel, StreamChat } from 'stream-chat'; +import type { StreamChat } from 'stream-chat'; import type { Schema } from './schema'; @@ -33,7 +33,7 @@ export type PendingTask = { id?: number; } & ( | { - payload: Parameters; + payload: Parameters; type: PendingTaskTypes['sendReaction']; } | { @@ -41,7 +41,7 @@ export type PendingTask = { type: PendingTaskTypes['deleteMessage']; } | { - payload: Parameters; + payload: Parameters; type: PendingTaskTypes['deleteReaction']; } );