Skip to content

fix: invalidation issues - Don't merge this till we merge rest of the deploy PRs#4000

Closed
ogzhanolguncu wants to merge 1 commit intomainfrom
fix-invalidation-issues
Closed

fix: invalidation issues - Don't merge this till we merge rest of the deploy PRs#4000
ogzhanolguncu wants to merge 1 commit intomainfrom
fix-invalidation-issues

Conversation

@ogzhanolguncu
Copy link
Contributor

@ogzhanolguncu ogzhanolguncu commented Sep 19, 2025

What does this PR do?

This PR:

  • Adds missing reset for newly created workspace.
  • Adds missing invalidation when switching workspace
  • Organized tanstack db part a bit
  • Fixes reset behavior

Fixes # (issue)

If there is not an issue for this, please create one first. This is used to tracking purposes and also helps use understand why this PR exists

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Chore (refactoring code, technical debt, workflow improvements)
  • Enhancement (small improvements)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How should this be tested?

  • Workspace switch should work and invalidate
  • Namespaces should invalidate correctly when workspace changes
  • Project should invalidate correctly when workspace changes
  • Workspace should revalidate when new workspaces created
  • Worksapce shouldn't change when same ws clicked

Checklist

Required

  • Filled out the "How to test" section in this PR
  • Read Contributing Guide
  • Self-reviewed my own code
  • Commented on my code in hard-to-understand areas
  • Ran pnpm build
  • Ran pnpm fmt
  • Checked for warnings, there are none
  • Removed all console.logs
  • Merged the latest changes from main onto my branch with git pull origin main
  • My changes don't cause any responsiveness issues

Appreciated

  • If a UI change was made: Added a screen recording or screenshots to this PR
  • Updated the Unkey Docs if changes were necessary

Summary by CodeRabbit

  • New Features
    • None.
  • Bug Fixes
    • Sidebar project and rate limit lists now refresh reliably.
    • Prevents redundant workspace switches when selecting the current workspace.
    • Memberships list updates immediately after creating a workspace.
  • Improvements
    • More consistent, user-friendly error messages when creating projects.
    • Faster, more reliable data refresh after workspace changes.
    • General stability and performance improvements across collections and navigation.
  • Refactor
    • Internal data layer restructured to support better cache handling and reset behavior.

@changeset-bot
Copy link

changeset-bot bot commented Sep 19, 2025

⚠️ No Changeset found

Latest commit: 3b0de7d

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link

vercel bot commented Sep 19, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
dashboard Ready Ready Preview Comment Sep 19, 2025 11:53am
engineering Ready Ready Preview Comment Sep 19, 2025 11:53am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 19, 2025

📝 Walkthrough

Walkthrough

Refactors the collections layer from static singletons to factory-created instances, updates reset/cleanup semantics, adds unique indexes, and standardizes error handling. Sidebar hooks now pass dependency arrays to useLiveQuery. Workspace creation invalidates memberships and resets collections. Team switcher adds a guard to avoid redundant workspace changes.

Changes

Cohort / File(s) Summary of changes
Collections factory refactor & reset flow
apps/dashboard/lib/collections/index.ts, apps/dashboard/lib/collections/deploy/projects.ts, apps/dashboard/lib/collections/ratelimit/namespaces.ts, apps/dashboard/lib/collections/ratelimit/overrides.ts
Replace static exported collections with factory functions (create*Collection). Build global collections dynamically from factories; refactor project-scoped collection creation/cleanup. Expand reset() to full cleanup/reinit/preload. Add unique indexes (name, identifier-per-namespace). Standardize error messages for projects.
Navigation hooks: live query deps
apps/dashboard/components/navigation/sidebar/app-sidebar/hooks/use-projects-navigation.tsx, .../hooks/use-ratelimit-navigation.tsx
Change useLiveQuery to two-argument form with explicit dependency arrays ([collection.projects], [collection.ratelimitNamespaces]) to ensure rerenders when collections change.
Workspace lifecycle
apps/dashboard/app/new/hooks/use-workspace-step.tsx
After successful workspace creation: invalidate trpcUtils.user.listMemberships and await reset() from collections. No public API signature changes.
Team switcher guard
apps/dashboard/components/navigation/sidebar/team-switcher.tsx
Introduce handleWorkspaceSwitch(targetOrgId) to no-op when switching to the current workspace; otherwise call changeWorkspace.mutateAsync. Update click handler to use the new function.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant UI as Dashboard UI
  participant TRPC as trpc.createWorkspace
  participant Utils as trpcUtils.user.listMemberships
  participant Coll as collections.reset()

  U->>UI: Submit "Create workspace"
  UI->>TRPC: mutateAsync(payload)
  TRPC-->>UI: onSuccess(newWorkspace)
  UI->>Utils: invalidate()
  UI->>Coll: reset()
  activate Coll
  note over Coll: Cleanup per-project and global collections<br/>Re-instantiate global collections from factories<br/>Preload collections
  Coll-->>UI: resolved
  deactivate Coll
  UI-->>U: UI reflects new memberships and data
