Skip to content

fix: add watch cleanup and promise reset in Pinia stores#1415

Merged
riderx merged 1 commit into
mainfrom
riderx/fix-pinia-watch-cleanup
Jan 10, 2026
Merged

fix: add watch cleanup and promise reset in Pinia stores#1415
riderx merged 1 commit into
mainfrom
riderx/fix-pinia-watch-cleanup

Conversation

@riderx
Copy link
Copy Markdown
Member

@riderx riderx commented Jan 10, 2026

Summary (AI generated)

  • Store unwatch functions in organization.ts and dialogv2.ts to prevent watcher accumulation
  • Clean up auth listener subscription on sign-out to prevent memory leaks
  • Reset _initialLoadPromise after rejection to allow subsequent awaits to succeed
  • Add proper TypeScript types with WatchStopHandle and Subscription for cleanup functions

Motivation (AI generated)

Pinia stores had multiple watch cleanup issues causing memory leaks and watcher accumulation. Watch functions without cleanup accumulate over time, especially on re-initialization. Auth listener subscriptions weren't being stored and cleaned up properly. Promise resolvers that reject become permanently failed, preventing retry logic from working.

Business Impact (AI generated)

Fixes memory leaks in the application that could cause performance degradation and increased memory usage over time. Ensures proper cleanup on authentication state changes and store re-initialization. Enables robust retry logic for failed dashboard and organization data loads.

Test Plan (AI generated)

  • Run bun typecheck to verify TypeScript types
  • Run bun lint to verify code style
  • Test store initialization and sign-out flows manually to verify no watcher leaks
  • Verify dashboard update retries succeed after failure

Generated with AI

Summary by CodeRabbit

  • Chores
    • Enhanced internal resource cleanup mechanisms across dialog and store management to improve app stability and prevent memory leaks.
    • Improved error recovery processes for initialization failures, enabling better retry behavior.
    • Strengthened lifecycle management during authentication state transitions to ensure proper cleanup and state resets.

✏️ Tip: You can customize this high-level summary in your review settings.

- Store unwatch functions in organization.ts and dialogv2.ts to prevent watcher accumulation
- Clean up auth listener subscription on sign-out to prevent memory leaks
- Reset _initialLoadPromise after rejection to allow subsequent awaits to succeed
- Add proper types with WatchStopHandle and Subscription for cleanup functions

Fixes watcher accumulation bugs that caused memory leaks during store lifecycle.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 10, 2026

📝 Walkthrough

Walkthrough

These changes introduce explicit watcher and subscription cleanup mechanisms across three store files, replacing implicit cleanup with stored handles that are properly disposed during state resets and lifecycle transitions to prevent memory leaks.

Changes

Cohort / File(s) Summary
Dialog dismiss watcher cleanup
src/stores/dialogv2.ts
Adds typed _unwatchDismiss handle to track and clean up the dialog dismiss watcher; implements guard for already-closed dialogs and ensures watcher cleanup before creating new ones.
Promise resolver typing
src/stores/main.ts
Tightens _initialLoadPromise type to Promise.withResolvers<boolean>() for explicit boolean typing; adds error recovery by resetting the promise on catch to allow subsequent initialization attempts.
Store lifecycle management
src/stores/organization.ts
Adds typed cleanup state (_unwatchCurrentOrganization, _unwatchOrganizations, _authSubscription); wraps watchers and subscriptions with stored handles; enhances sign-out and initialization flow to unsubscribe and clean up all tracked resources before state reset.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 Watchers watch and watchers wait,
But cleanup's now our tidy fate,
No leaks shall roam through memory halls—
Each handle stored, each cleanup calls,
A rabbit's gift: state fresh and clean!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Description check ❓ Inconclusive The description includes a summary, motivation, and business impact, but the test plan section is incomplete with only unchecked checkboxes and lacks verification steps for the specific changes made. Complete the test plan by checking which tests were run and providing specific verification steps for watcher cleanup and promise reset behavior to confirm the fixes work as intended.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and accurately summarizes the main change: fixing watch cleanup and promise reset in Pinia stores, which aligns with the substantial modifications across three store files.
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

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
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f5b70b486f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +254 to +256
// Clean up watchers to prevent callbacks on stale state
_unwatchCurrentOrganization?.()
_unwatchOrganizations?.()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Recreate org watchers after sign-out cleanup

Unsubscribing _unwatchCurrentOrganization and _unwatchOrganizations on SIGNED_OUT stops the only watchers that keep currentOrganization/_organizations in sync, but they are never re-registered afterward (the watch setup runs only once when the store is created). In a SPA session where a user signs out and then signs back in without a full reload, those watchers remain detached, so currentRole, the app/org mapping, and _initialLoadPromise resolution never update. This leaves subsequent sessions with stale org state and hanging awaitInitialLoad() calls. Consider re-establishing the watchers on re-init or avoid stopping them during sign-out.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/stores/dialogv2.ts (1)

