@metamask-previews/remote-feature-flag-controller 6.1.1-preview-abca9bcea → 7.0.0-preview-0a30e47

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.
Files changed (96) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/dist/client-config-api-service/{abstract-client-config-api-service.d.mts → abstract-client-config-api-service.d.ts} +3 -3
  3. package/dist/client-config-api-service/abstract-client-config-api-service.d.ts.map +1 -0
  4. package/dist/client-config-api-service/abstract-client-config-api-service.js +2 -0
  5. package/dist/client-config-api-service/abstract-client-config-api-service.js.map +1 -0
  6. package/dist/client-config-api-service/{client-config-api-service.d.cts → client-config-api-service.d.ts} +5 -5
  7. package/dist/client-config-api-service/client-config-api-service.d.ts.map +1 -0
  8. package/dist/client-config-api-service/client-config-api-service.js +86 -0
  9. package/dist/client-config-api-service/client-config-api-service.js.map +1 -0
  10. package/dist/{constants.d.cts → constants.d.ts} +1 -1
  11. package/dist/constants.d.ts.map +1 -0
  12. package/dist/{constants.mjs → constants.js} +1 -1
  13. package/dist/constants.js.map +1 -0
  14. package/dist/index.d.ts +8 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +5 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/{remote-feature-flag-controller-method-action-types.d.cts → remote-feature-flag-controller-method-action-types.d.ts} +2 -2
  19. package/dist/remote-feature-flag-controller-method-action-types.d.ts.map +1 -0
  20. package/dist/{remote-feature-flag-controller-method-action-types.mjs → remote-feature-flag-controller-method-action-types.js} +1 -1
  21. package/dist/remote-feature-flag-controller-method-action-types.js.map +1 -0
  22. package/dist/{remote-feature-flag-controller-types.d.mts → remote-feature-flag-controller-types.d.ts} +2 -2
  23. package/dist/remote-feature-flag-controller-types.d.ts.map +1 -0
  24. package/dist/{remote-feature-flag-controller-types.mjs → remote-feature-flag-controller-types.js} +1 -1
  25. package/dist/remote-feature-flag-controller-types.js.map +1 -0
  26. package/dist/{remote-feature-flag-controller.d.cts → remote-feature-flag-controller.d.ts} +8 -8
  27. package/dist/remote-feature-flag-controller.d.ts.map +1 -0
  28. package/dist/remote-feature-flag-controller.js +470 -0
  29. package/dist/{remote-feature-flag-controller.cjs.map → remote-feature-flag-controller.js.map} +1 -1
  30. package/dist/utils/{user-segmentation-utils.d.cts → user-segmentation-utils.d.ts} +3 -3
  31. package/dist/utils/user-segmentation-utils.d.ts.map +1 -0
  32. package/dist/utils/{user-segmentation-utils.mjs → user-segmentation-utils.js} +5 -5
  33. package/dist/utils/user-segmentation-utils.js.map +1 -0
  34. package/dist/utils/{version.d.cts → version.d.ts} +4 -4
  35. package/dist/utils/version.d.ts.map +1 -0
  36. package/dist/utils/{version.mjs → version.js} +2 -2
  37. package/dist/utils/version.js.map +1 -0
  38. package/package.json +16 -20
  39. package/dist/client-config-api-service/abstract-client-config-api-service.cjs +0 -3
  40. package/dist/client-config-api-service/abstract-client-config-api-service.cjs.map +0 -1
  41. package/dist/client-config-api-service/abstract-client-config-api-service.d.cts +0 -17
  42. package/dist/client-config-api-service/abstract-client-config-api-service.d.cts.map +0 -1
  43. package/dist/client-config-api-service/abstract-client-config-api-service.d.mts.map +0 -1
  44. package/dist/client-config-api-service/abstract-client-config-api-service.mjs +0 -2
  45. package/dist/client-config-api-service/abstract-client-config-api-service.mjs.map +0 -1
  46. package/dist/client-config-api-service/client-config-api-service.cjs +0 -94
  47. package/dist/client-config-api-service/client-config-api-service.cjs.map +0 -1
  48. package/dist/client-config-api-service/client-config-api-service.d.cts.map +0 -1
  49. package/dist/client-config-api-service/client-config-api-service.d.mts +0 -108
  50. package/dist/client-config-api-service/client-config-api-service.d.mts.map +0 -1
  51. package/dist/client-config-api-service/client-config-api-service.mjs +0 -90
  52. package/dist/client-config-api-service/client-config-api-service.mjs.map +0 -1
  53. package/dist/constants.cjs +0 -8
  54. package/dist/constants.cjs.map +0 -1
  55. package/dist/constants.d.cts.map +0 -1
  56. package/dist/constants.d.mts +0 -5
  57. package/dist/constants.d.mts.map +0 -1
  58. package/dist/constants.mjs.map +0 -1
  59. package/dist/index.cjs +0 -15
  60. package/dist/index.cjs.map +0 -1
  61. package/dist/index.d.cts +0 -8
  62. package/dist/index.d.cts.map +0 -1
  63. package/dist/index.d.mts +0 -8
  64. package/dist/index.d.mts.map +0 -1
  65. package/dist/index.mjs +0 -5
  66. package/dist/index.mjs.map +0 -1
  67. package/dist/remote-feature-flag-controller-method-action-types.cjs +0 -7
  68. package/dist/remote-feature-flag-controller-method-action-types.cjs.map +0 -1
  69. package/dist/remote-feature-flag-controller-method-action-types.d.cts.map +0 -1
  70. package/dist/remote-feature-flag-controller-method-action-types.d.mts +0 -62
  71. package/dist/remote-feature-flag-controller-method-action-types.d.mts.map +0 -1
  72. package/dist/remote-feature-flag-controller-method-action-types.mjs.map +0 -1
  73. package/dist/remote-feature-flag-controller-types.cjs +0 -32
  74. package/dist/remote-feature-flag-controller-types.cjs.map +0 -1
  75. package/dist/remote-feature-flag-controller-types.d.cts +0 -64
  76. package/dist/remote-feature-flag-controller-types.d.cts.map +0 -1
  77. package/dist/remote-feature-flag-controller-types.d.mts.map +0 -1
  78. package/dist/remote-feature-flag-controller-types.mjs.map +0 -1
  79. package/dist/remote-feature-flag-controller.cjs +0 -453
  80. package/dist/remote-feature-flag-controller.d.cts.map +0 -1
  81. package/dist/remote-feature-flag-controller.d.mts +0 -113
  82. package/dist/remote-feature-flag-controller.d.mts.map +0 -1
  83. package/dist/remote-feature-flag-controller.mjs +0 -448
  84. package/dist/remote-feature-flag-controller.mjs.map +0 -1
  85. package/dist/utils/user-segmentation-utils.cjs +0 -107
  86. package/dist/utils/user-segmentation-utils.cjs.map +0 -1
  87. package/dist/utils/user-segmentation-utils.d.cts.map +0 -1
  88. package/dist/utils/user-segmentation-utils.d.mts +0 -34
  89. package/dist/utils/user-segmentation-utils.d.mts.map +0 -1
  90. package/dist/utils/user-segmentation-utils.mjs.map +0 -1
  91. package/dist/utils/version.cjs +0 -78
  92. package/dist/utils/version.cjs.map +0 -1
  93. package/dist/utils/version.d.cts.map +0 -1
  94. package/dist/utils/version.d.mts +0 -26
  95. package/dist/utils/version.d.mts.map +0 -1
  96. package/dist/utils/version.mjs.map +0 -1
@@ -0,0 +1,470 @@
1
+ import { BaseController, } from '@metamask/base-controller';
2
+ import { hasProperty, isValidSemVerVersion } from '@metamask/utils';
3
+ import { calculateThresholdForFlag, isFeatureFlagWithScopeValue, } from './utils/user-segmentation-utils.js';
4
+ import { isVersionFeatureFlag, getVersionData } from './utils/version.js';
5
+ // === GENERAL ===
6
+ export const controllerName = 'RemoteFeatureFlagController';
7
+ export const DEFAULT_CACHE_DURATION = 24 * 60 * 60 * 1000; // 1 day
8
+ const remoteFeatureFlagControllerMetadata = {
9
+ remoteFeatureFlags: {
10
+ includeInStateLogs: true,
11
+ persist: true,
12
+ includeInDebugSnapshot: true,
13
+ usedInUi: true,
14
+ },
15
+ localOverrides: {
16
+ includeInStateLogs: true,
17
+ persist: true,
18
+ includeInDebugSnapshot: true,
19
+ usedInUi: true,
20
+ },
21
+ rawRemoteFeatureFlags: {
22
+ includeInStateLogs: true,
23
+ persist: true,
24
+ includeInDebugSnapshot: true,
25
+ usedInUi: false,
26
+ },
27
+ cacheTimestamp: {
28
+ includeInStateLogs: true,
29
+ persist: true,
30
+ includeInDebugSnapshot: true,
31
+ usedInUi: false,
32
+ },
33
+ thresholdCache: {
34
+ includeInStateLogs: false,
35
+ persist: true,
36
+ includeInDebugSnapshot: false,
37
+ usedInUi: false,
38
+ },
39
+ featureFlagThresholdGroups: {
40
+ includeInStateLogs: true,
41
+ persist: true,
42
+ includeInDebugSnapshot: true,
43
+ usedInUi: false,
44
+ },
45
+ };
46
+ // === MESSENGER ===
47
+ const MESSENGER_EXPOSED_METHODS = [
48
+ 'clearAllFlagOverrides',
49
+ 'disable',
50
+ 'enable',
51
+ 'removeFlagOverride',
52
+ 'setFlagOverride',
53
+ 'updateRemoteFeatureFlags',
54
+ ];
55
+ /**
56
+ * Returns the default state for the RemoteFeatureFlagController.
57
+ *
58
+ * @returns The default controller state.
59
+ */
60
+ export function getDefaultRemoteFeatureFlagControllerState() {
61
+ return {
62
+ remoteFeatureFlags: {},
63
+ localOverrides: {},
64
+ rawRemoteFeatureFlags: {},
65
+ cacheTimestamp: 0,
66
+ };
67
+ }
68
+ /**
69
+ * Searches threshold entries for an explicit MetaMetrics ID match.
70
+ * Returns the first entry whose `metaMetricsIds` list contains the given
71
+ * normalized ID. Entries with malformed `metaMetricsIds` (not an array) are
72
+ * skipped without throwing.
73
+ *
74
+ * @param entries - The array of raw threshold entries for a feature flag.
75
+ * @param normalizedId - The current user's MetaMetrics ID, already trimmed and
76
+ * lower-cased.
77
+ * @returns The first matching entry, or `undefined` if none match.
78
+ */
79
+ function findExplicitIdMatch(entries, normalizedId) {
80
+ for (const entry of entries) {
81
+ if (!isFeatureFlagWithScopeValue(entry)) {
82
+ continue;
83
+ }
84
+ const { metaMetricsIds } = entry;
85
+ if (!Array.isArray(metaMetricsIds)) {
86
+ continue;
87
+ }
88
+ const hasMatch = metaMetricsIds.some((id) => typeof id === 'string' && id.trim().toLowerCase() === normalizedId);
89
+ if (hasMatch) {
90
+ return entry;
91
+ }
92
+ }
93
+ return undefined;
94
+ }
95
+ /**
96
+ * The RemoteFeatureFlagController manages the retrieval and caching of remote feature flags.
97
+ * It fetches feature flags from a remote API, caches them, and provides methods to access
98
+ * and manage these flags. The controller ensures that feature flags are refreshed based on
99
+ * a specified interval and handles cases where the controller is disabled or the network is unavailable.
100
+ */
101
+ export class RemoteFeatureFlagController extends BaseController {
102
+ #fetchInterval;
103
+ #disabled;
104
+ #clientConfigApiService;
105
+ #inProgressFlagUpdate;
106
+ #getMetaMetricsId;
107
+ #getCanonicalProfileId;
108
+ #metaMetricsFlags;
109
+ #clientVersion;
110
+ #defaultFeatureFlags;
111
+ #processedRemoteFeatureFlags;
112
+ /**
113
+ * Constructs a new RemoteFeatureFlagController instance.
114
+ *
115
+ * @param options - The controller options.
116
+ * @param options.messenger - The messenger used for communication.
117
+ * @param options.state - The initial state of the controller.
118
+ * @param options.clientConfigApiService - The service instance to fetch remote feature flags.
119
+ * @param options.fetchInterval - The interval in milliseconds before cached flags expire. Defaults to 1 day.
120
+ * @param options.disabled - Determines if the controller should be disabled initially. Defaults to false.
121
+ * @param options.getMetaMetricsId - Returns metaMetricsId.
122
+ * @param options.getCanonicalProfileId - Returns the canonical profile identifier used for threshold flags by default. Must return an empty string, rather than throwing, when the identifier is unavailable.
123
+ * @param options.metaMetricsFlags - Names of feature flags that should use MetaMetrics ID for threshold assignment.
124
+ * @param options.clientVersion - The current client version for version-based feature flag filtering. Must be a valid 3-part SemVer version string.
125
+ * @param options.prevClientVersion - The previous client version for feature flag cache invalidation.
126
+ * @param options.defaultFeatureFlags - Client-side default feature flags used as the lowest-precedence layer under processed remote flags and local overrides. Not persisted.
127
+ */
128
+ constructor({ messenger, state, clientConfigApiService, fetchInterval = DEFAULT_CACHE_DURATION, disabled = false, getMetaMetricsId, getCanonicalProfileId, metaMetricsFlags = [], clientVersion, prevClientVersion, defaultFeatureFlags = {}, }) {
129
+ if (!isValidSemVerVersion(clientVersion)) {
130
+ throw new Error(`Invalid clientVersion: "${clientVersion}". Must be a valid 3-part SemVer version string`);
131
+ }
132
+ const initialState = {
133
+ ...getDefaultRemoteFeatureFlagControllerState(),
134
+ ...state,
135
+ };
136
+ const hasClientVersionChanged = isValidSemVerVersion(prevClientVersion) &&
137
+ prevClientVersion !== clientVersion;
138
+ super({
139
+ name: controllerName,
140
+ metadata: remoteFeatureFlagControllerMetadata,
141
+ messenger,
142
+ state: {
143
+ ...initialState,
144
+ cacheTimestamp: hasClientVersionChanged
145
+ ? 0
146
+ : initialState.cacheTimestamp,
147
+ },
148
+ });
149
+ this.#defaultFeatureFlags = defaultFeatureFlags;
150
+ // Last session's effective flags stand in for the remote layer until
151
+ // `init` re-derives it from the persisted raw flags, or a fetch replaces
152
+ // it. Overrides are layered on top rather than subtracted out, so a remote
153
+ // flag that happens to share an override's value is not lost.
154
+ this.#processedRemoteFeatureFlags = initialState.remoteFeatureFlags;
155
+ this.#fetchInterval = fetchInterval;
156
+ this.#disabled = disabled;
157
+ this.#clientConfigApiService = clientConfigApiService;
158
+ this.#getMetaMetricsId = getMetaMetricsId;
159
+ this.#getCanonicalProfileId = getCanonicalProfileId;
160
+ this.#metaMetricsFlags = new Set(metaMetricsFlags);
161
+ this.#clientVersion = clientVersion;
162
+ this.messenger.registerMethodActionHandlers(this, MESSENGER_EXPOSED_METHODS);
163
+ }
164
+ /**
165
+ * Computes effective feature flags with precedence:
166
+ * defaults < processed remote < local overrides.
167
+ *
168
+ * @param options - The layers to merge. Each defaults to the current layer.
169
+ * @param options.processedRemoteFeatureFlags - The processed remote feature
170
+ * flags. Defaults to the currently resolved remote layer.
171
+ * @param options.localOverrides - Local overrides. Defaults to current state
172
+ * overrides.
173
+ * @returns The effective feature flags.
174
+ */
175
+ #getEffectiveFeatureFlags({ processedRemoteFeatureFlags = this.#processedRemoteFeatureFlags, localOverrides = this.state.localOverrides, } = {}) {
176
+ return {
177
+ ...this.#defaultFeatureFlags,
178
+ ...processedRemoteFeatureFlags,
179
+ ...localOverrides,
180
+ };
181
+ }
182
+ /**
183
+ * Checks if the cached feature flags are expired based on the fetch interval.
184
+ *
185
+ * @returns Whether the cache is expired (`true`) or still valid (`false`).
186
+ */
187
+ #isCacheExpired() {
188
+ return Date.now() - this.state.cacheTimestamp > this.#fetchInterval;
189
+ }
190
+ /**
191
+ * Retrieves the remote feature flags, fetching from the API if necessary.
192
+ * Uses caching to prevent redundant API calls and handles concurrent fetches.
193
+ *
194
+ * @param force - When `true`, fetch even if the cache has not expired.
195
+ * Defaults to `false`. Has no effect when the controller is disabled.
196
+ * @returns A promise that resolves to the current set of feature flags.
197
+ */
198
+ async updateRemoteFeatureFlags(force = false) {
199
+ if (this.#disabled || (!force && !this.#isCacheExpired())) {
200
+ return;
201
+ }
202
+ let serverData;
203
+ if (this.#inProgressFlagUpdate) {
204
+ await this.#inProgressFlagUpdate;
205
+ return;
206
+ }
207
+ try {
208
+ this.#inProgressFlagUpdate =
209
+ this.#clientConfigApiService.fetchRemoteFeatureFlags();
210
+ serverData = await this.#inProgressFlagUpdate;
211
+ }
212
+ finally {
213
+ this.#inProgressFlagUpdate = undefined;
214
+ }
215
+ await this.#updateCache(serverData.remoteFeatureFlags);
216
+ }
217
+ /**
218
+ * Computes the effective feature flags, re-deriving the remote layer from the
219
+ * raw flags already in state. Threshold selection needs to await a hash and
220
+ * so cannot run in the constructor, which is why this cannot be part of
221
+ * construction. Clients must call this once after constructing the
222
+ * controller.
223
+ *
224
+ * When there are no persisted raw flags, as on a fresh install or for state
225
+ * persisted before raw flags were stored, the previous session's flags stand
226
+ * in for the remote layer so that nothing is lost.
227
+ */
228
+ async init() {
229
+ const { rawRemoteFeatureFlags } = this.state;
230
+ const hasRawRemoteFeatureFlags = rawRemoteFeatureFlags && Object.keys(rawRemoteFeatureFlags).length > 0;
231
+ const resolved = hasRawRemoteFeatureFlags
232
+ ? await this.#processRemoteFeatureFlags(rawRemoteFeatureFlags)
233
+ : undefined;
234
+ this.#processedRemoteFeatureFlags =
235
+ resolved?.processedFlags ?? this.state.remoteFeatureFlags;
236
+ this.update(() => {
237
+ return {
238
+ ...this.state,
239
+ remoteFeatureFlags: this.#getEffectiveFeatureFlags(),
240
+ ...(resolved && {
241
+ thresholdCache: resolved.thresholdCache,
242
+ featureFlagThresholdGroups: resolved.featureFlagThresholdGroups,
243
+ }),
244
+ };
245
+ });
246
+ }
247
+ /**
248
+ * Updates the controller's state with new feature flags and resets the cache timestamp.
249
+ *
250
+ * @param remoteFeatureFlags - The new feature flags to cache.
251
+ */
252
+ async #updateCache(remoteFeatureFlags) {
253
+ const resolved = await this.#processRemoteFeatureFlags(remoteFeatureFlags);
254
+ this.#processedRemoteFeatureFlags = resolved.processedFlags;
255
+ // Single state update with all changes batched together
256
+ this.update(() => {
257
+ return {
258
+ ...this.state,
259
+ remoteFeatureFlags: this.#getEffectiveFeatureFlags({
260
+ processedRemoteFeatureFlags: resolved.processedFlags,
261
+ }),
262
+ rawRemoteFeatureFlags: remoteFeatureFlags,
263
+ cacheTimestamp: Date.now(),
264
+ thresholdCache: resolved.thresholdCache,
265
+ featureFlagThresholdGroups: resolved.featureFlagThresholdGroups,
266
+ };
267
+ });
268
+ }
269
+ /**
270
+ * Processes a version-based feature flag to get the appropriate value for the current client version.
271
+ *
272
+ * @param flagValue - The feature flag value to process
273
+ * @returns The processed value, or null if no version qualifies (skip this flag)
274
+ */
275
+ #processVersionBasedFlag(flagValue) {
276
+ if (!isVersionFeatureFlag(flagValue)) {
277
+ return flagValue;
278
+ }
279
+ return getVersionData(flagValue, this.#clientVersion);
280
+ }
281
+ /**
282
+ * Selects the identifier used to bucket a threshold flag. Flags named in
283
+ * `metaMetricsFlags` segment by MetaMetrics ID; all others segment by
284
+ * canonical profile ID.
285
+ *
286
+ * @param featureFlagName - The name of the feature flag being processed.
287
+ * @returns The segmentation identifier, which may be empty when unavailable.
288
+ */
289
+ #getSegmentationId(featureFlagName) {
290
+ if (this.#metaMetricsFlags.has(featureFlagName)) {
291
+ return this.#getMetaMetricsId();
292
+ }
293
+ return this.#getCanonicalProfileId();
294
+ }
295
+ /**
296
+ * Resolves raw feature flags into the values that apply to this client and
297
+ * user, selecting version and threshold entries and reconciling the
298
+ * threshold cache against the flags the server currently serves.
299
+ *
300
+ * @param remoteFeatureFlags - The unprocessed feature flags.
301
+ * @returns The processed flags, the updated threshold cache, and the
302
+ * selected threshold group names.
303
+ */
304
+ async #processRemoteFeatureFlags(remoteFeatureFlags) {
305
+ const processedFlags = {};
306
+ const metaMetricsId = this.#getMetaMetricsId();
307
+ const thresholdCacheUpdates = {};
308
+ const featureFlagThresholdGroups = {};
309
+ for (const [remoteFeatureFlagName, remoteFeatureFlagValue,] of Object.entries(remoteFeatureFlags)) {
310
+ let processedValue = this.#processVersionBasedFlag(remoteFeatureFlagValue);
311
+ if (processedValue === null) {
312
+ continue;
313
+ }
314
+ if (Array.isArray(processedValue)) {
315
+ // Validate array has valid threshold items before doing expensive crypto operation
316
+ const hasValidThresholds = processedValue.some(isFeatureFlagWithScopeValue);
317
+ if (!hasValidThresholds) {
318
+ // Not a threshold array - preserve as-is
319
+ processedFlags[remoteFeatureFlagName] = processedValue;
320
+ continue;
321
+ }
322
+ // Explicit-ID matching: check before hash-based threshold, bypasses cache
323
+ const normalizedMetaMetricsId = metaMetricsId.trim().toLowerCase();
324
+ const explicitMatch = normalizedMetaMetricsId
325
+ ? findExplicitIdMatch(processedValue, normalizedMetaMetricsId)
326
+ : undefined;
327
+ if (explicitMatch) {
328
+ processedValue = explicitMatch.value;
329
+ if (explicitMatch.name) {
330
+ featureFlagThresholdGroups[remoteFeatureFlagName] =
331
+ explicitMatch.name;
332
+ }
333
+ }
334
+ else {
335
+ const segmentationId = this.#getSegmentationId(remoteFeatureFlagName);
336
+ if (!segmentationId) {
337
+ // The identifier needed to group this flag is unavailable, as it
338
+ // is when `init` runs before whatever backs these getters is
339
+ // ready. Exposing the unresolved threshold array would
340
+ // flip the flag for the end user until the next fetch.
341
+ if (hasProperty(this.#processedRemoteFeatureFlags, remoteFeatureFlagName)) {
342
+ processedFlags[remoteFeatureFlagName] =
343
+ this.#processedRemoteFeatureFlags[remoteFeatureFlagName];
344
+ // Keep the group name alongside the value it was selected with,
345
+ // so consumers segmenting on it do not see a value with no group.
346
+ const previousGroup = this.state.featureFlagThresholdGroups?.[remoteFeatureFlagName];
347
+ if (previousGroup) {
348
+ featureFlagThresholdGroups[remoteFeatureFlagName] =
349
+ previousGroup;
350
+ }
351
+ continue;
352
+ }
353
+ // No previous value to preserve, as on a fresh install.
354
+ processedFlags[remoteFeatureFlagName] = processedValue;
355
+ continue;
356
+ }
357
+ // Fall back to hash-based threshold selection with cache
358
+ const cacheKey = `${segmentationId}:${remoteFeatureFlagName}`;
359
+ let thresholdValue = this.state.thresholdCache?.[cacheKey];
360
+ if (thresholdValue === undefined) {
361
+ thresholdValue = await calculateThresholdForFlag(segmentationId, remoteFeatureFlagName);
362
+ // Collect new threshold for batched state update
363
+ thresholdCacheUpdates[cacheKey] = thresholdValue;
364
+ }
365
+ const threshold = thresholdValue;
366
+ const selectedGroup = processedValue.find((featureFlag) => {
367
+ if (!isFeatureFlagWithScopeValue(featureFlag)) {
368
+ return false;
369
+ }
370
+ return threshold <= featureFlag.scope.value;
371
+ });
372
+ if (selectedGroup) {
373
+ processedValue = selectedGroup.value;
374
+ if (selectedGroup.name) {
375
+ featureFlagThresholdGroups[remoteFeatureFlagName] =
376
+ selectedGroup.name;
377
+ }
378
+ }
379
+ }
380
+ }
381
+ processedFlags[remoteFeatureFlagName] = processedValue;
382
+ }
383
+ const thresholdCache = {
384
+ ...this.state.thresholdCache,
385
+ ...thresholdCacheUpdates,
386
+ };
387
+ // Drop cached thresholds for flags this user is no longer served, under
388
+ // either identifier they may have been bucketed by.
389
+ const canonicalProfileId = this.#getCanonicalProfileId();
390
+ const currentFlagNames = Object.keys(remoteFeatureFlags);
391
+ for (const cacheKey of Object.keys(thresholdCache)) {
392
+ const [cachedSegmentationId, ...cachedFlagNameParts] = cacheKey.split(':');
393
+ const cachedFlagName = cachedFlagNameParts.join(':');
394
+ if ((cachedSegmentationId === metaMetricsId ||
395
+ cachedSegmentationId === canonicalProfileId) &&
396
+ !currentFlagNames.includes(cachedFlagName)) {
397
+ delete thresholdCache[cacheKey];
398
+ }
399
+ }
400
+ return {
401
+ processedFlags,
402
+ thresholdCache,
403
+ featureFlagThresholdGroups,
404
+ };
405
+ }
406
+ /**
407
+ * Enables the controller, allowing it to make network requests.
408
+ */
409
+ enable() {
410
+ this.#disabled = false;
411
+ }
412
+ /**
413
+ * Disables the controller, preventing it from making network requests.
414
+ */
415
+ disable() {
416
+ this.#disabled = true;
417
+ }
418
+ /**
419
+ * Sets a local override for a specific feature flag.
420
+ *
421
+ * @param flagName - The name of the feature flag to override.
422
+ * @param value - The override value for the feature flag.
423
+ */
424
+ setFlagOverride(flagName, value) {
425
+ this.update(() => {
426
+ const localOverrides = {
427
+ ...this.state.localOverrides,
428
+ [flagName]: value,
429
+ };
430
+ return {
431
+ ...this.state,
432
+ localOverrides,
433
+ remoteFeatureFlags: this.#getEffectiveFeatureFlags({ localOverrides }),
434
+ };
435
+ });
436
+ }
437
+ /**
438
+ * Clears the local override for a specific feature flag.
439
+ *
440
+ * @param flagName - The name of the feature flag to clear.
441
+ */
442
+ removeFlagOverride(flagName) {
443
+ const newLocalOverrides = { ...this.state.localOverrides };
444
+ delete newLocalOverrides[flagName];
445
+ this.update(() => {
446
+ return {
447
+ ...this.state,
448
+ localOverrides: newLocalOverrides,
449
+ remoteFeatureFlags: this.#getEffectiveFeatureFlags({
450
+ localOverrides: newLocalOverrides,
451
+ }),
452
+ };
453
+ });
454
+ }
455
+ /**
456
+ * Clears all local feature flag overrides.
457
+ */
458
+ clearAllFlagOverrides() {
459
+ this.update(() => {
460
+ return {
461
+ ...this.state,
462
+ localOverrides: {},
463
+ remoteFeatureFlags: this.#getEffectiveFeatureFlags({
464
+ localOverrides: {},
465
+ }),
466
+ };
467
+ });
468
+ }
469
+ }
470
+ //# sourceMappingURL=remote-feature-flag-controller.js.map
@@ -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 ControllerGetStateAction,\n} from '@metamask/base-controller';\nimport type { ControllerStateChangeEvent } from '@metamask/base-controller';\nimport type { Messenger } from '@metamask/messenger';\nimport { hasProperty, isValidSemVerVersion } from '@metamask/utils';\nimport type { Json, SemVerVersion } from '@metamask/utils';\n\nimport type { AbstractClientConfigApiService } from './client-config-api-service/abstract-client-config-api-service.js';\nimport type { RemoteFeatureFlagControllerMethodActions } from './remote-feature-flag-controller-method-action-types.js';\nimport type {\n FeatureFlags,\n ServiceResponse,\n FeatureFlagScopeValue,\n} from './remote-feature-flag-controller-types.js';\nimport {\n calculateThresholdForFlag,\n isFeatureFlagWithScopeValue,\n} from './utils/user-segmentation-utils.js';\nimport { isVersionFeatureFlag, getVersionData } from './utils/version.js';\n\n// === GENERAL ===\n\nexport const controllerName = 'RemoteFeatureFlagController';\nexport const DEFAULT_CACHE_DURATION = 24 * 60 * 60 * 1000; // 1 day\n\n// === STATE ===\n\nexport type RemoteFeatureFlagControllerState = {\n remoteFeatureFlags: FeatureFlags;\n localOverrides?: FeatureFlags;\n rawRemoteFeatureFlags?: FeatureFlags;\n cacheTimestamp: number;\n thresholdCache?: Record<string, number>;\n featureFlagThresholdGroups?: Record<string, string>;\n};\n\nconst remoteFeatureFlagControllerMetadata = {\n remoteFeatureFlags: {\n includeInStateLogs: true,\n persist: true,\n includeInDebugSnapshot: true,\n usedInUi: true,\n },\n localOverrides: {\n includeInStateLogs: true,\n persist: true,\n includeInDebugSnapshot: true,\n usedInUi: true,\n },\n rawRemoteFeatureFlags: {\n includeInStateLogs: true,\n persist: true,\n includeInDebugSnapshot: true,\n usedInUi: false,\n },\n cacheTimestamp: {\n includeInStateLogs: true,\n persist: true,\n includeInDebugSnapshot: true,\n usedInUi: false,\n },\n thresholdCache: {\n includeInStateLogs: false,\n persist: true,\n includeInDebugSnapshot: false,\n usedInUi: false,\n },\n featureFlagThresholdGroups: {\n includeInStateLogs: true,\n persist: true,\n includeInDebugSnapshot: true,\n usedInUi: false,\n },\n};\n\n// === MESSENGER ===\n\nconst MESSENGER_EXPOSED_METHODS = [\n 'clearAllFlagOverrides',\n 'disable',\n 'enable',\n 'removeFlagOverride',\n 'setFlagOverride',\n 'updateRemoteFeatureFlags',\n] as const;\n\nexport type RemoteFeatureFlagControllerGetStateAction =\n ControllerGetStateAction<\n typeof controllerName,\n RemoteFeatureFlagControllerState\n >;\n\nexport type RemoteFeatureFlagControllerActions =\n | RemoteFeatureFlagControllerGetStateAction\n | RemoteFeatureFlagControllerMethodActions;\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 localOverrides: {},\n rawRemoteFeatureFlags: {},\n cacheTimestamp: 0,\n };\n}\n\n/**\n * Searches threshold entries for an explicit MetaMetrics ID match.\n * Returns the first entry whose `metaMetricsIds` list contains the given\n * normalized ID. Entries with malformed `metaMetricsIds` (not an array) are\n * skipped without throwing.\n *\n * @param entries - The array of raw threshold entries for a feature flag.\n * @param normalizedId - The current user's MetaMetrics ID, already trimmed and\n * lower-cased.\n * @returns The first matching entry, or `undefined` if none match.\n */\nfunction findExplicitIdMatch(\n entries: Json[],\n normalizedId: string,\n): FeatureFlagScopeValue | undefined {\n for (const entry of entries) {\n if (!isFeatureFlagWithScopeValue(entry)) {\n continue;\n }\n const { metaMetricsIds } = entry;\n if (!Array.isArray(metaMetricsIds)) {\n continue;\n }\n const hasMatch = metaMetricsIds.some(\n (id) =>\n typeof id === 'string' && id.trim().toLowerCase() === normalizedId,\n );\n if (hasMatch) {\n return entry;\n }\n }\n return undefined;\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 #getCanonicalProfileId: () => string;\n\n readonly #metaMetricsFlags: ReadonlySet<string>;\n\n readonly #clientVersion: SemVerVersion;\n\n readonly #defaultFeatureFlags: FeatureFlags;\n\n #processedRemoteFeatureFlags: FeatureFlags;\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.getCanonicalProfileId - Returns the canonical profile identifier used for threshold flags by default. Must return an empty string, rather than throwing, when the identifier is unavailable.\n * @param options.metaMetricsFlags - Names of feature flags that should use MetaMetrics ID for threshold assignment.\n * @param options.clientVersion - The current client version for version-based feature flag filtering. Must be a valid 3-part SemVer version string.\n * @param options.prevClientVersion - The previous client version for feature flag cache invalidation.\n * @param options.defaultFeatureFlags - Client-side default feature flags used as the lowest-precedence layer under processed remote flags and local overrides. Not persisted.\n */\n constructor({\n messenger,\n state,\n clientConfigApiService,\n fetchInterval = DEFAULT_CACHE_DURATION,\n disabled = false,\n getMetaMetricsId,\n getCanonicalProfileId,\n metaMetricsFlags = [],\n clientVersion,\n prevClientVersion,\n defaultFeatureFlags = {},\n }: {\n messenger: RemoteFeatureFlagControllerMessenger;\n state?: Partial<RemoteFeatureFlagControllerState>;\n clientConfigApiService: AbstractClientConfigApiService;\n getMetaMetricsId: () => string;\n getCanonicalProfileId: () => string;\n metaMetricsFlags?: readonly string[];\n fetchInterval?: number;\n disabled?: boolean;\n clientVersion: string;\n prevClientVersion?: string;\n defaultFeatureFlags?: FeatureFlags;\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 const initialState: RemoteFeatureFlagControllerState = {\n ...getDefaultRemoteFeatureFlagControllerState(),\n ...state,\n };\n\n const hasClientVersionChanged =\n isValidSemVerVersion(prevClientVersion) &&\n prevClientVersion !== clientVersion;\n\n super({\n name: controllerName,\n metadata: remoteFeatureFlagControllerMetadata,\n messenger,\n state: {\n ...initialState,\n cacheTimestamp: hasClientVersionChanged\n ? 0\n : initialState.cacheTimestamp,\n },\n });\n\n this.#defaultFeatureFlags = defaultFeatureFlags;\n // Last session's effective flags stand in for the remote layer until\n // `init` re-derives it from the persisted raw flags, or a fetch replaces\n // it. Overrides are layered on top rather than subtracted out, so a remote\n // flag that happens to share an override's value is not lost.\n this.#processedRemoteFeatureFlags = initialState.remoteFeatureFlags;\n this.#fetchInterval = fetchInterval;\n this.#disabled = disabled;\n this.#clientConfigApiService = clientConfigApiService;\n this.#getMetaMetricsId = getMetaMetricsId;\n this.#getCanonicalProfileId = getCanonicalProfileId;\n this.#metaMetricsFlags = new Set(metaMetricsFlags);\n this.#clientVersion = clientVersion;\n\n this.messenger.registerMethodActionHandlers(\n this,\n MESSENGER_EXPOSED_METHODS,\n );\n }\n\n /**\n * Computes effective feature flags with precedence:\n * defaults < processed remote < local overrides.\n *\n * @param options - The layers to merge. Each defaults to the current layer.\n * @param options.processedRemoteFeatureFlags - The processed remote feature\n * flags. Defaults to the currently resolved remote layer.\n * @param options.localOverrides - Local overrides. Defaults to current state\n * overrides.\n * @returns The effective feature flags.\n */\n #getEffectiveFeatureFlags({\n processedRemoteFeatureFlags = this.#processedRemoteFeatureFlags,\n localOverrides = this.state.localOverrides,\n }: {\n processedRemoteFeatureFlags?: FeatureFlags;\n localOverrides?: FeatureFlags;\n } = {}): FeatureFlags {\n return {\n ...this.#defaultFeatureFlags,\n ...processedRemoteFeatureFlags,\n ...localOverrides,\n };\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 * @param force - When `true`, fetch even if the cache has not expired.\n * Defaults to `false`. Has no effect when the controller is disabled.\n * @returns A promise that resolves to the current set of feature flags.\n */\n async updateRemoteFeatureFlags(force = false): Promise<void> {\n if (this.#disabled || (!force && !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 * Computes the effective feature flags, re-deriving the remote layer from the\n * raw flags already in state. Threshold selection needs to await a hash and\n * so cannot run in the constructor, which is why this cannot be part of\n * construction. Clients must call this once after constructing the\n * controller.\n *\n * When there are no persisted raw flags, as on a fresh install or for state\n * persisted before raw flags were stored, the previous session's flags stand\n * in for the remote layer so that nothing is lost.\n */\n async init(): Promise<void> {\n const { rawRemoteFeatureFlags } = this.state;\n const hasRawRemoteFeatureFlags =\n rawRemoteFeatureFlags && Object.keys(rawRemoteFeatureFlags).length > 0;\n\n const resolved = hasRawRemoteFeatureFlags\n ? await this.#processRemoteFeatureFlags(rawRemoteFeatureFlags)\n : undefined;\n\n this.#processedRemoteFeatureFlags =\n resolved?.processedFlags ?? this.state.remoteFeatureFlags;\n\n this.update(() => {\n return {\n ...this.state,\n remoteFeatureFlags: this.#getEffectiveFeatureFlags(),\n ...(resolved && {\n thresholdCache: resolved.thresholdCache,\n featureFlagThresholdGroups: resolved.featureFlagThresholdGroups,\n }),\n };\n });\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): Promise<void> {\n const resolved = await this.#processRemoteFeatureFlags(remoteFeatureFlags);\n\n this.#processedRemoteFeatureFlags = resolved.processedFlags;\n\n // Single state update with all changes batched together\n this.update(() => {\n return {\n ...this.state,\n remoteFeatureFlags: this.#getEffectiveFeatureFlags({\n processedRemoteFeatureFlags: resolved.processedFlags,\n }),\n rawRemoteFeatureFlags: remoteFeatureFlags,\n cacheTimestamp: Date.now(),\n thresholdCache: resolved.thresholdCache,\n featureFlagThresholdGroups: resolved.featureFlagThresholdGroups,\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 /**\n * Selects the identifier used to bucket a threshold flag. Flags named in\n * `metaMetricsFlags` segment by MetaMetrics ID; all others segment by\n * canonical profile ID.\n *\n * @param featureFlagName - The name of the feature flag being processed.\n * @returns The segmentation identifier, which may be empty when unavailable.\n */\n #getSegmentationId(featureFlagName: string): string {\n if (this.#metaMetricsFlags.has(featureFlagName)) {\n return this.#getMetaMetricsId();\n }\n return this.#getCanonicalProfileId();\n }\n\n /**\n * Resolves raw feature flags into the values that apply to this client and\n * user, selecting version and threshold entries and reconciling the\n * threshold cache against the flags the server currently serves.\n *\n * @param remoteFeatureFlags - The unprocessed feature flags.\n * @returns The processed flags, the updated threshold cache, and the\n * selected threshold group names.\n */\n async #processRemoteFeatureFlags(remoteFeatureFlags: FeatureFlags): Promise<{\n processedFlags: FeatureFlags;\n thresholdCache: Record<string, number>;\n featureFlagThresholdGroups: Record<string, string>;\n }> {\n const processedFlags: FeatureFlags = {};\n const metaMetricsId = this.#getMetaMetricsId();\n const thresholdCacheUpdates: Record<string, number> = {};\n const featureFlagThresholdGroups: Record<string, string> = {};\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)) {\n // Validate array has valid threshold items before doing expensive crypto operation\n const hasValidThresholds = processedValue.some(\n isFeatureFlagWithScopeValue,\n );\n\n if (!hasValidThresholds) {\n // Not a threshold array - preserve as-is\n processedFlags[remoteFeatureFlagName] = processedValue;\n continue;\n }\n\n // Explicit-ID matching: check before hash-based threshold, bypasses cache\n const normalizedMetaMetricsId = metaMetricsId.trim().toLowerCase();\n const explicitMatch = normalizedMetaMetricsId\n ? findExplicitIdMatch(processedValue, normalizedMetaMetricsId)\n : undefined;\n\n if (explicitMatch) {\n processedValue = explicitMatch.value;\n if (explicitMatch.name) {\n featureFlagThresholdGroups[remoteFeatureFlagName] =\n explicitMatch.name;\n }\n } else {\n const segmentationId = this.#getSegmentationId(remoteFeatureFlagName);\n\n if (!segmentationId) {\n // The identifier needed to group this flag is unavailable, as it\n // is when `init` runs before whatever backs these getters is\n // ready. Exposing the unresolved threshold array would\n // flip the flag for the end user until the next fetch.\n if (\n hasProperty(\n this.#processedRemoteFeatureFlags,\n remoteFeatureFlagName,\n )\n ) {\n processedFlags[remoteFeatureFlagName] =\n this.#processedRemoteFeatureFlags[remoteFeatureFlagName];\n // Keep the group name alongside the value it was selected with,\n // so consumers segmenting on it do not see a value with no group.\n const previousGroup =\n this.state.featureFlagThresholdGroups?.[remoteFeatureFlagName];\n if (previousGroup) {\n featureFlagThresholdGroups[remoteFeatureFlagName] =\n previousGroup;\n }\n continue;\n }\n\n // No previous value to preserve, as on a fresh install.\n processedFlags[remoteFeatureFlagName] = processedValue;\n continue;\n }\n\n // Fall back to hash-based threshold selection with cache\n const cacheKey =\n `${segmentationId}:${remoteFeatureFlagName}` as const;\n let thresholdValue = this.state.thresholdCache?.[cacheKey];\n\n if (thresholdValue === undefined) {\n thresholdValue = await calculateThresholdForFlag(\n segmentationId,\n remoteFeatureFlagName,\n );\n\n // Collect new threshold for batched state update\n thresholdCacheUpdates[cacheKey] = thresholdValue;\n }\n\n const threshold = thresholdValue;\n const selectedGroup = processedValue.find(\n (featureFlag): featureFlag is FeatureFlagScopeValue => {\n if (!isFeatureFlagWithScopeValue(featureFlag)) {\n return false;\n }\n\n return threshold <= featureFlag.scope.value;\n },\n );\n\n if (selectedGroup) {\n processedValue = selectedGroup.value;\n if (selectedGroup.name) {\n featureFlagThresholdGroups[remoteFeatureFlagName] =\n selectedGroup.name;\n }\n }\n }\n }\n\n processedFlags[remoteFeatureFlagName] = processedValue;\n }\n\n const thresholdCache = {\n ...this.state.thresholdCache,\n ...thresholdCacheUpdates,\n };\n\n // Drop cached thresholds for flags this user is no longer served, under\n // either identifier they may have been bucketed by.\n const canonicalProfileId = this.#getCanonicalProfileId();\n const currentFlagNames = Object.keys(remoteFeatureFlags);\n for (const cacheKey of Object.keys(thresholdCache)) {\n const [cachedSegmentationId, ...cachedFlagNameParts] =\n cacheKey.split(':');\n const cachedFlagName = cachedFlagNameParts.join(':');\n if (\n (cachedSegmentationId === metaMetricsId ||\n cachedSegmentationId === canonicalProfileId) &&\n !currentFlagNames.includes(cachedFlagName)\n ) {\n delete thresholdCache[cacheKey];\n }\n }\n\n return {\n processedFlags,\n thresholdCache,\n featureFlagThresholdGroups,\n };\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 /**\n * Sets a local override for a specific feature flag.\n *\n * @param flagName - The name of the feature flag to override.\n * @param value - The override value for the feature flag.\n */\n setFlagOverride(flagName: string, value: Json): void {\n this.update(() => {\n const localOverrides = {\n ...this.state.localOverrides,\n [flagName]: value,\n };\n\n return {\n ...this.state,\n localOverrides,\n remoteFeatureFlags: this.#getEffectiveFeatureFlags({ localOverrides }),\n };\n });\n }\n\n /**\n * Clears the local override for a specific feature flag.\n *\n * @param flagName - The name of the feature flag to clear.\n */\n removeFlagOverride(flagName: string): void {\n const newLocalOverrides = { ...this.state.localOverrides };\n delete newLocalOverrides[flagName];\n\n this.update(() => {\n return {\n ...this.state,\n localOverrides: newLocalOverrides,\n remoteFeatureFlags: this.#getEffectiveFeatureFlags({\n localOverrides: newLocalOverrides,\n }),\n };\n });\n }\n\n /**\n * Clears all local feature flag overrides.\n */\n clearAllFlagOverrides(): void {\n this.update(() => {\n return {\n ...this.state,\n localOverrides: {},\n remoteFeatureFlags: this.#getEffectiveFeatureFlags({\n localOverrides: {},\n }),\n };\n });\n }\n}\n"]}
1
+ {"version":3,"file":"remote-feature-flag-controller.js","sourceRoot":"","sources":["../src/remote-feature-flag-controller.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n BaseController,\n ControllerGetStateAction,\n} from '@metamask/base-controller';\nimport type { ControllerStateChangeEvent } from '@metamask/base-controller';\nimport type { Messenger } from '@metamask/messenger';\nimport { hasProperty, isValidSemVerVersion } from '@metamask/utils';\nimport type { Json, SemVerVersion } from '@metamask/utils';\n\nimport type { AbstractClientConfigApiService } from './client-config-api-service/abstract-client-config-api-service.js';\nimport type { RemoteFeatureFlagControllerMethodActions } from './remote-feature-flag-controller-method-action-types.js';\nimport type {\n FeatureFlags,\n ServiceResponse,\n FeatureFlagScopeValue,\n} from './remote-feature-flag-controller-types.js';\nimport {\n calculateThresholdForFlag,\n isFeatureFlagWithScopeValue,\n} from './utils/user-segmentation-utils.js';\nimport { isVersionFeatureFlag, getVersionData } from './utils/version.js';\n\n// === GENERAL ===\n\nexport const controllerName = 'RemoteFeatureFlagController';\nexport const DEFAULT_CACHE_DURATION = 24 * 60 * 60 * 1000; // 1 day\n\n// === STATE ===\n\nexport type RemoteFeatureFlagControllerState = {\n remoteFeatureFlags: FeatureFlags;\n localOverrides?: FeatureFlags;\n rawRemoteFeatureFlags?: FeatureFlags;\n cacheTimestamp: number;\n thresholdCache?: Record<string, number>;\n featureFlagThresholdGroups?: Record<string, string>;\n};\n\nconst remoteFeatureFlagControllerMetadata = {\n remoteFeatureFlags: {\n includeInStateLogs: true,\n persist: true,\n includeInDebugSnapshot: true,\n usedInUi: true,\n },\n localOverrides: {\n includeInStateLogs: true,\n persist: true,\n includeInDebugSnapshot: true,\n usedInUi: true,\n },\n rawRemoteFeatureFlags: {\n includeInStateLogs: true,\n persist: true,\n includeInDebugSnapshot: true,\n usedInUi: false,\n },\n cacheTimestamp: {\n includeInStateLogs: true,\n persist: true,\n includeInDebugSnapshot: true,\n usedInUi: false,\n },\n thresholdCache: {\n includeInStateLogs: false,\n persist: true,\n includeInDebugSnapshot: false,\n usedInUi: false,\n },\n featureFlagThresholdGroups: {\n includeInStateLogs: true,\n persist: true,\n includeInDebugSnapshot: true,\n usedInUi: false,\n },\n};\n\n// === MESSENGER ===\n\nconst MESSENGER_EXPOSED_METHODS = [\n 'clearAllFlagOverrides',\n 'disable',\n 'enable',\n 'removeFlagOverride',\n 'setFlagOverride',\n 'updateRemoteFeatureFlags',\n] as const;\n\nexport type RemoteFeatureFlagControllerGetStateAction =\n ControllerGetStateAction<\n typeof controllerName,\n RemoteFeatureFlagControllerState\n >;\n\nexport type RemoteFeatureFlagControllerActions =\n | RemoteFeatureFlagControllerGetStateAction\n | RemoteFeatureFlagControllerMethodActions;\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 localOverrides: {},\n rawRemoteFeatureFlags: {},\n cacheTimestamp: 0,\n };\n}\n\n/**\n * Searches threshold entries for an explicit MetaMetrics ID match.\n * Returns the first entry whose `metaMetricsIds` list contains the given\n * normalized ID. Entries with malformed `metaMetricsIds` (not an array) are\n * skipped without throwing.\n *\n * @param entries - The array of raw threshold entries for a feature flag.\n * @param normalizedId - The current user's MetaMetrics ID, already trimmed and\n * lower-cased.\n * @returns The first matching entry, or `undefined` if none match.\n */\nfunction findExplicitIdMatch(\n entries: Json[],\n normalizedId: string,\n): FeatureFlagScopeValue | undefined {\n for (const entry of entries) {\n if (!isFeatureFlagWithScopeValue(entry)) {\n continue;\n }\n const { metaMetricsIds } = entry;\n if (!Array.isArray(metaMetricsIds)) {\n continue;\n }\n const hasMatch = metaMetricsIds.some(\n (id) =>\n typeof id === 'string' && id.trim().toLowerCase() === normalizedId,\n );\n if (hasMatch) {\n return entry;\n }\n }\n return undefined;\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 #getCanonicalProfileId: () => string;\n\n readonly #metaMetricsFlags: ReadonlySet<string>;\n\n readonly #clientVersion: SemVerVersion;\n\n readonly #defaultFeatureFlags: FeatureFlags;\n\n #processedRemoteFeatureFlags: FeatureFlags;\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.getCanonicalProfileId - Returns the canonical profile identifier used for threshold flags by default. Must return an empty string, rather than throwing, when the identifier is unavailable.\n * @param options.metaMetricsFlags - Names of feature flags that should use MetaMetrics ID for threshold assignment.\n * @param options.clientVersion - The current client version for version-based feature flag filtering. Must be a valid 3-part SemVer version string.\n * @param options.prevClientVersion - The previous client version for feature flag cache invalidation.\n * @param options.defaultFeatureFlags - Client-side default feature flags used as the lowest-precedence layer under processed remote flags and local overrides. Not persisted.\n */\n constructor({\n messenger,\n state,\n clientConfigApiService,\n fetchInterval = DEFAULT_CACHE_DURATION,\n disabled = false,\n getMetaMetricsId,\n getCanonicalProfileId,\n metaMetricsFlags = [],\n clientVersion,\n prevClientVersion,\n defaultFeatureFlags = {},\n }: {\n messenger: RemoteFeatureFlagControllerMessenger;\n state?: Partial<RemoteFeatureFlagControllerState>;\n clientConfigApiService: AbstractClientConfigApiService;\n getMetaMetricsId: () => string;\n getCanonicalProfileId: () => string;\n metaMetricsFlags?: readonly string[];\n fetchInterval?: number;\n disabled?: boolean;\n clientVersion: string;\n prevClientVersion?: string;\n defaultFeatureFlags?: FeatureFlags;\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 const initialState: RemoteFeatureFlagControllerState = {\n ...getDefaultRemoteFeatureFlagControllerState(),\n ...state,\n };\n\n const hasClientVersionChanged =\n isValidSemVerVersion(prevClientVersion) &&\n prevClientVersion !== clientVersion;\n\n super({\n name: controllerName,\n metadata: remoteFeatureFlagControllerMetadata,\n messenger,\n state: {\n ...initialState,\n cacheTimestamp: hasClientVersionChanged\n ? 0\n : initialState.cacheTimestamp,\n },\n });\n\n this.#defaultFeatureFlags = defaultFeatureFlags;\n // Last session's effective flags stand in for the remote layer until\n // `init` re-derives it from the persisted raw flags, or a fetch replaces\n // it. Overrides are layered on top rather than subtracted out, so a remote\n // flag that happens to share an override's value is not lost.\n this.#processedRemoteFeatureFlags = initialState.remoteFeatureFlags;\n this.#fetchInterval = fetchInterval;\n this.#disabled = disabled;\n this.#clientConfigApiService = clientConfigApiService;\n this.#getMetaMetricsId = getMetaMetricsId;\n this.#getCanonicalProfileId = getCanonicalProfileId;\n this.#metaMetricsFlags = new Set(metaMetricsFlags);\n this.#clientVersion = clientVersion;\n\n this.messenger.registerMethodActionHandlers(\n this,\n MESSENGER_EXPOSED_METHODS,\n );\n }\n\n /**\n * Computes effective feature flags with precedence:\n * defaults < processed remote < local overrides.\n *\n * @param options - The layers to merge. Each defaults to the current layer.\n * @param options.processedRemoteFeatureFlags - The processed remote feature\n * flags. Defaults to the currently resolved remote layer.\n * @param options.localOverrides - Local overrides. Defaults to current state\n * overrides.\n * @returns The effective feature flags.\n */\n #getEffectiveFeatureFlags({\n processedRemoteFeatureFlags = this.#processedRemoteFeatureFlags,\n localOverrides = this.state.localOverrides,\n }: {\n processedRemoteFeatureFlags?: FeatureFlags;\n localOverrides?: FeatureFlags;\n } = {}): FeatureFlags {\n return {\n ...this.#defaultFeatureFlags,\n ...processedRemoteFeatureFlags,\n ...localOverrides,\n };\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 * @param force - When `true`, fetch even if the cache has not expired.\n * Defaults to `false`. Has no effect when the controller is disabled.\n * @returns A promise that resolves to the current set of feature flags.\n */\n async updateRemoteFeatureFlags(force = false): Promise<void> {\n if (this.#disabled || (!force && !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 * Computes the effective feature flags, re-deriving the remote layer from the\n * raw flags already in state. Threshold selection needs to await a hash and\n * so cannot run in the constructor, which is why this cannot be part of\n * construction. Clients must call this once after constructing the\n * controller.\n *\n * When there are no persisted raw flags, as on a fresh install or for state\n * persisted before raw flags were stored, the previous session's flags stand\n * in for the remote layer so that nothing is lost.\n */\n async init(): Promise<void> {\n const { rawRemoteFeatureFlags } = this.state;\n const hasRawRemoteFeatureFlags =\n rawRemoteFeatureFlags && Object.keys(rawRemoteFeatureFlags).length > 0;\n\n const resolved = hasRawRemoteFeatureFlags\n ? await this.#processRemoteFeatureFlags(rawRemoteFeatureFlags)\n : undefined;\n\n this.#processedRemoteFeatureFlags =\n resolved?.processedFlags ?? this.state.remoteFeatureFlags;\n\n this.update(() => {\n return {\n ...this.state,\n remoteFeatureFlags: this.#getEffectiveFeatureFlags(),\n ...(resolved && {\n thresholdCache: resolved.thresholdCache,\n featureFlagThresholdGroups: resolved.featureFlagThresholdGroups,\n }),\n };\n });\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): Promise<void> {\n const resolved = await this.#processRemoteFeatureFlags(remoteFeatureFlags);\n\n this.#processedRemoteFeatureFlags = resolved.processedFlags;\n\n // Single state update with all changes batched together\n this.update(() => {\n return {\n ...this.state,\n remoteFeatureFlags: this.#getEffectiveFeatureFlags({\n processedRemoteFeatureFlags: resolved.processedFlags,\n }),\n rawRemoteFeatureFlags: remoteFeatureFlags,\n cacheTimestamp: Date.now(),\n thresholdCache: resolved.thresholdCache,\n featureFlagThresholdGroups: resolved.featureFlagThresholdGroups,\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 /**\n * Selects the identifier used to bucket a threshold flag. Flags named in\n * `metaMetricsFlags` segment by MetaMetrics ID; all others segment by\n * canonical profile ID.\n *\n * @param featureFlagName - The name of the feature flag being processed.\n * @returns The segmentation identifier, which may be empty when unavailable.\n */\n #getSegmentationId(featureFlagName: string): string {\n if (this.#metaMetricsFlags.has(featureFlagName)) {\n return this.#getMetaMetricsId();\n }\n return this.#getCanonicalProfileId();\n }\n\n /**\n * Resolves raw feature flags into the values that apply to this client and\n * user, selecting version and threshold entries and reconciling the\n * threshold cache against the flags the server currently serves.\n *\n * @param remoteFeatureFlags - The unprocessed feature flags.\n * @returns The processed flags, the updated threshold cache, and the\n * selected threshold group names.\n */\n async #processRemoteFeatureFlags(remoteFeatureFlags: FeatureFlags): Promise<{\n processedFlags: FeatureFlags;\n thresholdCache: Record<string, number>;\n featureFlagThresholdGroups: Record<string, string>;\n }> {\n const processedFlags: FeatureFlags = {};\n const metaMetricsId = this.#getMetaMetricsId();\n const thresholdCacheUpdates: Record<string, number> = {};\n const featureFlagThresholdGroups: Record<string, string> = {};\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)) {\n // Validate array has valid threshold items before doing expensive crypto operation\n const hasValidThresholds = processedValue.some(\n isFeatureFlagWithScopeValue,\n );\n\n if (!hasValidThresholds) {\n // Not a threshold array - preserve as-is\n processedFlags[remoteFeatureFlagName] = processedValue;\n continue;\n }\n\n // Explicit-ID matching: check before hash-based threshold, bypasses cache\n const normalizedMetaMetricsId = metaMetricsId.trim().toLowerCase();\n const explicitMatch = normalizedMetaMetricsId\n ? findExplicitIdMatch(processedValue, normalizedMetaMetricsId)\n : undefined;\n\n if (explicitMatch) {\n processedValue = explicitMatch.value;\n if (explicitMatch.name) {\n featureFlagThresholdGroups[remoteFeatureFlagName] =\n explicitMatch.name;\n }\n } else {\n const segmentationId = this.#getSegmentationId(remoteFeatureFlagName);\n\n if (!segmentationId) {\n // The identifier needed to group this flag is unavailable, as it\n // is when `init` runs before whatever backs these getters is\n // ready. Exposing the unresolved threshold array would\n // flip the flag for the end user until the next fetch.\n if (\n hasProperty(\n this.#processedRemoteFeatureFlags,\n remoteFeatureFlagName,\n )\n ) {\n processedFlags[remoteFeatureFlagName] =\n this.#processedRemoteFeatureFlags[remoteFeatureFlagName];\n // Keep the group name alongside the value it was selected with,\n // so consumers segmenting on it do not see a value with no group.\n const previousGroup =\n this.state.featureFlagThresholdGroups?.[remoteFeatureFlagName];\n if (previousGroup) {\n featureFlagThresholdGroups[remoteFeatureFlagName] =\n previousGroup;\n }\n continue;\n }\n\n // No previous value to preserve, as on a fresh install.\n processedFlags[remoteFeatureFlagName] = processedValue;\n continue;\n }\n\n // Fall back to hash-based threshold selection with cache\n const cacheKey =\n `${segmentationId}:${remoteFeatureFlagName}` as const;\n let thresholdValue = this.state.thresholdCache?.[cacheKey];\n\n if (thresholdValue === undefined) {\n thresholdValue = await calculateThresholdForFlag(\n segmentationId,\n remoteFeatureFlagName,\n );\n\n // Collect new threshold for batched state update\n thresholdCacheUpdates[cacheKey] = thresholdValue;\n }\n\n const threshold = thresholdValue;\n const selectedGroup = processedValue.find(\n (featureFlag): featureFlag is FeatureFlagScopeValue => {\n if (!isFeatureFlagWithScopeValue(featureFlag)) {\n return false;\n }\n\n return threshold <= featureFlag.scope.value;\n },\n );\n\n if (selectedGroup) {\n processedValue = selectedGroup.value;\n if (selectedGroup.name) {\n featureFlagThresholdGroups[remoteFeatureFlagName] =\n selectedGroup.name;\n }\n }\n }\n }\n\n processedFlags[remoteFeatureFlagName] = processedValue;\n }\n\n const thresholdCache = {\n ...this.state.thresholdCache,\n ...thresholdCacheUpdates,\n };\n\n // Drop cached thresholds for flags this user is no longer served, under\n // either identifier they may have been bucketed by.\n const canonicalProfileId = this.#getCanonicalProfileId();\n const currentFlagNames = Object.keys(remoteFeatureFlags);\n for (const cacheKey of Object.keys(thresholdCache)) {\n const [cachedSegmentationId, ...cachedFlagNameParts] =\n cacheKey.split(':');\n const cachedFlagName = cachedFlagNameParts.join(':');\n if (\n (cachedSegmentationId === metaMetricsId ||\n cachedSegmentationId === canonicalProfileId) &&\n !currentFlagNames.includes(cachedFlagName)\n ) {\n delete thresholdCache[cacheKey];\n }\n }\n\n return {\n processedFlags,\n thresholdCache,\n featureFlagThresholdGroups,\n };\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 /**\n * Sets a local override for a specific feature flag.\n *\n * @param flagName - The name of the feature flag to override.\n * @param value - The override value for the feature flag.\n */\n setFlagOverride(flagName: string, value: Json): void {\n this.update(() => {\n const localOverrides = {\n ...this.state.localOverrides,\n [flagName]: value,\n };\n\n return {\n ...this.state,\n localOverrides,\n remoteFeatureFlags: this.#getEffectiveFeatureFlags({ localOverrides }),\n };\n });\n }\n\n /**\n * Clears the local override for a specific feature flag.\n *\n * @param flagName - The name of the feature flag to clear.\n */\n removeFlagOverride(flagName: string): void {\n const newLocalOverrides = { ...this.state.localOverrides };\n delete newLocalOverrides[flagName];\n\n this.update(() => {\n return {\n ...this.state,\n localOverrides: newLocalOverrides,\n remoteFeatureFlags: this.#getEffectiveFeatureFlags({\n localOverrides: newLocalOverrides,\n }),\n };\n });\n }\n\n /**\n * Clears all local feature flag overrides.\n */\n clearAllFlagOverrides(): void {\n this.update(() => {\n return {\n ...this.state,\n localOverrides: {},\n remoteFeatureFlags: this.#getEffectiveFeatureFlags({\n localOverrides: {},\n }),\n };\n });\n }\n}\n"]}
@@ -1,5 +1,5 @@
1
- import type { Json } from "@metamask/utils";
2
- import type { FeatureFlagScopeValue } from "../remote-feature-flag-controller-types.cjs";
1
+ import type { Json } from '@metamask/utils';
2
+ import type { FeatureFlagScopeValue } from '../remote-feature-flag-controller-types.js';
3
3
  /**
4
4
  * Calculates a deterministic threshold value between 0 and 1 for A/B testing.
5
5
  * This function hashes the segmentation ID combined with the feature flag name
@@ -31,4 +31,4 @@ export declare function generateDeterministicRandomNumber(metaMetricsId: string)
31
31
  * @returns True if the value is a feature flag with scope, false otherwise
32
32
  */
33
33
  export declare const isFeatureFlagWithScopeValue: (featureFlag: Json) => featureFlag is FeatureFlagScopeValue;
34
- //# sourceMappingURL=user-segmentation-utils.d.cts.map
34
+ //# sourceMappingURL=user-segmentation-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-segmentation-utils.d.ts","sourceRoot":"","sources":["../../src/utils/user-segmentation-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAI5C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4CAA4C,CAAC;AAkBxF;;;;;;;;;GASG;AACH,wBAAsB,yBAAyB,CAC7C,cAAc,EAAE,MAAM,EACtB,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,MAAM,CAAC,CAsBjB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iCAAiC,CAC/C,aAAa,EAAE,MAAM,GACpB,MAAM,CA0CR;AAED;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,gBACzB,IAAI,KAChB,WAAW,IAAI,qBAMjB,CAAC"}