Skip to content

Commit 6930199

Browse files
[FSSDK-10882] ProjectConfigManager SSR support (#965)
* [FSSDK-10882] ssr support addition
1 parent af9fcab commit 6930199

File tree

7 files changed

+127
-2
lines changed

7 files changed

+127
-2
lines changed

lib/index.node.tests.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ describe('optimizelyFactory', function() {
8686
assert.instanceOf(optlyInstance, Optimizely);
8787
assert.equal(optlyInstance.clientVersion, '5.3.4');
8888
});
89-
9089
// TODO: user will create and inject an event processor
9190
// these tests will be refactored accordingly
9291
// describe('event processor configuration', function() {

lib/optimizely/index.spec.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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+
* 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, expect, vi } from 'vitest';
18+
import Optimizely from '.';
19+
import { getMockProjectConfigManager } from '../tests/mock/mock_project_config_manager';
20+
import * as logger from '../plugins/logger';
21+
import * as jsonSchemaValidator from '../utils/json_schema_validator';
22+
import { LOG_LEVEL } from '../common_exports';
23+
import { createNotificationCenter } from '../core/notification_center';
24+
import testData from '../tests/test_data';
25+
import { getForwardingEventProcessor } from '../event_processor/forwarding_event_processor';
26+
import { LoggerFacade } from '../modules/logging';
27+
import { createProjectConfig } from '../project_config/project_config';
28+
29+
describe('lib/optimizely', () => {
30+
const errorHandler = { handleError: function() {} };
31+
32+
const eventDispatcher = {
33+
dispatchEvent: () => Promise.resolve({ statusCode: 200 }),
34+
};
35+
36+
const eventProcessor = getForwardingEventProcessor(eventDispatcher);
37+
38+
const createdLogger: LoggerFacade = {
39+
...logger.createLogger({
40+
logLevel: LOG_LEVEL.INFO,
41+
}),
42+
info: () => {},
43+
debug: () => {},
44+
warn: () => {},
45+
error: () => {},
46+
log: () => {},
47+
};
48+
49+
const notificationCenter = createNotificationCenter({ logger: createdLogger, errorHandler });
50+
51+
it('should pass ssr to the project config manager', () => {
52+
const projectConfigManager = getMockProjectConfigManager({
53+
initConfig: createProjectConfig(testData.getTestProjectConfig()),
54+
});
55+
56+
vi.spyOn(projectConfigManager, 'setSsr');
57+
58+
const instance = new Optimizely({
59+
clientEngine: 'node-sdk',
60+
projectConfigManager,
61+
errorHandler,
62+
jsonSchemaValidator,
63+
logger: createdLogger,
64+
notificationCenter,
65+
eventProcessor,
66+
isSsr: true,
67+
isValidInstance: true,
68+
});
69+
70+
expect(projectConfigManager.setSsr).toHaveBeenCalledWith(true);
71+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
72+
// @ts-ignore
73+
expect(instance.getProjectConfig()).toBe(projectConfigManager.config);
74+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
75+
// @ts-ignore
76+
expect(projectConfigManager.isSsr).toBe(true);
77+
});
78+
});

lib/optimizely/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ export default class Optimizely implements Client {
144144
this.updateOdpSettings();
145145
});
146146

147+
this.projectConfigManager.setSsr(config.isSsr)
147148
this.projectConfigManager.start();
148149
const projectConfigManagerRunningPromise = this.projectConfigManager.onRunning();
149150

lib/project_config/project_config_manager.spec.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,17 @@ describe('ProjectConfigManagerImpl', () => {
165165
await manager.onRunning();
166166
expect(manager.getConfig()).toEqual(createProjectConfig(testData.getTestProjectConfig()));
167167
});
168+
169+
it('should not start datafileManager if isSsr is true and return correct config', () => {
170+
const datafileManager = getMockDatafileManager({});
171+
vi.spyOn(datafileManager, 'start');
172+
const manager = new ProjectConfigManagerImpl({ datafile: testData.getTestProjectConfig(), datafileManager });
173+
manager.setSsr(true);
174+
manager.start();
175+
176+
expect(manager.getConfig()).toEqual(createProjectConfig(testData.getTestProjectConfig()));
177+
expect(datafileManager.start).not.toHaveBeenCalled();
178+
});
168179
});
169180

