From 2dc5ca7a6a08203811346d3cbe7485c75bde163b Mon Sep 17 00:00:00 2001 From: Marko Mlakar Date: Wed, 1 Oct 2025 19:12:04 +0200 Subject: [PATCH 1/2] refactor: simplify evaluation context updates for hooks - Remove redundant loop for updating the hook contexts - Add simple test case to make sure the same object reference is being updated Signed-off-by: Marko Mlakar --- .../client/internal/open-feature-client.ts | 5 +-- packages/server/test/hooks.spec.ts | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/packages/server/src/client/internal/open-feature-client.ts b/packages/server/src/client/internal/open-feature-client.ts index 7dcb13ef8..628c77b4c 100644 --- a/packages/server/src/client/internal/open-feature-client.ts +++ b/packages/server/src/client/internal/open-feature-client.ts @@ -344,10 +344,7 @@ export class OpenFeatureClient implements Client { const hookResult = await hook?.before?.(hookContext, Object.freeze(options.hookHints)); if (hookResult) { Object.assign(accumulatedContext, hookResult); - - for (let i = 0; i < hooks.length; i++) { - Object.assign(hookContexts[hookContextIndex].context, accumulatedContext); - } + Object.assign(hookContext.context, accumulatedContext); } } diff --git a/packages/server/test/hooks.spec.ts b/packages/server/test/hooks.spec.ts index 6e2deba04..8f67287e1 100644 --- a/packages/server/test/hooks.spec.ts +++ b/packages/server/test/hooks.spec.ts @@ -311,6 +311,45 @@ describe('Hooks', () => { expect.anything(), ); }); + it('Should share the same context object reference across all hooks', (done) => { + let hook1Context: EvaluationContext; + let hook2Context: EvaluationContext; + let hook3Context: EvaluationContext; + + client.getBooleanValue(FLAG_KEY, false, undefined, { + hooks: [ + { + before: (hookContext) => { + hook1Context = hookContext.context; + return { fromHook1: 'value1' }; + }, + }, + { + before: (hookContext) => { + hook2Context = hookContext.context; + return { fromHook2: 'value2' }; + }, + }, + { + before: (hookContext) => { + hook3Context = hookContext.context; + return { fromHook3: 'value3' }; + }, + + after: (hookContext) => { + try { + expect(hookContext.context).toBe(hook1Context); + expect(hookContext.context).toBe(hook2Context); + expect(hookContext.context).toBe(hook3Context); + done(); + } catch (err) { + done(err); + } + }, + }, + ], + }); + }); }); describe('Requirement 4.3.5', () => { From 3e69e0ea1624c5e2ec46bab18646022d92bb9628 Mon Sep 17 00:00:00 2001 From: MarkoMlakar Date: Wed, 17 Dec 2025 15:04:06 +0100 Subject: [PATCH 2/2] refactor: Expand the functionality of the unit test case to support API/Client and invocation level hooks Signed-off-by: MarkoMlakar --- packages/server/test/hooks.spec.ts | 93 +++++++++++++++++++----------- 1 file changed, 58 insertions(+), 35 deletions(-) diff --git a/packages/server/test/hooks.spec.ts b/packages/server/test/hooks.spec.ts index 8f67287e1..d5f154402 100644 --- a/packages/server/test/hooks.spec.ts +++ b/packages/server/test/hooks.spec.ts @@ -311,43 +311,66 @@ describe('Hooks', () => { expect.anything(), ); }); - it('Should share the same context object reference across all hooks', (done) => { - let hook1Context: EvaluationContext; - let hook2Context: EvaluationContext; - let hook3Context: EvaluationContext; + it('Should share the same context object reference across API, client, and invocation level hooks', (done) => { + OpenFeature.clearHooks(); + client.clearHooks(); - client.getBooleanValue(FLAG_KEY, false, undefined, { - hooks: [ - { - before: (hookContext) => { - hook1Context = hookContext.context; - return { fromHook1: 'value1' }; - }, - }, - { - before: (hookContext) => { - hook2Context = hookContext.context; - return { fromHook2: 'value2' }; - }, - }, - { - before: (hookContext) => { - hook3Context = hookContext.context; - return { fromHook3: 'value3' }; - }, + let apiHookContext: EvaluationContext; + let clientHookContext: EvaluationContext; + let invocation1Context: EvaluationContext; + let invocation2Context: EvaluationContext; - after: (hookContext) => { - try { - expect(hookContext.context).toBe(hook1Context); - expect(hookContext.context).toBe(hook2Context); - expect(hookContext.context).toBe(hook3Context); - done(); - } catch (err) { - done(err); - } - }, - }, - ], + const apiLevelHook: Hook = { + before: (hookContext) => { + apiHookContext = hookContext.context; + return { fromApiHook: 'apiValue' }; + }, + }; + + const clientLevelHook: Hook = { + before: (hookContext) => { + clientHookContext = hookContext.context; + return { fromClientHook: 'clientValue' }; + }, + }; + + const invocationHook1: Hook = { + before: (hookContext) => { + invocation1Context = hookContext.context; + return { fromInvocation1: 'invocation1Value' }; + }, + }; + + const invocationHook2: Hook = { + before: (hookContext) => { + invocation2Context = hookContext.context; + return { fromInvocation2: 'invocation2Value' }; + }, + after: (hookContext) => { + try { + // all hooks should share the same context object reference + expect(hookContext.context).toBe(apiHookContext); + expect(hookContext.context).toBe(clientHookContext); + expect(hookContext.context).toBe(invocation1Context); + expect(hookContext.context).toBe(invocation2Context); + + // verify all properties from different hook levels are present + expect(hookContext.context.fromApiHook).toBe('apiValue'); + expect(hookContext.context.fromClientHook).toBe('clientValue'); + expect(hookContext.context.fromInvocation1).toBe('invocation1Value'); + expect(hookContext.context.fromInvocation2).toBe('invocation2Value'); + + done(); + } catch (err) { + done(err); + } + }, + }; + + OpenFeature.addHooks(apiLevelHook); + client.addHooks(clientLevelHook); + client.getBooleanValue(FLAG_KEY, false, undefined, { + hooks: [invocationHook1, invocationHook2], }); }); });