Skip to content

Commit 67ed541

Browse files
committed
Expose creating new instances instead of an isolated singleton
1 parent e90d007 commit 67ed541

File tree

6 files changed

+60
-56
lines changed

6 files changed

+60
-56
lines changed

packages/react/src/context/use-context-mutator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export function useContextMutator(options: ContextMutationOptions = { defaultCon
3939
async (
4040
updatedContext: EvaluationContext | ((currentContext: EvaluationContext) => EvaluationContext),
4141
): Promise<void> => {
42-
// TODO: Needs to handle `isolated` option like OpenFeatureProvider
42+
// TODO: Needs to handle `openfeature` option like OpenFeatureProvider
4343
const previousContext = OpenFeature.getContext(options?.defaultContext ? undefined : domain);
4444
const resolvedContext = typeof updatedContext === 'function' ? updatedContext(previousContext) : updatedContext;
4545

packages/react/src/provider/provider.tsx

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Client } from '@openfeature/web-sdk';
1+
import type { Client, OpenFeatureAPI } from '@openfeature/web-sdk';
22
import { OpenFeature } from '@openfeature/web-sdk';
33
import * as React from 'react';
44
import type { ReactFlagEvaluationOptions } from '../options';
@@ -12,10 +12,10 @@ type ClientOrDomain =
1212
*/
1313
domain?: string;
1414
/**
15-
* If the package-local isolated OpenFeature singleton should be used
16-
* @see OpenFeature.isolated for more details.
15+
* An instance of the OpenFeature API to use.
16+
* @see OpenFeature.getIsolated for more details.
1717
*/
18-
isolated?: boolean;
18+
openfeature?: OpenFeatureAPI;
1919
client?: never;
2020
}
2121
| {
@@ -24,7 +24,7 @@ type ClientOrDomain =
2424
*/
2525
client?: Client;
2626
domain?: never;
27-
isolated?: never;
27+
openfeature?: never;
2828
};
2929

3030
type ProviderProps = {
@@ -37,11 +37,8 @@ type ProviderProps = {
3737
* @param {ProviderProps} properties props for the context provider
3838
* @returns {OpenFeatureProvider} context provider
3939
*/
40-
export function OpenFeatureProvider({ client, domain, isolated, children, ...options }: ProviderProps) {
41-
const stableClient = React.useMemo(
42-
() => client || (isolated ? OpenFeature.isolated : OpenFeature).getClient(domain),
43-
[client, domain],
44-
);
40+
export function OpenFeatureProvider({ client, domain, openfeature, children, ...options }: ProviderProps) {
41+
const stableClient = React.useMemo(() => client || (openfeature ?? OpenFeature).getClient(domain), [client, domain]);
4542

4643
return <Context.Provider value={{ client: stableClient, options, domain }}>{children}</Context.Provider>;
4744
}

packages/react/src/provider/test-provider.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export function OpenFeatureTestProvider(testProviderOptions: TestProviderProps)
8787
const effectiveProvider = (
8888
flagValueMap ? new TestProvider(flagValueMap, testProviderOptions.delayMs) : mixInNoop(provider) || NOOP_PROVIDER
8989
) as Provider;
90-
// TODO: Needs to handle `isolated` option like OpenFeatureProvider
90+
// TODO: Needs to handle `openfeature` option like OpenFeatureProvider
9191
testProviderOptions.domain
9292
? OpenFeature.setProvider(testProviderOptions.domain, effectiveProvider)
9393
: OpenFeature.setProvider(effectiveProvider);

packages/react/src/provider/use-open-feature-provider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@ export function useOpenFeatureProvider(): Provider {
1717
throw new MissingContextError('No OpenFeature context available');
1818
}
1919

20-
// TODO: Needs to handle `isolated` option like OpenFeatureProvider
20+
// TODO: Needs to handle `openfeature` option like OpenFeatureProvider
2121
return OpenFeature.getProvider(openFeatureContext.domain);
2222
}

packages/web/src/open-feature.ts

Lines changed: 29 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ type DomainRecord = {
1919
};
2020

2121
const _globalThis = globalThis as OpenFeatureGlobal;
22-
const _localThis = {} as OpenFeatureGlobal;
2322

2423
export class OpenFeatureAPI
2524
extends OpenFeatureCommonAPI<ClientProviderStatus, Provider, Hook>
@@ -40,21 +39,23 @@ export class OpenFeatureAPI
4039
}
4140

4241
/**
43-
* Gets a singleton instance of the OpenFeature API.
42+
* Gets a instance of the OpenFeature API.
4443
* @ignore
45-
* @param {boolean} global Whether to get the global (window) singleton instance or a package-local singleton instance.
44+
* @param {boolean} singleton Whether to get the global (window) singleton instance or an isolated non-singleton instance.
4645
* @returns {OpenFeatureAPI} OpenFeature API
4746
*/
48-
static getInstance(global = true): OpenFeatureAPI {
49-
const store = global ? _globalThis : _localThis;
47+
static getInstance(singleton = true): OpenFeatureAPI {
48+
if (!singleton) {
49+
return new OpenFeatureAPI();
50+
}
5051

51-
const globalApi = store[GLOBAL_OPENFEATURE_API_KEY];
52+
const globalApi = _globalThis[GLOBAL_OPENFEATURE_API_KEY];
5253
if (globalApi) {
5354
return globalApi;
5455
}
5556

5657
const instance = new OpenFeatureAPI();
57-
store[GLOBAL_OPENFEATURE_API_KEY] = instance;
58+
_globalThis[GLOBAL_OPENFEATURE_API_KEY] = instance;
5859
return instance;
5960
}
6061

@@ -427,55 +428,53 @@ export class OpenFeatureAPI
427428

428429
interface OpenFeatureAPIWithIsolated extends OpenFeatureAPI {
429430
/**
430-
* A package-local singleton instance of the OpenFeature API.
431+
* Create a new isolated, non-singleton instance of the OpenFeature API.
431432
*
432433
* By default, the OpenFeature API is exposed as a global singleton instance (stored on `window` in browsers).
433434
* While this can be very convenient as domains, providers, etc., are shared across an entire application,
434435
* this can mean that in multi-frontend architectures (e.g. micro-frontends) different parts of an application
435436
* can think they're loading different versions of OpenFeature, when they're actually all sharing the same instance.
436437
*
437-
* The `isolated` property provides access to a package-local singleton instance of the OpenFeature API,
438-
* which is not shared globally, isolated from the global singleton. As such, it will not share domains, providers,
439-
* etc., with the global singleton instance, and uses its own version of the SDK.
440-
*
441-
* The `isolated` property allows different parts of a multi-frontend application to have their own isolated
442-
* OpenFeature API instances, avoiding potential conflicts and ensuring they're using the expected version of the SDK.
443-
* However, it is still a singleton within the package though, so it will share state with other uses of the
444-
* `isolated` instance imported from the same package within the same micro-frontend.
438+
* The `getIsolated` method allows different parts of a multi-frontend application to have their own isolated
439+
* OpenFeature API instances, avoiding potential conflicts and ensuring they're using the expected version of the SDK,
440+
* and don't risk colliding with any other usages of OpenFeature on the same page.
445441
* @example
446442
* import { OpenFeature } from '@openfeature/web-sdk';
447443
*
448444
* OpenFeature.setProvider(new MyGlobalProvider()); // Sets the provider for the default domain on the global instance
449-
* OpenFeature.isolated.setProvider(new MyIsolatedProvider()); // Sets the provider for the default domain on the isolated instance
450-
*
451445
* const globalClient = OpenFeature.getClient(); // Uses MyGlobalProvider, the provider for the default domain on the global instance
452-
* const isolatedClient = OpenFeature.isolated.getClient(); // Uses MyIsolatedProvider, the provider for the default domain on the isolated instance
446+
*
447+
* export const OpenFeatureIsolated = OpenFeature.getIsolated(); // Create a new isolated instance of the OpenFeature API and export it
448+
* OpenFeatureIsolated.setProvider(new MyIsolatedProvider()); // Sets the provider for the default domain on the isolated instance
449+
* const isolatedClient = OpenFeatureIsolated.getClient(); // Uses MyIsolatedProvider, the provider for the default domain on the isolated instance
453450
*
454451
* // In the same micro-frontend, in a different file ...
455452
* import { OpenFeature } from '@openfeature/web-sdk';
453+
* import { OpenFeatureIsolated } from './other-file';
456454
*
457455
* const globalClient = OpenFeature.getClient(); // Uses MyGlobalProvider, the provider for the default domain on the global instance
458-
* const isolatedClient = OpenFeature.isolated.getClient(); // Uses MyIsolatedProvider, the provider for the default domain on the isolated instance
456+
* const isolatedClient = OpenFeatureIsolated.getClient(); // Uses MyIsolatedProvider, the provider for the default domain on the isolated instance
457+
*
458+
* const OpenFeatureIsolatedOther = OpenFeature.getIsolated(); // Create another new isolated instance of the OpenFeature API
459+
* const isolatedOtherClient = OpenFeatureIsolatedOther.getClient(); // Uses the NOOP provider, as this is a different isolated instance
459460
*
460461
* // In another micro-frontend, after the above has executed ...
461462
* import { OpenFeature } from '@openfeature/web-sdk';
462463
*
463464
* const globalClient = OpenFeature.getClient(); // Uses MyGlobalProvider, the provider for the default domain on the global instance
464-
* const isolatedClient = OpenFeature.isolated.getClient(); // Returns the NOOP provider, as this is a different isolated instance
465+
*
466+
* const OpenFeatureIsolated = OpenFeature.getIsolated(); // Create a new isolated instance of the OpenFeature API
467+
* const isolatedClient = OpenFeatureIsolated.getClient(); // Uses the NOOP provider, as this is a different isolated instance
465468
*/
466-
readonly isolated: OpenFeatureAPI;
469+
getIsolated: () => OpenFeatureAPI;
467470
}
468471

469-
const createOpenFeatureAPI = (): OpenFeatureAPIWithIsolated => {
470-
const globalInstance = OpenFeatureAPI.getInstance();
471-
const localInstance = OpenFeatureAPI.getInstance(false);
472-
473-
return Object.assign(globalInstance, {
474-
get isolated() {
475-
return localInstance;
472+
const createOpenFeatureAPI = (): OpenFeatureAPIWithIsolated =>
473+
Object.assign(OpenFeatureAPI.getInstance(), {
474+
getIsolated() {
475+
return OpenFeatureAPI.getInstance(false);
476476
},
477477
});
478-
};
479478

480479
/**
481480
* A singleton instance of the OpenFeature API.

packages/web/test/isolated.spec.ts

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,22 @@ describe('OpenFeature', () => {
5151
expect(_globalThis[GLOBAL_OPENFEATURE_API_KEY]).toBe(firstInstance);
5252
});
5353

54-
describe('OpenFeature.isolated', () => {
54+
describe('OpenFeature.getIsolated', () => {
5555
it('should not be the same instance as the global singleton', async () => {
5656
const { OpenFeature } = await import('../src');
5757

58-
expect(OpenFeature.isolated).not.toBe(OpenFeature);
58+
expect(OpenFeature.getIsolated()).not.toBe(OpenFeature);
59+
});
60+
61+
it('should not be the same instance as another isolated instance', async () => {
62+
const { OpenFeature } = await import('../src');
63+
64+
expect(OpenFeature.getIsolated()).not.toBe(OpenFeature.getIsolated());
5965
});
6066

6167
it('should not share state between global and isolated instances', async () => {
6268
const { OpenFeature, NOOP_PROVIDER } = await import('../src');
63-
const isolatedInstance = OpenFeature.isolated;
69+
const isolatedInstance = OpenFeature.getIsolated();
6470

6571
const globalProvider = new MockProvider({ name: 'global-provider' });
6672
OpenFeature.setProvider(globalProvider);
@@ -75,20 +81,22 @@ describe('OpenFeature', () => {
7581
expect(isolatedInstance.getProvider()).toBe(isolatedProvider);
7682
});
7783

78-
it('should persist when imported multiple times', async () => {
79-
const firstIsolatedInstance = (await import('../src')).OpenFeature.isolated;
80-
const secondIsolatedInstance = (await import('../src')).OpenFeature.isolated;
84+
it('should not share state between two isolated instances', async () => {
85+
const { OpenFeature, NOOP_PROVIDER } = await import('../src');
86+
const isolatedInstanceOne = OpenFeature.getIsolated();
87+
const isolatedInstanceTwo = OpenFeature.getIsolated();
8188

82-
expect(firstIsolatedInstance).toBe(secondIsolatedInstance);
83-
});
89+
const isolatedProviderOne = new MockProvider({ name: 'isolated-provider-one' });
90+
isolatedInstanceOne.setProvider(isolatedProviderOne);
8491

85-
it('should not persist via globalThis (window in browsers)', async () => {
86-
const firstIsolatedInstance = (await import('../src')).OpenFeature.isolated;
92+
expect(isolatedInstanceOne.getProvider()).toBe(isolatedProviderOne);
93+
expect(isolatedInstanceTwo.getProvider()).toBe(NOOP_PROVIDER);
8794

88-
jest.resetModules();
89-
const secondIsolatedInstance = (await import('../src')).OpenFeature.isolated;
95+
const isolatedProviderTwo = new MockProvider({ name: 'isolated-provider-two' });
96+
isolatedInstanceTwo.setProvider(isolatedProviderTwo);
9097

91-
expect(firstIsolatedInstance).not.toBe(secondIsolatedInstance);
98+
expect(isolatedInstanceOne.getProvider()).toBe(isolatedProviderOne);
99+
expect(isolatedInstanceTwo.getProvider()).toBe(isolatedProviderTwo);
92100
});
93101
});
94102
});

0 commit comments

Comments
 (0)