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 = () =>
|
const storedMessages = () =>
|
||||||
(
|
(
|
||||||
JSON.parse(localStorage.getItem("siftlode.notifications") ?? "[]") as {
|
JSON.parse(localStorage.getItem("siftlode.notifHistory") ?? "[]") as {
|
||||||
message: NotifText;
|
message: NotifText;
|
||||||
}[]
|
}[]
|
||||||
).map((n) => sayNotif(n.message));
|
).map((n) => sayNotif(n.message));
|
||||||
@@ -48,7 +48,7 @@ beforeEach(() => {
|
|||||||
describe("stores read at import, before the account is known", { timeout: CASE_BUDGET }, () => {
|
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 () => {
|
it("keep the account's notification history and settings once it is claimed", async () => {
|
||||||
localStorage.setItem("siftlode.owner", "5");
|
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 }));
|
localStorage.setItem("siftlode.notifSettings", JSON.stringify({ durationMs: 0 }));
|
||||||
const notifications = await import("./notifications");
|
const notifications = await import("./notifications");
|
||||||
const { claimLocalData } = await import("./storage");
|
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 }, () => {
|
describe("a browser that held another account's state", { timeout: CASE_BUDGET }, () => {
|
||||||
it("starts the new account's notices from an empty history", async () => {
|
it("starts the new account's notices from an empty history", async () => {
|
||||||
localStorage.setItem("siftlode.owner", "4");
|
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 notifications = await import("./notifications");
|
||||||
const { claimLocalData } = await import("./storage");
|
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[] {
|
function load(): Notification[] {
|
||||||
try {
|
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 [];
|
if (!Array.isArray(stored)) return [];
|
||||||
// Restored entries are history only. Mark them dismissed so they don't
|
// Restored entries are history only. Mark them dismissed so they don't
|
||||||
// resurrect as active toasts on reload — their auto-dismiss timers aren'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
|
// 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).
|
// every next video is loud again (same idea as plexPlayerPrefs).
|
||||||
ytPlayerPrefs: { key: "siftlode.ytPlayerPrefs", kind: "device" },
|
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" },
|
notifSettings: { key: "siftlode.notifSettings", kind: "pref" },
|
||||||
onboardingDismissed: { key: "siftlode.onboarding.dismissed", kind: "pref" },
|
onboardingDismissed: { key: "siftlode.onboarding.dismissed", kind: "pref" },
|
||||||
seenVersion: { key: "siftlode.seenVersion", kind: "pref" },
|
seenVersion: { key: "siftlode.seenVersion", kind: "pref" },
|
||||||
|
|||||||
Reference in New Issue
Block a user