@openfeature/flagd-provider 0.10.3 → 0.10.5

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 CHANGED
@@ -1,7 +1,6 @@
1
1
  # Server-Side flagd Provider
2
2
 
3
- Flagd is a simple daemon for evaluating feature flags.
4
- It is designed to conform to OpenFeature schema for flag definitions.
3
+ This provider is designed to use flagd's [evaluation protocol](https://github.com/open-feature/schemas/blob/main/protobuf/schema/v1/schema.proto), or locally evaluate flags defined in a flagd [flag definition](https://github.com/open-feature/schemas/blob/main/json/flagd-definitions.json).
5
4
  This repository and package provides the client code for interacting with it via the OpenFeature server-side JavaScript SDK.
6
5
 
7
6
  ## Installation
@@ -35,8 +34,9 @@ Options can be defined in the constructor or as environment variables. Construct
35
34
  | tls | FLAGD_TLS | boolean | false | |
36
35
  | socketPath | FLAGD_SOCKET_PATH | string | - | |
37
36
  | resolverType | FLAGD_SOURCE_RESOLVER | string | rpc | rpc, in-process |
37
+ | offlineFlagSourcePath | FLAGD_OFFLINE_FLAG_SOURCE_PATH | string | - | |
38
38
  | selector | FLAGD_SOURCE_SELECTOR | string | - | |
39
- | cache | FLAGD_CACHE | string | lru | lru,disabled |
39
+ | cache | FLAGD_CACHE | string | lru | lru, disabled |
40
40
  | maxCacheSize | FLAGD_MAX_CACHE_SIZE | int | 1000 | |
41
41
 
42
42
  Below are examples of usage patterns.
@@ -45,7 +45,7 @@ Below are examples of usage patterns.
45
45
 
46
46
  This is the default mode of operation of the provider.
47
47
  In this mode, FlagdProvider communicates with flagd via the gRPC protocol.
48
- Flag evaluations take place remotely at the connected [flagd](https://flagd.dev/) instance.
48
+ Flag evaluations take place remotely on the connected [flagd](https://flagd.dev/) instance.
49
49
 
50
50
  ```ts
51
51
  OpenFeature.setProvider(new FlagdProvider())
@@ -74,6 +74,19 @@ Flag configurations for evaluation are obtained via gRPC protocol using [sync pr
74
74
 
75
75
  In the above example, the provider expects a flag sync service implementation to be available at `localhost:8013` (default host and port).
76
76
 
77
+ In-process resolver can also work in an offline mode.
78
+ To enable this mode, you should provide a valid flag configuration file with the option `offlineFlagSourcePath`.
79
+
80
+ ```
81
+ OpenFeature.setProvider(new FlagdProvider({
82
+ resolverType: 'in-process',
83
+ offlineFlagSourcePath: './flags.json',
84
+ }))
85
+ ```
86
+
87
+ Offline mode uses `fs.watchFile` and polls every 5 seconds for changes to the file.
88
+ This mode is useful for local development, test cases, and for offline applications.
89
+
77
90
  ### Supported Events
78
91
 
79
92
  The flagd provider emits `PROVIDER_READY`, `PROVIDER_ERROR` and `PROVIDER_CONFIGURATION_CHANGED` events.
package/index.cjs.js CHANGED
@@ -8,6 +8,8 @@ var connectivityState = require('@grpc/grpc-js/build/src/connectivity-state');
8
8
  var lruCache = require('lru-cache');
9
9
  var util$6 = require('util');
10
10
  var flagdCore = require('@openfeature/flagd-core');
11
+ var core = require('@openfeature/core');
12
+ var fs = require('fs');
11
13
 
12
14
  const EVENT_CONFIGURATION_CHANGE = 'configuration_change';
13
15
  const EVENT_PROVIDER_READY = 'provider_ready';
@@ -32,10 +34,11 @@ var ENV_VAR;
32
34
  ENV_VAR["FLAGD_MAX_CACHE_SIZE"] = "FLAGD_MAX_CACHE_SIZE";
33
35
  ENV_VAR["FLAGD_SOURCE_SELECTOR"] = "FLAGD_SOURCE_SELECTOR";
34
36
  ENV_VAR["FLAGD_RESOLVER"] = "FLAGD_RESOLVER";
37
+ ENV_VAR["FLAGD_OFFLINE_FLAG_SOURCE_PATH"] = "FLAGD_OFFLINE_FLAG_SOURCE_PATH";
35
38
  })(ENV_VAR || (ENV_VAR = {}));
36
39
  const getEnvVarConfig = () => {
37
40
  var _a;
38
- return (Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (process.env[ENV_VAR.FLAGD_HOST] && {
41
+ return (Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (process.env[ENV_VAR.FLAGD_HOST] && {
39
42
  host: process.env[ENV_VAR.FLAGD_HOST],
40
43
  })), (Number(process.env[ENV_VAR.FLAGD_PORT]) && {
41
44
  port: Number(process.env[ENV_VAR.FLAGD_PORT]),
@@ -51,6 +54,8 @@ const getEnvVarConfig = () => {
51
54
  selector: process.env[ENV_VAR.FLAGD_SOURCE_SELECTOR],
52
55
  })), ((process.env[ENV_VAR.FLAGD_RESOLVER] === 'rpc' || process.env[ENV_VAR.FLAGD_RESOLVER] === 'in-process') && {
53
56
  resolverType: process.env[ENV_VAR.FLAGD_RESOLVER],
57
+ })), (process.env[ENV_VAR.FLAGD_OFFLINE_FLAG_SOURCE_PATH] && {
58
+ offlineFlagSourcePath: process.env[ENV_VAR.FLAGD_OFFLINE_FLAG_SOURCE_PATH],
54
59
  })));
55
60
  };
56
61
  function getConfig(options = {}) {
@@ -5942,13 +5947,15 @@ class GRPCService {
5942
5947
  if (data && typeof data === 'object' && 'flags' in data && (data === null || data === void 0 ? void 0 : data['flags'])) {
5943
5948
  const flagChangeMessage = data;
5944
5949
  const flagsChanged = Object.keys(flagChangeMessage.flags || []);
5945
- // remove each changed key from cache
5946
- flagsChanged.forEach((key) => {
5947
- var _a, _b;
5948
- if ((_a = this._cache) === null || _a === void 0 ? void 0 : _a.delete(key)) {
5949
- (_b = this.logger) === null || _b === void 0 ? void 0 : _b.debug(`${FlagdProvider.name}: evicted key: ${key} from cache.`);
5950
- }
5951
- });
5950
+ if (this._cacheEnabled) {
5951
+ // remove each changed key from cache
5952
+ flagsChanged.forEach((key) => {
5953
+ var _a, _b;
5954
+ if ((_a = this._cache) === null || _a === void 0 ? void 0 : _a.delete(key)) {
5955
+ (_b = this.logger) === null || _b === void 0 ? void 0 : _b.debug(`${FlagdProvider.name}: evicted key: ${key} from cache.`);
5956
+ }
5957
+ });
5958
+ }
5952
5959
  changedCallback(flagsChanged);
5953
5960
  }
5954
5961
  }
@@ -5961,7 +5968,7 @@ class GRPCService {
5961
5968
  }
5962
5969
  handleError(reconnectCallback, changedCallback, disconnectCallback) {
5963
5970
  var _a, _b;
5964
- disconnectCallback();
5971
+ disconnectCallback('streaming connection error, will attempt reconnect...');
5965
5972
  (_a = this.logger) === null || _a === void 0 ? void 0 : _a.error(`${FlagdProvider.name}: streaming connection error, will attempt reconnect...`);
5966
5973
  (_b = this._cache) === null || _b === void 0 ? void 0 : _b.clear();
5967
5974
  this.reconnect(reconnectCallback, changedCallback, disconnectCallback);
@@ -6356,6 +6363,18 @@ function isSet(value) {
6356
6363
  */
6357
6364
  class GrpcFetch {
6358
6365
  constructor(config, syncServiceClient, logger) {
6366
+ /**
6367
+ * Initialized will be set to true once the initial connection is successful
6368
+ * and the first payload has been received. Subsequent reconnects will not
6369
+ * change the initialized value.
6370
+ */
6371
+ this._initialized = false;
6372
+ /**
6373
+ * Is connected represents the current known connection state. It will be
6374
+ * set to true once the first payload has been received.but will be set to
6375
+ * false if the connection is lost.
6376
+ */
6377
+ this._isConnected = false;
6359
6378
  const { host, port, tls, socketPath, selector } = config;
6360
6379
  this._syncClient = syncServiceClient
6361
6380
  ? syncServiceClient
@@ -6363,55 +6382,131 @@ class GrpcFetch {
6363
6382
  this._logger = logger;
6364
6383
  this._request = { providerId: '', selector: selector ? selector : '' };
6365
6384
  }
6366
- connect(dataFillCallback, reconnectCallback, changedCallback, disconnectCallback) {
6367
- return new Promise((resolve, reject) => this.listen(dataFillCallback, reconnectCallback, changedCallback, disconnectCallback, resolve, reject));
6385
+ connect(dataCallback, reconnectCallback, changedCallback, disconnectCallback) {
6386
+ return new Promise((resolve, reject) => this.listen(dataCallback, reconnectCallback, changedCallback, disconnectCallback, resolve, reject)).then(() => {
6387
+ this._initialized = true;
6388
+ });
6368
6389
  }
6369
6390
  disconnect() {
6370
6391
  var _a;
6371
- (_a = this._logger) === null || _a === void 0 ? void 0 : _a.debug('Disconnecting gRPC sync connection');
6372
- closeStreamIfDefined(this._syncStream);
6373
- this._syncClient.close();
6392
+ return __awaiter(this, void 0, void 0, function* () {
6393
+ (_a = this._logger) === null || _a === void 0 ? void 0 : _a.debug('Disconnecting gRPC sync connection');
6394
+ closeStreamIfDefined(this._syncStream);
6395
+ this._syncClient.close();
6396
+ });
6374
6397
  }
6375
- listen(dataFillCallback, reconnectCallback, changedCallback, disconnectCallback, resolveConnect, rejectConnect) {
6398
+ listen(dataCallback, reconnectCallback, changedCallback, disconnectCallback, resolveConnect, rejectConnect) {
6399
+ var _a;
6400
+ (_a = this._logger) === null || _a === void 0 ? void 0 : _a.debug('Starting gRPC sync connection');
6376
6401
  closeStreamIfDefined(this._syncStream);
6377
6402
  this._syncStream = this._syncClient.syncFlags(this._request);
6378
6403
  this._syncStream.on('data', (data) => {
6379
- var _a;
6380
- (_a = this._logger) === null || _a === void 0 ? void 0 : _a.debug('Received sync payload');
6381
- dataFillCallback(data.flagConfiguration);
6382
- changedCallback([]); // flags changed list not supported
6383
- // if resolveConnect is undefined, this is a reconnection; we only want to fire the reconnect callback in that case
6404
+ var _a, _b, _c, _d;
6405
+ (_a = this._logger) === null || _a === void 0 ? void 0 : _a.debug(`Received sync payload`);
6406
+ try {
6407
+ const changes = dataCallback(data.flagConfiguration);
6408
+ if (this._initialized && changes.length > 0) {
6409
+ changedCallback(changes);
6410
+ }
6411
+ }
6412
+ catch (err) {
6413
+ (_b = this._logger) === null || _b === void 0 ? void 0 : _b.debug('Error processing sync payload: ', (_c = err === null || err === void 0 ? void 0 : err.message) !== null && _c !== void 0 ? _c : 'unknown error');
6414
+ }
6384
6415
  if (resolveConnect) {
6385
6416
  resolveConnect();
6386
6417
  }
6387
- else {
6418
+ else if (!this._isConnected) {
6419
+ // Not the first connection and there's no active connection.
6420
+ (_d = this._logger) === null || _d === void 0 ? void 0 : _d.debug('Reconnected to gRPC sync');
6388
6421
  reconnectCallback();
6389
6422
  }
6423
+ this._isConnected = true;
6390
6424
  });
6391
6425
  this._syncStream.on('error', (err) => {
6392
- var _a;
6393
- (_a = this._logger) === null || _a === void 0 ? void 0 : _a.error('Connection error, attempting to reconnect', err);
6394
- disconnectCallback();
6395
- rejectConnect === null || rejectConnect === void 0 ? void 0 : rejectConnect(new serverSdk.GeneralError('Failed to connect stream'));
6396
- this.reconnect(dataFillCallback, reconnectCallback, changedCallback, disconnectCallback);
6426
+ var _a, _b, _c;
6427
+ (_a = this._logger) === null || _a === void 0 ? void 0 : _a.error('Connection error, attempting to reconnect');
6428
+ (_b = this._logger) === null || _b === void 0 ? void 0 : _b.debug(err);
6429
+ this._isConnected = false;
6430
+ const errorMessage = (_c = err === null || err === void 0 ? void 0 : err.message) !== null && _c !== void 0 ? _c : 'Failed to connect to syncFlags stream';
6431
+ disconnectCallback(errorMessage);
6432
+ rejectConnect === null || rejectConnect === void 0 ? void 0 : rejectConnect(new serverSdk.GeneralError(errorMessage));
6433
+ this.reconnect(dataCallback, reconnectCallback, changedCallback, disconnectCallback);
6397
6434
  });
6398
6435
  }
6399
- reconnect(dataFillCallback, reconnectCallback, changedCallback, disconnectCallback) {
6436
+ reconnect(dataCallback, reconnectCallback, changedCallback, disconnectCallback) {
6400
6437
  const channel = this._syncClient.getChannel();
6401
6438
  channel.watchConnectivityState(channel.getConnectivityState(true), Infinity, () => {
6402
- this.listen(dataFillCallback, reconnectCallback, changedCallback, disconnectCallback);
6439
+ this.listen(dataCallback, reconnectCallback, changedCallback, disconnectCallback);
6440
+ });
6441
+ }
6442
+ }
6443
+
6444
+ const encoding = 'utf8';
6445
+ class FileFetch {
6446
+ constructor(filename, logger) {
6447
+ this._filename = filename;
6448
+ this._logger = logger;
6449
+ }
6450
+ connect(dataFillCallback, _, changedCallback) {
6451
+ var _a, _b;
6452
+ return __awaiter(this, void 0, void 0, function* () {
6453
+ (_a = this._logger) === null || _a === void 0 ? void 0 : _a.debug('Starting file sync connection');
6454
+ try {
6455
+ const output = yield fs.promises.readFile(this._filename, encoding);
6456
+ // Don't emit the change event for the initial read
6457
+ dataFillCallback(output);
6458
+ // Using watchFile instead of watch to support virtualized host file systems.
6459
+ fs.watchFile(this._filename, () => __awaiter(this, void 0, void 0, function* () {
6460
+ var _c;
6461
+ try {
6462
+ const data = yield fs.promises.readFile(this._filename, encoding);
6463
+ const changes = dataFillCallback(data);
6464
+ if (changes.length > 0) {
6465
+ changedCallback(changes);
6466
+ }
6467
+ }
6468
+ catch (err) {
6469
+ (_c = this._logger) === null || _c === void 0 ? void 0 : _c.error(`Error reading file: ${err}`);
6470
+ }
6471
+ }));
6472
+ }
6473
+ catch (err) {
6474
+ if (err instanceof core.OpenFeatureError) {
6475
+ throw err;
6476
+ }
6477
+ else {
6478
+ switch (err === null || err === void 0 ? void 0 : err.code) {
6479
+ case 'ENOENT':
6480
+ throw new core.GeneralError(`File not found: ${this._filename}`);
6481
+ case 'EACCES':
6482
+ throw new core.GeneralError(`File not accessible: ${this._filename}`);
6483
+ default:
6484
+ (_b = this._logger) === null || _b === void 0 ? void 0 : _b.debug(`Error reading file: ${err}`);
6485
+ throw new core.GeneralError();
6486
+ }
6487
+ }
6488
+ }
6489
+ });
6490
+ }
6491
+ disconnect() {
6492
+ return __awaiter(this, void 0, void 0, function* () {
6493
+ fs.unwatchFile(this._filename);
6403
6494
  });
6404
6495
  }
6405
6496
  }
6406
6497
 
6407
6498
  class InProcessService {
6408
6499
  constructor(config, dataFetcher, logger) {
6409
- this.logger = logger;
6500
+ this.config = config;
6410
6501
  this._flagdCore = new flagdCore.FlagdCore(undefined, logger);
6411
- this._dataFetcher = dataFetcher ? dataFetcher : new GrpcFetch(config, undefined, logger);
6502
+ this._dataFetcher = dataFetcher
6503
+ ? dataFetcher
6504
+ : config.offlineFlagSourcePath
6505
+ ? new FileFetch(config.offlineFlagSourcePath, logger)
6506
+ : new GrpcFetch(config, undefined, logger);
6412
6507
  }
6413
6508
  connect(reconnectCallback, changedCallback, disconnectCallback) {
6414
- return this._dataFetcher.connect(this.fill.bind(this), reconnectCallback, changedCallback, disconnectCallback);
6509
+ return this._dataFetcher.connect(this.setFlagConfiguration.bind(this), reconnectCallback, changedCallback, disconnectCallback);
6415
6510
  }
6416
6511
  disconnect() {
6417
6512
  return __awaiter(this, void 0, void 0, function* () {
@@ -6419,25 +6514,43 @@ class InProcessService {
6419
6514
  });
6420
6515
  }
6421
6516
  resolveBoolean(flagKey, defaultValue, context, logger) {
6422
- return Promise.resolve(this._flagdCore.resolveBooleanEvaluation(flagKey, defaultValue, context, logger));
6517
+ return __awaiter(this, void 0, void 0, function* () {
6518
+ return this.evaluate('boolean', flagKey, defaultValue, context, logger);
6519
+ });
6423
6520
  }
6424
6521
  resolveNumber(flagKey, defaultValue, context, logger) {
6425
- return Promise.resolve(this._flagdCore.resolveNumberEvaluation(flagKey, defaultValue, context, logger));
6522
+ return __awaiter(this, void 0, void 0, function* () {
6523
+ return this.evaluate('number', flagKey, defaultValue, context, logger);
6524
+ });
6426
6525
  }
6427
6526
  resolveString(flagKey, defaultValue, context, logger) {
6428
- return Promise.resolve(this._flagdCore.resolveStringEvaluation(flagKey, defaultValue, context, logger));
6527
+ return __awaiter(this, void 0, void 0, function* () {
6528
+ return this.evaluate('string', flagKey, defaultValue, context, logger);
6529
+ });
6429
6530
  }
6430
6531
  resolveObject(flagKey, defaultValue, context, logger) {
6431
- return Promise.resolve(this._flagdCore.resolveObjectEvaluation(flagKey, defaultValue, context, logger));
6532
+ return __awaiter(this, void 0, void 0, function* () {
6533
+ return this.evaluate('object', flagKey, defaultValue, context, logger);
6534
+ });
6432
6535
  }
6433
- fill(flags) {
6434
- var _a;
6435
- try {
6436
- this._flagdCore.setConfigurations(flags);
6437
- }
6438
- catch (err) {
6439
- (_a = this.logger) === null || _a === void 0 ? void 0 : _a.error(err);
6440
- }
6536
+ evaluate(type, flagKey, defaultValue, context, logger) {
6537
+ const details = this._flagdCore.resolve(type, flagKey, defaultValue, context, logger);
6538
+ return Object.assign(Object.assign({}, details), { flagMetadata: this.addFlagMetadata() });
6539
+ }
6540
+ /**
6541
+ * Adds the flag metadata to the resolution details
6542
+ */
6543
+ addFlagMetadata() {
6544
+ return Object.assign({}, (this.config.selector ? { scope: this.config.selector } : {}));
6545
+ }
6546
+ /**
6547
+ * Sets the flag configuration
6548
+ * @param flags The flags to set as stringified JSON
6549
+ * @returns {string[]} The flags that have changed
6550
+ * @throws — {Error} If the configuration string is invalid.
6551
+ */
6552
+ setFlagConfiguration(flags) {
6553
+ return this._flagdCore.setConfigurations(flags);
6441
6554
  }
6442
6555
  }
6443
6556
 
@@ -6484,9 +6597,10 @@ class FlagdProvider {
6484
6597
  this._status = serverSdk.ProviderStatus.READY;
6485
6598
  })
6486
6599
  .catch((err) => {
6487
- var _a;
6600
+ var _a, _b;
6488
6601
  this._status = serverSdk.ProviderStatus.ERROR;
6489
- (_a = this.logger) === null || _a === void 0 ? void 0 : _a.error(`${this.metadata.name}: error during initialization: ${err.message}, ${err.stack}`);
6602
+ (_a = this.logger) === null || _a === void 0 ? void 0 : _a.error(`${this.metadata.name}: error during initialization: ${err.message}`);
6603
+ (_b = this.logger) === null || _b === void 0 ? void 0 : _b.debug(err);
6490
6604
  throw err;
6491
6605
  });
6492
6606
  }
@@ -6519,9 +6633,9 @@ class FlagdProvider {
6519
6633
  this._status = serverSdk.ProviderStatus.READY;
6520
6634
  this._events.emit(serverSdk.ProviderEvents.Ready);
6521
6635
  }
6522
- handleError() {
6636
+ handleError(message) {
6523
6637
  this._status = serverSdk.ProviderStatus.ERROR;
6524
- this._events.emit(serverSdk.ProviderEvents.Error);
6638
+ this._events.emit(serverSdk.ProviderEvents.Error, { message });
6525
6639
  }
6526
6640
  handleChanged(flagsChanged) {
6527
6641
  this._events.emit(serverSdk.ProviderEvents.ConfigurationChanged, { flagsChanged });
package/index.esm.js CHANGED
@@ -4,6 +4,8 @@ import { ConnectivityState } from '@grpc/grpc-js/build/src/connectivity-state';
4
4
  import { LRUCache } from 'lru-cache';
5
5
  import { promisify } from 'util';
6
6
  import { FlagdCore } from '@openfeature/flagd-core';
7
+ import { OpenFeatureError, GeneralError as GeneralError$1 } from '@openfeature/core';
8
+ import { promises, watchFile, unwatchFile } from 'fs';
7
9
 
8
10
  const EVENT_CONFIGURATION_CHANGE = 'configuration_change';
9
11
  const EVENT_PROVIDER_READY = 'provider_ready';
@@ -28,10 +30,11 @@ var ENV_VAR;
28
30
  ENV_VAR["FLAGD_MAX_CACHE_SIZE"] = "FLAGD_MAX_CACHE_SIZE";
29
31
  ENV_VAR["FLAGD_SOURCE_SELECTOR"] = "FLAGD_SOURCE_SELECTOR";
30
32
  ENV_VAR["FLAGD_RESOLVER"] = "FLAGD_RESOLVER";
33
+ ENV_VAR["FLAGD_OFFLINE_FLAG_SOURCE_PATH"] = "FLAGD_OFFLINE_FLAG_SOURCE_PATH";
31
34
  })(ENV_VAR || (ENV_VAR = {}));
32
35
  const getEnvVarConfig = () => {
33
36
  var _a;
34
- return (Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (process.env[ENV_VAR.FLAGD_HOST] && {
37
+ return (Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (process.env[ENV_VAR.FLAGD_HOST] && {
35
38
  host: process.env[ENV_VAR.FLAGD_HOST],
36
39
  })), (Number(process.env[ENV_VAR.FLAGD_PORT]) && {
37
40
  port: Number(process.env[ENV_VAR.FLAGD_PORT]),
@@ -47,6 +50,8 @@ const getEnvVarConfig = () => {
47
50
  selector: process.env[ENV_VAR.FLAGD_SOURCE_SELECTOR],
48
51
  })), ((process.env[ENV_VAR.FLAGD_RESOLVER] === 'rpc' || process.env[ENV_VAR.FLAGD_RESOLVER] === 'in-process') && {
49
52
  resolverType: process.env[ENV_VAR.FLAGD_RESOLVER],
53
+ })), (process.env[ENV_VAR.FLAGD_OFFLINE_FLAG_SOURCE_PATH] && {
54
+ offlineFlagSourcePath: process.env[ENV_VAR.FLAGD_OFFLINE_FLAG_SOURCE_PATH],
50
55
  })));
51
56
  };
52
57
  function getConfig(options = {}) {
@@ -5938,13 +5943,15 @@ class GRPCService {
5938
5943
  if (data && typeof data === 'object' && 'flags' in data && (data === null || data === void 0 ? void 0 : data['flags'])) {
5939
5944
  const flagChangeMessage = data;
5940
5945
  const flagsChanged = Object.keys(flagChangeMessage.flags || []);
5941
- // remove each changed key from cache
5942
- flagsChanged.forEach((key) => {
5943
- var _a, _b;
5944
- if ((_a = this._cache) === null || _a === void 0 ? void 0 : _a.delete(key)) {
5945
- (_b = this.logger) === null || _b === void 0 ? void 0 : _b.debug(`${FlagdProvider.name}: evicted key: ${key} from cache.`);
5946
- }
5947
- });
5946
+ if (this._cacheEnabled) {
5947
+ // remove each changed key from cache
5948
+ flagsChanged.forEach((key) => {
5949
+ var _a, _b;
5950
+ if ((_a = this._cache) === null || _a === void 0 ? void 0 : _a.delete(key)) {
5951
+ (_b = this.logger) === null || _b === void 0 ? void 0 : _b.debug(`${FlagdProvider.name}: evicted key: ${key} from cache.`);
5952
+ }
5953
+ });
5954
+ }
5948
5955
  changedCallback(flagsChanged);
5949
5956
  }
5950
5957
  }
@@ -5957,7 +5964,7 @@ class GRPCService {
5957
5964
  }
5958
5965
  handleError(reconnectCallback, changedCallback, disconnectCallback) {
5959
5966
  var _a, _b;
5960
- disconnectCallback();
5967
+ disconnectCallback('streaming connection error, will attempt reconnect...');
5961
5968
  (_a = this.logger) === null || _a === void 0 ? void 0 : _a.error(`${FlagdProvider.name}: streaming connection error, will attempt reconnect...`);
5962
5969
  (_b = this._cache) === null || _b === void 0 ? void 0 : _b.clear();
5963
5970
  this.reconnect(reconnectCallback, changedCallback, disconnectCallback);
@@ -6352,6 +6359,18 @@ function isSet(value) {
6352
6359
  */
6353
6360
  class GrpcFetch {
6354
6361
  constructor(config, syncServiceClient, logger) {
6362
+ /**
6363
+ * Initialized will be set to true once the initial connection is successful
6364
+ * and the first payload has been received. Subsequent reconnects will not
6365
+ * change the initialized value.
6366
+ */
6367
+ this._initialized = false;
6368
+ /**
6369
+ * Is connected represents the current known connection state. It will be
6370
+ * set to true once the first payload has been received.but will be set to
6371
+ * false if the connection is lost.
6372
+ */
6373
+ this._isConnected = false;
6355
6374
  const { host, port, tls, socketPath, selector } = config;
6356
6375
  this._syncClient = syncServiceClient
6357
6376
  ? syncServiceClient
@@ -6359,55 +6378,131 @@ class GrpcFetch {
6359
6378
  this._logger = logger;
6360
6379
  this._request = { providerId: '', selector: selector ? selector : '' };
6361
6380
  }
6362
- connect(dataFillCallback, reconnectCallback, changedCallback, disconnectCallback) {
6363
- return new Promise((resolve, reject) => this.listen(dataFillCallback, reconnectCallback, changedCallback, disconnectCallback, resolve, reject));
6381
+ connect(dataCallback, reconnectCallback, changedCallback, disconnectCallback) {
6382
+ return new Promise((resolve, reject) => this.listen(dataCallback, reconnectCallback, changedCallback, disconnectCallback, resolve, reject)).then(() => {
6383
+ this._initialized = true;
6384
+ });
6364
6385
  }
6365
6386
  disconnect() {
6366
6387
  var _a;
6367
- (_a = this._logger) === null || _a === void 0 ? void 0 : _a.debug('Disconnecting gRPC sync connection');
6368
- closeStreamIfDefined(this._syncStream);
6369
- this._syncClient.close();
6388
+ return __awaiter(this, void 0, void 0, function* () {
6389
+ (_a = this._logger) === null || _a === void 0 ? void 0 : _a.debug('Disconnecting gRPC sync connection');
6390
+ closeStreamIfDefined(this._syncStream);
6391
+ this._syncClient.close();
6392
+ });
6370
6393
  }
6371
- listen(dataFillCallback, reconnectCallback, changedCallback, disconnectCallback, resolveConnect, rejectConnect) {
6394
+ listen(dataCallback, reconnectCallback, changedCallback, disconnectCallback, resolveConnect, rejectConnect) {
6395
+ var _a;
6396
+ (_a = this._logger) === null || _a === void 0 ? void 0 : _a.debug('Starting gRPC sync connection');
6372
6397
  closeStreamIfDefined(this._syncStream);
6373
6398
  this._syncStream = this._syncClient.syncFlags(this._request);
6374
6399
  this._syncStream.on('data', (data) => {
6375
- var _a;
6376
- (_a = this._logger) === null || _a === void 0 ? void 0 : _a.debug('Received sync payload');
6377
- dataFillCallback(data.flagConfiguration);
6378
- changedCallback([]); // flags changed list not supported
6379
- // if resolveConnect is undefined, this is a reconnection; we only want to fire the reconnect callback in that case
6400
+ var _a, _b, _c, _d;
6401
+ (_a = this._logger) === null || _a === void 0 ? void 0 : _a.debug(`Received sync payload`);
6402
+ try {
6403
+ const changes = dataCallback(data.flagConfiguration);
6404
+ if (this._initialized && changes.length > 0) {
6405
+ changedCallback(changes);
6406
+ }
6407
+ }
6408
+ catch (err) {
6409
+ (_b = this._logger) === null || _b === void 0 ? void 0 : _b.debug('Error processing sync payload: ', (_c = err === null || err === void 0 ? void 0 : err.message) !== null && _c !== void 0 ? _c : 'unknown error');
6410
+ }
6380
6411
  if (resolveConnect) {
6381
6412
  resolveConnect();
6382
6413
  }
6383
- else {
6414
+ else if (!this._isConnected) {
6415
+ // Not the first connection and there's no active connection.
6416
+ (_d = this._logger) === null || _d === void 0 ? void 0 : _d.debug('Reconnected to gRPC sync');
6384
6417
  reconnectCallback();
6385
6418
  }
6419
+ this._isConnected = true;
6386
6420
  });
6387
6421
  this._syncStream.on('error', (err) => {
6388
- var _a;
6389
- (_a = this._logger) === null || _a === void 0 ? void 0 : _a.error('Connection error, attempting to reconnect', err);
6390
- disconnectCallback();
6391
- rejectConnect === null || rejectConnect === void 0 ? void 0 : rejectConnect(new GeneralError('Failed to connect stream'));
6392
- this.reconnect(dataFillCallback, reconnectCallback, changedCallback, disconnectCallback);
6422
+ var _a, _b, _c;
6423
+ (_a = this._logger) === null || _a === void 0 ? void 0 : _a.error('Connection error, attempting to reconnect');
6424
+ (_b = this._logger) === null || _b === void 0 ? void 0 : _b.debug(err);
6425
+ this._isConnected = false;
6426
+ const errorMessage = (_c = err === null || err === void 0 ? void 0 : err.message) !== null && _c !== void 0 ? _c : 'Failed to connect to syncFlags stream';
6427
+ disconnectCallback(errorMessage);
6428
+ rejectConnect === null || rejectConnect === void 0 ? void 0 : rejectConnect(new GeneralError(errorMessage));
6429
+ this.reconnect(dataCallback, reconnectCallback, changedCallback, disconnectCallback);
6393
6430
  });
6394
6431
  }
6395
- reconnect(dataFillCallback, reconnectCallback, changedCallback, disconnectCallback) {
6432
+ reconnect(dataCallback, reconnectCallback, changedCallback, disconnectCallback) {
6396
6433
  const channel = this._syncClient.getChannel();
6397
6434
  channel.watchConnectivityState(channel.getConnectivityState(true), Infinity, () => {
6398
- this.listen(dataFillCallback, reconnectCallback, changedCallback, disconnectCallback);
6435
+ this.listen(dataCallback, reconnectCallback, changedCallback, disconnectCallback);
6436
+ });
6437
+ }
6438
+ }
6439
+
6440
+ const encoding = 'utf8';
6441
+ class FileFetch {
6442
+ constructor(filename, logger) {
6443
+ this._filename = filename;
6444
+ this._logger = logger;
6445
+ }
6446
+ connect(dataFillCallback, _, changedCallback) {
6447
+ var _a, _b;
6448
+ return __awaiter(this, void 0, void 0, function* () {
6449
+ (_a = this._logger) === null || _a === void 0 ? void 0 : _a.debug('Starting file sync connection');
6450
+ try {
6451
+ const output = yield promises.readFile(this._filename, encoding);
6452
+ // Don't emit the change event for the initial read
6453
+ dataFillCallback(output);
6454
+ // Using watchFile instead of watch to support virtualized host file systems.
6455
+ watchFile(this._filename, () => __awaiter(this, void 0, void 0, function* () {
6456
+ var _c;
6457
+ try {
6458
+ const data = yield promises.readFile(this._filename, encoding);
6459
+ const changes = dataFillCallback(data);
6460
+ if (changes.length > 0) {
6461
+ changedCallback(changes);
6462
+ }
6463
+ }
6464
+ catch (err) {
6465
+ (_c = this._logger) === null || _c === void 0 ? void 0 : _c.error(`Error reading file: ${err}`);
6466
+ }
6467
+ }));
6468
+ }
6469
+ catch (err) {
6470
+ if (err instanceof OpenFeatureError) {
6471
+ throw err;
6472
+ }
6473
+ else {
6474
+ switch (err === null || err === void 0 ? void 0 : err.code) {
6475
+ case 'ENOENT':
6476
+ throw new GeneralError$1(`File not found: ${this._filename}`);
6477
+ case 'EACCES':
6478
+ throw new GeneralError$1(`File not accessible: ${this._filename}`);
6479
+ default:
6480
+ (_b = this._logger) === null || _b === void 0 ? void 0 : _b.debug(`Error reading file: ${err}`);
6481
+ throw new GeneralError$1();
6482
+ }
6483
+ }
6484
+ }
6485
+ });
6486
+ }
6487
+ disconnect() {
6488
+ return __awaiter(this, void 0, void 0, function* () {
6489
+ unwatchFile(this._filename);
6399
6490
  });
6400
6491
  }
6401
6492
  }
6402
6493
 
6403
6494
  class InProcessService {
6404
6495
  constructor(config, dataFetcher, logger) {
6405
- this.logger = logger;
6496
+ this.config = config;
6406
6497
  this._flagdCore = new FlagdCore(undefined, logger);
6407
- this._dataFetcher = dataFetcher ? dataFetcher : new GrpcFetch(config, undefined, logger);
6498
+ this._dataFetcher = dataFetcher
6499
+ ? dataFetcher
6500
+ : config.offlineFlagSourcePath
6501
+ ? new FileFetch(config.offlineFlagSourcePath, logger)
6502
+ : new GrpcFetch(config, undefined, logger);
6408
6503
  }
6409
6504
  connect(reconnectCallback, changedCallback, disconnectCallback) {
6410
- return this._dataFetcher.connect(this.fill.bind(this), reconnectCallback, changedCallback, disconnectCallback);
6505
+ return this._dataFetcher.connect(this.setFlagConfiguration.bind(this), reconnectCallback, changedCallback, disconnectCallback);
6411
6506
  }
6412
6507
  disconnect() {
6413
6508
  return __awaiter(this, void 0, void 0, function* () {
@@ -6415,25 +6510,43 @@ class InProcessService {
6415
6510
  });
6416
6511
  }
6417
6512
  resolveBoolean(flagKey, defaultValue, context, logger) {
6418
- return Promise.resolve(this._flagdCore.resolveBooleanEvaluation(flagKey, defaultValue, context, logger));
6513
+ return __awaiter(this, void 0, void 0, function* () {
6514
+ return this.evaluate('boolean', flagKey, defaultValue, context, logger);
6515
+ });
6419
6516
  }
6420
6517
  resolveNumber(flagKey, defaultValue, context, logger) {
6421
- return Promise.resolve(this._flagdCore.resolveNumberEvaluation(flagKey, defaultValue, context, logger));
6518
+ return __awaiter(this, void 0, void 0, function* () {
6519
+ return this.evaluate('number', flagKey, defaultValue, context, logger);
6520
+ });
6422
6521
  }
6423
6522
  resolveString(flagKey, defaultValue, context, logger) {
6424
- return Promise.resolve(this._flagdCore.resolveStringEvaluation(flagKey, defaultValue, context, logger));
6523
+ return __awaiter(this, void 0, void 0, function* () {
6524
+ return this.evaluate('string', flagKey, defaultValue, context, logger);
6525
+ });
6425
6526
  }
6426
6527
  resolveObject(flagKey, defaultValue, context, logger) {
6427
- return Promise.resolve(this._flagdCore.resolveObjectEvaluation(flagKey, defaultValue, context, logger));
6528
+ return __awaiter(this, void 0, void 0, function* () {
6529
+ return this.evaluate('object', flagKey, defaultValue, context, logger);
6530
+ });
6428
6531
  }
6429
- fill(flags) {
6430
- var _a;
6431
- try {
6432
- this._flagdCore.setConfigurations(flags);
6433
- }
6434
- catch (err) {
6435
- (_a = this.logger) === null || _a === void 0 ? void 0 : _a.error(err);
6436
- }
6532
+ evaluate(type, flagKey, defaultValue, context, logger) {
6533
+ const details = this._flagdCore.resolve(type, flagKey, defaultValue, context, logger);
6534
+ return Object.assign(Object.assign({}, details), { flagMetadata: this.addFlagMetadata() });
6535
+ }
6536
+ /**
6537
+ * Adds the flag metadata to the resolution details
6538
+ */
6539
+ addFlagMetadata() {
6540
+ return Object.assign({}, (this.config.selector ? { scope: this.config.selector } : {}));
6541
+ }
6542
+ /**
6543
+ * Sets the flag configuration
6544
+ * @param flags The flags to set as stringified JSON
6545
+ * @returns {string[]} The flags that have changed
6546
+ * @throws — {Error} If the configuration string is invalid.
6547
+ */
6548
+ setFlagConfiguration(flags) {
6549
+ return this._flagdCore.setConfigurations(flags);
6437
6550
  }
6438
6551
  }
6439
6552
 
@@ -6480,9 +6593,10 @@ class FlagdProvider {
6480
6593
  this._status = ProviderStatus.READY;
6481
6594
  })
6482
6595
  .catch((err) => {
6483
- var _a;
6596
+ var _a, _b;
6484
6597
  this._status = ProviderStatus.ERROR;
6485
- (_a = this.logger) === null || _a === void 0 ? void 0 : _a.error(`${this.metadata.name}: error during initialization: ${err.message}, ${err.stack}`);
6598
+ (_a = this.logger) === null || _a === void 0 ? void 0 : _a.error(`${this.metadata.name}: error during initialization: ${err.message}`);
6599
+ (_b = this.logger) === null || _b === void 0 ? void 0 : _b.debug(err);
6486
6600
  throw err;
6487
6601
  });
6488
6602
  }
@@ -6515,9 +6629,9 @@ class FlagdProvider {
6515
6629
  this._status = ProviderStatus.READY;
6516
6630
  this._events.emit(ProviderEvents.Ready);
6517
6631
  }
6518
- handleError() {
6632
+ handleError(message) {
6519
6633
  this._status = ProviderStatus.ERROR;
6520
- this._events.emit(ProviderEvents.Error);
6634
+ this._events.emit(ProviderEvents.Error, { message });
6521
6635
  }
6522
6636
  handleChanged(flagsChanged) {
6523
6637
  this._events.emit(ProviderEvents.ConfigurationChanged, { flagsChanged });
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "@openfeature/flagd-provider",
3
- "version": "0.10.3",
3
+ "version": "0.10.5",
4
4
  "scripts": {
5
5
  "publish-if-not-exists": "cp $NPM_CONFIG_USERCONFIG .npmrc && if [ \"$(npm show $npm_package_name@$npm_package_version version)\" = \"$(npm run current-version -s)\" ]; then echo 'already published, skipping'; else npm publish --access public; fi",
6
6
  "current-version": "echo $npm_package_version"
7
7
  },
8
8
  "dependencies": {
9
- "@openfeature/flagd-core": "~0.1.4",
9
+ "@openfeature/flagd-core": "~0.1.10",
10
10
  "@protobuf-ts/runtime-rpc": "2.9.0",
11
11
  "lru-cache": "10.0.1",
12
12
  "util": "0.12.5"
13
13
  },
14
14
  "peerDependencies": {
15
15
  "@grpc/grpc-js": "~1.8.0 || ~1.9.0",
16
- "@openfeature/server-sdk": ">=1.6.0"
16
+ "@openfeature/server-sdk": ">=1.8.0"
17
17
  },
18
18
  "exports": {
19
19
  "./package.json": "./package.json",
@@ -36,6 +36,11 @@ export interface Config {
36
36
  * @default 'rpc'
37
37
  */
38
38
  resolverType?: ResolverType;
39
+ /**
40
+ * File source of flags to be used by offline mode.
41
+ * Setting this enables the offline mode of the in-process provider.
42
+ */
43
+ offlineFlagSourcePath?: string;
39
44
  /**
40
45
  * Selector to be used with flag sync gRPC contract.
41
46
  */
@@ -60,6 +65,7 @@ export declare function getConfig(options?: FlagdProviderOptions): {
60
65
  tls: boolean;
61
66
  socketPath?: string | undefined;
62
67
  resolverType?: ResolverType | undefined;
68
+ offlineFlagSourcePath?: string | undefined;
63
69
  selector?: string | undefined;
64
70
  cache?: CacheOption | undefined;
65
71
  maxCacheSize?: number | undefined;
@@ -26,7 +26,7 @@ export declare class GRPCService implements Service {
26
26
  private _eventStream;
27
27
  private get _cacheActive();
28
28
  constructor(config: Config, client?: ServiceClient, logger?: Logger | undefined);
29
- connect(reconnectCallback: () => void, changedCallback: (flagsChanged: string[]) => void, disconnectCallback: () => void): Promise<void>;
29
+ connect(reconnectCallback: () => void, changedCallback: (flagsChanged: string[]) => void, disconnectCallback: (message: string) => void): Promise<void>;
30
30
  disconnect(): Promise<void>;
31
31
  resolveBoolean(flagKey: string, _: boolean, context: EvaluationContext, logger: Logger): Promise<ResolutionDetails<boolean>>;
32
32
  resolveString(flagKey: string, _: string, context: EvaluationContext, logger: Logger): Promise<ResolutionDetails<string>>;
@@ -2,6 +2,32 @@
2
2
  * Contract of in-process resolver's data fetcher
3
3
  */
4
4
  export interface DataFetch {
5
- connect(dataFillCallback: (flags: string) => void, reconnectCallback: () => void, changedCallback: (flagsChanged: string[]) => void, disconnectCallback: () => void): Promise<void>;
6
- disconnect(): void;
5
+ /**
6
+ * Connects the data fetcher
7
+ */
8
+ connect(
9
+ /**
10
+ * Callback that runs when data is received from the source
11
+ * @param flags The flags from the source
12
+ * @returns The flags that have changed
13
+ */
14
+ dataCallback: (flags: string) => string[],
15
+ /**
16
+ * Callback that runs when the connection is re-established
17
+ */
18
+ reconnectCallback: () => void,
19
+ /**
20
+ * Callback that runs when flags have changed
21
+ * @param flagsChanged The flags that have changed
22
+ */
23
+ changedCallback: (flagsChanged: string[]) => void,
24
+ /**
25
+ * Callback that runs when the connection is disconnected
26
+ * @param message The reason for the disconnection
27
+ */
28
+ disconnectCallback: (message: string) => void): Promise<void>;
29
+ /**
30
+ * Disconnects the data fetcher
31
+ */
32
+ disconnect(): Promise<void>;
7
33
  }
@@ -0,0 +1,9 @@
1
+ import { Logger } from '@openfeature/core';
2
+ import { DataFetch } from '../data-fetch';
3
+ export declare class FileFetch implements DataFetch {
4
+ private _filename;
5
+ private _logger;
6
+ constructor(filename: string, logger?: Logger);
7
+ connect(dataFillCallback: (flags: string) => string[], _: () => void, changedCallback: (flagsChanged: string[]) => void): Promise<void>;
8
+ disconnect(): Promise<void>;
9
+ }
@@ -6,13 +6,25 @@ import { DataFetch } from '../data-fetch';
6
6
  * Implements the gRPC sync contract to fetch flag data.
7
7
  */
8
8
  export declare class GrpcFetch implements DataFetch {
9
- private _syncClient;
10
- private _syncStream;
9
+ private readonly _syncClient;
11
10
  private readonly _request;
11
+ private _syncStream;
12
12
  private _logger;
13
+ /**
14
+ * Initialized will be set to true once the initial connection is successful
15
+ * and the first payload has been received. Subsequent reconnects will not
16
+ * change the initialized value.
17
+ */
18
+ private _initialized;
19
+ /**
20
+ * Is connected represents the current known connection state. It will be
21
+ * set to true once the first payload has been received.but will be set to
22
+ * false if the connection is lost.
23
+ */
24
+ private _isConnected;
13
25
  constructor(config: Config, syncServiceClient?: FlagSyncServiceClient, logger?: Logger);
14
- connect(dataFillCallback: (flags: string) => void, reconnectCallback: () => void, changedCallback: (flagsChanged: string[]) => void, disconnectCallback: () => void): Promise<void>;
15
- disconnect(): void;
26
+ connect(dataCallback: (flags: string) => string[], reconnectCallback: () => void, changedCallback: (flagsChanged: string[]) => void, disconnectCallback: (message: string) => void): Promise<void>;
27
+ disconnect(): Promise<void>;
16
28
  private listen;
17
29
  private reconnect;
18
30
  }
@@ -3,15 +3,26 @@ import { EvaluationContext, JsonValue, Logger, ResolutionDetails } from '@openfe
3
3
  import { Config } from '../../configuration';
4
4
  import { DataFetch } from './data-fetch';
5
5
  export declare class InProcessService implements Service {
6
- private logger?;
6
+ private readonly config;
7
7
  private _flagdCore;
8
8
  private _dataFetcher;
9
- constructor(config: Config, dataFetcher?: DataFetch, logger?: Logger | undefined);
10
- connect(reconnectCallback: () => void, changedCallback: (flagsChanged: string[]) => void, disconnectCallback: () => void): Promise<void>;
9
+ constructor(config: Config, dataFetcher?: DataFetch, logger?: Logger);
10
+ connect(reconnectCallback: () => void, changedCallback: (flagsChanged: string[]) => void, disconnectCallback: (message: string) => void): Promise<void>;
11
11
  disconnect(): Promise<void>;
12
12
  resolveBoolean(flagKey: string, defaultValue: boolean, context: EvaluationContext, logger: Logger): Promise<ResolutionDetails<boolean>>;
13
13
  resolveNumber(flagKey: string, defaultValue: number, context: EvaluationContext, logger: Logger): Promise<ResolutionDetails<number>>;
14
14
  resolveString(flagKey: string, defaultValue: string, context: EvaluationContext, logger: Logger): Promise<ResolutionDetails<string>>;
15
15
  resolveObject<T extends JsonValue>(flagKey: string, defaultValue: T, context: EvaluationContext, logger: Logger): Promise<ResolutionDetails<T>>;
16
- private fill;
16
+ private evaluate;
17
+ /**
18
+ * Adds the flag metadata to the resolution details
19
+ */
20
+ private addFlagMetadata;
21
+ /**
22
+ * Sets the flag configuration
23
+ * @param flags The flags to set as stringified JSON
24
+ * @returns {string[]} The flags that have changed
25
+ * @throws — {Error} If the configuration string is invalid.
26
+ */
27
+ private setFlagConfiguration;
17
28
  }
@@ -1,6 +1,6 @@
1
1
  import { EvaluationContext, JsonValue, Logger, ResolutionDetails } from '@openfeature/server-sdk';
2
2
  export interface Service {
3
- connect(reconnectCallback: () => void, changedCallback: (flagsChanged: string[]) => void, disconnectCallback: () => void): Promise<void>;
3
+ connect(reconnectCallback: () => void, changedCallback: (flagsChanged: string[]) => void, disconnectCallback: (message: string) => void): Promise<void>;
4
4
  disconnect(): Promise<void>;
5
5
  resolveBoolean(flagKey: string, defaultValue: boolean, context: EvaluationContext, logger: Logger): Promise<ResolutionDetails<boolean>>;
6
6
  resolveString(flagKey: string, defaultValue: string, context: EvaluationContext, logger: Logger): Promise<ResolutionDetails<string>>;