Skip to content

Commit fc3e8a2

Browse files
committed
fix: fetch missing stories for registered games on startup
1 parent eb9e6be commit fc3e8a2

4 files changed

Lines changed: 146 additions & 2 deletions

File tree

src/App.svelte

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import { setupGlobalTooltips } from "@/lib/tooltip";
99
import { setupHistoryTracker } from "@/lib/historyTrack";
1010
import { initializeAllGameCache } from "@/lib/scrapeAllGame";
11+
import { works } from "@/store/works";
1112
import ImportDropFiles from "@/components/Home/ImportDropFiles.svelte";
1213
import { backgroundState } from "@/store/background";
1314
import { location, replace } from "svelte-spa-router";
@@ -58,6 +59,7 @@
5859
didInitializeMainApp = true;
5960
initialize();
6061
initializeAllGameCache();
62+
void works.ensureRegisteredStories();
6163
};
6264
6365
onMount(async () => {

src/lib/utils.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,18 @@ type LocalStorageCacheOptions = {
3939

4040
const DAY_MILLISECONDS = 1000 * 60 * 60 * 24;
4141

42+
export type LocalStorageCacheGetter<K extends string | number, T> = ((
43+
key: K
44+
) => Promise<T>) & {
45+
peek: (key: K) => T | undefined;
46+
setValue: (key: K, value: T) => void;
47+
};
48+
4249
export const createLocalStorageCache = <K extends string | number, T>(
4350
key: string,
4451
fetcher: (key: K) => Promise<T>,
4552
options: number | LocalStorageCacheOptions = DAY_MILLISECONDS
46-
) => {
53+
): LocalStorageCacheGetter<K, T> => {
4754
const invalidateMilliseconds =
4855
typeof options === "number"
4956
? options
@@ -79,7 +86,7 @@ export const createLocalStorageCache = <K extends string | number, T>(
7986
});
8087
};
8188

82-
const getter = async (key: K): Promise<T> => {
89+
const getter = (async (key: K): Promise<T> => {
8390
const now = new Date().getTime();
8491
prune(now);
8592
const cached = getCache()[key];
@@ -95,7 +102,17 @@ export const createLocalStorageCache = <K extends string | number, T>(
95102
};
96103
});
97104
return value;
105+
}) as LocalStorageCacheGetter<K, T>;
106+
107+
getter.peek = (key: K) => getCache()[key]?.value;
108+
getter.setValue = (key: K, value: T) => {
109+
const createdAt = new Date().getTime();
110+
cache.update((v) => ({
111+
...v,
112+
[key]: { value, createdAt, version },
113+
}));
98114
};
115+
99116
return getter;
100117
};
101118

src/store/works.test.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
import { commandGetAllElements } from "@/lib/command";
3+
import { getWorkByScrape } from "@/lib/scrapeWork";
4+
import type { CollectionElement, Work } from "@/lib/types";
5+
6+
vi.mock("@/lib/command", () => ({
7+
commandGetAllElements: vi.fn(),
8+
}));
9+
10+
vi.mock("@/lib/scrapeWork", () => ({
11+
getWorkByScrape: vi.fn(),
12+
}));
13+
14+
const work = (id: number, description?: string): Work => ({
15+
id,
16+
name: `work-${id}`,
17+
brandId: 1,
18+
brandName: "brand",
19+
description,
20+
officialHomePage: "",
21+
sellday: "2026-01-01",
22+
imgUrl: "",
23+
statistics: {
24+
median: 0,
25+
average: 0,
26+
count: 0,
27+
playTime: "0",
28+
},
29+
creators: {
30+
illustrators: [],
31+
writers: [],
32+
voiceActors: [],
33+
},
34+
musics: [],
35+
});
36+
37+
const collectionElement = (id: number): CollectionElement => ({
38+
id,
39+
gamename: `game-${id}`,
40+
gamenameRuby: "",
41+
brandname: "brand",
42+
brandnameRuby: "",
43+
sellday: "2026-01-01",
44+
isNukige: false,
45+
installAt: null,
46+
firstPlayAt: null,
47+
lastPlayAt: null,
48+
likeAt: null,
49+
playStatus: 0,
50+
totalPlayTimeSeconds: 0,
51+
registeredAt: "2026-01-01",
52+
exePath: "",
53+
lnkPath: "",
54+
icon: "",
55+
thumbnail: "",
56+
thumbnailWidth: null,
57+
thumbnailHeight: null,
58+
updatedAt: "2026-01-01",
59+
});
60+
61+
const loadWorks = async () => (await import("./works")).works;
62+
63+
describe("works.ensureRegisteredStories", () => {
64+
beforeEach(() => {
65+
vi.resetModules();
66+
vi.clearAllMocks();
67+
localStorage.clear();
68+
});
69+
70+
it("fetches stories only for registered works missing a cached description", async () => {
71+
localStorage.setItem(
72+
"works-cache",
73+
JSON.stringify({
74+
1: { value: work(1, "cached story"), createdAt: Date.now(), version: 8 },
75+
2: { value: work(2), createdAt: Date.now(), version: 8 },
76+
3: { value: work(3), createdAt: Date.now(), version: 8 },
77+
})
78+
);
79+
vi.mocked(commandGetAllElements).mockResolvedValue([
80+
collectionElement(1),
81+
collectionElement(2),
82+
]);
83+
vi.mocked(getWorkByScrape).mockResolvedValue(work(2, "fetched story"));
84+
85+
const works = await loadWorks();
86+
await works.ensureRegisteredStories();
87+
88+
expect(getWorkByScrape).toHaveBeenCalledTimes(1);
89+
expect(getWorkByScrape).toHaveBeenCalledWith(2);
90+
expect(await works.get(2)).toMatchObject({ description: "fetched story" });
91+
});
92+
});

src/store/works.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
import { getWorkByScrape } from "@/lib/scrapeWork";
2+
import { commandGetAllElements } from "@/lib/command";
23
import type { Work } from "@/lib/types";
34
import { createLocalStorageCache } from "@/lib/utils";
45

56
const WORKS_CACHE_VERSION = 8;
67
const WORKS_CACHE_TTL_MS = 1000 * 60 * 60 * 24 * 7;
8+
const DESCRIPTION_PREFETCH_DELAY_MS = 500;
9+
10+
const hasStoryDescription = (work: Work | undefined) =>
11+
!!work?.description?.trim();
12+
13+
const wait = (milliseconds: number) =>
14+
new Promise((resolve) => setTimeout(resolve, milliseconds));
715

816
const createWorks = () => {
917
const getter = createLocalStorageCache<number, Work>(
@@ -17,6 +25,31 @@ const createWorks = () => {
1725

1826
return {
1927
get: getter,
28+
ensureRegisteredStories: async () => {
29+
let elements;
30+
try {
31+
elements = await commandGetAllElements();
32+
} catch (error) {
33+
console.warn("[works] failed to load registered games", error);
34+
return;
35+
}
36+
37+
for (const element of elements) {
38+
if (hasStoryDescription(getter.peek(element.id))) {
39+
continue;
40+
}
41+
42+
try {
43+
getter.setValue(element.id, await getWorkByScrape(element.id));
44+
} catch (error) {
45+
console.warn(
46+
`[works] failed to fetch missing story for ${element.id}`,
47+
error
48+
);
49+
}
50+
await wait(DESCRIPTION_PREFETCH_DELAY_MS);
51+
}
52+
},
2053
};
2154
};
2255

0 commit comments

Comments
 (0)