From ad9aafa084a5bafada7fc06f02d75d5eb48ce0f2 Mon Sep 17 00:00:00 2001 From: petruki <31597636+petruki@users.noreply.github.com> Date: Tue, 23 May 2023 18:12:47 -0700 Subject: [PATCH] Closes #11 - Added Snapshot Auto Update scheduler --- README.md | 7 ++- snapshot/dev_v2.json | 26 +++++++++++ src/lib/utils/snapshotAutoUpdater.ts | 15 ++++++ src/switcher-client.ts | 70 ++++++++++++++++++---------- src/types/index.d.ts | 1 + test/playground/index.ts | 21 ++++++++- test/switcher-snapshot.test.ts | 42 +++++++++++++++-- test/switcher-watch-snapshot.test.ts | 3 +- 8 files changed, 151 insertions(+), 34 deletions(-) create mode 100644 snapshot/dev_v2.json create mode 100644 src/lib/utils/snapshotAutoUpdater.ts diff --git a/README.md b/README.md index d6ff0dc..2b43795 100644 --- a/README.md +++ b/README.md @@ -63,11 +63,12 @@ You can also activate features such as offline and silent mode: const offline = true; const logger = true; const snapshotLocation = './snapshot/'; +const snapshotAutoUpdateInterval = 3000; const silentMode = true; const retryAfter = '5m'; Switcher.buildContext({ url, apiKey, domain, component, environment }, { - offline, logger, snapshotLocation, silentMode, retryAfter + offline, logger, snapshotLocation, snapshotAutoUpdateInterval, silentMode, retryAfter }); const switcher = Switcher.factory(); @@ -76,6 +77,7 @@ const switcher = Switcher.factory(); - **offline**: If activated, the client will only fetch the configuration inside your snapshot file. The default value is 'false'. - **logger**: If activated, it is possible to retrieve the last results from a given Switcher key using Switcher.getLogger('KEY') - **snapshotLocation**: Location of snapshot files. The default value is './snapshot/'. +- **snapshotAutoUpdateInterval**: Enable Snapshot Auto Update given an interval in ms (default: 0 disabled). - **silentMode**: If activated, all connectivity issues will be ignored and the client will automatically fetch the configuration into your snapshot file. - **retryAfter** : Time given to the module to re-establish connectivity with the API - e.g. 5s (s: seconds - m: minutes - h: hours). - **regexMaxBlackList**: Number of entries cached when REGEX Strategy fails to perform (reDOS safe) - default: 50 @@ -165,7 +167,8 @@ Switcher.checkSwitchers(['FEATURE01', 'FEATURE02']) ## Loading Snapshot from the API This step is optional if you want to load a copy of the configuration that can be used to eliminate latency when offline mode is activated.
-Activate watchSnapshot optionally passing true in the arguments. +Activate watchSnapshot optionally passing true in the arguments.
+Auto load Snapshot from API passing true as second argument. ```ts Switcher.loadSnapshot(); diff --git a/snapshot/dev_v2.json b/snapshot/dev_v2.json new file mode 100644 index 0000000..17dd1d2 --- /dev/null +++ b/snapshot/dev_v2.json @@ -0,0 +1,26 @@ +{ + "data": { + "domain": { + "name": "Business", + "description": "Business description", + "version": 1588557288040, + "activated": true, + "group": [ + { + "name": "Rollout 2030", + "description": "Changes that will be applied during the rollout", + "activated": true, + "config": [ + { + "key": "FF2FOR2030", + "description": "Feature Flag", + "activated": true, + "strategies": [], + "components": [] + } + ] + } + ] + } + } +} \ No newline at end of file diff --git a/src/lib/utils/snapshotAutoUpdater.ts b/src/lib/utils/snapshotAutoUpdater.ts new file mode 100644 index 0000000..f00d725 --- /dev/null +++ b/src/lib/utils/snapshotAutoUpdater.ts @@ -0,0 +1,15 @@ +export default class SnapshotAutoUpdater { + static _worker: number | undefined; + + static schedule(interval: number, checkSnapshot: () => void) { + if (this._worker) { + this.terminate(); + } + + this._worker = setInterval(() => checkSnapshot(), interval); + } + + static terminate() { + clearInterval(this._worker); + } +} diff --git a/src/switcher-client.ts b/src/switcher-client.ts index f27d9ae..5fd19f8 100644 --- a/src/switcher-client.ts +++ b/src/switcher-client.ts @@ -3,6 +3,7 @@ import Bypasser from './lib/bypasser/index.ts'; import ExecutionLogger from './lib/utils/executionLogger.ts'; import DateMoment from './lib/utils/datemoment.ts'; import TimedMatch from './lib/utils/timed-match/index.ts'; +import SnapshotAutoUpdater from './lib/utils/snapshotAutoUpdater.ts'; import { checkSwitchers, loadDomain, validateSnapshot } from './lib/snapshot.ts'; import * as services from './lib/remote.ts'; import checkCriteriaOffline from './lib/resolver.ts'; @@ -56,27 +57,22 @@ export class Switcher { this._context.environment = context.environment || DEFAULT_ENVIRONMENT; // Default values - this._options = {}; - this._options.offline = DEFAULT_OFFLINE; - this._options.snapshotLocation = DEFAULT_SNAPSHOT_LOCATION; - this._options.logger = DEFAULT_LOGGER; + this._options = { + snapshotAutoUpdateInterval: 0, + snapshotLocation: options?.snapshotLocation || DEFAULT_SNAPSHOT_LOCATION, + offline: options?.offline != undefined ? options.offline : DEFAULT_OFFLINE, + logger: options?.logger != undefined ? options.logger : DEFAULT_LOGGER, + }; if (options) { - if ('offline' in options) { - this._options.offline = options.offline; - } - - if ('snapshotLocation' in options) { - this._options.snapshotLocation = options.snapshotLocation; - } - if ('silentMode' in options) { this._options.silentMode = options.silentMode; this.loadSnapshot(); } - if ('logger' in options) { - this._options.logger = options.logger; + if ('snapshotAutoUpdateInterval' in options) { + this._options.snapshotAutoUpdateInterval = options.snapshotAutoUpdateInterval; + this.scheduleSnapshotAutoUpdate(); } if ('retryAfter' in options) { @@ -113,17 +109,17 @@ export class Switcher { Date.now() > (Switcher._context.exp * 1000) ) { await Switcher._auth(); + } - const result = await validateSnapshot( - Switcher._context, - Switcher._options.snapshotLocation, - Switcher._snapshot.data.domain.version, - ); + const result = await validateSnapshot( + Switcher._context, + Switcher._options.snapshotLocation, + Switcher._snapshot.data.domain.version, + ); - if (result) { - Switcher.loadSnapshot(); - return true; - } + if (result) { + Switcher.loadSnapshot(); + return true; } } @@ -135,14 +131,14 @@ export class Switcher { * * @param watchSnapshot enable watchSnapshot when true */ - static async loadSnapshot(watchSnapshot?: boolean) { + static async loadSnapshot(watchSnapshot?: boolean, fecthOnline?: boolean) { Switcher._snapshot = loadDomain( Switcher._options.snapshotLocation || '', Switcher._context.environment, ); if ( Switcher._snapshot?.data.domain.version == 0 && - !Switcher._options.offline + (fecthOnline || !Switcher._options.offline) ) { await Switcher.checkSnapshot(); } @@ -195,6 +191,30 @@ export class Switcher { } } + /** + * Schedule Snapshot auto update. + * It can also be configured using SwitcherOptions 'snapshotAutoUpdateInterval' when + * building context + * + * @param interval in ms + */ + static scheduleSnapshotAutoUpdate(interval?: number) { + if (interval) { + Switcher._options.snapshotAutoUpdateInterval = interval; + } + + if (Switcher._options.snapshotAutoUpdateInterval && Switcher._options.snapshotAutoUpdateInterval > 0) { + SnapshotAutoUpdater.schedule(Switcher._options.snapshotAutoUpdateInterval, this.checkSnapshot); + } + } + + /** + * Terminates Snapshot Auto Update + */ + static terminateSnapshotAutoUpdate() { + SnapshotAutoUpdater.terminate(); + } + /** * Verifies if switchers are properly configured * diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 3b1998c..c82fb94 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -26,6 +26,7 @@ export interface SwitcherOptions { offline?: boolean; logger?: boolean; snapshotLocation?: string; + snapshotAutoUpdateInterval?: number; silentMode?: boolean; retryAfter?: string; regexMaxBlackList?: number; diff --git a/test/playground/index.ts b/test/playground/index.ts index 2539276..0958bf3 100644 --- a/test/playground/index.ts +++ b/test/playground/index.ts @@ -15,7 +15,9 @@ let switcher; */ function setupSwitcher(offline: boolean) { Switcher.buildContext({ url, apiKey, domain, component, environment }, { offline, logger: true }); - Switcher.loadSnapshot(); + Switcher.loadSnapshot() + .then(() => console.log('Snapshot loaded')) + .catch(() => console.log('Failed to load Snapshot')); } // Requires online API @@ -110,4 +112,19 @@ const testWatchSnapshot = () => { (err: any) => console.log(err)); }; -testSimpleAPICall(true); \ No newline at end of file +// Requires online API +const testSnapshotAutoUpdate = () => { + Switcher.buildContext({ url, apiKey, domain, component, environment }, + { offline: true, logger: true, snapshotAutoUpdateInterval: 3000 }); + + Switcher.loadSnapshot(); + const switcher = Switcher.factory(); + + setInterval(async () => { + const time = Date.now(); + await switcher.isItOn(SWITCHER_KEY, [checkValue('user_1')]); + console.log(Switcher.getLogger(SWITCHER_KEY), `executed in ${Date.now() - time}ms`); + }, 2000); +}; + +testSnapshotAutoUpdate(); \ No newline at end of file diff --git a/test/switcher-snapshot.test.ts b/test/switcher-snapshot.test.ts index ae0d512..b2a98a2 100644 --- a/test/switcher-snapshot.test.ts +++ b/test/switcher-snapshot.test.ts @@ -2,7 +2,7 @@ import { describe, it, afterAll, beforeEach } from 'https://deno.land/std@0.188. import { assertRejects, assertFalse, assertExists } from 'https://deno.land/std@0.188.0/testing/asserts.ts'; import { delay } from 'https://deno.land/std@0.177.0/async/delay.ts'; import { existsSync } from 'https://deno.land/std@0.110.0/fs/mod.ts'; -import { given, givenError, tearDown, generateAuth, generateStatus, assertTrue } from './helper/utils.ts'; +import { given, givenError, tearDown, generateAuth, generateStatus, assertTrue, WaitSafe } from './helper/utils.ts'; import { Switcher } from '../mod.ts'; import { SwitcherContext } from '../src/types/index.d.ts'; @@ -16,6 +16,9 @@ describe('E2E test - Switcher offline - Snapshot:', function () { const dataBuffer = Deno.readTextFileSync('./snapshot/dev.json'); const dataJSON = dataBuffer.toString(); + const dataBufferV2 = Deno.readTextFileSync('./snapshot/dev_v2.json'); + const dataJSONV2 = dataBufferV2.toString(); + beforeEach(function() { Switcher.unloadSnapshot(); @@ -42,7 +45,7 @@ describe('E2E test - Switcher offline - Snapshot:', function () { }); it('should NOT update snapshot - Too many requests at checkSnapshotVersion', testSettings, async function () { - //give + //given given('POST@/criteria/auth', generateAuth(token, 5)); given('GET@/criteria/snapshot_check/:version', null, 429); @@ -73,7 +76,7 @@ describe('E2E test - Switcher offline - Snapshot:', function () { it('should update snapshot', testSettings, async function () { await delay(2000); - //give + //given given('POST@/criteria/auth', generateAuth(token, 5)); given('GET@/criteria/snapshot_check/:version', generateStatus(false)); given('POST@/graphql', JSON.parse(dataJSON)); @@ -91,6 +94,39 @@ describe('E2E test - Switcher offline - Snapshot:', function () { Switcher.unloadSnapshot(); }); + it('should auto update snapshot every 1000ms', testSettings, async function () { + await delay(3000); + + //given + given('POST@/criteria/auth', generateAuth(token, 5)); + given('GET@/criteria/snapshot_check/:version', generateStatus(false)); + given('POST@/graphql', JSON.parse(dataJSON)); + + //test + Switcher.buildContext(contextSettings, { + snapshotLocation: 'generated-snapshots/', + offline: true, + snapshotAutoUpdateInterval: 500 + }); + + //optional (already set in the buildContext) + Switcher.scheduleSnapshotAutoUpdate(1000); + + await Switcher.loadSnapshot(false, true); + + const switcher = Switcher.factory(); + assertFalse(await switcher.isItOn('FF2FOR2030')); + + //given new version + given('POST@/graphql', JSON.parse(dataJSONV2)); + + WaitSafe.limit(2000); + await WaitSafe.wait(); + assertTrue(await switcher.isItOn('FF2FOR2030')); + + Switcher.terminateSnapshotAutoUpdate(); + }); + it('should NOT update snapshot', testSettings, async function () { await delay(2000); diff --git a/test/switcher-watch-snapshot.test.ts b/test/switcher-watch-snapshot.test.ts index d95a284..945d437 100644 --- a/test/switcher-watch-snapshot.test.ts +++ b/test/switcher-watch-snapshot.test.ts @@ -2,10 +2,9 @@ import { describe, it, afterAll, beforeEach } from 'https://deno.land/std@0.188.0/testing/bdd.ts'; import { assertEquals, assertFalse } from 'https://deno.land/std@0.188.0/testing/asserts.ts'; import { existsSync } from 'https://deno.land/std@0.110.0/fs/mod.ts'; -import { assertTrue } from './helper/utils.ts'; +import { assertTrue, WaitSafe } from './helper/utils.ts'; import { Switcher } from '../mod.ts'; -import { WaitSafe } from './helper/utils.ts'; const updateSwitcher = (status: boolean) => { const dataBuffer = Deno.readTextFileSync('./snapshot/dev.json');