From 1ed99695e9b04339466f50d44809be840e9b960e Mon Sep 17 00:00:00 2001 From: Dylan Audius Date: Fri, 5 Jun 2026 20:05:35 -0700 Subject: [PATCH] feat(contests): prime entry-count cache from /events/entity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Surfaces that resolve a contest via the events-by-entity-id batcher (the track-page contest section, cold/deep-linked contest pages, and web Explore's featured contests, which go through useExploreContent → ContestCard rather than the already-optimized useAllRemixContests) had no primed entry count, so each ContestCard fired its own /tracks/{id}/remixes?only_contest_entries=true&limit=0 just to render the entry-count badge — an N+1 on web Explore. The backend now returns related.entry_counts on /events/entity (keyed by the contest's parent track hashid). Read it in getEventsByEntityIdBatcher and prime getRemixesCountQueryKey({ trackId, isContestEntry: true }), mirroring useAllRemixContests / useUserRemixContests. The batcher already collapses all cards into one /events/entity request, so this drops the per-card count requests to zero. Also threads `related` through the generated EventsResponse SDK model (matches the swagger change reusing remix_contests_related). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../batchers/getEventsByEntityIdBatcher.ts | 26 ++++++++++++++++--- .../default/models/EventsResponse.ts | 22 +++++++++++++--- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/packages/common/src/api/tan-query/batchers/getEventsByEntityIdBatcher.ts b/packages/common/src/api/tan-query/batchers/getEventsByEntityIdBatcher.ts index 6cc4d2c2f1c..de7bdb09940 100644 --- a/packages/common/src/api/tan-query/batchers/getEventsByEntityIdBatcher.ts +++ b/packages/common/src/api/tan-query/batchers/getEventsByEntityIdBatcher.ts @@ -1,10 +1,12 @@ -import { Id, OptionalId } from '@audius/sdk' +import { Id, OptionalHashId, OptionalId } from '@audius/sdk' import { create, windowScheduler } from '@yornaath/batshit' import { memoize } from 'lodash' import { eventMetadataListFromSDK } from '~/adapters/event' import { ID, Event } from '~/models' +import { getRemixesCountQueryKey } from '../remixes/useRemixes' + import { contextCacheResolver } from './contextCacheResolver' import { BatchContext } from './types' @@ -12,13 +14,31 @@ export const getEventsByEntityIdBatcher = memoize( (context: BatchContext) => create({ fetcher: async (entityIds: ID[]): Promise => { - const { sdk, currentUserId } = context + const { sdk, currentUserId, queryClient } = context if (!entityIds.length) return [] - const { data } = await sdk.events.getEntityEvents({ + const { data, related } = await sdk.events.getEntityEvents({ entityId: entityIds.map((entityId) => Id.parse(entityId)), userId: OptionalId.parse(currentUserId) }) + // Prime the dedicated `useRemixesCount` cache from the entry counts + // delivered alongside the event list (keyed by the contest's parent + // track hashid). This turns ContestCard's entry-count badge into a + // cache hit so surfaces that resolve contests via this endpoint (the + // track-page contest section, cold contest pages, web Explore's + // featured contests) don't fire a count-only + // `/tracks/{id}/remixes?limit=0` per card. Same pattern as + // useAllRemixContests / useUserRemixContests. + const entryCounts = related?.entryCounts ?? {} + for (const [hashedTrackId, count] of Object.entries(entryCounts)) { + const trackId = OptionalHashId.parse(hashedTrackId) + if (!trackId) continue + queryClient.setQueryData( + getRemixesCountQueryKey({ trackId, isContestEntry: true }), + count + ) + } + return eventMetadataListFromSDK(data) }, resolver: (events: Event[], entityId: ID) => diff --git a/packages/sdk/src/sdk/api/generated/default/models/EventsResponse.ts b/packages/sdk/src/sdk/api/generated/default/models/EventsResponse.ts index 3460c025e74..894c7f349bf 100644 --- a/packages/sdk/src/sdk/api/generated/default/models/EventsResponse.ts +++ b/packages/sdk/src/sdk/api/generated/default/models/EventsResponse.ts @@ -19,19 +19,31 @@ import { EventFromJSONTyped, EventToJSON, } from './Event'; +import type { RemixContestsRelated } from './RemixContestsRelated'; +import { + RemixContestsRelatedFromJSON, + RemixContestsRelatedFromJSONTyped, + RemixContestsRelatedToJSON, +} from './RemixContestsRelated'; /** - * + * * @export * @interface EventsResponse */ export interface EventsResponse { /** - * + * * @type {Array} * @memberof EventsResponse */ data?: Array; + /** + * + * @type {RemixContestsRelated} + * @memberof EventsResponse + */ + related?: RemixContestsRelated; } /** @@ -52,8 +64,9 @@ export function EventsResponseFromJSONTyped(json: any, ignoreDiscriminator: bool return json; } return { - + 'data': !exists(json, 'data') ? undefined : ((json['data'] as Array).map(EventFromJSON)), + 'related': !exists(json, 'related') ? undefined : RemixContestsRelatedFromJSON(json['related']), }; } @@ -65,8 +78,9 @@ export function EventsResponseToJSON(value?: EventsResponse | null): any { return null; } return { - + 'data': value.data === undefined ? undefined : ((value.data as Array).map(EventToJSON)), + 'related': RemixContestsRelatedToJSON(value.related), }; }