Skip to content

Commit

Permalink
[edge-config] Add consistentRead option to client (#735)
Browse files Browse the repository at this point in the history
* [edge-config] Add `consistentRead` option to client

* Add changeset

* Remove from client and add to each function

* Add return type

* Update packages/edge-config/src/index.ts

Co-authored-by: Dominik Ferber <[email protected]>

* Add comments

* Update types

* Update changeset

---------

Co-authored-by: Dominik Ferber <[email protected]>
  • Loading branch information
AndyBitz and dferber90 authored Nov 4, 2024
1 parent c158aed commit d7ef349
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 22 deletions.
5 changes: 5 additions & 0 deletions .changeset/polite-taxis-repair.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@vercel/edge-config': minor
---

Add the `consistentRead` option to allow reading from the origin. Note that it's not recommended to use this property without good reason due to the extrem performance cost.
75 changes: 57 additions & 18 deletions packages/edge-config/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
EdgeConfigItems,
EdgeConfigValue,
EmbeddedEdgeConfig,
EdgeConfigFunctionsOptions,
} from './types';
import { fetchWithCachedResponse } from './utils/fetch-with-cached-response';
import { trace } from './utils/tracing';
Expand Down Expand Up @@ -125,7 +126,9 @@ function createGetInMemoryEdgeConfig(
connection: Connection,
headers: Record<string, string>,
fetchCache: EdgeConfigClientOptions['cache'],
): () => Promise<EmbeddedEdgeConfig | null> {
): (
localOptions?: EdgeConfigFunctionsOptions,
) => Promise<EmbeddedEdgeConfig | null> {
// Functions as cache to keep track of the Edge Config.
let embeddedEdgeConfigPromise: Promise<EmbeddedEdgeConfig | null> | null =
null;
Expand All @@ -137,8 +140,9 @@ function createGetInMemoryEdgeConfig(
let latestRequest: Promise<EmbeddedEdgeConfig | null> | null = null;

return trace(
() => {
if (!shouldUseDevelopmentCache) return Promise.resolve(null);
(localOptions) => {
if (localOptions?.consistentRead || !shouldUseDevelopmentCache)
return Promise.resolve(null);

if (!latestRequest) {
latestRequest = fetchWithCachedResponse(
Expand Down Expand Up @@ -196,11 +200,23 @@ function createGetInMemoryEdgeConfig(
}

/**
*
* Uses `MAX_SAFE_INTEGER` as minimum updated at timestamp to force
* a request to the origin.
*/
function addConsistentReadHeader(headers: Headers): void {
headers.set('x-edge-config-min-updated-at', `${Number.MAX_SAFE_INTEGER}`);
}

/**
* Reads the Edge Config from a local provider, if available,
* to avoid Network requests.
*/
async function getLocalEdgeConfig(
connection: Connection,
options?: EdgeConfigFunctionsOptions,
): Promise<EmbeddedEdgeConfig | null> {
if (options?.consistentRead) return null;

const edgeConfig =
(await getPrivateEdgeConfig(connection)) ||
(await getFileSystemEdgeConfig(connection));
Expand Down Expand Up @@ -329,10 +345,11 @@ export const createClient = trace(
get: trace(
async function get<T = EdgeConfigValue>(
key: string,
localOptions?: EdgeConfigFunctionsOptions,
): Promise<T | undefined> {
const localEdgeConfig =
(await getInMemoryEdgeConfig()) ||
(await getLocalEdgeConfig(connection));
(await getInMemoryEdgeConfig(localOptions)) ||
(await getLocalEdgeConfig(connection, localOptions));

assertIsKey(key);
if (isEmptyKey(key)) return undefined;
Expand All @@ -345,10 +362,14 @@ export const createClient = trace(
return Promise.resolve(localEdgeConfig.items[key] as T);
}

const localHeaders = new Headers(headers);
if (localOptions?.consistentRead)
addConsistentReadHeader(localHeaders);

return fetchWithCachedResponse(
`${baseUrl}/item/${key}?version=${version}`,
{
headers: new Headers(headers),
headers: localHeaders,
cache: fetchCache,
},
).then<T | undefined, undefined>(async (res) => {
Expand All @@ -372,10 +393,13 @@ export const createClient = trace(
{ name: 'get', isVerboseTrace: false, attributes: { edgeConfigId } },
),
has: trace(
async function has(key): Promise<boolean> {
async function has(
key,
localOptions?: EdgeConfigFunctionsOptions,
): Promise<boolean> {
const localEdgeConfig =
(await getInMemoryEdgeConfig()) ||
(await getLocalEdgeConfig(connection));
(await getInMemoryEdgeConfig(localOptions)) ||
(await getLocalEdgeConfig(connection, localOptions));

assertIsKey(key);
if (isEmptyKey(key)) return false;
Expand All @@ -384,10 +408,14 @@ export const createClient = trace(
return Promise.resolve(hasOwnProperty(localEdgeConfig.items, key));
}

const localHeaders = new Headers(headers);
if (localOptions?.consistentRead)
addConsistentReadHeader(localHeaders);

// this is a HEAD request anyhow, no need for fetchWithCachedResponse
return fetch(`${baseUrl}/item/${key}?version=${version}`, {
method: 'HEAD',
headers: new Headers(headers),
headers: localHeaders,
cache: fetchCache,
}).then((res) => {
if (res.status === 401) throw new Error(ERRORS.UNAUTHORIZED);
Expand All @@ -408,10 +436,11 @@ export const createClient = trace(
getAll: trace(
async function getAll<T = EdgeConfigItems>(
keys?: (keyof T)[],
localOptions?: EdgeConfigFunctionsOptions,
): Promise<T> {
const localEdgeConfig =
(await getInMemoryEdgeConfig()) ||
(await getLocalEdgeConfig(connection));
(await getInMemoryEdgeConfig(localOptions)) ||
(await getLocalEdgeConfig(connection, localOptions));

if (localEdgeConfig) {
if (keys === undefined) {
Expand All @@ -436,12 +465,16 @@ export const createClient = trace(
// so skip the request and return an empty object
if (search === '') return Promise.resolve({} as T);

const localHeaders = new Headers(headers);
if (localOptions?.consistentRead)
addConsistentReadHeader(localHeaders);

return fetchWithCachedResponse(
`${baseUrl}/items?version=${version}${
search === null ? '' : `&${search}`
}`,
{
headers: new Headers(headers),
headers: localHeaders,
cache: fetchCache,
},
).then<T>(async (res) => {
Expand All @@ -461,19 +494,25 @@ export const createClient = trace(
{ name: 'getAll', isVerboseTrace: false, attributes: { edgeConfigId } },
),
digest: trace(
async function digest(): Promise<string> {
async function digest(
localOptions?: EdgeConfigFunctionsOptions,
): Promise<string> {
const localEdgeConfig =
(await getInMemoryEdgeConfig()) ||
(await getLocalEdgeConfig(connection));
(await getInMemoryEdgeConfig(localOptions)) ||
(await getLocalEdgeConfig(connection, localOptions));

if (localEdgeConfig) {
return Promise.resolve(localEdgeConfig.digest);
}

const localHeaders = new Headers(headers);
if (localOptions?.consistentRead)
addConsistentReadHeader(localHeaders);

return fetchWithCachedResponse(
`${baseUrl}/digest?version=${version}`,
{
headers: new Headers(headers),
headers: localHeaders,
cache: fetchCache,
},
).then(async (res) => {
Expand Down
27 changes: 23 additions & 4 deletions packages/edge-config/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ export interface EdgeConfigClient {
* @param key - the key to read
* @returns the value stored under the given key, or undefined
*/
get: <T = EdgeConfigValue>(key: string) => Promise<T | undefined>;
get: <T = EdgeConfigValue>(
key: string,
options?: EdgeConfigFunctionsOptions,
) => Promise<T | undefined>;
/**
* Reads multiple or all values.
*
Expand All @@ -47,22 +50,25 @@ export interface EdgeConfigClient {
* @param keys - the keys to read
* @returns Returns all entries when called with no arguments or only entries matching the given keys otherwise.
*/
getAll: <T = EdgeConfigItems>(keys?: (keyof T)[]) => Promise<T>;
getAll: <T = EdgeConfigItems>(
keys?: (keyof T)[],
options?: EdgeConfigFunctionsOptions,
) => Promise<T>;
/**
* Check if a given key exists in the Edge Config.
*
* @param key - the key to check
* @returns true if the given key exists in the Edge Config.
*/
has: (key: string) => Promise<boolean>;
has: (key: string, options?: EdgeConfigFunctionsOptions) => Promise<boolean>;
/**
* Get the digest of the Edge Config.
*
* The digest is a unique hash result based on the contents stored in the Edge Config.
*
* @returns The digest of the Edge Config.
*/
digest: () => Promise<string>;
digest: (options?: EdgeConfigFunctionsOptions) => Promise<string>;
}

export type EdgeConfigItems = Record<string, EdgeConfigValue>;
Expand All @@ -73,3 +79,16 @@ export type EdgeConfigValue =
| null
| { [x: string]: EdgeConfigValue }
| EdgeConfigValue[];

export interface EdgeConfigFunctionsOptions {
/**
* Enabling `consistentRead` will bypass all caches and hit the origin
* directly. This will make sure to fetch the most recent version of
* an Edge Config with the downside of an increased latency.
*
* We do **not** recommend enabling this option, unless you are reading
* Edge Config specifically for generating a page using ISR and you
* need to ensure you generate with the latest content.
*/
consistentRead?: boolean;
}

0 comments on commit d7ef349

Please sign in to comment.