perf(desktop): persist channel snapshot hash (#5684)

## Summary

- persist each relay/identity's complete channel list and server hash as
one integrity-checked snapshot
- paint the snapshot immediately on cold boot, then revalidate with
`knownHash`
- fail slow-never-wrong: malformed/legacy/partial snapshots and
mismatched not-modified responses force an unhashed full fetch
- add sidebar boot diagnostics and deterministic unit/E2E coverage for
boot, identity/relay isolation, partial writes, mismatch fallback, and
community switches

## Safety invariants

- channel list and hash are serialized in one localStorage document and
replaced together
- snapshot ownership is scoped to normalized relay URL plus identity
pubkey
- a not-modified response is accepted only when its hash exactly matches
the hash describing the available list
- any missing or impossible hash/list pairing retries
`getChannels(null)` before replacing persistence

## Validation

At exact commit `19ca25d23c434cc0b8893a93691aaf4c77794f60` with a clean
working tree:

- `cd desktop && pnpm check && pnpm typecheck` — passed (existing
informational Biome findings only)
- `cd desktop && pnpm test` — 4,723 passed
- `cd desktop && node --import ./test-loader.mjs
--experimental-strip-types --test
src/features/channels/channelSnapshot.test.mjs` — 13 passed
- `cd desktop && pnpm exec playwright test e2e/sidebar-snapshot.spec.ts
--grep "cold boot paints" --repeat-each=5` — 5 passed
- `cd desktop && pnpm exec playwright test e2e/sidebar-snapshot.spec.ts`
— 8 passed
- push hooks repeated desktop check/typecheck and all 4,723 unit tests
successfully

The Playwright suite uses injected bridge delays. Its roughly 0.5–0.6 s
snapshot paint and 3.0 s snapshot-to-live readings are synthetic
invariant evidence, not production desktop performance measurements.

## Measurement context

The controlled current-main investigation is documented separately in
`RESEARCH/DESKTOP_PERF_DEEP_DIVE_2026_08_12.md`; its raw local artifacts
are `.scratch/summer-perf-deepdive-results-v2.json`,
`.scratch/summer-perf-deepdive-run.log`,
`.scratch/summer-perf-deepdive-run-2.log`, and
`.scratch/summer-perf-deepdive-build-2.log`. Those original timings are
also synthetic Chromium/mock-bridge measurements and are not presented
as shipped Tauri/WKWebView or production-relay numbers.


## Latest review delta

At exact tip `e8e2b1d617aac7ea008258ad9974bbf8da9cd2eb`, storage-denial
reads fail open to the live fetch, hashless retries reject `channels:
null` before pair/persistence updates, identity-read failure enables a
hashless live fetch, and repeated consumers reuse snapshot
parsing/integrity validation by storage key + raw document. The four
remaining review nits are deferred as non-blocking follow-ups.

Validation at this tip: sidebar snapshot E2E 30/30 serial; desktop unit
suite 4,725/4,725; full push gate green (desktop check/typecheck/unit,
Rust, Tauri).

---------

Signed-off-by: Summer <1fdd3cc104e2911eb3b2da6f97d1b25f4a7f3550ded4492b24ff1d95acd66766@buzz.block.builderlab.xyz>
Co-authored-by: Summer <1fdd3cc104e2911eb3b2da6f97d1b25f4a7f3550ded4492b24ff1d95acd66766@buzz.block.builderlab.xyz>
This commit is contained in:
Wes
2026-08-12 15:23:41 -06:00
committed by GitHub
parent 72d56e7bd3
commit c86443c599
9 changed files with 1340 additions and 164 deletions

View File

@@ -77,6 +77,7 @@ export default defineConfig({
"**/relay-connectivity.spec.ts",
"**/unread-pill.spec.ts",
"**/sidebar-more-unread-overlap.spec.ts",
"**/sidebar-snapshot.spec.ts",
"**/home-collapsed-top-chrome.spec.ts",
"**/top-chrome-zoom-clearance.spec.ts",
"**/thread-unread.spec.ts",

View File

@@ -3,6 +3,7 @@ import test from "node:test";
import {
channelSnapshotKey,
inspectChannelSnapshot,
readChannelSnapshot,
removeChannelSnapshotForRelay,
writeChannelSnapshot,
@@ -12,6 +13,10 @@ if (typeof globalThis.window === "undefined") {
const storage = new Map();
globalThis.window = {
localStorage: {
get length() {
return storage.size;
},
key: (index) => [...storage.keys()][index] ?? null,
getItem: (key) => storage.get(key) ?? null,
setItem: (key, value) => storage.set(key, value),
removeItem: (key) => storage.delete(key),
@@ -42,49 +47,214 @@ function makeChannel(overrides = {}) {
}
const RELAY = "wss://relay.example.com";
const OWNER = "ABCDEF";
const HASH = "channels-hash-v2";
test.beforeEach(() => {
removeChannelSnapshotForRelay(RELAY);
removeChannelSnapshotForRelay("wss://other.example.com");
});
test("channelSnapshotKey: normalizes trailing slash and case", () => {
assert.equal(
channelSnapshotKey("WSS://Relay.Example.com/"),
channelSnapshotKey("wss://relay.example.com"),
channelSnapshotKey("WSS://Relay.Example.com/", OWNER),
channelSnapshotKey("wss://relay.example.com", OWNER.toLowerCase()),
);
});
test("read after write returns the persisted channels", () => {
const channels = [makeChannel(), makeChannel({ id: "chan-2", name: "Dev" })];
writeChannelSnapshot(RELAY, channels);
assert.deepEqual(readChannelSnapshot(RELAY), channels);
test("read after write returns the complete list and matching hash", () => {
const channels = Array.from({ length: 14 }, (_, index) =>
makeChannel({ id: `chan-${index}`, name: `Channel ${index}` }),
);
writeChannelSnapshot(RELAY, OWNER, channels, HASH);
const snapshot = readChannelSnapshot(RELAY, OWNER.toLowerCase());
assert.deepEqual(snapshot, { channels, hash: HASH });
assert.equal(snapshot.channels.length, channels.length);
const { diagnostics } = inspectChannelSnapshot(RELAY, OWNER);
assert.equal(diagnostics.presence, "present");
assert.equal(diagnostics.channelCount, channels.length);
assert.ok(diagnostics.serializedBytes > 0);
assert.ok(diagnostics.ageMs >= 0);
});
test("inspection distinguishes absent from invalid snapshots", () => {
assert.deepEqual(inspectChannelSnapshot(RELAY, OWNER), {
diagnostics: {
ageMs: 0,
channelCount: 0,
presence: "absent",
serializedBytes: 0,
},
snapshot: null,
});
window.localStorage.setItem(channelSnapshotKey(RELAY, OWNER), "not-json{{{");
const invalid = inspectChannelSnapshot(RELAY, OWNER);
assert.equal(invalid.snapshot, null);
assert.equal(invalid.diagnostics.presence, "invalid");
assert.ok(invalid.diagnostics.serializedBytes > 0);
});
test("inspection memoizes parsing for repeated reads of the same document", () => {
writeChannelSnapshot(RELAY, OWNER, [makeChannel()], "memo-hash");
const originalParse = JSON.parse;
let parses = 0;
JSON.parse = (...args) => {
parses += 1;
return originalParse(...args);
};
try {
assert.notEqual(inspectChannelSnapshot(RELAY, OWNER).snapshot, null);
assert.notEqual(inspectChannelSnapshot(RELAY, OWNER).snapshot, null);
assert.equal(parses, 1);
} finally {
JSON.parse = originalParse;
}
});
test("inspection tolerates denied storage without retrying the read", () => {
const original = window.localStorage;
let reads = 0;
window.localStorage = {
get length() {
return 0;
},
key: () => null,
getItem: () => {
reads += 1;
throw new DOMException("storage denied", "SecurityError");
},
setItem: () => {},
removeItem: () => {},
};
try {
assert.deepEqual(inspectChannelSnapshot(RELAY, OWNER), {
diagnostics: {
ageMs: 0,
channelCount: 0,
presence: "absent",
serializedBytes: 0,
},
snapshot: null,
});
assert.equal(reads, 1);
} finally {
window.localStorage = original;
}
});
test("write replaces list and hash in lockstep", () => {
writeChannelSnapshot(RELAY, OWNER, [makeChannel()], "old-hash");
const channels = [
makeChannel({ id: "chan-2", name: "Dev" }),
makeChannel({ id: "chan-3", name: "Design" }),
];
writeChannelSnapshot(RELAY, OWNER, channels, "new-hash");
assert.deepEqual(readChannelSnapshot(RELAY, OWNER), {
channels,
hash: "new-hash",
});
});
test("read for an unknown relay returns null", () => {
assert.equal(readChannelSnapshot("wss://never-written.example.com"), null);
});
test("read returns null for malformed JSON", () => {
window.localStorage.setItem(channelSnapshotKey(RELAY), "not-json{{{");
assert.equal(readChannelSnapshot(RELAY), null);
});
test("read returns null for a wrong-version payload", () => {
window.localStorage.setItem(
channelSnapshotKey(RELAY),
JSON.stringify({ version: 2, channels: [makeChannel()] }),
assert.equal(
readChannelSnapshot("wss://never-written.example.com", OWNER),
null,
);
assert.equal(readChannelSnapshot(RELAY), null);
});
test("read returns null when channels is not an array", () => {
test("read rejects malformed JSON", () => {
window.localStorage.setItem(channelSnapshotKey(RELAY, OWNER), "not-json{{{");
assert.equal(readChannelSnapshot(RELAY, OWNER), null);
});
test("read rejects a legacy channel-only snapshot", () => {
window.localStorage.setItem(
channelSnapshotKey(RELAY),
JSON.stringify({ version: 1, channels: "nope" }),
channelSnapshotKey(RELAY, OWNER),
JSON.stringify({ version: 1, channels: [makeChannel()] }),
);
assert.equal(readChannelSnapshot(RELAY), null);
assert.equal(readChannelSnapshot(RELAY, OWNER), null);
});
test("remove clears the snapshot for that relay", () => {
writeChannelSnapshot(RELAY, [makeChannel()]);
test("read rejects a snapshot whose hash and list were partially changed", () => {
writeChannelSnapshot(RELAY, OWNER, [makeChannel()], "old-hash");
const key = channelSnapshotKey(RELAY, OWNER);
const stored = JSON.parse(window.localStorage.getItem(key));
window.localStorage.setItem(
key,
JSON.stringify({ ...stored, hash: "newer-partial-hash" }),
);
assert.equal(readChannelSnapshot(RELAY, OWNER), null);
assert.equal(
inspectChannelSnapshot(RELAY, OWNER).diagnostics.presence,
"invalid",
);
});
test("read rejects a v2 snapshot with a missing hash", () => {
window.localStorage.setItem(
channelSnapshotKey(RELAY, OWNER),
JSON.stringify({
version: 2,
updatedAt: Date.now(),
ownerPubkey: OWNER,
channels: [makeChannel()],
}),
);
assert.equal(readChannelSnapshot(RELAY, OWNER), null);
});
test("read rejects a snapshot owned by another identity", () => {
writeChannelSnapshot(RELAY, OWNER, [makeChannel()], HASH);
assert.equal(readChannelSnapshot(RELAY, "different-owner"), null);
});
test("same-relay identities retain independent snapshots", () => {
const ownerAChannels = [makeChannel({ id: "owner-a", name: "Owner A" })];
const ownerBChannels = [makeChannel({ id: "owner-b", name: "Owner B" })];
writeChannelSnapshot(RELAY, OWNER, ownerAChannels, "owner-a-hash");
writeChannelSnapshot(
RELAY,
"different-owner",
ownerBChannels,
"owner-b-hash",
);
assert.deepEqual(readChannelSnapshot(RELAY, OWNER), {
channels: ownerAChannels,
hash: "owner-a-hash",
});
assert.deepEqual(readChannelSnapshot(RELAY, "different-owner"), {
channels: ownerBChannels,
hash: "owner-b-hash",
});
});
test("remove clears every identity snapshot for that relay", () => {
writeChannelSnapshot(RELAY, OWNER, [makeChannel()], HASH);
writeChannelSnapshot(
RELAY,
"different-owner",
[makeChannel({ id: "owner-b" })],
"owner-b-hash",
);
writeChannelSnapshot(
"wss://other.example.com",
OWNER,
[makeChannel({ id: "other-relay" })],
"other-relay-hash",
);
removeChannelSnapshotForRelay(RELAY);
assert.equal(readChannelSnapshot(RELAY), null);
assert.equal(readChannelSnapshot(RELAY, OWNER), null);
assert.equal(readChannelSnapshot(RELAY, "different-owner"), null);
assert.notEqual(readChannelSnapshot("wss://other.example.com", OWNER), null);
});
test("cache write evicts disposable entries and retries at quota", () => {
@@ -108,8 +278,11 @@ test("cache write evicts disposable entries and retries at quota", () => {
removeItem: (key) => storage.delete(key),
};
try {
writeChannelSnapshot(RELAY, [makeChannel()]);
assert.deepEqual(readChannelSnapshot(RELAY), [makeChannel()]);
writeChannelSnapshot(RELAY, OWNER, [makeChannel()], HASH);
assert.deepEqual(readChannelSnapshot(RELAY, OWNER), {
channels: [makeChannel()],
hash: HASH,
});
assert.equal(storage.has("buzz-channel-messages.v1:relay:old"), false);
assert.equal(storage.has("buzz-timeline-skeleton-shape.v1:old"), false);
} finally {
@@ -123,7 +296,9 @@ test("write is tolerant of storage failures", () => {
throw new Error("quota exceeded");
};
try {
assert.doesNotThrow(() => writeChannelSnapshot(RELAY, [makeChannel()]));
assert.doesNotThrow(() =>
writeChannelSnapshot(RELAY, OWNER, [makeChannel()], HASH),
);
} finally {
window.localStorage.setItem = original;
}

View File

@@ -7,8 +7,9 @@
* channel list per relay so the sidebar can paint instantly from the snapshot
* while the live fetch revalidates in the background.
*
* Keyed per relay URL (not community id) so equivalent URL formatting maps to
* one slot and one relay's list never bleeds into another.
* Keyed per normalized relay URL and authoritative identity so identities that
* share a relay retain independent snapshots without exposing either list to
* the other.
*/
import type { Channel } from "@/shared/api/types";
@@ -17,57 +18,239 @@ import { setLocalStorageItemWithRecovery } from "@/shared/lib/localStorageQuota"
const STORAGE_KEY_PREFIX = "buzz-channels.v1";
export function channelSnapshotKey(relayUrl: string): string {
return `${STORAGE_KEY_PREFIX}:${normalizeRelayUrl(relayUrl)}`;
export type ChannelSnapshot = {
channels: Channel[];
hash: string;
};
export type ChannelSnapshotDiagnostics = {
ageMs: number;
channelCount: number;
presence: "absent" | "invalid" | "present";
serializedBytes: number;
};
export type ChannelSnapshotReadResult = {
diagnostics: ChannelSnapshotDiagnostics;
snapshot: ChannelSnapshot | null;
};
type StoredChannelSnapshot = ChannelSnapshot & {
version: 2;
updatedAt: number;
ownerPubkey: string;
integrity: string;
};
function channelSnapshotRelayPrefix(relayUrl: string): string {
return `${STORAGE_KEY_PREFIX}:${normalizeRelayUrl(relayUrl)}:`;
}
function parseChannelSnapshot(json: unknown): Channel[] | null {
export function channelSnapshotKey(
relayUrl: string,
ownerPubkey: string,
): string {
return `${channelSnapshotRelayPrefix(relayUrl)}${ownerPubkey.toLowerCase()}`;
}
function snapshotIntegrity(
ownerPubkey: string,
hash: string,
channels: Channel[],
): string {
const value = JSON.stringify([ownerPubkey.toLowerCase(), hash, channels]);
let result = 0x811c9dc5;
for (let index = 0; index < value.length; index += 1) {
result ^= value.charCodeAt(index);
result = Math.imul(result, 0x01000193);
}
return (result >>> 0).toString(16).padStart(8, "0");
}
function parseChannelSnapshot(
json: unknown,
ownerPubkey: string,
): ChannelSnapshot | null {
if (typeof json !== "object" || json === null) return null;
const obj = json as Record<string, unknown>;
if (obj.version !== 1 || !Array.isArray(obj.channels)) return null;
return obj.channels as Channel[];
// Version 1 did not record either the list hash or the owning identity. It
// cannot safely seed version 2: painting it could expose another identity's
// channels, and pairing it with any hash could suppress the corrective read.
if (
obj.version !== 2 ||
!Array.isArray(obj.channels) ||
typeof obj.hash !== "string" ||
obj.hash.length === 0 ||
typeof obj.updatedAt !== "number" ||
!Number.isFinite(obj.updatedAt) ||
typeof obj.ownerPubkey !== "string" ||
obj.ownerPubkey.toLowerCase() !== ownerPubkey.toLowerCase()
) {
return null;
}
const channels = obj.channels as Channel[];
if (
typeof obj.integrity !== "string" ||
obj.integrity !== snapshotIntegrity(ownerPubkey, obj.hash, channels)
) {
return null;
}
return { channels, hash: obj.hash };
}
function serializedBytes(value: string): number {
return new TextEncoder().encode(value).byteLength;
}
type ParsedChannelSnapshotCacheEntry = {
presence: "invalid" | "present";
raw: string;
serializedBytes: number;
snapshot: ChannelSnapshot | null;
updatedAt: number | null;
};
const parsedSnapshotCache = new Map<string, ParsedChannelSnapshotCacheEntry>();
function snapshotResultFromRaw(
key: string,
raw: string,
ownerPubkey: string,
): ChannelSnapshotReadResult {
let cached = parsedSnapshotCache.get(key);
if (!cached || cached.raw !== raw) {
let parsedJson: Record<string, unknown> | null = null;
try {
parsedJson = JSON.parse(raw) as Record<string, unknown>;
} catch {
// Malformed snapshots are invalid cache entries, never fatal reads.
}
const snapshot = parsedJson
? parseChannelSnapshot(parsedJson, ownerPubkey)
: null;
const updatedAt =
parsedJson &&
typeof parsedJson.updatedAt === "number" &&
Number.isFinite(parsedJson.updatedAt)
? parsedJson.updatedAt
: null;
cached = {
presence: snapshot ? "present" : "invalid",
raw,
serializedBytes: serializedBytes(raw),
snapshot,
updatedAt,
};
parsedSnapshotCache.set(key, cached);
}
return {
diagnostics: {
ageMs:
cached.updatedAt === null
? 0
: Math.max(0, Date.now() - cached.updatedAt),
channelCount: cached.snapshot?.channels.length ?? 0,
presence: cached.presence,
serializedBytes: cached.serializedBytes,
},
snapshot: cached.snapshot,
};
}
/**
* Reads the cached channel list for a relay, or null when absent or malformed.
* Reads the atomic snapshot and reports why it can or cannot seed the sidebar.
* Invalid includes malformed, legacy, hashless, partial, and wrong-owner data.
* Parsing and integrity validation are memoized by storage key and raw value so
* repeated hook consumers do not rescan the same multi-megabyte document.
*/
export function readChannelSnapshot(relayUrl: string): Channel[] | null {
export function inspectChannelSnapshot(
relayUrl: string,
ownerPubkey: string,
): ChannelSnapshotReadResult {
const absent: ChannelSnapshotDiagnostics = {
ageMs: 0,
channelCount: 0,
presence: "absent",
serializedBytes: 0,
};
const key = channelSnapshotKey(relayUrl, ownerPubkey);
let raw: string | null = null;
try {
const raw = window.localStorage.getItem(channelSnapshotKey(relayUrl));
if (!raw) return null;
return parseChannelSnapshot(JSON.parse(raw));
raw = window.localStorage.getItem(key);
if (!raw) return { diagnostics: absent, snapshot: null };
return snapshotResultFromRaw(key, raw, ownerPubkey);
} catch {
return null;
// `getItem` itself can throw when WebKit denies storage access. Never retry
// that read from the catch path: snapshot hydration is optional and must
// degrade to a live fetch rather than escaping into the render tree.
return {
diagnostics: {
...absent,
presence: raw === null ? "absent" : "invalid",
serializedBytes: raw === null ? 0 : serializedBytes(raw),
},
snapshot: null,
};
}
}
/**
* Persists the channel list for a relay. Skips the write when unchanged so the
* 60s background refetch does not re-serialize an identical list. Non-fatal on
* Reads the atomic channel-list/hash snapshot for an identity and relay, or
* null when absent, malformed, legacy, or owned by another identity.
*/
export function readChannelSnapshot(
relayUrl: string,
ownerPubkey: string,
): ChannelSnapshot | null {
return inspectChannelSnapshot(relayUrl, ownerPubkey).snapshot;
}
/**
* Persists the complete last successfully fetched channel list and the hash
* that describes it as one document. Atomic replacement prevents a stale hash
* from ever being paired with a newer list (or vice versa). Non-fatal on
* storage failure (e.g. quota exceeded).
*/
export function writeChannelSnapshot(
relayUrl: string,
ownerPubkey: string,
channels: Channel[],
hash: string,
): void {
try {
const key = channelSnapshotKey(relayUrl);
if (!ownerPubkey || !hash) return;
const key = channelSnapshotKey(relayUrl, ownerPubkey);
const previous = window.localStorage.getItem(key);
if (previous) {
try {
const parsed = parseChannelSnapshot(JSON.parse(previous));
if (parsed && JSON.stringify(parsed) === JSON.stringify(channels))
const parsed = parseChannelSnapshot(JSON.parse(previous), ownerPubkey);
if (
parsed &&
parsed.hash === hash &&
JSON.stringify(parsed.channels) === JSON.stringify(channels)
) {
return;
}
} catch {
// Malformed snapshots are replaced below.
}
}
const serialized = JSON.stringify({
version: 1,
const snapshot: StoredChannelSnapshot = {
version: 2,
updatedAt: Date.now(),
ownerPubkey: ownerPubkey.toLowerCase(),
hash,
channels,
});
setLocalStorageItemWithRecovery(key, serialized);
integrity: snapshotIntegrity(ownerPubkey, hash, channels),
};
setLocalStorageItemWithRecovery(key, JSON.stringify(snapshot));
} catch {
// Storage access failures are non-fatal.
}
@@ -78,7 +261,15 @@ export function writeChannelSnapshot(
*/
export function removeChannelSnapshotForRelay(relayUrl: string): void {
try {
window.localStorage.removeItem(channelSnapshotKey(relayUrl));
const prefix = channelSnapshotRelayPrefix(relayUrl);
const keys: string[] = [];
for (let index = 0; index < window.localStorage.length; index += 1) {
const key = window.localStorage.key(index);
if (key?.startsWith(prefix)) keys.push(key);
}
for (const key of keys) window.localStorage.removeItem(key);
// Also clear the superseded relay-only v1/v2 slot.
window.localStorage.removeItem(prefix.slice(0, -1));
} catch {
// Storage access failures are non-fatal.
}

View File

@@ -3,7 +3,9 @@ import test from "node:test";
import {
applyLastMessages,
canFetchChannelsForIdentity,
reconcileRefreshedCachedChannel,
requireFullChannelList,
upsertCachedChannel,
upsertCachedChannelMember,
} from "./hooks.ts";
@@ -110,6 +112,21 @@ test("reconcileRefreshedCachedChannel_restoresOpenedDmAfterStaleRefresh", () =>
assert.deepEqual(reconciled[0], openedDm);
});
test("identity failure enables a hashless live channel fetch", () => {
assert.equal(canFetchChannelsForIdentity(null, false), false);
assert.equal(canFetchChannelsForIdentity("owner-pubkey", false), true);
assert.equal(canFetchChannelsForIdentity(null, true), true);
});
test("hashless retry rejects null channels before persistence", () => {
const channels = [makeChannel("general", "General")];
assert.strictEqual(requireFullChannelList(channels), channels);
assert.throws(
() => requireFullChannelList(null),
/no list for a hashless request/,
);
});
// ── applyLastMessages ─────────────────────────────────────────────────────────
test("applyLastMessages_preservesReferenceWhenTimestampUnchanged", () => {

View File

@@ -37,7 +37,8 @@ import { useFocusedRefetchInterval } from "@/shared/lib/useDocumentVisible";
import { useCommunities } from "@/features/communities/useCommunities";
import { canAddChannelMembers } from "@/features/channels/lib/channelMemberAdmission";
import {
readChannelSnapshot,
inspectChannelSnapshot,
type ChannelSnapshot,
writeChannelSnapshot,
} from "@/features/channels/channelSnapshot";
@@ -53,12 +54,10 @@ export const channelsFocusRefetchPolicy = {
refetchOnWindowFocus: false,
} as const;
/**
* Query-cache key for the channels payload hash. Stored alongside the channel
* list so its lifecycle is tied to the channel cache — a community switch that
* evicts the channel cache implicitly invalidates the stored hash, preventing a
* stale hash from short-circuiting into an empty list.
* Authoritative server list/hash pair. Presentation mutations may patch
* `channelsQueryKey`, but may never change or be persisted with this hash.
*/
const channelsHashKey = ["channels", "_hash"] as const;
const channelsSnapshotPairKey = ["channels", "_snapshot-pair"] as const;
const channelDetailQueryKey = (channelId: string) =>
["channels", channelId, "detail"] as const;
const channelMembersQueryKey = (channelId: string) =>
@@ -88,6 +87,73 @@ function sortChannels(channels: Channel[]) {
});
}
export const CHANNELS_SNAPSHOT_DIAGNOSTIC_MARK =
"buzz:sidebar:snapshot-diagnostic";
export const CHANNELS_FULL_SIDEBAR_PAINT_MARK =
"buzz:sidebar:full-list-painted";
export const CHANNELS_BOOT_TO_FULL_SIDEBAR_MEASURE =
"buzz:sidebar:boot-to-full-list-painted";
const markedSnapshotKeys = new Set<string>();
const measuredSidebarKeys = new Set<string>();
const scheduledSidebarKeys = new Set<string>();
function sidebarMeasurementKey(relayUrl: string, ownerPubkey: string): string {
return `${relayUrl}\u0000${ownerPubkey.toLowerCase()}`;
}
function markSnapshotDiagnostic(
relayUrl: string,
ownerPubkey: string,
diagnostics: ReturnType<typeof inspectChannelSnapshot>["diagnostics"],
): void {
if (typeof performance === "undefined") return;
const key = sidebarMeasurementKey(relayUrl, ownerPubkey);
if (markedSnapshotKeys.has(key)) return;
markedSnapshotKeys.add(key);
performance.mark(CHANNELS_SNAPSHOT_DIAGNOSTIC_MARK, {
detail: { ...diagnostics, relayUrl },
});
console.info("[sidebar-perf] snapshot", { ...diagnostics, relayUrl });
}
function measureFullSidebarPaint(
relayUrl: string,
ownerPubkey: string,
channelCount: number,
): void {
if (typeof performance === "undefined") return;
const key = sidebarMeasurementKey(relayUrl, ownerPubkey);
if (measuredSidebarKeys.has(key) || scheduledSidebarKeys.has(key)) return;
scheduledSidebarKeys.add(key);
// The channels have committed to the shared query cache; two animation frames
// put the mark after React's sidebar DOM commit and the browser's next paint.
window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => {
scheduledSidebarKeys.delete(key);
if (measuredSidebarKeys.has(key)) return;
measuredSidebarKeys.add(key);
performance.mark(CHANNELS_FULL_SIDEBAR_PAINT_MARK, {
detail: { channelCount, relayUrl },
});
performance.measure(CHANNELS_BOOT_TO_FULL_SIDEBAR_MEASURE, {
detail: { channelCount, relayUrl },
duration: performance.now(),
start: 0,
});
const measure = performance
.getEntriesByName(CHANNELS_BOOT_TO_FULL_SIDEBAR_MEASURE)
.at(-1);
console.info("[sidebar-perf] full list painted", {
channelCount,
durationMs: measure?.duration,
relayUrl,
});
});
});
}
export type CachedChannelMember = {
membershipAdded: boolean;
name: string;
@@ -227,69 +293,162 @@ export function applyLastMessages(
});
}
/**
* A failed identity read must disable persisted snapshots, not the live channel
* request. The backend still resolves its authoritative current identity.
*/
export function canFetchChannelsForIdentity(
ownerPubkey: string | null,
identityReadFailed: boolean,
): boolean {
return ownerPubkey !== null || identityReadFailed;
}
/** A hashless retry must return a full list before it can become authoritative. */
export function requireFullChannelList(channels: Channel[] | null): Channel[] {
if (channels === null) {
throw new Error("get_channels returned no list for a hashless request");
}
return channels;
}
export function useChannelsQuery(options?: { enabled?: boolean }) {
const { activeCommunity } = useCommunities();
const relayUrl = activeCommunity?.relayUrl ?? null;
// CommunityQueryProvider remounts its QueryClient for every community. Only
// the active identity may authorize a persisted snapshot: Community.pubkey
// is creation-time display metadata and can be stale after identity changes.
const identityQuery = useIdentityQuery();
const ownerPubkey = identityQuery.data?.pubkey ?? null;
const queryClient = useQueryClient();
const snapshotRead = React.useMemo(
() =>
relayUrl && ownerPubkey
? inspectChannelSnapshot(relayUrl, ownerPubkey)
: null,
[ownerPubkey, relayUrl],
);
const snapshot = snapshotRead?.snapshot ?? null;
const initialSnapshotPair = React.useMemo(
() =>
snapshot
? { channels: sortChannels(snapshot.channels), hash: snapshot.hash }
: null,
[snapshot],
);
React.useEffect(() => {
if (relayUrl && ownerPubkey && snapshotRead && options?.enabled !== false) {
markSnapshotDiagnostic(relayUrl, ownerPubkey, snapshotRead.diagnostics);
}
}, [options?.enabled, ownerPubkey, relayUrl, snapshotRead]);
const refetchInterval = useFocusedRefetchInterval(
CHANNELS_REFETCH_INTERVAL_MS,
);
const queryClient = useQueryClient();
return useQuery({
enabled: options?.enabled ?? true,
const query = useQuery({
enabled:
(options?.enabled ?? true) &&
relayUrl !== null &&
canFetchChannelsForIdentity(ownerPubkey, identityQuery.isError),
queryKey: channelsQueryKey,
queryFn: async () => {
// Supply the stored hash only when the channel list is still in cache.
// If cache is absent (community switch, eviction) we send null so the
// Rust side never short-circuits into an empty list.
const cachedChannels =
queryClient.getQueryData<Channel[]>(channelsQueryKey);
const knownHash = cachedChannels
? (queryClient.getQueryData<string>(channelsHashKey) ?? null)
: null;
// Revalidation uses only an authoritative list/hash pair. The displayed
// channels cache is intentionally ignored because successful mutations
// patch it before the relay's list/hash has necessarily caught up.
const cachedPair =
queryClient.getQueryData<ChannelSnapshot>(channelsSnapshotPairKey) ??
initialSnapshotPair;
const knownHash = cachedPair?.hash ?? null;
const payload = await getChannels(knownHash);
// Pin the hash alongside the channel cache so it is implicitly
// invalidated whenever the channel list is evicted.
queryClient.setQueryData(channelsHashKey, payload.hash);
// A not-modified response is usable only when it echoes the exact hash
// that described the available list. Any other hash/list pairing fails
// slow-never-wrong by retrying without a hash.
const hasMatchingNotModifiedResponse =
payload.channels === null &&
knownHash !== null &&
payload.hash === knownHash;
const pairChannels =
payload.channels ??
(hasMatchingNotModifiedResponse ? cachedPair?.channels : undefined);
// Determine the base channel list: full payload on a normal response,
// or the still-valid cached list on a not-modified (channels === null) response.
const base = payload.channels ?? cachedChannels;
if (!base) {
// hash-match but cache somehow empty — shouldn't happen given the
// null-hash guard above, but handle defensively by re-fetching without
// the stale hash so we always have a channel list to return.
if (!pairChannels) {
// Missing cache or a mismatched not-modified response: discard the hash
// and fetch a complete authoritative list before updating persistence.
const full = await getChannels(null);
queryClient.setQueryData(channelsHashKey, full.hash);
const sorted = sortChannels(
applyLastMessages(full.channels ?? [], full.lastMessages),
applyLastMessages(
requireFullChannelList(full.channels),
full.lastMessages,
),
);
if (relayUrl) writeChannelSnapshot(relayUrl, sorted);
const pair = { channels: sorted, hash: full.hash };
queryClient.setQueryData(channelsSnapshotPairKey, pair);
if (relayUrl && ownerPubkey) {
writeChannelSnapshot(relayUrl, ownerPubkey, pair.channels, pair.hash);
}
return sorted;
}
const sorted = sortChannels(
applyLastMessages(base, payload.lastMessages),
const authoritativeChannels = sortChannels(
applyLastMessages(pairChannels, payload.lastMessages),
);
if (relayUrl) writeChannelSnapshot(relayUrl, sorted);
const pair = {
channels: authoritativeChannels,
hash: payload.hash,
};
queryClient.setQueryData(channelsSnapshotPairKey, pair);
// A matching not-modified result must merge timestamps into whatever is
// displayed at completion time. Reading through setQueryData avoids
// clobbering an optimistic mutation that landed while the request ran.
const sorted =
payload.channels === null
? (queryClient.setQueryData<Channel[]>(
channelsQueryKey,
(displayedChannels) =>
sortChannels(
applyLastMessages(
displayedChannels ?? authoritativeChannels,
payload.lastMessages,
),
),
) ?? authoritativeChannels)
: authoritativeChannels;
if (relayUrl && ownerPubkey) {
writeChannelSnapshot(relayUrl, ownerPubkey, pair.channels, pair.hash);
}
return sorted;
},
// Paint the sidebar instantly from the last-known list for this relay, then
// revalidate. initialDataUpdatedAt:0 marks the seed as already-stale so the
// background refetch still fires immediately.
initialData: relayUrl
? () => {
const snapshot = readChannelSnapshot(relayUrl);
return snapshot ? sortChannels(snapshot) : undefined;
}
: undefined,
// Paint the complete persisted list immediately. `initialDataUpdatedAt: 0`
// deliberately keeps it stale so every boot still validates against the
// relay; queryFn reads the matching hash from the same atomic document.
initialData: initialSnapshotPair?.channels,
initialDataUpdatedAt: 0,
refetchInterval,
...channelsFocusRefetchPolicy,
});
React.useEffect(() => {
if (
relayUrl &&
ownerPubkey &&
query.isSuccess &&
query.fetchStatus === "idle" &&
query.dataUpdatedAt > 0
) {
measureFullSidebarPaint(relayUrl, ownerPubkey, query.data.length);
}
}, [
query.data,
query.dataUpdatedAt,
query.fetchStatus,
query.isSuccess,
ownerPubkey,
relayUrl,
]);
return query;
}
export function useCreateChannelMutation() {

View File

@@ -313,6 +313,10 @@ type E2eConfig = {
/** Reject successive mock `join_channel` calls, then resume. */
joinChannelErrors?: string[];
channelsReadDelayMs?: number;
/** Return not-modified for this many reads before resuming full payloads. */
channelsNotModifiedResponses?: number;
/** When true, a matching knownHash returns a not-modified channel payload. */
honorChannelsKnownHash?: boolean;
/** Number of seeded rows in the deep-history fixture. Defaults to 600. */
deepHistoryMessageCount?: number;
feedReadError?: string;
@@ -5709,7 +5713,10 @@ function buildLastMessages(
return map;
}
async function handleGetChannels(config: E2eConfig | undefined) {
async function handleGetChannels(
payload: unknown,
config: E2eConfig | undefined,
) {
const channelsReadDelayMs = config?.mock?.channelsReadDelayMs ?? 0;
if (channelsReadDelayMs > 0) {
await new Promise((resolve) =>
@@ -5726,15 +5733,25 @@ async function handleGetChannels(config: E2eConfig | undefined) {
const identity = getIdentity(config);
if (!identity) {
// The hash is constant ("mock-hash") and channels are always returned in
// full — the not-modified short-circuit is intentionally never exercised
// here: mock channel data mutates during tests (last_message_at updates on
// message emission) while the hash never changes, so honoring knownHash
// would wrongly short-circuit after mutations.
// The hash is constant ("mock-hash") and full lists remain the default:
// mock channel data mutates during tests while the hash does not. Focused
// snapshot specs can opt into the not-modified branch explicitly.
const channels = listMockChannels(config);
const hash = "mock-hash";
const knownHash = (payload as { knownHash?: unknown } | null)?.knownHash;
const forcedNotModified =
(config?.mock?.channelsNotModifiedResponses ?? 0) > 0;
if (forcedNotModified && config?.mock) {
config.mock.channelsNotModifiedResponses =
(config.mock.channelsNotModifiedResponses ?? 1) - 1;
}
return {
hash: "mock-hash",
channels,
hash,
channels:
forcedNotModified ||
(config?.mock?.honorChannelsKnownHash && knownHash === hash)
? null
: channels,
last_messages: buildLastMessages(channels),
};
}
@@ -11939,7 +11956,7 @@ export function maybeInstallE2eTauriMocks() {
// invocation's completion. Later reads pass through and cannot join it.
const deferred = deferNextChannelsRead;
deferNextChannelsRead = false;
const channels = handleGetChannels(activeConfig);
const channels = handleGetChannels(payload, activeConfig);
if (deferred) {
await new Promise<void>((resolve) => {
deferredChannelsReadResolve = resolve;

View File

@@ -4,6 +4,11 @@ import { installMockBridge } from "../helpers/bridge";
import { FEATURE_OVERRIDES_STORAGE_KEY } from "../helpers/features";
const RELAY_URL = "ws://localhost:3000";
const OWNER_PUBKEY = "deadbeef".repeat(8);
function snapshotKey(relayUrl: string) {
return `buzz-channels.v1:${relayUrl}:${OWNER_PUBKEY.toLowerCase()}`;
}
const COMMUNITY_A = {
id: "ws-a",
@@ -412,33 +417,36 @@ test.describe("community rail", () => {
await seedCommunities(page, [COMMUNITY_A, COMMUNITY_B], COMMUNITY_A.id);
await page.goto("/");
await expect(page.getByTestId("app-sidebar")).toBeVisible();
const rememberedChannelId = await page.evaluate((communityId) => {
const source = window.localStorage.getItem(
"buzz-channels.v1:ws://localhost:3000",
);
if (!source) throw new Error("missing source channel snapshot");
const snapshot = JSON.parse(source) as {
channels: Array<{ id: string; name: string }>;
};
const generalChannel = snapshot.channels.find(
(channel) => channel.name === "general",
);
if (!generalChannel) throw new Error("missing general channel snapshot");
window.localStorage.setItem(
"buzz-channels.v1:ws://localhost:3001",
source,
);
window.localStorage.setItem(
"buzz-community-destinations",
JSON.stringify({
[communityId]: {
kind: "channel",
channelId: generalChannel.id,
},
}),
);
return generalChannel.id;
}, COMMUNITY_B.id);
const rememberedChannelId = await page.evaluate(
({ communityId, sourceSnapshotKey, targetSnapshotKey }) => {
const source = window.localStorage.getItem(sourceSnapshotKey);
if (!source) throw new Error("missing source channel snapshot");
const snapshot = JSON.parse(source) as {
channels: Array<{ id: string; name: string }>;
};
const generalChannel = snapshot.channels.find(
(channel) => channel.name === "general",
);
if (!generalChannel)
throw new Error("missing general channel snapshot");
window.localStorage.setItem(targetSnapshotKey, source);
window.localStorage.setItem(
"buzz-community-destinations",
JSON.stringify({
[communityId]: {
kind: "channel",
channelId: generalChannel.id,
},
}),
);
return generalChannel.id;
},
{
communityId: COMMUNITY_B.id,
sourceSnapshotKey: snapshotKey(COMMUNITY_A.relayUrl),
targetSnapshotKey: snapshotKey(COMMUNITY_B.relayUrl),
},
);
await page.evaluate(() => {
const testWindow = window as typeof window & {
@@ -478,28 +486,29 @@ test.describe("community rail", () => {
}, COMMUNITY_B.id);
await page.goto("/");
const sourceSnapshotKey = snapshotKey(COMMUNITY_A.relayUrl);
const targetSnapshotKey = snapshotKey(COMMUNITY_B.relayUrl);
await expect
.poll(() =>
page.evaluate(() =>
window.localStorage.getItem("buzz-channels.v1:ws://localhost:3000"),
page.evaluate(
(key) => window.localStorage.getItem(key),
sourceSnapshotKey,
),
)
.not.toBeNull();
await page.evaluate(() => {
const source = window.localStorage.getItem(
"buzz-channels.v1:ws://localhost:3000",
);
if (!source) throw new Error("missing source channel snapshot");
const snapshot = JSON.parse(source);
snapshot.channels = snapshot.channels.map(
(channel: Record<string, unknown>, index: number) =>
index === 0 ? { ...channel, id: "missing-channel" } : channel,
);
window.localStorage.setItem(
"buzz-channels.v1:ws://localhost:3001",
JSON.stringify(snapshot),
);
});
await page.evaluate(
({ sourceKey, targetKey }) => {
const source = window.localStorage.getItem(sourceKey);
if (!source) throw new Error("missing source channel snapshot");
const snapshot = JSON.parse(source);
snapshot.channels = snapshot.channels.map(
(channel: Record<string, unknown>, index: number) =>
index === 0 ? { ...channel, id: "missing-channel" } : channel,
);
window.localStorage.setItem(targetKey, JSON.stringify(snapshot));
},
{ sourceKey: sourceSnapshotKey, targetKey: targetSnapshotKey },
);
await page.getByTestId(`community-rail-button-${COMMUNITY_B.id}`).click();
await expect(page).not.toHaveURL(/#\/channels\//);
@@ -531,10 +540,12 @@ test.describe("community rail", () => {
}, COMMUNITY_B.id);
await page.goto("/");
await expect(page.getByTestId("app-sidebar")).toBeVisible();
const sourceSnapshotKey = snapshotKey(COMMUNITY_A.relayUrl);
await expect
.poll(() =>
page.evaluate(() =>
window.localStorage.getItem("buzz-channels.v1:ws://localhost:3000"),
page.evaluate(
(key) => window.localStorage.getItem(key),
sourceSnapshotKey,
),
)
.not.toBeNull();
@@ -563,20 +574,21 @@ test.describe("community rail", () => {
};
}),
).toEqual({ released: 0, pending: 0 });
await page.evaluate(() => {
const source = window.localStorage.getItem(
"buzz-channels.v1:ws://localhost:3000",
);
if (!source) throw new Error("missing source channel snapshot");
const snapshot = JSON.parse(source);
snapshot.channels = snapshot.channels.filter(
(channel: { id: string }) => channel.id !== "general",
);
window.localStorage.setItem(
"buzz-channels.v1:ws://localhost:3001",
JSON.stringify(snapshot),
);
});
await page.evaluate(
({ sourceKey, targetKey }) => {
const source = window.localStorage.getItem(sourceKey);
if (!source) throw new Error("missing source channel snapshot");
const snapshot = JSON.parse(source);
snapshot.channels = snapshot.channels.filter(
(channel: { id: string }) => channel.id !== "general",
);
window.localStorage.setItem(targetKey, JSON.stringify(snapshot));
},
{
sourceKey: sourceSnapshotKey,
targetKey: snapshotKey(COMMUNITY_B.relayUrl),
},
);
await page.evaluate(() => {
const config = (
window as Window & {

View File

@@ -0,0 +1,599 @@
import { expect, test, type Page } from "@playwright/test";
import { installMockBridge } from "../helpers/bridge";
const RELAY_URL = "ws://localhost:3000";
const OTHER_RELAY_URL = "ws://localhost:3001";
const OWNER_PUBKEY = "deadbeef".repeat(8);
const STALE_COMMUNITY_PUBKEY = "cafebabe".repeat(8);
const MATCHING_HASH = "mock-hash";
const READ_DELAY_MS = 600;
const SNAPSHOT_FRAME_DELAY_MS = 3_000;
function snapshotKey(relayUrl: string, ownerPubkey = OWNER_PUBKEY) {
return `buzz-channels.v1:${relayUrl}:${ownerPubkey.toLowerCase()}`;
}
function makeSnapshotChannel(index: number, prefix = "snapshot") {
return {
id: `${prefix}-${index}`,
name: `${prefix}-${String(index).padStart(2, "0")}`,
channelType: "stream",
visibility: "open",
description: "",
topic: null,
purpose: null,
memberCount: 1,
memberPubkeys: [OWNER_PUBKEY],
lastMessageAt: null,
archivedAt: null,
participants: [],
participantPubkeys: [],
isMember: true,
ttlSeconds: null,
ttlDeadline: null,
};
}
const FULL_SNAPSHOT = Array.from({ length: 14 }, (_, index) =>
makeSnapshotChannel(index),
);
const SNAPSHOT_DIAGNOSTIC_MARK = "buzz:sidebar:snapshot-diagnostic";
const FULL_SIDEBAR_PAINT_MARK = "buzz:sidebar:full-list-painted";
const BOOT_TO_FULL_SIDEBAR_MEASURE = "buzz:sidebar:boot-to-full-list-painted";
function snapshotIntegrity(
ownerPubkey: string,
hash: string,
channels: ReturnType<typeof makeSnapshotChannel>[],
) {
const value = JSON.stringify([ownerPubkey.toLowerCase(), hash, channels]);
let result = 0x811c9dc5;
for (let index = 0; index < value.length; index += 1) {
result ^= value.charCodeAt(index);
result = Math.imul(result, 0x01000193);
}
return (result >>> 0).toString(16).padStart(8, "0");
}
type SnapshotDiagnostics = {
ageMs: number;
channelCount: number;
presence: "absent" | "invalid" | "present";
serializedBytes: number;
};
async function getSnapshotDiagnostic(page: Page) {
return page.evaluate((name) => {
const entry = performance.getEntriesByName(name).at(-1) as
| PerformanceMark
| undefined;
return entry?.detail as SnapshotDiagnostics | undefined;
}, SNAPSHOT_DIAGNOSTIC_MARK);
}
async function getFullSidebarMeasure(page: Page) {
return page.evaluate(
({ markName, measureName }) => ({
markCount: performance.getEntriesByName(markName).length,
measure: performance.getEntriesByName(measureName).at(-1)?.duration,
}),
{
markName: FULL_SIDEBAR_PAINT_MARK,
measureName: BOOT_TO_FULL_SIDEBAR_MEASURE,
},
);
}
async function seedSnapshot(
page: Page,
options: {
channels?: ReturnType<typeof makeSnapshotChannel>[];
hash: string;
keyOwnerPubkey?: string;
ownerPubkey?: string;
relayUrl?: string;
},
) {
const channels = options.channels ?? FULL_SNAPSHOT;
const ownerPubkey = options.ownerPubkey ?? OWNER_PUBKEY;
await page.addInitScript(
({ channels, hash, integrity, key, ownerPubkey }) => {
window.localStorage.setItem(
key,
JSON.stringify({
version: 2,
updatedAt: Date.now(),
ownerPubkey,
hash,
channels,
integrity,
}),
);
},
{
channels,
hash: options.hash,
integrity: snapshotIntegrity(ownerPubkey, options.hash, channels),
key: snapshotKey(
options.relayUrl ?? RELAY_URL,
options.keyOwnerPubkey ?? ownerPubkey,
),
ownerPubkey,
},
);
}
async function seedCommunities(page: Page, communityPubkey = OWNER_PUBKEY) {
await page.addInitScript(
({ communityPubkey, relayA, relayB }) => {
const communities = [
{
id: "community-a",
name: "Alpha",
relayUrl: relayA,
pubkey: communityPubkey,
addedAt: "2026-01-01T00:00:00.000Z",
},
{
id: "community-b",
name: "Bravo",
relayUrl: relayB,
pubkey: communityPubkey,
addedAt: "2026-01-02T00:00:00.000Z",
},
];
window.localStorage.setItem(
"buzz-communities",
JSON.stringify(communities),
);
window.localStorage.setItem(
"buzz-active-community-id",
communities[0].id,
);
},
{ communityPubkey, relayA: RELAY_URL, relayB: OTHER_RELAY_URL },
);
}
async function getChannelsPayloads(page: Page) {
return page.evaluate(() =>
(window.__BUZZ_E2E_COMMAND_LOG__ ?? [])
.filter((entry) => entry.command === "get_channels")
.map((entry) => entry.payload as { knownHash?: unknown }),
);
}
async function mutateDisplayedChannels(page: Page, channelName: string) {
await page.evaluate((name) => {
const queryClient = window.__BUZZ_E2E_QUERY_CLIENT__ as
| {
setQueryData: (
queryKey: readonly string[],
updater: (
channels: ReturnType<typeof makeSnapshotChannel>[] | undefined,
) => ReturnType<typeof makeSnapshotChannel>[],
) => unknown;
}
| undefined;
if (!queryClient) {
throw new Error("E2E query client seam is not installed.");
}
queryClient.setQueryData(["channels"], (channels) => {
const template = channels?.[0];
if (!template) {
throw new Error("Displayed channel cache is empty.");
}
return [...channels, { ...template, id: "optimistic-channel", name }];
});
}, channelName);
}
async function readPersistedSnapshot(page: Page) {
return page.evaluate((key) => {
const raw = window.localStorage.getItem(key);
if (!raw) return null;
const snapshot = JSON.parse(raw) as {
channels: Array<{ name: string }>;
hash: string;
};
return {
channelNames: snapshot.channels.map((channel) => channel.name),
hash: snapshot.hash,
};
}, snapshotKey(RELAY_URL));
}
async function trackSnapshotRows(page: Page) {
await page.addInitScript(() => {
const testWindow = window as Window & {
__BUZZ_E2E_SNAPSHOT_ROWS_SEEN__?: string[];
};
const seen: string[] = [];
testWindow.__BUZZ_E2E_SNAPSHOT_ROWS_SEEN__ = seen;
const record = (node: Node) => {
if (!(node instanceof Element)) return;
const rows = node.matches('[data-channel-id^="snapshot-"]')
? [node]
: Array.from(node.querySelectorAll('[data-channel-id^="snapshot-"]'));
for (const row of rows) {
const id = row.getAttribute("data-channel-id");
if (id && !seen.includes(id)) seen.push(id);
}
};
new MutationObserver((records) => {
for (const mutation of records) {
for (const node of mutation.addedNodes) record(node);
}
}).observe(document, { childList: true, subtree: true });
});
}
async function getTrackedSnapshotRows(page: Page) {
return page.evaluate(
() =>
(
window as Window & {
__BUZZ_E2E_SNAPSHOT_ROWS_SEEN__?: string[];
}
).__BUZZ_E2E_SNAPSHOT_ROWS_SEEN__ ?? [],
);
}
test("cold boot paints the complete snapshot, sends its hash, and revalidates", async ({
page,
}) => {
await seedSnapshot(page, { hash: MATCHING_HASH });
await installMockBridge(page, {
channelsReadDelayMs: SNAPSHOT_FRAME_DELAY_MS,
honorChannelsKnownHash: true,
});
const navigationStartedAt = performance.now();
await page.goto("/");
const rows = page.locator('[data-channel-id^="snapshot-"]');
await expect(rows).toHaveCount(FULL_SNAPSHOT.length, { timeout: 2_000 });
const snapshotPaintedAt = performance.now();
expect(snapshotPaintedAt - navigationStartedAt).toBeLessThan(
SNAPSHOT_FRAME_DELAY_MS,
);
await expect(page.getByTestId("sidebar-loading")).toHaveCount(0);
await expect
.poll(() => getChannelsPayloads(page))
.toEqual([{ knownHash: MATCHING_HASH }]);
await expect
.poll(() => getSnapshotDiagnostic(page))
.toMatchObject({
channelCount: FULL_SNAPSHOT.length,
presence: "present",
});
const diagnostics = await getSnapshotDiagnostic(page);
expect(diagnostics?.serializedBytes).toBeGreaterThan(0);
expect(diagnostics?.ageMs).toBeGreaterThanOrEqual(0);
// The persisted data is deliberately stale: it paints immediately but still
// validates. The delayed matching response leaves a deterministic snapshot
// frame, then not-modified must not shrink the list.
await expect(rows).toHaveCount(FULL_SNAPSHOT.length);
await expect
.poll(() => getFullSidebarMeasure(page))
.toEqual({
markCount: expect.any(Number),
measure: expect.any(Number),
});
expect((await getFullSidebarMeasure(page)).markCount).toBeGreaterThan(0);
const liveSettledAt = performance.now();
console.info(
`[sidebar-snapshot] firstPaint=${Math.round(snapshotPaintedAt - navigationStartedAt)}ms snapshotToLive=${Math.round(liveSettledAt - snapshotPaintedAt)}ms rows=${FULL_SNAPSHOT.length}`,
);
});
test("matching not-modified preserves display mutations without persisting them", async ({
page,
}) => {
const optimisticName = "optimistic-local-channel";
await seedSnapshot(page, { hash: MATCHING_HASH });
await installMockBridge(page, {
channelsReadDelayMs: READ_DELAY_MS,
honorChannelsKnownHash: true,
});
await page.goto("/");
await expect(page.locator('[data-channel-id^="snapshot-"]')).toHaveCount(
FULL_SNAPSHOT.length,
{ timeout: 500 },
);
await mutateDisplayedChannels(page, optimisticName);
await expect(
page.locator('[data-channel-id="optimistic-channel"]'),
).toBeVisible();
await expect
.poll(() => getChannelsPayloads(page))
.toEqual([{ knownHash: MATCHING_HASH }]);
await expect
.poll(() => readPersistedSnapshot(page))
.toEqual({
channelNames: FULL_SNAPSHOT.map((channel) => channel.name),
hash: MATCHING_HASH,
});
await expect(
page.locator('[data-channel-id="optimistic-channel"]'),
).toBeVisible();
expect((await readPersistedSnapshot(page))?.channelNames).not.toContain(
optimisticName,
);
});
test("first-ever boot without a snapshot sends null and shows loading", async ({
page,
}) => {
await installMockBridge(page, { channelsReadDelayMs: READ_DELAY_MS });
await page.goto("/");
await expect(page.getByTestId("sidebar-loading")).toBeVisible({
timeout: 500,
});
await expect(page.locator('[data-channel-id^="snapshot-"]')).toHaveCount(0);
await expect
.poll(() => getChannelsPayloads(page))
.toEqual([{ knownHash: null }]);
await expect
.poll(() => getSnapshotDiagnostic(page))
.toMatchObject({
channelCount: 0,
presence: "absent",
serializedBytes: 0,
});
await expect(page.getByTestId("channel-general")).toBeVisible();
await expect
.poll(() => getFullSidebarMeasure(page))
.toEqual({
markCount: expect.any(Number),
measure: expect.any(Number),
});
expect((await getFullSidebarMeasure(page)).markCount).toBeGreaterThan(0);
});
test("a different identity's snapshot is ignored", async ({ page }) => {
await seedSnapshot(page, {
hash: "foreign-hash",
keyOwnerPubkey: OWNER_PUBKEY,
ownerPubkey: STALE_COMMUNITY_PUBKEY,
});
await installMockBridge(page, { channelsReadDelayMs: READ_DELAY_MS });
await page.goto("/");
await expect(page.getByTestId("sidebar-loading")).toBeVisible({
timeout: 500,
});
await expect(page.locator('[data-channel-id^="snapshot-"]')).toHaveCount(0);
await expect
.poll(() => getChannelsPayloads(page))
.toEqual([{ knownHash: null }]);
await expect
.poll(() => getSnapshotDiagnostic(page))
.toMatchObject({
channelCount: 0,
presence: "invalid",
});
await expect(page.getByTestId("channel-general")).toBeVisible();
});
test("display-only community pubkey cannot expose another identity's snapshot", async ({
page,
}) => {
await seedSnapshot(page, {
hash: "stale-community-identity-hash",
ownerPubkey: STALE_COMMUNITY_PUBKEY,
});
await trackSnapshotRows(page);
await installMockBridge(
page,
{ channelsReadDelayMs: READ_DELAY_MS },
{ skipCommunitySeed: true },
);
await seedCommunities(page, STALE_COMMUNITY_PUBKEY);
await page.goto("/");
await expect(page.getByTestId("channel-general")).toBeVisible();
expect(await getTrackedSnapshotRows(page)).toEqual([]);
const payloads = await getChannelsPayloads(page);
expect(payloads).toEqual([{ knownHash: null }]);
expect(payloads).not.toContainEqual({
knownHash: "stale-community-identity-hash",
});
});
test("partial hash/list write fails toward a full fetch", async ({ page }) => {
await seedSnapshot(page, { hash: MATCHING_HASH });
await page.addInitScript((key) => {
const raw = window.localStorage.getItem(key);
if (!raw) return;
const snapshot = JSON.parse(raw) as { hash: string };
window.localStorage.setItem(
key,
JSON.stringify({ ...snapshot, hash: "newer-partial-hash" }),
);
}, snapshotKey(RELAY_URL));
await installMockBridge(page, {
channelsReadDelayMs: READ_DELAY_MS,
honorChannelsKnownHash: true,
});
await page.goto("/");
await expect(page.getByTestId("sidebar-loading")).toBeVisible({
timeout: 500,
});
await expect(page.locator('[data-channel-id^="snapshot-"]')).toHaveCount(0);
await expect
.poll(() => getChannelsPayloads(page))
.toEqual([{ knownHash: null }]);
await expect
.poll(() => getSnapshotDiagnostic(page))
.toMatchObject({
channelCount: 0,
presence: "invalid",
});
await expect(page.getByTestId("channel-general")).toBeVisible();
await expect(page.locator('[data-channel-id^="snapshot-"]')).toHaveCount(0);
});
test("unexpected not-modified response retries without a hash", async ({
page,
}) => {
await installMockBridge(page, {
channelsNotModifiedResponses: 1,
});
await page.goto("/");
await expect(page.getByTestId("channel-general")).toBeVisible();
await expect
.poll(() => getChannelsPayloads(page))
.toEqual([{ knownHash: null }, { knownHash: null }]);
});
test("mismatched not-modified hash falls back to a full list", async ({
page,
}) => {
await seedSnapshot(page, { hash: "persisted-stale-hash" });
await installMockBridge(page, {
channelsReadDelayMs: READ_DELAY_MS,
channelsNotModifiedResponses: 1,
});
await page.goto("/");
const snapshotRows = page.locator('[data-channel-id^="snapshot-"]');
await expect(snapshotRows).toHaveCount(FULL_SNAPSHOT.length, {
timeout: 500,
});
await expect
.poll(() => getChannelsPayloads(page))
.toEqual([{ knownHash: "persisted-stale-hash" }, { knownHash: null }]);
// The stale snapshot may provide the immediate boot frame, but an impossible
// not-modified hash/list pairing must never survive as the settled result.
await expect(page.getByTestId("channel-general")).toBeVisible();
await expect(snapshotRows).toHaveCount(0);
await expect
.poll(() =>
page.evaluate((key) => {
const raw = window.localStorage.getItem(key);
if (!raw) return null;
const snapshot = JSON.parse(raw) as {
channels: Array<{ name: string }>;
hash: string;
};
return {
hash: snapshot.hash,
hasGeneral: snapshot.channels.some(
(channel) => channel.name === "general",
),
hasSnapshotRow: snapshot.channels.some((channel) =>
channel.name.startsWith("snapshot-"),
),
};
}, snapshotKey(RELAY_URL)),
)
.toEqual({ hash: MATCHING_HASH, hasGeneral: true, hasSnapshotRow: false });
});
test("hash mismatch replaces the snapshot with the full live list", async ({
page,
}) => {
await seedSnapshot(page, { hash: "stale-hash" });
await installMockBridge(page, {
channelsReadDelayMs: READ_DELAY_MS,
honorChannelsKnownHash: true,
});
await page.goto("/");
await expect(page.locator('[data-channel-id^="snapshot-"]')).toHaveCount(
FULL_SNAPSHOT.length,
{ timeout: 500 },
);
await expect
.poll(() => getChannelsPayloads(page))
.toEqual([{ knownHash: "stale-hash" }]);
await expect(page.getByTestId("channel-general")).toBeVisible();
await expect(page.locator('[data-channel-id^="snapshot-"]')).toHaveCount(0);
await expect
.poll(() =>
page.evaluate((key) => {
const raw = window.localStorage.getItem(key);
if (!raw) return null;
const snapshot = JSON.parse(raw) as {
channels: Array<{ name: string }>;
hash: string;
};
return {
hash: snapshot.hash,
hasGeneral: snapshot.channels.some(
(channel) => channel.name === "general",
),
hasSnapshotRow: snapshot.channels.some((channel) =>
channel.name.startsWith("snapshot-"),
),
};
}, snapshotKey(RELAY_URL)),
)
.toEqual({ hash: MATCHING_HASH, hasGeneral: true, hasSnapshotRow: false });
});
test("community switch validates a stale relay snapshot and replaces it", async ({
page,
}) => {
const switchedSnapshot = Array.from({ length: 6 }, (_, index) =>
makeSnapshotChannel(index, "switched"),
);
await seedSnapshot(page, {
channels: switchedSnapshot,
hash: "stale-community-hash",
relayUrl: OTHER_RELAY_URL,
});
await installMockBridge(
page,
{
channelsReadDelayMs: READ_DELAY_MS,
honorChannelsKnownHash: true,
},
{ skipCommunitySeed: true },
);
await seedCommunities(page);
await page.goto("/");
await expect(page.getByTestId("app-sidebar")).toBeVisible();
const payloadCountBeforeSwitch = (await getChannelsPayloads(page)).length;
await page.getByTestId("community-rail-button-community-b").click();
const switchedRows = page.locator('[data-channel-id^="switched-"]');
await expect(switchedRows).toHaveCount(switchedSnapshot.length, {
timeout: 500,
});
await expect
.poll(async () =>
(await getChannelsPayloads(page)).slice(payloadCountBeforeSwitch).at(0),
)
.toEqual({ knownHash: "stale-community-hash" });
await expect(page.getByTestId("channel-general")).toBeVisible();
await expect(switchedRows).toHaveCount(0);
await expect
.poll(() =>
page.evaluate((key) => {
const raw = window.localStorage.getItem(key);
if (!raw) return null;
const snapshot = JSON.parse(raw) as {
channels: Array<{ name: string }>;
hash: string;
};
return {
hash: snapshot.hash,
hasGeneral: snapshot.channels.some(
(channel) => channel.name === "general",
),
};
}, snapshotKey(OTHER_RELAY_URL)),
)
.toEqual({ hash: MATCHING_HASH, hasGeneral: true });
});

View File

@@ -267,6 +267,11 @@ type MockBridgeOptions = {
ensureStarterChannelsErrors?: string[];
/** Reject successive mock `join_channel` calls, then resume. */
joinChannelErrors?: string[];
channelsReadDelayMs?: number;
/** Return not-modified for this many reads before resuming full payloads. */
channelsNotModifiedResponses?: number;
/** When true, a matching knownHash returns a not-modified channel payload. */
honorChannelsKnownHash?: boolean;
/** Number of seeded rows in the deep-history fixture. Defaults to 600. */
deepHistoryMessageCount?: number;
feedReadError?: string;