@ttt-productions/notification-core 0.15.4 → 0.15.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"NotificationHistoryList.d.ts","sourceRoot":"","sources":["../../../src/react/components/NotificationHistoryList.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAA2B,4BAA4B,EAAE,MAAM,gBAAgB,CAAC;AAE5F;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,SAAS,EACT,KAAK,EACL,eAAe,GAChB,EAAE,4BAA4B,+BA2E9B"}
1
+ {"version":3,"file":"NotificationHistoryList.d.ts","sourceRoot":"","sources":["../../../src/react/components/NotificationHistoryList.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAA2B,4BAA4B,EAAE,MAAM,gBAAgB,CAAC;AAE5F;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,SAAS,EACT,KAAK,EACL,eAAe,GAChB,EAAE,4BAA4B,+BAqF9B"}
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useCallback } from 'react';
4
- import { Badge, Button, Separator } from '@ttt-productions/ui-core/react';
4
+ import { Badge, ListPagination, Separator, Spinner } from '@ttt-productions/ui-core/react';
5
5
  import { useNotificationHistory } from '../hooks/useNotificationHistory.js';
6
6
  import { NotificationEmptyState } from './NotificationEmptyState.js';
7
7
  import { formatRelativeTime } from './relative-time.js';
@@ -12,7 +12,7 @@ import { formatRelativeTime } from './relative-time.js';
12
12
  * ArrowRight "go to") is rendered by the consumer via `renderRowAction`.
13
13
  */
