Skip to content

Notifications n plus one#2348

Merged
simo6529 merged 44 commits into
mainfrom
notifications-n-plus-one
May 7, 2026
Merged

Notifications n plus one#2348
simo6529 merged 44 commits into
mainfrom
notifications-n-plus-one

Conversation

@simo6529
Copy link
Copy Markdown
Collaborator

@simo6529 simo6529 commented May 4, 2026

Summary by CodeRabbit

  • New Features

    • Unified unread counts across connected accounts for consolidated notification totals.
    • New read-marker system with queued/retryable read requests that respect identity/proxy context.
  • Bug Fixes

    • More reliable wave-chat cleanup on leave (divider resets and delivered-notification handling).
    • Hidden-tab behavior: skip reads while hidden and resume/sync correctly when visible.

simo6529 added 24 commits April 30, 2026 15:28
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0f3a1d6d-9984-4c12-a50e-f94ed1cc3e7e

📥 Commits

Reviewing files that changed from the base of the PR and between 24a2ac3 and 1b73142.

📒 Files selected for processing (4)
  • __tests__/hooks/useMarkWaveNotificationsRead.test.tsx
  • hooks/useMarkWaveNotificationsRead.cache.ts
  • hooks/useMarkWaveNotificationsRead.helpers.ts
  • hooks/useMarkWaveNotificationsRead.requests.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • hooks/useMarkWaveNotificationsRead.requests.ts

📝 Walkthrough

Walkthrough

Centralizes and JWT-awares wave-notification "mark as read" logic with identity caching and request queuing; adds a reusable leave/cleanup hook; merges connected-account and active-profile unread counts; replaces inline API/invalidation with marker hooks across drops, realtime, and MyStreamWaveChat; and expands tests across many notification scenarios.

Changes

Wave read marker, identity & integration

