Skip to content

Commit 3fae5be

Browse files
committed
update
1 parent 40502bb commit 3fae5be

File tree

7 files changed

+71
-175
lines changed

7 files changed

+71
-175
lines changed

src/AzureAppConfigurationImpl.ts

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -185,11 +185,12 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
185185
const clientWrappers = await this.#clientManager.getClients();
186186
if (clientWrappers.length === 0) {
187187
this.#clientManager.refreshClients();
188-
throw new Error("No client is available to connect to the target App Configuration store.");
188+
throw new Error("No client is available to connect to the target app configuration store.");
189189
}
190190

191+
let successful: boolean;
191192
for (const clientWrapper of clientWrappers) {
192-
let successful = false;
193+
successful = false;
193194
try {
194195
const result = await funcToExecute(clientWrapper.client);
195196
this.#isFailoverRequest = false;
@@ -214,7 +215,6 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
214215
async #loadSelectedKeyValues(): Promise<ConfigurationSetting[]> {
215216
// validate selectors
216217
const selectors = getValidKeyValueSelectors(this.#options?.selectors);
217-
let loadedSettings: ConfigurationSetting[] = [];
218218

219219
const funcToExecute = async (client) => {
220220
const loadedSettings: ConfigurationSetting[] = [];
@@ -239,8 +239,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
239239
return loadedSettings;
240240
};
241241

242-
loadedSettings = await this.#executeWithFailoverPolicy(funcToExecute);
243-
return loadedSettings;
242+
return await this.#executeWithFailoverPolicy(funcToExecute) as ConfigurationSetting[];
244243
}
245244

246245
/**
@@ -329,8 +328,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
329328
return featureFlagSettings;
330329
};
331330

332-
let featureFlagSettings: ConfigurationSetting[] = [];
333-
featureFlagSettings = await this.#executeWithFailoverPolicy(funcToExecute);
331+
const featureFlagSettings = await this.#executeWithFailoverPolicy(funcToExecute) as ConfigurationSetting[];
334332

335333
// parse feature flags
336334
const featureFlags = await Promise.all(
@@ -459,13 +457,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
459457
}
460458

461459
if (needRefresh) {
462-
try {
463-
await this.#loadSelectedAndWatchedKeyValues();
464-
} catch (error) {
465-
// if refresh failed, backoff
466-
this.#refreshTimer.backoff();
467-
throw error;
468-
}
460+
await this.#loadSelectedAndWatchedKeyValues();
469461
}
470462

471463
this.#refreshTimer.reset();
@@ -484,7 +476,6 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
484476

485477
// check if any feature flag is changed
486478
const funcToExecute = async (client) => {
487-
const needRefresh = false;
488479
for (const selector of this.#featureFlagSelectors) {
489480
const listOptions: ListConfigurationSettingsOptions = {
490481
keyFilter: `${featureFlagPrefix}${selector.keyFilter}`,
@@ -504,18 +495,12 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
504495
}
505496
}
506497
}
507-
return needRefresh;
498+
return false;
508499
};
509500

510501
const needRefresh: boolean = await this.#executeWithFailoverPolicy(funcToExecute);
511502
if (needRefresh) {
512-
try {
513-
await this.#loadFeatureFlags();
514-
} catch (error) {
515-
// if refresh failed, backoff
516-
this.#featureFlagRefreshTimer.backoff();
517-
throw error;
518-
}
503+
await this.#loadFeatureFlags();
519504
}
520505

521506
this.#featureFlagRefreshTimer.reset();
@@ -713,5 +698,6 @@ function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSel
713698
}
714699

715700
function isFailoverableError(error: any): boolean {
716-
return isRestError(error) && (error.statusCode === 408 || error.statusCode === 429 || (error.statusCode !== undefined && error.statusCode >= 500));
701+
return isRestError(error) && (error.statusCode === 401 || error.statusCode === 403 || error.statusCode === 408 || error.statusCode === 429 ||
702+
(error.statusCode !== undefined && error.statusCode >= 500));
717703
}

src/AzureAppConfigurationOptions.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,7 @@ export const MaxRetryDelayInMs = 60000;
1212

1313
export interface AzureAppConfigurationOptions {
1414
/**
15-
* Specifies whether enable replica discovery or not.
16-
*
17-
* @remarks
18-
* If not specified, the default value is true.
19-
*/
20-
replicaDiscoveryEnabled?: boolean;
21-
22-
/**
23-
* Specify what key-values to include in the configuration provider.
15+
* Specifies what key-values to include in the configuration provider.
2416
*
2517
* @remarks
2618
* If no selectors are specified then all key-values with no label will be included.
@@ -55,4 +47,12 @@ export interface AzureAppConfigurationOptions {
5547
* Specifies options used to configure feature flags.
5648
*/
5749
featureFlagOptions?: FeatureFlagOptions;
50+
51+
/**
52+
* Specifies whether to enable replica discovery or not.
53+
*
54+
* @remarks
55+
* If not specified, the default value is true.
56+
*/
57+
replicaDiscoveryEnabled?: boolean;
5858
}

src/ConfigurationClientManager.ts

Lines changed: 45 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,20 @@ import { AzureAppConfigurationOptions, MaxRetries, MaxRetryDelayInMs } from "./A
88
import { isFailoverableEnv } from "./requestTracing/utils";
99
import * as RequestTracing from "./requestTracing/constants";
1010

11-
const TCP_ORIGIN = "_origin._tcp";
12-
const ALT = "_alt";
13-
const TCP = "_tcp";
14-
const Endpoint = "Endpoint";
15-
const Id = "Id";
16-
const Secret = "Secret";
11+
const TCP_ORIGIN_KEY_NAME = "_origin._tcp";
12+
const ALT_KEY_NAME = "_alt";
13+
const TCP_KEY_NAME = "_tcp";
14+
const Endpoint_KEY_NAME = "Endpoint";
15+
const Id_KEY_NAME = "Id";
16+
const Secret_KEY_NAME = "Secret";
17+
const ConnectionStringRegex = /Endpoint=(.*);Id=(.*);Secret=(.*)/;
1718
const AzConfigDomainLabel = ".azconfig.";
1819
const AppConfigDomainLabel = ".appconfig.";
1920
const FallbackClientRefreshExpireInterval = 60 * 60 * 1000; // 1 hour in milliseconds
2021
const MinimalClientRefreshInterval = 30 * 1000; // 30 seconds in milliseconds
21-
const SrvQueryTimeout = 5000; // 5 seconds
22+
const SrvQueryTimeout = 5* 1000; // 5 seconds in milliseconds
2223

23-
interface IConfigurationClientManager {
24-
getClients(): Promise<ConfigurationClientWrapper[]>;
25-
refreshClients(): Promise<void>;
26-
}
27-
28-
export class ConfigurationClientManager implements IConfigurationClientManager {
24+
export class ConfigurationClientManager {
2925
isFailoverable: boolean;
3026
endpoint: string;
3127
#secret : string;
@@ -44,24 +40,41 @@ export class ConfigurationClientManager implements IConfigurationClientManager {
4440
appConfigOptions?: AzureAppConfigurationOptions
4541
) {
4642
let staticClient: AppConfigurationClient;
47-
let options: AzureAppConfigurationOptions;
43+
let options: AzureAppConfigurationOptions | undefined;
4844

49-
if (typeof connectionStringOrEndpoint === "string") {
45+
if (typeof connectionStringOrEndpoint === "string" && !instanceOfTokenCredential(credentialOrOptions)) {
5046
const connectionString = connectionStringOrEndpoint;
5147
options = credentialOrOptions as AzureAppConfigurationOptions;
5248
this.#clientOptions = getClientOptions(options);
5349
staticClient = new AppConfigurationClient(connectionString, this.#clientOptions);
54-
this.#secret = parseConnectionString(connectionString, Secret);
55-
this.#id = parseConnectionString(connectionString, Id);
56-
// TODO: need to check if it's CDN or not
57-
this.endpoint = parseConnectionString(connectionString, Endpoint);
50+
const regexMatch = connectionString.match(ConnectionStringRegex);
51+
if (regexMatch) {
52+
this.endpoint = regexMatch[1];
53+
this.#id = regexMatch[2];
54+
this.#secret = regexMatch[3];
55+
} else {
56+
throw new Error(`Invalid connection string. Valid connection strings should match the regex '${ConnectionStringRegex.source}'.`);
57+
}
58+
} else if ((connectionStringOrEndpoint instanceof URL || typeof connectionStringOrEndpoint === "string") && instanceOfTokenCredential(credentialOrOptions)) {
59+
let endpoint = connectionStringOrEndpoint;
60+
// ensure string is a valid URL.
61+
if (typeof endpoint === "string") {
62+
try {
63+
endpoint = new URL(endpoint);
64+
} catch (error) {
65+
if (error.code === "ERR_INVALID_URL") {
66+
throw new Error("Invalid endpoint URL.", { cause: error });
67+
} else {
68+
throw error;
69+
}
70+
}
71+
}
5872

59-
} else if (connectionStringOrEndpoint instanceof URL) {
6073
const credential = credentialOrOptions as TokenCredential;
6174
options = appConfigOptions as AzureAppConfigurationOptions;
6275
this.#clientOptions = getClientOptions(options);
6376
staticClient = new AppConfigurationClient(connectionStringOrEndpoint.toString(), credential, this.#clientOptions);
64-
this.endpoint = connectionStringOrEndpoint.toString();
77+
this.endpoint = endpoint.toString();
6578
this.#credential = credential;
6679
} else {
6780
throw new Error("A connection string or an endpoint with credential must be specified to create a client.");
@@ -84,7 +97,7 @@ export class ConfigurationClientManager implements IConfigurationClientManager {
8497
await this.#discoverFallbackClients(host);
8598
}
8699

87-
// Filter static clients where BackoffEndTime is less than or equal to now
100+
// Filter static clients whose backoff time has ended
88101
let availableClients = this.#staticClients.filter(client => client.backoffEndTime <= currentTime);
89102
// If there are dynamic clients, filter and concatenate them
90103
if (this.#dynamicClients && this.#dynamicClients.length > 0) {
@@ -101,8 +114,8 @@ export class ConfigurationClientManager implements IConfigurationClientManager {
101114
if (this.isFailoverable &&
102115
currentTime > new Date(this.#lastFallbackClientRefreshAttempt + MinimalClientRefreshInterval).getTime()) {
103116
this.#lastFallbackClientRefreshAttempt = currentTime;
104-
const url = new URL(this.endpoint);
105-
await this.#discoverFallbackClients(url.hostname);
117+
const host = new URL(this.endpoint).hostname;
118+
await this.#discoverFallbackClients(host);
106119
}
107120
}
108121

@@ -176,7 +189,7 @@ async function querySrvTargetHost(host: string): Promise<string[]> {
176189

177190
try {
178191
// Look up SRV records for the origin host
179-
const originRecords = await dns.resolveSrv(`${TCP_ORIGIN}.${host}`);
192+
const originRecords = await dns.resolveSrv(`${TCP_ORIGIN_KEY_NAME}.${host}`);
180193
if (originRecords.length === 0) {
181194
return results;
182195
}
@@ -189,9 +202,9 @@ async function querySrvTargetHost(host: string): Promise<string[]> {
189202
let index = 0;
190203
let moreAltRecordsExist = true;
191204
while (moreAltRecordsExist) {
192-
const currentAlt = `${ALT}${index}`;
205+
const currentAlt = `${ALT_KEY_NAME}${index}`;
193206
try {
194-
const altRecords = await dns.resolveSrv(`${currentAlt}.${TCP}.${originHost}`);
207+
const altRecords = await dns.resolveSrv(`${currentAlt}.${TCP_KEY_NAME}.${originHost}`);
195208
if (altRecords.length === 0) {
196209
moreAltRecordsExist = false;
197210
break; // No more alternate records, exit loop
@@ -223,30 +236,6 @@ async function querySrvTargetHost(host: string): Promise<string[]> {
223236
return results;
224237
}
225238

226-
/**
227-
* Parses the connection string to extract the value associated with a specific token.
228-
*/
229-
function parseConnectionString(connectionString, token: string): string {
230-
if (!connectionString) {
231-
throw new Error("connectionString is empty");
232-
}
233-
234-
// Token format is "token="
235-
const searchToken = `${token}=`;
236-
const startIndex = connectionString.indexOf(searchToken);
237-
if (startIndex === -1) {
238-
throw new Error(`Token ${token} not found in connectionString`);
239-
}
240-
241-
// Move startIndex to the beginning of the token value
242-
const valueStartIndex = startIndex + searchToken.length;
243-
const endIndex = connectionString.indexOf(";", valueStartIndex);
244-
const valueEndIndex = endIndex === -1 ? connectionString.length : endIndex;
245-
246-
// Extract and return the token value
247-
return connectionString.substring(valueStartIndex, valueEndIndex);
248-
}
249-
250239
/**
251240
* Builds a connection string from the given endpoint, secret, and id.
252241
* Returns an empty string if either secret or id is empty.
@@ -256,7 +245,7 @@ function buildConnectionString(endpoint, secret, id: string): string {
256245
return "";
257246
}
258247

259-
return `${Endpoint}=${endpoint};${Id}=${id};${Secret}=${secret}`;
248+
return `${Endpoint_KEY_NAME}=${endpoint};${Id_KEY_NAME}=${id};${Secret_KEY_NAME}=${secret}`;
260249
}
261250

262251
/**
@@ -315,3 +304,7 @@ export function getClientOptions(options?: AzureAppConfigurationOptions): AppCon
315304
});
316305
}
317306

307+
export function instanceOfTokenCredential(obj: unknown) {
308+
return obj && typeof obj === "object" && "getToken" in obj && typeof obj.getToken === "function";
309+
}
310+

src/load.ts

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { TokenCredential } from "@azure/identity";
55
import { AzureAppConfiguration } from "./AzureAppConfiguration.js";
66
import { AzureAppConfigurationImpl } from "./AzureAppConfigurationImpl.js";
77
import { AzureAppConfigurationOptions } from "./AzureAppConfigurationOptions.js";
8-
import { ConfigurationClientManager } from "./ConfigurationClientManager.js";
8+
import { ConfigurationClientManager, instanceOfTokenCredential } from "./ConfigurationClientManager.js";
99

1010
const MIN_DELAY_FOR_UNHANDLED_ERROR: number = 5000; // 5 seconds
1111

@@ -31,32 +31,12 @@ export async function load(
3131
): Promise<AzureAppConfiguration> {
3232
const startTimestamp = Date.now();
3333
let options: AzureAppConfigurationOptions | undefined;
34-
let clientManager: ConfigurationClientManager;
34+
const clientManager = new ConfigurationClientManager(connectionStringOrEndpoint, credentialOrOptions, appConfigOptions);
3535

36-
// input validation
37-
if (typeof connectionStringOrEndpoint === "string" && !instanceOfTokenCredential(credentialOrOptions)) {
38-
const connectionString = connectionStringOrEndpoint;
36+
if (!instanceOfTokenCredential(credentialOrOptions)) {
3937
options = credentialOrOptions as AzureAppConfigurationOptions;
40-
clientManager = new ConfigurationClientManager(connectionString, options);
41-
} else if ((connectionStringOrEndpoint instanceof URL || typeof connectionStringOrEndpoint === "string") && instanceOfTokenCredential(credentialOrOptions)) {
42-
let endpoint = connectionStringOrEndpoint;
43-
// ensure string is a valid URL.
44-
if (typeof endpoint === "string") {
45-
try {
46-
endpoint = new URL(endpoint);
47-
} catch (error) {
48-
if (error.code === "ERR_INVALID_URL") {
49-
throw new Error("Invalid endpoint URL.", { cause: error });
50-
} else {
51-
throw error;
52-
}
53-
}
54-
}
55-
const credential = credentialOrOptions as TokenCredential;
56-
options = appConfigOptions;
57-
clientManager = new ConfigurationClientManager(endpoint, credential, options);
5838
} else {
59-
throw new Error("A connection string or an endpoint with credential must be specified to create a client.");
39+
options = appConfigOptions;
6040
}
6141

6242
try {
@@ -74,7 +54,3 @@ export async function load(
7454
throw error;
7555
}
7656
}
77-
78-
function instanceOfTokenCredential(obj: unknown) {
79-
return obj && typeof obj === "object" && "getToken" in obj && typeof obj.getToken === "function";
80-
}

0 commit comments

Comments
 (0)