mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
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:
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 ({
|
||||
|
||||
Reference in New Issue
Block a user