Skip to content

Commit f6f4313

Browse files
feat: copy multi-provider and multi-provider-web from contrib repo
Signed-off-by: Jonathan Norris <[email protected]>
1 parent 1dbbd51 commit f6f4313

29 files changed

+14462
-8204
lines changed

package-lock.json

Lines changed: 10844 additions & 8203 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/server/README.md

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,85 @@ OpenFeature.setProvider(new MyProvider());
140140
Once the provider has been registered, the status can be tracked using [events](#eventing).
141141

142142
In some situations, it may be beneficial to register multiple providers in the same application.
143-
This is possible using [domains](#domains), which is covered in more details below.
143+
This is possible using [domains](#domains), which is covered in more detail below.
144+
145+
#### Multi-Provider
146+
147+
The Multi-Provider allows you to use multiple underlying providers as sources of flag data for the OpenFeature server SDK. When a flag is being evaluated, the Multi-Provider will consult each underlying provider it is managing in order to determine the final result. Different evaluation strategies can be defined to control which providers get evaluated and which result is used.
148+
149+
The Multi-Provider is a powerful tool for performing migrations between flag providers, or combining multiple providers into a single feature flagging interface. For example:
150+
151+
- **Migration**: Gradually migrate from one provider to another by serving some flags from your old provider and some from your new provider
152+
- **Backup**: Use one provider as a backup for another in case of failures
153+
- **Comparison**: Compare results from multiple providers to validate consistency
154+
- **Hybrid**: Combine multiple providers to leverage different strengths (e.g., one for simple flags, another for complex targeting)
155+
156+
```ts
157+
import { OpenFeature, MultiProvider, FirstMatchStrategy } from '@openfeature/server-sdk';
158+
159+
// Create providers
160+
const primaryProvider = new YourPrimaryProvider();
161+
const backupProvider = new YourBackupProvider();
162+
163+
// Create multi-provider with a strategy
164+
const multiProvider = new MultiProvider(
165+
[primaryProvider, backupProvider],
166+
new FirstMatchStrategy()
167+
);
168+
169+
// Register the multi-provider
170+
await OpenFeature.setProviderAndWait(multiProvider);
171+
172+
// Use as normal
173+
const client = OpenFeature.getClient();
174+
const value = await client.getBooleanValue('my-flag', false);
175+
```
176+
177+
**Available Strategies:**
178+
179+
- `FirstMatchStrategy`: Returns the first successful result from the list of providers
180+
- `ComparisonStrategy`: Compares results from multiple providers and can handle discrepancies
181+
182+
**Migration Example:**
183+
184+
```ts
185+
import { OpenFeature, MultiProvider, FirstMatchStrategy } from '@openfeature/server-sdk';
186+
187+
// During migration, serve some flags from the new provider and fallback to the old one
188+
const newProvider = new NewFlagProvider();
189+
const oldProvider = new OldFlagProvider();
190+
191+
const multiProvider = new MultiProvider(
192+
[newProvider, oldProvider], // New provider is consulted first
193+
new FirstMatchStrategy()
194+
);
195+
196+
await OpenFeature.setProviderAndWait(multiProvider);
197+
```
198+
199+
**Comparison Example:**
200+
201+
```ts
202+
import { OpenFeature, MultiProvider, ComparisonStrategy } from '@openfeature/server-sdk';
203+
204+
// Compare results from two providers for validation
205+
const providerA = new ProviderA();
206+
const providerB = new ProviderB();
207+
208+
const comparisonStrategy = new ComparisonStrategy({
209+
primary: 0, // Use first provider as primary
210+
onMismatch: (flagKey, primaryResult, results) => {
211+
console.warn(`Mismatch for ${flagKey}:`, primaryResult, results);
212+
}
213+
});
214+
215+
const multiProvider = new MultiProvider(
216+
[providerA, providerB],
217+
comparisonStrategy
218+
);
219+
220+
await OpenFeature.setProviderAndWait(multiProvider);
221+
```
144222

145223
### Targeting
146224

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './provider';
22
export * from './no-op-provider';
33
export * from './in-memory-provider';
4+
export * from './multi-provider';
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import type { ErrorCode } from '@openfeature/server-sdk';
2+
import { GeneralError, OpenFeatureError } from '@openfeature/server-sdk';
3+
import type { RegisteredProvider } from './types';
4+
5+
export class ErrorWithCode extends OpenFeatureError {
6+
constructor(
7+
public code: ErrorCode,
8+
message: string,
9+
) {
10+
super(message);
11+
}
12+
}
13+
14+
export class AggregateError extends GeneralError {
15+
constructor(
16+
message: string,
17+
public originalErrors: { source: string; error: unknown }[],
18+
) {
19+
super(message);
20+
}
21+
}
22+
23+
export const constructAggregateError = (providerErrors: { error: unknown; providerName: string }[]) => {
24+
const errorsWithSource = providerErrors
25+
.map(({ providerName, error }) => {
26+
return { source: providerName, error };
27+
})
28+
.flat();
29+
30+
// log first error in the message for convenience, but include all errors in the error object for completeness
31+
return new AggregateError(
32+
`Provider errors occurred: ${errorsWithSource[0].source}: ${errorsWithSource[0].error}`,
33+
errorsWithSource,
34+
);
35+
};
36+
37+
export const throwAggregateErrorFromPromiseResults = (
38+
result: PromiseSettledResult<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+
};
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import type { EvaluationDetails, FlagValue, Hook, HookContext, HookHints, Logger } from '@openfeature/server-sdk';
2+
3+
/**
4+
* Utility for executing a set of hooks of each type. Implementation is largely copied from the main OpenFeature SDK.
5+
*/
6+
export class HookExecutor {
7+
constructor(private logger: Logger) {}
8+
9+
async beforeHooks(hooks: Hook[] | undefined, hookContext: HookContext, hints: HookHints) {
10+
for (const hook of hooks ?? []) {
11+
// freeze the hookContext
12+
Object.freeze(hookContext);
13+
14+
Object.assign(hookContext.context, {
15+
...(await hook?.before?.(hookContext, Object.freeze(hints))),
16+
});
17+
}
18+
19+
// after before hooks, freeze the EvaluationContext.
20+
return Object.freeze(hookContext.context);
21+
}
22+
23+
async afterHooks(
24+
hooks: Hook[] | undefined,
25+
hookContext: HookContext,
26+
evaluationDetails: EvaluationDetails<FlagValue>,
27+
hints: HookHints,
28+
) {
29+
// run "after" hooks sequentially
30+
for (const hook of hooks ?? []) {
31+
await hook?.after?.(hookContext, evaluationDetails, hints);
32+
}
33+
}
34+
35+
async errorHooks(hooks: Hook[] | undefined, hookContext: HookContext, err: unknown, hints: HookHints) {
36+
// run "error" hooks sequentially
37+
for (const hook of hooks ?? []) {
38+
try {
39+
await hook?.error?.(hookContext, err, hints);
40+
} catch (err) {
41+
this.logger.error(`Unhandled error during 'error' hook: ${err}`);
42+
if (err instanceof Error) {
43+
this.logger.error(err.stack);
44+
}
45+
this.logger.error((err as Error)?.stack);
46+
}
47+
}
48+
}
49+
50+
async finallyHooks(
51+
hooks: Hook[] | undefined,
52+
hookContext: HookContext,
53+
evaluationDetails: EvaluationDetails<FlagValue>,
54+
hints: HookHints,
55+
) {
56+
// run "finally" hooks sequentially
57+
for (const hook of hooks ?? []) {
58+
try {
59+
await hook?.finally?.(hookContext, evaluationDetails, hints);
60+
} catch (err) {
61+
this.logger.error(`Unhandled error during 'finally' hook: ${err}`);
62+
if (err instanceof Error) {
63+
this.logger.error(err.stack);
64+
}
65+
this.logger.error((err as Error)?.stack);
66+
}
67+
}
68+
}
69+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './multi-provider';
2+
export * from './errors';
3+
export * from './strategies';

0 commit comments

Comments
 (0)