← All @molecule/* packages · App templates

@molecule/app-notification-center

Core interface · notification-center · App (browser) · v1.0.1 · Apache-2.0

Notification center core interface for molecule.dev — in-app notification bell with pagination, read tracking, realtime updates, and polling

npm install @molecule/app-notification-center

npm · Source on GitHub

How it works

@molecule/app-notification-center is the notification-center core interface on the app (browser) side: the API your app calls, with no vendor inside.

Choose the implementation by bonding one of its 1 provider: @molecule/app-notification-center-default.

import { setProvider, createNotificationCenter } from '@molecule/app-notification-center'
import { provider } from '@molecule/app-notification-center-default'

setProvider(provider)

const center = createNotificationCenter({
  fetchNotifications: (opts) => api.get('/notifications', opts),
  fetchUnreadCount: () => api.get('/notifications/unread-count'),
  markAsRead: (id) => api.post(`/notifications/${id}/read`),
  markAllAsRead: () => api.post('/notifications/read-all'),
  pollInterval: 30_000,
})

center.onUpdate((state) => {
  console.log('Unread:', state.unreadCount)
})

Providers (1): @molecule/app-notification-center-default

Works with: @molecule/app-bond

Reference

Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit src/index.ts JSDoc, not this file.

Notification center core interface for molecule.dev.

Provides a framework-agnostic contract for in-app notification center widgets with paginated fetching, read/unread tracking, realtime push updates, polling, and subscription-based state notifications. Bond a provider (e.g. @molecule/app-notification-center-default) at startup, then use {@link createNotificationCenter} anywhere.

Quick Start

import { setProvider, createNotificationCenter } from '@molecule/app-notification-center'
import { provider } from '@molecule/app-notification-center-default'

setProvider(provider)

const center = createNotificationCenter({
  fetchNotifications: (opts) => api.get('/notifications', opts),
  fetchUnreadCount: () => api.get('/notifications/unread-count'),
  markAsRead: (id) => api.post(`/notifications/${id}/read`),
  markAllAsRead: () => api.post('/notifications/read-all'),
  pollInterval: 30_000,
})

center.onUpdate((state) => {
  console.log('Unread:', state.unreadCount)
})

Type

core

Installation

npm install @molecule/app-notification-center @molecule/app-bond

API

Interfaces

AppNotification

A single in-app notification.

interface AppNotification {
  /** Unique identifier. */
  id: string
  /** Notification type key (e.g. `'mention'`, `'comment'`, `'invite'`). */
  type: string
  /** Short title for the notification (pass through i18n before setting). */
  title: string
  /** Body / description text (pass through i18n before setting). */
  body: string
  /** Whether the notification has been read. */
  read: boolean
  /** Optional URL the notification links to. */
  actionUrl?: string
  /** Optional avatar URL for the notification sender / actor. */
  avatar?: string
  /** Timestamp when the notification was created. */
  createdAt: Date
  /** Arbitrary metadata attached to the notification. */
  metadata?: Record<string, unknown>
}

FetchOptions

Options for fetching a page of notifications.

interface FetchOptions {
  /** Opaque cursor for cursor-based pagination. `undefined` fetches the first page. */
  cursor?: string
  /** Maximum number of items to return per page. */
  limit?: number
  /** Optional filter criteria. */
  filter?: NotificationFilter
}

NotificationCenterInstance

A live notification center instance exposing query, mutation, and subscription methods.

interface NotificationCenterInstance {
  // -- Query ---------------------------------------------------------------

  /**
   * Returns the currently loaded notifications.
   *
   * @returns Array of notifications loaded so far.
   */
  getNotifications(): AppNotification[]

  /**
   * Returns the current unread notification count.
   *
   * @returns The unread count.
   */
  getUnreadCount(): number

  /**
   * Returns whether more notifications can be loaded.
   *
   * @returns `true` if additional pages are available.
   */
  hasMore(): boolean

  /**
   * Returns whether a fetch operation is currently in progress.
   *
   * @returns `true` if loading.
   */
  isLoading(): boolean

  // -- Actions -------------------------------------------------------------

  /**
   * Loads the next page of notifications (appends to the current list).
   */
  loadMore(): Promise<void>

  /**
   * Refreshes the notification list and unread count from scratch.
   */
  refresh(): Promise<void>

