Skip to content

perf(studio): eliminate N duplicate getPkgVersionMeta calls on load#197

Open
jameswillis99 wants to merge 4 commits intomasterfrom
feat/studio-load-perf-optimizations
Open

perf(studio): eliminate N duplicate getPkgVersionMeta calls on load#197
jameswillis99 wants to merge 4 commits intomasterfrom
feat/studio-load-perf-optimizations

Conversation

@jameswillis99
Copy link
Copy Markdown
Collaborator

@jameswillis99 jameswillis99 commented Mar 27, 2026

Problem

On every studio open, checkDepPkgHosts fired N parallel getPkgVersionMeta API calls (one per dependency) to retrieve hostUrl, pkg.name, and pkg.projectId for the host-mismatch warning. This data is already present on the depPkgs array returned in the same getSiteInfo response that just completed — there is no need to re-fetch it.

For the current EP storefront template this halves the number of DB-read-heavy dependency calls on studio load from 8 to 4.

Change

client/init-ctx.tsx — simplify checkDepPkgHosts:

  • Change the function signature from ProjectDependency[] to PkgVersionInfo[]
  • Remove the N getPkgVersionMeta calls; iterate depPkgs directly using the hostUrl, pkg.name, and pkg.projectId fields already present on each entry
  • Pass depPkgs (the API response objects) to checkDepPkgHosts instead of the unbundled ProjectDependency model objects
  • Drop the now-unused ProjectDependency import; add PkgVersionInfo
  • The function becomes synchronous (no more async)

Verification

  • Open a project with dependencies from a differently-hosted project — the host-mismatch warning still appears
  • Network tab shows zero extra /getPkgVersionMeta calls on studio load
  • Projects with no deps and projects with same-host deps are unaffected

🤖 Generated with Claude Code

On every studio open, the client was firing N parallel requests to
`GET /api/v1/pkgs/:pkgId/versions-without-data` (one per direct
dependency) to populate "update available" badges in the sidebar. Each
request acquired its own DB connection, ran a full permission chain, and
saturated the pool alongside all the other concurrent startup requests.

This commit eliminates those calls on initial load by including the
latest published version for every direct dependency in the
`getSiteInfo` response, and parallelises the remaining sequential
queries in `getProjectRev`.

Changes by area:

server/routes/projects.ts + AppServer.ts
- Remove `withNext` (outer transaction) from `GET /api/v1/projects/:id`
  so queries use pool connections and can run concurrently.
- Restructure `getProjectRev` into three parallel rounds:
  Round 1 – 9 independent queries in Promise.all (branch, project, rev,
  perms, modelVersion, hostlessVersion, latestRevisionSynced,
  appAuthConfig, allowedDataSources).
  Round 2 – migratedBundle, owner fetch, workspaceTutorialDbs in
  parallel (depend on project/rev from Round 1).
  Round 3 – loadDepPackages → getLatestPkgVersionsForPkgIds (depend on
  migratedBundle).
- `claimPublicProject` (write path) gets its own explicit
  startTransaction since the outer withNext is removed.
- Add `latestDepPkgVersions` to the response payload.

server/db/DbMgr.ts
- Add `getLatestPkgVersionsForPkgIds`: single WHERE IN query fetching
  the latest main-branch PkgVersion per pkgId, excluding the heavy
  `model` column. No permission check — called inside getProjectRev
  which has already verified access, same trust level as
  loadDepPackages.
- Refactor `checkPkgPerms` to return the Pkg entity it already fetches,
  removing a redundant `getPkgById` call in `publishPkgVersion`.
- Remove redundant `checkPkgPerms` call from `listPkgVersionsRaw` (the
  endpoint-level auth check in `listPkgVersions` is sufficient).

server/db/bundle-migration-utils.ts
- Replace the 10-minute TTL cache invalidation for hostless data with an
  explicit flag (`_hostlessCacheStale`) that is set on startup and
  cleared only after a successful reload. `invalidateHostlessCache()` is
  called by `bumpHostlessVersion` so the cache is always fresh after a
  publish, without polling the DB on every request.
