mirror of
https://github.com/block/buzz.git
synced 2026-11-05 17:01:35 +00:00
## 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
205 lines
6.7 KiB
JavaScript
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");
|
|
});
|