14
14
  export function NotificationHistoryList({ config, userId, category, pageSize, staleTime, emptyText, title, renderRowAction, }) {
15
- const { data: notifications, isLoading, hasNextPage, nextPage, } = useNotificationHistory({
15
+ const { data: notifications, isLoading, isFetching, page, hasNextPage, hasPrevPage, nextPage, prevPage, } = useNotificationHistory({
16
16
  config,
17
17
  userId,
18
18
  category,
@@ -20,8 +20,14 @@ export function NotificationHistoryList({ config, userId, category, pageSize, st
20
20
  staleTime,
21
21
  });
22
22
  const getTypeIcon = useCallback((type) => config.types[type]?.icon ?? '🔔', [config]);
23
- return (_jsxs("div", { className: "ntf-list ntf-list-history", children: [title != null && (_jsxs(_Fragment, { children: [_jsx("div", { className: "ntf-list-header ntf-list-header-read-only", children: _jsx("div", { className: "ntf-list-title", children: title }) }), _jsx(Separator, {})] })), _jsx("div", { className: "ntf-list-body", children: isLoading ? (_jsx("div", { className: "ntf-loading", children: "Loading..." })) : !notifications || notifications.length === 0 ? (_jsx(NotificationEmptyState, { text: emptyText })) : (_jsxs(_Fragment, { children: [notifications.map((notification) => {
23
+ return (_jsxs("div", { className: "ntf-list ntf-list-history", children: [title != null && (_jsxs(_Fragment, { children: [_jsx("div", { className: "ntf-list-header ntf-list-header-read-only", children: _jsx("div", { className: "ntf-list-title", children: title }) }), _jsx(Separator, {})] })), _jsx("div", { className: "ntf-list-body", children: isLoading ? (_jsx("div", { className: "ntf-loading", children: _jsx(Spinner, { size: "md", label: "Loading notifications" }) })) : !notifications || notifications.length === 0 ? (_jsx(NotificationEmptyState, { text: emptyText })) : (_jsxs(_Fragment, { children: [notifications.map((notification) => {
24
24
  return (_jsxs("div", { className: "ntf-item ntf-item-archived", children: [_jsx("div", { className: "ntf-item-icon", children: getTypeIcon(notification.type) }), _jsxs("div", { className: "ntf-item-content", children: [_jsx("div", { className: "ntf-item-title", children: notification.title }), _jsx("div", { className: "ntf-item-message", children: notification.message }), _jsx("div", { className: "ntf-item-timestamp", children: formatRelativeTime(notification.archivedAt) })] }), notification.count > 1 && (_jsx("div", { className: "ntf-item-count", children: _jsxs(Badge, { variant: "secondary", children: ["\u00D7", notification.count] }) })), renderRowAction && (_jsx("div", { className: "ntf-item-row-action", children: renderRowAction(notification, {}) }))] }, notification.archiveOccurrenceId));
25
- }), hasNextPage && (_jsx("div", { className: "ntf-list-footer", children: _jsx(Button, { variant: "ghost", size: "sm", onClick: nextPage, children: "Load more" }) }))] })) })] }));
25
+ }), _jsx(ListPagination, { pagination: {
26
+ currentPage: page,
27
+ canPreviousPage: hasPrevPage,
28
+ canNextPage: hasNextPage,
29
+ goToPreviousPage: prevPage,
30
+ goToNextPage: nextPage,
31
+ }, busy: isFetching, className: "ntf-list-footer mt-0" })] })) })] }));
26
32
  }
27
33
  //# sourceMappingURL=NotificationHistoryList.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"NotificationHistoryList.js","sourceRoot":"","sources":["../../../src/react/components/NotificationHistoryList.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AACpC,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAC1E,OAAO,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAGxD;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CAAC,EACtC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,SAAS,EACT,KAAK,EACL,eAAe,GACc;IAC7B,MAAM,EACJ,IAAI,EAAE,aAAa,EACnB,SAAS,EACT,WAAW,EACX,QAAQ,GACT,GAAG,sBAAsB,CAAC;QACzB,MAAM;QACN,MAAM;QACN,QAAQ;QACR,QAAQ;QACR,SAAS;KACV,CAAC,CAAC;IAEH,MAAM,WAAW,GAAG,WAAW,CAC7B,CAAC,IAAY,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,IAAI,IAAI,EAClD,CAAC,MAAM,CAAC,CACT,CAAC;IAEF,OAAO,CACL,eAAK,SAAS,EAAC,2BAA2B,aACvC,KAAK,IAAI,IAAI,IAAI,CAChB,8BACE,cAAK,SAAS,EAAC,2CAA2C,YACxD,cAAK,SAAS,EAAC,gBAAgB,YAAE,KAAK,GAAO,GACzC,EACN,KAAC,SAAS,KAAG,IACZ,CACJ,EACD,cAAK,SAAS,EAAC,eAAe,YAC3B,SAAS,CAAC,CAAC,CAAC,CACX,cAAK,SAAS,EAAC,aAAa,2BAAiB,CAC9C,CAAC,CAAC,CAAC,CAAC,aAAa,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CACjD,KAAC,sBAAsB,IAAC,IAAI,EAAE,SAAS,GAAI,CAC5C,CAAC,CAAC,CAAC,CACF,8BACG,aAAa,CAAC,GAAG,CAAC,CAAC,YAAqC,EAAE,EAAE;4BAC3D,OAAO,CACL,eAEE,SAAS,EAAC,4BAA4B,aAEtC,cAAK,SAAS,EAAC,eAAe,YAAE,WAAW,CAAC,YAAY,CAAC,IAAI,CAAC,GAAO,EACrE,eAAK,SAAS,EAAC,kBAAkB,aAC/B,cAAK,SAAS,EAAC,gBAAgB,YAAE,YAAY,CAAC,KAAK,GAAO,EAC1D,cAAK,SAAS,EAAC,kBAAkB,YAAE,YAAY,CAAC,OAAO,GAAO,EAC9D,cAAK,SAAS,EAAC,oBAAoB,YAChC,kBAAkB,CAAC,YAAY,CAAC,UAAU,CAAC,GACxC,IACF,EACL,YAAY,CAAC,KAAK,GAAG,CAAC,IAAI,CACzB,cAAK,SAAS,EAAC,gBAAgB,YAC7B,MAAC,KAAK,IAAC,OAAO,EAAC,WAAW,uBAAG,YAAY,CAAC,KAAK,IAAS,GACpD,CACP,EACA,eAAe,IAAI,CAClB,cAAK,SAAS,EAAC,qBAAqB,YACjC,eAAe,CAAC,YAAY,EAAE,EAAE,CAAC,GAC9B,CACP,KApBI,YAAY,CAAC,mBAAmB,CAqBjC,CACP,CAAC;wBACJ,CAAC,CAAC,EACD,WAAW,IAAI,CACd,cAAK,SAAS,EAAC,iBAAiB,YAC9B,KAAC,MAAM,IAAC,OAAO,EAAC,OAAO,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAE,QAAQ,0BAE1C,GACL,CACP,IACA,CACJ,GACG,IACF,CACP,CAAC;AACJ,CAAC","sourcesContent":["'use client';\n\nimport { useCallback } from 'react';\nimport { Badge, Button, Separator } from '@ttt-productions/ui-core/react';\nimport { useNotificationHistory } from '../hooks/useNotificationHistory.js';\nimport { NotificationEmptyState } from './NotificationEmptyState.js';\nimport { formatRelativeTime } from './relative-time.js';\nimport type { NotificationHistoryItem, NotificationHistoryListProps } from '../../types.js';\n\n/**\n * Read-only, paginated list of ARCHIVED notifications (the history tier). Rows are\n * inert (DJ ruling 2026-07-07) and read-only — archive is one-way, there is no\n * re-archive, so the row exposes no `archive` action. Any per-row control (e.g. an\n * ArrowRight \"go to\") is rendered by the consumer via `renderRowAction`.\n */\nexport function NotificationHistoryList({\n config,\n userId,\n category,\n pageSize,\n staleTime,\n emptyText,\n title,\n renderRowAction,\n}: NotificationHistoryListProps) {\n const {\n data: notifications,\n isLoading,\n hasNextPage,\n nextPage,\n } = useNotificationHistory({\n config,\n userId,\n category,\n pageSize,\n staleTime,\n });\n\n const getTypeIcon = useCallback(\n (type: string) => config.types[type]?.icon ?? '🔔',\n [config],\n );\n\n return (\n <div className=\"ntf-list ntf-list-history\">\n {title != null && (\n <>\n <div className=\"ntf-list-header ntf-list-header-read-only\">\n <div className=\"ntf-list-title\">{title}</div>\n </div>\n <Separator />\n </>\n )}\n <div className=\"ntf-list-body\">\n {isLoading ? (\n <div className=\"ntf-loading\">Loading...</div>\n ) : !notifications || notifications.length === 0 ? (\n <NotificationEmptyState text={emptyText} />\n ) : (\n <>\n {notifications.map((notification: NotificationHistoryItem) => {\n return (\n <div\n key={notification.archiveOccurrenceId}\n className=\"ntf-item ntf-item-archived\"\n >\n <div className=\"ntf-item-icon\">{getTypeIcon(notification.type)}</div>\n <div className=\"ntf-item-content\">\n <div className=\"ntf-item-title\">{notification.title}</div>\n <div className=\"ntf-item-message\">{notification.message}</div>\n <div className=\"ntf-item-timestamp\">\n {formatRelativeTime(notification.archivedAt)}\n </div>\n </div>\n {notification.count > 1 && (\n <div className=\"ntf-item-count\">\n <Badge variant=\"secondary\">×{notification.count}</Badge>\n </div>\n )}\n {renderRowAction && (\n <div className=\"ntf-item-row-action\">\n {renderRowAction(notification, {})}\n </div>\n )}\n </div>\n );\n })}\n {hasNextPage && (\n <div className=\"ntf-list-footer\">\n <Button variant=\"ghost\" size=\"sm\" onClick={nextPage}>\n Load more\n </Button>\n </div>\n )}\n </>\n )}\n </div>\n </div>\n );\n}\n"]}
1
+ {"version":3,"file":"NotificationHistoryList.js","sourceRoot":"","sources":["../../../src/react/components/NotificationHistoryList.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useCallback } from 'react';\nimport { Badge, ListPagination, Separator, Spinner } from '@ttt-productions/ui-core/react';\nimport { useNotificationHistory } from '../hooks/useNotificationHistory.js';\nimport { NotificationEmptyState } from './NotificationEmptyState.js';\nimport { formatRelativeTime } from './relative-time.js';\nimport type { NotificationHistoryItem, NotificationHistoryListProps } from '../../types.js';\n\n/**\n * Read-only, paginated list of ARCHIVED notifications (the history tier). Rows are\n * inert (DJ ruling 2026-07-07) and read-only — archive is one-way, there is no\n * re-archive, so the row exposes no `archive` action. Any per-row control (e.g. an\n * ArrowRight \"go to\") is rendered by the consumer via `renderRowAction`.\n */\nexport function NotificationHistoryList({\n config,\n userId,\n category,\n pageSize,\n staleTime,\n emptyText,\n title,\n renderRowAction,\n}: NotificationHistoryListProps) {\n const {\n data: notifications,\n isLoading,\n isFetching,\n page,\n hasNextPage,\n hasPrevPage,\n nextPage,\n prevPage,\n } = useNotificationHistory({\n config,\n userId,\n category,\n pageSize,\n staleTime,\n });\n\n const getTypeIcon = useCallback(\n (type: string) => config.types[type]?.icon ?? '🔔',\n [config],\n );\n\n return (\n <div className=\"ntf-list ntf-list-history\">\n {title != null && (\n <>\n <div className=\"ntf-list-header ntf-list-header-read-only\">\n <div className=\"ntf-list-title\">{title}</div>\n </div>\n <Separator />\n </>\n )}\n <div className=\"ntf-list-body\">\n {isLoading ? (\n <div className=\"ntf-loading\">\n <Spinner size=\"md\" label=\"Loading notifications\" />\n </div>\n ) : !notifications || notifications.length === 0 ? (\n <NotificationEmptyState text={emptyText} />\n ) : (\n <>\n {notifications.map((notification: NotificationHistoryItem) => {\n return (\n <div\n key={notification.archiveOccurrenceId}\n className=\"ntf-item ntf-item-archived\"\n >\n <div className=\"ntf-item-icon\">{getTypeIcon(notification.type)}</div>\n <div className=\"ntf-item-content\">\n <div className=\"ntf-item-title\">{notification.title}</div>\n <div className=\"ntf-item-message\">{notification.message}</div>\n <div className=\"ntf-item-timestamp\">\n {formatRelativeTime(notification.archivedAt)}\n </div>\n </div>\n {notification.count > 1 && (\n <div className=\"ntf-item-count\">\n <Badge variant=\"secondary\">×{notification.count}</Badge>\n </div>\n )}\n {renderRowAction && (\n <div className=\"ntf-item-row-action\">\n {renderRowAction(notification, {})}\n </div>\n )}\n </div>\n );\n })}\n <ListPagination\n pagination={{\n currentPage: page,\n canPreviousPage: hasPrevPage,\n canNextPage: hasNextPage,\n goToPreviousPage: prevPage,\n goToNextPage: nextPage,\n }}\n busy={isFetching}\n className=\"ntf-list-footer mt-0\"\n />\n </>\n )}\n </div>\n </div>\n );\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"NotificationList.d.ts","sourceRoot":"","sources":["../../../src/react/components/NotificationList.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAmB,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAE7E;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,MAAM,EACN,MAAM,EACN,QAAQ,EACR,SAAS,EACT,mBAAmB,EACnB,qBAAqB,EACrB,KAAK,EACL,UAAU,EACV,eAAe,EACf,SAAS,EACT,eAAe,GAChB,EAAE,qBAAqB,+BA6KvB"}
1
+ {"version":3,"file":"NotificationList.d.ts","sourceRoot":"","sources":["../../../src/react/components/NotificationList.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAmB,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAE7E;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,MAAM,EACN,MAAM,EACN,QAAQ,EACR,SAAS,EACT,mBAAmB,EACnB,qBAAqB,EACrB,KAAK,EACL,UAAU,EACV,eAAe,EACf,SAAS,EACT,eAAe,GAChB,EAAE,qBAAqB,+BAuLvB"}
@@ -1,8 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useCallback, useEffect, useState } from 'react';
4
- import { Loader2 } from 'lucide-react';
5
- import { Badge, Button, Separator } from '@ttt-productions/ui-core/react';
4
+ import { Badge, Button, ListPagination, Separator, Spinner } from '@ttt-productions/ui-core/react';
6
5
  import { useActiveNotifications } from '../hooks/useActiveNotifications.js';
7
6
  import { useArchiveNotification } from '../hooks/useArchiveNotification.js';
8
7
  import { useArchiveAllNotifications } from '../hooks/useArchiveAllNotifications.js';
@@ -12,7 +11,7 @@ import { formatRelativeTime } from './relative-time.js';
12
11
  * Scrollable list of active notifications with click-to-archive and clear-all.
13
12
  */
14
13
  export function NotificationList({ config, userId, category, archiveFn, enqueueArchiveAllFn, getArchiveAllStatusFn, title, onClearAll, refetchInterval, emptyText, renderRowAction, }) {
15
- const { data: notifications, isLoading, hasNextPage, nextPage, } = useActiveNotifications({
14
+ const { data: notifications, isLoading, isFetching, page, hasNextPage, hasPrevPage, nextPage, prevPage, } = useActiveNotifications({
16
15
  config,
17
16
  userId,
18
17
  category,
@@ -97,9 +96,15 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
97
96
  }, [config]);
98
97
  const hasNotifications = !!notifications && notifications.length > 0;
99
98
  const isClearAllPending = archiveAllMutation.isPending || pendingClearAllIds.size > 0;
100
- return (_jsxs("div", { className: "ntf-list", children: [_jsxs("div", { className: "ntf-list-header", children: [title != null && _jsx("div", { className: "ntf-list-title", children: title }), _jsxs(Button, { className: "ntf-list-clear-all", variant: "ghost", size: "sm", onClick: handleClearAll, disabled: !hasNotifications || isClearAllPending, children: [isClearAllPending && _jsx(Loader2, { className: "mr-2 spinner-xs", "aria-hidden": "true" }), isClearAllPending ? 'Clearing...' : 'Clear All'] }), clearIncomplete && !isClearAllPending && (_jsx("div", { className: "ntf-list-clear-error", role: "status", children: "Some notifications remain \u2014 try again." }))] }), _jsx(Separator, {}), _jsx("div", { className: "ntf-list-body", children: isLoading ? (_jsx("div", { className: "ntf-loading", children: "Loading..." })) : !notifications || notifications.length === 0 ? (_jsx(NotificationEmptyState, { text: emptyText })) : (_jsxs(_Fragment, { children: [notifications.map((notification) => (_jsxs("div", { className: "ntf-item", children: [_jsx("div", { className: "ntf-item-icon", children: getTypeIcon(notification.type) }), _jsxs("div", { className: "ntf-item-content", children: [_jsx("div", { className: "ntf-item-title", children: notification.title }), _jsx("div", { className: "ntf-item-message", children: notification.message }), _jsx("div", { className: "ntf-item-timestamp", children: formatRelativeTime(notification.updatedAt) })] }), notification.count > 1 && (_jsx("div", { className: "ntf-item-count", children: _jsxs(Badge, { variant: "secondary", children: ["\u00D7", notification.count] }) })), renderRowAction && (_jsx("div", { className: "ntf-item-row-action", children: renderRowAction(notification, {
99
+ return (_jsxs("div", { className: "ntf-list", children: [_jsxs("div", { className: "ntf-list-header", children: [title != null && _jsx("div", { className: "ntf-list-title", children: title }), _jsx(Button, { className: "ntf-list-clear-all", variant: "ghost", size: "sm", onClick: handleClearAll, disabled: !hasNotifications, pending: isClearAllPending, children: isClearAllPending ? 'Clearing...' : 'Clear All' }), clearIncomplete && !isClearAllPending && (_jsx("div", { className: "ntf-list-clear-error", role: "status", children: "Some notifications remain \u2014 try again." }))] }), _jsx(Separator, {}), _jsx("div", { className: "ntf-list-body", children: isLoading ? (_jsx("div", { className: "ntf-loading", children: _jsx(Spinner, { size: "md", label: "Loading notifications" }) })) : !notifications || notifications.length === 0 ? (_jsx(NotificationEmptyState, { text: emptyText })) : (_jsxs(_Fragment, { children: [notifications.map((notification) => (_jsxs("div", { className: "ntf-item", children: [_jsx("div", { className: "ntf-item-icon", children: getTypeIcon(notification.type) }), _jsxs("div", { className: "ntf-item-content", children: [_jsx("div", { className: "ntf-item-title", children: notification.title }), _jsx("div", { className: "ntf-item-message", children: notification.message }), _jsx("div", { className: "ntf-item-timestamp", children: formatRelativeTime(notification.updatedAt) })] }), notification.count > 1 && (_jsx("div", { className: "ntf-item-count", children: _jsxs(Badge, { variant: "secondary", children: ["\u00D7", notification.count] }) })), renderRowAction && (_jsx("div", { className: "ntf-item-row-action", children: renderRowAction(notification, {
101
100
  archive: () => archiveOne(notification.id),
102
101
  isArchivePending: pendingClearAllIds.has(notification.id) || pendingArchiveIds.has(notification.id),
103
- }) }))] }, notification.id))), hasNextPage && (_jsx("div", { className: "ntf-list-footer", children: _jsx(Button, { variant: "ghost", size: "sm", onClick: nextPage, children: "Load more" }) }))] })) })] }));
102
+ }) }))] }, notification.id))), _jsx(ListPagination, { pagination: {
103
+ currentPage: page,
104
+ canPreviousPage: hasPrevPage,
105
+ canNextPage: hasNextPage,
106
+ goToPreviousPage: prevPage,
107
+ goToNextPage: nextPage,
108
+ }, busy: isFetching, className: "ntf-list-footer mt-0" })] })) })] }));
104
109
  }
105
110
  //# sourceMappingURL=NotificationList.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"NotificationList.js","sourceRoot":"","sources":["../../../src/react/components/NotificationList.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACzD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAC1E,OAAO,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,EAAE,0BAA0B,EAAE,MAAM,wCAAwC,CAAC;AACpF,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAGxD;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAC,EAC/B,MAAM,EACN,MAAM,EACN,QAAQ,EACR,SAAS,EACT,mBAAmB,EACnB,qBAAqB,EACrB,KAAK,EACL,UAAU,EACV,eAAe,EACf,SAAS,EACT,eAAe,GACO;IACtB,MAAM,EACJ,IAAI,EAAE,aAAa,EACnB,SAAS,EACT,WAAW,EACX,QAAQ,GACT,GAAG,sBAAsB,CAAC;QACzB,MAAM;QACN,MAAM;QACN,QAAQ;QACR,eAAe;KAChB,CAAC,CAAC;IAEH,MAAM,eAAe,GAAG,sBAAsB,CAAC;QAC7C,MAAM;QACN,QAAQ;QACR,SAAS;KACV,CAAC,CAAC;IAEH,MAAM,kBAAkB,GAAG,0BAA0B,CAAC;QACpD,MAAM;QACN,QAAQ;QACR,mBAAmB;QACnB,qBAAqB;KACtB,CAAC,CAAC;IAEH,iGAAiG;IACjG,gGAAgG;IAChG,kGAAkG;IAClG,gGAAgG;IAChG,2BAA2B;IAC3B,MAAM,CAAC,eAAe,EAAE,kBAAkB,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC9D,MAAM,CAAC,iBAAiB,EAAE,oBAAoB,CAAC,GAAG,QAAQ,CACxD,GAAG,EAAE,CAAC,IAAI,GAAG,EAAE,CAChB,CAAC;IACF,MAAM,CAAC,kBAAkB,EAAE,qBAAqB,CAAC,GAAG,QAAQ,CAC1D,GAAG,EAAE,CAAC,IAAI,GAAG,EAAE,CAChB,CAAC;IAEF,2EAA2E;IAC3E,6EAA6E;IAC7E,mDAAmD;IACnD,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,aAAa;YAAE,OAAO;QAC3B,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,YAAY,EAAE,EAAE,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,CAAC;QAChF,MAAM,UAAU,GAAG,CAAC,OAA4B,EAAE,EAAE;YAClD,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YACrE,OAAO,IAAI,CAAC,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;QACrD,CAAC,CAAC;QACF,oBAAoB,CAAC,UAAU,CAAC,CAAC;QACjC,qBAAqB,CAAC,UAAU,CAAC,CAAC;IACpC,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC;IAEpB,MAAM,UAAU,GAAG,WAAW,CAAC,KAAK,EAAE,cAAsB,EAAE,EAAE;QAC9D,oBAAoB,CAAC,CAAC,OAAO,EAAE,EAAE;YAC/B,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;YAC9B,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;YACzB,OAAO,IAAI,CAAC;QACd,CAAC,CAAC,CAAC;QACH,IAAI,CAAC;YACH,MAAM,eAAe,CAAC,WAAW,CAAC,cAAc,CAAC,CAAC;QACpD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,oBAAoB,CAAC,CAAC,OAAO,EAAE,EAAE;gBAC/B,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;gBAC9B,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;gBAC5B,OAAO,IAAI,CAAC;YACd,CAAC,CAAC,CAAC;YACH,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC,EAAE,CAAC,eAAe,CAAC,CAAC,CAAC;IAEtB,MAAM,cAAc,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QAC5C,kBAAkB,CAAC,KAAK,CAAC,CAAC;QAC1B,qBAAqB,CAAC,IAAI,GAAG,CAAC,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,YAAY,EAAE,EAAE,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC7F,IAAI,MAAM,CAAC;QACX,IAAI,CAAC;YACH,MAAM,GAAG,MAAM,kBAAkB,CAAC,WAAW,EAAE,CAAC;QAClD,CAAC;QAAC,MAAM,CAAC;YACP,2FAA2F;YAC3F,qBAAqB,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC;YACjC,kBAAkB,CAAC,IAAI,CAAC,CAAC;YACzB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;YACrB,oFAAoF;YACpF,oCAAoC;YACpC,qBAAqB,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC;YACjC,kBAAkB,CAAC,IAAI,CAAC,CAAC;YACzB,OAAO;QACT,CAAC;QACD,UAAU,EAAE,EAAE,CAAC;IACjB,CAAC,EAAE,CAAC,kBAAkB,EAAE,aAAa,EAAE,UAAU,CAAC,CAAC,CAAC;IAEpD,MAAM,WAAW,GAAG,WAAW,CAC7B,CAAC,IAAY,EAAE,EAAE;QACf,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACtC,OAAO,UAAU,EAAE,IAAI,IAAI,IAAI,CAAC;IAClC,CAAC,EACD,CAAC,MAAM,CAAC,CACT,CAAC;IAEF,MAAM,gBAAgB,GAAG,CAAC,CAAC,aAAa,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC,CAAC;IACrE,MAAM,iBAAiB,GAAG,kBAAkB,CAAC,SAAS,IAAI,kBAAkB,CAAC,IAAI,GAAG,CAAC,CAAC;IAEtF,OAAO,CACL,eAAK,SAAS,EAAC,UAAU,aACvB,eAAK,SAAS,EAAC,iBAAiB,aAC7B,KAAK,IAAI,IAAI,IAAI,cAAK,SAAS,EAAC,gBAAgB,YAAE,KAAK,GAAO,EAC/D,MAAC,MAAM,IACL,SAAS,EAAC,oBAAoB,EAC9B,OAAO,EAAC,OAAO,EACf,IAAI,EAAC,IAAI,EACT,OAAO,EAAE,cAAc,EACvB,QAAQ,EAAE,CAAC,gBAAgB,IAAI,iBAAiB,aAE/C,iBAAiB,IAAI,KAAC,OAAO,IAAC,SAAS,EAAC,iBAAiB,iBAAa,MAAM,GAAG,EAC/E,iBAAiB,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,WAAW,IACzC,EACR,eAAe,IAAI,CAAC,iBAAiB,IAAI,CACxC,cAAK,SAAS,EAAC,sBAAsB,EAAC,IAAI,EAAC,QAAQ,4DAE7C,CACP,IACG,EACN,KAAC,SAAS,KAAG,EAEb,cAAK,SAAS,EAAC,eAAe,YAC7B,SAAS,CAAC,CAAC,CAAC,CACX,cAAK,SAAS,EAAC,aAAa,2BAAiB,CAC9C,CAAC,CAAC,CAAC,CAAC,aAAa,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CACjD,KAAC,sBAAsB,IAAC,IAAI,EAAE,SAAS,GAAI,CAC5C,CAAC,CAAC,CAAC,CACF,8BACG,aAAa,CAAC,GAAG,CAAC,CAAC,YAA6B,EAAE,EAAE,CAAC,CACpD,eAA2B,SAAS,EAAC,UAAU,aAC7C,cAAK,SAAS,EAAC,eAAe,YAC3B,WAAW,CAAC,YAAY,CAAC,IAAI,CAAC,GAC3B,EACN,eAAK,SAAS,EAAC,kBAAkB,aAC/B,cAAK,SAAS,EAAC,gBAAgB,YAAE,YAAY,CAAC,KAAK,GAAO,EAC1D,cAAK,SAAS,EAAC,kBAAkB,YAAE,YAAY,CAAC,OAAO,GAAO,EAC9D,cAAK,SAAS,EAAC,oBAAoB,YAChC,kBAAkB,CAAC,YAAY,CAAC,SAAS,CAAC,GACvC,IACF,EACL,YAAY,CAAC,KAAK,GAAG,CAAC,IAAI,CACzB,cAAK,SAAS,EAAC,gBAAgB,YAC7B,MAAC,KAAK,IAAC,OAAO,EAAC,WAAW,uBAAG,YAAY,CAAC,KAAK,IAAS,GACpD,CACP,EACA,eAAe,IAAI,CAClB,cAAK,SAAS,EAAC,qBAAqB,YACjC,eAAe,CAAC,YAAY,EAAE;wCAC7B,OAAO,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;wCAC1C,gBAAgB,EACd,kBAAkB,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC,IAAI,iBAAiB,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC;qCACpF,CAAC,GACE,CACP,KAxBO,YAAY,CAAC,EAAE,CAyBnB,CACP,CAAC,EACD,WAAW,IAAI,CACd,cAAK,SAAS,EAAC,iBAAiB,YAC9B,KAAC,MAAM,IAAC,OAAO,EAAC,OAAO,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAE,QAAQ,0BAE1C,GACL,CACP,IACA,CACJ,GACK,IACF,CACP,CAAC;AACJ,CAAC","sourcesContent":["'use client';\n\nimport { useCallback, useEffect, useState } from 'react';\nimport { Loader2 } from 'lucide-react';\nimport { Badge, Button, Separator } from '@ttt-productions/ui-core/react';\nimport { useActiveNotifications } from '../hooks/useActiveNotifications.js';\nimport { useArchiveNotification } from '../hooks/useArchiveNotification.js';\nimport { useArchiveAllNotifications } from '../hooks/useArchiveAllNotifications.js';\nimport { NotificationEmptyState } from './NotificationEmptyState.js';\nimport { formatRelativeTime } from './relative-time.js';\nimport type { NotificationDoc, NotificationListProps } from '../../types.js';\n\n/**\n * Scrollable list of active notifications with click-to-archive and clear-all.\n */\nexport function NotificationList({\n config,\n userId,\n category,\n archiveFn,\n enqueueArchiveAllFn,\n getArchiveAllStatusFn,\n title,\n onClearAll,\n refetchInterval,\n emptyText,\n renderRowAction,\n}: NotificationListProps) {\n const {\n data: notifications,\n isLoading,\n hasNextPage,\n nextPage,\n } = useActiveNotifications({\n config,\n userId,\n category,\n refetchInterval,\n });\n\n const archiveMutation = useArchiveNotification({\n userId,\n category,\n archiveFn,\n });\n\n const archiveAllMutation = useArchiveAllNotifications({\n userId,\n category,\n enqueueArchiveAllFn,\n getArchiveAllStatusFn,\n });\n\n // Surface a failed/incomplete clear instead of swallowing it. `onClearAll` (and the tray-closing\n // side effects the app wires to it) fires ONLY when the server job fully drained the category —\n // otherwise the user keeps the \"notifications remain — try again\" affordance. The poller resolves\n // with an explicit terminal result (`complete` | `incomplete` | `failed`) and only rejects on a\n // hard enqueue/poll throw.\n const [clearIncomplete, setClearIncomplete] = useState(false);\n const [pendingArchiveIds, setPendingArchiveIds] = useState<ReadonlySet<string>>(\n () => new Set(),\n );\n const [pendingClearAllIds, setPendingClearAllIds] = useState<ReadonlySet<string>>(\n () => new Set(),\n );\n\n // Successful archive requests stay visibly pending until the authoritative\n // active-notification query removes each row. A callable response alone does\n // not prove the listener/cache has reconciled yet.\n useEffect(() => {\n if (!notifications) return;\n const activeIds = new Set(notifications.map((notification) => notification.id));\n const keepActive = (current: ReadonlySet<string>) => {\n const next = new Set([...current].filter((id) => activeIds.has(id)));\n return next.size === current.size ? current : next;\n };\n setPendingArchiveIds(keepActive);\n setPendingClearAllIds(keepActive);\n }, [notifications]);\n\n const archiveOne = useCallback(async (notificationId: string) => {\n setPendingArchiveIds((current) => {\n const next = new Set(current);\n next.add(notificationId);\n return next;\n });\n try {\n await archiveMutation.mutateAsync(notificationId);\n } catch (error) {\n setPendingArchiveIds((current) => {\n const next = new Set(current);\n next.delete(notificationId);\n return next;\n });\n throw error;\n }\n }, [archiveMutation]);\n\n const handleClearAll = useCallback(async () => {\n setClearIncomplete(false);\n setPendingClearAllIds(new Set((notifications ?? []).map((notification) => notification.id)));\n let result;\n try {\n result = await archiveAllMutation.mutateAsync();\n } catch {\n // Hard failure (enqueue/poll threw): do not fire onClearAll; surface the retry affordance.\n setPendingClearAllIds(new Set());\n setClearIncomplete(true);\n return;\n }\n if (!result.complete) {\n // Job terminated without fully draining the category (incomplete/failed) — keep the\n // notifications and prompt a retry.\n setPendingClearAllIds(new Set());\n setClearIncomplete(true);\n return;\n }\n onClearAll?.();\n }, [archiveAllMutation, notifications, onClearAll]);\n\n const getTypeIcon = useCallback(\n (type: string) => {\n const typeConfig = config.types[type];\n return typeConfig?.icon ?? '🔔';\n },\n [config],\n );\n\n const hasNotifications = !!notifications && notifications.length > 0;\n const isClearAllPending = archiveAllMutation.isPending || pendingClearAllIds.size > 0;\n\n return (\n <div className=\"ntf-list\">\n <div className=\"ntf-list-header\">\n {title != null && <div className=\"ntf-list-title\">{title}</div>}\n <Button\n className=\"ntf-list-clear-all\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleClearAll}\n disabled={!hasNotifications || isClearAllPending}\n >\n {isClearAllPending && <Loader2 className=\"mr-2 spinner-xs\" aria-hidden=\"true\" />}\n {isClearAllPending ? 'Clearing...' : 'Clear All'}\n </Button>\n {clearIncomplete && !isClearAllPending && (\n <div className=\"ntf-list-clear-error\" role=\"status\">\n Some notifications remain — try again.\n </div>\n )}\n </div>\n <Separator />\n\n <div className=\"ntf-list-body\">\n {isLoading ? (\n <div className=\"ntf-loading\">Loading...</div>\n ) : !notifications || notifications.length === 0 ? (\n <NotificationEmptyState text={emptyText} />\n ) : (\n <>\n {notifications.map((notification: NotificationDoc) => (\n <div key={notification.id} className=\"ntf-item\">\n <div className=\"ntf-item-icon\">\n {getTypeIcon(notification.type)}\n </div>\n <div className=\"ntf-item-content\">\n <div className=\"ntf-item-title\">{notification.title}</div>\n <div className=\"ntf-item-message\">{notification.message}</div>\n <div className=\"ntf-item-timestamp\">\n {formatRelativeTime(notification.updatedAt)}\n </div>\n </div>\n {notification.count > 1 && (\n <div className=\"ntf-item-count\">\n <Badge variant=\"secondary\">×{notification.count}</Badge>\n </div>\n )}\n {renderRowAction && (\n <div className=\"ntf-item-row-action\">\n {renderRowAction(notification, {\n archive: () => archiveOne(notification.id),\n isArchivePending:\n pendingClearAllIds.has(notification.id) || pendingArchiveIds.has(notification.id),\n })}\n </div>\n )}\n </div>\n ))}\n {hasNextPage && (\n <div className=\"ntf-list-footer\">\n <Button variant=\"ghost\" size=\"sm\" onClick={nextPage}>\n Load more\n </Button>\n </div>\n )}\n </>\n )}\n </div>\n </div>\n );\n}\n"]}
1
+ {"version":3,"file":"NotificationList.js","sourceRoot":"","sources":["../../../src/react/components/NotificationList.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useCallback, useEffect, useState } from 'react';\nimport { Badge, Button, ListPagination, Separator, Spinner } from '@ttt-productions/ui-core/react';\nimport { useActiveNotifications } from '../hooks/useActiveNotifications.js';\nimport { useArchiveNotification } from '../hooks/useArchiveNotification.js';\nimport { useArchiveAllNotifications } from '../hooks/useArchiveAllNotifications.js';\nimport { NotificationEmptyState } from './NotificationEmptyState.js';\nimport { formatRelativeTime } from './relative-time.js';\nimport type { NotificationDoc, NotificationListProps } from '../../types.js';\n\n/**\n * Scrollable list of active notifications with click-to-archive and clear-all.\n */\nexport function NotificationList({\n config,\n userId,\n category,\n archiveFn,\n enqueueArchiveAllFn,\n getArchiveAllStatusFn,\n title,\n onClearAll,\n refetchInterval,\n emptyText,\n renderRowAction,\n}: NotificationListProps) {\n const {\n data: notifications,\n isLoading,\n isFetching,\n page,\n hasNextPage,\n hasPrevPage,\n nextPage,\n prevPage,\n } = useActiveNotifications({\n config,\n userId,\n category,\n refetchInterval,\n });\n\n const archiveMutation = useArchiveNotification({\n userId,\n category,\n archiveFn,\n });\n\n const archiveAllMutation = useArchiveAllNotifications({\n userId,\n category,\n enqueueArchiveAllFn,\n getArchiveAllStatusFn,\n });\n\n // Surface a failed/incomplete clear instead of swallowing it. `onClearAll` (and the tray-closing\n // side effects the app wires to it) fires ONLY when the server job fully drained the category —\n // otherwise the user keeps the \"notifications remain — try again\" affordance. The poller resolves\n // with an explicit terminal result (`complete` | `incomplete` | `failed`) and only rejects on a\n // hard enqueue/poll throw.\n const [clearIncomplete, setClearIncomplete] = useState(false);\n const [pendingArchiveIds, setPendingArchiveIds] = useState<ReadonlySet<string>>(\n () => new Set(),\n );\n const [pendingClearAllIds, setPendingClearAllIds] = useState<ReadonlySet<string>>(\n () => new Set(),\n );\n\n // Successful archive requests stay visibly pending until the authoritative\n // active-notification query removes each row. A callable response alone does\n // not prove the listener/cache has reconciled yet.\n useEffect(() => {\n if (!notifications) return;\n const activeIds = new Set(notifications.map((notification) => notification.id));\n const keepActive = (current: ReadonlySet<string>) => {\n const next = new Set([...current].filter((id) => activeIds.has(id)));\n return next.size === current.size ? current : next;\n };\n setPendingArchiveIds(keepActive);\n setPendingClearAllIds(keepActive);\n }, [notifications]);\n\n const archiveOne = useCallback(async (notificationId: string) => {\n setPendingArchiveIds((current) => {\n const next = new Set(current);\n next.add(notificationId);\n return next;\n });\n try {\n await archiveMutation.mutateAsync(notificationId);\n } catch (error) {\n setPendingArchiveIds((current) => {\n const next = new Set(current);\n next.delete(notificationId);\n return next;\n });\n throw error;\n }\n }, [archiveMutation]);\n\n const handleClearAll = useCallback(async () => {\n setClearIncomplete(false);\n setPendingClearAllIds(new Set((notifications ?? []).map((notification) => notification.id)));\n let result;\n try {\n result = await archiveAllMutation.mutateAsync();\n } catch {\n // Hard failure (enqueue/poll threw): do not fire onClearAll; surface the retry affordance.\n setPendingClearAllIds(new Set());\n setClearIncomplete(true);\n return;\n }\n if (!result.complete) {\n // Job terminated without fully draining the category (incomplete/failed) — keep the\n // notifications and prompt a retry.\n setPendingClearAllIds(new Set());\n setClearIncomplete(true);\n return;\n }\n onClearAll?.();\n }, [archiveAllMutation, notifications, onClearAll]);\n\n const getTypeIcon = useCallback(\n (type: string) => {\n const typeConfig = config.types[type];\n return typeConfig?.icon ?? '🔔';\n },\n [config],\n );\n\n const hasNotifications = !!notifications && notifications.length > 0;\n const isClearAllPending = archiveAllMutation.isPending || pendingClearAllIds.size > 0;\n\n return (\n <div className=\"ntf-list\">\n <div className=\"ntf-list-header\">\n {title != null && <div className=\"ntf-list-title\">{title}</div>}\n <Button\n className=\"ntf-list-clear-all\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleClearAll}\n disabled={!hasNotifications}\n pending={isClearAllPending}\n >\n {isClearAllPending ? 'Clearing...' : 'Clear All'}\n </Button>\n {clearIncomplete && !isClearAllPending && (\n <div className=\"ntf-list-clear-error\" role=\"status\">\n Some notifications remain — try again.\n </div>\n )}\n </div>\n <Separator />\n\n <div className=\"ntf-list-body\">\n {isLoading ? (\n <div className=\"ntf-loading\">\n <Spinner size=\"md\" label=\"Loading notifications\" />\n </div>\n ) : !notifications || notifications.length === 0 ? (\n <NotificationEmptyState text={emptyText} />\n ) : (\n <>\n {notifications.map((notification: NotificationDoc) => (\n <div key={notification.id} className=\"ntf-item\">\n <div className=\"ntf-item-icon\">\n {getTypeIcon(notification.type)}\n </div>\n <div className=\"ntf-item-content\">\n <div className=\"ntf-item-title\">{notification.title}</div>\n <div className=\"ntf-item-message\">{notification.message}</div>\n <div className=\"ntf-item-timestamp\">\n {formatRelativeTime(notification.updatedAt)}\n </div>\n </div>\n {notification.count > 1 && (\n <div className=\"ntf-item-count\">\n <Badge variant=\"secondary\">×{notification.count}</Badge>\n </div>\n )}\n {renderRowAction && (\n <div className=\"ntf-item-row-action\">\n {renderRowAction(notification, {\n archive: () => archiveOne(notification.id),\n isArchivePending:\n pendingClearAllIds.has(notification.id) || pendingArchiveIds.has(notification.id),\n })}\n </div>\n )}\n </div>\n ))}\n <ListPagination\n pagination={{\n currentPage: page,\n canPreviousPage: hasPrevPage,\n canNextPage: hasNextPage,\n goToPreviousPage: prevPage,\n goToNextPage: nextPage,\n }}\n busy={isFetching}\n className=\"ntf-list-footer mt-0\"\n />\n </>\n )}\n </div>\n </div>\n );\n}\n"]}
@@ -8,7 +8,8 @@
8
8
  * selects rows per `materializationClass`. The app passes fully-formed
9
9
  * `DeliveryRowInput`s; this module never invents domain ids.
10
10
  *
11
- * Key invariants (frozen in NOTIFICATIONS_REDESIGN):
11
+ * Key invariants (FROZEN — this module is their single source; the consuming app's
12
+ * notification design doc explains the product rationale):
12
13
  * - Enqueue = create-if-absent. `ALREADY_EXISTS` is a per-row duplicate no-op,
13
14
  * never a failure — there is no marker-without-payload state, so the
14
15
  * "marker created, payload lost" crash window cannot exist.
@@ -1 +1 @@
1
- {"version":3,"file":"delivery-ledger.d.ts","sourceRoot":"","sources":["../../src/server/delivery-ledger.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"delivery-ledger.d.ts","sourceRoot":"","sources":["../../src/server/delivery-ledger.ts"],"names":[],"mappings":"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"}
@@ -8,7 +8,8 @@
8
8
  * selects rows per `materializationClass`. The app passes fully-formed
9
9
  * `DeliveryRowInput`s; this module never invents domain ids.
10
10
  *
11
- * Key invariants (frozen in NOTIFICATIONS_REDESIGN):
11
+ * Key invariants (FROZEN — this module is their single source; the consuming app's
12
+ * notification design doc explains the product rationale):
12
13
  * - Enqueue = create-if-absent. `ALREADY_EXISTS` is a per-row duplicate no-op,
13
14
  * never a failure — there is no marker-without-payload state, so the
14
15
  * "marker created, payload lost" crash window cannot exist.
@@ -1 +1 @@
1
- {"version":3,"file":"delivery-ledger.js","sourceRoot":"","sources":["../../src/server/delivery-ledger.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Generic notification delivery ledger (notification redesign — P1).\n *\n * One Firestore doc per (recipient|shared, type, occurrence) is BOTH the queue\n * row AND the idempotency ledger. This package owns the GENERIC mechanism;\n * the consuming app owns the concrete collection name, the deterministic\n * `deliveryId`/`eventId`/`aggregationKey` construction, and the worker that\n * selects rows per `materializationClass`. The app passes fully-formed\n * `DeliveryRowInput`s; this module never invents domain ids.\n *\n * Key invariants (frozen in NOTIFICATIONS_REDESIGN):\n * - Enqueue = create-if-absent. `ALREADY_EXISTS` is a per-row duplicate no-op,\n * never a failure — there is no marker-without-payload state, so the\n * \"marker created, payload lost\" crash window cannot exist.\n * - Materialize = ONE transaction that reads the delivery row + active card,\n * applies the aggregation exactly once (per strategy + caps), rotates the\n * opaque `activityGeneration`, resets `seenAt`, and flips the row to\n * `materialized` — so the \"applied but not marked\" double-apply window is gone.\n * Retrying a `materialized` row is a successful no-op.\n * - TTL (`expireAt`, a real Firestore Timestamp via `config.timestampFromMillis`)\n * is set ONLY at `materialized`; a `queued` or `deadLetter` row is NEVER TTL'd\n * (round-19) so an unresolved delivery can't be silently deleted before replay.\n */\n\nimport { randomUUID } from 'node:crypto';\nimport type { NotificationSystemConfig, NotificationDoc } from '../types.js';\nimport type { ServerFirestore, ServerDocRef } from './types.js';\nimport { buildActiveNotificationDocId } from './activeNotificationId.js';\n\nconst DEFAULT_DELIVERIES_PATH = 'notificationDeliveries';\nconst DEFAULT_DELIVERY_TTL_MS = 90 * 24 * 60 * 60 * 1000; // 90 days\nconst DEFAULT_MAX_ATTEMPTS = 8;\nconst DEFAULT_COUNT_CAP = 5000;\nconst DEFAULT_ACTOR_CAP = 5;\nconst BACKOFF_BASE_MS = 60 * 1000; // 1 min\nconst BACKOFF_MAX_MS = 60 * 60 * 1000; // 1 hour\n\nexport type DeliveryState = 'queued' | 'materialized' | 'deadLetter';\nexport type AggregationStrategy = 'increment' | 'staticRelight';\nexport type MaterializationClass =\n | 'directQueued'\n | 'realtimeFallback'\n | 'fanoutOrphan'\n | 'retry';\n\n/** The materialization payload carried on every delivery row. */\nexport interface DeliveryPayload {\n /** The acting user's uid, or `null`/`''` for a system / no-actor occurrence (N-M2). */\n actorId: string | null;\n metadata: Record<string, unknown>;\n occurrenceAt: number;\n}\n\n/** App-supplied, fully-formed delivery row (the app owns all id construction). */\nexport interface DeliveryRowInput {\n deliveryId: string;\n notificationType: string;\n eventId: string;\n /** `null` for shared-admin occurrences (the deliveryId hash uses the literal `'shared'`). */\n recipientUid: string | null;\n aggregationKey: string;\n strategy: AggregationStrategy;\n payload: DeliveryPayload;\n payloadVersion: number;\n materializationClass: MaterializationClass;\n}\n\nexport type EnqueueRowResult =\n | { deliveryId: string; outcome: 'created' }\n | { deliveryId: string; outcome: 'duplicate' }\n | { deliveryId: string; outcome: 'failed'; error: unknown };\n\nexport interface EnqueueResult {\n results: EnqueueRowResult[];\n /** True iff EVERY row is now represented by a created-or-pre-existing doc (page-cursor invariant input). */\n allResolved: boolean;\n}\n\nexport type MaterializeOutcome =\n | 'materialized'\n | 'already-materialized'\n | 'missing'\n | 'skipped-non-queued';\n\nexport interface DeliveryLedger {\n enqueue(rows: DeliveryRowInput[]): Promise<EnqueueResult>;\n materialize(deliveryId: string): Promise<MaterializeOutcome>;\n /** Materialize many rows with bounded concurrency; transient throws record a retry/dead-letter outcome. */\n materializeMany(deliveryIds: string[], options?: { concurrency?: number }): Promise<Record<MaterializeOutcome, number>>;\n recordTransientFailure(deliveryId: string, error: unknown): Promise<void>;\n deadLetter(deliveryId: string, error: unknown): Promise<void>;\n replay(deliveryId: string): Promise<void>;\n}\n\n/** gRPC ALREADY_EXISTS (6) — the create-if-absent duplicate signal. */\nfunction isAlreadyExists(error: unknown): boolean {\n const e = error as { code?: number; message?: string } | undefined;\n return e?.code === 6 || /already.?exists/i.test(e?.message ?? '');\n}\n\nfunction backoffMs(attemptCount: number): number {\n const exp = Math.min(BACKOFF_BASE_MS * 2 ** Math.max(0, attemptCount - 1), BACKOFF_MAX_MS);\n // Full jitter — spread retries so a burst of failures doesn't synchronize.\n return Math.floor(exp / 2 + Math.random() * (exp / 2));\n}\n\n/**\n * Apply the aggregation strategy to produce the active-doc field delta.\n * PURE — exported for direct unit testing. `existing` is the current active doc\n * (or null to create). Always rotates `activityGeneration` and resets `seenAt`.\n */\nexport function applyAggregation(params: {\n strategy: AggregationStrategy;\n existing: Pick<NotificationDoc, 'count' | 'latestActorIds'> | null;\n actorId: string | null;\n countCap: number;\n actorCap: number;\n buildMessage: (count: number) => string;\n now: number;\n generation: string;\n}): { count: number; latestActorIds: string[]; message: string; activityGeneration: string; seenAt: number; updatedAt: number } {\n const { strategy, existing, actorId, countCap, actorCap, buildMessage, now, generation } = params;\n const prevActors = existing?.latestActorIds ?? [];\n // N-M2: a system / no-actor occurrence (null or '') is never injected into the actor\n // list — it would render as a blank avatar / unresolvable uid in `latestActorIds`.\n const latestActorIds = actorId\n ? [actorId, ...prevActors.filter((id) => id !== actorId)].slice(0, actorCap)\n : prevActors.slice(0, actorCap);\n // staticRelight never shows a count (stays 1); increment counts up to the cap.\n const count =\n strategy === 'staticRelight'\n ? 1\n : Math.min((existing?.count ?? 0) + 1, countCap);\n return {\n count,\n latestActorIds,\n message: buildMessage(count),\n activityGeneration: generation,\n seenAt: 0,\n updatedAt: now,\n };\n}\n\nexport function createDeliveryLedger(\n db: ServerFirestore,\n config: NotificationSystemConfig,\n): DeliveryLedger {\n const deliveriesPath = config.deliveriesCollectionPath ?? DEFAULT_DELIVERIES_PATH;\n const ttlMs = config.deliveryTtlMs ?? DEFAULT_DELIVERY_TTL_MS;\n const maxAttempts = config.maxDeliveryAttempts ?? DEFAULT_MAX_ATTEMPTS;\n const configTimestampFromMillis = config.timestampFromMillis;\n if (!configTimestampFromMillis) {\n throw new Error(\n '[notification-core] createDeliveryLedger requires config.timestampFromMillis (Firestore TTL needs a real Timestamp for expireAt).',\n );\n }\n // Bind to an explicitly non-optional type so the nested transaction closures\n // (called later) see it as defined, not `... | undefined`.\n const toTimestamp: (ms: number) => unknown = configTimestampFromMillis;\n\n const deliveries = db.collection(deliveriesPath);\n\n function deliveryRef(deliveryId: string): ServerDocRef {\n return deliveries.doc(deliveryId);\n }\n\n function buildDeliveryDoc(row: DeliveryRowInput, now: number): Record<string, unknown> {\n return {\n deliveryId: row.deliveryId,\n state: 'queued' as DeliveryState,\n notificationType: row.notificationType,\n eventId: row.eventId,\n recipientUid: row.recipientUid,\n aggregationKey: row.aggregationKey,\n strategy: row.strategy,\n payload: row.payload,\n payloadVersion: row.payloadVersion,\n materializationClass: row.materializationClass,\n attemptCount: 0,\n nextAttemptAt: now,\n lastError: null,\n createdAt: now,\n materializedAt: null,\n deadLetteredAt: null,\n // No expireAt while queued (round-19).\n };\n }\n\n function getTypeConfig(type: string) {\n const typeConfig = config.types[type];\n if (!typeConfig) throw new Error(`[notification-core] Unknown notification type: ${type}`);\n const categoryConfig = config.categories[typeConfig.category];\n if (!categoryConfig) throw new Error(`[notification-core] Unknown category: ${typeConfig.category}`);\n return { typeConfig, categoryConfig };\n }\n\n async function enqueue(rows: DeliveryRowInput[]): Promise<EnqueueResult> {\n if (rows.length === 0) return { results: [], allResolved: true };\n const now = Date.now();\n const settled = await Promise.allSettled(\n rows.map((row) =>\n deliveryRef(row.deliveryId)\n .create(buildDeliveryDoc(row, now))\n .then(() => ({ deliveryId: row.deliveryId, outcome: 'created' as const })),\n ),\n );\n const results: EnqueueRowResult[] = settled.map((s, i) => {\n const deliveryId = rows[i].deliveryId;\n if (s.status === 'fulfilled') return s.value;\n if (isAlreadyExists(s.reason)) return { deliveryId, outcome: 'duplicate' };\n return { deliveryId, outcome: 'failed', error: s.reason };\n });\n return { results, allResolved: results.every((r) => r.outcome !== 'failed') };\n }\n\n async function materialize(deliveryId: string): Promise<MaterializeOutcome> {\n return db.runTransaction(async (tx) => {\n const dRef = deliveryRef(deliveryId);\n const dSnap = await tx.get(dRef);\n if (!dSnap.exists) return 'missing';\n const d = (dSnap.data() ?? {}) as Record<string, unknown>;\n const state = d.state as DeliveryState;\n if (state === 'materialized') return 'already-materialized';\n if (state !== 'queued') return 'skipped-non-queued';\n\n const notificationType = d.notificationType as string;\n const aggregationKey = d.aggregationKey as string;\n const recipientUid = (d.recipientUid as string | null) ?? null;\n const strategy = (d.strategy as AggregationStrategy) ?? 'increment';\n const payload = (d.payload as DeliveryPayload) ?? { actorId: '', metadata: {}, occurrenceAt: Date.now() };\n const { typeConfig, categoryConfig } = getTypeConfig(notificationType);\n\n const activeRef = db.collection(categoryConfig.activePath).doc(\n buildActiveNotificationDocId({\n category: typeConfig.category,\n audienceType: categoryConfig.audienceType,\n targetUserId: recipientUid,\n dedupKey: aggregationKey,\n notificationType,\n }),\n );\n const activeSnap = await tx.get(activeRef); // all reads before writes\n\n const now = Date.now();\n const generation = randomUUID();\n const countCap = typeConfig.countCap ?? DEFAULT_COUNT_CAP;\n const actorCap = typeConfig.actorCap ?? DEFAULT_ACTOR_CAP;\n const buildMessage = (count: number) => typeConfig.messagePattern(payload.metadata, count);\n // Latest-occurrence-derived fields — recomputed every materialize so an EXISTING\n // aggregate card reflects the newest activity (title/targetPath/metadata), not the\n // first occurrence's stale values (N-I4).\n const title = typeConfig.titlePattern(payload.metadata);\n // A type with no defaultTargetPath is linkless: the doc must OMIT the field\n // entirely (Firestore rejects `undefined` values). A pre-existing doc written\n // before a type went linkless keeps its stale targetPath until archived.\n const targetPath =\n typeConfig.defaultTargetPath === undefined\n ? undefined\n : typeof typeConfig.defaultTargetPath === 'function'\n ? typeConfig.defaultTargetPath(payload.metadata)\n : typeConfig.defaultTargetPath;\n const targetPathField = targetPath === undefined ? {} : { targetPath };\n\n if (activeSnap.exists) {\n const existing = (activeSnap.data() ?? {}) as Pick<NotificationDoc, 'count' | 'latestActorIds'>;\n const delta = applyAggregation({\n strategy,\n existing,\n actorId: payload.actorId,\n countCap,\n actorCap,\n buildMessage,\n now,\n generation,\n });\n tx.update(activeRef, { ...delta, title, ...targetPathField, metadata: payload.metadata });\n } else {\n const delta = applyAggregation({\n strategy,\n existing: null,\n actorId: payload.actorId,\n countCap,\n actorCap,\n buildMessage,\n now,\n generation,\n });\n const newDoc: Omit<NotificationDoc, 'id'> = {\n type: notificationType,\n dedupKey: aggregationKey,\n category: typeConfig.category,\n targetUserId: recipientUid,\n title,\n message: delta.message,\n count: delta.count,\n latestActorIds: delta.latestActorIds,\n ...targetPathField,\n metadata: payload.metadata,\n seenAt: 0,\n activityGeneration: generation,\n createdAt: now,\n updatedAt: now,\n };\n tx.set(activeRef, newDoc as Record<string, unknown>);\n }\n\n tx.update(dRef, {\n state: 'materialized' as DeliveryState,\n materializedAt: now,\n expireAt: toTimestamp(now + ttlMs),\n });\n return 'materialized';\n });\n }\n\n async function recordTransientFailure(deliveryId: string, error: unknown): Promise<void> {\n await db.runTransaction(async (tx) => {\n const dRef = deliveryRef(deliveryId);\n const dSnap = await tx.get(dRef);\n if (!dSnap.exists) return;\n const d = (dSnap.data() ?? {}) as Record<string, unknown>;\n if ((d.state as DeliveryState) !== 'queued') return;\n const attemptCount = ((d.attemptCount as number) ?? 0) + 1;\n const now = Date.now();\n const message = error instanceof Error ? error.message : String(error);\n if (attemptCount >= maxAttempts) {\n tx.update(dRef, {\n state: 'deadLetter' as DeliveryState,\n attemptCount,\n lastError: message,\n deadLetteredAt: now,\n });\n } else {\n // n1: re-stamp the row into the reserved `retry` materialization lane on backoff. Without\n // this the row keeps its original class and competes for that lane's capacity while backed\n // off; the worker's reserved `retry` slice (separate CLASS_CAPS capacity, drained in\n // DRAIN_ORDER) would always select 0 rows. Re-stamping moves a backed-off row into the\n // anti-starvation lane so it drains there instead of starving in its original lane.\n tx.update(dRef, {\n attemptCount,\n nextAttemptAt: now + backoffMs(attemptCount),\n lastError: message,\n materializationClass: 'retry' as MaterializationClass,\n });\n }\n });\n }\n\n async function deadLetter(deliveryId: string, error: unknown): Promise<void> {\n await db.runTransaction(async (tx) => {\n const dRef = deliveryRef(deliveryId);\n const dSnap = await tx.get(dRef);\n if (!dSnap.exists) return;\n const d = (dSnap.data() ?? {}) as Record<string, unknown>;\n if ((d.state as DeliveryState) !== 'queued') return;\n tx.update(dRef, {\n state: 'deadLetter' as DeliveryState,\n attemptCount: ((d.attemptCount as number) ?? 0) + 1,\n lastError: error instanceof Error ? error.message : String(error),\n deadLetteredAt: Date.now(),\n });\n });\n }\n\n async function replay(deliveryId: string): Promise<void> {\n await db.runTransaction(async (tx) => {\n const dRef = deliveryRef(deliveryId);\n const dSnap = await tx.get(dRef);\n if (!dSnap.exists) return;\n const d = (dSnap.data() ?? {}) as Record<string, unknown>;\n if ((d.state as DeliveryState) !== 'deadLetter') return;\n tx.update(dRef, {\n state: 'queued' as DeliveryState,\n attemptCount: 0,\n nextAttemptAt: Date.now(),\n lastError: null,\n deadLetteredAt: null,\n expireAt: null, // clear any TTL set on a prior terminal (round-19)\n });\n });\n }\n\n async function materializeMany(\n deliveryIds: string[],\n options?: { concurrency?: number },\n ): Promise<Record<MaterializeOutcome, number>> {\n const concurrency = Math.max(1, options?.concurrency ?? 10);\n const tally: Record<MaterializeOutcome, number> = {\n materialized: 0,\n 'already-materialized': 0,\n missing: 0,\n 'skipped-non-queued': 0,\n };\n let cursor = 0;\n async function worker(): Promise<void> {\n while (cursor < deliveryIds.length) {\n const id = deliveryIds[cursor++];\n try {\n const outcome = await materialize(id);\n tally[outcome] += 1;\n } catch (error) {\n await recordTransientFailure(id, error);\n }\n }\n }\n await Promise.all(Array.from({ length: Math.min(concurrency, deliveryIds.length) }, worker));\n return tally;\n }\n\n return { enqueue, materialize, materializeMany, recordTransientFailure, deadLetter, replay };\n}\n"]}
1
+ {"version":3,"file":"delivery-ledger.js","sourceRoot":"","sources":["../../src/server/delivery-ledger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAGzC,OAAO,EAAE,4BAA4B,EAAE,MAAM,2BAA2B,CAAC;AAEzE,MAAM,uBAAuB,GAAG,wBAAwB,CAAC;AACzD,MAAM,uBAAuB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,UAAU;AACpE,MAAM,oBAAoB,GAAG,CAAC,CAAC;AAC/B,MAAM,iBAAiB,GAAG,IAAI,CAAC;AAC/B,MAAM,iBAAiB,GAAG,CAAC,CAAC;AAC5B,MAAM,eAAe,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,QAAQ;AAC3C,MAAM,cAAc,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,SAAS;AA2DhD,uEAAuE;AACvE,SAAS,eAAe,CAAC,KAAc;IACrC,MAAM,CAAC,GAAG,KAAwD,CAAC;IACnE,OAAO,CAAC,EAAE,IAAI,KAAK,CAAC,IAAI,kBAAkB,CAAC,IAAI,CAAC,CAAC,EAAE,OAAO,IAAI,EAAE,CAAC,CAAC;AACpE,CAAC;AAED,SAAS,SAAS,CAAC,YAAoB;IACrC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,eAAe,GAAG,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,YAAY,GAAG,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC;IAC3F,2EAA2E;IAC3E,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;AACzD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAShC;IACC,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,YAAY,EAAE,GAAG,EAAE,UAAU,EAAE,GAAG,MAAM,CAAC;IAClG,MAAM,UAAU,GAAG,QAAQ,EAAE,cAAc,IAAI,EAAE,CAAC;IAClD,qFAAqF;IACrF,mFAAmF;IACnF,MAAM,cAAc,GAAG,OAAO;QAC5B,CAAC,CAAC,CAAC,OAAO,EAAE,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC;QAC5E,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;IAClC,+EAA+E;IAC/E,MAAM,KAAK,GACT,QAAQ,KAAK,eAAe;QAC1B,CAAC,CAAC,CAAC;QACH,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,KAAK,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,CAAC;IACrD,OAAO;QACL,KAAK;QACL,cAAc;QACd,OAAO,EAAE,YAAY,CAAC,KAAK,CAAC;QAC5B,kBAAkB,EAAE,UAAU;QAC9B,MAAM,EAAE,CAAC;QACT,SAAS,EAAE,GAAG;KACf,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,oBAAoB,CAClC,EAAmB,EACnB,MAAgC;IAEhC,MAAM,cAAc,GAAG,MAAM,CAAC,wBAAwB,IAAI,uBAAuB,CAAC;IAClF,MAAM,KAAK,GAAG,MAAM,CAAC,aAAa,IAAI,uBAAuB,CAAC;IAC9D,MAAM,WAAW,GAAG,MAAM,CAAC,mBAAmB,IAAI,oBAAoB,CAAC;IACvE,MAAM,yBAAyB,GAAG,MAAM,CAAC,mBAAmB,CAAC;IAC7D,IAAI,CAAC,yBAAyB,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CACb,mIAAmI,CACpI,CAAC;IACJ,CAAC;IACD,6EAA6E;IAC7E,2DAA2D;IAC3D,MAAM,WAAW,GAA4B,yBAAyB,CAAC;IAEvE,MAAM,UAAU,GAAG,EAAE,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC;IAEjD,SAAS,WAAW,CAAC,UAAkB;QACrC,OAAO,UAAU,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IACpC,CAAC;IAED,SAAS,gBAAgB,CAAC,GAAqB,EAAE,GAAW;QAC1D,OAAO;YACL,UAAU,EAAE,GAAG,CAAC,UAAU;YAC1B,KAAK,EAAE,QAAyB;YAChC,gBAAgB,EAAE,GAAG,CAAC,gBAAgB;YACtC,OAAO,EAAE,GAAG,CAAC,OAAO;YACpB,YAAY,EAAE,GAAG,CAAC,YAAY;YAC9B,cAAc,EAAE,GAAG,CAAC,cAAc;YAClC,QAAQ,EAAE,GAAG,CAAC,QAAQ;YACtB,OAAO,EAAE,GAAG,CAAC,OAAO;YACpB,cAAc,EAAE,GAAG,CAAC,cAAc;YAClC,oBAAoB,EAAE,GAAG,CAAC,oBAAoB;YAC9C,YAAY,EAAE,CAAC;YACf,aAAa,EAAE,GAAG;YAClB,SAAS,EAAE,IAAI;YACf,SAAS,EAAE,GAAG;YACd,cAAc,EAAE,IAAI;YACpB,cAAc,EAAE,IAAI;YACpB,uCAAuC;SACxC,CAAC;IACJ,CAAC;IAED,SAAS,aAAa,CAAC,IAAY;QACjC,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,CAAC,UAAU;YAAE,MAAM,IAAI,KAAK,CAAC,kDAAkD,IAAI,EAAE,CAAC,CAAC;QAC3F,MAAM,cAAc,GAAG,MAAM,CAAC,UAAU,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;QAC9D,IAAI,CAAC,cAAc;YAAE,MAAM,IAAI,KAAK,CAAC,yCAAyC,UAAU,CAAC,QAAQ,EAAE,CAAC,CAAC;QACrG,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,CAAC;IACxC,CAAC;IAED,KAAK,UAAU,OAAO,CAAC,IAAwB;QAC7C,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;QACjE,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CACtC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CACf,WAAW,CAAC,GAAG,CAAC,UAAU,CAAC;aACxB,MAAM,CAAC,gBAAgB,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;aAClC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,UAAU,EAAE,OAAO,EAAE,SAAkB,EAAE,CAAC,CAAC,CAC7E,CACF,CAAC;QACF,MAAM,OAAO,GAAuB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;YACvD,MAAM,UAAU,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC;YACtC,IAAI,CAAC,CAAC,MAAM,KAAK,WAAW;gBAAE,OAAO,CAAC,CAAC,KAAK,CAAC;YAC7C,IAAI,eAAe,CAAC,CAAC,CAAC,MAAM,CAAC;gBAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC;YAC3E,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC;QAC5D,CAAC,CAAC,CAAC;QACH,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,QAAQ,CAAC,EAAE,CAAC;IAChF,CAAC;IAED,KAAK,UAAU,WAAW,CAAC,UAAkB;QAC3C,OAAO,EAAE,CAAC,cAAc,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;YACpC,MAAM,IAAI,GAAG,WAAW,CAAC,UAAU,CAAC,CAAC;YACrC,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACjC,IAAI,CAAC,KAAK,CAAC,MAAM;gBAAE,OAAO,SAAS,CAAC;YACpC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,CAA4B,CAAC;YAC1D,MAAM,KAAK,GAAG,CAAC,CAAC,KAAsB,CAAC;YACvC,IAAI,KAAK,KAAK,cAAc;gBAAE,OAAO,sBAAsB,CAAC;YAC5D,IAAI,KAAK,KAAK,QAAQ;gBAAE,OAAO,oBAAoB,CAAC;YAEpD,MAAM,gBAAgB,GAAG,CAAC,CAAC,gBAA0B,CAAC;YACtD,MAAM,cAAc,GAAG,CAAC,CAAC,cAAwB,CAAC;YAClD,MAAM,YAAY,GAAI,CAAC,CAAC,YAA8B,IAAI,IAAI,CAAC;YAC/D,MAAM,QAAQ,GAAI,CAAC,CAAC,QAAgC,IAAI,WAAW,CAAC;YACpE,MAAM,OAAO,GAAI,CAAC,CAAC,OAA2B,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,YAAY,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;YAC1G,MAAM,EAAE,UAAU,EAAE,cAAc,EAAE,GAAG,aAAa,CAAC,gBAAgB,CAAC,CAAC;YAEvE,MAAM,SAAS,GAAG,EAAE,CAAC,UAAU,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,GAAG,CAC5D,4BAA4B,CAAC;gBAC3B,QAAQ,EAAE,UAAU,CAAC,QAAQ;gBAC7B,YAAY,EAAE,cAAc,CAAC,YAAY;gBACzC,YAAY,EAAE,YAAY;gBAC1B,QAAQ,EAAE,cAAc;gBACxB,gBAAgB;aACjB,CAAC,CACH,CAAC;YACF,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,0BAA0B;YAEtE,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YACvB,MAAM,UAAU,GAAG,UAAU,EAAE,CAAC;YAChC,MAAM,QAAQ,GAAG,UAAU,CAAC,QAAQ,IAAI,iBAAiB,CAAC;YAC1D,MAAM,QAAQ,GAAG,UAAU,CAAC,QAAQ,IAAI,iBAAiB,CAAC;YAC1D,MAAM,YAAY,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,UAAU,CAAC,cAAc,CAAC,OAAO,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;YAC3F,iFAAiF;YACjF,mFAAmF;YACnF,0CAA0C;YAC1C,MAAM,KAAK,GAAG,UAAU,CAAC,YAAY,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;YACxD,4EAA4E;YAC5E,8EAA8E;YAC9E,yEAAyE;YACzE,MAAM,UAAU,GACd,UAAU,CAAC,iBAAiB,KAAK,SAAS;gBACxC,CAAC,CAAC,SAAS;gBACX,CAAC,CAAC,OAAO,UAAU,CAAC,iBAAiB,KAAK,UAAU;oBAClD,CAAC,CAAC,UAAU,CAAC,iBAAiB,CAAC,OAAO,CAAC,QAAQ,CAAC;oBAChD,CAAC,CAAC,UAAU,CAAC,iBAAiB,CAAC;YACrC,MAAM,eAAe,GAAG,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC;YAEvE,IAAI,UAAU,CAAC,MAAM,EAAE,CAAC;gBACtB,MAAM,QAAQ,GAAG,CAAC,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,CAAsD,CAAC;gBAChG,MAAM,KAAK,GAAG,gBAAgB,CAAC;oBAC7B,QAAQ;oBACR,QAAQ;oBACR,OAAO,EAAE,OAAO,CAAC,OAAO;oBACxB,QAAQ;oBACR,QAAQ;oBACR,YAAY;oBACZ,GAAG;oBACH,UAAU;iBACX,CAAC,CAAC;gBACH,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,GAAG,eAAe,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;YAC5F,CAAC;iBAAM,CAAC;gBACN,MAAM,KAAK,GAAG,gBAAgB,CAAC;oBAC7B,QAAQ;oBACR,QAAQ,EAAE,IAAI;oBACd,OAAO,EAAE,OAAO,CAAC,OAAO;oBACxB,QAAQ;oBACR,QAAQ;oBACR,YAAY;oBACZ,GAAG;oBACH,UAAU;iBACX,CAAC,CAAC;gBACH,MAAM,MAAM,GAAgC;oBAC1C,IAAI,EAAE,gBAAgB;oBACtB,QAAQ,EAAE,cAAc;oBACxB,QAAQ,EAAE,UAAU,CAAC,QAAQ;oBAC7B,YAAY,EAAE,YAAY;oBAC1B,KAAK;oBACL,OAAO,EAAE,KAAK,CAAC,OAAO;oBACtB,KAAK,EAAE,KAAK,CAAC,KAAK;oBAClB,cAAc,EAAE,KAAK,CAAC,cAAc;oBACpC,GAAG,eAAe;oBAClB,QAAQ,EAAE,OAAO,CAAC,QAAQ;oBAC1B,MAAM,EAAE,CAAC;oBACT,kBAAkB,EAAE,UAAU;oBAC9B,SAAS,EAAE,GAAG;oBACd,SAAS,EAAE,GAAG;iBACf,CAAC;gBACF,EAAE,CAAC,GAAG,CAAC,SAAS,EAAE,MAAiC,CAAC,CAAC;YACvD,CAAC;YAED,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE;gBACd,KAAK,EAAE,cAA+B;gBACtC,cAAc,EAAE,GAAG;gBACnB,QAAQ,EAAE,WAAW,CAAC,GAAG,GAAG,KAAK,CAAC;aACnC,CAAC,CAAC;YACH,OAAO,cAAc,CAAC;QACxB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,UAAU,sBAAsB,CAAC,UAAkB,EAAE,KAAc;QACtE,MAAM,EAAE,CAAC,cAAc,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;YACnC,MAAM,IAAI,GAAG,WAAW,CAAC,UAAU,CAAC,CAAC;YACrC,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACjC,IAAI,CAAC,KAAK,CAAC,MAAM;gBAAE,OAAO;YAC1B,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,CAA4B,CAAC;YAC1D,IAAK,CAAC,CAAC,KAAuB,KAAK,QAAQ;gBAAE,OAAO;YACpD,MAAM,YAAY,GAAG,CAAE,CAAC,CAAC,YAAuB,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;YAC3D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YACvB,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACvE,IAAI,YAAY,IAAI,WAAW,EAAE,CAAC;gBAChC,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE;oBACd,KAAK,EAAE,YAA6B;oBACpC,YAAY;oBACZ,SAAS,EAAE,OAAO;oBAClB,cAAc,EAAE,GAAG;iBACpB,CAAC,CAAC;YACL,CAAC;iBAAM,CAAC;gBACN,0FAA0F;gBAC1F,2FAA2F;gBAC3F,qFAAqF;gBACrF,uFAAuF;gBACvF,oFAAoF;gBACpF,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE;oBACd,YAAY;oBACZ,aAAa,EAAE,GAAG,GAAG,SAAS,CAAC,YAAY,CAAC;oBAC5C,SAAS,EAAE,OAAO;oBAClB,oBAAoB,EAAE,OAA+B;iBACtD,CAAC,CAAC;YACL,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,UAAU,UAAU,CAAC,UAAkB,EAAE,KAAc;QAC1D,MAAM,EAAE,CAAC,cAAc,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;YACnC,MAAM,IAAI,GAAG,WAAW,CAAC,UAAU,CAAC,CAAC;YACrC,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACjC,IAAI,CAAC,KAAK,CAAC,MAAM;gBAAE,OAAO;YAC1B,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,CAA4B,CAAC;YAC1D,IAAK,CAAC,CAAC,KAAuB,KAAK,QAAQ;gBAAE,OAAO;YACpD,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE;gBACd,KAAK,EAAE,YAA6B;gBACpC,YAAY,EAAE,CAAE,CAAC,CAAC,YAAuB,IAAI,CAAC,CAAC,GAAG,CAAC;gBACnD,SAAS,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;gBACjE,cAAc,EAAE,IAAI,CAAC,GAAG,EAAE;aAC3B,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,UAAU,MAAM,CAAC,UAAkB;QACtC,MAAM,EAAE,CAAC,cAAc,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;YACnC,MAAM,IAAI,GAAG,WAAW,CAAC,UAAU,CAAC,CAAC;YACrC,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACjC,IAAI,CAAC,KAAK,CAAC,MAAM;gBAAE,OAAO;YAC1B,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,CAA4B,CAAC;YAC1D,IAAK,CAAC,CAAC,KAAuB,KAAK,YAAY;gBAAE,OAAO;YACxD,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE;gBACd,KAAK,EAAE,QAAyB;gBAChC,YAAY,EAAE,CAAC;gBACf,aAAa,EAAE,IAAI,CAAC,GAAG,EAAE;gBACzB,SAAS,EAAE,IAAI;gBACf,cAAc,EAAE,IAAI;gBACpB,QAAQ,EAAE,IAAI,EAAE,mDAAmD;aACpE,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,UAAU,eAAe,CAC5B,WAAqB,EACrB,OAAkC;QAElC,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,WAAW,IAAI,EAAE,CAAC,CAAC;QAC5D,MAAM,KAAK,GAAuC;YAChD,YAAY,EAAE,CAAC;YACf,sBAAsB,EAAE,CAAC;YACzB,OAAO,EAAE,CAAC;YACV,oBAAoB,EAAE,CAAC;SACxB,CAAC;QACF,IAAI,MAAM,GAAG,CAAC,CAAC;QACf,KAAK,UAAU,MAAM;YACnB,OAAO,MAAM,GAAG,WAAW,CAAC,MAAM,EAAE,CAAC;gBACnC,MAAM,EAAE,GAAG,WAAW,CAAC,MAAM,EAAE,CAAC,CAAC;gBACjC,IAAI,CAAC;oBACH,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,EAAE,CAAC,CAAC;oBACtC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;gBACtB,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,MAAM,sBAAsB,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;gBAC1C,CAAC;YACH,CAAC;QACH,CAAC;QACD,MAAM,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;QAC7F,OAAO,KAAK,CAAC;IACf,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,sBAAsB,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC;AAC/F,CAAC","sourcesContent":["/**\n * Generic notification delivery ledger (notification redesign — P1).\n *\n * One Firestore doc per (recipient|shared, type, occurrence) is BOTH the queue\n * row AND the idempotency ledger. This package owns the GENERIC mechanism;\n * the consuming app owns the concrete collection name, the deterministic\n * `deliveryId`/`eventId`/`aggregationKey` construction, and the worker that\n * selects rows per `materializationClass`. The app passes fully-formed\n * `DeliveryRowInput`s; this module never invents domain ids.\n *\n * Key invariants (FROZEN — this module is their single source; the consuming app's\n * notification design doc explains the product rationale):\n * - Enqueue = create-if-absent. `ALREADY_EXISTS` is a per-row duplicate no-op,\n * never a failure — there is no marker-without-payload state, so the\n * \"marker created, payload lost\" crash window cannot exist.\n * - Materialize = ONE transaction that reads the delivery row + active card,\n * applies the aggregation exactly once (per strategy + caps), rotates the\n * opaque `activityGeneration`, resets `seenAt`, and flips the row to\n * `materialized` — so the \"applied but not marked\" double-apply window is gone.\n * Retrying a `materialized` row is a successful no-op.\n * - TTL (`expireAt`, a real Firestore Timestamp via `config.timestampFromMillis`)\n * is set ONLY at `materialized`; a `queued` or `deadLetter` row is NEVER TTL'd\n * (round-19) so an unresolved delivery can't be silently deleted before replay.\n */\n\nimport { randomUUID } from 'node:crypto';\nimport type { NotificationSystemConfig, NotificationDoc } from '../types.js';\nimport type { ServerFirestore, ServerDocRef } from './types.js';\nimport { buildActiveNotificationDocId } from './activeNotificationId.js';\n\nconst DEFAULT_DELIVERIES_PATH = 'notificationDeliveries';\nconst DEFAULT_DELIVERY_TTL_MS = 90 * 24 * 60 * 60 * 1000; // 90 days\nconst DEFAULT_MAX_ATTEMPTS = 8;\nconst DEFAULT_COUNT_CAP = 5000;\nconst DEFAULT_ACTOR_CAP = 5;\nconst BACKOFF_BASE_MS = 60 * 1000; // 1 min\nconst BACKOFF_MAX_MS = 60 * 60 * 1000; // 1 hour\n\nexport type DeliveryState = 'queued' | 'materialized' | 'deadLetter';\nexport type AggregationStrategy = 'increment' | 'staticRelight';\nexport type MaterializationClass =\n | 'directQueued'\n | 'realtimeFallback'\n | 'fanoutOrphan'\n | 'retry';\n\n/** The materialization payload carried on every delivery row. */\nexport interface DeliveryPayload {\n /** The acting user's uid, or `null`/`''` for a system / no-actor occurrence (N-M2). */\n actorId: string | null;\n metadata: Record<string, unknown>;\n occurrenceAt: number;\n}\n\n/** App-supplied, fully-formed delivery row (the app owns all id construction). */\nexport interface DeliveryRowInput {\n deliveryId: string;\n notificationType: string;\n eventId: string;\n /** `null` for shared-admin occurrences (the deliveryId hash uses the literal `'shared'`). */\n recipientUid: string | null;\n aggregationKey: string;\n strategy: AggregationStrategy;\n payload: DeliveryPayload;\n payloadVersion: number;\n materializationClass: MaterializationClass;\n}\n\nexport type EnqueueRowResult =\n | { deliveryId: string; outcome: 'created' }\n | { deliveryId: string; outcome: 'duplicate' }\n | { deliveryId: string; outcome: 'failed'; error: unknown };\n\nexport interface EnqueueResult {\n results: EnqueueRowResult[];\n /** True iff EVERY row is now represented by a created-or-pre-existing doc (page-cursor invariant input). */\n allResolved: boolean;\n}\n\nexport type MaterializeOutcome =\n | 'materialized'\n | 'already-materialized'\n | 'missing'\n | 'skipped-non-queued';\n\nexport interface DeliveryLedger {\n enqueue(rows: DeliveryRowInput[]): Promise<EnqueueResult>;\n materialize(deliveryId: string): Promise<MaterializeOutcome>;\n /** Materialize many rows with bounded concurrency; transient throws record a retry/dead-letter outcome. */\n materializeMany(deliveryIds: string[], options?: { concurrency?: number }): Promise<Record<MaterializeOutcome, number>>;\n recordTransientFailure(deliveryId: string, error: unknown): Promise<void>;\n deadLetter(deliveryId: string, error: unknown): Promise<void>;\n replay(deliveryId: string): Promise<void>;\n}\n\n/** gRPC ALREADY_EXISTS (6) — the create-if-absent duplicate signal. */\nfunction isAlreadyExists(error: unknown): boolean {\n const e = error as { code?: number; message?: string } | undefined;\n return e?.code === 6 || /already.?exists/i.test(e?.message ?? '');\n}\n\nfunction backoffMs(attemptCount: number): number {\n const exp = Math.min(BACKOFF_BASE_MS * 2 ** Math.max(0, attemptCount - 1), BACKOFF_MAX_MS);\n // Full jitter — spread retries so a burst of failures doesn't synchronize.\n return Math.floor(exp / 2 + Math.random() * (exp / 2));\n}\n\n/**\n * Apply the aggregation strategy to produce the active-doc field delta.\n * PURE — exported for direct unit testing. `existing` is the current active doc\n * (or null to create). Always rotates `activityGeneration` and resets `seenAt`.\n */\nexport function applyAggregation(params: {\n strategy: AggregationStrategy;\n existing: Pick<NotificationDoc, 'count' | 'latestActorIds'> | null;\n actorId: string | null;\n countCap: number;\n actorCap: number;\n buildMessage: (count: number) => string;\n now: number;\n generation: string;\n}): { count: number; latestActorIds: string[]; message: string; activityGeneration: string; seenAt: number; updatedAt: number } {\n const { strategy, existing, actorId, countCap, actorCap, buildMessage, now, generation } = params;\n const prevActors = existing?.latestActorIds ?? [];\n // N-M2: a system / no-actor occurrence (null or '') is never injected into the actor\n // list — it would render as a blank avatar / unresolvable uid in `latestActorIds`.\n const latestActorIds = actorId\n ? [actorId, ...prevActors.filter((id) => id !== actorId)].slice(0, actorCap)\n : prevActors.slice(0, actorCap);\n // staticRelight never shows a count (stays 1); increment counts up to the cap.\n const count =\n strategy === 'staticRelight'\n ? 1\n : Math.min((existing?.count ?? 0) + 1, countCap);\n return {\n count,\n latestActorIds,\n message: buildMessage(count),\n activityGeneration: generation,\n seenAt: 0,\n updatedAt: now,\n };\n}\n\nexport function createDeliveryLedger(\n db: ServerFirestore,\n config: NotificationSystemConfig,\n): DeliveryLedger {\n const deliveriesPath = config.deliveriesCollectionPath ?? DEFAULT_DELIVERIES_PATH;\n const ttlMs = config.deliveryTtlMs ?? DEFAULT_DELIVERY_TTL_MS;\n const maxAttempts = config.maxDeliveryAttempts ?? DEFAULT_MAX_ATTEMPTS;\n const configTimestampFromMillis = config.timestampFromMillis;\n if (!configTimestampFromMillis) {\n throw new Error(\n '[notification-core] createDeliveryLedger requires config.timestampFromMillis (Firestore TTL needs a real Timestamp for expireAt).',\n );\n }\n // Bind to an explicitly non-optional type so the nested transaction closures\n // (called later) see it as defined, not `... | undefined`.\n const toTimestamp: (ms: number) => unknown = configTimestampFromMillis;\n\n const deliveries = db.collection(deliveriesPath);\n\n function deliveryRef(deliveryId: string): ServerDocRef {\n return deliveries.doc(deliveryId);\n }\n\n function buildDeliveryDoc(row: DeliveryRowInput, now: number): Record<string, unknown> {\n return {\n deliveryId: row.deliveryId,\n state: 'queued' as DeliveryState,\n notificationType: row.notificationType,\n eventId: row.eventId,\n recipientUid: row.recipientUid,\n aggregationKey: row.aggregationKey,\n strategy: row.strategy,\n payload: row.payload,\n payloadVersion: row.payloadVersion,\n materializationClass: row.materializationClass,\n attemptCount: 0,\n nextAttemptAt: now,\n lastError: null,\n createdAt: now,\n materializedAt: null,\n deadLetteredAt: null,\n // No expireAt while queued (round-19).\n };\n }\n\n function getTypeConfig(type: string) {\n const typeConfig = config.types[type];\n if (!typeConfig) throw new Error(`[notification-core] Unknown notification type: ${type}`);\n const categoryConfig = config.categories[typeConfig.category];\n if (!categoryConfig) throw new Error(`[notification-core] Unknown category: ${typeConfig.category}`);\n return { typeConfig, categoryConfig };\n }\n\n async function enqueue(rows: DeliveryRowInput[]): Promise<EnqueueResult> {\n if (rows.length === 0) return { results: [], allResolved: true };\n const now = Date.now();\n const settled = await Promise.allSettled(\n rows.map((row) =>\n deliveryRef(row.deliveryId)\n .create(buildDeliveryDoc(row, now))\n .then(() => ({ deliveryId: row.deliveryId, outcome: 'created' as const })),\n ),\n );\n const results: EnqueueRowResult[] = settled.map((s, i) => {\n const deliveryId = rows[i].deliveryId;\n if (s.status === 'fulfilled') return s.value;\n if (isAlreadyExists(s.reason)) return { deliveryId, outcome: 'duplicate' };\n return { deliveryId, outcome: 'failed', error: s.reason };\n });\n return { results, allResolved: results.every((r) => r.outcome !== 'failed') };\n }\n\n async function materialize(deliveryId: string): Promise<MaterializeOutcome> {\n return db.runTransaction(async (tx) => {\n const dRef = deliveryRef(deliveryId);\n const dSnap = await tx.get(dRef);\n if (!dSnap.exists) return 'missing';\n const d = (dSnap.data() ?? {}) as Record<string, unknown>;\n const state = d.state as DeliveryState;\n if (state === 'materialized') return 'already-materialized';\n if (state !== 'queued') return 'skipped-non-queued';\n\n const notificationType = d.notificationType as string;\n const aggregationKey = d.aggregationKey as string;\n const recipientUid = (d.recipientUid as string | null) ?? null;\n const strategy = (d.strategy as AggregationStrategy) ?? 'increment';\n const payload = (d.payload as DeliveryPayload) ?? { actorId: '', metadata: {}, occurrenceAt: Date.now() };\n const { typeConfig, categoryConfig } = getTypeConfig(notificationType);\n\n const activeRef = db.collection(categoryConfig.activePath).doc(\n buildActiveNotificationDocId({\n category: typeConfig.category,\n audienceType: categoryConfig.audienceType,\n targetUserId: recipientUid,\n dedupKey: aggregationKey,\n notificationType,\n }),\n );\n const activeSnap = await tx.get(activeRef); // all reads before writes\n\n const now = Date.now();\n const generation = randomUUID();\n const countCap = typeConfig.countCap ?? DEFAULT_COUNT_CAP;\n const actorCap = typeConfig.actorCap ?? DEFAULT_ACTOR_CAP;\n const buildMessage = (count: number) => typeConfig.messagePattern(payload.metadata, count);\n // Latest-occurrence-derived fields — recomputed every materialize so an EXISTING\n // aggregate card reflects the newest activity (title/targetPath/metadata), not the\n // first occurrence's stale values (N-I4).\n const title = typeConfig.titlePattern(payload.metadata);\n // A type with no defaultTargetPath is linkless: the doc must OMIT the field\n // entirely (Firestore rejects `undefined` values). A pre-existing doc written\n // before a type went linkless keeps its stale targetPath until archived.\n const targetPath =\n typeConfig.defaultTargetPath === undefined\n ? undefined\n : typeof typeConfig.defaultTargetPath === 'function'\n ? typeConfig.defaultTargetPath(payload.metadata)\n : typeConfig.defaultTargetPath;\n const targetPathField = targetPath === undefined ? {} : { targetPath };\n\n if (activeSnap.exists) {\n const existing = (activeSnap.data() ?? {}) as Pick<NotificationDoc, 'count' | 'latestActorIds'>;\n const delta = applyAggregation({\n strategy,\n existing,\n actorId: payload.actorId,\n countCap,\n actorCap,\n buildMessage,\n now,\n generation,\n });\n tx.update(activeRef, { ...delta, title, ...targetPathField, metadata: payload.metadata });\n } else {\n const delta = applyAggregation({\n strategy,\n existing: null,\n actorId: payload.actorId,\n countCap,\n actorCap,\n buildMessage,\n now,\n generation,\n });\n const newDoc: Omit<NotificationDoc, 'id'> = {\n type: notificationType,\n dedupKey: aggregationKey,\n category: typeConfig.category,\n targetUserId: recipientUid,\n title,\n message: delta.message,\n count: delta.count,\n latestActorIds: delta.latestActorIds,\n ...targetPathField,\n metadata: payload.metadata,\n seenAt: 0,\n activityGeneration: generation,\n createdAt: now,\n updatedAt: now,\n };\n tx.set(activeRef, newDoc as Record<string, unknown>);\n }\n\n tx.update(dRef, {\n state: 'materialized' as DeliveryState,\n materializedAt: now,\n expireAt: toTimestamp(now + ttlMs),\n });\n return 'materialized';\n });\n }\n\n async function recordTransientFailure(deliveryId: string, error: unknown): Promise<void> {\n await db.runTransaction(async (tx) => {\n const dRef = deliveryRef(deliveryId);\n const dSnap = await tx.get(dRef);\n if (!dSnap.exists) return;\n const d = (dSnap.data() ?? {}) as Record<string, unknown>;\n if ((d.state as DeliveryState) !== 'queued') return;\n const attemptCount = ((d.attemptCount as number) ?? 0) + 1;\n const now = Date.now();\n const message = error instanceof Error ? error.message : String(error);\n if (attemptCount >= maxAttempts) {\n tx.update(dRef, {\n state: 'deadLetter' as DeliveryState,\n attemptCount,\n lastError: message,\n deadLetteredAt: now,\n });\n } else {\n // n1: re-stamp the row into the reserved `retry` materialization lane on backoff. Without\n // this the row keeps its original class and competes for that lane's capacity while backed\n // off; the worker's reserved `retry` slice (separate CLASS_CAPS capacity, drained in\n // DRAIN_ORDER) would always select 0 rows. Re-stamping moves a backed-off row into the\n // anti-starvation lane so it drains there instead of starving in its original lane.\n tx.update(dRef, {\n attemptCount,\n nextAttemptAt: now + backoffMs(attemptCount),\n lastError: message,\n materializationClass: 'retry' as MaterializationClass,\n });\n }\n });\n }\n\n async function deadLetter(deliveryId: string, error: unknown): Promise<void> {\n await db.runTransaction(async (tx) => {\n const dRef = deliveryRef(deliveryId);\n const dSnap = await tx.get(dRef);\n if (!dSnap.exists) return;\n const d = (dSnap.data() ?? {}) as Record<string, unknown>;\n if ((d.state as DeliveryState) !== 'queued') return;\n tx.update(dRef, {\n state: 'deadLetter' as DeliveryState,\n attemptCount: ((d.attemptCount as number) ?? 0) + 1,\n lastError: error instanceof Error ? error.message : String(error),\n deadLetteredAt: Date.now(),\n });\n });\n }\n\n async function replay(deliveryId: string): Promise<void> {\n await db.runTransaction(async (tx) => {\n const dRef = deliveryRef(deliveryId);\n const dSnap = await tx.get(dRef);\n if (!dSnap.exists) return;\n const d = (dSnap.data() ?? {}) as Record<string, unknown>;\n if ((d.state as DeliveryState) !== 'deadLetter') return;\n tx.update(dRef, {\n state: 'queued' as DeliveryState,\n attemptCount: 0,\n nextAttemptAt: Date.now(),\n lastError: null,\n deadLetteredAt: null,\n expireAt: null, // clear any TTL set on a prior terminal (round-19)\n });\n });\n }\n\n async function materializeMany(\n deliveryIds: string[],\n options?: { concurrency?: number },\n ): Promise<Record<MaterializeOutcome, number>> {\n const concurrency = Math.max(1, options?.concurrency ?? 10);\n const tally: Record<MaterializeOutcome, number> = {\n materialized: 0,\n 'already-materialized': 0,\n missing: 0,\n 'skipped-non-queued': 0,\n };\n let cursor = 0;\n async function worker(): Promise<void> {\n while (cursor < deliveryIds.length) {\n const id = deliveryIds[cursor++];\n try {\n const outcome = await materialize(id);\n tally[outcome] += 1;\n } catch (error) {\n await recordTransientFailure(id, error);\n }\n }\n }\n await Promise.all(Array.from({ length: Math.min(concurrency, deliveryIds.length) }, worker));\n return tally;\n }\n\n return { enqueue, materialize, materializeMany, recordTransientFailure, deadLetter, replay };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttt-productions/notification-core",
3
- "version": "0.15.4",
3
+ "version": "0.15.6",
4
4
  "description": "Shared notification system for TTT Productions apps — active/history two-tier architecture with dedup, batch processing, and themed UI components",
5
5
  "repository": {
6
6
  "type": "git",
@@ -42,8 +42,8 @@
42
42
  },
43
43
  "peerDependencies": {
44
44
  "@tanstack/react-query": ">=5.0.0",
45
- "@ttt-productions/query-core": "^0.14.1",
46
- "@ttt-productions/ui-core": "^0.11.7",
45
+ "@ttt-productions/query-core": "^0.14.3",
46
+ "@ttt-productions/ui-core": "^0.11.15",
47
47
  "firebase": ">=12.0.0",
48
48
  "lucide-react": ">=0.0.0",
49
49
  "react": ">=19.0.0",
@@ -74,8 +74,8 @@
74
74
  },
75
75
  "devDependencies": {
76
76
  "@tanstack/react-query": "^5.0.0",
77
- "@ttt-productions/query-core": "^0.14.1",
78
- "@ttt-productions/ui-core": "^0.11.7",
77
+ "@ttt-productions/query-core": "^0.14.3",
78
+ "@ttt-productions/ui-core": "^0.11.15",
79
79
  "@types/react": "^19.0.0",
80
80
  "@types/react-dom": "^19.0.0",
81
81
  "firebase": "^11.0.0",