  /**
   * Marks a single notification as read.
   *
   * @param notificationId - The id of the notification to mark as read.
   */
  markAsRead(notificationId: string): Promise<void>

  /** Marks all notifications as read. */
  markAllAsRead(): Promise<void>

  // -- Subscriptions -------------------------------------------------------

  /**
   * Registers a handler that fires when the notification center state changes.
   *
   * @param handler - The update handler.
   */
  onUpdate(handler: NotificationUpdateHandler): void

  /**
   * Removes a previously registered update handler.
   *
   * @param handler - The handler to remove.
   */
  offUpdate(handler: NotificationUpdateHandler): void

  // -- Lifecycle -----------------------------------------------------------

  /**
   * Returns the current state snapshot.
   *
   * @returns The current {@link NotificationCenterState}.
   */
  getState(): NotificationCenterState

  /**
   * Releases resources held by the notification center instance
   * (stops polling, removes realtime listeners, clears subscriptions).
   */
  destroy(): void
}

NotificationCenterOptions

Configuration for creating a notification center instance.

interface NotificationCenterOptions {
  /**
   * Fetches a page of notifications.
   *
   * @param options - Pagination and filter options.
   * @returns A promise resolving to a paginated result of notifications.
   */
  fetchNotifications: (options: FetchOptions) => Promise<PaginatedResult<AppNotification>>

  /**
   * Fetches the current unread notification count.
   *
   * @returns A promise resolving to the unread count.
   */
  fetchUnreadCount: () => Promise<number>

  /**
   * Marks a single notification as read.
   *
   * @param notificationId - The id of the notification to mark as read.
   */
  markAsRead: (notificationId: string) => Promise<void>

  /** Marks all notifications as read. */
  markAllAsRead: () => Promise<void>

  /**
   * Called when a new notification is received (via polling or realtime).
   *
   * @param notification - The new notification.
   */
  onNotification?: (notification: AppNotification) => void

  /**
   * Polling interval in milliseconds for fetching new notifications.
   * Set to `0` or `undefined` to disable polling. Defaults to `0` (disabled).
   */
  pollInterval?: number

  /**
   * Optional realtime transport for receiving push notifications.
   * Any object satisfying {@link NotificationRealtimeAdapter} (e.g. a
   * `RealtimeConnection` from `@molecule/app-realtime`) can be used.
   */
  realtime?: NotificationRealtimeAdapter

  /**
   * The event name to listen for on the realtime adapter.
   * Defaults to `'notification'`.
   */
  realtimeEvent?: string
}

NotificationCenterProvider

Contract that bond packages must implement to provide notification center functionality.

interface NotificationCenterProvider {
  /**
   * Creates a new notification center instance from the given options.
   *
   * @param options - Notification center configuration.
   * @returns A notification center instance.
   */
  createNotificationCenter(options: NotificationCenterOptions): NotificationCenterInstance
}

NotificationCenterState

Snapshot of the notification center state, emitted to subscribers on change.

interface NotificationCenterState {
  /** Current list of loaded notifications. */
  notifications: AppNotification[]
  /** Current unread notification count. */
  unreadCount: number
  /** Whether a fetch operation is in progress. */
  loading: boolean
  /** Whether more notifications are available to load. */
  hasMore: boolean
  /**
   * The error from the most recent failed `refresh()` / `loadMore()` /
   * `poll()` attempt, or `undefined` if the last attempt (or the most
   * recent one per-operation) succeeded. Providers MUST clear this (set it
   * back to `undefined`) on the next successful fetch — it is not a sticky
   * banner.
   *
   * Without this field a provider that swallows fetch failures (documented
   * noop) renders identically whether the user genuinely has zero
   * notifications OR the very first fetch failed (network/server error):
   * both look like an empty inbox with `loading: false`. Consumers should
   * check this field to show a retry banner instead of a bare empty state.
   */
  lastError?: Error
}

NotificationFilter

Filter criteria for narrowing notification queries.

interface NotificationFilter {
  /** Filter by notification type (e.g. `'mention'`, `'comment'`). */
  type?: string
  /** Filter by read status. */
  read?: boolean
}

NotificationRealtimeAdapter

Minimal realtime transport interface for receiving push notifications.

Any object with on / off methods satisfies this contract — including a realtime connection from the @molecule/app-realtime package.

