Skip to content

fix(realtime): fix shared drive permissions not updating in sidebar - #248

Merged
2witstudios merged 1 commit into
masterfrom
claude/fix-shared-drive-permissions-tkD95
Jan 26, 2026
Merged

2witstudios merged 1 commit into
masterfrom
claude/fix-shared-drive-permissions-tkD95

Conversation

@2witstudios

@2witstudios 2witstudios commented Jan 26, 2026 •

Copy link
Copy Markdown
Owner
  • Fix useGlobalDriveSocket to use user.id instead of socket.id for channel subscription
    (socket.id is the Socket.IO connection ID, not the user ID, so member events were
    never being received)
  • Add permission cache invalidation when members are added/updated/removed
  • Store joined user ID in ref for proper cleanup on unmount

These changes ensure that when someone shares a drive with a user:

  1. The realtime event is properly received (correct channel subscription)
  2. Permission caches are invalidated immediately (no stale permissions)
  3. The sidebar refreshes to show the newly shared drive

https://claude.ai/code/session_017uoRQb8sw7PuoxVgK2hhhQ

Summary by CodeRabbit

  • Bug Fixes

    • Permission changes for drive members now take effect immediately via cache invalidation after updates or removals.
  • Improvements

    • Invitations and role updates propagate in real time more reliably.
    • Live-update connection now joins and leaves user-specific channels based on the signed-in user for cleaner session handling.
  • Tests

    • Added mocks to cover the new permission invalidation paths.

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

@coderabbitai

coderabbitai Bot commented Jan 26, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds permission-cache invalidation calls after member invite/update/delete operations and makes the global drive socket hook join/leave user-specific channels using the authenticated user ID (tracked via a ref) instead of socket IDs.

Changes

Cohort / File(s) Summary
Permission cache + member routes
apps/web/src/app/api/drives/[driveId]/members/[userId]/route.ts, apps/web/src/app/api/drives/[driveId]/members/invite/route.ts
Imported invalidateUserPermissions and invalidateDrivePermissions; invoke them concurrently after invite, permission update (PATCH), and delete flows to invalidate caches immediately.
Socket auth / subscriptions
apps/web/src/hooks/useGlobalDriveSocket.ts
Hook now uses useAuth to get user?.id; introduces joinedUserIdRef, joins global and user-specific channels with the user ID, and ensures cleanup leaves both channels using stored user ID.
Tests / service mocks
apps/web/src/app/api/drives/[driveId]/members/[userId]/__tests__/route.test.ts
Added mocks for invalidateUserPermissions and invalidateDrivePermissions to the @pagespace/lib/server test seam.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant API as Drive API
    participant DB as Database
    participant Cache as Permissions Cache
    participant Socket as Socket Server

    rect rgba(0,128,255,0.5)
    Client->>API: Invite / Update / Delete member
    API->>DB: persist change (create/update/delete member)
    DB-->>API: ack
    API->>Socket: broadcast member change event
    API->>Cache: invalidateUserPermissions(userId) & invalidateDrivePermissions(driveId)
    Cache-->>API: ack invalidation
    Socket-->>Client: emit member change event
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰✨ I nudged the caches, tapped the wire,
Joined with my name, not a ghostly sire,
Invites and goodbyes now echo true—
Fresh keys, fresh hops, a clearer view,
The warren hums with synchronized fire!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main fix: addressing shared drive permissions not updating in the sidebar through realtime socket subscription and cache invalidation improvements.