Loading
sequenceDiagram
  autonumber
  participant Reset as reset()
  participant Clean as cleanupAll()
  participant Global as GLOBAL_COLLECTION_FACTORIES
  participant Public as collection (public object)

  Reset->>Clean: run cleanup helper
  Clean-->>Reset: per-project & global cleaned
  Reset->>Global: instantiate each factory
  Global-->>Reset: new instances
  Reset->>Public: replace collection entries
  Reset-->>Reset: sequentially preload all
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

Bug

Suggested reviewers

  • mcstepp
  • chronark
  • perkinsjr

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The PR description contains a clear summary and high-level testing notes but omits required template items: the "Fixes # (issue)" reference is missing, the "Type of change" section has no selection, and the required checklist entries and "How should this be tested" steps are only high-level rather than reproducible; per the repository template the missing issue reference and incomplete checklist are required items to address. Please add the linked issue number (or create one and reference it), mark the appropriate "Type of change" checkbox, expand the "How should this be tested" section with concrete reproducible steps and expected results (including environment and commands), and complete the required checklist items (e.g., run pnpm build/fmt, remove console logs, merge latest main) before merging.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The title "fix: invalidation issues - Don't merge this till we merge rest of the deploy PRs" correctly reflects the main intent of the changes—addressing invalidation/reset behavior for workspaces and related collections—so it is related to the changeset; however it contains extraneous merge-coordination text that makes it noisy and less professional as a commit/PR title.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-invalidation-issues

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.

@vercel vercel bot temporarily deployed to Preview – dashboard September 19, 2025 11:53 Inactive
@ogzhanolguncu ogzhanolguncu marked this pull request as ready for review September 19, 2025 11:54
@github-actions
Copy link
Contributor

github-actions bot commented Sep 19, 2025

Thank you for following the naming conventions for pull request titles! 🙏

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: 3

Caution

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

⚠️ Outside diff range comments (1)
apps/dashboard/components/navigation/sidebar/app-sidebar/hooks/use-ratelimit-navigation.tsx (1)

29-31: Fix potential crash when data is undefined during initial load

data can be undefined while loading (projects hook guards for this). Accessing data.length would throw.

Apply this diff to guard consistently:

-  if (data.length === 0) {
-    return [];
-  }
+  if (!data?.length) {
+    return [];
+  }

Also applies to: 33-39

🧹 Nitpick comments (9)
apps/dashboard/components/navigation/sidebar/team-switcher.tsx (2)

86-93: Guard against concurrent switches and handle rejected promises

Double‑clicks can fire multiple mutateAsync calls; also, unhandled rejections can spam the console. Add a loading guard and wrap in try/catch (onError already toasts).