1-6: Missing watch and ref imports from Vue.

The file uses watch (line 77) and ref (lines 23-26) but neither is imported. Add them to the import statement.

🐛 Proposed fix
-import type { WatchStopHandle } from 'vue'
+import type { WatchStopHandle } from 'vue'
+import { ref, watch } from 'vue'
 import { acceptHMRUpdate, defineStore } from 'pinia'
🤖 Fix all issues with AI agents
In @src/stores/main.ts:
- Line 45: The code uses Promise.withResolvers() when initializing
_initialLoadPromise, which is an ES2024 API not available under the current
tsconfig target "es2020"; either update the TS target to "es2024" or replace the
call to Promise.withResolvers() in the _initialLoadPromise initialization with
an equivalent manual resolver pattern (create a new Promise and capture its
resolve/reject into an object) so the code matches the build target;
alternatively ensure a polyfill (e.g., core-js) is configured to provide
Promise.withResolvers before using it.

In @src/stores/organization.ts:
- Around line 40-43: The sign-out handlers call `_unwatchCurrentOrganization`
and `_unwatchOrganizations` then null them, but the watchers are only created
once, so they are not recreated on subsequent sign-ins; fix by extracting the
watcher creation logic into a helper (e.g., `setupWatchers`) that first invokes
and clears any existing `_unwatchCurrentOrganization`/`_unwatchOrganizations`,
then reattaches the `watch` on `currentOrganization` and `_organizations` (copy
the existing watcher bodies), call `setupWatchers()` at store creation and again
inside `fetchOrganizations` when `_initialized` transitions back to true (or
when `!_initialized.value` becomes false) so watchers are re-established after
sign-in; ensure cleanup code only nullifies after invoking the stop handles.
- Around line 1-2: The import order violates perfectionist/sort-imports: move
the Subscription import from "@supabase/supabase-js" so it appears before the
Vue imports (ComputedRef, Ref, WatchStopHandle) — update the top-level import
block in src/stores/organization.ts to list the "@supabase/supabase-js" import
before the "vue" import so ESLint no longer flags the ordering error.
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4922551 and f5b70b4.

📒 Files selected for processing (3)
  • src/stores/dialogv2.ts
  • src/stores/main.ts
  • src/stores/organization.ts
