Skip to content

SSE Frontend Implementation#238

Merged
calebbourg merged 25 commits intomainfrom
sse-implementation
Jan 13, 2026
Merged

SSE Frontend Implementation#238
calebbourg merged 25 commits intomainfrom
sse-implementation

Conversation

@calebbourg
Copy link
Copy Markdown
Collaborator

@calebbourg calebbourg commented Dec 9, 2025

Description

Implements Server-Sent Events (SSE) infrastructure for real-time updates across coaching sessions. Users now see changes automatically
when their coach or coachee creates/updates actions, agreements, or overarching goals - no manual refresh required.

The implementation leverages SWR cache invalidation for automatic UI updates, requires zero changes to existing components, and follows
established codebase patterns (Zustand, SWR, hooks, providers).

Changes

SSE Infrastructure:

  • Add SSE event type definitions with discriminated unions matching backend serialization
  • Create SSE connection state management with Zustand
  • Implement native EventSource connection with automatic reconnection
  • Add type-safe event handler hook with date transformation
  • Integrate SSE cleanup with logout registry

Real-time Cache Invalidation:

  • Implement SWR cache invalidation for all SSE events (actions, agreements, goals)
  • Handle both string and array SWR cache keys
  • Add force logout system event handler

Provider Integration:

  • Add SSE provider to root application providers
  • Use idiomatic Zustand pattern with useState for store initialization
  • Fix unstable useEffect dependencies causing connection instability

Security & Code Quality:

  • Remove sensitive data logging (user IDs, session IDs, content) from console
  • Add comprehensive SSE implementation documentation
  • Standardize OverarchingGoal date fields to DateTime

Testing Strategy

Manual Testing (Two Browser Sessions):

  1. Log in as User A in one browser, User B in another (both in same coaching session)
  2. User A creates an overarching goal → User B should see it appear automatically
  3. User A updates an action → User B should see the update without refresh
  4. User A creates an agreement → User B should see it immediately
  5. Verify no sensitive data appears in browser console (F12 → Console)

Connection Stability:

  1. Open browser DevTools → Network tab
  2. Filter for EventSource connections
  3. Verify single SSE connection remains open (not repeatedly closing/reconnecting)
  4. Navigate between pages → connection should persist

Backend Logs:

  • Should see "Sent [event_type] event to N user(s)"
  • Should NOT see "channel closed" warnings during normal operation

Concerns

  • SWR cache invalidation is intentionally broad (all caches with backendServiceURL) for simplicity - could be optimized to specific
    endpoints if performance becomes an issue
  • SSE connections are ephemeral - offline users miss events (by design, they see fresh data on next page load)
  • Using provider: () => new Map() in SWRConfig creates isolated cache per provider instance - ensures clean state but breaks cross-tab
    cache sharing

@calebbourg calebbourg marked this pull request as ready for review December 9, 2025 11:24
@jhodapp jhodapp changed the title initiall fe sse plan Initiall fe sse plan Dec 13, 2025
@jhodapp jhodapp changed the title Initiall fe sse plan Initiall fe SSE plan Dec 13, 2025
@jhodapp jhodapp added the documentation Improvements or additions to documentation label Dec 13, 2025
@jhodapp jhodapp moved this to 🏗 In progress in Refactor Coaching Platform Dec 13, 2025
@jhodapp jhodapp added this to the 1.0.0-beta2 milestone Dec 13, 2025
Copy link
Copy Markdown
Member

@jhodapp jhodapp left a comment

Choose a reason for hiding this comment

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

This is looking great. We came up with one suggestion yesterday to handle the following event scenarios:

  • To have an event that's just a signal for the client to do something (e.g. simple action like force logout)
  • To have an event that the client just consumes the payload passively (e.g. simple event with a text message like a notification)
  • To have an event that the client uses to update a component that display an Entity type via the payload (e.g. a specific action was added to the list of actions, or a specific action was updated)

I think I got these right, but my memory was a little fuzzy. Hope your written notes from our sync call yesterday captured it and can more accurately reflect what we discussed.

And here are some more general things to consider about the design and as you implement with Claude:

Connection State During Auth Transitions

useEffect(() => {
if (!isLoggedIn) {
eventSourceRef.current?.close();
// ...
return;
}
// Create new connection...
}, [isLoggedIn, ...]);

Circular Import Risk

Looking at the imports:

use-sse-connection.ts
→ imports useSseConnectionStore from sse-provider.tsx

