From a32ea73eae3f2eea04a60748c2186c97b7b47faf Mon Sep 17 00:00:00 2001 From: petruki <31597636+petruki@users.noreply.github.com> Date: Mon, 19 May 2025 13:27:24 -0700 Subject: [PATCH 1/2] Removing cycle dependency: remote-auth and remote --- package.json | 10 +++++----- src/lib/globals/globalAuth.js | 31 +++++++++++++++++++++++++++++++ src/lib/remote-auth.js | 28 +++++++++------------------- src/lib/remote.js | 18 +++++++++--------- src/switcher.js | 5 +++-- 5 files changed, 57 insertions(+), 35 deletions(-) create mode 100644 src/lib/globals/globalAuth.js diff --git a/package.json b/package.json index b5d2271..3fa3563 100644 --- a/package.json +++ b/package.json @@ -30,14 +30,14 @@ "src/" ], "devDependencies": { - "@babel/eslint-parser": "^7.27.0", - "@typescript-eslint/eslint-plugin": "^8.31.0", - "@typescript-eslint/parser": "^8.31.0", + "@babel/eslint-parser": "^7.27.1", + "@typescript-eslint/eslint-plugin": "^8.32.1", + "@typescript-eslint/parser": "^8.32.1", "c8": "^10.1.3", "chai": "^5.2.0", "env-cmd": "^10.1.0", - "eslint": "^9.25.1", - "mocha": "^11.1.0", + "eslint": "^9.27.0", + "mocha": "^11.4.0", "mocha-sonarqube-reporter": "^1.0.2", "sinon": "^20.0.0" }, diff --git a/src/lib/globals/globalAuth.js b/src/lib/globals/globalAuth.js new file mode 100644 index 0000000..6f300e1 --- /dev/null +++ b/src/lib/globals/globalAuth.js @@ -0,0 +1,31 @@ +export class GlobalAuth { + static #token; + static #exp; + static #url; + + static init(url) { + this.#url = url; + this.#token = undefined; + this.#exp = undefined; + } + + static get token() { + return this.#token; + } + + static set token(value) { + this.#token = value; + } + + static get exp() { + return this.#exp; + } + + static set exp(value) { + this.#exp = value; + } + + static get url() { + return this.#url; + } +} diff --git a/src/lib/remote-auth.js b/src/lib/remote-auth.js index 9573ba9..5f636f8 100644 --- a/src/lib/remote-auth.js +++ b/src/lib/remote-auth.js @@ -1,3 +1,4 @@ +import { GlobalAuth } from './globals/globalAuth.js'; import { auth, checkAPIHealth } from './remote.js'; import DateMoment from './utils/datemoment.js'; import * as util from './utils/index.js'; @@ -8,13 +9,10 @@ import * as util from './utils/index.js'; export class Auth { static #context; static #retryOptions; - static #token; - static #exp; static init(context) { this.#context = context; - this.#token = undefined; - this.#exp = undefined; + GlobalAuth.init(context.url); } static setRetryOptions(silentMode) { @@ -26,18 +24,18 @@ export class Auth { static async auth() { const response = await auth(this.#context); - this.#token = response.token; - this.#exp = response.exp; + GlobalAuth.token = response.token; + GlobalAuth.exp = response.exp; } static checkHealth() { - if (this.#token !== 'SILENT') { + if (GlobalAuth.token !== 'SILENT') { return; } if (this.isTokenExpired()) { this.updateSilentToken(); - checkAPIHealth(util.get(this.getURL(), '')) + checkAPIHealth(util.get(GlobalAuth.url, '')) .then((isAlive) => { if (isAlive) { this.auth(); @@ -50,12 +48,12 @@ export class Auth { const expirationTime = new DateMoment(new Date()) .add(this.#retryOptions.retryTime, this.#retryOptions.retryDurationIn).getDate(); - this.#token = 'SILENT'; - this.#exp = Math.round(expirationTime.getTime() / 1000); + GlobalAuth.token = 'SILENT'; + GlobalAuth.exp = Math.round(expirationTime.getTime() / 1000); } static isTokenExpired() { - return !this.#exp || Date.now() > (this.#exp * 1000); + return !GlobalAuth.exp || Date.now() > (GlobalAuth.exp * 1000); } static isValid() { @@ -79,12 +77,4 @@ export class Auth { return true; } - - static getToken() { - return this.#token; - } - - static getURL() { - return this.#context.url; - } } diff --git a/src/lib/remote.js b/src/lib/remote.js index 1615858..4f339eb 100644 --- a/src/lib/remote.js +++ b/src/lib/remote.js @@ -3,8 +3,8 @@ import { Agent } from 'node:https'; import { AuthError, CheckSwitcherError, CriteriaError, SnapshotServiceError } from './exceptions/index.js'; import FetchFacade from './utils/fetchFacade.js'; -import { Auth } from './remote-auth.js'; import * as util from './utils/index.js'; +import { GlobalAuth } from './globals/globalAuth.js'; let httpClient; @@ -85,10 +85,10 @@ export async function checkAPIHealth(url) { export async function checkCriteria(key, input, showDetail = false) { try { const entry = getEntry(input); - const response = await FetchFacade.fetch(`${Auth.getURL()}/criteria?showReason=${showDetail}&key=${key}`, { + const response = await FetchFacade.fetch(`${GlobalAuth.url}/criteria?showReason=${showDetail}&key=${key}`, { method: 'post', body: JSON.stringify({ entry }), - headers: getHeader(Auth.getToken()), + headers: getHeader(GlobalAuth.token), agent: httpClient }); @@ -104,10 +104,10 @@ export async function checkCriteria(key, input, showDetail = false) { export async function checkSwitchers(switcherKeys) { try { - const response = await FetchFacade.fetch(`${Auth.getURL()}/criteria/switchers_check`, { + const response = await FetchFacade.fetch(`${GlobalAuth.url}/criteria/switchers_check`, { method: 'post', body: JSON.stringify({ switchers: switcherKeys }), - headers: getHeader(Auth.getToken()), + headers: getHeader(GlobalAuth.token), agent: httpClient }); @@ -126,9 +126,9 @@ export async function checkSwitchers(switcherKeys) { export async function checkSnapshotVersion(version) { try { - const response = await FetchFacade.fetch(`${Auth.getURL()}/criteria/snapshot_check/${version}`, { + const response = await FetchFacade.fetch(`${GlobalAuth.url}/criteria/snapshot_check/${version}`, { method: 'get', - headers: getHeader(Auth.getToken()), + headers: getHeader(GlobalAuth.token), agent: httpClient }); @@ -159,10 +159,10 @@ export async function resolveSnapshot(domain, environment, component) { }; try { - const response = await FetchFacade.fetch(`${Auth.getURL()}/graphql`, { + const response = await FetchFacade.fetch(`${GlobalAuth.url}/graphql`, { method: 'post', body: JSON.stringify(data), - headers: getHeader(Auth.getToken()), + headers: getHeader(GlobalAuth.token), agent: httpClient }); diff --git a/src/switcher.js b/src/switcher.js index a65ae80..f21f1a5 100644 --- a/src/switcher.js +++ b/src/switcher.js @@ -6,6 +6,7 @@ import { Client } from './client.js'; import * as remote from './lib/remote.js'; import * as util from './lib/utils/index.js'; import { Auth } from './lib/remote-auth.js'; +import { GlobalAuth } from './lib/globals/globalAuth.js'; export class Switcher { #delay = 0; @@ -38,7 +39,7 @@ export class Switcher { } await this.#executeApiValidation(); - if (!Auth.getToken()) { + if (!GlobalAuth.token) { errors.push('Missing token field'); } @@ -66,7 +67,7 @@ export class Switcher { // otherwise, execute remote criteria or local snapshot when silent mode is enabled await this.validate(); - if (Auth.getToken() === 'SILENT') { + if (GlobalAuth.token === 'SILENT') { result = await this._executeLocalCriteria(); } else { result = await this._executeRemoteCriteria(); From 4f351ce5d8e3da19d80883c273574277df52c4a7 Mon Sep 17 00:00:00 2001 From: petruki <31597636+petruki@users.noreply.github.com> Date: Mon, 19 May 2025 13:47:31 -0700 Subject: [PATCH 2/2] Removing cycle dependency: switcher and client --- .github/workflows/master.yml | 2 +- src/client.js | 71 ++++++++++------------- src/lib/globals/globalOptions.js | 36 ++++++++++++ src/lib/globals/globalSnapshot.js | 15 +++++ src/lib/{remote-auth.js => remoteAuth.js} | 0 src/lib/utils/payloadReader.js | 2 +- src/switcher.js | 21 +++---- test/playground/snapshot/default.json | 12 ++-- test/playground/snapshot/local.json | 11 ++-- 9 files changed, 108 insertions(+), 62 deletions(-) create mode 100644 src/lib/globals/globalOptions.js create mode 100644 src/lib/globals/globalSnapshot.js rename src/lib/{remote-auth.js => remoteAuth.js} (100%) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index d9a5472..7ac57b7 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -29,7 +29,7 @@ jobs: SWITCHER_API_KEY: ${{ secrets.SWITCHER_API_KEY }} - name: SonarCloud Scan - uses: sonarsource/sonarqube-scan-action@v5.1.0 + uses: sonarsource/sonarqube-scan-action@v5.2.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/src/client.js b/src/client.js index 5c678a3..a330890 100644 --- a/src/client.js +++ b/src/client.js @@ -18,28 +18,28 @@ import SnapshotAutoUpdater from './lib/utils/snapshotAutoUpdater.js'; import { SnapshotNotFoundError } from './lib/exceptions/index.js'; import { loadDomain, validateSnapshot, checkSwitchersLocal } from './lib/snapshot.js'; import { Switcher } from './switcher.js'; -import { Auth } from './lib/remote-auth.js'; +import { Auth } from './lib/remoteAuth.js'; +import { GlobalOptions } from './lib/globals/globalOptions.js'; +import { GlobalSnapshot } from './lib/globals/globalSnapshot.js'; export class Client { - static #options; static #context; - static #snapshot; static buildContext(context, options) { this.testEnabled = DEFAULT_TEST_MODE; - this.#snapshot = undefined; this.#context = context; this.#context.environment = util.get(context.environment, DEFAULT_ENVIRONMENT); // Default values - this.#options = { + GlobalSnapshot.clear(); + GlobalOptions.init({ snapshotAutoUpdateInterval: 0, snapshotLocation: options?.snapshotLocation, local: util.get(options?.local, DEFAULT_LOCAL), logger: util.get(options?.logger, DEFAULT_LOGGER) - }; + }); if (options) { Client.#buildOptions(options); @@ -60,7 +60,7 @@ export class Client { } if (SWITCHER_OPTIONS.SNAPSHOT_AUTO_UPDATE_INTERVAL in options) { - this.#options.snapshotAutoUpdateInterval = options.snapshotAutoUpdateInterval; + GlobalOptions.updateOptions({ snapshotAutoUpdateInterval: options.snapshotAutoUpdateInterval }); this.scheduleSnapshotAutoUpdate(); } @@ -70,7 +70,7 @@ export class Client { static #initSilentMode(silentMode) { Auth.setRetryOptions(silentMode); - Client.#options.silentMode = silentMode; + GlobalOptions.updateOptions({ silentMode }); Client.loadSnapshot(); } @@ -94,7 +94,7 @@ export class Client { } static async checkSnapshot() { - if (!Client.#snapshot) { + if (!GlobalSnapshot.snapshot) { throw new SnapshotNotFoundError('Snapshot is not loaded. Use Client.loadSnapshot()'); } @@ -104,15 +104,15 @@ export class Client { const snapshot = await validateSnapshot( Client.#context, - Client.#snapshot.data.domain.version + GlobalSnapshot.snapshot.data.domain.version ); if (snapshot) { - if (Client.#options.snapshotLocation?.length) { - writeFileSync(`${Client.#options.snapshotLocation}/${Client.#context.environment}.json`, snapshot); + if (GlobalOptions.snapshotLocation?.length) { + writeFileSync(`${GlobalOptions.snapshotLocation}/${Client.#context.environment}.json`, snapshot); } - Client.#snapshot = JSON.parse(snapshot); + GlobalSnapshot.init(JSON.parse(snapshot)); return true; } @@ -120,13 +120,13 @@ export class Client { } static async loadSnapshot(options = { fetchRemote: false, watchSnapshot: false }) { - Client.#snapshot = loadDomain( - util.get(Client.#options.snapshotLocation, ''), + GlobalSnapshot.init(loadDomain( + util.get(GlobalOptions.snapshotLocation, ''), util.get(Client.#context.environment, DEFAULT_ENVIRONMENT) - ); + )); - if (Client.#snapshot.data.domain.version == 0 && - (options.fetchRemote || !Client.#options.local)) { + if (GlobalSnapshot.snapshot.data.domain.version == 0 && + (options.fetchRemote || !GlobalOptions.local)) { await Client.checkSnapshot(); } @@ -134,22 +134,22 @@ export class Client { Client.watchSnapshot(); } - return Client.#snapshot?.data.domain.version || 0; + return GlobalSnapshot.snapshot?.data.domain.version || 0; } static watchSnapshot(callback = {}) { const { success = () => {}, reject = () => {} } = callback; - if (Client.testEnabled || !Client.#options.snapshotLocation?.length) { + if (Client.testEnabled || !GlobalOptions.snapshotLocation?.length) { return reject(new Error('Watch Snapshot cannot be used in test mode or without a snapshot location')); } - const snapshotFile = `${Client.#options.snapshotLocation}/${Client.#context.environment}.json`; + const snapshotFile = `${GlobalOptions.snapshotLocation}/${Client.#context.environment}.json`; let lastUpdate; watchFile(snapshotFile, (listener) => { try { if (!lastUpdate || listener.ctime > lastUpdate) { - Client.#snapshot = loadDomain(Client.#options.snapshotLocation, Client.#context.environment); + GlobalSnapshot.init(loadDomain(GlobalOptions.snapshotLocation, Client.#context.environment)); success(); } } catch (e) { @@ -165,8 +165,8 @@ export class Client { return; } - const snapshotFile = `${Client.#options.snapshotLocation}${Client.#context.environment}.json`; - Client.#snapshot = undefined; + const snapshotFile = `${GlobalOptions.snapshotLocation}${Client.#context.environment}.json`; + GlobalSnapshot.clear(); unwatchFile(snapshotFile); } @@ -174,12 +174,12 @@ export class Client { const { success = () => {}, reject = () => {} } = callback; if (interval) { - Client.#options.snapshotAutoUpdateInterval = interval; + GlobalOptions.updateOptions({ snapshotAutoUpdateInterval: interval }); } - if (Client.#options.snapshotAutoUpdateInterval && Client.#options.snapshotAutoUpdateInterval > 0) { + if (GlobalOptions.snapshotAutoUpdateInterval && GlobalOptions.snapshotAutoUpdateInterval > 0) { SnapshotAutoUpdater.schedule( - Client.#options.snapshotAutoUpdateInterval, + GlobalOptions.snapshotAutoUpdateInterval, this.checkSnapshot, success, reject @@ -192,8 +192,8 @@ export class Client { } static async checkSwitchers(switcherKeys) { - if (Client.#options.local && Client.#snapshot) { - checkSwitchersLocal(Client.#snapshot, switcherKeys); + if (GlobalOptions.local && GlobalSnapshot.snapshot) { + checkSwitchersLocal(GlobalSnapshot.snapshot, switcherKeys); } else { await Client._checkSwitchersRemote(switcherKeys); } @@ -204,8 +204,8 @@ export class Client { await Auth.auth(); await remote.checkSwitchers(switcherKeys); } catch (e) { - if (Client.#options.silentMode) { - checkSwitchersLocal(Client.#snapshot, switcherKeys); + if (GlobalOptions.silentMode) { + checkSwitchersLocal(GlobalSnapshot.snapshot, switcherKeys); } else { throw e; } @@ -239,15 +239,6 @@ export class Client { static testMode(testEnabled = true) { Client.testEnabled = testEnabled; } - - static get options() { - return Client.#options; - } - - static get snapshot() { - return Client.#snapshot; - } - } // Type export placeholders diff --git a/src/lib/globals/globalOptions.js b/src/lib/globals/globalOptions.js new file mode 100644 index 0000000..01b7582 --- /dev/null +++ b/src/lib/globals/globalOptions.js @@ -0,0 +1,36 @@ +export class GlobalOptions { + static #options; + + static init(options) { + this.#options = { + ...options, + }; + } + + static updateOptions(options) { + this.#options = { + ...this.#options, + ...options, + }; + } + + static get local() { + return this.#options.local; + } + + static get logger() { + return this.#options.logger; + } + + static get snapshotLocation() { + return this.#options.snapshotLocation; + } + + static get snapshotAutoUpdateInterval() { + return this.#options.snapshotAutoUpdateInterval; + } + + static get silentMode() { + return this.#options.silentMode; + } +} diff --git a/src/lib/globals/globalSnapshot.js b/src/lib/globals/globalSnapshot.js new file mode 100644 index 0000000..70e66cd --- /dev/null +++ b/src/lib/globals/globalSnapshot.js @@ -0,0 +1,15 @@ +export class GlobalSnapshot { + static #snapshotStore; + + static init(snapshot) { + this.#snapshotStore = snapshot; + } + + static clear() { + this.#snapshotStore = undefined; + } + + static get snapshot() { + return this.#snapshotStore; + } +} diff --git a/src/lib/remote-auth.js b/src/lib/remoteAuth.js similarity index 100% rename from src/lib/remote-auth.js rename to src/lib/remoteAuth.js diff --git a/src/lib/utils/payloadReader.js b/src/lib/utils/payloadReader.js index 9f9255f..c96a720 100644 --- a/src/lib/utils/payloadReader.js +++ b/src/lib/utils/payloadReader.js @@ -1,5 +1,5 @@ export function payloadReader(payload) { - let payloadRead = payload + '' === payload || payload || 0; + let payloadRead = payload + '' === payload || payload; if (Array.isArray(payloadRead)) { return payloadRead.flatMap(p => payloadReader(p)); } diff --git a/src/switcher.js b/src/switcher.js index f21f1a5..ca64e5d 100644 --- a/src/switcher.js +++ b/src/switcher.js @@ -2,11 +2,12 @@ import Bypasser from './lib/bypasser/index.js'; import ExecutionLogger from './lib/utils/executionLogger.js'; import checkCriteriaLocal from './lib/resolver.js'; import { StrategiesType } from './lib/snapshot.js'; -import { Client } from './client.js'; import * as remote from './lib/remote.js'; import * as util from './lib/utils/index.js'; -import { Auth } from './lib/remote-auth.js'; +import { Auth } from './lib/remoteAuth.js'; import { GlobalAuth } from './lib/globals/globalAuth.js'; +import { GlobalOptions } from './lib/globals/globalOptions.js'; +import { GlobalSnapshot } from './lib/globals/globalSnapshot.js'; export class Switcher { #delay = 0; @@ -24,7 +25,7 @@ export class Switcher { async prepare(key) { this.#validateArgs(key); - if (!Client.options.local || this.#forceRemote) { + if (!GlobalOptions.local || this.#forceRemote) { await Auth.auth(); } } @@ -61,7 +62,7 @@ export class Switcher { try { // verify if query from local snapshot - if (Client.options.local && !this.#forceRemote) { + if (GlobalOptions.local && !this.#forceRemote) { return await this._executeLocalCriteria(); } @@ -75,7 +76,7 @@ export class Switcher { } catch (err) { this.#notifyError(err); - if (Client.options.silentMode) { + if (GlobalOptions.silentMode) { Auth.updateSilentToken(); return this._executeLocalCriteria(); } @@ -90,14 +91,14 @@ export class Switcher { this.#delay = delay; if (delay > 0) { - Client.options.logger = true; + GlobalOptions.updateOptions({ logger: true }); } return this; } remote(forceRemote = true) { - if (!Client.options.local) { + if (!GlobalOptions.local) { throw new Error('Local mode is not enabled'); } @@ -166,7 +167,7 @@ export class Switcher { responseCriteria = this.#getDefaultResultOrThrow(err); } - if (Client.options.logger && this.#key) { + if (GlobalOptions.logger && this.#key) { ExecutionLogger.add(responseCriteria, this.#key, this.#input); } } else { @@ -220,13 +221,13 @@ export class Switcher { response = await checkCriteriaLocal( util.get(this.#key, ''), util.get(this.#input, []), - Client.snapshot + GlobalSnapshot.snapshot ); } catch (err) { response = this.#getDefaultResultOrThrow(err); } - if (Client.options.logger) { + if (GlobalOptions.logger) { ExecutionLogger.add(response, this.#key, this.#input); } diff --git a/test/playground/snapshot/default.json b/test/playground/snapshot/default.json index 54af679..7747d85 100644 --- a/test/playground/snapshot/default.json +++ b/test/playground/snapshot/default.json @@ -1,21 +1,21 @@ { "data": { "domain": { - "name": "Playground", - "version": 1653971338700, + "name": "Switcher API", + "version": 1, "activated": true, "group": [ { - "name": "Release 1", + "name": "Test Project", "activated": true, "config": [ { - "key": "MY_SWITCHER", + "key": "CLIENT_JS_FEATURE", "activated": true, "strategies": [ { "strategy": "VALUE_VALIDATION", - "activated": true, + "activated": false, "operation": "EXIST", "values": [ "user_1" @@ -23,7 +23,7 @@ } ], "components": [ - "switcher-playground" + "switcher-client-js" ] } ] diff --git a/test/playground/snapshot/local.json b/test/playground/snapshot/local.json index 7c79c10..3df5455 100644 --- a/test/playground/snapshot/local.json +++ b/test/playground/snapshot/local.json @@ -1,18 +1,21 @@ { "data": { "domain": { - "name": "Local Playground", + "name": "Switcher API", "version": 1, "activated": true, "group": [ { - "name": "My Features", + "name": "Test Project", "activated": true, "config": [ { - "key": "MY_SWITCHER", + "key": "CLIENT_JS_FEATURE", "activated": true, - "strategies": [] + "strategies": [], + "components": [ + "switcher-client-js" + ] } ] }