Layer / File(s) Summary
Identity types & helpers
hooks/useMarkWaveNotificationsRead.identity.ts
New identity derivation: decodes JWT, computes addressKey, identityKey, proxyRoleIdentityKey, verifiedAuthHeaders, and temporary proxy-role identities; exports useWaveReadIdentityState and key helpers.
Type surface
hooks/useMarkWaveNotificationsRead.types.ts
Introduces types for mark-read results, options, request/intent shapes, cache refs, and marker state/config.
Cache & refs
hooks/useMarkWaveNotificationsRead.cache.ts
Implements caching of verified identities, temporary proxy roles, cache refs, eviction, and helpers to route mark-read via cache or enqueue requests; exposes cache sync and clear-on-unmount/address-change hooks.
Requests / queueing
hooks/useMarkWaveNotificationsRead.requests.ts
Adds batching/queueing, in-flight/pending tracking, identity-key management, enqueue/flush APIs, authenticated POST logic, requeue/retry, and verified-identity management.
Marker core & surface
hooks/useMarkWaveNotificationsRead.helpers.ts, hooks/useMarkWaveNotificationsRead.ts
Implements useWaveNotificationsReadMarkerStateFromConfig wiring auth/address/invalidateNotifications and exports public hooks useWaveNotificationsReadMarkerState and useMarkWaveNotificationsRead.
Leave cleanup hook
components/brain/my-stream/useWaveChatLeaveCleanup.ts
New client hook: clears unread divider and, if document.visibilityState === "visible", calls removeWaveDeliveredNotifications and markWaveNotificationsRead with per-operation try/catch logging.
Drops read sync refactor
components/waves/drops/wave-drops-all/hooks/useWaveDropsNotificationRead.ts
Refactors to use marker-state and useEffectEvent syncReadState; removes direct API/React Query calls; adds ReadSync state, gating (canSendReadForWave), follow-up attempt logic, and proxy-role suppression to avoid redundant syncs.
Realtime updater integration
contexts/wave/hooks/useWaveRealtimeUpdater.ts
Replaces inline POST/invalidation with markWaveNotificationsRead and removeWaveDeliveredNotifications calls wrapped in try/catch; updates hook dependencies.
Component wiring
components/brain/my-stream/MyStreamWaveChat.tsx
Replaces inline leave cleanup effect with useWaveChatLeaveCleanup, imports useMarkWaveNotificationsRead, and removes direct API/React Query wiring.
QueryKey & invalidation
components/react-query-wrapper/ReactQueryWrapper.tsx, hooks/useConnectedAccountsUnreadNotifications.ts
Adds QueryKey.CONNECTED_ACCOUNT_UNREAD_NOTIFICATIONS; extends invalidateNotifications() to invalidate it; updates connected-account hook to use the new key.
SeizeConnectContext unread merge
components/auth/SeizeConnectContext.tsx
Adds activeStoredAccount and jwtPollingStoredConnectedAccounts; reads active unread via `useUnreadNotifications(profileHandle
UX/perf tweaks
components/waves/drops/wave-drops-all/index.tsx
Adds empty-drops guard in revealPendingDrops; makes top-intersection non-async and swallows fetch errors; minor type-cast adjustment in handleQuoteClick.
Tests (widespread)
__tests__/*
Extensive test additions/updates: stabilize auth/JWT mocks, deterministic document.visibilityState helpers, add suites for marker-state/requests/cache, useWaveChatLeaveCleanup, useWaveDropsNotificationRead, useMarkWaveNotificationsRead, useConnectedAccountsUnreadNotifications, realtime updater, Wavedrops tests, and multiple component tests asserting Authorization headers and visibility gating.

Sequence Diagram

sequenceDiagram
    actor User
    participant MyStream as MyStreamWaveChat
    participant Cleanup as useWaveChatLeaveCleanup
    participant Marker as useMarkWaveNotificationsRead
    participant Identity as useWaveReadIdentityState
    participant Auth as getAuthJwt / jwtDecode
    participant API as Server (POST /notifications/wave/{id}/read)
    participant RQ as React Query

    User->>MyStream: leave wave
    MyStream->>Cleanup: register cleanup(enabled, waveId, callbacks)
    Cleanup->>Cleanup: on cleanup -> setUnreadDividerSerialNo(null)
    alt document.visibilityState == "visible"
        Cleanup->>Marker: markWaveNotificationsRead(waveId)
        Marker->>Identity: resolve identity keys & headers
        Identity->>Auth: decode/verify JWT
        Identity-->>Marker: identity keys + auth headers
        Marker->>API: POST /notifications/wave/{waveId}/read (Authorization: Bearer <jwt>)
        API-->>Marker: response
        Marker->>RQ: invalidateNotifications()
        RQ-->>Marker: queries invalidated
    else document hidden
        Cleanup-->>MyStream: skip network calls
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • ragnep
  • prxt6529
  • GelatoGenesis

"🐰
I nibble tokens, sift the state,
Merge unread maps, and mark what's late.
JWTs checked, the queues now sing,
Waves cleaned up — hop! — joy I bring. 🥕"

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Notifications n plus one' is vague and does not clearly convey the main purpose of the changeset, which involves refactoring notification read-marking logic across multiple components and hooks. Consider using a more descriptive title that reflects the primary change, such as 'Refactor wave notification read-marking into dedicated hooks and caching layer' or 'Implement centralized wave notification read-sync with multi-account support'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch notifications-n-plus-one

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: 0d9ebcac46

ℹ️ 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 thread components/waves/drops/wave-drops-all/hooks/useWaveDropsNotificationRead.ts Outdated
@simo6529
Copy link
Copy Markdown
Collaborator Author

simo6529 commented May 4, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@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

🧹 Nitpick comments (4)
contexts/wave/hooks/useWaveRealtimeUpdater.ts (1)

305-321: 💤 Low value

Consider combining the two IIFEs for cleaner code.

Both async operations are independent and currently run in parallel, which is correct. However, they could be combined into a single IIFE for slightly cleaner code structure.

♻️ Optional: Combine into single IIFE
       if (activeWaveId === waveId && document.visibilityState === "visible") {
-        void (async () => {
-          try {
-            await removeWaveDeliveredNotifications(waveId);
-          } catch (error) {
-            console.error(
-              "Failed to remove wave delivered notifications:",
-              error
-            );
-          }
-        })();
-        void (async () => {
-          try {
-            await markWaveNotificationsRead(waveId);
-          } catch (error) {
-            console.error("Failed to mark wave as read:", error);
-          }
-        })();
+        void (async () => {
+          try {
+            await removeWaveDeliveredNotifications(waveId);
+          } catch (error) {
+            console.error("Failed to remove wave delivered notifications:", error);
+          }
+          try {
+            await markWaveNotificationsRead(waveId);
+          } catch (error) {
+            console.error("Failed to mark wave as read:", error);
+          }
+        })();
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contexts/wave/hooks/useWaveRealtimeUpdater.ts` around lines 305 - 321, Two
adjacent IIFEs call removeWaveDeliveredNotifications(waveId) and
markWaveNotificationsRead(waveId) separately but can be combined for cleaner
code while preserving parallel execution; replace both with a single IIFE that
runs both calls in parallel (e.g., await
Promise.all([removeWaveDeliveredNotifications(waveId),
markWaveNotificationsRead(waveId)])) inside one try/catch and log errors with a
clear message on failure so behavior and error handling remain the same.
components/waves/drops/wave-drops-all/index.tsx (2)

222-251: 💤 Low value

The type assertion is verbose but functional.

The expanded type cast handles the structural mismatch between ApiWaveMin and the expected shape for isWaveDirectMessage. This works but is fragile if the API types change.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/waves/drops/wave-drops-all/index.tsx` around lines 222 - 251, The
verbose inline type assertion around drop.wave is fragile; update the typing so
handleQuoteClick can pass drop.wave directly to isWaveDirectMessage without that
cast. Either change isWaveDirectMessage’s parameter type to accept the actual
ApiWaveMin/ApiWave shape used by ApiDrop (or a union/partial that includes
chat?.scope?.group?.is_direct_message) or import the correct ApiWave type and
cast drop.wave to that single type (instead of the long ad-hoc object). Then
remove the big assertion in handleQuoteClick and call
isWaveDirectMessage(drop.wave.id, drop.wave) before building href with
getWaveRoute.

208-220: ⚡ Quick win

Consider logging pagination errors instead of silently swallowing them.

The .catch(() => undefined) pattern completely silences all errors from fetchNextPage. While this prevents unhandled rejections, it also hides legitimate errors (network failures, API errors) that could be useful for debugging.

The void operator usage (flagged by SonarCloud) is appropriate here for fire-and-forget async calls, but the error swallowing pattern deserves consideration.

♻️ Proposed fix to log errors
     void fetchNextPage(
       {
         waveId,
         type: DropSize.FULL,
       },
       dropId
-    ).catch(() => undefined);
+    ).catch((error) => {
+      console.error("Failed to fetch next page:", error);
+    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/waves/drops/wave-drops-all/index.tsx` around lines 208 - 220, The
handleTopIntersection callback currently swallows all errors from the
fire-and-forget fetchNextPage call; update it to catch and log errors instead of
using .catch(() => undefined). Keep the void fetchNextPage(...) pattern for
fire-and-forget but replace the empty catch with a handler that logs the caught
error (using the project's logger if available, e.g., console.error or the app's
logger) and include context (waveId, dropId, and DropSize.FULL) so failures in
fetchNextPage can be diagnosed; references: handleTopIntersection,
fetchNextPage, canFetchMoreDrops, waveId, dropId, DropSize.FULL.
__tests__/components/brain/my-stream/MyStreamWaveChat.test.tsx (1)

22-30: 💤 Low value

Consider extracting the visibility state helper to a shared test utility.

This same helper pattern appears in multiple test files (useWaveRealtimeUpdater.test.ts, MyStreamWaveChat.test.tsx). Extracting it to a shared test utility would reduce duplication.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@__tests__/components/brain/my-stream/MyStreamWaveChat.test.tsx` around lines
22 - 30, The duplicated visibility helper (documentVisibilityState and
setDocumentVisibilityState) should be extracted into a shared test utility:
create a new test helper module that exports the DocumentVisibilityState-typed
documentVisibilityState variable and the setDocumentVisibilityState function,
and then replace the inline definitions in tests like MyStreamWaveChat.test.tsx
and useWaveRealtimeUpdater.test.ts with imports from that helper; ensure the
helper uses Object.defineProperty(document, "visibilityState", ...) and export
types/signatures so existing tests can call setDocumentVisibilityState(...)
without other changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/auth/SeizeConnectContext.tsx`:
- Around line 1158-1195: The merge currently unconditionally replaces the active
account's unread count with activeUnreadNotifications?.unread_count ?? 0 which
wipes a valid JWT-backed value on first load or failed handle query; modify the
assignment in connectedAccountUnreadNotifications so it only overwrites the
JWT-backed count when the handle-scoped query has a real value (e.g.
activeUnreadNotifications is defined and unread_count is not null/undefined).
Use the existing jwtConnectedAccountUnreadNotifications as the fallback (keep
jwtPollingStoredConnectedAccounts/jwtConnectedAccountUnreadNotifications and
activeStoredAccount references) and assign
unreadNotificationsByAddress[activeAccountAddress] =
activeUnreadNotifications?.unread_count if that value exists, otherwise leave
the previous jwtConnectedAccountUnreadNotifications value intact.

In `@hooks/useMarkWaveNotificationsRead.helpers.ts`:
- Around line 62-68: The module-scoped maps inFlightWaveReadRequests,
pendingWaveReadRequests, clearedWaveReadIdentityKeysByAddress, and
latestVerifiedWaveReadIdentityByAddress can leak when auth/identity becomes
impossible or consumers unmount; add an explicit eviction and teardown path:
implement a function (e.g., clearPendingWaveReadsForAddress(address) and
clearAllWaveReadState()) that iterates pendingWaveReadRequests to reject each
pending promise with a clear error, delete entries from pendingWaveReadRequests
and inFlightWaveReadRequests, and clear the sets/maps
clearedWaveReadIdentityKeysByAddress and
latestVerifiedWaveReadIdentityByAddress; call the per-address clear on
identity-change/disconnect transitions and call clearAllWaveReadState in any
exported teardown/useEffect cleanup (or where useWaveChatLeaveCleanup awaits) so
queued promises cannot remain unresolved indefinitely.

In `@hooks/useMarkWaveNotificationsRead.identity.ts`:
- Around line 99-112: The decoded JWT in the walletAuth path (using jwtDecode
and WaveReadJwtPayload in useMarkWaveNotificationsRead.identity.ts) currently
ignores token expiry; update the identity extraction to check decodedJwt.exp
(expecting UNIX epoch seconds) against the current time and return undefined if
exp is missing or exp <= now so expired tokens are rejected before building
verifiedIdentity and attaching auth headers; ensure you lowercase decodedJwt.sub
as before and only proceed to build the addressKey/proxyCreatorId when the token
is not expired.

---

Nitpick comments:
In `@__tests__/components/brain/my-stream/MyStreamWaveChat.test.tsx`:
- Around line 22-30: The duplicated visibility helper (documentVisibilityState
and setDocumentVisibilityState) should be extracted into a shared test utility:
create a new test helper module that exports the DocumentVisibilityState-typed
documentVisibilityState variable and the setDocumentVisibilityState function,
and then replace the inline definitions in tests like MyStreamWaveChat.test.tsx
and useWaveRealtimeUpdater.test.ts with imports from that helper; ensure the
helper uses Object.defineProperty(document, "visibilityState", ...) and export
types/signatures so existing tests can call setDocumentVisibilityState(...)
without other changes.

In `@components/waves/drops/wave-drops-all/index.tsx`:
- Around line 222-251: The verbose inline type assertion around drop.wave is
fragile; update the typing so handleQuoteClick can pass drop.wave directly to
isWaveDirectMessage without that cast. Either change isWaveDirectMessage’s
parameter type to accept the actual ApiWaveMin/ApiWave shape used by ApiDrop (or
a union/partial that includes chat?.scope?.group?.is_direct_message) or import
the correct ApiWave type and cast drop.wave to that single type (instead of the
long ad-hoc object). Then remove the big assertion in handleQuoteClick and call
isWaveDirectMessage(drop.wave.id, drop.wave) before building href with
getWaveRoute.
- Around line 208-220: The handleTopIntersection callback currently swallows all
errors from the fire-and-forget fetchNextPage call; update it to catch and log
errors instead of using .catch(() => undefined). Keep the void
fetchNextPage(...) pattern for fire-and-forget but replace the empty catch with
a handler that logs the caught error (using the project's logger if available,
e.g., console.error or the app's logger) and include context (waveId, dropId,
and DropSize.FULL) so failures in fetchNextPage can be diagnosed; references:
handleTopIntersection, fetchNextPage, canFetchMoreDrops, waveId, dropId,
DropSize.FULL.

In `@contexts/wave/hooks/useWaveRealtimeUpdater.ts`:
- Around line 305-321: Two adjacent IIFEs call
removeWaveDeliveredNotifications(waveId) and markWaveNotificationsRead(waveId)
separately but can be combined for cleaner code while preserving parallel
execution; replace both with a single IIFE that runs both calls in parallel
(e.g., await Promise.all([removeWaveDeliveredNotifications(waveId),
markWaveNotificationsRead(waveId)])) inside one try/catch and log errors with a
clear message on failure so behavior and error handling remain the same.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e3f7b00e-ff07-4d61-b172-2edc3254c9cb

📥 Commits

Reviewing files that changed from the base of the PR and between 0c70235 and 0d9ebca.

📒 Files selected for processing (22)
  • __tests__/components/auth/SeizeConnectContext.addAccount.test.tsx
  • __tests__/components/auth/SeizeConnectContext.switch-sync.test.tsx
  • __tests__/components/auth/SeizeConnectContext.test.tsx
  • __tests__/components/brain/my-stream/MyStreamWaveChat.test.tsx
  • __tests__/components/brain/my-stream/useWaveChatLeaveCleanup.test.tsx
  • __tests__/components/react-query-wrapper/ReactQueryWrapper.test.tsx
  • __tests__/components/waves/drops/WaveDropsAll.test.tsx
  • __tests__/components/waves/drops/wave-drops-all/hooks/useWaveDropsNotificationRead.test.tsx
  • __tests__/hooks/useConnectedAccountsUnreadNotifications.test.ts
  • __tests__/hooks/useMarkWaveNotificationsRead.test.tsx
  • __tests__/useWaveRealtimeUpdater.test.ts
  • components/auth/SeizeConnectContext.tsx
  • components/brain/my-stream/MyStreamWaveChat.tsx
  • components/brain/my-stream/useWaveChatLeaveCleanup.ts
  • components/react-query-wrapper/ReactQueryWrapper.tsx
  • components/waves/drops/wave-drops-all/hooks/useWaveDropsNotificationRead.ts
  • components/waves/drops/wave-drops-all/index.tsx
  • contexts/wave/hooks/useWaveRealtimeUpdater.ts
  • hooks/useConnectedAccountsUnreadNotifications.ts
  • hooks/useMarkWaveNotificationsRead.helpers.ts
  • hooks/useMarkWaveNotificationsRead.identity.ts
  • hooks/useMarkWaveNotificationsRead.ts

Comment thread components/auth/SeizeConnectContext.tsx Outdated
Comment thread hooks/useMarkWaveNotificationsRead.helpers.ts Outdated
Comment thread hooks/useMarkWaveNotificationsRead.identity.ts
Signed-off-by: Simo <simo@6529.io>
simo6529 added 4 commits May 4, 2026 15:16
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
@simo6529
Copy link
Copy Markdown
Collaborator Author

simo6529 commented May 5, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@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.

🧹 Nitpick comments (2)
components/waves/drops/wave-drops-all/index.tsx (1)

208-226: 💤 Low value

Consider removing the void operator per SonarCloud guidance.

The void operator is flagged by static analysis. Since you're already swallowing errors with .catch(() => undefined), you can simply not assign the result to avoid the lint warning.

♻️ Proposed fix
   const handleTopIntersection = useCallback(() => {
     if (!canFetchMoreDrops) {
       return;
     }

-    void fetchNextPage(
+    fetchNextPage(
       {
         waveId,
         type: DropSize.FULL,
       },
       dropId
     ).catch(() => undefined);
   }, [canFetchMoreDrops, fetchNextPage, waveId, dropId]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/waves/drops/wave-drops-all/index.tsx` around lines 208 - 226, The
handleTopIntersection callback currently calls fetchNextPage with a leading void
operator, which SonarCloud flags; remove the void operator and invoke
fetchNextPage(...) directly (still appending .catch(() => undefined)) inside
handleTopIntersection so the promise is handled without using void; update the
closure to reference canFetchMoreDrops, fetchNextPage, waveId, and dropId as
before and keep the early return when !canFetchMoreDrops.
components/waves/drops/wave-drops-all/hooks/useWaveDropsNotificationRead.ts (1)

211-218: 💤 Low value

Consider removing the void operator per SonarCloud guidance.

Same pattern as in index.tsx - the void operator is flagged by static analysis.

♻️ Proposed fix
     const syncReadStateWhenVisible = () => {
       if (document.visibilityState === "visible") {
-        void syncReadState({
+        syncReadState({
           waveId,
           identityKey,
           proxyRoleIdentityKey,
         });
       }
     };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/waves/drops/wave-drops-all/hooks/useWaveDropsNotificationRead.ts`
around lines 211 - 218, The use of the `void` operator before the
promise-returning call in syncReadStateWhenVisible should be removed and the
returned promise must be handled; update the syncReadStateWhenVisible
implementation that calls syncReadState({ waveId, identityKey,
proxyRoleIdentityKey }) to either make syncReadStateWhenVisible async and await
the syncReadState call, or call syncReadState(...).catch(err => {/* log or
handle error */}) so the promise is not ignored; refer to the
syncReadStateWhenVisible function and the syncReadState call to locate and fix
the code.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@components/waves/drops/wave-drops-all/hooks/useWaveDropsNotificationRead.ts`:
- Around line 211-218: The use of the `void` operator before the
promise-returning call in syncReadStateWhenVisible should be removed and the
returned promise must be handled; update the syncReadStateWhenVisible
implementation that calls syncReadState({ waveId, identityKey,
proxyRoleIdentityKey }) to either make syncReadStateWhenVisible async and await
the syncReadState call, or call syncReadState(...).catch(err => {/* log or
handle error */}) so the promise is not ignored; refer to the
syncReadStateWhenVisible function and the syncReadState call to locate and fix
the code.

In `@components/waves/drops/wave-drops-all/index.tsx`:
- Around line 208-226: The handleTopIntersection callback currently calls
fetchNextPage with a leading void operator, which SonarCloud flags; remove the
void operator and invoke fetchNextPage(...) directly (still appending .catch(()
=> undefined)) inside handleTopIntersection so the promise is handled without
using void; update the closure to reference canFetchMoreDrops, fetchNextPage,
waveId, and dropId as before and keep the early return when !canFetchMoreDrops.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 170b5fd0-6d9e-40be-a2fe-31b6ee17a15f

📥 Commits

Reviewing files that changed from the base of the PR and between 0d9ebca and fe9b104.

📒 Files selected for processing (10)
  • __tests__/components/brain/my-stream/MyStreamWaveChat.test.tsx
  • __tests__/components/brain/my-stream/useWaveChatLeaveCleanup.test.tsx
  • __tests__/components/waves/drops/WaveDropsAll.test.tsx
  • __tests__/components/waves/drops/wave-drops-all/hooks/useWaveDropsNotificationRead.test.tsx
  • __tests__/hooks/useMarkWaveNotificationsRead.test.tsx
  • __tests__/useWaveRealtimeUpdater.test.ts
  • components/brain/my-stream/MyStreamWaveChat.tsx
  • components/brain/my-stream/useWaveChatLeaveCleanup.ts
  • components/waves/drops/wave-drops-all/hooks/useWaveDropsNotificationRead.ts
  • components/waves/drops/wave-drops-all/index.tsx
✅ Files skipped from review due to trivial changes (2)
  • components/brain/my-stream/useWaveChatLeaveCleanup.ts
  • tests/components/brain/my-stream/MyStreamWaveChat.test.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/components/brain/my-stream/useWaveChatLeaveCleanup.test.tsx
  • components/brain/my-stream/MyStreamWaveChat.tsx
  • tests/components/waves/drops/WaveDropsAll.test.tsx

simo6529 added 11 commits May 5, 2026 12:44
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Copy link
Copy Markdown

@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: 4

🧹 Nitpick comments (2)
__tests__/useWaveRealtimeUpdater.test.ts (2)

312-386: ⚡ Quick win

Add a positive test for replaying a queued read once JWT becomes available.

These new cases only prove the delayed read is dropped. If the retry path stops flushing queued reads when auth appears, this suite still passes. Please add one case where the wave stays active, the tab stays visible, getAuthJwt() flips from null to "test-jwt", and the read POST is sent exactly once.

Possible test shape
+  it("flushes a delayed active-wave read once JWT becomes available", async () => {
+    getAuthJwtMock.mockReturnValue(null);
+
+    const store = {
+      wave1: { drops: [], latestFetchedSerialNo: 10 },
+    };
+    const props = baseProps(store);
+    props.activeWaveId = "wave1";
+    const { result, rerender } = renderHook(() =>
+      useWaveRealtimeUpdater(props)
+    );
+
+    await act(async () =>
+      result.current.processIncomingDrop(
+        { id: "d-delayed-success", wave: { id: "wave1" }, author: {} } as any,
+        ProcessIncomingDropType.DROP_INSERT
+      )
+    );
+    await flushPromises();
+
+    expect(commonApiPostWithoutBodyAndResponse).not.toHaveBeenCalled();
+
+    getAuthJwtMock.mockReturnValue("test-jwt");
+    rerender();
+    await flushPromises();
+
+    expect(commonApiPostWithoutBodyAndResponse).toHaveBeenCalledTimes(1);
+    expect(commonApiPostWithoutBodyAndResponse).toHaveBeenCalledWith({
+      endpoint: "notifications/wave/wave1/read",
+      headers: { Authorization: "Bearer test-jwt" },
+    });
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@__tests__/useWaveRealtimeUpdater.test.ts` around lines 312 - 386, Add a
positive test to confirm a queued read is replayed when auth appears: create a
test that uses useWaveRealtimeUpdater and calls processIncomingDrop with a
DROP_INSERT when getAuthJwtMock initially returns null, keep props.activeWaveId
unchanged (e.g., "wave1") and document visibility "visible" (use
setDocumentVisibilityState if needed), then flip getAuthJwtMock to "test-jwt"
and rerender; assert removeWaveDeliveredNotifications was called for "wave1" and
that commonApiPostWithoutBodyAndResponse is called exactly once for the queued
read (and not zero times) to verify the retry path in useWaveRealtimeUpdater
replays the queued read. Ensure you reference processIncomingDrop,
getAuthJwtMock, commonApiPostWithoutBodyAndResponse,
removeWaveDeliveredNotifications, rerender and setDocumentVisibilityState in the
test.

59-65: ⚡ Quick win

Dispatch visibilitychange when you flip document.visibilityState.

The hook subscribes to visibilitychange events (line 429 of useWaveRealtimeUpdater.ts) and refreshes eligibility when the tab becomes visible. Redefining the getter alone only changes what a reread returns. Without dispatching the event, these tests cannot exercise the actual listener path—only the path triggered by rerender(). Update the helper to emit the event:

const setDocumentVisibilityState = (state: DocumentVisibilityState) => {
  documentVisibilityState = state;
  Object.defineProperty(document, "visibilityState", {
    configurable: true,
    get: () => documentVisibilityState,
  });
  document.dispatchEvent(new Event("visibilitychange"));
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@__tests__/useWaveRealtimeUpdater.test.ts` around lines 59 - 65, The test
helper setDocumentVisibilityState currently only redefines
document.visibilityState but doesn't dispatch the "visibilitychange" event, so
the hook's listener in useWaveRealtimeUpdater.ts (which listens for
visibilitychange) isn't exercised; update setDocumentVisibilityState to dispatch
document.dispatchEvent(new Event("visibilitychange")) after redefining the
getter so the hook's visibilitychange handler runs during tests and triggers the
real refresh/eligibility code path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@__tests__/hooks/useMarkWaveNotificationsRead.test.tsx`:
- Around line 128-146: The test fixture currently aliases
activeProfileProxyCreatorId to activeProfileProxyId via proxyCreatorId which
makes proxy id and creator id identical and hides regressions; change the setup
so proxyCreatorId is not defaulted to activeProfileProxyId (e.g., derive
proxyCreatorId directly from activeProfileProxyCreatorId or leave it
null/unique) and ensure the useAuthMock.mockReturnValue uses
activeProfileProxyCreatorId (or proxyCreatorId when explicitly provided) for
created_by.id instead of the aliased value—update any places that build
JwtPayload (sub/role) and the useSeizeConnectContextMock/ useAuthMock references
to reflect the distinct IDs.
- Around line 152-154: The beforeEach currently calls apiPostMock.mockReset()
which removes the previous async stub; after mockReset() re-stub apiPostMock to
return a resolved Promise by calling apiPostMock.mockResolvedValue(undefined) so
tests that rely on an async promise-returning API still get a promise
(preserving async/await sequencing); update the beforeEach where
jwtPayloadsByToken.clear() is called to add the mockResolvedValue restore for
apiPostMock.

In `@hooks/useMarkWaveNotificationsRead.cache.ts`:
- Around line 295-307: The cleanup currently in
useClearWaveReadStateOnLastUnmount uses useLayoutEffect and immediately calls
clearAllWaveReadState when mountedWaveNotificationsReadMarkerHookCount drops to
0, which triggers during React Strict Mode remounts; change this to defer the
actual clear by scheduling a setTimeout when the counter hits 0 and store the
timer id in a module-level variable, cancel that timeout if a new hook mounts
before it fires (clearTimeout) so the clearAllWaveReadState call is aborted on
remount, and ensure the timeout is cleared on real unmount after firing to avoid
leaks; update the logic inside the useLayoutEffect mount/unmount handlers around
mountedWaveNotificationsReadMarkerHookCount and clearAllWaveReadState to use
this deferred timer approach.

In `@hooks/useMarkWaveNotificationsRead.requests.ts`:
- Around line 497-545: The flush path groups and sends queuedRequests without
verifying each queuedRequest.addressEpoch against its current
latestAddressEpochRef.current, allowing stale reads from a prior session to be
replayed; before grouping/send (inside the loop over queuedRequests that
references pendingWaveReadRequests and getRequestKey), drop any queuedRequest
whose addressEpoch !== queuedRequest.latestAddressEpochRef.current (remove it
from pendingWaveReadRequests and do not add to queuedRequestsByRequestKey) so
only current-session requests are replayed; ensure the same stale-check is
applied for all places that call createWaveReadSendIntent/getRetryContext and
that you reject or resolve downstream promises appropriately when discarding
requests.

---

Nitpick comments:
In `@__tests__/useWaveRealtimeUpdater.test.ts`:
- Around line 312-386: Add a positive test to confirm a queued read is replayed
when auth appears: create a test that uses useWaveRealtimeUpdater and calls
processIncomingDrop with a DROP_INSERT when getAuthJwtMock initially returns
null, keep props.activeWaveId unchanged (e.g., "wave1") and document visibility
"visible" (use setDocumentVisibilityState if needed), then flip getAuthJwtMock
to "test-jwt" and rerender; assert removeWaveDeliveredNotifications was called
for "wave1" and that commonApiPostWithoutBodyAndResponse is called exactly once
for the queued read (and not zero times) to verify the retry path in
useWaveRealtimeUpdater replays the queued read. Ensure you reference
processIncomingDrop, getAuthJwtMock, commonApiPostWithoutBodyAndResponse,
removeWaveDeliveredNotifications, rerender and setDocumentVisibilityState in the
test.
- Around line 59-65: The test helper setDocumentVisibilityState currently only
redefines document.visibilityState but doesn't dispatch the "visibilitychange"
event, so the hook's listener in useWaveRealtimeUpdater.ts (which listens for
visibilitychange) isn't exercised; update setDocumentVisibilityState to dispatch
document.dispatchEvent(new Event("visibilitychange")) after redefining the
getter so the hook's visibilitychange handler runs during tests and triggers the
real refresh/eligibility code path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a1d4622b-ca28-4f9c-b1ea-2909583bd7d2

📥 Commits

Reviewing files that changed from the base of the PR and between fe9b104 and 24a2ac3.

📒 Files selected for processing (11)
  • __tests__/components/brain/my-stream/MyStreamWaveChat.test.tsx
  • __tests__/components/waves/drops/WaveDropsAll.test.tsx
  • __tests__/components/waves/drops/wave-drops-all/hooks/useWaveDropsNotificationRead.test.tsx
  • __tests__/hooks/useMarkWaveNotificationsRead.test.tsx
  • __tests__/useWaveRealtimeUpdater.test.ts
  • components/auth/SeizeConnectContext.tsx
  • hooks/useMarkWaveNotificationsRead.cache.ts
  • hooks/useMarkWaveNotificationsRead.helpers.ts
  • hooks/useMarkWaveNotificationsRead.identity.ts
  • hooks/useMarkWaveNotificationsRead.requests.ts
  • hooks/useMarkWaveNotificationsRead.types.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • hooks/useMarkWaveNotificationsRead.helpers.ts

Comment thread __tests__/hooks/useMarkWaveNotificationsRead.test.tsx
Comment thread __tests__/hooks/useMarkWaveNotificationsRead.test.tsx
Comment thread hooks/useMarkWaveNotificationsRead.cache.ts Outdated
Comment thread hooks/useMarkWaveNotificationsRead.requests.ts
simo6529 added 4 commits May 6, 2026 14:20
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented May 7, 2026

@simo6529 simo6529 merged commit 6f08543 into main May 7, 2026
8 checks passed
@simo6529 simo6529 deleted the notifications-n-plus-one branch May 7, 2026 11:29
@coderabbitai coderabbitai Bot mentioned this pull request May 8, 2026
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