fix(notifications): keep the history under a key of its own, so an older build never reads its new shape
A build from before the change renders history rows as text; an object there crashed its Notifications page after a rollback. The old key is read once while the new one does not exist, and never written.
This commit is contained in:
@@ -20,7 +20,7 @@ const entry = (id: number, message: string) => ({
|
||||
});
|
||||
const storedMessages = () =>
|
||||
(
|
||||
JSON.parse(localStorage.getItem("siftlode.notifications") ?? "[]") as {
|
||||
JSON.parse(localStorage.getItem("siftlode.notifHistory") ?? "[]") as {
|
||||
message: NotifText;
|
||||
}[]
|
||||
).map((n) => sayNotif(n.message));
|
||||
@@ -48,7 +48,7 @@ beforeEach(() => {
|
||||
describe("stores read at import, before the account is known", { timeout: CASE_BUDGET }, () => {
|
||||
it("keep the account's notification history and settings once it is claimed", async () => {
|
||||
localStorage.setItem("siftlode.owner", "5");
|
||||
localStorage.setItem("siftlode.notifications", JSON.stringify([entry(7, "older")]));
|
||||
localStorage.setItem("siftlode.notifHistory", JSON.stringify([entry(7, "older")]));
|
||||
localStorage.setItem("siftlode.notifSettings", JSON.stringify({ durationMs: 0 }));
|
||||
const notifications = await import("./notifications");
|
||||
const { claimLocalData } = await import("./storage");
|
||||
@@ -76,7 +76,7 @@ describe("stores read at import, before the account is known", { timeout: CASE_B
|
||||
describe("a browser that held another account's state", { timeout: CASE_BUDGET }, () => {
|
||||
it("starts the new account's notices from an empty history", async () => {
|
||||
localStorage.setItem("siftlode.owner", "4");
|
||||
localStorage.setItem("siftlode.notifications", JSON.stringify([entry(7, "four's")]));
|
||||
localStorage.setItem("siftlode.notifHistory", JSON.stringify([entry(7, "four's")]));
|
||||
const notifications = await import("./notifications");
|
||||
const { claimLocalData } = await import("./storage");
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// @vitest-environment jsdom
|
||||
//
|
||||
// The notice history changed shape (a notice keeps what to say, not the said text). A build from
|
||||
// before it reads the old key and renders its rows as text, so an object there crashed its
|
||||
// Notifications page after a rollback. The history now lives under a key of its own: the old one is
|
||||
// read once, while the new one does not exist yet, and never written.
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { msg, raw, sayNotif, type NotifText } from "./notifications";
|
||||
|
||||
const OLD_KEY = "siftlode.notifications";
|
||||
|
||||
// A row as the build before wrote it: its text already said.
|
||||
const oldRow = (id: number, message: string) => ({
|
||||
id,
|
||||
level: "info",
|
||||
message,
|
||||
ts: 1,
|
||||
read: true,
|
||||
dismissed: true,
|
||||
transient: false,
|
||||
repeat: 1,
|
||||
requiresInteraction: false,
|
||||
});
|
||||
|
||||
const saidHistory = (key: string) =>
|
||||
(JSON.parse(localStorage.getItem(key) ?? "[]") as { message: NotifText }[]).map((n) =>
|
||||
sayNotif(n.message),
|
||||
);
|
||||
|
||||
beforeAll(async () => {
|
||||
await import("./notifications");
|
||||
await import("./storage");
|
||||
}, 60_000);
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
vi.resetModules(); // the store loads at import: each case needs a fresh one
|
||||
});
|
||||
|
||||
describe("the notice history's own key", { timeout: 30_000 }, () => {
|
||||
it("carries the old history forward and leaves the old key as the build before wrote it", async () => {
|
||||
const old = JSON.stringify([oldRow(7, "older")]);
|
||||
localStorage.setItem("siftlode.owner", "5");
|
||||
localStorage.setItem(OLD_KEY, old);
|
||||
const notifications = await import("./notifications");
|
||||
const { claimLocalData, LS } = await import("./storage");
|
||||
|
||||
claimLocalData(5);
|
||||
notifications.notify({ message: msg("common.copy.done") });
|
||||
|
||||
expect(LS.notifHistory).not.toBe(OLD_KEY);
|
||||
expect(localStorage.getItem(OLD_KEY)).toBe(old);
|
||||
expect(saidHistory(LS.notifHistory)).toEqual(["older", "Copied to the clipboard"]);
|
||||
});
|
||||
|
||||
it("reads the old key only while the new one does not exist", async () => {
|
||||
const { LS } = await import("./storage");
|
||||
localStorage.setItem("siftlode.owner", "5");
|
||||
localStorage.setItem(OLD_KEY, JSON.stringify([oldRow(7, "older")]));
|
||||
localStorage.setItem(
|
||||
LS.notifHistory,
|
||||
JSON.stringify([{ ...oldRow(8, ""), message: raw("newer") }]),
|
||||
);
|
||||
const notifications = await import("./notifications");
|
||||
const { claimLocalData } = await import("./storage");
|
||||
|
||||
claimLocalData(5);
|
||||
|
||||
expect(notifications.getNotifications().map((n) => sayNotif(n.message))).toEqual(["newer"]);
|
||||
});
|
||||
});
|
||||
@@ -222,7 +222,9 @@ let cachedUnread = 0;
|
||||
|
||||
function load(): Notification[] {
|
||||
try {
|
||||
const stored = readAccount<unknown>(HISTORY_KEY, []);
|
||||
// The old key only until the new one exists: its rows carry forward, said text as `raw`.
|
||||
const stored =
|
||||
readAccount<unknown>(HISTORY_KEY, null) ?? readAccount<unknown>(LS.legacyNotifHistory, []);
|
||||
if (!Array.isArray(stored)) return [];
|
||||
// Restored entries are history only. Mark them dismissed so they don't
|
||||
// resurrect as active toasts on reload — their auto-dismiss timers aren't
|
||||
|
||||
@@ -100,7 +100,12 @@ const KEYS = {
|
||||
// flag). A YT.Player instance is created per video and starts at full volume, so without this
|
||||
// every next video is loud again (same idea as plexPlayerPrefs).
|
||||
ytPlayerPrefs: { key: "siftlode.ytPlayerPrefs", kind: "device" },
|
||||
notifHistory: { key: "siftlode.notifications", kind: "cache" },
|
||||
// The notice history: what each notice says (lib/notifications NotifText), not the said text.
|
||||
notifHistory: { key: "siftlode.notifHistory", kind: "cache" },
|
||||
// DEPRECATED — the history as said text, which a build from before the change still reads (and
|
||||
// renders as text: an object there crashed its Notifications page after a rollback). Read once while
|
||||
// `notifHistory` does not exist yet, then inert. Do not write.
|
||||
legacyNotifHistory: { key: "siftlode.notifications", kind: "cache" },
|
||||
notifSettings: { key: "siftlode.notifSettings", kind: "pref" },
|
||||
onboardingDismissed: { key: "siftlode.onboarding.dismissed", kind: "pref" },
|
||||
seenVersion: { key: "siftlode.seenVersion", kind: "pref" },
|
||||
|
||||
Reference in New Issue
Block a user