Skip to content

Commit d7ef349

Browse files
AndyBitzdferber90
andauthored
[edge-config] Add consistentRead option to client (#735)
* [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]>
1 parent c158aed commit d7ef349

File tree

3 files changed

+85
-22
lines changed

3 files changed

+85
-22
lines changed

.changeset/polite-taxis-repair.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@vercel/edge-config': minor
3+
---
4+
5+
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.

packages/edge-config/src/index.ts

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
EdgeConfigItems,
1717
EdgeConfigValue,
1818
EmbeddedEdgeConfig,
19+
EdgeConfigFunctionsOptions,
1920
} from './types';
2021
import { fetchWithCachedResponse } from './utils/fetch-with-cached-response';
2122
import { trace } from './utils/tracing';
@@ -125,7 +126,9 @@ function createGetInMemoryEdgeConfig(
125126
connection: Connection,
126127
headers: Record<string, string>,
127128
fetchCache: EdgeConfigClientOptions['cache'],
128-
): () => Promise<EmbeddedEdgeConfig | null> {
129+
): (
130+
localOptions?: EdgeConfigFunctionsOptions,
131+
) => Promise<EmbeddedEdgeConfig | null> {
129132
// Functions as cache to keep track of the Edge Config.
130133
let embeddedEdgeConfigPromise: Promise<EmbeddedEdgeConfig | null> | null =
131134
null;
@@ -137,8 +140,9 @@ function createGetInMemoryEdgeConfig(
137140
let latestRequest: Promise<EmbeddedEdgeConfig | null> | null = null;
138141

139142
return trace(
140-
() => {
141-
if (!shouldUseDevelopmentCache) return Promise.resolve(null);
143+
(localOptions) => {
144+
if (localOptions?.consistentRead || !shouldUseDevelopmentCache)
145+
return Promise.resolve(null);
142146

143147
if (!latestRequest) {
144148
latestRequest = fetchWithCachedResponse(
@@ -196,11 +200,23 @@ function createGetInMemoryEdgeConfig(
196200
}
197201

198202
/**
199-
*
203+
* Uses `MAX_SAFE_INTEGER` as minimum updated at timestamp to force
204+
* a request to the origin.
205+
*/
206+
function addConsistentReadHeader(headers: Headers): void {
207+
headers.set('x-edge-config-min-updated-at', `${Number.MAX_SAFE_INTEGER}`);
208+
}
209+
210+
/**
211+
* Reads the Edge Config from a local provider, if available,
212+
* to avoid Network requests.
200213
*/
201214
async function getLocalEdgeConfig(
202215
connection: Connection,
216+
options?: EdgeConfigFunctionsOptions,
203217
): Promise<EmbeddedEdgeConfig | null> {
218+
if (options?.consistentRead) return null;
219+
204220
const edgeConfig =
205221
(await getPrivateEdgeConfig(connection)) ||
206222
(await getFileSystemEdgeConfig(connection));
@@ -329,10 +345,11 @@ export const createClient = trace(
329345
get: trace(
330346
async function get<T = EdgeConfigValue>(
331347
key: string,
348+
localOptions?: EdgeConfigFunctionsOptions,
332349
): Promise<T | undefined> {
333350
const localEdgeConfig =
334-
(await getInMemoryEdgeConfig()) ||
335-
(await getLocalEdgeConfig(connection));
351+
(await getInMemoryEdgeConfig(localOptions)) ||
352+
(await getLocalEdgeConfig(connection, localOptions));
336353

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

365+
const localHeaders = new Headers(headers);
366+
if (localOptions?.consistentRead)
367+
addConsistentReadHeader(localHeaders);
368+
348369
return fetchWithCachedResponse(
349370
`${baseUrl}/item/${key}?version=${version}`,
350371
{
351-
headers: new Headers(headers),
372+
headers: localHeaders,
352373
cache: fetchCache,
353374
},
354375
).then<T | undefined, undefined>(async (res) => {
@@ -372,10 +393,13 @@ export const createClient = trace(
372393
{ name: 'get', isVerboseTrace: false, attributes: { edgeConfigId } },
373394
),
374395
has: trace(
375-
async function has(key): Promise<boolean> {
396+
async function has(
397+
key,
398+
localOptions?: EdgeConfigFunctionsOptions,
399+
): Promise<boolean> {
376400
const localEdgeConfig =
377-
(await getInMemoryEdgeConfig()) ||
378-
(await getLocalEdgeConfig(connection));
401+
(await getInMemoryEdgeConfig(localOptions)) ||
402+
(await getLocalEdgeConfig(connection, localOptions));
379403

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

411+
const localHeaders = new Headers(headers);
412+
if (localOptions?.consistentRead)
413+
addConsistentReadHeader(localHeaders);
414+
387415
// this is a HEAD request anyhow, no need for fetchWithCachedResponse
388416
return fetch(`${baseUrl}/item/${key}?version=${version}`, {
389417
method: 'HEAD',
390-
headers: new Headers(headers),
418+
headers: localHeaders,
391419
cache: fetchCache,
392420
}).then((res) => {
393421
if (res.status === 401) throw new Error(ERRORS.UNAUTHORIZED);
@@ -408,10 +436,11 @@ export const createClient = trace(
408436
getAll: trace(
409437
async function getAll<T = EdgeConfigItems>(
410438
keys?: (keyof T)[],
439+
localOptions?: EdgeConfigFunctionsOptions,
411440
): Promise<T> {
412441
const localEdgeConfig =
413-
(await getInMemoryEdgeConfig()) ||
414-
(await getLocalEdgeConfig(connection));
442+
(await getInMemoryEdgeConfig(localOptions)) ||
443+
(await getLocalEdgeConfig(connection, localOptions));
415444

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

468+
const localHeaders = new Headers(headers);
469+
if (localOptions?.consistentRead)
470+
addConsistentReadHeader(localHeaders);
471+
439472
return fetchWithCachedResponse(
440473
`${baseUrl}/items?version=${version}${
441474
search === null ? '' : `&${search}`
442475
}`,
443476
{
444-
headers: new Headers(headers),
477+
headers: localHeaders,
445478
cache: fetchCache,
446479
},
447480
).then<T>(async (res) => {
@@ -461,19 +494,25 @@ export const createClient = trace(
461494
{ name: 'getAll', isVerboseTrace: false, attributes: { edgeConfigId } },
462495
),
463496
digest: trace(
464-
async function digest(): Promise<string> {
497+
async function digest(
498+
localOptions?: EdgeConfigFunctionsOptions,
499+
): Promise<string> {
465500
const localEdgeConfig =
466-
(await getInMemoryEdgeConfig()) ||
467-
(await getLocalEdgeConfig(connection));
501+
(await getInMemoryEdgeConfig(localOptions)) ||
502+
(await getLocalEdgeConfig(connection, localOptions));
468503

469504
if (localEdgeConfig) {
470505
return Promise.resolve(localEdgeConfig.digest);
471506
}
472507

508+
const localHeaders = new Headers(headers);
509+
if (localOptions?.consistentRead)
510+
addConsistentReadHeader(localHeaders);
511+
473512
return fetchWithCachedResponse(
474513
`${baseUrl}/digest?version=${version}`,
475514
{
476-
headers: new Headers(headers),
515+
headers: localHeaders,
477516
cache: fetchCache,
478517
},
479518
).then(async (res) => {

packages/edge-config/src/types.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ export interface EdgeConfigClient {
3838
* @param key - the key to read
3939
* @returns the value stored under the given key, or undefined
4040
*/
41-
get: <T = EdgeConfigValue>(key: string) => Promise<T | undefined>;
41+
get: <T = EdgeConfigValue>(
42+
key: string,
43+
options?: EdgeConfigFunctionsOptions,
44+
) => Promise<T | undefined>;
4245
/**
4346
* Reads multiple or all values.
4447
*
@@ -47,22 +50,25 @@ export interface EdgeConfigClient {
4750
* @param keys - the keys to read
4851
* @returns Returns all entries when called with no arguments or only entries matching the given keys otherwise.
4952
*/
50-
getAll: <T = EdgeConfigItems>(keys?: (keyof T)[]) => Promise<T>;
53+
getAll: <T = EdgeConfigItems>(
54+
keys?: (keyof T)[],
55+
options?: EdgeConfigFunctionsOptions,
56+
) => Promise<T>;
5157
/**
5258
* Check if a given key exists in the Edge Config.
5359
*
5460
* @param key - the key to check
5561
* @returns true if the given key exists in the Edge Config.
5662
*/
57-
has: (key: string) => Promise<boolean>;
63+
has: (key: string, options?: EdgeConfigFunctionsOptions) => Promise<boolean>;
5864
/**
5965
* Get the digest of the Edge Config.
6066
*
6167
* The digest is a unique hash result based on the contents stored in the Edge Config.
6268
*
6369
* @returns The digest of the Edge Config.
6470
*/
65-
digest: () => Promise<string>;
71+
digest: (options?: EdgeConfigFunctionsOptions) => Promise<string>;
6672
}
6773

6874
export type EdgeConfigItems = Record<string, EdgeConfigValue>;
@@ -73,3 +79,16 @@ export type EdgeConfigValue =
7379
| null
7480
| { [x: string]: EdgeConfigValue }
7581
| EdgeConfigValue[];
82+
83+
export interface EdgeConfigFunctionsOptions {
84+
/**
85+
* Enabling `consistentRead` will bypass all caches and hit the origin
86+
* directly. This will make sure to fetch the most recent version of
87+
* an Edge Config with the downside of an increased latency.
88+
*
89+
* We do **not** recommend enabling this option, unless you are reading
90+
* Edge Config specifically for generating a page using ISR and you
91+
* need to ensure you generate with the latest content.
92+
*/
93+
consistentRead?: boolean;
94+
}

0 commit comments

Comments
 (0)