@openfeature/flagd-provider 0.10.2 → 0.10.4

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
@@ -45,7 +44,7 @@ Below are examples of usage patterns.
45
44
 
46
45
  This is the default mode of operation of the provider.
47
46
  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.
47
+ Flag evaluations take place remotely on the connected [flagd](https://flagd.dev/) instance.
49
48
 
50
49
  ```ts
51
50
  OpenFeature.setProvider(new FlagdProvider())
@@ -74,6 +73,19 @@ Flag configurations for evaluation are obtained via gRPC protocol using [sync pr
74
73
 
75
74
  In the above example, the provider expects a flag sync service implementation to be available at `localhost:8013` (default host and port).
76
75
 
76
+ In-process resolver can also work in an offline mode.
77
+ To enable this mode, you should provide a valid flag configuration file with the option `offlineFlagSourcePath`.
78
+
79
+ ```
80
+ OpenFeature.setProvider(new FlagdProvider({
81
+ resolverType: 'in-process',
82
+ offlineFlagSourcePath: './flags.json',
83
+ }))
84
+ ```
85
+
86
+ Offline mode uses `fs.watchFile` and polls every 5 seconds for changes to the file.
87
+ This mode is useful for local development, test cases, and for offline applications.
88
+
77
89
  ### Supported Events
78
90
 
79
91
  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';
@@ -5942,13 +5944,15 @@ class GRPCService {
5942
5944
  if (data && typeof data === 'object' && 'flags' in data && (data === null || data === void 0 ? void 0 : data['flags'])) {
5943
5945
  const flagChangeMessage = data;
5944
5946
  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
- });
5947
+ if (this._cacheEnabled) {
5948
+ // remove each changed key from cache
5949
+ flagsChanged.forEach((key) => {
5950
+ var _a, _b;
5951
+ if ((_a = this._cache) === null || _a === void 0 ? void 0 : _a.delete(key)) {
5952
+ (_b = this.logger) === null || _b === void 0 ? void 0 : _b.debug(`${FlagdProvider.name}: evicted key: ${key} from cache.`);
5953
+ }
5954
+ });
5955
+ }
5952
5956
  changedCallback(flagsChanged);
5953
5957
  }
5954
5958
  }
@@ -5961,7 +5965,7 @@ class GRPCService {
5961
5965
  }
5962
5966
  handleError(reconnectCallback, changedCallback, disconnectCallback) {
5963
5967
  var _a, _b;
5964
- disconnectCallback();
5968
+ disconnectCallback('streaming connection error, will attempt reconnect...');
5965
5969
  (_a = this.logger) === null || _a === void 0 ? void 0 : _a.error(`${FlagdProvider.name}: streaming connection error, will attempt reconnect...`);
5966
5970
  (_b = this._cache) === null || _b === void 0 ? void 0 : _b.clear();
5967
5971
  this.reconnect(reconnectCallback, changedCallback, disconnectCallback);
@@ -6356,6 +6360,18 @@ function isSet(value) {
6356
6360
  */
6357
6361
  class GrpcFetch {
6358
6362
  constructor(config, syncServiceClient, logger) {
6363
+ /**
6364
+ * Initialized will be set to true once the initial connection is successful
6365
+ * and the first payload has been received. Subsequent reconnects will not
6366
+ * change the initialized value.
6367
+ */
6368
+ this._initialized = false;
6369
+ /**
6370
+ * Is connected represents the current known connection state. It will be
6371
+ * set to true once the first payload has been received.but will be set to
6372
+ * false if the connection is lost.
6373
+ */
6374
+ this._isConnected = false;
6359
6375
  const { host, port, tls, socketPath, selector } = config;
6360
6376
  this._syncClient = syncServiceClient
6361
6377
  ? syncServiceClient
@@ -6363,54 +6379,131 @@ class GrpcFetch {
6363
6379
  this._logger = logger;
6364
6380
  this._request = { providerId: '', selector: selector ? selector : '' };
6365
6381
  }
6366
- connect(dataFillCallback, reconnectCallback, changedCallback, disconnectCallback) {
6367
- return new Promise((resolve, reject) => this.listen(dataFillCallback, reconnectCallback, changedCallback, disconnectCallback, resolve, reject));
6382
+ connect(dataCallback, reconnectCallback, changedCallback, disconnectCallback) {
6383
+ return new Promise((resolve, reject) => this.listen(dataCallback, reconnectCallback, changedCallback, disconnectCallback, resolve, reject)).then(() => {
6384
+ this._initialized = true;
6385
+ });
6368
6386
  }
6369
6387
  disconnect() {
6370
6388
  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();
6389
+ return __awaiter(this, void 0, void 0, function* () {
6390
+ (_a = this._logger) === null || _a === void 0 ? void 0 : _a.debug('Disconnecting gRPC sync connection');
6391
+ closeStreamIfDefined(this._syncStream);
6392
+ this._syncClient.close();
6393
+ });
6374
6394
  }
6375
- listen(dataFillCallback, reconnectCallback, changedCallback, disconnectCallback, resolveConnect, rejectConnect) {
6395
+ listen(dataCallback, reconnectCallback, changedCallback, disconnectCallback, resolveConnect, rejectConnect) {
6396
+ var _a;
6397
+ (_a = this._logger) === null || _a === void 0 ? void 0 : _a.debug('Starting gRPC sync connection');
6376
6398
  closeStreamIfDefined(this._syncStream);
6377
6399
  this._syncStream = this._syncClient.syncFlags(this._request);
6378
6400
  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
6401
+ var _a, _b, _c, _d;
6402
+ (_a = this._logger) === null || _a === void 0 ? void 0 : _a.debug(`Received sync payload`);
6403
+ try {
6404
+ const changes = dataCallback(data.flagConfiguration);
6405
+ if (this._initialized && changes.length > 0) {
6406
+ changedCallback(changes);
6407
+ }
6408
+ }
6409
+ catch (err) {
6410
+ (_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');
6411
+ }
6384
6412
  if (resolveConnect) {
6385
6413
  resolveConnect();
6386
6414
  }
6387
- else {
6415
+ else if (!this._isConnected) {
6416
+ // Not the first connection and there's no active connection.
6417
+ (_d = this._logger) === null || _d === void 0 ? void 0 : _d.debug('Reconnected to gRPC sync');
6388
6418
  reconnectCallback();
6389
6419
  }
6420
+ this._isConnected = true;
6390
6421
  });
6391
6422
  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);
6423
+ var _a, _b, _c;
6424
+ (_a = this._logger) === null || _a === void 0 ? void 0 : _a.error('Connection error, attempting to reconnect');
6425
+ (_b = this._logger) === null || _b === void 0 ? void 0 : _b.debug(err);
6426
+ this._isConnected = false;
6427
+ const errorMessage = (_c = err === null || err === void 0 ? void 0 : err.message) !== null && _c !== void 0 ? _c : 'Failed to connect to syncFlags stream';
6428
+ disconnectCallback(errorMessage);
6429
+ rejectConnect === null || rejectConnect === void 0 ? void 0 : rejectConnect(new serverSdk.GeneralError(errorMessage));
6430
+ this.reconnect(dataCallback, reconnectCallback, changedCallback, disconnectCallback);
6397
6431
  });
6398
6432
  }
6399
- reconnect(dataFillCallback, reconnectCallback, changedCallback, disconnectCallback) {
6433
+ reconnect(dataCallback, reconnectCallback, changedCallback, disconnectCallback) {
6400
6434
  const channel = this._syncClient.getChannel();
6401
6435
  channel.watchConnectivityState(channel.getConnectivityState(true), Infinity, () => {
6402
- this.listen(dataFillCallback, reconnectCallback, changedCallback, disconnectCallback);
6436
+ this.listen(dataCallback, reconnectCallback, changedCallback, disconnectCallback);
6437
+ });
6438
+ }
6439
+ }
6440
+
6441
+ const encoding = 'utf8';
6442
+ class FileFetch {
6443
+ constructor(filename, logger) {
6444
+ this._filename = filename;
6445
+ this._logger = logger;
6446
+ }
6447
+ connect(dataFillCallback, _, changedCallback) {
6448
+ var _a, _b;
6449
+ return __awaiter(this, void 0, void 0, function* () {
6450
+ (_a = this._logger) === null || _a === void 0 ? void 0 : _a.debug('Starting file sync connection');
6451
+ try {
6452
+ const output = yield fs.promises.readFile(this._filename, encoding);
6453
+ // Don't emit the change event for the initial read
6454
+ dataFillCallback(output);
6455
+ // Using watchFile instead of watch to support virtualized host file systems.
6456
+ fs.watchFile(this._filename, () => __awaiter(this, void 0, void 0, function* () {
6457
+ var _c;
6458
+ try {
6459
+ const data = yield fs.promises.readFile(this._filename, encoding);
6460
+ const changes = dataFillCallback(data);
6461
+ if (changes.length > 0) {
6462
+ changedCallback(changes);
6463
+ }
6464
+ }
6465
+ catch (err) {
6466
+ (_c = this._logger) === null || _c === void 0 ? void 0 : _c.error(`Error reading file: ${err}`);
6467
+ }
6468
+ }));
6469
+ }
6470
+ catch (err) {
6471
+ if (err instanceof core.OpenFeatureError) {
6472
+ throw err;
6473
+ }
6474
+ else {
6475
+ switch (err === null || err === void 0 ? void 0 : err.code) {
6476
+ case 'ENOENT':
6477
+ throw new core.GeneralError(`File not found: ${this._filename}`);
6478
+ case 'EACCES':
6479
+ throw new core.GeneralError(`File not accessible: ${this._filename}`);
6480
+ default:
6481
+ (_b = this._logger) === null || _b === void 0 ? void 0 : _b.debug(`Error reading file: ${err}`);
6482
+ throw new core.GeneralError();
6483
+ }
6484
+ }
6485
+ }
6486
+ });
6487
+ }
6488
+ disconnect() {
6489
+ return __awaiter(this, void 0, void 0, function* () {
6490
+ fs.unwatchFile(this._filename);
6403
6491
  });
6404
6492
  }
6405
6493
  }
6406
6494
 
6407
6495
  class InProcessService {
6408
6496
  constructor(config, dataFetcher, logger) {
6409
- this._flagdCore = new flagdCore.FlagdCore();
6410
- this._dataFetcher = dataFetcher ? dataFetcher : new GrpcFetch(config, undefined, logger);
6497
+ this.config = config;
6498
+ this._flagdCore = new flagdCore.FlagdCore(undefined, logger);
6499
+ this._dataFetcher = dataFetcher
6500
+ ? dataFetcher
6501
+ : config.offlineFlagSourcePath
6502
+ ? new FileFetch(config.offlineFlagSourcePath, logger)
6503
+ : new GrpcFetch(config, undefined, logger);
6411
6504
  }
6412
6505
  connect(reconnectCallback, changedCallback, disconnectCallback) {
6413
- return this._dataFetcher.connect(this.fill.bind(this), reconnectCallback, changedCallback, disconnectCallback);
6506
+ return this._dataFetcher.connect(this.setFlagConfiguration.bind(this), reconnectCallback, changedCallback, disconnectCallback);
6414
6507
  }
6415
6508
  disconnect() {
6416
6509
  return __awaiter(this, void 0, void 0, function* () {
@@ -6418,19 +6511,43 @@ class InProcessService {
6418
6511
  });
6419
6512
  }
6420
6513
  resolveBoolean(flagKey, defaultValue, context, logger) {
6421
- return Promise.resolve(this._flagdCore.resolveBooleanEvaluation(flagKey, defaultValue, context, logger));
6514
+ return __awaiter(this, void 0, void 0, function* () {
6515
+ return this.evaluate('boolean', flagKey, defaultValue, context, logger);
6516
+ });
6422
6517
  }
6423
6518
  resolveNumber(flagKey, defaultValue, context, logger) {
6424
- return Promise.resolve(this._flagdCore.resolveNumberEvaluation(flagKey, defaultValue, context, logger));
6519
+ return __awaiter(this, void 0, void 0, function* () {
6520
+ return this.evaluate('number', flagKey, defaultValue, context, logger);
6521
+ });
6425
6522
  }
6426
6523
  resolveString(flagKey, defaultValue, context, logger) {
6427
- return Promise.resolve(this._flagdCore.resolveStringEvaluation(flagKey, defaultValue, context, logger));
6524
+ return __awaiter(this, void 0, void 0, function* () {
6525
+ return this.evaluate('string', flagKey, defaultValue, context, logger);
6526
+ });
6428
6527
  }
6429
6528
  resolveObject(flagKey, defaultValue, context, logger) {
6430
- return Promise.resolve(this._flagdCore.resolveObjectEvaluation(flagKey, defaultValue, context, logger));
6529
+ return __awaiter(this, void 0, void 0, function* () {
6530
+ return this.evaluate('object', flagKey, defaultValue, context, logger);
6531
+ });
6532
+ }
6533
+ evaluate(type, flagKey, defaultValue, context, logger) {
6534
+ const details = this._flagdCore.resolve(type, flagKey, defaultValue, context, logger);
6535
+ return Object.assign(Object.assign({}, details), { flagMetadata: this.addFlagMetadata() });
6431
6536
  }
6432
- fill(flags) {
6433
- this._flagdCore.setConfigurations(flags);
6537
+ /**
6538
+ * Adds the flag metadata to the resolution details
6539
+ */
6540
+ addFlagMetadata() {
6541
+ return Object.assign({}, (this.config.selector ? { scope: this.config.selector } : {}));
6542
+ }
6543
+ /**
6544
+ * Sets the flag configuration
6545
+ * @param flags The flags to set as stringified JSON
6546
+ * @returns {string[]} The flags that have changed
6547
+ * @throws — {Error} If the configuration string is invalid.
6548
+ */
6549
+ setFlagConfiguration(flags) {
6550
+ return this._flagdCore.setConfigurations(flags);
6434
6551
  }
6435
6552
  }
6436
6553
 
@@ -6477,9 +6594,10 @@ class FlagdProvider {
6477
6594
  this._status = serverSdk.ProviderStatus.READY;
6478
6595
  })
6479
6596
  .catch((err) => {
6480
- var _a;
6597
+ var _a, _b;
6481
6598
  this._status = serverSdk.ProviderStatus.ERROR;
6482
- (_a = this.logger) === null || _a === void 0 ? void 0 : _a.error(`${this.metadata.name}: error during initialization: ${err.message}, ${err.stack}`);
6599
+ (_a = this.logger) === null || _a === void 0 ? void 0 : _a.error(`${this.metadata.name}: error during initialization: ${err.message}`);
6600
+ (_b = this.logger) === null || _b === void 0 ? void 0 : _b.debug(err);
6483
6601
  throw err;
6484
6602
  });
6485
6603
  }
@@ -6512,9 +6630,9 @@ class FlagdProvider {
6512
6630
  this._status = serverSdk.ProviderStatus.READY;
6513
6631
  this._events.emit(serverSdk.ProviderEvents.Ready);
6514
6632
  }
6515
- handleError() {
6633
+ handleError(message) {
6516
6634
  this._status = serverSdk.ProviderStatus.ERROR;
6517
- this._events.emit(serverSdk.ProviderEvents.Error);
6635
+ this._events.emit(serverSdk.ProviderEvents.Error, { message });
6518
6636
  }
6519
6637
  handleChanged(flagsChanged) {
6520
6638
  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';
@@ -5938,13 +5940,15 @@ class GRPCService {
5938
5940
  if (data && typeof data === 'object' && 'flags' in data && (data === null || data === void 0 ? void 0 : data['flags'])) {
5939
5941
  const flagChangeMessage = data;
5940
5942
  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
- });
5943
+ if (this._cacheEnabled) {
5944
+ // remove each changed key from cache
5945
+ flagsChanged.forEach((key) => {
5946
+ var _a, _b;
5947
+ if ((_a = this._cache) === null || _a === void 0 ? void 0 : _a.delete(key)) {
5948
+ (_b = this.logger) === null || _b === void 0 ? void 0 : _b.debug(`${FlagdProvider.name}: evicted key: ${key} from cache.`);
5949
+ }
5950
+ });
5951
+ }
5948
5952
  changedCallback(flagsChanged);
5949
5953
  }
5950
5954
  }
@@ -5957,7 +5961,7 @@ class GRPCService {
5957
5961
  }
5958
5962
  handleError(reconnectCallback, changedCallback, disconnectCallback) {
5959
5963
  var _a, _b;
5960
- disconnectCallback();
5964
+ disconnectCallback('streaming connection error, will attempt reconnect...');
5961
5965
  (_a = this.logger) === null || _a === void 0 ? void 0 : _a.error(`${FlagdProvider.name}: streaming connection error, will attempt reconnect...`);
5962
5966
  (_b = this._cache) === null || _b === void 0 ? void 0 : _b.clear();
5963
5967
  this.reconnect(reconnectCallback, changedCallback, disconnectCallback);
@@ -6352,6 +6356,18 @@ function isSet(value) {
6352
6356
  */
6353
6357
  class GrpcFetch {
6354
6358
  constructor(config, syncServiceClient, logger) {
6359
+ /**
6360
+ * Initialized will be set to true once the initial connection is successful
6361
+ * and the first payload has been received. Subsequent reconnects will not
6362
+ * change the initialized value.
6363
+ */
6364
+ this._initialized = false;
6365
+ /**
6366
+ * Is connected represents the current known connection state. It will be
6367
+ * set to true once the first payload has been received.but will be set to
6368
+ * false if the connection is lost.
6369
+ */
6370
+ this._isConnected = false;
6355
6371
  const { host, port, tls, socketPath, selector } = config;
6356
6372
  this._syncClient = syncServiceClient
6357
6373
  ? syncServiceClient
@@ -6359,54 +6375,131 @@ class GrpcFetch {
6359
6375
  this._logger = logger;
6360
6376
  this._request = { providerId: '', selector: selector ? selector : '' };
6361
6377
  }
6362
- connect(dataFillCallback, reconnectCallback, changedCallback, disconnectCallback) {
6363
- return new Promise((resolve, reject) => this.listen(dataFillCallback, reconnectCallback, changedCallback, disconnectCallback, resolve, reject));
6378
+ connect(dataCallback, reconnectCallback, changedCallback, disconnectCallback) {
6379
+ return new Promise((resolve, reject) => this.listen(dataCallback, reconnectCallback, changedCallback, disconnectCallback, resolve, reject)).then(() => {
6380
+ this._initialized = true;
6381
+ });
6364
6382
  }
6365
6383
  disconnect() {
6366
6384
  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();
6385
+ return __awaiter(this, void 0, void 0, function* () {
6386
+ (_a = this._logger) === null || _a === void 0 ? void 0 : _a.debug('Disconnecting gRPC sync connection');
6387
+ closeStreamIfDefined(this._syncStream);
6388
+ this._syncClient.close();
6389
+ });
6370
6390
  }
6371
- listen(dataFillCallback, reconnectCallback, changedCallback, disconnectCallback, resolveConnect, rejectConnect) {
6391
+ listen(dataCallback, reconnectCallback, changedCallback, disconnectCallback, resolveConnect, rejectConnect) {
6392
+ var _a;
6393
+ (_a = this._logger) === null || _a === void 0 ? void 0 : _a.debug('Starting gRPC sync connection');
6372
6394
  closeStreamIfDefined(this._syncStream);
6373
6395
  this._syncStream = this._syncClient.syncFlags(this._request);
6374
6396
  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
6397
+ var _a, _b, _c, _d;
6398
+ (_a = this._logger) === null || _a === void 0 ? void 0 : _a.debug(`Received sync payload`);
6399
+ try {
6400
+ const changes = dataCallback(data.flagConfiguration);
6401
+ if (this._initialized && changes.length > 0) {
6402
+ changedCallback(changes);
6403
+ }
6404
+ }
6405
+ catch (err) {
6406
+ (_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');
6407
+ }
6380
6408
  if (resolveConnect) {
6381
6409
  resolveConnect();
6382
6410
  }
6383
- else {
6411
+ else if (!this._isConnected) {
6412
+ // Not the first connection and there's no active connection.
6413
+ (_d = this._logger) === null || _d === void 0 ? void 0 : _d.debug('Reconnected to gRPC sync');
6384
6414
  reconnectCallback();
6385
6415
  }
6416
+ this._isConnected = true;
6386
6417
  });
6387
6418
  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);
6419
+ var _a, _b, _c;
6420
+ (_a = this._logger) === null || _a === void 0 ? void 0 : _a.error('Connection error, attempting to reconnect');
6421
+ (_b = this._logger) === null || _b === void 0 ? void 0 : _b.debug(err);
6422
+ this._isConnected = false;
6423
+ const errorMessage = (_c = err === null || err === void 0 ? void 0 : err.message) !== null && _c !== void 0 ? _c : 'Failed to connect to syncFlags stream';
6424
+ disconnectCallback(errorMessage);
6425
+ rejectConnect === null || rejectConnect === void 0 ? void 0 : rejectConnect(new GeneralError(errorMessage));
6426
+ this.reconnect(dataCallback, reconnectCallback, changedCallback, disconnectCallback);
6393
6427
  });
6394
6428
  }
6395
- reconnect(dataFillCallback, reconnectCallback, changedCallback, disconnectCallback) {
6429
+ reconnect(dataCallback, reconnectCallback, changedCallback, disconnectCallback) {
6396
6430
  const channel = this._syncClient.getChannel();
6397
6431
  channel.watchConnectivityState(channel.getConnectivityState(true), Infinity, () => {
6398
- this.listen(dataFillCallback, reconnectCallback, changedCallback, disconnectCallback);
6432
+ this.listen(dataCallback, reconnectCallback, changedCallback, disconnectCallback);
6433
+ });
6434
+ }
6435
+ }
6436
+
6437
+ const encoding = 'utf8';
6438
+ class FileFetch {
6439
+ constructor(filename, logger) {
6440
+ this._filename = filename;
6441
+ this._logger = logger;
6442
+ }
6443
+ connect(dataFillCallback, _, changedCallback) {
6444
+ var _a, _b;
6445
+ return __awaiter(this, void 0, void 0, function* () {
6446
+ (_a = this._logger) === null || _a === void 0 ? void 0 : _a.debug('Starting file sync connection');
6447
+ try {
6448
+ const output = yield promises.readFile(this._filename, encoding);
6449
+ // Don't emit the change event for the initial read
6450
+ dataFillCallback(output);
6451
+ // Using watchFile instead of watch to support virtualized host file systems.
6452
+ watchFile(this._filename, () => __awaiter(this, void 0, void 0, function* () {
6453
+ var _c;
6454
+ try {
6455
+ const data = yield promises.readFile(this._filename, encoding);
6456
+ const changes = dataFillCallback(data);
6457
+ if (changes.length > 0) {
6458
+ changedCallback(changes);
6459
+ }
6460
+ }
6461
+ catch (err) {
6462
+ (_c = this._logger) === null || _c === void 0 ? void 0 : _c.error(`Error reading file: ${err}`);
6463
+ }
6464
+ }));
6465
+ }
6466
+ catch (err) {
6467
+ if (err instanceof OpenFeatureError) {
6468
+ throw err;
6469
+ }
6470
+ else {
6471
+ switch (err === null || err === void 0 ? void 0 : err.code) {
6472
+ case 'ENOENT':
6473
+ throw new GeneralError$1(`File not found: ${this._filename}`);
6474
+ case 'EACCES':
6475
+ throw new GeneralError$1(`File not accessible: ${this._filename}`);
6476
+ default:
6477
+ (_b = this._logger) === null || _b === void 0 ? void 0 : _b.debug(`Error reading file: ${err}`);
6478
+ throw new GeneralError$1();
6479
+ }
6480
+ }
6481
+ }
6482
+ });
6483
+ }
6484
+ disconnect() {
6485
+ return __awaiter(this, void 0, void 0, function* () {
6486
+ unwatchFile(this._filename);
6399
6487
  });
6400
6488
  }
6401
6489
  }
6402
6490
 
6403
6491
  class InProcessService {
6404
6492
  constructor(config, dataFetcher, logger) {
6405
- this._flagdCore = new FlagdCore();
6406
- this._dataFetcher = dataFetcher ? dataFetcher : new GrpcFetch(config, undefined, logger);
6493
+ this.config = config;
6494
+ this._flagdCore = new FlagdCore(undefined, logger);
6495
+ this._dataFetcher = dataFetcher
6496
+ ? dataFetcher
6497
+ : config.offlineFlagSourcePath
6498
+ ? new FileFetch(config.offlineFlagSourcePath, logger)
6499
+ : new GrpcFetch(config, undefined, logger);
6407
6500
  }
6408
6501
  connect(reconnectCallback, changedCallback, disconnectCallback) {
6409
- return this._dataFetcher.connect(this.fill.bind(this), reconnectCallback, changedCallback, disconnectCallback);
6502
+ return this._dataFetcher.connect(this.setFlagConfiguration.bind(this), reconnectCallback, changedCallback, disconnectCallback);
6410
6503
  }
6411
6504
  disconnect() {
6412
6505
  return __awaiter(this, void 0, void 0, function* () {
@@ -6414,19 +6507,43 @@ class InProcessService {
6414
6507
  });
6415
6508
  }
6416
6509
  resolveBoolean(flagKey, defaultValue, context, logger) {
6417
- return Promise.resolve(this._flagdCore.resolveBooleanEvaluation(flagKey, defaultValue, context, logger));
6510
+ return __awaiter(this, void 0, void 0, function* () {
6511
+ return this.evaluate('boolean', flagKey, defaultValue, context, logger);
6512
+ });
6418
6513
  }
6419
6514
  resolveNumber(flagKey, defaultValue, context, logger) {
6420
- return Promise.resolve(this._flagdCore.resolveNumberEvaluation(flagKey, defaultValue, context, logger));
6515
+ return __awaiter(this, void 0, void 0, function* () {
6516
+ return this.evaluate('number', flagKey, defaultValue, context, logger);
6517
+ });
6421
6518
  }
6422
6519
  resolveString(flagKey, defaultValue, context, logger) {
6423
- return Promise.resolve(this._flagdCore.resolveStringEvaluation(flagKey, defaultValue, context, logger));
6520
+ return __awaiter(this, void 0, void 0, function* () {
6521
+ return this.evaluate('string', flagKey, defaultValue, context, logger);
6522
+ });
6424
6523
  }
6425
6524
  resolveObject(flagKey, defaultValue, context, logger) {
6426
- return Promise.resolve(this._flagdCore.resolveObjectEvaluation(flagKey, defaultValue, context, logger));
6525
+ return __awaiter(this, void 0, void 0, function* () {
6526
+ return this.evaluate('object', flagKey, defaultValue, context, logger);
6527
+ });
6528
+ }
6529
+ evaluate(type, flagKey, defaultValue, context, logger) {
6530
+ const details = this._flagdCore.resolve(type, flagKey, defaultValue, context, logger);
6531
+ return Object.assign(Object.assign({}, details), { flagMetadata: this.addFlagMetadata() });
6427
6532
  }
6428
- fill(flags) {
6429
- this._flagdCore.setConfigurations(flags);
6533
+ /**
6534
+ * Adds the flag metadata to the resolution details
6535
+ */
6536
+ addFlagMetadata() {
6537
+ return Object.assign({}, (this.config.selector ? { scope: this.config.selector } : {}));
6538
+ }
6539
+ /**
6540
+ * Sets the flag configuration
6541
+ * @param flags The flags to set as stringified JSON
6542
+ * @returns {string[]} The flags that have changed
6543
+ * @throws — {Error} If the configuration string is invalid.
6544
+ */
6545
+ setFlagConfiguration(flags) {
6546
+ return this._flagdCore.setConfigurations(flags);
6430
6547
  }
6431
6548
  }
6432
6549
 
@@ -6473,9 +6590,10 @@ class FlagdProvider {
6473
6590
  this._status = ProviderStatus.READY;
6474
6591
  })
6475
6592
  .catch((err) => {
6476
- var _a;
6593
+ var _a, _b;
6477
6594
  this._status = ProviderStatus.ERROR;
6478
- (_a = this.logger) === null || _a === void 0 ? void 0 : _a.error(`${this.metadata.name}: error during initialization: ${err.message}, ${err.stack}`);
6595
+ (_a = this.logger) === null || _a === void 0 ? void 0 : _a.error(`${this.metadata.name}: error during initialization: ${err.message}`);
6596
+ (_b = this.logger) === null || _b === void 0 ? void 0 : _b.debug(err);
6479
6597
  throw err;
6480
6598
  });
6481
6599
  }
@@ -6508,9 +6626,9 @@ class FlagdProvider {
6508
6626
  this._status = ProviderStatus.READY;
6509
6627
  this._events.emit(ProviderEvents.Ready);
6510
6628
  }
6511
- handleError() {
6629
+ handleError(message) {
6512
6630
  this._status = ProviderStatus.ERROR;
6513
- this._events.emit(ProviderEvents.Error);
6631
+ this._events.emit(ProviderEvents.Error, { message });
6514
6632
  }
6515
6633
  handleChanged(flagsChanged) {
6516
6634
  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.2",
3
+ "version": "0.10.4",
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.3",
9
+ "@openfeature/flagd-core": "~0.1.7",
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,14 +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 readonly config;
6
7
  private _flagdCore;
7
8
  private _dataFetcher;
8
9
  constructor(config: Config, dataFetcher?: DataFetch, logger?: Logger);
9
- connect(reconnectCallback: () => void, changedCallback: (flagsChanged: string[]) => void, disconnectCallback: () => void): Promise<void>;
10
+ connect(reconnectCallback: () => void, changedCallback: (flagsChanged: string[]) => void, disconnectCallback: (message: string) => void): Promise<void>;
10
11
  disconnect(): Promise<void>;
11
12
  resolveBoolean(flagKey: string, defaultValue: boolean, context: EvaluationContext, logger: Logger): Promise<ResolutionDetails<boolean>>;
12
13
  resolveNumber(flagKey: string, defaultValue: number, context: EvaluationContext, logger: Logger): Promise<ResolutionDetails<number>>;
13
14
  resolveString(flagKey: string, defaultValue: string, context: EvaluationContext, logger: Logger): Promise<ResolutionDetails<string>>;
14
15
  resolveObject<T extends JsonValue>(flagKey: string, defaultValue: T, context: EvaluationContext, logger: Logger): Promise<ResolutionDetails<T>>;
15
- 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;
16
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>>;