Skip to content

Commit 96e8336

Browse files
load feature flags from new endpoint (#341)
* load enhanced feature flags * fix tests * track whether enhanced feature flags were loaded * rename new feature flags to 'enhanced*', drop 'classic' for old feature flags * fix filter parameter handling * resolve comments * update * resolve comments * enhanced ff: remove prefix in feature flag reference and update client fitler parameter/variant value handling * update enhanced ff filter parameter handling with classic * enhanced ff filter parameter: preserve fallback to string * add null/undefined check for filter parameters * add api version validation * update dependency version * fix lint * minimum api version check
1 parent fbcb956 commit 96e8336

20 files changed

Lines changed: 1092 additions & 256 deletions

package-lock.json

Lines changed: 18 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@
6969
"vitest": "^4.1.8"
7070
},
7171
"dependencies": {
72-
"@azure/app-configuration": "^1.12.1",
7372
"@azure-rest/core-client": "^2.6.0",
73+
"@azure/app-configuration": "1.13.0-beta.1",
7474
"@azure/core-rest-pipeline": "^1.6.0",
7575
"@azure/identity": "^4.2.1",
7676
"@azure/keyvault-secrets": "^4.7.0",

src/appConfigurationClient.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
import {
5+
AppConfigurationClient as ConfigurationClient,
6+
AppConfigurationClientOptions as ConfigurationClientOptions,
7+
CheckConfigurationSettingsOptions,
8+
ConfigurationSettingId,
9+
FeatureFlagClient,
10+
FeatureFlagClientOptions,
11+
GetConfigurationSettingOptions,
12+
GetSnapshotOptions,
13+
KnownAppConfigurationApiVersion,
14+
ListConfigurationSettingsForSnapshotOptions,
15+
ListConfigurationSettingsOptions,
16+
ListFeatureFlagsOptions
17+
} from "@azure/app-configuration";
18+
import { TokenCredential } from "@azure/identity";
19+
import { instanceOfTokenCredential } from "./common/utils.js";
20+
import { ArgumentError } from "./common/errors.js";
21+
import { ErrorMessages } from "./common/errorMessages.js";
22+
import { RequestTracingOptions, applyRequestTracing } from "./requestTracing/utils.js";
23+
24+
/**
25+
* A client for the operations the provider needs from both the @see ConfigurationClient and
26+
* @see FeatureFlagClient SDK clients. Request tracing is applied before delegating each operation.
27+
*/
28+
export class AppConfigurationClient {
29+
#configurationClient: ConfigurationClient;
30+
#featureFlagClient: FeatureFlagClient;
31+
32+
constructor(connectionString: string, options?: ConfigurationClientOptions);
33+
constructor(endpoint: string, credential: TokenCredential, options?: ConfigurationClientOptions);
34+
constructor(
35+
connectionStringOrEndpoint: string,
36+
credentialOrOptions?: TokenCredential | ConfigurationClientOptions,
37+
options?: ConfigurationClientOptions
38+
) {
39+
const credentialPassed = instanceOfTokenCredential(credentialOrOptions);
40+
const configurationClientOptions = credentialPassed
41+
? options
42+
: credentialOrOptions as ConfigurationClientOptions | undefined;
43+
const featureFlagClientOptions = getFeatureFlagClientOptions(configurationClientOptions);
44+
45+
if (credentialPassed) {
46+
const credential = credentialOrOptions as TokenCredential;
47+
this.#configurationClient = new ConfigurationClient(connectionStringOrEndpoint, credential, configurationClientOptions);
48+
this.#featureFlagClient = new FeatureFlagClient(connectionStringOrEndpoint, credential, featureFlagClientOptions);
49+
} else {
50+
this.#configurationClient = new ConfigurationClient(connectionStringOrEndpoint, configurationClientOptions);
51+
this.#featureFlagClient = new FeatureFlagClient(connectionStringOrEndpoint, featureFlagClientOptions);
52+
}
53+
}
54+
55+
listConfigurationSettings(listOptions: ListConfigurationSettingsOptions, tracingOptions: RequestTracingOptions) {
56+
return this.#configurationClient.listConfigurationSettings(applyRequestTracing(tracingOptions, listOptions));
57+
}
58+
59+
checkConfigurationSettings(checkOptions: CheckConfigurationSettingsOptions, tracingOptions: RequestTracingOptions) {
60+
return this.#configurationClient.checkConfigurationSettings(applyRequestTracing(tracingOptions, checkOptions));
61+
}
62+
63+
getConfigurationSetting(configurationSettingId: ConfigurationSettingId, getOptions: GetConfigurationSettingOptions | undefined, tracingOptions: RequestTracingOptions) {
64+
return this.#configurationClient.getConfigurationSetting(configurationSettingId, applyRequestTracing(tracingOptions, getOptions));
65+
}
66+
67+
getSnapshot(snapshotName: string, getOptions: GetSnapshotOptions | undefined, tracingOptions: RequestTracingOptions) {
68+
return this.#configurationClient.getSnapshot(snapshotName, applyRequestTracing(tracingOptions, getOptions));
69+
}
70+
71+
listConfigurationSettingsForSnapshot(snapshotName: string, listOptions: ListConfigurationSettingsForSnapshotOptions | undefined, tracingOptions: RequestTracingOptions) {
72+
return this.#configurationClient.listConfigurationSettingsForSnapshot(snapshotName, applyRequestTracing(tracingOptions, listOptions));
73+
}
74+
75+
listFeatureFlags(listOptions: ListFeatureFlagsOptions, tracingOptions: RequestTracingOptions) {
76+
return this.#featureFlagClient.listFeatureFlags(applyRequestTracing(tracingOptions, listOptions));
77+
}
78+
}
79+
80+
export function getFeatureFlagClientOptions(options?: ConfigurationClientOptions): FeatureFlagClientOptions | undefined {
81+
if (options === undefined) {
82+
return undefined;
83+
}
84+
if (options.apiVersion !== undefined && !isSupportedApiVersion(options.apiVersion)) {
85+
throw new ArgumentError(ErrorMessages.API_VERSION_NOT_SUPPORTED);
86+
}
87+
88+
return {
89+
...options,
90+
...(options.retryOptions && { retryOptions: { ...options.retryOptions } }),
91+
...(options.proxyOptions && { proxyOptions: { ...options.proxyOptions } }),
92+
...(options.tlsOptions && { tlsOptions: { ...options.tlsOptions } }),
93+
...(options.userAgentOptions && { userAgentOptions: { ...options.userAgentOptions } }),
94+
...(options.telemetryOptions && { telemetryOptions: { ...options.telemetryOptions } }),
95+
...(options.additionalPolicies && { additionalPolicies: [...options.additionalPolicies] })
96+
};
97+
}
98+
99+
function isSupportedApiVersion(apiVersion: string): boolean {
100+
const apiVersionDate = parseApiVersionDate(apiVersion);
101+
const minimumApiVersionDate = parseApiVersionDate(KnownAppConfigurationApiVersion.V20260501Preview)!;
102+
return apiVersionDate !== undefined && apiVersionDate >= minimumApiVersionDate;
103+
}
104+
105+
function parseApiVersionDate(apiVersion: string): number | undefined {
106+
const match = /^(\d{4}-\d{2}-\d{2})(?:-[0-9A-Za-z.-]+)?$/.exec(apiVersion);
107+
if (match === null) {
108+
return undefined;
109+
}
110+
111+
const date = new Date(`${match[1]}T00:00:00Z`);
112+
if (Number.isNaN(date.getTime()) || date.toISOString().slice(0, 10) !== match[1]) {
113+
return undefined;
114+
}
115+
116+
return date.getTime();
117+
}
Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33

4-
import { AppConfigurationClient, AppConfigurationClientOptions } from "@azure/app-configuration";
5-
import { ConfigurationClientWrapper } from "./configurationClientWrapper.js";
4+
import { AppConfigurationClientOptions as ConfigurationClientOptions } from "@azure/app-configuration";
5+
import { AppConfigurationClient } from "./appConfigurationClient.js";
6+
import { AppConfigurationClientWrapper } from "./appConfigurationClientWrapper.js";
67
import { TokenCredential } from "@azure/identity";
78
import { AzureAppConfigurationOptions } from "./appConfigurationOptions.js";
89
import { isBrowser, isWebWorker } from "./requestTracing/utils.js";
@@ -28,18 +29,18 @@ const DNS_RESOLVER_TIMEOUT_IN_MS = 3_000;
2829
const DNS_RESOLVER_TRIES = 2;
2930
const MAX_ALTNATIVE_SRV_COUNT = 10;
3031

31-
export class ConfigurationClientManager {
32+
export class AppConfigurationClientManager {
3233
readonly endpoint: URL; // primary endpoint, which is the one specified in the connection string or passed in as a parameter
3334
#isFailoverable: boolean;
3435
#dns: any;
3536
#secret : string;
3637
#id : string;
3738
#credential: TokenCredential;
38-
#clientOptions: AppConfigurationClientOptions | undefined;
39+
#clientOptions: ConfigurationClientOptions | undefined;
3940
#appConfigOptions: AzureAppConfigurationOptions | undefined;
4041
#validDomain: string; // valid domain for the primary endpoint, which is used to discover replicas
41-
#staticClients: ConfigurationClientWrapper[]; // there should always be only one static client
42-
#dynamicClients: ConfigurationClientWrapper[];
42+
#staticClients: AppConfigurationClientWrapper[]; // there should always be only one static client
43+
#dynamicClients: AppConfigurationClientWrapper[];
4344
#replicaCount: number = 0;
4445
#lastFallbackClientUpdateTime: number = 0; // enforce to discover fallback client when it is expired
4546
#lastFallbackClientRefreshAttempt: number = 0; // avoid refreshing clients before the minimal refresh interval
@@ -84,7 +85,7 @@ export class ConfigurationClientManager {
8485
throw new ArgumentError(ErrorMessages.CONNECTION_STRING_OR_ENDPOINT_MISSED);
8586
}
8687

87-
this.#staticClients = [new ConfigurationClientWrapper(this.endpoint.origin, staticClient)];
88+
this.#staticClients = [new AppConfigurationClientWrapper(this.endpoint.origin, staticClient)];
8889
this.#validDomain = getValidDomain(this.endpoint.hostname.toLowerCase());
8990
}
9091

@@ -113,7 +114,7 @@ export class ConfigurationClientManager {
113114
return this.#replicaCount;
114115
}
115116

116-
async getClients(): Promise<ConfigurationClientWrapper[]> {
117+
async getClients(): Promise<AppConfigurationClientWrapper[]> {
117118
if (!this.#isFailoverable) {
118119
return this.#staticClients;
119120
}
@@ -159,17 +160,17 @@ export class ConfigurationClientManager {
159160
}
160161

161162
const srvTargetHosts = shuffleList(result);
162-
const newDynamicClients: ConfigurationClientWrapper[] = [];
163+
const newDynamicClients: AppConfigurationClientWrapper[] = [];
163164
for (const host of srvTargetHosts) {
164165
if (isValidEndpoint(host, this.#validDomain)) {
165166
const targetEndpoint = `https://${host}`;
166167
if (host.toLowerCase() === this.endpoint.hostname.toLowerCase()) {
167168
continue;
168169
}
169-
const client = this.#credential ?
170+
const appConfigurationClient = this.#credential ?
170171
new AppConfigurationClient(targetEndpoint, this.#credential, this.#clientOptions) :
171172
new AppConfigurationClient(buildConnectionString(targetEndpoint, this.#secret, this.#id), this.#clientOptions);
172-
newDynamicClients.push(new ConfigurationClientWrapper(targetEndpoint, client));
173+
newDynamicClients.push(new AppConfigurationClientWrapper(targetEndpoint, appConfigurationClient));
173174
}
174175
}
175176

@@ -256,7 +257,7 @@ export function isValidEndpoint(host: string, validDomain: string): boolean {
256257
return host.toLowerCase().endsWith(validDomain.toLowerCase());
257258
}
258259

259-
function getClientOptions(options?: AzureAppConfigurationOptions): AppConfigurationClientOptions | undefined {
260+
export function getClientOptions(options?: AzureAppConfigurationOptions): ConfigurationClientOptions {
260261
// user-agent
261262
let userAgentPrefix = RequestTracing.USER_AGENT_PREFIX; // Default UA for JavaScript Provider
262263
const userAgentOptions = options?.clientOptions?.userAgentOptions;
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33

4-
import { AppConfigurationClient } from "@azure/app-configuration";
4+
import { AppConfigurationClient } from "./appConfigurationClient.js";
55
import { getExponentialBackoffDuration } from "./common/backoffUtils.js";
66

7-
export class ConfigurationClientWrapper {
7+
export class AppConfigurationClientWrapper {
88
endpoint: string;
99
client: AppConfigurationClient;
1010
backoffEndTime: number = 0; // Timestamp

0 commit comments

Comments
 (0)