Skip to content

Commit

Permalink
[FSSDK-11114] refactor sdk entrypoints (#1003)
Browse files Browse the repository at this point in the history
  • Loading branch information
raju-opti authored Feb 17, 2025
1 parent 791ab90 commit 1f82bdb
Show file tree
Hide file tree
Showing 41 changed files with 595 additions and 491 deletions.
75 changes: 75 additions & 0 deletions lib/client_factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* Copyright 2025, 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
*
* https://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 { LoggerFacade } from "./logging/logger";
import { Client, Config } from "./shared_types";
import { Maybe } from "./utils/type";
import configValidator from './utils/config_validator';
import { extractLogger } from "./logging/logger_factory";
import { extractErrorNotifier } from "./error/error_notifier_factory";
import { extractConfigManager } from "./project_config/config_manager_factory";
import { extractEventProcessor } from "./event_processor/event_processor_factory";
import { extractOdpManager } from "./odp/odp_manager_factory";
import { extractVuidManager } from "./vuid/vuid_manager_factory";

import { CLIENT_VERSION, JAVASCRIPT_CLIENT_ENGINE } from "./utils/enums";
import Optimizely from "./optimizely";

export const getOptimizelyInstance = (config: Config): Client | null => {
let logger: Maybe<LoggerFacade>;

try {
logger = config.logger ? extractLogger(config.logger) : undefined;

configValidator.validate(config);

const {
clientEngine,
clientVersion,
jsonSchemaValidator,
userProfileService,
defaultDecideOptions,
disposable,
} = config;

const errorNotifier = config.errorNotifier ? extractErrorNotifier(config.errorNotifier) : undefined;

const projectConfigManager = extractConfigManager(config.projectConfigManager);
const eventProcessor = config.eventProcessor ? extractEventProcessor(config.eventProcessor) : undefined;
const odpManager = config.odpManager ? extractOdpManager(config.odpManager) : undefined;
const vuidManager = config.vuidManager ? extractVuidManager(config.vuidManager) : undefined;

const optimizelyOptions = {
clientEngine: clientEngine || JAVASCRIPT_CLIENT_ENGINE,
clientVersion: clientVersion || CLIENT_VERSION,
jsonSchemaValidator,
userProfileService,
defaultDecideOptions,
disposable,
logger,
errorNotifier,
projectConfigManager,
eventProcessor,
odpManager,
vuidManager,
};

return new Optimizely(optimizelyOptions);
} catch (e) {
logger?.error(e);
return null;
}
}
38 changes: 38 additions & 0 deletions lib/entrypoint.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Copyright 2025, 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
*
* https://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 { expectTypeOf } from 'vitest';

import * as browserEntrypoint from './index.browser';
import * as nodeEntrypoint from './index.node';
import * as reactNativeEntrypoint from './index.react_native';

import { Config, Client } from './shared_types';

export type Entrypoint = {
createInstance: (config: Config) => Client | null;
}


// these type tests will be fixed in a future PR

// expectTypeOf(browserEntrypoint).toEqualTypeOf<Entrypoint>();
// expectTypeOf(nodeEntrypoint).toEqualTypeOf<Entrypoint>();
// expectTypeOf(reactNativeEntrypoint).toEqualTypeOf<Entrypoint>();

// expectTypeOf(browserEntrypoint).toEqualTypeOf(nodeEntrypoint);
// expectTypeOf(browserEntrypoint).toEqualTypeOf(reactNativeEntrypoint);
// expectTypeOf(nodeEntrypoint).toEqualTypeOf(reactNativeEntrypoint);
65 changes: 34 additions & 31 deletions lib/event_processor/event_processor_factory.browser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@ vi.mock('./event_processor_factory', async (importOriginal) => {
const getBatchEventProcessor = vi.fn().mockImplementation(() => {
return {};
});
const getOpaqueBatchEventProcessor = vi.fn().mockImplementation(() => {
return {};
});
const original: any = await importOriginal();
return { ...original, getBatchEventProcessor };
return { ...original, getBatchEventProcessor, getOpaqueBatchEventProcessor };
});

vi.mock('../utils/cache/local_storage_cache.browser', () => {
Expand All @@ -47,11 +50,11 @@ import defaultEventDispatcher from './event_dispatcher/default_dispatcher.browse
import { LocalStorageCache } from '../utils/cache/local_storage_cache.browser';
import { SyncPrefixCache } from '../utils/cache/cache';
import { createForwardingEventProcessor, createBatchEventProcessor } from './event_processor_factory.browser';
import { EVENT_STORE_PREFIX, FAILED_EVENT_RETRY_INTERVAL } from './event_processor_factory';
import { EVENT_STORE_PREFIX, extractEventProcessor, FAILED_EVENT_RETRY_INTERVAL } from './event_processor_factory';
import sendBeaconEventDispatcher from './event_dispatcher/send_beacon_dispatcher.browser';
import { getForwardingEventProcessor } from './forwarding_event_processor';
import browserDefaultEventDispatcher from './event_dispatcher/default_dispatcher.browser';
import { getBatchEventProcessor } from './event_processor_factory';
import { getOpaqueBatchEventProcessor } from './event_processor_factory';

describe('createForwardingEventProcessor', () => {
const mockGetForwardingEventProcessor = vi.mocked(getForwardingEventProcessor);
Expand All @@ -65,35 +68,35 @@ describe('createForwardingEventProcessor', () => {
dispatchEvent: vi.fn(),
};

const processor = createForwardingEventProcessor(eventDispatcher);
const processor = extractEventProcessor(createForwardingEventProcessor(eventDispatcher));

expect(Object.is(processor, mockGetForwardingEventProcessor.mock.results[0].value)).toBe(true);
expect(mockGetForwardingEventProcessor).toHaveBeenNthCalledWith(1, eventDispatcher);
});

it('uses the browser default event dispatcher if none is provided', () => {
const processor = createForwardingEventProcessor();
const processor = extractEventProcessor(createForwardingEventProcessor());

expect(Object.is(processor, mockGetForwardingEventProcessor.mock.results[0].value)).toBe(true);
expect(mockGetForwardingEventProcessor).toHaveBeenNthCalledWith(1, browserDefaultEventDispatcher);
});
});

describe('createBatchEventProcessor', () => {
const mockGetBatchEventProcessor = vi.mocked(getBatchEventProcessor);
const mockGetOpaqueBatchEventProcessor = vi.mocked(getOpaqueBatchEventProcessor);
const MockLocalStorageCache = vi.mocked(LocalStorageCache);
const MockSyncPrefixCache = vi.mocked(SyncPrefixCache);

beforeEach(() => {
mockGetBatchEventProcessor.mockClear();
mockGetOpaqueBatchEventProcessor.mockClear();
MockLocalStorageCache.mockClear();
MockSyncPrefixCache.mockClear();
});

it('uses LocalStorageCache and SyncPrefixCache to create eventStore', () => {
const processor = createBatchEventProcessor({});
expect(Object.is(processor, mockGetBatchEventProcessor.mock.results[0].value)).toBe(true);
const eventStore = mockGetBatchEventProcessor.mock.calls[0][0].eventStore;
expect(Object.is(processor, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true);
const eventStore = mockGetOpaqueBatchEventProcessor.mock.calls[0][0].eventStore;
expect(Object.is(eventStore, MockSyncPrefixCache.mock.results[0].value)).toBe(true);

const [cache, prefix, transformGet, transformSet] = MockSyncPrefixCache.mock.calls[0];
Expand All @@ -111,14 +114,14 @@ describe('createBatchEventProcessor', () => {
};

const processor = createBatchEventProcessor({ eventDispatcher });
expect(Object.is(processor, mockGetBatchEventProcessor.mock.results[0].value)).toBe(true);
expect(mockGetBatchEventProcessor.mock.calls[0][0].eventDispatcher).toBe(eventDispatcher);
expect(Object.is(processor, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true);
expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].eventDispatcher).toBe(eventDispatcher);
});

it('uses the default browser event dispatcher if none is provided', () => {
const processor = createBatchEventProcessor({ });
expect(Object.is(processor, mockGetBatchEventProcessor.mock.results[0].value)).toBe(true);
expect(mockGetBatchEventProcessor.mock.calls[0][0].eventDispatcher).toBe(defaultEventDispatcher);
expect(Object.is(processor, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true);
expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].eventDispatcher).toBe(defaultEventDispatcher);
});

it('uses the provided closingEventDispatcher', () => {
Expand All @@ -127,8 +130,8 @@ describe('createBatchEventProcessor', () => {
};

const processor = createBatchEventProcessor({ closingEventDispatcher });
expect(Object.is(processor, mockGetBatchEventProcessor.mock.results[0].value)).toBe(true);
expect(mockGetBatchEventProcessor.mock.calls[0][0].closingEventDispatcher).toBe(closingEventDispatcher);
expect(Object.is(processor, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true);
expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].closingEventDispatcher).toBe(closingEventDispatcher);
});

it('does not use any closingEventDispatcher if eventDispatcher is provided but closingEventDispatcher is not', () => {
Expand All @@ -137,45 +140,45 @@ describe('createBatchEventProcessor', () => {
};

const processor = createBatchEventProcessor({ eventDispatcher });
expect(Object.is(processor, mockGetBatchEventProcessor.mock.results[0].value)).toBe(true);
expect(mockGetBatchEventProcessor.mock.calls[0][0].closingEventDispatcher).toBe(undefined);
expect(Object.is(processor, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true);
expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].closingEventDispatcher).toBe(undefined);
});

it('uses the default sendBeacon event dispatcher if neither eventDispatcher nor closingEventDispatcher is provided', () => {
const processor = createBatchEventProcessor({ });
expect(Object.is(processor, mockGetBatchEventProcessor.mock.results[0].value)).toBe(true);
expect(mockGetBatchEventProcessor.mock.calls[0][0].closingEventDispatcher).toBe(sendBeaconEventDispatcher);
expect(Object.is(processor, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true);
expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].closingEventDispatcher).toBe(sendBeaconEventDispatcher);
});

it('uses the provided flushInterval', () => {
const processor1 = createBatchEventProcessor({ flushInterval: 2000 });
expect(Object.is(processor1, mockGetBatchEventProcessor.mock.results[0].value)).toBe(true);
expect(mockGetBatchEventProcessor.mock.calls[0][0].flushInterval).toBe(2000);
expect(Object.is(processor1, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true);
expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].flushInterval).toBe(2000);

const processor2 = createBatchEventProcessor({ });
expect(Object.is(processor2, mockGetBatchEventProcessor.mock.results[1].value)).toBe(true);
expect(mockGetBatchEventProcessor.mock.calls[1][0].flushInterval).toBe(undefined);
expect(Object.is(processor2, mockGetOpaqueBatchEventProcessor.mock.results[1].value)).toBe(true);
expect(mockGetOpaqueBatchEventProcessor.mock.calls[1][0].flushInterval).toBe(undefined);
});

it('uses the provided batchSize', () => {
const processor1 = createBatchEventProcessor({ batchSize: 20 });
expect(Object.is(processor1, mockGetBatchEventProcessor.mock.results[0].value)).toBe(true);
expect(mockGetBatchEventProcessor.mock.calls[0][0].batchSize).toBe(20);
expect(Object.is(processor1, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true);
expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].batchSize).toBe(20);

const processor2 = createBatchEventProcessor({ });
expect(Object.is(processor2, mockGetBatchEventProcessor.mock.results[1].value)).toBe(true);
expect(mockGetBatchEventProcessor.mock.calls[1][0].batchSize).toBe(undefined);
expect(Object.is(processor2, mockGetOpaqueBatchEventProcessor.mock.results[1].value)).toBe(true);
expect(mockGetOpaqueBatchEventProcessor.mock.calls[1][0].batchSize).toBe(undefined);
});

it('uses maxRetries value of 5', () => {
const processor = createBatchEventProcessor({ });
expect(Object.is(processor, mockGetBatchEventProcessor.mock.results[0].value)).toBe(true);
expect(mockGetBatchEventProcessor.mock.calls[0][0].retryOptions?.maxRetries).toBe(5);
expect(Object.is(processor, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true);
expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].retryOptions?.maxRetries).toBe(5);
});

