feat(AppShell): show cached cover art for local Play tab recents - #2005
Merged
Conversation
Local Recently Played entries (Electron only) now show their actual
cover art instead of a generic icon, resolved via a new engine-backed
query (WasmEditorBridge.ResolveLocalCover, mirroring PlayerCore's
GameQuery) that handles both embedded-resource covers (.quest package,
legacy .asl/.cas) and sibling-file covers (a plain unpacked .aslx).
Resolving a cover means booting the full engine just to read one
field, which is too expensive to redo on every app startup. The result
is cached on the RecentGame record itself (coverDataUrl, persisted to
recent-played.json), resolved once at play time (fire-and-forget, off
the critical path of opening the player window) or lazily as a
one-time self-heal for a legacy/never-resolved entry.
Also fixes two bugs found while testing this feature live:
- wasm.ts's loadWasm() raced itself when multiple local recents
resolved their covers concurrently on mount, each seeing the WASM
bridge not yet loaded and calling dotnet.create() a second time
("Runtime module already loaded"), permanently breaking the editor
for the rest of that session. Now caches the in-flight load promise,
not just its result.
- recent-games.ts's read-modify-write cycle had no synchronization
across concurrent calls, so two cards self-healing their covers at
once could have their fs.writeFile calls genuinely interleave at the
OS level and corrupt recent-played.json outright (a short write's
bytes followed by a stray tail fragment of a longer concurrent one)
— silently wiping the entire Recently Played list on next read.
Fixed with a per-kind write queue plus atomic temp-file+rename.
Also capitalizes the "Recently Played" heading to match the site's
other section titles.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
WasmEditorBridge.ResolveLocalCover, mirroringPlayerCore.GameQuery) that handles both embedded-resource covers (.questpackage, legacy.asl/.cas) and sibling-file covers (a plain unpacked.aslx).RecentGamerecord (coverDataUrl, persisted torecent-played.json) — resolved once at play time (fire-and-forget, off the critical path of opening the player window) or lazily as a one-time self-heal for a legacy/never-resolved entry — since resolving a cover means booting the full engine just to read one field, too expensive to redo on every app startup.Bug fixes found while testing this live
wasm.ts'sloadWasm()self-race: multiple local recents resolving their covers concurrently on mount could each see the WASM bridge not yet loaded and calldotnet.create()a second time ("Runtime module already loaded"), permanently breaking the editor for the rest of that session. Fixed by caching the in-flight load promise, not just its eventual result.recent-games.tsfile corruption: the read-modify-write cycle behindadd/setCover/remove/clearhad no synchronization across concurrent calls, so two cards self-healing their covers at once could have theirfs.writeFilecalls genuinely interleave at the OS level and corruptrecent-played.jsonoutright (a short write's bytes followed by a stray tail fragment of a longer concurrent one) — silently wiping the entire Recently Played list on next read (JSON.parsethrows, caught, falls back to[]). Fixed with a per-kind write queue plus atomic temp-file+rename.Test plan
dotnet build --configuration Release(full solution) anddotnet test --configuration Release— all 336 tests passnpx svelte-check/npm run lint/tsc -p tsconfig.json(ElectronApp) — all cleanrecent-games.tsdirectly: carry-forward on replay, in-place cover patch without reordering/timestamp bump, explicit overwrite, no-op on a removed entry, and the exact concurrent-write race that previously corrupted the file (now produces valid JSON with both entries intact regardless of timing)WasmEditorBridge/loadWasmconfirming concurrentResolveLocalCovercalls resolve correctly with no cross-contamination, and thatloadWasm()no longer throws when called concurrently.questfiles (a published package with an embedded cover, and one with no cover set) in a running Electron build🤖 Generated with Claude Code