Skip to content

Commit

Permalink
refactor: feature toggle using hathor unleash client
Browse files Browse the repository at this point in the history
* refactor: feature toggle using hathor unleash client (#421)

* feat: handling feature updates

* refactor: using FetchTogglesStatus enum

* chore: added @hathor/unleash-client as a dependency
  • Loading branch information
andreabadesso authored and alexruzenhack committed May 15, 2024
1 parent 9de4b5a commit 8827c2a
Show file tree
Hide file tree
Showing 3 changed files with 42 additions and 97 deletions.
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@fortawesome/free-regular-svg-icons": "6.4.0",
"@fortawesome/free-solid-svg-icons": "6.4.0",
"@fortawesome/react-native-fontawesome": "0.2.7",
"@hathor/unleash-client": "0.1.0",
"@hathor/wallet-lib": "1.0.1",
"@notifee/react-native": "5.7.0",
"@react-native-async-storage/async-storage": "1.19.0",
Expand Down
129 changes: 32 additions & 97 deletions src/sagas/featureToggle.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,25 @@

import { Platform } from 'react-native';
import VersionNumber from 'react-native-version-number';
import { UnleashClient, EVENTS as UnleashEvents } from 'unleash-proxy-client';
import UnleashClient, { FetchTogglesStatus } from '@hathor/unleash-client';
import { get } from 'lodash';

import {
takeEvery,
all,
call,
delay,
put,
cancelled,
select,
race,
take,
fork,
spawn,
takeEvery,
} from 'redux-saga/effects';
import { eventChannel } from 'redux-saga';
import { getUniqueId } from 'react-native-device-info';
import {
types,
setUnleashClient,
setFeatureToggles,
featureToggleInitialized,
types,
} from '../actions';
import {
UNLEASH_URL,
Expand All @@ -40,7 +36,6 @@ import {
} from '../constants';
import { disableFeaturesIfNeeded } from './helpers';

const CONNECT_TIMEOUT = 10000;
const MAX_RETRIES = 5;

export function* handleInitFailed(currentRetry) {
Expand Down Expand Up @@ -71,9 +66,10 @@ export function* fetchTogglesRoutine() {
const unleashClient = yield select((state) => state.unleashClient);

try {
// This call always make unleash to emit the event 'UPDATE',
// which by its turn triggers the action 'FEATURE_TOGGLE_UPDATE'
yield call(() => unleashClient.fetchToggles());
const state = yield call(() => unleashClient.fetchToggles());
if (state === FetchTogglesStatus.Updated) {
yield put({ type: types.FEATURE_TOGGLE_UPDATE });
}
} catch (e) {
// No need to do anything here as it will try again automatically in
// UNLEASH_POLLING_INTERVAL. Just prevent it from crashing the saga.
Expand All @@ -82,15 +78,19 @@ export function* fetchTogglesRoutine() {
}
}

export function* monitorFeatureFlags(currentRetry = 0) {
const unleashClient = new UnleashClient({
url: UNLEASH_URL,
clientKey: UNLEASH_CLIENT_KEY,
refreshInterval: -1,
disableRefresh: true, // Disable it, we will handle it ourselves
appName: `wallet-mobile-${Platform.OS}`,
});
export function* handleToggleUpdate() {
console.log('Handling feature toggle update');
const unleashClient = yield select((state) => state.unleashClient);
const networkSettings = yield select((state) => state.networkSettings);

const toggles = unleashClient.getToggles();
const featureToggles = disableFeaturesIfNeeded(networkSettings, mapFeatureToggles(toggles));

yield put(setFeatureToggles(featureToggles));
yield put({ type: types.FEATURE_TOGGLE_UPDATED });
}

export function* monitorFeatureFlags(currentRetry = 0) {
const { appVersion } = VersionNumber;

const options = {
Expand All @@ -102,86 +102,37 @@ export function* monitorFeatureFlags(currentRetry = 0) {
},
};

const unleashClient = new UnleashClient({
url: UNLEASH_URL,
clientKey: UNLEASH_CLIENT_KEY,
refreshInterval: -1,
disableRefresh: true, // Disable it, we will handle it ourselves
appName: `wallet-mobile-${Platform.OS}`,
context: options,
});

try {
yield call(() => unleashClient.updateContext(options));
yield put(setUnleashClient(unleashClient));

// Listeners should be set before unleashClient.start so we don't miss
// updates
yield fork(setupUnleashListeners, unleashClient);

// Start without awaiting it so we can listen for the
// READY event
unleashClient.start();

const { error, timeout } = yield race({
error: take(types.FEATURE_TOGGLE_ERROR),
success: take(types.FEATURE_TOGGLE_READY),
timeout: delay(CONNECT_TIMEOUT),
});

if (error || timeout) {
throw new Error('Error or timeout while connecting to unleash proxy.');
}
yield call(() => unleashClient.fetchToggles());

// Fork the routine to download toggles.
yield fork(fetchTogglesRoutine);

// At this point, unleashClient.start() already fetched the toggles
const featureToggles = mapFeatureToggles(unleashClient.toggles);
// At this point, unleashClient.fetchToggles() already fetched the toggles
// (this will throw if it hasn't)
const featureToggles = mapFeatureToggles(unleashClient.getToggles());

yield put(setFeatureToggles(featureToggles));
yield put(featureToggleInitialized());
} catch (e) {
console.error('Error initializing unleash');
unleashClient.stop();

yield put(setUnleashClient(null));

// Wait 500ms before retrying
yield delay(500);

// Spawn so it's detached from the current thread
yield spawn(handleInitFailed, currentRetry);
} finally {
if (yield cancelled()) {
yield call(() => unleashClient.stop());
}
}
}

export function* setupUnleashListeners(unleashClient) {
const channel = eventChannel((emitter) => {
const l1 = () => emitter({ type: types.FEATURE_TOGGLE_UPDATE });
const l2 = () => emitter({ type: types.FEATURE_TOGGLE_READY });
const l3 = (err) => emitter({ type: types.FEATURE_TOGGLE_ERROR, data: err });

unleashClient.on(UnleashEvents.UPDATE, l1);
unleashClient.on(UnleashEvents.READY, l2);
unleashClient.on(UnleashEvents.ERROR, l3);

return () => {
// XXX: This should be a cleanup but removeListener does not exist
// This will throw an error and it will interfere with other sagas
// Since it works without the cleanup i will leave this method empty
// until have determined the best cleanup approach
};
});

try {
while (true) {
const message = yield take(channel);

yield put({
type: message.type,
payload: message.data,
});
}
} finally {
if (yield cancelled()) {
// When we close the channel, it will remove the event listener
channel.close();
}
}
}

Expand All @@ -198,22 +149,6 @@ function mapFeatureToggles(toggles) {
}, {});
}

export function* handleToggleUpdate() {
const unleashClient = yield select((state) => state.unleashClient);
const featureTogglesInitialized = yield select((state) => state.featureTogglesInitialized);
const networkSettings = yield select((state) => state.networkSettings);

if (!unleashClient || !featureTogglesInitialized) {
return;
}

const { toggles } = unleashClient;
const featureToggles = disableFeaturesIfNeeded(networkSettings, mapFeatureToggles(toggles));

yield put(setFeatureToggles(featureToggles));
yield put({ type: types.FEATURE_TOGGLE_UPDATED });
}

export function* saga() {
yield all([
fork(monitorFeatureFlags),
Expand Down

0 comments on commit 8827c2a

Please sign in to comment.