Skip to content

Commit f6d71e1

Browse files
authored
Add Notification Registry for ODP Setting Updates (#795)
1 parent 0d114f6 commit f6d71e1

File tree

18 files changed

+300
-134
lines changed

18 files changed

+300
-134
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* Copyright 2023, 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+
* https://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 { describe, it } from 'mocha';
18+
import { expect } from 'chai';
19+
20+
import { NotificationRegistry } from './notification_registry';
21+
22+
describe('Notification Registry', () => {
23+
it('Returns null notification center when SDK Key is null', () => {
24+
const notificationCenter = NotificationRegistry.getNotificationCenter();
25+
expect(notificationCenter).to.be.undefined;
26+
});
27+
28+
it('Returns the same notification center when SDK Keys are the same and not null', () => {
29+
const sdkKey = 'testSDKKey';
30+
const notificationCenterA = NotificationRegistry.getNotificationCenter(sdkKey);
31+
const notificationCenterB = NotificationRegistry.getNotificationCenter(sdkKey);
32+
expect(notificationCenterA).to.eql(notificationCenterB);
33+
});
34+
35+
it('Returns different notification centers when SDK Keys are not the same', () => {
36+
const sdkKeyA = 'testSDKKeyA';
37+
const sdkKeyB = 'testSDKKeyB';
38+
const notificationCenterA = NotificationRegistry.getNotificationCenter(sdkKeyA);
39+
const notificationCenterB = NotificationRegistry.getNotificationCenter(sdkKeyB);
40+
expect(notificationCenterA).to.not.eql(notificationCenterB);
41+
});
42+
43+
it('Removes old notification centers from the registry when removeNotificationCenter is called on the registry', () => {
44+
const sdkKey = 'testSDKKey';
45+
const notificationCenterA = NotificationRegistry.getNotificationCenter(sdkKey);
46+
NotificationRegistry.removeNotificationCenter(sdkKey);
47+
48+
const notificationCenterB = NotificationRegistry.getNotificationCenter(sdkKey);
49+
50+
expect(notificationCenterA).to.not.eql(notificationCenterB);
51+
});
52+
53+
it('Does not throw an error when calling removeNotificationCenter with a null SDK Key', () => {
54+
const sdkKey = 'testSDKKey';
55+
const notificationCenterA = NotificationRegistry.getNotificationCenter(sdkKey);
56+
NotificationRegistry.removeNotificationCenter();
57+
58+
const notificationCenterB = NotificationRegistry.getNotificationCenter(sdkKey);
59+
60+
expect(notificationCenterA).to.eql(notificationCenterB);
61+
});
62+
});
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* Copyright 2023, 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 { getLogger, LogHandler, LogLevel } from '../../modules/logging';
18+
import { NotificationCenter, createNotificationCenter } from '../../core/notification_center';
19+
20+
/**
21+
* Internal notification center registry for managing multiple notification centers.
22+
*/
23+
export class NotificationRegistry {
24+
private static _notificationCenters = new Map<string, NotificationCenter>();
25+
26+
constructor() {}
27+
28+
/**
29+
* Retrieves an SDK Key's corresponding notification center in the registry if it exists, otherwise it creates one
30+
* @param sdkKey SDK Key to be used for the notification center tied to the ODP Manager
31+
* @param logger Logger to be used for the corresponding notification center
32+
* @returns {NotificationCenter | undefined} a notification center instance for ODP Manager if a valid SDK Key is provided, otherwise undefined
33+
*/
34+
public static getNotificationCenter(
35+
sdkKey?: string,
36+
logger: LogHandler = getLogger()
37+
): NotificationCenter | undefined {
38+
if (!sdkKey) {
39+
logger.log(LogLevel.ERROR, 'No SDK key provided to getNotificationCenter.');
40+
return undefined;
41+
}
42+
43+
let notificationCenter;
44+
if (this._notificationCenters.has(sdkKey)) {
45+
notificationCenter = this._notificationCenters.get(sdkKey);
46+
} else {
47+
notificationCenter = createNotificationCenter({
48+
logger,
49+
errorHandler: { handleError: () => {} },
50+
});
51+
this._notificationCenters.set(sdkKey, notificationCenter);
52+
}
53+
54+
return notificationCenter;
55+
}
56+
57+
public static removeNotificationCenter(sdkKey?: string): void {
58+
if (!sdkKey) {
59+
return;
60+
}
61+
62+
const notificationCenter = this._notificationCenters.get(sdkKey);
63+
if (notificationCenter) {
64+
notificationCenter.clearAllNotificationListeners();
65+
this._notificationCenters.delete(sdkKey);
66+
}
67+
}
68+
}

packages/optimizely-sdk/lib/core/odp/odp_config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
* limitations under the License.
1515
*/
1616

17+
import { checkArrayEquality } from '../../../lib/utils/fns';
18+
1719
export class OdpConfig {
1820
/**
1921
* Host of ODP audience segments API.
@@ -98,7 +100,7 @@ export class OdpConfig {
98100
return (
99101
this._apiHost == config._apiHost &&
100102
this._apiKey == config._apiKey &&
101-
JSON.stringify(this.segmentsToCheck) == JSON.stringify(config._segmentsToCheck)
103+
checkArrayEquality(this.segmentsToCheck, config._segmentsToCheck)
102104
);
103105
}
104106
}

packages/optimizely-sdk/lib/core/odp/odp_manager.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import { OdpEventApiManager } from './odp_event_api_manager';
2929
import { OptimizelySegmentOption } from './optimizely_segment_option';
3030
import { areOdpDataTypesValid } from './odp_types';
3131
import { OdpEvent } from './odp_event';
32-
import { VuidManager } from '../../plugins/vuid_manager';
3332

3433
// Orchestrates segments manager, event manager, and ODP configuration
3534
export class OdpManager {
@@ -39,6 +38,8 @@ export class OdpManager {
3938
odpConfig: OdpConfig;
4039
logger: LogHandler;
4140

41+
// Note: VuidManager only utilized in Browser variation at /plugins/odp_manager/index.browser.ts
42+
4243
/**
4344
* ODP Segment Manager which provides an interface to the remote ODP server (GraphQL API) for audience segments mapping.
4445
* It fetches all qualified segments for the given user context and manages the segments cache for all user contexts.
@@ -107,9 +108,8 @@ export class OdpManager {
107108
}
108109

109110
// Set up Events Manager (Events REST API Interface)
110-
if (eventManager) {
111-
eventManager.updateSettings(this.odpConfig);
112-
this._eventManager = eventManager;
111+
if (this._eventManager) {
112+
this._eventManager.updateSettings(this.odpConfig);
113113
} else {
114114
this._eventManager = new OdpEventManager({
115115
odpConfig: this.odpConfig,
@@ -154,9 +154,10 @@ export class OdpManager {
154154
/**
155155
* Attempts to fetch and return a list of a user's qualified segments from the local segments cache.
156156
* If no cached data exists for the target user, this fetches and caches data from the ODP server instead.
157-
* @param userId Unique identifier of a target user.
158-
* @param options An array of OptimizelySegmentOption used to ignore and/or reset the cache.
159-
* @returns
157+
* @param {ODP_USER_KEY} userKey - Identifies the user id type.
158+
* @param {string} userId - Unique identifier of a target user.
159+
* @param {Array<OptimizelySegmentOption} options - An array of OptimizelySegmentOption used to ignore and/or reset the cache.
160+
* @returns {Promise<string[] | null>} A promise holding either a list of qualified segments or null.
160161
*/
161162
public async fetchQualifiedSegments(
162163
userKey: ODP_USER_KEY,

packages/optimizely-sdk/lib/core/odp/odp_segment_manager.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@ import { OptimizelySegmentOption } from './optimizely_segment_option';
2424
// Schedules connections to ODP for audience segmentation and caches the results.
2525
export class OdpSegmentManager {
2626
odpConfig: OdpConfig;
27-
segmentsCache: LRUCache<string, Array<string>>;
27+
segmentsCache: LRUCache<string, string[]>;
2828
odpSegmentApiManager: OdpSegmentApiManager;
2929
logger: LogHandler;
3030

3131
constructor(
3232
odpConfig: OdpConfig,
33-
segmentsCache: LRUCache<string, Array<string>>,
33+
segmentsCache: LRUCache<string, string[]>,
3434
odpSegmentApiManager: OdpSegmentApiManager,
3535
logger?: LogHandler
3636
) {
@@ -52,7 +52,7 @@ export class OdpSegmentManager {
5252
userKey: ODP_USER_KEY,
5353
userValue: string,
5454
options: Array<OptimizelySegmentOption>
55-
): Promise<Array<string> | null> {
55+
): Promise<string[] | null> {
5656
const { apiHost: odpApiHost, apiKey: odpApiKey } = this.odpConfig;
5757

5858
if (!odpApiKey || !odpApiHost) {

packages/optimizely-sdk/lib/core/project_config/index.tests.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
/**
2-
* Copyright 2016-2022, Optimizely
2+
* Copyright 2016-2023, 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.
66
* You may obtain a copy of the License at
77
*
8-
* http://www.apache.org/licenses/LICENSE-2.0
8+
* https://www.apache.org/licenses/LICENSE-2.0
99
*
1010
* Unless required by applicable law or agreed to in writing, software
1111
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -836,8 +836,8 @@ describe('lib/core/project_config', function () {
836836
})
837837

838838
it('should contain all expected unique odp segments in allSegments', () => {
839-
assert.equal(config.allSegments.size, 3)
840-
assert.deepEqual(config.allSegments, new Set(['odp-segment-1', 'odp-segment-2', 'odp-segment-3']))
839+
assert.equal(config.allSegments.length, 3)
840+
assert.deepEqual(config.allSegments, ['odp-segment-1', 'odp-segment-2', 'odp-segment-3'])
841841
})
842842
});
843843

@@ -863,7 +863,7 @@ describe('lib/core/project_config', function () {
863863
})
864864

865865
it('should contain all expected unique odp segments in all segments', () => {
866-
assert.equal(config.allSegments.size, 0)
866+
assert.equal(config.allSegments.length, 0)
867867
})
868868
});
869869

packages/optimizely-sdk/lib/core/project_config/index.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
/**
2-
* Copyright 2016-2022, Optimizely
2+
* Copyright 2016-2023, 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.
66
* You may obtain a copy of the License at
77
*
8-
* http://www.apache.org/licenses/LICENSE-2.0
8+
* https://www.apache.org/licenses/LICENSE-2.0
99
*
1010
* Unless required by applicable law or agreed to in writing, software
1111
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -103,7 +103,7 @@ export interface ProjectConfig {
103103
integrationKeyMap?: { [key: string]: Integration };
104104
publicKeyForOdp?: string;
105105
hostForOdp?: string;
106-
allSegments: Set<string>;
106+
allSegments: string[];
107107
}
108108

109109
const EXPERIMENT_RUNNING_STATUS = 'Running';
@@ -167,13 +167,13 @@ export const createProjectConfig = function (
167167
projectConfig.audiencesById = keyBy(projectConfig.audiences, 'id');
168168
assign(projectConfig.audiencesById, keyBy(projectConfig.typedAudiences, 'id'));
169169

170-
projectConfig.allSegments = new Set<string>([])
170+
projectConfig.allSegments = []
171171

172172
Object.keys(projectConfig.audiencesById)
173173
.map((audience) => getAudienceSegments(projectConfig.audiencesById[audience]))
174174
.forEach(audienceSegments => {
175175
audienceSegments.forEach(segment => {
176-
projectConfig.allSegments.add(segment)
176+
projectConfig.allSegments.push(segment)
177177
})
178178
})
179179

packages/optimizely-sdk/lib/index.browser.tests.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2016-2020, 2022 Optimizely
2+
* Copyright 2016-2020, 2022-2023 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.

packages/optimizely-sdk/lib/modules/datafile-manager/httpPollingDatafileManager.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2022, Optimizely
2+
* Copyright 2022-2023, 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.
@@ -23,6 +23,9 @@ import { DEFAULT_UPDATE_INTERVAL, MIN_UPDATE_INTERVAL, DEFAULT_URL_TEMPLATE } fr
2323
import BackoffController from './backoffController';
2424
import PersistentKeyValueCache from './persistentKeyValueCache';
2525

26+
import { NotificationRegistry } from './../../core/notification_center/notification_registry';
27+
import { NOTIFICATION_TYPES } from '../../../lib/utils/enums';
28+
2629
const logger = getLogger('DatafileManager');
2730

2831
const UPDATE_EVT = 'update';
@@ -95,6 +98,8 @@ export default abstract class HttpPollingDatafileManager implements DatafileMana
9598

9699
private cache: PersistentKeyValueCache;
97100

101+
private sdkKey: string;
102+
98103
// When true, this means the update interval timeout fired before the current
99104
// sync completed. In that case, we should sync again immediately upon
100105
// completion of the current request, instead of waiting another update
@@ -117,6 +122,7 @@ export default abstract class HttpPollingDatafileManager implements DatafileMana
117122

118123
this.cache = cache;
119124
this.cacheKey = 'opt-datafile-' + sdkKey;
125+
this.sdkKey = sdkKey;
120126
this.isReadyPromiseSettled = false;
121127
this.readyPromiseResolver = (): void => {};
122128
this.readyPromiseRejecter = (): void => {};
@@ -232,6 +238,9 @@ export default abstract class HttpPollingDatafileManager implements DatafileMana
232238
const datafileUpdate: DatafileUpdate = {
233239
datafile,
234240
};
241+
NotificationRegistry.getNotificationCenter(this.sdkKey, logger)?.sendNotifications(
242+
NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE
243+
);
235244
this.emitter.emit(UPDATE_EVT, datafileUpdate);
236245
}
237246
}

0 commit comments

Comments
 (0)