🧰 Additional context used
📓 Path-based instructions (6)
src/**/*.{ts,tsx,vue,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use ~/ alias for imports from src/ directory in frontend TypeScript and Vue components

Files:

  • src/stores/dialogv2.ts
  • src/stores/main.ts
  • src/stores/organization.ts
src/**/*.{vue,ts,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Frontend ESLint must pass before commit; run bun lint:fix to auto-fix issues in frontend files

Files:

  • src/stores/dialogv2.ts
  • src/stores/main.ts
  • src/stores/organization.ts
**/*.{vue,ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Run bun lint to lint Vue, TypeScript, and JavaScript files; use bun lint:fix to auto-fix issues

Files:

  • src/stores/dialogv2.ts
  • src/stores/main.ts
  • src/stores/organization.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use single quotes and no semicolons per @antfu/eslint-config rules

Files:

  • src/stores/dialogv2.ts
  • src/stores/main.ts
  • src/stores/organization.ts
src/**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Use path alias ~/ to map to src/ for imports in TypeScript and JavaScript files

Files:

  • src/stores/dialogv2.ts
  • src/stores/main.ts
  • src/stores/organization.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Run bun lint or lint/format command before validating any backend or frontend task to ensure consistent formatting

Files:

  • src/stores/dialogv2.ts
  • src/stores/main.ts
  • src/stores/organization.ts
🧠 Learnings (1)
📚 Learning: 2026-01-10T04:55:25.248Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.248Z
Learning: Applies to src/**/*.vue : Use Pinia stores for state management in Vue components

Applied to files:

  • src/stores/dialogv2.ts
  • src/stores/organization.ts
🪛 GitHub Actions: Run tests
src/stores/organization.ts

[error] 2-2: ESLint: perfectionist/sort-imports violated. Expected "@supabase/supabase-js" to come before "vue". (bun lint: eslint) - lint script exited with code 1.

🔇 Additional comments (4)
src/stores/main.ts (1)

116-121: Good fix for promise reset on rejection.

Resetting _initialLoadPromise after rejection correctly allows subsequent awaitInitialLoad() calls to succeed on retry. The error is still re-thrown for upstream handling.

src/stores/dialogv2.ts (1)

65-85: Well-structured dismiss watcher cleanup.

The implementation correctly:

  • Cleans up any existing watcher before creating a new one (line 68)
  • Handles the already-closed case with an early return (lines 71-75)
  • Nullifies the handle after cleanup to prevent stale references (line 80)

This prevents watcher accumulation when onDialogDismiss is called multiple times.

src/stores/organization.ts (2)

246-266: Auth subscription cleanup is correctly managed.

The sign-out handler properly:

  • Unsubscribes from auth state changes (lines 252-253)
  • Resets all state including _initialLoadPromise (line 260)
  • Resets _initialized to allow re-initialization (line 263)

The existing subscription is also cleaned before creating a new one (line 248).


37-37: Consistent use of Promise.withResolvers<boolean>() across stores.

This aligns with the same pattern in main.ts, ensuring both stores handle promise-based initialization consistently.

Comment thread src/stores/main.ts
const totalStorage = ref<number>(0)
const dashboardFetched = ref<boolean>(false)
const _initialLoadPromise = ref(Promise.withResolvers())
const _initialLoadPromise = ref(Promise.withResolvers<boolean>())
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check the actual file and its context
cat -n src/stores/main.ts | head -60

Repository: Cap-go/capgo

Length of output: 2447


🏁 Script executed:

# Check for tsconfig or build configuration
fd -e json | grep -E '(tsconfig|package\.json|vite|webpack|esbuild|tsup)' | head -20

Repository: Cap-go/capgo

Length of output: 104


🏁 Script executed:

# Check if Promise.withResolvers is used elsewhere in the codebase
rg "Promise\.withResolvers" --type ts --type js --type vue -A 2

Repository: Cap-go/capgo

Length of output: 83


🌐 Web query:

Promise.withResolvers browser support Node.js compatibility ES2024

💡 Result:

Summary — what Promise.withResolvers is, ES status, and runtime support:

  • What it does: static Promise.withResolvers() returns { promise, resolve, reject } (convenience for creating externally-resolvable promises). [1]
  • ECMAScript / spec: added to the language feature set (listed in ECMAScript spec; treated as a new baseline feature from 2024 onward). [1][2]
  • Browser support: available in modern browsers (Chrome 119+, Edge 119+, Firefox 121+, Safari 17.4+). [2]
  • Node.js support: available natively starting in Node.js v22 (older Node versions do not include it). Use a polyfill or upgrade Node if you need it on earlier runtimes. [2][3]

Sources: MDN documentation on Promise.withResolvers [1]; Web Platform / feature status (browser matrix and baseline date) [2]; Node/compatibility reports and community notes showing Node v22+ requirement and common polyfill/upgrade remedies [3].


🏁 Script executed:

# Check tsconfig.json for target
cat tsconfig.json

Repository: Cap-go/capgo

Length of output: 1257


🏁 Script executed:

# Check package.json for Node version requirement
jq '.engines, .devDependencies, .dependencies' package.json | head -50

Repository: Cap-go/capgo

Length of output: 1590


🏁 Script executed:

# Search for polyfills or Promise.withResolvers elsewhere
rg "Promise\.withResolvers|polyfill|Promise" --type ts --type js -l | head -20

Repository: Cap-go/capgo

Length of output: 674


🏁 Script executed:

# Check if there's any build configuration that might transpile/polyfill
rg "withResolvers" --type ts --type js -A 3 -B 3

Repository: Cap-go/capgo

Length of output: 1824


🏁 Script executed:

# Check vite config
cat vite.config.mts 2>/dev/null | head -100

Repository: Cap-go/capgo

Length of output: 3025


🏁 Script executed:

# Search for polyfill setup, core-js, or Promise shims
rg "polyfill|core-js|Promise" package.json

Repository: Cap-go/capgo

Length of output: 69


🏁 Script executed:

# Check for browserlist or runtime config
fd -e browserslistrc -e "\.browserslistrc"

Repository: Cap-go/capgo

Length of output: 38


🏁 Script executed:

# Check for any Promise.withResolvers polyfill/shim imports
rg "Promise\.withResolvers|withResolvers.*polyfill" --type ts --type js

Repository: Cap-go/capgo

Length of output: 421


Update TypeScript target or replace Promise.withResolvers to match es2020 target.

Promise.withResolvers() is an ES2024 feature, but your tsconfig.json targets es2020. This creates a mismatch—modern browsers (Chrome 119+, Firefox 121+, Safari 17.4+) support it natively, but the code violates your build target constraint. Either upgrade target to "es2024" in tsconfig.json, or replace this with the equivalent pattern: Promise.resolve().then(resolve => ({ promise: ..., resolve, reject: ... })), or ensure core-js is configured to polyfill this API.

🤖 Prompt for AI Agents
In @src/stores/main.ts at line 45, The code uses Promise.withResolvers() when
initializing _initialLoadPromise, which is an ES2024 API not available under the
current tsconfig target "es2020"; either update the TS target to "es2024" or
replace the call to Promise.withResolvers() in the _initialLoadPromise
initialization with an equivalent manual resolver pattern (create a new Promise
and capture its resolve/reject into an object) so the code matches the build
target; alternatively ensure a polyfill (e.g., core-js) is configured to provide
Promise.withResolvers before using it.

Comment on lines +1 to +2
import type { ComputedRef, Ref, WatchStopHandle } from 'vue'
import type { Subscription } from '@supabase/supabase-js'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix import order to resolve lint failure.

ESLint: perfectionist/sort-imports violated. Expected "@supabase/supabase-js" to come before "vue".

🔧 Proposed fix
-import type { ComputedRef, Ref, WatchStopHandle } from 'vue'
-import type { Subscription } from '@supabase/supabase-js'
+import type { Subscription } from '@supabase/supabase-js'
+import type { ComputedRef, Ref, WatchStopHandle } from 'vue'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import type { ComputedRef, Ref, WatchStopHandle } from 'vue'
import type { Subscription } from '@supabase/supabase-js'
import type { Subscription } from '@supabase/supabase-js'
import type { ComputedRef, Ref, WatchStopHandle } from 'vue'
🧰 Tools
🪛 GitHub Actions: Run tests

[error] 2-2: ESLint: perfectionist/sort-imports violated. Expected "@supabase/supabase-js" to come before "vue". (bun lint: eslint) - lint script exited with code 1.

🤖 Prompt for AI Agents
In @src/stores/organization.ts around lines 1 - 2, The import order violates
perfectionist/sort-imports: move the Subscription import from
"@supabase/supabase-js" so it appears before the Vue imports (ComputedRef, Ref,
WatchStopHandle) — update the top-level import block in
src/stores/organization.ts to list the "@supabase/supabase-js" import before the
"vue" import so ESLint no longer flags the ordering error.

Comment on lines +40 to +43
// Store cleanup functions
let _unwatchCurrentOrganization: WatchStopHandle | null = null
let _unwatchOrganizations: WatchStopHandle | null = null
let _authSubscription: Subscription | null = null
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Watchers are stopped on sign-out but never recreated on subsequent sign-in.

When SIGNED_OUT fires, _unwatchCurrentOrganization and _unwatchOrganizations are invoked (lines 255-256), stopping the watchers. However, the watchers are only created once at store definition time (lines 84, 133). After a user signs out and signs back in, the watchers won't exist, breaking organization change detection and app-to-org mapping.

Consider recreating the watchers in fetchOrganizations when _initialized becomes true again, or guard the cleanup to only nullify after invoking without permanently losing the watch.

💡 Suggested approach

Extract watcher setup into a helper function and call it when re-initializing:

const setupWatchers = () => {
  // Clean up existing watchers first
  _unwatchCurrentOrganization?.()
  _unwatchOrganizations?.()
  
  _unwatchCurrentOrganization = watch(currentOrganization, async (currentOrganizationRaw, oldOrganization) => {
    // ... existing watcher body
  })
  
  _unwatchOrganizations = watch(_organizations, async (organizationsMap) => {
    // ... existing watcher body
  })
}

Then call setupWatchers() at store creation and again in fetchOrganizations when !_initialized.value.

Also applies to: 83-84, 132-133, 254-256

🤖 Prompt for AI Agents
In @src/stores/organization.ts around lines 40 - 43, The sign-out handlers call
`_unwatchCurrentOrganization` and `_unwatchOrganizations` then null them, but
the watchers are only created once, so they are not recreated on subsequent
sign-ins; fix by extracting the watcher creation logic into a helper (e.g.,
`setupWatchers`) that first invokes and clears any existing
`_unwatchCurrentOrganization`/`_unwatchOrganizations`, then reattaches the
`watch` on `currentOrganization` and `_organizations` (copy the existing watcher
bodies), call `setupWatchers()` at store creation and again inside
`fetchOrganizations` when `_initialized` transitions back to true (or when
`!_initialized.value` becomes false) so watchers are re-established after
sign-in; ensure cleanup code only nullifies after invoking the stop handles.

@sonarqubecloud
Copy link
Copy Markdown

@riderx riderx merged commit d5faf2a into main Jan 10, 2026
10 of 11 checks passed
@riderx riderx deleted the riderx/fix-pinia-watch-cleanup branch January 10, 2026 05:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant