@openfeature/web-sdk 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -6
- package/dist/cjs/index.js +237 -202
- package/dist/cjs/index.js.map +4 -4
- package/dist/esm/index.js +237 -201
- package/dist/esm/index.js.map +4 -4
- package/dist/types.d.ts +85 -57
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
<!-- x-hide-in-docs-end -->
|
|
13
13
|
<!-- The 'github-badges' class is used in the docs -->
|
|
14
14
|
<p align="center" class="github-badges">
|
|
15
|
-
<a href="https://github.com/open-feature/spec/releases/tag/v0.
|
|
16
|
-
<img alt="Specification" src="https://img.shields.io/static/v1?label=specification&message=v0.
|
|
15
|
+
<a href="https://github.com/open-feature/spec/releases/tag/v0.8.0">
|
|
16
|
+
<img alt="Specification" src="https://img.shields.io/static/v1?label=specification&message=v0.8.0&color=yellow&style=for-the-badge" />
|
|
17
17
|
</a>
|
|
18
18
|
<!-- x-release-please-start-version -->
|
|
19
|
-
<a href="https://github.com/open-feature/js-sdk/releases/tag/web-sdk-v1.0
|
|
20
|
-
<img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v1.0
|
|
19
|
+
<a href="https://github.com/open-feature/js-sdk/releases/tag/web-sdk-v1.1.0">
|
|
20
|
+
<img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v1.1.0&color=blue&style=for-the-badge" />
|
|
21
21
|
</a>
|
|
22
22
|
<!-- x-release-please-end -->
|
|
23
23
|
<br/>
|
|
@@ -54,6 +54,9 @@
|
|
|
54
54
|
npm install --save @openfeature/web-sdk
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
> [!TIP]
|
|
58
|
+
> This SDK is designed to run in the browser. If you're interested in server support, check out the [Node.js SDK](https://openfeature.dev/docs/reference/technologies/server/javascript/).
|
|
59
|
+
|
|
57
60
|
#### yarn
|
|
58
61
|
|
|
59
62
|
```sh
|
|
@@ -118,7 +121,7 @@ To register a provider and ensure it is ready before further actions are taken,
|
|
|
118
121
|
|
|
119
122
|
```ts
|
|
120
123
|
await OpenFeature.setProviderAndWait(new MyProvider());
|
|
121
|
-
```
|
|
124
|
+
```
|
|
122
125
|
|
|
123
126
|
#### Synchronous
|
|
124
127
|
|
|
@@ -155,9 +158,16 @@ Sometimes, the value of a flag must consider some dynamic criteria about the app
|
|
|
155
158
|
In OpenFeature, we refer to this as [targeting](https://openfeature.dev/specification/glossary#targeting).
|
|
156
159
|
If the flag management system you're using supports targeting, you can provide the input data using the [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context).
|
|
157
160
|
|
|
161
|
+
```ts
|
|
162
|
+
// Sets global context during provider registration
|
|
163
|
+
await OpenFeature.setProvider(new MyProvider(), { origin: document.location.host });
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Change context after the provider has been registered using `setContext`.
|
|
167
|
+
|
|
158
168
|
```ts
|
|
159
169
|
// Set a value to the global context
|
|
160
|
-
await OpenFeature.setContext({
|
|
170
|
+
await OpenFeature.setContext({ targetingKey: localStorage.getItem("targetingKey") });
|
|
161
171
|
```
|
|
162
172
|
|
|
163
173
|
Context is global and setting it is `async`.
|
|
@@ -230,6 +240,24 @@ const domainScopedClient = OpenFeature.getClient("my-domain");
|
|
|
230
240
|
Domains can be defined on a provider during registration.
|
|
231
241
|
For more details, please refer to the [providers](#providers) section.
|
|
232
242
|
|
|
243
|
+
#### Manage evaluation context for domains
|
|
244
|
+
|
|
245
|
+
By default, domain-scoped clients use the global context.
|
|
246
|
+
This can be overridden by explicitly setting context when registering the provider or by references the domain when updating context:
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
OpenFeature.setProvider("my-domain", new NewCachedProvider(), { targetingKey: localStorage.getItem("targetingKey") });
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
To change context after the provider has been registered, use `setContext` with a name:
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
await OpenFeature.setContext("my-domain", { targetingKey: localStorage.getItem("targetingKey") })
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Once context has been defined for a named client, it will override the global context for all clients using the associated provider.
|
|
259
|
+
Context can be cleared using for a named provider using `OpenFeature.clearContext("my-domain")` or call `OpenFeature.clearContexts()` to reset all context.
|
|
260
|
+
|
|
233
261
|
### Eventing
|
|
234
262
|
|
|
235
263
|
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.
|
package/dist/cjs/index.js
CHANGED
|
@@ -220,38 +220,14 @@ __export(src_exports, {
|
|
|
220
220
|
NOOP_PROVIDER: () => NOOP_PROVIDER,
|
|
221
221
|
OpenFeature: () => OpenFeature,
|
|
222
222
|
OpenFeatureAPI: () => OpenFeatureAPI,
|
|
223
|
-
OpenFeatureClient: () => OpenFeatureClient,
|
|
224
223
|
OpenFeatureEventEmitter: () => OpenFeatureEventEmitter,
|
|
225
|
-
ProviderEvents: () =>
|
|
226
|
-
ProviderStatus: () =>
|
|
224
|
+
ProviderEvents: () => import_core3.ClientProviderEvents,
|
|
225
|
+
ProviderStatus: () => import_core.ClientProviderStatus
|
|
227
226
|
});
|
|
228
227
|
module.exports = __toCommonJS(src_exports);
|
|
229
228
|
|
|
230
|
-
// src/client/open-feature-client.ts
|
|
231
|
-
var import_core7 = require("@openfeature/core");
|
|
232
|
-
|
|
233
|
-
// src/open-feature.ts
|
|
234
|
-
var import_core6 = require("@openfeature/core");
|
|
235
|
-
|
|
236
|
-
// src/events/open-feature-event-emitter.ts
|
|
237
|
-
var import_core = require("@openfeature/core");
|
|
238
|
-
|
|
239
|
-
// ../../node_modules/eventemitter3/index.mjs
|
|
240
|
-
var import_index = __toESM(require_eventemitter3(), 1);
|
|
241
|
-
|
|
242
|
-
// src/events/open-feature-event-emitter.ts
|
|
243
|
-
var OpenFeatureEventEmitter = class extends import_core.GenericEventEmitter {
|
|
244
|
-
eventEmitter = new import_index.default();
|
|
245
|
-
constructor() {
|
|
246
|
-
super();
|
|
247
|
-
}
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
// src/events/events.ts
|
|
251
|
-
var import_core2 = require("@openfeature/core");
|
|
252
|
-
|
|
253
229
|
// src/provider/provider.ts
|
|
254
|
-
var
|
|
230
|
+
var import_core = require("@openfeature/core");
|
|
255
231
|
|
|
256
232
|
// src/provider/no-op-provider.ts
|
|
257
233
|
var REASON_NO_OP = "No-op";
|
|
@@ -283,6 +259,23 @@ var NOOP_PROVIDER = new NoopFeatureProvider();
|
|
|
283
259
|
// src/provider/in-memory-provider/in-memory-provider.ts
|
|
284
260
|
var import_core5 = require("@openfeature/core");
|
|
285
261
|
|
|
262
|
+
// src/events/open-feature-event-emitter.ts
|
|
263
|
+
var import_core2 = require("@openfeature/core");
|
|
264
|
+
|
|
265
|
+
// ../../node_modules/eventemitter3/index.mjs
|
|
266
|
+
var import_index = __toESM(require_eventemitter3(), 1);
|
|
267
|
+
|
|
268
|
+
// src/events/open-feature-event-emitter.ts
|
|
269
|
+
var OpenFeatureEventEmitter = class extends import_core2.GenericEventEmitter {
|
|
270
|
+
eventEmitter = new import_index.default();
|
|
271
|
+
constructor() {
|
|
272
|
+
super();
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
// src/events/events.ts
|
|
277
|
+
var import_core3 = require("@openfeature/core");
|
|
278
|
+
|
|
286
279
|
// src/provider/in-memory-provider/variant-not-found-error.ts
|
|
287
280
|
var import_core4 = require("@openfeature/core");
|
|
288
281
|
var VariantNotFoundError = class _VariantNotFoundError extends import_core4.OpenFeatureError {
|
|
@@ -326,9 +319,9 @@ var InMemoryProvider = class {
|
|
|
326
319
|
this._flagConfiguration = { ...flagConfiguration };
|
|
327
320
|
try {
|
|
328
321
|
await this.initialize(this._context);
|
|
329
|
-
this.events.emit(
|
|
322
|
+
this.events.emit(import_core3.ClientProviderEvents.ConfigurationChanged, { flagsChanged });
|
|
330
323
|
} catch (err) {
|
|
331
|
-
this.events.emit(
|
|
324
|
+
this.events.emit(import_core3.ClientProviderEvents.Error);
|
|
332
325
|
throw err;
|
|
333
326
|
}
|
|
334
327
|
}
|
|
@@ -390,172 +383,10 @@ var InMemoryProvider = class {
|
|
|
390
383
|
};
|
|
391
384
|
|
|
392
385
|
// src/open-feature.ts
|
|
393
|
-
var
|
|
394
|
-
var _globalThis = globalThis;
|
|
395
|
-
var OpenFeatureAPI = class _OpenFeatureAPI extends import_core6.OpenFeatureCommonAPI {
|
|
396
|
-
_statusEnumType = import_core3.ClientProviderStatus;
|
|
397
|
-
_apiEmitter = new OpenFeatureEventEmitter();
|
|
398
|
-
_defaultProvider = new import_core6.ProviderWrapper(NOOP_PROVIDER, import_core3.ClientProviderStatus.NOT_READY, this._statusEnumType);
|
|
399
|
-
_domainScopedProviders = /* @__PURE__ */ new Map();
|
|
400
|
-
_createEventEmitter = () => new OpenFeatureEventEmitter();
|
|
401
|
-
constructor() {
|
|
402
|
-
super("client");
|
|
403
|
-
}
|
|
404
|
-
/**
|
|
405
|
-
* Gets a singleton instance of the OpenFeature API.
|
|
406
|
-
* @ignore
|
|
407
|
-
* @returns {OpenFeatureAPI} OpenFeature API
|
|
408
|
-
*/
|
|
409
|
-
static getInstance() {
|
|
410
|
-
const globalApi = _globalThis[GLOBAL_OPENFEATURE_API_KEY];
|
|
411
|
-
if (globalApi) {
|
|
412
|
-
return globalApi;
|
|
413
|
-
}
|
|
414
|
-
const instance = new _OpenFeatureAPI();
|
|
415
|
-
_globalThis[GLOBAL_OPENFEATURE_API_KEY] = instance;
|
|
416
|
-
return instance;
|
|
417
|
-
}
|
|
418
|
-
getProviderStatus(domain) {
|
|
419
|
-
if (!domain) {
|
|
420
|
-
return this._defaultProvider.status;
|
|
421
|
-
}
|
|
422
|
-
return this._domainScopedProviders.get(domain)?.status ?? this._defaultProvider.status;
|
|
423
|
-
}
|
|
424
|
-
async setContext(domainOrContext, contextOrUndefined) {
|
|
425
|
-
const domain = (0, import_core6.stringOrUndefined)(domainOrContext);
|
|
426
|
-
const context = (0, import_core6.objectOrUndefined)(domainOrContext) ?? (0, import_core6.objectOrUndefined)(contextOrUndefined) ?? {};
|
|
427
|
-
if (domain) {
|
|
428
|
-
const wrapper = this._domainScopedProviders.get(domain);
|
|
429
|
-
if (wrapper) {
|
|
430
|
-
const oldContext = this.getContext(domain);
|
|
431
|
-
this._domainScopedContext.set(domain, context);
|
|
432
|
-
await this.runProviderContextChangeHandler(domain, wrapper, oldContext, context);
|
|
433
|
-
} else {
|
|
434
|
-
this._domainScopedContext.set(domain, context);
|
|
435
|
-
}
|
|
436
|
-
} else {
|
|
437
|
-
const oldContext = this._context;
|
|
438
|
-
this._context = context;
|
|
439
|
-
const unboundProviders = Array.from(this._domainScopedProviders.entries()).filter(([domain2]) => !this._domainScopedContext.has(domain2)).reduce((acc, [domain2, wrapper]) => {
|
|
440
|
-
acc.push({ domain: domain2, wrapper });
|
|
441
|
-
return acc;
|
|
442
|
-
}, []);
|
|
443
|
-
const allDomainRecords = [
|
|
444
|
-
// add in the default (no domain)
|
|
445
|
-
{ domain: void 0, wrapper: this._defaultProvider },
|
|
446
|
-
...unboundProviders
|
|
447
|
-
];
|
|
448
|
-
await Promise.all(
|
|
449
|
-
allDomainRecords.map(
|
|
450
|
-
(dm) => this.runProviderContextChangeHandler(dm.domain, dm.wrapper, oldContext, context)
|
|
451
|
-
)
|
|
452
|
-
);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
getContext(domainOrUndefined) {
|
|
456
|
-
const domain = (0, import_core6.stringOrUndefined)(domainOrUndefined);
|
|
457
|
-
if (domain) {
|
|
458
|
-
const context = this._domainScopedContext.get(domain);
|
|
459
|
-
if (context) {
|
|
460
|
-
return context;
|
|
461
|
-
} else {
|
|
462
|
-
this._logger.debug(`Unable to find context for '${domain}'.`);
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
return this._context;
|
|
466
|
-
}
|
|
467
|
-
async clearContext(domainOrUndefined) {
|
|
468
|
-
const domain = (0, import_core6.stringOrUndefined)(domainOrUndefined);
|
|
469
|
-
if (domain) {
|
|
470
|
-
const wrapper = this._domainScopedProviders.get(domain);
|
|
471
|
-
if (wrapper) {
|
|
472
|
-
const oldContext = this.getContext(domain);
|
|
473
|
-
this._domainScopedContext.delete(domain);
|
|
474
|
-
const newContext = this.getContext();
|
|
475
|
-
await this.runProviderContextChangeHandler(domain, wrapper, oldContext, newContext);
|
|
476
|
-
} else {
|
|
477
|
-
this._domainScopedContext.delete(domain);
|
|
478
|
-
}
|
|
479
|
-
} else {
|
|
480
|
-
return this.setContext({});
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
/**
|
|
484
|
-
* Resets the global evaluation context and removes the evaluation context for
|
|
485
|
-
* all domains.
|
|
486
|
-
*/
|
|
487
|
-
async clearContexts() {
|
|
488
|
-
await this.clearContext();
|
|
489
|
-
await Promise.allSettled(Array.from(this._domainScopedProviders.keys()).map((domain) => this.clearContext(domain)));
|
|
490
|
-
}
|
|
491
|
-
/**
|
|
492
|
-
* A factory function for creating new named OpenFeature clients. Clients can contain
|
|
493
|
-
* their own state (e.g. logger, hook, context). Multiple clients can be used
|
|
494
|
-
* to segment feature flag configuration.
|
|
495
|
-
*
|
|
496
|
-
* If there is already a provider bound to this name via {@link this.setProvider setProvider}, this provider will be used.
|
|
497
|
-
* Otherwise, the default provider is used until a provider is assigned to that name.
|
|
498
|
-
* @param {string} domain An identifier which logically binds clients with providers
|
|
499
|
-
* @param {string} version The version of the client (only used for metadata)
|
|
500
|
-
* @returns {Client} OpenFeature Client
|
|
501
|
-
*/
|
|
502
|
-
getClient(domain, version) {
|
|
503
|
-
return new OpenFeatureClient(
|
|
504
|
-
// functions are passed here to make sure that these values are always up to date,
|
|
505
|
-
// and so we don't have to make these public properties on the API class.
|
|
506
|
-
() => this.getProviderForClient(domain),
|
|
507
|
-
() => this.getProviderStatus(domain),
|
|
508
|
-
() => this.buildAndCacheEventEmitterForClient(domain),
|
|
509
|
-
() => this._logger,
|
|
510
|
-
{ domain, version }
|
|
511
|
-
);
|
|
512
|
-
}
|
|
513
|
-
/**
|
|
514
|
-
* Clears all registered providers and resets the default provider.
|
|
515
|
-
* @returns {Promise<void>}
|
|
516
|
-
*/
|
|
517
|
-
async clearProviders() {
|
|
518
|
-
await super.clearProvidersAndSetDefault(NOOP_PROVIDER);
|
|
519
|
-
this._domainScopedContext.clear();
|
|
520
|
-
}
|
|
521
|
-
async runProviderContextChangeHandler(domain, wrapper, oldContext, newContext) {
|
|
522
|
-
const providerName = wrapper.provider?.metadata?.name || "unnamed-provider";
|
|
523
|
-
try {
|
|
524
|
-
if (typeof wrapper.provider.onContextChange === "function") {
|
|
525
|
-
wrapper.incrementPendingContextChanges();
|
|
526
|
-
wrapper.status = this._statusEnumType.RECONCILING;
|
|
527
|
-
this.getAssociatedEventEmitters(domain).forEach((emitter) => {
|
|
528
|
-
emitter?.emit(import_core2.ClientProviderEvents.Reconciling, { domain, providerName });
|
|
529
|
-
});
|
|
530
|
-
this._apiEmitter?.emit(import_core2.ClientProviderEvents.Reconciling, { domain, providerName });
|
|
531
|
-
await wrapper.provider.onContextChange(oldContext, newContext);
|
|
532
|
-
wrapper.decrementPendingContextChanges();
|
|
533
|
-
}
|
|
534
|
-
wrapper.status = this._statusEnumType.READY;
|
|
535
|
-
if (wrapper.allContextChangesSettled) {
|
|
536
|
-
this.getAssociatedEventEmitters(domain).forEach((emitter) => {
|
|
537
|
-
emitter?.emit(import_core2.ClientProviderEvents.ContextChanged, { clientName: domain, domain, providerName });
|
|
538
|
-
});
|
|
539
|
-
this._apiEmitter?.emit(import_core2.ClientProviderEvents.ContextChanged, { clientName: domain, domain, providerName });
|
|
540
|
-
}
|
|
541
|
-
} catch (err) {
|
|
542
|
-
wrapper.decrementPendingContextChanges();
|
|
543
|
-
wrapper.status = this._statusEnumType.ERROR;
|
|
544
|
-
if (wrapper.allContextChangesSettled) {
|
|
545
|
-
const error = err;
|
|
546
|
-
const message = `Error running ${providerName}'s context change handler: ${error?.message}`;
|
|
547
|
-
this._logger?.error(`${message}`, err);
|
|
548
|
-
this.getAssociatedEventEmitters(domain).forEach((emitter) => {
|
|
549
|
-
emitter?.emit(import_core2.ClientProviderEvents.Error, { clientName: domain, domain, providerName, message });
|
|
550
|
-
});
|
|
551
|
-
this._apiEmitter?.emit(import_core2.ClientProviderEvents.Error, { clientName: domain, domain, providerName, message });
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
};
|
|
556
|
-
var OpenFeature = OpenFeatureAPI.getInstance();
|
|
386
|
+
var import_core7 = require("@openfeature/core");
|
|
557
387
|
|
|
558
|
-
// src/client/open-feature-client.ts
|
|
388
|
+
// src/client/internal/open-feature-client.ts
|
|
389
|
+
var import_core6 = require("@openfeature/core");
|
|
559
390
|
var OpenFeatureClient = class {
|
|
560
391
|
constructor(providerAccessor, providerStatusAccessor, emitterAccessor, globalLogger, options) {
|
|
561
392
|
this.providerAccessor = providerAccessor;
|
|
@@ -580,7 +411,7 @@ var OpenFeatureClient = class {
|
|
|
580
411
|
}
|
|
581
412
|
addHandler(eventType, handler) {
|
|
582
413
|
this.emitterAccessor().addHandler(eventType, handler);
|
|
583
|
-
const shouldRunNow = (0,
|
|
414
|
+
const shouldRunNow = (0, import_core6.statusMatchesEvent)(eventType, this.providerStatus);
|
|
584
415
|
if (shouldRunNow) {
|
|
585
416
|
try {
|
|
586
417
|
handler({
|
|
@@ -600,7 +431,7 @@ var OpenFeatureClient = class {
|
|
|
600
431
|
return this.emitterAccessor().getHandlers(eventType);
|
|
601
432
|
}
|
|
602
433
|
setLogger(logger) {
|
|
603
|
-
this._clientLogger = new
|
|
434
|
+
this._clientLogger = new import_core6.SafeLogger(logger);
|
|
604
435
|
return this;
|
|
605
436
|
}
|
|
606
437
|
addHooks(...hooks) {
|
|
@@ -674,10 +505,10 @@ var OpenFeatureClient = class {
|
|
|
674
505
|
};
|
|
675
506
|
try {
|
|
676
507
|
this.beforeHooks(allHooks, hookContext, options);
|
|
677
|
-
if (this.providerStatus ===
|
|
678
|
-
throw new
|
|
679
|
-
} else if (this.providerStatus ===
|
|
680
|
-
throw new
|
|
508
|
+
if (this.providerStatus === import_core.ClientProviderStatus.NOT_READY) {
|
|
509
|
+
throw new import_core6.ProviderNotReadyError("provider has not yet initialized");
|
|
510
|
+
} else if (this.providerStatus === import_core.ClientProviderStatus.FATAL) {
|
|
511
|
+
throw new import_core6.ProviderFatalError("provider is in an irrecoverable error state");
|
|
681
512
|
}
|
|
682
513
|
const resolution = resolver.call(this._provider, flagKey, defaultValue, context, this._logger);
|
|
683
514
|
const evaluationDetails = {
|
|
@@ -685,17 +516,20 @@ var OpenFeatureClient = class {
|
|
|
685
516
|
flagMetadata: Object.freeze(resolution.flagMetadata ?? {}),
|
|
686
517
|
flagKey
|
|
687
518
|
};
|
|
519
|
+
if (evaluationDetails.errorCode) {
|
|
520
|
+
throw (0, import_core6.instantiateErrorByErrorCode)(evaluationDetails.errorCode);
|
|
521
|
+
}
|
|
688
522
|
this.afterHooks(allHooksReversed, hookContext, evaluationDetails, options);
|
|
689
523
|
return evaluationDetails;
|
|
690
524
|
} catch (err) {
|
|
691
525
|
const errorMessage = err?.message;
|
|
692
|
-
const errorCode = err?.code ||
|
|
526
|
+
const errorCode = err?.code || import_core6.ErrorCode.GENERAL;
|
|
693
527
|
this.errorHooks(allHooksReversed, hookContext, err, options);
|
|
694
528
|
return {
|
|
695
529
|
errorCode,
|
|
696
530
|
errorMessage,
|
|
697
531
|
value: defaultValue,
|
|
698
|
-
reason:
|
|
532
|
+
reason: import_core6.StandardResolutionReasons.ERROR,
|
|
699
533
|
flagMetadata: Object.freeze({}),
|
|
700
534
|
flagKey
|
|
701
535
|
};
|
|
@@ -749,6 +583,207 @@ var OpenFeatureClient = class {
|
|
|
749
583
|
}
|
|
750
584
|
};
|
|
751
585
|
|
|
586
|
+
// src/open-feature.ts
|
|
587
|
+
var GLOBAL_OPENFEATURE_API_KEY = Symbol.for("@openfeature/web-sdk/api");
|
|
588
|
+
var _globalThis = globalThis;
|
|
589
|
+
var OpenFeatureAPI = class _OpenFeatureAPI extends import_core7.OpenFeatureCommonAPI {
|
|
590
|
+
_statusEnumType = import_core.ClientProviderStatus;
|
|
591
|
+
_apiEmitter = new OpenFeatureEventEmitter();
|
|
592
|
+
_defaultProvider = new import_core7.ProviderWrapper(
|
|
593
|
+
NOOP_PROVIDER,
|
|
594
|
+
import_core.ClientProviderStatus.NOT_READY,
|
|
595
|
+
this._statusEnumType
|
|
596
|
+
);
|
|
597
|
+
_domainScopedProviders = /* @__PURE__ */ new Map();
|
|
598
|
+
_createEventEmitter = () => new OpenFeatureEventEmitter();
|
|
599
|
+
constructor() {
|
|
600
|
+
super("client");
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Gets a singleton instance of the OpenFeature API.
|
|
604
|
+
* @ignore
|
|
605
|
+
* @returns {OpenFeatureAPI} OpenFeature API
|
|
606
|
+
*/
|
|
607
|
+
static getInstance() {
|
|
608
|
+
const globalApi = _globalThis[GLOBAL_OPENFEATURE_API_KEY];
|
|
609
|
+
if (globalApi) {
|
|
610
|
+
return globalApi;
|
|
611
|
+
}
|
|
612
|
+
const instance = new _OpenFeatureAPI();
|
|
613
|
+
_globalThis[GLOBAL_OPENFEATURE_API_KEY] = instance;
|
|
614
|
+
return instance;
|
|
615
|
+
}
|
|
616
|
+
getProviderStatus(domain) {
|
|
617
|
+
if (!domain) {
|
|
618
|
+
return this._defaultProvider.status;
|
|
619
|
+
}
|
|
620
|
+
return this._domainScopedProviders.get(domain)?.status ?? this._defaultProvider.status;
|
|
621
|
+
}
|
|
622
|
+
async setProviderAndWait(clientOrProvider, providerContextOrUndefined, contextOrUndefined) {
|
|
623
|
+
const domain = (0, import_core7.stringOrUndefined)(clientOrProvider);
|
|
624
|
+
const provider = domain ? (0, import_core7.objectOrUndefined)(providerContextOrUndefined) : (0, import_core7.objectOrUndefined)(clientOrProvider);
|
|
625
|
+
const context = domain ? (0, import_core7.objectOrUndefined)(contextOrUndefined) : (0, import_core7.objectOrUndefined)(providerContextOrUndefined);
|
|
626
|
+
if (context) {
|
|
627
|
+
if (domain) {
|
|
628
|
+
this._domainScopedContext.set(domain, context);
|
|
629
|
+
} else {
|
|
630
|
+
this._context = context;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
await this.setAwaitableProvider(domain, provider);
|
|
634
|
+
}
|
|
635
|
+
setProvider(domainOrProvider, providerContextOrUndefined, contextOrUndefined) {
|
|
636
|
+
const domain = (0, import_core7.stringOrUndefined)(domainOrProvider);
|
|
637
|
+
const provider = domain ? (0, import_core7.objectOrUndefined)(providerContextOrUndefined) : (0, import_core7.objectOrUndefined)(domainOrProvider);
|
|
638
|
+
const context = domain ? (0, import_core7.objectOrUndefined)(contextOrUndefined) : (0, import_core7.objectOrUndefined)(providerContextOrUndefined);
|
|
639
|
+
if (context) {
|
|
640
|
+
if (domain) {
|
|
641
|
+
this._domainScopedContext.set(domain, context);
|
|
642
|
+
} else {
|
|
643
|
+
this._context = context;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
const maybePromise = this.setAwaitableProvider(domain, provider);
|
|
647
|
+
Promise.resolve(maybePromise).catch((err) => {
|
|
648
|
+
this._logger.error("Error during provider initialization:", err);
|
|
649
|
+
});
|
|
650
|
+
return this;
|
|
651
|
+
}
|
|
652
|
+
async setContext(domainOrContext, contextOrUndefined) {
|
|
653
|
+
const domain = (0, import_core7.stringOrUndefined)(domainOrContext);
|
|
654
|
+
const context = (0, import_core7.objectOrUndefined)(domainOrContext) ?? (0, import_core7.objectOrUndefined)(contextOrUndefined) ?? {};
|
|
655
|
+
if (domain) {
|
|
656
|
+
const wrapper = this._domainScopedProviders.get(domain);
|
|
657
|
+
if (wrapper) {
|
|
658
|
+
const oldContext = this.getContext(domain);
|
|
659
|
+
this._domainScopedContext.set(domain, context);
|
|
660
|
+
await this.runProviderContextChangeHandler(domain, wrapper, oldContext, context);
|
|
661
|
+
} else {
|
|
662
|
+
this._domainScopedContext.set(domain, context);
|
|
663
|
+
}
|
|
664
|
+
} else {
|
|
665
|
+
const oldContext = this._context;
|
|
666
|
+
this._context = context;
|
|
667
|
+
const unboundProviders = Array.from(this._domainScopedProviders.entries()).filter(([domain2]) => !this._domainScopedContext.has(domain2)).reduce((acc, [domain2, wrapper]) => {
|
|
668
|
+
acc.push({ domain: domain2, wrapper });
|
|
669
|
+
return acc;
|
|
670
|
+
}, []);
|
|
671
|
+
const allDomainRecords = [
|
|
672
|
+
// add in the default (no domain)
|
|
673
|
+
{ domain: void 0, wrapper: this._defaultProvider },
|
|
674
|
+
...unboundProviders
|
|
675
|
+
];
|
|
676
|
+
await Promise.all(
|
|
677
|
+
allDomainRecords.map((dm) => this.runProviderContextChangeHandler(dm.domain, dm.wrapper, oldContext, context))
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
getContext(domainOrUndefined) {
|
|
682
|
+
const domain = (0, import_core7.stringOrUndefined)(domainOrUndefined);
|
|
683
|
+
if (domain) {
|
|
684
|
+
const context = this._domainScopedContext.get(domain);
|
|
685
|
+
if (context) {
|
|
686
|
+
return context;
|
|
687
|
+
} else {
|
|
688
|
+
this._logger.debug(`Unable to find context for '${domain}'.`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return this._context;
|
|
692
|
+
}
|
|
693
|
+
async clearContext(domainOrUndefined) {
|
|
694
|
+
const domain = (0, import_core7.stringOrUndefined)(domainOrUndefined);
|
|
695
|
+
if (domain) {
|
|
696
|
+
const wrapper = this._domainScopedProviders.get(domain);
|
|
697
|
+
if (wrapper) {
|
|
698
|
+
const oldContext = this.getContext(domain);
|
|
699
|
+
this._domainScopedContext.delete(domain);
|
|
700
|
+
const newContext = this.getContext();
|
|
701
|
+
await this.runProviderContextChangeHandler(domain, wrapper, oldContext, newContext);
|
|
702
|
+
} else {
|
|
703
|
+
this._domainScopedContext.delete(domain);
|
|
704
|
+
}
|
|
705
|
+
} else {
|
|
706
|
+
return this.setContext({});
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Resets the global evaluation context and removes the evaluation context for
|
|
711
|
+
* all domains.
|
|
712
|
+
*/
|
|
713
|
+
async clearContexts() {
|
|
714
|
+
await this.clearContext();
|
|
715
|
+
await Promise.allSettled(Array.from(this._domainScopedProviders.keys()).map((domain) => this.clearContext(domain)));
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* A factory function for creating new named OpenFeature clients. Clients can contain
|
|
719
|
+
* their own state (e.g. logger, hook, context). Multiple clients can be used
|
|
720
|
+
* to segment feature flag configuration.
|
|
721
|
+
*
|
|
722
|
+
* If there is already a provider bound to this name via {@link this.setProvider setProvider}, this provider will be used.
|
|
723
|
+
* Otherwise, the default provider is used until a provider is assigned to that name.
|
|
724
|
+
* @param {string} domain An identifier which logically binds clients with providers
|
|
725
|
+
* @param {string} version The version of the client (only used for metadata)
|
|
726
|
+
* @returns {Client} OpenFeature Client
|
|
727
|
+
*/
|
|
728
|
+
getClient(domain, version) {
|
|
729
|
+
return new OpenFeatureClient(
|
|
730
|
+
// functions are passed here to make sure that these values are always up to date,
|
|
731
|
+
// and so we don't have to make these public properties on the API class.
|
|
732
|
+
() => this.getProviderForClient(domain),
|
|
733
|
+
() => this.getProviderStatus(domain),
|
|
734
|
+
() => this.buildAndCacheEventEmitterForClient(domain),
|
|
735
|
+
() => this._logger,
|
|
736
|
+
{ domain, version }
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Clears all registered providers and resets the default provider.
|
|
741
|
+
* @returns {Promise<void>}
|
|
742
|
+
*/
|
|
743
|
+
async clearProviders() {
|
|
744
|
+
await super.clearProvidersAndSetDefault(NOOP_PROVIDER);
|
|
745
|
+
this._domainScopedContext.clear();
|
|
746
|
+
}
|
|
747
|
+
async runProviderContextChangeHandler(domain, wrapper, oldContext, newContext) {
|
|
748
|
+
const providerName = wrapper.provider?.metadata?.name || "unnamed-provider";
|
|
749
|
+
try {
|
|
750
|
+
if (typeof wrapper.provider.onContextChange === "function") {
|
|
751
|
+
const maybePromise = wrapper.provider.onContextChange(oldContext, newContext);
|
|
752
|
+
if (typeof maybePromise?.then === "function") {
|
|
753
|
+
wrapper.incrementPendingContextChanges();
|
|
754
|
+
wrapper.status = this._statusEnumType.RECONCILING;
|
|
755
|
+
this.getAssociatedEventEmitters(domain).forEach((emitter) => {
|
|
756
|
+
emitter?.emit(import_core3.ClientProviderEvents.Reconciling, { domain, providerName });
|
|
757
|
+
});
|
|
758
|
+
this._apiEmitter?.emit(import_core3.ClientProviderEvents.Reconciling, { domain, providerName });
|
|
759
|
+
await maybePromise;
|
|
760
|
+
wrapper.decrementPendingContextChanges();
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
wrapper.status = this._statusEnumType.READY;
|
|
764
|
+
if (wrapper.allContextChangesSettled) {
|
|
765
|
+
this.getAssociatedEventEmitters(domain).forEach((emitter) => {
|
|
766
|
+
emitter?.emit(import_core3.ClientProviderEvents.ContextChanged, { clientName: domain, domain, providerName });
|
|
767
|
+
});
|
|
768
|
+
this._apiEmitter?.emit(import_core3.ClientProviderEvents.ContextChanged, { clientName: domain, domain, providerName });
|
|
769
|
+
}
|
|
770
|
+
} catch (err) {
|
|
771
|
+
wrapper.decrementPendingContextChanges();
|
|
772
|
+
wrapper.status = this._statusEnumType.ERROR;
|
|
773
|
+
if (wrapper.allContextChangesSettled) {
|
|
774
|
+
const error = err;
|
|
775
|
+
const message = `Error running ${providerName}'s context change handler: ${error?.message}`;
|
|
776
|
+
this._logger?.error(`${message}`, err);
|
|
777
|
+
this.getAssociatedEventEmitters(domain).forEach((emitter) => {
|
|
778
|
+
emitter?.emit(import_core3.ClientProviderEvents.Error, { clientName: domain, domain, providerName, message });
|
|
779
|
+
});
|
|
780
|
+
this._apiEmitter?.emit(import_core3.ClientProviderEvents.Error, { clientName: domain, domain, providerName, message });
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
var OpenFeature = OpenFeatureAPI.getInstance();
|
|
786
|
+
|
|
752
787
|
// src/index.ts
|
|
753
788
|
__reExport(src_exports, require("@openfeature/core"), module.exports);
|
|
754
789
|
//# sourceMappingURL=index.js.map
|