Skip to content

fix(js): dedupe notifications by id in cache.unshift#10785

Open
avasis-ai wants to merge 1 commit intonovuhq:nextfrom
avasis-ai:fix/notifications-cache-dedupe-unshift
Open

fix(js): dedupe notifications by id in cache.unshift#10785
avasis-ai wants to merge 1 commit intonovuhq:nextfrom
avasis-ai:fix/notifications-cache-dedupe-unshift

Conversation

@avasis-ai
Copy link
Copy Markdown

@avasis-ai avasis-ai commented Apr 18, 2026

Summary

Fixes #10762

The notifications.cache.unshift() method did not deduplicate by notification.id, causing duplicate notifications in useNotifications() / cache.getAll() when multiple Novu client instances receive the same socket event (e.g., multiple browser tabs).

Changes

  • packages/js/src/cache/notifications-cache.ts: Before prepending a notification in unshift(), filter out any existing entry with the same id. This makes unshift() idempotent per notification id within a given query scope.

  • packages/js/src/cache/notifications-cache.test.ts: Added two tests:

    • Deduplication: unshifting a notification with the same id replaces the existing entry (count stays 1)
    • Prepend: unshifting a genuinely new notification correctly prepends it (count goes from 1 to 2)

How to test

  1. Open a React app with <NovuProvider> and useNotifications({ archived: false, limit: 10 }) in 2+ browser tabs
  2. Trigger a new in-app notification for that subscriber
  3. Before fix: new notification appears N times (N = number of mounted clients)
  4. After fix: new notification appears exactly once

Note

Low Risk
Low risk: small, localized change to in-memory cache update logic plus added tests; main impact is altering list ordering/contents when the same notification id is unshifted repeatedly.

Overview
Fixes duplicate entries when prepending notifications into the JS NotificationsCache.

unshift() now filters out any existing cached notification with the same id before prepending the new instance, making repeated socket events for the same notification idempotent.

Adds tests covering (1) replacing an existing notification with the same id and (2) prepending a genuinely new notification without creating duplicates.

Reviewed by Cursor Bugbot for commit 4a7b784. Configure here.

What changed

The notification cache's unshift method now deduplicates notifications by ID. When prepending a notification to the cache, the method filters out any existing entry with the same ID before adding it. This prevents duplicate notifications from appearing when multiple client instances (e.g., multiple browser tabs) receive the same socket event.

Affected areas

js: Updated NotificationsCache.unshift to filter out existing notifications with the same ID before prepending a new notification instance, ensuring idempotent behavior across query scopes. Added two test cases that verify deduplication (unshifting a notification with an existing ID updates that entry without increasing the count) and prepend behavior (unshifting a new notification correctly adds it to the list).

Testing

Added two Jest test cases for NotificationsCache.unshift that verify:

  • Deduplication: unshifting a notification with the same ID as an existing cached notification results in exactly one cached entry for that ID
  • Prepend: unshifting a notification with a new ID correctly prepends it to the list without removing existing entries

@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 18, 2026

👷 Deploy request for dashboard-v2-novu-staging pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit 4a7b784

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 18, 2026

📝 Walkthrough

Walkthrough

Fixed a deduplication bug in NotificationsCache.unshift where duplicate notifications with the same id were being inserted into the cache. The implementation now filters out existing notifications matching the incoming notification's id before prepending. Added corresponding test cases to verify deduplication and prepending behaviors.

Changes

Cohort / File(s) Summary
Tests
packages/js/src/cache/notifications-cache.test.ts
Added two test cases verifying that unshift deduplicates by id (updates existing entry when same id) and prepends new ids without removing existing entries.
Implementation
packages/js/src/cache/notifications-cache.ts
Updated unshift method to filter out any existing notification with the same id from cached data before prepending the new notification instance, preventing duplicates.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Suggested reviewers

  • scopsy
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title follows Conventional Commits format with valid type 'fix', valid scope 'js', and a clear lowercase imperative description that accurately describes the deduplication fix.
Linked Issues check ✅ Passed The pull request successfully addresses all primary coding requirements from issue #10762: implements deduplication by notification.id in cache.unshift(), ensures idempotency within query scope, and includes comprehensive regression tests.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the deduplication issue: the implementation filters duplicates before prepending, and tests verify the fix without introducing unrelated modifications.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/js/src/cache/notifications-cache.ts`:
- Around line 304-308: The current update only removes duplicates from the
single cached entry (`cachedData.notifications`) which still allows `getAll()`
to return duplicates across other cached pages; change the update to deduplicate
across the entire aggregated filter scope used by `getAll()`: locate all cache
entries that share the same filter/key (the same scope `getAll()` aggregates),
remove any notifications with `notification.id` from each of those cached
entries, and then insert `notificationInstance` at the front of the appropriate
page (e.g., the current `cachedData` entry) before calling `update(args,
{...})`; ensure you operate on every cached page for that filter so `getAll()`
cannot return duplicates.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: eb0ded45-9829-4cf7-b7b2-7ab99a34e7ab

📥 Commits

Reviewing files that changed from the base of the PR and between d69ebc9 and 4a7b784.

📒 Files selected for processing (2)
  • packages/js/src/cache/notifications-cache.test.ts
  • packages/js/src/cache/notifications-cache.ts

Comment on lines +304 to +308
const dedupedNotifications = cachedData.notifications.filter((n) => n.id !== notification.id);

this.update(args, {
...cachedData,
notifications: [notificationInstance, ...cachedData.notifications],
notifications: [notificationInstance, ...dedupedNotifications],
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

Deduplicate across the aggregated filter scope, not only this cache key.

getAll() aggregates all pages with the same filter, but this only removes duplicates from the exact limit/offset cache entry. If the same notification already exists in another cached page for the same filter, getAll() can still return duplicates after unshift.

🐛 Proposed fix
     const dedupedNotifications = cachedData.notifications.filter((n) => n.id !== notification.id);
+
+    const filterKey = getFilterKey(getFilter(cacheKey));
+    this.#cache.keys().forEach((key) => {
+      if (key === cacheKey || getFilterKey(getFilter(key)) !== filterKey) {
+        return;
+      }
+
+      const value = this.#cache.get(key);
+      if (!value) {
+        return;
+      }
+
+      const notifications = value.notifications.filter((n) => n.id !== notification.id);
+      if (notifications.length !== value.notifications.length) {
+        this.#cache.set(key, { ...value, notifications });
+      }
+    });
 
     this.update(args, {
       ...cachedData,
       notifications: [notificationInstance, ...dedupedNotifications],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/js/src/cache/notifications-cache.ts` around lines 304 - 308, The
current update only removes duplicates from the single cached entry
(`cachedData.notifications`) which still allows `getAll()` to return duplicates
across other cached pages; change the update to deduplicate across the entire
aggregated filter scope used by `getAll()`: locate all cache entries that share
the same filter/key (the same scope `getAll()` aggregates), remove any
notifications with `notification.id` from each of those cached entries, and then
insert `notificationInstance` at the front of the appropriate page (e.g., the
current `cachedData` entry) before calling `update(args, {...})`; ensure you
operate on every cached page for that filter so `getAll()` cannot return
duplicates.

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.

🐛 Bug Report: notifications.cache.unshift does not dedupe by id, causing duplicate notifications from useNotifications() / cache.getAll()

2 participants