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');