flagsmith-nodejs 2.0.0-beta.6 → 2.0.1

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.
Files changed (94) hide show
  1. package/.github/workflows/pull_request.yaml +37 -29
  2. package/README.md +7 -0
  3. package/example/server/api/index.js +1 -1
  4. package/flagsmith-engine/features/models.ts +28 -6
  5. package/flagsmith-engine/features/util.ts +9 -0
  6. package/flagsmith-engine/identities/models.ts +1 -1
  7. package/flagsmith-engine/index.ts +7 -5
  8. package/flagsmith-engine/organisations/models.ts +1 -1
  9. package/flagsmith-engine/segments/models.ts +21 -4
  10. package/flagsmith-engine/segments/util.ts +8 -0
  11. package/flagsmith-engine/utils/collections.ts +1 -12
  12. package/flagsmith-engine/utils/hashing/index.ts +5 -2
  13. package/flagsmith-engine/utils/index.ts +6 -2
  14. package/package.json +4 -2
  15. package/sdk/analytics.ts +0 -2
  16. package/sdk/index.ts +84 -28
  17. package/sdk/polling_manager.ts +0 -1
  18. package/sdk/types.ts +8 -0
  19. package/sdk/utils.ts +4 -5
  20. package/tests/engine/unit/{egine.test.ts → engine.test.ts} +20 -0
  21. package/tests/engine/unit/features/models.test.ts +8 -4
  22. package/tests/engine/unit/identities/identities_builders.test.ts +13 -0
  23. package/tests/engine/unit/identities/identities_models.test.ts +3 -14
  24. package/tests/engine/unit/organization/models.test.ts +1 -1
  25. package/tests/engine/unit/segments/segments_model.test.ts +22 -1
  26. package/tests/engine/unit/utils.ts +1 -1
  27. package/tests/sdk/analytics.test.ts +11 -0
  28. package/tests/sdk/flagsmith-cache.test.ts +150 -0
  29. package/tests/sdk/flagsmith-environment-flags.test.ts +197 -0
  30. package/tests/sdk/flagsmith-identity-flags.test.ts +140 -0
  31. package/tests/sdk/flagsmith.test.ts +100 -85
  32. package/tests/sdk/polling.test.ts +25 -0
  33. package/tests/sdk/utils.ts +21 -2
  34. package/tsconfig.json +1 -1
  35. package/build/flagsmith-engine/environments/integrations/models.d.ts +0 -4
  36. package/build/flagsmith-engine/environments/integrations/models.js +0 -11
  37. package/build/flagsmith-engine/environments/models.d.ts +0 -25
  38. package/build/flagsmith-engine/environments/models.js +0 -29
  39. package/build/flagsmith-engine/environments/util.d.ts +0 -3
  40. package/build/flagsmith-engine/environments/util.js +0 -21
  41. package/build/flagsmith-engine/features/constants.d.ts +0 -4
  42. package/build/flagsmith-engine/features/constants.js +0 -7
  43. package/build/flagsmith-engine/features/models.d.ts +0 -32
  44. package/build/flagsmith-engine/features/models.js +0 -102
  45. package/build/flagsmith-engine/features/util.d.ts +0 -3
  46. package/build/flagsmith-engine/features/util.js +0 -20
  47. package/build/flagsmith-engine/identities/models.d.ts +0 -15
  48. package/build/flagsmith-engine/identities/models.js +0 -112
  49. package/build/flagsmith-engine/identities/traits/models.d.ts +0 -5
  50. package/build/flagsmith-engine/identities/traits/models.js +0 -11
  51. package/build/flagsmith-engine/identities/util.d.ts +0 -4
  52. package/build/flagsmith-engine/identities/util.js +0 -46
  53. package/build/flagsmith-engine/index.d.ts +0 -8
  54. package/build/flagsmith-engine/index.js +0 -113
  55. package/build/flagsmith-engine/organisations/models.d.ts +0 -9
  56. package/build/flagsmith-engine/organisations/models.js +0 -21
  57. package/build/flagsmith-engine/organisations/util.d.ts +0 -2
  58. package/build/flagsmith-engine/organisations/util.js +0 -8
  59. package/build/flagsmith-engine/projects/models.d.ts +0 -10
  60. package/build/flagsmith-engine/projects/models.js +0 -14
  61. package/build/flagsmith-engine/projects/util.d.ts +0 -2
  62. package/build/flagsmith-engine/projects/util.js +0 -15
  63. package/build/flagsmith-engine/segments/constants.d.ts +0 -26
  64. package/build/flagsmith-engine/segments/constants.js +0 -31
  65. package/build/flagsmith-engine/segments/evaluators.d.ts +0 -6
  66. package/build/flagsmith-engine/segments/evaluators.js +0 -37
  67. package/build/flagsmith-engine/segments/models.d.ts +0 -31
  68. package/build/flagsmith-engine/segments/models.js +0 -92
  69. package/build/flagsmith-engine/segments/util.d.ts +0 -4
  70. package/build/flagsmith-engine/segments/util.js +0 -25
  71. package/build/flagsmith-engine/utils/collections.d.ts +0 -4
  72. package/build/flagsmith-engine/utils/collections.js +0 -97
  73. package/build/flagsmith-engine/utils/errors.d.ts +0 -2
  74. package/build/flagsmith-engine/utils/errors.js +0 -26
  75. package/build/flagsmith-engine/utils/hashing/index.d.ts +0 -9
  76. package/build/flagsmith-engine/utils/hashing/index.js +0 -57
  77. package/build/flagsmith-engine/utils/index.d.ts +0 -1
  78. package/build/flagsmith-engine/utils/index.js +0 -14
  79. package/build/index.d.ts +0 -1
  80. package/build/index.js +0 -11
  81. package/build/sdk/analytics.d.ts +0 -28
  82. package/build/sdk/analytics.js +0 -103
  83. package/build/sdk/errors.d.ts +0 -4
  84. package/build/sdk/errors.js +0 -34
  85. package/build/sdk/index.d.ts +0 -118
  86. package/build/sdk/index.js +0 -386
  87. package/build/sdk/models.d.ts +0 -55
  88. package/build/sdk/models.js +0 -149
  89. package/build/sdk/polling_manager.d.ts +0 -9
  90. package/build/sdk/polling_manager.js +0 -73
  91. package/build/sdk/utils.d.ts +0 -12
  92. package/build/sdk/utils.js +0 -94
  93. package/tests/engine/engine-tests/engine-test-data/data/environment_n9fbf9h3v4fFgH3U3ngWhb.json +0 -12393
  94. package/tests/engine/engine-tests/engine-test-data/readme.md +0 -30
