mirror of
https://github.com/block/buzz.git
synced 2026-10-30 05:51:46 +00:00
## Summary A failed initial channel-history request no longer appears as an authoritative empty channel. The timeline now shows an announced error with a Retry action, while cached messages remain visible when a later refresh fails; successful empty channels continue to use their normal intro state. ### Related issue None found. ### Testing - Full desktop unit suite (`pnpm test`) - Desktop TypeScript check (`pnpm exec tsc --noEmit`) - Biome checks for changed files - Repository file-size ratchet - Full pre-push desktop checks and tests - Desktop app launched successfully against local Postgres and Redis for manual testing No screenshot is included because the new UI is only shown after a terminal relay-history failure; the regression test pins the error/empty/list precedence directly. --------- Signed-off-by: Thomas Petersen <thomasp@squareup.com>
262 lines
7.8 KiB
JavaScript
262 lines
7.8 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { QueryClient, QueryObserver } from "@tanstack/react-query";
|
|
|
|
import {
|
|
resolveTimelineLoadingLatch,
|
|
resolveTimelineQueryLoadingState,
|
|
selectTimelineLoadingState,
|
|
} from "./timelineLoadingState.ts";
|
|
|
|
const settled = {
|
|
isPending: false,
|
|
isFetching: false,
|
|
isPlaceholderData: false,
|
|
dataLength: null,
|
|
};
|
|
|
|
function deferred() {
|
|
let resolve;
|
|
let reject;
|
|
const promise = new Promise((resolvePromise, rejectPromise) => {
|
|
resolve = resolvePromise;
|
|
reject = rejectPromise;
|
|
});
|
|
return { promise, reject, resolve };
|
|
}
|
|
|
|
function resolveQueryLoading(result, settledChannelId = null) {
|
|
return resolveTimelineQueryLoadingState(settledChannelId, "chan-a", {
|
|
isEnabled: true,
|
|
isPending: result.isPending,
|
|
isFetching: result.isFetching,
|
|
isPlaceholderData: result.isPlaceholderData,
|
|
dataLength: result.data?.length ?? null,
|
|
isError: result.isError,
|
|
});
|
|
}
|
|
|
|
test("pending first fetch with no cache is loading", () => {
|
|
assert.equal(
|
|
selectTimelineLoadingState({ ...settled, isPending: true }),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test("stale placeholder while refetching is loading", () => {
|
|
// Revisited within gcTime: placeholderData hands back a cached array while the
|
|
// authoritative fetch runs. Must keep the skeleton up, not flash the intro.
|
|
assert.equal(
|
|
selectTimelineLoadingState({
|
|
...settled,
|
|
isFetching: true,
|
|
isPlaceholderData: true,
|
|
dataLength: 0,
|
|
}),
|
|
true,
|
|
);
|
|
});
|
|
|
|
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.
|
|
assert.equal(
|
|
selectTimelineLoadingState({
|
|
...settled,
|
|
isFetching: true,
|
|
isPlaceholderData: false,
|
|
dataLength: 0,
|
|
}),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test("settled with rows is not loading", () => {
|
|
assert.equal(
|
|
selectTimelineLoadingState({ ...settled, dataLength: 5 }),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test("settled and genuinely empty is not loading (real empty channel)", () => {
|
|
assert.equal(
|
|
selectTimelineLoadingState({ ...settled, dataLength: 0 }),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test("background refetch of a populated channel is not loading", () => {
|
|
// staleTime expiry can trigger a background refetch; with rows already present
|
|
// we are loaded and must not re-show the skeleton.
|
|
assert.equal(
|
|
selectTimelineLoadingState({
|
|
...settled,
|
|
isFetching: true,
|
|
dataLength: 12,
|
|
}),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test("initial load holds the skeleton while the cold-load top-up fetches", () => {
|
|
// Cold load seeds rows before its row-floor top-up finishes, so dataLength>0
|
|
// while isFetching is still true. Before settle, hold the skeleton — dropping
|
|
// it here is what exposed the older-fetch spinner on first load.
|
|
assert.equal(
|
|
selectTimelineLoadingState(
|
|
{ ...settled, isFetching: true, dataLength: 8 },
|
|
false,
|
|
),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test("pre-settle placeholder rows paint immediately (snapshot revisit)", () => {
|
|
// A revisit painting from the React-Query cache or a persisted snapshot:
|
|
// placeholder rows are a previously-settled timeline, not a partial cold
|
|
// load — show them stale-then-revalidate instead of a skeleton.
|
|
assert.equal(
|
|
selectTimelineLoadingState(
|
|
{ ...settled, isFetching: true, isPlaceholderData: true, dataLength: 8 },
|
|
false,
|
|
),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test("pre-settle EMPTY placeholder still holds the skeleton", () => {
|
|
assert.equal(
|
|
selectTimelineLoadingState(
|
|
{ ...settled, isFetching: true, isPlaceholderData: true, dataLength: 0 },
|
|
false,
|
|
),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test("settled channel with rows mid-refetch is not loading", () => {
|
|
// Same query shape, but after first settle: the latch owns refetch blips, so
|
|
// present rows mean loaded.
|
|
assert.equal(
|
|
selectTimelineLoadingState(
|
|
{ ...settled, isFetching: true, dataLength: 8 },
|
|
true,
|
|
),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test("latch: loading on first entry to a channel", () => {
|
|
const r = resolveTimelineLoadingLatch(null, "chan-a", true);
|
|
assert.equal(r.isLoading, true);
|
|
assert.equal(r.settledChannelId, null);
|
|
});
|
|
|
|
test("latch: settles when loadingNow turns false, recording the channel", () => {
|
|
const r = resolveTimelineLoadingLatch(null, "chan-a", false);
|
|
assert.equal(r.isLoading, false);
|
|
assert.equal(r.settledChannelId, "chan-a");
|
|
});
|
|
|
|
test("latch: background refetch blip stays loaded once settled", () => {
|
|
// settled for chan-a, then loadingNow blips true (isFetching) — must NOT
|
|
// re-show the skeleton (the bounce Wes reported).
|
|
const r = resolveTimelineLoadingLatch("chan-a", "chan-a", true);
|
|
assert.equal(r.isLoading, false);
|
|
assert.equal(r.settledChannelId, "chan-a");
|
|
});
|
|
|
|
test("latch: switching channels resets and loads the new one", () => {
|
|
const r = resolveTimelineLoadingLatch("chan-a", "chan-b", true);
|
|
assert.equal(r.isLoading, true);
|
|
assert.equal(r.settledChannelId, "chan-a"); // not yet settled for b
|
|
});
|
|
|
|
test("latch: no active channel passes loadingNow through untouched", () => {
|
|
assert.equal(
|
|
resolveTimelineLoadingLatch("chan-a", null, true).isLoading,
|
|
true,
|
|
);
|
|
assert.equal(
|
|
resolveTimelineLoadingLatch("chan-a", null, false).isLoading,
|
|
false,
|
|
);
|
|
});
|
|
|
|
test("query wiring: cold error retry stays loading until successful empty result", async () => {
|
|
const client = new QueryClient({
|
|
defaultOptions: { queries: { retry: false } },
|
|
});
|
|
const first = deferred();
|
|
const retry = deferred();
|
|
let attempt = 0;
|
|
const observer = new QueryObserver(client, {
|
|
queryKey: ["messages", "chan-a"],
|
|
queryFn: () => [first.promise, retry.promise][attempt++],
|
|
});
|
|
const unsubscribe = observer.subscribe(() => {});
|
|
|
|
try {
|
|
first.reject(new Error("history unavailable"));
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
let loading = resolveQueryLoading(observer.getCurrentResult());
|
|
assert.equal(observer.getCurrentResult().status, "error");
|
|
assert.deepEqual(loading, { settledChannelId: null, isLoading: false });
|
|
|
|
const retryResult = observer.refetch();
|
|
loading = resolveQueryLoading(observer.getCurrentResult());
|
|
assert.equal(observer.getCurrentResult().status, "pending");
|
|
assert.deepEqual(loading, { settledChannelId: null, isLoading: true });
|
|
|
|
retry.resolve([]);
|
|
await retryResult;
|
|
loading = resolveQueryLoading(observer.getCurrentResult());
|
|
assert.equal(observer.getCurrentResult().status, "success");
|
|
assert.deepEqual(loading, {
|
|
settledChannelId: "chan-a",
|
|
isLoading: false,
|
|
});
|
|
} finally {
|
|
unsubscribe();
|
|
}
|
|
});
|
|
|
|
test("query wiring: repeated cold retry failure never settles as empty", async () => {
|
|
const client = new QueryClient({
|
|
defaultOptions: { queries: { retry: false } },
|
|
});
|
|
const first = deferred();
|
|
const retry = deferred();
|
|
let attempt = 0;
|
|
const observer = new QueryObserver(client, {
|
|
queryKey: ["messages", "chan-a"],
|
|
queryFn: () => [first.promise, retry.promise][attempt++],
|
|
});
|
|
const unsubscribe = observer.subscribe(() => {});
|
|
|
|
try {
|
|
first.reject(new Error("history unavailable"));
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
assert.deepEqual(resolveQueryLoading(observer.getCurrentResult()), {
|
|
settledChannelId: null,
|
|
isLoading: false,
|
|
});
|
|
|
|
const retryResult = observer.refetch();
|
|
assert.deepEqual(resolveQueryLoading(observer.getCurrentResult()), {
|
|
settledChannelId: null,
|
|
isLoading: true,
|
|
});
|
|
|
|
retry.reject(new Error("still unavailable"));
|
|
await retryResult;
|
|
assert.equal(observer.getCurrentResult().status, "error");
|
|
assert.deepEqual(resolveQueryLoading(observer.getCurrentResult()), {
|
|
settledChannelId: null,
|
|
isLoading: false,
|
|
});
|
|
} finally {
|
|
unsubscribe();
|
|
}
|
|
});
|