@@ -22,6 +22,7 @@ import {
22
22
StandardResolutionReasons ,
23
23
instantiateErrorByErrorCode ,
24
24
statusMatchesEvent ,
25
+ DefaultHookData ,
25
26
} from '@openfeature/core' ;
26
27
import type { FlagEvaluationOptions } from '../../evaluation' ;
27
28
import type { ProviderEvents } from '../../events' ;
@@ -276,22 +277,47 @@ 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 data instances for each hook
281
+ const hookDataMap = new WeakMap < Hook , DefaultHookData > ( ) ;
282
+ for ( const hook of allHooks ) {
283
+ hookDataMap . set ( hook , new DefaultHookData ( ) ) ;
284
+ }
285
+
286
+ // Create hook context instances for each hook (stable object references for the entire evaluation)
287
+ // This ensures hooks can use WeakMaps with hookContext as keys across lifecycle methods
288
+ const hookContextMap = new WeakMap < Hook , HookContext > ( ) ;
289
+ for ( const hook of allHooks ) {
290
+ const hookContext : HookContext = {
291
+ flagKey,
292
+ defaultValue,
293
+ flagValueType : flagType ,
294
+ clientMetadata : this . metadata ,
295
+ providerMetadata : this . _provider . metadata ,
296
+ context : mergedContext ,
297
+ logger : this . _logger ,
298
+ hookData : hookDataMap . get ( hook ) ! ,
299
+ } ;
300
+
301
+ // Make the core properties immutable while allowing context updates
302
+ Object . defineProperty ( hookContext , 'flagKey' , { writable : false , configurable : false } ) ;
303
+ Object . defineProperty ( hookContext , 'defaultValue' , { writable : false , configurable : false } ) ;
304
+ Object . defineProperty ( hookContext , 'flagValueType' , { writable : false , configurable : false } ) ;
305
+ Object . defineProperty ( hookContext , 'clientMetadata' , { writable : false , configurable : false } ) ;
306
+ Object . defineProperty ( hookContext , 'providerMetadata' , { writable : false , configurable : false } ) ;
307
+ Object . defineProperty ( hookContext , 'logger' , { writable : false , configurable : false } ) ;
308
+ Object . defineProperty ( hookContext , 'hookData' , { writable : false , configurable : false } ) ;
309
+
310
+ hookContextMap . set ( hook , hookContext ) ;
311
+ }
312
+
313
+ // Function to get the stable hook context for a given hook
314
+ const getHookContext = ( hook : Hook ) => hookContextMap . get ( hook ) ! ;
290
315
291
316
let evaluationDetails : EvaluationDetails < T > ;
317
+ let frozenContext = mergedContext ;
292
318
293
319
try {
294
- const frozenContext = await this . beforeHooks ( allHooks , hookContext , options ) ;
320
+ frozenContext = await this . beforeHooks ( allHooks , getHookContext , mergedContext , options ) ;
295
321
296
322
this . shortCircuitIfNotReady ( ) ;
297
323
@@ -306,53 +332,77 @@ export class OpenFeatureClient implements Client {
306
332
307
333
if ( resolutionDetails . errorCode ) {
308
334
const err = instantiateErrorByErrorCode ( resolutionDetails . errorCode , resolutionDetails . errorMessage ) ;
309
- await this . errorHooks ( allHooksReversed , hookContext , err , options ) ;
335
+ await this . errorHooks ( allHooksReversed , getHookContext , err , options ) ;
310
336
evaluationDetails = this . getErrorEvaluationDetails ( flagKey , defaultValue , err , resolutionDetails . flagMetadata ) ;
311
337
} else {
312
- await this . afterHooks ( allHooksReversed , hookContext , resolutionDetails , options ) ;
338
+ await this . afterHooks ( allHooksReversed , getHookContext , resolutionDetails , options ) ;
313
339
evaluationDetails = resolutionDetails ;
314
340
}
315
341
} catch ( err : unknown ) {
316
- await this . errorHooks ( allHooksReversed , hookContext , err , options ) ;
342
+ await this . errorHooks ( allHooksReversed , getHookContext , err , options ) ;
317
343
evaluationDetails = this . getErrorEvaluationDetails ( flagKey , defaultValue , err ) ;
318
344
}
319
345
320
- await this . finallyHooks ( allHooksReversed , hookContext , evaluationDetails , options ) ;
346
+ await this . finallyHooks ( allHooksReversed , getHookContext , evaluationDetails , options ) ;
321
347
return evaluationDetails ;
322
348
}
323
349
324
- private async beforeHooks ( hooks : Hook [ ] , hookContext : HookContext , options : FlagEvaluationOptions ) {
350
+ private async beforeHooks (
351
+ hooks : Hook [ ] ,
352
+ getHookContext : ( hook : Hook ) => HookContext ,
353
+ mergedContext : EvaluationContext ,
354
+ options : FlagEvaluationOptions
355
+ ) {
356
+ let accumulatedContext = mergedContext ;
357
+
325
358
for ( const hook of hooks ) {
326
- // freeze the hookContext
327
- Object . freeze ( hookContext ) ;
359
+ const hookContext = getHookContext ( hook ) ;
360
+
361
+ // Update the context on the stable hook context object
362
+ ( hookContext as any ) . context = accumulatedContext ;
363
+
364
+ const hookResult = await hook ?. before ?.( hookContext , Object . freeze ( options . hookHints ) ) ;
365
+ if ( hookResult ) {
366
+ accumulatedContext = {
367
+ ...accumulatedContext ,
368
+ ...hookResult ,
369
+ } ;
370
+ }
371
+ }
328
372
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
- } ) ;
373
+ // Update all hook contexts with the final accumulated context and freeze it
374
+ for ( const hook of hooks ) {
375
+ const hookContext = getHookContext ( hook ) ;
376
+ ( hookContext as any ) . context = Object . freeze ( accumulatedContext ) ;
334
377
}
335
378
336
379
// after before hooks, freeze the EvaluationContext.
337
- return Object . freeze ( hookContext . context ) ;
380
+ return Object . freeze ( accumulatedContext ) ;
338
381
}
339
382
340
383
private async afterHooks (
341
384
hooks : Hook [ ] ,
342
- hookContext : HookContext ,
385
+ getHookContext : ( hook : Hook ) => HookContext ,
343
386
evaluationDetails : EvaluationDetails < FlagValue > ,
344
387
options : FlagEvaluationOptions ,
345
388
) {
346
389
// run "after" hooks sequentially
347
390
for ( const hook of hooks ) {
391
+ const hookContext = getHookContext ( hook ) ;
348
392
await hook ?. after ?.( hookContext , evaluationDetails , options . hookHints ) ;
349
393
}
350
394
}
351
395
352
- private async errorHooks ( hooks : Hook [ ] , hookContext : HookContext , err : unknown , options : FlagEvaluationOptions ) {
396
+ private async errorHooks (
397
+ hooks : Hook [ ] ,
398
+ getHookContext : ( hook : Hook ) => HookContext ,
399
+ err : unknown ,
400
+ options : FlagEvaluationOptions
401
+ ) {
353
402
// run "error" hooks sequentially
354
403
for ( const hook of hooks ) {
355
404
try {
405
+ const hookContext = getHookContext ( hook ) ;
356
406
await hook ?. error ?.( hookContext , err , options . hookHints ) ;
357
407
} catch ( err ) {
358
408
this . _logger . error ( `Unhandled error during 'error' hook: ${ err } ` ) ;
@@ -366,13 +416,14 @@ export class OpenFeatureClient implements Client {
366
416
367
417
private async finallyHooks (
368
418
hooks : Hook [ ] ,
369
- hookContext : HookContext ,
419
+ getHookContext : ( hook : Hook ) => HookContext ,
370
420
evaluationDetails : EvaluationDetails < FlagValue > ,
371
421
options : FlagEvaluationOptions ,
372
422
) {
373
423
// run "finally" hooks sequentially
374
424
for ( const hook of hooks ) {
375
425
try {
426
+ const hookContext = getHookContext ( hook ) ;
376
427
await hook ?. finally ?.( hookContext , evaluationDetails , options . hookHints ) ;
377
428
} catch ( err ) {
378
429
this . _logger . error ( `Unhandled error during 'finally' hook: ${ err } ` ) ;
0 commit comments