Skip to content

Commit e119925

Browse files
authored
[FSSDK-10989] refactor notification center using event emitter (#975)
1 parent c2880e9 commit e119925

File tree

10 files changed

+342
-437
lines changed

10 files changed

+342
-437
lines changed

lib/export_types.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2022-2023, Optimizely
2+
* Copyright 2022-2024, Optimizely
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -39,7 +39,6 @@ export {
3939
ListenerPayload,
4040
OptimizelyDecision,
4141
OptimizelyUserContext,
42-
NotificationListener,
4342
Config,
4443
Client,
4544
ActivateListenerPayload,

lib/notification_center/index.tests.js

Lines changed: 141 additions & 181 deletions
Large diffs are not rendered by default.

lib/notification_center/index.ts

Lines changed: 71 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2020, 2022, Optimizely
2+
* Copyright 2020, 2022, 2024, Optimizely
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,29 +15,38 @@
1515
*/
1616
import { LogHandler, ErrorHandler } from '../modules/logging';
1717
import { objectValues } from '../utils/fns';
18-
import { NotificationListener, ListenerPayload } from '../shared_types';
1918

2019
import {
2120
LOG_LEVEL,
2221
LOG_MESSAGES,
23-
NOTIFICATION_TYPES,
2422
} from '../utils/enums';
2523

24+
import { NOTIFICATION_TYPES } from './type';
25+
import { NotificationType, NotificationPayload } from './type';
26+
import { Consumer, Fn } from '../utils/type';
27+
import { EventEmitter } from '../utils/event_emitter/event_emitter';
28+
2629
const MODULE_NAME = 'NOTIFICATION_CENTER';
2730

2831
interface NotificationCenterOptions {
2932
logger: LogHandler;
3033
errorHandler: ErrorHandler;
3134
}
32-
33-
interface ListenerEntry {
34-
id: number;
35-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
36-
callback: (notificationData: any) => void;
35+
export interface NotificationCenter {
36+
addNotificationListener<N extends NotificationType>(
37+
notificationType: N,
38+
callback: Consumer<NotificationPayload[N]>
39+
): number
40+
removeNotificationListener(listenerId: number): boolean;
41+
clearAllNotificationListeners(): void;
42+
clearNotificationListeners(notificationType: NotificationType): void;
3743
}
3844

39-
type NotificationListeners = {
40-
[key: string]: ListenerEntry[];
45+
export interface NotificationSender {
46+
sendNotifications<N extends NotificationType>(
47+
notificationType: N,
48+
notificationData: NotificationPayload[N]
49+
): void;
4150
}
4251

4352
/**
@@ -46,11 +55,13 @@ type NotificationListeners = {
4655
* - ACTIVATE: An impression event will be sent to Optimizely.
4756
* - TRACK a conversion event will be sent to Optimizely
4857
*/
49-
export class NotificationCenter {
58+
export class DefaultNotificationCenter implements NotificationCenter, NotificationSender {
5059
private logger: LogHandler;
5160
private errorHandler: ErrorHandler;
52-
private notificationListeners: NotificationListeners;
53-
private listenerId: number;
61+
62+
private removerId = 1;
63+
private eventEmitter: EventEmitter<NotificationPayload> = new EventEmitter();
64+
private removers: Map<number, Fn> = new Map();
5465

5566
/**
5667
* @constructor
@@ -61,13 +72,6 @@ export class NotificationCenter {
6172
constructor(options: NotificationCenterOptions) {
6273
this.logger = options.logger;
6374
this.errorHandler = options.errorHandler;
64-
this.notificationListeners = {};
65-
objectValues(NOTIFICATION_TYPES).forEach(
66-
(notificationTypeEnum) => {
67-
this.notificationListeners[notificationTypeEnum] = [];
68-
}
69-
);
70-
this.listenerId = 1;
7175
}
7276

7377
/**
@@ -80,47 +84,40 @@ export class NotificationCenter {
8084
* can happen if the first argument is not a valid notification type, or if the same callback
8185
* function was already added as a listener by a prior call to this function.
8286
*/
83-
addNotificationListener<T extends ListenerPayload>(
84-
notificationType: string,
85-
callback: NotificationListener<T>
87+
addNotificationListener<N extends NotificationType>(
88+
notificationType: N,
89+
callback: Consumer<NotificationPayload[N]>
8690
): number {
87-
try {
88-
const notificationTypeValues: string[] = objectValues(NOTIFICATION_TYPES);
89-
const isNotificationTypeValid = notificationTypeValues.indexOf(notificationType) > -1;
90-
if (!isNotificationTypeValid) {
91-
return -1;
92-
}
93-
94-
if (!this.notificationListeners[notificationType]) {
95-
this.notificationListeners[notificationType] = [];
96-
}
97-
98-
let callbackAlreadyAdded = false;
99-
(this.notificationListeners[notificationType] || []).forEach(
100-
(listenerEntry) => {
101-
if (listenerEntry.callback === callback) {
102-
callbackAlreadyAdded = true;
103-
return;
104-
}
105-
});
106-
107-
if (callbackAlreadyAdded) {
108-
return -1;
109-
}
110-
111-
this.notificationListeners[notificationType].push({
112-
id: this.listenerId,
113-
callback: callback,
114-
});
115-
116-
const returnId = this.listenerId;
117-
this.listenerId += 1;
118-
return returnId;
119-
} catch (e: any) {
120-
this.logger.log(LOG_LEVEL.ERROR, e.message);
121-
this.errorHandler.handleError(e);
91+
const notificationTypeValues: string[] = objectValues(NOTIFICATION_TYPES);
92+
const isNotificationTypeValid = notificationTypeValues.indexOf(notificationType) > -1;
93+
if (!isNotificationTypeValid) {
12294
return -1;
12395
}
96+
97+
const returnId = this.removerId++;
98+
const remover = this.eventEmitter.on(
99+
notificationType, this.wrapWithErrorHandling(notificationType, callback));
100+
this.removers.set(returnId, remover);
101+
return returnId;
102+
}
103+
104+
private wrapWithErrorHandling<N extends NotificationType>(
105+
notificationType: N,
106+
callback: Consumer<NotificationPayload[N]>
107+
): Consumer<NotificationPayload[N]> {
108+
return (notificationData: NotificationPayload[N]) => {
109+
try {
110+
callback(notificationData);
111+
} catch (ex: any) {
112+
this.logger.log(
113+
LOG_LEVEL.ERROR,
114+
LOG_MESSAGES.NOTIFICATION_LISTENER_EXCEPTION,
115+
MODULE_NAME,
116+
notificationType,
117+
ex.message,
118+
);
119+
}
120+
};
124121
}
125122

126123
/**
@@ -130,103 +127,40 @@ export class NotificationCenter {
130127
* otherwise.
131128
*/
132129
removeNotificationListener(listenerId: number): boolean {
133-
try {
134-
let indexToRemove: number | undefined;
135-
let typeToRemove: string | undefined;
136-
137-
Object.keys(this.notificationListeners).some(
138-
(notificationType) => {
139-
const listenersForType = this.notificationListeners[notificationType];
140-
(listenersForType || []).every((listenerEntry, i) => {
141-
if (listenerEntry.id === listenerId) {
142-
indexToRemove = i;
143-
typeToRemove = notificationType;
144-
return false;
145-
}
146-
147-
return true;
148-
});
149-
150-
if (indexToRemove !== undefined && typeToRemove !== undefined) {
151-
return true;
152-
}
153-
154-
return false;
155-
}
156-
);
157-
158-
if (indexToRemove !== undefined && typeToRemove !== undefined) {
159-
this.notificationListeners[typeToRemove].splice(indexToRemove, 1);
160-
return true;
161-
}
162-
} catch (e: any) {
163-
this.logger.log(LOG_LEVEL.ERROR, e.message);
164-
this.errorHandler.handleError(e);
130+
const remover = this.removers.get(listenerId);
131+
if (remover) {
132+
remover();
133+
return true;
165134
}
166-
167-
return false;
135+
return false
168136
}
169137

170138
/**
171139
* Removes all previously added notification listeners, for all notification types
172140
*/
173141
clearAllNotificationListeners(): void {
174-
try {
175-
objectValues(NOTIFICATION_TYPES).forEach(
176-
(notificationTypeEnum) => {
177-
this.notificationListeners[notificationTypeEnum] = [];
178-
}
179-
);
180-
} catch (e: any) {
181-
this.logger.log(LOG_LEVEL.ERROR, e.message);
182-
this.errorHandler.handleError(e);
183-
}
142+
this.eventEmitter.removeAllListeners();
184143
}
185144

186145
/**
187146
* Remove all previously added notification listeners for the argument type
188-
* @param {NOTIFICATION_TYPES} notificationType One of NOTIFICATION_TYPES
147+
* @param {NotificationType} notificationType One of NotificationType
189148
*/
190-
clearNotificationListeners(notificationType: NOTIFICATION_TYPES): void {
191-
try {
192-
this.notificationListeners[notificationType] = [];
193-
} catch (e: any) {
194-
this.logger.log(LOG_LEVEL.ERROR, e.message);
195-
this.errorHandler.handleError(e);
196-
}
149+
clearNotificationListeners(notificationType: NotificationType): void {
150+
this.eventEmitter.removeListeners(notificationType);
197151
}
198152

199153
/**
200154
* Fires notifications for the argument type. All registered callbacks for this type will be
201155
* called. The notificationData object will be passed on to callbacks called.
202-
* @param {string} notificationType One of NOTIFICATION_TYPES
156+
* @param {NotificationType} notificationType One of NotificationType
203157
* @param {Object} notificationData Will be passed to callbacks called
204158
*/
205-
sendNotifications<T extends ListenerPayload>(
206-
notificationType: string,
207-
notificationData?: T
159+
sendNotifications<N extends NotificationType>(
160+
notificationType: N,
161+
notificationData: NotificationPayload[N]
208162
): void {
209-
try {
210-
(this.notificationListeners[notificationType] || []).forEach(
211-
(listenerEntry) => {
212-
const callback = listenerEntry.callback;
213-
try {
214-
callback(notificationData);
215-
} catch (ex: any) {
216-
this.logger.log(
217-
LOG_LEVEL.ERROR,
218-
LOG_MESSAGES.NOTIFICATION_LISTENER_EXCEPTION,
219-
MODULE_NAME,
220-
notificationType,
221-
ex.message,
222-
);
223-
}
224-
}
225-
);
226-
} catch (e: any) {
227-
this.logger.log(LOG_LEVEL.ERROR, e.message);
228-
this.errorHandler.handleError(e);
229-
}
163+
this.eventEmitter.emit(notificationType, notificationData);
230164
}
231165
}
232166

@@ -235,12 +169,6 @@ export class NotificationCenter {
235169
* @param {NotificationCenterOptions} options
236170
* @returns {NotificationCenter} An instance of NotificationCenter
237171
*/
238-
export function createNotificationCenter(options: NotificationCenterOptions): NotificationCenter {
239-
return new NotificationCenter(options);
240-
}
241-
242-
export interface NotificationSender {
243-
// TODO[OASIS-6649]: Don't use any type
244-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
245-
sendNotifications(notificationType: NOTIFICATION_TYPES, notificationData?: any): void
172+
export function createNotificationCenter(options: NotificationCenterOptions): DefaultNotificationCenter {
173+
return new DefaultNotificationCenter(options);
246174
}

lib/notification_center/type.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* Copyright 2024, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { LogEvent } from '../event_processor/event_dispatcher/event_dispatcher';
18+
import { EventTags, Experiment, UserAttributes, Variation } from '../shared_types';
19+
20+
export type UserEventListenerPayload = {
21+
userId: string;
22+
attributes?: UserAttributes;
23+
}
24+
25+
export type ActivateListenerPayload = UserEventListenerPayload & {
26+
experiment: Experiment | null;
27+
variation: Variation | null;
28+
logEvent: LogEvent;
29+
}
30+
31+
export type TrackListenerPayload = UserEventListenerPayload & {
32+
eventKey: string;
33+
eventTags?: EventTags;
34+
logEvent: LogEvent;
35+
}
36+
37+
export const DECISION_NOTIFICATION_TYPES = {
38+
AB_TEST: 'ab-test',
39+
FEATURE: 'feature',
40+
FEATURE_TEST: 'feature-test',
41+
FEATURE_VARIABLE: 'feature-variable',
42+
ALL_FEATURE_VARIABLES: 'all-feature-variables',
43+
FLAG: 'flag',
44+
} as const;
45+
46+
export type DecisionNotificationType = typeof DECISION_NOTIFICATION_TYPES[keyof typeof DECISION_NOTIFICATION_TYPES];
47+
48+
// TODO: Add more specific types for decision info
49+
export type OptimizelyDecisionInfo = Record<string, any>;
50+
51+
export type DecisionListenerPayload = UserEventListenerPayload & {
52+
type: DecisionNotificationType;
53+
decisionInfo: OptimizelyDecisionInfo;
54+
}
55+
56+
export type LogEventListenerPayload = LogEvent;
57+
58+
export type OptimizelyConfigUpdateListenerPayload = undefined;
59+
60+
export type NotificationPayload = {
61+
ACTIVATE: ActivateListenerPayload;
62+
DECISION: DecisionListenerPayload;
63+
TRACK: TrackListenerPayload;
64+
LOG_EVENT: LogEventListenerPayload;
65+
OPTIMIZELY_CONFIG_UPDATE: OptimizelyConfigUpdateListenerPayload;
66+
};
67+
68+
export type NotificationType = keyof NotificationPayload;
69+
70+
export type NotificationTypeValues = {
71+
[key in NotificationType]: key;
72+
}
73+
74+
export const NOTIFICATION_TYPES: NotificationTypeValues = {
75+
ACTIVATE: 'ACTIVATE',
76+
DECISION: 'DECISION',
77+
LOG_EVENT: 'LOG_EVENT',
78+
OPTIMIZELY_CONFIG_UPDATE: 'OPTIMIZELY_CONFIG_UPDATE',
79+
TRACK: 'TRACK',
80+
};

0 commit comments

Comments
 (0)