Skip to content

Commit 55bf103

Browse files
Merge pull request #177 from Azure/main
Merge main to release v2
2 parents 0f60afe + 8a7a686 commit 55bf103

File tree

8 files changed

+38
-250
lines changed

8 files changed

+38
-250
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ This client library adds additional [functionality](https://learn.microsoft.com/
1414

1515
[Dynamic Configuration Tutorial](https://learn.microsoft.com/azure/azure-app-configuration/enable-dynamic-configuration-javascript): A tutorial about how to enable dynamic configuration in your JavaScript applications.
1616

17+
[Feature Overview](https://learn.microsoft.com/azure/azure-app-configuration/configuration-provider-overview#feature-development-status): This document provides a feature status overview.
18+
1719
[Feature Reference](https://learn.microsoft.com/azure/azure-app-configuration/reference-javascript-provider): This document provides a full feature rundown.
1820

1921
### Prerequisites

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.0",
3+
"version": "2.0.1",
44
"description": "The JavaScript configuration provider for Azure App Configuration",
55
"main": "dist/index.js",
66
"module": "./dist-esm/index.js",

src/AzureAppConfigurationImpl.ts

Lines changed: 0 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { IKeyValueAdapter } from "./IKeyValueAdapter.js";
99
import { JsonKeyValueAdapter } from "./JsonKeyValueAdapter.js";
1010
import { DEFAULT_REFRESH_INTERVAL_IN_MS, MIN_REFRESH_INTERVAL_IN_MS } from "./RefreshOptions.js";
1111
import { Disposable } from "./common/disposable.js";
12-
import { base64Helper, jsonSorter } from "./common/utils.js";
1312
import {
1413
FEATURE_FLAGS_KEY_NAME,
1514
FEATURE_MANAGEMENT_KEY_NAME,
@@ -20,16 +19,9 @@ import {
2019
ETAG_KEY_NAME,
2120
FEATURE_FLAG_ID_KEY_NAME,
2221
FEATURE_FLAG_REFERENCE_KEY_NAME,
23-
ALLOCATION_ID_KEY_NAME,
2422
ALLOCATION_KEY_NAME,
25-
DEFAULT_WHEN_ENABLED_KEY_NAME,
26-
PERCENTILE_KEY_NAME,
27-
FROM_KEY_NAME,
28-
TO_KEY_NAME,
2923
SEED_KEY_NAME,
30-
VARIANT_KEY_NAME,
3124
VARIANTS_KEY_NAME,
32-
CONFIGURATION_VALUE_KEY_NAME,
3325
CONDITIONS_KEY_NAME,
3426
CLIENT_FILTERS_KEY_NAME
3527
} from "./featureManagement/constants.js";
@@ -677,15 +669,10 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
677669

678670
if (featureFlag[TELEMETRY_KEY_NAME] && featureFlag[TELEMETRY_KEY_NAME][ENABLED_KEY_NAME] === true) {
679671
const metadata = featureFlag[TELEMETRY_KEY_NAME][METADATA_KEY_NAME];
680-
let allocationId = "";
681-
if (featureFlag[ALLOCATION_KEY_NAME] !== undefined) {
682-
allocationId = await this.#generateAllocationId(featureFlag);
683-
}
684672
featureFlag[TELEMETRY_KEY_NAME][METADATA_KEY_NAME] = {
685673
[ETAG_KEY_NAME]: setting.etag,
686674
[FEATURE_FLAG_ID_KEY_NAME]: await this.#calculateFeatureFlagId(setting),
687675
[FEATURE_FLAG_REFERENCE_KEY_NAME]: this.#createFeatureFlagReference(setting),
688-
...(allocationId !== "" && { [ALLOCATION_ID_KEY_NAME]: allocationId }),
689676
...(metadata || {})
690677
};
691678
}
@@ -769,116 +756,6 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
769756
}
770757
return featureFlagReference;
771758
}
772-
773-
async #generateAllocationId(featureFlag: any): Promise<string> {
774-
let rawAllocationId = "";
775-
// Only default variant when enabled and variants allocated by percentile involve in the experimentation
776-
// The allocation id is genearted from default variant when enabled and percentile allocation
777-
const variantsForExperimentation: string[] = [];
778-
779-
rawAllocationId += `seed=${featureFlag[ALLOCATION_KEY_NAME][SEED_KEY_NAME] ?? ""}\ndefault_when_enabled=`;
780-
781-
if (featureFlag[ALLOCATION_KEY_NAME][DEFAULT_WHEN_ENABLED_KEY_NAME]) {
782-
variantsForExperimentation.push(featureFlag[ALLOCATION_KEY_NAME][DEFAULT_WHEN_ENABLED_KEY_NAME]);
783-
rawAllocationId += `${featureFlag[ALLOCATION_KEY_NAME][DEFAULT_WHEN_ENABLED_KEY_NAME]}`;
784-
}
785-
786-
rawAllocationId += "\npercentiles=";
787-
788-
const percentileList = featureFlag[ALLOCATION_KEY_NAME][PERCENTILE_KEY_NAME];
789-
if (percentileList) {
790-
const sortedPercentileList = percentileList
791-
.filter(p =>
792-
(p[FROM_KEY_NAME] !== undefined) &&
793-
(p[TO_KEY_NAME] !== undefined) &&
794-
(p[VARIANT_KEY_NAME] !== undefined) &&
795-
(p[FROM_KEY_NAME] !== p[TO_KEY_NAME]))
796-
.sort((a, b) => a[FROM_KEY_NAME] - b[FROM_KEY_NAME]);
797-
798-
const percentileAllocation: string[] = [];
799-
for (const percentile of sortedPercentileList) {
800-
variantsForExperimentation.push(percentile[VARIANT_KEY_NAME]);
801-
percentileAllocation.push(`${percentile[FROM_KEY_NAME]},${base64Helper(percentile[VARIANT_KEY_NAME])},${percentile[TO_KEY_NAME]}`);
802-
}
803-
rawAllocationId += percentileAllocation.join(";");
804-
}
805-
806-
if (variantsForExperimentation.length === 0 && featureFlag[ALLOCATION_KEY_NAME][SEED_KEY_NAME] === undefined) {
807-
// All fields required for generating allocation id are missing, short-circuit and return empty string
808-
return "";
809-
}
810-
811-
rawAllocationId += "\nvariants=";
812-
813-
if (variantsForExperimentation.length !== 0) {
814-
const variantsList = featureFlag[VARIANTS_KEY_NAME];
815-
if (variantsList) {
816-
const sortedVariantsList = variantsList
817-
.filter(v =>
818-
(v[NAME_KEY_NAME] !== undefined) &&
819-
variantsForExperimentation.includes(v[NAME_KEY_NAME]))
820-
.sort((a, b) => (a.name > b.name ? 1 : -1));
821-
822-
const variantConfiguration: string[] = [];
823-
for (const variant of sortedVariantsList) {
824-
const configurationValue = JSON.stringify(variant[CONFIGURATION_VALUE_KEY_NAME], jsonSorter) ?? "";
825-
variantConfiguration.push(`${base64Helper(variant[NAME_KEY_NAME])},${configurationValue}`);
826-
}
827-
rawAllocationId += variantConfiguration.join(";");
828-
}
829-
}
830-
831-
let crypto;
832-
833-
// Check for browser environment
834-
if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
835-
crypto = window.crypto;
836-
}
837-
// Check for Node.js environment
838-
else if (typeof global !== "undefined" && global.crypto) {
839-
crypto = global.crypto;
840-
}
841-
// Fallback to native Node.js crypto module
842-
else {
843-
try {
844-
if (typeof module !== "undefined" && module.exports) {
845-
crypto = require("crypto");
846-
}
847-
else {
848-
crypto = await import("crypto");
849-
}
850-
} catch (error) {
851-
console.error("Failed to load the crypto module:", error.message);
852-
throw error;
853-
}
854-
}
855-
856-
// Convert to UTF-8 encoded bytes
857-
const data = new TextEncoder().encode(rawAllocationId);
858-
859-
// In the browser, use crypto.subtle.digest
860-
if (crypto.subtle) {
861-
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
862-
const hashArray = new Uint8Array(hashBuffer);
863-
864-
// Only use the first 15 bytes
865-
const first15Bytes = hashArray.slice(0, 15);
866-
867-
// btoa/atob is also available in Node.js 18+
868-
const base64String = btoa(String.fromCharCode(...first15Bytes));
869-
const base64urlString = base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
870-
return base64urlString;
871-
}
872-
// In Node.js, use the crypto module's hash function
873-
else {
874-
const hash = crypto.createHash("sha256").update(data).digest();
875-
876-
// Only use the first 15 bytes
877-
const first15Bytes = hash.slice(0, 15);
878-
879-
return first15Bytes.toString("base64url");
880-
}
881-
}
882759
}
883760

