diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..acd068bff --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,20 @@ +{ + "name": "Javascript SDK", + "image": "mcr.microsoft.com/devcontainers/typescript-node:1-18-bookworm", + + "postCreateCommand": "cd /workspaces/javascript-sdk/packages/optimizely-sdk && npm install -g npm && npm install", + + "customizations": { + "vscode": { + "extensions": [ + "dbaeumer.vscode-eslint", + "eamodio.gitlens", + "esbenp.prettier-vscode", + "Gruntfuggly.todo-tree", + "github.vscode-github-actions", + "Orta.vscode-jest", + "ms-vscode.test-adapter-converter" + ] + } + } +} diff --git a/.github/workflows/javascript.yml b/.github/workflows/javascript.yml index 18c063677..426192b7d 100644 --- a/.github/workflows/javascript.yml +++ b/.github/workflows/javascript.yml @@ -17,7 +17,7 @@ jobs: - name: Set up Node uses: actions/setup-node@v3 with: - node-version: 12 + node-version: 14 cache-dependency-path: packages/optimizely-sdk/package-lock.json cache: 'npm' - name: Run linting @@ -118,7 +118,7 @@ jobs: - name: Set up Node uses: actions/setup-node@v3 with: - node-version: 12 + node-version: 14 cache: 'npm' cache-dependency-path: ${{ matrix.package }}/package-lock.json - name: Test sub packages diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..67bbd4ff1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "jest.rootPath": "/workspaces/javascript-sdk/packages/optimizely-sdk", + "jest.jestCommandLine": "./node_modules/.bin/jest", + "jest.autoRevealOutput": "on-exec-error" +} \ No newline at end of file diff --git a/packages/optimizely-sdk/lib/modules/datafile-manager/config.ts b/packages/optimizely-sdk/lib/modules/datafile-manager/config.ts index 18de89eff..55e69a33e 100644 --- a/packages/optimizely-sdk/lib/modules/datafile-manager/config.ts +++ b/packages/optimizely-sdk/lib/modules/datafile-manager/config.ts @@ -1,11 +1,11 @@ /** - * Copyright 2022, Optimizely + * Copyright 2022-2023, 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 + * 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, @@ -14,9 +14,15 @@ * limitations under the License. */ -export const DEFAULT_UPDATE_INTERVAL = 5 * 60 * 1000; // 5 minutes +const DEFAULT_UPDATE_INTERVAL_MINUTES = 5; +/** Standard interval (5 minutes in milliseconds) for polling datafile updates.; */ +export const DEFAULT_UPDATE_INTERVAL = DEFAULT_UPDATE_INTERVAL_MINUTES * 60 * 1000; -export const MIN_UPDATE_INTERVAL = 1000; +const MIN_UPDATE_INTERVAL_SECONDS = 30; +/** Minimum allowed interval (30 seconds in milliseconds) for polling datafile updates. */ +export const MIN_UPDATE_INTERVAL = MIN_UPDATE_INTERVAL_SECONDS * 1000; + +export const UPDATE_INTERVAL_BELOW_MINIMUM_MESSAGE = `Polling intervals below ${MIN_UPDATE_INTERVAL_SECONDS} seconds are not recommended.`; export const DEFAULT_URL_TEMPLATE = `https://cdn.optimizely.com/datafiles/%s.json`; diff --git a/packages/optimizely-sdk/lib/modules/datafile-manager/datafileManager.ts b/packages/optimizely-sdk/lib/modules/datafile-manager/datafileManager.ts index 5730786c0..9e748d174 100644 --- a/packages/optimizely-sdk/lib/modules/datafile-manager/datafileManager.ts +++ b/packages/optimizely-sdk/lib/modules/datafile-manager/datafileManager.ts @@ -1,11 +1,11 @@ /** - * Copyright 2022, Optimizely + * Copyright 2022-2023, 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 + * 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, @@ -40,6 +40,7 @@ export interface DatafileManagerConfig { autoUpdate?: boolean; datafile?: string; sdkKey: string; + /** Polling interval in milliseconds to check for datafile updates. */ updateInterval?: number; urlTemplate?: string; cache?: PersistentKeyValueCache; diff --git a/packages/optimizely-sdk/lib/modules/datafile-manager/httpPollingDatafileManager.ts b/packages/optimizely-sdk/lib/modules/datafile-manager/httpPollingDatafileManager.ts index fcf2c0efd..ee595a526 100644 --- a/packages/optimizely-sdk/lib/modules/datafile-manager/httpPollingDatafileManager.ts +++ b/packages/optimizely-sdk/lib/modules/datafile-manager/httpPollingDatafileManager.ts @@ -5,7 +5,7 @@ * 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 + * 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, @@ -19,7 +19,7 @@ import { sprintf } from '../../../lib/utils/fns'; import { DatafileManager, DatafileManagerConfig, DatafileUpdate } from './datafileManager'; import EventEmitter, { Disposer } from './eventEmitter'; import { AbortableRequest, Response, Headers } from './http'; -import { DEFAULT_UPDATE_INTERVAL, MIN_UPDATE_INTERVAL, DEFAULT_URL_TEMPLATE } from './config'; +import { DEFAULT_UPDATE_INTERVAL, MIN_UPDATE_INTERVAL, DEFAULT_URL_TEMPLATE, UPDATE_INTERVAL_BELOW_MINIMUM_MESSAGE } from './config'; import BackoffController from './backoffController'; import PersistentKeyValueCache from './persistentKeyValueCache'; @@ -30,10 +30,6 @@ const logger = getLogger('DatafileManager'); const UPDATE_EVT = 'update'; -function isValidUpdateInterval(updateInterval: number): boolean { - return updateInterval >= MIN_UPDATE_INTERVAL; -} - function isSuccessStatusCode(statusCode: number): boolean { return statusCode >= 200 && statusCode < 400; } @@ -124,8 +120,8 @@ export default abstract class HttpPollingDatafileManager implements DatafileMana this.cacheKey = 'opt-datafile-' + sdkKey; this.sdkKey = sdkKey; this.isReadyPromiseSettled = false; - this.readyPromiseResolver = (): void => {}; - this.readyPromiseRejecter = (): void => {}; + this.readyPromiseResolver = (): void => { }; + this.readyPromiseRejecter = (): void => { }; this.readyPromise = new Promise((resolve, reject) => { this.readyPromiseResolver = resolve; this.readyPromiseRejecter = reject; @@ -145,16 +141,20 @@ export default abstract class HttpPollingDatafileManager implements DatafileMana this.datafileUrl = sprintf(urlTemplate, sdkKey); this.emitter = new EventEmitter(); + this.autoUpdate = autoUpdate; - if (isValidUpdateInterval(updateInterval)) { - this.updateInterval = updateInterval; - } else { - logger.warn('Invalid updateInterval %s, defaulting to %s', updateInterval, DEFAULT_UPDATE_INTERVAL); - this.updateInterval = DEFAULT_UPDATE_INTERVAL; + + this.updateInterval = updateInterval; + if (this.updateInterval < MIN_UPDATE_INTERVAL) { + logger.warn(UPDATE_INTERVAL_BELOW_MINIMUM_MESSAGE); } + this.currentTimeout = null; + this.currentRequest = null; + this.backoffController = new BackoffController(); + this.syncOnCurrentRequestComplete = false; } diff --git a/packages/optimizely-sdk/tests/httpPollingDatafileManager.spec.ts b/packages/optimizely-sdk/tests/httpPollingDatafileManager.spec.ts index 288a30367..ffb8e565b 100644 --- a/packages/optimizely-sdk/tests/httpPollingDatafileManager.spec.ts +++ b/packages/optimizely-sdk/tests/httpPollingDatafileManager.spec.ts @@ -35,7 +35,7 @@ import BackoffController from '../lib/modules/datafile-manager/backoffController // Test implementation: // - Does not make any real requests: just resolves with queued responses (tests push onto queuedResponses) -class TestDatafileManager extends HttpPollingDatafileManager { +export class TestDatafileManager extends HttpPollingDatafileManager { queuedResponses: (Response | Error)[] = []; responsePromises: Promise[] = []; diff --git a/packages/optimizely-sdk/tests/httpPollingDatafileManagerPolling.spec.ts b/packages/optimizely-sdk/tests/httpPollingDatafileManagerPolling.spec.ts new file mode 100644 index 000000000..a5be36eed --- /dev/null +++ b/packages/optimizely-sdk/tests/httpPollingDatafileManagerPolling.spec.ts @@ -0,0 +1,61 @@ +/** + * Copyright 2023 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 { resetCalls, spy, verify } from 'ts-mockito'; +import { LogLevel, LoggerFacade, getLogger, setLogLevel } from '../lib/modules/logging'; +import { UPDATE_INTERVAL_BELOW_MINIMUM_MESSAGE } from '../lib/modules/datafile-manager/config'; +import { TestDatafileManager } from './httpPollingDatafileManager.spec'; + +describe('HttpPollingDatafileManager polling', () => { + let spiedLogger: LoggerFacade; + + const loggerName = 'DatafileManager'; + const sdkKey = 'not-real-sdk'; + + beforeAll(() => { + setLogLevel(LogLevel.DEBUG); + const actualLogger = getLogger(loggerName); + spiedLogger = spy(actualLogger); + }); + + beforeEach(() => { + resetCalls(spiedLogger); + }); + + + it('should log polling interval below 30 seconds', () => { + const below30Seconds = 29 * 1000; + + new TestDatafileManager({ + sdkKey, + updateInterval: below30Seconds, + }); + + + verify(spiedLogger.warn(UPDATE_INTERVAL_BELOW_MINIMUM_MESSAGE)).once(); + }); + + it('should not log when polling interval above 30s', () => { + const oneMinute = 60 * 1000; + + new TestDatafileManager({ + sdkKey, + updateInterval: oneMinute, + }); + + verify(spiedLogger.warn(UPDATE_INTERVAL_BELOW_MINIMUM_MESSAGE)).never(); + }); +});