diff --git a/packages/edge-config/src/create-create-client.ts b/packages/edge-config/src/create-create-client.ts new file mode 100644 index 000000000..9ba170de2 --- /dev/null +++ b/packages/edge-config/src/create-create-client.ts @@ -0,0 +1,282 @@ +import { name as sdkName, version as sdkVersion } from '../package.json'; +import type * as deps from './edge-config'; +import type { + EdgeConfigClient, + EdgeConfigFunctionsOptions, + EdgeConfigItems, + EdgeConfigValue, + EmbeddedEdgeConfig, +} from './types'; +import { + assertIsKey, + assertIsKeys, + hasOwnProperty, + isEmptyKey, + parseConnectionString, + pick, +} from './utils'; +import { trace } from './utils/tracing'; + +type CreateClient = ( + connectionString: string | undefined, + options?: deps.EdgeConfigClientOptions, +) => EdgeConfigClient; + +export function createCreateClient({ + getInMemoryEdgeConfig, + getLocalEdgeConfig, + fetchEdgeConfigItem, + fetchEdgeConfigHas, + fetchAllEdgeConfigItem, + fetchEdgeConfigTrace, +}: { + getInMemoryEdgeConfig: typeof deps.getInMemoryEdgeConfig; + getLocalEdgeConfig: typeof deps.getLocalEdgeConfig; + fetchEdgeConfigItem: typeof deps.fetchEdgeConfigItem; + fetchEdgeConfigHas: typeof deps.fetchEdgeConfigHas; + fetchAllEdgeConfigItem: typeof deps.fetchAllEdgeConfigItem; + fetchEdgeConfigTrace: typeof deps.fetchEdgeConfigTrace; +}): CreateClient { + /** + * Create an Edge Config client. + * + * The client has multiple methods which allow you to read the Edge Config. + * + * If you need to programmatically write to an Edge Config, check out the [Update your Edge Config items](https://vercel.com/docs/storage/edge-config/vercel-api#update-your-edge-config-items) section. + * + * @param connectionString - A connection string. Usually you'd pass in `process.env.EDGE_CONFIG` here, which contains a connection string. + * @returns An Edge Config Client instance + */ + return trace( + function createClient( + connectionString, + options = { + staleIfError: 604800 /* one week */, + cache: 'no-store', + }, + ): EdgeConfigClient { + if (!connectionString) + throw new Error('@vercel/edge-config: No connection string provided'); + + const connection = parseConnectionString(connectionString); + + if (!connection) + throw new Error( + '@vercel/edge-config: Invalid connection string provided', + ); + + const edgeConfigId = connection.id; + const baseUrl = connection.baseUrl; + const version = connection.version; // version of the edge config read access api we talk to + const headers: Record = { + Authorization: `Bearer ${connection.token}`, + }; + + // eslint-disable-next-line @typescript-eslint/prefer-optional-chain -- [@vercel/style-guide@5 migration] + if (typeof process !== 'undefined' && process.env.VERCEL_ENV) + headers['x-edge-config-vercel-env'] = process.env.VERCEL_ENV; + + if (typeof sdkName === 'string' && typeof sdkVersion === 'string') + headers['x-edge-config-sdk'] = `${sdkName}@${sdkVersion}`; + + if (typeof options.staleIfError === 'number' && options.staleIfError > 0) + headers['cache-control'] = `stale-if-error=${options.staleIfError}`; + + const fetchCache = options.cache || 'no-store'; + + /** + * While in development we use SWR-like behavior for the api client to + * reduce latency. + */ + const shouldUseDevelopmentCache = + !options.disableDevelopmentCache && + process.env.NODE_ENV === 'development' && + process.env.EDGE_CONFIG_DISABLE_DEVELOPMENT_SWR !== '1'; + + const api: Omit = { + get: trace( + async function get( + key: string, + localOptions?: EdgeConfigFunctionsOptions, + ): Promise { + assertIsKey(key); + + let localEdgeConfig: EmbeddedEdgeConfig | null = null; + if (localOptions?.consistentRead) { + // fall through to fetching + } else if (shouldUseDevelopmentCache) { + localEdgeConfig = await getInMemoryEdgeConfig( + connectionString, + fetchCache, + options.staleIfError, + ); + } else { + localEdgeConfig = await getLocalEdgeConfig( + connection.type, + connection.id, + fetchCache, + ); + } + + if (localEdgeConfig) { + if (isEmptyKey(key)) return undefined; + // We need to return a clone of the value so users can't modify + // our original value, and so the reference changes. + // + // This makes it consistent with the real API. + return Promise.resolve(localEdgeConfig.items[key] as T); + } + + return fetchEdgeConfigItem( + baseUrl, + key, + version, + localOptions?.consistentRead, + headers, + fetchCache, + ); + }, + { name: 'get', isVerboseTrace: false, attributes: { edgeConfigId } }, + ), + has: trace( + async function has( + key, + localOptions?: EdgeConfigFunctionsOptions, + ): Promise { + assertIsKey(key); + if (isEmptyKey(key)) return false; + + let localEdgeConfig: EmbeddedEdgeConfig | null = null; + + if (localOptions?.consistentRead) { + // fall through to fetching + } else if (shouldUseDevelopmentCache) { + localEdgeConfig = await getInMemoryEdgeConfig( + connectionString, + fetchCache, + options.staleIfError, + ); + } else { + localEdgeConfig = await getLocalEdgeConfig( + connection.type, + connection.id, + fetchCache, + ); + } + + if (localEdgeConfig) { + return Promise.resolve( + hasOwnProperty(localEdgeConfig.items, key), + ); + } + + return fetchEdgeConfigHas( + baseUrl, + key, + version, + localOptions?.consistentRead, + headers, + fetchCache, + ); + }, + { name: 'has', isVerboseTrace: false, attributes: { edgeConfigId } }, + ), + getAll: trace( + async function getAll( + keys?: (keyof T)[], + localOptions?: EdgeConfigFunctionsOptions, + ): Promise { + if (keys) { + assertIsKeys(keys); + } + + let localEdgeConfig: EmbeddedEdgeConfig | null = null; + + if (localOptions?.consistentRead) { + // fall through to fetching + } else if (shouldUseDevelopmentCache) { + localEdgeConfig = await getInMemoryEdgeConfig( + connectionString, + fetchCache, + options.staleIfError, + ); + } else { + localEdgeConfig = await getLocalEdgeConfig( + connection.type, + connection.id, + fetchCache, + ); + } + + if (localEdgeConfig) { + if (keys === undefined) { + return Promise.resolve(localEdgeConfig.items as T); + } + + return Promise.resolve(pick(localEdgeConfig.items, keys) as T); + } + + return fetchAllEdgeConfigItem( + baseUrl, + keys, + version, + localOptions?.consistentRead, + headers, + fetchCache, + ); + }, + { + name: 'getAll', + isVerboseTrace: false, + attributes: { edgeConfigId }, + }, + ), + digest: trace( + async function digest( + localOptions?: EdgeConfigFunctionsOptions, + ): Promise { + let localEdgeConfig: EmbeddedEdgeConfig | null = null; + + if (localOptions?.consistentRead) { + // fall through to fetching + } else if (shouldUseDevelopmentCache) { + localEdgeConfig = await getInMemoryEdgeConfig( + connectionString, + fetchCache, + options.staleIfError, + ); + } else { + localEdgeConfig = await getLocalEdgeConfig( + connection.type, + connection.id, + fetchCache, + ); + } + + if (localEdgeConfig) { + return Promise.resolve(localEdgeConfig.digest); + } + + return fetchEdgeConfigTrace( + baseUrl, + version, + localOptions?.consistentRead, + headers, + fetchCache, + ); + }, + { + name: 'digest', + isVerboseTrace: false, + attributes: { edgeConfigId }, + }, + ), + }; + + return { ...api, connection }; + }, + { + name: 'createClient', + }, + ); +} diff --git a/packages/edge-config/src/edge-config.ts b/packages/edge-config/src/edge-config.ts index 3acc0ecc0..c572cbb24 100644 --- a/packages/edge-config/src/edge-config.ts +++ b/packages/edge-config/src/edge-config.ts @@ -1,31 +1,20 @@ import { readFile } from '@vercel/edge-config-fs'; import { name as sdkName, version as sdkVersion } from '../package.json'; -import { - isEmptyKey, - ERRORS, - UnexpectedNetworkError, - parseConnectionString, -} from './utils'; import type { Connection, - EdgeConfigClient, EdgeConfigItems, EdgeConfigValue, EmbeddedEdgeConfig, } from './types'; +import { + ERRORS, + isEmptyKey, + parseConnectionString, + UnexpectedNetworkError, +} from './utils'; import { fetchWithCachedResponse } from './utils/fetch-with-cached-response'; import { trace } from './utils/tracing'; -export { setTracerProvider } from './utils/tracing'; - -export { - parseConnectionString, - type EdgeConfigClient, - type EdgeConfigItems, - type EdgeConfigValue, - type EmbeddedEdgeConfig, -}; - const X_EDGE_CONFIG_SDK_HEADER = typeof sdkName === 'string' && typeof sdkVersion === 'string' ? `${sdkName}@${sdkVersion}` diff --git a/packages/edge-config/src/index.next-js.ts b/packages/edge-config/src/index.next-js.ts index 84be80772..cd0a8421e 100644 --- a/packages/edge-config/src/index.next-js.ts +++ b/packages/edge-config/src/index.next-js.ts @@ -1,30 +1,20 @@ import { cacheLife } from 'next/cache'; -import { name as sdkName, version as sdkVersion } from '../package.json'; +import { createCreateClient } from './create-create-client'; import { - assertIsKey, - assertIsKeys, - isEmptyKey, - hasOwnProperty, - parseConnectionString, - pick, -} from './utils'; + fetchAllEdgeConfigItem, + fetchEdgeConfigHas, + fetchEdgeConfigItem, + fetchEdgeConfigTrace, + getInMemoryEdgeConfig, + getLocalEdgeConfig, +} from './edge-config'; import type { EdgeConfigClient, EdgeConfigItems, EdgeConfigValue, EmbeddedEdgeConfig, - EdgeConfigFunctionsOptions, } from './types'; -import { trace } from './utils/tracing'; -import { - getInMemoryEdgeConfig, - getLocalEdgeConfig, - fetchEdgeConfigItem, - fetchEdgeConfigHas, - fetchAllEdgeConfigItem, - fetchEdgeConfigTrace, - type EdgeConfigClientOptions, -} from './edge-config'; +import { parseConnectionString } from './utils'; export { setTracerProvider } from './utils/tracing'; @@ -126,228 +116,14 @@ async function fetchEdgeConfigTraceForNext( * @param connectionString - A connection string. Usually you'd pass in `process.env.EDGE_CONFIG` here, which contains a connection string. * @returns An Edge Config Client instance */ -export const createClient = trace( - function createClient( - connectionString: string | undefined, - options: EdgeConfigClientOptions = { - staleIfError: 604800 /* one week */, - cache: 'no-store', - }, - ): EdgeConfigClient { - if (!connectionString) - throw new Error('@vercel/edge-config: No connection string provided'); - - const connection = parseConnectionString(connectionString); - - if (!connection) - throw new Error( - '@vercel/edge-config: Invalid connection string provided', - ); - - const edgeConfigId = connection.id; - const baseUrl = connection.baseUrl; - const version = connection.version; // version of the edge config read access api we talk to - const headers: Record = { - Authorization: `Bearer ${connection.token}`, - }; - - // eslint-disable-next-line @typescript-eslint/prefer-optional-chain -- [@vercel/style-guide@5 migration] - if (typeof process !== 'undefined' && process.env.VERCEL_ENV) - headers['x-edge-config-vercel-env'] = process.env.VERCEL_ENV; - - if (typeof sdkName === 'string' && typeof sdkVersion === 'string') - headers['x-edge-config-sdk'] = `${sdkName}@${sdkVersion}`; - - if (typeof options.staleIfError === 'number' && options.staleIfError > 0) - headers['cache-control'] = `stale-if-error=${options.staleIfError}`; - - const fetchCache = options.cache || 'no-store'; - - /** - * While in development we use SWR-like behavior for the api client to - * reduce latency. - */ - const shouldUseDevelopmentCache = - !options.disableDevelopmentCache && - process.env.NODE_ENV === 'development' && - process.env.EDGE_CONFIG_DISABLE_DEVELOPMENT_SWR !== '1'; - - const api: Omit = { - get: trace( - async function get( - key: string, - localOptions?: EdgeConfigFunctionsOptions, - ): Promise { - assertIsKey(key); - - let localEdgeConfig: EmbeddedEdgeConfig | null = null; - if (localOptions?.consistentRead) { - // fall through to fetching - } else if (shouldUseDevelopmentCache) { - localEdgeConfig = await getInMemoryEdgeConfigForNext( - connectionString, - fetchCache, - options.staleIfError, - ); - } else { - localEdgeConfig = await getLocalEdgeConfigForNext( - connection.type, - connection.id, - fetchCache, - ); - } - - if (localEdgeConfig) { - if (isEmptyKey(key)) return undefined; - // We need to return a clone of the value so users can't modify - // our original value, and so the reference changes. - // - // This makes it consistent with the real API. - return Promise.resolve(localEdgeConfig.items[key] as T); - } - - return fetchEdgeConfigItemForNext( - baseUrl, - key, - version, - localOptions?.consistentRead, - headers, - fetchCache, - ); - }, - { name: 'get', isVerboseTrace: false, attributes: { edgeConfigId } }, - ), - has: trace( - async function has( - key, - localOptions?: EdgeConfigFunctionsOptions, - ): Promise { - assertIsKey(key); - if (isEmptyKey(key)) return false; - - let localEdgeConfig: EmbeddedEdgeConfig | null = null; - - if (localOptions?.consistentRead) { - // fall through to fetching - } else if (shouldUseDevelopmentCache) { - localEdgeConfig = await getInMemoryEdgeConfigForNext( - connectionString, - fetchCache, - options.staleIfError, - ); - } else { - localEdgeConfig = await getLocalEdgeConfigForNext( - connection.type, - connection.id, - fetchCache, - ); - } - - if (localEdgeConfig) { - return Promise.resolve(hasOwnProperty(localEdgeConfig.items, key)); - } - - return fetchEdgeConfigHasForNext( - baseUrl, - key, - version, - localOptions?.consistentRead, - headers, - fetchCache, - ); - }, - { name: 'has', isVerboseTrace: false, attributes: { edgeConfigId } }, - ), - getAll: trace( - async function getAll( - keys?: (keyof T)[], - localOptions?: EdgeConfigFunctionsOptions, - ): Promise { - if (keys) { - assertIsKeys(keys); - } - - let localEdgeConfig: EmbeddedEdgeConfig | null = null; - - if (localOptions?.consistentRead) { - // fall through to fetching - } else if (shouldUseDevelopmentCache) { - localEdgeConfig = await getInMemoryEdgeConfigForNext( - connectionString, - fetchCache, - options.staleIfError, - ); - } else { - localEdgeConfig = await getLocalEdgeConfigForNext( - connection.type, - connection.id, - fetchCache, - ); - } - - if (localEdgeConfig) { - if (keys === undefined) { - return Promise.resolve(localEdgeConfig.items as T); - } - - return Promise.resolve(pick(localEdgeConfig.items, keys) as T); - } - - return fetchAllEdgeConfigItemForNext( - baseUrl, - keys, - version, - localOptions?.consistentRead, - headers, - fetchCache, - ); - }, - { name: 'getAll', isVerboseTrace: false, attributes: { edgeConfigId } }, - ), - digest: trace( - async function digest( - localOptions?: EdgeConfigFunctionsOptions, - ): Promise { - let localEdgeConfig: EmbeddedEdgeConfig | null = null; - - if (localOptions?.consistentRead) { - // fall through to fetching - } else if (shouldUseDevelopmentCache) { - localEdgeConfig = await getInMemoryEdgeConfigForNext( - connectionString, - fetchCache, - options.staleIfError, - ); - } else { - localEdgeConfig = await getLocalEdgeConfigForNext( - connection.type, - connection.id, - fetchCache, - ); - } - - if (localEdgeConfig) { - return Promise.resolve(localEdgeConfig.digest); - } - - return fetchEdgeConfigTraceForNext( - baseUrl, - version, - localOptions?.consistentRead, - headers, - fetchCache, - ); - }, - { name: 'digest', isVerboseTrace: false, attributes: { edgeConfigId } }, - ), - }; - - return { ...api, connection }; - }, - { - name: 'createClient', - }, -); +export const createClient = createCreateClient({ + getInMemoryEdgeConfig: getInMemoryEdgeConfigForNext, + getLocalEdgeConfig: getLocalEdgeConfigForNext, + fetchEdgeConfigItem: fetchEdgeConfigItemForNext, + fetchEdgeConfigHas: fetchEdgeConfigHasForNext, + fetchAllEdgeConfigItem: fetchAllEdgeConfigItemForNext, + fetchEdgeConfigTrace: fetchEdgeConfigTraceForNext, +}); let defaultEdgeConfigClient: EdgeConfigClient; diff --git a/packages/edge-config/src/index.ts b/packages/edge-config/src/index.ts index e4c072fd3..74c19639b 100644 --- a/packages/edge-config/src/index.ts +++ b/packages/edge-config/src/index.ts @@ -1,29 +1,19 @@ -import { name as sdkName, version as sdkVersion } from '../package.json'; +import { createCreateClient } from './create-create-client'; import { - assertIsKey, - assertIsKeys, - isEmptyKey, - hasOwnProperty, - parseConnectionString, - pick, -} from './utils'; + fetchAllEdgeConfigItem, + fetchEdgeConfigHas, + fetchEdgeConfigItem, + fetchEdgeConfigTrace, + getInMemoryEdgeConfig, + getLocalEdgeConfig, +} from './edge-config'; import type { EdgeConfigClient, EdgeConfigItems, EdgeConfigValue, EmbeddedEdgeConfig, - EdgeConfigFunctionsOptions, } from './types'; -import { trace } from './utils/tracing'; -import { - getInMemoryEdgeConfig, - getLocalEdgeConfig, - fetchEdgeConfigItem, - fetchEdgeConfigHas, - fetchAllEdgeConfigItem, - fetchEdgeConfigTrace, - type EdgeConfigClientOptions, -} from './edge-config'; +import { parseConnectionString } from './utils'; export { setTracerProvider } from './utils/tracing'; @@ -45,228 +35,14 @@ export { * @param connectionString - A connection string. Usually you'd pass in `process.env.EDGE_CONFIG` here, which contains a connection string. * @returns An Edge Config Client instance */ -export const createClient = trace( - function createClient( - connectionString: string | undefined, - options: EdgeConfigClientOptions = { - staleIfError: 604800 /* one week */, - cache: 'no-store', - }, - ): EdgeConfigClient { - if (!connectionString) - throw new Error('@vercel/edge-config: No connection string provided'); - - const connection = parseConnectionString(connectionString); - - if (!connection) - throw new Error( - '@vercel/edge-config: Invalid connection string provided', - ); - - const edgeConfigId = connection.id; - const baseUrl = connection.baseUrl; - const version = connection.version; // version of the edge config read access api we talk to - const headers: Record = { - Authorization: `Bearer ${connection.token}`, - }; - - // eslint-disable-next-line @typescript-eslint/prefer-optional-chain -- [@vercel/style-guide@5 migration] - if (typeof process !== 'undefined' && process.env.VERCEL_ENV) - headers['x-edge-config-vercel-env'] = process.env.VERCEL_ENV; - - if (typeof sdkName === 'string' && typeof sdkVersion === 'string') - headers['x-edge-config-sdk'] = `${sdkName}@${sdkVersion}`; - - if (typeof options.staleIfError === 'number' && options.staleIfError > 0) - headers['cache-control'] = `stale-if-error=${options.staleIfError}`; - - const fetchCache = options.cache || 'no-store'; - - /** - * While in development we use SWR-like behavior for the api client to - * reduce latency. - */ - const shouldUseDevelopmentCache = - !options.disableDevelopmentCache && - process.env.NODE_ENV === 'development' && - process.env.EDGE_CONFIG_DISABLE_DEVELOPMENT_SWR !== '1'; - - const api: Omit = { - get: trace( - async function get( - key: string, - localOptions?: EdgeConfigFunctionsOptions, - ): Promise { - assertIsKey(key); - - let localEdgeConfig: EmbeddedEdgeConfig | null = null; - if (localOptions?.consistentRead) { - // fall through to fetching - } else if (shouldUseDevelopmentCache) { - localEdgeConfig = await getInMemoryEdgeConfig( - connectionString, - fetchCache, - options.staleIfError, - ); - } else { - localEdgeConfig = await getLocalEdgeConfig( - connection.type, - connection.id, - fetchCache, - ); - } - - if (localEdgeConfig) { - if (isEmptyKey(key)) return undefined; - // We need to return a clone of the value so users can't modify - // our original value, and so the reference changes. - // - // This makes it consistent with the real API. - return Promise.resolve(localEdgeConfig.items[key] as T); - } - - return fetchEdgeConfigItem( - baseUrl, - key, - version, - localOptions?.consistentRead, - headers, - fetchCache, - ); - }, - { name: 'get', isVerboseTrace: false, attributes: { edgeConfigId } }, - ), - has: trace( - async function has( - key, - localOptions?: EdgeConfigFunctionsOptions, - ): Promise { - assertIsKey(key); - if (isEmptyKey(key)) return false; - - let localEdgeConfig: EmbeddedEdgeConfig | null = null; - - if (localOptions?.consistentRead) { - // fall through to fetching - } else if (shouldUseDevelopmentCache) { - localEdgeConfig = await getInMemoryEdgeConfig( - connectionString, - fetchCache, - options.staleIfError, - ); - } else { - localEdgeConfig = await getLocalEdgeConfig( - connection.type, - connection.id, - fetchCache, - ); - } - - if (localEdgeConfig) { - return Promise.resolve(hasOwnProperty(localEdgeConfig.items, key)); - } - - return fetchEdgeConfigHas( - baseUrl, - key, - version, - localOptions?.consistentRead, - headers, - fetchCache, - ); - }, - { name: 'has', isVerboseTrace: false, attributes: { edgeConfigId } }, - ), - getAll: trace( - async function getAll( - keys?: (keyof T)[], - localOptions?: EdgeConfigFunctionsOptions, - ): Promise { - if (keys) { - assertIsKeys(keys); - } - - let localEdgeConfig: EmbeddedEdgeConfig | null = null; - - if (localOptions?.consistentRead) { - // fall through to fetching - } else if (shouldUseDevelopmentCache) { - localEdgeConfig = await getInMemoryEdgeConfig( - connectionString, - fetchCache, - options.staleIfError, - ); - } else { - localEdgeConfig = await getLocalEdgeConfig( - connection.type, - connection.id, - fetchCache, - ); - } - - if (localEdgeConfig) { - if (keys === undefined) { - return Promise.resolve(localEdgeConfig.items as T); - } - - return Promise.resolve(pick(localEdgeConfig.items, keys) as T); - } - - return fetchAllEdgeConfigItem( - baseUrl, - keys, - version, - localOptions?.consistentRead, - headers, - fetchCache, - ); - }, - { name: 'getAll', isVerboseTrace: false, attributes: { edgeConfigId } }, - ), - digest: trace( - async function digest( - localOptions?: EdgeConfigFunctionsOptions, - ): Promise { - let localEdgeConfig: EmbeddedEdgeConfig | null = null; - - if (localOptions?.consistentRead) { - // fall through to fetching - } else if (shouldUseDevelopmentCache) { - localEdgeConfig = await getInMemoryEdgeConfig( - connectionString, - fetchCache, - options.staleIfError, - ); - } else { - localEdgeConfig = await getLocalEdgeConfig( - connection.type, - connection.id, - fetchCache, - ); - } - - if (localEdgeConfig) { - return Promise.resolve(localEdgeConfig.digest); - } - - return fetchEdgeConfigTrace( - baseUrl, - version, - localOptions?.consistentRead, - headers, - fetchCache, - ); - }, - { name: 'digest', isVerboseTrace: false, attributes: { edgeConfigId } }, - ), - }; - - return { ...api, connection }; - }, - { - name: 'createClient', - }, -); +export const createClient = createCreateClient({ + getInMemoryEdgeConfig, + getLocalEdgeConfig, + fetchEdgeConfigItem, + fetchEdgeConfigHas, + fetchAllEdgeConfigItem, + fetchEdgeConfigTrace, +}); let defaultEdgeConfigClient: EdgeConfigClient;