@@ -22,6 +22,7 @@ import {
2222 StandardResolutionReasons ,
2323 instantiateErrorByErrorCode ,
2424 statusMatchesEvent ,
25+ MapHookData ,
2526} from '@openfeature/core' ;
2627import type { FlagEvaluationOptions } from '../../evaluation' ;
2728import type { ProviderEvents } from '../../events' ;
@@ -276,22 +277,26 @@ export class OpenFeatureClient implements Client {
276277
277278 const mergedContext = this . mergeContexts ( invocationContext ) ;
278279
279- // this reference cannot change during the course of evaluation
280- // it may be used as a key in WeakMaps
281- const hookContext : Readonly < HookContext > = {
282- flagKey,
283- defaultValue,
284- flagValueType : flagType ,
285- clientMetadata : this . metadata ,
286- providerMetadata : this . _provider . metadata ,
287- context : mergedContext ,
288- logger : this . _logger ,
289- } ;
280+ // Create hook context instances for each hook (stable object references for the entire evaluation)
281+ // This ensures hooks can use WeakMaps with hookContext as keys across lifecycle methods
282+ // NOTE: Uses the reversed order to reduce the number of times we have to calculate the index.
283+ const hookContexts = allHooksReversed . map < HookContext > ( ( ) =>
284+ Object . freeze ( {
285+ flagKey,
286+ defaultValue,
287+ flagValueType : flagType ,
288+ clientMetadata : this . metadata ,
289+ providerMetadata : this . _provider . metadata ,
290+ context : mergedContext ,
291+ logger : this . _logger ,
292+ hookData : new MapHookData ( ) ,
293+ } ) ,
294+ ) ;
290295
291296 let evaluationDetails : EvaluationDetails < T > ;
292297
293298 try {
294- const frozenContext = await this . beforeHooks ( allHooks , hookContext , options ) ;
299+ const frozenContext = await this . beforeHooks ( allHooks , hookContexts , mergedContext , options ) ;
295300
296301 this . shortCircuitIfNotReady ( ) ;
297302
@@ -306,53 +311,71 @@ export class OpenFeatureClient implements Client {
306311
307312 if ( resolutionDetails . errorCode ) {
308313 const err = instantiateErrorByErrorCode ( resolutionDetails . errorCode , resolutionDetails . errorMessage ) ;
309- await this . errorHooks ( allHooksReversed , hookContext , err , options ) ;
314+ await this . errorHooks ( allHooksReversed , hookContexts , err , options ) ;
310315 evaluationDetails = this . getErrorEvaluationDetails ( flagKey , defaultValue , err , resolutionDetails . flagMetadata ) ;
311316 } else {
312- await this . afterHooks ( allHooksReversed , hookContext , resolutionDetails , options ) ;
317+ await this . afterHooks ( allHooksReversed , hookContexts , resolutionDetails , options ) ;
313318 evaluationDetails = resolutionDetails ;
314319 }
315320 } catch ( err : unknown ) {
316- await this . errorHooks ( allHooksReversed , hookContext , err , options ) ;
321+ await this . errorHooks ( allHooksReversed , hookContexts , err , options ) ;
317322 evaluationDetails = this . getErrorEvaluationDetails ( flagKey , defaultValue , err ) ;
318323 }
319324
320- await this . finallyHooks ( allHooksReversed , hookContext , evaluationDetails , options ) ;
325+ await this . finallyHooks ( allHooksReversed , hookContexts , evaluationDetails , options ) ;
321326 return evaluationDetails ;
322327 }
323328
324- private async beforeHooks ( hooks : Hook [ ] , hookContext : HookContext , options : FlagEvaluationOptions ) {
325- for ( const hook of hooks ) {
326- // freeze the hookContext
327- Object . freeze ( hookContext ) ;
329+ private async beforeHooks (
330+ hooks : Hook [ ] ,
331+ hookContexts : HookContext [ ] ,
332+ mergedContext : EvaluationContext ,
333+ options : FlagEvaluationOptions ,
334+ ) {
335+ let accumulatedContext = mergedContext ;
336+
337+ for ( const [ index , hook ] of hooks . entries ( ) ) {
338+ const hookContextIndex = hooks . length - 1 - index ; // reverse index for before hooks
339+ const hookContext = hookContexts [ hookContextIndex ] ;
328340
329- // use Object.assign to avoid modification of frozen hookContext
330- Object . assign ( hookContext . context , {
331- ...hookContext . context ,
332- ...( await hook ?. before ?.( hookContext , Object . freeze ( options . hookHints ) ) ) ,
333- } ) ;
341+ // Update the context on the stable hook context object
342+ Object . assign ( hookContext . context , accumulatedContext ) ;
343+
344+ const hookResult = await hook ?. before ?.( hookContext , Object . freeze ( options . hookHints ) ) ;
345+ if ( hookResult ) {
346+ accumulatedContext = {
347+ ...accumulatedContext ,
348+ ...hookResult ,
349+ } ;
350+
351+ for ( let i = 0 ; i < hooks . length ; i ++ ) {
352+ Object . assign ( hookContexts [ hookContextIndex ] . context , accumulatedContext ) ;
353+ }
354+ }
334355 }
335356
336357 // after before hooks, freeze the EvaluationContext.
337- return Object . freeze ( hookContext . context ) ;
358+ return Object . freeze ( accumulatedContext ) ;
338359 }
339360
340361 private async afterHooks (
341362 hooks : Hook [ ] ,
342- hookContext : HookContext ,
363+ hookContexts : HookContext [ ] ,
343364 evaluationDetails : EvaluationDetails < FlagValue > ,
344365 options : FlagEvaluationOptions ,
345366 ) {
346367 // run "after" hooks sequentially
347- for ( const hook of hooks ) {
368+ for ( const [ index , hook ] of hooks . entries ( ) ) {
369+ const hookContext = hookContexts [ index ] ;
348370 await hook ?. after ?.( hookContext , evaluationDetails , options . hookHints ) ;
349371 }
350372 }
351373
352- private async errorHooks ( hooks : Hook [ ] , hookContext : HookContext , err : unknown , options : FlagEvaluationOptions ) {
374+ private async errorHooks ( hooks : Hook [ ] , hookContexts : HookContext [ ] , err : unknown , options : FlagEvaluationOptions ) {
353375 // run "error" hooks sequentially
354- for ( const hook of hooks ) {
376+ for ( const [ index , hook ] of hooks . entries ( ) ) {
355377 try {
378+ const hookContext = hookContexts [ index ] ;
356379 await hook ?. error ?.( hookContext , err , options . hookHints ) ;
357380 } catch ( err ) {
358381 this . _logger . error ( `Unhandled error during 'error' hook: ${ err } ` ) ;
@@ -366,13 +389,14 @@ export class OpenFeatureClient implements Client {
366389
367390 private async finallyHooks (
368391 hooks : Hook [ ] ,
369- hookContext : HookContext ,
392+ hookContexts : HookContext [ ] ,
370393 evaluationDetails : EvaluationDetails < FlagValue > ,
371394 options : FlagEvaluationOptions ,
372395 ) {
373396 // run "finally" hooks sequentially
374- for ( const hook of hooks ) {
397+ for ( const [ index , hook ] of hooks . entries ( ) ) {
375398 try {
399+ const hookContext = hookContexts [ index ] ;
376400 await hook ?. finally ?.( hookContext , evaluationDetails , options . hookHints ) ;
377401 } catch ( err ) {
378402 this . _logger . error ( `Unhandled error during 'finally' hook: ${ err } ` ) ;
0 commit comments