Skip to content

PoC v2: layered primitives (batchify + memoize + reactive), migrate VariantCountCache#5565

Draft
alisman wants to merge 1 commit into
masterfrom
extract-batched-fetch-pipeline
Draft

PoC v2: layered primitives (batchify + memoize + reactive), migrate VariantCountCache#5565
alisman wants to merge 1 commit into
masterfrom
extract-batched-fetch-pipeline

Conversation

@alisman

@alisman alisman commented May 8, 2026

Copy link
Copy Markdown
Collaborator

Replaces #5563

Same goal — propose a replacement for the `LazyMobXCache` hierarchy — but factored differently per review feedback. The previous PoC bundled batching, dedup, and MobX reactivity into one fused helper. This version splits them into three independent primitives so each cache only opts into the concerns it needs.

Three primitives, ~120 lines total

```ts
// src/shared/lib/batchedFetch.ts

batchify(bulkFetch, queryKey, resultKey)
// Turns a bulk endpoint into a single-item function. Concurrent calls
// within one microtask tick collapse into one bulkFetch call.
// No caching, no MobX. (DataLoader pattern.)

memoize(fn, keyOf)
// Promise-level dedup. Same key in flight or already resolved → same
// Promise. Rejections evict so retries work.

reactive(fn, keyOf)
// Wraps a Promise-returning fn in a MobX observable.box so callers can
// read it synchronously during render. `get(q)` returns null while
// fetching, then flips to {status, data} reactively when it resolves.
```

Each cache then composes the layers it needs:

```ts
// src/shared/cache/VariantCountCache.ts (post-migration)
const fetchOne = memoize(
batchify(
qs => client.fetchVariantCountsUsingPOST({ ... }),
keyOf, keyOf,
),
keyOf,
);
return reactive(fetchOne, keyOf);
```

A cache that doesn't need batching skips `batchify`. A cache that doesn't need MobX skips `reactive` and uses the bare Promise-returning function. Etc.

Why this is better than the v1 fused helper

