From 30d20ecdd6a8f02a067779fec51076aa875c568d Mon Sep 17 00:00:00 2001
From: petruki <31597636+petruki@users.noreply.github.com>
Date: Thu, 8 Jan 2026 11:04:57 -0800
Subject: [PATCH 1/2] feat: added persisted Switchers for better resource
utilization
---
README.md | 25 +++++++-------
src/client.d.ts | 4 ++-
src/client.js | 16 ++++++++-
src/switcher.d.ts | 54 ++++++++++++++++++-------------
src/switcher.js | 28 +++++++++++-----
tests/playground/index.js | 40 ++++++++++++-----------
tests/switcher-client.test.js | 13 ++++----
tests/switcher-functional.test.js | 19 +++++++++++
8 files changed, 131 insertions(+), 68 deletions(-)
diff --git a/README.md b/README.md
index a8d1cf2..c8bab53 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
***
-Switcher Client SDK
+Switcher Client JS SDK
A JavaScript SDK for Switcher API
@@ -81,10 +81,10 @@ Client.buildContext({
});
// 2. Get a switcher instance
-const switcher = Client.getSwitcher();
+const switcher = Client.getSwitcher('FEATURE01');
// 3. Check if a feature is enabled
-const isFeatureEnabled = await switcher.isItOn('FEATURE01');
+const isFeatureEnabled = await switcher.isItOn();
console.log('Feature enabled:', isFeatureEnabled);
```
@@ -177,19 +177,22 @@ Client.buildContext({
Multiple ways to check if a feature is enabled:
```js
+// Non-persisted switcher instance
const switcher = Client.getSwitcher();
+// Persisted switcher instance
+const switcher = Client.getSwitcher('FEATURE01');
// 🚀 Synchronous (local mode only)
-const isEnabled = switcher.isItOn('FEATURE01'); // Returns: boolean
-const isEnabledBool = switcher.isItOnBool('FEATURE01'); // Returns: boolean
-const detailResult = switcher.detail().isItOn('FEATURE01'); // Returns: { result, reason, metadata }
-const detailDirect = switcher.isItOnDetail('FEATURE01'); // Returns: { result, reason, metadata }
+const isEnabled = switcher.isItOn(); // Returns: boolean
+const isEnabledBool = switcher.isItOnBool(); // Returns: boolean
+const detailResult = switcher.detail().isItOn(); // Returns: { result, reason, metadata }
+const detailDirect = switcher.isItOnDetail(); // Returns: { result, reason, metadata }
// 🌐 Asynchronous (remote/hybrid mode)
-const isEnabledAsync = await switcher.isItOn('FEATURE01'); // Returns: Promise
-const isEnabledBoolAsync = await switcher.isItOnBool('FEATURE01', true); // Returns: Promise
-const detailResultAsync = await switcher.detail().isItOn('FEATURE01'); // Returns: Promise
-const detailDirectAsync = await switcher.isItOnDetail('FEATURE01', true); // Returns: Promise
+const isEnabledAsync = await switcher.isItOn(); // Returns: Promise
+const isEnabledBoolAsync = await switcher.isItOnBool(true); // Returns: Promise
+const detailResultAsync = await switcher.detail().isItOn(); // Returns: Promise
+const detailDirectAsync = await switcher.isItOnDetail(true); // Returns: Promise
```
### Strategy Validation
diff --git a/src/client.d.ts b/src/client.d.ts
index 6c49699..f09bac1 100644
--- a/src/client.d.ts
+++ b/src/client.d.ts
@@ -16,7 +16,9 @@ export class Client {
static buildContext(context: SwitcherContext, options?: SwitcherOptions): void;
/**
- * Creates a new instance of Switcher
+ * Creates a new instance of Switcher.
+ *
+ * Provide a key if you want to persist the instance.
*/
static getSwitcher(key?: string): Switcher;
diff --git a/src/client.js b/src/client.js
index d707447..d9ac6eb 100644
--- a/src/client.js
+++ b/src/client.js
@@ -26,6 +26,7 @@ import { SnapshotWatcher } from './lib/snapshotWatcher.js';
export class Client {
static #snapshotWatcher = new SnapshotWatcher();
+ static #switchers = new Map();
static #testEnabled;
static #context;
@@ -103,8 +104,21 @@ export class Client {
}
static getSwitcher(key) {
- return new Switcher(util.get(key, ''))
+ const keyValue = util.get(key, '');
+ const persistedSwitcher = this.#switchers.get(keyValue);
+
+ if (persistedSwitcher) {
+ return persistedSwitcher;
+ }
+
+ const switcher = new Switcher(keyValue)
.restrictRelay(GlobalOptions.restrictRelay);
+
+ if (keyValue) {
+ this.#switchers.set(keyValue, switcher);
+ }
+
+ return switcher;
}
static async checkSnapshot() {
diff --git a/src/switcher.d.ts b/src/switcher.d.ts
index 9f6ecbe..14a55b7 100644
--- a/src/switcher.d.ts
+++ b/src/switcher.d.ts
@@ -16,65 +16,75 @@ export type SwitcherExecutionResult = Promise | boolea
* const { result, reason, metadata } = switcher.isItOnDetail();
*
* // Force asynchronous execution
- * const isOnAsync = await switcher.isItOnBool('MY_SWITCHER', true);
- * const detailAsync = await switcher.isItOnDetail('MY_SWITCHER', true);
+ * const isOnAsync = await switcher.isItOnBool(true);
+ * const detailAsync = await switcher.isItOnDetail(true);
* ```
*/
export class Switcher {
/**
- * Execute criteria with boolean result (synchronous version)
+ * Execute criteria with boolean result (synchronous, uses persisted key)
*
- * @param key - switcher key
- * @param forceAsync - when true, forces async execution
- * @returns boolean value
+ * @returns boolean result
*/
- isItOnBool(key: string, forceAsync?: false): boolean;
+ isItOnBool(): boolean;
/**
- * Execute criteria with boolean result (asynchronous version)
+ * Execute criteria with boolean result (synchronous)
*
* @param key - switcher key
+ * @returns boolean result
+ */
+ isItOnBool(key: string): boolean;
+
+ /**
+ * Execute criteria with boolean result (asynchronous, uses persisted key)
+ *
* @param forceAsync - when true, forces async execution
- * @returns Promise value
+ * @returns Promise result
*/
- isItOnBool(key: string, forceAsync?: true): Promise;
+ isItOnBool(forceAsync: true): Promise;
/**
- * Execute criteria with boolean result
+ * Execute criteria with boolean result (asynchronous)
*
* @param key - switcher key
* @param forceAsync - when true, forces async execution
- * @returns boolean value or Promise based on execution mode
+ * @returns Promise result
*/
- isItOnBool(key: string, forceAsync?: boolean): Promise | boolean;
+ isItOnBool(key: string, forceAsync: true): Promise;
/**
- * Execute criteria with detail information (synchronous version)
+ * Execute criteria with detail information (synchronous)
*
- * @param key - switcher key
- * @param forceAsync - when true, forces async execution
* @returns SwitcherResult object
*/
- isItOnDetail(key: string, forceAsync?: false): SwitcherResult;
+ isItOnDetail(): SwitcherResult;
/**
- * Execute criteria with detail information (asynchronous version)
+ * Execute criteria with detail information (synchronous)
*
* @param key - switcher key
+ * @returns SwitcherResult object
+ */
+ isItOnDetail(key: string): SwitcherResult;
+
+ /**
+ * Execute criteria with detail information (asynchronous, uses persisted key)
+ *
* @param forceAsync - when true, forces async execution
* @returns Promise object
*/
- isItOnDetail(key: string, forceAsync?: true): Promise;
+ isItOnDetail(forceAsync: true): Promise;
/**
- * Execute criteria with detail information
+ * Execute criteria with detail information (asynchronous)
*
* @param key - switcher key
* @param forceAsync - when true, forces async execution
- * @returns SwitcherResult or Promise based on execution mode
+ * @returns Promise object
*/
- isItOnDetail(key: string, forceAsync?: boolean): Promise | SwitcherResult;
+ isItOnDetail(key: string, forceAsync: true): Promise;
/**
* Execute criteria
diff --git a/src/switcher.js b/src/switcher.js
index 8520790..27a1121 100644
--- a/src/switcher.js
+++ b/src/switcher.js
@@ -43,24 +43,36 @@ export class Switcher extends SwitcherRequest {
}
}
- isItOnBool(key, forceAsync = false) {
+ isItOnBool(arg1, arg2) {
this.detail(false);
- if (forceAsync) {
- return Promise.resolve(this.isItOn(key));
+ // Handle case where first argument is forceAsync boolean
+ if (typeof arg1 === 'boolean') {
+ arg2 = arg1;
+ arg1 = undefined;
}
- return this.isItOn(key);
+ if (arg2) {
+ return Promise.resolve(this.isItOn(arg1));
+ }
+
+ return this.isItOn(arg1);
}
- isItOnDetail(key, forceAsync = false) {
+ isItOnDetail(arg1, arg2) {
this.detail(true);
- if (forceAsync) {
- return Promise.resolve(this.isItOn(key));
+ // Handle case where first argument is forceAsync boolean
+ if (typeof arg1 === 'boolean') {
+ arg2 = arg1;
+ arg1 = undefined;
+ }
+
+ if (arg2) {
+ return Promise.resolve(this.isItOn(arg1));
}
- return this.isItOn(key);
+ return this.isItOn(arg1);
}
isItOn(key) {
diff --git a/tests/playground/index.js b/tests/playground/index.js
index 34f9f3c..841de60 100644
--- a/tests/playground/index.js
+++ b/tests/playground/index.js
@@ -20,7 +20,7 @@ async function setupSwitcher(local) {
}
/**
- * This code snippet is a minimal example of how to configure and use Switcher4Deno locally.
+ * This code snippet is a minimal example of how to configure and use switcher-client-js locally.
* No remote API account is required.
*
* Snapshot is loaded from file at tests/playground/snapshot/local.json
@@ -57,11 +57,11 @@ const _testSimpleAPICall = async (local) => {
.then(() => console.log('Switcher checked'))
.catch(error => console.log(error));
- const switcher = Client.getSwitcher();
+ const switcher = Client.getSwitcher(SWITCHER_KEY);
setInterval(async () => {
const time = Date.now();
- const result = await switcher.detail().isItOn(SWITCHER_KEY);
+ const result = await switcher.detail().isItOn();
console.log(`- ${Date.now() - time} ms - ${JSON.stringify(result)}`);
}, 1000);
};
@@ -73,14 +73,15 @@ const _testThrottledAPICall = async () => {
await Client.checkSwitchers([SWITCHER_KEY]);
Client.subscribeNotifyError((error) => console.log(error));
- const switcher = Client.getSwitcher();
- switcher.throttle(1000);
-
+
setInterval(async () => {
const time = Date.now();
+
+ const switcher = Client.getSwitcher(SWITCHER_KEY);
+ switcher.throttle(5000);
const result = await switcher
.detail()
- .isItOn(SWITCHER_KEY);
+ .isItOn();
console.log(`- ${Date.now() - time} ms - ${JSON.stringify(result)}`);
}, 1000);
@@ -94,13 +95,14 @@ const _testSnapshotUpdate = async () => {
console.log('checkSnapshot:', await Client.checkSnapshot());
};
+// Requires remote API
const _testAsyncCall = async () => {
- setupSwitcher(true);
- const switcher = Client.getSwitcher();
+ setupSwitcher(false);
+ const switcher = Client.getSwitcher(SWITCHER_KEY);
- console.log('Sync:', await switcher.isItOn(SWITCHER_KEY));
+ console.log('Sync:', await switcher.isItOn());
- switcher.isItOn(SWITCHER_KEY)
+ switcher.isItOn()
.then(res => console.log('Promise result:', res))
.catch(error => console.log(error));
};
@@ -108,17 +110,17 @@ const _testAsyncCall = async () => {
// Does not require remote API
const _testBypasser = async () => {
setupSwitcher(true);
- const switcher = Client.getSwitcher();
+ const switcher = Client.getSwitcher(SWITCHER_KEY);
- let result = await switcher.isItOn(SWITCHER_KEY);
+ let result = await switcher.isItOn();
console.log(result);
Client.assume(SWITCHER_KEY).true();
- result = await switcher.isItOn(SWITCHER_KEY);
+ result = await switcher.isItOn();
console.log(result);
Client.forget(SWITCHER_KEY);
- result = await switcher.isItOn(SWITCHER_KEY);
+ result = await switcher.isItOn();
console.log(result);
Client.unloadSnapshot();
@@ -158,13 +160,13 @@ const _testWatchSnapshotContextOptions = async () => {
await Client.loadSnapshot();
- const switcher = Client.getSwitcher();
+ const switcher = Client.getSwitcher(SWITCHER_KEY);
setInterval(async () => {
const time = Date.now();
const result = await switcher
.detail()
- .isItOn(SWITCHER_KEY);
+ .isItOn();
console.log(`- ${Date.now() - time} ms - ${JSON.stringify(result)}`);
}, 1000);
@@ -176,7 +178,7 @@ const _testSnapshotAutoUpdate = async () => {
{ local: true, logger: true });
await Client.loadSnapshot({ watchSnapshot: false, fetchRemote: true });
- const switcher = Client.getSwitcher();
+ const switcher = Client.getSwitcher(SWITCHER_KEY);
Client.scheduleSnapshotAutoUpdate(1, {
success: (updated) => console.log('In-memory snapshot updated', updated),
@@ -185,7 +187,7 @@ const _testSnapshotAutoUpdate = async () => {
setInterval(async () => {
const time = Date.now();
- await switcher.checkValue('user_1').isItOn(SWITCHER_KEY);
+ await switcher.checkValue('user_1').isItOn();
console.clear();
console.log(JSON.stringify(Client.getLogger(SWITCHER_KEY)), `executed in ${Date.now() - time}ms`);
}, 2000);
diff --git a/tests/switcher-client.test.js b/tests/switcher-client.test.js
index f5065e3..8327d53 100644
--- a/tests/switcher-client.test.js
+++ b/tests/switcher-client.test.js
@@ -48,13 +48,14 @@ describe('E2E test - Client local #1:', function () {
await switcher
.checkValue('Japan')
.checkNetwork('10.0.0.3')
- .prepare();
+ .prepare('FF2FOR2020');
- assert.isTrue(await switcher.isItOn('FF2FOR2020') === true);
- assert.isTrue(switcher.isItOnBool('FF2FOR2020') === true);
- assert.isTrue(await switcher.isItOnBool('FF2FOR2020', true) === true);
- assert.isTrue(switcher.isItOnDetail('FF2FOR2020').result === true);
- assert.isTrue((await switcher.isItOnDetail('FF2FOR2020', true)).result === true);
+ assert.isTrue(switcher.isItOn() === true);
+ assert.isTrue(await switcher.isItOn() === true);
+ assert.isTrue(switcher.isItOnBool() === true);
+ assert.isTrue(await switcher.isItOnBool(true) === true);
+ assert.isTrue(switcher.isItOnDetail().result === true);
+ assert.isTrue((await switcher.isItOnDetail(true)).result === true);
});
it('should get execution from logger', async function () {
diff --git a/tests/switcher-functional.test.js b/tests/switcher-functional.test.js
index 54282e2..f2b758f 100644
--- a/tests/switcher-functional.test.js
+++ b/tests/switcher-functional.test.js
@@ -54,6 +54,25 @@ describe('Integrated test - Switcher:', function () {
assert.isTrue(await switcher.isItOn());
});
+ it('should be valid - using persisted switcher key', async function () {
+ // given API responses
+ given(fetchStub, 0, { json: () => generateAuth('[auth_token]', 5), status: 200 });
+ given(fetchStub, 1, { json: () => generateResult(true), status: 200 });
+
+ // test
+ Client.buildContext(contextSettings);
+
+ // Get switcher multiple times with the same key
+ const switcher1 = Client.getSwitcher('MY_PERSISTED_SWITCHER_KEY');
+ const switcher2 = Client.getSwitcher('MY_PERSISTED_SWITCHER_KEY');
+ const differentSwitcher = Client.getSwitcher('DIFFERENT_KEY');
+
+ // Verify they are the same instance (persisted)
+ assert.strictEqual(switcher1, switcher2, 'Switcher instances should be the same (persisted)');
+ assert.notStrictEqual(switcher1, differentSwitcher, 'Different keys should create different instances');
+ assert.isTrue(await switcher1.isItOn());
+ });
+
it('should NOT throw error when default result is provided using remote', async function () {
// given API responses
given(fetchStub, 0, { json: () => generateAuth('[auth_token]', 5), status: 200 });
From 60d736a7331ea5d8c1f82210d02cd61373629e70 Mon Sep 17 00:00:00 2001
From: petruki <31597636+petruki@users.noreply.github.com>
Date: Thu, 8 Jan 2026 11:09:05 -0800
Subject: [PATCH 2/2] chore(deps): bump dev deps
---
package.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/package.json b/package.json
index ff5884f..6fda0fb 100644
--- a/package.json
+++ b/package.json
@@ -32,8 +32,8 @@
],
"devDependencies": {
"@babel/eslint-parser": "^7.28.5",
- "@typescript-eslint/eslint-plugin": "^8.50.1",
- "@typescript-eslint/parser": "^8.50.1",
+ "@typescript-eslint/eslint-plugin": "^8.52.0",
+ "@typescript-eslint/parser": "^8.52.0",
"c8": "^10.1.3",
"chai": "^6.2.2",
"env-cmd": "^11.0.0",