perf(desktop): paint warm channels from cache

Use authoritative channel-window provenance to distinguish warm empty
channels from cold live-seeded caches. Mount each channel timeline with
its route-matched cached snapshot and revalidate the retained scrollback
extent atomically so background catch-up cannot erase the reader's anchor.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
This commit is contained in:
Wes
2026-08-14 14:33:39 -06:00
co-authored by Carl
parent 1f4c69eccf
commit a85fa30e0a
8 changed files with 216 additions and 31 deletions
@@ -568,6 +568,7 @@ export const ChannelPane = React.memo(function ChannelPane({
>
{isHuddleTranscript ? null : header}
<MessageTimeline
key={activeChannelId ?? "none"}
ref={messageTimelineRef}
channelId={activeChannel?.id}
channelIntro={channelIntro}
@@ -605,6 +605,7 @@ export function ChannelScreen({
isPending: messagesQuery.isPending,
isFetching: messagesQuery.isFetching,
isPlaceholderData: messagesQuery.isPlaceholderData,
hasAuthoritativePage: (windowQuery.data?.pages.length ?? 0) > 0,
dataLength: messagesQuery.data?.length ?? null,
},
hasSettledThisChannel,
+92 -5
View File
@@ -55,6 +55,8 @@ import type { Channel, Identity, RelayEvent } from "@/shared/api/types";
// from the on-render overlay.
import { applyEditTagOverlay } from "@/features/messages/lib/applyEditTagOverlay.mjs";
import {
appendOlderChannelWindow,
compareRelayOrder,
emptyChannelWindowStore,
mapChannelWindowEvents,
mergeLiveChannelWindowEvent,
@@ -245,21 +247,97 @@ export function reconcileFetchedChannelWindow(
events: Awaited<ReturnType<typeof getChannelWindowEvents>>,
previousMessages: RelayEvent[],
signal: AbortSignal,
): RelayEvent[] {
return reconcileFetchedChannelWindowPages(
queryClient,
channelId,
[parseChannelWindowResponse(events, channelId, null)],
previousMessages,
signal,
);
}
export function reconcileFetchedChannelWindowPages(
queryClient: QueryClient,
channelId: string,
pages: ReturnType<typeof parseChannelWindowResponse>[],
previousMessages: RelayEvent[],
signal: AbortSignal,
): RelayEvent[] {
// Tauri invokes cannot be canceled after dispatch. A replacement refetch can
// therefore win while this older request is still in flight. Never let that
// canceled request commit its stale page into the authoritative window.
signal.throwIfAborted();
const windowKey = channelWindowKey(channelId);
const page = parseChannelWindowResponse(events, channelId, null);
const current =
queryClient.getQueryData<ChannelWindowStore>(windowKey) ??
emptyChannelWindowStore();
const next = replaceNewestChannelWindow(current, page);
let next = replaceNewestChannelWindow(current, pages[0]);
for (const page of pages.slice(1)) {
next = appendOlderChannelWindow(next, page);
}
queryClient.setQueryData(windowKey, next);
return reconcileChannelWindowMessages(next, previousMessages);
}
const CHANNEL_WINDOW_PAGE_SIZE = 50;
const CHANNEL_WINDOW_MAX_REQUEST_ROWS = 200;
async function getRefreshedChannelWindowPages(
channelId: string,
retainedWindow: ChannelWindowStore | undefined,
) {
const retainedRowCount =
retainedWindow?.pages.reduce(
(count, page) => count + page.rows.length,
0,
) ?? 0;
const retainedOldest = retainedWindow?.pages.at(-1)?.rows.at(-1)?.event;
const targetRows = Math.max(CHANNEL_WINDOW_PAGE_SIZE, retainedRowCount);
const firstEvents = await getChannelWindowEvents(
channelId,
null,
Math.min(targetRows, CHANNEL_WINDOW_MAX_REQUEST_ROWS),
);
const firstPage = parseChannelWindowResponse(firstEvents, channelId, null);
const pages = [firstPage];
let rowCount = firstPage.rows.length;
const coversRetainedOldest = () => {
if (!retainedOldest) return true;
const refreshedOldest = pages.at(-1)?.rows.at(-1)?.event;
return (
refreshedOldest !== undefined &&
compareRelayOrder(refreshedOldest, retainedOldest) >= 0
);
};
while (
pages.at(-1)?.hasMore &&
(rowCount < targetRows || !coversRetainedOldest())
) {
const tail = pages.at(-1);
if (!tail?.nextCursor) break;
const nextEvents = await getChannelWindowEvents(
channelId,
tail.nextCursor,
Math.min(
Math.max(CHANNEL_WINDOW_PAGE_SIZE, targetRows - rowCount),
CHANNEL_WINDOW_MAX_REQUEST_ROWS,
),
);
const nextPage = parseChannelWindowResponse(
nextEvents,
channelId,
tail.nextCursor,
);
pages.push(nextPage);
rowCount += nextPage.rows.length;
}
return pages;
}
export function useChannelMessagesQuery(channel: Channel | null) {
const queryClient = useQueryClient();
const queryKey = channelMessagesKey(channel?.id ?? "none");
@@ -271,11 +349,20 @@ export function useChannelMessagesQuery(channel: Channel | null) {
if (!channel) throw new Error("No channel selected.");
const previousMessages =
queryClient.getQueryData<RelayEvent[]>(queryKey) ?? [];
const events = await getChannelWindowEvents(channel.id);
return reconcileFetchedChannelWindow(
const retainedWindow = queryClient.getQueryData<ChannelWindowStore>(
channelWindowKey(channel.id),
);
// A subscription/reconnect catch-up must cover the retained page extent.
// Refetching only the default head page would replace a multi-page window
// and delete the rows (and anchor) the reader is currently parked on.
const pages = await getRefreshedChannelWindowPages(
channel.id,
retainedWindow,
);
return reconcileFetchedChannelWindowPages(
queryClient,
channel.id,
events,
pages,
previousMessages,
signal,
);
@@ -2,7 +2,10 @@ import assert from "node:assert/strict";
import test from "node:test";
import { QueryClient, QueryObserver } from "@tanstack/react-query";
import { reconcileFetchedChannelWindow } from "../hooks.ts";
import {
reconcileFetchedChannelWindow,
reconcileFetchedChannelWindowPages,
} from "../hooks.ts";
import { channelMessagesKey, channelWindowKey } from "./messageQueryKeys.ts";
import {
appendOlderChannelWindow,
@@ -287,6 +290,61 @@ test("test_live_projection_retains_pending_send_and_non_broadcast_thread_reply",
]);
});
test("test_catch_up_atomically_replaces_the_full_retained_extent", () => {
const harness = createHarness();
const older = event("older", 50);
const firstCursor = { createdAt: 100, eventId: event("initial", 100).id };
const loaded = appendOlderChannelWindow(
replaceNewestChannelWindow(emptyChannelWindowStore(), {
...newestPage([event("initial", 100)]),
nextCursor: firstCursor,
hasMore: true,
}),
{
startCursor: firstCursor,
rows: [{ event: older, thread: null }],
aux: [],
nextCursor: null,
hasMore: false,
},
);
harness.client.setQueryData(harness.windowKey, loaded);
harness.client.setQueryData(harness.messagesKey, [
older,
event("initial", 100),
]);
const refreshedHead = {
startCursor: null,
rows: [event("gap", 110), event("initial", 100)].map((item) => ({
event: item,
thread: null,
})),
aux: [],
nextCursor: firstCursor,
hasMore: true,
};
const refreshedTail = {
startCursor: firstCursor,
rows: [{ event: older, thread: null }],
aux: [],
nextCursor: null,
hasMore: false,
};
const projected = reconcileFetchedChannelWindowPages(
harness.client,
harness.channelId,
[refreshedHead, refreshedTail],
harness.client.getQueryData(harness.messagesKey),
new AbortController().signal,
);
harness.client.setQueryData(harness.messagesKey, projected);
assert.equal(harness.client.getQueryData(harness.windowKey).pages.length, 2);
assert.deepEqual(contents(harness), ["older", "initial", "gap"]);
});
test("test_canceled_stale_fetch_cannot_overwrite_catch_up_window", async () => {
const harness = createHarness();
const requests = [];
@@ -31,6 +31,33 @@ test("stale placeholder while refetching is loading", () => {
);
});
test("authoritative warm empty stays visible while revalidating", () => {
assert.equal(
selectTimelineLoadingState({
...settled,
isFetching: true,
hasAuthoritativePage: true,
dataLength: 0,
}),
false,
);
});
test("authoritative warm rows stay visible before the local latch settles", () => {
assert.equal(
selectTimelineLoadingState(
{
...settled,
isFetching: true,
hasAuthoritativePage: true,
dataLength: 12,
},
false,
),
false,
);
});
test("subscription-seeded empty cache while fetching is loading", () => {
// The live subscription's setQueryData seeds [] before history settles, so
// data is defined but empty and a fetch is still in flight.
@@ -12,6 +12,8 @@ export type TimelineQueryStatus = {
isPending: boolean;
isFetching: boolean;
isPlaceholderData: boolean;
/** True once this channel has an authoritative page, including an empty one. */
hasAuthoritativePage?: boolean;
dataLength: number | null;
};
@@ -19,6 +21,12 @@ export function selectTimelineLoadingState(
status: TimelineQueryStatus,
hasSettled = true,
): boolean {
// Page provenance, not row count, distinguishes a warm empty channel from a
// cold cache seeded by the live subscription. Once a page exists, every
// subsequent fetch is background revalidation and must not cover the cache.
if (status.hasAuthoritativePage) {
return false;
}
if (status.isPending) {
return true;
}
@@ -146,12 +146,6 @@ type TimelineSnapshot = {
historyExhausted: boolean;
};
const EMPTY_TIMELINE_SNAPSHOT: TimelineSnapshot = {
channelId: null,
messages: EMPTY_MESSAGES,
historyExhausted: false,
};
const MessageTimelineBase = React.forwardRef<
MessageTimelineHandle,
MessageTimelineProps
@@ -232,28 +226,16 @@ const MessageTimelineBase = React.forwardRef<
[scrollContainerRef, virtualizerScrollParent],
);
// Gate the heavy timeline render (each row runs a synchronous
// react-markdown parse) behind React concurrency. `useDeferredValue` lets the
// commit that rebuilds the message list yield to higher-priority work, so the
// main thread stops freezing and the OS no longer shows the busy cursor when
// entering a channel. We pass `initialValue: []` so even the FIRST render on
// channel entry stays light — the heavy list streams in on a deferred commit
// rather than blocking the initial paint. We deliberately drive BOTH the
// scroll manager and the rendered list off the same deferred value —
// scroll/autoscroll/deep-link logic reads the DOM (`scrollIntoView`,
// ResizeObserver on the content), so it must stay consistent with what's
// actually painted. You can't scroll to a row that hasn't committed yet.
// Channel id travels with the deferred message snapshot. Without that guard, a
// route change can paint the previous channel's deferred rows for a frame even
// though the sidebar/header already moved to the new channel.
// The timeline itself is keyed by channel, so this is the selected channel's
// route-matched cache on mount. Warm channels paint it immediately; cold
// channels still initialize cheaply because their snapshot is empty. Keeping
// rows and history provenance in the same initial value also prevents scroll
// and deep-link logic from observing a different generation than the DOM.
const liveSnapshot = React.useMemo<TimelineSnapshot>(
() => ({ channelId: channelId ?? null, messages, historyExhausted }),
[channelId, historyExhausted, messages],
);
const deferredSnapshot = React.useDeferredValue(
liveSnapshot,
EMPTY_TIMELINE_SNAPSHOT,
);
const deferredSnapshot = React.useDeferredValue(liveSnapshot, liveSnapshot);
const deferredMessages = deferredSnapshot.messages;
const imagePreloadStateRef = React.useRef({
activeImages: new Set<HTMLImageElement>(),
@@ -219,7 +219,7 @@ function samePair(a, b) {
function makeHarness(records, scroller) {
return function Harness({ snapshot }) {
const deferredSnapshot = React.useDeferredValue(snapshot, EMPTY_SNAPSHOT);
const deferredSnapshot = React.useDeferredValue(snapshot, snapshot);
const buffered = useBufferedTimelineMessages({
channelId: deferredSnapshot.channelId,
isAtBottom: false, // reader is scrolled up — the tear's regime
@@ -272,6 +272,27 @@ async function mount(Comp, snapshot) {
// ── Tests ────────────────────────────────────────────────────────────────────
test("warm snapshot is the first committed render, including its provenance", async () => {
const warm = {
channelId: "chan-warm",
messages: rows(["cached-a", "cached-b"]),
historyExhausted: true,
};
const records = [];
const handle = await mount(makeHarness(records, makeFakeScroller()), warm);
assert.deepEqual(
{
count: records[0].count,
firstId: records[0].firstId,
exhausted: records[0].exhausted,
},
{ count: 2, firstId: "cached-a", exhausted: true },
);
await handle.unmount();
});
test("pass-1 landing: exhaustion proof can never pair with the stale row array", async () => {
const CHANNEL = "chan-tear";
// Snapshot A: 100 rows loaded, more history exists (mid-pagination).