fix: final review — cached links keep their expiry; history removal sticks; saves flushed

- A link reused from the rehost cache keeps the expiry it was uploaded with, and
  links near expiry are uploaded again (a dead litterbox link could be filed).
- History saving moves into HistoryKeeper (tested): removing an entry or clearing
  the history cancels a pending save and stops tracking the series on screen;
  a pending edit is saved before a new search, before opening another entry and
  when the page is hidden; opening an entry no longer re-saves the current one.
- The "couldn't be saved" line sits outside the History list, so it shows even
  when the list is empty; a refused IndexedDB says history is off.
- Recently searched bands wrap their meta line instead of cutting the count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-10-15 11:45:48 +02:00
1 parent 1cf31b26cf
commit 0d4a1a61fa
8 files changed
+362 -68

No files matched your search

+27 -3
View File
@@ -81,6 +81,8 @@ export class Session {
makeDefault = true;
private conflicts = 0;
private readonly hostedCache: Map<string, string>;
/** When each cached link expires (epoch ms; null = never). A cached link must not outlive its host. */
private readonly cacheUntil = new Map<string, number | null>();
constructor(private readonly deps: SessionDeps) {
this.hostedCache = deps.persistHostedUrls ? loadHosted(deps.store) : new Map();
@@ -115,7 +117,13 @@ export class Session {
this.makeDefault = makeDefault && result.suggestedDefault !== null;
this.input = input;
this.conflicts = 0;
for (const c of cards) if (c.hostedUrl && c.hosting?.key) this.hostedCache.set(c.hosting.key, c.hostedUrl);
const now = Date.now();
for (const c of cards) {
if (c.hostedUrl && c.hosting?.key && !expiresSoon(c.hosting.until, now)) {
this.hostedCache.set(c.hosting.key, c.hostedUrl);
this.cacheUntil.set(c.hosting.key, c.hosting.until);
}
}
}
update(sha256: string, patch: Partial<Pick<ReviewCard, "selected" | "type" | "note" | "contentRating">>): void {
@@ -164,6 +172,14 @@ export class Session {
/** Explicit user action only (spec §4.6). */
async rehost(hostId: HostId, options: HostOptions, onProgress?: (done: number, total: number) => void): Promise<RehostResult> {
const cards = this.selected();
const now = Date.now();
for (const [key, until] of this.cacheUntil) {
if (expiresSoon(until, now)) {
this.hostedCache.delete(key);
this.cacheUntil.delete(key);
}
}
const cachedBefore = new Set(this.hostedCache.keys());
const res = await rehostAll(
cards.map((c) => hostImage(c.cover)),
HOSTS[hostId],
@@ -171,10 +187,14 @@ export class Session {
this.hostedCache,
onProgress,
);
const until = linkExpiry(hostId, options, Date.now());
const fresh = linkExpiry(hostId, options, now);
for (const c of cards) {
c.hostedUrl = res.hosted.get(c.cover.sha256) ?? null;
c.hosting = c.hostedUrl ? { by: hostId, key: cacheKey(hostId, options, c.cover.sha256), until } : undefined;
const key = cacheKey(hostId, options, c.cover.sha256);
// A link reused from the cache keeps the expiry it was uploaded with.
const until = key && cachedBefore.has(key) ? (this.cacheUntil.get(key) ?? fresh) : fresh;
if (key && c.hostedUrl) this.cacheUntil.set(key, until);
c.hosting = c.hostedUrl ? { by: hostId, key, until } : undefined;
c.error = res.failures.find((f) => f.sha256 === c.cover.sha256)?.message ?? null;
}
if (this.deps.persistHostedUrls) saveHosted(this.deps.store, this.hostedCache);
@@ -229,6 +249,10 @@ export class Session {
}
}
/** Links this close to expiry are uploaded again rather than reused. */
const EXPIRY_MARGIN_MS = 5 * 60_000;
const expiresSoon = (until: number | null | undefined, now: number): boolean => until != null && until <= now + EXPIRY_MARGIN_MS;
/** Only catbox links are kept across runs: litterbox links die within 72 hours. */
function loadHosted(store: KeyValueStore): Map<string, string> {
try {