Speed up initial direct messages (#5658)

## Summary

- avoid blocking first-DM navigation on a full channel-list refresh
- publish the initial message through the acknowledged HTTP path instead
of waiting on a missing WebSocket acknowledgement

## Validation

- 4,715 desktop unit tests
- desktop typecheck and checks
- focused new-DM Playwright coverage

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Mongo <9cfd347903944d5b85aa6c93d2ab67381b978a92a31914bca69998968752a1d7@buzz.block.builderlab.xyz>
This commit is contained in:
klopez4212
2026-08-13 16:24:48 -07:00
committed by GitHub
co-authored by Mongo
parent 0571f5455b
commit c8da06c5e9
5 changed files with 83 additions and 71 deletions
+13 -6
View File
@@ -484,24 +484,31 @@ export function useOpenDmMutation() {
);
},
onSettled: () => {
void queryClient.invalidateQueries({ queryKey: channelsQueryKey });
// The relay-returned DM is already in the cache. Mark the list stale so
// the normal live/poll refresh can reconcile it later without putting a
// full get_channels round-trip on the critical path to the conversation.
void queryClient.invalidateQueries({
queryKey: channelsQueryKey,
refetchType: "none",
});
},
});
}
/**
* Waits for any active channel-list refresh to settle, then restores a
* relay-returned channel to the shared cache before a caller depends on it for
* navigation.
* Reasserts a relay-returned channel in the shared cache before a caller
* depends on it for navigation. The open-DM mutation already made the relay
* write authoritative, so cancel any older list read and stay local rather
* than blocking on a read-after-write channel-list refresh.
*/
export function useUpsertCachedChannel() {
const queryClient = useQueryClient();
return React.useCallback(
async (channel: Channel) => {
await queryClient.refetchQueries({
await queryClient.cancelQueries({
queryKey: channelsQueryKey,
type: "active",
exact: true,
});
queryClient.setQueryData<Channel[]>(channelsQueryKey, (current) =>
reconcileRefreshedCachedChannel(current, channel),
+3
View File
@@ -452,6 +452,7 @@ export function useSendMessageMutation(
mediaTags?: string[][];
sentFromThreadRootId?: string | null;
sentFromThreadRootExcerpt?: string | null;
transport?: "auto" | "http";
},
MessageQueryContext | undefined
>({
@@ -464,6 +465,7 @@ export function useSendMessageMutation(
mediaTags,
sentFromThreadRootId,
sentFromThreadRootExcerpt,
transport = "auto",
}) => {
// Prefer a channel captured by the caller at compose time. Otherwise,
// resolve a captured id from the shared channel cache so navigation
@@ -523,6 +525,7 @@ export function useSendMessageMutation(
// the relay's tag validation runs. The WebSocket path emits no extra
// tags, so emoji-only messages would otherwise lose their emoji tag.
if (
transport === "http" ||
parentEventId ||
imetaTags.length > 0 ||
emojiTags.length > 0 ||
@@ -263,6 +263,11 @@ export function NewMessageScreen() {
content,
mentionPubkeys,
mediaTags,
// A newly opened DM is not subscribed yet, so publish its first
// message through the acknowledged HTTP path. This avoids holding
// the entire navigation on a WebSocket OK frame that staging may
// never deliver.
transport: "http",
});
} catch (error) {
preparedDirectMessageRef.current = null;
+10
View File
@@ -9087,6 +9087,16 @@ async function handleSendChannelMessage(
);
}
// Mirror the WebSocket send path's failure injection so specs that route
// the first message through the acknowledged HTTP transport still exercise
// `sendMessageErrors`. The real command rejects on a relay `OK false`, which
// surfaces to callers as a thrown error carrying the relay reason.
const sendMessageError =
kind === 9 ? config?.mock?.sendMessageErrors?.shift() : null;
if (sendMessageError) {
throw new Error(sendMessageError);
}
// NIP-92 imeta attachments. The real relay echoes these back on the stored
// event; mirror that here so attachment renderers (FileCard, images, video)
// have the imeta tags they key on. `null`/empty → no extra tags.
+52 -65
View File
@@ -781,33 +781,18 @@ test("creates the DM before preparing a persona mention", async ({ page }) => {
expect(expandedOpenIndex).toBeLessThan(startIndex);
expect(sendCommands).not.toContain("add_channel_members");
const sentMessageCommand = sendCommandPayloads.find((entry) => {
if (entry.command !== "plugin:websocket|send") {
return false;
}
const data = (entry.payload as { message?: { data?: string } } | undefined)
?.message?.data;
if (!data) {
return false;
}
const frame = JSON.parse(data) as unknown[];
return (
frame[0] === "EVENT" &&
(frame[1] as { content?: string } | undefined)?.content.includes(
"for a hand",
)
);
});
const sentMessageData = (
sentMessageCommand?.payload as { message?: { data?: string } } | undefined
)?.message?.data;
expect(sentMessageData).toBeTruthy();
const sentMessageEvent = (
JSON.parse(sentMessageData ?? "[]") as [string, { tags?: string[][] }]
)[1];
const sentChannelId = sentMessageEvent.tags?.find(
(tag) => tag[0] === "h",
)?.[1];
const sentMessageCommand = sendCommandPayloads.find(
(entry) =>
entry.command === "send_channel_message" &&
(
entry.payload as { content?: string; channelId?: string } | undefined
)?.content?.includes("for a hand"),
);
const sentChannelId = (
sentMessageCommand?.payload as
| { content?: string; channelId?: string }
| undefined
)?.channelId;
expect(sentChannelId).toBeTruthy();
await expect(
page.locator("[data-active='true'][data-channel-id]"),
@@ -1048,7 +1033,15 @@ test("drops an expanded DM after the first message fails", async ({ page }) => {
await expect(input).toContainText("Fizz");
const commandsAfterFailure = await readCommandPayloadLog(page);
const failedSendChannelId = await readOutgoingChannelId(page, "for a hand");
const failedSendChannelId = (
commandsAfterFailure.find(
(entry) =>
entry.command === "send_channel_message" &&
(
entry.payload as { content?: string; channelId?: string } | undefined
)?.content?.includes("for a hand"),
)?.payload as { content?: string; channelId?: string } | undefined
)?.channelId;
expect(failedSendChannelId).toBeTruthy();
expect(commandsAfterFailure.map((entry) => entry.command)).not.toContain(
"add_channel_members",
@@ -1075,29 +1068,15 @@ test("drops an expanded DM after the first message fails", async ({ page }) => {
),
).toBe(baselineOpenDmCount + 1);
const retryCommands = allCommands.slice(retryBaseline);
const retrySend = retryCommands.find((entry) => {
if (entry.command !== "plugin:websocket|send") {
return false;
}
const data = (entry.payload as { message?: { data?: string } } | undefined)
?.message?.data;
if (!data) {
return false;
}
const frame = JSON.parse(data) as unknown[];
return (
frame[0] === "EVENT" &&
(frame[1] as { content?: string } | undefined)?.content === retryMessage
);
});
const retrySendData = (
retrySend?.payload as { message?: { data?: string } } | undefined
)?.message?.data;
expect(retrySendData).toBeTruthy();
const retryEvent = (
JSON.parse(retrySendData ?? "[]") as [string, { tags?: string[][] }]
)[1];
const retryChannelId = retryEvent.tags?.find((tag) => tag[0] === "h")?.[1];
const retrySend = retryCommands.find(
(entry) =>
entry.command === "send_channel_message" &&
(entry.payload as { content?: string; channelId?: string } | undefined)
?.content === retryMessage,
);
const retryChannelId = (
retrySend?.payload as { content?: string; channelId?: string } | undefined
)?.channelId;
expect(retryChannelId).toBeTruthy();
expect(retryChannelId).not.toBe(failedSendChannelId);
await expect(
@@ -1231,7 +1210,7 @@ test("does not reopen a direct message after leaving the composer", async ({
await expect(page.getByTestId("chat-title")).toHaveText("general");
});
test("does not reopen a sent direct message after leaving during cache reseed", async ({
test("opens a sent direct message without waiting for a channel-list refresh", async ({
page,
}) => {
await page.goto("/");
@@ -1241,29 +1220,37 @@ test("does not reopen a sent direct message after leaving during cache reseed",
await page
.getByTestId(`new-dm-result-${TEST_IDENTITIES.charlie.pubkey}`)
.click();
const staleMessage = "Stay on the channel after cache reseed";
await page.getByTestId("message-input").fill(staleMessage);
const message = "Open without a channel-list refresh";
await page.getByTestId("message-input").fill(message);
const baselineChannelsReads = commandCount(
await readCommandLog(page),
"get_channels",
);
const baselineHttpSends = commandCount(
await readCommandLog(page),
"send_channel_message",
);
await page.evaluate(() => {
const testWindow = window as Window & {
__BUZZ_E2E__?: { mock?: { channelsReadDelayMs?: number } };
};
testWindow.__BUZZ_E2E__ ??= {};
testWindow.__BUZZ_E2E__.mock ??= {};
testWindow.__BUZZ_E2E__.mock.channelsReadDelayMs = 1_000;
testWindow.__BUZZ_E2E__.mock.channelsReadDelayMs = 3_000;
});
await page.getByTestId("send-message").click();
await expect
.poll(async () => hasOutgoingEventWithContent(page, staleMessage))
.toBe(true);
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await page.waitForTimeout(1_250);
await expect(page).toHaveURL(
new RegExp(`/channels/${GENERAL_CHANNEL_ID}(?:\\?|$)`),
await expect(page.getByTestId("chat-title")).toHaveText("charlie", {
timeout: 1_000,
});
await expect(page.getByTestId("message-timeline")).toContainText(message);
expect(commandCount(await readCommandLog(page), "get_channels")).toBe(
baselineChannelsReads,
);
await expect(page.getByTestId("chat-title")).toHaveText("general");
expect(commandCount(await readCommandLog(page), "send_channel_message")).toBe(
baselineHttpSends + 1,
);
await expect(page).toHaveURL(/\/channels\/[0-9a-f-]+(?:\?|$)/);
});
test("shows capped participant stack in group direct message header", async ({