Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a0a551e
wip
zhiyuanliang-ms Sep 7, 2025
d0ecd09
load from azure front door
zhiyuanliang-ms Sep 7, 2025
8d06357
fix bug
zhiyuanliang-ms Sep 8, 2025
3310171
add more test
zhiyuanliang-ms Sep 8, 2025
6878b64
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Sep 8, 2025
9af1749
add browser test
zhiyuanliang-ms Sep 8, 2025
f97ae3d
update
zhiyuanliang-ms Sep 8, 2025
264113d
update
zhiyuanliang-ms Sep 8, 2025
58d1f2e
fix test
zhiyuanliang-ms Sep 8, 2025
725dd63
remove sync-token header
zhiyuanliang-ms Sep 9, 2025
665af4c
wip
zhiyuanliang-ms Sep 9, 2025
9d63b7c
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Sep 10, 2025
5087206
update
zhiyuanliang-ms Sep 10, 2025
339b3fa
fix lint
zhiyuanliang-ms Sep 10, 2025
b2b76ed
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Sep 10, 2025
1577bef
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Sep 26, 2025
87f952b
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Oct 2, 2025
4480562
update CDN tag
zhiyuanliang-ms Oct 14, 2025
d3c5f96
Merge branch 'zhiyuanliang/afd-support' of https://github.com/Azure/A…
zhiyuanliang-ms Nov 4, 2025
414a8c8
disallow sentinel key refresh for AFD
zhiyuanliang-ms Nov 5, 2025
bebf549
update
zhiyuanliang-ms Nov 5, 2025
564f16a
update
zhiyuanliang-ms Nov 6, 2025
28d37c2
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Nov 7, 2025
ca056b9
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Nov 7, 2025
b5f26c4
update
zhiyuanliang-ms Nov 7, 2025
5b09aee
update
zhiyuanliang-ms Nov 7, 2025
dcf5814
resolve merge conflict
zhiyuanliang-ms Nov 8, 2025
5d9477b
update error message
zhiyuanliang-ms Nov 8, 2025
2599b6a
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Nov 9, 2025
29f7958
update
zhiyuanliang-ms Nov 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
"playwright": "^1.55.0"
},
"dependencies": {
"@azure/app-configuration": "^1.9.0",
"@azure/app-configuration": "^1.9.2",
"@azure/core-rest-pipeline": "^1.6.0",
"@azure/identity": "^4.2.1",
"@azure/keyvault-secrets": "^4.7.0",
Expand Down
35 changes: 35 additions & 0 deletions src/afd/afdRequestPipelinePolicy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { PipelinePolicy } from "@azure/core-rest-pipeline";

/**
* The pipeline policy that remove the authorization header from the request to allow anonymous access to the Azure Front Door.
* @remarks
* The policy position should be perRetry, since it should be executed after the "Sign" phase: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/core/core-client/src/serviceClient.ts
*/
export class AnonymousRequestPipelinePolicy implements PipelinePolicy {
name: string = "AppConfigurationAnonymousRequestPolicy";

async sendRequest(request, next) {
if (request.headers.has("authorization")) {
request.headers.delete("authorization");
}
return next(request);
}
}

/**
* The pipeline policy that remove the "sync-token" header from the request.
* The policy position should be perRetry. It should be executed after the SyncTokenPolicy in @azure/app-configuration, which is executed after retry phase: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/appconfiguration/app-configuration/src/appConfigurationClient.ts#L198
*/
export class RemoveSyncTokenPipelinePolicy implements PipelinePolicy {
name: string = "AppConfigurationRemoveSyncTokenPolicy";

async sendRequest(request, next) {
if (request.headers.has("sync-token")) {
request.headers.delete("sync-token");
}
return next(request);
}
}
4 changes: 4 additions & 0 deletions src/afd/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

export const SERVER_TIMESTAMP_HEADER = "x-ms-date";
86 changes: 63 additions & 23 deletions src/appConfigurationImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ import {
GetSnapshotOptions,
ListConfigurationSettingsForSnapshotOptions,
GetSnapshotResponse,
KnownSnapshotComposition
KnownSnapshotComposition,
ListConfigurationSettingPage
} from "@azure/app-configuration";
import { isRestError } from "@azure/core-rest-pipeline";
import { isRestError, RestError } from "@azure/core-rest-pipeline";
import { AzureAppConfiguration, ConfigurationObjectConstructionOptions } from "./appConfiguration.js";
import { AzureAppConfigurationOptions } from "./appConfigurationOptions.js";
import { IKeyValueAdapter } from "./keyValueAdapter.js";
Expand Down Expand Up @@ -67,6 +68,7 @@ import { ConfigurationClientManager } from "./configurationClientManager.js";
import { getFixedBackoffDuration, getExponentialBackoffDuration } from "./common/backoffUtils.js";
import { InvalidOperationError, ArgumentError, isFailoverableError, isInputError } from "./common/errors.js";
import { ErrorMessages } from "./common/errorMessages.js";
import { SERVER_TIMESTAMP_HEADER } from "./afd/constants.js";

const MIN_DELAY_FOR_UNHANDLED_FAILURE = 5_000; // 5 seconds

Expand Down Expand Up @@ -128,12 +130,17 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
// Load balancing
#lastSuccessfulEndpoint: string = "";

// Azure Front Door
#isAfdUsed: boolean = false;

constructor(
clientManager: ConfigurationClientManager,
options: AzureAppConfigurationOptions | undefined,
isAfdUsed: boolean
) {
this.#options = options;
this.#clientManager = clientManager;
this.#isAfdUsed = isAfdUsed;

// enable request tracing if not opt-out
this.#requestTracingEnabled = requestTracingEnabled();
Expand Down Expand Up @@ -221,7 +228,8 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
isFailoverRequest: this.#isFailoverRequest,
featureFlagTracing: this.#featureFlagTracing,
fmVersion: this.#fmVersion,
aiConfigurationTracing: this.#aiConfigurationTracing
aiConfigurationTracing: this.#aiConfigurationTracing,
isAfdUsed: this.#isAfdUsed
};
}