-  const handleWorkspaceSwitch = async (targetOrgId: string) => {
+  const handleWorkspaceSwitch = async (targetOrgId: string) => {
     // Prevent switch if already on the target workspace
     if (targetOrgId === currentOrgMembership?.organization.id) {
       return;
     }
-
-    await changeWorkspace.mutateAsync(targetOrgId);
+    if (changeWorkspace.isLoading) return;
+    try {
+      await changeWorkspace.mutateAsync(targetOrgId);
+    } catch {
+      // handled by onError
+    }
   };

153-172: Disable the current/active item and while switching (UX + safety)

Prevent clicks on the active workspace and during an in‑flight switch. If DropdownMenuItem supports disabled, wire it up.

-              <DropdownMenuItem
+              <DropdownMenuItem
                 key={membership.id}
                 className="flex items-center justify-between"
-                onClick={() => handleWorkspaceSwitch(membership.organization.id)}
+                disabled={
+                  membership.organization.id === currentOrgMembership?.organization.id ||
+                  changeWorkspace.isLoading
+                }
+                onClick={() => handleWorkspaceSwitch(membership.organization.id)}
               >
apps/dashboard/lib/collections/deploy/projects.ts (1)

41-67: Broaden error map to common tRPC codes (auth/rate limit)

Add UNAUTHORIZED and TOO_MANY_REQUESTS for clearer UX. Optional: centralize this map for reuse across collections.

   FORBIDDEN: {
     message: "Permission Denied",
     description: "You don't have permission to create projects in this workspace.",
   },
+  UNAUTHORIZED: {
+    message: "Sign-in required",
+    description: "Your session expired. Please sign in and try again.",
+  },
   BAD_REQUEST: {
     message: "Invalid Configuration",
     description: "Please check your project settings.",
   },
+  TOO_MANY_REQUESTS: {
+    message: "Rate limit exceeded",
+    description: "Please wait a moment and try again.",
+  },
apps/dashboard/lib/collections/ratelimit/namespaces.ts (2)

26-33: Validate and trim name on create

Avoid manual null/empty checks; trim and enforce bounds with zod so whitespace‑only names don’t slip through and uniqueness behaves predictably.

-      onInsert: async ({ transaction }) => {
-        const { changes: newNamespace } = transaction.mutations[0];
-        if (!newNamespace.name) {
-          throw new Error("Namespace name is required");
-        }
-        const mutation = trpcClient.ratelimit.namespace.create.mutate({
-          name: newNamespace.name,
-        });
+      onInsert: async ({ transaction }) => {
+        const { changes: newNamespace } = transaction.mutations[0];
+        const { name } = z.object({ name: z.string().trim().min(1).max(50) }).parse({
+          name: newNamespace.name,
+        });
+        const mutation = trpcClient.ratelimit.namespace.create.mutate({ name });

47-51: Also sanitize on update

Trim and validate on rename to keep invariants consistent.

-        const mutation = trpcClient.ratelimit.namespace.update.name.mutate({
-          namespaceId: original.id,
-          name: modified.name,
-        });
+        const { name } = z.object({ name: z.string().trim().min(1).max(50) }).parse({
+          name: modified.name,
+        });
+        const mutation = trpcClient.ratelimit.namespace.update.name.mutate({
+          namespaceId: original.id,
+          name,
+        });
apps/dashboard/components/navigation/sidebar/app-sidebar/hooks/use-ratelimit-navigation.tsx (1)

41-46: Simplify “Requests” active state logic

The OR condition duplicates isExactlyRatelimitRoot.

-          active: isExactlyRatelimitRoot || (currentNamespaceActive && !segments.at(2)),
+          active: currentNamespaceActive && segments.length === 2,
apps/dashboard/components/navigation/sidebar/app-sidebar/hooks/use-projects-navigation.tsx (1)

36-45: Guard against duplicate child items if baseNavItems already contains dynamic entries

If callers ever pass previously “enhanced” items back as baseNavItems, appending again will duplicate children. Consider replacing only the dynamic portion or ensuring callers pass a stable, static base.

apps/dashboard/lib/collections/index.ts (2)

101-115: Serialize reset to avoid overlapping cleanups and instance churn

Concurrent calls to reset() could interleave cleanup/assign/preload and produce stale subscriptions. Serialize resets with an in‑flight promise.

Apply within reset():

-export async function reset() {
-  // This is GC cleanup only useful for better memory management
-  await collectionManager.cleanupAll();
-  // Without these components still subscribed to old collections, so create new instances for each reset. Mostly used when switching workspaces
-  Object.assign(
-    collection,
-    Object.fromEntries(
-      Object.entries(GLOBAL_COLLECTION_FACTORIES).map(([key, factory]) => [key, factory()]),
-    ),
-  );
-  // Preload all collections, please keep this sequential. Otherwise UI acts weird. react-query already takes care of batching.
-  for (const c of Object.values(collection)) {
-    await c.preload();
-  }
-}
+export async function reset() {
+  if (resetInFlight) return resetInFlight;
+  resetInFlight = (async () => {
+    await collectionManager.cleanupAll();
+    Object.assign(
+      collection,
+      Object.fromEntries(
+        Object.entries(GLOBAL_COLLECTION_FACTORIES).map(([key, factory]) => [key, factory()]),
+      ),
+    );
+    for (const c of Object.values(collection)) {
+      await c.preload();
+    }
+  })().finally(() => {
+    resetInFlight = null;
+  });
+  return resetInFlight;
+}

Add near module top:

let resetInFlight: Promise<void> | null = null;

73-87: Minor: clarify cleanup sequencing and intent

Project cleanups start immediately (promises created), globals are cleaned sequentially, then you await projectPromises. Add a short comment so future refactors don’t inadvertently flip the order.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c87b938 and 3b0de7d.

📒 Files selected for processing (8)
  • apps/dashboard/app/new/hooks/use-workspace-step.tsx (3 hunks)
  • apps/dashboard/components/navigation/sidebar/app-sidebar/hooks/use-projects-navigation.tsx (1 hunks)
  • apps/dashboard/components/navigation/sidebar/app-sidebar/hooks/use-ratelimit-navigation.tsx (1 hunks)
  • apps/dashboard/components/navigation/sidebar/team-switcher.tsx (2 hunks)
  • apps/dashboard/lib/collections/deploy/projects.ts (1 hunks)
  • apps/dashboard/lib/collections/index.ts (3 hunks)
  • apps/dashboard/lib/collections/ratelimit/namespaces.ts (1 hunks)
  • apps/dashboard/lib/collections/ratelimit/overrides.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-08-25T13:46:08.303Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3834
File: apps/dashboard/app/(app)/ratelimits/_components/controls/components/namespace-list-datetime/index.tsx:1-1
Timestamp: 2025-08-25T13:46:08.303Z
Learning: The NamespaceListDateTime component in apps/dashboard/app/(app)/ratelimits/_components/controls/components/namespace-list-datetime/index.tsx is intentionally designed to use the overview hook (useFilters from @/app/(app)/ratelimits/[namespaceId]/_overview/hooks/use-filters) rather than the namespace list hook, as clarified by ogzhanolguncu. This coupling is by design, not an architectural issue.

Applied to files:

  • apps/dashboard/components/navigation/sidebar/app-sidebar/hooks/use-ratelimit-navigation.tsx
📚 Learning: 2025-08-25T13:46:34.441Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3834
File: apps/dashboard/app/(app)/ratelimits/_components/controls/components/namespace-list-refresh.tsx:4-4
Timestamp: 2025-08-25T13:46:34.441Z
Learning: The namespace list refresh component (apps/dashboard/app/(app)/ratelimits/_components/controls/components/namespace-list-refresh.tsx) intentionally uses the overview hook (useFilters from @/app/(app)/ratelimits/[namespaceId]/_overview/hooks/use-filters) rather than a namespace-specific hook. This cross-coupling between namespace list components and overview hooks is an architectural design decision.

Applied to files:

  • apps/dashboard/components/navigation/sidebar/app-sidebar/hooks/use-ratelimit-navigation.tsx
📚 Learning: 2025-08-25T12:56:59.310Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3834
File: apps/dashboard/lib/trpc/routers/ratelimit/query-namespaces/index.ts:59-66
Timestamp: 2025-08-25T12:56:59.310Z
Learning: In the ratelimit namespace query system (apps/dashboard/lib/trpc/routers/ratelimit/query-namespaces/index.ts), the nameQuery filter is designed as an array for future extensibility to support multiple filters, but currently only the first filter (index 0) is processed. This is intentional future-proofing.

Applied to files:

  • apps/dashboard/components/navigation/sidebar/app-sidebar/hooks/use-ratelimit-navigation.tsx
  • apps/dashboard/lib/collections/ratelimit/namespaces.ts
🧬 Code graph analysis (7)
apps/dashboard/components/navigation/sidebar/app-sidebar/hooks/use-projects-navigation.tsx (1)
apps/dashboard/lib/collections/index.ts (1)
  • collection (93-99)
apps/dashboard/components/navigation/sidebar/app-sidebar/hooks/use-ratelimit-navigation.tsx (1)
apps/dashboard/lib/collections/index.ts (1)
  • collection (93-99)
apps/dashboard/app/new/hooks/use-workspace-step.tsx (1)
apps/dashboard/lib/collections/index.ts (1)
  • reset (101-115)
apps/dashboard/lib/collections/deploy/projects.ts (1)
apps/dashboard/lib/collections/client.ts (1)
  • trpcClient (12-25)
apps/dashboard/lib/collections/index.ts (6)
apps/dashboard/lib/collections/deploy/environments.ts (1)
  • createEnvironmentsCollection (15-30)
apps/dashboard/lib/collections/deploy/domains.ts (1)
  • createDomainsCollection (16-31)
apps/dashboard/lib/collections/deploy/deployments.ts (1)
  • createDeploymentsCollection (41-56)
apps/dashboard/lib/collections/deploy/projects.ts (1)
  • createProjectsCollection (69-111)
apps/dashboard/lib/collections/ratelimit/namespaces.ts (1)
  • createRatelimitNamespacesCollection (15-82)
apps/dashboard/lib/collections/ratelimit/overrides.ts (1)
  • createRatelimitOverridesCollection (18-81)
apps/dashboard/lib/collections/ratelimit/overrides.ts (3)
apps/dashboard/lib/collections/index.ts (1)
  • collection (93-99)
apps/dashboard/lib/collections/client.ts (1)
  • trpcClient (12-25)
internal/ui/src/components/toaster.tsx (1)
  • toast (29-29)
apps/dashboard/lib/collections/ratelimit/namespaces.ts (3)
apps/dashboard/lib/collections/index.ts (1)
  • collection (93-99)
apps/dashboard/lib/collections/client.ts (1)
  • trpcClient (12-25)
internal/ui/src/components/toaster.tsx (1)
  • toast (29-29)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Test Go API Local / Test
  • GitHub Check: Test API / API Test Local
  • GitHub Check: Build / Build
🔇 Additional comments (8)
apps/dashboard/lib/collections/ratelimit/overrides.ts (1)

73-78: Confirm uniqueness semantics (case sensitivity, server parity)

The in‑memory unique index enforces client‑side uniqueness for (namespaceId, identifier). Confirm the server enforces the same rule (and the same case‑sensitivity) to avoid client/server divergence.

apps/dashboard/lib/collections/deploy/projects.ts (1)

69-111: Factory migration looks good

Instantiation, query options, parsing, and toast flow are consistent with the new collections pattern.

apps/dashboard/lib/collections/ratelimit/namespaces.ts (1)

74-79: Uniqueness expectations

If names should be unique case‑insensitively, normalize (e.g., toLowerCase) before insert/update or enforce at the server. Otherwise “Foo” and “foo” will be distinct locally.

apps/dashboard/components/navigation/sidebar/app-sidebar/hooks/use-ratelimit-navigation.tsx (2)

18-25: Good: adding deps to useLiveQuery matches the reset/invalidations plan

Using [collection.ratelimitNamespaces] ensures re‑runs after workspace reset. This aligns with the factory-based collections.

Please confirm other hooks subscribe similarly so they re-run after reset.


21-23: Verify ordering by id is semantically correct

Descending by namespace.id assumes ids are time‑sortable (e.g., ULID/KSUID). If not, prefer createdAt.

apps/dashboard/components/navigation/sidebar/app-sidebar/hooks/use-projects-navigation.tsx (2)

11-15: Good: dependency array ensures reruns after reset

Passing [collection.projects] addresses the invalidation/rerender gap on workspace changes.


12-14: Confirm id-based ordering is intended

Ordering by project.id desc relies on ids being monotonic. If not guaranteed, switch to createdAt.

apps/dashboard/lib/collections/index.ts (1)

24-28: Solid move to factory-built global collections with in-place mutation on reset

Factory map + in-place Object.assign preserves the collection object identity while swapping instances—good for hooks depending on property identity.

Also applies to: 92-99

@ogzhanolguncu ogzhanolguncu changed the title fix: invalidation issues fix: invalidation issues - Don't merge this till we merge rest of the deploy PRs Sep 19, 2025
@ogzhanolguncu
Copy link
Contributor Author

Closing this in favor of @perkinsjr 's PR. Note for my future-self: We can use the factory structure in the future. It removes most of the boilerplate and makes it easy to add new collections

@chronark chronark deleted the fix-invalidation-issues branch November 5, 2025 12:46
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