Skip to content

Commit 98ba00a

Browse files
beeme1mrtoddbaert
andauthored
feat: add support for domains (#805)
## This PR - adds domain as a concept to the server and web SDK - adds a deprecation warning anywhere client name was exposed to users. - fixes an issue in the web SDK where context set on a domain before a provider is registered was not used. ## Addresses fixes #820 4aa9657 ### Notes This change is based on [this spec](open-feature/spec#229) change. I tried to make it a non-breaking change but I may have missed an untested condition. Please carefully review to make sure I didn't miss anything. ### Follow-up Tasks - Update the doc readme parser to support "domain". - Update the NestJS and React SDKS. We should consider making those a breaking change since they're sub 1.0. --------- Signed-off-by: Michael Beemer <[email protected]> Signed-off-by: Todd Baert <[email protected]> Co-authored-by: Todd Baert <[email protected]>
1 parent e2f24fc commit 98ba00a

19 files changed

+564
-460
lines changed

packages/client/README.md

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -89,16 +89,16 @@ See [here](https://open-feature.github.io/js-sdk/modules/_openfeature_web_sdk.ht
8989

9090
## 🌟 Features
9191

92-
| Status | Features | Description |
93-
| ------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
94-
|| [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. |
95-
|| [Targeting](#targeting-and-context) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). |
96-
|| [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. |
97-
|| [Logging](#logging) | Integrate with popular logging packages. |
98-
|| [Named clients](#named-clients) | Utilize multiple providers in a single application. |
99-
|| [Eventing](#eventing) | React to state changes in the provider or flag management system. |
100-
|| [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
101-
|| [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |
92+
| Status | Features | Description |
93+
| ------ | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
94+
|| [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. |
95+
|| [Targeting](#targeting-and-context) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). |
96+
|| [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. |
97+
|| [Logging](#logging) | Integrate with popular logging packages. |
98+
|| [Domains](#domains) | Logically bind clients with providers. |
99+
|| [Eventing](#eventing) | React to state changes in the provider or flag management system. |
100+
|| [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
101+
|| [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |
102102

103103
<sub>Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌</sub>
104104

@@ -129,7 +129,7 @@ OpenFeature.setProvider(new MyProvider());
129129
Once the provider has been registered, the status can be tracked using [events](#eventing).
130130

131131
In some situations, it may be beneficial to register multiple providers in the same application.
132-
This is possible using [named clients](#named-clients), which is covered in more detail below.
132+
This is possible using [domains](#domains), which is covered in more detail below.
133133

134134
### Flag evaluation flow
135135

@@ -205,26 +205,29 @@ const client = OpenFeature.getClient();
205205
client.setLogger(logger);
206206
```
207207

208-
### Named clients
208+
### Domains
209209

210-
Clients can be given a name.
211-
A name is a logical identifier that can be used to associate clients with a particular provider.
212-
If a name has no associated provider, the global provider is used.
210+
Clients can be assigned to a domain.
211+
A domain is a logical identifier which can be used to associate clients with a particular provider.
212+
If a domain has no associated provider, the default provider is used.
213213

214214
```ts
215-
import { OpenFeature } from "@openfeature/web-sdk";
215+
import { OpenFeature, InMemoryProvider } from "@openfeature/web-sdk";
216216

217217
// Registering the default provider
218-
OpenFeature.setProvider(NewLocalProvider());
219-
// Registering a named provider
220-
OpenFeature.setProvider("clientForCache", new NewCachedProvider());
218+
OpenFeature.setProvider(InMemoryProvider(myFlags));
219+
// Registering a provider to a domain
220+
OpenFeature.setProvider("my-domain", new InMemoryProvider(someOtherFlags));
221221

222-
// A Client backed by default provider
222+
// A Client bound to the default provider
223223
const clientWithDefault = OpenFeature.getClient();
224-
// A Client backed by NewCachedProvider
225-
const clientForCache = OpenFeature.getClient("clientForCache");
224+
// A Client bound to the InMemoryProvider provider
225+
const domainScopedClient = OpenFeature.getClient("my-domain");
226226
```
227227

228+
Domains can be defined on a provider during registration.
229+
For more details, please refer to the [providers](#providers) section.
230+
228231
### Eventing
229232

230233
Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions.

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ import { Provider } from '../provider';
2525
import { Client } from './client';
2626

2727
type OpenFeatureClientOptions = {
28+
/**
29+
* @deprecated Use `domain` instead.
30+
*/
2831
name?: string;
32+
domain?: string;
2933
version?: string;
3034
};
3135

@@ -44,7 +48,9 @@ export class OpenFeatureClient implements Client {
4448

4549
get metadata(): ClientMetadata {
4650
return {
47-
name: this.options.name,
51+
// Use domain if name is not provided
52+
name: this.options.domain ?? this.options.name,
53+
domain: this.options.domain ?? this.options.name,
4854
version: this.options.version,
4955
providerMetadata: this.providerAccessor().metadata,
5056
};
@@ -61,7 +67,11 @@ export class OpenFeatureClient implements Client {
6167
if (shouldRunNow) {
6268
// run immediately, we're in the matching state
6369
try {
64-
handler({ clientName: this.metadata.name, providerName: this._provider.metadata.name });
70+
handler({
71+
clientName: this.metadata.name,
72+
domain: this.metadata.domain,
73+
providerName: this._provider.metadata.name,
74+
});
6575
} catch (err) {
6676
this._logger?.error('Error running event handler:', err);
6777
}
@@ -179,7 +189,7 @@ export class OpenFeatureClient implements Client {
179189
const allHooksReversed = [...allHooks].reverse();
180190

181191
const context = {
182-
...OpenFeature.getContext(this?.options?.name),
192+
...OpenFeature.getContext(this?.options?.domain),
183193
};
184194

185195
// this reference cannot change during the course of evaluation

packages/client/src/open-feature.ts

Lines changed: 59 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,17 @@ const GLOBAL_OPENFEATURE_API_KEY = Symbol.for('@openfeature/web-sdk/api');
1717
type OpenFeatureGlobal = {
1818
[GLOBAL_OPENFEATURE_API_KEY]?: OpenFeatureAPI;
1919
};
20-
type NameProviderRecord = {
21-
name?: string;
20+
type DomainRecord = {
21+
domain?: string;
2222
provider: Provider;
23-
}
23+
};
2424

2525
const _globalThis = globalThis as OpenFeatureGlobal;
2626

2727
export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider, Hook> implements ManageContext<Promise<void>> {
2828
protected _events: GenericEventEmitter<ProviderEvents> = new OpenFeatureEventEmitter();
2929
protected _defaultProvider: Provider = NOOP_PROVIDER;
3030
protected _createEventEmitter = () => new OpenFeatureEventEmitter();
31-
protected _namedProviderContext: Map<string, EvaluationContext> = new Map();
3231

3332
private constructor() {
3433
super('client');
@@ -52,56 +51,56 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider, Hook> impleme
5251

5352
/**
5453
* Sets the evaluation context globally.
55-
* This will be used by all providers that have not been overridden with a named client.
54+
* This will be used by all providers that have not bound to a domain.
5655
* @param {EvaluationContext} context Evaluation context
5756
* @example
5857
* await OpenFeature.setContext({ region: "us" });
5958
*/
6059
async setContext(context: EvaluationContext): Promise<void>;
6160
/**
6261
* Sets the evaluation context for a specific provider.
63-
* This will only affect providers with a matching client name.
64-
* @param {string} clientName The name to identify the client
62+
* This will only affect providers bound to a domain.
63+
* @param {string} domain An identifier which logically binds clients with providers
6564
* @param {EvaluationContext} context Evaluation context
6665
* @example
6766
* await OpenFeature.setContext("test", { scope: "provider" });
6867
* OpenFeature.setProvider(new MyProvider()) // Uses the default context
6968
* OpenFeature.setProvider("test", new MyProvider()) // Uses context: { scope: "provider" }
7069
*/
71-
async setContext(clientName: string, context: EvaluationContext): Promise<void>;
72-
async setContext<T extends EvaluationContext>(nameOrContext: T | string, contextOrUndefined?: T): Promise<void> {
73-
const clientName = stringOrUndefined(nameOrContext);
74-
const context = objectOrUndefined<T>(nameOrContext) ?? objectOrUndefined(contextOrUndefined) ?? {};
70+
async setContext(domain: string, context: EvaluationContext): Promise<void>;
71+
async setContext<T extends EvaluationContext>(domainOrContext: T | string, contextOrUndefined?: T): Promise<void> {
72+
const domain = stringOrUndefined(domainOrContext);
73+
const context = objectOrUndefined<T>(domainOrContext) ?? objectOrUndefined(contextOrUndefined) ?? {};
7574

76-
if (clientName) {
77-
const provider = this._clientProviders.get(clientName);
75+
if (domain) {
76+
const provider = this._domainScopedProviders.get(domain);
7877
if (provider) {
79-
const oldContext = this.getContext(clientName);
80-
this._namedProviderContext.set(clientName, context);
81-
await this.runProviderContextChangeHandler(clientName, provider, oldContext, context);
78+
const oldContext = this.getContext(domain);
79+
this._domainScopedContext.set(domain, context);
80+
await this.runProviderContextChangeHandler(domain, provider, oldContext, context);
8281
} else {
83-
this._namedProviderContext.set(clientName, context);
82+
this._domainScopedContext.set(domain, context);
8483
}
8584
} else {
8685
const oldContext = this._context;
8786
this._context = context;
8887

89-
// collect all providers that are using the default context (not mapped to a name)
90-
const defaultContextNameProviders: NameProviderRecord[] = Array.from(this._clientProviders.entries())
91-
.filter(([name]) => !this._namedProviderContext.has(name))
92-
.reduce<NameProviderRecord[]>((acc, [name, provider]) => {
93-
acc.push({ name, provider });
88+
// collect all providers that are using the default context (not bound to a domain)
89+
const unboundProviders: DomainRecord[] = Array.from(this._domainScopedProviders.entries())
90+
.filter(([domain]) => !this._domainScopedContext.has(domain))
91+
.reduce<DomainRecord[]>((acc, [domain, provider]) => {
92+
acc.push({ domain, provider });
9493
return acc;
9594
}, []);
9695

97-
const allProviders: NameProviderRecord[] = [
98-
// add in the default (no name)
99-
{ name: undefined, provider: this._defaultProvider },
100-
...defaultContextNameProviders,
96+
const allProviders: DomainRecord[] = [
97+
// add in the default (no domain)
98+
{ domain: undefined, provider: this._defaultProvider },
99+
...unboundProviders,
101100
];
102101
await Promise.all(
103102
allProviders.map((tuple) =>
104-
this.runProviderContextChangeHandler(tuple.name, tuple.provider, oldContext, context),
103+
this.runProviderContextChangeHandler(tuple.domain, tuple.provider, oldContext, context),
105104
),
106105
);
107106
}
@@ -115,18 +114,18 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider, Hook> impleme
115114
/**
116115
* Access the evaluation context for a specific named client.
117116
* The global evaluation context is returned if a matching named client is not found.
118-
* @param {string} clientName The name to identify the client
117+
* @param {string} domain An identifier which logically binds clients with providers
119118
* @returns {EvaluationContext} Evaluation context
120119
*/
121-
getContext(clientName?: string): EvaluationContext;
122-
getContext(nameOrUndefined?: string): EvaluationContext {
123-
const clientName = stringOrUndefined(nameOrUndefined);
124-
if (clientName) {
125-
const context = this._namedProviderContext.get(clientName);
120+
getContext(domain?: string): EvaluationContext;
121+
getContext(domainOrUndefined?: string): EvaluationContext {
122+
const domain = stringOrUndefined(domainOrUndefined);
123+
if (domain) {
124+
const context = this._domainScopedContext.get(domain);
126125
if (context) {
127126
return context;
128127
} else {
129-
this._logger.debug(`Unable to find context for '${clientName}'.`);
128+
this._logger.debug(`Unable to find context for '${domain}'.`);
130129
}
131130
}
132131
return this._context;
@@ -138,20 +137,20 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider, Hook> impleme
138137
clearContext(): Promise<void>;
139138
/**
140139
* Removes the evaluation context for a specific named client.
141-
* @param {string} clientName The name to identify the client
140+
* @param {string} domain An identifier which logically binds clients with providers
142141
*/
143-
clearContext(clientName: string): Promise<void>;
144-
async clearContext(nameOrUndefined?: string): Promise<void> {
145-
const clientName = stringOrUndefined(nameOrUndefined);
146-
if (clientName) {
147-
const provider = this._clientProviders.get(clientName);
142+
clearContext(domain: string): Promise<void>;
143+
async clearContext(domainOrUndefined?: string): Promise<void> {
144+
const domain = stringOrUndefined(domainOrUndefined);
145+
if (domain) {
146+
const provider = this._domainScopedProviders.get(domain);
148147
if (provider) {
149-
const oldContext = this.getContext(clientName);
150-
this._namedProviderContext.delete(clientName);
148+
const oldContext = this.getContext(domain);
149+
this._domainScopedContext.delete(domain);
151150
const newContext = this.getContext();
152-
await this.runProviderContextChangeHandler(clientName, provider, oldContext, newContext);
151+
await this.runProviderContextChangeHandler(domain, provider, oldContext, newContext);
153152
} else {
154-
this._namedProviderContext.delete(clientName);
153+
this._domainScopedContext.delete(domain);
155154
}
156155
} else {
157156
return this.setContext({});
@@ -160,15 +159,15 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider, Hook> impleme
160159

161160
/**
162161
* Resets the global evaluation context and removes the evaluation context for
163-
* all named clients.
162+
* all domains.
164163
*/
165164
async clearContexts(): Promise<void> {
166165
// Default context must be cleared first to avoid calling the onContextChange
167-
// handler multiple times for named clients.
166+
// handler multiple times for clients bound to a domain.
168167
await this.clearContext();
169168

170169
// Use allSettled so a promise rejection doesn't affect others
171-
await Promise.allSettled(Array.from(this._clientProviders.keys()).map((name) => this.clearContext(name)));
170+
await Promise.allSettled(Array.from(this._domainScopedProviders.keys()).map((domain) => this.clearContext(domain)));
172171
}
173172

174173
/**
@@ -178,18 +177,18 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider, Hook> impleme
178177
*
179178
* If there is already a provider bound to this name via {@link this.setProvider setProvider}, this provider will be used.
180179
* Otherwise, the default provider is used until a provider is assigned to that name.
181-
* @param {string} name The name of the client
180+
* @param {string} domain An identifier which logically binds clients with providers
182181
* @param {string} version The version of the client (only used for metadata)
183182
* @returns {Client} OpenFeature Client
184183
*/
185-
getClient(name?: string, version?: string): Client {
184+
getClient(domain?: string, version?: string): Client {
186185
return new OpenFeatureClient(
187186
// functions are passed here to make sure that these values are always up to date,
188187
// and so we don't have to make these public properties on the API class.
189-
() => this.getProviderForClient(name),
190-
() => this.buildAndCacheEventEmitterForClient(name),
188+
() => this.getProviderForClient(domain),
189+
() => this.buildAndCacheEventEmitterForClient(domain),
191190
() => this._logger,
192-
{ name, version },
191+
{ domain, version },
193192
);
194193
}
195194

@@ -199,11 +198,11 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider, Hook> impleme
199198
*/
200199
async clearProviders(): Promise<void> {
201200
await super.clearProvidersAndSetDefault(NOOP_PROVIDER);
202-
this._namedProviderContext.clear();
201+
this._domainScopedContext.clear();
203202
}
204203

205204
private async runProviderContextChangeHandler(
206-
clientName: string | undefined,
205+
domain: string | undefined,
207206
provider: Provider,
208207
oldContext: EvaluationContext,
209208
newContext: EvaluationContext,
@@ -213,19 +212,19 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider, Hook> impleme
213212
await provider.onContextChange?.(oldContext, newContext);
214213

215214
// only run the event handlers if the onContextChange method succeeded
216-
this.getAssociatedEventEmitters(clientName).forEach((emitter) => {
217-
emitter?.emit(ProviderEvents.ContextChanged, { clientName, providerName });
215+
this.getAssociatedEventEmitters(domain).forEach((emitter) => {
216+
emitter?.emit(ProviderEvents.ContextChanged, { clientName: domain, domain, providerName });
218217
});
219-
this._events?.emit(ProviderEvents.ContextChanged, { clientName, providerName });
218+
this._events?.emit(ProviderEvents.ContextChanged, { clientName: domain, domain, providerName });
220219
} catch (err) {
221220
// run error handlers instead
222221
const error = err as Error | undefined;
223222
const message = `Error running ${provider?.metadata?.name}'s context change handler: ${error?.message}`;
224223
this._logger?.error(`${message}`, err);
225-
this.getAssociatedEventEmitters(clientName).forEach((emitter) => {
226-
emitter?.emit(ProviderEvents.Error, { clientName, providerName, message });
224+
this.getAssociatedEventEmitters(domain).forEach((emitter) => {
225+
emitter?.emit(ProviderEvents.Error, { clientName: domain, domain, providerName, message });
227226
});
228-
this._events?.emit(ProviderEvents.Error, { clientName, providerName, message });
227+
this._events?.emit(ProviderEvents.Error, { clientName: domain, domain, providerName, message });
229228
}
230229
}
231230
}

0 commit comments

Comments
 (0)