Skip to content

perf: move steam sync work off the main process#45

Open
metacurb wants to merge 1 commit intomainfrom
codex/steam-sync-worker-pipeline
Open

perf: move steam sync work off the main process#45
metacurb wants to merge 1 commit intomainfrom
codex/steam-sync-worker-pipeline

Conversation

@metacurb
Copy link
Copy Markdown
Member

Summary

  • move Steam library scanning and metadata fetching into a dedicated worker-thread pipeline
  • keep database writes and UI event emission in the main process while batching Steam sync persistence
  • throttle Steam metadata progress updates so sync no longer emits per-game churn

Details

  • extract shared Steam owned-games and installed-games readers so they can be reused by both the main process service and the worker
  • add SteamSyncWorkerService, worker message contracts, and a dedicated steam-sync.worker.ts
  • add GameStore batch helpers for installed-game reconciliation and metadata sync writes
  • route Steam sync through the worker-backed path in SyncService, while leaving the other launchers on the existing path for now

Validation

  • pnpm --dir apps/frontend typecheck
  • pnpm --dir apps/desktop lint (passes with existing warnings only)
  • pnpm --dir apps/desktop typecheck still fails on pre-existing unrelated issues in game-lifecycle.service.ts, integrations/auth.ts, epic-games-store-api.service.ts, and apps/frontend/src/ipc.types.ts
  • pnpm --dir apps/desktop exec electron-vite build --config electron.vite.config.ts

Notes

  • Steam is the only launcher moved to the worker path in this slice.
  • This follows the tactical sync responsiveness fix from #44 and keeps the renderer IPC contract unchanged.

@metacurb metacurb force-pushed the codex/steam-sync-worker-pipeline branch from 6f33a88 to 55e8537 Compare April 17, 2026 19:57
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6f33a882a9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +37 to +40
const libraryManifests = await Promise.all(
libraries.map(async (libraryPath) => {
const manifestPath = path.join(libraryPath, "steamapps");
const files = await fs.readdir(manifestPath);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Handle unreadable Steam library paths per-library

readSteamInstalledGames now scans all Steam libraries in a single Promise.all without a per-library try/catch, so one bad path (for example an unmounted drive or stale entry in libraryfolders.vdf) rejects the whole read. In the new worker flow this bubbles up as job-failed, which aborts the entire Steam library sync even when other library folders are valid; the previous strategy code tolerated individual path failures and continued.

Useful? React with 👍 / 👎.

Comment on lines +54 to +55
if (result.status === "fulfilled" && result.value) {
return {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat 404 Steam metadata as synced terminal state

This branch classifies a fulfilled null metadata response as a hard failure. fetchGameMetadata returns null on HTTP 404, and SyncService.synchroniseSteamMetadata only persists metadataSyncedAt for success entries, so not-found Steam games remain forever in the unsynced set and are retried on every sync cycle. That creates permanent failure churn and repeated API calls for titles the backend does not know about.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the Steam synchronization process by offloading library scanning and metadata fetching to background worker threads, significantly improving performance and UI responsiveness. Key changes include the introduction of SteamSyncWorkerService, batch metadata processing in GameStore, and frontend optimizations to prevent redundant startup syncs. Feedback focuses on addressing potential SQLite parameter limits for large libraries, adding timeouts to worker promises to prevent resource leaks, simplifying type casting for nullable fields, and increasing metadata batch sizes to minimize IPC overhead.

},
where: { isInstalled: true, library },
}),
installedGameIds.length ? this.repository.findBy({ gameId: In(installedGameIds), library }) : [],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using the In operator with a potentially large array of IDs can lead to performance issues or even crashes in SQLite due to the limit on the number of host parameters (usually 999). For users with very large Steam libraries, installedGameIds could exceed this limit. Consider chunking the IDs or using a different query strategy if the count is high.

await manager.update(
GameEntity,
{ gameId: In(gamesToUninstall), library },
{ installationDetails: null as unknown as GameInstallationDetails, isInstalled: false },
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The double cast null as unknown as GameInstallationDetails is unnecessary and masks type safety. Since the column is nullable in the database, you can simply use null as any or update the GameInstallationDetails type to be nullable if you want to maintain strict typing.

            { installationDetails: null as any, isInstalled: false },


this.logger.debug("Starting Steam library worker job", { jobId });

return await new Promise<Extract<SteamSyncWorkerResponse, { type: "library-scan-results" }>>((resolve, reject) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The worker job promise lacks a timeout mechanism. If the worker thread hangs or fails to send a job-complete or job-failed message, this promise will remain pending indefinitely, potentially leaking resources. Consider adding a timeout to ensure the promise eventually settles.

Comment on lines +216 to +259
private async synchroniseSteamMetadata(games: GameStoreModel[]) {
if (!games.length) return;

await this.steamSyncWorkerService.runMetadataJob(
{
apiBaseUrl: getStakloadApiBaseUrl(),
games: games.map(({ _id, gameId, name }) => ({ _id, gameId, name })),
},
async (results, progress) => {
const successfulEntries = results.flatMap((result) =>
result.status === "success"
? [
{
id: result.game._id,
metadata: result.metadata,
},
]
: [],
);

if (successfulEntries.length) {
await this.gameStore.applyMetadataSyncBatch(successfulEntries);
}

results
.filter((result) => result.status === "failure")
.forEach((result) => {
this.logger.warn("Steam metadata synchronisation failed", {
error: result.error,
game: result.game.name,
});
this.addFailureEntry({
action: "metadata",
code: "UNKNOWN_ERROR",
gameName: result.game.name,
library: "steam",
});
});

this.processing += results.length;
this.emitMetadataProgress(progress.processed === progress.total);
},
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The synchroniseSteamMetadata method processes games in batches of 10 (default). While this improves responsiveness, a batch size of 10 might be too small for large libraries, leading to excessive IPC overhead and frequent database transactions. Consider increasing the default batch size (e.g., to 50 or 100) to find a better balance between UI responsiveness and sync performance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant