Skip to content

feat(appkit-ui): share in-flight useAnalyticsQuery requests - #524

Open
MarioCadenas wants to merge 5 commits into
mainfrom
in-flight-requests
Open

feat(appkit-ui): share in-flight useAnalyticsQuery requests#524
MarioCadenas wants to merge 5 commits into
mainfrom
in-flight-requests

Conversation

@MarioCadenas

@MarioCadenasMarioCadenas commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

What

useAnalyticsQuery deduplicates identical in-flight requests. When multiple components call the hook with the same query key, parameters, format, and dev mode, they now share a single network request instead of each firing its own.

Closes#496.

How

  • New request-store.ts — a generic keyed request store, createRequestStore<S>(idle). It owns only the dedup lifecycle: keyed coalescing, refcount, deferred teardown, and subscribe/notify via useSyncExternalStore. Transport-agnostic — the caller supplies a run(controls) and reports state through controls.patch.
  • analytics-request-store.ts is now an instance of that factory. runAnalyticsRequest wires the transport (SSE for JSON_ARRAY, direct Arrow fetch for ARROW_STREAM) onto controls.patch, and fans both the final result and mid-flight warehouse_status updates out to every subscriber. Its public API (retain/start/subscribe/getSnapshot) is unchanged, so the hook and its tests didn't move.
  • use-analytics-query.ts is a thin useSyncExternalStore subscriber. Cache key = urlSuffix + serialized({parameters, format}). Warehouse-status mirroring into ResourceStatusProvider lives in a hook effect.
  • Lifecycle: a keyed entry lives as long as it has subscribers. Teardown is deferred one tick after the last unsubscribe, so a React StrictMode unmount→remount (or fast route swap) reuses the in-flight request instead of aborting and refetching. Late subscribers read the current snapshot immediately (including an already-resolved result). Dedup-only — no cross-lifecycle result cache.

useChartData and all charts route through useAnalyticsQuery, so they inherit dedup for free. UseAnalyticsQueryResult is unchanged — non-breaking.

Follow-up: useMetricView should adopt the same factory

request-store.ts was extracted as a generic factory precisely so the second analytics hook can share the mechanism. useMetricView currently hand-rolls its own weaker lifecycle (effectLease + queueMicrotask + activeRequestKeyRef) that only dedups a StrictMode remount within a single instance — two separate components with identical args still fire two requests.

A follow-up PR should migrate useMetricView onto createRequestStore, deleting that bespoke lifecycle and gaining cross-instance dedup for free. It needs two small additions to the factory when it lands: a current() control (for its shape-based "keep stale rows during a same-shape revalidation" policy) and a metadata field in its own snapshot type. Kept out of this PR to avoid rewriting a freshly-shipped hook + its ~40 tests here.

Showcase

New /query-dedup playground route (Data → "Query Dedup"). It wraps window.fetch while mounted to count analytics POSTs in-page, so you can watch "N components mounted → 1 network request fired" without the DevTools Network tab. Buttons mount more panels (count stays 1) and give the last panel a different key (count ticks to 2, proving distinct keys still fan out).

Testing

  • appkit-ui unit suite: 548 passed / 26 files — includes the new request-store factory-contract tests, the useAnalyticsQuery dedup + store-lifecycle tests, and the warehouse-status integration tests (test files reset the singleton store between cases).
  • Playground integration specs (Playwright): 10 passed against a freshly-built dist — dedup collapses per-signature request counts as expected.
  • typecheck, knip, and oxlint/oxfmt (post-chore: migrate from Biome to oxlint + oxfmt #538 toolchain): clean.

@github-actions

github-actionsBot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle size report

Compared against bundle-size-baseline.json (main).

@databricks/appkit

npm tarball (packed): 840 KB (+173 B) — gzipped download (dist + bin; excludes release-only docs/NOTICE).

distrawgzip
JS (runtime)869 KB303 KB
Type declarations315 KB109 KB
Source maps1.7 MB (+159 B)566 KB (+143 B)
Other11 KB3.7 KB
Total2.9 MB (+159 B)982 KB (+143 B)
Per-entry composition (own code — deps external (as shipped))
EntryInitial (gz)Lazy (gz)Total (gz)node_modules (min)Own code (min)
.88 KB2.5 KB91 KBexternal288 KB
./beta49 KB457 B49 KBexternal143 KB
./type-generator21 KB0 B21 KBexternal61 KB

Chunks:

EntryChunkLoadSize (gz)
.index.jsinitial84 KB
.utils.jsinitial4.0 KB
.remote-tunnel-manager.jslazy2.5 KB
./betabeta.jsinitial33 KB
./betastream-manager.jsinitial5.8 KB
./betawide-event-emitter.jsinitial3.2 KB
./betadatabricks.jsinitial3.0 KB
./betaconfiguration.jsinitial2.1 KB
./betaservice-context.jsinitial1.3 KB
./betaclient.jsinitial434 B
./betaclient-options.jsinitial220 B
./betasupervisor-api.jslazy192 B
./betadatabricks.jslazy142 B
./betaindex.jslazy123 B
./type-generatorindex.jsinitial21 KB

@databricks/appkit-ui

npm tarball (packed): 348 KB (+5.7 KB) — gzipped download (dist + bin; excludes release-only docs/NOTICE).

distrawgzip
JS (runtime)394 KB (+4.0 KB)132 KB (+1.7 KB)
Type declarations228 KB (+341 B)83 KB (+152 B)
Source maps764 KB (+11 KB)252 KB (+4.4 KB)
CSS16 KB (-462 B)3.2 KB (-90 B)
Total1.4 MB (+15 KB)471 KB (+6.2 KB)
Per-entry composition (consumer bundle — deps bundled, peerDeps external)
EntryInitial (gz)Lazy (gz)Total (gz)node_modules (min)Own code (min)
./js5.3 KB49 KB55 KB208 KB14 KB
./js/beta20 B0 B20 B0 B0 B
./react432 KB (+578 B)49 KB481 KB (+578 B)1.3 MB (+18 B)177 KB (+1.3 KB)
./react/beta1.0 KB0 B1.0 KB0 B1.9 KB

Chunks:

EntryChunkLoadSize (gz)
./jsindex.jsinitial5.2 KB
./jschunkinitial120 B
./jsapache-arrowlazy49 KB
./js/betabeta.jsinitial20 B
./reactindex.jsinitial430 KB
./reacttslibinitial2.1 KB
./reactapache-arrowlazy49 KB
./react/betabeta.jsinitial1.0 KB

@github-actions

github-actionsBot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

🤖 AppKit PR bot

🔬 Run evals

Start an eval for this PR from the evals-monitor app: Go to Evals Monitor →

📦 Try this PR's app template

Scaffolds a new app from this PR's SDK build. Run it in any folder (requires the GitHub CLI — gh auth login — and the Databricks CLI):

gh run download 32270171607 -R databricks/appkit -n appkit-template-0.61.1-pr.beb493c-in-flight-requests-524 -D appkit-pr-524 \
&& unzip -o "appkit-pr-524/appkit-template-0.61.1-pr.beb493c-in-flight-requests-524.zip" -d "appkit-pr-524" \
&& databricks apps init --template "appkit-pr-524"

The template pins @databricks/appkit and @databricks/appkit-ui to tarballs built from this branch, so the scaffolded app runs against this PR's code.

@MarioCadenas
MarioCadenasforce-pushed the in-flight-requests branch 2 times, most recently from d2c6b27 to 70f200eCompareAugust 13, 2026 10:55
@MarioCadenas
MarioCadenas marked this pull request as ready for review August 13, 2026 10:57
@MarioCadenas
MarioCadenas requested a review from a team as a code ownerAugust 13, 2026 10:57
Identical analytics requests (same query key, parameters, format, and dev
mode) now share a single in-flight network request instead of one per hook
instance. A module-singleton request store (mirroring the ResourceStatusStore
idiom) owns the transport; useAnalyticsQuery becomes a useSyncExternalStore
subscriber. Late subscribers read the current snapshot; the request is torn
down a tick after the last subscriber unmounts, so a StrictMode
unmount->remount reuses it rather than aborting.
useChartData and all charts inherit the dedup for free. Dedup-only, no result
cache. UseAnalyticsQueryResult is unchanged (non-breaking).
Adds a /query-dedup playground route that counts analytics fetches in-page to
make the behavior observable.
Closes#496
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
…ery dedup
The arrow-analytics and data-visualization integration specs asserted one
network request per chart instance (times a StrictMode x2 multiplier). With
shared in-flight requests, components resolving to the same (queryKey,
parameters, format) signature now share one request, and deferred teardown
means StrictMode remounts reuse it rather than refiring — so the multiplier no
longer applies to request counts.
Update expectations to the deduplicated counts (each key collapses to one
request per distinct resolved format) and drop the now-unused
STRICT_MODE_MULTIPLIER.
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Quality-only cleanup of the shared-request code, behavior unchanged:
- use-analytics-query: drop the redundant `cacheKey === null` guards and the
NOOP_SUBSCRIBE constant (a verbatim copy of the one in use-resource-status).
The store already returns the stable idle snapshot for unused keys, so the
cache key is computed unconditionally; only the `retain` and error-field
guards on `payload === null` remain. Un-export EMPTY_SNAPSHOT from the store.
- query-dedup demo route: replace the hand-rolled useSyncExternalStore counter
with a plain useState (single consumer).
- Trim verbose/duplicated comments across the store, hook, and integration
specs to their load-bearing facts.
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Split the analytics-query dedup store into a reusable core so useMetricView
can adopt the same mechanism (follow-up) instead of hand-rolling its own.
- New request-store.ts: createRequestStore<S>(idle) owns the lifecycle only —
keyed coalescing, refcount, deferred teardown, subscribe/getSnapshot. Runs
are transport-agnostic; controls surface is { signal, abort, patch }.
- analytics-request-store.ts becomes an instance: runAnalyticsRequest wires
the SSE/Arrow transport onto controls.patch. Public API (retain/start/
subscribe/getSnapshot/resetAnalyticsRequestStore) unchanged, so the hook and
its tests are untouched.
- Factory-contract tests in __tests__/request-store.test.ts.
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Sign up for freeto 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.

useAnalyticsQuery issues duplicate network requests for identical queries across hook instances

1 participant

@MarioCadenas