This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new a704b59ae fix(topic): lock message sending against repeat
confirmations (#6448)
a704b59ae is described below
commit a704b59ae3ce1ce7e8a71adc90f63b228a8ab8e0
Author: messere <[email protected]>
AuthorDate: Thu Oct 15 18:12:00 2026 +0800
fix(topic): lock message sending against repeat confirmations (#6448)
---
.../pages/instance/__tests__/TopicPage.test.tsx | 50 ++++++++++++++++++++++
web/src/pages/instance/topic.tsx | 8 +++-
2 files changed, 57 insertions(+), 1 deletion(-)
diff --git a/web/src/pages/instance/__tests__/TopicPage.test.tsx
b/web/src/pages/instance/__tests__/TopicPage.test.tsx
index f566d9a13..1178debe0 100644
--- a/web/src/pages/instance/__tests__/TopicPage.test.tsx
+++ b/web/src/pages/instance/__tests__/TopicPage.test.tsx
@@ -440,6 +440,56 @@ describe('TopicPage', () => {
});
});
+ it('sends each test message once per confirmation while the first request is
pending', async () => {
+ topicServiceMocks.sendTopicMessage.mockImplementation(() => new Promise(()
=> {}));
+ const user = userEvent.setup({ pointerEventsCheck: 0 });
+ instanceServiceMocks.listInstances.mockResolvedValue([selectedInstance]);
+ mockTopicsList([buildTopics(1)[0]]);
+ renderWithProviders();
+
+ await user.click(await screen.findByRole('button', { name: /发送/ }));
+ const dialog = await getSendDialog();
+ fireEvent.change(within(dialog).getByLabelText('消息体 Body'), {
+ target: { value: 'double-send-payload' },
+ });
+ const send = within(dialog).getByRole('button', { name: /发\s*送/ });
+
+ fireEvent.click(send);
+ // The OK button must lock with the first click: `sending` alone flips
only after
+ // validateFields resolves and React re-renders, so a repeat confirmation
in that
+ // window re-enters handleSend and sends the same message twice.
+ expect(send).toHaveClass('ant-btn-loading');
+ fireEvent.click(send);
+
+ await waitFor(() =>
expect(topicServiceMocks.sendTopicMessage).toHaveBeenCalledTimes(1));
+ expect(topicServiceMocks.sendTopicMessage).toHaveBeenCalledWith(
+ expect.objectContaining({ body: 'double-send-payload' }),
+ );
+ expect(send).toHaveClass('ant-btn-loading');
+ });
+
+ it('keeps the send dialog usable for consecutive sends after one completes',
async () => {
+ const user = userEvent.setup({ pointerEventsCheck: 0 });
+ instanceServiceMocks.listInstances.mockResolvedValue([selectedInstance]);
+ mockTopicsList([buildTopics(1)[0]]);
+ renderWithProviders();
+
+ await user.click(await screen.findByRole('button', { name: /发送/ }));
+ const dialog = await getSendDialog();
+ fireEvent.change(within(dialog).getByLabelText('消息体 Body'), {
+ target: { value: 'consecutive-payload' },
+ });
+ const send = within(dialog).getByRole('button', { name: /发\s*送/ });
+
+ await user.click(send);
+ await waitFor(() =>
expect(topicServiceMocks.sendTopicMessage).toHaveBeenCalledTimes(1));
+ await waitFor(() => expect(send).not.toHaveClass('ant-btn-loading'));
+
+ await user.click(send);
+ await waitFor(() =>
expect(topicServiceMocks.sendTopicMessage).toHaveBeenCalledTimes(2));
+ expect(within(dialog).getByText(/发送消息到 topic-01/)).toBeInTheDocument();
+ });
+
it('opens a clean create dialog after a cancelled edit', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderWithProviders();
diff --git a/web/src/pages/instance/topic.tsx b/web/src/pages/instance/topic.tsx
index e2382ad54..ee33974ae 100644
--- a/web/src/pages/instance/topic.tsx
+++ b/web/src/pages/instance/topic.tsx
@@ -361,6 +361,7 @@ const TopicPageContent = ({
const consumersRequestIdRef = useRef(0);
const syncRequestIdRef = useRef(0);
const createInFlightRef = useRef(false);
+ const sendInFlightRef = useRef(false);
useEffect(
() => () => {
@@ -1154,14 +1155,18 @@ const TopicPageContent = ({
// ─── Send message modal submit ────────────────────────────────
const handleSend = async () => {
+ if (sendInFlightRef.current) return;
+ sendInFlightRef.current = true;
+ setSending(true);
let values: SendMessageFormValues;
try {
values = await sendForm.validateFields();
} catch {
// validation error, keep the modal open
+ sendInFlightRef.current = false;
+ setSending(false);
return;
}
- setSending(true);
try {
const payloadPreview = analyzeMessagePayloadPreview({
topic: values.topic,
@@ -1191,6 +1196,7 @@ const TopicPageContent = ({
} catch (error) {
message.error(error instanceof Error ? error.message : '消息发送失败,请稍后重试');
} finally {
+ sendInFlightRef.current = false;
setSending(false);
}
};