CS-11121: dedup concurrent same-key store.search calls during prerender#4883
Conversation
Add an in-flight Map on StoreService keyed by (realms, query) so multiple Glimmer components rendering on the same prerender page that fire identical store.search calls share one `_federated-search` round-trip instead of each issuing its own. Mirrors the server-side `RealmIndexQueryEngine.#inFlightSearch` landed in CS-11115 Phase 1, with the same identity-checked finally cleanup. Gated on `__boxelRenderContext` so live user searches stay uncoalesced — write-then-read freshness story unchanged outside prerender. Refactor `fetchSearchData` and `fetchAndHydrateSearchResults` onto a shared `fetchSearchDoc` HTTP+JSON path so both benefit from the dedup. Wire `clearInFlightSearch()` to render-route deactivate so an in-flight entry doesn't leak across visits. The sibling resolved-doc job-scoped cache (CS-11175) stacks on top of this layer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Preview deploymentsHost Test Results 1 files ± 0 1 suites ±0 3h 21m 2s ⏱️ + 1h 48m 36s Results for commit 18320f8. ± Comparison against earlier commit b8a9b60. Realm Server Test Results 1 files ±0 1 suites ±0 9m 0s ⏱️ + 1m 7s Results for commit 18320f8. ± Comparison against earlier commit b8a9b60. |
There was a problem hiding this comment.
Pull request overview
This PR reduces duplicate _federated-search HTTP round-trips during prerender by coalescing concurrent store.search(realms, query) calls onto a single in-flight promise (gated to prerender via __boxelRenderContext). It also refactors search fetching so both hydrated and data-only search paths share the same HTTP/JSON implementation, and ensures in-flight entries are cleared on render-route deactivation.
Changes:
- Add prerender-only in-flight deduping for concurrent same-key searches in
StoreService. - Refactor search fetching to a shared
fetchSearchDoc()path used by both hydration and data-only callers. - Add unit + integration tests for key stability and coalescing behavior; clear in-flight search entries on render route deactivate.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/host/app/services/store.ts | Adds inflightSearch map + clearInFlightSearch(), refactors search fetching into fetchSearchDoc() with prerender-gated coalescing. |
| packages/host/app/routes/render.ts | Clears store in-flight search map on deactivate() to prevent cross-visit coalescing. |
| packages/host/app/lib/search-in-flight-key.ts | Introduces a stable (realms, query) signature helper for in-flight dedup keys. |
| packages/host/tests/unit/lib/search-in-flight-key-test.ts | Unit tests for key stability and differentiation across realms/query/pagination. |
| packages/host/tests/integration/resources/search-test.ts | Integration tests verifying coalescing behavior in prerender vs live-SPA and clearInFlightSearch() behavior. |
Comments suppressed due to low confidence (4)
packages/host/tests/integration/resources/search-test.ts:772
- This assertion relies on
setTimeout(10)to give the twostore.searchcalls time to reach the fetch hook. That introduces timing-based flakiness. Use a deterministic wait (likewaitUntil(() => fetchCalls === 2)) before asserting.
await new Promise((r) => setTimeout(r, 10));
packages/host/tests/integration/resources/search-test.ts:802
- The test waits using a fixed
setTimeout(10)before assertingfetchCalls. On slow machines the fetch interception may not have been hit yet, causing intermittent failures. Consider waiting untilfetchCallsreaches the expected value instead of sleeping.
let p1 = storeService.search(bookQuery, [testRealmURL]);
let p2 = storeService.search(otherQuery, [testRealmURL]);
await new Promise((r) => setTimeout(r, 10));
packages/host/tests/integration/resources/search-test.ts:842
- Using
setTimeout(10)to ensure the first fetch is in-flight can be flaky if the async chain hasn't executed within 10ms. Prefer a deterministic wait (e.g.,waitUntil(() => fetchCalls === 1)) before asserting.
let p1 = storeService.search(bookQuery, [testRealmURL]);
await new Promise((r) => setTimeout(r, 10));
assert.strictEqual(fetchCalls, 1, 'first fetch in-flight');
packages/host/tests/integration/resources/search-test.ts:849
- This check again depends on
setTimeout(10)for scheduling. To avoid timing-related flakes, wait untilfetchCallsis observed to be2(or at least>= 2) before asserting.
let p2 = storeService.search(bookQuery, [testRealmURL]);
await new Promise((r) => setTimeout(r, 10));
assert.strictEqual(
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Address PR feedback: replace `setTimeout(10)` waits with synchronous assertions, and narrow the fetch counter to `_federated-search` URLs. The whole sync portion of `search → fetchSearchData → fetchSearchDoc → maybeAuthedFetchForRealms` runs before each `storeService.search()` expression returns. The wrapped fetch's `fetchCalls++` is the first line before any `await`, so by the time both invocations have returned their Promises the counter reflects every fetch that's been committed. No timeout needed — and a broken dedup would tick the counter immediately too, so the test is strictly more sensitive than the timed variant. The URL filter scopes the counter to the store-search hot path, so unrelated `maybeAuthedFetchForRealms` traffic (auth probes, registry pings, future callers) can't inflate it and turn the dedup assertion flaky. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…n-flight-search-dedup-during-prerender-host-spa # Conflicts: # packages/host/app/services/store.ts
Summary
Adds an in-flight
Map<key, Promise<doc>>onStoreServiceso concurrent same-(realms, query)store.searchcalls during a prerender share a single_federated-searchround-trip. MirrorsRealmIndexQueryEngine.#inFlightSearch(CS-11115 Phase 1) on the host SPA.__boxelRenderContextso live user searches stay uncoalesced — write-then-read freshness story unchanged outside prerender.fetchSearchDataandfetchAndHydrateSearchResultsonto a sharedfetchSearchDocHTTP+JSON path so both benefit..finally()with identity check (same shape as the server-side reference).clearInFlightSearch()wired to render-route deactivate so an in-flight entry doesn't leak across visits.This is the host-side Phase 1. The sibling resolved-doc job-scoped cache (CS-11175) stacks on top of this layer; it answers from already-resolved bytes so sequential repeats inside a render get a sync hit, while this PR closes the concurrent-overlap window.
Linear: CS-11121
Test plan
Unit | Utility | searchInFlightKey (host)passes — eight key-composition cases (order invariance, distinct queries / realms / pagination, etc.).Integration | search resource > in-flight search dedupmodule passes — five behavior cases:clearInFlightSearch()drops pending entries so new same-key callers re-fetch._federated-searchHTTP requests for cohort-shaped fan-out (verifiable once stacked with CS-11175 against the/ambitious/prianhabench).🤖 Generated with Claude Code