@metamask-previews/remote-feature-flag-controller 2.0.1-preview-159e76e4 → 2.0.1-preview-7cfbb337

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Added
11
+
12
+ - Add version-gated feature flags with multi-version support ([#7277](https://github.com/MetaMask/core/pull/7277))
13
+ - Support for feature flags with multiple version entries: `{ versions: { "13.1.0": {...}, "13.2.0": {...} } }`
14
+ - Automatic selection of highest qualifying version based on semantic version comparison
15
+ - New utility functions: `isVersionFeatureFlag()`, `getVersionData()`, `isVersionAtLeast()`
16
+ - Enhanced type safety with `VersionEntry` and `MultiVersionFeatureFlagValue` types
17
+ - Comprehensive validation ensures only properly structured version entries are processed
18
+
19
+ ### Changed
20
+
21
+ - **BREAKING:** Add required `clientVersion` parameter to constructor for version-based filtering (expects semantic version string of client app) ([#7277](https://github.com/MetaMask/core/pull/7277))
22
+
10
23
  ## [2.0.1]
11
24
 
12
25
  ### Changed
@@ -1 +1 @@
1
- {"version":3,"file":"remote-feature-flag-controller-types.cjs","sourceRoot":"","sources":["../src/remote-feature-flag-controller-types.ts"],"names":[],"mappings":";;;AAEA,mEAAmE;AACnE,IAAY,UAGX;AAHD,WAAY,UAAU;IACpB,qCAAuB,CAAA;IACvB,+BAAiB,CAAA;AACnB,CAAC,EAHW,UAAU,0BAAV,UAAU,QAGrB;AAED,IAAY,gBAOX;AAPD,WAAY,gBAAgB;IAC1B,iCAAa,CAAA;IACb,mCAAe,CAAA;IACf;;OAEG;IACH,iCAAa,CAAA;AACf,CAAC,EAPW,gBAAgB,gCAAhB,gBAAgB,QAO3B;AAED,IAAY,eAOX;AAPD,WAAY,eAAe;IACzB,sCAAmB,CAAA;IACnB,0CAAuB,CAAA;IACvB,sCAAmB,CAAA;IACnB,gCAAa,CAAA;IACb,gCAAa,CAAA;IACb,8BAAW,CAAA;AACb,CAAC,EAPW,eAAe,+BAAf,eAAe,QAO1B","sourcesContent":["import type { Json } from '@metamask/utils';\n\n// Define accepted values for client, distribution, and environment\nexport enum ClientType {\n Extension = 'extension',\n Mobile = 'mobile',\n}\n\nexport enum DistributionType {\n Main = 'main',\n Flask = 'flask',\n /**\n * @deprecated Use DistributionType Main with EnvironmentType Beta instead\n */\n Beta = 'beta',\n}\n\nexport enum EnvironmentType {\n Production = 'prod',\n ReleaseCandidate = 'rc',\n Development = 'dev',\n Beta = 'beta',\n Test = 'test',\n Exp = 'exp',\n}\n\n/** Type representing the feature flags collection */\nexport type FeatureFlags = {\n [key: string]: Json;\n};\n\nexport type FeatureFlagScope = {\n type: string;\n value: number;\n};\n\nexport type FeatureFlagScopeValue = {\n name: string;\n scope: FeatureFlagScope;\n value: Json;\n};\n\nexport type ApiDataResponse = FeatureFlags[];\n\nexport type ServiceResponse = {\n remoteFeatureFlags: FeatureFlags;\n cacheTimestamp: number | null;\n};\n"]}
1
+ {"version":3,"file":"remote-feature-flag-controller-types.cjs","sourceRoot":"","sources":["../src/remote-feature-flag-controller-types.ts"],"names":[],"mappings":";;;AAEA,mEAAmE;AACnE,IAAY,UAGX;AAHD,WAAY,UAAU;IACpB,qCAAuB,CAAA;IACvB,+BAAiB,CAAA;AACnB,CAAC,EAHW,UAAU,0BAAV,UAAU,QAGrB;AAED,IAAY,gBAOX;AAPD,WAAY,gBAAgB;IAC1B,iCAAa,CAAA;IACb,mCAAe,CAAA;IACf;;OAEG;IACH,iCAAa,CAAA;AACf,CAAC,EAPW,gBAAgB,gCAAhB,gBAAgB,QAO3B;AAED,IAAY,eAOX;AAPD,WAAY,eAAe;IACzB,sCAAmB,CAAA;IACnB,0CAAuB,CAAA;IACvB,sCAAmB,CAAA;IACnB,gCAAa,CAAA;IACb,gCAAa,CAAA;IACb,8BAAW,CAAA;AACb,CAAC,EAPW,eAAe,+BAAf,eAAe,QAO1B","sourcesContent":["import type { Json, SemVerVersion } from '@metamask/utils';\n\n// Define accepted values for client, distribution, and environment\nexport enum ClientType {\n Extension = 'extension',\n Mobile = 'mobile',\n}\n\nexport enum DistributionType {\n Main = 'main',\n Flask = 'flask',\n /**\n * @deprecated Use DistributionType Main with EnvironmentType Beta instead\n */\n Beta = 'beta',\n}\n\nexport enum EnvironmentType {\n Production = 'prod',\n ReleaseCandidate = 'rc',\n Development = 'dev',\n Beta = 'beta',\n Test = 'test',\n Exp = 'exp',\n}\n\n/** Type representing a feature flag with multiple version entries */\nexport type MultiVersionFeatureFlagValue = {\n versions: Record<SemVerVersion, Json>;\n};\n\n/** Type representing the feature flags collection */\nexport type FeatureFlags = {\n [key: string]: Json;\n};\n\nexport type FeatureFlagScope = {\n type: string;\n value: number;\n};\n\nexport type FeatureFlagScopeValue = {\n name: string;\n scope: FeatureFlagScope;\n value: Json;\n};\n\nexport type ApiDataResponse = FeatureFlags[];\n\nexport type ServiceResponse = {\n remoteFeatureFlags: FeatureFlags;\n cacheTimestamp: number | null;\n};\n"]}
@@ -1,4 +1,4 @@
1
- import type { Json } from "@metamask/utils";
1
+ import type { Json, SemVerVersion } from "@metamask/utils";
2
2
  export declare enum ClientType {
3
3
  Extension = "extension",
4
4
  Mobile = "mobile"
@@ -19,6 +19,10 @@ export declare enum EnvironmentType {
19
19
  Test = "test",
20
20
  Exp = "exp"
21
21
  }
22
+ /** Type representing a feature flag with multiple version entries */
23
+ export type MultiVersionFeatureFlagValue = {
24
+ versions: Record<SemVerVersion, Json>;
25
+ };
22
26
  /** Type representing the feature flags collection */
23
27
  export type FeatureFlags = {
24
28
  [key: string]: Json;
@@ -1 +1 @@
1
- {"version":3,"file":"remote-feature-flag-controller-types.d.cts","sourceRoot":"","sources":["../src/remote-feature-flag-controller-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,wBAAwB;AAG5C,oBAAY,UAAU;IACpB,SAAS,cAAc;IACvB,MAAM,WAAW;CAClB;AAED,oBAAY,gBAAgB;IAC1B,IAAI,SAAS;IACb,KAAK,UAAU;IACf;;OAEG;IACH,IAAI,SAAS;CACd;AAED,oBAAY,eAAe;IACzB,UAAU,SAAS;IACnB,gBAAgB,OAAO;IACvB,WAAW,QAAQ;IACnB,IAAI,SAAS;IACb,IAAI,SAAS;IACb,GAAG,QAAQ;CACZ;AAED,qDAAqD;AACrD,MAAM,MAAM,YAAY,GAAG;IACzB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,gBAAgB,CAAC;IACxB,KAAK,EAAE,IAAI,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,YAAY,EAAE,CAAC;AAE7C,MAAM,MAAM,eAAe,GAAG;IAC5B,kBAAkB,EAAE,YAAY,CAAC;IACjC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B,CAAC"}
1
+ {"version":3,"file":"remote-feature-flag-controller-types.d.cts","sourceRoot":"","sources":["../src/remote-feature-flag-controller-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,wBAAwB;AAG3D,oBAAY,UAAU;IACpB,SAAS,cAAc;IACvB,MAAM,WAAW;CAClB;AAED,oBAAY,gBAAgB;IAC1B,IAAI,SAAS;IACb,KAAK,UAAU;IACf;;OAEG;IACH,IAAI,SAAS;CACd;AAED,oBAAY,eAAe;IACzB,UAAU,SAAS;IACnB,gBAAgB,OAAO;IACvB,WAAW,QAAQ;IACnB,IAAI,SAAS;IACb,IAAI,SAAS;IACb,GAAG,QAAQ;CACZ;AAED,qEAAqE;AACrE,MAAM,MAAM,4BAA4B,GAAG;IACzC,QAAQ,EAAE,MAAM,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC;CACvC,CAAC;AAEF,qDAAqD;AACrD,MAAM,MAAM,YAAY,GAAG;IACzB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,gBAAgB,CAAC;IACxB,KAAK,EAAE,IAAI,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,YAAY,EAAE,CAAC;AAE7C,MAAM,MAAM,eAAe,GAAG;IAC5B,kBAAkB,EAAE,YAAY,CAAC;IACjC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B,CAAC"}
@@ -1,4 +1,4 @@
1
- import type { Json } from "@metamask/utils";
1
+ import type { Json, SemVerVersion } from "@metamask/utils";
2
2
  export declare enum ClientType {
3
3
  Extension = "extension",
4
4
  Mobile = "mobile"
@@ -19,6 +19,10 @@ export declare enum EnvironmentType {
19
19
  Test = "test",
20
20
  Exp = "exp"
21
21
  }
22
+ /** Type representing a feature flag with multiple version entries */
23
+ export type MultiVersionFeatureFlagValue = {
24
+ versions: Record<SemVerVersion, Json>;
25
+ };
22
26
  /** Type representing the feature flags collection */
23
27
  export type FeatureFlags = {
24
28
  [key: string]: Json;
@@ -1 +1 @@
1
- {"version":3,"file":"remote-feature-flag-controller-types.d.mts","sourceRoot":"","sources":["../src/remote-feature-flag-controller-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,wBAAwB;AAG5C,oBAAY,UAAU;IACpB,SAAS,cAAc;IACvB,MAAM,WAAW;CAClB;AAED,oBAAY,gBAAgB;IAC1B,IAAI,SAAS;IACb,KAAK,UAAU;IACf;;OAEG;IACH,IAAI,SAAS;CACd;AAED,oBAAY,eAAe;IACzB,UAAU,SAAS;IACnB,gBAAgB,OAAO;IACvB,WAAW,QAAQ;IACnB,IAAI,SAAS;IACb,IAAI,SAAS;IACb,GAAG,QAAQ;CACZ;AAED,qDAAqD;AACrD,MAAM,MAAM,YAAY,GAAG;IACzB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,gBAAgB,CAAC;IACxB,KAAK,EAAE,IAAI,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,YAAY,EAAE,CAAC;AAE7C,MAAM,MAAM,eAAe,GAAG;IAC5B,kBAAkB,EAAE,YAAY,CAAC;IACjC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B,CAAC"}
1
+ {"version":3,"file":"remote-feature-flag-controller-types.d.mts","sourceRoot":"","sources":["../src/remote-feature-flag-controller-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,wBAAwB;AAG3D,oBAAY,UAAU;IACpB,SAAS,cAAc;IACvB,MAAM,WAAW;CAClB;AAED,oBAAY,gBAAgB;IAC1B,IAAI,SAAS;IACb,KAAK,UAAU;IACf;;OAEG;IACH,IAAI,SAAS;CACd;AAED,oBAAY,eAAe;IACzB,UAAU,SAAS;IACnB,gBAAgB,OAAO;IACvB,WAAW,QAAQ;IACnB,IAAI,SAAS;IACb,IAAI,SAAS;IACb,GAAG,QAAQ;CACZ;AAED,qEAAqE;AACrE,MAAM,MAAM,4BAA4B,GAAG;IACzC,QAAQ,EAAE,MAAM,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC;CACvC,CAAC;AAEF,qDAAqD;AACrD,MAAM,MAAM,YAAY,GAAG;IACzB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,gBAAgB,CAAC;IACxB,KAAK,EAAE,IAAI,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,YAAY,EAAE,CAAC;AAE7C,MAAM,MAAM,eAAe,GAAG;IAC5B,kBAAkB,EAAE,YAAY,CAAC;IACjC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"remote-feature-flag-controller-types.mjs","sourceRoot":"","sources":["../src/remote-feature-flag-controller-types.ts"],"names":[],"mappings":"AAEA,mEAAmE;AACnE,MAAM,CAAN,IAAY,UAGX;AAHD,WAAY,UAAU;IACpB,qCAAuB,CAAA;IACvB,+BAAiB,CAAA;AACnB,CAAC,EAHW,UAAU,KAAV,UAAU,QAGrB;AAED,MAAM,CAAN,IAAY,gBAOX;AAPD,WAAY,gBAAgB;IAC1B,iCAAa,CAAA;IACb,mCAAe,CAAA;IACf;;OAEG;IACH,iCAAa,CAAA;AACf,CAAC,EAPW,gBAAgB,KAAhB,gBAAgB,QAO3B;AAED,MAAM,CAAN,IAAY,eAOX;AAPD,WAAY,eAAe;IACzB,sCAAmB,CAAA;IACnB,0CAAuB,CAAA;IACvB,sCAAmB,CAAA;IACnB,gCAAa,CAAA;IACb,gCAAa,CAAA;IACb,8BAAW,CAAA;AACb,CAAC,EAPW,eAAe,KAAf,eAAe,QAO1B","sourcesContent":["import type { Json } from '@metamask/utils';\n\n// Define accepted values for client, distribution, and environment\nexport enum ClientType {\n Extension = 'extension',\n Mobile = 'mobile',\n}\n\nexport enum DistributionType {\n Main = 'main',\n Flask = 'flask',\n /**\n * @deprecated Use DistributionType Main with EnvironmentType Beta instead\n */\n Beta = 'beta',\n}\n\nexport enum EnvironmentType {\n Production = 'prod',\n ReleaseCandidate = 'rc',\n Development = 'dev',\n Beta = 'beta',\n Test = 'test',\n Exp = 'exp',\n}\n\n/** Type representing the feature flags collection */\nexport type FeatureFlags = {\n [key: string]: Json;\n};\n\nexport type FeatureFlagScope = {\n type: string;\n value: number;\n};\n\nexport type FeatureFlagScopeValue = {\n name: string;\n scope: FeatureFlagScope;\n value: Json;\n};\n\nexport type ApiDataResponse = FeatureFlags[];\n\nexport type ServiceResponse = {\n remoteFeatureFlags: FeatureFlags;\n cacheTimestamp: number | null;\n};\n"]}
1
+ {"version":3,"file":"remote-feature-flag-controller-types.mjs","sourceRoot":"","sources":["../src/remote-feature-flag-controller-types.ts"],"names":[],"mappings":"AAEA,mEAAmE;AACnE,MAAM,CAAN,IAAY,UAGX;AAHD,WAAY,UAAU;IACpB,qCAAuB,CAAA;IACvB,+BAAiB,CAAA;AACnB,CAAC,EAHW,UAAU,KAAV,UAAU,QAGrB;AAED,MAAM,CAAN,IAAY,gBAOX;AAPD,WAAY,gBAAgB;IAC1B,iCAAa,CAAA;IACb,mCAAe,CAAA;IACf;;OAEG;IACH,iCAAa,CAAA;AACf,CAAC,EAPW,gBAAgB,KAAhB,gBAAgB,QAO3B;AAED,MAAM,CAAN,IAAY,eAOX;AAPD,WAAY,eAAe;IACzB,sCAAmB,CAAA;IACnB,0CAAuB,CAAA;IACvB,sCAAmB,CAAA;IACnB,gCAAa,CAAA;IACb,gCAAa,CAAA;IACb,8BAAW,CAAA;AACb,CAAC,EAPW,eAAe,KAAf,eAAe,QAO1B","sourcesContent":["import type { Json, SemVerVersion } from '@metamask/utils';\n\n// Define accepted values for client, distribution, and environment\nexport enum ClientType {\n Extension = 'extension',\n Mobile = 'mobile',\n}\n\nexport enum DistributionType {\n Main = 'main',\n Flask = 'flask',\n /**\n * @deprecated Use DistributionType Main with EnvironmentType Beta instead\n */\n Beta = 'beta',\n}\n\nexport enum EnvironmentType {\n Production = 'prod',\n ReleaseCandidate = 'rc',\n Development = 'dev',\n Beta = 'beta',\n Test = 'test',\n Exp = 'exp',\n}\n\n/** Type representing a feature flag with multiple version entries */\nexport type MultiVersionFeatureFlagValue = {\n versions: Record<SemVerVersion, Json>;\n};\n\n/** Type representing the feature flags collection */\nexport type FeatureFlags = {\n [key: string]: Json;\n};\n\nexport type FeatureFlagScope = {\n type: string;\n value: number;\n};\n\nexport type FeatureFlagScopeValue = {\n name: string;\n scope: FeatureFlagScope;\n value: Json;\n};\n\nexport type ApiDataResponse = FeatureFlags[];\n\nexport type ServiceResponse = {\n remoteFeatureFlags: FeatureFlags;\n cacheTimestamp: number | null;\n};\n"]}
@@ -10,11 +10,13 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
10
10
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
11
11
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
12
12
  };
13
- var _RemoteFeatureFlagController_instances, _RemoteFeatureFlagController_fetchInterval, _RemoteFeatureFlagController_disabled, _RemoteFeatureFlagController_clientConfigApiService, _RemoteFeatureFlagController_inProgressFlagUpdate, _RemoteFeatureFlagController_getMetaMetricsId, _RemoteFeatureFlagController_isCacheExpired, _RemoteFeatureFlagController_updateCache, _RemoteFeatureFlagController_processRemoteFeatureFlags;
13
+ var _RemoteFeatureFlagController_instances, _RemoteFeatureFlagController_fetchInterval, _RemoteFeatureFlagController_disabled, _RemoteFeatureFlagController_clientConfigApiService, _RemoteFeatureFlagController_inProgressFlagUpdate, _RemoteFeatureFlagController_getMetaMetricsId, _RemoteFeatureFlagController_clientVersion, _RemoteFeatureFlagController_isCacheExpired, _RemoteFeatureFlagController_updateCache, _RemoteFeatureFlagController_processVersionBasedFlag, _RemoteFeatureFlagController_processRemoteFeatureFlags;
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.RemoteFeatureFlagController = exports.getDefaultRemoteFeatureFlagControllerState = exports.DEFAULT_CACHE_DURATION = void 0;
16
16
  const base_controller_1 = require("@metamask/base-controller");
17
+ const utils_1 = require("@metamask/utils");
17
18
  const user_segmentation_utils_1 = require("./utils/user-segmentation-utils.cjs");
19
+ const version_1 = require("./utils/version.cjs");
18
20
  // === GENERAL ===
19
21
  const controllerName = 'RemoteFeatureFlagController';
20
22
  exports.DEFAULT_CACHE_DURATION = 24 * 60 * 60 * 1000; // 1 day
@@ -61,8 +63,12 @@ class RemoteFeatureFlagController extends base_controller_1.BaseController {
61
63
  * @param options.fetchInterval - The interval in milliseconds before cached flags expire. Defaults to 1 day.
62
64
  * @param options.disabled - Determines if the controller should be disabled initially. Defaults to false.
63
65
  * @param options.getMetaMetricsId - Returns metaMetricsId.
66
+ * @param options.clientVersion - The current client version for version-based feature flag filtering. Must be a valid 3-part SemVer version string.
64
67
  */
65
- constructor({ messenger, state, clientConfigApiService, fetchInterval = exports.DEFAULT_CACHE_DURATION, disabled = false, getMetaMetricsId, }) {
68
+ constructor({ messenger, state, clientConfigApiService, fetchInterval = exports.DEFAULT_CACHE_DURATION, disabled = false, getMetaMetricsId, clientVersion, }) {
69
+ if (!(0, utils_1.isValidSemVerVersion)(clientVersion)) {
70
+ throw new Error(`Invalid clientVersion: "${clientVersion}". Must be a valid 3-part SemVer version string`);
71
+ }
66
72
  super({
67
73
  name: controllerName,
68
74
  metadata: remoteFeatureFlagControllerMetadata,
@@ -78,10 +84,12 @@ class RemoteFeatureFlagController extends base_controller_1.BaseController {
78
84
  _RemoteFeatureFlagController_clientConfigApiService.set(this, void 0);
79
85
  _RemoteFeatureFlagController_inProgressFlagUpdate.set(this, void 0);
80
86
  _RemoteFeatureFlagController_getMetaMetricsId.set(this, void 0);
87
+ _RemoteFeatureFlagController_clientVersion.set(this, void 0);
81
88
  __classPrivateFieldSet(this, _RemoteFeatureFlagController_fetchInterval, fetchInterval, "f");
82
89
  __classPrivateFieldSet(this, _RemoteFeatureFlagController_disabled, disabled, "f");
83
90
  __classPrivateFieldSet(this, _RemoteFeatureFlagController_clientConfigApiService, clientConfigApiService, "f");
84
91
  __classPrivateFieldSet(this, _RemoteFeatureFlagController_getMetaMetricsId, getMetaMetricsId, "f");
92
+ __classPrivateFieldSet(this, _RemoteFeatureFlagController_clientVersion, clientVersion, "f");
85
93
  }
86
94
  /**
87
95
  * Retrieves the remote feature flags, fetching from the API if necessary.
@@ -121,7 +129,7 @@ class RemoteFeatureFlagController extends base_controller_1.BaseController {
121
129
  }
122
130
  }
123
131
  exports.RemoteFeatureFlagController = RemoteFeatureFlagController;
124
- _RemoteFeatureFlagController_fetchInterval = new WeakMap(), _RemoteFeatureFlagController_disabled = new WeakMap(), _RemoteFeatureFlagController_clientConfigApiService = new WeakMap(), _RemoteFeatureFlagController_inProgressFlagUpdate = new WeakMap(), _RemoteFeatureFlagController_getMetaMetricsId = new WeakMap(), _RemoteFeatureFlagController_instances = new WeakSet(), _RemoteFeatureFlagController_isCacheExpired = function _RemoteFeatureFlagController_isCacheExpired() {
132
+ _RemoteFeatureFlagController_fetchInterval = new WeakMap(), _RemoteFeatureFlagController_disabled = new WeakMap(), _RemoteFeatureFlagController_clientConfigApiService = new WeakMap(), _RemoteFeatureFlagController_inProgressFlagUpdate = new WeakMap(), _RemoteFeatureFlagController_getMetaMetricsId = new WeakMap(), _RemoteFeatureFlagController_clientVersion = new WeakMap(), _RemoteFeatureFlagController_instances = new WeakSet(), _RemoteFeatureFlagController_isCacheExpired = function _RemoteFeatureFlagController_isCacheExpired() {
125
133
  return Date.now() - this.state.cacheTimestamp > __classPrivateFieldGet(this, _RemoteFeatureFlagController_fetchInterval, "f");
126
134
  }, _RemoteFeatureFlagController_updateCache =
127
135
  /**
@@ -137,14 +145,22 @@ async function _RemoteFeatureFlagController_updateCache(remoteFeatureFlags) {
137
145
  cacheTimestamp: Date.now(),
138
146
  };
139
147
  });
148
+ }, _RemoteFeatureFlagController_processVersionBasedFlag = function _RemoteFeatureFlagController_processVersionBasedFlag(flagValue) {
149
+ if (!(0, version_1.isVersionFeatureFlag)(flagValue)) {
150
+ return flagValue;
151
+ }
152
+ return (0, version_1.getVersionData)(flagValue, __classPrivateFieldGet(this, _RemoteFeatureFlagController_clientVersion, "f"));
140
153
  }, _RemoteFeatureFlagController_processRemoteFeatureFlags = async function _RemoteFeatureFlagController_processRemoteFeatureFlags(remoteFeatureFlags) {
141
154
  const processedRemoteFeatureFlags = {};
142
155
  const metaMetricsId = __classPrivateFieldGet(this, _RemoteFeatureFlagController_getMetaMetricsId, "f").call(this);
143
156
  const thresholdValue = (0, user_segmentation_utils_1.generateDeterministicRandomNumber)(metaMetricsId);
144
157
  for (const [remoteFeatureFlagName, remoteFeatureFlagValue,] of Object.entries(remoteFeatureFlags)) {
145
- let processedValue = remoteFeatureFlagValue;
146
- if (Array.isArray(remoteFeatureFlagValue) && thresholdValue) {
147
- const selectedGroup = remoteFeatureFlagValue.find((featureFlag) => {
158
+ let processedValue = __classPrivateFieldGet(this, _RemoteFeatureFlagController_instances, "m", _RemoteFeatureFlagController_processVersionBasedFlag).call(this, remoteFeatureFlagValue);
159
+ if (processedValue === null) {
160
+ continue;
161
+ }
162
+ if (Array.isArray(processedValue) && thresholdValue) {
163
+ const selectedGroup = processedValue.find((featureFlag) => {
148
164
  if (!(0, user_segmentation_utils_1.isFeatureFlagWithScopeValue)(featureFlag)) {
149
165
  return false;
150
166
  }
@@ -1 +1 @@
1
- {"version":3,"file":"remote-feature-flag-controller.cjs","sourceRoot":"","sources":["../src/remote-feature-flag-controller.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n BaseController,\n type ControllerGetStateAction,\n type ControllerStateChangeEvent,\n} from '@metamask/base-controller';\nimport type { Messenger } from '@metamask/messenger';\n\nimport type { AbstractClientConfigApiService } from './client-config-api-service/abstract-client-config-api-service';\nimport type {\n FeatureFlags,\n ServiceResponse,\n FeatureFlagScopeValue,\n} from './remote-feature-flag-controller-types';\nimport {\n generateDeterministicRandomNumber,\n isFeatureFlagWithScopeValue,\n} from './utils/user-segmentation-utils';\n\n// === GENERAL ===\n\nconst controllerName = 'RemoteFeatureFlagController';\nexport const DEFAULT_CACHE_DURATION = 24 * 60 * 60 * 1000; // 1 day\n\n// === STATE ===\n\nexport type RemoteFeatureFlagControllerState = {\n remoteFeatureFlags: FeatureFlags;\n cacheTimestamp: number;\n};\n\nconst remoteFeatureFlagControllerMetadata = {\n remoteFeatureFlags: {\n includeInStateLogs: true,\n persist: true,\n includeInDebugSnapshot: true,\n usedInUi: true,\n },\n cacheTimestamp: {\n includeInStateLogs: true,\n persist: true,\n includeInDebugSnapshot: true,\n usedInUi: false,\n },\n};\n\n// === MESSENGER ===\n\n/**\n * The action to retrieve the state of the {@link RemoteFeatureFlagController}.\n */\nexport type RemoteFeatureFlagControllerGetStateAction =\n ControllerGetStateAction<\n typeof controllerName,\n RemoteFeatureFlagControllerState\n >;\n\nexport type RemoteFeatureFlagControllerUpdateRemoteFeatureFlagsAction = {\n type: `${typeof controllerName}:updateRemoteFeatureFlags`;\n handler: RemoteFeatureFlagController['updateRemoteFeatureFlags'];\n};\n\nexport type RemoteFeatureFlagControllerActions =\n | RemoteFeatureFlagControllerGetStateAction\n | RemoteFeatureFlagControllerUpdateRemoteFeatureFlagsAction;\n\nexport type RemoteFeatureFlagControllerStateChangeEvent =\n ControllerStateChangeEvent<\n typeof controllerName,\n RemoteFeatureFlagControllerState\n >;\n\nexport type RemoteFeatureFlagControllerEvents =\n RemoteFeatureFlagControllerStateChangeEvent;\n\nexport type RemoteFeatureFlagControllerMessenger = Messenger<\n typeof controllerName,\n RemoteFeatureFlagControllerActions,\n RemoteFeatureFlagControllerEvents\n>;\n\n/**\n * Returns the default state for the RemoteFeatureFlagController.\n *\n * @returns The default controller state.\n */\nexport function getDefaultRemoteFeatureFlagControllerState(): RemoteFeatureFlagControllerState {\n return {\n remoteFeatureFlags: {},\n cacheTimestamp: 0,\n };\n}\n\n/**\n * The RemoteFeatureFlagController manages the retrieval and caching of remote feature flags.\n * It fetches feature flags from a remote API, caches them, and provides methods to access\n * and manage these flags. The controller ensures that feature flags are refreshed based on\n * a specified interval and handles cases where the controller is disabled or the network is unavailable.\n */\nexport class RemoteFeatureFlagController extends BaseController<\n typeof controllerName,\n RemoteFeatureFlagControllerState,\n RemoteFeatureFlagControllerMessenger\n> {\n readonly #fetchInterval: number;\n\n #disabled: boolean;\n\n readonly #clientConfigApiService: AbstractClientConfigApiService;\n\n #inProgressFlagUpdate?: Promise<ServiceResponse>;\n\n readonly #getMetaMetricsId: () => string;\n\n /**\n * Constructs a new RemoteFeatureFlagController instance.\n *\n * @param options - The controller options.\n * @param options.messenger - The messenger used for communication.\n * @param options.state - The initial state of the controller.\n * @param options.clientConfigApiService - The service instance to fetch remote feature flags.\n * @param options.fetchInterval - The interval in milliseconds before cached flags expire. Defaults to 1 day.\n * @param options.disabled - Determines if the controller should be disabled initially. Defaults to false.\n * @param options.getMetaMetricsId - Returns metaMetricsId.\n */\n constructor({\n messenger,\n state,\n clientConfigApiService,\n fetchInterval = DEFAULT_CACHE_DURATION,\n disabled = false,\n getMetaMetricsId,\n }: {\n messenger: RemoteFeatureFlagControllerMessenger;\n state?: Partial<RemoteFeatureFlagControllerState>;\n clientConfigApiService: AbstractClientConfigApiService;\n getMetaMetricsId: () => string;\n fetchInterval?: number;\n disabled?: boolean;\n }) {\n super({\n name: controllerName,\n metadata: remoteFeatureFlagControllerMetadata,\n messenger,\n state: {\n ...getDefaultRemoteFeatureFlagControllerState(),\n ...state,\n },\n });\n\n this.#fetchInterval = fetchInterval;\n this.#disabled = disabled;\n this.#clientConfigApiService = clientConfigApiService;\n this.#getMetaMetricsId = getMetaMetricsId;\n }\n\n /**\n * Checks if the cached feature flags are expired based on the fetch interval.\n *\n * @returns Whether the cache is expired (`true`) or still valid (`false`).\n */\n #isCacheExpired(): boolean {\n return Date.now() - this.state.cacheTimestamp > this.#fetchInterval;\n }\n\n /**\n * Retrieves the remote feature flags, fetching from the API if necessary.\n * Uses caching to prevent redundant API calls and handles concurrent fetches.\n *\n * @returns A promise that resolves to the current set of feature flags.\n */\n async updateRemoteFeatureFlags(): Promise<void> {\n if (this.#disabled || !this.#isCacheExpired()) {\n return;\n }\n\n let serverData;\n\n if (this.#inProgressFlagUpdate) {\n await this.#inProgressFlagUpdate;\n return;\n }\n\n try {\n this.#inProgressFlagUpdate =\n this.#clientConfigApiService.fetchRemoteFeatureFlags();\n\n serverData = await this.#inProgressFlagUpdate;\n } finally {\n this.#inProgressFlagUpdate = undefined;\n }\n\n await this.#updateCache(serverData.remoteFeatureFlags);\n }\n\n /**\n * Updates the controller's state with new feature flags and resets the cache timestamp.\n *\n * @param remoteFeatureFlags - The new feature flags to cache.\n */\n async #updateCache(remoteFeatureFlags: FeatureFlags) {\n const processedRemoteFeatureFlags =\n await this.#processRemoteFeatureFlags(remoteFeatureFlags);\n this.update(() => {\n return {\n remoteFeatureFlags: processedRemoteFeatureFlags,\n cacheTimestamp: Date.now(),\n };\n });\n }\n\n async #processRemoteFeatureFlags(\n remoteFeatureFlags: FeatureFlags,\n ): Promise<FeatureFlags> {\n const processedRemoteFeatureFlags: FeatureFlags = {};\n const metaMetricsId = this.#getMetaMetricsId();\n const thresholdValue = generateDeterministicRandomNumber(metaMetricsId);\n\n for (const [\n remoteFeatureFlagName,\n remoteFeatureFlagValue,\n ] of Object.entries(remoteFeatureFlags)) {\n let processedValue = remoteFeatureFlagValue;\n\n if (Array.isArray(remoteFeatureFlagValue) && thresholdValue) {\n const selectedGroup = remoteFeatureFlagValue.find(\n (featureFlag): featureFlag is FeatureFlagScopeValue => {\n if (!isFeatureFlagWithScopeValue(featureFlag)) {\n return false;\n }\n\n return thresholdValue <= featureFlag.scope.value;\n },\n );\n if (selectedGroup) {\n processedValue = {\n name: selectedGroup.name,\n value: selectedGroup.value,\n };\n }\n }\n\n processedRemoteFeatureFlags[remoteFeatureFlagName] = processedValue;\n }\n return processedRemoteFeatureFlags;\n }\n\n /**\n * Enables the controller, allowing it to make network requests.\n */\n enable(): void {\n this.#disabled = false;\n }\n\n /**\n * Disables the controller, preventing it from making network requests.\n */\n disable(): void {\n this.#disabled = true;\n }\n}\n"]}
1
+ {"version":3,"file":"remote-feature-flag-controller.cjs","sourceRoot":"","sources":["../src/remote-feature-flag-controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,+DAImC;AAEnC,2CAIyB;AAQzB,iFAGyC;AACzC,iDAAuE;AAEvE,kBAAkB;AAElB,MAAM,cAAc,GAAG,6BAA6B,CAAC;AACxC,QAAA,sBAAsB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,QAAQ;AASnE,MAAM,mCAAmC,GAAG;IAC1C,kBAAkB,EAAE;QAClB,kBAAkB,EAAE,IAAI;QACxB,OAAO,EAAE,IAAI;QACb,sBAAsB,EAAE,IAAI;QAC5B,QAAQ,EAAE,IAAI;KACf;IACD,cAAc,EAAE;QACd,kBAAkB,EAAE,IAAI;QACxB,OAAO,EAAE,IAAI;QACb,sBAAsB,EAAE,IAAI;QAC5B,QAAQ,EAAE,KAAK;KAChB;CACF,CAAC;AAqCF;;;;GAIG;AACH,SAAgB,0CAA0C;IACxD,OAAO;QACL,kBAAkB,EAAE,EAAE;QACtB,cAAc,EAAE,CAAC;KAClB,CAAC;AACJ,CAAC;AALD,gGAKC;AAED;;;;;GAKG;AACH,MAAa,2BAA4B,SAAQ,gCAIhD;IAaC;;;;;;;;;;;OAWG;IACH,YAAY,EACV,SAAS,EACT,KAAK,EACL,sBAAsB,EACtB,aAAa,GAAG,8BAAsB,EACtC,QAAQ,GAAG,KAAK,EAChB,gBAAgB,EAChB,aAAa,GASd;QACC,IAAI,CAAC,IAAA,4BAAoB,EAAC,aAAa,CAAC,EAAE,CAAC;YACzC,MAAM,IAAI,KAAK,CACb,2BAA2B,aAAa,iDAAiD,CAC1F,CAAC;QACJ,CAAC;QAED,KAAK,CAAC;YACJ,IAAI,EAAE,cAAc;YACpB,QAAQ,EAAE,mCAAmC;YAC7C,SAAS;YACT,KAAK,EAAE;gBACL,GAAG,0CAA0C,EAAE;gBAC/C,GAAG,KAAK;aACT;SACF,CAAC,CAAC;;QAvDI,6DAAuB;QAEhC,wDAAmB;QAEV,sEAAwD;QAEjE,oEAAiD;QAExC,gEAAgC;QAEhC,6DAA8B;QA+CrC,uBAAA,IAAI,8CAAkB,aAAa,MAAA,CAAC;QACpC,uBAAA,IAAI,yCAAa,QAAQ,MAAA,CAAC;QAC1B,uBAAA,IAAI,uDAA2B,sBAAsB,MAAA,CAAC;QACtD,uBAAA,IAAI,iDAAqB,gBAAgB,MAAA,CAAC;QAC1C,uBAAA,IAAI,8CAAkB,aAAa,MAAA,CAAC;IACtC,CAAC;IAWD;;;;;OAKG;IACH,KAAK,CAAC,wBAAwB;QAC5B,IAAI,uBAAA,IAAI,6CAAU,IAAI,CAAC,uBAAA,IAAI,2FAAgB,MAApB,IAAI,CAAkB,EAAE,CAAC;YAC9C,OAAO;QACT,CAAC;QAED,IAAI,UAAU,CAAC;QAEf,IAAI,uBAAA,IAAI,yDAAsB,EAAE,CAAC;YAC/B,MAAM,uBAAA,IAAI,yDAAsB,CAAC;YACjC,OAAO;QACT,CAAC;QAED,IAAI,CAAC;YACH,uBAAA,IAAI,qDACF,uBAAA,IAAI,2DAAwB,CAAC,uBAAuB,EAAE,MAAA,CAAC;YAEzD,UAAU,GAAG,MAAM,uBAAA,IAAI,yDAAsB,CAAC;QAChD,CAAC;gBAAS,CAAC;YACT,uBAAA,IAAI,qDAAyB,SAAS,MAAA,CAAC;QACzC,CAAC;QAED,MAAM,uBAAA,IAAI,wFAAa,MAAjB,IAAI,EAAc,UAAU,CAAC,kBAAkB,CAAC,CAAC;IACzD,CAAC;IAyED;;OAEG;IACH,MAAM;QACJ,uBAAA,IAAI,yCAAa,KAAK,MAAA,CAAC;IACzB,CAAC;IAED;;OAEG;IACH,OAAO;QACL,uBAAA,IAAI,yCAAa,IAAI,MAAA,CAAC;IACxB,CAAC;CACF;AAhMD,kEAgMC;;IArHG,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,cAAc,GAAG,uBAAA,IAAI,kDAAe,CAAC;AACtE,CAAC;AAgCD;;;;GAIG;AACH,KAAK,mDAAc,kBAAgC;IACjD,MAAM,2BAA2B,GAC/B,MAAM,uBAAA,IAAI,sGAA2B,MAA/B,IAAI,EAA4B,kBAAkB,CAAC,CAAC;IAC5D,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE;QACf,OAAO;YACL,kBAAkB,EAAE,2BAA2B;YAC/C,cAAc,EAAE,IAAI,CAAC,GAAG,EAAE;SAC3B,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC,uHAQwB,SAAe;IACtC,IAAI,CAAC,IAAA,8BAAoB,EAAC,SAAS,CAAC,EAAE,CAAC;QACrC,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,OAAO,IAAA,wBAAc,EAAC,SAAS,EAAE,uBAAA,IAAI,kDAAe,CAAC,CAAC;AACxD,CAAC,2DAED,KAAK,iEACH,kBAAgC;IAEhC,MAAM,2BAA2B,GAAiB,EAAE,CAAC;IACrD,MAAM,aAAa,GAAG,uBAAA,IAAI,qDAAkB,MAAtB,IAAI,CAAoB,CAAC;IAC/C,MAAM,cAAc,GAAG,IAAA,2DAAiC,EAAC,aAAa,CAAC,CAAC;IAExE,KAAK,MAAM,CACT,qBAAqB,EACrB,sBAAsB,EACvB,IAAI,MAAM,CAAC,OAAO,CAAC,kBAAkB,CAAC,EAAE,CAAC;QACxC,IAAI,cAAc,GAAG,uBAAA,IAAI,oGAAyB,MAA7B,IAAI,EACvB,sBAAsB,CACvB,CAAC;QACF,IAAI,cAAc,KAAK,IAAI,EAAE,CAAC;YAC5B,SAAS;QACX,CAAC;QAED,IAAI,KAAK,CAAC,OAAO,CAAC,cAAc,CAAC,IAAI,cAAc,EAAE,CAAC;YACpD,MAAM,aAAa,GAAG,cAAc,CAAC,IAAI,CACvC,CAAC,WAAW,EAAwC,EAAE;gBACpD,IAAI,CAAC,IAAA,qDAA2B,EAAC,WAAW,CAAC,EAAE,CAAC;oBAC9C,OAAO,KAAK,CAAC;gBACf,CAAC;gBAED,OAAO,cAAc,IAAI,WAAW,CAAC,KAAK,CAAC,KAAK,CAAC;YACnD,CAAC,CACF,CAAC;YACF,IAAI,aAAa,EAAE,CAAC;gBAClB,cAAc,GAAG;oBACf,IAAI,EAAE,aAAa,CAAC,IAAI;oBACxB,KAAK,EAAE,aAAa,CAAC,KAAK;iBAC3B,CAAC;YACJ,CAAC;QACH,CAAC;QAED,2BAA2B,CAAC,qBAAqB,CAAC,GAAG,cAAc,CAAC;IACtE,CAAC;IACD,OAAO,2BAA2B,CAAC;AACrC,CAAC","sourcesContent":["import {\n BaseController,\n type ControllerGetStateAction,\n type ControllerStateChangeEvent,\n} from '@metamask/base-controller';\nimport type { Messenger } from '@metamask/messenger';\nimport {\n isValidSemVerVersion,\n type Json,\n type SemVerVersion,\n} from '@metamask/utils';\n\nimport type { AbstractClientConfigApiService } from './client-config-api-service/abstract-client-config-api-service';\nimport type {\n FeatureFlags,\n ServiceResponse,\n FeatureFlagScopeValue,\n} from './remote-feature-flag-controller-types';\nimport {\n generateDeterministicRandomNumber,\n isFeatureFlagWithScopeValue,\n} from './utils/user-segmentation-utils';\nimport { isVersionFeatureFlag, getVersionData } from './utils/version';\n\n// === GENERAL ===\n\nconst controllerName = 'RemoteFeatureFlagController';\nexport const DEFAULT_CACHE_DURATION = 24 * 60 * 60 * 1000; // 1 day\n\n// === STATE ===\n\nexport type RemoteFeatureFlagControllerState = {\n remoteFeatureFlags: FeatureFlags;\n cacheTimestamp: number;\n};\n\nconst remoteFeatureFlagControllerMetadata = {\n remoteFeatureFlags: {\n includeInStateLogs: true,\n persist: true,\n includeInDebugSnapshot: true,\n usedInUi: true,\n },\n cacheTimestamp: {\n includeInStateLogs: true,\n persist: true,\n includeInDebugSnapshot: true,\n usedInUi: false,\n },\n};\n\n// === MESSENGER ===\n\n/**\n * The action to retrieve the state of the {@link RemoteFeatureFlagController}.\n */\nexport type RemoteFeatureFlagControllerGetStateAction =\n ControllerGetStateAction<\n typeof controllerName,\n RemoteFeatureFlagControllerState\n >;\n\nexport type RemoteFeatureFlagControllerUpdateRemoteFeatureFlagsAction = {\n type: `${typeof controllerName}:updateRemoteFeatureFlags`;\n handler: RemoteFeatureFlagController['updateRemoteFeatureFlags'];\n};\n\nexport type RemoteFeatureFlagControllerActions =\n | RemoteFeatureFlagControllerGetStateAction\n | RemoteFeatureFlagControllerUpdateRemoteFeatureFlagsAction;\n\nexport type RemoteFeatureFlagControllerStateChangeEvent =\n ControllerStateChangeEvent<\n typeof controllerName,\n RemoteFeatureFlagControllerState\n >;\n\nexport type RemoteFeatureFlagControllerEvents =\n RemoteFeatureFlagControllerStateChangeEvent;\n\nexport type RemoteFeatureFlagControllerMessenger = Messenger<\n typeof controllerName,\n RemoteFeatureFlagControllerActions,\n RemoteFeatureFlagControllerEvents\n>;\n\n/**\n * Returns the default state for the RemoteFeatureFlagController.\n *\n * @returns The default controller state.\n */\nexport function getDefaultRemoteFeatureFlagControllerState(): RemoteFeatureFlagControllerState {\n return {\n remoteFeatureFlags: {},\n cacheTimestamp: 0,\n };\n}\n\n/**\n * The RemoteFeatureFlagController manages the retrieval and caching of remote feature flags.\n * It fetches feature flags from a remote API, caches them, and provides methods to access\n * and manage these flags. The controller ensures that feature flags are refreshed based on\n * a specified interval and handles cases where the controller is disabled or the network is unavailable.\n */\nexport class RemoteFeatureFlagController extends BaseController<\n typeof controllerName,\n RemoteFeatureFlagControllerState,\n RemoteFeatureFlagControllerMessenger\n> {\n readonly #fetchInterval: number;\n\n #disabled: boolean;\n\n readonly #clientConfigApiService: AbstractClientConfigApiService;\n\n #inProgressFlagUpdate?: Promise<ServiceResponse>;\n\n readonly #getMetaMetricsId: () => string;\n\n readonly #clientVersion: SemVerVersion;\n\n /**\n * Constructs a new RemoteFeatureFlagController instance.\n *\n * @param options - The controller options.\n * @param options.messenger - The messenger used for communication.\n * @param options.state - The initial state of the controller.\n * @param options.clientConfigApiService - The service instance to fetch remote feature flags.\n * @param options.fetchInterval - The interval in milliseconds before cached flags expire. Defaults to 1 day.\n * @param options.disabled - Determines if the controller should be disabled initially. Defaults to false.\n * @param options.getMetaMetricsId - Returns metaMetricsId.\n * @param options.clientVersion - The current client version for version-based feature flag filtering. Must be a valid 3-part SemVer version string.\n */\n constructor({\n messenger,\n state,\n clientConfigApiService,\n fetchInterval = DEFAULT_CACHE_DURATION,\n disabled = false,\n getMetaMetricsId,\n clientVersion,\n }: {\n messenger: RemoteFeatureFlagControllerMessenger;\n state?: Partial<RemoteFeatureFlagControllerState>;\n clientConfigApiService: AbstractClientConfigApiService;\n getMetaMetricsId: () => string;\n fetchInterval?: number;\n disabled?: boolean;\n clientVersion: string;\n }) {\n if (!isValidSemVerVersion(clientVersion)) {\n throw new Error(\n `Invalid clientVersion: \"${clientVersion}\". Must be a valid 3-part SemVer version string`,\n );\n }\n\n super({\n name: controllerName,\n metadata: remoteFeatureFlagControllerMetadata,\n messenger,\n state: {\n ...getDefaultRemoteFeatureFlagControllerState(),\n ...state,\n },\n });\n\n this.#fetchInterval = fetchInterval;\n this.#disabled = disabled;\n this.#clientConfigApiService = clientConfigApiService;\n this.#getMetaMetricsId = getMetaMetricsId;\n this.#clientVersion = clientVersion;\n }\n\n /**\n * Checks if the cached feature flags are expired based on the fetch interval.\n *\n * @returns Whether the cache is expired (`true`) or still valid (`false`).\n */\n #isCacheExpired(): boolean {\n return Date.now() - this.state.cacheTimestamp > this.#fetchInterval;\n }\n\n /**\n * Retrieves the remote feature flags, fetching from the API if necessary.\n * Uses caching to prevent redundant API calls and handles concurrent fetches.\n *\n * @returns A promise that resolves to the current set of feature flags.\n */\n async updateRemoteFeatureFlags(): Promise<void> {\n if (this.#disabled || !this.#isCacheExpired()) {\n return;\n }\n\n let serverData;\n\n if (this.#inProgressFlagUpdate) {\n await this.#inProgressFlagUpdate;\n return;\n }\n\n try {\n this.#inProgressFlagUpdate =\n this.#clientConfigApiService.fetchRemoteFeatureFlags();\n\n serverData = await this.#inProgressFlagUpdate;\n } finally {\n this.#inProgressFlagUpdate = undefined;\n }\n\n await this.#updateCache(serverData.remoteFeatureFlags);\n }\n\n /**\n * Updates the controller's state with new feature flags and resets the cache timestamp.\n *\n * @param remoteFeatureFlags - The new feature flags to cache.\n */\n async #updateCache(remoteFeatureFlags: FeatureFlags) {\n const processedRemoteFeatureFlags =\n await this.#processRemoteFeatureFlags(remoteFeatureFlags);\n this.update(() => {\n return {\n remoteFeatureFlags: processedRemoteFeatureFlags,\n cacheTimestamp: Date.now(),\n };\n });\n }\n\n /**\n * Processes a version-based feature flag to get the appropriate value for the current client version.\n *\n * @param flagValue - The feature flag value to process\n * @returns The processed value, or null if no version qualifies (skip this flag)\n */\n #processVersionBasedFlag(flagValue: Json): Json | null {\n if (!isVersionFeatureFlag(flagValue)) {\n return flagValue;\n }\n\n return getVersionData(flagValue, this.#clientVersion);\n }\n\n async #processRemoteFeatureFlags(\n remoteFeatureFlags: FeatureFlags,\n ): Promise<FeatureFlags> {\n const processedRemoteFeatureFlags: FeatureFlags = {};\n const metaMetricsId = this.#getMetaMetricsId();\n const thresholdValue = generateDeterministicRandomNumber(metaMetricsId);\n\n for (const [\n remoteFeatureFlagName,\n remoteFeatureFlagValue,\n ] of Object.entries(remoteFeatureFlags)) {\n let processedValue = this.#processVersionBasedFlag(\n remoteFeatureFlagValue,\n );\n if (processedValue === null) {\n continue;\n }\n\n if (Array.isArray(processedValue) && thresholdValue) {\n const selectedGroup = processedValue.find(\n (featureFlag): featureFlag is FeatureFlagScopeValue => {\n if (!isFeatureFlagWithScopeValue(featureFlag)) {\n return false;\n }\n\n return thresholdValue <= featureFlag.scope.value;\n },\n );\n if (selectedGroup) {\n processedValue = {\n name: selectedGroup.name,\n value: selectedGroup.value,\n };\n }\n }\n\n processedRemoteFeatureFlags[remoteFeatureFlagName] = processedValue;\n }\n return processedRemoteFeatureFlags;\n }\n\n /**\n * Enables the controller, allowing it to make network requests.\n */\n enable(): void {\n this.#disabled = false;\n }\n\n /**\n * Disables the controller, preventing it from making network requests.\n */\n disable(): void {\n this.#disabled = true;\n }\n}\n"]}
@@ -44,14 +44,16 @@ export declare class RemoteFeatureFlagController extends BaseController<typeof c
44
44
  * @param options.fetchInterval - The interval in milliseconds before cached flags expire. Defaults to 1 day.
45
45
  * @param options.disabled - Determines if the controller should be disabled initially. Defaults to false.
46
46
  * @param options.getMetaMetricsId - Returns metaMetricsId.
47
+ * @param options.clientVersion - The current client version for version-based feature flag filtering. Must be a valid 3-part SemVer version string.
47
48
  */
48
- constructor({ messenger, state, clientConfigApiService, fetchInterval, disabled, getMetaMetricsId, }: {
49
+ constructor({ messenger, state, clientConfigApiService, fetchInterval, disabled, getMetaMetricsId, clientVersion, }: {
49
50
  messenger: RemoteFeatureFlagControllerMessenger;
50
51
  state?: Partial<RemoteFeatureFlagControllerState>;
51
52
  clientConfigApiService: AbstractClientConfigApiService;
52
53
  getMetaMetricsId: () => string;
53
54
  fetchInterval?: number;
54
55
  disabled?: boolean;
56
+ clientVersion: string;
55
57
  });
56
58
  /**
57
59
  * Retrieves the remote feature flags, fetching from the API if necessary.
@@ -1 +1 @@
1
- {"version":3,"file":"remote-feature-flag-controller.d.cts","sourceRoot":"","sources":["../src/remote-feature-flag-controller.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,KAAK,wBAAwB,EAC7B,KAAK,0BAA0B,EAChC,kCAAkC;AACnC,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAErD,OAAO,KAAK,EAAE,8BAA8B,EAAE,2EAAuE;AACrH,OAAO,KAAK,EACV,YAAY,EAGb,mDAA+C;AAQhD,QAAA,MAAM,cAAc,gCAAgC,CAAC;AACrD,eAAO,MAAM,sBAAsB,QAAsB,CAAC;AAI1D,MAAM,MAAM,gCAAgC,GAAG;IAC7C,kBAAkB,EAAE,YAAY,CAAC;IACjC,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAmBF;;GAEG;AACH,MAAM,MAAM,yCAAyC,GACnD,wBAAwB,CACtB,OAAO,cAAc,EACrB,gCAAgC,CACjC,CAAC;AAEJ,MAAM,MAAM,yDAAyD,GAAG;IACtE,IAAI,EAAE,GAAG,OAAO,cAAc,2BAA2B,CAAC;IAC1D,OAAO,EAAE,2BAA2B,CAAC,0BAA0B,CAAC,CAAC;CAClE,CAAC;AAEF,MAAM,MAAM,kCAAkC,GAC1C,yCAAyC,GACzC,yDAAyD,CAAC;AAE9D,MAAM,MAAM,2CAA2C,GACrD,0BAA0B,CACxB,OAAO,cAAc,EACrB,gCAAgC,CACjC,CAAC;AAEJ,MAAM,MAAM,iCAAiC,GAC3C,2CAA2C,CAAC;AAE9C,MAAM,MAAM,oCAAoC,GAAG,SAAS,CAC1D,OAAO,cAAc,EACrB,kCAAkC,EAClC,iCAAiC,CAClC,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,0CAA0C,IAAI,gCAAgC,CAK7F;AAED;;;;;GAKG;AACH,qBAAa,2BAA4B,SAAQ,cAAc,CAC7D,OAAO,cAAc,EACrB,gCAAgC,EAChC,oCAAoC,CACrC;;IAWC;;;;;;;;;;OAUG;gBACS,EACV,SAAS,EACT,KAAK,EACL,sBAAsB,EACtB,aAAsC,EACtC,QAAgB,EAChB,gBAAgB,GACjB,EAAE;QACD,SAAS,EAAE,oCAAoC,CAAC;QAChD,KAAK,CAAC,EAAE,OAAO,CAAC,gCAAgC,CAAC,CAAC;QAClD,sBAAsB,EAAE,8BAA8B,CAAC;QACvD,gBAAgB,EAAE,MAAM,MAAM,CAAC;QAC/B,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;KACpB;IA0BD;;;;;OAKG;IACG,wBAAwB,IAAI,OAAO,CAAC,IAAI,CAAC;IA4E/C;;OAEG;IACH,MAAM,IAAI,IAAI;IAId;;OAEG;IACH,OAAO,IAAI,IAAI;CAGhB"}
1
+ {"version":3,"file":"remote-feature-flag-controller.d.cts","sourceRoot":"","sources":["../src/remote-feature-flag-controller.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,KAAK,wBAAwB,EAC7B,KAAK,0BAA0B,EAChC,kCAAkC;AACnC,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAOrD,OAAO,KAAK,EAAE,8BAA8B,EAAE,2EAAuE;AACrH,OAAO,KAAK,EACV,YAAY,EAGb,mDAA+C;AAShD,QAAA,MAAM,cAAc,gCAAgC,CAAC;AACrD,eAAO,MAAM,sBAAsB,QAAsB,CAAC;AAI1D,MAAM,MAAM,gCAAgC,GAAG;IAC7C,kBAAkB,EAAE,YAAY,CAAC;IACjC,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAmBF;;GAEG;AACH,MAAM,MAAM,yCAAyC,GACnD,wBAAwB,CACtB,OAAO,cAAc,EACrB,gCAAgC,CACjC,CAAC;AAEJ,MAAM,MAAM,yDAAyD,GAAG;IACtE,IAAI,EAAE,GAAG,OAAO,cAAc,2BAA2B,CAAC;IAC1D,OAAO,EAAE,2BAA2B,CAAC,0BAA0B,CAAC,CAAC;CAClE,CAAC;AAEF,MAAM,MAAM,kCAAkC,GAC1C,yCAAyC,GACzC,yDAAyD,CAAC;AAE9D,MAAM,MAAM,2CAA2C,GACrD,0BAA0B,CACxB,OAAO,cAAc,EACrB,gCAAgC,CACjC,CAAC;AAEJ,MAAM,MAAM,iCAAiC,GAC3C,2CAA2C,CAAC;AAE9C,MAAM,MAAM,oCAAoC,GAAG,SAAS,CAC1D,OAAO,cAAc,EACrB,kCAAkC,EAClC,iCAAiC,CAClC,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,0CAA0C,IAAI,gCAAgC,CAK7F;AAED;;;;;GAKG;AACH,qBAAa,2BAA4B,SAAQ,cAAc,CAC7D,OAAO,cAAc,EACrB,gCAAgC,EAChC,oCAAoC,CACrC;;IAaC;;;;;;;;;;;OAWG;gBACS,EACV,SAAS,EACT,KAAK,EACL,sBAAsB,EACtB,aAAsC,EACtC,QAAgB,EAChB,gBAAgB,EAChB,aAAa,GACd,EAAE;QACD,SAAS,EAAE,oCAAoC,CAAC;QAChD,KAAK,CAAC,EAAE,OAAO,CAAC,gCAAgC,CAAC,CAAC;QAClD,sBAAsB,EAAE,8BAA8B,CAAC;QACvD,gBAAgB,EAAE,MAAM,MAAM,CAAC;QAC/B,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,aAAa,EAAE,MAAM,CAAC;KACvB;IAiCD;;;;;OAKG;IACG,wBAAwB,IAAI,OAAO,CAAC,IAAI,CAAC;IA+F/C;;OAEG;IACH,MAAM,IAAI,IAAI;IAId;;OAEG;IACH,OAAO,IAAI,IAAI;CAGhB"}
@@ -44,14 +44,16 @@ export declare class RemoteFeatureFlagController extends BaseController<typeof c
44
44
  * @param options.fetchInterval - The interval in milliseconds before cached flags expire. Defaults to 1 day.
45
45
  * @param options.disabled - Determines if the controller should be disabled initially. Defaults to false.
46
46
  * @param options.getMetaMetricsId - Returns metaMetricsId.
47
+ * @param options.clientVersion - The current client version for version-based feature flag filtering. Must be a valid 3-part SemVer version string.
47
48
  */
48
- constructor({ messenger, state, clientConfigApiService, fetchInterval, disabled, getMetaMetricsId, }: {
49
+ constructor({ messenger, state, clientConfigApiService, fetchInterval, disabled, getMetaMetricsId, clientVersion, }: {
49
50
  messenger: RemoteFeatureFlagControllerMessenger;
50
51
  state?: Partial<RemoteFeatureFlagControllerState>;
51
52
  clientConfigApiService: AbstractClientConfigApiService;
52
53
  getMetaMetricsId: () => string;
53
54
  fetchInterval?: number;
54
55
  disabled?: boolean;
56
+ clientVersion: string;
55
57
  });
56
58
  /**
57
59
  * Retrieves the remote feature flags, fetching from the API if necessary.
@@ -1 +1 @@
1
- {"version":3,"file":"remote-feature-flag-controller.d.mts","sourceRoot":"","sources":["../src/remote-feature-flag-controller.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,KAAK,wBAAwB,EAC7B,KAAK,0BAA0B,EAChC,kCAAkC;AACnC,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAErD,OAAO,KAAK,EAAE,8BAA8B,EAAE,2EAAuE;AACrH,OAAO,KAAK,EACV,YAAY,EAGb,mDAA+C;AAQhD,QAAA,MAAM,cAAc,gCAAgC,CAAC;AACrD,eAAO,MAAM,sBAAsB,QAAsB,CAAC;AAI1D,MAAM,MAAM,gCAAgC,GAAG;IAC7C,kBAAkB,EAAE,YAAY,CAAC;IACjC,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAmBF;;GAEG;AACH,MAAM,MAAM,yCAAyC,GACnD,wBAAwB,CACtB,OAAO,cAAc,EACrB,gCAAgC,CACjC,CAAC;AAEJ,MAAM,MAAM,yDAAyD,GAAG;IACtE,IAAI,EAAE,GAAG,OAAO,cAAc,2BAA2B,CAAC;IAC1D,OAAO,EAAE,2BAA2B,CAAC,0BAA0B,CAAC,CAAC;CAClE,CAAC;AAEF,MAAM,MAAM,kCAAkC,GAC1C,yCAAyC,GACzC,yDAAyD,CAAC;AAE9D,MAAM,MAAM,2CAA2C,GACrD,0BAA0B,CACxB,OAAO,cAAc,EACrB,gCAAgC,CACjC,CAAC;AAEJ,MAAM,MAAM,iCAAiC,GAC3C,2CAA2C,CAAC;AAE9C,MAAM,MAAM,oCAAoC,GAAG,SAAS,CAC1D,OAAO,cAAc,EACrB,kCAAkC,EAClC,iCAAiC,CAClC,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,0CAA0C,IAAI,gCAAgC,CAK7F;AAED;;;;;GAKG;AACH,qBAAa,2BAA4B,SAAQ,cAAc,CAC7D,OAAO,cAAc,EACrB,gCAAgC,EAChC,oCAAoC,CACrC;;IAWC;;;;;;;;;;OAUG;gBACS,EACV,SAAS,EACT,KAAK,EACL,sBAAsB,EACtB,aAAsC,EACtC,QAAgB,EAChB,gBAAgB,GACjB,EAAE;QACD,SAAS,EAAE,oCAAoC,CAAC;QAChD,KAAK,CAAC,EAAE,OAAO,CAAC,gCAAgC,CAAC,CAAC;QAClD,sBAAsB,EAAE,8BAA8B,CAAC;QACvD,gBAAgB,EAAE,MAAM,MAAM,CAAC;QAC/B,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;KACpB;IA0BD;;;;;OAKG;IACG,wBAAwB,IAAI,OAAO,CAAC,IAAI,CAAC;IA4E/C;;OAEG;IACH,MAAM,IAAI,IAAI;IAId;;OAEG;IACH,OAAO,IAAI,IAAI;CAGhB"}
1
+ {"version":3,"file":"remote-feature-flag-controller.d.mts","sourceRoot":"","sources":["../src/remote-feature-flag-controller.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,KAAK,wBAAwB,EAC7B,KAAK,0BAA0B,EAChC,kCAAkC;AACnC,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAOrD,OAAO,KAAK,EAAE,8BAA8B,EAAE,2EAAuE;AACrH,OAAO,KAAK,EACV,YAAY,EAGb,mDAA+C;AAShD,QAAA,MAAM,cAAc,gCAAgC,CAAC;AACrD,eAAO,MAAM,sBAAsB,QAAsB,CAAC;AAI1D,MAAM,MAAM,gCAAgC,GAAG;IAC7C,kBAAkB,EAAE,YAAY,CAAC;IACjC,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAmBF;;GAEG;AACH,MAAM,MAAM,yCAAyC,GACnD,wBAAwB,CACtB,OAAO,cAAc,EACrB,gCAAgC,CACjC,CAAC;AAEJ,MAAM,MAAM,yDAAyD,GAAG;IACtE,IAAI,EAAE,GAAG,OAAO,cAAc,2BAA2B,CAAC;IAC1D,OAAO,EAAE,2BAA2B,CAAC,0BAA0B,CAAC,CAAC;CAClE,CAAC;AAEF,MAAM,MAAM,kCAAkC,GAC1C,yCAAyC,GACzC,yDAAyD,CAAC;AAE9D,MAAM,MAAM,2CAA2C,GACrD,0BAA0B,CACxB,OAAO,cAAc,EACrB,gCAAgC,CACjC,CAAC;AAEJ,MAAM,MAAM,iCAAiC,GAC3C,2CAA2C,CAAC;AAE9C,MAAM,MAAM,oCAAoC,GAAG,SAAS,CAC1D,OAAO,cAAc,EACrB,kCAAkC,EAClC,iCAAiC,CAClC,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,0CAA0C,IAAI,gCAAgC,CAK7F;AAED;;;;;GAKG;AACH,qBAAa,2BAA4B,SAAQ,cAAc,CAC7D,OAAO,cAAc,EACrB,gCAAgC,EAChC,oCAAoC,CACrC;;IAaC;;;;;;;;;;;OAWG;gBACS,EACV,SAAS,EACT,KAAK,EACL,sBAAsB,EACtB,aAAsC,EACtC,QAAgB,EAChB,gBAAgB,EAChB,aAAa,GACd,EAAE;QACD,SAAS,EAAE,oCAAoC,CAAC;QAChD,KAAK,CAAC,EAAE,OAAO,CAAC,gCAAgC,CAAC,CAAC;QAClD,sBAAsB,EAAE,8BAA8B,CAAC;QACvD,gBAAgB,EAAE,MAAM,MAAM,CAAC;QAC/B,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,aAAa,EAAE,MAAM,CAAC;KACvB;IAiCD;;;;;OAKG;IACG,wBAAwB,IAAI,OAAO,CAAC,IAAI,CAAC;IA+F/C;;OAEG;IACH,MAAM,IAAI,IAAI;IAId;;OAEG;IACH,OAAO,IAAI,IAAI;CAGhB"}
@@ -9,9 +9,11 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
9
9
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
10
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
11
  };
12
- var _RemoteFeatureFlagController_instances, _RemoteFeatureFlagController_fetchInterval, _RemoteFeatureFlagController_disabled, _RemoteFeatureFlagController_clientConfigApiService, _RemoteFeatureFlagController_inProgressFlagUpdate, _RemoteFeatureFlagController_getMetaMetricsId, _RemoteFeatureFlagController_isCacheExpired, _RemoteFeatureFlagController_updateCache, _RemoteFeatureFlagController_processRemoteFeatureFlags;
12
+ var _RemoteFeatureFlagController_instances, _RemoteFeatureFlagController_fetchInterval, _RemoteFeatureFlagController_disabled, _RemoteFeatureFlagController_clientConfigApiService, _RemoteFeatureFlagController_inProgressFlagUpdate, _RemoteFeatureFlagController_getMetaMetricsId, _RemoteFeatureFlagController_clientVersion, _RemoteFeatureFlagController_isCacheExpired, _RemoteFeatureFlagController_updateCache, _RemoteFeatureFlagController_processVersionBasedFlag, _RemoteFeatureFlagController_processRemoteFeatureFlags;
13
13
  import { BaseController } from "@metamask/base-controller";
14
+ import { isValidSemVerVersion } from "@metamask/utils";
14
15
  import { generateDeterministicRandomNumber, isFeatureFlagWithScopeValue } from "./utils/user-segmentation-utils.mjs";
16
+ import { isVersionFeatureFlag, getVersionData } from "./utils/version.mjs";
15
17
  // === GENERAL ===
16
18
  const controllerName = 'RemoteFeatureFlagController';
17
19
  export const DEFAULT_CACHE_DURATION = 24 * 60 * 60 * 1000; // 1 day
@@ -57,8 +59,12 @@ export class RemoteFeatureFlagController extends BaseController {
57
59
  * @param options.fetchInterval - The interval in milliseconds before cached flags expire. Defaults to 1 day.
58
60
  * @param options.disabled - Determines if the controller should be disabled initially. Defaults to false.
59
61
  * @param options.getMetaMetricsId - Returns metaMetricsId.
62
+ * @param options.clientVersion - The current client version for version-based feature flag filtering. Must be a valid 3-part SemVer version string.
60
63
  */
61
- constructor({ messenger, state, clientConfigApiService, fetchInterval = DEFAULT_CACHE_DURATION, disabled = false, getMetaMetricsId, }) {
64
+ constructor({ messenger, state, clientConfigApiService, fetchInterval = DEFAULT_CACHE_DURATION, disabled = false, getMetaMetricsId, clientVersion, }) {
65
+ if (!isValidSemVerVersion(clientVersion)) {
66
+ throw new Error(`Invalid clientVersion: "${clientVersion}". Must be a valid 3-part SemVer version string`);
67
+ }
62
68
  super({
63
69
  name: controllerName,
64
70
  metadata: remoteFeatureFlagControllerMetadata,
@@ -74,10 +80,12 @@ export class RemoteFeatureFlagController extends BaseController {
74
80
  _RemoteFeatureFlagController_clientConfigApiService.set(this, void 0);
75
81
  _RemoteFeatureFlagController_inProgressFlagUpdate.set(this, void 0);
76
82
  _RemoteFeatureFlagController_getMetaMetricsId.set(this, void 0);
83
+ _RemoteFeatureFlagController_clientVersion.set(this, void 0);
77
84
  __classPrivateFieldSet(this, _RemoteFeatureFlagController_fetchInterval, fetchInterval, "f");
78
85
  __classPrivateFieldSet(this, _RemoteFeatureFlagController_disabled, disabled, "f");
79
86
  __classPrivateFieldSet(this, _RemoteFeatureFlagController_clientConfigApiService, clientConfigApiService, "f");
80
87
  __classPrivateFieldSet(this, _RemoteFeatureFlagController_getMetaMetricsId, getMetaMetricsId, "f");
88
+ __classPrivateFieldSet(this, _RemoteFeatureFlagController_clientVersion, clientVersion, "f");
81
89
  }
82
90
  /**
83
91
  * Retrieves the remote feature flags, fetching from the API if necessary.
@@ -116,7 +124,7 @@ export class RemoteFeatureFlagController extends BaseController {
116
124
  __classPrivateFieldSet(this, _RemoteFeatureFlagController_disabled, true, "f");
117
125
  }
118
126
  }
119
- _RemoteFeatureFlagController_fetchInterval = new WeakMap(), _RemoteFeatureFlagController_disabled = new WeakMap(), _RemoteFeatureFlagController_clientConfigApiService = new WeakMap(), _RemoteFeatureFlagController_inProgressFlagUpdate = new WeakMap(), _RemoteFeatureFlagController_getMetaMetricsId = new WeakMap(), _RemoteFeatureFlagController_instances = new WeakSet(), _RemoteFeatureFlagController_isCacheExpired = function _RemoteFeatureFlagController_isCacheExpired() {
127
+ _RemoteFeatureFlagController_fetchInterval = new WeakMap(), _RemoteFeatureFlagController_disabled = new WeakMap(), _RemoteFeatureFlagController_clientConfigApiService = new WeakMap(), _RemoteFeatureFlagController_inProgressFlagUpdate = new WeakMap(), _RemoteFeatureFlagController_getMetaMetricsId = new WeakMap(), _RemoteFeatureFlagController_clientVersion = new WeakMap(), _RemoteFeatureFlagController_instances = new WeakSet(), _RemoteFeatureFlagController_isCacheExpired = function _RemoteFeatureFlagController_isCacheExpired() {
120
128
  return Date.now() - this.state.cacheTimestamp > __classPrivateFieldGet(this, _RemoteFeatureFlagController_fetchInterval, "f");
121
129
  }, _RemoteFeatureFlagController_updateCache =
122
130
  /**
@@ -132,14 +140,22 @@ async function _RemoteFeatureFlagController_updateCache(remoteFeatureFlags) {
132
140
  cacheTimestamp: Date.now(),
133
141
  };
134
142
  });
143
+ }, _RemoteFeatureFlagController_processVersionBasedFlag = function _RemoteFeatureFlagController_processVersionBasedFlag(flagValue) {
144
+ if (!isVersionFeatureFlag(flagValue)) {
145
+ return flagValue;
146
+ }
147
+ return getVersionData(flagValue, __classPrivateFieldGet(this, _RemoteFeatureFlagController_clientVersion, "f"));
135
148
  }, _RemoteFeatureFlagController_processRemoteFeatureFlags = async function _RemoteFeatureFlagController_processRemoteFeatureFlags(remoteFeatureFlags) {
136
149
  const processedRemoteFeatureFlags = {};
137
150
  const metaMetricsId = __classPrivateFieldGet(this, _RemoteFeatureFlagController_getMetaMetricsId, "f").call(this);
138
151
  const thresholdValue = generateDeterministicRandomNumber(metaMetricsId);
139
152
  for (const [remoteFeatureFlagName, remoteFeatureFlagValue,] of Object.entries(remoteFeatureFlags)) {
140
- let processedValue = remoteFeatureFlagValue;
141
- if (Array.isArray(remoteFeatureFlagValue) && thresholdValue) {
142
- const selectedGroup = remoteFeatureFlagValue.find((featureFlag) => {
153
+ let processedValue = __classPrivateFieldGet(this, _RemoteFeatureFlagController_instances, "m", _RemoteFeatureFlagController_processVersionBasedFlag).call(this, remoteFeatureFlagValue);
154
+ if (processedValue === null) {
155
+ continue;
156
+ }
157
+ if (Array.isArray(processedValue) && thresholdValue) {
158
+ const selectedGroup = processedValue.find((featureFlag) => {
143
159
  if (!isFeatureFlagWithScopeValue(featureFlag)) {
144
160
  return false;
145
161
  }
@@ -1 +1 @@
1
- {"version":3,"file":"remote-feature-flag-controller.mjs","sourceRoot":"","sources":["../src/remote-feature-flag-controller.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n BaseController,\n type ControllerGetStateAction,\n type ControllerStateChangeEvent,\n} from '@metamask/base-controller';\nimport type { Messenger } from '@metamask/messenger';\n\nimport type { AbstractClientConfigApiService } from './client-config-api-service/abstract-client-config-api-service';\nimport type {\n FeatureFlags,\n ServiceResponse,\n FeatureFlagScopeValue,\n} from './remote-feature-flag-controller-types';\nimport {\n generateDeterministicRandomNumber,\n isFeatureFlagWithScopeValue,\n} from './utils/user-segmentation-utils';\n\n// === GENERAL ===\n\nconst controllerName = 'RemoteFeatureFlagController';\nexport const DEFAULT_CACHE_DURATION = 24 * 60 * 60 * 1000; // 1 day\n\n// === STATE ===\n\nexport type RemoteFeatureFlagControllerState = {\n remoteFeatureFlags: FeatureFlags;\n cacheTimestamp: number;\n};\n\nconst remoteFeatureFlagControllerMetadata = {\n remoteFeatureFlags: {\n includeInStateLogs: true,\n persist: true,\n includeInDebugSnapshot: true,\n usedInUi: true,\n },\n cacheTimestamp: {\n includeInStateLogs: true,\n persist: true,\n includeInDebugSnapshot: true,\n usedInUi: false,\n },\n};\n\n// === MESSENGER ===\n\n/**\n * The action to retrieve the state of the {@link RemoteFeatureFlagController}.\n */\nexport type RemoteFeatureFlagControllerGetStateAction =\n ControllerGetStateAction<\n typeof controllerName,\n RemoteFeatureFlagControllerState\n >;\n\nexport type RemoteFeatureFlagControllerUpdateRemoteFeatureFlagsAction = {\n type: `${typeof controllerName}:updateRemoteFeatureFlags`;\n handler: RemoteFeatureFlagController['updateRemoteFeatureFlags'];\n};\n\nexport type RemoteFeatureFlagControllerActions =\n | RemoteFeatureFlagControllerGetStateAction\n | RemoteFeatureFlagControllerUpdateRemoteFeatureFlagsAction;\n\nexport type RemoteFeatureFlagControllerStateChangeEvent =\n ControllerStateChangeEvent<\n typeof controllerName,\n RemoteFeatureFlagControllerState\n >;\n\nexport type RemoteFeatureFlagControllerEvents =\n RemoteFeatureFlagControllerStateChangeEvent;\n\nexport type RemoteFeatureFlagControllerMessenger = Messenger<\n typeof controllerName,\n RemoteFeatureFlagControllerActions,\n RemoteFeatureFlagControllerEvents\n>;\n\n/**\n * Returns the default state for the RemoteFeatureFlagController.\n *\n * @returns The default controller state.\n */\nexport function getDefaultRemoteFeatureFlagControllerState(): RemoteFeatureFlagControllerState {\n return {\n remoteFeatureFlags: {},\n cacheTimestamp: 0,\n };\n}\n\n/**\n * The RemoteFeatureFlagController manages the retrieval and caching of remote feature flags.\n * It fetches feature flags from a remote API, caches them, and provides methods to access\n * and manage these flags. The controller ensures that feature flags are refreshed based on\n * a specified interval and handles cases where the controller is disabled or the network is unavailable.\n */\nexport class RemoteFeatureFlagController extends BaseController<\n typeof controllerName,\n RemoteFeatureFlagControllerState,\n RemoteFeatureFlagControllerMessenger\n> {\n readonly #fetchInterval: number;\n\n #disabled: boolean;\n\n readonly #clientConfigApiService: AbstractClientConfigApiService;\n\n #inProgressFlagUpdate?: Promise<ServiceResponse>;\n\n readonly #getMetaMetricsId: () => string;\n\n /**\n * Constructs a new RemoteFeatureFlagController instance.\n *\n * @param options - The controller options.\n * @param options.messenger - The messenger used for communication.\n * @param options.state - The initial state of the controller.\n * @param options.clientConfigApiService - The service instance to fetch remote feature flags.\n * @param options.fetchInterval - The interval in milliseconds before cached flags expire. Defaults to 1 day.\n * @param options.disabled - Determines if the controller should be disabled initially. Defaults to false.\n * @param options.getMetaMetricsId - Returns metaMetricsId.\n */\n constructor({\n messenger,\n state,\n clientConfigApiService,\n fetchInterval = DEFAULT_CACHE_DURATION,\n disabled = false,\n getMetaMetricsId,\n }: {\n messenger: RemoteFeatureFlagControllerMessenger;\n state?: Partial<RemoteFeatureFlagControllerState>;\n clientConfigApiService: AbstractClientConfigApiService;\n getMetaMetricsId: () => string;\n fetchInterval?: number;\n disabled?: boolean;\n }) {\n super({\n name: controllerName,\n metadata: remoteFeatureFlagControllerMetadata,\n messenger,\n state: {\n ...getDefaultRemoteFeatureFlagControllerState(),\n ...state,\n },\n });\n\n this.#fetchInterval = fetchInterval;\n this.#disabled = disabled;\n this.#clientConfigApiService = clientConfigApiService;\n this.#getMetaMetricsId = getMetaMetricsId;\n }\n\n /**\n * Checks if the cached feature flags are expired based on the fetch interval.\n *\n * @returns Whether the cache is expired (`true`) or still valid (`false`).\n */\n #isCacheExpired(): boolean {\n return Date.now() - this.state.cacheTimestamp > this.#fetchInterval;\n }\n\n /**\n * Retrieves the remote feature flags, fetching from the API if necessary.\n * Uses caching to prevent redundant API calls and handles concurrent fetches.\n *\n * @returns A promise that resolves to the current set of feature flags.\n */\n async updateRemoteFeatureFlags(): Promise<void> {\n if (this.#disabled || !this.#isCacheExpired()) {\n return;\n }\n\n let serverData;\n\n if (this.#inProgressFlagUpdate) {\n await this.#inProgressFlagUpdate;\n return;\n }\n\n try {\n this.#inProgressFlagUpdate =\n this.#clientConfigApiService.fetchRemoteFeatureFlags();\n\n serverData = await this.#inProgressFlagUpdate;\n } finally {\n this.#inProgressFlagUpdate = undefined;\n }\n\n await this.#updateCache(serverData.remoteFeatureFlags);\n }\n\n /**\n * Updates the controller's state with new feature flags and resets the cache timestamp.\n *\n * @param remoteFeatureFlags - The new feature flags to cache.\n */\n async #updateCache(remoteFeatureFlags: FeatureFlags) {\n const processedRemoteFeatureFlags =\n await this.#processRemoteFeatureFlags(remoteFeatureFlags);\n this.update(() => {\n return {\n remoteFeatureFlags: processedRemoteFeatureFlags,\n cacheTimestamp: Date.now(),\n };\n });\n }\n\n async #processRemoteFeatureFlags(\n remoteFeatureFlags: FeatureFlags,\n ): Promise<FeatureFlags> {\n const processedRemoteFeatureFlags: FeatureFlags = {};\n const metaMetricsId = this.#getMetaMetricsId();\n const thresholdValue = generateDeterministicRandomNumber(metaMetricsId);\n\n for (const [\n remoteFeatureFlagName,\n remoteFeatureFlagValue,\n ] of Object.entries(remoteFeatureFlags)) {\n let processedValue = remoteFeatureFlagValue;\n\n if (Array.isArray(remoteFeatureFlagValue) && thresholdValue) {\n const selectedGroup = remoteFeatureFlagValue.find(\n (featureFlag): featureFlag is FeatureFlagScopeValue => {\n if (!isFeatureFlagWithScopeValue(featureFlag)) {\n return false;\n }\n\n return thresholdValue <= featureFlag.scope.value;\n },\n );\n if (selectedGroup) {\n processedValue = {\n name: selectedGroup.name,\n value: selectedGroup.value,\n };\n }\n }\n\n processedRemoteFeatureFlags[remoteFeatureFlagName] = processedValue;\n }\n return processedRemoteFeatureFlags;\n }\n\n /**\n * Enables the controller, allowing it to make network requests.\n */\n enable(): void {\n this.#disabled = false;\n }\n\n /**\n * Disables the controller, preventing it from making network requests.\n */\n disable(): void {\n this.#disabled = true;\n }\n}\n"]}
1
+ {"version":3,"file":"remote-feature-flag-controller.mjs","sourceRoot":"","sources":["../src/remote-feature-flag-controller.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n BaseController,\n type ControllerGetStateAction,\n type ControllerStateChangeEvent,\n} from '@metamask/base-controller';\nimport type { Messenger } from '@metamask/messenger';\nimport {\n isValidSemVerVersion,\n type Json,\n type SemVerVersion,\n} from '@metamask/utils';\n\nimport type { AbstractClientConfigApiService } from './client-config-api-service/abstract-client-config-api-service';\nimport type {\n FeatureFlags,\n ServiceResponse,\n FeatureFlagScopeValue,\n} from './remote-feature-flag-controller-types';\nimport {\n generateDeterministicRandomNumber,\n isFeatureFlagWithScopeValue,\n} from './utils/user-segmentation-utils';\nimport { isVersionFeatureFlag, getVersionData } from './utils/version';\n\n// === GENERAL ===\n\nconst controllerName = 'RemoteFeatureFlagController';\nexport const DEFAULT_CACHE_DURATION = 24 * 60 * 60 * 1000; // 1 day\n\n// === STATE ===\n\nexport type RemoteFeatureFlagControllerState = {\n remoteFeatureFlags: FeatureFlags;\n cacheTimestamp: number;\n};\n\nconst remoteFeatureFlagControllerMetadata = {\n remoteFeatureFlags: {\n includeInStateLogs: true,\n persist: true,\n includeInDebugSnapshot: true,\n usedInUi: true,\n },\n cacheTimestamp: {\n includeInStateLogs: true,\n persist: true,\n includeInDebugSnapshot: true,\n usedInUi: false,\n },\n};\n\n// === MESSENGER ===\n\n/**\n * The action to retrieve the state of the {@link RemoteFeatureFlagController}.\n */\nexport type RemoteFeatureFlagControllerGetStateAction =\n ControllerGetStateAction<\n typeof controllerName,\n RemoteFeatureFlagControllerState\n >;\n\nexport type RemoteFeatureFlagControllerUpdateRemoteFeatureFlagsAction = {\n type: `${typeof controllerName}:updateRemoteFeatureFlags`;\n handler: RemoteFeatureFlagController['updateRemoteFeatureFlags'];\n};\n\nexport type RemoteFeatureFlagControllerActions =\n | RemoteFeatureFlagControllerGetStateAction\n | RemoteFeatureFlagControllerUpdateRemoteFeatureFlagsAction;\n\nexport type RemoteFeatureFlagControllerStateChangeEvent =\n ControllerStateChangeEvent<\n typeof controllerName,\n RemoteFeatureFlagControllerState\n >;\n\nexport type RemoteFeatureFlagControllerEvents =\n RemoteFeatureFlagControllerStateChangeEvent;\n\nexport type RemoteFeatureFlagControllerMessenger = Messenger<\n typeof controllerName,\n RemoteFeatureFlagControllerActions,\n RemoteFeatureFlagControllerEvents\n>;\n\n/**\n * Returns the default state for the RemoteFeatureFlagController.\n *\n * @returns The default controller state.\n */\nexport function getDefaultRemoteFeatureFlagControllerState(): RemoteFeatureFlagControllerState {\n return {\n remoteFeatureFlags: {},\n cacheTimestamp: 0,\n };\n}\n\n/**\n * The RemoteFeatureFlagController manages the retrieval and caching of remote feature flags.\n * It fetches feature flags from a remote API, caches them, and provides methods to access\n * and manage these flags. The controller ensures that feature flags are refreshed based on\n * a specified interval and handles cases where the controller is disabled or the network is unavailable.\n */\nexport class RemoteFeatureFlagController extends BaseController<\n typeof controllerName,\n RemoteFeatureFlagControllerState,\n RemoteFeatureFlagControllerMessenger\n> {\n readonly #fetchInterval: number;\n\n #disabled: boolean;\n\n readonly #clientConfigApiService: AbstractClientConfigApiService;\n\n #inProgressFlagUpdate?: Promise<ServiceResponse>;\n\n readonly #getMetaMetricsId: () => string;\n\n readonly #clientVersion: SemVerVersion;\n\n /**\n * Constructs a new RemoteFeatureFlagController instance.\n *\n * @param options - The controller options.\n * @param options.messenger - The messenger used for communication.\n * @param options.state - The initial state of the controller.\n * @param options.clientConfigApiService - The service instance to fetch remote feature flags.\n * @param options.fetchInterval - The interval in milliseconds before cached flags expire. Defaults to 1 day.\n * @param options.disabled - Determines if the controller should be disabled initially. Defaults to false.\n * @param options.getMetaMetricsId - Returns metaMetricsId.\n * @param options.clientVersion - The current client version for version-based feature flag filtering. Must be a valid 3-part SemVer version string.\n */\n constructor({\n messenger,\n state,\n clientConfigApiService,\n fetchInterval = DEFAULT_CACHE_DURATION,\n disabled = false,\n getMetaMetricsId,\n clientVersion,\n }: {\n messenger: RemoteFeatureFlagControllerMessenger;\n state?: Partial<RemoteFeatureFlagControllerState>;\n clientConfigApiService: AbstractClientConfigApiService;\n getMetaMetricsId: () => string;\n fetchInterval?: number;\n disabled?: boolean;\n clientVersion: string;\n }) {\n if (!isValidSemVerVersion(clientVersion)) {\n throw new Error(\n `Invalid clientVersion: \"${clientVersion}\". Must be a valid 3-part SemVer version string`,\n );\n }\n\n super({\n name: controllerName,\n metadata: remoteFeatureFlagControllerMetadata,\n messenger,\n state: {\n ...getDefaultRemoteFeatureFlagControllerState(),\n ...state,\n },\n });\n\n this.#fetchInterval = fetchInterval;\n this.#disabled = disabled;\n this.#clientConfigApiService = clientConfigApiService;\n this.#getMetaMetricsId = getMetaMetricsId;\n this.#clientVersion = clientVersion;\n }\n\n /**\n * Checks if the cached feature flags are expired based on the fetch interval.\n *\n * @returns Whether the cache is expired (`true`) or still valid (`false`).\n */\n #isCacheExpired(): boolean {\n return Date.now() - this.state.cacheTimestamp > this.#fetchInterval;\n }\n\n /**\n * Retrieves the remote feature flags, fetching from the API if necessary.\n * Uses caching to prevent redundant API calls and handles concurrent fetches.\n *\n * @returns A promise that resolves to the current set of feature flags.\n */\n async updateRemoteFeatureFlags(): Promise<void> {\n if (this.#disabled || !this.#isCacheExpired()) {\n return;\n }\n\n let serverData;\n\n if (this.#inProgressFlagUpdate) {\n await this.#inProgressFlagUpdate;\n return;\n }\n\n try {\n this.#inProgressFlagUpdate =\n this.#clientConfigApiService.fetchRemoteFeatureFlags();\n\n serverData = await this.#inProgressFlagUpdate;\n } finally {\n this.#inProgressFlagUpdate = undefined;\n }\n\n await this.#updateCache(serverData.remoteFeatureFlags);\n }\n\n /**\n * Updates the controller's state with new feature flags and resets the cache timestamp.\n *\n * @param remoteFeatureFlags - The new feature flags to cache.\n */\n async #updateCache(remoteFeatureFlags: FeatureFlags) {\n const processedRemoteFeatureFlags =\n await this.#processRemoteFeatureFlags(remoteFeatureFlags);\n this.update(() => {\n return {\n remoteFeatureFlags: processedRemoteFeatureFlags,\n cacheTimestamp: Date.now(),\n };\n });\n }\n\n /**\n * Processes a version-based feature flag to get the appropriate value for the current client version.\n *\n * @param flagValue - The feature flag value to process\n * @returns The processed value, or null if no version qualifies (skip this flag)\n */\n #processVersionBasedFlag(flagValue: Json): Json | null {\n if (!isVersionFeatureFlag(flagValue)) {\n return flagValue;\n }\n\n return getVersionData(flagValue, this.#clientVersion);\n }\n\n async #processRemoteFeatureFlags(\n remoteFeatureFlags: FeatureFlags,\n ): Promise<FeatureFlags> {\n const processedRemoteFeatureFlags: FeatureFlags = {};\n const metaMetricsId = this.#getMetaMetricsId();\n const thresholdValue = generateDeterministicRandomNumber(metaMetricsId);\n\n for (const [\n remoteFeatureFlagName,\n remoteFeatureFlagValue,\n ] of Object.entries(remoteFeatureFlags)) {\n let processedValue = this.#processVersionBasedFlag(\n remoteFeatureFlagValue,\n );\n if (processedValue === null) {\n continue;\n }\n\n if (Array.isArray(processedValue) && thresholdValue) {\n const selectedGroup = processedValue.find(\n (featureFlag): featureFlag is FeatureFlagScopeValue => {\n if (!isFeatureFlagWithScopeValue(featureFlag)) {\n return false;\n }\n\n return thresholdValue <= featureFlag.scope.value;\n },\n );\n if (selectedGroup) {\n processedValue = {\n name: selectedGroup.name,\n value: selectedGroup.value,\n };\n }\n }\n\n processedRemoteFeatureFlags[remoteFeatureFlagName] = processedValue;\n }\n return processedRemoteFeatureFlags;\n }\n\n /**\n * Enables the controller, allowing it to make network requests.\n */\n enable(): void {\n this.#disabled = false;\n }\n\n /**\n * Disables the controller, preventing it from making network requests.\n */\n disable(): void {\n this.#disabled = true;\n }\n}\n"]}
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getVersionData = exports.isVersionFeatureFlag = exports.MULTI_VERSION_FLAG_KEYS = void 0;
4
+ const utils_1 = require("@metamask/utils");
5
+ /**
6
+ * Constants for MultiVersionFeatureFlagValue property names
7
+ * to ensure consistency across validation, type checking, and other usage.
8
+ */
9
+ exports.MULTI_VERSION_FLAG_KEYS = {
10
+ VERSIONS: 'versions',
11
+ };
12
+ /**
13
+ * Checks if a feature flag value is a multi-version gated flag (contains versions object with valid SemVer keys).
14
+ *
15
+ * @param value - The feature flag value to check
16
+ * @returns true if the value is a multi-version feature flag with valid SemVer version keys, false otherwise
17
+ */
18
+ function isVersionFeatureFlag(value) {
19
+ if (typeof value !== 'object' ||
20
+ value === null ||
21
+ Array.isArray(value) ||
22
+ !(exports.MULTI_VERSION_FLAG_KEYS.VERSIONS in value)) {
23
+ return false;
24
+ }
25
+ const versions = value[exports.MULTI_VERSION_FLAG_KEYS.VERSIONS];
26
+ if (typeof versions !== 'object' ||
27
+ versions === null ||
28
+ Array.isArray(versions)) {
29
+ return false;
30
+ }
31
+ // Validate that all version keys are valid SemVer versions
32
+ const versionKeys = Object.keys(versions);
33
+ return versionKeys.every((versionKey) => (0, utils_1.isValidSemVerVersion)(versionKey));
34
+ }
35
+ exports.isVersionFeatureFlag = isVersionFeatureFlag;
36
+ /**
37
+ * Selects the appropriate version value from a multi-version feature flag based on current app version.
38
+ * Returns the value from the highest version that the app version meets or exceeds.
39
+ *
40
+ * @param multiVersionFlag - The multi-version feature flag
41
+ * @param currentAppVersion - The current application version
42
+ * @returns The selected version value, or null if no version requirements are met
43
+ */
44
+ function getVersionData(multiVersionFlag, currentAppVersion) {
45
+ const sortedVersions = getObjectEntries(multiVersionFlag.versions).sort(([versionA], [versionB]) => {
46
+ return isVersionAtLeast(versionA, versionB) ? -1 : 1;
47
+ });
48
+ for (const [version, data] of sortedVersions) {
49
+ if (isVersionAtLeast(currentAppVersion, version)) {
50
+ return data;
51
+ }
52
+ }
53
+ return null;
54
+ }
55
+ exports.getVersionData = getVersionData;
56
+ /**
57
+ * Compares two semantic version strings.
58
+ * Both versions are expected to be valid SemVer format (e.g., "13.9.5").
59
+ *
60
+ * @param currentVersion - The current version (e.g., "13.9.5")
61
+ * @param requiredVersion - The required minimum version (e.g., "13.10.0")
62
+ * @returns true if currentVersion >= requiredVersion, false otherwise
63
+ */
64
+ function isVersionAtLeast(currentVersion, requiredVersion) {
65
+ return (currentVersion === requiredVersion ||
66
+ (0, utils_1.gtVersion)(currentVersion, requiredVersion));
67
+ }
68
+ /**
69
+ * A utility function that calls `Object.entries` while preserving the index type.
70
+ *
71
+ * @param input - The object to get the entries of.
72
+ * @returns - The object entries.
73
+ */
74
+ function getObjectEntries(input) {
75
+ // Use cast to preserve index type. Object.entries always widens to string.
76
+ return Object.entries(input);
77
+ }
78
+ //# sourceMappingURL=version.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.cjs","sourceRoot":"","sources":["../../src/utils/version.ts"],"names":[],"mappings":";;;AAAA,2CAAkE;AAKlE;;;GAGG;AACU,QAAA,uBAAuB,GAAG;IACrC,QAAQ,EAAE,UAAU;CACZ,CAAC;AAEX;;;;;GAKG;AACH,SAAgB,oBAAoB,CAClC,KAAW;IAEX,IACE,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QACpB,CAAC,CAAC,+BAAuB,CAAC,QAAQ,IAAI,KAAK,CAAC,EAC5C,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,QAAQ,GAAG,KAAK,CAAC,+BAAuB,CAAC,QAAQ,CAAC,CAAC;IAEzD,IACE,OAAO,QAAQ,KAAK,QAAQ;QAC5B,QAAQ,KAAK,IAAI;QACjB,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EACvB,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,2DAA2D;IAC3D,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC1C,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,IAAA,4BAAoB,EAAC,UAAU,CAAC,CAAC,CAAC;AAC7E,CAAC;AAzBD,oDAyBC;AAED;;;;;;;GAOG;AACH,SAAgB,cAAc,CAC5B,gBAA8C,EAC9C,iBAAgC;IAEhC,MAAM,cAAc,GAAG,gBAAgB,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC,IAAI,CACrE,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,EAAE,EAAE;QACzB,OAAO,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACvD,CAAC,CACF,CAAC;IAEF,KAAK,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,cAAc,EAAE,CAAC;QAC7C,IAAI,gBAAgB,CAAC,iBAAiB,EAAE,OAAO,CAAC,EAAE,CAAC;YACjD,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAjBD,wCAiBC;AAED;;;;;;;GAOG;AACH,SAAS,gBAAgB,CACvB,cAA6B,EAC7B,eAA8B;IAE9B,OAAO,CACL,cAAc,KAAK,eAAe;QAClC,IAAA,iBAAS,EAAC,cAAc,EAAE,eAAe,CAAC,CAC3C,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,SAAS,gBAAgB,CACvB,KAAY;IAEZ,2EAA2E;IAC3E,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAwC,CAAC;AACtE,CAAC","sourcesContent":["import { gtVersion, isValidSemVerVersion } from '@metamask/utils';\nimport type { Json, SemVerVersion } from '@metamask/utils';\n\nimport type { MultiVersionFeatureFlagValue } from '../remote-feature-flag-controller-types';\n\n/**\n * Constants for MultiVersionFeatureFlagValue property names\n * to ensure consistency across validation, type checking, and other usage.\n */\nexport const MULTI_VERSION_FLAG_KEYS = {\n VERSIONS: 'versions',\n} as const;\n\n/**\n * Checks if a feature flag value is a multi-version gated flag (contains versions object with valid SemVer keys).\n *\n * @param value - The feature flag value to check\n * @returns true if the value is a multi-version feature flag with valid SemVer version keys, false otherwise\n */\nexport function isVersionFeatureFlag(\n value: Json,\n): value is MultiVersionFeatureFlagValue {\n if (\n typeof value !== 'object' ||\n value === null ||\n Array.isArray(value) ||\n !(MULTI_VERSION_FLAG_KEYS.VERSIONS in value)\n ) {\n return false;\n }\n\n const versions = value[MULTI_VERSION_FLAG_KEYS.VERSIONS];\n\n if (\n typeof versions !== 'object' ||\n versions === null ||\n Array.isArray(versions)\n ) {\n return false;\n }\n\n // Validate that all version keys are valid SemVer versions\n const versionKeys = Object.keys(versions);\n return versionKeys.every((versionKey) => isValidSemVerVersion(versionKey));\n}\n\n/**\n * Selects the appropriate version value from a multi-version feature flag based on current app version.\n * Returns the value from the highest version that the app version meets or exceeds.\n *\n * @param multiVersionFlag - The multi-version feature flag\n * @param currentAppVersion - The current application version\n * @returns The selected version value, or null if no version requirements are met\n */\nexport function getVersionData(\n multiVersionFlag: MultiVersionFeatureFlagValue,\n currentAppVersion: SemVerVersion,\n): Json | null {\n const sortedVersions = getObjectEntries(multiVersionFlag.versions).sort(\n ([versionA], [versionB]) => {\n return isVersionAtLeast(versionA, versionB) ? -1 : 1;\n },\n );\n\n for (const [version, data] of sortedVersions) {\n if (isVersionAtLeast(currentAppVersion, version)) {\n return data;\n }\n }\n\n return null;\n}\n\n/**\n * Compares two semantic version strings.\n * Both versions are expected to be valid SemVer format (e.g., \"13.9.5\").\n *\n * @param currentVersion - The current version (e.g., \"13.9.5\")\n * @param requiredVersion - The required minimum version (e.g., \"13.10.0\")\n * @returns true if currentVersion >= requiredVersion, false otherwise\n */\nfunction isVersionAtLeast(\n currentVersion: SemVerVersion,\n requiredVersion: SemVerVersion,\n): boolean {\n return (\n currentVersion === requiredVersion ||\n gtVersion(currentVersion, requiredVersion)\n );\n}\n\n/**\n * A utility function that calls `Object.entries` while preserving the index type.\n *\n * @param input - The object to get the entries of.\n * @returns - The object entries.\n */\nfunction getObjectEntries<Input extends Record<string, unknown>>(\n input: Input,\n): [keyof Input, Input[keyof Input]][] {\n // Use cast to preserve index type. Object.entries always widens to string.\n return Object.entries(input) as [keyof Input, Input[keyof Input]][];\n}\n"]}
@@ -0,0 +1,26 @@
1
+ import type { Json, SemVerVersion } from "@metamask/utils";
2
+ import type { MultiVersionFeatureFlagValue } from "../remote-feature-flag-controller-types.cjs";
3
+ /**
4
+ * Constants for MultiVersionFeatureFlagValue property names
5
+ * to ensure consistency across validation, type checking, and other usage.
6
+ */
7
+ export declare const MULTI_VERSION_FLAG_KEYS: {
8
+ readonly VERSIONS: "versions";
9
+ };
10
+ /**
11
+ * Checks if a feature flag value is a multi-version gated flag (contains versions object with valid SemVer keys).
12
+ *
13
+ * @param value - The feature flag value to check
14
+ * @returns true if the value is a multi-version feature flag with valid SemVer version keys, false otherwise
15
+ */
16
+ export declare function isVersionFeatureFlag(value: Json): value is MultiVersionFeatureFlagValue;
17
+ /**
18
+ * Selects the appropriate version value from a multi-version feature flag based on current app version.
19
+ * Returns the value from the highest version that the app version meets or exceeds.
20
+ *
21
+ * @param multiVersionFlag - The multi-version feature flag
22
+ * @param currentAppVersion - The current application version
23
+ * @returns The selected version value, or null if no version requirements are met
24
+ */
25
+ export declare function getVersionData(multiVersionFlag: MultiVersionFeatureFlagValue, currentAppVersion: SemVerVersion): Json | null;
26
+ //# sourceMappingURL=version.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.d.cts","sourceRoot":"","sources":["../../src/utils/version.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,wBAAwB;AAE3D,OAAO,KAAK,EAAE,4BAA4B,EAAE,oDAAgD;AAE5F;;;GAGG;AACH,eAAO,MAAM,uBAAuB;;CAE1B,CAAC;AAEX;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,IAAI,GACV,KAAK,IAAI,4BAA4B,CAuBvC;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,gBAAgB,EAAE,4BAA4B,EAC9C,iBAAiB,EAAE,aAAa,GAC/B,IAAI,GAAG,IAAI,CAcb"}
@@ -0,0 +1,26 @@
1
+ import type { Json, SemVerVersion } from "@metamask/utils";
2
+ import type { MultiVersionFeatureFlagValue } from "../remote-feature-flag-controller-types.mjs";
3
+ /**
4
+ * Constants for MultiVersionFeatureFlagValue property names
5
+ * to ensure consistency across validation, type checking, and other usage.
6
+ */
7
+ export declare const MULTI_VERSION_FLAG_KEYS: {
8
+ readonly VERSIONS: "versions";
9
+ };
10
+ /**
11
+ * Checks if a feature flag value is a multi-version gated flag (contains versions object with valid SemVer keys).
12
+ *
13
+ * @param value - The feature flag value to check
14
+ * @returns true if the value is a multi-version feature flag with valid SemVer version keys, false otherwise
15
+ */
16
+ export declare function isVersionFeatureFlag(value: Json): value is MultiVersionFeatureFlagValue;
17
+ /**
18
+ * Selects the appropriate version value from a multi-version feature flag based on current app version.
19
+ * Returns the value from the highest version that the app version meets or exceeds.
20
+ *
21
+ * @param multiVersionFlag - The multi-version feature flag
22
+ * @param currentAppVersion - The current application version
23
+ * @returns The selected version value, or null if no version requirements are met
24
+ */
25
+ export declare function getVersionData(multiVersionFlag: MultiVersionFeatureFlagValue, currentAppVersion: SemVerVersion): Json | null;
26
+ //# sourceMappingURL=version.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.d.mts","sourceRoot":"","sources":["../../src/utils/version.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,wBAAwB;AAE3D,OAAO,KAAK,EAAE,4BAA4B,EAAE,oDAAgD;AAE5F;;;GAGG;AACH,eAAO,MAAM,uBAAuB;;CAE1B,CAAC;AAEX;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,IAAI,GACV,KAAK,IAAI,4BAA4B,CAuBvC;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,gBAAgB,EAAE,4BAA4B,EAC9C,iBAAiB,EAAE,aAAa,GAC/B,IAAI,GAAG,IAAI,CAcb"}
@@ -0,0 +1,73 @@
1
+ import { gtVersion, isValidSemVerVersion } from "@metamask/utils";
2
+ /**
3
+ * Constants for MultiVersionFeatureFlagValue property names
4
+ * to ensure consistency across validation, type checking, and other usage.
5
+ */
6
+ export const MULTI_VERSION_FLAG_KEYS = {
7
+ VERSIONS: 'versions',
8
+ };
9
+ /**
10
+ * Checks if a feature flag value is a multi-version gated flag (contains versions object with valid SemVer keys).
11
+ *
12
+ * @param value - The feature flag value to check
13
+ * @returns true if the value is a multi-version feature flag with valid SemVer version keys, false otherwise
14
+ */
15
+ export function isVersionFeatureFlag(value) {
16
+ if (typeof value !== 'object' ||
17
+ value === null ||
18
+ Array.isArray(value) ||
19
+ !(MULTI_VERSION_FLAG_KEYS.VERSIONS in value)) {
20
+ return false;
21
+ }
22
+ const versions = value[MULTI_VERSION_FLAG_KEYS.VERSIONS];
23
+ if (typeof versions !== 'object' ||
24
+ versions === null ||
25
+ Array.isArray(versions)) {
26
+ return false;
27
+ }
28
+ // Validate that all version keys are valid SemVer versions
29
+ const versionKeys = Object.keys(versions);
30
+ return versionKeys.every((versionKey) => isValidSemVerVersion(versionKey));
31
+ }
32
+ /**
33
+ * Selects the appropriate version value from a multi-version feature flag based on current app version.
34
+ * Returns the value from the highest version that the app version meets or exceeds.
35
+ *
36
+ * @param multiVersionFlag - The multi-version feature flag
37
+ * @param currentAppVersion - The current application version
38
+ * @returns The selected version value, or null if no version requirements are met
39
+ */
40
+ export function getVersionData(multiVersionFlag, currentAppVersion) {
41
+ const sortedVersions = getObjectEntries(multiVersionFlag.versions).sort(([versionA], [versionB]) => {
42
+ return isVersionAtLeast(versionA, versionB) ? -1 : 1;
43
+ });
44
+ for (const [version, data] of sortedVersions) {
45
+ if (isVersionAtLeast(currentAppVersion, version)) {
46
+ return data;
47
+ }
48
+ }
49
+ return null;
50
+ }
51
+ /**
52
+ * Compares two semantic version strings.
53
+ * Both versions are expected to be valid SemVer format (e.g., "13.9.5").
54
+ *
55
+ * @param currentVersion - The current version (e.g., "13.9.5")
56
+ * @param requiredVersion - The required minimum version (e.g., "13.10.0")
57
+ * @returns true if currentVersion >= requiredVersion, false otherwise
58
+ */
59
+ function isVersionAtLeast(currentVersion, requiredVersion) {
60
+ return (currentVersion === requiredVersion ||
61
+ gtVersion(currentVersion, requiredVersion));
62
+ }
63
+ /**
64
+ * A utility function that calls `Object.entries` while preserving the index type.
65
+ *
66
+ * @param input - The object to get the entries of.
67
+ * @returns - The object entries.
68
+ */
69
+ function getObjectEntries(input) {
70
+ // Use cast to preserve index type. Object.entries always widens to string.
71
+ return Object.entries(input);
72
+ }
73
+ //# sourceMappingURL=version.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.mjs","sourceRoot":"","sources":["../../src/utils/version.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,oBAAoB,EAAE,wBAAwB;AAKlE;;;GAGG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG;IACrC,QAAQ,EAAE,UAAU;CACZ,CAAC;AAEX;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAClC,KAAW;IAEX,IACE,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QACpB,CAAC,CAAC,uBAAuB,CAAC,QAAQ,IAAI,KAAK,CAAC,EAC5C,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,QAAQ,GAAG,KAAK,CAAC,uBAAuB,CAAC,QAAQ,CAAC,CAAC;IAEzD,IACE,OAAO,QAAQ,KAAK,QAAQ;QAC5B,QAAQ,KAAK,IAAI;QACjB,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EACvB,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,2DAA2D;IAC3D,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC1C,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,oBAAoB,CAAC,UAAU,CAAC,CAAC,CAAC;AAC7E,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAC5B,gBAA8C,EAC9C,iBAAgC;IAEhC,MAAM,cAAc,GAAG,gBAAgB,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC,IAAI,CACrE,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,EAAE,EAAE;QACzB,OAAO,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACvD,CAAC,CACF,CAAC;IAEF,KAAK,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,cAAc,EAAE,CAAC;QAC7C,IAAI,gBAAgB,CAAC,iBAAiB,EAAE,OAAO,CAAC,EAAE,CAAC;YACjD,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,gBAAgB,CACvB,cAA6B,EAC7B,eAA8B;IAE9B,OAAO,CACL,cAAc,KAAK,eAAe;QAClC,SAAS,CAAC,cAAc,EAAE,eAAe,CAAC,CAC3C,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,SAAS,gBAAgB,CACvB,KAAY;IAEZ,2EAA2E;IAC3E,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAwC,CAAC;AACtE,CAAC","sourcesContent":["import { gtVersion, isValidSemVerVersion } from '@metamask/utils';\nimport type { Json, SemVerVersion } from '@metamask/utils';\n\nimport type { MultiVersionFeatureFlagValue } from '../remote-feature-flag-controller-types';\n\n/**\n * Constants for MultiVersionFeatureFlagValue property names\n * to ensure consistency across validation, type checking, and other usage.\n */\nexport const MULTI_VERSION_FLAG_KEYS = {\n VERSIONS: 'versions',\n} as const;\n\n/**\n * Checks if a feature flag value is a multi-version gated flag (contains versions object with valid SemVer keys).\n *\n * @param value - The feature flag value to check\n * @returns true if the value is a multi-version feature flag with valid SemVer version keys, false otherwise\n */\nexport function isVersionFeatureFlag(\n value: Json,\n): value is MultiVersionFeatureFlagValue {\n if (\n typeof value !== 'object' ||\n value === null ||\n Array.isArray(value) ||\n !(MULTI_VERSION_FLAG_KEYS.VERSIONS in value)\n ) {\n return false;\n }\n\n const versions = value[MULTI_VERSION_FLAG_KEYS.VERSIONS];\n\n if (\n typeof versions !== 'object' ||\n versions === null ||\n Array.isArray(versions)\n ) {\n return false;\n }\n\n // Validate that all version keys are valid SemVer versions\n const versionKeys = Object.keys(versions);\n return versionKeys.every((versionKey) => isValidSemVerVersion(versionKey));\n}\n\n/**\n * Selects the appropriate version value from a multi-version feature flag based on current app version.\n * Returns the value from the highest version that the app version meets or exceeds.\n *\n * @param multiVersionFlag - The multi-version feature flag\n * @param currentAppVersion - The current application version\n * @returns The selected version value, or null if no version requirements are met\n */\nexport function getVersionData(\n multiVersionFlag: MultiVersionFeatureFlagValue,\n currentAppVersion: SemVerVersion,\n): Json | null {\n const sortedVersions = getObjectEntries(multiVersionFlag.versions).sort(\n ([versionA], [versionB]) => {\n return isVersionAtLeast(versionA, versionB) ? -1 : 1;\n },\n );\n\n for (const [version, data] of sortedVersions) {\n if (isVersionAtLeast(currentAppVersion, version)) {\n return data;\n }\n }\n\n return null;\n}\n\n/**\n * Compares two semantic version strings.\n * Both versions are expected to be valid SemVer format (e.g., \"13.9.5\").\n *\n * @param currentVersion - The current version (e.g., \"13.9.5\")\n * @param requiredVersion - The required minimum version (e.g., \"13.10.0\")\n * @returns true if currentVersion >= requiredVersion, false otherwise\n */\nfunction isVersionAtLeast(\n currentVersion: SemVerVersion,\n requiredVersion: SemVerVersion,\n): boolean {\n return (\n currentVersion === requiredVersion ||\n gtVersion(currentVersion, requiredVersion)\n );\n}\n\n/**\n * A utility function that calls `Object.entries` while preserving the index type.\n *\n * @param input - The object to get the entries of.\n * @returns - The object entries.\n */\nfunction getObjectEntries<Input extends Record<string, unknown>>(\n input: Input,\n): [keyof Input, Input[keyof Input]][] {\n // Use cast to preserve index type. Object.entries always widens to string.\n return Object.entries(input) as [keyof Input, Input[keyof Input]][];\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metamask-previews/remote-feature-flag-controller",
3
- "version": "2.0.1-preview-159e76e4",
3
+ "version": "2.0.1-preview-7cfbb337",
4
4
  "description": "The RemoteFeatureFlagController manages the retrieval and caching of remote feature flags",
5
5
  "keywords": [
6
6
  "MetaMask",