884761
function getValidSelectors(selectors: SettingSelector[]): SettingSelector[] {

src/ConfigurationClientManager.ts

Lines changed: 32 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ const ENDPOINT_KEY_NAME = "Endpoint";
1616
const ID_KEY_NAME = "Id";
1717
const SECRET_KEY_NAME = "Secret";
1818
const TRUSTED_DOMAIN_LABELS = [".azconfig.", ".appconfig."];
19-
const FALLBACK_CLIENT_REFRESH_EXPIRE_INTERVAL = 60 * 60 * 1000; // 1 hour in milliseconds
20-
const MINIMAL_CLIENT_REFRESH_INTERVAL = 30 * 1000; // 30 seconds in milliseconds
21-
const SRV_QUERY_TIMEOUT = 30 * 1000; // 30 seconds in milliseconds
19+
const FALLBACK_CLIENT_EXPIRE_INTERVAL = 60 * 60 * 1000; // 1 hour in milliseconds
20+
const MINIMAL_CLIENT_REFRESH_INTERVAL = 30_000; // 30 seconds in milliseconds
21+
const DNS_RESOLVER_TIMEOUT = 3_000; // 3 seconds in milliseconds, in most cases, dns resolution should be within 200 milliseconds
22+
const DNS_RESOLVER_TRIES = 2;
23+
const MAX_ALTNATIVE_SRV_COUNT = 10;
2224

2325
export class ConfigurationClientManager {
2426
#isFailoverable: boolean;
@@ -33,8 +35,8 @@ export class ConfigurationClientManager {
3335
#staticClients: ConfigurationClientWrapper[]; // there should always be only one static client
3436
#dynamicClients: ConfigurationClientWrapper[];
3537
#replicaCount: number = 0;
36-
#lastFallbackClientRefreshTime: number = 0;
37-
#lastFallbackClientRefreshAttempt: number = 0;
38+
#lastFallbackClientUpdateTime: number = 0; // enforce to discover fallback client when it is expired
39+
#lastFallbackClientRefreshAttempt: number = 0; // avoid refreshing clients before the minimal refresh interval
3840

3941
constructor (
4042
connectionStringOrEndpoint?: string | URL,
@@ -85,10 +87,11 @@ export class ConfigurationClientManager {
8587
this.#isFailoverable = false;
8688
return;
8789
}
88-
if (this.#dns) {
90+
if (this.#dns) { // dns module is already loaded
8991
return;
9092
}
9193

94+
// We can only know whether dns module is available during runtime.
9295
try {
9396
this.#dns = await import("dns/promises");
9497
} catch (error) {
@@ -116,8 +119,7 @@ export class ConfigurationClientManager {
116119
(!this.#dynamicClients ||
117120
// All dynamic clients are in backoff means no client is available
118121
this.#dynamicClients.every(client => currentTime < client.backoffEndTime) ||
119-
currentTime >= this.#lastFallbackClientRefreshTime + FALLBACK_CLIENT_REFRESH_EXPIRE_INTERVAL)) {
120-
this.#lastFallbackClientRefreshAttempt = currentTime;
122+
currentTime >= this.#lastFallbackClientUpdateTime + FALLBACK_CLIENT_EXPIRE_INTERVAL)) {
121123
await this.#discoverFallbackClients(this.endpoint.hostname);
122124
return availableClients.concat(this.#dynamicClients);
123125
}
@@ -135,27 +137,22 @@ export class ConfigurationClientManager {
135137
async refreshClients() {
136138
const currentTime = Date.now();
137139
if (this.#isFailoverable &&
138-
currentTime >= new Date(this.#lastFallbackClientRefreshAttempt + MINIMAL_CLIENT_REFRESH_INTERVAL).getTime()) {
139-
this.#lastFallbackClientRefreshAttempt = currentTime;
140+
currentTime >= this.#lastFallbackClientRefreshAttempt + MINIMAL_CLIENT_REFRESH_INTERVAL) {
140141
await this.#discoverFallbackClients(this.endpoint.hostname);
141142
}
142143
}
143144

144145
async #discoverFallbackClients(host: string) {
145-
let result;
146-
let timeout;
146+
this.#lastFallbackClientRefreshAttempt = Date.now();
147+
let result: string[];
147148
try {
148-
result = await Promise.race([
149-
new Promise((_, reject) => timeout = setTimeout(() => reject(new Error("SRV record query timed out.")), SRV_QUERY_TIMEOUT)),
150-
this.#querySrvTargetHost(host)
151-
]);
149+
result = await this.#querySrvTargetHost(host);
152150
} catch (error) {
153-
throw new Error(`Failed to build fallback clients, ${error.message}`);
154-
} finally {
155-
clearTimeout(timeout);
151+
console.warn(`Failed to build fallback clients. ${error.message}`);
152+
return; // swallow the error when srv query fails
156153
}
157154

158-
const srvTargetHosts = shuffleList(result) as string[];
155+
const srvTargetHosts = shuffleList(result);
159156
const newDynamicClients: ConfigurationClientWrapper[] = [];
160157
for (const host of srvTargetHosts) {
161158
if (isValidEndpoint(host, this.#validDomain)) {
@@ -164,43 +161,36 @@ export class ConfigurationClientManager {
164161
continue;
165162
}
166163
const client = this.#credential ?
167-
new AppConfigurationClient(targetEndpoint, this.#credential, this.#clientOptions) :
168-
new AppConfigurationClient(buildConnectionString(targetEndpoint, this.#secret, this.#id), this.#clientOptions);
164+
new AppConfigurationClient(targetEndpoint, this.#credential, this.#clientOptions) :
165+
new AppConfigurationClient(buildConnectionString(targetEndpoint, this.#secret, this.#id), this.#clientOptions);
169166
newDynamicClients.push(new ConfigurationClientWrapper(targetEndpoint, client));
170167
}
171168
}
172169

173170
this.#dynamicClients = newDynamicClients;
174-
this.#lastFallbackClientRefreshTime = Date.now();
171+
this.#lastFallbackClientUpdateTime = Date.now();
175172
this.#replicaCount = this.#dynamicClients.length;
176173
}
177174

178175
/**
179-
* Query SRV records and return target hosts.
176+
* Queries SRV records for the given host and returns the target hosts.
180177
*/
181178
async #querySrvTargetHost(host: string): Promise<string[]> {
182179
const results: string[] = [];
183180

184181
try {
185-
// Look up SRV records for the origin host
186-
const originRecords = await this.#dns.resolveSrv(`${TCP_ORIGIN_KEY_NAME}.${host}`);
187-
if (originRecords.length === 0) {
188-
return results;
189-
}
190-
191-
// Add the first origin record to results
182+
// https://nodejs.org/api/dns.html#dnspromisesresolvesrvhostname
183+
const resolver = new this.#dns.Resolver({timeout: DNS_RESOLVER_TIMEOUT, tries: DNS_RESOLVER_TRIES});
184+
// On success, resolveSrv() returns an array of SrvRecord
185+
// On failure, resolveSrv() throws an error with code 'ENOTFOUND'.
186+
const originRecords = await resolver.resolveSrv(`${TCP_ORIGIN_KEY_NAME}.${host}`); // look up SRV records for the origin host
192187
const originHost = originRecords[0].name;
193-
results.push(originHost);
188+
results.push(originHost); // add the first origin record to results
194189

195-
// Look up SRV records for alternate hosts
196190
let index = 0;
197-
// eslint-disable-next-line no-constant-condition
198-
while (true) {
199-
const currentAlt = `${ALT_KEY_NAME}${index}`;
200-
const altRecords = await this.#dns.resolveSrv(`${currentAlt}.${TCP_KEY_NAME}.${originHost}`);
201-
if (altRecords.length === 0) {
202-
break; // No more alternate records, exit loop
203-
}
191+
while (index < MAX_ALTNATIVE_SRV_COUNT) {
192+
const currentAlt = `${ALT_KEY_NAME}${index}`; // look up SRV records for alternate hosts
193+
const altRecords = await resolver.resolveSrv(`${currentAlt}.${TCP_KEY_NAME}.${originHost}`);
204194

205195
altRecords.forEach(record => {
206196
const altHost = record.name;
@@ -212,7 +202,8 @@ export class ConfigurationClientManager {
212202
}
213203
} catch (err) {
214204
if (err.code === "ENOTFOUND") {
215-
return results; // No more SRV records found, return results
205+
// No more SRV records found, return results.
206+
return results;
216207
} else {
217208
throw new Error(`Failed to lookup SRV records: ${err.message}`);
218209
}

src/common/utils.ts

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,6 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33

4-
export function base64Helper(str: string): string {
5-
const bytes = new TextEncoder().encode(str); // UTF-8 encoding
6-
let chars = "";
7-
for (let i = 0; i < bytes.length; i++) {
8-
chars += String.fromCharCode(bytes[i]);
9-
}
10-
return btoa(chars);
11-
}
12-
13-
export function jsonSorter(key, value) {
14-
if (value === null) {
15-
return null;
16-
}
17-
if (Array.isArray(value)) {
18-
return value;
19-
}
20-
if (typeof value === "object") {
21-
return Object.fromEntries(Object.entries(value).sort());
22-
}
23-
return value;
24-
}
25-
264
export function shuffleList<T>(array: T[]): T[] {
275
for (let i = array.length - 1; i > 0; i--) {
286
const j = Math.floor(Math.random() * (i + 1));

src/version.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33

4-
export const VERSION = "2.0.0";
4+
export const VERSION = "2.0.1";

0 commit comments

Comments
 (0)