Skip to content

Commit 7348a0c

Browse files
Merge branch 'zhiyuanliang/snapshot-reference' of https://github.com/Azure/AppConfiguration-JavaScriptProvider into zhiyuanliang/snapshot-reference
2 parents 2b929c2 + d8e8c6c commit 7348a0c

File tree

3 files changed

+138
-65
lines changed

3 files changed

+138
-65
lines changed

src/appConfigurationImpl.ts

Lines changed: 131 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
isFeatureFlag,
1313
isSecretReference,
1414
GetSnapshotOptions,
15+
ListConfigurationSettingsForSnapshotOptions,
1516
GetSnapshotResponse,
1617
KnownSnapshotComposition
1718
} from "@azure/app-configuration";
@@ -81,6 +82,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
8182
#featureFlagTracing: FeatureFlagTracingOptions | undefined;
8283
#fmVersion: string | undefined;
8384
#aiConfigurationTracing: AIConfigurationTracingOptions | undefined;
85+
#useSnapshotReference: boolean = false;
8486

8587
// Refresh
8688
#refreshInProgress: boolean = false;
@@ -212,7 +214,8 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
212214
isFailoverRequest: this.#isFailoverRequest,
213215
featureFlagTracing: this.#featureFlagTracing,
214216
fmVersion: this.#fmVersion,
215-
aiConfigurationTracing: this.#aiConfigurationTracing
217+
aiConfigurationTracing: this.#aiConfigurationTracing,
218+
useSnapshotReference: this.#useSnapshotReference
216219
};
217220
}
218221

@@ -482,71 +485,59 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
482485
*/
483486
async #loadConfigurationSettings(loadFeatureFlag: boolean = false): Promise<ConfigurationSetting[]> {
484487
const selectors = loadFeatureFlag ? this.#ffSelectors : this.#kvSelectors;
485-
const funcToExecute = async (client) => {
486-
// Use a Map to deduplicate configuration settings by key. When multiple selectors return settings with the same key,
487-
// the configuration setting loaded by the later selector in the iteration order will override the one from the earlier selector.
488-
const loadedSettings: Map<string, ConfigurationSetting> = new Map<string, ConfigurationSetting>();
489-
// deep copy selectors to avoid modification if current client fails
490-
const selectorsToUpdate: PagedSettingsWatcher[] = JSON.parse(
491-
JSON.stringify(selectors)
492-
);
493488

494-
for (const selector of selectorsToUpdate) {
495-
if (selector.snapshotName === undefined) {
496-
const listOptions: ListConfigurationSettingsOptions = {
497-
keyFilter: selector.keyFilter,
498-
labelFilter: selector.labelFilter,
499-
tagsFilter: selector.tagFilters
500-
};
501-
const pageWatchers: SettingWatcher[] = [];
502-
const pageIterator = listConfigurationSettingsWithTrace(
503-
this.#requestTraceOptions,
504-
client,
505-
listOptions
506-
).byPage();
507-
508-
for await (const page of pageIterator) {
509-
pageWatchers.push({ etag: page.etag });
510-
for (const setting of page.items) {
511-
if (loadFeatureFlag === isFeatureFlag(setting)) {
512-
loadedSettings.set(setting.key, setting);
513-
}
514-
}
515-
}
516-
selector.pageWatchers = pageWatchers;
517-
} else { // snapshot selector
518-
const snapshot = await this.#getSnapshot(selector.snapshotName);
519-
if (snapshot === undefined) {
520-
throw new InvalidOperationError(`Could not find snapshot with name ${selector.snapshotName}.`);
521-
}
522-
if (snapshot.compositionType != KnownSnapshotComposition.Key) {
523-
throw new InvalidOperationError(`Composition type for the selected snapshot with name ${selector.snapshotName} must be 'key'.`);
524-
}
525-
const pageIterator = listConfigurationSettingsForSnapshotWithTrace(
526-
this.#requestTraceOptions,
527-
client,
528-
selector.snapshotName
529-
).byPage();
530-
531-
for await (const page of pageIterator) {
532-
for (const setting of page.items) {
533-
if (loadFeatureFlag === isFeatureFlag(setting)) {
534-
loadedSettings.set(setting.key, setting);
535-
}
489+
// Use a Map to deduplicate configuration settings by key. When multiple selectors return settings with the same key,
490+
// the configuration setting loaded by the later selector in the iteration order will override the one from the earlier selector.
491+
const loadedSettings: Map<string, ConfigurationSetting> = new Map<string, ConfigurationSetting>();
492+
// deep copy selectors to avoid modification if current client fails
493+
const selectorsToUpdate: PagedSettingsWatcher[] = JSON.parse(
494+
JSON.stringify(selectors)
495+
);
496+
497+
for (const selector of selectorsToUpdate) {
498+
let settings: ConfigurationSetting[] = [];
499+
if (selector.snapshotName === undefined) {
500+
const listOptions: ListConfigurationSettingsOptions = {
501+
keyFilter: selector.keyFilter,
502+
labelFilter: selector.labelFilter,
503+
tagsFilter: selector.tagFilters
504+
};
505+
const { items, pageWatchers } = await this.#listConfigurationSettings(listOptions);
506+
selector.pageWatchers = pageWatchers;
507+
settings = items;
508+
} else { // snapshot selector
509+
settings = await this.#loadConfigurationSettingsFromSnapshot(selector.snapshotName);
510+
}
511+
512+
for (const setting of settings) {
513+
if (isSnapshotReference(setting) && !loadFeatureFlag) {
514+
this.#useSnapshotReference = true;
515+
516+
// TODO: When SDK supports snapshot reference, use the helper method from SDK.
517+
const snapshotName = parseSnapshotReference(setting).value.snapshotName;
518+
const settingsFromSnapshot = await this.#loadConfigurationSettingsFromSnapshot(snapshotName);
519+
520+
for (const snapshotSetting of settingsFromSnapshot) {
521+
if (!isFeatureFlag(snapshotSetting)) {
522+
// Feature flags inside snapshot are ignored. This is consistent the behavior that key value selectors ignore feature flags.
523+
loadedSettings.set(snapshotSetting.key, snapshotSetting);
536524
}
537525
}
526+
continue;
538527
}
539-
}
540528

541-
if (loadFeatureFlag) {
542-
this.#ffSelectors = selectorsToUpdate;
543-
} else {
544-
this.#kvSelectors = selectorsToUpdate;
529+
if (loadFeatureFlag === isFeatureFlag(setting)) {
530+
loadedSettings.set(setting.key, setting);
531+
}
545532
}
546-
return Array.from(loadedSettings.values());
547-
};
533+
}
548534

549-
return await this.#executeWithFailoverPolicy(funcToExecute) as ConfigurationSetting[];
535+
if (loadFeatureFlag) {
536+
this.#ffSelectors = selectorsToUpdate;
537+
} else {
538+
this.#kvSelectors = selectorsToUpdate;
539+
}
540+
return Array.from(loadedSettings.values());
550541
}
551542

552543
/**
@@ -595,6 +586,18 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
595586
}
596587
}
597588

589+
async #loadConfigurationSettingsFromSnapshot(snapshotName: string): Promise<ConfigurationSetting[]> {
590+
const snapshot = await this.#getSnapshot(snapshotName);
591+
if (snapshot === undefined) {
592+
throw new InvalidOperationError(`Could not find snapshot with name ${snapshotName}.`);
593+
}
594+
if (snapshot.compositionType != KnownSnapshotComposition.Key) {
595+
throw new InvalidOperationError(`Composition type for the selected snapshot with name ${snapshotName} must be 'key'.`);
596+
}
597+
const settings: ConfigurationSetting[] = await this.#listConfigurationSettingsForSnapshot(snapshotName);
598+
return settings;
599+
}
600+
598601
/**
599602
* Clears all existing key-values in the local configuration except feature flags.
600603
*/
@@ -754,13 +757,13 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
754757
/**
755758
* Gets a configuration setting by key and label.If the setting is not found, return undefine instead of throwing an error.
756759
*/
757-
async #getConfigurationSetting(configurationSettingId: ConfigurationSettingId, customOptions?: GetConfigurationSettingOptions): Promise<GetConfigurationSettingResponse | undefined> {
760+
async #getConfigurationSetting(configurationSettingId: ConfigurationSettingId, getOptions?: GetConfigurationSettingOptions): Promise<GetConfigurationSettingResponse | undefined> {
758761
const funcToExecute = async (client) => {
759762
return getConfigurationSettingWithTrace(
760763
this.#requestTraceOptions,
761764
client,
762765
configurationSettingId,
763-
customOptions
766+
getOptions
764767
);
765768
};
766769

@@ -777,13 +780,33 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
777780
return response;
778781
}
779782

780-
async #getSnapshot(snapshotName: string, customOptions?: GetSnapshotOptions): Promise<GetSnapshotResponse | undefined> {
783+
async #listConfigurationSettings(listOptions: ListConfigurationSettingsOptions): Promise<{ items: ConfigurationSetting[]; pageWatchers: SettingWatcher[] }> {
784+
const funcToExecute = async (client) => {
785+
const pageWatchers: SettingWatcher[] = [];
786+
const pageIterator = listConfigurationSettingsWithTrace(
787+
this.#requestTraceOptions,
788+
client,
789+
listOptions
790+
).byPage();
791+
792+
const items: ConfigurationSetting[] = [];
793+
for await (const page of pageIterator) {
794+
pageWatchers.push({ etag: page.etag });
795+
items.push(...page.items);
796+
}
797+
return { items, pageWatchers };
798+
};
799+
800+
return await this.#executeWithFailoverPolicy(funcToExecute);
801+
}
802+
803+
async #getSnapshot(snapshotName: string, getOptions?: GetSnapshotOptions): Promise<GetSnapshotResponse | undefined> {
781804
const funcToExecute = async (client) => {
782805
return getSnapshotWithTrace(
783806
this.#requestTraceOptions,
784807
client,
785808
snapshotName,
786-
customOptions
809+
getOptions
787810
);
788811
};
789812

@@ -800,6 +823,25 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
800823
return response;
801824
}
802825

