-
Notifications
You must be signed in to change notification settings - Fork 0
⚡ Bolt: badge evaluation query collapsing with transient EvaluationCache #115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
04ab24e
84efec6
82cf117
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -51,6 +51,68 @@ export interface BadgeEvaluationResult { | |
| totalXpGained: number; | ||
| } | ||
|
|
||
| /** | ||
| * Transient cache to collapse duplicate database queries into a single promise | ||
| * during a badge evaluation run. | ||
| */ | ||
| class EvaluationCache { | ||
| private userId: string; | ||
| private completedCountPromise: Promise<number> | null = null; | ||
| private toolSessionsCountPromises = new Map<string, Promise<number>>(); | ||
| private genericToolSessionsCountPromise: Promise<number> | null = null; | ||
| private userPromise: Promise<{ streakDays: number; xp: number } | null> | null = null; | ||
|
|
||
| constructor(userId: string) { | ||
| this.userId = userId; | ||
| } | ||
|
|
||
| getCompletedCount(): Promise<number> { | ||
| if (!this.completedCountPromise) { | ||
| this.completedCountPromise = db.lessonProgress.count({ | ||
| where: { userId: this.userId, status: 'COMPLETED' }, | ||
| }); | ||
| } | ||
| return this.completedCountPromise; | ||
| } | ||
|
|
||
| getToolSessionsCount(toolType?: string): Promise<number> { | ||
| if (toolType) { | ||
| let p = this.toolSessionsCountPromises.get(toolType); | ||
| if (!p) { | ||
| p = db.toolSession.count({ | ||
| where: { | ||
| userId: this.userId, | ||
| status: 'GRADED', | ||
| toolType, | ||
| }, | ||
| }); | ||
| this.toolSessionsCountPromises.set(toolType, p); | ||
| } | ||
| return p; | ||
| } else { | ||
| if (!this.genericToolSessionsCountPromise) { | ||
| this.genericToolSessionsCountPromise = db.toolSession.count({ | ||
| where: { | ||
| userId: this.userId, | ||
| status: 'GRADED', | ||
| }, | ||
| }); | ||
| } | ||
| return this.genericToolSessionsCountPromise; | ||
| } | ||
| } | ||
|
|
||
| getUser(): Promise<{ streakDays: number; xp: number } | null> { | ||
| if (!this.userPromise) { | ||
| this.userPromise = db.user.findUnique({ | ||
| where: { id: this.userId }, | ||
| select: { streakDays: true, xp: true }, | ||
| }) as Promise<{ streakDays: number; xp: number } | null>; | ||
| } | ||
| return this.userPromise; | ||
| } | ||
| } | ||
|
|
||
|
Comment on lines
+54
to
+115
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win Add cache regression tests. This change adds database-call caching, but this PR has no changed test segment. Add tests that evaluate multiple badges and assert that duplicate lesson, user, and same-scope tool-session queries execute once. Also assert that different As per coding guidelines, “New features must include tests; admin and business-layer features require tests.” 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| /** | ||
| * Evaluate all badges for a user against the current database state. Award any | ||
| * newly-earned ones. Idempotent — re-running with no new events returns | ||
|
|
@@ -95,6 +157,8 @@ export async function evaluateBadges( | |
| xpReward: number; | ||
| }> = []; | ||
|
|
||
| const cache = new EvaluationCache(userId); | ||
|
|
||
| for (const badge of published) { | ||
| if (alreadyAwardedSet.has(badge.id)) continue; | ||
|
|
||
|
|
@@ -106,7 +170,7 @@ export async function evaluateBadges( | |
| continue; | ||
| } | ||
|
|
||
| const qualifies = await checkCriteria(userId, criteria, event); | ||
| const qualifies = await checkCriteria(userId, criteria, event, cache); | ||
| if (qualifies) earnedNow.push(badge); | ||
| } | ||
|
|
||
|
|
@@ -146,12 +210,11 @@ async function checkCriteria( | |
| userId: string, | ||
| criteria: BadgeCriteria, | ||
| event: BadgeTrigger, | ||
| cache: EvaluationCache, | ||
| ): Promise<boolean> { | ||
| switch (criteria.type) { | ||
| case 'module_complete': { | ||
| const completedCount = await db.lessonProgress.count({ | ||
| where: { userId, status: 'COMPLETED' }, | ||
| }); | ||
| const completedCount = await cache.getCompletedCount(); | ||
| // Treat each completed lesson as progress toward module_complete; the | ||
| // seed threshold is 1 so this triggers after the first lesson. | ||
| return completedCount >= criteria.threshold; | ||
|
|
@@ -165,30 +228,18 @@ async function checkCriteria( | |
|
|
||
| case 'tool_sessions': { | ||
| const scopeToolType = criteria.scope?.toolType; | ||
| const count = await db.toolSession.count({ | ||
| where: { | ||
| userId, | ||
| status: 'GRADED', | ||
| ...(scopeToolType ? { toolType: scopeToolType } : {}), | ||
| }, | ||
| }); | ||
| const count = await cache.getToolSessionsCount(scopeToolType); | ||
| return count >= criteria.threshold; | ||
| } | ||
|
|
||
| case 'streak_days': { | ||
| const user = await db.user.findUnique({ | ||
| where: { id: userId }, | ||
| select: { streakDays: true }, | ||
| }); | ||
| const user = await cache.getUser(); | ||
| if (!user) return false; | ||
| return user.streakDays >= criteria.threshold; | ||
| } | ||
|
|
||
| case 'xp_threshold': { | ||
| const user = await db.user.findUnique({ | ||
| where: { id: userId }, | ||
| select: { xp: true }, | ||
| }); | ||
| const user = await cache.getUser(); | ||
| if (!user) return false; | ||
| return user.xp >= criteria.threshold; | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Use plain definitions for the new cache behavior.
The new text uses terms such as “transient cache” and “query promises” without definitions.
src/lib/badges.ts#L54-L56: Explain that the cache exists only during one badge evaluation and reuses the same database request..jules/bolt.md#L7-L9: Replace or define “transient,” “query promises,” and “evaluation lifecycle” in direct language.As per coding guidelines, use direct, plain-spoken language for the Filipino VA audience and define jargon.
📍 Affects 2 files
src/lib/badges.ts#L54-L56(this comment).jules/bolt.md#L7-L9🤖 Prompt for AI Agents
Source: Coding guidelines