Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 15 additions & 9 deletions redis/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,10 @@ export const CacheSet = {

type MethodName = 'dashboardData' | 'paymentList' | 'addressBalance' | 'paymentsCount'

type InFlightCalls = Partial<Record<MethodName, Promise<unknown>>>

interface PendingCalls {
[userId: string]: Set<MethodName>
[userId: string]: InFlightCalls
}

export class CacheGet {
Expand All @@ -89,21 +91,25 @@ export class CacheGet {
fn: () => Promise<T>
): Promise<T> {
if (this.pendingCalls[userId] === undefined) {
this.pendingCalls[userId] = new Set()
this.pendingCalls[userId] = {}
}

if (this.pendingCalls[userId].has(methodName)) {
throw new Error(`Method "${methodName}" is already being executed for user "${userId}".`)
const existingCall = this.pendingCalls[userId][methodName]
if (existingCall !== undefined) {
return await (existingCall as Promise<T>)
}

this.pendingCalls[userId].add(methodName)
const pendingCall = fn()
this.pendingCalls[userId][methodName] = pendingCall as Promise<unknown>
Comment on lines +97 to +103
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Deduplication key is not argument-safe for dashboardData/paymentsCount.

At Line 97, in-flight reuse is keyed only by [userId][methodName]. Concurrent requests with different timezone/buttonIds can incorrectly share one Promise and return the wrong payload.

💡 Suggested fix (argument-aware keying)
-  private static async executeCall<T>(
-    userId: string,
-    methodName: MethodName,
-    fn: () => Promise<T>
-  ): Promise<T> {
+  private static async executeCall<T>(
+    scopeId: string,
+    methodName: MethodName,
+    fn: () => Promise<T>
+  ): Promise<T> {
-    if (this.pendingCalls[userId] === undefined) {
-      this.pendingCalls[userId] = {}
+    if (this.pendingCalls[scopeId] === undefined) {
+      this.pendingCalls[scopeId] = {}
     }

-    const existingCall = this.pendingCalls[userId][methodName]
+    const existingCall = this.pendingCalls[scopeId][methodName]
     if (existingCall !== undefined) {
       return await (existingCall as Promise<T>)
     }

     const pendingCall = fn()
-    this.pendingCalls[userId][methodName] = pendingCall as Promise<unknown>
+    this.pendingCalls[scopeId][methodName] = pendingCall as Promise<unknown>
// call-site examples
return await this.executeCall(`${userId}:${timezone}:${buttonIds?.join(',') ?? ''}`, 'dashboardData', async () => ...)
return await this.executeCall(`${userId}:${timezone}`, 'paymentsCount', async () => ...)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@redis/index.ts` around lines 97 - 103, The dedupe currently keys in-flight
requests only by this.pendingCalls[userId][methodName], which causes
different-argument calls (e.g., dashboardData with different timezone/buttonIds)
to incorrectly share a Promise; change the keying to include an argument-aware
dedupe key: add or compute a dedupeKey parameter (e.g.,
`${userId}:${timezone}:${buttonIds?.join(',')}` or `${userId}:${timezone}`) and
use this.pendingCalls[userId][dedupeKey] (or a nested Map keyed by methodName
then dedupeKey) when reading/setting existingCall and pendingCall; update the
call sites that invoke executeCall to pass the appropriate dedupeKey for
dashboardData/paymentsCount so concurrent calls with different args do not
collide.


try {
return await fn()
return await pendingCall
} finally {
this.pendingCalls[userId].delete(methodName)
if (this.pendingCalls[userId].size === 0) {
this.pendingCalls[userId] = undefined as unknown as Set<MethodName>
if (this.pendingCalls[userId]?.[methodName] === pendingCall) {
this.pendingCalls[userId][methodName] = undefined
}
if (this.pendingCalls[userId] !== undefined && Object.keys(this.pendingCalls[userId]).length === 0) {
this.pendingCalls[userId] = {}
}
}
}
Expand Down
Loading