@@ -1,33 +1,41 @@
1
1
  name: Unit/Integration Tests
2
2
 
3
3
  on:
4
- pull_request:
5
- types:
6
- - opened
7
- - synchronize
8
- - reopened
9
- - ready_for_review
10
- push:
11
- branches:
12
- - main
4
+ pull_request:
5
+ types:
6
+ - opened
7
+ - synchronize
8
+ - reopened
9
+ - ready_for_review
10
+ push:
11
+ branches:
12
+ - main
13
13
  jobs:
14
- build-and-test:
15
- runs-on: ubuntu-latest
16
- steps:
17
- - uses: actions/checkout@v2
18
- - uses: actions/setup-node@v1
19
- with:
20
- node-version: '15.x'
21
- - name: cache node modules
22
- uses: actions/cache@v1
23
- with:
24
- path: ~/.npm # npm cache files are stored in `~/.npm` on Linux/macOS
25
- key: npm-${{ hashFiles('package-lock.json') }}
26
- restore-keys: |
27
- npm-${{ hashFiles('package-lock.json') }}
28
- npm-
29
- - run: npm i -g npm@7.0.2
30
- - run: npm install
31
- - run: npm test
32
- env:
33
- CI: true
14
+ build-and-test:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v2
18
+ - name: Checkout submodules # checkout rest
19
+ shell: bash
20
+ run: |
21
+ # If your submodules are configured to use SSH instead of HTTPS please uncomment the following line
22
+ git config --global url."https://github.com/".insteadOf "git@github.com:"
23
+ auth_header="$(git config --local --get http.https://github.com/.extraheader)"
24
+ git submodule sync --recursive
25
+ git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1
26
+ - uses: actions/setup-node@v1
27
+ with:
28
+ node-version: "15.x"
29
+ - name: cache node modules
30
+ uses: actions/cache@v1
31
+ with:
32
+ path: ~/.npm # npm cache files are stored in `~/.npm` on Linux/macOS
33
+ key: npm-${{ hashFiles('package-lock.json') }}
34
+ restore-keys: |
35
+ npm-${{ hashFiles('package-lock.json') }}
36
+ npm-
37
+ - run: npm i -g npm@7.0.2
38
+ - run: npm install
39
+ - run: npm test
40
+ env:
41
+ CI: true
package/README.md CHANGED
@@ -19,6 +19,13 @@ Please read [CONTRIBUTING.md](https://gist.github.com/kyle-ssg/c36a03aebe492e45c
19
19
 
20
20
  If you encounter a bug or feature request we would like to hear about it. Before you submit an issue please search existing issues in order to prevent duplicates.
21
21
 
22
+ ## Testing
23
+
24
+ To run the local tests you need to run following command beforehand:
25
+ ```
26
+ git submodule add git@github.com:Flagsmith/engine-test-data.git tests/engine/engine-tests/engine-test-data/
27
+ ```
28
+
22
29
  ## Get in touch
23
30
 
24
31
  If you have any questions about our projects you can email <a href="mailto:support@flagsmith.com">support@flagsmith.com</a>.
@@ -1,6 +1,6 @@
1
1
  const Router = require('express').Router;
2
2
  const Flagsmith = require('../../../build');
3
- const environmentKey = 'ser.dCxh8XoQc3Sw3JqMqMbhXz';
3
+ const environmentKey = '';
4
4
  if (!environmentKey) {
5
5
  throw new Error(
6
6
  'Please generate a Server Side SDK Key in environment settings to run the example'
@@ -51,6 +51,7 @@ export class FeatureStateModel {
51
51
  enabled: boolean;
52
52
  djangoID: number;
53
53
  featurestateUUID: string = uuidv4();
54
+ featureSegment?: FeatureSegment;
54
55
  private value: any;
55
56
  multivariateFeatureStateValues: MultivariateFeatureStateValueModel[] = [];
56
57
 
@@ -79,10 +80,23 @@ export class FeatureStateModel {
79
80
  return this.value;
80
81
  }
81
82
 
82
- get_feature_state_value() {
83
- return this.getValue();
83
+ /*
84
+ Returns `True` if `this` is higher segment priority than `other`
85
+ (i.e. has lower value for featureSegment.priority)
86
+ NOTE:
87
+ A segment will be considered higher priority only if:
88
+ 1. `other` does not have a feature segment(i.e: it is an environment feature state or it's a
89
+ feature state with feature segment but from an old document that does not have `featureSegment.priority`)
90
+ but `this` does.
91
+ 2. `other` have a feature segment with high priority
92
+ */
93
+ isHigherSegmentPriority(other: FeatureStateModel): boolean {
94
+ if (!other.featureSegment || !this.featureSegment) {
95
+ return !!this.featureSegment && !other.featureSegment;
96
+ }
97
+ return this.featureSegment.priority < other.featureSegment.priority;
84
98
  }
85
-
99
+
86
100
  getMultivariateValue(identityID: number | string) {
87
101
  const percentageValue = getHashedPercentateForObjIds([
88
102
  this.djangoID || this.featurestateUUID,
@@ -90,9 +104,9 @@ export class FeatureStateModel {
90
104
  ]);
91
105
 
92
106
  let startPercentage = 0;
93
- const sortedF = this.multivariateFeatureStateValues.sort((a, b) =>
94
- !!(a.id && b.id) ? a.id - b.id : a.mvFsValueUuid > b.mvFsValueUuid ? -1 : 1
95
- );
107
+ const sortedF = this.multivariateFeatureStateValues.sort((a, b) =>{
108
+ return a.id - b.id;
109
+ });
96
110
  for (const myValue of sortedF) {
97
111
  const limit = myValue.percentageAllocation + startPercentage;
98
112
  if (startPercentage <= percentageValue && percentageValue < limit) {
@@ -103,3 +117,11 @@ export class FeatureStateModel {
103
117
  return this.value;
104
118
  }
105
119
  }
120
+
121
+ export class FeatureSegment {
122
+ priority: number;
123
+
124
+ constructor(priority: number) {
125
+ this.priority = priority;
126
+ }
127
+ }
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  FeatureModel,
3
+ FeatureSegment,
3
4
  FeatureStateModel,
4
5
  MultivariateFeatureOptionModel,
5
6
  MultivariateFeatureStateValueModel
@@ -18,6 +19,10 @@ export function buildFeatureStateModel(featuresStateModelJSON: any): FeatureStat
18
19
  featuresStateModelJSON.uuid
19
20
  );
20
21
 
22
+ featureStateModel.featureSegment = featuresStateModelJSON.feature_segment ?
23
+ buildFeatureSegment(featuresStateModelJSON.feature_segment) :
24
+ undefined;
25
+
21
26
  const multivariateFeatureStateValues = featuresStateModelJSON.multivariate_feature_state_values
22
27
  ? featuresStateModelJSON.multivariate_feature_state_values.map((fsv: any) => {
23
28
  const featureOption = new MultivariateFeatureOptionModel(
@@ -36,3 +41,7 @@ export function buildFeatureStateModel(featuresStateModelJSON: any): FeatureStat
36
41
 
37
42
  return featureStateModel;
38
43
  }
44
+
45
+ export function buildFeatureSegment(featureSegmentJSON: any): FeatureSegment {
46
+ return new FeatureSegment(featureSegmentJSON.priority);
47
+ }
@@ -37,7 +37,7 @@ export class IdentityModel {
37
37
  return `${env_key}_${identifier}`;
38
38
  }
39
39
 
40
- update_traits(traits: TraitModel[]) {
40
+ updateTraits(traits: TraitModel[]) {
41
41
  const existingTraits: Map<string, TraitModel> = new Map();
42
42
  for (const trait of this.identityTraits) {
43
43
  existingTraits.set(trait.traitKey, trait);
@@ -25,15 +25,17 @@ function getIdentityFeatureStatesDict(
25
25
  );
26
26
  for (const matchingSegment of identitySegments) {
27
27
  for (const featureState of matchingSegment.featureStates) {
28
- // note that feature states are stored on the segment in descending priority
29
- // order so we only care that the last one is added
30
- // TODO: can we optimise this?
28
+ if (featureStates[featureState.feature.id]) {
29
+ if (featureStates[featureState.feature.id].isHigherSegmentPriority(featureState)) {
30
+ continue;
31
+ }
32
+ }
31
33
  featureStates[featureState.feature.id] = featureState;
32
34
  }
33
35
  }
34
36
 
35
37
  // Override with any feature states defined directly the identity
36
- for (const fs of identity.identityFeatures || []) {
38
+ for (const fs of identity.identityFeatures) {
37
39
  if (featureStates[fs.feature.id]) {
38
40
  featureStates[fs.feature.id] = fs;
39
41
  }
@@ -79,7 +81,7 @@ export function getEnvironmentFeatureState(environment: EnvironmentModel, featur
79
81
  const featuresStates = environment.featureStates.filter(f => f.feature.name === featureName);
80
82
 
81
83
  if (featuresStates.length === 0) {
82
- throw new Error('Feature State Not Found');
84
+ throw new FeatureStateNotFound('Feature State Not Found');
83
85
  }
84
86
 
85
87
  return featuresStates[0];
@@ -19,7 +19,7 @@ export class OrganisationModel {
19
19
  this.persistTraitData = persistTraitData;
20
20
  }
21
21
 
22
- get unique_slug() {
22
+ get uniqueSlug() {
23
23
  return this.id.toString() + '-' + this.name;
24
24
  }
25
25
  }
@@ -1,3 +1,5 @@
1
+ import semver from 'semver';
2
+
1
3
  import { FeatureStateModel } from '../features/models';
2
4
  import { getCastingFunction as getCastingFunction } from '../utils';
3
5
  import {
@@ -8,6 +10,7 @@ import {
8
10
  REGEX,
9
11
  CONDITION_OPERATORS
10
12
  } from './constants';
13
+ import { isSemver } from './util';
11
14
 
12
15
  export const all = (iterable: Array<any>) => iterable.filter(e => !!e).length === iterable.length;
13
16
  export const any = (iterable: Array<any>) => iterable.filter(e => !!e).length > 0;
@@ -23,10 +26,21 @@ export const matchingFunctions = {
23
26
  [CONDITION_OPERATORS.NOT_EQUAL]: (thisValue: any, otherValue: any) => thisValue != otherValue,
24
27
  [CONDITION_OPERATORS.CONTAINS]: (thisValue: any, otherValue: any) =>
25
28
  otherValue.includes(thisValue),
26
- [CONDITION_OPERATORS.NOT_CONTAINS]: (thisValue: any, otherValue: any) =>
27
- !otherValue.includes(thisValue)
28
29
  };
29
30
 
31
+ export const semverMatchingFunction = {
32
+ ...matchingFunctions,
33
+ [CONDITION_OPERATORS.EQUAL]: (thisValue: any, otherValue: any) => semver.eq(thisValue, otherValue),
34
+ [CONDITION_OPERATORS.GREATER_THAN]: (thisValue: any, otherValue: any) => semver.gt(otherValue, thisValue),
35
+ [CONDITION_OPERATORS.GREATER_THAN_INCLUSIVE]: (thisValue: any, otherValue: any) =>
36
+ semver.gte(otherValue, thisValue),
37
+ [CONDITION_OPERATORS.LESS_THAN]: (thisValue: any, otherValue: any) => semver.gt(thisValue, otherValue),
38
+ [CONDITION_OPERATORS.LESS_THAN_INCLUSIVE]: (thisValue: any, otherValue: any) =>
39
+ semver.gte(thisValue, otherValue),
40
+ }
41
+
42
+ export const getMatchingFunctions = (semver: boolean) => (semver ? semverMatchingFunction : matchingFunctions);
43
+
30
44
  export class SegmentConditionModel {
31
45
  EXCEPTION_OPERATOR_METHODS: { [key: string]: string } = {
32
46
  [NOT_CONTAINS]: 'evaluateNotContains',
@@ -61,9 +75,12 @@ export class SegmentConditionModel {
61
75
 
62
76
  const defaultFunction = (x: any, y: any) => false;
63
77
 
64
- const matchingFunction = matchingFunctions[this.operator] || defaultFunction;
78
+ const matchingFunctionSet = getMatchingFunctions(isSemver(this.value));
79
+ const matchingFunction = matchingFunctionSet[this.operator] || defaultFunction;
80
+
81
+ const traitType = isSemver(this.value) ? 'semver' : typeof traitValue;
82
+ const castToTypeOfTraitValue = getCastingFunction(traitType);
65
83
 
66
- const castToTypeOfTraitValue = getCastingFunction(traitValue);
67
84
  return matchingFunction(castToTypeOfTraitValue(this.value), traitValue);
68
85
  }
69
86
  }
@@ -27,3 +27,11 @@ export function buildSegmentModel(segmentModelJSON: any): SegmentModel {
27
27
 
28
28
  return model;
29
29
  }
30
+
31
+ export function isSemver(value: any) {
32
+ return typeof value == 'string' && value.endsWith(':semver');
33
+ }
34
+
35
+ export function removeSemverSuffix(value: string) {
36
+ return value.replace(':semver', '');
37
+ }
@@ -1,14 +1,3 @@
1
1
  import { FeatureStateModel } from '../features/models';
2
2
 
3
- export class IdentityFeaturesList extends Array<FeatureStateModel> {
4
- public push(...e: FeatureStateModel[]): number {
5
- for (const [_, item] of e.entries()) {
6
- for (const [k, v] of this.entries()) {
7
- if (v.djangoID === item.djangoID) {
8
- throw new Error('feature state for this feature already exists');
9
- }
10
- }
11
- }
12
- return super.push(...e);
13
- }
14
- }
3
+ export class IdentityFeaturesList extends Array<FeatureStateModel> {}
@@ -39,12 +39,15 @@ function h2d(s: any): string {
39
39
  * @returns number number between 0 (inclusive) and 100 (exclusive)
40
40
  */
41
41
  export function getHashedPercentateForObjIds(objectIds: Array<any>, iterations = 1): number {
42
- let to_hash = makeRepeated(objectIds, iterations).join(',');
43
- const hashedValue = md5(to_hash);
42
+ let toHash = makeRepeated(objectIds, iterations).join(',');
43
+ const hashedValue = md5(toHash);
44
44
  const hashedInt = bigInt(h2d(hashedValue));
45
45
  const value = (hashedInt.mod(9999).toJSNumber() / 9998) * 100;
46
46
 
47
+ // we ignore this for it's nearly impossible use case to catch
48
+ /* istanbul ignore next */
47
49
  if (value === 100) {
50
+ /* istanbul ignore next */
48
51
  return getHashedPercentateForObjIds(objectIds, iterations + 1);
49
52
  }
50
53
 
@@ -1,9 +1,13 @@
1
- export function getCastingFunction(input: any): CallableFunction {
2
- switch (typeof input) {
1
+ import { removeSemverSuffix } from "../segments/util";
2
+
3
+ export function getCastingFunction(traitType: 'boolean' | 'string' | 'number' | 'semver' | any): CallableFunction {
4
+ switch (traitType) {
3
5
  case 'boolean':
4
6
  return (x: any) => !['False', 'false'].includes(x);
5
7
  case 'number':
6
8
  return (x: any) => parseFloat(x);
9
+ case 'semver':
10
+ return (x: any) => removeSemverSuffix(x);
7
11
  default:
8
12
  return (x: any) => String(x);
9
13
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flagsmith-nodejs",
3
- "version": "2.0.0-beta.6",
3
+ "version": "2.0.1",
4
4
  "description": "Flagsmith lets you manage features flags and remote config across web, mobile and server side applications. Deliver true Continuous Integration. Get builds out faster. Control who has access to new features.",
5
5
  "main": "build/index.js",
6
6
  "repository": {
@@ -46,19 +46,21 @@
46
46
  "test:watch": "jest --coverage --watch --coverageReporters='text'",
47
47
  "test:debug": "node --inspect-brk node_modules/.bin/jest --coverage --watch --coverageReporters='text'",
48
48
  "build": "tsc",
49
- "prepublish": "npm run build",
49
+ "prepublish": "npm i && npm run build",
50
50
  "prepare": "husky install"
51
51
  },
52
52
  "dependencies": {
53
53
  "big-integer": "^1.6.51",
54
54
  "md5": "^2.3.0",
55
55
  "node-fetch": "^2.1.2",
56
+ "semver": "^7.3.7",
56
57
  "uuid": "^8.3.2"
57
58
  },
58
59
  "devDependencies": {
59
60
  "@types/jest": "^27.4.1",
60
61
  "@types/md5": "^2.3.2",
61
62
  "@types/node-fetch": "^2.6.1",
63
+ "@types/semver": "^7.3.9",
62
64
  "@types/uuid": "^8.3.4",
63
65
  "esbuild": "^0.14.25",
64
66
  "husky": "^7.0.4",
package/sdk/analytics.ts CHANGED
@@ -5,8 +5,6 @@ const ANALYTICS_ENDPOINT = 'analytics/flags/';
5
5
  // Used to control how often we send data(in seconds)
6
6
  const ANALYTICS_TIMER = 10;
7
7
 
8
- const delay = (ms: number) => new Promise(resolve => setTimeout(() => resolve(undefined), ms));
9
-
10
8
  export class AnalyticsProcessor {
11
9
  private analyticsEndpoint: string;
12
10
  private environmentKey: string;
package/sdk/index.ts CHANGED
@@ -12,8 +12,9 @@ import { EnvironmentDataPollingManager } from './polling_manager';
12
12
  import { generateIdentitiesData, retryFetch } from './utils';
13
13
  import { SegmentModel } from '../flagsmith-engine/segments/models';
14
14
  import { getIdentitySegments } from '../flagsmith-engine/segments/evaluators';
15
+ import { FlagsmithCache } from './types';
15
16
 
16
- const DEFAULT_API_URL = 'https://api.flagsmith.com/api/v1/';
17
+ const DEFAULT_API_URL = 'https://edge.api.flagsmith.com/api/v1/';
17
18
 
18
19
  export class Flagsmith {
19
20
  environmentKey?: string;
@@ -26,12 +27,16 @@ export class Flagsmith {
26
27
  enableAnalytics: boolean = false;
27
28
  defaultFlagHandler?: (featureName: string) => DefaultFlag;
28
29
 
30
+
29
31
  environmentFlagsUrl: string;
30
32
  identitiesUrl: string;
31
33
  environmentUrl: string;
32
34
 
33
35
  environmentDataPollingManager?: EnvironmentDataPollingManager;
34
36
  environment!: EnvironmentModel;
37
+
38
+ private cache?: FlagsmithCache;
39
+ private onEnvironmentChange?: (error: Error | null, result: EnvironmentModel) => void;
35
40
  private analyticsProcessor?: AnalyticsProcessor;
36
41
  /**
37
42
  * A Flagsmith client.
@@ -73,6 +78,8 @@ export class Flagsmith {
73
78
  retries?: number;
74
79
  enableAnalytics?: boolean;
75
80
  defaultFlagHandler?: (featureName: string) => DefaultFlag;
81
+ cache?: FlagsmithCache,
82
+ onEnvironmentChange?: (error: Error | null, result: EnvironmentModel) => void,
76
83
  }) {
77
84
  this.environmentKey = data.environmentKey;
78
85
  this.apiUrl = data.apiUrl || this.apiUrl;
@@ -88,6 +95,20 @@ export class Flagsmith {
88
95
  this.environmentFlagsUrl = `${this.apiUrl}flags/`;
89
96
  this.identitiesUrl = `${this.apiUrl}identities/`;
90
97
  this.environmentUrl = `${this.apiUrl}environment-document/`;
98
+ this.onEnvironmentChange = data.onEnvironmentChange;
99
+
100
+ if (!!data.cache) {
101
+ const missingMethods: string[] = ['has', 'get', 'set'].filter(method => data.cache && !data.cache[method]);
102
+
103
+ if (missingMethods.length > 0) {
104
+ throw new Error(
105
+ `Please implement the following methods in your cache: ${missingMethods.join(
106
+ ', '
107
+ )}`
108
+ );
109
+ }
110
+ this.cache = data.cache;
111
+ }
91
112
 
92
113
  if (this.enableLocalEvaluation) {
93
114
  if (!this.environmentKey.startsWith('ser.')) {
@@ -105,10 +126,10 @@ export class Flagsmith {
105
126
 
106
127
  this.analyticsProcessor = data.enableAnalytics
107
128
  ? new AnalyticsProcessor({
108
- environmentKey: this.environmentKey,
109
- baseApiUrl: this.apiUrl,
110
- timeout: this.requestTimeoutSeconds
111
- })
129
+ environmentKey: this.environmentKey,
130
+ baseApiUrl: this.apiUrl,
131
+ timeout: this.requestTimeoutSeconds
132
+ })
112
133
  : undefined;
113
134
  }
114
135
  /**
@@ -117,8 +138,19 @@ export class Flagsmith {
117
138
  * @returns Flags object holding all the flags for the current environment.
118
139
  */
119
140
  async getEnvironmentFlags(): Promise<Flags> {
141
+ const cachedItem = !!this.cache && await this.cache.get(`flags`);
142
+ if (!!cachedItem) {
143
+ return cachedItem;
144
+ }
145
+ if (this.enableLocalEvaluation) {
146
+ return new Promise(resolve =>
147
+ this.environmentPromise!.then(() => {
148
+ resolve(this.getEnvironmentFlagsFromDocument());
149
+ })
150
+ );
151
+ }
120
152
  if (this.environment) {
121
- return new Promise(resolve => resolve(this.getEnvironmentFlagsFromDocument()));
153
+ return this.getEnvironmentFlagsFromDocument();
122
154
  }
123
155
 
124
156
  return this.getEnvironmentFlagsFromApi();
@@ -134,7 +166,11 @@ export class Flagsmith {
134
166
  Flagsmith, e.g. {"num_orders": 10}
135
167
  * @returns Flags object holding all the flags for the given identity.
136
168
  */
137
- getIdentityFlags(identifier: string, traits?: { [key: string]: any }): Promise<Flags> {
169
+ async getIdentityFlags(identifier: string, traits?: { [key: string]: any }): Promise<Flags> {
170
+ const cachedItem = !!this.cache && await this.cache.get(`flags-${identifier}`);
171
+ if (!!cachedItem) {
172
+ return cachedItem;
173
+ }
138
174
  traits = traits || {};
139
175
  if (this.enableLocalEvaluation) {
140
176
  return new Promise(resolve =>
@@ -188,14 +224,23 @@ export class Flagsmith {
188
224
  * You only need to call this if you wish to bypass environmentRefreshIntervalSeconds.
189
225
  */
190
226
  async updateEnvironment() {
191
- const request = this.getEnvironmentFromApi();
192
- if (!this.environmentPromise) {
193
- this.environmentPromise = request.then(res => {
194
- this.environment = res;
195
- });
196
- await this.environmentPromise;
197
- } else {
198
- this.environment = await request;
227
+ try {
228
+ const request = this.getEnvironmentFromApi();
229
+ if (!this.environmentPromise) {
230
+ this.environmentPromise = request.then(res => {
231
+ this.environment = res;
232
+ });
233
+ await this.environmentPromise;
234
+ } else {
235
+ this.environment = await request;
236
+ }
237
+ if (this.onEnvironmentChange) {
238
+ this.onEnvironmentChange(null, this.environment);
239
+ }
240
+ } catch (e) {
241
+ if (this.onEnvironmentChange) {
242
+ this.onEnvironmentChange(e as Error, this.environment);
243
+ }
199
244
  }
200
245
  }
201
246
 
@@ -224,7 +269,6 @@ export class Flagsmith {
224
269
  headers: headers
225
270
  },
226
271
  this.retries,
227
- 1000,
228
272
  (this.requestTimeoutSeconds || 10) * 1000
229
273
  );
230
274
 
@@ -247,15 +291,19 @@ export class Flagsmith {
247
291
  return buildEnvironmentModel(environment_data);
248
292
  }
249
293
 
250
- private getEnvironmentFlagsFromDocument() {
251
- return Flags.fromFeatureStateModels({
294
+ private async getEnvironmentFlagsFromDocument(): Promise<Flags> {
295
+ const flags = Flags.fromFeatureStateModels({
252
296
  featureStates: getEnvironmentFeatureStates(this.environment),
253
297
  analyticsProcessor: this.analyticsProcessor,
254
298
  defaultFlagHandler: this.defaultFlagHandler
255
299
  });
300
+ if (!!this.cache) {
301
+ await this.cache.set('flags', flags);
302
+ }
303
+ return flags;
256
304
  }
257
305
 
258
- private getIdentityFlagsFromDocument(identifier: string, traits: { [key: string]: any }) {
306
+ private async getIdentityFlagsFromDocument(identifier: string, traits: { [key: string]: any }): Promise<Flags> {
259
307
  const identityModel = this.buildIdentityModel(
260
308
  identifier,
261
309
  Object.keys(traits).map(key => ({
@@ -266,21 +314,31 @@ export class Flagsmith {
266
314
 
267
315
  const featureStates = getIdentityFeatureStates(this.environment, identityModel);
268
316
 
269
- return Flags.fromFeatureStateModels({
317
+ const flags = Flags.fromFeatureStateModels({
270
318
  featureStates: featureStates,
271
319
  analyticsProcessor: this.analyticsProcessor,
272
320
  defaultFlagHandler: this.defaultFlagHandler
273
321
  });
322
+
323
+ if (!!this.cache) {
324
+ await this.cache.set(`flags-${identifier}`, flags);
325
+ }
326
+
327
+ return flags;
274
328
  }
275
329
 
276
330
  private async getEnvironmentFlagsFromApi() {
277
331
  try {
278
332
  const apiFlags = await this.getJSONResponse(this.environmentFlagsUrl, 'GET');
279
- return Flags.fromAPIFlags({
333
+ const flags = Flags.fromAPIFlags({
280
334
  apiFlags: apiFlags,
281
335
  analyticsProcessor: this.analyticsProcessor,
282
336
  defaultFlagHandler: this.defaultFlagHandler
283
337
  });
338
+ if (!!this.cache) {
339
+ await this.cache.set('flags', flags);
340
+ }
341
+ return flags;
284
342
  } catch (e) {
285
343
  if (this.defaultFlagHandler) {
286
344
  return new Flags({
@@ -297,11 +355,15 @@ export class Flagsmith {
297
355
  try {
298
356
  const data = generateIdentitiesData(identifier, traits);
299
357
  const jsonResponse = await this.getJSONResponse(this.identitiesUrl, 'POST', data);
300
- return Flags.fromAPIFlags({
358
+ const flags = Flags.fromAPIFlags({
301
359
  apiFlags: jsonResponse['flags'],
302
360
  analyticsProcessor: this.analyticsProcessor,
303
361
  defaultFlagHandler: this.defaultFlagHandler
304
362
  });
363
+ if (!!this.cache) {
364
+ await this.cache.set(`flags-${identifier}`, flags);
365
+ }
366
+ return flags;
305
367
  } catch (e) {
306
368
  if (this.defaultFlagHandler) {
307
369
  return new Flags({
@@ -315,12 +377,6 @@ export class Flagsmith {
315
377
  }
316
378
 
317
379
  private buildIdentityModel(identifier: string, traits: { key: string; value: any }[]) {
318
- if (!this.environment) {
319
- throw new FlagsmithClientError(
320
- 'Unable to build identity model when no local environment present.'
321
- );
322
- }
323
-
324
380
  const traitModels = traits.map(trait => new TraitModel(trait.key, trait.value));
325
381
  return new IdentityModel('0', traitModels, [], this.environment.apiKey, identifier);
326
382
  }
@@ -17,7 +17,6 @@ export class EnvironmentDataPollingManager {
17
17
  await this.main.updateEnvironment();
18
18
  }, this.refreshIntervalSeconds * 1000);
19
19
  };
20
- // todo: this call should be awaited for getIdentityFlags/getEnvironmentFlags when enableLocalEvaluation is true
21
20
  this.main.updateEnvironment();
22
21
  updateEnvironment();
23
22
  }
package/sdk/types.ts ADDED
@@ -0,0 +1,8 @@
1
+ import { Flags } from "./models";
2
+
3
+ export interface FlagsmithCache {
4
+ get(key: string): Promise<Flags>;
5
+ set(key: string, value: Flags): Promise<void>;
6
+ has(key: string): Promise<boolean>;
7
+ [key: string]: any;
8
+ }