Skip to content

Commit a7d0b95

Browse files
fix: make hooks in client sdk only return void (#671)
<!-- Please use this template for your pull request. --> <!-- Please use the sections that you need and delete other sections --> ## This PR Fixes return type of hooks in client SDK. Maybe we should make this more clear in the spec @toddbaert. ### Related Issues Fixes #630 ### Notes <!-- any additional notes for this PR --> ### Follow-up Tasks <!-- anything that is related to this PR but not done here should be noted under this section --> <!-- if there is a need for a new issue, please link it here --> ### How to test <!-- if applicable, add testing instructions under this section --> --------- Signed-off-by: Lukas Reining <[email protected]> Co-authored-by: Todd Baert <[email protected]>
1 parent 00a6d2e commit a7d0b95

File tree

21 files changed

+130
-193
lines changed

21 files changed

+130
-193
lines changed

packages/client/src/client/open-feature-client.ts

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
EventHandler,
77
FlagValue,
88
FlagValueType,
9-
Hook,
109
HookContext,
1110
JsonValue,
1211
Logger,
@@ -22,6 +21,7 @@ import { OpenFeature } from '../open-feature';
2221
import { Provider } from '../provider';
2322
import { InternalEventEmitter } from '../events/internal/internal-event-emitter';
2423
import { Client } from './client';
24+
import { Hook } from '../hooks';
2525

2626
type OpenFeatureClientOptions = {
2727
name?: string;
@@ -38,7 +38,7 @@ export class OpenFeatureClient implements Client {
3838
private readonly providerAccessor: () => Provider,
3939
private readonly emitterAccessor: () => InternalEventEmitter,
4040
private readonly globalLogger: () => Logger,
41-
private readonly options: OpenFeatureClientOptions
41+
private readonly options: OpenFeatureClientOptions,
4242
) {}
4343

4444
get metadata(): ClientMetadata {
@@ -76,12 +76,12 @@ export class OpenFeatureClient implements Client {
7676
return this;
7777
}
7878

79-
addHooks(...hooks: Hook<FlagValue>[]): this {
79+
addHooks(...hooks: Hook[]): this {
8080
this._hooks = [...this._hooks, ...hooks];
8181
return this;
8282
}
8383

84-
getHooks(): Hook<FlagValue>[] {
84+
getHooks(): Hook[] {
8585
return this._hooks;
8686
}
8787

@@ -97,7 +97,7 @@ export class OpenFeatureClient implements Client {
9797
getBooleanDetails(
9898
flagKey: string,
9999
defaultValue: boolean,
100-
options?: FlagEvaluationOptions
100+
options?: FlagEvaluationOptions,
101101
): EvaluationDetails<boolean> {
102102
return this.evaluate<boolean>(flagKey, this._provider.resolveBooleanEvaluation, defaultValue, 'boolean', options);
103103
}
@@ -109,15 +109,15 @@ export class OpenFeatureClient implements Client {
109109
getStringDetails<T extends string = string>(
110110
flagKey: string,
111111
defaultValue: T,
112-
options?: FlagEvaluationOptions
112+
options?: FlagEvaluationOptions,
113113
): EvaluationDetails<T> {
114114
return this.evaluate<T>(
115115
flagKey,
116116
// this isolates providers from our restricted string generic argument.
117117
this._provider.resolveStringEvaluation as () => EvaluationDetails<T>,
118118
defaultValue,
119119
'string',
120-
options
120+
options,
121121
);
122122
}
123123

@@ -128,30 +128,30 @@ export class OpenFeatureClient implements Client {
128128
getNumberDetails<T extends number = number>(
129129
flagKey: string,
130130
defaultValue: T,
131-
options?: FlagEvaluationOptions
131+
options?: FlagEvaluationOptions,
132132
): EvaluationDetails<T> {
133133
return this.evaluate<T>(
134134
flagKey,
135135
// this isolates providers from our restricted number generic argument.
136136
this._provider.resolveNumberEvaluation as () => EvaluationDetails<T>,
137137
defaultValue,
138138
'number',
139-
options
139+
options,
140140
);
141141
}
142142

143143
getObjectValue<T extends JsonValue = JsonValue>(
144144
flagKey: string,
145145
defaultValue: T,
146-
options?: FlagEvaluationOptions
146+
options?: FlagEvaluationOptions,
147147
): T {
148148
return this.getObjectDetails(flagKey, defaultValue, options).value;
149149
}
150150

151151
getObjectDetails<T extends JsonValue = JsonValue>(
152152
flagKey: string,
153153
defaultValue: T,
154-
options?: FlagEvaluationOptions
154+
options?: FlagEvaluationOptions,
155155
): EvaluationDetails<T> {
156156
return this.evaluate<T>(flagKey, this._provider.resolveObjectEvaluation, defaultValue, 'object', options);
157157
}
@@ -161,7 +161,7 @@ export class OpenFeatureClient implements Client {
161161
resolver: (flagKey: string, defaultValue: T, context: EvaluationContext, logger: Logger) => ResolutionDetails<T>,
162162
defaultValue: T,
163163
flagType: FlagValueType,
164-
options: FlagEvaluationOptions = {}
164+
options: FlagEvaluationOptions = {},
165165
): EvaluationDetails<T> {
166166
// merge global, client, and evaluation context
167167

@@ -224,26 +224,19 @@ export class OpenFeatureClient implements Client {
224224
}
225225

226226
private beforeHooks(hooks: Hook[], hookContext: HookContext, options: FlagEvaluationOptions) {
227+
Object.freeze(hookContext);
228+
Object.freeze(hookContext.context);
229+
227230
for (const hook of hooks) {
228-
// freeze the hookContext
229-
Object.freeze(hookContext);
230-
231-
// use Object.assign to avoid modification of frozen hookContext
232-
Object.assign(hookContext.context, {
233-
...hookContext.context,
234-
...hook?.before?.(hookContext, Object.freeze(options.hookHints)),
235-
});
231+
hook?.before?.(hookContext, Object.freeze(options.hookHints));
236232
}
237-
238-
// after before hooks, freeze the EvaluationContext.
239-
return Object.freeze(hookContext.context);
240233
}
241234

242235
private afterHooks(
243236
hooks: Hook[],
244237
hookContext: HookContext,
245238
evaluationDetails: EvaluationDetails<FlagValue>,
246-
options: FlagEvaluationOptions
239+
options: FlagEvaluationOptions,
247240
) {
248241
// run "after" hooks sequentially
249242
for (const hook of hooks) {

packages/client/src/evaluation/evaluation.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { EvaluationDetails, Hook, HookHints, JsonValue } from '@openfeature/core';
1+
import { EvaluationDetails, BaseHook, HookHints, JsonValue } from '@openfeature/core';
22

33
export interface FlagEvaluationOptions {
4-
hooks?: Hook[];
4+
hooks?: BaseHook[];
55
hookHints?: HookHints;
66
}
77

@@ -25,7 +25,7 @@ export interface Features {
2525
getBooleanDetails(
2626
flagKey: string,
2727
defaultValue: boolean,
28-
options?: FlagEvaluationOptions
28+
options?: FlagEvaluationOptions,
2929
): EvaluationDetails<boolean>;
3030

3131
/**
@@ -53,7 +53,7 @@ export interface Features {
5353
getStringDetails<T extends string = string>(
5454
flagKey: string,
5555
defaultValue: T,
56-
options?: FlagEvaluationOptions
56+
options?: FlagEvaluationOptions,
5757
): EvaluationDetails<T>;
5858

5959
/**
@@ -81,7 +81,7 @@ export interface Features {
8181
getNumberDetails<T extends number = number>(
8282
flagKey: string,
8383
defaultValue: T,
84-
options?: FlagEvaluationOptions
84+
options?: FlagEvaluationOptions,
8585
): EvaluationDetails<T>;
8686

8787
/**
@@ -107,12 +107,12 @@ export interface Features {
107107
getObjectDetails(
108108
flagKey: string,
109109
defaultValue: JsonValue,
110-
options?: FlagEvaluationOptions
110+
options?: FlagEvaluationOptions,
111111
): EvaluationDetails<JsonValue>;
112112

113113
getObjectDetails<T extends JsonValue = JsonValue>(
114114
flagKey: string,
115115
defaultValue: T,
116-
options?: FlagEvaluationOptions
116+
options?: FlagEvaluationOptions,
117117
): EvaluationDetails<T>;
118118
}

packages/client/src/hooks/hook.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { BaseHook, FlagValue } from '@openfeature/core';
2+
3+
export type Hook = BaseHook<FlagValue, void, void>;

packages/client/src/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './hook';

packages/client/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ export * from './provider';
33
export * from './evaluation';
44
export * from './open-feature';
55
export * from './events';
6+
export * from './hooks';
67
export * from '@openfeature/core';

packages/client/src/open-feature.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { EvaluationContext, ManageContext, OpenFeatureCommonAPI } from '@openfea
22
import { Client, OpenFeatureClient } from './client';
33
import { NOOP_PROVIDER, Provider } from './provider';
44
import { OpenFeatureEventEmitter } from './events';
5+
import { Hook } from './hooks';
56

67
// use a symbol as a key for the global singleton
78
const GLOBAL_OPENFEATURE_API_KEY = Symbol.for('@openfeature/web-sdk/api');
@@ -11,7 +12,7 @@ type OpenFeatureGlobal = {
1112
};
1213
const _globalThis = globalThis as OpenFeatureGlobal;
1314

14-
export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider> implements ManageContext<Promise<void>> {
15+
export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider, Hook> implements ManageContext<Promise<void>> {
1516
protected _events = new OpenFeatureEventEmitter();
1617
protected _defaultProvider: Provider = NOOP_PROVIDER;
1718
protected _createEventEmitter = () => new OpenFeatureEventEmitter();
@@ -49,7 +50,7 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider> implements Ma
4950
} catch (err) {
5051
this._logger?.error(`Error running context change handler of provider ${provider.metadata.name}:`, err);
5152
}
52-
})
53+
}),
5354
);
5455
}
5556

@@ -75,7 +76,7 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider> implements Ma
7576
() => this.getProviderForClient(name),
7677
() => this.buildAndCacheEventEmitterForClient(name),
7778
() => this._logger,
78-
{ name, version }
79+
{ name, version },
7980
);
8081
}
8182

packages/client/src/provider/provider.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { CommonProvider, EvaluationContext, Hook, JsonValue, Logger, ResolutionDetails } from '@openfeature/core';
1+
import { CommonProvider, EvaluationContext, JsonValue, Logger, ResolutionDetails } from '@openfeature/core';
2+
import { Hook } from '../hooks';
23

34
/**
45
* Interface that providers must implement to resolve flag values for their particular
@@ -30,7 +31,7 @@ export interface Provider extends CommonProvider {
3031
flagKey: string,
3132
defaultValue: boolean,
3233
context: EvaluationContext,
33-
logger: Logger
34+
logger: Logger,
3435
): ResolutionDetails<boolean>;
3536

3637
/**
@@ -40,7 +41,7 @@ export interface Provider extends CommonProvider {
4041
flagKey: string,
4142
defaultValue: string,
4243
context: EvaluationContext,
43-
logger: Logger
44+
logger: Logger,
4445
): ResolutionDetails<string>;
4546

4647
/**
@@ -50,7 +51,7 @@ export interface Provider extends CommonProvider {
5051
flagKey: string,
5152
defaultValue: number,
5253
context: EvaluationContext,
53-
logger: Logger
54+
logger: Logger,
5455
): ResolutionDetails<number>;
5556

5657
/**
@@ -60,6 +61,6 @@ export interface Provider extends CommonProvider {
6061
flagKey: string,
6162
defaultValue: T,
6263
context: EvaluationContext,
63-
logger: Logger
64+
logger: Logger,
6465
): ResolutionDetails<T>;
6566
}

packages/client/test/hooks.spec.ts

Lines changed: 1 addition & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import {
44
Client,
55
FlagValueType,
66
EvaluationContext,
7-
Hook,
87
GeneralError,
98
OpenFeature,
9+
Hook,
1010
} from '../src';
1111

1212
const BOOLEAN_VALUE = true;
@@ -107,27 +107,6 @@ describe('Hooks', () => {
107107
});
108108

109109
describe('Requirement 4.1.4', () => {
110-
describe('before', () => {
111-
it('evaluationContext must be mutable', (done) => {
112-
client.getBooleanValue(FLAG_KEY, false, {
113-
hooks: [
114-
{
115-
before: (hookContext) => {
116-
try {
117-
// evaluation context is mutable in before, so this should work.
118-
hookContext.context.newBeforeProp = 'new!';
119-
expect(hookContext.context.newBeforeProp).toBeTruthy();
120-
done();
121-
} catch (err) {
122-
done(err);
123-
}
124-
},
125-
},
126-
],
127-
});
128-
});
129-
});
130-
131110
describe('after', () => {
132111
it('evaluationContext must be immutable', (done) => {
133112
client.getBooleanValue(FLAG_KEY, false, {
@@ -151,58 +130,6 @@ describe('Hooks', () => {
151130
});
152131
});
153132

154-
describe('4.3.2', () => {
155-
it('"before" must run before flag resolution', async () => {
156-
await client.getBooleanValue(FLAG_KEY, false, {
157-
hooks: [
158-
{
159-
before: () => {
160-
// add a prop to the context.
161-
return { beforeRan: true };
162-
},
163-
},
164-
],
165-
});
166-
167-
expect(MOCK_PROVIDER.resolveBooleanEvaluation).toHaveBeenCalledWith(
168-
expect.anything(),
169-
expect.anything(),
170-
// ensure property was added by the time the flag resolution occurred.
171-
expect.objectContaining({
172-
beforeRan: true,
173-
}),
174-
expect.anything()
175-
);
176-
});
177-
});
178-
179-
describe('Requirement 4.3.3', () => {
180-
it('EvaluationContext must be passed to next "before" hook', (done) => {
181-
client.getBooleanValue(FLAG_KEY, false, {
182-
hooks: [
183-
{
184-
before: () => {
185-
// add a prop to the context.
186-
return { beforeRan: true };
187-
},
188-
},
189-
{
190-
before: (hookContext) => {
191-
// ensure added prop exists in next hook
192-
try {
193-
expect(hookContext.context.beforeRan).toBeTruthy();
194-
done();
195-
} catch (err) {
196-
done(err);
197-
}
198-
return { beforeRan: true };
199-
},
200-
},
201-
],
202-
});
203-
});
204-
});
205-
206133
describe('Requirement 4.3.5', () => {
207134
it('"after" must run after flag evaluation', (done) => {
208135
client.getBooleanValue(FLAG_KEY, false, {

0 commit comments

Comments
 (0)