UNPKG

@azure/app-configuration

Version:

An isomorphic client library for the Azure App Configuration service.

117 lines 4.33 kB
// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. import { logger } from "../logger.js"; /** * The sync token header, as described here: * https://learn.microsoft.com/azure/azure-app-configuration/rest-api-consistency * @internal */ export const SyncTokenHeaderName = "sync-token"; /** * A policy factory for injecting sync tokens properly into outgoing requests. * @param syncTokens - the sync tokens store to be used across requests. * @internal */ export function syncTokenPolicy(syncTokens) { return { name: "Sync Token Policy", async sendRequest(request, next) { const syncTokenHeaderValue = syncTokens.getSyncTokenHeaderValue(); if (syncTokenHeaderValue) { logger.info("[syncTokenPolicy] Setting headers with ${SyncTokenHeaderName} and ${syncTokenHeaderValue}"); request.headers.set(SyncTokenHeaderName, syncTokenHeaderValue); } const response = await next(request); syncTokens.addSyncTokenFromHeaderValue(response.headers.get(SyncTokenHeaderName)); return response; }, }; } /** * Sync token tracker (allows for real-time consistency, even in the face of * caching and load balancing within App Configuration). * * (protocol and format described here) * https://learn.microsoft.com/azure/azure-app-configuration/rest-api-consistency * * @internal */ export class SyncTokens { constructor() { this._currentSyncTokens = new Map(); } /** * Takes the value from the header named after the constant `SyncTokenHeaderName` * and adds it to our list of accumulated sync tokens. * * If given an empty value (or undefined) it clears the current list of sync tokens. * (indicates the service has properly absorbed values into the cluster). * * @param syncTokenHeaderValue - The full value of the sync token header. */ addSyncTokenFromHeaderValue(syncTokenHeaderValue) { if (syncTokenHeaderValue == null || syncTokenHeaderValue === "") { // eventually everything gets synced up and we don't have to track // these headers anymore this._currentSyncTokens.clear(); return; } const newTokens = syncTokenHeaderValue.split(",").map(parseSyncToken); for (const newToken of newTokens) { const existingToken = this._currentSyncTokens.get(newToken.id); if (!existingToken || existingToken.sequenceNumber < newToken.sequenceNumber) { this._currentSyncTokens.set(newToken.id, newToken); continue; } } } /** * Gets a properly formatted SyncToken header value. */ getSyncTokenHeaderValue() { if (this._currentSyncTokens.size === 0) { return undefined; } const syncTokenStrings = []; for (const syncToken of this._currentSyncTokens.values()) { // note that you don't include the 'sn' field here - that's only // used for internal tracking of the 'version' for the token itself syncTokenStrings.push(`${syncToken.id}=${syncToken.value}`); } return syncTokenStrings.join(","); } } // An example sync token (from their documentation): // // jtqGc1I4=MDoyOA==;sn=28 // // Which breaks down to: // id: jtqGc1I4 // value: MDoyOA== // sequence number: 28 const syncTokenRegex = /^([^=]+)=([^;]+);sn=(\d+)$/; /** * Parses a single sync token into it's constituent parts. * * @param syncToken - A single sync token. * * @internal */ export function parseSyncToken(syncToken) { const matches = syncToken.match(syncTokenRegex); if (matches == null) { throw new Error(`Failed to parse sync token '${syncToken}' with regex ${syncTokenRegex.source}`); } const sequenceNumber = parseInt(matches[3], 10); if (isNaN(sequenceNumber)) { // this should be impossible since our regex restricts to just digits // but there's nothing wrong with being thorough. throw new Error(`${syncToken}: The sequence number value '${matches[3]}' wasn't a number`); } return { id: matches[1], value: matches[2], sequenceNumber, }; } //# sourceMappingURL=synctokenpolicy.js.map