- Remove `hostlessVersionCount` from the cached payload (no longer
  needed since the cache is invalidated explicitly rather than by
  comparing version counts).

server/db/BundleMigrator.ts
- Hoist the `bundleHasStaleHostlessDeps` call before the
  `isExpectedBundleVersion` check so both conditions are evaluated
  before branching, avoiding a potential double-await.

shared/ApiSchema.ts
- Add `latestDepPkgVersions: Record<string, PkgVersionInfoMeta>` to
  `GetProjectResponse`.

client/db.ts
- Add `latestDepPkgVersions` to `DbCtxArgs` / `DbCtx` so the
  server-provided map flows through to StudioCtx.

client/init-ctx.tsx
- Destructure `latestDepPkgVersions` from the getSiteInfo response and
  pass it to DbCtx.
- Simplify `checkDepPkgHosts`: it already has the full PkgVersionInfo
  from `depPkgs` so there's no need to fire N extra `getPkgVersionMeta`
  calls — use the already-loaded data directly.

client/ProjectDependencyManager.ts
- Add `seedLatestVersionMeta`: pre-populates latestPkgVersionMeta in
  _dependencyMap from the server-provided map. Since
  `_fetchLatestVersionMeta` skips the API call when the field is already
  set, this eliminates all N badge-fetch calls on first load.
- Add `setPlumePkgFetch`: allows the Plume package to be fetched in
  parallel with project load by accepting a pre-started Promise.
- Split `_fetchData` into `_fetchLatestVersionMeta` (spawned, non-
  blocking) and `_fetchPlumeSite` (awaited, required before sync), so
  badge updates do not block studio startup.
- Change `refreshDeps()` to accept `{ forceVersionMeta?: boolean }`
  (default false) so the initial load does not bypass pre-seeded data.

client/studio-ctx/StudioCtx.tsx
- Call `seedLatestVersionMeta` immediately after constructing
  ProjectDependencyManager.

client/components/sidebar/ProjectDependencies.tsx
- Pass `{ forceVersionMeta: true }` to `refreshDeps` when the user
  clicks "Check for updates", so manual refresh still hits the API.

client/components/studio/studio-initializer.tsx
- Fire `getPlumePkg()` before `initStudioCtx` and hand the in-flight
  Promise to `setPlumePkgFetch`, so the Plume fetch runs in parallel
  with the project load rather than serially after it.

client/api.ts
- Remove the now-unnecessary `listPkgVersionsWithoutData` override from
  `filteredApi` (the function is no longer called by the client in a
  context that requires it to be filtered).
…idation

Revert the _hostlessCacheStale flag approach back to the original
10-minute TTL + hostlessVersionCount comparison. The cache change
is out of scope for this PR and can be revisited separately.
Keep only the removal of N duplicate getPkgVersionMeta API calls in
checkDepPkgHosts. The data (hostUrl, pkg name/id) is already present
on the depPkgs returned in the getSiteInfo response, so there is no
need to re-fetch it. All server-side changes and broader client
optimisations are reverted for a separate PR.
@jameswillis99 jameswillis99 changed the title perf(studio): eliminate N badge-fetch API calls and parallelise studio load perf(studio): eliminate N duplicate getPkgVersionMeta calls on load Mar 27, 2026
The previous commit changed checkDepPkgHosts from async (ProjectDependency[])
to sync (PkgVersionInfo[]) but missed two other call sites that still passed
ProjectDependency[], causing TypeScript compilation failures.

- ProjectDependencyManager.ts: pass [latest, ...depPkgs] (PkgVersionInfo[])
  from the getPkgVersion response; drop the unused depPkgVersions destructure
  from unbundleProjectDependency
- StudioCtx.tsx: pass depPkgs (PkgVersionInfo[]) directly; simplify the
  unbundleSite destructure to { site } since depPkgVersions is unused;
  remove now-unused isKnownProjectDependency import
- All three call sites: remove spawn() wrapper since checkDepPkgHosts is now
  synchronous (spawn expects PromiseLike, not void)
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.

2 participants