11package controllers
22
33import jakarta .inject .{Inject , Singleton }
4+ import models .domain .genomics .VariantGroup
45import org .webjars .play .WebJarsUtil
6+ import play .api .cache .AsyncCacheApi
57import play .api .i18n .I18nSupport
68import play .api .mvc .*
79import repositories .{HaplogroupVariantRepository , VariantAliasRepository , VariantRepository }
810
11+ import scala .concurrent .duration .*
912import scala .concurrent .{ExecutionContext , Future }
1013
1114/**
1215 * Public controller for browsing variants (read-only).
1316 * Provides a searchable variant database for researchers.
17+ * Results are cached to improve performance for the public view.
1418 */
1519@ Singleton
1620class VariantBrowserController @ Inject ()(
1721 val controllerComponents : ControllerComponents ,
1822 variantRepository : VariantRepository ,
1923 variantAliasRepository : VariantAliasRepository ,
20- haplogroupVariantRepository : HaplogroupVariantRepository
24+ haplogroupVariantRepository : HaplogroupVariantRepository ,
25+ cache : AsyncCacheApi
2126 )(using webJarsUtil : WebJarsUtil , ec : ExecutionContext )
2227 extends BaseController with I18nSupport {
2328
2429 private val DefaultPageSize = 25
2530
31+ // Cache durations - public view can be stale
32+ private val SearchCacheDuration = 15 .minutes
33+ private val DetailCacheDuration = 1 .hour
34+ private val TotalCountCacheDuration = 30 .minutes
35+
2636 /**
2737 * Main variant browser page with search functionality.
2838 */
2939 def index (query : Option [String ], page : Int , pageSize : Int ): Action [AnyContent ] = Action .async {
3040 implicit request : Request [AnyContent ] =>
3141 val offset = (page - 1 ) * pageSize
3242 for {
33- (variantGroups, totalCount) <- variantRepository.searchGroupedPaginated (query.getOrElse(" " ), offset, pageSize)
43+ (variantGroups, totalCount) <- getCachedSearchResults (query.getOrElse(" " ), offset, pageSize)
3444 } yield {
3545 val totalPages = Math .max(1 , (totalCount + pageSize - 1 ) / pageSize)
3646 Ok (views.html.variants.browser(variantGroups, query, page, totalPages, pageSize, totalCount))
@@ -44,7 +54,7 @@ class VariantBrowserController @Inject()(
4454 implicit request : Request [AnyContent ] =>
4555 val offset = (page - 1 ) * pageSize
4656 for {
47- (variantGroups, totalCount) <- variantRepository.searchGroupedPaginated (query.getOrElse(" " ), offset, pageSize)
57+ (variantGroups, totalCount) <- getCachedSearchResults (query.getOrElse(" " ), offset, pageSize)
4858 } yield {
4959 val totalPages = Math .max(1 , (totalCount + pageSize - 1 ) / pageSize)
5060 Ok (views.html.variants.listFragment(variantGroups, query, page, totalPages, pageSize, totalCount))
@@ -55,23 +65,46 @@ class VariantBrowserController @Inject()(
5565 * HTMX fragment for variant detail panel (read-only).
5666 */
5767 def detailPanel (id : Int ): Action [AnyContent ] = Action .async { implicit request : Request [AnyContent ] =>
58- for {
59- variantOpt <- variantRepository.findByIdWithContig(id)
60- allVariantsInGroup <- variantOpt match {
61- case Some (vwc) =>
62- val groupKey = vwc.variant.commonName.orElse(vwc.variant.rsId).getOrElse(s " variant_ ${id}" )
63- variantRepository.getVariantsByGroupKey(groupKey)
64- case None => Future .successful(Seq .empty)
65- }
66- aliases <- variantAliasRepository.findByVariantId(id)
67- haplogroups <- haplogroupVariantRepository.getHaplogroupsByVariant(id)
68- } yield {
69- variantOpt match {
70- case Some (variantWithContig) =>
71- val variantGroup = variantRepository.groupVariants(allVariantsInGroup).headOption
72- Ok (views.html.variants.detailPanel(variantWithContig, variantGroup, aliases, haplogroups))
73- case None =>
74- NotFound (" Variant not found" )
68+ getCachedDetailPanel(id)
69+ }
70+
71+ // === Caching helpers ===
72+
73+ /**
74+ * Get cached search results or fetch from database.
75+ * Cache key includes query, offset, and limit for proper pagination caching.
76+ */
77+ private def getCachedSearchResults (query : String , offset : Int , limit : Int ): Future [(Seq [VariantGroup ], Int )] = {
78+ val cacheKey = s " variant-search: ${query.toLowerCase.trim}: $offset: $limit"
79+ cache.getOrElseUpdate(cacheKey, SearchCacheDuration ) {
80+ variantRepository.searchGroupedPaginated(query, offset, limit)
81+ }
82+ }
83+
84+ /**
85+ * Get cached detail panel or fetch from database.
86+ */
87+ private def getCachedDetailPanel (id : Int )(implicit request : Request [AnyContent ]): Future [Result ] = {
88+ val cacheKey = s " variant-detail: $id"
89+ cache.getOrElseUpdate(cacheKey, DetailCacheDuration ) {
90+ for {
91+ variantOpt <- variantRepository.findByIdWithContig(id)
92+ allVariantsInGroup <- variantOpt match {
93+ case Some (vwc) =>
94+ val groupKey = vwc.variant.commonName.orElse(vwc.variant.rsId).getOrElse(s " variant_ ${id}" )
95+ variantRepository.getVariantsByGroupKey(groupKey)
96+ case None => Future .successful(Seq .empty)
97+ }
98+ aliases <- variantAliasRepository.findByVariantId(id)
99+ haplogroups <- haplogroupVariantRepository.getHaplogroupsByVariant(id)
100+ } yield {
101+ variantOpt match {
102+ case Some (variantWithContig) =>
103+ val variantGroup = variantRepository.groupVariants(allVariantsInGroup).headOption
104+ Ok (views.html.variants.detailPanel(variantWithContig, variantGroup, aliases, haplogroups))
105+ case None =>
106+ NotFound (" Variant not found" )
107+ }
75108 }
76109 }
77110 }
0 commit comments