170181
describe('when datafile is invalid', () => {
@@ -398,6 +409,16 @@ describe('ProjectConfigManagerImpl', () => {
398409
expect(logger.error).toHaveBeenCalled();
399410
});
400411

412+
it('should reject onRunning() and log error if isSsr is true and datafile is not provided', async () =>{
413+
const logger = getMockLogger();
414+
const manager = new ProjectConfigManagerImpl({ logger, datafileManager: getMockDatafileManager({})});
415+
manager.setSsr(true);
416+
manager.start();
417+
418+
await expect(manager.onRunning()).rejects.toThrow();
419+
expect(logger.error).toHaveBeenCalled();
420+
});
421+
401422
it('should reject onRunning() and log error if the datafile version is not supported', async () => {
402423
const logger = getMockLogger();
403424
const datafile = testData.getUnsupportedVersionConfig();

lib/project_config/project_config_manager.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ interface ProjectConfigManagerConfig {
3434

3535
export interface ProjectConfigManager extends Service {
3636
setLogger(logger: LoggerFacade): void;
37+
setSsr(isSsr?: boolean): void;
3738
getConfig(): ProjectConfig | undefined;
3839
getOptimizelyConfig(): OptimizelyConfig | undefined;
3940
onUpdate(listener: Consumer<ProjectConfig>): Fn;
@@ -53,6 +54,7 @@ export class ProjectConfigManagerImpl extends BaseService implements ProjectConf
5354
public jsonSchemaValidator?: Transformer<unknown, boolean>;
5455
public datafileManager?: DatafileManager;
5556
private eventEmitter: EventEmitter<{ update: ProjectConfig }> = new EventEmitter();
57+
private isSsr = false;
5658

5759
constructor(config: ProjectConfigManagerConfig) {
5860
super();
@@ -68,9 +70,18 @@ export class ProjectConfigManagerImpl extends BaseService implements ProjectConf
6870
}
6971

7072
this.state = ServiceState.Starting;
73+
74+
if(this.isSsr) {
75+
// If isSsr is true, we don't need to poll for datafile updates
76+
this.datafileManager = undefined
77+
}
78+
7179
if (!this.datafile && !this.datafileManager) {
80+
const errorMessage = this.isSsr
81+
? 'You must provide datafile in SSR'
82+
: 'You must provide at least one of sdkKey or datafile';
7283
// TODO: replace message with imported constants
73-
this.handleInitError(new Error('You must provide at least one of sdkKey or datafile'));
84+
this.handleInitError(new Error(errorMessage));
7485
return;
7586
}
7687

@@ -211,4 +222,13 @@ export class ProjectConfigManagerImpl extends BaseService implements ProjectConf
211222
this.stopPromise.reject(err);
212223
});
213224
}
225+
226+
/**
227+
* Set the isSsr flag to indicate if the project config manager is being used in a server side rendering environment
228+
* @param {Boolean} isSsr
229+
* @returns {void}
230+
*/
231+
setSsr(isSsr: boolean): void {
232+
this.isSsr = isSsr;
233+
}
214234
}

lib/shared_types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ export interface OptimizelyOptions {
292292
sdkKey?: string;
293293
userProfileService?: UserProfileService | null;
294294
defaultDecideOptions?: OptimizelyDecideOption[];
295+
isSsr?:boolean;
295296
odpManager?: IOdpManager;
296297
notificationCenter: NotificationCenterImpl;
297298
}
@@ -426,6 +427,7 @@ export interface ConfigLite {
426427
defaultDecideOptions?: OptimizelyDecideOption[];
427428
clientEngine?: string;
428429
clientVersion?: string;
430+
isSsr?: boolean;
429431
}
430432

431433
export type OptimizelyExperimentsMap = {

lib/tests/mock/mock_project_config_manager.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,12 @@ type MockOpt = {
2626

2727
export const getMockProjectConfigManager = (opt: MockOpt = {}): ProjectConfigManager => {
2828
return {
29+
isSsr: false,
2930
config: opt.initConfig,
3031
start: () => {},
32+
setSsr: function(isSsr:boolean) {
33+
this.isSsr = isSsr;
34+
},
3135
onRunning: () => opt.onRunning || Promise.resolve(),
3236
stop: () => {},
3337
onTerminated: () => opt.onTerminated || Promise.resolve(),

0 commit comments

Comments
 (0)