Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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
Expand Down Expand Up @@ -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.<br>
Activate watchSnapshot optionally passing true in the arguments.
Activate watchSnapshot optionally passing true in the arguments.<br>
Auto load Snapshot from API passing true as second argument.

```ts
Switcher.loadSnapshot();
Expand Down
26 changes: 26 additions & 0 deletions snapshot/dev_v2.json
Original file line number Diff line number Diff line change
@@ -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": []
}
]
}
]
}
}
}
15 changes: 15 additions & 0 deletions src/lib/utils/snapshotAutoUpdater.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
70 changes: 45 additions & 25 deletions src/switcher-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
}

Expand All @@ -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();
}
Expand Down Expand Up @@ -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
*
Expand Down
1 change: 1 addition & 0 deletions src/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface SwitcherOptions {
offline?: boolean;
logger?: boolean;
snapshotLocation?: string;
snapshotAutoUpdateInterval?: number;
silentMode?: boolean;
retryAfter?: string;
regexMaxBlackList?: number;
Expand Down
21 changes: 19 additions & 2 deletions test/playground/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -110,4 +112,19 @@ const testWatchSnapshot = () => {
(err: any) => console.log(err));
};

testSimpleAPICall(true);
// 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();
42 changes: 39 additions & 3 deletions test/switcher-snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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();

Expand All @@ -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);

Expand Down Expand Up @@ -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));
Expand All @@ -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);

Expand Down
3 changes: 1 addition & 2 deletions test/switcher-watch-snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down