Expand Down Expand Up @@ -490,7 +498,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
* If false, loads key-value using the key-value selectors. Defaults to false.
*/
async #loadConfigurationSettings(loadFeatureFlag: boolean = false): Promise<ConfigurationSetting[]> {
const selectors = loadFeatureFlag ? this.#ffSelectors : this.#kvSelectors;
const selectors: PagedSettingsWatcher[] = loadFeatureFlag ? this.#ffSelectors : this.#kvSelectors;

// Use a Map to deduplicate configuration settings by key. When multiple selectors return settings with the same key,
// the configuration setting loaded by the later selector in the iteration order will override the one from the earlier selector.
Expand All @@ -509,6 +517,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
tagsFilter: selector.tagFilters
};
const { items, pageWatchers } = await this.#listConfigurationSettings(listOptions);

selector.pageWatchers = pageWatchers;
settings = items;
} else { // snapshot selector
Expand Down Expand Up @@ -627,23 +636,23 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {

// try refresh if any of watched settings is changed.
let needRefresh = false;
let changedSentinel;
let changedSentinelWatcher;
let changedSentinel: WatchedSetting | undefined;
let changedSentinelWatcher: SettingWatcher | undefined;
if (this.#watchAll) {
needRefresh = await this.#checkConfigurationSettingsChange(this.#kvSelectors);
} else {
for (const watchedSetting of this.#sentinels.keys()) {
const configurationSettingId: ConfigurationSettingId = { key: watchedSetting.key, label: watchedSetting.label, etag: this.#sentinels.get(watchedSetting)?.etag };
const response = await this.#getConfigurationSetting(configurationSettingId, {
onlyIfChanged: true
});

const watcher = this.#sentinels.get(watchedSetting);
if (response?.statusCode === 200 // created or changed
|| (response === undefined && watcher?.etag !== undefined) // deleted
) {
const response: GetConfigurationSettingResponse | undefined =
await this.#getConfigurationSetting(configurationSettingId, { onlyIfChanged: true });

const watcher: SettingWatcher = this.#sentinels.get(watchedSetting)!; // watcher should always exist for sentinels
const isDeleted = response === undefined && watcher.etag !== undefined; // previously existed, now deleted
const isChanged = response && response.statusCode === 200 && watcher.etag !== response.etag; // etag changed

if (isDeleted || isChanged) {
changedSentinel = watchedSetting;
changedSentinelWatcher = watcher;
changedSentinelWatcher = { etag: isChanged ? response.etag : undefined };
needRefresh = true;
break;
}
Expand All @@ -655,7 +664,11 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
await adapter.onChangeDetected();
}
await this.#loadSelectedKeyValues();
this.#sentinels.set(changedSentinel, changedSentinelWatcher); // update the changed sentinel's watcher

if (changedSentinel && changedSentinelWatcher) {
// update the changed sentinel's watcher after loading new values, this can ensure a failed refresh will retry on next refresh
this.#sentinels.set(changedSentinel, changedSentinelWatcher);
}
}

this.#kvRefreshTimer.reset();
Expand All @@ -672,7 +685,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
return Promise.resolve(false);
}

const needRefresh = await this.#checkConfigurationSettingsChange(this.#ffSelectors);
let needRefresh = false;
needRefresh = await this.#checkConfigurationSettingsChange(this.#ffSelectors);

if (needRefresh) {
await this.#loadFeatureFlags();
}
Expand Down Expand Up @@ -715,21 +730,33 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
const listOptions: ListConfigurationSettingsOptions = {
keyFilter: selector.keyFilter,
labelFilter: selector.labelFilter,
tagsFilter: selector.tagFilters,
pageEtags: pageWatchers.map(w => w.etag ?? "")
tagsFilter: selector.tagFilters
};

if (!this.#isAfdUsed) {
// if AFD is not used, add page etags to the listOptions to send conditional request
listOptions.pageEtags = pageWatchers.map(w => w.etag ?? "") ;
}

const pageIterator = listConfigurationSettingsWithTrace(
this.#requestTraceOptions,
client,
listOptions
).byPage();

let i = 0;
for await (const page of pageIterator) {
// when conditional request is sent, the response will be 304 if not changed
if (page._response.status === 200) { // created or changed
const timestamp = this.#getResponseTimestamp(page);
if (i >= pageWatchers.length) {
return true;
}

const lastServerResponseTime = pageWatchers[i].lastServerResponseTime;
const isUpToDate = lastServerResponseTime ? timestamp > lastServerResponseTime : true;
if (isUpToDate && page._response.status === 200 && page.etag !== pageWatchers[i].etag) {
return true;
}
i++;
}
}
return false;
Expand All @@ -740,7 +767,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
}