826+
async #listConfigurationSettingsForSnapshot(snapshotName: string, listOptions?: ListConfigurationSettingsForSnapshotOptions): Promise<ConfigurationSetting[]> {
827+
const funcToExecute = async (client) => {
828+
const pageIterator = listConfigurationSettingsForSnapshotWithTrace(
829+
this.#requestTraceOptions,
830+
client,
831+
snapshotName,
832+
listOptions
833+
).byPage();
834+
835+
const items: ConfigurationSetting[] = [];
836+
for await (const page of pageIterator) {
837+
items.push(...page.items);
838+
}
839+
return items;
840+
};
841+
842+
return await this.#executeWithFailoverPolicy(funcToExecute);
843+
}
844+
803845
// Only operations related to Azure App Configuration should be executed with failover policy.
804846
async #executeWithFailoverPolicy(funcToExecute: (client: AppConfigurationClient) => Promise<any>): Promise<any> {
805847
let clientWrappers = await this.#clientManager.getClients();
@@ -875,7 +917,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
875917
#setAIConfigurationTracing(setting: ConfigurationSetting<string>): void {
876918
if (this.#requestTracingEnabled && this.#aiConfigurationTracing !== undefined) {
877919
const contentType = parseContentType(setting.contentType);
878-
// content type: "application/json; profile=\"https://azconfig.io/mime-profiles/ai\"""
920+
// content type: "application/json; profile=\"https://azconfig.io/mime-profiles/ai\""
879921
if (isJsonContentType(contentType) &&
880922
!isFeatureFlagContentType(contentType) &&
881923
!isSecretReferenceContentType(contentType)) {
@@ -1049,3 +1091,28 @@ function validateTagFilters(tagFilters: string[]): void {
10491091
}
10501092
}
10511093
}
1094+
1095+
// TODO: Temporary workaround until SDK supports snapshot reference
1096+
const snapshotReferenceContentType = "application/json; profile=\"https://azconfig.io/mime-profiles/snapshot-ref\"; charset=utf-8";
1097+
1098+
interface JsonSnapshotReferenceValue {
1099+
snapshot_name: string;
1100+
}
1101+
1102+
function isSnapshotReference(setting: ConfigurationSetting):
1103+
setting is ConfigurationSetting & Required<Pick<ConfigurationSetting, "value">> {
1104+
return (setting && setting.contentType === snapshotReferenceContentType && typeof setting.value === "string");
1105+
}
1106+
1107+
function parseSnapshotReference(setting: ConfigurationSetting) {
1108+
if (!isSnapshotReference(setting)) {
1109+
throw new Error(`Invalid snapshot reference: ${setting}`);
1110+
}
1111+
const jsonSnapshotReferenceValue = JSON.parse(setting.value) as JsonSnapshotReferenceValue;
1112+
1113+
const snapshotReference = {
1114+
...setting,
1115+
value: { snapshotName: jsonSnapshotReferenceValue.snapshot_name },
1116+
};
1117+
return snapshotReference;
1118+
}

src/requestTracing/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export const REPLICA_COUNT_KEY = "ReplicaCount";
5151
export const KEY_VAULT_CONFIGURED_TAG = "UsesKeyVault";
5252
export const KEY_VAULT_REFRESH_CONFIGURED_TAG = "RefreshesKeyVault";
5353
export const FAILOVER_REQUEST_TAG = "Failover";
54+
export const SNAPSHOT_REFERENCE_TAG = "SnapshotRef";
5455

5556
// Compact feature tags
5657
export const FEATURES_KEY = "Features";

src/requestTracing/utils.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ import {
4141
FM_VERSION_KEY,
4242
DELIMITER,
4343
AI_CONFIGURATION_TAG,
44-
AI_CHAT_COMPLETION_CONFIGURATION_TAG
44+
AI_CHAT_COMPLETION_CONFIGURATION_TAG,
45+
SNAPSHOT_REFERENCE_TAG
4546
} from "./constants.js";
4647

4748
export interface RequestTracingOptions {
@@ -53,6 +54,7 @@ export interface RequestTracingOptions {
5354
featureFlagTracing: FeatureFlagTracingOptions | undefined;
5455
fmVersion: string | undefined;
5556
aiConfigurationTracing: AIConfigurationTracingOptions | undefined;
57+
useSnapshotReference: boolean;
5658
}
5759

5860
// Utils
@@ -195,6 +197,9 @@ function createFeaturesString(requestTracingOptions: RequestTracingOptions): str
195197
if (requestTracingOptions.aiConfigurationTracing?.usesAIChatCompletionConfiguration) {
196198
tags.push(AI_CHAT_COMPLETION_CONFIGURATION_TAG);
197199
}
200+
if (requestTracingOptions.useSnapshotReference) {
201+
tags.push(SNAPSHOT_REFERENCE_TAG);
202+
}
198203
return tags.join(DELIMITER);
199204
}
200205

0 commit comments

Comments
 (0)