Skip to content

Commit 97bdfaa

Browse files
committed
address review comments
Signed-off-by: Adam Wootton <[email protected]>
1 parent 30245ad commit 97bdfaa

File tree

8 files changed

+79
-43
lines changed

8 files changed

+79
-43
lines changed

libs/providers/multi-provider/src/lib/errors.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { ErrorCode, GeneralError, OpenFeatureError } from '@openfeature/server-sdk';
2+
import { PromiseAllSettledResult } from '@opentelemetry/sdk-metrics/build/esnext/utils';
3+
import { RegisteredProvider } from './types';
24

35
export class ErrorWithCode extends OpenFeatureError {
46
constructor(
@@ -31,3 +33,21 @@ export const constructAggregateError = (providerErrors: { error: unknown; provid
3133
errorsWithSource,
3234
);
3335
};
36+
37+
export const throwAggregateErrorFromPromiseResults = (
38+
result: PromiseAllSettledResult<unknown>[],
39+
providerEntries: RegisteredProvider[],
40+
) => {
41+
const errors = result
42+
.map((r, i) => {
43+
if (r.status === 'rejected') {
44+
return { error: r.reason, providerName: providerEntries[i].name };
45+
}
46+
return null;
47+
})
48+
.filter((val): val is { error: unknown; providerName: string } => Boolean(val));
49+
50+
if (errors.length) {
51+
throw constructAggregateError(errors);
52+
}
53+
};

libs/providers/multi-provider/src/lib/multi-provider.ts

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,15 @@ import {
1010
Logger,
1111
OpenFeatureEventEmitter,
1212
Provider,
13+
ProviderMetadata,
14+
BeforeHookContext,
1315
ResolutionDetails,
1416
} from '@openfeature/server-sdk';
15-
import { BeforeHookContext, ProviderMetadata } from '@openfeature/core';
1617
import { HookExecutor } from './hook-executor';
17-
import { constructAggregateError } from './errors';
18+
import { constructAggregateError, throwAggregateErrorFromPromiseResults } from './errors';
1819
import { BaseEvaluationStrategy, ProviderResolutionResult, FirstMatchStrategy } from './strategies';
1920
import { StatusTracker } from './status-tracker';
20-
21-
// Represents an entry in the constructor's provider array which may or may not have a name set
22-
export type ProviderEntryInput = {
23-
provider: Provider;
24-
name?: string;
25-
};
26-
27-
// Represents a processed and "registered" provider entry where a name has been chosen
28-
export type RegisteredProvider = {
29-
name: string;
30-
provider: Provider;
31-
};
21+
import { ProviderEntryInput, RegisteredProvider } from './types';
3222

3323
export class MultiProvider implements Provider {
3424
readonly runsOn = 'server';
@@ -76,7 +66,7 @@ export class MultiProvider implements Provider {
7666
throw new Error('Provider names must be unique');
7767
}
7868

79-
providersByName[candidateName] ||= [];
69+
providersByName[candidateName] ??= [];
8070
providersByName[candidateName].push(constructorProvider.provider);
8171
}
8272

@@ -96,11 +86,15 @@ export class MultiProvider implements Provider {
9686
}
9787

9888
async initialize(context?: EvaluationContext): Promise<void> {
99-
await Promise.all(this.providerEntries.map((provider) => provider.provider.initialize?.(context)));
89+
const result = await Promise.allSettled(
90+
this.providerEntries.map((provider) => provider.provider.initialize?.(context)),
91+
);
92+
throwAggregateErrorFromPromiseResults(result, this.providerEntries);
10093
}
10194

10295
async onClose(): Promise<void> {
103-
await Promise.all(this.providerEntries.map((provider) => provider.provider.onClose?.()));
96+
const result = await Promise.allSettled(this.providerEntries.map((provider) => provider.provider.onClose?.()));
97+
throwAggregateErrorFromPromiseResults(result, this.providerEntries);
10498
}
10599

106100
resolveBooleanEvaluation(
@@ -145,11 +139,11 @@ export class MultiProvider implements Provider {
145139
const hookHints = this.hookHints.get(context);
146140

147141
if (!hookContext || !hookHints) {
148-
throw new GeneralError(`Hook context not available for evaluation`);
142+
throw new GeneralError('Hook context not available for evaluation');
149143
}
150144

151-
let tasks: Promise<boolean>[] = [];
152-
let resolutions: ProviderResolutionResult<T>[] = [];
145+
const tasks: Promise<boolean>[] = [];
146+
const resolutions: ProviderResolutionResult<T>[] = [];
153147

154148
for (const providerEntry of this.providerEntries) {
155149
const task = this.evaluateProviderEntry(
@@ -232,11 +226,12 @@ export class MultiProvider implements Provider {
232226
}
233227

234228
if (this.evaluationStrategy.runMode === 'sequential') {
235-
if (
236-
!this.evaluationStrategy.shouldEvaluateNextProvider(strategyContext, context, evaluationResult, thrownError)
237-
) {
238-
return false;
239-
}
229+
return this.evaluationStrategy.shouldEvaluateNextProvider(
230+
strategyContext,
231+
context,
232+
evaluationResult,
233+
thrownError,
234+
);
240235
}
241236
return true;
242237
}

libs/providers/multi-provider/src/lib/status-tracker.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { EventDetails, OpenFeatureEventEmitter, ProviderEvents, ProviderStatus } from '@openfeature/server-sdk';
2-
import { RegisteredProvider } from '@openfeature/multi-provider';
2+
import { RegisteredProvider } from './types';
33

44
/**
55
* Tracks each individual provider's status by listening to emitted events

libs/providers/multi-provider/src/lib/strategies/BaseEvaluationStrategy.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
ProviderStatus,
77
ResolutionDetails,
88
} from '@openfeature/server-sdk';
9-
import { ErrorWithCode } from '@openfeature/multi-provider';
9+
import { ErrorWithCode } from '../errors';
1010

1111
export type StrategyEvaluationContext = {
1212
flagKey: string;
@@ -18,12 +18,23 @@ export type StrategyPerProviderContext = StrategyEvaluationContext & {
1818
providerStatus: ProviderStatus;
1919
};
2020

21-
export type ProviderResolutionResult<T extends FlagValue> = {
21+
type ProviderResolutionResultBase = {
2222
provider: Provider;
2323
providerName: string;
24-
thrownError?: unknown;
25-
details?: ResolutionDetails<T>;
2624
};
25+
26+
export type ProviderResolutionSuccessResult<T extends FlagValue> = ProviderResolutionResultBase & {
27+
details: ResolutionDetails<T>;
28+
};
29+
30+
export type ProviderResolutionErrorResult = ProviderResolutionResultBase & {
31+
thrownError: unknown;
32+
};
33+
34+
export type ProviderResolutionResult<T extends FlagValue> =
35+
| ProviderResolutionSuccessResult<T>
36+
| ProviderResolutionErrorResult;
37+
2738
export type FinalResult<T extends FlagValue> = {
2839
details?: ResolutionDetails<T>;
2940
provider?: Provider;
@@ -67,16 +78,15 @@ export abstract class BaseEvaluationStrategy {
6778
): FinalResult<T>;
6879

6980
protected hasError(resolution: ProviderResolutionResult<FlagValue>): boolean {
70-
return 'thrownError' in resolution || !!resolution.details?.errorCode;
81+
return 'thrownError' in resolution || !!resolution.details.errorCode;
7182
}
7283

7384
protected collectProviderErrors<T extends FlagValue>(resolutions: ProviderResolutionResult<T>[]): FinalResult<T> {
74-
let errors: FinalResult<FlagValue>['errors'] = [];
85+
const errors: FinalResult<FlagValue>['errors'] = [];
7586
for (const resolution of resolutions) {
7687
if ('thrownError' in resolution) {
7788
errors.push({ providerName: resolution.providerName, error: resolution.thrownError });
78-
}
79-
if (resolution.details?.errorCode) {
89+
} else if (resolution.details.errorCode) {
8090
errors.push({
8191
providerName: resolution.providerName,
8292
error: new ErrorWithCode(resolution.details.errorCode, resolution.details.errorMessage ?? 'unknown error'),
@@ -86,7 +96,7 @@ export abstract class BaseEvaluationStrategy {
8696
return { errors };
8797
}
8898

89-
protected resolutionToFinalResult<T extends FlagValue>(resolution: ProviderResolutionResult<T>) {
99+
protected resolutionToFinalResult<T extends FlagValue>(resolution: ProviderResolutionSuccessResult<T>) {
90100
return { details: resolution.details, provider: resolution.provider, providerName: resolution.providerName };
91101
}
92102
}

libs/providers/multi-provider/src/lib/strategies/ComparisonStrategy.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
BaseEvaluationStrategy,
33
FinalResult,
44
ProviderResolutionResult,
5+
ProviderResolutionSuccessResult,
56
StrategyPerProviderContext,
67
} from './BaseEvaluationStrategy';
78
import { EvaluationContext, FlagValue, Provider } from '@openfeature/server-sdk';
@@ -25,29 +26,29 @@ export class ComparisonStrategy extends BaseEvaluationStrategy {
2526
override determineFinalResult<T extends FlagValue>(
2627
strategyContext: StrategyPerProviderContext,
2728
context: EvaluationContext,
28-
resolutions: ProviderResolutionResult<T>[],
29+
resolutions: ProviderResolutionSuccessResult<T>[],
2930
): FinalResult<T> {
3031
let value: T | undefined;
31-
let fallbackResolution: ProviderResolutionResult<T> | undefined;
32+
let fallbackResolution: ProviderResolutionSuccessResult<T> | undefined;
3233
let mismatch = false;
3334
for (const resolution of resolutions) {
34-
if (resolution.thrownError || resolution.details?.errorCode) {
35+
if ('thrownError' in resolution || resolution.details.errorCode) {
3536
return this.collectProviderErrors(resolutions);
3637
}
3738
if (resolution.provider === this.fallbackProvider) {
3839
fallbackResolution = resolution;
3940
}
40-
if (typeof value !== 'undefined' && value !== resolution.details!.value) {
41+
if (typeof value !== 'undefined' && value !== resolution.details.value) {
4142
mismatch = true;
4243
} else {
43-
value = resolution.details!.value;
44+
value = resolution.details.value;
4445
}
4546
}
4647

4748
if (mismatch) {
4849
this.onMismatch?.(resolutions);
4950
return {
50-
details: fallbackResolution!.details!,
51+
details: fallbackResolution!.details,
5152
provider: fallbackResolution!.provider,
5253
};
5354
}

libs/providers/multi-provider/src/lib/strategies/FirstMatchStrategy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export class FirstMatchStrategy extends BaseEvaluationStrategy {
3232
resolutions: ProviderResolutionResult<T>[],
3333
): FinalResult<T> {
3434
const finalResolution = resolutions[resolutions.length - 1];
35-
if (this.hasError(finalResolution)) {
35+
if ('thrownError' in finalResolution || !!finalResolution.details.errorCode) {
3636
return this.collectProviderErrors(resolutions);
3737
}
3838
return this.resolutionToFinalResult(finalResolution);

libs/providers/multi-provider/src/lib/strategies/FirstSuccessfulStrategy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export class FirstSuccessfulStrategy extends BaseEvaluationStrategy {
2828
resolutions: ProviderResolutionResult<T>[],
2929
): FinalResult<T> {
3030
const finalResolution = resolutions[resolutions.length - 1];
31-
if (this.hasError(finalResolution)) {
31+
if ('thrownError' in finalResolution || !!finalResolution.details.errorCode) {
3232
return this.collectProviderErrors(resolutions);
3333
}
3434
return this.resolutionToFinalResult(finalResolution);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Represents an entry in the constructor's provider array which may or may not have a name set
2+
import { Provider } from '@openfeature/server-sdk';
3+
4+
export type ProviderEntryInput = {
5+
provider: Provider;
6+
name?: string;
7+
};
8+
9+
// Represents a processed and "registered" provider entry where a name has been chosen
10+
export type RegisteredProvider = Required<ProviderEntryInput>;

0 commit comments

Comments
 (0)