Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 71 additions & 12 deletions dist/index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ function Enable({
// src/Features.tsx








// src/FeatureContext.tsx

var FeatureContext = _react.createContext.call(void 0, null);
Expand Down Expand Up @@ -374,8 +380,26 @@ function usePersist(storage, features, overrideState) {
// src/useTestCallback.tsx


// src/rolloutHash.tsx
function hashToPercentage(str) {
let hash = 5381;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) + hash + char | 0;
}
const unsigned = hash >>> 0;
return unsigned / 4294967296;
}
function isInRollout(featureName, rolloutStableId, percentage) {
if (percentage <= 0) return false;
if (percentage >= 1) return true;
const combinedKey = `${featureName}:${rolloutStableId}`;
const hash = hashToPercentage(combinedKey);
return hash < percentage;
}

// src/testFeature.tsx
function testFeature(feature, states) {
function testFeature(feature, states, rolloutStableId) {
const values = states.map((state) => valueOfFeature(state, feature));
for (const [featureValue, featureForced] of values) {
if (featureValue != null && featureForced) {
Expand All @@ -387,26 +411,61 @@ function testFeature(feature, states) {
return featureValue;
}
}
if (rolloutStableId != null) {
for (const state of states) {
if (state.value === "ready" && state.context.features[feature] != null) {
const featureState = state.context.features[feature];
const enableFor = _optionalChain([featureState, 'access', _15 => _15.featureDesc, 'optionalAccess', _16 => _16.enableFor]);
if (enableFor != null) {
return isInRollout(feature, rolloutStableId, enableFor);
}
}
}
}
return void 0;
}

// src/useTestCallback.tsx
function useTestCallback(defaultsState, overridesState) {
function useTestCallback(overridesState, defaultsState, rolloutStableId) {
return _react.useCallback.call(void 0,
(f) => testFeature(f, [defaultsState, overridesState]),
[defaultsState, overridesState]
(f) => testFeature(f, [overridesState, defaultsState], rolloutStableId),
[overridesState, defaultsState, rolloutStableId]
);
}

// src/Features.tsx

var ROLLOUT_ID_KEY = "react-enable:rollout-stable-id";
function Features({
children,
features,
disableConsole = false,
storage = window.sessionStorage
storage = window.sessionStorage,
rolloutStableId
}) {
const featuresRef = _react.useRef.call(void 0, features);
const stableId = _react.useMemo.call(void 0, () => {
if (rolloutStableId != null) {
return rolloutStableId;
}
if (storage != null) {
try {
const existingId = storage.getItem(ROLLOUT_ID_KEY);
if (existingId != null) {
return existingId;
}
} catch (e) {
}
}
const newId = `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
if (storage != null) {
try {
storage.setItem(ROLLOUT_ID_KEY, newId);
} catch (e) {
}
}
return newId;
}, [rolloutStableId, storage]);
const [overridesState, overridesDispatch] = _react.useReducer.call(void 0,
featuresReducer,
initialFeaturesState
Expand All @@ -415,13 +474,13 @@ function Features({
featuresReducer,
initialFeaturesState
);
_react.useEffect.call(void 0, () => {
_react.useLayoutEffect.call(void 0, () => {
defaultsDispatch({ type: "INIT", features });
return () => {
defaultsDispatch({ type: "DE_INIT" });
};
}, [features]);
_react.useEffect.call(void 0, () => {
_react.useLayoutEffect.call(void 0, () => {
let f = {};
if (storage != null) {
try {
Expand All @@ -439,7 +498,7 @@ function Features({
features: (_nullishCoalesce(featuresRef.current, () => ( []))).filter((x) => x.noOverride !== true).map((x) => ({
name: x.name,
description: x.description,
defaultValue: _nullishCoalesce(_optionalChain([f, 'optionalAccess', _15 => _15[x.name]]), () => ( void 0))
defaultValue: _nullishCoalesce(_optionalChain([f, 'optionalAccess', _17 => _17[x.name]]), () => ( void 0))
}))
});
return () => {
Expand All @@ -454,7 +513,7 @@ function Features({
([name, feature]) => {
if (feature.value === "asyncEnabled" || feature.value === "asyncDisabled" || feature.value === "asyncUnspecified") {
const targetValue = feature.value === "asyncEnabled" ? true : feature.value === "asyncDisabled" ? false : void 0;
const onChangeDefault = _optionalChain([feature, 'access', _16 => _16.featureDesc, 'optionalAccess', _17 => _17.onChangeDefault]);
const onChangeDefault = _optionalChain([feature, 'access', _18 => _18.featureDesc, 'optionalAccess', _19 => _19.onChangeDefault]);
if (onChangeDefault != null && feature.featureDesc != null) {
onChangeDefault(feature.featureDesc.name, targetValue).then((result) => {
defaultsDispatch({ type: "ASYNC_DONE", name, value: result });
Expand All @@ -471,7 +530,7 @@ function Features({
);
}, [defaultsState]);
usePersist(storage, featuresRef.current, overridesState);
const testCallback = useTestCallback(overridesState, defaultsState);
const testCallback = useTestCallback(overridesState, defaultsState, stableId);
useConsoleOverride(
!disableConsole,
featuresRef.current,
Expand Down Expand Up @@ -511,7 +570,7 @@ function ToggleFeature({
const context = _react.useContext.call(void 0, FeatureContext);
const handleChangeSelection = _react.useCallback.call(void 0,
(value) => {
if (_optionalChain([context, 'optionalAccess', _18 => _18.overridesSend]) != null) {
if (_optionalChain([context, 'optionalAccess', _20 => _20.overridesSend]) != null) {
switch (value) {
case "true": {
context.overridesSend({ type: "ENABLE", name: feature.name });
Expand Down Expand Up @@ -701,7 +760,7 @@ function ToggleFeatures({
if (host == null || root != null) {
return;
}
const shadowRoot = _optionalChain([host, 'optionalAccess', _19 => _19.attachShadow, 'call', _20 => _20({ mode: "open" })]);
const shadowRoot = _optionalChain([host, 'optionalAccess', _21 => _21.attachShadow, 'call', _22 => _22({ mode: "open" })]);
const style = document.createElement("style");
const renderDiv = document.createElement("div");
style.textContent = tailwind_default;
Expand Down
2 changes: 1 addition & 1 deletion dist/index.cjs.map

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion dist/index.d.cts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ interface FeatureDescription<K extends string = string> {
readonly force?: boolean;
readonly noOverride?: boolean;
readonly defaultValue?: FeatureValue;
readonly enableFor?: number;
}
/**
* Actions that can be performed on a feature.
Expand Down Expand Up @@ -123,14 +124,15 @@ interface FeatureProps {
readonly children?: ReactNode;
readonly disableConsole?: boolean;
readonly storage?: Storage;
readonly rolloutStableId?: string;
}
/**
* A more batteries-enabled parent component that keeps track of feature state
* internally, and creates window.feature.enable("f") and window.feature.disable("f").
* Keeps track of overrides and defaults, with defaults potentially coming from your props
* and overrides being persisted to your choice of storage layer.
*/
declare function Features({ children, features, disableConsole, storage, }: FeatureProps): JSX.Element;
declare function Features({ children, features, disableConsole, storage, rolloutStableId, }: FeatureProps): JSX.Element;

declare function ToggleFeatures({ defaultOpen, hidden, }: {
defaultOpen?: boolean;
Expand Down
4 changes: 3 additions & 1 deletion dist/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ interface FeatureDescription<K extends string = string> {
readonly force?: boolean;
readonly noOverride?: boolean;
readonly defaultValue?: FeatureValue;
readonly enableFor?: number;
}
/**
* Actions that can be performed on a feature.
Expand Down Expand Up @@ -123,14 +124,15 @@ interface FeatureProps {
readonly children?: ReactNode;
readonly disableConsole?: boolean;
readonly storage?: Storage;
readonly rolloutStableId?: string;
}
/**
* A more batteries-enabled parent component that keeps track of feature state
* internally, and creates window.feature.enable("f") and window.feature.disable("f").
* Keeps track of overrides and defaults, with defaults potentially coming from your props
* and overrides being persisted to your choice of storage layer.
*/
declare function Features({ children, features, disableConsole, storage, }: FeatureProps): JSX.Element;
declare function Features({ children, features, disableConsole, storage, rolloutStableId, }: FeatureProps): JSX.Element;

declare function ToggleFeatures({ defaultOpen, hidden, }: {
defaultOpen?: boolean;
Expand Down
77 changes: 68 additions & 9 deletions dist/index.js

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

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package-lock.json

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

5 changes: 5 additions & 0 deletions src/FeatureState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ export interface FeatureDescription<K extends string = string> {

/// can be used to specify what should happen if the feature is not set to a particular value.
readonly defaultValue?: FeatureValue;

/// Percentage-based rollout (0-1). If set, the feature will be enabled for a percentage of users
/// based on a stable identifier. For example, 0.3 means 30% of users will see this feature enabled.
/// Requires a rolloutStableId to be provided to the Features component.
readonly enableFor?: number;
}

/**
Expand Down
Loading