@@ -22,6 +22,7 @@ import {
22
22
StandardResolutionReasons ,
23
23
instantiateErrorByErrorCode ,
24
24
statusMatchesEvent ,
25
+ MapHookData ,
25
26
} from '@openfeature/core' ;
26
27
import type { FlagEvaluationOptions } from '../../evaluation' ;
27
28
import type { ProviderEvents } from '../../events' ;
@@ -276,22 +277,26 @@ export class OpenFeatureClient implements Client {
276
277
277
278
const mergedContext = this . mergeContexts ( invocationContext ) ;
278
279
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
+ ) ;
290
295
291
296
let evaluationDetails : EvaluationDetails < T > ;
292
297
293
298
try {
294
- const frozenContext = await this . beforeHooks ( allHooks , hookContext , options ) ;
299
+ const frozenContext = await this . beforeHooks ( allHooks , hookContexts , mergedContext , options ) ;
295
300
296
301
this . shortCircuitIfNotReady ( ) ;
297
302
@@ -306,53 +311,71 @@ export class OpenFeatureClient implements Client {
306
311
307
312
if ( resolutionDetails . errorCode ) {
308
313
const err = instantiateErrorByErrorCode ( resolutionDetails . errorCode , resolutionDetails . errorMessage ) ;
309
- await this . errorHooks ( allHooksReversed , hookContext , err , options ) ;
314
+ await this . errorHooks ( allHooksReversed , hookContexts , err , options ) ;
310
315
evaluationDetails = this . getErrorEvaluationDetails ( flagKey , defaultValue , err , resolutionDetails . flagMetadata ) ;
311
316
} else {
312
- await this . afterHooks ( allHooksReversed , hookContext , resolutionDetails , options ) ;
317
+ await this . afterHooks ( allHooksReversed , hookContexts , resolutionDetails , options ) ;
313
318
evaluationDetails = resolutionDetails ;
314
319
}
315
320
} catch ( err : unknown ) {
316
- await this . errorHooks ( allHooksReversed , hookContext , err , options ) ;
321
+ await this . errorHooks ( allHooksReversed , hookContexts , err , options ) ;
317
322
evaluationDetails = this . getErrorEvaluationDetails ( flagKey , defaultValue , err ) ;
318
323
}
319
324
320
- await this . finallyHooks ( allHooksReversed , hookContext , evaluationDetails , options ) ;
325
+ await this . finallyHooks ( allHooksReversed , hookContexts , evaluationDetails , options ) ;
321
326
return evaluationDetails ;
322
327
}
323
328
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 ] ;
328
340
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
+ }
334
355
}
335
356
336
357
// after before hooks, freeze the EvaluationContext.
337
- return Object . freeze ( hookContext . context ) ;
358
+ return Object . freeze ( accumulatedContext ) ;
338
359
}
339
360
340
361
private async afterHooks (
341
362
hooks : Hook [ ] ,
342
- hookContext : HookContext ,
363
+ hookContexts : HookContext [ ] ,
343
364
evaluationDetails : EvaluationDetails < FlagValue > ,
344
365
options : FlagEvaluationOptions ,
345
366
) {
346
367
// run "after" hooks sequentially
347
- for ( const hook of hooks ) {
368
+ for ( const [ index , hook ] of hooks . entries ( ) ) {
369
+ const hookContext = hookContexts [ index ] ;
348
370
await hook ?. after ?.( hookContext , evaluationDetails , options . hookHints ) ;
349
371
}
350
372
}
351
373
352
- private async errorHooks ( hooks : Hook [ ] , hookContext : HookContext , err : unknown , options : FlagEvaluationOptions ) {
374
+ private async errorHooks ( hooks : Hook [ ] , hookContexts : HookContext [ ] , err : unknown , options : FlagEvaluationOptions ) {
353
375
// run "error" hooks sequentially
354
- for ( const hook of hooks ) {
376
+ for ( const [ index , hook ] of hooks . entries ( ) ) {
355
377
try {
378
+ const hookContext = hookContexts [ index ] ;
356
379
await hook ?. error ?.( hookContext , err , options . hookHints ) ;
357
380
} catch ( err ) {
358
381
this . _logger . error ( `Unhandled error during 'error' hook: ${ err } ` ) ;
@@ -366,13 +389,14 @@ export class OpenFeatureClient implements Client {
366
389
367
390
private async finallyHooks (
368
391
hooks : Hook [ ] ,
369
- hookContext : HookContext ,
392
+ hookContexts : HookContext [ ] ,
370
393
evaluationDetails : EvaluationDetails < FlagValue > ,
371
394
options : FlagEvaluationOptions ,
372
395
) {
373
396
// run "finally" hooks sequentially
374
- for ( const hook of hooks ) {
397
+ for ( const [ index , hook ] of hooks . entries ( ) ) {
375
398
try {
399
+ const hookContext = hookContexts [ index ] ;
376
400
await hook ?. finally ?.( hookContext , evaluationDetails , options . hookHints ) ;
377
401
} catch ( err ) {
378
402
this . _logger . error ( `Unhandled error during 'finally' hook: ${ err } ` ) ;
0 commit comments