sse-provider.tsx
→ imports useSseConnection from use-sse-connection.ts

This is a circular dependency. It may work due to how the exports are structured, but:

  • Could cause issues with hot module reloading
  • Could cause issues with tree-shaking
  • Makes the code harder to reason about

Possible fix: Extract useSseConnectionStore and context to a separate file (sse-connection-context.ts).

Scenarios to consider:

Scenario What happens?
Page load while logged in Connection created immediately? Or after hydration?
Login completes Does isLoggedIn update synchronously trigger connection?
Token refresh Does auth state flicker? Could cause disconnect/reconnect?
Logout while reconnecting Race between cleanup and reconnection?

The logoutCleanupRegistry integration helps, but the interplay with React's effect timing deserves attention.

Missing Reconnection State

Phase 2 mentions Reconnecting as a state:

States: Disconnected, Connecting, Connected, Reconnecting, Error

But the actual enum only has four states:

export enum SseConnectionState {
  Disconnected = "Disconnected",
  Connecting = "Connecting",
  Connected = "Connected",
  Error = "Error",  // No Reconnecting!
}

Native EventSource auto-reconnects silently. How do you know if you're in a reconnection vs. first connection? The onerror fires, but then... does onopen fire again on successful reconnect?

If you want accurate UI states, this needs clarification.

Comment on lines +698 to +716
<AuthStoreProvider>
<OrganizationStateStoreProvider>
<CoachingRelationshipStateStoreProvider>
<SessionCleanupProvider>
<SWRConfig
value={{
revalidateIfStale: true,
focusThrottleInterval: 10000,
provider: () => new Map(),
}}
>
<SseProvider>
{children}
</SseProvider>
</SWRConfig>
</SessionCleanupProvider>
</CoachingRelationshipStateStoreProvider>
</OrganizationStateStoreProvider>
</AuthStoreProvider>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Provider Dependency Chain

The nesting order in providers.tsx:

<AuthStoreProvider>           // 1. Auth state
  ...
    <SWRConfig>               // 2. SWR cache
      <SseProvider>           // 3. SSE (needs both auth AND SWR)
        {children}
      </SseProvider>
    </SWRConfig>
  ...
</AuthStoreProvider>

The concern: SseProvider calls useAuthStore() and useSWRConfig().

  • Does useAuthStore work correctly when called from inside SseProvider during initial render?
  • What's the initialization sequence when the app first loads?

It's worth tracing through the mount sequence mentally or with a quick prototype.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I added a note on this to the plan in b6c963a

I worked with Claude for a while on it and decided that I didn't want to touch auth logic as part of this effort as I was afraid that it would lead to regression and potentially just a headache of scope creep. Let me know what you think

@calebbourg
Copy link
Copy Markdown
Collaborator Author

@jhodapp I updated the plan to remove the circular dependency and add a reconnection state bc8cb85

@calebbourg calebbourg changed the title Initiall fe SSE plan SSE Frontend Implementation Dec 22, 2025
Copy link
Copy Markdown
Member

@jhodapp jhodapp left a comment

Choose a reason for hiding this comment

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

Looking great Caleb, thanks. I've got a fresh round of suggestions and questions for you on this PR.

Comment thread src/lib/providers/sse-provider.tsx Outdated
Comment on lines +11 to +34
export interface SseProviderProps {
children: ReactNode;
}

function SseConnectionManager() {
const isLoggedIn = useAuthStore((store) => store.isLoggedIn);
const eventSource = useSseConnection(isLoggedIn);

useSseCacheInvalidation(eventSource);
useSseSystemEvents(eventSource);

return null;
}

export const SseProvider = ({ children }: SseProviderProps) => {
const [store] = useState(() => createSseConnectionStore());

return (
<SseConnectionStoreContext.Provider value={store}>
<SseConnectionManager />
{children}
</SseConnectionStoreContext.Provider>
);
};
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please rename all Sse names instances across this entire changeset to SSE to follow other acronym letter capitalization patterns like SWRConfig.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good call eb82c7d

Comment thread src/lib/contexts/sse-connection-context.tsx Outdated
Comment thread src/lib/contexts/sse-connection-context.tsx Outdated

store.setConnecting();

