Skip to content

Commit 14afefe

Browse files
committed
update
1 parent 14fa3fb commit 14afefe

11 files changed

+100
-176
lines changed

src/AzureAppConfigurationImpl.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import { RefreshTimer } from "./refresh/RefreshTimer.js";
1515
import { getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils.js";
1616
import { KeyFilter, LabelFilter, SettingSelector } from "./types.js";
1717
import { ConfigurationClientManager } from "./ConfigurationClientManager.js";
18-
import { updateClientBackoffStatus } from "./ConfigurationClientWrapper.js";
1918

2019
type PagedSettingSelector = SettingSelector & {
2120
/**
@@ -185,7 +184,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
185184
const clientWrappers = await this.#clientManager.getClients();
186185
if (clientWrappers.length === 0) {
187186
this.#clientManager.refreshClients();
188-
throw new Error("No client is available to connect to the target app configuration store.");
187+
console.warn("Refresh skipped because no endpoint is accessible.");
189188
}
190189

191190
let successful: boolean;
@@ -195,11 +194,11 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
195194
const result = await funcToExecute(clientWrapper.client);
196195
this.#isFailoverRequest = false;
197196
successful = true;
198-
updateClientBackoffStatus(clientWrapper, successful);
197+
clientWrapper.updateBackoffStatus(successful);
199198
return result;
200199
} catch (error) {
201200
if (isFailoverableError(error)) {
202-
updateClientBackoffStatus(clientWrapper, successful);
201+
clientWrapper.updateBackoffStatus(successful);
203202
this.#isFailoverRequest = true;
204203
continue;
205204
}
@@ -209,7 +208,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
209208
}
210209

211210
this.#clientManager.refreshClients();
212-
throw new Error("All app configuration clients failed to get settings.");
211+
throw new Error("Failed to get configuration settings from endpoint.");
213212
}
214213

215214
async #loadSelectedKeyValues(): Promise<ConfigurationSetting[]> {
@@ -646,7 +645,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
646645
}
647646

648647
#createFeatureFlagReference(setting: ConfigurationSetting<string>): string {
649-
let featureFlagReference = `${this.#clientManager.endpoint}/kv/${setting.key}`;
648+
let featureFlagReference = `${this.#clientManager.endpoint.origin}/kv/${setting.key}`;
650649
if (setting.label && setting.label.trim().length !== 0) {
651650
featureFlagReference += `?label=${setting.label}`;
652651
}

src/ConfigurationClientManager.ts

Lines changed: 57 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ const MINIMAL_CLIENT_REFRESH_INTERVAL = 30 * 1000; // 30 seconds in milliseconds
2020
const SRV_QUERY_TIMEOUT = 30 * 1000; // 30 seconds in milliseconds
2121

2222
export class ConfigurationClientManager {
23-
isFailoverable: boolean;
24-
dns: any;
25-
endpoint: string;
23+
#isFailoverable: boolean;
24+
#dns: any;
25+
endpoint: URL;
2626
#secret : string;
2727
#id : string;
2828
#credential: TokenCredential;
@@ -40,81 +40,79 @@ export class ConfigurationClientManager {
4040
appConfigOptions?: AzureAppConfigurationOptions
4141
) {
4242
let staticClient: AppConfigurationClient;
43+
const credentialPassed = instanceOfTokenCredential(credentialOrOptions);
4344

44-
if (typeof connectionStringOrEndpoint === "string" && !instanceOfTokenCredential(credentialOrOptions)) {
45+
if (typeof connectionStringOrEndpoint === "string" && !credentialPassed) {
4546
const connectionString = connectionStringOrEndpoint;
4647
this.#appConfigOptions = credentialOrOptions as AzureAppConfigurationOptions;
4748
this.#clientOptions = getClientOptions(this.#appConfigOptions);
48-
staticClient = new AppConfigurationClient(connectionString, this.#clientOptions);
4949
const ConnectionStringRegex = /Endpoint=(.*);Id=(.*);Secret=(.*)/;
5050
const regexMatch = connectionString.match(ConnectionStringRegex);
5151
if (regexMatch) {
52-
this.endpoint = regexMatch[1];
52+
const endpointFromConnectionStr = regexMatch[1];
53+
this.endpoint = getValidUrl(endpointFromConnectionStr);
5354
this.#id = regexMatch[2];
5455
this.#secret = regexMatch[3];
5556
} else {
5657
throw new Error(`Invalid connection string. Valid connection strings should match the regex '${ConnectionStringRegex.source}'.`);
5758
}
58-
} else if ((connectionStringOrEndpoint instanceof URL || typeof connectionStringOrEndpoint === "string") && instanceOfTokenCredential(credentialOrOptions)) {
59+
staticClient = new AppConfigurationClient(connectionString, this.#clientOptions);
60+
} else if ((connectionStringOrEndpoint instanceof URL || typeof connectionStringOrEndpoint === "string") && credentialPassed) {
5961
let endpoint = connectionStringOrEndpoint;
6062
// ensure string is a valid URL.
6163
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-
}
64+
endpoint = getValidUrl(endpoint);
7165
}
7266

7367
const credential = credentialOrOptions as TokenCredential;
7468
this.#appConfigOptions = appConfigOptions as AzureAppConfigurationOptions;
7569
this.#clientOptions = getClientOptions(this.#appConfigOptions);
76-
staticClient = new AppConfigurationClient(connectionStringOrEndpoint.toString(), credential, this.#clientOptions);
77-
this.endpoint = endpoint.toString();
70+
this.endpoint = endpoint;
7871
this.#credential = credential;
72+
staticClient = new AppConfigurationClient(this.endpoint.origin, this.#credential, this.#clientOptions);
7973
} else {
8074
throw new Error("A connection string or an endpoint with credential must be specified to create a client.");
8175
}
8276

83-
this.#staticClients = [new ConfigurationClientWrapper(this.endpoint, staticClient)];
84-
this.#validDomain = getValidDomain(this.endpoint);
77+
this.#staticClients = [new ConfigurationClientWrapper(this.endpoint.origin, staticClient)];
78+
this.#validDomain = getValidDomain(this.endpoint.hostname.toLowerCase());
8579
}
8680

8781
async init() {
8882
if (this.#appConfigOptions?.replicaDiscoveryEnabled === false || isBrowser() || isWebWorker()) {
89-
this.isFailoverable = false;
83+
this.#isFailoverable = false;
9084
return;
9185
}
9286

9387
try {
94-
this.dns = await import("dns/promises");
88+
this.#dns = await import("dns/promises");
9589
}catch (error) {
96-
this.isFailoverable = false;
90+
this.#isFailoverable = false;
9791
console.warn("Failed to load the dns module:", error.message);
9892
return;
9993
}
10094

101-
this.isFailoverable = true;
95+
this.#isFailoverable = true;
10296
}
10397

10498
async getClients() : Promise<ConfigurationClientWrapper[]> {
105-
if (!this.isFailoverable) {
99+
if (!this.#isFailoverable) {
106100
return this.#staticClients;
107101
}
108102

109103
const currentTime = Date.now();
110-
if (this.#isFallbackClientDiscoveryDue(currentTime)) {
104+
// Filter static clients whose backoff time has ended
105+
let availableClients = this.#staticClients.filter(client => client.backoffEndTime <= currentTime);
106+
if (currentTime >= this.#lastFallbackClientRefreshAttempt + MINIMAL_CLIENT_REFRESH_INTERVAL &&
107+
(!this.#dynamicClients ||
108+
// All dynamic clients are in backoff means no client is available
109+
this.#dynamicClients.every(client => currentTime < client.backoffEndTime) ||
110+
currentTime >= this.#lastFallbackClientRefreshTime + FALLBACK_CLIENT_REFRESH_EXPIRE_INTERVAL)) {
111111
this.#lastFallbackClientRefreshAttempt = currentTime;
112-
const host = new URL(this.endpoint).hostname;
113-
await this.#discoverFallbackClients(host);
112+
await this.#discoverFallbackClients(this.endpoint.hostname);
113+
return availableClients.concat(this.#dynamicClients);
114114
}
115115

116-
// Filter static clients whose backoff time has ended
117-
let availableClients = this.#staticClients.filter(client => client.backoffEndTime <= currentTime);
118116
// If there are dynamic clients, filter and concatenate them
119117
if (this.#dynamicClients && this.#dynamicClients.length > 0) {
120118
availableClients = availableClients.concat(
@@ -127,23 +125,22 @@ export class ConfigurationClientManager {
127125

128126
async refreshClients() {
129127
const currentTime = Date.now();
130-
if (this.isFailoverable &&
131-
currentTime > new Date(this.#lastFallbackClientRefreshAttempt + MINIMAL_CLIENT_REFRESH_INTERVAL).getTime()) {
128+
if (this.#isFailoverable &&
129+
currentTime >= new Date(this.#lastFallbackClientRefreshAttempt + MINIMAL_CLIENT_REFRESH_INTERVAL).getTime()) {
132130
this.#lastFallbackClientRefreshAttempt = currentTime;
133-
const host = new URL(this.endpoint).hostname;
134-
await this.#discoverFallbackClients(host);
131+
await this.#discoverFallbackClients(this.endpoint.hostname);
135132
}
136133
}
137134

138-
async #discoverFallbackClients(host) {
135+
async #discoverFallbackClients(host: string) {
139136
let result;
140137
try {
141138
result = await Promise.race([
142139
new Promise((_, reject) => setTimeout(() => reject(new Error("SRV record query timed out.")), SRV_QUERY_TIMEOUT)),
143140
this.#querySrvTargetHost(host)
144141
]);
145142
} catch (error) {
146-
throw new Error(`Fail to build fallback clients, ${error.message}`);
143+
throw new Error(`Failed to build fallback clients, ${error.message}`);
147144
}
148145

149146
const srvTargetHosts = result as string[];
@@ -157,10 +154,12 @@ export class ConfigurationClientManager {
157154
for (const host of srvTargetHosts) {
158155
if (isValidEndpoint(host, this.#validDomain)) {
159156
const targetEndpoint = `https://${host}`;
160-
if (targetEndpoint.toLowerCase() === this.endpoint.toLowerCase()) {
157+
if (host.toLowerCase() === this.endpoint.hostname.toLowerCase()) {
161158
continue;
162159
}
163-
const client = this.#credential ? new AppConfigurationClient(targetEndpoint, this.#credential, this.#clientOptions) : new AppConfigurationClient(buildConnectionString(targetEndpoint, this.#secret, this.#id), this.#clientOptions);
160+
const client = this.#credential ?
161+
new AppConfigurationClient(targetEndpoint, this.#credential, this.#clientOptions) :
162+
new AppConfigurationClient(buildConnectionString(targetEndpoint, this.#secret, this.#id), this.#clientOptions);
164163
newDynamicClients.push(new ConfigurationClientWrapper(targetEndpoint, client));
165164
}
166165
}
@@ -169,13 +168,6 @@ export class ConfigurationClientManager {
169168
this.#lastFallbackClientRefreshTime = Date.now();
170169
}
171170

172-
#isFallbackClientDiscoveryDue(dateTime) {
173-
return dateTime >= this.#lastFallbackClientRefreshAttempt + MINIMAL_CLIENT_REFRESH_INTERVAL
174-
&& (!this.#dynamicClients
175-
|| this.#dynamicClients.every(client => dateTime < client.backoffEndTime)
176-
|| dateTime >= this.#lastFallbackClientRefreshTime + FALLBACK_CLIENT_REFRESH_EXPIRE_INTERVAL);
177-
}
178-
179171
/**
180172
* Query SRV records and return target hosts.
181173
*/
@@ -184,7 +176,7 @@ export class ConfigurationClientManager {
184176

185177
try {
186178
// Look up SRV records for the origin host
187-
const originRecords = await this.dns.resolveSrv(`${TCP_ORIGIN_KEY_NAME}.${host}`);
179+
const originRecords = await this.#dns.resolveSrv(`${TCP_ORIGIN_KEY_NAME}.${host}`);
188180
if (originRecords.length === 0) {
189181
return results;
190182
}
@@ -198,7 +190,7 @@ export class ConfigurationClientManager {
198190
// eslint-disable-next-line no-constant-condition
199191
while (true) {
200192
const currentAlt = `${ALT_KEY_NAME}${index}`;
201-
const altRecords = await this.dns.resolveSrv(`${currentAlt}.${TCP_KEY_NAME}.${originHost}`);
193+
const altRecords = await this.#dns.resolveSrv(`${currentAlt}.${TCP_KEY_NAME}.${originHost}`);
202194
if (altRecords.length === 0) {
203195
break; // No more alternate records, exit loop
204196
}
@@ -238,19 +230,12 @@ function buildConnectionString(endpoint, secret, id: string): string {
238230
/**
239231
* Extracts a valid domain from the given endpoint URL based on trusted domain labels.
240232
*/
241-
export function getValidDomain(endpoint: string): string {
242-
try {
243-
const url = new URL(endpoint);
244-
const host = url.hostname.toLowerCase();
245-
246-
for (const label of TRUSTED_DOMAIN_LABELS) {
247-
const index = host.lastIndexOf(label);
248-
if (index !== -1) {
249-
return host.substring(index);
250-
}
233+
export function getValidDomain(host: string): string {
234+
for (const label of TRUSTED_DOMAIN_LABELS) {
235+
const index = host.lastIndexOf(label);
236+
if (index !== -1) {
237+
return host.substring(index);
251238
}
252-
} catch (error) {
253-
console.error("Error parsing URL:", error.message);
254239
}
255240

256241
return "";
@@ -267,7 +252,7 @@ export function isValidEndpoint(host: string, validDomain: string): boolean {
267252
return host.toLowerCase().endsWith(validDomain.toLowerCase());
268253
}
269254

270-
export function getClientOptions(options?: AzureAppConfigurationOptions): AppConfigurationClientOptions | undefined {
255+
function getClientOptions(options?: AzureAppConfigurationOptions): AppConfigurationClientOptions | undefined {
271256
// user-agent
272257
let userAgentPrefix = RequestTracing.USER_AGENT_PREFIX; // Default UA for JavaScript Provider
273258
const userAgentOptions = options?.clientOptions?.userAgentOptions;
@@ -290,6 +275,18 @@ export function getClientOptions(options?: AzureAppConfigurationOptions): AppCon
290275
});
291276
}
292277

278+
function getValidUrl(endpoint: string): URL {
279+
try {
280+
return new URL(endpoint);
281+
} catch (error) {
282+
if (error.code === "ERR_INVALID_URL") {
283+
throw new Error("Invalid endpoint URL.", { cause: error });
284+
} else {
285+
throw error;
286+
}
287+
}
288+
}
289+
293290
export function instanceOfTokenCredential(obj: unknown) {
294291
return obj && typeof obj === "object" && "getToken" in obj && typeof obj.getToken === "function";
295292
}

src/ConfigurationClientWrapper.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,21 @@ export class ConfigurationClientWrapper {
1212
endpoint: string;
1313
client: AppConfigurationClient;
1414
backoffEndTime: number = 0; // Timestamp
15-
failedAttempts: number = 0;
15+
#failedAttempts: number = 0;
1616

1717
constructor(endpoint: string, client: AppConfigurationClient) {
1818
this.endpoint = endpoint;
1919
this.client = client;
2020
}
21-
}
2221

23-
export function updateClientBackoffStatus(clientWrapper: ConfigurationClientWrapper, successfull: boolean) {
24-
if (successfull) {
25-
clientWrapper.failedAttempts = 0;
26-
clientWrapper.backoffEndTime = Date.now();
27-
} else {
28-
clientWrapper.failedAttempts += 1;
29-
clientWrapper.backoffEndTime = Date.now() + calculateBackoffDuration(clientWrapper.failedAttempts);
22+
updateBackoffStatus(successfull: boolean) {
23+
if (successfull) {
24+
this.#failedAttempts = 0;
25+
this.backoffEndTime = Date.now();
26+
} else {
27+
this.#failedAttempts += 1;
28+
this.backoffEndTime = Date.now() + calculateBackoffDuration(this.#failedAttempts);
29+
}
3030
}
3131
}
3232

@@ -35,7 +35,7 @@ export function calculateBackoffDuration(failedAttempts: number) {
3535
return MinBackoffDuration;
3636
}
3737

38-
// exponential: minBackoff * 2^(failedAttempts-1)
38+
// exponential: minBackoff * 2 ^ (failedAttempts - 1)
3939
const exponential = Math.min(failedAttempts - 1, MAX_SAFE_EXPONENTIAL);
4040
let calculatedBackoffDuration = MinBackoffDuration * (1 << exponential);
4141
if (calculatedBackoffDuration > MaxBackoffDuration) {

test/clientOptions.test.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ describe("custom client options", function () {
3030
this.timeout(15000);
3131

3232
const fakeEndpoint = "https://azure.azconfig.io";
33-
const replicaDiscoveryEnabled = false;
3433
beforeEach(() => {
3534
// Thus here mock it to reply 500, in which case the retry mechanism works.
3635
nock(fakeEndpoint).persist().get(() => true).reply(500);
@@ -44,7 +43,6 @@ describe("custom client options", function () {
4443
const countPolicy = new HttpRequestCountPolicy();
4544
const loadPromise = () => {
4645
return load(createMockedConnectionString(fakeEndpoint), {
47-
replicaDiscoveryEnabled: replicaDiscoveryEnabled,
4846
clientOptions: {
4947
additionalPolicies: [{
5048
policy: countPolicy,
@@ -67,7 +65,6 @@ describe("custom client options", function () {
6765
const countPolicy = new HttpRequestCountPolicy();
6866
const loadWithMaxRetries = (maxRetries: number) => {
6967
return load(createMockedConnectionString(fakeEndpoint), {
70-
replicaDiscoveryEnabled: replicaDiscoveryEnabled,
7168
clientOptions: {
7269
additionalPolicies: [{
7370
policy: countPolicy,
@@ -106,7 +103,6 @@ describe("custom client options", function () {
106103
const countPolicy = new HttpRequestCountPolicy();
107104
const loadPromise = () => {
108105
return load(createMockedConnectionString(fakeEndpoint), {
109-
replicaDiscoveryEnabled: replicaDiscoveryEnabled,
110106
clientOptions: {
111107
additionalPolicies: [{
112108
policy: countPolicy,

0 commit comments

Comments
 (0)