@luxexchange/notifications 1.0.0

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.
Files changed (45) hide show
  1. package/.depcheckrc +14 -0
  2. package/.eslintrc.js +20 -0
  3. package/README.md +548 -0
  4. package/package.json +42 -0
  5. package/project.json +30 -0
  6. package/src/getIsNotificationServiceLocalOverrideEnabled.ts +7 -0
  7. package/src/global.d.ts +2 -0
  8. package/src/index.ts +41 -0
  9. package/src/notification-data-source/NotificationDataSource.ts +8 -0
  10. package/src/notification-data-source/getNotificationQueryOptions.ts +85 -0
  11. package/src/notification-data-source/implementations/createIntervalNotificationDataSource.ts +73 -0
  12. package/src/notification-data-source/implementations/createLocalTriggerDataSource.test.ts +492 -0
  13. package/src/notification-data-source/implementations/createLocalTriggerDataSource.ts +177 -0
  14. package/src/notification-data-source/implementations/createNotificationDataSource.ts +19 -0
  15. package/src/notification-data-source/implementations/createPollingNotificationDataSource.test.ts +398 -0
  16. package/src/notification-data-source/implementations/createPollingNotificationDataSource.ts +74 -0
  17. package/src/notification-data-source/implementations/createReactiveDataSource.ts +113 -0
  18. package/src/notification-data-source/types/ReactiveCondition.ts +60 -0
  19. package/src/notification-processor/NotificationProcessor.ts +26 -0
  20. package/src/notification-processor/implementations/createBaseNotificationProcessor.test.ts +854 -0
  21. package/src/notification-processor/implementations/createBaseNotificationProcessor.ts +239 -0
  22. package/src/notification-processor/implementations/createNotificationProcessor.test.ts +130 -0
  23. package/src/notification-processor/implementations/createNotificationProcessor.ts +15 -0
  24. package/src/notification-renderer/NotificationRenderer.ts +8 -0
  25. package/src/notification-renderer/components/BannerTemplate.tsx +188 -0
  26. package/src/notification-renderer/components/InlineBannerNotification.tsx +123 -0
  27. package/src/notification-renderer/implementations/createNotificationRenderer.ts +16 -0
  28. package/src/notification-renderer/utils/iconUtils.ts +103 -0
  29. package/src/notification-service/NotificationService.ts +47 -0
  30. package/src/notification-service/implementations/createNotificationService.test.ts +1092 -0
  31. package/src/notification-service/implementations/createNotificationService.ts +364 -0
  32. package/src/notification-telemetry/NotificationTelemetry.ts +44 -0
  33. package/src/notification-telemetry/implementations/createNotificationTelemetry.test.ts +99 -0
  34. package/src/notification-telemetry/implementations/createNotificationTelemetry.ts +33 -0
  35. package/src/notification-tracker/NotificationTracker.ts +14 -0
  36. package/src/notification-tracker/implementations/createApiNotificationTracker.test.ts +465 -0
  37. package/src/notification-tracker/implementations/createApiNotificationTracker.ts +154 -0
  38. package/src/notification-tracker/implementations/createNoopNotificationTracker.ts +44 -0
  39. package/src/notification-tracker/implementations/createNotificationTracker.ts +31 -0
  40. package/src/utils/formatNotificationType.test.ts +25 -0
  41. package/src/utils/formatNotificationType.ts +25 -0
  42. package/tsconfig.json +24 -0
  43. package/tsconfig.lint.json +8 -0
  44. package/vitest-setup.ts +1 -0
  45. package/vitest.config.ts +14 -0