✏️ 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.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/web/src/hooks/useGlobalDriveSocket.ts`:
- Around line 106-124: The cleanup that leaves channels and resets refs must be
moved into the first useEffect that depends on [socket, user?.id,
handleDriveEvent]: inside that effect's cleanup, clear revalidationTimeoutRef if
present, and if (socket && hasJoinedRef.current && joinedUserIdRef.current) emit
'leave_global_drives' and emit 'leave' for
`user:${joinedUserIdRef.current}:drives`, then set hasJoinedRef.current = false
and joinedUserIdRef.current = null; after moving this logic, remove the separate
second useEffect that only depended on [socket] so channel leave/reset happens
whenever user?.id or socket changes and new users can re-join correctly.
🧹 Nitpick comments (1)
apps/web/src/app/api/drives/[driveId]/members/invite/route.ts (1)

122-126: Cache invalidation placement is acceptable, though timing could be optimized.

The invalidation occurs before page permissions are created (lines 129-177). In a theoretical race condition, if the cache is re-populated by another request between invalidation and page permission creation, it might cache stale permissions. Consider moving this block after line 180 (after Promise.all(permissionPromises)) to ensure all permissions are committed before cache invalidation.

That said, the current placement is functional—the user already has drive membership at this point, and subsequent cache misses will fetch correct data.

Comment on lines 106 to 124
}, [socket, user?.id, handleDriveEvent]);

// Clean up timeout on unmount
useEffect(() => {
return () => {
if (revalidationTimeoutRef.current) {
clearTimeout(revalidationTimeoutRef.current);
}

// Leave the global drives channel and user-specific drives channel when unmounting
if (socket && hasJoinedRef.current) {
if (socket && hasJoinedRef.current && joinedUserIdRef.current) {
socket.emit('leave_global_drives');
socket.emit('leave', `user:${socket.id}:drives`);
socket.emit('leave', `user:${joinedUserIdRef.current}:drives`);
console.log(`🌍 Left global:drives and user:${joinedUserIdRef.current}:drives channels`);
hasJoinedRef.current = false;
console.log('🌍 Left global:drives and user:drives channels');
joinedUserIdRef.current = null;
}
};
}, [socket]);

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

🧩 Analysis chain

🏁 Script executed:

# Read the full useGlobalDriveSocket.ts file
cat -n apps/web/src/hooks/useGlobalDriveSocket.ts

Repository: 2witstudios/PageSpace

Length of output: 5318


Consolidate channel cleanup into the first effect to properly handle user changes.

When user?.id changes (logout then login as different user), the first effect's cleanup runs and removes event listeners, but doesn't reset hasJoinedRef or leave channels. The second effect has [socket] as its only dependency, so its cleanup only runs on unmount or socket reconnection—not on user change. This means hasJoinedRef.current remains true and joinedUserIdRef.current keeps the old user ID. When the first effect runs again for the new user, the join condition on line 68 fails (if (!hasJoinedRef.current && user?.id)), and the new user never subscribes to their channel.

Move the channel cleanup and ref reset logic from the second effect's cleanup (lines 110-122) into the first effect's cleanup (after line 104), then remove the second effect entirely. This ensures channels are properly left and refs are reset whenever the user or socket changes.

Proposed fix
     // Cleanup function
     return () => {
       console.log('🌍 useGlobalDriveSocket: Cleaning up global drive listeners');
       
       // Remove all event listeners
       events.forEach(event => {
         socket.off(event, handleDriveEvent);
       });
       
       // Clear any pending debounced refetch
       if (revalidationTimeoutRef.current) {
         clearTimeout(revalidationTimeoutRef.current);
         revalidationTimeoutRef.current = undefined;
       }
+
+      // Leave channels and reset join state when user changes
+      if (hasJoinedRef.current && joinedUserIdRef.current) {
+        socket.emit('leave_global_drives');
+        socket.emit('leave', `user:${joinedUserIdRef.current}:drives`);
+        console.log(`🌍 Left global:drives and user:${joinedUserIdRef.current}:drives channels`);
+        hasJoinedRef.current = false;
+        joinedUserIdRef.current = null;
+      }
     };
   }, [socket, user?.id, handleDriveEvent]);
-
-  // Clean up timeout on unmount
-  useEffect(() => {
-    return () => {
-      if (revalidationTimeoutRef.current) {
-        clearTimeout(revalidationTimeoutRef.current);
-      }
-
-      // Leave the global drives channel and user-specific drives channel when unmounting
-      if (socket && hasJoinedRef.current && joinedUserIdRef.current) {
-        socket.emit('leave_global_drives');
-        socket.emit('leave', `user:${joinedUserIdRef.current}:drives`);
-        console.log(`🌍 Left global:drives and user:${joinedUserIdRef.current}:drives channels`);
-        hasJoinedRef.current = false;
-        joinedUserIdRef.current = null;
-      }
-    };
-  }, [socket]);
🤖 Prompt for AI Agents
In `@apps/web/src/hooks/useGlobalDriveSocket.ts` around lines 106 - 124, The
cleanup that leaves channels and resets refs must be moved into the first
useEffect that depends on [socket, user?.id, handleDriveEvent]: inside that
effect's cleanup, clear revalidationTimeoutRef if present, and if (socket &&
hasJoinedRef.current && joinedUserIdRef.current) emit 'leave_global_drives' and
emit 'leave' for `user:${joinedUserIdRef.current}:drives`, then set
hasJoinedRef.current = false and joinedUserIdRef.current = null; after moving
this logic, remove the separate second useEffect that only depended on [socket]
so channel leave/reset happens whenever user?.id or socket changes and new users
can re-join correctly.

- Fix useGlobalDriveSocket to use user.id instead of socket.id for channel subscription
  (socket.id is the Socket.IO connection ID, not the user ID, so member events were
  never being received)
- Add permission cache invalidation when members are added/updated/removed
- Consolidate cleanup into single useEffect so channel leave/reset happens when
  user?.id or socket changes
- Add mocks for invalidateUserPermissions/invalidateDrivePermissions in tests

These changes ensure that when someone shares a drive with a user:
1. The realtime event is properly received (correct channel subscription)
2. Permission caches are invalidated immediately (no stale permissions)
3. The sidebar refreshes to show the newly shared drive

https://claude.ai/code/session_017uoRQb8sw7PuoxVgK2hhhQ
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.

2 participants