it('uses the default failedEventRetryInterval', () => {
const processor = createBatchEventProcessor({ });
expect(Object.is(processor, mockGetBatchEventProcessor.mock.results[0].value)).toBe(true);
expect(mockGetBatchEventProcessor.mock.calls[0][0].failedEventRetryInterval).toBe(FAILED_EVENT_RETRY_INTERVAL);
expect(Object.is(processor, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true);
expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].failedEventRetryInterval).toBe(FAILED_EVENT_RETRY_INTERVAL);
});
});
15 changes: 10 additions & 5 deletions lib/event_processor/event_processor_factory.browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ import { getForwardingEventProcessor } from './forwarding_event_processor';
import { EventDispatcher } from './event_dispatcher/event_dispatcher';
import { EventProcessor } from './event_processor';
import { EventWithId } from './batch_event_processor';
import { getBatchEventProcessor, BatchEventProcessorOptions } from './event_processor_factory';
import {
getOpaqueBatchEventProcessor,
BatchEventProcessorOptions,
OpaqueEventProcessor,
wrapEventProcessor,
} from './event_processor_factory';
import defaultEventDispatcher from './event_dispatcher/default_dispatcher.browser';
import sendBeaconEventDispatcher from './event_dispatcher/send_beacon_dispatcher.browser';
import { LocalStorageCache } from '../utils/cache/local_storage_cache.browser';
Expand All @@ -27,23 +32,23 @@ import { EVENT_STORE_PREFIX, FAILED_EVENT_RETRY_INTERVAL } from './event_process

export const createForwardingEventProcessor = (
eventDispatcher: EventDispatcher = defaultEventDispatcher,
): EventProcessor => {
return getForwardingEventProcessor(eventDispatcher);
): OpaqueEventProcessor => {
return wrapEventProcessor(getForwardingEventProcessor(eventDispatcher));
};

const identity = <T>(v: T): T => v;

export const createBatchEventProcessor = (
options: BatchEventProcessorOptions
): EventProcessor => {
): OpaqueEventProcessor => {
const localStorageCache = new LocalStorageCache<EventWithId>();
const eventStore = new SyncPrefixCache<EventWithId, EventWithId>(
localStorageCache, EVENT_STORE_PREFIX,
identity,
identity,
);

return getBatchEventProcessor({
return getOpaqueBatchEventProcessor({
eventDispatcher: options.eventDispatcher || defaultEventDispatcher,
closingEventDispatcher: options.closingEventDispatcher ||
(options.eventDispatcher ? undefined : sendBeaconEventDispatcher),
Expand Down
Loading

0 comments on commit 1f82bdb

Please sign in to comment.