fix: harden favorites storage recovery
This commit is contained in:
@@ -245,6 +245,14 @@ function getChromeLocalStorage(): ChromeStorageLocalArea {
|
|||||||
|
|
||||||
function normalizeState(value: unknown, now: () => string): FavoritesStateV1 {
|
function normalizeState(value: unknown, now: () => string): FavoritesStateV1 {
|
||||||
const source = isRecord(value) ? value : {};
|
const source = isRecord(value) ? value : {};
|
||||||
|
if (source.version !== 1) {
|
||||||
|
return {
|
||||||
|
creators: [],
|
||||||
|
folders: [],
|
||||||
|
memberships: [],
|
||||||
|
version: 1
|
||||||
|
};
|
||||||
|
}
|
||||||
const folders = normalizeFolders(source.folders, now);
|
const folders = normalizeFolders(source.folders, now);
|
||||||
const creators = normalizeCreators(source.creators, now);
|
const creators = normalizeCreators(source.creators, now);
|
||||||
const knownFolderIds = new Set(folders.map((folder) => folder.id));
|
const knownFolderIds = new Set(folders.map((folder) => folder.id));
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ describe("favorites-store", () => {
|
|||||||
test("recovers from malformed, duplicate, and dangling stored values", async () => {
|
test("recovers from malformed, duplicate, and dangling stored values", async () => {
|
||||||
const { repository } = createRepository(
|
const { repository } = createRepository(
|
||||||
createMemoryStorage({
|
createMemoryStorage({
|
||||||
version: 42,
|
version: 1,
|
||||||
folders: [
|
folders: [
|
||||||
{
|
{
|
||||||
id: "folder-1",
|
id: "folder-1",
|
||||||
@@ -214,6 +214,44 @@ describe("favorites-store", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("treats a future stored version as an empty V1 state", async () => {
|
||||||
|
const { repository } = createRepository(
|
||||||
|
createMemoryStorage({
|
||||||
|
version: 2,
|
||||||
|
folders: [
|
||||||
|
{
|
||||||
|
id: "future-folder",
|
||||||
|
name: "未来收藏夹",
|
||||||
|
createdAt: "future",
|
||||||
|
updatedAt: "future"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
creators: [
|
||||||
|
{
|
||||||
|
authorId: "future-author",
|
||||||
|
authorName: "Future",
|
||||||
|
savedAt: "future"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
memberships: [
|
||||||
|
{
|
||||||
|
folderId: "future-folder",
|
||||||
|
authorId: "future-author",
|
||||||
|
addedAt: "future"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
futureOnlyField: "do not retain"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(repository.read()).resolves.toEqual({
|
||||||
|
creators: [],
|
||||||
|
folders: [],
|
||||||
|
memberships: [],
|
||||||
|
version: 1
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("serializes concurrent mutations so neither folder is overwritten", async () => {
|
test("serializes concurrent mutations so neither folder is overwritten", async () => {
|
||||||
const { repository } = createRepository();
|
const { repository } = createRepository();
|
||||||
|
|
||||||
@@ -230,18 +268,40 @@ describe("favorites-store", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("propagates rejected storage writes", async () => {
|
test("recovers the mutation queue after one rejected storage write", async () => {
|
||||||
|
let writeAttempts = 0;
|
||||||
|
let storedState: FavoritesStateV1 | undefined;
|
||||||
const storage: FavoritesStorage = {
|
const storage: FavoritesStorage = {
|
||||||
async get() {
|
async get() {
|
||||||
return undefined;
|
return storedState;
|
||||||
},
|
},
|
||||||
async set() {
|
async set(value) {
|
||||||
throw new Error("write failed");
|
writeAttempts += 1;
|
||||||
|
if (writeAttempts === 1) {
|
||||||
|
throw new Error("write failed");
|
||||||
|
}
|
||||||
|
storedState = value;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const { repository } = createRepository(storage);
|
const { repository } = createRepository(storage);
|
||||||
|
|
||||||
await expect(repository.createFolder("合作")).rejects.toThrow("write failed");
|
await expect(repository.createFolder("合作")).rejects.toThrow("write failed");
|
||||||
|
await expect(repository.createFolder("待联系")).resolves.toMatchObject({
|
||||||
|
name: "待联系"
|
||||||
|
});
|
||||||
|
await expect(repository.read()).resolves.toEqual({
|
||||||
|
creators: [],
|
||||||
|
folders: [
|
||||||
|
{
|
||||||
|
id: "folder-2",
|
||||||
|
name: "待联系",
|
||||||
|
createdAt: "2026-07-17T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-07-17T00:00:00.000Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
memberships: [],
|
||||||
|
version: 1
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("uses the exact Chrome storage key and reports unavailable storage", async () => {
|
test("uses the exact Chrome storage key and reports unavailable storage", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user