@@ -0,0 +1,113 @@
1
+ import { type InAppNotification } from '@luxexchange/api'
2
+ import { createNotificationDataSource } from '@luxexchange/notifications/src/notification-data-source/implementations/createNotificationDataSource'
3
+ import { type NotificationDataSource } from '@luxexchange/notifications/src/notification-data-source/NotificationDataSource'
4
+ import { type ReactiveCondition } from '@luxexchange/notifications/src/notification-data-source/types/ReactiveCondition'
5
+ import { type NotificationTracker } from '@luxexchange/notifications/src/notification-tracker/NotificationTracker'
6
+ import { getLogger } from '@luxfi/utilities/src/logger/logger'
7
+
8
+ export interface CreateReactiveDataSourceContext<TState> {
9
+ /** The reactive condition that determines when to show the notification */
10
+ condition: ReactiveCondition<TState>
11
+
12
+ /** Tracker for checking/storing processed state */
13
+ tracker: NotificationTracker
14
+
15
+ /** Source identifier for telemetry (default: 'reactive') */
16
+ source?: string
17
+
18
+ /** File tag for logging (default: 'createReactiveDataSource') */
19
+ logFileTag?: string
20
+ }
21
+
22
+ const DEFAULT_SOURCE = 'reactive'
23
+ const DEFAULT_LOG_FILE_TAG = 'createReactiveDataSource'
24
+
25
+ /**
26
+ * Creates a data source for reactive, state-driven notifications.
27
+ *
28
+ * Unlike polling-based data sources, this subscribes to state changes and
29
+ * immediately re-evaluates whether to show/hide the notification. The notification
30
+ * is emitted when shouldShow returns true and removed when it returns false.
31
+ *
32
+ * Key behaviors:
33
+ * - Subscribes to condition's state changes on start()
34
+ * - When state changes, checks shouldShow(state)
35
+ * - Emits [notification] when shouldShow is true and not already processed
36
+ * - Emits [] when shouldShow is false (hides the notification)
37
+ * - Checks tracker.isProcessed to prevent showing dismissed notifications
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * const offlineDataSource = createReactiveDataSource({
42
+ * condition: createOfflineCondition({ getState }),
43
+ * tracker,
44
+ * })
45
+ *
46
+ * notificationService.registerDataSource(offlineDataSource)
47
+ * ```
48
+ */
49
+ export function createReactiveDataSource<TState>(ctx: CreateReactiveDataSourceContext<TState>): NotificationDataSource {
50
+ const { condition, tracker, source = DEFAULT_SOURCE, logFileTag = DEFAULT_LOG_FILE_TAG } = ctx
51
+
52
+ let unsubscribe: (() => void) | null = null
53
+ let currentCallback: ((notifications: InAppNotification[], source: string) => void) | null = null
54
+
55
+ const emitNotifications = async (state: TState): Promise<void> => {
56
+ if (!currentCallback) {
57
+ return
58
+ }
59
+
60
+ try {
61
+ // Check if notification was already dismissed/processed
62
+ const isProcessed = await tracker.isProcessed(condition.notificationId)
63
+ if (isProcessed) {
64
+ // Already dismissed, emit empty array
65
+ currentCallback([], source)
66
+ return
67
+ }
68
+
69
+ // Evaluate the condition
70
+ const shouldShow = condition.shouldShow(state)
71
+
72
+ if (shouldShow) {
73
+ const notification = condition.createNotification(state)
74
+ currentCallback([notification], source)
75
+ } else {
76
+ // Condition not met, emit empty array to hide
77
+ currentCallback([], source)
78
+ }
79
+ } catch (error) {
80
+ getLogger().error(error, {
81
+ tags: { file: logFileTag, function: 'emitNotifications' },
82
+ extra: { notificationId: condition.notificationId },
83
+ })
84
+ }
85
+ }
86
+
87
+ const start = (onNotifications: (notifications: InAppNotification[], source: string) => void): void => {
88
+ if (unsubscribe) {
89
+ return
90
+ }
91
+
92
+ currentCallback = onNotifications
93
+
94
+ // Subscribe to state changes
95
+ unsubscribe = condition.subscribe((state: TState) => {
96
+ emitNotifications(state).catch((error) => {
97
+ getLogger().error(error, {
98
+ tags: { file: logFileTag, function: 'subscribe' },
99
+ })
100
+ })
101
+ })
102
+ }
103
+
104
+ const stop = async (): Promise<void> => {
105
+ if (unsubscribe) {
106
+ unsubscribe()
107
+ unsubscribe = null
108
+ }
109
+ currentCallback = null
110
+ }
111
+
112
+ return createNotificationDataSource({ start, stop })
113
+ }
@@ -0,0 +1,60 @@
1
+ import { type InAppNotification } from '@luxexchange/api'
2
+
3
+ /**
4
+ * A reactive condition for state-driven notifications.
5
+ *
6
+ * Unlike polling-based TriggerCondition, ReactiveCondition uses push-based updates
7
+ * via a subscribe mechanism. The data source subscribes to state changes and
8
+ * immediately re-evaluates whether to show/hide the notification.
9
+ *
10
+ * This is ideal for conditions that:
11
+ * - Need instant response to state changes (e.g., network status)
12
+ * - Already have observable state (e.g., NetInfo, Redux store subscriptions)
13
+ * - Should show/hide without polling delay
14
+ *
15
+ * @template TState - The shape of the state that drives the condition
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * const offlineCondition: ReactiveCondition<OfflineState> = {
20
+ * notificationId: 'local:session:offline',
21
+ * subscribe: (onStateChange) => {
22
+ * return NetInfo.addEventListener((state) => {
23
+ * onStateChange({ isConnected: state.isConnected })
24
+ * })
25
+ * },
26
+ * shouldShow: (state) => state.isConnected === false,
27
+ * createNotification: (state) => new Notification({ ... })
28
+ * }
29
+ * ```
30
+ */
31
+ export interface ReactiveCondition<TState> {
32
+ /**
33
+ * Unique notification ID.
34
+ * Must use 'local:' prefix to distinguish from backend-generated notifications.
35
+ * Use 'local:session:' prefix for session-scoped notifications that reset on app restart.
36
+ */
37
+ notificationId: string
38
+
39
+ /**
40
+ * Subscribe to state changes.
41
+ * @param onStateChange - Callback to invoke when state changes
42
+ * @returns Unsubscribe function to stop receiving updates
43
+ */
44
+ subscribe: (onStateChange: (state: TState) => void) => () => void
45
+
46
+ /**
47
+ * Check if the notification should be shown based on current state.
48
+ * @param state - The current state
49
+ * @returns true if the notification should be visible
50
+ */
51
+ shouldShow: (state: TState) => boolean
52
+
53
+ /**
54
+ * Create the notification object to be rendered.
55
+ * Called when shouldShow returns true.
56
+ * @param state - The current state (may be useful for dynamic notification content)
57
+ * @returns The notification to display
58
+ */
59
+ createNotification: (state: TState) => InAppNotification
60
+ }
@@ -0,0 +1,26 @@
1
+ import { type InAppNotification } from '@luxfi/api'
2
+
3
+ /**
4
+ * Result of processing notifications, separating primary notifications
5
+ * from chained notifications that should be shown later
6
+ */
7
+ export interface NotificationProcessorResult {
8
+ /**
9
+ * Primary notifications that should be rendered immediately
10
+ */
11
+ primary: InAppNotification[]
12
+
13
+ /**
14
+ * Chained notifications that should be stored for later triggering
15
+ */
16
+ chained: Map<string, InAppNotification>
17
+ }
18
+
19
+ export interface NotificationProcessor {
20
+ /**
21
+ * Process incoming notifications against current state
22
+ * Separates primary notifications (to be shown immediately) from chained notifications
23
+ * (to be shown when triggered by another notification's POPUP action)
24
+ */
25
+ process(notifications: InAppNotification[]): Promise<NotificationProcessorResult>
26
+ }