/**
* Gets a configuration setting by key and label.If the setting is not found, return undefine instead of throwing an error.
* Gets a configuration setting by key and label. If the setting is not found, return undefined instead of throwing an error.
*/
async #getConfigurationSetting(configurationSettingId: ConfigurationSettingId, getOptions?: GetConfigurationSettingOptions): Promise<GetConfigurationSettingResponse | undefined> {
const funcToExecute = async (client) => {
Expand Down Expand Up @@ -776,7 +803,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {

const items: ConfigurationSetting[] = [];
for await (const page of pageIterator) {
pageWatchers.push({ etag: page.etag });
pageWatchers.push({ etag: page.etag, lastServerResponseTime: this.#getResponseTimestamp(page) });
items.push(...page.items);
}
return { items, pageWatchers };
Expand Down Expand Up @@ -1104,6 +1131,19 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
return first15Bytes.toString("base64url");
}
}

/**
* Extracts the response timestamp from the headers. If not found, returns the current time.
*/
#getResponseTimestamp(response: GetConfigurationSettingResponse | ListConfigurationSettingPage | RestError): Date {
let header: string | undefined;
if (isRestError(response)) {
header = response.response?.headers?.get(SERVER_TIMESTAMP_HEADER) ?? undefined;
} else {
header = response._response?.headers?.get(SERVER_TIMESTAMP_HEADER) ?? undefined;
}
return header ? new Date(header) : new Date();
}
}

function getValidSettingSelectors(selectors: SettingSelector[]): SettingSelector[] {
Expand Down
3 changes: 3 additions & 0 deletions src/common/errorMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export const enum ErrorMessages {
INVALID_LABEL_FILTER = "The characters '*' and ',' are not supported in label filters.",
INVALID_TAG_FILTER = "Tag filter must follow the format 'tagName=tagValue'",
CONNECTION_STRING_OR_ENDPOINT_MISSED = "A connection string or an endpoint with credential must be specified to create a client.",
REPLICA_DISCOVERY_NOT_SUPPORTED = "Replica discovery is not supported when loading from Azure Front Door.",
LOAD_BALANCING_NOT_SUPPORTED = "Load balancing is not supported when loading from Azure Front Door.",
WATCHED_SETTINGS_NOT_SUPPORTED = "Specifying watched settings is not supported when loading from Azure Front Door. If refresh is enabled, all loaded configuration settings will be watched automatically."
}

export const enum KeyVaultReferenceErrorMessages {
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@

export { AzureAppConfiguration } from "./appConfiguration.js";
export { Disposable } from "./common/disposable.js";
export { load } from "./load.js";
export { load, loadFromAzureFrontDoor } from "./load.js";
export { KeyFilter, LabelFilter } from "./types.js";
export { VERSION } from "./version.js";
Loading