diff --git a/packages/client/README.md b/packages/client/README.md index a60d22542..c9b2bb4ff 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -89,16 +89,16 @@ See [here](https://open-feature.github.io/js-sdk/modules/_openfeature_web_sdk.ht ## 🌟 Features -| Status | Features | Description | -| ------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -| ✅ | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. | -| ✅ | [Targeting](#targeting-and-context) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). | -| ✅ | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. | -| ✅ | [Logging](#logging) | Integrate with popular logging packages. | -| ✅ | [Named clients](#named-clients) | Utilize multiple providers in a single application. | -| ✅ | [Eventing](#eventing) | React to state changes in the provider or flag management system. | -| ✅ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | -| ✅ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | +| Status | Features | Description | +| ------ | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| ✅ | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. | +| ✅ | [Targeting](#targeting-and-context) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). | +| ✅ | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. | +| ✅ | [Logging](#logging) | Integrate with popular logging packages. | +| ✅ | [Domains](#domains) | Logically bind clients with providers. | +| ✅ | [Eventing](#eventing) | React to state changes in the provider or flag management system. | +| ✅ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | +| ✅ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌ @@ -129,7 +129,7 @@ OpenFeature.setProvider(new MyProvider()); Once the provider has been registered, the status can be tracked using [events](#eventing). In some situations, it may be beneficial to register multiple providers in the same application. -This is possible using [named clients](#named-clients), which is covered in more detail below. +This is possible using [domains](#domains), which is covered in more detail below. ### Flag evaluation flow @@ -205,26 +205,29 @@ const client = OpenFeature.getClient(); client.setLogger(logger); ``` -### Named clients +### Domains -Clients can be given a name. -A name is a logical identifier that can be used to associate clients with a particular provider. -If a name has no associated provider, the global provider is used. +Clients can be assigned to a domain. +A domain is a logical identifier which can be used to associate clients with a particular provider. +If a domain has no associated provider, the default provider is used. ```ts -import { OpenFeature } from "@openfeature/web-sdk"; +import { OpenFeature, InMemoryProvider } from "@openfeature/web-sdk"; // Registering the default provider -OpenFeature.setProvider(NewLocalProvider()); -// Registering a named provider -OpenFeature.setProvider("clientForCache", new NewCachedProvider()); +OpenFeature.setProvider(InMemoryProvider(myFlags)); +// Registering a provider to a domain +OpenFeature.setProvider("my-domain", new InMemoryProvider(someOtherFlags)); -// A Client backed by default provider +// A Client bound to the default provider const clientWithDefault = OpenFeature.getClient(); -// A Client backed by NewCachedProvider -const clientForCache = OpenFeature.getClient("clientForCache"); +// A Client bound to the InMemoryProvider provider +const domainScopedClient = OpenFeature.getClient("my-domain"); ``` +Domains can be defined on a provider during registration. +For more details, please refer to the [providers](#providers) section. + ### Eventing 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. diff --git a/packages/client/src/client/open-feature-client.ts b/packages/client/src/client/open-feature-client.ts index dc86f0a50..6f14dc249 100644 --- a/packages/client/src/client/open-feature-client.ts +++ b/packages/client/src/client/open-feature-client.ts @@ -25,7 +25,11 @@ import { Provider } from '../provider'; import { Client } from './client'; type OpenFeatureClientOptions = { + /** + * @deprecated Use `domain` instead. + */ name?: string; + domain?: string; version?: string; }; @@ -44,7 +48,9 @@ export class OpenFeatureClient implements Client { get metadata(): ClientMetadata { return { - name: this.options.name, + // Use domain if name is not provided + name: this.options.domain ?? this.options.name, + domain: this.options.domain ?? this.options.name, version: this.options.version, providerMetadata: this.providerAccessor().metadata, }; @@ -61,7 +67,11 @@ export class OpenFeatureClient implements Client { if (shouldRunNow) { // run immediately, we're in the matching state try { - handler({ clientName: this.metadata.name, providerName: this._provider.metadata.name }); + handler({ + clientName: this.metadata.name, + domain: this.metadata.domain, + providerName: this._provider.metadata.name, + }); } catch (err) { this._logger?.error('Error running event handler:', err); } @@ -179,7 +189,7 @@ export class OpenFeatureClient implements Client { const allHooksReversed = [...allHooks].reverse(); const context = { - ...OpenFeature.getContext(this?.options?.name), + ...OpenFeature.getContext(this?.options?.domain), }; // this reference cannot change during the course of evaluation diff --git a/packages/client/src/open-feature.ts b/packages/client/src/open-feature.ts index 77e3889db..b3a3bb893 100644 --- a/packages/client/src/open-feature.ts +++ b/packages/client/src/open-feature.ts @@ -17,10 +17,10 @@ const GLOBAL_OPENFEATURE_API_KEY = Symbol.for('@openfeature/web-sdk/api'); type OpenFeatureGlobal = { [GLOBAL_OPENFEATURE_API_KEY]?: OpenFeatureAPI; }; -type NameProviderRecord = { - name?: string; +type DomainRecord = { + domain?: string; provider: Provider; -} +}; const _globalThis = globalThis as OpenFeatureGlobal; @@ -28,7 +28,6 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI impleme protected _events: GenericEventEmitter = new OpenFeatureEventEmitter(); protected _defaultProvider: Provider = NOOP_PROVIDER; protected _createEventEmitter = () => new OpenFeatureEventEmitter(); - protected _namedProviderContext: Map = new Map(); private constructor() { super('client'); @@ -52,7 +51,7 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI impleme /** * Sets the evaluation context globally. - * This will be used by all providers that have not been overridden with a named client. + * This will be used by all providers that have not bound to a domain. * @param {EvaluationContext} context Evaluation context * @example * await OpenFeature.setContext({ region: "us" }); @@ -60,48 +59,48 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI impleme async setContext(context: EvaluationContext): Promise; /** * Sets the evaluation context for a specific provider. - * This will only affect providers with a matching client name. - * @param {string} clientName The name to identify the client + * This will only affect providers bound to a domain. + * @param {string} domain An identifier which logically binds clients with providers * @param {EvaluationContext} context Evaluation context * @example * await OpenFeature.setContext("test", { scope: "provider" }); * OpenFeature.setProvider(new MyProvider()) // Uses the default context * OpenFeature.setProvider("test", new MyProvider()) // Uses context: { scope: "provider" } */ - async setContext(clientName: string, context: EvaluationContext): Promise; - async setContext(nameOrContext: T | string, contextOrUndefined?: T): Promise { - const clientName = stringOrUndefined(nameOrContext); - const context = objectOrUndefined(nameOrContext) ?? objectOrUndefined(contextOrUndefined) ?? {}; + async setContext(domain: string, context: EvaluationContext): Promise; + async setContext(domainOrContext: T | string, contextOrUndefined?: T): Promise { + const domain = stringOrUndefined(domainOrContext); + const context = objectOrUndefined(domainOrContext) ?? objectOrUndefined(contextOrUndefined) ?? {}; - if (clientName) { - const provider = this._clientProviders.get(clientName); + if (domain) { + const provider = this._domainScopedProviders.get(domain); if (provider) { - const oldContext = this.getContext(clientName); - this._namedProviderContext.set(clientName, context); - await this.runProviderContextChangeHandler(clientName, provider, oldContext, context); + const oldContext = this.getContext(domain); + this._domainScopedContext.set(domain, context); + await this.runProviderContextChangeHandler(domain, provider, oldContext, context); } else { - this._namedProviderContext.set(clientName, context); + this._domainScopedContext.set(domain, context); } } else { const oldContext = this._context; this._context = context; - // collect all providers that are using the default context (not mapped to a name) - const defaultContextNameProviders: NameProviderRecord[] = Array.from(this._clientProviders.entries()) - .filter(([name]) => !this._namedProviderContext.has(name)) - .reduce((acc, [name, provider]) => { - acc.push({ name, provider }); + // collect all providers that are using the default context (not bound to a domain) + const unboundProviders: DomainRecord[] = Array.from(this._domainScopedProviders.entries()) + .filter(([domain]) => !this._domainScopedContext.has(domain)) + .reduce((acc, [domain, provider]) => { + acc.push({ domain, provider }); return acc; }, []); - const allProviders: NameProviderRecord[] = [ - // add in the default (no name) - { name: undefined, provider: this._defaultProvider }, - ...defaultContextNameProviders, + const allProviders: DomainRecord[] = [ + // add in the default (no domain) + { domain: undefined, provider: this._defaultProvider }, + ...unboundProviders, ]; await Promise.all( allProviders.map((tuple) => - this.runProviderContextChangeHandler(tuple.name, tuple.provider, oldContext, context), + this.runProviderContextChangeHandler(tuple.domain, tuple.provider, oldContext, context), ), ); } @@ -115,18 +114,18 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI impleme /** * Access the evaluation context for a specific named client. * The global evaluation context is returned if a matching named client is not found. - * @param {string} clientName The name to identify the client + * @param {string} domain An identifier which logically binds clients with providers * @returns {EvaluationContext} Evaluation context */ - getContext(clientName?: string): EvaluationContext; - getContext(nameOrUndefined?: string): EvaluationContext { - const clientName = stringOrUndefined(nameOrUndefined); - if (clientName) { - const context = this._namedProviderContext.get(clientName); + getContext(domain?: string): EvaluationContext; + getContext(domainOrUndefined?: string): EvaluationContext { + const domain = stringOrUndefined(domainOrUndefined); + if (domain) { + const context = this._domainScopedContext.get(domain); if (context) { return context; } else { - this._logger.debug(`Unable to find context for '${clientName}'.`); + this._logger.debug(`Unable to find context for '${domain}'.`); } } return this._context; @@ -138,20 +137,20 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI impleme clearContext(): Promise; /** * Removes the evaluation context for a specific named client. - * @param {string} clientName The name to identify the client + * @param {string} domain An identifier which logically binds clients with providers */ - clearContext(clientName: string): Promise; - async clearContext(nameOrUndefined?: string): Promise { - const clientName = stringOrUndefined(nameOrUndefined); - if (clientName) { - const provider = this._clientProviders.get(clientName); + clearContext(domain: string): Promise; + async clearContext(domainOrUndefined?: string): Promise { + const domain = stringOrUndefined(domainOrUndefined); + if (domain) { + const provider = this._domainScopedProviders.get(domain); if (provider) { - const oldContext = this.getContext(clientName); - this._namedProviderContext.delete(clientName); + const oldContext = this.getContext(domain); + this._domainScopedContext.delete(domain); const newContext = this.getContext(); - await this.runProviderContextChangeHandler(clientName, provider, oldContext, newContext); + await this.runProviderContextChangeHandler(domain, provider, oldContext, newContext); } else { - this._namedProviderContext.delete(clientName); + this._domainScopedContext.delete(domain); } } else { return this.setContext({}); @@ -160,15 +159,15 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI impleme /** * Resets the global evaluation context and removes the evaluation context for - * all named clients. + * all domains. */ async clearContexts(): Promise { // Default context must be cleared first to avoid calling the onContextChange - // handler multiple times for named clients. + // handler multiple times for clients bound to a domain. await this.clearContext(); // Use allSettled so a promise rejection doesn't affect others - await Promise.allSettled(Array.from(this._clientProviders.keys()).map((name) => this.clearContext(name))); + await Promise.allSettled(Array.from(this._domainScopedProviders.keys()).map((domain) => this.clearContext(domain))); } /** @@ -178,18 +177,18 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI impleme * * If there is already a provider bound to this name via {@link this.setProvider setProvider}, this provider will be used. * Otherwise, the default provider is used until a provider is assigned to that name. - * @param {string} name The name of the client + * @param {string} domain An identifier which logically binds clients with providers * @param {string} version The version of the client (only used for metadata) * @returns {Client} OpenFeature Client */ - getClient(name?: string, version?: string): Client { + getClient(domain?: string, version?: string): Client { return new OpenFeatureClient( // functions are passed here to make sure that these values are always up to date, // and so we don't have to make these public properties on the API class. - () => this.getProviderForClient(name), - () => this.buildAndCacheEventEmitterForClient(name), + () => this.getProviderForClient(domain), + () => this.buildAndCacheEventEmitterForClient(domain), () => this._logger, - { name, version }, + { domain, version }, ); } @@ -199,11 +198,11 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI impleme */ async clearProviders(): Promise { await super.clearProvidersAndSetDefault(NOOP_PROVIDER); - this._namedProviderContext.clear(); + this._domainScopedContext.clear(); } private async runProviderContextChangeHandler( - clientName: string | undefined, + domain: string | undefined, provider: Provider, oldContext: EvaluationContext, newContext: EvaluationContext, @@ -213,19 +212,19 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI impleme await provider.onContextChange?.(oldContext, newContext); // only run the event handlers if the onContextChange method succeeded - this.getAssociatedEventEmitters(clientName).forEach((emitter) => { - emitter?.emit(ProviderEvents.ContextChanged, { clientName, providerName }); + this.getAssociatedEventEmitters(domain).forEach((emitter) => { + emitter?.emit(ProviderEvents.ContextChanged, { clientName: domain, domain, providerName }); }); - this._events?.emit(ProviderEvents.ContextChanged, { clientName, providerName }); + this._events?.emit(ProviderEvents.ContextChanged, { clientName: domain, domain, providerName }); } catch (err) { // run error handlers instead const error = err as Error | undefined; const message = `Error running ${provider?.metadata?.name}'s context change handler: ${error?.message}`; this._logger?.error(`${message}`, err); - this.getAssociatedEventEmitters(clientName).forEach((emitter) => { - emitter?.emit(ProviderEvents.Error, { clientName, providerName, message }); + this.getAssociatedEventEmitters(domain).forEach((emitter) => { + emitter?.emit(ProviderEvents.Error, { clientName: domain, domain, providerName, message }); }); - this._events?.emit(ProviderEvents.Error, { clientName, providerName, message }); + this._events?.emit(ProviderEvents.Error, { clientName: domain, domain, providerName, message }); } } } diff --git a/packages/client/test/client.spec.ts b/packages/client/test/client.spec.ts index 73ca224a1..3088e5cf6 100644 --- a/packages/client/test/client.spec.ts +++ b/packages/client/test/client.spec.ts @@ -146,7 +146,7 @@ describe('OpenFeatureClient', () => { expect(provider.status).toBe(ProviderStatus.NOT_READY); await OpenFeature.setProviderAndWait(provider); expect(provider.status).toBe(ProviderStatus.READY); - expect(spy).toBeCalled(); + expect(spy).toHaveBeenCalled(); }); it('should wait for the provider to fail during initialization', async () => { @@ -156,7 +156,7 @@ describe('OpenFeatureClient', () => { expect(provider.status).toBe(ProviderStatus.NOT_READY); await expect(OpenFeature.setProviderAndWait(provider)).rejects.toThrow(); expect(provider.status).toBe(ProviderStatus.ERROR); - expect(spy).toBeCalled(); + expect(spy).toHaveBeenCalled(); }); }); @@ -167,10 +167,15 @@ describe('OpenFeatureClient', () => { }); describe('Requirement 1.2.1', () => { - const NAME = 'my-client'; - const client = OpenFeature.getClient(NAME); - it('should have metadata accessor with name', () => { - expect(client.metadata.name).toEqual(NAME); + const domain = 'my-domain'; + const client = OpenFeature.getClient(domain); + + it('should have metadata accessor with name for backwards compatibility', () => { + expect(client.metadata.name).toEqual(domain); + }); + + it('should have metadata accessor with domain', () => { + expect(client.metadata.domain).toEqual(domain); }); }); diff --git a/packages/client/test/evaluation-context.spec.ts b/packages/client/test/evaluation-context.spec.ts index 11892ddaf..1d9c66e4a 100644 --- a/packages/client/test/evaluation-context.spec.ts +++ b/packages/client/test/evaluation-context.spec.ts @@ -44,14 +44,14 @@ describe('Evaluation Context', () => { expect(OpenFeature.getContext()).toEqual(context); }); - it('the API MUST have a method for setting evaluation context for a named client', async () => { + it('the API MUST have a method for setting evaluation context for a domain', async () => { const context: EvaluationContext = { property1: false }; - const clientName = 'valid'; - await OpenFeature.setContext(clientName, context); - expect(OpenFeature.getContext(clientName)).toEqual(context); + const domain = 'valid'; + await OpenFeature.setContext(domain, context); + expect(OpenFeature.getContext(domain)).toEqual(context); }); - it('the API MUST return the default context if not match is found', async () => { + it('the API MUST return the default context if no matching domain is found', async () => { const defaultContext: EvaluationContext = { name: 'test' }; const nameContext: EvaluationContext = { property1: false }; await OpenFeature.setContext(defaultContext); @@ -68,33 +68,33 @@ describe('Evaluation Context', () => { expect(OpenFeature.getContext()).toEqual({}); }); - it('should remove context from a name provider', async () => { + it('should remove context from a domain', async () => { const globalContext: EvaluationContext = { scope: 'global' }; const testContext: EvaluationContext = { scope: 'test' }; - const clientName = 'test'; + const domain = 'test'; await OpenFeature.setContext(globalContext); - await OpenFeature.setContext(clientName, testContext); - expect(OpenFeature.getContext(clientName)).toEqual(testContext); - await OpenFeature.clearContext(clientName); - expect(OpenFeature.getContext(clientName)).toEqual(globalContext); + await OpenFeature.setContext(domain, testContext); + expect(OpenFeature.getContext(domain)).toEqual(testContext); + await OpenFeature.clearContext(domain); + expect(OpenFeature.getContext(domain)).toEqual(globalContext); }); it('should call onContextChange for appropriate provider with appropriate context', async () => { const globalContext: EvaluationContext = { scope: 'global' }; const testContext: EvaluationContext = { scope: 'test' }; - const clientName = 'appropriateProviderTest'; + const domain = 'appropriateProviderTest'; const defaultProvider = new MockProvider(); const provider1 = new MockProvider(); await OpenFeature.setProviderAndWait(defaultProvider); - await OpenFeature.setProviderAndWait(clientName, provider1); + await OpenFeature.setProviderAndWait(domain, provider1); // Spy on context changed handlers of both providers const defaultProviderSpy = jest.spyOn(defaultProvider, 'onContextChange'); const provider1Spy = jest.spyOn(provider1, 'onContextChange'); await OpenFeature.setContext(globalContext); - await OpenFeature.setContext(clientName, testContext); + await OpenFeature.setContext(domain, testContext); // provider one should get global and specific context calls expect(defaultProviderSpy).toHaveBeenCalledWith({}, globalContext); @@ -104,22 +104,22 @@ describe('Evaluation Context', () => { it('should pass correct context to resolver', async () => { const globalContext: EvaluationContext = { scope: 'global' }; const testContext: EvaluationContext = { scope: 'test' }; - const clientName = 'correctContextTest'; + const domain = 'correctContextTest'; const defaultProvider = new MockProvider(); const provider1 = new MockProvider(); await OpenFeature.setProviderAndWait(defaultProvider); - await OpenFeature.setProviderAndWait(clientName, provider1); + await OpenFeature.setProviderAndWait(domain, provider1); // Spy on boolean resolvers of both providers const defaultProviderSpy = jest.spyOn(defaultProvider, 'resolveBooleanEvaluation'); const provider1Spy = jest.spyOn(provider1, 'resolveBooleanEvaluation'); await OpenFeature.setContext(globalContext); - await OpenFeature.setContext(clientName, testContext); + await OpenFeature.setContext(domain, testContext); const defaultClient = OpenFeature.getClient(); - const provider1Client = OpenFeature.getClient(clientName); + const provider1Client = OpenFeature.getClient(domain); const flagName = 'some-flag'; defaultClient.getBooleanValue(flagName, false); @@ -133,15 +133,15 @@ describe('Evaluation Context', () => { it('should only call a providers onContextChange once when clearing context', async () => { const globalContext: EvaluationContext = { scope: 'global' }; const testContext: EvaluationContext = { scope: 'test' }; - const clientName = 'test'; + const domain = 'test'; await OpenFeature.setContext(globalContext); - await OpenFeature.setContext(clientName, testContext); + await OpenFeature.setContext(domain, testContext); const defaultProvider = new MockProvider(); const provider1 = new MockProvider(); OpenFeature.setProvider(defaultProvider); - OpenFeature.setProvider(clientName, provider1); + OpenFeature.setProvider(domain, provider1); // Spy on context changed handlers of all providers const contextChangedSpies = [defaultProvider, provider1].map((provider) => @@ -168,8 +168,8 @@ describe('Evaluation Context', () => { const provider2 = new MockProvider(); OpenFeature.setProvider(defaultProvider); - OpenFeature.setProvider('client1', provider1); - OpenFeature.setProvider('client2', provider2); + OpenFeature.setProvider('domain1', provider1); + OpenFeature.setProvider('domain2', provider2); // Spy on context changed handlers of all providers const contextChangedSpies = [defaultProvider, provider1, provider2].map((provider) => @@ -193,15 +193,15 @@ describe('Evaluation Context', () => { const provider1 = new MockProvider(); const provider2 = new MockProvider(); - const client1 = 'client1'; - const client2 = 'client2'; + const domain1 = 'domain1'; + const domain2 = 'domain2'; OpenFeature.setProvider(defaultProvider); - OpenFeature.setProvider(client1, provider1); - OpenFeature.setProvider(client2, provider2); + OpenFeature.setProvider(domain1, provider1); + OpenFeature.setProvider(domain2, provider2); - // Set context for client1 - await OpenFeature.setContext(client1, { property1: 'test' }); + // Set context for domain1 + await OpenFeature.setContext(domain1, { property1: 'test' }); // Spy on context changed handlers of all providers const contextShouldChangeSpies = [defaultProvider, provider2].map((provider) => @@ -229,8 +229,8 @@ describe('Evaluation Context', () => { const provider2 = new MockProvider(); OpenFeature.setProvider(defaultProvider); - OpenFeature.setProvider('client1', provider1); - OpenFeature.setProvider('client2', provider2); + OpenFeature.setProvider('domain1', provider1); + OpenFeature.setProvider('domain2', provider2); // Spy on context changed handlers of all providers const contextChangedSpies = [defaultProvider, provider1, provider2].map((provider) => diff --git a/packages/client/test/events.spec.ts b/packages/client/test/events.spec.ts index 02a47a577..a358499c6 100644 --- a/packages/client/test/events.spec.ts +++ b/packages/client/test/events.spec.ts @@ -99,13 +99,13 @@ class MockProvider implements Provider { describe('Events', () => { // set timeouts short for this suite. jest.setTimeout(TIMEOUT); - let clientId = uuid(); + let domain = uuid(); afterEach(async () => { await OpenFeature.clearProviders(); OpenFeature.clearHandlers(); jest.clearAllMocks(); - clientId = uuid(); + domain = uuid(); // hacky, but it's helpful to clear the handlers between tests /* eslint-disable @typescript-eslint/no-explicit-any */ (OpenFeature as any)._clientEventHandlers = new Map(); @@ -121,7 +121,7 @@ describe('Events', () => { describe('provider implements events', () => { it('The provider defines a mechanism for signalling the occurrence of an event`PROVIDER_READY`', (done) => { const provider = new MockProvider(); - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); client.addHandler(ProviderEvents.Ready, () => { try { expect(client.metadata.providerMetadata.name).toBe(provider.metadata.name); @@ -131,13 +131,13 @@ describe('Events', () => { done(err); } }); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); }); it('It defines a mechanism for signalling `PROVIDER_ERROR`', (done) => { //make sure an error event is fired when initialize promise reject const provider = new MockProvider({ failOnInit: true }); - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); client.addHandler(ProviderEvents.Error, () => { try { @@ -149,14 +149,14 @@ describe('Events', () => { } }); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); }); }); describe('provider does not implement events', () => { it('The provider defines a mechanism for signalling the occurrence of an event`PROVIDER_READY`', (done) => { const provider = new MockProvider({ enableEvents: false }); - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); client.addHandler(ProviderEvents.Ready, () => { try { @@ -167,12 +167,12 @@ describe('Events', () => { } }); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); }); it('It defines a mechanism for signalling `PROVIDER_ERROR`', (done) => { const provider = new MockProvider({ enableEvents: false, failOnInit: true }); - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); client.addHandler(ProviderEvents.Error, () => { try { @@ -184,7 +184,7 @@ describe('Events', () => { } }); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); }); }); }); @@ -192,7 +192,7 @@ describe('Events', () => { describe('Requirement 5.1.2', () => { it('When a provider signals the occurrence of a particular event, the associated client and API event handlers run', (done) => { const provider = new MockProvider(); - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); Promise.all([ new Promise((resolve) => { @@ -209,7 +209,7 @@ describe('Events', () => { done(); }); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); provider.events?.emit(ProviderEvents.Error); }); }); @@ -217,8 +217,8 @@ describe('Events', () => { describe('Requirement 5.1.3', () => { it('When a provider signals the occurrence of a particular event, event handlers on clients which are not associated with that provider do not run', (done) => { const provider = new MockProvider(); - const client0 = OpenFeature.getClient(clientId); - const client1 = OpenFeature.getClient(clientId + '1'); + const client0 = OpenFeature.getClient(domain); + const client1 = OpenFeature.getClient(domain + '1'); const client1Handler = jest.fn(); const client0Handler = () => { @@ -229,7 +229,7 @@ describe('Events', () => { client0.addHandler(ProviderEvents.Ready, client0Handler); client1.addHandler(ProviderEvents.Ready, client1Handler); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); }); it('anonymous provider with anonymous client should run non-init events', (done) => { @@ -350,20 +350,20 @@ describe('Events', () => { it('PROVIDER_ERROR events populates the message field', (done) => { const provider = new MockProvider({ failOnInit: true }); - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); client.addHandler(ProviderEvents.Error, (details) => { expect(details?.message).toBeDefined(); done(); }); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); }); }); describe('Requirement 5.2.1,', () => { it('The client provides a function for associating handler functions with a particular provider event type', () => { - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); expect(client.addHandler).toBeDefined(); }); }); @@ -378,27 +378,29 @@ describe('Events', () => { it('The event details MUST contain the provider name associated with the event.', (done) => { const providerName = '5.2.3'; const provider = new MockProvider({ name: providerName }); - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); client.addHandler(ProviderEvents.Ready, (details) => { expect(details?.providerName).toEqual(providerName); - expect(details?.clientName).toEqual(clientId); + expect(details?.clientName).toEqual(domain); + expect(details?.domain).toEqual(domain); done(); }); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); }); it('The event details contain the client name associated with the event in the client', (done) => { const provider = new MockProvider(); - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); client.addHandler(ProviderEvents.Ready, (details) => { - expect(details?.clientName).toEqual(clientId); + expect(details?.clientName).toEqual(domain); + expect(details?.domain).toEqual(domain); done(); }); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); }); }); @@ -406,14 +408,14 @@ describe('Events', () => { it('The handler function accepts a event details parameter.', (done) => { const details: StaleEvent = { message: 'message' }; const provider = new MockProvider(); - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); client.addHandler(ProviderEvents.Stale, (givenDetails) => { expect(givenDetails?.message).toEqual(details.message); done(); }); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); provider.events?.emit(ProviderEvents.Stale, details); }); }); @@ -421,7 +423,7 @@ describe('Events', () => { describe('Requirement 5.2.5', () => { it('If a handler function terminates abnormally, other handler functions run', (done) => { const provider = new MockProvider(); - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); const handler0 = jest.fn(() => { throw new Error('Error during initialization'); @@ -435,7 +437,7 @@ describe('Events', () => { client.addHandler(ProviderEvents.Ready, handler0); client.addHandler(ProviderEvents.Ready, handler1); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); }); }); @@ -443,12 +445,12 @@ describe('Events', () => { it('Event handlers MUST persist across `provider` changes.', (done) => { const provider1 = new MockProvider({ name: 'provider-1' }); const provider2 = new MockProvider({ name: 'provider-2' }); - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); let counter = 0; client.addHandler(ProviderEvents.Ready, () => { if (client.metadata.providerMetadata.name === provider1.metadata.name) { - OpenFeature.setProvider(clientId, provider2); + OpenFeature.setProvider(domain, provider2); counter++; } else { expect(counter).toBeGreaterThan(0); @@ -459,7 +461,7 @@ describe('Events', () => { } }); - OpenFeature.setProvider(clientId, provider1); + OpenFeature.setProvider(domain, provider1); }); }); @@ -475,7 +477,7 @@ describe('Events', () => { }); it('The API provides a function allowing the removal of event handlers', () => { - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); const handler = jest.fn(); const eventType = ProviderEvents.Stale; @@ -489,37 +491,37 @@ describe('Events', () => { describe('Requirement 5.3.1', () => { it('If the provider `initialize` function terminates normally, `PROVIDER_READY` handlers MUST run', (done) => { const provider = new MockProvider(); - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); client.addHandler(ProviderEvents.Ready, () => { done(); }); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); }); }); describe('Requirement 5.3.2', () => { it('If the provider `initialize` function terminates abnormally, `PROVIDER_ERROR` handlers MUST run.', (done) => { const provider = new MockProvider({ failOnInit: true }); - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); client.addHandler(ProviderEvents.Error, () => { done(); }); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); }); it('It defines a mechanism for signalling `PROVIDER_CONFIGURATION_CHANGED`', (done) => { const provider = new MockProvider(); - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); client.addHandler(ProviderEvents.ConfigurationChanged, () => { done(); }); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); // emit a change event from the mock provider provider.events?.emit(ProviderEvents.ConfigurationChanged); }); @@ -531,7 +533,7 @@ describe('Events', () => { it('Ready', (done) => { const provider = new MockProvider({ initialStatus: ProviderStatus.READY }); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); expect(provider.initialize).not.toHaveBeenCalled(); OpenFeature.addHandler(ProviderEvents.Ready, () => { @@ -542,7 +544,7 @@ describe('Events', () => { it('Error', (done) => { const provider = new MockProvider({ initialStatus: ProviderStatus.ERROR }); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); expect(provider.initialize).not.toHaveBeenCalled(); OpenFeature.addHandler(ProviderEvents.Error, () => { @@ -553,7 +555,7 @@ describe('Events', () => { it('Stale', (done) => { const provider = new MockProvider({ initialStatus: ProviderStatus.STALE }); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); expect(provider.initialize).not.toHaveBeenCalled(); OpenFeature.addHandler(ProviderEvents.Stale, () => { @@ -567,9 +569,9 @@ describe('Events', () => { describe('Handlers attached after the provider is already in the associated state, MUST run immediately.', () => { it('Ready', (done) => { const provider = new MockProvider({ initialStatus: ProviderStatus.READY }); - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); expect(provider.initialize).not.toHaveBeenCalled(); client.addHandler(ProviderEvents.Ready, () => { @@ -579,9 +581,9 @@ describe('Events', () => { it('Error', (done) => { const provider = new MockProvider({ initialStatus: ProviderStatus.ERROR }); - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); expect(provider.initialize).not.toHaveBeenCalled(); client.addHandler(ProviderEvents.Error, () => { @@ -591,9 +593,9 @@ describe('Events', () => { it('Stale', (done) => { const provider = new MockProvider({ initialStatus: ProviderStatus.STALE }); - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); expect(provider.initialize).not.toHaveBeenCalled(); client.addHandler(ProviderEvents.Stale, () => { @@ -610,12 +612,13 @@ describe('Events', () => { it("If the provider's `on context changed` function terminates normally, associated `PROVIDER_CONTEXT_CHANGED` handlers MUST run.", (done) => { const provider = new MockProvider({ initialStatus: ProviderStatus.READY }); - OpenFeature.setProvider(clientId, provider); - OpenFeature.setContext(clientId, {}); + OpenFeature.setProvider(domain, provider); + OpenFeature.setContext(domain, {}); const handler = (details?: EventDetails) => { try { - expect(details?.clientName).toEqual(clientId); + expect(details?.clientName).toEqual(domain); + expect(details?.domain).toEqual(domain); expect(details?.providerName).toEqual(provider.metadata.name); done(); } catch (e) { @@ -629,12 +632,13 @@ describe('Events', () => { it("If the provider's `on context changed` function terminates abnormally, associated `PROVIDER_ERROR` handlers MUST run.", (done) => { const provider = new MockProvider({ initialStatus: ProviderStatus.READY, failOnContextChange: true }); - OpenFeature.setProvider(clientId, provider); - OpenFeature.setContext(clientId, {}); + OpenFeature.setProvider(domain, provider); + OpenFeature.setContext(domain, {}); const handler = (details?: EventDetails) => { try { - expect(details?.clientName).toEqual(clientId); + expect(details?.clientName).toEqual(domain); + expect(details?.domain).toEqual(domain); expect(details?.providerName).toEqual(provider.metadata.name); done(); } catch (e) { @@ -651,18 +655,19 @@ describe('Events', () => { const provider = new MockProvider({ initialStatus: ProviderStatus.READY }); let runCount = 0; - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); // expect 2 runs, since 2 providers are impacted by this context change (global) const handler = (details?: EventDetails) => { try { runCount++; // one run should be global - if (details?.clientName === undefined) { + if (details?.domain === undefined) { expect(details?.providerName).toEqual(OpenFeature.getProviderMetadata().name); - } else if (details?.clientName === clientId) { + } else if (details?.domain === domain) { // one run should be for client - expect(details?.clientName).toEqual(clientId); + expect(details?.clientName).toEqual(domain); + expect(details?.domain).toEqual(domain); expect(details?.providerName).toEqual(provider.metadata.name); } if (runCount == 2) { @@ -675,18 +680,18 @@ describe('Events', () => { OpenFeature.addHandler(ProviderEvents.ContextChanged, handler); OpenFeature.setContext({}); - }); it("If the provider's `on context changed` function terminates abnormally, associated `PROVIDER_ERROR` handlers MUST run.", (done) => { const provider = new MockProvider({ initialStatus: ProviderStatus.READY, failOnContextChange: true }); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); const handler = (details?: EventDetails) => { try { // expect only one error run, because only one provider throws - expect(details?.clientName).toEqual(clientId); + expect(details?.clientName).toEqual(domain); + expect(details?.domain).toEqual(domain); expect(details?.providerName).toEqual(provider.metadata.name); expect(details?.message).toBeTruthy(); done(); @@ -704,13 +709,14 @@ describe('Events', () => { describe('client', () => { it("If the provider's `on context changed` function terminates normally, associated `PROVIDER_CONTEXT_CHANGED` handlers MUST run.", (done) => { const provider = new MockProvider({ initialStatus: ProviderStatus.READY }); - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); const handler = (details?: EventDetails) => { try { - expect(details?.clientName).toEqual(clientId); + expect(details?.clientName).toEqual(domain); + expect(details?.domain).toEqual(domain); expect(details?.providerName).toEqual(provider.metadata.name); done(); } catch (e) { @@ -719,18 +725,19 @@ describe('Events', () => { }; client.addHandler(ProviderEvents.ContextChanged, handler); - OpenFeature.setContext(clientId, {}); + OpenFeature.setContext(domain, {}); }); it("If the provider's `on context changed` function terminates abnormally, associated `PROVIDER_ERROR` handlers MUST run.", (done) => { const provider = new MockProvider({ initialStatus: ProviderStatus.READY, failOnContextChange: true }); - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); const handler = (details?: EventDetails) => { try { - expect(details?.clientName).toEqual(clientId); + expect(details?.clientName).toEqual(domain); + expect(details?.domain).toEqual(domain); expect(details?.providerName).toEqual(provider.metadata.name); expect(details?.message).toBeTruthy(); done(); @@ -740,8 +747,7 @@ describe('Events', () => { }; client.addHandler(ProviderEvents.Error, handler); - OpenFeature.setContext(clientId, {}); - + OpenFeature.setContext(domain, {}); }); }); @@ -750,20 +756,21 @@ describe('Events', () => { it('runs API ContextChanged event handler', (done) => { const noChangeHandlerProvider = 'noChangeHandlerProvider'; const provider = new MockProvider({ initialStatus: ProviderStatus.READY, noContextChanged: true }); - + OpenFeature.setProvider(noChangeHandlerProvider, provider); OpenFeature.setContext(noChangeHandlerProvider, {}); - + const handler = (details?: EventDetails) => { try { expect(details?.clientName).toEqual(noChangeHandlerProvider); + expect(details?.domain).toEqual(noChangeHandlerProvider); expect(details?.providerName).toEqual(provider.metadata.name); done(); } catch (e) { done(e); } }; - + OpenFeature.addHandler(ProviderEvents.ContextChanged, handler); }); }); diff --git a/packages/client/test/in-memory-provider.spec.ts b/packages/client/test/in-memory-provider.spec.ts index c14816811..b17922f34 100644 --- a/packages/client/test/in-memory-provider.spec.ts +++ b/packages/client/test/in-memory-provider.spec.ts @@ -2,7 +2,7 @@ import { FlagNotFoundError, GeneralError, InMemoryProvider, ProviderEvents, Prov import { FlagConfiguration } from '../src/provider/in-memory-provider/flag-configuration'; import { VariantNotFoundError } from '../src/provider/in-memory-provider/variant-not-found-error'; -describe(InMemoryProvider, () => { +describe('in-memory provider', () => { describe('initialize', () => { it('Should have provider status as NOT_READY after instantiation and emit READY and have READY state after initialuzation', async () => { const booleanFlagSpec = { @@ -20,7 +20,6 @@ describe(InMemoryProvider, () => { await provider.initialize(); expect(provider.status).toBe(ProviderStatus.READY); - }); it('Should have provider status as ERROR after instantiation, emit ERROR and have ERROR state if initialization throws', async () => { @@ -32,7 +31,9 @@ describe(InMemoryProvider, () => { }, disabled: false, defaultVariant: 'on', - contextEvaluator: () => { throw new GeneralError('context eval error'); }, + contextEvaluator: () => { + throw new GeneralError('context eval error'); + }, }, }; const provider = new InMemoryProvider(throwingFlagSpec); diff --git a/packages/client/test/open-feature.spec.ts b/packages/client/test/open-feature.spec.ts index e19d3e071..e061acea4 100644 --- a/packages/client/test/open-feature.spec.ts +++ b/packages/client/test/open-feature.spec.ts @@ -77,76 +77,76 @@ describe('OpenFeature', () => { }); describe('Requirement 1.1.3', () => { - it('should set the default provider if no name is provided', () => { + it('should set the default provider if no domain is provided', () => { const provider = mockProvider(); OpenFeature.setProvider(provider); const client = OpenFeature.getClient(); expect(client.metadata.providerMetadata.name).toEqual(provider.metadata.name); }); - it('should not change named providers when setting a new default provider', () => { - const name = 'my-client'; + it('should not change providers associated with a domain when setting a new default provider', () => { + const domain = 'my-domain'; const fakeProvider = { metadata: { name: 'test' } } as unknown as Provider; const provider = mockProvider(); OpenFeature.setProvider(provider); - OpenFeature.setProvider(name, fakeProvider); + OpenFeature.setProvider(domain, fakeProvider); - const unnamedClient = OpenFeature.getClient(); - const namedClient = OpenFeature.getClient(name); + const defaultClient = OpenFeature.getClient(); + const domainSpecificClient = OpenFeature.getClient(domain); - expect(unnamedClient.metadata.providerMetadata.name).toEqual(provider.metadata.name); - expect(namedClient.metadata.providerMetadata.name).toEqual(fakeProvider.metadata.name); + expect(defaultClient.metadata.providerMetadata.name).toEqual(provider.metadata.name); + expect(domainSpecificClient.metadata.providerMetadata.name).toEqual(fakeProvider.metadata.name); }); - it('should assign a new provider to existing clients', () => { - const name = 'my-client'; + it('should bind a new provider to existing clients in a matching domain', () => { + const domain = 'my-domain'; const fakeProvider = { metadata: { name: 'test' } } as unknown as Provider; const provider = mockProvider(); OpenFeature.setProvider(provider); - const namedClient = OpenFeature.getClient(name); - OpenFeature.setProvider(name, fakeProvider); + const domainSpecificClient = OpenFeature.getClient(domain); + OpenFeature.setProvider(domain, fakeProvider); - expect(namedClient.metadata.providerMetadata.name).toEqual(fakeProvider.metadata.name); + expect(domainSpecificClient.metadata.providerMetadata.name).toEqual(fakeProvider.metadata.name); }); it('should close a provider if it is replaced and no other client uses it', async () => { const provider1 = { ...mockProvider(), onClose: jest.fn() }; const provider2 = { ...mockProvider(), onClose: jest.fn() }; - OpenFeature.setProvider('client1', provider1); + OpenFeature.setProvider('domain1', provider1); expect(provider1.onClose).not.toHaveBeenCalled(); - OpenFeature.setProvider('client1', provider2); + OpenFeature.setProvider('domain1', provider2); expect(provider1.onClose).toHaveBeenCalledTimes(1); }); it('should not close provider if it is used by another client', async () => { const provider1 = { ...mockProvider(), onClose: jest.fn() }; - OpenFeature.setProvider('client1', provider1); - OpenFeature.setProvider('client2', provider1); + OpenFeature.setProvider('domain1', provider1); + OpenFeature.setProvider('domain2', provider1); - OpenFeature.setProvider('client1', { ...provider1 }); + OpenFeature.setProvider('domain1', { ...provider1 }); expect(provider1.onClose).not.toHaveBeenCalled(); - OpenFeature.setProvider('client2', { ...provider1 }); + OpenFeature.setProvider('domain2', { ...provider1 }); expect(provider1.onClose).toHaveBeenCalledTimes(1); }); - it('should return the default provider metadata when passing an unregistered client name', async () => { + it('should return the default provider metadata when passing an unregistered domain', async () => { const mockProvider = { metadata: { name: 'test' } } as unknown as Provider; OpenFeature.setProvider(mockProvider); const metadata = OpenFeature.getProviderMetadata('unused'); expect(metadata.name === mockProvider.metadata.name).toBeTruthy(); }); - it('should return the named provider metadata when passing a registered client name', async () => { + it('should return domain specific provider metadata when passing a registered domain', async () => { const mockProvider = { metadata: { name: 'mock' } } as unknown as Provider; - const mockNamedProvider = { metadata: { name: 'named-mock' } } as unknown as Provider; + const mockDomainProvider = { metadata: { name: 'named-mock' } } as unknown as Provider; OpenFeature.setProvider(mockProvider); - OpenFeature.setProvider('mocked', mockNamedProvider); + OpenFeature.setProvider('mocked', mockDomainProvider); const metadata = OpenFeature.getProviderMetadata('mocked'); - expect(metadata.name === mockNamedProvider.metadata.name).toBeTruthy(); + expect(metadata.name === mockDomainProvider.metadata.name).toBeTruthy(); }); }); @@ -167,27 +167,29 @@ describe('OpenFeature', () => { expect(OpenFeature.getClient).toBeDefined(); expect(OpenFeature.getClient()).toBeInstanceOf(OpenFeatureClient); - const name = 'my-client'; - const namedClient = OpenFeature.getClient(name); + const domain = 'my-domain'; + const domainSpecificClient = OpenFeature.getClient(domain); // check that using a named configuration also works as expected. - expect(namedClient).toBeInstanceOf(OpenFeatureClient); - expect(namedClient.metadata.name).toEqual(name); + expect(domainSpecificClient).toBeInstanceOf(OpenFeatureClient); + // Alias for domain, left for backwards compatibility + expect(domainSpecificClient.metadata.name).toEqual(domain); + expect(domainSpecificClient.metadata.domain).toEqual(domain); }); - it('should return a client with the default provider if no provider has been bound to the name', () => { - const namedClient = OpenFeature.getClient('unbound'); - expect(namedClient.metadata.providerMetadata.name).toEqual(OpenFeature.providerMetadata.name); + it('should return a client with the default provider if no provider has been bound to the domain', () => { + const domainSpecificClient = OpenFeature.getClient('unbound'); + expect(domainSpecificClient.metadata.providerMetadata.name).toEqual(OpenFeature.providerMetadata.name); }); - it('should return a client with the provider bound to the name', () => { - const name = 'my-named-client'; + it('should return a client with the provider bound to the domain', () => { + const domain = 'my-domain'; const fakeProvider = { metadata: { name: 'test' } } as unknown as Provider; - OpenFeature.setProvider(name, fakeProvider); + OpenFeature.setProvider(domain, fakeProvider); - const namedClient = OpenFeature.getClient(name); + const domainSpecificClient = OpenFeature.getClient(domain); - expect(namedClient.metadata.providerMetadata.name).toEqual(fakeProvider.metadata.name); + expect(domainSpecificClient.metadata.providerMetadata.name).toEqual(fakeProvider.metadata.name); }); it('should be chainable', () => { @@ -208,8 +210,8 @@ describe('OpenFeature', () => { it('runs the shutdown function on all providers for all clients', async () => { const provider = mockProvider(); OpenFeature.setProvider(provider); - OpenFeature.setProvider('client1', { ...provider }); - OpenFeature.setProvider('client2', { ...provider }); + OpenFeature.setProvider('domain1', { ...provider }); + OpenFeature.setProvider('domain2', { ...provider }); expect(OpenFeature.providerMetadata.name).toBe(provider.metadata.name); await OpenFeature.close(); @@ -226,12 +228,37 @@ describe('OpenFeature', () => { const provider3 = mockProvider(); OpenFeature.setProvider(provider1); - OpenFeature.setProvider('client1', provider2); - OpenFeature.setProvider('client2', provider3); + OpenFeature.setProvider('domain1', provider2); + OpenFeature.setProvider('domain2', provider3); expect(OpenFeature.providerMetadata.name).toBe(provider1.metadata.name); await OpenFeature.close(); expect(provider3.onClose).toHaveBeenCalled(); }); + + describe('context during initialization', () => { + it('should use the context set in the domain', async () => { + const domain = 'test'; + await OpenFeature.setContext(domain, { user: 'mike' }); + + const provider = mockProvider(); + const spy = jest.spyOn(provider, 'initialize'); + OpenFeature.setProvider(domain, provider); + + expect(spy).toHaveBeenCalledWith({ user: 'mike' }); + }); + + it('should use the default context', async () => { + const domain = 'test'; + await OpenFeature.setContext(domain, { user: 'mike' }); + await OpenFeature.setContext({ name: 'todd' }); + + const provider = mockProvider(); + const spy = jest.spyOn(provider, 'initialize'); + OpenFeature.setProvider(provider); + + expect(spy).toHaveBeenCalledWith({ name: 'todd' }); + }); + }); }); }); diff --git a/packages/server/README.md b/packages/server/README.md index a7ee06da9..1a8906321 100644 --- a/packages/server/README.md +++ b/packages/server/README.md @@ -86,16 +86,16 @@ See [here](https://open-feature.github.io/js-sdk/modules/_openfeature_server_sdk ## 🌟 Features -| Status | Features | Description | -| ------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -| ✅ | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. | -| ✅ | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). | -| ✅ | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. | -| ✅ | [Logging](#logging) | Integrate with popular logging packages. | -| ✅ | [Named clients](#named-clients) | Utilize multiple providers in a single application. | -| ✅ | [Eventing](#eventing) | React to state changes in the provider or flag management system. | -| ✅ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | -| ✅ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | +| Status | Features | Description | +| ------ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| ✅ | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. | +| ✅ | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). | +| ✅ | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. | +| ✅ | [Logging](#logging) | Integrate with popular logging packages. | +| ✅ | [Domains](#domains) | Logically bind clients with providers. | +| ✅ | [Eventing](#eventing) | React to state changes in the provider or flag management system. | +| ✅ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | +| ✅ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌ @@ -126,7 +126,7 @@ OpenFeature.setProvider(new MyProvider()); Once the provider has been registered, the status can be tracked using [events](#eventing). In some situations, it may be beneficial to register multiple providers in the same application. -This is possible using [named clients](#named-clients), which is covered in more detail below. +This is possible using [domains](#domains), which is covered in more details below. ### Targeting @@ -194,11 +194,11 @@ const client = OpenFeature.getClient(); client.setLogger(logger); ``` -### Named clients +### Domains -Clients can be given a name. -A name is a logical identifier which can be used to associate clients with a particular provider. -If a name has no associated provider, the global provider is used. +Clients can be assigned to a domain. +A domain is a logical identifier which can be used to associate clients with a particular provider. +If a domain has no associated provider, the default provider is used. ```ts import { OpenFeature, InMemoryProvider } from "@openfeature/server-sdk"; @@ -216,16 +216,16 @@ const myFlags = { // Registering the default provider OpenFeature.setProvider(InMemoryProvider(myFlags)); -// Registering a named provider -OpenFeature.setProvider("otherClient", new InMemoryProvider(someOtherFlags)); +// Registering a provider to a domain +OpenFeature.setProvider("my-domain", new InMemoryProvider(someOtherFlags)); -// A Client backed by default provider +// A Client bound to the default provider const clientWithDefault = OpenFeature.getClient(); -// A Client backed by NewCachedProvider -const clientForCache = OpenFeature.getClient("otherClient"); +// A Client bound to the InMemoryProvider provider +const domainScopedClient = OpenFeature.getClient("my-domain"); ``` -Named providers can be set in an awaitable or synchronous way. +Domains can be defined on a provider during registration. For more details, please refer to the [providers](#providers) section. ### Eventing diff --git a/packages/server/src/client/open-feature-client.ts b/packages/server/src/client/open-feature-client.ts index dc5ad4bf2..1e6e54334 100644 --- a/packages/server/src/client/open-feature-client.ts +++ b/packages/server/src/client/open-feature-client.ts @@ -25,7 +25,11 @@ import { Provider } from '../provider'; import { Client } from './client'; type OpenFeatureClientOptions = { + /** + * @deprecated Use `domain` instead. + */ name?: string; + domain?: string; version?: string; }; @@ -48,7 +52,9 @@ export class OpenFeatureClient implements Client, ManageContext(nameOrContext) ?? + objectOrUndefined(domainOrContext) ?? objectOrUndefined(versionOrContext) ?? objectOrUndefined(contextOrUndefined); return new OpenFeatureClient( - () => this.getProviderForClient(name), - () => this.buildAndCacheEventEmitterForClient(name), + () => this.getProviderForClient(domain), + () => this.buildAndCacheEventEmitterForClient(domain), () => this._logger, - { name, version }, + { domain, version }, context, ); } diff --git a/packages/server/test/client.spec.ts b/packages/server/test/client.spec.ts index 19c0829b7..218d2aeb2 100644 --- a/packages/server/test/client.spec.ts +++ b/packages/server/test/client.spec.ts @@ -137,7 +137,7 @@ describe('OpenFeatureClient', () => { expect(provider.status).toBe(ProviderStatus.NOT_READY); await OpenFeature.setProviderAndWait(provider); expect(provider.status).toBe(ProviderStatus.READY); - expect(spy).toBeCalled(); + expect(spy).toHaveBeenCalled(); }); it('should wait for the provider to fail during initialization', async () => { @@ -147,7 +147,7 @@ describe('OpenFeatureClient', () => { expect(provider.status).toBe(ProviderStatus.NOT_READY); await expect(OpenFeature.setProviderAndWait(provider)).rejects.toThrow(); expect(provider.status).toBe(ProviderStatus.ERROR); - expect(spy).toBeCalled(); + expect(spy).toHaveBeenCalled(); }); }); @@ -158,10 +158,15 @@ describe('OpenFeatureClient', () => { }); describe('Requirement 1.2.1', () => { - const NAME = 'my-client'; - const client = OpenFeature.getClient(NAME); - it('should have metadata accessor with name', () => { - expect(client.metadata.name).toEqual(NAME); + const domain = 'my-domain'; + const client = OpenFeature.getClient(domain); + + it('should have metadata accessor with name for backwards compatibility', () => { + expect(client.metadata.name).toEqual(domain); + }); + + it('should have metadata accessor with domain', () => { + expect(client.metadata.domain).toEqual(domain); }); }); diff --git a/packages/server/test/events.spec.ts b/packages/server/test/events.spec.ts index 1e596c0d3..7f8033b76 100644 --- a/packages/server/test/events.spec.ts +++ b/packages/server/test/events.spec.ts @@ -80,12 +80,12 @@ class MockProvider implements Provider { describe('Events', () => { // set timeouts short for this suite. jest.setTimeout(TIMEOUT); - let clientId = uuid(); + let domain = uuid(); afterEach(async () => { await OpenFeature.clearProviders(); jest.clearAllMocks(); - clientId = uuid(); + domain = uuid(); // hacky, but it's helpful to clear the handlers between tests /* eslint-disable @typescript-eslint/no-explicit-any */ (OpenFeature as any)._clientEventHandlers = new Map(); @@ -101,7 +101,7 @@ describe('Events', () => { describe('provider implements events', () => { it('The provider defines a mechanism for signalling the occurrence of an event`PROVIDER_READY`', (done) => { const provider = new MockProvider(); - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); client.addHandler(ProviderEvents.Ready, () => { try { expect(client.metadata.providerMetadata.name).toBe(provider.metadata.name); @@ -111,13 +111,13 @@ describe('Events', () => { done(err); } }); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); }); it('It defines a mechanism for signalling `PROVIDER_ERROR`', (done) => { //make sure an error event is fired when initialize promise reject const provider = new MockProvider({ failOnInit: true }); - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); client.addHandler(ProviderEvents.Error, () => { try { @@ -129,14 +129,14 @@ describe('Events', () => { } }); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); }); }); describe('provider does not implement events', () => { it('The provider defines a mechanism for signalling the occurrence of an event`PROVIDER_READY`', (done) => { const provider = new MockProvider({ enableEvents: false }); - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); client.addHandler(ProviderEvents.Ready, () => { try { @@ -147,12 +147,12 @@ describe('Events', () => { } }); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); }); it('It defines a mechanism for signalling `PROVIDER_ERROR`', (done) => { const provider = new MockProvider({ enableEvents: false, failOnInit: true }); - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); client.addHandler(ProviderEvents.Error, () => { try { @@ -164,7 +164,7 @@ describe('Events', () => { } }); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); }); }); }); @@ -172,7 +172,7 @@ describe('Events', () => { describe('Requirement 5.1.2', () => { it('When a provider signals the occurrence of a particular event, the associated client and API event handlers run', (done) => { const provider = new MockProvider(); - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); Promise.all([ new Promise((resolve) => { @@ -189,7 +189,7 @@ describe('Events', () => { done(); }); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); provider.events?.emit(ProviderEvents.Error); }); }); @@ -197,8 +197,8 @@ describe('Events', () => { describe('Requirement 5.1.3', () => { it('When a provider signals the occurrence of a particular event, event handlers on clients which are not associated with that provider do not run', (done) => { const provider = new MockProvider(); - const client0 = OpenFeature.getClient(clientId); - const client1 = OpenFeature.getClient(clientId + '1'); + const client0 = OpenFeature.getClient(domain); + const client1 = OpenFeature.getClient(domain + '1'); const client1Handler = jest.fn(); const client0Handler = () => { @@ -209,7 +209,7 @@ describe('Events', () => { client0.addHandler(ProviderEvents.Ready, client0Handler); client1.addHandler(ProviderEvents.Ready, client1Handler); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); }); it('anonymous provider with anonymous client should run non-init events', (done) => { @@ -330,20 +330,20 @@ describe('Events', () => { it('PROVIDER_ERROR events populates the message field', (done) => { const provider = new MockProvider({ failOnInit: true }); - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); client.addHandler(ProviderEvents.Error, (details) => { expect(details?.message).toBeDefined(); done(); }); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); }); }); describe('Requirement 5.2.1,', () => { it('The client provides a function for associating handler functions with a particular provider event type', () => { - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); expect(client.addHandler).toBeDefined(); }); }); @@ -358,27 +358,29 @@ describe('Events', () => { it('The event details MUST contain the provider name associated with the event.', (done) => { const providerName = '5.2.3'; const provider = new MockProvider({ name: providerName }); - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); client.addHandler(ProviderEvents.Ready, (details) => { expect(details?.providerName).toEqual(providerName); - expect(details?.clientName).toEqual(clientId); + expect(details?.clientName).toEqual(domain); + expect(details?.domain).toEqual(domain); done(); }); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); }); it('The event details contain the client name associated with the event in the client', (done) => { const provider = new MockProvider(); - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); client.addHandler(ProviderEvents.Ready, (details) => { - expect(details?.clientName).toEqual(clientId); + expect(details?.clientName).toEqual(domain); + expect(details?.domain).toEqual(domain); done(); }); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); }); }); @@ -386,14 +388,14 @@ describe('Events', () => { it('The handler function accepts a event details parameter.', (done) => { const details: StaleEvent = { message: 'message' }; const provider = new MockProvider(); - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); client.addHandler(ProviderEvents.Stale, (givenDetails) => { expect(givenDetails?.message).toEqual(details.message); done(); }); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); provider.events?.emit(ProviderEvents.Stale, details); }); }); @@ -401,7 +403,7 @@ describe('Events', () => { describe('Requirement 5.2.5', () => { it('If a handler function terminates abnormally, other handler functions run', (done) => { const provider = new MockProvider(); - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); const handler0 = jest.fn(() => { throw new Error('Error during initialization'); @@ -415,7 +417,7 @@ describe('Events', () => { client.addHandler(ProviderEvents.Ready, handler0); client.addHandler(ProviderEvents.Ready, handler1); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); }); }); @@ -423,12 +425,12 @@ describe('Events', () => { it('Event handlers MUST persist across `provider` changes.', (done) => { const provider1 = new MockProvider({ name: 'provider-1' }); const provider2 = new MockProvider({ name: 'provider-2' }); - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); let counter = 0; client.addHandler(ProviderEvents.Ready, () => { if (client.metadata.providerMetadata.name === provider1.metadata.name) { - OpenFeature.setProvider(clientId, provider2); + OpenFeature.setProvider(domain, provider2); counter++; } else { expect(counter).toBeGreaterThan(0); @@ -439,7 +441,7 @@ describe('Events', () => { } }); - OpenFeature.setProvider(clientId, provider1); + OpenFeature.setProvider(domain, provider1); }); }); @@ -455,7 +457,7 @@ describe('Events', () => { }); it('The API provides a function allowing the removal of event handlers', () => { - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); const handler = jest.fn(); const eventType = ProviderEvents.Stale; @@ -469,37 +471,37 @@ describe('Events', () => { describe('Requirement 5.3.1', () => { it('If the provider `initialize` function terminates normally, `PROVIDER_READY` handlers MUST run', (done) => { const provider = new MockProvider(); - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); client.addHandler(ProviderEvents.Ready, () => { done(); }); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); }); }); describe('Requirement 5.3.2', () => { it('If the provider `initialize` function terminates abnormally, `PROVIDER_ERROR` handlers MUST run.', (done) => { const provider = new MockProvider({ failOnInit: true }); - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); client.addHandler(ProviderEvents.Error, () => { done(); }); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); }); it('It defines a mechanism for signalling `PROVIDER_CONFIGURATION_CHANGED`', (done) => { const provider = new MockProvider(); - const client = OpenFeature.getClient(clientId); + const client = OpenFeature.getClient(domain); client.addHandler(ProviderEvents.ConfigurationChanged, () => { done(); }); - OpenFeature.setProvider(clientId, provider); + OpenFeature.setProvider(domain, provider); // emit a change event from the mock provider provider.events?.emit(ProviderEvents.ConfigurationChanged); }); @@ -510,10 +512,10 @@ describe('Events', () => { describe('Handlers attached after the provider is already in the associated state, MUST run immediately.', () => { it('Ready', (done) => { const provider = new MockProvider({ initialStatus: ProviderStatus.READY }); - - OpenFeature.setProvider(clientId, provider); + + OpenFeature.setProvider(domain, provider); expect(provider.initialize).not.toHaveBeenCalled(); - + OpenFeature.addHandler(ProviderEvents.Ready, () => { done(); }); @@ -521,10 +523,10 @@ describe('Events', () => { it('Error', (done) => { const provider = new MockProvider({ initialStatus: ProviderStatus.ERROR }); - - OpenFeature.setProvider(clientId, provider); + + OpenFeature.setProvider(domain, provider); expect(provider.initialize).not.toHaveBeenCalled(); - + OpenFeature.addHandler(ProviderEvents.Error, () => { done(); }); @@ -532,10 +534,10 @@ describe('Events', () => { it('Stale', (done) => { const provider = new MockProvider({ initialStatus: ProviderStatus.STALE }); - - OpenFeature.setProvider(clientId, provider); + + OpenFeature.setProvider(domain, provider); expect(provider.initialize).not.toHaveBeenCalled(); - + OpenFeature.addHandler(ProviderEvents.Stale, () => { done(); }); @@ -547,11 +549,11 @@ describe('Events', () => { describe('Handlers attached after the provider is already in the associated state, MUST run immediately.', () => { it('Ready', (done) => { const provider = new MockProvider({ initialStatus: ProviderStatus.READY }); - const client = OpenFeature.getClient(clientId); - - OpenFeature.setProvider(clientId, provider); + const client = OpenFeature.getClient(domain); + + OpenFeature.setProvider(domain, provider); expect(provider.initialize).not.toHaveBeenCalled(); - + client.addHandler(ProviderEvents.Ready, () => { done(); }); @@ -559,11 +561,11 @@ describe('Events', () => { it('Error', (done) => { const provider = new MockProvider({ initialStatus: ProviderStatus.ERROR }); - const client = OpenFeature.getClient(clientId); - - OpenFeature.setProvider(clientId, provider); + const client = OpenFeature.getClient(domain); + + OpenFeature.setProvider(domain, provider); expect(provider.initialize).not.toHaveBeenCalled(); - + client.addHandler(ProviderEvents.Error, () => { done(); }); @@ -571,11 +573,11 @@ describe('Events', () => { it('Stale', (done) => { const provider = new MockProvider({ initialStatus: ProviderStatus.STALE }); - const client = OpenFeature.getClient(clientId); - - OpenFeature.setProvider(clientId, provider); + const client = OpenFeature.getClient(domain); + + OpenFeature.setProvider(domain, provider); expect(provider.initialize).not.toHaveBeenCalled(); - + client.addHandler(ProviderEvents.Stale, () => { done(); }); diff --git a/packages/server/test/in-memory-provider.spec.ts b/packages/server/test/in-memory-provider.spec.ts index 8266e08e3..e61c4bad8 100644 --- a/packages/server/test/in-memory-provider.spec.ts +++ b/packages/server/test/in-memory-provider.spec.ts @@ -1,7 +1,7 @@ import { FlagNotFoundError, InMemoryProvider, ProviderEvents, StandardResolutionReasons, TypeMismatchError } from '../src'; import { VariantFoundError } from '../src/provider/in-memory-provider/variant-not-found-error'; -describe(InMemoryProvider, () => { +describe('in-memory provider', () => { describe('boolean flags', () => { const provider = new InMemoryProvider({}); it('resolves to default variant with reason static', async () => { @@ -146,7 +146,9 @@ describe(InMemoryProvider, () => { provider.putConfiguration(StringFlagSpec); - await expect(provider.resolveStringEvaluation('another-string-flag', itsDefault)).rejects.toThrow(FlagNotFoundError); + await expect(provider.resolveStringEvaluation('another-string-flag', itsDefault)).rejects.toThrow( + FlagNotFoundError, + ); }); it('resolves to default value with reason disabled if flag is disabled', async () => { @@ -257,7 +259,9 @@ describe(InMemoryProvider, () => { }; provider.putConfiguration(numberFlagSpec); - await expect(provider.resolveNumberEvaluation('another-number-flag', defaultNumber)).rejects.toThrow(FlagNotFoundError); + await expect(provider.resolveNumberEvaluation('another-number-flag', defaultNumber)).rejects.toThrow( + FlagNotFoundError, + ); }); it('resolves to default value with reason disabled if flag is disabled', async () => { @@ -372,7 +376,9 @@ describe(InMemoryProvider, () => { }; provider.putConfiguration(ObjectFlagSpec); - await expect(provider.resolveObjectEvaluation('another-number-flag', defaultObject)).rejects.toThrow(FlagNotFoundError); + await expect(provider.resolveObjectEvaluation('another-number-flag', defaultObject)).rejects.toThrow( + FlagNotFoundError, + ); }); it('resolves to default value with reason disabled if flag is disabled', async () => { diff --git a/packages/server/test/open-feature.spec.ts b/packages/server/test/open-feature.spec.ts index 624142114..a1a0bb66f 100644 --- a/packages/server/test/open-feature.spec.ts +++ b/packages/server/test/open-feature.spec.ts @@ -77,76 +77,76 @@ describe('OpenFeature', () => { }); describe('Requirement 1.1.3', () => { - it('should set the default provider if no name is provided', () => { + it('should set the default provider if no domain is provided', () => { const provider = mockProvider(); OpenFeature.setProvider(provider); const client = OpenFeature.getClient(); expect(client.metadata.providerMetadata.name).toEqual(provider.metadata.name); }); - it('should not change named providers when setting a new default provider', () => { - const name = 'my-client'; + it('should not change providers associated with a domain when setting a new default provider', () => { + const domain = 'my-domain'; const fakeProvider = { metadata: { name: 'test' } } as unknown as Provider; const provider = mockProvider(); OpenFeature.setProvider(provider); - OpenFeature.setProvider(name, fakeProvider); + OpenFeature.setProvider(domain, fakeProvider); - const unnamedClient = OpenFeature.getClient(); - const namedClient = OpenFeature.getClient(name); + const defaultClient = OpenFeature.getClient(); + const domainSpecificClient = OpenFeature.getClient(domain); - expect(unnamedClient.metadata.providerMetadata.name).toEqual(provider.metadata.name); - expect(namedClient.metadata.providerMetadata.name).toEqual(fakeProvider.metadata.name); + expect(defaultClient.metadata.providerMetadata.name).toEqual(provider.metadata.name); + expect(domainSpecificClient.metadata.providerMetadata.name).toEqual(fakeProvider.metadata.name); }); - it('should assign a new provider to existing clients', () => { - const name = 'my-client'; + it('should bind a new provider to existing clients in a matching domain', () => { + const domain = 'my-domain'; const fakeProvider = { metadata: { name: 'test' } } as unknown as Provider; const provider = mockProvider(); OpenFeature.setProvider(provider); - const namedClient = OpenFeature.getClient(name); - OpenFeature.setProvider(name, fakeProvider); + const domainSpecificClient = OpenFeature.getClient(domain); + OpenFeature.setProvider(domain, fakeProvider); - expect(namedClient.metadata.providerMetadata.name).toEqual(fakeProvider.metadata.name); + expect(domainSpecificClient.metadata.providerMetadata.name).toEqual(fakeProvider.metadata.name); }); it('should close a provider if it is replaced and no other client uses it', async () => { const provider1 = { ...mockProvider(), onClose: jest.fn() }; const provider2 = { ...mockProvider(), onClose: jest.fn() }; - OpenFeature.setProvider('client1', provider1); + OpenFeature.setProvider('domain1', provider1); expect(provider1.onClose).not.toHaveBeenCalled(); - OpenFeature.setProvider('client1', provider2); + OpenFeature.setProvider('domain1', provider2); expect(provider1.onClose).toHaveBeenCalledTimes(1); }); it('should not close provider if it is used by another client', async () => { const provider1 = { ...mockProvider(), onClose: jest.fn() }; - OpenFeature.setProvider('client1', provider1); - OpenFeature.setProvider('client2', provider1); + OpenFeature.setProvider('domain1', provider1); + OpenFeature.setProvider('domain2', provider1); - OpenFeature.setProvider('client1', { ...provider1 }); + OpenFeature.setProvider('domain1', { ...provider1 }); expect(provider1.onClose).not.toHaveBeenCalled(); - OpenFeature.setProvider('client2', { ...provider1 }); + OpenFeature.setProvider('domain2', { ...provider1 }); expect(provider1.onClose).toHaveBeenCalledTimes(1); }); - it('should return the default provider metadata when passing an unregistered client name', async () => { + it('should return the default provider metadata when passing an unregistered domain', async () => { const mockProvider = { metadata: { name: 'test' } } as unknown as Provider; OpenFeature.setProvider(mockProvider); const metadata = OpenFeature.getProviderMetadata('unused'); expect(metadata.name === mockProvider.metadata.name).toBeTruthy(); }); - it('should return the named provider metadata when passing a registered client name', async () => { + it('should return the domain specific provider metadata when passing a registered domain', async () => { const mockProvider = { metadata: { name: 'mock' } } as unknown as Provider; - const mockNamedProvider = { metadata: { name: 'named-mock' } } as unknown as Provider; + const mockDomainProvider = { metadata: { name: 'named-mock' } } as unknown as Provider; OpenFeature.setProvider(mockProvider); - OpenFeature.setProvider('mocked', mockNamedProvider); + OpenFeature.setProvider('mocked', mockDomainProvider); const metadata = OpenFeature.getProviderMetadata('mocked'); - expect(metadata.name === mockNamedProvider.metadata.name).toBeTruthy(); + expect(metadata.name === mockDomainProvider.metadata.name).toBeTruthy(); }); }); @@ -167,25 +167,27 @@ describe('OpenFeature', () => { expect(OpenFeature.getClient).toBeDefined(); expect(OpenFeature.getClient()).toBeInstanceOf(OpenFeatureClient); - const name = 'my-client'; - const namedClient = OpenFeature.getClient(name); + const domain = 'my-domain'; + const domainSpecificClient = OpenFeature.getClient(domain); // check that using a named configuration also works as expected. - expect(namedClient).toBeInstanceOf(OpenFeatureClient); - expect(namedClient.metadata.name).toEqual(name); + expect(domainSpecificClient).toBeInstanceOf(OpenFeatureClient); + // Alias for domain, left for backwards compatibility + expect(domainSpecificClient.metadata.name).toEqual(domain); + expect(domainSpecificClient.metadata.domain).toEqual(domain); }); - it('should return a client with the default provider if no provider has been bound to the name', () => { - const namedClient = OpenFeature.getClient('unbound'); - expect(namedClient.metadata.providerMetadata.name).toEqual(OpenFeature.providerMetadata.name); + it('should return a client with the default provider if no provider has been bound to the domain', () => { + const domainSpecificClient = OpenFeature.getClient('unbound'); + expect(domainSpecificClient.metadata.providerMetadata.name).toEqual(OpenFeature.providerMetadata.name); }); - it('should return a client with the provider bound to the name', () => { - const name = 'my-named-client'; + it('should return a client with the provider bound to the domain', () => { + const domain = 'my-domain'; const fakeProvider = { metadata: { name: 'test' } } as unknown as Provider; - OpenFeature.setProvider(name, fakeProvider); + OpenFeature.setProvider(domain, fakeProvider); - const namedClient = OpenFeature.getClient(name); + const namedClient = OpenFeature.getClient(domain); expect(namedClient.metadata.providerMetadata.name).toEqual(fakeProvider.metadata.name); }); @@ -208,8 +210,8 @@ describe('OpenFeature', () => { it('runs the shutdown function on all providers for all clients', async () => { const provider = mockProvider(); OpenFeature.setProvider(provider); - OpenFeature.setProvider('client1', { ...provider }); - OpenFeature.setProvider('client2', { ...provider }); + OpenFeature.setProvider('domain1', { ...provider }); + OpenFeature.setProvider('domain2', { ...provider }); expect(OpenFeature.providerMetadata.name).toBe(provider.metadata.name); await OpenFeature.close(); @@ -226,8 +228,8 @@ describe('OpenFeature', () => { const provider3 = mockProvider(); OpenFeature.setProvider(provider1); - OpenFeature.setProvider('client1', provider2); - OpenFeature.setProvider('client2', provider3); + OpenFeature.setProvider('domain1', provider2); + OpenFeature.setProvider('domain2', provider3); expect(OpenFeature.providerMetadata.name).toBe(provider1.metadata.name); await OpenFeature.close(); diff --git a/packages/shared/src/client/client.ts b/packages/shared/src/client/client.ts index d44ebbd5c..550262efa 100644 --- a/packages/shared/src/client/client.ts +++ b/packages/shared/src/client/client.ts @@ -2,7 +2,11 @@ import { Metadata } from '../types'; import { ProviderMetadata } from '../provider/provider'; export interface ClientMetadata extends Metadata { - readonly version?: string; + /** + * @deprecated alias of "domain", use domain instead + */ readonly name?: string; + readonly domain?: string; + readonly version?: string; readonly providerMetadata: ProviderMetadata; } diff --git a/packages/shared/src/events/eventing.ts b/packages/shared/src/events/eventing.ts index 8b72cae84..78bccc010 100644 --- a/packages/shared/src/events/eventing.ts +++ b/packages/shared/src/events/eventing.ts @@ -6,7 +6,11 @@ export type EventMetadata = { export type CommonEventDetails = { providerName: string; + /** + * @deprecated alias of "domain", use domain instead + */ clientName?: string; + readonly domain?: string; }; type CommonEventProps = { diff --git a/packages/shared/src/hooks/evaluation-lifecycle.ts b/packages/shared/src/hooks/evaluation-lifecycle.ts index 378169029..86c5e953e 100644 --- a/packages/shared/src/hooks/evaluation-lifecycle.ts +++ b/packages/shared/src/hooks/evaluation-lifecycle.ts @@ -1,5 +1,4 @@ import { BaseHook } from './hook'; -import { FlagValue } from '../evaluation'; export interface EvaluationLifeCycle { /** @@ -16,7 +15,7 @@ export interface EvaluationLifeCycle { /** * Access all the hooks that are registered on this receiver. - * @returns {BaseHook[]} A list of the client hooks + * @returns {BaseHook[]} A list of the client hooks */ getHooks(): BaseHook[]; diff --git a/packages/shared/src/open-feature.ts b/packages/shared/src/open-feature.ts index e57ea5601..009eddc5e 100644 --- a/packages/shared/src/open-feature.ts +++ b/packages/shared/src/open-feature.ts @@ -28,7 +28,8 @@ export abstract class OpenFeatureCommonAPI

= new Map(); - protected _clientProviders: Map = new Map(); + protected _domainScopedProviders: Map = new Map(); + protected _domainScopedContext: Map = new Map(); protected _clientEvents: Map> = new Map(); protected _runsOn: Paradigm; @@ -66,11 +67,11 @@ export abstract class OpenFeatureCommonAPI

(eventType: T, handler: EventHandler): void { - [...new Map([[undefined, this._defaultProvider]]), ...this._clientProviders].forEach((keyProviderTuple) => { - const clientName = keyProviderTuple[0]; + [...new Map([[undefined, this._defaultProvider]]), ...this._domainScopedProviders].forEach((keyProviderTuple) => { + const domain = keyProviderTuple[0]; const provider = keyProviderTuple[1]; const shouldRunNow = statusMatchesEvent(eventType, keyProviderTuple[1].status); if (shouldRunNow) { // run immediately, we're in the matching state try { - handler({ clientName, providerName: provider.metadata.name }); + handler({ domain, providerName: provider.metadata.name }); } catch (err) { this._logger?.error('Error running event handler:', err); } @@ -126,8 +127,8 @@ export abstract class OpenFeatureCommonAPI

} @@ -135,40 +136,40 @@ export abstract class OpenFeatureCommonAPI

; /** - * Sets the provider that OpenFeature will use for flag evaluations of providers with the given name. + * Sets the provider that OpenFeature will use for flag evaluations on clients bound to the same domain. * A promise is returned that resolves when the provider is ready. - * Setting a provider supersedes the current provider used in new and existing clients with that name. + * Setting a provider supersedes the current provider used in new and existing clients in the same domain. * @template P - * @param {string} clientName The name to identify the client + * @param {string} domain An identifier which logically binds clients with providers * @param {P} provider The provider responsible for flag evaluations. * @returns {Promise} * @throws Uncaught exceptions thrown by the provider during initialization. */ - async setProviderAndWait(clientName: string, provider: P): Promise; - async setProviderAndWait(clientOrProvider?: string | P, providerOrUndefined?: P): Promise { - await this.setAwaitableProvider(clientOrProvider, providerOrUndefined); + async setProviderAndWait(domain: string, provider: P): Promise; + async setProviderAndWait(domainOrProvider?: string | P, providerOrUndefined?: P): Promise { + await this.setAwaitableProvider(domainOrProvider, providerOrUndefined); } /** * Sets the default provider for flag evaluations. - * This provider will be used by unnamed clients and named clients to which no provider is bound. - * Setting a provider supersedes the current provider used in new and existing clients without a name. + * The default provider will be used by domainless clients and clients associated with domains to which no provider is bound. + * Setting a provider supersedes the current provider used in new and existing unbound clients. * @template P * @param {P} provider The provider responsible for flag evaluations. * @returns {this} OpenFeature API */ setProvider(provider: P): this; /** - * Sets the provider that OpenFeature will use for flag evaluations of providers with the given name. - * Setting a provider supersedes the current provider used in new and existing clients with that name. + * Sets the provider that OpenFeature will use for flag evaluations on clients bound to the same domain. + * Setting a provider supersedes the current provider used in new and existing clients in the same domain. * @template P - * @param {string} clientName The name to identify the client + * @param {string} domain An identifier which logically binds clients with providers * @param {P} provider The provider responsible for flag evaluations. * @returns {this} OpenFeature API */ - setProvider(clientName: string, provider: P): this; - setProvider(clientOrProvider?: string | P, providerOrUndefined?: P): this { - const maybePromise = this.setAwaitableProvider(clientOrProvider, providerOrUndefined); + setProvider(domain: string, provider: P): this; + setProvider(domainOrProvider?: string | P, providerOrUndefined?: P): this { + const maybePromise = this.setAwaitableProvider(domainOrProvider, providerOrUndefined); if (maybePromise) { maybePromise.catch(() => { /* ignore, errors are emitted via the event emitter */ @@ -177,16 +178,16 @@ export abstract class OpenFeatureCommonAPI

| void { - const clientName = stringOrUndefined(clientOrProvider); - const provider = objectOrUndefined

(clientOrProvider) ?? objectOrUndefined

(providerOrUndefined); + private setAwaitableProvider(domainOrProvider?: string | P, providerOrUndefined?: P): Promise | void { + const domain = stringOrUndefined(domainOrProvider); + const provider = objectOrUndefined

(domainOrProvider) ?? objectOrUndefined

(providerOrUndefined); if (!provider) { this._logger.debug('No provider defined, ignoring setProvider call'); return; } - const oldProvider = this.getProviderForClient(clientName); + const oldProvider = this.getProviderForClient(domain); const providerName = provider.metadata.name; // ignore no-ops @@ -201,7 +202,7 @@ export abstract class OpenFeatureCommonAPI

{ // fetch the most recent event emitters, some may have been added during init - this.getAssociatedEventEmitters(clientName).forEach((emitter) => { - emitter?.emit(AllProviderEvents.Ready, { clientName, providerName }); + this.getAssociatedEventEmitters(domain).forEach((emitter) => { + emitter?.emit(AllProviderEvents.Ready, { clientName: domain, domain, providerName }); }); - this._events?.emit(AllProviderEvents.Ready, { clientName, providerName }); + this._events?.emit(AllProviderEvents.Ready, { clientName: domain, domain, providerName }); }) ?.catch((error) => { - this.getAssociatedEventEmitters(clientName).forEach((emitter) => { - emitter?.emit(AllProviderEvents.Error, { clientName, providerName, message: error?.message }); + this.getAssociatedEventEmitters(domain).forEach((emitter) => { + emitter?.emit(AllProviderEvents.Error, { + clientName: domain, + domain, + providerName, + message: error?.message, + }); + }); + this._events?.emit(AllProviderEvents.Error, { + clientName: domain, + domain, + providerName, + message: error?.message, }); - this._events?.emit(AllProviderEvents.Error, { clientName, providerName, message: error?.message }); // rethrow after emitting error events, so that public methods can control error handling throw error; }); } else { emitters.forEach((emitter) => { - emitter?.emit(AllProviderEvents.Ready, { clientName, providerName }); + emitter?.emit(AllProviderEvents.Ready, { clientName: domain, domain, providerName }); }); - this._events?.emit(AllProviderEvents.Ready, { clientName, providerName }); + this._events?.emit(AllProviderEvents.Ready, { clientName: domain, domain, providerName }); } - if (clientName) { - this._clientProviders.set(clientName, provider); + if (domain) { + this._domainScopedProviders.set(domain, provider); } else { this._defaultProvider = provider; } - this.transferListeners(oldProvider, provider, clientName, emitters); + this.transferListeners(oldProvider, provider, domain, emitters); // Do not close a provider that is bound to any client - if (![...this._clientProviders.values(), this._defaultProvider].includes(oldProvider)) { + if (![...this._domainScopedProviders.values(), this._defaultProvider].includes(oldProvider)) { oldProvider?.onClose?.(); } return initializationPromise; } - protected getProviderForClient(name?: string): P { - if (!name) { + protected getProviderForClient(domain?: string): P { + if (!domain) { return this._defaultProvider; } - return this._clientProviders.get(name) ?? this._defaultProvider; + return this._domainScopedProviders.get(domain) ?? this._defaultProvider; } - protected buildAndCacheEventEmitterForClient(name?: string): GenericEventEmitter { - const emitter = this._clientEvents.get(name); + protected buildAndCacheEventEmitterForClient(domain?: string): GenericEventEmitter { + const emitter = this._clientEvents.get(domain); if (emitter) { return emitter; @@ -271,23 +282,27 @@ export abstract class OpenFeatureCommonAPI

(AllProviderEvents).forEach( - (eventType) => - clientProvider.events?.addHandler(eventType, async (details) => { - newEmitter.emit(eventType, { ...details, clientName: name, providerName: clientProvider.metadata.name }); - }), + this._clientEvents.set(domain, newEmitter); + + const clientProvider = this.getProviderForClient(domain); + Object.values(AllProviderEvents).forEach((eventType) => + clientProvider.events?.addHandler(eventType, async (details) => { + newEmitter.emit(eventType, { + ...details, + clientName: domain, + domain, + providerName: clientProvider.metadata.name, + }); + }), ); return newEmitter; } private getUnboundEmitters(): GenericEventEmitter[] { - const namedProviders = [...this._clientProviders.keys()]; + const domainScopedProviders = [...this._domainScopedProviders.keys()]; const eventEmitterNames = [...this._clientEvents.keys()].filter(isDefined); - const unboundEmitterNames = eventEmitterNames.filter((name) => !namedProviders.includes(name)); + const unboundEmitterNames = eventEmitterNames.filter((name) => !domainScopedProviders.includes(name)); return [ // all unbound, named emitters ...unboundEmitterNames.map((name) => this._clientEvents.get(name)), @@ -296,18 +311,18 @@ export abstract class OpenFeatureCommonAPI

| undefined)[], ) { this._clientEventHandlers - .get(clientName) + .get(domain) ?.forEach((eventHandler) => oldProvider.events?.removeHandler(...eventHandler)); // iterate over the event types @@ -315,15 +330,20 @@ export abstract class OpenFeatureCommonAPI

{ // on each event type, fire the associated handlers emitters.forEach((emitter) => { - emitter?.emit(eventType, { ...details, clientName, providerName: newProvider.metadata.name }); + emitter?.emit(eventType, { ...details, clientName: domain, domain, providerName: newProvider.metadata.name }); + }); + this._events.emit(eventType, { + ...details, + clientName: domain, + domain, + providerName: newProvider.metadata.name, }); - this._events.emit(eventType, { ...details, clientName, providerName: newProvider.metadata.name }); }; return [eventType, handler]; }); - this._clientEventHandlers.set(clientName, newClientHandlers); + this._clientEventHandlers.set(domain, newClientHandlers); newClientHandlers.forEach((eventHandler) => newProvider.events?.addHandler(...eventHandler)); } @@ -334,7 +354,7 @@ export abstract class OpenFeatureCommonAPI

{ @@ -353,7 +373,7 @@ export abstract class OpenFeatureCommonAPI