Skip to content

fix: Attempted fix for dashboard not loading sometimes#1109

Open
Klakurka wants to merge 2 commits intomasterfrom
fix/dashboard-not-loading
Open

fix: Attempted fix for dashboard not loading sometimes#1109
Klakurka wants to merge 2 commits intomasterfrom
fix/dashboard-not-loading

Conversation

@Klakurka
Copy link
Member

@Klakurka Klakurka commented Feb 20, 2026

Related to #1098

Description

Sometimes (especially after a re-deploy) the dashboard doesn't load without doing an extra refresh.

Test plan

I'm not 100% how to reproduce it without trying this for real in prod but perhaps there is a good way? We can also give it a quick deploy in prod together.

Summary by CodeRabbit

  • Bug Fixes

    • Concurrent requests now return the existing in-flight result instead of raising an error, improving reliability for identical simultaneous requests.
    • Per-user request tracking and cleanup corrected to avoid stale state after calls complete.
  • Refactor

    • Internal request-tracking updated to store and manage in-flight results per method for more predictable behavior.

@Klakurka Klakurka self-assigned this Feb 20, 2026
@Klakurka Klakurka added the bug Something isn't working label Feb 20, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 20, 2026

📝 Walkthrough

Walkthrough

Modified in-flight method call tracking in the Redis module from a per-user Set of method names to a per-user map of method name → Promise. Duplicate concurrent calls now return the existing Promise instead of throwing; types and cleanup logic adjusted accordingly.

Changes

Cohort / File(s) Summary
Redis in-flight tracking
redis/index.ts
Added InFlightCalls type and changed PendingCalls from Set<MethodName> to a Partial<Record<MethodName, Promise<unknown>>>. Initialize per-user entries as {}. Store promises per method, return existing promise on duplicate calls, and clear only matching entries in finally.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Suggested reviewers

  • lissavxo

Poem

🐇 I nudge a promise, soft and bright,

two callers knock — I hold it tight.
No clash, no throw, just one shared way,
a single hop that saves the day. ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title describes the intended fix for a dashboard loading issue, which matches the changeset's modification of promise handling to prevent duplicate concurrent requests.
Description check ✅ Passed The description includes the related issue (#1098), a clear non-technical explanation of the problem, and a test plan, covering all required template sections.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/dashboard-not-loading

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@redis/index.ts`:
- Around line 97-100: The code double-awaits and miscasts the resolved value:
change the return to cast the stored promise before awaiting so you await the
Promise<T>, not assert the resolved value as a promise; locate the pendingCalls
lookup (this.pendingCalls[userId][methodName]) and replace `return await (await
existingCall as Promise<T>)` with `return await (existingCall as Promise<T>)`
(or simply `return await existingCall` with the appropriate generic typing) to
ensure the cast applies to the Promise, not its resolved value.
- Around line 108-113: The code sets this.pendingCalls[userId][methodName] =
undefined which leaves the property present so Object.keys(...) never becomes
zero; change the removal to delete this.pendingCalls[userId][methodName] (in the
block that checks this.pendingCalls[userId]?.[methodName] === pendingCall) so
the property is actually removed and the subsequent cleanup check for
Object.keys(this.pendingCalls[userId]).length === 0 in the pendingCalls cleanup
logic will work as intended.

@Klakurka Klakurka requested a review from chedieck February 25, 2026 23:00
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
redis/index.ts (1)

108-113: ⚠️ Potential issue | 🟠 Major

Cleanup logic still keeps stale keys and prevents bucket removal.

Line 109 assigns undefined instead of removing the property, so Object.keys(...) at Line 111 still sees the key. Also, Line 112 setting {} keeps an empty user bucket around.

🐛 Proposed cleanup fix
     if (this.pendingCalls[userId]?.[methodName] === pendingCall) {
-      this.pendingCalls[userId][methodName] = undefined
+      delete this.pendingCalls[userId][methodName]
     }
     if (this.pendingCalls[userId] !== undefined && Object.keys(this.pendingCalls[userId]).length === 0) {
-      this.pendingCalls[userId] = {}
+      delete this.pendingCalls[userId]
     }
#!/bin/bash
# Read-only verification: confirm cleanup uses assignment-to-undefined and empty-object retention.
rg -nP 'pendingCalls\[userId\]\[methodName\]\s*=\s*undefined|delete\s+this\.pendingCalls\[userId\]\[methodName\]|Object\.keys\(this\.pendingCalls\[userId\]\)\.length\s*===\s*0|this\.pendingCalls\[userId\]\s*=\s*\{\}' redis/index.ts
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@redis/index.ts` around lines 108 - 113, The cleanup leaves stale keys because
code sets this.pendingCalls[userId][methodName] = undefined and later assigns an
empty object; change the logic to delete the method entry and remove the user
bucket when empty: replace the assignment-to-undefined with delete
this.pendingCalls[userId][methodName] (use the same userId, methodName,
pendingCall check) and when Object.keys(this.pendingCalls[userId]).length === 0
call delete this.pendingCalls[userId] instead of assigning {} so no empty user
bucket remains.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@redis/index.ts`:
- Around line 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.

---

Duplicate comments:
In `@redis/index.ts`:
- Around line 108-113: The cleanup leaves stale keys because code sets
this.pendingCalls[userId][methodName] = undefined and later assigns an empty
object; change the logic to delete the method entry and remove the user bucket
when empty: replace the assignment-to-undefined with delete
this.pendingCalls[userId][methodName] (use the same userId, methodName,
pendingCall check) and when Object.keys(this.pendingCalls[userId]).length === 0
call delete this.pendingCalls[userId] instead of assigning {} so no empty user
bucket remains.

ℹ️ Review info
Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 75b4283c-7efd-4b58-8590-63341ec4ccd8

📥 Commits

Reviewing files that changed from the base of the PR and between 4b57bac and ac39238.

📒 Files selected for processing (1)
  • redis/index.ts

Comment on lines +97 to +103
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>
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.

@Fabcien
Copy link
Collaborator

Fabcien commented Mar 4, 2026

Not sure about the fix. The root cause seems that either the dashboard cache request is emitted concurrently, which is dubious, or that a call is missing an await.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants