@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.
- package/.depcheckrc +14 -0
- package/.eslintrc.js +20 -0
- package/README.md +548 -0
- package/package.json +42 -0
- package/project.json +30 -0
- package/src/getIsNotificationServiceLocalOverrideEnabled.ts +7 -0
- package/src/global.d.ts +2 -0
- package/src/index.ts +41 -0
- package/src/notification-data-source/NotificationDataSource.ts +8 -0
- package/src/notification-data-source/getNotificationQueryOptions.ts +85 -0
- package/src/notification-data-source/implementations/createIntervalNotificationDataSource.ts +73 -0
- package/src/notification-data-source/implementations/createLocalTriggerDataSource.test.ts +492 -0
- package/src/notification-data-source/implementations/createLocalTriggerDataSource.ts +177 -0
- package/src/notification-data-source/implementations/createNotificationDataSource.ts +19 -0
- package/src/notification-data-source/implementations/createPollingNotificationDataSource.test.ts +398 -0
- package/src/notification-data-source/implementations/createPollingNotificationDataSource.ts +74 -0
- package/src/notification-data-source/implementations/createReactiveDataSource.ts +113 -0
- package/src/notification-data-source/types/ReactiveCondition.ts +60 -0
- package/src/notification-processor/NotificationProcessor.ts +26 -0
- package/src/notification-processor/implementations/createBaseNotificationProcessor.test.ts +854 -0
- package/src/notification-processor/implementations/createBaseNotificationProcessor.ts +239 -0
- package/src/notification-processor/implementations/createNotificationProcessor.test.ts +130 -0
- package/src/notification-processor/implementations/createNotificationProcessor.ts +15 -0
- package/src/notification-renderer/NotificationRenderer.ts +8 -0
- package/src/notification-renderer/components/BannerTemplate.tsx +188 -0
- package/src/notification-renderer/components/InlineBannerNotification.tsx +123 -0
- package/src/notification-renderer/implementations/createNotificationRenderer.ts +16 -0
- package/src/notification-renderer/utils/iconUtils.ts +103 -0
- package/src/notification-service/NotificationService.ts +47 -0
- package/src/notification-service/implementations/createNotificationService.test.ts +1092 -0
- package/src/notification-service/implementations/createNotificationService.ts +364 -0
- package/src/notification-telemetry/NotificationTelemetry.ts +44 -0
- package/src/notification-telemetry/implementations/createNotificationTelemetry.test.ts +99 -0
- package/src/notification-telemetry/implementations/createNotificationTelemetry.ts +33 -0
- package/src/notification-tracker/NotificationTracker.ts +14 -0
- package/src/notification-tracker/implementations/createApiNotificationTracker.test.ts +465 -0
- package/src/notification-tracker/implementations/createApiNotificationTracker.ts +154 -0
- package/src/notification-tracker/implementations/createNoopNotificationTracker.ts +44 -0
- package/src/notification-tracker/implementations/createNotificationTracker.ts +31 -0
- package/src/utils/formatNotificationType.test.ts +25 -0
- package/src/utils/formatNotificationType.ts +25 -0
- package/tsconfig.json +24 -0
- package/tsconfig.lint.json +8 -0
- package/vitest-setup.ts +1 -0
- 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
|
+
}
|