@@ -11,6 +11,7 @@ import {
1111 type UserContext ,
1212} from '@growthbook/growthbook' ;
1313import { createClient } from '@vercel/edge-config' ;
14+ import { AsyncLocalStorage } from 'async_hooks' ;
1415import type { Adapter } from 'flags' ;
1516
1617export { getProviderData } from './provider' ;
@@ -36,6 +37,7 @@ type EdgeConfig = {
3637type AdapterResponse = {
3738 feature : < T > ( ) => Adapter < T , Attributes > ;
3839 initialize : ( ) => Promise < GrowthBookClient > ;
40+ refresh : ( ) => Promise < void > ;
3941 setTrackingCallback : ( cb : TrackingCallback ) => void ;
4042 setStickyBucketService : ( stickyBucketService : StickyBucketService ) => void ;
4143 stickyBucketService ?: StickyBucketService ;
@@ -72,49 +74,70 @@ export function createGrowthbookAdapter(options: {
7274 ...( options . clientOptions || { } ) ,
7375 } ) ;
7476
75- let _initializePromise : Promise < void > | undefined ;
77+ const edgeConfigClient = options . edgeConfig
78+ ? createClient ( options . edgeConfig . connectionString )
79+ : null ;
80+ const edgeConfigKey = options . edgeConfig ?. itemKey || options . clientKey ;
7681
77- const initializeGrowthBook = async ( ) : Promise < void > => {
78- let payload : FeatureApiResponse | string | undefined ;
79- if ( options . edgeConfig ) {
80- try {
81- const edgeConfigClient = createClient (
82- options . edgeConfig . connectionString ,
83- ) ;
84- payload = await edgeConfigClient . get < FeatureApiResponse > (
85- options . edgeConfig . itemKey || options . clientKey ,
86- ) ;
82+ const store = new AsyncLocalStorage < WeakKey > ( ) ;
83+ const cache = new WeakMap < WeakKey , Promise < FeatureApiResponse | null > > ( ) ;
84+
85+ const getEdgePayload = async ( ) : Promise < FeatureApiResponse | null > => {
86+ if ( ! edgeConfigClient ) return null ;
87+
88+ // Only do this once per request using AsyncLocalStorage
89+ const currentRequest = store . getStore ( ) ;
90+ if ( currentRequest ) {
91+ const cached = cache . get ( currentRequest ) ;
92+ if ( cached ) {
93+ return cached ;
94+ }
95+ }
96+
97+ // Fetch from Edge Config
98+ const payloadPromise = edgeConfigClient
99+ . get < FeatureApiResponse | string > ( edgeConfigKey )
100+ . then ( ( payload ) => {
87101 if ( ! payload ) {
88102 console . error ( 'No payload found in edge config' ) ;
103+ return null ;
104+ } else if ( typeof payload === 'string' ) {
105+ // Older GrowthBook integrations use WebHooks directly to store
106+ // data in Edge Config, but they store the data as a string.
107+ //
108+ // We need to parse the string to JSON before passing it to GrowthBook.
109+ //
110+ // https://docs.growthbook.io/app/webhooks/sdk-webhooks#vercel-edge-config
111+ // https://github.com/vercel/flags/issues/209
112+ try {
113+ return JSON . parse ( payload ) as FeatureApiResponse ;
114+ } catch {
115+ console . error ( 'Invalid payload format' ) ;
116+ return null ;
117+ }
118+ } else {
119+ return payload ;
89120 }
90- } catch ( e ) {
121+ } )
122+ . catch ( ( e ) => {
91123 console . error ( 'Error fetching edge config' , e ) ;
92- }
93- }
124+ return null ;
125+ } ) ;
94126
95- // Older GrowthBook integrations use WebHooks directly to store
96- // data in Edge Config, but they store the data as a string.
97- //
98- // We need to parse the string to JSON before passing it to GrowthBook.
99- //
100- // https://docs.growthbook.io/app/webhooks/sdk-webhooks#vercel-edge-config
101- // https://github.com/vercel/flags/issues/209
102- if ( typeof payload === 'string' ) {
103- try {
104- payload = JSON . parse ( payload ) as FeatureApiResponse ;
105- } catch {
106- console . error ( 'Invalid payload format' ) ;
107- payload = undefined ;
108- }
109- }
127+ if ( currentRequest ) cache . set ( currentRequest , payloadPromise ) ;
128+ return payloadPromise ;
129+ } ;
110130
131+ const initializeGrowthBook = async ( ) : Promise < void > => {
132+ const payload = await getEdgePayload ( ) ;
111133 await growthbook . init ( {
112134 streaming : false ,
113- payload,
135+ payload : payload ?? undefined ,
114136 ...( options . initOptions || { } ) ,
115137 } ) ;
116138 } ;
117139
140+ let _initializePromise : Promise < void > | undefined ;
118141 /**
119142 * Initialize the GrowthBook SDK.
120143 *
@@ -131,6 +154,18 @@ export function createGrowthbookAdapter(options: {
131154 return growthbook ;
132155 } ;
133156
157+ const refresh = async ( ) : Promise < void > => {
158+ if ( options . edgeConfig ) {
159+ const payload = await getEdgePayload ( ) ;
160+ if ( payload && payload !== growthbook . getPayload ( ) ) {
161+ await growthbook . setPayload ( payload ) ;
162+ }
163+ } else {
164+ // Init does a refresh with a stale-while-revalidate strategy
165+ await growthbook . init ( ) ;
166+ }
167+ } ;
168+
134169 function origin ( prefix : string ) {
135170 return ( key : string ) => {
136171 const appOrigin = options . appOrigin || 'https://app.growthbook.io' ;
@@ -151,8 +186,12 @@ export function createGrowthbookAdapter(options: {
151186 ) : Adapter < T , Attributes > {
152187 return {
153188 origin : origin ( 'features' ) ,
154- decide : async ( { key, entities, defaultValue } ) => {
155- await initialize ( ) ;
189+ decide : async ( { key, entities, defaultValue, headers } ) => {
190+ await store . run ( headers , async ( ) => {
191+ await initialize ( ) ;
192+ await refresh ( ) ;
193+ } ) ;
194+
156195 const userContext : UserContext = {
157196 attributes : entities as Attributes ,
158197 trackingCallback : opts . exposureLogging ? trackingCallback : undefined ,
@@ -186,6 +225,7 @@ export function createGrowthbookAdapter(options: {
186225 return {
187226 feature,
188227 initialize,
228+ refresh,
189229 setTrackingCallback,
190230 setStickyBucketService,
191231 stickyBucketService,
@@ -269,6 +309,7 @@ export function getOrCreateDefaultGrowthbookAdapter(): AdapterResponse {
269309export const growthbookAdapter : AdapterResponse = {
270310 feature : ( ...args ) => getOrCreateDefaultGrowthbookAdapter ( ) . feature ( ...args ) ,
271311 initialize : ( ) => getOrCreateDefaultGrowthbookAdapter ( ) . initialize ( ) ,
312+ refresh : ( ) => getOrCreateDefaultGrowthbookAdapter ( ) . refresh ( ) ,
272313 setTrackingCallback : ( ...args ) =>
273314 getOrCreateDefaultGrowthbookAdapter ( ) . setTrackingCallback ( ...args ) ,
274315 setStickyBucketService : ( ...args ) =>
0 commit comments