Files
buzz/desktop/src/shared/lib/localStorageQuota.test.mjs
Luke Tornquist d0d4acd4fa fix(desktop): show cached display names on startup (#3317)
## Why

Buzz restores cached channels and messages before profile lookups
complete. On launch, that briefly exposes pubkey-derived labels in place
of familiar display names.

## What

- Persist a bounded, relay-scoped cache of last-known display names,
NIP-01 names, and NIP-05 handles
- Seed batch profile queries from those labels immediately, while
keeping them stale so the existing relay request revalidates them
- Keep cached data presentation-only: avatars and ownership metadata are
not persisted or used to seed profile-detail caches
- Remove cleared or missing profiles, purge a relay's labels when its
community is removed, and include the cache in local-storage quota
recovery
- Add unit coverage for parsing, bounds, eviction, malformed data, and
cleared profiles
- Add an E2E regression that delays the relay profile response and
verifies the cached name is rendered first

## Risk Assessment

Low. The cache is disposable, capped at 1,000 entries per relay, scoped
by normalized relay URL, and always revalidated. It contains only public
label fields and does not restore avatars, agent ownership, or
authorization state.

## Verification

- `just ci`
- `pnpm typecheck`
- `pnpm test` — 3,727 passed
- `pnpm exec playwright test tests/e2e/channels.spec.ts --grep "cached
profile labels"` — passed

Generated with Codex
2026-08-04 11:36:42 -04:00

205 lines
6.7 KiB
JavaScript

import assert from "node:assert/strict";
import test from "node:test";
import {
recoverLocalStorageQuotaOnStartup,
setLocalStorageItemWithRecovery,
} from "./localStorageQuota.ts";
function makeQuotaLocalStorage({ maxEntries }) {
const store = new Map();
return {
store,
get length() {
return store.size;
},
key: (i) => [...store.keys()][i] ?? null,
getItem: (key) => store.get(key) ?? null,
setItem(key, value) {
if (!store.has(key) && store.size >= maxEntries) {
throw new Error("QuotaExceededError");
}
store.set(key, value);
},
removeItem: (key) => store.delete(key),
};
}
function install(ls) {
if (typeof globalThis.window === "undefined") {
globalThis.window = {};
}
globalThis.window.localStorage = ls;
globalThis.localStorage = ls;
}
test("startup recovery removes disposable caches but preserves user state", () => {
const ls = makeQuotaLocalStorage({ maxEntries: 6 });
install(ls);
ls.store.set("buzz-channel-messages.v1:relay:chan", "big");
ls.store.set("buzz-channels.v1:relay", "big");
ls.store.set("buzz-timeline-skeleton-shape.v1:chan", "small");
ls.store.set("buzz-sidebar-skeleton-shape.v1:community:user", "small");
ls.store.set("buzz-user-labels.v1:relay", "small");
ls.store.set("buzz-communities", "keep");
recoverLocalStorageQuotaOnStartup();
assert.equal(ls.getItem("buzz-channel-messages.v1:relay:chan"), null);
assert.equal(ls.getItem("buzz-channels.v1:relay"), null);
assert.equal(ls.getItem("buzz-timeline-skeleton-shape.v1:chan"), null);
assert.equal(
ls.getItem("buzz-sidebar-skeleton-shape.v1:community:user"),
null,
);
assert.equal(ls.getItem("buzz-user-labels.v1:relay"), null);
assert.equal(ls.getItem("buzz-communities"), "keep");
assert.equal(ls.getItem("buzz-local-storage-quota-recovery.v1"), "1");
});
test("healthy startup preserves disposable caches", () => {
const ls = makeQuotaLocalStorage({ maxEntries: 10 });
install(ls);
ls.store.set("buzz-channel-messages.v1:relay:new", "snapshot");
recoverLocalStorageQuotaOnStartup();
assert.equal(ls.getItem("buzz-channel-messages.v1:relay:new"), "snapshot");
assert.equal(ls.getItem("buzz-local-storage-quota-recovery.v1"), "1");
});
test("startup recovery does not remove namespace near misses", () => {
const ls = makeQuotaLocalStorage({ maxEntries: 2 });
install(ls);
ls.store.set("buzz-channels.v10:durable", "keep");
ls.store.set("buzz-channel-messages.v1-durable", "keep");
recoverLocalStorageQuotaOnStartup();
assert.equal(ls.getItem("buzz-channels.v10:durable"), "keep");
assert.equal(ls.getItem("buzz-channel-messages.v1-durable"), "keep");
assert.equal(ls.getItem("buzz-local-storage-quota-recovery.v1"), null);
});
test("startup recovery runs only once", () => {
const ls = makeQuotaLocalStorage({ maxEntries: 10 });
install(ls);
recoverLocalStorageQuotaOnStartup();
ls.store.set("buzz-channel-messages.v1:relay:new", "new snapshot");
recoverLocalStorageQuotaOnStartup();
assert.equal(
ls.getItem("buzz-channel-messages.v1:relay:new"),
"new snapshot",
);
});
test("startup recovery retries after marker write fails", () => {
const ls = makeQuotaLocalStorage({ maxEntries: 1 });
install(ls);
ls.store.set("buzz-communities", "keep");
recoverLocalStorageQuotaOnStartup();
assert.equal(ls.getItem("buzz-local-storage-quota-recovery.v1"), null);
ls.store.delete("buzz-communities");
ls.store.set("buzz-channel-messages.v1:relay:chan", "big");
recoverLocalStorageQuotaOnStartup();
assert.equal(ls.getItem("buzz-channel-messages.v1:relay:chan"), null);
assert.equal(ls.getItem("buzz-local-storage-quota-recovery.v1"), "1");
});
test("global cache byte budget evicts only oldest entries needed", () => {
const ls = makeQuotaLocalStorage({ maxEntries: 20 });
install(ls);
ls.store.set("buzz-communities", "keep");
const snapshot = (updatedAt) =>
JSON.stringify({ updatedAt, payload: "x".repeat(400_000) });
const oldestKey = "buzz-channel-messages.v1:relay:oldest";
const newerKey = "buzz-channels.v1:relay-newer";
const newestKey = "buzz-channel-messages.v1:relay:newest";
assert.equal(setLocalStorageItemWithRecovery(oldestKey, snapshot(1)), true);
assert.equal(setLocalStorageItemWithRecovery(newerKey, snapshot(2)), true);
assert.equal(setLocalStorageItemWithRecovery(newestKey, snapshot(3)), true);
assert.equal(ls.getItem(oldestKey), null);
assert.notEqual(ls.getItem(newerKey), null);
assert.notEqual(ls.getItem(newestKey), null);
assert.equal(ls.getItem("buzz-communities"), "keep");
});
test("global cache byte budget spans relays and preserves durable state", () => {
const ls = makeQuotaLocalStorage({ maxEntries: 20 });
install(ls);
ls.store.set("buzz-communities", "keep");
const largeSnapshot = "x".repeat(600_000);
assert.equal(
setLocalStorageItemWithRecovery(
"buzz-channel-messages.v1:relay-one:chan",
largeSnapshot,
),
true,
);
assert.equal(
setLocalStorageItemWithRecovery(
"buzz-channel-messages.v1:relay-two:chan",
largeSnapshot,
),
true,
);
assert.equal(ls.getItem("buzz-channel-messages.v1:relay-one:chan"), null);
assert.equal(
ls.getItem("buzz-channel-messages.v1:relay-two:chan"),
largeSnapshot,
);
assert.equal(ls.getItem("buzz-communities"), "keep");
});
test("rejects a single cache entry larger than the global byte budget", () => {
const ls = makeQuotaLocalStorage({ maxEntries: 10 });
install(ls);
const key = "buzz-channel-messages.v1:relay:oversized";
ls.store.set(key, "previous snapshot");
assert.equal(
setLocalStorageItemWithRecovery(key, "x".repeat(1_100_000)),
false,
);
assert.equal(ls.getItem(key), null);
});
test("writes normally when under quota", () => {
const ls = makeQuotaLocalStorage({ maxEntries: 10 });
install(ls);
assert.equal(setLocalStorageItemWithRecovery("k", "v"), true);
assert.equal(ls.getItem("k"), "v");
});
test("evicts pure caches and retries on quota failure", () => {
const ls = makeQuotaLocalStorage({ maxEntries: 2 });
install(ls);
ls.store.set("buzz-channel-messages.v1:relay:chan", "big");
ls.store.set("buzz-channels.v1:relay", "big");
assert.equal(setLocalStorageItemWithRecovery("k", "v"), true);
assert.equal(ls.getItem("k"), "v");
assert.equal(ls.getItem("buzz-channel-messages.v1:relay:chan"), null);
assert.equal(ls.getItem("buzz-channels.v1:relay"), null);
});
test("returns false when eviction frees nothing", () => {
const ls = makeQuotaLocalStorage({ maxEntries: 2 });
install(ls);
ls.store.set("buzz-workspaces", "keep");
ls.store.set("buzz-active-workspace-id", "keep");
assert.equal(setLocalStorageItemWithRecovery("k", "v"), false);
assert.equal(ls.getItem("k"), null);
assert.equal(ls.getItem("buzz-workspaces"), "keep");
});