interface NotificationRealtimeAdapter {
  /**
   * Registers a handler for an incoming event.
   *
   * @param event - The event name to listen for.
   * @param handler - The handler callback.
   */
  on(event: string, handler: (data: unknown) => void): void

  /**
   * Removes a handler for an event.
   *
   * @param event - The event name.
   * @param handler - The specific handler to remove (optional).
   */
  off(event: string, handler?: (data: unknown) => void): void
}

PaginatedResult

A generic paginated result set.

interface PaginatedResult<T> {
  /** The items for the current page. */
  items: T[]
  /** Cursor for fetching the next page. `undefined` when there are no more pages. */
  nextCursor?: string
  /** Whether more items are available beyond this page. */
  hasMore: boolean
  /** Optional total count of items across all pages. */
  total?: number
}

Types

NotificationUpdateHandler

Handler invoked when the notification center state changes.

type NotificationUpdateHandler = (state: NotificationCenterState) => void

Functions

createNotificationCenter(options)

Creates a notification center instance using the bonded provider.

function createNotificationCenter(options: NotificationCenterOptions): NotificationCenterInstance
  • options — Notification center configuration.

Returns: A notification center instance.

getProvider()

Retrieves the bonded notification center provider, throwing if none is configured.

function getProvider(): NotificationCenterProvider

Returns: The bonded notification center provider.

hasProvider()

Checks whether a notification center provider is currently bonded.

function hasProvider(): boolean

Returns: true if a notification center provider is bonded.

setProvider(provider)

Registers a notification center provider as the active singleton. Called by bond packages (e.g. @molecule/app-notification-center-default) during app startup.

function setProvider(provider: NotificationCenterProvider): void
  • provider — The notification center provider implementation to bond.

Available Providers

ProviderPackage
Notification Center@molecule/app-notification-center-default

Injection Notes

Requirements

Peer dependencies:

  • @molecule/app-bond ^1.0.1

Runtime Dependencies

  • @molecule/app-bond

NotificationCenterState.lastError is the error surface for a failed refresh() / loadMore() / poll() attempt. Without it, a provider that documents fetch failures as a silent noop renders a FIRST-load failure identically to "you have no notifications" — both are an empty list with loading: false, and a consumer UI cannot tell "network/server error" from "genuinely empty" or show a retry banner.

Provider implementations (e.g. @molecule/app-notification-center-default) MUST populate lastError in their fetch catch blocks and clear it (lastError: undefined) on the next successful fetch — it is not a sticky banner that survives a later success. Framework bindings (e.g. @molecule/app-notification-center-react) should read this field to render a retry affordance instead of a bare empty state.

E2E Tests

Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:

  • Unread notifications render with a distinct unread treatment vs read ones, and the unread badge/count (unreadCount / getUnreadCount()) exactly equals the number of loaded AppNotifications with read: false.
  • Clicking a notification navigates to its own actionUrl (its target, not a shared/hardcoded route) AND marks it read (markAsRead(id)): its read flag flips, it loses the unread treatment, and the badge decrements by one — and that stays after a full reload (persisted server-side, not just local state).
  • markAllAsRead() drops the unread badge to 0 and NO remaining item still shows the unread treatment; the cleared state survives a reload.
  • Applying a filter (unread-only via NotificationFilter.read / by-type via NotificationFilter.type, passed as FetchOptions.filter) renders ONLY matching notifications and the visible count reflects the filter honestly — no read items leak into an unread-only view.
  • Load-more / pagination (loadMore() following PaginatedResult.hasMore
    • nextCursor) appends OLDER notifications to the list with zero duplicates — every rendered id is unique — and the control stops once hasMore is false.
  • A notification pushed over the realtime transport (NotificationRealtimeAdapter wired via realtime / realtimeEvent, surfaced through onNotification) appears at the TOP of the list and bumps the unread badge WITHOUT any manual reload or refetch.
  • With zero notifications the panel shows a real empty state (not a blank or broken widget), and that empty state is visibly distinct from a failed fetch — a lastError renders a retry affordance, never a silent "empty".
  • A user sees ONLY their own notifications: an id belonging to another user is never listed, and cannot be reached via markAsRead(id) or the click-through actionUrl (no cross-user read/navigation).

Translations

Translation strings are provided by @molecule/app-locales-notification-center.