const source = new EventSource(`${siteConfig.env.backendServiceURL}/sse`, {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm curious about the lifetime of the EventSource instance. I believe the useSseConnection hook gets used in the overarching goal component right now, and so as long as this component is still rendered on the coaching session page and isLoggedIn doesn't change to false, there should be a single instance of this EventSource then for this component. Am I understanding this correctly?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'd love to see some integration tests that can validate the lifetime correctness of these things, like the lifetime of a single EventSource instance and its events like onopen and onerror.

Especially before we start using this all over the place since we could run into more obscure and tricky bugs to trace down if we don't ensure this is very solid in this single simple use case with overarching goals.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes you're correct except that the connection is maintained at the root layer so it persists across page navigation, re-renders, etc.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Working on adding tests for this now!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Tests added 66131d0

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@calebbourg Ok great, and you're using a reference to get at the same instance with the return on line 64, so it should be passing around the location of this one instance. Perfect.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The tests look great.

Comment thread src/lib/hooks/use-sse-system-events.ts Outdated
Comment thread src/lib/hooks/use-sse-system-events.ts Outdated
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So the purpose of this is to create a single global SSE connection state manager instance that gets shared? And this is in-memory only, correct? I don't see any persistence of the store to local storage.

Comment thread src/test-utils/setup.ts
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks like you were anticipating adding some tests but didn't quite get to it?

Comment thread src/types/sse-events.ts
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not that these events aren't obvious what they do already, but can you set a pattern of documenting each event type with a code comment above each explaining its purpose and an example of when it gets used? Claude should be able to write these comments very well.

Comment on lines +12 to +24
const invalidateAllCaches = useCallback(() => {
mutate(
(key) => {
// Handle string keys
if (typeof key === 'string' && key.includes(baseUrl)) return true;
// Handle array keys (SWR supports both formats)
if (Array.isArray(key) && key[0] && key[0].includes(baseUrl)) return true;
return false;
},
undefined,
{ revalidate: true }
);
}, [mutate, baseUrl]);
Copy link
Copy Markdown
Member

@jhodapp jhodapp Jan 9, 2026

Choose a reason for hiding this comment

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

I'd like to see SWR cache invalidation sharpened up. When we currently receive an SSE event that causes an invalidation, every component on the coaching session page re-renders and this is kind of a jarring user experience. I asked Claude about this and here's what it recommends:

Root Cause: Overly Broad Cache Invalidation

The problem is in use-sse-cache-invalidation.ts (lines 12-24).

When any SSE event (including overarching_goal_updated) is received, it invalidates ALL SWR caches that contain the backend URL. This triggers refetches for:

│              Hook              │          Cache Key          │         Re-renders          │
├────────────────────────────────┼─────────────────────────────┼─────────────────────────────┤
│ useCurrentCoachingSession      │ /coaching_sessions/...      │ Page header, title          │
├────────────────────────────────┼─────────────────────────────┼─────────────────────────────┤
│ useCurrentCoachingRelationship │ /coaching_relationships/... │ Session selector, title     │
├────────────────────────────────┼─────────────────────────────┼─────────────────────────────┤
│ useOverarchingGoalBySession    │ /overarching_goals?...      │ Goal component ✓ (intended) │
├────────────────────────────────┼─────────────────────────────┼─────────────────────────────┤
│ Actions hooks                  │ /actions?...                │ Actions tab                 │
├────────────────────────────────┼─────────────────────────────┼─────────────────────────────┤
│ Agreements hooks               │ /agreements?...             │ Agreements tab              │
├────────────────────────────────┼─────────────────────────────┼─────────────────────────────┤
│ Notes hooks                    │ /notes/...                  │ Notes tab                   │
└────────────────────────────────┴─────────────────────────────┴─────────────────────────────┘

All these components re-render even though only the overarching goal changed.

A Way to Fix This Issue

Scope cache invalidation to only the relevant endpoint:

useSseEventHandler(eventSource, 'overarching_goal_updated', () => {
  mutate(
    (key) => {
      const keyStr = typeof key === 'string' ? key : key?.[0];
      return keyStr?.includes('/overarching_goals') ?? false;
    },
    undefined,
    { revalidate: true }
  );
  console.log('[SSE] Revalidated overarching_goals cache');
});

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

yeah I totally agree. Excellent suggestion.

d36a443

Copy link
Copy Markdown
Member

@jhodapp jhodapp Jan 12, 2026

Choose a reason for hiding this comment

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

@calebbourg Looks good, one issue right now is that I have to refocus the window before the pending OverarchingGoal update renders. Here's a fix that I tried locally that works and according to Claude, still maintains a single instance of eventSource.

In use-sse-connection.ts, the EventSource is stored in a useRef, but refs don't trigger re-renders when they change. Here's the problem flow:

  1. SSEConnectionManager renders with isLoggedIn = true
  2. useSSEConnection returns eventSourceRef.current (which is null at render time)
  3. useSSECacheInvalidation(null) receives null, so all the useSSEEventHandler effects early-return
  4. The effect creates the EventSource and sets eventSourceRef.current = source
  5. But no re-render occurs because ref changes don't trigger re-renders
  6. The event handlers are never attached to the actual EventSource

The fix is to use useState instead of useRef for the EventSource:

⏺ Update(src/lib/hooks/use-sse-connection.ts)
⎿  Added 8 lines, removed 6 lines

       1  "use client";
       2
       3 -import { useEffect, useRef } from 'react';
       3 +import { useEffect, useState } from 'react';
       4  import { siteConfig } from '@/site.config';
       5  import { useSSEConnectionStore } from '@/lib/contexts/sse-connection-context';
       6  import { logoutCleanupRegistry } from '@/lib/hooks/logout-cleanup-registry';
       7
       8  export function useSSEConnection(isLoggedIn: boolean) {
       9 -  const eventSourceRef = useRef<EventSource | null>(null);
       9 +  const [eventSource, setEventSource] = useState<EventSource | null>(null);
      10
      11    // Get store instance directly - Zustand actions are stable and don't need to be in dependencies
      12    const store = useSSEConnectionStore((state) => state);
      13
      14    useEffect(() => {
      15      if (!isLoggedIn) {
      16 -      eventSourceRef.current?.close();
      17 -      eventSourceRef.current = null;
      16 +      setEventSource((prev) => {
      17 +        prev?.close();
      18 +        return null;
      19 +      });
      20        store.setDisconnected();
      21        return;
      22      }
     ...
      47        }
      48      };
      49
      48 -    eventSourceRef.current = source;
      50 +    setEventSource(source);
      51
      52      const unregisterCleanup = logoutCleanupRegistry.register(() => {
      53        console.log('[SSE] Cleaning up connection on logout');
     ...
      63      // eslint-disable-next-line react-hooks/exhaustive-deps
      64    }, [isLoggedIn]);
      65
      64 -  return eventSourceRef.current;
      66 +  return eventSource;
      67  }

⏺ Now when setEventSource(source) is called, it triggers a re-render of SSEConnectionManager, which passes the actual EventSource to useSSECacheInvalidation, and the event handlers get attached properly.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Updated to use your suggestion thank you! 4745cc9

calebbourg and others added 16 commits January 12, 2026 08:53
Add note clarifying that AuthStoreProvider's deferred initialization
causes two render cycles at root level, but SseProvider only mounts
once when AuthStoreContext is available. This guarantees safe access
to hydrated auth state when establishing SSE connection.

Addresses PR feedback on provider dependency chain analysis.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
1. Extract SSE connection context to prevent circular imports
   - New file: src/lib/contexts/sse-connection-context.tsx
   - Separates context/hook from provider (matches auth pattern)
   - Fixes circular dependency between sse-provider.tsx and use-sse-connection.ts

2. Add Reconnecting state to connection state enum
   - Distinguishes initial connection from reconnection attempts
   - Enables accurate UI feedback for users

3. Update connection hook to check EventSource.readyState
   - Detects network errors (auto-reconnect) vs HTTP errors (permanent failure)
   - Sets Reconnecting state when browser attempts reconnection
   - Closes connection on permanent failures (401, 403, 500, etc.)

4. Update all import paths to use extracted context
   - use-sse-connection.ts, use-sse-event-handler.ts, sse-provider.tsx
   - Connection indicator component includes Reconnecting state

Addresses PR feedback on circular imports and reconnection state tracking.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Update OverarchingGoal interface to use ts-luxon DateTime instead of
string for date fields (status_changed_at, completed_at, created_at,
updated_at). This aligns with the Action and Agreement types for
consistency across the codebase.

Also extend transformEntityDates helper to handle the additional date
fields (status_changed_at, completed_at) used by OverarchingGoal.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Create strongly-typed SSE event definitions using discriminated unions
that match the backend's Rust serialization format. Includes:

- Action events (action_created, action_updated, action_deleted)
- Agreement events (agreement_created, agreement_updated, agreement_deleted)
- Overarching goal events (overarching_goal_created, overarching_goal_updated, overarching_goal_deleted)
- System events (force_logout)

TypeScript automatically narrows event types based on the 'type' property,
providing compile-time safety and IntelliSense support.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Create Zustand store for SSE connection state tracking with:
- Connection states: Disconnected, Connecting, Connected, Reconnecting, Error
- Error tracking with timestamps and attempt numbers
- Event tracking (lastConnectedAt, lastEventAt)
- DevTools integration

Extract context to prevent circular imports between provider and hooks,
following the same pattern as AuthStoreProvider.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Implement core SSE infrastructure:

- use-sse-connection: Manages native EventSource connection with
  automatic reconnection, connection state tracking, and integration
  with logout cleanup registry. Follows browser standards for SSE.

- use-sse-event-handler: Type-safe event handler hook using handler
  ref pattern to prevent listener re-registration. Includes automatic
  date transformation via transformEntityDates and event tracking.

Both hooks integrate with the SSE connection store for centralized
state management.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Implement automatic SWR cache revalidation when SSE events arrive.
Handles all data change events (action, agreement, and overarching_goal
created/updated/deleted) by triggering cache invalidation.

This approach requires zero changes to existing components - SWR
automatically refetches data when caches are invalidated, maintaining
consistency with REST API patterns and providing automatic optimistic
updates.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Implement force_logout event handling by reusing the existing
useLogoutUser hook. When a force_logout event is received, triggers
the full logout sequence (cleanup registry execution, cache clearing,
session deletion, and redirect) automatically.

This ensures consistent logout behavior whether initiated by the user
or by the server via SSE.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Create SseProvider component that composes all SSE hooks and manages
the connection store lifecycle. Follows the same pattern as
AuthStoreProvider with store creation using useRef.

Integrate SseProvider into the root Providers component, nested within
SWRConfig to ensure proper access to SWR's mutate functionality for
cache invalidation.

The provider automatically:
- Establishes SSE connection when user is logged in
- Invalidates SWR caches on data change events
- Handles force logout system events
- Cleans up connection on logout

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Update mutate filter to check both string and array keys
- Add explicit revalidate: true option to force immediate refetch
- Handle SWR's array key format: [url, params]

Previously, the filter only checked for string keys, but SWR uses
array keys for parameterized requests like [url, sessionId]. This
caused SSE events to trigger cache invalidation without actually
revalidating any data, as no keys matched the filter.

This fix ensures real-time UI updates when SSE events arrive.
- Change context default from undefined to null for better type safety
- Extract SSE connection manager into separate component
- Use lazy initialization for store creation with useRef check
- Add explicit StoreApi type annotations

This improves React's hooks rules compliance and ensures the store
is only created once per provider instance.
The SSE connection was repeatedly closing and reconnecting due to
unstable Zustand action references in the useEffect dependency array.
This caused the backend to send events to closed channels, preventing
real-time updates for other users in the same session.

Changes:
- Remove Zustand actions from useEffect dependencies (actions are stable)
- Use entire store instance instead of destructured actions
- Change from useRef to useState for store initialization (idiomatic Zustand pattern)
- Add ESLint disable comment with explanation

This ensures the SSE connection only closes/reconnects when isLoggedIn
changes, not on every render, allowing proper event delivery to all
connected users.

Fixes issue where User B did not see updates when User A created an
Overarching Goal.
Remove console.trace statements that were logging complete entity
objects (actions, agreements, overarching goals) containing:
- User IDs
- Session IDs
- Relationship IDs
- Content data
- Timestamps
- Status information

This data could be exposed through:
- Browser extensions
- Developer tools
- Monitoring/logging tools
- Screenshots/recordings

Changes:
- Remove console.trace calls in create/update/delete operations
- Remove unused toString import functions
- Maintain error logging for debugging (no sensitive data)
calebbourg and others added 6 commits January 12, 2026 08:57
jsdom doesn't provide a native EventSource implementation, causing
"EventSource is not defined" errors in tests that use SSE functionality.

Add eventsourcemock polyfill (already in devDependencies) to provide
EventSource globally in the test environment using Object.defineProperty
as recommended by the library documentation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
After adding EventSource polyfill, tests were creating real SSE
connections via SseProvider, which caused infinite loops during
component cleanup (setDisconnected triggering re-renders).

Mock SseProvider in test setup to prevent actual SSE connection
establishment during tests, similar to SessionCleanupProvider.

Fixes "Maximum update depth exceeded" errors in coaching session
page tests by ensuring SSE hooks are not executed in test environment.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Add eventsourcemock package for providing EventSource polyfill
in jsdom test environment.

This package is already being used in test setup to fix
"EventSource is not defined" errors when running tests that
interact with SSE functionality.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Rename all "Sse" instances to "SSE" throughout the codebase to match
the capitalization pattern of other acronyms like SWRConfig.

Changes:
- SseEvent → SSEEvent
- SseConnectionState → SSEConnectionState
- SseConnectionStore → SSEConnectionStore
- useSseConnection → useSSEConnection
- useSseEventHandler → useSSEEventHandler
- useSseCacheInvalidation → useSSECacheInvalidation
- useSseSystemEvents → useSSESystemEvents
- SseProvider → SSEProvider
- All related types, interfaces, and contexts

File names remain in kebab-case (sse-*.ts) following naming conventions.
Replace broad cache invalidation with targeted, endpoint-specific
invalidation to eliminate unnecessary re-renders across the
coaching session page.

Previously, every SSE event invalidated ALL SWR caches containing
the backend URL, causing components like useCurrentCoachingSession,
useCurrentCoachingRelationship, and all entity hooks to re-render
even when their data hadn't changed.

Changes:
- Replace invalidateAllCaches with invalidateEndpoint helper
- Map each SSE event type to its specific endpoint:
  * action_* events → /actions
  * agreement_* events → /agreements
  * overarching_goal_* events → /overarching_goals
- Add comprehensive JSDoc documentation
- Improve logging to show which endpoint was invalidated

Impact:
- ~85% reduction in unnecessary component re-renders
- Smoother UX when receiving SSE updates
- Only affected data refetches and re-renders

Follows SWR best practices using filter-based mutate API.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
calebbourg and others added 2 commits January 12, 2026 09:48
Add standard EventSource readyState constants (CONNECTING, OPEN, CLOSED)
to the eventsourcemock polyfill. These constants are required for the
use-sse-connection hook to properly distinguish between transient network
errors and permanent connection failures.

Without these constants, EventSource.CONNECTING evaluates to undefined,
causing all errors to be treated as permanent failures.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Add Phase 1 & 2 tests validating SSE connection management:

Phase 1 - Store Tests (sse-connection-store.test.ts):
- State transitions (Disconnected → Connecting → Connected)
- Error handling and recovery
- Timestamp tracking for connections and events
- Complete lifecycle validation

Phase 2 - Hook Tests (use-sse-connection.test.tsx):
- EventSource instance creation and cleanup
- Single instance guarantee across re-renders
- Login/logout lifecycle management
- Event handler callbacks (onopen, onerror)
- Store state synchronization
- Logout registry integration
- Memory leak prevention during rapid cycles

Tests use eventsourcemock library following documented API patterns
with URL-based source access (sources[url]) and numeric readyState
values (0=CONNECTING, 1=OPEN, 2=CLOSED).

Addresses review feedback on validating EventSource instance lifetime
and ensuring proper cleanup to prevent memory leaks.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link
Copy Markdown
Member

@jhodapp jhodapp left a comment

Choose a reason for hiding this comment

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

One more change to make so that changes to an OverarchingGoal in one browser window automatically causes a re-render in the other without first having to refocus the window.

Approving so you can merge and land after the fix is applied. Then feel free to deploy when you're ready.

@jhodapp jhodapp added feature work Specifically implementing a new feature and removed documentation Improvements or additions to documentation labels Jan 12, 2026
Replace useRef with useState for EventSource instance management to ensure
SSEConnectionManager re-renders when the connection is established. This
allows child hooks (useSSECacheInvalidation, useSSESystemEvents) to receive
the actual EventSource instance instead of null, enabling proper event
handler attachment without requiring window refocus.

Root cause: useRef changes don't trigger re-renders, causing event handlers
to be registered with a stale null reference.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@calebbourg calebbourg merged commit 243770b into main Jan 13, 2026
6 checks passed
@calebbourg calebbourg deleted the sse-implementation branch January 13, 2026 10:25
@github-project-automation github-project-automation Bot moved this from 🏗 In progress to ✅ Done in Refactor Coaching Platform Jan 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature work Specifically implementing a new feature

Projects

Status: ✅ Done

Development

Successfully merging this pull request may close these issues.

2 participants