Skip to content

Commit 726c112

Browse files
Merge pull request #190 from Azure/merge-main-to-preview
Merge main to preview
2 parents f23045d + 1eb7aac commit 726c112

30 files changed

+832
-277
lines changed

package-lock.json

Lines changed: 2 additions & 2 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
@@ -1,6 +1,6 @@
11
{
22
"name": "@azure/app-configuration-provider",
3-
"version": "2.0.1",
3+
"version": "2.0.2",
44
"description": "The JavaScript configuration provider for Azure App Configuration",
55
"main": "dist/index.js",
66
"module": "./dist-esm/index.js",

rollup.config.mjs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,15 @@ import dts from "rollup-plugin-dts";
44

55
export default [
66
{
7-
external: ["@azure/app-configuration", "@azure/keyvault-secrets", "@azure/core-rest-pipeline", "crypto", "dns/promises", "@microsoft/feature-management"],
7+
external: [
8+
"@azure/app-configuration",
9+
"@azure/keyvault-secrets",
10+
"@azure/core-rest-pipeline",
11+
"@azure/identity",
12+
"crypto",
13+
"dns/promises",
14+
"@microsoft/feature-management"
15+
],
816
input: "src/index.ts",
917
output: [
1018
{

src/AzureAppConfiguration.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33

44
import { Disposable } from "./common/disposable.js";
55

6+
/**
7+
* Azure App Configuration provider.
8+
*/
69
export type AzureAppConfiguration = {
710
/**
811
* API to trigger refresh operation.

src/AzureAppConfigurationImpl.ts

Lines changed: 147 additions & 42 deletions
Large diffs are not rendered by default.

src/AzureAppConfigurationOptions.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,10 @@
33

44
import { AppConfigurationClientOptions } from "@azure/app-configuration";
55
import { KeyVaultOptions } from "./keyvault/KeyVaultOptions.js";
6-
import { RefreshOptions } from "./RefreshOptions.js";
6+
import { RefreshOptions } from "./refresh/refreshOptions.js";
77
import { SettingSelector } from "./types.js";
88
import { FeatureFlagOptions } from "./featureManagement/FeatureFlagOptions.js";
9-
10-
export const MaxRetries = 2;
11-
export const MaxRetryDelayInMs = 60000;
9+
import { StartupOptions } from "./StartupOptions.js";
1210

1311
export interface AzureAppConfigurationOptions {
1412
/**
@@ -48,6 +46,11 @@ export interface AzureAppConfigurationOptions {
4846
*/
4947
featureFlagOptions?: FeatureFlagOptions;
5048

49+
/**
50+
* Specifies options used to configure provider startup.
51+
*/
52+
startupOptions?: StartupOptions;
53+
5154
/**
5255
* Specifies whether to enable replica discovery or not.
5356
*

src/ConfigurationClientManager.ts

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,15 @@
44
import { AppConfigurationClient, AppConfigurationClientOptions } from "@azure/app-configuration";
55
import { ConfigurationClientWrapper } from "./ConfigurationClientWrapper.js";
66
import { TokenCredential } from "@azure/identity";
7-
import { AzureAppConfigurationOptions, MaxRetries, MaxRetryDelayInMs } from "./AzureAppConfigurationOptions.js";
7+
import { AzureAppConfigurationOptions } from "./AzureAppConfigurationOptions.js";
88
import { isBrowser, isWebWorker } from "./requestTracing/utils.js";
99
import * as RequestTracing from "./requestTracing/constants.js";
10-
import { shuffleList } from "./common/utils.js";
10+
import { shuffleList, instanceOfTokenCredential } from "./common/utils.js";
11+
import { ArgumentError } from "./common/error.js";
12+
13+
// Configuration client retry options
14+
const CLIENT_MAX_RETRIES = 2;
15+
const CLIENT_MAX_RETRY_DELAY = 60_000; // 1 minute in milliseconds
1116

1217
const TCP_ORIGIN_KEY_NAME = "_origin._tcp";
1318
const ALT_KEY_NAME = "_alt";
@@ -54,18 +59,18 @@ export class ConfigurationClientManager {
5459
const regexMatch = connectionString.match(ConnectionStringRegex);
5560
if (regexMatch) {
5661
const endpointFromConnectionStr = regexMatch[1];
57-
this.endpoint = getValidUrl(endpointFromConnectionStr);
62+
this.endpoint = new URL(endpointFromConnectionStr);
5863
this.#id = regexMatch[2];
5964
this.#secret = regexMatch[3];
6065
} else {
61-
throw new Error(`Invalid connection string. Valid connection strings should match the regex '${ConnectionStringRegex.source}'.`);
66+
throw new ArgumentError(`Invalid connection string. Valid connection strings should match the regex '${ConnectionStringRegex.source}'.`);
6267
}
6368
staticClient = new AppConfigurationClient(connectionString, this.#clientOptions);
6469
} else if ((connectionStringOrEndpoint instanceof URL || typeof connectionStringOrEndpoint === "string") && credentialPassed) {
6570
let endpoint = connectionStringOrEndpoint;
6671
// ensure string is a valid URL.
6772
if (typeof endpoint === "string") {
68-
endpoint = getValidUrl(endpoint);
73+
endpoint = new URL(endpoint);
6974
}
7075

7176
const credential = credentialOrOptions as TokenCredential;
@@ -75,7 +80,7 @@ export class ConfigurationClientManager {
7580
this.#credential = credential;
7681
staticClient = new AppConfigurationClient(this.endpoint.origin, this.#credential, this.#clientOptions);
7782
} else {
78-
throw new Error("A connection string or an endpoint with credential must be specified to create a client.");
83+
throw new ArgumentError("A connection string or an endpoint with credential must be specified to create a client.");
7984
}
8085

8186
this.#staticClients = [new ConfigurationClientWrapper(this.endpoint.origin, staticClient)];
@@ -200,12 +205,12 @@ export class ConfigurationClientManager {
200205
});
201206
index++;
202207
}
203-
} catch (err) {
204-
if (err.code === "ENOTFOUND") {
208+
} catch (error) {
209+
if (error.code === "ENOTFOUND") {
205210
// No more SRV records found, return results.
206211
return results;
207212
} else {
208-
throw new Error(`Failed to lookup SRV records: ${err.message}`);
213+
throw new Error(`Failed to lookup SRV records: ${error.message}`);
209214
}
210215
}
211216

@@ -260,8 +265,8 @@ function getClientOptions(options?: AzureAppConfigurationOptions): AppConfigurat
260265

261266
// retry options
262267
const defaultRetryOptions = {
263-
maxRetries: MaxRetries,
264-
maxRetryDelayInMs: MaxRetryDelayInMs,
268+
maxRetries: CLIENT_MAX_RETRIES,
269+
maxRetryDelayInMs: CLIENT_MAX_RETRY_DELAY,
265270
};
266271
const retryOptions = Object.assign({}, defaultRetryOptions, options?.clientOptions?.retryOptions);
267272

@@ -272,20 +277,3 @@ function getClientOptions(options?: AzureAppConfigurationOptions): AppConfigurat
272277
}
273278
});
274279
}
275-
276-
function getValidUrl(endpoint: string): URL {
277-
try {
278-
return new URL(endpoint);
279-
} catch (error) {
280-
if (error.code === "ERR_INVALID_URL") {
281-
throw new Error("Invalid endpoint URL.", { cause: error });
282-
} else {
283-
throw error;
284-
}
285-
}
286-
}
287-
288-
export function instanceOfTokenCredential(obj: unknown) {
289-
return obj && typeof obj === "object" && "getToken" in obj && typeof obj.getToken === "function";
290-
}
291-

src/ConfigurationClientWrapper.ts

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,7 @@
22
// Licensed under the MIT license.
33

44
import { AppConfigurationClient } from "@azure/app-configuration";
5-
6-
const MaxBackoffDuration = 10 * 60 * 1000; // 10 minutes in milliseconds
7-
const MinBackoffDuration = 30 * 1000; // 30 seconds in milliseconds
8-
const MAX_SAFE_EXPONENTIAL = 30; // Used to avoid overflow. bitwise operations in JavaScript are limited to 32 bits. It overflows at 2^31 - 1.
9-
const JITTER_RATIO = 0.25;
5+
import { getExponentialBackoffDuration } from "./common/backoffUtils.js";
106

117
export class ConfigurationClientWrapper {
128
endpoint: string;
@@ -25,25 +21,7 @@ export class ConfigurationClientWrapper {
2521
this.backoffEndTime = Date.now();
2622
} else {
2723
this.#failedAttempts += 1;
28-
this.backoffEndTime = Date.now() + calculateBackoffDuration(this.#failedAttempts);
24+
this.backoffEndTime = Date.now() + getExponentialBackoffDuration(this.#failedAttempts);
2925
}
3026
}
3127
}
32-
33-
export function calculateBackoffDuration(failedAttempts: number) {
34-
if (failedAttempts <= 1) {
35-
return MinBackoffDuration;
36-
}
37-
38-
// exponential: minBackoff * 2 ^ (failedAttempts - 1)
39-
const exponential = Math.min(failedAttempts - 1, MAX_SAFE_EXPONENTIAL);
40-
let calculatedBackoffDuration = MinBackoffDuration * (1 << exponential);
41-
if (calculatedBackoffDuration > MaxBackoffDuration) {
42-
calculatedBackoffDuration = MaxBackoffDuration;
43-
}
44-
45-
// jitter: random value between [-1, 1) * jitterRatio * calculatedBackoffMs
46-
const jitter = JITTER_RATIO * (Math.random() * 2 - 1);
47-
48-
return calculatedBackoffDuration * (1 + jitter);
49-
}

src/JsonKeyValueAdapter.ts

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT license.
33

44
import { ConfigurationSetting, featureFlagContentType, secretReferenceContentType } from "@azure/app-configuration";
5+
import { parseContentType, isJsonContentType } from "./common/contentType.js";
56
import { IKeyValueAdapter } from "./IKeyValueAdapter.js";
67

78
export class JsonKeyValueAdapter implements IKeyValueAdapter {
@@ -17,7 +18,8 @@ export class JsonKeyValueAdapter implements IKeyValueAdapter {
1718
if (JsonKeyValueAdapter.#ExcludedJsonContentTypes.includes(setting.contentType)) {
1819
return false;
1920
}
20-
return isJsonContentType(setting.contentType);
21+
const contentType = parseContentType(setting.contentType);
22+
return isJsonContentType(contentType);
2123
}
2224

2325
async processKeyValue(setting: ConfigurationSetting): Promise<[string, unknown]> {
@@ -34,24 +36,3 @@ export class JsonKeyValueAdapter implements IKeyValueAdapter {
3436
return [setting.key, parsedValue];
3537
}
3638
}
37-
38-
// Determine whether a content type string is a valid JSON content type.
39-
// https://docs.microsoft.com/en-us/azure/azure-app-configuration/howto-leverage-json-content-type
40-
function isJsonContentType(contentTypeValue: string): boolean {
41-
if (!contentTypeValue) {
42-
return false;
43-
}
44-
45-
const contentTypeNormalized: string = contentTypeValue.trim().toLowerCase();
46-
const mimeType: string = contentTypeNormalized.split(";", 1)[0].trim();
47-
const typeParts: string[] = mimeType.split("/");
48-
if (typeParts.length !== 2) {
49-
return false;
50-
}
51-
52-
if (typeParts[0] !== "application") {
53-
return false;
54-
}
55-
56-
return typeParts[1].split("+").includes("json");
57-
}

src/StartupOptions.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
export const DEFAULT_STARTUP_TIMEOUT_IN_MS = 100 * 1000; // 100 seconds in milliseconds
5+
6+
export interface StartupOptions {
7+
/**
8+
* The amount of time allowed to load data from Azure App Configuration on startup.
9+
*
10+
* @remarks
11+
* If not specified, the default value is 100 seconds.
12+
*/
13+
timeoutInMs?: number;
14+
}

0 commit comments

Comments
 (0)