From 91ffac57e6de791c2db43f81bb261c49699449c6 Mon Sep 17 00:00:00 2001 From: petruki <31597636+petruki@users.noreply.github.com> Date: Thu, 8 Jan 2026 10:59:03 -0800 Subject: [PATCH 1/3] feat: added persisted Switchers for better resource utilization --- .devcontainer/Dockerfile | 2 +- .github/workflows/master.yml | 6 +- .github/workflows/sonar.yml | 4 +- README.md | 78 +++++++++++++------------- deno.jsonc | 2 +- src/client.ts | 16 +++++- src/switcher.ts | 92 ++++++++++++++++++++++--------- tests/deps.ts | 4 +- tests/playground/index.ts | 40 +++++++------- tests/switcher-client.test.ts | 13 +++-- tests/switcher-functional.test.ts | 21 ++++++- 11 files changed, 175 insertions(+), 103 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index ad4c0a8..be7d10b 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM denoland/deno:2.5.4 +FROM denoland/deno:2.6.4 # Install tools RUN apt-get update && \ diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index e8d47dd..1a1dc99 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -17,10 +17,10 @@ jobs: with: fetch-depth: 0 - - name: Setup Deno v2.6.3 + - name: Setup Deno v2.6.4 uses: denoland/setup-deno@v2 with: - deno-version: v2.6.3 + deno-version: v2.6.4 - name: Setup LCOV run: sudo apt install -y lcov @@ -45,7 +45,7 @@ jobs: strategy: fail-fast: false matrix: - deno-version: [v1.46.3, v2.6.3] + deno-version: [v1.46.3, v2.6.4] os: [ ubuntu-latest, windows-latest ] runs-on: ${{ matrix.os }} diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index a69ed3f..0614086 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -33,10 +33,10 @@ jobs: ref: ${{ steps.pr.outputs.head_sha }} fetch-depth: 0 - - name: Setup Deno v2.6.3 + - name: Setup Deno v2.6.4 uses: denoland/setup-deno@v2 with: - deno-version: v2.6.3 + deno-version: v2.6.4 - name: Setup LCOV run: sudo apt install -y lcov diff --git a/README.md b/README.md index debed6d..29749f0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ -# Switcher4Deno -
-Switcher4Deno
+Switcher Client Deno SDK
A Deno SDK for Switcher API
@@ -22,37 +20,37 @@ A Deno SDK for Switcher API ## ๐Ÿ“‹ Table of Contents -- [๐ŸŽฏ About](#-about) - - [โœจ Key Features](#-key-features) -- [๐Ÿš€ Quick Start](#-quick-start) +- [About](#-about) + - [Key Features](#-key-features) +- [Quick Start](#-quick-start) - [Prerequisites](#prerequisites) - [Installation](#installation) - [Basic Setup](#basic-setup) -- [โš™๏ธ Configuration](#๏ธ-configuration) +- [Configuration](#๏ธ-configuration) - [Context Parameters](#context-parameters) - [Advanced Options](#advanced-options) - [Options Reference](#options-reference) -- [๐Ÿ’ก Usage Examples](#-usage-examples) +- [Usage Examples](#-usage-examples) - [1. Basic Feature Flag Check](#1-basic-feature-flag-check) - [2. Strategy Validation with Input Preparation](#2-strategy-validation-with-input-preparation) - [3. All-in-One Execution](#3-all-in-one-execution) - [4. Performance Optimization with Throttling](#4-performance-optimization-with-throttling) - [5. Hybrid Mode - Force Remote Resolution](#5-hybrid-mode---force-remote-resolution) -- [๐Ÿงช Testing & Development](#-testing--development) +- [Testing & Development](#-testing--development) - [Built-in Stub Feature](#built-in-stub-feature) - [Test Mode](#test-mode) - [Smoke Testing](#smoke-testing) -- [๐Ÿ“ธ Snapshot Management](#-snapshot-management) +- [Snapshot Management](#-snapshot-management) - [Loading Snapshots](#loading-snapshots) - [Real-time Snapshot Monitoring](#real-time-snapshot-monitoring) - [Snapshot Version Checking](#snapshot-version-checking) - [Automatic Snapshot Updates](#automatic-snapshot-updates) -## ๐ŸŽฏ About +## About -**Switcher4Deno** is a feature-rich SDK for integrating [Switcher API](https://github.com/switcherapi/switcher-api) into your Deno applications. It provides robust feature flag management with enterprise-grade capabilities. +**Switcher Client Deno** (former Switcher4Deno) is a feature-rich SDK for integrating [Switcher API](https://github.com/switcherapi/switcher-api) into your Deno applications. It provides robust feature flag management with enterprise-grade capabilities. -### โœจ Key Features +### Key Features - ๐Ÿš€ **Zero Latency**: Local mode with snapshot files or in-memory for instant feature flag resolution - ๐Ÿ”„ **Hybrid Configuration**: Silent mode with automatic fallback handling @@ -60,11 +58,11 @@ A Deno SDK for Switcher API - โšก **Performance Optimized**: Throttling optimizes remote API calls to reduce bottlenecks in critical code paths - ๐Ÿ› ๏ธ **Developer Tools**: Runtime snapshot updates without app restart and automatic sync with remote API -## ๐Ÿš€ Quick Start +## Quick Start ### Prerequisites -- **Deno**: Version 1.4x or higher +- **Deno**: Version 1.4x or above - **Required Permissions**: ```bash --allow-read --allow-write --allow-net @@ -85,7 +83,7 @@ import { Client } from 'https://deno.land/x/switcher4deno@v[VERSION]/mod.ts'; ```ts import { Client } from "@switcherapi/switcher-client-deno"; -// Initialize the client context +// 1. Initialize the client Client.buildContext({ url: 'https://api.switcherapi.com', apiKey: '[YOUR_API_KEY]', @@ -94,15 +92,15 @@ Client.buildContext({ environment: 'default' }); -// Get a switcher instance -const switcher = Client.getSwitcher(); +// 2. Get a switcher instance +const switcher = Client.getSwitcher('FEATURE01'); -// Check if a feature is enabled -const isEnabled = await switcher.isItOn('FEATURE01'); +// 3. Check if a feature is enabled +const isEnabled = await switcher.isItOn(); console.log('Feature enabled:', isEnabled); ``` -## โš™๏ธ Configuration +## Configuration ### Context Parameters | Parameter | Type | Required | Description | @@ -153,31 +151,29 @@ Client.buildContext({ > **โš ๏ธ Note on regexSafe**: This feature protects against reDOS attacks but uses Web Workers, which are incompatible with compiled executables. -## ๐Ÿ’ก Usage Examples +## Usage Examples ### 1. Basic Feature Flag Check Simple on/off checks for feature flags: ```ts +// Non-persisted switcher instance const switcher = Client.getSwitcher(); - -// Synchronous (local mode only) -const isEnabled = switcher.isItOn('FEATURE01') as boolean; -const isEnabledBool = switcher.isItOnBool('FEATURE01'); -// Returns: true or false - -// With detailed response -const response = switcher.detail().isItOn('FEATURE01') as SwitcherResult; -const detailedResponse = switcher.isItOnDetail('FEATURE01'); -// Returns: { result: true, reason: 'Success', metadata: {} } - -// Asynchronous (remote/hybrid mode) -const isEnabledAsync = await switcher.isItOn('FEATURE01'); -const isEnabledBoolAsync = await switcher.isItOnBool('FEATURE01', true); -const responseAsync = await switcher.detail().isItOn('FEATURE01'); -const detailedResponseAsync = await switcher.isItOnDetail('FEATURE01', true); -// Returns: Promise or Promise +// Persisted switcher instance +const switcher = Client.getSwitcher('FEATURE01'); + +// ๐Ÿš€ Synchronous (local mode only) +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(); // 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 ``` ### 2. Strategy Validation with Input Preparation @@ -243,7 +239,7 @@ This is useful for: - Critical features that must be resolved remotely - Real-time configuration updates -## ๐Ÿงช Testing & Development +## Testing & Development ### Built-in Stub Feature @@ -301,7 +297,7 @@ try { } ``` -## ๐Ÿ“ธ Snapshot Management +## Snapshot Management Snapshots enable zero-latency local mode by caching your feature flag configuration. diff --git a/deno.jsonc b/deno.jsonc index 4d3fea3..de987dd 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,7 +1,7 @@ { "name": "@switcherapi/switcher-client-deno", "version": "2.4.1", - "description": "Switcher4Deno is a Feature Flag Deno Client SDK for Switcher API", + "description": "Switcher Client Deno is a Feature Flag Deno Client SDK for Switcher API", "tasks": { "cache-reload": "deno cache --reload --lock=deno.lock mod.ts", "fmt": "deno fmt mod.ts src/ --options-single-quote --options-line-width=120 --check", diff --git a/src/client.ts b/src/client.ts index d948bfe..2d58565 100644 --- a/src/client.ts +++ b/src/client.ts @@ -46,6 +46,7 @@ import { SnapshotWatcher } from './lib/snapshotWatcher.ts'; */ export class Client { private static readonly _snapshotWatcher = new SnapshotWatcher(); + private static readonly _switchers = new Map(); private static _testEnabled: boolean; private static _context: SwitcherContext; @@ -131,8 +132,21 @@ export class Client { * Creates a new instance of Switcher */ static getSwitcher(key?: string): Switcher { - return new Switcher(util.get(key, '')) + const keyValue = util.get(key, ''); + const persistedSwitcher = Client._switchers.get(keyValue); + + if (persistedSwitcher) { + return persistedSwitcher; + } + + const switcher = new Switcher(keyValue) .restrictRelay(GlobalOptions.restrictRelay); + + if (keyValue) { + Client._switchers.set(keyValue, switcher); + } + + return switcher; } /** diff --git a/src/switcher.ts b/src/switcher.ts index 9525fb6..78a52af 100644 --- a/src/switcher.ts +++ b/src/switcher.ts @@ -27,8 +27,8 @@ 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 extends SwitcherBuilder implements SwitcherRequest { @@ -38,73 +38,113 @@ export class Switcher extends SwitcherBuilder implements SwitcherRequest { } /** - * Execute criteria with boolean result (synchronous version) + * Execute criteria with boolean result (synchronous, uses persisted key) + * + * @returns boolean result + */ + isItOnBool(): boolean; + + /** + * 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 boolean value + * @returns Promise result */ - isItOnBool(key: string, forceAsync?: false): boolean; + isItOnBool(forceAsync: true): Promise; /** - * Execute criteria with boolean result (asynchronous version) + * Execute criteria with boolean result (asynchronous) * * @param key - switcher key * @param forceAsync - when true, forces async execution - * @returns Promise value + * @returns Promise result */ - isItOnBool(key: string, forceAsync?: true): Promise; + isItOnBool(key: string, forceAsync: true): Promise; /** * Execute criteria with boolean result * - * @param key - switcher key - * @param forceAsync - when true, forces async execution - * @returns boolean value or Promise based on execution mode + * @param arg1 - switcher key or forceAsync boolean + * @param arg2 - when true, forces async execution + * @returns boolean result or Promise based on execution mode */ - isItOnBool(key: string, forceAsync?: boolean): Promise | boolean { + isItOnBool(arg1?: string | boolean, arg2?: boolean): Promise | boolean { this.detail(false); - if (forceAsync) { - return this.isItOn(key) as Promise; + // Handle case where first argument is forceAsync boolean + if (typeof arg1 === 'boolean') { + arg2 = arg1; + arg1 = undefined; } - return this.isItOn(key) as boolean; + if (arg2) { + return this.isItOn(arg1) as Promise; + } + + return this.isItOn(arg1) as boolean; } /** - * 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 Promise object + */ + isItOnDetail(key: string, forceAsync: true): Promise; + + /** + * Execute criteria with detail information + * + * @param arg1 - switcher key + * @param arg2 - when true, forces async execution * @returns SwitcherResult or Promise based on execution mode */ - isItOnDetail(key: string, forceAsync?: boolean): Promise | SwitcherResult { + isItOnDetail(arg1?: string | boolean, arg2?: boolean): Promise | SwitcherResult { this.detail(true); - if (forceAsync) { - return this.isItOn(key) as Promise; + // Handle case where first argument is forceAsync boolean + if (typeof arg1 === 'boolean') { + arg2 = arg1; + arg1 = undefined; + } + + if (arg2) { + return this.isItOn(arg1) as Promise; } - return this.isItOn(key) as SwitcherResult; + return this.isItOn(arg1) as SwitcherResult; } /** diff --git a/tests/deps.ts b/tests/deps.ts index 03af5d6..d681351 100644 --- a/tests/deps.ts +++ b/tests/deps.ts @@ -10,7 +10,9 @@ export { assertExists, assertNotEquals, assertGreater, - assertArrayIncludes + assertArrayIncludes, + assertStrictEquals, + assertNotStrictEquals } from 'jsr:@std/assert@1.0.16'; export { assertSpyCalls, spy } from 'jsr:@std/testing@1.0.16/mock'; export { diff --git a/tests/playground/index.ts b/tests/playground/index.ts index 2d88f43..b5158ff 100644 --- a/tests/playground/index.ts +++ b/tests/playground/index.ts @@ -25,7 +25,7 @@ async function setupSwitcher(local: boolean) { } /** - * 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-deno (former: Switcher4Deno) locally. * No remote API account is required. * * Snapshot is loaded from file at tests/playground/snapshot/local.json @@ -61,11 +61,11 @@ const _testSimpleAPICall = async (local: boolean) => { .then(() => console.log('Switcher checked')) .catch(error => console.log(error)); - switcher = Client.getSwitcher(); + 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); }; @@ -77,14 +77,14 @@ const _testThrottledAPICall = async () => { await Client.checkSwitchers([SWITCHER_KEY]); Client.subscribeNotifyError((error) => console.log(error)); - switcher = Client.getSwitcher(); - switcher.throttle(1000); + switcher = Client.getSwitcher(SWITCHER_KEY); setInterval(async () => { const time = Date.now(); const result = await switcher + .throttle(1000) .detail() - .isItOn(SWITCHER_KEY); + .isItOn(); console.log(`- ${Date.now() - time} ms - ${JSON.stringify(result)}`); }, 1000); @@ -100,14 +100,14 @@ const _testSnapshotUpdate = async () => { console.log('checkSnapshot:', await Client.checkSnapshot()); }; -// Does not require remote API +// Requires remote API const _testAsyncCall = async () => { - setupSwitcher(true); - switcher = Client.getSwitcher(); + setupSwitcher(false); + switcher = Client.getSwitcher(SWITCHER_KEY); - console.log("Sync:", await switcher.isItOn(SWITCHER_KEY)); + console.log("Sync:", await switcher.isItOn()); - (switcher.isItOn(SWITCHER_KEY) as Promise) + (switcher.isItOn() as Promise) .then(res => console.log('Promise result:', res)) .catch(error => console.log(error)); }; @@ -115,17 +115,17 @@ const _testAsyncCall = async () => { // Does not require remote API const _testBypasser = async () => { setupSwitcher(true); - switcher = Client.getSwitcher(); + 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(); @@ -149,7 +149,7 @@ const _testWatchSnapshot = async () => { setInterval(() => { const time = Date.now(); - const result = switcher.isItOn(SWITCHER_KEY); + const result = switcher.isItOn(); console.log(`- ${Date.now() - time} ms - ${JSON.stringify(result)}`); }, 1000); @@ -166,13 +166,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); @@ -184,7 +184,7 @@ const _testSnapshotAutoUpdate = async () => { { local: true, logger: true }); await Client.loadSnapshot({ fetchRemote: true }); - const switcher = Client.getSwitcher(); + const switcher = Client.getSwitcher(SWITCHER_KEY); Client.scheduleSnapshotAutoUpdate(3, { success: (updated) => console.log('In-memory snapshot updated', updated), @@ -193,7 +193,7 @@ const _testSnapshotAutoUpdate = async () => { setInterval(async () => { const time = Date.now(); - await switcher.isItOn(SWITCHER_KEY); + await switcher.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.ts b/tests/switcher-client.test.ts index 7180512..b2ee7d7 100644 --- a/tests/switcher-client.test.ts +++ b/tests/switcher-client.test.ts @@ -43,13 +43,14 @@ describe('E2E test - Client local #1:', function () { await switcher .checkValue('Japan') .checkNetwork('10.0.0.3') - .prepare(); + .prepare('FF2FOR2020'); - assertTrue(await switcher.isItOn('FF2FOR2020') === true); - assertTrue(switcher.isItOnBool('FF2FOR2020') === true); - assertTrue(await switcher.isItOnBool('FF2FOR2020', true) === true); - assertTrue(switcher.isItOnDetail('FF2FOR2020').result === true); - assertTrue((await switcher.isItOnDetail('FF2FOR2020', true)).result === true); + assertTrue(switcher.isItOn() === true); + assertTrue(await switcher.isItOn() === true); + assertTrue(switcher.isItOnBool() === true); + assertTrue(await switcher.isItOnBool(true) === true); + assertTrue(switcher.isItOnDetail().result === true); + assertTrue((await switcher.isItOnDetail(true)).result === true); }); it('should get execution from logger', async function () { diff --git a/tests/switcher-functional.test.ts b/tests/switcher-functional.test.ts index 2ca1ba9..ef4c93f 100644 --- a/tests/switcher-functional.test.ts +++ b/tests/switcher-functional.test.ts @@ -1,6 +1,6 @@ import { describe, it, afterAll, afterEach, beforeEach, assertEquals, assertRejects, assertThrows, assertFalse, - assertSpyCalls, spy } from './deps.ts'; + assertSpyCalls, assertStrictEquals, assertNotStrictEquals, spy } from './deps.ts'; import { given, givenError, tearDown, assertTrue, generateAuth, generateResult, generateDetailedResult, sleep } from './helper/utils.ts' import { Client, type SwitcherResult, type SwitcherContext } from '../mod.ts'; @@ -52,6 +52,25 @@ describe('Integrated test - Client:', function () { assertTrue(await switcher.isItOn()); }); + it('should be valid - using persisted switcher key', async function () { + // given API responses + given('POST@/criteria/auth', generateAuth('[auth_token]', 5)); + given('POST@/criteria', generateResult(true)); + + // 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) + assertStrictEquals(switcher1, switcher2, 'Switcher instances should be the same (persisted)'); + assertNotStrictEquals(switcher1, differentSwitcher, 'Different keys should create different instances'); + assertTrue(await switcher1.isItOn()); + }); + it('should NOT throw error when default result is provided using remote', async function () { // given API responses given('POST@/criteria/auth', generateAuth('[auth_token]', 5)); From 05ea3b26390959d69e0c16bf85eb7d3a4b3409b7 Mon Sep 17 00:00:00 2001 From: petruki <31597636+petruki@users.noreply.github.com> Date: Thu, 8 Jan 2026 11:02:13 -0800 Subject: [PATCH 2/3] chore(docs): elaborated getSwitcher new persistent option --- src/client.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/client.ts b/src/client.ts index 2d58565..043dc61 100644 --- a/src/client.ts +++ b/src/client.ts @@ -129,7 +129,9 @@ export class Client { } /** - * 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 { const keyValue = util.get(key, ''); From c77e3817d2490e781db75f4fb5acf59360f3eedb Mon Sep 17 00:00:00 2001 From: petruki <31597636+petruki@users.noreply.github.com> Date: Thu, 8 Jan 2026 11:06:31 -0800 Subject: [PATCH 3/3] chore: formatting issue --- src/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.ts b/src/client.ts index 043dc61..e93671e 100644 --- a/src/client.ts +++ b/src/client.ts @@ -130,7 +130,7 @@ export class Client { /** * Creates a new instance of Switcher. - * + * * Provide a key if you want to persist the instance. */ static getSwitcher(key?: string): Switcher {