v1 (#5563, closed) v2 (this PR)
Primitives 1 fused `createBatchedReactiveFetch` 3 generic, named primitives
Pattern recognition Custom thing DataLoader + memoize + observable wrapper
Reactivity is replaceable No, baked in Yes, swap `reactive` for a different reactivity story
Each layer separately testable No Yes (4 tests per layer + 3 composition tests)
LOC for primitives ~50 ~120 (more, but each piece is smaller and self-contained)
LOC per migrated cache ~30 ~25

Diff

+378 / -38 across 6 files. The bulk of the new lines are the three primitives plus 15 tests.

Test plan

  • `pnpm typecheck` clean
  • 15 new unit tests in `batchedFetch.spec.ts` pass — 4 each for batchify/memoize/reactive plus 3 composition tests proving `reactive(memoize(batchify(...)))` collapses 500 in-tick calls into 1 fetch with full memoization across renders
  • Existing `LazyMobXCache.spec.ts` still passes (old infra unchanged)
  • Both `CohortColumnFormatter` specs pass (37 tests total in the relevant slice)
  • Manual smoke: open a patient view, verify Cohort column still renders frequencies correctly (needs a reviewer with a working dev env)
  • CI green

What's intentionally NOT in this PR

  • The other 18 LazyMobXCache subclasses
  • `LazyMobXCache.ts`, `SampleGeneCache.ts`, `SimpleCache.ts` — untouched
  • A cache-level helper that bundles all three primitives. If you find yourself writing the same 4-line `reactive(memoize(batchify(...)))` recipe in 18 places, that's the natural moment to add a convenience wrapper. We're not there yet.

Review questions

  1. API shape. `batchify(bulkFetch, queryKey, resultKey)` requires both a query key and a result key because results in the bulk response may have a different shape from queries. Reasonable, or worth simplifying for the common case where they're equal?
  2. Should `memoize` be unbounded? It currently holds every resolved Promise forever. For caches that live for the lifetime of a page store this is fine; for caches with very wide query spaces it could leak. The current `LazyMobXCache` has the same behavior.
  3. Microtask-tick batching window vs. `accumulatingDebounce` (delay 0). Behaviorally equivalent in practice — both fire on the next macrotask boundary — but worth flagging.

If approved, follow-ups

Each migrating one or two caches:

  1. Per-row hot paths: `MrnaExprRankCache`, `DiscreteCNACache`, `MutationCountCache`, `CopyNumberCountCache`
  2. `getPromise`-using sites (oncoprint/plots): need a small `getPromise`-style helper or inlined `Promise.all`
  3. Remaining incidental caches — most can probably skip `batchify` entirely

Once all subclasses are migrated, the LazyMobXCache hierarchy and ~20 wrapper files can be deleted in a final cleanup PR.

🤖 Generated with Claude Code


View in Codesmith
Need help on this PR? Tag @codesmith with what you need.

  • Let Codesmith autofix CI failures and bot reviews

…he (PoC v2)

Three composable primitives replace the LazyMobXCache hierarchy. Each
addresses one concern, each is independently usable. Cache classes
become 5–10 lines of composition; no inheritance.

  batchify : turns a bulk endpoint into a single-item function that
             auto-coalesces concurrent calls within one microtask tick
             (DataLoader pattern). No caching, no MobX.

  memoize  : promise-level dedup. Same key in flight or resolved →
             same Promise. Rejections evict so callers can retry.

  reactive : wrap a Promise-returning fn so MobX observers can read it
             synchronously during render. Returns null while fetching;
             observable.box flips when it resolves.

VariantCountCache is migrated as a worked example. The pipeline reads:

  reactive(memoize(batchify(bulkFetch, keyOf, keyOf), keyOf), keyOf)

Compared to the v1 PoC (#5563, closed): same call-site shape, but the
internals are three small, generic, recognizable primitives instead of
one fused custom helper. Each layer can be swapped or omitted at
use-site for caches that don't need all three concerns.

LazyMobXCache, the 18 other subclasses, SampleGeneCache, and SimpleCache
are untouched. This PR proposes the building blocks; follow-up PRs
migrate caches one or two at a time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 8, 2026 15:23
@netlify

netlify Bot commented May 8, 2026

Copy link
Copy Markdown

Deploy Preview for cbioportalfrontend ready!

Name Link
🔨 Latest commit f096e21
🔍 Latest deploy log https://app.netlify.com/projects/cbioportalfrontend/deploys/69fdffe41878f6000810a760
😎 Deploy Preview https://deploy-preview-5565.preview.cbioportal.org
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces three small composable primitives (batchify, memoize, reactive) to replace parts of the LazyMobXCache hierarchy, and migrates VariantCountCache to the new layered approach. The goal is to keep batching/dedup/reactivity opt-in per cache rather than inherited via a single fused helper or base class.

Changes:

  • Added src/shared/lib/batchedFetch.ts implementing batchify (microtask batching), memoize (promise dedup), and reactive (MobX observable read-through wrapper).
  • Added unit tests for each primitive and their composition in src/shared/lib/batchedFetch.spec.ts.
  • Migrated VariantCountCache from a LazyMobXCache subclass to a factory that composes the primitives, and updated call sites/types accordingly.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/shared/lib/batchedFetch.ts Adds the new composable batching/memoization/reactive primitives.
src/shared/lib/batchedFetch.spec.ts Adds unit coverage for each primitive and a composition test suite.
src/shared/components/mutationTable/MutationTable.tsx Updates VariantCountCache import to the migrated cache’s exported type.
src/shared/components/mutationTable/column/CohortColumnFormatter.tsx Switches from CacheData to ReactiveEntry for the cohort frequency column.
src/shared/cache/VariantCountCache.ts Migrates VariantCountCache to reactive(memoize(batchify(...))) and exports a factory + type alias.
src/pages/patientView/clinicalInformation/PatientViewPageStore.ts Uses createVariantCountCache(...) instead of instantiating the old class.

Comment on lines +92 to +102
const state = observable.box<{ [key: string]: ReactiveEntry<V> }>(
{},
{ deep: false },
);
const inflight = new Set<string>();

const settle = (key: string, entry: ReactiveEntry<V>) =>
runInAction(() => {
state.set({ ...state.get(), [key]: entry });
inflight.delete(key);
});
@alisman alisman marked this pull request as draft May 15, 2026 19:54
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