diff --git a/lib/export_types.ts b/lib/export_types.ts index 759bb86c0..df11a89a8 100644 --- a/lib/export_types.ts +++ b/lib/export_types.ts @@ -1,5 +1,5 @@ /** - * Copyright 2022-2023, Optimizely + * Copyright 2022-2024, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,7 +39,6 @@ export { ListenerPayload, OptimizelyDecision, OptimizelyUserContext, - NotificationListener, Config, Client, ActivateListenerPayload, diff --git a/lib/notification_center/index.tests.js b/lib/notification_center/index.tests.js index e1459af41..2a398c4cf 100644 --- a/lib/notification_center/index.tests.js +++ b/lib/notification_center/index.tests.js @@ -1,18 +1,18 @@ -/**************************************************************************** - * Copyright 2020, Optimizely, Inc. and contributors * - * * - * Licensed under the Apache License, Version 2.0 (the "License"); * - * you may not use this file except in compliance with the License. * - * You may obtain a copy of the License at * - * * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an "AS IS" BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - ***************************************************************************/ +/** + * Copyright 2020, 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ import sinon from 'sinon'; import { assert } from 'chai'; @@ -20,6 +20,7 @@ import { createNotificationCenter } from './'; import * as enums from '../utils/enums'; import { createLogger } from '../plugins/logger'; import errorHandler from '../plugins/error_handler'; +import { NOTIFICATION_TYPES } from './type'; var LOG_LEVEL = enums.LOG_LEVEL; @@ -62,47 +63,6 @@ describe('lib/core/notification_center', function() { }); context('the listener type is a valid type', function() { - it('should return -1 if that same callback is already added', function() { - var activateCallback; - var decisionCallback; - var logEventCallback; - var configUpdateCallback; - var trackCallback; - // add a listener for each type - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.ACTIVATE, activateCallback); - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.DECISION, decisionCallback); - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.LOG_EVENT, logEventCallback); - notificationCenterInstance.addNotificationListener( - enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, - configUpdateCallback - ); - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.TRACK, trackCallback); - // assertions - assert.strictEqual( - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.ACTIVATE, activateCallback), - -1 - ); - assert.strictEqual( - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.DECISION, decisionCallback), - -1 - ); - assert.strictEqual( - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.LOG_EVENT, logEventCallback), - -1 - ); - assert.strictEqual( - notificationCenterInstance.addNotificationListener( - enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, - configUpdateCallback - ), - -1 - ); - assert.strictEqual( - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.TRACK, trackCallback), - -1 - ); - }); - it('should return an id (listenerId) > 0 of the notification listener if callback is not already added', function() { var activateCallback; var decisionCallback; @@ -111,23 +71,23 @@ describe('lib/core/notification_center', function() { var trackCallback; // store a listenerId for each type var activateListenerId = notificationCenterInstance.addNotificationListener( - enums.NOTIFICATION_TYPES.ACTIVATE, + NOTIFICATION_TYPES.ACTIVATE, activateCallback ); var decisionListenerId = notificationCenterInstance.addNotificationListener( - enums.NOTIFICATION_TYPES.DECISION, + NOTIFICATION_TYPES.DECISION, decisionCallback ); var logEventListenerId = notificationCenterInstance.addNotificationListener( - enums.NOTIFICATION_TYPES.LOG_EVENT, + NOTIFICATION_TYPES.LOG_EVENT, logEventCallback ); var configUpdateListenerId = notificationCenterInstance.addNotificationListener( - enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, configUpdateCallback ); var trackListenerId = notificationCenterInstance.addNotificationListener( - enums.NOTIFICATION_TYPES.TRACK, + NOTIFICATION_TYPES.TRACK, trackCallback ); // assertions @@ -157,23 +117,23 @@ describe('lib/core/notification_center', function() { var trackCallback; // add listeners for each type var activateListenerId = notificationCenterInstance.addNotificationListener( - enums.NOTIFICATION_TYPES.ACTIVATE, + NOTIFICATION_TYPES.ACTIVATE, activateCallback ); var decisionListenerId = notificationCenterInstance.addNotificationListener( - enums.NOTIFICATION_TYPES.DECISION, + NOTIFICATION_TYPES.DECISION, decisionCallback ); var logEventListenerId = notificationCenterInstance.addNotificationListener( - enums.NOTIFICATION_TYPES.LOG_EVENT, + NOTIFICATION_TYPES.LOG_EVENT, logEventCallback ); var configListenerId = notificationCenterInstance.addNotificationListener( - enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, configUpdateCallback ); var trackListenerId = notificationCenterInstance.addNotificationListener( - enums.NOTIFICATION_TYPES.TRACK, + NOTIFICATION_TYPES.TRACK, trackCallback ); // remove listeners for each type @@ -204,34 +164,34 @@ describe('lib/core/notification_center', function() { var trackCallbackSpy2 = sinon.spy(); // register listeners for each type var activateListenerId1 = notificationCenterInstance.addNotificationListener( - enums.NOTIFICATION_TYPES.ACTIVATE, + NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1 ); var decisionListenerId1 = notificationCenterInstance.addNotificationListener( - enums.NOTIFICATION_TYPES.DECISION, + NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1 ); var logeventlistenerId1 = notificationCenterInstance.addNotificationListener( - enums.NOTIFICATION_TYPES.LOG_EVENT, + NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1 ); var configUpdateListenerId1 = notificationCenterInstance.addNotificationListener( - enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, configUpdateCallbackSpy1 ); var trackListenerId1 = notificationCenterInstance.addNotificationListener( - enums.NOTIFICATION_TYPES.TRACK, + NOTIFICATION_TYPES.TRACK, trackCallbackSpy1 ); // register second listeners for each type - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy2); - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.DECISION, decisionCallbackSpy2); - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy2); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy2); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy2); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy2); notificationCenterInstance.addNotificationListener( - enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, configUpdateCallbackSpy2 ); - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.TRACK, trackCallbackSpy2); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy2); // remove first listener var activateListenerRemoved1 = notificationCenterInstance.removeNotificationListener(activateListenerId1); var decisionListenerRemoved1 = notificationCenterInstance.removeNotificationListener(decisionListenerId1); @@ -241,11 +201,11 @@ describe('lib/core/notification_center', function() { ); var trackListenerRemoved1 = notificationCenterInstance.removeNotificationListener(trackListenerId1); // send notifications - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.ACTIVATE, {}); - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.DECISION, {}); - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.LOG_EVENT, {}); - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, {}); - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.TRACK, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, {}); // Assertions assert.strictEqual(activateListenerRemoved1, true); sinon.assert.notCalled(activateCallbackSpy1); @@ -274,22 +234,22 @@ describe('lib/core/notification_center', function() { var configUpdateCallbackSpy1 = sinon.spy(); var trackCallbackSpy1 = sinon.spy(); // add a listener for each notification type - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); notificationCenterInstance.addNotificationListener( - enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, configUpdateCallbackSpy1 ); - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); // remove all listeners notificationCenterInstance.clearAllNotificationListeners(); // trigger send notifications - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.ACTIVATE, {}); - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.DECISION, {}); - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.LOG_EVENT, {}); - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, {}); - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.TRACK, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, {}); // check that none of the now removed listeners were called sinon.assert.notCalled(activateCallbackSpy1); sinon.assert.notCalled(decisionCallbackSpy1); @@ -305,12 +265,12 @@ describe('lib/core/notification_center', function() { var activateCallbackSpy1 = sinon.spy(); var activateCallbackSpy2 = sinon.spy(); //add 2 different listeners for ACTIVATE - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy2); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy2); // remove ACTIVATE listeners - notificationCenterInstance.clearNotificationListeners(enums.NOTIFICATION_TYPES.ACTIVATE); + notificationCenterInstance.clearNotificationListeners(NOTIFICATION_TYPES.ACTIVATE); // trigger send notifications - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.ACTIVATE, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, {}); // check that none of the ACTIVATE listeners were called sinon.assert.notCalled(activateCallbackSpy1); sinon.assert.notCalled(activateCallbackSpy2); @@ -320,12 +280,12 @@ describe('lib/core/notification_center', function() { var decisionCallbackSpy1 = sinon.spy(); var decisionCallbackSpy2 = sinon.spy(); //add 2 different listeners for DECISION - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.DECISION, decisionCallbackSpy2); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy2); // remove DECISION listeners - notificationCenterInstance.clearAllNotificationListeners(enums.NOTIFICATION_TYPES.DECISION); + notificationCenterInstance.clearAllNotificationListeners(NOTIFICATION_TYPES.DECISION); // trigger send notifications - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.DECISION, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, {}); // check that none of the DECISION listeners were called sinon.assert.notCalled(decisionCallbackSpy1); sinon.assert.notCalled(decisionCallbackSpy2); @@ -335,12 +295,12 @@ describe('lib/core/notification_center', function() { var logEventCallbackSpy1 = sinon.spy(); var logEventCallbackSpy2 = sinon.spy(); //add 2 different listeners for LOG_EVENT - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy2); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy2); // remove LOG_EVENT listeners - notificationCenterInstance.clearAllNotificationListeners(enums.NOTIFICATION_TYPES.LOG_EVENT); + notificationCenterInstance.clearAllNotificationListeners(NOTIFICATION_TYPES.LOG_EVENT); // trigger send notifications - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.LOG_EVENT, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, {}); // check that none of the LOG_EVENT listeners were called sinon.assert.notCalled(logEventCallbackSpy1); sinon.assert.notCalled(logEventCallbackSpy2); @@ -351,17 +311,17 @@ describe('lib/core/notification_center', function() { var configUpdateCallbackSpy2 = sinon.spy(); //add 2 different listeners for OPTIMIZELY_CONFIG_UPDATE notificationCenterInstance.addNotificationListener( - enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, configUpdateCallbackSpy1 ); notificationCenterInstance.addNotificationListener( - enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, configUpdateCallbackSpy2 ); // remove OPTIMIZELY_CONFIG_UPDATE listeners - notificationCenterInstance.clearAllNotificationListeners(enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE); + notificationCenterInstance.clearAllNotificationListeners(NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE); // trigger send notifications - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, {}); // check that none of the OPTIMIZELY_CONFIG_UPDATE listeners were called sinon.assert.notCalled(configUpdateCallbackSpy1); sinon.assert.notCalled(configUpdateCallbackSpy2); @@ -371,12 +331,12 @@ describe('lib/core/notification_center', function() { var trackCallbackSpy1 = sinon.spy(); var trackCallbackSpy2 = sinon.spy(); //add 2 different listeners for TRACK - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.TRACK, trackCallbackSpy2); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy2); // remove TRACK listeners - notificationCenterInstance.clearAllNotificationListeners(enums.NOTIFICATION_TYPES.TRACK); + notificationCenterInstance.clearAllNotificationListeners(NOTIFICATION_TYPES.TRACK); // trigger send notifications - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.TRACK, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, {}); // check that none of the TRACK listeners were called sinon.assert.notCalled(trackCallbackSpy1); sinon.assert.notCalled(trackCallbackSpy2); @@ -392,24 +352,24 @@ describe('lib/core/notification_center', function() { var configUpdateCallbackSpy1 = sinon.spy(); var trackCallbackSpy1 = sinon.spy(); //add 2 different listeners for ACTIVATE - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy2); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy2); // add a listener for each notification type - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); notificationCenterInstance.addNotificationListener( - enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, configUpdateCallbackSpy1 ); - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); // remove only ACTIVATE type - notificationCenterInstance.clearNotificationListeners(enums.NOTIFICATION_TYPES.ACTIVATE); + notificationCenterInstance.clearNotificationListeners(NOTIFICATION_TYPES.ACTIVATE); // trigger send notifications - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.ACTIVATE, {}); - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.DECISION, {}); - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.LOG_EVENT, {}); - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, {}); - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.TRACK, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, {}); // check that ACTIVATE listeners were note called sinon.assert.notCalled(activateCallbackSpy1); sinon.assert.notCalled(activateCallbackSpy2); @@ -428,24 +388,24 @@ describe('lib/core/notification_center', function() { var configUpdateCallbackSpy1 = sinon.spy(); var trackCallbackSpy1 = sinon.spy(); // add 2 different listeners for DECISION - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.DECISION, decisionCallbackSpy2); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy2); // add a listener for each notification type - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); notificationCenterInstance.addNotificationListener( - enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, configUpdateCallbackSpy1 ); - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); // remove only DECISION type - notificationCenterInstance.clearNotificationListeners(enums.NOTIFICATION_TYPES.DECISION); + notificationCenterInstance.clearNotificationListeners(NOTIFICATION_TYPES.DECISION); // trigger send notifications - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.ACTIVATE, {}); - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.DECISION, {}); - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.LOG_EVENT, {}); - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, {}); - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.TRACK, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, {}); // check that DECISION listeners were not called sinon.assert.notCalled(decisionCallbackSpy1); sinon.assert.notCalled(decisionCallbackSpy2); @@ -464,24 +424,24 @@ describe('lib/core/notification_center', function() { var configUpdateCallbackSpy1 = sinon.spy(); var trackCallbackSpy1 = sinon.spy(); // add 2 different listeners for LOG_EVENT - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy2); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy2); // add a listener for each notification type - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); notificationCenterInstance.addNotificationListener( - enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, configUpdateCallbackSpy1 ); - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); // remove only LOG_EVENT type - notificationCenterInstance.clearNotificationListeners(enums.NOTIFICATION_TYPES.LOG_EVENT); + notificationCenterInstance.clearNotificationListeners(NOTIFICATION_TYPES.LOG_EVENT); // trigger send notifications - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.ACTIVATE, {}); - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.DECISION, {}); - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.LOG_EVENT, {}); - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, {}); - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.TRACK, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, {}); // check that LOG_EVENT listeners were not called sinon.assert.notCalled(logEventCallbackSpy1); sinon.assert.notCalled(logEventCallbackSpy2); @@ -501,26 +461,26 @@ describe('lib/core/notification_center', function() { var trackCallbackSpy1 = sinon.spy(); // add 2 different listeners for OPTIMIZELY_CONFIG_UPDATE notificationCenterInstance.addNotificationListener( - enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, configUpdateCallbackSpy1 ); notificationCenterInstance.addNotificationListener( - enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, configUpdateCallbackSpy2 ); // add a listener for each notification type - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); // remove only OPTIMIZELY_CONFIG_UPDATE type - notificationCenterInstance.clearNotificationListeners(enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE); + notificationCenterInstance.clearNotificationListeners(NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE); // trigger send notifications - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.ACTIVATE, {}); - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.DECISION, {}); - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.LOG_EVENT, {}); - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, {}); - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.TRACK, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, {}); // check that OPTIMIZELY_CONFIG_UPDATE listeners were not called sinon.assert.notCalled(configUpdateCallbackSpy1); sinon.assert.notCalled(configUpdateCallbackSpy2); @@ -539,24 +499,24 @@ describe('lib/core/notification_center', function() { var logEventCallbackSpy1 = sinon.spy(); var configUpdateCallbackSpy1 = sinon.spy(); // add 2 different listeners for TRACK - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.TRACK, trackCallbackSpy2); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy2); // add a listener for each notification type - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); notificationCenterInstance.addNotificationListener( - enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, configUpdateCallbackSpy1 ); // remove only TRACK type - notificationCenterInstance.clearNotificationListeners(enums.NOTIFICATION_TYPES.TRACK); + notificationCenterInstance.clearNotificationListeners(NOTIFICATION_TYPES.TRACK); // trigger send notifications - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.ACTIVATE, {}); - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.DECISION, {}); - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.LOG_EVENT, {}); - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, {}); - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.TRACK, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, {}); // check that TRACK listeners were not called sinon.assert.notCalled(trackCallbackSpy1); sinon.assert.notCalled(trackCallbackSpy2); @@ -604,23 +564,23 @@ describe('lib/core/notification_center', function() { eventTags: {}, }; // add listeners - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); notificationCenterInstance.addNotificationListener( - enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, configUpdateCallbackSpy1 ); - notificationCenterInstance.addNotificationListener(enums.NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); // send notifications - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.ACTIVATE, activateData); - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.DECISION, decisionData); - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.LOG_EVENT, logEventData); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, activateData); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, decisionData); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, logEventData); notificationCenterInstance.sendNotifications( - enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, configUpdateData ); - notificationCenterInstance.sendNotifications(enums.NOTIFICATION_TYPES.TRACK, trackData); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, trackData); // assertions sinon.assert.calledWithExactly(activateCallbackSpy1, activateData); sinon.assert.calledWithExactly(decisionCallbackSpy1, decisionData); diff --git a/lib/notification_center/index.ts b/lib/notification_center/index.ts index ee2135104..d33c3fa2e 100644 --- a/lib/notification_center/index.ts +++ b/lib/notification_center/index.ts @@ -1,5 +1,5 @@ /** - * Copyright 2020, 2022, Optimizely + * Copyright 2020, 2022, 2024, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,29 +15,38 @@ */ import { LogHandler, ErrorHandler } from '../modules/logging'; import { objectValues } from '../utils/fns'; -import { NotificationListener, ListenerPayload } from '../shared_types'; import { LOG_LEVEL, LOG_MESSAGES, - NOTIFICATION_TYPES, } from '../utils/enums'; +import { NOTIFICATION_TYPES } from './type'; +import { NotificationType, NotificationPayload } from './type'; +import { Consumer, Fn } from '../utils/type'; +import { EventEmitter } from '../utils/event_emitter/event_emitter'; + const MODULE_NAME = 'NOTIFICATION_CENTER'; interface NotificationCenterOptions { logger: LogHandler; errorHandler: ErrorHandler; } - -interface ListenerEntry { - id: number; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - callback: (notificationData: any) => void; +export interface NotificationCenter { + addNotificationListener( + notificationType: N, + callback: Consumer + ): number + removeNotificationListener(listenerId: number): boolean; + clearAllNotificationListeners(): void; + clearNotificationListeners(notificationType: NotificationType): void; } -type NotificationListeners = { - [key: string]: ListenerEntry[]; +export interface NotificationSender { + sendNotifications( + notificationType: N, + notificationData: NotificationPayload[N] + ): void; } /** @@ -46,11 +55,13 @@ type NotificationListeners = { * - ACTIVATE: An impression event will be sent to Optimizely. * - TRACK a conversion event will be sent to Optimizely */ -export class NotificationCenter { +export class DefaultNotificationCenter implements NotificationCenter, NotificationSender { private logger: LogHandler; private errorHandler: ErrorHandler; - private notificationListeners: NotificationListeners; - private listenerId: number; + + private removerId = 1; + private eventEmitter: EventEmitter = new EventEmitter(); + private removers: Map = new Map(); /** * @constructor @@ -61,13 +72,6 @@ export class NotificationCenter { constructor(options: NotificationCenterOptions) { this.logger = options.logger; this.errorHandler = options.errorHandler; - this.notificationListeners = {}; - objectValues(NOTIFICATION_TYPES).forEach( - (notificationTypeEnum) => { - this.notificationListeners[notificationTypeEnum] = []; - } - ); - this.listenerId = 1; } /** @@ -80,47 +84,40 @@ export class NotificationCenter { * can happen if the first argument is not a valid notification type, or if the same callback * function was already added as a listener by a prior call to this function. */ - addNotificationListener( - notificationType: string, - callback: NotificationListener + addNotificationListener( + notificationType: N, + callback: Consumer ): number { - try { - const notificationTypeValues: string[] = objectValues(NOTIFICATION_TYPES); - const isNotificationTypeValid = notificationTypeValues.indexOf(notificationType) > -1; - if (!isNotificationTypeValid) { - return -1; - } - - if (!this.notificationListeners[notificationType]) { - this.notificationListeners[notificationType] = []; - } - - let callbackAlreadyAdded = false; - (this.notificationListeners[notificationType] || []).forEach( - (listenerEntry) => { - if (listenerEntry.callback === callback) { - callbackAlreadyAdded = true; - return; - } - }); - - if (callbackAlreadyAdded) { - return -1; - } - - this.notificationListeners[notificationType].push({ - id: this.listenerId, - callback: callback, - }); - - const returnId = this.listenerId; - this.listenerId += 1; - return returnId; - } catch (e: any) { - this.logger.log(LOG_LEVEL.ERROR, e.message); - this.errorHandler.handleError(e); + const notificationTypeValues: string[] = objectValues(NOTIFICATION_TYPES); + const isNotificationTypeValid = notificationTypeValues.indexOf(notificationType) > -1; + if (!isNotificationTypeValid) { return -1; } + + const returnId = this.removerId++; + const remover = this.eventEmitter.on( + notificationType, this.wrapWithErrorHandling(notificationType, callback)); + this.removers.set(returnId, remover); + return returnId; + } + + private wrapWithErrorHandling( + notificationType: N, + callback: Consumer + ): Consumer { + return (notificationData: NotificationPayload[N]) => { + try { + callback(notificationData); + } catch (ex: any) { + this.logger.log( + LOG_LEVEL.ERROR, + LOG_MESSAGES.NOTIFICATION_LISTENER_EXCEPTION, + MODULE_NAME, + notificationType, + ex.message, + ); + } + }; } /** @@ -130,103 +127,40 @@ export class NotificationCenter { * otherwise. */ removeNotificationListener(listenerId: number): boolean { - try { - let indexToRemove: number | undefined; - let typeToRemove: string | undefined; - - Object.keys(this.notificationListeners).some( - (notificationType) => { - const listenersForType = this.notificationListeners[notificationType]; - (listenersForType || []).every((listenerEntry, i) => { - if (listenerEntry.id === listenerId) { - indexToRemove = i; - typeToRemove = notificationType; - return false; - } - - return true; - }); - - if (indexToRemove !== undefined && typeToRemove !== undefined) { - return true; - } - - return false; - } - ); - - if (indexToRemove !== undefined && typeToRemove !== undefined) { - this.notificationListeners[typeToRemove].splice(indexToRemove, 1); - return true; - } - } catch (e: any) { - this.logger.log(LOG_LEVEL.ERROR, e.message); - this.errorHandler.handleError(e); + const remover = this.removers.get(listenerId); + if (remover) { + remover(); + return true; } - - return false; + return false } /** * Removes all previously added notification listeners, for all notification types */ clearAllNotificationListeners(): void { - try { - objectValues(NOTIFICATION_TYPES).forEach( - (notificationTypeEnum) => { - this.notificationListeners[notificationTypeEnum] = []; - } - ); - } catch (e: any) { - this.logger.log(LOG_LEVEL.ERROR, e.message); - this.errorHandler.handleError(e); - } + this.eventEmitter.removeAllListeners(); } /** * Remove all previously added notification listeners for the argument type - * @param {NOTIFICATION_TYPES} notificationType One of NOTIFICATION_TYPES + * @param {NotificationType} notificationType One of NotificationType */ - clearNotificationListeners(notificationType: NOTIFICATION_TYPES): void { - try { - this.notificationListeners[notificationType] = []; - } catch (e: any) { - this.logger.log(LOG_LEVEL.ERROR, e.message); - this.errorHandler.handleError(e); - } + clearNotificationListeners(notificationType: NotificationType): void { + this.eventEmitter.removeListeners(notificationType); } /** * Fires notifications for the argument type. All registered callbacks for this type will be * called. The notificationData object will be passed on to callbacks called. - * @param {string} notificationType One of NOTIFICATION_TYPES + * @param {NotificationType} notificationType One of NotificationType * @param {Object} notificationData Will be passed to callbacks called */ - sendNotifications( - notificationType: string, - notificationData?: T + sendNotifications( + notificationType: N, + notificationData: NotificationPayload[N] ): void { - try { - (this.notificationListeners[notificationType] || []).forEach( - (listenerEntry) => { - const callback = listenerEntry.callback; - try { - callback(notificationData); - } catch (ex: any) { - this.logger.log( - LOG_LEVEL.ERROR, - LOG_MESSAGES.NOTIFICATION_LISTENER_EXCEPTION, - MODULE_NAME, - notificationType, - ex.message, - ); - } - } - ); - } catch (e: any) { - this.logger.log(LOG_LEVEL.ERROR, e.message); - this.errorHandler.handleError(e); - } + this.eventEmitter.emit(notificationType, notificationData); } } @@ -235,12 +169,6 @@ export class NotificationCenter { * @param {NotificationCenterOptions} options * @returns {NotificationCenter} An instance of NotificationCenter */ -export function createNotificationCenter(options: NotificationCenterOptions): NotificationCenter { - return new NotificationCenter(options); -} - -export interface NotificationSender { - // TODO[OASIS-6649]: Don't use any type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - sendNotifications(notificationType: NOTIFICATION_TYPES, notificationData?: any): void +export function createNotificationCenter(options: NotificationCenterOptions): DefaultNotificationCenter { + return new DefaultNotificationCenter(options); } diff --git a/lib/notification_center/type.ts b/lib/notification_center/type.ts new file mode 100644 index 000000000..7dcc132ab --- /dev/null +++ b/lib/notification_center/type.ts @@ -0,0 +1,80 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { LogEvent } from '../event_processor/event_dispatcher/event_dispatcher'; +import { EventTags, Experiment, UserAttributes, Variation } from '../shared_types'; + +export type UserEventListenerPayload = { + userId: string; + attributes?: UserAttributes; +} + +export type ActivateListenerPayload = UserEventListenerPayload & { + experiment: Experiment | null; + variation: Variation | null; + logEvent: LogEvent; +} + +export type TrackListenerPayload = UserEventListenerPayload & { + eventKey: string; + eventTags?: EventTags; + logEvent: LogEvent; +} + +export const DECISION_NOTIFICATION_TYPES = { + AB_TEST: 'ab-test', + FEATURE: 'feature', + FEATURE_TEST: 'feature-test', + FEATURE_VARIABLE: 'feature-variable', + ALL_FEATURE_VARIABLES: 'all-feature-variables', + FLAG: 'flag', +} as const; + +export type DecisionNotificationType = typeof DECISION_NOTIFICATION_TYPES[keyof typeof DECISION_NOTIFICATION_TYPES]; + +// TODO: Add more specific types for decision info +export type OptimizelyDecisionInfo = Record; + +export type DecisionListenerPayload = UserEventListenerPayload & { + type: DecisionNotificationType; + decisionInfo: OptimizelyDecisionInfo; +} + +export type LogEventListenerPayload = LogEvent; + +export type OptimizelyConfigUpdateListenerPayload = undefined; + +export type NotificationPayload = { + ACTIVATE: ActivateListenerPayload; + DECISION: DecisionListenerPayload; + TRACK: TrackListenerPayload; + LOG_EVENT: LogEventListenerPayload; + OPTIMIZELY_CONFIG_UPDATE: OptimizelyConfigUpdateListenerPayload; +}; + +export type NotificationType = keyof NotificationPayload; + +export type NotificationTypeValues = { + [key in NotificationType]: key; +} + +export const NOTIFICATION_TYPES: NotificationTypeValues = { + ACTIVATE: 'ACTIVATE', + DECISION: 'DECISION', + LOG_EVENT: 'LOG_EVENT', + OPTIMIZELY_CONFIG_UPDATE: 'OPTIMIZELY_CONFIG_UPDATE', + TRACK: 'TRACK', +}; diff --git a/lib/optimizely/index.tests.js b/lib/optimizely/index.tests.js index e7fc378f7..187764625 100644 --- a/lib/optimizely/index.tests.js +++ b/lib/optimizely/index.tests.js @@ -16,7 +16,7 @@ import { assert, expect } from 'chai'; import sinon from 'sinon'; import { sprintf } from '../utils/fns'; -import { NOTIFICATION_TYPES } from '../utils/enums'; +import { NOTIFICATION_TYPES } from '../notification_center/type'; import * as logging from '../modules/logging'; import Optimizely from './'; @@ -37,13 +37,13 @@ import { getForwardingEventProcessor } from '../event_processor/forwarding_event import { createNotificationCenter } from '../notification_center'; import { createProjectConfig } from '../project_config/project_config'; import { getMockProjectConfigManager } from '../tests/mock/mock_project_config_manager'; +import { DECISION_NOTIFICATION_TYPES } from '../notification_center/type'; var ERROR_MESSAGES = enums.ERROR_MESSAGES; var LOG_LEVEL = enums.LOG_LEVEL; var LOG_MESSAGES = enums.LOG_MESSAGES; var DECISION_SOURCES = enums.DECISION_SOURCES; var DECISION_MESSAGES = enums.DECISION_MESSAGES; -var DECISION_NOTIFICATION_TYPES = enums.DECISION_NOTIFICATION_TYPES; var FEATURE_VARIABLE_TYPES = enums.FEATURE_VARIABLE_TYPES; var buildLogMessageFromArgs = args => sprintf(args[1], ...args.splice(2)); @@ -2316,14 +2316,14 @@ describe('lib/optimizely', function() { }); it('should call a listener added for activate when activate is called', function() { - optlyInstance.notificationCenter.addNotificationListener(enums.NOTIFICATION_TYPES.ACTIVATE, activateListener); + optlyInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateListener); var variationKey = optlyInstance.activate('testExperiment', 'testUser'); assert.strictEqual(variationKey, 'variation'); sinon.assert.calledOnce(activateListener); }); it('should call a listener added for track when track is called', function() { - optlyInstance.notificationCenter.addNotificationListener(enums.NOTIFICATION_TYPES.TRACK, trackListener); + optlyInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackListener); optlyInstance.activate('testExperiment', 'testUser'); optlyInstance.track('testEvent', 'testUser'); sinon.assert.calledOnce(trackListener); @@ -2331,7 +2331,7 @@ describe('lib/optimizely', function() { it('should not call a removed activate listener when activate is called', function() { var listenerId = optlyInstance.notificationCenter.addNotificationListener( - enums.NOTIFICATION_TYPES.ACTIVATE, + NOTIFICATION_TYPES.ACTIVATE, activateListener ); optlyInstance.notificationCenter.removeNotificationListener(listenerId); @@ -2342,7 +2342,7 @@ describe('lib/optimizely', function() { it('should not call a removed track listener when track is called', function() { var listenerId = optlyInstance.notificationCenter.addNotificationListener( - enums.NOTIFICATION_TYPES.TRACK, + NOTIFICATION_TYPES.TRACK, trackListener ); optlyInstance.notificationCenter.removeNotificationListener(listenerId); @@ -2352,9 +2352,9 @@ describe('lib/optimizely', function() { }); it('removeNotificationListener should only remove the listener with the argument ID', function() { - optlyInstance.notificationCenter.addNotificationListener(enums.NOTIFICATION_TYPES.ACTIVATE, activateListener); + optlyInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateListener); var trackListenerId = optlyInstance.notificationCenter.addNotificationListener( - enums.NOTIFICATION_TYPES.TRACK, + NOTIFICATION_TYPES.TRACK, trackListener ); optlyInstance.notificationCenter.removeNotificationListener(trackListenerId); @@ -2364,8 +2364,8 @@ describe('lib/optimizely', function() { }); it('should clear all notification listeners when clearAllNotificationListeners is called', function() { - optlyInstance.notificationCenter.addNotificationListener(enums.NOTIFICATION_TYPES.ACTIVATE, activateListener); - optlyInstance.notificationCenter.addNotificationListener(enums.NOTIFICATION_TYPES.TRACK, trackListener); + optlyInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateListener); + optlyInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackListener); optlyInstance.notificationCenter.clearAllNotificationListeners(); optlyInstance.activate('testExperiment', 'testUser'); optlyInstance.track('testEvent', 'testUser'); @@ -2375,9 +2375,9 @@ describe('lib/optimizely', function() { }); it('should clear listeners of certain notification type when clearNotificationListeners is called', function() { - optlyInstance.notificationCenter.addNotificationListener(enums.NOTIFICATION_TYPES.ACTIVATE, activateListener); - optlyInstance.notificationCenter.addNotificationListener(enums.NOTIFICATION_TYPES.TRACK, trackListener); - optlyInstance.notificationCenter.clearNotificationListeners(enums.NOTIFICATION_TYPES.ACTIVATE); + optlyInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateListener); + optlyInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackListener); + optlyInstance.notificationCenter.clearNotificationListeners(NOTIFICATION_TYPES.ACTIVATE); optlyInstance.activate('testExperiment', 'testUser'); optlyInstance.track('testEvent', 'testUser'); @@ -2385,13 +2385,6 @@ describe('lib/optimizely', function() { sinon.assert.calledOnce(trackListener); }); - it('should only call the listener once after the same listener was added twice', function() { - optlyInstance.notificationCenter.addNotificationListener(enums.NOTIFICATION_TYPES.ACTIVATE, activateListener); - optlyInstance.notificationCenter.addNotificationListener(enums.NOTIFICATION_TYPES.ACTIVATE, activateListener); - optlyInstance.activate('testExperiment', 'testUser'); - sinon.assert.calledOnce(activateListener); - }); - it('should not add a listener with an invalid type argument', function() { var listenerId = optlyInstance.notificationCenter.addNotificationListener( 'not a notification type', @@ -2405,16 +2398,16 @@ describe('lib/optimizely', function() { }); it('should call multiple notification listeners for activate when activate is called', function() { - optlyInstance.notificationCenter.addNotificationListener(enums.NOTIFICATION_TYPES.ACTIVATE, activateListener); - optlyInstance.notificationCenter.addNotificationListener(enums.NOTIFICATION_TYPES.ACTIVATE, activateListener2); + optlyInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateListener); + optlyInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateListener2); optlyInstance.activate('testExperiment', 'testUser'); sinon.assert.calledOnce(activateListener); sinon.assert.calledOnce(activateListener2); }); it('should call multiple notification listeners for track when track is called', function() { - optlyInstance.notificationCenter.addNotificationListener(enums.NOTIFICATION_TYPES.TRACK, trackListener); - optlyInstance.notificationCenter.addNotificationListener(enums.NOTIFICATION_TYPES.TRACK, trackListener2); + optlyInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackListener); + optlyInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackListener2); optlyInstance.activate('testExperiment', 'testUser'); optlyInstance.track('testEvent', 'testUser'); sinon.assert.calledOnce(trackListener); @@ -2422,7 +2415,7 @@ describe('lib/optimizely', function() { }); it('should pass the correct arguments to an activate listener when activate is called', function() { - optlyInstance.notificationCenter.addNotificationListener(enums.NOTIFICATION_TYPES.ACTIVATE, activateListener); + optlyInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateListener); optlyInstance.activate('testExperiment', 'testUser'); var expectedImpressionEvent = { httpVerb: 'POST', @@ -2484,7 +2477,7 @@ describe('lib/optimizely', function() { var attributes = { browser_type: 'firefox', }; - optlyInstance.notificationCenter.addNotificationListener(enums.NOTIFICATION_TYPES.ACTIVATE, activateListener); + optlyInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateListener); optlyInstance.activate('testExperiment', 'testUser', attributes); var expectedImpressionEvent = { httpVerb: 'POST', @@ -2550,7 +2543,7 @@ describe('lib/optimizely', function() { }); it('should pass the correct arguments to a track listener when track is called', function() { - optlyInstance.notificationCenter.addNotificationListener(enums.NOTIFICATION_TYPES.TRACK, trackListener); + optlyInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackListener); optlyInstance.activate('testExperiment', 'testUser'); optlyInstance.track('testEvent', 'testUser'); var expectedConversionEvent = { @@ -2598,7 +2591,7 @@ describe('lib/optimizely', function() { var attributes = { browser_type: 'firefox', }; - optlyInstance.notificationCenter.addNotificationListener(enums.NOTIFICATION_TYPES.TRACK, trackListener); + optlyInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackListener); optlyInstance.activate('testExperiment', 'testUser', attributes); optlyInstance.track('testEvent', 'testUser', attributes); var expectedConversionEvent = { @@ -2657,7 +2650,7 @@ describe('lib/optimizely', function() { value: 1.234, non_revenue: 'abc', }; - optlyInstance.notificationCenter.addNotificationListener(enums.NOTIFICATION_TYPES.TRACK, trackListener); + optlyInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackListener); optlyInstance.activate('testExperiment', 'testUser', attributes); optlyInstance.track('testEvent', 'testUser', attributes, eventTags); var expectedConversionEvent = { @@ -2738,7 +2731,7 @@ describe('lib/optimizely', function() { }); optlyInstance.notificationCenter.addNotificationListener( - enums.NOTIFICATION_TYPES.DECISION, + NOTIFICATION_TYPES.DECISION, decisionListener ); }); @@ -2802,7 +2795,7 @@ describe('lib/optimizely', function() { }); optlyInstance.notificationCenter.addNotificationListener( - enums.NOTIFICATION_TYPES.DECISION, + NOTIFICATION_TYPES.DECISION, decisionListener ); }); @@ -2857,7 +2850,7 @@ describe('lib/optimizely', function() { notificationCenter, }); - optly.notificationCenter.addNotificationListener(enums.NOTIFICATION_TYPES.DECISION, decisionListener); + optly.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionListener); fakeDecisionResponse = { result: '594099', @@ -2899,7 +2892,7 @@ describe('lib/optimizely', function() { }); optlyInstance.notificationCenter.addNotificationListener( - enums.NOTIFICATION_TYPES.DECISION, + NOTIFICATION_TYPES.DECISION, decisionListener ); }); @@ -6915,7 +6908,7 @@ describe('lib/optimizely', function() { var decisionListener = sinon.spy(); var attributes = { test_attribute: 'test_value' }; - optlyInstance.notificationCenter.addNotificationListener(enums.NOTIFICATION_TYPES.DECISION, decisionListener); + optlyInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionListener); var result = optlyInstance.getEnabledFeatures('test_user', attributes); assert.strictEqual(result.length, 5); assert.deepEqual(result, [ @@ -10163,7 +10156,7 @@ describe('lib/optimizely', function() { it('emits a notification when the project config manager emits a new project config object', function() { var listener = sinon.spy(); optlyInstance.notificationCenter.addNotificationListener( - enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, listener ); var newConfig = createProjectConfig(testData.getTestProjectConfigWithFeatures()); @@ -10214,7 +10207,7 @@ describe('lib/optimizely', function() { it('should trigger a log event notification when an impression event is dispatched', function() { var notificationListener = sinon.spy(); optlyInstance.notificationCenter.addNotificationListener( - enums.NOTIFICATION_TYPES.LOG_EVENT, + NOTIFICATION_TYPES.LOG_EVENT, notificationListener ); fakeDecisionResponse = { @@ -10232,7 +10225,7 @@ describe('lib/optimizely', function() { it('should trigger a log event notification when a conversion event is dispatched', function() { var notificationListener = sinon.spy(); optlyInstance.notificationCenter.addNotificationListener( - enums.NOTIFICATION_TYPES.LOG_EVENT, + NOTIFICATION_TYPES.LOG_EVENT, notificationListener ); optlyInstance.track('testEvent', 'testUser'); diff --git a/lib/optimizely/index.ts b/lib/optimizely/index.ts index 96ef632f3..7628a0a17 100644 --- a/lib/optimizely/index.ts +++ b/lib/optimizely/index.ts @@ -16,7 +16,7 @@ import { LoggerFacade, ErrorHandler } from '../modules/logging'; import { sprintf, objectValues } from '../utils/fns'; -import { NotificationCenter } from '../notification_center'; +import { DefaultNotificationCenter, NotificationCenter } from '../notification_center'; import { EventProcessor } from '../event_processor/event_processor'; import { IOdpManager } from '../odp/odp_manager'; @@ -59,8 +59,8 @@ import { DECISION_SOURCES, DECISION_MESSAGES, FEATURE_VARIABLE_TYPES, - DECISION_NOTIFICATION_TYPES, - NOTIFICATION_TYPES, + // DECISION_NOTIFICATION_TYPES, + // NOTIFICATION_TYPES, NODE_CLIENT_ENGINE, CLIENT_VERSION, ODP_DEFAULT_EVENT_TYPE, @@ -69,7 +69,8 @@ import { } from '../utils/enums'; import { Fn } from '../utils/type'; import { resolvablePromise } from '../utils/promise/resolvablePromise'; -import { time } from 'console'; + +import { NOTIFICATION_TYPES, DecisionNotificationType, DECISION_NOTIFICATION_TYPES } from '../notification_center/type'; const MODULE_NAME = 'OPTIMIZELY'; @@ -99,7 +100,7 @@ export default class Optimizely implements Client { private eventProcessor?: EventProcessor; private defaultDecideOptions: { [key: string]: boolean }; protected odpManager?: IOdpManager; - public notificationCenter: NotificationCenter; + public notificationCenter: DefaultNotificationCenter; constructor(config: OptimizelyOptions) { let clientEngine = config.clientEngine; @@ -142,7 +143,7 @@ export default class Optimizely implements Client { configObj.projectId ); - this.notificationCenter.sendNotifications(NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE); + this.notificationCenter.sendNotifications(NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, undefined); this.updateOdpSettings(); }); @@ -178,7 +179,7 @@ export default class Optimizely implements Client { Promise.resolve(undefined); this.eventProcessor?.onDispatch((event) => { - this.notificationCenter.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, event as any); + this.notificationCenter.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, event); }); this.readyPromise = Promise.all([ @@ -415,7 +416,7 @@ export default class Optimizely implements Client { experiment, this.createInternalUserContext(userId, attributes) as OptimizelyUserContext ).result; - const decisionNotificationType = projectConfig.isFeatureExperiment(configObj, experiment.id) + const decisionNotificationType: DecisionNotificationType = projectConfig.isFeatureExperiment(configObj, experiment.id) ? DECISION_NOTIFICATION_TYPES.FEATURE_TEST : DECISION_NOTIFICATION_TYPES.AB_TEST; diff --git a/lib/optimizely_user_context/index.tests.js b/lib/optimizely_user_context/index.tests.js index a895d928d..fc72ffe0e 100644 --- a/lib/optimizely_user_context/index.tests.js +++ b/lib/optimizely_user_context/index.tests.js @@ -19,7 +19,7 @@ import sinon from 'sinon'; import * as logging from '../modules/logging'; import { sprintf } from '../utils/fns'; -import { NOTIFICATION_TYPES } from '../utils/enums'; +import { NOTIFICATION_TYPES } from '../notification_center/type'; import OptimizelyUserContext from './'; import { createLogger } from '../plugins/logger'; diff --git a/lib/shared_types.ts b/lib/shared_types.ts index f021124ab..2cab1c052 100644 --- a/lib/shared_types.ts +++ b/lib/shared_types.ts @@ -21,8 +21,7 @@ import { ErrorHandler, LogHandler, LogLevel, LoggerFacade } from './modules/logging'; -import { NotificationCenter as NotificationCenterImpl } from './notification_center'; -import { NOTIFICATION_TYPES } from './utils/enums'; +import { NotificationCenter, DefaultNotificationCenter } from './notification_center'; import { IOptimizelyUserContext as OptimizelyUserContext } from './optimizely_user_context'; @@ -43,6 +42,8 @@ import { EventProcessor } from './event_processor/event_processor'; export { EventDispatcher } from './event_processor/event_dispatcher/event_dispatcher'; export { EventProcessor } from './event_processor/event_processor'; +export { NotificationCenter } from './notification_center'; + export interface BucketerParams { experimentId: string; experimentKey: string; @@ -120,19 +121,6 @@ export interface ListenerPayload { attributes?: UserAttributes; } -export type NotificationListener = (notificationData: T) => void; - -// NotificationCenter-related types -export interface NotificationCenter { - addNotificationListener( - notificationType: string, - callback: NotificationListener - ): number; - removeNotificationListener(listenerId: number): boolean; - clearAllNotificationListeners(): void; - clearNotificationListeners(notificationType: NOTIFICATION_TYPES): void; -} - // An event to be submitted to Optimizely, enabling tracking the reach and impact of // tests and feature rollouts. export interface Event { @@ -295,7 +283,7 @@ export interface OptimizelyOptions { defaultDecideOptions?: OptimizelyDecideOption[]; isSsr?:boolean; odpManager?: IOdpManager; - notificationCenter: NotificationCenterImpl; + notificationCenter: DefaultNotificationCenter; } /** diff --git a/lib/utils/enums/index.ts b/lib/utils/enums/index.ts index db8575729..10a5deb3f 100644 --- a/lib/utils/enums/index.ts +++ b/lib/utils/enums/index.ts @@ -285,55 +285,7 @@ export const DECISION_MESSAGES = { VARIABLE_VALUE_INVALID: 'Variable value for key "%s" is invalid or wrong type.', }; -/* - * Notification types for use with NotificationCenter - * Format is EVENT: - * - * SDK consumers can use these to register callbacks with the notification center. - * - * @deprecated since 3.1.0 - * ACTIVATE: An impression event will be sent to Optimizely - * Callbacks will receive an object argument with the following properties: - * - experiment {Object} - * - userId {string} - * - attributes {Object|undefined} - * - variation {Object} - * - logEvent {Object} - * - * DECISION: A decision is made in the system. i.e. user activation, - * feature access or feature-variable value retrieval - * Callbacks will receive an object argument with the following properties: - * - type {string} - * - userId {string} - * - attributes {Object|undefined} - * - decisionInfo {Object|undefined} - * - * LOG_EVENT: A batch of events, which could contain impressions and/or conversions, - * will be sent to Optimizely - * Callbacks will receive an object argument with the following properties: - * - url {string} - * - httpVerb {string} - * - params {Object} - * - * OPTIMIZELY_CONFIG_UPDATE: This Optimizely instance has been updated with a new - * config - * - * TRACK: A conversion event will be sent to Optimizely - * Callbacks will receive the an object argument with the following properties: - * - eventKey {string} - * - userId {string} - * - attributes {Object|undefined} - * - eventTags {Object|undefined} - * - logEvent {Object} - * - */ -export enum NOTIFICATION_TYPES { - ACTIVATE = 'ACTIVATE:experiment, user_id,attributes, variation, event', - DECISION = 'DECISION:type, userId, attributes, decisionInfo', - LOG_EVENT = 'LOG_EVENT:logEvent', - OPTIMIZELY_CONFIG_UPDATE = 'OPTIMIZELY_CONFIG_UPDATE', - TRACK = 'TRACK:event_key, user_id, attributes, event_tags, event', -} +export { NOTIFICATION_TYPES } from '../../notification_center/type'; /** * Default milliseconds before request timeout diff --git a/lib/utils/event_emitter/event_emitter.ts b/lib/utils/event_emitter/event_emitter.ts index 22b22be5d..6bfa57f8d 100644 --- a/lib/utils/event_emitter/event_emitter.ts +++ b/lib/utils/event_emitter/event_emitter.ts @@ -47,6 +47,10 @@ export class EventEmitter { } } + removeListeners(eventName: E): void { + this.listeners[eventName]?.clear(); + } + removeAllListeners(): void { this.listeners = {}; }