Skip to content

Commit 13c9812

Browse files
authored
Major improvement on performance - added static context option (#203)
1 parent 25a7096 commit 13c9812

File tree

14 files changed

+351
-151
lines changed

14 files changed

+351
-151
lines changed

README.md

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ https://github.com/switcherapi/switcher-api
2828
- Flexible and robust functions that will keep your code clean and maintainable.
2929
- Able to work locally using a snapshot file downloaded from your remote Switcher-API Domain.
3030
- Silent mode is a hybrid configuration that automatically enables a contingent sub-process in case of any connectivity issue.
31-
- Built-in mock implementation for clear and easy implementation of automated testing.
31+
- Built-in stub implementation for clear and easy implementation of automated testing.
3232
- Easy to setup. Switcher Context is responsible to manage all the complexity between your application and API.
3333

3434
# Usage
@@ -60,6 +60,7 @@ You can also activate features such as local and silent mode:
6060

6161
```js
6262
const local = true;
63+
const static = true;
6364
const logger = true;
6465
const snapshotLocation = './snapshot/';
6566
const snapshotAutoUpdateInterval = 3;
@@ -69,14 +70,15 @@ const restrictRelay = true;
6970
const certPath = './certs/ca.pem';
7071

7172
Client.buildContext({ url, apiKey, domain, component, environment }, {
72-
local, logger, snapshotLocation, snapshotAutoUpdateInterval,
73+
local, static, logger, snapshotLocation, snapshotAutoUpdateInterval,
7374
snapshotWatcher, silentMode, restrictRelay, certPath
7475
});
7576

7677
const switcher = Client.getSwitcher();
7778
```
7879

7980
- **local**: If activated, the client will only fetch the configuration inside your snapshot file. The default value is 'false'
81+
- **static**: If activated, the client will not perform any API calls and will only use the in-memory snapshot. The default value is 'false'
8082
- **logger**: If activated, it is possible to retrieve the last results from a given Switcher key using Client.getLogger('KEY')
8183
- **snapshotLocation**: Location of snapshot files
8284
- **snapshotAutoUpdateInterval**: Enable Snapshot Auto Update given an interval in seconds (default: 0 disabled)
@@ -91,11 +93,11 @@ const switcher = Client.getSwitcher();
9193
(*) regexSafe is a feature that prevents your application from being exposed to a reDOS attack. It is recommended to keep this feature enabled.<br>
9294

9395
## Executing
94-
There are a few different ways to call the API using the JavaScript module.
96+
There are a few different ways to call the API.
9597
Here are some examples:
9698

9799
1. **No parameters**
98-
Invoking the API can be done by instantiating the switcher and calling *isItOn* passing its key as a parameter.
100+
This is the simplest way to execute a criteria. It will return a boolean value indicating whether the feature is enabled or not.
99101

100102
```js
101103
const switcher = Client.getSwitcher();
@@ -104,36 +106,29 @@ await switcher.isItOn('FEATURE01');
104106
const { result, reason, metadata } = await switcher.detail().isItOn('FEATURE01');
105107
```
106108

107-
2. **Promise**
108-
Most functions were implemented using async operations. Here it is a differnet way to execute the criteria:
109-
110-
```js
111-
switcher.isItOn('KEY')
112-
.then(result => console.log('Result:', result))
113-
.catch(error => console.log(error));
114-
```
115-
116-
3. **Strategy validation - preparing input**
109+
2. **Strategy validation - preparing input**
117110
Loading information into the switcher can be made by using *prepare*, in case you want to include input from a different place of your code. Otherwise, it is also possible to include everything in the same call.
118111

119112
```js
120-
switcher.checkValue('USER_1').prepare('FEATURE01');
121-
switcher.isItOn();
113+
await switcher.checkValue('USER_1').prepare('FEATURE01');
114+
await switcher.isItOn();
122115
```
123116

124-
4. **Strategy validation - all-in-one execution**
117+
3. **Strategy validation - all-in-one execution**
125118
All-in-one method is fast and include everything you need to execute a complex call to the API.
126119

127120
```js
128121
await switcher
122+
.defaultResult(true) // Default result to be returned in case of no API response
123+
.throttle(1000) // Throttle the API call to improve performance
129124
.checkValue('User 1')
130125
.checkNetwork('192.168.0.1')
131126
.isItOn('FEATURE01');
132127
```
133128

134-
5. **Throttle**
135-
Throttling is useful when placing Feature Flags at critical code blocks require zero-latency without having to switch to local.
136-
API calls will happen asynchronously and the result returned is based on the last API response.
129+
4. **Throttle**
130+
Throttling is useful when placing Feature Flags at critical code blocks that require zero-latency.
131+
API calls will be scheduled to be executed after the throttle time has passed.
137132

138133
```js
139134
const switcher = Client.getSwitcher();
@@ -150,16 +145,19 @@ Client.subscribeNotifyError((error) => {
150145
});
151146
```
152147

153-
6. **Hybrid mode**
148+
5. **Hybrid mode**
154149
Forcing Switchers to resolve remotely can help you define exclusive features that cannot be resolved locally.
155150
This feature is ideal if you want to run the SDK in local mode but still want to resolve a specific switcher remotely.
151+
152+
A particular use case is when a swithcer has a Relay Strategy that requires a remote call to resolve the value.
153+
156154
```js
157155
const switcher = Client.getSwitcher();
158156
await switcher.remote().isItOn('FEATURE01');
159157
```
160158

161-
## Built-in mock feature
162-
You can also bypass your switcher configuration by invoking 'Client.assume'. This is perfect for your test code where you want to validate both scenarios when the switcher is true and false.
159+
## Built-in stub feature
160+
You can also bypass your switcher configuration with 'Client.assume' API. This is perfect for your test code where you want to validate both scenarios when the switcher is true and false.
163161

164162
```js
165163
Client.assume('FEATURE01').true();

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "switcher-client",
3-
"version": "4.3.0",
3+
"version": "4.4.0",
44
"description": "Client JS SDK for working with Switcher-API",
55
"main": "./switcher-client.js",
66
"type": "module",
@@ -32,12 +32,12 @@
3232
],
3333
"devDependencies": {
3434
"@babel/eslint-parser": "^7.27.5",
35-
"@typescript-eslint/eslint-plugin": "^8.35.0",
36-
"@typescript-eslint/parser": "^8.35.0",
35+
"@typescript-eslint/eslint-plugin": "^8.35.1",
36+
"@typescript-eslint/parser": "^8.35.1",
3737
"c8": "^10.1.3",
3838
"chai": "^5.2.0",
3939
"env-cmd": "^10.1.0",
40-
"eslint": "^9.29.0",
40+
"eslint": "^9.30.0",
4141
"mocha": "^11.7.1",
4242
"mocha-sonarqube-reporter": "^1.0.2",
4343
"sinon": "^21.0.0"

sonar-project.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
sonar.projectKey=switcherapi_switcher-client-master
22
sonar.projectName=switcher-client-js
33
sonar.organization=switcherapi
4-
sonar.projectVersion=4.2.0
4+
sonar.projectVersion=4.4.0
55
sonar.links.homepage=https://github.com/switcherapi/switcher-client-js
66

77
sonar.javascript.lcov.reportPaths=coverage/lcov.info

src/client.d.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,24 +180,37 @@ export type SwitcherContext = {
180180
export type SwitcherOptions = {
181181
/**
182182
* When enabled it will use the local snapshot (file or in-memory)
183+
*
183184
* If not set, it will use the remote API
184185
*/
185186
local?: boolean;
186187

188+
/**
189+
* When enabled it will always use in-memory cached results
190+
*
191+
* This option prevents the scheduling of background updates to improve overall performance
192+
*
193+
* Use Client.clearLogger() to reset the in-memory cache if snapshot are renewed
194+
*/
195+
static?: boolean;
196+
187197
/**
188198
* When enabled it allows inspecting the result details with Client.getLogger(key)
199+
*
189200
* If not set, it will not log the result details
190201
*/
191202
logger?: boolean;
192203

193204
/**
194205
* The location of the snapshot file
206+
*
195207
* If not set, it will use the in-memory snapshot
196208
*/
197209
snapshotLocation?: string;
198210

199211
/**
200212
* The interval in milliseconds to auto-update the snapshot
213+
*
201214
* If not set, it will not auto-update the snapshot
202215
*/
203216
snapshotAutoUpdateInterval?: number;
@@ -208,36 +221,41 @@ export type SwitcherOptions = {
208221
snapshotWatcher?: boolean;
209222

210223
/**
211-
* Allow local snapshots to ignore or require Relay verification.
224+
* Allow local snapshots to ignore or require Relay verification
212225
*/
213226
restrictRelay?: boolean;
214227

215228
/**
216229
* When defined it will switch to local during the specified time before it switches back to remote
230+
*
217231
* e.g. 5s (s: seconds - m: minutes - h: hours)
218232
*/
219233
silentMode?: string;
220234

221235
/**
222236
* When enabled it will check Regex strategy using background workers
237+
*
223238
* If not set, it will check Regex strategy synchronously
224239
*/
225240
regexSafe?: boolean;
226241

227242
/**
228-
* The regex max black list
243+
* The regex max black list.
244+
*
229245
* If not set, it will use the default value
230246
*/
231247
regexMaxBlackList?: number;
232248

233249
/**
234250
* The regex max time limit in milliseconds
251+
*
235252
* If not set, it will use the default value
236253
*/
237254
regexMaxTimeLimit?: number;
238255

239256
/**
240257
* The certificate path for secure connections
258+
*
241259
* If not set, it will use the default certificate
242260
*/
243261
certPath?: string;

src/client.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
DEFAULT_LOGGER,
1010
DEFAULT_REGEX_MAX_BLACKLISTED,
1111
DEFAULT_REGEX_MAX_TIME_LIMIT,
12+
DEFAULT_STATIC,
1213
DEFAULT_TEST_MODE,
1314
SWITCHER_OPTIONS
1415
} from './lib/constants.js';
@@ -38,6 +39,7 @@ export class Client {
3839
snapshotAutoUpdateInterval: 0,
3940
snapshotLocation: options?.snapshotLocation,
4041
local: util.get(options?.local, DEFAULT_LOCAL),
42+
static: util.get(options?.static, DEFAULT_STATIC),
4143
logger: util.get(options?.logger, DEFAULT_LOGGER)
4244
});
4345

@@ -130,18 +132,20 @@ export class Client {
130132
return false;
131133
}
132134

133-
static async loadSnapshot(options = { fetchRemote: false, watchSnapshot: false }) {
135+
static async loadSnapshot(options = {}) {
136+
const { fetchRemote = false, watchSnapshot = false } = options;
137+
134138
GlobalSnapshot.init(loadDomain(
135139
util.get(GlobalOptions.snapshotLocation, ''),
136140
util.get(Client.#context.environment, DEFAULT_ENVIRONMENT)
137141
));
138142

139143
if (GlobalSnapshot.snapshot.data.domain.version == 0 &&
140-
(options.fetchRemote || !GlobalOptions.local)) {
144+
(fetchRemote || !GlobalOptions.local)) {
141145
await Client.checkSnapshot();
142146
}
143147

144-
if (options.watchSnapshot) {
148+
if (watchSnapshot) {
145149
Client.watchSnapshot();
146150
}
147151

src/lib/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export const DEFAULT_ENVIRONMENT = 'default';
22
export const DEFAULT_LOCAL = false;
3+
export const DEFAULT_STATIC = false;
34
export const DEFAULT_LOGGER = false;
45
export const DEFAULT_TEST_MODE = false;
56
export const DEFAULT_REGEX_MAX_BLACKLISTED = 50;

src/lib/globals/globalOptions.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ export class GlobalOptions {
1818
return this.#options.local;
1919
}
2020

21+
static get static() {
22+
return this.#options.static;
23+
}
24+
2125
static get logger() {
2226
return this.#options.logger;
2327
}

src/lib/utils/executionLogger.js

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { SwitcherResult } from '../result.js';
2+
13
const logger = new Array();
24

35
export default class ExecutionLogger {
@@ -20,7 +22,14 @@ export default class ExecutionLogger {
2022
}
2123
}
2224

23-
logger.push({ key, input, response });
25+
logger.push({
26+
key,
27+
input,
28+
response: SwitcherResult.create(response.result, response.reason, {
29+
...response.metadata,
30+
cached: true,
31+
}),
32+
});
2433
}
2534

2635
/**
@@ -30,11 +39,13 @@ export default class ExecutionLogger {
3039
* @param input Switcher input
3140
*/
3241
static getExecution(key, input) {
33-
const result = logger.filter(
34-
value => value.key === key &&
35-
JSON.stringify(value.input) === JSON.stringify(input));
42+
for (const log of logger) {
43+
if (this.#hasExecution(log, key, input)) {
44+
return log;
45+
}
46+
}
3647

37-
return result[0];
48+
return new ExecutionLogger();
3849
}
3950

4051
/**
@@ -68,5 +79,20 @@ export default class ExecutionLogger {
6879
ExecutionLogger.callbackError(error);
6980
}
7081
}
82+
83+
static #hasExecution(log, key, input) {
84+
return log.key === key && this.#checkStrategyInputs(log.input, input);
85+
}
86+
87+
static #checkStrategyInputs(loggerInputs, inputs) {
88+
for (const [strategy, input] of loggerInputs || []) {
89+
const found = inputs?.find((i) => i[0] === strategy && i[1] === input);
90+
if (!found) {
91+
return false;
92+
}
93+
}
94+
95+
return true;
96+
}
7197

7298
}

src/switcher.d.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,25 @@ export class Switcher {
1919

2020
/**
2121
* Execute criteria
22-
*
22+
*
2323
* @returns boolean or SwitcherResult when detail() is used
2424
*/
25-
isItOn(key?: string): Promise<boolean | SwitcherResult>;
25+
isItOn(key?: string): Promise<boolean | SwitcherResult> | boolean | SwitcherResult;
26+
27+
/**
28+
* Schedules background refresh of the last criteria request
29+
*/
30+
scheduleBackgroundRefresh(): void;
31+
32+
/**
33+
* Execute criteria from remote API
34+
*/
35+
async executeRemoteCriteria(): Promise<boolean | SwitcherResult>
36+
37+
/**
38+
* Execute criteria from local snapshot
39+
*/
40+
async executeLocalCriteria(): Promise<boolean | SwitcherResult>
2641

2742
/**
2843
* Define a delay (ms) for the next async execution.

0 commit comments

Comments
 (0)