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",